Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions packages/icrc-ledger-types/src/icrc122/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::borrow::Cow;

use crate::icrc::generic_value_predicate::{
ItemRequirement, ValuePredicate, ValuePredicateFailures, and, is, is_blob, is_equal_to, is_map,
is_more_or_equal_to, is_nat, is_principal, is_text, item, len, or,
is_more_or_equal_to, is_principal, is_text, item, len, or,
};
use crate::icrc::{generic_value::Value, generic_value_predicate::is_account};

Expand All @@ -26,17 +26,19 @@ fn block_validator(
strict: bool,
) -> ValuePredicate {
use ItemRequirement::*;
let caller_mthd_req = if strict { Required } else { Optional };
let strict_req = if strict { Required } else { Optional };

let is_timestamp = is_more_or_equal_to(0);
let is_parent_hash = and(vec![is_blob(), len(is_equal_to(32))]);
let is_created_at_time = is_more_or_equal_to(0);
let is_transaction = and(vec![
is_map(),
item("mthd", caller_mthd_req.clone(), is_text()),
item("mthd", strict_req.clone(), is_text()),
item(account_field, Required, is_account()),
item("amt", Required, is_nat()),
item("caller", caller_mthd_req, is_principal()),
item("amt", Required, is_more_or_equal_to(1)),
Comment thread
bogwar marked this conversation as resolved.
item("caller", strict_req.clone(), is_principal()),
item("reason", Optional, is_text()),
item("ts", strict_req, is_created_at_time),
]);
and(vec![
is_map(),
Expand Down Expand Up @@ -142,6 +144,7 @@ mod tests {
("to", account_value()),
("amt", Value::Nat(1000_u64.into())),
("caller", principal_blob()),
("ts", Value::Nat(999_000_000_u64.into())),
]),
),
])
Expand All @@ -159,6 +162,7 @@ mod tests {
("from", account_value()),
("amt", Value::Nat(1000_u64.into())),
("caller", principal_blob()),
("ts", Value::Nat(999_000_000_u64.into())),
]),
),
])
Expand Down
62 changes: 62 additions & 0 deletions packages/icrc-ledger-types/src/icrc3/transactions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub const TRANSACTION_BURN: &str = "burn";
pub const TRANSACTION_MINT: &str = "mint";
pub const TRANSACTION_TRANSFER: &str = "transfer";
pub const TRANSACTION_FEE_COLLECTOR: &str = "107feecol";
pub const TRANSACTION_AUTHORIZED_MINT: &str = "122mint";
pub const TRANSACTION_AUTHORIZED_BURN: &str = "122burn";

pub type GenericTransaction = Value;

Expand Down Expand Up @@ -73,6 +75,26 @@ pub struct FeeCollector {
pub mthd: Option<String>,
}

#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct AuthorizedMint {
pub amount: Nat,
pub to: Account,
pub created_at_time: Option<u64>,
pub caller: Option<Principal>,
pub mthd: Option<String>,
pub reason: Option<String>,
}

#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct AuthorizedBurn {
pub amount: Nat,
pub from: Account,
pub created_at_time: Option<u64>,
pub caller: Option<Principal>,
pub mthd: Option<String>,
pub reason: Option<String>,
}

// Representation of a Transaction which supports the Icrc1 Standard functionalities
#[derive(CandidType, Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Transaction {
Expand All @@ -82,6 +104,8 @@ pub struct Transaction {
pub transfer: Option<Transfer>,
pub approve: Option<Approve>,
pub fee_collector: Option<FeeCollector>,
pub authorized_mint: Option<AuthorizedMint>,
pub authorized_burn: Option<AuthorizedBurn>,
pub timestamp: u64,
}

Expand All @@ -95,6 +119,8 @@ impl Transaction {
transfer: None,
approve: None,
fee_collector: None,
authorized_mint: None,
authorized_burn: None,
}
}

Expand All @@ -107,6 +133,8 @@ impl Transaction {
transfer: None,
approve: None,
fee_collector: None,
authorized_mint: None,
authorized_burn: None,
}
}

Expand All @@ -119,6 +147,8 @@ impl Transaction {
transfer: Some(transfer),
approve: None,
fee_collector: None,
authorized_mint: None,
authorized_burn: None,
}
}

Expand All @@ -131,6 +161,8 @@ impl Transaction {
transfer: None,
approve: Some(approve),
fee_collector: None,
authorized_mint: None,
authorized_burn: None,
}
}

Expand All @@ -143,6 +175,36 @@ impl Transaction {
transfer: None,
approve: None,
fee_collector: Some(fee_collector),
authorized_mint: None,
authorized_burn: None,
}
}

pub fn authorized_mint(authorized_mint: AuthorizedMint, timestamp: u64) -> Self {
Self {
kind: TRANSACTION_AUTHORIZED_MINT.into(),
timestamp,
mint: None,
burn: None,
transfer: None,
approve: None,
fee_collector: None,
authorized_mint: Some(authorized_mint),
authorized_burn: None,
}
}

pub fn authorized_burn(authorized_burn: AuthorizedBurn, timestamp: u64) -> Self {
Self {
kind: TRANSACTION_AUTHORIZED_BURN.into(),
timestamp,
mint: None,
burn: None,
transfer: None,
approve: None,
fee_collector: None,
authorized_mint: None,
authorized_burn: Some(authorized_burn),
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions rs/ledger_suite/icrc1/archive/archive.did
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,30 @@ type Transaction = record {
mint : opt Mint;
approve : opt Approve;
fee_collector : opt FeeCollector;
authorized_mint : opt AuthorizedMint;
authorized_burn : opt AuthorizedBurn;
timestamp : nat64;
transfer : opt Transfer
};

type AuthorizedMint = record {
to : Account;
amount : nat;
created_at_time : opt nat64;
caller : opt principal;
mthd : opt text;
reason : opt text
};

type AuthorizedBurn = record {
from : Account;
amount : nat;
created_at_time : opt nat64;
caller : opt principal;
mthd : opt text;
reason : opt text
};

type Approve = record {
fee : opt nat;
from : Account;
Expand Down
20 changes: 20 additions & 0 deletions rs/ledger_suite/icrc1/index-ng/index-ng.did
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,30 @@ type Transaction = record {
mint : opt Mint;
approve : opt Approve;
fee_collector : opt FeeCollector;
authorized_mint : opt AuthorizedMint;
authorized_burn : opt AuthorizedBurn;
timestamp : nat64;
transfer : opt Transfer
};

type AuthorizedMint = record {
to : Account;
amount : nat;
created_at_time : opt nat64;
caller : opt principal;
mthd : opt text;
reason : opt text
};

type AuthorizedBurn = record {
from : Account;
amount : nat;
created_at_time : opt nat64;
caller : opt principal;
mthd : opt text;
reason : opt text
};

type FeeCollector = record {
caller : opt principal;
fee_collector : opt Account;
Expand Down
12 changes: 7 additions & 5 deletions rs/ledger_suite/icrc1/index-ng/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1113,8 +1113,11 @@ fn process_balance_changes(block_index: BlockIndex64, block: &Block<Tokens>) {
} => {
// Does not affect the balance
}
Operation::AuthorizedMint { .. } | Operation::AuthorizedBurn { .. } => {
panic!("AuthorizedMint/AuthorizedBurn not yet supported in index-ng")
Operation::AuthorizedMint { to, amount, .. } => {
credit(block_index, to, amount);
}
Operation::AuthorizedBurn { from, amount, .. } => {
debit(block_index, from, amount);
}
},
);
Expand Down Expand Up @@ -1159,9 +1162,8 @@ fn get_accounts(block: &Block<Tokens>) -> Vec<Account> {
}
Operation::Approve { from, .. } => vec![from],
Operation::FeeCollector { .. } => vec![],
Operation::AuthorizedMint { .. } | Operation::AuthorizedBurn { .. } => {
panic!("AuthorizedMint/AuthorizedBurn not yet supported in index-ng")
}
Operation::AuthorizedMint { to, .. } => vec![to],
Operation::AuthorizedBurn { from, .. } => vec![from],
}
}

Expand Down
95 changes: 95 additions & 0 deletions rs/ledger_suite/icrc1/index-ng/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -969,6 +969,8 @@ fn test_get_account_transactions_pagination() {
approve: None,
timestamp: 0,
fee_collector: None,
authorized_mint: None,
authorized_burn: None,
},
transaction,
);
Expand Down Expand Up @@ -1571,6 +1573,99 @@ fn test_block_with_no_btype_and_no_mthd() {
);
}

#[test]
fn test_authorized_mint_and_burn_indexing() {
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 account_1 = account(1, 0);
let account_2 = account(2, 0);
let caller = PrincipalId::new_user_test_id(99);

// Block 0: regular mint to give account_1 initial balance
let block0 = BlockBuilder::new(0, 1000)
.mint(account_1, Tokens::from(10_000_000_u64))
.build();
assert_eq!(
Nat::from(0u64),
add_block(env, ledger_id, &block0).expect("failed to add block 0")
);

// Block 1: authorized mint to account_2 (ICRC-152 style, with caller/mthd/ts)
let block1 = BlockBuilder::new(1, 2000)
.authorized_mint(account_2, Tokens::from(5_000_000_u64))
.with_caller(caller.0)
.with_mthd("152mint".to_string())
.with_created_at_time(1500)
.build();
assert_eq!(
Nat::from(1u64),
add_block(env, ledger_id, &block1).expect("failed to add block 1")
);

// Block 2: authorized burn from account_1
let block2 = BlockBuilder::new(2, 3000)
.authorized_burn(account_1, Tokens::from(3_000_000_u64))
.with_caller(caller.0)
.with_mthd("152burn".to_string())
.with_created_at_time(2500)
.with_reason("compliance".to_string())
.build();
assert_eq!(
Nat::from(2u64),
add_block(env, ledger_id, &block2).expect("failed to add block 2")
);

wait_until_sync_is_completed(env, index_id, ledger_id);

// Verify balances
// account_1: 10_000_000 (mint) - 3_000_000 (authorized burn) = 7_000_000
assert_eq!(icrc1_balance_of(env, index_id, account_1), 7_000_000);
// account_2: 5_000_000 (authorized mint)
assert_eq!(icrc1_balance_of(env, index_id, account_2), 5_000_000);

// Verify account_2 transactions (should have the authorized mint)
let txs = get_account_transactions(env, index_id, account_2, None, u64::MAX);
assert_eq!(txs.transactions.len(), 1);
assert_eq!(txs.transactions[0].id, Nat::from(1u64));
let tx = &txs.transactions[0].transaction;
assert_eq!(tx.kind, "122mint");
assert!(tx.authorized_mint.is_some());

// Verify account_1 transactions (should have the mint and the authorized burn)
let txs = get_account_transactions(env, index_id, account_1, None, u64::MAX);
assert_eq!(txs.transactions.len(), 2);
// Transactions are in descending order
assert_eq!(txs.transactions[0].id, Nat::from(2u64));
assert_eq!(txs.transactions[0].transaction.kind, "122burn");
assert!(txs.transactions[0].transaction.authorized_burn.is_some());
assert_eq!(txs.transactions[1].id, Nat::from(0u64));
assert_eq!(txs.transactions[1].transaction.kind, "mint");
}

#[test]
fn test_authorized_mint_minimal_icrc122_block() {
// Test a minimal ICRC-122 block (no caller, no mthd — permissive schema)
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 account_1 = account(1, 0);

let block0 = BlockBuilder::new(0, 1000)
.authorized_mint(account_1, Tokens::from(1_000_000_u64))
.build();
assert_eq!(
Nat::from(0u64),
add_block(env, ledger_id, &block0).expect("failed to add block 0")
);

wait_until_sync_is_completed(env, index_id, ledger_id);

assert_eq!(icrc1_balance_of(env, index_id, account_1), 1_000_000);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to the comment regarding allowances in the in_memory_ledger, could we add a test here that creates an allowance for the authorized burn principal (and default subaccount), then a block that performs an authorized burn that for a regular burn would (partially) use the aforementioned allowance, and afterward check that the allowance created was not affected by the authorized burn?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added two unit tests in the in_memory_ledger test module (rather than in the index-ng integration tests, since index-ng doesn't track allowances):

  1. should_not_consume_allowance_on_authorized_burn — mints, approves, then does an authorized burn, and verifies the allowance is fully intact.
  2. should_consume_allowance_on_regular_burn_after_authorized_burn — same setup plus a regular burn via spender afterward, verifying only the regular burn deducts from the allowance.

#[test]
fn test_index_ledger_coherence() {
let mut runner = TestRunner::new(TestRunnerConfig::with_cases(1));
Expand Down
20 changes: 20 additions & 0 deletions rs/ledger_suite/icrc1/ledger/ledger.did
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,30 @@ type Transaction = record {
mint : opt Mint;
approve : opt Approve;
fee_collector : opt FeeCollector;
authorized_mint : opt AuthorizedMint;
authorized_burn : opt AuthorizedBurn;
timestamp : Timestamp;
transfer : opt Transfer
};

type AuthorizedMint = record {
to : Account;
amount : nat;
created_at_time : opt Timestamp;
caller : opt principal;
mthd : opt text;
reason : opt text
};

type AuthorizedBurn = record {
from : Account;
amount : nat;
created_at_time : opt Timestamp;
caller : opt principal;
mthd : opt text;
reason : opt text
};

type FeeCollector = record {
caller : opt principal;
fee_collector : opt Account;
Expand Down
Loading
Loading