diff --git a/erlang/ebin/vanillae.app b/erlang/ebin/vanillae.app index 6279e2e..cdb3716 100644 --- a/erlang/ebin/vanillae.app +++ b/erlang/ebin/vanillae.app @@ -4,6 +4,5 @@ {included_applications,[]}, {applications,[stdlib,kernel]}, {vsn,"0.1.0"}, - {modules,[v_client,v_client_man,v_client_sup,v_clients,v_sup, - vanillae]}, + {modules,[vanillae,vanillae_fetcher,vanillae_man]}, {mod,{vanillae,[]}}]}. diff --git a/erlang/src/v_client.erl b/erlang/src/v_client.erl deleted file mode 100644 index cd0c58f..0000000 --- a/erlang/src/v_client.erl +++ /dev/null @@ -1,204 +0,0 @@ -%%% @doc -%%% Vanillae for Erlang Client -%%% -%%% An extremely naive (currently Telnet) client handler. -%%% Unlike other modules that represent discrete processes, this one does not adhere -%%% to any OTP behavior. It does, however, adhere to OTP. -%%% -%%% In some cases it is more comfortable to write socket handlers or a certain -%%% category of state machines as "pure" Erlang processes. This approach is made -%%% OTP-able by use of the proc_lib module, which is the underlying library used -%%% to write the stdlib's behaviors like gen_server, gen_statem, gen_fsm, etc. -%%% -%%% http://erlang.org/doc/design_principles/spec_proc.html -%%% @end - --module(v_client). --vsn("0.1.0"). --author("Craig Everett "). --copyright("Craig Everett "). --license("MIT"). - --export([start/1]). --export([start_link/1, init/2]). --export([system_continue/3, system_terminate/4, - system_get_state/1, system_replace_state/2]). - - -%%% Type and Record Definitions - - --record(s, {socket = none :: none | gen_tcp:socket()}). - - -%% An alias for the state record above. Aliasing state can smooth out annoyances -%% that can arise from using the record directly as its own type all over the code. - --type state() :: #s{}. - - -%%% Service Interface - - --spec start(ListenSocket) -> Result - when ListenSocket :: gen_tcp:socket(), - Result :: {ok, pid()} - | {error, Reason}, - Reason :: {already_started, pid()} - | {shutdown, term()} - | term(). -%% @private -%% How the v_client_man or a prior v_client kicks things off. -%% This is called in the context of v_client_man or the prior v_client. - -start(ListenSocket) -> - v_client_sup:start_acceptor(ListenSocket). - - --spec start_link(ListenSocket) -> Result - when ListenSocket :: gen_tcp:socket(), - Result :: {ok, pid()} - | {error, Reason}, - Reason :: {already_started, pid()} - | {shutdown, term()} - | term(). -%% @private -%% This is called by the v_client_sup. While start/1 is called to iniate a startup -%% (essentially requesting a new worker be started by the supervisor), this is -%% actually called in the context of the supervisor. - -start_link(ListenSocket) -> - proc_lib:start_link(?MODULE, init, [self(), ListenSocket]). - - --spec init(Parent, ListenSocket) -> no_return() - when Parent :: pid(), - ListenSocket :: gen_tcp:socket(). -%% @private -%% This is the first code executed in the context of the new worker itself. -%% This function does not have any return value, as the startup return is -%% passed back to the supervisor by calling proc_lib:init_ack/2. -%% We see the initial form of the typical arity-3 service loop form here in the -%% call to listen/3. - -init(Parent, ListenSocket) -> - ok = io:format("~p Listening.~n", [self()]), - Debug = sys:debug_options([]), - ok = proc_lib:init_ack(Parent, {ok, self()}), - listen(Parent, Debug, ListenSocket). - - --spec listen(Parent, Debug, ListenSocket) -> no_return() - when Parent :: pid(), - Debug :: [sys:dbg_opt()], - ListenSocket :: gen_tcp:socket(). -%% @private -%% This function waits for a TCP connection. The owner of the socket is still -%% the v_client_man (so it can still close it on a call to v_client_man:ignore/0), -%% but the only one calling gen_tcp:accept/1 on it is this process. Closing the socket -%% is one way a manager process can gracefully unblock child workers that are blocking -%% on a network accept. -%% -%% Once it makes a TCP connection it will call start/1 to spawn its successor. - -listen(Parent, Debug, ListenSocket) -> - case gen_tcp:accept(ListenSocket) of - {ok, Socket} -> - {ok, _} = start(ListenSocket), - {ok, Peer} = inet:peername(Socket), - ok = io:format("~p Connection accepted from: ~p~n", [self(), Peer]), - ok = v_client_man:enroll(), - State = #s{socket = Socket}, - loop(Parent, Debug, State); - {error, closed} -> - ok = io:format("~p Retiring: Listen socket closed.~n", [self()]), - exit(normal) - end. - - --spec loop(Parent, Debug, State) -> no_return() - when Parent :: pid(), - Debug :: [sys:dbg_opt()], - State :: state(). -%% @private -%% The service loop itself. This is the service state. The process blocks on receive -%% of Erlang messages, TCP segments being received themselves as Erlang messages. - -loop(Parent, Debug, State = #s{socket = Socket}) -> - ok = inet:setopts(Socket, [{active, once}]), - receive - {tcp, Socket, <<"bye\r\n">>} -> - ok = io:format("~p Client saying goodbye. Bye!~n", [self()]), - ok = gen_tcp:send(Socket, "Bye!\r\n"), - ok = gen_tcp:shutdown(Socket, read_write), - exit(normal); - {tcp, Socket, Message} -> - ok = io:format("~p received: ~tp~n", [self(), Message]), - ok = v_client_man:echo(Message), - loop(Parent, Debug, State); - {relay, Sender, Message} when Sender == self() -> - ok = gen_tcp:send(Socket, ["Message from YOU: ", Message]), - loop(Parent, Debug, State); - {relay, Sender, Message} -> - From = io_lib:format("Message from ~tp: ", [Sender]), - ok = gen_tcp:send(Socket, [From, Message]), - loop(Parent, Debug, State); - {tcp_closed, Socket} -> - ok = io:format("~p Socket closed, retiring.~n", [self()]), - exit(normal); - {system, From, Request} -> - sys:handle_system_msg(Request, From, Parent, ?MODULE, Debug, State); - Unexpected -> - ok = io:format("~p Unexpected message: ~tp", [self(), Unexpected]), - loop(Parent, Debug, State) - end. - - --spec system_continue(Parent, Debug, State) -> no_return() - when Parent :: pid(), - Debug :: [sys:dbg_opt()], - State :: state(). -%% @private -%% The function called by the OTP internal functions after a system message has been -%% handled. If the worker process has several possible states this is one place -%% resumption of a specific state can be specified and dispatched. - -system_continue(Parent, Debug, State) -> - loop(Parent, Debug, State). - - --spec system_terminate(Reason, Parent, Debug, State) -> no_return() - when Reason :: term(), - Parent :: pid(), - Debug :: [sys:dbg_opt()], - State :: state(). -%% @private -%% Called by the OTP inner bits to allow the process to terminate gracefully. -%% Exactly when and if this is callback gets called is specified in the docs: -%% See: http://erlang.org/doc/design_principles/spec_proc.html#msg - -system_terminate(Reason, _Parent, _Debug, _State) -> - exit(Reason). - - - --spec system_get_state(State) -> {ok, State} - when State :: state(). -%% @private -%% This function allows the runtime (or anything else) to inspect the running state -%% of the worker process at any arbitrary time. - -system_get_state(State) -> {ok, State}. - - --spec system_replace_state(StateFun, State) -> {ok, NewState, State} - when StateFun :: fun(), - State :: state(), - NewState :: term(). -%% @private -%% This function allows the system to update the process state in-place. This is most -%% useful for state transitions between code types, like when performing a hot update -%% (very cool, but sort of hard) or hot patching a running system (living on the edge!). - -system_replace_state(StateFun, State) -> - {ok, StateFun(State), State}. diff --git a/erlang/src/v_client_man.erl b/erlang/src/v_client_man.erl deleted file mode 100644 index fa775c3..0000000 --- a/erlang/src/v_client_man.erl +++ /dev/null @@ -1,298 +0,0 @@ -%%% @doc -%%% Vanillae for Erlang Client Manager -%%% -%%% This is the "manager" part of the service->worker pattern. -%%% It keeps track of who is connected and can act as a router among the workers. -%%% Having this process allows us to abstract and customize service-level concepts -%%% (the high-level ideas we care about in terms of solving an external problem in the -%%% real world) and keep them separate from the lower-level details of supervision that -%%% OTP should take care of for us. -%%% @end - --module(v_client_man). --vsn("0.1.0"). --behavior(gen_server). --author("Craig Everett "). --copyright("Craig Everett "). --license("MIT"). - --export([listen/1, ignore/0]). --export([enroll/0, echo/1]). --export([start_link/0]). --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - code_change/3, terminate/2]). - - -%%% Type and Record Definitions - - --record(s, {port_num = none :: none | inet:port_number(), - listener = none :: none | gen_tcp:socket(), - clients = [] :: [pid()]}). - - --type state() :: #s{}. - - - -%%% Service Interface - - --spec listen(PortNum) -> Result - when PortNum :: inet:port_number(), - Result :: ok - | {error, Reason}, - Reason :: {listening, inet:port_number()}. -%% @doc -%% Tell the service to start listening on a given port. -%% Only one port can be listened on at a time in the current implementation, so -%% an error is returned if the service is already listening. - -listen(PortNum) -> - gen_server:call(?MODULE, {listen, PortNum}). - - --spec ignore() -> ok. -%% @doc -%% Tell the service to stop listening. -%% It is not an error to call this function when the service is not listening. - -ignore() -> - gen_server:cast(?MODULE, ignore). - - - -%%% Client Process Interface - - --spec enroll() -> ok. -%% @doc -%% Clients register here when they establish a connection. -%% Other processes can enroll as well. - -enroll() -> - gen_server:cast(?MODULE, {enroll, self()}). - - --spec echo(Message) -> ok - when Message :: string(). -%% @doc -%% The function that tells the manager to broadcast a message to all clients. -%% This can broadcast arbitrary strings to clients from non-clients as well. - -echo(Message) -> - gen_server:cast(?MODULE, {echo, Message, self()}). - - - -%%% 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("Starting.~n"), - State = #s{}, - {ok, State}. - - - -%%% gen_server Message Handling Callbacks - - --spec handle_call(Message, From, State) -> Result - when Message :: term(), - From :: {pid(), reference()}, - State :: state(), - Result :: {reply, Response, NewState} - | {noreply, State}, - Response :: ok - | {error, {listening, inet:port_number()}}, - NewState :: state(). -%% @private -%% The gen_server:handle_call/3 callback. -%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_call-3 - -handle_call({listen, PortNum}, _, State) -> - {Response, NewState} = do_listen(PortNum, State), - {reply, Response, NewState}; -handle_call(Unexpected, From, State) -> - ok = io:format("~p Unexpected call from ~tp: ~tp~n", [self(), From, Unexpected]), - {noreply, State}. - - --spec handle_cast(Message, State) -> {noreply, NewState} - when Message :: term(), - State :: state(), - NewState :: state(). -%% @private -%% The gen_server:handle_cast/2 callback. -%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_cast-2 - -handle_cast({enroll, Pid}, State) -> - NewState = do_enroll(Pid, State), - {noreply, NewState}; -handle_cast({echo, Message, Sender}, State) -> - ok = do_echo(Message, Sender, State), - {noreply, State}; -handle_cast(ignore, State) -> - NewState = do_ignore(State), - {noreply, NewState}; -handle_cast(Unexpected, State) -> - ok = io:format("~p Unexpected cast: ~tp~n", [self(), Unexpected]), - {noreply, State}. - - --spec handle_info(Message, State) -> {noreply, NewState} - when Message :: term(), - State :: state(), - NewState :: state(). -%% @private -%% The gen_server:handle_info/2 callback. -%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2 - -handle_info({'DOWN', Mon, process, Pid, Reason}, State) -> - NewState = handle_down(Mon, Pid, Reason, State), - {noreply, NewState}; -handle_info(Unexpected, State) -> - ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]), - {noreply, State}. - - - -%%% OTP Service Functions - --spec code_change(OldVersion, State, Extra) -> Result - when OldVersion :: {down, Version} | Version, - Version :: term(), - State :: state(), - Extra :: term(), - Result :: {ok, NewState} - | {error, Reason :: term()}, - NewState :: state(). -%% @private -%% The gen_server:code_change/3 callback. -%% See: http://erlang.org/doc/man/gen_server.html#Module:code_change-3 - -code_change(_, State, _) -> - {ok, State}. - - --spec terminate(Reason, State) -> no_return() - when Reason :: normal - | shutdown - | {shutdown, term()} - | term(), - State :: state(). -%% @private -%% The gen_server:terminate/2 callback. -%% See: http://erlang.org/doc/man/gen_server.html#Module:terminate-2 - -terminate(_, _) -> - ok. - - - -%%% Doer Functions - --spec do_listen(PortNum, State) -> {Result, NewState} - when PortNum :: inet:port_number(), - State :: state(), - Result :: ok - | {error, Reason :: {listening, inet:port_number()}}, - NewState :: state(). -%% @private -%% The "doer" procedure called when a "listen" message is received. - -do_listen(PortNum, State = #s{port_num = none}) -> - SocketOptions = - [inet6, - {packet, line}, - {active, once}, - {mode, binary}, - {keepalive, true}, - {reuseaddr, true}], - {ok, Listener} = gen_tcp:listen(PortNum, SocketOptions), - {ok, _} = v_client:start(Listener), - {ok, State#s{port_num = PortNum, listener = Listener}}; -do_listen(_, State = #s{port_num = PortNum}) -> - ok = io:format("~p Already listening on ~p~n", [self(), PortNum]), - {{error, {listening, PortNum}}, State}. - - --spec do_ignore(State) -> NewState - when State :: state(), - NewState :: state(). -%% @private -%% The "doer" procedure called when an "ignore" message is received. - -do_ignore(State = #s{listener = none}) -> - State; -do_ignore(State = #s{listener = Listener}) -> - ok = gen_tcp:close(Listener), - State#s{port_num = none, listener = none}. - - --spec do_enroll(Pid, State) -> NewState - when Pid :: pid(), - State :: state(), - NewState :: state(). - -do_enroll(Pid, State = #s{clients = Clients}) -> - case lists:member(Pid, Clients) of - false -> - Mon = monitor(process, Pid), - ok = io:format("Monitoring ~tp @ ~tp~n", [Pid, Mon]), - State#s{clients = [Pid | Clients]}; - true -> - State - end. - - --spec do_echo(Message, Sender, State) -> ok - when Message :: string(), - Sender :: pid(), - State :: state(). -%% @private -%% The "doer" procedure called when an "echo" message is received. - -do_echo(Message, Sender, #s{clients = Clients}) -> - Send = fun(Client) -> Client ! {relay, Sender, Message} end, - lists:foreach(Send, Clients). - - --spec handle_down(Mon, Pid, Reason, State) -> NewState - when Mon :: reference(), - Pid :: pid(), - Reason :: term(), - State :: state(), - NewState :: state(). -%% @private -%% Deal with monitors. When a new process enrolls as a client a monitor is set and -%% the process is added to the client list. When the process terminates we receive -%% a 'DOWN' message from the monitor. More sophisticated work managers typically have -%% an "unenroll" function, but this echo service doesn't need one. - -handle_down(Mon, Pid, Reason, State = #s{clients = Clients}) -> - case lists:member(Pid, Clients) of - true -> - NewClients = lists:delete(Pid, Clients), - State#s{clients = NewClients}; - false -> - Unexpected = {'DOWN', Mon, process, Pid, Reason}, - ok = io:format("~p Unexpected info: ~tp~n", [self(), Unexpected]), - State - end. diff --git a/erlang/src/v_client_sup.erl b/erlang/src/v_client_sup.erl deleted file mode 100644 index 0c0ca96..0000000 --- a/erlang/src/v_client_sup.erl +++ /dev/null @@ -1,70 +0,0 @@ -%%% @doc -%%% Vanillae for Erlang Client Supervisor -%%% -%%% This process supervises the client socket handlers themselves. It is a peer of the -%%% v_client_man (the manager interface to this network service component), -%%% and a child of the supervisor named v_clients. -%%% -%%% Because we don't know (or care) how many client connections the server may end up -%%% handling this is a simple_one_for_one supervisor which can spawn and manage as -%%% many identically defined workers as required, but cannot supervise any other types -%%% of processes (one of the tradeoffs of the "simple" in `simple_one_for_one'). -%%% -%%% http://erlang.org/doc/design_principles/sup_princ.html#id79244 -%%% @end - --module(v_client_sup). --vsn("0.1.0"). --behaviour(supervisor). --author("Craig Everett "). --copyright("Craig Everett "). --license("MIT"). - - --export([start_acceptor/1]). --export([start_link/0]). --export([init/1]). - - - --spec start_acceptor(ListenSocket) -> Result - when ListenSocket :: gen_tcp:socket(), - Result :: {ok, pid()} - | {error, Reason}, - Reason :: {already_started, pid()} - | {shutdown, term()} - | term(). -%% @private -%% Spawns the first listener at the request of the v_client_man when -%% vanillae:listen/1 is called, or the next listener at the request of the -%% currently listening v_client when a connection is made. -%% -%% Error conditions, supervision strategies and other important issues are -%% explained in the supervisor module docs: -%% http://erlang.org/doc/man/supervisor.html - -start_acceptor(ListenSocket) -> - supervisor:start_child(?MODULE, [ListenSocket]). - - --spec start_link() -> {ok, pid()}. -%% @private -%% This supervisor's own start function. - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, none). - - --spec init(none) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. -%% @private -%% The OTP init/1 function. - -init(none) -> - RestartStrategy = {simple_one_for_one, 1, 60}, - Client = {v_client, - {v_client, start_link, []}, - temporary, - brutal_kill, - worker, - [v_client]}, - {ok, {RestartStrategy, [Client]}}. diff --git a/erlang/src/v_clients.erl b/erlang/src/v_clients.erl deleted file mode 100644 index 2dce21f..0000000 --- a/erlang/src/v_clients.erl +++ /dev/null @@ -1,48 +0,0 @@ -%%% @doc -%%% Vanillae for Erlang Client Service Supervisor -%%% -%%% This is the service-level supervisor of the system. It is the parent of both the -%%% client connection handlers and the client manager (which manages the client -%%% connection handlers). This is the child of v_sup. -%%% -%%% See: http://erlang.org/doc/apps/kernel/application.html -%%% @end - --module(v_clients). --vsn("0.1.0"). --behavior(supervisor). --author("Craig Everett "). --copyright("Craig Everett "). --license("MIT"). - --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, none). - --spec init(none) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. -%% @private -%% The OTP init/1 function. - -init(none) -> - RestartStrategy = {rest_for_one, 1, 60}, - ClientMan = {v_client_man, - {v_client_man, start_link, []}, - permanent, - 5000, - worker, - [v_client_man]}, - ClientSup = {v_client_sup, - {v_client_sup, start_link, []}, - permanent, - 5000, - supervisor, - [v_client_sup]}, - Children = [ClientSup, ClientMan], - {ok, {RestartStrategy, Children}}. diff --git a/erlang/src/v_sup.erl b/erlang/src/v_sup.erl deleted file mode 100644 index d71820f..0000000 --- a/erlang/src/v_sup.erl +++ /dev/null @@ -1,46 +0,0 @@ -%%% @doc -%%% Vanillae for Erlang Top-level Supervisor -%%% -%%% The very top level supervisor in the system. It only has one service branch: the -%%% client handling service. In a more complex system the client handling service would -%%% only be one part of a larger system. Were this a game system, for example, the -%%% item data management service would be a peer, as would a login credential provision -%%% service, game world event handling, and so on. -%%% -%%% See: http://erlang.org/doc/design_principles/applications.html -%%% See: http://zxq9.com/archives/1311 -%%% @end - --module(v_sup). --vsn("0.1.0"). --behaviour(supervisor). --author("Craig Everett "). --copyright("Craig Everett "). --license("MIT"). - --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, 1, 60}, - Clients = {v_clients, - {v_clients, start_link, []}, - permanent, - 5000, - supervisor, - [v_clients]}, - Children = [Clients], - {ok, {RestartStrategy, Children}}. diff --git a/erlang/src/vanillae.erl b/erlang/src/vanillae.erl index 8e1a93b..50d64da 100644 --- a/erlang/src/vanillae.erl +++ b/erlang/src/vanillae.erl @@ -1,76 +1,1054 @@ %%% @doc -%%% Vanillae for Erlang +%%% The Vanillae Erlang Interface to Aeternity +%%% +%%% This module is the high-level interface to the Aeternity blockchain system. +%%% The interface is split into three main sections: +%%% - Get/Set admin functions +%%% - AE node JSON query interface functions +%%% - AE contract call and serialization interface functions +%%% +%%% The get/set admin functions are for setting or checking things like the Aeternity +%%% "network ID" and list of addresses of AE nodes you want to use for answering +%%% queries to the blockchain (usually you will run these nodes in your own back end). +%%% +%%% The JSON query interface functions are the blockchain query functions themselves +%%% which are translated to network queries and return Erlang messages as responses. +%%% +%%% The contract call and serialization interface are the functions used to convert +%%% a desired call to a smart contract on the chain to call data serialized in a form +%%% that an Aeternity compatible wallet, SDK or (in the case of a web service) in-page +%%% code based on a JS library such as Sidekick (another component of Vanillae) can +%%% use to generate signature requests and signed transaction objects for submission +%%% to an Aeternity network node for inclusion in the transaction mempool. +%%% +%%% This module also includes the standard OTP "application" interface and start/stop +%%% helper functions. %%% @end -module(vanillae). -vsn("0.1.0"). --behavior(application). +%-behavior(application). -author("Craig Everett "). -copyright("Craig Everett "). --license("MIT"). +-license("GPL-3.0-or-later"). --export([listen/1, ignore/0]). --export([start/0, start/1]). --export([start/2, stop/1]). +% Get/Set admin functions. +-export([network_id/0, network_id/1, + ae_nodes/0, ae_nodes/1, + timeout/0, timeout/1]). + +% AE node JSON query interface functions +-export([top_height/0, top_block/0, + kb_current/0, kb_current_hash/0, kb_current_height/0, +% kb_pending/0, + kb_by_hash/1, kb_by_height/1, +% kb_insert/1, + mb_header/1, mb_txs/1, mb_tx_index/2, mb_tx_count/1, + gen_current/0, gen_by_id/1, gen_by_height/1, + acc/1, acc_at_height/2, acc_at_block_id/2, +% acc_pending_txs/1, + next_nonce/1, + dry_run/1, dry_run/2, + 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, + prepare_contract/1, + contract_call/6, + contract_call/10]). + +% OTP Application Interface +%-export([start/0, stop/0]). +%-export([start/2, stop/1]). + + +%%% Types + +-export_type([ae_node/0, network_id/0]). + + +-type ae_node() :: {inet:ip_address(), inet:port_number()}. +-type network_id() :: string(). +-type ae_error() :: not_started + | no_nodes + | timeout + | {timeout, Received :: binary()} + | inet:posix() + | {received, binary()} + | headers + | {headers, map()} + | bad_length + | gc_out_of_range. +-type pubkey() :: string(). % "ak_" ++ _ +-type account_id() :: pubkey(). +-type contract_id() :: string(). % "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" => map()}. % FIXME +-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()}. --spec listen(PortNum) -> Result - when PortNum :: inet:port_num(), - Result :: ok - | {error, {listening, inet:port_num()}}. + +%%% Get/Set admin functions + +-spec network_id() -> NetworkID + when NetworkID :: string() | none. %% @doc -%% Make the server start listening on a port. -%% Returns an {error, Reason} tuple if it is already listening. +%% 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. -listen(PortNum) -> - v_client_man:listen(PortNum). +network_id() -> + vanillae_man:network_id(). --spec ignore() -> ok. +-spec network_id(Identifier) -> ok | {error, Reason} + when Identifier :: string() | none, + Reason :: not_started. %% @doc -%% Make the server stop listening if it is, or continue to do nothing if it isn't. +%% Sets the network ID, or returns `not_started' if the service is not yet started. -ignore() -> - v_client_man:ignore(). +network_id(Identifier) -> + vanillae_man:network_id(Identifier). --spec start() -> ok. +-spec ae_nodes() -> [ae_node()]. %% @doc -%% Start the server in an "ignore" state. +%% 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. -start() -> - ok = application:ensure_started(sasl), - ok = application:start(vanillae), - io:format("Starting..."). +ae_nodes() -> + vanillae_man:ae_nodes(). --spec start(PortNum) -> ok - when PortNum :: inet:port_number(). +-spec ae_nodes(List) -> ok | {error, Reason} + when List :: [ae_node()], + Reason :: {invalid, [term()]}. %% @doc -%% Start the server and begin listening immediately. Slightly more convenient when -%% playing around in the shell. +%% Sets the AE nodes that are intended to be used as your interface to the AE peer +%% network. The common situation is that your project runs a non-mining AE node as +%% part of your backend infrastructure. Typically one or two nodes is plenty, but +%% this may need to expand depending on how much query load your application generates. +%% The Vanillae manager will load balance by round-robin distribution. -start(PortNum) -> - ok = start(), - ok = v_client_man:listen(PortNum), - io:format("Startup complete, listening on ~w~n", [PortNum]). +ae_nodes(List) -> + vanillae_man:ae_nodes(List). --spec start(normal, term()) -> {ok, pid()}. +-spec timeout() -> Timeout + when Timeout :: pos_integer() | infinity. +%% @doc +%% Returns the current request timeout setting in milliseconds. + +timeout() -> + vanillae_man:timeout(). + + +-spec timeout(MS) -> ok + when MS :: pos_integer() | infinity. +%% @doc +%% Sets the request timeout in milliseconds. + +timeout(MS) -> + vanillae_man:timeout(MS). + + + +%%% AE node JSON query interface functions + + +-spec top_height() -> {ok, Height} | {error, Reason} + when Height :: pos_integer(), + Reason :: ae_error(). + +top_height() -> + case top_block() of + {ok, #{"micro_block" := #{"height" := Height}}} -> {ok, Height}; + {ok, #{"key_block " := #{"height" := Height}}} -> {ok, Height}; + Error -> Error + end. + + +-spec top_block() -> {ok, TopBlock} | {error, Reason} + when TopBlock :: #{Type := Block}, + Type :: string(), % "key_block" | "micro_block" + Block :: keyblock() | microblock_header(), + Reason :: ae_error(). +%% @doc +%% Returns the current block height as an integer. + +top_block() -> + request("/v2/blocks/top"). + + +-spec kb_current() -> {ok, CurrentBlock} | {error, Reason} + when CurrentBlock :: keyblock(), + Reason :: ae_error(). +%% @doc +%% Returns the current keyblock's metadata as a map. + +kb_current() -> + request("/v2/key-blocks/current"). + + +-spec kb_current_hash() -> {ok, Hash} | {error, Reason} + when Hash :: keyblock_hash(), + Reason :: ae_error(). +%% @doc +%% Returns the current keyblock's hash. +%% Equivalent of calling: +%% ``` +%% {ok, Current} = kb_current(), +%% maps:get("hash", Current), +%% ''' + +kb_current_hash() -> + case request("/v2/key-blocks/current/hash") of + {ok, #{"reason" := Reason}} -> {error, Reason}; + {ok, #{"hash" := Hash}} -> {ok, Hash}; + Error -> Error + end. + + +-spec kb_current_height() -> {ok, Height} | {error, Reason} + when Height :: pos_integer(), + Reason :: ae_error() | string(). +%% @doc +%% Returns the current keyblock's height as an integer. +%% Equivalent of calling: +%% ``` +%% {ok, Current} = kb_current(), +%% maps:get("height", Current), +%% ''' + +kb_current_height() -> + case request("/v2/key-blocks/current/height") of + {ok, #{"reason" := Reason}} -> {error, Reason}; + {ok, #{"height" := Height}} -> {ok, Height}; + Error -> Error + end. + + +%-spec kb_pending() -> +% +%kb_pending() -> +% request("/v2/key-blocks/pending"). + + +-spec kb_by_hash(ID) -> {ok, KeyBlock} | {error, Reason} + when ID :: keyblock_hash(), + KeyBlock :: keyblock(), + Reason :: ae_error() | string(). +%% @doc +%% Returns the keyblock identified by the provided hash. + +kb_by_hash(ID) -> + result(request(["/v2/key-blocks/hash/", ID])). + + +-spec kb_by_height(Height) -> {ok, KeyBlock} | {error, Reason} + when Height :: non_neg_integer(), + KeyBlock :: keyblock(), + Reason :: ae_error() | string(). +%% @doc +%% Returns the keyblock identigied by the provided height. + +kb_by_height(Height) -> + StringN = integer_to_list(Height), + result(request(["/v2/key-blocks/height/", StringN])). + + +%kb_insert(KeyblockData) -> +% request("/v2/key-blocks", KeyblockData). + + +-spec mb_header(ID) -> {ok, MB_Header} | {error, Reason} + when ID :: microblock_hash(), + MB_Header :: microblock_header(), + Reason :: ae_error() | string(). +%% @doc +%% Returns the header of the microblock indicated by the provided ID (hash). + +mb_header(ID) -> + result(request(["/v2/micro-blocks/hash/", ID, "/header"])). + + +-spec mb_txs(ID) -> {ok, TXs} | {error, Reason} + when ID :: microblock_hash(), + TXs :: [transaction()], + Reason :: ae_error() | string(). +%% @doc +%% Returns a list of transactions included in the microblock. + +mb_txs(ID) -> + case request(["/v2/micro-blocks/hash/", ID, "/transactions"]) of + {ok, #{"transactions" := TXs}} -> {ok, TXs}; + {ok, #{"reason" := Reason}} -> {error, Reason}; + Error -> Error + end. + + +-spec mb_tx_index(MicroblockID, Index) -> {ok, TX} | {error, Reason} + when MicroblockID :: microblock_hash(), + Index :: pos_integer(), + TX :: transaction(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve a single transaction from a microblock by index. +%% (Note that indexes start from 1, not zero.) + +mb_tx_index(ID, Index) -> + StrHeight = integer_to_list(Index), + result(request(["/v2/micro-blocks/hash/", ID, "/transactions/index/", StrHeight])). + + +-spec mb_tx_count(ID) -> {ok, Count} | {error, Reason} + when ID :: microblock_hash(), + Count :: non_neg_integer(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve the number of transactions contained in the indicated microblock. + +mb_tx_count(ID) -> + case request(["/v2/micro-blocks/hash/", ID, "/transactions/count"]) of + {ok, #{"count" := Count}} -> {ok, Count}; + {ok, #{"reason" := Reason}} -> {error, Reason}; + Error -> Error + end. + + +-spec gen_current() -> {ok, Generation} | {error, Reason} + when Generation :: generation(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve the generation data (keyblock and list of associated microblocks) for +%% the current generation. + +gen_current() -> + result(request("/v2/generations/current")). + + +-spec gen_by_id(ID) -> {ok, Generation} | {error, Reason} + when ID :: keyblock_hash(), + Generation :: generation(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve generation data (keyblock and list of associated microblocks) by keyhash. + +gen_by_id(ID) -> + result(request(["/v2/generations/hash/", ID])). + + +-spec gen_by_height(Height) -> {ok, Generation} | {error, Reason} + when Height :: non_neg_integer(), + Generation :: generation(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve generation data (keyblock and list of associated microblocks) by height. + +gen_by_height(Height) -> + StrHeight = integer_to_list(Height), + result(request(["/v2/generations/height/", StrHeight])). + + +-spec acc(AccountID) -> {ok, Account} | {error, Reason} + when AccountID :: account_id(), + Account :: account(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve account data by account ID (public key). + +acc(AccountID) -> + result(request(["/v2/accounts/", AccountID])). + + +-spec acc_at_height(AccountID, Height) -> {ok, Account} | {error, Reason} + when AccountID :: account_id(), + Height :: non_neg_integer(), + Account :: account(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve data for an account as that account existed at the given height. + +acc_at_height(AccountID, Height) -> + StrHeight = integer_to_list(Height), + case request(["/v2/accounts/", AccountID, "/height/", StrHeight]) of + {ok, #{"reason" := "Internal server error"}} -> {error, gc_out_of_range}; + {ok, #{"reason" := Reason}} -> {error, Reason}; + Result -> Result + end. + + +-spec acc_at_block_id(AccountID, BlockID) -> {ok, Account} | {error, Reason} + when AccountID :: account_id(), + BlockID :: keyblock_hash() | microblock_hash(), + Account :: account(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve data for an account as that account existed at the moment the given +%% block represented the current state of the chain. + +acc_at_block_id(AccountID, BlockID) -> + case request(["/v2/accounts/", AccountID, "/hash/", BlockID]) of + {ok, #{"reason" := "Internal server error"}} -> {error, gc_out_of_range}; + {ok, #{"reason" := Reason}} -> {error, Reason}; + Result -> Result + end. + + +% TODO +%-spec acc_pending_txs(AccountID) -> {ok, TXs} | {error, Reason} +% when AccountID :: account_id(), +% TXs :: +% Reason :: +%%% @doc +%%% Retrieve a list of transactions pending for the given account. +% +%acc_pending_txs(AccountID) -> +% request(["/v2/accounts/", AccountID, "/transactions/pending"]). + + +-spec next_nonce(AccountID) -> {ok, Nonce} | {error, Reason} + when AccountID :: account_id(), + Nonce :: non_neg_integer(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve the next nonce for the given account + +next_nonce(AccountID) -> + case request(["/v2/accounts/", AccountID, "/next-nonce"]) of + {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce}; + {ok, #{"reason" := "Account not found"}} -> {ok, 1}; + {ok, #{"reason" := Reason}} -> {error, Reason}; + Error -> Error + end. + + +-spec dry_run(TX) -> {ok, Result} | {error, Reason} + when TX :: binary() | string(), + Result :: term(), % FIXME + Reason :: term(). % FIXME +%% @doc +%% Execute a read-only transaction on the chain at the current height. +%% Equivalent of +%% ``` +%% {ok, Hash} = vanillae:kb_current_hash(), +%% vanilla:dry_run(TX, Hash), +%% ''' + +dry_run(TX) -> + case kb_current_hash() of + {ok, Hash} -> dry_run(TX, Hash); + Error -> Error + end. + + +-spec dry_run(TX, KBHash) -> {ok, Result} | {error, Reason} + when TX :: binary() | string(), + 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, KBHash) -> + KBB = to_binary(KBHash), + TXB = to_binary(TX), + JSON = zj:binary_encode(#{top => KBB, accounts => [], txs => [#{tx => TXB}]}), + request("/v2/dry-run", JSON). + +to_binary(S) when is_binary(S) -> S; +to_binary(S) when is_list(S) -> list_to_binary(S). + + +-spec tx(ID) -> {ok, TX} | {error, Reason} + when ID :: tx_hash(), + TX :: transaction(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve a transaction by ID. + +tx(ID) -> + request(["/v2/transactions/", ID]). + + +-spec tx_info(ID) -> {ok, Info} | {error, Reason} + when ID :: tx_hash(), + Info :: term(), % FIXME + Reason :: ae_error() | string(). +%% @doc +%% Retrieve TX metadata by ID. + +tx_info(ID) -> + result(request(["/v2/transactions/", ID, "/info"])). + + +-spec post_tx(Data) -> {ok, Result} | {error, Reason} + when Data :: term(), % FIXME + Result :: term(), % FIXME + Reason :: ae_error() | string(). +%% @doc +%% Post a transaction to the chain. + +post_tx(Data) -> + request("/v2/transactions", Data). + + +-spec contract(ID) -> {ok, ContractData} | {error, Reason} + when ID :: contract_id(), + ContractData :: contract_data(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve a contract's metadata by ID. + +contract(ID) -> + result(request(["/v2/contracts/", ID])). + + +-spec contract_code(ID) -> {ok, Bytecode} | {error, Reason} + when ID :: contract_id(), + Bytecode :: contract_byte_array(), + Reason :: ae_error() | string(). + +contract_code(ID) -> + case request(["/v2/contracts/", ID, "/code"]) of + {ok, #{"bytecode" := Bytecode}} -> {ok, Bytecode}; + {ok, #{"reason" := Reason}} -> {error, Reason}; + Error -> Error + end. + + +% FIXME: Is this broken? Seems to just stall +% -spec conract_poi(ID) -> +% +%contract_poi(ID) -> +% request(["/v2/contracts/", ID, "/poi"]). + +% TODO +%oracle(ID) -> +% request(["/v2/oracles/", ID]). + +% TODO +%oracle_queries(ID) -> +% request(["/v2/oracles/", ID, "/queries"]). + +% TODO +%oracle_queries_by_id(OracleID, QueryID) -> +% request(["/v2/oracles/", OracleID, "/queries/", QueryID]). + + +-spec name(Name) -> {ok, Info} | {error, Reason} + when Name :: string(), % _ ++ ".chain" + Info :: name_info(), + Reason :: ae_error() | string(). +%% @doc +%% Retrieve a name's chain information. + +name(Name) -> + result(request(["/v2/names/", Name])). + + +% TODO +%channel(ID) -> +% request(["/v2/channels/", ID]). + + +% FIXME: This should take a specific peer address:port otherwise it will be pointlessly +% random. +-spec peer_pubkey() -> {ok, Pubkey} | {error, Reason} + when Pubkey :: peer_pubkey(), + Reason :: term(). % FIXME +%% @doc +%% Returns the given node's public key, assuming there an AE node is reachable at +%% the given address. + +peer_pubkey() -> + case request("/v2/peers/pubkey") of + {ok, #{"pubkey" := Pubkey}} -> {ok, Pubkey}; + {ok, #{"reason" := Reason}} -> {error, Reason}; + Error -> Error + end. + + +% TODO: Make a status/1 that allows the caller to query a specific node rather than +% a random one from the pool. +-spec status() -> {ok, Status} | {error, Reason} + when Status :: status(), + Reason :: ae_error(). +%% @doc +%% Retrieve the node's status and meta it currently has about the chain. + +status() -> + request("/v2/status"). + + +% TODO +%-spec status_chainends() -> {ok, ChainEnds} | {error, Reason} +% when ChainEnds :: [keyblock_hash()], +% Reason :: ae_error(). +%%% @doc +%%% Retrieve the latest keyblock hashes +% +%status_chainends() -> +% request("/v2/status/chain-ends"). + + +request(Path) -> + vanillae_man:request(Path). + + +request(Path, Payload) -> + vanillae_man:request(Path, Payload). + + +result({ok, #{"reason" := Reason}}) -> {error, Reason}; +result(Received) -> Received. + + + +%%% Contract calls + +-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 Aeternity +%% 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 Aeternity'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, Nonce, ACI, ConID, Fun, Args) -> CallTX + when CallerID :: binary(), + Nonce :: pos_integer(), + ACI :: binary(), + ConID :: binary(), + Fun :: string(), + Args :: [string()], + CallTX :: string(). +%% @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, Nonce, ACI, ConID, Fun, Args) -> + Gas = 20000, + GasPrice = min_gas_price(), + Fee = 20000, + Amount = 0, + contract_call(CallerID, Nonce, + Gas, GasPrice, Fee, Amount, + ACI, ConID, Fun, Args). + + +-spec contract_call(CallerID, Nonce, + Gas, GasPrice, Fee, Amount, + ACI, ConID, Fun, Args) -> CallTX + when CallerID :: binary(), + Nonce :: pos_integer(), + Gas :: pos_integer(), + GasPrice :: pos_integer(), + Fee :: non_neg_integer(), + Amount :: pos_integer(), + ACI :: binary(), + ConID :: binary(), + Fun :: string(), + Args :: [string()], + CallTX :: string(). +%% @doc +%% Form a contract call using the supplied values. +%% +%% Contract call formation is a rather opaque process if you're new to Aeternity 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 binary string prefixed with <<"ak_">>. +%% The returned call will still need to be signed by the caller's private. +%%
  • +%%
  • +%% 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 Aeternity node (via `v_ejaa:next_nonce(CallerID, Node)', 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 Aeternity 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 Aeternity 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 +%% Aeternity'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 Aeternity 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 Aeternity 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 Aeternity 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, GasPrice, Fee, Amount, ACI, ConID, Fun, Args) -> + {ok, CallData} = encode_call_data(ACI, Fun, Args), + ABI = 3, + TTL = 100, + CallVersion = 1, + Type = contract_call_tx, + {account_pubkey, PK} = aeser_api_encoder:decode(CallerID), + {contract_pubkey, CK} = aeser_api_encoder:decode(ConID), + Fields = + [{caller_id, {id, account, PK}}, + {nonce, Nonce}, + {contract_id, {id, 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), + aeser_api_encoder:encode(transaction, TXB). + + +-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}} -> prepare_aaci(ACI); + Error -> Error + end. + +prepare_aaci(ACI) -> + [{NameBin, SpecDefs}] = + [{N, F} + || #{contract := #{kind := contract_main, + functions := F, + name := N}} <- ACI], + Name = binary_to_list(NameBin), + Specs = lists:foldl(fun simplify_specs/2, #{}, SpecDefs), + {aaci, Name, Specs}. + +simplify_specs(#{name := NameBin, arguments := ArgDefs}, Specs) -> + Name = binary_to_list(NameBin), + ArgTypes = lists:map(fun simplify_args/1, ArgDefs), + maps:put(Name, ArgTypes, Specs). + +simplify_args(#{name := NameBin, type := TypeBin}) -> + Name = binary_to_list(NameBin), + Type = type(TypeBin), + {Name, Type}. + +type(<<"int">>) -> integer; +type(<<"address">>) -> address; +type(<<"contract">>) -> contract; +type(<<"bool">>) -> boolean; +type(Name) -> binary_to_list(Name). +%type(#{<<"list">> := T}) -> {list, type(T)}; +%type(#{<<"tuple">> := T}) -> {tuple, type(T)}; +%type(#{<<"map">> := {K, V}} -> {map, type(K), type(V)}; +%type(<<"string">>) -> string; + +coerce({integer, S}) -> + list_to_integer(S); +coerce({address, S}) -> + {account_pubkey, Key} = aeser_api_encoder:decode(S), + {address, Key}; +coerce({contract, S}) -> + aeser_api_encoder:decode(S); +coerce({bool, S}) -> + S; +coerce({_, S}) -> + S. + + +-spec min_gas_price() -> integer(). %% @private -%% Called by OTP to kick things off. This is for the use of the "application" part of -%% OTP, not to be called by user code. -%% See: http://erlang.org/doc/apps/kernel/application.html +%% 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. -start(normal, _Args) -> - v_sup:start_link(). +min_gas_price() -> + 1000000000. --spec stop(term()) -> ok. -%% @private -%% Similar to start/2 above, this is to be called by the "application" part of OTP, -%% not client code. Causes a (hopefully graceful) shutdown of the application. +encode_call_data({aaci, _Name, FunDefs}, Fun, Args) -> + ArgDef = maps:get(Fun, FunDefs), + Binding = lists:zip([element(2, D) || D <- ArgDef], Args), + Coerced = lists:map(fun coerce/1, Binding), + aeb_fate_abi:create_calldata(Fun, Coerced). -stop(_State) -> - ok. + + +%%% Debug functionality + +% debug_network() -> +% request("/v2/debug/network"). +% +% /v2/debug/contracts/create +% /v2/debug/contracts/call +% /v2/debug/oracles/register +% /v2/debug/oracles/extend +% /v2/debug/oracles/query +% /v2/debug/oracles/respond +% /v2/debug/names/preclaim +% /v2/debug/names/claim +% /v2/debug/names/update +% /v2/debug/names/transfer +% /v2/debug/names/revoke +% /v2/debug/transactions/spend +% /v2/debug/channels/create +% /v2/debug/channels/deposit +% /v2/debug/channels/withdraw +% /v2/debug/channels/snapshot/solo +% /v2/debug/channels/set-delegates +% /v2/debug/channels/close/mutual +% /v2/debug/channels/close/solo +% /v2/debug/channels/slash +% /v2/debug/channels/settle +% /v2/debug/transactions/pending +% /v2/debug/names/commitment-id +% /v2/debug/accounts/beneficiary +% /v2/debug/accounts/node +% /v2/debug/peers +% /v2/debug/transactions/dry-run +% /v2/debug/transactions/paying-for +% /v2/debug/check-tx/pool/{hash} +% /v2/debug/token-supply/height/{height} +% /v2/debug/crash + + +%-spec start() -> ok | {error, Reason :: term()}. +% +%start() -> +% application:start(vanillae). +% +% +%-spec start(normal, term()) -> {ok, pid()}. +% +%start(normal, _Args) -> +% vanillae_sup:start_link(). +% +% +%-spec stop(term()) -> ok. +% +%stop(_State) -> +% ok. diff --git a/erlang/src/vanillae_fetcher.erl b/erlang/src/vanillae_fetcher.erl new file mode 100644 index 0000000..571de3f --- /dev/null +++ b/erlang/src/vanillae_fetcher.erl @@ -0,0 +1,208 @@ +-module(vanillae_fetcher). +-vsn("0.1.0"). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("MIT"). + +-export([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: Vanillae/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: Vanillae/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}. diff --git a/erlang/src/vanillae_man.erl b/erlang/src/vanillae_man.erl new file mode 100644 index 0000000..a090b35 --- /dev/null +++ b/erlang/src/vanillae_man.erl @@ -0,0 +1,241 @@ +%%% @doc +%%% Vanillae 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(vanillae_man). +-vsn("0.1.0"). +-behavior(gen_server). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("MIT"). + +%% Admin functions +-export([network_id/0, network_id/1, + ae_nodes/0, ae_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 | erlang:timestamp(), + node = none :: none | vanilae:ae_node(), + from = none :: none | gen_server:from(), + req = none :: none | binary()}). + +-record(s, + {network_id = "ae_mainnet" :: string(), + ae_nodes = {[], []} :: {[vanillae:ae_node()], [vanillae:ae_node()]}, + fetchers = [] :: [#fetcher{}], + timeout = 5000 :: pos_integer()}). + + +-type state() :: #s{}. + + + +%%% Service Interface + +-spec network_id() -> Name + when Name :: vanillae:network_id(). + +network_id() -> + gen_server:call(?MODULE, network_id). + + +-spec network_id(Name) -> ok + when Name :: vanillae:network_id(). + +network_id(Name) -> + gen_server:cast(?MODULE, {network_id, Name}). + + +-spec ae_nodes() -> Used + when Used :: [vanillae:ae_nodes()]. + +ae_nodes() -> + gen_server:call(?MODULE, ae_nodes). + + +-spec ae_nodes(ToUse) -> ok + when ToUse :: [vanillae:ae_nodes()]. + +ae_nodes(ToUse) -> + gen_server:cast(?MODULE, {ae_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 :: vanillae:ae_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 :: vanillae:ae_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("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(ae_nodes, _, State = #s{ae_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({ae_nodes, []}, State) -> + {noreply, State#s{ae_nodes = none}}; +handle_cast({ae_nodes, ToUse}, State) -> + {noreply, State#s{ae_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 ~s: Fetcher process ~p making request to ~p exited with ~p~n" + "Request contents:~n~n" + "~s", + 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_request(_, From, State = #s{ae_nodes = {[], []}}) -> + ok = gen_server:reply(From, {error, no_nodes}), + State; +do_request(Request, + From, + State = #s{fetchers = Fetchers, + ae_nodes = {[Node | Rest], Used}, + timeout = Timeout}) -> + Now = erlang:system_time(nanosecond), + Fetcher = fun() -> vanillae_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], ae_nodes = {Rest, [Node | Used]}}; +do_request(Request, From, State = #s{ae_nodes = {[], Used}}) -> + Fresh = lists:reverse(Used), + do_request(Request, From, State#s{ae_nodes = {Fresh, []}}).