diff --git a/ebin/clutch.app b/ebin/clutch.app index cd50d62..2dc3bab 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.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/include/gmc.hrl b/include/gmc.hrl index c2cab3f..261f9cb 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, @@ -75,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..7f689f1 100644 --- a/src/clutch.erl +++ b/src/clutch.erl @@ -3,7 +3,7 @@ %%% @end -module(clutch). --vsn("0.1.4"). +-vsn("0.3.0"). -behavior(application). -author("Craig Everett "). -copyright("QPQ AG "). @@ -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() -> @@ -55,9 +57,9 @@ 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), + ok = application:ensure_started(sophia), gmc_sup:start_link(). diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 1c8eae7..1d03026 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -1,11 +1,9 @@ %%% @doc -%%% Clutch Controller -%%% -%%% This process is a in charge of maintaining the program's state. +%%% GajuDesk Controller %%% @end -module(gmc_con). --vsn("0.1.4"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). @@ -13,10 +11,12 @@ -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, - 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]). @@ -38,13 +38,14 @@ mon = none :: none | reference()}). -record(s, - {version = 1 :: integer(), - window = none :: none | wx:wx_object(), - tasks = [] :: [#ui{}], - wallet = none :: none | #wallet{}, - pass = none :: none | binary(), - prefs = #{} :: #{module() := term()}, - wallets = [] :: [#wr{}]}). + {version = 1 :: integer(), + window = none :: none | wx:wx_object(), + tasks = [] :: [#ui{}], + selected = 0 :: non_neg_integer(), + wallet = none :: none | #wallet{}, + pass = none :: none | binary(), + prefs = #{} :: #{module() := term()}, + wallets = [] :: [#wr{}]}). -type state() :: #s{}. @@ -104,6 +105,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 +170,35 @@ sign_tx(Request) -> gen_server:cast(?MODULE, {sign_tx, Request}). +-spec sign_call(ConID, PubKey, TX) -> ok + when ConID :: clutch:id(), + PubKey :: clutch:id(), + TX :: binary(). + +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 when Type :: {eddsa, ed25519}, Size :: 256, @@ -211,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{}. @@ -262,6 +307,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), @@ -295,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 +373,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), @@ -337,6 +386,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}; @@ -358,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}; @@ -587,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}) @@ -612,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 -> @@ -646,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}) @@ -670,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 = @@ -683,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, @@ -725,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 = @@ -736,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). @@ -790,6 +895,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, @@ -870,6 +978,42 @@ do_mnemonic(ID, #s{wallet = #wallet{keys = Keys}}) -> end. +do_deploy(CreatorID, + Build, + InitArgs, + #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 -> 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} -> + gmc_v_devman:trouble({error, WTF}); + 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}}} -> + gmc_v_devman:open_contract(ConID); + {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_rename_key(ID, NewName, State = #s{wallet = W}) -> #wallet{poas = POAs, keys = Keys} = W, A = lists:keyfind(ID, #poa.id, POAs), @@ -1073,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) -> @@ -1115,7 +1259,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_grids.erl b/src/gmc_grids.erl index db63290..f921706 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.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 66f1c05..4af6e5f 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -1,13 +1,9 @@ %%% @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 +%%% GajuDesk GUI %%% @end -module(gmc_gui). --vsn("0.1.4"). +-vsn("0.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). @@ -62,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() -> @@ -239,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), @@ -321,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}. @@ -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} @@ -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) -> @@ -924,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_jt.erl b/src/gmc_jt.erl index 510f277..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.1.4"). +-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 1f92aef..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.1.4"). +-vsn("0.3.0"). -export([make_key/2, encode/1, decode/1]). @@ -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_sup.erl b/src/gmc_sup.erl index 0bece53..40f58d6 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.3.0"). -behaviour(supervisor). -author("Craig Everett "). -copyright("QPQ AG "). @@ -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.erl b/src/gmc_v.erl index 31c0457..b4933be 100644 --- a/src/gmc_v.erl +++ b/src/gmc_v.erl @@ -1,5 +1,5 @@ -module(gmc_v). --vsn("0.1.4"). +-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 c552dd8..818e250 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -1,6 +1,6 @@ -module(gmc_v_devman). --vsn("0.1.4"). --author("Craig Everett "). +-vsn("0.3.0"). +-author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). @@ -8,35 +8,42 @@ %-behavior(gmc_v). -include_lib("wx/include/wx.hrl"). -export([to_front/1]). --export([set_manifest/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]). -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(), - 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()}}). + {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, + win = none :: none | wx:wx_object(), + code = none :: none | wxTextCtrl:wxTextCtrl()}). +% Contract pages +-record(c, + {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, {wx = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(), @@ -44,7 +51,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()). @@ -57,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 @@ -67,6 +77,36 @@ 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(). + +trouble(Info) -> + wx_object:cast(?MODULE, {trouble, Info}). + + %%% Startup Functions @@ -83,133 +123,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 +214,18 @@ 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}; handle_cast(Unexpected, State) -> ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. @@ -234,15 +237,20 @@ 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(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 @@ -267,6 +275,10 @@ handle_event(Event, State) -> {noreply, State}. +handle_troubling(#s{frame = Frame}, Info) -> + zxw:show_message(Frame, Info). + + code_change(_, State, _) -> {ok, State}. @@ -279,29 +291,186 @@ terminate(Reason, State) -> %%% Doers -clicked(State = #s{book = {Notebook, Pages}}, Name) -> - case wxNotebook:getSelection(Notebook) of +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 -> - Page = lists:nth(Index + 1, Pages), - clicked(State, Page, Name) + Contract = lists:nth(Index + 1, Contracts), + clicked2(State, Contract, Name) end. -clicked(State, #p{instances = Is, funs = {_, Funs}, builds = Builds}, {<<"init">>, call}) -> - BuildLabel = wxChoice:getStringSelection(Is), - Build = maps:get(BuildLabel, 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; -clicked(State, Page, Name) -> - ok = tell("Button: ~p~nPage: ~p", [Name, Page]), +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({_, InputField, _}) -> - wxTextCtrl:getValue(InputField). +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) -> + 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}) -> @@ -338,7 +507,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 @@ -347,139 +516,150 @@ 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), + #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) -> - 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}; +deploy2(State, Source) -> + case compile(Source) of +% Options = sophia_options(), +% case so_compiler:from_string(Source, Options) of + {ok, Build} -> + deploy3(State, Build); 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, 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. -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), +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), - 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} + 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, - Ifaces = lists:map(MakeIface, Funs), - NewButtons = lists:foldl(fun map_iface_buttons/2, NextButtons, Ifaces), - ok = wxSizer:layout(FunSizer), - {NewButtons, {ScrollWin, Ifaces}}. + 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. -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([]) -> - []. +deploy5(State, SigID, Build, Args) -> + tell(info, "Build: ~p", [Build]), + ok = gmc_con:deploy(SigID, Build, Args), + State. -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")), + 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}) -> @@ -515,7 +695,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 @@ -524,20 +704,452 @@ 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 = wxTextCtrl: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. + 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), + 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 = wxTextCtrl:getValue(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 = wxTextCtrl: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 = 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, 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, + 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}. + +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]. + + + +%% 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, 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/src/gmc_v_netman.erl b/src/gmc_v_netman.erl index d94c1bf..f3f0dfa 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.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 214ac8e..3ff978b 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.3.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). @@ -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 d22e4e5..cd216b8 100644 --- a/zomp.meta +++ b/zomp.meta @@ -2,14 +2,15 @@ {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,4}}}. -{deps,[{"otpr","hakuzaru",{0,2,0}}, - {"otpr","aesophia",{8,0,1}}, - {"otpr","aeserialization",{0,1,2}}, +{desc,"A desktop client for the Gajumaru network of blockchain networks"}. +{package_id,{"otpr","clutch",{0,3,0}}}. +{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}},