%%% @doc %%% Clutch GUI %%% %%% This process is responsible for creating the main GUI frame displayed to the user. %%% %%% Reference: http://erlang.org/doc/man/wx_object.html %%% @end -module(gmc_gui). -vsn("0.1.0"). -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([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("gmc.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 = #{} :: #{atom() := term()}, accounts = [] :: [clutch:poa()], picker = none :: none | wx:wx_object(), id = {#w{}, #w{}} :: labeled(), balance = {#w{}, #w{}} :: labeled(), buttons = [] :: [widget()]}). -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(Message) -> wx_object:cast(?MODULE, {trouble, Message}). ask_password() -> wx_object:cast(?MODULE, password). %%% 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 = gmc_jt:read_translations(?MODULE), J = gmc_jt:j(Lang, Trans), Wx = wx:new(), Frame = wxFrame:new(Wx, ?wxID_ANY, J("Gajumaru Clutch")), 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}]), 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}, 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 | lists:map(MakeButton, ButtonTemplates)], ChainSz = wxBoxSizer:new(?wxHORIZONTAL), AccountSz = wxBoxSizer:new(?wxHORIZONTAL), DetailsSz = wxBoxSizer:new(?wxHORIZONTAL), ActionsSz = wxBoxSizer:new(?wxHORIZONTAL), HistorySz = wxBoxSizer:new(?wxVERTICAL), _ = wxSizer:add(ChainSz, WallB, zxw:flags(wide)), _ = wxSizer:add(ChainSz, ChainB, zxw:flags(wide)), _ = wxSizer:add(ChainSz, NodeB, zxw:flags(wide)), #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), _ = wxSizer:add(HistorySz, Refresh, zxw:flags(base)), _ = 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, HistorySz, [{proportion, 3}, {flag, ?wxEXPAND}]), ok = wxFrame:setSizer(Frame, MainSz), ok = wxSizer:layout(MainSz), ok = gmc_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, lang = Lang, j = J, prefs = Prefs, accounts = [], picker = Picker, id = ID_W, balance = Balance, buttons = Buttons}, {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_trouble(Info, State), {noreply, State}; handle_cast(password, State) -> ok = do_ask_password(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 = 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 = gmc_con:save(?MODULE, NewPrefs), 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}. 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 = gmc_con:refresh(), State. wallman(State) -> ok = gmc_con:show_ui(gmc_v_wallman), State. netman(State) -> ok = gmc_con:show_ui(gmc_v_netman), 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 = wxFrame:setSize(Dialog, {500, 200}), ok = wxFrame: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}, gmc_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", "Direct Input (no transform)"], 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}; 3 -> raw end, gmc_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), gmc_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} = gmc_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), Affirm = wxButton:new(Dialog, ?wxID_OK), _ = wxBoxSizer:add(ButtSz, Affirm, 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), ?wxID_OK = wxDialog:showModal(Dialog), 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(base)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxFrame:center(Dialog), ok = wxStyledTextCtrl:setFocus(NameTx), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> NewName = wxTextCtrl:getValue(NameTx), gmc_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 -> gmc_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), CB = wxClipboard:get(), ok = case wxClipboard:open(CB) of true -> Text = wxTextDataObject:new([{text, String}]), case wxClipboard:setData(CB, Text) of true -> R = wxClipboard:flush(CB), log(info, "Address 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, State. www(State = #s{id = {_, #w{wx = ID_T}}}) -> String = wxStaticText:getLabel(ID_T), URL = unicode:characters_to_list(["https://aescan.io/accounts/", String]), ok = case wx_misc:launchDefaultBrowser(URL) of true -> log(info, "Opened in browser: ~ts", [URL]); false -> log(info, "Failed to open browser: ~ts", [URL]) end, State. 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 gmc_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")}]), _ = wxStaticBoxSizer:add(AmtSz, AmtTx, 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 -> {ok, PK} = decode_account_id(ID), TX = #spend_tx{sender_id = aeser_id:create(account, PK), 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(ID, TX); ?wxID_CANCEL -> ok end, ok = wxDialog:destroy(Dialog), State. clean_spend(_, #spend_tx{recipient_id = ""}) -> ok; clean_spend(ID, TX = #spend_tx{recipient_id = S}) when is_list(S) -> case decode_account_id(S) of {ok, PK} -> clean_spend(ID, TX#spend_tx{recipient_id = aeser_id:create(account, PK)}); Error -> tell("Decode recipient_id failed with: ~tp", [Error]) end; clean_spend(ID, TX = #spend_tx{amount = S}) when is_list(S) -> case string_to_price(S) of {ok, Amount} -> clean_spend(ID, TX#spend_tx{amount = Amount}); {error, _} -> ok end; clean_spend(ID, TX = #spend_tx{gas_price = S}) when is_list(S) -> case is_int(S) of true -> clean_spend(ID, TX#spend_tx{gas_price = list_to_integer(S)}); false -> ok end; clean_spend(ID, TX = #spend_tx{gas = S}) when is_list(S) -> case is_int(S) of true -> clean_spend(ID, TX#spend_tx{gas = list_to_integer(S)}); false -> ok end; clean_spend(ID, TX = #spend_tx{payload = S}) when is_list(S) -> clean_spend(ID, TX#spend_tx{payload = list_to_binary(S)}); clean_spend(ID, TX) -> gmc_con:spend(ID, TX). decode_account_id(S) when is_list(S) -> decode_account_id(list_to_binary(S)); decode_account_id(B) -> try {account_pubkey, PK} = aeser_api_encoder:decode(B), {ok, PK} catch E:R -> {E, R} end. 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("Password")), 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), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, URL_Sz, 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(URL_Tx), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> case wxTextCtrl:getValue(URL_Tx) of "" -> ok; String -> gmc_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) -> #poa{id = ID, balances = Balances} = lists:nth(Selected + 1, Accounts), [#balance{total = Pucks}] = Balances, ok = wxStaticText:setLabel(I, ID), ok = wxStaticText:setLabel(B, price_to_string(Pucks)), NewPrefs = maps:put(selected, Selected, Prefs), State#s{prefs = NewPrefs}; do_selection(_, State) -> do_selection(0, State). do_show(Accounts, State = #s{prefs = Prefs, picker = Picker}) -> AKs = [Name || #poa{name = Name} <- Accounts], ok = wxListBox:set(Picker, AKs), case Accounts of [] -> 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. do_wallet(none, #s{buttons = Buttons}) -> #w{wx = WalletB} = lists:keyfind(wallet, #w.name, Buttons), ok = wxButton:setLabel(WalletB, "[no wallet]"); do_wallet(Name, #s{buttons = Buttons}) -> #w{wx = WalletB} = lists:keyfind(wallet, #w.name, Buttons), ok = wxButton:setLabel(WalletB, Name). 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). handle_trouble(Info, #s{frame = Frame}) -> zxw:show_message(Frame, Info). 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", "clutch"), "default.gaju")), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> Phrase = case wxTextCtrl:getValue(PassTx) of "" -> none; P -> P end, gmc_con:open_wallet(Path, Phrase); ?wxID_CANCEL -> gmc_con:open_wallet(Path, none) end, wxDialog:destroy(Dialog). %%% 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.