First-run wallet creatorator (v0.7.0) (#24)

Adds a first-run wallet and account creator option for noobs.
If they select the "create a default wallet" option, they are jumped to the name + password screen for wallet creation, and a new account is generated for them named "Account 1".

This leapfrogs the problem of users having to know what is going on with the blockchain and wallet at all before getting started.
#18
#19

Reviewed-on: #24
Reviewed-by: Ulf Wiger <ulfwiger@qpq.swiss>
Co-authored-by: Craig Everett <zxq9@zxq9.com>
Co-committed-by: Craig Everett <zxq9@zxq9.com>
This commit is contained in:
Craig Everett 2025-08-07 15:47:05 +09:00 committed by Craig Everett
parent a2a9da0d98
commit f92c5fbde0
13 changed files with 236 additions and 80 deletions

View File

@ -3,7 +3,7 @@
{registered,[]},
{included_applications,[]},
{applications,[stdlib,kernel,sasl,ssl]},
{vsn,"0.6.6"},
{vsn,"0.7.0"},
{modules,[gajudesk,gd_con,gd_grids,gd_gui,gd_jt,
gd_sophia_editor,gd_sup,gd_v,gd_v_devman,gd_v_netman,
gd_v_wallman]},

View File

@ -3,7 +3,7 @@
%%% @end
-module(gajudesk).
-vsn("0.6.6").
-vsn("0.7.0").
-behavior(application).
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").

View File

@ -3,7 +3,7 @@
%%% @end
-module(gd_con).
-vsn("0.6.6").
-vsn("0.7.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").
@ -38,15 +38,15 @@
mon = none :: none | reference()}).
-record(s,
{version = 1 :: integer(),
window = none :: none | wx:wx_object(),
timer = none :: none | reference(),
tasks = [] :: [#ui{}],
selected = 0 :: non_neg_integer(),
wallet = none :: none | #wallet{},
pass = none :: none | binary(),
prefs = #{} :: #{module() := term()},
wallets = [] :: [#wr{}]}).
{version = 1 :: integer(),
window = none :: none | wx:wx_object(),
timer = none :: none | reference(),
tasks = [] :: [#ui{}],
selected = 0 :: non_neg_integer(),
wallet = none :: none | #wallet{},
pass = none :: none | binary(),
prefs = #{} :: #{module() := term()},
wallets = [] :: [#wr{}]}).
-type state() :: #s{}.
@ -63,15 +63,16 @@
when Name :: ui_name().
show_ui(Name) ->
gen_server:cast(?MODULE, {show_ui, Name}).
gen_server:call(?MODULE, {show_ui, Name}).
-spec open_wallet(Path, Phrase) -> ok
-spec open_wallet(Path, Phrase) -> Result
when Path :: file:filename(),
Phrase :: string().
Phrase :: string(),
Result :: ok | {error, Reason :: term()}.
open_wallet(Path, Phrase) ->
gen_server:cast(?MODULE, {open_wallet, Path, Phrase}).
gen_server:call(?MODULE, {open_wallet, Path, Phrase}).
-spec close_wallet() -> ok.
@ -217,7 +218,7 @@ deploy(Build, Params, InitArgs) ->
Size :: 256,
Name :: string(),
Seed :: string(),
Encoding :: utf8 | base64 | base58,
Encoding :: utf8 | base64 | base58 | none,
Transform :: {Algo, Yugeness},
Algo :: sha3 | sha2 | x_or | pbkdf2,
Yugeness :: rand | non_neg_integer().
@ -328,6 +329,7 @@ init(none) ->
T = erlang:send_after(tic(), self(), tic),
State = #s{window = Window, timer = T, wallets = Wallets, prefs = Prefs},
NewState = do_show_ui(gd_v_wallman, State),
ok = gd_v_wallman:first_run(),
{ok, NewState}.
@ -365,6 +367,9 @@ handle_call(list_keys, _, State) ->
handle_call({nonce, ID}, _, State) ->
Response = do_nonce(ID),
{reply, Response, State};
handle_call({open_wallet, Path, Phrase}, _, State) ->
{Response, NewState} = do_open_wallet(Path, Phrase, State),
{reply, Response, NewState};
handle_call(network, _, State) ->
Response = do_network(State),
{reply, Response, State};
@ -374,6 +379,9 @@ handle_call({save, Module, Prefs}, _, State) ->
handle_call({mnemonic, ID}, _, State) ->
Response = do_mnemonic(ID, State),
{reply, Response, State};
handle_call({show_ui, Name}, _, State) ->
NewState = do_show_ui(Name, State),
{reply, ok, NewState};
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
{noreply, State}.
@ -387,16 +395,11 @@ handle_call(Unexpected, From, State) ->
%% 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) ->
NextState = do_close_wallet(State),
ok = gd_gui:show([]),
NewState = do_show_ui(gd_v_wallman, NextState),
ok = gd_v_wallman:to_front(),
{noreply, NewState};
handle_cast({new_wallet, Net, Name, Path, Password}, State) ->
NewState = do_new_wallet(Net, Name, Path, Password, State),
@ -477,6 +480,9 @@ handle_cast(Unexpected, State) ->
handle_info(tic, State) ->
NewState = do_tic(State),
{noreply, NewState};
handle_info({show_ui, Name}, State) ->
NewState = do_show_ui(Name, State),
{noreply, NewState};
handle_info({'DOWN', Mon, process, PID, Info}, State) ->
NewState = handle_down(Mon, PID, Info, State),
{noreply, NewState};
@ -531,7 +537,6 @@ do_show_ui(Name, State = #s{tasks = Tasks, prefs = Prefs}) ->
Win = Name:start_link({TaskPrefs, TaskData}),
PID = wx_object:get_pid(Win),
Mon = monitor(process, PID),
ok = Name:to_front(Win),
UI = #ui{name = Name, pid = PID, wx = Win, mon = Mon},
State#s{tasks = [UI | Tasks]}
end.
@ -863,7 +868,9 @@ do_make_key(Name, Seed, base58, Transform, State) ->
do_make_key2(_, _, _, State = #s{wallet = none}) ->
ok = gd_gui:trouble("No wallet selected!"),
do_show_ui(gd_v_wallman, State);
NewState = do_show_ui(gd_v_wallman, State),
ok = gd_v_wallman:to_front(),
NewState;
do_make_key2(Name, Bin, Transform,
State = #s{wallet = Current, wallets = Wallets, pass = Pass}) ->
#wallet{name = WalletName, poas = POAs, keys = Keys} = Current,
@ -1020,17 +1027,12 @@ do_open_wallet(Path, Phrase, State) ->
{ok, ChainID} -> gd_gui:chain(ChainID, Node);
Error -> gd_gui:trouble(Error)
end,
State#s{pass = Pass, wallet = Recovered};
{ok, State#s{pass = Pass, wallet = Recovered}};
Error ->
ok = gd_gui:trouble(Error),
New = default_wallet(),
State#s{wallet = New}
{Error, State}
end.
default_wallet() ->
default_wallet(testnet).
default_wallet(mainnet) ->
Node = #node{ip = "groot.mainnet.gajumaru.io"},
Groot = #chain{id = <<"groot.mainnet">>,

View File

@ -37,7 +37,7 @@
%%% @end
-module(gd_grids).
-vsn("0.6.6").
-vsn("0.7.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").
@ -59,7 +59,7 @@
Reason :: bad_url.
parse(URL) ->
case uri_string:parse(URL) of
case uri_string:parse(string:trim(URL)) of
#{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grids"} ->
spend(R, chain, list_to_binary(H), Q);
#{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grid"} ->

View File

@ -3,7 +3,7 @@
%%% @end
-module(gd_gui).
-vsn("0.6.6").
-vsn("0.7.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").
@ -361,6 +361,7 @@ refresh(State) ->
wallman(State) ->
ok = gd_con:show_ui(gd_v_wallman),
ok = gd_v_wallman:to_front(),
State.

View File

@ -15,7 +15,7 @@
%%% translation library is retained).
-module(gd_jt).
-vsn("0.6.6").
-vsn("0.7.0").
-export([read_translations/1, j/2, oneshot_j/2]).

View File

@ -1,5 +1,5 @@
-module(gd_sophia_editor).
-vsn("0.6.6").
-vsn("0.7.0").
-export([new/1, update/2,
get_text/1, set_text/2]).

View File

@ -12,7 +12,7 @@
%%% @end
-module(gd_sup).
-vsn("0.6.6").
-vsn("0.7.0").
-behaviour(supervisor).
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").

View File

@ -1,5 +1,5 @@
-module(gd_v).
-vsn("0.6.6").
-vsn("0.7.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").

View File

@ -1,5 +1,5 @@
-module(gd_v_devman).
-vsn("0.6.6").
-vsn("0.7.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").

View File

@ -1,5 +1,5 @@
-module(gd_v_netman).
-vsn("0.6.6").
-vsn("0.7.0").
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").

View File

@ -1,5 +1,19 @@
%%% @doc
%%% The GajuDesk Wallet Manager
%%%
%%% This is an interface for managing multiple wallets.
%%% A wallet is defined as a collection of accounts/keys.
%%%
%%% NOTE:
%%% Any call to `gd_con:show_ui(gd_v_wallman)' must be followed by a call to
%%% either `gd_v_wallman:to_front()' or `gd_v_wallman:first_run()' or else
%%% this UI process will just sit in the background (invisible) until told to
%%% do something.
%%% @end
-module(gd_v_wallman).
-vsn("0.6.6").
-vsn("0.7.0").
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").
@ -7,7 +21,7 @@
-behavior(wx_object).
%-behavior(gd_v).
-include_lib("wx/include/wx.hrl").
-export([to_front/1]).
-export([to_front/0, to_front/1, first_run/0, trouble/1]).
-export([show/2]).
-export([start_link/1]).
-export([init/1, terminate/2, code_change/3,
@ -29,11 +43,18 @@
prefs = #{} :: map(),
wallets = [] :: [#wr{}],
picker = none :: none | wx:wx_object(),
buttons = [] :: [#w{}]}).
buttons = [] :: [#w{}],
wiz = none :: none | {wx:wx_object(), Buttons :: [#w{}]}}).
%%% Interface
-spec to_front() -> ok.
to_front() ->
wx_object:cast(?MODULE, to_front).
-spec to_front(Win) -> ok
when Win :: wx:wx_object().
@ -41,6 +62,19 @@ to_front(Win) ->
wx_object:cast(Win, to_front).
-spec first_run() -> ok.
first_run() ->
wx_object:cast(?MODULE, first_run).
-spec trouble(Info) -> ok
when Info :: term().
trouble(Info) ->
wx_object:cast(?MODULE, {trouble, Info}).
-spec show(Win, Manifest) -> ok
when Win :: wx:xw_object(),
Manifest :: [#wr{}].
@ -107,34 +141,32 @@ init({Prefs, Manifest}) ->
ok = wxFrame:connect(Frame, command_button_clicked),
ok = wxFrame:connect(Frame, close_window),
true = wxFrame:show(Frame),
ok = wxListBox:connect(Picker, command_listbox_doubleclicked),
ok =
case length(Manifest) =:= 0 of
false ->
ok;
true ->
self() ! new,
ok
end,
State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs,
wallets = Manifest,
picker = Picker,
buttons = Buttons},
wallets = Manifest,
picker = Picker,
buttons = Buttons},
{Frame, State}.
%%% wx_object
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
{noreply, State}.
handle_cast(to_front, State = #s{frame = Frame}) ->
ok = ensure_shown(Frame),
ok = wxFrame:raise(Frame),
{noreply, State};
handle_cast(first_run, State) ->
NewState = do_first_run(State),
{noreply, NewState};
handle_cast({trouble, Info}, State) ->
ok = handle_troubling(State, Info),
{noreply, State};
handle_cast({show, Manifest}, State) ->
NewState = do_show(Manifest, State),
{noreply, NewState};
@ -143,9 +175,6 @@ handle_cast(Unexpected, State) ->
{noreply, State}.
handle_info(new, State) ->
NewState = do_new(State),
{noreply, NewState};
handle_info(Unexpected, State) ->
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
{noreply, State}.
@ -153,7 +182,7 @@ handle_info(Unexpected, State) ->
handle_event(#wx{event = #wxCommand{type = command_button_clicked},
id = ID},
State = #s{buttons = Buttons}) ->
State = #s{buttons = Buttons, wiz = none}) ->
NewState =
case lists:keyfind(ID, #w.id, Buttons) of
#w{name = open} -> do_open(State);
@ -166,17 +195,34 @@ handle_event(#wx{event = #wxCommand{type = command_button_clicked},
{noreply, NewState};
handle_event(#wx{event = #wxCommand{type = command_listbox_doubleclicked,
commandInt = Selected}},
State) ->
NewState = do_open2(Selected + 1, State),
{noreply, NewState};
handle_event(#wx{event = #wxClose{}}, State) ->
State = #s{wiz = none}) ->
ok = do_open2(Selected + 1, State),
{noreply, State};
handle_event(#wx{event = #wxClose{}}, State = #s{wiz = none}) ->
ok = do_close(State),
{noreply, State};
handle_event(#wx{event = #wxCommand{type = command_button_clicked},
id = ID},
State = #s{wiz = {_, WizButtons}}) ->
NewState =
case lists:keyfind(ID, #w.id, WizButtons) of
#w{name = noob} -> wiz_noob_assist(State);
#w{name = l33t} -> close_wiz(State);
false -> State
end,
{noreply, NewState};
handle_event(#wx{event = #wxClose{}}, State = #s{wiz = {_, _}}) ->
NewState = close_wiz(State),
{noreply, NewState};
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}.
@ -197,6 +243,26 @@ do_show(Manifest, State = #s{picker = Picker}) ->
State#s{wallets = Manifest}.
do_first_run(State = #s{frame = Frame, wallets = Manifest}) ->
Count = length(Manifest),
if
Count =:= 0 ->
do_wiz(State);
Count =:= 1 ->
do_open(State);
Count > 1 ->
true = wxFrame:show(Frame),
wxFrame:raise(Frame),
State
end.
close_wiz(State = #s{frame = Frame, wiz = {Wiz, _}}) ->
ok = wxWindow:destroy(Wiz),
true = wxFrame:show(Frame),
State#s{wiz = none}.
do_close(#s{frame = Frame, prefs = Prefs}) ->
Geometry =
case wxTopLevelWindow:isMaximized(Frame) of
@ -219,10 +285,26 @@ handle_button(Name, State) ->
do_open(State = #s{wallets = []}) ->
State;
do_open(State = #s{wallets = [#wr{pass = true, path = Path}]}) ->
ok = do_open3(Path, State),
State;
do_open(State = #s{wallets = [#wr{pass = false, path = Path}], frame = Frame}) ->
ok =
case gd_con:open_wallet(Path, none) of
ok ->
do_close(State);
Error ->
ok = ensure_shown(Frame),
trouble(Error)
end,
State;
do_open(State = #s{picker = Picker}) ->
case wxListBox:getSelection(Picker) of
-1 -> State;
Selected -> do_open2(Selected + 1, State)
-1 ->
State;
Selected ->
ok = do_open2(Selected + 1, State),
State
end.
do_open2(Selected, State = #s{wallets = Wallets}) ->
@ -231,8 +313,7 @@ do_open2(Selected, State = #s{wallets = Wallets}) ->
do_open3(Path, State);
#wr{pass = false, path = Path} ->
ok = gd_con:open_wallet(Path, none),
ok = do_close(State),
State
do_close(State)
end.
do_open3(Path, State = #s{frame = Frame, j = J}) ->
@ -250,32 +331,90 @@ do_open3(Path, State = #s{frame = Frame, j = J}) ->
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxDialog:setSize(Dialog, {500, 130}),
ok = wxFrame:center(Dialog),
ok = wxDialog:center(Dialog),
ok = wxStyledTextCtrl:setFocus(PassTx),
case wxDialog:showModal(Dialog) of
?wxID_OK ->
case wxTextCtrl:getValue(PassTx) of
"" ->
ok = wxDialog:destroy(Dialog),
State;
ensure_shown(Frame);
Phrase ->
ok = wxDialog:destroy(Dialog),
ok = gd_con:open_wallet(Path, Phrase),
ok = do_close(State),
State
case gd_con:open_wallet(Path, Phrase) of
ok ->
do_close(State);
Error ->
ok = ensure_shown(Frame),
trouble(Error)
end
end;
?wxID_CANCEL ->
ok = wxDialog:destroy(Dialog),
ensure_shown(Frame)
end.
do_wiz(State = #s{wx = WX, lang = Lang, wiz = none}) ->
Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans),
Wiz = wxFrame:new(WX, ?wxID_ANY, J("Initializing Wallet")),
MainSz = wxBoxSizer:new(?wxVERTICAL),
ButtonTemplates =
[{noob, J("I'm new!\nCreate a new account for me.")},
{l33t, J("Open the wallet manager.")}],
MakeButton =
fun({Name, Label}) ->
B = wxButton:new(Wiz, ?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(Wiz, MainSz),
ok = wxSizer:layout(MainSz),
ok = wxFrame:connect(Wiz, command_button_clicked),
ok = wxFrame:connect(Wiz, close_window),
ok = wxFrame:setSize(Wiz, {300, 300}),
ok = wxFrame:center(Wiz),
true = wxFrame:show(Wiz),
State#s{wiz = {Wiz, Buttons}}.
wiz_noob_assist(State = #s{j = J, wiz = {Wiz, _}}) ->
DefaultDir = zx_lib:path(var, "otpr", "gajudesk"),
Name = default_name(),
Path = filename:join(DefaultDir, Name),
case do_new2(Path, J, Wiz) of
ok ->
Label = J("Account 1"),
ok = gd_con:make_key({eddsa, ed25519}, 256, Label, <<>>, none, {sha3, 256}),
ok = do_close(State),
State;
abort ->
State
end.
default_name() ->
{{YY, MM, DD}, {Hr, Mn, Sc}} = calendar:local_time(),
Form = "~4.10.0B-~2.10.0B-~2.10.0B_~2.10.0B-~2.10.0B-~2.10.0B",
Name = io_lib:format(Form, [YY, MM, DD, Hr, Mn, Sc]),
unicode:characters_to_list(Name ++ ".gaju").
do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) ->
DefaultDir = maps:get(dir, Prefs, zx_lib:path(var, "otpr", "gajudesk")),
Options =
[{message, J("Save Location")},
{defaultDir, DefaultDir},
{defaultFile, "default.gaju"},
{defaultFile, default_name()},
{wildCard, "*.gaju"},
{sz, {300, 270}},
{style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}],
@ -289,7 +428,9 @@ do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) ->
case do_new2(Path, J, Frame) of
ok ->
NewPrefs = maps:put(dir, Dir, Prefs),
do_close(State#s{prefs = NewPrefs});
NewState = State#s{prefs = NewPrefs},
ok = do_close(NewState),
NewState;
abort ->
State
end;
@ -301,8 +442,8 @@ do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) ->
do_new2(Path, J, Frame) ->
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Wallet"), [{size, {400, 250}}]),
Sizer = wxBoxSizer:new(?wxVERTICAL),
Network = wxRadioBox:new(Dialog, ?wxID_ANY, J("Network"), {0, 0}, {50, 50}, [J("Mainnet"), J("Testnet")]),
NetworkOptions = [J("Mainnet"), J("Testnet")],
Network = wxRadioBox:new(Dialog, ?wxID_ANY, J("Network"), {0, 0}, {50, 50}, NetworkOptions),
NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]),
NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
_ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)),
@ -382,7 +523,9 @@ do_import(State = #s{frame = Frame, j = J, prefs = Prefs}) ->
case do_import2(Dir, File, J, Frame) of
ok ->
NewPrefs = maps:put(dir, Dir, Prefs),
do_close(State#s{prefs = NewPrefs});
NewState = State#s{prefs = NewPrefs},
ok = do_close(NewState),
NewState;
abort ->
State
end;
@ -484,3 +627,13 @@ do_drop(Selected, State = #s{j = J, frame = Frame, wallets = Wallets}) ->
ok
end,
State.
ensure_shown(Frame) ->
case wxWindow:isShown(Frame) of
true ->
ok;
false ->
true = wxFrame:show(Frame),
ok
end.

View File

@ -4,7 +4,7 @@
{prefix,"gd"}.
{author,"Craig Everett"}.
{desc,"A desktop client for the Gajumaru network of blockchain networks"}.
{package_id,{"otpr","gajudesk",{0,6,6}}}.
{package_id,{"otpr","gajudesk",{0,7,0}}}.
{deps,[{"otpr","hakuzaru",{0,6,1}},
{"otpr","eblake2",{1,0,1}},
{"otpr","base58",{0,1,1}},