1131 lines
34 KiB
Erlang
1131 lines
34 KiB
Erlang
%%% @doc
|
|
%%% Clutch Controller
|
|
%%%
|
|
%%% This process is a in charge of maintaining the program's state.
|
|
%%% @end
|
|
|
|
-module(gmc_con).
|
|
-vsn("0.1.4").
|
|
-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
|
|
| gmc_v_devman.
|
|
|
|
|
|
|
|
%% Interface
|
|
|
|
|
|
-spec show_ui(Name) -> ok
|
|
when Name :: ui_name().
|
|
|
|
show_ui(Name) ->
|
|
gen_server:cast(?MODULE, {show_ui, Name}).
|
|
|
|
|
|
-spec open_wallet(Path, Phrase) -> ok
|
|
when Path :: file:filename(),
|
|
Phrase :: string().
|
|
|
|
open_wallet(Path, Phrase) ->
|
|
gen_server:cast(?MODULE, {open_wallet, Path, Phrase}).
|
|
|
|
|
|
-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;
|
|
task_data(gmc_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}) ->
|
|
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 = Current, wallets = Wallets, pass = Pass}) ->
|
|
#wallet{name = WalletName, poas = POAs, keys = Keys} = Current,
|
|
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],
|
|
Updated = Current#wallet{poas = NewPOAs, keys = NewKeys},
|
|
RW = lists:keyfind(WalletName, #wr.name, Wallets),
|
|
ok = save_wallet(RW, Pass, Updated),
|
|
ok = gmc_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 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 = Current, wallets = Wallets, pass = Pass}) ->
|
|
#wallet{name = WalletName, keys = Keys, poas = POAs} = Current,
|
|
Recovered = #key{id = ID, name = AccName} = gmc_key_master:make_key("", Seed),
|
|
case lists:keymember(ID, #key.id, Keys) of
|
|
false ->
|
|
NewKeys = [Recovered | Keys],
|
|
POA = #poa{name = AccName, id = ID},
|
|
NewPOAs = [POA | POAs],
|
|
ok = gmc_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 = 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{pass = Pass, 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").
|