146 lines
6.6 KiB
Markdown
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.
|