@@ -223,7 +223,6 @@ function App() {
223223
224224 useEffect ( ( ) => { refreshBalance ( ) } , [ refreshBalance ] )
225225
226- // sendConn: optionally send the signed tx to a different connection (e.g. ER)
227226 // Signing always uses L1 blockhash so Phantom recognizes it as devnet
228227 async function sendTx ( sendConn : Connection , tx : Transaction ) : Promise < string > {
229228 if ( ! publicKey || ! signTransaction ) throw new Error ( 'Wallet not connected' )
@@ -233,9 +232,16 @@ function App() {
233232 const signed = await signTransaction ( tx )
234233 const raw = signed . serialize ( )
235234 const sig = await sendConn . sendRawTransaction ( raw , { skipPreflight : true } )
236- // For ER txs, don't await L1 confirmation — ER has its own consensus
235+
236+ // Confirm with timeout
237237 if ( sendConn === connection ) {
238- await connection . confirmTransaction ( sig , 'confirmed' )
238+ const timeout = new Promise < never > ( ( _ , reject ) =>
239+ setTimeout ( ( ) => reject ( new Error ( 'Confirmation timeout (30s)' ) ) , 30000 )
240+ )
241+ await Promise . race ( [
242+ connection . confirmTransaction ( sig , 'confirmed' ) ,
243+ timeout ,
244+ ] )
239245 } else {
240246 // Poll ER for confirmation
241247 for ( let i = 0 ; i < 30 ; i ++ ) {
@@ -247,6 +253,24 @@ function App() {
247253 return sig
248254 }
249255
256+ // Check if account is already delegated to ER
257+ async function checkDelegation ( ) : Promise < string | null > {
258+ if ( ! jobPDA ) return null
259+ try {
260+ const resp = await fetch ( MAGIC_ROUTER , {
261+ method : 'POST' ,
262+ headers : { 'Content-Type' : 'application/json' } ,
263+ body : JSON . stringify ( { jsonrpc : '2.0' , id : 1 , method : 'getDelegationStatus' , params : [ jobPDA . toBase58 ( ) ] } ) ,
264+ } )
265+ const result : any = await resp . json ( )
266+ if ( result . result ?. isDelegated && result . result ?. fqdn ) {
267+ const ep = result . result . fqdn . startsWith ( 'http' ) ? result . result . fqdn : `https://${ result . result . fqdn } `
268+ return ep
269+ }
270+ } catch { /* not delegated */ }
271+ return null
272+ }
273+
250274 // ---------- Lifecycle Steps ----------
251275 async function stepAirdrop ( ) {
252276 if ( ! publicKey ) return
@@ -304,18 +328,26 @@ function App() {
304328 }
305329 }
306330
307- async function stepDelegate ( ) : Promise < boolean > {
331+ async function stepDelegate ( ) : Promise < string | false > {
308332 if ( ! program || ! publicKey || ! jobPDA ) return false
309333 setActiveStep ( 'delegate' )
310334 addEvent ( 'Delegating to Ephemeral Rollup...' , 'l1' )
311335 const start = Date . now ( )
312336 try {
337+ // Check if already delegated
338+ const existingER = await checkDelegation ( )
339+ if ( existingER ) {
340+ addEvent ( `Already delegated → ${ new URL ( existingER ) . hostname } ` , 'info' , { ms : Date . now ( ) - start } )
341+ setCompletedSteps ( prev => new Set ( [ ...prev , 'delegate' ] ) )
342+ return existingER
343+ }
344+
313345 const job = await program . account . job . fetch ( jobPDA )
314346 const status = job . status as number
315347 if ( status !== 0 ) {
316348 addEvent ( `Job status=${ status } , skipping delegation` , 'info' , { ms : Date . now ( ) - start } )
317349 setCompletedSteps ( prev => new Set ( [ ...prev , 'delegate' ] ) )
318- return true
350+ return 'skip'
319351 }
320352
321353 const tx = await program . methods
@@ -326,7 +358,7 @@ function App() {
326358 const sig = await sendTx ( connection , tx )
327359 addEvent ( `Delegated → MagicBlock ER` , 'success' , { txHash : sig , ms : Date . now ( ) - start } )
328360 setCompletedSteps ( prev => new Set ( [ ...prev , 'delegate' ] ) )
329- return true
361+ return 'delegated'
330362 } catch ( e ) {
331363 addEvent ( `Delegation failed: ${ ( e as Error ) . message . slice ( 0 , 80 ) } ` , 'error' )
332364 return false
@@ -491,15 +523,23 @@ function App() {
491523 if ( ! await stepInit ( ) ) { setRunning ( false ) ; return }
492524 await new Promise ( r => setTimeout ( r , 800 ) )
493525
494- if ( ! await stepDelegate ( ) ) { setRunning ( false ) ; return }
526+ const delegateResult = await stepDelegate ( )
527+ if ( ! delegateResult ) { setRunning ( false ) ; return }
495528 await new Promise ( r => setTimeout ( r , 1000 ) )
496529
497530 const job = await program . account . job . fetch ( jobPDA )
498531 const status = job . status as number
499532 let erEndpoint : string | null = null
500533
501534 if ( status < 2 ) {
502- erEndpoint = await discoverER ( )
535+ // If stepDelegate returned an ER URL directly (already delegated), use it
536+ if ( delegateResult . startsWith ( 'http' ) ) {
537+ erEndpoint = delegateResult
538+ addEvent ( `Using existing ER: ${ new URL ( erEndpoint ) . hostname } ` , 'er' )
539+ } else {
540+ // Fresh delegation — discover ER
541+ erEndpoint = await discoverER ( )
542+ }
503543 if ( ! erEndpoint ) { addEvent ( 'Cannot proceed without ER endpoint' , 'error' ) ; setRunning ( false ) ; return }
504544 if ( ! await stepBid ( erEndpoint ) ) { setRunning ( false ) ; return }
505545 await new Promise ( r => setTimeout ( r , 800 ) )
0 commit comments