Skip to content

feat(backend): close-account REST API (plan/transactions/submit) + verifiable intent#34

Merged
miguelnietoa merged 10 commits into
mainfrom
feat/close-account-api
Jun 22, 2026
Merged

feat(backend): close-account REST API (plan/transactions/submit) + verifiable intent#34
miguelnietoa merged 10 commits into
mainfrom
feat/close-account-api

Conversation

@miguelnietoa

Copy link
Copy Markdown
Member

Summary

  • New stateless REST API to close a Stellar account: POST /v1/{network}/close/plan, POST /v1/{network}/close/transactions, POST /v1/{network}/submit.
  • The backend builds the unsigned XDR; keys never leave the client and signing stays client-side. The API never accepts a secret.
  • Same contract for the LumenWipe front-end and third-party integrators. Chain is the source of truth (every call re-reads live state).
  • Design + plan included: docs/plans/2026-06-21-close-account-api-design.md, docs/plans/2026-06-21-close-account-api-plan.md.

What's here

  • types/close-api.ts — the plan / transaction / decision / intent contract.
  • lib/stellar/intent/serialize.ts — pure XDR→intent serializer (normalized, verifiable). This is the core the future SDK verify() reuses.
  • lib/close-api/decisions.ts — derive/resolve per-asset decision points (convert_to_xlm vs return_to_issuer).
  • lib/close-api/plan-response.ts — map buildPlan to the API response, stable planHash, execution breakdown.
  • lib/close-api/build-transactions.ts — assemble the fused close (reuses the existing pure tx-builder) and attach the verifiable intent.
  • lib/close-api/read-account.ts — shared live-rescan read (parity with the account route).
  • app/api/v1/[network]/close/{plan,transactions} and app/api/v1/[network]/submit.

Trust model

Each returned transaction carries a structured intent (ops, merge destination, payment destinations, minXlmFromConversions). It is present from day one so the SDK can later re-decode the raw XDR and refuse to sign anything that doesn't match the declared intent and the user's decisions. The backend builds bytes but is not a blind-trust point.

Phase-1 scope (follow-ups documented in the design)

  • Direct destinations only → a single fused transaction. Exchange/mediator destinations, claimable-balance accounts, and closes over the per-tx op cap return typed errors (mediator_destination_unsupported, claimable_balances_unsupported, too_many_operations).
  • The SDK and client-side verify(), the DeFi frontier multi-call path, and auth/rate-limiting are follow-ups.

Verification

  • bun type-check + bun lint clean; 238/238 unit tests (intent serializer, decisions, plan-response mapper).
  • New testnet e2e tests/e2e/close-api.spec.ts: plan → transactions → local sign → submit → asserts the source is merged away on-chain. Passes (~26s).
  • ⚠️ Full e2e run: 14/16 passed. The 2 failures are the pre-existing DEX-conversion UI tests (fast-path-close, single-tx-flow) timing out on on-chain confirmation during a congested 16.8m testnet run. They exercise the existing browser flow, which this PR does not touch (additive API files only). Recommend a CI re-run.

Notes

⚠️ Security-sensitive

Touches transaction construction (lib/close-api/build-transactions.ts, reuses tx-builder). Per CLAUDE.md, flagging for closer review. The pure tx-builder is unchanged and still has no network access; the API only returns unsigned XDR.

Server-built unsigned XDR for account closes, consumed by both the LumenWipe
front-end and third parties. Stateless with the chain as source of truth;
/plan returns the full preview, /transactions returns ordered unsigned txs with
a verifiable intent. Covers per-asset decisions, advisory quotes, multi-tx
(all vs frontier), errors/blockers/drift, and how it maps onto the current
pure tx-builder.
Bite-sized TDD plan for the /v1/{network}/close/* endpoints and the pure intent
serializer, reusing the existing tx-builder. Scopes out the client refactor and
the DeFi frontier path as follow-ups.
Thin handler over getAccountState + buildPlan + deriveDecisionPoints. Best-effort
convertibility via path finding; verified end-to-end by the testnet e2e.
Builds one fused close transaction (direct destinations, phase 1) with a verifiable
intent, mirroring the existing fused-close assembly. Mediator/claimable/oversized
closes surface as typed errors. Verified end-to-end by the testnet e2e.
Wraps submitAndWait; accepts a signed envelope only, never a secret. Maps signature,
timeout, malformed-XDR, and upstream failures to typed error responses.
Drives plan -> transactions -> local sign -> submit and asserts the source account
is merged away on-chain. Adds a shared readAccountState helper (live-rescan fallback,
parity with the account route) used by both handlers.
@mintlify

mintlify Bot commented Jun 22, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
lumenwipe 🟢 Ready View Preview Jun 22, 2026, 4:52 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
lumenwipe Ready Ready Preview, Comment Jun 22, 2026 4:52am

@miguelnietoa miguelnietoa merged commit 8d9cd76 into main Jun 22, 2026
4 checks passed
@miguelnietoa miguelnietoa deleted the feat/close-account-api branch June 22, 2026 06:03
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