-module(gd_v_call). -vsn("0.8.1"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). -behavior(wx_object). %-behavior(gd_v). -include_lib("wx/include/wx.hrl"). -export([to_front/1, tx_hash/1, tx_data/1, tx_info/1]). -export([start_link/1]). -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). -include("$zx_include/zx_logger.hrl"). -include("gd.hrl"). -include("gdl.hrl"). % State -record(s, {wx = none :: none | wx:wx_object(), frame = none :: none | wx:wx_object(), lang = en :: en | jp, j = none :: none | fun(), prefs = #{} :: map(), con_id = <<"">> :: binary(), fundef = none :: none | fun_def(), funret = none :: none | term(), % FIXME build = none :: none | map(), args = [] :: [#w{}], kp = #w{} :: #w{}, params = [] :: [param()], return = #w{} :: #w{}, % wxTextCtrl, single-line copy = #w{} :: #w{}, status = none :: status(), action = #w{} :: #w{}, tx_data = none :: none | map(), tx_info = none :: none | map(), hash = #w{} :: #w{}, % wxTextCtrl, single-line info = #w{} :: #w{}}). % wxTextCtrl, multi-line -type fun_name() :: string(). -type fun_ilk() :: call | dryr | init. -type fun_def() :: {fun_name(), fun_ilk()}. -type param() :: {Label :: string(), Check :: fun(), #w{}}. -type status() :: none | submitted | rejected | included. %%% Interface -spec to_front(Win) -> ok when Win :: wx:wx_object(). to_front(Win) -> wx_object:cast(Win, to_front). -spec tx_hash(Win) -> Result when Win :: pid() | wx:wx_object(), Result :: none | string(). tx_hash(Win) -> wx_object:call(Win, tx_hash). -spec tx_data(Win) -> Result when Win :: pid() | wx:wx_object(), Result :: none | map(). tx_data(Win) -> wx_object:call(Win, tx_data). -spec tx_info(Win) -> Result when Win :: pid() | wx:wx_object(), Result :: none | map(). tx_info(Win) -> wx_object:call(Win, tx_info). %%% Startup Functions start_link(Args) -> wx_object:start_link(?MODULE, Args, []). init({Prefs, FunDef = {FunName, FunIlk}, ConID, Build, Selected, Keys}) -> Lang = maps:get(lang, Prefs, en), Trans = gd_jt:read_translations(?MODULE), J = gd_jt:j(Lang, Trans), {aaci, ConName, FunSpecs, _} = maps:get(aaci, Build), FunSpec = {FunArgs, FunReturn} = maps:get(FunName, FunSpecs), {CallTypeLabel, ActionLabel} = case FunIlk of call -> {J("Contract Call"), J("Submit Call")}; dryr -> {J("Dry Run"), J("Submit Dry Run")}; init -> {J("Deploy"), J("Deploy")} end, Arity = integer_to_list(length(FunArgs)), Title = [CallTypeLabel, ": ", ConName, ".", FunName, "/", Arity], Wx = wx:new(), Frame = wxFrame:new(Wx, ?wxID_ANY, Title), MainSz = wxBoxSizer:new(?wxVERTICAL), KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Signature Key")}]), KeyBox = wxStaticBoxSizer:getStaticBox(KeySz), KeyPicker = wxChoice:new(KeyBox, ?wxID_ANY, [{choices, Keys}]), KP = #w{name = key_picker, id = wxChoice:getId(KeyPicker), wx = KeyPicker}, ZeroBasedSelected = Selected - 1, ok = wxChoice:setSelection(KeyPicker, ZeroBasedSelected), _ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)), {ArgSz, Args, Return, Copy, HasArgs} = call_arg_sizer(Frame, J, FunIlk, FunSpec), {ParamSz, Params} = call_param_sizer(Frame, J), Action = #w{wx = ActionBn} = gd_lib:button(Frame, ActionLabel), TX_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Transaction Info")}]), TX_Sz_Box = wxStaticBoxSizer:getStaticBox(TX_Sz), Single = [{style, ?wxTE_READONLY}], Multi = [{style, ?wxTE_MULTILINE bor ?wxTE_READONLY}], TX_Hash = #w{wx = HashT} = gd_lib:mono_text(TX_Sz_Box, tx_hash, "", Single), TX_Info = #w{wx = InfoT} = gd_lib:mono_text(TX_Sz_Box, tx_info, "", Multi), _ = wxStaticBoxSizer:add(TX_Sz, HashT, zxw:flags({base, 5})), _ = wxStaticBoxSizer:add(TX_Sz, InfoT, zxw:flags({wide, 5})), ArgSzArgs = case HasArgs of true -> [{proportion, 2}, {flag, ?wxEXPAND bor ?wxALL}, {border, 5}]; false -> zxw:flags({base, 5}) end, _ = wxSizer:add(MainSz, ArgSz, ArgSzArgs), _ = wxSizer:add(MainSz, KeySz, zxw:flags({base, 5})), _ = wxSizer:add(MainSz, ParamSz, zxw:flags({base, 5})), _ = wxSizer:add(MainSz, ActionBn, zxw:flags({base, 5})), _ = wxSizer:add(MainSz, TX_Sz, [{proportion, 1}, {flag, ?wxEXPAND bor ?wxALL}, {border, 5}]), _ = wxFrame:setSizer(Frame, MainSz), _ = wxFrame:setSize(Frame, {900, 900}), _ = wxSizer:layout(MainSz), ok = wxFrame:connect(Frame, close_window), ok = wxFrame:connect(Frame, command_button_clicked), true = wxFrame:show(Frame), State = #s{wx = Wx, frame = Frame, j = J, prefs = Prefs, fundef = FunDef, funret = FunReturn, con_id = ConID, build = Build, args = Args, kp = KP, params = Params, return = Return, copy = Copy, action = Action, status = none, hash = TX_Hash, info = TX_Info}, {Frame, State}. call_arg_sizer(Frame, J, FunIlk, {CallArgs, ReturnType}) -> SpecSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Function Spec")}]), SpecBox = wxStaticBoxSizer:getStaticBox(SpecSz), {CallSz, CallControls, HasArgs} = call_sizer(SpecBox, J, CallArgs), {ReturnSz, Return, Copy} = return_sizer(SpecBox, J, FunIlk, ReturnType), _ = wxStaticBoxSizer:add(SpecSz, CallSz, zxw:flags({wide, 5})), _ = wxStaticBoxSizer:add(SpecSz, ReturnSz, zxw:flags({base, 5})), {SpecSz, CallControls, Return, Copy, HasArgs}. call_sizer(Parent, J, []) -> CallSz = wxStaticBoxSizer:new(?wxVERTICAL, Parent, [{label, J("Call Args")}]), CallBox = wxStaticBoxSizer:getStaticBox(CallSz), Args = wxStaticText:new(CallBox, ?wxID_ANY, ["[", J("No Args"), "]"]), _ = wxStaticBoxSizer:add(CallSz, Args, zxw:flags({wide, 5})), {CallSz, [], false}; call_sizer(Parent, J, CallArgs) -> CallSz = wxStaticBoxSizer:new(?wxVERTICAL, Parent, [{label, J("Call Args")}]), CallBox = wxStaticBoxSizer:getStaticBox(CallSz), ScrollWin = wxScrolledWindow:new(CallBox), ScrollSz = wxBoxSizer:new(?wxVERTICAL), ok = wxScrolledWindow:setSizerAndFit(ScrollWin, ScrollSz), ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), GridSz = wxFlexGridSizer:new(2, [{gap, {4, 4}}]), ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), _ = wxStaticBoxSizer:add(ScrollSz, GridSz, zxw:flags(wide)), _ = wxStaticBoxSizer:add(CallSz, ScrollWin, zxw:flags(wide)), AddArg = fun({Name, Type}) -> L = wxStaticText:new(ScrollWin, ?wxID_ANY, [Name, " : ", textify(Type)]), C = #w{wx = T} = gd_lib:mono_text(ScrollWin, Name), _ = wxFlexGridSizer:add(GridSz, L, zxw:flags({base, 5})), _ = wxFlexGridSizer:add(GridSz, T, zxw:flags({wide, 5})), C end, Controls = lists:map(AddArg, CallArgs), {CallSz, Controls, true}. return_sizer(Parent, J, FunIlk, ReturnType) -> IlkLabel = case FunIlk =:= init of false -> textify(ReturnType); true -> J("Contract Address") end, ReturnSz = wxStaticBoxSizer:new(?wxVERTICAL, Parent, [{label, J("Return Type")}]), ReturnSzBox = wxStaticBoxSizer:getStaticBox(ReturnSz), ReturnLabel = wxStaticText:new(ReturnSzBox, ?wxID_ANY, IlkLabel), Single = [{style, ?wxTE_READONLY}], Return = #w{wx = ReturnTx} = gd_lib:mono_text(ReturnSzBox, return, "", Single), Copy = #w{wx = CopyB} = gd_lib:button(ReturnSzBox, J("Copy")), _ = wxButton:disable(CopyB), _ = wxStaticBoxSizer:add(ReturnSz, ReturnLabel, zxw:flags({wide, 5})), _ = wxStaticBoxSizer:add(ReturnSz, ReturnTx, zxw:flags({wide, 5})), _ = wxStaticBoxSizer:add(ReturnSz, CopyB, zxw:flags({wide, 5})), {ReturnSz, Return, Copy}. call_param_sizer(Frame, J) -> {ok, Height} = hz:top_height(), DefTTL = Height + 10000, DefGasP = hz:min_gas_price(), DefGas = 5000000, DefAmount = 0, ParamSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("TX Parameters")}]), ParamBox = wxStaticBoxSizer:getStaticBox(ParamSz), GridSz = wxFlexGridSizer:new(2, 4, 4), ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), Amount_L = wxStaticText:new(ParamBox, ?wxID_ANY, J("Amount")), Amount_T = wxTextCtrl:new(ParamBox, ?wxID_ANY), ok = wxTextCtrl:setValue(Amount_T, integer_to_list(DefAmount)), Gas_L = wxStaticText:new(ParamBox, ?wxID_ANY, J("Gas")), Gas_T = wxTextCtrl:new(ParamBox, ?wxID_ANY), ok = wxTextCtrl:setValue(Gas_T, integer_to_list(DefGas)), GasP_L = wxStaticText:new(ParamBox, ?wxID_ANY, J("Gas Price")), GasP_T = wxTextCtrl:new(ParamBox, ?wxID_ANY), ok = wxTextCtrl:setValue(GasP_T, integer_to_list(DefGasP)), TTL_L = wxStaticText:new(ParamBox, ?wxID_ANY, "TTL"), TTL_T = wxTextCtrl:new(ParamBox, ?wxID_ANY), ok = wxTextCtrl:setValue(TTL_T, integer_to_list(DefTTL)), _ = wxFlexGridSizer:add(GridSz, Amount_L, zxw:flags({base, 5})), _ = wxFlexGridSizer:add(GridSz, Amount_T, zxw:flags({wide, 5})), _ = wxFlexGridSizer:add(GridSz, Gas_L, zxw:flags({base, 5})), _ = wxFlexGridSizer:add(GridSz, Gas_T, zxw:flags({wide, 5})), _ = wxFlexGridSizer:add(GridSz, GasP_L, zxw:flags({base, 5})), _ = wxFlexGridSizer:add(GridSz, GasP_T, zxw:flags({wide, 5})), _ = wxFlexGridSizer:add(GridSz, TTL_L, zxw:flags({base, 5})), _ = wxFlexGridSizer:add(GridSz, TTL_T, zxw:flags({wide, 5})), _ = wxSizer:add(ParamSz, GridSz, zxw:flags(wide)), Params = [{J("TX Amount"), fun gte_0/1, Amount_T} , {J("Gas") , fun gt_0/1, Gas_T}, {J("Gas Price"), fun gt_0/1, GasP_T}, { "TTL", fun gte_0/1, TTL_T}], {ParamSz, Params}. %%% Spine handle_call(tx_hash, _, State) -> TXHash = do_tx_hash(State), {reply, TXHash, State}; handle_call(tx_data, _, State = #s{tx_data = TXData}) -> {reply, TXData, State}; handle_call(tx_info, _, State = #s{tx_info = TXInfo}) -> {reply, TXInfo, State}; handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. handle_cast(Unexpected, State) -> ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. handle_info(retire, State) -> retire(State); handle_info(Unexpected, State) -> ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), {noreply, State}. handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{action = #w{id = ID}, status = none}) -> NewState = prep_call(State), {noreply, NewState}; handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{action = #w{id = ID}}) -> NewState = check_tx(State), {noreply, NewState}; handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{copy = #w{id = ID}}) -> ok = copy(State), {noreply, State}; handle_event(#wx{event = #wxClose{}}, State) -> retire(State); handle_event(Event, State) -> ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), {noreply, State}. code_change(_, State, _) -> {ok, State}. retire(State = #s{frame = Frame}) -> ok = wxWindow:destroy(Frame), {stop, normal, State}. terminate(Reason, State) -> ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), wx:destroy(). %%% Handlers handle_troubling(State = #s{frame = Frame}, Info) -> ok = wxFrame:raise(Frame), ok = zxw:show_message(Frame, Info), State. do_tx_hash(#s{tx_data = #{"tx_hash" := TXHash}}) -> TXHash; do_tx_hash(#s{tx_data = none}) -> none. prep_call(State) -> case gd_con:chain_id() of {ok, ChainID} -> prep_call2(State, ChainID); Error -> handle_troubling(State, Error) end. prep_call2(State, ChainID) -> case params(State) of {ok, Params} -> prep_call3(State, ChainID, Params); Error -> handle_troubling(State, Error) end. prep_call3(State, ChainID, Params) -> case args(State) of {ok, Args} -> prep_call4(State, ChainID, Params, {sophia, Args}); Error -> handle_troubling(State, Error) end. prep_call4(State = #s{fundef = {"init", init}, build = Build}, ChainID, Params, Args) -> {CallerID, Nonce, Gas, GP, Amount, TTL} = Params, case hz:contract_create_built(CallerID, Nonce, Gas, GP, Amount, TTL, Build, Args) of {ok, CreateTX} -> deploy(State, ChainID, CallerID, CreateTX); Error -> handle_troubling(State, Error) end; prep_call4(State = #s{fundef = {Name, Ilk}, con_id = ConID, build = Build}, ChainID, Params, Args) -> {CallerID, Nonce, Gas, GP, Amount, TTL} = Params, AACI = maps:get(aaci, Build), case hz:contract_call(CallerID, Nonce, Gas, GP, Amount, TTL, AACI, ConID, Name, Args) of {ok, UnsignedTX} -> case Ilk of call -> do_call(State, ChainID, CallerID, UnsignedTX); dryr -> do_dry_run(State, ConID, UnsignedTX) end; Error -> handle_troubling(State, Error) end. params(State = #s{kp = #w{wx = KeyPicker}}) -> ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)), PK = unicode:characters_to_binary(ID), case hz:next_nonce(PK) of {ok, Nonce} -> params2(State, PK, Nonce); Error -> handle_troubling(State, Error) end. params2(#s{params = Params}, PK, Nonce) -> case lists:foldl(fun extract/2, {ok, []}, Params) of {ok, [TTL, GP, Gas, Amount]} -> {ok, {PK, Nonce, Gas, GP, Amount, TTL}}; Error -> Error end. extract({Name, Check, Widget}, {Status, Out}) -> case Check(wxTextCtrl:getValue(Widget)) of {ok, Value} -> {Status, [Value | Out]}; Error -> {error, [{Name, Error}, Out]} end. % TODO: Put some basic checking in here, like for blank strings, at least. % It should be possible to perform type/parse checks over this since we have % access to the AACI. But not today. args(#s{args = ArgFields}) -> Values = [wxTextCtrl:getValue(W) || #w{wx = W} <- ArgFields], {ok, Values}. deploy(State, ChainID, CallerID, CreateTX) -> case gd_con:sign_call(ChainID, CallerID, CreateTX) of {ok, SignedTX} -> deploy2(State, SignedTX); Error -> handle_troubling(State, Error) end. deploy2(State = #s{j = J, hash = #w{wx = HashT}, action = #w{wx = ActionB}}, SignedTX) -> case hz:post_tx(SignedTX) of {ok, Data = #{"tx_hash" := TXHash}} -> _ = wxButton:disable(ActionB), ok = wxButton:setLabel(ActionB, J("Check Transaction Status")), ok = log(info, "Submitted transaction ~s", [TXHash]), ok = wxTextCtrl:setValue(HashT, unicode:characters_to_list(TXHash)), check_tx(State#s{tx_data = Data, status = submitted}); {ok, #{"reason" := Reason}} -> handle_troubling(State, {error, Reason}); Error -> handle_troubling(State, Error) end. do_call(State, ChainID, CallerID, UnsignedTX) -> case gd_con:sign_call(ChainID, CallerID, UnsignedTX) of {ok, SignedTX} -> do_call2(State, SignedTX); Error -> handle_troubling(State, Error) end. do_call2(State = #s{action = #w{wx = ActionB}}, SignedTX) -> _ = wxButton:disable(ActionB), case hz:post_tx(SignedTX) of {ok, #{"reason" := Reason}} -> handle_troubling(State#s{status = rejected}, Reason); {ok, Data} -> check_tx(State#s{tx_data = Data, status = submitted}); Error -> handle_troubling(State, Error) end. do_dry_run(State = #s{action = #w{wx = ActionB}}, ConID, TX) -> _ = wxButton:disable(ActionB), case hz:dry_run(TX) of {ok, Result} -> dry_run2(State#s{tx_info = Result}); Other -> handle_troubling(State, {error, ConID, Other}) end. dry_run2(State = #s{funret = ReturnType, return = #w{wx = ReturnT}, copy = #w{wx = CopyB}, tx_data = TXData, tx_info = TXInfo, hash = #w{wx = HashT}, info = #w{wx = InfoT}}) -> ReturnV = case TXInfo of #{"results" := [#{"call_obj" := #{"return_type" := "revert", "return_value" := ReturnCB}, "result" := "ok","type" := "contract_call"}]} -> io_lib:format("Revert: ~ts", [hz:decode_bytearray(ReturnCB, sophia)]); #{"results" := [#{"call_obj" := #{"return_type" := "ok", "return_value" := ReturnCB}, "result" := "ok","type" := "contract_call"}]} -> hz:decode_bytearray(ReturnCB, {sophia, ReturnType}); Other -> io_lib:format("???: ~tp", [Other]) end, _ = wxButton:enable(CopyB), FormattedHash = io_lib:format("~tp", [TXData]), FormattedInfo = io_lib:format("~tp", [TXInfo]), ok = wxTextCtrl:setValue(ReturnT, ReturnV), ok = wxTextCtrl:setValue(HashT, FormattedHash), ok = wxTextCtrl:setValue(InfoT, FormattedInfo), State. check_tx(State = #s{j = J, fundef = {_, init}, tx_data = #{"tx_hash" := TXHash}, tx_info = none, status = submitted, action = #w{wx = ActionB}, info = #w{wx = InfoT}}) -> case hz:tx_info(TXHash) of {ok, Info = #{"call_info" := #{"return_type" := "ok", "contract_id" := ConID}}} -> ok = tell(info, "Contract deployed: ~p", [Info]), _ = wxButton:disable(ActionB), ok = gd_con:open_contract(ConID), self() ! retire, State; {error, "Tx not mined"} -> ok = wxTextCtrl:setValue(InfoT, J("[Transaction not yet mined.]")), ok = wxButton:setLabel(ActionB, J("Check Deployment Status")), _ = wxButton:enable(ActionB), State; Other -> FormattedJunk = io_lib:format("~tp", [Other]), ok = wxTextCtrl:setValue(InfoT, FormattedJunk), ok = wxButton:setLabel(ActionB, J("Check Depoyment Status")), _ = wxButton:enable(ActionB), State end; check_tx(State = #s{j = J, funret = ReturnType, tx_data = #{"tx_hash" := TXHash}, tx_info = none, status = submitted, return = #w{wx = ReturnT}, copy = #w{wx = CopyB}, action = #w{wx = ActionB}, info = #w{wx = InfoT}}) -> case hz:tx_info(TXHash) of {ok, Info = #{"call_info" := #{"return_type" := "ok", "return_value" := ReturnCB}}} -> FormattedInfo = io_lib:format("~tp", [Info]), _ = wxButton:enable(CopyB), _ = wxButton:disable(ActionB), ReturnV = hz:decode_bytearray(ReturnCB, {sophia, ReturnType}), ok = wxTextCtrl:setValue(ReturnT, ReturnV), ok = wxTextCtrl:setValue(InfoT, FormattedInfo), State#s{status = included, tx_info = Info}; {ok, Reason = #{"call_info" := #{"return_type" := "revert", "return_value" := ReturnCB}}} -> _ = wxButton:enable(CopyB), _ = wxButton:disable(ActionB), ReturnV = io_lib:format("Revert: ~ts", [hz:decode_bytearray(ReturnCB, sophia)]), ok = wxTextCtrl:setValue(ReturnT, ReturnV), FormattedInfo = io_lib:format("~tp", [Reason]), ok = wxTextCtrl:setValue(InfoT, FormattedInfo), State#s{status = rejected, tx_info = Reason}; {error, "Tx not mined"} -> ok = wxTextCtrl:setValue(InfoT, J("[Transaction not yet mined.]")), ok = wxButton:setLabel(ActionB, J("Check Transaction Status")), _ = wxButton:enable(ActionB), State; Error -> handle_troubling(State, Error) end; check_tx(State = #s{tx_data = TXData, action = #w{wx = ActionB}}) -> _ = wxButton:disable(ActionB), tell(info, "TXData: ~p", [TXData]), ok = tell("Nothing to check"), State. copy(#s{tx_info = none}) -> ok; copy(#s{return = #w{wx = ReturnT}}) -> Output = wxTextCtrl:getValue(ReturnT), gd_lib:copy_to_clipboard(Output). textify({integer, _, _}) -> "int"; textify({boolean, _, _}) -> "bool"; textify({{bytes, [I]}, _, _}) -> io_lib:format("bytes(~w)", [I]); textify({{bytes, any}, _, _}) -> "bytes()"; textify({T, _, _}) when is_atom(T) -> atom_to_list(T); textify({T, _, _}) when is_list(T) -> T; textify({T, _, _}) -> io_lib:format("~tp", [T]). gt_0(S) -> C = "Must be an integer greater than 0", R = try {ok, list_to_integer(S)} catch error:badarg -> {error, {S, C}} end, case R of {ok, N} when N > 0 -> {ok, N}; {ok, N} when N =< 0 -> {error, {S, C}}; Error -> Error end. gte_0(S) -> C = "Must be a non-negative integer.", R = try {ok, list_to_integer(S)} catch error:badarg -> {error, {S, C}} end, case R of {ok, N} when N >= 0 -> {ok, N}; {ok, N} when N < 0 -> {error, {S, C}}; Error -> Error end.