hakuzaru/src/hz.erl
Jarvis Carroll 6f5525afcf Rename get_function_signature
hz_aaci:aaci_get_function_signature is a bit redundant.
2026-01-15 01:50:50 +00:00

1842 lines
64 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. Get functions are arity 0, and set functions are arity 1.
%%%
%%% 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.
%%%
%%% NOTE:
%%% This module does not implement the OTP application behavior. Refer to hakuzaru.erl.
%%% @end
-module(hz).
-vsn("0.8.2").
-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,
cache_aaci/2,
lookup_aaci/1,
aaci_lookup_spec/2,
contract_call/5,
contract_call/6,
contract_call/10,
decode_bytearray_fate/1, decode_bytearray/2,
spend/5, spend/10,
sign_tx/2, sign_tx/3,
sign_message/2, verify_signature/3,
sign_binary/2, verify_bin_signature/3]).
%%% Types
-export_type([chain_node/0, network_id/0, chain_error/0, aaci/0]).
-include_lib("eunit/include/eunit.hrl").
-type chain_node() :: {inet:ip_address(), inet:port_number()}.
-type network_id() :: string().
-type chain_error() :: not_started
| no_nodes
| timeout
| {timeout, Received :: binary()}
| inet:posix()
| {received, binary()}
| headers
| {headers, map()}
| bad_length
| gc_out_of_range.
-type aaci() :: {aaci, term(), term(), term()}.
-type pubkey() :: unicode:chardata(). % "ak_" ++ _
-type account_id() :: pubkey().
-type contract_id() :: unicode:chardata(). % "ct_" ++ _
-type peer_pubkey() :: string(). % "pp_" ++ _
-type keyblock_hash() :: string(). % "kh_" ++ _
-type contract_byte_array() :: string(). % "cb_" ++ _
-type microblock_hash() :: string(). % "mh_" ++ _
%-type block_state_hash() :: string(). % "bs_" ++ _
%-type proof_of_fraud_hash() :: string() | no_fraud. % "bf_" ++ _
%-type signature() :: string(). % "sg_" ++ _
%-type block_tx_hash() :: string(). % "bx_" ++ _
-type tx_hash() :: string(). % "th_" ++ _
%-type name_hash() :: string(). % "nm_" ++ _
%-type protocol_info() :: #{string() => term()}.
% #{"effective_at_height" => non_neg_integer(),
% "version" => pos_integer()}.
-type keyblock() :: #{string() => term()}.
% <pre>
% #{"beneficiary" => account_id(),
% "hash" => keyblock_hash(),
% "height" => pos_integer(),
% "info" => contract_byte_array(),
% "miner" => account_id(),
% "nonce" => non_neg_integer(),
% "pow" => [non_neg_integer()],
% "prev_hash" => microblock_hash(),
% "prev_key_hash" => keyblock_hash(),
% "state_hash" => block_state_hash(),
% "target" => non_neg_integer(),
% "time" => non_neg_integer(),
% "version" => 5}.
% </pre>
-type microblock_header() :: #{string() => term()}.
% <pre>
% #{"hash" => microblock_hash(),
% "height" => pos_integer(),
% "pof_hash" => proof_of_fraud_hash(),
% "prev_hash" => microblock_hash() | keyblock_hash(),
% "prev_key_hash" => keyblock_hash(),
% "signature" => signature(),
% "state_hash" => block_state_hash(),
% "time" => non_neg_integer(),
% "txs_hash" => block_tx_hash(),
% "version" => 1}.
% </pre>
-type transaction() :: #{string() => term()}.
% <pre>
% #{"block_hash" => microblock_hash(),
% "block_height" => pos_integer(),
% "hash" => tx_hash(),
% "signatures" => [signature()],
% "tx" =>
% #{"abi_version" => pos_integer(),
% "amount" => non_neg_integer(),
% "call_data" => contract_byte_array(),
% "code" => contract_byte_array(),
% "deposit" => non_neg_integer(),
% "gas" => pos_integer(),
% "gas_price" => pos_integer(),
% "nonce" => pos_integer(),
% "owner_id" => account_id(),
% "type" => string(),
% "version" => pos_integer(),
% "vm_version" => pos_integer()}}
% </pre>
-type generation() :: #{string() => term()}.
% <pre>
% #{"key_block" => keyblock(),
% "micro_blocks" => [microblock_hash()]}.
% </pre>
-type account() :: #{string() => term()}.
% <pre>
% #{"balance" => non_neg_integer(),
% "id" => account_id(),
% "kind" => "basic",
% "nonce" => pos_integer(),
% "payable" => true}.
% </pre>
-type contract_data() :: #{string() => term()}.
% <pre>
% #{"abi_version " => pos_integer(),
% "active" => boolean(),
% "deposit" => non_neg_integer(),
% "id" => contract_id(),
% "owner_id" => account_id() | contract_id(),
% "referrer_ids" => [],
% "vm_version" => pos_integer()}.
% </pre>
-type name_info() :: #{string() => term()}.
% <pre>
% #{"id" => name_hash(),
% "owner" => account_id(),
% "pointers" => [],
% "ttl" => non_neg_integer()}.
% </pre>
-type status() :: #{string() => term()}.
% <pre>
% #{"difficulty" => non_neg_integer(),
% "genesis_key_block_hash" => keyblock_hash(),
% "listening" => boolean(),
% "network_id" => string(),
% "node_revision" => string(),
% "node_version" => string(),
% "peer_connections" => #{"inbound" => non_neg_integer(),
% "outbound" => non_neg_integer()},
% "peer_count" => non_neg_integer(),
% "peer_pubkey" => peer_pubkey(),
% "pending_transactions_count" => 51,
% "protocols" => [protocol_info()],
% "solutions" => non_neg_integer(),
% "sync_progress" => float(),
% "syncing" => boolean(),
% "top_block_height" => non_neg_integer(),
% "top_key_block_hash" => keyblock_hash()}.
% </pre>
%%% Get/Set admin functions
-spec network_id() -> Result
when Result :: {ok, NetworkID} | {error, Reason},
NetworkID :: string(),
Reason :: term().
%% @doc
%% Returns the network ID or the atom `none' if unavailable.
%% 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. Note that the first node in the list is
%% the "sticky" node: the one that will be used for submitting transactions and
%% querying `next_nonce'.
chain_nodes() ->
hz_man:chain_nodes().
-spec chain_nodes(List) -> ok | {error, Reason}
when List :: [chain_node()],
Reason :: {invalid, [term()]}.
%% @doc
%% Sets the chain nodes that will be queried whenever you communicate with the chain.
%%
%% The common situation is that a project runs a non-mining node as part of the backend
%% infrastructure. Typically one or two nodes is plenty, but this may need to expand
%% depending on how much query load your application generates.
%%
%% There are two situations: one node, or multiple nodes.
%%
%% Single node:
%% In the case of a single node, everything passes through that one node. Duh.
%%
%% Multiple nodes:
%% In the case of multiple nodes a distinction is made between the node to which
%% transactions that update the chain state are made and to which `next_nonce' queries
%% are made, and nodes that are used for read-only queries. The node to which stateful
%% transactions are submitted is called the "sticky node". This is the first node
%% (head position) in the list of nodes submitted to the chain when `chain_nodes/1'
%% is called. If using multiple nodes but the sticky node should also be used for
%% read-only queries, submit the sticky node at the head of the list and again in
%% the tail.
chain_nodes(List) when is_list(List) ->
hz_man:chain_nodes(List).
-spec tls() -> boolean().
%% @doc
%% Check whether TLS is in use. The typical situation is to not use TLS as nodes that
%% serve as part of the backend of an application are typically run in the same
%% backend network as the application service. When accessing chain nodes over the WAN
%% however, TLS is strongly recommended to avoid a MITM attack.
%%
%% In this version of Hakuzaru TLS is either on or off for all nodes, making a mixed
%% infrastructure complicated to support without two Hakuzaru instances. This will
%% likely become a per-node setting in the future.
%%
%% TLS defaults to `false'.
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 defaults to `false'.
tls(Boolean) ->
hz_man:tls(Boolean).
-spec timeout() -> Timeout
when Timeout :: pos_integer() | infinity.
%% @doc
%% Returns the current request timeout setting in milliseconds.
%% The default timeout is 5,000ms.
%% The max timeout is 120,000ms.
timeout() ->
hz_man:timeout().
-spec timeout(MS) -> ok
when MS :: pos_integer() | infinity.
%% @doc
%% Sets the request timeout in milliseconds.
%% The default timeout is 5,000ms.
%% The max timeout is 120,000ms.
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_sticky(["/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_sticky(["/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} -> hz_aaci:fate_to_erlang(Type, Object);
{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_sticky("/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_sticky(Path) ->
hz_man:request_sticky(unicode:characters_to_list(Path)).
request_sticky(Path, Payload) ->
hz_man:request_sticky(unicode:characters_to_list(Path), Payload).
request(Path) ->
hz_man:request(unicode:characters_to_list(Path)).
request(Path, Payload) ->
hz_man:request(unicode:characters_to_list(Path), Payload).
result({ok, #{"reason" := Reason}}) -> {error, Reason};
result(Received) -> Received.
%%% Contract calls
-spec contract_create(CreatorID, Path, InitArgs) -> Result
when CreatorID :: unicode:chardata(),
Path :: file:filename(),
InitArgs :: [string()],
Result :: {ok, CreateTX} | {error, Reason},
CreateTX :: binary(),
Reason :: file:posix() | term().
%% @doc
%% This function reads the source of a Sophia contract (an .aes file)
%% and returns the unsigned create contract call data with default values.
%% For more control over exactly what those values are, use create_contract/8.
contract_create(CreatorID, Path, InitArgs) ->
case next_nonce(CreatorID) of
{ok, Nonce} ->
Amount = 0,
{ok, Height} = top_height(),
TTL = Height + 262980,
Gas = 500000,
GasPrice = min_gas_price(),
contract_create(CreatorID, Nonce,
Amount, TTL, Gas, GasPrice,
Path, InitArgs);
Error ->
Error
end.
-spec contract_create(CreatorID, Nonce,
Amount, TTL, Gas, GasPrice,
Path, InitArgs) -> Result
when CreatorID :: pubkey(),
Nonce :: pos_integer(),
Amount :: non_neg_integer(),
TTL :: non_neg_integer(),
Gas :: pos_integer(),
GasPrice :: pos_integer(),
Path :: file:filename(),
InitArgs :: [string()],
Result :: {ok, CreateTX} | {error, Reason},
CreateTX :: binary(),
Reason :: term().
%% @doc
%% Create a "create contract" call using the supplied values.
%%
%% Contract creation is an even more opaque process than contract calls if you're new
%% to Gajumaru.
%%
%% The meaning of each argument is as follows:
%% <ul>
%% <li>
%% <b>CreatorID:</b>
%% This is the <em>public</em> key of the entity who will be posting the contract
%% to the chain.
%% The key must be encoded as a string prefixed with `"ak_"' (or `<<"ak_">>' in the
%% case of a binary string, which is also acceptable).
%% The returned call will still need to be signed by the caller's <em>private</em>
%% key.
%% </li>
%% <li>
%% <b>Nonce:</b>
%% This is a sequential integer value that ensures that the hash value of two
%% sequential signed calls with the same contract ID, function and arguments can
%% never be the same.
%% This avoids replay attacks and ensures indempotency despite the distributed
%% nature of the blockchain network).
%% Every CallerID on the chain has a "next nonce" value that can be discovered by
%% querying your Gajumaru node (via `hz:next_nonce(CallerID)', for example).
%% </li>
%% <li>
%% <b>Amount:</b>
%% All Gajumaru transactions can carry an "amount" spent from the origin account
%% (in this case the `CallerID') to the destination. In a "Spend" transaction this
%% is the only value that really matters, but in a contract call the utility is
%% quite different, as you can pay money <em>into</em> a contract and have that
%% contract hold it (for future payouts, to be held in escrow, as proof of intent
%% to purchase or engage in an auction, whatever). Typically this value is 0, but
%% of course there are very good reasons why it should be set to a non-zero value
%% in the case of calls related to contract-governed payment systems.
%% </li>
%% <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() | bad_fun_name | aaci_not_found | 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 = hz_aaci:prepare_aaci(maps:get(aci, Compiled)),
case encode_call_data(AACI, "init", InitArgs) of
{ok, CallData} ->
assemble_calldata(CreatorID, Nonce, Amount, TTL, Gas, GasPrice,
Compiled, CallData);
Error ->
Error
end.
assemble_calldata(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) ->
PK = unicode:characters_to_binary(CreatorID),
try
{account_pubkey, OwnerID} = gmser_api_encoder:decode(PK),
assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData)
catch
Error:Reason -> {Error, Reason}
end.
assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) ->
Code = gmser_contract_code:serialize(Compiled),
Source = unicode:characters_to_binary(maps:get(contract_source, Compiled, <<>>)),
VM = 1,
ABI = 1,
<<CTVersion:32>> = <<VM:16, ABI:16>>,
ContractCreateVersion = 1,
Type = contract_create_tx,
Fields =
[{owner_id, gmser_id:create(account, OwnerID)},
{nonce, Nonce},
{code, Code},
{source, Source},
{ct_version, CTVersion},
{ttl, TTL},
{deposit, 0},
{amount, Amount},
{gas_price, GasPrice},
{gas, Gas},
{call_data, CallData}],
Template =
[{owner_id, id},
{nonce, int},
{code, binary},
{source, binary},
{ct_version, int},
{ttl, int},
{deposit, int},
{amount, int},
{gas_price, int},
{gas, int},
{call_data, binary}],
TXB = gmser_chain_objects:serialize(Type, ContractCreateVersion, Template, Fields),
try
{ok, gmser_api_encoder:encode(transaction, TXB)}
catch
error:Reason -> {error, Reason}
end.
-spec read_aci(Path) -> Result
when Path :: file:filename(),
Result :: {ok, ACI} | {error, Reason},
ACI :: tuple(), % FIXME: Change to correct Sophia record
Reason :: file:posix() | bad_aci.
%% @doc
%% This function reads the contents of an .aci file.
%% ACI data is required for the contract call encoder to function properly.
%% ACI data is can be generated and stored in JSON data, and the Sophia CLI tool
%% can perform this action. Unfortunately, JSON is not the way that ACI data is
%% represented internally, and here we need the actual native representation.
%%
%% ACI encding/decoding and contract call encoding is significantly complex enough that
%% this provides for a pretty large savings in complexity for this library, dramatically
%% reduces runtime dependencies, and makes call encoding much more efficient (as a
%% huge number of steps are completely eliminated by this).
read_aci(Path) ->
case file:read_file(Path) of
{ok, Bin} ->
try
{ok, binary_to_term(Bin, [safe])}
catch
error:badarg -> {error, bad_aci}
end;
Error ->
Error
end.
-spec contract_call(CallerID, AACI, ConID, Fun, Args) -> Result
when CallerID :: unicode:chardata(),
AACI :: aaci() | {aaci, Label :: term()},
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() | {aaci, Label :: term()},
ConID :: unicode:chardata(),
Fun :: string(),
Args :: [string()],
Result :: {ok, CallTX} | {error, Reason},
CallTX :: binary(),
Reason :: term().
%% @doc
%% Just like contract_call/5, but allows you to specify the amount of gas
%% without getting into a major adventure with the other arguments.
%%
%% For details on the meaning of these and other argument values see the doc comment
%% for contract_call/10.
contract_call(CallerID, Gas, AACI, ConID, Fun, Args) ->
case next_nonce(CallerID) of
{ok, Nonce} ->
GasPrice = min_gas_price(),
Amount = 0,
{ok, Height} = top_height(),
TTL = Height + 262980,
contract_call(CallerID, Nonce,
Gas, GasPrice, Amount, TTL,
AACI, ConID, Fun, Args);
Error ->
Error
end.
-spec contract_call(CallerID, Nonce,
Gas, GasPrice, Amount, TTL,
AACI, ConID, Fun, Args) -> Result
when CallerID :: unicode:chardata(),
Nonce :: pos_integer(),
Gas :: pos_integer(),
GasPrice :: pos_integer(),
Amount :: non_neg_integer(),
TTL :: non_neg_integer(),
AACI :: aaci() | {aaci, Label :: term()},
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, hz_aaci:prepare_aaci(ACI)};
Error -> Error
end.
-spec cache_aaci(Label, AACI) -> ok
when Label :: term(),
AACI :: aaci().
%% @doc
%% Caches an AACI for future reference in calls that would otherwise require
%% the AACI as an argument. Once cached, a pre-built AACI can be referenced in
%% later calls by substituting the AACI argument with `{aaci, Label}'.
cache_aaci(Label, AACI) ->
hz_man:cache_aaci(Label, AACI).
-spec lookup_aaci(Label) -> Result
when Label :: term(),
Result :: {ok, aaci()} | error.
%% @doc
%% Retrieve a previously prepared and cached AACI.
lookup_aaci(Label) ->
hz_man:lookup_aaci(Label).
-spec aaci_lookup_spec(AACI, Fun) -> {ok, Type} | {error, Reason}
when AACI :: aaci() | {aaci, Label :: term()},
Fun :: binary() | string(),
Type :: {term(), term()}, % FIXME
Reason :: bad_fun_name | aaci_not_found.
%% @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 = {aaci, _, _, _}, Fun) ->
hz_aaci:get_function_signature(AACI, Fun);
aaci_lookup_spec({aaci, Label}, Fun) ->
case hz_man:lookup_aaci(Label) of
{ok, AACI} -> hz_aaci:get_function_signature(AACI, Fun);
error -> {error, aaci_not_found}
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_data({aaci, Label}, Fun, Args) ->
case hz_man:lookup_aaci(Label) of
{ok, AACI} -> encode_call_data(AACI, Fun, Args);
error -> {error, aaci_not_found}
end.
encode_call_data2(ArgDef, Fun, Args) ->
case hz_aaci:erlang_args_to_fate(ArgDef, Args) of
{ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced);
Errors -> Errors
end.
sign_tx(Unsigned, SecKey) ->
case network_id() of
{ok, NetworkID} -> sign_tx(Unsigned, SecKey, NetworkID);
Error -> Error
end.
sign_tx(Unsigned, SecKey, MNetworkID) ->
UnsignedBin = unicode:characters_to_binary(Unsigned),
NetworkID = unicode:characters_to_binary(MNetworkID),
{ok, TX_Data} = gmser_api_encoder:safe_decode(transaction, UnsignedBin),
{ok, Hash} = eblake2:blake2b(32, TX_Data),
NetworkHash = <<NetworkID/binary, Hash/binary>>,
Signature = ecu_eddsa:sign_detached(NetworkHash, SecKey),
SigTxType = signed_tx,
SigTxVsn = 1,
SigTemplate =
[{signatures, [binary]},
{transaction, binary}],
TX =
[{signatures, [Signature]},
{transaction, TX_Data}],
SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX),
gmser_api_encoder:encode(transaction, SignedTX).
spend(SenderID, SecKey, ReceipientID, Amount, Payload) ->
case status() of
{ok, #{"top_block_height" := Height, "network_id" := NetworkID}} ->
spend(SenderID, SecKey, ReceipientID, Amount, Payload, Height, NetworkID);
Error ->
Error
end.
spend(SenderID, SecKey, RecipientID, Amount, Payload, Height, NetworkID) ->
case next_nonce(SenderID) of
{ok, Nonce} ->
{ok, Height} = top_height(),
TTL = Height + 262980,
Gas = 20000,
GasPrice = min_gas_price(),
spend(SenderID,
SecKey,
RecipientID,
Amount,
GasPrice,
Gas,
TTL,
Nonce,
Payload,
NetworkID);
Error ->
Error
end.
spend(SenderID,
SecKey,
RecipientID,
Amount,
GasPrice,
Gas,
TTL,
Nonce,
Payload,
NetworkID) ->
case decode_account_id(unicode:characters_to_binary(SenderID)) of
{ok, DSenderID} ->
spend2(gmser_id:create(account, DSenderID),
SecKey,
RecipientID,
Amount,
GasPrice,
Gas,
TTL,
Nonce,
Payload,
NetworkID);
Error ->
Error
end.
spend2(DSenderID,
SecKey,
RecipientID,
Amount,
GasPrice,
Gas,
TTL,
Nonce,
Payload,
NetworkID) ->
case decode_account_id(unicode:characters_to_binary(RecipientID)) of
{ok, DRecipientID} ->
spend3(DSenderID,
SecKey,
gmser_id:create(account, DRecipientID),
Amount,
GasPrice,
Gas,
TTL,
Nonce,
Payload,
NetworkID);
Error ->
Error
end.
decode_account_id(B) ->
try
{account_pubkey, PK} = gmser_api_encoder:decode(B),
{ok, PK}
catch
E:R -> {E, R}
end.
spend3(DSenderID,
SecKey,
DRecipientID,
Amount,
GasPrice,
Gas,
TTL,
Nonce,
Payload,
MNetworkID) ->
NetworkID = unicode:characters_to_binary(MNetworkID),
Type = spend_tx,
Vsn = 1,
Fields =
[{sender_id, DSenderID},
{recipient_id, DRecipientID},
{amount, Amount},
{gas_price, GasPrice},
{gas, Gas},
{ttl, TTL},
{nonce, Nonce},
{payload, Payload}],
Template =
[{sender_id, id},
{recipient_id, id},
{amount, int},
{gas_price, int},
{gas, int},
{ttl, int},
{nonce, int},
{payload, binary}],
BinaryTX = gmser_chain_objects:serialize(Type, Vsn, Template, Fields),
NetworkTX = <<NetworkID/binary, BinaryTX/binary>>,
Signature = ecu_eddsa:sign_detached(NetworkTX, SecKey),
SigTxType = signed_tx,
SigTxVsn = 1,
SigTemplate =
[{signatures, [binary]},
{transaction, binary}],
TX_Data =
[{signatures, [Signature]},
{transaction, BinaryTX}],
SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data),
Encoded = gmser_api_encoder:encode(transaction, SignedTX),
hz:post_tx(Encoded).
-spec sign_message(Message, SecKey) -> Sig
when Message :: binary(),
SecKey :: binary(),
Sig :: binary().
sign_message(Message, SecKey) ->
Prefix = message_sig_prefix(),
{ok, PSize} = vencode(byte_size(Prefix)),
{ok, MSize} = vencode(byte_size(Message)),
Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
{ok, Hashed} = eblake2:blake2b(32, Smashed),
ecu_eddsa:sign_detached(Hashed, SecKey).
-spec verify_signature(Sig, Message, PubKey) -> Result
when Sig :: string(), % base64 encoded signature,
Message :: iodata(),
PubKey :: pubkey(),
Result :: {ok, Outcome :: boolean()}
| {error, Reason :: term()}.
%% @doc
%% Verify a message signature given the signature, the message that was signed, and the
%% public half of the key that was used to sign.
%%
%% The result of a complete signature check is a boolean value return in an `{ok, Outcome}'
%% tuple, and any `{error, Reason}' return value is an indication that something about the
%% check failed before verification was able to pass or fail (bad key encoding or similar).
verify_signature(Sig, Message, PubKey) ->
case gmser_api_encoder:decode(PubKey) of
{account_pubkey, PK} -> verify_signature2(Sig, Message, PK);
Other -> {error, {bad_key, Other}}
end.
verify_signature2(Sig, Message, PK) ->
% Gajumaru signatures require messages to be salted and hashed, then
% the hash is what gets signed in order to protect
% the user from accidentally signing a transaction disguised as a message.
%
% Salt the message then hash with blake2b.
Prefix = message_sig_prefix(),
{ok, PSize} = vencode(byte_size(Prefix)),
{ok, MSize} = vencode(byte_size(Message)),
Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
{ok, Hashed} = eblake2:blake2b(32, Smashed),
% Signature = <<(binary_to_integer(Sig, 16)):(64 * 8)>>,
Signature = base64:decode(Sig),
Result = ecu_eddsa:sign_verify_detached(Signature, Hashed, PK),
{ok, Result}.
message_sig_prefix() -> <<"Gajumaru Signed Message:\n">>.
% This is Bitcoin's variable-length unsigned integer encoding
% See: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
vencode(N) when N =< 0 ->
{error, {non_pos_N, N}};
vencode(N) when N < 16#FD ->
{ok, <<N>>};
vencode(N) when N =< 16#FFFF ->
NBytes = eu(N, 2),
{ok, <<16#FD, NBytes/binary>>};
vencode(N) when N =< 16#FFFF_FFFF ->
NBytes = eu(N, 4),
{ok, <<16#FE, NBytes/binary>>};
vencode(N) when N < (2 bsl 64) ->
NBytes = eu(N, 8),
{ok, <<16#FF, NBytes/binary>>}.
% eu = encode unsigned (little endian with a given byte width)
% means add zero bytes to the end as needed
eu(N, Size) ->
Bytes = binary:encode_unsigned(N, little),
NExtraZeros = Size - byte_size(Bytes),
ExtraZeros = << <<0>> || _ <- lists:seq(1, NExtraZeros) >>,
<<Bytes/binary, ExtraZeros/binary>>.
-spec sign_binary(Binary, SecKey) -> Sig
when Binary :: binary(),
SecKey :: binary(),
Sig :: binary().
sign_binary(Binary, SecKey) ->
Prefix = binary_sig_prefix(),
Target = <<Prefix/binary, Binary/binary>>,
{ok, Hash} = eblake2:blake2b(32, Target),
ecu_eddsa:sign_detached(Hash, SecKey).
-spec verify_bin_signature(Sig, Binary, PubKey) -> Result
when Sig :: string(), % base64 encoded signature,
Binary :: binary(),
PubKey :: pubkey(),
Result :: {ok, Outcome :: boolean()}
| {error, Reason :: term()}.
verify_bin_signature(Sig, Binary, PubKey) ->
case gmser_api_encoder:decode(PubKey) of
{account_pubkey, PK} -> verify_bin_signature2(Sig, Binary, PK);
Other -> {error, {bad_key, Other}}
end.
verify_bin_signature2(Sig, Binary, PK) ->
Prefix = binary_sig_prefix(),
Target = <<Prefix/binary, Binary/binary>>,
{ok, Hash} = eblake2:blake2b(32, Target),
Signature = base64:decode(Sig),
Result = ecu_eddsa:sign_verify_detached(Signature, Hash, PK),
{ok, Result}.
binary_sig_prefix() -> <<"Gajumaru Signed Binary:">>.
%%% Debug functionality
% debug_network() ->
% request("/v3/debug/network").
%
% /v3/debug/contracts/create
% /v3/debug/contracts/call
% /v3/debug/oracles/register
% /v3/debug/oracles/extend
% /v3/debug/oracles/query
% /v3/debug/oracles/respond
% /v3/debug/names/preclaim
% /v3/debug/names/claim
% /v3/debug/names/update
% /v3/debug/names/transfer
% /v3/debug/names/revoke
% /v3/debug/transactions/spend
% /v3/debug/channels/create
% /v3/debug/channels/deposit
% /v3/debug/channels/withdraw
% /v3/debug/channels/snapshot/solo
% /v3/debug/channels/set-delegates
% /v3/debug/channels/close/mutual
% /v3/debug/channels/close/solo
% /v3/debug/channels/slash
% /v3/debug/channels/settle
% /v3/debug/transactions/pending
% /v3/debug/names/commitment-id
% /v3/debug/accounts/beneficiary
% /v3/debug/accounts/node
% /v3/debug/peers
% /v3/debug/transactions/dry-run
% /v3/debug/transactions/paying-for
% /v3/debug/check-tx/pool/{hash}
% /v3/debug/token-supply/height/{height}
% /v3/debug/crash