Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/selfish-turkeys-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@across-protocol/app-sdk": patch
---

Add integratorId to getSwapQuote action
4 changes: 2 additions & 2 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down Expand Up @@ -63,4 +63,4 @@
"peerDependencies": {
"viem": "^2.31.2"
}
}
}
5 changes: 5 additions & 0 deletions packages/sdk/src/actions/getSwapQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
20 changes: 20 additions & 0 deletions packages/sdk/src/utils/hex.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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));
}

Comment on lines +27 to +36
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it safe to rather check calldata.includes(integratorIdSuffix.slice(2)) ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, both work but I think we can keep it as is since we use .concat on the swap marker after the integrator id as you mentioned.

export function isValidIntegratorId(integratorId: string) {
return (
isHex(integratorId) &&
Expand Down
68 changes: 67 additions & 1 deletion packages/sdk/test/unit/actions/getSwapQuote.test.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -68,4 +70,68 @@ describe("getSwapQuote", () => {
assert(quote, "No swap quote returned for the provided parameters");
assertType<SwapApprovalApiResponse>(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",
);
});
});