Complete Swap Flow

We’ve created some demonstration integration code in NodeJS to show how you can replicate Clipper quotes locally and send the transaction using Clipper RFQ API. The code has demonstration solvers for both the closed form and root finding modalities.

Add package.json file with this code:

{
  "name": "clipper",
  "version": "1.0.0",
  "main": "main.js",
  "license": "MIT",
  "dependencies": {
    "bignumber.js": "^9.0.2",
    "node-fetch": "^3.2.0",
    "root-finding": "^1.0.2",
    "web3": "^1.7.0"
  },
  "type": "module"
}

Add main.js file with this code

import fetch from 'node-fetch'
import BN from 'bignumber.js'
import nr from 'root-finding'
import Web3 from 'web3'
import fs from 'fs'
import ethers from "ethers";
import exchangeAbi from "./exchangeAbi.json";

const nodeUrl = process.env.NODE_URL

const rawdata = fs.readFileSync('ERC20.abi.json');
const ERC20ABI = JSON.parse(rawdata);
const authorizationHeader = 'Basic YXBpLXVzZXI6dXNlci1wYXNz';  // This is a placeholder 

const web3 = new Web3(nodeUrl)


async function main() {
    // 1. GET POOL INFO AND ESTIMATE CLIPPER PRICES
    const prices = await fetch('https://api.clipper.exchange/rfq/pool?chain_id=137&fieldset=offchain-data', {
        method: 'GET',
        headers: {'Authorization': authorizationHeader}
    }).then((x) => x.json());

    const poolAddress = prices.pool.address

    const asset_one = 'MATIC'
    const asset_two = 'USDC'
    const decimalsX = 18
    const decimalsY = 6
    const humanInX = 10000


    let fee_in_basis_points = 0
    for (const pair of prices.pairs) {
        if(((pair.assets[0] === asset_one) && (pair.assets[1] === asset_two)) ||
           ((pair.assets[1] === asset_one) && (pair.assets[0] === asset_two))){
            fee_in_basis_points = pair.fee_in_basis_points
            break
        }
    }

    const assetOneObj = prices.assets.find((x) => x.name === asset_one)
    const assetTwoObj = prices.assets.find((x) => x.name === asset_two)

    const assetOneContract = new web3.eth.Contract(ERC20ABI, assetOneObj.address)
    const assetTwoContract = new web3.eth.Contract(ERC20ABI, assetTwoObj.address)

    const assetOneBalance = await assetOneContract.methods.balanceOf(poolAddress).call()
    const assetTwoBalance = await assetTwoContract.methods.balanceOf(poolAddress).call()

    const M = (10000-fee_in_basis_points)/10000

    const scaledDecimalsX = new BN(10).exponentiatedBy(decimalsX)
    const scaledDecimalsY = new BN(10).exponentiatedBy(decimalsY)
    
    const adjustedInX = humanInX*M
    const inX = new BN(humanInX).multipliedBy(scaledDecimalsX)

    // Pull in additional onchain state. This is from a snapshot of USDC and USDT, respectively.
    const qX = new BN(assetOneBalance).dividedBy(scaledDecimalsX).toNumber()
    const qY = new BN(assetTwoBalance).dividedBy(scaledDecimalsY).toNumber()
    // Pull additional offchain state
    const k = prices.pool.k
    // format variables according to formula
    const pX = assetOneObj.price_in_usd
    const wX = assetOneObj.listing_weight

    const pY = assetTwoObj.price_in_usd
    const wY = assetTwoObj.listing_weight

    const initialGuess = (adjustedInX*pX)/pY

    const initialUtility = Math.pow(pX*qX, 1-k)/Math.pow(wX, k) + Math.pow(pY*qY, 1-k)/Math.pow(wY, k) 
    const newXUtility =  Math.pow(pX*(qX+adjustedInX), 1-k)/Math.pow(wX, k)

    let outY;

    // CLOSED FORM SOLUTION
    const outYClosedForm = qY-Math.pow((initialUtility - newXUtility)*Math.pow(wY,k), (1/(1-k)))/pY

    // ROOT FINDING SOLUTION
    function newYUtility(outY) {
        return Math.pow(pY*(qY-outY), 1-k)/Math.pow(wY, k) 
    }

    function zeroMe(outY) {
        return initialUtility - newXUtility - newYUtility(outY)
    }
    // nr.newtonRaphson(guess, increment, iteration, eps, f);
    // These increment, iteration, and eps values may need to be changed
    const outYRootFinding = nr.newtonRaphson(initialGuess, 1e-6, 50, 1e-8, zeroMe)
    console.log('Closed Form: ', outYClosedForm);
    console.log('Root Finding: ', outYRootFinding);
    outY = outYClosedForm;
    // Adjust for FMV restriction, input >= output (without respect to fees!)
    outY = Math.min(outY, (humanInX*pX)/pY).toFixed(decimalsY)

    // 2. CREATE A QUOTE
    const quote = await fetch('https://api.clipper.exchange/rfq/quote', {
        method: 'POST',
        body: JSON.stringify({
            "input_asset_symbol": asset_one,
            "output_asset_symbol": asset_two,
            "chain_id": 137,
            "input_amount": inX.toFixed(),
            "time_in_seconds": prices.pool.default_time_in_seconds
        }),
        headers: {
            'Content-Type': 'application/json',
            'Authorization': authorizationHeader
        }
    })
        .then((x) => x.json())

    console.log({
        fromFormula: outY,
        fromQuote: new BN(quote.output_amount).dividedBy(scaledDecimalsY).toFixed(decimalsY)
    })
    
    // 3. SIGN THE QUOTE
    const sign = await fetch('https://api.clipper.exchange/rfq/quote', {
        method: 'POST',
        body: JSON.stringify({
            quote_id: quote.id,  // Use the quote_id from the previous /quote call
            destination_address: "0x5901920A7b8cb1Bba39220FAC138Ffb3800dD212",
            sender_address: "0x5901920A7b8cb1Bba39220FAC138Ffb3800dD212", // Optional: it is for DEX aggregator partners that are using their own smart contract
        }),
        headers: {
            'Content-Type': 'application/json',
            'Authorization': authorizationHeader
        }
    }).then((x) => x.json());
    
    // 4. SEND THE TRANSACTION
    const provider = new ethers.providers.JsonRpcProvider(process.env.RPC_URL);
    const exchangeContract = new ethers.Contract(
        sign.clipper_exchange_address,
        exchangeAbi,
        provider
    );
    
    // Any 32 bytes identifier
    const auxData = "0x00000000000000000000000000000000000000000000000000";

    const result = await exchangeContract.swap(
        sign.input_asset_address,
        sign.output_asset_address,
        sign.input_amount,
        sign.output_amount,
        sign.good_until,
        sign.destination_address,
        [sign.signature.v, sign.signature.r, sign.signature.s],
        auxData
    );
    console.log("Swap Result:", result);
}
main()

Add ERC20.abi.json

Add exchangeAbi.json

Install packages

npm install

Run the code

NODE_URL="https://polygon-rpc.com/" node main.js

Extra notes of the code

The code compares the output from a root-finding implementation in our code (”fromFormula”) to the output from the Clipper servers (”fromQuote”) on Polygon. Note that sometimes these values may diverge slightly because of Polygon block updates.

You can change the tokens and amounts by changing these values in main.js

const asset_one = 'USDT'
const asset_two = 'USDC'
const decimalsX = 6
const decimalsY = 6
const humanInX = 100

The asset_ values are token names, decimals values should be the decimals for the first and second tokens, respectively, and humanInX should be the sold amount of asset_one in human terms (i.e., 1000 USDC ≈ $1,000 rather than “1000000000” USDC).

Last updated

#221: Integrating with Clipper RFQ

Change request updated