From 3f80b11d6397e826abbe7c4b6aec06fd4966b33b Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Wed, 27 Mar 2024 14:45:50 +0900 Subject: [PATCH] Initial --- Emakefile | 1 + LICENSE | 19 + doc/overview.edoc | 77 ++ ebin/hakuzaru.app | 8 + ebin/hakuzaru.beam | Bin 0 -> 1396 bytes ebin/hz.beam | Bin 0 -> 56220 bytes ebin/hz_fetcher.beam | Bin 0 -> 15712 bytes ebin/hz_man.beam | Bin 0 -> 11876 bytes ebin/hz_sup.beam | Bin 0 -> 1696 bytes src/hakuzaru.erl | 50 + src/hz.erl | 2139 ++++++++++++++++++++++++++++++++++++++++++ src/hz_fetcher.erl | 240 +++++ src/hz_man.erl | 289 ++++++ src/hz_sup.erl | 43 + zomp.meta | 25 + 15 files changed, 2891 insertions(+) create mode 100644 Emakefile create mode 100644 LICENSE create mode 100644 doc/overview.edoc create mode 100644 ebin/hakuzaru.app create mode 100644 ebin/hakuzaru.beam create mode 100644 ebin/hz.beam create mode 100644 ebin/hz_fetcher.beam create mode 100644 ebin/hz_man.beam create mode 100644 ebin/hz_sup.beam create mode 100644 src/hakuzaru.erl create mode 100644 src/hz.erl create mode 100644 src/hz_fetcher.erl create mode 100644 src/hz_man.erl create mode 100644 src/hz_sup.erl create mode 100644 zomp.meta diff --git a/Emakefile b/Emakefile new file mode 100644 index 0000000..68c7b67 --- /dev/null +++ b/Emakefile @@ -0,0 +1 @@ +{"src/*", [debug_info, {i, "include/"}, {outdir, "ebin/"}]}. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f206bfb --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2022 Craig Everett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/doc/overview.edoc b/doc/overview.edoc new file mode 100644 index 0000000..4890d35 --- /dev/null +++ b/doc/overview.edoc @@ -0,0 +1,77 @@ +@author Craig Everett [https://gitlab.com/zxq9/zj] +@version 0.3.0 +@title Vanillae: Aeternity blockchain bindings for Erlang + +@doc +This Erlang application provides bindings for the Erlang blockchain. +The primary goal is for usage to be easy to understand and as simple as possible to use. +The secondary goal is to enable real-world projects to more easily connect with the blockchain in an obvious way and provide a clear path for them to provide feedback regarding areas that are difficult to understand, functionality that is lacking, and explain their use cases to us so we can more easily provide needed features and usage examples to make adoption easier. + +== Basic operation == +All external interfaces expected to be used by authors of programs that use Vanillae are built into the `vanillae' module. + +When Vanillae is started as an application a named process called `vanillae_man' is spawned that manages interactions with and the state of the service, as well as a simple-one-for-one supervisor that manages the lifecycle of Vanillae workers (defined in `vanillae_fetcher'). + +After startup `vanillae_man' must be given the address and port of a list of Aeternity nodes that are available to service requests. Note that the service nodes will need to have the "dry run" endpoint enabled and the internal service query port made available in order to provide "dry run" and mempool TX submission functionality. + +The `vanillae_man' will round-robin requests to however many Aeternity nodes are provided in its configuration. Note that this congiruation is dynamic and can be changed completely at runtime. + +== Functions == +The `vanillae' module exposes one function per blockchain feature provided. Most of these are actually wrappers for blockchain endpoint functions, others provide functionality specific to accomplishing a local processing task related to chain data. + +== Initialization == +When Vanillae is first started the vanillae_man is started but does not yet know what Aeternity nodes to use to service queries. You will need to provide it with at least one node and port where it can make Aeternity endpoint calls. + +Note that if you will need to make read-only calls to contracts that are deployed on chain (to queery their state or perform specific read-only operations provided by the contract) the backend nodes you configure will need to be configured with "dry-run" enabled. + +Example of a shell session where vanillae is started and initialized manually with an AE node in the local network at 192.168.10.10:3013: + +``` +1> vanillae:start(). +Starting. +ok +2> vanillae:status(). +{error,no_nodes} +3> vanillae:ae_nodes([{"192.168.7.7", 3013}]). +ok +4> vanillae:status(). +{ok,#{"difficulty" => 59729882, + "genesis_key_block_hash" => + "kh_wUCideEB8aDtUaiHCtKcfywU6oHZW6gnyci8Mw6S1RSTCnCRu", + "listening" => true,"network_id" => "ae_uat", + "node_revision" => + "3a08153c635c53d92029a617f2e784731ba367c6", + "node_version" => "6.7.0", + "peer_connections" => #{"inbound" => 25,"outbound" => 10}, + "peer_count" => 50, + "peer_pubkey" => + "pp_fCBqobeSwhdnrzC8DoSsmWbf2GzDK61CJujmsCEd3RUkmh9Ny", + "pending_transactions_count" => 2, + "protocols" => + [#{"effective_at_height" => 425900,"version" => 5}, + #{"effective_at_height" => 154300,"version" => 4}, + #{"effective_at_height" => 82900,"version" => 3}, + #{"effective_at_height" => 40900,"version" => 2}, + #{"effective_at_height" => 0,"version" => 1}], + "solutions" => 0,"sync_progress" => 100.0, + "syncing" => false,"top_block_height" => 802644, + "top_key_block_hash" => + "kh_28LZSvHZPCGqeWsMsqtSjxQjQHKW1pHzoBex97oMT7U2HcLPgV"}} +''' + +Alternatively, here is a start function for an application using Vanillae that initializes vanillae_man with a list of nodes provided by a configuration file: + +``` +start(normal, _Args) -> + ok = application:ensure_started(sasl), + {ok, Started} = application:ensure_all_started(cowboy), + ok = application:ensure_started(vanillae), + Nodes = proplists:get_value(ae_nodes, read_config(), []), + ok = vanillae:ae_nodes(Nodes), + ok = log(info, "Started: ~p~n", [[vanillae | Started]]), + Routes = [{'_', [{"/", count_top, []}]}], + Dispatch = cowboy_router:compile(Routes), + Env = #{env => #{dispatch => Dispatch}}, + {ok, _} = cowboy:start_clear(count_listener, [{port, 8080}], Env), + count_sup:start_link(). +''' diff --git a/ebin/hakuzaru.app b/ebin/hakuzaru.app new file mode 100644 index 0000000..2332c9f --- /dev/null +++ b/ebin/hakuzaru.app @@ -0,0 +1,8 @@ +{application,hakuzaru, + [{registered,[]}, + {included_applications,[]}, + {applications,[stdlib,kernel]}, + {description,"Gajumaru interoperation library"}, + {vsn,"0.1.0"}, + {modules,[hakuzaru,hz,hz_fetcher,hz_man,hz_sup]}, + {mod,{hakuzaru,[]}}]}. diff --git a/ebin/hakuzaru.beam b/ebin/hakuzaru.beam new file mode 100644 index 0000000000000000000000000000000000000000..ee022be029b8fc0e3c67613c9e9485256f3de02b GIT binary patch literal 1396 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 literal 0 HcmV?d00001 diff --git a/ebin/hz_fetcher.beam b/ebin/hz_fetcher.beam new file mode 100644 index 0000000000000000000000000000000000000000..02f0ae3a2219e9adcc49b0a68422963c28d49467 GIT binary patch literal 15712 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 literal 0 HcmV?d00001 diff --git a/ebin/hz_man.beam b/ebin/hz_man.beam new file mode 100644 index 0000000000000000000000000000000000000000..51a679438ad007468f9551856deaff7e9feba66c GIT binary patch literal 11876 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 literal 0 HcmV?d00001 diff --git a/ebin/hz_sup.beam b/ebin/hz_sup.beam new file mode 100644 index 0000000000000000000000000000000000000000..213a2daf3410bd6343381e8be8a34ecd0083d387 GIT binary patch literal 1696 zcma)6Yfuwc6y79|m|!IY1yPhB4DkU;cqA%&-<>`81cpVr zLlAYpUw~L5#$%R15EKkRqpCAx2BV%~z~vY&(;_+z11w?8gcwlF4ACLDMKY>1YGE0o zOGK$Krj_edmMR#RePRZI{81$gNCChEAVw_!9l(slEM<~e^B@vi#IhEVokT$prIan@ zLnMAFpUm=Ol0;qtiYSOlu_lF=LS$AXM2Z)&C{iYqq@P$bNm(L_HDO}2DNq!SMT59d zaI&7@`2dtjkSCc6mklTK%7JNx+aw-$n$rH4&QkDqIaU%vBl9EDE1)-794T*6a z19W`C-yHZ;Oa`3>GUs!>xb9#{Pk_~O6M`DCiGZn}{PiTTO_dvQHHvAVIsTX&QLzI| zFb3l|+b1I}WvPhxa206saw3od)u&^KN{#;yXPOpKz&Zo0ftW$zp>CdBz8i|UY2`SK zC4m|W)+GYX*%O*6AT|wTH6SRVdˢmh;E#XzVgr!tgqf>AY5n!ufeXrL*GPN6j_ zVcx_o4B&nWkd4bzqscH&0e`p#&!jN$QuHZ2wOnJ&kYh%k0aNfkyaX;NaiC>If(ij; zHD_x$D5SG#WqV`p>+Xq|f_S#bk6imrWaM-!^^2$7ZP6ihArUwC#U8(B*+@OrC+Jvp zTO;hW9X_?JiE_erDExD%e5t33tFq;AnyE(|3S0**O9ibZg7dx>@3!>CTU<`0JXZSt z#6MoJU=`b}dMy0VXC6hx&xO+&0VrVYg*!-oOLDc=8rsQ@f)1#tn9X(muG6*TKh#oG?{9fwd1rG z<#YeZe9ziAtQEdJk*f=G79LoRd1DvG17F7D*Q$iCC>}yn``G(UuPRfGKaS?mvQONI zD;#vTjOqyRdi?e7pBv2d^7I=HTOSRQTI)zIBehw-GKK|ySK>pooVsd>p9I-?5h2%i zKHv{KZHPT(TW!BD(PAk`n`Ff-w1H2sSZ`hXFzAGyKkVkE_nUc@9unMh0B@Vz1l3lcpO zO+9;3-sBk1^zChGu;KFa>zxfvHd!5+`pd5Qwl2Nd1+JABXJzmirw=zA{ry_s*&Kn- z;yRzjyrIlp@cqi(A9fxabSZRcHsP|`s@R-c<+E)MX#5?E%Xh@|bJ$v1eQr@u?}Mpn zL-U$97q5vt_o{uma=3;u$oy+~?zEKl8PBaXm%ht+IIu9%ffBr{(PF#Kd;7B0Zk1If zS-fvYDVbk&F^se%E2Fsbg~<0-zggytyE!fEmA2-mR&eVjBcraIn>kJ+_H9AsEH1rj z2d}%wUi>{evvu=~(Uj ok | {error, Reason :: term()}. +%% @doc +%% Public function for manually starting the Hakuzaru application. +%% +%% NOTE: +%% To start it as a subordinate service within your own supervision tree rather than +%% as a peer Erlang application within your node, add the hz_sup to your own +%% supervision tree. + +start() -> + application:start(hakuzaru). + + +-spec stop() -> ok | {error, Reason :: term()}. +%% @doc +%% Public function for manually stopping the Hakuzaru application. + +stop() -> + application:stop(hakuzaru). + + +-spec start(normal, term()) -> {ok, pid()}. +%% @private + +start(normal, _Args) -> + hz_sup:start_link(). + + +-spec stop(term()) -> ok. +%% @private + +stop(_State) -> + ok. diff --git a/src/hz.erl b/src/hz.erl new file mode 100644 index 0000000..f9e28c5 --- /dev/null +++ b/src/hz.erl @@ -0,0 +1,2139 @@ +%%% @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 +%%% - 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 Gajumaru +%%% "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 Gajumaru 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 Hakuzaru) can +%%% use to generate signature requests and signed transaction objects for submission +%%% to an Gajumaru network node for inclusion in the transaction mempool. +%%% +%%% This module also includes the standard OTP "application" interface and start/stop +%%% helper functions. +%%% @end + +-module(hz). +-vsn("0.4.1"). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0-or-later"). + +% Get/Set admin functions. +-export([network_id/0, network_id/1, + chain_nodes/0, chain_nodes/1, + tls/0, tls/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, + aaci_lookup_spec/2, + contract_call/5, + contract_call/6, + contract_call/10, + decode_bytearray_fate/1, decode_bytearray/2, + verify_signature/3]). + + +%%% Types + +-export_type([chain_node/0, network_id/0, chain_error/0]). + + +-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 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() -> + hz_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) -> + hz_man:network_id(Identifier). + + +-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 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 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). + + + +%%% AE node 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 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("/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 aeb_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} = aeser_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 = aeb_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 :: term(), % FIXME + Result :: term(), % FIXME + Reason :: chain_error() | string(). +%% @doc +%% Post a transaction to the chain. + +post_tx(Data) -> + JSON = zj:binary_encode(#{tx => Data}), + request("/v3/transactions", JSON). + + +-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_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"]). + +% TODO +%oracle(ID) -> +% request(["/v3/oracles/", ID]). + +% TODO +%oracle_queries(ID) -> +% request(["/v3/oracles/", ID, "/queries"]). + +% TODO +%oracle_queries_by_id(OracleID, QueryID) -> +% request(["/v3/oracles/", OracleID, "/queries/", QueryID]). + + +-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 there an AE 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, + 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 :: pubkey(), + 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 Gajumaru. +%% +%% The meaning of each argument is as follows: +%%
    +%%
  • +%% CreatorID: +%% This is the public 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 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). +%%
  • +%%
  • +%% 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. +%%
  • +%%
  • +%% 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 aettos (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. +%%
  • +%%
  • +%% Fee: +%% This value should really be caled `Bribe' or `Tip'. +%% This is a flat fee in aettos that is paid into the block reward, thereby allowing +%% an additional way to prioritize a given transaction above others, even if the +%% transaction will not consume much gas. +%%
  • +%%
  • +%% 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_create(CreatorID, Nonce, + Amount, Gas, GasPrice, Fee, + Path, InitArgs) -> + case aeso_compiler:file(Path, [{aci, json}]) of + {ok, Compiled} -> + contract_create2(CreatorID, Nonce, + Amount, Gas, GasPrice, Fee, + Compiled, InitArgs); + Error -> + Error + end. + +contract_create2(CreatorID, Nonce, + Amount, Gas, GasPrice, Fee, + Compiled, InitArgs) -> + AACI = prepare_aaci(maps:get(aci, Compiled)), + case encode_call_data(AACI, "init", InitArgs) of + {ok, CallData} -> + contract_create3(CreatorID, Nonce, + Amount, Gas, GasPrice, Fee, + Compiled, CallData); + Error -> + Error + end. + +contract_create3(CreatorID, Nonce, + Amount, Gas, GasPrice, Fee, + Compiled, CallData) -> + PK = unicode:characters_to_binary(CreatorID), + try + {account_pubkey, OwnerID} = aeser_api_encoder:decode(PK), + contract_create4(OwnerID, Nonce, + Amount, Gas, GasPrice, Fee, + Compiled, CallData) + catch + Error:Reason -> {Error, Reason} + end. + +contract_create4(OwnerID, Nonce, + Amount, Gas, GasPrice, Fee, + Compiled, CallData) -> + Code = aeser_contract_code:serialize(Compiled), + VM = 7, + ABI = 3, + <> = <>, + ContractCreateVersion = 1, + TTL = 0, + Type = contract_create_tx, + Fields = + [{owner_id, aeser_id:create(account, OwnerID)}, + {nonce, Nonce}, + {code, Code}, + {ct_version, CTVersion}, + {fee, Fee}, + {ttl, TTL}, + {deposit, 0}, + {amount, Amount}, + {gas, Gas}, + {gas_price, GasPrice}, + {call_data, CallData}], + Template = + [{owner_id, id}, + {nonce, int}, + {code, binary}, + {ct_version, int}, + {fee, int}, + {ttl, int}, + {deposit, int}, + {amount, int}, + {gas, int}, + {gas_price, int}, + {call_data, binary}], + TXB = aeser_chain_objects:serialize(Type, ContractCreateVersion, Template, Fields), + try + {ok, aeser_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 produced by AEL (the Gajumaru +%% Launcher). 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. For +%% that reason Gajumaru's GUI launcher (AEL) has a "Developer's Workbench" tool +%% that can produce an .aci file from a contract's source code and store it in the +%% native Erlang format. +%% +%% 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 :: map(), + 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', `Fee', +%% and `Amount' to simplify the call (10 args is a bit much for normal calls!). +%% The values used are 20k for `Gas' and `Fee', 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(), + Fee = min_fee(), + Amount = 0, + contract_call(CallerID, Nonce, + Gas, GasPrice, Fee, Amount, + 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 :: map(), + 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(), + Fee = min_fee(), + Amount = 0, + contract_call(CallerID, Nonce, + Gas, GasPrice, Fee, Amount, + AACI, ConID, Fun, Args); + Error -> + Error + end. + + +-spec contract_call(CallerID, Nonce, + Gas, GasPrice, Fee, Amount, + AACI, ConID, Fun, Args) -> Result + when CallerID :: unicode:chardata(), + Nonce :: pos_integer(), + Gas :: pos_integer(), + GasPrice :: pos_integer(), + Fee :: non_neg_integer(), + Amount :: non_neg_integer(), + AACI :: map(), + 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 aettos (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. +%%
  • +%%
  • +%% Fee: +%% This value should really be caled `Bribe' or `Tip'. +%% This is a flat fee in aettos that is paid into the block reward, thereby allowing +%% an additional way to prioritize a given transaction above others, even if the +%% transaction will not consume much gas. +%%
  • +%%
  • +%% 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, Fee, Amount, AACI, ConID, Fun, Args) -> + case encode_call_data(AACI, Fun, Args) of + {ok, CD} -> contract_call2(CallerID, Nonce, Gas, GP, Fee, Amount, ConID, CD); + Error -> Error + end. + +contract_call2(CallerID, Nonce, Gas, GasPrice, Fee, Amount, ConID, CallData) -> + CallerBin = unicode:characters_to_binary(CallerID), + try + {account_pubkey, PK} = aeser_api_encoder:decode(CallerBin), + contract_call3(PK, Nonce, Gas, GasPrice, Fee, Amount, ConID, CallData) + catch + Error:Reason -> {Error, Reason} + end. + +contract_call3(PK, Nonce, Gas, GasPrice, Fee, Amount, ConID, CallData) -> + ConBin = unicode:characters_to_binary(ConID), + try + {contract_pubkey, CK} = aeser_api_encoder:decode(ConBin), + contract_call4(PK, Nonce, Gas, GasPrice, Fee, Amount, CK, CallData) + catch + Error:Reason -> {Error, Reason} + end. + +contract_call4(PK, Nonce, Gas, GasPrice, Fee, Amount, CK, CallData) -> + ABI = 3, + TTL = 0, + CallVersion = 1, + Type = contract_call_tx, + Fields = + [{caller_id, aeser_id:create(account, PK)}, + {nonce, Nonce}, + {contract_id, aeser_id:create(contract, CK)}, + {abi_version, ABI}, + {fee, Fee}, + {ttl, TTL}, + {amount, Amount}, + {gas, Gas}, + {gas_price, GasPrice}, + {call_data, CallData}], + Template = + [{caller_id, id}, + {nonce, int}, + {contract_id, id}, + {abi_version, int}, + {fee, int}, + {ttl, int}, + {amount, int}, + {gas, int}, + {gas_price, int}, + {call_data, binary}], + TXB = aeser_chain_objects:serialize(Type, CallVersion, Template, Fields), + try + {ok, aeser_api_encoder:encode(transaction, TXB)} + catch + error:Reason -> {error, Reason} + end. + + +-spec prepare_contract(File) -> {ok, AACI} | {error, Reason} + when File :: file:filename(), + AACI :: map(), + Reason :: term(). +%% @doc +%% Compile a contract and extract the function spec meta for use in future formation +%% of calldata + +prepare_contract(File) -> + case aeso_compiler:file(File, [{aci, json}]) of + {ok, #{aci := ACI}} -> {ok, prepare_aaci(ACI)}; + Error -> Error + end. + +prepare_aaci(ACI) -> + % NOTE this will also pick up the main contract; as a result the main + % contract extraction later on shouldn't bother with typedefs. + Contracts = [ContractDef || #{contract := ContractDef} <- ACI], + Types = simplify_contract_types(Contracts, #{}), + + [{NameBin, SpecDefs}] = + [{N, F} + || #{contract := #{kind := contract_main, + functions := F, + name := N}} <- ACI], + Name = binary_to_list(NameBin), + Specs = simplify_specs(SpecDefs, #{}, Types), + {aaci, Name, Specs, Types}. + +simplify_contract_types([], Types) -> + Types; +simplify_contract_types([Next | Rest], Types) -> + TypeDefs = maps:get(typedefs, Next), + NameBin = maps:get(name, Next), + Name = binary_to_list(NameBin), + Types2 = maps:put(Name, {[], contract}, Types), + Types3 = case maps:find(state, Next) of + {ok, StateDefACI} -> + StateDefOpaque = opaque_type([], StateDefACI), + maps:put(Name ++ ".state", {[], StateDefOpaque}, Types2); + error -> + Types2 + end, + Types4 = simplify_typedefs(TypeDefs, Types3, Name ++ "."), + simplify_contract_types(Rest, Types4). + +simplify_typedefs([], Types, _NamePrefix) -> + Types; +simplify_typedefs([Next | Rest], Types, NamePrefix) -> + #{name := NameBin, vars := ParamDefs, typedef := T} = Next, + Name = NamePrefix ++ binary_to_list(NameBin), + Params = [binary_to_list(Param) || #{name := Param} <- ParamDefs], + Type = opaque_type(Params, T), + NewTypes = maps:put(Name, {Params, Type}, Types), + simplify_typedefs(Rest, NewTypes, NamePrefix). + +simplify_specs([], Specs, _Types) -> + Specs; +simplify_specs([Next | Rest], Specs, Types) -> + #{name := NameBin, arguments := ArgDefs, returns := ResultDef} = Next, + Name = binary_to_list(NameBin), + ArgTypes = [simplify_args(Arg, Types) || Arg <- ArgDefs], + {ok, ResultType} = type(ResultDef, Types), + NewSpecs = maps:put(Name, {ArgTypes, ResultType}, Specs), + simplify_specs(Rest, NewSpecs, Types). + +simplify_args(#{name := NameBin, type := TypeDef}, Types) -> + Name = binary_to_list(NameBin), + % FIXME We should make this error more informative, and continue + % propogating it up, so that the user can provide their own ACI and find + % out whether it worked or not. At that point ACI -> AACI could almost be a + % module or package of its own. + {ok, Type} = type(TypeDef, Types), + {Name, Type}. + +% 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 aebytecode 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 flattened +% 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 flattened 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. + +type(T, Types) -> + O = opaque_type([], T), + flatten_opaque_type(O, Types). + +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(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, lists for user defined types +opaque_type_name(<<"int">>) -> integer; +opaque_type_name(<<"address">>) -> address; +opaque_type_name(<<"contract">>) -> contract; +opaque_type_name(<<"bool">>) -> boolean; +opaque_type_name(<<"option">>) -> option; +opaque_type_name(<<"list">>) -> list; +opaque_type_name(<<"map">>) -> map; +opaque_type_name(<<"string">>) -> string; +opaque_type_name(Name) -> binary_to_list(Name). + +flatten_opaque_type(T, Types) -> + case normalize_opaque_type(T, Types) of + {ok, AlreadyNormalized, NOpaque, NExpanded} -> + flatten_opaque_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types); + Error -> + Error + end. + +flatten_opaque_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) -> + case flatten_normalized_type(NExpanded, Types) of + {ok, Flat} -> + case AlreadyNormalized of + true -> {ok, {T, already_normalized, Flat}}; + false -> {ok, {T, NOpaque, Flat}} + end; + Error -> + Error + end. + +flatten_opaque_types([T | Rest], Types, Acc) -> + case flatten_opaque_type(T, Types) of + {ok, Type} -> flatten_opaque_types(Rest, Types, [Type | Acc]); + Error -> Error + end; +flatten_opaque_types([], _Types, Acc) -> + {ok, lists:reverse(Acc)}. + +flatten_opaque_bindings([{Name, T} | Rest], Types, Acc) -> + case flatten_opaque_type(T, Types) of + {ok, Type} -> flatten_opaque_bindings(Rest, Types, [{Name, Type} | Acc]); + Error -> Error + end; +flatten_opaque_bindings([], _Types, Acc) -> + {ok, lists:reverse(Acc)}. + +flatten_opaque_variants([{Name, Elems} | Rest], Types, Acc) -> + case flatten_opaque_types(Elems, Types, []) of + {ok, ElemsFlat} -> flatten_opaque_variants(Rest, Types, [{Name, ElemsFlat} | Acc]); + Error -> Error + end; +flatten_opaque_variants([], _Types, Acc) -> + {ok, lists:reverse(Acc)}. + +flatten_normalized_type(PrimitiveType, _Types) when is_atom(PrimitiveType) -> + {ok, PrimitiveType}; +flatten_normalized_type({variant, VariantsOpaque}, Types) -> + case flatten_opaque_variants(VariantsOpaque, Types, []) of + {ok, Variants} -> {ok, {variant, Variants}}; + Error -> Error + end; +flatten_normalized_type({record, FieldsOpaque}, Types) -> + case flatten_opaque_bindings(FieldsOpaque, Types, []) of + {ok, Fields} -> {ok, {record, Fields}}; + Error -> Error + end; +flatten_normalized_type({T, ElemsOpaque}, Types) -> + case flatten_opaque_types(ElemsOpaque, Types, []) of + {ok, Elems} -> {ok, {T, Elems}}; + Error -> Error + end. + +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, {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_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) -> + try + case aeser_api_encoder:decode(unicode:characters_to_binary(S)) of + {account_pubkey, Key} -> {ok, {address, Key}}; + _ -> single_error({invalid, O, N, S}) + end + catch + error:_ -> single_error({invalid, O, N, S}) + end; +coerce({_, _, address}, {address, Bin}, from_fate) -> + Address = aeser_api_encoder:encode(account_pubkey, Bin), + {ok, unicode:characters_to_list(Address)}; +coerce({O, N, contract}, S, to_fate) -> + try + case aeser_api_encoder:decode(unicode:characters_to_binary(S)) of + {contract_pubkey, Key} -> {ok, {contract, Key}}; + _ -> single_error({invalid, O, N, S}) + end + catch + error:_ -> single_error({invalid, O, N, S}) + end; +coerce({_, _, contract}, {contract, Bin}, from_fate) -> + Address = aeser_api_encoder:encode(contract_pubkey, Bin), + {ok, unicode:characters_to_list(Address)}; +coerce({_, _, boolean}, true, _) -> + {ok, true}; +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({_, _, {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_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, term(), term(), term()}, % FIXME + 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(), +%% (the default set in aeternity_config_schema.json). +%% +%% 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() -> + 1000000000. + + +-spec min_gas() -> integer(). +%% @doc +%% This function always returns 20,000 in the current version. +%% +%% There is no actual minimum gas price, but this figure provides a lower limit toward +%% successful completion of general contract calls while not too severely limiting the +%% number of TXs that may appear in a single microblock based on the per-block gas +%% maximum (6,000,000 / 20,000 = 300 TXs in a microblock -- which at the moment seems +%% like plenty). + +min_gas() -> + 20000. + + +-spec min_fee() -> integer(). +%% @doc +%% This function always returns 200,000,000,000,000 in the current version. +%% +%% This is the minimum fee amount currently accepted -- it is up to callers whether +%% they want to customize this value higher (or possibly lower, though as things stand +%% that would only work on an independent AE-based network, not the actual Gajumaru +%% mainnet or testnet). + +min_fee() -> + 200000000000000. + + +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} -> aeb_fate_abi:create_calldata(Fun, Coerced); + Errors -> Errors + end. + + +-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 aeser_api_encoder:decode(PubKey) of + {account_pubkey, PK} -> verify_signature2(Sig, Message, PK); + Other -> {error, {bad_key, Other}} + end. + +verify_signature2(Sig, Message, PK) -> + % Superhero salts/hashes the message before signing it, in order to protect + % the user from accidentally signing a transaction disguised as a message. + % In order to verify the signature, we have to duplicate superhero's + % salt/hash procedure here. + % + % Salt the message then hash with blake2b. See: + % 1. Erlang Blake2 blake2b/2 function: + % https://gitlab.com/ioecs/eblake2/blob/60a079f00d72d1bfcc25de8e6996d28f912db3fd/src/eblake2.erl#L23-L25 + % 2. SDK salting step: + % https://gitlab.com/ioecs/aepp-sdk-js/blob/370f1e30064ad0239ba59931908d9aba0a2e86b6/src/utils/crypto.ts#L171-L175 + % 3. SDK hashing: + % https://gitlab.com/ioecs/aepp-sdk-js/blob/370f1e30064ad0239ba59931908d9aba0a2e86b6/src/utils/crypto.ts#L83-L85 + Prefix = <<"aeternity Signed Message:\n">>, +% Prefix = <<"gajumaru Signed Message:\n">>, % TODO: Switch the prefix after we kill Superhero + {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)>>, + 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, {negative_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 diff --git a/src/hz_fetcher.erl b/src/hz_fetcher.erl new file mode 100644 index 0000000..fd9c33d --- /dev/null +++ b/src/hz_fetcher.erl @@ -0,0 +1,240 @@ +%%% @private + +-module(hz_fetcher). +-vsn("0.4.1"). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("MIT"). + +-export([connect/4, slowly_connect/4]). + +-include("$zx_include/zx_logger.hrl"). + + +connect(Node = {Host, Port}, Request, From, Timeout) -> + Timer = erlang:send_after(Timeout, self(), timeout), + Options = [{mode, binary}, {nodelay, true}, {active, once}], + case gen_tcp:connect(Host, Port, Options, 3000) of + {ok, Sock} -> do(Request, Sock, Node, From, Timer); + Error -> gen_server:reply(From, Error) + end. + +do(Request, Sock, Node, From, Timer) -> + Formed = unicode:characters_to_list(form(Request, Node)), + case gen_tcp:send(Sock, Formed) of + ok -> await(Sock, From, Timer); + Error -> gen_server:reply(From, Error) + end. + +await(Sock, From, Timer) -> + receive + {tcp, Sock, Bin} -> + parse(Bin, Sock, From, Timer); + {tcp_closed, Sock} -> + ok = erlang:cancel_timer(Timer, [{async, true}]), + gen_server:reply(From, {error, enotconn}); + timeout -> + gen_server:reply(From, {error, timeout}) + after 120000 -> + gen_server:reply(From, {error, timeout}) + end. + + +form({get, Path}, Node) -> + ["GET ", Path, " HTTP/1.1\r\n", + "Host: ", host_string(Node), "\r\n", + "User-Agent: Kanou/0.1.0\r\n", + "Accept: */*\r\n\r\n"]; +form({post, Path, Payload}, Node) -> + ByteSize = integer_to_list(byte_size(Payload)), + ["POST ", Path, " HTTP/1.1\r\n", + "Host: ", host_string(Node), "\r\n", + "Content-Type: application/json\r\n", + "Content-Length: ", ByteSize, "\r\n", + "User-Agent: Kanou/0.1.0\r\n", + "Accept: */*\r\n\r\n", + Payload]. + + +host_string({Address, Port}) when is_list(Address) -> + PortS = integer_to_list(Port), + [Address, ":", PortS]; +host_string({Address, Port}) when is_atom(Address) -> + AddressS = atom_to_list(Address), + PortS = integer_to_list(Port), + [AddressS, ":", PortS]; +host_string({Address, Port}) -> + AddressS = inet:ntoa(Address), + PortS = integer_to_list(Port), + [AddressS, ":", PortS]. + + +parse(Received, Sock, From, Timer) -> + case Received of + <<"HTTP/1.1 200 OK\r\n", Tail/binary>> -> + parse2(200, Tail, Sock, From, Timer); + <<"HTTP/1.1 400 Bad Request\r\n", Tail/binary>> -> + parse2(400, Tail, Sock, From, Timer); + <<"HTTP/1.1 404 Not Found\r\n", Tail/binary>> -> + parse2(404, Tail, Sock, From, Timer); + <<"HTTP/1.1 500 Internal Server Error\r\n", Tail/binary>> -> + parse2(500, Tail, Sock, From, Timer); + _ -> + ok = zx_net:disconnect(Sock), + ok = erlang:cancel_timer(Timer, [{async, true}]), + gen_server:reply(From, {error, {received, Received}}) + end. + +parse2(Code, Received, Sock, From, Timer) -> + case read_headers(Sock, Received) of + {ok, Headers, Rest} -> consume(Code, Rest, Headers, Sock, From, Timer); + Error -> gen_server:reply(From, Error) + end. + + +consume(Code, Rest, Headers, Sock, From, Timer) -> + case maps:find(<<"content-length">>, Headers) of + error -> + ok = erlang:cancel_timer(Timer, [{async, true}]), + gen_server:reply(From, {error, {headers, Headers}}); + {ok, <<"0">>} -> + ok = erlang:cancel_timer(Timer, [{async, true}]), + Result = case Code =:= 200 of true -> ok; false -> {error, Code} end, + gen_server:reply(From, Result); + {ok, Size} -> + try + Length = binary_to_integer(Size), + consume2(Length, Rest, Sock, From, Timer) + catch + error:badarg -> + ok = erlang:cancel_timer(Timer, [{async, true}]), + gen_server:reply(From, {error, {headers, Headers}}) + end + end. + +consume2(Length, Received, Sock, From, Timer) -> + Size = byte_size(Received), + if + Size == Length -> + ok = erlang:cancel_timer(Timer, [{async, true}]), + ok = zx_net:disconnect(Sock), + Result = zj:decode(Received), + gen_server:reply(From, Result); + Size < Length -> + consume3(Length, Received, Sock, From, Timer); + Size > Length -> + ok = erlang:cancel_timer(Timer, [{async, true}]), + gen_server:reply(From, {error, bad_length}) + end. + +consume3(Length, Received, Sock, From, Timer) -> + ok = inet:setopts(Sock, [{active, once}]), + receive + {tcp, Sock, Bin} -> + consume2(Length, <>, Sock, From, Timer); + timeout -> + gen_server:reply(From, {error, {timeout, Received}}) + end. + + +read_headers(Socket, <<"\r">>) -> + ok = inet:setopts(Socket, [{active, once}]), + receive + {tcp, Socket, Bin} -> read_headers(Socket, <<"\r", Bin/binary>>); + timeout -> {error, timeout} + after 120000 -> {error, timeout} + end; +read_headers(_, <<"\r\n", Received/binary>>) -> + log(info, "~p Headers died at: ~p", [?LINE, Received]), + {error, headers}; +read_headers(Socket, Received) -> + read_hkey(Socket, Received, <<>>, #{}). + +read_hkey(Socket, <>, Acc, Headers) + when $A =< Char, Char =< $Z -> + read_hkey(Socket, Rest, <>, Headers); +read_hkey(Socket, <>, Acc, Headers) + when 32 =< Char, Char =< 57; + 59 =< Char, Char =< 126 -> + read_hkey(Socket, Rest, <>, Headers); +read_hkey(Socket, <<":", Rest/binary>>, Key, Headers) -> + skip_hblanks(Socket, Rest, Key, Headers); +read_hkey(_, <<"\r\n", Rest/binary>>, <<>>, Headers) -> + {ok, Headers, Rest}; +read_hkey(Socket, <<>>, Acc, Headers) -> + ok = inet:setopts(Socket, [{active, once}]), + receive + {tcp, Socket, Bin} -> read_hkey(Socket, Bin, Acc, Headers); + timeout -> {error, timeout} + after 120000 -> {error, timeout} + end; +read_hkey(_, Received, _, _) -> + log(info, "~p Headers died at: ~p", [?LINE, Received]), + {error, headers}. + +skip_hblanks(Socket, <<" ", Rest/binary>>, Key, Headers) -> + skip_hblanks(Socket, Rest, Key, Headers); +skip_hblanks(Socket, <<>>, Key, Headers) -> + ok = inet:setopts(Socket, [{active, once}]), + receive + {tcp, Socket, Bin} -> skip_hblanks(Socket, Bin, Key, Headers); + timeout -> {error, timeout} + after 120000 -> {error, timeout} + end; +skip_hblanks(_, Received = <<"\r", _/binary>>, _, _) -> + log(info, "~p Headers died at: ~p", [?LINE, Received]), + {error, headers}; +skip_hblanks(_, Received = <<"\n", _/binary>>, _, _) -> + log(info, "~p Headers died at: ~p", [?LINE, Received]), + {error, headers}; +skip_hblanks(Socket, Rest, Key, Headers) -> + read_hval(Socket, Rest, <<>>, Key, Headers). + +read_hval(_, Received = <<"\r\n", _/binary>>, <<>>, _, _) -> + log(info, "~p Headers died at: ~p", [?LINE, Received]), + {error, headers}; +read_hval(Socket, <<"\r\n", Rest/binary>>, Val, Key, Headers) -> + read_hkey(Socket, Rest, <<>>, maps:put(Key, Val, Headers)); +read_hval(Socket, <>, Acc, Key, Headers) + when 32 =< Char, Char =< 126 -> + read_hval(Socket, Rest, <>, Key, Headers); +read_hval(Socket, <<>>, Val, Key, Headers) -> + ok = inet:setopts(Socket, [{active, once}]), + receive + {tcp, Socket, Bin} -> read_hval(Socket, Bin, Val, Key, Headers); + timeout -> {error, timeout} + after 120000 -> {error, timeout} + end; +read_hval(_, Received, _, _, _) -> + log(info, "~p Headers died at: ~p", [?LINE, Received]), + {error, headers}. + + +slowly_connect(Node, {get, Path}, From, Timeout) -> + HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}], + URL = lists:flatten(url(Node, Path)), + Request = {URL, []}, + Result = + case httpc:request(get, Request, HttpOptions, []) of + {ok, {{_, 200, _}, _, JSON}} -> zj:decode(JSON); + {ok, {{_, BAD, _}, _, _}} -> {error, BAD}; + BAD -> {error, BAD} + end, + gen_server:reply(From, Result); +slowly_connect(Node, {post, Path, Payload}, From, Timeout) -> + HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}], + URL = lists:flatten(url(Node, Path)), + Request = {URL, [], "application/json", Payload}, + Result = + case httpc:request(post, Request, HttpOptions, []) of + {ok, {{_, 200, _}, _, JSON}} -> zj:decode(JSON); + {ok, {{_, BAD, _}, _, _}} -> {error, BAD}; + BAD -> {error, BAD} + end, + gen_server:reply(From, Result). + + +url({Node, Port}, Path) when is_list(Node) -> + ["https://", Node, ":", integer_to_list(Port), Path]; +url({Node, Port}, Path) when is_tuple(Node) -> + ["https://", inet:ntoa(Node), ":", integer_to_list(Port), Path]. diff --git a/src/hz_man.erl b/src/hz_man.erl new file mode 100644 index 0000000..866504e --- /dev/null +++ b/src/hz_man.erl @@ -0,0 +1,289 @@ +%%% @private +%%% Hakuzaru Request Manager for Erlang +%%% +%%% This process is responsible for remembering the configured nodes and dispatching +%%% requests to them. Request dispatch is made in a round-robin fashion with forwarded +%%% gen_server return `From' values passed to the request worker instead of being +%%% responded to directly by the manager itself (despite requests being generated as +%%% gen_server:call/3s. +%%% @end + +-module(hz_man). +-vsn("0.4.1"). +-behavior(gen_server). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("MIT"). + +%% Admin functions +-export([network_id/0, network_id/1, + tls/0, tls/1, + chain_nodes/0, chain_nodes/1, + timeout/0, timeout/1]). + +%% The whole point of this module: +-export([request/1, request/2]). + +%% gen_server goo +-export([start_link/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). + +%% TODO: Make logging more flexible +-include("$zx_include/zx_logger.hrl"). + + +%%% Type and Record Definitions + +-record(fetcher, + {pid = none :: none | pid(), + mon = none :: none | reference(), + time = none :: none | integer(), % nanosecond timestamp + node = none :: none | hz:chain_node(), + from = none :: none | gen_server:from(), + req = none :: none | binary()}). + +-record(s, + {network_id = "gm_mainnet" :: string(), + tls = false :: boolean(), + chain_nodes = {[], []} :: {[hz:chain_node()], [hz:chain_node()]}, + sticky = none :: none | hz:chain_node(), + fetchers = [] :: [#fetcher{}], + timeout = 5000 :: pos_integer()}). + + +-type state() :: #s{}. + + + +%%% Service Interface + +-spec network_id() -> Name + when Name :: hz:network_id(). + +network_id() -> + gen_server:call(?MODULE, network_id). + + +-spec network_id(Name) -> ok + when Name :: hz:network_id(). + +network_id(Name) -> + gen_server:cast(?MODULE, {network_id, Name}). + + +-spec tls() -> boolean(). + +tls() -> + gen_server:call(?MODULE, tls). + + +-spec tls(boolean()) -> ok. + +tls(Boolean) -> + gen_server:cast(?MODULE, {tls, Boolean}). + + +-spec chain_nodes() -> Used + when Used :: [hz:chain_node()]. + +chain_nodes() -> + gen_server:call(?MODULE, chain_nodes). + + +-spec chain_nodes(ToUse) -> ok + when ToUse :: [hz:chain_nodes()]. + +chain_nodes(ToUse) -> + gen_server:cast(?MODULE, {chain_nodes, ToUse}). + + +-spec timeout() -> Value + when Value :: pos_integer(). + +timeout() -> + gen_server:call(?MODULE, timeout). + + +-spec timeout(Value) -> ok + when Value :: pos_integer(). + +timeout(Value) when 0 < Value, Value =< 120000 -> + gen_server:cast(?MODULE, {timeout, Value}). + + +-spec request(Path) -> {ok, Value} | {error, Reason} + when Path :: unicode:charlist(), + Value :: map(), + Reason :: hz:chain_error(). + +request(Path) -> + gen_server:call(?MODULE, {request, {get, Path}}, infinity). + + +-spec request(Path, Data) -> {ok, Value} | {error, Reason} + when Path :: unicode:charlist(), + Data :: unicode:charlist(), + Value :: map(), + Reason :: hz:chain_error(). + +request(Path, Data) -> + gen_server:call(?MODULE, {request, {post, Path, Data}}, infinity). + + + +%%% Startup Functions + + +-spec start_link() -> Result + when Result :: {ok, pid()} + | {error, Reason :: term()}. +%% @private +%% This should only ever be called by v_clients (the service-level supervisor). + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, none, []). + + +-spec init(none) -> {ok, state()}. +%% @private +%% Called by the supervisor process to give the process a chance to perform any +%% preparatory work necessary for proper function. + +init(none) -> + ok = io:format("hz_man starting.~n"), + State = #s{}, + {ok, State}. + + + +%%% gen_server Message Handling Callbacks + + +handle_call({request, Request}, From, State) -> + NewState = do_request(Request, From, State), + {noreply, NewState}; +handle_call(network_id, _, State = #s{network_id = Name}) -> + {reply, Name, State}; +handle_call(tls, _, State = #s{tls = TLS}) -> + {reply, TLS, State}; +handle_call(chain_nodes, _, State = #s{chain_nodes = {Wait, Used}}) -> + Nodes = lists:append(Wait, Used), + {reply, Nodes, State}; +handle_call(timeout, _, State = #s{timeout = Value}) -> + {reply, Value, State}; +handle_call(Unexpected, From, State) -> + ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), + {noreply, State}. + + +handle_cast({network_id, Name}, State) -> + {noreply, State#s{network_id = Name}}; +handle_cast({tls, Boolean}, State) -> + NewState = do_tls(Boolean, State), + {noreply, NewState}; +handle_cast({chain_nodes, []}, State) -> + {noreply, State#s{chain_nodes = none}}; +handle_cast({chain_nodes, ToUse}, State) -> + {noreply, State#s{chain_nodes = {ToUse, []}}}; +handle_cast({timeout, Value}, State) -> + {noreply, State#s{timeout = Value}}; +handle_cast(Unexpected, State) -> + ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_info({'DOWN', Mon, process, PID, Info}, State) -> + NewState = handle_down(PID, Mon, Info, State), + {noreply, NewState}; +handle_info(Unexpected, State) -> + ok = log("Unexpected info: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_down(_, Mon, normal, State = #s{fetchers = Fetchers}) -> + NewFetchers = lists:keydelete(Mon, #fetcher.mon, Fetchers), + State#s{fetchers = NewFetchers}; +handle_down(PID, Mon, Info, State = #s{fetchers = Fetchers}) -> + case lists:keytake(Mon, #fetcher.mon, Fetchers) of + {value, #fetcher{time = Time, node = Node, from = From, req = R}, Remaining} -> + TS = calendar:system_time_to_rfc3339(Time, [{unit, nanosecond}]), + Format = + "ERROR ~ts: Fetcher process ~130tp exited while making request to ~130tp~n" + "Exit reason:~n" + "~tp~n" + "Request contents:~n" + "~tp~n~n", + Formatted = io_lib:format(Format, [TS, PID, Node, Info, R]), + Message = unicode:characters_to_list(Formatted), + ok = gen_server:reply(From, {error, Message}), + State#s{fetchers = Remaining}; + false -> + Unexpected = {'DOWN', Mon, process, PID, Info}, + ok = log(warning, "Unexpected info: ~w", [Unexpected]), + State + end. + + + + +%%% OTP Service Functions + +code_change(_, State, _) -> + {ok, State}. + + +terminate(_, _) -> + ok. + + + +%%% Doer Functions + +do_tls(true, State) -> + ok = ssl:start(), + State#s{tls = true}; +do_tls(false, State) -> + State#s{tls = false}; +do_tls(_, State) -> + State. + + +do_request(_, From, State = #s{chain_nodes = {[], []}}) -> + ok = gen_server:reply(From, {error, no_nodes}), + State; +do_request(Request, + From, + State = #s{tls = false, + fetchers = Fetchers, + chain_nodes = {[Node | Rest], Used}, + timeout = Timeout}) -> + Now = erlang:system_time(nanosecond), + Fetcher = fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end, + {PID, Mon} = spawn_monitor(Fetcher), + New = #fetcher{pid = PID, + mon = Mon, + time = Now, + node = Node, + from = From, + req = Request}, + State#s{fetchers = [New | Fetchers], chain_nodes = {Rest, [Node | Used]}}; +do_request(Request, + From, + State = #s{tls = true, + fetchers = Fetchers, + chain_nodes = {[Node | Rest], Used}, + timeout = Timeout}) -> + Now = erlang:system_time(nanosecond), + Fetcher = fun() -> hz_fetcher:slowly_connect(Node, Request, From, Timeout) end, + {PID, Mon} = spawn_monitor(Fetcher), + New = #fetcher{pid = PID, + mon = Mon, + time = Now, + node = Node, + from = From, + req = Request}, + State#s{fetchers = [New | Fetchers], chain_nodes = {Rest, [Node | Used]}}; +do_request(Request, From, State = #s{chain_nodes = {[], Used}}) -> + Fresh = lists:reverse(Used), + do_request(Request, From, State#s{chain_nodes = {Fresh, []}}). diff --git a/src/hz_sup.erl b/src/hz_sup.erl new file mode 100644 index 0000000..b3b2ee1 --- /dev/null +++ b/src/hz_sup.erl @@ -0,0 +1,43 @@ +%%% @private +%%% Hakuzaru Erlang Gajumaru application supervisor +%%% +%%% The very top level supervisor in the system. It only has one service branch: the +%%% "hz_man" (Hakuzaru Manager). +%%% +%%% See: http://erlang.org/doc/design_principles/applications.html +%%% See: http://zxq9.com/archives/1311 +%%% @end + +-module(hz_sup). +-vsn("0.4.1"). +-behaviour(supervisor). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0-or-later"). + +-export([start_link/0]). +-export([init/1]). + + +-spec start_link() -> {ok, pid()}. +%% @private +%% This supervisor's own start function. + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + + +-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +%% @private +%% The OTP init/1 function. + +init([]) -> + RestartStrategy = {one_for_one, 0, 60}, + Manager = {hz_man, + {hz_man, start_link, []}, + permanent, + 5000, + worker, + [hz_man]}, + Children = [Manager], + {ok, {RestartStrategy, Children}}. diff --git a/zomp.meta b/zomp.meta new file mode 100644 index 0000000..83a5dcb --- /dev/null +++ b/zomp.meta @@ -0,0 +1,25 @@ +{name,"Hakuzaru"}. +{type,app}. +{modules,[]}. +{author,"Craig Everett"}. +{prefix,"hz"}. +{desc,"Gajumaru interoperation library"}. +{package_id,{"otpr","hakuzaru",{0,1,0}}}. +{deps,[{"otpr","erl_base58",{0,1,0}}, + {"otpr","ec_utils",{1,0,0}}, + {"otpr","aebytecode",{3,2,1}}, + {"otpr","aesophia",{7,1,2}}, + {"otpr","aeserialization",{0,1,0}}, + {"otpr","zj",{1,1,0}}, + {"otpr","eblake2",{1,0,0}}, + {"otpr","getopt",{1,0,2}}]}. +{key_name,none}. +{a_email,"ceverett@tsuriai.jp"}. +{c_email,"ceverett@tsuriai.jp"}. +{copyright,"Craig Everett"}. +{file_exts,[]}. +{license,"MIT"}. +{repo_url,"https://gitlab.com/ioecs/hakuzaru"}. +{tags,["aeternity","qpq","gajumaru","blockchain","hakuzaru","crypto","ae", + "defi"]}. +{ws_url,"https://gitlab.com/ioecs/hakuzaru"}.