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.
Summary
CaatingaContractClient.invoke()returnsstatus: "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:A
grepforSUCCESS/FAILED/PENDING/NOT_FOUND/TRY_AGAIN_LATER/getTransactionacrosspackages/client/srcreturns 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()(orsend()) returned an object that has a hash or a result field on it". That is true for:getTransactionResponse.status === "FAILED")NOT_FOUNDwhen the SDK's polling window expiresTRY_AGAIN_LATERsubmission that never made it into a ledgerIn 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
The transfer panics on-chain (insufficient balance).
signAndSendresolves with aSentTransactioncarryinggetTransactionResponse.status === "FAILED"and a hash.assertSubmitResultRecognizedseeshashpresent and returns.invokereportsconfirmed. The UI tells the user the transfer succeeded; the balance never moved.Suggested fix
getTransactionResponse.status,sendTransactionResponse.status) and only report"confirmed"onSUCCESS."failed"and"pending"— toCaatingaInvokeResultinpackages/client/src/types.ts, or throw aCaatingaErrorwithXDR_RESULT_FAILEDon a failed ledger result.resultXdr/ diagnostic events on failure; that is the information a developer needs and it is currently discarded unlessdebugRawis set.assertSubmitResultRecognizedshould 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.