%%% @doc %%% GajuMine Controller %%% @end -module(gmc_con). -vsn("0.1.0"). -author("Craig Everett "). -copyright("Craig Everett "). -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 := <>}} = 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.