Skip to content

Commit 6744d0a

Browse files
gregfromstlClaude
and
Claude
committed
[SDK] Feature: Add Bridge.Transfer module
- Adds new Transfer module for direct token transfers - Implements prepare function for getting transfer quotes and transactions - Adds tests for the Transfer module 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent efc4687 commit 6744d0a

File tree

2 files changed

+346
-0
lines changed

2 files changed

+346
-0
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { toWei } from "src/utils/units.js";
2+
import { describe, expect, it } from "vitest";
3+
import { TEST_CLIENT } from "~test/test-clients.js";
4+
import * as Transfer from "./Transfer.js";
5+
6+
describe.runIf(process.env.TW_SECRET_KEY)("Bridge.Transfer.prepare", () => {
7+
it("should get a valid prepared quote", async () => {
8+
const quote = await Transfer.prepare({
9+
chainId: 1,
10+
tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
11+
amount: toWei("0.01"),
12+
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
13+
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
14+
client: TEST_CLIENT,
15+
purchaseData: {
16+
reference: "test-transfer",
17+
},
18+
});
19+
20+
expect(quote).toBeDefined();
21+
expect(quote.intent.amount).toEqual(toWei("0.01"));
22+
for (const step of quote.steps) {
23+
expect(step.transactions.length).toBeGreaterThan(0);
24+
}
25+
expect(quote.intent).toBeDefined();
26+
});
27+
28+
it("should surface any errors", async () => {
29+
await expect(
30+
Transfer.prepare({
31+
chainId: 444, // Invalid chain ID
32+
tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
33+
amount: toWei("1000000000"),
34+
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
35+
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
36+
client: TEST_CLIENT,
37+
}),
38+
).rejects.toThrowError();
39+
});
40+
41+
it("should support the feePayer option", async () => {
42+
const senderQuote = await Transfer.prepare({
43+
chainId: 1,
44+
tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
45+
amount: toWei("0.01"),
46+
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
47+
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
48+
feePayer: "sender",
49+
client: TEST_CLIENT,
50+
});
51+
52+
expect(senderQuote).toBeDefined();
53+
expect(senderQuote.intent.feePayer).toBe("sender");
54+
55+
const receiverQuote = await Transfer.prepare({
56+
chainId: 1,
57+
tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
58+
amount: toWei("0.01"),
59+
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
60+
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
61+
feePayer: "receiver",
62+
client: TEST_CLIENT,
63+
});
64+
65+
expect(receiverQuote).toBeDefined();
66+
expect(receiverQuote.intent.feePayer).toBe("receiver");
67+
68+
// When receiver pays fees, the destination amount should be less than the requested amount
69+
expect(receiverQuote.destinationAmount).toBeLessThan(toWei("0.01"));
70+
71+
// When sender pays fees, the origin amount should be more than the requested amount
72+
// and the destination amount should equal the requested amount
73+
expect(senderQuote.originAmount).toBeGreaterThan(toWei("0.01"));
74+
expect(senderQuote.destinationAmount).toEqual(toWei("0.01"));
75+
});
76+
});
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import type { Address as ox__Address } from "ox";
2+
import { defineChain } from "../chains/utils.js";
3+
import type { ThirdwebClient } from "../client/client.js";
4+
import { getClientFetch } from "../utils/fetch.js";
5+
import { stringify } from "../utils/json.js";
6+
import { UNIVERSAL_BRIDGE_URL } from "./constants.js";
7+
import type { PreparedQuote } from "./types/Quote.js";
8+
9+
/**
10+
* Prepares a **finalized** Universal Bridge quote for the provided transfer request with transaction data.
11+
*
12+
* @example
13+
* ```typescript
14+
* import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
15+
*
16+
* const quote = await Bridge.Transfer.prepare({
17+
* chainId: 1,
18+
* tokenAddress: NATIVE_TOKEN_ADDRESS,
19+
* amount: toWei("0.01"),
20+
* sender: "0x...",
21+
* receiver: "0x...",
22+
* client: thirdwebClient,
23+
* });
24+
* ```
25+
*
26+
* This will return a quote that might look like:
27+
* ```typescript
28+
* {
29+
* originAmount: 10000026098875381n,
30+
* destinationAmount: 10000000000000000n,
31+
* blockNumber: 22026509n,
32+
* timestamp: 1741730936680,
33+
* estimatedExecutionTimeMs: 1000
34+
* steps: [
35+
* {
36+
* originToken: {
37+
* chainId: 1,
38+
* address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
39+
* symbol: "ETH",
40+
* name: "Ethereum",
41+
* decimals: 18,
42+
* priceUsd: 2000,
43+
* iconUri: "https://..."
44+
* },
45+
* destinationToken: {
46+
* chainId: 1,
47+
* address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
48+
* symbol: "ETH",
49+
* name: "Ethereum",
50+
* decimals: 18,
51+
* priceUsd: 2000,
52+
* iconUri: "https://..."
53+
* },
54+
* originAmount: 10000026098875381n,
55+
* destinationAmount: 10000000000000000n,
56+
* estimatedExecutionTimeMs: 1000
57+
* transactions: [
58+
* {
59+
* action: "approval",
60+
* id: "0x",
61+
* to: "0x...",
62+
* data: "0x...",
63+
* chainId: 1,
64+
* type: "eip1559"
65+
* },
66+
* {
67+
* action: "transfer",
68+
* to: "0x...",
69+
* value: 10000026098875381n,
70+
* data: "0x...",
71+
* chainId: 1,
72+
* type: "eip1559"
73+
* }
74+
* ]
75+
* }
76+
* ],
77+
* expiration: 1741730936680,
78+
* intent: {
79+
* chainId: 1,
80+
* tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
81+
* amount: 10000000000000000n,
82+
* sender: "0x...",
83+
* receiver: "0x..."
84+
* }
85+
* }
86+
* ```
87+
*
88+
* ## Sending the transactions
89+
* The `transactions` array is a series of [ox](https://oxlib.sh) EIP-1559 transactions that must be executed one after the other in order to fulfill the complete route. There are a few things to keep in mind when executing these transactions:
90+
* - Approvals will have the `approval` action specified. You can perform approvals with `sendAndConfirmTransaction`, then proceed to the next transaction.
91+
* - All transactions are assumed to be executed by the `sender` address, regardless of which chain they are on. The final transaction will use the `receiver` as the recipient address.
92+
* - If an `expiration` timestamp is provided, all transactions must be executed before that time to guarantee successful execution at the specified price.
93+
*
94+
* NOTE: To get the status of each non-approval transaction, use `Bridge.status` rather than checking for transaction inclusion. This function will ensure full completion of the transfer.
95+
*
96+
* You can access this functions input and output types with `Transfer.prepare.Options` and `Transfer.prepare.Result`, respectively.
97+
*
98+
* You can include arbitrary data to be included on any webhooks and status responses with the `purchaseData` option.
99+
*
100+
* ```ts
101+
* const quote = await Bridge.Transfer.prepare({
102+
* chainId: 1,
103+
* tokenAddress: NATIVE_TOKEN_ADDRESS,
104+
* amount: toWei("0.01"),
105+
* sender: "0x...",
106+
* receiver: "0x...",
107+
* purchaseData: {
108+
* reference: "payment-123",
109+
* metadata: {
110+
* note: "Transfer to Alice"
111+
* }
112+
* },
113+
* client: thirdwebClient,
114+
* });
115+
* ```
116+
*
117+
* ## Fees
118+
* There may be fees associated with the transfer. These fees are paid by the `feePayer` address, which defaults to the `sender` address. You can specify a different address with the `feePayer` option. If you do not specify an option or explicitly specify `sender`, the fees will be added to the input amount. If you specify the `receiver` as the fee payer the fees will be subtracted from the destination amount.
119+
*
120+
* For example, if you were to request a transfer with `feePayer` set to `receiver`:
121+
* ```typescript
122+
* const quote = await Bridge.Transfer.prepare({
123+
* chainId: 1,
124+
* tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
125+
* amount: 100_000_000n, // 100 USDC
126+
* sender: "0x...",
127+
* receiver: "0x...",
128+
* feePayer: "receiver",
129+
* client: thirdwebClient,
130+
* });
131+
* ```
132+
*
133+
* The returned quote might look like:
134+
* ```typescript
135+
* {
136+
* originAmount: 100_000_000n, // 100 USDC
137+
* destinationAmount: 99_970_000n, // 99.97 USDC
138+
* ...
139+
* }
140+
* ```
141+
*
142+
* If you were to request a transfer with `feePayer` set to `sender`:
143+
* ```typescript
144+
* const quote = await Bridge.Transfer.prepare({
145+
* chainId: 1,
146+
* tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
147+
* amount: 100_000_000n, // 100 USDC
148+
* sender: "0x...",
149+
* receiver: "0x...",
150+
* feePayer: "sender",
151+
* client: thirdwebClient,
152+
* });
153+
* ```
154+
*
155+
* The returned quote might look like:
156+
* ```typescript
157+
* {
158+
* originAmount: 100_030_000n, // 100.03 USDC
159+
* destinationAmount: 100_000_000n, // 100 USDC
160+
* ...
161+
* }
162+
* ```
163+
*
164+
* @param options - The options for the quote.
165+
* @param options.chainId - The chain ID of the token.
166+
* @param options.tokenAddress - The address of the token.
167+
* @param options.amount - The amount of the token to transfer.
168+
* @param options.sender - The address of the sender.
169+
* @param options.receiver - The address of the recipient.
170+
* @param options.purchaseData - Arbitrary data to be passed to the transfer function and included with any webhooks or status calls.
171+
* @param options.client - Your thirdweb client.
172+
* @param [options.feePayer] - The address that will pay the fees for the transfer. If not specified, the sender will be used. Values can be "sender" or "receiver".
173+
*
174+
* @returns A promise that resolves to a finalized quote and transactions for the requested transfer.
175+
*
176+
* @throws Will throw an error if there is an issue fetching the quote.
177+
* @bridge Transfer
178+
* @beta
179+
*/
180+
export async function prepare(
181+
options: prepare.Options,
182+
): Promise<prepare.Result> {
183+
const {
184+
chainId,
185+
tokenAddress,
186+
sender,
187+
receiver,
188+
client,
189+
amount,
190+
purchaseData,
191+
feePayer,
192+
} = options;
193+
194+
const clientFetch = getClientFetch(client);
195+
const url = new URL(`${UNIVERSAL_BRIDGE_URL}/transfer/prepare`);
196+
197+
const response = await clientFetch(url.toString(), {
198+
method: "POST",
199+
headers: {
200+
"Content-Type": "application/json",
201+
},
202+
body: stringify({
203+
transferAmountWei: amount.toString(), // legacy
204+
amount: amount.toString(),
205+
chainId: chainId.toString(),
206+
tokenAddress,
207+
sender,
208+
receiver,
209+
purchaseData,
210+
feePayer,
211+
}),
212+
});
213+
if (!response.ok) {
214+
const errorJson = await response.json();
215+
throw new Error(
216+
`${errorJson.code} | ${errorJson.message} - ${errorJson.correlationId}`,
217+
);
218+
}
219+
220+
const { data }: { data: PreparedQuote } = await response.json();
221+
return {
222+
originAmount: BigInt(data.originAmount),
223+
destinationAmount: BigInt(data.destinationAmount),
224+
blockNumber: data.blockNumber ? BigInt(data.blockNumber) : undefined,
225+
timestamp: data.timestamp,
226+
estimatedExecutionTimeMs: data.estimatedExecutionTimeMs,
227+
steps: data.steps.map((step) => ({
228+
...step,
229+
transactions: step.transactions.map((transaction) => ({
230+
...transaction,
231+
value: transaction.value ? BigInt(transaction.value) : undefined,
232+
client,
233+
chain: defineChain(transaction.chainId),
234+
})),
235+
})),
236+
intent: {
237+
chainId,
238+
tokenAddress,
239+
amount,
240+
sender,
241+
receiver,
242+
feePayer,
243+
},
244+
};
245+
}
246+
247+
export declare namespace prepare {
248+
type Options = {
249+
chainId: number;
250+
tokenAddress: ox__Address.Address;
251+
sender: ox__Address.Address;
252+
receiver: ox__Address.Address;
253+
amount: bigint;
254+
client: ThirdwebClient;
255+
purchaseData?: unknown;
256+
feePayer?: "sender" | "receiver";
257+
};
258+
259+
type Result = PreparedQuote & {
260+
intent: {
261+
chainId: number;
262+
tokenAddress: ox__Address.Address;
263+
amount: bigint;
264+
sender: ox__Address.Address;
265+
receiver: ox__Address.Address;
266+
purchaseData?: unknown;
267+
feePayer?: "sender" | "receiver";
268+
};
269+
};
270+
}

0 commit comments

Comments
 (0)