Skip to content

Commit a78e0fe

Browse files
bogwarclaude
andcommitted
test(ic-icrc-rosetta): add AuthorizedMint/AuthorizedBurn test coverage
- Add proptest strategies for AuthorizedMint/AuthorizedBurn in types.rs - Fix btype matching in operation/transaction/block codec proptests - Add storage tests for store/read of authorized mint/burn blocks - Add balance update tests verifying credit/debit for authorized operations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f7ae436 commit a78e0fe

File tree

2 files changed

+295
-0
lines changed

2 files changed

+295
-0
lines changed

rs/rosetta-api/icrc1/src/common/storage/storage_operations/tests.rs

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::common::storage::types::{
55
};
66
use candid::{Nat, Principal};
77
use icrc_ledger_types::icrc1::account::Account;
8+
use icrc_ledger_types::icrc122::schema::{BTYPE_122_BURN, BTYPE_122_MINT};
89
use rusqlite::{Connection, params};
910
use tempfile::tempdir;
1011

@@ -716,3 +717,250 @@ fn test_get_blocks_by_index_range_returns_ascending_order() {
716717
assert_eq!(retrieved[0].index, 0);
717718
assert_eq!(retrieved[1].index, 1);
718719
}
720+
721+
// Helper function to create a test block with an AuthorizedMint operation
722+
fn create_test_authorized_mint_block(
723+
index: u64,
724+
timestamp: u64,
725+
principal: &[u8],
726+
amount: u64,
727+
) -> RosettaBlock {
728+
let account = Account {
729+
owner: Principal::from_slice(principal),
730+
subaccount: None,
731+
};
732+
733+
let transaction = IcrcTransaction {
734+
operation: IcrcOperation::AuthorizedMint {
735+
to: account,
736+
amount: Nat::from(amount),
737+
caller: Some(Principal::from_slice(b"\x01")),
738+
mthd: Some("152mint".to_string()),
739+
reason: Some("test".to_string()),
740+
},
741+
memo: None,
742+
created_at_time: Some(timestamp),
743+
};
744+
745+
let icrc_block = IcrcBlock {
746+
parent_hash: None,
747+
transaction,
748+
timestamp,
749+
effective_fee: None,
750+
fee_collector: None,
751+
fee_collector_block_index: None,
752+
btype: Some(BTYPE_122_MINT.to_string()),
753+
};
754+
755+
RosettaBlock {
756+
index,
757+
block: icrc_block,
758+
}
759+
}
760+
761+
// Helper function to create a test block with an AuthorizedBurn operation
762+
fn create_test_authorized_burn_block(
763+
index: u64,
764+
timestamp: u64,
765+
principal: &[u8],
766+
amount: u64,
767+
) -> RosettaBlock {
768+
let account = Account {
769+
owner: Principal::from_slice(principal),
770+
subaccount: None,
771+
};
772+
773+
let transaction = IcrcTransaction {
774+
operation: IcrcOperation::AuthorizedBurn {
775+
from: account,
776+
amount: Nat::from(amount),
777+
caller: Some(Principal::from_slice(b"\x01")),
778+
mthd: Some("152burn".to_string()),
779+
reason: Some("compliance".to_string()),
780+
},
781+
memo: None,
782+
created_at_time: Some(timestamp),
783+
};
784+
785+
let icrc_block = IcrcBlock {
786+
parent_hash: None,
787+
transaction,
788+
timestamp,
789+
effective_fee: None,
790+
fee_collector: None,
791+
fee_collector_block_index: None,
792+
btype: Some(BTYPE_122_BURN.to_string()),
793+
};
794+
795+
RosettaBlock {
796+
index,
797+
block: icrc_block,
798+
}
799+
}
800+
801+
#[test]
802+
fn test_store_and_read_authorized_mint_block() -> anyhow::Result<()> {
803+
let temp_dir = tempdir()?;
804+
let db_path = temp_dir.path().join("test_authorized_mint_db.sqlite");
805+
let mut connection = Connection::open(&db_path)?;
806+
schema::create_tables(&connection)?;
807+
808+
let principal = vec![1, 2, 3, 4];
809+
let block = create_test_authorized_mint_block(0, 1000000000, &principal, 500);
810+
811+
store_blocks(&mut connection, vec![block.clone()])?;
812+
813+
let retrieved = get_block_at_idx(&connection, 0)?.unwrap();
814+
815+
assert_eq!(retrieved.index, block.index);
816+
assert_eq!(retrieved.get_timestamp(), 1000000000);
817+
assert_eq!(
818+
retrieved.block.transaction.created_at_time,
819+
block.block.transaction.created_at_time
820+
);
821+
assert_eq!(
822+
retrieved.block.btype,
823+
Some(BTYPE_122_MINT.to_string()),
824+
"btype should be preserved as BTYPE_122_MINT"
825+
);
826+
827+
match &retrieved.block.transaction.operation {
828+
IcrcOperation::AuthorizedMint {
829+
to,
830+
amount,
831+
caller,
832+
mthd,
833+
reason,
834+
} => {
835+
assert_eq!(
836+
*to,
837+
Account {
838+
owner: Principal::from_slice(&principal),
839+
subaccount: None,
840+
}
841+
);
842+
assert_eq!(*amount, Nat::from(500_u64));
843+
assert_eq!(*caller, Some(Principal::from_slice(b"\x01")));
844+
assert_eq!(*mthd, Some("152mint".to_string()));
845+
assert_eq!(*reason, Some("test".to_string()));
846+
}
847+
_ => panic!("Expected AuthorizedMint operation"),
848+
}
849+
850+
Ok(())
851+
}
852+
853+
#[test]
854+
fn test_store_and_read_authorized_burn_block() -> anyhow::Result<()> {
855+
let temp_dir = tempdir()?;
856+
let db_path = temp_dir.path().join("test_authorized_burn_db.sqlite");
857+
let mut connection = Connection::open(&db_path)?;
858+
schema::create_tables(&connection)?;
859+
860+
let principal = vec![5, 6, 7, 8];
861+
let block = create_test_authorized_burn_block(0, 2000000000, &principal, 300);
862+
863+
store_blocks(&mut connection, vec![block.clone()])?;
864+
865+
let retrieved = get_block_at_idx(&connection, 0)?.unwrap();
866+
867+
assert_eq!(retrieved.index, block.index);
868+
assert_eq!(retrieved.get_timestamp(), 2000000000);
869+
assert_eq!(
870+
retrieved.block.transaction.created_at_time,
871+
block.block.transaction.created_at_time
872+
);
873+
assert_eq!(
874+
retrieved.block.btype,
875+
Some(BTYPE_122_BURN.to_string()),
876+
"btype should be preserved as BTYPE_122_BURN"
877+
);
878+
879+
match &retrieved.block.transaction.operation {
880+
IcrcOperation::AuthorizedBurn {
881+
from,
882+
amount,
883+
caller,
884+
mthd,
885+
reason,
886+
} => {
887+
assert_eq!(
888+
*from,
889+
Account {
890+
owner: Principal::from_slice(&principal),
891+
subaccount: None,
892+
}
893+
);
894+
assert_eq!(*amount, Nat::from(300_u64));
895+
assert_eq!(*caller, Some(Principal::from_slice(b"\x01")));
896+
assert_eq!(*mthd, Some("152burn".to_string()));
897+
assert_eq!(*reason, Some("compliance".to_string()));
898+
}
899+
_ => panic!("Expected AuthorizedBurn operation"),
900+
}
901+
902+
Ok(())
903+
}
904+
905+
#[test]
906+
fn test_update_account_balances_authorized_mint() -> anyhow::Result<()> {
907+
let temp_dir = tempdir()?;
908+
let db_path = temp_dir
909+
.path()
910+
.join("test_balance_authorized_mint_db.sqlite");
911+
let mut connection = Connection::open(&db_path)?;
912+
schema::create_tables(&connection)?;
913+
914+
let principal = vec![1, 2, 3, 4];
915+
let account = Account {
916+
owner: Principal::from_slice(&principal),
917+
subaccount: None,
918+
};
919+
920+
let block = create_test_authorized_mint_block(0, 1000000000, &principal, 500);
921+
store_blocks(&mut connection, vec![block])?;
922+
923+
update_account_balances(&mut connection, false, BALANCE_SYNC_BATCH_SIZE_DEFAULT)?;
924+
925+
let balance = get_account_balance_at_block_idx(&connection, &account, 0)?;
926+
assert_eq!(
927+
balance,
928+
Some(Nat::from(500_u64)),
929+
"AuthorizedMint should credit the 'to' account"
930+
);
931+
932+
Ok(())
933+
}
934+
935+
#[test]
936+
fn test_update_account_balances_authorized_burn() -> anyhow::Result<()> {
937+
let temp_dir = tempdir()?;
938+
let db_path = temp_dir
939+
.path()
940+
.join("test_balance_authorized_burn_db.sqlite");
941+
let mut connection = Connection::open(&db_path)?;
942+
schema::create_tables(&connection)?;
943+
944+
let principal = vec![5, 6, 7, 8];
945+
let account = Account {
946+
owner: Principal::from_slice(&principal),
947+
subaccount: None,
948+
};
949+
950+
// First mint tokens so the account has a balance to burn from
951+
let mint_block = create_test_authorized_mint_block(0, 1000000000, &principal, 1000);
952+
let burn_block = create_test_authorized_burn_block(1, 1000000001, &principal, 300);
953+
954+
store_blocks(&mut connection, vec![mint_block, burn_block])?;
955+
956+
update_account_balances(&mut connection, false, BALANCE_SYNC_BATCH_SIZE_DEFAULT)?;
957+
958+
let balance = get_account_balance_at_block_idx(&connection, &account, 1)?;
959+
assert_eq!(
960+
balance,
961+
Some(Nat::from(700_u64)),
962+
"AuthorizedBurn should debit the 'from' account (1000 - 300 = 700)"
963+
);
964+
965+
Ok(())
966+
}

rs/rosetta-api/icrc1/src/common/storage/types.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,7 @@ mod tests {
822822
use icrc_ledger_types::icrc1::account::Account;
823823
use icrc_ledger_types::icrc1::transfer::Memo;
824824
use icrc_ledger_types::icrc107::schema::SET_FEE_COL_107;
825+
use icrc_ledger_types::icrc122::schema::{BTYPE_122_BURN, BTYPE_122_MINT};
825826
use num_bigint::BigUint;
826827
use proptest::collection::vec;
827828
use proptest::prelude::{Just, any};
@@ -929,13 +930,53 @@ mod tests {
929930
)
930931
}
931932

933+
fn arb_authorized_mint() -> impl Strategy<Value = IcrcOperation> {
934+
(
935+
arb_account(), // to
936+
arb_nat(), // amount
937+
option::of(arb_principal()), // caller
938+
option::of("[a-z]{3,10}"), // mthd
939+
option::of("[a-z]{3,20}"), // reason
940+
)
941+
.prop_map(
942+
|(to, amount, caller, mthd, reason)| IcrcOperation::AuthorizedMint {
943+
to,
944+
amount,
945+
caller,
946+
mthd,
947+
reason,
948+
},
949+
)
950+
}
951+
952+
fn arb_authorized_burn() -> impl Strategy<Value = IcrcOperation> {
953+
(
954+
arb_account(), // from
955+
arb_nat(), // amount
956+
option::of(arb_principal()), // caller
957+
option::of("[a-z]{3,10}"), // mthd
958+
option::of("[a-z]{3,20}"), // reason
959+
)
960+
.prop_map(|(from, amount, caller, mthd, reason)| {
961+
IcrcOperation::AuthorizedBurn {
962+
from,
963+
amount,
964+
caller,
965+
mthd,
966+
reason,
967+
}
968+
})
969+
}
970+
932971
fn arb_op() -> impl Strategy<Value = IcrcOperation> {
933972
prop_oneof![
934973
arb_approve(),
935974
arb_burn(),
936975
arb_mint(),
937976
arb_transfer(),
938977
arb_fee_collector(),
978+
arb_authorized_mint(),
979+
arb_authorized_burn(),
939980
]
940981
}
941982

@@ -983,6 +1024,8 @@ mod tests {
9831024
caller: _,
9841025
mthd: _,
9851026
} => Some(BTYPE_107.to_string()),
1027+
IcrcOperation::AuthorizedMint { .. } => Some(BTYPE_122_MINT.to_string()),
1028+
IcrcOperation::AuthorizedBurn { .. } => Some(BTYPE_122_BURN.to_string()),
9861029
_ => None,
9871030
},
9881031
},
@@ -994,6 +1037,8 @@ mod tests {
9941037
proptest!(|(op in arb_op().no_shrink())| {
9951038
let btype = match op {
9961039
IcrcOperation::FeeCollector { .. } => Some(BTYPE_107.to_string()),
1040+
IcrcOperation::AuthorizedMint { .. } => Some(BTYPE_122_MINT.to_string()),
1041+
IcrcOperation::AuthorizedBurn { .. } => Some(BTYPE_122_BURN.to_string()),
9971042
_ => None,
9981043
};
9991044
let actual_op = match IcrcOperation::try_from((btype, BTreeMap::from(op.clone()))) {
@@ -1009,6 +1054,8 @@ mod tests {
10091054
proptest!(|(tx in arb_transaction().no_shrink())| {
10101055
let btype = match tx.operation {
10111056
IcrcOperation::FeeCollector { .. } => Some(BTYPE_107.to_string()),
1057+
IcrcOperation::AuthorizedMint { .. } => Some(BTYPE_122_MINT.to_string()),
1058+
IcrcOperation::AuthorizedBurn { .. } => Some(BTYPE_122_BURN.to_string()),
10121059
_ => None,
10131060
};
10141061
let actual_tx = match IcrcTransaction::try_from((btype, Value::from(tx.clone()))) {

0 commit comments

Comments
 (0)