diff --git a/Cargo.lock b/Cargo.lock index 24621cd2c6f..6bdae9d83fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9191,7 +9191,9 @@ version = "0.9.0" dependencies = [ "candid", "ic-ledger-hash-of", + "ic-stable-structures", "num-traits", + "proptest", "serde", "serde_bytes", ] diff --git a/rs/ledger_suite/common/ledger_core/BUILD.bazel b/rs/ledger_suite/common/ledger_core/BUILD.bazel index 797c654a270..ba8f054c248 100644 --- a/rs/ledger_suite/common/ledger_core/BUILD.bazel +++ b/rs/ledger_suite/common/ledger_core/BUILD.bazel @@ -11,6 +11,7 @@ rust_library( # Keep sorted. "//packages/ic-ledger-hash-of:ic_ledger_hash_of", "@crate_index//:candid", + "@crate_index//:ic-stable-structures", "@crate_index//:num-traits", "@crate_index//:serde", "@crate_index//:serde_bytes", @@ -21,6 +22,7 @@ rust_test( name = "ledger_core_test", crate = ":ledger_core", deps = [ + "@crate_index//:proptest", ], ) diff --git a/rs/ledger_suite/common/ledger_core/Cargo.toml b/rs/ledger_suite/common/ledger_core/Cargo.toml index fa577268bfe..469dc7b61ed 100644 --- a/rs/ledger_suite/common/ledger_core/Cargo.toml +++ b/rs/ledger_suite/common/ledger_core/Cargo.toml @@ -9,8 +9,10 @@ documentation.workspace = true [dependencies] candid = { workspace = true } ic-ledger-hash-of = { path = "../../../../packages/ic-ledger-hash-of" } +ic-stable-structures = { workspace = true } num-traits = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } [dev-dependencies] +proptest = { workspace = true } diff --git a/rs/ledger_suite/common/ledger_core/src/approvals.rs b/rs/ledger_suite/common/ledger_core/src/approvals.rs index 5939d2a791e..9cf9006e7f7 100644 --- a/rs/ledger_suite/common/ledger_core/src/approvals.rs +++ b/rs/ledger_suite/common/ledger_core/src/approvals.rs @@ -1,7 +1,13 @@ use crate::timestamp::TimeStamp; use crate::tokens::{CheckedSub, TokensType, Zero}; +use candid::Nat; +use ic_stable_structures::{storable::Bound, Storable}; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; +use std::{ + borrow::Cow, + io::{Cursor, Read}, +}; #[cfg(test)] mod tests; @@ -474,3 +480,40 @@ where fn remote_future() -> TimeStamp { TimeStamp::from_nanos_since_unix_epoch(u64::MAX) } + +impl + TryFrom> Storable for Allowance { + fn to_bytes(&self) -> Cow<[u8]> { + let mut buffer = vec![]; + let amount: Nat = self.amount.clone().into(); + amount + .encode(&mut buffer) + .expect("Unable to serialize amount"); + if let Some(expires_at) = self.expires_at { + buffer.extend(expires_at.as_nanos_since_unix_epoch().to_le_bytes()); + } + // We don't serialize arrived_at - it is not used after stable structures migration. + Cow::Owned(buffer) + } + + fn from_bytes(bytes: Cow<[u8]>) -> Self { + let mut cursor = Cursor::new(bytes.into_owned()); + let amount = Nat::decode(&mut cursor).expect("Unable to deserialize amount"); + let amount = Tokens::try_from(amount).expect("Unable to convert Nat to Tokens"); + // arrived_at was not serialized, use a default value. + let arrived_at = TimeStamp::from_nanos_since_unix_epoch(0); + let mut expires_at_bytes = [0u8; 8]; + let expires_at = match cursor.read_exact(&mut expires_at_bytes) { + Ok(()) => Some(TimeStamp::from_nanos_since_unix_epoch(u64::from_le_bytes( + expires_at_bytes, + ))), + _ => None, + }; + Self { + amount, + arrived_at, + expires_at, + } + } + + const BOUND: Bound = Bound::Unbounded; +} diff --git a/rs/ledger_suite/common/ledger_core/src/approvals/tests.rs b/rs/ledger_suite/common/ledger_core/src/approvals/tests.rs index c6ea7acb0b4..c112741de90 100644 --- a/rs/ledger_suite/common/ledger_core/src/approvals/tests.rs +++ b/rs/ledger_suite/common/ledger_core/src/approvals/tests.rs @@ -3,6 +3,7 @@ use std::collections::HashSet; use super::*; use crate::timestamp::TimeStamp; use crate::tokens::Tokens; +use ic_stable_structures::Storable; use std::cmp; fn ts(n: u64) -> TimeStamp { @@ -619,3 +620,37 @@ fn expected_allowance_if_zero_no_approval() { } ); } + +use proptest::prelude::{any, prop_assert_eq, proptest}; +use proptest::strategy::Strategy; + +#[test] +fn allowance_serialization() { + fn arb_token() -> impl Strategy { + any::().prop_map(Tokens::from_e8s) + } + fn arb_timestamp() -> impl Strategy { + any::().prop_map(TimeStamp::from_nanos_since_unix_epoch) + } + fn arb_opt_expiration() -> impl Strategy> { + proptest::option::of(any::().prop_map(TimeStamp::from_nanos_since_unix_epoch)) + } + fn arb_allowance() -> impl Strategy> { + (arb_token(), arb_opt_expiration(), arb_timestamp()).prop_map( + |(amount, expires_at, arrived_at)| Allowance { + amount, + expires_at, + arrived_at, + }, + ) + } + proptest!(|(allowance in arb_allowance())| { + let new_allowance: Allowance = Allowance::from_bytes(allowance.to_bytes()); + prop_assert_eq!(new_allowance.amount, allowance.amount); + prop_assert_eq!(new_allowance.expires_at, allowance.expires_at); + prop_assert_eq!( + new_allowance.arrived_at, + TimeStamp::from_nanos_since_unix_epoch(0) + ); + }) +}