%%%------------------------------------------------------------------- %%% @copyright (C) 2025, QPQ AG %%% @copyright (C) 2017, Aeternity Anstalt %%% @doc %%% API encoding for the Gajumaru %%% @end %%%------------------------------------------------------------------- -module(gmser_api_encoder). -export([encode/2, decode/1, safe_decode/2, byte_size_for_type/1]). -export_type([encoded/0]). -type known_type() :: key_block_hash | micro_block_hash | block_pof_hash | block_tx_hash | block_state_hash | block_witness_hash | channel | contract_bytearray | contract_pubkey | contract_store_key | contract_store_value | contract_source | transaction | tx_hash | account_pubkey | account_seckey | associate_chain | entry | signature | name | native_token | commitment | peer_pubkey | state | poi | state_trees | call_state_tree | mp_tree_hash | bytearray. -type extended_type() :: known_type() | block_hash | {id_hash, [known_type()]}. -type payload() :: binary(). -type encoded() :: binary(). -define(BASE58, 1). -define(BASE64, 2). -spec encode(known_type(), payload() | gmser_id:id()) -> encoded(). encode(id_hash, Payload) -> {IdType, Val} = gmser_id:specialize(Payload), encode(id2type(IdType), Val); encode(Type, Payload) -> Pfx = type2pfx(Type), Enc = case type2enc(Type) of ?BASE58 -> base58_check(Payload); ?BASE64 -> base64_check(Payload) end, <>. -spec decode(binary()) -> {known_type(), payload()}. decode(Bin0) -> case split(Bin0) of [Pfx, Payload] -> Type = pfx2type(Pfx), Bin = decode_check(Type, Payload), case type_size_check(Type, Bin) of ok -> {Type, Bin}; {error, Reason} -> erlang:error(Reason) end; _ -> %% {<<>>, decode_check(Bin)} erlang:error(missing_prefix) end. type_size_check(Type, Bin) -> case byte_size_for_type(Type) of not_applicable -> ok; CorrectSize -> Size = byte_size(Bin), case Size =:= CorrectSize of true -> ok; false -> {error, incorrect_size} end end. -spec safe_decode(extended_type(), encoded()) -> {'ok', payload() | gmser_id:id()} | {'error', atom()}. safe_decode({id_hash, AllowedTypes}, Enc) -> try decode(Enc) of {ActualType, Dec} -> case lists:member(ActualType, AllowedTypes) of true -> try {ok, gmser_id:create(type2id(ActualType), Dec)} catch error:_ -> {error, invalid_prefix} end; false -> {error, invalid_prefix} end catch error:_ -> {error, invalid_encoding} end; safe_decode(block_hash, Enc) -> try decode(Enc) of {key_block_hash, Dec} -> {ok, Dec}; {micro_block_hash, Dec} -> {ok, Dec}; {_, _} -> {error, invalid_prefix} catch error:_ -> {error, invalid_encoding} end; safe_decode(Type, Enc) -> try decode(Enc) of {Type, Dec} -> {ok, Dec}; {_, _} -> {error, invalid_prefix} catch error:_ -> {error, invalid_encoding} end. decode_check(Type, Bin) -> Dec = case type2enc(Type) of ?BASE58 -> base58_to_binary(Bin); ?BASE64 -> base64_to_binary(Bin) end, Sz = byte_size(Dec), BSz = Sz - 4, <> = Dec, C = check_str(Body), Body. base64_check(Bin) -> C = check_str(Bin), binary_to_base64(iolist_to_binary([Bin, C])). %% modified from github.com/mbrix/lib_hd base58_check(Bin) -> C = check_str(Bin), binary_to_base58(iolist_to_binary([Bin, C])). split(Bin) -> binary:split(Bin, [<<"_">>], []). check_str(Bin) -> <> = sha256_hash(sha256_hash(Bin)), C. sha256_hash(Bin) -> crypto:hash(sha256, Bin). id2type(account) -> account_pubkey; id2type(associate_chain) -> associate_chain; id2type(channel) -> channel; id2type(commitment) -> commitment; id2type(contract) -> contract_pubkey; id2type(contract_source) -> contract_source; id2type(name) -> name; id2type(native_token) -> native_token; id2type(entry) -> entry. type2id(account_pubkey) -> account; type2id(associate_chain) -> associate_chain; type2id(channel) -> channel; type2id(commitment) -> commitment; type2id(contract_pubkey) -> contract; type2id(contract_source) -> contract_source; type2id(name) -> name; type2id(native_token) -> native_token; type2id(entry) -> entry. type2enc(key_block_hash) -> ?BASE58; type2enc(micro_block_hash) -> ?BASE58; type2enc(block_pof_hash) -> ?BASE58; type2enc(block_tx_hash) -> ?BASE58; type2enc(block_state_hash) -> ?BASE58; type2enc(block_witness_hash) -> ?BASE58; type2enc(channel) -> ?BASE58; type2enc(contract_pubkey) -> ?BASE58; type2enc(contract_bytearray) -> ?BASE64; type2enc(contract_store_key) -> ?BASE64; type2enc(contract_store_value) -> ?BASE64; type2enc(contract_source) -> ?BASE64; type2enc(transaction) -> ?BASE64; type2enc(tx_hash) -> ?BASE58; type2enc(account_pubkey) -> ?BASE58; type2enc(account_seckey) -> ?BASE58; type2enc(associate_chain) -> ?BASE58; type2enc(signature) -> ?BASE58; type2enc(commitment) -> ?BASE58; type2enc(peer_pubkey) -> ?BASE58; type2enc(name) -> ?BASE58; type2enc(native_token) -> ?BASE58; type2enc(state) -> ?BASE64; type2enc(poi) -> ?BASE64; type2enc(state_trees) -> ?BASE64; type2enc(call_state_tree) -> ?BASE64; type2enc(mp_tree_hash) -> ?BASE58; type2enc(hash) -> ?BASE58; type2enc(entry) -> ?BASE64; type2enc(bytearray) -> ?BASE64. type2pfx(key_block_hash) -> <<"kh">>; type2pfx(micro_block_hash) -> <<"mh">>; type2pfx(block_pof_hash) -> <<"bf">>; type2pfx(block_tx_hash) -> <<"bx">>; type2pfx(block_state_hash) -> <<"bs">>; type2pfx(block_witness_hash) -> <<"ws">>; type2pfx(channel) -> <<"ch">>; type2pfx(contract_pubkey) -> <<"ct">>; type2pfx(contract_bytearray) -> <<"cb">>; type2pfx(contract_store_key) -> <<"ck">>; type2pfx(contract_store_value) -> <<"cv">>; type2pfx(contract_source) -> <<"cx">>; type2pfx(transaction) -> <<"tx">>; type2pfx(tx_hash) -> <<"th">>; type2pfx(account_pubkey) -> <<"ak">>; type2pfx(account_seckey) -> <<"sk">>; type2pfx(associate_chain) -> <<"ac">>; type2pfx(signature) -> <<"sg">>; type2pfx(commitment) -> <<"cm">>; type2pfx(peer_pubkey) -> <<"pp">>; type2pfx(name) -> <<"nm">>; type2pfx(native_token) -> <<"nt">>; type2pfx(state) -> <<"st">>; type2pfx(poi) -> <<"pi">>; type2pfx(state_trees) -> <<"ss">>; type2pfx(call_state_tree) -> <<"cs">>; type2pfx(mp_tree_hash) -> <<"mt">>; type2pfx(hash) -> <<"hs">>; type2pfx(entry) -> <<"en">>; type2pfx(bytearray) -> <<"ba">>. pfx2type(<<"kh">>) -> key_block_hash; pfx2type(<<"mh">>) -> micro_block_hash; pfx2type(<<"bf">>) -> block_pof_hash; pfx2type(<<"bx">>) -> block_tx_hash; pfx2type(<<"bs">>) -> block_state_hash; pfx2type(<<"ws">>) -> block_witness_hash; pfx2type(<<"ch">>) -> channel; pfx2type(<<"cb">>) -> contract_bytearray; pfx2type(<<"ck">>) -> contract_store_key; pfx2type(<<"cv">>) -> contract_store_value; pfx2type(<<"ct">>) -> contract_pubkey; pfx2type(<<"cx">>) -> contract_source; pfx2type(<<"tx">>) -> transaction; pfx2type(<<"th">>) -> tx_hash; pfx2type(<<"ak">>) -> account_pubkey; pfx2type(<<"sk">>) -> account_seckey; pfx2type(<<"ac">>) -> associate_chain; pfx2type(<<"sg">>) -> signature; pfx2type(<<"cm">>) -> commitment; pfx2type(<<"pp">>) -> peer_pubkey; pfx2type(<<"nm">>) -> name; pfx2type(<<"nt">>) -> native_token; pfx2type(<<"st">>) -> state; pfx2type(<<"pi">>) -> poi; pfx2type(<<"ss">>) -> state_trees; pfx2type(<<"cs">>) -> call_state_tree; pfx2type(<<"mt">>) -> mp_tree_hash; pfx2type(<<"hs">>) -> hash; pfx2type(<<"en">>) -> entry; pfx2type(<<"ba">>) -> bytearray. -spec byte_size_for_type(known_type()) -> non_neg_integer() | not_applicable. byte_size_for_type(key_block_hash) -> 32; byte_size_for_type(micro_block_hash) -> 32; byte_size_for_type(block_pof_hash) -> 32; byte_size_for_type(block_tx_hash) -> 32; byte_size_for_type(block_state_hash) -> 32; byte_size_for_type(block_witness_hash) -> 32; byte_size_for_type(channel) -> 32; byte_size_for_type(contract_pubkey) -> 32; byte_size_for_type(contract_bytearray) -> not_applicable; byte_size_for_type(contract_store_key) -> not_applicable; byte_size_for_type(contract_store_value) -> not_applicable; byte_size_for_type(contract_source) -> not_applicable; byte_size_for_type(transaction) -> not_applicable; byte_size_for_type(tx_hash) -> 32; byte_size_for_type(account_pubkey) -> 32; byte_size_for_type(account_seckey) -> 32; byte_size_for_type(associate_chain) -> 32; byte_size_for_type(signature) -> 64; byte_size_for_type(name) -> not_applicable; byte_size_for_type(native_token) -> 32; byte_size_for_type(commitment) -> 32; byte_size_for_type(peer_pubkey) -> 32; byte_size_for_type(state) -> 32; byte_size_for_type(poi) -> not_applicable; byte_size_for_type(state_trees) -> not_applicable; byte_size_for_type(call_state_tree) -> not_applicable; byte_size_for_type(mp_tree_hash) -> 32; byte_size_for_type(hash) -> 32; byte_size_for_type(entry) -> not_applicable; byte_size_for_type(bytearray) -> not_applicable. %% TODO: Fix the base58 module so that it consistently uses binaries instead %% binary_to_base58(Bin) -> iolist_to_binary(base58:binary_to_base58(Bin)). base58_to_binary(Bin) when is_binary(Bin) -> base58:base58_to_binary(binary_to_list(Bin)). binary_to_base64(Bin) -> base64:encode(Bin). base64_to_binary(Bin) when is_binary(Bin) -> base64:decode(Bin).