Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/playground-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-tooltip": "1.1.4",
"@tanstack/react-query": "5.61.4",
"@thirdweb-dev/engine": "^0.0.16",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "0.461.0",
Expand All @@ -41,6 +42,7 @@
"shiki": "1.22.2",
"tailwind-merge": "^2.5.5",
"thirdweb": "workspace:*",
"timeago.js": "^4.0.2",
"use-debounce": "^10.0.4"
},
"devDependencies": {
Expand Down
Binary file added apps/playground-web/public/BaseSep.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/playground-web/public/Ethereum.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/playground-web/public/airdrop.avif
Binary file not shown.
Binary file added apps/playground-web/public/minting.avif
Binary file not shown.
Binary file added apps/playground-web/public/webhooks.avif
Binary file not shown.
155 changes: 155 additions & 0 deletions apps/playground-web/src/app/api/airdrop/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { Engine } from "@thirdweb-dev/engine";
import * as dotenv from "dotenv";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

dotenv.config();

const CHAIN_ID = "84532";
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;

console.log("Environment Variables:");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove logs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed this file entirely, it is unused, removed logs from the rest as well

console.log("CHAIN_ID:", CHAIN_ID);
console.log("BACKEND_WALLET_ADDRESS:", BACKEND_WALLET_ADDRESS);
console.log("ENGINE_URL:", process.env.ENGINE_URL);
console.log(
"ACCESS_TOKEN:",
process.env.ENGINE_ACCESS_TOKEN ? "Set" : "Not Set",
);

const engine = new Engine({
url: process.env.ENGINE_URL as string,
accessToken: process.env.ENGINE_ACCESS_TOKEN as string,
});

interface MintResult {
queueId: string;
status: "Queued" | "Sent" | "Mined" | "error";
transactionHash?: string;
blockExplorerUrl?: string;
errorMessage?: string;
toAddress: string;
amount: string;
chainId: number;
network: "Base Sep";
}

export async function POST(req: NextRequest) {
try {
const body = await req.json();
console.log("Request body:", body);

const { contractAddress, data } = body;
if (!Array.isArray(data)) {
return NextResponse.json(
{ error: "Invalid data format" },
{ status: 400 },
);
}

if (data.length === 0) {
return NextResponse.json({ error: "Empty data array" }, { status: 400 });
}

console.log(`Attempting to mint batch to ${data.length} receivers`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More logs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

console.log("Using CONTRACT_ADDRESS:", contractAddress);

const res = await engine.erc20.mintBatchTo(
CHAIN_ID,
contractAddress,
BACKEND_WALLET_ADDRESS,
{
data: data.map((item) => ({
toAddress: item.toAddress,
amount: item.amount,
})),
},
);

console.log("Mint batch initiated, queue ID:", res.result.queueId);
const result = await pollToMine(res.result.queueId, data[0]);
return NextResponse.json([result]);
} catch (error: unknown) {
console.error("Error minting ERC20 tokens", error);
return NextResponse.json(
[
{
queueId: "",
status: "error",
errorMessage:
error instanceof Error
? error.message
: "An unknown error occurred",
toAddress: "",
amount: "",
chainId: Number.parseInt(CHAIN_ID),
network: "Base Sep",
},
],
{ status: 500 },
);
}
}

async function pollToMine(
queueId: string,
firstItem: { toAddress: string; amount: string },
): Promise<MintResult> {
let attempts = 0;
const maxAttempts = 10;

while (attempts < maxAttempts) {
try {
const status = await engine.transaction.status(queueId);

if (status.result.status === "mined") {
console.log(
"Transaction mined! 🥳 ERC20 tokens have been minted",
queueId,
);
const transactionHash = status.result.transactionHash;
const blockExplorerUrl = `https://base-sepolia.blockscout.com/tx/${transactionHash}`;
console.log("View transaction on the blockexplorer:", blockExplorerUrl);
return {
queueId,
status: "Mined",
transactionHash: transactionHash ?? undefined,
blockExplorerUrl: blockExplorerUrl,
toAddress: firstItem.toAddress,
amount: firstItem.amount,
chainId: Number.parseInt(CHAIN_ID),
network: "Base Sep",
};
}

if (status.result.status === "errored") {
console.error("Mint failed", queueId);
console.error(status.result.errorMessage);
return {
queueId,
status: "error",
errorMessage: status.result.errorMessage ?? "Unknown error occurred",
toAddress: firstItem.toAddress,
amount: firstItem.amount,
chainId: Number.parseInt(CHAIN_ID),
network: "Base Sep",
};
}
} catch (error) {
console.error("Error checking transaction status:", error);
}

attempts++;
await new Promise((resolve) => setTimeout(resolve, 5000));
}

return {
queueId,
status: "error",
errorMessage: "Transaction did not mine within the expected time",
toAddress: firstItem.toAddress,
amount: firstItem.amount,
chainId: Number.parseInt(CHAIN_ID),
network: "Base Sep",
};
}
226 changes: 226 additions & 0 deletions apps/playground-web/src/app/api/claimTo/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { Engine } from "@thirdweb-dev/engine";
import * as dotenv from "dotenv";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

dotenv.config();

const BASESEP_CHAIN_ID = "84532";
const BACKEND_WALLET_ADDRESS = process.env.ENGINE_BACKEND_WALLET as string;

console.log("Environment Variables:");
console.log("CHAIN_ID:", BASESEP_CHAIN_ID);
console.log("BACKEND_WALLET_ADDRESS:", BACKEND_WALLET_ADDRESS);
console.log("ENGINE_URL:", process.env.ENGINE_URL);
console.log(
"ACCESS_TOKEN:",
process.env.ENGINE_ACCESS_TOKEN ? "Set" : "Not Set",
);

const engine = new Engine({
url: process.env.ENGINE_URL as string,
accessToken: process.env.ENGINE_ACCESS_TOKEN as string,
});

type TransactionStatus = "Queued" | "Sent" | "Mined" | "error";

interface ClaimResult {
queueId: string;
status: TransactionStatus;
transactionHash?: string | undefined | null;
blockExplorerUrl?: string | undefined | null;
errorMessage?: string;
toAddress?: string;
amount?: string;
chainId?: string;
timestamp?: number;
}

// Store ongoing polling processes
const pollingProcesses = new Map<string, NodeJS.Timeout>();

// Helper function to make a single claim
async function makeClaimRequest(
chainId: string,
contractAddress: string,
data: {
recipient: string;
quantity: number;
},
): Promise<ClaimResult> {
try {
// Validate the recipient address format
if (!data.recipient.match(/^0x[a-fA-F0-9]{40}$/)) {
throw new Error("Invalid wallet address format");
}

const res = await engine.erc721.claimTo(
chainId,
contractAddress,
BACKEND_WALLET_ADDRESS,
{
receiver: data.recipient.toString(),
quantity: data.quantity.toString(),
txOverrides: {
gas: "530000",
maxFeePerGas: "1000000000",
maxPriorityFeePerGas: "1000000000",
},
},
);

const initialResponse: ClaimResult = {
queueId: res.result.queueId,
status: "Queued",
toAddress: data.recipient,
amount: data.quantity.toString(),
chainId,
timestamp: Date.now(),
};

startPolling(res.result.queueId);
return initialResponse;
} catch (error) {
console.error("Claim request error:", error);
throw error;
}
}

export async function POST(req: NextRequest) {
try {
const body = await req.json();

if (!body.receiver || !body.quantity || !body.contractAddress) {
return NextResponse.json(
{ error: "Missing receiver, quantity, or contract address" },
{ status: 400 },
);
}

// Validate contract address format
if (!body.contractAddress.match(/^0x[a-fA-F0-9]{40}$/)) {
return NextResponse.json(
{ error: "Invalid contract address format" },
{ status: 400 },
);
}

const result = await makeClaimRequest(
BASESEP_CHAIN_ID,
body.contractAddress,
{
recipient: body.receiver,
quantity: Number.parseInt(body.quantity),
},
);

return NextResponse.json({ result });
} catch (error) {
console.error("API Error:", error);
return NextResponse.json(
{
error:
error instanceof Error ? error.message : "Unknown error occurred",
details: error instanceof Error ? error.stack : undefined,
},
{ status: 400 },
);
}
}

function startPolling(queueId: string) {
const maxPollingTime = 5 * 60 * 1000; // 5 minutes timeout
const startTime = Date.now();

const pollingInterval = setInterval(async () => {
try {
// Check if we've exceeded the maximum polling time
if (Date.now() - startTime > maxPollingTime) {
clearInterval(pollingInterval);
pollingProcesses.delete(queueId);
console.log(`Polling timeout for queue ID: ${queueId}`);
return;
}

const result = await pollToMine(queueId);
if (result.status === "Mined" || result.status === "error") {
clearInterval(pollingInterval);
pollingProcesses.delete(queueId);
console.log("Final result:", result);
}
} catch (error) {
console.error("Error in polling process:", error);
clearInterval(pollingInterval);
pollingProcesses.delete(queueId);
}
}, 1500);

pollingProcesses.set(queueId, pollingInterval);
}

async function pollToMine(queueId: string): Promise<ClaimResult> {
console.log(`Polling for queue ID: ${queueId}`);
const status = await engine.transaction.status(queueId);
console.log(`Current status: ${status.result.status}`);

switch (status.result.status) {
case "queued":
console.log("Transaction is queued");
return { queueId, status: "Queued" };
case "sent":
console.log("Transaction is submitted to the network");
return { queueId, status: "Sent" };
case "mined": {
console.log(
"Transaction mined! 🥳 ERC721 token has been claimed",
queueId,
);
const transactionHash = status.result.transactionHash;
const blockExplorerUrl =
status.result.chainId === BASESEP_CHAIN_ID
? `https://base-sepolia.blockscout.com/tx/${transactionHash}`
: "";
console.log("View transaction on the blockexplorer:", blockExplorerUrl);
return {
queueId,
status: "Mined",
transactionHash: transactionHash ?? undefined,
blockExplorerUrl: blockExplorerUrl,
};
}
case "errored":
console.error("Claim failed", queueId);
console.error(status.result.errorMessage);
return {
queueId,
status: "error",
errorMessage: status.result.errorMessage || "Transaction failed",
};
default:
return { queueId, status: "Queued" };
}
}

// Add a new endpoint to check the status
export async function GET(req: NextRequest) {
const { searchParams } = new URL(req.url);
const queueId = searchParams.get("queueId");

if (!queueId) {
return NextResponse.json({ error: "Missing queueId" }, { status: 400 });
}

try {
const result = await pollToMine(queueId);
return NextResponse.json(result);
} catch (error) {
console.error("Error checking transaction status:", error);
return NextResponse.json(
{
status: "error" as TransactionStatus,
error: "Failed to check transaction status",
},
{ status: 500 },
);
}
}
Loading
Loading