2852 lines
107 KiB
Erlang
2852 lines
107 KiB
Erlang
%%% @doc
|
|
%%% The Hakuzaru Erlang Interface to Gajumaru
|
|
%%%
|
|
%%% This module is the high-level interface to the Gajumaru blockchain system.
|
|
%%% The interface is split into three main sections:
|
|
%%% - Get/Set admin functions
|
|
%%% - Node JSON query interface functions
|
|
%%% - Contract call and serialization interface functions
|
|
%%%
|
|
%%% The get/set admin functions are for setting or checking things like the Gajumaru
|
|
%%% "network ID" and list of addresses of nodes you want to use for answering
|
|
%%% queries to the blockchain.
|
|
%%%
|
|
%%% The JSON query interface functions are the blockchain query functions themselves
|
|
%%% which are translated to network queries and return Erlang messages as responses.
|
|
%%%
|
|
%%% The contract call and serialization interface are the functions used to convert
|
|
%%% a desired call to a smart contract on the chain to call data serialized in a form
|
|
%%% that a Gajumaru compatible wallet or library can sign and submit to a Gajumaru node.
|
|
%%%
|
|
%%% This module does not implement the OTP application behavior.
|
|
%%% helper functions.
|
|
%%% @end
|
|
|
|
-module(hz).
|
|
-vsn("0.7.0").
|
|
-author("Craig Everett <ceverett@tsuriai.jp>").
|
|
-copyright("Craig Everett <ceverett@tsuriai.jp>").
|
|
-license("GPL-3.0-or-later").
|
|
|
|
% Get/Set admin functions.
|
|
-export([chain_nodes/0, chain_nodes/1,
|
|
tls/0, tls/1,
|
|
timeout/0, timeout/1]).
|
|
|
|
% Node JSON query interface functions
|
|
-export([network_id/0,
|
|
top_height/0, top_block/0,
|
|
kb_current/0, kb_current_hash/0, kb_current_height/0,
|
|
kb_pending/0,
|
|
kb_by_hash/1, kb_by_height/1,
|
|
% kb_insert/1,
|
|
mb_header/1, mb_txs/1, mb_tx_index/2, mb_tx_count/1,
|
|
gen_current/0, gen_by_id/1, gen_by_height/1,
|
|
acc/1, acc_at_height/2, acc_at_block_id/2,
|
|
acc_pending_txs/1,
|
|
next_nonce/1,
|
|
dry_run/1, dry_run/2, dry_run/3, dry_run_map/1,
|
|
tx/1, tx_info/1,
|
|
post_tx/1,
|
|
contract/1, contract_code/1, contract_source/1,
|
|
contract_poi/1,
|
|
name/1,
|
|
% channel/1,
|
|
peer_pubkey/0,
|
|
status/0,
|
|
status_chainends/0]).
|
|
|
|
% Contract call and serialization interface functions
|
|
-export([read_aci/1,
|
|
min_gas/0,
|
|
min_gas_price/0,
|
|
contract_create/3,
|
|
contract_create_built/3,
|
|
contract_create_built/8,
|
|
contract_create/8,
|
|
prepare_contract/1,
|
|
prepare_aaci/1,
|
|
aaci_lookup_spec/2,
|
|
contract_call/5,
|
|
contract_call/6,
|
|
contract_call/10,
|
|
decode_bytearray_fate/1, decode_bytearray/2,
|
|
spend/5, spend/10,
|
|
sign_tx/2, sign_tx/3,
|
|
sign_message/2, verify_signature/3,
|
|
sign_binary/2, verify_bin_signature/3]).
|
|
|
|
|
|
%%% Types
|
|
|
|
-export_type([chain_node/0, network_id/0, chain_error/0, aaci/0]).
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
-type chain_node() :: {inet:ip_address(), inet:port_number()}.
|
|
-type network_id() :: string().
|
|
-type chain_error() :: not_started
|
|
| no_nodes
|
|
| timeout
|
|
| {timeout, Received :: binary()}
|
|
| inet:posix()
|
|
| {received, binary()}
|
|
| headers
|
|
| {headers, map()}
|
|
| bad_length
|
|
| gc_out_of_range.
|
|
-type aaci() :: {aaci, term(), term(), term()}.
|
|
-type pubkey() :: unicode:chardata(). % "ak_" ++ _
|
|
-type account_id() :: pubkey().
|
|
-type contract_id() :: unicode:chardata(). % "ct_" ++ _
|
|
-type peer_pubkey() :: string(). % "pp_" ++ _
|
|
-type keyblock_hash() :: string(). % "kh_" ++ _
|
|
-type contract_byte_array() :: string(). % "cb_" ++ _
|
|
-type microblock_hash() :: string(). % "mh_" ++ _
|
|
|
|
%-type block_state_hash() :: string(). % "bs_" ++ _
|
|
%-type proof_of_fraud_hash() :: string() | no_fraud. % "bf_" ++ _
|
|
%-type signature() :: string(). % "sg_" ++ _
|
|
%-type block_tx_hash() :: string(). % "bx_" ++ _
|
|
|
|
-type tx_hash() :: string(). % "th_" ++ _
|
|
|
|
%-type name_hash() :: string(). % "nm_" ++ _
|
|
%-type protocol_info() :: #{string() => term()}.
|
|
% #{"effective_at_height" => non_neg_integer(),
|
|
% "version" => pos_integer()}.
|
|
|
|
-type keyblock() :: #{string() => term()}.
|
|
% <pre>
|
|
% #{"beneficiary" => account_id(),
|
|
% "hash" => keyblock_hash(),
|
|
% "height" => pos_integer(),
|
|
% "info" => contract_byte_array(),
|
|
% "miner" => account_id(),
|
|
% "nonce" => non_neg_integer(),
|
|
% "pow" => [non_neg_integer()],
|
|
% "prev_hash" => microblock_hash(),
|
|
% "prev_key_hash" => keyblock_hash(),
|
|
% "state_hash" => block_state_hash(),
|
|
% "target" => non_neg_integer(),
|
|
% "time" => non_neg_integer(),
|
|
% "version" => 5}.
|
|
% </pre>
|
|
-type microblock_header() :: #{string() => term()}.
|
|
% <pre>
|
|
% #{"hash" => microblock_hash(),
|
|
% "height" => pos_integer(),
|
|
% "pof_hash" => proof_of_fraud_hash(),
|
|
% "prev_hash" => microblock_hash() | keyblock_hash(),
|
|
% "prev_key_hash" => keyblock_hash(),
|
|
% "signature" => signature(),
|
|
% "state_hash" => block_state_hash(),
|
|
% "time" => non_neg_integer(),
|
|
% "txs_hash" => block_tx_hash(),
|
|
% "version" => 1}.
|
|
% </pre>
|
|
-type transaction() :: #{string() => term()}.
|
|
% <pre>
|
|
% #{"block_hash" => microblock_hash(),
|
|
% "block_height" => pos_integer(),
|
|
% "hash" => tx_hash(),
|
|
% "signatures" => [signature()],
|
|
% "tx" =>
|
|
% #{"abi_version" => pos_integer(),
|
|
% "amount" => non_neg_integer(),
|
|
% "call_data" => contract_byte_array(),
|
|
% "code" => contract_byte_array(),
|
|
% "deposit" => non_neg_integer(),
|
|
% "gas" => pos_integer(),
|
|
% "gas_price" => pos_integer(),
|
|
% "nonce" => pos_integer(),
|
|
% "owner_id" => account_id(),
|
|
% "type" => string(),
|
|
% "version" => pos_integer(),
|
|
% "vm_version" => pos_integer()}}
|
|
% </pre>
|
|
-type generation() :: #{string() => term()}.
|
|
% <pre>
|
|
% #{"key_block" => keyblock(),
|
|
% "micro_blocks" => [microblock_hash()]}.
|
|
% </pre>
|
|
-type account() :: #{string() => term()}.
|
|
% <pre>
|
|
% #{"balance" => non_neg_integer(),
|
|
% "id" => account_id(),
|
|
% "kind" => "basic",
|
|
% "nonce" => pos_integer(),
|
|
% "payable" => true}.
|
|
% </pre>
|
|
-type contract_data() :: #{string() => term()}.
|
|
% <pre>
|
|
% #{"abi_version " => pos_integer(),
|
|
% "active" => boolean(),
|
|
% "deposit" => non_neg_integer(),
|
|
% "id" => contract_id(),
|
|
% "owner_id" => account_id() | contract_id(),
|
|
% "referrer_ids" => [],
|
|
% "vm_version" => pos_integer()}.
|
|
% </pre>
|
|
-type name_info() :: #{string() => term()}.
|
|
% <pre>
|
|
% #{"id" => name_hash(),
|
|
% "owner" => account_id(),
|
|
% "pointers" => [],
|
|
% "ttl" => non_neg_integer()}.
|
|
% </pre>
|
|
-type status() :: #{string() => term()}.
|
|
% <pre>
|
|
% #{"difficulty" => non_neg_integer(),
|
|
% "genesis_key_block_hash" => keyblock_hash(),
|
|
% "listening" => boolean(),
|
|
% "network_id" => string(),
|
|
% "node_revision" => string(),
|
|
% "node_version" => string(),
|
|
% "peer_connections" => #{"inbound" => non_neg_integer(),
|
|
% "outbound" => non_neg_integer()},
|
|
% "peer_count" => non_neg_integer(),
|
|
% "peer_pubkey" => peer_pubkey(),
|
|
% "pending_transactions_count" => 51,
|
|
% "protocols" => [protocol_info()],
|
|
% "solutions" => non_neg_integer(),
|
|
% "sync_progress" => float(),
|
|
% "syncing" => boolean(),
|
|
% "top_block_height" => non_neg_integer(),
|
|
% "top_key_block_hash" => keyblock_hash()}.
|
|
% </pre>
|
|
|
|
|
|
|
|
%%% Get/Set admin functions
|
|
|
|
-spec network_id() -> Result
|
|
when Result :: {ok, NetworkID} | {error, Reason},
|
|
NetworkID :: string(),
|
|
Reason :: term().
|
|
%% @doc
|
|
%% Returns the network ID or the atom `none' if it is unset.
|
|
%% Checking this is not normally necessary, but if network ID assignment is dynamic
|
|
%% in your system it may be necessary to call this before attempting to form
|
|
%% call data or perform other actions on chain that require a signature.
|
|
|
|
network_id() ->
|
|
case status() of
|
|
{ok, #{"network_id" := NetworkID}} -> {ok, NetworkID};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec chain_nodes() -> [chain_node()].
|
|
%% @doc
|
|
%% Returns the list of currently assigned nodes.
|
|
%% The normal reason to call this is in preparation for altering the nodes list or
|
|
%% checking the current list in debugging.
|
|
|
|
chain_nodes() ->
|
|
hz_man:chain_nodes().
|
|
|
|
|
|
-spec chain_nodes(List) -> ok | {error, Reason}
|
|
when List :: [chain_node()],
|
|
Reason :: {invalid, [term()]}.
|
|
%% @doc
|
|
%% Sets the nodes that are intended to be used as your interface to the peer
|
|
%% network. The common situation is that your project runs a non-mining node as
|
|
%% part of your backend infrastructure. Typically one or two nodes is plenty, but
|
|
%% this may need to expand depending on how much query load your application generates.
|
|
%% The Hakuzaru manager will load balance by round-robin distribution.
|
|
%%
|
|
%% NOTE: When load balancing in this way be aware that there can be race conditions
|
|
%% among the backend nodes with regard to a single account's current nonce when performing
|
|
%% contract calls in quick succession. Round robin distribution is extremely useful when
|
|
%% performing rapid lookups to the chain, but does not work well when submitting many
|
|
%% transactions to the chain from a single user in a short period of time. A future version
|
|
%% of this library will allow the caller to designate a single node as "sticky" to be used
|
|
%% exclusively in the case of nonce reads and TX submissions.
|
|
|
|
chain_nodes(List) when is_list(List) ->
|
|
hz_man:chain_nodes(List).
|
|
|
|
|
|
-spec tls() -> boolean().
|
|
%% @doc
|
|
%% Check whether TLS is in use.
|
|
|
|
tls() ->
|
|
hz_man:tls().
|
|
|
|
|
|
-spec tls(boolean()) -> ok.
|
|
%% @doc
|
|
%% Set TLS true or false. That's what a boolean is, by the way, `true' or `false'.
|
|
%% This is a condescending comment. That means I am talking down to you.
|
|
|
|
tls(Boolean) ->
|
|
hz_man:tls(Boolean).
|
|
|
|
|
|
|
|
-spec timeout() -> Timeout
|
|
when Timeout :: pos_integer() | infinity.
|
|
%% @doc
|
|
%% Returns the current request timeout setting in milliseconds.
|
|
|
|
timeout() ->
|
|
hz_man:timeout().
|
|
|
|
|
|
-spec timeout(MS) -> ok
|
|
when MS :: pos_integer() | infinity.
|
|
%% @doc
|
|
%% Sets the request timeout in milliseconds.
|
|
|
|
timeout(MS) ->
|
|
hz_man:timeout(MS).
|
|
|
|
|
|
|
|
%%% JSON query interface functions
|
|
|
|
|
|
-spec top_height() -> {ok, Height} | {error, Reason}
|
|
when Height :: pos_integer(),
|
|
Reason :: chain_error().
|
|
%% @doc
|
|
%% Retrieve the current height of the chain.
|
|
%%
|
|
%% NOTE:
|
|
%% This will return the currently synced height, which may be different than the
|
|
%% actual current top of the entire chain if the node being queried is still syncing
|
|
%% (has not yet caught up with the chain).
|
|
|
|
top_height() ->
|
|
case top_block() of
|
|
{ok, #{"height" := Height}} -> {ok, Height};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec top_block() -> {ok, TopBlock} | {error, Reason}
|
|
when TopBlock :: microblock_header(),
|
|
Reason :: chain_error().
|
|
%% @doc
|
|
%% Returns the current block height as an integer.
|
|
|
|
top_block() ->
|
|
request("/v3/headers/top").
|
|
|
|
|
|
-spec kb_current() -> {ok, CurrentBlock} | {error, Reason}
|
|
when CurrentBlock :: keyblock(),
|
|
Reason :: chain_error().
|
|
%% @doc
|
|
%% Returns the current keyblock's metadata as a map.
|
|
|
|
kb_current() ->
|
|
request("/v3/key-blocks/current").
|
|
|
|
|
|
-spec kb_current_hash() -> {ok, Hash} | {error, Reason}
|
|
when Hash :: keyblock_hash(),
|
|
Reason :: chain_error().
|
|
%% @doc
|
|
%% Returns the current keyblock's hash.
|
|
%% Equivalent of calling:
|
|
%% ```
|
|
%% {ok, Current} = kb_current(),
|
|
%% maps:get("hash", Current),
|
|
%% '''
|
|
|
|
kb_current_hash() ->
|
|
case request("/v3/key-blocks/current/hash") of
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
{ok, #{"hash" := Hash}} -> {ok, Hash};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec kb_current_height() -> {ok, Height} | {error, Reason}
|
|
when Height :: pos_integer(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Returns the current keyblock's height as an integer.
|
|
%% Equivalent of calling:
|
|
%% ```
|
|
%% {ok, Current} = kb_current(),
|
|
%% maps:get("height", Current),
|
|
%% '''
|
|
|
|
kb_current_height() ->
|
|
case request("/v3/key-blocks/current/height") of
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
{ok, #{"height" := Height}} -> {ok, Height};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec kb_pending() -> {ok, keyblock_hash()} | {error, Reason}
|
|
when Reason :: string().
|
|
%% @doc
|
|
%% Request the hash of the pending keyblock of a mining node's beneficiary.
|
|
%% If the node queried is not configured for mining it will return
|
|
%% `{error, "Beneficiary not configured"}'
|
|
|
|
kb_pending() ->
|
|
result(request("/v3/key-blocks/pending")).
|
|
|
|
|
|
-spec kb_by_hash(ID) -> {ok, KeyBlock} | {error, Reason}
|
|
when ID :: keyblock_hash(),
|
|
KeyBlock :: keyblock(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Returns the keyblock identified by the provided hash.
|
|
|
|
kb_by_hash(ID) ->
|
|
result(request(["/v3/key-blocks/hash/", ID])).
|
|
|
|
|
|
-spec kb_by_height(Height) -> {ok, KeyBlock} | {error, Reason}
|
|
when Height :: non_neg_integer(),
|
|
KeyBlock :: keyblock(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Returns the keyblock identigied by the provided height.
|
|
|
|
kb_by_height(Height) ->
|
|
StringN = integer_to_list(Height),
|
|
result(request(["/v3/key-blocks/height/", StringN])).
|
|
|
|
|
|
%kb_insert(KeyblockData) ->
|
|
% request("/v3/key-blocks", KeyblockData).
|
|
|
|
|
|
-spec mb_header(ID) -> {ok, MB_Header} | {error, Reason}
|
|
when ID :: microblock_hash(),
|
|
MB_Header :: microblock_header(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Returns the header of the microblock indicated by the provided ID (hash).
|
|
|
|
mb_header(ID) ->
|
|
result(request(["/v3/micro-blocks/hash/", ID, "/header"])).
|
|
|
|
|
|
-spec mb_txs(ID) -> {ok, TXs} | {error, Reason}
|
|
when ID :: microblock_hash(),
|
|
TXs :: [transaction()],
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Returns a list of transactions included in the microblock.
|
|
|
|
mb_txs(ID) ->
|
|
case request(["/v3/micro-blocks/hash/", ID, "/transactions"]) of
|
|
{ok, #{"transactions" := TXs}} -> {ok, TXs};
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec mb_tx_index(MicroblockID, Index) -> {ok, TX} | {error, Reason}
|
|
when MicroblockID :: microblock_hash(),
|
|
Index :: pos_integer(),
|
|
TX :: transaction(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve a single transaction from a microblock by index.
|
|
%% (Note that indexes start from 1, not zero.)
|
|
|
|
mb_tx_index(ID, Index) ->
|
|
StrHeight = integer_to_list(Index),
|
|
result(request(["/v3/micro-blocks/hash/", ID, "/transactions/index/", StrHeight])).
|
|
|
|
|
|
-spec mb_tx_count(ID) -> {ok, Count} | {error, Reason}
|
|
when ID :: microblock_hash(),
|
|
Count :: non_neg_integer(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve the number of transactions contained in the indicated microblock.
|
|
|
|
mb_tx_count(ID) ->
|
|
case request(["/v3/micro-blocks/hash/", ID, "/transactions/count"]) of
|
|
{ok, #{"count" := Count}} -> {ok, Count};
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec gen_current() -> {ok, Generation} | {error, Reason}
|
|
when Generation :: generation(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve the generation data (keyblock and list of associated microblocks) for
|
|
%% the current generation.
|
|
|
|
gen_current() ->
|
|
result(request("/v3/generations/current")).
|
|
|
|
|
|
-spec gen_by_id(ID) -> {ok, Generation} | {error, Reason}
|
|
when ID :: keyblock_hash(),
|
|
Generation :: generation(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve generation data (keyblock and list of associated microblocks) by keyhash.
|
|
|
|
gen_by_id(ID) ->
|
|
result(request(["/v3/generations/hash/", ID])).
|
|
|
|
|
|
-spec gen_by_height(Height) -> {ok, Generation} | {error, Reason}
|
|
when Height :: non_neg_integer(),
|
|
Generation :: generation(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve generation data (keyblock and list of associated microblocks) by height.
|
|
|
|
gen_by_height(Height) ->
|
|
StrHeight = integer_to_list(Height),
|
|
result(request(["/v3/generations/height/", StrHeight])).
|
|
|
|
|
|
-spec acc(AccountID) -> {ok, Account} | {error, Reason}
|
|
when AccountID :: account_id(),
|
|
Account :: account(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve account data by account ID (public key).
|
|
|
|
acc(AccountID) ->
|
|
result(request(["/v3/accounts/", AccountID])).
|
|
|
|
|
|
-spec acc_at_height(AccountID, Height) -> {ok, Account} | {error, Reason}
|
|
when AccountID :: account_id(),
|
|
Height :: non_neg_integer(),
|
|
Account :: account(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve data for an account as that account existed at the given height.
|
|
|
|
acc_at_height(AccountID, Height) ->
|
|
StrHeight = integer_to_list(Height),
|
|
case request(["/v3/accounts/", AccountID, "/height/", StrHeight]) of
|
|
{ok, #{"reason" := "Internal server error"}} -> {error, gc_out_of_range};
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
Result -> Result
|
|
end.
|
|
|
|
|
|
-spec acc_at_block_id(AccountID, BlockID) -> {ok, Account} | {error, Reason}
|
|
when AccountID :: account_id(),
|
|
BlockID :: keyblock_hash() | microblock_hash(),
|
|
Account :: account(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve data for an account as that account existed at the moment the given
|
|
%% block represented the current state of the chain.
|
|
|
|
acc_at_block_id(AccountID, BlockID) ->
|
|
case request(["/v3/accounts/", AccountID, "/hash/", BlockID]) of
|
|
{ok, #{"reason" := "Internal server error"}} -> {error, gc_out_of_range};
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
Result -> Result
|
|
end.
|
|
|
|
|
|
-spec acc_pending_txs(AccountID) -> {ok, TXs} | {error, Reason}
|
|
when AccountID :: account_id(),
|
|
TXs :: [tx_hash()],
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve a list of transactions pending for the given account.
|
|
|
|
acc_pending_txs(AccountID) ->
|
|
request(["/v3/accounts/", AccountID, "/transactions/pending"]).
|
|
|
|
|
|
-spec next_nonce(AccountID) -> {ok, Nonce} | {error, Reason}
|
|
when AccountID :: account_id(),
|
|
Nonce :: non_neg_integer(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve the next nonce for the given account
|
|
|
|
next_nonce(AccountID) ->
|
|
% case request(["/v3/accounts/", AccountID, "/next-nonce"]) of
|
|
% {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce};
|
|
% {ok, #{"reason" := "Account not found"}} -> {ok, 1};
|
|
% {ok, #{"reason" := Reason}} -> {error, Reason};
|
|
% Error -> Error
|
|
% end.
|
|
case request(["/v3/accounts/", AccountID]) of
|
|
{ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1};
|
|
{ok, #{"reason" := "Account not found"}} -> {ok, 1};
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec dry_run(TX) -> {ok, Result} | {error, Reason}
|
|
when TX :: binary() | string(),
|
|
Result :: term(), % FIXME
|
|
Reason :: term(). % FIXME
|
|
%% @doc
|
|
%% Execute a read-only transaction on the chain at the current height.
|
|
%% Equivalent of
|
|
%% ```
|
|
%% {ok, Hash} = hz:kb_current_hash(),
|
|
%% hz:dry_run(TX, Hash),
|
|
%% '''
|
|
%% NOTE:
|
|
%% For this function to work the Gajumaru node you are sending the request
|
|
%% to must have its configuration set to `http: endpoints: dry-run: true'
|
|
|
|
dry_run(TX) ->
|
|
dry_run(TX, []).
|
|
|
|
|
|
-spec dry_run(TX, Accounts) -> {ok, Result} | {error, Reason}
|
|
when TX :: binary() | string(),
|
|
Accounts :: [pubkey()],
|
|
Result :: term(), % FIXME
|
|
Reason :: term(). % FIXME
|
|
%% @doc
|
|
%% Execute a read-only transaction on the chain at the current height with the
|
|
%% supplied accounts.
|
|
|
|
dry_run(TX, Accounts) ->
|
|
case top_block() of
|
|
{ok, #{"hash" := Hash}} -> dry_run(TX, Accounts, Hash);
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec dry_run(TX, Accounts, KBHash) -> {ok, Result} | {error, Reason}
|
|
when TX :: binary() | string(),
|
|
Accounts :: [pubkey()],
|
|
KBHash :: binary() | string(),
|
|
Result :: term(), % FIXME
|
|
Reason :: term(). % FIXME
|
|
%% @doc
|
|
%% Execute a read-only transaction on the chain at the height indicated by the
|
|
%% hash provided.
|
|
|
|
dry_run(TX, Accounts, KBHash) ->
|
|
KBB = to_binary(KBHash),
|
|
TXB = to_binary(TX),
|
|
DryData = #{top => KBB,
|
|
accounts => Accounts,
|
|
txs => [#{tx => TXB}],
|
|
tx_events => true},
|
|
JSON = zj:binary_encode(DryData),
|
|
request("/v3/dry_run", JSON).
|
|
|
|
|
|
dry_run_map(Map) ->
|
|
JSON = zj:binary_encode(Map),
|
|
request("/v3/dry_run", JSON).
|
|
|
|
|
|
-spec decode_bytearray_fate(EncodedStr) -> {ok, Result} | {error, Reason}
|
|
when EncodedStr :: binary() | string(),
|
|
Result :: none | term(),
|
|
Reason :: term().
|
|
|
|
%% @doc
|
|
%% Decode the "cb_XXXX" string that came out of a tx_info or dry_run, to
|
|
%% the Erlang representation of FATE objects used by gmb_fate_encoding. See
|
|
%% decode_bytearray/2 for an alternative that provides simpler outputs based on
|
|
%% information provided by an AACI.
|
|
|
|
decode_bytearray_fate(EncodedStr) ->
|
|
Encoded = unicode:characters_to_binary(EncodedStr),
|
|
{contract_bytearray, Binary} = gmser_api_encoder:decode(Encoded),
|
|
case Binary of
|
|
<<>> -> {ok, none};
|
|
<<"Out of gas">> -> {error, out_of_gas};
|
|
_ ->
|
|
% FIXME there may be other errors that are encoded directly into
|
|
% the byte array. We could try and catch to at least return
|
|
% *something* for cases that we don't already detect.
|
|
Object = gmb_fate_encoding:deserialize(Binary),
|
|
{ok, Object}
|
|
end.
|
|
|
|
-spec decode_bytearray(Type, EncodedStr) -> {ok, Result} | {error, Reason}
|
|
when Type :: term(),
|
|
EncodedStr :: binary() | string(),
|
|
Result :: none | term(),
|
|
Reason :: term().
|
|
|
|
%% @doc
|
|
%% Decode the "cb_XXXX" string that came out of a tx_info or dry_run, to the
|
|
%% same format used by contract_call/* and contract_create/*. The Type argument
|
|
%% must be the result type of the same function in the same AACI that was used
|
|
%% to create the transaction that EncodedStr came from.
|
|
|
|
decode_bytearray(Type, EncodedStr) ->
|
|
case decode_bytearray_fate(EncodedStr) of
|
|
{ok, none} -> {ok, none};
|
|
{ok, Object} -> coerce(Type, Object, from_fate);
|
|
{error, Reason} -> {error, Reason}
|
|
end.
|
|
|
|
to_binary(S) when is_binary(S) -> S;
|
|
to_binary(S) when is_list(S) -> list_to_binary(S).
|
|
|
|
|
|
-spec tx(ID) -> {ok, TX} | {error, Reason}
|
|
when ID :: tx_hash(),
|
|
TX :: transaction(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve a transaction by ID.
|
|
|
|
tx(ID) ->
|
|
request(["/v3/transactions/", ID]).
|
|
|
|
|
|
-spec tx_info(ID) -> {ok, Info} | {error, Reason}
|
|
when ID :: tx_hash(),
|
|
Info :: term(), % FIXME
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve TX metadata by ID.
|
|
|
|
tx_info(ID) ->
|
|
result(request(["/v3/transactions/", ID, "/info"])).
|
|
|
|
-spec post_tx(Data) -> {ok, Result} | {error, Reason}
|
|
when Data :: string() | binary(),
|
|
Result :: term(), % FIXME
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Post a transaction to the chain.
|
|
|
|
post_tx(Data) when is_binary(Data) ->
|
|
JSON = zj:binary_encode(#{tx => Data}),
|
|
request("/v3/transactions", JSON);
|
|
post_tx(Data) when is_list(Data) ->
|
|
post_tx(list_to_binary(Data)).
|
|
|
|
|
|
-spec contract(ID) -> {ok, ContractData} | {error, Reason}
|
|
when ID :: contract_id(),
|
|
ContractData :: contract_data(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve a contract's metadata by ID.
|
|
|
|
contract(ID) ->
|
|
result(request(["/v3/contracts/", ID])).
|
|
|
|
|
|
-spec contract_code(ID) -> {ok, Bytecode} | {error, Reason}
|
|
when ID :: contract_id(),
|
|
Bytecode :: contract_byte_array(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve the code of a contract as represented on chain.
|
|
|
|
contract_code(ID) ->
|
|
case request(["/v3/contracts/", ID, "/code"]) of
|
|
{ok, #{"bytecode" := Bytecode}} -> {ok, Bytecode};
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec contract_source(ID) -> {ok, Bytecode} | {error, Reason}
|
|
when ID :: contract_id(),
|
|
Bytecode :: contract_byte_array(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve the code of a contract as represented on chain.
|
|
|
|
contract_source(ID) ->
|
|
case request(["/v3/contracts/", ID, "/source"]) of
|
|
{ok, #{"source" := Source}} -> {ok, Source};
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec contract_poi(ID) -> {ok, Bytecode} | {error, Reason}
|
|
when ID :: contract_id(),
|
|
Bytecode :: contract_byte_array(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve the POI of a contract stored on chain.
|
|
|
|
contract_poi(ID) ->
|
|
request(["/v3/contracts/", ID, "/poi"]).
|
|
|
|
|
|
-spec name(Name) -> {ok, Info} | {error, Reason}
|
|
when Name :: string(), % _ ++ ".chain"
|
|
Info :: name_info(),
|
|
Reason :: chain_error() | string().
|
|
%% @doc
|
|
%% Retrieve a name's chain information.
|
|
|
|
name(Name) ->
|
|
result(request(["/v3/names/", Name])).
|
|
|
|
|
|
% TODO
|
|
%channel(ID) ->
|
|
% request(["/v3/channels/", ID]).
|
|
|
|
|
|
% FIXME: This should take a specific peer address:port otherwise it will be pointlessly
|
|
% random.
|
|
-spec peer_pubkey() -> {ok, Pubkey} | {error, Reason}
|
|
when Pubkey :: peer_pubkey(),
|
|
Reason :: term(). % FIXME
|
|
%% @doc
|
|
%% Returns the given node's public key, assuming a node is reachable at
|
|
%% the given address.
|
|
|
|
peer_pubkey() ->
|
|
case request("/v3/peers/pubkey") of
|
|
{ok, #{"pubkey" := Pubkey}} -> {ok, Pubkey};
|
|
{ok, #{"reason" := Reason}} -> {error, Reason};
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
% TODO: Make a status/1 that allows the caller to query a specific node rather than
|
|
% a random one from the pool.
|
|
-spec status() -> {ok, Status} | {error, Reason}
|
|
when Status :: status(),
|
|
Reason :: chain_error().
|
|
%% @doc
|
|
%% Retrieve the node's status and meta it currently has about the chain.
|
|
|
|
status() ->
|
|
request("/v3/status").
|
|
|
|
|
|
-spec status_chainends() -> {ok, ChainEnds} | {error, Reason}
|
|
when ChainEnds :: [keyblock_hash()],
|
|
Reason :: chain_error().
|
|
%% @doc
|
|
%% Retrieve the latest keyblock hashes
|
|
|
|
status_chainends() ->
|
|
request("/v3/status/chain-ends").
|
|
|
|
|
|
request(Path) ->
|
|
hz_man:request(unicode:characters_to_list(Path)).
|
|
|
|
|
|
request(Path, Payload) ->
|
|
hz_man:request(unicode:characters_to_list(Path), Payload).
|
|
|
|
|
|
result({ok, #{"reason" := Reason}}) -> {error, Reason};
|
|
result(Received) -> Received.
|
|
|
|
|
|
|
|
%%% Contract calls
|
|
|
|
-spec contract_create(CreatorID, Path, InitArgs) -> Result
|
|
when CreatorID :: unicode:chardata(),
|
|
Path :: file:filename(),
|
|
InitArgs :: [string()],
|
|
Result :: {ok, CreateTX} | {error, Reason},
|
|
CreateTX :: binary(),
|
|
Reason :: file:posix() | term().
|
|
%% @doc
|
|
%% This function reads the source of a Sophia contract (an .aes file)
|
|
%% and returns the unsigned create contract call data with default values.
|
|
%% For more control over exactly what those values are, use create_contract/8.
|
|
|
|
contract_create(CreatorID, Path, InitArgs) ->
|
|
case next_nonce(CreatorID) of
|
|
{ok, Nonce} ->
|
|
Amount = 0,
|
|
{ok, Height} = top_height(),
|
|
TTL = Height + 262980,
|
|
Gas = 500000,
|
|
GasPrice = min_gas_price(),
|
|
contract_create(CreatorID, Nonce,
|
|
Amount, TTL, Gas, GasPrice,
|
|
Path, InitArgs);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
-spec contract_create(CreatorID, Nonce,
|
|
Amount, TTL, Gas, GasPrice,
|
|
Path, InitArgs) -> Result
|
|
when CreatorID :: pubkey(),
|
|
Nonce :: pos_integer(),
|
|
Amount :: non_neg_integer(),
|
|
TTL :: non_neg_integer(),
|
|
Gas :: pos_integer(),
|
|
GasPrice :: pos_integer(),
|
|
Path :: file:filename(),
|
|
InitArgs :: [string()],
|
|
Result :: {ok, CreateTX} | {error, Reason},
|
|
CreateTX :: binary(),
|
|
Reason :: term().
|
|
%% @doc
|
|
%% Create a "create contract" call using the supplied values.
|
|
%%
|
|
%% Contract creation is an even more opaque process than contract calls if you're new
|
|
%% to Gajumaru.
|
|
%%
|
|
%% The meaning of each argument is as follows:
|
|
%% <ul>
|
|
%% <li>
|
|
%% <b>CreatorID:</b>
|
|
%% This is the <em>public</em> key of the entity who will be posting the contract
|
|
%% to the chain.
|
|
%% The key must be encoded as a string prefixed with `"ak_"' (or `<<"ak_">>' in the
|
|
%% case of a binary string, which is also acceptable).
|
|
%% The returned call will still need to be signed by the caller's <em>private</em>
|
|
%% key.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Nonce:</b>
|
|
%% This is a sequential integer value that ensures that the hash value of two
|
|
%% sequential signed calls with the same contract ID, function and arguments can
|
|
%% never be the same.
|
|
%% This avoids replay attacks and ensures indempotency despite the distributed
|
|
%% nature of the blockchain network).
|
|
%% Every CallerID on the chain has a "next nonce" value that can be discovered by
|
|
%% querying your Gajumaru node (via `hz:next_nonce(CallerID)', for example).
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Amount:</b>
|
|
%% All Gajumaru transactions can carry an "amount" spent from the origin account
|
|
%% (in this case the `CallerID') to the destination. In a "Spend" transaction this
|
|
%% is the only value that really matters, but in a contract call the utility is
|
|
%% quite different, as you can pay money <em>into</em> a contract and have that
|
|
%% contract hold it (for future payouts, to be held in escrow, as proof of intent
|
|
%% to purchase or engage in an auction, whatever). Typically this value is 0, but
|
|
%% of course there are very good reasons why it should be set to a non-zero value
|
|
%% in the case of calls related to contract-governed payment systems.
|
|
%% </li>
|
|
%% <b>TTL:</b>
|
|
%% This stands for "Time-To-Live", meaning the height beyond which this element is
|
|
%% considered to be eligible for garbage collection (and therefore inaccessible!).
|
|
%% The TTL can be extended by a "live extension" transaction (basically pay for the
|
|
%% data to remain alive longer).
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Gas:</b>
|
|
%% This number sets a limit on the maximum amount of computation the caller is willing
|
|
%% to pay for on the chain.
|
|
%% Both storage and thunks are costly as the entire Gajumaru network must execute,
|
|
%% verify, store and replicate all state changes to the chain.
|
|
%% Each byte stored on the chain carries a cost of 20 gas, which is not an issue if
|
|
%% you are storing persistent values of some state trasforming computation, but
|
|
%% high enough to discourage frivolous storage of media on the chain (which would be
|
|
%% a burden to the entire network).
|
|
%% Computation is less expensive, but still costs and is calculated very similarly
|
|
%% to the Erlang runtime's per-process reduction budget.
|
|
%% The maximum amount of gas that a microblock is permitted to carry (its maximum
|
|
%% computational weight, so to speak) is 6,000,000.
|
|
%% Typical contract calls range between about 100 to 15,000 gas, so the default gas
|
|
%% limit set by the `contract_call/6' function is only 20,000.
|
|
%% Setting the gas limit to 6,000,000 or more will cause your contract call to fail.
|
|
%% All transactions cost some gas with the exception of stateless or read-only
|
|
%% calls to your Gajumaru node (executed as "dry run" calls and not propagated to
|
|
%% the network).
|
|
%% The gas consumed by the contract call transaction is multiplied by the `GasPrice'
|
|
%% provided and rolled into the block reward paid out to the node that mines the
|
|
%% transaction into a microblock.
|
|
%% Unused gas is refunded to the caller.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>GasPrice:</b>
|
|
%% This is a factor that is used calculate a value in pucks (the smallest unit of
|
|
%% Gajumaru's currency value) for the gas consumed. In times of high contention
|
|
%% in the mempool increasing the gas price increases the value of mining a given
|
|
%% transaction, thus making miners more likely to prioritize the high value ones.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>ACI:</b>
|
|
%% This is the compiled contract's metadata. It provides the information necessary
|
|
%% for the contract call data to be formed in a way that the Gajumaru runtime will
|
|
%% understand.
|
|
%% This ACI data must be already formatted in the native Erlang format as an .aci
|
|
%% file rather than as the JSON serialized format produced by the Sophia CLI tool.
|
|
%% The easiest way to create native ACI data is to use the Gajumaru Launcher,
|
|
%% a GUI tool with a "Developers' Workbench" feature that can assist with this.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>ConID:</b>
|
|
%% This is the on-chain address of the contract instance that is to be called.
|
|
%% Note, this is different from the `name' of the contract, as a single contract may
|
|
%% be deployed multiple times.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Fun:</b>
|
|
%% This is the name of the entrypoint function to be called on the contract,
|
|
%% provided as a string (not a binary string, but a textual string as a list).
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Args:</b>
|
|
%% This is a list of the arguments to provide to the function, listed in order
|
|
%% according to the function's spec, and represented as strings (that is, an integer
|
|
%% argument of `10' must be cast to the textual representation `"10"').
|
|
%% </li>
|
|
%% </ul>
|
|
%% As should be obvious from the above description, it is pretty helpful to have a
|
|
%% source copy of the contract you intend to call so that you can re-generate the ACI
|
|
%% if you do not already have a copy, and can check the spec of a function before
|
|
%% trying to form a contract call.
|
|
|
|
contract_create(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Path, InitArgs) ->
|
|
case file:read_file(Path) of
|
|
{ok, Source} ->
|
|
Dir = filename:dirname(Path),
|
|
{ok, CWD} = file:get_cwd(),
|
|
SrcDir = so_utils:canonical_dir(Path),
|
|
Options =
|
|
[{aci, json},
|
|
{src_file, Path},
|
|
{src_dir, SrcDir},
|
|
{include, {file_system, [CWD, so_utils:canonical_dir(Dir)]}}],
|
|
contract_create2(CreatorID, Nonce, Amount, TTL, Gas, GasPrice,
|
|
Source, Options, InitArgs);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
contract_create2(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Source, Options, InitArgs) ->
|
|
case so_compiler:from_string(Source, Options) of
|
|
{ok, Compiled} ->
|
|
contract_create_built(CreatorID, Nonce, Amount, TTL, Gas, GasPrice,
|
|
Compiled, InitArgs);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
-spec contract_create_built(CreatorID, Compiled, InitArgs) -> Result
|
|
when CreatorID :: unicode:chardata(),
|
|
Compiled :: map(),
|
|
InitArgs :: [string()],
|
|
Result :: {ok, CreateTX} | {error, Reason},
|
|
CreateTX :: binary(),
|
|
Reason :: file:posix() | term().
|
|
%% @doc
|
|
%% This function takes the compiler output (instead of starting from source),
|
|
%% and returns the unsigned create contract call data with default values.
|
|
%% For more control over exactly what those values are, use create_contract/8.
|
|
|
|
contract_create_built(CreatorID, Compiled, InitArgs) ->
|
|
case next_nonce(CreatorID) of
|
|
{ok, Nonce} ->
|
|
Amount = 0,
|
|
{ok, Height} = top_height(),
|
|
TTL = Height + 262980,
|
|
Gas = 500000,
|
|
GasPrice = min_gas_price(),
|
|
contract_create_built(CreatorID, Nonce,
|
|
Amount, TTL, Gas, GasPrice,
|
|
Compiled, InitArgs);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
contract_create_built(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, InitArgs) ->
|
|
AACI = prepare_aaci(maps:get(aci, Compiled)),
|
|
case encode_call_data(AACI, "init", InitArgs) of
|
|
{ok, CallData} ->
|
|
assemble_calldata(CreatorID, Nonce, Amount, TTL, Gas, GasPrice,
|
|
Compiled, CallData);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
assemble_calldata(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) ->
|
|
PK = unicode:characters_to_binary(CreatorID),
|
|
try
|
|
{account_pubkey, OwnerID} = gmser_api_encoder:decode(PK),
|
|
assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData)
|
|
catch
|
|
Error:Reason -> {Error, Reason}
|
|
end.
|
|
|
|
assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) ->
|
|
Code = gmser_contract_code:serialize(Compiled),
|
|
Source = unicode:characters_to_binary(maps:get(contract_source, Compiled, <<>>)),
|
|
VM = 1,
|
|
ABI = 1,
|
|
<<CTVersion:32>> = <<VM:16, ABI:16>>,
|
|
ContractCreateVersion = 1,
|
|
Type = contract_create_tx,
|
|
Fields =
|
|
[{owner_id, gmser_id:create(account, OwnerID)},
|
|
{nonce, Nonce},
|
|
{code, Code},
|
|
{source, Source},
|
|
{ct_version, CTVersion},
|
|
{ttl, TTL},
|
|
{deposit, 0},
|
|
{amount, Amount},
|
|
{gas_price, GasPrice},
|
|
{gas, Gas},
|
|
{call_data, CallData}],
|
|
Template =
|
|
[{owner_id, id},
|
|
{nonce, int},
|
|
{code, binary},
|
|
{source, binary},
|
|
{ct_version, int},
|
|
{ttl, int},
|
|
{deposit, int},
|
|
{amount, int},
|
|
{gas_price, int},
|
|
{gas, int},
|
|
{call_data, binary}],
|
|
TXB = gmser_chain_objects:serialize(Type, ContractCreateVersion, Template, Fields),
|
|
try
|
|
{ok, gmser_api_encoder:encode(transaction, TXB)}
|
|
catch
|
|
error:Reason -> {error, Reason}
|
|
end.
|
|
|
|
|
|
-spec read_aci(Path) -> Result
|
|
when Path :: file:filename(),
|
|
Result :: {ok, ACI} | {error, Reason},
|
|
ACI :: tuple(), % FIXME: Change to correct Sophia record
|
|
Reason :: file:posix() | bad_aci.
|
|
%% @doc
|
|
%% This function reads the contents of an .aci file.
|
|
%% ACI data is required for the contract call encoder to function properly.
|
|
%% ACI data is can be generated and stored in JSON data, and the Sophia CLI tool
|
|
%% can perform this action. Unfortunately, JSON is not the way that ACI data is
|
|
%% represented internally, and here we need the actual native representation.
|
|
%%
|
|
%% ACI encding/decoding and contract call encoding is significantly complex enough that
|
|
%% this provides for a pretty large savings in complexity for this library, dramatically
|
|
%% reduces runtime dependencies, and makes call encoding much more efficient (as a
|
|
%% huge number of steps are completely eliminated by this).
|
|
|
|
read_aci(Path) ->
|
|
case file:read_file(Path) of
|
|
{ok, Bin} ->
|
|
try
|
|
{ok, binary_to_term(Bin, [safe])}
|
|
catch
|
|
error:badarg -> {error, bad_aci}
|
|
end;
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
-spec contract_call(CallerID, AACI, ConID, Fun, Args) -> Result
|
|
when CallerID :: unicode:chardata(),
|
|
AACI :: aaci(),
|
|
ConID :: unicode:chardata(),
|
|
Fun :: string(),
|
|
Args :: [string()],
|
|
Result :: {ok, CallTX} | {error, Reason},
|
|
CallTX :: binary(),
|
|
Reason :: term().
|
|
%% @doc
|
|
%% Form a contract call using hardcoded default values for `Gas', `GasPrice',
|
|
%% and `Amount' to simplify the call (10 args is a bit much for normal calls!).
|
|
%% The values used are 20k for `Gas', the `GasPrice' is fixed at 1b (the
|
|
%% default "miner minimum" defined in default configs), and the `Amount' is 0.
|
|
%%
|
|
%% For details on the meaning of these and other argument values see the doc comment
|
|
%% for contract_call/10.
|
|
|
|
contract_call(CallerID, AACI, ConID, Fun, Args) ->
|
|
case next_nonce(CallerID) of
|
|
{ok, Nonce} ->
|
|
Gas = min_gas(),
|
|
GasPrice = min_gas_price(),
|
|
Amount = 0,
|
|
{ok, Height} = top_height(),
|
|
TTL = Height + 262980,
|
|
contract_call(CallerID, Nonce,
|
|
Gas, GasPrice, Amount, TTL,
|
|
AACI, ConID, Fun, Args);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
-spec contract_call(CallerID, Gas, AACI, ConID, Fun, Args) -> Result
|
|
when CallerID :: unicode:chardata(),
|
|
Gas :: pos_integer(),
|
|
AACI :: aaci(),
|
|
ConID :: unicode:chardata(),
|
|
Fun :: string(),
|
|
Args :: [string()],
|
|
Result :: {ok, CallTX} | {error, Reason},
|
|
CallTX :: binary(),
|
|
Reason :: term().
|
|
%% @doc
|
|
%% Just like contract_call/5, but allows you to specify the amount of gas
|
|
%% without getting into a major adventure with the other arguments.
|
|
%%
|
|
%% For details on the meaning of these and other argument values see the doc comment
|
|
%% for contract_call/10.
|
|
|
|
contract_call(CallerID, Gas, AACI, ConID, Fun, Args) ->
|
|
case next_nonce(CallerID) of
|
|
{ok, Nonce} ->
|
|
GasPrice = min_gas_price(),
|
|
Amount = 0,
|
|
{ok, Height} = top_height(),
|
|
TTL = Height + 262980,
|
|
contract_call(CallerID, Nonce,
|
|
Gas, GasPrice, Amount, TTL,
|
|
AACI, ConID, Fun, Args);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
-spec contract_call(CallerID, Nonce,
|
|
Gas, GasPrice, Amount, TTL,
|
|
AACI, ConID, Fun, Args) -> Result
|
|
when CallerID :: unicode:chardata(),
|
|
Nonce :: pos_integer(),
|
|
Gas :: pos_integer(),
|
|
GasPrice :: pos_integer(),
|
|
Amount :: non_neg_integer(),
|
|
TTL :: non_neg_integer(),
|
|
AACI :: aaci(),
|
|
ConID :: unicode:chardata(),
|
|
Fun :: string(),
|
|
Args :: [string()],
|
|
Result :: {ok, CallTX} | {error, Reason},
|
|
CallTX :: binary(),
|
|
Reason :: term().
|
|
%% @doc
|
|
%% Form a contract call using the supplied values.
|
|
%%
|
|
%% Contract call formation is a rather opaque process if you're new to Gajumaru or
|
|
%% smart contract execution in general.
|
|
%%
|
|
%% The meaning of each argument is as follows:
|
|
%% <ul>
|
|
%% <li>
|
|
%% <b>CallerID:</b>
|
|
%% This is the <em>public</em> key of the entity making the contract call.
|
|
%% The key must be encoded as a string prefixed with `"ak_"' (or `<<"ak_">>' in the
|
|
%% case of a binary string, which is also acceptable).
|
|
%% The returned call will still need to be signed by the caller's <em>private</em>
|
|
%% key.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Nonce:</b>
|
|
%% This is a sequential integer value that ensures that the hash value of two
|
|
%% sequential signed calls with the same contract ID, function and arguments can
|
|
%% never be the same.
|
|
%% This avoids replay attacks and ensures indempotency despite the distributed
|
|
%% nature of the blockchain network).
|
|
%% Every CallerID on the chain has a "next nonce" value that can be discovered by
|
|
%% querying your Gajumaru node (via `hz:next_nonce(CallerID)', for example).
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Gas:</b>
|
|
%% This number sets a limit on the maximum amount of computation the caller is willing
|
|
%% to pay for on the chain.
|
|
%% Both storage and thunks are costly as the entire Gajumaru network must execute,
|
|
%% verify, store and replicate all state changes to the chain.
|
|
%% Each byte stored on the chain carries a cost of 20 gas, which is not an issue if
|
|
%% you are storing persistent values of some state trasforming computation, but
|
|
%% high enough to discourage frivolous storage of media on the chain (which would be
|
|
%% a burden to the entire network).
|
|
%% Computation is less expensive, but still costs and is calculated very similarly
|
|
%% to the Erlang runtime's per-process reduction budget.
|
|
%% The maximum amount of gas that a microblock is permitted to carry (its maximum
|
|
%% computational weight, so to speak) is 6,000,000.
|
|
%% Typical contract calls range between about 100 to 15,000 gas, so the default gas
|
|
%% limit set by the `contract_call/6' function is only 20,000.
|
|
%% Setting the gas limit to 6,000,000 or more will cause your contract call to fail.
|
|
%% All transactions cost some gas with the exception of stateless or read-only
|
|
%% calls to your Gajumaru node (executed as "dry run" calls and not propagated to
|
|
%% the network).
|
|
%% The gas consumed by the contract call transaction is multiplied by the `GasPrice'
|
|
%% provided and rolled into the block reward paid out to the node that mines the
|
|
%% transaction into a microblock.
|
|
%% Unused gas is refunded to the caller.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>GasPrice:</b>
|
|
%% This is a factor that is used calculate a value in pucks (the smallest unit of
|
|
%% Gajumaru's currency value) for the gas consumed. In times of high contention
|
|
%% in the mempool increasing the gas price increases the value of mining a given
|
|
%% transaction, thus making miners more likely to prioritize the high value ones.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Amount:</b>
|
|
%% All Gajumaru transactions can carry an "amount" spent from the origin account
|
|
%% (in this case the `CallerID') to the destination. In a "Spend" transaction this
|
|
%% is the only value that really matters, but in a contract call the utility is
|
|
%% quite different, as you can pay money <em>into</em> a contract and have that
|
|
%% contract hold it (for future payouts, to be held in escrow, as proof of intent
|
|
%% to purchase or engage in an auction, whatever). Typically this value is 0, but
|
|
%% of course there are very good reasons why it should be set to a non-zero value
|
|
%% in the case of calls related to contract-governed payment systems.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>ACI:</b>
|
|
%% This is the compiled contract's metadata. It provides the information necessary
|
|
%% for the contract call data to be formed in a way that the Gajumaru runtime will
|
|
%% understand.
|
|
%% This ACI data must be already formatted in the native Erlang format as an .aci
|
|
%% file rather than as the JSON serialized format produced by the Sophia CLI tool.
|
|
%% The easiest way to create native ACI data is to use the Gajumaru Launcher,
|
|
%% a GUI tool with a "Developers' Workbench" feature that can assist with this.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>ConID:</b>
|
|
%% This is the on-chain address of the contract instance that is to be called.
|
|
%% Note, this is different from the `name' of the contract, as a single contract may
|
|
%% be deployed multiple times.
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Fun:</b>
|
|
%% This is the name of the entrypoint function to be called on the contract,
|
|
%% provided as a string (not a binary string, but a textual string as a list).
|
|
%% </li>
|
|
%% <li>
|
|
%% <b>Args:</b>
|
|
%% This is a list of the arguments to provide to the function, listed in order
|
|
%% according to the function's spec, and represented as strings (that is, an integer
|
|
%% argument of `10' must be cast to the textual representation `"10"').
|
|
%% </li>
|
|
%% </ul>
|
|
%% As should be obvious from the above description, it is pretty helpful to have a
|
|
%% source copy of the contract you intend to call so that you can re-generate the ACI
|
|
%% if you do not already have a copy, and can check the spec of a function before
|
|
%% trying to form a contract call.
|
|
|
|
contract_call(CallerID, Nonce, Gas, GP, Amount, TTL, AACI, ConID, Fun, Args) ->
|
|
case encode_call_data(AACI, Fun, Args) of
|
|
{ok, CD} -> contract_call2(CallerID, Nonce, Gas, GP, Amount, TTL, ConID, CD);
|
|
Error -> Error
|
|
end.
|
|
|
|
contract_call2(CallerID, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) ->
|
|
CallerBin = unicode:characters_to_binary(CallerID),
|
|
try
|
|
{account_pubkey, PK} = gmser_api_encoder:decode(CallerBin),
|
|
contract_call3(PK, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData)
|
|
catch
|
|
Error:Reason -> {Error, Reason}
|
|
end.
|
|
|
|
contract_call3(PK, Nonce, Gas, GasPrice, Amount, TTL, ConID, CallData) ->
|
|
ConBin = unicode:characters_to_binary(ConID),
|
|
try
|
|
{contract_pubkey, CK} = gmser_api_encoder:decode(ConBin),
|
|
contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData)
|
|
catch
|
|
Error:Reason -> {Error, Reason}
|
|
end.
|
|
|
|
contract_call4(PK, Nonce, Gas, GasPrice, Amount, TTL, CK, CallData) ->
|
|
ABI = 1,
|
|
CallVersion = 1,
|
|
Type = contract_call_tx,
|
|
Fields =
|
|
[{caller_id, gmser_id:create(account, PK)},
|
|
{nonce, Nonce},
|
|
{contract_id, gmser_id:create(contract, CK)},
|
|
{abi_version, ABI},
|
|
{ttl, TTL},
|
|
{amount, Amount},
|
|
{gas_price, GasPrice},
|
|
{gas, Gas},
|
|
{call_data, CallData}],
|
|
Template =
|
|
[{caller_id, id},
|
|
{nonce, int},
|
|
{contract_id, id},
|
|
{abi_version, int},
|
|
{ttl, int},
|
|
{amount, int},
|
|
{gas_price, int},
|
|
{gas, int},
|
|
{call_data, binary}],
|
|
TXB = gmser_chain_objects:serialize(Type, CallVersion, Template, Fields),
|
|
try
|
|
{ok, gmser_api_encoder:encode(transaction, TXB)}
|
|
catch
|
|
error:Reason -> {error, Reason}
|
|
end.
|
|
|
|
|
|
-spec prepare_contract(File) -> {ok, AACI} | {error, Reason}
|
|
when File :: file:filename(),
|
|
AACI :: aaci(),
|
|
Reason :: term().
|
|
%% @doc
|
|
%% Compile a contract and extract the function spec meta for use in future formation
|
|
%% of calldata
|
|
|
|
prepare_contract(File) ->
|
|
case so_compiler:file(File, [{aci, json}]) of
|
|
{ok, #{aci := ACI}} -> {ok, prepare_aaci(ACI)};
|
|
Error -> Error
|
|
end.
|
|
|
|
prepare_aaci(ACI) ->
|
|
% We want to take the types represented by the ACI, things like N1.T(N2.T),
|
|
% and dereference them down to concrete types like
|
|
% {tuple, [integer, string]}. Our type dereferencing algorithms
|
|
% shouldn't act directly on the JSON-based structures that the compiler
|
|
% gives us, though, though, so before we do the analysis, we should strip
|
|
% the ACI down to a list of 'opaque' type defintions and function specs.
|
|
{Name, OpaqueSpecs, TypeDefs} = convert_aci_types(ACI),
|
|
|
|
% Now that we have the opaque types, we can dereference the function specs
|
|
% down to the concrete types they actually represent. We annotate each
|
|
% subexpression of this concrete type with other info too, in case it helps
|
|
% make error messages easier to understand.
|
|
InternalTypeDefs = maps:merge(builtin_typedefs(), TypeDefs),
|
|
Specs = annotate_function_specs(OpaqueSpecs, InternalTypeDefs, #{}),
|
|
|
|
{aaci, Name, Specs, TypeDefs}.
|
|
|
|
convert_aci_types(ACI) ->
|
|
% Find the main contract, so we can get the specifications of its
|
|
% entrypoints.
|
|
[{NameBin, SpecDefs}] =
|
|
[{N, F}
|
|
|| #{contract := #{kind := contract_main,
|
|
functions := F,
|
|
name := N}} <- ACI],
|
|
Name = binary_to_list(NameBin),
|
|
% Turn these specifications into opaque types that we can reason about.
|
|
Specs = lists:map(fun convert_function_spec/1, SpecDefs),
|
|
|
|
% These specifications can reference other type definitions from the main
|
|
% contract and any other namespaces, so extract these types and convert
|
|
% them too.
|
|
TypeDefTree = lists:map(fun convert_namespace_typedefs/1, ACI),
|
|
% The tree structure of the ACI naturally leads to a tree of opaque types,
|
|
% but we want a map, so flatten it out before we continue.
|
|
TypeDefMap = collect_opaque_types(TypeDefTree, #{}),
|
|
|
|
% This is all the information we actually need from the ACI, the rest is
|
|
% just pre-compute and acceleration.
|
|
{Name, Specs, TypeDefMap}.
|
|
|
|
convert_function_spec(#{name := NameBin, arguments := Args, returns := Result}) ->
|
|
Name = binary_to_list(NameBin),
|
|
ArgTypes = lists:map(fun convert_arg/1, Args),
|
|
ResultType = opaque_type([], Result),
|
|
{Name, ArgTypes, ResultType}.
|
|
|
|
convert_arg(#{name := NameBin, type := TypeDef}) ->
|
|
Name = binary_to_list(NameBin),
|
|
Type = opaque_type([], TypeDef),
|
|
{Name, Type}.
|
|
|
|
convert_namespace_typedefs(#{namespace := NS}) ->
|
|
Name = namespace_name(NS),
|
|
convert_typedefs(NS, Name);
|
|
convert_namespace_typedefs(#{contract := NS}) ->
|
|
Name = namespace_name(NS),
|
|
ImplicitTypes = convert_implicit_types(NS, Name),
|
|
ExplicitTypes = convert_typedefs(NS, Name),
|
|
[ImplicitTypes, ExplicitTypes].
|
|
|
|
namespace_name(#{name := NameBin}) ->
|
|
binary_to_list(NameBin).
|
|
|
|
convert_implicit_types(#{state := StateDefACI}, Name) ->
|
|
StateDefOpaque = opaque_type([], StateDefACI),
|
|
[{Name, [], contract},
|
|
{Name ++ ".state", [], StateDefOpaque}];
|
|
convert_implicit_types(_, Name) ->
|
|
[{Name, [], contract}].
|
|
|
|
convert_typedefs(#{typedefs := TypeDefs}, Name) ->
|
|
convert_typedefs_loop(TypeDefs, Name ++ ".", []).
|
|
|
|
% Take a namespace that has already had a period appended, and use that as a
|
|
% prefix to convert and annotate a list of types.
|
|
convert_typedefs_loop([], _NamePrefix, Converted) ->
|
|
Converted;
|
|
convert_typedefs_loop([Next | Rest], NamePrefix, Converted) ->
|
|
#{name := NameBin, vars := ParamDefs, typedef := DefACI} = Next,
|
|
Name = NamePrefix ++ binary_to_list(NameBin),
|
|
Params = [binary_to_list(Param) || #{name := Param} <- ParamDefs],
|
|
Def = opaque_type(Params, DefACI),
|
|
convert_typedefs_loop(Rest, NamePrefix, [Converted, {Name, Params, Def}]).
|
|
|
|
collect_opaque_types([], Types) ->
|
|
Types;
|
|
collect_opaque_types([L | R], Types) ->
|
|
NewTypes = collect_opaque_types(L, Types),
|
|
collect_opaque_types(R, NewTypes);
|
|
collect_opaque_types({Name, Params, Def}, Types) ->
|
|
maps:put(Name, {Params, Def}, Types).
|
|
|
|
% Convert an ACI type defintion/spec into the 'opaque type' representation that
|
|
% our dereferencing algorithms can reason about.
|
|
opaque_type(Params, NameBin) when is_binary(NameBin) ->
|
|
Name = opaque_type_name(NameBin),
|
|
case not is_atom(Name) and lists:member(Name, Params) of
|
|
false -> Name;
|
|
true -> {var, Name}
|
|
end;
|
|
opaque_type(Params, #{record := FieldDefs}) ->
|
|
Fields = [{binary_to_list(Name), opaque_type(Params, Type)}
|
|
|| #{name := Name, type := Type} <- FieldDefs],
|
|
{record, Fields};
|
|
opaque_type(Params, #{variant := VariantDefs}) ->
|
|
ConvertVariant = fun(Pair) ->
|
|
[{Name, Types}] = maps:to_list(Pair),
|
|
{binary_to_list(Name), [opaque_type(Params, Type) || Type <- Types]}
|
|
end,
|
|
Variants = lists:map(ConvertVariant, VariantDefs),
|
|
{variant, Variants};
|
|
opaque_type(Params, #{tuple := TypeDefs}) ->
|
|
{tuple, [opaque_type(Params, Type) || Type <- TypeDefs]};
|
|
opaque_type(_, #{bytes := Count}) ->
|
|
{bytes, [Count]};
|
|
opaque_type(Params, Pair) when is_map(Pair) ->
|
|
[{Name, TypeArgs}] = maps:to_list(Pair),
|
|
{opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}.
|
|
|
|
% Atoms for any builtins that aren't qualified by a namespace in Sophia.
|
|
% Everything else stays as a string, user-defined or not.
|
|
opaque_type_name(<<"int">>) -> integer;
|
|
opaque_type_name(<<"bool">>) -> boolean;
|
|
opaque_type_name(<<"bits">>) -> bits;
|
|
opaque_type_name(<<"char">>) -> char;
|
|
opaque_type_name(<<"string">>) -> string;
|
|
opaque_type_name(<<"address">>) -> address;
|
|
opaque_type_name(<<"signature">>) -> signature;
|
|
opaque_type_name(<<"contract">>) -> contract;
|
|
opaque_type_name(<<"list">>) -> list;
|
|
opaque_type_name(<<"map">>) -> map;
|
|
% I'm not sure how to produce channels in Sophia source, but they seem to exist
|
|
% in gmb still.
|
|
opaque_type_name(<<"channel">>) -> channel;
|
|
opaque_type_name(Name) -> binary_to_list(Name).
|
|
|
|
builtin_typedefs() ->
|
|
#{"unit" => {[], {tuple, []}},
|
|
"void" => {[], {variant, []}},
|
|
"hash" => {[], {bytes, [32]}},
|
|
"option" => {["'T"], {variant, [{"None", []},
|
|
{"Some", [{var, "'T"}]}]}},
|
|
"Chain.ttl" => {[], {variant, [{"FixedTTL", [integer]},
|
|
{"RelativeTTL", [integer]}]}},
|
|
"AENS.pointee" => {[], {variant, [{"AccountPt", [address]},
|
|
{"OraclePt", [address]},
|
|
{"ContractPt", [address]},
|
|
{"ChannelPt", [address]}]}},
|
|
"AENS.name" => {[], {variant, [{"Name", [address,
|
|
"Chain.ttl",
|
|
{map, [string, "AENS.pointee"]}]}]}},
|
|
"AENSv2.pointee" => {[], {variant, [{"AccountPt", [address]},
|
|
{"OraclePt", [address]},
|
|
{"ContractPt", [address]},
|
|
{"ChannelPt", [address]},
|
|
{"DataPt", [{bytes, [any]}]}]}},
|
|
"AENSv2.name" => {[], {variant, [{"Name", [address,
|
|
"Chain.ttl",
|
|
{map, [string, "AENSv2.pointee"]}]}]}},
|
|
"Chain.ga_meta_tx" => {[], {variant, [{"GAMetaTx", [address, integer]}]}},
|
|
"Chain.paying_for_tx" => {[], {variant, [{"PayingForTx", [address, integer]}]}},
|
|
"Chain.base_tx" => {[], {variant, [{"SpendTx", [address, integer, string]},
|
|
{"OracleRegisterTx", []},
|
|
{"OracleQueryTx", []},
|
|
{"OracleResponseTx", []},
|
|
{"OracleExtendTx", []},
|
|
{"NamePreclaimTx", []},
|
|
{"NameClaimTx", ["hash"]},
|
|
{"NameUpdateTx", [string]},
|
|
{"NameRevokeTx", ["hash"]},
|
|
{"NameTransferTx", [address, string]},
|
|
{"ChannelCreateTx", [address]},
|
|
{"ChannelDepositTx", [address, integer]},
|
|
{"ChannelWithdrawTx", [address, integer]},
|
|
{"ChannelForceProgressTx", [address]},
|
|
{"ChannelCloseMutualTx", [address]},
|
|
{"ChannelCloseSoloTx", [address]},
|
|
{"ChannelSlashTx", [address]},
|
|
{"ChannelSettleTx", [address]},
|
|
{"ChannelSnapshotSoloTx", [address]},
|
|
{"ContractCreateTx", [integer]},
|
|
{"ContractCallTx", [address, integer]},
|
|
{"GAAttachTx", []}]}},
|
|
"Chain.tx" => {[], {record, [{"paying_for", {"option", ["Chain.paying_for_tx"]}},
|
|
{"ga_metas", {list, ["Chain.ga_meta_tx"]}},
|
|
{"actor", address},
|
|
{"fee", integer},
|
|
{"ttl", integer},
|
|
{"tx", "Chain.base_tx"}]}},
|
|
"MCL_BLS12_381.fr" => {[], {bytes, [32]}},
|
|
"MCL_BLS12_381.fp" => {[], {bytes, [48]}}
|
|
}.
|
|
|
|
% Type preparation has two goals. First, we need a data structure that can be
|
|
% traversed quickly, to take sophia-esque erlang expressions and turn them into
|
|
% fate-esque erlang expressions that gmbytecode can serialize. Second, we need
|
|
% partially substituted names, so that error messages can be generated for why
|
|
% "foobar" is not valid as the third field of a `bazquux`, because the third
|
|
% field is supposed to be `option(integer)`, not `string`.
|
|
%
|
|
% To achieve this we need three representations of each type expression, which
|
|
% together form an 'annotated type'. First, we need the fully opaque name,
|
|
% "bazquux", then we need the normalized name, which is an opaque name with the
|
|
% bare-minimum substitution needed to make the outer-most type-constructor an
|
|
% identifiable built-in, ADT, or record type, and then we need the dereferenced
|
|
% type, which is the raw {variant, [{Name, Fields}, ...]} or
|
|
% {record, [{Name, Type}]} expression that can be used in actual Sophia->FATE
|
|
% coercion. The type sub-expressions in these dereferenced types will each be
|
|
% fully annotated as well, i.e. they will each contain *all three* of the above
|
|
% representations, so that coercion of subexpressions remains fast AND
|
|
% informative.
|
|
%
|
|
% In a lot of cases the opaque type given will already be normalized, in which
|
|
% case either the normalized field or the non-normalized field of an annotated
|
|
% type can simple be the atom `already_normalized`, which means error messages
|
|
% can simply render the normalized type expression and know that the error will
|
|
% make sense.
|
|
|
|
annotate_function_specs([], _Types, Specs) ->
|
|
Specs;
|
|
annotate_function_specs([{Name, ArgsOpaque, ResultOpaque} | Rest], Types, Specs) ->
|
|
{ok, Args} = annotate_bindings(ArgsOpaque, Types, []),
|
|
{ok, Result} = annotate_type(ResultOpaque, Types),
|
|
NewSpecs = maps:put(Name, {Args, Result}, Specs),
|
|
annotate_function_specs(Rest, Types, NewSpecs).
|
|
|
|
annotate_type(T, Types) ->
|
|
case normalize_opaque_type(T, Types) of
|
|
{ok, AlreadyNormalized, NOpaque, NExpanded} ->
|
|
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
annotate_type2(T, _, _, unknown_type, _) ->
|
|
% If a type is unknown, then it should not be reported as the normalized
|
|
% name.
|
|
{ok, {T, unknown_type, unknown_type}};
|
|
annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) ->
|
|
case annotate_type_subexpressions(NExpanded, Types) of
|
|
{ok, Flat} ->
|
|
case AlreadyNormalized of
|
|
true -> {ok, {T, already_normalized, Flat}};
|
|
false -> {ok, {T, NOpaque, Flat}}
|
|
end;
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
annotate_types([T | Rest], Types, Acc) ->
|
|
case annotate_type(T, Types) of
|
|
{ok, Type} -> annotate_types(Rest, Types, [Type | Acc]);
|
|
Error -> Error
|
|
end;
|
|
annotate_types([], _Types, Acc) ->
|
|
{ok, lists:reverse(Acc)}.
|
|
|
|
annotate_type_subexpressions(PrimitiveType, _Types) when is_atom(PrimitiveType) ->
|
|
{ok, PrimitiveType};
|
|
annotate_type_subexpressions({bytes, [Count]}, _Types) ->
|
|
% bytes is weird, because it has an argument, but that argument isn't an
|
|
% opaque type.
|
|
{ok, {bytes, [Count]}};
|
|
annotate_type_subexpressions({variant, VariantsOpaque}, Types) ->
|
|
case annotate_variants(VariantsOpaque, Types, []) of
|
|
{ok, Variants} -> {ok, {variant, Variants}};
|
|
Error -> Error
|
|
end;
|
|
annotate_type_subexpressions({record, FieldsOpaque}, Types) ->
|
|
case annotate_bindings(FieldsOpaque, Types, []) of
|
|
{ok, Fields} -> {ok, {record, Fields}};
|
|
Error -> Error
|
|
end;
|
|
annotate_type_subexpressions({T, ElemsOpaque}, Types) ->
|
|
case annotate_types(ElemsOpaque, Types, []) of
|
|
{ok, Elems} -> {ok, {T, Elems}};
|
|
Error -> Error
|
|
end.
|
|
|
|
annotate_bindings([{Name, T} | Rest], Types, Acc) ->
|
|
case annotate_type(T, Types) of
|
|
{ok, Type} -> annotate_bindings(Rest, Types, [{Name, Type} | Acc]);
|
|
Error -> Error
|
|
end;
|
|
annotate_bindings([], _Types, Acc) ->
|
|
{ok, lists:reverse(Acc)}.
|
|
|
|
annotate_variants([{Name, Elems} | Rest], Types, Acc) ->
|
|
case annotate_types(Elems, Types, []) of
|
|
{ok, ElemsFlat} -> annotate_variants(Rest, Types, [{Name, ElemsFlat} | Acc]);
|
|
Error -> Error
|
|
end;
|
|
annotate_variants([], _Types, Acc) ->
|
|
{ok, lists:reverse(Acc)}.
|
|
|
|
% This function evaluates type aliases in a loop, until eventually a usable
|
|
% definition is found.
|
|
normalize_opaque_type(T, Types) -> normalize_opaque_type(T, Types, true).
|
|
|
|
% FIXME detect infinite loops
|
|
% FIXME detect builtins with the wrong number of arguments
|
|
% FIXME should nullary types have an empty list of arguments added before now?
|
|
normalize_opaque_type(T, _Types, IsFirst) when is_atom(T) ->
|
|
% Once we have eliminated the above rewrite cases, all other cases are
|
|
% handled explicitly by the coerce logic, and so are considered normalized.
|
|
{ok, IsFirst, T, T};
|
|
normalize_opaque_type(Type = {T, _}, _Types, IsFirst) when is_atom(T) ->
|
|
% Once we have eliminated the above rewrite cases, all other cases are
|
|
% handled explicitly by the coerce logic, and so are considered normalized.
|
|
{ok, IsFirst, Type, Type};
|
|
normalize_opaque_type(T, Types, IsFirst) when is_list(T) ->
|
|
% Lists/strings indicate userspace types, which may require arg
|
|
% substitutions. Convert to an explicit but empty arg list, for uniformity.
|
|
normalize_opaque_type({T, []}, Types, IsFirst);
|
|
normalize_opaque_type({T, TypeArgs}, Types, IsFirst) when is_list(T) ->
|
|
case maps:find(T, Types) of
|
|
error ->
|
|
% We couldn't find this named type... Keep building the AACI, but
|
|
% mark this type expression as unknown, so that FATE coercions
|
|
% aren't attempted.
|
|
{ok, IsFirst, {T, TypeArgs}, unknown_type};
|
|
{ok, {TypeParamNames, Definition}} ->
|
|
% We have a definition for this type, including names for whatever
|
|
% args we have been given. Subtitute our args into this.
|
|
NewType = substitute_opaque_type(TypeParamNames, Definition, TypeArgs),
|
|
% Now continue on to see if we need to restart the loop or not.
|
|
normalize_opaque_type2(IsFirst, {T, TypeArgs}, NewType, Types)
|
|
end.
|
|
|
|
normalize_opaque_type2(IsFirst, PrevType, NextType = {variant, _}, _) ->
|
|
% We have reduced to a variant. Report the type name as the normalized
|
|
% type, but also provide the variant definition itself as the candidate
|
|
% flattened type for further annotation.
|
|
{ok, IsFirst, PrevType, NextType};
|
|
normalize_opaque_type2(IsFirst, PrevType, NextType = {record, _}, _) ->
|
|
% We have reduced to a record. Report the type name as the normalized
|
|
% type, but also provide the record definition itself as the candidate
|
|
% flattened type for further annotation.
|
|
{ok, IsFirst, PrevType, NextType};
|
|
normalize_opaque_type2(_, _, NextType, Types) ->
|
|
% Not a variant or record yet, so go back to the start of the loop.
|
|
% It will no longer be the first iteration.
|
|
normalize_opaque_type(NextType, Types, false).
|
|
|
|
% Perform a beta-reduction on a type expression.
|
|
substitute_opaque_type([], Definition, _) ->
|
|
% There are no parameters to substitute. This is the simplest way of
|
|
% defining type aliases, records, and variants, so we should make sure to
|
|
% short circuit all the recursive descent logic, since it won't actually
|
|
% do anything.
|
|
Definition;
|
|
substitute_opaque_type(TypeParamNames, Definition, TypeArgs) ->
|
|
% Bundle the param names alongside the args that we want to substitute, so
|
|
% that we can keyfind the one list.
|
|
Bindings = lists:zip(TypeParamNames, TypeArgs),
|
|
substitute_opaque_type(Bindings, Definition).
|
|
|
|
substitute_opaque_type(Bindings, {var, VarName}) ->
|
|
case lists:keyfind(VarName, 1, Bindings) of
|
|
{_, TypeArg} -> TypeArg;
|
|
% No valid ACI will create this case. Regardless, the user should
|
|
% still be able to specify arbitrary gmb FATE terms for whatever this
|
|
% is meant to be.
|
|
false -> unknown_type
|
|
end;
|
|
substitute_opaque_type(Bindings, {variant, Variants}) ->
|
|
Each = fun({VariantName, Elements}) ->
|
|
NewElements = substitute_opaque_types(Bindings, Elements),
|
|
{VariantName, NewElements}
|
|
end,
|
|
NewVariants = lists:map(Each, Variants),
|
|
{variant, NewVariants};
|
|
substitute_opaque_type(Bindings, {record, Fields}) ->
|
|
Each = fun({FieldName, FieldType}) ->
|
|
NewType = substitute_opaque_type(Bindings, FieldType),
|
|
{FieldName, NewType}
|
|
end,
|
|
NewFields = lists:map(Each, Fields),
|
|
{record, NewFields};
|
|
substitute_opaque_type(Bindings, {Connective, Args}) ->
|
|
NewArgs = substitute_opaque_types(Bindings, Args),
|
|
{Connective, NewArgs};
|
|
substitute_opaque_type(_Bindings, Type) ->
|
|
Type.
|
|
|
|
substitute_opaque_types(Bindings, Types) ->
|
|
Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end,
|
|
lists:map(Each, Types).
|
|
|
|
coerce_bindings(VarTypes, Terms, Direction) ->
|
|
DefLength = length(VarTypes),
|
|
ArgLength = length(Terms),
|
|
if
|
|
DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), Direction, arg);
|
|
DefLength > ArgLength -> {error, too_few_args};
|
|
DefLength < ArgLength -> {error, too_many_args}
|
|
end.
|
|
|
|
coerce_zipped_bindings(Bindings, Direction, Tag) ->
|
|
coerce_zipped_bindings(Bindings, Direction, Tag, [], []).
|
|
|
|
coerce_zipped_bindings([Next | Rest], Direction, Tag, Good, Broken) ->
|
|
{{ArgName, Type}, Term} = Next,
|
|
case coerce(Type, Term, Direction) of
|
|
{ok, NewTerm} ->
|
|
coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken);
|
|
{error, Errors} ->
|
|
Wrapped = wrap_errors({Tag, ArgName}, Errors),
|
|
coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken])
|
|
end;
|
|
coerce_zipped_bindings([], _, _, Good, []) ->
|
|
{ok, lists:reverse(Good)};
|
|
coerce_zipped_bindings([], _, _, _, Broken) ->
|
|
{error, combine_errors(Broken)}.
|
|
|
|
wrap_errors(Location, Errors) ->
|
|
F = fun({Error, Path}) ->
|
|
{Error, [Location | Path]}
|
|
end,
|
|
lists:map(F, Errors).
|
|
|
|
combine_errors(Broken) ->
|
|
F = fun(NextErrors, Acc) ->
|
|
NextErrors ++ Acc
|
|
end,
|
|
lists:foldl(F, [], Broken).
|
|
|
|
coerce({_, _, integer}, S, _) when is_integer(S) ->
|
|
{ok, S};
|
|
coerce({O, N, integer}, S, to_fate) when is_list(S) ->
|
|
try
|
|
Val = list_to_integer(S),
|
|
{ok, Val}
|
|
catch
|
|
error:badarg -> single_error({invalid, O, N, S})
|
|
end;
|
|
coerce({O, N, address}, S, to_fate) ->
|
|
coerce_chain_object(O, N, address, account_pubkey, S);
|
|
coerce({_, _, address}, {address, Bin}, from_fate) ->
|
|
Address = gmser_api_encoder:encode(account_pubkey, Bin),
|
|
{ok, unicode:characters_to_list(Address)};
|
|
coerce({O, N, contract}, S, to_fate) ->
|
|
coerce_chain_object(O, N, contract, contract_pubkey, S);
|
|
coerce({_, _, contract}, {contract, Bin}, from_fate) ->
|
|
Address = gmser_api_encoder:encode(contract_pubkey, Bin),
|
|
{ok, unicode:characters_to_list(Address)};
|
|
coerce({_, _, signature}, S, to_fate) when is_binary(S) andalso (byte_size(S) =:= 64) ->
|
|
% Usually to pass a binary in, you need to wrap it as {raw, Binary}, but
|
|
% since sg_... strings OR hex blobs can be used as signatures in Sophia, we
|
|
% special case this case based on the length. Even if a binary starts with
|
|
% "sg_", 64 characters is not enough to represent a 64 byte signature, so
|
|
% the most optimistic interpretation is to use the binary directly.
|
|
{ok, S};
|
|
coerce({O, N, signature}, S, to_fate) ->
|
|
coerce_chain_object(O, N, signature, signature, S);
|
|
coerce({_, _, signature}, Bin, from_fate) ->
|
|
Address = gmser_api_encoder:encode(signature, Bin),
|
|
{ok, unicode:characters_to_list(Address)};
|
|
%coerce({_, _, channel}, S, to_fate) when is_binary(S) ->
|
|
%{ok, {channel, S}};
|
|
%coerce({_, _, channel}, {channel, S}, from_fate) when is_binary(S) ->
|
|
%{ok, S};
|
|
coerce({_, _, boolean}, true, _) ->
|
|
{ok, true};
|
|
coerce({_, _, boolean}, "true", _) ->
|
|
{ok, true};
|
|
coerce({_, _, boolean}, false, _) ->
|
|
{ok, false};
|
|
coerce({_, _, boolean}, "false", _) ->
|
|
{ok, false};
|
|
coerce({O, N, boolean}, S, _) ->
|
|
single_error({invalid, O, N, S});
|
|
coerce({O, N, string}, Str, Direction) ->
|
|
Result = case Direction of
|
|
to_fate -> unicode:characters_to_binary(Str);
|
|
from_fate -> unicode:characters_to_list(Str)
|
|
end,
|
|
case Result of
|
|
{error, _, _} ->
|
|
single_error({invalid, O, N, Str});
|
|
{incomplete, _, _} ->
|
|
single_error({invalid, O, N, Str});
|
|
StrBin ->
|
|
{ok, StrBin}
|
|
end;
|
|
coerce({_, _, char}, Val, _Direction) when is_integer(Val) ->
|
|
{ok, Val};
|
|
coerce({O, N, char}, Str, to_fate) ->
|
|
Result = unicode:characters_to_list(Str),
|
|
case Result of
|
|
{error, _, _} ->
|
|
single_error({invalid, O, N, Str});
|
|
{incomplete, _, _} ->
|
|
single_error({invalid, O, N, Str});
|
|
[C] ->
|
|
{ok, C};
|
|
_ ->
|
|
single_error({invalid, O, N, Str})
|
|
end;
|
|
coerce({O, N, {bytes, [Count]}}, Bytes, _Direction) when is_bitstring(Bytes) ->
|
|
coerce_bytes(O, N, Count, Bytes);
|
|
coerce({_, _, bits}, {bits, Num}, from_fate) ->
|
|
{ok, Num};
|
|
coerce({_, _, bits}, Num, to_fate) when is_integer(Num) ->
|
|
{ok, {bits, Num}};
|
|
coerce({_, _, bits}, Bits, to_fate) when is_bitstring(Bits) ->
|
|
Size = bit_size(Bits),
|
|
<<IntValue:Size>> = Bits,
|
|
{ok, {bits, IntValue}};
|
|
coerce({_, _, {list, [Type]}}, Data, Direction) when is_list(Data) ->
|
|
coerce_list(Type, Data, Direction);
|
|
coerce({_, _, {map, [KeyType, ValType]}}, Data, Direction) when is_map(Data) ->
|
|
coerce_map(KeyType, ValType, Data, Direction);
|
|
coerce({O, N, {tuple, ElementTypes}}, Data, to_fate) when is_tuple(Data) ->
|
|
ElementList = tuple_to_list(Data),
|
|
coerce_tuple(O, N, ElementTypes, ElementList, to_fate);
|
|
coerce({O, N, {tuple, ElementTypes}}, {tuple, Data}, from_fate) ->
|
|
ElementList = tuple_to_list(Data),
|
|
coerce_tuple(O, N, ElementTypes, ElementList, from_fate);
|
|
coerce({O, N, {variant, Variants}}, Data, to_fate) when is_tuple(Data), tuple_size(Data) > 0 ->
|
|
[Name | Terms] = tuple_to_list(Data),
|
|
case lookup_variant(Name, Variants) of
|
|
{Tag, TermTypes} ->
|
|
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, to_fate);
|
|
not_found ->
|
|
ValidNames = [Valid || {Valid, _} <- Variants],
|
|
single_error({invalid_variant, O, N, Name, ValidNames})
|
|
end;
|
|
coerce({O, N, {variant, Variants}}, Name, to_fate) when is_list(Name) ->
|
|
coerce({O, N, {variant, Variants}}, {Name}, to_fate);
|
|
coerce({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}, from_fate) ->
|
|
Terms = tuple_to_list(Tuple),
|
|
{Name, TermTypes} = lists:nth(Tag + 1, Variants),
|
|
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate);
|
|
coerce({O, N, {record, MemberTypes}}, Map, to_fate) when is_map(Map) ->
|
|
coerce_map_to_record(O, N, MemberTypes, Map);
|
|
coerce({O, N, {record, MemberTypes}}, {tuple, Tuple}, from_fate) ->
|
|
coerce_record_to_map(O, N, MemberTypes, Tuple);
|
|
coerce({O, N, {unknown_type, _}}, Data, _) ->
|
|
case N of
|
|
already_normalized ->
|
|
Message = "Warning: Unknown type ~p. Using term ~p as is.~n",
|
|
io:format(Message, [O, Data]);
|
|
_ ->
|
|
Message = "Warning: Unknown type ~p (i.e. ~p). Using term ~p as is.~n",
|
|
io:format(Message, [O, N, Data])
|
|
end,
|
|
{ok, Data};
|
|
coerce({O, N, _}, Data, from_fate) ->
|
|
case N of
|
|
already_normalized ->
|
|
io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]);
|
|
_ ->
|
|
io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data])
|
|
end,
|
|
{ok, Data};
|
|
coerce({O, N, _}, Data, _) -> single_error({invalid, O, N, Data}).
|
|
|
|
coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 ->
|
|
single_error({partial_bytes, O, N, bit_size(Bytes)});
|
|
coerce_bytes(_, _, any, Bytes) ->
|
|
{ok, Bytes};
|
|
coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count ->
|
|
single_error({incorrect_size, O, N, Bytes});
|
|
coerce_bytes(_, _, _, Bytes) ->
|
|
{ok, Bytes}.
|
|
|
|
coerce_chain_object(_, _, _, _, {raw, Binary}) ->
|
|
{ok, Binary};
|
|
coerce_chain_object(O, N, T, Tag, S) ->
|
|
case decode_chain_object(Tag, S) of
|
|
{ok, Data} -> {ok, coerce_chain_object2(T, Data)};
|
|
{error, Reason} -> single_error({Reason, O, N, S})
|
|
end.
|
|
|
|
coerce_chain_object2(address, Data) -> {address, Data};
|
|
coerce_chain_object2(contract, Data) -> {contract, Data};
|
|
coerce_chain_object2(signature, Data) -> Data.
|
|
|
|
decode_chain_object(Tag, S) ->
|
|
try
|
|
case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of
|
|
{Tag, Data} -> {ok, Data};
|
|
{_, _} -> {error, wrong_prefix}
|
|
end
|
|
catch
|
|
error:missing_prefix -> {error, missing_prefix};
|
|
error:incorrect_size -> {error, incorrect_size}
|
|
end.
|
|
|
|
coerce_list(Type, Elements, Direction) ->
|
|
% 0 index since it represents a sophia list
|
|
coerce_list(Type, Elements, Direction, 0, [], []).
|
|
|
|
coerce_list(Type, [Next | Rest], Direction, Index, Good, Broken) ->
|
|
case coerce(Type, Next, Direction) of
|
|
{ok, Coerced} -> coerce_list(Type, Rest, Direction, Index + 1, [Coerced | Good], Broken);
|
|
{error, Errors} ->
|
|
Wrapped = wrap_errors({index, Index}, Errors),
|
|
coerce_list(Type, Rest, Direction, Index + 1, Good, [Wrapped | Broken])
|
|
end;
|
|
coerce_list(_Type, [], _, _, Good, []) ->
|
|
{ok, lists:reverse(Good)};
|
|
coerce_list(_, [], _, _, _, Broken) ->
|
|
{error, combine_errors(Broken)}.
|
|
|
|
coerce_map(KeyType, ValType, Data, Direction) ->
|
|
coerce_map(KeyType, ValType, maps:iterator(Data), Direction, #{}, []).
|
|
|
|
coerce_map(KeyType, ValType, Remaining, Direction, Good, Broken) ->
|
|
case maps:next(Remaining) of
|
|
{K, V, RemainingAfter} ->
|
|
coerce_map2(KeyType, ValType, RemainingAfter, Direction, Good, Broken, K, V);
|
|
none ->
|
|
coerce_map_finish(Good, Broken)
|
|
end.
|
|
|
|
coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) ->
|
|
case coerce(KeyType, K, Direction) of
|
|
{ok, KFATE} ->
|
|
coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE);
|
|
{error, Errors} ->
|
|
Wrapped = wrap_errors(map_key, Errors),
|
|
% Continue as if the key coerced successfully, so that we can give
|
|
% errors for both the key and the value.
|
|
coerce_map3(KeyType, ValType, Remaining, Direction, Good, [Wrapped | Broken], K, V, error)
|
|
end.
|
|
|
|
coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE) ->
|
|
case coerce(ValType, V, Direction) of
|
|
{ok, VFATE} ->
|
|
NewGood = Good#{KFATE => VFATE},
|
|
coerce_map(KeyType, ValType, Remaining, Direction, NewGood, Broken);
|
|
{error, Errors} ->
|
|
Wrapped = wrap_errors({map_value, K}, Errors),
|
|
coerce_map(KeyType, ValType, Remaining, Direction, Good, [Wrapped | Broken])
|
|
end.
|
|
|
|
coerce_map_finish(Good, []) ->
|
|
{ok, Good};
|
|
coerce_map_finish(_, Broken) ->
|
|
{error, combine_errors(Broken)}.
|
|
|
|
lookup_variant(Name, Variants) -> lookup_variant(Name, Variants, 0).
|
|
|
|
lookup_variant(Name, [{Name, Terms} | _], Tag) ->
|
|
{Tag, Terms};
|
|
lookup_variant(Name, [_ | Rest], Tag) ->
|
|
lookup_variant(Name, Rest, Tag + 1);
|
|
lookup_variant(_Name, [], _Tag) ->
|
|
not_found.
|
|
|
|
coerce_tuple(O, N, TermTypes, Terms, Direction) ->
|
|
case coerce_tuple_elements(TermTypes, Terms, Direction, tuple_element) of
|
|
{ok, Converted} ->
|
|
case Direction of
|
|
to_fate -> {ok, {tuple, list_to_tuple(Converted)}};
|
|
from_fate -> {ok, list_to_tuple(Converted)}
|
|
end;
|
|
{error, too_few_terms} ->
|
|
single_error({tuple_too_few_terms, O, N, list_to_tuple(Terms)});
|
|
{error, too_many_terms} ->
|
|
single_error({tuple_too_many_terms, O, N, list_to_tuple(Terms)});
|
|
Errors -> Errors
|
|
end.
|
|
|
|
% Wraps a single error in a list, along with an empty path, so that other
|
|
% accumulating error handlers can work with it.
|
|
single_error(Reason) ->
|
|
{error, [{Reason, []}]}.
|
|
|
|
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) ->
|
|
% FIXME: we could go through and add the variant tag to the adt_element
|
|
% paths?
|
|
case coerce_tuple_elements(TermTypes, Terms, Direction, adt_element) of
|
|
{ok, Converted} ->
|
|
case Direction of
|
|
to_fate ->
|
|
Arities = [length(VariantTerms)
|
|
|| {_, VariantTerms} <- Variants],
|
|
{ok, {variant, Arities, Tag, list_to_tuple(Converted)}};
|
|
from_fate ->
|
|
{ok, list_to_tuple([Name | Converted])}
|
|
end;
|
|
{error, too_few_terms} ->
|
|
single_error({adt_too_few_terms, O, N, Name, TermTypes, Terms});
|
|
{error, too_many_terms} ->
|
|
single_error({adt_too_many_terms, O, N, Name, TermTypes, Terms});
|
|
Errors -> Errors
|
|
end.
|
|
|
|
coerce_tuple_elements(Types, Terms, Direction, Tag) ->
|
|
% The sophia standard library uses 0 indexing for lists, and fst/snd/thd
|
|
% for tuples... Not sure how we should report errors in tuples, then.
|
|
coerce_tuple_elements(Types, Terms, Direction, Tag, 0, [], []).
|
|
|
|
coerce_tuple_elements([Type | Types], [Term | Terms], Direction, Tag, Index, Good, Broken) ->
|
|
case coerce(Type, Term, Direction) of
|
|
{ok, Value} ->
|
|
coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, [Value | Good], Broken);
|
|
{error, Errors} ->
|
|
Wrapped = wrap_errors({Tag, Index}, Errors),
|
|
coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, Good, [Wrapped | Broken])
|
|
end;
|
|
coerce_tuple_elements([], [], _, _, _, Good, []) ->
|
|
{ok, lists:reverse(Good)};
|
|
coerce_tuple_elements([], [], _, _, _, _, Broken) ->
|
|
{error, combine_errors(Broken)};
|
|
coerce_tuple_elements(_, [], _, _, _, _, _) ->
|
|
{error, too_few_terms};
|
|
coerce_tuple_elements([], _, _, _, _, _, _) ->
|
|
{error, too_many_terms}.
|
|
|
|
coerce_map_to_record(O, N, MemberTypes, Map) ->
|
|
case zip_record_fields(MemberTypes, Map) of
|
|
{ok, Zipped} ->
|
|
case coerce_zipped_bindings(Zipped, to_fate, field) of
|
|
{ok, Converted} ->
|
|
{ok, {tuple, list_to_tuple(Converted)}};
|
|
Errors ->
|
|
Errors
|
|
end;
|
|
{error, {missing_fields, Missing}} ->
|
|
single_error({missing_fields, O, N, Missing});
|
|
{error, {unexpected_fields, Unexpected}} ->
|
|
Names = [Name || {Name, _} <- maps:to_list(Unexpected)],
|
|
single_error({unexpected_fields, O, N, Names})
|
|
end.
|
|
|
|
coerce_record_to_map(O, N, MemberTypes, Tuple) ->
|
|
Names = [Name || {Name, _} <- MemberTypes],
|
|
Types = [Type || {_, Type} <- MemberTypes],
|
|
Terms = tuple_to_list(Tuple),
|
|
% FIXME: We could go through and change the record_element paths into field
|
|
% paths?
|
|
case coerce_tuple_elements(Types, Terms, from_fate, record_element) of
|
|
{ok, Converted} ->
|
|
Map = maps:from_list(lists:zip(Names, Converted)),
|
|
{ok, Map};
|
|
{error, too_few_terms} ->
|
|
single_error({record_too_few_terms, O, N, Tuple});
|
|
{error, too_many_terms} ->
|
|
single_error({record_too_many_terms, O, N, Tuple});
|
|
Errors ->
|
|
Errors
|
|
end.
|
|
|
|
zip_record_fields(Fields, Map) ->
|
|
case lists:mapfoldl(fun zip_record_field/2, {Map, []}, Fields) of
|
|
{_, {_, Missing = [_|_]}} ->
|
|
{error, {missing_fields, lists:reverse(Missing)}};
|
|
{_, {Remaining, _}} when map_size(Remaining) > 0 ->
|
|
{error, {unexpected_fields, Remaining}};
|
|
{Zipped, _} ->
|
|
{ok, Zipped}
|
|
end.
|
|
|
|
zip_record_field({Name, Type}, {Remaining, Missing}) ->
|
|
case maps:take(Name, Remaining) of
|
|
{Term, RemainingAfter} ->
|
|
ZippedTerm = {{Name, Type}, Term},
|
|
{ZippedTerm, {RemainingAfter, Missing}};
|
|
error ->
|
|
{missing, {Remaining, [Name | Missing]}}
|
|
end.
|
|
|
|
-spec aaci_lookup_spec(AACI, Fun) -> {ok, Type} | {error, Reason}
|
|
when AACI :: aaci(),
|
|
Fun :: binary() | string(),
|
|
Type :: {term(), term()}, % FIXME
|
|
Reason :: bad_fun_name.
|
|
|
|
%% @doc
|
|
%% Look up the type information of a given function, in the AACI provided by
|
|
%% prepare_contract/1. This type information, particularly the return type, is
|
|
%% useful for calling decode_bytearray/2.
|
|
|
|
aaci_lookup_spec({aaci, _, FunDefs, _}, Fun) ->
|
|
case maps:find(Fun, FunDefs) of
|
|
A = {ok, _} -> A;
|
|
error -> {error, bad_fun_name}
|
|
end.
|
|
|
|
-spec min_gas_price() -> integer().
|
|
%% @doc
|
|
%% This function always returns 1,000,000,000 in the current version.
|
|
%%
|
|
%% This is the minimum gas price returned by aec_tx_pool:minimum_miner_gas_price()
|
|
%%
|
|
%% Surely there can be some more nuance to this, but until a "gas station" type
|
|
%% market/chain survey service exists we will use this naive value as a default
|
|
%% and users can call contract_call/10 if they want more fine-tuned control over the
|
|
%% price. This won't really matter much until the chain has a high enough TPS that
|
|
%% contention becomes an issue.
|
|
|
|
min_gas_price() ->
|
|
1_000_000_000.
|
|
|
|
|
|
-spec min_gas() -> integer().
|
|
%% @doc
|
|
%% This function always returns 200,000 in the current version.
|
|
|
|
min_gas() ->
|
|
200000.
|
|
|
|
|
|
encode_call_data({aaci, _ContractName, FunDefs, _TypeDefs}, Fun, Args) ->
|
|
case maps:find(Fun, FunDefs) of
|
|
{ok, {ArgDef, _ResultDef}} -> encode_call_data2(ArgDef, Fun, Args);
|
|
error -> {error, bad_fun_name}
|
|
end.
|
|
|
|
encode_call_data2(ArgDef, Fun, Args) ->
|
|
case coerce_bindings(ArgDef, Args, to_fate) of
|
|
{ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced);
|
|
Errors -> Errors
|
|
end.
|
|
|
|
|
|
sign_tx(Unsigned, SecKey) ->
|
|
case network_id() of
|
|
{ok, NetworkID} -> sign_tx(Unsigned, SecKey, NetworkID);
|
|
Error -> Error
|
|
end.
|
|
|
|
sign_tx(Unsigned, SecKey, MNetworkID) ->
|
|
UnsignedBin = unicode:characters_to_binary(Unsigned),
|
|
NetworkID = unicode:characters_to_binary(MNetworkID),
|
|
{ok, TX_Data} = gmser_api_encoder:safe_decode(transaction, UnsignedBin),
|
|
{ok, Hash} = eblake2:blake2b(32, TX_Data),
|
|
NetworkHash = <<NetworkID/binary, Hash/binary>>,
|
|
Signature = ecu_eddsa:sign_detached(NetworkHash, SecKey),
|
|
SigTxType = signed_tx,
|
|
SigTxVsn = 1,
|
|
SigTemplate =
|
|
[{signatures, [binary]},
|
|
{transaction, binary}],
|
|
TX =
|
|
[{signatures, [Signature]},
|
|
{transaction, TX_Data}],
|
|
SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX),
|
|
gmser_api_encoder:encode(transaction, SignedTX).
|
|
|
|
|
|
spend(SenderID, SecKey, ReceipientID, Amount, Payload) ->
|
|
case status() of
|
|
{ok, #{"top_block_height" := Height, "network_id" := NetworkID}} ->
|
|
spend(SenderID, SecKey, ReceipientID, Amount, Payload, Height, NetworkID);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
spend(SenderID, SecKey, RecipientID, Amount, Payload, Height, NetworkID) ->
|
|
case next_nonce(SenderID) of
|
|
{ok, Nonce} ->
|
|
{ok, Height} = top_height(),
|
|
TTL = Height + 262980,
|
|
Gas = 20000,
|
|
GasPrice = min_gas_price(),
|
|
spend(SenderID,
|
|
SecKey,
|
|
RecipientID,
|
|
Amount,
|
|
GasPrice,
|
|
Gas,
|
|
TTL,
|
|
Nonce,
|
|
Payload,
|
|
NetworkID);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
spend(SenderID,
|
|
SecKey,
|
|
RecipientID,
|
|
Amount,
|
|
GasPrice,
|
|
Gas,
|
|
TTL,
|
|
Nonce,
|
|
Payload,
|
|
NetworkID) ->
|
|
case decode_account_id(unicode:characters_to_binary(SenderID)) of
|
|
{ok, DSenderID} ->
|
|
spend2(gmser_id:create(account, DSenderID),
|
|
SecKey,
|
|
RecipientID,
|
|
Amount,
|
|
GasPrice,
|
|
Gas,
|
|
TTL,
|
|
Nonce,
|
|
Payload,
|
|
NetworkID);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
spend2(DSenderID,
|
|
SecKey,
|
|
RecipientID,
|
|
Amount,
|
|
GasPrice,
|
|
Gas,
|
|
TTL,
|
|
Nonce,
|
|
Payload,
|
|
NetworkID) ->
|
|
case decode_account_id(unicode:characters_to_binary(RecipientID)) of
|
|
{ok, DRecipientID} ->
|
|
spend3(DSenderID,
|
|
SecKey,
|
|
gmser_id:create(account, DRecipientID),
|
|
Amount,
|
|
GasPrice,
|
|
Gas,
|
|
TTL,
|
|
Nonce,
|
|
Payload,
|
|
NetworkID);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
decode_account_id(B) ->
|
|
try
|
|
{account_pubkey, PK} = gmser_api_encoder:decode(B),
|
|
{ok, PK}
|
|
catch
|
|
E:R -> {E, R}
|
|
end.
|
|
|
|
|
|
spend3(DSenderID,
|
|
SecKey,
|
|
DRecipientID,
|
|
Amount,
|
|
GasPrice,
|
|
Gas,
|
|
TTL,
|
|
Nonce,
|
|
Payload,
|
|
MNetworkID) ->
|
|
NetworkID = unicode:characters_to_binary(MNetworkID),
|
|
Type = spend_tx,
|
|
Vsn = 1,
|
|
Fields =
|
|
[{sender_id, DSenderID},
|
|
{recipient_id, DRecipientID},
|
|
{amount, Amount},
|
|
{gas_price, GasPrice},
|
|
{gas, Gas},
|
|
{ttl, TTL},
|
|
{nonce, Nonce},
|
|
{payload, Payload}],
|
|
Template =
|
|
[{sender_id, id},
|
|
{recipient_id, id},
|
|
{amount, int},
|
|
{gas_price, int},
|
|
{gas, int},
|
|
{ttl, int},
|
|
{nonce, int},
|
|
{payload, binary}],
|
|
BinaryTX = gmser_chain_objects:serialize(Type, Vsn, Template, Fields),
|
|
NetworkTX = <<NetworkID/binary, BinaryTX/binary>>,
|
|
Signature = ecu_eddsa:sign_detached(NetworkTX, SecKey),
|
|
SigTxType = signed_tx,
|
|
SigTxVsn = 1,
|
|
SigTemplate =
|
|
[{signatures, [binary]},
|
|
{transaction, binary}],
|
|
TX_Data =
|
|
[{signatures, [Signature]},
|
|
{transaction, BinaryTX}],
|
|
SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data),
|
|
Encoded = gmser_api_encoder:encode(transaction, SignedTX),
|
|
hz:post_tx(Encoded).
|
|
|
|
|
|
-spec sign_message(Message, SecKey) -> Sig
|
|
when Message :: binary(),
|
|
SecKey :: binary(),
|
|
Sig :: binary().
|
|
|
|
sign_message(Message, SecKey) ->
|
|
Prefix = message_sig_prefix(),
|
|
{ok, PSize} = vencode(byte_size(Prefix)),
|
|
{ok, MSize} = vencode(byte_size(Message)),
|
|
Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
|
|
{ok, Hashed} = eblake2:blake2b(32, Smashed),
|
|
ecu_eddsa:sign_detached(Hashed, SecKey).
|
|
|
|
|
|
-spec verify_signature(Sig, Message, PubKey) -> Result
|
|
when Sig :: string(), % base64 encoded signature,
|
|
Message :: iodata(),
|
|
PubKey :: pubkey(),
|
|
Result :: {ok, Outcome :: boolean()}
|
|
| {error, Reason :: term()}.
|
|
%% @doc
|
|
%% Verify a message signature given the signature, the message that was signed, and the
|
|
%% public half of the key that was used to sign.
|
|
%%
|
|
%% The result of a complete signature check is a boolean value return in an `{ok, Outcome}'
|
|
%% tuple, and any `{error, Reason}' return value is an indication that something about the
|
|
%% check failed before verification was able to pass or fail (bad key encoding or similar).
|
|
|
|
verify_signature(Sig, Message, PubKey) ->
|
|
case gmser_api_encoder:decode(PubKey) of
|
|
{account_pubkey, PK} -> verify_signature2(Sig, Message, PK);
|
|
Other -> {error, {bad_key, Other}}
|
|
end.
|
|
|
|
verify_signature2(Sig, Message, PK) ->
|
|
% Gajumaru signatures require messages to be salted and hashed, then
|
|
% the hash is what gets signed in order to protect
|
|
% the user from accidentally signing a transaction disguised as a message.
|
|
%
|
|
% Salt the message then hash with blake2b.
|
|
Prefix = message_sig_prefix(),
|
|
{ok, PSize} = vencode(byte_size(Prefix)),
|
|
{ok, MSize} = vencode(byte_size(Message)),
|
|
Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
|
|
{ok, Hashed} = eblake2:blake2b(32, Smashed),
|
|
% Signature = <<(binary_to_integer(Sig, 16)):(64 * 8)>>,
|
|
Signature = base64:decode(Sig),
|
|
Result = ecu_eddsa:sign_verify_detached(Signature, Hashed, PK),
|
|
{ok, Result}.
|
|
|
|
message_sig_prefix() -> <<"Gajumaru Signed Message:\n">>.
|
|
|
|
% This is Bitcoin's variable-length unsigned integer encoding
|
|
% See: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
|
|
vencode(N) when N =< 0 ->
|
|
{error, {non_pos_N, N}};
|
|
vencode(N) when N < 16#FD ->
|
|
{ok, <<N>>};
|
|
vencode(N) when N =< 16#FFFF ->
|
|
NBytes = eu(N, 2),
|
|
{ok, <<16#FD, NBytes/binary>>};
|
|
vencode(N) when N =< 16#FFFF_FFFF ->
|
|
NBytes = eu(N, 4),
|
|
{ok, <<16#FE, NBytes/binary>>};
|
|
vencode(N) when N < (2 bsl 64) ->
|
|
NBytes = eu(N, 8),
|
|
{ok, <<16#FF, NBytes/binary>>}.
|
|
|
|
|
|
% eu = encode unsigned (little endian with a given byte width)
|
|
% means add zero bytes to the end as needed
|
|
eu(N, Size) ->
|
|
Bytes = binary:encode_unsigned(N, little),
|
|
NExtraZeros = Size - byte_size(Bytes),
|
|
ExtraZeros = << <<0>> || _ <- lists:seq(1, NExtraZeros) >>,
|
|
<<Bytes/binary, ExtraZeros/binary>>.
|
|
|
|
|
|
-spec sign_binary(Binary, SecKey) -> Sig
|
|
when Binary :: binary(),
|
|
SecKey :: binary(),
|
|
Sig :: binary().
|
|
|
|
sign_binary(Binary, SecKey) ->
|
|
Prefix = binary_sig_prefix(),
|
|
Target = <<Prefix/binary, Binary/binary>>,
|
|
{ok, Hash} = eblake2:blake2b(32, Target),
|
|
ecu_eddsa:sign_detached(Hash, SecKey).
|
|
|
|
|
|
-spec verify_bin_signature(Sig, Binary, PubKey) -> Result
|
|
when Sig :: string(), % base64 encoded signature,
|
|
Binary :: binary(),
|
|
PubKey :: pubkey(),
|
|
Result :: {ok, Outcome :: boolean()}
|
|
| {error, Reason :: term()}.
|
|
|
|
verify_bin_signature(Sig, Binary, PubKey) ->
|
|
case gmser_api_encoder:decode(PubKey) of
|
|
{account_pubkey, PK} -> verify_bin_signature2(Sig, Binary, PK);
|
|
Other -> {error, {bad_key, Other}}
|
|
end.
|
|
|
|
verify_bin_signature2(Sig, Binary, PK) ->
|
|
Prefix = binary_sig_prefix(),
|
|
Target = <<Prefix/binary, Binary/binary>>,
|
|
{ok, Hash} = eblake2:blake2b(32, Target),
|
|
Signature = base64:decode(Sig),
|
|
Result = ecu_eddsa:sign_verify_detached(Signature, Hash, PK),
|
|
{ok, Result}.
|
|
|
|
binary_sig_prefix() -> <<"Gajumaru Signed Binary:">>.
|
|
|
|
|
|
%%% Debug functionality
|
|
|
|
% debug_network() ->
|
|
% request("/v3/debug/network").
|
|
%
|
|
% /v3/debug/contracts/create
|
|
% /v3/debug/contracts/call
|
|
% /v3/debug/oracles/register
|
|
% /v3/debug/oracles/extend
|
|
% /v3/debug/oracles/query
|
|
% /v3/debug/oracles/respond
|
|
% /v3/debug/names/preclaim
|
|
% /v3/debug/names/claim
|
|
% /v3/debug/names/update
|
|
% /v3/debug/names/transfer
|
|
% /v3/debug/names/revoke
|
|
% /v3/debug/transactions/spend
|
|
% /v3/debug/channels/create
|
|
% /v3/debug/channels/deposit
|
|
% /v3/debug/channels/withdraw
|
|
% /v3/debug/channels/snapshot/solo
|
|
% /v3/debug/channels/set-delegates
|
|
% /v3/debug/channels/close/mutual
|
|
% /v3/debug/channels/close/solo
|
|
% /v3/debug/channels/slash
|
|
% /v3/debug/channels/settle
|
|
% /v3/debug/transactions/pending
|
|
% /v3/debug/names/commitment-id
|
|
% /v3/debug/accounts/beneficiary
|
|
% /v3/debug/accounts/node
|
|
% /v3/debug/peers
|
|
% /v3/debug/transactions/dry-run
|
|
% /v3/debug/transactions/paying-for
|
|
% /v3/debug/check-tx/pool/{hash}
|
|
% /v3/debug/token-supply/height/{height}
|
|
% /v3/debug/crash
|
|
|
|
|
|
%%% Simple coerce/3 tests
|
|
|
|
% Round trip coerce run for the eunit tests below. If these results don't match
|
|
% then the test should fail.
|
|
try_coerce(Type, Sophia, Fate) ->
|
|
% Run both first, to see if they fail to produce any result.
|
|
{ok, FateActual} = coerce(Type, Sophia, to_fate),
|
|
{ok, SophiaActual} = coerce(Type, Fate, from_fate),
|
|
% Now check that the results were what we expected.
|
|
case FateActual of
|
|
Fate ->
|
|
ok;
|
|
_ ->
|
|
erlang:error({to_fate_failed, Fate, FateActual})
|
|
end,
|
|
case SophiaActual of
|
|
Sophia ->
|
|
ok;
|
|
_ ->
|
|
erlang:error({from_fate_failed, Sophia, SophiaActual})
|
|
end,
|
|
% Finally, check that the FATE result is something that gmb understands.
|
|
gmb_fate_encoding:serialize(Fate),
|
|
ok.
|
|
|
|
coerce_int_test() ->
|
|
{ok, Type} = annotate_type(integer, #{}),
|
|
try_coerce(Type, 123, 123).
|
|
|
|
coerce_address_test() ->
|
|
{ok, Type} = annotate_type(address, #{}),
|
|
try_coerce(Type,
|
|
"ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
|
|
{address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
|
|
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
|
|
210,39,214>>}).
|
|
|
|
coerce_contract_test() ->
|
|
{ok, Type} = annotate_type(contract, #{}),
|
|
try_coerce(Type,
|
|
"ct_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
|
|
{contract, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
|
|
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
|
|
210,39,214>>}).
|
|
|
|
coerce_signature_test() ->
|
|
{ok, Type} = annotate_type(signature, #{}),
|
|
try_coerce(Type,
|
|
"sg_XDyF8LJC4tpMyAySvpaG1f5V9F2XxAbRx9iuVjvvdNMwVracLhzAuXhRM5kXAFtpwW1DCHuz5jGehUayCah4jub32Ti2n",
|
|
<<231,4,97,129,16,173,37,42,194,249,28,94,134,163,208,84,22,135,
|
|
169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222,
|
|
249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114,
|
|
233,110,119,143,206,137,124,36,63,154,85,7>>).
|
|
|
|
coerce_signature_binary_test() ->
|
|
{ok, Type} = annotate_type(signature, #{}),
|
|
Binary = <<231,4,97,129,16,173,37,42,194,249,28,94,134,163,208,84,22,135,
|
|
169,85,212,142,14,12,233,252,97,50,193,158,229,51,123,206,222,
|
|
249,2,3,85,173,106,150,243,253,89,128,248,52,195,140,95,114,
|
|
233,110,119,143,206,137,124,36,63,154,85,7>>,
|
|
{ok, Binary} = coerce(Type, {raw, Binary}, to_fate),
|
|
{ok, Binary} = coerce(Type, Binary, to_fate),
|
|
ok.
|
|
|
|
coerce_bool_test() ->
|
|
{ok, Type} = annotate_type(boolean, #{}),
|
|
try_coerce(Type, true, true),
|
|
try_coerce(Type, false, false).
|
|
|
|
coerce_string_test() ->
|
|
{ok, Type} = annotate_type(string, #{}),
|
|
try_coerce(Type, "hello world", <<"hello world">>).
|
|
|
|
coerce_list_test() ->
|
|
{ok, Type} = annotate_type({list, [string]}, #{}),
|
|
try_coerce(Type, ["hello world", [65, 32, 65]], [<<"hello world">>, <<65, 32, 65>>]).
|
|
|
|
coerce_map_test() ->
|
|
{ok, Type} = annotate_type({map, [string, {list, [integer]}]}, #{}),
|
|
try_coerce(Type, #{"a" => "a", "b" => "b"}, #{<<"a">> => "a", <<"b">> => "b"}).
|
|
|
|
coerce_tuple_test() ->
|
|
{ok, Type} = annotate_type({tuple, [integer, string]}, #{}),
|
|
try_coerce(Type, {123, "456"}, {tuple, {123, <<"456">>}}).
|
|
|
|
coerce_variant_test() ->
|
|
{ok, Type} = annotate_type({variant, [{"A", [integer]},
|
|
{"B", [integer, integer]}]},
|
|
#{}),
|
|
try_coerce(Type, {"A", 123}, {variant, [1, 2], 0, {123}}),
|
|
try_coerce(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}).
|
|
|
|
coerce_option_test() ->
|
|
{ok, Type} = annotate_type({"option", [integer]}, builtin_typedefs()),
|
|
try_coerce(Type, {"None"}, {variant, [0, 1], 0, {}}),
|
|
try_coerce(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}).
|
|
|
|
coerce_record_test() ->
|
|
{ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}),
|
|
try_coerce(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
|
|
|
|
coerce_bytes_test() ->
|
|
{ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}),
|
|
try_coerce(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}).
|
|
|
|
coerce_bits_test() ->
|
|
{ok, Type} = annotate_type(bits, #{}),
|
|
try_coerce(Type, 5, {bits, 5}).
|
|
|
|
coerce_char_test() ->
|
|
{ok, Type} = annotate_type(char, #{}),
|
|
try_coerce(Type, $?, $?).
|
|
|
|
coerce_unicode_test() ->
|
|
{ok, Type} = annotate_type(char, #{}),
|
|
% Latin Small Letter C with cedilla and acute
|
|
{ok, $ḉ} = coerce(Type, <<"ḉ"/utf8>>, to_fate),
|
|
ok.
|
|
|
|
coerce_hash_test() ->
|
|
{ok, Type} = annotate_type("hash", builtin_typedefs()),
|
|
Hash = list_to_binary(lists:seq(1,32)),
|
|
try_coerce(Type, Hash, Hash),
|
|
ok.
|
|
|
|
|
|
|
|
%%% Complex AACI paramter and namespace tests
|
|
|
|
aaci_from_string(String) ->
|
|
case so_compiler:from_string(String, [{aci, json}]) of
|
|
{ok, #{aci := ACI}} -> {ok, prepare_aaci(ACI)};
|
|
Error -> Error
|
|
end.
|
|
|
|
namespace_coerce_test() ->
|
|
Contract = "
|
|
namespace N =
|
|
record pair = { a : int, b : int }
|
|
|
|
contract C =
|
|
entrypoint f(): N.pair = { a = 1, b = 2 }
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = aaci_lookup_spec(AACI, "f"),
|
|
try_coerce(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
|
|
|
|
record_substitution_test() ->
|
|
Contract = "
|
|
contract C =
|
|
record pair('t) = { a : 't, b : 't }
|
|
entrypoint f(): pair(int) = { a = 1, b = 2 }
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = aaci_lookup_spec(AACI, "f"),
|
|
try_coerce(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
|
|
|
|
tuple_substitution_test() ->
|
|
Contract = "
|
|
contract C =
|
|
type triple('t1, 't2) = int * 't1 * 't2
|
|
entrypoint f(): triple(int, string) = (1, 2, \"hello\")
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = aaci_lookup_spec(AACI, "f"),
|
|
try_coerce(Output, {1, 2, "hello"}, {tuple, {1, 2, <<"hello">>}}).
|
|
|
|
variant_substitution_test() ->
|
|
Contract = "
|
|
contract C =
|
|
datatype adt('a, 'b) = Left('a, 'b) | Right('b, int)
|
|
entrypoint f(): adt(string, int) = Left(\"hi\", 1)
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = aaci_lookup_spec(AACI, "f"),
|
|
try_coerce(Output, {"Left", "hi", 1}, {variant, [2, 2], 0, {<<"hi">>, 1}}),
|
|
try_coerce(Output, {"Right", 2, 3}, {variant, [2, 2], 1, {2, 3}}).
|
|
|
|
nested_coerce_test() ->
|
|
Contract = "
|
|
contract C =
|
|
type pair('t) = 't * 't
|
|
record r = { f1 : pair(int), f2: pair(string) }
|
|
entrypoint f(): r = { f1 = (1, 2), f2 = (\"a\", \"b\") }
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = aaci_lookup_spec(AACI, "f"),
|
|
try_coerce(Output,
|
|
#{ "f1" => {1, 2}, "f2" => {"a", "b"}},
|
|
{tuple, {{tuple, {1, 2}}, {tuple, {<<"a">>, <<"b">>}}}}).
|
|
|
|
state_coerce_test() ->
|
|
Contract = "
|
|
contract C =
|
|
type state = int
|
|
entrypoint init(): state = 0
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[], Output}} = aaci_lookup_spec(AACI, "init"),
|
|
try_coerce(Output, 0, 0).
|
|
|
|
param_test() ->
|
|
Contract = "
|
|
contract C =
|
|
type state = int
|
|
entrypoint init(x): state = x
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
{ok, {[{"x", Input}], Output}} = aaci_lookup_spec(AACI, "init"),
|
|
try_coerce(Input, 0, 0),
|
|
try_coerce(Output, 0, 0).
|
|
|
|
%%% Obscure Sophia types where we should check the AACI as well
|
|
|
|
obscure_aaci_test() ->
|
|
Contract = "
|
|
include \"Set.aes\"
|
|
contract C =
|
|
entrypoint options(): option(int) = None
|
|
entrypoint fixed_bytes(): bytes(4) = #DEADBEEF
|
|
entrypoint any_bytes(): bytes() = Bytes.to_any_size(#112233)
|
|
entrypoint bits(): bits = Bits.all
|
|
entrypoint character(): char = 'a'
|
|
entrypoint hash(): hash = #00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF
|
|
entrypoint unit(): unit = ()
|
|
|
|
entrypoint ttl(x): Chain.ttl = FixedTTL(x)
|
|
entrypoint paying_for(x, y): Chain.paying_for_tx = Chain.PayingForTx(x, y)
|
|
entrypoint ga_meta_tx(x, y): Chain.ga_meta_tx = Chain.GAMetaTx(x, y)
|
|
entrypoint base_tx(x, y, z): Chain.base_tx = Chain.SpendTx(x, y, z)
|
|
entrypoint tx(a, b, c, d, e, f): Chain.tx =
|
|
{paying_for = a,
|
|
ga_metas = b,
|
|
actor = c,
|
|
fee = d,
|
|
ttl = e,
|
|
tx = f}
|
|
|
|
entrypoint pointee(x): AENS.pointee = AENS.AccountPt(x)
|
|
entrypoint name(x, y, z): AENS.name = AENS.Name(x, y, z)
|
|
entrypoint pointee2(x): AENSv2.pointee = AENSv2.DataPt(x)
|
|
entrypoint name2(x, y, z): AENSv2.name = AENSv2.Name(x, y, z)
|
|
|
|
entrypoint fr(x): MCL_BLS12_381.fr = x
|
|
entrypoint fp(x): MCL_BLS12_381.fp = x
|
|
|
|
entrypoint set(): Set.set(int) = Set.new()
|
|
|
|
",
|
|
{ok, AACI} = aaci_from_string(Contract),
|
|
|
|
{ok, {[], {{bytes, [4]}, _, _}}} = aaci_lookup_spec(AACI, "fixed_bytes"),
|
|
{ok, {[], {{bytes, [any]}, _, _}}} = aaci_lookup_spec(AACI, "any_bytes"),
|
|
{ok, {[], {bits, _, _}}} = aaci_lookup_spec(AACI, "bits"),
|
|
{ok, {[], {char, _, _}}} = aaci_lookup_spec(AACI, "character"),
|
|
|
|
{ok, {[], {{"option", [integer]}, _, {variant, [{"None", []}, {"Some", [_]}]}}}} = aaci_lookup_spec(AACI, "options"),
|
|
{ok, {[], {"hash", _, {bytes, [32]}}}} = aaci_lookup_spec(AACI, "hash"),
|
|
{ok, {[], {"unit", _, {tuple, []}}}} = aaci_lookup_spec(AACI, "unit"),
|
|
|
|
{ok, {_, {"Chain.ttl", _, {variant, _}}}} = aaci_lookup_spec(AACI, "ttl"),
|
|
{ok, {_, {"Chain.paying_for_tx", _, {variant, _}}}} = aaci_lookup_spec(AACI, "paying_for"),
|
|
{ok, {_, {"Chain.ga_meta_tx", _, {variant, _}}}} = aaci_lookup_spec(AACI, "ga_meta_tx"),
|
|
{ok, {_, {"Chain.base_tx", _, {variant, _}}}} = aaci_lookup_spec(AACI, "base_tx"),
|
|
{ok, {_, {"Chain.tx", _, {record, _}}}} = aaci_lookup_spec(AACI, "tx"),
|
|
|
|
{ok, {_, {"AENS.pointee", _, {variant, _}}}} = aaci_lookup_spec(AACI, "pointee"),
|
|
{ok, {_, {"AENS.name", _, {variant, _}}}} = aaci_lookup_spec(AACI, "name"),
|
|
{ok, {_, {"AENSv2.pointee", _, {variant, _}}}} = aaci_lookup_spec(AACI, "pointee2"),
|
|
{ok, {_, {"AENSv2.name", _, {variant, _}}}} = aaci_lookup_spec(AACI, "name2"),
|
|
|
|
{ok, {_, {"MCL_BLS12_381.fr", _, {bytes, [32]}}}} = aaci_lookup_spec(AACI, "fr"),
|
|
{ok, {_, {"MCL_BLS12_381.fp", _, {bytes, [48]}}}} = aaci_lookup_spec(AACI, "fp"),
|
|
|
|
{ok, {[], {{"Set.set", [integer]}, _, {record, [{"to_map", _}]}}}} = aaci_lookup_spec(AACI, "set"),
|
|
|
|
ok.
|
|
|
|
name_coerce_test() ->
|
|
AddrSoph = "ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
|
|
AddrFate = {address, <<164,136,155,90,124,22,40,206,255,76,213,56,238,123,
|
|
167,208,53,78,40,235,2,163,132,36,47,183,228,151,9,
|
|
210,39,214>>},
|
|
{ok, TTL} = annotate_type("Chain.ttl", builtin_typedefs()),
|
|
TTLSoph = {"FixedTTL", 0},
|
|
TTLFate = {variant, [1, 1], 0, {0}},
|
|
try_coerce(TTL, TTLSoph, TTLFate),
|
|
{ok, Pointee} = annotate_type("AENS.pointee", builtin_typedefs()),
|
|
PointeeSoph = {"AccountPt", AddrSoph},
|
|
PointeeFate = {variant, [1, 1, 1, 1], 0, {AddrFate}},
|
|
try_coerce(Pointee, PointeeSoph, PointeeFate),
|
|
{ok, Name} = annotate_type("AENS.name", builtin_typedefs()),
|
|
NameSoph = {"Name", AddrSoph, TTLSoph, #{"myname" => PointeeSoph}},
|
|
NameFate = {variant, [3], 0, {AddrFate, TTLFate, #{<<"myname">> => PointeeFate}}},
|
|
try_coerce(Name, NameSoph, NameFate).
|
|
|
|
void_coerce_test() ->
|
|
% Void itself can't be represented, but other types built out of void are
|
|
% valid.
|
|
{ok, NonOption} = annotate_type({"option", ["void"]}, builtin_typedefs()),
|
|
try_coerce(NonOption, {"None"}, {variant, [0, 1], 0, {}}),
|
|
{ok, NonList} = annotate_type({list, ["void"]}, builtin_typedefs()),
|
|
try_coerce(NonList, [], []).
|
|
|