fix: refresh Solana blockhash before signing and expire unseen pending txs(OK-63381) - #13522
Draft
weatherstar wants to merge 1 commit into
Draft
weatherstar wants to merge 1 commit into
weatherstar wants to merge 1 commit into
Conversation
…g txs (OK-63381) Hardware PIN/passphrase/confirm can outlive the ~60-90 s validity of the blockhash a dApp (or the server) baked into a Solana tx, so the broadcast failed with "Blockhash not found" and any txid that slipped through a lagging custom RPC preflight stayed "confirming" forever. - Add a vault hook that re-stamps short-lived tx fields right before signing for wallet-broadcast txs; the Solana vault refreshes the blockhash for single-signer, non-durable-nonce txs and leaves dApp sign-only txs intact. - Send custom RPC broadcasts with preflightCommitment 'confirmed' like the proxy, and retry the plain JSON-RPC "Blockhash not found" answer the same way as proxy code 40028. - Surface a "Transaction expired" message instead of the raw node text once the retries are exhausted (hardcoded until the i18n key exists). - Mark Solana pending txs that the chain has never seen after three minutes as Dropped so they leave the pending bucket, and show the failure badge on dropped history rows.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
preflightCommitment: 'confirmed', the same bounded retry on "Blockhash not found", and a "Transaction expired" message instead of the raw node text.Intent & Context
Slack report (Windows desktop, hardware wallet, custom RPC
solana-rpc.publicnode.com): a Kamino USDG transfer viasignAndSendTransactionfailed after hardware confirmation withError JSON RPC response: Transaction simulation failed: Blockhash not found. Some attempts got a txid and then sat in local history as "Confirming" forever with no on-chain trace, until the user cleared pending manually. Jira OK-63381.User log timeline: dApp request 22:33:54 →
solSignTransactionstarts 22:34:05 (PIN / passphrase / device confirm) → signature returned 22:34:51 → immediate broadcast rejected. From the dApp's blockhash fetch to broadcast was ≥ 57 s, past Solana's ~150-slot (60–90 s) validity window. A retry with the device already unlocked signed in seconds and landed.Root Cause
_buildUnsignedTxFromEncodedTxonly refreshed it for OKX swap,refreshUnsignedTxBeforeBatchSignonly ran for the 2nd+ tx of a batch. Once hardware interaction exceeded the window the signed tx could not be repaired.ClientCustomRpcSol.broadcastTransactionsent only{ encoding: 'base64' }, so preflight ran at the Solana defaultfinalized, which lags the tip by ~30 slots. A nearly expired blockhash could pass preflight there, return a txid, and then be dropped at the tip. The server proxy usesconfirmed.checkShouldRetryBroadcastTxmatched only the proxy business code 40028; the custom RPC path throws a plainJsonRPCResponseErrorwithout a code, so it got zero retries.Design Decisions
VaultBase.refreshUnsignedTxBeforeSignhook called inServiceSend.signTransactionafter the password prompt and beforewithHardwareProcessing, i.e. as late as possible. It runs only whensignOnlyis false: sign-only dApp requests (signTransaction/signAllTransactions) return the serialized tx to the dApp, which may co-sign or verify it, so their payload stays untouched.AdvanceNonceAccount, ornonceInfoset), whererecentBlockhashis the nonce value. Detection is done on the raw compiled instruction for both legacy and v0 messages, so it needs no lookup-table resolution.getSignatureStatuses(withsearchTransactionHistory) and treats only an explicitnullfor a tx whose localcreatedAtis ≥ 3 minutes old as dropped;undefined, length mismatch, or an RPC error keeps everything pending. Dropped txs are moved to the confirmed bucket withstatus: Dropped, isFinal: truethrough the existingconfirmedTxsflow, so the pending-changed detection andLocalPendingTxConfirmedevent fire as usual (the DeFi consumer already ignores non-Confirmed statuses).TxActionCommonnow shows the critical "Failed" badge forDroppedtoo. EVM replaced-prev txs (the only other Dropped source) are hidden from lists byreplacedNextId, so this only affects the new Solana case.Transaction expired, please try again.) with aTODO(OK-63381); the Lokalise key will replace it later.sendOptions(skipPreflight/maxRetries) through to the custom RPC. It needs the options threaded through ~8 UI hops, and honoringskipPreflight: truewould reintroduce the silent-drop case this PR removes.Changes Detail
packages/kit-bg/src/vaults/base/VaultBase.ts:refreshUnsignedTxBeforeSignandgetDroppedPendingTxsno-op hooks.packages/kit-bg/src/services/ServiceSend.ts: call the refresh hook insignTransactionfor non-sign-only requests.packages/kit-bg/src/vaults/impls/sol/utils.ts:isDurableNonceSolTx,canRefreshSolTxBlockhash,serializeSolTx,replaceSolTxRecentBlockhash.packages/kit-bg/src/vaults/impls/sol/Vault.ts: hook overrides, broadcast error normalization for both paths, retry check by code or message, 3-minute dropped timeout.packages/kit-bg/src/vaults/impls/sol/sdkSol/ClientCustomRpcSol.ts: typed options withpreflightCommitment: 'confirmed'default.packages/kit-bg/src/services/ServiceHistory.ts: after pending detail polling, ask the vaults for dropped txs and move them out of the pending bucket.packages/kit/src/components/TxAction/TxActionCommon.tsx: failure badge forDropped.sol/Vault.test.ts(14 cases) and 2 new cases inServiceSend.broadcastDeadline.test.ts.Risk Assessment
lastValidBlockHeightmay report "expired" while the tx actually lands 1–2 s later; this is the trade-off against a guaranteed failure, and a blind user retry could double-send in that window.ServiceSend.signTransactionnow awaits one extra RPC round trip before hardware signing on Solana.ServiceHistory.fetchAccountHistoryruns one extragetSignatureStatusescall per account only when aged Solana pending txs exist.Test plan
yarn jest packages/kit-bg/src/vaults/impls/sol/Vault.test.ts packages/kit-bg/src/services/ServiceSend.broadcastDeadline.test.tsyarn agent:check --profile commitsignAndSendTransaction, take 60–90 s on the device before confirming; tx lands on both default RPC and a custom RPC, legacy and versioned.signTransaction(sign-only) returns the tx with the dApp's original blockhash unchanged.