Skip to content

Commit 70e48a9

Browse files
bogwarclaude
andcommitted
feat(icrc-ledger-types): add ICRC-122 block schema validators
Adds the `icrc122` module with constants and validator functions for the two new block types defined by ICRC-122: - `validate_burn` for `122burn` blocks - `validate_mint` for `122mint` blocks Required fields: `btype`, `ts` (block timestamp), and a `tx` map containing `amt` + `from` (burn) or `amt` + `to` (mint). Optional tx fields: `op`, `caller`, `reason`, `ts` (created_at_time). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 74793a4 commit 70e48a9

File tree

3 files changed

+264
-0
lines changed

3 files changed

+264
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod schema;
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
use std::borrow::Cow;
2+
3+
use crate::icrc::{
4+
generic_value::Value,
5+
generic_value_predicate::{
6+
ItemRequirement, ValuePredicateFailures, and, is, is_account, is_blob, is_equal_to, is_map,
7+
is_more_or_equal_to, is_nat, is_principal, is_text, item, len,
8+
},
9+
};
10+
11+
pub const BTYPE_122_BURN: &str = "122burn";
12+
pub const BTYPE_122_MINT: &str = "122mint";
13+
pub const OP_152_BURN: &str = "152burn";
14+
pub const OP_152_MINT: &str = "152mint";
15+
16+
/// Validate whether a block conforms to the ICRC-122 `122burn` block schema.
17+
pub fn validate_burn(block: &Value) -> Result<(), ValuePredicateFailures> {
18+
burn_block_predicate()(Cow::Borrowed(block))
19+
}
20+
21+
/// Validate whether a block conforms to the ICRC-122 `122mint` block schema.
22+
pub fn validate_mint(block: &Value) -> Result<(), ValuePredicateFailures> {
23+
mint_block_predicate()(Cow::Borrowed(block))
24+
}
25+
26+
fn burn_block_predicate() -> impl Fn(Cow<Value>) -> Result<(), ValuePredicateFailures> {
27+
use ItemRequirement::*;
28+
let is_timestamp = is_more_or_equal_to(0);
29+
let is_parent_hash = and(vec![is_blob(), len(is_equal_to(32))]);
30+
let is_tx = and(vec![
31+
is_map(),
32+
item("op", Optional, is_text()),
33+
item("amt", Required, is_nat()),
34+
item("from", Required, is_account()),
35+
item("caller", Optional, is_principal()),
36+
item("reason", Optional, is_text()),
37+
item("ts", Optional, is_timestamp.clone()),
38+
]);
39+
move |block| {
40+
and(vec![
41+
is_map(),
42+
item("phash", Optional, is_parent_hash.clone()),
43+
item("btype", Required, is(Value::text(BTYPE_122_BURN))),
44+
item("ts", Required, is_timestamp.clone()),
45+
item("tx", Required, is_tx.clone()),
46+
])(block)
47+
}
48+
}
49+
50+
fn mint_block_predicate() -> impl Fn(Cow<Value>) -> Result<(), ValuePredicateFailures> {
51+
use ItemRequirement::*;
52+
let is_timestamp = is_more_or_equal_to(0);
53+
let is_parent_hash = and(vec![is_blob(), len(is_equal_to(32))]);
54+
let is_tx = and(vec![
55+
is_map(),
56+
item("op", Optional, is_text()),
57+
item("amt", Required, is_nat()),
58+
item("to", Required, is_account()),
59+
item("caller", Optional, is_principal()),
60+
item("reason", Optional, is_text()),
61+
item("ts", Optional, is_timestamp.clone()),
62+
]);
63+
move |block| {
64+
and(vec![
65+
is_map(),
66+
item("phash", Optional, is_parent_hash.clone()),
67+
item("btype", Required, is(Value::text(BTYPE_122_MINT))),
68+
item("ts", Required, is_timestamp.clone()),
69+
item("tx", Required, is_tx.clone()),
70+
])(block)
71+
}
72+
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use super::*;
77+
use candid::Nat;
78+
use std::collections::BTreeMap;
79+
80+
/// Minimal valid account: an array with one blob (principal bytes).
81+
fn account(owner: &[u8]) -> Value {
82+
Value::Array(vec![Value::blob(owner.to_vec())])
83+
}
84+
85+
/// Build a minimal valid `122burn` block.
86+
fn minimal_burn_block() -> Value {
87+
Value::map([
88+
("btype", Value::text(BTYPE_122_BURN)),
89+
("ts", Value::Nat(Nat::from(1_000_000_000_u64))),
90+
(
91+
"tx",
92+
Value::map([
93+
("amt", Value::Nat(Nat::from(100_u64))),
94+
("from", account(&[1u8; 29])),
95+
]),
96+
),
97+
])
98+
}
99+
100+
/// Build a minimal valid `122mint` block.
101+
fn minimal_mint_block() -> Value {
102+
Value::map([
103+
("btype", Value::text(BTYPE_122_MINT)),
104+
("ts", Value::Nat(Nat::from(1_000_000_000_u64))),
105+
(
106+
"tx",
107+
Value::map([
108+
("amt", Value::Nat(Nat::from(100_u64))),
109+
("to", account(&[2u8; 29])),
110+
]),
111+
),
112+
])
113+
}
114+
115+
#[test]
116+
fn test_validate_burn_minimal() {
117+
assert!(validate_burn(&minimal_burn_block()).is_ok());
118+
}
119+
120+
#[test]
121+
fn test_validate_mint_minimal() {
122+
assert!(validate_mint(&minimal_mint_block()).is_ok());
123+
}
124+
125+
#[test]
126+
fn test_validate_burn_full() {
127+
let block = Value::map([
128+
("btype", Value::text(BTYPE_122_BURN)),
129+
("ts", Value::Nat(Nat::from(1_000_000_000_u64))),
130+
("phash", Value::blob(vec![0u8; 32])),
131+
(
132+
"tx",
133+
Value::map([
134+
("op", Value::text(OP_152_BURN)),
135+
("amt", Value::Nat(Nat::from(500_u64))),
136+
("from", account(&[1u8; 29])),
137+
("caller", Value::blob(vec![1u8; 29])),
138+
("reason", Value::text("treasury rebalance")),
139+
("ts", Value::Nat(Nat::from(999_u64))),
140+
]),
141+
),
142+
]);
143+
assert!(validate_burn(&block).is_ok());
144+
}
145+
146+
#[test]
147+
fn test_validate_mint_full() {
148+
let block = Value::map([
149+
("btype", Value::text(BTYPE_122_MINT)),
150+
("ts", Value::Nat(Nat::from(1_000_000_000_u64))),
151+
("phash", Value::blob(vec![0u8; 32])),
152+
(
153+
"tx",
154+
Value::map([
155+
("op", Value::text(OP_152_MINT)),
156+
("amt", Value::Nat(Nat::from(500_u64))),
157+
("to", account(&[2u8; 29])),
158+
("caller", Value::blob(vec![1u8; 29])),
159+
("reason", Value::text("emergency issuance")),
160+
("ts", Value::Nat(Nat::from(999_u64))),
161+
]),
162+
),
163+
]);
164+
assert!(validate_mint(&block).is_ok());
165+
}
166+
167+
#[test]
168+
fn test_validate_burn_missing_btype() {
169+
let mut block = match minimal_burn_block() {
170+
Value::Map(m) => m,
171+
_ => panic!("expected map"),
172+
};
173+
block.remove("btype");
174+
assert!(validate_burn(&Value::Map(block)).is_err());
175+
}
176+
177+
#[test]
178+
fn test_validate_burn_wrong_btype() {
179+
let mut block = match minimal_burn_block() {
180+
Value::Map(m) => m,
181+
_ => panic!("expected map"),
182+
};
183+
block.insert("btype".to_string(), Value::text(BTYPE_122_MINT));
184+
assert!(validate_burn(&Value::Map(block)).is_err());
185+
}
186+
187+
#[test]
188+
fn test_validate_mint_wrong_btype() {
189+
let mut block = match minimal_mint_block() {
190+
Value::Map(m) => m,
191+
_ => panic!("expected map"),
192+
};
193+
block.insert("btype".to_string(), Value::text(BTYPE_122_BURN));
194+
assert!(validate_mint(&Value::Map(block)).is_err());
195+
}
196+
197+
#[test]
198+
fn test_validate_burn_missing_ts() {
199+
let mut block = match minimal_burn_block() {
200+
Value::Map(m) => m,
201+
_ => panic!("expected map"),
202+
};
203+
block.remove("ts");
204+
assert!(validate_burn(&Value::Map(block)).is_err());
205+
}
206+
207+
#[test]
208+
fn test_validate_burn_missing_amt() {
209+
let inner = Value::map([("from", account(&[1u8; 29]))]);
210+
let block = Value::map([
211+
("btype", Value::text(BTYPE_122_BURN)),
212+
("ts", Value::Nat(Nat::from(1_u64))),
213+
("tx", inner),
214+
]);
215+
assert!(validate_burn(&block).is_err());
216+
}
217+
218+
#[test]
219+
fn test_validate_burn_missing_from() {
220+
let inner = Value::map([("amt", Value::Nat(Nat::from(100_u64)))]);
221+
let block = Value::map([
222+
("btype", Value::text(BTYPE_122_BURN)),
223+
("ts", Value::Nat(Nat::from(1_u64))),
224+
("tx", inner),
225+
]);
226+
assert!(validate_burn(&block).is_err());
227+
}
228+
229+
#[test]
230+
fn test_validate_mint_missing_to() {
231+
let inner = Value::map([("amt", Value::Nat(Nat::from(100_u64)))]);
232+
let block = Value::map([
233+
("btype", Value::text(BTYPE_122_MINT)),
234+
("ts", Value::Nat(Nat::from(1_u64))),
235+
("tx", inner),
236+
]);
237+
assert!(validate_mint(&block).is_err());
238+
}
239+
240+
#[test]
241+
fn test_validate_burn_phash_wrong_length() {
242+
let block = Value::map([
243+
("btype", Value::text(BTYPE_122_BURN)),
244+
("ts", Value::Nat(Nat::from(1_u64))),
245+
("phash", Value::blob(vec![0u8; 16])), // should be 32
246+
(
247+
"tx",
248+
Value::map([
249+
("amt", Value::Nat(Nat::from(100_u64))),
250+
("from", account(&[1u8; 29])),
251+
]),
252+
),
253+
]);
254+
assert!(validate_burn(&block).is_err());
255+
}
256+
257+
#[test]
258+
fn test_validate_not_a_map() {
259+
assert!(validate_burn(&Value::text("not a block")).is_err());
260+
assert!(validate_mint(&Value::text("not a block")).is_err());
261+
}
262+
}

packages/icrc-ledger-types/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod icrc1;
55
pub mod icrc103;
66
pub mod icrc106;
77
pub mod icrc107;
8+
pub mod icrc122;
89
pub mod icrc2;
910
pub mod icrc21;
1011
pub mod icrc3;

0 commit comments

Comments
 (0)