diff --git a/src/gd_gui.erl b/src/gd_gui.erl index 033de68..e152c2a 100644 --- a/src/gd_gui.erl +++ b/src/gd_gui.erl @@ -38,7 +38,7 @@ accounts = [] :: [gajudesk:poa()], picker = none :: none | wx:wx_object(), id = {#w{}, #w{}} :: labeled(), - balance = {#w{}, #w{}} :: labeled(), + balance = #w{} :: #w{}, buttons = [] :: [widget()], history = #h{} :: #h{}}). @@ -117,13 +117,9 @@ init(Prefs) -> _ = wxSizer:add(ID_Sz, ID_L, zxw:flags(base)), _ = wxSizer:add(ID_Sz, ID_T, zxw:flags(wide)), - BalanceL = wxStaticText:new(Panel, ?wxID_ANY, "木"), - BalanceT = wxStaticText:new(Panel, ?wxID_ANY, price_to_string(0)), - Balance = - {#w{id = wxStaticText:getId(BalanceL), wx = BalanceL}, - #w{id = wxStaticText:getId(BalanceT), wx = BalanceT}}, + BalanceT = wxStaticText:new(Panel, ?wxID_ANY, hz_format:amount(0)), + Balance = #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), @@ -729,22 +725,16 @@ spend(Selected, State = #s{accounts = Accounts}) -> end. spend2(#poa{id = ID, name = Name}, Nonce, Height, State = #s{frame = Frame, j = J}) -> - Title = J("Transfer Gajus"), Account = [Name, " (", ID, ")"], - Args = {Account, Nonce, Height}, - Labels = {J("From"), J("To"), J("Amount"), J("Message (optional)"), J("TTL"), J("Gas Price")}, + Args = {Account, J}, ok = - case gd_m_spend:show(Frame, Title, Args, Labels) of - {ok, RecipientID, Amount, GasPrice, TTL, Payload} -> + case gd_m_spend:show(Frame, Args) of + {ok, Partial = #spend_tx{ttl = TTL}} -> TX = - #spend_tx{sender_id = ID, - recipient_id = RecipientID, - amount = Amount, - gas_price = GasPrice, - gas = 20000, - ttl = Height + TTL, - nonce = Nonce, - payload = Payload}, + Partial#spend_tx{sender_id = ID, + gas = 20000, + ttl = Height + TTL, + nonce = Nonce}, gd_con:spend(TX); cancel -> ok @@ -770,13 +760,13 @@ handle_button(Name, State) -> do_selection(Selected, State = #s{prefs = Prefs, accounts = Accounts, - balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}}) + 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 = wxStaticText:setLabel(B, hz_format:amount(Pucks)), ok = gd_con:selected(OneBasedIndex), NewPrefs = maps:put(selected, Selected, Prefs), State#s{prefs = NewPrefs}; @@ -803,7 +793,7 @@ do_show(Accounts, State = #s{sizer = Sizer, prefs = Prefs, picker = Picker}) -> ok = wxSizer:layout(Sizer), NewState. -clear_account(State = #s{balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}}) -> +clear_account(State = #s{balance = #w{wx = B}, id = {_, #w{wx = I}}}) -> ok = wxStaticText:setLabel(I, ""), ok = wxStaticText:setLabel(B, ""), State. diff --git a/src/gd_m_spend.erl b/src/gd_m_spend.erl index 5040444..66b56ef 100644 --- a/src/gd_m_spend.erl +++ b/src/gd_m_spend.erl @@ -4,7 +4,7 @@ %%% Interprets [ENTER] as an "OK" button click and [ESC] as a "Cancel" button click. %%% @end --module(gd_m_wallet_importer). +-module(gd_m_spend). -vsn("0.8.0"). -author("Craig Everett "). -copyright("Craig Everett "). @@ -12,7 +12,7 @@ -behavior(zxw_modal). --export([show/6]). +-export([show/2]). -export([init/1, handle_info/2, handle_event/2]). -include_lib("wx/include/wx.hrl"). @@ -23,145 +23,139 @@ {frame = none :: none | wx:wx_object(), parent = none :: none | wx:wx_object(), caller = none :: none | pid(), + j = j() :: fun(), to_tx = none :: none | wx:wx_object(), amount_tx = none :: none | wx:wx_object(), payload_tx = none :: none | wx:wx_object(), ttl_sl = none :: none | wx:wx_object(), gas_sl = none :: none | wx:wx_object(), - ok = none :: none | wx:wx_object(), + affirm = none :: none | wx:wx_object(), cancel = none :: none | wx:wx_object()}). +j() -> + fun(X) -> X end. + %%% Interface --spec show(Parent, Title, Args, Labels) -> {ok, ReceipientID, Amount, GasPrice, TTL, Payload} | cancel +-spec show(Parent, Args) -> {ok, #spend_tx{}} | cancel when Parent :: wxFrame:wxFrame(), - Title :: string(), Args :: {Account :: iolist(), Nonce :: pos_integer(), - Height :: pos_integer()}, - Labels :: {FromL :: string(), - ToL :: string(), - AmountL :: string(), - MessageL :: string(), - TTL_L :: string(), - GasL :: string(), - AffirmL :: string(), - CancelL :: string()}, - RecipientID :: binary(), % <<"ak_...">> | <<"ct_...">> - Amount :: non_neg_integer(), % Pucks - GasPrice :: pos_integer(), % Pucks - TTL :: non_neg_integer(), % Generations - Payload :: binary(). + Height :: pos_integer(), + J :: fun()}. -show(Parent, Title, Args, Labels) -> - zxw_modal:show(Parent, ?MODULE, {Title, Args, Labels}). +show(Parent, Args) -> + zxw_modal:show(Parent, ?MODULE, Args). %% Init -init({Parent, Caller, {Title, Args, Labels}}) -> - {Account, Nonce, Height} = Args, - {FromL, ToL, AmountL, MessageL, TTL_L, GasL, AffirmL, CancelL} = Labels, - Frame = wxFrame:new(Parent, ?wxID_ANY, Title), +init({Parent, Caller, {Account, J}}) -> + Frame = wxFrame:new(Parent, ?wxID_ANY, J("Transfer Gajus")), Panel = wxWindow:new(Frame, ?wxID_ANY), TopSz = wxBoxSizer:new(?wxVERTICAL), _ = wxBoxSizer:add(TopSz, Panel, zxw:flags(wide)), MainSz = wxBoxSizer:new(?wxVERTICAL), FromTx = wxStaticText:new(Panel, ?wxID_ANY, Account), - FromSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, FromL}]), + FromSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("From")}]), _ = wxStaticBoxSizer:add(FromSz, FromTx, zxw:flags({wide, 5})), ToTx = wxTextCtrl:new(Panel, ?wxID_ANY), - ToSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, ToL}]), + ToSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("To")}]), _ = wxStaticBoxSizer:add(ToSz, ToTx, zxw:flags({wide, 5})), AmtTx = wxTextCtrl:new(Panel, ?wxID_ANY), - AmtSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, AmountL}]), + AmtSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Amount")}]), AmtInSz = wxBoxSizer:new(?wxHORIZONTAL), AmtLabel = wxStaticText:new(Panel, ?wxID_ANY, "木"), _ = wxStaticBoxSizer:add(AmtInSz, AmtLabel, zxw:flags(base)), _ = wxStaticBoxSizer:add(AmtInSz, AmtTx, zxw:flags({wide, 5})), _ = wxStaticBoxSizer:add(AmtSz, AmtInSz, zxw:flags({wide, 5})), PayloadTx = wxTextCtrl:new(Panel, ?wxID_ANY, [{style, ?wxTE_MULTILINE}]), - PayloadSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, MessageL}]), + PayloadSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Message")}]), _ = wxStaticBoxSizer:add(PayloadSz, PayloadTx, zxw:flags({wide, 5})), Style = [{style, ?wxSL_HORIZONTAL bor ?wxSL_LABELS}], TTL_Sl = wxSlider:new(Panel, ?wxID_ANY, 100, 10, 1000, Style), - TTL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, TTL_L}]), + TTL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("TTL")}]), _ = wxStaticBoxSizer:add(TTL_Sz, TTL_Sl, zxw:flags({wide, 5})), Min = hz:min_gas_price(), Max = Min * 2, GasSl = wxSlider:new(Panel, ?wxID_ANY, Min, Min, Max, Style), - GasSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, GasL}]), + GasSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Gas Price")}]), _ = wxStaticBoxSizer:add(GasSz, GasSl, zxw:flags({wide, 5})), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), - Affirm = wxButton:new(Panel, ?wxID_ANY, [{label, AffirmL}]), - Cancel = wxButton:new(Panel, ?wxID_ANY, [{label, CancelL}]), + Affirm = wxButton:new(Panel, ?wxID_ANY, [{label, J("OK")}]), + Cancel = wxButton:new(Panel, ?wxID_ANY, [{label, J("Cancel")}]), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags({wide, 5})), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags({wide, 5})), _ = wxBoxSizer:add(MainSz, FromSz, zxw:flags({base, 5})), _ = wxBoxSizer:add(MainSz, ToSz, zxw:flags({base, 5})), _ = wxBoxSizer:add(MainSz, AmtSz, zxw:flags({base, 5})), _ = wxBoxSizer:add(MainSz, PayloadSz, zxw:flags({wide, 5})), - _ = wxBoxSizer:add(MainSz, TTL_Sz, zxw:flags({wide, 5})), + _ = wxBoxSizer:add(MainSz, TTL_Sz, zxw:flags({base, 5})), _ = wxBoxSizer:add(MainSz, GasSz, zxw:flags({base, 5})), _ = wxBoxSizer:add(MainSz, ButtSz, zxw:flags({base, 5})), - HandleKey = key_handler(ToTx, AmtTx, PayloadTx, TTL_Sl, GasSl, OK, Cancel), + HandleKey = key_handler(ToTx, AmtTx, PayloadTx, TTL_Sl, GasSl, Affirm, Cancel), ok = wxFrame:connect(Frame, key_down, [{callback, HandleKey}]), ok = wxTextCtrl:connect(ToTx, key_down, [{callback, HandleKey}]), ok = wxTextCtrl:connect(AmtTx, key_down, [{callback, HandleKey}]), ok = wxTextCtrl:connect(PayloadTx, key_down, [{callback, HandleKey}]), ok = wxTextCtrl:connect(TTL_Sl, key_down, [{callback, HandleKey}]), ok = wxTextCtrl:connect(GasSl, key_down, [{callback, HandleKey}]), - ok = wxTextCtrl:connect(OK, key_down, [{callback, HandleKey}]), + ok = wxTextCtrl:connect(Affirm, key_down, [{callback, HandleKey}]), ok = wxTextCtrl:connect(Cancel, key_down, [{callback, HandleKey}]), ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:connect(Frame, close_window), - ok = wxFrame:setSize(Frame, {500, 450}), + ok = wxFrame:setSize(Frame, {500, 650}), ok = wxFrame:center(Frame), ok = wxWindow:setSizer(Panel, MainSz), ok = wxFrame:setSizer(Frame, TopSz), ok = wxBoxSizer:layout(MainSz), ok = wxFrame:centerOnParent(Frame), - ok = wxTextCtrl:setFocus(NameTx), true = wxFrame:show(Frame), + self() ! {tab, cancel}, State = - #s{frame = Frame, parent = Parent, caller = Caller, - to_tx = ToTx, amount_tx = AmtTx, payload_tx = PayloadTx, - ttl_sl = TTL_Sl, gas_sl = GasSl, - ok = OK, cancel = Cancel}, + #s{frame = Frame, parent = Parent, caller = Caller, + to_tx = ToTx, amount_tx = AmtTx, payload_tx = PayloadTx, + ttl_sl = TTL_Sl, gas_sl = GasSl, + affirm = Affirm, cancel = Cancel}, {Frame, State}. -key_handler(ToTx, AmtTx, PayloadTx, TTL_Sl, GasSl, OK, Cancel) -> +key_handler(ToTx, AmtTx, PayloadTx, TTL_Sl, GasSl, Affirm, Cancel) -> Me = self(), ToID = wxTextCtrl:getId(ToTx), AmtID = wxTextCtrl:getId(AmtTx), PL_ID = wxTextCtrl:getId(PayloadTx), TTL_ID = wxSlider:getId(TTL_Sl), GasID = wxSlider:getId(GasSl), - OK_ID = wxButton:getId(OK), + AffirmID = wxButton:getId(Affirm), CancelID = wxButton:getId(Cancel), fun(#wx{id = ID, event = #wxKey{type = key_down, keyCode = Code}}, KeyPress) -> case Code of -% 9 -> -% case ID of -% ToID -> Me ! {tab, to_tx}; -% AmtID -> Me ! {tab, amount_tx}; -% PL_ID -> Me ! {tab, payload_tx}; -% TTL_ID -> Me ! {tab, ttl_sl}; -% GasID -> Me ! {tab, gas_sl}; -% OK_ID -> Me ! {tab, ok}; -% CancelID -> Me ! {tab, cancel}; -% _ -> wxEvent:skip(KeyPress) -% end; -% 13 -> -% case ID of -% NameID -> Me ! {enter, name}; -% PassID -> Me ! {enter, pass}; -% _ -> wxEvent:skip(KeyPress) -% end; + 9 -> + case ID of + ToID -> Me ! {tab, to_tx}; + AmtID -> Me ! {tab, amount_tx}; + PL_ID -> Me ! {tab, payload_tx}; + TTL_ID -> Me ! {tab, ttl_sl}; + GasID -> Me ! {tab, gas_sl}; + AffirmID -> Me ! {tab, affirm}; + CancelID -> Me ! {tab, cancel}; + _ -> wxEvent:skip(KeyPress) + end; + 13 -> + case ID of + ToID -> Me ! {tab, to_tx}; + AmtID -> Me ! {tab, amount_tx}; + PL_ID -> Me ! {tab, payload_tx}; + TTL_ID -> Me ! {tab, ttl_sl}; + GasID -> Me ! {tab, gas_sl}; + AffirmID -> Me ! {enter, affirm}; + CancelID -> Me ! {enter, cancel}; + _ -> wxEvent:skip(KeyPress) + end; 27 -> Me ! esc; _ -> @@ -174,11 +168,11 @@ key_handler(ToTx, AmtTx, PayloadTx, TTL_Sl, GasSl, OK, Cancel) -> handle_info({tab, Element}, State) -> ok = tab_traverse(Element, State), {noreply, State}; -handle_info({enter, name}, State = #s{pass_tx = PassTx}) -> - ok = wxTextCtrl:setFocus(PassTx), - {noreply, State}; -handle_info({enter, pass}, State) -> - done(State); +handle_info({enter, affirm}, State) -> + NewState = check(State), + {noreply, NewState}; +handle_info({enter, cancel}, State) -> + cancel(State); handle_info(esc, State) -> ok = cancel(State), {noreply, State}; @@ -188,13 +182,15 @@ handle_info(Message, State) -> handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, - State = #s{ok = OK, cancel = Cancel}) -> - OK_ID = wxButton:getId(OK), + State = #s{affirm = Affirm, cancel = Cancel}) -> + AffirmID = wxButton:getId(Affirm), CancelID = wxButton:getId(Cancel), - case ID of - OK_ID -> done(State); - CancelID -> cancel(State) - end; + NewState = + case ID of + AffirmID -> check(State); + CancelID -> cancel(State) + end, + {noreply, NewState}; handle_event(#wx{event = #wxClose{}}, State) -> NewState = cancel(State), {noreply, NewState}; @@ -205,14 +201,20 @@ handle_event(Event, State) -> %%% Doers -%tab_traverse(name, #s{pass_tx = PassTx}) -> -% wxTextCtrl:setFocus(PassTx); -%tab_traverse(pass, #s{ok = OK}) -> -% wxButton:setFocus(OK); -%tab_traverse(ok, #s{cancel = Cancel}) -> -% wxButton:setFocus(Cancel); -%tab_traverse(cancel, #s{name_tx = NameTx}) -> -% wxTextCtrl:setFocus(NameTx). +tab_traverse(to_tx, #s{amount_tx = AmountTX}) -> + wxTextCtrl:setFocus(AmountTX); +tab_traverse(amount_tx, #s{payload_tx = PayloadTX}) -> + wxTextCtrl:setFocus(PayloadTX); +tab_traverse(payload_tx, #s{ttl_sl = TTL_SL}) -> + wxSlider:setFocus(TTL_SL); +tab_traverse(ttl_sl, #s{gas_sl = GasSL}) -> + wxSlider:setFocus(GasSL); +tab_traverse(gas_sl, #s{affirm = Affirm}) -> + wxButton:setFocus(Affirm); +tab_traverse(affirm, #s{cancel = Cancel}) -> + wxButton:setFocus(Cancel); +tab_traverse(cancel, #s{to_tx = ToTX}) -> + wxTextCtrl:setFocus(ToTX). cancel(#s{frame = Frame, caller = Caller}) -> @@ -220,56 +222,63 @@ cancel(#s{frame = Frame, caller = Caller}) -> zxw_modal:done(Caller, cancel). -done(#s{frame = Frame, caller = Caller, - to_tx = ToTx, amount_tx = AmountTx, payload_tx = PayloadTx, - ttl_sl = TTL_Sl, gas_sl = GasSl}) -> +check(State =#s{frame = Frame, caller = Caller, j = J, + to_tx = ToTx, amount_tx = AmountTx, payload_tx = PayloadTx, + ttl_sl = TTL_Sl, gas_sl = GasSl}) -> DirtyTX = - #spend_tx{recipient_id = wxTextCtrl:getValue(ToTx), - amount = wxTextCtrl:getValue(AmountTx), - gas_price = wxSlider:getValue(GasSl), - ttl = wxSlider:getValue(TTL_Sl), - payload = wxTextCtrl:getValue(PayloadTX)}, - Result = - case clean_spend(TX) of - {ok, CleanTX} -> + [{recipient_id, wxTextCtrl:getValue(ToTx)}, + {amount, wxTextCtrl:getValue(AmountTx)}, + {gas_price, wxSlider:getValue(GasSl)}, + {ttl, wxSlider:getValue(TTL_Sl)}, + {payload, wxTextCtrl:getValue(PayloadTx)}], + ok = + case clean_spend(DirtyTX, #spend_tx{}, J, []) of + {ok, CleanTX} -> + zxw_modal:done(Caller, {ok, CleanTX}); + {error, Errors} -> + DerpyDerpDerp = form_message(Errors, J), + ok = zxw:show_message(Frame, DerpyDerpDerp), + State + end. + +% TODO: There should be some suggestive logic around gas prices, both based on how large +% the payload and TTL are, but also the ingoing current common gas rate. This will require +% a "gas station" sort of analysis app to query, though. +clean_spend([{recipient_id, ""} | Rest], TX, J, Errors) -> + NewErrors = [{recipient_id, J("Recipient field is empty.")} | Errors], + clean_spend(Rest, TX, J, NewErrors); +clean_spend([{recipient_id, Recipient} | Rest], TX, J, Errors) -> + clean_spend(Rest, TX#spend_tx{recipient_id = list_to_binary(Recipient)}, J, Errors); +clean_spend([{amount, ""} | Rest], TX, J, Errors) -> + clean_spend(Rest, TX#spend_tx{amount = 0}, J, Errors); +clean_spend([{amount, S} | Rest], TX, J, Errors) -> + {NewTX, NewErrors} = + case hz_format:read(S) of + {ok, Amount} -> + {TX#spend_tx{amount = Amount}, Errors}; + error -> + Derp = J("Amount field is not properly formatted."), + {TX, [{amount, Derp} | Errors]} end, - Result = - case wxTextCtrl:getValue(NameTx) of - "" -> - cancel; - Name -> - case wxTextCtrl:getValue(PassTx) of - "" -> {ok, Name, none}; - Pass -> {ok, Name, Pass} - end - end, - ok = wxFrame:destroy(Frame), - zxw_modal:done(Caller, Result). - -clean_spend(TX) -> - clean_spend(TX, []). - -clean_spend(TX = #spend_tx{recipient_id = ""}, Errors) -> - clean_spend(TX#spend_tx{recipient_id = none}, [recipient_id | Errors]); -clean_spend(TX = #spend_tx{amount = S}, Errors) 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) -> - {ok, CleanTX}. + clean_spend(Rest, NewTX, J, NewErrors); +clean_spend([{gas_price, GasPrice} | Rest], TX, J, Errors) -> + clean_spend(Rest, TX#spend_tx{gas_price = GasPrice}, J, Errors); +clean_spend([{ttl, TTL} | Rest], TX, J, Errors) -> + clean_spend(Rest, TX#spend_tx{ttl = TTL}, J, Errors); +clean_spend([{payload = S} | Rest], TX, J, Errors) -> + clean_spend(Rest, TX#spend_tx{payload = list_to_binary(S)}, J, Errors); +clean_spend([], TX, _, []) -> + {ok, TX}; +clean_spend([], _, _, Errors) -> + {error, Errors}. +form_message(Errors, J) -> + Header = J("The following errors were encountered:"), + unicode:characters_to_list([Header, "\n", assemble(Errors)]). + +% TODO: Highlight the fields since we know them. +assemble([{_, M} | T]) -> [M, "\n" | assemble(T)]; +assemble([]) -> []. + diff --git a/zomp.meta b/zomp.meta index 71fdb54..f1f4f59 100644 --- a/zomp.meta +++ b/zomp.meta @@ -5,8 +5,8 @@ {author,"Craig Everett"}. {desc,"A desktop client for the Gajumaru network of blockchain networks"}. {package_id,{"otpr","gajudesk",{0,8,0}}}. -{deps,[{"otpr","zxwidgets",{1,1,0}}, - {"otpr","hakuzaru",{0,7,0}}, +{deps,[{"otpr","hakuzaru",{0,8,2}}, + {"otpr","zxwidgets",{1,1,0}}, {"otpr","eblake2",{1,0,1}}, {"otpr","base58",{0,1,1}}, {"otpr","gmserialization",{0,1,3}},