This commit is contained in:
2025-04-17 01:05:03 +09:00
commit a62f250df9
10 changed files with 1731 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
%%% @doc
%%% GajuMine
%%% @end
-module(gajumine).
-vsn("0.1.0").
-behavior(application).
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("Craig Everett <craigeverett@qpq.swiss>").
-license("GPL-3.0-or-later").
-export([start/2, stop/1]).
-include("$zx_include/zx_logger.hrl").
-spec start(normal, Args :: term()) -> {ok, pid()}.
start(normal, _Args) ->
ok =
case net_kernel:stop() of
ok ->
log(info, "SAFE: This node is running standalone.");
{error, not_found} ->
log(warning, "SAFETY: Distribution has been disabled on this node.");
{error, not_allowed} ->
ok = tell(error, "DANGER! This node is in distributed mode!"),
init:stop(1)
end,
ok = application:ensure_started(hakuzaru),
ok = application:ensure_started(zxwidgets),
ok = application:ensure_started(sophia),
gmc_sup:start_link().
-spec stop(term()) -> ok.
stop(_State) ->
ok.
+332
View File
@@ -0,0 +1,332 @@
%%% @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([unlock/1, make_key/1, load_key/2, end_setup/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 = none :: none | mainnet | testnet,
key = none :: none | {blob, binary()} | #key{},
pass = none :: none | binary()}).
-type state() :: #s{}.
%% Interface
-spec unlock(Phrase) -> ok
when Phrase :: string().
unlock(Phrase) ->
gen_server:cast(?MODULE, {unlock, Phrase}).
-spec make_key(Phrase) -> ok
when Phrase :: string().
make_key(Phrase) ->
gen_server:cast(?MODULE, {make_key, Phrase}).
-spec load_key(Phrase, Mnemonic) -> ok
when Phrase :: string(),
Mnemonic :: string().
load_key(Phrase, Mnemonic) ->
gen_server:cast(?MODULE, {load_key, Phrase, Mnemonic}).
-spec end_setup() -> ok.
end_setup() ->
gen_server:call(?MODULE, end_setup).
-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} ->
NewState = start_gui(#s{key = Wallet}),
{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}} = zx_lib:b_to_t(Binary),
Key = lists:keyfind(gm_name(), #key.name, Keys),
{ok, Key};
Error ->
Error
end;
false ->
error
end.
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(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}, State) ->
NewState = make_key(Phrase, State),
{noreply, NewState};
handle_cast({load_key, Phrase, Mnemonic}, State) ->
NewState = load_key(Phrase, Mnemonic, State),
{noreply, NewState};
handle_cast(stop, State) ->
ok = log(info, "Received a 'stop' message."),
{stop, normal, State};
handle_cast(Unexpected, State) ->
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
{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.
%%% 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, 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, State).
load_key(Phrase, Mnemonic, State) ->
case hz_key_master:decode(Mnemonic) of
{ok, Secret} ->
{ID, Pair} = hz_key_master:make_key(Secret),
finalize_key(Phrase, ID, Pair, State);
Error ->
ok = gmc_gui:trouble(Error),
ok = gmc_gui:bad_mnemonic(),
State
end.
finalize_key(Phrase, ID, Pair, State) ->
Pass = pass(unicode:characters_to_binary(Phrase)),
Key = #key{id = ID} = add_wallet(Pass, ID, Pair),
State#s{key = Key, pass = Pass}.
add_wallet(Pass, ID, Pair = #{secret := Secret}) ->
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(),
Entry = #wr{name = gm_name(), path = WalletPath, pass = true},
NewWallets = lists:keystore(gm_name(), #wr.name, Wallets, Entry),
NewGDPrefs = gd_put_prefs(wallets, NewWallets, GDPrefs),
Created = #key{name = gm_name(), id = ID, pair = Pair},
New = #wallet{name = gm_name(),
poas = [#poa{name = gm_name(), id = ID}],
keys = [Created],
chain_id = <<"groot.devnet">>,
endpoint = #node{},
nets = [#net{}]},
Cipher = encrypt(Pass, term_to_binary(New)),
ok = file:write_file(WalletPath, Cipher),
ok = zx_lib:write_terms(GDPrefsPath, proplists:from_map(NewGDPrefs)),
Encrypted = Pair#{secret => encrypt(Pass, Secret)},
Created#key{pair = Encrypted}.
end_setup(State = #s{window = Window}) ->
PID = wx_object:get_pid(Window),
true = unlink(PID),
start_gui(State#s{window = none}).
%%% 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.
+292
View File
@@ -0,0 +1,292 @@
%%% @doc
%%% GajuMine GUI
%%% @end
-module(gmc_gui).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("Craig Everett <craigeverett@qpq.swiss>").
-license("GPL-3.0-or-later").
-behavior(wx_object).
-include_lib("wx/include/wx.hrl").
-export([ask_passphrase/0, set_account/1, show/1]).
-export([start_link/1]).
-export([init/1, terminate/2, code_change/3,
handle_call/3, handle_cast/2, handle_info/2, handle_event/2]).
-include("$zx_include/zx_logger.hrl").
-record(w,
{name = none :: atom(),
id = 0 :: integer(),
wx = none :: none | wx:wx_object()}).
-record(s,
{wx = none :: none | wx:wx_object(),
frame = none :: none | wx:wx_object(),
lang = en :: en | jp,
j = none :: none | fun(),
id = none :: none | wx:wx_object(),
diff = none :: none | wx:wx_object(),
perf = none :: none | wx:wx_object(),
candy = none :: none | wx:wx_object(),
mess = none :: none | wx:wx_object(),
buff = [] :: [string()],
buttons = [] :: [#w{}]}).
%%% Interface functions
set_account(ID) ->
wx_object:cast(?MODULE, {set_account, ID}).
ask_passphrase() ->
wx_object:cast(?MODULE, ask_passphrase).
show(Terms) ->
wx_object:cast(?MODULE, {show, Terms}).
%%% Startup Functions
start_link(Title) ->
wx_object:start_link({local, ?MODULE}, ?MODULE, Title, []).
init(Prefs) ->
ok = log(info, "GUI starting..."),
Lang = maps:get(lang, Prefs, en_US),
Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans),
WX = wx:new(),
Frame = wxFrame:new(WX, ?wxID_ANY, J("GajuMine")),
MainSz = wxBoxSizer:new(?wxVERTICAL),
LeftRight = wxBoxSizer:new(?wxHORIZONTAL),
Left = wxBoxSizer:new(?wxVERTICAL),
Right = wxBoxSizer:new(?wxVERTICAL),
Labels = [J("ID"), J("Difficulty"), J("Maps/s"), J("Candidate")],
{Grid, [ID_C, DiffC, PerfC, CandyC]} = display_box(Frame, Labels),
Style = ?wxDEFAULT bor ?wxTE_MULTILINE bor ?wxTE_READONLY,
MessSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Messages")}]),
MessC = wxTextCtrl:new(Frame, ?wxID_ANY, [{style, Style}]),
_ = wxStaticBoxSizer:add(MessSz, MessC, zxw:flags(wide)),
ButtonTemplates =
[{start_stop, J("Start")},
{gajudesk, J("Open Wallet")},
{eureka, J("GajuMining")},
{explorer, J("ChainExplorer")}],
MakeButton =
fun({Name, Label}) ->
B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]),
#w{name = Name, id = wxButton:getId(B), wx = B}
end,
Buttons = lists:map(MakeButton, ButtonTemplates),
_ = wxBoxSizer:add(Left, Grid, zxw:flags(base)),
_ = wxBoxSizer:add(Left, MessSz, zxw:flags(wide)),
Add = fun(#w{wx = Button}) -> wxBoxSizer:add(Right, Button, zxw:flags(wide)) end,
ok = lists:foreach(Add, Buttons),
_ = wxBoxSizer:add(LeftRight, Left, zxw:flags(wide)),
_ = wxBoxSizer:add(LeftRight, Right, zxw:flags(base)),
_ = wxBoxSizer:add(MainSz, LeftRight, zxw:flags(wide)),
ok = wxFrame:setSizer(Frame, MainSz),
ok = wxSizer:layout(MainSz),
ok = wxFrame:connect(Frame, command_button_clicked),
ok = wxFrame:connect(Frame, close_window),
ok = wxFrame:setSize(Frame, {650, 160}),
ok = wxFrame:center(Frame),
true = wxFrame:show(Frame),
State =
#s{wx = WX, frame = Frame,
lang = Lang, j = J,
id = ID_C, diff = DiffC, perf = PerfC, candy = CandyC,
mess = MessC,
buttons = Buttons},
{Frame, State}.
display_box(Parent, Labels) ->
Grid = wxFlexGridSizer:new(2, 4, 4),
ok = wxFlexGridSizer:setFlexibleDirection(Grid, ?wxHORIZONTAL),
ok = wxFlexGridSizer:addGrowableCol(Grid, 1),
Make =
fun(S) ->
L = [S, ":"],
T_L = wxStaticText:new(Parent, ?wxID_ANY, L),
T_C = wxStaticText:new(Parent, ?wxID_ANY, "", [{style, ?wxALIGN_RIGHT}]),
_ = wxFlexGridSizer:add(Grid, T_L, zxw:flags(base)),
_ = wxFlexGridSizer:add(Grid, T_C, zxw:flags(wide)),
T_C
end,
Controls = lists:map(Make, Labels),
{Grid, Controls}.
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
{noreply, State}.
handle_cast(ask_passphrase, State) ->
ok = ask_passphrase(State),
{noreply, State};
handle_cast({set_account, ID}, State) ->
ok = set_account(ID, State),
{noreply, State};
handle_cast({show, Terms}, State) ->
ok = do_show(Terms, State),
{noreply, State};
handle_cast(Unexpected, State) ->
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
{noreply, State}.
handle_info(Unexpected, State) ->
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
{noreply, State}.
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID},
State = #s{buttons = Buttons}) ->
NewState =
case lists:keyfind(ID, #w.id, Buttons) of
#w{name = start_stop} -> start_stop(State);
#w{name = gajudesk} -> gajudesk(State);
#w{name = eureka} -> eureka(State);
#w{name = explorer} -> explorer(State);
false -> State
end,
{noreply, NewState};
handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame}) ->
ok = retire(Frame),
{noreply, State};
handle_event(Event, State) ->
ok = tell(info, "Unexpected event ~tp", [Event]),
{noreply, State}.
code_change(_, State, _) ->
{ok, State}.
terminate(Reason, State) ->
ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]),
wx:destroy().
ask_passphrase(#s{frame = Frame, j = J}) ->
Label = J("Unlock Account"),
Dialog = wxDialog:new(Frame, ?wxID_ANY, Label, [{size, {500, 115}}]),
Sizer = wxBoxSizer:new(?wxVERTICAL),
PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Password")}]),
PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]),
_ = wxStaticBoxSizer:add(PassSz, PassTx, zxw:flags(wide)),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
Affirm = wxButton:new(Dialog, ?wxID_OK),
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
_ = wxBoxSizer:add(Sizer, PassSz, zxw:flags(base)),
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(wide)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxFrame:center(Dialog),
ok =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
Phrase = wxTextCtrl:getValue(PassTx),
gmc_con:unlock(Phrase);
?wxID_CANCEL ->
retire(Frame)
end,
wxDialog:destroy(Dialog).
set_account(ID, #s{id = Widget}) ->
wxStaticText:setLabel(Widget, ID).
do_show(Terms, #s{mess = Mess}) ->
String = io_lib:format("Received args: ~tp", [Terms]),
wxTextCtrl:changeValue(Mess, String).
start_stop(State) ->
ok = gmc_con:start_stop(),
State.
gajudesk(State) ->
ok = tell(info, "Running gajudesk"),
Launch =
fun() ->
R = "otpr",
N = "gajudesk",
{ok, V} = zx:latest({R, N}),
{ok, PackageString} = zx_lib:package_string({R, N, V}),
try
case zx:run(PackageString, []) of
ok -> ok;
Error -> tell(error, "gajudesk died with: ~p", [Error])
end
catch
E:R -> tell(error, "gajudesk died with: ~p", [{E, R}])
end
end,
PID = spawn(Launch),
ok = tell(info, "GajuDesk launched at PID: ~p", [PID]),
State.
eureka(State = #s{frame = Frame, j = J}) ->
ok = tell(info, "Opening Eureka"),
ok = open_browser(Frame, J, eureka_url()),
State.
eureka_url() ->
case gmc_con:network() of
mainnet -> "https://eureka.gajumining.com";
testnet -> "https://eureka.test.gajumarumining.com";
none -> "https://gajumarumining.com"
end.
explorer(State = #s{frame = Frame, j = J}) ->
ok = tell(info, "Opening Explorer"),
ok = open_browser(Frame, J, explorer_url()),
State.
explorer_url() ->
case gmc_con:network() of
mainnet -> "https://groot.mainnet.gajumaru.io";
testnet -> "https://groot.testnet.gajumaru.io";
none -> "https://gajumaru.io"
end.
open_browser(Frame, J, URL) ->
case wx_misc:launchDefaultBrowser(URL) of
true ->
ok;
false ->
Format = J("Trouble launching browser.\nOpen URL here: ~s"),
Message = io_lib:format(Format, [URL]),
zxw:show_message(Frame, Message)
end.
retire(Frame) ->
ok = gmc_con:stop(),
wxWindow:destroy(Frame).
+298
View File
@@ -0,0 +1,298 @@
%%% @doc
%%% GajuMine Setup GUI
%%%
%%% In the common case, when a user first runs the program they will not have any
%%% accounrts, keys, or mining system setup. The purpose of this program is to provide
%%% a first-run guide for them to get set up for mining and explain to the user what is
%%% going on.
%%%
%%% There are two cases to cover:
%%% 1. A new miner who doesn't know how anything works and needs to get set up from scratch.
%%% 2. A miner who does know a bit about how things work and wants to import a key.
%%% @end
-module(gmc_setup).
-vsn("0.1.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("Craig Everett <craigeverett@qpq.swiss>").
-license("GPL-3.0-or-later").
-behavior(wx_object).
-include_lib("wx/include/wx.hrl").
-export([trouble/1, done/1]).
-export([start_link/1]).
-export([init/1, terminate/2, code_change/3,
handle_call/3, handle_cast/2, handle_info/2, handle_event/2]).
-include("$zx_include/zx_logger.hrl").
-record(w,
{name = none :: atom(),
id = 0 :: integer(),
wx = none :: none | wx:wx_object()}).
-record(s,
{wx = none :: none | wx:wx_object(),
frame = none :: none | wx:wx_object(),
lang = en :: en | jp,
j = none :: none | fun(),
buttons = [] :: [#w{}]}).
%%% Interface functions
trouble(Info) ->
wx_object:cast(?MODULE, {trouble, Info}).
done(Mnemonic) ->
wx_object:cast(?MODULE, {done, Mnemonic}).
%%% Startup Functions
start_link(Prefs) ->
wx_object:start_link({local, ?MODULE}, ?MODULE, Prefs, []).
init(Prefs) ->
ok = log(info, "GUI starting..."),
Lang = maps:get(lang, Prefs, en_US),
Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans),
WX = wx:new(),
Frame = wxFrame:new(WX, ?wxID_ANY, J("GajuMine Setup")),
MainSz = wxBoxSizer:new(?wxVERTICAL),
ButtonTemplates =
[{new_key, J("Create a New Account")},
{recover, J("Recover Existing Account")}],
MakeButton =
fun({Name, Label}) ->
B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]),
#w{name = Name, id = wxButton:getId(B), wx = B}
end,
Buttons = lists:map(MakeButton, ButtonTemplates),
Add = fun(#w{wx = Button}) -> wxBoxSizer:add(MainSz, Button, zxw:flags(wide)) end,
ok = lists:foreach(Add, Buttons),
ok = wxFrame:setSizer(Frame, MainSz),
ok = wxSizer:layout(MainSz),
ok = wxFrame:connect(Frame, command_button_clicked),
ok = wxFrame:connect(Frame, close_window),
ok = wxFrame:setSize(Frame, {300, 300}),
ok = wxFrame:center(Frame),
true = wxFrame:show(Frame),
State =
#s{wx = WX, frame = Frame,
lang = Lang, j = J,
buttons = Buttons},
{Frame, State}.
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
{noreply, State}.
handle_cast({trouble, Info}, State) ->
ok = handle_troubling(State, Info),
{noreply, State};
handle_cast({done, Mnemonic}, State) ->
ok = done(Mnemonic, State),
{noreply, State};
handle_cast(Unexpected, State) ->
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
{noreply, State}.
handle_info(Unexpected, State) ->
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
{noreply, State}.
handle_event(#wx{event = #wxCommand{type = command_button_clicked},
id = ID},
State = #s{buttons = Buttons}) ->
ok =
case lists:keyfind(ID, #w.id, Buttons) of
#w{name = new_key} -> new_key(State);
#w{name = recover} -> recover(State)
end,
{noreply, State};
handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame}) ->
ok = gmc_con:stop(),
ok = wxWindow:destroy(Frame),
{noreply, State};
handle_event(Event, State) ->
ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]),
{noreply, State}.
handle_troubling(#s{frame = Frame}, Info) ->
zxw:show_message(Frame, Info).
code_change(_, State, _) ->
{ok, State}.
terminate(Reason, State) ->
ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]),
wx:destroy().
new_key(State = #s{frame = Frame, j = J}) ->
case prompt_passphrase(Frame, J) of
{ok, Phrase} ->
gmc_con:make_key(Phrase);
mismatch ->
ok = zxw:show_message(Frame, J("Password entered incorrectly. Try again.")),
new_key(State);
cancel ->
ok
end.
prompt_passphrase(Frame, J) ->
Label = J("Generate New Account Key"),
Dialog = wxDialog:new(Frame, ?wxID_ANY, Label, [{size, {500, 180}}]),
Sizer = wxBoxSizer:new(?wxVERTICAL),
PassSz1 = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Create Password")}]),
PassTx1 = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]),
_ = wxStaticBoxSizer:add(PassSz1, PassTx1, zxw:flags(wide)),
PassSz2 = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Confirm Password")}]),
PassTx2 = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]),
_ = wxStaticBoxSizer:add(PassSz2, PassTx2, zxw:flags(wide)),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
Affirm = wxButton:new(Dialog, ?wxID_OK),
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
_ = wxBoxSizer:add(Sizer, PassSz1, zxw:flags(base)),
_ = wxBoxSizer:add(Sizer, PassSz2, zxw:flags(base)),
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(wide)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxFrame:center(Dialog),
Outcome =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
Pass1 = wxTextCtrl:getValue(PassTx1),
Pass2 = wxTextCtrl:getValue(PassTx2),
case Pass1 =:= Pass2 of
true -> {ok, Pass1};
false -> mismatch
end;
?wxID_CANCEL ->
cancel
end,
ok = wxDialog:destroy(Dialog),
Outcome.
recover(State = #s{frame = Frame, j = J}) ->
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Import Account Key")),
Sizer = wxBoxSizer:new(?wxVERTICAL),
PassSz1 = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Create Password")}]),
PassTx1 = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]),
_ = wxStaticBoxSizer:add(PassSz1, PassTx1, zxw:flags(wide)),
PassSz2 = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Confirm Password")}]),
PassTx2 = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]),
_ = wxStaticBoxSizer:add(PassSz2, PassTx2, zxw:flags(wide)),
MnemSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Recovery Phrase")}]),
MnemTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_MULTILINE}]),
_ = wxStaticBoxSizer:add(MnemSz, MnemTx, zxw:flags(wide)),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
Affirm = wxButton:new(Dialog, ?wxID_OK),
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
_ = wxBoxSizer:add(Sizer, PassSz1, zxw:flags(base)),
_ = wxBoxSizer:add(Sizer, PassSz2, zxw:flags(base)),
_ = wxBoxSizer:add(Sizer, MnemSz, zxw:flags(wide)),
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxFrame:center(Dialog),
Outcome =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
Pass1 = wxTextCtrl:getValue(PassTx1),
Pass2 = wxTextCtrl:getValue(PassTx2),
case Pass1 =:= Pass2 of
true ->
Mnemonic = wxTextCtrl:getValue(MnemTx),
gmc_con:load_key(Pass1, Mnemonic);
false ->
mismatch
end;
?wxID_CANCEL ->
cancel
end,
ok = wxDialog:destroy(Dialog),
case Outcome of
ok ->
ok;
mismatch ->
ok = zxw:show_message(Frame, J("Password entered incorrectly. Try again.")),
recover(State);
cancel ->
ok
end.
done(Mnemonic, #s{frame = Frame, j = J}) ->
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Mnemonic")),
Sizer = wxBoxSizer:new(?wxVERTICAL),
MnemSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Recovery Phrase")}]),
Options = [{value, Mnemonic}, {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}],
MnemTx = wxTextCtrl:new(Dialog, ?wxID_ANY, Options),
_ = wxStaticBoxSizer:add(MnemSz, MnemTx, zxw:flags(wide)),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
CloseB = wxButton:new(Dialog, ?wxID_CANCEL, [{label, J("Close")}]),
CopyB = wxButton:new(Dialog, ?wxID_OK, [{label, J("Copy to Clipboard")}]),
_ = wxBoxSizer:add(ButtSz, CloseB, zxw:flags(wide)),
_ = wxBoxSizer:add(ButtSz, CopyB, zxw:flags(wide)),
_ = wxBoxSizer:add(Sizer, MnemSz, zxw:flags(wide)),
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxFrame:center(Dialog),
ok = wxStyledTextCtrl:setFocus(MnemTx),
ok =
case wxDialog:showModal(Dialog) of
?wxID_CANCEL -> ok;
?wxID_OK -> copy_to_clipboard(Mnemonic)
end,
ok = wxDialog:destroy(Dialog),
ok = gmc_con:end_setup(),
ok = wxWindow:destroy(Frame),
ok.
copy_to_clipboard(String) ->
CB = wxClipboard:get(),
case wxClipboard:open(CB) of
true ->
Text = wxTextDataObject:new([{text, String}]),
case wxClipboard:setData(CB, Text) of
true ->
R = wxClipboard:flush(CB),
log(info, "String copied to system clipboard. Flushed: ~p", [R]);
false ->
log(info, "Failed to copy to clipboard")
end,
ok = wxClipboard:close(CB);
false ->
log(info, "Failed to acquire the clipboard.")
end.
+46
View File
@@ -0,0 +1,46 @@
%%% @doc
%%% GajuMine Top-level Supervisor
%%%
%%% The very top level supervisor in the system. It only has one service branch: the
%%% "con" (program controller). The con is the
%%% only be one part of a larger system. Were this a game system, for example, the
%%% item data management service would be a peer, as would a login credential provision
%%% service, game world event handling, and so on.
%%%
%%% See: http://erlang.org/doc/design_principles/applications.html
%%% See: http://zxq9.com/archives/1311
%%% @end
-module(gmc_sup).
-vsn("0.1.0").
-behaviour(supervisor).
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("Craig Everett <craigeverett@qpq.swiss>").
-license("GPL-3.0-or-later").
-export([start_link/0]).
-export([init/1]).
-spec start_link() -> {ok, pid()}.
%% @private
%% This supervisor's own start function.
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
%% @private
%% The OTP init/1 function.
init([]) ->
RestartStrategy = {one_for_one, 0, 60},
Clients = {gmc_con,
{gmc_con, start_link, []},
permanent,
5000,
worker,
[gmc_con]},
Children = [Clients],
{ok, {RestartStrategy, Children}}.