Skip to content

Commit 2ee4fcc

Browse files
bogwarclaude
andcommitted
feat(ic-icrc1-ledger): add icrc152_mint and icrc152_burn endpoints
- Add Icrc152MintArgs/Icrc152MintError and Icrc152BurnArgs/Icrc152BurnError types - Add icrc152_mint and icrc152_burn #[update] endpoints - Enforce controller-only authorization via ic_cdk::api::is_controller - Guard behind the icrc152 feature flag (returns GenericError when disabled) - Validate: non-anonymous account, non-zero amount, not minting account (burn) - Map apply_transaction errors to typed endpoint errors - Update icrc3_supported_block_types to include 122burn and 122mint - Update icrc1_supported_standards to include ICRC-152 - Update ledger.did with new types and service endpoints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f5b4ce5 commit 2ee4fcc

File tree

2 files changed

+260
-1
lines changed

2 files changed

+260
-1
lines changed

rs/ledger_suite/icrc1/ledger/ledger.did

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,35 @@ type GetIndexPrincipalError = variant {
548548
}
549549
};
550550

551+
type Icrc152MintArgs = record {
552+
to : Account;
553+
amount : nat;
554+
created_at_time : opt nat64;
555+
reason : opt text
556+
};
557+
558+
type Icrc152MintError = variant {
559+
Unauthorized : text;
560+
InvalidAccount : text;
561+
Duplicate : record { duplicate_of : nat };
562+
GenericError : record { error_code : nat; message : text }
563+
};
564+
565+
type Icrc152BurnArgs = record {
566+
from : Account;
567+
amount : nat;
568+
created_at_time : opt nat64;
569+
reason : opt text
570+
};
571+
572+
type Icrc152BurnError = variant {
573+
Unauthorized : text;
574+
InvalidAccount : text;
575+
InsufficientBalance : record { balance : nat };
576+
Duplicate : record { duplicate_of : nat };
577+
GenericError : record { error_code : nat; message : text }
578+
};
579+
551580
service : (ledger_arg : LedgerArg) -> {
552581
archives : () -> (vec ArchiveInfo) query;
553582
get_transactions : (GetTransactionsRequest) -> (GetTransactionsResponse) query;
@@ -581,5 +610,8 @@ service : (ledger_arg : LedgerArg) -> {
581610

582611
icrc106_get_index_principal : () -> (GetIndexPrincipalResult) query;
583612

613+
icrc152_mint : (Icrc152MintArgs) -> (variant { Ok : nat; Err : Icrc152MintError });
614+
icrc152_burn : (Icrc152BurnArgs) -> (variant { Ok : nat; Err : Icrc152BurnError });
615+
584616
is_ledger_ready : () -> (bool) query
585617
}

rs/ledger_suite/icrc1/ledger/src/main.rs

Lines changed: 228 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
#[cfg(feature = "canbench-rs")]
33
mod benches;
44

5-
use candid::Principal;
65
use candid::types::number::Nat;
6+
use candid::{CandidType, Principal};
77
use ic_canister_log::{declare_log_buffer, export, log};
88
use ic_cdk::api::stable::StableReader;
99
use ic_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder};
@@ -67,6 +67,7 @@ use icrc_ledger_types::{
6767
icrc2::transfer_from::{TransferFromArgs, TransferFromError},
6868
};
6969
use num_traits::{ToPrimitive, bounds::Bounded};
70+
use serde::{Deserialize, Serialize};
7071
use serde_bytes::ByteBuf;
7172
use std::{
7273
cell::RefCell,
@@ -776,6 +777,10 @@ fn supported_standards() -> Vec<StandardRecord> {
776777
name: "ICRC-106".to_string(),
777778
url: "https://github.com/dfinity/ICRC/pull/106".to_string(),
778779
},
780+
StandardRecord {
781+
name: "ICRC-152".to_string(),
782+
url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-152.md".to_string(),
783+
},
779784
];
780785
standards
781786
}
@@ -954,6 +959,14 @@ fn icrc3_supported_block_types() -> Vec<icrc_ledger_types::icrc3::blocks::Suppor
954959
url: "https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md"
955960
.to_string(),
956961
},
962+
SupportedBlockType {
963+
block_type: "122burn".to_string(),
964+
url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-122.md".to_string(),
965+
},
966+
SupportedBlockType {
967+
block_type: "122mint".to_string(),
968+
url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-122.md".to_string(),
969+
},
957970
]
958971
}
959972

@@ -1020,6 +1033,220 @@ fn icrc103_get_allowances(arg: GetAllowancesArgs) -> Result<Allowances, GetAllow
10201033
))
10211034
}
10221035

1036+
// ── ICRC-152: privileged supply-change endpoints ─────────────────────────────
1037+
1038+
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
1039+
pub struct Icrc152MintArgs {
1040+
pub to: Account,
1041+
pub amount: Nat,
1042+
pub created_at_time: Option<u64>,
1043+
pub reason: Option<String>,
1044+
}
1045+
1046+
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
1047+
pub enum Icrc152MintError {
1048+
Unauthorized(String),
1049+
InvalidAccount(String),
1050+
Duplicate { duplicate_of: Nat },
1051+
GenericError { error_code: Nat, message: String },
1052+
}
1053+
1054+
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
1055+
pub struct Icrc152BurnArgs {
1056+
pub from: Account,
1057+
pub amount: Nat,
1058+
pub created_at_time: Option<u64>,
1059+
pub reason: Option<String>,
1060+
}
1061+
1062+
#[derive(Clone, Debug, CandidType, Deserialize, Serialize)]
1063+
pub enum Icrc152BurnError {
1064+
Unauthorized(String),
1065+
InvalidAccount(String),
1066+
InsufficientBalance { balance: Nat },
1067+
Duplicate { duplicate_of: Nat },
1068+
GenericError { error_code: Nat, message: String },
1069+
}
1070+
1071+
#[update]
1072+
async fn icrc152_mint(arg: Icrc152MintArgs) -> Result<Nat, Icrc152MintError> {
1073+
let caller = ic_cdk::api::caller();
1074+
1075+
if !ic_cdk::api::is_controller(&caller) {
1076+
return Err(Icrc152MintError::Unauthorized(
1077+
"caller is not a controller".to_string(),
1078+
));
1079+
}
1080+
1081+
let enabled = Access::with_ledger(|l| l.feature_flags().icrc152);
1082+
if !enabled {
1083+
return Err(Icrc152MintError::GenericError {
1084+
error_code: Nat::from(4u64),
1085+
message: "ICRC-152 is not enabled on this ledger".to_string(),
1086+
});
1087+
}
1088+
1089+
if arg.to.owner == Principal::anonymous() {
1090+
return Err(Icrc152MintError::InvalidAccount(
1091+
"anonymous account not allowed".to_string(),
1092+
));
1093+
}
1094+
1095+
let amount = match Tokens::try_from(arg.amount) {
1096+
Ok(n) => n,
1097+
Err(_) => {
1098+
return Err(Icrc152MintError::GenericError {
1099+
error_code: Nat::from(0u64),
1100+
message: "amount too large".to_string(),
1101+
});
1102+
}
1103+
};
1104+
1105+
if Tokens::is_zero(&amount) {
1106+
return Err(Icrc152MintError::GenericError {
1107+
error_code: Nat::from(0u64),
1108+
message: "amount must be greater than zero".to_string(),
1109+
});
1110+
}
1111+
1112+
let block_idx = Access::with_ledger_mut(|ledger| {
1113+
let now = TimeStamp::from_nanos_since_unix_epoch(ic_cdk::api::time());
1114+
let created_at_time = arg
1115+
.created_at_time
1116+
.map(TimeStamp::from_nanos_since_unix_epoch);
1117+
1118+
if &arg.to == ledger.minting_account() {
1119+
return Err(Icrc152MintError::InvalidAccount(
1120+
"cannot mint to the minting account".to_string(),
1121+
));
1122+
}
1123+
1124+
let tx = Transaction {
1125+
operation: Operation::AuthorizedMint {
1126+
to: arg.to,
1127+
amount,
1128+
caller,
1129+
reason: arg.reason,
1130+
},
1131+
created_at_time: created_at_time.map(|t| t.as_nanos_since_unix_epoch()),
1132+
memo: None,
1133+
};
1134+
1135+
let (block_idx, _) =
1136+
apply_transaction(ledger, tx, now, Tokens::zero()).map_err(|e| match e {
1137+
CoreTransferError::TxTooOld { .. } => Icrc152MintError::GenericError {
1138+
error_code: Nat::from(1u64),
1139+
message: "transaction too old".to_string(),
1140+
},
1141+
CoreTransferError::TxCreatedInFuture { .. } => Icrc152MintError::GenericError {
1142+
error_code: Nat::from(2u64),
1143+
message: "transaction created in the future".to_string(),
1144+
},
1145+
CoreTransferError::TxDuplicate { duplicate_of } => Icrc152MintError::Duplicate {
1146+
duplicate_of: Nat::from(duplicate_of),
1147+
},
1148+
e => Icrc152MintError::GenericError {
1149+
error_code: Nat::from(0u64),
1150+
message: format!("{e:?}"),
1151+
},
1152+
})?;
1153+
Ok(block_idx)
1154+
})?;
1155+
1156+
ic_cdk::api::set_certified_data(&Access::with_ledger(Ledger::root_hash));
1157+
archive_blocks::<Access>(&LOG, MAX_MESSAGE_SIZE).await;
1158+
Ok(Nat::from(block_idx))
1159+
}
1160+
1161+
#[update]
1162+
async fn icrc152_burn(arg: Icrc152BurnArgs) -> Result<Nat, Icrc152BurnError> {
1163+
let caller = ic_cdk::api::caller();
1164+
1165+
if !ic_cdk::api::is_controller(&caller) {
1166+
return Err(Icrc152BurnError::Unauthorized(
1167+
"caller is not a controller".to_string(),
1168+
));
1169+
}
1170+
1171+
let enabled = Access::with_ledger(|l| l.feature_flags().icrc152);
1172+
if !enabled {
1173+
return Err(Icrc152BurnError::GenericError {
1174+
error_code: Nat::from(4u64),
1175+
message: "ICRC-152 is not enabled on this ledger".to_string(),
1176+
});
1177+
}
1178+
1179+
if arg.from.owner == Principal::anonymous() {
1180+
return Err(Icrc152BurnError::InvalidAccount(
1181+
"anonymous account not allowed".to_string(),
1182+
));
1183+
}
1184+
1185+
let amount = match Tokens::try_from(arg.amount) {
1186+
Ok(n) => n,
1187+
Err(_) => {
1188+
return Err(Icrc152BurnError::GenericError {
1189+
error_code: Nat::from(0u64),
1190+
message: "amount too large".to_string(),
1191+
});
1192+
}
1193+
};
1194+
1195+
if Tokens::is_zero(&amount) {
1196+
return Err(Icrc152BurnError::GenericError {
1197+
error_code: Nat::from(0u64),
1198+
message: "amount must be greater than zero".to_string(),
1199+
});
1200+
}
1201+
1202+
let block_idx = Access::with_ledger_mut(|ledger| {
1203+
let now = TimeStamp::from_nanos_since_unix_epoch(ic_cdk::api::time());
1204+
let created_at_time = arg
1205+
.created_at_time
1206+
.map(TimeStamp::from_nanos_since_unix_epoch);
1207+
1208+
let tx = Transaction {
1209+
operation: Operation::AuthorizedBurn {
1210+
from: arg.from,
1211+
amount,
1212+
caller,
1213+
reason: arg.reason,
1214+
},
1215+
created_at_time: created_at_time.map(|t| t.as_nanos_since_unix_epoch()),
1216+
memo: None,
1217+
};
1218+
1219+
let (block_idx, _) =
1220+
apply_transaction(ledger, tx, now, Tokens::zero()).map_err(|e| match e {
1221+
CoreTransferError::InsufficientFunds { balance } => {
1222+
Icrc152BurnError::InsufficientBalance {
1223+
balance: balance.into(),
1224+
}
1225+
}
1226+
CoreTransferError::TxTooOld { .. } => Icrc152BurnError::GenericError {
1227+
error_code: Nat::from(1u64),
1228+
message: "transaction too old".to_string(),
1229+
},
1230+
CoreTransferError::TxCreatedInFuture { .. } => Icrc152BurnError::GenericError {
1231+
error_code: Nat::from(2u64),
1232+
message: "transaction created in the future".to_string(),
1233+
},
1234+
CoreTransferError::TxDuplicate { duplicate_of } => Icrc152BurnError::Duplicate {
1235+
duplicate_of: Nat::from(duplicate_of),
1236+
},
1237+
e => Icrc152BurnError::GenericError {
1238+
error_code: Nat::from(0u64),
1239+
message: format!("{e:?}"),
1240+
},
1241+
})?;
1242+
Ok(block_idx)
1243+
})?;
1244+
1245+
ic_cdk::api::set_certified_data(&Access::with_ledger(Ledger::root_hash));
1246+
archive_blocks::<Access>(&LOG, MAX_MESSAGE_SIZE).await;
1247+
Ok(Nat::from(block_idx))
1248+
}
1249+
10231250
candid::export_service!();
10241251

10251252
#[query]

0 commit comments

Comments
 (0)