Skip to content

Commit 31e5eb4

Browse files
Add Engine user transactions example to playground
1 parent 8e29d48 commit 31e5eb4

File tree

6 files changed

+474
-121
lines changed

6 files changed

+474
-121
lines changed

apps/playground-web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@radix-ui/react-switch": "^1.2.5",
1414
"@radix-ui/react-tooltip": "1.2.7",
1515
"@tanstack/react-query": "5.80.7",
16+
"@thirdweb-dev/engine": "workspace:*",
1617
"class-variance-authority": "^0.7.1",
1718
"clsx": "^2.1.1",
1819
"date-fns": "4.1.0",
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { Metadata } from "next";
2+
import { GatewayPreview } from "@/components/account-abstraction/gateway";
3+
import { PageLayout } from "@/components/blocks/APIHeader";
4+
import { CodeExample } from "@/components/code/code-example";
5+
import ThirdwebProvider from "@/components/thirdweb-provider";
6+
import { metadataBase } from "@/lib/constants";
7+
8+
export const metadata: Metadata = {
9+
description: "Transactions from user wallets with monitoring and retries",
10+
metadataBase,
11+
title: "User Transactions | thirdweb",
12+
};
13+
14+
export default function Page() {
15+
return (
16+
<ThirdwebProvider>
17+
<PageLayout
18+
description={
19+
<>Transactions from user wallets with monitoring and retries.</>
20+
}
21+
docsLink="https://portal.thirdweb.com/engine?utm_source=playground"
22+
title="User Transactions"
23+
>
24+
<UserTransactions />
25+
</PageLayout>
26+
</ThirdwebProvider>
27+
);
28+
}
29+
30+
function UserTransactions() {
31+
return (
32+
<>
33+
<CodeExample
34+
code={`\
35+
import { inAppWallet } from "thirdweb/wallets/in-app";
36+
import { ConnectButton, useActiveAccount } from "thirdweb/react";
37+
38+
const wallet = inAppWallet();
39+
40+
function App() {
41+
const activeWallet = useActiveWallet();
42+
43+
const handleClick = async () => {
44+
const walletAddress = activeWallet?.getAccount()?.address;
45+
// transactions are a simple POST request to the engine API
46+
// or use the @thirdweb-dev/engine type-safe JS SDK
47+
const response = await fetch(
48+
"https://engine.thirdweb.com/v1/write/contract",
49+
{
50+
method: "POST",
51+
headers: {
52+
"Content-Type": "application/json",
53+
"x-client-id": "<your-project-client-id>",
54+
// uses the in-app wallet's auth token to authenticate the request
55+
"x-wallet-access-token": activeWallet?.getAuthToken?.(),
56+
},
57+
body: JSON.stringify({
58+
executionOptions: {
59+
from: walletAddress,
60+
chainId: "84532",
61+
type: "auto", // defaults to sponsored transactions
62+
},
63+
params: [
64+
{
65+
contractAddress: "0x...",
66+
method: "function claim(address to, uint256 amount)",
67+
params: [walletAddress, "1"],
68+
},
69+
],
70+
}),
71+
});
72+
};
73+
74+
return (
75+
<>
76+
<ConnectButton
77+
client={client}
78+
wallet={[wallet]}
79+
connectButton={{
80+
label: "Login to mint!",
81+
}}
82+
/>
83+
<Button
84+
onClick={handleClick}
85+
>
86+
Mint
87+
</Button>
88+
</>
89+
);
90+
}`}
91+
header={{
92+
description:
93+
"Engine can queue, monitor, and retry transactions from your users in-app wallets. All transactions and analytics will be displayed in your developer dashboard.",
94+
title: "Transactions from User Wallets",
95+
}}
96+
lang="tsx"
97+
preview={<GatewayPreview />}
98+
/>
99+
</>
100+
);
101+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ const engineSidebarLinks: SidebarLink = {
116116
expanded: false,
117117
isCollapsible: false,
118118
links: [
119+
{
120+
href: "/engine/users",
121+
name: "From User Wallets",
122+
},
119123
{
120124
href: "/engine/airdrop",
121125
name: "Airdrop",
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
"use client";
2+
3+
import { useQuery } from "@tanstack/react-query";
4+
import { configure, sendTransaction } from "@thirdweb-dev/engine";
5+
import { useState } from "react";
6+
import { Engine, encode, getContract } from "thirdweb";
7+
import { baseSepolia } from "thirdweb/chains";
8+
import { claimTo, getNFT, getOwnedNFTs } from "thirdweb/extensions/erc1155";
9+
import {
10+
ConnectButton,
11+
MediaRenderer,
12+
useActiveAccount,
13+
useActiveWallet,
14+
useDisconnect,
15+
useReadContract,
16+
} from "thirdweb/react";
17+
import { stringify } from "thirdweb/utils";
18+
import { inAppWallet } from "thirdweb/wallets/in-app";
19+
import { THIRDWEB_CLIENT } from "../../lib/client";
20+
import { Badge } from "../ui/badge";
21+
import { Button } from "../ui/button";
22+
import {
23+
Table,
24+
TableBody,
25+
TableCell,
26+
TableContainer,
27+
TableHead,
28+
TableHeader,
29+
TableRow,
30+
} from "../ui/table";
31+
32+
const chain = baseSepolia;
33+
const editionDropAddress = "0x638263e3eAa3917a53630e61B1fBa685308024fa";
34+
const editionDropTokenId = 2n;
35+
36+
const editionDropContract = getContract({
37+
address: editionDropAddress,
38+
chain,
39+
client: THIRDWEB_CLIENT,
40+
});
41+
42+
const iaw = inAppWallet();
43+
configure({
44+
clientId: THIRDWEB_CLIENT.clientId,
45+
});
46+
47+
function TransactionRow({ transactionId }: { transactionId: string }) {
48+
const { data: txStatus, isLoading } = useQuery({
49+
enabled: !!transactionId,
50+
queryFn: async () => {
51+
return await Engine.getTransactionStatus({
52+
client: THIRDWEB_CLIENT,
53+
transactionId,
54+
});
55+
},
56+
queryKey: ["txStatus", transactionId],
57+
refetchInterval: 2000,
58+
});
59+
60+
const getStatusBadge = (status: string) => {
61+
switch (status) {
62+
case undefined:
63+
case "QUEUED":
64+
return <Badge variant="warning">Queued</Badge>;
65+
case "SUBMITTED":
66+
return <Badge variant="default">Submitted</Badge>;
67+
case "CONFIRMED":
68+
return <Badge variant="success">Confirmed</Badge>;
69+
case "FAILED":
70+
return <Badge variant="destructive">Failed</Badge>;
71+
default:
72+
return <Badge variant="outline">{"Unknown"}</Badge>;
73+
}
74+
};
75+
76+
const renderTransactionHash = () => {
77+
if (!txStatus) return "-";
78+
79+
let txHash: string | undefined;
80+
if (txStatus.status === "CONFIRMED") {
81+
txHash = txStatus.transactionHash;
82+
} else if (txStatus.status === "SUBMITTED") {
83+
txHash = txStatus.userOpHash;
84+
}
85+
86+
if (txHash && chain.blockExplorers?.[0]?.url) {
87+
return (
88+
<a
89+
className="text-blue-500 hover:text-blue-700 underline font-mono text-sm"
90+
href={`${chain.blockExplorers[0].url}/tx/${txHash}`}
91+
rel="noopener noreferrer"
92+
target="_blank"
93+
>
94+
{txHash.slice(0, 6)}...{txHash.slice(-4)}
95+
</a>
96+
);
97+
}
98+
99+
return txHash ? (
100+
<span className="font-mono text-sm">
101+
{txHash.slice(0, 6)}...{txHash.slice(-4)}
102+
</span>
103+
) : (
104+
"-"
105+
);
106+
};
107+
108+
return (
109+
<TableRow>
110+
<TableCell className="font-mono text-sm">
111+
{transactionId.slice(0, 8)}...{transactionId.slice(-4)}
112+
</TableCell>
113+
<TableCell>
114+
{isLoading || !txStatus ? (
115+
<Badge variant="warning">Queued</Badge>
116+
) : (
117+
getStatusBadge(txStatus.status)
118+
)}
119+
</TableCell>
120+
<TableCell>{renderTransactionHash()}</TableCell>
121+
</TableRow>
122+
);
123+
}
124+
125+
export function GatewayPreview() {
126+
const [txIds, setTxIds] = useState<string[]>([]);
127+
const activeEOA = useActiveAccount();
128+
const activeWallet = useActiveWallet();
129+
const { disconnect } = useDisconnect();
130+
const { data: nft, isLoading: isNftLoading } = useReadContract(getNFT, {
131+
contract: editionDropContract,
132+
tokenId: editionDropTokenId,
133+
});
134+
const { data: ownedNfts } = useReadContract(getOwnedNFTs, {
135+
// biome-ignore lint/style/noNonNullAssertion: handled by queryOptions
136+
address: activeEOA?.address!,
137+
contract: editionDropContract,
138+
queryOptions: { enabled: !!activeEOA, refetchInterval: 2000 },
139+
useIndexer: false,
140+
});
141+
142+
const { data: preparedTx } = useQuery({
143+
enabled: !!activeEOA,
144+
queryFn: async () => {
145+
if (!activeEOA) {
146+
throw new Error("No active EOA");
147+
}
148+
const tx = claimTo({
149+
contract: editionDropContract,
150+
quantity: 1n,
151+
to: activeEOA.address,
152+
tokenId: editionDropTokenId,
153+
});
154+
return {
155+
data: await encode(tx),
156+
to: editionDropContract.address,
157+
};
158+
},
159+
queryKey: ["tx", activeEOA?.address],
160+
});
161+
162+
if (activeEOA && activeWallet && activeWallet?.id !== iaw.id) {
163+
return (
164+
<div className="flex flex-col items-center justify-center gap-4">
165+
Please connect with an in-app wallet for this example
166+
<Button
167+
onClick={() => {
168+
disconnect(activeWallet);
169+
}}
170+
>
171+
Disconnect current wallet
172+
</Button>
173+
</div>
174+
);
175+
}
176+
177+
const handleClick = async () => {
178+
if (!preparedTx || !activeEOA) {
179+
return;
180+
}
181+
const body = {
182+
executionOptions: {
183+
chainId: baseSepolia.id,
184+
from: activeEOA.address,
185+
type: "auto" as const,
186+
},
187+
params: [preparedTx],
188+
};
189+
const result = await sendTransaction({
190+
body,
191+
headers: {
192+
"x-wallet-access-token": iaw.getAuthToken?.(),
193+
},
194+
});
195+
if (result.error) {
196+
throw new Error(`Error sending transaction: ${stringify(result.error)}`);
197+
}
198+
199+
const txId = result.data?.result.transactions[0]?.id;
200+
if (!txId) {
201+
throw new Error("No transaction ID");
202+
}
203+
204+
setTxIds((prev) => [...prev, txId]);
205+
};
206+
207+
return (
208+
<div className="flex flex-col items-center justify-center gap-4">
209+
{isNftLoading ? (
210+
<div className="mt-24 w-full">Loading...</div>
211+
) : (
212+
<>
213+
<div className="flex flex-col justify-center gap-2 p-2">
214+
<ConnectButton
215+
chain={chain}
216+
client={THIRDWEB_CLIENT}
217+
connectButton={{
218+
label: "Login to mint!",
219+
}}
220+
wallets={[iaw]}
221+
/>
222+
</div>
223+
{nft ? (
224+
<MediaRenderer
225+
client={THIRDWEB_CLIENT}
226+
src={nft.metadata.image}
227+
style={{ marginTop: "10px", width: "300px" }}
228+
/>
229+
) : null}
230+
{activeEOA ? (
231+
<div className="flex flex-col justify-center gap-4 p-2">
232+
<p className="mb-2 text-center font-semibold">
233+
You own {ownedNfts?.[0]?.quantityOwned.toString() || "0"}{" "}
234+
{nft?.metadata?.name}
235+
</p>
236+
<Button onClick={handleClick}>Mint NFT</Button>
237+
</div>
238+
) : null}
239+
{txIds.length > 0 && (
240+
<div className="w-full max-w-2xl">
241+
<TableContainer>
242+
<Table>
243+
<TableHeader>
244+
<TableRow>
245+
<TableHead>Tx ID</TableHead>
246+
<TableHead>Status</TableHead>
247+
<TableHead>TX Hash</TableHead>
248+
</TableRow>
249+
</TableHeader>
250+
<TableBody>
251+
{txIds.map((txId) => (
252+
<TransactionRow key={txId} transactionId={txId} />
253+
))}
254+
</TableBody>
255+
</Table>
256+
</TableContainer>
257+
</div>
258+
)}
259+
</>
260+
)}
261+
</div>
262+
);
263+
}

0 commit comments

Comments
 (0)