From 8f299a45d211948f1239a9efcd934cb31a6c9aeb Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Fri, 6 Sep 2024 16:36:38 -0400 Subject: [PATCH] feat(API): Align limits and suggested-fees relayerFeeDetails computations Add the following optional parameters to `/limits` to make it possible to compute the exact same limits in suggested fees: - message - recipient - relayer - amount This way the `/suggested-fees` endpoint can query `/limits` using those parameters so it doesn't need to recompute `relayerFeeDetails()` which might return very different values. I believe this also removes the need for `relayerFeeDetails` to compute the `isAmountTooLow` boolean anymore. --- api/_utils.ts | 64 ++++++++++++++++++++++++++++++++- api/limits.ts | 40 ++++++++++++++++++--- api/suggested-fees.ts | 83 ++++++++++++------------------------------- 3 files changed, 120 insertions(+), 67 deletions(-) diff --git a/api/_utils.ts b/api/_utils.ts index 2ed3eb3d8..27cddf387 100644 --- a/api/_utils.ts +++ b/api/_utils.ts @@ -284,6 +284,52 @@ export const validateChainAndTokenParams = ( }; }; +export const validateDepositMessage = async ( + recipient: string, + destinationChainId: number, + relayer: string, + outputTokenAddress: string, + amountInput: string, + message: string +) => { + if (!sdk.utils.isMessageEmpty(message)) { + if (!ethers.utils.isHexString(message)) { + throw new InputError("Message must be a hex string"); + } + if (message.length % 2 !== 0) { + // Our message encoding is a hex string, so we need to check that the length is even. + throw new InputError("Message must be an even hex string"); + } + const isRecipientAContract = await sdk.utils.isContractDeployedToAddress( + recipient, + getProvider(destinationChainId) + ); + if (!isRecipientAContract) { + throw new InputError( + "Recipient must be a contract when a message is provided" + ); + } else { + // If we're in this case, it's likely that we're going to have to simulate the execution of + // a complex message handling from the specified relayer to the specified recipient by calling + // the arbitrary function call `handleAcrossMessage` at the recipient. So that we can discern + // the difference between an OUT_OF_FUNDS error in either the transfer or through the execution + // of the `handleAcrossMessage` we will check that the balance of the relayer is sufficient to + // support this deposit. + const balanceOfToken = await getCachedTokenBalance( + destinationChainId, + relayer, + outputTokenAddress + ); + if (balanceOfToken.lt(amountInput)) { + throw new InputError( + `Relayer Address (${relayer}) doesn't have enough funds to support this deposit;` + + ` for help, please reach out to https://discord.across.to` + ); + } + } + } +}; + /** * Utility function to resolve route details based on given `inputTokenAddress` and `destinationChainId`. * The optional parameter `originChainId` can be omitted if the `inputTokenAddress` is unique across all @@ -665,13 +711,25 @@ export const getCachedLimits = async ( inputToken: string, outputToken: string, originChainId: number, - destinationChainId: number + destinationChainId: number, + amount: string, + recipient: string, + relayer: string, + message?: string ): Promise<{ minDeposit: string; maxDeposit: string; maxDepositInstant: string; maxDepositShortDelay: string; recommendedDepositInstant: string; + relayerFeeDetails: { + relayFeeTotal: string; + relayFeePercent: string; + capitalFeePercent: string; + capitalFeeTotal: string; + gasFeePercent: string; + gasFeeTotal: string; + }; }> => { return ( await axios(`${resolveVercelEndpoint()}/api/limits`, { @@ -680,6 +738,10 @@ export const getCachedLimits = async ( outputToken, originChainId, destinationChainId, + amount, + message, + recipient, + relayer, }, }) ).data; diff --git a/api/limits.ts b/api/limits.ts index 66b298542..be2eccb6c 100644 --- a/api/limits.ts +++ b/api/limits.ts @@ -3,7 +3,7 @@ import { VercelResponse } from "@vercel/node"; import { BigNumber, ethers } from "ethers"; import { DEFAULT_SIMULATED_RECIPIENT_ADDRESS } from "./_constants"; import { TokenInfo, TypedVercelRequest } from "./_types"; -import { object, assert, Infer, optional } from "superstruct"; +import { object, assert, Infer, optional, string } from "superstruct"; import { ENABLED_ROUTES, @@ -31,6 +31,9 @@ import { validateChainAndTokenParams, getCachedLatestBlock, getCachedGasPrice, + parsableBigNumberString, + validateDepositMessage, + InputError, } from "./_utils"; const LimitsQueryParamsSchema = object({ @@ -39,6 +42,10 @@ const LimitsQueryParamsSchema = object({ outputToken: optional(validAddress()), destinationChainId: positiveIntStr(), originChainId: optional(positiveIntStr()), + amount: optional(parsableBigNumberString()), + message: optional(string()), + recipient: optional(validAddress()), + relayer: optional(validAddress()), }); type LimitsQueryParams = Infer; @@ -88,6 +95,28 @@ const handler = async ( outputToken, } = validateChainAndTokenParams(query); + // Optional parameters that caller can use to specify specific deposit details with which + // to compute limits. + let { amount: amountInput, recipient, relayer, message } = query; + recipient ??= DEFAULT_SIMULATED_RECIPIENT_ADDRESS; + relayer ??= getDefaultRelayerAddress(destinationChainId, inputToken.symbol); + if (sdk.utils.isDefined(message)) { + if (!sdk.utils.isDefined(amountInput)) { + throw new InputError("amount must be defined when message is defined"); + } + validateDepositMessage( + recipient, + destinationChainId, + relayer, + outputToken.address, + amountInput, + message + ); + } + const amount = BigNumber.from( + amountInput ?? ethers.BigNumber.from("10").pow(l1Token.decimals) + ); + const hubPool = getHubPool(provider); const configStoreClient = new sdk.contracts.acrossConfigStore.Client( ENABLED_ROUTES.acrossConfigStoreAddress, @@ -133,13 +162,13 @@ const handler = async ( getRelayerFeeDetails( inputToken.address, outputToken.address, - ethers.BigNumber.from("10").pow(l1Token.decimals), + amount, computedOriginChainId, destinationChainId, - DEFAULT_SIMULATED_RECIPIENT_ADDRESS, + recipient, tokenPriceNative, - undefined, - getDefaultRelayerAddress(destinationChainId, l1Token.symbol), + message, + relayer, gasPrice ), callViaMulticall3(provider, multiCalls, { @@ -312,6 +341,7 @@ const handler = async ( maxDepositInstant: bufferedMaxDepositInstant.toString(), maxDepositShortDelay: bufferedMaxDepositShortDelay.toString(), recommendedDepositInstant: bufferedRecommendedDepositInstant.toString(), + relayerFeeDetails, }; logger.debug({ at: "Limits", diff --git a/api/suggested-fees.ts b/api/suggested-fees.ts index ef704f0d5..a2febe790 100644 --- a/api/suggested-fees.ts +++ b/api/suggested-fees.ts @@ -11,7 +11,6 @@ import { getLogger, InputError, getProvider, - getRelayerFeeDetails, getCachedTokenPrice, handleErrorCondition, parsableBigNumberString, @@ -21,14 +20,13 @@ import { HUB_POOL_CHAIN_ID, ENABLED_ROUTES, getSpokePoolAddress, - getCachedTokenBalance, getDefaultRelayerAddress, getHubPool, callViaMulticall3, validateChainAndTokenParams, getCachedLimits, getCachedLatestBlock, - getCachedGasPrice, + validateDepositMessage, } from "./_utils"; import { selectExclusiveRelayer } from "./_exclusivity"; import { resolveTiming, resolveRebalanceTiming } from "./_timings"; @@ -91,42 +89,15 @@ const handler = async ( relayer ??= getDefaultRelayerAddress(destinationChainId, inputToken.symbol); recipient ??= DEFAULT_SIMULATED_RECIPIENT_ADDRESS; - - if (sdk.utils.isDefined(message) && !sdk.utils.isMessageEmpty(message)) { - if (!ethers.utils.isHexString(message)) { - throw new InputError("Message must be a hex string"); - } - if (message.length % 2 !== 0) { - // Our message encoding is a hex string, so we need to check that the length is even. - throw new InputError("Message must be an even hex string"); - } - const isRecipientAContract = await sdk.utils.isContractDeployedToAddress( + if (sdk.utils.isDefined(message)) { + validateDepositMessage( recipient, - getProvider(destinationChainId) + destinationChainId, + relayer, + outputToken.address, + amountInput, + message ); - if (!isRecipientAContract) { - throw new InputError( - "Recipient must be a contract when a message is provided" - ); - } else { - // If we're in this case, it's likely that we're going to have to simulate the execution of - // a complex message handling from the specified relayer to the specified recipient by calling - // the arbitrary function call `handleAcrossMessage` at the recipient. So that we can discern - // the difference between an OUT_OF_FUNDS error in either the transfer or through the execution - // of the `handleAcrossMessage` we will check that the balance of the relayer is sufficient to - // support this deposit. - const balanceOfToken = await getCachedTokenBalance( - destinationChainId, - relayer, - outputToken.address - ); - if (balanceOfToken.lt(amountInput)) { - throw new InputError( - `Relayer Address (${relayer}) doesn't have enough funds to support this deposit;` + - ` for help, please reach out to https://discord.across.to` - ); - } - } } const latestBlock = await getCachedLatestBlock(HUB_POOL_CHAIN_ID); @@ -179,7 +150,9 @@ const handler = async ( ENABLED_ROUTES.acrossConfigStoreAddress, provider ); - const baseCurrency = destinationChainId === 137 ? "matic" : "eth"; + const baseCurrency = sdk.utils + .getNativeTokenSymbol(destinationChainId) + .toLowerCase(); // Aggregate multiple calls into a single multicall to decrease // opportunities for RPC calls to be delayed. @@ -207,32 +180,34 @@ const handler = async ( const [ [currentUt, nextUt, _quoteTimestamp, rawL1TokenConfig], - tokenPrice, tokenPriceUsd, limits, - gasPrice, ] = await Promise.all([ callViaMulticall3(provider, multiCalls, { blockTag: quoteBlockNumber }), - getCachedTokenPrice(l1Token.address, baseCurrency), getCachedTokenPrice(l1Token.address, "usd"), getCachedLimits( inputToken.address, outputToken.address, computedOriginChainId, - destinationChainId + destinationChainId, + amountInput, + recipient, + relayer, + message ), - getCachedGasPrice(destinationChainId), ]); + const { maxDeposit, maxDepositInstant, minDeposit, relayerFeeDetails } = + limits; const quoteTimestamp = parseInt(_quoteTimestamp.toString()); const amountInUsd = amount .mul(parseUnits(tokenPriceUsd.toString(), 18)) .div(parseUnits("1", inputToken.decimals)); - if (amount.gt(limits.maxDeposit)) { + if (amount.gt(maxDeposit)) { throw new InputError( `Amount exceeds max. deposit limit: ${ethers.utils.formatUnits( - limits.maxDeposit, + maxDeposit, inputToken.decimals )} ${inputToken.symbol}` ); @@ -252,22 +227,8 @@ const handler = async ( nextUt ); const lpFeeTotal = amount.mul(lpFeePct).div(ethers.constants.WeiPerEther); - const relayerFeeDetails = await getRelayerFeeDetails( - inputToken.address, - outputToken.address, - amount, - computedOriginChainId, - destinationChainId, - recipient, - tokenPrice, - message, - relayer, - gasPrice - ); - const isAmountTooLow = - relayerFeeDetails.isAmountTooLow || - BigNumber.from(amountInput).lt(limits.minDeposit); + const isAmountTooLow = BigNumber.from(amountInput).lt(minDeposit); const skipAmountLimitEnabled = skipAmountLimit === "true"; if (!skipAmountLimitEnabled && isAmountTooLow) { @@ -282,7 +243,7 @@ const handler = async ( relayerFeeDetails.relayFeePercent ).add(lpFeePct); - const estimatedFillTimeSec = amount.gte(limits.maxDepositInstant) + const estimatedFillTimeSec = amount.gte(maxDepositInstant) ? resolveRebalanceTiming(String(destinationChainId)) : resolveTiming( String(computedOriginChainId),