Skip to content

fix(client): fetchAccount error discrimination, buildTransaction blockhash expiry, sendTransaction confirmation#4

Open
lpsmurf wants to merge 1 commit into
OOBE-PROTOCOL:mainfrom
lpsmurf:fix/client-fetchaccount-error-handling
Open

fix(client): fetchAccount error discrimination, buildTransaction blockhash expiry, sendTransaction confirmation#4
lpsmurf wants to merge 1 commit into
OOBE-PROTOCOL:mainfrom
lpsmurf:fix/client-fetchaccount-error-handling

Conversation

@lpsmurf

@lpsmurf lpsmurf commented Jun 3, 2026

Copy link
Copy Markdown

Summary

Three bugs in SapClient (src/client.ts) found while building on the SDK for the OOBE bounty. All three affect real workflows — particularly x402 payment + SAP registration flows.


Bug 1 — fetchAccount swallows all errors (@ts-ignore + catch-return-null)

Before:

async fetchAccount<T = any>(name: string, address: PublicKey): Promise<T | null> {
  try { // @ts-ignore
  return await this.program.account[name].fetch(address) as T; }
  catch { return null; }
}

Problem: Every error returns null — network failures, wrong IDL, RPC timeouts, malformed responses. Callers cannot tell whether an agent is unregistered or whether the RPC is down. In our registration flow, an RPC timeout silently looked like "agent not registered" and triggered a duplicate registration attempt.

Fix: Inspect the error message. Anchor wraps missing accounts in messages containing "Account does not exist", "AccountNotFound", or "AccountDiscriminatorMismatch" — return null for those. Re-throw everything else.

Also removes the @ts-ignore by accessing program.account through a typed cast, keeping TypeScript safety intact.


Bug 2 — buildTransaction discards lastValidBlockHeight

Before:

const { blockhash } = await this.connection.getLatestBlockhash();
// lastValidBlockHeight is fetched but immediately discarded
return new VersionedTransaction(msg);

Problem: getLatestBlockhash() returns both blockhash and lastValidBlockHeight. The height is needed for confirmTransaction's blockheight-based strategy (the recommended approach since web3.js 1.73). Without it, callers have no transaction deadline — confirmTransaction either uses an unsafe fallback or hangs indefinitely on slow/congested RPCs.

Fix: Return { tx, blockhash, lastValidBlockHeight } so callers can pass the full confirmation strategy. This is a breaking change for the return type — see migration below.

Migration:

// Before
const tx = await client.buildTransaction(ixs, payer);
await client.sendTransaction(tx, [signer]);

// After
const { tx, blockhash, lastValidBlockHeight } = await client.buildTransaction(ixs, payer);
await client.sendTransaction(tx, [signer], { blockhash, lastValidBlockHeight });

Bug 3 — sendTransaction sends but never confirms

Before:

return await this.connection.sendTransaction(tx as any, { ... });

Problem: Returns a signature as soon as the transaction is accepted by the RPC node — not when it's confirmed on-chain. For x402 payment flows this is critical: the caller receives a signature, passes it as X-Payment to the API, and the API verifies on-chain — but the transaction may not be confirmed yet, causing spurious payment failures.

Fix: When the caller provides blockhash + lastValidBlockHeight (from buildTransaction), confirm via BlockheightBasedTransactionConfirmationStrategy before returning. Also throws explicitly if the transaction fails on-chain rather than letting the caller discover it later.


Bug 4 (minor) — No warning when defaulting to mainnet-beta

Added a console.warn in development (NODE_ENV !== "production") when no rpcUrl or connection is provided. Prevents accidental mainnet usage during testing — a common footgun for new integrators.


Testing

Verified against our live SAP registration (agent PDA 8m5MXkunTGabXKLrmVCj2WekLF4V2WwB3gKGuAc872rx) and 54 x402 payment flows on Solana mainnet as part of the OOBE bounty submission.

🤖 Generated with Claude Code

…khash expiry, sendTransaction confirmation

Three bugs fixed in SapClient:

1. fetchAccount swallowed ALL errors via catch-return-null with @ts-ignore.
   Now distinguishes "account not found" (returns null) from real RPC/network
   errors (re-throws). Callers can now reliably differentiate between
   "agent not registered" and "RPC is down".

2. buildTransaction returned only VersionedTransaction, discarding the
   lastValidBlockHeight from getLatestBlockhash. Callers had no deadline
   to use with confirmTransaction, risking open-ended hangs on slow RPCs.
   Now returns { tx, blockhash, lastValidBlockHeight } so callers can pass
   a blockheight-based confirmation strategy.

3. sendTransaction sent but never confirmed. For x402 payment flows this
   means callers received a signature and assumed confirmed status while the
   transaction was still in-flight or could have been dropped. Now confirms
   via blockheight strategy when blockhash context is provided, and throws
   an explicit error if the transaction fails on-chain.

Bonus: constructor warns in development when no rpcUrl is provided so
developers don't hit mainnet accidentally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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