GajuDesk/src/gd_gui.erl

1154 lines
42 KiB
Erlang
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

%%% @doc
%%% GajuDesk GUI
%%% @end
-module(gd_gui).
-vsn("0.6.6").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-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.