diff --git a/apps/example/scripts/sdk.ts b/apps/example/scripts/sdk.ts index 6812eec..baedf50 100644 --- a/apps/example/scripts/sdk.ts +++ b/apps/example/scripts/sdk.ts @@ -1,4 +1,5 @@ import { AcrossClient } from "@across-toolkit/sdk"; +import { encodeFunctionData, parseAbiItem, Address, Hex } from "viem"; // test using client with node (async function main() { @@ -7,12 +8,98 @@ import { AcrossClient } from "@across-toolkit/sdk"; integratorId: "TEST", }); - const res = await client.actions.getSuggestedFees({ - token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - originChainId: 1, - destinationChainId: 10, - amount: "1000000000000000000", - }); + // available routes + const routes = await client.actions.getAvailableRoutes({ + originChainId: 42161, + destinationChainId: 1, + })!; + const route = routes.find((r) => r.inputTokenSymbol === "DAI")!; + console.log(route); + + // quote + const inputAmount = 1000000000000000000000n; + const userAddress = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; + // Aave v2 Lending Pool: https://etherscan.io/address/0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9 + const aaveAddress = "0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9"; + // DAI + const depositCurrency = "0x6B175474E89094C44Da98b954EedeAC495271d0F"; + const aaveReferralCode = 0; - console.log(res); + const quoteRes = await client.actions.getQuote({ + ...route, + inputAmount, + recipient: "0x924a9f036260DdD5808007E1AA95f08eD08aA569", + crossChainMessage: { + actions: [ + { + target: depositCurrency, + callData: generateApproveCallData({ + aaveAddress, + depositAmount: inputAmount, + }), + value: 0n, + updateCallData: (outputAmount: bigint) => + generateApproveCallData({ + aaveAddress, + depositAmount: outputAmount, + }), + }, + { + target: aaveAddress, + callData: generateDepositCallDataForAave({ + userAddress, + depositAmount: inputAmount, + depositCurrency, + aaveReferralCode, + }), + value: 0n, + updateCallData: (outputAmount: bigint) => + generateDepositCallDataForAave({ + userAddress, + depositAmount: outputAmount, + depositCurrency, + aaveReferralCode, + }), + }, + ], + fallbackRecipient: "0x924a9f036260DdD5808007E1AA95f08eD08aA569", + }, + }); + console.log(quoteRes); })(); + +function generateApproveCallData({ + aaveAddress, + depositAmount, +}: { + aaveAddress: Address; + depositAmount: bigint; +}) { + const approveCallData = encodeFunctionData({ + abi: [parseAbiItem("function approve(address spender, uint256 value)")], + args: [aaveAddress, depositAmount], + }); + + return approveCallData; +} + +function generateDepositCallDataForAave({ + userAddress, + depositAmount, + depositCurrency, + aaveReferralCode, +}: { + userAddress: Address; + depositAmount: bigint; + depositCurrency: Address; + aaveReferralCode: number; +}) { + return encodeFunctionData({ + abi: [ + parseAbiItem( + "function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)" + ), + ], + args: [depositCurrency, depositAmount, userAddress, aaveReferralCode], + }); +} diff --git a/packages/sdk/src/actions/getAvailableRoutes.ts b/packages/sdk/src/actions/getAvailableRoutes.ts index 6c1af2d..bf16af4 100644 --- a/packages/sdk/src/actions/getAvailableRoutes.ts +++ b/packages/sdk/src/actions/getAvailableRoutes.ts @@ -20,14 +20,21 @@ export type AvailableRoutesResponse = { export async function getAvailableRoutes(params?: AvailableRoutesParams) { const client = getClient(); - try { - const searchParams = params ? buildSearchParams(params) : ""; - const res = await fetchAcross( - `${client.apiUrl}/available-routes?${searchParams}` - ); - return (await res.json()) as AvailableRoutesResponse; - } catch (error) { - client.log.error(error); - } + const searchParams = params ? buildSearchParams(params) : ""; + + const res = await fetchAcross( + `${client.apiUrl}/available-routes?${searchParams}` + ); + const data = (await res.json()) as AvailableRoutesResponse; + + // Transform to internal type consistency + return data.map((route) => ({ + originChainId: route.originChainId, + inputToken: route.originToken as Address, + destinationChainId: route.destinationChainId, + outputToken: route.destinationToken as Address, + inputTokenSymbol: route.originTokenSymbol, + outputTokenSymbol: route.destinationTokenSymbol, + })); } diff --git a/packages/sdk/src/actions/getQuote.ts b/packages/sdk/src/actions/getQuote.ts index e1051f3..9ff4b10 100644 --- a/packages/sdk/src/actions/getQuote.ts +++ b/packages/sdk/src/actions/getQuote.ts @@ -1,24 +1,121 @@ +import { Address, Hex } from "viem"; import { getClient } from "../client"; +import { Amount, CrossChainAction } from "../types"; +import { + getMultiCallHandlerAddress, + buildMulticallHandlerMessage, +} from "../utils"; export type QuoteParams = { - token: string; + inputToken: Address; + outputToken: Address; originChainId: number; destinationChainId: number; - amount: string; // bignumber string + inputAmount: Amount; + outputAmount?: Amount; // @todo add support for outputAmount + recipient?: Address; + crossChainMessage?: + | { + actions: CrossChainAction[]; + fallbackRecipient: Address; + } + | Hex; }; -// @todo add support for "message" for Across+ integrations export async function getQuote(params: QuoteParams) { const client = getClient(); - try { - const suggestedFees = await client.actions.getSuggestedFees(params); - if (!suggestedFees) { - client.log.error("suggested fees failed with params: \n", params); + + const { + inputToken, + outputToken, + originChainId, + destinationChainId, + recipient: _recipient, + inputAmount, + crossChainMessage, + } = params; + + let message = "0x"; + let recipient = _recipient; + + if (crossChainMessage && typeof crossChainMessage === "object") { + if (crossChainMessage.actions.length === 0) { + throw new Error("No 'crossChainMessage.actions' provided"); + } + + message = buildMulticallHandlerMessage({ + actions: crossChainMessage.actions, + fallbackRecipient: crossChainMessage.fallbackRecipient, + }); + recipient = getMultiCallHandlerAddress(destinationChainId); + } + + const { outputAmount, ...fees } = await client.actions.getSuggestedFees({ + inputToken, + outputToken, + originChainId, + destinationChainId, + amount: inputAmount, + recipient, + message, + }); + + // If a given cross-chain message is dependent on the outputAmount, update it + if (crossChainMessage && typeof crossChainMessage === "object") { + for (const action of crossChainMessage.actions) { + if (action.updateCallData) { + action.callData = action.updateCallData(outputAmount); + } } - return suggestedFees; - } catch (error) { - client.log.error(error); + message = buildMulticallHandlerMessage({ + actions: crossChainMessage.actions, + fallbackRecipient: crossChainMessage.fallbackRecipient, + }); } + + const { + // partial deposit args + timestamp, + exclusiveRelayer, + exclusivityDeadline, + spokePoolAddress, + // limits + isAmountTooLow, + limits, + // fees + lpFee, + relayerGasFee, + relayerCapitalFee, + totalRelayFee, + // misc + estimatedFillTimeSec, + } = fees; + + return { + deposit: { + inputAmount, + outputAmount, + originChainId, + destinationChainId, + recipient, + message, + quoteTimestamp: timestamp, + exclusiveRelayer, + exclusivityDeadline, + spokePoolAddress, + inputToken, + outputToken, + }, + limits, + fees: { + lpFee, + relayerGasFee, + relayerCapitalFee, + totalRelayFee, + }, + isAmountTooLow, + estimatedFillTimeSec, + }; } export type QuoteResponse = {}; diff --git a/packages/sdk/src/actions/getSuggestedFees.ts b/packages/sdk/src/actions/getSuggestedFees.ts index e6d198d..c59893d 100644 --- a/packages/sdk/src/actions/getSuggestedFees.ts +++ b/packages/sdk/src/actions/getSuggestedFees.ts @@ -1,24 +1,43 @@ +import { Address } from "viem"; + import { getClient } from "../client"; import { buildSearchParams, fetchAcross } from "../utils"; +import { Amount } from "../types"; export type SuggestedFeesParams = { - token: string; + inputToken: Address; + outputToken: Address; originChainId: number; destinationChainId: number; - amount: string; // bignumber string + amount: Amount; + recipient?: Address; + message?: string; }; export async function getSuggestedFees(params: SuggestedFeesParams) { const client = getClient(); - try { - const searchParams = buildSearchParams(params); - const res = await fetchAcross( - `${client.apiUrl}/suggested-fees?${searchParams}` + const searchParams = buildSearchParams({ + ...params, + depositMethod: "depositExclusive", + }); + const res = await fetchAcross( + `${client.apiUrl}/suggested-fees?${searchParams}` + ); + + if (!res.ok) { + throw new Error( + `Failed to fetch suggested fees: ${res.status}, ${await res.text()}` ); - return (await res.json()) as SuggestedFeesResponse; - } catch (error) { - client.log.error(error); } + + const data = (await res.json()) as SuggestedFeesResponse; + + const outputAmount = BigInt(params.amount) - BigInt(data.totalRelayFee.total); + return { + // @todo: more data transformations for easier consumptions + ...data, + outputAmount, + }; } export type SuggestedFeesResponse = { diff --git a/packages/sdk/src/actions/index.ts b/packages/sdk/src/actions/index.ts index 1e53af4..9471438 100644 --- a/packages/sdk/src/actions/index.ts +++ b/packages/sdk/src/actions/index.ts @@ -2,3 +2,4 @@ export * from "./getSuggestedFees"; export * from "./getAvailableRoutes"; export * from "./getOriginChains"; export * from "./getLimits"; +export * from "./getQuote"; diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index c0331bb..b5507b0 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -3,6 +3,7 @@ import { getSuggestedFees, getLimits, getOriginChains, + getQuote, } from "./actions"; import { MAINNET_API_URL, TESTNET_API_URL } from "./constants"; import { LogLevel, DefaultLogger, LoggerT } from "./utils"; @@ -31,6 +32,7 @@ export class AcrossClient { getAvailableRoutes: typeof getAvailableRoutes; getLimits: typeof getLimits; getOriginChains: typeof getOriginChains; + getQuote: typeof getQuote; // ... actions go here }; @@ -45,6 +47,7 @@ export class AcrossClient { getAvailableRoutes: getAvailableRoutes.bind(this), getLimits: getLimits.bind(this), getOriginChains: getOriginChains.bind(this), + getQuote: getQuote.bind(this), }; this.log.debug( diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts new file mode 100644 index 0000000..a8cba1e --- /dev/null +++ b/packages/sdk/src/types/index.ts @@ -0,0 +1,10 @@ +import { Address, Hex } from "viem"; + +export type Amount = string | bigint; + +export type CrossChainAction = { + target: Address; + callData: Hex; + value: Amount; + updateCallData?: (outputAmount: bigint) => Hex; +}; diff --git a/packages/sdk/src/utils/fetch.ts b/packages/sdk/src/utils/fetch.ts index 4c5e5be..39dbcf6 100644 --- a/packages/sdk/src/utils/fetch.ts +++ b/packages/sdk/src/utils/fetch.ts @@ -12,7 +12,7 @@ export const fetchAcross = globalThis.fetch.bind(globalThis); */ export function buildSearchParams( - params: Record> + params: Record> ): string { const searchParams = new URLSearchParams(); for (const key in params) { diff --git a/packages/sdk/src/utils/index.ts b/packages/sdk/src/utils/index.ts index 983c38d..9f9ea34 100644 --- a/packages/sdk/src/utils/index.ts +++ b/packages/sdk/src/utils/index.ts @@ -1,2 +1,3 @@ export * from "./fetch"; export * from "./logger"; +export * from "./multicallHandler"; diff --git a/packages/sdk/src/utils/multicallHandler.ts b/packages/sdk/src/utils/multicallHandler.ts new file mode 100644 index 0000000..ae2111c --- /dev/null +++ b/packages/sdk/src/utils/multicallHandler.ts @@ -0,0 +1,37 @@ +import { Address, encodeAbiParameters, parseAbiParameters } from "viem"; + +import { CrossChainAction } from "../types"; + +export type BuildMessageParams = { + fallbackRecipient: Address; + actions: CrossChainAction[]; +}; + +export function getMultiCallHandlerAddress(chainId: number) { + // @todo: use sdk or API to source addresses? + const defaultAddress = "0x924a9f036260DdD5808007E1AA95f08eD08aA569"; + switch (chainId) { + case 324: + return "0x863859ef502F0Ee9676626ED5B418037252eFeb2"; + case 59144: + return "0x1015c58894961F4F7Dd7D68ba033e28Ed3ee1cDB"; + default: + return defaultAddress; + } +} + +export function buildMulticallHandlerMessage(params: BuildMessageParams) { + const instructionsAbiParams = parseAbiParameters( + "((address target, bytes callData, uint256 value)[], address fallbackRecipient)" + ); + return encodeAbiParameters(instructionsAbiParams, [ + [ + params.actions.map(({ target, callData, value }) => ({ + target, + callData, + value: BigInt(value), + })), + params.fallbackRecipient, + ], + ]); +} diff --git a/packages/typescript-config/next.json b/packages/typescript-config/next.json index b6ef83f..de5d4ae 100644 --- a/packages/typescript-config/next.json +++ b/packages/typescript-config/next.json @@ -9,10 +9,10 @@ "declarationMap": false, "incremental": true, "jsx": "preserve", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": ["dom", "dom.iterable", "ES2020", "esnext"], "module": "esnext", "noEmit": true, "resolveJsonModule": true, - "target": "es5" + "target": "ES2020" } }