From 95015eb50c4972e5df9a5853ca3858a32d872f92 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sun, 5 Jan 2025 13:34:56 +0900 Subject: [PATCH 01/10] Stashing --- src/gmc_con.erl | 65 ++++++++++++++++++++++++++++++++++++++++++++ src/gmc_gui.erl | 4 ++- src/gmc_v_devman.erl | 34 ++++++++++++----------- 3 files changed, 86 insertions(+), 17 deletions(-) diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 1c8eae7..a17eb4d 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -13,9 +13,11 @@ -behavior(gen_server). -export([show_ui/1, open_wallet/2, close_wallet/0, new_wallet/3, import_wallet/3, drop_wallet/2, + selected/1, password/2, refresh/0, nonce/1, spend/2, chain/1, grids/1, sign_mess/1, sign_tx/1, + deploy/2, make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1, add_node/1, set_sole_node/1]). -export([encrypt/2, decrypt/2]). @@ -41,6 +43,7 @@ {version = 1 :: integer(), window = none :: none | wx:wx_object(), tasks = [] :: [#ui{}], + picked = 0 :: non_neg_integer(), wallet = none :: none | #wallet{}, pass = none :: none | binary(), prefs = #{} :: #{module() := term()}, @@ -104,6 +107,13 @@ drop_wallet(Path, Delete) -> gen_server:cast(?MODULE, {drop_wallet, Path, Delete}). +-spec selected(Index) -> ok + when Index :: pos_integer() | none. + +selected(Index) -> + gen_server:cast(?MODULE, {selected, Index}). + + -spec password(Old, New) -> ok when Old :: none | string(), New :: none | string(). @@ -162,6 +172,17 @@ sign_tx(Request) -> gen_server:cast(?MODULE, {sign_tx, Request}). +-spec deploy(Build, InitArgs) -> Result + when Build :: map(), + InitArgs :: [Arg :: string()], + Result :: {ok, TX_Hash :: clutch:id()} + | {error, Reason}, + Reason :: term(). % FIXME + +deploy(Build, InitArgs) -> + gen_server:call(?MODULE, {deploy, Build, InitArgs}). + + -spec make_key(Type, Size, Name, Seed, Encoding, Transform) -> ok when Type :: {eddsa, ed25519}, Size :: 256, @@ -262,6 +283,7 @@ start_link() -> init(none) -> ok = log(info, "Starting"), + process_flag(sensitive, true), Prefs = read_prefs(), GUI_Prefs = maps:get(gmc_gui, Prefs, #{}), Window = gmc_gui:start_link(GUI_Prefs), @@ -304,6 +326,9 @@ handle_call({save, Module, Prefs}, _, State) -> handle_call({mnemonic, ID}, _, State) -> Response = do_mnemonic(ID, State), {reply, Response, State}; +handle_call({deploy, Build, InitArgs}, _, State) -> + Result = do_deploy(Build, InitArgs, State), + {reply, Result, State}; handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. @@ -337,6 +362,9 @@ handle_cast({import_wallet, Name, Path, Password}, State) -> handle_cast({drop_wallet, Path, Delete}, State) -> NewState = do_drop_wallet(Path, Delete, State), {noreply, NewState}; +handle_cast({selected, Index}, State) -> + NewState = State#s{selected = Index}, + {noreply, NewState}; handle_cast({password, Old, New}, State) -> NewState = do_password(Old, New, State), {noreply, NewState}; @@ -870,6 +898,43 @@ do_mnemonic(ID, #s{wallet = #wallet{keys = Keys}}) -> end. +do_deploy(Build, InitArgs, #s{selected = Index, wallet = #wallet{keys = Keys}}) -> + #key{pair = #s{public = PubKey, secret := SecKey}} = lists:nth(Index, Keys), + case hz:contract_create_built(PubKey, Build, InitArgs) of + {ok, CreateTX} -> do_deploy2(SecKey, CreateTX); + Error -> Error + end. + +do_deploy2(SecKey, CreateTX) -> + case gmc_con:sign(Request) of + {ok, SignedTX} -> do_deploy3(SignedTX); + Error -> Error + end. + +do_deploy3(SignedTX) -> + case hz:post_tx(SignedTX) of + {ok, Data = #{"tx_hash" := TXHash}} -> + ok = tell("Contract deploy TX succeded with: ~p", [TXHash]), + do_deploy4(Data); + {ok, WTF} -> + {error, WTF}; + Error -> + Error + end. + +do_deploy4(#{"tx_hash" := TXHash}) -> + case hz:tx_info(TZHash) of + {ok, {"call_info" := #{"return_type" := "ok", "contract_id" := ConID}}} -> + {contract_id, ConID}; + {error, "Tx not mined"} -> + {tx_hash, TXHash}; + {ok, Reason = {"call_info" := #{"return_type" := "revert"}}} -> + {error, Reason}; + Error -> + Error + end. + + do_rename_key(ID, NewName, State = #s{wallet = W}) -> #wallet{poas = POAs, keys = Keys} = W, A = lists:keyfind(ID, #poa.id, POAs), diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 66f1c05..1bbb0e5 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -863,10 +863,12 @@ do_selection(Selected, State = #s{prefs = Prefs, accounts = Accounts, balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}}) when Selected < length(Accounts) -> - #poa{id = ID, balances = Balances} = lists:nth(Selected + 1, Accounts), + OneBasedIndex = Selected + 1, + #poa{id = ID, balances = Balances} = lists:nth(OneBasedIndex, Accounts), [#balance{total = Pucks}] = Balances, ok = wxStaticText:setLabel(I, ID), ok = wxStaticText:setLabel(B, price_to_string(Pucks)), + ok = gmc_con:selected(OneBasedIndex), NewPrefs = maps:put(selected, Selected, Prefs), State#s{prefs = NewPrefs}; do_selection(_, State) -> diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl index c552dd8..cf8034e 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -234,15 +234,15 @@ handle_info(Unexpected, State) -> handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, - id = ID}, + id = ID}, State = #s{buttons = Buttons}) -> NewState = case maps:get(ID, Buttons, undefined) of - #w{name = new} -> new_file(State); - #w{name = open} -> open_file(State); - #w{name = close} -> close_file(State); - #w{name = compile} -> compile(State); - #w{name = Name} -> clicked(State, Name); + #w{name = new} -> new_file(State); + #w{name = open} -> open_file(State); + #w{name = close} -> close_file(State); + #w{name = compile} -> compile(State); + #w{name = Name, wx = Button} -> clicked(State, Name, Button); undefined -> tell("Received message: ~w", [E]), State @@ -279,30 +279,32 @@ terminate(Reason, State) -> %%% Doers -clicked(State = #s{book = {Notebook, Pages}}, Name) -> +clicked(State = #s{book = {Notebook, Pages}}, Name, Button) -> case wxNotebook:getSelection(Notebook) of ?wxNOT_FOUND -> + ok = tell("Inconcievable! No notebook page is selected!"), State; Index -> Page = lists:nth(Index + 1, Pages), - clicked(State, Page, Name) + clicked(State, Page, Name, Button) end. clicked(State, #p{instances = Is, funs = {_, Funs}, builds = Builds}, {<<"init">>, call}) -> - BuildLabel = wxChoice:getStringSelection(Is), - Build = maps:get(BuildLabel, Builds), + Label = wxChoice:getStringSelection(Is), + Build = maps:get(Label, Builds), #f{args = Args} = lists:keyfind(<<"init">>, #f.name, Funs), InitArgs = lists:map(fun get_arg/1, Args), - ok = tell("BuildLabel: ~p~nArgs: ~p~nInitArgs: ~p", [BuildLabel, Args, InitArgs]), -% contract_create_built( - State; + ok = tell("Label: ~p~nArgs: ~p~nInitArgs: ~p", [Label, Args, InitArgs]), + case gmc_con:deploy(Build, InitArgs) of + {contract_id, ConID} -> + {tx_hash, TX_Hash} -> + {error, Reason} -> + ok = tell("Deploy failed with: ~p", [Reason]) + State clicked(State, Page, Name) -> ok = tell("Button: ~p~nPage: ~p", [Name, Page]), State. -get_arg({_, InputField, _}) -> - wxTextCtrl:getValue(InputField). - new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = -- 2.30.2 From bfa8cc91c26752d03680e0a96f3151fd86c7ce12 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Wed, 8 Jan 2025 14:49:22 +0900 Subject: [PATCH 02/10] WIP --- include/gmc.hrl | 8 +++++--- src/gmc_con.erl | 19 +++++++++---------- src/gmc_gui.erl | 4 ---- src/gmc_sup.erl | 14 +++++++------- src/gmc_v_devman.erl | 7 +++++-- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/include/gmc.hrl b/include/gmc.hrl index c2cab3f..d68e777 100644 --- a/include/gmc.hrl +++ b/include/gmc.hrl @@ -58,9 +58,11 @@ -record(tx, - {id = none :: none | clutch:id(), - amount = 0 :: non_neg_integer(), - type = spend :: spend | atom()}). + {id = none :: none | clutch:id(), + amount = 0 :: non_neg_integer(), + type = spend :: atom(), + status = submitted :: submitted | mined | rejected | failed, + meta = "" :: string()}). -record(spend_tx, diff --git a/src/gmc_con.erl b/src/gmc_con.erl index a17eb4d..6dd215d 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -1,7 +1,5 @@ %%% @doc %%% Clutch Controller -%%% -%%% This process is a in charge of maintaining the program's state. %%% @end -module(gmc_con). @@ -40,14 +38,15 @@ mon = none :: none | reference()}). -record(s, - {version = 1 :: integer(), - window = none :: none | wx:wx_object(), - tasks = [] :: [#ui{}], - picked = 0 :: non_neg_integer(), - wallet = none :: none | #wallet{}, - pass = none :: none | binary(), - prefs = #{} :: #{module() := term()}, - wallets = [] :: [#wr{}]}). + {version = 1 :: integer(), + window = none :: none | wx:wx_object(), + tasks = [] :: [#ui{}], + picked = 0 :: non_neg_integer(), + wallet = none :: none | #wallet{}, + pass = none :: none | binary(), + prefs = #{} :: #{module() := term()}, + wallets = [] :: [#wr{}], + in_flight = [] :: [#tx{}]}). -type state() :: #s{}. diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 1bbb0e5..384f49c 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -1,9 +1,5 @@ %%% @doc %%% Clutch GUI -%%% -%%% This process is responsible for creating the main GUI frame displayed to the user. -%%% -%%% Reference: http://erlang.org/doc/man/wx_object.html %%% @end -module(gmc_gui). diff --git a/src/gmc_sup.erl b/src/gmc_sup.erl index 0bece53..d48dc72 100644 --- a/src/gmc_sup.erl +++ b/src/gmc_sup.erl @@ -36,11 +36,11 @@ start_link() -> init([]) -> RestartStrategy = {one_for_one, 0, 60}, - Clients = {gmc_con, - {gmc_con, start_link, []}, - permanent, - 5000, - worker, - [gmc_con]}, - Children = [Clients], + Controller = {gmc_con, + {gmc_con, start_link, []}, + permanent, + 5000, + worker, + [gmc_con]}, + Children = [Controller], {ok, {RestartStrategy, Children}}. diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl index cf8034e..100b904 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -282,14 +282,17 @@ terminate(Reason, State) -> clicked(State = #s{book = {Notebook, Pages}}, Name, Button) -> case wxNotebook:getSelection(Notebook) of ?wxNOT_FOUND -> - ok = tell("Inconcievable! No notebook page is selected!"), + ok = tell(warning, "Inconcievable! No notebook page is selected!"), State; Index -> Page = lists:nth(Index + 1, Pages), clicked(State, Page, Name, Button) end. -clicked(State, #p{instances = Is, funs = {_, Funs}, builds = Builds}, {<<"init">>, call}) -> +clicked(State, + #p{instances = Is, funs = {_, Funs}, builds = Builds}, + {<<"init">>, call}, + Button) -> Label = wxChoice:getStringSelection(Is), Build = maps:get(Label, Builds), #f{args = Args} = lists:keyfind(<<"init">>, #f.name, Funs), -- 2.30.2 From c904ed66f3f087fab46d3ef8fb963a67af8437e1 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Wed, 15 Jan 2025 13:17:18 +0900 Subject: [PATCH 03/10] WIP --- include/gmc.hrl | 5 +++-- src/clutch.erl | 14 ++++++++------ src/gmc_con.erl | 35 ++++++++++++++++------------------- src/gmc_v_devman.erl | 18 +++++++++++++----- 4 files changed, 40 insertions(+), 32 deletions(-) diff --git a/include/gmc.hrl b/include/gmc.hrl index d68e777..261f9cb 100644 --- a/include/gmc.hrl +++ b/include/gmc.hrl @@ -77,13 +77,14 @@ -record(wallet, - {version = 1 :: integer(), + {version = 2 :: integer(), name = "" :: string(), poas = [] :: [#poa{}], keys = [] :: [#key{}], chain_id = <<"groot.devnet">> :: binary(), endpoint = #node{} :: #node{}, - nets = [#net{}] :: [#net{}]}). + nets = [#net{}] :: [#net{}], + txs = #{} :: clutch:key_txs()}). diff --git a/src/clutch.erl b/src/clutch.erl index 777272e..01bf0a8 100644 --- a/src/clutch.erl +++ b/src/clutch.erl @@ -12,17 +12,19 @@ -export([ts/0]). -export([start/2, stop/1]). --export_type([id/0, key/0, poa/0, tx/0, ts/0]). +-export_type([id/0, key/0, poa/0, tx/0, ts/0, key_txs/0]). -include("$zx_include/zx_logger.hrl"). -include("gmc.hrl"). --type id() :: binary(). --type key() :: #key{}. --type poa() :: #poa{}. --type tx() :: #tx{}. --type ts() :: integer(). +-type id() :: binary(). +-type key() :: #key{}. +-type poa() :: #poa{}. +-type tx() :: #tx{}. +-type ts() :: integer(). +-type chain_txs() :: #{ChainID :: binary() := [tx()]}. +-type key_txs() :: #{id() := {{LastCheck :: ts(), mdw | node}, chain_txs()}}. ts() -> diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 6dd215d..275eabf 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -41,12 +41,11 @@ {version = 1 :: integer(), window = none :: none | wx:wx_object(), tasks = [] :: [#ui{}], - picked = 0 :: non_neg_integer(), + selected = 0 :: non_neg_integer(), wallet = none :: none | #wallet{}, pass = none :: none | binary(), prefs = #{} :: #{module() := term()}, - wallets = [] :: [#wr{}], - in_flight = [] :: [#tx{}]}). + wallets = [] :: [#wr{}]}). -type state() :: #s{}. @@ -897,37 +896,34 @@ do_mnemonic(ID, #s{wallet = #wallet{keys = Keys}}) -> end. -do_deploy(Build, InitArgs, #s{selected = Index, wallet = #wallet{keys = Keys}}) -> - #key{pair = #s{public = PubKey, secret := SecKey}} = lists:nth(Index, Keys), +do_deploy(Build, + InitArgs, + #s{selected = Index, wallet = #wallet{keys = Keys, chain_id = ChainID}}) -> + #key{pair = #{public := PubKey, secret := SecKey}} = lists:nth(Index, Keys), case hz:contract_create_built(PubKey, Build, InitArgs) of - {ok, CreateTX} -> do_deploy2(SecKey, CreateTX); + {ok, CreateTX} -> do_deploy2(SecKey, CreateTX, ChainID); Error -> Error end. -do_deploy2(SecKey, CreateTX) -> - case gmc_con:sign(Request) of - {ok, SignedTX} -> do_deploy3(SignedTX); - Error -> Error - end. - -do_deploy3(SignedTX) -> +do_deploy2(SecKey, CreateTX, ChainID) -> + SignedTX = sign_tx_hash(CreateTX, SecKey, ChainID), case hz:post_tx(SignedTX) of {ok, Data = #{"tx_hash" := TXHash}} -> ok = tell("Contract deploy TX succeded with: ~p", [TXHash]), - do_deploy4(Data); + do_deploy3(Data); {ok, WTF} -> {error, WTF}; Error -> Error end. -do_deploy4(#{"tx_hash" := TXHash}) -> - case hz:tx_info(TZHash) of - {ok, {"call_info" := #{"return_type" := "ok", "contract_id" := ConID}}} -> +do_deploy3(#{"tx_hash" := TXHash}) -> + case hz:tx_info(TXHash) of + {ok, #{"call_info" := #{"return_type" := "ok", "contract_id" := ConID}}} -> {contract_id, ConID}; {error, "Tx not mined"} -> {tx_hash, TXHash}; - {ok, Reason = {"call_info" := #{"return_type" := "revert"}}} -> + {ok, Reason = #{"call_info" := #{"return_type" := "revert"}}} -> {error, Reason}; Error -> Error @@ -1179,7 +1175,8 @@ read2(Bin) -> read3(T) -> case element(2, T) of - 1 -> {ok, T}; + 1 -> read3(setelement(2, erlang:append_element(T, #{}), 2)); + 2 -> {ok, T}; _ -> {error, bad_wallet} end. diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl index 100b904..06b0216 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -292,7 +292,7 @@ clicked(State = #s{book = {Notebook, Pages}}, Name, Button) -> clicked(State, #p{instances = Is, funs = {_, Funs}, builds = Builds}, {<<"init">>, call}, - Button) -> + _Button) -> Label = wxChoice:getStringSelection(Is), Build = maps:get(Label, Builds), #f{args = Args} = lists:keyfind(<<"init">>, #f.name, Funs), @@ -300,14 +300,22 @@ clicked(State, ok = tell("Label: ~p~nArgs: ~p~nInitArgs: ~p", [Label, Args, InitArgs]), case gmc_con:deploy(Build, InitArgs) of {contract_id, ConID} -> + ok = tell("Got ConID: ~p", [ConID]), + State; {tx_hash, TX_Hash} -> + ok = tell("Got TX_Hash: ~p", [TX_Hash]), + State; {error, Reason} -> - ok = tell("Deploy failed with: ~p", [Reason]) + ok = tell("Deploy failed with: ~p", [Reason]), State -clicked(State, Page, Name) -> - ok = tell("Button: ~p~nPage: ~p", [Name, Page]), + end; +clicked(State, Page, Name, Button) -> + ok = tell("Button: ~p ~p~nPage: ~p", [Button, Name, Page]), State. +get_arg({_, TextCtrl, _}) -> + wxTextCtrl:getValue(TextCtrl). + new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = @@ -409,7 +417,7 @@ find_main([], M, Is) -> fun_interfaces(Window, Buttons, {OldScrollWin, OldIfaces}, - {#{name := Name, functions := Funs}, ConIfaces}, + {#{name := Name, functions := Funs}, _ConIfaces}, J) -> ok = wxScrolledWindow:destroy(OldScrollWin), OldButtonIDs = button_key_list(OldIfaces), -- 2.30.2 From 82c23bbfe6224a7ab1d96767fb7afacf884d2d02 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Fri, 31 Jan 2025 19:14:59 +0900 Subject: [PATCH 04/10] SSL fix. Whoops! --- ebin/clutch.app | 4 ++-- src/clutch.erl | 3 +-- src/gmc_con.erl | 2 +- src/gmc_grids.erl | 2 +- src/gmc_gui.erl | 2 +- src/gmc_jt.erl | 2 +- src/gmc_key_master.erl | 2 +- src/gmc_sup.erl | 2 +- src/gmc_v.erl | 2 +- src/gmc_v_devman.erl | 2 +- src/gmc_v_netman.erl | 2 +- src/gmc_v_wallman.erl | 2 +- zomp.meta | 2 +- 13 files changed, 14 insertions(+), 15 deletions(-) diff --git a/ebin/clutch.app b/ebin/clutch.app index cd50d62..4949e2a 100644 --- a/ebin/clutch.app +++ b/ebin/clutch.app @@ -2,8 +2,8 @@ [{description,"A desktop client for the Gajumaru network of blockchain networks"}, {registered,[]}, {included_applications,[]}, - {applications,[stdlib,kernel]}, - {vsn,"0.1.4"}, + {applications,[stdlib,kernel,sasl,ssl]}, + {vsn,"0.1.5"}, {modules,[clutch,gmc_con,gmc_grids,gmc_gui,gmc_jt, gmc_key_master,gmc_sup,gmc_v,gmc_v_netman, gmc_v_wallman]}, diff --git a/src/clutch.erl b/src/clutch.erl index 01bf0a8..5d5f408 100644 --- a/src/clutch.erl +++ b/src/clutch.erl @@ -3,7 +3,7 @@ %%% @end -module(clutch). --vsn("0.1.4"). +-vsn("0.1.5"). -behavior(application). -author("Craig Everett "). -copyright("QPQ AG "). @@ -57,7 +57,6 @@ start(normal, _Args) -> ok = tell(error, "DANGER! This node is in distributed mode!"), init:stop(1) end, - ok = application:ensure_started(sasl), ok = application:ensure_started(hakuzaru), ok = application:ensure_started(zxwidgets), gmc_sup:start_link(). diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 275eabf..139ad2b 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -3,7 +3,7 @@ %%% @end -module(gmc_con). --vsn("0.1.4"). +-vsn("0.1.5"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_grids.erl b/src/gmc_grids.erl index db63290..08d31a1 100644 --- a/src/gmc_grids.erl +++ b/src/gmc_grids.erl @@ -37,7 +37,7 @@ %%% @end -module(gmc_grids). --vsn("0.1.4"). +-vsn("0.1.5"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 384f49c..f9e1a0a 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -3,7 +3,7 @@ %%% @end -module(gmc_gui). --vsn("0.1.4"). +-vsn("0.1.5"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_jt.erl b/src/gmc_jt.erl index 510f277..f85c24a 100644 --- a/src/gmc_jt.erl +++ b/src/gmc_jt.erl @@ -15,7 +15,7 @@ %%% translation library is retained). -module(gmc_jt). --vsn("0.1.4"). +-vsn("0.1.5"). -export([read_translations/1, j/2, oneshot_j/2]). diff --git a/src/gmc_key_master.erl b/src/gmc_key_master.erl index 1f92aef..003a5fc 100644 --- a/src/gmc_key_master.erl +++ b/src/gmc_key_master.erl @@ -8,7 +8,7 @@ %%% @end -module(gmc_key_master). --vsn("0.1.4"). +-vsn("0.1.5"). -export([make_key/2, encode/1, decode/1]). diff --git a/src/gmc_sup.erl b/src/gmc_sup.erl index d48dc72..1aec97c 100644 --- a/src/gmc_sup.erl +++ b/src/gmc_sup.erl @@ -12,7 +12,7 @@ %%% @end -module(gmc_sup). --vsn("0.1.4"). +-vsn("0.1.5"). -behaviour(supervisor). -author("Craig Everett "). -copyright("QPQ AG "). diff --git a/src/gmc_v.erl b/src/gmc_v.erl index 31c0457..4f024b4 100644 --- a/src/gmc_v.erl +++ b/src/gmc_v.erl @@ -1,5 +1,5 @@ -module(gmc_v). --vsn("0.1.4"). +-vsn("0.1.5"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl index 06b0216..f726286 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -1,5 +1,5 @@ -module(gmc_v_devman). --vsn("0.1.4"). +-vsn("0.1.5"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_v_netman.erl b/src/gmc_v_netman.erl index d94c1bf..b76176c 100644 --- a/src/gmc_v_netman.erl +++ b/src/gmc_v_netman.erl @@ -1,5 +1,5 @@ -module(gmc_v_netman). --vsn("0.1.4"). +-vsn("0.1.5"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_v_wallman.erl b/src/gmc_v_wallman.erl index 214ac8e..92da42c 100644 --- a/src/gmc_v_wallman.erl +++ b/src/gmc_v_wallman.erl @@ -1,5 +1,5 @@ -module(gmc_v_wallman). --vsn("0.1.4"). +-vsn("0.1.5"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/zomp.meta b/zomp.meta index d22e4e5..94a4b39 100644 --- a/zomp.meta +++ b/zomp.meta @@ -4,7 +4,7 @@ {prefix,"gmc"}. {desc,"A desktop client for the Gajumaru network of blockchain networks"}. {author,"Craig Everett"}. -{package_id,{"otpr","clutch",{0,1,4}}}. +{package_id,{"otpr","clutch",{0,1,5}}}. {deps,[{"otpr","hakuzaru",{0,2,0}}, {"otpr","aesophia",{8,0,1}}, {"otpr","aeserialization",{0,1,2}}, -- 2.30.2 From 5cdb770d236e7a126c6fa1fecdb387270a98e4aa Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sat, 1 Feb 2025 13:24:26 +0900 Subject: [PATCH 05/10] Tiny fixes - close_wallet bad match (derp) - don't crash on key creation with no wallet selected --- src/gmc_con.erl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 139ad2b..e2e2d6f 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -347,9 +347,9 @@ handle_cast({open_wallet, Path, Phrase}, State) -> NewState = do_open_wallet(Path, Phrase, State), {noreply, NewState}; handle_cast(close_wallet, State) -> - NewState = do_close_wallet(State), + NextState = do_close_wallet(State), ok = gmc_gui:show([]), - ok = do_show_ui(gmc_v_wallman, NewState), + NewState = do_show_ui(gmc_v_wallman, NextState), {noreply, NewState}; handle_cast({new_wallet, Name, Path, Password}, State) -> NewState = do_new_wallet(Name, Path, Password, State), @@ -816,6 +816,9 @@ do_make_key(Name, Seed, base58, Transform, State) -> end. +do_make_key2(_, _, _, State = #s{wallet = none}) -> + ok = gmc_gui:trouble("No wallet selected!"), + do_show_ui(gmc_v_wallman, State); do_make_key2(Name, Bin, Transform, State = #s{wallet = Current, wallets = Wallets, pass = Pass}) -> #wallet{name = WalletName, poas = POAs, keys = Keys} = Current, -- 2.30.2 From 99545e35a56b12a728309b33ad5731908051d4f5 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sat, 1 Feb 2025 13:26:27 +0900 Subject: [PATCH 06/10] Verup before I forget --- ebin/clutch.app | 2 +- src/clutch.erl | 2 +- src/gmc_con.erl | 2 +- src/gmc_grids.erl | 2 +- src/gmc_gui.erl | 2 +- src/gmc_jt.erl | 2 +- src/gmc_key_master.erl | 2 +- src/gmc_sup.erl | 2 +- src/gmc_v.erl | 2 +- src/gmc_v_devman.erl | 2 +- src/gmc_v_netman.erl | 2 +- src/gmc_v_wallman.erl | 2 +- zomp.meta | 4 ++-- 13 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ebin/clutch.app b/ebin/clutch.app index 4949e2a..f6d168b 100644 --- a/ebin/clutch.app +++ b/ebin/clutch.app @@ -3,7 +3,7 @@ {registered,[]}, {included_applications,[]}, {applications,[stdlib,kernel,sasl,ssl]}, - {vsn,"0.1.5"}, + {vsn,"0.2.0"}, {modules,[clutch,gmc_con,gmc_grids,gmc_gui,gmc_jt, gmc_key_master,gmc_sup,gmc_v,gmc_v_netman, gmc_v_wallman]}, diff --git a/src/clutch.erl b/src/clutch.erl index 5d5f408..ead5b07 100644 --- a/src/clutch.erl +++ b/src/clutch.erl @@ -3,7 +3,7 @@ %%% @end -module(clutch). --vsn("0.1.5"). +-vsn("0.2.0"). -behavior(application). -author("Craig Everett "). -copyright("QPQ AG "). diff --git a/src/gmc_con.erl b/src/gmc_con.erl index e2e2d6f..26af293 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -3,7 +3,7 @@ %%% @end -module(gmc_con). --vsn("0.1.5"). +-vsn("0.2.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_grids.erl b/src/gmc_grids.erl index 08d31a1..2f58d7c 100644 --- a/src/gmc_grids.erl +++ b/src/gmc_grids.erl @@ -37,7 +37,7 @@ %%% @end -module(gmc_grids). --vsn("0.1.5"). +-vsn("0.2.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index f9e1a0a..8c92f86 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -3,7 +3,7 @@ %%% @end -module(gmc_gui). --vsn("0.1.5"). +-vsn("0.2.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_jt.erl b/src/gmc_jt.erl index f85c24a..f58cc76 100644 --- a/src/gmc_jt.erl +++ b/src/gmc_jt.erl @@ -15,7 +15,7 @@ %%% translation library is retained). -module(gmc_jt). --vsn("0.1.5"). +-vsn("0.2.0"). -export([read_translations/1, j/2, oneshot_j/2]). diff --git a/src/gmc_key_master.erl b/src/gmc_key_master.erl index 003a5fc..f32a4bb 100644 --- a/src/gmc_key_master.erl +++ b/src/gmc_key_master.erl @@ -8,7 +8,7 @@ %%% @end -module(gmc_key_master). --vsn("0.1.5"). +-vsn("0.2.0"). -export([make_key/2, encode/1, decode/1]). diff --git a/src/gmc_sup.erl b/src/gmc_sup.erl index 1aec97c..9e541cd 100644 --- a/src/gmc_sup.erl +++ b/src/gmc_sup.erl @@ -12,7 +12,7 @@ %%% @end -module(gmc_sup). --vsn("0.1.5"). +-vsn("0.2.0"). -behaviour(supervisor). -author("Craig Everett "). -copyright("QPQ AG "). diff --git a/src/gmc_v.erl b/src/gmc_v.erl index 4f024b4..9065f12 100644 --- a/src/gmc_v.erl +++ b/src/gmc_v.erl @@ -1,5 +1,5 @@ -module(gmc_v). --vsn("0.1.5"). +-vsn("0.2.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl index f726286..38439fa 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -1,5 +1,5 @@ -module(gmc_v_devman). --vsn("0.1.5"). +-vsn("0.2.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_v_netman.erl b/src/gmc_v_netman.erl index b76176c..4b0e662 100644 --- a/src/gmc_v_netman.erl +++ b/src/gmc_v_netman.erl @@ -1,5 +1,5 @@ -module(gmc_v_netman). --vsn("0.1.5"). +-vsn("0.2.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_v_wallman.erl b/src/gmc_v_wallman.erl index 92da42c..0ad83e2 100644 --- a/src/gmc_v_wallman.erl +++ b/src/gmc_v_wallman.erl @@ -1,5 +1,5 @@ -module(gmc_v_wallman). --vsn("0.1.5"). +-vsn("0.2.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/zomp.meta b/zomp.meta index 94a4b39..254046e 100644 --- a/zomp.meta +++ b/zomp.meta @@ -2,9 +2,9 @@ {type,gui}. {modules,[]}. {prefix,"gmc"}. -{desc,"A desktop client for the Gajumaru network of blockchain networks"}. {author,"Craig Everett"}. -{package_id,{"otpr","clutch",{0,1,5}}}. +{desc,"A desktop client for the Gajumaru network of blockchain networks"}. +{package_id,{"otpr","clutch",{0,2,0}}}. {deps,[{"otpr","hakuzaru",{0,2,0}}, {"otpr","aesophia",{8,0,1}}, {"otpr","aeserialization",{0,1,2}}, -- 2.30.2 From 0b02d6dd2985edab3379332ae1ff67ab3aa89aeb Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sun, 23 Feb 2025 23:38:54 +0900 Subject: [PATCH 07/10] WIP: Need to add back the actual calls. --- src/gmc_con.erl | 2 +- src/gmc_gui.erl | 16 +- src/gmc_v_devman.erl | 781 ++++++++++++++++++++++++++++++++---------- src/gmc_v_wallman.erl | 4 +- zomp.meta | 5 +- 5 files changed, 613 insertions(+), 195 deletions(-) diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 26af293..1753774 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -1,5 +1,5 @@ %%% @doc -%%% Clutch Controller +%%% GajuDesk Controller %%% @end -module(gmc_con). diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 8c92f86..7e2ba22 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -1,5 +1,5 @@ %%% @doc -%%% Clutch GUI +%%% GajuDesk GUI %%% @end -module(gmc_gui). @@ -58,8 +58,8 @@ chain(ChainID, Node) -> wx_object:cast(?MODULE, {chain, ChainID, Node}). -trouble(Message) -> - wx_object:cast(?MODULE, {trouble, Message}). +trouble(Info) -> + wx_object:cast(?MODULE, {trouble, Info}). ask_password() -> @@ -235,7 +235,7 @@ handle_cast({chain, ChainID, Node}, State) -> ok = do_chain(ChainID, Node, State), {noreply, State}; handle_cast({trouble, Info}, State) -> - ok = handle_trouble(Info, State), + ok = handle_troubling(State, Info), {noreply, State}; handle_cast(password, State) -> ok = do_ask_password(State), @@ -317,6 +317,10 @@ handle_event(Event, State) -> {noreply, State}. +handle_troubling(#s{frame = Frame}, Info) -> + zxw:show_message(Frame, Info). + + code_change(_, State, _) -> {ok, State}. @@ -922,10 +926,6 @@ do_chain(ChainID, #node{ip = IP}, #s{buttons = Buttons}) -> ok = wxButton:setLabel(NodeB, Address). -handle_trouble(Info, #s{frame = Frame}) -> - zxw:show_message(Frame, Info). - - do_ask_password(#s{frame = Frame, prefs = Prefs, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Password")), Sizer = wxBoxSizer:new(?wxVERTICAL), diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl index 38439fa..30ed377 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -1,6 +1,6 @@ -module(gmc_v_devman). -vsn("0.2.0"). --author("Craig Everett "). +-author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). @@ -8,35 +8,41 @@ %-behavior(gmc_v). -include_lib("wx/include/wx.hrl"). -export([to_front/1]). --export([set_manifest/1]). +-export([set_manifest/1, trouble/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("gmc.hrl"). - +% Widgets -record(w, {name = none :: atom() | {FunName :: binary(), call | dryr}, id = 0 :: integer(), wx = none :: none | wx:wx_object()}). +% Contract functions in an ACI -record(f, {name = <<"">> :: binary(), call = #w{} :: #w{}, dryrun = #w{} :: none | #w{}, args = [] :: [{wx:wx_object(), wx:wx_object(), argt()}]}). +% Code book pages -record(p, - {path = "" :: file:filename(), + {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, + win = none :: none | wx:wx_object(), + code = none :: none | wxStyledTextCtrl:wxStyledTextCtrl()}). + +% Contract pages +-record(c, + {id = "" :: string(), win = none :: none | wx:wx_object(), code = none :: none | wxStyledTextCtrl:wxStyledTextCtrl(), cons = none :: none | wxStyledTextCtrl:wxStyledTextCtrl(), - side_sz = none :: none | wx:wx_object(), - instances = none :: none | wx:wx_object(), - funs = {#w{}, []} :: {#w{}, [#f{}]}, - builds = #{} :: #{InstanceID :: binary() := Build :: map()}}). + funs = {#w{}, []} :: {#w{}, [#f{}]}}). +% State -record(s, {wx = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(), @@ -44,7 +50,9 @@ j = none :: none | fun(), prefs = #{} :: map(), buttons = #{} :: #{WX_ID :: integer() := #w{}}, - book = {none, []} :: {Notebook :: none | wx:wx_object(), Pages :: [#p{}]}}). + tabs = none :: none | wx:wx_object(), + code = {none, []} :: {Codebook :: none | wx:wx_object(), Pages :: [#p{}]}, + cons = {none, []} :: {Consbook :: none | wx:wx_object(), Pages :: [#c{}]}}). -type argt() :: int | string | address | list(argt()). @@ -67,6 +75,13 @@ set_manifest(Entries) -> end. +-spec trouble(Info) -> ok + when Info :: term(). + +trouble(Info) -> + wx_object:cast(?MODULE, {trouble, Info}). + + %%% Startup Functions @@ -83,133 +98,84 @@ init({Prefs, Manifest}) -> Frame = wxFrame:new(Wx, ?wxID_ANY, J("Contracts")), MainSz = wxBoxSizer:new(?wxVERTICAL), - ButtSz = wxBoxSizer:new(?wxHORIZONTAL), - - ButtonTemplates = - [{new, J("New")}, - {open, J("Open")}, - {save, J("Save")}, - {saven, J("Save (new name)")}, - {compile, J("Compile")}, - {close, J("Close")}], - - MakeButton = - fun({Name, Label}) -> - B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]), - #w{name = Name, id = wxButton:getId(B), wx = B} - end, - - Buttons = lists:map(MakeButton, ButtonTemplates), - - AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, - - Notebook = wxNotebook:new(Frame, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), - - ok = lists:foreach(AddButton, Buttons), - _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), - _ = wxSizer:add(MainSz, Notebook, zxw:flags(wide)), + TopBook = wxNotebook:new(Frame, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + {LWin, LButtons, Codebook} = make_l_win(TopBook, J), + {RWin, RButtons, Consbook} = make_r_win(TopBook, J), + ButtonMap = maps:merge(LButtons, RButtons), + true = wxNotebook:addPage(TopBook, LWin, J("Contract Editor"), []), + true = wxNotebook:addPage(TopBook, RWin, J("Deployed Contracts"), []), + State = + #s{wx = Wx, frame = Frame, + j = J, prefs = Prefs, + buttons = ButtonMap, tabs = TopBook, + code = {Codebook, []}, cons = {Consbook, []}}, + _ = wxSizer:add(MainSz, TopBook, zxw:flags(wide)), _ = wxFrame:setSizer(Frame, MainSz), _ = wxSizer:layout(MainSz), - ok = gmc_v:safe_size(Frame, Prefs), - ok = wxFrame:connect(Frame, close_window), ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:center(Frame), true = wxFrame:show(Frame), - - MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, - ButtonMap = lists:foldl(MapButton, #{}, Buttons), - State = - #s{wx = Wx, frame = Frame, - j = J, prefs = Prefs, - buttons = ButtonMap, book = {Notebook, []}}, - NewState = add_pages(State, Manifest), + NewState = add_code_pages(State, Manifest), {Frame, NewState}. -add_pages(State, Files) -> - lists:foldl(fun add_page/2, State, Files). +make_l_win(TopBook, J) -> + Win = wxWindow:new(TopBook, ?wxID_ANY), + MainSz = wxBoxSizer:new(?wxVERTICAL), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + ButtonTemplates = + [{new, J("New")}, + {open, J("Open")}, + {save, J("Save")}, + {rename, J("Save (rename)")}, + {deploy, J("Deploy")}, + {close_source, J("Close")}], + MakeButton = + fun({Name, Label}) -> + B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), + #w{name = Name, id = wxButton:getId(B), wx = B} + end, + Buttons = lists:map(MakeButton, ButtonTemplates), + AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, + Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + ok = lists:foreach(AddButton, Buttons), + _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), + _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), + _ = wxWindow:setSizer(Win, MainSz), + MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, + ButtonMap = lists:foldl(MapButton, #{}, Buttons), + {Win, ButtonMap, Codebook}. -add_page(State = #s{book = {Notebook, Pages}}, File) -> - case keyfind_index(File, #p.path, Pages) of - error -> - add_page2(State, File); - {ok, Index} -> - _ = wxNotebook:setSelection(Notebook, Index - 1), - State - end. +make_r_win(TopBook, J) -> + Win = wxWindow:new(TopBook, ?wxID_ANY), + MainSz = wxBoxSizer:new(?wxVERTICAL), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + ButtonTemplates = + [{load, J("Load from Chain")}, + {edit, J("Copy to Editor")}, + {close_instance, J("Close")}], + MakeButton = + fun({Name, Label}) -> + B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), + #w{name = Name, id = wxButton:getId(B), wx = B} + end, + Buttons = lists:map(MakeButton, ButtonTemplates), + AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, + Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + ok = lists:foreach(AddButton, Buttons), + _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), + _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), + _ = wxWindow:setSizer(Win, MainSz), + MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, + ButtonMap = lists:foldl(MapButton, #{}, Buttons), + {Win, ButtonMap, Codebook}. -add_page2(State = #s{j = J}, File) -> - case file:read_file(File) of - {ok, Bin} -> - case unicode:characters_to_list(Bin) of - Code when is_list(Code) -> - add_page(State, File, Code); - Error -> - Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Error]), - ok = gmc_gui:trouble(Message), - State - end; - {error, Reason} -> - Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Reason]), - ok = gmc_gui:trouble(Message), - State - end. -add_page(State = #s{j = J, book = {Notebook, Pages}}, File, Code) -> -% FIXME: One of these days we need to define the text area as a wxStyledTextCtrl and will -% have to contend with system theme issues (light/dark themese, namely) -% Leaving this little thing here to remind myself how any of that works later. -% The call below returns a wx_color4() type (not that we need alpha...). -% Color = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW), -% tell("Color: ~p", [Color]), - Window = wxWindow:new(Notebook, ?wxID_ANY), - PageSz = wxBoxSizer:new(?wxHORIZONTAL), - ProgSz = wxBoxSizer:new(?wxVERTICAL), - CodeSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Code")}]), - ConsSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Console")}]), - CodeTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_PROCESS_TAB bor ?wxTE_DONTWRAP}, - CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), - ConsTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}, - ConsTx = wxTextCtrl:new(Window, ?wxID_ANY, [ConsTxStyle]), - Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), - TextAt = wxTextAttr:new(), - ok = wxTextAttr:setFont(TextAt, Mono), - true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), - ok = wxTextCtrl:setValue(CodeTx, Code), - - _ = wxSizer:add(CodeSz, CodeTx, zxw:flags(wide)), - _ = wxSizer:add(ConsSz, ConsTx, zxw:flags(wide)), - _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 5}, {flag, ?wxEXPAND}]), - _ = wxSizer:add(ProgSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND}]), - - SideSz = wxBoxSizer:new(?wxVERTICAL), - - InstanceSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Instances")}]), - Instances = wxChoice:new(Window, ?wxID_ANY, [{choices, []}]), - _ = wxSizer:add(InstanceSz, Instances, zxw:flags(wide)), - - ScrollWin = wxScrolledWindow:new(Window), - FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Function Interfaces")}]), - ok = wxWindow:setSizer(ScrollWin, FunSz), - _ = wxSizer:add(SideSz, InstanceSz, zxw:flags(base)), - _ = wxSizer:add(SideSz, ScrollWin, zxw:flags(wide)), - - _ = wxSizer:add(PageSz, ProgSz, [{proportion, 2}, {flag, ?wxEXPAND}]), - _ = wxSizer:add(PageSz, SideSz, [{proportion, 1}, {flag, ?wxEXPAND}]), - - ok = wxWindow:setSizer(Window, PageSz), - ok = wxSizer:layout(PageSz), - FileName = filename:basename(File), - true = wxNotebook:addPage(Notebook, Window, FileName, [{bSelect, true}]), - Page = #p{path = File, win = Window, - code = CodeTx, cons = ConsTx, - side_sz = SideSz, instances = Instances, - funs = {ScrollWin, []}}, - NewPages = Pages ++ [Page], - State#s{book = {Notebook, NewPages}}. +add_code_pages(State, Files) -> + lists:foldl(fun add_code_page/2, State, Files). @@ -223,6 +189,9 @@ handle_call(Unexpected, From, State) -> handle_cast(to_front, State = #s{frame = Frame}) -> ok = wxFrame:raise(Frame), {noreply, State}; +handle_cast({trouble, Info}, State) -> + ok = handle_troubling(State, Info), + {noreply, State}; handle_cast(Unexpected, State) -> ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. @@ -239,9 +208,14 @@ handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, NewState = case maps:get(ID, Buttons, undefined) of #w{name = new} -> new_file(State); - #w{name = open} -> open_file(State); - #w{name = close} -> close_file(State); - #w{name = compile} -> compile(State); + #w{name = open} -> open(State); + #w{name = save} -> save(State); + #w{name = rename} -> rename(State); + #w{name = deploy} -> deploy(State); + #w{name = close_source} -> close_source(State); + #w{name = load} -> load(State); + #w{name = edit} -> edit(State); + #w{name = close_instance} -> close_instance(State); #w{name = Name, wx = Button} -> clicked(State, Name, Button); undefined -> tell("Received message: ~w", [E]), @@ -267,6 +241,10 @@ handle_event(Event, State) -> {noreply, State}. +handle_troubling(#s{frame = Frame}, Info) -> + zxw:show_message(Frame, Info). + + code_change(_, State, _) -> {ok, State}. @@ -279,44 +257,86 @@ terminate(Reason, State) -> %%% Doers -clicked(State = #s{book = {Notebook, Pages}}, Name, Button) -> - case wxNotebook:getSelection(Notebook) of +clicked(State = #s{cons = {Consbook, Contracts}}, Name, Button) -> + case wxNotebook:getSelection(Consbook) of ?wxNOT_FOUND -> ok = tell(warning, "Inconcievable! No notebook page is selected!"), State; Index -> - Page = lists:nth(Index + 1, Pages), - clicked(State, Page, Name, Button) + Contract = lists:nth(Index + 1, Contracts), + clicked(State, Contract, Name, Button) end. -clicked(State, - #p{instances = Is, funs = {_, Funs}, builds = Builds}, - {<<"init">>, call}, - _Button) -> - Label = wxChoice:getStringSelection(Is), - Build = maps:get(Label, Builds), - #f{args = Args} = lists:keyfind(<<"init">>, #f.name, Funs), - InitArgs = lists:map(fun get_arg/1, Args), - ok = tell("Label: ~p~nArgs: ~p~nInitArgs: ~p", [Label, Args, InitArgs]), - case gmc_con:deploy(Build, InitArgs) of - {contract_id, ConID} -> - ok = tell("Got ConID: ~p", [ConID]), - State; - {tx_hash, TX_Hash} -> - ok = tell("Got TX_Hash: ~p", [TX_Hash]), - State; - {error, Reason} -> - ok = tell("Deploy failed with: ~p", [Reason]), - State - end; -clicked(State, Page, Name, Button) -> - ok = tell("Button: ~p ~p~nPage: ~p", [Button, Name, Page]), +clicked(State, Contract, Name, Button) -> + ok = tell("Button: ~p ~p~nContract: ~p", [Button, Name, Contract]), State. get_arg({_, TextCtrl, _}) -> wxTextCtrl:getValue(TextCtrl). +add_code_page(State = #s{code = {Codebook, Pages}}, File) -> + case keyfind_index({file, File}, #p.path, Pages) of + error -> + add_code_page2(State, File); + {ok, Index} -> + _ = wxNotebook:setSelection(Codebook, Index - 1), + State + end. + +add_code_page2(State = #s{j = J}, {file, File}) -> + case file:read_file(File) of + {ok, Bin} -> + case unicode:characters_to_list(Bin) of + Code when is_list(Code) -> + add_code_page(State, {file, File}, Code); + Error -> + Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Error]), + ok = handle_troubling(Message, State), + State + end; + {error, Reason} -> + Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Reason]), + ok = handle_troubling(Message, State), + State + end; +add_code_page2(State, {hash, Address}) -> + open_hash2(State, Address). + +add_code_page(State = #s{j = J, tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> +% FIXME: One of these days we need to define the text area as a wxStyledTextCtrl and will +% have to contend with system theme issues (light/dark themese, namely) +% Leaving this little thing here to remind myself how any of that works later. +% The call below returns a wx_color4() type (not that we need alpha...). +% Color = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW), +% tell("Color: ~p", [Color]), + Window = wxWindow:new(Codebook, ?wxID_ANY), + PageSz = wxBoxSizer:new(?wxHORIZONTAL), + + CodeTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_PROCESS_TAB bor ?wxTE_DONTWRAP}, + CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), + TextAt = wxTextAttr:new(), + Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), + ok = wxTextAttr:setFont(TextAt, Mono), + true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), + ok = wxTextCtrl:setValue(CodeTx, Code), + + _ = wxSizer:add(PageSz, CodeTx, zxw:flags(wide)), + + ok = wxWindow:setSizer(Window, PageSz), + ok = wxSizer:layout(PageSz), + _ = wxNotebook:changeSelection(TopBook, 0), + FileName = + case Location of + {file, Path} -> filename:basename(Path); + {hash, Addr} -> Addr + end, + true = wxNotebook:addPage(Codebook, Window, FileName, [{bSelect, true}]), + Page = #p{path = Location, win = Window, code = CodeTx}, + NewPages = Pages ++ [Page], + State#s{code = {Codebook, NewPages}}. + + new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = case maps:find(dir, Prefs) of @@ -351,7 +371,7 @@ new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> Path = filename:join(Dir, File), NewPrefs = maps:put(dir, Dir, Prefs), NextState = State#s{prefs = NewPrefs}, - add_page(NextState, Path, "") + add_code_page(NextState, {file, Path}, "") end; ?wxID_CANCEL -> State @@ -360,44 +380,74 @@ new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> NewState. -compile(State = #s{book = {Notebook, Pages}}) -> - case wxNotebook:getSelection(Notebook) of +deploy(State = #s{code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of ?wxNOT_FOUND -> State; Index -> Page = #p{code = CodeTx} = lists:nth(Index + 1, Pages), Source = wxTextCtrl:getValue(CodeTx), - compile(State, Page, Source) + deploy2(State, Source) end. -compile(State = #s{book = {Notebook, Pages}, j = J, buttons = Buttons}, - Page = #p{path = Path, win = Win, cons = ConsTx, - side_sz = SideSz, instances = Instances, funs = Funs, builds = Builds}, - Source) -> +deploy2(State, Source) -> case aeso_compiler:from_string(Source, [{aci, json}]) of {ok, Build = #{aci := ACI}} -> - BuildName = build_name(Path), - I = wxChoice:append(Instances, BuildName), - ok = wxChoice:select(Instances, I), - NewBuilds = maps:put(BuildName, Build, Builds), - FunDefs = find_main(ACI), - UpdateInterfaces = fun() -> fun_interfaces(Win, Buttons, Funs, FunDefs, J) end, - {NewButtons, NewFuns} = wx:batch(UpdateInterfaces), - ScrollWin = element(1, NewFuns), - _ = wxSizer:add(SideSz, ScrollWin, zxw:flags(wide)), - ok = wxSizer:layout(SideSz), - NewPage = Page#p{funs = NewFuns, builds = NewBuilds}, - NewPages = lists:keystore(Path, #p.path, Pages, NewPage), - State#s{book = {Notebook, NewPages}, buttons = NewButtons}; + FunDefs = {#{functions := Funs}, _} = find_main(ACI), + Init = lom:find(name, <<"init">>, Funs), + ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), + deploy3(State, Init); Other -> - ok = wxTextCtrl:setValue(ConsTx, io_lib:format("~tp", [Other])), + ok = tell(info, "Compilation Failed!~n~tp", [Other]), State end. -build_name(Path) -> - File = filename:basename(Path, ".aes"), - TS = integer_to_list(os:system_time(millisecond)), - unicode:characters_to_list([File, "-", TS]). +deploy3(State = #s{frame = Frame, j = J}, #{arguments := As}) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + ScrollWin = wxScrolledWindow:new(Dialog), + FunName = unicode:characters_to_list(["init/", integer_to_list(length(As))]), + FunSizer = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), + ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSizer), + ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), + ButtSz = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL), + GridSz = wxFlexGridSizer:new(2, 4, 4), + ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), + ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), + MakeArgField = + fun(#{name := AN, type := T}) -> + Type = + case T of + <<"address">> -> address; + <<"int">> -> integer; + <<"bool">> -> boolean; + L when is_list(L) -> list; % FIXME +% I when is_binary(I) -> iface % FIXME + I when is_binary(I) -> address % FIXME + end, + ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), + TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), + _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), + _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), + {ANT, TCT, Type} + end, + ArgFields = lists:map(MakeArgField, As), + _ = wxStaticBoxSizer:add(FunSizer, GridSz, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ScrollWin, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:setSize(Dialog, {500, 300}), + ok = wxDialog:center(Dialog), + ok = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + tell(info, "DEPLOYING!"); + ?wxID_CANCEL -> + ok + end, + ok = wxDialog:destroy(Dialog), + State. find_main(ACI) -> find_main(ACI, none, []). @@ -495,6 +545,51 @@ map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = none}, A) -> maps:put(CID, C, A). +open(State = #s{frame = Frame, j = J}) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Open Contract Source")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + Choices = wxRadioBox:new(Dialog, + ?wxID_ANY, + J("Select Origin"), + ?wxDefaultPosition, + ?wxDefaultSize, + [J("From File"), J("From Hash")], + [{majorDim, 1}]), + 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, Choices, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxDialog:setSize(Dialog, {250, 170}), + ok = wxBoxSizer:layout(Sizer), + Choice = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + case wxRadioBox:getSelection(Choices) of + 0 -> file; + 1 -> hash; + ?wxNOT_FOUND -> none + end; + ?wxID_CANCEL -> cancel + end, + ok = wxDialog:destroy(Dialog), + case Choice of + file -> + open_file(State); + hash -> + open_hash(State); + none -> + ok = tell(info, "No selection."), + State; + cancel -> + ok = tell(info, "Cancelled."), + State + end. + + open_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = case maps:find(dir, Prefs) of @@ -528,7 +623,7 @@ open_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> Path = filename:join(Dir, File), NewPrefs = maps:put(dir, Dir, Prefs), NextState = State#s{prefs = NewPrefs}, - add_page(NextState, Path) + add_code_page(NextState, {file, Path}) end; ?wxID_CANCEL -> State @@ -537,17 +632,339 @@ open_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> NewState. -close_file(State = #s{book = {Notebook, Pages}}) -> - case wxNotebook:getSelection(Notebook) of +open_hash(State = #s{frame = Frame, j = J}) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), + AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(AddressSz, AddressTx, 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)), + _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:setSize(Dialog, {500, 200}), + ok = wxDialog:center(Dialog), + ok = wxStyledTextCtrl:setFocus(AddressTx), + Choice = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + case wxTextCtrl:getValue(AddressTx) of + "" -> cancel; + A -> {ok, A} + end; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Choice of + {ok, Address} -> open_hash2(State, Address); + cancel -> State + end. + +open_hash2(State, Address) -> + case hz:contract_source(Address) of + {ok, Source} -> + open_hash3(State, Address, Source); + Error -> + ok = handle_troubling(Error, State), + State + end. + +open_hash3(State, Address, Source) -> + % TODO: Compile on load and verify the deployed hash for validity. + case aeso_compiler:from_string(Source, [{aci, json}]) of + {ok, Build = #{aci := ACI}} -> + {Defs = #{functions := Funs}, ConIfaces} = find_main(ACI), + Callable = lom:delete(name, <<"init">>, Funs), + FunDefs = {maps:put(functions, Callable, Defs), ConIfaces}, + ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), + add_code_page(State, {hash, Address}, Source); + Other -> + ok = tell(info, "Compilation Failed!~n~tp", [Other]), + State + end. + + +% TODO: Break this down -- tons of things in here recur. +save(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + case lists:nth(Index + 1, Pages) of + #p{path = {file, Path}, code = Widget} -> + Source = wxStyledTextCtrl:getText(Widget), + case filelib:ensure_dir(Path) of + ok -> + case file:write_file(Path, Source) of + ok -> + State; + Error -> + ok = handle_troubling(State, Error), + State + end; + Error -> + ok = handle_troubling(State, Error), + State + end; + Page = #p{path = {hash, Hash}, code = Widget} -> + DefaultDir = + case maps:find(dir, Prefs) of + {ok, PrefDir} -> + PrefDir; + error -> + case os:getenv("ZOMP_DIR") of + "" -> file:get_pwd(); + D -> filename:basename(D) + end + end, + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, unicode:characters_to_list([Hash, ".aes"])}, + {wildCard, "*.aes"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + Path = filename:join(Dir, File), + Source = wxTextCtrl:getValue(Widget), + case filelib:ensure_dir(Path) of + ok -> + case file:write_file(Path, Source) of + ok -> + true = wxNotebook:setPageText(Codebook, Index, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NewPage = Page#p{path = {file, Path}}, + NewPages = store_nth(Index + 1, NewPage, Pages), + NewCode = {Codebook, NewPages}, + State#s{prefs = NewPrefs, code = NewCode}; + Error -> + ok = handle_troubling(State, Error), + State + end; + Error -> + ok = handle_troubling(State, Error), + State + end + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState + end + end. + +% TODO: Break this down -- tons of things in here recur. +rename(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + case lists:nth(Index + 1, Pages) of + Page = #p{path = {file, Path}, code = Widget} -> + DefaultDir = filename:dirname(Path), + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, filename:basename(Path)}, + {wildCard, "*.aes"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + NewPath = filename:join(Dir, File), + Source = wxTextCtrl:getValue(Widget), + case filelib:ensure_dir(NewPath) of + ok -> + case file:write_file(NewPath, Source) of + ok -> + true = wxNotebook:setPageText(Codebook, Index, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NewPage = Page#p{path = {file, NewPath}}, + NewPages = store_nth(Index + 1, NewPage, Pages), + NewCode = {Codebook, NewPages}, + State#s{prefs = NewPrefs, code = NewCode}; + Error -> + ok = handle_troubling(State, Error), + State + end; + Error -> + ok = handle_troubling(State, Error), + State + end + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState; + #p{path = {hash, _}} -> + save(State) + end + end. + + +close_source(State = #s{code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of ?wxNOT_FOUND -> State; Index -> NewPages = drop_nth(Index + 1, Pages), - true = wxNotebook:deletePage(Notebook, Index), - State#s{book = {Notebook, NewPages}} + true = wxNotebook:deletePage(Codebook, Index), + State#s{code = {Codebook, NewPages}} end. +load(State = #s{frame = Frame, j = J}) -> + % TODO: Extract the exact compiler version, load it, and use only that or fail if + % the specific version is unavailable. + % TODO: Compile on load and verify the deployed hash for validity. + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), + AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(AddressSz, AddressTx, 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)), + _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:setSize(Dialog, {500, 200}), + ok = wxDialog:center(Dialog), + ok = wxStyledTextCtrl:setFocus(AddressTx), + Choice = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + case wxTextCtrl:getValue(AddressTx) of + "" -> cancel; + A -> {ok, A} + end; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Choice of + {ok, Address} -> load2(State, Address); + cancel -> State + end. + +load2(State, Address) -> + case hz:contract_source(Address) of + {ok, Source} -> + load3(State, Address, Source); + Error -> + ok = handle_troubling(Error, State), + State + end. + +load3(State, Address, Source) -> + % TODO: Compile on load and verify the deployed hash for validity. + case aeso_compiler:from_string(Source, [{aci, json}]) of + {ok, Build = #{aci := ACI}} -> + {Defs = #{functions := Funs}, ConIfaces} = find_main(ACI), + Callable = lom:delete(name, <<"init">>, Funs), + FunDefs = {maps:put(functions, Callable, Defs), ConIfaces}, + ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), + add_instance_page(State, Address, Source); + Other -> + ok = tell(info, "Compilation Failed!~n~tp", [Other]), + State + end. + +add_instance_page(State = #s{cons = {Consbook, Pages}, j = J}, Address, Source) -> + Window = wxWindow:new(Consbook, ?wxID_ANY), + PageSz = wxBoxSizer:new(?wxVERTICAL), + + ProgSz = wxBoxSizer:new(?wxHORIZONTAL), + + CodeSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Contract Source")}]), + CodeTxStyle = {style, ?wxTE_MULTILINE + bor ?wxTE_PROCESS_TAB + bor ?wxTE_DONTWRAP + bor ?wxTE_READONLY}, + CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), + Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), + TextAt = wxTextAttr:new(), + ok = wxTextAttr:setFont(TextAt, Mono), + true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), + ok = wxTextCtrl:setValue(CodeTx, Source), + _ = wxSizer:add(CodeSz, CodeTx, zxw:flags(wide)), + + ScrollWin = wxScrolledWindow:new(Window), + FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Function Interfaces")}]), + ok = wxWindow:setSizer(ScrollWin, FunSz), + + ConsSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Console")}]), + ConsTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}, + ConsTx = wxTextCtrl:new(Window, ?wxID_ANY, [ConsTxStyle]), + _ = wxSizer:add(ConsSz, ConsTx, zxw:flags(wide)), + + _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 3}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(ProgSz, ScrollWin, [{proportion, 1}, {flag, ?wxEXPAND}]), + + _ = wxSizer:add(PageSz, ProgSz, [{proportion, 3}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(PageSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND}]), + + ok = wxWindow:setSizer(Window, PageSz), + ok = wxSizer:layout(PageSz), + true = wxNotebook:addPage(Consbook, Window, Address, [{bSelect, true}]), + Page = #c{id = Address, win = Window, + code = CodeTx, cons = ConsTx, + funs = {ScrollWin, []}}, + NewPages = Pages ++ [Page], + State#s{cons = {Consbook, NewPages}}. + + +edit(State) -> + ok = tell(info, "EDIT clicked"), + State. + + +close_instance(State) -> + ok = tell(info, "CLOSE_INSTANCE clicked"), + State. + + + +%% (Somewhat silly) Data operations + +store_nth(1, E, [_ | T]) -> [E | T]; +store_nth(N, E, [H | T]) -> [H | store_nth(N - 1, T, E)]. + + drop_nth(1, [_ | T]) -> T; drop_nth(N, [H | T]) -> [H | drop_nth(N - 1, T)]. diff --git a/src/gmc_v_wallman.erl b/src/gmc_v_wallman.erl index 0ad83e2..03dcee5 100644 --- a/src/gmc_v_wallman.erl +++ b/src/gmc_v_wallman.erl @@ -273,7 +273,7 @@ do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) -> end. do_new2(Path, J, Frame) -> - Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Set Node"), [{size, {400, 250}}]), + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Wallet"), [{size, {400, 250}}]), Sizer = wxBoxSizer:new(?wxVERTICAL), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), @@ -360,7 +360,7 @@ do_import2(_, "", _, _) -> abort; do_import2(Dir, File, J, Frame) -> Path = filename:join(Dir, File), - Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Set Node")), + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Import Wallet")), Sizer = wxBoxSizer:new(?wxVERTICAL), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), diff --git a/zomp.meta b/zomp.meta index 254046e..7a03bb0 100644 --- a/zomp.meta +++ b/zomp.meta @@ -2,10 +2,11 @@ {type,gui}. {modules,[]}. {prefix,"gmc"}. -{author,"Craig Everett"}. {desc,"A desktop client for the Gajumaru network of blockchain networks"}. +{author,"Craig Everett"}. {package_id,{"otpr","clutch",{0,2,0}}}. -{deps,[{"otpr","hakuzaru",{0,2,0}}, +{deps,[{"otpr","lom",{1,0,0}}, + {"otpr","hakuzaru",{0,2,0}}, {"otpr","aesophia",{8,0,1}}, {"otpr","aeserialization",{0,1,2}}, {"otpr","zj",{1,1,0}}, -- 2.30.2 From eac630168cc260acfdb183109a14e86e777b0526 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Tue, 25 Feb 2025 16:32:11 +0900 Subject: [PATCH 08/10] Shaky, but working-ish contract deployment and calls --- src/: | 1029 ++++++++++++++++++++++++++++++++++++++++ src/clutch.erl | 1 + src/gmc_con.erl | 169 +++++-- src/gmc_gui.erl | 6 +- src/gmc_key_master.erl | 8 +- src/gmc_v_devman.erl | 532 ++++++++++++++------- zomp.meta | 12 +- 7 files changed, 1525 insertions(+), 232 deletions(-) create mode 100644 src/: diff --git a/src/: b/src/: new file mode 100644 index 0000000..15b8cf4 --- /dev/null +++ b/src/: @@ -0,0 +1,1029 @@ +-module(gmc_v_devman). +-vsn("0.2.0"). +-author("Craig Everett "). +-copyright("QPQ AG "). +-license("GPL-3.0-or-later"). + +-behavior(wx_object). +%-behavior(gmc_v). +-include_lib("wx/include/wx.hrl"). +-export([to_front/1]). +-export([set_manifest/1, open_contract/1, trouble/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("gmc.hrl"). + +% Widgets +-record(w, + {name = none :: atom() | {FunName :: binary(), call | dryr}, + id = 0 :: integer(), + wx = none :: none | wx:wx_object()}). + +% Contract functions in an ACI +-record(f, + {name = <<"">> :: binary(), + call = #w{} :: #w{}, + dryrun = #w{} :: none | #w{}, + args = [] :: [{wx:wx_object(), wx:wx_object(), argt()}]}). + +% Code book pages +-record(p, + {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, + win = none :: none | wx:wx_object(), + code = none :: none | wxTextCtrl:wxTextCtrl()}). + +% Contract pages +-record(c, + {id = "" :: string(), + win = none :: none | wx:wx_object(), + code = none :: none | wxTextCtrl:wxTextCtrl(), + cons = none :: none | wxTextCtrl:wxTextCtrl(), + build = none :: none | map(), + funs = {#w{}, []} :: {#w{}, [#f{}]}}). + +% 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(), + buttons = #{} :: #{WX_ID :: integer() := #w{}}, + tabs = none :: none | wx:wx_object(), + code = {none, []} :: {Codebook :: none | wx:wx_object(), Pages :: [#p{}]}, + cons = {none, []} :: {Consbook :: none | wx:wx_object(), Pages :: [#c{}]}}). + +-type argt() :: int | string | address | list(argt()). + +%%% Interface + +-spec to_front(Win) -> ok + when Win :: wx:wx_object(). + +to_front(Win) -> + wx_object:cast(Win, to_front). + + +% TODO: Probably kill this +-spec set_manifest(Entries) -> ok + when Entries :: list(). + +set_manifest(Entries) -> + case is_pid(whereis(?MODULE)) of + true -> wx_object:cast(?MODULE, {set_manifest, Entries}); + false -> ok + end. + + +-spec open_contract(Address) -> ok + when Address :: string(). + +open_contract(Address) -> + wx_object:cast(?MODULE, {open_contract, Address}). + + +-spec trouble(Info) -> ok + when Info :: term(). + +trouble(Info) -> + wx_object:cast(?MODULE, {trouble, Info}). + + + + +%%% Startup Functions + +start_link(Args) -> + wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []). + + +init({Prefs, Manifest}) -> + Lang = maps:get(lang, Prefs, en_us), + Trans = gmc_jt:read_translations(?MODULE), + J = gmc_jt:j(Lang, Trans), + Wx = wx:new(), + Frame = wxFrame:new(Wx, ?wxID_ANY, J("Contracts")), + + MainSz = wxBoxSizer:new(?wxVERTICAL), + TopBook = wxNotebook:new(Frame, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + {LWin, LButtons, Codebook} = make_l_win(TopBook, J), + {RWin, RButtons, Consbook} = make_r_win(TopBook, J), + ButtonMap = maps:merge(LButtons, RButtons), + true = wxNotebook:addPage(TopBook, LWin, J("Contract Editor"), []), + true = wxNotebook:addPage(TopBook, RWin, J("Deployed Contracts"), []), + State = + #s{wx = Wx, frame = Frame, + j = J, prefs = Prefs, + buttons = ButtonMap, tabs = TopBook, + code = {Codebook, []}, cons = {Consbook, []}}, + _ = wxSizer:add(MainSz, TopBook, zxw:flags(wide)), + _ = wxFrame:setSizer(Frame, MainSz), + _ = wxSizer:layout(MainSz), + ok = gmc_v:safe_size(Frame, Prefs), + ok = wxFrame:connect(Frame, close_window), + ok = wxFrame:connect(Frame, command_button_clicked), + ok = wxFrame:center(Frame), + true = wxFrame:show(Frame), + NewState = add_code_pages(State, Manifest), + {Frame, NewState}. + + +make_l_win(TopBook, J) -> + Win = wxWindow:new(TopBook, ?wxID_ANY), + MainSz = wxBoxSizer:new(?wxVERTICAL), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + ButtonTemplates = + [{new, J("New")}, + {open, J("Open")}, + {save, J("Save")}, + {rename, J("Save (rename)")}, + {deploy, J("Deploy")}, + {close_source, J("Close")}], + MakeButton = + fun({Name, Label}) -> + B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), + #w{name = Name, id = wxButton:getId(B), wx = B} + end, + Buttons = lists:map(MakeButton, ButtonTemplates), + AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, + Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + ok = lists:foreach(AddButton, Buttons), + _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), + _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), + _ = wxWindow:setSizer(Win, MainSz), + MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, + ButtonMap = lists:foldl(MapButton, #{}, Buttons), + {Win, ButtonMap, Codebook}. + +make_r_win(TopBook, J) -> + Win = wxWindow:new(TopBook, ?wxID_ANY), + MainSz = wxBoxSizer:new(?wxVERTICAL), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + ButtonTemplates = + [{load, J("Load from Chain")}, + {edit, J("Copy to Editor")}, + {close_instance, J("Close")}], + MakeButton = + fun({Name, Label}) -> + B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), + #w{name = Name, id = wxButton:getId(B), wx = B} + end, + Buttons = lists:map(MakeButton, ButtonTemplates), + AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, + Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + ok = lists:foreach(AddButton, Buttons), + _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), + _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), + _ = wxWindow:setSizer(Win, MainSz), + MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, + ButtonMap = lists:foldl(MapButton, #{}, Buttons), + {Win, ButtonMap, Codebook}. + + + +add_code_pages(State, Files) -> + lists:foldl(fun add_code_page/2, State, Files). + + + +%%% OTP callbacks + +handle_call(Unexpected, From, State) -> + ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), + {noreply, State}. + + +handle_cast(to_front, State = #s{frame = Frame}) -> + ok = wxFrame:raise(Frame), + {noreply, State}; +handle_cast({open_contract, Address}, State) -> + NewState = load2(State, Address), + {noreply, NewState}; +handle_cast({trouble, Info}, State) -> + ok = handle_troubling(State, Info), + {noreply, State}; +handle_cast(Unexpected, State) -> + ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_info(Unexpected, State) -> + ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, + id = ID}, + State = #s{buttons = Buttons}) -> + NewState = + case maps:get(ID, Buttons, undefined) of + #w{name = new} -> new_file(State); + #w{name = open} -> open(State); + #w{name = save} -> save(State); + #w{name = rename} -> rename(State); + #w{name = deploy} -> deploy(State); + #w{name = close_source} -> close_source(State); + #w{name = load} -> load(State); + #w{name = edit} -> edit(State); + #w{name = close_instance} -> close_instance(State); + #w{name = Name, wx = Button} -> clicked(State, Name, Button); + undefined -> + tell("Received message: ~w", [E]), + State + end, + {noreply, NewState}; +handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) -> + Geometry = + case wxTopLevelWindow:isMaximized(Frame) of + true -> + max; + false -> + {X, Y} = wxWindow:getPosition(Frame), + {W, H} = wxWindow:getSize(Frame), + {X, Y, W, H} + end, + NewPrefs = maps:put(geometry, Geometry, Prefs), + ok = gmc_con:save(?MODULE, NewPrefs), + ok = wxWindow:destroy(Frame), + {noreply, State}; +handle_event(Event, State) -> + ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), + {noreply, State}. + + +handle_troubling(#s{frame = Frame}, Info) -> + zxw:show_message(Frame, Info). + + +code_change(_, State, _) -> + {ok, State}. + + +terminate(Reason, State) -> + ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), + wx:destroy(). + + + +%%% Doers + +clicked(State = #s{cons = {Consbook, Contracts}}, Name, Button) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + ok = tell(warning, "Inconcievable! No notebook page is selected!"), + State; + Index -> + Contract = lists:nth(Index + 1, Contracts), + clicked(State, Contract, Name, Button) + end. + +clicked(State, Contract, Name, Button) -> + ok = tell("Button: ~p ~p~nContract: ~p", [Button, Name, Contract]), + State. + + +add_code_page(State = #s{code = {Codebook, Pages}}, File) -> + case keyfind_index({file, File}, #p.path, Pages) of + error -> + add_code_page2(State, File); + {ok, Index} -> + _ = wxNotebook:setSelection(Codebook, Index - 1), + State + end. + +add_code_page2(State = #s{j = J}, {file, File}) -> + case file:read_file(File) of + {ok, Bin} -> + case unicode:characters_to_list(Bin) of + Code when is_list(Code) -> + add_code_page(State, {file, File}, Code); + Error -> + Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Error]), + ok = handle_troubling(Message, State), + State + end; + {error, Reason} -> + Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Reason]), + ok = handle_troubling(Message, State), + State + end; +add_code_page2(State, {hash, Address}) -> + open_hash2(State, Address). + +add_code_page(State = #s{tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> +% FIXME: One of these days we need to define the text area as a wxStyledTextCtrl and will +% have to contend with system theme issues (light/dark themese, namely) +% Leaving this little thing here to remind myself how any of that works later. +% The call below returns a wx_color4() type (not that we need alpha...). +% Color = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW), +% tell("Color: ~p", [Color]), + Window = wxWindow:new(Codebook, ?wxID_ANY), + PageSz = wxBoxSizer:new(?wxHORIZONTAL), + + CodeTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_PROCESS_TAB bor ?wxTE_DONTWRAP}, + CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), + TextAt = wxTextAttr:new(), + Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), + ok = wxTextAttr:setFont(TextAt, Mono), + true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), + ok = wxTextCtrl:setValue(CodeTx, Code), + + _ = wxSizer:add(PageSz, CodeTx, zxw:flags(wide)), + + ok = wxWindow:setSizer(Window, PageSz), + ok = wxSizer:layout(PageSz), + _ = wxNotebook:changeSelection(TopBook, 0), + FileName = + case Location of + {file, Path} -> filename:basename(Path); + {hash, Addr} -> Addr + end, + true = wxNotebook:addPage(Codebook, Window, FileName, [{bSelect, true}]), + Page = #p{path = Location, win = Window, code = CodeTx}, + NewPages = Pages ++ [Page], + State#s{code = {Codebook, NewPages}}. + + +new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> + DefaultDir = + case maps:find(dir, Prefs) of + {ok, PrefDir} -> + PrefDir; + error -> + case os:getenv("ZOMP_DIR") of + "" -> file:get_pwd(); + D -> filename:basename(D) + end + end, + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, "my_contract.aes"}, + {wildCard, "*.aes"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + Path = filename:join(Dir, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NextState = State#s{prefs = NewPrefs}, + add_code_page(NextState, {file, Path}, "") + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState. + + +deploy(State = #s{code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + #p{code = CodeTx} = lists:nth(Index + 1, Pages), + Source = wxTextCtrl:getValue(CodeTx), + deploy2(State, Source) + end. + +deploy2(State, Source) -> + case so_compiler:from_string(Source, [{aci, json}]) of + {ok, Build} -> + deploy3(State, Build); + Other -> + ok = tell(info, "Compilation Failed!~n~tp", [Other]), + State + end. + +deploy3(State, Build) -> + case gmc_con:list_keys() of + {ok, 0, []} -> + handle_troubling(State, "No keys exist in the current wallet."); + {ok, Selected, Keys} -> + deploy4(State, Build, Selected, Keys); + error -> + handle_troubling(State, "No wallet is selected!") + end. + +deploy4(State = #s{frame = Frame, j = J}, Build = #{aci := ACI}, Selected, Keys) -> + {#{functions := Funs}, _} = find_main(ACI), + #{arguments := As} = lom:find(name, <<"init">>, Funs), + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + ScrollWin = wxScrolledWindow:new(Dialog), + FunName = unicode:characters_to_list(["init/", integer_to_list(length(As))]), + FunSizer = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), + ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSizer), + ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), + KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Key")}]), + KeyPicker = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Keys}]), + _ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)), + ok = wxChoice:setSelection(KeyPicker, Selected - 1), + 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)), + GridSz = wxFlexGridSizer:new(2, 4, 4), + ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), + ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), + MakeArgField = + fun(#{name := AN, type := T}) -> + Type = + case T of + <<"address">> -> address; + <<"int">> -> integer; + <<"bool">> -> boolean; + L when is_list(L) -> list; % FIXME +% I when is_binary(I) -> iface % FIXME + I when is_binary(I) -> address % FIXME + end, + ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), + TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), + _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), + _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), + {ANT, TCT, Type} + end, + ArgFields = lists:map(MakeArgField, As), + _ = wxStaticBoxSizer:add(FunSizer, GridSz, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ScrollWin, [{proportion, 5}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, KeySz, [{proportion, 0}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, ButtSz, [{proportion, 1}, {flag, ?wxEXPAND}]), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:setSize(Dialog, {500, 300}), + ok = wxDialog:center(Dialog), + Outcome = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)), + BinID = unicode:characters_to_binary(ID), + Inputs = lists:map(fun get_arg/1, ArgFields), + {ok, BinID, Inputs}; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Outcome of + {ok, SigID, Args} -> deploy5(State, SigID, Build, Args); + cancel -> State + end. + +deploy5(State, SigID, Build, Args) -> + tell(info, "Build: ~p", [Build]), + ok = gmc_con:deploy(SigID, Build, Args), + State. + + +open(State = #s{frame = Frame, j = J}) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Open Contract Source")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + Choices = wxRadioBox:new(Dialog, + ?wxID_ANY, + J("Select Origin"), + ?wxDefaultPosition, + ?wxDefaultSize, + [J("From File"), J("From Hash")], + [{majorDim, 1}]), + 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, Choices, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxDialog:setSize(Dialog, {250, 170}), + ok = wxBoxSizer:layout(Sizer), + Choice = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + case wxRadioBox:getSelection(Choices) of + 0 -> file; + 1 -> hash; + ?wxNOT_FOUND -> none + end; + ?wxID_CANCEL -> cancel + end, + ok = wxDialog:destroy(Dialog), + case Choice of + file -> + open_file(State); + hash -> + open_hash(State); + none -> + ok = tell(info, "No selection."), + State; + cancel -> + ok = tell(info, "Cancelled."), + State + end. + + +open_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> + DefaultDir = + case maps:find(dir, Prefs) of + {ok, PrefDir} -> + PrefDir; + error -> + case os:getenv("ZOMP_DIR") of + "" -> file:get_pwd(); + D -> filename:basename(D) + end + end, + Options = + [{message, J("Load Contract Source")}, + {defaultDir, DefaultDir}, + {wildCard, "*.aes"}, + {style, ?wxFD_OPEN}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + Path = filename:join(Dir, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NextState = State#s{prefs = NewPrefs}, + add_code_page(NextState, {file, Path}) + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState. + + +open_hash(State = #s{frame = Frame, j = J}) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), + AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(AddressSz, AddressTx, 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)), + _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:setSize(Dialog, {500, 200}), + ok = wxDialog:center(Dialog), + ok = wxStyledTextCtrl:setFocus(AddressTx), + Choice = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + case wxTextCtrl:getValue(AddressTx) of + "" -> cancel; + A -> {ok, A} + end; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Choice of + {ok, Address} -> open_hash2(State, Address); + cancel -> State + end. + +open_hash2(State, Address) -> + case hz:contract_source(Address) of + {ok, Source} -> + open_hash3(State, Address, Source); + Error -> + ok = handle_troubling(Error, State), + State + end. + +open_hash3(State, Address, Source) -> + % TODO: Compile on load and verify the deployed hash for validity. + case so_compiler:from_string(Source, [{aci, json}]) of + {ok, Build = #{aci := ACI}} -> + {Defs = #{functions := Funs}, ConIfaces} = find_main(ACI), + Callable = lom:delete(name, <<"init">>, Funs), + FunDefs = {maps:put(functions, Callable, Defs), ConIfaces}, + ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), + add_code_page(State, {hash, Address}, Source); + Other -> + ok = tell(info, "Compilation Failed!~n~tp", [Other]), + State + end. + + +% TODO: Break this down -- tons of things in here recur. +save(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + case lists:nth(Index + 1, Pages) of + #p{path = {file, Path}, code = Widget} -> + Source = wxStyledTextCtrl:getText(Widget), + case filelib:ensure_dir(Path) of + ok -> + case file:write_file(Path, Source) of + ok -> + State; + Error -> + ok = handle_troubling(State, Error), + State + end; + Error -> + ok = handle_troubling(State, Error), + State + end; + Page = #p{path = {hash, Hash}, code = Widget} -> + DefaultDir = + case maps:find(dir, Prefs) of + {ok, PrefDir} -> + PrefDir; + error -> + case os:getenv("ZOMP_DIR") of + "" -> file:get_pwd(); + D -> filename:basename(D) + end + end, + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, unicode:characters_to_list([Hash, ".aes"])}, + {wildCard, "*.aes"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + Path = filename:join(Dir, File), + Source = wxTextCtrl:getValue(Widget), + case filelib:ensure_dir(Path) of + ok -> + case file:write_file(Path, Source) of + ok -> + true = wxNotebook:setPageText(Codebook, Index, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NewPage = Page#p{path = {file, Path}}, + NewPages = store_nth(Index + 1, NewPage, Pages), + NewCode = {Codebook, NewPages}, + State#s{prefs = NewPrefs, code = NewCode}; + Error -> + ok = handle_troubling(State, Error), + State + end; + Error -> + ok = handle_troubling(State, Error), + State + end + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState + end + end. + +% TODO: Break this down -- tons of things in here recur. +rename(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + case lists:nth(Index + 1, Pages) of + Page = #p{path = {file, Path}, code = Widget} -> + DefaultDir = filename:dirname(Path), + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, filename:basename(Path)}, + {wildCard, "*.aes"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + NewPath = filename:join(Dir, File), + Source = wxTextCtrl:getValue(Widget), + case filelib:ensure_dir(NewPath) of + ok -> + case file:write_file(NewPath, Source) of + ok -> + true = wxNotebook:setPageText(Codebook, Index, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NewPage = Page#p{path = {file, NewPath}}, + NewPages = store_nth(Index + 1, NewPage, Pages), + NewCode = {Codebook, NewPages}, + State#s{prefs = NewPrefs, code = NewCode}; + Error -> + ok = handle_troubling(State, Error), + State + end; + Error -> + ok = handle_troubling(State, Error), + State + end + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState; + #p{path = {hash, _}} -> + save(State) + end + end. + + +close_source(State = #s{code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + NewPages = drop_nth(Index + 1, Pages), + true = wxNotebook:deletePage(Codebook, Index), + State#s{code = {Codebook, NewPages}} + end. + + +load(State = #s{frame = Frame, j = J}) -> + % TODO: Extract the exact compiler version, load it, and use only that or fail if + % the specific version is unavailable. + % TODO: Compile on load and verify the deployed hash for validity. + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), + AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(AddressSz, AddressTx, 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)), + _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:setSize(Dialog, {500, 200}), + ok = wxDialog:center(Dialog), + ok = wxStyledTextCtrl:setFocus(AddressTx), + Choice = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + case wxTextCtrl:getValue(AddressTx) of + "" -> cancel; + A -> {ok, A} + end; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Choice of + {ok, Address} -> load2(State, Address); + cancel -> State + end. + +load2(State, Address) -> + case hz:contract_source(Address) of + {ok, Source} -> + load3(State, Address, Source); + Error -> + ok = handle_troubling(Error, State), + State + end. + +load3(State = #s{tabs = TopBook, cons = {Consbook, Pages}, buttons = Buttons, j = J}, + Address, + Source) -> + Window = wxWindow:new(Consbook, ?wxID_ANY), + PageSz = wxBoxSizer:new(?wxVERTICAL), + ProgSz = wxBoxSizer:new(?wxHORIZONTAL), + CodeSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Contract Source")}]), + CodeTxStyle = {style, ?wxTE_MULTILINE + bor ?wxTE_PROCESS_TAB + bor ?wxTE_DONTWRAP + bor ?wxTE_READONLY}, + CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), + Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), + TextAt = wxTextAttr:new(), + ok = wxTextAttr:setFont(TextAt, Mono), + true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), + ok = wxTextCtrl:setValue(CodeTx, Source), + _ = wxSizer:add(CodeSz, CodeTx, zxw:flags(wide)), + ScrollWin = wxScrolledWindow:new(Window), + FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Function Interfaces")}]), + ok = wxWindow:setSizer(ScrollWin, FunSz), + ConsSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Console")}]), + ConsTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}, + ConsTx = wxTextCtrl:new(Window, ?wxID_ANY, [ConsTxStyle]), + _ = wxSizer:add(ConsSz, ConsTx, zxw:flags(wide)), + _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 3}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(ProgSz, ScrollWin, [{proportion, 1}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(PageSz, ProgSz, [{proportion, 3}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(PageSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND}]), + {Out, IFaces, Build, NewButtons} = + case so_compiler:from_string(Source, [{aci, json}]) of + {ok, B = #{aci := ACI}} -> + {#{functions := Fs}, _} = find_main(ACI), + Callable = lom:delete(name, <<"init">>, Fs), + {NB, IFs} = fun_interfaces(ScrollWin, FunSz, Buttons, Callable, J), + O = io_lib:format("Compilation Succeeded!~n~tp~n~nDone!~n", [B]), + {O, IFs, B, NB}; + Other -> + O = io_llib:format("Compilation Failed!~n~tp~n", [Other]), + {O, [], none, Buttons} + end, + ok = wxWindow:setSizer(Window, PageSz), + ok = wxSizer:layout(PageSz), + true = wxNotebook:addPage(Consbook, Window, Address, [{bSelect, true}]), + Page = #c{id = Address, win = Window, + code = CodeTx, cons = ConsTx, + build = Build, funs = {ScrollWin, IFaces}}, + NewPages = Pages ++ [Page], + ok = wxTextCtrl:appendText(ConsTx, Out), + _ = wxNotebook:changeSelection(TopBook, 1), + % TODO: Verify the deployed hash for validity. + State#s{cons = {Consbook, NewPages}, buttons = NewButtons}. + + +get_arg({_, TextCtrl, _}) -> + wxTextCtrl:getValue(TextCtrl). + +find_main(ACI) -> + find_main(ACI, none, []). + +find_main([#{contract := I = #{kind := contract_interface}} | T], M, Is) -> + find_main(T, M, [I | Is]); +find_main([#{contract := M = #{kind := contract_main}} | T], _, Is) -> + find_main(T, M, Is); +find_main([#{namespace := _} | T], M, Is) -> + find_main(T, M, Is); +find_main([C | T], M, Is) -> + ok = tell("Surprising ACI element: ~p", [C]), + find_main(T, M, Is); +find_main([], M, Is) -> + {M, Is}. + +fun_interfaces(ScrollWin, FunSz, Buttons, Funs, J) -> + MakeIface = + fun(#{name := N, arguments := As}) -> + FunName = unicode:characters_to_list([N, "/", integer_to_list(length(As))]), + FN = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), + GridSz = wxFlexGridSizer:new(2, 4, 4), + ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), + ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), + MakeArgField = + fun(#{name := AN, type := T}) -> + Type = + case T of + <<"address">> -> address; + <<"int">> -> integer; + <<"bool">> -> boolean; + L when is_list(L) -> list; % FIXME +% I when is_binary(I) -> iface % FIXME + I when is_binary(I) -> address % FIXME + end, + ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), + TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), + _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), + _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), + {ANT, TCT, Type} + end, + ArgFields = lists:map(MakeArgField, As), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + {CallButton, DryRunButton} = + case N =:= <<"init">> of + false -> + CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]), + DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]), + _ = wxBoxSizer:add(ButtSz, CallBn, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, DryRBn, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(CallBn), wx = CallBn}, + #w{name = {N, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}}; + true -> + Deploy = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Deploy")}]), + _ = wxBoxSizer:add(ButtSz, Deploy, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(Deploy), wx = Deploy}, + none} + end, + _ = wxStaticBoxSizer:add(FN, GridSz, zxw:flags(wide)), + _ = wxStaticBoxSizer:add(FN, ButtSz, zxw:flags(base)), + _ = wxSizer:add(FunSz, FN, zxw:flags(base)), + #f{name = N, call = CallButton, dryrun = DryRunButton, args = ArgFields} + end, + IFaces = lists:map(MakeIface, Funs), + NewButtons = lists:foldl(fun map_iface_buttons/2, Buttons, IFaces), + {NewButtons, IFaces}. + +button_key_list([#f{call = #w{id = C}, dryrun = #w{id = D}} | T]) -> + [C, D | button_key_list(T)]; +button_key_list([#f{call = #w{id = C}, dryrun = none} | T]) -> + [C | button_key_list(T)]; +button_key_list([]) -> + []. + +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) -> + maps:put(DID, D, maps:put(CID, C, A)); +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = none}, A) -> + maps:put(CID, C, A). + + +edit(State = #s{cons = {Consbook, Pages}}) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + State; + Index -> + #c{code = CodeTx} = lists:nth(Index + 1, Pages), + Address = wxNotebook:getPageText(Consbook, Index), + Source = wxTextCtrl:getValue(CodeTx), + add_code_page(State, {hash, Address}, Source) + end. + + +close_instance(State = #s{cons = {Consbook, Pages}, buttons = Buttons}) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + State; + Index -> + {#c{funs = {_, IFaces}}, NewPages} = take_nth(Index + 1, Pages), + IDs = list_iface_buttons(IFaces), + NewButtons = maps:without(IDs, Buttons), + true = wxNotebook:deletePage(Consbook, Index), + State#s{cons = {Consbook, NewPages}, buttons = NewButtons} + end. + +list_iface_buttons(IFaces) -> + lists:foldl(fun list_iface_buttons/2, [], IFaces). + +list_iface_buttons(#f{call = #w{id = CID}, dryrun = #w{id = DID}}, A) -> + [CID, DID | A]. + + + +%% (Somewhat silly) Data operations + +store_nth(1, E, [_ | T]) -> [E | T]; +store_nth(N, E, [H | T]) -> [H | store_nth(N - 1, E, T)]. + + +drop_nth(1, [_ | T]) -> T; +drop_nth(N, [H | T]) -> [H | drop_nth(N - 1, T)]. + +take_nth(N, L) -> + take_nth(N, L, []). + +take_nth(1, [E | T], A) -> {E, lists:reverse(A) ++ T}; +take_nth(N, [H | T], A) -> take_nth(N - 1, T, [H | A]). + + +keyfind_index(K, E, L) -> + keyfind_index(K, E, 1, L). + +keyfind_index(K, E, I, [H | T]) -> + case element(E, H) =:= K of + false -> keyfind_index(K, E, I + 1, T); + true -> {ok, I} + end; +keyfind_index(_, _, _, []) -> + error. diff --git a/src/clutch.erl b/src/clutch.erl index ead5b07..4ebecdc 100644 --- a/src/clutch.erl +++ b/src/clutch.erl @@ -59,6 +59,7 @@ start(normal, _Args) -> end, ok = application:ensure_started(hakuzaru), ok = application:ensure_started(zxwidgets), + ok = application:ensure_started(sophia), gmc_sup:start_link(). diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 1753774..f3fa634 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -14,9 +14,9 @@ selected/1, password/2, refresh/0, - nonce/1, spend/2, chain/1, grids/1, sign_mess/1, sign_tx/1, - deploy/2, - make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1, + nonce/1, spend/2, chain/1, grids/1, sign_mess/1, sign_tx/1, sign_call/3, dry_run/2, + deploy/3, + make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1, list_keys/0, add_node/1, set_sole_node/1]). -export([encrypt/2, decrypt/2]). -export([save/2]). @@ -170,15 +170,33 @@ sign_tx(Request) -> gen_server:cast(?MODULE, {sign_tx, Request}). --spec deploy(Build, InitArgs) -> Result - when Build :: map(), - InitArgs :: [Arg :: string()], - Result :: {ok, TX_Hash :: clutch:id()} - | {error, Reason}, - Reason :: term(). % FIXME +-spec sign_call(ConID, PubKey, TX) -> ok + when ConID :: clutch:id(), + PubKey :: clutch:id(), + TX :: binary(). -deploy(Build, InitArgs) -> - gen_server:call(?MODULE, {deploy, Build, InitArgs}). +sign_call(ConID, PubKey, TX) -> + gen_server:cast(?MODULE, {sign_call, ConID, PubKey, TX}). + + +-spec dry_run(ConID, TX) -> ok + when ConID :: clutch:id(), + TX :: binary(). + +dry_run(ConID, TX) -> + gen_server:cast(?MODULE, {dry_run, ConID, TX}). + + +-spec deploy(CreatorID, Build, InitArgs) -> Result + when CreatorID :: clutch:id(), + Build :: map(), + InitArgs :: [Arg :: string()], + Result :: {ok, TX_Hash :: clutch:id()} + | {error, Reason}, + Reason :: term(). % FIXME + +deploy(CreatorID, Build, InitArgs) -> + gen_server:cast(?MODULE, {deploy, CreatorID, Build, InitArgs}). -spec make_key(Type, Size, Name, Seed, Encoding, Transform) -> ok @@ -230,6 +248,14 @@ drop_key(ID) -> gen_server:cast(?MODULE, {drop_key, ID}). +-spec list_keys() -> Result + when Result :: {ok, Selected :: non_neg_integer(), Keys :: [clutch:id()]} + | error. + +list_keys() -> + gen_server:call(?MODULE, list_keys). + + -spec add_node(New) -> ok when New :: #node{}. @@ -315,6 +341,9 @@ read_prefs() -> %% The gen_server:handle_call/3 callback. %% See: http://erlang.org/doc/man/gen_server.html#Module:handle_call-3 +handle_call(list_keys, _, State) -> + Response = do_list_keys(State), + {reply, Response, State}; handle_call({nonce, ID}, _, State) -> Response = do_nonce(ID), {reply, Response, State}; @@ -324,9 +353,6 @@ handle_call({save, Module, Prefs}, _, State) -> handle_call({mnemonic, ID}, _, State) -> Response = do_mnemonic(ID, State), {reply, Response, State}; -handle_call({deploy, Build, InitArgs}, _, State) -> - Result = do_deploy(Build, InitArgs, State), - {reply, Result, State}; handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. @@ -384,6 +410,15 @@ handle_cast({sign_mess, Request}, State) -> handle_cast({sign_tx, Request}, State) -> ok = do_sign_tx(Request, State), {noreply, State}; +handle_cast({sign_call, ConID, PubKey, TX}, State) -> + ok = do_sign_call(State, ConID, PubKey, TX), + {noreply, State}; +handle_cast({dry_run, ConID, TX}, State) -> + ok = do_dry_run(ConID, TX), + {noreply, State}; +handle_cast({deploy, CreatorID, Build, InitArgs}, State) -> + ok = do_deploy(CreatorID, Build, InitArgs, State), + {noreply, State}; handle_cast({make_key, Name, Seed, Encoding, Transform}, State) -> NewState = do_make_key(Name, Seed, Encoding, Transform, State), {noreply, NewState}; @@ -613,8 +648,8 @@ do_grids_sig2(WTF) -> do_sign_mess(Request = #{"public_id" := ID, "payload" := Message}, #s{wallet = #wallet{keys = Keys}}) -> case lists:keyfind(ID, #key.id, Keys) of - #key{pair = #{secret := PrivKey}} -> - Sig = base64:encode(sign_message(list_to_binary(Message), PrivKey)), + #key{pair = #{secret := SecKey}} -> + Sig = base64:encode(sign_message(list_to_binary(Message), SecKey)), do_sign_mess2(Request#{"signature" => Sig}); false -> gmc_gui:trouble({bad_key, ID}) @@ -638,13 +673,13 @@ do_sign_mess2(Request = #{"url" := URL}) -> % TODO: Should probably be part of Hakuzaru -sign_message(Message, PrivKey) -> +sign_message(Message, SecKey) -> Prefix = <<"Gajumaru Signed Message:\n">>, {ok, PSize} = vencode(byte_size(Prefix)), {ok, MSize} = vencode(byte_size(Message)), Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]), {ok, Hashed} = eblake2:blake2b(32, Smashed), - ecu_eddsa:sign_detached(Hashed, PrivKey). + ecu_eddsa:sign_detached(Hashed, SecKey). vencode(N) when N < 0 -> @@ -672,9 +707,9 @@ do_sign_tx(Request = #{"public_id" := ID, "payload" := CallData, "network_id" := #s{wallet = #wallet{keys = Keys}}) -> BinNID = list_to_binary(NID), case lists:keyfind(ID, #key.id, Keys) of - #key{pair = #{secret := PrivKey}} -> + #key{pair = #{secret := SecKey}} -> BinaryTX = list_to_binary(CallData), - SignedTX = sign_tx_hash(BinaryTX, PrivKey, BinNID), + SignedTX = sign_tx_hash(BinaryTX, SecKey, BinNID), do_sign_tx2(Request#{"signed" => true, "payload" := SignedTX}); false -> gmc_gui:trouble({bad_key, ID}) @@ -696,11 +731,11 @@ do_sign_tx2(Request = #{"url" := URL}) -> Error -> gmc_gui:trouble(Error) end. -sign_tx_hash(Unsigned, PrivKey, NetworkID) -> - {ok, TX_Data} = aeser_api_encoder:safe_decode(transaction, Unsigned), +sign_tx_hash(Unsigned, SecKey, NetworkID) -> + {ok, TX_Data} = gmser_api_encoder:safe_decode(transaction, Unsigned), {ok, Hash} = eblake2:blake2b(32, TX_Data), NetworkHash = <>, - Signature = ecu_eddsa:sign_detached(NetworkHash, PrivKey), + Signature = ecu_eddsa:sign_detached(NetworkHash, SecKey), SigTxType = signed_tx, SigTxVsn = 1, SigTemplate = @@ -709,19 +744,57 @@ sign_tx_hash(Unsigned, PrivKey, NetworkID) -> TX = [{signatures, [Signature]}, {transaction, TX_Data}], - SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX), - aeser_api_encoder:encode(transaction, SignedTX). + SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX), + gmser_api_encoder:encode(transaction, SignedTX). + + +do_sign_call(#s{wallet = #wallet{keys = Keys, chain_id = ChainID}}, + ConID, + PubKey, + TX) -> + #key{pair = #{secret := SecKey}} = lists:keyfind(PubKey, #key.id, Keys), + SignedTX = sign_tx_hash(TX, SecKey, ChainID), + case hz:post_tx(SignedTX) of + {ok, Data = #{"tx_hash" := TXHash}} -> + ok = tell("Contract deploy TX succeded with: ~p", [TXHash]), + do_sign_call2(ConID, Data); + {ok, WTF} -> + gmc_v_devman:trouble({error, WTF}); + Error -> + gmc_v_devman:trouble(Error) + end; +do_sign_call(_, _, _, _) -> + gmc_v_devman:trouble({error, no_chain}). + +do_sign_call2(ConID, #{"tx_hash" := TXHash}) -> + case hz:tx_info(TXHash) of + {ok, CallInfo = #{"call_info" := #{"return_type" := "ok"}}} -> + gmc_v_devman:call_result(ConID, CallInfo); + {error, "Tx not mined"} -> + gmc_v_devman:trouble({tx_hash, TXHash}); + {ok, Reason = #{"call_info" := #{"return_type" := "revert"}}} -> + gmc_v_devman:trouble({error, Reason}); + Error -> + gmc_v_devman:trouble(Error) + end. + + +do_dry_run(ConID, TX) -> + case hz:dry_run(TX) of + {ok, Result} -> gmc_v_devman:dryrun_result(ConID, Result); + Other -> gmc_v_devmam:trouble({error, ConID, Other}) +end. do_spend(KeyID, TX, State = #s{wallet = #wallet{keys = Keys}}) -> case lists:keyfind(KeyID, #key.id, Keys) of - #key{pair = #{secret := PrivKey}} -> - do_spend2(PrivKey, TX, State); + #key{pair = #{secret := SecKey}} -> + do_spend2(SecKey, TX, State); false -> log(warning, "Tried do_spend with a bad key: ~p", [KeyID]) end. -do_spend2(PrivKey, +do_spend2(SecKey, #spend_tx{sender_id = SenderID, recipient_id = RecipientID, amount = Amount, @@ -751,9 +824,9 @@ do_spend2(PrivKey, {ttl, int}, {nonce, int}, {payload, binary}], - BinaryTX = aeser_chain_objects:serialize(Type, Vsn, Template, Fields), + BinaryTX = gmser_chain_objects:serialize(Type, Vsn, Template, Fields), NetworkTX = <>, - Signature = ecu_eddsa:sign_detached(NetworkTX, PrivKey), + Signature = ecu_eddsa:sign_detached(NetworkTX, SecKey), SigTxType = signed_tx, SigTxVsn = 1, SigTemplate = @@ -762,12 +835,18 @@ do_spend2(PrivKey, TX_Data = [{signatures, [Signature]}, {transaction, BinaryTX}], - SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data), - Encoded = aeser_api_encoder:encode(transaction, SignedTX), + SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data), + Encoded = gmser_api_encoder:encode(transaction, SignedTX), Outcome = hz:post_tx(Encoded), tell("Outcome: ~p", [Outcome]). +do_list_keys(#s{selected = Selected, wallet = #wallet{poas = POAs}}) -> + {ok, Selected, [ID || #poa{id = ID} <- POAs]}; +do_list_keys(#s{wallet = none}) -> + error. + + do_nonce(ID) -> hz:next_nonce(ID). @@ -899,37 +978,39 @@ do_mnemonic(ID, #s{wallet = #wallet{keys = Keys}}) -> end. -do_deploy(Build, +do_deploy(CreatorID, + Build, InitArgs, - #s{selected = Index, wallet = #wallet{keys = Keys, chain_id = ChainID}}) -> - #key{pair = #{public := PubKey, secret := SecKey}} = lists:nth(Index, Keys), - case hz:contract_create_built(PubKey, Build, InitArgs) of + #s{wallet = #wallet{keys = Keys, chain_id = ChainID}}) -> + #key{pair = #{secret := SecKey}} = lists:keyfind(CreatorID, #key.id, Keys), + case hz:contract_create_built(CreatorID, Build, InitArgs) of {ok, CreateTX} -> do_deploy2(SecKey, CreateTX, ChainID); - Error -> Error + Error -> gmc_v_devman:trouble(Error) end. do_deploy2(SecKey, CreateTX, ChainID) -> SignedTX = sign_tx_hash(CreateTX, SecKey, ChainID), + tell(info, "SignedTX: ~p", [SignedTX]), case hz:post_tx(SignedTX) of {ok, Data = #{"tx_hash" := TXHash}} -> ok = tell("Contract deploy TX succeded with: ~p", [TXHash]), do_deploy3(Data); {ok, WTF} -> - {error, WTF}; + gmc_v_devman:trouble({error, WTF}); Error -> - Error + gmc_v_devman:trouble(Error) end. do_deploy3(#{"tx_hash" := TXHash}) -> case hz:tx_info(TXHash) of {ok, #{"call_info" := #{"return_type" := "ok", "contract_id" := ConID}}} -> - {contract_id, ConID}; + gmc_v_devman:open_contract(ConID); {error, "Tx not mined"} -> - {tx_hash, TXHash}; + gmc_v_devman:trouble({tx_hash, TXHash}); {ok, Reason = #{"call_info" := #{"return_type" := "revert"}}} -> - {error, Reason}; + gmc_v_devman:trouble({error, Reason}); Error -> - Error + gmc_v_devman:trouble(Error) end. @@ -1136,7 +1217,7 @@ do_close_wallet(State = #s{wallet = Current, wallets = Wallets, pass = Pass}) -> #wallet{name = Name} = Current, RW = lists:keyfind(Name, #wr.name, Wallets), ok = save_wallet(RW, Pass, Current), - State#s{pass = none, wallet = none}. + State#s{selected = 0, pass = none, wallet = none}. save_wallet(#wr{path = Path, pass = false}, none, Wallet) -> diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 7e2ba22..730e93e 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -765,7 +765,7 @@ spend2(#poa{id = ID, name = Name}, Nonce, Height, State = #s{frame = Frame, j = ?wxID_OK -> {ok, PK} = decode_account_id(ID), TX = - #spend_tx{sender_id = aeser_id:create(account, PK), + #spend_tx{sender_id = gmser_id:create(account, PK), recipient_id = wxTextCtrl:getValue(ToTx), amount = wxTextCtrl:getValue(AmtTx), gas_price = wxSlider:getValue(GasSl), @@ -784,7 +784,7 @@ clean_spend(_, #spend_tx{recipient_id = ""}) -> ok; clean_spend(ID, TX = #spend_tx{recipient_id = S}) when is_list(S) -> case decode_account_id(S) of - {ok, PK} -> clean_spend(ID, TX#spend_tx{recipient_id = aeser_id:create(account, PK)}); + {ok, PK} -> clean_spend(ID, TX#spend_tx{recipient_id = gmser_id:create(account, PK)}); Error -> tell("Decode recipient_id failed with: ~tp", [Error]) end; clean_spend(ID, TX = #spend_tx{amount = S}) when is_list(S) -> @@ -811,7 +811,7 @@ decode_account_id(S) when is_list(S) -> decode_account_id(list_to_binary(S)); decode_account_id(B) -> try - {account_pubkey, PK} = aeser_api_encoder:decode(B), + {account_pubkey, PK} = gmser_api_encoder:decode(B), {ok, PK} catch E:R -> {E, R} diff --git a/src/gmc_key_master.erl b/src/gmc_key_master.erl index f32a4bb..b259456 100644 --- a/src/gmc_key_master.erl +++ b/src/gmc_key_master.erl @@ -18,21 +18,21 @@ make_key("", <<>>) -> Pair = #{public := Public} = ecu_eddsa:sign_keypair(), - ID = aeser_api_encoder:encode(account_pubkey, Public), + 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 = aeser_api_encoder:encode(account_pubkey, Public), + 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 = aeser_api_encoder:encode(account_pubkey, Public), + 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 = aeser_api_encoder:encode(account_pubkey, Public), + ID = gmser_api_encoder:encode(account_pubkey, Public), #key{name = Name, id = ID, pair = Pair}. diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl index 30ed377..83b6269 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -8,7 +8,7 @@ %-behavior(gmc_v). -include_lib("wx/include/wx.hrl"). -export([to_front/1]). --export([set_manifest/1, trouble/1]). +-export([set_manifest/1, open_contract/1, call_result/2, dryrun_result/2, trouble/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]). @@ -30,17 +30,18 @@ % Code book pages -record(p, - {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, - win = none :: none | wx:wx_object(), - code = none :: none | wxStyledTextCtrl:wxStyledTextCtrl()}). + {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, + win = none :: none | wx:wx_object(), + code = none :: none | wxTextCtrl:wxTextCtrl()}). % Contract pages -record(c, - {id = "" :: string(), - win = none :: none | wx:wx_object(), - code = none :: none | wxStyledTextCtrl:wxStyledTextCtrl(), - cons = none :: none | wxStyledTextCtrl:wxStyledTextCtrl(), - funs = {#w{}, []} :: {#w{}, [#f{}]}}). + {id = <<"">> :: binary(), + win = none :: none | wx:wx_object(), + code = none :: none | wxTextCtrl:wxTextCtrl(), + cons = none :: none | wxTextCtrl:wxTextCtrl(), + build = none :: none | map(), + funs = {#w{}, []} :: {#w{}, [#f{}]}}). % State -record(s, @@ -65,8 +66,9 @@ to_front(Win) -> wx_object:cast(Win, to_front). +% TODO: Probably kill this -spec set_manifest(Entries) -> ok - when Entries :: [ael:conf_meta()]. + when Entries :: list(). set_manifest(Entries) -> case is_pid(whereis(?MODULE)) of @@ -75,6 +77,29 @@ set_manifest(Entries) -> end. +-spec open_contract(Address) -> ok + when Address :: string(). + +open_contract(Address) -> + wx_object:cast(?MODULE, {open_contract, Address}). + + +-spec call_result(ConID, CallInfo) -> ok + when ConID :: clutch:id(), + CallInfo :: map(). + +call_result(ConID, CallInfo) -> + wx_object:cast(?MODULE, {call_result, ConID, CallInfo}). + + +-spec dryrun_result(ConID, CallInfo) -> ok + when ConID :: clutch:id(), + CallInfo :: map(). + +dryrun_result(ConID, CallInfo) -> + wx_object:cast(?MODULE, {dryrun_result, ConID, CallInfo}). + + -spec trouble(Info) -> ok when Info :: term(). @@ -189,6 +214,15 @@ handle_call(Unexpected, From, State) -> handle_cast(to_front, State = #s{frame = Frame}) -> ok = wxFrame:raise(Frame), {noreply, State}; +handle_cast({open_contract, Address}, State) -> + NewState = load2(State, Address), + {noreply, NewState}; +handle_cast({call_result, ConID, CallInfo}, State) -> + ok = do_call_result(State, ConID, CallInfo), + {noreply, State}; +handle_cast({dryrun_result, ConID, CallInfo}, State) -> + ok = do_dryrun_result(State, ConID, CallInfo), + {noreply, State}; handle_cast({trouble, Info}, State) -> ok = handle_troubling(State, Info), {noreply, State}; @@ -207,16 +241,16 @@ handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, State = #s{buttons = Buttons}) -> NewState = case maps:get(ID, Buttons, undefined) of - #w{name = new} -> new_file(State); - #w{name = open} -> open(State); - #w{name = save} -> save(State); - #w{name = rename} -> rename(State); - #w{name = deploy} -> deploy(State); - #w{name = close_source} -> close_source(State); - #w{name = load} -> load(State); - #w{name = edit} -> edit(State); - #w{name = close_instance} -> close_instance(State); - #w{name = Name, wx = Button} -> clicked(State, Name, Button); + #w{name = new} -> new_file(State); + #w{name = open} -> open(State); + #w{name = save} -> save(State); + #w{name = rename} -> rename(State); + #w{name = deploy} -> deploy(State); + #w{name = close_source} -> close_source(State); + #w{name = load} -> load(State); + #w{name = edit} -> edit(State); + #w{name = close_instance} -> close_instance(State); + #w{name = Name} -> clicked(State, Name); undefined -> tell("Received message: ~w", [E]), State @@ -257,22 +291,124 @@ terminate(Reason, State) -> %%% Doers -clicked(State = #s{cons = {Consbook, Contracts}}, Name, Button) -> +clicked(State = #s{cons = {Consbook, Contracts}}, Name) -> case wxNotebook:getSelection(Consbook) of ?wxNOT_FOUND -> ok = tell(warning, "Inconcievable! No notebook page is selected!"), State; Index -> Contract = lists:nth(Index + 1, Contracts), - clicked(State, Contract, Name, Button) + clicked2(State, Contract, Name) end. -clicked(State, Contract, Name, Button) -> - ok = tell("Button: ~p ~p~nContract: ~p", [Button, Name, Contract]), +clicked2(State, Contract, Name) -> + case gmc_con:list_keys() of + {ok, 0, []} -> + handle_troubling(State, "No keys exist in the current wallet."); + {ok, Selected, Keys} -> + clicked3(State, Contract, Name, Selected, Keys); + error -> + handle_troubling(State, "No wallet is selected!") + end. + +clicked3(State = #s{frame = Frame, j = J}, Contract, Name, Selected, Keys) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Key")}]), + KeyPicker = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Keys}]), + _ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)), + ok = wxChoice:setSelection(KeyPicker, Selected - 1), + 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, KeySz, [{proportion, 0}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, ButtSz, [{proportion, 1}, {flag, ?wxEXPAND}]), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:center(Dialog), + Outcome = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)), + BinID = unicode:characters_to_binary(ID), + {ok, BinID}; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Outcome of + {ok, CallerID} -> clicked4(State, Contract, Name, CallerID); + cancel -> State + end. + +clicked4(State, + #c{id = ConID, build = #{aci := ACI}, funs = {_, Funs}}, + {Name, Type}, + PK) -> + AACI = hz:prepare_aaci(ACI), + #f{args = ArgFields} = lists:keyfind(Name, #f.name, Funs), + Args = lists:map(fun get_arg/1, ArgFields), + FunName = binary_to_list(Name), + {ok, Nonce} = hz:next_nonce(PK), + {ok, Height} = hz:top_height(), + TTL = Height + 10000, + GasP = hz:min_gas_price(), + Gas = 5000000, + Amount = 0, + case hz:contract_call(PK, Nonce, Gas, GasP, Amount, TTL, AACI, ConID, FunName, Args) of + {ok, UnsignedTX} -> + case Type of + call -> do_call(State, ConID, PK, UnsignedTX); + dryr -> do_dry_run(State, ConID, UnsignedTX) + end; + Error -> + handle_troubling(State, Error), + State + end. + +do_call(State, ConID, CallerID, UnsignedTX) -> + ok = gmc_con:sign_call(ConID, CallerID, UnsignedTX), State. -get_arg({_, TextCtrl, _}) -> - wxTextCtrl:getValue(TextCtrl). +do_dry_run(State, ConID, TX) -> + ok = gmc_con:dry_run(ConID, TX), + State. + + +do_call_result(#s{tabs = TopBook, cons = {Consbook, Contracts}}, ConID, CallInfo) -> + case lookup_contract(ConID, Contracts) of + {#c{cons = Console}, ZeroIndex} -> + _ = wxNotebook:changeSelection(TopBook, 1), + _ = wxNotebook:changeSelection(Consbook, ZeroIndex), + Out = io_lib:format("Call Result:~n~p~n~n", [CallInfo]), + wxTextCtrl:appendText(Console, Out); + error -> + tell(info, "Received result for ~p:~n~p ", [ConID, CallInfo]) + end. + + +do_dryrun_result(#s{tabs = TopBook, cons = {Consbook, Contracts}}, ConID, CallInfo) -> + case lookup_contract(ConID, Contracts) of + {#c{cons = Console}, ZeroIndex} -> + _ = wxNotebook:changeSelection(TopBook, 1), + _ = wxNotebook:changeSelection(Consbook, ZeroIndex), + Out = io_lib:format("Call Result:~n~p~n~n", [CallInfo]), + wxTextCtrl:appendText(Console, Out); + error -> + tell(info, "Received result for ~p:~n~p ", [ConID, CallInfo]) + end. + +lookup_contract(ConID, Contracts) -> + lookup_contract(ConID, Contracts, 0). + +lookup_contract(ConID, [Contract = #c{id = ConID} | _], I) -> + {Contract, I}; +lookup_contract(ConID, [#c{} | T], I) -> + lookup_contract(ConID, T, I + 1); +lookup_contract(_, [], _) -> + error. add_code_page(State = #s{code = {Codebook, Pages}}, File) -> @@ -303,7 +439,7 @@ add_code_page2(State = #s{j = J}, {file, File}) -> add_code_page2(State, {hash, Address}) -> open_hash2(State, Address). -add_code_page(State = #s{j = J, tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> +add_code_page(State = #s{tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> % FIXME: One of these days we need to define the text area as a wxStyledTextCtrl and will % have to contend with system theme issues (light/dark themese, namely) % Leaving this little thing here to remind myself how any of that works later. @@ -385,24 +521,35 @@ deploy(State = #s{code = {Codebook, Pages}}) -> ?wxNOT_FOUND -> State; Index -> - Page = #p{code = CodeTx} = lists:nth(Index + 1, Pages), + #p{code = CodeTx} = lists:nth(Index + 1, Pages), Source = wxTextCtrl:getValue(CodeTx), deploy2(State, Source) end. deploy2(State, Source) -> - case aeso_compiler:from_string(Source, [{aci, json}]) of - {ok, Build = #{aci := ACI}} -> - FunDefs = {#{functions := Funs}, _} = find_main(ACI), - Init = lom:find(name, <<"init">>, Funs), - ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), - deploy3(State, Init); + case compile(Source) of +% Options = sophia_options(), +% case so_compiler:from_string(Source, Options) of + {ok, Build} -> + deploy3(State, Build); Other -> ok = tell(info, "Compilation Failed!~n~tp", [Other]), State end. -deploy3(State = #s{frame = Frame, j = J}, #{arguments := As}) -> +deploy3(State, Build) -> + case gmc_con:list_keys() of + {ok, 0, []} -> + handle_troubling(State, "No keys exist in the current wallet."); + {ok, Selected, Keys} -> + deploy4(State, Build, Selected, Keys); + error -> + handle_troubling(State, "No wallet is selected!") + end. + +deploy4(State = #s{frame = Frame, j = J}, Build = #{aci := ACI}, Selected, Keys) -> + {#{functions := Funs}, _} = find_main(ACI), + #{arguments := As} = lom:find(name, <<"init">>, Funs), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")), Sizer = wxBoxSizer:new(?wxVERTICAL), ScrollWin = wxScrolledWindow:new(Dialog), @@ -410,7 +557,15 @@ deploy3(State = #s{frame = Frame, j = J}, #{arguments := As}) -> FunSizer = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSizer), ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), - ButtSz = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL), + KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Key")}]), + KeyPicker = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Keys}]), + _ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)), + ok = wxChoice:setSelection(KeyPicker, Selected - 1), + 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)), GridSz = wxFlexGridSizer:new(2, 4, 4), ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), @@ -433,117 +588,34 @@ deploy3(State = #s{frame = Frame, j = J}, #{arguments := As}) -> end, ArgFields = lists:map(MakeArgField, As), _ = wxStaticBoxSizer:add(FunSizer, GridSz, zxw:flags(wide)), - _ = wxSizer:add(Sizer, ScrollWin, zxw:flags(wide)), - _ = wxSizer:add(Sizer, ButtSz), + _ = wxSizer:add(Sizer, ScrollWin, [{proportion, 5}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, KeySz, [{proportion, 0}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, ButtSz, [{proportion, 1}, {flag, ?wxEXPAND}]), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 300}), ok = wxDialog:center(Dialog), - ok = + Outcome = case wxDialog:showModal(Dialog) of ?wxID_OK -> - tell(info, "DEPLOYING!"); + ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)), + BinID = unicode:characters_to_binary(ID), + Inputs = lists:map(fun get_arg/1, ArgFields), + {ok, BinID, Inputs}; ?wxID_CANCEL -> - ok + cancel end, ok = wxDialog:destroy(Dialog), + case Outcome of + {ok, SigID, Args} -> deploy5(State, SigID, Build, Args); + cancel -> State + end. + +deploy5(State, SigID, Build, Args) -> + tell(info, "Build: ~p", [Build]), + ok = gmc_con:deploy(SigID, Build, Args), State. -find_main(ACI) -> - find_main(ACI, none, []). - -find_main([#{contract := I = #{kind := contract_interface}} | T], M, Is) -> - find_main(T, M, [I | Is]); -find_main([#{contract := M = #{kind := contract_main}} | T], _, Is) -> - find_main(T, M, Is); -find_main([#{namespace := _} | T], M, Is) -> - find_main(T, M, Is); -find_main([C | T], M, Is) -> - ok = tell("Surprising ACI element: ~p", [C]), - find_main(T, M, Is); -find_main([], M, Is) -> - {M, Is}. - -fun_interfaces(Window, - Buttons, - {OldScrollWin, OldIfaces}, - {#{name := Name, functions := Funs}, _ConIfaces}, - J) -> - ok = wxScrolledWindow:destroy(OldScrollWin), - OldButtonIDs = button_key_list(OldIfaces), - NextButtons = maps:without(OldButtonIDs, Buttons), - ScrollWin = wxScrolledWindow:new(Window), - FSOpts = [{label, J("Function Interfaces")}], - FunSizer = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, FSOpts), - ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSizer), - ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), - ConName = wxStaticText:new(ScrollWin, ?wxID_ANY, Name), - _ = wxSizer:add(FunSizer, ConName), - MakeIface = - fun(#{name := N, arguments := As}) -> - FunName = unicode:characters_to_list([N, "/", integer_to_list(length(As))]), - FN = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), - GridSz = wxFlexGridSizer:new(2, 4, 4), - ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), - ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), - MakeArgField = - fun(#{name := AN, type := T}) -> - Type = - case T of - <<"address">> -> address; - <<"int">> -> integer; - <<"bool">> -> boolean; - L when is_list(L) -> list; % FIXME -% I when is_binary(I) -> iface % FIXME - I when is_binary(I) -> address % FIXME - end, - ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), - TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), - _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), - _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), - {ANT, TCT, Type} - end, - ArgFields = lists:map(MakeArgField, As), - ButtSz = wxBoxSizer:new(?wxHORIZONTAL), - {CallButton, DryRunButton} = - case N =:= <<"init">> of - false -> - CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]), - DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]), - true = wxButton:disable(CallBn), - true = wxButton:disable(DryRBn), - _ = wxBoxSizer:add(ButtSz, CallBn, zxw:flags(wide)), - _ = wxBoxSizer:add(ButtSz, DryRBn, zxw:flags(wide)), - {#w{name = {N, call}, id = wxButton:getId(CallBn), wx = CallBn}, - #w{name = {N, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}}; - true -> - Deploy = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Deploy")}]), - _ = wxBoxSizer:add(ButtSz, Deploy, zxw:flags(wide)), - {#w{name = {N, call}, id = wxButton:getId(Deploy), wx = Deploy}, - none} - end, - _ = wxStaticBoxSizer:add(FN, GridSz, zxw:flags(wide)), - _ = wxStaticBoxSizer:add(FN, ButtSz, zxw:flags(base)), - _ = wxSizer:add(FunSizer, FN, zxw:flags(base)), - #f{name = N, call = CallButton, dryrun = DryRunButton, args = ArgFields} - end, - Ifaces = lists:map(MakeIface, Funs), - NewButtons = lists:foldl(fun map_iface_buttons/2, NextButtons, Ifaces), - ok = wxSizer:layout(FunSizer), - {NewButtons, {ScrollWin, Ifaces}}. - -button_key_list([#f{call = #w{id = C}, dryrun = #w{id = D}} | T]) -> - [C, D | button_key_list(T)]; -button_key_list([#f{call = #w{id = C}, dryrun = none} | T]) -> - [C | button_key_list(T)]; -button_key_list([]) -> - []. - -map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) -> - maps:put(DID, D, maps:put(CID, C, A)); -map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = none}, A) -> - maps:put(CID, C, A). - open(State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Open Contract Source")), @@ -649,7 +721,7 @@ open_hash(State = #s{frame = Frame, j = J}) -> ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 200}), ok = wxDialog:center(Dialog), - ok = wxStyledTextCtrl:setFocus(AddressTx), + ok = wxTextCtrl:setFocus(AddressTx), Choice = case wxDialog:showModal(Dialog) of ?wxID_OK -> @@ -677,7 +749,8 @@ open_hash2(State, Address) -> open_hash3(State, Address, Source) -> % TODO: Compile on load and verify the deployed hash for validity. - case aeso_compiler:from_string(Source, [{aci, json}]) of + Options = sophia_options(), + case so_compiler:from_string(Source, Options) of {ok, Build = #{aci := ACI}} -> {Defs = #{functions := Funs}, ConIfaces} = find_main(ACI), Callable = lom:delete(name, <<"init">>, Funs), @@ -698,7 +771,7 @@ save(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) Index -> case lists:nth(Index + 1, Pages) of #p{path = {file, Path}, code = Widget} -> - Source = wxStyledTextCtrl:getText(Widget), + Source = wxTextCtrl:getValue(Widget), case filelib:ensure_dir(Path) of ok -> case file:write_file(Path, Source) of @@ -864,7 +937,7 @@ load(State = #s{frame = Frame, j = J}) -> ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 200}), ok = wxDialog:center(Dialog), - ok = wxStyledTextCtrl:setFocus(AddressTx), + ok = wxTextCtrl:setFocus(AddressTx), Choice = case wxDialog:showModal(Dialog) of ?wxID_OK -> @@ -890,26 +963,12 @@ load2(State, Address) -> State end. -load3(State, Address, Source) -> - % TODO: Compile on load and verify the deployed hash for validity. - case aeso_compiler:from_string(Source, [{aci, json}]) of - {ok, Build = #{aci := ACI}} -> - {Defs = #{functions := Funs}, ConIfaces} = find_main(ACI), - Callable = lom:delete(name, <<"init">>, Funs), - FunDefs = {maps:put(functions, Callable, Defs), ConIfaces}, - ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), - add_instance_page(State, Address, Source); - Other -> - ok = tell(info, "Compilation Failed!~n~tp", [Other]), - State - end. - -add_instance_page(State = #s{cons = {Consbook, Pages}, j = J}, Address, Source) -> +load3(State = #s{tabs = TopBook, cons = {Consbook, Pages}, buttons = Buttons, j = J}, + Address, + Source) -> Window = wxWindow:new(Consbook, ?wxID_ANY), PageSz = wxBoxSizer:new(?wxVERTICAL), - ProgSz = wxBoxSizer:new(?wxHORIZONTAL), - CodeSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Contract Source")}]), CodeTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_PROCESS_TAB @@ -922,52 +981,175 @@ add_instance_page(State = #s{cons = {Consbook, Pages}, j = J}, Address, Source) true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), ok = wxTextCtrl:setValue(CodeTx, Source), _ = wxSizer:add(CodeSz, CodeTx, zxw:flags(wide)), - ScrollWin = wxScrolledWindow:new(Window), FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Function Interfaces")}]), - ok = wxWindow:setSizer(ScrollWin, FunSz), - + ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSz), + ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), ConsSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Console")}]), ConsTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}, ConsTx = wxTextCtrl:new(Window, ?wxID_ANY, [ConsTxStyle]), _ = wxSizer:add(ConsSz, ConsTx, zxw:flags(wide)), - - _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 3}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 2}, {flag, ?wxEXPAND}]), _ = wxSizer:add(ProgSz, ScrollWin, [{proportion, 1}, {flag, ?wxEXPAND}]), - _ = wxSizer:add(PageSz, ProgSz, [{proportion, 3}, {flag, ?wxEXPAND}]), _ = wxSizer:add(PageSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND}]), - + {Out, IFaces, Build, NewButtons} = + case compile(Source) of + {ok, B = #{aci := ACI}} -> + {#{functions := Fs}, _} = find_main(ACI), + Callable = lom:delete(name, <<"init">>, Fs), + {NB, IFs} = fun_interfaces(ScrollWin, FunSz, Buttons, Callable, J), + O = io_lib:format("Compilation Succeeded!~n~tp~n~nDone!~n", [B]), + {O, IFs, B, NB}; + Other -> + O = io_llib:format("Compilation Failed!~n~tp~n", [Other]), + {O, [], none, Buttons} + end, ok = wxWindow:setSizer(Window, PageSz), ok = wxSizer:layout(PageSz), true = wxNotebook:addPage(Consbook, Window, Address, [{bSelect, true}]), - Page = #c{id = Address, win = Window, - code = CodeTx, cons = ConsTx, - funs = {ScrollWin, []}}, + Page = #c{id = Address, win = Window, + code = CodeTx, cons = ConsTx, + build = Build, funs = {ScrollWin, IFaces}}, NewPages = Pages ++ [Page], - State#s{cons = {Consbook, NewPages}}. + ok = wxTextCtrl:appendText(ConsTx, Out), + _ = wxNotebook:changeSelection(TopBook, 1), + % TODO: Verify the deployed hash for validity. + State#s{cons = {Consbook, NewPages}, buttons = NewButtons}. -edit(State) -> - ok = tell(info, "EDIT clicked"), - State. +get_arg({_, TextCtrl, _}) -> + wxTextCtrl:getValue(TextCtrl). + +find_main(ACI) -> + find_main(ACI, none, []). + +find_main([#{contract := I = #{kind := contract_interface}} | T], M, Is) -> + find_main(T, M, [I | Is]); +find_main([#{contract := M = #{kind := contract_main}} | T], _, Is) -> + find_main(T, M, Is); +find_main([#{namespace := _} | T], M, Is) -> + find_main(T, M, Is); +find_main([C | T], M, Is) -> + ok = tell("Surprising ACI element: ~p", [C]), + find_main(T, M, Is); +find_main([], M, Is) -> + {M, Is}. + +fun_interfaces(ScrollWin, FunSz, Buttons, Funs, J) -> + MakeIface = + fun(#{name := N, arguments := As}) -> + FunName = unicode:characters_to_list([N, "/", integer_to_list(length(As))]), + FN = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), + GridSz = wxFlexGridSizer:new(2, 4, 4), + ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), + ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), + MakeArgField = + fun(#{name := AN, type := T}) -> + Type = + case T of + <<"address">> -> address; + <<"int">> -> integer; + <<"bool">> -> boolean; + L when is_list(L) -> list; % FIXME +% I when is_binary(I) -> iface % FIXME + I when is_binary(I) -> address % FIXME + end, + ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), + TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), + _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), + _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), + {ANT, TCT, Type} + end, + ArgFields = lists:map(MakeArgField, As), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + {CallButton, DryRunButton} = + case N =:= <<"init">> of + false -> + CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]), + DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]), + _ = wxBoxSizer:add(ButtSz, CallBn, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, DryRBn, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(CallBn), wx = CallBn}, + #w{name = {N, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}}; + true -> + Deploy = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Deploy")}]), + _ = wxBoxSizer:add(ButtSz, Deploy, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(Deploy), wx = Deploy}, + none} + end, + _ = wxStaticBoxSizer:add(FN, GridSz, zxw:flags(wide)), + _ = wxStaticBoxSizer:add(FN, ButtSz, zxw:flags(base)), + _ = wxSizer:add(FunSz, FN, zxw:flags(base)), + #f{name = N, call = CallButton, dryrun = DryRunButton, args = ArgFields} + end, + IFaces = lists:map(MakeIface, Funs), + NewButtons = lists:foldl(fun map_iface_buttons/2, Buttons, IFaces), + {NewButtons, IFaces}. + +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) -> + maps:put(DID, D, maps:put(CID, C, A)); +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = none}, A) -> + maps:put(CID, C, A). -close_instance(State) -> - ok = tell(info, "CLOSE_INSTANCE clicked"), - State. +edit(State = #s{cons = {Consbook, Pages}}) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + State; + Index -> + #c{code = CodeTx} = lists:nth(Index + 1, Pages), + Address = wxNotebook:getPageText(Consbook, Index), + Source = wxTextCtrl:getValue(CodeTx), + add_code_page(State, {hash, Address}, Source) + end. +close_instance(State = #s{cons = {Consbook, Pages}, buttons = Buttons}) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + State; + Index -> + {#c{funs = {_, IFaces}}, NewPages} = take_nth(Index + 1, Pages), + IDs = list_iface_buttons(IFaces), + NewButtons = maps:without(IDs, Buttons), + true = wxNotebook:deletePage(Consbook, Index), + State#s{cons = {Consbook, NewPages}, buttons = NewButtons} + end. + +list_iface_buttons(IFaces) -> + lists:foldl(fun list_iface_buttons/2, [], IFaces). + +list_iface_buttons(#f{call = #w{id = CID}, dryrun = #w{id = DID}}, A) -> + [CID, DID | A]. + + + +%% Incomplete compiler wrangling + +compile(Source) -> + Options = sophia_options(), + so_compiler:from_string(Source, Options). + +sophia_options() -> + [{aci, json}]. + %% (Somewhat silly) Data operations store_nth(1, E, [_ | T]) -> [E | T]; -store_nth(N, E, [H | T]) -> [H | store_nth(N - 1, T, E)]. +store_nth(N, E, [H | T]) -> [H | store_nth(N - 1, E, T)]. drop_nth(1, [_ | T]) -> T; drop_nth(N, [H | T]) -> [H | drop_nth(N - 1, T)]. +take_nth(N, L) -> + take_nth(N, L, []). + +take_nth(1, [E | T], A) -> {E, lists:reverse(A) ++ T}; +take_nth(N, [H | T], A) -> take_nth(N - 1, T, [H | A]). + keyfind_index(K, E, L) -> keyfind_index(K, E, 1, L). diff --git a/zomp.meta b/zomp.meta index 7a03bb0..4e63379 100644 --- a/zomp.meta +++ b/zomp.meta @@ -2,15 +2,15 @@ {type,gui}. {modules,[]}. {prefix,"gmc"}. -{desc,"A desktop client for the Gajumaru network of blockchain networks"}. {author,"Craig Everett"}. +{desc,"A desktop client for the Gajumaru network of blockchain networks"}. {package_id,{"otpr","clutch",{0,2,0}}}. -{deps,[{"otpr","lom",{1,0,0}}, - {"otpr","hakuzaru",{0,2,0}}, - {"otpr","aesophia",{8,0,1}}, - {"otpr","aeserialization",{0,1,2}}, +{deps,[{"otpr","sophia",{9,0,0}}, + {"otpr","hakuzaru",{0,3,0}}, + {"otpr","gmbytecode",{3,4,1}}, + {"otpr","lom",{1,0,0}}, + {"otpr","gmserialization",{0,1,2}}, {"otpr","zj",{1,1,0}}, - {"otpr","aebytecode",{3,2,1}}, {"otpr","erl_base58",{0,1,0}}, {"otpr","eblake2",{1,0,0}}, {"otpr","ec_utils",{1,0,0}}, -- 2.30.2 From b0dfe86fbcf58949ab32a9181bc4ec41d4012223 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Tue, 25 Feb 2025 16:33:54 +0900 Subject: [PATCH 09/10] Verup --- ebin/clutch.app | 2 +- src/: | 2 +- src/clutch.erl | 2 +- src/gmc_con.erl | 2 +- src/gmc_grids.erl | 2 +- src/gmc_gui.erl | 2 +- src/gmc_jt.erl | 2 +- src/gmc_key_master.erl | 2 +- src/gmc_sup.erl | 2 +- src/gmc_v.erl | 2 +- src/gmc_v_devman.erl | 2 +- src/gmc_v_netman.erl | 2 +- src/gmc_v_wallman.erl | 2 +- zomp.meta | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ebin/clutch.app b/ebin/clutch.app index f6d168b..2dc3bab 100644 --- a/ebin/clutch.app +++ b/ebin/clutch.app @@ -3,7 +3,7 @@ {registered,[]}, {included_applications,[]}, {applications,[stdlib,kernel,sasl,ssl]}, - {vsn,"0.2.0"}, + {vsn,"0.3.0"}, {modules,[clutch,gmc_con,gmc_grids,gmc_gui,gmc_jt, gmc_key_master,gmc_sup,gmc_v,gmc_v_netman, gmc_v_wallman]}, diff --git a/src/: b/src/: index 15b8cf4..8a37adc 100644 --- a/src/: +++ b/src/: @@ -1,5 +1,5 @@ -module(gmc_v_devman). --vsn("0.2.0"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/clutch.erl b/src/clutch.erl index 4ebecdc..7f689f1 100644 --- a/src/clutch.erl +++ b/src/clutch.erl @@ -3,7 +3,7 @@ %%% @end -module(clutch). --vsn("0.2.0"). +-vsn("0.3.0"). -behavior(application). -author("Craig Everett "). -copyright("QPQ AG "). diff --git a/src/gmc_con.erl b/src/gmc_con.erl index f3fa634..1d03026 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -3,7 +3,7 @@ %%% @end -module(gmc_con). --vsn("0.2.0"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_grids.erl b/src/gmc_grids.erl index 2f58d7c..f921706 100644 --- a/src/gmc_grids.erl +++ b/src/gmc_grids.erl @@ -37,7 +37,7 @@ %%% @end -module(gmc_grids). --vsn("0.2.0"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 730e93e..4af6e5f 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -3,7 +3,7 @@ %%% @end -module(gmc_gui). --vsn("0.2.0"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_jt.erl b/src/gmc_jt.erl index f58cc76..6815e5e 100644 --- a/src/gmc_jt.erl +++ b/src/gmc_jt.erl @@ -15,7 +15,7 @@ %%% translation library is retained). -module(gmc_jt). --vsn("0.2.0"). +-vsn("0.3.0"). -export([read_translations/1, j/2, oneshot_j/2]). diff --git a/src/gmc_key_master.erl b/src/gmc_key_master.erl index b259456..f692caf 100644 --- a/src/gmc_key_master.erl +++ b/src/gmc_key_master.erl @@ -8,7 +8,7 @@ %%% @end -module(gmc_key_master). --vsn("0.2.0"). +-vsn("0.3.0"). -export([make_key/2, encode/1, decode/1]). diff --git a/src/gmc_sup.erl b/src/gmc_sup.erl index 9e541cd..40f58d6 100644 --- a/src/gmc_sup.erl +++ b/src/gmc_sup.erl @@ -12,7 +12,7 @@ %%% @end -module(gmc_sup). --vsn("0.2.0"). +-vsn("0.3.0"). -behaviour(supervisor). -author("Craig Everett "). -copyright("QPQ AG "). diff --git a/src/gmc_v.erl b/src/gmc_v.erl index 9065f12..b4933be 100644 --- a/src/gmc_v.erl +++ b/src/gmc_v.erl @@ -1,5 +1,5 @@ -module(gmc_v). --vsn("0.2.0"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl index 83b6269..818e250 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -1,5 +1,5 @@ -module(gmc_v_devman). --vsn("0.2.0"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_v_netman.erl b/src/gmc_v_netman.erl index 4b0e662..f3f0dfa 100644 --- a/src/gmc_v_netman.erl +++ b/src/gmc_v_netman.erl @@ -1,5 +1,5 @@ -module(gmc_v_netman). --vsn("0.2.0"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gmc_v_wallman.erl b/src/gmc_v_wallman.erl index 03dcee5..3ff978b 100644 --- a/src/gmc_v_wallman.erl +++ b/src/gmc_v_wallman.erl @@ -1,5 +1,5 @@ -module(gmc_v_wallman). --vsn("0.2.0"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/zomp.meta b/zomp.meta index 4e63379..cd216b8 100644 --- a/zomp.meta +++ b/zomp.meta @@ -4,7 +4,7 @@ {prefix,"gmc"}. {author,"Craig Everett"}. {desc,"A desktop client for the Gajumaru network of blockchain networks"}. -{package_id,{"otpr","clutch",{0,2,0}}}. +{package_id,{"otpr","clutch",{0,3,0}}}. {deps,[{"otpr","sophia",{9,0,0}}, {"otpr","hakuzaru",{0,3,0}}, {"otpr","gmbytecode",{3,4,1}}, -- 2.30.2 From 52a9b1fed64616354ef06b9002c4cfacceec236c Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Tue, 25 Feb 2025 16:35:07 +0900 Subject: [PATCH 10/10] Remove trash file --- src/: | 1029 --------------------------------------------------------- 1 file changed, 1029 deletions(-) delete mode 100644 src/: diff --git a/src/: b/src/: deleted file mode 100644 index 8a37adc..0000000 --- a/src/: +++ /dev/null @@ -1,1029 +0,0 @@ --module(gmc_v_devman). --vsn("0.3.0"). --author("Craig Everett "). --copyright("QPQ AG "). --license("GPL-3.0-or-later"). - --behavior(wx_object). -%-behavior(gmc_v). --include_lib("wx/include/wx.hrl"). --export([to_front/1]). --export([set_manifest/1, open_contract/1, trouble/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("gmc.hrl"). - -% Widgets --record(w, - {name = none :: atom() | {FunName :: binary(), call | dryr}, - id = 0 :: integer(), - wx = none :: none | wx:wx_object()}). - -% Contract functions in an ACI --record(f, - {name = <<"">> :: binary(), - call = #w{} :: #w{}, - dryrun = #w{} :: none | #w{}, - args = [] :: [{wx:wx_object(), wx:wx_object(), argt()}]}). - -% Code book pages --record(p, - {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, - win = none :: none | wx:wx_object(), - code = none :: none | wxTextCtrl:wxTextCtrl()}). - -% Contract pages --record(c, - {id = "" :: string(), - win = none :: none | wx:wx_object(), - code = none :: none | wxTextCtrl:wxTextCtrl(), - cons = none :: none | wxTextCtrl:wxTextCtrl(), - build = none :: none | map(), - funs = {#w{}, []} :: {#w{}, [#f{}]}}). - -% 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(), - buttons = #{} :: #{WX_ID :: integer() := #w{}}, - tabs = none :: none | wx:wx_object(), - code = {none, []} :: {Codebook :: none | wx:wx_object(), Pages :: [#p{}]}, - cons = {none, []} :: {Consbook :: none | wx:wx_object(), Pages :: [#c{}]}}). - --type argt() :: int | string | address | list(argt()). - -%%% Interface - --spec to_front(Win) -> ok - when Win :: wx:wx_object(). - -to_front(Win) -> - wx_object:cast(Win, to_front). - - -% TODO: Probably kill this --spec set_manifest(Entries) -> ok - when Entries :: list(). - -set_manifest(Entries) -> - case is_pid(whereis(?MODULE)) of - true -> wx_object:cast(?MODULE, {set_manifest, Entries}); - false -> ok - end. - - --spec open_contract(Address) -> ok - when Address :: string(). - -open_contract(Address) -> - wx_object:cast(?MODULE, {open_contract, Address}). - - --spec trouble(Info) -> ok - when Info :: term(). - -trouble(Info) -> - wx_object:cast(?MODULE, {trouble, Info}). - - - - -%%% Startup Functions - -start_link(Args) -> - wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []). - - -init({Prefs, Manifest}) -> - Lang = maps:get(lang, Prefs, en_us), - Trans = gmc_jt:read_translations(?MODULE), - J = gmc_jt:j(Lang, Trans), - Wx = wx:new(), - Frame = wxFrame:new(Wx, ?wxID_ANY, J("Contracts")), - - MainSz = wxBoxSizer:new(?wxVERTICAL), - TopBook = wxNotebook:new(Frame, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), - {LWin, LButtons, Codebook} = make_l_win(TopBook, J), - {RWin, RButtons, Consbook} = make_r_win(TopBook, J), - ButtonMap = maps:merge(LButtons, RButtons), - true = wxNotebook:addPage(TopBook, LWin, J("Contract Editor"), []), - true = wxNotebook:addPage(TopBook, RWin, J("Deployed Contracts"), []), - State = - #s{wx = Wx, frame = Frame, - j = J, prefs = Prefs, - buttons = ButtonMap, tabs = TopBook, - code = {Codebook, []}, cons = {Consbook, []}}, - _ = wxSizer:add(MainSz, TopBook, zxw:flags(wide)), - _ = wxFrame:setSizer(Frame, MainSz), - _ = wxSizer:layout(MainSz), - ok = gmc_v:safe_size(Frame, Prefs), - ok = wxFrame:connect(Frame, close_window), - ok = wxFrame:connect(Frame, command_button_clicked), - ok = wxFrame:center(Frame), - true = wxFrame:show(Frame), - NewState = add_code_pages(State, Manifest), - {Frame, NewState}. - - -make_l_win(TopBook, J) -> - Win = wxWindow:new(TopBook, ?wxID_ANY), - MainSz = wxBoxSizer:new(?wxVERTICAL), - ButtSz = wxBoxSizer:new(?wxHORIZONTAL), - ButtonTemplates = - [{new, J("New")}, - {open, J("Open")}, - {save, J("Save")}, - {rename, J("Save (rename)")}, - {deploy, J("Deploy")}, - {close_source, J("Close")}], - MakeButton = - fun({Name, Label}) -> - B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), - #w{name = Name, id = wxButton:getId(B), wx = B} - end, - Buttons = lists:map(MakeButton, ButtonTemplates), - AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, - Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), - ok = lists:foreach(AddButton, Buttons), - _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), - _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), - _ = wxWindow:setSizer(Win, MainSz), - MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, - ButtonMap = lists:foldl(MapButton, #{}, Buttons), - {Win, ButtonMap, Codebook}. - -make_r_win(TopBook, J) -> - Win = wxWindow:new(TopBook, ?wxID_ANY), - MainSz = wxBoxSizer:new(?wxVERTICAL), - ButtSz = wxBoxSizer:new(?wxHORIZONTAL), - ButtonTemplates = - [{load, J("Load from Chain")}, - {edit, J("Copy to Editor")}, - {close_instance, J("Close")}], - MakeButton = - fun({Name, Label}) -> - B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), - #w{name = Name, id = wxButton:getId(B), wx = B} - end, - Buttons = lists:map(MakeButton, ButtonTemplates), - AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, - Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), - ok = lists:foreach(AddButton, Buttons), - _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), - _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), - _ = wxWindow:setSizer(Win, MainSz), - MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, - ButtonMap = lists:foldl(MapButton, #{}, Buttons), - {Win, ButtonMap, Codebook}. - - - -add_code_pages(State, Files) -> - lists:foldl(fun add_code_page/2, State, Files). - - - -%%% OTP callbacks - -handle_call(Unexpected, From, State) -> - ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), - {noreply, State}. - - -handle_cast(to_front, State = #s{frame = Frame}) -> - ok = wxFrame:raise(Frame), - {noreply, State}; -handle_cast({open_contract, Address}, State) -> - NewState = load2(State, Address), - {noreply, NewState}; -handle_cast({trouble, Info}, State) -> - ok = handle_troubling(State, Info), - {noreply, State}; -handle_cast(Unexpected, State) -> - ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), - {noreply, State}. - - -handle_info(Unexpected, State) -> - ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), - {noreply, State}. - - -handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, - id = ID}, - State = #s{buttons = Buttons}) -> - NewState = - case maps:get(ID, Buttons, undefined) of - #w{name = new} -> new_file(State); - #w{name = open} -> open(State); - #w{name = save} -> save(State); - #w{name = rename} -> rename(State); - #w{name = deploy} -> deploy(State); - #w{name = close_source} -> close_source(State); - #w{name = load} -> load(State); - #w{name = edit} -> edit(State); - #w{name = close_instance} -> close_instance(State); - #w{name = Name, wx = Button} -> clicked(State, Name, Button); - undefined -> - tell("Received message: ~w", [E]), - State - end, - {noreply, NewState}; -handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) -> - Geometry = - case wxTopLevelWindow:isMaximized(Frame) of - true -> - max; - false -> - {X, Y} = wxWindow:getPosition(Frame), - {W, H} = wxWindow:getSize(Frame), - {X, Y, W, H} - end, - NewPrefs = maps:put(geometry, Geometry, Prefs), - ok = gmc_con:save(?MODULE, NewPrefs), - ok = wxWindow:destroy(Frame), - {noreply, State}; -handle_event(Event, State) -> - ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), - {noreply, State}. - - -handle_troubling(#s{frame = Frame}, Info) -> - zxw:show_message(Frame, Info). - - -code_change(_, State, _) -> - {ok, State}. - - -terminate(Reason, State) -> - ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), - wx:destroy(). - - - -%%% Doers - -clicked(State = #s{cons = {Consbook, Contracts}}, Name, Button) -> - case wxNotebook:getSelection(Consbook) of - ?wxNOT_FOUND -> - ok = tell(warning, "Inconcievable! No notebook page is selected!"), - State; - Index -> - Contract = lists:nth(Index + 1, Contracts), - clicked(State, Contract, Name, Button) - end. - -clicked(State, Contract, Name, Button) -> - ok = tell("Button: ~p ~p~nContract: ~p", [Button, Name, Contract]), - State. - - -add_code_page(State = #s{code = {Codebook, Pages}}, File) -> - case keyfind_index({file, File}, #p.path, Pages) of - error -> - add_code_page2(State, File); - {ok, Index} -> - _ = wxNotebook:setSelection(Codebook, Index - 1), - State - end. - -add_code_page2(State = #s{j = J}, {file, File}) -> - case file:read_file(File) of - {ok, Bin} -> - case unicode:characters_to_list(Bin) of - Code when is_list(Code) -> - add_code_page(State, {file, File}, Code); - Error -> - Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Error]), - ok = handle_troubling(Message, State), - State - end; - {error, Reason} -> - Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Reason]), - ok = handle_troubling(Message, State), - State - end; -add_code_page2(State, {hash, Address}) -> - open_hash2(State, Address). - -add_code_page(State = #s{tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> -% FIXME: One of these days we need to define the text area as a wxStyledTextCtrl and will -% have to contend with system theme issues (light/dark themese, namely) -% Leaving this little thing here to remind myself how any of that works later. -% The call below returns a wx_color4() type (not that we need alpha...). -% Color = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW), -% tell("Color: ~p", [Color]), - Window = wxWindow:new(Codebook, ?wxID_ANY), - PageSz = wxBoxSizer:new(?wxHORIZONTAL), - - CodeTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_PROCESS_TAB bor ?wxTE_DONTWRAP}, - CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), - TextAt = wxTextAttr:new(), - Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), - ok = wxTextAttr:setFont(TextAt, Mono), - true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), - ok = wxTextCtrl:setValue(CodeTx, Code), - - _ = wxSizer:add(PageSz, CodeTx, zxw:flags(wide)), - - ok = wxWindow:setSizer(Window, PageSz), - ok = wxSizer:layout(PageSz), - _ = wxNotebook:changeSelection(TopBook, 0), - FileName = - case Location of - {file, Path} -> filename:basename(Path); - {hash, Addr} -> Addr - end, - true = wxNotebook:addPage(Codebook, Window, FileName, [{bSelect, true}]), - Page = #p{path = Location, win = Window, code = CodeTx}, - NewPages = Pages ++ [Page], - State#s{code = {Codebook, NewPages}}. - - -new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> - DefaultDir = - case maps:find(dir, Prefs) of - {ok, PrefDir} -> - PrefDir; - error -> - case os:getenv("ZOMP_DIR") of - "" -> file:get_pwd(); - D -> filename:basename(D) - end - end, - Options = - [{message, J("Save Location")}, - {defaultDir, DefaultDir}, - {defaultFile, "my_contract.aes"}, - {wildCard, "*.aes"}, - {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], - Dialog = wxFileDialog:new(Frame, Options), - NewState = - case wxFileDialog:showModal(Dialog) of - ?wxID_OK -> - Dir = wxFileDialog:getDirectory(Dialog), - case wxFileDialog:getFilename(Dialog) of - "" -> - State; - Name -> - File = - case filename:extension(Name) of - ".aes" -> Name; - _ -> Name ++ ".aes" - end, - Path = filename:join(Dir, File), - NewPrefs = maps:put(dir, Dir, Prefs), - NextState = State#s{prefs = NewPrefs}, - add_code_page(NextState, {file, Path}, "") - end; - ?wxID_CANCEL -> - State - end, - ok = wxFileDialog:destroy(Dialog), - NewState. - - -deploy(State = #s{code = {Codebook, Pages}}) -> - case wxNotebook:getSelection(Codebook) of - ?wxNOT_FOUND -> - State; - Index -> - #p{code = CodeTx} = lists:nth(Index + 1, Pages), - Source = wxTextCtrl:getValue(CodeTx), - deploy2(State, Source) - end. - -deploy2(State, Source) -> - case so_compiler:from_string(Source, [{aci, json}]) of - {ok, Build} -> - deploy3(State, Build); - Other -> - ok = tell(info, "Compilation Failed!~n~tp", [Other]), - State - end. - -deploy3(State, Build) -> - case gmc_con:list_keys() of - {ok, 0, []} -> - handle_troubling(State, "No keys exist in the current wallet."); - {ok, Selected, Keys} -> - deploy4(State, Build, Selected, Keys); - error -> - handle_troubling(State, "No wallet is selected!") - end. - -deploy4(State = #s{frame = Frame, j = J}, Build = #{aci := ACI}, Selected, Keys) -> - {#{functions := Funs}, _} = find_main(ACI), - #{arguments := As} = lom:find(name, <<"init">>, Funs), - Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")), - Sizer = wxBoxSizer:new(?wxVERTICAL), - ScrollWin = wxScrolledWindow:new(Dialog), - FunName = unicode:characters_to_list(["init/", integer_to_list(length(As))]), - FunSizer = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), - ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSizer), - ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), - KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Key")}]), - KeyPicker = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Keys}]), - _ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)), - ok = wxChoice:setSelection(KeyPicker, Selected - 1), - 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)), - GridSz = wxFlexGridSizer:new(2, 4, 4), - ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), - ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), - MakeArgField = - fun(#{name := AN, type := T}) -> - Type = - case T of - <<"address">> -> address; - <<"int">> -> integer; - <<"bool">> -> boolean; - L when is_list(L) -> list; % FIXME -% I when is_binary(I) -> iface % FIXME - I when is_binary(I) -> address % FIXME - end, - ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), - TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), - _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), - _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), - {ANT, TCT, Type} - end, - ArgFields = lists:map(MakeArgField, As), - _ = wxStaticBoxSizer:add(FunSizer, GridSz, zxw:flags(wide)), - _ = wxSizer:add(Sizer, ScrollWin, [{proportion, 5}, {flag, ?wxEXPAND}]), - _ = wxSizer:add(Sizer, KeySz, [{proportion, 0}, {flag, ?wxEXPAND}]), - _ = wxSizer:add(Sizer, ButtSz, [{proportion, 1}, {flag, ?wxEXPAND}]), - ok = wxDialog:setSizer(Dialog, Sizer), - ok = wxBoxSizer:layout(Sizer), - ok = wxDialog:setSize(Dialog, {500, 300}), - ok = wxDialog:center(Dialog), - Outcome = - case wxDialog:showModal(Dialog) of - ?wxID_OK -> - ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)), - BinID = unicode:characters_to_binary(ID), - Inputs = lists:map(fun get_arg/1, ArgFields), - {ok, BinID, Inputs}; - ?wxID_CANCEL -> - cancel - end, - ok = wxDialog:destroy(Dialog), - case Outcome of - {ok, SigID, Args} -> deploy5(State, SigID, Build, Args); - cancel -> State - end. - -deploy5(State, SigID, Build, Args) -> - tell(info, "Build: ~p", [Build]), - ok = gmc_con:deploy(SigID, Build, Args), - State. - - -open(State = #s{frame = Frame, j = J}) -> - Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Open Contract Source")), - Sizer = wxBoxSizer:new(?wxVERTICAL), - Choices = wxRadioBox:new(Dialog, - ?wxID_ANY, - J("Select Origin"), - ?wxDefaultPosition, - ?wxDefaultSize, - [J("From File"), J("From Hash")], - [{majorDim, 1}]), - 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, Choices, zxw:flags(wide)), - _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), - ok = wxDialog:setSizer(Dialog, Sizer), - ok = wxDialog:setSize(Dialog, {250, 170}), - ok = wxBoxSizer:layout(Sizer), - Choice = - case wxDialog:showModal(Dialog) of - ?wxID_OK -> - case wxRadioBox:getSelection(Choices) of - 0 -> file; - 1 -> hash; - ?wxNOT_FOUND -> none - end; - ?wxID_CANCEL -> cancel - end, - ok = wxDialog:destroy(Dialog), - case Choice of - file -> - open_file(State); - hash -> - open_hash(State); - none -> - ok = tell(info, "No selection."), - State; - cancel -> - ok = tell(info, "Cancelled."), - State - end. - - -open_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> - DefaultDir = - case maps:find(dir, Prefs) of - {ok, PrefDir} -> - PrefDir; - error -> - case os:getenv("ZOMP_DIR") of - "" -> file:get_pwd(); - D -> filename:basename(D) - end - end, - Options = - [{message, J("Load Contract Source")}, - {defaultDir, DefaultDir}, - {wildCard, "*.aes"}, - {style, ?wxFD_OPEN}], - Dialog = wxFileDialog:new(Frame, Options), - NewState = - case wxFileDialog:showModal(Dialog) of - ?wxID_OK -> - Dir = wxFileDialog:getDirectory(Dialog), - case wxFileDialog:getFilename(Dialog) of - "" -> - State; - Name -> - File = - case filename:extension(Name) of - ".aes" -> Name; - _ -> Name ++ ".aes" - end, - Path = filename:join(Dir, File), - NewPrefs = maps:put(dir, Dir, Prefs), - NextState = State#s{prefs = NewPrefs}, - add_code_page(NextState, {file, Path}) - end; - ?wxID_CANCEL -> - State - end, - ok = wxFileDialog:destroy(Dialog), - NewState. - - -open_hash(State = #s{frame = Frame, j = J}) -> - Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), - Sizer = wxBoxSizer:new(?wxVERTICAL), - AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), - AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), - _ = wxSizer:add(AddressSz, AddressTx, 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)), - _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), - _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), - ok = wxDialog:setSizer(Dialog, Sizer), - ok = wxBoxSizer:layout(Sizer), - ok = wxDialog:setSize(Dialog, {500, 200}), - ok = wxDialog:center(Dialog), - ok = wxStyledTextCtrl:setFocus(AddressTx), - Choice = - case wxDialog:showModal(Dialog) of - ?wxID_OK -> - case wxTextCtrl:getValue(AddressTx) of - "" -> cancel; - A -> {ok, A} - end; - ?wxID_CANCEL -> - cancel - end, - ok = wxDialog:destroy(Dialog), - case Choice of - {ok, Address} -> open_hash2(State, Address); - cancel -> State - end. - -open_hash2(State, Address) -> - case hz:contract_source(Address) of - {ok, Source} -> - open_hash3(State, Address, Source); - Error -> - ok = handle_troubling(Error, State), - State - end. - -open_hash3(State, Address, Source) -> - % TODO: Compile on load and verify the deployed hash for validity. - case so_compiler:from_string(Source, [{aci, json}]) of - {ok, Build = #{aci := ACI}} -> - {Defs = #{functions := Funs}, ConIfaces} = find_main(ACI), - Callable = lom:delete(name, <<"init">>, Funs), - FunDefs = {maps:put(functions, Callable, Defs), ConIfaces}, - ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), - add_code_page(State, {hash, Address}, Source); - Other -> - ok = tell(info, "Compilation Failed!~n~tp", [Other]), - State - end. - - -% TODO: Break this down -- tons of things in here recur. -save(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) -> - case wxNotebook:getSelection(Codebook) of - ?wxNOT_FOUND -> - State; - Index -> - case lists:nth(Index + 1, Pages) of - #p{path = {file, Path}, code = Widget} -> - Source = wxStyledTextCtrl:getText(Widget), - case filelib:ensure_dir(Path) of - ok -> - case file:write_file(Path, Source) of - ok -> - State; - Error -> - ok = handle_troubling(State, Error), - State - end; - Error -> - ok = handle_troubling(State, Error), - State - end; - Page = #p{path = {hash, Hash}, code = Widget} -> - DefaultDir = - case maps:find(dir, Prefs) of - {ok, PrefDir} -> - PrefDir; - error -> - case os:getenv("ZOMP_DIR") of - "" -> file:get_pwd(); - D -> filename:basename(D) - end - end, - Options = - [{message, J("Save Location")}, - {defaultDir, DefaultDir}, - {defaultFile, unicode:characters_to_list([Hash, ".aes"])}, - {wildCard, "*.aes"}, - {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], - Dialog = wxFileDialog:new(Frame, Options), - NewState = - case wxFileDialog:showModal(Dialog) of - ?wxID_OK -> - Dir = wxFileDialog:getDirectory(Dialog), - case wxFileDialog:getFilename(Dialog) of - "" -> - State; - Name -> - File = - case filename:extension(Name) of - ".aes" -> Name; - _ -> Name ++ ".aes" - end, - Path = filename:join(Dir, File), - Source = wxTextCtrl:getValue(Widget), - case filelib:ensure_dir(Path) of - ok -> - case file:write_file(Path, Source) of - ok -> - true = wxNotebook:setPageText(Codebook, Index, File), - NewPrefs = maps:put(dir, Dir, Prefs), - NewPage = Page#p{path = {file, Path}}, - NewPages = store_nth(Index + 1, NewPage, Pages), - NewCode = {Codebook, NewPages}, - State#s{prefs = NewPrefs, code = NewCode}; - Error -> - ok = handle_troubling(State, Error), - State - end; - Error -> - ok = handle_troubling(State, Error), - State - end - end; - ?wxID_CANCEL -> - State - end, - ok = wxFileDialog:destroy(Dialog), - NewState - end - end. - -% TODO: Break this down -- tons of things in here recur. -rename(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) -> - case wxNotebook:getSelection(Codebook) of - ?wxNOT_FOUND -> - State; - Index -> - case lists:nth(Index + 1, Pages) of - Page = #p{path = {file, Path}, code = Widget} -> - DefaultDir = filename:dirname(Path), - Options = - [{message, J("Save Location")}, - {defaultDir, DefaultDir}, - {defaultFile, filename:basename(Path)}, - {wildCard, "*.aes"}, - {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], - Dialog = wxFileDialog:new(Frame, Options), - NewState = - case wxFileDialog:showModal(Dialog) of - ?wxID_OK -> - Dir = wxFileDialog:getDirectory(Dialog), - case wxFileDialog:getFilename(Dialog) of - "" -> - State; - Name -> - File = - case filename:extension(Name) of - ".aes" -> Name; - _ -> Name ++ ".aes" - end, - NewPath = filename:join(Dir, File), - Source = wxTextCtrl:getValue(Widget), - case filelib:ensure_dir(NewPath) of - ok -> - case file:write_file(NewPath, Source) of - ok -> - true = wxNotebook:setPageText(Codebook, Index, File), - NewPrefs = maps:put(dir, Dir, Prefs), - NewPage = Page#p{path = {file, NewPath}}, - NewPages = store_nth(Index + 1, NewPage, Pages), - NewCode = {Codebook, NewPages}, - State#s{prefs = NewPrefs, code = NewCode}; - Error -> - ok = handle_troubling(State, Error), - State - end; - Error -> - ok = handle_troubling(State, Error), - State - end - end; - ?wxID_CANCEL -> - State - end, - ok = wxFileDialog:destroy(Dialog), - NewState; - #p{path = {hash, _}} -> - save(State) - end - end. - - -close_source(State = #s{code = {Codebook, Pages}}) -> - case wxNotebook:getSelection(Codebook) of - ?wxNOT_FOUND -> - State; - Index -> - NewPages = drop_nth(Index + 1, Pages), - true = wxNotebook:deletePage(Codebook, Index), - State#s{code = {Codebook, NewPages}} - end. - - -load(State = #s{frame = Frame, j = J}) -> - % TODO: Extract the exact compiler version, load it, and use only that or fail if - % the specific version is unavailable. - % TODO: Compile on load and verify the deployed hash for validity. - Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), - Sizer = wxBoxSizer:new(?wxVERTICAL), - AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), - AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), - _ = wxSizer:add(AddressSz, AddressTx, 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)), - _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), - _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), - ok = wxDialog:setSizer(Dialog, Sizer), - ok = wxBoxSizer:layout(Sizer), - ok = wxDialog:setSize(Dialog, {500, 200}), - ok = wxDialog:center(Dialog), - ok = wxStyledTextCtrl:setFocus(AddressTx), - Choice = - case wxDialog:showModal(Dialog) of - ?wxID_OK -> - case wxTextCtrl:getValue(AddressTx) of - "" -> cancel; - A -> {ok, A} - end; - ?wxID_CANCEL -> - cancel - end, - ok = wxDialog:destroy(Dialog), - case Choice of - {ok, Address} -> load2(State, Address); - cancel -> State - end. - -load2(State, Address) -> - case hz:contract_source(Address) of - {ok, Source} -> - load3(State, Address, Source); - Error -> - ok = handle_troubling(Error, State), - State - end. - -load3(State = #s{tabs = TopBook, cons = {Consbook, Pages}, buttons = Buttons, j = J}, - Address, - Source) -> - Window = wxWindow:new(Consbook, ?wxID_ANY), - PageSz = wxBoxSizer:new(?wxVERTICAL), - ProgSz = wxBoxSizer:new(?wxHORIZONTAL), - CodeSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Contract Source")}]), - CodeTxStyle = {style, ?wxTE_MULTILINE - bor ?wxTE_PROCESS_TAB - bor ?wxTE_DONTWRAP - bor ?wxTE_READONLY}, - CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), - Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), - TextAt = wxTextAttr:new(), - ok = wxTextAttr:setFont(TextAt, Mono), - true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), - ok = wxTextCtrl:setValue(CodeTx, Source), - _ = wxSizer:add(CodeSz, CodeTx, zxw:flags(wide)), - ScrollWin = wxScrolledWindow:new(Window), - FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Function Interfaces")}]), - ok = wxWindow:setSizer(ScrollWin, FunSz), - ConsSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Console")}]), - ConsTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}, - ConsTx = wxTextCtrl:new(Window, ?wxID_ANY, [ConsTxStyle]), - _ = wxSizer:add(ConsSz, ConsTx, zxw:flags(wide)), - _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 3}, {flag, ?wxEXPAND}]), - _ = wxSizer:add(ProgSz, ScrollWin, [{proportion, 1}, {flag, ?wxEXPAND}]), - _ = wxSizer:add(PageSz, ProgSz, [{proportion, 3}, {flag, ?wxEXPAND}]), - _ = wxSizer:add(PageSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND}]), - {Out, IFaces, Build, NewButtons} = - case so_compiler:from_string(Source, [{aci, json}]) of - {ok, B = #{aci := ACI}} -> - {#{functions := Fs}, _} = find_main(ACI), - Callable = lom:delete(name, <<"init">>, Fs), - {NB, IFs} = fun_interfaces(ScrollWin, FunSz, Buttons, Callable, J), - O = io_lib:format("Compilation Succeeded!~n~tp~n~nDone!~n", [B]), - {O, IFs, B, NB}; - Other -> - O = io_llib:format("Compilation Failed!~n~tp~n", [Other]), - {O, [], none, Buttons} - end, - ok = wxWindow:setSizer(Window, PageSz), - ok = wxSizer:layout(PageSz), - true = wxNotebook:addPage(Consbook, Window, Address, [{bSelect, true}]), - Page = #c{id = Address, win = Window, - code = CodeTx, cons = ConsTx, - build = Build, funs = {ScrollWin, IFaces}}, - NewPages = Pages ++ [Page], - ok = wxTextCtrl:appendText(ConsTx, Out), - _ = wxNotebook:changeSelection(TopBook, 1), - % TODO: Verify the deployed hash for validity. - State#s{cons = {Consbook, NewPages}, buttons = NewButtons}. - - -get_arg({_, TextCtrl, _}) -> - wxTextCtrl:getValue(TextCtrl). - -find_main(ACI) -> - find_main(ACI, none, []). - -find_main([#{contract := I = #{kind := contract_interface}} | T], M, Is) -> - find_main(T, M, [I | Is]); -find_main([#{contract := M = #{kind := contract_main}} | T], _, Is) -> - find_main(T, M, Is); -find_main([#{namespace := _} | T], M, Is) -> - find_main(T, M, Is); -find_main([C | T], M, Is) -> - ok = tell("Surprising ACI element: ~p", [C]), - find_main(T, M, Is); -find_main([], M, Is) -> - {M, Is}. - -fun_interfaces(ScrollWin, FunSz, Buttons, Funs, J) -> - MakeIface = - fun(#{name := N, arguments := As}) -> - FunName = unicode:characters_to_list([N, "/", integer_to_list(length(As))]), - FN = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), - GridSz = wxFlexGridSizer:new(2, 4, 4), - ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), - ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), - MakeArgField = - fun(#{name := AN, type := T}) -> - Type = - case T of - <<"address">> -> address; - <<"int">> -> integer; - <<"bool">> -> boolean; - L when is_list(L) -> list; % FIXME -% I when is_binary(I) -> iface % FIXME - I when is_binary(I) -> address % FIXME - end, - ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), - TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), - _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), - _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), - {ANT, TCT, Type} - end, - ArgFields = lists:map(MakeArgField, As), - ButtSz = wxBoxSizer:new(?wxHORIZONTAL), - {CallButton, DryRunButton} = - case N =:= <<"init">> of - false -> - CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]), - DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]), - _ = wxBoxSizer:add(ButtSz, CallBn, zxw:flags(wide)), - _ = wxBoxSizer:add(ButtSz, DryRBn, zxw:flags(wide)), - {#w{name = {N, call}, id = wxButton:getId(CallBn), wx = CallBn}, - #w{name = {N, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}}; - true -> - Deploy = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Deploy")}]), - _ = wxBoxSizer:add(ButtSz, Deploy, zxw:flags(wide)), - {#w{name = {N, call}, id = wxButton:getId(Deploy), wx = Deploy}, - none} - end, - _ = wxStaticBoxSizer:add(FN, GridSz, zxw:flags(wide)), - _ = wxStaticBoxSizer:add(FN, ButtSz, zxw:flags(base)), - _ = wxSizer:add(FunSz, FN, zxw:flags(base)), - #f{name = N, call = CallButton, dryrun = DryRunButton, args = ArgFields} - end, - IFaces = lists:map(MakeIface, Funs), - NewButtons = lists:foldl(fun map_iface_buttons/2, Buttons, IFaces), - {NewButtons, IFaces}. - -button_key_list([#f{call = #w{id = C}, dryrun = #w{id = D}} | T]) -> - [C, D | button_key_list(T)]; -button_key_list([#f{call = #w{id = C}, dryrun = none} | T]) -> - [C | button_key_list(T)]; -button_key_list([]) -> - []. - -map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) -> - maps:put(DID, D, maps:put(CID, C, A)); -map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = none}, A) -> - maps:put(CID, C, A). - - -edit(State = #s{cons = {Consbook, Pages}}) -> - case wxNotebook:getSelection(Consbook) of - ?wxNOT_FOUND -> - State; - Index -> - #c{code = CodeTx} = lists:nth(Index + 1, Pages), - Address = wxNotebook:getPageText(Consbook, Index), - Source = wxTextCtrl:getValue(CodeTx), - add_code_page(State, {hash, Address}, Source) - end. - - -close_instance(State = #s{cons = {Consbook, Pages}, buttons = Buttons}) -> - case wxNotebook:getSelection(Consbook) of - ?wxNOT_FOUND -> - State; - Index -> - {#c{funs = {_, IFaces}}, NewPages} = take_nth(Index + 1, Pages), - IDs = list_iface_buttons(IFaces), - NewButtons = maps:without(IDs, Buttons), - true = wxNotebook:deletePage(Consbook, Index), - State#s{cons = {Consbook, NewPages}, buttons = NewButtons} - end. - -list_iface_buttons(IFaces) -> - lists:foldl(fun list_iface_buttons/2, [], IFaces). - -list_iface_buttons(#f{call = #w{id = CID}, dryrun = #w{id = DID}}, A) -> - [CID, DID | A]. - - - -%% (Somewhat silly) Data operations - -store_nth(1, E, [_ | T]) -> [E | T]; -store_nth(N, E, [H | T]) -> [H | store_nth(N - 1, E, T)]. - - -drop_nth(1, [_ | T]) -> T; -drop_nth(N, [H | T]) -> [H | drop_nth(N - 1, T)]. - -take_nth(N, L) -> - take_nth(N, L, []). - -take_nth(1, [E | T], A) -> {E, lists:reverse(A) ++ T}; -take_nth(N, [H | T], A) -> take_nth(N - 1, T, [H | A]). - - -keyfind_index(K, E, L) -> - keyfind_index(K, E, 1, L). - -keyfind_index(K, E, I, [H | T]) -> - case element(E, H) =:= K of - false -> keyfind_index(K, E, I + 1, T); - true -> {ok, I} - end; -keyfind_index(_, _, _, []) -> - error. -- 2.30.2