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 ( / ^ 0 x [ a - f A - F 0 - 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 ( / ^ 0 x [ a - f A - F 0 - 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