97 Commits

Author SHA1 Message Date
zxq9 903a32931d Update deps 2026-05-10 18:00:59 +09:00
zxq9 cfafa27c2f Merge pull request 'Make Contract Calls Great Again' (#35) from iface3 into master
Reviewed-on: #35
2026-05-10 15:39:24 +09:00
zxq9 46537a896f Update deps 2026-05-10 15:35:31 +09:00
zxq9 97589a1da5 Remove wx logging junk 2026-05-10 13:31:57 +09:00
zxq9 e5db26ab4c verup 2026-05-10 12:36:44 +09:00
zxq9 28f4a20bc0 Adapt to possible hz outputs for contract source 2026-05-09 21:44:04 +09:00
zxq9 6f9555a7c9 Handle invalid calls slightly better 2026-05-07 19:53:10 +09:00
zxq9 0c9e9805f0 Fix highlighting 2026-05-07 18:43:43 +09:00
zxq9 115ca656ba Marginally better line numbers 2026-05-07 17:04:43 +09:00
zxq9 5a745af71b Line number colors 2026-05-07 16:46:04 +09:00
zxq9 1c59ed413a Line numbers 2026-05-07 16:28:23 +09:00
zxq9 ca933a33db WIP 2026-05-07 12:46:00 +09:00
zxq9 b4dbf21c41 WIP: Fixing silly display cases 2026-05-06 18:31:01 +09:00
zxq9 41bd9eeacd Ajust spacing 2026-05-06 13:10:38 +09:00
zxq9 b071467faa Make Contract Calls Great Again 2026-05-06 12:46:26 +09:00
zxq9 8b390f8d82 WIP 2026-05-05 22:11:14 +09:00
zxq9 2a52516fd3 WIP 2026-05-05 13:47:38 +09:00
zxq9 223552324f WIP: Breaking reminder to add value amounts to total call cost 2026-05-05 00:27:59 +09:00
zxq9 ae8c659c03 WIP: Add gd_con:list_calls/0 and gd_v_call:tx_*/1 2026-05-04 23:58:02 +09:00
zxq9 90d99e1eca WIP 2026-05-03 06:05:18 +09:00
zxq9 ac18ecc916 WIP: De-kajiggering 2026-05-01 14:03:07 +09:00
zxq9 d1ee4c6a24 WIP 2026-04-29 12:55:26 +09:00
zxq9 0a671e7c9c WIP 2026-04-28 18:46:43 +09:00
zxq9 7008195090 WIP 2026-04-28 14:39:37 +09:00
zxq9 24ce75f520 WIP 2026-04-28 14:15:28 +09:00
zxq9 fadc252fb2 WIP 2026-04-27 19:00:47 +09:00
zxq9 a4db8d9a95 WIP 2026-04-07 18:40:17 +09:00
zxq9 acd84d65a8 WIP 2026-04-06 17:28:31 +09:00
zxq9 9365c33351 WIP 2026-04-06 14:42:21 +09:00
zxq9 9fa365e83f WIP 2026-04-06 10:59:45 +09:00
zxq9 713650f88b doodles 2026-02-27 01:34:30 +09:00
zxq9 ae17d21f4f doodles 2026-02-25 16:21:56 +09:00
zxq9 fcf44f55a1 De-snaggle some of the snaggle-toothed save routine nonsense 2026-02-13 15:07:22 +09:00
zxq9 3a570f000e Adapting in the new hz functions 2026-02-12 17:46:52 +09:00
zxq9 34a73a8e99 Fix silly state bug 2026-01-28 19:58:55 +09:00
zxq9 df44118463 WIP 2026-01-08 15:59:46 +09:00
zxq9 f9cb72598f WIP 2026-01-06 19:49:10 +09:00
zxq9 66f5795c49 WIP 2026-01-05 21:06:04 +09:00
zxq9 d2d9ae613e WIP 2026-01-05 15:08:05 +09:00
zxq9 2a52b593bb Merge pull request 'Fix wallet open timer' (#33) from iface into master
Reviewed-on: #33
2025-12-30 23:11:42 +09:00
zxq9 e10236214d Fix wallet open timer 2025-12-30 23:12:43 +09:00
zxq9 b0b3392c91 Adjust timeout. Verup. 2025-12-30 22:49:29 +09:00
zxq9 7697417dd8 Merge pull request 'iface' (#32) from iface into master
Reviewed-on: #32
2025-12-30 22:49:01 +09:00
zxq9 3e638cd100 Pull in out-comment 2025-12-30 22:48:01 +09:00
zxq9 9ea34f43bd Merge pull request 'iface' (#31) from iface into master
Reviewed-on: #31
2025-12-30 22:32:32 +09:00
zxq9 347aec2e46 Merge branch 'master' into iface 2025-12-30 22:25:39 +09:00
zxq9 d59278a26c Drop net* 2025-12-30 18:42:37 +09:00
zxq9 07875ab0e0 Kill spend window 2025-12-30 18:00:37 +09:00
zxq9 03b1f31935 Fix balance check 2025-12-30 17:05:56 +09:00
zxq9 99669a50c7 Catch HZ timeout on balance check 2025-12-30 16:33:32 +09:00
zxq9 52994a12cb Monkeypatch net crash on open 2025-12-30 16:27:29 +09:00
zxq9 8a8bee0bbd WIP 2025-12-27 21:04:30 +09:00
zxq9 39b2e48f68 WIP 2025-12-23 17:02:52 +09:00
zxq9 638c88f900 Dekajigger the kajiggerlicious SpendTX 2025-12-23 15:59:48 +09:00
zxq9 d9acde1b83 WIP :Move formatting functions 2025-12-01 11:52:00 +09:00
zxq9 6a8c4fe1e1 WIP: Add formatter and some other stuff 2025-11-29 14:33:15 +09:00
zxq9 d70f5bb389 WIP 2025-11-19 21:18:18 +09:00
zxq9 d7a8a81fa9 Update zxw dep 2025-11-07 19:56:31 +09:00
zxq9 5a4c934d10 WIP 2025-11-07 18:51:18 +09:00
zxq9 c59ef4b14e WIP 2025-11-07 15:45:57 +09:00
zxq9 bd047a6a46 WIP 2025-11-07 13:13:27 +09:00
zxq9 76f9d074a9 Merge pull request 'binarysign' (#29) from binarysign into master
Reviewed-on: #29
2025-11-01 11:32:03 +09:00
zxq9 07628a71b3 Merge branch 'binarysign' into iface 2025-10-29 16:59:04 +09:00
zxq9 363dfaf271 Update hz dep 2025-10-29 16:16:23 +09:00
zxq9 25550cca32 Verup 2025-10-29 16:15:29 +09:00
zxq9 c452604f4b WIP 2025-10-29 14:05:56 +09:00
zxq9 4446357437 Add interface functions 2025-10-25 12:11:03 +09:00
zxq9 d8b8fee232 WIP: Adding binary signatures 2025-10-25 10:41:32 +09:00
zxq9 6a1c8ecf62 WIP 2025-10-17 19:35:55 +09:00
Jarvis Carroll b8a31e994f factor compile/aaci logic
Instead of removing `init` and changing the definition of what the AACI
is, we can just remove it in the one case that needs `init` removed,
which is load3.
2025-08-22 22:57:12 +10:00
Jarvis Carroll 1d1e735b16 Remove some trace tells
d'oh
2025-08-22 19:59:47 +10:00
Jarvis Carroll 51463b8b74 Make deploy thingy break less 2025-08-22 19:52:42 +10:00
Jarvis Carroll f78d929fb9 Sketchy list, map, variant interface
None of these are quite working right, but the hive contract can open in
the 'open from chain' button now.
2025-08-22 18:38:57 +10:00
zxq9 b4ea4bdf10 Fix bad widget call 2025-08-22 15:24:32 +09:00
Jarvis Carroll dc0b620a4c Make styled text control when loading 2025-08-22 15:22:39 +10:00
Jarvis Carroll 2952822a8f Debug print AACI instead 2025-08-22 14:21:25 +10:00
zxq9 2ecde986bf WIP 2025-08-15 13:24:39 +09:00
zxq9 5ebb532b86 Stop trying to pull the AACI from the build directly 2025-08-14 14:00:47 +09:00
zxq9 f92c5fbde0 First-run wallet creatorator (v0.7.0) (#24)
Adds a first-run wallet and account creator option for noobs.
If they select the "create a default wallet" option, they are jumped to the name + password screen for wallet creation, and a new account is generated for them named "Account 1".

This leapfrogs the problem of users having to know what is going on with the blockchain and wallet at all before getting started.
#18
#19

Reviewed-on: #24
Reviewed-by: Ulf Wiger <ulfwiger@qpq.swiss>
Co-authored-by: Craig Everett <zxq9@zxq9.com>
Co-committed-by: Craig Everett <zxq9@zxq9.com>
2025-08-07 15:47:05 +09:00
zxq9 a2a9da0d98 Fix UI problem where key interactions could be requests without any wallet being selected 2025-06-13 17:47:37 +09:00
zxq9 d595beb08d Minor display changes 2025-06-12 19:43:02 +09:00
zxq9 cf6f16e3db Updates 2025-06-12 19:25:36 +09:00
zxq9 88b2e986b7 Default walletses 2025-05-20 17:31:09 +09:00
zxq9 09d31e74e5 Add refresh tic 2025-05-09 21:17:20 +09:00
zxq9 86e9ff3e44 Fix runtime shutdown problem with GajuDesk 2025-05-08 13:06:27 +09:00
zxq9 57a81e7f3f Issue proper explorer URLs 2025-04-30 17:39:26 +09:00
zxq9 58a58c693f Minor usability tweaks 2025-04-25 22:52:25 +09:00
zxq9 28b0b2c6f3 Update dependencies and verup 2025-04-16 16:13:42 +09:00
zxq9 cbc823aba1 args (#10)
More complete types.

Reviewed-on: #10
Co-authored-by: Craig Everett <zxq9@zxq9.com>
Co-committed-by: Craig Everett <zxq9@zxq9.com>
2025-04-16 16:03:09 +09:00
zxq9 2bef1fd323 Fix app name 2025-04-03 15:01:16 +09:00
zxq9 509f36c403 Merge pull request 'First stab at supporting syntax highlighting via wxStyledTextCtrl' (#8) from uw-syntax-highlighting into master
Reviewed-on: #8
Reviewed-by: Craig Everett <craigeverett@qpq.swiss>
2025-04-02 16:34:57 +09:00
zxq9 1f17d36c42 Merge pull request 'style_massage' (#9) from style_massage into uw-syntax-highlighting
Reviewed-on: #9
Reviewed-by: Ulf Wiger <ulfwiger@qpq.swiss>
2025-03-31 20:11:53 +09:00
zxq9 ea6667c05f Remove keymaster; update dep 2025-03-31 15:58:56 +09:00
zxq9 020d3f554b Remove dead file 2025-03-31 15:39:25 +09:00
zxq9 dea44561ee Restyle whole buffer on event to fix style breaks
TODO: Make this more precise... some day
2025-03-31 15:36:31 +09:00
zxq9 9d70f98ed0 Make light/dark mode work 2025-03-31 14:41:03 +09:00
zxq9 9aa2c2f79d Give the main frame visibility on events 2025-03-31 13:58:48 +09:00
21 changed files with 2504 additions and 5633 deletions
+3 -2
View File
@@ -3,7 +3,8 @@
{registered,[]}, {registered,[]},
{included_applications,[]}, {included_applications,[]},
{applications,[stdlib,kernel,sasl,ssl]}, {applications,[stdlib,kernel,sasl,ssl]},
{vsn,"0.5.4"}, {vsn,"0.9.0"},
{modules,[gajudesk,gd_con,gd_grids,gd_gui,gd_jt,gd_key_master, {modules,[gajudesk,gd_con,gd_grids,gd_gui,gd_jt,gd_lib,
gd_m_spend,gd_m_wallet_importer,gd_sophia_editor,
gd_sup,gd_v,gd_v_devman,gd_v_netman,gd_v_wallman]}, gd_sup,gd_v,gd_v_devman,gd_v_netman,gd_v_wallman]},
{mod,{gajudesk,[]}}]}. {mod,{gajudesk,[]}}]}.
+6 -6
View File
@@ -1,7 +1,7 @@
%% Node, Chain and Net represent the physical network. %% Node, Chain and Net represent the physical network.
-record(node, -record(node,
{ip = {161,97,102,143} :: string() | inet:ip_address(), {ip = "groot.testnet.gajumaru.io" :: string() | inet:ip_address(),
external = 3013 :: none | inet:port_number(), % 3013 external = 3013 :: none | inet:port_number(), % 3013
internal = none :: none | inet:port_number(), % 3113 internal = none :: none | inet:port_number(), % 3113
rosetta = none :: none | inet:port_number(), % 8080 rosetta = none :: none | inet:port_number(), % 8080
@@ -10,13 +10,13 @@
-record(chain, -record(chain,
{id = <<"groot.devnet">> :: binary(), {id = <<"groot.testnet">> :: binary(),
coins = ["gaju"] :: [string()], coins = ["gaju"] :: [string()],
nodes = [#node{}] :: [#node{}]}). nodes = [#node{}] :: [#node{}]}).
-record(net, -record(net,
{id = <<"devnet">> :: binary(), {id = <<"testnet">> :: binary(),
chains = [#chain{}] :: [#chain{}]}). chains = [#chain{}] :: [#chain{}]}).
@@ -30,7 +30,7 @@
-record(coin, -record(coin,
{id = "gaju" :: string(), {id = "gaju" :: string(),
mint = <<"groot.devnet">> :: binary(), mint = <<"groot.testnet">> :: binary(),
acs = [#ac{}] :: [#ac{}]}). acs = [#ac{}] :: [#ac{}]}).
@@ -39,7 +39,7 @@
-record(balance, -record(balance,
{coin = "gaju" :: string(), {coin = "gaju" :: string(),
total = 0 :: non_neg_integer(), total = 0 :: non_neg_integer(),
dist = [{<<"groot.devnet">>, 0}] :: [{Chain :: binary(), non_neg_integer()}]}). dist = [{<<"groot.testnet">>, 0}] :: [{Chain :: binary(), non_neg_integer()}]}).
-record(poa, -record(poa,
@@ -81,7 +81,7 @@
name = "" :: string(), name = "" :: string(),
poas = [] :: [#poa{}], poas = [] :: [#poa{}],
keys = [] :: [#key{}], keys = [] :: [#key{}],
chain_id = <<"groot.devnet">> :: binary(), chain_id = <<"groot.testnet">> :: binary(),
endpoint = #node{} :: #node{}, endpoint = #node{} :: #node{},
nets = [#net{}] :: [#net{}], nets = [#net{}] :: [#net{}],
txs = #{} :: gajudesk:key_txs()}). txs = #{} :: gajudesk:key_txs()}).
+5
View File
@@ -0,0 +1,5 @@
% Widgets
-record(w,
{name = none :: string() | atom() | {FunName :: binary(), call | dryr},
id = 0 :: integer(),
wx = none :: none | wx:wx_object()}).
-4096
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -3,7 +3,7 @@
%%% @end %%% @end
-module(gajudesk). -module(gajudesk).
-vsn("0.5.4"). -vsn("0.9.0").
-behavior(application). -behavior(application).
-author("Craig Everett <craigeverett@qpq.swiss>"). -author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>"). -copyright("QPQ AG <info@qpq.swiss>").
+533 -308
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -37,7 +37,7 @@
%%% @end %%% @end
-module(gd_grids). -module(gd_grids).
-vsn("0.5.4"). -vsn("0.9.0").
-author("Craig Everett <craigeverett@qpq.swiss>"). -author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>"). -copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later"). -license("GPL-3.0-or-later").
@@ -59,7 +59,7 @@
Reason :: bad_url. Reason :: bad_url.
parse(URL) -> parse(URL) ->
case uri_string:parse(URL) of case uri_string:parse(string:trim(URL)) of
#{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grids"} -> #{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grids"} ->
spend(R, chain, list_to_binary(H), Q); spend(R, chain, list_to_binary(H), Q);
#{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grid"} -> #{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grid"} ->
+143 -235
View File
@@ -3,7 +3,7 @@
%%% @end %%% @end
-module(gd_gui). -module(gd_gui).
-vsn("0.5.4"). -vsn("0.9.0").
-author("Craig Everett <craigeverett@qpq.swiss>"). -author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>"). -copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later"). -license("GPL-3.0-or-later").
@@ -17,13 +17,9 @@
handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). handle_call/3, handle_cast/2, handle_info/2, handle_event/2]).
-include("$zx_include/zx_logger.hrl"). -include("$zx_include/zx_logger.hrl").
-include("gd.hrl"). -include("gd.hrl").
-include("gdl.hrl").
-record(w,
{name = none :: atom(),
id = 0 :: integer(),
wx = none :: none | wx:wx_object()}).
-record(h, -record(h,
{win = none :: none | wx:wx_object(), {win = none :: none | wx:wx_object(),
sz = none :: none | wx:wx_object()}). sz = none :: none | wx:wx_object()}).
@@ -32,13 +28,13 @@
{wx = none :: none | wx:wx_object(), {wx = none :: none | wx:wx_object(),
frame = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(),
sizer = none :: none | wx:wx_object(), sizer = none :: none | wx:wx_object(),
lang = en :: en | jp, lang = en_US :: en_US | ja_JP,
j = none :: none | fun(), j = none :: none | fun(),
prefs = #{} :: #{atom() := term()}, prefs = #{} :: #{atom() := term()},
accounts = [] :: [gajudesk:poa()], accounts = [] :: [gajudesk:poa()],
picker = none :: none | wx:wx_object(), picker = none :: none | wx:wx_object(),
id = {#w{}, #w{}} :: labeled(), id = {#w{}, #w{}} :: labeled(),
balance = {#w{}, #w{}} :: labeled(), balance = #w{} :: #w{},
buttons = [] :: [widget()], buttons = [] :: [widget()],
history = #h{} :: #h{}}). history = #h{} :: #h{}}).
@@ -84,26 +80,32 @@ start_link(Accounts) ->
init(Prefs) -> init(Prefs) ->
ok = log(info, "GUI starting..."), ok = log(info, "GUI starting..."),
Lang = maps:get(lang, Prefs, en_us), Lang = maps:get(lang, Prefs, en_US),
Trans = gd_jt:read_translations(?MODULE), Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans), J = gd_jt:j(Lang, Trans),
AppName = J("GajuDesk"),
VSN = proplists:get_value(vsn, ?MODULE:module_info(attributes)),
Wx = wx:new(), Wx = wx:new(),
Frame = wxFrame:new(Wx, ?wxID_ANY, J("GajuDesk")), Frame = wxFrame:new(Wx, ?wxID_ANY, AppName ++ " v" ++ VSN),
Panel = wxWindow:new(Frame, ?wxID_ANY),
TopSz = wxBoxSizer:new(?wxVERTICAL),
_ = wxBoxSizer:add(TopSz, Panel, zxw:flags(wide)),
MainSz = wxBoxSizer:new(?wxVERTICAL), MainSz = wxBoxSizer:new(?wxVERTICAL),
Picker = wxListBox:new(Frame, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]), Picker = wxListBox:new(Panel, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]),
WallB = wxButton:new(Frame, ?wxID_ANY, [{label, "[none]"}, {style, ?wxBORDER_NONE}]), WallB = wxButton:new(Panel, ?wxID_ANY, [{label, "[none]"}, {style, ?wxBORDER_NONE}]),
WallW = #w{name = wallet, id = wxButton:getId(WallB), wx = WallB}, WallW = #w{name = wallet, id = wxButton:getId(WallB), wx = WallB},
ChainB = wxButton:new(Frame, ?wxID_ANY, [{label, "[ChainID]"}, {style, ?wxBORDER_NONE}]), ChainB = wxButton:new(Panel, ?wxID_ANY, [{label, "[ChainID]"}, {style, ?wxBORDER_NONE}]),
_ = wxButton:disable(ChainB),
ChainW = #w{name = chain, id = wxButton:getId(ChainB), wx = ChainB}, ChainW = #w{name = chain, id = wxButton:getId(ChainB), wx = ChainB},
NodeB = wxButton:new(Frame, ?wxID_ANY, [{label, "[Node]"}, {style, ?wxBORDER_NONE}]), NodeB = wxButton:new(Panel, ?wxID_ANY, [{label, "[Node]"}, {style, ?wxBORDER_NONE}]),
NodeW = #w{name = node, id = wxButton:getId(NodeB), wx = NodeB}, NodeW = #w{name = node, id = wxButton:getId(NodeB), wx = NodeB},
DevB = wxButton:new(Frame, ?wxID_ANY, [{label, "𝑓 () →"}]), DevB = wxButton:new(Panel, ?wxID_ANY, [{label, "𝑓 () →"}]),
DevW = #w{name = dev, id = wxButton:getId(DevB), wx = DevB}, DevW = #w{name = dev, id = wxButton:getId(DevB), wx = DevB},
ID_L = wxStaticText:new(Frame, ?wxID_ANY, J("Account ID: ")), ID_L = wxStaticText:new(Panel, ?wxID_ANY, J("Account ID: ")),
ID_T = wxStaticText:new(Frame, ?wxID_ANY, ""), ID_T = wxStaticText:new(Panel, ?wxID_ANY, ""),
ID_W = ID_W =
{#w{id = wxStaticText:getId(ID_L), wx = ID_L}, {#w{id = wxStaticText:getId(ID_L), wx = ID_L},
#w{id = wxStaticText:getId(ID_T), wx = ID_T}}, #w{id = wxStaticText:getId(ID_T), wx = ID_T}},
@@ -111,13 +113,9 @@ init(Prefs) ->
_ = wxSizer:add(ID_Sz, ID_L, zxw:flags(base)), _ = wxSizer:add(ID_Sz, ID_L, zxw:flags(base)),
_ = wxSizer:add(ID_Sz, ID_T, zxw:flags(wide)), _ = wxSizer:add(ID_Sz, ID_T, zxw:flags(wide)),
BalanceL = wxStaticText:new(Frame, ?wxID_ANY, ""), BalanceT = wxStaticText:new(Panel, ?wxID_ANY, hz_format:amount(0)),
BalanceT = wxStaticText:new(Frame, ?wxID_ANY, price_to_string(0)), Balance = #w{id = wxStaticText:getId(BalanceT), wx = BalanceT},
Balance =
{#w{id = wxStaticText:getId(BalanceL), wx = BalanceL},
#w{id = wxStaticText:getId(BalanceT), wx = BalanceT}},
BalanceSz = wxBoxSizer:new(?wxHORIZONTAL), BalanceSz = wxBoxSizer:new(?wxHORIZONTAL),
_ = wxSizer:add(BalanceSz, BalanceL, zxw:flags(base)),
_ = wxSizer:add(BalanceSz, BalanceT, zxw:flags(wide)), _ = wxSizer:add(BalanceSz, BalanceT, zxw:flags(wide)),
NumbersSz = wxBoxSizer:new(?wxVERTICAL), NumbersSz = wxBoxSizer:new(?wxVERTICAL),
@@ -131,7 +129,7 @@ init(Prefs) ->
{rename, J("Rename")}, {rename, J("Rename")},
{drop_key, J("Delete")}, {drop_key, J("Delete")},
{send, J("Send Money")}, {send, J("Send Money")},
{recv, J("Receive Money")}, % {recv, J("Receive Money")},
{grids, J("GRIDS URL")}, {grids, J("GRIDS URL")},
{copy, J("Copy")}, {copy, J("Copy")},
{www, J("WWW")}, {www, J("WWW")},
@@ -139,11 +137,17 @@ init(Prefs) ->
MakeButton = MakeButton =
fun({Name, Label}) -> fun({Name, Label}) ->
B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]), B = wxButton:new(Panel, ?wxID_ANY, [{label, Label}]),
#w{name = Name, id = wxButton:getId(B), wx = B} #w{name = Name, id = wxButton:getId(B), wx = B}
end, end,
Buttons = [WallW, ChainW, NodeW, DevW | lists:map(MakeButton, ButtonTemplates)], 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), ChainSz = wxBoxSizer:new(?wxHORIZONTAL),
AccountSz = wxBoxSizer:new(?wxHORIZONTAL), AccountSz = wxBoxSizer:new(?wxHORIZONTAL),
@@ -173,18 +177,18 @@ init(Prefs) ->
_ = wxSizer:add(AccountSz, DropBn, zxw:flags(wide)), _ = wxSizer:add(AccountSz, DropBn, zxw:flags(wide)),
#w{wx = SendBn} = lists:keyfind(send, #w.name, Buttons), #w{wx = SendBn} = lists:keyfind(send, #w.name, Buttons),
#w{wx = RecvBn} = lists:keyfind(recv, #w.name, Buttons), % #w{wx = RecvBn} = lists:keyfind(recv, #w.name, Buttons),
#w{wx = GridsBn} = lists:keyfind(grids, #w.name, Buttons), #w{wx = GridsBn} = lists:keyfind(grids, #w.name, Buttons),
_ = wxSizer:add(ActionsSz, SendBn, zxw:flags(wide)), _ = wxSizer:add(ActionsSz, SendBn, zxw:flags(wide)),
_ = wxSizer:add(ActionsSz, RecvBn, zxw:flags(wide)), % _ = wxSizer:add(ActionsSz, RecvBn, zxw:flags(wide)),
_ = wxSizer:add(ActionsSz, GridsBn, zxw:flags(wide)), _ = wxSizer:add(ActionsSz, GridsBn, zxw:flags(wide)),
#w{wx = Refresh} = lists:keyfind(refresh, #w.name, Buttons), #w{wx = Refresh} = lists:keyfind(refresh, #w.name, Buttons),
HistoryWin = wxScrolledWindow:new(Frame), % HistoryWin = wxScrolledWindow:new(Panel),
HistorySz = wxBoxSizer:new(?wxVERTICAL), % HistorySz = wxBoxSizer:new(?wxVERTICAL),
ok = wxScrolledWindow:setSizerAndFit(HistoryWin, HistorySz), % ok = wxScrolledWindow:setSizerAndFit(HistoryWin, HistorySz),
ok = wxScrolledWindow:setScrollRate(HistoryWin, 5, 5), % ok = wxScrolledWindow:setScrollRate(HistoryWin, 5, 5),
_ = wxSizer:add(MainSz, ChainSz, zxw:flags(base)), _ = wxSizer:add(MainSz, ChainSz, zxw:flags(base)),
_ = wxSizer:add(MainSz, AccountSz, zxw:flags(base)), _ = wxSizer:add(MainSz, AccountSz, zxw:flags(base)),
@@ -192,8 +196,9 @@ init(Prefs) ->
_ = wxSizer:add(MainSz, DetailsSz, zxw:flags(base)), _ = wxSizer:add(MainSz, DetailsSz, zxw:flags(base)),
_ = wxSizer:add(MainSz, ActionsSz, zxw:flags(base)), _ = wxSizer:add(MainSz, ActionsSz, zxw:flags(base)),
_ = wxSizer:add(MainSz, Refresh, zxw:flags(base)), _ = wxSizer:add(MainSz, Refresh, zxw:flags(base)),
_ = wxSizer:add(MainSz, HistoryWin, zxw:flags(wide)), % _ = wxSizer:add(MainSz, HistoryWin, zxw:flags(wide)),
ok = wxFrame:setSizer(Frame, MainSz), ok = wxWindow:setSizer(Panel, MainSz),
ok = wxFrame:setSizer(Frame, TopSz),
ok = wxSizer:layout(MainSz), ok = wxSizer:layout(MainSz),
ok = gd_v:safe_size(Frame, Prefs), ok = gd_v:safe_size(Frame, Prefs),
@@ -207,8 +212,8 @@ init(Prefs) ->
picker = Picker, picker = Picker,
id = ID_W, id = ID_W,
balance = Balance, balance = Balance,
buttons = Buttons, buttons = Buttons},
history = #h{win = HistoryWin, sz = HistorySz}}, % history = #h{win = HistoryWin, sz = HistorySz}},
{Frame, State}. {Frame, State}.
@@ -293,7 +298,7 @@ handle_event(#wx{event = #wxCommand{type = command_button_clicked},
#w{name = mnemonic} -> show_mnemonic(State); #w{name = mnemonic} -> show_mnemonic(State);
#w{name = rename} -> rename_key(State); #w{name = rename} -> rename_key(State);
#w{name = drop_key} -> drop_key(State); #w{name = drop_key} -> drop_key(State);
#w{name = copy} -> copy(State); #w{name = copy} -> copy_pk(State);
#w{name = www} -> www(State); #w{name = www} -> www(State);
#w{name = send} -> spend(State); #w{name = send} -> spend(State);
#w{name = grids} -> grids_dialogue(State); #w{name = grids} -> grids_dialogue(State);
@@ -328,6 +333,7 @@ handle_event(Event, State) ->
handle_troubling(#s{frame = Frame}, Info) -> handle_troubling(#s{frame = Frame}, Info) ->
ok = wxFrame:raise(Frame),
zxw:show_message(Frame, Info). zxw:show_message(Frame, Info).
@@ -352,6 +358,7 @@ refresh(State) ->
wallman(State) -> wallman(State) ->
ok = gd_con:show_ui(gd_v_wallman), ok = gd_con:show_ui(gd_v_wallman),
ok = gd_v_wallman:to_front(),
State. State.
@@ -581,7 +588,7 @@ show_mnemonic(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) -
ok = ok =
case wxDialog:showModal(Dialog) of case wxDialog:showModal(Dialog) of
?wxID_CANCEL -> ok; ?wxID_CANCEL -> ok;
?wxID_OK -> copy_to_clipboard(Mnemonic) ?wxID_OK -> gd_lib:copy_to_clipboard(Mnemonic)
end, end,
ok = wxDialog:destroy(Dialog), ok = wxDialog:destroy(Dialog),
State. State.
@@ -597,33 +604,15 @@ rename_key(State = #s{picker = Picker}) ->
rename_key(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) -> rename_key(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) ->
#poa{id = ID, name = Name} = lists:nth(Selected, Accounts), #poa{id = ID, name = Name} = lists:nth(Selected, Accounts),
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key")), Title = J("Rename Key"),
Sizer = wxBoxSizer:new(?wxVERTICAL), Label = J("New Name"),
NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name (Optional)")}]), Options = [{label, Label}, {init, Name}, selected, empty],
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 = ok =
case wxDialog:showModal(Dialog) of case zxw_modal_text:show(Frame, Title, Options) of
?wxID_OK -> {ok, ""} -> gd_con:rename_key(ID, binary_to_list(ID));
NewName = wxTextCtrl:getValue(NameTx), {ok, NewName} -> gd_con:rename_key(ID, NewName);
gd_con:rename_key(ID, NewName); cancel -> ok
?wxID_CANCEL ->
ok
end, end,
ok = wxDialog:destroy(Dialog),
State. State.
@@ -667,38 +656,33 @@ drop_key(Selected, State = #s{frame = Frame, j = J, accounts = Accounts, prefs =
State#s{prefs = NewPrefs}. State#s{prefs = NewPrefs}.
copy(State = #s{id = {_, #w{wx = ID_T}}}) -> copy_pk(State = #s{id = {_, #w{wx = ID_T}}}) ->
String = wxStaticText:getLabel(ID_T), String = wxStaticText:getLabel(ID_T),
ok = copy_to_clipboard(String), ok = gd_lib:copy_to_clipboard(String),
State. State.
copy_to_clipboard(String) ->
CB = wxClipboard:get(), www(State = #s{id = {_, #w{wx = ID_T}}, j = J}) ->
case wxClipboard:open(CB) of case wxStaticText:getLabel(ID_T) of
true -> "" ->
Text = wxTextDataObject:new([{text, String}]), ok = handle_troubling(State, J("No key selected.")),
case wxClipboard:setData(CB, Text) of State;
true -> ID ->
R = wxClipboard:flush(CB), ok = www2(State, ID),
log(info, "String copied to system clipboard. Flushed: ~p", [R]); State
false ->
log(info, "Failed to copy to clipboard")
end,
ok = wxClipboard:close(CB);
false ->
log(info, "Failed to acquire the clipboard.")
end. end.
www2(State = #s{j = J}, AccountID) ->
www(State = #s{id = {_, #w{wx = ID_T}}}) -> case gd_con:network() of
String = wxStaticText:getLabel(ID_T), {ok, ChainID} ->
URL = unicode:characters_to_list(["https://aescan.io/accounts/", String]), URL = unicode:characters_to_list(["https://", ChainID, ".gajumaru.io/account/", AccountID]),
ok =
case wx_misc:launchDefaultBrowser(URL) of case wx_misc:launchDefaultBrowser(URL) of
true -> log(info, "Opened in browser: ~ts", [URL]); true -> log(info, "Opened in browser: ~ts", [URL]);
false -> log(info, "Failed to open browser: ~ts", [URL]) false -> log(info, "Failed to open browser: ~ts", [URL])
end, end;
State. none ->
handle_troubling(State, J("No chain assigned."))
end.
spend(State = #s{accounts = []}) -> spend(State = #s{accounts = []}) ->
@@ -721,129 +705,29 @@ spend(Selected, State = #s{accounts = Accounts}) ->
end. end.
spend2(#poa{id = ID, name = Name}, Nonce, Height, State = #s{frame = Frame, j = J}) -> 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, ")"], Account = [Name, " (", ID, ")"],
FromTx = wxStaticText:new(Dialog, ?wxID_ANY, Account), Args = {Account, J},
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 = ok =
case wxDialog:showModal(Dialog) of case gd_m_spend:show(Frame, Args) of
?wxID_OK -> {ok, Partial = #spend_tx{ttl = TTL}} ->
TX = TX =
#spend_tx{sender_id = ID, Partial#spend_tx{sender_id = ID,
recipient_id = wxTextCtrl:getValue(ToTx),
amount = wxTextCtrl:getValue(AmtTx),
gas_price = wxSlider:getValue(GasSl),
gas = 20000, gas = 20000,
ttl = Height + wxSlider:getValue(TTL_Sl), ttl = Height + TTL,
nonce = Nonce, nonce = Nonce},
payload = wxTextCtrl:getValue(DataTx)}, gd_con:spend(TX);
clean_spend(TX); cancel ->
?wxID_CANCEL ->
ok ok
end, end,
ok = wxDialog:destroy(Dialog),
State. 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}) -> grids_dialogue(State = #s{frame = Frame, j = J}) ->
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("GRIDS URL")), Title = 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 = ok =
case wxDialog:showModal(Dialog) of case zxw_modal_text:show(Frame, Title) of
?wxID_OK -> {ok, String} -> gd_con:grids(String);
case wxTextCtrl:getValue(URL_Tx) of cancel -> ok
"" -> ok;
String -> gd_con:grids(String)
end;
?wxID_CANCEL ->
ok
end, end,
State. State.
@@ -856,13 +740,13 @@ handle_button(Name, State) ->
do_selection(Selected, do_selection(Selected,
State = #s{prefs = Prefs, accounts = Accounts, State = #s{prefs = Prefs, accounts = Accounts,
balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}}) balance = #w{wx = B}, id = {_, #w{wx = I}}})
when Selected < length(Accounts) -> when Selected < length(Accounts) ->
OneBasedIndex = Selected + 1, OneBasedIndex = Selected + 1,
#poa{id = ID, balances = Balances} = lists:nth(OneBasedIndex, Accounts), #poa{id = ID, balances = Balances} = lists:nth(OneBasedIndex, Accounts),
[#balance{total = Pucks}] = Balances, [#balance{total = Pucks}] = Balances,
ok = wxStaticText:setLabel(I, ID), ok = wxStaticText:setLabel(I, ID),
ok = wxStaticText:setLabel(B, price_to_string(Pucks)), ok = wxStaticText:setLabel(B, hz_format:amount(Pucks)),
ok = gd_con:selected(OneBasedIndex), ok = gd_con:selected(OneBasedIndex),
NewPrefs = maps:put(selected, Selected, Prefs), NewPrefs = maps:put(selected, Selected, Prefs),
State#s{prefs = NewPrefs}; State#s{prefs = NewPrefs};
@@ -889,7 +773,7 @@ do_show(Accounts, State = #s{sizer = Sizer, prefs = Prefs, picker = Picker}) ->
ok = wxSizer:layout(Sizer), ok = wxSizer:layout(Sizer),
NewState. NewState.
clear_account(State = #s{balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}}) -> clear_account(State = #s{balance = #w{wx = B}, id = {_, #w{wx = I}}}) ->
ok = wxStaticText:setLabel(I, ""), ok = wxStaticText:setLabel(I, ""),
ok = wxStaticText:setLabel(B, ""), ok = wxStaticText:setLabel(B, ""),
State. State.
@@ -897,12 +781,28 @@ clear_account(State = #s{balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}}) ->
do_wallet(none, #s{buttons = Buttons}) -> do_wallet(none, #s{buttons = Buttons}) ->
#w{wx = WalletB} = lists:keyfind(wallet, #w.name, 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]"); ok = wxButton:setLabel(WalletB, "[no wallet]");
do_wallet(Name, #s{buttons = Buttons}) -> do_wallet(Name, #s{buttons = Buttons}) ->
#w{wx = WalletB} = lists:keyfind(wallet, #w.name, 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). ok = wxButton:setLabel(WalletB, Name).
key_buttons() ->
[make_key, recover, mnemonic, rename, drop_key].
do_chain(none, none, #s{buttons = Buttons}) -> do_chain(none, none, #s{buttons = Buttons}) ->
#w{wx = ChainB} = lists:keyfind(chain, #w.name, Buttons), #w{wx = ChainB} = lists:keyfind(chain, #w.name, Buttons),
#w{wx = NodeB} = lists:keyfind(node, #w.name, Buttons), #w{wx = NodeB} = lists:keyfind(node, #w.name, Buttons),
@@ -1018,6 +918,48 @@ do_grids_mess_sig2(Request = #{"grids" := 1,
?wxID_CANCEL -> ok ?wxID_CANCEL -> ok
end, end,
wxDialog:destroy(Dialog); wxDialog:destroy(Dialog);
do_grids_mess_sig2(Request = #{"grids" := 1,
"type" := "binary",
"url" := URL,
"public_id" := ID,
"payload" := Base64},
#s{frame = Frame, j = J}) ->
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Binary Data Signature Request")),
Sizer = wxBoxSizer:new(?wxVERTICAL),
Instruction =
J("The server at the URL below is requesting you sign the following binary data."),
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("Base-64 Data")}]),
MessStyle = ?wxTE_MULTILINE bor ?wxTE_READONLY,
MessTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{value, Base64}, {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_binary(Request);
?wxID_CANCEL -> ok
end,
wxDialog:destroy(Dialog);
do_grids_mess_sig2(Request = #{"grids" := 1, do_grids_mess_sig2(Request = #{"grids" := 1,
"type" := "tx", "type" := "tx",
"url" := URL, "url" := URL,
@@ -1076,37 +1018,3 @@ do_grids_mess_sig2(Request = #{"grids" := 1,
wxDialog:destroy(Dialog); wxDialog:destroy(Dialog);
do_grids_mess_sig2(BadRequest, _) -> do_grids_mess_sig2(BadRequest, _) ->
tell("Bad request: ~tp", [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.
+1 -1
View File
@@ -15,7 +15,7 @@
%%% translation library is retained). %%% translation library is retained).
-module(gd_jt). -module(gd_jt).
-vsn("0.5.4"). -vsn("0.9.0").
-export([read_translations/1, j/2, oneshot_j/2]). -export([read_translations/1, j/2, oneshot_j/2]).
-164
View File
@@ -1,164 +0,0 @@
%%% @doc
%%% Key functions go here.
%%%
%%% The main reason this is a module of its own is that in the original architecture
%%% it was a process rather than just a library of functions. Now that it exists, though,
%%% there is little motivation to cram everything here into the controller process's
%%% code.
%%% @end
-module(gd_key_master).
-vsn("0.5.4").
-export([make_key/2, encode/1, decode/1]).
-export([lcg/1]).
-include("gd.hrl").
make_key("", <<>>) ->
Pair = #{public := Public} = ecu_eddsa:sign_keypair(),
ID = gmser_api_encoder:encode(account_pubkey, Public),
Name = binary_to_list(ID),
#key{name = Name, id = ID, pair = Pair};
make_key("", Seed) ->
Pair = #{public := Public} = ecu_eddsa:sign_seed_keypair(Seed),
ID = gmser_api_encoder:encode(account_pubkey, Public),
Name = binary_to_list(ID),
#key{name = Name, id = ID, pair = Pair};
make_key(Name, <<>>) ->
Pair = #{public := Public} = ecu_eddsa:sign_keypair(),
ID = gmser_api_encoder:encode(account_pubkey, Public),
#key{name = Name, id = ID, pair = Pair};
make_key(Name, Seed) ->
Pair = #{public := Public} = ecu_eddsa:sign_seed_keypair(Seed),
ID = gmser_api_encoder:encode(account_pubkey, Public),
#key{name = Name, id = ID, pair = Pair}.
-spec encode(Secret) -> Phrase
when Secret :: binary(),
Phrase :: string().
%% @doc
%% The encoding and decoding procesures are written to be able to handle any
%% width of bitstring or binary and a variable size dictionary. The magic numbers
%% 32, 4096 and 12 have been dropped in because currently these are known, but that
%% will change in the future if the key size or type changes.
encode(Bin) ->
<<Number:(32 * 8)>> = Bin,
DictSize = 4096,
Words = read_words(),
% Width = chunksize(DictSize - 1, 2),
Width = 12,
Chunks = chunksize(Number, DictSize),
Binary = <<Number:(Chunks * Width)>>,
encode(Width, Binary, Words).
encode(Width, Bits, Words) ->
CheckSum = checksum(Width, Bits),
encode(Width, <<CheckSum:Width, Bits/bitstring>>, Words, []).
encode(_, <<>>, _, Acc) ->
unicode:characters_to_list(lists:join(" ", lists:reverse(Acc)));
encode(Width, Bits, Words, Acc) ->
<<I:Width, Rest/bitstring>> = Bits,
Word = lists:nth(I + 1, Words),
encode(Width, Rest, Words, [Word | Acc]).
-spec decode(Phrase) -> {ok, Secret} | {error, Reason}
when Phrase :: string(),
Secret :: binary(),
Reason :: bad_phrase | bad_word.
%% @doc
%% Reverses the encoded secret string back into its binary representation.
decode(Encoded) ->
DictSize = 4096,
Words = read_words(),
Width = chunksize(DictSize - 1, 2),
decode(Width, Words, Encoded).
decode(Width, Words, Encoded) when is_list(Encoded) ->
decode(Width, Words, list_to_binary(Encoded));
decode(Width, Words, Encoded) ->
Split = string:lexemes(Encoded, " "),
decode(Width, Words, Split, <<>>).
decode(Width, Words, [Word | Rest], Acc) ->
case find(Word, Words) of
{ok, N} -> decode(Width, Words, Rest, <<Acc/bitstring, N:Width>>);
Error -> Error
end;
decode(Width, _, [], Acc) ->
sumcheck(Width, Acc).
chunksize(N, C) ->
chunksize(N, C, 0).
chunksize(0, _, A) -> A;
chunksize(N, C, A) -> chunksize(N div C, C, A + 1).
read_words() ->
Path = filename:join([zx:get_home(), "priv", "words4096.txt"]),
{ok, Bin} = file:read_file(Path),
string:lexemes(Bin, "\n").
find(Word, Words) ->
find(Word, Words, 0).
find(Word, [Word | _], N) -> {ok, N};
find(Word, [_ | Rest], N) -> find(Word, Rest, N + 1);
find(Word, [], _) -> {error, {bad_word, Word}}.
checksum(Width, Bits) ->
checksum(Width, Bits, 0).
checksum(_, <<>>, Sum) ->
Sum;
checksum(Width, Bits, Sum) ->
<<N:Width, Rest/bitstring>> = Bits,
checksum(Width, Rest, N bxor Sum).
sumcheck(Width, Bits) ->
<<CheckSum:Width, Binary/bitstring>> = Bits,
case checksum(Width, Binary) =:= CheckSum of
true ->
<<N:(bit_size(Binary))>> = Binary,
{ok, <<N:(32 * 8)>>};
false ->
{error, bad_phrase}
end.
-spec lcg(integer()) -> integer().
%% A simple PRNG that fits into 32 bits and is easy to implement anywhere (Kotlin).
%% Specifically, it is a "linear congruential generator" of the Lehmer variety.
%% The constants used are based on recommendations from Park, Miller and Stockmeyer:
%% https://www.firstpr.com.au/dsp/rand31/p105-crawford.pdf#page=4
%%
%% The input value should be between 1 and 2^31-1.
%%
%% The purpose of this PRNG is for password-based dictionary shuffling.
lcg(N) ->
M = 16#7FFFFFFF,
A = 48271,
Q = 44488, % M div A
R = 3399, % M rem A
Div = N div Q,
Rem = N rem Q,
S = Rem * A,
T = Div * R,
Result = S - T,
case Result < 0 of
false -> Result;
true -> Result + M
end.
+104
View File
@@ -0,0 +1,104 @@
%%% @doc
%%% GajuDesk Helper Functions
%%% @end
-module(gd_lib).
-vsn("0.9.0").
-include_lib("wx/include/wx.hrl").
-export([is_int/1,
mono_text/2, mono_text/3, mono_text/4,
button/2, button/3,
copy_to_clipboard/1]).
-include("$zx_include/zx_logger.hrl").
-include("gd.hrl").
-include("gdl.hrl").
-spec is_int(string()) -> boolean().
%% @doc
%% A simple boolean check over whether every character in a string is part of an integer value
%% without the trashy `try .. catch' way.
is_int(S) ->
lists:all(fun(C) -> $0 =< C andalso C =< $9 end, S).
-spec mono_text(Parent, Name) -> StaticText
when Parent :: wxWindow:wxWindow(),
Name :: term(),
StaticText :: #wx{}.
%% @doc
%% Creates a blank monospace font static text field.
%% @equiv mono_text(Parent, Name, "").
mono_text(Parent, Name) ->
mono_text(Parent, Name, "").
-spec mono_text(Parent, Name, Value) -> StaticText
when Parent :: wxWindow:wxWindow(),
Name :: term(),
Value :: string(),
StaticText :: #w{}.
%% @doc
%% Creats a monospace font static text field with the given value.
%% This exists so that I don't have to remember the difference between how to create a styled
%% wxStaticText vs wxTextCtrl (which has to do it in a weird way because it has a much richer
%% notion of text styling).
mono_text(Parent, Name, Value) ->
mono_text(Parent, Name, Value, []).
mono_text(Parent, Name, Value, Options) ->
Text = wxTextCtrl:new(Parent, ?wxID_ANY, [{value, Value} | Options]),
Font = wxFont:new(10, ?wxFONTFAMILY_TELETYPE, ?wxFONTSTYLE_NORMAL, ?wxFONTWEIGHT_NORMAL),
ok =
case wxTextCtrl:setFont(Text, Font) of
true -> ok;
false -> tell(info, "wxStaticText ~p is already monospace.", [Text])
end,
#w{name = Name, id = wxTextCtrl:getId(Text), wx = Text}.
-spec button(Parent, Label) -> Button
when Parent :: wxWindow:wxWindow(),
Label :: string(),
Button :: #w{}.
button(Parent, Label) ->
button(Parent, Label, Label).
-spec button(Parent, Name, Label) -> Button
when Parent :: wxWindow:wxWindow(),
Name :: term(),
Label :: string(),
Button :: #wx{}.
button(Parent, Name, Label) ->
Button = wxButton:new(Parent, ?wxID_ANY, [{label, Label}]),
#w{name = Name, id = wxButton:getId(Button), wx = Button}.
-spec copy_to_clipboard(String) -> ok
when String :: unicode:charlist().
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.
+284
View File
@@ -0,0 +1,284 @@
%%% @doc
%%% A live modal for creating a SpendTX
%%% @end
-module(gd_m_spend).
-vsn("0.9.0").
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0-or-later").
%-behavior(zxw_modal).
-export([show/2]).
-export([init/1, handle_info/2, handle_event/2]).
-include_lib("wx/include/wx.hrl").
-include("$zx_include/zx_logger.hrl").
-include("gd.hrl").
-record(s,
{frame = none :: none | wx:wx_object(),
parent = none :: none | wx:wx_object(),
caller = none :: none | pid(),
j = j() :: fun(),
to_tx = none :: none | wx:wx_object(),
amount_tx = none :: none | wx:wx_object(),
payload_tx = none :: none | wx:wx_object(),
ttl_sl = none :: none | wx:wx_object(),
gas_sl = none :: none | wx:wx_object(),
affirm = none :: none | wx:wx_object(),
cancel = none :: none | wx:wx_object()}).
j() ->
fun(X) -> X end.
%%% Interface
-spec show(Parent, Args) -> {ok, #spend_tx{}} | cancel
when Parent :: wxFrame:wxFrame(),
Args :: {Account :: iolist(),
Nonce :: pos_integer(),
Height :: pos_integer(),
J :: fun()}.
show(Parent, Args) ->
zxw_modal:show(Parent, ?MODULE, Args).
%% Init
init({Parent, Caller, {Account, J}}) ->
Frame = wxFrame:new(Parent, ?wxID_ANY, J("Transfer Gajus")),
Panel = wxWindow:new(Frame, ?wxID_ANY),
TopSz = wxBoxSizer:new(?wxVERTICAL),
_ = wxBoxSizer:add(TopSz, Panel, zxw:flags(wide)),
MainSz = wxBoxSizer:new(?wxVERTICAL),
FromTx = wxStaticText:new(Panel, ?wxID_ANY, Account),
FromSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("From")}]),
_ = wxStaticBoxSizer:add(FromSz, FromTx, zxw:flags({wide, 5})),
ToTx = wxTextCtrl:new(Panel, ?wxID_ANY),
ToSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("To")}]),
_ = wxStaticBoxSizer:add(ToSz, ToTx, zxw:flags({wide, 5})),
AmtTx = wxTextCtrl:new(Panel, ?wxID_ANY),
AmtSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Amount")}]),
AmtInSz = wxBoxSizer:new(?wxHORIZONTAL),
_ = wxStaticBoxSizer:add(AmtInSz, AmtTx, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(AmtSz, AmtInSz, zxw:flags({wide, 5})),
PayloadTx = wxTextCtrl:new(Panel, ?wxID_ANY, [{style, ?wxTE_MULTILINE}]),
PayloadSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Message")}]),
_ = wxStaticBoxSizer:add(PayloadSz, PayloadTx, zxw:flags({wide, 5})),
Style = [{style, ?wxSL_HORIZONTAL bor ?wxSL_LABELS}],
TTL_Sl = wxSlider:new(Panel, ?wxID_ANY, 100, 10, 1000, Style),
TTL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("TTL")}]),
_ = wxStaticBoxSizer:add(TTL_Sz, TTL_Sl, zxw:flags({wide, 5})),
Min = hz:min_gas_price(),
Max = Min * 2,
GasSl = wxSlider:new(Panel, ?wxID_ANY, Min, Min, Max, Style),
GasSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, J("Gas Price")}]),
_ = wxStaticBoxSizer:add(GasSz, GasSl, zxw:flags({wide, 5})),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
Affirm = wxButton:new(Panel, ?wxID_ANY, [{label, J("OK")}]),
Cancel = wxButton:new(Panel, ?wxID_ANY, [{label, J("Cancel")}]),
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags({wide, 5})),
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags({wide, 5})),
_ = wxBoxSizer:add(MainSz, FromSz, zxw:flags({base, 5})),
_ = wxBoxSizer:add(MainSz, ToSz, zxw:flags({base, 5})),
_ = wxBoxSizer:add(MainSz, AmtSz, zxw:flags({base, 5})),
_ = wxBoxSizer:add(MainSz, PayloadSz, zxw:flags({wide, 5})),
_ = wxBoxSizer:add(MainSz, TTL_Sz, zxw:flags({base, 5})),
_ = wxBoxSizer:add(MainSz, GasSz, zxw:flags({base, 5})),
_ = wxBoxSizer:add(MainSz, ButtSz, zxw:flags({base, 5})),
HandleKey = key_handler(ToTx, AmtTx, PayloadTx, TTL_Sl, GasSl, Affirm, Cancel),
ok = wxFrame:connect(Frame, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(ToTx, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(AmtTx, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(PayloadTx, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(TTL_Sl, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(GasSl, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(Affirm, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(Cancel, key_down, [{callback, HandleKey}]),
ok = wxFrame:connect(Frame, command_button_clicked),
ok = wxFrame:connect(Frame, close_window),
ok = wxFrame:setSize(Frame, {500, 650}),
ok = wxFrame:center(Frame),
ok = wxWindow:setSizer(Panel, MainSz),
ok = wxFrame:setSizer(Frame, TopSz),
ok = wxBoxSizer:layout(MainSz),
ok = wxFrame:centerOnParent(Frame),
true = wxFrame:show(Frame),
self() ! {focus, to_tx},
State =
#s{frame = Frame, parent = Parent, caller = Caller,
to_tx = ToTx, amount_tx = AmtTx, payload_tx = PayloadTx,
ttl_sl = TTL_Sl, gas_sl = GasSl,
affirm = Affirm, cancel = Cancel},
{Frame, State}.
key_handler(ToTx, AmtTx, PayloadTx, TTL_Sl, GasSl, Affirm, Cancel) ->
Me = self(),
ToID = wxTextCtrl:getId(ToTx),
AmtID = wxTextCtrl:getId(AmtTx),
PL_ID = wxTextCtrl:getId(PayloadTx),
TTL_ID = wxSlider:getId(TTL_Sl),
GasID = wxSlider:getId(GasSl),
AffirmID = wxButton:getId(Affirm),
CancelID = wxButton:getId(Cancel),
fun(#wx{id = ID, event = #wxKey{type = key_down, keyCode = Code}}, KeyPress) ->
case {Code, wxKeyEvent:shiftDown(KeyPress)} of
{9, false} ->
case ID of
ToID -> Me ! {focus, amount_tx};
AmtID -> Me ! {focus, payload_tx};
PL_ID -> Me ! {focus, ttl_sl};
TTL_ID -> Me ! {focus, gas_sl};
GasID -> Me ! {focus, affirm};
AffirmID -> Me ! {focus, cancel};
CancelID -> Me ! {focus, to_tx};
_ -> wxEvent:skip(KeyPress)
end;
{9, true} ->
case ID of
ToID -> Me ! {focus, cancel};
AmtID -> Me ! {focus, to_tx};
PL_ID -> Me ! {focus, amount_tx};
TTL_ID -> Me ! {focus, payload_tx};
GasID -> Me ! {focus, ttl_sl};
AffirmID -> Me ! {focus, gas_sl};
CancelID -> Me ! {focus, affirm};
_ -> wxEvent:skip(KeyPress)
end;
{13, _} ->
case ID of
ToID -> Me ! {tab, to_tx};
AmtID -> Me ! {tab, amount_tx};
PL_ID -> Me ! {tab, payload_tx};
TTL_ID -> Me ! {tab, ttl_sl};
GasID -> Me ! {tab, gas_sl};
AffirmID -> Me ! {enter, affirm};
CancelID -> Me ! {enter, cancel};
_ -> wxEvent:skip(KeyPress)
end;
{27, _} ->
Me ! esc;
{_, _} ->
wxEvent:skip(KeyPress)
end
end.
handle_info({focus, Element}, State) ->
ok = focus(Element, State),
{noreply, State};
handle_info({enter, affirm}, State) ->
check(State);
handle_info({enter, cancel}, State) ->
cancel(State);
handle_info(esc, State) ->
ok = cancel(State),
{noreply, State};
handle_info(Message, State) ->
ok = log(warning, "Message: ~p~nState: ~p~n", [Message, State]),
{noreply, State}.
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID},
State = #s{affirm = Affirm, cancel = Cancel}) ->
AffirmID = wxButton:getId(Affirm),
CancelID = wxButton:getId(Cancel),
NewState =
case ID of
AffirmID -> check(State);
CancelID -> cancel(State)
end,
{noreply, NewState};
handle_event(#wx{event = #wxClose{}}, State) ->
NewState = cancel(State),
{noreply, NewState};
handle_event(Event, State) ->
ok = log(warning, "Unexpected event ~tp State: ~tp~n", [Event, State]),
{noreply, State}.
%%% Doers
focus(to_tx, #s{to_tx = ToTX}) -> wxTextCtrl:setFocus(ToTX);
focus(amount_tx, #s{amount_tx = AmountTX}) -> wxTextCtrl:setFocus(AmountTX);
focus(payload_tx, #s{payload_tx = PayloadTX}) -> wxTextCtrl:setFocus(PayloadTX);
focus(ttl_sl, #s{ttl_sl = TTL_SL}) -> wxSlider:setFocus(TTL_SL);
focus(gas_sl, #s{gas_sl = GasSL}) -> wxSlider:setFocus(GasSL);
focus(affirm, #s{affirm = Affirm}) -> wxButton:setFocus(Affirm);
focus(cancel, #s{cancel = Cancel}) -> wxButton:setFocus(Cancel).
cancel(#s{frame = Frame, caller = Caller}) ->
ok = wxFrame:destroy(Frame),
zxw_modal:done(Caller, cancel).
check(State =#s{frame = Frame, caller = Caller, j = J,
to_tx = ToTx, amount_tx = AmountTx, payload_tx = PayloadTx,
ttl_sl = TTL_Sl, gas_sl = GasSl}) ->
DirtyTX =
[{recipient_id, wxTextCtrl:getValue(ToTx)},
{amount, wxTextCtrl:getValue(AmountTx)},
{gas_price, wxSlider:getValue(GasSl)},
{ttl, wxSlider:getValue(TTL_Sl)},
{payload, wxTextCtrl:getValue(PayloadTx)}],
ok =
case clean_spend(DirtyTX, #spend_tx{}, J, []) of
{ok, CleanTX} ->
ok = wxFrame:destroy(Frame),
zxw_modal:done(Caller, {ok, CleanTX});
{error, Errors} ->
DerpyDerpDerp = form_message(Errors, J),
ok = zxw:show_message(Frame, DerpyDerpDerp),
State
end.
% TODO: There should be some suggestive logic around gas prices, both based on how large
% the payload and TTL are, but also the ingoing current common gas rate. This will require
% a "gas station" sort of analysis app to query, though.
clean_spend([{recipient_id, ""} | Rest], TX, J, Errors) ->
NewErrors = [{recipient_id, J("Recipient field is empty.")} | Errors],
clean_spend(Rest, TX, J, NewErrors);
clean_spend([{recipient_id, Recipient} | Rest], TX, J, Errors) ->
clean_spend(Rest, TX#spend_tx{recipient_id = list_to_binary(Recipient)}, J, Errors);
clean_spend([{amount, ""} | Rest], TX, J, Errors) ->
clean_spend(Rest, TX#spend_tx{amount = 0}, J, Errors);
clean_spend([{amount, S} | Rest], TX, J, Errors) ->
{NewTX, NewErrors} =
case hz_format:read(S) of
{ok, Amount} ->
{TX#spend_tx{amount = Amount}, Errors};
error ->
Derp = J("Amount field is not properly formatted."),
{TX, [{amount, Derp} | Errors]}
end,
clean_spend(Rest, NewTX, J, NewErrors);
clean_spend([{gas_price, GasPrice} | Rest], TX, J, Errors) ->
clean_spend(Rest, TX#spend_tx{gas_price = GasPrice}, J, Errors);
clean_spend([{ttl, TTL} | Rest], TX, J, Errors) ->
clean_spend(Rest, TX#spend_tx{ttl = TTL}, J, Errors);
clean_spend([{payload, S} | Rest], TX, J, Errors) ->
clean_spend(Rest, TX#spend_tx{payload = list_to_binary(S)}, J, Errors);
clean_spend([], TX, _, []) ->
{ok, TX};
clean_spend([], _, _, Errors) ->
{error, Errors}.
form_message(Errors, J) ->
Header = J("The following errors were encountered:"),
unicode:characters_to_list([Header, "\n", assemble(Errors)]).
% TODO: Highlight the fields since we know them.
assemble([{_, M} | T]) -> [M, "\n" | assemble(T)];
assemble([]) -> [].
+184
View File
@@ -0,0 +1,184 @@
%%% @doc
%%% A live modal for importing a wallet.
%%%
%%% Interprets [ENTER] as an "OK" button click and [ESC] as a "Cancel" button click.
%%% @end
-module(gd_m_wallet_importer).
-vsn("0.9.0").
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0-or-later").
-behavior(zxw_modal).
-export([show/6]).
-export([init/1, handle_info/2, handle_event/2]).
-include_lib("wx/include/wx.hrl").
-include("$zx_include/zx_logger.hrl").
-record(s,
{frame = none :: none | wx:wx_object(),
parent = none :: none | wx:wx_object(),
caller = none :: none | pid(),
name_tx = none :: none | wx:wx_object(),
pass_tx = none :: none | wx:wx_object(),
ok = none :: none | wx:wx_object(),
cancel = none :: none | wx:wx_object()}).
%%% Interface
-spec show(Parent, Title, NameLabel, PassLabel, OK_L, Cancel_L) -> {ok, Name, Pass} | cancel
when Parent :: wxFrame:wxFrame(),
Title :: string(),
NameLabel :: string(),
PassLabel :: string(),
OK_L :: string(),
Cancel_L :: string(),
Name :: string(),
Pass :: string() | none.
show(Parent, Title, NameLabel, PassLabel, OK_L, Cancel_L) ->
zxw_modal:show(Parent, ?MODULE, {Title, NameLabel, PassLabel, OK_L, Cancel_L}).
%% Init
init({Parent, Caller, {Title, NameLabel, PassLabel, OK_L, Cancel_L}}) ->
Frame = wxFrame:new(Parent, ?wxID_ANY, Title),
Panel = wxWindow:new(Frame, ?wxID_ANY),
TopSz = wxBoxSizer:new(?wxVERTICAL),
_ = wxBoxSizer:add(TopSz, Panel, zxw:flags(wide)),
MainSz = wxBoxSizer:new(?wxVERTICAL),
NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, NameLabel}]),
NameTx = wxTextCtrl:new(Panel, ?wxID_ANY, [{style, ?wxTE_PROCESS_TAB}]),
_ = wxSizer:add(NameSz, NameTx, zxw:flags({wide, 5})),
PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Panel, [{label, PassLabel}]),
PassTx = wxTextCtrl:new(Panel, ?wxID_ANY, [{style, ?wxTE_PROCESS_TAB bor ?wxTE_PASSWORD}]),
_ = wxSizer:add(PassSz, PassTx, zxw:flags({wide, 5})),
OK = wxButton:new(Panel, ?wxID_ANY, [{label, OK_L}]),
Cancel = wxButton:new(Panel, ?wxID_ANY, [{label, Cancel_L}]),
ButtSz = wxBoxSizer:new(?wxHORIZONTAL),
_ = wxBoxSizer:add(ButtSz, OK, zxw:flags({wide, 5})),
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags({wide, 5})),
_ = wxBoxSizer:add(MainSz, NameSz, zxw:flags({wide, 5})),
_ = wxBoxSizer:add(MainSz, PassSz, zxw:flags({wide, 5})),
_ = wxBoxSizer:add(MainSz, ButtSz, zxw:flags({wide, 5})),
HandleKey = key_handler(NameTx, PassTx, OK, Cancel),
ok = wxFrame:connect(Frame, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(NameTx, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(PassTx, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(OK, key_down, [{callback, HandleKey}]),
ok = wxTextCtrl:connect(Cancel, key_down, [{callback, HandleKey}]),
ok = wxFrame:connect(Frame, command_button_clicked),
ok = wxFrame:connect(Frame, close_window),
ok = wxFrame:setSize(Frame, {500, 220}),
ok = wxWindow:setSizer(Panel, MainSz),
ok = wxFrame:setSizer(Frame, TopSz),
ok = wxBoxSizer:layout(MainSz),
ok = wxFrame:centerOnParent(Frame),
ok = wxTextCtrl:setFocus(NameTx),
true = wxFrame:show(Frame),
State =
#s{frame = Frame, parent = Parent, caller = Caller,
name_tx = NameTx, pass_tx = PassTx,
ok = OK, cancel = Cancel},
{Frame, State}.
key_handler(NameTx, PassTx, OK, Cancel) ->
Me = self(),
NameID = wxTextCtrl:getId(NameTx),
PassID = wxTextCtrl:getId(PassTx),
OK_ID = wxButton:getId(OK),
CancelID = wxButton:getId(Cancel),
fun(#wx{id = ID, event = #wxKey{type = key_down, keyCode = Code}}, KeyPress) ->
case Code of
9 ->
case ID of
NameID -> Me ! {tab, name};
PassID -> Me ! {tab, pass};
OK_ID -> Me ! {tab, ok};
CancelID -> Me ! {tab, cancel};
_ -> wxEvent:skip(KeyPress)
end;
13 ->
case ID of
NameID -> Me ! {enter, name};
PassID -> Me ! {enter, pass};
_ -> wxEvent:skip(KeyPress)
end;
27 ->
Me ! esc;
_ ->
wxEvent:skip(KeyPress)
end
end.
handle_info({tab, Element}, State) ->
ok = tab_traverse(Element, State),
{noreply, State};
handle_info({enter, name}, State = #s{pass_tx = PassTx}) ->
ok = wxTextCtrl:setFocus(PassTx),
{noreply, State};
handle_info({enter, pass}, State) ->
done(State);
handle_info(esc, State) ->
ok = cancel(State),
{noreply, State};
handle_info(Message, State) ->
ok = log(warning, "Message: ~p~nState: ~p~n", [Message, State]),
{noreply, State}.
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID},
State = #s{ok = OK, cancel = Cancel}) ->
OK_ID = wxButton:getId(OK),
CancelID = wxButton:getId(Cancel),
case ID of
OK_ID -> done(State);
CancelID -> cancel(State)
end;
handle_event(#wx{event = #wxClose{}}, State) ->
NewState = cancel(State),
{noreply, NewState};
handle_event(Event, State) ->
ok = log(warning, "Unexpected event ~tp State: ~tp~n", [Event, State]),
{noreply, State}.
%%% Doers
tab_traverse(name, #s{pass_tx = PassTx}) ->
wxTextCtrl:setFocus(PassTx);
tab_traverse(pass, #s{ok = OK}) ->
wxButton:setFocus(OK);
tab_traverse(ok, #s{cancel = Cancel}) ->
wxButton:setFocus(Cancel);
tab_traverse(cancel, #s{name_tx = NameTx}) ->
wxTextCtrl:setFocus(NameTx).
cancel(#s{frame = Frame, caller = Caller}) ->
ok = wxFrame:destroy(Frame),
zxw_modal:done(Caller, cancel).
done(#s{frame = Frame, caller = Caller, name_tx = NameTx, pass_tx = PassTx}) ->
Result =
case wxTextCtrl:getValue(NameTx) of
"" ->
cancel;
Name ->
case wxTextCtrl:getValue(PassTx) of
"" -> {ok, Name, none};
Pass -> {ok, Name, Pass}
end
end,
ok = wxFrame:destroy(Frame),
zxw_modal:done(Caller, Result).
+154 -67
View File
@@ -1,84 +1,174 @@
-module(gd_sophia_editor). -module(gd_sophia_editor).
-export([new/3, get_text/1, set_text/2]). -vsn("0.9.0").
-export([new/1, update/1, update/2,
get_text/1, set_text/2]).
-include("$zx_include/zx_logger.hrl").
-include_lib("wx/include/wx.hrl"). -include_lib("wx/include/wx.hrl").
new(Parent, Id, Opts) -> %%% Formatting Constants
STC = wxStyledTextCtrl:new(Parent, [{id, Id} | Opts]),
%% Set up the container lexer %% Style labels
wxStyledTextCtrl:setLexer(STC, ?wxSTC_LEX_CONTAINER), -define(DEFAULT, 0).
-define(KEYWORD, 1).
-define(IDENTIFIER, 2).
-define(COMMENT, 3).
-define(STRING, 4).
-define(NUMBER, 5).
-define(OPERATOR, 6).
%% Color palette
% Intensities:
-define(H, 255). % High
-define(M, 192). % Medium
-define(L, 128). % Low
-define(D, 64). % Deep-Low
-define(X, 32). % X-Low
-define(Z, 0). % Zilch
% RGB values
% R G B
-define(black, {?Z, ?Z, ?Z}).
-define(light_red, {?H, ?Z, ?Z}).
-define(light_green, {?Z, ?H, ?Z}).
-define(light_blue, {?Z, ?Z, ?H}).
-define(yellow, {?H, ?H, ?Z}).
-define(light_magenta, {?H, ?Z, ?H}).
-define(light_cyan, {?Z, ?H, ?H}).
-define(high_white, {?H, ?H, ?H}).
-define(red, {?L, ?Z, ?Z}).
-define(green, {?Z, ?L, ?Z}).
-define(blue, {?Z, ?Z, ?L}).
-define(brown, {?L, ?L, ?Z}).
-define(magenta, {?L, ?Z, ?L}).
-define(cyan, {?Z, ?L, ?L}).
-define(grey, {?L, ?L, ?L}).
-define(dark_grey, {?D, ?D, ?D}).
-define(not_black, {?X, ?X, ?X}).
-define(white, {?M, ?M, ?M}).
styles() ->
[?DEFAULT,
?KEYWORD,
?IDENTIFIER,
?COMMENT,
?STRING,
?NUMBER,
?OPERATOR].
palette(light) ->
#{?DEFAULT => ?black,
?KEYWORD => ?blue,
?IDENTIFIER => ?cyan,
?COMMENT => ?grey,
?STRING => ?red,
?NUMBER => ?magenta,
?OPERATOR => ?brown,
sel_back => ?grey,
line_num => ?brown,
cursor => ?black,
bg => ?high_white};
palette(dark) ->
#{?DEFAULT => ?white,
?KEYWORD => ?light_cyan,
?IDENTIFIER => ?green,
?COMMENT => ?grey,
?STRING => ?light_red,
?NUMBER => ?light_magenta,
?OPERATOR => ?yellow,
sel_back => ?dark_grey,
line_num => ?yellow,
cursor => ?white,
bg => ?not_black}.
color_mode() ->
{R, G, B, _} = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW),
case (lists:sum([R, G, B]) div 3) > 128 of
true -> light;
false -> dark
end.
new(Parent) ->
STC = wxStyledTextCtrl:new(Parent),
ok = wxStyledTextCtrl:setLexer(STC, ?wxSTC_LEX_CONTAINER),
FontSize = 13, FontSize = 13,
Mono = wxFont:new(FontSize, ?wxFONTFAMILY_TELETYPE, ?wxFONTSTYLE_NORMAL, Mono = wxFont:new(FontSize,
?wxFONTWEIGHT_NORMAL, [{face, "Monospace"}]), ?wxFONTFAMILY_TELETYPE,
lists:foreach( ?wxFONTSTYLE_NORMAL,
fun(Style) -> ?wxFONTWEIGHT_NORMAL,
wxStyledTextCtrl:styleSetFont(STC, Style, Mono) [{face, "Monospace"}]),
end, SetMonospace = fun(Style) -> wxStyledTextCtrl:styleSetFont(STC, Style, Mono) end,
lists:seq(0, 6) ok = lists:foreach(SetMonospace, styles()),
), ok = wxStyledTextCtrl:setMarginType(STC, 0, ?wxSTC_MARGIN_NUMBER),
%% ok = wxStyledTextCtrl:setMarginWidth(STC, 0, 40),
wxStyledTextCtrl:styleSetFont(STC, ?wxSTC_STYLE_DEFAULT, Mono), ok = wxStyledTextCtrl:styleSetFont(STC, ?wxSTC_STYLE_DEFAULT, Mono),
%% Define styles ok = set_colors(STC),
set_colors(STC),
%% Connect the styling event
wxStyledTextCtrl:connect(
STC, 'stc_styleneeded',
[{callback, fun(Event, _) -> style_needed(Event, STC) end}]),
STC. STC.
get_text(STC) -> get_text(STC) ->
wxStyledTextCtrl:getText(STC). wxStyledTextCtrl:getText(STC).
set_text(STC, Text) -> set_text(STC, Text) ->
wxStyledTextCtrl:setText(STC, Text), ok = wxStyledTextCtrl:setText(STC, Text),
%% Force Scintilla to request styling for the entire text %% Force Scintilla to request styling for the entire text
wxStyledTextCtrl:colourise(STC, 0, -1). wxStyledTextCtrl:colourise(STC, 0, -1).
%% ======================================================================
set_colors(STC) -> set_colors(STC) ->
wxStyledTextCtrl:styleClearAll(STC), ok = wxStyledTextCtrl:styleClearAll(STC),
wxStyledTextCtrl:styleSetForeground(STC, 0, {0, 0, 0}), % Default: Black Palette = palette(color_mode()),
wxStyledTextCtrl:styleSetForeground(STC, 1, {0, 0, 192}), % Keywords: Deep Blue #{bg := BGC, cursor := CC, line_num := LNC, sel_back := SFB} = Palette,
wxStyledTextCtrl:styleSetForeground(STC, 2, {0, 0, 0}), % Identifiers: Black Colorize =
wxStyledTextCtrl:styleSetForeground(STC, 3, {128, 128, 128}), % Comments: Gray fun(Style) ->
wxStyledTextCtrl:styleSetForeground(STC, 4, {163, 21, 21}), % Strings/Chars: Reddish Color = maps:get(Style, Palette),
wxStyledTextCtrl:styleSetForeground(STC, 5, {128, 0, 128}), % Numbers: Purple ok = wxStyledTextCtrl:styleSetForeground(STC, Style, Color),
wxStyledTextCtrl:styleSetForeground(STC, 6, {0, 0, 0}). % Operators: Black ok = wxStyledTextCtrl:styleSetBackground(STC, Style, BGC)
end,
ok = wxStyledTextCtrl:setCaretForeground(STC, CC),
ok = wxStyledTextCtrl:setSelBackground(STC, true, SFB),
ok = wxStyledTextCtrl:styleSetForeground(STC, ?wxSTC_STYLE_LINENUMBER, LNC),
ok = wxStyledTextCtrl:styleSetBackground(STC, ?wxSTC_STYLE_DEFAULT, BGC),
lists:foreach(Colorize, styles()).
style_needed(_Event, STC) ->
try update(_Event, STC) ->
StartPos = wxStyledTextCtrl:getEndStyled(STC), update(STC).
EndPos = wxStyledTextCtrl:getLength(STC),
Text = wxStyledTextCtrl:getTextRange(STC, StartPos, EndPos), update(STC) ->
Text = wxStyledTextCtrl:getText(STC),
case so_scan:scan(Text) of case so_scan:scan(Text) of
{ok, Tokens} -> {ok, Tokens} ->
wxStyledTextCtrl:startStyling(STC, StartPos), ok = wxStyledTextCtrl:startStyling(STC, 0),
apply_styles(STC, Tokens); apply_styles(STC, Tokens);
{error, _Reason} -> {error, _Reason} ->
wxStyledTextCtrl:startStyling(STC, StartPos), ok
wxStyledTextCtrl:setStyling(STC, EndPos - StartPos, 0)
end
catch
error:E:T ->
dbg("CAUGHT error:~p / ~p~n", [E, T])
end. end.
apply_styles(STC, Tokens) -> apply_styles(STC, Tokens) ->
lists:foreach(fun(Token) -> style_token(STC, Token) end, Tokens). lists:foreach(fun(Token) -> style_token(STC, Token) end, Tokens).
% FIXME: 'qid' is not properly handled. If there are multi-dot qids, they will break
style_token(STC, Token) -> style_token(STC, Token) ->
{Type, Value} = type_and_value(Token), {Type, Value} = type_and_value(Token),
{StartOffset, LengthOffset} = offset(Type),
{Line, Col} = element(2, Token), {Line, Col} = element(2, Token),
Len = byte_size(to_binary(Value)), Length = byte_size(to_binary(Value)) + LengthOffset,
Style = classify_style(Type, Value), Style = classify_style(Type, Value),
% Get exact position from Line and Column Start = wxStyledTextCtrl:positionFromLine(STC, Line - 1) + Col + StartOffset,
Start = wxStyledTextCtrl:positionFromLine(STC, Line - 1) + (Col - 1),
wxStyledTextCtrl:startStyling(STC, Start), wxStyledTextCtrl:startStyling(STC, Start),
wxStyledTextCtrl:setStyling(STC, Len, Style). wxStyledTextCtrl:setStyling(STC, Length, Style).
offset(string) -> { 0, 0};
offset(qid) -> {-1, 1};
offset(_) -> {-1, 0}.
to_binary(S) when is_list(S) -> to_binary(S) when is_list(S) ->
unicode:characters_to_binary(S); unicode:characters_to_binary(S);
@@ -87,38 +177,35 @@ to_binary(S) when is_binary(S) ->
to_binary(I) when is_integer(I) -> to_binary(I) when is_integer(I) ->
integer_to_binary(I). integer_to_binary(I).
classify_style(Type, Value) -> classify_style(Type, Value) ->
case Type of case Type of
symbol -> symbol ->
case lists:member(Value, keywords()) of case lists:member(Value, keywords()) of
true -> 1; true -> ?KEYWORD;
false -> 6 false -> ?OPERATOR
end; end;
id -> 2; id -> ?IDENTIFIER;
qid -> 2; qid -> ?IDENTIFIER;
con -> 2; con -> ?IDENTIFIER;
qcon -> 2; qcon -> ?IDENTIFIER;
tvar -> 2; tvar -> ?IDENTIFIER;
string -> 4; string -> ?STRING;
char -> 4; char -> ?STRING;
int -> 5; int -> ?NUMBER;
hex -> 5; hex -> ?NUMBER;
bytes -> 5; bytes -> ?NUMBER;
skip -> 3; skip -> ?COMMENT;
_Other -> 1 _ -> ?DEFAULT
end. end.
type_and_value({Type, _Line, Value}) -> {Type, Value}; type_and_value({Type, _Line, Value}) -> {Type, Value};
type_and_value({Type, _}) -> {Type, atom_to_list(Type)}. type_and_value({Type, _}) -> {Type, atom_to_list(Type)}.
keywords() -> keywords() ->
["contract", "include", "let", "switch", "type", "record", "datatype", "if", ["contract", "include", "let", "switch", "type", "record", "datatype", "if",
"elif", "else", "function", "stateful", "payable", "true", "false", "mod", "elif", "else", "function", "stateful", "payable", "true", "false", "mod",
"public", "entrypoint", "private", "indexed", "namespace", "interface", "public", "entrypoint", "private", "indexed", "namespace", "interface",
"main", "using", "as", "for", "hiding", "band", "bor", "bxor", "bnot"]. "main", "using", "as", "for", "hiding", "band", "bor", "bxor", "bnot"].
%% dbg(Fmt) ->
%% dbg(Fmt, []).
dbg(Fmt, Args) ->
io:format("~p: " ++ Fmt, [self()|Args]).
+1 -1
View File
@@ -12,7 +12,7 @@
%%% @end %%% @end
-module(gd_sup). -module(gd_sup).
-vsn("0.5.4"). -vsn("0.9.0").
-behaviour(supervisor). -behaviour(supervisor).
-author("Craig Everett <craigeverett@qpq.swiss>"). -author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>"). -copyright("QPQ AG <info@qpq.swiss>").
+1 -1
View File
@@ -1,5 +1,5 @@
-module(gd_v). -module(gd_v).
-vsn("0.5.4"). -vsn("0.9.0").
-author("Craig Everett <craigeverett@qpq.swiss>"). -author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>"). -copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later"). -license("GPL-3.0-or-later").
+591
View File
@@ -0,0 +1,591 @@
-module(gd_v_call).
-vsn("0.9.0").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").
-behavior(wx_object).
%-behavior(gd_v).
-include_lib("wx/include/wx.hrl").
-export([to_front/1, tx_hash/1, tx_data/1, tx_info/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").
-include("gdl.hrl").
% State
-record(s,
{wx = none :: none | wx:wx_object(),
frame = none :: none | wx:wx_object(),
lang = en :: en | jp,
j = none :: none | fun(),
prefs = #{} :: map(),
con_id = <<"">> :: binary(),
fundef = none :: none | fun_def(),
funret = none :: none | term(), % FIXME
build = none :: none | map(),
args = [] :: [#w{}],
kp = #w{} :: #w{},
params = [] :: [param()],
return = #w{} :: #w{}, % wxTextCtrl, single-line
copy = #w{} :: #w{},
status = none :: status(),
action = #w{} :: #w{},
tx_data = none :: none | map(),
tx_info = none :: none | map(),
hash = #w{} :: #w{}, % wxTextCtrl, single-line
info = #w{} :: #w{}}). % wxTextCtrl, multi-line
-type fun_name() :: string().
-type fun_ilk() :: call | dryr | init.
-type fun_def() :: {fun_name(), fun_ilk()}.
-type param() :: {Label :: string(), Check :: fun(), #w{}}.
-type status() :: none
| submitted
| rejected
| included.
%%% Interface
-spec to_front(Win) -> ok
when Win :: wx:wx_object().
to_front(Win) ->
wx_object:cast(Win, to_front).
-spec tx_hash(Win) -> Result
when Win :: pid() | wx:wx_object(),
Result :: none | string().
tx_hash(Win) ->
wx_object:call(Win, tx_hash).
-spec tx_data(Win) -> Result
when Win :: pid() | wx:wx_object(),
Result :: none | map().
tx_data(Win) ->
wx_object:call(Win, tx_data).
-spec tx_info(Win) -> Result
when Win :: pid() | wx:wx_object(),
Result :: none | map().
tx_info(Win) ->
wx_object:call(Win, tx_info).
%%% Startup Functions
start_link(Args) ->
wx_object:start_link(?MODULE, Args, []).
init({Prefs, FunDef = {FunName, FunIlk}, ConID, Build, Selected, Keys}) ->
Lang = maps:get(lang, Prefs, en),
Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans),
{aaci, ConName, FunSpecs, _} = maps:get(aaci, Build),
FunSpec = {FunArgs, FunReturn} = maps:get(FunName, FunSpecs),
{CallTypeLabel, ActionLabel} =
case FunIlk of
call -> {J("Contract Call"), J("Submit Call")};
dryr -> {J("Dry Run"), J("Submit Dry Run")};
init -> {J("Deploy"), J("Deploy")}
end,
Arity = integer_to_list(length(FunArgs)),
Title = [CallTypeLabel, ": ", ConName, ".", FunName, "/", Arity],
Wx = wx:new(),
Frame = wxFrame:new(Wx, ?wxID_ANY, Title),
MainSz = wxBoxSizer:new(?wxVERTICAL),
KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Signature Key")}]),
KeyBox = wxStaticBoxSizer:getStaticBox(KeySz),
KeyPicker = wxChoice:new(KeyBox, ?wxID_ANY, [{choices, Keys}]),
KP = #w{name = key_picker, id = wxChoice:getId(KeyPicker), wx = KeyPicker},
ZeroBasedSelected = Selected - 1,
ok = wxChoice:setSelection(KeyPicker, ZeroBasedSelected),
_ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)),
{ArgSz, Args, Return, Copy, HasArgs} = call_arg_sizer(Frame, J, FunIlk, FunSpec),
{ParamSz, Params} = call_param_sizer(Frame, J),
Action = #w{wx = ActionBn} = gd_lib:button(Frame, ActionLabel),
TX_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Transaction Info")}]),
TX_Sz_Box = wxStaticBoxSizer:getStaticBox(TX_Sz),
Single = [{style, ?wxTE_READONLY}],
Multi = [{style, ?wxTE_MULTILINE bor ?wxTE_READONLY}],
TX_Hash = #w{wx = HashT} = gd_lib:mono_text(TX_Sz_Box, tx_hash, "", Single),
TX_Info = #w{wx = InfoT} = gd_lib:mono_text(TX_Sz_Box, tx_info, "", Multi),
_ = wxStaticBoxSizer:add(TX_Sz, HashT, zxw:flags({base, 5})),
_ = wxStaticBoxSizer:add(TX_Sz, InfoT, zxw:flags({wide, 5})),
ArgSzArgs =
case HasArgs of
true -> [{proportion, 2}, {flag, ?wxEXPAND bor ?wxALL}, {border, 5}];
false -> zxw:flags({base, 5})
end,
_ = wxSizer:add(MainSz, ArgSz, ArgSzArgs),
_ = wxSizer:add(MainSz, KeySz, zxw:flags({base, 5})),
_ = wxSizer:add(MainSz, ParamSz, zxw:flags({base, 5})),
_ = wxSizer:add(MainSz, ActionBn, zxw:flags({base, 5})),
_ = wxSizer:add(MainSz, TX_Sz, [{proportion, 1}, {flag, ?wxEXPAND bor ?wxALL}, {border, 5}]),
_ = wxFrame:setSizer(Frame, MainSz),
_ = wxFrame:setSize(Frame, {900, 900}),
_ = wxSizer:layout(MainSz),
ok = wxFrame:connect(Frame, close_window),
ok = wxFrame:connect(Frame, command_button_clicked),
true = wxFrame:show(Frame),
State =
#s{wx = Wx, frame = Frame, j = J, prefs = Prefs,
fundef = FunDef, funret = FunReturn, con_id = ConID, build = Build,
args = Args, kp = KP, params = Params,
return = Return, copy = Copy,
action = Action, status = none,
hash = TX_Hash, info = TX_Info},
{Frame, State}.
call_arg_sizer(Frame, J, FunIlk, {CallArgs, ReturnType}) ->
SpecSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Function Spec")}]),
SpecBox = wxStaticBoxSizer:getStaticBox(SpecSz),
{CallSz, CallControls, HasArgs} = call_sizer(SpecBox, J, CallArgs),
{ReturnSz, Return, Copy} = return_sizer(SpecBox, J, FunIlk, ReturnType),
_ = wxStaticBoxSizer:add(SpecSz, CallSz, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(SpecSz, ReturnSz, zxw:flags({base, 5})),
{SpecSz, CallControls, Return, Copy, HasArgs}.
call_sizer(Parent, J, []) ->
CallSz = wxStaticBoxSizer:new(?wxVERTICAL, Parent, [{label, J("Call Args")}]),
CallBox = wxStaticBoxSizer:getStaticBox(CallSz),
Args = wxStaticText:new(CallBox, ?wxID_ANY, ["[", J("No Args"), "]"]),
_ = wxStaticBoxSizer:add(CallSz, Args, zxw:flags({wide, 5})),
{CallSz, [], false};
call_sizer(Parent, J, CallArgs) ->
CallSz = wxStaticBoxSizer:new(?wxVERTICAL, Parent, [{label, J("Call Args")}]),
CallBox = wxStaticBoxSizer:getStaticBox(CallSz),
ScrollWin = wxScrolledWindow:new(CallBox),
ScrollSz = wxBoxSizer:new(?wxVERTICAL),
ok = wxScrolledWindow:setSizerAndFit(ScrollWin, ScrollSz),
ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5),
GridSz = wxFlexGridSizer:new(2, [{gap, {4, 4}}]),
ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL),
ok = wxFlexGridSizer:addGrowableCol(GridSz, 1),
_ = wxStaticBoxSizer:add(ScrollSz, GridSz, zxw:flags(wide)),
_ = wxStaticBoxSizer:add(CallSz, ScrollWin, zxw:flags(wide)),
AddArg =
fun({Name, Type}) ->
L = wxStaticText:new(ScrollWin, ?wxID_ANY, [Name, " : ", textify(Type)]),
C = #w{wx = T} = gd_lib:mono_text(ScrollWin, Name),
_ = wxFlexGridSizer:add(GridSz, L, zxw:flags({base, 5})),
_ = wxFlexGridSizer:add(GridSz, T, zxw:flags({wide, 5})),
C
end,
Controls = lists:map(AddArg, CallArgs),
{CallSz, Controls, true}.
return_sizer(Parent, J, FunIlk, ReturnType) ->
IlkLabel =
case FunIlk =:= init of
false -> textify(ReturnType);
true -> J("Contract Address")
end,
ReturnSz = wxStaticBoxSizer:new(?wxVERTICAL, Parent, [{label, J("Return Type")}]),
ReturnSzBox = wxStaticBoxSizer:getStaticBox(ReturnSz),
ReturnLabel = wxStaticText:new(ReturnSzBox, ?wxID_ANY, IlkLabel),
Single = [{style, ?wxTE_READONLY}],
Return = #w{wx = ReturnTx} = gd_lib:mono_text(ReturnSzBox, return, "", Single),
Copy = #w{wx = CopyB} = gd_lib:button(ReturnSzBox, J("Copy")),
_ = wxButton:disable(CopyB),
_ = wxStaticBoxSizer:add(ReturnSz, ReturnLabel, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(ReturnSz, ReturnTx, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(ReturnSz, CopyB, zxw:flags({wide, 5})),
{ReturnSz, Return, Copy}.
call_param_sizer(Frame, J) ->
{ok, Height} = hz:top_height(),
DefTTL = Height + 10000,
DefGasP = hz:min_gas_price(),
DefGas = 5000000,
DefAmount = 0,
ParamSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("TX Parameters")}]),
ParamBox = wxStaticBoxSizer:getStaticBox(ParamSz),
GridSz = wxFlexGridSizer:new(2, 4, 4),
ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL),
ok = wxFlexGridSizer:addGrowableCol(GridSz, 1),
Amount_L = wxStaticText:new(ParamBox, ?wxID_ANY, J("Amount")),
Amount_T = wxTextCtrl:new(ParamBox, ?wxID_ANY),
ok = wxTextCtrl:setValue(Amount_T, integer_to_list(DefAmount)),
Gas_L = wxStaticText:new(ParamBox, ?wxID_ANY, J("Gas")),
Gas_T = wxTextCtrl:new(ParamBox, ?wxID_ANY),
ok = wxTextCtrl:setValue(Gas_T, integer_to_list(DefGas)),
GasP_L = wxStaticText:new(ParamBox, ?wxID_ANY, J("Gas Price")),
GasP_T = wxTextCtrl:new(ParamBox, ?wxID_ANY),
ok = wxTextCtrl:setValue(GasP_T, integer_to_list(DefGasP)),
TTL_L = wxStaticText:new(ParamBox, ?wxID_ANY, "TTL"),
TTL_T = wxTextCtrl:new(ParamBox, ?wxID_ANY),
ok = wxTextCtrl:setValue(TTL_T, integer_to_list(DefTTL)),
_ = wxFlexGridSizer:add(GridSz, Amount_L, zxw:flags({base, 5})),
_ = wxFlexGridSizer:add(GridSz, Amount_T, zxw:flags({wide, 5})),
_ = wxFlexGridSizer:add(GridSz, Gas_L, zxw:flags({base, 5})),
_ = wxFlexGridSizer:add(GridSz, Gas_T, zxw:flags({wide, 5})),
_ = wxFlexGridSizer:add(GridSz, GasP_L, zxw:flags({base, 5})),
_ = wxFlexGridSizer:add(GridSz, GasP_T, zxw:flags({wide, 5})),
_ = wxFlexGridSizer:add(GridSz, TTL_L, zxw:flags({base, 5})),
_ = wxFlexGridSizer:add(GridSz, TTL_T, zxw:flags({wide, 5})),
_ = wxSizer:add(ParamSz, GridSz, zxw:flags(wide)),
Params =
[{J("TX Amount"), fun gte_0/1, Amount_T} ,
{J("Gas") , fun gt_0/1, Gas_T},
{J("Gas Price"), fun gt_0/1, GasP_T},
{ "TTL", fun gte_0/1, TTL_T}],
{ParamSz, Params}.
%%% Spine
handle_call(tx_hash, _, State) ->
TXHash = do_tx_hash(State),
{reply, TXHash, State};
handle_call(tx_data, _, State = #s{tx_data = TXData}) ->
{reply, TXData, State};
handle_call(tx_info, _, State = #s{tx_info = TXInfo}) ->
{reply, TXInfo, State};
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
{noreply, State}.
handle_cast(Unexpected, State) ->
ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]),
{noreply, State}.
handle_info(retire, State) ->
ok = retire(State),
{noreply, State};
handle_info(Unexpected, State) ->
ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]),
{noreply, State}.
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID},
State = #s{action = #w{id = ID}, status = none}) ->
NewState = prep_call(State),
{noreply, NewState};
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID},
State = #s{action = #w{id = ID}}) ->
NewState = check_tx(State),
{noreply, NewState};
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID},
State = #s{copy = #w{id = ID}}) ->
ok = copy(State),
{noreply, State};
handle_event(#wx{event = #wxClose{}}, State) ->
ok = retire(State),
{noreply, State};
handle_event(Event, State) ->
ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]),
{noreply, State}.
code_change(_, State, _) ->
{ok, State}.
retire(#s{frame = Frame}) ->
wxWindow:destroy(Frame).
terminate(wx_deleted, _) ->
wx:destroy();
terminate(Reason, State) ->
ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]),
wx:destroy().
%%% Handlers
handle_troubling(State = #s{frame = Frame}, Info) ->
ok = wxFrame:raise(Frame),
ok = zxw:show_message(Frame, Info),
State.
do_tx_hash(#s{tx_data = #{"tx_hash" := TXHash}}) ->
TXHash;
do_tx_hash(#s{tx_data = none}) ->
none.
prep_call(State) ->
case gd_con:chain_id() of
{ok, ChainID} -> prep_call2(State, ChainID);
Error -> handle_troubling(State, Error)
end.
prep_call2(State, ChainID) ->
case params(State) of
{ok, Params} -> prep_call3(State, ChainID, Params);
Error -> handle_troubling(State, Error)
end.
prep_call3(State, ChainID, Params) ->
case args(State) of
{ok, Args} -> prep_call4(State, ChainID, Params, {sophia, Args});
Error -> handle_troubling(State, Error)
end.
prep_call4(State = #s{fundef = {"init", init}, build = Build}, ChainID, Params, Args) ->
{CallerID, Nonce, Gas, GP, Amount, TTL} = Params,
case hz:contract_create_built(CallerID, Nonce, Gas, GP, Amount, TTL, Build, Args) of
{ok, CreateTX} -> deploy(State, ChainID, CallerID, CreateTX);
Error -> handle_troubling(State, Error)
end;
prep_call4(State = #s{fundef = {Name, Ilk}, con_id = ConID, build = Build}, ChainID, Params, Args) ->
{CallerID, Nonce, Gas, GP, Amount, TTL} = Params,
AACI = maps:get(aaci, Build),
case hz:contract_call(CallerID, Nonce, Gas, GP, Amount, TTL, AACI, ConID, Name, Args) of
{ok, UnsignedTX} ->
case Ilk of
call -> do_call(State, ChainID, CallerID, UnsignedTX);
dryr -> do_dry_run(State, ConID, UnsignedTX)
end;
Error ->
handle_troubling(State, Error)
end.
params(State = #s{kp = #w{wx = KeyPicker}}) ->
ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)),
PK = unicode:characters_to_binary(ID),
case hz:next_nonce(PK) of
{ok, Nonce} -> params2(State, PK, Nonce);
Error -> handle_troubling(State, Error)
end.
params2(#s{params = Params}, PK, Nonce) ->
case lists:foldl(fun extract/2, {ok, []}, Params) of
{ok, [TTL, GP, Gas, Amount]} -> {ok, {PK, Nonce, Gas, GP, Amount, TTL}};
Error -> Error
end.
extract({Name, Check, Widget}, {Status, Out}) ->
case Check(wxTextCtrl:getValue(Widget)) of
{ok, Value} -> {Status, [Value | Out]};
Error -> {error, [{Name, Error}, Out]}
end.
% TODO: Put some basic checking in here, like for blank strings, at least.
% It should be possible to perform type/parse checks over this since we have
% access to the AACI. But not today.
args(#s{args = ArgFields}) ->
Values = [wxTextCtrl:getValue(W) || #w{wx = W} <- ArgFields],
{ok, Values}.
deploy(State, ChainID, CallerID, CreateTX) ->
case gd_con:sign_call(ChainID, CallerID, CreateTX) of
{ok, SignedTX} -> deploy2(State, SignedTX);
Error -> handle_troubling(State, Error)
end.
deploy2(State = #s{j = J, hash = #w{wx = HashT}, action = #w{wx = ActionB}}, SignedTX) ->
case hz:post_tx(SignedTX) of
{ok, Data = #{"tx_hash" := TXHash}} ->
_ = wxButton:disable(ActionB),
ok = wxButton:setLabel(ActionB, J("Check Transaction Status")),
ok = log(info, "Submitted transaction ~s", [TXHash]),
ok = wxTextCtrl:setValue(HashT, unicode:characters_to_list(TXHash)),
check_tx(State#s{tx_data = Data, status = submitted});
{ok, #{"reason" := Reason}} ->
handle_troubling(State, {error, Reason});
Error ->
handle_troubling(State, Error)
end.
do_call(State, ChainID, CallerID, UnsignedTX) ->
case gd_con:sign_call(ChainID, CallerID, UnsignedTX) of
{ok, SignedTX} -> do_call2(State, SignedTX);
Error -> handle_troubling(State, Error)
end.
do_call2(State = #s{action = #w{wx = ActionB}}, SignedTX) ->
_ = wxButton:disable(ActionB),
case hz:post_tx(SignedTX) of
{ok, #{"reason" := Reason}} -> handle_troubling(State#s{status = rejected}, Reason);
{ok, Data} -> check_tx(State#s{tx_data = Data, status = submitted});
Error -> handle_troubling(State, Error)
end.
do_dry_run(State = #s{action = #w{wx = ActionB}}, ConID, TX) ->
_ = wxButton:disable(ActionB),
case hz:dry_run(TX) of
{ok, Result} -> dry_run2(State#s{tx_info = Result});
Other -> handle_troubling(State, {error, ConID, Other})
end.
dry_run2(State = #s{funret = ReturnType,
return = #w{wx = ReturnT},
copy = #w{wx = CopyB},
tx_data = TXData,
tx_info = TXInfo,
hash = #w{wx = HashT},
info = #w{wx = InfoT}}) ->
ReturnV =
case TXInfo of
#{"results" :=
[#{"call_obj" :=
#{"return_type" := "revert",
"return_value" := ReturnCB},
"result" := "ok","type" := "contract_call"}]} ->
io_lib:format("Revert: ~ts", [hz:decode_bytearray(ReturnCB, sophia)]);
#{"results" :=
[#{"call_obj" :=
#{"return_type" := "ok",
"return_value" := ReturnCB},
"result" := "ok","type" := "contract_call"}]} ->
hz:decode_bytearray(ReturnCB, {sophia, ReturnType});
Other ->
io_lib:format("???: ~tp", [Other])
end,
_ = wxButton:enable(CopyB),
FormattedHash = io_lib:format("~tp", [TXData]),
FormattedInfo = io_lib:format("~tp", [TXInfo]),
ok = wxTextCtrl:setValue(ReturnT, ReturnV),
ok = wxTextCtrl:setValue(HashT, FormattedHash),
ok = wxTextCtrl:setValue(InfoT, FormattedInfo),
State.
check_tx(State = #s{j = J,
fundef = {_, init},
tx_data = #{"tx_hash" := TXHash},
tx_info = none,
status = submitted,
action = #w{wx = ActionB},
info = #w{wx = InfoT}}) ->
case hz:tx_info(TXHash) of
{ok, Info = #{"call_info" := #{"return_type" := "ok", "contract_id" := ConID}}} ->
ok = tell(info, "Contract deployed: ~p", [Info]),
_ = wxButton:disable(ActionB),
ok = gd_con:open_contract(ConID),
self() ! retire,
State;
{error, "Tx not mined"} ->
ok = wxTextCtrl:setValue(InfoT, J("[Transaction not yet mined.]")),
ok = wxButton:setLabel(ActionB, J("Check Deployment Status")),
_ = wxButton:enable(ActionB),
State;
Other ->
FormattedJunk = io_lib:format("~tp", [Other]),
ok = wxTextCtrl:setValue(InfoT, FormattedJunk),
ok = wxButton:setLabel(ActionB, J("Check Depoyment Status")),
_ = wxButton:enable(ActionB),
State
end;
check_tx(State = #s{j = J,
funret = ReturnType,
tx_data = #{"tx_hash" := TXHash},
tx_info = none,
status = submitted,
return = #w{wx = ReturnT},
copy = #w{wx = CopyB},
action = #w{wx = ActionB},
info = #w{wx = InfoT}}) ->
case hz:tx_info(TXHash) of
{ok, Info = #{"call_info" := #{"return_type" := "ok",
"return_value" := ReturnCB}}} ->
FormattedInfo = io_lib:format("~tp", [Info]),
_ = wxButton:enable(CopyB),
_ = wxButton:disable(ActionB),
ReturnV = hz:decode_bytearray(ReturnCB, {sophia, ReturnType}),
ok = wxTextCtrl:setValue(ReturnT, ReturnV),
ok = wxTextCtrl:setValue(InfoT, FormattedInfo),
State#s{status = included, tx_info = Info};
{ok, Reason = #{"call_info" := #{"return_type" := "revert", "return_value" := ReturnCB}}} ->
_ = wxButton:enable(CopyB),
_ = wxButton:disable(ActionB),
ReturnV = io_lib:format("Revert: ~ts", [hz:decode_bytearray(ReturnCB, sophia)]),
ok = wxTextCtrl:setValue(ReturnT, ReturnV),
FormattedInfo = io_lib:format("~tp", [Reason]),
ok = wxTextCtrl:setValue(InfoT, FormattedInfo),
State#s{status = rejected, tx_info = Reason};
{error, "Tx not mined"} ->
ok = wxTextCtrl:setValue(InfoT, J("[Transaction not yet mined.]")),
ok = wxButton:setLabel(ActionB, J("Check Transaction Status")),
_ = wxButton:enable(ActionB),
State;
Error ->
handle_troubling(State, Error)
end;
check_tx(State = #s{tx_data = TXData,
action = #w{wx = ActionB}}) ->
_ = wxButton:disable(ActionB),
tell(info, "TXData: ~p", [TXData]),
ok = tell("Nothing to check"),
State.
copy(#s{tx_info = none}) ->
ok;
copy(#s{return = #w{wx = ReturnT}}) ->
Output = wxTextCtrl:getValue(ReturnT),
gd_lib:copy_to_clipboard(Output).
textify({integer, _, _}) -> "int";
textify({boolean, _, _}) -> "bool";
textify({{bytes, [I]}, _, _}) -> io_lib:format("bytes(~w)", [I]);
textify({{bytes, any}, _, _}) -> "bytes()";
textify({T, _, _}) when is_atom(T) -> atom_to_list(T);
textify({T, _, _}) when is_list(T) -> T;
textify({T, _, _}) -> io_lib:format("~tp", [T]).
gt_0(S) ->
C = "Must be an integer greater than 0",
R =
try
{ok, list_to_integer(S)}
catch
error:badarg -> {error, {S, C}}
end,
case R of
{ok, N} when N > 0 -> {ok, N};
{ok, N} when N =< 0 -> {error, {S, C}};
Error -> Error
end.
gte_0(S) ->
C = "Must be a non-negative integer.",
R =
try
{ok, list_to_integer(S)}
catch
error:badarg -> {error, {S, C}}
end,
case R of
{ok, N} when N >= 0 -> {ok, N};
{ok, N} when N < 0 -> {error, {S, C}};
Error -> Error
end.
+174 -550
View File
File diff suppressed because it is too large Load Diff
+3 -7
View File
@@ -1,5 +1,5 @@
-module(gd_v_netman). -module(gd_v_netman).
-vsn("0.5.4"). -vsn("0.9.0").
-author("Craig Everett <zxq9@zxq9.com>"). -author("Craig Everett <zxq9@zxq9.com>").
-copyright("QPQ AG <info@qpq.swiss>"). -copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later"). -license("GPL-3.0-or-later").
@@ -14,13 +14,9 @@
handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). handle_call/3, handle_cast/2, handle_info/2, handle_event/2]).
-include("$zx_include/zx_logger.hrl"). -include("$zx_include/zx_logger.hrl").
-include("gd.hrl"). -include("gd.hrl").
-include("gdl.hrl").
-record(w,
{name = none :: atom(),
id = 0 :: integer(),
wx = none :: none | wx:wx_object()}).
-record(s, -record(s,
{wx = none :: none | wx:wx_object(), {wx = none :: none | wx:wx_object(),
frame = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(),
@@ -60,7 +56,7 @@ start_link(Args) ->
init({Prefs, Manifest}) -> init({Prefs, Manifest}) ->
Lang = maps:get(lang, Prefs, en_us), Lang = maps:get(lang, Prefs, en),
Trans = gd_jt:read_translations(?MODULE), Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans), J = gd_jt:j(Lang, Trans),
Wx = wx:new(), Wx = wx:new(),
+225 -103
View File
@@ -1,5 +1,19 @@
%%% @doc
%%% The GajuDesk Wallet Manager
%%%
%%% This is an interface for managing multiple wallets.
%%% A wallet is defined as a collection of accounts/keys.
%%%
%%% NOTE:
%%% Any call to `gd_con:show_ui(gd_v_wallman)' must be followed by a call to
%%% either `gd_v_wallman:to_front()' or `gd_v_wallman:first_run()' or else
%%% this UI process will just sit in the background (invisible) until told to
%%% do something.
%%% @end
-module(gd_v_wallman). -module(gd_v_wallman).
-vsn("0.5.4"). -vsn("0.9.0").
-author("Craig Everett <zxq9@zxq9.com>"). -author("Craig Everett <zxq9@zxq9.com>").
-copyright("QPQ AG <info@qpq.swiss>"). -copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later"). -license("GPL-3.0-or-later").
@@ -7,20 +21,16 @@
-behavior(wx_object). -behavior(wx_object).
%-behavior(gd_v). %-behavior(gd_v).
-include_lib("wx/include/wx.hrl"). -include_lib("wx/include/wx.hrl").
-export([to_front/1]). -export([to_front/0, to_front/1, first_run/0, trouble/1]).
-export([show/2]). -export([show/2]).
-export([start_link/1]). -export([start_link/1]).
-export([init/1, terminate/2, code_change/3, -export([init/1, terminate/2, code_change/3,
handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). handle_call/3, handle_cast/2, handle_info/2, handle_event/2]).
-include("$zx_include/zx_logger.hrl"). -include("$zx_include/zx_logger.hrl").
-include("gd.hrl"). -include("gd.hrl").
-include("gdl.hrl").
-record(w,
{name = none :: atom(),
id = 0 :: integer(),
wx = none :: none | wx:wx_object()}).
-record(s, -record(s,
{wx = none :: none | wx:wx_object(), {wx = none :: none | wx:wx_object(),
frame = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(),
@@ -29,11 +39,18 @@
prefs = #{} :: map(), prefs = #{} :: map(),
wallets = [] :: [#wr{}], wallets = [] :: [#wr{}],
picker = none :: none | wx:wx_object(), picker = none :: none | wx:wx_object(),
buttons = [] :: [#w{}]}). buttons = [] :: [#w{}],
wiz = none :: none | {wx:wx_object(), Buttons :: [#w{}]}}).
%%% Interface %%% Interface
-spec to_front() -> ok.
to_front() ->
wx_object:cast(?MODULE, to_front).
-spec to_front(Win) -> ok -spec to_front(Win) -> ok
when Win :: wx:wx_object(). when Win :: wx:wx_object().
@@ -41,6 +58,19 @@ to_front(Win) ->
wx_object:cast(Win, to_front). wx_object:cast(Win, to_front).
-spec first_run() -> ok.
first_run() ->
wx_object:cast(?MODULE, first_run).
-spec trouble(Info) -> ok
when Info :: term().
trouble(Info) ->
wx_object:cast(?MODULE, {trouble, Info}).
-spec show(Win, Manifest) -> ok -spec show(Win, Manifest) -> ok
when Win :: wx:xw_object(), when Win :: wx:xw_object(),
Manifest :: [#wr{}]. Manifest :: [#wr{}].
@@ -56,7 +86,7 @@ start_link(Args) ->
init({Prefs, Manifest}) -> init({Prefs, Manifest}) ->
Lang = maps:get(lang, Prefs, en_us), Lang = maps:get(lang, Prefs, en),
Trans = gd_jt:read_translations(?MODULE), Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans), J = gd_jt:j(Lang, Trans),
Wx = wx:new(), Wx = wx:new(),
@@ -89,11 +119,24 @@ init({Prefs, Manifest}) ->
ok = wxFrame:setSizer(Frame, MainSz), ok = wxFrame:setSizer(Frame, MainSz),
ok = wxSizer:layout(MainSz), ok = wxSizer:layout(MainSz),
ok = gd_v:safe_size(Frame, Prefs), NewPrefs =
case maps:is_key(geometry, Prefs) of
true ->
Prefs;
false ->
Display = wxDisplay:new(),
{_, _, W, H} = wxDisplay:getGeometry(Display),
ok = wxDisplay:destroy(Display),
WW = 500,
WH = 350,
X = (W div 2) - (WW div 2),
Y = (H div 2) - (WH div 2),
Prefs#{geometry => {X, Y, WW, WH}}
end,
ok = gd_v:safe_size(Frame, NewPrefs),
ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:connect(Frame, command_button_clicked),
ok = wxFrame:connect(Frame, close_window), ok = wxFrame:connect(Frame, close_window),
true = wxFrame:show(Frame),
ok = wxListBox:connect(Picker, command_listbox_doubleclicked), ok = wxListBox:connect(Picker, command_listbox_doubleclicked),
State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs, State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs,
wallets = Manifest, wallets = Manifest,
@@ -103,16 +146,23 @@ init({Prefs, Manifest}) ->
%%% wx_object %%% wx_object
handle_call(Unexpected, From, State) -> handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]),
{noreply, State}. {noreply, State}.
handle_cast(to_front, State = #s{frame = Frame}) -> handle_cast(to_front, State = #s{frame = Frame}) ->
ok = ensure_shown(Frame),
ok = wxFrame:raise(Frame), ok = wxFrame:raise(Frame),
{noreply, State}; {noreply, State};
handle_cast(first_run, State) ->
NewState = do_first_run(State),
{noreply, NewState};
handle_cast({trouble, Info}, State) ->
ok = handle_troubling(State, Info),
{noreply, State};
handle_cast({show, Manifest}, State) -> handle_cast({show, Manifest}, State) ->
NewState = do_show(Manifest, State), NewState = do_show(Manifest, State),
{noreply, NewState}; {noreply, NewState};
@@ -128,7 +178,7 @@ handle_info(Unexpected, State) ->
handle_event(#wx{event = #wxCommand{type = command_button_clicked}, handle_event(#wx{event = #wxCommand{type = command_button_clicked},
id = ID}, id = ID},
State = #s{buttons = Buttons}) -> State = #s{buttons = Buttons, wiz = none}) ->
NewState = NewState =
case lists:keyfind(ID, #w.id, Buttons) of case lists:keyfind(ID, #w.id, Buttons) of
#w{name = open} -> do_open(State); #w{name = open} -> do_open(State);
@@ -141,17 +191,34 @@ handle_event(#wx{event = #wxCommand{type = command_button_clicked},
{noreply, NewState}; {noreply, NewState};
handle_event(#wx{event = #wxCommand{type = command_listbox_doubleclicked, handle_event(#wx{event = #wxCommand{type = command_listbox_doubleclicked,
commandInt = Selected}}, commandInt = Selected}},
State) -> State = #s{wiz = none}) ->
NewState = do_open2(Selected + 1, State), ok = do_open2(Selected + 1, State),
{noreply, NewState}; {noreply, State};
handle_event(#wx{event = #wxClose{}}, State) -> handle_event(#wx{event = #wxClose{}}, State = #s{wiz = none}) ->
ok = do_close(State), ok = do_close(State),
{noreply, State}; {noreply, State};
handle_event(#wx{event = #wxCommand{type = command_button_clicked},
id = ID},
State = #s{wiz = {_, WizButtons}}) ->
NewState =
case lists:keyfind(ID, #w.id, WizButtons) of
#w{name = noob} -> wiz_noob_assist(State);
#w{name = l33t} -> close_wiz(State);
false -> State
end,
{noreply, NewState};
handle_event(#wx{event = #wxClose{}}, State = #s{wiz = {_, _}}) ->
NewState = close_wiz(State),
{noreply, NewState};
handle_event(Event, State) -> handle_event(Event, State) ->
ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]),
{noreply, State}. {noreply, State}.
handle_troubling(#s{frame = Frame}, Info) ->
zxw:show_message(Frame, Info).
code_change(_, State, _) -> code_change(_, State, _) ->
{ok, State}. {ok, State}.
@@ -172,6 +239,26 @@ do_show(Manifest, State = #s{picker = Picker}) ->
State#s{wallets = Manifest}. State#s{wallets = Manifest}.
do_first_run(State = #s{frame = Frame, wallets = Manifest}) ->
Count = length(Manifest),
if
Count =:= 0 ->
do_wiz(State);
Count =:= 1 ->
do_open(State);
Count > 1 ->
true = wxFrame:show(Frame),
wxFrame:raise(Frame),
State
end.
close_wiz(State = #s{frame = Frame, wiz = {Wiz, _}}) ->
ok = wxWindow:destroy(Wiz),
true = wxFrame:show(Frame),
State#s{wiz = none}.
do_close(#s{frame = Frame, prefs = Prefs}) -> do_close(#s{frame = Frame, prefs = Prefs}) ->
Geometry = Geometry =
case wxTopLevelWindow:isMaximized(Frame) of case wxTopLevelWindow:isMaximized(Frame) of
@@ -194,10 +281,26 @@ handle_button(Name, State) ->
do_open(State = #s{wallets = []}) -> do_open(State = #s{wallets = []}) ->
State; State;
do_open(State = #s{wallets = [#wr{pass = true, path = Path}]}) ->
ok = do_open3(Path, State),
State;
do_open(State = #s{wallets = [#wr{pass = false, path = Path}], frame = Frame}) ->
ok =
case gd_con:open_wallet(Path, none) of
ok ->
do_close(State);
Error ->
ok = ensure_shown(Frame),
trouble(Error)
end,
State;
do_open(State = #s{picker = Picker}) -> do_open(State = #s{picker = Picker}) ->
case wxListBox:getSelection(Picker) of case wxListBox:getSelection(Picker) of
-1 -> State; -1 ->
Selected -> do_open2(Selected + 1, State) State;
Selected ->
ok = do_open2(Selected + 1, State),
State
end. end.
do_open2(Selected, State = #s{wallets = Wallets}) -> do_open2(Selected, State = #s{wallets = Wallets}) ->
@@ -206,52 +309,84 @@ do_open2(Selected, State = #s{wallets = Wallets}) ->
do_open3(Path, State); do_open3(Path, State);
#wr{pass = false, path = Path} -> #wr{pass = false, path = Path} ->
ok = gd_con:open_wallet(Path, none), ok = gd_con:open_wallet(Path, none),
ok = do_close(State), do_close(State)
State
end. end.
do_open3(Path, State = #s{frame = Frame, j = J}) -> do_open3(Path, State = #s{frame = Frame, j = J}) ->
Label = J("Password"), Title = J("Passphrase"),
Dialog = wxDialog:new(Frame, ?wxID_ANY, Label), case zxw_modal_text:show(Frame, Title, [{password, true}]) of
Sizer = wxBoxSizer:new(?wxVERTICAL), {ok, Phrase} ->
PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label}]), case gd_con:open_wallet(Path, Phrase) of
PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY, [{style, ?wxTE_PASSWORD}]), ok -> do_close(State);
_ = wxStaticBoxSizer:add(PassSz, PassTx, zxw:flags(wide)), Error -> trouble(Error)
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(wide)),
ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxBoxSizer:layout(Sizer),
ok = wxDialog:setSize(Dialog, {500, 130}),
ok = wxFrame:center(Dialog),
ok = wxStyledTextCtrl:setFocus(PassTx),
case wxDialog:showModal(Dialog) of
?wxID_OK ->
case wxTextCtrl:getValue(PassTx) of
"" ->
ok = wxDialog:destroy(Dialog),
State;
Phrase ->
ok = wxDialog:destroy(Dialog),
ok = gd_con:open_wallet(Path, Phrase),
ok = do_close(State),
State
end; end;
?wxID_CANCEL -> cancel ->
ok = wxDialog:destroy(Dialog), ok
end.
do_wiz(State = #s{wx = WX, lang = Lang, wiz = none}) ->
Trans = gd_jt:read_translations(?MODULE),
J = gd_jt:j(Lang, Trans),
Wiz = wxFrame:new(WX, ?wxID_ANY, J("Initializing Wallet")),
MainSz = wxBoxSizer:new(?wxVERTICAL),
ButtonTemplates =
[{noob, J("I'm new!\nCreate a new account for me.")},
{l33t, J("Open the wallet manager.")}],
MakeButton =
fun({Name, Label}) ->
B = wxButton:new(Wiz, ?wxID_ANY, [{label, Label}]),
#w{name = Name, id = wxButton:getId(B), wx = B}
end,
Buttons = lists:map(MakeButton, ButtonTemplates),
Add = fun(#w{wx = Button}) -> wxBoxSizer:add(MainSz, Button, zxw:flags(wide)) end,
ok = lists:foreach(Add, Buttons),
ok = wxFrame:setSizer(Wiz, MainSz),
ok = wxSizer:layout(MainSz),
ok = wxFrame:connect(Wiz, command_button_clicked),
ok = wxFrame:connect(Wiz, close_window),
ok = wxFrame:setSize(Wiz, {300, 300}),
ok = wxFrame:center(Wiz),
true = wxFrame:show(Wiz),
State#s{wiz = {Wiz, Buttons}}.
wiz_noob_assist(State = #s{j = J, wiz = {Wiz, _}}) ->
DefaultDir = zx_lib:path(var, "otpr", "gajudesk"),
Name = default_name(),
Path = filename:join(DefaultDir, Name),
case do_new2(Path, J, Wiz) of
ok ->
Label = J("Account 1"),
ok = gd_con:make_key({eddsa, ed25519}, 256, Label, <<>>, none, {sha3, 256}),
ok = do_close(State),
State;
abort ->
State State
end. end.
default_name() ->
{{YY, MM, DD}, {Hr, Mn, Sc}} = calendar:local_time(),
Form = "~4.10.0B-~2.10.0B-~2.10.0B_~2.10.0B-~2.10.0B-~2.10.0B",
Name = io_lib:format(Form, [YY, MM, DD, Hr, Mn, Sc]),
unicode:characters_to_list(Name ++ ".gaju").
do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) -> do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) ->
DefaultDir = maps:get(dir, Prefs, zx_lib:path(var, "otpr", "gajudesk")), DefaultDir = maps:get(dir, Prefs, zx_lib:path(var, "otpr", "gajudesk")),
Options = Options =
[{message, J("Save Location")}, [{message, J("Save Location")},
{defaultDir, DefaultDir}, {defaultDir, DefaultDir},
{defaultFile, "default.gaju"}, {defaultFile, default_name()},
{wildCard, "*.gaju"}, {wildCard, "*.gaju"},
{sz, {300, 270}},
{style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}],
Dialog = wxFileDialog:new(Frame, Options), Dialog = wxFileDialog:new(Frame, Options),
case wxFileDialog:showModal(Dialog) of case wxFileDialog:showModal(Dialog) of
@@ -263,7 +398,9 @@ do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) ->
case do_new2(Path, J, Frame) of case do_new2(Path, J, Frame) of
ok -> ok ->
NewPrefs = maps:put(dir, Dir, Prefs), NewPrefs = maps:put(dir, Dir, Prefs),
do_close(State#s{prefs = NewPrefs}); NewState = State#s{prefs = NewPrefs},
ok = do_close(NewState),
NewState;
abort -> abort ->
State State
end; end;
@@ -275,7 +412,8 @@ do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) ->
do_new2(Path, J, Frame) -> do_new2(Path, J, Frame) ->
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Wallet"), [{size, {400, 250}}]), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Wallet"), [{size, {400, 250}}]),
Sizer = wxBoxSizer:new(?wxVERTICAL), Sizer = wxBoxSizer:new(?wxVERTICAL),
NetworkOptions = [J("Mainnet"), J("Testnet")],
Network = wxRadioBox:new(Dialog, ?wxID_ANY, J("Network"), {0, 0}, {50, 50}, NetworkOptions),
NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]),
NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY),
_ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)), _ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)),
@@ -294,12 +432,14 @@ do_new2(Path, J, Frame) ->
_ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)),
_ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)),
_ = wxBoxSizer:add(Sizer, Network, zxw:flags(base)),
_ = wxSizer:add(Sizer, NameSz, zxw:flags(base)), _ = wxSizer:add(Sizer, NameSz, zxw:flags(base)),
_ = wxSizer:add(Sizer, PassSz, zxw:flags(base)), _ = wxSizer:add(Sizer, PassSz, zxw:flags(base)),
_ = wxSizer:add(Sizer, PassConSz, zxw:flags(base)), _ = wxSizer:add(Sizer, PassConSz, zxw:flags(base)),
_ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)),
ok = wxDialog:setSizer(Dialog, Sizer), ok = wxDialog:setSizer(Dialog, Sizer),
ok = wxDialog:setSize(Dialog, {300, 270}),
ok = wxBoxSizer:layout(Sizer), ok = wxBoxSizer:layout(Sizer),
ok = wxDialog:center(Dialog), ok = wxDialog:center(Dialog),
ok = wxStyledTextCtrl:setFocus(NameTx), ok = wxStyledTextCtrl:setFocus(NameTx),
@@ -307,6 +447,12 @@ do_new2(Path, J, Frame) ->
Result = Result =
case wxDialog:showModal(Dialog) of case wxDialog:showModal(Dialog) of
?wxID_OK -> ?wxID_OK ->
Net =
case wxRadioBox:getSelection(Network) of
0 -> mainnet;
1 -> testnet;
_ -> mainnet
end,
Name = Name =
case wxTextCtrl:getValue(NameTx) of case wxTextCtrl:getValue(NameTx) of
"" -> Path; "" -> Path;
@@ -318,17 +464,17 @@ do_new2(Path, J, Frame) ->
{P, P} -> P; {P, P} -> P;
{_, _} -> bad {_, _} -> bad
end, end,
do_new3(Name, Path, Pass); do_new3(Net, Name, Path, Pass);
?wxID_CANCEL -> ?wxID_CANCEL ->
abort abort
end, end,
ok = wxDialog:destroy(Dialog), ok = wxDialog:destroy(Dialog),
Result. Result.
do_new3(_, _, bad) -> do_new3(_, _, _, bad) ->
abort; abort;
do_new3(Name, Path, Pass) -> do_new3(Net, Name, Path, Pass) ->
gd_con:new_wallet(Name, Path, Pass). gd_con:new_wallet(Net, Name, Path, Pass).
do_import(State = #s{frame = Frame, j = J, prefs = Prefs}) -> do_import(State = #s{frame = Frame, j = J, prefs = Prefs}) ->
@@ -347,7 +493,9 @@ do_import(State = #s{frame = Frame, j = J, prefs = Prefs}) ->
case do_import2(Dir, File, J, Frame) of case do_import2(Dir, File, J, Frame) of
ok -> ok ->
NewPrefs = maps:put(dir, Dir, Prefs), NewPrefs = maps:put(dir, Dir, Prefs),
do_close(State#s{prefs = NewPrefs}); NewState = State#s{prefs = NewPrefs},
ok = do_close(NewState),
NewState;
abort -> abort ->
State State
end; end;
@@ -360,51 +508,15 @@ do_import2(_, "", _, _) ->
abort; abort;
do_import2(Dir, File, J, Frame) -> do_import2(Dir, File, J, Frame) ->
Path = filename:join(Dir, File), Path = filename:join(Dir, File),
Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Import Wallet")), Title = J("Import Wallet"),
Sizer = wxBoxSizer:new(?wxVERTICAL), NameL = J("Wallet Name"),
PassL = J("Passphrase (leave blank if none)"),
NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), OK_L = J("OK"),
NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), CancelL = J("Cancel"),
_ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)), case gd_m_wallet_importer:show(Frame, Title, NameL, PassL, OK_L, CancelL) of
PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Passphrase")}]), {ok, Name, Pass} -> gd_con:import_wallet(Name, Path, Pass);
PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY), cancel -> abort
_ = wxSizer:add(PassSz, PassTx, zxw:flags(wide)), end.
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, NameSz, zxw:flags(base)),
_ = wxSizer:add(Sizer, PassSz, 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(NameTx),
Result =
case wxDialog:showModal(Dialog) of
?wxID_OK ->
Name =
case wxTextCtrl:getValue(NameTx) of
"" -> Path;
N -> N
end,
Pass =
case wxTextCtrl:getValue(PassTx) of
"" -> none;
P -> P
end,
gd_con:import_wallet(Name, Path, Pass);
?wxID_CANCEL ->
abort
end,
ok = wxDialog:destroy(Dialog),
Result.
do_drop(State = #s{picker = Picker}) -> do_drop(State = #s{picker = Picker}) ->
@@ -449,3 +561,13 @@ do_drop(Selected, State = #s{j = J, frame = Frame, wallets = Wallets}) ->
ok ok
end, end,
State. State.
ensure_shown(Frame) ->
case wxWindow:isShown(Frame) of
true ->
ok;
false ->
true = wxFrame:show(Frame),
ok
end.
+7 -7
View File
@@ -1,20 +1,20 @@
{name,"Clutch"}. {name,"GajuDesk"}.
{type,gui}. {type,gui}.
{modules,[]}. {modules,[]}.
{prefix,"gd"}. {prefix,"gd"}.
{author,"Craig Everett"}. {author,"Craig Everett"}.
{desc,"A desktop client for the Gajumaru network of blockchain networks"}. {desc,"A desktop client for the Gajumaru network of blockchain networks"}.
{package_id,{"otpr","gajudesk",{0,5,4}}}. {package_id,{"otpr","gajudesk",{0,9,0}}}.
{deps,[{"otpr","hakuzaru",{0,5,1}}, {deps,[{"otpr","hakuzaru",{0,9,1}},
{"otpr","zxwidgets",{1,1,0}},
{"otpr","eblake2",{1,0,1}},
{"otpr","base58",{0,1,1}},
{"otpr","gmserialization",{0,1,3}}, {"otpr","gmserialization",{0,1,3}},
{"otpr","sophia",{9,0,0}}, {"otpr","sophia",{9,0,0}},
{"otpr","gmbytecode",{3,4,1}}, {"otpr","gmbytecode",{3,4,1}},
{"otpr","lom",{1,0,0}}, {"otpr","lom",{1,0,0}},
{"otpr","zj",{1,1,0}}, {"otpr","zj",{1,1,0}},
{"otpr","erl_base58",{0,1,0}}, {"otpr","ec_utils",{1,0,0}}]}.
{"otpr","eblake2",{1,0,0}},
{"otpr","ec_utils",{1,0,0}},
{"otpr","zxwidgets",{1,0,1}}]}.
{key_name,none}. {key_name,none}.
{a_email,"craigeverett@qpq.swiss"}. {a_email,"craigeverett@qpq.swiss"}.
{c_email,"info@qpq.swiss"}. {c_email,"info@qpq.swiss"}.