Skip to content

Commit 2998cfe

Browse files
UI polish for transactions and tokens
1 parent 74faebe commit 2998cfe

File tree

12 files changed

+648
-216
lines changed

12 files changed

+648
-216
lines changed

packages/thirdweb/src/react/core/hooks/useBridgeQuote.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use client";
22
import { useQuery } from "@tanstack/react-query";
33
import * as Buy from "../../../bridge/Buy.js";
4+
import * as Transfer from "../../../bridge/Transfer.js";
45
import type { Token } from "../../../bridge/types/Token.js";
56
import type { ThirdwebClient } from "../../../client/client.js";
67
import { toUnits } from "../../../utils/units.js";
@@ -35,6 +36,23 @@ export function useBridgeQuote({
3536
destinationToken.decimals,
3637
);
3738

39+
// if ssame token and chain, use transfer
40+
if (
41+
originToken.address.toLowerCase() ===
42+
destinationToken.address.toLowerCase() &&
43+
originToken.chainId === destinationToken.chainId
44+
) {
45+
const transfer = await Transfer.prepare({
46+
client,
47+
chainId: originToken.chainId,
48+
tokenAddress: originToken.address,
49+
sender: originToken.address,
50+
receiver: destinationToken.address,
51+
amount: destinationAmountWei,
52+
});
53+
return transfer;
54+
}
55+
3856
const quote = await Buy.quote({
3957
originChainId: originToken.chainId,
4058
originTokenAddress: originToken.address,

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

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -37,38 +37,49 @@ export function usePaymentMethods(options: {
3737
destinationToken: Token;
3838
destinationAmount: string;
3939
client: ThirdwebClient;
40-
activeWallet?: Wallet;
40+
payerWallet?: Wallet;
41+
includeDestinationToken?: boolean;
4142
}) {
42-
const { destinationToken, destinationAmount, client, activeWallet } = options;
43+
const {
44+
destinationToken,
45+
destinationAmount,
46+
client,
47+
payerWallet,
48+
includeDestinationToken,
49+
} = options;
4350
const localWallet = useActiveWallet(); // TODO (bridge): get all connected wallets
44-
const wallet = activeWallet || localWallet;
51+
const wallet = payerWallet || localWallet;
4552

4653
const routesQuery = useQuery({
4754
queryKey: [
4855
"bridge-routes",
4956
destinationToken.chainId,
5057
destinationToken.address,
5158
destinationAmount,
52-
activeWallet?.getAccount()?.address,
59+
payerWallet?.getAccount()?.address,
60+
includeDestinationToken,
5361
],
5462
queryFn: async (): Promise<PaymentMethod[]> => {
5563
if (!wallet) {
5664
throw new Error("No wallet connected");
5765
}
58-
console.time("routes");
5966
const allRoutes = await routes({
6067
client,
6168
destinationChainId: destinationToken.chainId,
6269
destinationTokenAddress: destinationToken.address,
6370
sortBy: "popularity",
6471
includePrices: true,
72+
maxSteps: 3,
6573
limit: 100, // Get top 100 most popular routes
6674
});
67-
console.log("allRoutes", allRoutes);
75+
76+
const allOriginTokens = includeDestinationToken
77+
? [destinationToken, ...allRoutes.map((route) => route.originToken)]
78+
: allRoutes.map((route) => route.originToken);
6879

6980
// 1. Resolve all unique chains in the supported token map
7081
const uniqueChains = Array.from(
71-
new Set(allRoutes.map((route) => route.originToken.chainId)),
82+
new Set(allOriginTokens.map((t) => t.chainId)),
7283
);
7384

7485
// 2. Check insight availability once per chain
@@ -95,9 +106,6 @@ export function usePaymentMethods(options: {
95106
page,
96107
metadata: "false",
97108
},
98-
}).catch((err) => {
99-
console.error("error fetching balances from insight", err);
100-
return [];
101109
});
102110

103111
if (batch.length === 0) {
@@ -107,12 +115,11 @@ export function usePaymentMethods(options: {
107115
// find matching origin token in allRoutes
108116
const tokensWithBalance = batch
109117
.map((b) => ({
110-
originToken: allRoutes.find(
118+
originToken: allOriginTokens.find(
111119
(t) =>
112-
t.originToken.address.toLowerCase() ===
113-
b.tokenAddress.toLowerCase() &&
114-
t.originToken.chainId === b.chainId,
115-
)?.originToken,
120+
t.address.toLowerCase() === b.tokenAddress.toLowerCase() &&
121+
t.chainId === b.chainId,
122+
),
116123
balance: b.value,
117124
originAmount: 0n,
118125
}))
@@ -124,8 +131,8 @@ export function usePaymentMethods(options: {
124131

125132
const requiredDollarAmount =
126133
Number.parseFloat(destinationAmount) * destinationToken.priceUsd;
127-
console.log("requiredDollarAmount", requiredDollarAmount);
128134

135+
// sort by dollar balance descending
129136
owned.sort((a, b) => {
130137
const aDollarBalance =
131138
Number.parseFloat(toTokens(a.balance, a.originToken.decimals)) *
@@ -143,23 +150,22 @@ export function usePaymentMethods(options: {
143150
const dollarBalance =
144151
Number.parseFloat(toTokens(b.balance, b.originToken.decimals)) *
145152
b.originToken.priceUsd;
146-
console.log(
147-
"required amount for",
148-
b.originToken.symbol,
149-
"is",
150-
requiredDollarAmount,
151-
"Price is",
152-
b.originToken.priceUsd,
153-
"Chain is",
154-
b.originToken.chainId,
155-
);
156-
console.log("dollarBalance", dollarBalance);
157153
if (b.originToken.priceUsd && dollarBalance < requiredDollarAmount) {
158-
console.log(
159-
"skipping",
160-
b.originToken.symbol,
161-
"because it's not enough",
162-
);
154+
continue;
155+
}
156+
157+
if (
158+
includeDestinationToken &&
159+
b.originToken.address.toLowerCase() ===
160+
destinationToken.address.toLowerCase() &&
161+
b.originToken.chainId === destinationToken.chainId
162+
) {
163+
// add same token to the front of the list
164+
suitableOriginTokens.unshift({
165+
balance: b.balance,
166+
originAmount: 0n,
167+
originToken: b.originToken,
168+
});
163169
continue;
164170
}
165171

@@ -171,9 +177,6 @@ export function usePaymentMethods(options: {
171177
}
172178
}
173179

174-
console.log("suitableOriginTokens", suitableOriginTokens.length);
175-
console.timeEnd("routes");
176-
177180
const transformedRoutes = [
178181
...suitableOriginTokens.map((s) => ({
179182
type: "wallet" as const,
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import type { AbiFunction } from "abitype";
3+
import { toFunctionSelector } from "viem";
4+
import type { Token } from "../../../bridge/index.js";
5+
import type { ThirdwebClient } from "../../../client/client.js";
6+
import { NATIVE_TOKEN_ADDRESS } from "../../../constants/addresses.js";
7+
import type { CompilerMetadata } from "../../../contract/actions/compiler-metadata.js";
8+
import { getCompilerMetadata } from "../../../contract/actions/get-compiler-metadata.js";
9+
import { getContract } from "../../../contract/contract.js";
10+
import { decimals } from "../../../extensions/erc20/read/decimals.js";
11+
import { getToken } from "../../../pay/convert/get-token.js";
12+
import { encode } from "../../../transaction/actions/encode.js";
13+
import type { PreparedTransaction } from "../../../transaction/prepare-transaction.js";
14+
import { getTransactionGasCost } from "../../../transaction/utils.js";
15+
import { resolvePromisedValue } from "../../../utils/promise/resolve-promised-value.js";
16+
import { toTokens } from "../../../utils/units.js";
17+
import {
18+
formatCurrencyAmount,
19+
formatTokenAmount,
20+
} from "../../web/ui/ConnectWallet/screens/formatTokenBalance.js";
21+
import { useChainMetadata } from "./others/useChainQuery.js";
22+
23+
export interface TransactionDetails {
24+
contractMetadata: CompilerMetadata | null;
25+
functionInfo: {
26+
functionName: string;
27+
selector: string;
28+
description?: string;
29+
};
30+
usdValueDisplay: string | null;
31+
txCostDisplay: string;
32+
gasCostDisplay: string | null;
33+
tokenInfo: Token | null;
34+
costWei: bigint;
35+
gasCostWei: bigint | null;
36+
totalCost: string;
37+
totalCostWei: bigint;
38+
}
39+
40+
export interface UseTransactionDetailsOptions {
41+
transaction: PreparedTransaction;
42+
client: ThirdwebClient;
43+
}
44+
45+
/**
46+
* Hook to fetch comprehensive transaction details including contract metadata,
47+
* function information, cost calculations, and gas estimates.
48+
*/
49+
export function useTransactionDetails({
50+
transaction,
51+
client,
52+
}: UseTransactionDetailsOptions) {
53+
const chainMetadata = useChainMetadata(transaction.chain);
54+
55+
return useQuery({
56+
queryKey: [
57+
"transaction-details",
58+
transaction.to,
59+
transaction.chain.id,
60+
transaction.erc20Value,
61+
],
62+
queryFn: async (): Promise<TransactionDetails> => {
63+
// Create contract instance for metadata fetching
64+
const contract = getContract({
65+
client,
66+
chain: transaction.chain,
67+
address: transaction.to as string,
68+
});
69+
70+
const [contractMetadata, value, erc20Value, transactionData] =
71+
await Promise.all([
72+
getCompilerMetadata(contract).catch(() => null),
73+
resolvePromisedValue(transaction.value),
74+
resolvePromisedValue(transaction.erc20Value),
75+
encode(transaction).catch(() => "0x"),
76+
]);
77+
78+
const [tokenInfo, gasCostWei] = await Promise.all([
79+
getToken(
80+
client,
81+
erc20Value ? erc20Value.tokenAddress : NATIVE_TOKEN_ADDRESS,
82+
transaction.chain.id,
83+
).catch(() => null),
84+
getTransactionGasCost(transaction).catch(() => null),
85+
]);
86+
87+
// Process function info from ABI if available
88+
let functionInfo = {
89+
functionName: "Contract Call",
90+
selector: "0x",
91+
description: undefined,
92+
};
93+
94+
if (contractMetadata?.abi && transactionData.length >= 10) {
95+
try {
96+
const selector = transactionData.slice(0, 10) as `0x${string}`;
97+
const abi = contractMetadata.abi;
98+
99+
// Find matching function in ABI
100+
const abiItems = Array.isArray(abi) ? abi : [];
101+
const functions = abiItems
102+
.filter(
103+
(item) =>
104+
item &&
105+
typeof item === "object" &&
106+
"type" in item &&
107+
(item as { type: string }).type === "function",
108+
)
109+
.map((item) => item as AbiFunction);
110+
111+
const matchingFunction = functions.find((fn) => {
112+
return toFunctionSelector(fn) === selector;
113+
});
114+
115+
if (matchingFunction) {
116+
functionInfo = {
117+
functionName: matchingFunction.name,
118+
selector,
119+
description: undefined, // Skip devdoc for now
120+
};
121+
}
122+
} catch {
123+
// Keep default values
124+
}
125+
}
126+
127+
const resolveDecimals = async () => {
128+
if (tokenInfo) {
129+
return tokenInfo.decimals;
130+
}
131+
if (erc20Value) {
132+
return decimals({
133+
contract: getContract({
134+
client,
135+
chain: transaction.chain,
136+
address: erc20Value.tokenAddress,
137+
}),
138+
});
139+
}
140+
return 18;
141+
};
142+
143+
const decimal = await resolveDecimals();
144+
const costWei = erc20Value ? erc20Value.amountWei : value || 0n;
145+
const nativeTokenSymbol =
146+
chainMetadata.data?.nativeCurrency?.symbol || "ETH";
147+
const tokenSymbol = tokenInfo?.symbol || nativeTokenSymbol;
148+
149+
const totalCostWei = erc20Value
150+
? erc20Value.amountWei
151+
: (value || 0n) + (gasCostWei || 0n);
152+
const totalCost = toTokens(totalCostWei, decimal);
153+
154+
const usdValue = tokenInfo?.priceUsd
155+
? Number(totalCost) * tokenInfo.priceUsd
156+
: null;
157+
158+
return {
159+
contractMetadata,
160+
functionInfo,
161+
usdValueDisplay: usdValue
162+
? formatCurrencyAmount("USD", usdValue)
163+
: null,
164+
txCostDisplay: `${formatTokenAmount(costWei, decimal)} ${tokenSymbol}`,
165+
gasCostDisplay: gasCostWei
166+
? `${formatTokenAmount(gasCostWei, 18)} ${nativeTokenSymbol}`
167+
: null,
168+
tokenInfo,
169+
costWei,
170+
gasCostWei,
171+
totalCost,
172+
totalCostWei,
173+
};
174+
},
175+
enabled: !!transaction.to && !!chainMetadata.data,
176+
});
177+
}

packages/thirdweb/src/react/web/ui/Bridge/BridgeOrchestrator.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,18 +241,21 @@ export function BridgeOrchestrator({
241241

242242
{state.value === "methodSelection" &&
243243
state.context.destinationToken &&
244-
state.context.destinationAmount && (
244+
state.context.destinationAmount &&
245+
state.context.receiverAddress && (
245246
<PaymentSelection
246247
destinationToken={state.context.destinationToken}
247248
client={client}
248249
destinationAmount={state.context.destinationAmount}
250+
receiverAddress={state.context.receiverAddress}
249251
onPaymentMethodSelected={handlePaymentMethodSelected}
250252
onError={handleError}
251253
onBack={() => {
252254
send({ type: "BACK" });
253255
}}
254256
connectOptions={connectOptions}
255257
connectLocale={connectLocale || en}
258+
includeDestinationToken={uiOptions.mode !== "fund_wallet"}
256259
/>
257260
)}
258261

0 commit comments

Comments
 (0)