diff --git a/.env.example b/.env.example index 6bea24a..5582c8c 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ ONEINCH_API_KEY="" -ZEROEX_API_KEY="" \ No newline at end of file +ZEROEX_API_KEY="" +DEFILLAMA_API_KEY="" \ No newline at end of file diff --git a/README.md b/README.md index 3711bcd..99624aa 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,12 @@ Then you need to copy .env.example file to .env and set your API key there. ONEINCH_API_KEY=YOUR_API_KEY_FROM_1INCH ``` +For trading with the DefiLlama API you neeed to set: + +``` +DEFILLAMA_API_KEY=YOUR_API_KEY_FROM_DEFILLAMA +``` + Initialize the sdk with an [ethers wallet](https://docs.ethers.io/v5/api/signer/#Wallet) and the network. ```ts @@ -216,6 +222,22 @@ const tx = await pool.trade( ) ``` +#### 11. Trade pool assets with the DefiLlama API + +Trade 1 USDC into DAI either on 1Inch or 0x, it will be executed on the protocol with the best price + +```ts +const amountIn = "1000000" +const slippage = 0.5 +const tx = await pool.tradeDefiLlama( + null, + "USDC_TOKEN_ADDRESS", + "DAI_TOKEN_ADDRESS", + amountIn, + slippage +) +``` + ### Liquidity --- diff --git a/package.json b/package.json index 853f8a2..01bf3bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dhedge/v2-sdk", - "version": "1.10.4", + "version": "1.10.5", "license": "MIT", "description": "🛠 An SDK for building applications on top of dHEDGE V2", "main": "dist/index.js", diff --git a/src/config.ts b/src/config.ts index dd3793a..0fa45b7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -169,3 +169,8 @@ export const flatMoneyContractAddresses: Readonly>> = { + [Dapp.ONEINCH]: "1inch", + [Dapp.ZEROEX]: "Matcha/0x" +}; diff --git a/src/entities/pool.ts b/src/entities/pool.ts index d1789da..a431ddd 100644 --- a/src/entities/pool.ts +++ b/src/entities/pool.ts @@ -76,6 +76,7 @@ import { getCompoundV3LendTxData, getCompoundV3WithdrawTxData } from "../services/compound/lending"; +import { getDefiLlamaTxData } from "../services/defiLlama"; export class Pool { public readonly poolLogic: Contract; @@ -437,6 +438,43 @@ export class Pool { return tx; } + /** + * Uses the DefiLlama API to trade an asset into another asset + * @param {Dapp | null} dapp Protocol to use for trading, if null, the protocol with the best price will be used + * @param {string} assetFrom Asset to trade from + * @param {string} assetTo Asset to trade into + * @param {BigNumber | string} amountIn Amount + * @param {number} slippage Slippage tolerance in % + * @param {any} options Transaction options + * @param {boolean} estimateGas Simulate/estimate gas + * @returns {Promise} Transaction + */ + async tradeDefiLlama( + dapp: Dapp | null, + assetFrom: string, + assetTo: string, + amountIn: BigNumber | string, + slippage = 0.5, + options: any = null, + estimateGas = false + ): Promise { + const { txData, protocolAddress } = await getDefiLlamaTxData( + this, + dapp, + assetFrom, + assetTo, + amountIn, + slippage + ); + + const tx = await getPoolTxOrGasEstimate( + this, + [protocolAddress, txData, options], + estimateGas + ); + return tx; + } + /** * Add liquidity to a liquidity pool * @param {Dapp} dapp Platform like Sushiswap or Uniswap diff --git a/src/services/defiLlama/index.ts b/src/services/defiLlama/index.ts new file mode 100644 index 0000000..e74ec7a --- /dev/null +++ b/src/services/defiLlama/index.ts @@ -0,0 +1,108 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import axios from "axios"; +import { ApiError, Dapp, ethers } from "../.."; + +import { Pool } from "../../entities"; +import BigNumber from "bignumber.js"; +import { dappDefiLlamaMap } from "../../config"; + +export type DefiLlamaResult = { + amountReturned: BigNumber; + protocolAddress: string; + txData: string; +}; + +export async function getDefiLlamaSwapResult( + pool: Pool, + protocol: "1inch" | "Matcha/0x", + assetFrom: string, + assetTo: string, + amountIn: ethers.BigNumber | string, + slippage: number +): Promise { + if (!process.env.DEFILLAMA_API_KEY) + throw new Error("DEFILLAMA_API_KEY not configured in .env file"); + + const apiUrl = "https://swap-api.defillama.com/dexAggregatorQuote"; + const params = { + from: assetFrom, + to: assetTo, + amount: amountIn.toString(), + chain: pool.network, + api_key: process.env.DEFILLAMA_API_KEY, + protocol + }; + const [fromDecimals, toDecimals] = await Promise.all( + [assetFrom, assetTo].map(async asset => pool.utils.getDecimals(asset)) + ); + const body = { + userAddress: pool.address, + slippage: slippage, + fromToken: { + decimals: fromDecimals + }, + toToken: { + decimals: toDecimals + } + }; + try { + const response = await axios.post(apiUrl, body, { + params + }); + + return { + amountReturned: new BigNumber(response.data.amountReturned), + protocolAddress: + protocol === "1inch" + ? response.data.rawQuote.tx.to + : response.data.rawQuote.to, + txData: + protocol === "1inch" + ? response.data.rawQuote.tx.data + : response.data.rawQuote.data + }; + } catch (e) { + throw new ApiError("Swap api request of DefiLlama failed"); + } +} + +export async function getDefiLlamaTxData( + pool: Pool, + dapp: Dapp | null, + assetFrom: string, + assetTo: string, + amountIn: ethers.BigNumber | string, + slippage: number +): Promise { + if (!dapp) { + const result = await Promise.all( + Object.values(dappDefiLlamaMap).map(async protocol => + getDefiLlamaSwapResult( + pool, + protocol as any, + assetFrom, + assetTo, + amountIn, + slippage + ) + ) + ); + const maxResult = result.reduce((max, current) => { + return current.amountReturned.isGreaterThan(max.amountReturned) + ? current + : max; + }); + return maxResult; + } else { + if (!(dapp === Dapp.ONEINCH || dapp === Dapp.ZEROEX)) + throw new Error("dapp not supported"); + return await getDefiLlamaSwapResult( + pool, + dappDefiLlamaMap[dapp] as any, + assetFrom, + assetTo, + amountIn, + slippage + ); + } +} diff --git a/src/test/defiLlama.test.ts b/src/test/defiLlama.test.ts new file mode 100644 index 0000000..7117ced --- /dev/null +++ b/src/test/defiLlama.test.ts @@ -0,0 +1,114 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ + +import { Dhedge, Pool } from ".."; + +import { Dapp, Network } from "../types"; +import { CONTRACT_ADDRESS, MAX_AMOUNT, TEST_POOL } from "./constants"; +import { + TestingRunParams, + setUSDCAmount, + testingHelper +} from "./utils/testingHelper"; +import BigNumber from "bignumber.js"; +import { balanceDelta } from "./utils/token"; + +import { getTxOptions } from "./txOptions"; + +const testDefiLlama = ({ wallet, network, provider }: TestingRunParams) => { + const USDC = CONTRACT_ADDRESS[network].USDC; + const WETH = CONTRACT_ADDRESS[network].WETH; + + let dhedge: Dhedge; + let pool: Pool; + jest.setTimeout(100000); + + describe(`pool on ${network}`, () => { + beforeAll(async () => { + dhedge = new Dhedge(wallet, network); + pool = await dhedge.loadPool(TEST_POOL[network]); + // top up gas + await provider.send("hardhat_setBalance", [ + wallet.address, + "0x10000000000000000" + ]); + await provider.send("evm_mine", []); + // top up USDC + await setUSDCAmount({ + amount: new BigNumber(10).times(1e6).toFixed(0), + userAddress: pool.address, + network, + provider + }); + await pool.approve(Dapp.ONEINCH, USDC, MAX_AMOUNT); + await pool.approve(Dapp.ZEROEX, USDC, MAX_AMOUNT); + }); + + it("trades 2 USDC into WETH on 0x", async () => { + await pool.tradeDefiLlama( + Dapp.ZEROEX, + USDC, + WETH, + "2000000", + 0.5, + await getTxOptions(network) + ); + const wethBalanceDelta = await balanceDelta( + pool.address, + WETH, + pool.signer + ); + expect(wethBalanceDelta.gt(0)); + }); + + it("trades 2 USDC into WETH on 1inch", async () => { + await pool.tradeDefiLlama( + Dapp.ONEINCH, + USDC, + WETH, + "2000000", + 0.5, + await getTxOptions(network) + ); + const wethBalanceDelta = await balanceDelta( + pool.address, + WETH, + pool.signer + ); + expect(wethBalanceDelta.gt(0)); + }); + + it("trades 2 USDC into WETH protocol with best price", async () => { + await pool.tradeDefiLlama( + null, + USDC, + WETH, + "2000000", + 0.5, + await getTxOptions(network) + ); + const wethBalanceDelta = await balanceDelta( + pool.address, + WETH, + pool.signer + ); + expect(wethBalanceDelta.gt(0)); + }); + }); +}; + +testingHelper({ + network: Network.OPTIMISM, + testingRun: testDefiLlama +}); + +// testingHelper({ +// network: Network.POLYGON, +// onFork: false, +// testingRun: testOneInch +// }); + +// testingHelper({ +// network: Network.BASE, +// onFork: false, +// testingRun: testOneInch +// });