|
2 | 2 | #[cfg(feature = "canbench-rs")] |
3 | 3 | mod benches; |
4 | 4 |
|
5 | | -use candid::Principal; |
6 | 5 | use candid::types::number::Nat; |
| 6 | +use candid::{CandidType, Principal}; |
7 | 7 | use ic_canister_log::{declare_log_buffer, export, log}; |
8 | 8 | use ic_cdk::api::stable::StableReader; |
9 | 9 | use ic_http_types::{HttpRequest, HttpResponse, HttpResponseBuilder}; |
@@ -67,6 +67,7 @@ use icrc_ledger_types::{ |
67 | 67 | icrc2::transfer_from::{TransferFromArgs, TransferFromError}, |
68 | 68 | }; |
69 | 69 | use num_traits::{ToPrimitive, bounds::Bounded}; |
| 70 | +use serde::{Deserialize, Serialize}; |
70 | 71 | use serde_bytes::ByteBuf; |
71 | 72 | use std::{ |
72 | 73 | cell::RefCell, |
@@ -776,6 +777,10 @@ fn supported_standards() -> Vec<StandardRecord> { |
776 | 777 | name: "ICRC-106".to_string(), |
777 | 778 | url: "https://github.com/dfinity/ICRC/pull/106".to_string(), |
778 | 779 | }, |
| 780 | + StandardRecord { |
| 781 | + name: "ICRC-152".to_string(), |
| 782 | + url: "https://github.com/dfinity/ICRC/blob/main/ICRCs/ICRC-152.md".to_string(), |
| 783 | + }, |
779 | 784 | ]; |
780 | 785 | standards |
781 | 786 | } |
@@ -954,6 +959,14 @@ fn icrc3_supported_block_types() -> Vec<icrc_ledger_types::icrc3::blocks::Suppor |
954 | 959 | url: "https://github.com/dfinity/ICRC-1/blob/main/standards/ICRC-2/README.md" |
955 | 960 | .to_string(), |
956 | 961 | }, |
| 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 | + }, |
957 | 970 | ] |
958 | 971 | } |
959 | 972 |
|
@@ -1020,6 +1033,220 @@ fn icrc103_get_allowances(arg: GetAllowancesArgs) -> Result<Allowances, GetAllow |
1020 | 1033 | )) |
1021 | 1034 | } |
1022 | 1035 |
|
| 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 | + |
1023 | 1250 | candid::export_service!(); |
1024 | 1251 |
|
1025 | 1252 | #[query] |
|
0 commit comments