diff --git a/README.md b/README.md index 2c69238e..559c1d0f 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,17 @@ import curve from "@curvefi/api"; // '0xb0cada2a2983dc0ed85a26916d32b9caefe45fecde47640bd7d0e214ff22aed3', // '0x00ea7d827b3ad50ce933e96c579810cd7e70d66a034a86ec4e1e10005634d041' // ] + + // Get populated approve transactions (without executing) + const approveTxs = await curve.populateApprove(["DAI", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"], ['1000', '1000'], spender); + // OR with custom user address (for API): + // await curve.populateApprove(["DAI", "USDC"], ['1000', '1000'], spender, false, '0x...userAddress'); + // Returns array of TransactionLike objects (may include reset to 0 if needed for some tokens) + console.log(approveTxs); + // [ + // { to: '0x6B17...', data: '0x095ea7b3...', hash: '0x...', ... }, + // { to: '0xA0b8...', data: '0x095ea7b3...', hash: '0x...', ... } + // ] })() ``` @@ -1191,6 +1202,13 @@ import curve from "@curvefi/api"; // [ // '0xc111e471715ae6f5437e12d3b94868a5b6542cd7304efca18b5782d315760ae5' // ] + + // Get populated transactions for approve (without executing) + const approveTxs = await curve.router.populateApprove('DAI', 1000, false, userAddress); + // OR const approveTxs = await curve.router.populateApprove('0x6B175474E89094C44Da98b954EedeAC495271d0F', 1000, false, userAddress); + console.log(approveTxs); + // [{ to: '0x6B17...', data: '0x...', ... }] + // Returns array of TransactionLike objects const swapTx = await curve.router.swap('DAI', 'CRV', '1000'); // OR const swapTx = await curve.router.swap('0x6B175474E89094C44Da98b954EedeAC495271d0F', '0xD533a949740bb3306d119CC777fa900bA034cd52', '1000'); console.log(swapTx.hash); @@ -1200,6 +1218,16 @@ import curve from "@curvefi/api"; await curve.getBalances(['DAI', 'CRV']); // [ '8900.0', '100428.626463428100672494' ] + + // Get calldata for swap (without executing transaction) + // First, you need to call getBestRouteAndOutput to cache the route + await curve.router.getBestRouteAndOutput('DAI', 'CRV', '1000'); + + // Then get calldata + const { data, to, from, amount } = await curve.router.populateSwap('DAI', 'CRV', '1000', 0.5); + // OR const tx = await curve.router.populateSwap('0x6B175474E89094C44Da98b954EedeAC495271d0F', '0xD533a949740bb3306d119CC777fa900bA034cd52', '1000', 0.5); + console.log(data); + // 0x8f726f1c000000000000000000000000... })() ``` diff --git a/package-lock.json b/package-lock.json index 86206b6e..000089d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@curvefi/api", - "version": "2.68.17", + "version": "2.68.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@curvefi/api", - "version": "2.68.17", + "version": "2.68.18", "license": "MIT", "dependencies": { "@curvefi/ethcall": "^6.0.16", diff --git a/package.json b/package.json index d9cc5ab4..ff71d2be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@curvefi/api", - "version": "2.68.17", + "version": "2.68.18", "description": "JavaScript library for curve.finance", "main": "lib/index.js", "author": "Macket", diff --git a/src/index.ts b/src/index.ts index 8480767b..d53f3ba3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,8 +16,10 @@ import { swapIsApproved, swapApproveEstimateGas, swapApprove, + swapPopulateApprove, swapEstimateGas, swap, + populateSwap, getSwappedAmount, } from "./router.js"; import { Curve } from "./curve.js"; @@ -62,6 +64,7 @@ import { hasAllowance, ensureAllowanceEstimateGas, ensureAllowance, + populateApprove, getUsdRate, getGasPriceFromL1, getGasPriceFromL2, @@ -175,6 +178,7 @@ export const createCurve = () => { getAllowance: getAllowance.bind(_curve), hasAllowance: hasAllowance.bind(_curve), ensureAllowance: ensureAllowance.bind(_curve), + populateApprove: populateApprove.bind(_curve), getCoinsData: getCoinsData.bind(_curve), getVolume: getVolume.bind(_curve), hasDepositAndStake: hasDepositAndStake.bind(_curve), @@ -352,7 +356,9 @@ export const createCurve = () => { priceImpact: swapPriceImpact.bind(_curve), isApproved: swapIsApproved.bind(_curve), approve: swapApprove.bind(_curve), + populateApprove: swapPopulateApprove.bind(_curve), swap: swap.bind(_curve), + populateSwap: populateSwap.bind(_curve), getSwappedAmount: getSwappedAmount.bind(_curve), estimateGas: { approve: swapApproveEstimateGas.bind(_curve), diff --git a/src/router.ts b/src/router.ts index 5a10504b..6901966b 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,5 +1,5 @@ import BigNumber from "bignumber.js"; -import {ethers} from "ethers"; +import {ethers, TransactionLike} from "ethers"; import {type Curve, OLD_CHAINS} from "./curve.js"; import {IChainId, IDict, IRoute, IRouteOutputAndCost, IRouteStep} from "./interfaces"; import { @@ -8,6 +8,7 @@ import { _get_small_x, _getCoinAddresses, _getCoinDecimals, + _getAllowance, _getUsdRate, BN, DIGas, @@ -20,6 +21,7 @@ import { hasAllowance, isEth, parseUnits, + populateApprove, runWorker, smartNumber, toBN, @@ -406,6 +408,10 @@ export async function swapApprove(this: Curve, inputCoin: string, amount: number return await ensureAllowance.call(this, [inputCoin], [amount], this.constants.ALIASES.router); } +export async function swapPopulateApprove(this: Curve, inputCoin: string, amount: number | string, isMax = true, userAddress: string): Promise { + return await populateApprove.call(this, [inputCoin], [amount], this.constants.ALIASES.router, isMax, userAddress); +} + export async function swapEstimateGas(this: Curve, inputCoin: string, outputCoin: string, amount: number | string): Promise { const [inputCoinAddress, outputCoinAddress] = _getCoinAddresses.call(this, inputCoin, outputCoin); const [inputCoinDecimals] = _getCoinDecimals.call(this, inputCoinAddress, outputCoinAddress); @@ -459,6 +465,30 @@ export async function swap(this: Curve, inputCoin: string, outputCoin: string, a } } +export async function populateSwap(this: Curve, inputCoin: string, outputCoin: string, amount: number | string, slippage = 0.5): Promise { + console.log(inputCoin, outputCoin, amount, slippage); + const [inputCoinAddress, outputCoinAddress] = _getCoinAddresses.call(this, inputCoin, outputCoin); + const [inputCoinDecimals, outputCoinDecimals] = _getCoinDecimals.call(this, inputCoinAddress, outputCoinAddress); + + const { route, output } = _getBestRouteAndOutput.call(this, inputCoinAddress, outputCoinAddress, amount); + + if (route.length === 0) { + throw new Error("This pair can't be exchanged"); + } + + const { _route, _swapParams, _pools } = _getExchangeArgs.call(this, route); + const _amount = parseUnits(amount, inputCoinDecimals); + const minRecvAmountBN: BigNumber = BN(output).times(100 - slippage).div(100); + const _minRecvAmount = fromBN(minRecvAmountBN, outputCoinDecimals); + + const contract = this.contracts[this.constants.ALIASES.router].contract; + return await contract.exchange.populateTransaction(...[ + _route, _swapParams, _amount, _minRecvAmount, + ..._pools ? [_pools] : [], + { value: isEth(inputCoinAddress) ? _amount : this.parseUnits("0") }, + ]) +} + export async function getSwappedAmount(this: Curve, tx: ethers.ContractTransactionResponse, outputCoin: string): Promise { const [outputCoinAddress] = _getCoinAddresses.call(this, outputCoin); const [outputCoinDecimals] = _getCoinDecimals.call(this, outputCoinAddress); diff --git a/src/utils.ts b/src/utils.ts index 43ffd9ce..3fe95736 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import {Contract, ethers} from 'ethers'; +import {Contract, ethers, TransactionLike} from 'ethers'; import {Contract as MulticallContract} from "@curvefi/ethcall"; import BigNumber from 'bignumber.js'; import { @@ -310,6 +310,34 @@ export async function ensureAllowance(this: Curve, coins: string[], amounts: (nu return await _ensureAllowance.call(this, coinAddresses, _amounts, spender, isMax) } +export async function populateApprove(this: Curve, coins: string[], amounts: (number | string)[], spender: string, isMax = true, userAddress?: string): Promise { + const coinAddresses = _getCoinAddresses.call(this, coins); + const decimals = _getCoinDecimals.call(this, coinAddresses); + const _amounts = amounts.map((a, i) => parseUnits(a, decimals[i])); + + const address = userAddress || this.signerAddress; + if (!address) throw Error("User address is not defined. Pass userAddress parameter."); + + const allowance = await _getAllowance.call(this, coinAddresses, address, spender); + + const transactions: TransactionLike[] = []; + + for (let i = 0; i < allowance.length; i++) { + if (allowance[i] < _amounts[i]) { + const contract = this.contracts[coinAddresses[i]].contract; + const _approveAmount = isMax ? MAX_ALLOWANCE : _amounts[i]; + + if (allowance[i] > parseUnits("0")) { + transactions.push(await contract.approve.populateTransaction(spender, parseUnits("0"))); + } + + transactions.push(await contract.approve.populateTransaction(spender, _approveAmount)); + } + } + + return transactions; +} + export function getPoolIdBySwapAddress(this: Curve, swapAddress: string): string { const poolsData = this.getPoolsData(); const poolIds = Object.entries(poolsData).filter(([, poolData]) => poolData.swap_address.toLowerCase() === swapAddress.toLowerCase());