11import { useQuery } from "@tanstack/react-query" ;
2- import { chains } from "../../../bridge/Chains .js" ;
3- import { routes } from "../../../bridge/Routes .js" ;
2+ import type { Quote } from "../../../bridge/index .js" ;
3+ import { ApiError } from "../../../bridge/types/Errors .js" ;
44import type { Token } from "../../../bridge/types/Token.js" ;
5- import {
6- getCachedChain ,
7- getInsightEnabledChainIds ,
8- } from "../../../chains/utils.js" ;
95import type { ThirdwebClient } from "../../../client/client.js" ;
10- import { getOwnedTokens } from "../../../insight/get-tokens.js" ;
11- import { toTokens } from "../../../utils/units.js" ;
6+ import { getThirdwebBaseUrl } from "../../../utils/domains.js" ;
7+ import { getClientFetch } from "../../../utils/fetch.js" ;
8+ import { toTokens , toUnits } from "../../../utils/units.js" ;
129import type { Wallet } from "../../../wallets/interfaces/wallet.js" ;
13- import {
14- type GetWalletBalanceResult ,
15- getWalletBalance ,
16- } from "../../../wallets/utils/getWalletBalance.js" ;
1710import type { PaymentMethod } from "../machines/paymentMachine.js" ;
1811import { useActiveWallet } from "./wallets/useActiveWallet.js" ;
1912
20- type OwnedTokenWithQuote = {
21- originToken : Token ;
22- balance : bigint ;
23- originAmount : bigint ;
24- } ;
25-
2613/**
2714 * Hook that returns available payment methods for BridgeEmbed
2815 * Fetches real routes data based on the destination token
@@ -57,225 +44,85 @@ export function usePaymentMethods(options: {
5744 const localWallet = useActiveWallet ( ) ; // TODO (bridge): get all connected wallets
5845 const wallet = payerWallet || localWallet ;
5946
60- const routesQuery = useQuery ( {
47+ const query = useQuery ( {
6148 enabled : ! ! wallet ,
6249 queryFn : async ( ) : Promise < PaymentMethod [ ] > => {
63- if ( ! wallet ) {
50+ const account = wallet ?. getAccount ( ) ;
51+ if ( ! wallet || ! account ) {
6452 throw new Error ( "No wallet connected" ) ;
6553 }
6654
67- // 1. Get all supported chains
68- const [ allChains , insightEnabledChainIds ] = await Promise . all ( [
69- chains ( { client } ) ,
70- getInsightEnabledChainIds ( ) ,
71- ] ) ;
72-
73- // 2. Check insight availability for all chains
74- const insightEnabledChains = allChains . filter ( ( c ) =>
75- insightEnabledChainIds . includes ( c . chainId ) ,
55+ const url = new URL (
56+ `${ getThirdwebBaseUrl ( "bridge" ) } /v1/buy/quote/${ account . address } ` ,
7657 ) ;
77-
78- // 3. Get all owned tokens for insight-enabled chains
79- let allOwnedTokens : Array < {
80- balance : bigint ;
81- originToken : Token ;
82- } > = [ ] ;
83- let page = 0 ;
84- const limit = 500 ;
85-
86- while ( true ) {
87- let batch : GetWalletBalanceResult [ ] ;
88- try {
89- batch = await getOwnedTokens ( {
90- chains : insightEnabledChains . map ( ( c ) => getCachedChain ( c . chainId ) ) ,
91- client,
92- ownerAddress : wallet . getAccount ( ) ?. address || "" ,
93- queryOptions : {
94- limit,
95- metadata : "false" ,
96- page,
97- } ,
98- } ) ;
99- } catch ( error ) {
100- // If the batch fails, fall back to getting native balance for each chain
101- console . warn ( `Failed to get owned tokens for batch ${ page } :` , error ) ;
102-
103- const chainsInBatch = insightEnabledChains . map ( ( c ) =>
104- getCachedChain ( c . chainId ) ,
105- ) ;
106- const nativeBalances = await Promise . allSettled (
107- chainsInBatch . map ( async ( chain ) => {
108- const balance = await getWalletBalance ( {
109- address : wallet . getAccount ( ) ?. address || "" ,
110- chain,
111- client,
112- } ) ;
113- return balance ;
114- } ) ,
115- ) ;
116-
117- // Transform successful native balances into the same format as getOwnedTokens results
118- batch = nativeBalances
119- . filter ( ( result ) => result . status === "fulfilled" )
120- . map ( ( result ) => result . value )
121- . filter ( ( balance ) => balance . value > 0n ) ;
122-
123- // Convert to our format
124- const tokensWithBalance = batch . map ( ( b ) => ( {
125- balance : b . value ,
126- originToken : {
127- address : b . tokenAddress ,
128- chainId : b . chainId ,
129- decimals : b . decimals ,
130- iconUri : "" ,
131- name : b . name ,
132- prices : {
133- USD : 0 ,
134- } ,
135- symbol : b . symbol ,
136- } as Token ,
137- } ) ) ;
138-
139- allOwnedTokens = [ ...allOwnedTokens , ...tokensWithBalance ] ;
140- break ;
141- }
142-
143- if ( batch . length === 0 ) {
144- break ;
145- }
146-
147- // Convert to our format and filter out zero balances
148- const tokensWithBalance = batch
149- . filter ( ( b ) => b . value > 0n )
150- . map ( ( b ) => ( {
151- balance : b . value ,
152- originToken : {
153- address : b . tokenAddress ,
154- chainId : b . chainId ,
155- decimals : b . decimals ,
156- iconUri : "" ,
157- name : b . name ,
158- prices : {
159- USD : 0 ,
160- } ,
161- symbol : b . symbol ,
162- } as Token ,
163- } ) ) ;
164-
165- allOwnedTokens = [ ...allOwnedTokens , ...tokensWithBalance ] ;
166- page += 1 ;
167- }
168-
169- // 4. For each chain where we have owned tokens, fetch possible routes
170- const chainsWithOwnedTokens = Array . from (
171- new Set ( allOwnedTokens . map ( ( t ) => t . originToken . chainId ) ) ,
58+ url . searchParams . set (
59+ "destinationChainId" ,
60+ destinationToken . chainId . toString ( ) ,
17261 ) ;
173-
174- const allValidOriginTokens = new Map < string , Token > ( ) ;
175-
176- // Add destination token if included
177- if ( includeDestinationToken ) {
178- const tokenKey = `${
179- destinationToken . chainId
180- } -${ destinationToken . address . toLowerCase ( ) } `;
181- allValidOriginTokens . set ( tokenKey , destinationToken ) ;
182- }
183-
184- // Fetch routes for each chain with owned tokens
185- await Promise . all (
186- chainsWithOwnedTokens . map ( async ( chainId ) => {
187- try {
188- // TODO (bridge): this is quite inefficient, need to fix the popularity sorting to really capture all users tokens
189- const routesForChain = await routes ( {
190- client,
191- destinationChainId : destinationToken . chainId ,
192- destinationTokenAddress : destinationToken . address ,
193- includePrices : true ,
194- limit : 100 ,
195- maxSteps : 3 ,
196- originChainId : chainId ,
197- } ) ;
198-
199- // Add all origin tokens from this chain's routes
200- for ( const route of routesForChain ) {
201- // Skip if the origin token is the same as the destination token, will be added later only if includeDestinationToken is true
202- if (
203- route . originToken . chainId === destinationToken . chainId &&
204- route . originToken . address . toLowerCase ( ) ===
205- destinationToken . address . toLowerCase ( )
206- ) {
207- continue ;
208- }
209- const tokenKey = `${
210- route . originToken . chainId
211- } -${ route . originToken . address . toLowerCase ( ) } `;
212- allValidOriginTokens . set ( tokenKey , route . originToken ) ;
213- }
214- } catch ( error ) {
215- // Log error but don't fail the entire operation
216- console . warn ( `Failed to fetch routes for chain ${ chainId } :` , error ) ;
217- }
218- } ) ,
62+ url . searchParams . set ( "destinationTokenAddress" , destinationToken . address ) ;
63+ url . searchParams . set (
64+ "amount" ,
65+ toUnits ( destinationAmount , destinationToken . decimals ) . toString ( ) ,
21966 ) ;
22067
221- // 5. Filter owned tokens to only include valid origin tokens
222- const validOwnedTokens : OwnedTokenWithQuote [ ] = [ ] ;
223-
224- for ( const ownedToken of allOwnedTokens ) {
225- const tokenKey = `${
226- ownedToken . originToken . chainId
227- } -${ ownedToken . originToken . address . toLowerCase ( ) } `;
228- const validOriginToken = allValidOriginTokens . get ( tokenKey ) ;
229-
230- if ( validOriginToken ) {
231- validOwnedTokens . push ( {
232- balance : ownedToken . balance ,
233- originAmount : 0n ,
234- originToken : validOriginToken , // Use the token with pricing info from routes
235- } ) ;
236- }
68+ const clientFetch = getClientFetch ( client ) ;
69+ const response = await clientFetch ( url . toString ( ) ) ;
70+ if ( ! response . ok ) {
71+ const errorJson = await response . json ( ) ;
72+ throw new ApiError ( {
73+ code : errorJson . code || "UNKNOWN_ERROR" ,
74+ correlationId : errorJson . correlationId || undefined ,
75+ message : errorJson . message || response . statusText ,
76+ statusCode : response . status ,
77+ } ) ;
23778 }
23879
239- // Sort by dollar balance descending
240- validOwnedTokens . sort ( ( a , b ) => {
241- const aDollarBalance =
242- Number . parseFloat ( toTokens ( a . balance , a . originToken . decimals ) ) *
243- ( a . originToken . prices . USD || 0 ) ;
244- const bDollarBalance =
245- Number . parseFloat ( toTokens ( b . balance , b . originToken . decimals ) ) *
246- ( b . originToken . prices . USD || 0 ) ;
247- return bDollarBalance - aDollarBalance ;
248- } ) ;
249-
250- const suitableOriginTokens : OwnedTokenWithQuote [ ] = [ ] ;
251-
252- for ( const token of validOwnedTokens ) {
253- if (
254- includeDestinationToken &&
255- token . originToken . address . toLowerCase ( ) ===
256- destinationToken . address . toLowerCase ( ) &&
257- token . originToken . chainId === destinationToken . chainId
258- ) {
259- // Add same token to the front of the list
260- suitableOriginTokens . unshift ( token ) ;
261- continue ;
262- }
263-
264- suitableOriginTokens . push ( token ) ;
265- }
266-
267- const transformedRoutes = [
268- ...suitableOriginTokens . map ( ( s ) => ( {
269- balance : s . balance ,
270- originToken : s . originToken ,
271- payerWallet : wallet ,
272- type : "wallet" as const ,
273- } ) ) ,
274- ] ;
275- return transformedRoutes ;
80+ const {
81+ data : allValidOriginTokens ,
82+ } : { data : { quote : Quote ; balance : string ; token : Token } [ ] } =
83+ await response . json ( ) ;
84+
85+ // Sort by enough balance to pay THEN gross balance
86+ const validTokenQuotes = allValidOriginTokens . map ( ( s ) => ( {
87+ balance : BigInt ( s . balance ) ,
88+ originToken : s . token ,
89+ payerWallet : wallet ,
90+ type : "wallet" as const ,
91+ quote : s . quote ,
92+ } ) ) ;
93+ const insufficientBalanceQuotes = validTokenQuotes
94+ . filter ( ( s ) => s . balance < s . quote . originAmount )
95+ . sort ( ( a , b ) => {
96+ return (
97+ Number . parseFloat (
98+ toTokens ( a . quote . originAmount , a . originToken . decimals ) ,
99+ ) *
100+ ( a . originToken . prices . USD || 1 ) -
101+ Number . parseFloat (
102+ toTokens ( b . quote . originAmount , b . originToken . decimals ) ,
103+ ) *
104+ ( b . originToken . prices . USD || 1 )
105+ ) ;
106+ } ) ;
107+ const sufficientBalanceQuotes = validTokenQuotes
108+ . filter ( ( s ) => s . balance >= s . quote . originAmount )
109+ . sort ( ( a , b ) => {
110+ return (
111+ Number . parseFloat (
112+ toTokens ( b . quote . originAmount , b . originToken . decimals ) ,
113+ ) *
114+ ( b . originToken . prices . USD || 1 ) -
115+ Number . parseFloat (
116+ toTokens ( a . quote . originAmount , a . originToken . decimals ) ,
117+ ) *
118+ ( a . originToken . prices . USD || 1 )
119+ ) ;
120+ } ) ;
121+ // Move all sufficient balance quotes to the top
122+ return [ ...sufficientBalanceQuotes , ...insufficientBalanceQuotes ] ;
276123 } ,
277124 queryKey : [
278- "bridge-routes " ,
125+ "payment-methods " ,
279126 destinationToken . chainId ,
280127 destinationToken . address ,
281128 destinationAmount ,
@@ -287,11 +134,11 @@ export function usePaymentMethods(options: {
287134 } ) ;
288135
289136 return {
290- data : routesQuery . data || [ ] ,
291- error : routesQuery . error ,
292- isError : routesQuery . isError ,
293- isLoading : routesQuery . isLoading ,
294- isSuccess : routesQuery . isSuccess ,
295- refetch : routesQuery . refetch ,
137+ data : query . data || [ ] ,
138+ error : query . error ,
139+ isError : query . isError ,
140+ isLoading : query . isLoading ,
141+ isSuccess : query . isSuccess ,
142+ refetch : query . refetch ,
296143 } ;
297144}
0 commit comments