Skip to content

fix(api): derive the plan transaction count from the builder splitter - #596

Merged
salazarsebas merged 2 commits into
mainfrom
fix/plan-estimated-transaction-count
Oct 10, 2026
Merged

salazarsebas merged 2 commits into
mainfrom
fix/plan-estimated-transaction-count

Conversation

@salazarsebas

Copy link
Copy Markdown
Member

What

  • execution.estimatedTransactionCount and execution.transactions[].covers on the close plan are now derived from the same splitter the builder packs transactions with (splitCloseOps, the single place the 100-operation cap is applied), instead of the hard-coded 1.
  • The count is null (with transactions empty) when it cannot be known safely at plan time: the plan contains a DeFi exit (its transaction count depends on live debt and simulation), a held Soroban token has no decision yet, or no destination was given (an exchange adds a transaction).
  • The fused-close module and its tests are renamed to close-operations (CloseOperationsInput, assembleCloseOps[Tagged], packCloseTransactions, buildCloseTx), and the "fused" wording is removed from apps/api/src and the SDK doc comment.

Why

CLAUDE.md and docs/architecture.md say there is no fused mode: exchange closes, claimable balances, Soroban token moves and more than 100 operations all need more transactions. The plan always said 1, so an integrator showing "1 transaction" before signing was wrong for many accounts.

Commits

  1. refactor(builder): rename the fused close module to close operations - rename only.
  2. fix(api): derive the plan transaction count from the builder splitter - behavior, types, OpenAPI, SDK.

Evidence the rename does not change emitted operations

A throwaway script (not committed) built a fixed matrix with pinned keys, sequence and clock through packFusedCloseTransactions, buildFusedCloseTx and assembleFusedCloseOpsTagged before the rename and through the renamed functions after: 13 inputs (empty, 3 and 150 trustlines, memo id and text, no merge, 150 claims, claim plus add-trustline, issuer, transfer, data plus offers, sponsorship revoke, signer normalization) x funded and underfunded (sponsored-fee) accounts. Output covers every XDR, covers, summary and needsSponsoredFee. diff before.json after.json is empty; both files have sha256 4b0d875b681c17e190208d15014c3aa287fe6ee528403530dc1a44dbe601f5dd. The only builder change in the second commit is batchItems(tagged, OP_BATCH_LIMIT) becoming splitCloseOps(tagged), which is that exact call.

Tests

  • plan-transaction-count.test.ts (new) compares the plan against the number of transactions buildCloseTransactions returns summed across rounds, and the per-transaction covers, for: a single-transaction account (1), an exchange via the mediator (2), a claimable-balance account (2) and a 150-trustline account over the cap (2).
  • close-api-plan-response.test.ts covers single, exchange, claim round, cap split with reason: "op_batch", token moves, exit and undecided input (null) and empty (0).
  • Run against the unmodified sources, the new tests fail (exchange, claimable, >100 ops report 1 instead of 2).
  • bun run type-check, lint, test (api 1645, web 844, sdk 50, playground 55) and format:check pass locally.

Consumers of the nullable field

apps/web, apps/playground, the SDK and docs/ do not read the value anywhere in rendering: the only readers are the web duplicate type (updated), examples/headless-close.ts (now prints no figure for null) and the testnet e2e spec (still 1 for its account). No UI shows the count, so nothing can render "1" or "null" for an unknown count.

SDK

Breaking type change, so 0.4.2 to 0.5.0 (package.json, src/version.ts, etc/sdk.api.md, CHANGELOG naming who must change code: anyone reading the field as a number).

Issue checklist

  • Unit test reproducing the bug, count equals builder output: done.
  • Single-transaction account reports 1, nothing to do reports 0: done.
  • OpenAPI schema (regenerated, nullable) and @lumenwipe/types doc comments describe the field; SDK comment no longer says "fused close": done.
  • grep -rniw fused apps/api/src packages/sdk/src is empty. The issue's literal grep -rni still matches the substring in "refused" (for example decisions.ts), which is unrelated.

Risks and scope

Security review

Security-sensitive (transaction construction naming and splitting). The security-review skill ran from the worktree but saw an empty diff (it inspects the main checkout), so this was hand-reviewed: no operation, verify allowlist, key handling or network input changes; the new code only counts plan steps with no I/O; the rename is proven byte-identical above. No findings.

Closes #481

@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lumenwipe Ready Ready Preview Oct 10, 2026 6:28pm UTC
playground Ready Ready Preview Oct 10, 2026 6:28pm UTC

@salazarsebas salazarsebas self-assigned this Oct 10, 2026
@salazarsebas
salazarsebas merged commit 8a4b512 into main Oct 10, 2026
24 checks passed
@salazarsebas
salazarsebas deleted the fix/plan-estimated-transaction-count branch October 10, 2026 18:30

This branch was successfully deployed

2 active deployments
Preview – lumenwipe — 1af9e749 Deployed Oct 10, 2026 by vercel[bot]
Preview – playground — 1af9e749 Deployed Oct 10, 2026 by vercel[bot]
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.

[api] Derive the plan's estimated transaction count from the builder and drop fused-close naming

1 participant