This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user