Inflight holds let you reserve funds now and settle later: authorize a trade, then confirm it (in full or in parts) or void it. This is the authorization/capture pattern, applied to a multi-leg trade.
The design and its rationale are in adr/0004-inflight-holds-via-holding-accounts.md. This page is the usage guide.
An inflight transaction is an ordinary trade whose every destination is
rewritten to a fresh per-destination holding account (NoOverdraft, flagged
INFLIGHT). Committing that rewritten transfer parks the funds:
Confirmed trade Inflight form
----------------- --------------------------------
A -> B -> 100 EUR A -> hold(B) -> 100 EUR
B -> A -> 10 BTC B -> hold(A) -> 10 BTC
A -> fee -> 1 BTC A -> hold(fee) -> 1 BTC
B -> fee -> 1 EUR B -> hold(fee) -> 1 EUR
A hold is keyed by destination, so hold(fee) collects EUR from B and BTC from
A. Each holding posting's funder is recorded in the authorize transfer's leg
table, so a void returns each posting to the account that paid it.
Nothing new is stored. The authorize transfer is the record: its EnvelopeId is
the inflight handle, and its metadata carries the leg table. Every artifact
(holding accounts, authorize, confirm, void) is tagged in an inflight.
metadata namespace, so the lifecycle is read from recorded fields, not inferred.
stateDiagram-v2
[*] --> Held: authorize
Held --> Held: confirm (partial)
Held --> Confirmed: confirm_all / drained
Held --> Voided: void
Confirmed --> [*]
Voided --> [*]
Every operation is an ordinary commit, so idempotency, conservation, and crash
recovery are inherited unchanged. A hold closes automatically once drained.
All methods hang off Ledger.
use kuatia::prelude::*;
// Authorize the trade. Funds leave A and B and park in the holds.
let trade = TransferBuilder::new()
.pay(a, b, eur, Cent::from(100))
.pay(b, a, btc, Cent::from(10))
.pay(a, fee, btc, Cent::from(1))
.pay(b, fee, eur, Cent::from(1))
.build();
let auth = ledger.authorize(trade).await?;
// Confirm one leg partially: deliver 40 EUR of B's hold to B now.
ledger.confirm(&auth.inflight, &b, &eur, Cent::from(40)).await?;
// Confirm everything else and close the holds.
ledger.confirm_all(&auth.inflight).await?;
// ...or return everything to the funders instead.
ledger.void(&auth.inflight).await?;
// Derived status: per-leg authorized / confirmed / voided / held, plus state.
let status = ledger.inflight_status(&auth.inflight).await?;
// The holding accounts of every open inflight.
let open = ledger.list_open_inflights().await?;
authorize returns an Authorization { inflight, receipt, legs }. The
inflight field (an EnvelopeId) is the handle passed to every other call.
NoOverdraft, so confirming
more than it holds fails validation. The sum of confirmations can never exceed
the authorized amount.balance(hold, asset).
Confirmed and voided amounts are summed from the metadata-tagged settling
transfers. Nothing mutable is stored.(hold, asset). When two accounts fund the same asset
into the same destination hold, a partially-confirmed remainder cannot be
split back to each funder exactly; void returns it in leg order.INFLIGHT flag).crates/kuatia/src/inflight.rs — the API and metadata schema.crates/kuatia/tests/inflight.rs — authorize, confirm, partial confirm, void,
over-confirm rejection, one-open-per-account, and status tests.AccountFlags::INFLIGHT — crates/kuatia-types/src/lib.rs.