Skip to content

Commit d4f5a9d

Browse files
committed
[Portal] Add cross-chain swapping guide and add bridge submodules (#6571)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the documentation and functionality surrounding the `thirdweb` Universal Bridge, particularly for cross-chain swapping and related features. It updates guides, introduces new functionalities, and improves the clarity of the code. ### Detailed summary - Updated `yarn` command in `page.mdx` for installing `thirdweb`. - Renamed the guide from "Build a Custom Experience" to "Build a Custom Onramp Experience" in `sidebar.tsx`. - Added a new guide for "Cross-Chain Swapping" in `page.mdx`. - Enhanced tag filtering in `slugs.ts` to include `@bridge`. - Updated comments in `Buy.ts` and `Sell.ts` to specify `@bridge Buy` and `@bridge Sell`. - Improved sidebar link generation in `getSidebarLinkGroups.ts` to include `@bridge` links. - Added comprehensive steps for cross-chain swapping in the new guide, including installation, obtaining quotes, preparing transactions, and checking swap status. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 3b827ea commit d4f5a9d

File tree

8 files changed

+298
-10
lines changed

8 files changed

+298
-10
lines changed

apps/portal/src/app/connect/pay/guides/build-a-custom-experience/page.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ In this guide, we'll show you how to purchase 0.01 Base ETH from USD in Typescri
3636
<Step title='Install the Connect SDK'>
3737
<InstallTabs
3838
npm="npm i thirdweb"
39-
yarn="yarn install thirdweb"
39+
yarn="yarn add thirdweb"
4040
pnpm="pnpm i thirdweb"
4141
/>
4242
<Step title='Get Your Client ID'>
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import {
2+
createMetadata,
3+
Callout,
4+
DocImage,
5+
InstallTabs,
6+
Steps,
7+
Step,
8+
} from "@doc";
9+
import OnrampStepOne from "../../assets/avax-to-usd.png";
10+
11+
export const metadata = createMetadata({
12+
image: {
13+
title: "thirdweb Universal Bridge - Cross-Chain Swapping",
14+
icon: "thirdweb",
15+
},
16+
title: "thirdweb Universal Bridge - Cross-Chain Swapping | thirdweb",
17+
description:
18+
"Learn how to build a custom cross-chain swapping experience with thirdweb Universal Bridge.",
19+
});
20+
21+
# Leverage Cross-Chain Swaps with Universal Bridge
22+
23+
Learn how to enable your users to swap from any asset to any other with thirdweb's Universal Bridge.
24+
25+
In this guide, we'll show you how to purchase 10 USDC on Optimism in Typescript.
26+
27+
---
28+
29+
<Steps>
30+
<Step title='Install the Connect SDK'>
31+
<InstallTabs
32+
npm="npm i thirdweb"
33+
yarn="yarn add thirdweb"
34+
pnpm="pnpm i thirdweb"
35+
/>
36+
<Step title='Get Your Client ID'>
37+
38+
Log in to the [thirdweb dashboard](https://thirdweb.com/team). Click on Create New > Project to get your **Client ID**. You'll need your Client ID to interact with the Connect SDK.
39+
40+
</Step>
41+
</Step>
42+
<Step title='Find available routes'>
43+
Before your user can select which tokens they'd like to swap, they'll need to find available routes.
44+
45+
You can do this using the `routes` function in our `Bridge` namespace. You or your users can filter by origin and destination chains and/or tokens. Pagination is also built-in to the function.
46+
47+
```tsx
48+
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
49+
50+
// Get all available routes
51+
const allRoutes = await Bridge.routes({
52+
client: thirdwebClient,
53+
});
54+
55+
// Filter routes for a specific token or chain
56+
const filteredRoutes = await Bridge.routes({
57+
originChainId: 1, // From Ethereum
58+
originTokenAddress: NATIVE_TOKEN_ADDRESS,
59+
destinationChainId: 10, // To Optimism
60+
client: thirdwebClient,
61+
});
62+
63+
// Paginate through routes
64+
const paginatedRoutes = await Bridge.routes({
65+
limit: 10,
66+
offset: 0,
67+
client: thirdwebClient,
68+
});
69+
```
70+
71+
This will return an array of `Route` objects, which will include information such as `symbol`, `address`, and `chainId` for both the origin and destination tokens.
72+
73+
</Step>
74+
<Step title='Get a quote'>
75+
Once you know which routes are available, you can retrieve a quote to show the user how much they can expect to pay for a given swap.
76+
77+
In this example, we'll use the `Buy.quote` function to get a quote for buying 10 USDC on Optimism for Base ETH.
78+
79+
<Callout variant="info">
80+
The `Buy` namespace is purpose-built for when you want to obtain a specific amount of the output token.
81+
If you have a specific input amount and are flexible on the output amount, you can use the `Sell` namespace.
82+
Learn more about sells [here](https://portal.thirdweb.com/references/typescript/v5/sell/prepare).
83+
</Callout>
84+
85+
Quote allows us to get an expected amount before the user has connected their wallet. This quote won't come with executable transactions, and won't be a guaranteed price.
86+
87+
88+
```tsx
89+
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
90+
91+
const buyQuote = await Bridge.Buy.quote({
92+
originChainId: 8453, // Base
93+
originTokenAddress: NATIVE_TOKEN_ADDRESS,
94+
destinationChainId: 10, // Optimism
95+
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism USDC
96+
buyAmountWei: 10000000n, // 10 USDC
97+
client: thirdwebClient,
98+
});
99+
100+
console.log(
101+
`To get ${buyQuote.destinationAmount} wei on destination chain, you need to pay ${buyQuote.originAmount} wei`,
102+
);
103+
```
104+
105+
This will return a `Quote` object, which will include the `originAmount` and `destinationAmount` in wei, along with some more useful information about the predicted quote.
106+
</Step>
107+
<Step title='Get the prepared transaction'>
108+
Now that we know how much the user can expect to pay, we can have them connect their wallet and execute the swap.
109+
110+
To get a prepared quote, we'll use the `Buy.prepare` function. The key difference with this function is it requires a `sender` and `receiver` to be specified.
111+
112+
```tsx
113+
import { Bridge, NATIVE_TOKEN_ADDRESS } from "thirdweb";
114+
115+
const preparedBuy = await Bridge.Buy.prepare({
116+
originChainId: 8453, // Base
117+
originTokenAddress: NATIVE_TOKEN_ADDRESS,
118+
destinationChainId: 10, // Optimism
119+
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism USDC
120+
buyAmountWei: 10000000n, // 10 USDC
121+
sender: "0x...", // Your user's wallet address
122+
receiver: "0x...", // Recipient address (can be the same as sender)
123+
client: thirdwebClient,
124+
});
125+
126+
// The prepared quote contains the transactions you need to execute
127+
console.log(`Transactions to execute: ${preparedBuy.transactions.length}`);
128+
```
129+
130+
This will return a `PreparedQuote` object. It will look very similar to the `Quote` you received in the previous step, but it will include a `transactions` array. This array will contain the transactions you need to execute to complete the swap.
131+
</Step>
132+
<Step title='Execute the swap'>
133+
To execute the swap, we'll need to send all transactions in the `transactions` array one after the other.
134+
135+
<Callout variant="warning">
136+
Currently, the `transactions` array does not include approvals. You'll need to execute any necessary approvals prior to executing the swap transactions.
137+
</Callout>
138+
139+
```tsx
140+
import { sendTransaction, waitForReceipt } from "thirdweb";
141+
142+
const preparedBuy = await Bridge.Buy.prepare({
143+
originChainId: 8453, // Base
144+
originTokenAddress: NATIVE_TOKEN_ADDRESS,
145+
destinationChainId: 10, // Optimism
146+
destinationTokenAddress: "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", // Optimism USDC
147+
buyAmountWei: 10000000n, // 10 USDC
148+
sender: "0x...", // Your user's wallet address
149+
receiver: "0x...", // Recipient address (can be the same as sender)
150+
client: thirdwebClient,
151+
});
152+
153+
};
154+
155+
for (const transaction of preparedBuy.transactions) {
156+
const tx = prepareTransaction({
157+
to: transaction.to as string,
158+
value: BigInt(transaction.value ?? 0n),
159+
data: transaction.data,
160+
chain: defineChain(transaction.chainId),
161+
client
162+
});
163+
164+
const result = await sendAndConfirmTransaction({ transaction: tx, account });
165+
let swapStatus;
166+
do {
167+
swapStatus = await Bridge.status({
168+
transactionHash: result.transactionHash,
169+
client,
170+
});
171+
} while (swapStatus.status !== "COMPLETED");
172+
};
173+
```
174+
175+
<Callout variant="info">
176+
The returned transactions follow the [Ox](https://oxlib.sh/) standard [TransactionEnvelopeEip1559](https://oxlib.sh/guides/transaction-envelopes)
177+
format. This is a simple transaction with `data`, `to`, `value`, and `chainId` properties. **Gas is not included in the transaction, and should
178+
be estimated separately if necessary.** Normally, the thirdweb SDK will handle this for you.
179+
</Callout>
180+
181+
</Step>
182+
<Step title='Getting the swap status'>
183+
You'll notice in the previous step we call `Bridge.status` to get the status of the swap. We can get the status of any past swap using just the transaction hash and chain ID of its origin transaction.
184+
185+
When sending the transactions in a prepared quote, you **must** use `Bridge.status` to get the `COMPLETED` status before moving on to the next transaction. This is because `status` waits for both the origin
186+
and destination transactions to complete. Waiting for the origin transaction receipt is not sufficient since the funds might not have arrived on the destination chain yet.
187+
188+
The `status` will also return all transactions (origin and destination) involved in the swap.
189+
```tsx
190+
import { Bridge } from "thirdweb";
191+
192+
// Check the status of a bridge transaction
193+
const bridgeStatus = await Bridge.status({
194+
transactionHash:
195+
"0xe199ef82a0b6215221536e18ec512813c1aa10b4f5ed0d4dfdfcd703578da56d",
196+
chainId: 8453, // The chain ID where the transaction was initiated
197+
client: thirdwebClient,
198+
});
199+
200+
// The status will be one of: "COMPLETED", "PENDING", "FAILED", or "NOT_FOUND"
201+
if (bridgeStatus.status === "completed") {
202+
console.log(`
203+
Bridge completed!
204+
Sent: ${bridgeStatus.originAmount} wei on chain ${bridgeStatus.originChainId}
205+
Received: ${bridgeStatus.destinationAmount} wei on chain ${bridgeStatus.destinationChainId}
206+
`);
207+
} else if (bridgeStatus.status === "pending") {
208+
console.log("Bridge transaction is still pending...");
209+
} else {
210+
console.log("Bridge transaction failed");
211+
}
212+
```
213+
</Step>
214+
</Steps>

apps/portal/src/app/connect/sidebar.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,9 +314,13 @@ export const sidebar: SideBar = {
314314
href: `${paySlug}/guides/accept-direct-payments`,
315315
},
316316
{
317-
name: "Build a Custom Experience",
317+
name: "Build a Custom Onramp Experience",
318318
href: `${paySlug}/guides/build-a-custom-experience`,
319319
},
320+
{
321+
name: "Cross-Chain Swapping",
322+
href: `${paySlug}/guides/cross-chain-swapping`,
323+
},
320324
],
321325
},
322326
{

apps/portal/src/app/references/components/TDoc/utils/getSidebarLinkgroups.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ export function getSidebarLinkGroups(doc: TransformedDoc, path: string) {
167167
return tag === "@modules";
168168
});
169169

170+
const bridge = docs.filter((d) => {
171+
const [tag] = getCustomTag(d) || [];
172+
return tag === "@bridge";
173+
});
174+
170175
// sort extensions into their own groups
171176
if (extensions.length) {
172177
const extensionGroups = extensions.reduce(
@@ -247,9 +252,68 @@ export function getSidebarLinkGroups(doc: TransformedDoc, path: string) {
247252
}
248253
}
249254

255+
if (bridge.length) {
256+
const bridgeGroups = bridge.reduce(
257+
(acc, d) => {
258+
const [, moduleName] = getCustomTag(d) || [];
259+
if (moduleName) {
260+
if (!acc[moduleName]) {
261+
acc[moduleName] = [];
262+
}
263+
acc[moduleName]?.push(d);
264+
}
265+
return acc;
266+
},
267+
{} as Record<string, SomeDoc[]>,
268+
);
269+
const bridgeLinkGroups: {
270+
name: string;
271+
href: string;
272+
links?: { name: string; href?: string }[];
273+
}[] = Object.entries(bridgeGroups)
274+
.filter(([namespaceName]) => namespaceName.toLowerCase() !== "common")
275+
.map(([namespaceName, docs]) => {
276+
const links = docs.map((d) => ({
277+
name: d.name,
278+
href: getLink(`${path}/${namespaceName.toLowerCase()}/${d.name}`),
279+
}));
280+
return {
281+
name: namespaceName,
282+
href: "",
283+
links,
284+
};
285+
});
286+
287+
// Add the top-level functions
288+
for (const group of Object.entries(bridgeGroups).filter(
289+
([namespaceName]) => namespaceName.toLowerCase() === "common",
290+
)) {
291+
const docs = group[1];
292+
for (const doc of docs) {
293+
bridgeLinkGroups.push({
294+
name: doc.name,
295+
href: getLink(`${path}/${doc.name}`),
296+
});
297+
}
298+
}
299+
300+
if (!linkGroups.find((group) => group.name === name)) {
301+
linkGroups.push({
302+
name: name,
303+
href: getLink(`${path}/${key}`),
304+
links: [{ name: "Universal Bridge", links: bridgeLinkGroups }],
305+
isCollapsible: false,
306+
});
307+
} else {
308+
linkGroups
309+
.find((group) => group.name === name)
310+
?.links.push({ name: "Universal Bridge", links: bridgeLinkGroups });
311+
}
312+
}
313+
250314
const nonExtensions = docs.filter((d) => {
251315
const [tag] = getCustomTag(d) || [];
252-
return tag !== "@extension" && tag !== "@modules";
316+
return tag !== "@extension" && tag !== "@modules" && tag !== "@bridge";
253317
});
254318

255319
// sort into groups

apps/portal/src/app/references/components/TDoc/utils/slugs.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,17 @@ export function getSlugToDocMap(doc: TransformedDoc) {
3636
const extensionBlockTag = v.signatures
3737
?.find((s) =>
3838
s.blockTags?.some(
39-
(tag) => tag.tag === "@extension" || tag.tag === "@modules",
39+
(tag) =>
40+
tag.tag === "@extension" ||
41+
tag.tag === "@modules" ||
42+
tag.tag === "@bridge",
4043
),
4144
)
4245
?.blockTags?.find(
43-
(tag) => tag.tag === "@extension" || tag.tag === "@modules",
46+
(tag) =>
47+
tag.tag === "@extension" ||
48+
tag.tag === "@modules" ||
49+
tag.tag === "@bridge",
4450
);
4551

4652
if (extensionBlockTag) {

packages/thirdweb/src/bridge/Buy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import type { PreparedQuote, Quote } from "./types/Quote.js";
5555
* @returns A promise that resolves to a non-finalized quote for the requested buy.
5656
*
5757
* @throws Will throw an error if there is an issue fetching the quote.
58-
* @bridge
58+
* @bridge Buy
5959
* @beta
6060
*/
6161
export async function quote(options: quote.Options): Promise<quote.Result> {
@@ -188,7 +188,7 @@ export declare namespace quote {
188188
* @returns A promise that resolves to a non-finalized quote for the requested buy.
189189
*
190190
* @throws Will throw an error if there is an issue fetching the quote.
191-
* @bridge
191+
* @bridge Buy
192192
* @beta
193193
*/
194194
export async function prepare(

packages/thirdweb/src/bridge/Sell.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import type { PreparedQuote, Quote } from "./types/Quote.js";
5555
* @returns A promise that resolves to a non-finalized quote for the requested sell.
5656
*
5757
* @throws Will throw an error if there is an issue fetching the quote.
58-
* @bridge
58+
* @bridge Sell
5959
* @beta
6060
*/
6161
export async function quote(options: quote.Options): Promise<quote.Result> {
@@ -188,7 +188,7 @@ export declare namespace quote {
188188
* @returns A promise that resolves to a non-finalized quote for the requested buy.
189189
*
190190
* @throws Will throw an error if there is an issue fetching the quote.
191-
* @bridge
191+
* @bridge Sell
192192
* @beta
193193
*/
194194
export async function prepare(

packages/thirdweb/src/bridge/Status.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { Status } from "./types/Status.js";
77
/**
88
* Retrieves a Universal Bridge quote for the provided sell intent. The quote will specify the expected `destinationAmount` that will be received in exchange for the specified `originAmount`, which is specified with the `sellAmountWei` option.
99
*
10-
+ * The returned status will include both the origin and destination transactions and any finalized amounts for the route.
10+
* The returned status will include both the origin and destination transactions and any finalized amounts for the route.
1111
*
1212
* @example
1313
* ```typescript

0 commit comments

Comments
 (0)