Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
307 changes: 77 additions & 230 deletions packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import { chains } from "../../../bridge/Chains.js";
import { routes } from "../../../bridge/Routes.js";
import type { Quote } from "../../../bridge/index.js";
import { ApiError } from "../../../bridge/types/Errors.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 { toTokens, toUnits } 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
Expand Down Expand Up @@ -57,225 +44,85 @@
const localWallet = useActiveWallet(); // TODO (bridge): get all connected wallets
const wallet = payerWallet || localWallet;

const routesQuery = useQuery({
const query = useQuery({

Check warning on line 47 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L47

Added line #L47 was not covered by tests
enabled: !!wallet,
queryFn: async (): Promise<PaymentMethod[]> => {
if (!wallet) {
const account = wallet?.getAccount();
if (!wallet || !account) {

Check warning on line 51 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L50-L51

Added lines #L50 - L51 were not covered by tests
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 url = new URL(
`${getThirdwebBaseUrl("bridge")}/v1/buy/quote/${account.address}`,

Check warning on line 56 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L55-L56

Added lines #L55 - L56 were not covered by tests
);

// 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(
"destinationChainId",
destinationToken.chainId.toString(),

Check warning on line 60 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L58-L60

Added lines #L58 - L60 were not covered by tests
);

const allValidOriginTokens = new Map<string, Token>();

// 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.set("destinationTokenAddress", destinationToken.address);
url.searchParams.set(
"amount",
toUnits(destinationAmount, destinationToken.decimals).toString(),

Check warning on line 65 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L62-L65

Added lines #L62 - L65 were not covered by tests
);

// 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 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,
});

Check warning on line 77 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L68-L77

Added lines #L68 - L77 were not covered by tests
}

// 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;
const {
data: allValidOriginTokens,
}: { data: { quote: Quote; balance: string; token: Token }[] } =
await response.json();

Check warning on line 83 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L80-L83

Added lines #L80 - L83 were not covered by tests
Comment on lines +80 to +83
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add response data validation.

The response data is cast to a specific type without validation. Consider adding runtime validation to ensure the API response matches the expected structure.

// Add validation before casting
if (!Array.isArray(responseData.data)) {
  throw new ApiError({
    code: "INVALID_RESPONSE",
    message: "Invalid response format from API",
    statusCode: 200,
  });
}
🤖 Prompt for AI Agents
In packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts around lines 80
to 83, the API response data is directly cast to a specific type without runtime
validation. To fix this, add a check to verify that the response data matches
the expected structure, such as confirming that responseData.data is an array
before casting. If the validation fails, throw an ApiError with an appropriate
message and status code to handle invalid response formats safely.


// 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)

Check warning on line 104 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L86-L104

Added lines #L86 - L104 were not covered by tests
);
});
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)

Check warning on line 118 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L106-L118

Added lines #L106 - L118 were not covered by tests
);
});

Check warning on line 120 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L120

Added line #L120 was not covered by tests
// Move all sufficient balance quotes to the top
return [...sufficientBalanceQuotes, ...insufficientBalanceQuotes];

Check warning on line 122 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L122

Added line #L122 was not covered by tests
},
queryKey: [
"bridge-routes",
"payment-methods",

Check warning on line 125 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L125

Added line #L125 was not covered by tests
destinationToken.chainId,
destinationToken.address,
destinationAmount,
Expand All @@ -287,11 +134,11 @@
});

return {
data: routesQuery.data || [],
error: routesQuery.error,
isError: routesQuery.isError,
isLoading: routesQuery.isLoading,
isSuccess: routesQuery.isSuccess,
refetch: routesQuery.refetch,
data: query.data || [],
error: query.error,
isError: query.isError,
isLoading: query.isLoading,
isSuccess: query.isSuccess,
refetch: query.refetch,

Check warning on line 142 in packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts

View check run for this annotation

Codecov / codecov/patch

packages/thirdweb/src/react/core/hooks/usePaymentMethods.ts#L137-L142

Added lines #L137 - L142 were not covered by tests
};
}
2 changes: 2 additions & 0 deletions packages/thirdweb/src/react/core/machines/paymentMachine.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -24,6 +25,7 @@ export type PaymentMethod =
payerWallet: Wallet;
originToken: Token;
balance: bigint;
quote: Quote;
}
| {
type: "fiat";
Expand Down
Loading
Loading