Skip to content

Commit c39f3f4

Browse files
Add Engine user transactions example to playground
1 parent c92081b commit c39f3f4

File tree

5 files changed

+519
-115
lines changed

5 files changed

+519
-115
lines changed

apps/playground-web/src/app/navLinks.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ const transactions: ShadcnSidebarLink = {
219219
href: "/transactions",
220220
exactMatch: true,
221221
},
222+
{
223+
href: "/transactions/users",
224+
label: "From User Wallets",
225+
},
222226
{
223227
href: "/transactions/airdrop-tokens",
224228
label: "Airdrop Tokens",
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { User2Icon } from "lucide-react";
2+
import type { Metadata } from "next";
3+
import { GatewayPreview } from "@/components/account-abstraction/gateway";
4+
import { PageLayout } from "@/components/blocks/APIHeader";
5+
import { CodeExample } from "@/components/code/code-example";
6+
import ThirdwebProvider from "@/components/thirdweb-provider";
7+
import { metadataBase } from "@/lib/constants";
8+
9+
export const metadata: Metadata = {
10+
description: "Transactions from user wallets with monitoring and retries",
11+
metadataBase,
12+
title: "User Transactions | thirdweb",
13+
};
14+
15+
export default function Page() {
16+
return (
17+
<ThirdwebProvider>
18+
<PageLayout
19+
icon={User2Icon}
20+
description={
21+
<>Transactions from user wallets with monitoring and retries.</>
22+
}
23+
docsLink="https://portal.thirdweb.com/transactions?utm_source=playground"
24+
title="User Transactions"
25+
>
26+
<UserTransactions />
27+
</PageLayout>
28+
</ThirdwebProvider>
29+
);
30+
}
31+
32+
function UserTransactions() {
33+
return (
34+
<>
35+
<CodeExample
36+
code={`\
37+
import { inAppWallet } from "thirdweb/wallets/in-app";
38+
import { ConnectButton, useActiveAccount } from "thirdweb/react";
39+
40+
const wallet = inAppWallet();
41+
42+
function App() {
43+
const activeWallet = useActiveWallet();
44+
45+
const handleClick = async () => {
46+
const walletAddress = activeWallet?.getAccount()?.address;
47+
// transactions are a simple POST request to the thirdweb API
48+
// or use the @thirdweb-dev/api type-safe JS SDK
49+
const response = await fetch(
50+
"https://api.thirdweb.com/v1/contract/write",
51+
{
52+
method: "POST",
53+
headers: {
54+
"Content-Type": "application/json",
55+
"x-client-id": "<your-project-client-id>",
56+
// uses the in-app wallet's auth token to authenticate the request
57+
"Authorization": "Bearer " + activeWallet?.getAuthToken?.(),
58+
},
59+
body: JSON.stringify({
60+
chainId: "84532",
61+
calls: [
62+
{
63+
contractAddress: "0x...",
64+
method: "function claim(address to, uint256 amount)",
65+
params: [walletAddress, "1"],
66+
},
67+
],
68+
}),
69+
});
70+
};
71+
72+
return (
73+
<>
74+
<ConnectButton
75+
client={client}
76+
wallet={[wallet]}
77+
connectButton={{
78+
label: "Login to mint!",
79+
}}
80+
/>
81+
<Button
82+
onClick={handleClick}
83+
>
84+
Mint
85+
</Button>
86+
</>
87+
);
88+
}`}
89+
header={{
90+
description:
91+
"Engine can queue, monitor, and retry transactions from your users in-app wallets. All transactions and analytics will be displayed in your developer dashboard.",
92+
title: "Transactions from User Wallets",
93+
}}
94+
lang="tsx"
95+
preview={<GatewayPreview />}
96+
/>
97+
</>
98+
);
99+
}
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
"use client";
2+
3+
import { useQuery } from "@tanstack/react-query";
4+
import { useState } from "react";
5+
import { encode, getContract } from "thirdweb";
6+
import { baseSepolia } from "thirdweb/chains";
7+
import { claimTo, getNFT, getOwnedNFTs } from "thirdweb/extensions/erc1155";
8+
import {
9+
ConnectButton,
10+
MediaRenderer,
11+
useActiveAccount,
12+
useActiveWallet,
13+
useDisconnect,
14+
useReadContract,
15+
} from "thirdweb/react";
16+
import { stringify } from "thirdweb/utils";
17+
import { inAppWallet } from "thirdweb/wallets/in-app";
18+
import { THIRDWEB_CLIENT } from "../../lib/client";
19+
import { Badge } from "../ui/badge";
20+
import { Button } from "../ui/button";
21+
import {
22+
Table,
23+
TableBody,
24+
TableCell,
25+
TableContainer,
26+
TableHead,
27+
TableHeader,
28+
TableRow,
29+
} from "../ui/table";
30+
31+
const url = `https://${process.env.NEXT_PUBLIC_API_URL}`;
32+
33+
const chain = baseSepolia;
34+
const editionDropAddress = "0x638263e3eAa3917a53630e61B1fBa685308024fa";
35+
const editionDropTokenId = 2n;
36+
37+
const editionDropContract = getContract({
38+
address: editionDropAddress,
39+
chain,
40+
client: THIRDWEB_CLIENT,
41+
});
42+
43+
const iaw = inAppWallet();
44+
45+
function TransactionRow({ transactionId }: { transactionId: string }) {
46+
const { data: txStatus, isLoading } = useQuery({
47+
enabled: !!transactionId,
48+
queryFn: async () => {
49+
const response = await fetch(`${url}/v1/transactions/${transactionId}`, {
50+
headers: {
51+
"Content-type": "application/json",
52+
"x-client-id": THIRDWEB_CLIENT.clientId,
53+
},
54+
});
55+
56+
if (!response.ok) {
57+
const text = await response.text();
58+
throw new Error(
59+
`Failed to send transaction: ${response.statusText} - ${text}`,
60+
);
61+
}
62+
63+
const results = await response.json();
64+
const transaction = results.result;
65+
66+
return transaction;
67+
},
68+
queryKey: ["txStatus", transactionId],
69+
refetchInterval: 2000,
70+
});
71+
72+
const getStatusBadge = (status: string) => {
73+
switch (status) {
74+
case undefined:
75+
case "QUEUED":
76+
return <Badge variant="warning">Queued</Badge>;
77+
case "SUBMITTED":
78+
return <Badge variant="default">Submitted</Badge>;
79+
case "CONFIRMED":
80+
return <Badge variant="success">Confirmed</Badge>;
81+
case "FAILED":
82+
return <Badge variant="destructive">Failed</Badge>;
83+
default:
84+
return <Badge variant="outline">{"Unknown"}</Badge>;
85+
}
86+
};
87+
88+
const renderTransactionHash = () => {
89+
if (!txStatus) return "-";
90+
91+
const execStatus = txStatus?.status;
92+
93+
let txHash: string | undefined;
94+
if (execStatus === "CONFIRMED") {
95+
txHash = txStatus.transactionHash;
96+
}
97+
98+
if (txHash && chain.blockExplorers?.[0]?.url) {
99+
return (
100+
<a
101+
className="text-blue-500 hover:text-blue-700 underline font-mono text-sm"
102+
href={`${chain.blockExplorers[0].url}/tx/${txHash}`}
103+
rel="noopener noreferrer"
104+
target="_blank"
105+
>
106+
{txHash.slice(0, 6)}...{txHash.slice(-4)}
107+
</a>
108+
);
109+
}
110+
111+
return txHash ? (
112+
<span className="font-mono text-sm">
113+
{txHash.slice(0, 6)}...{txHash.slice(-4)}
114+
</span>
115+
) : (
116+
"-"
117+
);
118+
};
119+
120+
return (
121+
<TableRow>
122+
<TableCell className="font-mono text-sm">
123+
{transactionId.slice(0, 8)}...{transactionId.slice(-4)}
124+
</TableCell>
125+
<TableCell>
126+
{isLoading || !txStatus.executionResult?.status ? (
127+
<Badge variant="warning">Queued</Badge>
128+
) : (
129+
getStatusBadge(txStatus.executionResult?.status)
130+
)}
131+
</TableCell>
132+
<TableCell>{renderTransactionHash()}</TableCell>
133+
</TableRow>
134+
);
135+
}
136+
137+
export function GatewayPreview() {
138+
const [txIds, setTxIds] = useState<string[]>([]);
139+
const activeEOA = useActiveAccount();
140+
const activeWallet = useActiveWallet();
141+
const { disconnect } = useDisconnect();
142+
const { data: nft, isLoading: isNftLoading } = useReadContract(getNFT, {
143+
contract: editionDropContract,
144+
tokenId: editionDropTokenId,
145+
});
146+
const { data: ownedNfts } = useReadContract(getOwnedNFTs, {
147+
// biome-ignore lint/style/noNonNullAssertion: handled by queryOptions
148+
address: activeEOA?.address!,
149+
contract: editionDropContract,
150+
queryOptions: { enabled: !!activeEOA, refetchInterval: 2000 },
151+
useIndexer: false,
152+
});
153+
154+
const { data: preparedTx } = useQuery({
155+
enabled: !!activeEOA,
156+
queryFn: async () => {
157+
if (!activeEOA) {
158+
throw new Error("No active EOA");
159+
}
160+
const tx = claimTo({
161+
contract: editionDropContract,
162+
quantity: 1n,
163+
to: activeEOA.address,
164+
tokenId: editionDropTokenId,
165+
});
166+
return {
167+
data: await encode(tx),
168+
to: editionDropContract.address,
169+
};
170+
},
171+
queryKey: ["tx", activeEOA?.address],
172+
});
173+
174+
if (activeEOA && activeWallet && activeWallet?.id !== iaw.id) {
175+
return (
176+
<div className="flex flex-col items-center justify-center gap-4">
177+
Please connect with an in-app wallet for this example
178+
<Button
179+
onClick={() => {
180+
disconnect(activeWallet);
181+
}}
182+
>
183+
Disconnect current wallet
184+
</Button>
185+
</div>
186+
);
187+
}
188+
189+
const handleClick = async () => {
190+
if (!preparedTx || !activeEOA) {
191+
return;
192+
}
193+
194+
const response = await fetch(`${url}/v1/transactions`, {
195+
body: stringify({
196+
from: activeEOA.address,
197+
chainId: baseSepolia.id,
198+
transactions: [preparedTx],
199+
}),
200+
headers: {
201+
Authorization: `Bearer ${iaw.getAuthToken?.()}`,
202+
"Content-type": "application/json",
203+
"x-client-id": THIRDWEB_CLIENT.clientId,
204+
},
205+
method: "POST",
206+
});
207+
208+
if (!response.ok) {
209+
const text = await response.text();
210+
throw new Error(
211+
`Failed to send transaction: ${response.statusText} - ${text}`,
212+
);
213+
}
214+
215+
const results = await response.json();
216+
const txId = results.result?.transactionIds?.[0];
217+
if (!txId) {
218+
throw new Error("No transaction ID");
219+
}
220+
221+
setTxIds((prev) => [...prev, txId]);
222+
};
223+
224+
return (
225+
<div className="flex flex-col items-center justify-center gap-4">
226+
{isNftLoading ? (
227+
<div className="mt-24 w-full">Loading...</div>
228+
) : (
229+
<>
230+
<div className="flex flex-col justify-center gap-2 p-2">
231+
<ConnectButton
232+
chain={chain}
233+
client={THIRDWEB_CLIENT}
234+
connectButton={{
235+
label: "Login to mint!",
236+
}}
237+
wallets={[iaw]}
238+
/>
239+
</div>
240+
{nft ? (
241+
<MediaRenderer
242+
client={THIRDWEB_CLIENT}
243+
src={nft.metadata.image}
244+
style={{ marginTop: "10px", width: "300px" }}
245+
/>
246+
) : null}
247+
{activeEOA ? (
248+
<div className="flex flex-col justify-center gap-4 p-2">
249+
<p className="mb-2 text-center font-semibold">
250+
You own {ownedNfts?.[0]?.quantityOwned.toString() || "0"}{" "}
251+
{nft?.metadata?.name}
252+
</p>
253+
<Button onClick={handleClick}>Mint NFT</Button>
254+
</div>
255+
) : null}
256+
{txIds.length > 0 && (
257+
<div className="w-full max-w-2xl">
258+
<TableContainer>
259+
<Table>
260+
<TableHeader>
261+
<TableRow>
262+
<TableHead>Tx ID</TableHead>
263+
<TableHead>Status</TableHead>
264+
<TableHead>TX Hash</TableHead>
265+
</TableRow>
266+
</TableHeader>
267+
<TableBody>
268+
{txIds.map((txId) => (
269+
<TransactionRow key={txId} transactionId={txId} />
270+
))}
271+
</TableBody>
272+
</Table>
273+
</TableContainer>
274+
</div>
275+
)}
276+
</>
277+
)}
278+
</div>
279+
);
280+
}

0 commit comments

Comments
 (0)