Skip to content

fix(solana): send with preflight and rebroadcast so QuickNode sends land - #733

Closed
fengtality wants to merge 4 commits into
developmentfrom
fix/solana-rebroadcast
Closed

fengtality wants to merge 4 commits into
developmentfrom
fix/solana-rebroadcast

Conversation

@fengtality

@fengtality fengtality commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Solana sends through a QuickNode RPC time out and do not land. Gateway sent with skipPreflight: true and maxRetries: 0. QuickNode accepts such a transaction (it answers the signature) but does not forward it to the leader. The same swap through the public RPC lands.

Cause, measured on mainnet

A 0.5 USDC → SOL Jupiter swap through a Gateway on a spare port, using the QuickNode RPC:

Send options Rebroadcast Result
skipPreflight: true, maxRetries: 0 (current development) no 504, did not land (5ipnwb…, 2YgyaV…)
skipPreflight: true every 2 s, 15–16 resends 504, did not land (3 of 3, one with 10× priority fee)
skipPreflight: false no / yes landed in about 1 s (2 of 2)
skipPreflight: true, public RPC yes landed in 2 s

So the cause is skipPreflight, not the missing rebroadcast.

Changes

  1. fix(solana): send with preflight so QuickNode forwards the transaction: every broadcast now uses one SEND_OPTIONS (skipPreflight: false, preflightCommitment: 'confirmed', maxRetries: 0). Preflight refusal means the transaction was not broadcast. The send now throws the parsed program error (slippage, balance, or simulation failed) and no longer returns an unconfirmed result with an empty signature.
  2. fix(solana): rebroadcast a sent transaction until it confirms or expires: while the send waits to confirm (WebSocket or polling), Gateway sends the same signed bytes again every REBROADCAST_INTERVAL_MS (2 s). It stops when the confirmation ends, when the block height passes lastValidBlockHeight, or when the RPC reports the transaction as already processed. Gateway does not re-sign the transaction or change its blockhash. With maxRetries: 0, a dropped send otherwise gets no retry.

Expiry uses the same lastValidBlockHeight that polling already uses: the latest blockhash at send time. Connector-built versioned transactions (Jupiter) keep their own, older blockhash, so the real expiry can be earlier. The only cost is a few resends that cannot land.

POST /chains/solana/submit (#721) sends through the same _sendAndConfirmRawTransaction, so it gets both fixes when it rebases.

Verification

  • With the fix, QuickNode swaps landed: rtQL3f2iaYMKxy23x3bFYJbsmwuEeD1en6ysQCUZ1boSMFJMpxNfyTUkix8F8TRzeVtRudGPQDcSXP42o8bAt5C (PR tip) and 5FVTZRKckqXQs3xxPK4pjK3nLw84Cz1DCExZhYmgWv4JRj6CMz1AQkgJJgFZVnDqgEStSGYMdbkvzev2hdR7z2MU. Both are confirmed on the public RPC.
  • Unit tests use a stub RPC that drops sends. send-rebroadcast.test.ts shows identical bytes resent until delivery, a single blockhash fetch, a stop at blockhash expiry with no sends after it, and a confirmation that does not wait for a resend still in flight. send-preflight.test.ts shows preflight is requested and a preflight refusal surfaces as SLIPPAGE_EXCEEDED.
  • pnpm build, pnpm lint (0 errors), pnpm test -w 3: 171 suites, 1511 tests passed.

🤖 Generated with Claude Code

Through the QuickNode Solana RPC, a transaction sent with skipPreflight is
accepted (the RPC answers its signature) but never reaches a leader, so every
send timed out at blockhash expiry. Rebroadcasting the same bytes every 2 s
(15 resends) did not help; the same send with preflight on landed in about
a second, and the same code through the public RPC lands either way.

Every broadcast now uses one SEND_OPTIONS with preflight on at 'confirmed'.
A transaction that preflight refuses was not broadcast, so the send now
throws its parsed program error (slippage, balance, or simulation failed)
instead of reporting an unconfirmed send with no signature.
Gateway sends with maxRetries 0, so the RPC hands the transaction to the
leader once and a dropped send is never retried. While the send waits to
confirm (WebSocket or polling), the same signed bytes are now sent again
every REBROADCAST_INTERVAL_MS (2 s), with the same send options. The loop
stops when the confirmation ends, when the block height passes the
lastValidBlockHeight the confirmation uses, or when the RPC says the
transaction was already processed. Nothing is re-signed and the blockhash
is not changed, so the transaction can land only once.

POST /chains/solana/submit (#721) sends through the same
_sendAndConfirmRawTransaction, so it gets the rebroadcast too.
@greptile-apps

greptile-apps Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues were found.

Summary

The PR enables preflight checks for Solana sends and rebroadcasts the same signed bytes while awaiting confirmation.

  • Solana sends run preflight before the RPC broadcasts them.
  • Solana transactions are resent while Gateway waits for confirmation.

Reviews (3) · Last reviewed commit: "fix(solana): check the stop flag before ..." · Reviewed by Greptile

Comment thread src/chains/solana/solana.ts Resolved
…ight

Stopping the rebroadcast awaited the loop, so a slow getBlockHeight or resend
held back a send that had already confirmed. Stopping now only sets the flag
and wakes the timer; the loop catches its own errors and schedules nothing
after it is stopped.
A resend that returns after the stop scheduled one more timer before the
loop saw the flag. The loop condition now checks it first.
@fengtality

Copy link
Copy Markdown
Contributor Author

Bundled into #735 with the other fix PRs.

@fengtality fengtality closed this Oct 20, 2026
@fengtality
fengtality deleted the fix/solana-rebroadcast branch October 20, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant