diff --git a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts index 97986e4b577..dda533d5225 100644 --- a/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts +++ b/packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts @@ -1,28 +1,14 @@ import { useQuery } from "@tanstack/react-query"; -import { chains } from "../../../bridge/Chains.js"; -import { routes } from "../../../bridge/Routes.js"; +import { Value } from "ox"; +import type { Quote } from "../../../bridge/index.js"; import type { Token } from "../../../bridge/types/Token.js"; -import { - getCachedChain, - getInsightEnabledChainIds, -} from "../../../chains/utils.js"; import type { ThirdwebClient } from "../../../client/client.js"; -import { getOwnedTokens } from "../../../insight/get-tokens.js"; -import { toTokens } from "../../../utils/units.js"; +import { getThirdwebBaseUrl } from "../../../utils/domains.js"; +import { getClientFetch } from "../../../utils/fetch.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 @@ -54,220 +40,48 @@ export function usePaymentMethods(options: { payerWallet, includeDestinationToken, } = options; - const localWallet = useActiveWallet(); // TODO (bridge): get all connected wallets + const localWallet = useActiveWallet(); const wallet = payerWallet || localWallet; const routesQuery = useQuery({ enabled: !!wallet, queryFn: async (): Promise => { - if (!wallet) { + const account = wallet?.getAccount(); + if (!wallet || !account) { throw new Error("No wallet connected"); } - // 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), + const clientFetch = getClientFetch(client); + const url = new URL( + `${getThirdwebBaseUrl("bridge")}/v1/buy/quote/${account.address}`, ); - - // 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.append( + "destinationTokenAddress", + destinationToken.address, ); - - 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); - } - }), + url.searchParams.append( + "destinationChainId", + destinationToken.chainId.toString(), ); + url.searchParams.append( + "amount", + Value.from(destinationAmount, destinationToken.decimals).toString(), + ); + const response = await clientFetch(url.toString()); - // 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 - }); - } + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText); } - // 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 { data: quotes }: { data: _WalletQuotesResponse } = + await response.json(); const transformedRoutes = [ - ...suitableOriginTokens.map((s) => ({ - balance: s.balance, - originToken: s.originToken, + ...quotes.map((s) => ({ + balance: BigInt(s.balance), + quote: s.quote, + originToken: s.token, payerWallet: wallet, type: "wallet" as const, })), @@ -295,3 +109,9 @@ export function usePaymentMethods(options: { refetch: routesQuery.refetch, }; } + +type _WalletQuotesResponse = Array<{ + quote: Quote; + balance: string; + token: Token; +}>; diff --git a/packages/thirdweb/src/react/core/machines/paymentMachine.ts b/packages/thirdweb/src/react/core/machines/paymentMachine.ts index e98484ad2dc..b2b1d06984a 100644 --- a/packages/thirdweb/src/react/core/machines/paymentMachine.ts +++ b/packages/thirdweb/src/react/core/machines/paymentMachine.ts @@ -1,4 +1,5 @@ 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"; @@ -24,6 +25,7 @@ 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 4d02c9cc2ae..faec386c48f 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,7 +3,6 @@ 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"; @@ -36,29 +35,13 @@ interface PaymentMethodTokenRowProps { function PaymentMethodTokenRow({ paymentMethod, - destinationToken, - destinationAmount, client, onPaymentMethodSelected, - feePayer, }: PaymentMethodTokenRowProps) { const theme = useCustomTheme(); - // 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 displayOriginAmount = paymentMethod.quote?.originAmount; const hasEnoughBalance = displayOriginAmount ? paymentMethod.balance >= displayOriginAmount : false; @@ -97,21 +80,7 @@ function PaymentMethodTokenRow({ gap="3xs" style={{ alignItems: "flex-end", flex: 1 }} > - {quoteLoading ? ( - <> - {/* Price amount skeleton */} - - {/* Balance skeleton */} - - - - - - ) : quoteError ? ( - - Quote failed - - ) : displayOriginAmount ? ( + {displayOriginAmount ? ( - - Balance:{" "} - - - {formatTokenAmount( - paymentMethod.balance, - paymentMethod.originToken.decimals, - )} - - - )}