%%% @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 "). -copyright("Craig Everett "). -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()}. %
% #{"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}.
% 
-type microblock_header() :: #{string() => term()}. %
% #{"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}.
% 
-type transaction() :: #{string() => term()}. %
% #{"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()}}
% 
-type generation() :: #{string() => term()}. %
% #{"key_block"     => keyblock(),
%   "micro_blocks"  => [microblock_hash()]}.
% 
-type account() :: #{string() => term()}. %
% #{"balance" => non_neg_integer(),
%   "id"      => account_id(),
%   "kind"    => "basic",
%   "nonce"   => pos_integer(),
%   "payable" => true}.
% 
-type contract_data() :: #{string() => term()}. %
% #{"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()}.
% 
-type name_info() :: #{string() => term()}. %
% #{"id"       => name_hash(),
%   "owner"    => account_id(),
%   "pointers" => [],
%   "ttl"      => non_neg_integer()}.
% 
-type status() :: #{string() => term()}. %
% #{"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()}.
% 
%%% 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: %% %% 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, <> = <>, 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: %%
    %%
  • %% CallerID: %% This is the public 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 private %% key. %%
  • %%
  • %% Nonce: %% 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). %%
  • %%
  • %% Gas: %% 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. %%
  • %%
  • %% GasPrice: %% 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. %%
  • %%
  • %% Amount: %% 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 into 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. %%
  • %%
  • %% ACI: %% 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. %%
  • %%
  • %% ConID: %% 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. %%
  • %%
  • %% Fun: %% 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). %%
  • %%
  • %% Args: %% 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"'). %%
  • %%
%% 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. Specs = annotate_function_specs(OpaqueSpecs, TypeDefs, #{}), {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 builtins, strings (lists) for user-defined types. % % There are some magic built in types that may or may not also need atoms to % represent them, and may or may not need to be handled explicitly in % coerce/3, if we can't flatten them directly % % These types represent some FATE variant: % Chain.ttl, AENS.pointee, AENS.name, AENSv2.pointee, AENSv2.name, % Chain.ga_meta_tx, Chain.paying_for_tx, Chain.base_tx, % % And then MCL_BLS12_381.fr represent bytes(32), and MCL_BLS12_381.fp % represents bytes(48). 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(<<"hash">>) -> hash; opaque_type_name(<<"signature">>) -> signature; opaque_type_name(<<"contract">>) -> contract; opaque_type_name(<<"list">>) -> list; opaque_type_name(<<"map">>) -> map; opaque_type_name(<<"option">>) -> option; opaque_type_name(<<"name">>) -> name; opaque_type_name(<<"channel">>) -> channel; opaque_type_name(Name) -> binary_to_list(Name). % 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, 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)}. normalize_opaque_type(T, Types) -> case type_is_expanded(T) of false -> normalize_opaque_type(T, Types, true); true -> {ok, true, T, T} end. % 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({option, [T]}, _Types, IsFirst) -> % Just like user-made ADTs, 'option' is considered part of the type, and so % options are considered normalised. {ok, IsFirst, {option, [T]}, {variant, [{"None", []}, {"Some", [T]}]}}; normalize_opaque_type(T, Types, IsFirst) when is_list(T) -> normalize_opaque_type({T, []}, Types, IsFirst); normalize_opaque_type({T, TypeArgs}, Types, IsFirst) when is_list(T) -> case maps:find(T, Types) of %{error, invalid_aci}; % FIXME more info error -> {ok, IsFirst, {T, TypeArgs}, {unknown_type, TypeArgs}}; {ok, {TypeParamNames, Definition}} -> Bindings = lists:zip(TypeParamNames, TypeArgs), normalize_opaque_type2(T, TypeArgs, Types, IsFirst, Bindings, Definition) end. normalize_opaque_type2(T, TypeArgs, Types, IsFirst, Bindings, Definition) -> SubResult = case Bindings of [] -> {ok, Definition}; _ -> substitute_opaque_type(Bindings, Definition) end, case SubResult of % Type names were already normalized if they were ADTs or records, % since for those connectives the name is considered part of the type. {ok, NextT = {variant, _}} -> {ok, IsFirst, {T, TypeArgs}, NextT}; {ok, NextT = {record, _}} -> {ok, IsFirst, {T, TypeArgs}, NextT}; % Everything else has to be substituted down to a built-in connective % to be considered normalized. {ok, NextT} -> normalize_opaque_type3(NextT, Types); Error -> Error end. % while this does look like normalize_opaque_type/2, it sets IsFirst to false % instead of true, and is part of the loop, instead of being an initial % condition for the loop. normalize_opaque_type3(NextT, Types) -> case type_is_expanded(NextT) of false -> normalize_opaque_type(NextT, Types, false); true -> {ok, false, NextT, NextT} end. % Strings indicate names that should be substituted. Atoms indicate built in % types, which don't need to be expanded, except for option. type_is_expanded({option, _}) -> false; type_is_expanded(X) when is_atom(X) -> true; type_is_expanded({X, _}) when is_atom(X) -> true; type_is_expanded(_) -> false. % Skip traversal if there is nothing to substitute. This will often be the % most common case. substitute_opaque_type(Bindings, {var, VarName}) -> case lists:keyfind(VarName, 1, Bindings) of false -> {error, invalid_aci}; {_, TypeArg} -> {ok, TypeArg} end; substitute_opaque_type(Bindings, {variant, Args}) -> case substitute_variant_types(Bindings, Args, []) of {ok, Result} -> {ok, {variant, Result}}; Error -> Error end; substitute_opaque_type(Bindings, {record, Args}) -> case substitute_record_types(Bindings, Args, []) of {ok, Result} -> {ok, {record, Result}}; Error -> Error end; substitute_opaque_type(Bindings, {Connective, Args}) -> case substitute_opaque_types(Bindings, Args, []) of {ok, Result} -> {ok, {Connective, Result}}; Error -> Error end; substitute_opaque_type(_Bindings, Type) -> {ok, Type}. substitute_variant_types(Bindings, [{VariantName, Elements} | Rest], Acc) -> case substitute_opaque_types(Bindings, Elements, []) of {ok, Result} -> substitute_variant_types(Bindings, Rest, [{VariantName, Result} | Acc]); Error -> Error end; substitute_variant_types(_Bindings, [], Acc) -> {ok, lists:reverse(Acc)}. substitute_record_types(Bindings, [{ElementName, Type} | Rest], Acc) -> case substitute_opaque_type(Bindings, Type) of {ok, Result} -> substitute_record_types(Bindings, Rest, [{ElementName, Result} | Acc]); Error -> Error end; substitute_record_types(_Bindings, [], Acc) -> {ok, lists:reverse(Acc)}. substitute_opaque_types(Bindings, [Next | Rest], Acc) -> case substitute_opaque_type(Bindings, Next) of {ok, Result} -> substitute_opaque_types(Bindings, Rest, [Result | Acc]); Error -> Error end; substitute_opaque_types(_Bindings, [], Acc) -> {ok, lists:reverse(Acc)}. 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({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({_, _, 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, {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), <> = 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 = <>, 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 = <>, 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, <>}; 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) >>, <>. %%% 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_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_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, $?, $?). %%% 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 = " 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' ", {ok, AACI} = aaci_from_string(Contract), IntAnnotated = {integer, already_normalized, integer}, OptionFlat = {variant, [{"None", []}, {"Some", [IntAnnotated]}]}, OptionAnnotated = {{option, [integer]}, already_normalized, OptionFlat}, {ok, {[], OptionAnnotated}} = aaci_lookup_spec(AACI, "options"), {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.