kuatia-types, kuatia-core, kuatia-storage,
kuatia-storage-sql, kuatia (ledger, inflight), kuatia-dashboardADR-0004 modeled an inflight hold as a fresh standalone holding account per
destination and allowed only one open inflight per destination at a time
(open_inflight_hold_for rejected a second). Two limits followed: a destination
could not host several concurrent inflights, and a hold account was a random id
unrelated to the destination, so "which holds belong to account X" was a metadata
scan rather than a property of identity.
We want many concurrent inflights per account, with holds that are attributable
to their destination. The natural model is a subaccount: an account is
identified by a base id plus an i64-range subaccount, default 0 (the main
account), and a hold is a subaccount of its destination.
NoOverdraft policy regardless of the destination's.AccountId (a composite {id, sub})Make the base id itself carry the subaccount.
Pros:
AccountId carries the
subaccount with no signature change.Cons:
.0 access across the codebase and both storage
backends.AccountRef { account, sub } owner/identity type (chosen)Keep AccountId as the i64 base. Introduce AccountRef as the
owner/endpoint/entity identity, and let aggregate reads take a base AccountId
plus an optional subaccount filter.
Pros:
AccountRef, so per-subaccount balances fall out of the
existing keys with almost no logic change.(&AccountId, sub: Option<..>): None spans every
subaccount, Some(s) restricts to one. This is exactly the segregated,
optionally-filtered query the balances API needs.(base, sub) is a full account record with its own policy,
so inflight holds stay NoOverdraft no matter the destination.Cons:
AccountId -> AccountRef in
every owner/identity position) plus a schema migration.(id, sub) in its key, so
the accounts primary key and the transfer_accounts index widen.Track a subaccount only on postings, with the account entity keyed by base id and its policy shared by all subaccounts.
Pros:
accounts table does not change.Cons:
SystemAccount/overdraft destination would inherit
that policy and could be over-confirmed. The structural over-confirm guard is
lost for those destinations.Chosen option: Option 2, a separate AccountRef identity, because it is the
only option that keeps balances naturally segregated and optionally filtered,
keeps inflight holds NoOverdraft by giving every subaccount its own record, and
still threads through the code with a mechanical rename rather than new
parameters everywhere. Concretely:
AccountRef { account: AccountId, sub: u64 } is the owner of a posting, the
endpoint of a movement, the id of an Account, and the subject of a snapshot.
sub = 0 is the main account. sub is unsigned because it is an opaque,
unordered id (an inflight hold's subaccount is the low 64 bits of a hash of the
submitted trade), so the full range is usable.(account, sub) is its own account record with its own policy, flags,
book, and version. The accounts primary key becomes (id, subaccount,
version) and transfer_accounts carries the subaccount.get_postings_by_account and
get_transfers_for_account take sub: Option<u64>. Ledger::balances(base,
asset, sub) returns one SubAccountBalance per non-closed subaccount and never
a summed total; balance(&AccountRef, asset) reads exactly one subaccount.
Closed subaccounts are excluded from the aggregate reads.sub is
derived from a hash of the submitted trade (deterministic and known before the
holds are created, unlike the authorize transfer's own id). Each destination's
hold is (destination, sub). Re-authorizing the identical trade collides on the
existing hold (rejected); a different trade derives a different sub, so a
destination hosts many concurrent inflights. open_inflight_hold_for and the
"one open inflight per account" rule are removed.002_subaccounts migration adds the
subaccount column (existing rows default to 0, the main account) and rebuilds
accounts / transfer_accounts for the widened keys. 001_init.sql is left
intact.list_subaccounts.NoOverdraft records, and the reservation protocol still serializes captures.AccountRef is folded into canonical bytes), and
the schema migrates. Existing data upgrades in place to subaccount = 0.(hold, asset) still cannot split a
partially-confirmed remainder exactly on void; unchanged from ADR-0004.