Skip to content

Commit 3364960

Browse files
committed
Add Engine Components
1 parent d9a63a6 commit 3364960

File tree

22 files changed

+2321
-117
lines changed

22 files changed

+2321
-117
lines changed

apps/playground-web/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
"@radix-ui/react-tabs": "^1.1.1",
2929
"@radix-ui/react-tooltip": "1.1.4",
3030
"@tanstack/react-query": "5.60.2",
31+
"@thirdweb-dev/engine": "^0.0.16",
3132
"class-variance-authority": "^0.7.0",
3233
"clsx": "^2.1.1",
34+
"dotenv": "^16.4.5",
3335
"lucide-react": "0.456.0",
3436
"next": "15.0.3",
3537
"next-themes": "^0.4.3",
@@ -41,6 +43,7 @@
4143
"shiki": "1.22.2",
4244
"tailwind-merge": "^2.5.4",
4345
"thirdweb": "workspace:*",
46+
"timeago.js": "^4.0.2",
4447
"use-debounce": "^10.0.4"
4548
},
4649
"devDependencies": {
21.7 KB
Loading
13.7 KB
Loading
35.5 KB
Loading
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { Engine } from "@thirdweb-dev/engine";
2+
import * as dotenv from "dotenv";
3+
import { NextRequest, NextResponse } from "next/server";
4+
5+
dotenv.config();
6+
7+
const CHAIN_ID = "84532";
8+
const BACKEND_WALLET_ADDRESS = process.env.BACKEND_WALLET as string;
9+
10+
console.log("Environment Variables:");
11+
console.log("CHAIN_ID:", CHAIN_ID);
12+
console.log("BACKEND_WALLET_ADDRESS:", BACKEND_WALLET_ADDRESS);
13+
console.log("ENGINE_URL:", process.env.ENGINE_URL);
14+
console.log("ACCESS_TOKEN:", process.env.ACCESS_TOKEN ? "Set" : "Not Set");
15+
16+
const engine = new Engine({
17+
url: process.env.ENGINE_URL as string,
18+
accessToken: process.env.ACCESS_TOKEN as string,
19+
});
20+
21+
interface MintResult {
22+
queueId: string;
23+
status: "Queued" | "Sent" | "Mined" | "error";
24+
transactionHash?: string;
25+
blockExplorerUrl?: string;
26+
errorMessage?: string;
27+
toAddress: string;
28+
amount: string;
29+
chainId: number;
30+
network: 'Base Sep';
31+
}
32+
33+
export async function POST(req: NextRequest) {
34+
try {
35+
const body = await req.json();
36+
console.log("Request body:", body);
37+
38+
const { contractAddress, data } = body;
39+
if (!Array.isArray(data)) {
40+
return NextResponse.json(
41+
{ error: "Invalid data format" },
42+
{ status: 400 }
43+
);
44+
}
45+
46+
if (data.length === 0) {
47+
return NextResponse.json(
48+
{ error: "Empty data array" },
49+
{ status: 400 }
50+
);
51+
}
52+
53+
console.log(`Attempting to mint batch to ${data.length} receivers`);
54+
console.log("Using CONTRACT_ADDRESS:", contractAddress);
55+
56+
const res = await engine.erc20.mintBatchTo(
57+
CHAIN_ID,
58+
contractAddress,
59+
BACKEND_WALLET_ADDRESS,
60+
{
61+
data: data.map(item => ({
62+
toAddress: item.toAddress,
63+
amount: item.amount,
64+
})),
65+
}
66+
);
67+
68+
console.log("Mint batch initiated, queue ID:", res.result.queueId);
69+
const result = await pollToMine(res.result.queueId, data[0]);
70+
return NextResponse.json([result]);
71+
} catch (error: unknown) {
72+
console.error("Error minting ERC20 tokens", error);
73+
return NextResponse.json([{
74+
queueId: "",
75+
status: "error",
76+
errorMessage: error instanceof Error ? error.message : "An unknown error occurred",
77+
toAddress: "",
78+
amount: "",
79+
chainId: parseInt(CHAIN_ID),
80+
network: "Base Sep"
81+
}], { status: 500 });
82+
}
83+
}
84+
85+
async function pollToMine(queueId: string, firstItem: { toAddress: string, amount: string }): Promise<MintResult> {
86+
let attempts = 0;
87+
const maxAttempts = 10;
88+
89+
while (attempts < maxAttempts) {
90+
try {
91+
const status = await engine.transaction.status(queueId);
92+
93+
if (status.result.status === "mined") {
94+
console.log("Transaction mined! 🥳 ERC20 tokens have been minted", queueId);
95+
const transactionHash = status.result.transactionHash;
96+
const blockExplorerUrl = `https://base-sepolia.blockscout.com/tx/${transactionHash}`;
97+
console.log("View transaction on the blockexplorer:", blockExplorerUrl);
98+
return {
99+
queueId,
100+
status: "Mined",
101+
transactionHash: transactionHash ?? undefined,
102+
blockExplorerUrl: blockExplorerUrl,
103+
toAddress: firstItem.toAddress,
104+
amount: firstItem.amount,
105+
chainId: parseInt(CHAIN_ID),
106+
network: "Base Sep"
107+
};
108+
} else if (status.result.status === "errored") {
109+
console.error("Mint failed", queueId);
110+
console.error(status.result.errorMessage);
111+
return {
112+
queueId,
113+
status: "error",
114+
errorMessage: status.result.errorMessage ?? "Unknown error occurred",
115+
toAddress: firstItem.toAddress,
116+
amount: firstItem.amount,
117+
chainId: parseInt(CHAIN_ID),
118+
network: "Base Sep"
119+
};
120+
}
121+
} catch (error) {
122+
console.error("Error checking transaction status:", error);
123+
}
124+
125+
attempts++;
126+
await new Promise((resolve) => setTimeout(resolve, 5000));
127+
}
128+
129+
return {
130+
queueId,
131+
status: "error",
132+
errorMessage: "Transaction did not mine within the expected time",
133+
toAddress: firstItem.toAddress,
134+
amount: firstItem.amount,
135+
chainId: parseInt(CHAIN_ID),
136+
network: "Base Sep"
137+
};
138+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import { Engine } from "@thirdweb-dev/engine";
2+
import * as dotenv from "dotenv";
3+
import { NextRequest, NextResponse } from "next/server";
4+
5+
dotenv.config();
6+
7+
const BASESEP_CHAIN_ID = "84532";
8+
const BACKEND_WALLET_ADDRESS = process.env.BACKEND_WALLET as string;
9+
10+
console.log("Environment Variables:");
11+
console.log("CHAIN_ID:", BASESEP_CHAIN_ID);
12+
console.log("BACKEND_WALLET_ADDRESS:", BACKEND_WALLET_ADDRESS);
13+
console.log("ENGINE_URL:", process.env.ENGINE_URL);
14+
console.log("ACCESS_TOKEN:", process.env.ACCESS_TOKEN ? "Set" : "Not Set");
15+
16+
const engine = new Engine({
17+
url: process.env.ENGINE_URL as string,
18+
accessToken: process.env.ACCESS_TOKEN as string,
19+
});
20+
21+
type TransactionStatus = "Queued" | "Sent" | "Mined" | "error";
22+
23+
interface ClaimResult {
24+
queueId: string;
25+
status: TransactionStatus;
26+
transactionHash?: string | undefined | null;
27+
blockExplorerUrl?: string | undefined | null;
28+
errorMessage?: string;
29+
toAddress?: string;
30+
amount?: string;
31+
chainId?: string;
32+
timestamp?: number;
33+
}
34+
35+
// Store ongoing polling processes
36+
const pollingProcesses = new Map<string, NodeJS.Timeout>();
37+
38+
// Helper function to make a single claim
39+
async function makeClaimRequest(
40+
chainId: string,
41+
contractAddress: string,
42+
data: {
43+
recipient: string,
44+
quantity: number
45+
}
46+
): Promise<ClaimResult> {
47+
try {
48+
// Validate the recipient address format
49+
if (!data.recipient.match(/^0x[a-fA-F0-9]{40}$/)) {
50+
throw new Error("Invalid wallet address format");
51+
}
52+
53+
const res = await engine.erc721.claimTo(
54+
chainId,
55+
contractAddress,
56+
BACKEND_WALLET_ADDRESS,
57+
{
58+
receiver: data.recipient.toString(),
59+
quantity: data.quantity.toString(),
60+
txOverrides: {
61+
gas: "530000",
62+
maxFeePerGas: "1000000000",
63+
maxPriorityFeePerGas: "1000000000",
64+
},
65+
}
66+
);
67+
68+
const initialResponse: ClaimResult = {
69+
queueId: res.result.queueId,
70+
status: "Queued",
71+
toAddress: data.recipient,
72+
amount: data.quantity.toString(),
73+
chainId,
74+
timestamp: Date.now(),
75+
};
76+
77+
startPolling(res.result.queueId);
78+
return initialResponse;
79+
} catch (error) {
80+
console.error("Claim request error:", error);
81+
throw error;
82+
}
83+
}
84+
85+
export async function POST(req: NextRequest) {
86+
try {
87+
const body = await req.json();
88+
89+
if (!body.receiver || !body.quantity || !body.contractAddress) {
90+
return NextResponse.json(
91+
{ error: "Missing receiver, quantity, or contract address" },
92+
{ status: 400 }
93+
);
94+
}
95+
96+
// Validate contract address format
97+
if (!body.contractAddress.match(/^0x[a-fA-F0-9]{40}$/)) {
98+
return NextResponse.json(
99+
{ error: "Invalid contract address format" },
100+
{ status: 400 }
101+
);
102+
}
103+
104+
const result = await makeClaimRequest(
105+
BASESEP_CHAIN_ID,
106+
body.contractAddress,
107+
{
108+
recipient: body.receiver,
109+
quantity: parseInt(body.quantity)
110+
}
111+
);
112+
113+
return NextResponse.json({ result });
114+
115+
} catch (error) {
116+
console.error("API Error:", error);
117+
return NextResponse.json(
118+
{
119+
error: error instanceof Error ? error.message : "Unknown error occurred",
120+
details: error instanceof Error ? error.stack : undefined
121+
},
122+
{ status: 400 }
123+
);
124+
}
125+
}
126+
127+
function startPolling(queueId: string) {
128+
const maxPollingTime = 5 * 60 * 1000; // 5 minutes timeout
129+
const startTime = Date.now();
130+
131+
const pollingInterval = setInterval(async () => {
132+
try {
133+
// Check if we've exceeded the maximum polling time
134+
if (Date.now() - startTime > maxPollingTime) {
135+
clearInterval(pollingInterval);
136+
pollingProcesses.delete(queueId);
137+
console.log(`Polling timeout for queue ID: ${queueId}`);
138+
return;
139+
}
140+
141+
const result = await pollToMine(queueId);
142+
if (result.status === "Mined" || result.status === "error") {
143+
clearInterval(pollingInterval);
144+
pollingProcesses.delete(queueId);
145+
console.log("Final result:", result);
146+
}
147+
} catch (error) {
148+
console.error("Error in polling process:", error);
149+
clearInterval(pollingInterval);
150+
pollingProcesses.delete(queueId);
151+
}
152+
}, 1500);
153+
154+
pollingProcesses.set(queueId, pollingInterval);
155+
}
156+
157+
async function pollToMine(queueId: string): Promise<ClaimResult> {
158+
console.log(`Polling for queue ID: ${queueId}`);
159+
const status = await engine.transaction.status(queueId);
160+
console.log(`Current status: ${status.result.status}`);
161+
162+
switch (status.result.status) {
163+
case "queued":
164+
console.log("Transaction is queued");
165+
return { queueId, status: "Queued" };
166+
case "sent":
167+
console.log("Transaction is submitted to the network");
168+
return { queueId, status: "Sent" };
169+
case "mined":
170+
console.log("Transaction mined! 🥳 ERC721 token has been claimed", queueId);
171+
const transactionHash = status.result.transactionHash;
172+
const blockExplorerUrl = status.result.chainId === BASESEP_CHAIN_ID ?
173+
`https://base-sepolia.blockscout.com/tx/${transactionHash}` : '';
174+
console.log("View transaction on the blockexplorer:", blockExplorerUrl);
175+
return {
176+
queueId,
177+
status: "Mined",
178+
transactionHash: transactionHash ?? undefined,
179+
blockExplorerUrl: blockExplorerUrl,
180+
};
181+
case "errored":
182+
console.error("Claim failed", queueId);
183+
console.error(status.result.errorMessage);
184+
return {
185+
queueId,
186+
status: "error",
187+
errorMessage: status.result.errorMessage || "Transaction failed",
188+
};
189+
default:
190+
return { queueId, status: "Queued" };
191+
}
192+
}
193+
194+
// Add a new endpoint to check the status
195+
export async function GET(req: NextRequest) {
196+
const { searchParams } = new URL(req.url);
197+
const queueId = searchParams.get('queueId');
198+
199+
if (!queueId) {
200+
return NextResponse.json({ error: "Missing queueId" }, { status: 400 });
201+
}
202+
203+
try {
204+
const result = await pollToMine(queueId);
205+
return NextResponse.json(result);
206+
} catch (error) {
207+
console.error("Error checking transaction status:", error);
208+
return NextResponse.json(
209+
{
210+
status: "error" as TransactionStatus,
211+
error: "Failed to check transaction status"
212+
},
213+
{ status: 500 }
214+
);
215+
}
216+
}

0 commit comments

Comments
 (0)