Files
GajuDesk/src/gd_v_call.erl
T
2026-04-28 14:15:28 +09:00

432 lines
16 KiB
Erlang

-module(gd_v_call).
-vsn("0.8.1").
-author("Craig Everett <craigeverett@qpq.swiss>").
-copyright("QPQ AG <info@qpq.swiss>").
-license("GPL-3.0-or-later").
-behavior(wx_object).
%-behavior(gd_v).
-include_lib("wx/include/wx.hrl").
-export([to_front/1, result/2]).
-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(),
build = none :: none | map(),
args = [] :: [#w{}],
kp = #w{} :: #w{},
params = [] :: [param()],
ttl = #w{} :: #w{},
gasprice = #w{} :: #w{},
gas = #w{} :: #w{},
amount = #w{} :: #w{},
action = #w{} :: #w{},
tx_data = none :: none | map(),
tx_hash = #w{} :: #w{},
tx_info = none :: none | hz:transaction(),
out = #w{} :: #w{},
copy = #w{} :: #w{}}).
-type fun_name() :: string().
-type fun_type() :: call | dryr | init.
-type fun_def() :: {fun_name(), fun_type()}.
-type param() :: {Label :: string(), Check :: fun(), #w{}}.
%%% Interface
-spec to_front(Win) -> ok
when Win :: wx:wx_object().
to_front(Win) ->
wx_object:cast(Win, to_front).
-spec result(Win, Outcome) -> ok
when Win :: wx:wx_object(),
Outcome :: term().
result(Win, Outcome) ->
wx_object:cast(Win, {result, Outcome}).
%%% Startup Functions
start_link(Args) ->
wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []).
init({Prefs, {FunName, FunType}, 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 = maps:get(FunName, FunSpecs),
{CallType, ActionLabel} =
case FunType 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(element(1, FunSpec))),
Title = [CallType, ": ", 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")}]),
KeyPicker = wxChoice:new(Frame, ?wxID_ANY, [{choices, Keys}]),
ok = wxChoice:setSelection(KeyPicker, Selected),
_ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)),
{ArgSz, Args, Dimensions} = call_arg_sizer(Frame, J, 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),
TX_Data = #w{wx = DataT} = gd_lib:mono_text(TX_Sz_Box, tx_hash),
TX_Hash = #w{wx = HashT} = gd_lib:mono_text(TX_Sz_Box, tx_hash),
TX_Info = #w{wx = InfoT} = gd_lib:mono_text(TX_Sz_Box, tx_hash),
Line = wxStaticLine:new(TX_Sz, [{style, ?wxLI_HORIZONTAL}]),
Out = #w{wx = OutTxt} = gd_lib:mono_text(TX_Sz_Box, out),
Copy = #w{wx = CopyBn} = gd_lib:button(Frame, J("Copy")),
_ = wxStaticBoxSizer:add(TX_Sz, DataT, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(TX_Sz, HashT, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(TX_Sz, InfoT, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(TX_Sz, Line, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(TX_Sz, OutTxt, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(TX_Sz, CopyBn, zxw:flags({wide, 5})),
_ = wxSizer:add(MainSz, ArgSz, zxw:flags({wide, 5})),
_ = wxSizer:add(MainSz, KeySz, zxw:flags({wide, 5})),
_ = wxSizer:add(MainSz, ParamSz, zxw:flags({wide, 5})),
_ = wxSizer:add(MainSz, ActionBn, zxw:flags({wide, 5})),
_ = wxSizer:add(MainSz, TX_Sz, zxw:flags({wide, 5})),
_ = wxFrame:setSizer(Frame, MainSz),
_ = wxSizer:layout(MainSz),
ok = gd_v:safe_size(Frame, Prefs),
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,
con_id = ConID, build = Build,
args = Args, kp = KeyPicker, params = Params,
action = Action,
tx_data = TX_Data, tx_hash = TX_Hash, tx_info = TX_Info,
out = Out, copy = Copy},
{Frame, State}.
call_arg_sizer(Frame, J, {CallArgs, ReturnType}) ->
SpecSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Function Spec")}]),
{CallSz, CallControls, Dimensions} = call_sizer(Frame, J, CallArgs),
ReturnSz = return_sizer(Frame, J, ReturnType),
_ = wxStaticBoxSizer:add(SpecSz, CallSz, zxw:flags({wide, 5})),
_ = wxStaticBoxSizer:add(SpecSz, ReturnSz, zxw:flags({base, 5})),
{SpecSz, CallControls, Dimensions}.
call_sizer(Frame, J, []) ->
CallSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Call Args")}]),
Args = wxStaticText:new(Frame, ?wxID_ANY, ["[", J("No Args"), "]"]),
_ = wxStaticBoxSizer:add(CallSz, Args, zxw:flags({wide, 5})),
{CallSz, [], {500, 500}};
call_sizer(Frame, J, CallArgs) ->
ScrollWin = wxScrolledWindow:new(Frame),
ScrollSz = wxBoxSizer:new(?wxVERTICAL),
ok = wxScrolledWindow:setSizerAndFit(ScrollWin, ScrollSz),
ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5),
CallSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Call Args")}]),
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 = wxTextCtrl:new(ScrollWin, ?wxID_ANY),
_ = wxFlexGridSizer:add(GridSz, L, zxw:flags({base, 5})),
_ = wxFlexGridSizer:add(GridSz, C, zxw:flags({wide, 5})),
{Name, C}
end,
Controls = lists:map(AddArg, CallArgs),
{CallSz, Controls, {600, 700}}.
return_sizer(Frame, J, ReturnType) ->
ReturnSz = wxStaticBoxSizer:new(?wxVERTICAL, Frame, [{label, J("Return Type")}]),
Return = wxStaticText:new(Frame, ?wxID_ANY, textify(ReturnType)),
_ = wxStaticBoxSizer:add(ReturnSz, Return, zxw:flags({wide, 5})),
ReturnSz.
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")}]),
GridSz = wxFlexGridSizer:new(2, 4, 4),
ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL),
ok = wxFlexGridSizer:addGrowableCol(GridSz, 1),
TTL_L = wxStaticText:new(Frame, ?wxID_ANY, "TTL"),
TTL_T = wxTextCtrl:new(Frame, ?wxID_ANY),
ok = wxTextCtrl:setValue(TTL_T, integer_to_list(DefTTL)),
GasP_L = wxStaticText:new(Frame, ?wxID_ANY, J("Gas Price")),
GasP_T = wxTextCtrl:new(Frame, ?wxID_ANY),
ok = wxTextCtrl:setValue(GasP_T, integer_to_list(DefGasP)),
Gas_L = wxStaticText:new(Frame, ?wxID_ANY, J("Gas")),
Gas_T = wxTextCtrl:new(Frame, ?wxID_ANY),
ok = wxTextCtrl:setValue(Gas_T, integer_to_list(DefGas)),
Amount_L = wxStaticText:new(Frame, ?wxID_ANY, J("TX Amount")),
Amount_T = wxTextCtrl:new(Frame, ?wxID_ANY),
ok = wxTextCtrl:setValue(Amount_T, integer_to_list(DefAmount)),
_ = wxFlexGridSizer:add(GridSz, TTL_L, zxw:flags({base, 5})),
_ = wxFlexGridSizer:add(GridSz, TTL_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, Gas_L, zxw:flags({base, 5})),
_ = wxFlexGridSizer:add(GridSz, Gas_T, zxw:flags({wide, 5})),
_ = wxFlexGridSizer:add(GridSz, Amount_L, zxw:flags({base, 5})),
_ = wxFlexGridSizer:add(GridSz, Amount_T, zxw:flags({wide, 5})),
_ = wxSizer:add(ParamSz, GridSz, zxw:flags(wide)),
Params =
[{ "TTL", fun gte_0/1, TTL_T},
{J("Gas Price"), fun gt_0/1, GasP_T},
{J("Gas") , fun gt_0/1, Gas_T},
{J("TX Amount"), fun gt_0/1, Amount_T}],
{ParamSz, Params}.
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(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}}) ->
NewState = engage(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(Event, State) ->
ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]),
{noreply, State}.
code_change(_, State, _) ->
{ok, State}.
terminate(Reason, State) ->
ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]),
wx:destroy().
handle_troubling(State = #s{frame = Frame}, Info) ->
ok = wxFrame:raise(Frame),
ok = zxw:show_message(Frame, Info),
State.
engage(State) ->
case gd_con:chain_id() of
{ok, ChainID} -> engage2(State, ChainID);
Error -> handle_troubling(State, Error)
end.
engage2(State, ChainID) ->
case params(State) of
{ok, Params} -> engage3(State, ChainID, Params);
Error -> handle_troubling(State, Error)
end.
engage3(State, ChainID, Params) ->
case args(State) of
{ok, Args} -> engage4(State, ChainID, Params, {sophia, Args});
Error -> handle_troubling(State, Error)
end.
engage4(State = #s{fundef = {"init", init}, build = Build}, ChainID, Params, Args) ->
{CallerID, Nonce, TTL, GP, Gas, Amount} = 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;
engage4(State = #s{fundef = {Name, Type}, con_id = ConID, build = Build}, ChainID, Params, Args) ->
{CallerID, Nonce, TTL, GP, Gas, Amount} = Params,
AACI = maps:get(aaci, Build),
case hz:contract_call(CallerID, Nonce, Gas, GP, Amount, TTL, AACI, ConID, Name, Args) of
{ok, UnsignedTX} ->
case Type 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, TTL, GP, Gas, Amount}};
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_tx(ChainID, CallerID, CreateTX) of
{ok, SignedTX} -> deploy2(State, SignedTX);
Error -> handle_troubling(State, Error)
end.
deploy2(State, SignedTX) ->
case hz:post_tx(SignedTX) of
{ok, Data} -> check_tx(State#s{tx_data = Data});
% {ok, WTF} -> handle_troubling(State, {error, WTF});
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, SignedTX) ->
case hz:post_tx(SignedTX) of
{ok, Data} -> check_tx(State = #s{tx_data = Data});
Error -> handle_troubling(State, Error)
end.
do_dry_run(State, ConID, TX) ->
case hz:dry_run(TX) of
{ok, Result} -> update_info(State#s{tx_info = Result});
Other -> handle_troubling(State, {error, ConID, Other})
end.
check_tx(State = #s{tx_data = #{"tx_hash" := TXHash} = TXData, tx_info = none}) ->
ok = tell("TXData: ~p", [TXData]),
case hz:tx_info(TXHash) of
{ok, Info = #{"call_info" := #{"return_type" := "ok", "contract_id" := ConID}}} ->
ok = gd_v_devman:open_contract(ConID),
update_info(State#s{tx_info = Info});
{error, "Tx not mined"} ->
handle_troubling(State, {error, not_mined});
{ok, Reason = #{"call_info" := #{"return_type" := "revert"}}} ->
handle_troubling(State, {error, Reason});
Error ->
handle_troubling(State, Error)
end;
check_tx(State) ->
ok = tell("Nothing to check"),
State.
update_info(State = #s{tx_info = TXInfo, out = #w{wx = Out}}) ->
Formatted = io_lib:format("TXInfo: ~p~n", [TXInfo]),
ok = wxStaticText:setLabel(Out, Formatted),
State.
copy(#s{tx_info = none}) ->
ok;
copy(#s{out = #w{wx = Out}}) ->
Output = wxStaticText:getLabel(Out),
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.