add more atoms to AACI Complete AACI definition serialize signatures This took a surprising number of goose chases to work out... I had to find out - what is the gmser prefix for a signature (sg_) - what is the gmb wrapper for a signature (none) - what errors gmser can report when a signature is invalid - what an example of a valid signature is - what that example signature serializes to coerce stringy booleans coerce bytes coerce bits The thing to remember about bits is that they are actually integers... It is tempting to present bits as binaries, but that hides the nuance of the infinite leading zeroes, the potential for infinite leading ones, etc. coerce character It's really just an integer... Should we flatten it to an integer instead? I don't know. Also coerce unicode strings to FATE This is mainly so that gajudesk can pass text box content to hz as-is, but also allows users to pass utf8 binaries in, if they want to, for some reason. Coerce binaries as-is Sophia accepts both sg_... and #... as signatures, so we should probably accept binaries as signatures directly. People might expect to be able to put the listy string "#..." in too, but that is more complex to do. coerce hashes It turns out there are a lot of types that, like option, should only be valid as an opaque/normalized type, but should be substituted for something different in the flat representation. If we restructure things a little then we can implement all of these in one go. Refactor type normalization Some of these checks were redundant, and we probably don't actually need substitution to wrap success/failure, since it isn't expected to fail anyway... Now the logic is much simpler, and adding more built-in type definitions should be easy. Add a map for builtin types This makes it much easier to implement all these standard library things. In doing so I changed the convention for option, hash, unit, to be stringy rather than atoms. Also I changed some error messages based on what was more helpful during debugging of the unit tests. Add more builtin types We probably should extract these from the standard library instead of cherry picking the ones that are needed by the chain? e.g. Chain.tx still doesn't work. remaining types `tx` isn't defined in all the same places that pointee, name, base_tx, fr, fp are defined, but actually it is the only one not in the list I was looking at, so we are all good. As demonstration, there is also a test case for Set.set, despite Set.set not being defined as a builtin type.
2809 lines
105 KiB
Erlang
2809 lines
105 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.6.1").
|
|
-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]).
|
|
|
|
|
|
%%% 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 :: pos_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} ->
|
|
case zx_lib:b_to_ts(Bin) of
|
|
error -> {error, bad_aci};
|
|
OK -> OK
|
|
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 :: pos_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) ->
|
|
% If it is a binary of 64 bytes then it can be used as is... If it is an
|
|
% sg_... string of 64 bytes, then it is too short to be valid, so just
|
|
% interpret it as a 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(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 = <<"Gajumaru Signed Message:\n">>,
|
|
{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 :: binary(),
|
|
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 = <<"Gajumaru Signed Message:\n">>,
|
|
{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}.
|
|
|
|
|
|
% 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>>.
|
|
|
|
|
|
%%% 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, 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, [], []).
|
|
|