Skip to content

Commit c0429e1

Browse files
committed
feat: auto-add unsupported tokens
1 parent 90fef12 commit c0429e1

File tree

5 files changed

+113
-47
lines changed

5 files changed

+113
-47
lines changed

packages/thirdweb/src/bridge/Token.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,84 @@ export declare namespace tokens {
182182
*/
183183
type Result = Token[];
184184
}
185+
186+
/**
187+
* Adds a token to the Universal Bridge for indexing.
188+
*
189+
* This function requests the Universal Bridge to index a specific token on a given chain.
190+
* Once indexed, the token will be available for cross-chain operations.
191+
*
192+
* @example
193+
* ```typescript
194+
* import { Bridge } from "thirdweb";
195+
*
196+
* // Add a token for indexing
197+
* const result = await Bridge.add({
198+
* client: thirdwebClient,
199+
* chainId: 1,
200+
* tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
201+
* });
202+
* ```
203+
*
204+
* @param options - The options for adding a token.
205+
* @param options.client - Your thirdweb client.
206+
* @param options.chainId - The chain ID where the token is deployed.
207+
* @param options.tokenAddress - The contract address of the token to add.
208+
*
209+
* @returns A promise that resolves when the token has been successfully submitted for indexing.
210+
*
211+
* @throws Will throw an error if there is an issue adding the token.
212+
* @bridge
213+
* @beta
214+
*/
215+
export async function add(options: add.Options): Promise<add.Result> {
216+
const { client, chainId, tokenAddress } = options;
217+
218+
const clientFetch = getClientFetch(client);
219+
const url = `${getThirdwebBaseUrl("bridge")}/v1/tokens`;
220+
221+
const requestBody = {
222+
chainId,
223+
tokenAddress,
224+
};
225+
226+
const response = await clientFetch(url, {
227+
method: "POST",
228+
headers: {
229+
"Content-Type": "application/json",
230+
},
231+
body: JSON.stringify(requestBody),
232+
});
233+
234+
if (!response.ok) {
235+
const errorJson = await response.json();
236+
throw new ApiError({
237+
code: errorJson.code || "UNKNOWN_ERROR",
238+
message: errorJson.message || response.statusText,
239+
correlationId: errorJson.correlationId || undefined,
240+
statusCode: response.status,
241+
});
242+
}
243+
244+
const { data }: { data: Token } = await response.json();
245+
return data;
246+
}
247+
248+
export declare namespace add {
249+
/**
250+
* Input parameters for {@link add}.
251+
*/
252+
type Options = {
253+
/** Your {@link ThirdwebClient} instance. */
254+
client: ThirdwebClient;
255+
/** The chain ID where the token is deployed. */
256+
chainId: number;
257+
/** The contract address of the token to add. */
258+
tokenAddress: string;
259+
};
260+
261+
/**
262+
* The result returned from {@link Bridge.add}.
263+
*/
264+
type Result = Token;
265+
}

packages/thirdweb/src/pay/convert/get-token.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { tokens } from "../../bridge/Token.js";
1+
import { add, tokens } from "../../bridge/Token.js";
2+
import type { Token } from "../../bridge/types/Token.js";
23
import type { ThirdwebClient } from "../../client/client.js";
34
import { withCache } from "../../utils/promise/withCache.js";
45

56
export async function getToken(
67
client: ThirdwebClient,
78
tokenAddress: string,
89
chainId: number,
9-
) {
10+
): Promise<Token> {
1011
return withCache(
1112
async () => {
1213
const result = await tokens({
@@ -16,7 +17,15 @@ export async function getToken(
1617
});
1718
const token = result[0];
1819
if (!token) {
19-
throw new Error("Token not found");
20+
// Attempt to add the token
21+
const tokenResult = await add({
22+
client,
23+
chainId,
24+
tokenAddress,
25+
}).catch(() => {
26+
throw new Error("Token not supported");
27+
});
28+
return tokenResult;
2029
}
2130
return token;
2231
},

packages/thirdweb/src/react/web/ui/Bridge/UnsupportedTokenScreen.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { iconSize } from "../../../core/design-system/index.js";
33
import { useChainMetadata } from "../../../core/hooks/others/useChainQuery.js";
44
import { AccentFailIcon } from "../ConnectWallet/icons/AccentFailIcon.js";
55
import { Spacer } from "../components/Spacer.js";
6-
import { Spinner } from "../components/Spinner.js";
76
import { Container } from "../components/basic.js";
87
import { Text } from "../components/text.js";
98

@@ -12,14 +11,6 @@ export interface UnsupportedTokenScreenProps {
1211
* The chain the token is on
1312
*/
1413
chain: Chain;
15-
/**
16-
* Callback when user wants to try a different token
17-
*/
18-
onTryDifferentToken: () => void;
19-
/**
20-
* Optional callback when user wants to contact support
21-
*/
22-
onContactSupport?: () => void;
2314
}
2415

2516
/**
@@ -69,13 +60,13 @@ export function UnsupportedTokenScreen(props: UnsupportedTokenScreenProps) {
6960
center="both"
7061
style={{ minHeight: "350px" }}
7162
>
72-
{/* Loading Spinner */}
73-
<Spinner size="xl" color="accentText" />
63+
{/* Error Icon */}
64+
<AccentFailIcon size={iconSize["3xl"]} />
7465
<Spacer y="lg" />
7566

7667
{/* Title */}
7768
<Text center color="primaryText" size="lg" weight={600}>
78-
Indexing Token
69+
Token Not Supported
7970
</Text>
8071
<Spacer y="sm" />
8172

@@ -86,8 +77,7 @@ export function UnsupportedTokenScreen(props: UnsupportedTokenScreenProps) {
8677
size="sm"
8778
style={{ maxWidth: "280px", lineHeight: 1.5 }}
8879
>
89-
This token is being indexed by the Universal Bridge. Please check back
90-
later.
80+
This token or chain is not supported by the Universal Bridge.
9181
</Text>
9282
</Container>
9383
);

packages/thirdweb/src/react/web/ui/PayEmbed.tsx

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useQuery } from "@tanstack/react-query";
44
import { useEffect, useState } from "react";
5+
import type { Token } from "../../../bridge/index.js";
56
import type { Chain } from "../../../chains/types.js";
67
import type { ThirdwebClient } from "../../../client/client.js";
78
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
@@ -163,9 +164,14 @@ export type PayEmbedProps = {
163164
// Enhanced UIOptions to handle unsupported token state
164165
type UIOptionsResult =
165166
| { type: "success"; data: UIOptions }
167+
| {
168+
type: "indexing_token";
169+
token: Token;
170+
chain: Chain;
171+
}
166172
| {
167173
type: "unsupported_token";
168-
token?: { address: string; symbol?: string; name?: string };
174+
token: { address: string; symbol?: string; name?: string };
169175
chain: Chain;
170176
};
171177

@@ -360,7 +366,9 @@ export function PayEmbed(props: PayEmbedProps) {
360366
prefillInfo.token?.address || NATIVE_TOKEN_ADDRESS,
361367
prefillInfo.chain.id,
362368
).catch((err) =>
363-
err.message.includes("not found") ? undefined : Promise.reject(err),
369+
err.message.includes("not supported")
370+
? undefined
371+
: Promise.reject(err),
364372
);
365373
if (!token) {
366374
return {
@@ -390,7 +398,9 @@ export function PayEmbed(props: PayEmbedProps) {
390398
paymentInfo.token?.address || NATIVE_TOKEN_ADDRESS,
391399
paymentInfo.chain.id,
392400
).catch((err) =>
393-
err.message.includes("not found") ? undefined : Promise.reject(err),
401+
err.message.includes("not supported")
402+
? undefined
403+
: Promise.reject(err),
394404
);
395405
if (!token) {
396406
return {
@@ -439,16 +449,6 @@ export function PayEmbed(props: PayEmbedProps) {
439449
},
440450
});
441451

442-
const handleTryDifferentToken = () => {
443-
// Refetch to allow user to try again (they might have changed something)
444-
bridgeDataQuery.refetch();
445-
};
446-
447-
const handleContactSupport = () => {
448-
// Open support link or modal (this could be configurable via props)
449-
window.open("https://support.thirdweb.com", "_blank");
450-
};
451-
452452
let content = null;
453453
if (!localeQuery.data || bridgeDataQuery.isLoading) {
454454
content = (
@@ -465,13 +465,7 @@ export function PayEmbed(props: PayEmbedProps) {
465465
);
466466
} else if (bridgeDataQuery.data?.type === "unsupported_token") {
467467
// Show unsupported token screen
468-
content = (
469-
<UnsupportedTokenScreen
470-
chain={bridgeDataQuery.data.chain}
471-
onTryDifferentToken={handleTryDifferentToken}
472-
onContactSupport={handleContactSupport}
473-
/>
474-
);
468+
content = <UnsupportedTokenScreen chain={bridgeDataQuery.data.chain} />;
475469
} else if (bridgeDataQuery.data?.type === "success") {
476470
// Show normal bridge orchestrator
477471
content = (

packages/thirdweb/src/stories/Bridge/UnsupportedTokenScreen.stories.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@ import {
77
} from "../../react/web/ui/Bridge/UnsupportedTokenScreen.js";
88
import { ModalThemeWrapper } from "../utils.js";
99

10-
// Mock functions for story interactions
11-
const mockTryDifferentToken = () => console.log("Try different token clicked");
12-
const mockContactSupport = () => console.log("Contact support clicked");
13-
1410
// Props interface for the wrapper component
1511
interface UnsupportedTokenScreenWithThemeProps
1612
extends UnsupportedTokenScreenProps {
@@ -44,8 +40,6 @@ const meta = {
4440
tags: ["autodocs"],
4541
args: {
4642
chain: defineChain(1), // Ethereum mainnet
47-
onTryDifferentToken: mockTryDifferentToken,
48-
onContactSupport: mockContactSupport,
4943
theme: "dark",
5044
},
5145
argTypes: {
@@ -54,15 +48,13 @@ const meta = {
5448
options: ["light", "dark"],
5549
description: "Theme for the component",
5650
},
57-
onTryDifferentToken: { action: "try different token clicked" },
58-
onContactSupport: { action: "contact support clicked" },
5951
},
6052
} satisfies Meta<typeof UnsupportedTokenScreenWithTheme>;
6153

6254
export default meta;
6355
type Story = StoryObj<typeof meta>;
6456

65-
export const IndexingToken: Story = {
57+
export const TokenNotSupported: Story = {
6658
args: {
6759
theme: "dark",
6860
chain: defineChain(1), // Ethereum mainnet - will show indexing spinner
@@ -78,7 +70,7 @@ export const IndexingToken: Story = {
7870
},
7971
};
8072

81-
export const IndexingTokenLight: Story = {
73+
export const TokenNotSupportedLight: Story = {
8274
args: {
8375
theme: "light",
8476
chain: defineChain(1), // Ethereum mainnet - will show indexing spinner

0 commit comments

Comments
 (0)