Skip to content

Commit

Permalink
feat(API): Align limits and suggested-fees relayerFeeDetails computat…
Browse files Browse the repository at this point in the history
…ions

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.
  • Loading branch information
nicholaspai committed Sep 6, 2024
1 parent 7ec25d9 commit 8f299a4
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 67 deletions.
64 changes: 63 additions & 1 deletion api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`, {
Expand All @@ -680,6 +738,10 @@ export const getCachedLimits = async (
outputToken,
originChainId,
destinationChainId,
amount,
message,
recipient,
relayer,
},
})
).data;
Expand Down
40 changes: 35 additions & 5 deletions api/limits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -31,6 +31,9 @@ import {
validateChainAndTokenParams,
getCachedLatestBlock,
getCachedGasPrice,
parsableBigNumberString,
validateDepositMessage,
InputError,
} from "./_utils";

const LimitsQueryParamsSchema = object({
Expand All @@ -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<typeof LimitsQueryParamsSchema>;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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, {
Expand Down Expand Up @@ -312,6 +341,7 @@ const handler = async (
maxDepositInstant: bufferedMaxDepositInstant.toString(),
maxDepositShortDelay: bufferedMaxDepositShortDelay.toString(),
recommendedDepositInstant: bufferedRecommendedDepositInstant.toString(),
relayerFeeDetails,
};
logger.debug({
at: "Limits",
Expand Down
83 changes: 22 additions & 61 deletions api/suggested-fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
getLogger,
InputError,
getProvider,
getRelayerFeeDetails,
getCachedTokenPrice,
handleErrorCondition,
parsableBigNumberString,
Expand All @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -179,7 +150,9 @@ const handler = async (
ENABLED_ROUTES.acrossConfigStoreAddress,
provider
);
const baseCurrency = destinationChainId === 137 ? "matic" : "eth";
const baseCurrency = sdk.utils

Check warning on line 153 in api/suggested-fees.ts

View workflow job for this annotation

GitHub Actions / format-and-lint

'baseCurrency' is assigned a value but never used
.getNativeTokenSymbol(destinationChainId)
.toLowerCase();

// Aggregate multiple calls into a single multicall to decrease
// opportunities for RPC calls to be delayed.
Expand Down Expand Up @@ -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}`
);
Expand All @@ -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) {
Expand All @@ -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),
Expand Down

0 comments on commit 8f299a4

Please sign in to comment.