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.
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";constnodeUrl=process.env.NODE_URLconstrawdata=fs.readFileSync('ERC20.abi.json');constERC20ABI=JSON.parse(rawdata);constauthorizationHeader='Basic YXBpLXVzZXI6dXNlci1wYXNz'; // This is a placeholder constweb3=newWeb3(nodeUrl)asyncfunctionmain() {// 1. GET POOL INFO AND ESTIMATE CLIPPER PRICESconstprices=awaitfetch('https://api.clipper.exchange/rfq/pool?chain_id=137&fieldset=offchain-data', { method:'GET', headers: {'Authorization': authorizationHeader} }).then((x) =>x.json());constpoolAddress=prices.pool.addressconstasset_one='MATIC'constasset_two='USDC'constdecimalsX=18constdecimalsY=6consthumanInX=10000let fee_in_basis_points =0for (constpairofprices.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_pointsbreak } }constassetOneObj=prices.assets.find((x) =>x.name === asset_one)constassetTwoObj=prices.assets.find((x) =>x.name === asset_two)constassetOneContract=newweb3.eth.Contract(ERC20ABI,assetOneObj.address)constassetTwoContract=newweb3.eth.Contract(ERC20ABI,assetTwoObj.address)constassetOneBalance=awaitassetOneContract.methods.balanceOf(poolAddress).call()constassetTwoBalance=awaitassetTwoContract.methods.balanceOf(poolAddress).call()constM= (10000-fee_in_basis_points)/10000constscaledDecimalsX=newBN(10).exponentiatedBy(decimalsX)constscaledDecimalsY=newBN(10).exponentiatedBy(decimalsY)constadjustedInX= humanInX*MconstinX=newBN(humanInX).multipliedBy(scaledDecimalsX)// Pull in additional onchain state. This is from a snapshot of USDC and USDT, respectively.constqX=newBN(assetOneBalance).dividedBy(scaledDecimalsX).toNumber()constqY=newBN(assetTwoBalance).dividedBy(scaledDecimalsY).toNumber()// Pull additional offchain stateconstk=prices.pool.k// format variables according to formulaconstpX=assetOneObj.price_in_usdconstwX=assetOneObj.listing_weightconstpY=assetTwoObj.price_in_usdconstwY=assetTwoObj.listing_weightconstinitialGuess= (adjustedInX*pX)/pYconstinitialUtility=Math.pow(pX*qX,1-k)/Math.pow(wX, k) +Math.pow(pY*qY,1-k)/Math.pow(wY, k) constnewXUtility=Math.pow(pX*(qX+adjustedInX),1-k)/Math.pow(wX, k)let outY;// CLOSED FORM SOLUTIONconstoutYClosedForm= qY-Math.pow((initialUtility - newXUtility)*Math.pow(wY,k), (1/(1-k)))/pY// ROOT FINDING SOLUTIONfunctionnewYUtility(outY) {returnMath.pow(pY*(qY-outY),1-k)/Math.pow(wY, k) }functionzeroMe(outY) {return initialUtility - newXUtility -newYUtility(outY) }// nr.newtonRaphson(guess, increment, iteration, eps, f);// These increment, iteration, and eps values may need to be changedconstoutYRootFinding=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 QUOTEconstquote=awaitfetch('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:newBN(quote.output_amount).dividedBy(scaledDecimalsY).toFixed(decimalsY) })// 3. SIGN THE QUOTEconstsign=awaitfetch('https://api.clipper.exchange/rfq/sign', { 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 TRANSACTIONconstprovider=newethers.providers.JsonRpcProvider(process.env.RPC_URL);constexchangeContract=newethers.Contract(sign.clipper_exchange_address, exchangeAbi, provider );// Any 32 bytes identifierconstauxData="0x00000000000000000000000000000000000000000000000000";constresult=awaitexchangeContract.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
npminstall
Run the code
NODE_URL="https://polygon-rpc.com/"nodemain.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
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).