From fc1e131bf3a6e4676855b9dc12f33b8058ee1e4d Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 24 Oct 2025 09:52:22 +0000 Subject: [PATCH 01/85] add fee collector operation --- .../src/icrc3/transactions.rs | 24 +++++++++ rs/ledger_suite/icrc1/src/endpoints.rs | 16 +++++- rs/ledger_suite/icrc1/src/lib.rs | 53 +++++++++++++++++++ rs/ledger_suite/icrc1/test_utils/src/lib.rs | 37 ++++++++----- .../test_utils/in_memory_ledger/src/lib.rs | 6 +++ 5 files changed, 123 insertions(+), 13 deletions(-) diff --git a/packages/icrc-ledger-types/src/icrc3/transactions.rs b/packages/icrc-ledger-types/src/icrc3/transactions.rs index f71945bce7aa..f01f0a718a7a 100644 --- a/packages/icrc-ledger-types/src/icrc3/transactions.rs +++ b/packages/icrc-ledger-types/src/icrc3/transactions.rs @@ -58,6 +58,13 @@ pub struct Approve { pub created_at_time: Option, } +#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct FeeCollector { + pub fee_collector: Option, + pub caller: Option, + pub created_at_time: Option, +} + // Representation of a Transaction which supports the Icrc1 Standard functionalities #[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct Transaction { @@ -66,6 +73,7 @@ pub struct Transaction { pub burn: Option, pub transfer: Option, pub approve: Option, + pub fee_collector: Option, pub timestamp: u64, } @@ -78,6 +86,7 @@ impl Transaction { burn: Some(burn), transfer: None, approve: None, + fee_collector: None, } } @@ -89,6 +98,7 @@ impl Transaction { burn: None, transfer: None, approve: None, + fee_collector: None, } } @@ -100,6 +110,7 @@ impl Transaction { burn: None, transfer: Some(transfer), approve: None, + fee_collector: None, } } @@ -111,6 +122,19 @@ impl Transaction { burn: None, transfer: None, approve: Some(approve), + fee_collector: None, + } + } + + pub fn fee_collector(fee_collector: FeeCollector, timestamp: u64) -> Self { + Self { + kind: "107feecol".into(), + timestamp, + mint: None, + burn: None, + transfer: None, + approve: None, + fee_collector: Some(fee_collector), } } } diff --git a/rs/ledger_suite/icrc1/src/endpoints.rs b/rs/ledger_suite/icrc1/src/endpoints.rs index 4930df6f1902..581898d3fdad 100644 --- a/rs/ledger_suite/icrc1/src/endpoints.rs +++ b/rs/ledger_suite/icrc1/src/endpoints.rs @@ -6,7 +6,9 @@ use ic_ledger_core::tokens::TokensType; use icrc_ledger_types::icrc1::transfer::TransferError; use icrc_ledger_types::icrc2::approve::ApproveError; use icrc_ledger_types::icrc2::transfer_from::TransferFromError; -use icrc_ledger_types::icrc3::transactions::{Approve, Burn, Mint, Transaction, Transfer}; +use icrc_ledger_types::icrc3::transactions::{ + Approve, Burn, FeeCollector, Mint, Transaction, Transfer, +}; use serde::Deserialize; pub fn convert_transfer_error( @@ -159,6 +161,7 @@ impl From> for Transaction { burn: None, transfer: None, approve: None, + fee_collector: None, timestamp: b.timestamp, }; let created_at_time = b.transaction.created_at_time; @@ -231,6 +234,17 @@ impl From> for Transaction { memo, }); } + Operation::FeeCollector { + fee_collector, + caller, + } => { + tx.kind = "107feecol".to_string(); + tx.fee_collector = Some(FeeCollector { + fee_collector, + caller, + created_at_time, + }); + } } tx diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index 7389b917586e..9afaeecff31b 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -80,6 +80,21 @@ pub enum Operation { #[serde(skip_serializing_if = "Option::is_none")] fee: Option, }, + #[serde(rename = "107feecol")] + FeeCollector { + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "compact_account::opt" + )] + fee_collector: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "compact_account::opt" + )] + caller: Option, + }, } // A [Transaction] but flattened meaning that [Operation] @@ -131,6 +146,16 @@ struct FlattenedTransaction { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] expires_at: Option, + + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "compact_account::opt")] + fee_collector: Option, + + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "compact_account::opt")] + caller: Option, } impl TryFrom> for Transaction { @@ -194,6 +219,7 @@ impl From> for FlattenedTransaction "mint", Transfer { .. } => "xfer", Approve { .. } => "approve", + FeeCollector { .. } => "107feecol", } .into(), from: match &t.operation { @@ -214,12 +240,14 @@ impl From> for FlattenedTransaction amount.clone(), + FeeCollector { .. } => Tokens::zero(), }, fee: match &t.operation { Transfer { fee, .. } | Approve { fee, .. } | Mint { fee, .. } | Burn { fee, .. } => fee.to_owned(), + FeeCollector { .. } => None, }, expected_allowance: match &t.operation { Approve { @@ -231,6 +259,14 @@ impl From> for FlattenedTransaction expires_at.to_owned(), _ => None, }, + fee_collector: match &t.operation { + FeeCollector { fee_collector, .. } => fee_collector.to_owned(), + _ => None, + }, + caller: match &t.operation { + FeeCollector { caller, .. } => caller.to_owned(), + _ => None, + }, } } } @@ -421,6 +457,12 @@ impl LedgerTransaction for Transaction { return Err(e); } } + Operation::FeeCollector { + fee_collector, + caller, + } => { + panic!("not implemented") + } } Ok(()) } @@ -554,6 +596,17 @@ impl TryFrom( Operation::Approve { ref fee, .. } => fee.clone().is_none().then_some(arb_fee), Operation::Burn { ref fee, .. } => fee.clone().is_none().then_some(arb_fee), Operation::Mint { ref fee, .. } => fee.clone().is_none().then_some(arb_fee), + Operation::FeeCollector { .. } => None, }; Block { @@ -539,19 +540,19 @@ impl TransactionsAndBalances { if let Some(spender_account) = spender { let used_allowance = amount.get_e8s() + fee; self.allowances.entry((from, spender_account)).and_modify( - |current_allowance| { - let current_amount = current_allowance.get_e8s(); - *current_allowance = Tokens::from_e8s( - current_amount - .checked_sub(used_allowance) - .unwrap_or_else(|| { - panic!( - "Allowance {current_amount} not enough to cover amount and fee {used_allowance} - from: {from}, to: {to}, spender: {spender_account}" - ) - }), + |current_allowance| { + let current_amount = current_allowance.get_e8s(); + *current_allowance = Tokens::from_e8s( + current_amount + .checked_sub(used_allowance) + .unwrap_or_else(|| { + panic!( + "Allowance {current_amount} not enough to cover amount and fee {used_allowance} - from: {from}, to: {to}, spender: {spender_account}" + ) + }), + ); + }, ); - }, - ); // Remove allowance entry if it's now zero if let Some(allowance) = self.allowances.get(&(from, spender_account)) @@ -577,6 +578,12 @@ impl TransactionsAndBalances { .or_insert(amount); self.debit(from, fee); } + Operation::FeeCollector { + fee_collector, + caller, + } => { + panic!("not implemented") + } }; self.transactions.push(tx); @@ -604,6 +611,12 @@ impl TransactionsAndBalances { // (allowance was added/modified for this account) self.check_and_update_account_validity(*from, default_fee); } + Operation::FeeCollector { + fee_collector, + caller, + } => { + panic!("not implemented") + } } } 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 f7a18a09f146..bf6532f4ddfc 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 @@ -538,6 +538,12 @@ where &fee.clone().or(block.effective_fee.clone()), TimeStamp::from_nanos_since_unix_epoch(block.timestamp), ), + Operation::FeeCollector { + fee_collector, + caller, + } => { + panic!("not implemented") + } } } self.post_process_ledger_blocks(blocks); From 9e7c58d08a5c36524785f0f94f965fb9279d5bd0 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 24 Oct 2025 15:31:23 +0000 Subject: [PATCH 02/85] index build fix --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 6984dbcece0d..8b404f3ad6ed 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -989,6 +989,9 @@ fn get_accounts(block: &Block) -> Vec { Operation::Mint { to, .. } => vec![to], Operation::Transfer { from, to, .. } => vec![from, to], Operation::Approve { from, .. } => vec![from], + Operation::FeeCollector { fee_collector, .. } => { + panic!("not implemented") + } } } From 8914b52b0c97e146536314af0402258e9edcdc05 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 27 Oct 2025 14:39:44 +0000 Subject: [PATCH 03/85] remove unused conversion --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 6 ++ rs/ledger_suite/icrc1/src/lib.rs | 102 --------------------- 2 files changed, 6 insertions(+), 102 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 8b404f3ad6ed..cc389ac8c0fd 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -955,6 +955,12 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { debit(block_index, from, fee); } + Operation::FeeCollector { + fee_collector, + caller, + } => { + panic!("not implemented") + } }, ); } diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index 9afaeecff31b..023b89fe393d 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -509,108 +509,6 @@ impl Transaction { } } -impl TryFrom - for Transaction -{ - type Error = String; - fn try_from( - value: icrc_ledger_types::icrc3::transactions::Transaction, - ) -> Result { - if let Some(mint) = value.mint { - let amount = Tokens::try_from(mint.amount) - .map_err(|_| "Could not convert Nat to Tokens".to_string())?; - let fee = match mint.fee { - Some(fee) => Some( - Tokens::try_from(fee) - .map_err(|_| "Could not convert Nat to Tokens".to_string())?, - ), - None => None, - }; - let operation = Operation::Mint { - to: mint.to, - amount, - fee, - }; - return Ok(Self { - operation, - created_at_time: mint.created_at_time, - memo: mint.memo, - }); - } - if let Some(burn) = value.burn { - let amount = Tokens::try_from(burn.amount) - .map_err(|_| "Could not convert Nat to Tokens".to_string())?; - let fee = match burn.fee { - Some(fee) => Some( - Tokens::try_from(fee) - .map_err(|_| "Could not convert Nat to Tokens".to_string())?, - ), - None => None, - }; - let operation = Operation::Burn { - from: burn.from, - spender: burn.spender, - amount, - fee, - }; - return Ok(Self { - operation, - created_at_time: burn.created_at_time, - memo: burn.memo, - }); - } - if let Some(transfer) = value.transfer { - let amount = Tokens::try_from(transfer.amount) - .map_err(|_| "Could not convert Nat to Tokens".to_string())?; - match transfer.fee { - Some(fee) => { - let fee = Tokens::try_from(fee) - .map_err(|_| "Could not convert Nat to Tokens".to_string())?; - - let operation = Operation::Transfer { - to: transfer.to, - amount, - from: transfer.from, - spender: transfer.spender, - fee: Some(fee), - }; - return Ok(Self { - operation, - created_at_time: transfer.created_at_time, - memo: transfer.memo, - }); - } - None => { - let operation = Operation::Transfer { - to: transfer.to, - amount, - from: transfer.from, - spender: transfer.spender, - fee: None, - }; - return Ok(Self { - operation, - created_at_time: transfer.created_at_time, - memo: transfer.memo, - }); - } - } - } - if let Some(fee_collector) = value.fee_collector { - let operation = Operation::FeeCollector { - fee_collector: fee_collector.fee_collector, - caller: fee_collector.caller, - }; - return Ok(Self { - operation: operation, - created_at_time: fee_collector.created_at_time, - memo: None, - }); - } - Err("Transaction has neither mint, burn nor transfer operation".to_owned()) - } -} - #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)] #[serde(bound = "")] pub struct Block { From 440545a185058105258c266e5346a2c011f5e302 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 27 Oct 2025 14:44:59 +0000 Subject: [PATCH 04/85] convert back from flattened --- rs/ledger_suite/icrc1/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index 023b89fe393d..0f9556805296 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -197,6 +197,10 @@ impl TryFrom> for Transaction Operation::FeeCollector { + fee_collector: value.fee_collector, + caller: value.caller, + }, unknown_op => return Err(format!("Unknown operation name {unknown_op}")), }; Ok(Transaction { From 5ed8d30d46232010a5bdb2127a17db3957fea4fa Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Tue, 28 Oct 2025 19:21:59 +0000 Subject: [PATCH 05/85] clippy --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 7 ++----- rs/ledger_suite/icrc1/src/lib.rs | 5 +---- rs/ledger_suite/icrc1/test_utils/src/lib.rs | 10 ++-------- rs/ledger_suite/test_utils/in_memory_ledger/src/lib.rs | 5 +---- 4 files changed, 6 insertions(+), 21 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index cc389ac8c0fd..484b5f04fb59 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -955,10 +955,7 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { debit(block_index, from, fee); } - Operation::FeeCollector { - fee_collector, - caller, - } => { + Operation::FeeCollector { .. } => { panic!("not implemented") } }, @@ -995,7 +992,7 @@ fn get_accounts(block: &Block) -> Vec { Operation::Mint { to, .. } => vec![to], Operation::Transfer { from, to, .. } => vec![from, to], Operation::Approve { from, .. } => vec![from], - Operation::FeeCollector { fee_collector, .. } => { + Operation::FeeCollector { .. } => { panic!("not implemented") } } diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index 0f9556805296..a84bb5876911 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -461,10 +461,7 @@ impl LedgerTransaction for Transaction { return Err(e); } } - Operation::FeeCollector { - fee_collector, - caller, - } => { + Operation::FeeCollector { .. } => { panic!("not implemented") } } diff --git a/rs/ledger_suite/icrc1/test_utils/src/lib.rs b/rs/ledger_suite/icrc1/test_utils/src/lib.rs index 616ddcec6c81..ffaa1cca4699 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/lib.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/lib.rs @@ -578,10 +578,7 @@ impl TransactionsAndBalances { .or_insert(amount); self.debit(from, fee); } - Operation::FeeCollector { - fee_collector, - caller, - } => { + Operation::FeeCollector { .. } => { panic!("not implemented") } }; @@ -611,10 +608,7 @@ impl TransactionsAndBalances { // (allowance was added/modified for this account) self.check_and_update_account_validity(*from, default_fee); } - Operation::FeeCollector { - fee_collector, - caller, - } => { + Operation::FeeCollector { .. } => { panic!("not implemented") } } 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 bf6532f4ddfc..94e8b9484454 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 @@ -538,10 +538,7 @@ where &fee.clone().or(block.effective_fee.clone()), TimeStamp::from_nanos_since_unix_epoch(block.timestamp), ), - Operation::FeeCollector { - fee_collector, - caller, - } => { + Operation::FeeCollector { .. } => { panic!("not implemented") } } From b1a7a4068fe4241624ce38fc4b5dc6d00d076291 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Tue, 28 Oct 2025 19:24:05 +0000 Subject: [PATCH 06/85] panic for fee col blocks --- rs/ledger_suite/icrc1/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index a84bb5876911..4634cf3289e8 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -588,6 +588,9 @@ impl BlockType for Block { let effective_fee = match &transaction.operation { Operation::Transfer { fee, .. } => fee.is_none().then_some(effective_fee), Operation::Approve { fee, .. } => fee.is_none().then_some(effective_fee), + Operation::FeeCollector { .. } => { + panic!("not implemented") + } _ => None, }; let (fee_collector, fee_collector_block_index) = match fee_collector { From 372c508b09d5cf5acc5cc7cddb7402a42018a97d Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Wed, 29 Oct 2025 09:38:27 +0000 Subject: [PATCH 07/85] build fix --- rs/nervous_system/initial_supply/src/tests.rs | 1 + rs/sns/governance/token_valuation/src/tests.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/rs/nervous_system/initial_supply/src/tests.rs b/rs/nervous_system/initial_supply/src/tests.rs index baa9a761bc00..94274e15c173 100644 --- a/rs/nervous_system/initial_supply/src/tests.rs +++ b/rs/nervous_system/initial_supply/src/tests.rs @@ -76,6 +76,7 @@ async fn test_initial_supply() { burn: None, approve: None, transfer: None, + fee_collector: None, } }; diff --git a/rs/sns/governance/token_valuation/src/tests.rs b/rs/sns/governance/token_valuation/src/tests.rs index 5b4e5875cce4..09eb9eada405 100644 --- a/rs/sns/governance/token_valuation/src/tests.rs +++ b/rs/sns/governance/token_valuation/src/tests.rs @@ -163,6 +163,7 @@ async fn test_icps_per_sns_token_client() { burn: None, approve: None, transfer: None, + fee_collector: None, }, ], From 3dcf6a60c5f508c51b7ad48eb7a9833cda25316c Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Wed, 29 Oct 2025 14:40:09 +0000 Subject: [PATCH 08/85] build fix --- rs/rosetta-api/icrc1/src/common/storage/types.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 7f473f9828fd..b3d9ed714a5e 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -608,6 +608,9 @@ where amount: amount.into(), fee: fee.map(Into::into), }, + Op::FeeCollector { .. } => { + panic!("not implemented") + } } } } From d010fbdb011f31e65a40af3b68d95d7ec32a95b4 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Wed, 29 Oct 2025 14:58:57 +0000 Subject: [PATCH 09/85] revert unintentional change --- rs/ledger_suite/icrc1/test_utils/src/lib.rs | 24 ++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/rs/ledger_suite/icrc1/test_utils/src/lib.rs b/rs/ledger_suite/icrc1/test_utils/src/lib.rs index ffaa1cca4699..4643d55ff108 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/lib.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/lib.rs @@ -540,19 +540,19 @@ impl TransactionsAndBalances { if let Some(spender_account) = spender { let used_allowance = amount.get_e8s() + fee; self.allowances.entry((from, spender_account)).and_modify( - |current_allowance| { - let current_amount = current_allowance.get_e8s(); - *current_allowance = Tokens::from_e8s( - current_amount - .checked_sub(used_allowance) - .unwrap_or_else(|| { - panic!( - "Allowance {current_amount} not enough to cover amount and fee {used_allowance} - from: {from}, to: {to}, spender: {spender_account}" - ) - }), - ); - }, + |current_allowance| { + let current_amount = current_allowance.get_e8s(); + *current_allowance = Tokens::from_e8s( + current_amount + .checked_sub(used_allowance) + .unwrap_or_else(|| { + panic!( + "Allowance {current_amount} not enough to cover amount and fee {used_allowance} - from: {from}, to: {to}, spender: {spender_account}" + ) + }), ); + }, + ); // Remove allowance entry if it's now zero if let Some(allowance) = self.allowances.get(&(from, spender_account)) From 42072ed7d75b4ab90f26a11f351cb0023c059eef Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 30 Oct 2025 10:30:30 +0000 Subject: [PATCH 10/85] build fix --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 4 +--- rs/ledger_suite/icrc1/index-ng/tests/tests.rs | 1 + rs/rosetta-api/icrc1/src/construction_api/services.rs | 3 +++ rs/rosetta-api/icrc1/tests/multitoken_system_tests.rs | 1 + rs/rosetta-api/icrc1/tests/system_tests.rs | 1 + 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 484b5f04fb59..d31d0b51e7d8 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -992,9 +992,7 @@ fn get_accounts(block: &Block) -> Vec { Operation::Mint { to, .. } => vec![to], Operation::Transfer { from, to, .. } => vec![from, to], Operation::Approve { from, .. } => vec![from], - Operation::FeeCollector { .. } => { - panic!("not implemented") - } + Operation::FeeCollector { .. } => vec![], } } diff --git a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs index 2c197e22aa77..f603ced774da 100644 --- a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs +++ b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs @@ -889,6 +889,7 @@ fn test_get_account_transactions_pagination() { transfer: None, approve: None, timestamp: 0, + fee_collector: None, }, transaction, ); diff --git a/rs/rosetta-api/icrc1/src/construction_api/services.rs b/rs/rosetta-api/icrc1/src/construction_api/services.rs index 297742f6d2b0..969c60c88854 100644 --- a/rs/rosetta-api/icrc1/src/construction_api/services.rs +++ b/rs/rosetta-api/icrc1/src/construction_api/services.rs @@ -544,6 +544,9 @@ mod tests { ic_icrc1::Operation::Approve { .. } => CanisterMethodName::Icrc2Approve, ic_icrc1::Operation::Mint { .. } => CanisterMethodName::Icrc1Transfer, ic_icrc1::Operation::Burn { .. } => CanisterMethodName::Icrc1Transfer, + ic_icrc1::Operation::FeeCollector { .. } => { + panic!("not implemented") + } }; let args = match arg_with_caller.arg { LedgerEndpointArg::TransferArg(arg) => Encode!(&arg), diff --git a/rs/rosetta-api/icrc1/tests/multitoken_system_tests.rs b/rs/rosetta-api/icrc1/tests/multitoken_system_tests.rs index 36f6e2e3369b..22f2bf570f8c 100644 --- a/rs/rosetta-api/icrc1/tests/multitoken_system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/multitoken_system_tests.rs @@ -1839,6 +1839,7 @@ fn test_construction_submit() { ic_icrc1::Operation::Approve { fee, .. } => fee, ic_icrc1::Operation::Mint { .. } => None, ic_icrc1::Operation::Burn { .. } => None, + ic_icrc1::Operation::FeeCollector { .. } => None, }; if matches!( diff --git a/rs/rosetta-api/icrc1/tests/system_tests.rs b/rs/rosetta-api/icrc1/tests/system_tests.rs index 65f43e55de03..51ea41998273 100644 --- a/rs/rosetta-api/icrc1/tests/system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/system_tests.rs @@ -1321,6 +1321,7 @@ fn test_construction_submit() { ic_icrc1::Operation::Approve { fee, .. } => fee, ic_icrc1::Operation::Mint { .. } => None, ic_icrc1::Operation::Burn { .. } => None, + ic_icrc1::Operation::FeeCollector { .. } => None, }; // Rosetta does not support mint and burn operations From 03980119be9d0fd6e01df5f2013068a974bab0bb Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 30 Oct 2025 11:31:31 +0000 Subject: [PATCH 11/85] update did files --- rs/ledger_suite/icrc1/archive/archive.did | 7 +++++++ rs/ledger_suite/icrc1/index-ng/index-ng.did | 7 +++++++ rs/ledger_suite/icrc1/ledger/ledger.did | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/rs/ledger_suite/icrc1/archive/archive.did b/rs/ledger_suite/icrc1/archive/archive.did index 4eb4323600db..fffc257829cf 100644 --- a/rs/ledger_suite/icrc1/archive/archive.did +++ b/rs/ledger_suite/icrc1/archive/archive.did @@ -6,6 +6,7 @@ type Transaction = record { kind : text; mint : opt Mint; approve : opt Approve; + fee_collector : opt FeeCollector; timestamp : nat64; transfer : opt Transfer }; @@ -21,6 +22,12 @@ type Approve = record { spender : Account }; +type FeeCollector = record { + caller : opt Account; + fee_collector : opt Account; + created_at_time : opt nat64 +}; + type Burn = record { from : Account; memo : opt vec nat8; diff --git a/rs/ledger_suite/icrc1/index-ng/index-ng.did b/rs/ledger_suite/icrc1/index-ng/index-ng.did index 0f2351be1f92..91d629b99a15 100644 --- a/rs/ledger_suite/icrc1/index-ng/index-ng.did +++ b/rs/ledger_suite/icrc1/index-ng/index-ng.did @@ -56,10 +56,17 @@ type Transaction = record { kind : text; mint : opt Mint; approve : opt Approve; + fee_collector : opt FeeCollector; timestamp : nat64; transfer : opt Transfer }; +type FeeCollector = record { + caller : opt Account; + fee_collector : opt Account; + created_at_time : opt nat64 +}; + type Approve = record { fee : opt Tokens; from : Account; diff --git a/rs/ledger_suite/icrc1/ledger/ledger.did b/rs/ledger_suite/icrc1/ledger/ledger.did index b093ec9bfada..d9376f97bbb0 100644 --- a/rs/ledger_suite/icrc1/ledger/ledger.did +++ b/rs/ledger_suite/icrc1/ledger/ledger.did @@ -224,10 +224,17 @@ type Transaction = record { kind : text; mint : opt Mint; approve : opt Approve; + fee_collector : opt FeeCollector; timestamp : Timestamp; transfer : opt Transfer }; +type FeeCollector = record { + caller : opt Account; + fee_collector : opt Account; + created_at_time : opt nat64 +}; + type Burn = record { from : Account; memo : opt blob; From a5b886cdf4371e008150020585b850b10bdc45c6 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 31 Oct 2025 20:31:36 +0000 Subject: [PATCH 12/85] handle new fee collector in index --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 31 ++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index d31d0b51e7d8..a4dd93d9b7b7 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -140,6 +140,8 @@ struct State { /// index. Lower values will result in a more responsive UI, but higher costs due to increased /// cycle burn for the index, ledger and archive(s). retrieve_blocks_from_ledger_interval: Option, + + fee_collector_107: Option>, } impl State { @@ -161,6 +163,7 @@ impl Default for State { fee_collectors: Default::default(), last_fee: None, retrieve_blocks_from_ledger_interval: None, + fee_collector_107: None, } } } @@ -955,8 +958,11 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { debit(block_index, from, fee); } - Operation::FeeCollector { .. } => { - panic!("not implemented") + Operation::FeeCollector { + fee_collector, + caller: _, + } => { + mutate_state(|s| s.fee_collector_107 = Some(fee_collector)); } }, ); @@ -997,6 +1003,27 @@ fn get_accounts(block: &Block) -> Vec { } fn get_fee_collector(block_index: BlockIndex64, block: &Block) -> Option { + let fee_collector_107 = match block.transaction.operation { + Operation::FeeCollector { + fee_collector, + caller: _, + } => Some(fee_collector), + _ => None, + }; + match fee_collector_107 { + Some(fee_collector) => fee_collector, + None => match get_fee_collector_from_state() { + Some(fee_collector) => fee_collector, + None => get_legacy_fee_collector(block_index, block), + }, + } +} + +fn get_fee_collector_from_state() -> Option> { + with_state(|s| s.fee_collector_107) +} + +fn get_legacy_fee_collector(block_index: BlockIndex64, block: &Block) -> Option { if block.fee_collector.is_some() { block.fee_collector } else if let Some(fee_collector_block_index) = block.fee_collector_block_index { From 2fe87b4e0c1cf0729670dfb633228befa4c683ac Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 31 Oct 2025 20:34:36 +0000 Subject: [PATCH 13/85] add fee collector to approve --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index a4dd93d9b7b7..2c524b32fbe9 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -957,6 +957,12 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { change_balance(spender, |balance| balance); debit(block_index, from, fee); + + if let Some(fee_collector_107) = get_fee_collector_107() { + if let Some(fee_collector) = fee_collector_107 { + credit(block_index, fee_collector, fee); + } + } } Operation::FeeCollector { fee_collector, @@ -1012,14 +1018,14 @@ fn get_fee_collector(block_index: BlockIndex64, block: &Block) -> Option }; match fee_collector_107 { Some(fee_collector) => fee_collector, - None => match get_fee_collector_from_state() { + None => match get_fee_collector_107() { Some(fee_collector) => fee_collector, None => get_legacy_fee_collector(block_index, block), }, } } -fn get_fee_collector_from_state() -> Option> { +fn get_fee_collector_107() -> Option> { with_state(|s| s.fee_collector_107) } From 5d5e118de0f33b5c6198551859e689e7f3845045 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 31 Oct 2025 20:48:13 +0000 Subject: [PATCH 14/85] clippy --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 2c524b32fbe9..169e8c90f51c 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -958,10 +958,10 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { debit(block_index, from, fee); - if let Some(fee_collector_107) = get_fee_collector_107() { - if let Some(fee_collector) = fee_collector_107 { - credit(block_index, fee_collector, fee); - } + if let Some(fee_collector_107) = get_fee_collector_107() + && let Some(fee_collector) = fee_collector_107 + { + credit(block_index, fee_collector, fee); } } Operation::FeeCollector { From 8102274eaabc71baa3e409920b16ece6b9c8e289 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Tue, 11 Nov 2025 18:59:55 +0000 Subject: [PATCH 15/85] refactor get_fee_collector --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 59 +++++++++++++--------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 169e8c90f51c..5646997a4319 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -786,8 +786,11 @@ fn append_block(block_index: BlockIndex64, block: GenericBlock) -> Result<(), Sy } }); + // change the fee collector if block is a 107 block + process_fee_collector_block(&decoded_block); + // add the block to the fee_collector if one is set - index_fee_collector(block_index, &decoded_block); + index_fee_collector(block_index); // change the balance of the involved accounts process_balance_changes(block_index, &decoded_block); @@ -831,8 +834,20 @@ fn append_icrc3_blocks(new_blocks: Vec) -> Result<(), SyncError> { Ok(()) } -fn index_fee_collector(block_index: BlockIndex64, block: &Block) { - if let Some(fee_collector) = get_fee_collector(block_index, block) { +fn process_fee_collector_block(block: &Block) { + match block.transaction.operation { + Operation::FeeCollector { + fee_collector, + caller: _, + } => { + mutate_state(|s| s.fee_collector_107 = Some(fee_collector)); + } + _ => {} + } +} + +fn index_fee_collector(block_index: BlockIndex64) { + if let Some(fee_collector) = get_fee_collector() { mutate_state(|s| { s.fee_collectors .entry(fee_collector) @@ -878,7 +893,7 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { )) }); mutate_state(|s| s.last_fee = Some(fee)); - if let Some(fee_collector) = get_fee_collector(block_index, block) { + if let Some(fee_collector) = get_fee_collector() { credit(block_index, fee_collector, fee); } } @@ -894,7 +909,7 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { )) }); mutate_state(|s| s.last_fee = Some(fee)); - if let Some(fee_collector) = get_fee_collector(block_index, block) { + if let Some(fee_collector) = get_fee_collector() { credit(block_index, fee_collector, fee); } } @@ -923,7 +938,7 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { }), ); credit(block_index, to, amount); - if let Some(fee_collector) = get_fee_collector(block_index, block) { + if let Some(fee_collector) = get_fee_collector() { credit(block_index, fee_collector, fee); } } @@ -965,10 +980,10 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { } } Operation::FeeCollector { - fee_collector, + fee_collector: _, caller: _, } => { - mutate_state(|s| s.fee_collector_107 = Some(fee_collector)); + // Does not affect the balance } }, ); @@ -1008,20 +1023,10 @@ fn get_accounts(block: &Block) -> Vec { } } -fn get_fee_collector(block_index: BlockIndex64, block: &Block) -> Option { - let fee_collector_107 = match block.transaction.operation { - Operation::FeeCollector { - fee_collector, - caller: _, - } => Some(fee_collector), - _ => None, - }; - match fee_collector_107 { +fn get_fee_collector() -> Option { + match get_fee_collector_107() { Some(fee_collector) => fee_collector, - None => match get_fee_collector_107() { - Some(fee_collector) => fee_collector, - None => get_legacy_fee_collector(block_index, block), - }, + None => get_legacy_fee_collector(), } } @@ -1029,16 +1034,22 @@ fn get_fee_collector_107() -> Option> { with_state(|s| s.fee_collector_107) } -fn get_legacy_fee_collector(block_index: BlockIndex64, block: &Block) -> Option { +fn get_legacy_fee_collector() -> Option { + let chain_length = with_blocks(|blocks| blocks.len()); + let last_block_index = chain_length - 1; + let block = match get_decoded_block(last_block_index) { + Some(block) => block, + None => return None, + }; if block.fee_collector.is_some() { block.fee_collector } else if let Some(fee_collector_block_index) = block.fee_collector_block_index { let block = get_decoded_block(fee_collector_block_index) .unwrap_or_else(|| - ic_cdk::trap(format!("Block at index {block_index} has fee_collector_block_index {fee_collector_block_index} but there is no block at that index"))); + ic_cdk::trap(format!("Block at index {last_block_index} has fee_collector_block_index {fee_collector_block_index} but there is no block at that index"))); if block.fee_collector.is_none() { ic_cdk::trap(format!( - "Block at index {block_index} has fee_collector_block_index {fee_collector_block_index} but that block has no fee_collector set" + "Block at index {last_block_index} has fee_collector_block_index {fee_collector_block_index} but that block has no fee_collector set" )) } else { block.fee_collector From cbe515638b375add990365759ff7014a9f27950f Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Tue, 11 Nov 2025 22:15:13 +0000 Subject: [PATCH 16/85] clippy --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 23 +++++++++++----------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 5646997a4319..ca5083c900e7 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -835,14 +835,12 @@ fn append_icrc3_blocks(new_blocks: Vec) -> Result<(), SyncError> { } fn process_fee_collector_block(block: &Block) { - match block.transaction.operation { - Operation::FeeCollector { - fee_collector, - caller: _, - } => { - mutate_state(|s| s.fee_collector_107 = Some(fee_collector)); - } - _ => {} + if let Operation::FeeCollector { + fee_collector, + caller: _, + } = block.transaction.operation + { + mutate_state(|s| s.fee_collector_107 = Some(fee_collector)); } } @@ -1036,11 +1034,12 @@ fn get_fee_collector_107() -> Option> { fn get_legacy_fee_collector() -> Option { let chain_length = with_blocks(|blocks| blocks.len()); + if chain_length == 0 { + return None; + } let last_block_index = chain_length - 1; - let block = match get_decoded_block(last_block_index) { - Some(block) => block, - None => return None, - }; + let block = get_decoded_block(last_block_index) + .expect("chain_length is positive, should have at least one block"); if block.fee_collector.is_some() { block.fee_collector } else if let Some(fee_collector_block_index) = block.fee_collector_block_index { From b5530ec47b6665c84ce650699e04a3566c2e6a9f Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Wed, 12 Nov 2025 00:01:08 +0000 Subject: [PATCH 17/85] change op name to 107set_fee_collector --- packages/icrc-ledger-types/src/icrc3/transactions.rs | 2 +- rs/ledger_suite/icrc1/src/endpoints.rs | 2 +- rs/ledger_suite/icrc1/src/lib.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/icrc-ledger-types/src/icrc3/transactions.rs b/packages/icrc-ledger-types/src/icrc3/transactions.rs index f01f0a718a7a..cf3c41bd50fc 100644 --- a/packages/icrc-ledger-types/src/icrc3/transactions.rs +++ b/packages/icrc-ledger-types/src/icrc3/transactions.rs @@ -128,7 +128,7 @@ impl Transaction { pub fn fee_collector(fee_collector: FeeCollector, timestamp: u64) -> Self { Self { - kind: "107feecol".into(), + kind: "107set_fee_collector".into(), timestamp, mint: None, burn: None, diff --git a/rs/ledger_suite/icrc1/src/endpoints.rs b/rs/ledger_suite/icrc1/src/endpoints.rs index 581898d3fdad..d1169c7fa65b 100644 --- a/rs/ledger_suite/icrc1/src/endpoints.rs +++ b/rs/ledger_suite/icrc1/src/endpoints.rs @@ -238,7 +238,7 @@ impl From> for Transaction { fee_collector, caller, } => { - tx.kind = "107feecol".to_string(); + tx.kind = "107set_fee_collector".to_string(); tx.fee_collector = Some(FeeCollector { fee_collector, caller, diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index 4634cf3289e8..9c8549704d40 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -80,7 +80,7 @@ pub enum Operation { #[serde(skip_serializing_if = "Option::is_none")] fee: Option, }, - #[serde(rename = "107feecol")] + #[serde(rename = "107set_fee_collector")] FeeCollector { #[serde( default, @@ -197,7 +197,7 @@ impl TryFrom> for Transaction Operation::FeeCollector { + "107set_fee_collector" => Operation::FeeCollector { fee_collector: value.fee_collector, caller: value.caller, }, @@ -223,7 +223,7 @@ impl From> for FlattenedTransaction "mint", Transfer { .. } => "xfer", Approve { .. } => "approve", - FeeCollector { .. } => "107feecol", + FeeCollector { .. } => "107set_fee_collector", } .into(), from: match &t.operation { From fd4dab5f9bb497bd3cd120b9dd425aa697887814 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Wed, 12 Nov 2025 20:26:30 +0000 Subject: [PATCH 18/85] add fee collector block generation to the test block builder --- .../src/icrc/generic_value.rs | 15 +++++ rs/ledger_suite/icrc1/test_utils/src/icrc3.rs | 62 ++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/packages/icrc-ledger-types/src/icrc/generic_value.rs b/packages/icrc-ledger-types/src/icrc/generic_value.rs index c578796875dd..752d81230039 100644 --- a/packages/icrc-ledger-types/src/icrc/generic_value.rs +++ b/packages/icrc-ledger-types/src/icrc/generic_value.rs @@ -334,6 +334,21 @@ impl From for Value { } } +impl TryFrom for Principal { + type Error = String; + + fn try_from(value: Value) -> Result { + Principal::try_from_slice(value.as_blob()?.as_slice()) + .map_err(|err| format!("Unable to decode the principal, error {err}")) + } +} + +impl From for Value { + fn from(principal: Principal) -> Self { + Self::blob(principal.as_slice()) + } +} + impl std::fmt::Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs b/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs index fe1e31a92d38..794ea106eb16 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs @@ -1,6 +1,6 @@ -use candid::Nat; +use candid::{Nat, Principal}; use ic_ledger_core::tokens::TokensType; -use icrc_ledger_types::icrc::generic_value::ICRC3Value; +use icrc_ledger_types::icrc::generic_value::{ICRC3Value, Value}; use icrc_ledger_types::icrc1::account::Account; use serde_bytes::ByteBuf; use std::collections::BTreeMap; @@ -21,6 +21,7 @@ pub struct BlockBuilder { fee_collector_block: Option, fee: Option, parent_hash: Option>, + btype: Option, } impl BlockBuilder { @@ -33,6 +34,7 @@ impl BlockBuilder { fee_collector_block: None, fee: None, parent_hash: None, + btype: None, } } @@ -60,6 +62,12 @@ impl BlockBuilder { self } + /// Set the block type + pub fn with_btype(mut self, btype: String) -> Self { + self.btype = Some(btype); + self + } + /// Create a transfer operation pub fn transfer(self, from: Account, to: Account, amount: Tokens) -> TransferBuilder { TransferBuilder { @@ -107,6 +115,21 @@ impl BlockBuilder { } } + /// Create an approve operation + pub fn fee_collector( + self, + fee_collector: Option, + caller: Option, + ts: Option, + ) -> FeeCollectorBuilder { + FeeCollectorBuilder { + builder: self, + fee_collector, + caller, + ts, + } + } + /// Build the final ICRC3Value block fn build_with_operation( self, @@ -156,6 +179,11 @@ impl BlockBuilder { ); } + // Add fee collector block if specified + if let Some(btype) = self.btype { + block_map.insert("btype".to_string(), ICRC3Value::Text(btype)); + } + ICRC3Value::Map(block_map) } } @@ -289,6 +317,36 @@ impl ApproveBuilder { } } +/// Builder for fee collector operations +pub struct FeeCollectorBuilder { + builder: BlockBuilder, + fee_collector: Option, + caller: Option, + ts: Option, +} + +impl FeeCollectorBuilder { + /// Build the mint block + pub fn build(self) -> ICRC3Value { + let mut tx_fields = BTreeMap::new(); + match &self.fee_collector { + Some(fee_collector) => tx_fields.insert( + "fee_collector".to_string(), + account_to_icrc3_value(fee_collector), + ), + None => tx_fields.insert("fee_collector".to_string(), ICRC3Value::Array(vec![])), + }; + if let Some(caller) = &self.caller { + tx_fields.insert("caller".to_string(), ICRC3Value::from(Value::from(*caller))); + } + if let Some(ts) = self.ts { + tx_fields.insert("ts".to_string(), ICRC3Value::Nat(Nat::from(ts))); + } + self.builder + .build_with_operation("107set_fee_collector", tx_fields) + } +} + #[cfg(test)] mod builder_tests { use super::*; From 08aba671ada7efbdf5f64eab434889034e7e2006 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 00:03:59 +0000 Subject: [PATCH 19/85] fix block conversion, caller doesn't work still --- rs/ledger_suite/icrc1/BUILD.bazel | 1 + rs/ledger_suite/icrc1/src/lib.rs | 29 ++++++++++++++----- rs/ledger_suite/icrc1/test_utils/src/lib.rs | 3 ++ rs/ledger_suite/icrc1/tests/tests.rs | 31 +++++++++++++++++++++ rs/ledger_suite/tests/sm-tests/src/lib.rs | 1 + 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/rs/ledger_suite/icrc1/BUILD.bazel b/rs/ledger_suite/icrc1/BUILD.bazel index 4e3a84b71017..70d5926e1c87 100644 --- a/rs/ledger_suite/icrc1/BUILD.bazel +++ b/rs/ledger_suite/icrc1/BUILD.bazel @@ -75,6 +75,7 @@ rust_test( "//packages/ic-ledger-hash-of:ic_ledger_hash_of", "//rs/ledger_suite/icrc1/tokens_u256", "//rs/ledger_suite/icrc1/tokens_u64", + "//rs/types/base_types", ] + DEV_DEPENDENCIES, ) diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index 9c8549704d40..cd261be8040c 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -132,8 +132,10 @@ struct FlattenedTransaction { #[serde(with = "compact_account::opt")] spender: Option, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "amt")] - amount: Tokens, + amount: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -167,13 +169,17 @@ impl TryFrom> for Transaction Operation::Mint { to: value.to.ok_or("`to` field required for `mint` operation")?, - amount: value.amount.clone(), + amount: value + .amount + .ok_or("`amount` required for `mint` operations")?, fee: value.fee, }, "xfer" => Operation::Transfer { @@ -182,7 +188,9 @@ impl TryFrom> for Transaction Operation::Approve { @@ -192,7 +200,9 @@ impl TryFrom> for Transaction From> for FlattenedTransaction amount.clone(), - FeeCollector { .. } => Tokens::zero(), + | Approve { amount, .. } => Some(amount.clone()), + FeeCollector { .. } => None, }, fee: match &t.operation { Transfer { fee, .. } @@ -535,6 +545,10 @@ pub struct Block { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "fee_col_block")] pub fee_collector_block_index: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "btype")] + pub btype: Option, } type TaggedBlock = Required, 55799>; @@ -608,6 +622,7 @@ impl BlockType for Block { timestamp: timestamp.as_nanos_since_unix_epoch(), fee_collector, fee_collector_block_index, + btype: None, } } } diff --git a/rs/ledger_suite/icrc1/test_utils/src/lib.rs b/rs/ledger_suite/icrc1/test_utils/src/lib.rs index 4643d55ff108..54a0ee2a741b 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/lib.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/lib.rs @@ -235,6 +235,7 @@ pub fn blocks_strategy( timestamp, fee_collector, fee_collector_block_index: None, + btype: None, } .encode(), )), @@ -243,6 +244,7 @@ pub fn blocks_strategy( timestamp, fee_collector, fee_collector_block_index: None, + btype: None, } }) } @@ -1499,6 +1501,7 @@ where timestamp: ts, fee_collector: fee_col, fee_collector_block_index: fee_col_block, + btype: None, }, ) } diff --git a/rs/ledger_suite/icrc1/tests/tests.rs b/rs/ledger_suite/icrc1/tests/tests.rs index ec4c8e6572ad..252326f6c508 100644 --- a/rs/ledger_suite/icrc1/tests/tests.rs +++ b/rs/ledger_suite/icrc1/tests/tests.rs @@ -1,8 +1,10 @@ +use ic_base_types::PrincipalId; use ic_icrc1::blocks::{ encoded_block_to_generic_block, generic_block_to_encoded_block, generic_transaction_from_generic_block, }; use ic_icrc1::{Block, Transaction, hash}; +use ic_icrc1_test_utils::icrc3::BlockBuilder; use ic_icrc1_test_utils::{arb_amount, arb_block, arb_small_amount, blocks_strategy}; use ic_icrc1_tokens_u64::U64; use ic_icrc1_tokens_u256::U256; @@ -11,6 +13,7 @@ use ic_ledger_core::Tokens; use ic_ledger_core::block::BlockType; use ic_ledger_core::tokens::TokensType; use ic_ledger_hash_of::HashOf; +use icrc_ledger_types::icrc1::account::Account; use proptest::prelude::*; fn arb_u256() -> impl Strategy { @@ -170,3 +173,31 @@ fn test_encoding_decoding_block_u256( fn arb_token_u256() -> impl Strategy { (any::(), any::()).prop_map(|(hi, lo)| U256::from_words(hi, lo)) } + +#[test] +fn test_fee_collector_block() { + const TEST_USER: PrincipalId = PrincipalId::new_user_test_id(1); + const TEST_ACCOUNT: Account = Account { + owner: TEST_USER.0, + subaccount: Some([1u8; 32]), + }; + + let original_block = BlockBuilder::::new(1, 111) + .with_btype("107feecol".to_string()) + .with_parent_hash(vec![1u8; 32]) + .fee_collector(Some(TEST_ACCOUNT), None, Some(123)) + .build(); + + let block = generic_block_to_encoded_block(original_block.clone().into()) + .expect("failed to decode generic block"); + + let decoded_block = + Block::::decode(block.clone()).expect("failed to decode encoded block"); + + let decoded_value = encoded_block_to_generic_block(&decoded_block.clone().encode()); + + println!("original: {}", original_block); + println!("decoded: {}", decoded_value); + + assert_eq!(original_block.clone().hash(), decoded_value.hash()); +} diff --git a/rs/ledger_suite/tests/sm-tests/src/lib.rs b/rs/ledger_suite/tests/sm-tests/src/lib.rs index 8a9a33b9fb26..3a747f93a0a7 100644 --- a/rs/ledger_suite/tests/sm-tests/src/lib.rs +++ b/rs/ledger_suite/tests/sm-tests/src/lib.rs @@ -343,6 +343,7 @@ fn arb_block() -> impl Strategy> { timestamp: ts, fee_collector: fee_col, fee_collector_block_index: fee_col_block, + btype: None, }, ) } From eba90cd8329ef6aeaa84f315939f488bd55c058d Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 00:09:25 +0000 Subject: [PATCH 20/85] change caller to principal --- packages/icrc-ledger-types/src/icrc3/transactions.rs | 4 ++-- rs/ledger_suite/icrc1/src/lib.rs | 12 ++++-------- rs/ledger_suite/icrc1/tests/tests.rs | 2 +- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/icrc-ledger-types/src/icrc3/transactions.rs b/packages/icrc-ledger-types/src/icrc3/transactions.rs index cf3c41bd50fc..92a199b452bd 100644 --- a/packages/icrc-ledger-types/src/icrc3/transactions.rs +++ b/packages/icrc-ledger-types/src/icrc3/transactions.rs @@ -1,4 +1,4 @@ -use candid::{CandidType, Deserialize, Nat}; +use candid::{CandidType, Deserialize, Nat, Principal}; use serde::Serialize; use crate::{ @@ -61,7 +61,7 @@ pub struct Approve { #[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] pub struct FeeCollector { pub fee_collector: Option, - pub caller: Option, + pub caller: Option, pub created_at_time: Option, } diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index cd261be8040c..c6a60c84b5a8 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -4,6 +4,7 @@ pub mod endpoints; pub mod hash; pub(crate) mod known_tags; +use candid::Principal; use ciborium::tag::Required; use ic_ledger_canister_core::ledger::{LedgerContext, LedgerTransaction, TxApplyError}; use ic_ledger_core::{ @@ -88,12 +89,8 @@ pub enum Operation { with = "compact_account::opt" )] fee_collector: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - with = "compact_account::opt" - )] - caller: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + caller: Option, }, } @@ -156,8 +153,7 @@ struct FlattenedTransaction { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - #[serde(with = "compact_account::opt")] - caller: Option, + caller: Option, } impl TryFrom> for Transaction { diff --git a/rs/ledger_suite/icrc1/tests/tests.rs b/rs/ledger_suite/icrc1/tests/tests.rs index 252326f6c508..424f28dda4e1 100644 --- a/rs/ledger_suite/icrc1/tests/tests.rs +++ b/rs/ledger_suite/icrc1/tests/tests.rs @@ -185,7 +185,7 @@ fn test_fee_collector_block() { let original_block = BlockBuilder::::new(1, 111) .with_btype("107feecol".to_string()) .with_parent_hash(vec![1u8; 32]) - .fee_collector(Some(TEST_ACCOUNT), None, Some(123)) + .fee_collector(Some(TEST_ACCOUNT), Some(TEST_USER.0), Some(123)) .build(); let block = generic_block_to_encoded_block(original_block.clone().into()) From 8125a18a565585b89eccffe549c9d81f1910d277 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 00:43:53 +0000 Subject: [PATCH 21/85] build fix --- rs/ledger_suite/icrc1/archive/tests/tests.rs | 2 ++ rs/ledger_suite/icrc1/ledger/tests/tests.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/rs/ledger_suite/icrc1/archive/tests/tests.rs b/rs/ledger_suite/icrc1/archive/tests/tests.rs index df26c0d640fd..e928a2766f45 100644 --- a/rs/ledger_suite/icrc1/archive/tests/tests.rs +++ b/rs/ledger_suite/icrc1/archive/tests/tests.rs @@ -111,6 +111,7 @@ fn test_icrc3_get_blocks() { created_at_time: None, memo: None, }, + btype: None, } }; @@ -265,6 +266,7 @@ fn test_icrc3_get_blocks_number_of_blocks_limit() { created_at_time: None, memo: None, }, + btype: None, } .encode() } diff --git a/rs/ledger_suite/icrc1/ledger/tests/tests.rs b/rs/ledger_suite/icrc1/ledger/tests/tests.rs index ddf47da6e0a2..d23890c9602f 100644 --- a/rs/ledger_suite/icrc1/ledger/tests/tests.rs +++ b/rs/ledger_suite/icrc1/ledger/tests/tests.rs @@ -1216,6 +1216,7 @@ fn test_icrc3_get_archives() { timestamp: 0, fee_collector: None, fee_collector_block_index: None, + btype: None, } .encode() .size_bytes(); From a782c66f043f1b2747807cfc1be0bd439b43bc8b Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 04:23:43 +0000 Subject: [PATCH 22/85] update did files --- rs/ledger_suite/icrc1/archive/archive.did | 2 +- rs/ledger_suite/icrc1/index-ng/index-ng.did | 2 +- rs/ledger_suite/icrc1/ledger/ledger.did | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rs/ledger_suite/icrc1/archive/archive.did b/rs/ledger_suite/icrc1/archive/archive.did index fffc257829cf..5ceb4a125a84 100644 --- a/rs/ledger_suite/icrc1/archive/archive.did +++ b/rs/ledger_suite/icrc1/archive/archive.did @@ -23,7 +23,7 @@ type Approve = record { }; type FeeCollector = record { - caller : opt Account; + caller : opt principal; fee_collector : opt Account; created_at_time : opt nat64 }; diff --git a/rs/ledger_suite/icrc1/index-ng/index-ng.did b/rs/ledger_suite/icrc1/index-ng/index-ng.did index 91d629b99a15..f025893421c1 100644 --- a/rs/ledger_suite/icrc1/index-ng/index-ng.did +++ b/rs/ledger_suite/icrc1/index-ng/index-ng.did @@ -62,7 +62,7 @@ type Transaction = record { }; type FeeCollector = record { - caller : opt Account; + caller : opt principal; fee_collector : opt Account; created_at_time : opt nat64 }; diff --git a/rs/ledger_suite/icrc1/ledger/ledger.did b/rs/ledger_suite/icrc1/ledger/ledger.did index d9376f97bbb0..6a3e865a8628 100644 --- a/rs/ledger_suite/icrc1/ledger/ledger.did +++ b/rs/ledger_suite/icrc1/ledger/ledger.did @@ -230,7 +230,7 @@ type Transaction = record { }; type FeeCollector = record { - caller : opt Account; + caller : opt principal; fee_collector : opt Account; created_at_time : opt nat64 }; From a201faf1ad05f0fc51ec702e625d4af7815e80c5 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 17:54:20 +0000 Subject: [PATCH 23/85] create a proptest --- rs/ledger_suite/icrc1/test_utils/src/icrc3.rs | 9 ++- rs/ledger_suite/icrc1/tests/tests.rs | 60 +++++++++++-------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs b/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs index 794ea106eb16..37bf1e28e9f2 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs @@ -329,13 +329,12 @@ impl FeeCollectorBuilder { /// Build the mint block pub fn build(self) -> ICRC3Value { let mut tx_fields = BTreeMap::new(); - match &self.fee_collector { - Some(fee_collector) => tx_fields.insert( + if let Some(fee_collector) = &self.fee_collector { + tx_fields.insert( "fee_collector".to_string(), account_to_icrc3_value(fee_collector), - ), - None => tx_fields.insert("fee_collector".to_string(), ICRC3Value::Array(vec![])), - }; + ); + } if let Some(caller) = &self.caller { tx_fields.insert("caller".to_string(), ICRC3Value::from(Value::from(*caller))); } diff --git a/rs/ledger_suite/icrc1/tests/tests.rs b/rs/ledger_suite/icrc1/tests/tests.rs index 424f28dda4e1..3cb3faa5ad42 100644 --- a/rs/ledger_suite/icrc1/tests/tests.rs +++ b/rs/ledger_suite/icrc1/tests/tests.rs @@ -1,11 +1,11 @@ -use ic_base_types::PrincipalId; +use candid::Principal; use ic_icrc1::blocks::{ encoded_block_to_generic_block, generic_block_to_encoded_block, generic_transaction_from_generic_block, }; use ic_icrc1::{Block, Transaction, hash}; use ic_icrc1_test_utils::icrc3::BlockBuilder; -use ic_icrc1_test_utils::{arb_amount, arb_block, arb_small_amount, blocks_strategy}; +use ic_icrc1_test_utils::{arb_account, arb_amount, arb_block, arb_small_amount, blocks_strategy}; use ic_icrc1_tokens_u64::U64; use ic_icrc1_tokens_u256::U256; use ic_ledger_canister_core::ledger::LedgerTransaction; @@ -13,7 +13,7 @@ use ic_ledger_core::Tokens; use ic_ledger_core::block::BlockType; use ic_ledger_core::tokens::TokensType; use ic_ledger_hash_of::HashOf; -use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc::generic_value::ICRC3Value; use proptest::prelude::*; fn arb_u256() -> impl Strategy { @@ -174,30 +174,40 @@ fn arb_token_u256() -> impl Strategy { (any::(), any::()).prop_map(|(hi, lo)| U256::from_words(hi, lo)) } -#[test] -fn test_fee_collector_block() { - const TEST_USER: PrincipalId = PrincipalId::new_user_test_id(1); - const TEST_ACCOUNT: Account = Account { - owner: TEST_USER.0, - subaccount: Some([1u8; 32]), - }; - - let original_block = BlockBuilder::::new(1, 111) - .with_btype("107feecol".to_string()) - .with_parent_hash(vec![1u8; 32]) - .fee_collector(Some(TEST_ACCOUNT), Some(TEST_USER.0), Some(123)) - .build(); +pub fn arb_fee_collector_block() -> impl Strategy { + ( + any::(), + any::(), + any::>(), + proptest::option::of(arb_account()), + proptest::option::of(proptest::collection::vec(any::(), 28)), + any::>(), + ) + .prop_map( + |(block_id, block_ts, parent_hash, fee_collector, caller, tx_ts)| { + let caller = caller.map(|mut c| { + c.push(0x00); + Principal::try_from_slice(&c[..]).unwrap() + }); + let builder = BlockBuilder::::new(block_id, block_ts) + .with_btype("107feecol".to_string()); + let builder = match parent_hash { + Some(parent_hash) => builder.with_parent_hash(parent_hash.to_vec()), + None => builder, + }; + builder.fee_collector(fee_collector, caller, tx_ts).build() + }, + ) +} - let block = generic_block_to_encoded_block(original_block.clone().into()) +#[test_strategy::proptest] +fn test_encoding_decoding_fee_collector_block( + #[strategy(arb_fee_collector_block())] original_block: ICRC3Value, +) { + let encoded_block = generic_block_to_encoded_block(original_block.clone().into()) .expect("failed to decode generic block"); - let decoded_block = - Block::::decode(block.clone()).expect("failed to decode encoded block"); - + Block::::decode(encoded_block.clone()).expect("failed to decode encoded block"); let decoded_value = encoded_block_to_generic_block(&decoded_block.clone().encode()); - - println!("original: {}", original_block); - println!("decoded: {}", decoded_value); - - assert_eq!(original_block.clone().hash(), decoded_value.hash()); + prop_assert_eq!(original_block.clone().hash(), decoded_value.hash()); } From 5f2c5dfb55805778f76639f3f8f38d16f05c4046 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 18:05:06 +0000 Subject: [PATCH 24/85] test u64 and u256 blocks --- rs/ledger_suite/icrc1/tests/tests.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/rs/ledger_suite/icrc1/tests/tests.rs b/rs/ledger_suite/icrc1/tests/tests.rs index 3cb3faa5ad42..cfce334ea75f 100644 --- a/rs/ledger_suite/icrc1/tests/tests.rs +++ b/rs/ledger_suite/icrc1/tests/tests.rs @@ -174,7 +174,10 @@ fn arb_token_u256() -> impl Strategy { (any::(), any::()).prop_map(|(hi, lo)| U256::from_words(hi, lo)) } -pub fn arb_fee_collector_block() -> impl Strategy { +pub fn arb_fee_collector_block() -> impl Strategy +where + Tokens: TokensType, +{ ( any::(), any::(), @@ -189,7 +192,7 @@ pub fn arb_fee_collector_block() -> impl Strategy { c.push(0x00); Principal::try_from_slice(&c[..]).unwrap() }); - let builder = BlockBuilder::::new(block_id, block_ts) + let builder = BlockBuilder::::new(block_id, block_ts) .with_btype("107feecol".to_string()); let builder = match parent_hash { Some(parent_hash) => builder.with_parent_hash(parent_hash.to_vec()), @@ -201,8 +204,8 @@ pub fn arb_fee_collector_block() -> impl Strategy { } #[test_strategy::proptest] -fn test_encoding_decoding_fee_collector_block( - #[strategy(arb_fee_collector_block())] original_block: ICRC3Value, +fn test_encoding_decoding_fee_collector_block_u64( + #[strategy(arb_fee_collector_block::())] original_block: ICRC3Value, ) { let encoded_block = generic_block_to_encoded_block(original_block.clone().into()) .expect("failed to decode generic block"); @@ -211,3 +214,15 @@ fn test_encoding_decoding_fee_collector_block( let decoded_value = encoded_block_to_generic_block(&decoded_block.clone().encode()); prop_assert_eq!(original_block.clone().hash(), decoded_value.hash()); } + +#[test_strategy::proptest] +fn test_encoding_decoding_fee_collector_block_u256( + #[strategy(arb_fee_collector_block::())] original_block: ICRC3Value, +) { + let encoded_block = generic_block_to_encoded_block(original_block.clone().into()) + .expect("failed to decode generic block"); + let decoded_block = + Block::::decode(encoded_block.clone()).expect("failed to decode encoded block"); + let decoded_value = encoded_block_to_generic_block(&decoded_block.clone().encode()); + prop_assert_eq!(original_block.clone().hash(), decoded_value.hash()); +} From f28d46b34beb056b4ce887f90e774425e96ae19c Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 18:22:57 +0000 Subject: [PATCH 25/85] change created_at_time to ts --- packages/icrc-ledger-types/src/icrc3/transactions.rs | 2 +- rs/ledger_suite/icrc1/archive/archive.did | 2 +- rs/ledger_suite/icrc1/index-ng/index-ng.did | 2 +- rs/ledger_suite/icrc1/ledger/ledger.did | 2 +- rs/ledger_suite/icrc1/src/endpoints.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/icrc-ledger-types/src/icrc3/transactions.rs b/packages/icrc-ledger-types/src/icrc3/transactions.rs index 92a199b452bd..f1da04573c5b 100644 --- a/packages/icrc-ledger-types/src/icrc3/transactions.rs +++ b/packages/icrc-ledger-types/src/icrc3/transactions.rs @@ -62,7 +62,7 @@ pub struct Approve { pub struct FeeCollector { pub fee_collector: Option, pub caller: Option, - pub created_at_time: Option, + pub ts: Option, } // Representation of a Transaction which supports the Icrc1 Standard functionalities diff --git a/rs/ledger_suite/icrc1/archive/archive.did b/rs/ledger_suite/icrc1/archive/archive.did index 5ceb4a125a84..cf9bc873895a 100644 --- a/rs/ledger_suite/icrc1/archive/archive.did +++ b/rs/ledger_suite/icrc1/archive/archive.did @@ -25,7 +25,7 @@ type Approve = record { type FeeCollector = record { caller : opt principal; fee_collector : opt Account; - created_at_time : opt nat64 + ts : opt nat64 }; type Burn = record { diff --git a/rs/ledger_suite/icrc1/index-ng/index-ng.did b/rs/ledger_suite/icrc1/index-ng/index-ng.did index f025893421c1..5bba0a9060e4 100644 --- a/rs/ledger_suite/icrc1/index-ng/index-ng.did +++ b/rs/ledger_suite/icrc1/index-ng/index-ng.did @@ -64,7 +64,7 @@ type Transaction = record { type FeeCollector = record { caller : opt principal; fee_collector : opt Account; - created_at_time : opt nat64 + ts : opt nat64 }; type Approve = record { diff --git a/rs/ledger_suite/icrc1/ledger/ledger.did b/rs/ledger_suite/icrc1/ledger/ledger.did index 6a3e865a8628..79bd1c745855 100644 --- a/rs/ledger_suite/icrc1/ledger/ledger.did +++ b/rs/ledger_suite/icrc1/ledger/ledger.did @@ -232,7 +232,7 @@ type Transaction = record { type FeeCollector = record { caller : opt principal; fee_collector : opt Account; - created_at_time : opt nat64 + ts : opt nat64 }; type Burn = record { diff --git a/rs/ledger_suite/icrc1/src/endpoints.rs b/rs/ledger_suite/icrc1/src/endpoints.rs index d1169c7fa65b..2f3864c81087 100644 --- a/rs/ledger_suite/icrc1/src/endpoints.rs +++ b/rs/ledger_suite/icrc1/src/endpoints.rs @@ -242,7 +242,7 @@ impl From> for Transaction { tx.fee_collector = Some(FeeCollector { fee_collector, caller, - created_at_time, + ts: created_at_time, }); } } From eeca831f77946e1a0c507291b39604e657eefaec Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 20:58:49 +0000 Subject: [PATCH 26/85] test index handling of the 107 fee collector --- rs/ledger_suite/icrc1/index-ng/tests/tests.rs | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs index f603ced774da..efcd4314d94a 100644 --- a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs +++ b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs @@ -1259,6 +1259,96 @@ fn test_fee_collector() { ); } +#[test] +fn test_fee_collector_107() { + let env = &StateMachine::new(); + let ledger_id = install_icrc3_test_ledger(env); + let index_id = install_index_ng(env, index_init_arg_without_interval(ledger_id)); + let fee_collector_1 = account(101, 0); + let fee_collector_2 = account(102, 0); + let regular_account = account(1, 0); + + let mut block_id = 0; + + let add_mint_block = |block_id: u64, fc: Option, fc_id: Option| { + let mint = BlockBuilder::new(block_id, block_id).with_fee(Tokens::from(1)); + let mint = match fc { + Some(fc) => mint.with_fee_collector(fc), + None => mint, + }; + let mint = match fc_id { + Some(fc_id) => mint.with_fee_collector_block(fc_id), + None => mint, + }; + let mint = mint.mint(regular_account, Tokens::from(1)).build(); + + assert_eq!( + Nat::from(block_id), + add_block(&env, ledger_id, &mint) + .expect("error adding mint block to ICRC-3 test ledger") + ); + wait_until_sync_is_completed(env, index_id, ledger_id); + block_id + 1 + }; + + let add_fee_collector_107_block = |block_id: u64, fc: Option| { + let fee_collector = BlockBuilder::::new(block_id, block_id) + .with_btype("107feecol".to_string()) + .fee_collector(fc, None, None) + .build(); + + assert_eq!( + Nat::from(block_id), + add_block(&env, ledger_id, &fee_collector) + .expect("error adding mint block to ICRC-3 test ledger") + ); + wait_until_sync_is_completed(env, index_id, ledger_id); + block_id + 1 + }; + + // Legacy fee collector collects the fees + block_id = add_mint_block(block_id, Some(fee_collector_1), None); + assert_eq!(1, icrc1_balance_of(env, index_id, fee_collector_1)); + block_id = add_mint_block(block_id, None, Some(0)); + assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + + // Set 107 fee collector to burn + block_id = add_fee_collector_107_block(block_id, None); + + // No fees collected + block_id = add_mint_block(block_id, None, None); + assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + assert_eq!(0, icrc1_balance_of(env, index_id, fee_collector_2)); + + // No fees collected with the legacy fee collector + block_id = add_mint_block(block_id, Some(fee_collector_1), None); + block_id = add_mint_block(block_id, None, Some(block_id - 1)); + assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + assert_eq!(0, icrc1_balance_of(env, index_id, fee_collector_2)); + + // Set 107 fee collector to fee_collector_2 + block_id = add_fee_collector_107_block(block_id, Some(fee_collector_2)); + + // New fee collector receives the fees + block_id = add_mint_block(block_id, None, None); + assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + assert_eq!(1, icrc1_balance_of(env, index_id, fee_collector_2)); + + // Legacy fee collector has no effect, new fee collector receives the fees + block_id = add_mint_block(block_id, Some(fee_collector_1), None); + block_id = add_mint_block(block_id, None, Some(block_id - 1)); + assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + assert_eq!(3, icrc1_balance_of(env, index_id, fee_collector_2)); + + // Set 107 fee collector to burn + block_id = add_fee_collector_107_block(block_id, None); + + // No fees collected + add_mint_block(block_id, None, None); + assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + assert_eq!(3, icrc1_balance_of(env, index_id, fee_collector_2)); +} + #[test] fn test_index_ledger_coherence() { let mut runner = TestRunner::new(TestRunnerConfig::with_cases(1)); From c9a3a327313681446b44544a2fd31dbb297167d1 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 21:07:23 +0000 Subject: [PATCH 27/85] change panic message --- rs/ledger_suite/icrc1/src/lib.rs | 4 ++-- rs/ledger_suite/icrc1/test_utils/src/lib.rs | 4 ++-- rs/ledger_suite/test_utils/in_memory_ledger/src/lib.rs | 2 +- rs/rosetta-api/icrc1/src/common/storage/types.rs | 2 +- rs/rosetta-api/icrc1/src/construction_api/services.rs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index c6a60c84b5a8..7af166b27b06 100644 --- a/rs/ledger_suite/icrc1/src/lib.rs +++ b/rs/ledger_suite/icrc1/src/lib.rs @@ -468,7 +468,7 @@ impl LedgerTransaction for Transaction { } } Operation::FeeCollector { .. } => { - panic!("not implemented") + panic!("FeeCollector107 not implemented") } } Ok(()) @@ -599,7 +599,7 @@ impl BlockType for Block { Operation::Transfer { fee, .. } => fee.is_none().then_some(effective_fee), Operation::Approve { fee, .. } => fee.is_none().then_some(effective_fee), Operation::FeeCollector { .. } => { - panic!("not implemented") + panic!("FeeCollector107 not implemented") } _ => None, }; diff --git a/rs/ledger_suite/icrc1/test_utils/src/lib.rs b/rs/ledger_suite/icrc1/test_utils/src/lib.rs index 54a0ee2a741b..100445935b35 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/lib.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/lib.rs @@ -581,7 +581,7 @@ impl TransactionsAndBalances { self.debit(from, fee); } Operation::FeeCollector { .. } => { - panic!("not implemented") + panic!("FeeCollector107 not implemented") } }; self.transactions.push(tx); @@ -611,7 +611,7 @@ impl TransactionsAndBalances { self.check_and_update_account_validity(*from, default_fee); } Operation::FeeCollector { .. } => { - panic!("not implemented") + panic!("FeeCollector107 not implemented") } } } 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 94e8b9484454..39ce7e849c78 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 @@ -539,7 +539,7 @@ where TimeStamp::from_nanos_since_unix_epoch(block.timestamp), ), Operation::FeeCollector { .. } => { - panic!("not implemented") + panic!("FeeCollector107 not implemented") } } } diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index b3d9ed714a5e..3648a7bb985f 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -609,7 +609,7 @@ where fee: fee.map(Into::into), }, Op::FeeCollector { .. } => { - panic!("not implemented") + panic!("FeeCollector107 not implemented") } } } diff --git a/rs/rosetta-api/icrc1/src/construction_api/services.rs b/rs/rosetta-api/icrc1/src/construction_api/services.rs index 969c60c88854..1db71511166c 100644 --- a/rs/rosetta-api/icrc1/src/construction_api/services.rs +++ b/rs/rosetta-api/icrc1/src/construction_api/services.rs @@ -545,7 +545,7 @@ mod tests { ic_icrc1::Operation::Mint { .. } => CanisterMethodName::Icrc1Transfer, ic_icrc1::Operation::Burn { .. } => CanisterMethodName::Icrc1Transfer, ic_icrc1::Operation::FeeCollector { .. } => { - panic!("not implemented") + panic!("FeeCollector107 not implemented") } }; let args = match arg_with_caller.arg { From 61d4e0ba06893e8183d794abfbfa8b6c6e7ccf6c Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 21:19:20 +0000 Subject: [PATCH 28/85] build fix --- rs/ledger_suite/icrc1/index-ng/tests/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs index efcd4314d94a..caf82115ef78 100644 --- a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs +++ b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs @@ -1271,7 +1271,7 @@ fn test_fee_collector_107() { let mut block_id = 0; let add_mint_block = |block_id: u64, fc: Option, fc_id: Option| { - let mint = BlockBuilder::new(block_id, block_id).with_fee(Tokens::from(1)); + let mint = BlockBuilder::new(block_id, block_id).with_fee(Tokens::from(1u64)); let mint = match fc { Some(fc) => mint.with_fee_collector(fc), None => mint, @@ -1280,7 +1280,7 @@ fn test_fee_collector_107() { Some(fc_id) => mint.with_fee_collector_block(fc_id), None => mint, }; - let mint = mint.mint(regular_account, Tokens::from(1)).build(); + let mint = mint.mint(regular_account, Tokens::from(1u64)).build(); assert_eq!( Nat::from(block_id), From 975cdc975e8ab64afd2bdff4720bacc0fb30673d Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 21:30:18 +0000 Subject: [PATCH 29/85] clippy --- rs/ledger_suite/icrc1/index-ng/tests/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs index caf82115ef78..dfc74ffdf59d 100644 --- a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs +++ b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs @@ -1284,7 +1284,7 @@ fn test_fee_collector_107() { assert_eq!( Nat::from(block_id), - add_block(&env, ledger_id, &mint) + add_block(env, ledger_id, &mint) .expect("error adding mint block to ICRC-3 test ledger") ); wait_until_sync_is_completed(env, index_id, ledger_id); @@ -1299,7 +1299,7 @@ fn test_fee_collector_107() { assert_eq!( Nat::from(block_id), - add_block(&env, ledger_id, &fee_collector) + add_block(env, ledger_id, &fee_collector) .expect("error adding mint block to ICRC-3 test ledger") ); wait_until_sync_is_completed(env, index_id, ledger_id); From 50e90aea3a074151707d189862d22355c30fec44 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 13 Nov 2025 23:10:17 +0000 Subject: [PATCH 30/85] test approve fees --- rs/ledger_suite/icrc1/index-ng/tests/tests.rs | 34 +++++++++++++++++-- rs/ledger_suite/icrc1/test_utils/src/icrc3.rs | 7 ++-- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs index dfc74ffdf59d..7102016c1a47 100644 --- a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs +++ b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs @@ -1280,7 +1280,7 @@ fn test_fee_collector_107() { Some(fc_id) => mint.with_fee_collector_block(fc_id), None => mint, }; - let mint = mint.mint(regular_account, Tokens::from(1u64)).build(); + let mint = mint.mint(regular_account, Tokens::from(1000u64)).build(); assert_eq!( Nat::from(block_id), @@ -1291,6 +1291,25 @@ fn test_fee_collector_107() { block_id + 1 }; + let add_approve_block = |block_id: u64, fc: Option| { + let approve = BlockBuilder::new(block_id, block_id).with_fee(Tokens::from(1u64)); + let approve = match fc { + Some(fc) => approve.with_fee_collector(fc), + None => approve, + }; + let approve = approve + .approve(regular_account, fee_collector_2, Tokens::from(1u64)) + .build(); + + assert_eq!( + Nat::from(block_id), + add_block(env, ledger_id, &approve) + .expect("error adding approve block to ICRC-3 test ledger") + ); + wait_until_sync_is_completed(env, index_id, ledger_id); + block_id + 1 + }; + let add_fee_collector_107_block = |block_id: u64, fc: Option| { let fee_collector = BlockBuilder::::new(block_id, block_id) .with_btype("107feecol".to_string()) @@ -1300,7 +1319,7 @@ fn test_fee_collector_107() { assert_eq!( Nat::from(block_id), add_block(env, ledger_id, &fee_collector) - .expect("error adding mint block to ICRC-3 test ledger") + .expect("error adding fee collector block to ICRC-3 test ledger") ); wait_until_sync_is_completed(env, index_id, ledger_id); block_id + 1 @@ -1312,6 +1331,10 @@ fn test_fee_collector_107() { block_id = add_mint_block(block_id, None, Some(0)); assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + // Legacy fee collector does not collect approve fees + block_id = add_approve_block(block_id, Some(fee_collector_1)); + assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + // Set 107 fee collector to burn block_id = add_fee_collector_107_block(block_id, None); @@ -1340,13 +1363,18 @@ fn test_fee_collector_107() { assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); assert_eq!(3, icrc1_balance_of(env, index_id, fee_collector_2)); + // 107 fee collector is credited the approve fee + block_id = add_approve_block(block_id, None); + assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + assert_eq!(4, icrc1_balance_of(env, index_id, fee_collector_2)); + // Set 107 fee collector to burn block_id = add_fee_collector_107_block(block_id, None); // No fees collected add_mint_block(block_id, None, None); assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); - assert_eq!(3, icrc1_balance_of(env, index_id, fee_collector_2)); + assert_eq!(4, icrc1_balance_of(env, index_id, fee_collector_2)); } #[test] diff --git a/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs b/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs index 37bf1e28e9f2..47354e0f0816 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs @@ -294,10 +294,7 @@ impl ApproveBuilder { let mut tx_fields = BTreeMap::new(); tx_fields.insert("from".to_string(), account_to_icrc3_value(&self.from)); tx_fields.insert("spender".to_string(), account_to_icrc3_value(&self.spender)); - tx_fields.insert( - "allowance".to_string(), - ICRC3Value::Nat(self.allowance.into()), - ); + tx_fields.insert("amt".to_string(), ICRC3Value::Nat(self.allowance.into())); if let Some(expected_allowance) = self.expected_allowance { tx_fields.insert( @@ -326,7 +323,7 @@ pub struct FeeCollectorBuilder { } impl FeeCollectorBuilder { - /// Build the mint block + /// Build the fee collector block pub fn build(self) -> ICRC3Value { let mut tx_fields = BTreeMap::new(); if let Some(fee_collector) = &self.fee_collector { From bfb852ed422a76573c3d03ad59ab8fa145c2e51e Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 14 Nov 2025 02:24:41 +0000 Subject: [PATCH 31/85] small refactor --- rs/ledger_suite/icrc1/index-ng/tests/tests.rs | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs index 7102016c1a47..c22a4dea163b 100644 --- a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs +++ b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs @@ -1264,8 +1264,8 @@ fn test_fee_collector_107() { let env = &StateMachine::new(); let ledger_id = install_icrc3_test_ledger(env); let index_id = install_index_ng(env, index_init_arg_without_interval(ledger_id)); - let fee_collector_1 = account(101, 0); - let fee_collector_2 = account(102, 0); + let feecol_legacy = account(101, 0); + let feecol_107 = account(102, 0); let regular_account = account(1, 0); let mut block_id = 0; @@ -1298,7 +1298,7 @@ fn test_fee_collector_107() { None => approve, }; let approve = approve - .approve(regular_account, fee_collector_2, Tokens::from(1u64)) + .approve(regular_account, regular_account, Tokens::from(1u64)) .build(); assert_eq!( @@ -1326,55 +1326,55 @@ fn test_fee_collector_107() { }; // Legacy fee collector collects the fees - block_id = add_mint_block(block_id, Some(fee_collector_1), None); - assert_eq!(1, icrc1_balance_of(env, index_id, fee_collector_1)); + block_id = add_mint_block(block_id, Some(feecol_legacy), None); + assert_eq!(1, icrc1_balance_of(env, index_id, feecol_legacy)); block_id = add_mint_block(block_id, None, Some(0)); - assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + assert_eq!(2, icrc1_balance_of(env, index_id, feecol_legacy)); // Legacy fee collector does not collect approve fees - block_id = add_approve_block(block_id, Some(fee_collector_1)); - assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); + block_id = add_approve_block(block_id, Some(feecol_legacy)); + assert_eq!(2, icrc1_balance_of(env, index_id, feecol_legacy)); // Set 107 fee collector to burn block_id = add_fee_collector_107_block(block_id, None); // No fees collected block_id = add_mint_block(block_id, None, None); - assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); - assert_eq!(0, icrc1_balance_of(env, index_id, fee_collector_2)); + assert_eq!(2, icrc1_balance_of(env, index_id, feecol_legacy)); + assert_eq!(0, icrc1_balance_of(env, index_id, feecol_107)); // No fees collected with the legacy fee collector - block_id = add_mint_block(block_id, Some(fee_collector_1), None); + block_id = add_mint_block(block_id, Some(feecol_legacy), None); block_id = add_mint_block(block_id, None, Some(block_id - 1)); - assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); - assert_eq!(0, icrc1_balance_of(env, index_id, fee_collector_2)); + assert_eq!(2, icrc1_balance_of(env, index_id, feecol_legacy)); + assert_eq!(0, icrc1_balance_of(env, index_id, feecol_107)); // Set 107 fee collector to fee_collector_2 - block_id = add_fee_collector_107_block(block_id, Some(fee_collector_2)); + block_id = add_fee_collector_107_block(block_id, Some(feecol_107)); // New fee collector receives the fees block_id = add_mint_block(block_id, None, None); - assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); - assert_eq!(1, icrc1_balance_of(env, index_id, fee_collector_2)); + assert_eq!(2, icrc1_balance_of(env, index_id, feecol_legacy)); + assert_eq!(1, icrc1_balance_of(env, index_id, feecol_107)); // Legacy fee collector has no effect, new fee collector receives the fees - block_id = add_mint_block(block_id, Some(fee_collector_1), None); + block_id = add_mint_block(block_id, Some(feecol_legacy), None); block_id = add_mint_block(block_id, None, Some(block_id - 1)); - assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); - assert_eq!(3, icrc1_balance_of(env, index_id, fee_collector_2)); + assert_eq!(2, icrc1_balance_of(env, index_id, feecol_legacy)); + assert_eq!(3, icrc1_balance_of(env, index_id, feecol_107)); // 107 fee collector is credited the approve fee block_id = add_approve_block(block_id, None); - assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); - assert_eq!(4, icrc1_balance_of(env, index_id, fee_collector_2)); + assert_eq!(2, icrc1_balance_of(env, index_id, feecol_legacy)); + assert_eq!(4, icrc1_balance_of(env, index_id, feecol_107)); // Set 107 fee collector to burn block_id = add_fee_collector_107_block(block_id, None); // No fees collected add_mint_block(block_id, None, None); - assert_eq!(2, icrc1_balance_of(env, index_id, fee_collector_1)); - assert_eq!(4, icrc1_balance_of(env, index_id, fee_collector_2)); + assert_eq!(2, icrc1_balance_of(env, index_id, feecol_legacy)); + assert_eq!(4, icrc1_balance_of(env, index_id, feecol_107)); } #[test] From 17cb2b23be0f68cffd9550c3a6b708faaf4ae0f2 Mon Sep 17 00:00:00 2001 From: maciejdfinity <122265298+maciejdfinity@users.noreply.github.com> Date: Fri, 14 Nov 2025 08:58:04 -0800 Subject: [PATCH 32/85] Update rs/ledger_suite/icrc1/index-ng/src/main.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathias Björkqvist --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index ca5083c900e7..8e442a100524 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -1022,10 +1022,7 @@ fn get_accounts(block: &Block) -> Vec { } fn get_fee_collector() -> Option { - match get_fee_collector_107() { - Some(fee_collector) => fee_collector, - None => get_legacy_fee_collector(), - } + get_fee_collector_107().unwrap_or_else(|| get_legacy_fee_collector()) } fn get_fee_collector_107() -> Option> { From dc2eb009382fa4d9b03f0bce1977d0042e1e1f9d Mon Sep 17 00:00:00 2001 From: maciejdfinity <122265298+maciejdfinity@users.noreply.github.com> Date: Fri, 14 Nov 2025 08:58:18 -0800 Subject: [PATCH 33/85] Update rs/ledger_suite/icrc1/test_utils/src/icrc3.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathias Björkqvist --- rs/ledger_suite/icrc1/test_utils/src/icrc3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs b/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs index 47354e0f0816..89e948ca89fb 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs @@ -115,7 +115,7 @@ impl BlockBuilder { } } - /// Create an approve operation + /// Create a fee collector block pub fn fee_collector( self, fee_collector: Option, From 3a858f5d4f9cbf6a558ae99b7e0ade7a59392344 Mon Sep 17 00:00:00 2001 From: maciejdfinity <122265298+maciejdfinity@users.noreply.github.com> Date: Fri, 14 Nov 2025 09:18:08 -0800 Subject: [PATCH 34/85] Update rs/ledger_suite/icrc1/index-ng/src/main.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathias Björkqvist --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 8e442a100524..8e1fe2f9bc91 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -971,10 +971,8 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block) { debit(block_index, from, fee); - if let Some(fee_collector_107) = get_fee_collector_107() - && let Some(fee_collector) = fee_collector_107 - { - credit(block_index, fee_collector, fee); + if let Some(fee_collector_107) = get_fee_collector_107().flatten() { + credit(block_index, fee_collector_107, fee); } } Operation::FeeCollector { From 26b1efc3e9a7dafa84bb085b3b35e312e97a1c78 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 14 Nov 2025 17:23:13 +0000 Subject: [PATCH 35/85] clippy --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 8e1fe2f9bc91..8d2d71f14bcd 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -1020,7 +1020,7 @@ fn get_accounts(block: &Block) -> Vec { } fn get_fee_collector() -> Option { - get_fee_collector_107().unwrap_or_else(|| get_legacy_fee_collector()) + get_fee_collector_107().unwrap_or_else(get_legacy_fee_collector) } fn get_fee_collector_107() -> Option> { From de0607830793dc4f7f4c1e2aaf53b36b402f8f6b Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 14 Nov 2025 17:52:12 +0000 Subject: [PATCH 36/85] rustdoc for fee collector 107 --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 8d2d71f14bcd..1e6ed064f81f 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -141,6 +141,10 @@ struct State { /// cycle burn for the index, ledger and archive(s). retrieve_blocks_from_ledger_interval: Option, + /// The ICRC-107 fee collector. Example values: + /// - `None` - legacy fee collector is used. + /// - `Some(None)` - 107 fee collecor is enabled but fees are burned. + /// - `Some(Some(account1))` - 107 fee collecor is enabled, `account1` collects the fees. fee_collector_107: Option>, } From 6fa9607c7678b634c1821b7a85c59343a10d7a11 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Tue, 18 Nov 2025 12:10:35 +0000 Subject: [PATCH 37/85] add fee collector operation --- .../src/common/storage/storage_operations.rs | 28 +++++++++++++++++ .../icrc1/src/common/storage/types.rs | 30 +++++++++++++++---- .../icrc1/src/common/utils/utils.rs | 6 ++++ .../icrc1/src/construction_api/utils.rs | 6 ++++ rs/rosetta-api/icrc1/src/data_api/services.rs | 12 ++++++++ 5 files changed, 76 insertions(+), 6 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 71018261c4d6..c872336178ed 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -388,6 +388,12 @@ pub fn update_account_balances(connection: &mut Connection) -> anyhow::Result<() )?; } } + crate::common::storage::types::IcrcOperation::FeeCollector { + fee_collector, + caller, + } => { + // we don't need to credit fee to anyone here + } } } @@ -506,6 +512,28 @@ pub fn store_blocks( fee, expires_at, ), + // can we reuse some of the fields, e.g. from = caller, to=fee_collector? + // + // we probably need to extend the block table with fee collector column + // we could copy the fee collector from previous column or have another + // column with feecol block index (similar to legacy mechanism) + // or add a separate table where we add fee collectors (probably now) + crate::common::storage::types::IcrcOperation::FeeCollector { + fee_collector, + caller, + } => ( + "107feecol", + None, + None, + None, + None, + None, + None, + Nat::from(0u64), + None, + None, + None, + ), }; // 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 3648a7bb985f..24057c3a2bdd 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -1,8 +1,6 @@ use anyhow::anyhow; use anyhow::bail; -use candid::CandidType; -use candid::Nat; -use candid::{Decode, Encode}; +use candid::{CandidType, Decode, Encode, Nat, Principal}; use ic_icrc1::blocks::encoded_block_to_generic_block; use ic_ledger_core::block::EncodedBlock; use ic_ledger_core::tokens::TokensType; @@ -138,6 +136,7 @@ impl RosettaBlock { IcrcOperation::Transfer { fee, .. } => fee, IcrcOperation::Approve { fee, .. } => fee, IcrcOperation::Burn { fee, .. } => fee, + IcrcOperation::FeeCollector { .. } => None, })) } @@ -363,6 +362,10 @@ pub enum IcrcOperation { expires_at: Option, fee: Option, }, + FeeCollector { + fee_collector: Option, + caller: Option, + }, } impl TryFrom> for IcrcOperation { @@ -495,6 +498,17 @@ impl From for BTreeMap { map.insert("fee".to_string(), Value::Nat(fee)); } } + Op::FeeCollector { + fee_collector, + caller, + } => { + if let Some(fee_collector) = fee_collector { + map.insert("fee_collector".to_string(), Value::from(fee_collector)); + } + if let Some(caller) = caller { + map.insert("caller".to_string(), Value::from(caller)); + } + } } map } @@ -608,9 +622,13 @@ where amount: amount.into(), fee: fee.map(Into::into), }, - Op::FeeCollector { .. } => { - panic!("FeeCollector107 not implemented") - } + Op::FeeCollector { + fee_collector, + caller, + } => Self::FeeCollector { + fee_collector, + caller, + }, } } } diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index e522537b919e..19ea220766fb 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -608,6 +608,12 @@ pub fn icrc1_operation_to_rosetta_core_operations( )); } } + crate::common::storage::types::IcrcOperation::FeeCollector { .. } => { + // There is no fee crediting operation at his moment so we might not need to do anything here + // Below in icrc1_rosetta_block_to_rosetta_core_operations we create an operation that credits + // the fee to the legacy fee collector. This needs to be adapted to not do anything if the + // new fee collector is present. + } }; Ok(operations) diff --git a/rs/rosetta-api/icrc1/src/construction_api/utils.rs b/rs/rosetta-api/icrc1/src/construction_api/utils.rs index b88d3746f8d3..90eaaf56ddef 100644 --- a/rs/rosetta-api/icrc1/src/construction_api/utils.rs +++ b/rs/rosetta-api/icrc1/src/construction_api/utils.rs @@ -272,6 +272,9 @@ pub fn build_icrc1_ledger_canister_method_args( }) } } + crate::common::storage::types::IcrcOperation::FeeCollector { .. } => { + bail!("FeeCollector Operation not supported") + } } .context("Unable to encode canister method args") } @@ -298,6 +301,9 @@ fn extract_caller_principal_from_icrc1_ledger_operation( crate::common::storage::types::IcrcOperation::Transfer { from, spender, .. } => { spender.unwrap_or(*from).owner } + crate::common::storage::types::IcrcOperation::FeeCollector { .. } => { + bail!("FeeCollector Operation not supported") + } }) } diff --git a/rs/rosetta-api/icrc1/src/data_api/services.rs b/rs/rosetta-api/icrc1/src/data_api/services.rs index d622f755177e..b09f75b9e0a0 100644 --- a/rs/rosetta-api/icrc1/src/data_api/services.rs +++ b/rs/rosetta-api/icrc1/src/data_api/services.rs @@ -1173,6 +1173,12 @@ mod test { IcrcOperation::Mint { to, .. } => to, IcrcOperation::Burn { from, .. } => from, IcrcOperation::Approve { from, .. } => from, + IcrcOperation::FeeCollector { + fee_collector, + caller, + } => { + panic!("FeeCollector107 not implemented") + } } .into(), ); @@ -1219,6 +1225,12 @@ mod test { .try_into() .unwrap(), ), + IcrcOperation::FeeCollector { + fee_collector, + caller, + } => { + panic!("FeeCollector107 not implemented") + } }) .count(); From f3c2280225b00947a036ed89b18af37e136ed1c7 Mon Sep 17 00:00:00 2001 From: maciejdfinity <122265298+maciejdfinity@users.noreply.github.com> Date: Tue, 18 Nov 2025 13:13:51 +0100 Subject: [PATCH 38/85] Update rs/ledger_suite/icrc1/index-ng/src/main.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathias Björkqvist --- rs/ledger_suite/icrc1/index-ng/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 1e6ed064f81f..16e8800243b7 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -143,8 +143,8 @@ struct State { /// The ICRC-107 fee collector. Example values: /// - `None` - legacy fee collector is used. - /// - `Some(None)` - 107 fee collecor is enabled but fees are burned. - /// - `Some(Some(account1))` - 107 fee collecor is enabled, `account1` collects the fees. + /// - `Some(None)` - 107 fee collector is enabled but fees are burned. + /// - `Some(Some(account1))` - 107 fee collector is enabled, `account1` collects the fees. fee_collector_107: Option>, } From 3c8283f93d5031282fd21f67e1e4066745c90e2e Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Wed, 19 Nov 2025 12:53:44 +0000 Subject: [PATCH 39/85] update changelog --- packages/icrc-ledger-types/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/icrc-ledger-types/CHANGELOG.md b/packages/icrc-ledger-types/CHANGELOG.md index 40734b730384..7978e708ecf8 100644 --- a/packages/icrc-ledger-types/CHANGELOG.md +++ b/packages/icrc-ledger-types/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add ICRC-107 fee collector transaction type. + ## 0.1.12 ### Added From bf53fb87893aedc48c073eb96d0c477fa507f6cf Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 24 Nov 2025 09:52:49 +0000 Subject: [PATCH 40/85] rename fee_collector to set_fee_collector --- packages/icrc-ledger-types/src/icrc3/transactions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/icrc-ledger-types/src/icrc3/transactions.rs b/packages/icrc-ledger-types/src/icrc3/transactions.rs index f1da04573c5b..4e855910cf01 100644 --- a/packages/icrc-ledger-types/src/icrc3/transactions.rs +++ b/packages/icrc-ledger-types/src/icrc3/transactions.rs @@ -126,7 +126,7 @@ impl Transaction { } } - pub fn fee_collector(fee_collector: FeeCollector, timestamp: u64) -> Self { + pub fn set_fee_collector(fee_collector: FeeCollector, timestamp: u64) -> Self { Self { kind: "107set_fee_collector".into(), timestamp, From b41b5667d43e5ba256e4cbcbf9dc14e421c7a2b4 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 24 Nov 2025 12:24:14 +0000 Subject: [PATCH 41/85] update canbench results --- .../ledger/canbench_results/canbench_u256.yml | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) 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 c4e7626a8e88..39e1c1b1eec5 100644 --- a/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml +++ b/rs/ledger_suite/icrc1/ledger/canbench_results/canbench_u256.yml @@ -2,70 +2,70 @@ benches: bench_icrc1_transfers: total: calls: 1 - instructions: 53985058770 - heap_increase: 262 + instructions: 54207644282 + heap_increase: 263 stable_memory_increase: 256 scopes: icrc103_get_allowances: calls: 1 - instructions: 6502280 + instructions: 6493127 heap_increase: 0 stable_memory_increase: 0 icrc1_transfer: calls: 1 - instructions: 12837236114 + instructions: 12874221706 heap_increase: 31 stable_memory_increase: 0 icrc2_approve: calls: 1 - instructions: 19208458344 - heap_increase: 28 + instructions: 19247077490 + heap_increase: 29 stable_memory_increase: 128 icrc2_transfer_from: calls: 1 - instructions: 21223966970 + instructions: 21368223296 heap_increase: 3 stable_memory_increase: 0 icrc3_get_blocks: calls: 1 - instructions: 8831859 + instructions: 8827484 heap_increase: 0 stable_memory_increase: 0 post_upgrade: calls: 1 - instructions: 357852389 + instructions: 357939096 heap_increase: 71 stable_memory_increase: 0 pre_upgrade: calls: 1 - instructions: 149850505 + instructions: 150903114 heap_increase: 129 stable_memory_increase: 128 upgrade: calls: 1 - instructions: 507705704 + instructions: 508845011 heap_increase: 200 stable_memory_increase: 128 bench_upgrade_baseline: total: calls: 1 - instructions: 8695350 + instructions: 8695587 heap_increase: 258 stable_memory_increase: 128 scopes: post_upgrade: calls: 1 - instructions: 8614551 + instructions: 8614550 heap_increase: 129 stable_memory_increase: 0 pre_upgrade: calls: 1 - instructions: 77963 + instructions: 78132 heap_increase: 129 stable_memory_increase: 128 upgrade: calls: 1 - instructions: 8694453 + instructions: 8694690 heap_increase: 258 stable_memory_increase: 128 version: 0.2.1 From 3171dd651fd353cc13d8364858024b51f0b0c0b4 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 27 Nov 2025 15:41:06 +0000 Subject: [PATCH 42/85] fee collector storing and retrieving from the database --- .../icrc1/src/common/storage/schema.rs | 13 +++ .../src/common/storage/storage_operations.rs | 110 +++++++++++++++--- 2 files changed, 108 insertions(+), 15 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/schema.rs b/rs/rosetta-api/icrc1/src/common/storage/schema.rs index a7f1eb4faa2c..58d4ac76581f 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/schema.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/schema.rs @@ -91,6 +91,19 @@ pub fn create_tables(connection: &Connection) -> Result<()> { [], )?; + // Fee Collectors 107 table + connection.execute( + r#" + CREATE TABLE IF NOT EXISTS fee_collectors_107 ( + block_idx INTEGER NOT NULL PRIMARY KEY, + caller_principal BLOB, + fee_collector_principal BLOB, + fee_collector_subaccount BLOB + ) + "#, + [], + )?; + create_indexes(connection) } diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 98641d86eddb..f4e3efb85599 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -1,11 +1,11 @@ use crate::MetadataEntry; use crate::common::storage::types::{RosettaBlock, RosettaCounter}; use anyhow::{Context, bail}; -use candid::Nat; +use candid::{Nat, Principal}; use ic_base_types::PrincipalId; use ic_ledger_core::tokens::Zero; use ic_ledger_core::tokens::{CheckedAdd, CheckedSub}; -use icrc_ledger_types::icrc1::account::Account; +use icrc_ledger_types::icrc1::account::{Account, Subaccount}; use num_bigint::BigUint; use rusqlite::Connection; use rusqlite::{CachedStatement, Params, named_params, params}; @@ -111,6 +111,12 @@ pub fn get_fee_collector_from_block( rosetta_block: &RosettaBlock, connection: &Connection, ) -> anyhow::Result> { + // First check if we have a 107 fee collector + if let Some(fee_collector_107) = get_fee_collector_107_for_idx(connection, rosetta_block.index)? + { + return Ok(fee_collector_107); + } + // First check if the fee collector is directly specified in the block if let Some(fee_collector) = rosetta_block.get_fee_collector() { return Ok(Some(fee_collector)); @@ -342,11 +348,23 @@ pub fn update_account_balances( .unwrap_or(Nat(BigUint::zero())); debit( from, - fee, + fee.clone(), rosetta_block.index, connection, &mut account_balances_cache, )?; + + if let Some(Some(collector)) = + get_fee_collector_107_for_idx(connection, rosetta_block.index)? + { + credit( + collector, + fee, + rosetta_block.index, + connection, + &mut account_balances_cache, + )?; + } } crate::common::storage::types::IcrcOperation::Transfer { from, @@ -391,10 +409,10 @@ pub fn update_account_balances( } } crate::common::storage::types::IcrcOperation::FeeCollector { - fee_collector, - caller, + fee_collector: _, + caller: _, } => { - // we don't need to credit fee to anyone here + // Fee Collector setting operations don't have a fee } } } @@ -520,21 +538,15 @@ pub fn store_blocks( fee, expires_at, ), - // can we reuse some of the fields, e.g. from = caller, to=fee_collector? - // - // we probably need to extend the block table with fee collector column - // we could copy the fee collector from previous column or have another - // column with feecol block index (similar to legacy mechanism) - // or add a separate table where we add fee collectors (probably now) crate::common::storage::types::IcrcOperation::FeeCollector { fee_collector, caller, } => ( "107feecol", + caller, None, - None, - None, - None, + fee_collector.map(|fc| fc.owner), + fee_collector.map(|fc| *fc.effective_subaccount()), None, None, Nat::from(0u64), @@ -577,6 +589,16 @@ pub fn store_blocks( ":transaction_created_at_time":transaction_created_at_time_i64, ":approval_expires_at":approval_expires_at_i64 })?; + if operation_type == "107feecol" { + insert_tx.prepare_cached( + "INSERT OR IGNORE INTO fee_collectors_107 (block_idx, caller_principal, fee_collector_principal, fee_collector_subaccount) VALUES (:block_idx, :caller_principal, :fee_collector_principal, :fee_collector_subaccount)")? + .execute(named_params! { + ":block_idx":rosetta_block.index, + ":caller_principal":from_principal.map(|x| x.as_slice().to_vec()), + ":fee_collector_principal":to_principal.map(|x| x.as_slice().to_vec()), + ":fee_collector_subaccount":to_subaccount, + })?; + } } insert_tx.commit()?; Ok(()) @@ -620,6 +642,18 @@ fn get_block_at_next_idx( read_single_block(&mut stmt, params![]) } +// Returns ICRC-107 fee collector that has effect on block at `block_idx` +pub fn get_fee_collector_107_for_idx( + connection: &Connection, + block_idx: u64, +) -> anyhow::Result>> { + let command = format!( + "SELECT fee_collector_principal,fee_collector_subaccount FROM fee_collectors_107 WHERE block_idx < {block_idx} ORDER BY block_idx DESC LIMIT 1" + ); + let mut stmt = connection.prepare_cached(&command)?; + read_single_fee_collector_107(&mut stmt, params![]) +} + // Returns a RosettaBlock if the block hash exists in the database, else returns None. // Returns an Error if the query fails. pub fn get_block_by_hash( @@ -865,6 +899,52 @@ where Ok(result) } +fn read_single_fee_collector_107

( + stmt: &mut CachedStatement, + params: P, +) -> anyhow::Result>> +where + P: Params, +{ + let fcs: Vec> = read_fee_collectors_107(stmt, params)?; + if fcs.len() == 1 { + // Return the fee collector if only one was found + Ok(Some(fcs[0].clone())) + } else if fcs.is_empty() { + // Return None if no block was found + Ok(None) + } else { + // If more than one block was found return an error + bail!("Multiple fee collectors found with given parameters".to_owned(),) + } +} + +// Executes the constructed statement that reads the 107 fee collectors. +fn read_fee_collectors_107

( + stmt: &mut CachedStatement, + params: P, +) -> anyhow::Result>> +where + P: Params, +{ + let fee_collectors = stmt.query_map(params, |row| { + let owner_bytes: Option<[u8; 29]> = row.get(0)?; + let subaccount: Option = row.get(1)?; + match owner_bytes { + Some(owner_bytes) => Ok(Some(Account { + owner: Principal::from_slice(&owner_bytes), + subaccount, + })), + None => Ok(None), + } + })?; + let mut result = vec![]; + for fc in fee_collectors { + result.push(fc?); + } + Ok(result) +} + /// Repairs account balances for databases created before the fee collector block index fix. /// This function clears the account_balances table and rebuilds it from scratch using the /// corrected fee collector resolution logic by reprocessing all blocks. From 015b692d51d978ab955b2f81b02b6b879f75a76d Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 28 Nov 2025 09:22:00 +0000 Subject: [PATCH 43/85] typo --- rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index f4e3efb85599..e5674d403fbb 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -911,10 +911,10 @@ where // Return the fee collector if only one was found Ok(Some(fcs[0].clone())) } else if fcs.is_empty() { - // Return None if no block was found + // Return None if no fee collector was found Ok(None) } else { - // If more than one block was found return an error + // If more than one fee collector was found return an error bail!("Multiple fee collectors found with given parameters".to_owned(),) } } From 74e805461c17a16de89e20dfa84058eb7a71d429 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 28 Nov 2025 14:11:02 +0000 Subject: [PATCH 44/85] add simple test --- .../src/common/storage/storage_operations.rs | 6 +- .../icrc1/src/common/storage/types.rs | 32 +++++++-- .../icrc1/src/common/utils/utils.rs | 1 + rs/rosetta-api/icrc1/tests/system_tests.rs | 67 +++++++++++++++++++ 4 files changed, 99 insertions(+), 7 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index e5674d403fbb..6c05286d6810 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -117,7 +117,9 @@ pub fn get_fee_collector_from_block( return Ok(fee_collector_107); } - // First check if the fee collector is directly specified in the block + // FeeCollector 107 does not apply to this block, see if there is a legacy fee collector + + // Check if the fee collector is directly specified in the block if let Some(fee_collector) = rosetta_block.get_fee_collector() { return Ok(Some(fee_collector)); } @@ -928,7 +930,7 @@ where P: Params, { let fee_collectors = stmt.query_map(params, |row| { - let owner_bytes: Option<[u8; 29]> = row.get(0)?; + let owner_bytes: Option> = row.get(0)?; let subaccount: Option = row.get(1)?; match owner_bytes { Some(owner_bytes) => Ok(Some(Account { diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 24057c3a2bdd..5612bf9c9d01 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -181,6 +181,7 @@ pub struct IcrcBlock { pub timestamp: u64, pub fee_collector: Option, pub fee_collector_block_index: Option, + pub btype: Option, } impl IcrcBlock { @@ -216,6 +217,7 @@ impl TryFrom for IcrcBlock { .transpose()?; let timestamp = get_field::(&map, &[], "ts")?; let effective_fee = get_opt_field::(&map, &[], "fee")?; + let btype = get_opt_field::(&map, &[], "btype")?; let fee_collector = get_opt_field::(&map, &[], "fee_col")?; let fee_collector_block_index = get_opt_field::(&map, &[], "fee_col_block")?; let transaction = map.get("tx").ok_or(anyhow!("Missing field 'tx'"))?.clone(); @@ -228,6 +230,7 @@ impl TryFrom for IcrcBlock { timestamp, fee_collector, fee_collector_block_index, + btype, }) } } @@ -248,6 +251,9 @@ impl From for Value { if let Some(fee_col) = block.fee_collector { map.insert("fee_col".to_string(), Value::from(fee_col)); } + if let Some(btype) = block.btype { + map.insert("btype".to_string(), Value::Text(btype)); + } if let Some(fee_col_block) = block.fee_collector_block_index { map.insert( "fee_col_block".to_string(), @@ -365,6 +371,7 @@ pub enum IcrcOperation { FeeCollector { fee_collector: Option, caller: Option, + // ts: Option, panic!("FeeCollector107 not implemented") }, } @@ -373,7 +380,7 @@ impl TryFrom> for IcrcOperation { fn try_from(map: BTreeMap) -> anyhow::Result { const FIELD_PREFIX: &[&str] = &["tx"]; - let amount: Nat = get_field(&map, FIELD_PREFIX, "amt")?; + let amount: Option = get_opt_field(&map, FIELD_PREFIX, "amt")?; let fee: Option = get_opt_field(&map, FIELD_PREFIX, "fee")?; match get_field::(&map, FIELD_PREFIX, "op")?.as_str() { "burn" => { @@ -382,13 +389,17 @@ impl TryFrom> for IcrcOperation { Ok(Self::Burn { from, spender, - amount, + amount: amount.expect("should be an amount"), // panic!("FeeCollector107 not implemented") fee, }) } "mint" => { let to: Account = get_field(&map, FIELD_PREFIX, "to")?; - Ok(Self::Mint { to, amount, fee }) + Ok(Self::Mint { + to, + amount: amount.expect("should be an amount"), // panic!("FeeCollector107 not implemented") + fee, + }) } "xfer" => { let from: Account = get_field(&map, FIELD_PREFIX, "from")?; @@ -398,7 +409,7 @@ impl TryFrom> for IcrcOperation { from, to, spender, - amount, + amount: amount.expect("should be an amount"), // panic!("FeeCollector107 not implemented") fee, }) } @@ -411,12 +422,21 @@ impl TryFrom> for IcrcOperation { Ok(Self::Approve { from, spender, - amount, + amount: amount.expect("should be an amount"), // panic!("FeeCollector107 not implemented") fee, expected_allowance, expires_at, }) } + "107set_fee_collector" => { + let fee_collector: Option = + get_opt_field(&map, FIELD_PREFIX, "fee_collector")?; + let caller: Option = get_opt_field(&map, FIELD_PREFIX, "caller")?; + Ok(Self::FeeCollector { + fee_collector, + caller, + }) + } found => { bail!( "Expected field 'op' to be 'burn', 'mint', 'xfer' or 'approve' but found {found}" @@ -502,6 +522,7 @@ impl From for BTreeMap { fee_collector, caller, } => { + map.insert("op".to_string(), Value::text("107set_fee_collector")); if let Some(fee_collector) = fee_collector { map.insert("fee_collector".to_string(), Value::from(fee_collector)); } @@ -791,6 +812,7 @@ mod tests { timestamp, fee_collector, fee_collector_block_index, + btype: None, // panic!("FeeCollector107 not implemented") }, ) } diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index 19ea220766fb..87b8f2d56fb5 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -720,6 +720,7 @@ pub fn rosetta_core_block_to_icrc1_block( ) }, fee_collector_block_index: block_metadata.fee_collector_block_index, + btype: None, // TODO: implement panic!("FeeCollector107 not implemented") }) } diff --git a/rs/rosetta-api/icrc1/tests/system_tests.rs b/rs/rosetta-api/icrc1/tests/system_tests.rs index 51ea41998273..1ceb5599b2ce 100644 --- a/rs/rosetta-api/icrc1/tests/system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/system_tests.rs @@ -882,6 +882,73 @@ fn test_error_backoff() { }); } +#[test] +fn test_fc_107() { + let rt = Runtime::new().unwrap(); + let setup = Setup::builder() + .with_custom_ledger_wasm(icrc3_test_ledger()) + .build(); + + rt.block_on(async { + let env = RosettaTestingEnvironmentBuilder::new(&setup).build().await; + + let agent = get_custom_agent(Arc::new(test_identity()), setup.port).await; + + let fc_account = Account { + owner: PrincipalId::new_user_test_id(12).into(), + subaccount: None, + }; + + let block0 = BlockBuilder::new(0, 0) + .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block0) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(0u64)); + + let block_index = + wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 0).await; + assert_eq!(block_index.unwrap(), 0); + + let block1 = BlockBuilder::::new(1, 1) + .with_btype("107feecol".to_string()) + .with_parent_hash(block0.hash().to_vec()) + .fee_collector(Some(fc_account), None, None) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block1) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(1u64)); + + let block2 = BlockBuilder::new(2, 2) + .with_parent_hash(block1.hash().to_vec()) + .with_fee(Tokens::from(123)) + .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block2) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(2u64)); + + let block_index = + wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 2).await; + assert_eq!(block_index.unwrap(), 2); + + assert_rosetta_balance( + fc_account, + 2, + 123u64, + &env.rosetta_client, + env.network_identifier.clone(), + ) + .await; + }); +} + const NUM_BLOCKS: u64 = 6; async fn verify_unrecognized_block_handling(setup: &Setup, bad_block_index: u64) { let mut log_file = NamedTempFile::new().expect("failed to create a temp file"); From e55154914bcc2bd9990b7fb55d735df99335dbe0 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 28 Nov 2025 15:07:04 +0000 Subject: [PATCH 45/85] extend the test --- rs/rosetta-api/icrc1/tests/system_tests.rs | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/rs/rosetta-api/icrc1/tests/system_tests.rs b/rs/rosetta-api/icrc1/tests/system_tests.rs index 1ceb5599b2ce..08ee2c9b8ddf 100644 --- a/rs/rosetta-api/icrc1/tests/system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/system_tests.rs @@ -946,6 +946,37 @@ fn test_fc_107() { env.network_identifier.clone(), ) .await; + + let block3 = BlockBuilder::::new(3, 3) + .with_btype("107feecol".to_string()) + .with_parent_hash(block2.hash().to_vec()) + .fee_collector(None, None, None) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block3) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(3u64)); + + let block4 = BlockBuilder::new(4, 4) + .with_parent_hash(block3.hash().to_vec()) + .with_fee(Tokens::from(123)) + .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block4) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(4u64)); + + assert_rosetta_balance( + fc_account, + 4, + 123u64, + &env.rosetta_client, + env.network_identifier.clone(), + ) + .await; }); } From 22272e577a479d813d58947302389876db956a34 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 28 Nov 2025 16:05:28 +0000 Subject: [PATCH 46/85] dummy principal for fc block in balances table, test --- .../src/common/storage/storage_operations.rs | 13 +++- rs/rosetta-api/icrc1/tests/system_tests.rs | 61 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 6c05286d6810..92f707b4d5af 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -273,6 +273,7 @@ pub fn update_account_balances( // For faster inserts, keep a cache of the account balances within a batch range in memory // This also makes the inserting of the account balances batchable and therefore faster let mut account_balances_cache: HashMap> = HashMap::new(); + let mut dummy_principals = vec![]; // As long as there are blocks to be fetched, keep on iterating over the blocks in the database with the given BATCH_SIZE interval while !rosetta_blocks.is_empty() { @@ -414,7 +415,7 @@ pub fn update_account_balances( fee_collector: _, caller: _, } => { - // Fee Collector setting operations don't have a fee + dummy_principals.push((rosetta_block.index, [107u8; 30])); } } } @@ -433,6 +434,16 @@ pub fn update_account_balances( })?; } } + for (block_idx, principal) in &dummy_principals { + insert_tx + .prepare_cached("INSERT INTO account_balances (block_idx, principal, subaccount, amount) VALUES (:block_idx, :principal, :subaccount, :amount)")? + .execute(named_params! { + ":block_idx": block_idx, + ":principal": principal, + ":subaccount": [0u8;32], + ":amount": "0", + })?; + } insert_tx.commit()?; if flush_cache_and_shrink_memory { diff --git a/rs/rosetta-api/icrc1/tests/system_tests.rs b/rs/rosetta-api/icrc1/tests/system_tests.rs index 08ee2c9b8ddf..a174d27d0583 100644 --- a/rs/rosetta-api/icrc1/tests/system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/system_tests.rs @@ -883,7 +883,66 @@ fn test_error_backoff() { } #[test] -fn test_fc_107() { +fn test_sync_to_fee_collector_107_block() { + let rt = Runtime::new().unwrap(); + let setup = Setup::builder() + .with_custom_ledger_wasm(icrc3_test_ledger()) + .build(); + + rt.block_on(async { + let env = RosettaTestingEnvironmentBuilder::new(&setup).build().await; + + let agent = get_custom_agent(Arc::new(test_identity()), setup.port).await; + + let fc_account = Account { + owner: PrincipalId::new_user_test_id(12).into(), + subaccount: None, + }; + + let block0 = BlockBuilder::::new(0, 0) + .with_btype("107feecol".to_string()) + .fee_collector(Some(fc_account), None, None) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block0) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(0u64)); + + let block_index = + wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 0).await; + assert_eq!(block_index.unwrap(), 0); + + let block1 = BlockBuilder::new(2, 2) + .with_parent_hash(block0.hash().to_vec()) + .with_fee(Tokens::from(123)) + .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block1) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(1u64)); + + let block2 = BlockBuilder::::new(1, 1) + .with_btype("107feecol".to_string()) + .with_parent_hash(block1.hash().to_vec()) + .fee_collector(Some(fc_account), None, None) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block2) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(2u64)); + + let block_index = + wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 2).await; + assert_eq!(block_index.unwrap(), 2); + }); +} + +#[test] +fn test_fee_collector_107_smoke() { let rt = Runtime::new().unwrap(); let setup = Setup::builder() .with_custom_ledger_wasm(icrc3_test_ledger()) From 27754a39227d399b88513874a4c628daeff4ef82 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 1 Dec 2025 10:25:13 +0000 Subject: [PATCH 47/85] remove separate table for fee collectors --- rs/rosetta-api/icrc1/src/common/storage/schema.rs | 13 ------------- .../icrc1/src/common/storage/storage_operations.rs | 12 +----------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/schema.rs b/rs/rosetta-api/icrc1/src/common/storage/schema.rs index 58d4ac76581f..a7f1eb4faa2c 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/schema.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/schema.rs @@ -91,19 +91,6 @@ pub fn create_tables(connection: &Connection) -> Result<()> { [], )?; - // Fee Collectors 107 table - connection.execute( - r#" - CREATE TABLE IF NOT EXISTS fee_collectors_107 ( - block_idx INTEGER NOT NULL PRIMARY KEY, - caller_principal BLOB, - fee_collector_principal BLOB, - fee_collector_subaccount BLOB - ) - "#, - [], - )?; - create_indexes(connection) } diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 92f707b4d5af..73e9018e8c0d 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -602,16 +602,6 @@ pub fn store_blocks( ":transaction_created_at_time":transaction_created_at_time_i64, ":approval_expires_at":approval_expires_at_i64 })?; - if operation_type == "107feecol" { - insert_tx.prepare_cached( - "INSERT OR IGNORE INTO fee_collectors_107 (block_idx, caller_principal, fee_collector_principal, fee_collector_subaccount) VALUES (:block_idx, :caller_principal, :fee_collector_principal, :fee_collector_subaccount)")? - .execute(named_params! { - ":block_idx":rosetta_block.index, - ":caller_principal":from_principal.map(|x| x.as_slice().to_vec()), - ":fee_collector_principal":to_principal.map(|x| x.as_slice().to_vec()), - ":fee_collector_subaccount":to_subaccount, - })?; - } } insert_tx.commit()?; Ok(()) @@ -661,7 +651,7 @@ pub fn get_fee_collector_107_for_idx( block_idx: u64, ) -> anyhow::Result>> { let command = format!( - "SELECT fee_collector_principal,fee_collector_subaccount FROM fee_collectors_107 WHERE block_idx < {block_idx} ORDER BY block_idx DESC LIMIT 1" + "SELECT to_principal,to_subaccount FROM blocks WHERE idx < {block_idx} AND operation_type = '107feecol' ORDER BY idx DESC LIMIT 1" ); let mut stmt = connection.prepare_cached(&command)?; read_single_fee_collector_107(&mut stmt, params![]) From c45a30228cfdc26510f090a5ecd3f489cd003111 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 1 Dec 2025 10:36:54 +0000 Subject: [PATCH 48/85] add index on operation_type --- rs/rosetta-api/icrc1/src/common/storage/schema.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rs/rosetta-api/icrc1/src/common/storage/schema.rs b/rs/rosetta-api/icrc1/src/common/storage/schema.rs index a7f1eb4faa2c..7c29dc5b0bdf 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/schema.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/schema.rs @@ -120,5 +120,13 @@ pub fn create_indexes(connection: &Connection) -> Result<()> { [], )?; + connection.execute( + r#" + CREATE INDEX IF NOT EXISTS block_operation_type + ON blocks(operation_type) + "#, + [], + )?; + Ok(()) } From 2a865c13b789ba132abe66d589a06d8d613741e7 Mon Sep 17 00:00:00 2001 From: Maciej Modelski Date: Mon, 1 Dec 2025 13:31:03 +0100 Subject: [PATCH 49/85] return error instead of panic if amount is missing --- rs/rosetta-api/icrc1/src/common/storage/types.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 5612bf9c9d01..29a0631b0499 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -389,7 +389,7 @@ impl TryFrom> for IcrcOperation { Ok(Self::Burn { from, spender, - amount: amount.expect("should be an amount"), // panic!("FeeCollector107 not implemented") + amount: amount.ok_or_else(|| anyhow!("Missing field 'amt'"))?, fee, }) } @@ -397,7 +397,7 @@ impl TryFrom> for IcrcOperation { let to: Account = get_field(&map, FIELD_PREFIX, "to")?; Ok(Self::Mint { to, - amount: amount.expect("should be an amount"), // panic!("FeeCollector107 not implemented") + amount: amount.ok_or_else(|| anyhow!("Missing field 'amt'"))?, fee, }) } @@ -409,7 +409,7 @@ impl TryFrom> for IcrcOperation { from, to, spender, - amount: amount.expect("should be an amount"), // panic!("FeeCollector107 not implemented") + amount: amount.ok_or_else(|| anyhow!("Missing field 'amt'"))?, fee, }) } @@ -422,7 +422,7 @@ impl TryFrom> for IcrcOperation { Ok(Self::Approve { from, spender, - amount: amount.expect("should be an amount"), // panic!("FeeCollector107 not implemented") + amount: amount.ok_or_else(|| anyhow!("Missing field 'amt'"))?, fee, expected_allowance, expires_at, From 3deef1169bdbe4e468cf5cf8d9ddf4b78d9d7d04 Mon Sep 17 00:00:00 2001 From: Maciej Modelski Date: Mon, 1 Dec 2025 13:34:23 +0100 Subject: [PATCH 50/85] better error msg --- rs/rosetta-api/icrc1/src/common/storage/types.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 29a0631b0499..ae80d99fb83f 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -389,7 +389,7 @@ impl TryFrom> for IcrcOperation { Ok(Self::Burn { from, spender, - amount: amount.ok_or_else(|| anyhow!("Missing field 'amt'"))?, + amount: amount.ok_or_else(|| anyhow!("Missing field 'amt' for Burn operation"))?, fee, }) } @@ -397,7 +397,7 @@ impl TryFrom> for IcrcOperation { let to: Account = get_field(&map, FIELD_PREFIX, "to")?; Ok(Self::Mint { to, - amount: amount.ok_or_else(|| anyhow!("Missing field 'amt'"))?, + amount: amount.ok_or_else(|| anyhow!("Missing field 'amt' for Mint operation"))?, fee, }) } @@ -409,7 +409,7 @@ impl TryFrom> for IcrcOperation { from, to, spender, - amount: amount.ok_or_else(|| anyhow!("Missing field 'amt'"))?, + amount: amount.ok_or_else(|| anyhow!("Missing field 'amt' for Transfer operation"))?, fee, }) } @@ -422,7 +422,7 @@ impl TryFrom> for IcrcOperation { Ok(Self::Approve { from, spender, - amount: amount.ok_or_else(|| anyhow!("Missing field 'amt'"))?, + amount: amount.ok_or_else(|| anyhow!("Missing field 'amt' for Approve operation"))?, fee, expected_allowance, expires_at, From d9ac1b5c73a6b1fe606b7dfc579acd4a316a2617 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 1 Dec 2025 14:25:26 +0000 Subject: [PATCH 51/85] remove unused FeeCollector rosetta core operation --- .../common/storage/storage_operations_test.rs | 2 ++ rs/rosetta-api/icrc1/src/common/types.rs | 1 - .../icrc1/src/common/utils/utils.rs | 24 +------------------ rs/rosetta-api/icrc1/src/data_api/services.rs | 12 ++++++++++ 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs index 05f6b1c17771..1023d8b6681b 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs @@ -54,6 +54,7 @@ fn create_test_rosetta_block( effective_fee: None, fee_collector: None, fee_collector_block_index: None, + btype: None, }; RosettaBlock { @@ -108,6 +109,7 @@ fn create_test_approve_block( effective_fee: None, fee_collector: None, fee_collector_block_index: None, + btype: None, }; RosettaBlock { diff --git a/rs/rosetta-api/icrc1/src/common/types.rs b/rs/rosetta-api/icrc1/src/common/types.rs index 6f3605ab922c..1687200f46ae 100644 --- a/rs/rosetta-api/icrc1/src/common/types.rs +++ b/rs/rosetta-api/icrc1/src/common/types.rs @@ -198,7 +198,6 @@ pub enum OperationType { Spender, Approve, Fee, - FeeCollector, } #[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index 87b8f2d56fb5..12983a7ab681 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -374,8 +374,6 @@ pub fn rosetta_core_operations_to_icrc1_operation( )?; icrc1_operation_builder.with_spender_accountidentifier(spender) } - // We do not have to convert this Operation on the icrc1 side as the crate::common::storage::types::IcrcOperation does not know anything about the FeeCollector - OperationType::FeeCollector => icrc1_operation_builder, }; } icrc1_operation_builder.build() @@ -628,32 +626,12 @@ pub fn icrc1_rosetta_block_to_rosetta_core_operations( ) -> anyhow::Result> { let icrc1_transaction = rosetta_block.get_transaction(); - let mut operations = icrc1_operation_to_rosetta_core_operations( + let operations = icrc1_operation_to_rosetta_core_operations( icrc1_transaction.operation, currency.clone(), rosetta_block.get_fee_paid()?, )?; - if let Some(fee_collector) = rosetta_block.get_fee_collector() - && let Some(_fee_payed) = rosetta_block.get_fee_paid()? - { - operations.push(rosetta_core::objects::Operation::new( - operations.len().try_into().unwrap(), - OperationType::FeeCollector.to_string(), - Some(fee_collector.into()), - Some(rosetta_core::objects::Amount::new( - BigInt::from( - rosetta_block - .get_fee_paid()? - .context("Fee payed needs to be populated for FeeCollector operation")? - .0, - ), - currency.clone(), - )), - None, - None, - )); - } Ok(operations) } diff --git a/rs/rosetta-api/icrc1/src/data_api/services.rs b/rs/rosetta-api/icrc1/src/data_api/services.rs index b09f75b9e0a0..5d9c1e5742da 100644 --- a/rs/rosetta-api/icrc1/src/data_api/services.rs +++ b/rs/rosetta-api/icrc1/src/data_api/services.rs @@ -1557,6 +1557,7 @@ mod test { timestamp: 1000, fee_collector: None, fee_collector_block_index: None, + btype: None, }, 0, )]; @@ -1625,6 +1626,7 @@ mod test { timestamp: 1000, fee_collector: None, fee_collector_block_index: None, + btype: None, }, 0, ), @@ -1644,6 +1646,7 @@ mod test { timestamp: 2000, fee_collector: None, fee_collector_block_index: None, + btype: None, }, 1, ), @@ -1794,6 +1797,7 @@ mod test { timestamp: 1000, fee_collector: None, fee_collector_block_index: None, + btype: None, }, }, // Block 1: Transfer 300 from main account to subaccount1 @@ -1816,6 +1820,7 @@ mod test { timestamp: 2000, fee_collector: None, fee_collector_block_index: None, + btype: None, }, }, // Block 2: Transfer 200 from main account to subaccount2 @@ -1838,6 +1843,7 @@ mod test { timestamp: 3000, fee_collector: None, fee_collector_block_index: None, + btype: None, }, }, // Block 3: Transfer 150 from subaccount1 to other_account @@ -1860,6 +1866,7 @@ mod test { timestamp: 4000, fee_collector: None, fee_collector_block_index: None, + btype: None, }, }, ]; @@ -2150,6 +2157,7 @@ mod test { timestamp: 1, fee_collector: None, fee_collector_block_index: None, + btype: None, }, 0, ), @@ -2170,6 +2178,7 @@ mod test { timestamp: 2, fee_collector: None, fee_collector_block_index: None, + btype: None, }, 1, ), @@ -2190,6 +2199,7 @@ mod test { timestamp: 3, fee_collector: None, fee_collector_block_index: None, + btype: None, }, 2, ), @@ -2302,6 +2312,7 @@ mod test { timestamp: 1, fee_collector: None, fee_collector_block_index: None, + btype: None, }, block_id, )]; @@ -2329,6 +2340,7 @@ mod test { timestamp: 1, fee_collector: None, fee_collector_block_index: None, + btype: None, }, block_id, )]; From 1dd9d73480bed85f4b24a3e587b6ee35b8b58bde Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 1 Dec 2025 14:27:43 +0000 Subject: [PATCH 52/85] clippy --- rs/rosetta-api/icrc1/src/common/storage/schema.rs | 2 +- rs/rosetta-api/icrc1/src/common/storage/types.rs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/schema.rs b/rs/rosetta-api/icrc1/src/common/storage/schema.rs index 7c29dc5b0bdf..4b1d2e84c317 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/schema.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/schema.rs @@ -126,7 +126,7 @@ pub fn create_indexes(connection: &Connection) -> Result<()> { ON blocks(operation_type) "#, [], - )?; + )?; Ok(()) } diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index ae80d99fb83f..278a87524d0a 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -389,7 +389,8 @@ impl TryFrom> for IcrcOperation { Ok(Self::Burn { from, spender, - amount: amount.ok_or_else(|| anyhow!("Missing field 'amt' for Burn operation"))?, + amount: amount + .ok_or_else(|| anyhow!("Missing field 'amt' for Burn operation"))?, fee, }) } @@ -397,7 +398,8 @@ impl TryFrom> for IcrcOperation { let to: Account = get_field(&map, FIELD_PREFIX, "to")?; Ok(Self::Mint { to, - amount: amount.ok_or_else(|| anyhow!("Missing field 'amt' for Mint operation"))?, + amount: amount + .ok_or_else(|| anyhow!("Missing field 'amt' for Mint operation"))?, fee, }) } @@ -409,7 +411,8 @@ impl TryFrom> for IcrcOperation { from, to, spender, - amount: amount.ok_or_else(|| anyhow!("Missing field 'amt' for Transfer operation"))?, + amount: amount + .ok_or_else(|| anyhow!("Missing field 'amt' for Transfer operation"))?, fee, }) } @@ -422,7 +425,8 @@ impl TryFrom> for IcrcOperation { Ok(Self::Approve { from, spender, - amount: amount.ok_or_else(|| anyhow!("Missing field 'amt' for Approve operation"))?, + amount: amount + .ok_or_else(|| anyhow!("Missing field 'amt' for Approve operation"))?, fee, expected_allowance, expires_at, From 681d089fba0af9103ec943538f07d71f201d9ea0 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 1 Dec 2025 18:54:44 +0000 Subject: [PATCH 53/85] clippy --- rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 73e9018e8c0d..0bd5c795dd0b 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -912,7 +912,7 @@ where let fcs: Vec> = read_fee_collectors_107(stmt, params)?; if fcs.len() == 1 { // Return the fee collector if only one was found - Ok(Some(fcs[0].clone())) + Ok(Some(fcs[0])) } else if fcs.is_empty() { // Return None if no fee collector was found Ok(None) From 21055eb97ff995466163bda14e6e8931246e5dbb Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 1 Dec 2025 19:08:09 +0000 Subject: [PATCH 54/85] clippy --- rs/rosetta-api/icrc1/src/data_api/services.rs | 8 ++++---- rs/rosetta-api/icrc1/tests/system_tests.rs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/data_api/services.rs b/rs/rosetta-api/icrc1/src/data_api/services.rs index 5d9c1e5742da..10ab9070a257 100644 --- a/rs/rosetta-api/icrc1/src/data_api/services.rs +++ b/rs/rosetta-api/icrc1/src/data_api/services.rs @@ -1174,8 +1174,8 @@ mod test { IcrcOperation::Burn { from, .. } => from, IcrcOperation::Approve { from, .. } => from, IcrcOperation::FeeCollector { - fee_collector, - caller, + fee_collector: _, + caller: _, } => { panic!("FeeCollector107 not implemented") } @@ -1226,8 +1226,8 @@ mod test { .unwrap(), ), IcrcOperation::FeeCollector { - fee_collector, - caller, + fee_collector: _, + caller: _, } => { panic!("FeeCollector107 not implemented") } diff --git a/rs/rosetta-api/icrc1/tests/system_tests.rs b/rs/rosetta-api/icrc1/tests/system_tests.rs index a174d27d0583..378356a261ac 100644 --- a/rs/rosetta-api/icrc1/tests/system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/system_tests.rs @@ -915,7 +915,7 @@ fn test_sync_to_fee_collector_107_block() { let block1 = BlockBuilder::new(2, 2) .with_parent_hash(block0.hash().to_vec()) - .with_fee(Tokens::from(123)) + .with_fee(Tokens::from(123u64)) .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) .build(); @@ -984,7 +984,7 @@ fn test_fee_collector_107_smoke() { let block2 = BlockBuilder::new(2, 2) .with_parent_hash(block1.hash().to_vec()) - .with_fee(Tokens::from(123)) + .with_fee(Tokens::from(123u64)) .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) .build(); @@ -1019,7 +1019,7 @@ fn test_fee_collector_107_smoke() { let block4 = BlockBuilder::new(4, 4) .with_parent_hash(block3.hash().to_vec()) - .with_fee(Tokens::from(123)) + .with_fee(Tokens::from(123u64)) .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) .build(); From 255084c0c0cc041253952b5044abd5de0305b5dc Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 1 Dec 2025 19:51:17 +0000 Subject: [PATCH 55/85] remove unused comment --- rs/rosetta-api/icrc1/src/common/storage/types.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 278a87524d0a..2c0a648e43cc 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -371,7 +371,6 @@ pub enum IcrcOperation { FeeCollector { fee_collector: Option, caller: Option, - // ts: Option, panic!("FeeCollector107 not implemented") }, } From e457710963b781d0bae965b9efa78784100d7301 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 1 Dec 2025 21:26:32 +0000 Subject: [PATCH 56/85] small fixes, implement arb_fee_collector --- .../src/common/storage/storage_operations.rs | 5 ++- .../icrc1/src/common/storage/types.rs | 44 ++++++++++++++----- .../icrc1/src/common/utils/utils.rs | 10 ++--- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 0bd5c795dd0b..2f8d66a71636 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -415,6 +415,9 @@ pub fn update_account_balances( fee_collector: _, caller: _, } => { + // Since we use the account_balances table to determine the synced height + // and since there is no credit or debit operation for the fee collector block, + // we have to add a dummy principal with the block index, to indicate the synced height dummy_principals.push((rosetta_block.index, [107u8; 30])); } } @@ -651,7 +654,7 @@ pub fn get_fee_collector_107_for_idx( block_idx: u64, ) -> anyhow::Result>> { let command = format!( - "SELECT to_principal,to_subaccount FROM blocks WHERE idx < {block_idx} AND operation_type = '107feecol' ORDER BY idx DESC LIMIT 1" + "SELECT to_principal,to_subaccount FROM blocks WHERE idx <= {block_idx} AND operation_type = '107feecol' ORDER BY idx DESC LIMIT 1" ); let mut stmt = connection.prepare_cached(&command)?; read_single_fee_collector_107(&mut stmt, params![]) diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 2c0a648e43cc..cabcd4e79e60 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -695,14 +695,15 @@ mod tests { use serde_bytes::ByteBuf; use std::collections::BTreeMap; + fn arb_principal() -> impl Strategy { + (vec(any::(), 0..30)).prop_map(|principal| Principal::from_slice(principal.as_slice())) + } + fn arb_account() -> impl Strategy { - (vec(any::(), 0..30), option::of(vec(any::(), 32))).prop_map( - |(owner, subaccount)| { - let owner = Principal::from_slice(owner.as_slice()); - let subaccount = subaccount.map(|v| v.try_into().unwrap()); - Account { owner, subaccount } - }, - ) + (arb_principal(), option::of(vec(any::(), 32))).prop_map(|(owner, subaccount)| { + let subaccount = subaccount.map(|v| v.try_into().unwrap()); + Account { owner, subaccount } + }) } fn arb_nat() -> impl Strategy { @@ -773,8 +774,25 @@ mod tests { }) } + fn arb_fee_collector() -> impl Strategy { + ( + option::of(arb_account()), // fee_collector + option::of(arb_principal()), // caller + ) + .prop_map(|(fee_collector, caller)| IcrcOperation::FeeCollector { + fee_collector, + caller, + }) + } + fn arb_op() -> impl Strategy { - prop_oneof![arb_approve(), arb_burn(), arb_mint(), arb_transfer(),] + prop_oneof![ + arb_approve(), + arb_burn(), + arb_mint(), + arb_transfer(), + arb_fee_collector(), + ] } fn arb_memo() -> impl Strategy { @@ -810,12 +828,18 @@ mod tests { fee_collector_block_index, )| IcrcBlock { parent_hash, - transaction, + transaction: transaction.clone(), effective_fee, timestamp, fee_collector, fee_collector_block_index, - btype: None, // panic!("FeeCollector107 not implemented") + btype: match transaction.operation { + IcrcOperation::FeeCollector { + fee_collector: _, + caller: _, + } => Some("107feecol".to_string()), + _ => None, + }, }, ) } diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index 12983a7ab681..da8686895df7 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -606,11 +606,11 @@ pub fn icrc1_operation_to_rosetta_core_operations( )); } } - crate::common::storage::types::IcrcOperation::FeeCollector { .. } => { - // There is no fee crediting operation at his moment so we might not need to do anything here - // Below in icrc1_rosetta_block_to_rosetta_core_operations we create an operation that credits - // the fee to the legacy fee collector. This needs to be adapted to not do anything if the - // new fee collector is present. + crate::common::storage::types::IcrcOperation::FeeCollector { + fee_collector: _, + caller: _, + } => { + // We don't support rosetta core fee collector operations } }; From 90f6fddeb9922d45adccb59fed709c1ee146a624 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Wed, 3 Dec 2025 14:06:38 +0000 Subject: [PATCH 57/85] add btype to rosetta block metadata --- rs/rosetta-api/icrc1/src/common/types.rs | 4 ++++ rs/rosetta-api/icrc1/src/common/utils/utils.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/rs/rosetta-api/icrc1/src/common/types.rs b/rs/rosetta-api/icrc1/src/common/types.rs index 1687200f46ae..fa161dfed6f8 100644 --- a/rs/rosetta-api/icrc1/src/common/types.rs +++ b/rs/rosetta-api/icrc1/src/common/types.rs @@ -307,6 +307,9 @@ pub struct BlockMetadata { // The Rosetta API standard field for timestamp is required in milliseconds // To ensure a lossless conversion we need to store the nano seconds for the timestamp pub block_created_at_nano_seconds: u64, + + #[serde(skip_serializing_if = "Option::is_none")] + pub btype: Option, } impl TryFrom for ObjectMap { @@ -345,6 +348,7 @@ impl BlockMetadata { effective_fee: block .effective_fee .map(|fee| Amount::new(BigInt::from(fee), currency)), + btype: block.btype, }) } } diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index da8686895df7..f89e0f505aef 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -698,7 +698,7 @@ pub fn rosetta_core_block_to_icrc1_block( ) }, fee_collector_block_index: block_metadata.fee_collector_block_index, - btype: None, // TODO: implement panic!("FeeCollector107 not implemented") + btype: block_metadata.btype, }) } From 6a58bb98ea6bb0d732ba285f852daebf9ffd6b82 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Wed, 3 Dec 2025 14:27:38 +0000 Subject: [PATCH 58/85] remove todos --- rs/rosetta-api/icrc1/src/data_api/services.rs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/data_api/services.rs b/rs/rosetta-api/icrc1/src/data_api/services.rs index 10ab9070a257..84ee04993a2c 100644 --- a/rs/rosetta-api/icrc1/src/data_api/services.rs +++ b/rs/rosetta-api/icrc1/src/data_api/services.rs @@ -1167,21 +1167,17 @@ mod test { }; // We make sure that the service returns the correct number of transactions for each account - search_transactions_request.account_identifier = Some( + search_transactions_request.account_identifier = match rosetta_blocks[0].block.transaction.operation { - IcrcOperation::Transfer { from, .. } => from, - IcrcOperation::Mint { to, .. } => to, - IcrcOperation::Burn { from, .. } => from, - IcrcOperation::Approve { from, .. } => from, + IcrcOperation::Transfer { from, .. } => Some(from.into()), + IcrcOperation::Mint { to, .. } => Some(to.into()), + IcrcOperation::Burn { from, .. } => Some(from.into()), + IcrcOperation::Approve { from, .. } => Some(from.into()), IcrcOperation::FeeCollector { fee_collector: _, caller: _, - } => { - panic!("FeeCollector107 not implemented") - } - } - .into(), - ); + } => None, + }; let num_of_transactions_with_account = rosetta_blocks .iter() @@ -1228,9 +1224,7 @@ mod test { IcrcOperation::FeeCollector { fee_collector: _, caller: _, - } => { - panic!("FeeCollector107 not implemented") - } + } => false, // Search for fee collector tx is not supported }) .count(); From ff5258a9e8b848fcbcedc068e66ab1ae548cee5c Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 4 Dec 2025 16:38:09 +0000 Subject: [PATCH 59/85] add fee collector to block strategy --- rs/ledger_suite/icrc1/test_utils/src/lib.rs | 10 ++++ .../src/common/storage/storage_client.rs | 6 +- .../icrc1/src/common/storage/types.rs | 13 +++++ rs/rosetta-api/icrc1/src/common/types.rs | 33 +++++++++++ .../icrc1/src/common/utils/utils.rs | 55 ++++++++++++++++--- 5 files changed, 108 insertions(+), 9 deletions(-) diff --git a/rs/ledger_suite/icrc1/test_utils/src/lib.rs b/rs/ledger_suite/icrc1/test_utils/src/lib.rs index 100445935b35..2aa2fc9df31c 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/lib.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/lib.rs @@ -146,11 +146,21 @@ fn operation_strategy( fee, }); + let fee_collector_strategy = ( + prop::option::of(principal_strategy()), + prop::option::of(account_strategy()), + ) + .prop_map(move |(caller, fee_collector)| Operation::FeeCollector { + fee_collector, + caller, + }); + prop_oneof![ mint_strategy, burn_strategy, transfer_strategy, approve_strategy, + fee_collector_strategy, ] }) } diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs index b5ccef59734b..294b27a88fa2 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs @@ -508,10 +508,12 @@ mod tests { // Duplicate the last transaction generated let duplicate_tx_block = RosettaBlock::from_generic_block(last_block.get_generic_block(), last_block.index + 1).unwrap(); + let count_before = storage_client_memory.get_transactions_by_hash(duplicate_tx_block.clone().get_transaction_hash()).unwrap().len(); storage_client_memory.store_blocks([duplicate_tx_block.clone()].to_vec()).unwrap(); - // The hash of the duplicated transaction should still be the same --> There should be two transactions with the same transaction hash. - assert_eq!(storage_client_memory.get_transactions_by_hash(duplicate_tx_block.clone().get_transaction_hash()).unwrap().len(),2); + // The hash of the duplicated transaction should still be the same --> There should be one more transaction with the same transaction hash. + assert_eq!(storage_client_memory.get_transactions_by_hash(duplicate_tx_block.clone().get_transaction_hash()).unwrap().len(), count_before + 1); + //assert_eq!(storage_client_memory.get_transactions_by_hash(duplicate_tx_block.clone().get_transaction_hash()).unwrap().len(),2, "{}", format!("duplicate_tx_block: {:?}, hash {:?}",duplicate_tx_block, duplicate_tx_block.clone().get_transaction_hash())); } } diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index cabcd4e79e60..325e075d473c 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -1039,6 +1039,19 @@ mod tests { assert_eq!(amount.into(), rosetta_amount, "amount"); assert_eq!(fee.map(|t| t.into()), rosetta_fee, "fee"); } + ( + ic_icrc1::Operation::FeeCollector { + fee_collector, + caller, + }, + IcrcOperation::FeeCollector { + fee_collector: rosetta_fee_collector, + caller: rosetta_caller, + }, + ) => { + assert_eq!(fee_collector, rosetta_fee_collector, "fee_collector"); + assert_eq!(caller, rosetta_caller, "caller"); + } (l, r) => panic!( "Found different type of operations. Operation:{l:?} rosetta's Operation:{r:?}" ), diff --git a/rs/rosetta-api/icrc1/src/common/types.rs b/rs/rosetta-api/icrc1/src/common/types.rs index fa161dfed6f8..b8c0e68f3512 100644 --- a/rs/rosetta-api/icrc1/src/common/types.rs +++ b/rs/rosetta-api/icrc1/src/common/types.rs @@ -1,4 +1,6 @@ use anyhow::Context; +use candid::Principal; +use icrc_ledger_types::icrc1::account::Account; use axum::{Json, http::StatusCode, response::IntoResponse}; use candid::Deserialize; use num_bigint::BigInt; @@ -198,6 +200,7 @@ pub enum OperationType { Spender, Approve, Fee, + FeeCollector, } #[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] @@ -390,3 +393,33 @@ impl TryFrom for FeeMetadata { .context("Could not parse FeeMetadata from JSON object") } } + +#[derive(Clone, Eq, PartialEq, Debug, Deserialize, Serialize)] +pub struct FeeCollectorMetadata { + pub fee_collector: Option, + pub caller: Option, +} + +impl TryFrom for ObjectMap { + type Error = anyhow::Error; + fn try_from(d: FeeCollectorMetadata) -> Result { + match serde_json::to_value(d) { + Ok(v) => match v { + serde_json::Value::Object(ob) => Ok(ob), + _ => anyhow::bail!( + "Could not convert FeeCollectorMetadata to ObjectMap. Expected type Object but received: {:?}", + v + ), + }, + Err(err) => anyhow::bail!("Could not convert FeeCollectorMetadata to ObjectMap: {:?}", err), + } + } +} + +impl TryFrom for FeeCollectorMetadata { + type Error = anyhow::Error; + fn try_from(o: ObjectMap) -> anyhow::Result { + serde_json::from_value(serde_json::Value::Object(o)) + .context("Could not parse FeeCollectorMetadata from JSON object") + } +} \ No newline at end of file diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index f89e0f505aef..d8716cd1a3c1 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -1,5 +1,5 @@ use crate::common::storage::types::{IcrcOperation, RosettaBlock}; -use crate::common::types::{FeeMetadata, FeeSetter}; +use crate::common::types::{FeeMetadata, FeeSetter, FeeCollectorMetadata}; use crate::{ AppState, MultiTokenAppState, common::{ @@ -9,7 +9,8 @@ use crate::{ }, }; use anyhow::{Context, bail}; -use candid::Nat; +use candid::{Principal,Nat}; +use icrc_ledger_types::icrc1::account::Account; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; use num_bigint::BigInt; use rosetta_core::identifiers::*; @@ -149,6 +150,7 @@ pub fn rosetta_core_operations_to_icrc1_operation( Burn, Transfer, Approve, + FeeCollector, } // A builder which helps depict the icrc1 Operation and allows for an arbitrary order of rosetta_core Operations @@ -162,6 +164,8 @@ pub fn rosetta_core_operations_to_icrc1_operation( expected_allowance: Option, expires_at: Option, allowance: Option, + fee_collector: Option, + caller: Option, } impl IcrcOperationBuilder { @@ -176,6 +180,8 @@ pub fn rosetta_core_operations_to_icrc1_operation( expected_allowance: None, expires_at: None, allowance: None, + fee_collector: None, + caller: None, } } @@ -224,6 +230,16 @@ pub fn rosetta_core_operations_to_icrc1_operation( self } + pub fn with_fee_collector(mut self, fee_collector: Option) -> Self { + self.fee_collector = fee_collector; + self + } + + pub fn with_caller(mut self, caller: Option) -> Self { + self.caller = caller; + self + } + pub fn build(self) -> anyhow::Result { Ok(match self.icrc_operation.context("Icrc Operation type needs to be of type Mint, Burn, Transfer or Approve")? { IcrcOperation::Mint => { @@ -267,6 +283,10 @@ pub fn rosetta_core_operations_to_icrc1_operation( expected_allowance: self.expected_allowance, expires_at: self.expires_at, }}, + IcrcOperation::FeeCollector => crate::common::storage::types::IcrcOperation::FeeCollector{ + fee_collector: self.fee_collector, + caller: self.caller, + }, }) } } @@ -347,8 +367,7 @@ pub fn rosetta_core_operations_to_icrc1_operation( OperationType::Fee => { let fee = operation .amount - .context("Amount field needs to be populated for Approve operation")?; - + .context("Amount field needs to be populated for Approve operation")?; // The fee inside of icrc1 operation is always the fee set by the user let icrc1_operation_fee_set = match operation.metadata { Some(metadata) => { @@ -374,6 +393,15 @@ pub fn rosetta_core_operations_to_icrc1_operation( )?; icrc1_operation_builder.with_spender_accountidentifier(spender) } + OperationType::FeeCollector => { + let metadata = operation.metadata.context("metadata should be set for fee collector operations")?; + let fc_metadata = FeeCollectorMetadata::try_from(metadata)?; + icrc1_operation_builder + .with_icrc_operation(IcrcOperation::FeeCollector) + .with_fee_collector(fc_metadata.fee_collector) + .with_caller(fc_metadata.caller) + + } }; } icrc1_operation_builder.build() @@ -607,10 +635,23 @@ pub fn icrc1_operation_to_rosetta_core_operations( } } crate::common::storage::types::IcrcOperation::FeeCollector { - fee_collector: _, - caller: _, + fee_collector, + caller, } => { - // We don't support rosetta core fee collector operations + operations.push(rosetta_core::objects::Operation::new( + 0, + OperationType::FeeCollector.to_string(), + None, + None, + None, + Some( + FeeCollectorMetadata { + fee_collector, + caller, + } + .try_into()?, + ), + )); } }; From b61ec33e34bee951fc6625cde061a1c96dc351a3 Mon Sep 17 00:00:00 2001 From: Maciej Modelski Date: Thu, 4 Dec 2025 17:46:41 +0100 Subject: [PATCH 60/85] formatting --- rs/ledger_suite/icrc1/test_utils/src/lib.rs | 2 +- rs/rosetta-api/icrc1/src/common/storage/types.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/ledger_suite/icrc1/test_utils/src/lib.rs b/rs/ledger_suite/icrc1/test_utils/src/lib.rs index 2aa2fc9df31c..572a7b46800d 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/lib.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/lib.rs @@ -153,7 +153,7 @@ fn operation_strategy( .prop_map(move |(caller, fee_collector)| Operation::FeeCollector { fee_collector, caller, - }); + }); prop_oneof![ mint_strategy, diff --git a/rs/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 325e075d473c..e4e8a0cad1a3 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/types.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/types.rs @@ -1051,7 +1051,7 @@ mod tests { ) => { assert_eq!(fee_collector, rosetta_fee_collector, "fee_collector"); assert_eq!(caller, rosetta_caller, "caller"); - } + } (l, r) => panic!( "Found different type of operations. Operation:{l:?} rosetta's Operation:{r:?}" ), From ee9d5418eca954f2312c747a1d9f6509ad654b1e Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 4 Dec 2025 17:04:40 +0000 Subject: [PATCH 61/85] clippy --- rs/rosetta-api/icrc1/src/common/utils/utils.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index d8716cd1a3c1..cd0be56cde1c 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -283,7 +283,7 @@ pub fn rosetta_core_operations_to_icrc1_operation( expected_allowance: self.expected_allowance, expires_at: self.expires_at, }}, - IcrcOperation::FeeCollector => crate::common::storage::types::IcrcOperation::FeeCollector{ + IcrcOperation::FeeCollector => crate::common::storage::types::IcrcOperation::FeeCollector{ fee_collector: self.fee_collector, caller: self.caller, }, @@ -367,7 +367,7 @@ pub fn rosetta_core_operations_to_icrc1_operation( OperationType::Fee => { let fee = operation .amount - .context("Amount field needs to be populated for Approve operation")?; + .context("Amount field needs to be populated for Approve operation")?; // The fee inside of icrc1 operation is always the fee set by the user let icrc1_operation_fee_set = match operation.metadata { Some(metadata) => { @@ -394,14 +394,15 @@ pub fn rosetta_core_operations_to_icrc1_operation( icrc1_operation_builder.with_spender_accountidentifier(spender) } OperationType::FeeCollector => { - let metadata = operation.metadata.context("metadata should be set for fee collector operations")?; + let metadata = operation + .metadata + .context("metadata should be set for fee collector operations")?; let fc_metadata = FeeCollectorMetadata::try_from(metadata)?; icrc1_operation_builder .with_icrc_operation(IcrcOperation::FeeCollector) .with_fee_collector(fc_metadata.fee_collector) - .with_caller(fc_metadata.caller) - - } + .with_caller(fc_metadata.caller) + } }; } icrc1_operation_builder.build() @@ -651,7 +652,7 @@ pub fn icrc1_operation_to_rosetta_core_operations( } .try_into()?, ), - )); + )); } }; From 84c1536357850e150ca0cc04726411a012db93bb Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 4 Dec 2025 17:11:09 +0000 Subject: [PATCH 62/85] clippy --- rs/rosetta-api/icrc1/src/common/types.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/types.rs b/rs/rosetta-api/icrc1/src/common/types.rs index b8c0e68f3512..0e6d59d09a1f 100644 --- a/rs/rosetta-api/icrc1/src/common/types.rs +++ b/rs/rosetta-api/icrc1/src/common/types.rs @@ -411,7 +411,10 @@ impl TryFrom for ObjectMap { v ), }, - Err(err) => anyhow::bail!("Could not convert FeeCollectorMetadata to ObjectMap: {:?}", err), + Err(err) => anyhow::bail!( + "Could not convert FeeCollectorMetadata to ObjectMap: {:?}", + err + ), } } } @@ -422,4 +425,4 @@ impl TryFrom for FeeCollectorMetadata { serde_json::from_value(serde_json::Value::Object(o)) .context("Could not parse FeeCollectorMetadata from JSON object") } -} \ No newline at end of file +} From 04a9925623b6174a21f256f14a34d735f5183ce4 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 4 Dec 2025 17:12:34 +0000 Subject: [PATCH 63/85] clippy --- rs/rosetta-api/icrc1/src/common/utils/utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index cd0be56cde1c..f4dc41f03aac 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -9,7 +9,7 @@ use crate::{ }, }; use anyhow::{Context, bail}; -use candid::{Principal,Nat}; +use candid::{Nat, Principal}; use icrc_ledger_types::icrc1::account::Account; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; use num_bigint::BigInt; @@ -401,7 +401,7 @@ pub fn rosetta_core_operations_to_icrc1_operation( icrc1_operation_builder .with_icrc_operation(IcrcOperation::FeeCollector) .with_fee_collector(fc_metadata.fee_collector) - .with_caller(fc_metadata.caller) + .with_caller(fc_metadata.caller) } }; } From 6837917b259f3fd41d9b78cbadf6d1ee6d19b765 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 4 Dec 2025 17:14:30 +0000 Subject: [PATCH 64/85] formatting --- rs/rosetta-api/icrc1/src/common/types.rs | 4 ++-- rs/rosetta-api/icrc1/src/common/utils/utils.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/types.rs b/rs/rosetta-api/icrc1/src/common/types.rs index 0e6d59d09a1f..3e62c473636f 100644 --- a/rs/rosetta-api/icrc1/src/common/types.rs +++ b/rs/rosetta-api/icrc1/src/common/types.rs @@ -1,8 +1,8 @@ use anyhow::Context; -use candid::Principal; -use icrc_ledger_types::icrc1::account::Account; use axum::{Json, http::StatusCode, response::IntoResponse}; use candid::Deserialize; +use candid::Principal; +use icrc_ledger_types::icrc1::account::Account; use num_bigint::BigInt; use rosetta_core::identifiers::*; use rosetta_core::objects::*; diff --git a/rs/rosetta-api/icrc1/src/common/utils/utils.rs b/rs/rosetta-api/icrc1/src/common/utils/utils.rs index f4dc41f03aac..6f7699976971 100644 --- a/rs/rosetta-api/icrc1/src/common/utils/utils.rs +++ b/rs/rosetta-api/icrc1/src/common/utils/utils.rs @@ -1,5 +1,5 @@ use crate::common::storage::types::{IcrcOperation, RosettaBlock}; -use crate::common::types::{FeeMetadata, FeeSetter, FeeCollectorMetadata}; +use crate::common::types::{FeeCollectorMetadata, FeeMetadata, FeeSetter}; use crate::{ AppState, MultiTokenAppState, common::{ From 5d0e1b49a78dfb017385ba9f6230deec939108fe Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 5 Dec 2025 10:25:00 +0000 Subject: [PATCH 65/85] fix search transactions test --- rs/rosetta-api/icrc1/src/data_api/services.rs | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/data_api/services.rs b/rs/rosetta-api/icrc1/src/data_api/services.rs index 84ee04993a2c..d309f98f1414 100644 --- a/rs/rosetta-api/icrc1/src/data_api/services.rs +++ b/rs/rosetta-api/icrc1/src/data_api/services.rs @@ -1167,17 +1167,27 @@ mod test { }; // We make sure that the service returns the correct number of transactions for each account - search_transactions_request.account_identifier = - match rosetta_blocks[0].block.transaction.operation { - IcrcOperation::Transfer { from, .. } => Some(from.into()), - IcrcOperation::Mint { to, .. } => Some(to.into()), - IcrcOperation::Burn { from, .. } => Some(from.into()), - IcrcOperation::Approve { from, .. } => Some(from.into()), - IcrcOperation::FeeCollector { - fee_collector: _, - caller: _, - } => None, - }; + for block in &rosetta_blocks { + search_transactions_request.account_identifier = + match block.block.transaction.operation { + IcrcOperation::Transfer { from, .. } => Some(from.into()), + IcrcOperation::Mint { to, .. } => Some(to.into()), + IcrcOperation::Burn { from, .. } => Some(from.into()), + IcrcOperation::Approve { from, .. } => Some(from.into()), + IcrcOperation::FeeCollector { + fee_collector: _, + caller: _, + } => None, + }; + if search_transactions_request.account_identifier.is_some() { + break; + } + } + if search_transactions_request.account_identifier.is_none() { + // Only fee collector blocks found, we cannot search for transactions by accounts. + // This situation is similar to blockchain.is_empty() above. + return Ok(()); + } let num_of_transactions_with_account = rosetta_blocks .iter() @@ -1224,7 +1234,7 @@ mod test { IcrcOperation::FeeCollector { fee_collector: _, caller: _, - } => false, // Search for fee collector tx is not supported + } => false, }) .count(); From e0d7205b3fbc4b8c06b77f1633b5c425c4631f4d Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 5 Dec 2025 10:49:19 +0000 Subject: [PATCH 66/85] change to valid blockchain strategy --- rs/rosetta-api/icrc1/src/common/storage/storage_client.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs index 294b27a88fa2..de06a336f449 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs @@ -405,7 +405,8 @@ mod tests { use ic_icrc1::blocks::encoded_block_to_generic_block; use ic_icrc1::blocks::generic_block_to_encoded_block; use ic_icrc1_test_utils::{ - arb_amount, blocks_strategy, metadata_strategy, valid_blockchain_with_gaps_strategy, + arb_amount, blocks_strategy, metadata_strategy, valid_blockchain_strategy, + valid_blockchain_with_gaps_strategy, }; use ic_icrc1_tokens_u64::U64; use ic_icrc1_tokens_u256::U256; @@ -432,7 +433,7 @@ mod tests { proptest! { #[test] - fn test_read_and_write_blocks_u64(blockchain in prop::collection::vec(blocks_strategy::(arb_amount()),0..5)){ + fn test_read_and_write_blocks_u64(blockchain in valid_blockchain_strategy::(5)){ let storage_client_memory = StorageClient::new_in_memory().unwrap(); let mut rosetta_blocks = vec![]; for (index,block) in blockchain.into_iter().enumerate(){ @@ -459,7 +460,7 @@ mod tests { } #[test] - fn test_read_and_write_blocks_u256(blockchain in prop::collection::vec(blocks_strategy::(arb_amount()),0..5)){ + fn test_read_and_write_blocks_u256(blockchain in valid_blockchain_strategy::(5)){ let storage_client_memory = StorageClient::new_in_memory().unwrap(); let mut rosetta_blocks = vec![]; for (index,block) in blockchain.into_iter().enumerate(){ From 78e974a04880959bc5a29c6aa127ea283e5376f4 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 5 Dec 2025 11:50:19 +0000 Subject: [PATCH 67/85] remove line --- rs/rosetta-api/icrc1/src/common/storage/storage_client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs index de06a336f449..dc04d81c9937 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs @@ -514,7 +514,6 @@ mod tests { // The hash of the duplicated transaction should still be the same --> There should be one more transaction with the same transaction hash. assert_eq!(storage_client_memory.get_transactions_by_hash(duplicate_tx_block.clone().get_transaction_hash()).unwrap().len(), count_before + 1); - //assert_eq!(storage_client_memory.get_transactions_by_hash(duplicate_tx_block.clone().get_transaction_hash()).unwrap().len(),2, "{}", format!("duplicate_tx_block: {:?}, hash {:?}",duplicate_tx_block, duplicate_tx_block.clone().get_transaction_hash())); } } From a8f8a857adb6df34aadf254f24477d748fb0da4c Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 5 Dec 2025 13:28:40 +0000 Subject: [PATCH 68/85] ignore legacy fc test --- rs/rosetta-api/icrc1/tests/system_tests.rs | 134 +++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/rs/rosetta-api/icrc1/tests/system_tests.rs b/rs/rosetta-api/icrc1/tests/system_tests.rs index 378356a261ac..f3b5819202c3 100644 --- a/rs/rosetta-api/icrc1/tests/system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/system_tests.rs @@ -1039,6 +1039,140 @@ fn test_fee_collector_107_smoke() { }); } +#[test] +fn test_fee_collector_107_ignore_legacy() { + let rt = Runtime::new().unwrap(); + let setup = Setup::builder() + .with_custom_ledger_wasm(icrc3_test_ledger()) + .build(); + + rt.block_on(async { + let env = RosettaTestingEnvironmentBuilder::new(&setup).build().await; + + let agent = get_custom_agent(Arc::new(test_identity()), setup.port).await; + + let fc_legacy_account = Account { + owner: PrincipalId::new_user_test_id(111).into(), + subaccount: None, + }; + + let fc_107_account = Account { + owner: PrincipalId::new_user_test_id(107).into(), + subaccount: None, + }; + + let block0 = BlockBuilder::new(0, 0) + .with_fee_collector(fc_legacy_account) + .with_fee(Tokens::from(1u64)) + .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block0) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(0u64)); + + let block_index = + wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 0).await; + assert_eq!(block_index.unwrap(), 0); + + assert_rosetta_balance( + fc_legacy_account, + 0, + 1u64, + &env.rosetta_client, + env.network_identifier.clone(), + ) + .await; + + let block1 = BlockBuilder::::new(1, 1) + .with_btype("107feecol".to_string()) + .with_parent_hash(block0.hash().to_vec()) + .fee_collector(Some(fc_107_account), None, None) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block1) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(1u64)); + + let block2 = BlockBuilder::new(2, 2) + .with_parent_hash(block1.hash().to_vec()) + .with_fee_collector(fc_legacy_account) + .with_fee(Tokens::from(1u64)) + .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block2) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(2u64)); + + let block_index = + wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 2).await; + assert_eq!(block_index.unwrap(), 2); + + assert_rosetta_balance( + fc_107_account, + 2, + 1u64, + &env.rosetta_client, + env.network_identifier.clone(), + ) + .await; + + assert_rosetta_balance( + fc_legacy_account, + 2, + 1u64, + &env.rosetta_client, + env.network_identifier.clone(), + ) + .await; + + let block3 = BlockBuilder::::new(3, 3) + .with_btype("107feecol".to_string()) + .with_parent_hash(block2.hash().to_vec()) + .fee_collector(None, None, None) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block3) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(3u64)); + + let block4 = BlockBuilder::new(4, 4) + .with_fee_collector_block(0) + .with_parent_hash(block3.hash().to_vec()) + .with_fee(Tokens::from(123u64)) + .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) + .build(); + + let block_index = add_block(&agent, &env.icrc1_ledger_id, &block4) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(4u64)); + + assert_rosetta_balance( + fc_107_account, + 4, + 1u64, + &env.rosetta_client, + env.network_identifier.clone(), + ) + .await; + + assert_rosetta_balance( + fc_legacy_account, + 4, + 1u64, + &env.rosetta_client, + env.network_identifier.clone(), + ) + .await; + }); +} + const NUM_BLOCKS: u64 = 6; async fn verify_unrecognized_block_handling(setup: &Setup, bad_block_index: u64) { let mut log_file = NamedTempFile::new().expect("failed to create a temp file"); From 76cb1d38f2ed83beb34e5ad5999e899a28c3c507 Mon Sep 17 00:00:00 2001 From: Maciej Modelski Date: Sat, 6 Dec 2025 17:11:50 +0100 Subject: [PATCH 69/85] use separate fee collector table instead of index on operation_type --- .../icrc1/src/common/storage/schema.rs | 21 ++++++++++++------- .../src/common/storage/storage_operations.rs | 18 ++++++++++++---- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/schema.rs b/rs/rosetta-api/icrc1/src/common/storage/schema.rs index 4b1d2e84c317..58d4ac76581f 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/schema.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/schema.rs @@ -91,6 +91,19 @@ pub fn create_tables(connection: &Connection) -> Result<()> { [], )?; + // Fee Collectors 107 table + connection.execute( + r#" + CREATE TABLE IF NOT EXISTS fee_collectors_107 ( + block_idx INTEGER NOT NULL PRIMARY KEY, + caller_principal BLOB, + fee_collector_principal BLOB, + fee_collector_subaccount BLOB + ) + "#, + [], + )?; + create_indexes(connection) } @@ -120,13 +133,5 @@ pub fn create_indexes(connection: &Connection) -> Result<()> { [], )?; - connection.execute( - r#" - CREATE INDEX IF NOT EXISTS block_operation_type - ON blocks(operation_type) - "#, - [], - )?; - Ok(()) } diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 2f8d66a71636..4f610c33ec5d 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -559,10 +559,10 @@ pub fn store_blocks( caller, } => ( "107feecol", - caller, None, - fee_collector.map(|fc| fc.owner), - fee_collector.map(|fc| *fc.effective_subaccount()), + None, + None, + None, None, None, Nat::from(0u64), @@ -605,6 +605,16 @@ pub fn store_blocks( ":transaction_created_at_time":transaction_created_at_time_i64, ":approval_expires_at":approval_expires_at_i64 })?; + if operation_type == "107feecol" { + insert_tx.prepare_cached( + "INSERT OR IGNORE INTO fee_collectors_107 (block_idx, caller_principal, fee_collector_principal, fee_collector_subaccount) VALUES (:block_idx, :caller_principal, :fee_collector_principal, :fee_collector_subaccount)")? + .execute(named_params! { + ":block_idx":rosetta_block.index, + ":caller_principal":from_principal.map(|x| x.as_slice().to_vec()), + ":fee_collector_principal":to_principal.map(|x| x.as_slice().to_vec()), + ":fee_collector_subaccount":to_subaccount, + })?; + } } insert_tx.commit()?; Ok(()) @@ -654,7 +664,7 @@ pub fn get_fee_collector_107_for_idx( block_idx: u64, ) -> anyhow::Result>> { let command = format!( - "SELECT to_principal,to_subaccount FROM blocks WHERE idx <= {block_idx} AND operation_type = '107feecol' ORDER BY idx DESC LIMIT 1" + "SELECT fee_collector_principal,fee_collector_subaccount FROM fee_collectors_107 WHERE block_idx < {block_idx} ORDER BY block_idx DESC LIMIT 1" ); let mut stmt = connection.prepare_cached(&command)?; read_single_fee_collector_107(&mut stmt, params![]) From b9aa45935f317ff06a2ec85da62559113e4866d5 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Sat, 6 Dec 2025 16:22:30 +0000 Subject: [PATCH 70/85] fix, record to and from pricipals --- .../icrc1/src/common/storage/storage_operations.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 4f610c33ec5d..2ef59dda8a26 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -559,10 +559,10 @@ pub fn store_blocks( caller, } => ( "107feecol", + caller, None, - None, - None, - None, + fee_collector.map(|fc| fc.owner), + fee_collector.map(|fc| *fc.effective_subaccount()), None, None, Nat::from(0u64), From 19f0491b465362767541a96226a7f6a51aae5c5f Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Sun, 7 Dec 2025 15:57:03 +0000 Subject: [PATCH 71/85] improve tests --- rs/rosetta-api/icrc1/tests/system_tests.rs | 397 +++++++++------------ 1 file changed, 161 insertions(+), 236 deletions(-) diff --git a/rs/rosetta-api/icrc1/tests/system_tests.rs b/rs/rosetta-api/icrc1/tests/system_tests.rs index f3b5819202c3..7a9bcf8e4f22 100644 --- a/rs/rosetta-api/icrc1/tests/system_tests.rs +++ b/rs/rosetta-api/icrc1/tests/system_tests.rs @@ -882,165 +882,115 @@ fn test_error_backoff() { }); } -#[test] -fn test_sync_to_fee_collector_107_block() { - let rt = Runtime::new().unwrap(); - let setup = Setup::builder() - .with_custom_ledger_wasm(icrc3_test_ledger()) +async fn set_fee_col_107( + agent: &Agent, + env: &RosettaTestingEnvironment, + ledger_canister_id: &Principal, + start_index: u64, + prev_block_hash: Option>, + fee_col: Option, +) -> (u64, Vec) { + let mut builder = BlockBuilder::::new(start_index, start_index); + if let Some(prev_block_hash) = prev_block_hash { + builder = builder.with_parent_hash(prev_block_hash); + } + let fee_col_block = builder + .with_btype("107feecol".to_string()) + .fee_collector(fee_col, None, None) .build(); + let block_index = add_block(agent, ledger_canister_id, &fee_col_block) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(start_index)); - rt.block_on(async { - let env = RosettaTestingEnvironmentBuilder::new(&setup).build().await; - - let agent = get_custom_agent(Arc::new(test_identity()), setup.port).await; - - let fc_account = Account { - owner: PrincipalId::new_user_test_id(12).into(), - subaccount: None, - }; - - let block0 = BlockBuilder::::new(0, 0) - .with_btype("107feecol".to_string()) - .fee_collector(Some(fc_account), None, None) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block0) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(0u64)); - - let block_index = - wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 0).await; - assert_eq!(block_index.unwrap(), 0); - - let block1 = BlockBuilder::new(2, 2) - .with_parent_hash(block0.hash().to_vec()) - .with_fee(Tokens::from(123u64)) - .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block1) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(1u64)); - - let block2 = BlockBuilder::::new(1, 1) - .with_btype("107feecol".to_string()) - .with_parent_hash(block1.hash().to_vec()) - .fee_collector(Some(fc_account), None, None) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block2) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(2u64)); + let latest_rosetta_block = wait_for_rosetta_block( + &env.rosetta_client, + env.network_identifier.clone(), + start_index, + ) + .await + .expect("Unable to call wait_for_rosetta_block"); + assert_eq!(latest_rosetta_block, start_index); - let block_index = - wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 2).await; - assert_eq!(block_index.unwrap(), 2); - }); + (start_index + 1, fee_col_block.hash().to_vec()) } -#[test] -fn test_fee_collector_107_smoke() { - let rt = Runtime::new().unwrap(); - let setup = Setup::builder() - .with_custom_ledger_wasm(icrc3_test_ledger()) +async fn transfer_and_check_collected_fees( + agent: &Agent, + env: &RosettaTestingEnvironment, + ledger_canister_id: &Principal, + start_index: u64, + prev_block_hash: Option>, + legacy_fee_col: Account, + expected_balances: Vec<(Account, u64)>, +) -> (u64, Vec) { + let mut idx = start_index; + let mut builder = BlockBuilder::new(idx, idx); + if let Some(prev_block_hash) = prev_block_hash { + builder = builder.with_parent_hash(prev_block_hash); + } + let mint = builder + .with_fee(Tokens::from(1u64)) + .with_fee_collector(legacy_fee_col) + .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) .build(); + let block_index = add_block(agent, ledger_canister_id, &mint) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(idx)); + idx += 1; + + let transfer = BlockBuilder::new(idx, idx) + .with_parent_hash(mint.hash().to_vec()) + .with_fee_collector_block(start_index) + .with_fee(Tokens::from(1u64)) + .transfer(*TEST_ACCOUNT, *TEST_ACCOUNT, Tokens::from(1u64)) + .build(); + let block_index = add_block(agent, ledger_canister_id, &transfer) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(idx)); + idx += 1; + + let approve = BlockBuilder::new(idx, idx) + .with_parent_hash(transfer.hash().to_vec()) + .with_fee_collector_block(start_index) + .with_fee(Tokens::from(1u64)) + .approve(*TEST_ACCOUNT, *TEST_ACCOUNT, Tokens::from(u64::MAX)) + .build(); + let block_index = add_block(agent, ledger_canister_id, &approve) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(idx)); + idx += 1; + + let transfer_from = BlockBuilder::new(idx, idx) + .with_parent_hash(approve.hash().to_vec()) + .with_fee(Tokens::from(1u64)) + .transfer(*TEST_ACCOUNT, *TEST_ACCOUNT, Tokens::from(1u64)) + .with_spender(*TEST_ACCOUNT) + .build(); + let block_index = add_block(agent, ledger_canister_id, &transfer_from) + .await + .expect("failed to add block"); + assert_eq!(block_index, Nat::from(idx)); - rt.block_on(async { - let env = RosettaTestingEnvironmentBuilder::new(&setup).build().await; - - let agent = get_custom_agent(Arc::new(test_identity()), setup.port).await; - - let fc_account = Account { - owner: PrincipalId::new_user_test_id(12).into(), - subaccount: None, - }; - - let block0 = BlockBuilder::new(0, 0) - .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block0) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(0u64)); - - let block_index = - wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 0).await; - assert_eq!(block_index.unwrap(), 0); - - let block1 = BlockBuilder::::new(1, 1) - .with_btype("107feecol".to_string()) - .with_parent_hash(block0.hash().to_vec()) - .fee_collector(Some(fc_account), None, None) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block1) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(1u64)); - - let block2 = BlockBuilder::new(2, 2) - .with_parent_hash(block1.hash().to_vec()) - .with_fee(Tokens::from(123u64)) - .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block2) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(2u64)); - - let block_index = - wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 2).await; - assert_eq!(block_index.unwrap(), 2); - + for (account, balance) in expected_balances { assert_rosetta_balance( - fc_account, - 2, - 123u64, + account, + idx, + balance, &env.rosetta_client, env.network_identifier.clone(), ) .await; + } - let block3 = BlockBuilder::::new(3, 3) - .with_btype("107feecol".to_string()) - .with_parent_hash(block2.hash().to_vec()) - .fee_collector(None, None, None) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block3) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(3u64)); - - let block4 = BlockBuilder::new(4, 4) - .with_parent_hash(block3.hash().to_vec()) - .with_fee(Tokens::from(123u64)) - .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block4) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(4u64)); - - assert_rosetta_balance( - fc_account, - 4, - 123u64, - &env.rosetta_client, - env.network_identifier.clone(), - ) - .await; - }); + (idx + 1, transfer_from.hash().to_vec()) } #[test] -fn test_fee_collector_107_ignore_legacy() { +fn test_fee_collector_107() { let rt = Runtime::new().unwrap(); let setup = Setup::builder() .with_custom_ledger_wasm(icrc3_test_ledger()) @@ -1051,123 +1001,98 @@ fn test_fee_collector_107_ignore_legacy() { let agent = get_custom_agent(Arc::new(test_identity()), setup.port).await; - let fc_legacy_account = Account { + let fc_legacy = Account { owner: PrincipalId::new_user_test_id(111).into(), subaccount: None, }; - let fc_107_account = Account { + let fc_107 = Account { owner: PrincipalId::new_user_test_id(107).into(), subaccount: None, }; - let block0 = BlockBuilder::new(0, 0) - .with_fee_collector(fc_legacy_account) - .with_fee(Tokens::from(1u64)) - .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block0) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(0u64)); - - let block_index = - wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 0).await; - assert_eq!(block_index.unwrap(), 0); - - assert_rosetta_balance( - fc_legacy_account, + // There are 4 transactions with fees, but approve fees are not collected + // by the legacy fee collector and transfer from does not specify + // the fee collector or its index. + let (block_index, block_hash) = transfer_and_check_collected_fees( + &agent, + &env, + &env.icrc1_ledger_id, 0, - 1u64, - &env.rosetta_client, - env.network_identifier.clone(), + None, + fc_legacy, + [(fc_legacy, 2), (fc_107, 0)].to_vec(), ) .await; - let block1 = BlockBuilder::::new(1, 1) - .with_btype("107feecol".to_string()) - .with_parent_hash(block0.hash().to_vec()) - .fee_collector(Some(fc_107_account), None, None) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block1) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(1u64)); - - let block2 = BlockBuilder::new(2, 2) - .with_parent_hash(block1.hash().to_vec()) - .with_fee_collector(fc_legacy_account) - .with_fee(Tokens::from(1u64)) - .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block2) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(2u64)); - - let block_index = - wait_for_rosetta_block(&env.rosetta_client, env.network_identifier.clone(), 2).await; - assert_eq!(block_index.unwrap(), 2); - - assert_rosetta_balance( - fc_107_account, - 2, - 1u64, - &env.rosetta_client, - env.network_identifier.clone(), + let (block_index, block_hash) = set_fee_col_107( + &agent, + &env, + &env.icrc1_ledger_id, + block_index, + Some(block_hash), + Some(fc_107), ) .await; - assert_rosetta_balance( - fc_legacy_account, - 2, - 1u64, - &env.rosetta_client, - env.network_identifier.clone(), + // The 107 fee collector should collect all 4 fees. + let (block_index, block_hash) = transfer_and_check_collected_fees( + &agent, + &env, + &env.icrc1_ledger_id, + block_index, + Some(block_hash), + fc_legacy, + [(fc_legacy, 2), (fc_107, 4)].to_vec(), ) .await; - let block3 = BlockBuilder::::new(3, 3) - .with_btype("107feecol".to_string()) - .with_parent_hash(block2.hash().to_vec()) - .fee_collector(None, None, None) - .build(); - - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block3) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(3u64)); + let (block_index, block_hash) = set_fee_col_107( + &agent, + &env, + &env.icrc1_ledger_id, + block_index, + Some(block_hash), + None, + ) + .await; - let block4 = BlockBuilder::new(4, 4) - .with_fee_collector_block(0) - .with_parent_hash(block3.hash().to_vec()) - .with_fee(Tokens::from(123u64)) - .mint(*TEST_ACCOUNT, Tokens::from(1_000u64)) - .build(); + // Fee collecor was set to None, no fees should be collectes. + let (block_index, block_hash) = transfer_and_check_collected_fees( + &agent, + &env, + &env.icrc1_ledger_id, + block_index, + Some(block_hash), + fc_legacy, + [(fc_legacy, 2), (fc_107, 4)].to_vec(), + ) + .await; - let block_index = add_block(&agent, &env.icrc1_ledger_id, &block4) - .await - .expect("failed to add block"); - assert_eq!(block_index, Nat::from(4u64)); + let fc_107_new = Account { + owner: PrincipalId::new_user_test_id(108).into(), + subaccount: None, + }; - assert_rosetta_balance( - fc_107_account, - 4, - 1u64, - &env.rosetta_client, - env.network_identifier.clone(), + let (block_index, block_hash) = set_fee_col_107( + &agent, + &env, + &env.icrc1_ledger_id, + block_index, + Some(block_hash), + Some(fc_107_new), ) .await; - assert_rosetta_balance( - fc_legacy_account, - 4, - 1u64, - &env.rosetta_client, - env.network_identifier.clone(), + // The new fee collector should collect all fees. + transfer_and_check_collected_fees( + &agent, + &env, + &env.icrc1_ledger_id, + block_index, + Some(block_hash), + fc_legacy, + [(fc_legacy, 2), (fc_107, 4), (fc_107_new, 4)].to_vec(), ) .await; }); From f81748843e00b5c18c03751663d6b5cd45a73754 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 16:27:37 +0000 Subject: [PATCH 72/85] store fee collector information in rosetta_metadata table --- .../icrc1/src/common/storage/schema.rs | 13 +- .../src/common/storage/storage_operations.rs | 148 ++++++++---------- 2 files changed, 67 insertions(+), 94 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/schema.rs b/rs/rosetta-api/icrc1/src/common/storage/schema.rs index 58d4ac76581f..66b97e2aa7ff 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/schema.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/schema.rs @@ -91,15 +91,14 @@ pub fn create_tables(connection: &Connection) -> Result<()> { [], )?; - // Fee Collectors 107 table + // Rosetta metadata table. Meant to store values like `synced_block_height`. + // The `metadata` table defined above stores the ICRC1 token metadata. connection.execute( r#" - CREATE TABLE IF NOT EXISTS fee_collectors_107 ( - block_idx INTEGER NOT NULL PRIMARY KEY, - caller_principal BLOB, - fee_collector_principal BLOB, - fee_collector_subaccount BLOB - ) + CREATE TABLE IF NOT EXISTS rosetta_metadata ( + key TEXT PRIMARY KEY, + value BLOB NOT NULL + ); "#, [], )?; diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 2ef59dda8a26..b8e0675dce16 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -1,11 +1,11 @@ use crate::MetadataEntry; use crate::common::storage::types::{RosettaBlock, RosettaCounter}; use anyhow::{Context, bail}; -use candid::{Nat, Principal}; +use candid::Nat; use ic_base_types::PrincipalId; use ic_ledger_core::tokens::Zero; use ic_ledger_core::tokens::{CheckedAdd, CheckedSub}; -use icrc_ledger_types::icrc1::account::{Account, Subaccount}; +use icrc_ledger_types::icrc1::account::Account; use num_bigint::BigUint; use rusqlite::Connection; use rusqlite::{CachedStatement, Params, named_params, params}; @@ -14,6 +14,8 @@ use std::collections::{BTreeMap, HashMap}; use std::str::FromStr; use tracing::{info, trace}; +const METADATA_FEE_COL: &str = "fee_collector_107"; + /// Gets the current value of a counter from the database. /// Returns None if the counter doesn't exist. pub fn get_counter_value( @@ -107,17 +109,17 @@ pub fn initialize_counter_if_missing( } // Helper function to resolve the fee collector account from a block -pub fn get_fee_collector_from_block( +pub fn get_107_fee_collector_or_legacy( rosetta_block: &RosettaBlock, connection: &Connection, + fee_collector_107: Option>, ) -> anyhow::Result> { // First check if we have a 107 fee collector - if let Some(fee_collector_107) = get_fee_collector_107_for_idx(connection, rosetta_block.index)? - { + if let Some(fee_collector_107) = fee_collector_107 { return Ok(fee_collector_107); } - // FeeCollector 107 does not apply to this block, see if there is a legacy fee collector + // There is not 107 fee collector, check legacy fee collector in the block // Check if the fee collector is directly specified in the block if let Some(fee_collector) = rosetta_block.get_fee_collector() { @@ -178,6 +180,27 @@ pub fn get_metadata(connection: &Connection) -> anyhow::Result anyhow::Result>> { + let mut stmt_metadata = connection.prepare_cached(&format!( + "SELECT value FROM rosetta_metadata WHERE key = '{key}'" + ))?; + let rows = stmt_metadata.query_map(params![], |row| Ok(row.get(0)?))?; + let mut result = vec![]; + for row in rows { + let entry: Vec = row?; + result.push(entry); + } + if result.len() == 1 { + Ok(Some(result[0].clone())) + } else if result.is_empty() { + // Return None if no metadata entry found + Ok(None) + } else { + // If more than one metadata entry was found return an error + bail!(format!("Multiple metadata entries found for key: {key}")) + } +} + pub fn update_account_balances( connection: &mut Connection, flush_cache_and_shrink_memory: bool, @@ -275,6 +298,15 @@ pub fn update_account_balances( let mut account_balances_cache: HashMap> = HashMap::new(); let mut dummy_principals = vec![]; + let mut current_fee_collector_107 = match get_rosetta_metadata(connection, METADATA_FEE_COL)? { + Some(value) => { + let fc: Option = candid::decode_one(&value)?; + Some(fc) + } + None => None, + }; + let collector_before = current_fee_collector_107; + // As long as there are blocks to be fetched, keep on iterating over the blocks in the database with the given BATCH_SIZE interval while !rosetta_blocks.is_empty() { for rosetta_block in rosetta_blocks { @@ -299,9 +331,11 @@ pub fn update_account_balances( connection, &mut account_balances_cache, )?; - if let Some(collector) = - get_fee_collector_from_block(&rosetta_block, connection)? - { + if let Some(collector) = get_107_fee_collector_or_legacy( + &rosetta_block, + connection, + current_fee_collector_107, + )? { credit( collector, fee, @@ -326,9 +360,11 @@ pub fn update_account_balances( connection, &mut account_balances_cache, )?; - if let Some(collector) = - get_fee_collector_from_block(&rosetta_block, connection)? - { + if let Some(collector) = get_107_fee_collector_or_legacy( + &rosetta_block, + connection, + current_fee_collector_107, + )? { credit( collector, fee, @@ -357,9 +393,7 @@ pub fn update_account_balances( &mut account_balances_cache, )?; - if let Some(Some(collector)) = - get_fee_collector_107_for_idx(connection, rosetta_block.index)? - { + if let Some(Some(collector)) = current_fee_collector_107 { credit( collector, fee, @@ -399,9 +433,11 @@ pub fn update_account_balances( &mut account_balances_cache, )?; - if let Some(collector) = - get_fee_collector_from_block(&rosetta_block, connection)? - { + if let Some(collector) = get_107_fee_collector_or_legacy( + &rosetta_block, + connection, + current_fee_collector_107, + )? { credit( collector, fee, @@ -412,13 +448,14 @@ pub fn update_account_balances( } } crate::common::storage::types::IcrcOperation::FeeCollector { - fee_collector: _, + fee_collector, caller: _, } => { // Since we use the account_balances table to determine the synced height // and since there is no credit or debit operation for the fee collector block, // we have to add a dummy principal with the block index, to indicate the synced height dummy_principals.push((rosetta_block.index, [107u8; 30])); + current_fee_collector_107 = Some(fee_collector); } } } @@ -447,6 +484,11 @@ pub fn update_account_balances( ":amount": "0", })?; } + if collector_before != current_fee_collector_107 + && let Some(collector) = current_fee_collector_107 + { + insert_tx.prepare_cached("INSERT INTO rosetta_metadata (key, value) VALUES (?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value;")?.execute(params![METADATA_FEE_COL, candid::encode_one(collector)?])?; + } insert_tx.commit()?; if flush_cache_and_shrink_memory { @@ -605,16 +647,6 @@ pub fn store_blocks( ":transaction_created_at_time":transaction_created_at_time_i64, ":approval_expires_at":approval_expires_at_i64 })?; - if operation_type == "107feecol" { - insert_tx.prepare_cached( - "INSERT OR IGNORE INTO fee_collectors_107 (block_idx, caller_principal, fee_collector_principal, fee_collector_subaccount) VALUES (:block_idx, :caller_principal, :fee_collector_principal, :fee_collector_subaccount)")? - .execute(named_params! { - ":block_idx":rosetta_block.index, - ":caller_principal":from_principal.map(|x| x.as_slice().to_vec()), - ":fee_collector_principal":to_principal.map(|x| x.as_slice().to_vec()), - ":fee_collector_subaccount":to_subaccount, - })?; - } } insert_tx.commit()?; Ok(()) @@ -658,18 +690,6 @@ fn get_block_at_next_idx( read_single_block(&mut stmt, params![]) } -// Returns ICRC-107 fee collector that has effect on block at `block_idx` -pub fn get_fee_collector_107_for_idx( - connection: &Connection, - block_idx: u64, -) -> anyhow::Result>> { - let command = format!( - "SELECT fee_collector_principal,fee_collector_subaccount FROM fee_collectors_107 WHERE block_idx < {block_idx} ORDER BY block_idx DESC LIMIT 1" - ); - let mut stmt = connection.prepare_cached(&command)?; - read_single_fee_collector_107(&mut stmt, params![]) -} - // Returns a RosettaBlock if the block hash exists in the database, else returns None. // Returns an Error if the query fails. pub fn get_block_by_hash( @@ -915,52 +935,6 @@ where Ok(result) } -fn read_single_fee_collector_107

( - stmt: &mut CachedStatement, - params: P, -) -> anyhow::Result>> -where - P: Params, -{ - let fcs: Vec> = read_fee_collectors_107(stmt, params)?; - if fcs.len() == 1 { - // Return the fee collector if only one was found - Ok(Some(fcs[0])) - } else if fcs.is_empty() { - // Return None if no fee collector was found - Ok(None) - } else { - // If more than one fee collector was found return an error - bail!("Multiple fee collectors found with given parameters".to_owned(),) - } -} - -// Executes the constructed statement that reads the 107 fee collectors. -fn read_fee_collectors_107

( - stmt: &mut CachedStatement, - params: P, -) -> anyhow::Result>> -where - P: Params, -{ - let fee_collectors = stmt.query_map(params, |row| { - let owner_bytes: Option> = row.get(0)?; - let subaccount: Option = row.get(1)?; - match owner_bytes { - Some(owner_bytes) => Ok(Some(Account { - owner: Principal::from_slice(&owner_bytes), - subaccount, - })), - None => Ok(None), - } - })?; - let mut result = vec![]; - for fc in fee_collectors { - result.push(fc?); - } - Ok(result) -} - /// Repairs account balances for databases created before the fee collector block index fix. /// This function clears the account_balances table and rebuilds it from scratch using the /// corrected fee collector resolution logic by reprocessing all blocks. From 197b78110cbc8ae1c2ec863632cd3187a87825c3 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 16:51:20 +0000 Subject: [PATCH 73/85] store highest processed block index in metadata --- .../src/common/storage/storage_client.rs | 4 +-- .../src/common/storage/storage_operations.rs | 34 +++++-------------- rs/rosetta-api/icrc1/src/data_api/services.rs | 4 +-- 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs index dc04d81c9937..bd7a4fde05aa 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs @@ -337,9 +337,9 @@ impl StorageClient { /// Retrieves the highest block index in the account balance table. /// Returns None if the account balance table is empty. - pub fn get_highest_block_idx_in_account_balance_table(&self) -> Result> { + pub fn get_highest_processed_block_idx(&self) -> Result> { let open_connection = self.storage_connection.lock().unwrap(); - storage_operations::get_highest_block_idx_in_account_balance_table(&open_connection) + storage_operations::get_highest_processed_block_idx(&open_connection) } // Retrieves the account balance at a certain block height diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index b8e0675dce16..7a436949c27e 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -15,6 +15,7 @@ use std::str::FromStr; use tracing::{info, trace}; const METADATA_FEE_COL: &str = "fee_collector_107"; +const METADATA_BLOCK_IDX: &str = "highest_processed_block_index"; /// Gets the current value of a counter from the database. /// Returns None if the counter doesn't exist. @@ -281,7 +282,7 @@ pub fn update_account_balances( // The next block to be updated is the highest block index in the account balance table + 1 if the table is not empty and 0 otherwise let next_block_to_be_updated = - get_highest_block_idx_in_account_balance_table(connection)?.map_or(0, |idx| idx + 1); + get_highest_processed_block_idx(connection)?.map_or(0, |idx| idx + 1); let highest_block_idx = get_block_with_highest_block_idx(connection)?.map_or(0, |block| block.index); @@ -296,7 +297,6 @@ pub fn update_account_balances( // For faster inserts, keep a cache of the account balances within a batch range in memory // This also makes the inserting of the account balances batchable and therefore faster let mut account_balances_cache: HashMap> = HashMap::new(); - let mut dummy_principals = vec![]; let mut current_fee_collector_107 = match get_rosetta_metadata(connection, METADATA_FEE_COL)? { Some(value) => { @@ -309,6 +309,7 @@ pub fn update_account_balances( // As long as there are blocks to be fetched, keep on iterating over the blocks in the database with the given BATCH_SIZE interval while !rosetta_blocks.is_empty() { + let last_block_index_bytes = rosetta_blocks.last().unwrap().index.to_le_bytes(); for rosetta_block in rosetta_blocks { match rosetta_block.get_transaction().operation { crate::common::storage::types::IcrcOperation::Burn { @@ -451,10 +452,6 @@ pub fn update_account_balances( fee_collector, caller: _, } => { - // Since we use the account_balances table to determine the synced height - // and since there is no credit or debit operation for the fee collector block, - // we have to add a dummy principal with the block index, to indicate the synced height - dummy_principals.push((rosetta_block.index, [107u8; 30])); current_fee_collector_107 = Some(fee_collector); } } @@ -474,21 +471,12 @@ pub fn update_account_balances( })?; } } - for (block_idx, principal) in &dummy_principals { - insert_tx - .prepare_cached("INSERT INTO account_balances (block_idx, principal, subaccount, amount) VALUES (:block_idx, :principal, :subaccount, :amount)")? - .execute(named_params! { - ":block_idx": block_idx, - ":principal": principal, - ":subaccount": [0u8;32], - ":amount": "0", - })?; - } if collector_before != current_fee_collector_107 && let Some(collector) = current_fee_collector_107 { insert_tx.prepare_cached("INSERT INTO rosetta_metadata (key, value) VALUES (?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value;")?.execute(params![METADATA_FEE_COL, candid::encode_one(collector)?])?; } + insert_tx.prepare_cached("INSERT INTO rosetta_metadata (key, value) VALUES (?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value;")?.execute(params![METADATA_BLOCK_IDX, last_block_index_bytes])?; insert_tx.commit()?; if flush_cache_and_shrink_memory { @@ -498,7 +486,7 @@ pub fn update_account_balances( } // Fetch the next batch of blocks - batch_start_idx = get_highest_block_idx_in_account_balance_table(connection)? + batch_start_idx = get_highest_processed_block_idx(connection)? .context("No blocks in account balance table after inserting")? + 1; batch_end_idx = batch_start_idx + batch_size; @@ -764,16 +752,10 @@ pub fn get_blocks_by_transaction_hash( read_blocks(&mut stmt, params![hash.as_slice().to_vec()]) } -pub fn get_highest_block_idx_in_account_balance_table( - connection: &Connection, -) -> anyhow::Result> { - match connection - .prepare_cached("SELECT block_idx FROM account_balances WHERE block_idx = (SELECT MAX(block_idx) FROM account_balances)")? - .query_map(params![], |row| row.get(0))? - .next() - { +pub fn get_highest_processed_block_idx(connection: &Connection) -> anyhow::Result> { + match get_rosetta_metadata(connection, METADATA_BLOCK_IDX)? { + Some(value) => Ok(Some(u64::from_le_bytes(value.as_slice().try_into()?))), None => Ok(None), - Some(res) => Ok(res?), } } diff --git a/rs/rosetta-api/icrc1/src/data_api/services.rs b/rs/rosetta-api/icrc1/src/data_api/services.rs index d309f98f1414..015993dea61b 100644 --- a/rs/rosetta-api/icrc1/src/data_api/services.rs +++ b/rs/rosetta-api/icrc1/src/data_api/services.rs @@ -86,7 +86,7 @@ pub fn network_options(ledger_id: &Principal) -> NetworkOptionsResponse { pub fn network_status(storage_client: &StorageClient) -> Result { let highest_processed_block = storage_client - .get_highest_block_idx_in_account_balance_table() + .get_highest_processed_block_idx() .map_err(|e| Error::unable_to_find_block(&e))? .ok_or_else(|| { Error::unable_to_find_block(&"Highest processed block not found".to_owned()) @@ -538,7 +538,7 @@ pub fn initial_sync_is_completed( synched.unwrap() } else { let block_count = storage_client.get_block_count(); - let highest_index = storage_client.get_highest_block_idx_in_account_balance_table(); + let highest_index = storage_client.get_highest_processed_block_idx(); *synched = Some(match (block_count, highest_index) { // If the blockchain contains no blocks we mark it as not completed (Ok(block_count), Ok(Some(highest_index))) if block_count == highest_index + 1 => true, From c7eda701aae1c86f3445bc7e823c279bede60218 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 16:54:57 +0000 Subject: [PATCH 74/85] clippy --- rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 7a436949c27e..a36e225e85e5 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -185,7 +185,7 @@ pub fn get_rosetta_metadata(connection: &Connection, key: &str) -> anyhow::Resul let mut stmt_metadata = connection.prepare_cached(&format!( "SELECT value FROM rosetta_metadata WHERE key = '{key}'" ))?; - let rows = stmt_metadata.query_map(params![], |row| Ok(row.get(0)?))?; + let rows = stmt_metadata.query_map(params![], |row| row.get(0))?; let mut result = vec![]; for row in rows { let entry: Vec = row?; From fb40fe29ac11f900280114c0d943f4482c0be243 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 17:03:47 +0000 Subject: [PATCH 75/85] build fix --- .../src/common/storage/storage_operations_test.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs index 1023d8b6681b..5b50cd915a7c 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs @@ -419,23 +419,26 @@ fn test_fee_collector_resolution_and_repair() -> anyhow::Result<()> { // Test fee collector resolution assert_eq!( - get_fee_collector_from_block(&block1, &connection)?, + get_107_fee_collector_or_legacy(&block1, &connection, None)?, Some(fee_collector_account) ); assert_eq!( - get_fee_collector_from_block(&block2, &connection)?, + get_107_fee_collector_or_legacy(&block2, &connection, None)?, Some(fee_collector_account) ); - assert_eq!(get_fee_collector_from_block(&block3, &connection)?, None); + assert_eq!( + get_107_fee_collector_or_legacy(&block3, &connection, None)?, + None + ); // Test error cases (without storing invalid blocks in DB to avoid repair conflicts) let mut invalid_block = create_test_rosetta_block(999, 1000000003, &principal1, 400); invalid_block.block.fee_collector_block_index = Some(999); // Non-existent - assert!(get_fee_collector_from_block(&invalid_block, &connection).is_err()); + assert!(get_107_fee_collector_or_legacy(&invalid_block, &connection, None).is_err()); let mut invalid_block2 = create_test_rosetta_block(998, 1000000004, &principal1, 500); invalid_block2.block.fee_collector_block_index = Some(3); // Block with no fee collector - assert!(get_fee_collector_from_block(&invalid_block2, &connection).is_err()); + assert!(get_107_fee_collector_or_legacy(&invalid_block2, &connection, None).is_err()); // Test 2: Repair functionality with broken state simulation // Manually create broken balances (missing fee collector credits for block 2) From 75798c6492d1020e35be443bf0731eb9aad87136 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 17:13:04 +0000 Subject: [PATCH 76/85] small refactor --- .../icrc1/src/common/storage/storage_operations.rs | 10 ++++++++-- .../src/common/storage/storage_operations_test.rs | 13 +++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index a36e225e85e5..7832ff198481 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -109,7 +109,6 @@ pub fn initialize_counter_if_missing( Ok(()) } -// Helper function to resolve the fee collector account from a block pub fn get_107_fee_collector_or_legacy( rosetta_block: &RosettaBlock, connection: &Connection, @@ -121,8 +120,15 @@ pub fn get_107_fee_collector_or_legacy( } // There is not 107 fee collector, check legacy fee collector in the block + get_fee_collector_from_block(rosetta_block, connection) +} - // Check if the fee collector is directly specified in the block +// Helper function to resolve the fee collector account from a block +pub fn get_fee_collector_from_block( + rosetta_block: &RosettaBlock, + connection: &Connection, +) -> anyhow::Result> { + // First check if the fee collector is directly specified in the block if let Some(fee_collector) = rosetta_block.get_fee_collector() { return Ok(Some(fee_collector)); } diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs index 5b50cd915a7c..1023d8b6681b 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs @@ -419,26 +419,23 @@ fn test_fee_collector_resolution_and_repair() -> anyhow::Result<()> { // Test fee collector resolution assert_eq!( - get_107_fee_collector_or_legacy(&block1, &connection, None)?, + get_fee_collector_from_block(&block1, &connection)?, Some(fee_collector_account) ); assert_eq!( - get_107_fee_collector_or_legacy(&block2, &connection, None)?, + get_fee_collector_from_block(&block2, &connection)?, Some(fee_collector_account) ); - assert_eq!( - get_107_fee_collector_or_legacy(&block3, &connection, None)?, - None - ); + assert_eq!(get_fee_collector_from_block(&block3, &connection)?, None); // Test error cases (without storing invalid blocks in DB to avoid repair conflicts) let mut invalid_block = create_test_rosetta_block(999, 1000000003, &principal1, 400); invalid_block.block.fee_collector_block_index = Some(999); // Non-existent - assert!(get_107_fee_collector_or_legacy(&invalid_block, &connection, None).is_err()); + assert!(get_fee_collector_from_block(&invalid_block, &connection).is_err()); let mut invalid_block2 = create_test_rosetta_block(998, 1000000004, &principal1, 500); invalid_block2.block.fee_collector_block_index = Some(3); // Block with no fee collector - assert!(get_107_fee_collector_or_legacy(&invalid_block2, &connection, None).is_err()); + assert!(get_fee_collector_from_block(&invalid_block2, &connection).is_err()); // Test 2: Repair functionality with broken state simulation // Manually create broken balances (missing fee collector credits for block 2) From eba6fac7100130920dcf7f72136fbb6c8566d732 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 17:14:08 +0000 Subject: [PATCH 77/85] typo --- rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 7832ff198481..660baa9f4940 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -119,7 +119,7 @@ pub fn get_107_fee_collector_or_legacy( return Ok(fee_collector_107); } - // There is not 107 fee collector, check legacy fee collector in the block + // There is no 107 fee collector, check legacy fee collector in the block get_fee_collector_from_block(rosetta_block, connection) } From 9b99d3e3c9afd9cbbbe7fc534e6bedb86cb023e0 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 18:34:29 +0000 Subject: [PATCH 78/85] check if blocks process in order --- .../icrc1/src/common/storage/storage_operations.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 660baa9f4940..3d6d02e1eb03 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -315,8 +315,15 @@ pub fn update_account_balances( // As long as there are blocks to be fetched, keep on iterating over the blocks in the database with the given BATCH_SIZE interval while !rosetta_blocks.is_empty() { - let last_block_index_bytes = rosetta_blocks.last().unwrap().index.to_le_bytes(); + let mut last_block_index = batch_start_idx; for rosetta_block in rosetta_blocks { + if rosetta_block.index < last_block_index { + bail!(format!( + "Processing blocks not in order, previous processed block: {last_block_index}, current block {}", + rosetta_block.index + )); + } + last_block_index = rosetta_block.index; match rosetta_block.get_transaction().operation { crate::common::storage::types::IcrcOperation::Burn { from, @@ -482,6 +489,7 @@ pub fn update_account_balances( { insert_tx.prepare_cached("INSERT INTO rosetta_metadata (key, value) VALUES (?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value;")?.execute(params![METADATA_FEE_COL, candid::encode_one(collector)?])?; } + let last_block_index_bytes = last_block_index.to_le_bytes(); insert_tx.prepare_cached("INSERT INTO rosetta_metadata (key, value) VALUES (?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value;")?.execute(params![METADATA_BLOCK_IDX, last_block_index_bytes])?; insert_tx.commit()?; From ed6cbb7dc3c51619a067cea094659815ed23ed18 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 20:21:27 +0000 Subject: [PATCH 79/85] clear rosetta_metadata before recalculating balances --- .../icrc1/src/common/storage/storage_operations.rs | 8 ++++++-- .../src/common/storage/storage_operations_test.rs | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 3d6d02e1eb03..81efb555198f 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -14,8 +14,8 @@ use std::collections::{BTreeMap, HashMap}; use std::str::FromStr; use tracing::{info, trace}; -const METADATA_FEE_COL: &str = "fee_collector_107"; -const METADATA_BLOCK_IDX: &str = "highest_processed_block_index"; +pub const METADATA_FEE_COL: &str = "fee_collector_107"; +pub const METADATA_BLOCK_IDX: &str = "highest_processed_block_index"; /// Gets the current value of a counter from the database. /// Returns None if the counter doesn't exist. @@ -959,6 +959,10 @@ pub fn repair_fee_collector_balances( info!("Starting balance reconciliation..."); connection.execute("DELETE FROM account_balances", params![])?; + connection.execute( + &format!("DELETE FROM rosetta_metadata WHERE key = '{METADATA_BLOCK_IDX}' OR key = '{METADATA_FEE_COL}'"), + params![], + )?; if block_count > 0 { info!("Reprocessing all blocks..."); diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs index 1023d8b6681b..d30a36b88df4 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations_test.rs @@ -441,6 +441,20 @@ fn test_fee_collector_resolution_and_repair() -> anyhow::Result<()> { // Manually create broken balances (missing fee collector credits for block 2) connection.execute("DELETE FROM account_balances", params![])?; + // Insert metadata that needs to be cleared + connection.execute( + "INSERT INTO rosetta_metadata (key, value) VALUES (?1, ?2)", + params![METADATA_BLOCK_IDX, 100_000_000u64.to_le_bytes()], + )?; + let no_fee_col: Option = None; + connection.execute( + "INSERT INTO rosetta_metadata (key, value) VALUES (?1, ?2)", + params![ + METADATA_FEE_COL, + candid::encode_one(no_fee_col).expect("failed to encode fee collector") + ], + )?; + // Correct balances for mint and block 1 connection.execute("INSERT INTO account_balances (block_idx, principal, subaccount, amount) VALUES (0, ?1, ?2, '1000000000')", params![from_account.owner.as_slice(), from_account.effective_subaccount().as_slice()])?; From 843e630d1414a09115b3d5d34d77ba42faeabd51 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 20:29:38 +0000 Subject: [PATCH 80/85] update comment --- rs/rosetta-api/icrc1/src/common/storage/schema.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/schema.rs b/rs/rosetta-api/icrc1/src/common/storage/schema.rs index 66b97e2aa7ff..cd9201d825f3 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/schema.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/schema.rs @@ -91,8 +91,9 @@ pub fn create_tables(connection: &Connection) -> Result<()> { [], )?; - // Rosetta metadata table. Meant to store values like `synced_block_height`. - // The `metadata` table defined above stores the ICRC1 token metadata. + // The rosetta_metadata table is meant to store values like the index + // of the highest processed block. This is different than the metadata + // table above which stores the ICRC1 token metadata. connection.execute( r#" CREATE TABLE IF NOT EXISTS rosetta_metadata ( From 78875e0b65255b88c9f7319b78b4b67e2ff95e3e Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Mon, 8 Dec 2025 20:43:33 +0000 Subject: [PATCH 81/85] only store operation in the blocks table --- .../icrc1/src/common/storage/storage_operations.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 81efb555198f..297c62f0ae56 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -599,14 +599,14 @@ pub fn store_blocks( expires_at, ), crate::common::storage::types::IcrcOperation::FeeCollector { - fee_collector, - caller, + fee_collector: _, + caller: _, } => ( "107feecol", - caller, None, - fee_collector.map(|fc| fc.owner), - fee_collector.map(|fc| *fc.effective_subaccount()), + None, + None, + None, None, None, Nat::from(0u64), From d582ae622d7db6a2a8f0617ff972a0a3c4cca139 Mon Sep 17 00:00:00 2001 From: maciejdfinity <122265298+maciejdfinity@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:02:49 +0100 Subject: [PATCH 82/85] Update rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathias Björkqvist --- .../icrc1/src/common/storage/storage_operations.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 297c62f0ae56..340769712218 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -304,13 +304,9 @@ pub fn update_account_balances( // This also makes the inserting of the account balances batchable and therefore faster let mut account_balances_cache: HashMap> = HashMap::new(); - let mut current_fee_collector_107 = match get_rosetta_metadata(connection, METADATA_FEE_COL)? { - Some(value) => { - let fc: Option = candid::decode_one(&value)?; - Some(fc) - } - None => None, - }; + let mut current_fee_collector_107 = get_rosetta_metadata(connection, METADATA_FEE_COL)? + .map(|value| candid::decode_one::>(&value)) + .transpose()?; let collector_before = current_fee_collector_107; // As long as there are blocks to be fetched, keep on iterating over the blocks in the database with the given BATCH_SIZE interval From 416e0e3d970ae592f437278482c983eac545bdc1 Mon Sep 17 00:00:00 2001 From: maciejdfinity <122265298+maciejdfinity@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:03:07 +0100 Subject: [PATCH 83/85] Update rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mathias Björkqvist --- .../icrc1/src/common/storage/storage_operations.rs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 340769712218..1f599a0840c6 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -197,14 +197,10 @@ pub fn get_rosetta_metadata(connection: &Connection, key: &str) -> anyhow::Resul let entry: Vec = row?; result.push(entry); } - if result.len() == 1 { - Ok(Some(result[0].clone())) - } else if result.is_empty() { - // Return None if no metadata entry found - Ok(None) - } else { - // If more than one metadata entry was found return an error - bail!(format!("Multiple metadata entries found for key: {key}")) + match result.len() { + 0 => Ok(None), + 1 => Ok(Some(result.swap_remove(0))), + _ => bail!(format!("Multiple metadata entries found for key: {key}")), } } From 61dc965b543944bbc4867c56ed078e588bc36e83 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Thu, 11 Dec 2025 14:16:03 +0000 Subject: [PATCH 84/85] update comment, handle db with no processed block index in metadata --- .../icrc1/src/common/storage/storage_client.rs | 5 +++-- .../icrc1/src/common/storage/storage_operations.rs | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs index bd7a4fde05aa..cd540c942d89 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_client.rs @@ -335,8 +335,9 @@ impl StorageClient { ) } - /// Retrieves the highest block index in the account balance table. - /// Returns None if the account balance table is empty. + /// Retrieves the highest block index that was fully processed, + /// i.e. the block was synced and balances were updated according the the tx in the block. + /// Returns None if there are no processed blocks. pub fn get_highest_processed_block_idx(&self) -> Result> { let open_connection = self.storage_connection.lock().unwrap(); storage_operations::get_highest_processed_block_idx(&open_connection) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index 1f599a0840c6..9e9e2ff31d5a 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -761,7 +761,16 @@ pub fn get_blocks_by_transaction_hash( pub fn get_highest_processed_block_idx(connection: &Connection) -> anyhow::Result> { match get_rosetta_metadata(connection, METADATA_BLOCK_IDX)? { Some(value) => Ok(Some(u64::from_le_bytes(value.as_slice().try_into()?))), - None => Ok(None), + None => { + match connection + .prepare_cached("SELECT block_idx FROM account_balances WHERE block_idx = (SELECT MAX(block_idx) FROM account_balances)")? + .query_map(params![], |row| row.get(0))? + .next() + { + None => Ok(None), + Some(res) => Ok(res?), + } + } } } From eb04d777df9af58fede1ae86517c9b902151e5d7 Mon Sep 17 00:00:00 2001 From: Maciej Dfinity Date: Fri, 12 Dec 2025 16:30:47 +0000 Subject: [PATCH 85/85] prevent switching to legacy fee collector --- .../icrc1/src/common/storage/storage_operations.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs index a9f61993adf4..529860676417 100644 --- a/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs +++ b/rs/rosetta-api/icrc1/src/common/storage/storage_operations.rs @@ -1,6 +1,6 @@ use crate::MetadataEntry; use crate::common::storage::types::{RosettaBlock, RosettaCounter}; -use anyhow::{Context, bail}; +use anyhow::{Context, anyhow, bail}; use candid::Nat; use ic_base_types::PrincipalId; use ic_ledger_core::tokens::Zero; @@ -477,9 +477,9 @@ pub fn update_account_balances( })?; } } - if collector_before != current_fee_collector_107 - && let Some(collector) = current_fee_collector_107 - { + if collector_before != current_fee_collector_107 { + let collector = current_fee_collector_107 + .ok_or_else(|| anyhow!("Cannot switch from fee collector 107 to legacy"))?; insert_tx.prepare_cached("INSERT INTO rosetta_metadata (key, value) VALUES (?1, ?2) ON CONFLICT (key) DO UPDATE SET value = excluded.value;")?.execute(params![METADATA_FEE_COL, candid::encode_one(collector)?])?; } let last_block_index_bytes = last_block_index.to_le_bytes();