Files
GL/deployment/AUTHORITY_API.md
T
nfel 88e227f80c
CI/CD Stage / stage (push) Failing after 1m12s
feat(gl): add standalone ledger authority mode
2026-10-06 22:40:07 +03:30

146 lines
6.6 KiB
Markdown

# GL authority API
The `ledger.v1.GeneralLedgerService` gRPC service exposes four additional RPCs.
The contract is in `../proto/ledger/v1/{msg,srv}.proto` from the GL directory;
the generated Go client is in `gen/ledger/v1`.
## Operator toggle
In the existing AdminPanel, open **GL journals → GL operating mode**.
The checkbox, required reason, and recovery confirmation call GL directly.
The operator's ID comes from the authenticated session. Permission
`ledgerops.set_operating_mode` is required; superusers already have it.
Operators also need `ledgerops.view_generalledgerjournal` for journal navigation.
Register the permission using AdminPanel's usual Django `migrate` command
and grant it only to designated operators. Configure the existing
`GL_ADMIN_GRPC_ADDRESS` and `GL_ADMIN_GRPC_TOKEN` settings, with the token
matching GL's `DARANO_GL_ADMIN_TOKEN`. These are server-side settings.
The AdminPanel control changes GL's authority state. Existing Wallet flows
still need to call the internal posting API; changing Wallet's old
`kuknos-enabled` configuration alone continues to select read-only behavior.
## Future Wallet routing contract
Wallet remains the entry point in both modes. The public Wallet RPCs, request
and response schemas, authentication, and client UI stay unchanged; clients
must not call GL directly or choose the execution mode.
| GL mode | Transaction execution | User balance response |
| --- | --- | --- |
| `NORMAL` (default) | Existing Wallet admission → GL settlement queue → Stellar/Kuknos | Existing Wallet behavior |
| `GL_ONLY` | Wallet admission → GL `PostInternalJournal`, with no blockchain call | Wallet returns GL `GetUserBalances` values in its existing response format |
This routing is reserved for future activation. The GL backend and AdminPanel
control exist, but the Wallet routing above is not implemented yet. Keep
`NORMAL` selected for the current Wallet deployment. Enabling the control now
pauses settlement and causes existing Wallet financial writes to fail closed;
it does not automatically turn those endpoints into internal execution.
The future implementation must retain Wallet's authorization and business
validation, persist the execution choice and exact command for recovery, and
use the same transaction identity for retries. A GL commit establishes
financial success; subsequent Wallet history updates are recoverable
projections and must not produce a second financial journal through the normal
outbox. Reconciliation must preserve the original execution choice across
mode changes rather than reinterpret an older blockchain transaction as an
internal transfer. Cutover requires draining normal-mode work and checking
opening balances. Returning to normal requires reconciliation before external
settlement resumes.
Internal transfers, freezes, releases, trades, and their fees can execute
inside GL. An actual external blockchain payment cannot be reported as
externally settled while Stellar/Kuknos is unavailable.
`GetOperatingMode(base.v1.Empty)` returns `mode`, `revision`, `actor_id`,
`reason`, and `changed_at`.
`SetOperatingMode` requires exactly one `x-admin-token` metadata value matching
`DARANO_GL_ADMIN_TOKEN`. To enable internal authority, send:
```json
{
"mode": "GL_ONLY",
"expectedRevision": "0",
"actorId": "operator-17",
"reason": "Kuknos outage; opening balances and outbox checked"
}
```
Use the revision from the latest read, rather than always sending zero.
Mode persists across service restarts. A concurrent toggle returns `ABORTED`;
read the state before deciding whether another change is needed. A toggle
with in-flight settlements or negative user balances returns
`FAILED_PRECONDITION`. Returning to `NORMAL` requires
`reconciliationConfirmed: true` after operator reconciliation.
## Internal execution
`PostInternalJournal` accepts the existing `AppendJournalRequest` and requires
the administrative token, an actor ID, and `GL_ONLY` mode for new postings.
For a transfer, debit the sender's `USER_AVAILABLE` account and credit the
recipient's account by the same exact decimal amount. For a freeze, debit
available and credit frozen; for a release, reverse those legs. Multi-asset
trade and fee legs must balance independently per asset.
The API accepts exact balanced entries so it can represent all internal
business effects without depending on Stellar XDR, account sequences, or a
Horizon endpoint. It does not determine business fees, asset eligibility, or
whether the caller's user is allowed to move those funds; trusted application
admission must supply and authorize those legs. It forbids blockchain
references and `EXTERNAL_BLOCKCHAIN` accounts. The ordinary journal API
remains the normal-mode ingestion interface and rejects new effects in
`GL_ONLY`, while allowing identical retries of previously ingested journals.
Success means the journal and every entry committed atomically. Insufficient
available or frozen funds returns `FAILED_PRECONDITION`. Retry uncertain
outcomes with the exact same idempotency key and payload. Identical retries
return `already_existed=true`; conflicting payloads return `ALREADY_EXISTS`.
A concurrency abort returns `ABORTED`; retry that same request rather than
allocating a new transaction identity. Database errors never mean success.
The server reserves journal metadata key `gl_execution` and stores
`GL_ONLY` as immutable execution provenance. Callers do not set it.
## User balances
`GetUserBalances` reads GL entries without any blockchain call:
```json
{"ownerType": "user", "ownerId": "17", "assetId": "7"}
```
Example response:
```json
{
"ownerType": "user",
"ownerId": "17",
"balances": [
{"assetId": "7", "available": "34", "frozen": "6", "total": "40"}
]
}
```
Omit `assetId` to return all journaled assets for that owner. An unknown owner
has an empty list; a requested asset with no entries returns zero amounts.
Amounts are decimal strings. Available and frozen are read in one database
snapshot. The endpoint is available in either operating mode.
The API is an internal service: expose it through an authenticated application
gateway that binds the requested owner to the logged-in user. Do not expose
arbitrary owner lookups or the operator token directly to a browser.
## Verification
The opt-in PostgreSQL test creates and removes its own isolated schema:
```bash
GL_TEST_DATABASE_URL='<disposable PostgreSQL connection URL>' \
go test -race ./infrastructure/postgres -run TestAuthorityWithPostgres -v
```
It exercises mode authentication, revision conflicts, immutable audit records,
settlement suspension, concurrent overspending prevention, idempotent replay,
freezes/releases, internal RPC posting, and user balance reconstruction.