Skip to content

client: invoke() hardcodes status "confirmed" and never reads the on-chain transaction status — failed and pending transactions are reported as success #140

Description

@Dione-b

Summary

CaatingaContractClient.invoke() returns status: "confirmed" as a hardcoded literal (packages/client/src/client/caatinga-contract-client.ts:125). Nothing in the submit path ever inspects the transaction's on-chain outcome.

The only post-submit check is assertSubmitResultRecognized (packages/client/src/client/transaction-submit.ts:93-119), and it validates shape, not status:

const hasTransactionId = "txHash" in record || "transactionHash" in record || "hash" in record || ...;
const hasResult = "result" in record;
if (hasTransactionId || hasResult) return;   // ← accepted

A grep for SUCCESS / FAILED / PENDING / NOT_FOUND / TRY_AGAIN_LATER / getTransaction across packages/client/src returns no matches. The Soroban RPC lifecycle states are never read.

Why it matters

status: "confirmed" is the primary signal the client hands to application code, and it is documented as a result state. In practice it means only "signAndSend() (or send()) returned an object that has a hash or a result field on it". That is true for:

  • a transaction that reached the ledger and failed (getTransactionResponse.status === "FAILED")
  • a transaction still pending / NOT_FOUND when the SDK's polling window expires
  • a TRY_AGAIN_LATER submission that never made it into a ledger

In every one of those cases the app receives { status: "confirmed", transactionHash: "..." } and will show the user a success state for a transaction that did not succeed.

Failure scenario

const res = await client.contract("token").invoke("transfer", { to, amount });
if (res.status === "confirmed") showSuccess(res.transactionHash);

The transfer panics on-chain (insufficient balance). signAndSend resolves with a SentTransaction carrying getTransactionResponse.status === "FAILED" and a hash. assertSubmitResultRecognized sees hash present and returns. invoke reports confirmed. The UI tells the user the transfer succeeded; the balance never moved.

Suggested fix

  • Read the real status off the submit response (getTransactionResponse.status, sendTransactionResponse.status) and only report "confirmed" on SUCCESS.
  • Add distinct result states — at minimum "failed" and "pending" — to CaatingaInvokeResult in packages/client/src/types.ts, or throw a CaatingaError with XDR_RESULT_FAILED on a failed ledger result.
  • Surface the decoded resultXdr / diagnostic events on failure; that is the information a developer needs and it is currently discarded unless debugRaw is set.
  • assertSubmitResultRecognized should stay as the shape guard, but must not be the only gate before claiming success.

Scope: audit was read-only, no code changed. The XDR path lives in @caatinga/client, so this review covers that package plus the bindings pipeline in @caatinga/core.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Stellar WaveIssues in the Stellar wave programarea: contractsDeploy, upgrade, invoke, contract logicbugSomething isn't workingpriority: highHigh priority fix - data corruption, security, or recovery failure risk

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions