diff --git a/.changeset/selfish-turkeys-knock.md b/.changeset/selfish-turkeys-knock.md new file mode 100644 index 0000000..0b6ce18 --- /dev/null +++ b/.changeset/selfish-turkeys-knock.md @@ -0,0 +1,5 @@ +--- +"@across-protocol/app-sdk": patch +--- + +Add integratorId to getSwapQuote action diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 341bf86..fe69721 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@across-protocol/app-sdk", - "version": "0.4.0", + "version": "0.4.1", "main": "./dist/index.js", "type": "module", "description": "The official SDK for integrating Across bridge into your dapp.", @@ -63,4 +63,4 @@ "peerDependencies": { "viem": "^2.31.2" } -} \ No newline at end of file +} diff --git a/packages/sdk/src/actions/getSwapQuote.ts b/packages/sdk/src/actions/getSwapQuote.ts index 8ce18df..c794fdd 100644 --- a/packages/sdk/src/actions/getSwapQuote.ts +++ b/packages/sdk/src/actions/getSwapQuote.ts @@ -33,6 +33,11 @@ export type GetSwapQuoteParams = Omit< slippage?: number; appFee?: number; actions?: Action[]; + /** + * [Optional] Integrator identifier to be forwarded to the swap API so it can + * append the integrator tag to the final deposit calldata when applicable. + */ + integratorId?: string; /** * [Optional] The logger to use. */ diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts index bc38852..12ce434 100644 --- a/packages/sdk/src/client.ts +++ b/packages/sdk/src/client.ts @@ -661,6 +661,7 @@ export class AcrossClient { const quote = await getSwapQuote({ ...params, skipOriginTxEstimation: true, + integratorId: params?.integratorId ?? this.integratorId, logger: params?.logger ?? this.logger, apiUrl: params?.apiUrl ?? this.apiUrl, }); diff --git a/packages/sdk/src/utils/hex.ts b/packages/sdk/src/utils/hex.ts index ecd0547..068c7c0 100644 --- a/packages/sdk/src/utils/hex.ts +++ b/packages/sdk/src/utils/hex.ts @@ -1,6 +1,7 @@ import { Address, concat, Hex, isAddress, isHex, padHex } from "viem"; export const DOMAIN_CALLDATA_DELIMITER = "0x1dc0de"; +export const SWAP_CALLDATA_MARKER = "0x73c0de"; export function tagIntegratorId(integratorId: Hex, txData: Hex) { assertValidIntegratorId(integratorId); @@ -14,6 +15,25 @@ export function getIntegratorDataSuffix(integratorId: Hex) { return concat([DOMAIN_CALLDATA_DELIMITER, integratorId]); } +export function hasIntegratorIdAppended( + calldata: Hex, + integratorId: Hex, + options: { + isSwap?: boolean; + } = { + isSwap: false, + }, +): boolean { + const integratorIdSuffix = getIntegratorDataSuffix(integratorId); + const swapSuffix = SWAP_CALLDATA_MARKER; + // swap/approval first appends the integratorId, then the swap marker + const suffix = options.isSwap + ? concat([integratorIdSuffix, swapSuffix]) + : integratorIdSuffix; + + return calldata.endsWith(suffix.slice(2)); +} + export function isValidIntegratorId(integratorId: string) { return ( isHex(integratorId) && diff --git a/packages/sdk/test/unit/actions/getSwapQuote.test.ts b/packages/sdk/test/unit/actions/getSwapQuote.test.ts index 6e330e8..892bf8a 100644 --- a/packages/sdk/test/unit/actions/getSwapQuote.test.ts +++ b/packages/sdk/test/unit/actions/getSwapQuote.test.ts @@ -1,7 +1,9 @@ import { assert, assertType, describe, test } from "vitest"; import { type SwapApprovalApiResponse } from "../../../src/api/swap-approval.js"; import { getSwapQuote } from "../../../src/actions/getSwapQuote.js"; -import { parseEther } from "viem"; +import { Hex, parseEther } from "viem"; +import { hasIntegratorIdAppended } from "../../../src/utils/hex.js"; +import { mainnetTestClient } from "../../common/sdk.js"; // Mainnet WETH const inputToken = { @@ -68,4 +70,68 @@ describe("getSwapQuote", () => { assert(quote, "No swap quote returned for the provided parameters"); assertType(quote); }); + + test("swap approval calldata has integrator id appended", async () => { + const integratorId: Hex = "0xdead"; + + const quote = await getSwapQuote({ + amount: parseEther(inputAmount), + route: { + originChainId: 1, + inputToken: inputToken.address, + destinationChainId: 10, + outputToken: outputToken.address, + }, + depositor: testRecipient, + recipient: testRecipient, + integratorId, + }); + + assert(quote.swapTx, "swapTx missing in swap approval response"); + let data: Hex; + if ("eip712" in quote.swapTx) { + data = quote.swapTx.swapTx.data as Hex; + } else { + const simple = quote.swapTx as { data: string }; + data = simple.data as Hex; + } + + assert( + hasIntegratorIdAppended(data, integratorId, { + isSwap: true, + }), + "Expected swap calldata to have integrator id suffix", + ); + }); + + test("client injects integratorId when omitted", async () => { + const quote = await mainnetTestClient.getSwapQuote({ + amount: parseEther(inputAmount), + route: { + originChainId: 1, + inputToken: inputToken.address, + destinationChainId: 10, + outputToken: outputToken.address, + }, + depositor: testRecipient, + recipient: testRecipient, + // no integrator ID + }); + + assert(quote.swapTx, "swapTx missing in swap approval response"); + let data: Hex; + if ("eip712" in quote.swapTx) { + data = quote.swapTx.swapTx.data as Hex; + } else { + const simple = quote.swapTx as { data: string }; + data = simple.data as Hex; + } + + // Client default integratorId is 0xdead when not configured explicitly + const expectedIntegratorId: Hex = "0xdead"; + assert( + hasIntegratorIdAppended(data, expectedIntegratorId, { isSwap: true }), + "Expected swap calldata to include client's integrator id when omitted in params", + ); + }); });