@@ -54,6 +54,22 @@ pub enum Operation<Tokens: TokensType> {
5454 caller : Option < Principal > ,
5555 mthd : Option < String > ,
5656 } ,
57+ /// A mint performed by a ledger controller via ICRC-152 (`icrc152_mint`).
58+ /// Produces a `122mint` block.
59+ AuthorizedMint {
60+ to : Account ,
61+ amount : Tokens ,
62+ caller : Principal ,
63+ reason : Option < String > ,
64+ } ,
65+ /// A burn performed by a ledger controller via ICRC-152 (`icrc152_burn`).
66+ /// Produces a `122burn` block.
67+ AuthorizedBurn {
68+ from : Account ,
69+ amount : Tokens ,
70+ caller : Principal ,
71+ reason : Option < String > ,
72+ } ,
5773}
5874
5975// A [Transaction] but flattened meaning that [Operation]
@@ -122,6 +138,10 @@ struct FlattenedTransaction<Tokens: TokensType> {
122138 #[ serde( default ) ]
123139 #[ serde( skip_serializing_if = "Option::is_none" ) ]
124140 pub mthd : Option < String > ,
141+
142+ #[ serde( default ) ]
143+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
144+ pub reason : Option < String > ,
125145}
126146
127147impl < Tokens : TokensType > TryFrom < FlattenedTransaction < Tokens > > for Transaction < Tokens > {
@@ -162,6 +182,22 @@ impl<Tokens: TokensType> TryFrom<(Option<String>, FlattenedTransaction<Tokens>)>
162182 caller : value. caller ,
163183 mthd : value. mthd ,
164184 } ,
185+ Some ( "122mint" ) => Operation :: AuthorizedMint {
186+ to : value. to . ok_or ( "`to` required for `122mint` block" ) ?,
187+ amount : value. amount . ok_or ( "`amt` required for `122mint` block" ) ?,
188+ caller : value
189+ . caller
190+ . ok_or ( "`caller` required for `122mint` block" ) ?,
191+ reason : value. reason ,
192+ } ,
193+ Some ( "122burn" ) => Operation :: AuthorizedBurn {
194+ from : value. from . ok_or ( "`from` required for `122burn` block" ) ?,
195+ amount : value. amount . ok_or ( "`amt` required for `122burn` block" ) ?,
196+ caller : value
197+ . caller
198+ . ok_or ( "`caller` required for `122burn` block" ) ?,
199+ reason : value. reason ,
200+ } ,
165201 _ => Operation :: try_from ( value)
166202 . map_err ( |e| format ! ( "{} and/or unknown btype {:?}" , e, btype_str) ) ?,
167203 } ;
@@ -241,14 +277,16 @@ impl<Tokens: TokensType> From<Transaction<Tokens>> for FlattenedTransaction<Toke
241277 Mint { .. } => Some ( "mint" . to_string ( ) ) ,
242278 Transfer { .. } => Some ( "xfer" . to_string ( ) ) ,
243279 Approve { .. } => Some ( "approve" . to_string ( ) ) ,
244- FeeCollector { .. } => None ,
280+ FeeCollector { .. } | AuthorizedMint { .. } | AuthorizedBurn { .. } => None ,
245281 } ,
246282 from : match & t. operation {
247283 Transfer { from, .. } | Burn { from, .. } | Approve { from, .. } => Some ( * from) ,
284+ AuthorizedBurn { from, .. } => Some ( * from) ,
248285 _ => None ,
249286 } ,
250287 to : match & t. operation {
251288 Mint { to, .. } | Transfer { to, .. } => Some ( * to) ,
289+ AuthorizedMint { to, .. } => Some ( * to) ,
252290 _ => None ,
253291 } ,
254292 spender : match & t. operation {
@@ -261,14 +299,17 @@ impl<Tokens: TokensType> From<Transaction<Tokens>> for FlattenedTransaction<Toke
261299 | Mint { amount, .. }
262300 | Transfer { amount, .. }
263301 | Approve { amount, .. } => Some ( amount. clone ( ) ) ,
302+ AuthorizedMint { amount, .. } | AuthorizedBurn { amount, .. } => {
303+ Some ( amount. clone ( ) )
304+ }
264305 FeeCollector { .. } => None ,
265306 } ,
266307 fee : match & t. operation {
267308 Transfer { fee, .. }
268309 | Approve { fee, .. }
269310 | Mint { fee, .. }
270311 | Burn { fee, .. } => fee. to_owned ( ) ,
271- FeeCollector { .. } => None ,
312+ FeeCollector { .. } | AuthorizedMint { .. } | AuthorizedBurn { .. } => None ,
272313 } ,
273314 expected_allowance : match & t. operation {
274315 Approve {
@@ -285,13 +326,18 @@ impl<Tokens: TokensType> From<Transaction<Tokens>> for FlattenedTransaction<Toke
285326 _ => None ,
286327 } ,
287328 caller : match & t. operation {
288- FeeCollector { caller, .. } => caller. to_owned ( ) ,
329+ FeeCollector { caller, .. } => * caller,
330+ AuthorizedMint { caller, .. } | AuthorizedBurn { caller, .. } => Some ( * caller) ,
289331 _ => None ,
290332 } ,
291333 mthd : match & t. operation {
292334 FeeCollector { mthd, .. } => mthd. to_owned ( ) ,
293335 _ => None ,
294336 } ,
337+ reason : match & t. operation {
338+ AuthorizedMint { reason, .. } | AuthorizedBurn { reason, .. } => reason. clone ( ) ,
339+ _ => None ,
340+ } ,
295341 }
296342 }
297343}
@@ -485,6 +531,12 @@ impl<Tokens: TokensType> LedgerTransaction for Transaction<Tokens> {
485531 Operation :: FeeCollector { .. } => {
486532 panic ! ( "FeeCollector107 not implemented" )
487533 }
534+ Operation :: AuthorizedMint { to, amount, .. } => {
535+ context. balances_mut ( ) . mint ( to, amount. clone ( ) ) ?;
536+ }
537+ Operation :: AuthorizedBurn { from, amount, .. } => {
538+ context. balances_mut ( ) . burn ( from, amount. clone ( ) ) ?;
539+ }
488540 }
489541 Ok ( ( ) )
490542 }
@@ -669,6 +721,11 @@ impl<Tokens: TokensType> BlockType for Block<Tokens> {
669721 effective_fee : Tokens ,
670722 fee_collector : Option < FeeCollector < Self :: AccountId > > ,
671723 ) -> Self {
724+ let btype = match & transaction. operation {
725+ Operation :: AuthorizedMint { .. } => Some ( "122mint" . to_string ( ) ) ,
726+ Operation :: AuthorizedBurn { .. } => Some ( "122burn" . to_string ( ) ) ,
727+ _ => None ,
728+ } ;
672729 let effective_fee = match & transaction. operation {
673730 Operation :: Transfer { fee, .. } => fee. is_none ( ) . then_some ( effective_fee) ,
674731 Operation :: Approve { fee, .. } => fee. is_none ( ) . then_some ( effective_fee) ,
@@ -692,10 +749,163 @@ impl<Tokens: TokensType> BlockType for Block<Tokens> {
692749 timestamp : timestamp. as_nanos_since_unix_epoch ( ) ,
693750 fee_collector,
694751 fee_collector_block_index,
695- btype : None ,
752+ btype,
696753 }
697754 }
698755}
699756
700757pub type LedgerBalances < Tokens > = Balances < BTreeMap < Account , Tokens > > ;
701758pub type LedgerAllowances < Tokens > = AllowanceTable < HeapAllowancesData < Account , Tokens > > ;
759+
760+ #[ cfg( test) ]
761+ mod tests {
762+ use super :: * ;
763+ use candid:: Principal ;
764+ use ic_icrc1_tokens_u64:: U64 ;
765+ use ic_ledger_core:: block:: BlockType ;
766+ use ic_ledger_hash_of:: HashOf ;
767+
768+ type Tokens = U64 ;
769+
770+ fn controller ( ) -> Principal {
771+ Principal :: from_slice ( & [ 1u8 ; 29 ] )
772+ }
773+
774+ fn account ( seed : u8 ) -> Account {
775+ Account {
776+ owner : Principal :: from_slice ( & [ seed; 29 ] ) ,
777+ subaccount : None ,
778+ }
779+ }
780+
781+ fn make_authorized_mint_tx ( created_at_time : Option < u64 > ) -> Transaction < Tokens > {
782+ Transaction {
783+ operation : Operation :: AuthorizedMint {
784+ to : account ( 2 ) ,
785+ amount : U64 :: from ( 1000u64 ) ,
786+ caller : controller ( ) ,
787+ reason : Some ( "test mint" . to_string ( ) ) ,
788+ } ,
789+ created_at_time,
790+ memo : None ,
791+ }
792+ }
793+
794+ fn make_authorized_burn_tx ( created_at_time : Option < u64 > ) -> Transaction < Tokens > {
795+ Transaction {
796+ operation : Operation :: AuthorizedBurn {
797+ from : account ( 2 ) ,
798+ amount : U64 :: from ( 500u64 ) ,
799+ caller : controller ( ) ,
800+ reason : None ,
801+ } ,
802+ created_at_time,
803+ memo : None ,
804+ }
805+ }
806+
807+ /// Serialise a Transaction via its FlattenedTransaction impl and deserialise it back.
808+ fn cbor_roundtrip ( tx : & Transaction < Tokens > ) -> Transaction < Tokens > {
809+ let mut buf = vec ! [ ] ;
810+ ciborium:: ser:: into_writer ( tx, & mut buf) . expect ( "serialise" ) ;
811+ ciborium:: de:: from_reader ( buf. as_slice ( ) ) . expect ( "deserialise" )
812+ }
813+
814+ #[ test]
815+ fn authorized_mint_cbor_roundtrip ( ) {
816+ let tx = make_authorized_mint_tx ( Some ( 1_000_000_000 ) ) ;
817+ let recovered = cbor_roundtrip ( & tx) ;
818+ assert_eq ! ( tx, recovered) ;
819+ }
820+
821+ #[ test]
822+ fn authorized_burn_cbor_roundtrip ( ) {
823+ let tx = make_authorized_burn_tx ( Some ( 2_000_000_000 ) ) ;
824+ let recovered = cbor_roundtrip ( & tx) ;
825+ assert_eq ! ( tx, recovered) ;
826+ }
827+
828+ #[ test]
829+ fn authorized_mint_cbor_roundtrip_no_reason ( ) {
830+ let tx = Transaction {
831+ operation : Operation :: AuthorizedMint {
832+ to : account ( 3 ) ,
833+ amount : U64 :: from ( 42u64 ) ,
834+ caller : controller ( ) ,
835+ reason : None ,
836+ } ,
837+ created_at_time : None ,
838+ memo : None ,
839+ } ;
840+ assert_eq ! ( tx, cbor_roundtrip( & tx) ) ;
841+ }
842+
843+ #[ test]
844+ fn authorized_mint_block_has_122mint_btype ( ) {
845+ let tx = make_authorized_mint_tx ( None ) ;
846+ let block = Block :: < Tokens > :: from_transaction (
847+ None ,
848+ tx,
849+ TimeStamp :: from_nanos_since_unix_epoch ( 1_000_000_000 ) ,
850+ Tokens :: from ( 0u64 ) ,
851+ None ,
852+ ) ;
853+ assert_eq ! ( block. btype. as_deref( ) , Some ( "122mint" ) ) ;
854+ }
855+
856+ #[ test]
857+ fn authorized_burn_block_has_122burn_btype ( ) {
858+ let tx = make_authorized_burn_tx ( None ) ;
859+ let block = Block :: < Tokens > :: from_transaction (
860+ None ,
861+ tx,
862+ TimeStamp :: from_nanos_since_unix_epoch ( 1_000_000_000 ) ,
863+ Tokens :: from ( 0u64 ) ,
864+ None ,
865+ ) ;
866+ assert_eq ! ( block. btype. as_deref( ) , Some ( "122burn" ) ) ;
867+ }
868+
869+ #[ test]
870+ fn regular_mint_block_has_no_btype ( ) {
871+ let tx = Transaction :: < Tokens > :: mint ( account ( 2 ) , Tokens :: from ( 100u64 ) , None , None ) ;
872+ let block = Block :: < Tokens > :: from_transaction (
873+ None ,
874+ tx,
875+ TimeStamp :: from_nanos_since_unix_epoch ( 1_000_000_000 ) ,
876+ Tokens :: from ( 0u64 ) ,
877+ None ,
878+ ) ;
879+ assert_eq ! ( block. btype, None ) ;
880+ }
881+
882+ #[ test]
883+ fn block_encode_decode_roundtrip_authorized_mint ( ) {
884+ let tx = make_authorized_mint_tx ( Some ( 999_999 ) ) ;
885+ let block = Block :: < Tokens > :: from_transaction (
886+ Some ( HashOf :: new ( [ 0u8 ; 32 ] ) ) ,
887+ tx,
888+ TimeStamp :: from_nanos_since_unix_epoch ( 1_000_000_000 ) ,
889+ Tokens :: from ( 0u64 ) ,
890+ None ,
891+ ) ;
892+ let encoded = block. clone ( ) . encode ( ) ;
893+ let decoded = Block :: < Tokens > :: decode ( encoded) . expect ( "decode failed" ) ;
894+ assert_eq ! ( block, decoded) ;
895+ }
896+
897+ #[ test]
898+ fn block_encode_decode_roundtrip_authorized_burn ( ) {
899+ let tx = make_authorized_burn_tx ( Some ( 888_888 ) ) ;
900+ let block = Block :: < Tokens > :: from_transaction (
901+ None ,
902+ tx,
903+ TimeStamp :: from_nanos_since_unix_epoch ( 2_000_000_000 ) ,
904+ Tokens :: from ( 0u64 ) ,
905+ None ,
906+ ) ;
907+ let encoded = block. clone ( ) . encode ( ) ;
908+ let decoded = Block :: < Tokens > :: decode ( encoded) . expect ( "decode failed" ) ;
909+ assert_eq ! ( block, decoded) ;
910+ }
911+ }
0 commit comments