%%% @doc %%% GajuDesk GUI %%% @end -module(gd_gui). -vsn("0.6.6"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). -behavior(wx_object). -include_lib("wx/include/wx.hrl"). -export([show/1, wallet/1, chain/2, trouble/1, ask_password/0]). -export([grids_mess_sig/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"). -include("gd.hrl"). -record(w, {name = none :: atom(), id = 0 :: integer(), wx = none :: none | wx:wx_object()}). -record(h, {win = none :: none | wx:wx_object(), sz = none :: none | wx:wx_object()}). -record(s, {wx = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(), sizer = none :: none | wx:wx_object(), lang = en_US :: en_US | ja_JP, j = none :: none | fun(), prefs = #{} :: #{atom() := term()}, accounts = [] :: [gajudesk:poa()], picker = none :: none | wx:wx_object(), id = {#w{}, #w{}} :: labeled(), balance = {#w{}, #w{}} :: labeled(), buttons = [] :: [widget()], history = #h{} :: #h{}}). -type state() :: term(). -type widget() :: #w{}. -type labeled() :: {Label :: #w{}, Control :: #w{}}. %%% Interface functions show(Accounts) -> wx_object:cast(?MODULE, {show, Accounts}). wallet(Name) -> wx_object:cast(?MODULE, {wallet, Name}). chain(ChainID, Node) -> wx_object:cast(?MODULE, {chain, ChainID, Node}). trouble(Info) -> wx_object:cast(?MODULE, {trouble, Info}). ask_password() -> wx_object:cast(?MODULE, password). grids_mess_sig(Request) -> wx_object:cast(?MODULE, {grids_mess_sig, Request}). %%% Startup Functions start_link(Accounts) -> wx_object:start_link({local, ?MODULE}, ?MODULE, Accounts, []). 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), AppName = J("GajuDesk"), VSN = proplists:get_value(vsn, ?MODULE:module_info(attributes)), Wx = wx:new(), Frame = wxFrame:new(Wx, ?wxID_ANY, AppName ++ " v" ++ VSN), MainSz = wxBoxSizer:new(?wxVERTICAL), Picker = wxListBox:new(Frame, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]), WallB = wxButton:new(Frame, ?wxID_ANY, [{label, "[none]"}, {style, ?wxBORDER_NONE}]), WallW = #w{name = wallet, id = wxButton:getId(WallB), wx = WallB}, ChainB = wxButton:new(Frame, ?wxID_ANY, [{label, "[ChainID]"}, {style, ?wxBORDER_NONE}]), _ = wxButton:disable(ChainB), ChainW = #w{name = chain, id = wxButton:getId(ChainB), wx = ChainB}, NodeB = wxButton:new(Frame, ?wxID_ANY, [{label, "[Node]"}, {style, ?wxBORDER_NONE}]), NodeW = #w{name = node, id = wxButton:getId(NodeB), wx = NodeB}, DevB = wxButton:new(Frame, ?wxID_ANY, [{label, "𝑓 () →"}]), DevW = #w{name = dev, id = wxButton:getId(DevB), wx = DevB}, ID_L = wxStaticText:new(Frame, ?wxID_ANY, J("Account ID: ")), ID_T = wxStaticText:new(Frame, ?wxID_ANY, ""), ID_W = {#w{id = wxStaticText:getId(ID_L), wx = ID_L}, #w{id = wxStaticText:getId(ID_T), wx = ID_T}}, ID_Sz = wxBoxSizer:new(?wxHORIZONTAL), _ = wxSizer:add(ID_Sz, ID_L, zxw:flags(base)), _ = wxSizer:add(ID_Sz, ID_T, zxw:flags(wide)), BalanceL = wxStaticText:new(Frame, ?wxID_ANY, "木"), BalanceT = wxStaticText:new(Frame, ?wxID_ANY, price_to_string(0)), Balance = {#w{id = wxStaticText:getId(BalanceL), wx = BalanceL}, #w{id = wxStaticText:getId(BalanceT), wx = BalanceT}}, BalanceSz = wxBoxSizer:new(?wxHORIZONTAL), _ = wxSizer:add(BalanceSz, BalanceL, zxw:flags(base)), _ = wxSizer:add(BalanceSz, BalanceT, zxw:flags(wide)), NumbersSz = wxBoxSizer:new(?wxVERTICAL), _ = wxSizer:add(NumbersSz, ID_Sz, zxw:flags(wide)), _ = wxSizer:add(NumbersSz, BalanceSz, zxw:flags(wide)), ButtonTemplates = [{make_key, J("Create")}, {recover, J("Recover")}, {mnemonic, J("Mnemonic")}, {rename, J("Rename")}, {drop_key, J("Delete")}, {send, J("Send Money")}, % {recv, J("Receive Money")}, {grids, J("GRIDS URL")}, {copy, J("Copy")}, {www, J("WWW")}, {refresh, J("Refresh")}], MakeButton = fun({Name, Label}) -> B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]), #w{name = Name, id = wxButton:getId(B), wx = B} end, Buttons = [WallW, ChainW, NodeW, DevW | lists:map(MakeButton, ButtonTemplates)], Disable = fun(Button) -> #w{wx = W} = lists:keyfind(Button, #w.name, Buttons), wxButton:disable(W) end, ok = lists:foreach(Disable, key_buttons()), ChainSz = wxBoxSizer:new(?wxHORIZONTAL), AccountSz = wxBoxSizer:new(?wxHORIZONTAL), DetailsSz = wxBoxSizer:new(?wxHORIZONTAL), ActionsSz = wxBoxSizer:new(?wxHORIZONTAL), _ = wxSizer:add(ChainSz, WallB, zxw:flags(wide)), _ = wxSizer:add(ChainSz, ChainB, zxw:flags(wide)), _ = wxSizer:add(ChainSz, NodeB, zxw:flags(wide)), _ = wxSizer:add(ChainSz, DevB, zxw:flags(base)), #w{wx = CopyBn} = lists:keyfind(copy, #w.name, Buttons), #w{wx = WWW_Bn} = lists:keyfind(www, #w.name, Buttons), _ = wxSizer:add(DetailsSz, NumbersSz, zxw:flags(wide)), _ = wxSizer:add(DetailsSz, CopyBn, zxw:flags(base)), _ = wxSizer:add(DetailsSz, WWW_Bn, zxw:flags(base)), #w{wx = MakeBn} = lists:keyfind(make_key, #w.name, Buttons), #w{wx = RecoBn} = lists:keyfind(recover, #w.name, Buttons), #w{wx = MnemBn} = lists:keyfind(mnemonic, #w.name, Buttons), #w{wx = Rename} = lists:keyfind(rename, #w.name, Buttons), #w{wx = DropBn} = lists:keyfind(drop_key, #w.name, Buttons), _ = wxSizer:add(AccountSz, MakeBn, zxw:flags(wide)), _ = wxSizer:add(AccountSz, RecoBn, zxw:flags(wide)), _ = wxSizer:add(AccountSz, MnemBn, zxw:flags(wide)), _ = wxSizer:add(AccountSz, Rename, zxw:flags(wide)), _ = wxSizer:add(AccountSz, DropBn, zxw:flags(wide)), #w{wx = SendBn} = lists:keyfind(send, #w.name, Buttons), % #w{wx = RecvBn} = lists:keyfind(recv, #w.name, Buttons), #w{wx = GridsBn} = lists:keyfind(grids, #w.name, Buttons), _ = wxSizer:add(ActionsSz, SendBn, zxw:flags(wide)), % _ = wxSizer:add(ActionsSz, RecvBn, zxw:flags(wide)), _ = wxSizer:add(ActionsSz, GridsBn, zxw:flags(wide)), #w{wx = Refresh} = lists:keyfind(refresh, #w.name, Buttons), % HistoryWin = wxScrolledWindow:new(Frame), % HistorySz = wxBoxSizer:new(?wxVERTICAL), % ok = wxScrolledWindow:setSizerAndFit(HistoryWin, HistorySz), % ok = wxScrolledWindow:setScrollRate(HistoryWin, 5, 5), _ = wxSizer:add(MainSz, ChainSz, zxw:flags(base)), _ = wxSizer:add(MainSz, AccountSz, zxw:flags(base)), _ = wxSizer:add(MainSz, Picker, zxw:flags(wide)), _ = wxSizer:add(MainSz, DetailsSz, zxw:flags(base)), _ = wxSizer:add(MainSz, ActionsSz, zxw:flags(base)), _ = wxSizer:add(MainSz, Refresh, zxw:flags(base)), % _ = wxSizer:add(MainSz, HistoryWin, zxw:flags(wide)), ok = wxFrame:setSizer(Frame, MainSz), ok = wxSizer:layout(MainSz), ok = gd_v:safe_size(Frame, Prefs), ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:connect(Frame, close_window), true = wxFrame:show(Frame), ok = wxListBox:connect(Picker, command_listbox_selected), State = #s{wx = Wx, frame = Frame, sizer = MainSz, lang = Lang, j = J, prefs = Prefs, accounts = [], picker = Picker, id = ID_W, balance = Balance, buttons = Buttons}, % history = #h{win = HistoryWin, sz = HistorySz}}, {Frame, State}. -spec handle_call(Message, From, State) -> Result when Message :: term(), From :: {pid(), reference()}, State :: state(), Result :: {reply, Response, NewState} | {noreply, State}, Response :: ok | {error, {listening, inet:port_number()}}, NewState :: state(). handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. -spec handle_cast(Message, State) -> {noreply, NewState} when Message :: term(), State :: state(), NewState :: state(). %% @private %% The gen_server:handle_cast/2 callback. %% See: http://erlang.org/doc/man/gen_server.html#Module:handle_cast-2 handle_cast({show, Accounts}, State) -> NewState = do_show(Accounts, State), {noreply, NewState}; handle_cast({wallet, Name}, State) -> ok = do_wallet(Name, State), {noreply, State}; handle_cast({chain, ChainID, Node}, State) -> ok = do_chain(ChainID, Node, State), {noreply, State}; handle_cast({trouble, Info}, State) -> ok = handle_troubling(State, Info), {noreply, State}; handle_cast(password, State) -> ok = do_ask_password(State), {noreply, State}; handle_cast({grids_mess_sig, Request}, State) -> ok = do_grids_mess_sig(Request, State), {noreply, State}; handle_cast(Unexpected, State) -> ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. -spec handle_info(Message, State) -> {noreply, NewState} when Message :: term(), State :: state(), NewState :: state(). %% @private %% The gen_server:handle_info/2 callback. %% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2 handle_info(Unexpected, State) -> ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), {noreply, State}. -spec handle_event(Event, State) -> {noreply, NewState} when Event :: term(), State :: state(), NewState :: state(). %% @private %% The wx_object:handle_event/2 callback. %% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2 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 = wallet} -> wallman(State); #w{name = chain} -> netman(State); #w{name = dev} -> devman(State); #w{name = node} -> set_node(State); #w{name = make_key} -> make_key(State); #w{name = recover} -> recover_key(State); #w{name = mnemonic} -> show_mnemonic(State); #w{name = rename} -> rename_key(State); #w{name = drop_key} -> drop_key(State); #w{name = copy} -> copy(State); #w{name = www} -> www(State); #w{name = send} -> spend(State); #w{name = grids} -> grids_dialogue(State); #w{name = refresh} -> refresh(State); #w{name = Name} -> handle_button(Name, State); false -> State end, {noreply, NewState}; handle_event(#wx{event = #wxCommand{type = command_listbox_selected, commandInt = Selected}}, State) -> NewState = do_selection(Selected, State), {noreply, NewState}; handle_event(#wx{event = #wxClose{}}, State = #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 = gd_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(wx_deleted, _) -> wx:destroy(); terminate(Reason, State) -> ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), wx:destroy(). %%% Doers refresh(State) -> ok = gd_con:refresh(), State. wallman(State) -> ok = gd_con:show_ui(gd_v_wallman), State. netman(State) -> ok = gd_con:show_ui(gd_v_netman), State. devman(State) -> ok = gd_con:show_ui(gd_v_devman), State. set_node(State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Set Node")), Sizer = wxBoxSizer:new(?wxVERTICAL), AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address")}]), AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer:add(AddressSz, AddressTx, zxw:flags(wide)), PortSz = wxBoxSizer:new(?wxHORIZONTAL), Labels = [J("External"), J("Internal"), J("Rosetta"), J("Channel"), J("Middleware")], MakePortCtrl = fun(L) -> Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, L}]), Tx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer:add(Sz, Tx, zxw:flags(wide)), _ = wxSizer:add(PortSz, Sz, zxw:flags(wide)), Tx end, PortCtrls = lists:map(MakePortCtrl, Labels), 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, AddressSz, zxw:flags(base)), _ = wxSizer:add(Sizer, PortSz, zxw:flags(base)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 200}), ok = wxDialog:center(Dialog), ok = wxStyledTextCtrl:setFocus(AddressTx), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> case wxTextCtrl:getValue(AddressTx) of "" -> ok; Address -> add_node2(Address, PortCtrls) end; ?wxID_CANCEL -> ok end, ok = wxDialog:destroy(Dialog), State. add_node2(Address, PortCtrls) -> IP = case inet:parse_address(Address) of {ok, N} -> N; {error, einval} -> Address end, [E, I, R, C, M] = lists:map(fun numerify_port/1, PortCtrls), New = #node{ip = IP, external = E, internal = I, rosetta = R, channel = C, mdw = M}, gd_con:set_sole_node(New). numerify_port(Ctrl) -> case wxTextCtrl:getValue(Ctrl) of "" -> none; String -> case s_to_i(String) of {ok, PortNum} -> PortNum; error -> none end end. s_to_i(S) -> try I = list_to_integer(S), {ok, I} catch error:badarg -> error end. make_key(State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key")), Sizer = wxBoxSizer:new(?wxVERTICAL), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name (Optional)")}]), NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), SeedSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Seed (Optional)")}]), SeedTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxDEFAULT bor ?wxTE_MULTILINE}]), ok = wxTextCtrl:setValue(SeedTx, base64:encode(crypto:strong_rand_bytes(64))), OptionsSz = wxStaticBoxSizer:new(?wxHORIZONTAL, Dialog, [{label, J("Options")}]), Encodings = ["Base64", "UTF-8", "Base58"], EncodingOptions = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Encodings}]), ok = wxChoice:setSelection(EncodingOptions, 0), Transforms = ["SHA-3 (256)", "SHA-2 (256)", "XOR"], TransformOptions = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Transforms}]), ok = wxChoice:setSelection(TransformOptions, 0), 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)), _ = wxStaticBoxSizer:add(NameSz, NameTx, zxw:flags(base)), _ = wxStaticBoxSizer:add(SeedSz, SeedTx, zxw:flags(wide)), _ = wxStaticBoxSizer:add(OptionsSz, EncodingOptions, zxw:flags(wide)), _ = wxStaticBoxSizer:add(OptionsSz, TransformOptions, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, NameSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, SeedSz, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, OptionsSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:setSize(Dialog, {400, 300}), ok = wxFrame:center(Dialog), ok = wxStyledTextCtrl:setFocus(NameTx), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> Type = {eddsa, ed25519}, Size = 256, Name = wxTextCtrl:getValue(NameTx), Seed = wxTextCtrl:getValue(SeedTx), Encoding = case wxChoice:getSelection(EncodingOptions) of 0 -> base64; 1 -> utf8; 2 -> base58 end, Transform = case wxChoice:getSelection(TransformOptions) of 0 -> {sha3, 256}; 1 -> {sha2, 256}; 2 -> {x_or, 256} end, gd_con:make_key(Type, Size, Name, Seed, Encoding, Transform); ?wxID_CANCEL -> ok end, ok = wxDialog:destroy(Dialog), State. recover_key(State = #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")}]), 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, 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_OK -> Mnemonic = wxTextCtrl:getValue(MnemTx), gd_con:recover_key(Mnemonic); ?wxID_CANCEL -> ok end, ok = wxDialog:destroy(Dialog), State. show_mnemonic(State = #s{accounts = []}) -> State; show_mnemonic(State = #s{picker = Picker}) -> case wxListBox:getSelection(Picker) of -1 -> State; Selected -> show_mnemonic(Selected + 1, State) end. show_mnemonic(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) -> #poa{id = ID} = lists:nth(Selected, Accounts), {ok, Mnemonic} = gd_con:mnemonic(ID), 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), State. rename_key(State = #s{accounts = []}) -> State; rename_key(State = #s{picker = Picker}) -> case wxListBox:getSelection(Picker) of -1 -> State; Selected -> rename_key(Selected + 1, State) end. rename_key(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) -> #poa{id = ID, name = Name} = lists:nth(Selected, Accounts), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key")), Sizer = wxBoxSizer:new(?wxVERTICAL), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name (Optional)")}]), NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), ok = wxTextCtrl:setValue(NameTx, Name), _ = wxStaticBoxSizer:add(NameSz, NameTx, zxw:flags(base)), 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, NameSz, 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(NameTx), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> NewName = wxTextCtrl:getValue(NameTx), gd_con:rename_key(ID, NewName); ?wxID_CANCEL -> ok end, ok = wxDialog:destroy(Dialog), State. drop_key(State = #s{accounts = []}) -> State; drop_key(State = #s{picker = Picker}) -> case wxListBox:getSelection(Picker) of -1 -> State; Selected -> drop_key(Selected + 1, State) end. drop_key(Selected, State = #s{frame = Frame, j = J, accounts = Accounts, prefs = Prefs}) -> #poa{id = ID, name = Name} = lists:nth(Selected, Accounts), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key"), [{size, {500, 150}}]), Sizer = wxBoxSizer:new(?wxVERTICAL), Message = ["REALLY delete key?\r\n\r\n\"", Name, "\"\r\n(", ID, ")"], MessageT = wxStaticText:new(Dialog, ?wxID_ANY, Message, [{style, ?wxALIGN_CENTRE_HORIZONTAL}]), 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, MessageT, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:center(Dialog), NewPrefs = case wxDialog:showModal(Dialog) of ?wxID_OK -> gd_con:drop_key(ID), case Selected =:= length(Accounts) of true -> maps:put(selected, Selected - 2, Prefs); false -> Prefs end; ?wxID_CANCEL -> Prefs end, ok = wxDialog:destroy(Dialog), State#s{prefs = NewPrefs}. copy(State = #s{id = {_, #w{wx = ID_T}}}) -> String = wxStaticText:getLabel(ID_T), ok = copy_to_clipboard(String), State. 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. www(State = #s{id = {_, #w{wx = ID_T}}, j = J}) -> case wxStaticText:getLabel(ID_T) of "" -> ok = handle_troubling(State, J("No key selected.")), State; ID -> ok = www2(State, ID), State end. www2(State = #s{j = J}, AccountID) -> case gd_con:network() of {ok, ChainID} -> URL = unicode:characters_to_list(["https://", ChainID, ".gajumaru.io/account/", AccountID]), case wx_misc:launchDefaultBrowser(URL) of true -> log(info, "Opened in browser: ~ts", [URL]); false -> log(info, "Failed to open browser: ~ts", [URL]) end; none -> handle_troubling(State, J("No chain assigned.")) end. spend(State = #s{accounts = []}) -> State; spend(State = #s{picker = Picker}) -> case wxListBox:getSelection(Picker) of -1 -> State; Selected -> spend(Selected + 1, State) end. spend(Selected, State = #s{accounts = Accounts}) -> POA = #poa{id = ID} = lists:nth(Selected, Accounts), case gd_con:nonce(ID) of {ok, Nonce} -> {ok, #{"top_block_height" := Height}} = hz:status(), spend2(POA, Nonce, Height, State); {error, Reason} -> tell("spend/2 failed to get nonce with ~tp", [Reason]), State end. spend2(#poa{id = ID, name = Name}, Nonce, Height, State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Transfer"), [{size, {500, 400}}]), Sizer = wxBoxSizer:new(?wxVERTICAL), Account = [Name, " (", ID, ")"], FromTx = wxStaticText:new(Dialog, ?wxID_ANY, Account), FromSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("From")}]), _ = wxStaticBoxSizer:add(FromSz, FromTx, zxw:flags(wide)), ToTx = wxTextCtrl:new(Dialog, ?wxID_ANY), ToSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("To")}]), _ = wxStaticBoxSizer:add(ToSz, ToTx, zxw:flags(wide)), AmtTx = wxTextCtrl:new(Dialog, ?wxID_ANY), AmtSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Amount")}]), AmtInSz = wxBoxSizer:new(?wxHORIZONTAL), AmtLabel = wxStaticText:new(Dialog, ?wxID_ANY, "木"), _ = wxStaticBoxSizer:add(AmtInSz, AmtLabel, zxw:flags(base)), _ = wxStaticBoxSizer:add(AmtInSz, AmtTx, zxw:flags(wide)), _ = wxStaticBoxSizer:add(AmtSz, AmtInSz, zxw:flags(wide)), DataTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_MULTILINE}]), DataSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Message (optional)")}]), _ = wxStaticBoxSizer:add(DataSz, DataTx, zxw:flags(wide)), Style = [{style, ?wxSL_HORIZONTAL bor ?wxSL_LABELS}], TTL_Sl = wxSlider:new(Dialog, ?wxID_ANY, 100, 10, 1000, Style), TTL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("TTL")}]), _ = wxStaticBoxSizer:add(TTL_Sz, TTL_Sl, zxw:flags(wide)), Min = hz:min_gas_price(), Max = Min * 2, GasSl = wxSlider:new(Dialog, ?wxID_ANY, Min, Min, Max, Style), GasSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Gas Price")}]), _ = wxStaticBoxSizer:add(GasSz, GasSl, 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, FromSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, ToSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, AmtSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, DataSz, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, TTL_Sz, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, GasSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:setSize(Dialog, {500, 450}), ok = wxFrame:center(Dialog), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> TX = #spend_tx{sender_id = ID, recipient_id = wxTextCtrl:getValue(ToTx), amount = wxTextCtrl:getValue(AmtTx), gas_price = wxSlider:getValue(GasSl), gas = 20000, ttl = Height + wxSlider:getValue(TTL_Sl), nonce = Nonce, payload = wxTextCtrl:getValue(DataTx)}, clean_spend(TX); ?wxID_CANCEL -> ok end, ok = wxDialog:destroy(Dialog), State. clean_spend(#spend_tx{recipient_id = ""}) -> ok; clean_spend( TX = #spend_tx{amount = S}) when is_list(S) -> case string_to_price(S) of {ok, Amount} -> clean_spend(TX#spend_tx{amount = Amount}); {error, _} -> ok end; clean_spend(TX = #spend_tx{gas_price = S}) when is_list(S) -> case is_int(S) of true -> clean_spend(TX#spend_tx{gas_price = list_to_integer(S)}); false -> ok end; clean_spend(TX = #spend_tx{gas = S}) when is_list(S) -> case is_int(S) of true -> clean_spend(TX#spend_tx{gas = list_to_integer(S)}); false -> ok end; clean_spend(TX = #spend_tx{payload = S}) when is_list(S) -> clean_spend(TX#spend_tx{payload = list_to_binary(S)}); clean_spend(TX) -> gd_con:spend(TX). is_int(S) -> lists:all(fun(C) -> $0 =< C andalso C =< $9 end, S). grids_dialogue(State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("GRIDS URL")), Sizer = wxBoxSizer:new(?wxVERTICAL), Label = J("GRIDS URL"), URL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J(Label)}]), URL_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxStaticBoxSizer:add(URL_Sz, URL_Tx, 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, URL_Sz, 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(URL_Tx), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> case wxTextCtrl:getValue(URL_Tx) of "" -> ok; String -> gd_con:grids(String) end; ?wxID_CANCEL -> ok end, State. handle_button(Name, State) -> ok = tell("Button Click: ~p", [Name]), State. do_selection(Selected, State = #s{prefs = Prefs, accounts = Accounts, balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}}) when Selected < length(Accounts) -> OneBasedIndex = Selected + 1, #poa{id = ID, balances = Balances} = lists:nth(OneBasedIndex, Accounts), [#balance{total = Pucks}] = Balances, ok = wxStaticText:setLabel(I, ID), ok = wxStaticText:setLabel(B, price_to_string(Pucks)), ok = gd_con:selected(OneBasedIndex), NewPrefs = maps:put(selected, Selected, Prefs), State#s{prefs = NewPrefs}; do_selection(_, State) -> do_selection(0, State). do_show(Accounts, State = #s{sizer = Sizer, prefs = Prefs, picker = Picker}) -> AKs = [Name || #poa{name = Name} <- Accounts], ok = wxListBox:set(Picker, AKs), NewState = case Accounts of [] -> clear_account(State); [_] -> Selected = 0, ok = wxListBox:setSelection(Picker, Selected), do_selection(Selected, State#s{accounts = Accounts}); _ -> Selected = maps:get(selected, Prefs, 0), ok = wxListBox:setSelection(Picker, Selected), do_selection(Selected, State#s{accounts = Accounts}) end, ok = wxSizer:layout(Sizer), NewState. clear_account(State = #s{balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}}) -> ok = wxStaticText:setLabel(I, ""), ok = wxStaticText:setLabel(B, ""), State. do_wallet(none, #s{buttons = Buttons}) -> #w{wx = WalletB} = lists:keyfind(wallet, #w.name, Buttons), Disable = fun(Button) -> #w{wx = W} = lists:keyfind(Button, #w.name, Buttons), wxButton:disable(W) end, ok = lists:foreach(Disable, key_buttons()), ok = wxButton:setLabel(WalletB, "[no wallet]"); do_wallet(Name, #s{buttons = Buttons}) -> #w{wx = WalletB} = lists:keyfind(wallet, #w.name, Buttons), Enable = fun(Button) -> #w{wx = W} = lists:keyfind(Button, #w.name, Buttons), wxButton:enable(W) end, ok = lists:foreach(Enable, key_buttons()), ok = wxButton:setLabel(WalletB, Name). key_buttons() -> [make_key, recover, mnemonic, rename, drop_key]. do_chain(none, none, #s{buttons = Buttons}) -> #w{wx = ChainB} = lists:keyfind(chain, #w.name, Buttons), #w{wx = NodeB} = lists:keyfind(node, #w.name, Buttons), ok = wxButton:setLabel(ChainB, "[no chain]"), ok = wxButton:setLabel(NodeB, "[no node]"); do_chain(ChainID, #node{ip = IP}, #s{buttons = Buttons}) -> #w{wx = ChainB} = lists:keyfind(chain, #w.name, Buttons), #w{wx = NodeB} = lists:keyfind(node, #w.name, Buttons), Address = case inet:is_ip_address(IP) of true -> inet:ntoa(IP); false -> IP end, Label = unicode:characters_to_list(ChainID), ok = wxButton:setLabel(ChainB, Label), ok = wxButton:setLabel(NodeB, Address). do_ask_password(#s{frame = Frame, prefs = Prefs, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Password")), Sizer = wxBoxSizer:new(?wxVERTICAL), Label = J("Password (leave blank for no password)"), PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label}]), PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = 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(base)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:center(Dialog), ok = wxStyledTextCtrl:setFocus(PassTx), Path = maps:get(last, Prefs, filename:join(zx_lib:path(var, "otpr", "gajudesk"), "default.gaju")), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> Phrase = case wxTextCtrl:getValue(PassTx) of "" -> none; P -> P end, gd_con:open_wallet(Path, Phrase); ?wxID_CANCEL -> gd_con:open_wallet(Path, none) end, wxDialog:destroy(Dialog). do_grids_mess_sig(_, #s{accounts = []}) -> ok; do_grids_mess_sig(Request = #{"public_id" := false}, State = #s{picker = Picker, accounts = Accounts}) -> case wxListBox:getSelection(Picker) of -1 -> ok; Selected -> #poa{id = ID} = lists:nth(Selected + 1, Accounts), do_grids_mess_sig2(Request#{"public_id" := ID}, State) end; do_grids_mess_sig(Request = #{"public_id" := ID}, State = #s{accounts = Accounts}) -> BinID = list_to_binary(ID), case lists:keymember(BinID, #poa.id, Accounts) of true -> do_grids_mess_sig2(Request#{"public_id" := BinID}, State); false -> tell("Request sig from ID we don't have: ~p", [BinID]), tell("Accounts: ~p", [Accounts]) end. do_grids_mess_sig2(Request = #{"grids" := 1, "type" := "message", "url" := URL, "public_id" := ID, "payload" := Message}, #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Message Signature Request")), Sizer = wxBoxSizer:new(?wxVERTICAL), Instruction = J("The server at the URL below is requesting you sign the following message."), InstTx = wxStaticText:new(Dialog, ?wxID_ANY, Instruction), AcctSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Account")}]), AcctTx = wxStaticText:new(Dialog, ?wxID_ANY, ID), _ = wxStaticBoxSizer:add(AcctSz, AcctTx, zxw:flags(wide)), URL_Label = J("Originating URL"), URL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, URL_Label}]), URL_Tx = wxStaticText:new(Dialog, ?wxID_ANY, URL), _ = wxStaticBoxSizer:add(URL_Sz, URL_Tx, zxw:flags(wide)), MessSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Message")}]), MessStyle = ?wxTE_MULTILINE bor ?wxTE_READONLY, MessTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{value, Message}, {style, MessStyle}]), _ = wxStaticBoxSizer:add(MessSz, MessTx, 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, InstTx, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, AcctSz, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, URL_Sz, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, MessSz, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxDialog:setSize(Dialog, {500, 500}), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:center(Dialog), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> gd_con:sign_mess(Request); ?wxID_CANCEL -> ok end, wxDialog:destroy(Dialog); do_grids_mess_sig2(Request = #{"grids" := 1, "type" := "tx", "url" := URL, "chain" := Chain, "network_id" := NetID, "public_id" := ID, "payload" := CallData}, #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Message Signature Request")), Sizer = wxBoxSizer:new(?wxVERTICAL), Title = J("Transaction Signature Request"), TitleTx = wxStaticText:new(Dialog, ?wxID_ANY, Title), Big = wxFont:new(20, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Sans"}]), true = wxStaticText:setFont(TitleTx, Big), LabeledSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Details")}]), DetailSz = wxFlexGridSizer:new(4, 2, 5, 5), Details = [J("Account"), ID, J("Chain"), Chain, J("Network ID"), NetID, J("Originating URL"), URL], AddDetail = fun(D) -> T = wxStaticText:new(Dialog, ?wxID_ANY, D), _ = wxFlexGridSizer:add(DetailSz, T) end, ok = lists:foreach(AddDetail, Details), _ = wxStaticBoxSizer:add(LabeledSz, DetailSz, zxw:flags(wide)), DataLabel = wxStaticText:new(Dialog, ?wxID_ANY, J("TX Data")), DataStyle = ?wxTE_MULTILINE bor ?wxTE_READONLY, DataTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{value, CallData}, {style, DataStyle}]), 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, TitleTx, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, LabeledSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, DataLabel, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, DataTx, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxDialog:setSize(Dialog, {700, 400}), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:center(Dialog), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> gd_con:sign_tx(Request); ?wxID_CANCEL -> ok end, wxDialog:destroy(Dialog); do_grids_mess_sig2(BadRequest, _) -> tell("Bad request: ~tp", [BadRequest]). %%% Helpers price_to_string(Pucks) -> Gaju = 1_000_000_000_000_000_000, H = integer_to_list(Pucks div Gaju), R = Pucks rem Gaju, case string:strip(lists:flatten(io_lib:format("~18..0w", [R])), right, $0) of [] -> H; T -> string:join([H, T], ".") end. string_to_price(String) -> case string:split(String, ".") of [H] -> join_price(H, "0"); [H, T] -> join_price(H, T); _ -> {error, bad_price} end. join_price(H, T) -> try Parts = [H, string:pad(T, 18, trailing, $0)], Price = list_to_integer(unicode:characters_to_list(Parts)), case Price < 0 of false -> {ok, Price}; true -> {error, negative_price} end catch error:R -> {error, R} end.