Evidence: business modules, indexer and reconciliation end to end on local Stellar (Stage 14 parts 4–5)
Date: 2026-10-30. Network: Quickstart --local, protocol 30. Database: PostgreSQL 17.6. Command:
TEST_DATABASE_URL=… STELLAR_RPC_URL=http://localhost:8000/rpc REQUIRE_INTEGRATION=1 \
go test -count=1 -race ./internal/e2e/Result: PASS (-race, 43 s; part 4 alone: 32 s). The full backend suite (go test ./..., including the 7 transaction-manager tests) also
passes after the handler-routing change.
TestPlatformLifecycle uses the production signer layout (issuer H1–H4 ×10 + AUTH ×1, thresholds 1/20/30; COLD T1–T4;
HOT and SPONSOR online keys; FUNDER) and the real services, wired as the application will wire them. After each
money-moving step it compares every on-chain balance (COLD, HOT, each customer) with its ledger position, and the
on-chain total with TOKEN_SUPPLY.
| Step | Proves |
|---|---|
| Register the COLD account as issuer | Refused: ceremony.Verify finds signers/thresholds/flags that do not match the issuer layout |
| Register the real issuer, activate | Treasury trustlines authorized by AUTH through the engine; activation refused without a unit definition (Q-04), then ACTIVE after on-chain verification |
| KYC approval ×2 | One sponsored transaction per customer; account ACTIVE; trustline authorized on-chain |
| Reserve propose / approve | The proposer's role cannot approve; four-eyes approval makes the snapshot EFFECTIVE |
| ISSUE 1000.001, then 1000 | Above backing refused (capacity_exceeded); within backing: four-eyes, then H1 + H3 sign; COLD = 1000 on-chain and in the ledger |
| REFILL 600, then sweep | T2 + T4 sign COLD → HOT; the automatic sweep returns 500 (HOT 600 > ceiling 500, down to target 100) |
| Price, deposit, gate | Maker-checker price and deposit; gate opens with every Stellar condition healthy |
| BUY 150, then BUY 10 | Above the HOT float refused before anything is signed (insufficient_treasury_inventory); BUY 10 settles |
| TRANSFER 3, SELL 1 | Customer-to-customer and customer-to-HOT payments signed by CUSTODY; bob = 2 on-chain |
| Freeze, unfreeze | Quotes refused at once (account_frozen); trustline unauthorized on-chain; the requester cannot approve the unfreeze (self_approval); a second officer can, and the trustline is authorized again |
| BURN 200 | T1 + T3 sign COLD → issuer; supply 800 on-chain and in the ledger |
| Transfer to a trustline revoked behind the platform's back | Fails in the ledger (NOT_AUTHORIZED); order FAILED, alice's hold released, ledger still equals the chain |
| Reconciliation after the lifecycle (part 5) | Indexer caught up from before the ceremony; every check PASS from one node at one ledger (recorded on the run); every indexed movement is linked to an intent; the ceremony's own setup transactions open no authority incident |
| Same trustline revocation | trustline_states_match FAILs (business view ACTIVE, chain unauthorized); after re-authorizing, all PASS |
| HOT key used outside the platform (no memo) | Indexer: CRITICAL UNEXPLAINED_TOKEN_MOVEMENT, trading blocked. Reconciliation: exactly the HOT and alice balance checks FAIL; supply unchanged (a transfer) |
| CHANNEL key used to add a signer | Indexer: CRITICAL UNEXPECTED_AUTHORITY_CHANGE |
| A third party deploys the asset's SAC | Indexer: WARNING SAC_DEPLOYED (no block) |
Not covered here (later parts): a claimable balance, liquidity pool or SAC contract balance actually created in our
asset (the platform's accounts cannot create one, and an outsider cannot hold the asset: it would need the leaked AUTH
key first), the data-lake backfill source against a real bucket, stellar.toml on a non-local network, HTTP API and RBAC at the edge, and fault injection for
the business handlers (RECONCILIATION_REQUIRED parking).
Binary smoke run (Stage 14 part 6)
The real dara binary against Quickstart and a fresh PostgreSQL database, driven with curl:
| Step | Result |
|---|---|
dara migrate | 7 migrations applied |
dara bootstrap-local (GOLD, 2 + 1 channels) | Issuer, COLD, HOT, sponsor, funder and channels created with the production layouts; manifest written. Never overwrites a manifest: one that matches the chain is a no-op (part 7), one that does not is an error |
dara all | Startup verifies every manifest account's layout on-chain and registers it; /readyz ready; gate opens after the hold-down; restart against the same manifest and database is clean |
| Admin bootstrap, TOTP enrollment, login | MFA-pending session until TOTP is confirmed; then full session |
| Asset over the API: create → register-issuer → unit-definition → activate | DRAFT → CONFIGURING → ACTIVE, with the Treasury trustlines authorized by the running worker |
| Manual reconciliation run | PASS, recording node local and the ledger read; indexer 0 ledgers behind |
| Customer register / login on the public listener | KYC PENDING; assets listed |
| Staff email + right password on the public listener | Before the fix: mfa_required (account and password confirmed). After: unauthenticated, identical to a wrong password or an unknown email (TestStaffLoginOnCustomerListenerRevealsNothing) |
make demo in containers (Stage 14 part 7)
make demo on rootless Podman (docker-compose v5): Stellar Quickstart --local, PostgreSQL, migrate, bootstrap
(the ceremony with stand-in human signers), worker, api, api-admin, then dara demo. Staff and customers act
through the customer and admin HTTP APIs; the stand-in signers verify each proposal's envelope (network, and that the
hash shown is the hash of the envelope) before signing, and upload signatures through
POST /v1/admin/treasury/proposals/{id}/signatures, as people would.
| Step | Checked against chain, ledger and API |
|---|---|
| 1 | GOLD created, the manifest's issuer registered, unit definition set, activated once the worker authorized the COLD and HOT trustlines |
| 2 | Reserve snapshot of 1,000,000 g proposed and approved by two people: EFFECTIVE |
| 3–4 | ISSUE 1,000,000 to COLD (four-eyes + 2 of 4 issuer signers), REFILL 1,000 to HOT (four-eyes + 2 of 4 Treasury signers) |
| 5 | Supply 1,000,000 on-chain and in the ledger; COLD 999,000 and HOT 1,000 on both |
| 6–8 | User A registered, KYC → sponsored account with an authorized trustline, funded; BUY 100 from HOT; amounts to the unit |
| 9–11 | User B; TRANSFER 20 A → B; SELL 30 to HOT; every balance on both sides |
| 12 | Supply unchanged; fee revenue 32,587,500 |
| 13 | Freeze B: trustline revoked on-chain. Unfreeze: the requesting officer's own approval is refused (403); a second officer's re-authorizes on-chain |
| 14 | Reconciliation (after the indexer caught up): 15 checks PASS (ledger, COLD/HOT/customer balances, trustline states, supply, backing, indexer, issuer layout) |
| 15 | Gate open, both listeners ready, no open incidents |
Result: DEMO PASS: 67 checks. Also verified: make demo-down and up resume the same network (the bootstrap finds
a matching manifest and does nothing; the worker starts clean; the gate reopens); make demo-seed seeds GOLD and
SILVER, nine staff and two funded customers, and a later scheduled reconciliation passes; alice logs in from the
host and sees both assets tradable; the admin listener is not published to the host.
Problems found and fixed while getting here:
| Problem | Fix |
|---|---|
docker-compose run recreated every dependency, Quickstart included, although their configuration was unchanged (compose v5 with Podman). That reset the network under the manifest; bootstrap then refused the stale manifest, as designed | run --rm --no-deps after up -d |
A freshly started Quickstart answered getLedgerEntries with "could not query captive core (404)" shortly after reporting healthy, so dara demo failed its startup verification | seed and demo retry startup until their wait deadline; the long-running services already restart |
The demo polled POST …/activate every 500 ms while trustlines were authorized and hit the per-user mutation limit (30/min) | The demo waits on reads (GET /v1/admin/blockchain-operations) and activates once |
seed retried activation with a variable the failed call had zeroed, then reported "already present" on the half-seeded database | Fixed; a part-way seed is now reported as such |
Web apps on the platform (Stage 14 part 8)
Production builds of the admin console and the wallet (next start), against dara all on a seeded database and
local Quickstart, driven in Chrome:
| Check | Result |
|---|---|
| Staff sign-in with TOTP through the console's same-origin proxy | Signed in; the admin API is reached only through the console |
| A REFILL proposed (treasury.a) and approved (reserve.approver) through the proxy | Listed on Signing with network passphrase, hash, envelope, and "weight 0 of 20 (Medium threshold)" for COLD |
| T1 then T2 signatures pasted into the Signing page (computed with the stand-in keys) | "weight 10 of 20" after T1; after T2 the proposal left the queue and Supply showed the refill COMPLETED with its transaction |
| An ISSUE above the effective backing | Refused: capacity_exceeded (available_to_issue 0) |
| Overview, System, Customers | Hot and cold Treasury on ledger and chain agree; RPC node HEALTHY; indexer 1 ledger behind; customers with their Stellar accounts |
| Wallet (Persian, RTL): account page | "حساب استلار شما", QR of the G… account, custodial note, issuer powers incl. clawback |
| Wallet trade with a stale price | "Not tradable" with the stale-price reason (the seeded price was older than max_price_age) |
| Wallet trade after a fresh price | Quote 2 g: 20,100,000 + fee 50,250 = 20,150,250 Toman; confirmed; order COMPLETED, "final on Stellar", hash linked to Dara Scan |
Found while doing it: the console's two-key navigation shortcuts fire when typing lands outside an input (a mis-aimed
click moved between pages, and the theme toggled). This is upstream starter behaviour, not a data risk (no
shortcut submits anything), but the signing fields now carry explicit accessible names. Workspace checks:
pnpm -r typecheck, lint, test, check:deps, check:rtl all pass; pnpm build builds the three apps.