249 lines
8.3 KiB
Erlang
249 lines
8.3 KiB
Erlang
%% @doc
|
|
%% Vanillae data composer/decomposer
|
|
%%
|
|
%% This is similar to serialization/deserialization, but not the same thing
|
|
%%
|
|
%% This code exists to work out concepts and code structure for Vanillae TS. It
|
|
%% may eventually become productized. Please do not use this right now.
|
|
%%
|
|
%% References:
|
|
%%
|
|
%% 1. https://github.com/aeternity/protocol/blob/master/serializations.md
|
|
%% 2. https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
|
|
-module(vd).
|
|
|
|
-compile([export_all, nowarn_export_all]).
|
|
|
|
%%% TYPES
|
|
|
|
-type obj() :: #{type := atom(),
|
|
vsn := integer(),
|
|
fields := map()}.
|
|
|
|
|
|
|
|
-spec decompose(API_String) -> MaybeObject
|
|
when API_String :: string(),
|
|
MaybeObject :: {ok, obj()}
|
|
| {error, Reason :: term()}.
|
|
%% @doc
|
|
%% Decompose API-encoded data
|
|
|
|
decompose("tx_" ++ Base64) -> decompose_tx_b64(Base64);
|
|
decompose(X) -> {error, {nyi, {decompose, X}}}.
|
|
|
|
|
|
|
|
|
|
%% decode the base64 and check the hash thing
|
|
decompose_tx_b64(B64_str) ->
|
|
B64_Bytes = list_to_binary(B64_str),
|
|
%% This has the double sha at the end
|
|
Stupid_Bytes = base64:decode(B64_Bytes),
|
|
Stupid_Size = byte_size(Stupid_Bytes),
|
|
%% pull apart data
|
|
<<RLP_encoded_data : (Stupid_Size - 4) /binary,
|
|
Check : 4 /binary>> = Stupid_Bytes,
|
|
ActualDoubleSha = shasha(RLP_encoded_data),
|
|
case Check =:= ActualDoubleSha of
|
|
false ->
|
|
{error, {checksum_mismatch, Check, ActualDoubleSha}};
|
|
true ->
|
|
decode_and_dispatch(RLP_encoded_data)
|
|
end.
|
|
|
|
|
|
|
|
%% Double sha
|
|
shasha(Bytes) ->
|
|
<<Result:4/binary, _/binary>> = crypto:hash(sha256, crypto:hash(sha256, Bytes)),
|
|
Result.
|
|
|
|
|
|
|
|
%% decode rlp data
|
|
decode_and_dispatch(RLP_encoded_bytes) ->
|
|
{DecodedData, Remainder} = vrlp:decode(RLP_encoded_bytes),
|
|
case Remainder of
|
|
<<>> -> decom_dispatch(DecodedData);
|
|
_ -> {error, trailing_data}
|
|
end.
|
|
|
|
|
|
|
|
%% at this point we have the rlp data, and based on the first field, we are
|
|
%% going to decompose the data
|
|
decom_dispatch([Tag_Bytes, Vsn_Bytes | Fields]) ->
|
|
Tag = binary:decode_unsigned(Tag_Bytes),
|
|
Vsn = binary:decode_unsigned(Vsn_Bytes),
|
|
dd2(Tag, Vsn, Fields);
|
|
decom_dispatch(X) ->
|
|
{error, {invalid_data, X}}.
|
|
|
|
|
|
|
|
%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#table-of-object-tags
|
|
dd2(_Account = 10, Vsn = 1, Fields) -> maybe(fun decompose_fields_account1/1 , 'Account' , Vsn, Fields);
|
|
dd2(_Account = 10, Vsn = 2, Fields) -> maybe(fun decompose_fields_account2/1 , 'Account' , Vsn, Fields);
|
|
dd2(_SignedTx = 11, Vsn = 1, Fields) -> maybe(fun decompose_fields_signedtx/1 , 'SignedTx' , Vsn, Fields);
|
|
dd2(_SpendTx = 12, Vsn = 1, Fields) -> maybe(fun decompose_fields_spendtx/1 , 'SpendTx' , Vsn, Fields);
|
|
dd2(_ContractCallTx = 43, Vsn = 1, Fields) -> maybe(fun decompose_fields_contractcalltx/1, 'ContractCallTx', Vsn, Fields);
|
|
dd2(Tag , Vsn , Fields) -> {error, {nyi, {hd2, Tag, Vsn, Fields}}}.
|
|
|
|
|
|
|
|
maybe(MaybeDecompose, Type, Vsn, Fields) ->
|
|
case MaybeDecompose(Fields) of
|
|
{ok, DecFields} ->
|
|
{ok, #{type => Type,
|
|
vsn => Vsn,
|
|
fields => DecFields}};
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
|
|
% 10 = account, version 1
|
|
% https://github.com/aeternity/protocol/blob/master/serializations.md#accounts-version-1-basic-accounts
|
|
decompose_fields_account1([NonceBytes, BalanceBytes]) ->
|
|
{ok, #{nonce => binary:decode_unsigned(NonceBytes),
|
|
balance => binary:decode_unsigned(BalanceBytes)}};
|
|
decompose_fields_account1(BadFields) ->
|
|
{error, {invalid_account_v1_fields, BadFields}}.
|
|
|
|
|
|
|
|
% 10 = account, version 2
|
|
% https://github.com/aeternity/protocol/blob/master/serializations.md#accounts-version-2-generalized-accounts-from-fortuna-release
|
|
decompose_fields_account2([Flags, %% :: int()
|
|
Nonce, %% :: int()
|
|
Balance, %% :: int()
|
|
GaContract, %% :: id()
|
|
GaAuthFun]) -> %% :: binary()
|
|
{ok, #{flags => bdu(Flags),
|
|
nonce => bdu(Nonce),
|
|
balance => bdu(Balance),
|
|
ga_contract => encode_id(GaContract),
|
|
ga_auth_fun => GaAuthFun}};
|
|
decompose_fields_account2(BadFields) ->
|
|
{error, {invalid_account_v2_fields, BadFields}}.
|
|
|
|
|
|
|
|
%% 11 = signedtx
|
|
decompose_fields_signedtx([Signatures, Transaction]) ->
|
|
Sigs = lists:map(fun encode_sg/1, Signatures),
|
|
TxStr = encode_tx(Transaction),
|
|
{ok, #{signatures => Sigs,
|
|
transaction => TxStr}};
|
|
decompose_fields_signedtx(Fields) ->
|
|
{error, {invalid_signedtx_fields, Fields}}.
|
|
|
|
|
|
|
|
%% 12 = spendtx
|
|
%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#spend-transaction
|
|
decompose_fields_spendtx([SenderBytes,
|
|
RecipBytes,
|
|
AmountBytes,
|
|
FeeBytes,
|
|
TTLBytes,
|
|
NonceBytes,
|
|
Payload]) ->
|
|
% TODO: drop-through to make sure id humanization works
|
|
SenderStr = encode_id(SenderBytes),
|
|
RecipStr = encode_id(RecipBytes),
|
|
Amount = binary:decode_unsigned(AmountBytes),
|
|
Fee = binary:decode_unsigned(FeeBytes),
|
|
TTL = binary:decode_unsigned(TTLBytes),
|
|
Nonce = binary:decode_unsigned(NonceBytes),
|
|
{ok, #{sender => SenderStr,
|
|
recipient => RecipStr,
|
|
amount => Amount,
|
|
fee => Fee,
|
|
ttl => TTL,
|
|
nonce => Nonce,
|
|
payload => Payload}};
|
|
decompose_fields_spendtx(Fields) ->
|
|
{error, {invalid_spendtx_fields, Fields}}.
|
|
|
|
|
|
|
|
%% 43 = contract call tx
|
|
%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#contract-call-transaction
|
|
decompose_fields_contractcalltx([Caller, % :: id()
|
|
Nonce, % :: int()
|
|
Contract, % :: id()
|
|
AbiVersion, % :: int()
|
|
Fee, % :: int()
|
|
Ttl, % :: int()
|
|
Amount, % :: int()
|
|
Gas, % :: int()
|
|
GasPrice, % :: int()
|
|
CallData]) -> % :: binary()
|
|
{ok, #{caller => encode_id(Caller),
|
|
nonce => binary:decode_unsigned(Nonce),
|
|
contract => encode_id(Contract),
|
|
abi_version => binary:decode_unsigned(AbiVersion),
|
|
fee => binary:decode_unsigned(Fee),
|
|
ttl => binary:decode_unsigned(Ttl),
|
|
amount => binary:decode_unsigned(Amount),
|
|
gas => binary:decode_unsigned(Gas),
|
|
gas_price => binary:decode_unsigned(GasPrice),
|
|
call_data => CallData}};
|
|
decompose_fields_contractcalltx(X) ->
|
|
{error, {invalid_contractcalltx_fields, X}}.
|
|
|
|
|
|
|
|
%% general byte array
|
|
encode_ba(Bytes) ->
|
|
"ba_" ++ sha64enc(Bytes).
|
|
|
|
|
|
|
|
%% contract byte array (see: https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md)
|
|
encode_cb(Bytes) ->
|
|
"cb_" ++ sha64enc(Bytes).
|
|
|
|
|
|
|
|
%% See: https://github.com/aeternity/protocol/blob/master/serializations.md#the-id-type
|
|
%% https://github.com/aeternity/protocol/blob/master/node/api/api_encoding.md
|
|
|
|
encode_id(<<1, IdBytes:32/binary>>) -> "ak_" ++ sha58enc(IdBytes); %% ak_ account
|
|
encode_id(<<2, IdBytes:32/binary>>) -> "nm_" ++ sha58enc(IdBytes); %% nm_ name
|
|
encode_id(<<3, IdBytes:32/binary>>) -> "nm_" ++ sha58enc(IdBytes); %% cm_ commitment
|
|
encode_id(<<4, IdBytes:32/binary>>) -> "ok_" ++ sha58enc(IdBytes); %% ok_ oracle
|
|
encode_id(<<5, IdBytes:32/binary>>) -> "ct_" ++ sha58enc(IdBytes); %% ct_ contract
|
|
encode_id(<<6, IdBytes:32/binary>>) -> "ch_" ++ sha58enc(IdBytes). %% ch_ channel
|
|
|
|
|
|
|
|
encode_sg(Sig) ->
|
|
"sg_" ++ sha58enc(Sig).
|
|
|
|
|
|
|
|
encode_tx(TxData) ->
|
|
"tx_" ++ sha64enc(TxData).
|
|
|
|
|
|
|
|
sha58enc(Bytes) ->
|
|
Check = shasha(Bytes),
|
|
vb58:enc(<<Bytes/binary, Check/binary>>).
|
|
|
|
|
|
|
|
sha64enc(Bytes) ->
|
|
Check = shasha(Bytes),
|
|
binary_to_list(base64:encode(<<Bytes/binary, Check/binary>>)).
|
|
|
|
|
|
|
|
%% tired of typing this
|
|
bdu(X) ->
|
|
binary:decode_unsigned(X).
|