Receiving an RFQ

Whenever a taker makes an RFQ, the Upshot servers determine the best way to route that RFQ among the existing makers. The winning maker(s) receive messages of the following type:

{
  "messageType": "rfqT";
  "message": {
  "protocols": ["pool"],
    "data": {
      "rfqId": string;
      "source": string;
      "nonce": number;
      "baseToken": string;
      "quoteToken": string;
      "trader": string;
      "effectiveTrader": string;
      "baseTokenAmount"?: number;
      "quoteTokenAmount"?: number;
      "baseChain": {
        "chainType": string;
        "chainId": number;
      };
      "quoteChain": {
        "chainType": string;
        "chainId": number;
      };
     "feesBps": number;
     "pool": {}, // This data is for signing EIP712, but you can ignore it.
    }
  }
}

Exactly one of baseTokenAmount / quoteTokenAmount will be present in the RFQ.

If baseTokenAmount is present, then the rfqTQuote will need to fill the quoteTokenAmount. This is the equivalent of a request asking "How much USDC can I get if I pay 1 ETH?".

If quoteTokenAmount is present, then the rfqTQuote will have to fill the baseTokenAmount. This is the equivalent of a request asking "How much ETH do I have to pay to get 100 USDC?".

The generated quote should have baseTokenAmount / quoteTokenAmount fields that respect the price level values sent. The price levels are used in deciding which makers to route the RFQs to. Therefore, the UpShot servers will check the quote values for correctness against Price Levels and penalize makers that do not respect the prices sent in advance.

Example: Receiving the RFQ, Signing EIP712 Data, and Sending Back RFQQuote

The following code snippet demonstrates how to receive a Request for Quote (RFQ) via the socket, sign the EIP712 data, and then send back the signed RFQQuote. You will need to fill in the fulfilledAmount, poolAddress, and externalAccount based on your specific setup.

const { ethers } = require("ethers");
const WebSocket = require("ws");

const PRIVATE_KEY = "YOUR_PRIVATE_KEY_HERE";
const wallet = new ethers.Wallet(PRIVATE_KEY);
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

const ws = new WebSocket('ws://localhost:8080');

function fillMissingFields(request, fulfilledAmount, poolAddress, externalAccount) {
  request.message.data.pool.eip712Data.message.quoteTokenAmount = fulfilledAmount.toString();
  request.message.data.pool.eip712Data.message.externalAccount = externalAccount;
  request.message.data.pool.eip712Data.domain.verifyingContract = poolAddress;
}

async function signEIP712(request) {
  const domain = request.message.data.pool.eip712Data.domain;
  const types = request.message.data.pool.eip712Data.types;
  const message = request.message.data.pool.eip712Data.message;

  const signature = await wallet._signTypedData(domain, types, message);
  return signature;
}

async function processSignatureRequest(request) {
  try {
    const fulfilledAmount = ""
    const poolAddress = ""
    let externalAccount = ""

    externalAccount = (externalAccount === poolAddress) ? ZERO_ADDRESS : externalAccount;
    fillMissingFields(request, fulfilledAmount, poolAddress, externalAccount)
    const signature = await signEIP712(request, fulfilledAmount, poolAddress, externalAccount);
    
    const baseTokenAmount = request.message.data.baseTokenAmount || fulfilledAmount.toString();
    const quoteTokenAmount = request.message.data.quoteTokenAmount || fulfilledAmount.toString();

    return {
      messageType: "rfqTQuote",
      message: {
        protocols: request.message.protocols,
        data: {
          rfqId: request.message.data.rfqId,
          source: request.message.data.source,
          nonce: request.message.data.nonce,
          quoteExpiry: request.message.data.pool.eip712Data.message.quoteExpiry,
          baseToken: request.message.data.baseToken,
          quoteToken: request.message.data.quoteToken,
          trader: request.message.data.trader,
          poolAddress: poolAddress,
          effectiveTrader: request.message.data.effectiveTrader,
          baseTokenAmount: baseTokenAmount,
          quoteTokenAmount: quoteTokenAmount,
          externalAccount: externalAccount,
          baseChain: request.message.data.baseChain,
          quoteChain: request.message.data.quoteChain,
          feesBps: request.message.data.feesBps,
          signatures: [{ signature }],
        },
      },
    };
  } catch (error) {
    return {
      success: false,
      message: error.message,
    };
  }
}

ws.on('open', () => {
  console.log('Connected to server');

  const token = 'your_token_here';
  ws.send(JSON.stringify({ token }));
});

ws.on("message", async (message) => {
  const request = JSON.parse(message);
  if (request.messageType === "request-signature") {
    const response = await processSignatureRequest(request);
    ws.send(JSON.stringify(response));
  }
});

The fulfilledAmount is the amount that the pool will fulfill in response to the RFQ. It represents the quantity of the asset being traded, expressed in its smallest unit. For example, if the asset has 6 decimal places and you want to fulfill an amount of 1, the fulfilledAmount should be set to 1000000.

The poolAddress is obtained from the pool creation step. It is used as the verifyingContract in the EIP712 domain structure, ensuring that the signature is tied to this specific pool.

The externalAccount represents an external account that may be involved in the transaction, such as a wallet address. If you have added tokens to the pool, you can simply use externalAccount = poolAddress. Otherwise, you can replace externalAccount with the appropriate address based on your specific setup.

Last updated