Skip to content

Commit f129e87

Browse files
committed
fix: check delegation status before re-delegating, add tx timeout
- stepDelegate checks Magic Router for existing delegation first - If already delegated, skips CPI and returns ER endpoint directly - 30s timeout on confirmTransaction to prevent infinite hangs - runFullDemo uses returned ER endpoint to skip discovery
1 parent 08c1048 commit f129e87

1 file changed

Lines changed: 48 additions & 8 deletions

File tree

client/src/App.tsx

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)