-module(gd_v_devman). -vsn("0.5.4"). -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]). -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("gd.hrl"). -define(editorMode, sophia). % Widgets -record(w, {name = none :: atom() | {FunName :: binary(), call | dryr}, id = 0 :: integer(), wx = none :: none | wx:wx_object()}). % Contract functions in an ACI -record(f, {name = <<"">> :: binary(), call = #w{} :: #w{}, dryrun = #w{} :: none | #w{}, args = [] :: [{wx:wx_object(), wx:wx_object(), argt()}]}). % Code book pages -record(p, {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, win = none :: none | wx:wx_object(), code = none :: none | wxTextCtrl:wxTextCtrl()}). % Contract pages -record(c, {id = <<"">> :: 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(), lang = en :: en | jp, j = none :: none | fun(), prefs = #{} :: map(), buttons = #{} :: #{WX_ID :: integer() := #w{}}, tabs = none :: none | wx:wx_object(), code = {none, []} :: {Codebook :: none | wx:wx_object(), Pages :: [#p{}]}, cons = {none, []} :: {Consbook :: none | wx:wx_object(), Pages :: [#c{}]}}). -type argt() :: int | string | address | list(argt()). %%% Interface -spec to_front(Win) -> ok when Win :: wx:wx_object(). to_front(Win) -> wx_object:cast(Win, to_front). % TODO: Probably kill this -spec set_manifest(Entries) -> ok when Entries :: list(). set_manifest(Entries) -> case is_pid(whereis(?MODULE)) of true -> wx_object:cast(?MODULE, {set_manifest, Entries}); false -> ok end. -spec open_contract(Address) -> ok when Address :: string(). open_contract(Address) -> wx_object:cast(?MODULE, {open_contract, Address}). -spec call_result(ConID, CallInfo) -> ok when ConID :: gajudesk:id(), CallInfo :: map(). call_result(ConID, CallInfo) -> wx_object:cast(?MODULE, {call_result, ConID, CallInfo}). -spec dryrun_result(ConID, CallInfo) -> ok when ConID :: gajudesk: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 start_link(Args) -> wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []). init({Prefs, Manifest}) -> Lang = maps:get(lang, Prefs, en_us), Trans = gd_jt:read_translations(?MODULE), J = gd_jt:j(Lang, Trans), Wx = wx:new(), Frame = wxFrame:new(Wx, ?wxID_ANY, J("Contracts")), MainSz = wxBoxSizer:new(?wxVERTICAL), TopBook = wxNotebook:new(Frame, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), {LWin, LButtons, Codebook} = make_l_win(TopBook, J), {RWin, RButtons, Consbook} = make_r_win(TopBook, J), ButtonMap = maps:merge(LButtons, RButtons), true = wxNotebook:addPage(TopBook, LWin, J("Contract Editor"), []), true = wxNotebook:addPage(TopBook, RWin, J("Deployed Contracts"), []), State = #s{wx = Wx, frame = Frame, j = J, prefs = Prefs, buttons = ButtonMap, tabs = TopBook, code = {Codebook, []}, cons = {Consbook, []}}, _ = wxSizer:add(MainSz, TopBook, zxw:flags(wide)), _ = wxFrame:setSizer(Frame, MainSz), _ = wxSizer:layout(MainSz), ok = gd_v:safe_size(Frame, Prefs), ok = wxFrame:connect(Frame, close_window), ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:center(Frame), true = wxFrame:show(Frame), NewState = add_code_pages(State, Manifest), {Frame, NewState}. make_l_win(TopBook, J) -> Win = wxWindow:new(TopBook, ?wxID_ANY), MainSz = wxBoxSizer:new(?wxVERTICAL), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), ButtonTemplates = [{new, J("New")}, {open, J("Open")}, {save, J("Save")}, {rename, J("Save (rename)")}, {deploy, J("Deploy")}, {close_source, J("Close")}], MakeButton = fun({Name, Label}) -> B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), #w{name = Name, id = wxButton:getId(B), wx = B} end, Buttons = lists:map(MakeButton, ButtonTemplates), AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), ok = lists:foreach(AddButton, Buttons), _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), _ = wxWindow:setSizer(Win, MainSz), MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, ButtonMap = lists:foldl(MapButton, #{}, Buttons), {Win, ButtonMap, Codebook}. make_r_win(TopBook, J) -> Win = wxWindow:new(TopBook, ?wxID_ANY), MainSz = wxBoxSizer:new(?wxVERTICAL), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), ButtonTemplates = [{load, J("Load from Chain")}, {edit, J("Copy to Editor")}, {close_instance, J("Close")}], MakeButton = fun({Name, Label}) -> B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), #w{name = Name, id = wxButton:getId(B), wx = B} end, Buttons = lists:map(MakeButton, ButtonTemplates), AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), ok = lists:foreach(AddButton, Buttons), _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), _ = wxWindow:setSizer(Win, MainSz), MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, ButtonMap = lists:foldl(MapButton, #{}, Buttons), {Win, ButtonMap, Codebook}. add_code_pages(State, Files) -> lists:foldl(fun add_code_page/2, State, Files). %%% OTP callbacks handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. handle_cast(to_front, State = #s{frame = Frame}) -> ok = wxFrame:raise(Frame), {noreply, State}; handle_cast({open_contract, Address}, State) -> NewState = load2(State, Address), {noreply, NewState}; handle_cast({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}. handle_info(Unexpected, State) -> ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), {noreply, State}. handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{buttons = Buttons}) -> NewState = case maps:get(ID, Buttons, undefined) of #w{name = new} -> new_file(State); #w{name = open} -> open(State); #w{name = save} -> save(State); #w{name = rename} -> rename(State); #w{name = deploy} -> deploy(State); #w{name = close_source} -> close_source(State); #w{name = load} -> load(State); #w{name = edit} -> edit(State); #w{name = close_instance} -> close_instance(State); #w{name = Name} -> clicked(State, Name); undefined -> tell("Received message: ~w", [E]), State end, {noreply, NewState}; handle_event(#wx{event = Event = #wxStyledText{type = stc_styleneeded}, obj = Win}, State) -> ok = style(State, Win, Event), {noreply, State}; handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) -> Geometry = case wxTopLevelWindow:isMaximized(Frame) of true -> max; false -> {X, Y} = wxWindow:getPosition(Frame), {W, H} = wxWindow:getSize(Frame), {X, Y, W, H} end, NewPrefs = maps:put(geometry, Geometry, Prefs), ok = gd_con:save(?MODULE, NewPrefs), ok = wxWindow:destroy(Frame), {noreply, State}; handle_event(Event, State) -> ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), {noreply, State}. handle_troubling(#s{frame = Frame}, Info) -> zxw:show_message(Frame, Info). code_change(_, State, _) -> {ok, State}. terminate(Reason, State) -> ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), wx:destroy(). %%% Doers style(#s{code = {_, Pages}}, Win, Event) -> case lists:keyfind(Win, #p.win, Pages) of #p{code = STC} -> gd_sophia_editor:update(Event, STC); false -> tell("Received bogus style event.~nWin: ~p~nEvent: ~p", [Win, Event]) end. clicked(State = #s{cons = {Consbook, Contracts}}, Name) -> case wxNotebook:getSelection(Consbook) of ?wxNOT_FOUND -> ok = tell(warning, "Inconcievable! No notebook page is selected!"), State; Index -> Contract = lists:nth(Index + 1, Contracts), clicked2(State, Contract, Name) end. clicked2(State, Contract, Name) -> case gd_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) -> Label = case element(2, Name) of call -> "Contract Call"; dryr -> "Dry Run" end, Dialog = wxDialog:new(Frame, ?wxID_ANY, J(Label)), 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), {ParamSz, TTL_Tx, GasP_Tx, Gas_Tx, Amount_Tx} = call_param_sizer(Dialog, J), 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, zxw:flags(wide)), _ = wxSizer:add(Sizer, ParamSz, zxw:flags(wide)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), 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)), PK = unicode:characters_to_binary(ID), Controls = [{"TTL", TTL_Tx}, {"Gas Price", GasP_Tx}, {"Gas", Gas_Tx}, {"Amount", Amount_Tx}], case call_params(Controls) of {ok, [TTL, GasP, Gas, Amount]} -> {ok, Nonce} = hz:next_nonce(PK), {ok, {PK, Nonce, TTL, GasP, Gas, Amount}}; E -> E end; ?wxID_CANCEL -> cancel end, ok = wxDialog:destroy(Dialog), case Outcome of {ok, Params} -> clicked4(State, Contract, Name, Params); cancel -> State; Error -> handle_troubling(State, Error) end. call_param_sizer(Dialog, J) -> {ok, Height} = hz:top_height(), DefTTL = Height + 10000, DefGasP = hz:min_gas_price(), DefGas = 5000000, DefAmount = 0, ParamSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("TX Parameters")}]), GridSz = wxFlexGridSizer:new(2, 4, 4), ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), TTL_L = wxStaticText:new(Dialog, ?wxID_ANY, "TTL"), TTL_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY), ok = wxTextCtrl:setValue(TTL_Tx, integer_to_list(DefTTL)), GasP_L = wxStaticText:new(Dialog, ?wxID_ANY, J("Gas Price")), GasP_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY), ok = wxTextCtrl:setValue(GasP_Tx, integer_to_list(DefGasP)), Gas_L = wxStaticText:new(Dialog, ?wxID_ANY, J("Gas")), Gas_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY), ok = wxTextCtrl:setValue(Gas_Tx, integer_to_list(DefGas)), Amount_L = wxStaticText:new(Dialog, ?wxID_ANY, J("TX Amount")), Amount_Tx = wxTextCtrl:new(Dialog, ?wxID_ANY), ok = wxTextCtrl:setValue(Amount_Tx, integer_to_list(DefAmount)), _ = wxFlexGridSizer:add(GridSz, TTL_L, zxw:flags(base)), _ = wxFlexGridSizer:add(GridSz, TTL_Tx, zxw:flags(wide)), _ = wxFlexGridSizer:add(GridSz, GasP_L, zxw:flags(base)), _ = wxFlexGridSizer:add(GridSz, GasP_Tx, zxw:flags(wide)), _ = wxFlexGridSizer:add(GridSz, Gas_L, zxw:flags(base)), _ = wxFlexGridSizer:add(GridSz, Gas_Tx, zxw:flags(wide)), _ = wxFlexGridSizer:add(GridSz, Amount_L, zxw:flags(base)), _ = wxFlexGridSizer:add(GridSz, Amount_Tx, zxw:flags(wide)), _ = wxSizer:add(ParamSz, GridSz, zxw:flags(wide)), {ParamSz, TTL_Tx, GasP_Tx, Gas_Tx, Amount_Tx}. call_params(Controls) -> call_params(Controls, []). call_params([], A) -> {ok, lists:reverse(A)}; call_params([{L, C} | T], A) -> O = try {ok, list_to_integer(wxTextCtrl:getValue(C))} catch error:badarg -> {error, {L, not_an_integer}} end, case O of {ok, N} -> call_params(T, [N | A]); Error -> Error end. clicked4(State, #c{id = ConID, build = #{aci := ACI}, funs = {_, Funs}}, {Name, Type}, {PK, Nonce, TTL, GasP, Gas, Amount}) -> 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), 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 = gd_con:sign_call(ConID, CallerID, UnsignedTX), State. do_dry_run(State, ConID, TX) -> ok = gd_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(State, Message), State end; {error, Reason} -> Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Reason]), ok = handle_troubling(State, Message), State end; add_code_page2(State, {hash, Address}) -> open_hash2(State, Address). add_code_page(State = #s{tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> Color = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW), tell("Color: ~p", [Color]), Window = wxWindow:new(Codebook, ?wxID_ANY), PageSz = wxBoxSizer:new(?wxHORIZONTAL), CodeTx = gd_sophia_editor:new(Window), ok = gd_sophia_editor:set_text(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, ok = wxStyledTextCtrl:connect(Window, stc_styleneeded), true = wxNotebook:addPage(Codebook, Window, FileName, [{bSelect, true}]), Page = #p{path = Location, win = Window, code = CodeTx}, NewPages = Pages ++ [Page], State#s{code = {Codebook, NewPages}}. new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = case maps:find(dir, Prefs) of {ok, PrefDir} -> PrefDir; error -> case os:getenv("ZOMP_DIR") of "" -> file:get_pwd(); D -> filename:basename(D) end end, Options = [{message, J("Save Location")}, {defaultDir, DefaultDir}, {defaultFile, "my_contract.aes"}, {wildCard, "*.aes"}, {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], Dialog = wxFileDialog:new(Frame, Options), NewState = case wxFileDialog:showModal(Dialog) of ?wxID_OK -> Dir = wxFileDialog:getDirectory(Dialog), case wxFileDialog:getFilename(Dialog) of "" -> State; Name -> File = case filename:extension(Name) of ".aes" -> Name; _ -> Name ++ ".aes" end, Path = filename:join(Dir, File), NewPrefs = maps:put(dir, Dir, Prefs), NextState = State#s{prefs = NewPrefs}, add_code_page(NextState, {file, Path}, "") end; ?wxID_CANCEL -> State end, ok = wxFileDialog:destroy(Dialog), NewState. deploy(State = #s{code = {Codebook, Pages}}) -> case wxNotebook:getSelection(Codebook) of ?wxNOT_FOUND -> State; Index -> #p{code = CodeTx} = lists:nth(Index + 1, Pages), Source = wxTextCtrl:getValue(CodeTx), deploy2(State, Source) end. deploy2(State, Source) -> case compile(Source) of % Options = sophia_options(), % case so_compiler:from_string(Source, Options) of {ok, Build} -> deploy3(State, Build); Other -> ok = tell(info, "Compilation Failed!~n~tp", [Other]), State end. deploy3(State, Build) -> case gd_con:list_keys() of {ok, 0, []} -> handle_troubling(State, "No keys exist in the current wallet."); {ok, Selected, Keys} -> deploy4(State, Build, Selected, Keys); error -> handle_troubling(State, "No wallet is selected!") end. deploy4(State = #s{frame = Frame, j = J}, Build = #{aci := ACI}, Selected, Keys) -> {#{functions := Funs}, _} = find_main(ACI), #{arguments := As} = lom:find(name, <<"init">>, Funs), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")), Sizer = wxBoxSizer:new(?wxVERTICAL), ScrollWin = wxScrolledWindow:new(Dialog), ScrollSz = wxBoxSizer:new(?wxVERTICAL), ok = wxScrolledWindow:setSizerAndFit(ScrollWin, ScrollSz), ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), FunName = unicode:characters_to_list(["init/", integer_to_list(length(As))]), FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), KeySz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Signature Key")}]), KeyPicker = wxChoice:new(ScrollWin, ?wxID_ANY, [{choices, Keys}]), _ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)), ok = wxChoice:setSelection(KeyPicker, Selected - 1), {ParamSz, TTL_Tx, GasP_Tx, Gas_Tx, Amount_Tx} = call_param_sizer(ScrollWin, J), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), Affirm = wxButton:new(Dialog, ?wxID_OK), Cancel = wxButton:new(Dialog, ?wxID_CANCEL), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), GridSz = wxFlexGridSizer:new(2, 4, 4), ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), MakeArgField = fun(#{name := AN, type := T}) -> Type = case T of <<"address">> -> address; <<"int">> -> integer; <<"bool">> -> boolean; L when is_list(L) -> list; % FIXME % I when is_binary(I) -> iface % FIXME I when is_binary(I) -> address % FIXME end, ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), {ANT, TCT, Type} end, ArgFields = lists:map(MakeArgField, As), _ = wxStaticBoxSizer:add(FunSz, GridSz, zxw:flags(wide)), _ = wxStaticBoxSizer:add(ScrollSz, FunSz, [{proportion, 1}, {flag, ?wxEXPAND}]), _ = wxStaticBoxSizer:add(ScrollSz, KeySz, [{proportion, 0}, {flag, ?wxEXPAND}]), _ = wxStaticBoxSizer:add(ScrollSz, ParamSz, [{proportion, 0}, {flag, ?wxEXPAND}]), _ = wxSizer:add(Sizer, ScrollWin, [{proportion, 5}, {flag, ?wxEXPAND}]), _ = wxSizer:add(Sizer, ButtSz, [{proportion, 1}, {flag, ?wxEXPAND}]), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 500}), ok = wxDialog:center(Dialog), Outcome = case wxDialog:showModal(Dialog) of ?wxID_OK -> ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)), PK = unicode:characters_to_binary(ID), InitArgs = lists:map(fun get_arg/1, ArgFields), Controls = [{"TTL", TTL_Tx}, {"Gas Price", GasP_Tx}, {"Gas", Gas_Tx}, {"Amount", Amount_Tx}], case call_params(Controls) of {ok, [TTL, GasP, Gas, Amount]} -> {ok, Nonce} = hz:next_nonce(PK), DeployParams = {PK, Nonce, TTL, GasP, Gas, Amount}, {ok, DeployParams, InitArgs}; E -> E end; ?wxID_CANCEL -> cancel end, ok = wxDialog:destroy(Dialog), case Outcome of {ok, Params, Args} -> deploy5(State, Build, Params, Args); cancel -> State; Error -> handle_troubling(State, Error) end. deploy5(State, Build, Params, Args) -> tell(info, "Build: ~p", [Build]), ok = gd_con:deploy(Build, Params, Args), State. open(State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Open Contract Source")), Sizer = wxBoxSizer:new(?wxVERTICAL), Choices = wxRadioBox:new(Dialog, ?wxID_ANY, J("Select Origin"), ?wxDefaultPosition, ?wxDefaultSize, [J("From File"), J("From Hash")], [{majorDim, 1}]), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), Affirm = wxButton:new(Dialog, ?wxID_OK), Cancel = wxButton:new(Dialog, ?wxID_CANCEL), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), _ = wxSizer:add(Sizer, Choices, zxw:flags(wide)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxDialog:setSize(Dialog, {250, 170}), ok = wxBoxSizer:layout(Sizer), Choice = case wxDialog:showModal(Dialog) of ?wxID_OK -> case wxRadioBox:getSelection(Choices) of 0 -> file; 1 -> hash; ?wxNOT_FOUND -> none end; ?wxID_CANCEL -> cancel end, ok = wxDialog:destroy(Dialog), case Choice of file -> open_file(State); hash -> open_hash(State); none -> ok = tell(info, "No selection."), State; cancel -> ok = tell(info, "Cancelled."), State end. open_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = case maps:find(dir, Prefs) of {ok, PrefDir} -> PrefDir; error -> case os:getenv("ZOMP_DIR") of "" -> file:get_pwd(); D -> filename:basename(D) end end, Options = [{message, J("Load Contract Source")}, {defaultDir, DefaultDir}, {wildCard, "*.aes"}, {style, ?wxFD_OPEN}], Dialog = wxFileDialog:new(Frame, Options), NewState = case wxFileDialog:showModal(Dialog) of ?wxID_OK -> Dir = wxFileDialog:getDirectory(Dialog), case wxFileDialog:getFilename(Dialog) of "" -> State; Name -> File = case filename:extension(Name) of ".aes" -> Name; _ -> Name ++ ".aes" end, Path = filename:join(Dir, File), NewPrefs = maps:put(dir, Dir, Prefs), NextState = State#s{prefs = NewPrefs}, add_code_page(NextState, {file, Path}) end; ?wxID_CANCEL -> State end, ok = wxFileDialog:destroy(Dialog), NewState. open_hash(State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), Sizer = wxBoxSizer:new(?wxVERTICAL), AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer:add(AddressSz, AddressTx, zxw:flags(wide)), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), Affirm = wxButton:new(Dialog, ?wxID_OK), Cancel = wxButton:new(Dialog, ?wxID_CANCEL), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 200}), ok = wxDialog:center(Dialog), ok = 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 = "ct_" ++ _} -> open_hash2(State, Address); {ok, Address = "th_" ++ _} -> get_contract_from_tx(State, Address); {ok, Turd} -> handle_troubling(State, {bad_address, Turd}); cancel -> State end. get_contract_from_tx(State, Address) -> case hz:tx_info(Address) of {ok, #{"call_info" := #{"contract_id" := Contract}}} -> open_hash2(State, Contract); {ok, Other} -> handle_troubling(State, {bad_address, Other}); Error -> handle_troubling(State, Error) end. open_hash2(State, Address) -> case hz:contract_source(Address) of {ok, Source} -> open_hash3(State, Address, Source); Error -> ok = handle_troubling(State, Error), 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 = get_source(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. get_source(Widget) -> case ?editorMode of plain -> wxTextCtrl:getValue(Widget); sophia -> gd_sophia_editor:get_text(Widget) end. set_source(Widget, Src) -> case ?editorMode of plain -> wxTextCtrl:setValue(Widget, Src); sophia -> gd_sophia_editor:set_text(Widget, Src) 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 = get_source(Widget), case filelib:ensure_dir(NewPath) of ok -> case file:write_file(NewPath, Source) of ok -> true = wxNotebook:setPageText(Codebook, Index, File), NewPrefs = maps:put(dir, Dir, Prefs), NewPage = Page#p{path = {file, NewPath}}, NewPages = store_nth(Index + 1, NewPage, Pages), NewCode = {Codebook, NewPages}, State#s{prefs = NewPrefs, code = NewCode}; Error -> ok = handle_troubling(State, Error), State end; Error -> ok = handle_troubling(State, Error), State end end; ?wxID_CANCEL -> State end, ok = wxFileDialog:destroy(Dialog), NewState; #p{path = {hash, _}} -> save(State) end end. close_source(State = #s{code = {Codebook, Pages}}) -> case wxNotebook:getSelection(Codebook) of ?wxNOT_FOUND -> State; Index -> NewPages = drop_nth(Index + 1, Pages), true = wxNotebook:deletePage(Codebook, Index), State#s{code = {Codebook, NewPages}} end. load(State = #s{frame = Frame, j = J}) -> % TODO: Extract the exact compiler version, load it, and use only that or fail if % the specific version is unavailable. % TODO: Compile on load and verify the deployed hash for validity. Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), Sizer = wxBoxSizer:new(?wxVERTICAL), AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer:add(AddressSz, AddressTx, zxw:flags(wide)), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), Affirm = wxButton:new(Dialog, ?wxID_OK), Cancel = wxButton:new(Dialog, ?wxID_CANCEL), _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 200}), ok = wxDialog:center(Dialog), ok = 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 = "ct_" ++ _} -> load2(State, Address); {ok, Address = "th_" ++ _} -> load_from_tx(State, Address); {ok, Turd} -> handle_troubling(State, {bad_address, Turd}); cancel -> State end. load_from_tx(State, Address) -> case hz:tx_info(Address) of {ok, #{"call_info" := #{"contract_id" := Contract}}} -> load2(State, Contract); {ok, Other} -> handle_troubling(State, {bad_address, Other}); Error -> handle_troubling(State, Error) end. load2(State, Address) -> case hz:contract_source(Address) of {ok, Source} -> load3(State, Address, Source); Error -> ok = handle_troubling(State, Error), 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 = set_source(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_lib: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). keyfind_index(K, E, I, [H | T]) -> case element(E, H) =:= K of false -> keyfind_index(K, E, I + 1, T); true -> {ok, I} end; keyfind_index(_, _, _, []) -> error.