From 0b6c20c7e850bcd978e6cd911a8da93656d3f001 Mon Sep 17 00:00:00 2001 From: Bogdan Warinschi Date: Wed, 22 Apr 2026 12:26:11 +0000 Subject: [PATCH] feat(ic-icrc1): add freeze/unfreeze Operation variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 4 new Operation variants: FreezeAccount, UnfreezeAccount, FreezePrincipal, UnfreezePrincipal (ICRC-123). These are no-op operations that record freeze/unfreeze events on the ledger without affecting balances. - Add variants with account/principal, caller, mthd, reason fields - Add FlattenedTransaction fields (account, principal) for CBOR serde - Set btype correctly using BTYPE_123_* constants from icrc-ledger-types - Wire btype-based deserialization in Block::decode path - No-op apply() — no balance changes - Fix all exhaustive matches across ic-icrc1, index-ng, rosetta, test utils - Add freeze_operation_tests: CBOR round-trip, btype, generic block tests Co-Authored-By: Claude Opus 4.6 (1M context) --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 10 + .../ledger/canbench_results/canbench_u256.yml | 18 +- .../ledger/canbench_results/canbench_u64.yml | 22 +- rs/ledger_suite/icrc1/src/endpoints.rs | 6 + rs/ledger_suite/icrc1/src/lib.rs | 191 ++++++++++++- rs/ledger_suite/icrc1/test_utils/src/lib.rs | 28 +- rs/ledger_suite/icrc1/tests/tests.rs | 250 ++++++++++++++++++ .../test_utils/in_memory_ledger/src/lib.rs | 6 + .../common/storage/storage_operations/mod.rs | 12 + .../icrc1/src/common/storage/types.rs | 54 +++- .../icrc1/src/common/utils/utils.rs | 6 + .../icrc1/src/construction_api/services.rs | 6 +- .../icrc1/src/construction_api/utils.rs | 12 + rs/rosetta-api/icrc1/src/data_api/services.rs | 10 + .../icrc1/tests/multitoken_system_tests.rs | 6 +- rs/rosetta-api/icrc1/tests/system_tests.rs | 6 +- 16 files changed, 605 insertions(+), 38 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index f0b763a70610..9b18134211d4 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -1119,6 +1119,12 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { Operation::AuthorizedBurn { from, amount, .. } => { debit(block_index, from, amount); } + Operation::FreezeAccount { .. } + | Operation::UnfreezeAccount { .. } + | Operation::FreezePrincipal { .. } + | Operation::UnfreezePrincipal { .. } => { + // Freeze/unfreeze operations do not affect balances + } }, ); } @@ -1164,6 +1170,10 @@ fn get_accounts(block: &Block) -> Vec { Operation::FeeCollector { .. } => vec![], Operation::AuthorizedMint { to, .. } => vec![to], Operation::AuthorizedBurn { from, .. } => vec![from], + Operation::FreezeAccount { account, .. } | Operation::UnfreezeAccount { account, .. } => { + vec![account] + } + Operation::FreezePrincipal { .. } | Operation::UnfreezePrincipal { .. } => vec![], } } diff --git a/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml b/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml index 9996b29af8a8..ad9e789b07c6 100644 --- a/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml +++ b/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml @@ -2,7 +2,7 @@ benches: bench_icrc1_transfers: total: calls: 1 - instructions: 54503709185 + instructions: 54516392211 heap_increase: 264 stable_memory_increase: 256 scopes: @@ -13,17 +13,17 @@ benches: stable_memory_increase: 0 icrc1_transfer: calls: 1 - instructions: 12932131566 + instructions: 12936411636 heap_increase: 32 stable_memory_increase: 0 icrc2_approve: calls: 1 - instructions: 19387929174 + instructions: 19392009244 heap_increase: 29 stable_memory_increase: 128 icrc2_transfer_from: calls: 1 - instructions: 21482922683 + instructions: 21487202753 heap_increase: 3 stable_memory_increase: 0 icrc3_get_blocks: @@ -38,18 +38,18 @@ benches: stable_memory_increase: 0 pre_upgrade: calls: 1 - instructions: 148924963 + instructions: 148924979 heap_increase: 129 stable_memory_increase: 128 upgrade: calls: 1 - instructions: 491281484 + instructions: 491281500 heap_increase: 200 stable_memory_increase: 128 bench_upgrade_baseline: total: calls: 1 - instructions: 8690537 + instructions: 8690553 heap_increase: 258 stable_memory_increase: 128 scopes: @@ -60,12 +60,12 @@ benches: stable_memory_increase: 0 pre_upgrade: calls: 1 - instructions: 78458 + instructions: 78474 heap_increase: 129 stable_memory_increase: 128 upgrade: calls: 1 - instructions: 8689629 + instructions: 8689645 heap_increase: 258 stable_memory_increase: 128 version: 0.4.1 diff --git a/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u64.yml b/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u64.yml index 0db46f8c3142..1e3886e3e59d 100644 --- a/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u64.yml +++ b/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u64.yml @@ -2,7 +2,7 @@ benches: bench_icrc1_transfers: total: calls: 1 - instructions: 52125398932 + instructions: 52134023453 heap_increase: 263 stable_memory_increase: 256 scopes: @@ -13,17 +13,17 @@ benches: stable_memory_increase: 0 icrc1_transfer: calls: 1 - instructions: 12277234966 + instructions: 12283171030 heap_increase: 34 stable_memory_increase: 0 icrc2_approve: calls: 1 - instructions: 18497580928 + instructions: 18503661238 heap_increase: 25 stable_memory_increase: 128 icrc2_transfer_from: calls: 1 - instructions: 20649025716 + instructions: 20654602846 heap_increase: 3 stable_memory_increase: 0 icrc3_get_blocks: @@ -33,39 +33,39 @@ benches: stable_memory_increase: 0 post_upgrade: calls: 1 - instructions: 351211421 + instructions: 342206914 heap_increase: 72 stable_memory_increase: 0 pre_upgrade: calls: 1 - instructions: 148925235 + instructions: 148925244 heap_increase: 129 stable_memory_increase: 128 upgrade: calls: 1 - instructions: 500139397 + instructions: 491134899 heap_increase: 201 stable_memory_increase: 128 bench_upgrade_baseline: total: calls: 1 - instructions: 8696311 + instructions: 8691726 heap_increase: 258 stable_memory_increase: 128 scopes: post_upgrade: calls: 1 - instructions: 8613896 + instructions: 8609302 heap_increase: 129 stable_memory_increase: 0 pre_upgrade: calls: 1 - instructions: 79491 + instructions: 79500 heap_increase: 129 stable_memory_increase: 128 upgrade: calls: 1 - instructions: 8695403 + instructions: 8690818 heap_increase: 258 stable_memory_increase: 128 version: 0.4.1 diff --git a/rs/ledger_suite/icrc1/src/endpoints.rs b/rs/ledger_suite/icrc1/src/endpoints.rs index 5efea769b263..b95ff3d2de8c 100644 --- a/rs/ledger_suite/icrc1/src/endpoints.rs +++ b/rs/ledger_suite/icrc1/src/endpoints.rs @@ -285,6 +285,12 @@ impl From> for Transaction { reason, }); } + Operation::FreezeAccount { .. } + | Operation::UnfreezeAccount { .. } + | Operation::FreezePrincipal { .. } + | Operation::UnfreezePrincipal { .. } => { + panic!("freeze/unfreeze not yet supported in candid conversion") + } } tx diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index 9e22523ec16e..883f0d5e6c1c 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -17,6 +17,10 @@ use ic_ledger_core::{ use ic_ledger_hash_of::HashOf; use icrc_ledger_types::icrc1::account::Account; use icrc_ledger_types::icrc122::schema::{BTYPE_122_BURN, BTYPE_122_MINT}; +use icrc_ledger_types::icrc123::schema::{ + BTYPE_123_FREEZE_ACCOUNT, BTYPE_123_FREEZE_PRINCIPAL, BTYPE_123_UNFREEZE_ACCOUNT, + BTYPE_123_UNFREEZE_PRINCIPAL, +}; use icrc_ledger_types::{icrc1::transfer::Memo, icrc3::transactions::TRANSACTION_FEE_COLLECTOR}; use serde::{Deserialize, Serialize}; @@ -69,6 +73,30 @@ pub enum Operation { mthd: Option, reason: Option, }, + FreezeAccount { + account: Account, + caller: Option, + mthd: Option, + reason: Option, + }, + UnfreezeAccount { + account: Account, + caller: Option, + mthd: Option, + reason: Option, + }, + FreezePrincipal { + principal: Principal, + caller: Option, + mthd: Option, + reason: Option, + }, + UnfreezePrincipal { + principal: Principal, + caller: Option, + mthd: Option, + reason: Option, + }, } // A [Transaction] but flattened meaning that [Operation] @@ -141,6 +169,15 @@ struct FlattenedTransaction { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub reason: Option, + + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "compact_account::opt")] + account: Option, + + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + principal: Option, } impl TryFrom> for Transaction { @@ -201,6 +238,38 @@ impl TryFrom<(Option, FlattenedTransaction)> mthd: value.mthd, reason: value.reason, }, + Some(BTYPE_123_FREEZE_ACCOUNT) => Operation::FreezeAccount { + account: value + .account + .ok_or("`account` field required for `123freezeaccount` block")?, + caller: value.caller, + mthd: value.mthd, + reason: value.reason, + }, + Some(BTYPE_123_UNFREEZE_ACCOUNT) => Operation::UnfreezeAccount { + account: value + .account + .ok_or("`account` field required for `123unfreezeaccount` block")?, + caller: value.caller, + mthd: value.mthd, + reason: value.reason, + }, + Some(BTYPE_123_FREEZE_PRINCIPAL) => Operation::FreezePrincipal { + principal: value + .principal + .ok_or("`principal` field required for `123freezeprincipal` block")?, + caller: value.caller, + mthd: value.mthd, + reason: value.reason, + }, + Some(BTYPE_123_UNFREEZE_PRINCIPAL) => Operation::UnfreezePrincipal { + principal: value + .principal + .ok_or("`principal` field required for `123unfreezeprincipal` block")?, + caller: value.caller, + mthd: value.mthd, + reason: value.reason, + }, _ => Operation::try_from(value) .map_err(|e| format!("{} and/or unknown btype {:?}", e, btype_str))?, }; @@ -280,18 +349,37 @@ impl From> for FlattenedTransaction Some("mint".to_string()), Transfer { .. } => Some("xfer".to_string()), Approve { .. } => Some("approve".to_string()), - FeeCollector { .. } | AuthorizedMint { .. } | AuthorizedBurn { .. } => None, + FeeCollector { .. } + | AuthorizedMint { .. } + | AuthorizedBurn { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, }, from: match &t.operation { Transfer { from, .. } | Burn { from, .. } | Approve { from, .. } | AuthorizedBurn { from, .. } => Some(*from), - Mint { .. } | FeeCollector { .. } | AuthorizedMint { .. } => None, + Mint { .. } + | FeeCollector { .. } + | AuthorizedMint { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, }, to: match &t.operation { Mint { to, .. } | Transfer { to, .. } | AuthorizedMint { to, .. } => Some(*to), - Burn { .. } | Approve { .. } | FeeCollector { .. } | AuthorizedBurn { .. } => None, + Burn { .. } + | Approve { .. } + | FeeCollector { .. } + | AuthorizedBurn { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, }, spender: match &t.operation { Transfer { spender, .. } | Burn { spender, .. } => spender.to_owned(), @@ -299,7 +387,11 @@ impl From> for FlattenedTransaction None, + | AuthorizedBurn { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, }, amount: match &t.operation { Burn { amount, .. } @@ -308,14 +400,24 @@ impl From> for FlattenedTransaction Some(amount.clone()), - FeeCollector { .. } => None, + FeeCollector { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, }, fee: match &t.operation { Transfer { fee, .. } | Approve { fee, .. } | Mint { fee, .. } | Burn { fee, .. } => fee.to_owned(), - FeeCollector { .. } | AuthorizedMint { .. } | AuthorizedBurn { .. } => None, + FeeCollector { .. } + | AuthorizedMint { .. } + | AuthorizedBurn { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, }, expected_allowance: match &t.operation { Approve { @@ -326,7 +428,11 @@ impl From> for FlattenedTransaction None, + | AuthorizedBurn { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, }, expires_at: match &t.operation { Approve { expires_at, .. } => expires_at.to_owned(), @@ -335,7 +441,11 @@ impl From> for FlattenedTransaction None, + | AuthorizedBurn { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, }, fee_collector: match &t.operation { FeeCollector { fee_collector, .. } => fee_collector.to_owned(), @@ -344,28 +454,71 @@ impl From> for FlattenedTransaction None, + | AuthorizedBurn { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, }, caller: match &t.operation { FeeCollector { caller, .. } | AuthorizedMint { caller, .. } - | AuthorizedBurn { caller, .. } => caller.to_owned(), + | AuthorizedBurn { caller, .. } + | FreezeAccount { caller, .. } + | UnfreezeAccount { caller, .. } + | FreezePrincipal { caller, .. } + | UnfreezePrincipal { caller, .. } => caller.to_owned(), Mint { .. } | Transfer { .. } | Burn { .. } | Approve { .. } => None, }, mthd: match &t.operation { FeeCollector { mthd, .. } | AuthorizedMint { mthd, .. } - | AuthorizedBurn { mthd, .. } => mthd.to_owned(), + | AuthorizedBurn { mthd, .. } + | FreezeAccount { mthd, .. } + | UnfreezeAccount { mthd, .. } + | FreezePrincipal { mthd, .. } + | UnfreezePrincipal { mthd, .. } => mthd.to_owned(), Mint { .. } | Transfer { .. } | Burn { .. } | Approve { .. } => None, }, reason: match &t.operation { - AuthorizedMint { reason, .. } | AuthorizedBurn { reason, .. } => reason.to_owned(), + AuthorizedMint { reason, .. } + | AuthorizedBurn { reason, .. } + | FreezeAccount { reason, .. } + | UnfreezeAccount { reason, .. } + | FreezePrincipal { reason, .. } + | UnfreezePrincipal { reason, .. } => reason.to_owned(), Mint { .. } | Transfer { .. } | Burn { .. } | Approve { .. } | FeeCollector { .. } => None, }, + account: match &t.operation { + FreezeAccount { account, .. } | UnfreezeAccount { account, .. } => Some(*account), + Mint { .. } + | Transfer { .. } + | Burn { .. } + | Approve { .. } + | FeeCollector { .. } + | AuthorizedMint { .. } + | AuthorizedBurn { .. } + | FreezePrincipal { .. } + | UnfreezePrincipal { .. } => None, + }, + principal: match &t.operation { + FreezePrincipal { principal, .. } | UnfreezePrincipal { principal, .. } => { + Some(*principal) + } + Mint { .. } + | Transfer { .. } + | Burn { .. } + | Approve { .. } + | FeeCollector { .. } + | AuthorizedMint { .. } + | AuthorizedBurn { .. } + | FreezeAccount { .. } + | UnfreezeAccount { .. } => None, + }, } } } @@ -565,6 +718,10 @@ impl LedgerTransaction for Transaction { Operation::FeeCollector { .. } => { panic!("FeeCollector107 not implemented") } + Operation::FreezeAccount { .. } + | Operation::UnfreezeAccount { .. } + | Operation::FreezePrincipal { .. } + | Operation::UnfreezePrincipal { .. } => {} } Ok(()) } @@ -752,6 +909,10 @@ impl BlockType for Block { let btype = match &transaction.operation { Operation::AuthorizedMint { .. } => Some(BTYPE_122_MINT.to_string()), Operation::AuthorizedBurn { .. } => Some(BTYPE_122_BURN.to_string()), + Operation::FreezeAccount { .. } => Some(BTYPE_123_FREEZE_ACCOUNT.to_string()), + Operation::UnfreezeAccount { .. } => Some(BTYPE_123_UNFREEZE_ACCOUNT.to_string()), + Operation::FreezePrincipal { .. } => Some(BTYPE_123_FREEZE_PRINCIPAL.to_string()), + Operation::UnfreezePrincipal { .. } => Some(BTYPE_123_UNFREEZE_PRINCIPAL.to_string()), _ => None, }; let effective_fee = match &transaction.operation { @@ -763,7 +924,11 @@ impl BlockType for Block { Operation::Mint { .. } | Operation::Burn { .. } | Operation::AuthorizedMint { .. } - | Operation::AuthorizedBurn { .. } => None, + | Operation::AuthorizedBurn { .. } + | Operation::FreezeAccount { .. } + | Operation::UnfreezeAccount { .. } + | Operation::FreezePrincipal { .. } + | Operation::UnfreezePrincipal { .. } => None, }; let (fee_collector, fee_collector_block_index) = match fee_collector { Some(FeeCollector { diff --git a/rs/ledger_suite/icrc1/test_utils/src/lib.rs b/rs/ledger_suite/icrc1/test_utils/src/lib.rs index 9428c7f1cb69..3f7c59ec2d3d 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/lib.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/lib.rs @@ -17,6 +17,10 @@ use icrc_ledger_types::icrc2::approve::ApproveArgs; use icrc_ledger_types::icrc2::transfer_from::TransferFromArgs; use icrc_ledger_types::icrc107::schema::BTYPE_107; use icrc_ledger_types::icrc122::schema::{BTYPE_122_BURN, BTYPE_122_MINT}; +use icrc_ledger_types::icrc123::schema::{ + BTYPE_123_FREEZE_ACCOUNT, BTYPE_123_FREEZE_PRINCIPAL, BTYPE_123_UNFREEZE_ACCOUNT, + BTYPE_123_UNFREEZE_PRINCIPAL, +}; use num_traits::cast::ToPrimitive; use proptest::prelude::*; use proptest::sample::select; @@ -298,12 +302,22 @@ pub fn blocks_strategy( Operation::Mint { ref fee, .. } => fee.clone().is_none().then_some(arb_fee), Operation::FeeCollector { .. } | Operation::AuthorizedMint { .. } - | Operation::AuthorizedBurn { .. } => None, + | Operation::AuthorizedBurn { .. } + | Operation::FreezeAccount { .. } + | Operation::UnfreezeAccount { .. } + | Operation::FreezePrincipal { .. } + | Operation::UnfreezePrincipal { .. } => None, }; let btype = match transaction.operation { Operation::FeeCollector { .. } => Some(BTYPE_107.to_string()), Operation::AuthorizedMint { .. } => Some(BTYPE_122_MINT.to_string()), Operation::AuthorizedBurn { .. } => Some(BTYPE_122_BURN.to_string()), + Operation::FreezeAccount { .. } => Some(BTYPE_123_FREEZE_ACCOUNT.to_string()), + Operation::UnfreezeAccount { .. } => Some(BTYPE_123_UNFREEZE_ACCOUNT.to_string()), + Operation::FreezePrincipal { .. } => Some(BTYPE_123_FREEZE_PRINCIPAL.to_string()), + Operation::UnfreezePrincipal { .. } => { + Some(BTYPE_123_UNFREEZE_PRINCIPAL.to_string()) + } _ => None, }; @@ -672,6 +686,12 @@ impl TransactionsAndBalances { Operation::AuthorizedBurn { from, amount, .. } => { self.debit(from, amount.get_e8s()); } + Operation::FreezeAccount { .. } + | Operation::UnfreezeAccount { .. } + | Operation::FreezePrincipal { .. } + | Operation::UnfreezePrincipal { .. } => { + // Freeze/unfreeze operations do not affect balances + } }; self.transactions.push(tx); @@ -708,6 +728,12 @@ impl TransactionsAndBalances { Operation::AuthorizedBurn { from, .. } => { self.check_and_update_account_validity(*from, default_fee); } + Operation::FreezeAccount { .. } + | Operation::UnfreezeAccount { .. } + | Operation::FreezePrincipal { .. } + | Operation::UnfreezePrincipal { .. } => { + // Freeze/unfreeze operations do not affect balances or allowances + } } } diff --git a/rs/ledger_suite/icrc1/tests/tests.rs b/rs/ledger_suite/icrc1/tests/tests.rs index a0c8a7fa047f..183e9424800b 100644 --- a/rs/ledger_suite/icrc1/tests/tests.rs +++ b/rs/ledger_suite/icrc1/tests/tests.rs @@ -571,3 +571,253 @@ mod authorized_mint_burn_tests { assert!(validate_152_burn(&generic_block).is_err()); } } + +mod freeze_operation_tests { + use super::*; + use candid::Principal; + use ic_icrc1::Operation; + use ic_ledger_core::block::BlockType; + use icrc_ledger_types::icrc1::account::Account; + use icrc_ledger_types::icrc123::schema::{ + BTYPE_123_FREEZE_ACCOUNT, BTYPE_123_FREEZE_PRINCIPAL, BTYPE_123_UNFREEZE_ACCOUNT, + BTYPE_123_UNFREEZE_PRINCIPAL, + }; + + fn test_account(n: u64) -> Account { + Account { + owner: Principal::from_slice(&n.to_be_bytes()), + subaccount: None, + } + } + + fn test_principal(n: u64) -> Principal { + Principal::from_slice(&n.to_be_bytes()) + } + + fn make_freeze_account_block( + caller: Option, + mthd: Option, + reason: Option, + created_at_time: Option, + ) -> Block { + let transaction = Transaction { + operation: Operation::FreezeAccount { + account: test_account(1), + caller, + mthd, + reason, + }, + created_at_time, + memo: None, + }; + Block::from_transaction( + None, + transaction, + ic_ledger_core::timestamp::TimeStamp::from_nanos_since_unix_epoch(1_000_000_000), + U64::from(0_u64), + None, + ) + } + + fn make_unfreeze_account_block( + caller: Option, + mthd: Option, + reason: Option, + created_at_time: Option, + ) -> Block { + let transaction = Transaction { + operation: Operation::UnfreezeAccount { + account: test_account(1), + caller, + mthd, + reason, + }, + created_at_time, + memo: None, + }; + Block::from_transaction( + None, + transaction, + ic_ledger_core::timestamp::TimeStamp::from_nanos_since_unix_epoch(1_000_000_000), + U64::from(0_u64), + None, + ) + } + + fn make_freeze_principal_block( + caller: Option, + mthd: Option, + reason: Option, + created_at_time: Option, + ) -> Block { + let transaction = Transaction { + operation: Operation::FreezePrincipal { + principal: test_principal(2), + caller, + mthd, + reason, + }, + created_at_time, + memo: None, + }; + Block::from_transaction( + None, + transaction, + ic_ledger_core::timestamp::TimeStamp::from_nanos_since_unix_epoch(1_000_000_000), + U64::from(0_u64), + None, + ) + } + + fn make_unfreeze_principal_block( + caller: Option, + mthd: Option, + reason: Option, + created_at_time: Option, + ) -> Block { + let transaction = Transaction { + operation: Operation::UnfreezePrincipal { + principal: test_principal(2), + caller, + mthd, + reason, + }, + created_at_time, + memo: None, + }; + Block::from_transaction( + None, + transaction, + ic_ledger_core::timestamp::TimeStamp::from_nanos_since_unix_epoch(1_000_000_000), + U64::from(0_u64), + None, + ) + } + + // --- CBOR round-trip tests --- + + #[test] + fn test_freeze_account_cbor_round_trip() { + let block = make_freeze_account_block( + Some(Principal::anonymous()), + Some("153freeze_account".to_string()), + Some("test reason".to_string()), + Some(1_000_000_000), + ); + let encoded = block.clone().encode(); + let decoded = Block::::decode(encoded).unwrap(); + assert_eq!(block, decoded); + } + + #[test] + fn test_unfreeze_account_cbor_round_trip() { + let block = make_unfreeze_account_block( + Some(Principal::anonymous()), + Some("153unfreeze_account".to_string()), + None, + Some(1_000_000_000), + ); + let encoded = block.clone().encode(); + let decoded = Block::::decode(encoded).unwrap(); + assert_eq!(block, decoded); + } + + #[test] + fn test_freeze_principal_cbor_round_trip() { + let block = make_freeze_principal_block( + Some(Principal::anonymous()), + Some("153freeze_principal".to_string()), + Some("suspicious activity".to_string()), + Some(1_000_000_000), + ); + let encoded = block.clone().encode(); + let decoded = Block::::decode(encoded).unwrap(); + assert_eq!(block, decoded); + } + + #[test] + fn test_unfreeze_principal_cbor_round_trip() { + let block = make_unfreeze_principal_block( + Some(Principal::anonymous()), + Some("153unfreeze_principal".to_string()), + None, + Some(1_000_000_000), + ); + let encoded = block.clone().encode(); + let decoded = Block::::decode(encoded).unwrap(); + assert_eq!(block, decoded); + } + + // --- btype tests --- + + #[test] + fn test_freeze_account_btype_set_correctly() { + let block = make_freeze_account_block(None, None, None, None); + assert_eq!(block.btype.as_deref(), Some(BTYPE_123_FREEZE_ACCOUNT)); + } + + #[test] + fn test_unfreeze_account_btype_set_correctly() { + let block = make_unfreeze_account_block(None, None, None, None); + assert_eq!(block.btype.as_deref(), Some(BTYPE_123_UNFREEZE_ACCOUNT)); + } + + #[test] + fn test_freeze_principal_btype_set_correctly() { + let block = make_freeze_principal_block(None, None, None, None); + assert_eq!(block.btype.as_deref(), Some(BTYPE_123_FREEZE_PRINCIPAL)); + } + + #[test] + fn test_unfreeze_principal_btype_set_correctly() { + let block = make_unfreeze_principal_block(None, None, None, None); + assert_eq!(block.btype.as_deref(), Some(BTYPE_123_UNFREEZE_PRINCIPAL)); + } + + // --- FlattenedTransaction / generic block round-trip tests --- + + #[test] + fn test_freeze_account_generic_block_round_trip() { + let block = make_freeze_account_block( + Some(Principal::anonymous()), + Some("153freeze_account".to_string()), + None, + Some(1_000_000_000), + ); + let generic_block = encoded_block_to_generic_block(&block.clone().encode()); + let encoded_block = generic_block_to_encoded_block(generic_block.clone()).unwrap(); + let round_tripped = Block::::decode(encoded_block).unwrap(); + assert_eq!(block, round_tripped); + } + + #[test] + fn test_freeze_principal_generic_block_round_trip() { + let block = make_freeze_principal_block( + Some(Principal::anonymous()), + Some("153freeze_principal".to_string()), + Some("reason".to_string()), + Some(1_000_000_000), + ); + let generic_block = encoded_block_to_generic_block(&block.clone().encode()); + let encoded_block = generic_block_to_encoded_block(generic_block.clone()).unwrap(); + let round_tripped = Block::::decode(encoded_block).unwrap(); + assert_eq!(block, round_tripped); + } + + #[test] + fn test_freeze_account_hash_stability() { + let block = make_freeze_account_block( + Some(Principal::anonymous()), + Some("153freeze_account".to_string()), + None, + Some(1_000_000_000), + ); + let generic_block = encoded_block_to_generic_block(&block.clone().encode()); + assert_eq!( + generic_block.hash().to_vec(), + Block::::block_hash(&block.encode()) + .as_slice() + .to_vec() + ); + } +} diff --git a/rs/ledger_suite/test_utils/in_memory_ledger/src/lib.rs b/rs/ledger_suite/test_utils/in_memory_ledger/src/lib.rs index 7f8464add27a..b77b2a584393 100644 --- a/rs/ledger_suite/test_utils/in_memory_ledger/src/lib.rs +++ b/rs/ledger_suite/test_utils/in_memory_ledger/src/lib.rs @@ -547,6 +547,12 @@ where Operation::AuthorizedBurn { from, amount, .. } => { self.process_burn(from, &None, amount, index); } + Operation::FreezeAccount { .. } + | Operation::UnfreezeAccount { .. } + | Operation::FreezePrincipal { .. } + | Operation::UnfreezePrincipal { .. } => { + // Freeze/unfreeze operations do not affect balances + } } } self.post_process_ledger_blocks(blocks); diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations/mod.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations/mod.rs index 233c743c7ecf..cc9bd29dd679 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations/mod.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations/mod.rs @@ -495,6 +495,12 @@ pub fn update_account_balances( } => { current_fee_collector_107 = Some(fee_collector); } + crate::common::storage::types::IcrcOperation::FreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::UnfreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::FreezePrincipal { .. } + | crate::common::storage::types::IcrcOperation::UnfreezePrincipal { .. } => { + panic!("freeze/unfreeze not yet supported in Rosetta") + } } } @@ -671,6 +677,12 @@ pub fn store_blocks( None, None, ), + crate::common::storage::types::IcrcOperation::FreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::UnfreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::FreezePrincipal { .. } + | crate::common::storage::types::IcrcOperation::UnfreezePrincipal { .. } => { + panic!("freeze/unfreeze not yet supported in Rosetta") + } }; // SQLite doesn't support unsigned 64-bit integers. We need to convert the timestamps to signed diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 52cdd7d08141..981a46c2409b 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -9,6 +9,10 @@ use icrc_ledger_types::icrc::metadata_key::MetadataKey; use icrc_ledger_types::icrc3::blocks::GenericBlock; use icrc_ledger_types::icrc107::schema::BTYPE_107; use icrc_ledger_types::icrc122::schema::{BTYPE_122_BURN, BTYPE_122_MINT}; +use icrc_ledger_types::icrc123::schema::{ + BTYPE_123_FREEZE_ACCOUNT, BTYPE_123_FREEZE_PRINCIPAL, BTYPE_123_UNFREEZE_ACCOUNT, + BTYPE_123_UNFREEZE_PRINCIPAL, +}; use icrc_ledger_types::{ icrc::generic_value::Value, icrc1::{account::Account, transfer::Memo}, @@ -141,7 +145,11 @@ impl RosettaBlock { IcrcOperation::Burn { fee, .. } => fee, IcrcOperation::FeeCollector { .. } | IcrcOperation::AuthorizedMint { .. } - | IcrcOperation::AuthorizedBurn { .. } => None, + | IcrcOperation::AuthorizedBurn { .. } + | IcrcOperation::FreezeAccount { .. } + | IcrcOperation::UnfreezeAccount { .. } + | IcrcOperation::FreezePrincipal { .. } + | IcrcOperation::UnfreezePrincipal { .. } => None, })) } @@ -395,6 +403,30 @@ pub enum IcrcOperation { mthd: Option, reason: Option, }, + FreezeAccount { + account: Account, + caller: Option, + mthd: Option, + reason: Option, + }, + UnfreezeAccount { + account: Account, + caller: Option, + mthd: Option, + reason: Option, + }, + FreezePrincipal { + principal: Principal, + caller: Option, + mthd: Option, + reason: Option, + }, + UnfreezePrincipal { + principal: Principal, + caller: Option, + mthd: Option, + reason: Option, + }, } impl TryFrom<(Option, BTreeMap)> for IcrcOperation { @@ -502,6 +534,14 @@ impl TryFrom<(Option, BTreeMap)> for IcrcOperation { reason, }) } + found + if found == BTYPE_123_FREEZE_ACCOUNT + || found == BTYPE_123_UNFREEZE_ACCOUNT + || found == BTYPE_123_FREEZE_PRINCIPAL + || found == BTYPE_123_UNFREEZE_PRINCIPAL => + { + panic!("freeze/unfreeze not yet supported in Rosetta") + } found => { bail!( "Expected field 'op' to be 'burn', 'mint', 'xfer', 'approve', '{BTYPE_122_MINT}' or '{BTYPE_122_BURN}' but found {found}" @@ -639,6 +679,12 @@ impl From for BTreeMap { map.insert("reason".to_string(), Value::text(reason)); } } + Op::FreezeAccount { .. } + | Op::UnfreezeAccount { .. } + | Op::FreezePrincipal { .. } + | Op::UnfreezePrincipal { .. } => { + panic!("freeze/unfreeze not yet supported in Rosetta") + } } map } @@ -787,6 +833,12 @@ where mthd, reason, }, + Op::FreezeAccount { .. } + | Op::UnfreezeAccount { .. } + | Op::FreezePrincipal { .. } + | Op::UnfreezePrincipal { .. } => { + panic!("freeze/unfreeze not yet supported in Rosetta") + } } } } diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index b84b9c635c1c..547b1c02d8a3 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -813,6 +813,12 @@ pub fn icrc1_operation_to_rosetta_core_operations( ), )); } + crate::common::storage::types::IcrcOperation::FreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::UnfreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::FreezePrincipal { .. } + | crate::common::storage::types::IcrcOperation::UnfreezePrincipal { .. } => { + panic!("freeze/unfreeze not yet supported in Rosetta") + } }; Ok(operations) diff --git a/rs/rosetta-api/icrc1/src/construction_api/services.rs b/rs/rosetta-api/icrc1/src/construction_api/services.rs index 2c4a1c29c294..b1145847059c 100644 --- a/rs/rosetta-api/icrc1/src/construction_api/services.rs +++ b/rs/rosetta-api/icrc1/src/construction_api/services.rs @@ -548,7 +548,11 @@ mod tests { panic!("FeeCollector107 not implemented") } ic_icrc1::Operation::AuthorizedMint { .. } - | ic_icrc1::Operation::AuthorizedBurn { .. } => continue, + | ic_icrc1::Operation::AuthorizedBurn { .. } + | ic_icrc1::Operation::FreezeAccount { .. } + | ic_icrc1::Operation::UnfreezeAccount { .. } + | ic_icrc1::Operation::FreezePrincipal { .. } + | ic_icrc1::Operation::UnfreezePrincipal { .. } => continue, }; let args = match arg_with_caller.arg { LedgerEndpointArg::TransferArg(arg) => Encode!(&arg), diff --git a/rs/rosetta-api/icrc1/src/construction_api/utils.rs b/rs/rosetta-api/icrc1/src/construction_api/utils.rs index 6a3f27557522..d4ee27742950 100644 --- a/rs/rosetta-api/icrc1/src/construction_api/utils.rs +++ b/rs/rosetta-api/icrc1/src/construction_api/utils.rs @@ -281,6 +281,12 @@ pub fn build_icrc1_ledger_canister_method_args( crate::common::storage::types::IcrcOperation::AuthorizedBurn { .. } => { bail!("AuthorizedBurn operation not supported") } + crate::common::storage::types::IcrcOperation::FreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::UnfreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::FreezePrincipal { .. } + | crate::common::storage::types::IcrcOperation::UnfreezePrincipal { .. } => { + bail!("freeze/unfreeze operations not yet supported") + } } .context("Unable to encode canister method args") } @@ -316,6 +322,12 @@ fn extract_caller_principal_from_icrc1_ledger_operation( crate::common::storage::types::IcrcOperation::AuthorizedBurn { .. } => { bail!("AuthorizedBurn operation not supported") } + crate::common::storage::types::IcrcOperation::FreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::UnfreezeAccount { .. } + | crate::common::storage::types::IcrcOperation::FreezePrincipal { .. } + | crate::common::storage::types::IcrcOperation::UnfreezePrincipal { .. } => { + bail!("freeze/unfreeze operations not yet supported") + } }) } diff --git a/rs/rosetta-api/icrc1/src/data_api/services.rs b/rs/rosetta-api/icrc1/src/data_api/services.rs index 7c26ae259239..8da58fa8d00c 100644 --- a/rs/rosetta-api/icrc1/src/data_api/services.rs +++ b/rs/rosetta-api/icrc1/src/data_api/services.rs @@ -1238,6 +1238,12 @@ mod test { IcrcOperation::AuthorizedBurn { from, .. } => { Some(from.into()) } + IcrcOperation::FreezeAccount { account, .. } + | IcrcOperation::UnfreezeAccount { account, .. } => { + Some(account.into()) + } + IcrcOperation::FreezePrincipal { .. } + | IcrcOperation::UnfreezePrincipal { .. } => None, }; if search_transactions_request.account_identifier.is_some() { break; @@ -1312,6 +1318,10 @@ mod test { .try_into() .unwrap() } + IcrcOperation::FreezeAccount { .. } + | IcrcOperation::UnfreezeAccount { .. } + | IcrcOperation::FreezePrincipal { .. } + | IcrcOperation::UnfreezePrincipal { .. } => false, }) .count(); diff --git a/rs/rosetta-api/icrc1/tests/multitoken_system_tests.rs b/rs/rosetta-api/icrc1/tests/multitoken_system_tests.rs index 5aa2faaa0f15..515ac6b1602a 100644 --- a/rs/rosetta-api/icrc1/tests/multitoken_system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/multitoken_system_tests.rs @@ -1843,7 +1843,11 @@ fn test_construction_submit() { ic_icrc1::Operation::Burn { .. } => None, ic_icrc1::Operation::FeeCollector { .. } => None, ic_icrc1::Operation::AuthorizedMint { .. } - | ic_icrc1::Operation::AuthorizedBurn { .. } => None, + | ic_icrc1::Operation::AuthorizedBurn { .. } + | ic_icrc1::Operation::FreezeAccount { .. } + | ic_icrc1::Operation::UnfreezeAccount { .. } + | ic_icrc1::Operation::FreezePrincipal { .. } + | ic_icrc1::Operation::UnfreezePrincipal { .. } => None, }; if matches!( diff --git a/rs/rosetta-api/icrc1/tests/system_tests.rs b/rs/rosetta-api/icrc1/tests/system_tests.rs index 48fdccba3c1f..1c1bc4d5dc5c 100644 --- a/rs/rosetta-api/icrc1/tests/system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/system_tests.rs @@ -1539,7 +1539,11 @@ fn test_construction_submit() { ic_icrc1::Operation::Burn { .. } => None, ic_icrc1::Operation::FeeCollector { .. } => None, ic_icrc1::Operation::AuthorizedMint { .. } - | ic_icrc1::Operation::AuthorizedBurn { .. } => None, + | ic_icrc1::Operation::AuthorizedBurn { .. } + | ic_icrc1::Operation::FreezeAccount { .. } + | ic_icrc1::Operation::UnfreezeAccount { .. } + | ic_icrc1::Operation::FreezePrincipal { .. } + | ic_icrc1::Operation::UnfreezePrincipal { .. } => None, }; // Rosetta does not support mint and burn operations