1154 lines
42 KiB
Erlang
1154 lines
42 KiB
Erlang
%%% @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.
|