From cac3039cb7d30d91fb089575bc6d77165d4f97a1 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Sat, 31 May 2025 21:05:21 +1200 Subject: [PATCH] [Bridge] Add tokens() function to retrieve supported tokens --- .changeset/bridge-tokens-function.md | 24 +++ packages/thirdweb/src/bridge/Token.test.ts | 90 +++++++++ packages/thirdweb/src/bridge/Token.ts | 184 ++++++++++++++++++ packages/thirdweb/src/bridge/index.ts | 1 + .../thirdweb/src/pay/convert/cryptoToFiat.ts | 39 +--- .../thirdweb/src/pay/convert/fiatToCrypto.ts | 39 +--- .../thirdweb/src/pay/convert/get-token.ts | 24 +++ 7 files changed, 335 insertions(+), 66 deletions(-) create mode 100644 .changeset/bridge-tokens-function.md create mode 100644 packages/thirdweb/src/bridge/Token.test.ts create mode 100644 packages/thirdweb/src/bridge/Token.ts create mode 100644 packages/thirdweb/src/pay/convert/get-token.ts diff --git a/.changeset/bridge-tokens-function.md b/.changeset/bridge-tokens-function.md new file mode 100644 index 00000000000..69b826b43fb --- /dev/null +++ b/.changeset/bridge-tokens-function.md @@ -0,0 +1,24 @@ +--- +"thirdweb": patch +--- + +Add `Bridge.tokens()` function to retrieve supported Universal Bridge tokens + +New function allows fetching and filtering tokens supported by the Universal Bridge service. Supports filtering by chain ID, token address, symbol, name, and includes pagination with limit/offset parameters. + +```typescript +import { Bridge } from "thirdweb"; + +// Get all supported tokens +const tokens = await Bridge.tokens({ + client: thirdwebClient, +}); + +// Filter tokens by chain and symbol +const ethTokens = await Bridge.tokens({ + chainId: 1, + symbol: "USDC", + limit: 50, + client: thirdwebClient, +}); +``` diff --git a/packages/thirdweb/src/bridge/Token.test.ts b/packages/thirdweb/src/bridge/Token.test.ts new file mode 100644 index 00000000000..f54d8847aa3 --- /dev/null +++ b/packages/thirdweb/src/bridge/Token.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "vitest"; +import { TEST_CLIENT } from "~test/test-clients.js"; +import { tokens } from "./Token.js"; + +describe.runIf(process.env.TW_SECRET_KEY)("tokens", () => { + it("should fetch tokens", async () => { + // Setup + const client = TEST_CLIENT; + + // Test + const result = await tokens({ client }); + + // Verify + expect(result).toBeInstanceOf(Array); + + // Basic structure validation + if (result.length > 0) { + const token = result[0]; + expect(token).toBeDefined(); + expect(token).toHaveProperty("chainId"); + expect(token).toHaveProperty("address"); + expect(token).toHaveProperty("decimals"); + expect(token).toHaveProperty("symbol"); + expect(token).toHaveProperty("name"); + expect(token).toHaveProperty("priceUsd"); + + if (token) { + expect(typeof token.chainId).toBe("number"); + expect(typeof token.address).toBe("string"); + expect(typeof token.decimals).toBe("number"); + expect(typeof token.symbol).toBe("string"); + expect(typeof token.name).toBe("string"); + expect(typeof token.priceUsd).toBe("number"); + } + } + }); + + it("should filter tokens by chainId", async () => { + // Setup + const client = TEST_CLIENT; + + // Test + const result = await tokens({ + client, + chainId: 1, + }); + + // Verify + expect(result).toBeInstanceOf(Array); + + // All tokens should have chainId 1 + for (const token of result) { + expect(token.chainId).toBe(1); + } + }); + + it("should respect limit parameter", async () => { + // Setup + const client = TEST_CLIENT; + + // Test + const result = await tokens({ + client, + limit: 5, + }); + + // Verify + expect(result).toBeInstanceOf(Array); + expect(result.length).toBeLessThanOrEqual(5); + }); + + it("should filter tokens by symbol", async () => { + // Setup + const client = TEST_CLIENT; + + // Test + const result = await tokens({ + client, + symbol: "ETH", + }); + + // Verify + expect(result).toBeInstanceOf(Array); + + // All tokens should have symbol "ETH" + for (const token of result) { + expect(token.symbol).toContain("ETH"); + } + }); +}); diff --git a/packages/thirdweb/src/bridge/Token.ts b/packages/thirdweb/src/bridge/Token.ts new file mode 100644 index 00000000000..68699915fb9 --- /dev/null +++ b/packages/thirdweb/src/bridge/Token.ts @@ -0,0 +1,184 @@ +import type { ThirdwebClient } from "../client/client.js"; +import { getThirdwebBaseUrl } from "../utils/domains.js"; +import { getClientFetch } from "../utils/fetch.js"; +import { ApiError } from "./types/Errors.js"; +import type { Token } from "./types/Token.js"; + +/** + * Retrieves supported Universal Bridge tokens based on the provided filters. + * + * When multiple filters are specified, a token must satisfy all filters to be included (it acts as an AND operator). + * + * @example + * ```typescript + * import { Bridge } from "thirdweb"; + * + * const tokens = await Bridge.tokens({ + * client: thirdwebClient, + * }); + * ``` + * + * Returned tokens might look something like: + * ```typescript + * [ + * { + * chainId: 1, + * address: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", + * decimals: 18, + * symbol: "ETH", + * name: "Ethereum", + * iconUri: "https://assets.relay.link/icons/1/light.png", + * priceUsd: 2000.50 + * }, + * { + * chainId: 1, + * address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + * decimals: 6, + * symbol: "USDC", + * name: "USD Coin", + * iconUri: "https://assets.coingecko.com/coins/images/6319/large/USD_Coin_icon.png", + * priceUsd: 1.00 + * } + * ] + * ``` + * + * You can filter for specific chains or tokens: + * ```typescript + * import { Bridge } from "thirdweb"; + * + * // Get all tokens on Ethereum mainnet + * const ethTokens = await Bridge.tokens({ + * chainId: 1, + * client: thirdwebClient, + * }); + * ``` + * + * You can search for tokens by symbol or name: + * ```typescript + * import { Bridge } from "thirdweb"; + * + * // Search for USDC tokens + * const usdcTokens = await Bridge.tokens({ + * symbol: "USDC", + * client: thirdwebClient, + * }); + * + * // Search for tokens by name + * const ethereumTokens = await Bridge.tokens({ + * name: "Ethereum", + * client: thirdwebClient, + * }); + * ``` + * + * You can filter by a specific token address: + * ```typescript + * import { Bridge } from "thirdweb"; + * + * // Get a specific token + * const token = await Bridge.tokens({ + * tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + * client: thirdwebClient, + * }); + * ``` + * + * The returned tokens will be limited based on the API. You can paginate through the results using the `limit` and `offset` parameters: + * ```typescript + * import { Bridge } from "thirdweb"; + * + * // Get the first 50 tokens + * const tokens = await Bridge.tokens({ + * limit: 50, + * offset: 0, + * client: thirdwebClient, + * }); + * + * // Get the next 50 tokens + * const nextTokens = await Bridge.tokens({ + * limit: 50, + * offset: 50, + * client: thirdwebClient, + * }); + * ``` + * + * @param options - The options for retrieving tokens. + * @param options.client - Your thirdweb client. + * @param options.chainId - Filter by a specific chain ID. + * @param options.tokenAddress - Filter by a specific token address. + * @param options.symbol - Filter by token symbol. + * @param options.name - Filter by token name. + * @param options.limit - Number of tokens to return (min: 1, default: 100). + * @param options.offset - Number of tokens to skip (min: 0, default: 0). + * + * @returns A promise that resolves to an array of tokens. + * + * @throws Will throw an error if there is an issue fetching the tokens. + * @bridge + * @beta + */ +export async function tokens(options: tokens.Options): Promise { + const { client, chainId, tokenAddress, symbol, name, limit, offset } = + options; + + const clientFetch = getClientFetch(client); + const url = new URL(`${getThirdwebBaseUrl("bridge")}/v1/tokens`); + + if (chainId !== null && chainId !== undefined) { + url.searchParams.set("chainId", chainId.toString()); + } + if (tokenAddress) { + url.searchParams.set("tokenAddress", tokenAddress); + } + if (symbol) { + url.searchParams.set("symbol", symbol); + } + if (name) { + url.searchParams.set("name", name); + } + if (limit !== undefined) { + url.searchParams.set("limit", limit.toString()); + } + if (offset !== null && offset !== undefined) { + url.searchParams.set("offset", offset.toString()); + } + + const response = await clientFetch(url.toString()); + if (!response.ok) { + const errorJson = await response.json(); + throw new ApiError({ + code: errorJson.code || "UNKNOWN_ERROR", + message: errorJson.message || response.statusText, + correlationId: errorJson.correlationId || undefined, + statusCode: response.status, + }); + } + + const { data }: { data: Token[] } = await response.json(); + return data; +} + +export declare namespace tokens { + /** + * Input parameters for {@link Bridge.tokens}. + */ + type Options = { + /** Your {@link ThirdwebClient} instance. */ + client: ThirdwebClient; + /** Filter by a specific chain ID. */ + chainId?: number | null; + /** Filter by a specific token address. */ + tokenAddress?: string; + /** Filter by token symbol. */ + symbol?: string; + /** Filter by token name. */ + name?: string; + /** Number of tokens to return (min: 1, default: 100). */ + limit?: number; + /** Number of tokens to skip (min: 0, default: 0). */ + offset?: number | null; + }; + + /** + * The result returned from {@link Bridge.tokens}. + */ + type Result = Token[]; +} diff --git a/packages/thirdweb/src/bridge/index.ts b/packages/thirdweb/src/bridge/index.ts index 51d403f9774..83b0fe868b9 100644 --- a/packages/thirdweb/src/bridge/index.ts +++ b/packages/thirdweb/src/bridge/index.ts @@ -6,6 +6,7 @@ export * as Webhook from "./Webhook.js"; export { status } from "./Status.js"; export { routes } from "./Routes.js"; export { chains } from "./Chains.js"; +export { tokens } from "./Token.js"; export { parse } from "./Webhook.js"; export type { Chain } from "./types/Chain.js"; diff --git a/packages/thirdweb/src/pay/convert/cryptoToFiat.ts b/packages/thirdweb/src/pay/convert/cryptoToFiat.ts index 245e8e55532..d27fb397f97 100644 --- a/packages/thirdweb/src/pay/convert/cryptoToFiat.ts +++ b/packages/thirdweb/src/pay/convert/cryptoToFiat.ts @@ -1,4 +1,3 @@ -import { getV1TokensPrice } from "@thirdweb-dev/insight"; import type { Address } from "abitype"; import type { Chain } from "../../chains/types.js"; import type { ThirdwebClient } from "../../client/client.js"; @@ -6,10 +5,7 @@ import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js"; import { getBytecode } from "../../contract/actions/get-bytecode.js"; import { getContract } from "../../contract/contract.js"; import { isAddress } from "../../utils/address.js"; -import { getThirdwebDomains } from "../../utils/domains.js"; -import { getClientFetch } from "../../utils/fetch.js"; -import { stringify } from "../../utils/json.js"; -import { withCache } from "../../utils/promise/withCache.js"; +import { getTokenPrice } from "./get-token.js"; import type { SupportedFiatCurrency } from "./type.js"; /** @@ -63,7 +59,7 @@ export type ConvertCryptoToFiatParams = { export async function convertCryptoToFiat( options: ConvertCryptoToFiatParams, ): Promise<{ result: number }> { - const { client, fromTokenAddress, to, chain, fromAmount } = options; + const { client, fromTokenAddress, chain, fromAmount } = options; if (Number(fromAmount) === 0) { return { result: 0 }; } @@ -96,34 +92,11 @@ export async function convertCryptoToFiat( } } - const result = await withCache( - () => - getV1TokensPrice({ - baseUrl: `https://${getThirdwebDomains().insight}`, - fetch: getClientFetch(client), - query: { - address: fromTokenAddress, - chain_id: [chain.id], - }, - }), - { - cacheKey: `convert-fiat-to-crypto-${fromTokenAddress}-${chain.id}`, - cacheTime: 1000 * 60, // 1 minute cache - }, - ); - - if (result.error) { - throw new Error( - `Failed to fetch ${to} value for token (${fromTokenAddress}) on chainId: ${chain.id} - ${result.response.status} ${result.response.statusText} - ${result.error ? stringify(result.error) : "Unknown error"}`, - ); - } - - const firstResult = result.data?.data[0]; - - if (!firstResult) { + const price = await getTokenPrice(client, fromTokenAddress, chain.id); + if (!price) { throw new Error( - `Failed to fetch ${to} value for token (${fromTokenAddress}) on chainId: ${chain.id}`, + `Error: Failed to fetch price for token ${fromTokenAddress} on chainId: ${chain.id}`, ); } - return { result: firstResult.price_usd * fromAmount }; + return { result: price * fromAmount }; } diff --git a/packages/thirdweb/src/pay/convert/fiatToCrypto.ts b/packages/thirdweb/src/pay/convert/fiatToCrypto.ts index 58ba8944ee0..e4b027840a4 100644 --- a/packages/thirdweb/src/pay/convert/fiatToCrypto.ts +++ b/packages/thirdweb/src/pay/convert/fiatToCrypto.ts @@ -1,4 +1,3 @@ -import { getV1TokensPrice } from "@thirdweb-dev/insight"; import type { Address } from "abitype"; import type { Chain } from "../../chains/types.js"; import type { ThirdwebClient } from "../../client/client.js"; @@ -6,10 +5,7 @@ import { NATIVE_TOKEN_ADDRESS } from "../../constants/addresses.js"; import { getBytecode } from "../../contract/actions/get-bytecode.js"; import { getContract } from "../../contract/contract.js"; import { isAddress } from "../../utils/address.js"; -import { getThirdwebDomains } from "../../utils/domains.js"; -import { getClientFetch } from "../../utils/fetch.js"; -import { stringify } from "../../utils/json.js"; -import { withCache } from "../../utils/promise/withCache.js"; +import { getTokenPrice } from "./get-token.js"; import type { SupportedFiatCurrency } from "./type.js"; /** @@ -64,7 +60,7 @@ export type ConvertFiatToCryptoParams = { export async function convertFiatToCrypto( options: ConvertFiatToCryptoParams, ): Promise<{ result: number }> { - const { client, from, to, chain, fromAmount } = options; + const { client, to, chain, fromAmount } = options; if (Number(fromAmount) === 0) { return { result: 0 }; } @@ -94,34 +90,11 @@ export async function convertFiatToCrypto( ); } } - const result = await withCache( - () => - getV1TokensPrice({ - baseUrl: `https://${getThirdwebDomains().insight}`, - fetch: getClientFetch(client), - query: { - address: to, - chain_id: [chain.id], - }, - }), - { - cacheKey: `convert-fiat-to-crypto-${to}-${chain.id}`, - cacheTime: 1000 * 60, // 1 minute cache - }, - ); - - if (result.error) { - throw new Error( - `Failed to fetch ${from} value for token (${to}) on chainId: ${chain.id} - ${result.response.status} ${result.response.statusText} - ${result.error ? stringify(result.error) : "Unknown error"}`, - ); - } - - const firstResult = result.data?.data[0]; - - if (!firstResult || firstResult.price_usd === 0) { + const price = await getTokenPrice(client, to, chain.id); + if (!price || price === 0) { throw new Error( - `Failed to fetch ${from} value for token (${to}) on chainId: ${chain.id}`, + `Error: Failed to fetch price for token ${to} on chainId: ${chain.id}`, ); } - return { result: fromAmount / firstResult.price_usd }; + return { result: fromAmount / price }; } diff --git a/packages/thirdweb/src/pay/convert/get-token.ts b/packages/thirdweb/src/pay/convert/get-token.ts new file mode 100644 index 00000000000..6ec2eace996 --- /dev/null +++ b/packages/thirdweb/src/pay/convert/get-token.ts @@ -0,0 +1,24 @@ +import { tokens } from "../../bridge/Token.js"; +import type { ThirdwebClient } from "../../client/client.js"; +import { withCache } from "../../utils/promise/withCache.js"; + +export async function getTokenPrice( + client: ThirdwebClient, + tokenAddress: string, + chainId: number, +) { + return withCache( + async () => { + const result = await tokens({ + client, + tokenAddress, + chainId, + }); + return result[0]?.priceUsd; + }, + { + cacheKey: `get-token-price-${tokenAddress}-${chainId}`, + cacheTime: 1000 * 60, // 1 minute + }, + ); +}