433 lines
12 KiB
Erlang
433 lines
12 KiB
Erlang
%%% @doc
|
|
%%% GajuMine Controller
|
|
%%% @end
|
|
|
|
-module(gmc_con).
|
|
-vsn("0.1.0").
|
|
-author("Craig Everett <craigeverett@qpq.swiss>").
|
|
-copyright("Craig Everett <craigeverett@qpq.swiss>").
|
|
-license("GPL-3.0-or-later").
|
|
|
|
-behavior(gen_server).
|
|
-export([start_stop/0]).
|
|
-export([unlock/1, make_key/2, load_key/3, end_setup/0, bin_dir/0]).
|
|
-export([network/0]).
|
|
-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("$gajudesk_include/gd.hrl").
|
|
|
|
|
|
%%% Type and Record Definitions
|
|
|
|
|
|
-record(s,
|
|
{version = 1 :: integer(),
|
|
window = none :: none | wx:wx_object(),
|
|
network = <<"testnet">> :: binary(), % <<"testnet">> | <<"mainnet">>
|
|
exec_dir = platform_dir() :: file:filename(),
|
|
key = none :: none | {blob, binary()} | #key{},
|
|
pass = none :: none | binary()}).
|
|
|
|
-type state() :: #s{}.
|
|
|
|
|
|
|
|
%% Interface
|
|
|
|
-spec start_stop() -> ok.
|
|
|
|
start_stop() ->
|
|
gen_server:cast(?MODULE, start_stop).
|
|
|
|
|
|
-spec unlock(Phrase) -> ok
|
|
when Phrase :: string().
|
|
|
|
unlock(Phrase) ->
|
|
gen_server:cast(?MODULE, {unlock, Phrase}).
|
|
|
|
|
|
-spec make_key(Phrase, Network) -> ok
|
|
when Phrase :: string(),
|
|
Network :: mainnet | testnet.
|
|
|
|
make_key(Phrase, Network) ->
|
|
gen_server:cast(?MODULE, {make_key, Phrase, Network}).
|
|
|
|
|
|
-spec load_key(Phrase, Mnemonic, Network) -> ok
|
|
when Phrase :: string(),
|
|
Mnemonic :: string(),
|
|
Network :: mainnet | testnet.
|
|
|
|
load_key(Phrase, Mnemonic, Network) ->
|
|
gen_server:cast(?MODULE, {load_key, Phrase, Mnemonic, Network}).
|
|
|
|
|
|
-spec end_setup() -> ok.
|
|
|
|
end_setup() ->
|
|
gen_server:call(?MODULE, end_setup).
|
|
|
|
|
|
-spec bin_dir() -> file:filename().
|
|
|
|
bin_dir() ->
|
|
gen_server:call(?MODULE, bin_dir).
|
|
|
|
|
|
-spec network() -> mainnet | testnet | none.
|
|
|
|
network() ->
|
|
gen_server:call(?MODULE, network).
|
|
|
|
|
|
-spec stop() -> ok.
|
|
|
|
stop() ->
|
|
gen_server:cast(?MODULE, stop).
|
|
|
|
|
|
|
|
%%% 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"),
|
|
_ = process_flag(sensitive, true),
|
|
case open_wallet() of
|
|
{blob, Binary} ->
|
|
NewState = start_gui(#s{key = {blob, Binary}}),
|
|
ok = gmc_gui:ask_passphrase(),
|
|
{ok, NewState};
|
|
{ok, Wallet, NetworkID} ->
|
|
NewState = start_gui(#s{key = Wallet, network = NetworkID}),
|
|
{ok, NewState};
|
|
error ->
|
|
Window = gmc_setup:start_link(#{}),
|
|
ok = log(info, "Window: ~p", [Window]),
|
|
State = #s{window = Window},
|
|
{ok, State}
|
|
end.
|
|
|
|
open_wallet() ->
|
|
GDPrefs = gd_read_prefs(),
|
|
Wallets = gd_get_prefs(wallets, GDPrefs, []),
|
|
case lists:keyfind(gm_name(), #wr.name, Wallets) of
|
|
#wr{path = Path, pass = true} ->
|
|
case file:read_file(Path) of
|
|
{ok, Binary} -> {blob, Binary};
|
|
Error -> Error
|
|
end;
|
|
#wr{path = Path, pass = false} ->
|
|
case file:read_file(Path) of
|
|
{ok, Binary} ->
|
|
% TODO: Comply with GD's wallet upgrade system
|
|
{ok, #wallet{keys = Keys, nets = [#net{id = NetID}]}} = zx_lib:b_to_t(Binary),
|
|
Key = lists:keyfind(gm_name(), #key.name, Keys),
|
|
{ok, Key, NetID};
|
|
Error ->
|
|
Error
|
|
end;
|
|
false ->
|
|
error
|
|
end.
|
|
|
|
platform_dir() ->
|
|
#{deps := Deps} = zx_daemon:meta(),
|
|
AppID = lists:keyfind("cuckoo_cpu", 2, Deps),
|
|
Priv = filename:join(zx_lib:ppath(lib, AppID), "priv"),
|
|
Dir =
|
|
case os:type() of
|
|
{unix, linux} ->
|
|
"linux_x86_64";
|
|
{unix, darwin} ->
|
|
% TODO: Check M2 vs x86
|
|
"mac_m2";
|
|
{win32, nt} ->
|
|
"win_x86_64"
|
|
end,
|
|
filename:join(Priv, Dir).
|
|
|
|
|
|
start_gui(State = #s{key = #key{id = ID}}) ->
|
|
Window = gmc_gui:start_link(#{}),
|
|
ok = gmc_gui:set_account(ID),
|
|
State#s{window = Window};
|
|
start_gui(State) ->
|
|
Window = gmc_gui:start_link(#{}),
|
|
State#s{window = Window}.
|
|
|
|
|
|
%%% gen_server Message Handling Callbacks
|
|
|
|
|
|
handle_call(end_setup, _, State) ->
|
|
NewState = end_setup(State),
|
|
{reply, ok, NewState};
|
|
handle_call(network, _, State = #s{network = Network}) ->
|
|
{reply, Network, State};
|
|
handle_call(bin_dir, _, State = #s{exec_dir = BinDir}) ->
|
|
{reply, BinDir, State};
|
|
handle_call(Unexpected, From, State) ->
|
|
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
handle_cast({unlock, Phrase}, State) ->
|
|
NewState = unlock(Phrase, State),
|
|
{noreply, NewState};
|
|
handle_cast({make_key, Phrase, Network}, State) ->
|
|
NewState = make_key(Phrase, Network, State),
|
|
{noreply, NewState};
|
|
handle_cast({load_key, Phrase, Mnemonic, Network}, State) ->
|
|
NewState = load_key(Phrase, Mnemonic, Network, State),
|
|
{noreply, NewState};
|
|
handle_cast(start_stop, State) ->
|
|
ok = do_start_stop(State),
|
|
{noreply, State};
|
|
handle_cast(stop, State) ->
|
|
ok = do_stop(),
|
|
ok = log(info, "Received a 'stop' message."),
|
|
{noreply, State};
|
|
handle_cast(Unexpected, State) ->
|
|
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
handle_info({gproc_ps_event, Event, Data}, State) ->
|
|
ok = gmc_gui:message({Event, Data}),
|
|
{noreply, State};
|
|
handle_info(Unexpected, State) ->
|
|
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
code_change(_, State, _) ->
|
|
{ok, State}.
|
|
|
|
|
|
terminate(Reason, _) ->
|
|
ok = log(info, "Reason: ~p,", [Reason]),
|
|
case whereis(gd_con) of
|
|
undefined ->
|
|
zx:stop();
|
|
PID ->
|
|
ok = log(info, "gd_con found at: ~p", [PID]),
|
|
application:stop(gajumine)
|
|
end.
|
|
|
|
|
|
do_stop() ->
|
|
case is_pid(whereis(gd_con)) of
|
|
false -> zx:stop();
|
|
true -> application:stop(gajumine)
|
|
end.
|
|
|
|
|
|
%%% Doers
|
|
|
|
unlock(Phrase, State = #s{key = {blob, Cipher}}) ->
|
|
Pass = pass(unicode:characters_to_binary(Phrase)),
|
|
case decrypt(Pass, Cipher) of
|
|
{ok, Binary} ->
|
|
{ok, #wallet{keys = Keys}} = zx_lib:b_to_t(Binary),
|
|
Name = gm_name(),
|
|
Unlocked = #key{id = ID, pair = Pair} = lists:keyfind(Name, #key.name, Keys),
|
|
#{secret := Secret} = Pair,
|
|
Encrypted = Pair#{secret => encrypt(Pass, Secret)},
|
|
ok = gmc_gui:set_account(ID),
|
|
State#s{key = Unlocked#key{pair = Encrypted}, pass = Pass};
|
|
{error, bad_password} ->
|
|
ok = gmc_gui:ask_passphrase(),
|
|
State
|
|
end.
|
|
|
|
|
|
make_key(Phrase, Network, State) ->
|
|
{ID, Pair = #{secret := <<K:32/binary, _/binary>>}} = hz_key_master:make_key(<<>>),
|
|
Mnemonic = hz_key_master:encode(K),
|
|
ok = gmc_setup:done(Mnemonic),
|
|
finalize_key(Phrase, ID, Pair, Network, State).
|
|
|
|
|
|
load_key(Phrase, Mnemonic, Network, State) ->
|
|
case hz_key_master:decode(Mnemonic) of
|
|
{ok, Secret} ->
|
|
{ID, Pair} = hz_key_master:make_key(Secret),
|
|
ok = gmc_setup:done(),
|
|
finalize_key(Phrase, ID, Pair, Network, State);
|
|
Error ->
|
|
ok = gmc_setup:trouble(Error),
|
|
State
|
|
end.
|
|
|
|
|
|
finalize_key(Phrase, ID, Pair, Network, State) ->
|
|
NetBin = atom_to_binary(Network),
|
|
Pass =
|
|
case Phrase =:= "" of
|
|
false -> pass(unicode:characters_to_binary(Phrase));
|
|
true -> none
|
|
end,
|
|
Key = #key{id = ID} = add_wallet(Pass, ID, Pair, NetBin),
|
|
State#s{key = Key, pass = Pass, network = NetBin}.
|
|
|
|
|
|
add_wallet(Pass, ID, Pair = #{secret := Secret}, Network) ->
|
|
GDPrefsPath = gd_prefs_path(),
|
|
GDPrefs =
|
|
case file:consult(GDPrefsPath) of
|
|
{ok, Prefs} -> proplists:to_map(Prefs);
|
|
{error, enoent} -> #{}
|
|
end,
|
|
Wallets = gd_get_prefs(wallets, GDPrefs, []),
|
|
WalletPath = gajumining_wallet(),
|
|
Created = #key{name = gm_name(), id = ID, pair = Pair},
|
|
Net = #net{id = Network},
|
|
ChainID = <<"groot.", Network/binary>>,
|
|
Node = #node{ip = unicode:characters_to_list(lists:join($., ["groot", Network, "gajumaru", "io"]))},
|
|
New = #wallet{name = gm_name(),
|
|
poas = [#poa{name = gm_name(), id = ID}],
|
|
keys = [Created],
|
|
chain_id = ChainID,
|
|
endpoint = Node,
|
|
nets = [Net]},
|
|
{WalletBin, KeyPair, HasPass} =
|
|
case Pass =:= none of
|
|
false -> {encrypt(Pass, term_to_binary(New)), Pair#{secret => {cipher, encrypt(Pass, Secret)}}, true};
|
|
true -> {term_to_binary(New), Pair, false}
|
|
end,
|
|
Entry = #wr{name = gm_name(), path = WalletPath, pass = HasPass},
|
|
NewWallets = lists:keystore(gm_name(), #wr.name, Wallets, Entry),
|
|
NewGDPrefs = gd_put_prefs(wallets, NewWallets, GDPrefs),
|
|
ok = file:write_file(WalletPath, WalletBin),
|
|
ok = zx_lib:write_terms(GDPrefsPath, proplists:from_map(NewGDPrefs)),
|
|
Created#key{pair = KeyPair}.
|
|
|
|
|
|
end_setup(State = #s{window = Window}) ->
|
|
PID = wx_object:get_pid(Window),
|
|
true = unlink(PID),
|
|
start_gui(State#s{window = none}).
|
|
|
|
|
|
do_start_stop(#s{key = #key{id = PubKey}, network = Network}) ->
|
|
% smrt guy stuff happens here
|
|
{Bits, Eureka} =
|
|
case Network of
|
|
<<"mainnet">> -> {"29", <<"https://gajumining.com/api/workers/", PubKey/binary>>};
|
|
<<"testnet">> -> {"15", <<"https://test.gajumining.com/api/workers/", PubKey/binary>>}
|
|
end,
|
|
{Fatness, Type} =
|
|
case os:type() of
|
|
{unix, linux} ->
|
|
case Network of
|
|
<<"mainnet">> -> {"mean", "avx2"};
|
|
<<"testnet">> -> {"mean", "generic"}
|
|
end;
|
|
{unix, darwin} ->
|
|
% Check memory. >7gb gets mean, <7gb gets lean
|
|
{"mean", "generic"};
|
|
{win32, nt} ->
|
|
% Check memory. >7gb gets mean, <7gb gets lean
|
|
% Check avx2.
|
|
% Both should be provided by the F# start program
|
|
case Network of
|
|
<<"mainnet">> -> {"mean", "avx2.exe"};
|
|
<<"testnet">> -> {"mean", "generic.exe"}
|
|
end
|
|
end,
|
|
Miner = unicode:characters_to_binary([Fatness, Bits, "-", Type]),
|
|
Profile =
|
|
[{pubkey, PubKey},
|
|
{mining, [#{<<"executable">> => Miner, <<"executable_group">> => <<"gajumine">>}]},
|
|
{pool_admin_url, Eureka}],
|
|
{ok, Apps} = gmmpc_app:start(Profile),
|
|
ok = log(info, "Apps started: ~p", [Apps]),
|
|
Events =
|
|
[pool_notification,
|
|
{pool_notification, new_generation},
|
|
connected,
|
|
error,
|
|
disconnected],
|
|
lists:foreach(fun gmmpc_events:ensure_subscribed/1, Events).
|
|
|
|
|
|
%%% Utils
|
|
|
|
%% Encryption stuff
|
|
% TODO: Expose these from GD itself
|
|
|
|
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}],
|
|
try
|
|
{ok, crypto:crypto_one_time(aes_256_ecb, Pass, Binary, Flags)}
|
|
catch
|
|
error:{error, L, "Can't finalize"} ->
|
|
ok = log(info, "Decrypt failed at ~p", [L]),
|
|
{error, bad_password};
|
|
E:R ->
|
|
{E, R}
|
|
end.
|
|
|
|
|
|
pass(Phrase) ->
|
|
crypto:hash(sha3_256, Phrase).
|
|
|
|
|
|
%% Paths and Names
|
|
|
|
gm_name() ->
|
|
"GajuMine".
|
|
|
|
gajumining_wallet() ->
|
|
filename:join(zx_lib:path(var, "otpr", "gajudesk"), "gajumining.gaju").
|
|
|
|
|
|
gd_prefs_path() ->
|
|
filename:join(zx_lib:path(etc, "otpr", "gajudesk"), "prefs.eterms").
|
|
|
|
|
|
%% GajuDesk Prefs
|
|
% TODO: Glob these into a gd_prefs module exposed from GD itself
|
|
|
|
gd_get_prefs(K, M, D) ->
|
|
P = maps:get(gd_con, M, #{}),
|
|
maps:get(K, P, D).
|
|
|
|
|
|
gd_put_prefs(K, V, M) ->
|
|
P = maps:get(gd_con, M, #{}),
|
|
NewP = maps:put(K, V, P),
|
|
maps:put(gd_con, NewP, M).
|
|
|
|
|
|
gd_read_prefs() ->
|
|
case file:consult(gd_prefs_path()) of
|
|
{ok, Prefs} -> proplists:to_map(Prefs);
|
|
{error, enoent} -> #{}
|
|
end.
|