diff --git a/src/consts/interfaces.consts.ts b/src/consts/interfaces.consts.ts index 36364ce..b139b40 100644 --- a/src/consts/interfaces.consts.ts +++ b/src/consts/interfaces.consts.ts @@ -171,6 +171,7 @@ export interface IPrepareXchainRequestFulfillmentTransactionResponse { unsignedTxs: IPeanutUnsignedTransaction[] feeEstimation: string estimatedFromAmount: string + slippagePercentage: number } //signAndSubmitTx diff --git a/src/request.ts b/src/request.ts index cf8c231..97bb05f 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,8 +1,8 @@ -import { ethers, getDefaultProvider, utils } from 'ethersv5' +import { ethers, getDefaultProvider } from 'ethersv5' import { EPeanutLinkType, IPeanutUnsignedTransaction } from './consts/interfaces.consts' import { ERC20_ABI, LATEST_STABLE_BATCHER_VERSION } from './data' -import { config, getSquidRoute, interfaces, prepareApproveERC20Tx, resolveFromEnsName } from '.' -import { prepareXchainFromAmountCalculation, normalizePath } from './util' +import { config, interfaces, prepareApproveERC20Tx, resolveFromEnsName } from '.' +import { normalizePath, routeForTargetAmount } from './util' // INTERFACES export interface ICreateRequestLinkProps { @@ -50,6 +50,7 @@ export type IPrepareXchainRequestFulfillmentTransactionProps = { squidRouterUrl: string provider: ethers.providers.Provider tokenType: EPeanutLinkType + slippagePercentage?: number } & ( | { link: string @@ -186,7 +187,16 @@ export async function getRequestLinkDetails( export async function prepareXchainRequestFulfillmentTransaction( props: IPrepareXchainRequestFulfillmentTransactionProps ): Promise { - let { senderAddress, fromToken, fromTokenDecimals, fromChainId, squidRouterUrl, provider, tokenType } = props + let { + senderAddress, + fromToken, + fromTokenDecimals, + fromChainId, + squidRouterUrl, + provider, + tokenType, + slippagePercentage, + } = props let linkDetails: Pick< IGetRequestLinkDetailsResponse, 'chainId' | 'recipientAddress' | 'tokenAmount' | 'tokenDecimals' | 'tokenAddress' @@ -246,32 +256,15 @@ export async function prepareXchainRequestFulfillmentTransaction( chainId: destinationChainId, decimals: destinationTokenDecimals, } - const estimatedFromAmount = await prepareXchainFromAmountCalculation({ + + const { estimatedFromAmount, weiFromAmount, routeResult, finalSlippage } = await routeForTargetAmount({ + slippagePercentage, fromToken: fromTokenData, - toAmount: destinationTokenAmount, toToken: toTokenData, - slippagePercentage: 0.3, // this can be low because squid will add slippage - }) - - console.log('estimatedFromAmount', estimatedFromAmount) - if (!estimatedFromAmount) { - throw new Error('Failed to estimate from amount') - } - // get wei of amount being withdrawn and send as string (e.g. "10000000000000000") - const tokenAmount = utils.parseUnits(estimatedFromAmount, fromTokenDecimals) - config.verbose && console.log('Getting squid info..') - const unsignedTxs: interfaces.IPeanutUnsignedTransaction[] = [] - - const routeResult = await getSquidRoute({ + targetAmount: destinationTokenAmount, squidRouterUrl, - fromChain: fromChainId, - fromToken: fromToken, - fromAmount: tokenAmount.toString(), - toChain: destinationChainId, - toToken: destinationToken, fromAddress: senderAddress, toAddress: recipientAddress, - enableBoost: true, }) // Transaction estimation from Squid API allows us to know the transaction fees (gas and fee), then we can iterate over them and add the values ​​that are in dollars @@ -294,6 +287,8 @@ export async function prepareXchainRequestFulfillmentTransaction( }) } + const unsignedTxs: interfaces.IPeanutUnsignedTransaction[] = [] + if (tokenType == EPeanutLinkType.native) { txOptions = { ...txOptions, @@ -305,7 +300,7 @@ export async function prepareXchainRequestFulfillmentTransaction( senderAddress, destinationChainId, fromToken, - tokenAmount, + weiFromAmount, fromTokenDecimals, true, LATEST_STABLE_BATCHER_VERSION, @@ -334,7 +329,12 @@ export async function prepareXchainRequestFulfillmentTransaction( value: BigInt(routeResult.value.toString()), }) - return { unsignedTxs, feeEstimation: feeEstimation.toString(), estimatedFromAmount } + return { + unsignedTxs, + feeEstimation: feeEstimation.toString(), + estimatedFromAmount, + slippagePercentage: finalSlippage, + } } export function prepareRequestLinkFulfillmentTransaction({ @@ -378,7 +378,6 @@ export async function submitRequestLinkFulfillment({ chainId, hash, payerAddress, - signedTx, apiUrl = 'https://api.peanut.to/', link, amountUsd, diff --git a/src/util.ts b/src/util.ts index 7639d27..9fd2529 100644 --- a/src/util.ts +++ b/src/util.ts @@ -4,6 +4,7 @@ import { config } from './config.ts' import * as interfaces from './consts/interfaces.consts.ts' import { ANYONE_WITHDRAWAL_MODE, PEANUT_SALT, RECIPIENT_WITHDRAWAL_MODE } from './consts/misc.ts' import { TransactionRequest } from '@ethersproject/abstract-provider' +import { getSquidRoute } from '.' export function assert(condition: any, message: string) { if (!condition) { @@ -685,7 +686,13 @@ export function compareVersions(version1: string, version2: string, lead: string return true } -async function getTokenPrice({ tokenAddress, chainId }: { tokenAddress: string; chainId: string | number }) { +export async function getTokenPrice({ + tokenAddress, + chainId, +}: { + tokenAddress: string + chainId: string | number +}): Promise { const response = await fetch( 'https://api.0xsquid.com/v1/token-price?' + new URLSearchParams({ tokenAddress, chainId: chainId.toString() }) ) @@ -704,11 +711,15 @@ export async function prepareXchainFromAmountCalculation({ toAmount, toToken, slippagePercentage = 0.3, // 0.3% + fromTokenPrice, + toTokenPrice, }: { fromToken: TokenData toToken: TokenData toAmount: string slippagePercentage?: number + fromTokenPrice?: number + toTokenPrice?: number }): Promise { if (slippagePercentage < 0) { console.error('Invalid slippagePercentage: Cannot be negative.') @@ -717,15 +728,19 @@ export async function prepareXchainFromAmountCalculation({ try { // Get usd prices for both tokens - const [fromTokenPrice, toTokenPrice] = await Promise.all([ - getTokenPrice({ - chainId: fromToken.chainId, - tokenAddress: fromToken.address, - }), - getTokenPrice({ - chainId: toToken.chainId, - tokenAddress: toToken.address, - }), + ;[fromTokenPrice, toTokenPrice] = await Promise.all([ + fromTokenPrice + ? Promise.resolve(fromTokenPrice) + : getTokenPrice({ + chainId: fromToken.chainId, + tokenAddress: fromToken.address, + }), + toTokenPrice + ? Promise.resolve(toTokenPrice) + : getTokenPrice({ + chainId: toToken.chainId, + tokenAddress: toToken.address, + }), ]) // Normalize prices to account for different decimal counts between tokens. @@ -750,6 +765,139 @@ export async function prepareXchainFromAmountCalculation({ } } +async function estimateRouteWithMinSlippage({ + slippagePercentage, + fromToken, + toToken, + targetAmount, + squidRouterUrl, + fromAddress, + toAddress, + fromTokenPrice, + toTokenPrice, +}: { + slippagePercentage: number + fromToken: TokenData + toToken: TokenData + targetAmount: string + squidRouterUrl: string + fromAddress: string + toAddress: string + fromTokenPrice: number + toTokenPrice: number +}): Promise<{ + estimatedFromAmount: string + weiFromAmount: ethers.BigNumber + routeResult: interfaces.ISquidRoute +}> { + const estimatedFromAmount = await prepareXchainFromAmountCalculation({ + fromToken, + toAmount: targetAmount, + toToken, + slippagePercentage, + fromTokenPrice, + toTokenPrice, + }) + console.log('estimatedFromAmount', estimatedFromAmount) + if (!estimatedFromAmount) { + throw new Error('Failed to estimate from amount') + } + const weiFromAmount = ethers.utils.parseUnits(estimatedFromAmount, fromToken.decimals) + const routeResult = await getSquidRoute({ + squidRouterUrl, + fromChain: fromToken.chainId, + fromToken: fromToken.address, + fromAmount: weiFromAmount.toString(), + toChain: toToken.chainId, + toToken: toToken.address, + fromAddress, + toAddress, + enableBoost: true, + }) + return { estimatedFromAmount, weiFromAmount, routeResult } +} + +/** + * For a token pair and target amount calculates the minium from amount + * needed to get the target amount, and the squid route to get there + */ +export async function routeForTargetAmount({ + slippagePercentage, + fromToken, + toToken, + targetAmount, + squidRouterUrl, + fromAddress, + toAddress, +}: { + slippagePercentage?: number + fromToken: TokenData + toToken: TokenData + targetAmount: string + squidRouterUrl: string + fromAddress: string + toAddress: string +}): Promise<{ + estimatedFromAmount: string + weiFromAmount: ethers.BigNumber + routeResult: interfaces.ISquidRoute + finalSlippage: number +}> { + const [fromTokenPrice, toTokenPrice] = await Promise.all([ + getTokenPrice({ + chainId: fromToken.chainId, + tokenAddress: fromToken.address, + }), + getTokenPrice({ + chainId: toToken.chainId, + tokenAddress: toToken.address, + }), + ]) + + if (slippagePercentage) { + return { + ...(await estimateRouteWithMinSlippage({ + slippagePercentage, + fromToken, + toToken, + targetAmount, + squidRouterUrl, + fromAddress, + toAddress, + fromTokenPrice, + toTokenPrice, + })), + finalSlippage: slippagePercentage, + } + } + + let result: { estimatedFromAmount: string; weiFromAmount: ethers.BigNumber; routeResult: interfaces.ISquidRoute } + + const weiToAmount = ethers.utils.parseUnits(targetAmount, toToken.decimals) + let minToAmount: ethers.BigNumber = ethers.BigNumber.from(0) + slippagePercentage = 0 + while (minToAmount.lt(weiToAmount)) { + result = await estimateRouteWithMinSlippage({ + slippagePercentage, + fromToken, + toToken, + targetAmount, + squidRouterUrl, + fromAddress, + toAddress, + fromTokenPrice, + toTokenPrice, + }) + minToAmount = ethers.BigNumber.from(result.routeResult.txEstimation.toAmountMin) + slippagePercentage += 0.1 + if (5.0 < slippagePercentage) { + // we dont want to go over 5% slippage + throw new Error('Slippage percentage exceeded maximum allowed value') + } + } + return { ...result, finalSlippage: slippagePercentage } +} + export function normalizePath(url: string): string { try { const urlObject = new URL(url)