Skip to content

Commit e27cbf0

Browse files
committed
feat: faster quote generation
1 parent b371bf9 commit e27cbf0

File tree

3 files changed

+98
-296
lines changed

3 files changed

+98
-296
lines changed
Lines changed: 77 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
11
import { 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";
44
import type { Token } from "../../../bridge/types/Token.js";
5-
import {
6-
getCachedChain,
7-
getInsightEnabledChainIds,
8-
} from "../../../chains/utils.js";
95
import 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";
129
import type { Wallet } from "../../../wallets/interfaces/wallet.js";
13-
import {
14-
type GetWalletBalanceResult,
15-
getWalletBalance,
16-
} from "../../../wallets/utils/getWalletBalance.js";
1710
import type { PaymentMethod } from "../machines/paymentMachine.js";
1811
import { 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
}

packages/thirdweb/src/react/core/machines/paymentMachine.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useState } from "react";
2+
import type { Quote } from "../../../bridge/index.js";
23
import type { Token } from "../../../bridge/types/Token.js";
34
import type { Address } from "../../../utils/address.js";
45
import type { AsyncStorage } from "../../../utils/storage/AsyncStorage.js";
@@ -24,6 +25,7 @@ export type PaymentMethod =
2425
payerWallet: Wallet;
2526
originToken: Token;
2627
balance: bigint;
28+
quote: Quote;
2729
}
2830
| {
2931
type: "fiat";

0 commit comments

Comments
 (0)