GajuDesk/src/gd_con.erl
Craig Everett f92c5fbde0 First-run wallet creatorator (v0.7.0) (#24)
Adds a first-run wallet and account creator option for noobs.
If they select the "create a default wallet" option, they are jumped to the name + password screen for wallet creation, and a new account is generated for them named "Account 1".

This leapfrogs the problem of users having to know what is going on with the blockchain and wallet at all before getting started.
#18
#19

Reviewed-on: #24
Reviewed-by: Ulf Wiger <ulfwiger@qpq.swiss>
Co-authored-by: Craig Everett <zxq9@zxq9.com>
Co-committed-by: Craig Everett <zxq9@zxq9.com>
2025-08-07 15:47:05 +09:00

1300 lines
40 KiB
Erlang

%%% @doc
%%% GajuDesk Controller
%%% @end
-module(gd_con).
-vsn("0.7.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-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_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}).
-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_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_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" := "tx"}) ->
gd_gui:grids_mess_sig(Request);
do_grids_sig2(WTF) ->
gd_gui:trouble({trash, WTF}).
do_sign_mess(Request = #{"public_id" := ID, "payload" := Message},
#s{wallet = #wallet{keys = Keys}}) ->
case lists:keyfind(ID, #key.id, Keys) of
#key{pair = #{secret := SecKey}} ->
Sig = base64:encode(hz:sign_message(list_to_binary(Message), SecKey)),
do_sign_mess2(Request#{"signature" => Sig});
false ->
gd_gui:trouble({bad_key, ID})
end.
do_sign_mess2(Request = #{"url" := URL}) ->
ResponseKeys =
["grids",
"chain",
"network_id",
"type",
"public_id",
"payload",
"signature"],
Response = zj:encode(maps:with(ResponseKeys, Request)),
case httpc:request(post, {URL, [], "application/json", Response}, [], []) of
{ok, {{_, 200, _}, _, JSON}} -> log(info, "Signature posted: ~p", [JSON]);
{error, socket_closed_remotely} -> tell("Yep, closed remotely.");
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),
do_sign_tx2(Request#{"signed" => true, "payload" := SignedTX});
false ->
gd_gui:trouble({bad_key, ID})
end.
do_sign_tx2(Request = #{"url" := URL}) ->
ResponseKeys =
["grids",
"chain",
"network_id",
"type",
"public_id",
"payload",
"signed"],
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(<<H:32/binary, T/binary>>, 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 := <<K:32/binary, _/binary>>}} ->
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").