Skip to content
Closed
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
154 changes: 154 additions & 0 deletions .changeset/stale-yaks-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
---
"thirdweb": minor
---

Adds Bridge.Transfer module for direct token transfers:

```typescript
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";

const quote = await Bridge.Transfer.prepare({
chainId: 1,
tokenAddress: NATIVE_TOKEN_ADDRESS,
amount: toWei("0.01"),
sender: "0x...",
receiver: "0x...",
client: thirdwebClient,
});
```

This will return a quote that might look like:
```typescript
{
originAmount: 10000026098875381n,
destinationAmount: 10000000000000000n,
blockNumber: 22026509n,
timestamp: 1741730936680,
estimatedExecutionTimeMs: 1000
steps: [
{
originToken: {
chainId: 1,
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
symbol: "ETH",
name: "Ethereum",
decimals: 18,
priceUsd: 2000,
iconUri: "https://..."
},
destinationToken: {
chainId: 1,
address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
symbol: "ETH",
name: "Ethereum",
decimals: 18,
priceUsd: 2000,
iconUri: "https://..."
},
originAmount: 10000026098875381n,
destinationAmount: 10000000000000000n,
estimatedExecutionTimeMs: 1000
transactions: [
{
action: "approval",
id: "0x",
to: "0x...",
data: "0x...",
chainId: 1,
type: "eip1559"
},
{
action: "transfer",
to: "0x...",
value: 10000026098875381n,
data: "0x...",
chainId: 1,
type: "eip1559"
}
]
}
],
expiration: 1741730936680,
intent: {
chainId: 1,
tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
amount: 10000000000000000n,
sender: "0x...",
receiver: "0x..."
}
}
```

## Sending the transactions
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:
- Approvals will have the `approval` action specified. You can perform approvals with `sendAndConfirmTransaction`, then proceed to the next transaction.
- 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.
- If an `expiration` timestamp is provided, all transactions must be executed before that time to guarantee successful execution at the specified price.

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.

You can include arbitrary data to be included on any webhooks and status responses with the `purchaseData` option:

```ts
const quote = await Bridge.Transfer.prepare({
chainId: 1,
tokenAddress: NATIVE_TOKEN_ADDRESS,
amount: toWei("0.01"),
sender: "0x...",
receiver: "0x...",
purchaseData: {
reference: "payment-123",
metadata: {
note: "Transfer to Alice"
}
},
client: thirdwebClient,
});
```

## Fees
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.

For example, if you were to request a transfer with `feePayer` set to `receiver`:
```typescript
const quote = await Bridge.Transfer.prepare({
chainId: 1,
tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
amount: 100_000_000n, // 100 USDC
sender: "0x...",
receiver: "0x...",
feePayer: "receiver",
client: thirdwebClient,
});
```

The returned quote might look like:
```typescript
{
originAmount: 100_000_000n, // 100 USDC
destinationAmount: 99_970_000n, // 99.97 USDC
...
}
```

If you were to request a transfer with `feePayer` set to `sender`:
```typescript
const quote = await Bridge.Transfer.prepare({
chainId: 1,
tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
amount: 100_000_000n, // 100 USDC
sender: "0x...",
receiver: "0x...",
feePayer: "sender",
client: thirdwebClient,
});
```

The returned quote might look like:
```typescript
{
originAmount: 100_030_000n, // 100.03 USDC
destinationAmount: 100_000_000n, // 100 USDC
...
}
```
24 changes: 15 additions & 9 deletions packages/thirdweb/src/bridge/Buy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export async function quote(options: quote.Options): Promise<quote.Result> {
url.searchParams.set("destinationChainId", destinationChainId.toString());
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
url.searchParams.set("buyAmountWei", amount.toString());
url.searchParams.set("amount", amount.toString());
if (maxSteps) {
url.searchParams.set("maxSteps", maxSteps.toString());
}
Expand Down Expand Up @@ -199,7 +200,7 @@ export declare namespace quote {
* This will return a quote that might look like:
* ```typescript
* {
* originAmount: 10000026098875381n,
* originAmount: 2000030000n,
* destinationAmount: 1000000000000000000n,
* blockNumber: 22026509n,
* timestamp: 1741730936680,
Expand All @@ -208,11 +209,11 @@ export declare namespace quote {
* {
* originToken: {
* chainId: 1,
* address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* symbol: "ETH",
* name: "Ethereum",
* decimals: 18,
* priceUsd: 2000,
* address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
* symbol: "USDC",
* name: "USDC",
* decimals: 6,
* priceUsd: 1,
* iconUri: "https://..."
* },
* destinationToken: {
Expand All @@ -224,7 +225,7 @@ export declare namespace quote {
* priceUsd: 2000,
* iconUri: "https://..."
* },
* originAmount: 10000026098875381n,
* originAmount: 2000030000n,
* destinationAmount: 1000000000000000000n,
* estimatedExecutionTimeMs: 1000
* transactions: [
Expand All @@ -250,7 +251,7 @@ export declare namespace quote {
* expiration: 1741730936680,
* intent: {
* originChainId: 1,
* originTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
* destinationChainId: 10,
* destinationTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* amount: 1000000000000000000n
Expand Down Expand Up @@ -342,7 +343,8 @@ export async function prepare(
"Content-Type": "application/json",
},
body: stringify({
buyAmountWei: amount.toString(),
buyAmountWei: amount.toString(), // legacy
amount: amount.toString(),
originChainId: originChainId.toString(),
originTokenAddress,
destinationChainId: destinationChainId.toString(),
Expand Down Expand Up @@ -382,6 +384,8 @@ export async function prepare(
destinationChainId,
destinationTokenAddress,
amount,
sender,
receiver,
},
};
}
Expand All @@ -407,6 +411,8 @@ export declare namespace prepare {
destinationChainId: number;
destinationTokenAddress: ox__Address.Address;
amount: bigint;
sender: ox__Address.Address;
receiver: ox__Address.Address;
purchaseData?: unknown;
};
};
Expand Down
26 changes: 16 additions & 10 deletions packages/thirdweb/src/bridge/Sell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export async function quote(options: quote.Options): Promise<quote.Result> {
url.searchParams.set("destinationChainId", destinationChainId.toString());
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
url.searchParams.set("sellAmountWei", amount.toString());
url.searchParams.set("amount", amount.toString());
if (typeof maxSteps !== "undefined") {
url.searchParams.set("maxSteps", maxSteps.toString());
}
Expand Down Expand Up @@ -190,7 +191,7 @@ export declare namespace quote {
* This will return a quote that might look like:
* ```typescript
* {
* originAmount: 1000000000000000000n,
* originAmount: 2000000000n,
* destinationAmount: 9980000000000000000n,
* blockNumber: 22026509n,
* timestamp: 1741730936680,
Expand All @@ -199,11 +200,11 @@ export declare namespace quote {
* {
* originToken: {
* chainId: 1,
* address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* symbol: "ETH",
* name: "Ethereum",
* decimals: 18,
* priceUsd: 2000,
* address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
* symbol: "USDC",
* name: "USDC",
* decimals: 6,
* priceUsd: 1,
* iconUri: "https://..."
* },
* destinationToken: {
Expand All @@ -215,7 +216,7 @@ export declare namespace quote {
* priceUsd: 2000,
* iconUri: "https://..."
* },
* originAmount: 1000000000000000000n,
* originAmount: 2000000000n,
* destinationAmount: 9980000000000000000n,
* estimatedExecutionTimeMs: 1000
* }
Expand All @@ -241,10 +242,10 @@ export declare namespace quote {
* expiration: 1741730936680,
* intent: {
* originChainId: 1,
* originTokenAddress: NATIVE_TOKEN_ADDRESS,
* originTokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
* destinationChainId: 10,
* destinationTokenAddress: NATIVE_TOKEN_ADDRESS,
* amount: 1000000000000000000n
* destinationTokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
* amount: 2000000000n
* }
* }
* ```
Expand Down Expand Up @@ -334,6 +335,7 @@ export async function prepare(
},
body: stringify({
sellAmountWei: amount.toString(),
amount: amount.toString(),
originChainId: originChainId.toString(),
originTokenAddress,
destinationChainId: destinationChainId.toString(),
Expand Down Expand Up @@ -374,6 +376,8 @@ export async function prepare(
destinationChainId,
destinationTokenAddress,
amount,
sender,
receiver,
purchaseData,
},
};
Expand All @@ -400,6 +404,8 @@ export declare namespace prepare {
destinationChainId: number;
destinationTokenAddress: ox__Address.Address;
amount: bigint;
sender: ox__Address.Address;
receiver: ox__Address.Address;
purchaseData?: unknown;
};
};
Expand Down
76 changes: 76 additions & 0 deletions packages/thirdweb/src/bridge/Transfer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { toWei } from "src/utils/units.js";
import { describe, expect, it } from "vitest";
import { TEST_CLIENT } from "~test/test-clients.js";
import * as Transfer from "./Transfer.js";

describe.runIf(process.env.TW_SECRET_KEY)("Bridge.Transfer.prepare", () => {
it("should get a valid prepared quote", async () => {
const quote = await Transfer.prepare({
chainId: 1,
tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
amount: toWei("0.01"),
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
client: TEST_CLIENT,
purchaseData: {
reference: "test-transfer",
},
});

expect(quote).toBeDefined();
expect(quote.intent.amount).toEqual(toWei("0.01"));
for (const step of quote.steps) {
expect(step.transactions.length).toBeGreaterThan(0);
}
expect(quote.intent).toBeDefined();
});

it("should surface any errors", async () => {
await expect(
Transfer.prepare({
chainId: 444, // Invalid chain ID
tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
amount: toWei("1000000000"),
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
client: TEST_CLIENT,
}),
).rejects.toThrowError();
});

it("should support the feePayer option", async () => {
const senderQuote = await Transfer.prepare({
chainId: 1,
tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
amount: toWei("0.01"),
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
feePayer: "sender",
client: TEST_CLIENT,
});

expect(senderQuote).toBeDefined();
expect(senderQuote.intent.feePayer).toBe("sender");

const receiverQuote = await Transfer.prepare({
chainId: 1,
tokenAddress: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
amount: toWei("0.01"),
sender: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
receiver: "0x2a4f24F935Eb178e3e7BA9B53A5Ee6d8407C0709",
feePayer: "receiver",
client: TEST_CLIENT,
});

expect(receiverQuote).toBeDefined();
expect(receiverQuote.intent.feePayer).toBe("receiver");

// When receiver pays fees, the destination amount should be less than the requested amount
expect(receiverQuote.destinationAmount).toBeLessThan(toWei("0.01"));

// When sender pays fees, the origin amount should be more than the requested amount
// and the destination amount should equal the requested amount
expect(senderQuote.originAmount).toBeGreaterThan(toWei("0.01"));
expect(senderQuote.destinationAmount).toEqual(toWei("0.01"));
});
});
Loading
Loading