%%% @doc %%% The Vanillae Erlang Interface to Aeternity %%% %%% This module is the high-level interface to the Aeternity blockchain system. %%% The interface is split into three main sections: %%% - Get/Set admin functions %%% - AE node JSON query interface functions %%% - AE contract call and serialization interface functions %%% %%% The get/set admin functions are for setting or checking things like the Aeternity %%% "network ID" and list of addresses of AE nodes you want to use for answering %%% queries to the blockchain (usually you will run these nodes in your own back end). %%% %%% 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 an Aeternity compatible wallet, SDK or (in the case of a web service) in-page %%% code based on a JS library such as Sidekick (another component of Vanillae) can %%% use to generate signature requests and signed transaction objects for submission %%% to an Aeternity network node for inclusion in the transaction mempool. %%% %%% This module also includes the standard OTP "application" interface and start/stop %%% helper functions. %%% @end -module(vanillae). -vsn("0.1.0"). %-behavior(application). -author("Craig Everett "). -copyright("Craig Everett "). -license("GPL-3.0-or-later"). % Get/Set admin functions. -export([network_id/0, network_id/1, ae_nodes/0, ae_nodes/1, timeout/0, timeout/1]). % AE node JSON query interface functions -export([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, tx/1, tx_info/1, post_tx/1, contract/1, contract_code/1, contract_poi/1, % oracle/1, oracle_queries/1, oracle_queries_by_id/2, name/1, % channel/1, peer_pubkey/0, status/0, status_chainends/0]). % AE contract call and serialization interface functions -export([read_aci/1, min_gas/0, min_gas_price/0, min_fee/0, contract_create/3, contract_create/8, prepare_contract/1, contract_call/5, contract_call/6, contract_call/10, verify_signature/3]). % OTP Application Interface -export([start/0, stop/0]). -export([start/2, stop/1]). %%% Types -export_type([ae_node/0, network_id/0, ae_error/0]). -type ae_node() :: {inet:ip_address(), inet:port_number()}. -type network_id() :: string(). -type ae_error() :: not_started | no_nodes | timeout | {timeout, Received :: binary()} | inet:posix() | {received, binary()} | headers | {headers, map()} | bad_length | gc_out_of_range. -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(), % "fee" => pos_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() -> NetworkID when NetworkID :: string() | none. %% @doc %% Returns the AE 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() -> vanillae_man:network_id(). -spec network_id(Identifier) -> ok | {error, Reason} when Identifier :: string() | none, Reason :: not_started. %% @doc %% Sets the network ID, or returns `not_started' if the service is not yet started. network_id(Identifier) -> vanillae_man:network_id(Identifier). -spec ae_nodes() -> [ae_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. ae_nodes() -> vanillae_man:ae_nodes(). -spec ae_nodes(List) -> ok | {error, Reason} when List :: [ae_node()], Reason :: {invalid, [term()]}. %% @doc %% Sets the AE nodes that are intended to be used as your interface to the AE peer %% network. The common situation is that your project runs a non-mining AE 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 Vanillae manager will load balance by round-robin distribution. ae_nodes(List) -> vanillae_man:ae_nodes(List). -spec timeout() -> Timeout when Timeout :: pos_integer() | infinity. %% @doc %% Returns the current request timeout setting in milliseconds. timeout() -> vanillae_man:timeout(). -spec timeout(MS) -> ok when MS :: pos_integer() | infinity. %% @doc %% Sets the request timeout in milliseconds. timeout(MS) -> vanillae_man:timeout(MS). %%% AE node JSON query interface functions -spec top_height() -> {ok, Height} | {error, Reason} when Height :: pos_integer(), Reason :: ae_error(). top_height() -> case top_block() of {ok, #{"micro_block" := #{"height" := Height}}} -> {ok, Height}; {ok, #{"key_block" := #{"height" := Height}}} -> {ok, Height}; Error -> Error end. -spec top_block() -> {ok, TopBlock} | {error, Reason} when TopBlock :: #{Type := Block}, Type :: string(), % "key_block" | "micro_block" Block :: keyblock() | microblock_header(), Reason :: ae_error(). %% @doc %% Returns the current block height as an integer. top_block() -> request("/v2/blocks/top"). -spec kb_current() -> {ok, CurrentBlock} | {error, Reason} when CurrentBlock :: keyblock(), Reason :: ae_error(). %% @doc %% Returns the current keyblock's metadata as a map. kb_current() -> request("/v2/key-blocks/current"). -spec kb_current_hash() -> {ok, Hash} | {error, Reason} when Hash :: keyblock_hash(), Reason :: ae_error(). %% @doc %% Returns the current keyblock's hash. %% Equivalent of calling: %% ``` %% {ok, Current} = kb_current(), %% maps:get("hash", Current), %% ''' kb_current_hash() -> case request("/v2/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 :: ae_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("/v2/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("/v2/key-blocks/pending")). -spec kb_by_hash(ID) -> {ok, KeyBlock} | {error, Reason} when ID :: keyblock_hash(), KeyBlock :: keyblock(), Reason :: ae_error() | string(). %% @doc %% Returns the keyblock identified by the provided hash. kb_by_hash(ID) -> result(request(["/v2/key-blocks/hash/", ID])). -spec kb_by_height(Height) -> {ok, KeyBlock} | {error, Reason} when Height :: non_neg_integer(), KeyBlock :: keyblock(), Reason :: ae_error() | string(). %% @doc %% Returns the keyblock identigied by the provided height. kb_by_height(Height) -> StringN = integer_to_list(Height), result(request(["/v2/key-blocks/height/", StringN])). %kb_insert(KeyblockData) -> % request("/v2/key-blocks", KeyblockData). -spec mb_header(ID) -> {ok, MB_Header} | {error, Reason} when ID :: microblock_hash(), MB_Header :: microblock_header(), Reason :: ae_error() | string(). %% @doc %% Returns the header of the microblock indicated by the provided ID (hash). mb_header(ID) -> result(request(["/v2/micro-blocks/hash/", ID, "/header"])). -spec mb_txs(ID) -> {ok, TXs} | {error, Reason} when ID :: microblock_hash(), TXs :: [transaction()], Reason :: ae_error() | string(). %% @doc %% Returns a list of transactions included in the microblock. mb_txs(ID) -> case request(["/v2/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 :: ae_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(["/v2/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 :: ae_error() | string(). %% @doc %% Retrieve the number of transactions contained in the indicated microblock. mb_tx_count(ID) -> case request(["/v2/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 :: ae_error() | string(). %% @doc %% Retrieve the generation data (keyblock and list of associated microblocks) for %% the current generation. gen_current() -> result(request("/v2/generations/current")). -spec gen_by_id(ID) -> {ok, Generation} | {error, Reason} when ID :: keyblock_hash(), Generation :: generation(), Reason :: ae_error() | string(). %% @doc %% Retrieve generation data (keyblock and list of associated microblocks) by keyhash. gen_by_id(ID) -> result(request(["/v2/generations/hash/", ID])). -spec gen_by_height(Height) -> {ok, Generation} | {error, Reason} when Height :: non_neg_integer(), Generation :: generation(), Reason :: ae_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(["/v2/generations/height/", StrHeight])). -spec acc(AccountID) -> {ok, Account} | {error, Reason} when AccountID :: account_id(), Account :: account(), Reason :: ae_error() | string(). %% @doc %% Retrieve account data by account ID (public key). acc(AccountID) -> result(request(["/v2/accounts/", AccountID])). -spec acc_at_height(AccountID, Height) -> {ok, Account} | {error, Reason} when AccountID :: account_id(), Height :: non_neg_integer(), Account :: account(), Reason :: ae_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(["/v2/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 :: ae_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(["/v2/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 :: ae_error() | string(). %% @doc %% Retrieve a list of transactions pending for the given account. acc_pending_txs(AccountID) -> request(["/v2/accounts/", AccountID, "/transactions/pending"]). -spec next_nonce(AccountID) -> {ok, Nonce} | {error, Reason} when AccountID :: account_id(), Nonce :: non_neg_integer(), Reason :: ae_error() | string(). %% @doc %% Retrieve the next nonce for the given account next_nonce(AccountID) -> % case request(["/v2/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(["/v2/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} = vanillae:kb_current_hash(), %% vanilla:dry_run(TX, Hash), %% ''' %% NOTE: %% For this function to work the Aeternity 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 dry_run(TX, Accounts) -> case kb_current_hash() of {ok, 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("/v2/dry-run", JSON). 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 :: ae_error() | string(). %% @doc %% Retrieve a transaction by ID. tx(ID) -> request(["/v2/transactions/", ID]). -spec tx_info(ID) -> {ok, Info} | {error, Reason} when ID :: tx_hash(), Info :: term(), % FIXME Reason :: ae_error() | string(). %% @doc %% Retrieve TX metadata by ID. tx_info(ID) -> result(request(["/v2/transactions/", ID, "/info"])). -spec post_tx(Data) -> {ok, Result} | {error, Reason} when Data :: term(), % FIXME Result :: term(), % FIXME Reason :: ae_error() | string(). %% @doc %% Post a transaction to the chain. post_tx(Data) -> JSON = zj:binary_encode(#{tx => Data}), request("/v2/transactions", JSON). -spec contract(ID) -> {ok, ContractData} | {error, Reason} when ID :: contract_id(), ContractData :: contract_data(), Reason :: ae_error() | string(). %% @doc %% Retrieve a contract's metadata by ID. contract(ID) -> result(request(["/v2/contracts/", ID])). -spec contract_code(ID) -> {ok, Bytecode} | {error, Reason} when ID :: contract_id(), Bytecode :: contract_byte_array(), Reason :: ae_error() | string(). contract_code(ID) -> case request(["/v2/contracts/", ID, "/code"]) of {ok, #{"bytecode" := Bytecode}} -> {ok, Bytecode}; {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 :: ae_error() | string(). contract_poi(ID) -> request(["/v2/contracts/", ID, "/poi"]). % TODO %oracle(ID) -> % request(["/v2/oracles/", ID]). % TODO %oracle_queries(ID) -> % request(["/v2/oracles/", ID, "/queries"]). % TODO %oracle_queries_by_id(OracleID, QueryID) -> % request(["/v2/oracles/", OracleID, "/queries/", QueryID]). -spec name(Name) -> {ok, Info} | {error, Reason} when Name :: string(), % _ ++ ".chain" Info :: name_info(), Reason :: ae_error() | string(). %% @doc %% Retrieve a name's chain information. name(Name) -> result(request(["/v2/names/", Name])). % TODO %channel(ID) -> % request(["/v2/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 there an AE node is reachable at %% the given address. peer_pubkey() -> case request("/v2/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 :: ae_error(). %% @doc %% Retrieve the node's status and meta it currently has about the chain. status() -> request("/v2/status"). -spec status_chainends() -> {ok, ChainEnds} | {error, Reason} when ChainEnds :: [keyblock_hash()], Reason :: ae_error(). %% @doc %% Retrieve the latest keyblock hashes status_chainends() -> request("/v2/status/chain-ends"). request(Path) -> vanillae_man:request(unicode:characters_to_list(Path)). request(Path, Payload) -> vanillae_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, Gas = 100000, GasPrice = min_gas_price(), Fee = min_fee(), contract_create(CreatorID, Nonce, Amount, Gas, GasPrice, Fee, Path, InitArgs); Error -> Error end. -spec contract_create(CreatorID, Nonce, Amount, Gas, GasPrice, Fee, Path, InitArgs) -> Result when CreatorID :: unicode:chardata(), Nonce :: pos_integer(), Amount :: non_neg_integer(), Gas :: pos_integer(), GasPrice :: pos_integer(), Fee :: non_neg_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 Aeternity. %% %% The meaning of each argument is as follows: %%