GajuDesk/src/gmc_con.erl
2024-11-13 23:28:46 +09:00

1123 lines
33 KiB
Erlang

%%% @doc
%%% Clutch Controller
%%%
%%% This process is a in charge of maintaining the program's state.
%%% @end
-module(gmc_con).
-vsn("0.1.3").
-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/3, import_wallet/3, drop_wallet/2,
password/2,
refresh/0,
nonce/1, spend/2, chain/1, grids/1, sign_mess/1, sign_tx/1,
make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1,
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("gmc.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(),
tasks = [] :: [#ui{}],
wallet = none :: none | #wallet{},
pass = none :: none | binary(),
prefs = #{} :: #{module() := term()},
wallets = [] :: [#wr{}]}).
-type state() :: #s{}.
-type ui_name() :: gmc_v_netman
| gmc_v_wallman.
%% Interface
-spec show_ui(Name) -> ok
when Name :: ui_name().
show_ui(Name) ->
gen_server:cast(?MODULE, {show_ui, Name}).
-spec open_wallet(Path, Password) -> ok
when Path :: file:filename(),
Password :: string().
open_wallet(Path, Password) ->
gen_server:cast(?MODULE, {open_wallet, Path, Password}).
-spec close_wallet() -> ok.
close_wallet() ->
gen_server:cast(?MODULE, close_wallet).
-spec new_wallet(Name, Path, Password) -> ok
when Name :: string(),
Path :: string(),
Password :: string().
new_wallet(Name, Path, Password) ->
gen_server:cast(?MODULE, {new_wallet, 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 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 :: clutch:id(),
Nonce :: integer(),
Reason :: term(). % FIXME
nonce(ID) ->
gen_server:call(?MODULE, {nonce, ID}).
-spec spend(KeyID, TX) -> ok
when KeyID :: clutch:id(),
TX :: #spend_tx{}.
spend(KeyID, TX) ->
gen_server:cast(?MODULE, {spend, KeyID, 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 make_key(Type, Size, Name, Seed, Encoding, Transform) -> ok
when Type :: {eddsa, ed25519},
Size :: 256,
Name :: string(),
Seed :: string(),
Encoding :: utf8 | base64 | base58,
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 :: clutch:id(),
Mnemonic :: string().
mnemonic(ID) ->
gen_server:call(?MODULE, {mnemonic, ID}).
-spec rename_key(ID, NewName) -> ok
when ID :: clutch:id(),
NewName :: string().
rename_key(ID, NewName) ->
gen_server:cast(?MODULE, {rename_key, ID, NewName}).
-spec drop_key(ID) -> ok
when ID :: clutch:id().
drop_key(ID) ->
gen_server:cast(?MODULE, {drop_key, ID}).
-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 gmc_sup.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, none, []).
-spec init(none) -> {ok, state()}.
init(none) ->
ok = log(info, "Starting"),
Prefs = read_prefs(),
GUI_Prefs = maps:get(gmc_gui, Prefs, #{}),
Window = gmc_gui:start_link(GUI_Prefs),
Wallets = get_prefs(wallets, Prefs, []),
State = #s{window = Window, wallets = Wallets, prefs = Prefs},
NewState = do_show_ui(gmc_v_wallman, State),
{ok, NewState}.
read_prefs() ->
case file:consult(prefs_path()) of
{ok, Prefs} -> proplists:to_map(Prefs);
_ -> #{}
end.
%%% 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({nonce, ID}, _, State) ->
Response = do_nonce(ID),
{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(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({show_ui, Name}, State) ->
NewState = do_show_ui(Name, State),
{noreply, NewState};
handle_cast({open_wallet, Path, Phrase}, State) ->
NewState = do_open_wallet(Path, Phrase, State),
{noreply, NewState};
handle_cast(close_wallet, State) ->
NewState = do_close_wallet(State),
ok = gmc_gui:show([]),
ok = do_show_ui(gmc_v_wallman, NewState),
{noreply, NewState};
handle_cast({new_wallet, Name, Path, Password}, State) ->
NewState = do_new_wallet(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({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, KeyID, TX}, State) ->
ok = do_spend(KeyID, 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({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),
ok = zx:stop(),
{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({'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(normal, _) ->
zx:stop();
terminate(Reason, State) ->
ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]),
zx:stop().
%%% 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(gmc_v_netman, #s{wallet = #wallet{nets = Nets}}) ->
Nets;
task_data(gmc_v_netman, #s{wallet = none}) ->
[];
task_data(gmc_v_wallman, #s{wallets = Wallets}) ->
Wallets.
%%% 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}) ->
ok = hz:chain_nodes([{IP, Port}]),
case ensure_hz_set(New) of
{ok, ChainID} ->
Net = #net{id = ChainID, chains = [#chain{id = ChainID, nodes = [New]}]},
NewWallet = W#wallet{chain_id = ChainID, endpoint = New, nets = [Net]},
NewState = State#s{wallet = NewWallet},
do_refresh(NewState);
Error ->
gmc_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 = gmc_gui:trouble({do_refresh, 1, Error}),
State
end.
do_refresh2(ChainID, State = #s{wallet = W = #wallet{poas = POAs}}) ->
CheckBalance =
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,
NewPOAs = lists:map(CheckBalance, POAs),
ok = gmc_gui:show(NewPOAs),
NewW = W#wallet{chain_id = ChainID, poas = NewPOAs},
State#s{wallet = NewW}.
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 = hz:network_id(ChainID),
ok = gmc_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_connected(ChainID, IP, Port) ->
% ok = hz:chain_nodes([{IP, Port}]),
% ok = hz:network_id(ChainID).
%%% Chain operations
do_grids(String) ->
case gmc_grids:parse(String) of
{ok, Instruction} -> do_grids2(Instruction);
Error -> gmc_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 -> gmc_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 -> gmc_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 -> gmc_gui:trouble(Error)
end.
do_grids_sig2(Request = #{"grids" := 1, "type" := "message"}) ->
gmc_gui:grids_mess_sig(Request);
do_grids_sig2(Request = #{"grids" := 1, "type" := "tx"}) ->
gmc_gui:grids_mess_sig(Request);
do_grids_sig2(WTF) ->
gmc_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 := PrivKey}} ->
Sig = base64:encode(sign_message(list_to_binary(Message), PrivKey)),
do_sign_mess2(Request#{"signature" => Sig});
false ->
gmc_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 -> gmc_gui:trouble(Error)
end.
% TODO: Should probably be part of Hakuzaru
sign_message(Message, PrivKey) ->
Prefix = <<"Gajumaru Signed Message:\n">>,
{ok, PSize} = vencode(byte_size(Prefix)),
{ok, MSize} = vencode(byte_size(Message)),
Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]),
{ok, Hashed} = eblake2:blake2b(32, Smashed),
ecu_eddsa:sign_detached(Hashed, PrivKey).
vencode(N) when N < 0 ->
{error, {negative_N, N}};
vencode(N) when N < 16#FD ->
{ok, <<N>>};
vencode(N) when N =< 16#FFFF ->
NBytes = eu(N, 2),
{ok, <<16#FD, NBytes/binary>>};
vencode(N) when N =< 16#FFFF_FFFF ->
NBytes = eu(N, 4),
{ok, <<16#FE, NBytes/binary>>};
vencode(N) when N < (2 bsl 64) ->
NBytes = eu(N, 8),
{ok, <<16#FF, NBytes/binary>>}.
eu(N, Size) ->
Bytes = binary:encode_unsigned(N, little),
NExtraZeros = Size - byte_size(Bytes),
ExtraZeros = << <<0>> || _ <- lists:seq(1, NExtraZeros) >>,
<<Bytes/binary, ExtraZeros/binary>>.
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 := PrivKey}} ->
BinaryTX = list_to_binary(CallData),
SignedTX = sign_tx_hash(BinaryTX, PrivKey, BinNID),
do_sign_tx2(Request#{"signed" => true, "payload" := SignedTX});
false ->
gmc_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 -> gmc_gui:trouble(Error)
end.
sign_tx_hash(Unsigned, PrivKey, NetworkID) ->
{ok, TX_Data} = aeser_api_encoder:safe_decode(transaction, Unsigned),
{ok, Hash} = eblake2:blake2b(32, TX_Data),
NetworkHash = <<NetworkID/binary, Hash/binary>>,
Signature = ecu_eddsa:sign_detached(NetworkHash, PrivKey),
SigTxType = signed_tx,
SigTxVsn = 1,
SigTemplate =
[{signatures, [binary]},
{transaction, binary}],
TX =
[{signatures, [Signature]},
{transaction, TX_Data}],
SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX),
aeser_api_encoder:encode(transaction, SignedTX).
do_spend(KeyID, TX, State = #s{wallet = #wallet{keys = Keys}}) ->
case lists:keyfind(KeyID, #key.id, Keys) of
#key{pair = #{secret := PrivKey}} ->
do_spend2(PrivKey, TX, State);
false ->
log(warning, "Tried do_spend with a bad key: ~p", [KeyID])
end.
do_spend2(PrivKey,
#spend_tx{sender_id = SenderID,
recipient_id = RecipientID,
amount = Amount,
gas_price = GasPrice,
gas = Gas,
ttl = TTL,
nonce = Nonce,
payload = Payload},
#s{wallet = #wallet{chain_id = ChainID}}) ->
Type = spend_tx,
Vsn = 1,
Fields =
[{sender_id, SenderID},
{recipient_id, RecipientID},
{amount, Amount},
{gas_price, GasPrice},
{gas, Gas},
{ttl, TTL},
{nonce, Nonce},
{payload, Payload}],
Template =
[{sender_id, id},
{recipient_id, id},
{amount, int},
{gas_price, int},
{gas, int},
{ttl, int},
{nonce, int},
{payload, binary}],
BinaryTX = aeser_chain_objects:serialize(Type, Vsn, Template, Fields),
NetworkTX = <<ChainID/binary, BinaryTX/binary>>,
Signature = ecu_eddsa:sign_detached(NetworkTX, PrivKey),
SigTxType = signed_tx,
SigTxVsn = 1,
SigTemplate =
[{signatures, [binary]},
{transaction, binary}],
TX_Data =
[{signatures, [Signature]},
{transaction, BinaryTX}],
SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data),
Encoded = aeser_api_encoder:encode(transaction, SignedTX),
Outcome = hz:post_tx(Encoded),
tell("Outcome: ~p", [Outcome]).
do_nonce(ID) ->
hz:next_nonce(ID).
%%% 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 = gmc_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 = gmc_gui:trouble({error, {base58, badarg}}),
State
end.
do_make_key2(Name, Bin, Transform, State = #s{wallet = W}) ->
#wallet{poas = POAs, keys = Keys} = W,
T = transform(Transform),
Seed = T(Bin),
Key = #key{name = KeyName, id = ID} = gmc_key_master:make_key(Name, Seed),
POA = #poa{name = KeyName, id = ID},
NewKeys = [Key | Keys],
NewPOAs = [POA | POAs],
NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys},
ok = gmc_gui:show(NewPOAs),
State#s{wallet = NewWallet}.
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 gmc_key_master:decode(Mnemonic) of
{ok, Seed} ->
do_recover_key2(Seed, State);
Error ->
ok = gmc_gui:trouble(Error),
State
end.
do_recover_key2(Seed, State = #s{wallet = W}) ->
#wallet{keys = Keys, poas = POAs} = W,
Recovered = #key{id = ID, name = Name} = gmc_key_master:make_key("", Seed),
case lists:keymember(ID, #key.id, Keys) of
false ->
NewKeys = [Recovered | Keys],
POA = #poa{name = Name, id = ID},
NewPOAs = [POA | POAs],
ok = gmc_gui:show(NewPOAs),
NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys},
State#s{wallet = NewWallet};
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 = gmc_key_master:encode(K),
{ok, Mnemonic};
false ->
{error, bad_key}
end.
do_rename_key(ID, NewName, State = #s{wallet = W}) ->
#wallet{poas = POAs, keys = Keys} = W,
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 = gmc_gui:show(NewPOAs),
State#s{wallet = NewWallet}.
do_drop_key(ID, State = #s{wallet = W}) ->
#wallet{poas = POAs, keys = Keys} = W,
NewPOAs = lists:keydelete(ID, #poa.id, POAs),
NewKeys = lists:keydelete(ID, #key.id, Keys),
NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys},
ok = gmc_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 = gmc_gui:show(POAs),
ok = gmc_gui:wallet(Name),
ok =
case ensure_hz_set(Node) of
{ok, ChainID} -> gmc_gui:chain(ChainID, Node);
Error -> gmc_gui:trouble(Error)
end,
State#s{wallet = Recovered};
Error ->
ok = gmc_gui:trouble(Error),
New = default_wallet(),
State#s{wallet = New}
end.
default_wallet() ->
DevNet = #net{id = <<"devnet">>, chains = [#chain{}]},
% TestChain1 = #chain{id = <<"groot.testnet">>,
% nodes = [#node{ip = {1,2,3,4}}, #node{ip = {5,6,7,8}}]},
% TestChain2 = #chain{id = <<"test_ac.testnet">>,
% nodes = [#node{ip = {11,12,13,14}}, #node{ip = {15,16,17,18}}]},
% TestNet = #net{id = <<"testnet">>, chains = [TestChain1, TestChain2]},
#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),
do_close_wallet(State).
do_new_wallet(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},
New = #wallet{name = Name},
ok = save_wallet(Entry, Pass, New),
ok = gmc_gui:show([]),
ok = gmc_gui:wallet(Name),
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 = gmc_gui:trouble({error, name_exists}),
State;
{false, true} ->
ok = gmc_gui:trouble({error, path_exists}),
State;
{true, true} ->
ok = gmc_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 = gmc_gui:show(POAs),
ok = gmc_gui:chain(ChainID, Endpoint),
ok = gmc_gui:wallet(Name),
State#s{wallet = Imported, wallets = NewWallets, prefs = NewPrefs};
Error ->
ok = gmc_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 = gmc_gui:show([]),
ok = gmc_gui:wallet(none),
ok = gmc_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(gmc_v_wallman, #ui.name, Tasks),
ok = gmc_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 -> gmc_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{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 -> {ok, T};
_ -> {error, bad_wallet}
end.
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", "clutch"), "prefs.eterms").