960 lines
34 KiB
Erlang
960 lines
34 KiB
Erlang
%%% @doc
|
|
%%% Clutch GUI
|
|
%%%
|
|
%%% This process is responsible for creating the main GUI frame displayed to the user.
|
|
%%%
|
|
%%% Reference: http://erlang.org/doc/man/wx_object.html
|
|
%%% @end
|
|
|
|
-module(gmc_gui).
|
|
-vsn("0.1.0").
|
|
-author("Craig Everett <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([start_link/1]).
|
|
-export([init/1, terminate/2, code_change/3,
|
|
handle_call/3, handle_cast/2, handle_info/2, handle_event/2]).
|
|
-include("$zx_include/zx_logger.hrl").
|
|
-include("gmc.hrl").
|
|
|
|
|
|
-record(w,
|
|
{name = none :: atom(),
|
|
id = 0 :: integer(),
|
|
wx = none :: none | wx:wx_object()}).
|
|
|
|
-record(s,
|
|
{wx = none :: none | wx:wx_object(),
|
|
frame = none :: none | wx:wx_object(),
|
|
lang = en :: en | jp,
|
|
j = none :: none | fun(),
|
|
prefs = #{} :: #{atom() := term()},
|
|
accounts = [] :: [clutch:poa()],
|
|
picker = none :: none | wx:wx_object(),
|
|
id = {#w{}, #w{}} :: labeled(),
|
|
balance = {#w{}, #w{}} :: labeled(),
|
|
buttons = [] :: [widget()]}).
|
|
|
|
|
|
-type state() :: term().
|
|
-type widget() :: #w{}.
|
|
-type labeled() :: {Label :: #w{}, Control :: #w{}}.
|
|
|
|
|
|
|
|
%%% Interface functions
|
|
|
|
show(Accounts) ->
|
|
wx_object:cast(?MODULE, {show, Accounts}).
|
|
|
|
|
|
wallet(Name) ->
|
|
wx_object:cast(?MODULE, {wallet, Name}).
|
|
|
|
|
|
chain(ChainID, Node) ->
|
|
wx_object:cast(?MODULE, {chain, ChainID, Node}).
|
|
|
|
|
|
trouble(Message) ->
|
|
wx_object:cast(?MODULE, {trouble, Message}).
|
|
|
|
|
|
ask_password() ->
|
|
wx_object:cast(?MODULE, password).
|
|
|
|
|
|
|
|
%%% Startup Functions
|
|
|
|
start_link(Accounts) ->
|
|
wx_object:start_link({local, ?MODULE}, ?MODULE, Accounts, []).
|
|
|
|
|
|
init(Prefs) ->
|
|
ok = log(info, "GUI starting..."),
|
|
Lang = maps:get(lang, Prefs, en_us),
|
|
Trans = gmc_jt:read_translations(?MODULE),
|
|
J = gmc_jt:j(Lang, Trans),
|
|
|
|
Wx = wx:new(),
|
|
Frame = wxFrame:new(Wx, ?wxID_ANY, J("Gajumaru Clutch")),
|
|
MainSz = wxBoxSizer:new(?wxVERTICAL),
|
|
Picker = wxListBox:new(Frame, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]),
|
|
|
|
WallB = wxButton:new(Frame, ?wxID_ANY, [{label, "[none]"}, {style, ?wxBORDER_NONE}]),
|
|
WallW = #w{name = wallet, id = wxButton:getId(WallB), wx = WallB},
|
|
ChainB = wxButton:new(Frame, ?wxID_ANY, [{label, "[ChainID]"}, {style, ?wxBORDER_NONE}]),
|
|
ChainW = #w{name = chain, id = wxButton:getId(ChainB), wx = ChainB},
|
|
NodeB = wxButton:new(Frame, ?wxID_ANY, [{label, "[Node]"}, {style, ?wxBORDER_NONE}]),
|
|
NodeW = #w{name = node, id = wxButton:getId(NodeB), wx = NodeB},
|
|
|
|
ID_L = wxStaticText:new(Frame, ?wxID_ANY, J("Account ID: ")),
|
|
ID_T = wxStaticText:new(Frame, ?wxID_ANY, ""),
|
|
ID_W =
|
|
{#w{id = wxStaticText:getId(ID_L), wx = ID_L},
|
|
#w{id = wxStaticText:getId(ID_T), wx = ID_T}},
|
|
ID_Sz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
_ = wxSizer:add(ID_Sz, ID_L, zxw:flags(base)),
|
|
_ = wxSizer:add(ID_Sz, ID_T, zxw:flags(wide)),
|
|
|
|
BalanceL = wxStaticText:new(Frame, ?wxID_ANY, "木"),
|
|
BalanceT = wxStaticText:new(Frame, ?wxID_ANY, price_to_string(0)),
|
|
Balance =
|
|
{#w{id = wxStaticText:getId(BalanceL), wx = BalanceL},
|
|
#w{id = wxStaticText:getId(BalanceT), wx = BalanceT}},
|
|
BalanceSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
_ = wxSizer:add(BalanceSz, BalanceL, zxw:flags(base)),
|
|
_ = wxSizer:add(BalanceSz, BalanceT, zxw:flags(wide)),
|
|
|
|
NumbersSz = wxBoxSizer:new(?wxVERTICAL),
|
|
_ = wxSizer:add(NumbersSz, ID_Sz, zxw:flags(wide)),
|
|
_ = wxSizer:add(NumbersSz, BalanceSz, zxw:flags(wide)),
|
|
|
|
ButtonTemplates =
|
|
[{make_key, J("Create")},
|
|
{recover, J("Recover")},
|
|
{mnemonic, J("Mnemonic")},
|
|
{rename, J("Rename")},
|
|
{drop_key, J("Delete")},
|
|
{send, J("Send Money")},
|
|
{recv, J("Receive Money")},
|
|
{grids, J("GRIDS URL")},
|
|
{copy, J("Copy")},
|
|
{www, J("WWW")},
|
|
{refresh, J("Refresh")}],
|
|
|
|
MakeButton =
|
|
fun({Name, Label}) ->
|
|
B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]),
|
|
#w{name = Name, id = wxButton:getId(B), wx = B}
|
|
end,
|
|
|
|
Buttons = [WallW, ChainW, NodeW | lists:map(MakeButton, ButtonTemplates)],
|
|
|
|
ChainSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
AccountSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
DetailsSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
ActionsSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
HistorySz = wxBoxSizer:new(?wxVERTICAL),
|
|
|
|
_ = wxSizer:add(ChainSz, WallB, zxw:flags(wide)),
|
|
_ = wxSizer:add(ChainSz, ChainB, zxw:flags(wide)),
|
|
_ = wxSizer:add(ChainSz, NodeB, zxw:flags(wide)),
|
|
|
|
#w{wx = CopyBn} = lists:keyfind(copy, #w.name, Buttons),
|
|
#w{wx = WWW_Bn} = lists:keyfind(www, #w.name, Buttons),
|
|
_ = wxSizer:add(DetailsSz, NumbersSz, zxw:flags(wide)),
|
|
_ = wxSizer:add(DetailsSz, CopyBn, zxw:flags(base)),
|
|
_ = wxSizer:add(DetailsSz, WWW_Bn, zxw:flags(base)),
|
|
|
|
#w{wx = MakeBn} = lists:keyfind(make_key, #w.name, Buttons),
|
|
#w{wx = RecoBn} = lists:keyfind(recover, #w.name, Buttons),
|
|
#w{wx = MnemBn} = lists:keyfind(mnemonic, #w.name, Buttons),
|
|
#w{wx = Rename} = lists:keyfind(rename, #w.name, Buttons),
|
|
#w{wx = DropBn} = lists:keyfind(drop_key, #w.name, Buttons),
|
|
_ = wxSizer:add(AccountSz, MakeBn, zxw:flags(wide)),
|
|
_ = wxSizer:add(AccountSz, RecoBn, zxw:flags(wide)),
|
|
_ = wxSizer:add(AccountSz, MnemBn, zxw:flags(wide)),
|
|
_ = wxSizer:add(AccountSz, Rename, zxw:flags(wide)),
|
|
_ = wxSizer:add(AccountSz, DropBn, zxw:flags(wide)),
|
|
|
|
#w{wx = SendBn} = lists:keyfind(send, #w.name, Buttons),
|
|
#w{wx = RecvBn} = lists:keyfind(recv, #w.name, Buttons),
|
|
#w{wx = GridsBn} = lists:keyfind(grids, #w.name, Buttons),
|
|
_ = wxSizer:add(ActionsSz, SendBn, zxw:flags(wide)),
|
|
_ = wxSizer:add(ActionsSz, RecvBn, zxw:flags(wide)),
|
|
_ = wxSizer:add(ActionsSz, GridsBn, zxw:flags(wide)),
|
|
|
|
#w{wx = Refresh} = lists:keyfind(refresh, #w.name, Buttons),
|
|
_ = wxSizer:add(HistorySz, Refresh, zxw:flags(base)),
|
|
|
|
_ = wxSizer:add(MainSz, ChainSz, zxw:flags(base)),
|
|
_ = wxSizer:add(MainSz, AccountSz, zxw:flags(base)),
|
|
_ = wxSizer:add(MainSz, Picker, zxw:flags(wide)),
|
|
_ = wxSizer:add(MainSz, DetailsSz, zxw:flags(base)),
|
|
_ = wxSizer:add(MainSz, ActionsSz, zxw:flags(base)),
|
|
_ = wxSizer:add(MainSz, HistorySz, [{proportion, 3}, {flag, ?wxEXPAND}]),
|
|
ok = wxFrame:setSizer(Frame, MainSz),
|
|
ok = wxSizer:layout(MainSz),
|
|
|
|
ok = gmc_v:safe_size(Frame, Prefs),
|
|
|
|
ok = wxFrame:connect(Frame, command_button_clicked),
|
|
ok = wxFrame:connect(Frame, close_window),
|
|
true = wxFrame:show(Frame),
|
|
ok = wxListBox:connect(Picker, command_listbox_selected),
|
|
State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs,
|
|
accounts = [],
|
|
picker = Picker,
|
|
id = ID_W,
|
|
balance = Balance,
|
|
buttons = Buttons},
|
|
{Frame, State}.
|
|
|
|
|
|
-spec handle_call(Message, From, State) -> Result
|
|
when Message :: term(),
|
|
From :: {pid(), reference()},
|
|
State :: state(),
|
|
Result :: {reply, Response, NewState}
|
|
| {noreply, State},
|
|
Response :: ok
|
|
| {error, {listening, inet:port_number()}},
|
|
NewState :: state().
|
|
|
|
handle_call(Unexpected, From, State) ->
|
|
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
-spec handle_cast(Message, State) -> {noreply, NewState}
|
|
when Message :: term(),
|
|
State :: state(),
|
|
NewState :: state().
|
|
%% @private
|
|
%% The gen_server:handle_cast/2 callback.
|
|
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_cast-2
|
|
|
|
handle_cast({show, Accounts}, State) ->
|
|
NewState = do_show(Accounts, State),
|
|
{noreply, NewState};
|
|
handle_cast({wallet, Name}, State) ->
|
|
ok = do_wallet(Name, State),
|
|
{noreply, State};
|
|
handle_cast({chain, ChainID, Node}, State) ->
|
|
ok = do_chain(ChainID, Node, State),
|
|
{noreply, State};
|
|
handle_cast({trouble, Info}, State) ->
|
|
ok = handle_trouble(Info, State),
|
|
{noreply, State};
|
|
handle_cast(password, State) ->
|
|
ok = do_ask_password(State),
|
|
{noreply, State};
|
|
handle_cast(Unexpected, State) ->
|
|
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
-spec handle_info(Message, State) -> {noreply, NewState}
|
|
when Message :: term(),
|
|
State :: state(),
|
|
NewState :: state().
|
|
%% @private
|
|
%% The gen_server:handle_info/2 callback.
|
|
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2
|
|
|
|
handle_info(Unexpected, State) ->
|
|
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
|
|
{noreply, State}.
|
|
|
|
|
|
-spec handle_event(Event, State) -> {noreply, NewState}
|
|
when Event :: term(),
|
|
State :: state(),
|
|
NewState :: state().
|
|
%% @private
|
|
%% The wx_object:handle_event/2 callback.
|
|
%% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2
|
|
|
|
handle_event(#wx{event = #wxCommand{type = command_button_clicked},
|
|
id = ID},
|
|
State = #s{buttons = Buttons}) ->
|
|
NewState =
|
|
case lists:keyfind(ID, #w.id, Buttons) of
|
|
#w{name = wallet} -> wallman(State);
|
|
#w{name = chain} -> netman(State);
|
|
#w{name = node} -> set_node(State);
|
|
#w{name = make_key} -> make_key(State);
|
|
#w{name = recover} -> recover_key(State);
|
|
#w{name = mnemonic} -> show_mnemonic(State);
|
|
#w{name = rename} -> rename_key(State);
|
|
#w{name = drop_key} -> drop_key(State);
|
|
#w{name = copy} -> copy(State);
|
|
#w{name = www} -> www(State);
|
|
#w{name = send} -> spend(State);
|
|
#w{name = grids} -> grids_dialogue(State);
|
|
#w{name = refresh} -> refresh(State);
|
|
#w{name = Name} -> handle_button(Name, State);
|
|
false -> State
|
|
end,
|
|
{noreply, NewState};
|
|
handle_event(#wx{event = #wxCommand{type = command_listbox_selected,
|
|
commandInt = Selected}},
|
|
State) ->
|
|
NewState = do_selection(Selected, State),
|
|
{noreply, NewState};
|
|
handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) ->
|
|
Geometry =
|
|
case wxTopLevelWindow:isMaximized(Frame) of
|
|
true ->
|
|
max;
|
|
false ->
|
|
{X, Y} = wxWindow:getPosition(Frame),
|
|
{W, H} = wxWindow:getSize(Frame),
|
|
{X, Y, W, H}
|
|
end,
|
|
NewPrefs = maps:put(geometry, Geometry, Prefs),
|
|
ok = gmc_con:save(?MODULE, NewPrefs),
|
|
ok = gmc_con:stop(),
|
|
ok = wxWindow:destroy(Frame),
|
|
{noreply, State};
|
|
handle_event(Event, State) ->
|
|
ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]),
|
|
{noreply, State}.
|
|
|
|
|
|
|
|
code_change(_, State, _) ->
|
|
{ok, State}.
|
|
|
|
|
|
terminate(wx_deleted, _) ->
|
|
wx:destroy();
|
|
terminate(Reason, State) ->
|
|
ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]),
|
|
wx:destroy().
|
|
|
|
|
|
%%% Doers
|
|
|
|
refresh(State) ->
|
|
ok = gmc_con:refresh(),
|
|
State.
|
|
|
|
|
|
wallman(State) ->
|
|
ok = gmc_con:show_ui(gmc_v_wallman),
|
|
State.
|
|
|
|
|
|
netman(State) ->
|
|
ok = gmc_con:show_ui(gmc_v_netman),
|
|
State.
|
|
|
|
set_node(State = #s{frame = Frame, j = J}) ->
|
|
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Set Node")),
|
|
Sizer = wxBoxSizer:new(?wxVERTICAL),
|
|
|
|
AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address")}]),
|
|
AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
|
|
_ = wxSizer:add(AddressSz, AddressTx, zxw:flags(wide)),
|
|
|
|
PortSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Labels =
|
|
[J("External"),
|
|
J("Internal"),
|
|
J("Rosetta"),
|
|
J("Channel"),
|
|
J("Middleware")],
|
|
MakePortCtrl =
|
|
fun(L) ->
|
|
Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, L}]),
|
|
Tx = wxTextCtrl:new(Dialog, ?wxID_ANY),
|
|
_ = wxSizer:add(Sz, Tx, zxw:flags(wide)),
|
|
_ = wxSizer:add(PortSz, Sz, zxw:flags(wide)),
|
|
Tx
|
|
end,
|
|
PortCtrls = lists:map(MakePortCtrl, Labels),
|
|
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Dialog, ?wxID_OK),
|
|
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
|
|
|
|
_ = wxSizer:add(Sizer, AddressSz, zxw:flags(base)),
|
|
_ = wxSizer:add(Sizer, PortSz, zxw:flags(base)),
|
|
_ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)),
|
|
|
|
ok = wxDialog:setSizer(Dialog, Sizer),
|
|
ok = wxBoxSizer:layout(Sizer),
|
|
ok = wxFrame:setSize(Dialog, {500, 200}),
|
|
ok = wxFrame:center(Dialog),
|
|
ok = wxStyledTextCtrl:setFocus(AddressTx),
|
|
|
|
ok =
|
|
case wxDialog:showModal(Dialog) of
|
|
?wxID_OK ->
|
|
case wxTextCtrl:getValue(AddressTx) of
|
|
"" -> ok;
|
|
Address -> add_node2(Address, PortCtrls)
|
|
end;
|
|
?wxID_CANCEL ->
|
|
ok
|
|
end,
|
|
ok = wxDialog:destroy(Dialog),
|
|
State.
|
|
|
|
add_node2(Address, PortCtrls) ->
|
|
IP =
|
|
case inet:parse_address(Address) of
|
|
{ok, N} -> N;
|
|
{error, einval} -> Address
|
|
end,
|
|
[E, I, R, C, M] = lists:map(fun numerify_port/1, PortCtrls),
|
|
New = #node{ip = IP, external = E, internal = I, rosetta = R, channel = C, mdw = M},
|
|
gmc_con:set_sole_node(New).
|
|
|
|
numerify_port(Ctrl) ->
|
|
case wxTextCtrl:getValue(Ctrl) of
|
|
"" -> none;
|
|
String ->
|
|
case s_to_i(String) of
|
|
{ok, PortNum} -> PortNum;
|
|
error -> none
|
|
end
|
|
end.
|
|
|
|
s_to_i(S) ->
|
|
try
|
|
I = list_to_integer(S),
|
|
{ok, I}
|
|
catch error:badarg ->
|
|
error
|
|
end.
|
|
|
|
|
|
make_key(State = #s{frame = Frame, j = J}) ->
|
|
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key")),
|
|
Sizer = wxBoxSizer:new(?wxVERTICAL),
|
|
|
|
NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name (Optional)")}]),
|
|
NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
|
|
|
|
SeedSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Seed (Optional)")}]),
|
|
SeedTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxDEFAULT bor ?wxTE_MULTILINE}]),
|
|
ok = wxTextCtrl:setValue(SeedTx, base64:encode(crypto:strong_rand_bytes(64))),
|
|
|
|
OptionsSz = wxStaticBoxSizer:new(?wxHORIZONTAL, Dialog, [{label, J("Options")}]),
|
|
Encodings =
|
|
["Base64",
|
|
"UTF-8",
|
|
"Base58"],
|
|
EncodingOptions = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Encodings}]),
|
|
ok = wxChoice:setSelection(EncodingOptions, 0),
|
|
Transforms =
|
|
["SHA-3 (256)",
|
|
"SHA-2 (256)",
|
|
"XOR",
|
|
"Direct Input (no transform)"],
|
|
TransformOptions = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Transforms}]),
|
|
ok = wxChoice:setSelection(TransformOptions, 0),
|
|
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Dialog, ?wxID_OK),
|
|
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
|
|
|
|
_ = wxStaticBoxSizer:add(NameSz, NameTx, zxw:flags(base)),
|
|
_ = wxStaticBoxSizer:add(SeedSz, SeedTx, zxw:flags(wide)),
|
|
_ = wxStaticBoxSizer:add(OptionsSz, EncodingOptions, zxw:flags(wide)),
|
|
_ = wxStaticBoxSizer:add(OptionsSz, TransformOptions, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, NameSz, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Sizer, SeedSz, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, OptionsSz, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
|
|
|
|
ok = wxDialog:setSizer(Dialog, Sizer),
|
|
ok = wxBoxSizer:layout(Sizer),
|
|
ok = wxFrame:setSize(Dialog, {400, 300}),
|
|
ok = wxFrame:center(Dialog),
|
|
|
|
ok = wxStyledTextCtrl:setFocus(NameTx),
|
|
ok =
|
|
case wxDialog:showModal(Dialog) of
|
|
?wxID_OK ->
|
|
Type = {eddsa, ed25519},
|
|
Size = 256,
|
|
Name = wxTextCtrl:getValue(NameTx),
|
|
Seed = wxTextCtrl:getValue(SeedTx),
|
|
Encoding =
|
|
case wxChoice:getSelection(EncodingOptions) of
|
|
0 -> base64;
|
|
1 -> utf8;
|
|
2 -> base58
|
|
end,
|
|
Transform =
|
|
case wxChoice:getSelection(TransformOptions) of
|
|
0 -> {sha3, 256};
|
|
1 -> {sha2, 256};
|
|
2 -> {x_or, 256};
|
|
3 -> raw
|
|
end,
|
|
gmc_con:make_key(Type, Size, Name, Seed, Encoding, Transform);
|
|
?wxID_CANCEL ->
|
|
ok
|
|
end,
|
|
ok = wxDialog:destroy(Dialog),
|
|
State.
|
|
|
|
|
|
recover_key(State = #s{frame = Frame, j = J}) ->
|
|
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Mnemonic")),
|
|
Sizer = wxBoxSizer:new(?wxVERTICAL),
|
|
MnemSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Recovery Phrase")}]),
|
|
MnemTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_MULTILINE}]),
|
|
_ = wxStaticBoxSizer:add(MnemSz, MnemTx, zxw:flags(wide)),
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Dialog, ?wxID_OK),
|
|
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, MnemSz, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
|
|
ok = wxDialog:setSizer(Dialog, Sizer),
|
|
ok = wxBoxSizer:layout(Sizer),
|
|
ok = wxFrame:center(Dialog),
|
|
ok = wxStyledTextCtrl:setFocus(MnemTx),
|
|
ok =
|
|
case wxDialog:showModal(Dialog) of
|
|
?wxID_OK ->
|
|
Mnemonic = wxTextCtrl:getValue(MnemTx),
|
|
gmc_con:recover_key(Mnemonic);
|
|
?wxID_CANCEL ->
|
|
ok
|
|
end,
|
|
ok = wxDialog:destroy(Dialog),
|
|
State.
|
|
|
|
|
|
show_mnemonic(State = #s{accounts = []}) ->
|
|
State;
|
|
show_mnemonic(State = #s{picker = Picker}) ->
|
|
case wxListBox:getSelection(Picker) of
|
|
-1 -> State;
|
|
Selected -> show_mnemonic(Selected + 1, State)
|
|
end.
|
|
|
|
show_mnemonic(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) ->
|
|
#poa{id = ID} = lists:nth(Selected, Accounts),
|
|
{ok, Mnemonic} = gmc_con:mnemonic(ID),
|
|
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Mnemonic")),
|
|
Sizer = wxBoxSizer:new(?wxVERTICAL),
|
|
MnemSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Recovery Phrase")}]),
|
|
Options = [{value, Mnemonic}, {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}],
|
|
MnemTx = wxTextCtrl:new(Dialog, ?wxID_ANY, Options),
|
|
_ = wxStaticBoxSizer:add(MnemSz, MnemTx, zxw:flags(wide)),
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Dialog, ?wxID_OK),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, MnemSz, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
|
|
ok = wxDialog:setSizer(Dialog, Sizer),
|
|
ok = wxBoxSizer:layout(Sizer),
|
|
ok = wxFrame:center(Dialog),
|
|
ok = wxStyledTextCtrl:setFocus(MnemTx),
|
|
?wxID_OK = wxDialog:showModal(Dialog),
|
|
ok = wxDialog:destroy(Dialog),
|
|
State.
|
|
|
|
|
|
rename_key(State = #s{accounts = []}) ->
|
|
State;
|
|
rename_key(State = #s{picker = Picker}) ->
|
|
case wxListBox:getSelection(Picker) of
|
|
-1 -> State;
|
|
Selected -> rename_key(Selected + 1, State)
|
|
end.
|
|
|
|
rename_key(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) ->
|
|
#poa{id = ID, name = Name} = lists:nth(Selected, Accounts),
|
|
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key")),
|
|
Sizer = wxBoxSizer:new(?wxVERTICAL),
|
|
NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name (Optional)")}]),
|
|
NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
|
|
ok = wxTextCtrl:setValue(NameTx, Name),
|
|
_ = wxStaticBoxSizer:add(NameSz, NameTx, zxw:flags(base)),
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Dialog, ?wxID_OK),
|
|
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, NameSz, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
|
|
ok = wxDialog:setSizer(Dialog, Sizer),
|
|
ok = wxBoxSizer:layout(Sizer),
|
|
ok = wxFrame:center(Dialog),
|
|
ok = wxStyledTextCtrl:setFocus(NameTx),
|
|
ok =
|
|
case wxDialog:showModal(Dialog) of
|
|
?wxID_OK ->
|
|
NewName = wxTextCtrl:getValue(NameTx),
|
|
gmc_con:rename_key(ID, NewName);
|
|
?wxID_CANCEL ->
|
|
ok
|
|
end,
|
|
ok = wxDialog:destroy(Dialog),
|
|
State.
|
|
|
|
|
|
drop_key(State = #s{accounts = []}) ->
|
|
State;
|
|
drop_key(State = #s{picker = Picker}) ->
|
|
case wxListBox:getSelection(Picker) of
|
|
-1 -> State;
|
|
Selected -> drop_key(Selected + 1, State)
|
|
end.
|
|
|
|
drop_key(Selected, State = #s{frame = Frame, j = J, accounts = Accounts, prefs = Prefs}) ->
|
|
#poa{id = ID, name = Name} = lists:nth(Selected, Accounts),
|
|
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key"), [{size, {500, 150}}]),
|
|
Sizer = wxBoxSizer:new(?wxVERTICAL),
|
|
Message = ["REALLY delete key?\r\n\r\n\"", Name, "\"\r\n(", ID, ")"],
|
|
MessageT = wxStaticText:new(Dialog, ?wxID_ANY, Message,
|
|
[{style, ?wxALIGN_CENTRE_HORIZONTAL}]),
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Dialog, ?wxID_OK),
|
|
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, MessageT, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
|
|
ok = wxDialog:setSizer(Dialog, Sizer),
|
|
ok = wxBoxSizer:layout(Sizer),
|
|
ok = wxFrame:center(Dialog),
|
|
NewPrefs =
|
|
case wxDialog:showModal(Dialog) of
|
|
?wxID_OK ->
|
|
gmc_con:drop_key(ID),
|
|
case Selected =:= length(Accounts) of
|
|
true -> maps:put(selected, Selected - 2, Prefs);
|
|
false -> Prefs
|
|
end;
|
|
?wxID_CANCEL ->
|
|
Prefs
|
|
end,
|
|
ok = wxDialog:destroy(Dialog),
|
|
State#s{prefs = NewPrefs}.
|
|
|
|
|
|
copy(State = #s{id = {_, #w{wx = ID_T}}}) ->
|
|
String = wxStaticText:getLabel(ID_T),
|
|
CB = wxClipboard:get(),
|
|
ok =
|
|
case wxClipboard:open(CB) of
|
|
true ->
|
|
Text = wxTextDataObject:new([{text, String}]),
|
|
case wxClipboard:setData(CB, Text) of
|
|
true ->
|
|
R = wxClipboard:flush(CB),
|
|
log(info, "Address copied to system clipboard. Flushed: ~p", [R]);
|
|
false ->
|
|
log(info, "Failed to copy to clipboard")
|
|
end,
|
|
ok = wxClipboard:close(CB);
|
|
false ->
|
|
log(info, "Failed to acquire the clipboard.")
|
|
end,
|
|
State.
|
|
|
|
|
|
www(State = #s{id = {_, #w{wx = ID_T}}}) ->
|
|
String = wxStaticText:getLabel(ID_T),
|
|
URL = unicode:characters_to_list(["https://aescan.io/accounts/", String]),
|
|
ok =
|
|
case wx_misc:launchDefaultBrowser(URL) of
|
|
true -> log(info, "Opened in browser: ~ts", [URL]);
|
|
false -> log(info, "Failed to open browser: ~ts", [URL])
|
|
end,
|
|
State.
|
|
|
|
|
|
spend(State = #s{accounts = []}) ->
|
|
State;
|
|
spend(State = #s{picker = Picker}) ->
|
|
case wxListBox:getSelection(Picker) of
|
|
-1 -> State;
|
|
Selected -> spend(Selected + 1, State)
|
|
end.
|
|
|
|
spend(Selected, State = #s{accounts = Accounts}) ->
|
|
POA = #poa{id = ID} = lists:nth(Selected, Accounts),
|
|
case gmc_con:nonce(ID) of
|
|
{ok, Nonce} ->
|
|
{ok, #{"top_block_height" := Height}} = hz:status(),
|
|
spend2(POA, Nonce, Height, State);
|
|
{error, Reason} ->
|
|
tell("spend/2 failed to get nonce with ~tp", [Reason]),
|
|
State
|
|
end.
|
|
|
|
spend2(#poa{id = ID, name = Name}, Nonce, Height, State = #s{frame = Frame, j = J}) ->
|
|
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Transfer"), [{size, {500, 400}}]),
|
|
Sizer = wxBoxSizer:new(?wxVERTICAL),
|
|
|
|
Account = [Name, " (", ID, ")"],
|
|
FromTx = wxStaticText:new(Dialog, ?wxID_ANY, Account),
|
|
FromSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("From")}]),
|
|
_ = wxStaticBoxSizer:add(FromSz, FromTx, zxw:flags(wide)),
|
|
|
|
ToTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
|
|
ToSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("To")}]),
|
|
_ = wxStaticBoxSizer:add(ToSz, ToTx, zxw:flags(wide)),
|
|
|
|
AmtTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
|
|
AmtSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Amount")}]),
|
|
_ = wxStaticBoxSizer:add(AmtSz, AmtTx, zxw:flags(wide)),
|
|
|
|
DataTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_MULTILINE}]),
|
|
DataSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Message (optional)")}]),
|
|
_ = wxStaticBoxSizer:add(DataSz, DataTx, zxw:flags(wide)),
|
|
|
|
Style = [{style, ?wxSL_HORIZONTAL bor ?wxSL_LABELS}],
|
|
|
|
TTL_Sl = wxSlider:new(Dialog, ?wxID_ANY, 100, 10, 1000, Style),
|
|
TTL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("TTL")}]),
|
|
_ = wxStaticBoxSizer:add(TTL_Sz, TTL_Sl, zxw:flags(wide)),
|
|
|
|
Min = hz:min_gas_price(),
|
|
Max = Min * 2,
|
|
GasSl = wxSlider:new(Dialog, ?wxID_ANY, Min, Min, Max, Style),
|
|
GasSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Gas Price")}]),
|
|
_ = wxStaticBoxSizer:add(GasSz, GasSl, zxw:flags(wide)),
|
|
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Dialog, ?wxID_OK),
|
|
Cancel = wxButton:new(Dialog, ?wxID_CANCEL),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
|
|
|
|
_ = wxBoxSizer:add(Sizer, FromSz, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Sizer, ToSz, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Sizer, AmtSz, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Sizer, DataSz, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, TTL_Sz, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, GasSz, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
|
|
ok = wxDialog:setSizer(Dialog, Sizer),
|
|
ok = wxBoxSizer:layout(Sizer),
|
|
ok = wxFrame:setSize(Dialog, {500, 450}),
|
|
ok = wxFrame:center(Dialog),
|
|
ok =
|
|
case wxDialog:showModal(Dialog) of
|
|
?wxID_OK ->
|
|
{ok, PK} = decode_account_id(ID),
|
|
TX =
|
|
#spend_tx{sender_id = aeser_id:create(account, PK),
|
|
recipient_id = wxTextCtrl:getValue(ToTx),
|
|
amount = wxTextCtrl:getValue(AmtTx),
|
|
gas_price = wxSlider:getValue(GasSl),
|
|
gas = 20000,
|
|
ttl = Height + wxSlider:getValue(TTL_Sl),
|
|
nonce = Nonce,
|
|
payload = wxTextCtrl:getValue(DataTx)},
|
|
clean_spend(ID, TX);
|
|
?wxID_CANCEL ->
|
|
ok
|
|
end,
|
|
ok = wxDialog:destroy(Dialog),
|
|
State.
|
|
|
|
clean_spend(_, #spend_tx{recipient_id = ""}) ->
|
|
ok;
|
|
clean_spend(ID, TX = #spend_tx{recipient_id = S}) when is_list(S) ->
|
|
case decode_account_id(S) of
|
|
{ok, PK} -> clean_spend(ID, TX#spend_tx{recipient_id = aeser_id:create(account, PK)});
|
|
Error -> tell("Decode recipient_id failed with: ~tp", [Error])
|
|
end;
|
|
clean_spend(ID, TX = #spend_tx{amount = S}) when is_list(S) ->
|
|
case string_to_price(S) of
|
|
{ok, Amount} -> clean_spend(ID, TX#spend_tx{amount = Amount});
|
|
{error, _} -> ok
|
|
end;
|
|
clean_spend(ID, TX = #spend_tx{gas_price = S}) when is_list(S) ->
|
|
case is_int(S) of
|
|
true -> clean_spend(ID, TX#spend_tx{gas_price = list_to_integer(S)});
|
|
false -> ok
|
|
end;
|
|
clean_spend(ID, TX = #spend_tx{gas = S}) when is_list(S) ->
|
|
case is_int(S) of
|
|
true -> clean_spend(ID, TX#spend_tx{gas = list_to_integer(S)});
|
|
false -> ok
|
|
end;
|
|
clean_spend(ID, TX = #spend_tx{payload = S}) when is_list(S) ->
|
|
clean_spend(ID, TX#spend_tx{payload = list_to_binary(S)});
|
|
clean_spend(ID, TX) ->
|
|
gmc_con:spend(ID, TX).
|
|
|
|
decode_account_id(S) when is_list(S) ->
|
|
decode_account_id(list_to_binary(S));
|
|
decode_account_id(B) ->
|
|
try
|
|
{account_pubkey, PK} = aeser_api_encoder:decode(B),
|
|
{ok, PK}
|
|
catch
|
|
E:R -> {E, R}
|
|
end.
|
|
|
|
is_int(S) ->
|
|
lists:all(fun(C) -> $0 =< C andalso C =< $9 end, S).
|
|
|
|
|
|
grids_dialogue(State = #s{frame = Frame, j = J}) ->
|
|
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Password")),
|
|
Sizer = wxBoxSizer:new(?wxVERTICAL),
|
|
Label = J("GRIDS URL"),
|
|
URL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J(Label)}]),
|
|
URL_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY),
|
|
_ = wxStaticBoxSizer:add(URL_Sz, URL_Tx, zxw:flags(wide)),
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Dialog, ?wxID_OK),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, URL_Sz, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
|
|
ok = wxDialog:setSizer(Dialog, Sizer),
|
|
ok = wxBoxSizer:layout(Sizer),
|
|
ok = wxFrame:center(Dialog),
|
|
ok = wxStyledTextCtrl:setFocus(URL_Tx),
|
|
ok =
|
|
case wxDialog:showModal(Dialog) of
|
|
?wxID_OK ->
|
|
case wxTextCtrl:getValue(URL_Tx) of
|
|
"" -> ok;
|
|
String -> gmc_con:grids(String)
|
|
end;
|
|
?wxID_CANCEL ->
|
|
ok
|
|
end,
|
|
State.
|
|
|
|
|
|
handle_button(Name, State) ->
|
|
ok = tell("Button Click: ~p", [Name]),
|
|
State.
|
|
|
|
|
|
|
|
do_selection(Selected,
|
|
State = #s{prefs = Prefs, accounts = Accounts,
|
|
balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}})
|
|
when Selected < length(Accounts) ->
|
|
#poa{id = ID, balances = Balances} = lists:nth(Selected + 1, Accounts),
|
|
[#balance{total = Pucks}] = Balances,
|
|
ok = wxStaticText:setLabel(I, ID),
|
|
ok = wxStaticText:setLabel(B, price_to_string(Pucks)),
|
|
NewPrefs = maps:put(selected, Selected, Prefs),
|
|
State#s{prefs = NewPrefs};
|
|
do_selection(_, State) ->
|
|
do_selection(0, State).
|
|
|
|
|
|
do_show(Accounts, State = #s{prefs = Prefs, picker = Picker}) ->
|
|
AKs = [Name || #poa{name = Name} <- Accounts],
|
|
ok = wxListBox:set(Picker, AKs),
|
|
case Accounts of
|
|
[] ->
|
|
State;
|
|
[_] ->
|
|
Selected = 0,
|
|
ok = wxListBox:setSelection(Picker, Selected),
|
|
do_selection(Selected, State#s{accounts = Accounts});
|
|
_ ->
|
|
Selected = maps:get(selected, Prefs, 0),
|
|
ok = wxListBox:setSelection(Picker, Selected),
|
|
do_selection(Selected, State#s{accounts = Accounts})
|
|
end.
|
|
|
|
|
|
do_wallet(none, #s{buttons = Buttons}) ->
|
|
#w{wx = WalletB} = lists:keyfind(wallet, #w.name, Buttons),
|
|
ok = wxButton:setLabel(WalletB, "[no wallet]");
|
|
do_wallet(Name, #s{buttons = Buttons}) ->
|
|
#w{wx = WalletB} = lists:keyfind(wallet, #w.name, Buttons),
|
|
ok = wxButton:setLabel(WalletB, Name).
|
|
|
|
|
|
do_chain(none, none, #s{buttons = Buttons}) ->
|
|
#w{wx = ChainB} = lists:keyfind(chain, #w.name, Buttons),
|
|
#w{wx = NodeB} = lists:keyfind(node, #w.name, Buttons),
|
|
ok = wxButton:setLabel(ChainB, "[no chain]"),
|
|
ok = wxButton:setLabel(NodeB, "[no node]");
|
|
do_chain(ChainID, #node{ip = IP}, #s{buttons = Buttons}) ->
|
|
#w{wx = ChainB} = lists:keyfind(chain, #w.name, Buttons),
|
|
#w{wx = NodeB} = lists:keyfind(node, #w.name, Buttons),
|
|
Address =
|
|
case inet:is_ip_address(IP) of
|
|
true -> inet:ntoa(IP);
|
|
false -> IP
|
|
end,
|
|
Label = unicode:characters_to_list(ChainID),
|
|
ok = wxButton:setLabel(ChainB, Label),
|
|
ok = wxButton:setLabel(NodeB, Address).
|
|
|
|
|
|
handle_trouble(Info, #s{frame = Frame}) ->
|
|
zxw:show_message(Frame, Info).
|
|
|
|
|
|
do_ask_password(#s{frame = Frame, prefs = Prefs, j = J}) ->
|
|
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Password")),
|
|
Sizer = wxBoxSizer:new(?wxVERTICAL),
|
|
Label = J("Password (leave blank for no password)"),
|
|
PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label}]),
|
|
PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
|
|
_ = wxStaticBoxSizer:add(PassSz, PassTx, zxw:flags(wide)),
|
|
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
|
|
Affirm = wxButton:new(Dialog, ?wxID_OK),
|
|
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
|
|
_ = wxBoxSizer:add(Sizer, PassSz, zxw:flags(base)),
|
|
_ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)),
|
|
ok = wxDialog:setSizer(Dialog, Sizer),
|
|
ok = wxBoxSizer:layout(Sizer),
|
|
ok = wxFrame:center(Dialog),
|
|
ok = wxStyledTextCtrl:setFocus(PassTx),
|
|
Path =
|
|
maps:get(last,
|
|
Prefs,
|
|
filename:join(zx_lib:path(var, "otpr", "clutch"), "default.gaju")),
|
|
ok =
|
|
case wxDialog:showModal(Dialog) of
|
|
?wxID_OK ->
|
|
Phrase =
|
|
case wxTextCtrl:getValue(PassTx) of
|
|
"" -> none;
|
|
P -> P
|
|
end,
|
|
gmc_con:open_wallet(Path, Phrase);
|
|
?wxID_CANCEL ->
|
|
gmc_con:open_wallet(Path, none)
|
|
end,
|
|
wxDialog:destroy(Dialog).
|
|
|
|
|
|
|
|
%%% Helpers
|
|
|
|
|
|
price_to_string(Pucks) ->
|
|
Gaju = 1_000_000_000_000_000_000,
|
|
H = integer_to_list(Pucks div Gaju),
|
|
R = Pucks rem Gaju,
|
|
case string:strip(lists:flatten(io_lib:format("~18..0w", [R])), right, $0) of
|
|
[] -> H;
|
|
T -> string:join([H, T], ".")
|
|
end.
|
|
|
|
string_to_price(String) ->
|
|
case string:split(String, ".") of
|
|
[H] -> join_price(H, "0");
|
|
[H, T] -> join_price(H, T);
|
|
_ -> {error, bad_price}
|
|
end.
|
|
|
|
join_price(H, T) ->
|
|
try
|
|
Parts = [H, string:pad(T, 18, trailing, $0)],
|
|
Price = list_to_integer(unicode:characters_to_list(Parts)),
|
|
case Price < 0 of
|
|
false -> {ok, Price};
|
|
true -> {error, negative_price}
|
|
end
|
|
catch
|
|
error:R -> {error, R}
|
|
end.
|