Pārlūkot izejas kodu

Remove the vestigial UserData type and the orphaned withdraw saga step

UserData held three fixed-width correlation slots on Account, Transfer, and
Envelope, but no caller ever set them: every construction site used
UserData::default(). The slots still cost a schema column, a JSON round-trip on
every account write, and bytes in the content-addressed hash preimage, so they
were pure overhead. Drop the type, its fields, the builder methods, and the
accessor.

Because the fields were part of the canonical ToBytes serialization for
Envelope and Account, removing them changes the hash preimage. Bump
CANONICAL_VERSION so the format change is explicit rather than silent, and add
migration 003 to drop the accounts.user_data column. The historical 001/002
migrations keep the column so an existing database still upgrades in order.

Also delete WithdrawInput and WithdrawMovementStep. They were defined and
implemented alongside the pay and deposit saga steps but never referenced,
because withdrawals go through TransferBuilder::withdraw() instead. Both
slipped past dead-code detection only because they are pub in a library crate.
Cesar Rodas 17 stundas atpakaļ
vecāks
revīzija
ee20cc1f2b

+ 0 - 17
crates/kuatia-core/src/validate.rs

@@ -485,7 +485,6 @@ mod tests {
             policy,
             flags: AccountFlags::empty(),
             book: BookId(0),
-            user_data: UserData::default(),
             metadata: BTreeMap::new(),
         }
     }
@@ -514,7 +513,6 @@ mod tests {
                 },
             ],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         }
@@ -547,7 +545,6 @@ mod tests {
             consumes: vec![],
             creates: vec![],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -578,7 +575,6 @@ mod tests {
                 payer: None,
             }],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -608,7 +604,6 @@ mod tests {
             consumes: vec![missing_pid],
             creates: vec![],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -651,7 +646,6 @@ mod tests {
                 payer: None,
             }],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -741,7 +735,6 @@ mod tests {
                 payer: None,
             }],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -793,7 +786,6 @@ mod tests {
                 payer: None,
             }],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -846,7 +838,6 @@ mod tests {
                 payer: None,
             }],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -905,7 +896,6 @@ mod tests {
                 payer: None,
             }],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -940,7 +930,6 @@ mod tests {
             consumes: vec![pid, pid], // duplicate
             creates: vec![],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -992,7 +981,6 @@ mod tests {
                 },
             ],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -1025,7 +1013,6 @@ mod tests {
             policy,
             flags: AccountFlags::empty(),
             book: BookId(0),
-            user_data: UserData::default(),
             metadata: BTreeMap::new(),
         }
     }
@@ -1053,7 +1040,6 @@ mod tests {
                 },
             ],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -1098,7 +1084,6 @@ mod tests {
                 },
             ],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -1147,7 +1132,6 @@ mod tests {
                 },
             ],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };
@@ -1187,7 +1171,6 @@ mod tests {
                 },
             ],
             book: BookId(0),
-            user_data: UserData::default(),
             account_snapshots: vec![],
             metadata: BTreeMap::new(),
         };

+ 6 - 8
crates/kuatia-storage-sql/src/lib.rs

@@ -100,6 +100,10 @@ impl SqlStore {
                 "002_subaccounts",
                 include_str!("migrations/002_subaccounts.sql"),
             ),
+            (
+                "003_drop_user_data",
+                include_str!("migrations/003_drop_user_data.sql"),
+            ),
         ];
 
         for (name, sql) in migrations {
@@ -228,9 +232,6 @@ fn row_to_account(row: &sqlx::any::AnyRow) -> Result<Account, StoreError> {
     let book: i64 = row
         .try_get("book")
         .map_err(|e| StoreError::Internal(e.to_string()))?;
-    let user_data_json: String = row
-        .try_get("user_data")
-        .map_err(|e| StoreError::Internal(e.to_string()))?;
     let metadata_json: String = row
         .try_get("metadata")
         .map_err(|e| StoreError::Internal(e.to_string()))?;
@@ -241,7 +242,6 @@ fn row_to_account(row: &sqlx::any::AnyRow) -> Result<Account, StoreError> {
         policy: deserialize_policy(&policy_str)?,
         flags: AccountFlags::from_bits_truncate(flags_bits as u32),
         book: BookId::new(book),
-        user_data: deserialize_json(&user_data_json)?,
         metadata: deserialize_json(&metadata_json)?,
     })
 }
@@ -342,7 +342,7 @@ impl AccountStore for SqlStore {
         }
 
         let res = sqlx::query(
-            "INSERT INTO accounts (id, subaccount, version, policy, flags, book, user_data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id, subaccount, version) DO NOTHING"
+            "INSERT INTO accounts (id, subaccount, version, policy, flags, book, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id, subaccount, version) DO NOTHING"
         )
             .bind(account.id.id)
             .bind(account.id.sub)
@@ -350,7 +350,6 @@ impl AccountStore for SqlStore {
             .bind(serialize_policy(&account.policy)?)
             .bind(account.flags.bits() as i32)
             .bind(account.book.0)
-            .bind(serialize_json(&account.user_data)?)
             .bind(serialize_json(&account.metadata)?)
             .execute(&mut *tx)
             .await
@@ -407,7 +406,7 @@ impl AccountStore for SqlStore {
         }
 
         let res = sqlx::query(
-            "INSERT INTO accounts (id, subaccount, version, policy, flags, book, user_data, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id, subaccount, version) DO NOTHING"
+            "INSERT INTO accounts (id, subaccount, version, policy, flags, book, metadata) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (id, subaccount, version) DO NOTHING"
         )
             .bind(account.id.id)
             .bind(account.id.sub)
@@ -415,7 +414,6 @@ impl AccountStore for SqlStore {
             .bind(serialize_policy(&account.policy)?)
             .bind(account.flags.bits() as i32)
             .bind(account.book.0)
-            .bind(serialize_json(&account.user_data)?)
             .bind(serialize_json(&account.metadata)?)
             .execute(&mut *tx)
             .await

+ 1 - 0
crates/kuatia-storage-sql/src/migrations/003_drop_user_data.sql

@@ -0,0 +1 @@
+ALTER TABLE accounts DROP COLUMN user_data;

+ 7 - 6
crates/kuatia-storage-sql/tests/sqlite.rs

@@ -38,7 +38,6 @@ async fn columns_store_hex_ids_and_json_text() {
         policy: AccountPolicy::NoOverdraft,
         flags: AccountFlags::empty(),
         book: BookId(0),
-        user_data: UserData::default(),
         metadata: std::collections::BTreeMap::new(),
     };
     store.create_account(account).await.unwrap();
@@ -64,14 +63,14 @@ async fn columns_store_hex_ids_and_json_text() {
     assert_eq!(transfer_id, "ab".repeat(32));
 
     // The account payload is readable JSON text, not a blob.
-    let row = sqlx::query("SELECT user_data FROM accounts")
+    let row = sqlx::query("SELECT metadata FROM accounts")
         .fetch_one(&pool)
         .await
         .unwrap();
-    let user_data: String = row.try_get("user_data").unwrap();
+    let metadata: String = row.try_get("metadata").unwrap();
     assert!(
-        serde_json::from_str::<serde_json::Value>(&user_data).is_ok(),
-        "user_data should be JSON text, got: {user_data}"
+        serde_json::from_str::<serde_json::Value>(&metadata).is_ok(),
+        "metadata should be JSON text, got: {metadata}"
     );
 }
 
@@ -154,7 +153,9 @@ async fn migration_upgrades_existing_rows_to_main_account() {
         .execute(&pool)
         .await
         .unwrap();
-    let user_data = serde_json::to_string(&UserData::default()).unwrap();
+    // Legacy 001/002 schema carried a user_data JSON column; the 003 migration
+    // drops it. Seed it with the value the old UserData type serialized to.
+    let user_data = r#"{"d128":0,"d64":0,"d32":0}"#.to_string();
     let metadata =
         serde_json::to_string(&std::collections::BTreeMap::<String, String>::new()).unwrap();
     sqlx::query(

+ 0 - 1
crates/kuatia-storage/src/store_tests.rs

@@ -26,7 +26,6 @@ fn make_account(id: i64, policy: AccountPolicy) -> Account {
         policy,
         flags: AccountFlags::empty(),
         book: BookId(0),
-        user_data: UserData::default(),
         metadata: BTreeMap::new(),
     }
 }

+ 3 - 50
crates/kuatia-types/src/lib.rs

@@ -31,7 +31,7 @@ pub trait ToBytes {
 /// Bumped to 2 when `Cent` moved to a fixed 16-byte canonical encoding (ADR-0011).
 /// Bumped to 3 when `AccountId` gained a `subaccount` leg folded into its
 /// canonical bytes (ADR-0012).
-pub const CANONICAL_VERSION: u8 = 3;
+pub const CANONICAL_VERSION: u8 = 4;
 
 /// Append a `u16` in big-endian to `buf`.
 pub fn write_u16(buf: &mut Vec<u8>, v: u16) {
@@ -660,17 +660,6 @@ pub struct NewPosting {
 // Transfer
 // ---------------------------------------------------------------------------
 
-/// Fixed-width secondary identifiers.
-#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
-pub struct UserData {
-    /// 128-bit user-defined slot (e.g. external UUID).
-    pub d128: u128,
-    /// 64-bit user-defined slot (e.g. correlation id).
-    pub d64: u64,
-    /// 32-bit user-defined slot (e.g. category code).
-    pub d32: u32,
-}
-
 /// Free-form key→value metadata.
 pub type Metadata = BTreeMap<String, Vec<u8>>;
 
@@ -687,8 +676,6 @@ pub struct Envelope {
     pub account_snapshots: Vec<AccountSnapshotId>,
     /// Book this envelope belongs to.
     pub book: BookId,
-    /// Fixed-width secondary identifiers.
-    pub user_data: UserData,
     /// Free-form key-value metadata.
     pub metadata: Metadata,
 }
@@ -714,11 +701,6 @@ impl Envelope {
         self.book
     }
 
-    /// Fixed-width secondary identifiers.
-    pub fn user_data(&self) -> &UserData {
-        &self.user_data
-    }
-
     /// Free-form key-value metadata.
     pub fn metadata(&self) -> &Metadata {
         &self.metadata
@@ -772,12 +754,6 @@ impl EnvelopeBuilder {
         self
     }
 
-    /// Set the fixed-width secondary identifiers.
-    pub fn user_data(mut self, user_data: UserData) -> Self {
-        self.envelope.user_data = user_data;
-        self
-    }
-
     /// Set the account version pins.
     pub fn account_snapshots(mut self, snapshots: Vec<AccountSnapshotId>) -> Self {
         self.envelope.account_snapshots = snapshots;
@@ -865,16 +841,14 @@ pub struct Account {
     pub flags: AccountFlags,
     /// Book this entity belongs to.
     pub book: BookId,
-    /// Fixed-width secondary identifiers.
-    pub user_data: UserData,
     /// Free-form key-value metadata.
     pub metadata: Metadata,
 }
 
 impl Account {
     /// Create a version-1 main-subaccount account with the given policy: no flags,
-    /// the default book, and empty user data / metadata. Convenience for the common
-    /// case — set the other fields explicitly when you need them.
+    /// the default book, and empty metadata. Convenience for the common case; set
+    /// the other fields explicitly when you need them.
     pub fn new(id: AccountId, policy: AccountPolicy) -> Self {
         Self::new_ref(id, policy)
     }
@@ -887,7 +861,6 @@ impl Account {
             policy,
             flags: AccountFlags::empty(),
             book: DEFAULT_BOOK,
-            user_data: UserData::default(),
             metadata: Metadata::new(),
         }
     }
@@ -946,8 +919,6 @@ pub struct Transfer {
     pub movements: Vec<Movement>,
     /// Book this entity belongs to.
     pub book: BookId,
-    /// Fixed-width secondary identifiers.
-    pub user_data: UserData,
     /// Free-form key-value metadata.
     pub metadata: Metadata,
 }
@@ -1030,12 +1001,6 @@ impl TransferBuilder {
         self
     }
 
-    /// Set the fixed-width secondary identifiers.
-    pub fn user_data(mut self, user_data: UserData) -> Self {
-        self.transfer.user_data = user_data;
-        self
-    }
-
     /// Set the free-form metadata.
     pub fn metadata(mut self, metadata: Metadata) -> Self {
         self.transfer.metadata = metadata;
@@ -1093,16 +1058,6 @@ impl ToBytes for PostingId {
     }
 }
 
-impl ToBytes for UserData {
-    fn to_bytes(&self) -> Vec<u8> {
-        let mut buf = Vec::with_capacity(28);
-        write_u128(&mut buf, self.d128);
-        write_u64(&mut buf, self.d64);
-        write_u32(&mut buf, self.d32);
-        buf
-    }
-}
-
 impl ToBytes for AccountPolicy {
     fn to_bytes(&self) -> Vec<u8> {
         let mut buf = Vec::with_capacity(9);
@@ -1186,7 +1141,6 @@ impl ToBytes for Envelope {
         }
 
         buf.extend(self.book.to_bytes());
-        buf.extend(self.user_data.to_bytes());
 
         write_u32(&mut buf, self.metadata.len() as u32);
         for (key, value) in &self.metadata {
@@ -1210,7 +1164,6 @@ impl ToBytes for Account {
         buf.extend(self.policy.to_bytes());
         buf.extend(self.flags.to_bytes());
         buf.extend(self.book.to_bytes());
-        buf.extend(self.user_data.to_bytes());
 
         write_u32(&mut buf, self.metadata.len() as u32);
         for (key, value) in &self.metadata {

+ 0 - 1
crates/kuatia/examples/create_accounts.rs

@@ -39,7 +39,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
         policy: AccountPolicy::ExternalAccount, // boundary for deposits/withdrawals
         flags: AccountFlags::empty(),           // not frozen, not closed
         book: DEFAULT_BOOK,                     // the implicit default book
-        user_data: UserData::default(),         // fixed-width correlation slots
         metadata: BTreeMap::new(),              // free-form key/value metadata
     };
     ledger.create_account(external).await?;

+ 1 - 3
crates/kuatia/src/ledger.rs

@@ -268,7 +268,6 @@ impl Ledger {
             .consumes(consumes)
             .creates(creates)
             .book(transfer.book)
-            .user_data(transfer.user_data.clone())
             .metadata(transfer.metadata.clone())
             .build();
 
@@ -956,7 +955,7 @@ pub struct LoadedState {
 #[cfg(test)]
 mod recovery_tests {
     use super::*;
-    use kuatia_core::{Account, AccountFlags, ReservationId, TransferBuilder, UserData};
+    use kuatia_core::{Account, AccountFlags, ReservationId, TransferBuilder};
     use kuatia_storage::mem_store::InMemoryStore;
     use std::collections::BTreeMap;
 
@@ -967,7 +966,6 @@ mod recovery_tests {
             policy,
             flags: AccountFlags::empty(),
             book: kuatia_core::BookId(0),
-            user_data: UserData::default(),
             metadata: BTreeMap::new(),
         }
     }

+ 2 - 40
crates/kuatia/src/saga.rs

@@ -20,7 +20,7 @@
 //!
 //! # High-level composition
 //!
-//! High-level steps (`PayMovementStep`, `DepositMovementStep`, etc.) compose over
+//! High-level steps (`PayMovementStep` and `DepositMovementStep`) compose over
 //! the intent-layer API and can be combined into multi-transfer sagas via `legend!`.
 
 use std::sync::Arc;
@@ -316,7 +316,7 @@ impl Step<LedgerCtx, SagaError> for FinalizeTransferStep {
 }
 
 // ===========================================================================
-// High-level steps (pay / deposit / withdraw movement steps)
+// High-level steps (pay / deposit movement steps)
 // ===========================================================================
 
 /// Input for the pay movement saga step.
@@ -345,19 +345,6 @@ pub struct DepositInput {
     pub external: AccountId,
 }
 
-/// Input for the withdraw movement saga step.
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct WithdrawInput {
-    /// Account to withdraw from.
-    pub from: AccountId,
-    /// Asset being withdrawn.
-    pub asset: AssetId,
-    /// Amount to withdraw.
-    pub amount: Cent,
-    /// External account receiving the withdrawal.
-    pub external: AccountId,
-}
-
 // ---------------------------------------------------------------------------
 // Helpers
 // ---------------------------------------------------------------------------
@@ -424,28 +411,3 @@ impl Step<LedgerCtx, SagaError> for DepositMovementStep {
         compensate_last_receipt(ctx).await
     }
 }
-
-/// Saga step: withdraw value to an external account via a single-movement transfer.
-pub struct WithdrawMovementStep;
-
-#[async_trait]
-impl Step<LedgerCtx, SagaError> for WithdrawMovementStep {
-    type Input = WithdrawInput;
-
-    async fn execute(ctx: &mut LedgerCtx, input: &WithdrawInput) -> Result<StepOutcome, SagaError> {
-        let ledger = ctx.ledger_arc()?;
-        let transfer = TransferBuilder::new()
-            .withdraw(input.from, input.asset, input.amount, input.external)
-            .build();
-        let receipt = ledger.commit(transfer).await?;
-        ctx.receipts.push(receipt);
-        Ok(StepOutcome::Continue)
-    }
-
-    async fn compensate(
-        ctx: &mut LedgerCtx,
-        _input: &WithdrawInput,
-    ) -> Result<CompensationOutcome, SagaError> {
-        compensate_last_receipt(ctx).await
-    }
-}

+ 0 - 1
crates/kuatia/tests/concurrency.rs

@@ -38,7 +38,6 @@ fn make_account(id: i64, policy: AccountPolicy) -> Account {
         policy,
         flags: AccountFlags::empty(),
         book: BookId(0),
-        user_data: UserData::default(),
         metadata: BTreeMap::new(),
     }
 }

+ 0 - 1
crates/kuatia/tests/integration.rs

@@ -30,7 +30,6 @@ fn make_account(id: i64, policy: AccountPolicy) -> Account {
         policy,
         flags: AccountFlags::empty(),
         book: BookId(0),
-        user_data: UserData::default(),
         metadata: BTreeMap::new(),
     }
 }

+ 0 - 1
crates/kuatia/tests/saga.rs

@@ -28,7 +28,6 @@ fn make_account(id: i64, policy: AccountPolicy) -> Account {
         policy,
         flags: AccountFlags::empty(),
         book: BookId(0),
-        user_data: UserData::default(),
         metadata: BTreeMap::new(),
     }
 }

+ 2 - 2
doc/accounting-mapping.md

@@ -83,8 +83,8 @@ Both are a single balanced event. In the classical entry, `Σ Dr (115) = Σ Cr
 These differ in grain: one record vs. the collection of all records.
 
 - `Transfer` / `Envelope` = one record (one journal entry).
-  - `Transfer` is the intent: `{ movements: Vec<Movement>, book, user_data,
-    metadata }`. Callers express what should happen, not which postings.
+  - `Transfer` is the intent: `{ movements: Vec<Movement>, book, metadata }`.
+    Callers express what should happen, not which postings.
   - `Envelope` is the resolved form produced by `resolve()`: `{ consumes:
     Vec<PostingId>, creates: Vec<NewPosting>, account_snapshots, book, … }`.
     It names the concrete postings to spend and create.

+ 0 - 1
doc/accounts.md

@@ -18,7 +18,6 @@ the ledger balance.
 | `policy` | `AccountPolicy` | Balance floor rule (see below) |
 | `flags` | `AccountFlags` | Lifecycle flags (`FROZEN`, `CLOSED`) + user-defined (`USER_0` to `USER_7`) |
 | `book` | `BookId` | Book this account belongs to |
-| `user_data` | `UserData` | Fixed 28 bytes: `u128 + u64 + u32` for external refs |
 | `metadata` | `Metadata` | `BTreeMap<String, Vec<u8>>` for free-form data |
 
 ## Subaccounts

+ 1 - 2
doc/architecture.md

@@ -206,7 +206,7 @@ serialization. This serves two purposes:
 - **Tamper evidence**: any modification to a transfer's data changes its ID.
 
 All domain types implement deterministic binary serialization (`ToBytes` trait)
-using big-endian encoding with a version prefix (`CANONICAL_VERSION = 1`).
+using big-endian encoding with a version prefix (`CANONICAL_VERSION = 4`).
 
 ## Append-Only Account Versioning
 
@@ -377,7 +377,6 @@ workflows:
 |------|---------|------------|
 | `PayMovementStep` | Build pay transfer, `ledger.commit(...)` | `ledger.reverse(receipt.transfer_id)` |
 | `DepositMovementStep` | Build deposit transfer, `ledger.commit(...)` | `ledger.reverse(receipt.transfer_id)` |
-| `WithdrawMovementStep` | Build withdraw transfer, `ledger.commit(...)` | `ledger.reverse(receipt.transfer_id)` |
 
 ### Custom orchestration with legend
 

+ 1 - 3
doc/crates.md

@@ -32,10 +32,9 @@ dependencies (`sha2`, `serde`, `bitflags`).
 | `NewPosting` | Posting to be created (no id yet, assigned during validation) |
 | `Transfer` | Atomic unit: consumes postings + creates postings + metadata |
 | `EnvelopeBuilder` | Fluent builder for `Transfer` construction |
-| `Account` | Versioned entity with policy, flags, book, user_data, metadata |
+| `Account` | Versioned entity with policy, flags, book, metadata |
 | `AccountPolicy` | Balance floor rule: `NoOverdraft`, `CappedOverdraft`, `UncappedOverdraft`, `SystemAccount`, `ExternalAccount` |
 | `AccountFlags` | Bitflags: `FROZEN`, `CLOSED` |
-| `UserData` | Fixed 28 bytes (u128 + u64 + u32) for correlation IDs, external refs |
 | `Metadata` | `BTreeMap<String, Vec<u8>>` for free-form key-value data |
 | `Receipt` | Confirmation of a committed transfer (contains `transfer_id`) |
 | `AutoId` | Snowflake-inspired i64 ID generator: `[0][40-bit ms][23-bit CRC32 or counter]`. The ms field counts from `KUATIA_EPOCH_MS` (2026-01-01T00:00:00Z), giving ~34.8 years forward. Lives in `kuatia-types::autoid` |
@@ -285,7 +284,6 @@ sagas and re-runs the whole saga for `Reserving` ones.
 |------|---------|------------|
 | `PayMovementStep` | Build pay transfer, `ledger.commit(...)` | `ledger.reverse(receipt.transfer_id)` |
 | `DepositMovementStep` | Build deposit transfer, `ledger.commit(...)` | `ledger.reverse(receipt.transfer_id)` |
-| `WithdrawMovementStep` | Build withdraw transfer, `ledger.commit(...)` | `ledger.reverse(receipt.transfer_id)` |
 
 #### Custom orchestration
 

+ 0 - 1
doc/transfers.md

@@ -153,7 +153,6 @@ struct Envelope {
     creates: Vec<NewPosting>,       // new postings to create
     account_snapshots: Vec<AccountSnapshotId>,
     book: BookId,
-    user_data: UserData,
     metadata: Metadata,
 }
 ```