From da0a3b76b64bd4c0f8962061ce1aad2ea06b14f5 Mon Sep 17 00:00:00 2001 From: gregfromstl Date: Tue, 29 Jul 2025 12:41:49 -0700 Subject: [PATCH] Revert "[SDK] Feature: Faster payment widget quote generation (#7745)" This reverts commit c717eaadae09c65b2f651145f1279ba876c90ace. --- .changeset/cute-actors-beam.md | 5 - .../src/react/core/hooks/useBridgeQuote.ts | 71 ++++ .../src/react/core/hooks/usePaymentMethods.ts | 307 +++++++++++++----- .../src/react/core/machines/paymentMachine.ts | 2 - .../payment-selection/TokenSelection.tsx | 85 +++-- 5 files changed, 367 insertions(+), 103 deletions(-) delete mode 100644 .changeset/cute-actors-beam.md create mode 100644 packages/thirdweb/src/react/core/hooks/useBridgeQuote.ts diff --git a/.changeset/cute-actors-beam.md b/.changeset/cute-actors-beam.md deleted file mode 100644 index a4cb7aa1404..00000000000 --- a/.changeset/cute-actors-beam.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"thirdweb": patch ---- - -Faster payment widget quote discovery diff --git a/packages/thirdweb/src/react/core/hooks/useBridgeQuote.ts b/packages/thirdweb/src/react/core/hooks/useBridgeQuote.ts new file mode 100644 index 00000000000..7565c720ab2 --- /dev/null +++ b/packages/thirdweb/src/react/core/hooks/useBridgeQuote.ts @@ -0,0 +1,71 @@ +"use client"; +import { useQuery } from "@tanstack/react-query"; +import * as Buy from "../../../bridge/Buy.js"; +import * as Transfer from "../../../bridge/Transfer.js"; +import type { Token } from "../../../bridge/types/Token.js"; +import type { ThirdwebClient } from "../../../client/client.js"; +import { checksumAddress } from "../../../utils/address.js"; + +interface UseBridgeQuoteParams { + originToken: Token; + destinationToken: Token; + destinationAmount: bigint; + client: ThirdwebClient; + enabled?: boolean; + feePayer?: "sender" | "receiver"; +} + +export function useBridgeQuote({ + originToken, + destinationToken, + destinationAmount, + feePayer, + client, + enabled = true, +}: UseBridgeQuoteParams) { + return useQuery({ + enabled: + enabled && !!originToken && !!destinationToken && !!destinationAmount, + queryFn: async () => { + // if ssame token and chain, use transfer + if ( + checksumAddress(originToken.address) === + checksumAddress(destinationToken.address) && + originToken.chainId === destinationToken.chainId + ) { + const transfer = await Transfer.prepare({ + amount: destinationAmount, + chainId: originToken.chainId, + client, + feePayer, + receiver: destinationToken.address, + sender: originToken.address, + tokenAddress: originToken.address, + }); + return transfer; + } + const quote = await Buy.quote({ + amount: destinationAmount, + client, + destinationChainId: destinationToken.chainId, + destinationTokenAddress: destinationToken.address, + originChainId: originToken.chainId, + originTokenAddress: originToken.address, + }); + + return quote; + }, + queryKey: [ + "bridge-quote", + originToken.chainId, + originToken.address, + destinationToken.chainId, + destinationToken.address, + destinationAmount.toString(), + feePayer, + ], + refetchInterval: 60000, // 30 seconds + retry: 3, // 1 minute + staleTime: 30000, + }); +} diff --git a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts index d633d0ab336..323adf45f6b 100644 --- a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts +++ b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts @@ -1,15 +1,28 @@ import { useQuery } from "@tanstack/react-query"; -import type { Quote } from "../../../bridge/index.js"; -import { ApiError } from "../../../bridge/types/Errors.js"; +import { chains } from "../../../bridge/Chains.js"; +import { routes } from "../../../bridge/Routes.js"; import type { Token } from "../../../bridge/types/Token.js"; +import { + getCachedChain, + getInsightEnabledChainIds, +} from "../../../chains/utils.js"; import type { ThirdwebClient } from "../../../client/client.js"; -import { getThirdwebBaseUrl } from "../../../utils/domains.js"; -import { getClientFetch } from "../../../utils/fetch.js"; -import { toTokens, toUnits } from "../../../utils/units.js"; +import { getOwnedTokens } from "../../../insight/get-tokens.js"; +import { toTokens } from "../../../utils/units.js"; import type { Wallet } from "../../../wallets/interfaces/wallet.js"; +import { + type GetWalletBalanceResult, + getWalletBalance, +} from "../../../wallets/utils/getWalletBalance.js"; import type { PaymentMethod } from "../machines/paymentMachine.js"; import { useActiveWallet } from "./wallets/useActiveWallet.js"; +type OwnedTokenWithQuote = { + originToken: Token; + balance: bigint; + originAmount: bigint; +}; + /** * Hook that returns available payment methods for BridgeEmbed * Fetches real routes data based on the destination token @@ -44,85 +57,225 @@ export function usePaymentMethods(options: { const localWallet = useActiveWallet(); // TODO (bridge): get all connected wallets const wallet = payerWallet || localWallet; - const query = useQuery({ + const routesQuery = useQuery({ enabled: !!wallet, queryFn: async (): Promise => { - const account = wallet?.getAccount(); - if (!wallet || !account) { + if (!wallet) { throw new Error("No wallet connected"); } - const url = new URL( - `${getThirdwebBaseUrl("bridge")}/v1/buy/quote/${account.address}`, + // 1. Get all supported chains + const [allChains, insightEnabledChainIds] = await Promise.all([ + chains({ client }), + getInsightEnabledChainIds(), + ]); + + // 2. Check insight availability for all chains + const insightEnabledChains = allChains.filter((c) => + insightEnabledChainIds.includes(c.chainId), ); - url.searchParams.set( - "destinationChainId", - destinationToken.chainId.toString(), + + // 3. Get all owned tokens for insight-enabled chains + let allOwnedTokens: Array<{ + balance: bigint; + originToken: Token; + }> = []; + let page = 0; + const limit = 500; + + while (true) { + let batch: GetWalletBalanceResult[]; + try { + batch = await getOwnedTokens({ + chains: insightEnabledChains.map((c) => getCachedChain(c.chainId)), + client, + ownerAddress: wallet.getAccount()?.address || "", + queryOptions: { + limit, + metadata: "false", + page, + }, + }); + } catch (error) { + // If the batch fails, fall back to getting native balance for each chain + console.warn(`Failed to get owned tokens for batch ${page}:`, error); + + const chainsInBatch = insightEnabledChains.map((c) => + getCachedChain(c.chainId), + ); + const nativeBalances = await Promise.allSettled( + chainsInBatch.map(async (chain) => { + const balance = await getWalletBalance({ + address: wallet.getAccount()?.address || "", + chain, + client, + }); + return balance; + }), + ); + + // Transform successful native balances into the same format as getOwnedTokens results + batch = nativeBalances + .filter((result) => result.status === "fulfilled") + .map((result) => result.value) + .filter((balance) => balance.value > 0n); + + // Convert to our format + const tokensWithBalance = batch.map((b) => ({ + balance: b.value, + originToken: { + address: b.tokenAddress, + chainId: b.chainId, + decimals: b.decimals, + iconUri: "", + name: b.name, + prices: { + USD: 0, + }, + symbol: b.symbol, + } as Token, + })); + + allOwnedTokens = [...allOwnedTokens, ...tokensWithBalance]; + break; + } + + if (batch.length === 0) { + break; + } + + // Convert to our format and filter out zero balances + const tokensWithBalance = batch + .filter((b) => b.value > 0n) + .map((b) => ({ + balance: b.value, + originToken: { + address: b.tokenAddress, + chainId: b.chainId, + decimals: b.decimals, + iconUri: "", + name: b.name, + prices: { + USD: 0, + }, + symbol: b.symbol, + } as Token, + })); + + allOwnedTokens = [...allOwnedTokens, ...tokensWithBalance]; + page += 1; + } + + // 4. For each chain where we have owned tokens, fetch possible routes + const chainsWithOwnedTokens = Array.from( + new Set(allOwnedTokens.map((t) => t.originToken.chainId)), ); - url.searchParams.set("destinationTokenAddress", destinationToken.address); - url.searchParams.set( - "amount", - toUnits(destinationAmount, destinationToken.decimals).toString(), + + const allValidOriginTokens = new Map(); + + // Add destination token if included + if (includeDestinationToken) { + const tokenKey = `${ + destinationToken.chainId + }-${destinationToken.address.toLowerCase()}`; + allValidOriginTokens.set(tokenKey, destinationToken); + } + + // Fetch routes for each chain with owned tokens + await Promise.all( + chainsWithOwnedTokens.map(async (chainId) => { + try { + // TODO (bridge): this is quite inefficient, need to fix the popularity sorting to really capture all users tokens + const routesForChain = await routes({ + client, + destinationChainId: destinationToken.chainId, + destinationTokenAddress: destinationToken.address, + includePrices: true, + limit: 100, + maxSteps: 3, + originChainId: chainId, + }); + + // Add all origin tokens from this chain's routes + for (const route of routesForChain) { + // Skip if the origin token is the same as the destination token, will be added later only if includeDestinationToken is true + if ( + route.originToken.chainId === destinationToken.chainId && + route.originToken.address.toLowerCase() === + destinationToken.address.toLowerCase() + ) { + continue; + } + const tokenKey = `${ + route.originToken.chainId + }-${route.originToken.address.toLowerCase()}`; + allValidOriginTokens.set(tokenKey, route.originToken); + } + } catch (error) { + // Log error but don't fail the entire operation + console.warn(`Failed to fetch routes for chain ${chainId}:`, error); + } + }), ); - const clientFetch = getClientFetch(client); - const response = await clientFetch(url.toString()); - if (!response.ok) { - const errorJson = await response.json(); - throw new ApiError({ - code: errorJson.code || "UNKNOWN_ERROR", - correlationId: errorJson.correlationId || undefined, - message: errorJson.message || response.statusText, - statusCode: response.status, - }); + // 5. Filter owned tokens to only include valid origin tokens + const validOwnedTokens: OwnedTokenWithQuote[] = []; + + for (const ownedToken of allOwnedTokens) { + const tokenKey = `${ + ownedToken.originToken.chainId + }-${ownedToken.originToken.address.toLowerCase()}`; + const validOriginToken = allValidOriginTokens.get(tokenKey); + + if (validOriginToken) { + validOwnedTokens.push({ + balance: ownedToken.balance, + originAmount: 0n, + originToken: validOriginToken, // Use the token with pricing info from routes + }); + } } - const { - data: allValidOriginTokens, - }: { data: { quote: Quote; balance: string; token: Token }[] } = - await response.json(); - - // Sort by enough balance to pay THEN gross balance - const validTokenQuotes = allValidOriginTokens.map((s) => ({ - balance: BigInt(s.balance), - originToken: s.token, - payerWallet: wallet, - type: "wallet" as const, - quote: s.quote, - })); - const insufficientBalanceQuotes = validTokenQuotes - .filter((s) => s.balance < s.quote.originAmount) - .sort((a, b) => { - return ( - Number.parseFloat( - toTokens(a.quote.originAmount, a.originToken.decimals), - ) * - (a.originToken.prices.USD || 1) - - Number.parseFloat( - toTokens(b.quote.originAmount, b.originToken.decimals), - ) * - (b.originToken.prices.USD || 1) - ); - }); - const sufficientBalanceQuotes = validTokenQuotes - .filter((s) => s.balance >= s.quote.originAmount) - .sort((a, b) => { - return ( - Number.parseFloat( - toTokens(b.quote.originAmount, b.originToken.decimals), - ) * - (b.originToken.prices.USD || 1) - - Number.parseFloat( - toTokens(a.quote.originAmount, a.originToken.decimals), - ) * - (a.originToken.prices.USD || 1) - ); - }); - // Move all sufficient balance quotes to the top - return [...sufficientBalanceQuotes, ...insufficientBalanceQuotes]; + // Sort by dollar balance descending + validOwnedTokens.sort((a, b) => { + const aDollarBalance = + Number.parseFloat(toTokens(a.balance, a.originToken.decimals)) * + (a.originToken.prices.USD || 0); + const bDollarBalance = + Number.parseFloat(toTokens(b.balance, b.originToken.decimals)) * + (b.originToken.prices.USD || 0); + return bDollarBalance - aDollarBalance; + }); + + const suitableOriginTokens: OwnedTokenWithQuote[] = []; + + for (const token of validOwnedTokens) { + if ( + includeDestinationToken && + token.originToken.address.toLowerCase() === + destinationToken.address.toLowerCase() && + token.originToken.chainId === destinationToken.chainId + ) { + // Add same token to the front of the list + suitableOriginTokens.unshift(token); + continue; + } + + suitableOriginTokens.push(token); + } + + const transformedRoutes = [ + ...suitableOriginTokens.map((s) => ({ + balance: s.balance, + originToken: s.originToken, + payerWallet: wallet, + type: "wallet" as const, + })), + ]; + return transformedRoutes; }, queryKey: [ - "payment-methods", + "bridge-routes", destinationToken.chainId, destinationToken.address, destinationAmount, @@ -134,11 +287,11 @@ export function usePaymentMethods(options: { }); return { - data: query.data || [], - error: query.error, - isError: query.isError, - isLoading: query.isLoading, - isSuccess: query.isSuccess, - refetch: query.refetch, + data: routesQuery.data || [], + error: routesQuery.error, + isError: routesQuery.isError, + isLoading: routesQuery.isLoading, + isSuccess: routesQuery.isSuccess, + refetch: routesQuery.refetch, }; } diff --git a/packages/thirdweb/src/react/core/machines/paymentMachine.ts b/packages/thirdweb/src/react/core/machines/paymentMachine.ts index 337db24c424..e98484ad2dc 100644 --- a/packages/thirdweb/src/react/core/machines/paymentMachine.ts +++ b/packages/thirdweb/src/react/core/machines/paymentMachine.ts @@ -1,5 +1,4 @@ import { useCallback, useState } from "react"; -import type { Quote } from "../../../bridge/index.js"; import type { Token } from "../../../bridge/types/Token.js"; import type { Address } from "../../../utils/address.js"; import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js"; @@ -25,7 +24,6 @@ export type PaymentMethod = payerWallet: Wallet; originToken: Token; balance: bigint; - quote: Quote; } | { type: "fiat"; diff --git a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/TokenSelection.tsx b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/TokenSelection.tsx index e889fed9cc5..4d02c9cc2ae 100644 --- a/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/TokenSelection.tsx +++ b/packages/thirdweb/src/react/web/ui/Bridge/payment-selection/TokenSelection.tsx @@ -3,6 +3,7 @@ import type { Token } from "../../../../../bridge/types/Token.js"; import type { ThirdwebClient } from "../../../../../client/client.js"; import { useCustomTheme } from "../../../../core/design-system/CustomThemeProvider.js"; import { radius, spacing } from "../../../../core/design-system/index.js"; +import { useBridgeQuote } from "../../../../core/hooks/useBridgeQuote.js"; import type { PaymentMethod } from "../../../../core/machines/paymentMachine.js"; import { formatTokenAmount } from "../../ConnectWallet/screens/formatTokenBalance.js"; import { Container } from "../../components/basic.js"; @@ -35,12 +36,29 @@ interface PaymentMethodTokenRowProps { function PaymentMethodTokenRow({ paymentMethod, + destinationToken, + destinationAmount, client, onPaymentMethodSelected, + feePayer, }: PaymentMethodTokenRowProps) { const theme = useCustomTheme(); - const displayOriginAmount = paymentMethod.quote.originAmount; + // Fetch individual quote for this specific token pair + const { + data: quote, + isLoading: quoteLoading, + error: quoteError, + } = useBridgeQuote({ + client, + destinationAmount, + destinationToken, + feePayer, + originToken: paymentMethod.originToken, + }); + + // Use the fetched originAmount if available, otherwise fall back to the one from paymentMethod + const displayOriginAmount = quote?.originAmount; const hasEnoughBalance = displayOriginAmount ? paymentMethod.balance >= displayOriginAmount : false; @@ -79,28 +97,57 @@ function PaymentMethodTokenRow({ gap="3xs" style={{ alignItems: "flex-end", flex: 1 }} > - - {formatTokenAmount( - displayOriginAmount, - paymentMethod.originToken.decimals, - )}{" "} - {paymentMethod.originToken.symbol} - - - - Balance:{" "} + {quoteLoading ? ( + <> + {/* Price amount skeleton */} + + {/* Balance skeleton */} + + + + + + ) : quoteError ? ( + + Quote failed - + ) : displayOriginAmount ? ( + {formatTokenAmount( - paymentMethod.balance, + displayOriginAmount, paymentMethod.originToken.decimals, - )} + )}{" "} + {paymentMethod.originToken.symbol} - + ) : ( + "--.--" + )} + {!quoteLoading && ( + + + Balance:{" "} + + + {formatTokenAmount( + paymentMethod.balance, + paymentMethod.originToken.decimals, + )} + + + )}