# General Ledger service design Status: accepted for the first implementation slice on 2026-08-14. ## Purpose and ownership `GL` is Darano's durable financial journal. It records every committed wallet value movement independently of Stellar so balances and transaction history can be reconstructed during a blockchain or provider outage. GL owns its PostgreSQL database and exposes an internal gRPC API. Wallet does not write GL tables directly, and GL does not write wallet tables. In normal operation Wallet remains the transaction orchestrator and Stellar remains the external settlement network. Promoting GL from a mirror to an operational fallback is an explicit, audited mode change; an outage must never make a failed blockchain operation appear successful automatically. ## Non-negotiable invariants 1. Every monetary journal is double entry. Signed entries sum to zero for each asset in a journal. 2. Journals and entries are append-only. Corrections append a full reversal and a replacement; update and delete operations are not exposed. 3. Amounts cross the API as canonical decimal strings and are stored as `numeric(38,18)`. Floating-point values are rejected at the GL boundary. 4. A source idempotency key identifies one immutable payload. Replaying the same key and payload succeeds with the original result; reusing the key for different content fails. 5. Account, asset, source transaction, correlation, actor, occurrence time, and recording time are retained on every applicable record. 6. A journal commits atomically with all its entries or not at all. 7. A financial effect is journaled only when Wallet has committed that effect. Pending and failed lifecycle events may be retained as audit events but do not create monetary entries. 8. Derived balances are rebuildable solely by ordering and summing immutable entries. Cached balances are disposable projections, never source data. ## Data model ### `ledger_accounts` A stable account is scoped to an asset and optional owner. Initial account classes are `USER_AVAILABLE`, `USER_FROZEN`, `EXTERNAL_BLOCKCHAIN`, `TREASURY`, `MARKET_CLEARING`, `IPG_CLEARING`, and `COMMISSION_REVENUE`. The natural identity `(class, owner_type, owner_id, asset_id)` is unique. ### `journals` Each journal contains a generated UUID, source service, unique idempotency key, source transaction ID and tracking code, effect kind and version, optional reversal target, occurred/recorded timestamps, correlation and actor IDs, blockchain network/hash/sequence references, and JSON metadata. A canonical payload hash detects conflicting reuse of an idempotency key. ### `entries` Each entry contains its journal ID, line number, ledger account, asset ID, signed decimal amount, and optional description. `(journal_id, line_number)` is unique. Positive means credit and negative means debit. Zero entries are invalid. Database constraints and the application transaction jointly enforce precision, immutability, account/asset agreement, and per-asset balance. ### `transaction_events` Lifecycle observations such as `CREATED`, `PENDING_TRX`, `PENDING_ADMIN`, `FAILED`, and `SUCCESSFUL` are append-only events keyed idempotently by source transaction and event version. They preserve error and blockchain metadata but are separate from monetary journals. ## Wallet-to-ledger mapping Every row below balances independently per asset. Where one business operation moves two assets, the entries belong to one correlated journal containing two balanced asset groups. | Wallet effect | Debit | Credit | |---|---|---| | Internal transfer | sender `USER_AVAILABLE` | recipient `USER_AVAILABLE` | | External deposit | `EXTERNAL_BLOCKCHAIN` | recipient `USER_AVAILABLE` | | External withdrawal | sender `USER_AVAILABLE` | `EXTERNAL_BLOCKCHAIN` | | Freeze/lock | user `USER_AVAILABLE` | user `USER_FROZEN` | | Release/unlock | user `USER_FROZEN` | user `USER_AVAILABLE` | | IRT/IPG deposit | `IPG_CLEARING` | user `USER_AVAILABLE` | | IRT withdrawal | user `USER_AVAILABLE` | `IPG_CLEARING` | | Commission | payer or originating clearing account | `COMMISSION_REVENUE` | | Buy/ICO | buyer IRT; treasury asset inventory | `TREASURY` IRT; buyer asset | | Sell/redeem | seller asset; `TREASURY` IRT | treasury asset inventory; seller IRT | | Market trade | buyer quote and seller base | seller quote and buyer base | Trustline creation is non-monetary and is recorded only as a transaction event. The adapter must derive accounts from stable IDs, not display names or public keys. Before integration, each existing Wallet call site must confirm its exact counterparty and fee legs; missing information is a validation failure, not an implicit suspense posting. ## Delivery and ordering Wallet owns a durable `ledger_outbox` table. The wallet state change, wallet transaction update, and outbox insert occur in the same PostgreSQL transaction. A dispatcher delivers events to GL with at-least-once semantics. It retries with backoff and never drops a record; poison records are quarantined and alerted but remain replayable. The idempotency key format is `wallet:::v`. A transaction hash is metadata, not an identity, because it can be absent or replaced. GL serializes posts that affect the same accounts, records both source and receipt ordering, and returns the existing journal for identical duplicates. Together, the transactional outbox and GL idempotency provide effectively-once posting. Wallet must not synchronously dual-write its database and GL. GL/network unavailability therefore cannot lose an already committed wallet effect and does not hold a wallet database transaction open. ## Operating modes and failure semantics - `MIRROR`: normal mode. Wallet follows its existing settlement policy and GL asynchronously records every committed effect. - `DEGRADED_LEDGER`: explicitly enabled by an authorized operator. Eligible internal operations may settle against GL while blockchain-bound operations remain pending. The initial implementation does not activate this mode. - `RECONCILE`: outbound posting is paused or restricted while tooling compares Wallet, GL, and blockchain state and appends approved reversals/corrections. GL rejects unbalanced journals, invalid precision, unknown account/asset combinations, duplicate line numbers, missing source identity, conflicting idempotency payloads, and invalid reversals. Identical retries are successful. If Wallet cannot write its outbox in the local transaction, that wallet transaction rolls back. If dispatch fails, Wallet records retry state without changing the committed financial result. ## Internal service boundary The first `ledger.v1.GeneralLedgerService` contract provides: - `AppendJournal` for one atomic, idempotent balanced journal; - `AppendTransactionEvent` for non-monetary lifecycle history; - `GetJournal` and `ListEntries` for audit and replay inspection; - `GetBalance` for a rebuildable account/asset projection; - `Health` for readiness and database health. Wallet depends on its own `Ledger` port. Its gRPC implementation translates wallet domain values to this contract; Wallet never imports GL persistence types. Deadlines, retry classification, authentication, and correlation metadata are adapter concerns. ## Implementation sequence 1. Add backward-compatible `ledger/v1` protobuf messages and service methods. 2. Scaffold GL with domain, application, infrastructure, and interface layers. 3. Add explicit SQL migrations and a transactional PostgreSQL repository. 4. Implement invariant validation, idempotency, queries, and reversals. 5. Add Wallet's ledger port, gRPC adapter, transactional outbox, and dispatcher. 6. Map each Wallet transaction path and add outage/replay/reconciliation tests. ## Initial non-goals - Replacing Stellar automatically on health-check failure. - Editing or deleting posted journals. - Storing binary floats or using Wallet's mutable transaction table as GL. - Sharing a database schema between Wallet and GL. - Migrating UI, documentation-site, or DevOps repositories.