Skip to content

fix(chat): Phantom — verify the signing path, and fail clearly when short on USDC - #26

Merged
VickyXAI merged 1 commit into
masterfrom
fix/phantom-payment-path
Aug 26, 2026
Merged

fix(chat): Phantom — verify the signing path, and fail clearly when short on USDC#26
VickyXAI merged 1 commit into
masterfrom
fix/phantom-payment-path

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

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:signTransaction and solana:signAndSendTransaction, so createSignerFromWalletAccount in @solana/wallet-account-signer attaches both modifyAndSignTransactions and signAndSendTransactions to 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: partiallySignTransactionMessageWithSigners filters to isTransactionModifyingSigner || isTransactionPartialSigner and ignores the sending capability entirely.

Verified against production with a signer built to Phantom's exact dual-capability shape, whose signAndSendTransactions throws if reached:

PASS  payment built with a Phantom-shaped dual-capability signer
PASS  took the sign path, not sign-and-send
PASS  fee payer is the facilitator, not the wallet
PASS  wallet is NOT the fee payer
PASS  wallet's signature is present
PASS  facilitator slot left empty for its co-signature
PASS  three instructions, transfer last
PASS  blockhash taken from the 402
PASS  amount echoed unchanged
PASS  facilitator accepted the shape (rejected only on funds): insufficient_funds payer=matches

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.

useUsdcBalance has exposed hasSufficientBalance since before Solana existed and nothing has ever called it. The Solana payment path now does, and quotes the actual numbers:

Not enough USDC on Solana — this costs $0.0010, your wallet has $0.00.

Skipped while the balance is still loading, so a slow RPC read never blocks a payment the user can afford.

hasSufficientBalance is also now memoized. It was rebuilt every render, and useX402Payment lists it as a dependency — an unstable identity would have rebuilt the whole paidFetch callback chain in use-franklin-chat on every render.

Verified

typecheck / eslint src / build clean, 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.

…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.
@VickyXAI
VickyXAI merged commit a51482c into master Aug 26, 2026
1 check passed
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