fix(chat): Phantom — verify the signing path, and fail clearly when short on USDC - #26
Merged
Merged
Conversation
…he wallet Phantom simulates whatever it is asked to sign, and an SPL transfer the payer can't cover simulates as a failure. So a Solana user without enough USDC met a red "this transaction will likely fail" panel, approved it anyway, and got back an opaque `insufficient_funds` from the facilitator. useUsdcBalance has exposed hasSufficientBalance since before Solana existed and nothing has ever called it. The Solana payment path now does, and says the actual numbers. Skipped while the balance is still loading — a slow RPC read must not block a payment the user can afford. Memoized hasSufficientBalance while wiring it up. It was rebuilt on every render, and useX402Payment lists it as a dependency, so an unstable identity would have rebuilt the entire paidFetch callback chain in use-franklin-chat on each render. Verified separately that the payment path itself works with Phantom's signer shape. Phantom advertises both solana:signTransaction and solana:signAndSendTransaction, so @solana/kit-plugin-wallet builds one signer carrying modifyAndSignTransactions AND signAndSendTransactions — a dual-capability object my earlier test (a plain local keypair) never exercised. Kit's partiallySignTransactionMessageWithSigners filters to signers that can sign and ignores the sending capability, so the x402 path takes the sign branch and never broadcasts. Confirmed against production with a Phantom-shaped signer: correct fee payer, wallet signature present, facilitator slot left empty, and the facilitator rejecting only on funds.
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.
Focused pass on Phantom specifically: one verification gap closed, one rough edge fixed.
The verification gap (no code change needed, but it could have been bad)
My earlier structural test of the Solana x402 payload used a plain local keypair — a
TransactionPartialSigner. That is not the shape Phantom produces.Phantom advertises both
solana:signTransactionandsolana:signAndSendTransaction, socreateSignerFromWalletAccountin@solana/wallet-account-signerattaches bothmodifyAndSignTransactionsandsignAndSendTransactionsto a single signer object. If Kit had preferred the sending capability, our payment path would have broadcast the transaction itself instead of handing signed bytes to the facilitator — which fails, and fails confusingly.It doesn't:
partiallySignTransactionMessageWithSignersfilters toisTransactionModifyingSigner || isTransactionPartialSignerand ignores the sending capability entirely.Verified against production with a signer built to Phantom's exact dual-capability shape, whose
signAndSendTransactionsthrows if reached:The rough edge
Phantom simulates whatever it is asked to sign, and an SPL transfer the payer can't cover simulates as a failure. So a Solana user short on USDC met a red "this transaction will likely fail" panel, approved it anyway, and got back an opaque
insufficient_funds.useUsdcBalancehas exposedhasSufficientBalancesince before Solana existed and nothing has ever called it. The Solana payment path now does, and quotes the actual numbers:Skipped while the balance is still loading, so a slow RPC read never blocks a payment the user can afford.
hasSufficientBalanceis also now memoized. It was rebuilt every render, anduseX402Paymentlists it as a dependency — an unstable identity would have rebuilt the wholepaidFetchcallback chain inuse-franklin-chaton every render.Verified
typecheck/eslint src/buildclean, plus the production signer test above.Not covered
A real Phantom extension approving a real transfer with real USDC and zero SOL. Everything up to the wallet prompt is now proven; the prompt itself still needs a human.