From dc605971ddb71659f2eac53b8310a5a12ef5cc1b Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Mon, 16 Dec 2024 12:57:45 -0600 Subject: [PATCH 1/4] allow pre-encoded message --- apps/example/lib/stake.ts | 1 - packages/sdk/src/actions/getQuote.ts | 6 +- .../sdk/test/unit/actions/getQuote.test.ts | 56 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 packages/sdk/test/unit/actions/getQuote.test.ts diff --git a/apps/example/lib/stake.ts b/apps/example/lib/stake.ts index 1e98412..3df7971 100644 --- a/apps/example/lib/stake.ts +++ b/apps/example/lib/stake.ts @@ -1,4 +1,3 @@ -import { CrossChainAction } from "@across-protocol/app-sdk"; import { Address, encodeFunctionData } from "viem"; import { optimism } from "viem/chains"; diff --git a/packages/sdk/src/actions/getQuote.ts b/packages/sdk/src/actions/getQuote.ts index 107105d..0832282 100644 --- a/packages/sdk/src/actions/getQuote.ts +++ b/packages/sdk/src/actions/getQuote.ts @@ -1,4 +1,4 @@ -import { Address, Hex } from "viem"; +import { Address, Hex, isHex } from "viem"; import { Amount, CrossChainAction } from "../types/index.js"; import { getMultiCallHandlerAddress, @@ -129,6 +129,10 @@ export async function getQuote(params: GetQuoteParams): Promise { let message: Hex = "0x"; let recipient = _recipient; + if (isHex(crossChainMessage)) { + message = crossChainMessage; + } + if (crossChainMessage && typeof crossChainMessage === "object") { if (crossChainMessage.actions.length === 0) { throw new Error("No 'crossChainMessage.actions' provided"); diff --git a/packages/sdk/test/unit/actions/getQuote.test.ts b/packages/sdk/test/unit/actions/getQuote.test.ts new file mode 100644 index 0000000..4e9b67c --- /dev/null +++ b/packages/sdk/test/unit/actions/getQuote.test.ts @@ -0,0 +1,56 @@ +import { assert, assertType, describe, test } from "vitest"; +import { testClient } from "../../common/sdk.js"; +import { type Quote, type Route } from "../../../src/index.js"; +import { encodeFunctionData, erc20Abi, parseUnits } from "viem"; + +const inputToken = { + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + symbol: "USDC", + name: "USD Coin", + decimals: 6, + logoUrl: + "https://raw.githubusercontent.com/across-protocol/frontend/master/src/assets/token-logos/usdc.svg", +} as const; + +const inputAmount = 100; +const inputAmountBN = parseUnits(inputAmount.toString(), inputToken.decimals); + +// MAINNET => ARBITRUM +const testRoute = { + originChainId: 1, + destinationChainId: 42161, + originToken: inputToken.address, +} as const; + +let route: Route; + +const USDC_Arbitrum = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; +const spender = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; +const spendAmount = parseUnits("10", 6); + +const message = encodeFunctionData({ + abi: erc20Abi, + functionName: "approve", + args: [spender, spendAmount], +}); + +describe("getQuote", async () => { + test("Gets available routes for intent", async () => { + const [_route] = await testClient.getAvailableRoutes(testRoute); + assert(_route !== undefined, "route is not defined"); + assertType(_route); + route = _route; + }); + + test("Gets a quote for a route with raw message", async () => { + const _quote = await testClient.getQuote({ + route, + recipient: USDC_Arbitrum, + inputAmount: inputAmountBN, + crossChainMessage: message, + }); + + assert(_quote, "No quote for route"); + assertType(_quote); + }); +}); From db0df4003b59ffdb8a2edc899b54aea6e658f2d8 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 18 Dec 2024 13:04:48 +0200 Subject: [PATCH 2/4] test with simple transfer --- .../sdk/test/unit/actions/getQuote.test.ts | 114 ++++++++++++------ 1 file changed, 80 insertions(+), 34 deletions(-) diff --git a/packages/sdk/test/unit/actions/getQuote.test.ts b/packages/sdk/test/unit/actions/getQuote.test.ts index 4e9b67c..239dfd4 100644 --- a/packages/sdk/test/unit/actions/getQuote.test.ts +++ b/packages/sdk/test/unit/actions/getQuote.test.ts @@ -1,56 +1,102 @@ import { assert, assertType, describe, test } from "vitest"; import { testClient } from "../../common/sdk.js"; -import { type Quote, type Route } from "../../../src/index.js"; +import { + buildMulticallHandlerMessage, + getMultiCallHandlerAddress, + type Quote, + type Route, +} from "../../../src/index.js"; import { encodeFunctionData, erc20Abi, parseUnits } from "viem"; +import { arbitrum, optimism } from "viem/chains"; +// arbitrum USDC const inputToken = { - address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", symbol: "USDC", name: "USD Coin", decimals: 6, - logoUrl: - "https://raw.githubusercontent.com/across-protocol/frontend/master/src/assets/token-logos/usdc.svg", } as const; -const inputAmount = 100; -const inputAmountBN = parseUnits(inputAmount.toString(), inputToken.decimals); - -// MAINNET => ARBITRUM -const testRoute = { - originChainId: 1, - destinationChainId: 42161, - originToken: inputToken.address, +// optimism USDC +const outputToken = { + address: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + symbol: "USDC", + name: "USD Coin", + decimals: 6, } as const; -let route: Route; - -const USDC_Arbitrum = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831"; -const spender = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; -const spendAmount = parseUnits("10", 6); +const testEoa = "0x9A8f92a830A5cB89a3816e3D267CB7791c16b04D"; +const inputAmount = 100; +const inputAmountBN = parseUnits(inputAmount.toString(), inputToken.decimals); +const outputAmountBN = inputAmountBN / 2n; // 50% of input ensures this doesn't fail, since we cannot recompute after getting the initial quote -const message = encodeFunctionData({ - abi: erc20Abi, - functionName: "approve", - args: [spender, spendAmount], -}); +// ARBITRUM => OPTIMISM +const testRoute: Route = { + originChainId: arbitrum.id, + destinationChainId: optimism.id, + inputToken: inputToken.address, + outputToken: outputToken.address, + inputTokenSymbol: inputToken.symbol, + outputTokenSymbol: outputToken.symbol, + isNative: false, +}; -describe("getQuote", async () => { - test("Gets available routes for intent", async () => { - const [_route] = await testClient.getAvailableRoutes(testRoute); - assert(_route !== undefined, "route is not defined"); - assertType(_route); - route = _route; - }); +describe("getQuote with Raw Pre-Encoded Message", () => { + test("Gets a quote using a raw pre-encoded message from a test EOA", async () => { + // pre compute a simple transfer message + const calldata = buildMulticallHandlerMessage({ + fallbackRecipient: testEoa, + actions: [ + { + target: outputToken.address, + value: 0n, + callData: encodeFunctionData({ + abi: erc20Abi, + functionName: "transfer", + args: [testEoa, outputAmountBN], + }), + }, + ], + }); - test("Gets a quote for a route with raw message", async () => { + // Invoke getQuote with the raw message const _quote = await testClient.getQuote({ - route, - recipient: USDC_Arbitrum, + route: testRoute, + recipient: getMultiCallHandlerAddress(testRoute.destinationChainId), inputAmount: inputAmountBN, - crossChainMessage: message, + crossChainMessage: calldata, + // Removed 'sender' as it's not a parameter for getQuote }); - assert(_quote, "No quote for route"); + // Assert that a quote was returned + assert(_quote, "No quote returned for the provided route and message"); assertType(_quote); + + // Optional: Additional assertions to verify quote details + assert.equal( + _quote.deposit.inputAmount.toString(), + inputAmountBN.toString(), + "Input amounts should match", + ); + assert.equal( + _quote.deposit.originChainId, + testRoute.originChainId, + "Origin chain IDs should match", + ); + assert.equal( + _quote.deposit.destinationChainId, + testRoute.destinationChainId, + "Destination chain IDs should match", + ); + assert.equal( + _quote.deposit.inputToken, + testRoute.inputToken, + "Input tokens should match", + ); + assert.equal( + _quote.deposit.outputToken, + testRoute.outputToken, + "Output tokens should match", + ); }); }); From d2fe324270d3c179ef39826d5f500ebbf35370a8 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 18 Dec 2024 13:10:11 +0200 Subject: [PATCH 3/4] add changeset --- .changeset/tidy-kings-kick.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-kings-kick.md diff --git a/.changeset/tidy-kings-kick.md b/.changeset/tidy-kings-kick.md new file mode 100644 index 0000000..b670a31 --- /dev/null +++ b/.changeset/tidy-kings-kick.md @@ -0,0 +1,5 @@ +--- +"@across-protocol/app-sdk": patch +--- + +Allow passing of pre encoded message field in getQuote From 6e776b340deb2d3d86c58c73e4a2d9a62b0df2e7 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 19 Dec 2024 15:49:35 +0200 Subject: [PATCH 4/4] assert message --- packages/sdk/test/unit/actions/getQuote.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/sdk/test/unit/actions/getQuote.test.ts b/packages/sdk/test/unit/actions/getQuote.test.ts index 239dfd4..2a65b2c 100644 --- a/packages/sdk/test/unit/actions/getQuote.test.ts +++ b/packages/sdk/test/unit/actions/getQuote.test.ts @@ -72,6 +72,12 @@ describe("getQuote with Raw Pre-Encoded Message", () => { assert(_quote, "No quote returned for the provided route and message"); assertType(_quote); + assert.equal( + _quote.deposit.message, + calldata, + "Message should be equal to pre-encoded calldata", + ); + // Optional: Additional assertions to verify quote details assert.equal( _quote.deposit.inputAmount.toString(),