From e8febcf8d5b2e6542de722ed4f1931a4166deb24 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sat, 25 Oct 2025 13:45:40 +0900 Subject: [PATCH 1/3] Finally implement the "sticky" chain node --- src/hz.erl | 12 ++++++-- src/hz_man.erl | 74 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/hz.erl b/src/hz.erl index e09ad7b..787875e 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -582,7 +582,7 @@ next_nonce(AccountID) -> % {ok, #{"reason" := Reason}} -> {error, Reason}; % Error -> Error % end. - case request(["/v3/accounts/", AccountID]) of + case request_sticky(["/v3/accounts/", AccountID]) of {ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1}; {ok, #{"reason" := "Account not found"}} -> {ok, 1}; {ok, #{"reason" := Reason}} -> {error, Reason}; @@ -729,7 +729,7 @@ tx_info(ID) -> post_tx(Data) when is_binary(Data) -> JSON = zj:binary_encode(#{tx => Data}), - request("/v3/transactions", JSON); + request_sticky("/v3/transactions", JSON); post_tx(Data) when is_list(Data) -> post_tx(list_to_binary(Data)). @@ -841,6 +841,14 @@ status_chainends() -> request("/v3/status/chain-ends"). +request_sticky(Path) -> + hz_man:request_sticky(unicode:characters_to_list(Path)). + + +request_sticky(Path, Payload) -> + hz_man:request_sticky(unicode:characters_to_list(Path), Payload). + + request(Path) -> hz_man:request(unicode:characters_to_list(Path)). diff --git a/src/hz_man.erl b/src/hz_man.erl index 4e0207f..17ad352 100644 --- a/src/hz_man.erl +++ b/src/hz_man.erl @@ -21,7 +21,7 @@ timeout/0, timeout/1]). %% The whole point of this module: --export([request/1, request/2]). +-export([request_sticky/1, request_sticky/2,request/1, request/2]). %% gen_server goo -export([start_link/0]). @@ -94,6 +94,25 @@ timeout(Value) when 0 < Value, Value =< 120000 -> gen_server:cast(?MODULE, {timeout, Value}). +-spec request_sticky(Path) -> {ok, Value} | {error, Reason} + when Path :: unicode:charlist(), + Value :: map(), + Reason :: hz:chain_error(). + +request_sticky(Path) -> + gen_server:call(?MODULE, {request_sticky, {get, Path}}, infinity). + + +-spec request_sticky(Path, Data) -> {ok, Value} | {error, Reason} + when Path :: unicode:charlist(), + Data :: unicode:charlist(), + Value :: map(), + Reason :: hz:chain_error(). + +request_sticky(Path, Data) -> + gen_server:call(?MODULE, {request_sticky, {post, Path, Data}}, infinity). + + -spec request(Path) -> {ok, Value} | {error, Reason} when Path :: unicode:charlist(), Value :: map(), @@ -145,6 +164,9 @@ init(none) -> handle_call({request, Request}, From, State) -> NewState = do_request(Request, From, State), {noreply, NewState}; +handle_call({request_sticky, Request}, From, State) -> + NewState = do_request_sticky(Request, From, State), + {noreply, NewState}; handle_call(tls, _, State = #s{tls = TLS}) -> {reply, TLS, State}; handle_call(chain_nodes, _, State = #s{chain_nodes = {Wait, Used}}) -> @@ -160,10 +182,9 @@ handle_call(Unexpected, From, State) -> handle_cast({tls, Boolean}, State) -> NewState = do_tls(Boolean, State), {noreply, NewState}; -handle_cast({chain_nodes, []}, State) -> - {noreply, State#s{chain_nodes = {[], []}}}; -handle_cast({chain_nodes, ToUse}, State) -> - {noreply, State#s{chain_nodes = {ToUse, []}}}; +handle_cast({chain_nodes, List}, State) -> + NewState = do_chain_nodes(List, State), + {noreply, NewState}; handle_cast({timeout, Value}, State) -> {noreply, State#s{timeout = Value}}; handle_cast(Unexpected, State) -> @@ -218,6 +239,14 @@ terminate(_, _) -> %%% Doer Functions +do_chain_nodes([], State) -> + State#s{sticky = none, chain_nodes = {[], []}}; +do_chain_nodes(List = [Sticky], State) -> + State#s{sticky = Sticky, chain_nodes = {List, []}}; +do_chain_nodes([Sticky | List], State) -> + State#s{sticky = Sticky, chain_nodes = {List, []}}. + + do_tls(true, State) -> ok = ssl:start(), State#s{tls = true}; @@ -227,17 +256,21 @@ do_tls(_, State) -> State. -do_request(_, From, State = #s{chain_nodes = {[], []}}) -> +do_request_sticky(_, From, State = #s{sticky = none}) -> 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}) -> +do_request_sticky(Request, + From, + State = #s{tls = TLS, + fetchers = Fetchers, + sticky = Node, + timeout = Timeout}) -> Now = erlang:system_time(nanosecond), - Fetcher = fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end, + Fetcher = + case TLS of + true -> fun() -> hz_fetcher:slowly_connect(Node, Request, From, Timeout) end; + false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end + end, {PID, Mon} = spawn_monitor(Fetcher), New = #fetcher{pid = PID, mon = Mon, @@ -245,15 +278,24 @@ do_request(Request, node = Node, from = From, req = Request}, - State#s{fetchers = [New | Fetchers], chain_nodes = {Rest, [Node | Used]}}; + State#s{fetchers = [New | Fetchers]}. + + +do_request(_, From, State = #s{chain_nodes = {[], []}}) -> + ok = gen_server:reply(From, {error, no_nodes}), + State; do_request(Request, From, - State = #s{tls = true, + State = #s{tls = TLS, 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, + Fetcher = + case TLS of + true -> fun() -> hz_fetcher:slowly_connect(Node, Request, From, Timeout) end; + false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end + end, {PID, Mon} = spawn_monitor(Fetcher), New = #fetcher{pid = PID, mon = Mon, From 7252ecd40b836a03c66550703f1d68de60376a99 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sat, 25 Oct 2025 13:52:05 +0900 Subject: [PATCH 2/3] spacing --- src/hz_man.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hz_man.erl b/src/hz_man.erl index 17ad352..e9f2ceb 100644 --- a/src/hz_man.erl +++ b/src/hz_man.erl @@ -21,7 +21,7 @@ timeout/0, timeout/1]). %% The whole point of this module: --export([request_sticky/1, request_sticky/2,request/1, request/2]). +-export([request_sticky/1, request_sticky/2, request/1, request/2]). %% gen_server goo -export([start_link/0]). From c5349f57366a65264005fda6cac6daa282186185 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Wed, 29 Oct 2025 15:35:05 +0900 Subject: [PATCH 3/3] Fix silly nodes report bug --- src/: | 1332 ++++++++++++++++++++++++++++++++++++++++++++ src/hz.erl | 76 ++- src/hz_fetcher.erl | 6 +- src/hz_man.erl | 27 +- 4 files changed, 1403 insertions(+), 38 deletions(-) create mode 100644 src/: diff --git a/src/: b/src/: new file mode 100644 index 0000000..138f7f2 --- /dev/null +++ b/src/: @@ -0,0 +1,1332 @@ +%%% @doc +%%% GajuDesk Controller +%%% @end + +-module(gd_con). +-vsn("0.7.0"). +-author("Craig Everett "). +-copyright("QPQ AG "). +-license("GPL-3.0-or-later"). + +-behavior(gen_server). +-export([show_ui/1, + open_wallet/2, close_wallet/0, new_wallet/4, import_wallet/3, drop_wallet/2, + selected/1, network/0, + password/2, + refresh/0, + nonce/1, spend/1, chain/1, grids/1, + sign_mess/1, sign_binary/1, sign_tx/1, sign_call/3, dry_run/2, + deploy/3, + make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1, list_keys/0, + add_node/1, set_sole_node/1]). +-export([encrypt/2, decrypt/2]). +-export([save/2]). +-export([start_link/0, stop/0]). +-export([init/1, terminate/2, code_change/3, + handle_call/3, handle_cast/2, handle_info/2]). +-include("$zx_include/zx_logger.hrl"). + +-include("gd.hrl"). + + +%%% Type and Record Definitions + + +-record(ui, + {name = none :: none | ui_name(), + pid = none :: none | pid(), + wx = none :: none | wx:wx_object(), + mon = none :: none | reference()}). + +-record(s, + {version = 1 :: integer(), + window = none :: none | wx:wx_object(), + timer = none :: none | reference(), + tasks = [] :: [#ui{}], + selected = 0 :: non_neg_integer(), + wallet = none :: none | #wallet{}, + pass = none :: none | binary(), + prefs = #{} :: #{module() := term()}, + wallets = [] :: [#wr{}]}). + + +-type state() :: #s{}. +-type ui_name() :: gd_v_netman + | gd_v_wallman + | gd_v_devman. + + + +%% Interface + + +-spec show_ui(Name) -> ok + when Name :: ui_name(). + +show_ui(Name) -> + gen_server:call(?MODULE, {show_ui, Name}). + + +-spec open_wallet(Path, Phrase) -> Result + when Path :: file:filename(), + Phrase :: string(), + Result :: ok | {error, Reason :: term()}. + +open_wallet(Path, Phrase) -> + gen_server:call(?MODULE, {open_wallet, Path, Phrase}, 6000). + + +-spec close_wallet() -> ok. + +close_wallet() -> + gen_server:cast(?MODULE, close_wallet). + + +-spec new_wallet(Net, Name, Path, Password) -> ok + when Net :: mainnet | testnet | devnet, + Name :: string(), + Path :: string(), + Password :: string(). + +new_wallet(Net, Name, Path, Password) -> + gen_server:cast(?MODULE, {new_wallet, Net, Name, Path, Password}). + + +-spec import_wallet(Name, Path, Password) -> ok + when Name :: string(), + Path :: string(), + Password :: string(). + +import_wallet(Name, Path, Password) -> + gen_server:cast(?MODULE, {import_wallet, Name, Path, Password}). + + +-spec drop_wallet(Path, Delete) -> ok + when Path :: file:filename(), + Delete :: boolean(). + +drop_wallet(Path, Delete) -> + gen_server:cast(?MODULE, {drop_wallet, Path, Delete}). + + +-spec selected(Index) -> ok + when Index :: pos_integer() | none. + +selected(Index) -> + gen_server:cast(?MODULE, {selected, Index}). + + +-spec network() -> {ok, NetworkID} | none + when NetworkID :: binary(). + +network() -> + gen_server:call(?MODULE, network). + + +-spec password(Old, New) -> ok + when Old :: none | string(), + New :: none | string(). + +password(Old, New) -> + gen_server:cast(?MODULE, {password, Old, New}). + + +-spec refresh() -> ok. + +refresh() -> + gen_server:cast(?MODULE, refresh). + + +-spec nonce(ID) -> {ok, Nonce} | {error, Reason} + when ID :: gajudesk:id(), + Nonce :: integer(), + Reason :: term(). % FIXME + +nonce(ID) -> + gen_server:call(?MODULE, {nonce, ID}). + + +-spec spend(TX) -> ok + when TX :: #spend_tx{}. + +spend(TX) -> + gen_server:cast(?MODULE, {spend, TX}). + + +-spec chain(ID) -> ok + when ID :: string(). + +chain(ID) -> + gen_server:cast(?MODULE, {chain, ID}). + + +-spec grids(string()) -> ok. + +grids(String) -> + gen_server:cast(?MODULE, {grids, String}). + + +-spec sign_mess(Request) -> ok + when Request :: map(). + +sign_mess(Request) -> + gen_server:cast(?MODULE, {sign_mess, Request}). + + +-spec sign_binary(Request) -> ok + when Request :: map(). + +sign_binary(Request) -> + gen_server:cast(?MODULE, {sign_binary, Request}). + + +-spec sign_tx(Request) -> ok + when Request :: map(). + +sign_tx(Request) -> + gen_server:cast(?MODULE, {sign_tx, Request}). + + +-spec sign_call(ConID, PubKey, TX) -> ok + when ConID :: gajudesk:id(), + PubKey :: gajudesk:id(), + TX :: binary(). + +sign_call(ConID, PubKey, TX) -> + gen_server:cast(?MODULE, {sign_call, ConID, PubKey, TX}). + + +-spec dry_run(ConID, TX) -> ok + when ConID :: gajudesk:id(), + TX :: binary(). + +dry_run(ConID, TX) -> + gen_server:cast(?MODULE, {dry_run, ConID, TX}). + + +-spec deploy(Build, Params, InitArgs) -> Result + when Build :: map(), + Params :: {PK :: gajudesk:id(), + Nonce :: non_neg_integer(), + TTL :: pos_integer(), + GasP :: pos_integer(), + Gas :: pos_integer(), + Amount :: pos_integer()}, + InitArgs :: [Arg :: string()], + Result :: {ok, TX_Hash :: gajudesk:id()} + | {error, Reason}, + Reason :: term(). % FIXME + +deploy(Build, Params, InitArgs) -> + gen_server:cast(?MODULE, {deploy, Build, Params, InitArgs}). + + +-spec make_key(Type, Size, Name, Seed, Encoding, Transform) -> ok + when Type :: {eddsa, ed25519}, + Size :: 256, + Name :: string(), + Seed :: string(), + Encoding :: utf8 | base64 | base58 | none, + Transform :: {Algo, Yugeness}, + Algo :: sha3 | sha2 | x_or | pbkdf2, + Yugeness :: rand | non_neg_integer(). +%% @doc +%% Generate a new key. +%% The magic first two args are here because `ak_*' basically has no option other than +%% to mean a 256 bit key based on Curve 25519. When more key varieties are added to the +%% system this will change quite a lot. + +make_key({eddsa, ed25519}, 256, Name, Seed, Encoding, Transform) -> + gen_server:cast(?MODULE, {make_key, Name, Seed, Encoding, Transform}). + + +-spec recover_key(Mnemonic) -> ok + when Mnemonic :: string(). + +recover_key(Mnemonic) -> + gen_server:cast(?MODULE, {recover_key, Mnemonic}). + + +-spec mnemonic(ID) -> {ok, Mnemonic} | error + when ID :: gajudesk:id(), + Mnemonic :: string(). + +mnemonic(ID) -> + gen_server:call(?MODULE, {mnemonic, ID}). + + +-spec rename_key(ID, NewName) -> ok + when ID :: gajudesk:id(), + NewName :: string(). + +rename_key(ID, NewName) -> + gen_server:cast(?MODULE, {rename_key, ID, NewName}). + + +-spec drop_key(ID) -> ok + when ID :: gajudesk:id(). + +drop_key(ID) -> + gen_server:cast(?MODULE, {drop_key, ID}). + + +-spec list_keys() -> Result + when Result :: {ok, Selected :: non_neg_integer(), Keys :: [gajudesk:id()]} + | error. + +list_keys() -> + gen_server:call(?MODULE, list_keys). + + +-spec add_node(New) -> ok + when New :: #node{}. + +add_node(New) -> + gen_server:cast(?MODULE, {add_node, New}). + + +-spec set_sole_node(TheOneTrueNode) -> ok + when TheOneTrueNode :: #node{}. + +set_sole_node(TheOneTrueNode) -> + gen_server:cast(?MODULE, {set_sole_node, TheOneTrueNode}). + + +%%% Lifecycle functions +-spec stop() -> ok. + +stop() -> + gen_server:cast(?MODULE, stop). + + +-spec save(Module, Prefs) -> ok | {error, Reason} + when Module :: module(), + Prefs :: #{atom() := term()}, + Reason :: file:posix(). + +save(Module, Prefs) -> + gen_server:call(?MODULE, {save, Module, Prefs}). + + + +%%% Startup Functions + + +-spec start_link() -> Result + when Result :: {ok, pid()} + | {error, Reason}, + Reason :: {already_started, pid()} + | {shutdown, term()} + | term(). +%% @private +%% Called by gd_sup. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, none, []). + + +-spec init(none) -> {ok, state()}. + +init(none) -> + ok = log(info, "Starting"), + process_flag(sensitive, true), + Prefs = read_prefs(), + GUI_Prefs = maps:get(gd_gui, Prefs, #{}), + Window = gd_gui:start_link(GUI_Prefs), + Wallets = get_prefs(wallets, Prefs, []), + T = erlang:send_after(tic(), self(), tic), + State = #s{window = Window, timer = T, wallets = Wallets, prefs = Prefs}, + NewState = do_show_ui(gd_v_wallman, State), + ok = gd_v_wallman:first_run(), + {ok, NewState}. + + +read_prefs() -> + case file:consult(prefs_path()) of + {ok, Prefs} -> proplists:to_map(Prefs); + _ -> #{} + end. + + +tic() -> + 6000. + + + +%%% 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(list_keys, _, State) -> + Response = do_list_keys(State), + {reply, Response, State}; +handle_call({nonce, ID}, _, State) -> + Response = do_nonce(ID), + {reply, Response, State}; +handle_call({open_wallet, Path, Phrase}, _, State) -> + {Response, NewState} = do_open_wallet(Path, Phrase, State), + {reply, Response, NewState}; +handle_call(network, _, State) -> + Response = do_network(State), + {reply, Response, State}; +handle_call({save, Module, Prefs}, _, State) -> + NewState = do_save(Module, Prefs, State), + {reply, ok, NewState}; +handle_call({mnemonic, ID}, _, State) -> + Response = do_mnemonic(ID, State), + {reply, Response, State}; +handle_call({show_ui, Name}, _, State) -> + NewState = do_show_ui(Name, State), + {reply, ok, NewState}; +handle_call(Unexpected, From, State) -> + ok = log(warning, "Unexpected call from ~tp: ~tp~n", [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(close_wallet, State) -> + NextState = do_close_wallet(State), + ok = gd_gui:show([]), + NewState = do_show_ui(gd_v_wallman, NextState), + ok = gd_v_wallman:to_front(), + {noreply, NewState}; +handle_cast({new_wallet, Net, Name, Path, Password}, State) -> + NewState = do_new_wallet(Net, Name, Path, Password, State), + {noreply, NewState}; +handle_cast({import_wallet, Name, Path, Password}, State) -> + NewState = do_import_wallet(Name, Path, Password, State), + {noreply, NewState}; +handle_cast({drop_wallet, Path, Delete}, State) -> + NewState = do_drop_wallet(Path, Delete, State), + {noreply, NewState}; +handle_cast({selected, Index}, State) -> + NewState = State#s{selected = Index}, + {noreply, NewState}; +handle_cast({password, Old, New}, State) -> + NewState = do_password(Old, New, State), + {noreply, NewState}; +handle_cast(refresh, State) -> + NewState = do_refresh(State), + {noreply, NewState}; +handle_cast({spend, TX}, State) -> + ok = do_spend(TX, State), + {noreply, State}; +handle_cast({chain, ID}, State) -> + NewState = do_chain(ID, State), + {noreply, NewState}; +handle_cast({grids, String}, State) -> + ok = do_grids(String), + {noreply, State}; +handle_cast({sign_mess, Request}, State) -> + ok = do_sign_mess(Request, State), + {noreply, State}; +handle_cast({sign_binary, Request}, State) -> + ok = do_sign_binary(Request, State), + {noreply, State}; +handle_cast({sign_tx, Request}, State) -> + ok = do_sign_tx(Request, State), + {noreply, State}; +handle_cast({sign_call, ConID, PubKey, TX}, State) -> + ok = do_sign_call(State, ConID, PubKey, TX), + {noreply, State}; +handle_cast({dry_run, ConID, TX}, State) -> + ok = do_dry_run(ConID, TX), + {noreply, State}; +handle_cast({deploy, Build, Params, InitArgs}, State) -> + ok = do_deploy(Build, Params, InitArgs, State), + {noreply, State}; +handle_cast({make_key, Name, Seed, Encoding, Transform}, State) -> + NewState = do_make_key(Name, Seed, Encoding, Transform, State), + {noreply, NewState}; +handle_cast({recover_key, Mnemonic}, State) -> + NewState = do_recover_key(Mnemonic, State), + {noreply, NewState}; +handle_cast({rename_key, ID, NewName}, State) -> + NewState = do_rename_key(ID, NewName, State), + {noreply, NewState}; +handle_cast({drop_key, ID}, State) -> + NewState = do_drop_key(ID, State), + {noreply, NewState}; +handle_cast({add_node, New}, State) -> + NewState = do_add_node(New, State), + {noreply, NewState}; +handle_cast({set_sole_node, TheOneTrueNode}, State) -> + NewState = do_set_sole_node(TheOneTrueNode, State), + {noreply, NewState}; +handle_cast(stop, State) -> + NewState = do_stop(State), + {noreply, NewState}; +handle_cast(Unexpected, State) -> + ok = tell(warning, "Unexpected cast: ~tp~n", [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(tic, State) -> + NewState = do_tic(State), + {noreply, NewState}; +handle_info({show_ui, Name}, State) -> + NewState = do_show_ui(Name, State), + {noreply, NewState}; +handle_info({'DOWN', Mon, process, PID, Info}, State) -> + NewState = handle_down(Mon, PID, Info, State), + {noreply, NewState}; +handle_info(Unexpected, State) -> + ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_down(Mon, PID, Info, State = #s{tasks = Tasks}) -> + case lists:keytake(Mon, #ui.mon, Tasks) of + {value, #ui{}, NewTasks} -> + State#s{tasks = NewTasks}; + false -> + Unexpected = {'DOWN', Mon, process, PID, Info}, + ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), + State + end. + + + +%% @private +%% gen_server callback to handle state transformations necessary for hot +%% code updates. This template performs no transformation. + +code_change(_, State, _) -> + {ok, State}. + + +terminate(Reason, _) -> + ok = log(info, "Reason: ~p,", [Reason]), + case whereis(gmc_con) of + undefined -> + zx:stop(); + PID -> + ok = log(info, "gd_con found at: ~p", [PID]), + application:stop(gajumine) + end. + + + +%%% GUI doers + + +do_show_ui(Name, State = #s{tasks = Tasks, prefs = Prefs}) -> + case lists:keyfind(Name, #ui.name, Tasks) of + #ui{wx = Win} -> + ok = Name:to_front(Win), + State; + false -> + TaskPrefs = maps:get(Name, Prefs, #{}), + TaskData = task_data(Name, State), + Win = Name:start_link({TaskPrefs, TaskData}), + PID = wx_object:get_pid(Win), + Mon = monitor(process, PID), + UI = #ui{name = Name, pid = PID, wx = Win, mon = Mon}, + State#s{tasks = [UI | Tasks]} + end. + +task_data(gd_v_netman, #s{wallet = #wallet{nets = Nets}}) -> + Nets; +task_data(gd_v_netman, #s{wallet = none}) -> + []; +task_data(gd_v_wallman, #s{wallets = Wallets}) -> + Wallets; +task_data(gd_v_devman, #s{}) -> + []. + + + +%%% Network operations + +do_chain(_, State) -> + tell("Would be doing chain in do_chain/2 here"), + State. + + +do_add_node(New, State) -> + tell("New node: ~p", [New]), + State. + + +do_set_sole_node(TOTN = #node{external = none}, State) -> + do_set_sole_node(TOTN#node{external = 3013}, State); +do_set_sole_node(New = #node{ip = IP, external = Port}, State = #s{wallet = W, wallets = Ws, pass = Pass}) -> + ok = hz:chain_nodes([{IP, Port}]), + case ensure_hz_set(New) of + {ok, ChainID} -> + #wallet{name = Name} = W, + RW = lists:keyfind(Name, #wr.name, Ws), + Net = #net{id = ChainID, chains = [#chain{id = ChainID, nodes = [New]}]}, + NewWallet = W#wallet{chain_id = ChainID, endpoint = New, nets = [Net]}, + ok = save_wallet(RW, Pass, NewWallet), + NewState = State#s{wallet = NewWallet}, + do_refresh(NewState); + Error -> + gd_gui:trouble(Error), + State + end. + + +do_refresh(State = #s{wallet = none}) -> + State; +do_refresh(State = #s{wallet = #wallet{endpoint = none}}) -> + State; +do_refresh(State = #s{wallet = #wallet{endpoint = Node}}) -> + case ensure_hz_set(Node) of + {ok, ChainID} -> + do_refresh2(ChainID, State); + Error -> + ok = gd_gui:trouble({do_refresh, 1, Error}), + State + end. + + +do_refresh2(ChainID, State = #s{wallet = W = #wallet{poas = POAs}}) -> + CheckBalance = check_balance(ChainID), + NewPOAs = lists:map(CheckBalance, POAs), + ok = gd_gui:show(NewPOAs), + NewW = W#wallet{chain_id = ChainID, poas = NewPOAs}, + State#s{wallet = NewW}. + + +check_balance(ChainID) -> + fun(This = #poa{id = ID}) -> + Pucks = + case hz:acc(ID) of + {ok, #{"balance" := P}} -> P; + {error, "Account not found"} -> 0 + end, + Dist = [{ChainID, Pucks}], + Gaju = #balance{coin = "gaju", total = Pucks, dist = Dist}, + This#poa{balances = [Gaju]} + end. + + +ensure_hz_set(Node = #node{ip = IP, external = Port}) -> + case hz:chain_nodes() of + [{IP, Port}] -> + case hz:status() of + {ok, #{"network_id" := ChainID}} -> + ok = gd_gui:chain(ChainID, Node), + {ok, list_to_binary(ChainID)}; + {error, no_nodes} -> + ok = hz:chain_nodes([{IP, Port}]), + ensure_hz_set(Node); + Error -> + Error + end; + _ -> + ok = hz:chain_nodes([{IP, Port}]), + ensure_hz_set(Node) + end; +ensure_hz_set(none) -> + {error, no_nodes}. + + +%ensure_connected(ChainID, IP, Port) -> +% ok = hz:chain_nodes([{IP, Port}]), +% ok = hz:network_id(ChainID). + + + +%%% Chain operations + +do_grids(String) -> + case gd_grids:parse(String) of + {ok, Instruction} -> do_grids2(Instruction); + Error -> gd_gui:trouble(Error) + end. + +do_grids2({{sign, http}, URL}) -> + ok = log(info, "Making request to ~p", [URL]), + case httpc:request(URL) of + {ok, {{_, 200, _}, _, JSON}} -> do_grids_sig(JSON, URL); + {error, socket_closed_remotely} -> log(info, "Socket closed remotely."); + Error -> gd_gui:trouble(Error) + end; +do_grids2({{sign, https}, URL}) -> + ok = log(info, "Making request to ~p", [URL]), + case httpc:request(URL) of + {ok, {{_, 200, _}, _, JSON}} -> do_grids_sig(JSON, URL); + {error, socket_closed_remotely} -> log(info, "Socket closed remotely."); + Error -> gd_gui:trouble(Error) + end; +do_grids2(Instruction) -> + tell("GRIDS: ~tp", [Instruction]). + +do_grids_sig(JSON, URL) -> + ok = log(info, "Received: ~p", [JSON]), + case zj:decode(JSON) of + {ok, GRIDS} -> do_grids_sig2(GRIDS#{"url" => URL}); + Error -> gd_gui:trouble(Error) + end. + +do_grids_sig2(Request = #{"grids" := 1, "type" := "message"}) -> + gd_gui:grids_mess_sig(Request); +do_grids_sig2(Request = #{"grids" := 1, "type" := "binary"}) -> + gd_gui:grids_mess_sig(Request); +do_grids_sig2(Request = #{"grids" := 1, "type" := "tx"}) -> + gd_gui:grids_mess_sig(Request); +do_grids_sig2(WTF) -> + gd_gui:trouble({trash, WTF}). + + +do_sign_mess(Request = #{"public_id" := ID}, #s{wallet = #wallet{keys = Keys}}) -> + case lists:keyfind(ID, #key.id, Keys) of + #key{pair = #{secret := SecKey}} -> do_sign_mess2(Request, SecKey); + false -> gd_gui:trouble({bad_key, ID}) + end. + +do_sign_mess2(Request = #{"payload" := Message}, SecKey) -> + Sig = base64:encode(hz:sign_message(list_to_binary(Message), SecKey)), + SignedRequest = maps:put("signature", Sig, Request), + ResponseKeys = + ["grids", + "chain", + "network_id", + "type", + "public_id", + "payload", + "signature"], + post_grids_response(ResponseKeys, SignedRequest). + + +do_sign_binary(Request = #{"public_id" := ID}, #s{wallet = #wallet{keys = Keys}}) -> + case lists:keyfind(ID, #key.id, Keys) of + #key{pair = #{secret := SecKey}} -> do_sign_binary2(Request, SecKey); + false -> gd_gui:trouble({bad_key, ID}) + end. + +do_sign_binary2(Request = #{"payload" := Payload}, SecKey) -> + case base64_decode(Payload) of + {ok, Binary} -> + Sig = base64:encode(hz:sign_binary(Binary, SecKey)), + SignedRequest = maps:put("signature", Sig, Request), + ResponseKeys = + ["grids", + "chain", + "network_id", + "type", + "public_id", + "payload", + "signature"], + post_grids_response(ResponseKeys, SignedRequest); + Error -> + gd_gui:trouble(Error) + end. + + +do_sign_tx(Request = #{"public_id" := ID, "payload" := CallData, "network_id" := NID}, + #s{wallet = #wallet{keys = Keys}}) -> + BinNID = list_to_binary(NID), + case lists:keyfind(ID, #key.id, Keys) of + #key{pair = #{secret := SecKey}} -> + BinaryTX = list_to_binary(CallData), + SignedTX = hz:sign_tx(BinaryTX, SecKey, BinNID), + SignedRequest = Request#{"signed" => true, "payload" := SignedTX}, + ResponseKeys = + ["grids", + "chain", + "network_id", + "type", + "public_id", + "payload", + "signed"], + post_grids_response(ResponseKeys, SignedRequest); + false -> + gd_gui:trouble({bad_key, ID}) + end. + + +post_grids_response(ResponseKeys, Request = #{"url" := URL}) -> + Response = zj:encode(maps:with(ResponseKeys, Request)), + case httpc:request(post, {URL, [], "application/json", Response}, [], []) of + {ok, {{_, 200, _}, _, JSON}} -> log(info, "Signed TX posted: ~p", [JSON]); + {error, socket_closed_remotely} -> log(info, "Socket closed remotely."); + Error -> gd_gui:trouble(Error) + end. + + +do_sign_call(#s{wallet = #wallet{keys = Keys, chain_id = ChainID}}, + ConID, + PubKey, + TX) -> + #key{pair = #{secret := SecKey}} = lists:keyfind(PubKey, #key.id, Keys), + SignedTX = hz:sign_tx(TX, SecKey, ChainID), + case hz:post_tx(SignedTX) of + {ok, Data = #{"tx_hash" := TXHash}} -> + ok = tell("TX succeded with: ~p", [TXHash]), + do_sign_call2(ConID, Data); + {ok, WTF} -> + gd_v_devman:trouble({error, WTF}); + Error -> + gd_v_devman:trouble(Error) + end; +do_sign_call(_, _, _, _) -> + gd_v_devman:trouble({error, no_chain}). + +do_sign_call2(ConID, #{"tx_hash" := TXHash}) -> + case hz:tx_info(TXHash) of + {ok, CallInfo = #{"call_info" := #{"return_type" := "ok"}}} -> + gd_v_devman:call_result(ConID, CallInfo); + {error, "Tx not mined"} -> + gd_v_devman:trouble({tx_hash, TXHash}); + {ok, Reason = #{"call_info" := #{"return_type" := "revert"}}} -> + gd_v_devman:trouble({error, Reason}); + Error -> + gd_v_devman:trouble(Error) + end. + + +do_dry_run(ConID, TX) -> + case hz:dry_run(TX) of + {ok, Result} -> gd_v_devman:dryrun_result(ConID, Result); + Other -> gd_v_devmam:trouble({error, ConID, Other}) + end. + + +do_spend(#spend_tx{sender_id = SenderID, + recipient_id = RecipientID, + amount = Amount, + gas_price = GasPrice, + gas = Gas, + ttl = TTL, + nonce = Nonce, + payload = Payload}, + #s{wallet = #wallet{keys = Keys, chain_id = NetworkID}}) -> + case lists:keyfind(SenderID, #key.id, Keys) of + #key{pair = #{secret := SecKey}} -> + Outcome = hz:spend(SenderID, + SecKey, + RecipientID, + Amount, + GasPrice, + Gas, + TTL, + Nonce, + Payload, + NetworkID), + tell(info, "Outcome: ~p", [Outcome]); + false -> + log(warning, "Tried do_spend with a bad key: ~p", [SenderID]) + end. + + +do_list_keys(#s{selected = Selected, wallet = #wallet{poas = POAs}}) -> + {ok, Selected, [ID || #poa{id = ID} <- POAs]}; +do_list_keys(#s{wallet = none}) -> + error. + + +do_nonce(ID) -> + hz:next_nonce(ID). + + +do_network(#s{wallet = none}) -> + none; +do_network(#s{wallet = #wallet{chain_id = ChainID}}) -> + {ok, ChainID}. + + + +%%% State Operations + +encrypt(Pass, Binary) -> + Flags = [{encrypt, true}, {padding, pkcs_padding}], + crypto:crypto_one_time(aes_256_ecb, Pass, Binary, Flags). + + +decrypt(Pass, Binary) -> + Flags = [{encrypt, false}, {padding, pkcs_padding}], + crypto:crypto_one_time(aes_256_ecb, Pass, Binary, Flags). + + +pass(none) -> + none; +pass(Phrase) -> + crypto:hash(sha3_256, Phrase). + + +do_make_key(Name, <<>>, _, Transform, State) -> + Bin = crypto:strong_rand_bytes(32), + do_make_key2(Name, Bin, Transform, State); +do_make_key(Name, Seed, utf8, Transform, State) -> + Bin = unicode:characters_to_binary(Seed), + do_make_key2(Name, Bin, Transform, State); +do_make_key(Name, Seed, base64, Transform, State) -> + case base64_decode(Seed) of + {ok, Bin} -> + do_make_key2(Name, Bin, Transform, State); + {error, Reason} -> + ok = gd_gui:trouble({error, {base64, Reason}}), + State + end; +do_make_key(Name, Seed, base58, Transform, State) -> + case base58:check_base58(Seed) of + true -> + Bin = base58:base58_to_binary(Seed), + do_make_key2(Name, Bin, Transform, State); + false -> + ok = gd_gui:trouble({error, {base58, badarg}}), + State + end. + + +do_make_key2(_, _, _, State = #s{wallet = none}) -> + ok = gd_gui:trouble("No wallet selected!"), + NewState = do_show_ui(gd_v_wallman, State), + ok = gd_v_wallman:to_front(), + NewState; +do_make_key2(Name, Bin, Transform, + State = #s{wallet = Current, wallets = Wallets, pass = Pass}) -> + #wallet{name = WalletName, poas = POAs, keys = Keys} = Current, + T = transform(Transform), + Seed = T(Bin), + {ID, Pair} = hz_key_master:make_key(Seed), + KeyName = case Name =:= "" of true -> ID; false -> Name end, + Key = #key{name = KeyName, id = ID, pair = Pair}, + POA = #poa{name = KeyName, id = ID}, + NewKeys = [Key | Keys], + NewPOAs = [POA | POAs], + Updated = Current#wallet{poas = NewPOAs, keys = NewKeys}, + RW = lists:keyfind(WalletName, #wr.name, Wallets), + ok = save_wallet(RW, Pass, Updated), + ok = gd_gui:show(NewPOAs), + State#s{wallet = Updated}. + + +base64_decode(String) -> + try + {ok, base64:decode(String)} + catch + E:R -> {E, R} + end. + + +transform({sha3, 256}) -> + fun(D) -> crypto:hash(sha3_256, D) end; +transform({sha2, 256}) -> + fun(D) -> crypto:hash(sha256, D) end; +transform({x_or, 256}) -> + fun t_xor/1. + + +t_xor(Bin) -> t_xor(Bin, <<0:256>>). + +t_xor(<>, A) -> + t_xor(T, crypto:exor(H, A)); +t_xor(<<>>, A) -> + A; +t_xor(B, A) -> + H = <<0:(256 - bit_size(B)), B/binary>>, + crypto:exor(H, A). + + +do_recover_key(Mnemonic, State) -> + case hz_key_master:decode(Mnemonic) of + {ok, Seed} -> + do_recover_key2(Seed, State); + Error -> + ok = gd_gui:trouble(Error), + State + end. + +do_recover_key2(Seed, State = #s{wallet = Current, wallets = Wallets, pass = Pass}) -> + #wallet{name = WalletName, keys = Keys, poas = POAs} = Current, + {ID, Pair} = hz_key_master:make_key(Seed), + Recovered = #key{id = ID, name = ID, pair = Pair}, + case lists:keymember(ID, #key.id, Keys) of + false -> + NewKeys = [Recovered | Keys], + POA = #poa{name = ID, id = ID}, + NewPOAs = [POA | POAs], + ok = gd_gui:show(NewPOAs), + Updated = Current#wallet{poas = NewPOAs, keys = NewKeys}, + RW = lists:keyfind(WalletName, #wr.name, Wallets), + ok = save_wallet(RW, Pass, Updated), + State#s{wallet = Updated}; + true -> + State + end. + + +do_mnemonic(ID, #s{wallet = #wallet{keys = Keys}}) -> + case lists:keyfind(ID, #key.id, Keys) of + #key{pair = #{secret := <>}} -> + Mnemonic = hz_key_master:encode(K), + {ok, Mnemonic}; + false -> + {error, bad_key} + end. + + +do_deploy(Build, + {PubKey, Nonce, TTL, GasPrice, Gas, Amount}, + InitArgs, + #s{wallet = #wallet{keys = Keys, chain_id = ChainID}}) -> + #key{pair = #{secret := SecKey}} = lists:keyfind(PubKey, #key.id, Keys), + case hz:contract_create_built(PubKey, + Nonce, Amount, TTL, Gas, GasPrice, + Build, InitArgs) of + {ok, CreateTX} -> do_deploy2(SecKey, CreateTX, ChainID); + Error -> gd_v_devman:trouble(Error) + end. + +do_deploy2(SecKey, CreateTX, ChainID) -> + SignedTX = hz:sign_tx(CreateTX, SecKey, ChainID), + tell(info, "SignedTX: ~p", [SignedTX]), + case hz:post_tx(SignedTX) of + {ok, Data = #{"tx_hash" := TXHash}} -> + ok = tell("Contract deploy TX succeded with: ~p", [TXHash]), + do_deploy3(Data); + {ok, WTF} -> + gd_v_devman:trouble({error, WTF}); + Error -> + gd_v_devman:trouble(Error) + end. + +do_deploy3(#{"tx_hash" := TXHash}) -> + case hz:tx_info(TXHash) of + {ok, #{"call_info" := #{"return_type" := "ok", "contract_id" := ConID}}} -> + gd_v_devman:open_contract(ConID); + {error, "Tx not mined"} -> + gd_v_devman:trouble({tx_hash, TXHash}); + {ok, Reason = #{"call_info" := #{"return_type" := "revert"}}} -> + gd_v_devman:trouble({error, Reason}); + Error -> + gd_v_devman:trouble(Error) + end. + + +do_rename_key(ID, NewName, State = #s{wallet = W, wallets = Wallets, pass = Pass}) -> + #wallet{name = Name, poas = POAs, keys = Keys} = W, + RW = lists:keyfind(Name, #wr.name, Wallets), + A = lists:keyfind(ID, #poa.id, POAs), + K = lists:keyfind(ID, #key.id, Keys), + NewPOAs = lists:keystore(ID, #poa.id, POAs, A#poa{name = NewName}), + NewKeys = lists:keystore(ID, #key.id, Keys, K#key{name = NewName}), + NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys}, + ok = save_wallet(RW, Pass, NewWallet), + ok = gd_gui:show(NewPOAs), + State#s{wallet = NewWallet}. + + +do_drop_key(ID, State = #s{wallet = W, wallets = Wallets, pass = Pass}) -> + #wallet{name = Name, poas = POAs, keys = Keys} = W, + RW = lists:keyfind(Name, #wr.name, Wallets), + NewPOAs = lists:keydelete(ID, #poa.id, POAs), + NewKeys = lists:keydelete(ID, #key.id, Keys), + NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys}, + ok = save_wallet(RW, Pass, NewWallet), + ok = gd_gui:show(NewPOAs), + State#s{wallet = NewWallet}. + + +do_open_wallet(Path, Phrase, State) -> + Pass = pass(Phrase), + case read(Path, Pass) of + {ok, Recovered = #wallet{name = Name, poas = POAs, endpoint = Node}} -> + ok = gd_gui:show(POAs), + ok = gd_gui:wallet(Name), + ok = + case ensure_hz_set(Node) of + {ok, ChainID} -> gd_gui:chain(ChainID, Node); + Error -> gd_gui:trouble(Error) + end, + {ok, State#s{pass = Pass, wallet = Recovered}}; + Error -> + {Error, State} + end. + + +default_wallet(mainnet) -> + Node = #node{ip = "groot.mainnet.gajumaru.io"}, + Groot = #chain{id = <<"groot.mainnet">>, + nodes = [Node]}, + MainNet = #net{id = <<"mainnet">>, chains = [Groot]}, + #wallet{nets = [MainNet], endpoint = Node}; +default_wallet(testnet) -> + Node = #node{ip = "groot.testnet.gajumaru.io"}, + Groot = #chain{id = <<"groot.testnet">>, + nodes = [Node]}, + TestNet = #net{id = <<"testnet">>, chains = [Groot]}, + #wallet{nets = [TestNet], endpoint = Node}; +default_wallet(devnet) -> + % TODO: This accounts for the nature of devnets by defining *no* chains. + % The GUI/CON will need to manage this properly when encountered and prompt. + DevNet = #net{id = <<"devnet">>, + chains = []}, + #wallet{nets = [DevNet]}. + + +do_password(none, none, State) -> + State; +do_password(none, New, State = #s{pass = none, + wallet = #wallet{name = Name}, + wallets = Wallets}) -> + Pass = pass(New), + Selected = lists:keyfind(Name, #wr.name, Wallets), + Updated = Selected#wr{pass = true}, + NewWallets = lists:keystore(Name, #wr.name, Wallets, Updated), + State#s{wallet = pass = Pass, wallets = NewWallets}; +do_password(Old, none, State = #s{pass = Pass, + wallet = #wallet{name = Name}, + wallets = Wallets}) -> + case pass(Old) =:= Pass of + true -> + Selected = lists:keyfind(Name, #wr.name, Wallets), + Updated = Selected#wr{pass = false}, + NewWallets = lists:keystore(Name, #wr.name, Wallets, Updated), + State#s{pass = none, wallets = NewWallets}; + false -> + State + end; +do_password(Old, New, State = #s{pass = Pass}) -> + case pass(Old) =:= Pass of + true -> State#s{pass = pass(New)}; + false -> State + end. + + +do_stop(State = #s{prefs = Prefs}) -> + ok = persist(Prefs), + NewState = do_close_wallet(State), + ok = + case is_pid(whereis(gmc_con)) of + false -> zx:stop(); + true -> application:stop(gajudesk) + end, + NewState. + + +do_new_wallet(Net, Name, Path, Password, State = #s{wallets = Wallets, prefs = Prefs}) -> + case lists:keyfind(Name, #wr.name, Wallets) of + false -> + NextState = do_close_wallet(State), + Pass = pass(Password), + HasPass = Pass =/= none, + Entry = #wr{name = Name, path = Path, pass = HasPass}, + DW = #wallet{endpoint = Node} = default_wallet(Net), + New = DW#wallet{name = Name}, + ok = save_wallet(Entry, Pass, New), + ok = gd_gui:show([]), + ok = gd_gui:wallet(Name), + ok = + case ensure_hz_set(Node) of + {ok, ChainID} -> gd_gui:chain(ChainID, Node); + Error -> gd_gui:trouble(Error) + end, + NewWallets = [Entry | Wallets], + NewPrefs = put_prefs(wallets, NewWallets, Prefs), + ok = persist(NewPrefs), + NextState#s{wallet = New, pass = Pass, wallets = NewWallets, prefs = NewPrefs}; + #wr{} -> + % FIXME + % Need to provide feedback based on where this came from + State + end. + +do_import_wallet(Name, Path, Password, State = #s{wallets = Wallets}) -> + NameExists = lists:keymember(Name, #wr.name, Wallets), + PathExists = lists:keymember(Path, #wr.path, Wallets), + case {NameExists, PathExists} of + {false, false} -> + do_import_wallet2(Name, Path, Password, State); + {true, false} -> + ok = gd_gui:trouble({error, name_exists}), + State; + {false, true} -> + ok = gd_gui:trouble({error, path_exists}), + State; + {true, true} -> + ok = gd_gui:trouble("Whoa! This exact wallet already exists!"), + State + end. + +do_import_wallet2(Name, Path, Password, State = #s{wallets = Wallets, prefs = Prefs}) -> + Pass = pass(Password), + case read(Path, Pass) of + {ok, Recovered = #wallet{poas = POAs, chain_id = ChainID, endpoint = Endpoint}} -> + Imported = Recovered#wallet{name = Name}, + HasPass = Pass =/= none, + Record = #wr{name = Name, path = Path, pass = HasPass}, + NewWallets = [Record | Wallets], + NewPrefs = put_prefs(wallets, NewWallets, Prefs), + ok = save_wallet(Record, Pass, Imported), + ok = persist(NewPrefs), + ok = gd_gui:show(POAs), + ok = gd_gui:chain(ChainID, Endpoint), + ok = gd_gui:wallet(Name), + State#s{wallet = Imported, wallets = NewWallets, prefs = NewPrefs}; + Error -> + ok = gd_gui:trouble(Error), + State + end. + + +do_drop_wallet(Path, Delete, State = #s{tasks = Tasks, + wallet = Wallet, + wallets = Wallets, + prefs = Prefs}) -> + CurrentName = + case Wallet of + #wallet{name = N} -> N; + none -> none + end, + case lists:keytake(Path, #wr.path, Wallets) of + {value, #wr{name = Name}, NewWallets} -> + ok = + case Name =:= CurrentName of + true -> + ok = gd_gui:show([]), + ok = gd_gui:wallet(none), + ok = gd_gui:chain(none, none); + false -> + ok + end, + ok = maybe_clean(Delete, Path), + NewPrefs = put_prefs(wallets, NewWallets, Prefs), + ok = persist(NewPrefs), + #ui{wx = WallMan} = lists:keyfind(gd_v_wallman, #ui.name, Tasks), + ok = gd_v_wallman:show(WallMan, NewWallets), + State#s{wallets = NewWallets, prefs = NewPrefs}; + false -> + State + end. + +maybe_clean(true, Path) -> + case file:delete(Path) of + ok -> ok; + Error -> gd_gui:trouble(Error) + end; +maybe_clean(false, _) -> + ok. + + +get_prefs(K, M, D) -> + P = maps:get(?MODULE, M, #{}), + maps:get(K, P, D). + +put_prefs(K, V, M) -> + P = maps:get(?MODULE, M, #{}), + NewP = maps:put(K, V, P), + maps:put(?MODULE, NewP, M). + + +do_save(Module, Prefs, State = #s{prefs = Cached}) -> + Updated = maps:put(Module, Prefs, Cached), + ok = persist(Updated), + State#s{prefs = Updated}. + + +do_close_wallet(State = #s{wallet = none}) -> + State; +do_close_wallet(State = #s{wallet = Current, wallets = Wallets, pass = Pass}) -> + #wallet{name = Name} = Current, + RW = lists:keyfind(Name, #wr.name, Wallets), + ok = save_wallet(RW, Pass, Current), + State#s{selected = 0, pass = none, wallet = none}. + + +save_wallet(#wr{path = Path, pass = false}, none, Wallet) -> + ok = filelib:ensure_dir(Path), + file:write_file(Path, term_to_binary(Wallet)); +save_wallet(#wr{path = Path, pass = true}, Pass, Wallet) -> + ok = filelib:ensure_dir(Path), + Cipher = encrypt(Pass, term_to_binary(Wallet)), + file:write_file(Path, Cipher). + + +read(Path, none) -> + case file:read_file(Path) of + {ok, Bin} -> read2(Bin); + Error -> Error + end; +read(Path, Pass) -> + case file:read_file(Path) of + {ok, Cipher} -> + try + Bin = decrypt(Pass, Cipher), + read2(Bin) + catch + error:{error, L, "Can't finalize"} -> + ok = log(info, "Decrypt failed at ~p", [L]), + {error, bad_password}; + E:R -> + {E, R} + end; + Error -> + Error + end. + +read2(Bin) -> + case zx_lib:b_to_t(Bin) of + {ok, T} -> read3(T); + error -> {error, badarg} + end. + +read3(T) -> + case element(2, T) of + 1 -> read3(setelement(2, erlang:append_element(T, #{}), 2)); + 2 -> {ok, T}; + _ -> {error, bad_wallet} + end. + + +do_tic(State = #s{wallet = This = #wallet{poas = POAs, endpoint = Node}, selected = Selected}) when Selected > 0 -> + NewState = + case ensure_hz_set(Node) of + {ok, ChainID} -> + POA = #poa{id = ID} = lists:nth(Selected, POAs), + CheckBalance = check_balance(ChainID), + NewPOA = CheckBalance(POA), + NewPOAs = lists:keystore(ID, #poa.id, POAs, NewPOA), + ok = gd_gui:show(NewPOAs), + State#s{wallet = This#wallet{poas = POAs}}; + Error -> + ok = log(info, "Balance update on tic failed with: ~p", [Error]), + State + end, + T = erlang:send_after(tic(), self(), tic), + NewState#s{timer = T}; +do_tic(State) -> + T = erlang:send_after(tic(), self(), tic), + State#s{timer = T}. + + + +persist(Prefs) -> + Path = prefs_path(), + ok = filelib:ensure_dir(Path), + zx_lib:write_terms(Path, proplists:from_map(Prefs)). + + +prefs_path() -> + filename:join(zx_lib:path(etc, "otpr", "gajudesk"), "prefs.eterms"). diff --git a/src/hz.erl b/src/hz.erl index 787875e..829f178 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -9,7 +9,7 @@ %%% %%% The get/set admin functions are for setting or checking things like the Gajumaru %%% "network ID" and list of addresses of nodes you want to use for answering -%%% queries to the blockchain. +%%% queries to the blockchain. Get functions are arity 0, and set functions are arity 1. %%% %%% The JSON query interface functions are the blockchain query functions themselves %%% which are translated to network queries and return Erlang messages as responses. @@ -18,8 +18,8 @@ %%% a desired call to a smart contract on the chain to call data serialized in a form %%% that a Gajumaru compatible wallet or library can sign and submit to a Gajumaru node. %%% -%%% This module does not implement the OTP application behavior. -%%% helper functions. +%%% NOTE: +%%% This module does not implement the OTP application behavior. Refer to hakuzaru.erl. %%% @end -module(hz). @@ -225,7 +225,7 @@ NetworkID :: string(), Reason :: term(). %% @doc -%% Returns the network ID or the atom `none' if it is unset. +%% Returns the network ID or the atom `none' if unavailable. %% Checking this is not normally necessary, but if network ID assignment is dynamic %% in your system it may be necessary to call this before attempting to form %% call data or perform other actions on chain that require a signature. @@ -241,7 +241,9 @@ network_id() -> %% @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. +%% checking the current list in debugging. Note that the first node in the list is +%% the "sticky" node: the one that will be used for submitting transactions and +%% querying `next_nonce'. chain_nodes() -> hz_man:chain_nodes(). @@ -251,19 +253,26 @@ chain_nodes() -> when List :: [chain_node()], Reason :: {invalid, [term()]}. %% @doc -%% Sets the nodes that are intended to be used as your interface to the peer -%% network. The common situation is that your project runs a non-mining node as -%% part of your backend infrastructure. Typically one or two nodes is plenty, but -%% this may need to expand depending on how much query load your application generates. -%% The Hakuzaru manager will load balance by round-robin distribution. +%% Sets the chain nodes that will be queried whenever you communicate with the chain. %% -%% 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. +%% The common situation is that a project runs a non-mining node as part of the backend +%% infrastructure. Typically one or two nodes is plenty, but this may need to expand +%% depending on how much query load your application generates. +%% +%% There are two situations: one node, or multiple nodes. +%% +%% Single node: +%% In the case of a single node, everything passes through that one node. Duh. +%% +%% Multiple nodes: +%% In the case of multiple nodes a distinction is made between the node to which +%% transactions that update the chain state are made and to which `next_nonce' queries +%% are made, and nodes that are used for read-only queries. The node to which stateful +%% transactions are submitted is called the "sticky node". This is the first node +%% (head position) in the list of nodes submitted to the chain when `chain_nodes/1' +%% is called. If using multiple nodes but the sticky node should also be used for +%% read-only queries, submit the sticky node at the head of the list and again in +%% the tail. chain_nodes(List) when is_list(List) -> hz_man:chain_nodes(List). @@ -271,7 +280,16 @@ chain_nodes(List) when is_list(List) -> -spec tls() -> boolean(). %% @doc -%% Check whether TLS is in use. +%% Check whether TLS is in use. The typical situation is to not use TLS as nodes that +%% serve as part of the backend of an application are typically run in the same +%% backend network as the application service. When accessing chain nodes over the WAN +%% however, TLS is strongly recommended to avoid a MITM attack. +%% +%% In this version of Hakuzaru TLS is either on or off for all nodes, making a mixed +%% infrastructure complicated to support without two Hakuzaru instances. This will +%% likely become a per-node setting in the future. +%% +%% TLS defaults to `false'. tls() -> hz_man:tls(). @@ -281,6 +299,8 @@ tls() -> %% @doc %% Set TLS true or false. That's what a boolean is, by the way, `true' or `false'. %% This is a condescending comment. That means I am talking down to you. +%% +%% TLS defaults to `false'. tls(Boolean) -> hz_man:tls(Boolean). @@ -291,6 +311,8 @@ tls(Boolean) -> when Timeout :: pos_integer() | infinity. %% @doc %% Returns the current request timeout setting in milliseconds. +%% The default timeout is 5,000ms. +%% The max timeout is 120,000ms. timeout() -> hz_man:timeout(). @@ -300,6 +322,8 @@ timeout() -> when MS :: pos_integer() | infinity. %% @doc %% Sets the request timeout in milliseconds. +%% The default timeout is 5,000ms. +%% The max timeout is 120,000ms. timeout(MS) -> hz_man:timeout(MS). @@ -576,18 +600,18 @@ acc_pending_txs(AccountID) -> %% 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_sticky(["/v3/accounts/", AccountID]) of - {ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1}; + case request_sticky(["/v3/accounts/", AccountID, "/next-nonce"]) of + {ok, #{"next_nonce" := Nonce}} -> {ok, Nonce}; {ok, #{"reason" := "Account not found"}} -> {ok, 1}; {ok, #{"reason" := Reason}} -> {error, Reason}; Error -> Error end. +% case request_sticky(["/v3/accounts/", AccountID]) of +% {ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1}; +% {ok, #{"reason" := "Account not found"}} -> {ok, 1}; +% {ok, #{"reason" := Reason}} -> {error, Reason}; +% Error -> Error +% end. -spec dry_run(TX) -> {ok, Result} | {error, Reason} diff --git a/src/hz_fetcher.erl b/src/hz_fetcher.erl index 0651716..9fa30c9 100644 --- a/src/hz_fetcher.erl +++ b/src/hz_fetcher.erl @@ -4,7 +4,7 @@ -copyright("Craig Everett "). -license("MIT"). --export([connect/4, slowly_connect/4]). +-export([connect/4, connect_slowly/4]). connect(Node = {Host, Port}, Request, From, Timeout) -> @@ -206,7 +206,7 @@ read_hval(_, Received, _, _, _) -> {error, headers}. -slowly_connect(Node, {get, Path}, From, Timeout) -> +connect_slowly(Node, {get, Path}, From, Timeout) -> HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}], URL = lists:flatten(url(Node, Path)), Request = {URL, []}, @@ -217,7 +217,7 @@ slowly_connect(Node, {get, Path}, From, Timeout) -> BAD -> {error, BAD} end, gen_server:reply(From, Result); -slowly_connect(Node, {post, Path, Payload}, From, Timeout) -> +connect_slowly(Node, {post, Path, Payload}, From, Timeout) -> HttpOptions = [{connect_timeout, 3000}, {timeout, Timeout}], URL = lists:flatten(url(Node, Path)), Request = {URL, [], "application/json", Payload}, diff --git a/src/hz_man.erl b/src/hz_man.erl index e9f2ceb..16120f2 100644 --- a/src/hz_man.erl +++ b/src/hz_man.erl @@ -40,11 +40,11 @@ req = none :: none | binary()}). -record(s, - {tls = false :: boolean(), - chain_nodes = {[], []} :: {[hz:chain_node()], [hz:chain_node()]}, - sticky = none :: none | hz:chain_node(), - fetchers = [] :: [#fetcher{}], - timeout = 5000 :: pos_integer()}). + {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{}. @@ -169,8 +169,8 @@ handle_call({request_sticky, Request}, From, State) -> {noreply, NewState}; 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), +handle_call(chain_nodes, _, State) -> + Nodes = do_chain_nodes(State), {reply, Nodes, State}; handle_call(timeout, _, State = #s{timeout = Value}) -> {reply, Value, State}; @@ -239,6 +239,15 @@ terminate(_, _) -> %%% Doer Functions +do_chain_nodes(#s{sticky = none, chain_nodes = {Wait, Used}}) -> + lists:append(Wait, Used); +do_chain_nodes(#s{sticky = Sticky, chain_nodes = {Wait, Used}}) -> + case lists:append(Wait, Used) of + [Sticky] -> [Sticky]; + Nodes -> [Sticky | Nodes] + end. + + do_chain_nodes([], State) -> State#s{sticky = none, chain_nodes = {[], []}}; do_chain_nodes(List = [Sticky], State) -> @@ -268,7 +277,7 @@ do_request_sticky(Request, Now = erlang:system_time(nanosecond), Fetcher = case TLS of - true -> fun() -> hz_fetcher:slowly_connect(Node, Request, From, Timeout) end; + true -> fun() -> hz_fetcher:connect_slowly(Node, Request, From, Timeout) end; false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end end, {PID, Mon} = spawn_monitor(Fetcher), @@ -293,7 +302,7 @@ do_request(Request, Now = erlang:system_time(nanosecond), Fetcher = case TLS of - true -> fun() -> hz_fetcher:slowly_connect(Node, Request, From, Timeout) end; + true -> fun() -> hz_fetcher:connect_slowly(Node, Request, From, Timeout) end; false -> fun() -> hz_fetcher:connect(Node, Request, From, Timeout) end end, {PID, Mon} = spawn_monitor(Fetcher),