# Transfers ## Overview A transfer is the atomic unit of value movement in the ledger. It consumes existing postings and creates new ones, preserving per-asset conservation. There are two layers: - **Intent layer** — callers express movements (from, to, asset, amount). The ledger resolves these into concrete postings. - **Envelope layer** — concrete postings to consume and create. Used internally after resolution and available for direct callers. ## Movements A movement is the fundamental building block: ```rust struct Movement { from: AccountId, // account being debited to: AccountId, // account being credited asset: AssetId, // asset to transfer amount: Cent, // amount (may be negative for offset postings) } ``` Every operation — pay, deposit, withdraw — is expressed as one or more movements. The resolve step aggregates net debits per (account, asset) and selects postings only for accounts with a positive net debit. ## Operations ### Pay Transfer value between two accounts. ```rust TransferBuilder::new() .pay(from, to, asset, amount) .build() ``` Produces one movement: | from | to | asset | amount | |------|----|-------|--------| | A | B | USD | 50 | Resolve selects postings from A to cover 50, creates a +50 posting on B, and returns change to A if the selected postings exceed 50. ### Deposit Fund an account from a system/external source. Creates an offset posting on the source and a credit on the target. ```rust TransferBuilder::new() .deposit(to, asset, amount, external) .build() ``` Produces two movements: | from | to | asset | amount | |------|----|-------|--------| | external | external | USD | -100 | | external | to | USD | +100 | The first movement creates a -100 offset posting on the external account. The second creates a +100 posting on the target account. Net debit on the external account: -100 + 100 = **0**. No posting selection is needed — the offset is created directly. Conservation: created sum = -100 + 100 = 0. Consumed sum = 0. Both sides balance. ### Withdraw Move value from an account to an external destination. ```rust TransferBuilder::new() .withdraw(from, asset, amount, external) .build() ``` Produces one movement: | from | to | asset | amount | |------|----|-------|--------| | A | external | USD | 50 | Resolve selects postings from A to cover 50, creates a +50 posting on the external account, and returns change to A. ### Raw movement For operations that don't fit the convenience methods: ```rust TransferBuilder::new() .movement(from, to, asset, amount) .build() ``` ## Resolve Algorithm The resolve step converts a `Transfer` (intent) into an `Envelope` (concrete postings) using a two-pass algorithm: ### Pass 1: Create output postings and aggregate debits For each movement: 1. Create a `NewPosting { owner: to, asset, value: amount }` with `payer: Some(from)` when `from != to` 2. Accumulate the movement's amount into a net debit map keyed by `(from, asset)` ### Pass 2: Select postings for accounts with positive net debit For each `(account, asset)` pair where net debit > 0: 1. Query active postings for that account and asset 2. If positive postings cover the net debit: run greedy largest-first selection, compute change = selected sum − net debit, and (if change > 0) create a change posting returning the remainder to the account. 3. If positive postings are **insufficient**: - For `CappedOverdraft` / `UncappedOverdraft` accounts: consume all positive postings and create a **negative posting** for the shortfall (`net_debit − total_positive`). The `CappedOverdraft` floor is enforced later in validation. - For any other policy: fail with `InsufficientFunds`. Pairs with net debit <= 0 (e.g. the external account in a deposit) are skipped — no posting selection needed. ### Aggregation benefit Aggregating debits before selection means that multiple movements debiting the same account share one selection pass. For example, if a transfer contains two payments from account A (50 + 30), the resolve selects postings once for 80 rather than twice. ## Envelope After resolution, the result is an `Envelope`: ```rust struct Envelope { consumes: Vec, // postings to deactivate creates: Vec, // new postings to create account_snapshots: Vec, book: BookId, user_data: UserData, metadata: Metadata, } ``` The envelope is content-addressed: its `EnvelopeId` is the double-SHA-256 of its canonical binary serialization. This provides idempotency (committing the same envelope twice returns the cached receipt) and tamper evidence. ## Transfer Builder The `TransferBuilder` provides a fluent API for constructing transfers: ```rust let transfer = TransferBuilder::new() .deposit(alice, usd, Cent::from(1000), bank) .pay(alice, bob, usd, Cent::from(200)) .book(sales_book) .metadata(metadata) .build(); ``` A single transfer can contain multiple movements of different types. All movements execute atomically. ## Commit Paths ### Saga commit (default) ``` Transfer → resolve → Envelope → reserve → validate → finalize → Receipt ``` Four-phase pipeline with automatic retry and LIFO compensation on failure. Used by `ledger.commit(transfer)`. ### Atomic commit ``` Envelope → load → plan → apply → Receipt ``` Single-pass pipeline without reservation. Used by `ledger.commit_atomic(envelope)` and internally by `reverse()`. ## Reversal `reverse(transfer_id)` creates a compensating envelope that: 1. Consumes the original transfer's created postings 2. Recreates the original transfer's consumed postings This undoes the operation while preserving the full audit trail — no postings are deleted. ## Validation Every envelope passes through `validate_and_plan()` before being applied. The validation steps are: 1. Non-empty (must consume or create at least one posting) 2. No duplicate consumed PostingIds 3. All consumed postings exist 4. All consumed postings are Active or PendingInactive 5. All referenced accounts exist, not frozen, not closed 6. Account snapshot pinning (if provided) 7. Book policy (if a book is loaded): referenced assets/accounts/flags allowed by the book 8. Per-asset conservation: `sum(consumed) == sum(created)` 9. Negative postings forbidden only on `NoOverdraft` accounts (allowed on overdraft/system/external) 10. Policy enforcement: projected balance satisfies account floor After validation, the effects are applied through a single atomic `commit_transfer` (postings, transfer record, account index, and events commit together or not at all), which also enforces the `CappedOverdraft` CAS guards. See [architecture.md](architecture.md) for details on each check.