Writing TZBTC big_map_diff parser script in Michelson

Goal: to create an analogue of balance_updates for transfer / mint / burn operations

TZBTC big_map_diff for a transfer operation looks like this:

[..., {'action': 'update',
  'big_map': '31',
  'key_hash': 'expruiaeokjY8rPY52YXKZ6zK7oBN9Cx52psQyHtup13vMUmM7e4X2',
  'key': {'bytes': '05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3'},
  'value': {'bytes': '05070700ac9a010200000000'}}, ...]

What we want to get is changed balances with some metadata (for displaying):

{'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT': {'balance': 9900,
                                          'decimals': 8,
                                          'symbol': 'TZBTC'}}
In [2]:
parameter (pair %decodeBigMapDiff (bytes %key) (option (bytes %value)))  # value can be null, so we made it optional
In [3]:
storage (map (address %holder) 
             (pair (nat %balance) 
                   (pair (nat %decimals) 
                         (string %symbol))))

Step-by-step contract development

You can scroll down to the result

In [4]:
BEGIN %decodeBigMapDiff (Pair 0x05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3 
                              (Some 0x05070700ac9a010200000000)) 
                        {}  # empty map
Out[4]:
value type
Pair
  (Pair 0x05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3
        (Some 0x05070700ac9a010200000000))
  {}
pair (pair %decodeBigMapDiff (bytes %key) (option (bytes %value)))
      (map (address %holder) (pair (nat %balance) (pair (nat %decimals) (string %symbol))))
In [5]:
DUP ; DIP { CDR @storage } ;  # save storage on the bottom of the stack
CAR @parameter
DUP: push ((b'\x05\x07\x07\x01\x00\x00\x00\x06ledger\n\x00\x00\x00\x16\x00\x00\x16\xe6I\x94\xc2\xdd\xbd)6\x95\xb6>L\xad\xe0)\xd3\xc8\xb5\xe3', (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',)), {});
DIP: protect 1 item(s);
  CDR: pop ((b'\x05\x07\x07\x01\x00\x00\x00\x06ledger\n\x00\x00\x00\x16\x00\x00\x16\xe6I\x94\xc2\xdd\xbd)6\x95\xb6>L\xad\xe0)\xd3\xc8\xb5\xe3', (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',)), {}); push {};
  restore 1 item(s);
CAR: pop ((b'\x05\x07\x07\x01\x00\x00\x00\x06ledger\n\x00\x00\x00\x16\x00\x00\x16\xe6I\x94\xc2\xdd\xbd)6\x95\xb6>L\xad\xe0)\xd3\xc8\xb5\xe3', (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',)), {}); push (b'\x05\x07\x07\x01\x00\x00\x00\x06ledger\n\x00\x00\x00\x16\x00\x00\x16\xe6I\x94\xc2\xdd\xbd)6\x95\xb6>L\xad\xe0)\xd3\xc8\xb5\xe3', (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',));
Out[5]:
value type name
Pair 0x05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3
      (Some 0x05070700ac9a010200000000)
pair %decodeBigMapDiff (bytes %key) (option (bytes %value))
@parameter
In [6]:
DUP ; CAR @key  # first let's check the key
DUP: push (b'\x05\x07\x07\x01\x00\x00\x00\x06ledger\n\x00\x00\x00\x16\x00\x00\x16\xe6I\x94\xc2\xdd\xbd)6\x95\xb6>L\xad\xe0)\xd3\xc8\xb5\xe3', (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',));
CAR: pop (b'\x05\x07\x07\x01\x00\x00\x00\x06ledger\n\x00\x00\x00\x16\x00\x00\x16\xe6I\x94\xc2\xdd\xbd)6\x95\xb6>L\xad\xe0)\xd3\xc8\xb5\xe3', (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',)); push 05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3;
Out[6]:
value type name
0x05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3
bytes %key
@key
In [7]:
UNPACK (pair string address) ; # trying to unpack big map key (they can be of different types underneath)
ASSERT_SOME
UNPACK: pop 05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3; push (('ledger', 'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT'),);
IF_NONE: pop (('ledger', 'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT'),); push ('ledger', 'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT');
  RENAME: pop ('ledger', 'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT'); push ('ledger', 'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT');
Out[7]:
value type
Pair "ledger" 0x000016e64994c2ddbd293695b63e4cade029d3c8b5e3
pair string address
In [8]:
DUP ; CAR @label
DUP: push ('ledger', 'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT');
CAR: pop ('ledger', 'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT'); push ledger;
Out[8]:
value type name
"ledger"
string
@label
In [9]:
PUSH string "ledger" ; ASSERT_CMPEQ ;  # make sure it's balance data
PUSH: push ledger;
COMPARE: pop ledger, ledger; push 0;
EQ: pop 0; push True;
IF: pop True;
In [10]:
DUMP
Out[10]:
value type name
Pair "ledger" 0x000016e64994c2ddbd293695b63e4cade029d3c8b5e3
pair string address
Pair 0x05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3
      (Some 0x05070700ac9a010200000000)
pair %decodeBigMapDiff (bytes %key) (option (bytes %value))
@parameter
{}
map (address %holder) (pair (nat %balance) (pair (nat %decimals) (string %symbol)))
@storage
In [11]:
CDR @holder ;
Out[11]:
value type name
0x000016e64994c2ddbd293695b63e4cade029d3c8b5e3
address
@holder
In [12]:
SWAP ; CDR @value ; 
ASSERT_SOME  # actually we need to handle cases when a key is removed from big_map, will do later
SWAP: pop tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT, (b'\x05\x07\x07\x01\x00\x00\x00\x06ledger\n\x00\x00\x00\x16\x00\x00\x16\xe6I\x94\xc2\xdd\xbd)6\x95\xb6>L\xad\xe0)\xd3\xc8\xb5\xe3', (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',)); push tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT; push (b'\x05\x07\x07\x01\x00\x00\x00\x06ledger\n\x00\x00\x00\x16\x00\x00\x16\xe6I\x94\xc2\xdd\xbd)6\x95\xb6>L\xad\xe0)\xd3\xc8\xb5\xe3', (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',));
CDR: pop (b'\x05\x07\x07\x01\x00\x00\x00\x06ledger\n\x00\x00\x00\x16\x00\x00\x16\xe6I\x94\xc2\xdd\xbd)6\x95\xb6>L\xad\xe0)\xd3\xc8\xb5\xe3', (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',)); push (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',);
IF_NONE: pop (b'\x05\x07\x07\x00\xac\x9a\x01\x02\x00\x00\x00\x00',); push 05070700ac9a010200000000;
  RENAME: pop 05070700ac9a010200000000; push 05070700ac9a010200000000;
Out[12]:
value type
0x05070700ac9a010200000000
bytes %value
In [ ]:
 
In [13]:
UNPACK (pair nat (map address nat)) ; 
ASSERT_SOME  # here we can fail, since we know exactly how data is packed
UNPACK: pop 05070700ac9a010200000000; push ((9900, {}),);
IF_NONE: pop ((9900, {}),); push (9900, {});
  RENAME: pop (9900, {}); push (9900, {});
Out[13]:
value type
Pair 9900 {}
pair nat (map address nat)
In [14]:
CAR @balance
Out[14]:
value type name
9900
nat
@balance
In [15]:
PUSH @symbol string "TZBTC" ;  # adding metadata
PUSH @decimals nat 8 ;
PUSH: push TZBTC;
PUSH: push 8;
Out[15]:
value type name
8
nat
@decimals
In [16]:
DUMP
Out[16]:
value type name
8
nat
@decimals
"TZBTC"
string
@symbol
9900
nat
@balance
0x000016e64994c2ddbd293695b63e4cade029d3c8b5e3
address
{}
map (address %holder) (pair (nat %balance) (pair (nat %decimals) (string %symbol)))
@storage
In [17]:
PAIR ; SWAP ; PAIR ; SOME ;
PAIR: pop 8, TZBTC; push (8, 'TZBTC');
SWAP: pop (8, 'TZBTC'), 9900; push (8, 'TZBTC'); push 9900;
PAIR: pop 9900, (8, 'TZBTC'); push (9900, (8, 'TZBTC'));
SOME: pop (9900, (8, 'TZBTC')); push ((9900, (8, 'TZBTC')),);
Out[17]:
value type
Some (Pair 9900 (Pair 8 "TZBTC"))
option (pair nat (pair nat string))
In [18]:
SWAP ; UPDATE  # writing to the storage
SWAP: pop ((9900, (8, 'TZBTC')),), tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT; push ((9900, (8, 'TZBTC')),); push tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT;
UPDATE: pop tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT, ((9900, (8, 'TZBTC')),), {}; push {'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT': (9900, (8, 'TZBTC'))};
Out[18]:
value type
{ Elt 0x000016e64994c2ddbd293695b63e4cade029d3c8b5e3 (Pair 9900 (Pair 8 "TZBTC")) }
map (address %holder) (pair (nat %balance) (pair (nat %decimals) (string %symbol)))
In [19]:
NIL operation ; PAIR ; COMMIT
NIL: push [];
PAIR: pop [], {'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT': (9900, (8, 'TZBTC'))}; push ([], {'tz1Mj7RzPmMAqDUNFBn5t5VbXmWW4cSUAdtT': (9900, (8, 'TZBTC'))});
COMMIT:
Out[19]:
value type
{ Elt 0x000016e64994c2ddbd293695b63e4cade029d3c8b5e3 (Pair 9900 (Pair 8 "TZBTC")) }
map (address %holder) (pair (nat %balance) (pair (nat %decimals) (string %symbol)))

Resulting script

Actually a valid Tezos contract

In [20]:
parameter (pair %decodeBigMapDiff (bytes %key) (option (bytes %value))) ;
storage (map (address %holder) 
             (pair (nat %balance) 
                   (pair (nat %decimals) 
                         (string %symbol)))) ;
code {
    DUP ; CAR ; DIP { CDR } ;
    DUP ; CAR ;
    UNPACK (pair string address) ;
    IF_SOME { 
        DUP ; CAR ;
        PUSH string "ledger" ;
        IFCMPEQ { 
            CDR @holder ; 
            SWAP ; CDR ;
            IF_SOME { 
                UNPACK (pair nat (map address nat)) ;
                ASSERT_SOME ;
                CAR @balance ;
            } { 
                PUSH @balance nat 0;
            } ;
            PUSH @symbol string "TZBTC" ;
            PUSH @decimals nat 8 ;
            PAIR ; SWAP ; PAIR ; SOME ;
            SWAP ; UPDATE                
        } {
            DROP 2
        } ;
    } { 
        DROP
    } ;
    NIL operation ; PAIR ;
}
parameter (pair %decodeBigMapDiff (bytes %key) (option (bytes %value)));
storage (map (address %holder) (pair (nat %balance) (pair (nat %decimals) (string %symbol))));
code { DUP ; CAR ; DIP { CDR } ; DUP ; CAR ; UNPACK (pair string address) ; { IF_NONE { DROP } { DUP ; CAR ; PUSH string "ledger" ; { { COMPARE ; EQ } ; IF { CDR @holder ; SWAP ; CDR ; { IF_NONE { PUSH @balance nat 0 } { UNPACK (pair nat (map address nat)) ; { IF_NONE { { UNIT ; FAILWITH } } { RENAME } } ; CAR @balance } } ; PUSH @symbol string "TZBTC" ; PUSH @decimals nat 8 ; PAIR ; SWAP ; PAIR ; SOME ; SWAP ; UPDATE } { DROP 2 } } } } ; NIL operation ; PAIR };

Now let's run some tests

In [21]:
DEBUG False
In [22]:
RUN %decodeBigMapDiff (Pair 0x05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3 
                            (Some 0x05070700ac9a010200000000)) 
                      {}
Out[22]:
value type
{ Elt 0x000016e64994c2ddbd293695b63e4cade029d3c8b5e3 (Pair 9900 (Pair 8 "TZBTC")) }
map (address %holder) (pair (nat %balance) (pair (nat %decimals) (string %symbol)))
In [23]:
RUN %decodeBigMapDiff (Pair 0x05070701000000066c65646765720a00000016000016e64994c2ddbd293695b63e4cade029d3c8b5e3 
                            None) 
                      {}
Out[23]:
value type
{ Elt 0x000016e64994c2ddbd293695b63e4cade029d3c8b5e3 (Pair 0 (Pair 8 "TZBTC")) }
map (address %holder) (pair (nat %balance) (pair (nat %decimals) (string %symbol)))
In [24]:
RUN %decodeBigMapDiff (Pair 0xdeadbeef None) {}
Out[24]:
value type
{}
map (address %holder) (pair (nat %balance) (pair (nat %decimals) (string %symbol)))