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 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/packages/icrc-ledger-types/src/icrc3/transactions.rs b/packages/icrc-ledger-types/src/icrc3/transactions.rs index f71945bce7aa..4e855910cf01 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::{ @@ -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 ts: 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 set_fee_collector(fee_collector: FeeCollector, timestamp: u64) -> Self { + Self { + kind: "107set_fee_collector".into(), + timestamp, + mint: None, + burn: None, + transfer: None, + approve: None, + fee_collector: Some(fee_collector), } } } 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/archive/archive.did b/rs/ledger_suite/icrc1/archive/archive.did index 4eb4323600db..cf9bc873895a 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 principal; + fee_collector : opt Account; + ts : opt nat64 +}; + type Burn = record { from : Account; memo : opt vec nat8; 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/index-ng/index-ng.did b/rs/ledger_suite/icrc1/index-ng/index-ng.did index 0f2351be1f92..5bba0a9060e4 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 principal; + fee_collector : opt Account; + ts : opt nat64 +}; + type Approve = record { fee : opt Tokens; from : Account; diff --git a/rs/ledger_suite/icrc1/index-ng/src/main.rs b/rs/ledger_suite/icrc1/index-ng/src/main.rs index 6984dbcece0d..16e8800243b7 100644 --- a/rs/ledger_suite/icrc1/index-ng/src/main.rs +++ b/rs/ledger_suite/icrc1/index-ng/src/main.rs @@ -140,6 +140,12 @@ 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, + + /// The ICRC-107 fee collector. Example values: + /// - `None` - legacy fee collector is used. + /// - `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>, } impl State { @@ -161,6 +167,7 @@ impl Default for State { fee_collectors: Default::default(), last_fee: None, retrieve_blocks_from_ledger_interval: None, + fee_collector_107: None, } } } @@ -783,8 +790,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); @@ -828,8 +838,18 @@ 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) { + if let Operation::FeeCollector { + fee_collector, + caller: _, + } = block.transaction.operation + { + 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) @@ -875,7 +895,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); } } @@ -891,7 +911,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); } } @@ -920,7 +940,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); } } @@ -954,6 +974,16 @@ 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().flatten() { + credit(block_index, fee_collector_107, fee); + } + } + Operation::FeeCollector { + fee_collector: _, + caller: _, + } => { + // Does not affect the balance } }, ); @@ -989,19 +1019,35 @@ 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 { .. } => vec![], } } -fn get_fee_collector(block_index: BlockIndex64, block: &Block) -> Option { +fn get_fee_collector() -> Option { + get_fee_collector_107().unwrap_or_else(get_legacy_fee_collector) +} + +fn get_fee_collector_107() -> Option> { + with_state(|s| s.fee_collector_107) +} + +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 = 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 { 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 diff --git a/rs/ledger_suite/icrc1/index-ng/tests/tests.rs b/rs/ledger_suite/icrc1/index-ng/tests/tests.rs index 2c197e22aa77..c22a4dea163b 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, ); @@ -1258,6 +1259,124 @@ 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 feecol_legacy = account(101, 0); + let feecol_107 = 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(1u64)); + 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(1000u64)).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_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, regular_account, 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()) + .fee_collector(fc, None, None) + .build(); + + assert_eq!( + Nat::from(block_id), + add_block(env, ledger_id, &fee_collector) + .expect("error adding fee collector 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(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, feecol_legacy)); + + // Legacy fee collector does not collect approve fees + 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, 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(feecol_legacy), None); + block_id = add_mint_block(block_id, None, Some(block_id - 1)); + 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(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, 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(feecol_legacy), None); + block_id = add_mint_block(block_id, None, Some(block_id - 1)); + 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, 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, feecol_legacy)); + assert_eq!(4, icrc1_balance_of(env, index_id, feecol_107)); +} + #[test] fn test_index_ledger_coherence() { let mut runner = TestRunner::new(TestRunnerConfig::with_cases(1)); 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 diff --git a/rs/ledger_suite/icrc1/ledger/ledger.did b/rs/ledger_suite/icrc1/ledger/ledger.did index b093ec9bfada..79bd1c745855 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 principal; + fee_collector : opt Account; + ts : opt nat64 +}; + type Burn = record { from : Account; memo : opt blob; 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(); diff --git a/rs/ledger_suite/icrc1/src/endpoints.rs b/rs/ledger_suite/icrc1/src/endpoints.rs index 4930df6f1902..2f3864c81087 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 = "107set_fee_collector".to_string(); + tx.fee_collector = Some(FeeCollector { + fee_collector, + caller, + ts: created_at_time, + }); + } } tx diff --git a/rs/ledger_suite/icrc1/src/lib.rs b/rs/ledger_suite/icrc1/src/lib.rs index 1a433a61e9b0..7af166b27b06 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::{ @@ -80,6 +81,17 @@ pub enum Operation { #[serde(skip_serializing_if = "Option::is_none")] fee: Option, }, + #[serde(rename = "107set_fee_collector")] + FeeCollector { + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "compact_account::opt" + )] + fee_collector: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + caller: Option, + }, } // A [Transaction] but flattened meaning that [Operation] @@ -117,8 +129,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")] @@ -131,6 +145,15 @@ 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")] + caller: Option, } impl TryFrom> for Transaction { @@ -142,13 +165,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 { @@ -157,7 +184,9 @@ impl TryFrom> for Transaction Operation::Approve { @@ -167,11 +196,17 @@ 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 { @@ -194,6 +229,7 @@ impl From> for FlattenedTransaction "mint", Transfer { .. } => "xfer", Approve { .. } => "approve", + FeeCollector { .. } => "107set_fee_collector", } .into(), from: match &t.operation { @@ -213,13 +249,15 @@ impl From> for FlattenedTransaction amount.clone(), + | Approve { amount, .. } => Some(amount.clone()), + FeeCollector { .. } => None, }, 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 +269,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 +467,9 @@ impl LedgerTransaction for Transaction { return Err(e); } } + Operation::FeeCollector { .. } => { + panic!("FeeCollector107 not implemented") + } } Ok(()) } @@ -492,6 +541,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>; @@ -545,6 +598,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!("FeeCollector107 not implemented") + } _ => None, }; let (fee_collector, fee_collector_block_index) = match fee_collector { @@ -562,6 +618,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/icrc3.rs b/rs/ledger_suite/icrc1/test_utils/src/icrc3.rs index fe1e31a92d38..89e948ca89fb 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 a fee collector block + 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) } } @@ -266,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( @@ -289,6 +314,35 @@ impl ApproveBuilder { } } +/// Builder for fee collector operations +pub struct FeeCollectorBuilder { + builder: BlockBuilder, + fee_collector: Option, + caller: Option, + ts: Option, +} + +impl FeeCollectorBuilder { + /// Build the fee collector block + pub fn build(self) -> ICRC3Value { + let mut tx_fields = BTreeMap::new(); + if let Some(fee_collector) = &self.fee_collector { + tx_fields.insert( + "fee_collector".to_string(), + account_to_icrc3_value(fee_collector), + ); + } + 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::*; diff --git a/rs/ledger_suite/icrc1/test_utils/src/lib.rs b/rs/ledger_suite/icrc1/test_utils/src/lib.rs index 681a0dee4450..100445935b35 100644 --- a/rs/ledger_suite/icrc1/test_utils/src/lib.rs +++ b/rs/ledger_suite/icrc1/test_utils/src/lib.rs @@ -223,6 +223,7 @@ pub fn blocks_strategy( 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 { @@ -234,6 +235,7 @@ pub fn blocks_strategy( timestamp, fee_collector, fee_collector_block_index: None, + btype: None, } .encode(), )), @@ -242,6 +244,7 @@ pub fn blocks_strategy( timestamp, fee_collector, fee_collector_block_index: None, + btype: None, } }) } @@ -577,6 +580,9 @@ impl TransactionsAndBalances { .or_insert(amount); self.debit(from, fee); } + Operation::FeeCollector { .. } => { + panic!("FeeCollector107 not implemented") + } }; self.transactions.push(tx); @@ -604,6 +610,9 @@ impl TransactionsAndBalances { // (allowance was added/modified for this account) self.check_and_update_account_validity(*from, default_fee); } + Operation::FeeCollector { .. } => { + panic!("FeeCollector107 not implemented") + } } } @@ -1492,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..cfce334ea75f 100644 --- a/rs/ledger_suite/icrc1/tests/tests.rs +++ b/rs/ledger_suite/icrc1/tests/tests.rs @@ -1,9 +1,11 @@ +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::{arb_amount, arb_block, arb_small_amount, blocks_strategy}; +use ic_icrc1_test_utils::icrc3::BlockBuilder; +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; @@ -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::icrc::generic_value::ICRC3Value; use proptest::prelude::*; fn arb_u256() -> impl Strategy { @@ -170,3 +173,56 @@ fn test_encoding_decoding_block_u256( 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 +where + Tokens: TokensType, +{ + ( + 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() + }, + ) +} + +#[test_strategy::proptest] +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"); + 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()); +} + +#[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()); +} 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..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 @@ -538,6 +538,9 @@ where &fee.clone().or(block.effective_fee.clone()), TimeStamp::from_nanos_since_unix_epoch(block.timestamp), ), + Operation::FeeCollector { .. } => { + panic!("FeeCollector107 not implemented") + } } } self.post_process_ledger_blocks(blocks); 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, }, ) } 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/rosetta-api/icrc1/src/common/storage/types.rs b/rs/rosetta-api/icrc1/src/common/storage/types.rs index 7f473f9828fd..3648a7bb985f 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!("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 297742f6d2b0..1db71511166c 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!("FeeCollector107 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 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, }, ],