%%% @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.8.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). -behavior(wx_object). %-behavior(gd_v). -include_lib("wx/include/wx.hrl"). -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, handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). -include("$zx_include/zx_logger.hrl"). -include("gd.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(), prefs = #{} :: map(), wallets = [] :: [#wr{}], picker = none :: none | wx:wx_object(), 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(). 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{}]. show(Win, Manifest) -> wx_object:cast(Win, {show, Manifest}). %%% Startup start_link(Args) -> wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []). init({Prefs, Manifest}) -> 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("Wallets")), MainSz = wxBoxSizer:new(?wxVERTICAL), Picker = wxListBox:new(Frame, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]), Names = [Name || #wr{name = Name} <- Manifest], ok = wxListBox:set(Picker, Names), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), ButtonTemplates = [{open, J("Open")}, {new, J("New")}, {move, J("Move")}, {import, J("Import")}, {drop, J("Drop")}], MakeButton = fun({Name, Label}) -> B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]), _ = wxSizer:add(ButtSz, B, zxw:flags(wide)), #w{name = Name, id = wxButton:getId(B), wx = B} end, Buttons = lists:map(MakeButton, ButtonTemplates), _ = wxSizer:add(MainSz, Picker, zxw:flags(wide)), _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), ok = wxFrame:setSizer(Frame, MainSz), ok = wxSizer:layout(MainSz), NewPrefs = case maps:is_key(geometry, Prefs) of true -> Prefs; false -> Display = wxDisplay:new(), {_, _, W, H} = wxDisplay:getGeometry(Display), ok = wxDisplay:destroy(Display), WW = 500, WH = 350, X = (W div 2) - (WW div 2), Y = (H div 2) - (WH div 2), Prefs#{geometry => {X, Y, WW, WH}} end, ok = gd_v:safe_size(Frame, NewPrefs), ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:connect(Frame, close_window), ok = wxListBox:connect(Picker, command_listbox_doubleclicked), State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs, 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}; 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, wiz = none}) -> NewState = case lists:keyfind(ID, #w.id, Buttons) of #w{name = open} -> do_open(State); #w{name = new} -> do_new(State); #w{name = import} -> do_import(State); #w{name = drop} -> do_drop(State); #w{name = Name} -> handle_button(Name, State); false -> State end, {noreply, NewState}; handle_event(#wx{event = #wxCommand{type = command_listbox_doubleclicked, commandInt = Selected}}, 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}. terminate(wx_deleted, _) -> wx:destroy(); terminate(Reason, State) -> ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), wx:destroy(). %%% doers do_show(Manifest, State = #s{picker = Picker}) -> Names = [Name || #wr{name = Name} <- Manifest], ok = wxListBox:set(Picker, Names), 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 true -> max; false -> {X, Y} = wxWindow:getPosition(Frame), {W, H} = wxWindow:getSize(Frame), {X, Y, W, H} end, NewPrefs = maps:put(geometry, Geometry, Prefs), ok = gd_con:save(?MODULE, NewPrefs), ok = wxWindow:destroy(Frame). handle_button(Name, State) -> ok = tell("Button Click: ~p", [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 -> ok = do_open2(Selected + 1, State), State end. do_open2(Selected, State = #s{wallets = Wallets}) -> case lists:nth(Selected, Wallets) of #wr{pass = true, path = Path} -> do_open3(Path, State); #wr{pass = false, path = Path} -> ok = gd_con:open_wallet(Path, none), do_close(State) end. do_open3(Path, State = #s{frame = Frame, j = J}) -> Label = J("Password"), Dialog = wxDialog:new(Frame, ?wxID_ANY, Label), Sizer = wxBoxSizer:new(?wxVERTICAL), PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label}]), 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), _ = wxBoxSizer:add(ButtSz, Affirm, 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 = wxDialog:setSize(Dialog, {500, 130}), ok = wxDialog:center(Dialog), ok = wxStyledTextCtrl:setFocus(PassTx), case wxDialog:showModal(Dialog) of ?wxID_OK -> case wxTextCtrl:getValue(PassTx) of "" -> ok = wxDialog:destroy(Dialog), ensure_shown(Frame); Phrase -> ok = wxDialog:destroy(Dialog), 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_name()}, {wildCard, "*.gaju"}, {sz, {300, 270}}, {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], Dialog = wxFileDialog:new(Frame, Options), case wxFileDialog:showModal(Dialog) of ?wxID_OK -> Dir = wxFileDialog:getDirectory(Dialog), File = wxFileDialog:getFilename(Dialog), Path = filename:join(Dir, File), ok = wxFileDialog:destroy(Dialog), case do_new2(Path, J, Frame) of ok -> NewPrefs = maps:put(dir, Dir, Prefs), NewState = State#s{prefs = NewPrefs}, ok = do_close(NewState), NewState; abort -> State end; ?wxID_CANCEL -> ok = wxFileDialog:destroy(Dialog), State end. do_new2(Path, J, Frame) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Wallet"), [{size, {400, 250}}]), Sizer = wxBoxSizer:new(?wxVERTICAL), 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)), Label1 = J("Passphrase (optional)"), PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label1}]), PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]), _ = wxSizer:add(PassSz, PassTx, zxw:flags(wide)), Label2= J("Passphrase Confirmation"), PassConSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label2}]), PassConTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]), _ = wxSizer:add(PassConSz, PassConTx, 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, Network, zxw:flags(base)), _ = wxSizer:add(Sizer, NameSz, zxw:flags(base)), _ = wxSizer:add(Sizer, PassSz, zxw:flags(base)), _ = wxSizer:add(Sizer, PassConSz, zxw:flags(base)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxDialog:setSize(Dialog, {300, 270}), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:center(Dialog), ok = wxStyledTextCtrl:setFocus(NameTx), Result = case wxDialog:showModal(Dialog) of ?wxID_OK -> Net = case wxRadioBox:getSelection(Network) of 0 -> mainnet; 1 -> testnet; _ -> mainnet end, Name = case wxTextCtrl:getValue(NameTx) of "" -> Path; N -> N end, Pass = case {wxTextCtrl:getValue(PassTx), wxTextCtrl:getValue(PassConTx)} of {"", ""} -> none; {P, P} -> P; {_, _} -> bad end, do_new3(Net, Name, Path, Pass); ?wxID_CANCEL -> abort end, ok = wxDialog:destroy(Dialog), Result. do_new3(_, _, _, bad) -> abort; do_new3(Net, Name, Path, Pass) -> gd_con:new_wallet(Net, Name, Path, Pass). do_import(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = maps:get(dir, Prefs, zx_lib:path(var, "otpr", "gajudesk")), Options = [{message, J("Select Wallet File")}, {defaultDir, DefaultDir}, {wildCard, "*.gaju"}, {style, ?wxFD_OPEN}], Dialog = wxFileDialog:new(Frame, Options), case wxFileDialog:showModal(Dialog) of ?wxID_OK -> Dir = wxFileDialog:getDirectory(Dialog), File = wxFileDialog:getFilename(Dialog), ok = wxFileDialog:destroy(Dialog), case do_import2(Dir, File, J, Frame) of ok -> NewPrefs = maps:put(dir, Dir, Prefs), NewState = State#s{prefs = NewPrefs}, ok = do_close(NewState), NewState; abort -> State end; ?wxID_CANCEL -> ok = wxFileDialog:destroy(Dialog), State end. do_import2(_, "", _, _) -> abort; do_import2(Dir, File, J, Frame) -> Path = filename:join(Dir, File), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Import Wallet")), Sizer = wxBoxSizer:new(?wxVERTICAL), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)), PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Passphrase")}]), PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer: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)), _ = wxSizer:add(Sizer, NameSz, zxw:flags(base)), _ = wxSizer:add(Sizer, PassSz, zxw:flags(base)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:setSize(Dialog, {500, 200}), ok = wxFrame:center(Dialog), ok = wxStyledTextCtrl:setFocus(NameTx), Result = case wxDialog:showModal(Dialog) of ?wxID_OK -> Name = case wxTextCtrl:getValue(NameTx) of "" -> Path; N -> N end, Pass = case wxTextCtrl:getValue(PassTx) of "" -> none; P -> P end, gd_con:import_wallet(Name, Path, Pass); ?wxID_CANCEL -> abort end, ok = wxDialog:destroy(Dialog), Result. do_drop(State = #s{picker = Picker}) -> case wxListBox:getSelection(Picker) of -1 -> State; Selected -> do_drop(Selected + 1, State) end. do_drop(Selected, State = #s{j = J, frame = Frame, wallets = Wallets}) -> #wr{name = Name, path = Path} = lists:nth(Selected, Wallets), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Drop Wallet")), Sizer = wxBoxSizer:new(?wxVERTICAL), MessageM = J("REALLY delete wallet?"), Message = ["\r\n", MessageM, "\r\n\r\n\"", Name, "\""], MessageT = wxStaticText:new(Dialog, ?wxID_ANY, Message, [{style, ?wxALIGN_CENTRE_HORIZONTAL}]), DeleteM = J("Delete file data?"), DeleteCheck = wxCheckBox:new(Dialog, ?wxID_ANY, DeleteM), 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)), _ = wxSizer:add(Sizer, MessageT, zxw:flags(base)), _ = wxSizer:add(Sizer, DeleteCheck, zxw:flags(base)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:setSize(Dialog, {500, 200}), ok = wxFrame:center(Dialog), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> Delete = wxCheckBox:getValue(DeleteCheck), gd_con:drop_wallet(Path, Delete); ?wxID_CANCEL -> ok end, State. ensure_shown(Frame) -> case wxWindow:isShown(Frame) of true -> ok; false -> true = wxFrame:show(Frame), ok end.