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}, chosen)Make the account identity itself two legs: AccountId { id: i64, sub: u64 }, with
sub = 0 the main account. Aggregate reads take a base id: i64 plus an optional
subaccount filter.
Pros:
AccountId, so per-subaccount balances
fall out of the existing keys with no new wrapper type.(id: i64, sub: Option<u64>) — None spans every subaccount, Some(s)
restricts to one — while entity ops take the full &AccountId.(id, sub) is a full account record with its own policy, so
inflight holds stay NoOverdraft no matter the destination.Cons:
account.id rather than
passing a distinct base type; the split between base reads (i64) and exact
entity ops (&AccountId) has to be kept clear..0
accesses become .id) plus a schema migration.AccountRef { account, sub } owner/identity typeKeep AccountId as the i64 base and add a separate AccountRef wrapper as the
owner/endpoint/entity identity.
Pros:
AccountId stays a bare i64, so aggregate "all
subaccounts" reads keep a natural base handle.Cons:
AccountId vs
AccountRef) that every layer has to convert between.AccountRef in every owner position) without collapsing to a single identity.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 1, fold the subaccount into AccountId, because a
single two-leg identity keeps balances naturally segregated, keeps inflight holds
NoOverdraft by giving every subaccount its own record, and avoids carrying two
account-identity types. Concretely:
AccountId { id: i64, 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. AccountId::new(id) builds a main account;
with_sub(id, sub) a subaccount; base() drops back to the main account.(id, 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(id: i64, sub: Option<u64>, ..) and
get_transfers_for_account(id: i64, sub: Option<u64>) span all subaccounts when
sub is None and one when Some(s). Ledger::balances(base, asset, sub)
returns one SubAccountBalance per non-closed subaccount and never a summed
total; balance(&AccountId, 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.AccountId's 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.