Skip to content

fix(app): resubmit signed tx until confirmed — closes #291#296

Merged
rz1989s merged 1 commit into
mainfrom
fix/issue-291-resubmit-loop
May 23, 2026
Merged

fix(app): resubmit signed tx until confirmed — closes #291#296
rz1989s merged 1 commit into
mainfrom
fix/issue-291-resubmit-loop

Conversation

@rz1989s
Copy link
Copy Markdown
Member

@rz1989s rz1989s commented May 23, 2026

Summary

Closes #291.

Public Solana RPCs (api.devnet.solana.com, api.mainnet-beta.solana.com) drop transactions silently under load. The current broadcast → confirmTransaction flow doesn't resubmit if the first send was lost, so we get spurious "block height exceeded" failures even when the user signed promptly.

Fix

Adds an aggressive resubmit loop that re-broadcasts the signed bytes every 2s (idempotent on Solana RPCs — duplicate sends return the same signature) while confirmation polls in parallel. First confirmation wins; the loop stops in a finally{}.

// New helper — app/src/lib/sendWithRetry.ts
const signature = await connection.sendRawTransaction(signedTx, { skipPreflight: true, maxRetries: 0 })

let stopped = false
const resubmit = async () => {
  while (!stopped) {
    await sleep(2000)
    if (stopped) return
    connection.sendRawTransaction(signedTx, { skipPreflight: true, maxRetries: 0 }).catch(() => {})
  }
}
const resubmitPromise = resubmit()

try {
  await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed')
  return signature
} finally {
  stopped = true
  await resubmitPromise.catch(() => {})
}

Pattern is standard in Solana DeFi (Jupiter, Phantom, Drift all do variants of this).

Why this fixes the failure mode

#291 traced the race in session frontier_sip_17 to the FE's broadcast path. The FE does refresh the blockhash before signing (useTransactionSigner.ts:46-54), so the blockhash itself is fresh. The failure mode is that the broadcast itself doesn't always land:

  • User signs in Phantom (5-15s typical)
  • FE calls sendRawTransaction(signed) — but public devnet RPC silently drops it (rate-limit / congestion)
  • confirmTransaction polls for 60-90s, never sees the tx, returns "block height exceeded"
  • On-chain getTransaction for the signature: null (tx never landed)

Background resubmit defends against the silent-drop case. As long as one resubmit lands before lastValidBlockHeight, the tx confirms.

Files changed

  • new app/src/lib/sendWithRetry.ts — helper + JSDoc citing fix(send): chat send broken on devnet — blockhash expires before broadcast #291
  • new app/src/lib/__tests__/sendWithRetry.test.ts — 5 unit tests (happy path, resubmit-while-pending, swallow-rate-limit errors, first-send error propagates, block-height-exceeded propagates + stops resubmits)
  • modified app/src/hooks/useTransactionSigner.ts — replace inline sendRawTransaction + confirmTransaction with sendAndConfirmWithRetry call

Test plan

  • pnpm exec vitest run src/lib/__tests__/sendWithRetry.test.ts — 5/5 pass
  • pnpm exec vitest run src/components/__tests__/SignTxCard.test.tsx — 15/15 pass (hook external API unchanged)
  • pnpm --filter @sipher/app test -- --run — 577/577 pass (was 572 baseline, +5 new)
  • pnpm typecheck (full workspace: root + sdk + app + agent) — clean

Verification post-deploy

Re-run the frontier_sip_17 chat send → scan → claim cycle (sipher#288's prod verification). Sign card → Phantom sign → cluster confirmation should now land within the blockhash window even when the first broadcast is rate-limited.

Scope notes / non-goals

Public Solana RPCs (api.devnet.solana.com, api.mainnet-beta.solana.com)
drop transactions silently under load. The current broadcast →
confirmTransaction flow doesn't resubmit if the first send was lost,
so we get spurious "block height exceeded" failures even when the user
signed promptly.

This adds an aggressive resubmit loop that re-broadcasts the signed
bytes every 2s (idempotent on Solana RPCs — duplicate sends return the
same signature) while confirmation polls in parallel. First confirmation
wins; the loop stops in a finally{}.

- new app/src/lib/sendWithRetry.ts (helper + 5 unit tests)
- app/src/hooks/useTransactionSigner.ts wired to use it
- existing SignTxCard tests still green (hook external API unchanged)
- app test suite: 572 → 577
- full workspace typecheck clean
@rz1989s rz1989s added bug Something isn't working judge-readiness Hackathon judge preparation priority:critical Must fix before deadline labels May 23, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sipher Ready Ready Preview, Comment May 23, 2026 6:55am

@rz1989s rz1989s merged commit 1552043 into main May 23, 2026
8 checks passed
@rz1989s rz1989s deleted the fix/issue-291-resubmit-loop branch May 23, 2026 06:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working judge-readiness Hackathon judge preparation priority:critical Must fix before deadline

Projects

None yet

1 participant