Skip to content

Commit 496615f

Browse files
[SDK] Improve token info discovery for x402 payments (#8142)
1 parent e2931df commit 496615f

File tree

14 files changed

+769
-280
lines changed

14 files changed

+769
-280
lines changed

.changeset/dirty-experts-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": patch
3+
---
4+
5+
Improve token info discovery for x402 payments
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"use client";
2+
3+
import type React from "react";
4+
import { useId, useState } from "react";
5+
import { defineChain } from "thirdweb/chains";
6+
import { BridgeNetworkSelector } from "@/components/blocks/NetworkSelectors";
7+
import { Input } from "@/components/ui/input";
8+
import { Label } from "@/components/ui/label";
9+
import {
10+
Select,
11+
SelectContent,
12+
SelectItem,
13+
SelectTrigger,
14+
SelectValue,
15+
} from "@/components/ui/select";
16+
import { TokenSelector } from "@/components/ui/TokenSelector";
17+
import { THIRDWEB_CLIENT } from "@/lib/client";
18+
import type { TokenMetadata } from "@/lib/types";
19+
import type { X402PlaygroundOptions } from "./types";
20+
21+
export function X402LeftSection(props: {
22+
options: X402PlaygroundOptions;
23+
setOptions: React.Dispatch<React.SetStateAction<X402PlaygroundOptions>>;
24+
}) {
25+
const { options, setOptions } = props;
26+
27+
// Local state for chain and token selection
28+
const [selectedChain, setSelectedChain] = useState<number | undefined>(() => {
29+
return options.chain?.id;
30+
});
31+
32+
const [selectedToken, setSelectedToken] = useState<
33+
{ chainId: number; address: string } | undefined
34+
>(() => {
35+
if (options.tokenAddress && options.chain?.id) {
36+
return {
37+
address: options.tokenAddress,
38+
chainId: options.chain.id,
39+
};
40+
}
41+
return undefined;
42+
});
43+
44+
const chainId = useId();
45+
const tokenId = useId();
46+
const amountId = useId();
47+
const payToId = useId();
48+
const waitUntilId = useId();
49+
50+
const handleChainChange = (chainId: number) => {
51+
setSelectedChain(chainId);
52+
// Clear token selection when chain changes
53+
setSelectedToken(undefined);
54+
55+
setOptions((v) => ({
56+
...v,
57+
chain: defineChain(chainId),
58+
tokenAddress: "0x0000000000000000000000000000000000000000" as const,
59+
tokenSymbol: "",
60+
tokenDecimals: 18,
61+
}));
62+
};
63+
64+
const handleTokenChange = (token: TokenMetadata) => {
65+
setSelectedToken({
66+
address: token.address,
67+
chainId: selectedChain!,
68+
});
69+
70+
setOptions((v) => ({
71+
...v,
72+
tokenAddress: token.address as `0x${string}`,
73+
tokenSymbol: token.symbol ?? "",
74+
tokenDecimals: token.decimals ?? 18,
75+
}));
76+
};
77+
78+
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
79+
setOptions((v) => ({
80+
...v,
81+
amount: e.target.value,
82+
}));
83+
};
84+
85+
const handlePayToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
86+
setOptions((v) => ({
87+
...v,
88+
payTo: e.target.value as `0x${string}`,
89+
}));
90+
};
91+
92+
const handleWaitUntilChange = (
93+
value: "simulated" | "submitted" | "confirmed",
94+
) => {
95+
setOptions((v) => ({
96+
...v,
97+
waitUntil: value,
98+
}));
99+
};
100+
101+
return (
102+
<div className="space-y-6">
103+
<div>
104+
<h2 className="mb-4 text-xl font-semibold">Configuration</h2>
105+
<div className="space-y-4">
106+
{/* Chain selection */}
107+
<div className="flex flex-col gap-2">
108+
<Label htmlFor={chainId}>Chain</Label>
109+
<BridgeNetworkSelector
110+
chainId={selectedChain}
111+
onChange={handleChainChange}
112+
placeholder="Select a chain"
113+
className="bg-card"
114+
/>
115+
</div>
116+
117+
{/* Token selection - only show if chain is selected */}
118+
{selectedChain && (
119+
<div className="flex flex-col gap-2">
120+
<Label htmlFor={tokenId}>Token</Label>
121+
<TokenSelector
122+
chainId={selectedChain}
123+
client={THIRDWEB_CLIENT}
124+
enabled={true}
125+
onChange={handleTokenChange}
126+
placeholder="Select a token"
127+
selectedToken={selectedToken}
128+
className="bg-card"
129+
/>
130+
</div>
131+
)}
132+
133+
{/* Amount input */}
134+
<div className="flex flex-col gap-2">
135+
<Label htmlFor={amountId}>Amount</Label>
136+
<Input
137+
id={amountId}
138+
type="text"
139+
placeholder="0.01"
140+
value={options.amount}
141+
onChange={handleAmountChange}
142+
className="bg-card"
143+
/>
144+
{options.tokenSymbol && (
145+
<p className="text-sm text-muted-foreground">
146+
Amount in {options.tokenSymbol}
147+
</p>
148+
)}
149+
</div>
150+
151+
{/* Pay To input */}
152+
<div className="flex flex-col gap-2">
153+
<Label htmlFor={payToId}>Pay To Address</Label>
154+
<Input
155+
id={payToId}
156+
type="text"
157+
placeholder="0x..."
158+
value={options.payTo}
159+
onChange={handlePayToChange}
160+
className="bg-card"
161+
/>
162+
<p className="text-sm text-muted-foreground">
163+
The wallet address that will receive the payment
164+
</p>
165+
</div>
166+
167+
{/* Wait Until selection */}
168+
<div className="flex flex-col gap-2">
169+
<Label htmlFor={waitUntilId}>Wait Until</Label>
170+
<Select
171+
value={options.waitUntil}
172+
onValueChange={handleWaitUntilChange}
173+
>
174+
<SelectTrigger className="bg-card">
175+
<SelectValue placeholder="Select wait condition" />
176+
</SelectTrigger>
177+
<SelectContent>
178+
<SelectItem value="simulated">Simulated</SelectItem>
179+
<SelectItem value="submitted">Submitted</SelectItem>
180+
<SelectItem value="confirmed">Confirmed</SelectItem>
181+
</SelectContent>
182+
</Select>
183+
<p className="text-sm text-muted-foreground">
184+
When to consider the payment settled: simulated (fastest),
185+
submitted (medium), or confirmed (most secure)
186+
</p>
187+
</div>
188+
</div>
189+
</div>
190+
</div>
191+
);
192+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client";
2+
3+
import React, { useState } from "react";
4+
import { useActiveAccount } from "thirdweb/react";
5+
import { chain, token } from "./constants";
6+
import type { X402PlaygroundOptions } from "./types";
7+
import { X402LeftSection } from "./X402LeftSection";
8+
import { X402RightSection } from "./X402RightSection";
9+
10+
const defaultOptions: X402PlaygroundOptions = {
11+
chain: chain,
12+
tokenAddress: token.address as `0x${string}`,
13+
tokenSymbol: token.symbol,
14+
tokenDecimals: token.decimals,
15+
amount: "0.01",
16+
payTo: "0x0000000000000000000000000000000000000000",
17+
waitUntil: "simulated",
18+
};
19+
20+
export function X402Playground() {
21+
const [options, setOptions] = useState<X402PlaygroundOptions>(defaultOptions);
22+
const activeAccount = useActiveAccount();
23+
24+
// Update payTo address when wallet connects, but only if it's still the default
25+
React.useEffect(() => {
26+
if (
27+
activeAccount?.address &&
28+
options.payTo === "0x0000000000000000000000000000000000000000"
29+
) {
30+
setOptions((prev) => ({
31+
...prev,
32+
payTo: activeAccount.address as `0x${string}`,
33+
}));
34+
}
35+
}, [activeAccount?.address, options.payTo]);
36+
37+
return (
38+
<div className="relative flex flex-col-reverse gap-6 xl:min-h-[900px] xl:flex-row xl:gap-6">
39+
<div className="grow border-b pb-10 xl:mb-0 xl:border-r xl:border-b-0 xl:pr-6">
40+
<X402LeftSection options={options} setOptions={setOptions} />
41+
</div>
42+
<X402RightSection options={options} />
43+
</div>
44+
);
45+
}

0 commit comments

Comments
 (0)