From eac630168cc260acfdb183109a14e86e777b0526 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Tue, 25 Feb 2025 16:32:11 +0900 Subject: [PATCH] Shaky, but working-ish contract deployment and calls --- src/: | 1029 ++++++++++++++++++++++++++++++++++++++++ src/clutch.erl | 1 + src/gmc_con.erl | 169 +++++-- src/gmc_gui.erl | 6 +- src/gmc_key_master.erl | 8 +- src/gmc_v_devman.erl | 532 ++++++++++++++------- zomp.meta | 12 +- 7 files changed, 1525 insertions(+), 232 deletions(-) create mode 100644 src/: diff --git a/src/: b/src/: new file mode 100644 index 0000000..15b8cf4 --- /dev/null +++ b/src/: @@ -0,0 +1,1029 @@ +-module(gmc_v_devman). +-vsn("0.2.0"). +-author("Craig Everett "). +-copyright("QPQ AG "). +-license("GPL-3.0-or-later"). + +-behavior(wx_object). +%-behavior(gmc_v). +-include_lib("wx/include/wx.hrl"). +-export([to_front/1]). +-export([set_manifest/1, open_contract/1, trouble/1]). +-export([start_link/1]). +-export([init/1, terminate/2, code_change/3, + handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). +-include("$zx_include/zx_logger.hrl"). +-include("gmc.hrl"). + +% Widgets +-record(w, + {name = none :: atom() | {FunName :: binary(), call | dryr}, + id = 0 :: integer(), + wx = none :: none | wx:wx_object()}). + +% Contract functions in an ACI +-record(f, + {name = <<"">> :: binary(), + call = #w{} :: #w{}, + dryrun = #w{} :: none | #w{}, + args = [] :: [{wx:wx_object(), wx:wx_object(), argt()}]}). + +% Code book pages +-record(p, + {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, + win = none :: none | wx:wx_object(), + code = none :: none | wxTextCtrl:wxTextCtrl()}). + +% Contract pages +-record(c, + {id = "" :: string(), + win = none :: none | wx:wx_object(), + code = none :: none | wxTextCtrl:wxTextCtrl(), + cons = none :: none | wxTextCtrl:wxTextCtrl(), + build = none :: none | map(), + funs = {#w{}, []} :: {#w{}, [#f{}]}}). + +% State +-record(s, + {wx = none :: none | wx:wx_object(), + frame = none :: none | wx:wx_object(), + lang = en :: en | jp, + j = none :: none | fun(), + prefs = #{} :: map(), + buttons = #{} :: #{WX_ID :: integer() := #w{}}, + tabs = none :: none | wx:wx_object(), + code = {none, []} :: {Codebook :: none | wx:wx_object(), Pages :: [#p{}]}, + cons = {none, []} :: {Consbook :: none | wx:wx_object(), Pages :: [#c{}]}}). + +-type argt() :: int | string | address | list(argt()). + +%%% Interface + +-spec to_front(Win) -> ok + when Win :: wx:wx_object(). + +to_front(Win) -> + wx_object:cast(Win, to_front). + + +% TODO: Probably kill this +-spec set_manifest(Entries) -> ok + when Entries :: list(). + +set_manifest(Entries) -> + case is_pid(whereis(?MODULE)) of + true -> wx_object:cast(?MODULE, {set_manifest, Entries}); + false -> ok + end. + + +-spec open_contract(Address) -> ok + when Address :: string(). + +open_contract(Address) -> + wx_object:cast(?MODULE, {open_contract, Address}). + + +-spec trouble(Info) -> ok + when Info :: term(). + +trouble(Info) -> + wx_object:cast(?MODULE, {trouble, Info}). + + + + +%%% Startup Functions + +start_link(Args) -> + wx_object:start_link({local, ?MODULE}, ?MODULE, Args, []). + + +init({Prefs, Manifest}) -> + Lang = maps:get(lang, Prefs, en_us), + Trans = gmc_jt:read_translations(?MODULE), + J = gmc_jt:j(Lang, Trans), + Wx = wx:new(), + Frame = wxFrame:new(Wx, ?wxID_ANY, J("Contracts")), + + MainSz = wxBoxSizer:new(?wxVERTICAL), + TopBook = wxNotebook:new(Frame, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + {LWin, LButtons, Codebook} = make_l_win(TopBook, J), + {RWin, RButtons, Consbook} = make_r_win(TopBook, J), + ButtonMap = maps:merge(LButtons, RButtons), + true = wxNotebook:addPage(TopBook, LWin, J("Contract Editor"), []), + true = wxNotebook:addPage(TopBook, RWin, J("Deployed Contracts"), []), + State = + #s{wx = Wx, frame = Frame, + j = J, prefs = Prefs, + buttons = ButtonMap, tabs = TopBook, + code = {Codebook, []}, cons = {Consbook, []}}, + _ = wxSizer:add(MainSz, TopBook, zxw:flags(wide)), + _ = wxFrame:setSizer(Frame, MainSz), + _ = wxSizer:layout(MainSz), + ok = gmc_v:safe_size(Frame, Prefs), + ok = wxFrame:connect(Frame, close_window), + ok = wxFrame:connect(Frame, command_button_clicked), + ok = wxFrame:center(Frame), + true = wxFrame:show(Frame), + NewState = add_code_pages(State, Manifest), + {Frame, NewState}. + + +make_l_win(TopBook, J) -> + Win = wxWindow:new(TopBook, ?wxID_ANY), + MainSz = wxBoxSizer:new(?wxVERTICAL), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + ButtonTemplates = + [{new, J("New")}, + {open, J("Open")}, + {save, J("Save")}, + {rename, J("Save (rename)")}, + {deploy, J("Deploy")}, + {close_source, J("Close")}], + MakeButton = + fun({Name, Label}) -> + B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), + #w{name = Name, id = wxButton:getId(B), wx = B} + end, + Buttons = lists:map(MakeButton, ButtonTemplates), + AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, + Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + ok = lists:foreach(AddButton, Buttons), + _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), + _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), + _ = wxWindow:setSizer(Win, MainSz), + MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, + ButtonMap = lists:foldl(MapButton, #{}, Buttons), + {Win, ButtonMap, Codebook}. + +make_r_win(TopBook, J) -> + Win = wxWindow:new(TopBook, ?wxID_ANY), + MainSz = wxBoxSizer:new(?wxVERTICAL), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + ButtonTemplates = + [{load, J("Load from Chain")}, + {edit, J("Copy to Editor")}, + {close_instance, J("Close")}], + MakeButton = + fun({Name, Label}) -> + B = wxButton:new(Win, ?wxID_ANY, [{label, Label}]), + #w{name = Name, id = wxButton:getId(B), wx = B} + end, + Buttons = lists:map(MakeButton, ButtonTemplates), + AddButton = fun(#w{wx = B}) -> wxSizer:add(ButtSz, B, zxw:flags(wide)) end, + Codebook = wxNotebook:new(Win, ?wxID_ANY, [{style, ?wxBK_DEFAULT}]), + ok = lists:foreach(AddButton, Buttons), + _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), + _ = wxSizer:add(MainSz, Codebook, zxw:flags(wide)), + _ = wxWindow:setSizer(Win, MainSz), + MapButton = fun(B = #w{id = I}, M) -> maps:put(I, B, M) end, + ButtonMap = lists:foldl(MapButton, #{}, Buttons), + {Win, ButtonMap, Codebook}. + + + +add_code_pages(State, Files) -> + lists:foldl(fun add_code_page/2, State, Files). + + + +%%% OTP callbacks + +handle_call(Unexpected, From, State) -> + ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), + {noreply, State}. + + +handle_cast(to_front, State = #s{frame = Frame}) -> + ok = wxFrame:raise(Frame), + {noreply, State}; +handle_cast({open_contract, Address}, State) -> + NewState = load2(State, Address), + {noreply, NewState}; +handle_cast({trouble, Info}, State) -> + ok = handle_troubling(State, Info), + {noreply, State}; +handle_cast(Unexpected, State) -> + ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_info(Unexpected, State) -> + ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, + id = ID}, + State = #s{buttons = Buttons}) -> + NewState = + case maps:get(ID, Buttons, undefined) of + #w{name = new} -> new_file(State); + #w{name = open} -> open(State); + #w{name = save} -> save(State); + #w{name = rename} -> rename(State); + #w{name = deploy} -> deploy(State); + #w{name = close_source} -> close_source(State); + #w{name = load} -> load(State); + #w{name = edit} -> edit(State); + #w{name = close_instance} -> close_instance(State); + #w{name = Name, wx = Button} -> clicked(State, Name, Button); + undefined -> + tell("Received message: ~w", [E]), + State + end, + {noreply, NewState}; +handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) -> + Geometry = + case wxTopLevelWindow:isMaximized(Frame) of + true -> + max; + false -> + {X, Y} = wxWindow:getPosition(Frame), + {W, H} = wxWindow:getSize(Frame), + {X, Y, W, H} + end, + NewPrefs = maps:put(geometry, Geometry, Prefs), + ok = gmc_con:save(?MODULE, NewPrefs), + ok = wxWindow:destroy(Frame), + {noreply, State}; +handle_event(Event, State) -> + ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), + {noreply, State}. + + +handle_troubling(#s{frame = Frame}, Info) -> + zxw:show_message(Frame, Info). + + +code_change(_, State, _) -> + {ok, State}. + + +terminate(Reason, State) -> + ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), + wx:destroy(). + + + +%%% Doers + +clicked(State = #s{cons = {Consbook, Contracts}}, Name, Button) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + ok = tell(warning, "Inconcievable! No notebook page is selected!"), + State; + Index -> + Contract = lists:nth(Index + 1, Contracts), + clicked(State, Contract, Name, Button) + end. + +clicked(State, Contract, Name, Button) -> + ok = tell("Button: ~p ~p~nContract: ~p", [Button, Name, Contract]), + State. + + +add_code_page(State = #s{code = {Codebook, Pages}}, File) -> + case keyfind_index({file, File}, #p.path, Pages) of + error -> + add_code_page2(State, File); + {ok, Index} -> + _ = wxNotebook:setSelection(Codebook, Index - 1), + State + end. + +add_code_page2(State = #s{j = J}, {file, File}) -> + case file:read_file(File) of + {ok, Bin} -> + case unicode:characters_to_list(Bin) of + Code when is_list(Code) -> + add_code_page(State, {file, File}, Code); + Error -> + Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Error]), + ok = handle_troubling(Message, State), + State + end; + {error, Reason} -> + Message = io_lib:format(J("Opening ~p failed with: ~p"), [File, Reason]), + ok = handle_troubling(Message, State), + State + end; +add_code_page2(State, {hash, Address}) -> + open_hash2(State, Address). + +add_code_page(State = #s{tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> +% FIXME: One of these days we need to define the text area as a wxStyledTextCtrl and will +% have to contend with system theme issues (light/dark themese, namely) +% Leaving this little thing here to remind myself how any of that works later. +% The call below returns a wx_color4() type (not that we need alpha...). +% Color = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW), +% tell("Color: ~p", [Color]), + Window = wxWindow:new(Codebook, ?wxID_ANY), + PageSz = wxBoxSizer:new(?wxHORIZONTAL), + + CodeTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_PROCESS_TAB bor ?wxTE_DONTWRAP}, + CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), + TextAt = wxTextAttr:new(), + Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), + ok = wxTextAttr:setFont(TextAt, Mono), + true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), + ok = wxTextCtrl:setValue(CodeTx, Code), + + _ = wxSizer:add(PageSz, CodeTx, zxw:flags(wide)), + + ok = wxWindow:setSizer(Window, PageSz), + ok = wxSizer:layout(PageSz), + _ = wxNotebook:changeSelection(TopBook, 0), + FileName = + case Location of + {file, Path} -> filename:basename(Path); + {hash, Addr} -> Addr + end, + true = wxNotebook:addPage(Codebook, Window, FileName, [{bSelect, true}]), + Page = #p{path = Location, win = Window, code = CodeTx}, + NewPages = Pages ++ [Page], + State#s{code = {Codebook, NewPages}}. + + +new_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> + DefaultDir = + case maps:find(dir, Prefs) of + {ok, PrefDir} -> + PrefDir; + error -> + case os:getenv("ZOMP_DIR") of + "" -> file:get_pwd(); + D -> filename:basename(D) + end + end, + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, "my_contract.aes"}, + {wildCard, "*.aes"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + Path = filename:join(Dir, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NextState = State#s{prefs = NewPrefs}, + add_code_page(NextState, {file, Path}, "") + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState. + + +deploy(State = #s{code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + #p{code = CodeTx} = lists:nth(Index + 1, Pages), + Source = wxTextCtrl:getValue(CodeTx), + deploy2(State, Source) + end. + +deploy2(State, Source) -> + case so_compiler:from_string(Source, [{aci, json}]) of + {ok, Build} -> + deploy3(State, Build); + Other -> + ok = tell(info, "Compilation Failed!~n~tp", [Other]), + State + end. + +deploy3(State, Build) -> + case gmc_con:list_keys() of + {ok, 0, []} -> + handle_troubling(State, "No keys exist in the current wallet."); + {ok, Selected, Keys} -> + deploy4(State, Build, Selected, Keys); + error -> + handle_troubling(State, "No wallet is selected!") + end. + +deploy4(State = #s{frame = Frame, j = J}, Build = #{aci := ACI}, Selected, Keys) -> + {#{functions := Funs}, _} = find_main(ACI), + #{arguments := As} = lom:find(name, <<"init">>, Funs), + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + ScrollWin = wxScrolledWindow:new(Dialog), + FunName = unicode:characters_to_list(["init/", integer_to_list(length(As))]), + FunSizer = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), + ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSizer), + ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), + KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Key")}]), + KeyPicker = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Keys}]), + _ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)), + ok = wxChoice:setSelection(KeyPicker, Selected - 1), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + Affirm = wxButton:new(Dialog, ?wxID_OK), + Cancel = wxButton:new(Dialog, ?wxID_CANCEL), + _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), + GridSz = wxFlexGridSizer:new(2, 4, 4), + ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), + ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), + MakeArgField = + fun(#{name := AN, type := T}) -> + Type = + case T of + <<"address">> -> address; + <<"int">> -> integer; + <<"bool">> -> boolean; + L when is_list(L) -> list; % FIXME +% I when is_binary(I) -> iface % FIXME + I when is_binary(I) -> address % FIXME + end, + ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), + TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), + _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), + _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), + {ANT, TCT, Type} + end, + ArgFields = lists:map(MakeArgField, As), + _ = wxStaticBoxSizer:add(FunSizer, GridSz, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ScrollWin, [{proportion, 5}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, KeySz, [{proportion, 0}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, ButtSz, [{proportion, 1}, {flag, ?wxEXPAND}]), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:setSize(Dialog, {500, 300}), + ok = wxDialog:center(Dialog), + Outcome = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)), + BinID = unicode:characters_to_binary(ID), + Inputs = lists:map(fun get_arg/1, ArgFields), + {ok, BinID, Inputs}; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Outcome of + {ok, SigID, Args} -> deploy5(State, SigID, Build, Args); + cancel -> State + end. + +deploy5(State, SigID, Build, Args) -> + tell(info, "Build: ~p", [Build]), + ok = gmc_con:deploy(SigID, Build, Args), + State. + + +open(State = #s{frame = Frame, j = J}) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Open Contract Source")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + Choices = wxRadioBox:new(Dialog, + ?wxID_ANY, + J("Select Origin"), + ?wxDefaultPosition, + ?wxDefaultSize, + [J("From File"), J("From Hash")], + [{majorDim, 1}]), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + Affirm = wxButton:new(Dialog, ?wxID_OK), + Cancel = wxButton:new(Dialog, ?wxID_CANCEL), + _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), + _ = wxSizer:add(Sizer, Choices, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxDialog:setSize(Dialog, {250, 170}), + ok = wxBoxSizer:layout(Sizer), + Choice = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + case wxRadioBox:getSelection(Choices) of + 0 -> file; + 1 -> hash; + ?wxNOT_FOUND -> none + end; + ?wxID_CANCEL -> cancel + end, + ok = wxDialog:destroy(Dialog), + case Choice of + file -> + open_file(State); + hash -> + open_hash(State); + none -> + ok = tell(info, "No selection."), + State; + cancel -> + ok = tell(info, "Cancelled."), + State + end. + + +open_file(State = #s{frame = Frame, j = J, prefs = Prefs}) -> + DefaultDir = + case maps:find(dir, Prefs) of + {ok, PrefDir} -> + PrefDir; + error -> + case os:getenv("ZOMP_DIR") of + "" -> file:get_pwd(); + D -> filename:basename(D) + end + end, + Options = + [{message, J("Load Contract Source")}, + {defaultDir, DefaultDir}, + {wildCard, "*.aes"}, + {style, ?wxFD_OPEN}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + Path = filename:join(Dir, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NextState = State#s{prefs = NewPrefs}, + add_code_page(NextState, {file, Path}) + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState. + + +open_hash(State = #s{frame = Frame, j = J}) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), + AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(AddressSz, AddressTx, zxw:flags(wide)), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + Affirm = wxButton:new(Dialog, ?wxID_OK), + Cancel = wxButton:new(Dialog, ?wxID_CANCEL), + _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), + _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:setSize(Dialog, {500, 200}), + ok = wxDialog:center(Dialog), + ok = wxStyledTextCtrl:setFocus(AddressTx), + Choice = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + case wxTextCtrl:getValue(AddressTx) of + "" -> cancel; + A -> {ok, A} + end; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Choice of + {ok, Address} -> open_hash2(State, Address); + cancel -> State + end. + +open_hash2(State, Address) -> + case hz:contract_source(Address) of + {ok, Source} -> + open_hash3(State, Address, Source); + Error -> + ok = handle_troubling(Error, State), + State + end. + +open_hash3(State, Address, Source) -> + % TODO: Compile on load and verify the deployed hash for validity. + case so_compiler:from_string(Source, [{aci, json}]) of + {ok, Build = #{aci := ACI}} -> + {Defs = #{functions := Funs}, ConIfaces} = find_main(ACI), + Callable = lom:delete(name, <<"init">>, Funs), + FunDefs = {maps:put(functions, Callable, Defs), ConIfaces}, + ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), + add_code_page(State, {hash, Address}, Source); + Other -> + ok = tell(info, "Compilation Failed!~n~tp", [Other]), + State + end. + + +% TODO: Break this down -- tons of things in here recur. +save(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + case lists:nth(Index + 1, Pages) of + #p{path = {file, Path}, code = Widget} -> + Source = wxStyledTextCtrl:getText(Widget), + case filelib:ensure_dir(Path) of + ok -> + case file:write_file(Path, Source) of + ok -> + State; + Error -> + ok = handle_troubling(State, Error), + State + end; + Error -> + ok = handle_troubling(State, Error), + State + end; + Page = #p{path = {hash, Hash}, code = Widget} -> + DefaultDir = + case maps:find(dir, Prefs) of + {ok, PrefDir} -> + PrefDir; + error -> + case os:getenv("ZOMP_DIR") of + "" -> file:get_pwd(); + D -> filename:basename(D) + end + end, + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, unicode:characters_to_list([Hash, ".aes"])}, + {wildCard, "*.aes"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + Path = filename:join(Dir, File), + Source = wxTextCtrl:getValue(Widget), + case filelib:ensure_dir(Path) of + ok -> + case file:write_file(Path, Source) of + ok -> + true = wxNotebook:setPageText(Codebook, Index, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NewPage = Page#p{path = {file, Path}}, + NewPages = store_nth(Index + 1, NewPage, Pages), + NewCode = {Codebook, NewPages}, + State#s{prefs = NewPrefs, code = NewCode}; + Error -> + ok = handle_troubling(State, Error), + State + end; + Error -> + ok = handle_troubling(State, Error), + State + end + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState + end + end. + +% TODO: Break this down -- tons of things in here recur. +rename(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + case lists:nth(Index + 1, Pages) of + Page = #p{path = {file, Path}, code = Widget} -> + DefaultDir = filename:dirname(Path), + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, filename:basename(Path)}, + {wildCard, "*.aes"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + NewState = + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + case wxFileDialog:getFilename(Dialog) of + "" -> + State; + Name -> + File = + case filename:extension(Name) of + ".aes" -> Name; + _ -> Name ++ ".aes" + end, + NewPath = filename:join(Dir, File), + Source = wxTextCtrl:getValue(Widget), + case filelib:ensure_dir(NewPath) of + ok -> + case file:write_file(NewPath, Source) of + ok -> + true = wxNotebook:setPageText(Codebook, Index, File), + NewPrefs = maps:put(dir, Dir, Prefs), + NewPage = Page#p{path = {file, NewPath}}, + NewPages = store_nth(Index + 1, NewPage, Pages), + NewCode = {Codebook, NewPages}, + State#s{prefs = NewPrefs, code = NewCode}; + Error -> + ok = handle_troubling(State, Error), + State + end; + Error -> + ok = handle_troubling(State, Error), + State + end + end; + ?wxID_CANCEL -> + State + end, + ok = wxFileDialog:destroy(Dialog), + NewState; + #p{path = {hash, _}} -> + save(State) + end + end. + + +close_source(State = #s{code = {Codebook, Pages}}) -> + case wxNotebook:getSelection(Codebook) of + ?wxNOT_FOUND -> + State; + Index -> + NewPages = drop_nth(Index + 1, Pages), + true = wxNotebook:deletePage(Codebook, Index), + State#s{code = {Codebook, NewPages}} + end. + + +load(State = #s{frame = Frame, j = J}) -> + % TODO: Extract the exact compiler version, load it, and use only that or fail if + % the specific version is unavailable. + % TODO: Compile on load and verify the deployed hash for validity. + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Retrieve Contract Source")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + AddressSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Address Hash")}]), + AddressTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(AddressSz, AddressTx, zxw:flags(wide)), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + Affirm = wxButton:new(Dialog, ?wxID_OK), + Cancel = wxButton:new(Dialog, ?wxID_CANCEL), + _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), + _ = wxSizer:add(Sizer, AddressSz, zxw:flags(wide)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:setSize(Dialog, {500, 200}), + ok = wxDialog:center(Dialog), + ok = wxStyledTextCtrl:setFocus(AddressTx), + Choice = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + case wxTextCtrl:getValue(AddressTx) of + "" -> cancel; + A -> {ok, A} + end; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Choice of + {ok, Address} -> load2(State, Address); + cancel -> State + end. + +load2(State, Address) -> + case hz:contract_source(Address) of + {ok, Source} -> + load3(State, Address, Source); + Error -> + ok = handle_troubling(Error, State), + State + end. + +load3(State = #s{tabs = TopBook, cons = {Consbook, Pages}, buttons = Buttons, j = J}, + Address, + Source) -> + Window = wxWindow:new(Consbook, ?wxID_ANY), + PageSz = wxBoxSizer:new(?wxVERTICAL), + ProgSz = wxBoxSizer:new(?wxHORIZONTAL), + CodeSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Contract Source")}]), + CodeTxStyle = {style, ?wxTE_MULTILINE + bor ?wxTE_PROCESS_TAB + bor ?wxTE_DONTWRAP + bor ?wxTE_READONLY}, + CodeTx = wxTextCtrl:new(Window, ?wxID_ANY, [CodeTxStyle]), + Mono = wxFont:new(10, ?wxMODERN, ?wxNORMAL, ?wxNORMAL, [{face, "Monospace"}]), + TextAt = wxTextAttr:new(), + ok = wxTextAttr:setFont(TextAt, Mono), + true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), + ok = wxTextCtrl:setValue(CodeTx, Source), + _ = wxSizer:add(CodeSz, CodeTx, zxw:flags(wide)), + ScrollWin = wxScrolledWindow:new(Window), + FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Function Interfaces")}]), + ok = wxWindow:setSizer(ScrollWin, FunSz), + ConsSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Console")}]), + ConsTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}, + ConsTx = wxTextCtrl:new(Window, ?wxID_ANY, [ConsTxStyle]), + _ = wxSizer:add(ConsSz, ConsTx, zxw:flags(wide)), + _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 3}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(ProgSz, ScrollWin, [{proportion, 1}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(PageSz, ProgSz, [{proportion, 3}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(PageSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND}]), + {Out, IFaces, Build, NewButtons} = + case so_compiler:from_string(Source, [{aci, json}]) of + {ok, B = #{aci := ACI}} -> + {#{functions := Fs}, _} = find_main(ACI), + Callable = lom:delete(name, <<"init">>, Fs), + {NB, IFs} = fun_interfaces(ScrollWin, FunSz, Buttons, Callable, J), + O = io_lib:format("Compilation Succeeded!~n~tp~n~nDone!~n", [B]), + {O, IFs, B, NB}; + Other -> + O = io_llib:format("Compilation Failed!~n~tp~n", [Other]), + {O, [], none, Buttons} + end, + ok = wxWindow:setSizer(Window, PageSz), + ok = wxSizer:layout(PageSz), + true = wxNotebook:addPage(Consbook, Window, Address, [{bSelect, true}]), + Page = #c{id = Address, win = Window, + code = CodeTx, cons = ConsTx, + build = Build, funs = {ScrollWin, IFaces}}, + NewPages = Pages ++ [Page], + ok = wxTextCtrl:appendText(ConsTx, Out), + _ = wxNotebook:changeSelection(TopBook, 1), + % TODO: Verify the deployed hash for validity. + State#s{cons = {Consbook, NewPages}, buttons = NewButtons}. + + +get_arg({_, TextCtrl, _}) -> + wxTextCtrl:getValue(TextCtrl). + +find_main(ACI) -> + find_main(ACI, none, []). + +find_main([#{contract := I = #{kind := contract_interface}} | T], M, Is) -> + find_main(T, M, [I | Is]); +find_main([#{contract := M = #{kind := contract_main}} | T], _, Is) -> + find_main(T, M, Is); +find_main([#{namespace := _} | T], M, Is) -> + find_main(T, M, Is); +find_main([C | T], M, Is) -> + ok = tell("Surprising ACI element: ~p", [C]), + find_main(T, M, Is); +find_main([], M, Is) -> + {M, Is}. + +fun_interfaces(ScrollWin, FunSz, Buttons, Funs, J) -> + MakeIface = + fun(#{name := N, arguments := As}) -> + FunName = unicode:characters_to_list([N, "/", integer_to_list(length(As))]), + FN = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), + GridSz = wxFlexGridSizer:new(2, 4, 4), + ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), + ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), + MakeArgField = + fun(#{name := AN, type := T}) -> + Type = + case T of + <<"address">> -> address; + <<"int">> -> integer; + <<"bool">> -> boolean; + L when is_list(L) -> list; % FIXME +% I when is_binary(I) -> iface % FIXME + I when is_binary(I) -> address % FIXME + end, + ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), + TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), + _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), + _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), + {ANT, TCT, Type} + end, + ArgFields = lists:map(MakeArgField, As), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + {CallButton, DryRunButton} = + case N =:= <<"init">> of + false -> + CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]), + DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]), + _ = wxBoxSizer:add(ButtSz, CallBn, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, DryRBn, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(CallBn), wx = CallBn}, + #w{name = {N, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}}; + true -> + Deploy = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Deploy")}]), + _ = wxBoxSizer:add(ButtSz, Deploy, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(Deploy), wx = Deploy}, + none} + end, + _ = wxStaticBoxSizer:add(FN, GridSz, zxw:flags(wide)), + _ = wxStaticBoxSizer:add(FN, ButtSz, zxw:flags(base)), + _ = wxSizer:add(FunSz, FN, zxw:flags(base)), + #f{name = N, call = CallButton, dryrun = DryRunButton, args = ArgFields} + end, + IFaces = lists:map(MakeIface, Funs), + NewButtons = lists:foldl(fun map_iface_buttons/2, Buttons, IFaces), + {NewButtons, IFaces}. + +button_key_list([#f{call = #w{id = C}, dryrun = #w{id = D}} | T]) -> + [C, D | button_key_list(T)]; +button_key_list([#f{call = #w{id = C}, dryrun = none} | T]) -> + [C | button_key_list(T)]; +button_key_list([]) -> + []. + +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) -> + maps:put(DID, D, maps:put(CID, C, A)); +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = none}, A) -> + maps:put(CID, C, A). + + +edit(State = #s{cons = {Consbook, Pages}}) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + State; + Index -> + #c{code = CodeTx} = lists:nth(Index + 1, Pages), + Address = wxNotebook:getPageText(Consbook, Index), + Source = wxTextCtrl:getValue(CodeTx), + add_code_page(State, {hash, Address}, Source) + end. + + +close_instance(State = #s{cons = {Consbook, Pages}, buttons = Buttons}) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + State; + Index -> + {#c{funs = {_, IFaces}}, NewPages} = take_nth(Index + 1, Pages), + IDs = list_iface_buttons(IFaces), + NewButtons = maps:without(IDs, Buttons), + true = wxNotebook:deletePage(Consbook, Index), + State#s{cons = {Consbook, NewPages}, buttons = NewButtons} + end. + +list_iface_buttons(IFaces) -> + lists:foldl(fun list_iface_buttons/2, [], IFaces). + +list_iface_buttons(#f{call = #w{id = CID}, dryrun = #w{id = DID}}, A) -> + [CID, DID | A]. + + + +%% (Somewhat silly) Data operations + +store_nth(1, E, [_ | T]) -> [E | T]; +store_nth(N, E, [H | T]) -> [H | store_nth(N - 1, E, T)]. + + +drop_nth(1, [_ | T]) -> T; +drop_nth(N, [H | T]) -> [H | drop_nth(N - 1, T)]. + +take_nth(N, L) -> + take_nth(N, L, []). + +take_nth(1, [E | T], A) -> {E, lists:reverse(A) ++ T}; +take_nth(N, [H | T], A) -> take_nth(N - 1, T, [H | A]). + + +keyfind_index(K, E, L) -> + keyfind_index(K, E, 1, L). + +keyfind_index(K, E, I, [H | T]) -> + case element(E, H) =:= K of + false -> keyfind_index(K, E, I + 1, T); + true -> {ok, I} + end; +keyfind_index(_, _, _, []) -> + error. diff --git a/src/clutch.erl b/src/clutch.erl index ead5b07..4ebecdc 100644 --- a/src/clutch.erl +++ b/src/clutch.erl @@ -59,6 +59,7 @@ start(normal, _Args) -> end, ok = application:ensure_started(hakuzaru), ok = application:ensure_started(zxwidgets), + ok = application:ensure_started(sophia), gmc_sup:start_link(). diff --git a/src/gmc_con.erl b/src/gmc_con.erl index 1753774..f3fa634 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -14,9 +14,9 @@ selected/1, password/2, refresh/0, - nonce/1, spend/2, chain/1, grids/1, sign_mess/1, sign_tx/1, - deploy/2, - make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1, + nonce/1, spend/2, chain/1, grids/1, sign_mess/1, sign_tx/1, sign_call/3, dry_run/2, + deploy/3, + make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1, list_keys/0, add_node/1, set_sole_node/1]). -export([encrypt/2, decrypt/2]). -export([save/2]). @@ -170,15 +170,33 @@ sign_tx(Request) -> gen_server:cast(?MODULE, {sign_tx, Request}). --spec deploy(Build, InitArgs) -> Result - when Build :: map(), - InitArgs :: [Arg :: string()], - Result :: {ok, TX_Hash :: clutch:id()} - | {error, Reason}, - Reason :: term(). % FIXME +-spec sign_call(ConID, PubKey, TX) -> ok + when ConID :: clutch:id(), + PubKey :: clutch:id(), + TX :: binary(). -deploy(Build, InitArgs) -> - gen_server:call(?MODULE, {deploy, Build, InitArgs}). +sign_call(ConID, PubKey, TX) -> + gen_server:cast(?MODULE, {sign_call, ConID, PubKey, TX}). + + +-spec dry_run(ConID, TX) -> ok + when ConID :: clutch:id(), + TX :: binary(). + +dry_run(ConID, TX) -> + gen_server:cast(?MODULE, {dry_run, ConID, TX}). + + +-spec deploy(CreatorID, Build, InitArgs) -> Result + when CreatorID :: clutch:id(), + Build :: map(), + InitArgs :: [Arg :: string()], + Result :: {ok, TX_Hash :: clutch:id()} + | {error, Reason}, + Reason :: term(). % FIXME + +deploy(CreatorID, Build, InitArgs) -> + gen_server:cast(?MODULE, {deploy, CreatorID, Build, InitArgs}). -spec make_key(Type, Size, Name, Seed, Encoding, Transform) -> ok @@ -230,6 +248,14 @@ drop_key(ID) -> gen_server:cast(?MODULE, {drop_key, ID}). +-spec list_keys() -> Result + when Result :: {ok, Selected :: non_neg_integer(), Keys :: [clutch:id()]} + | error. + +list_keys() -> + gen_server:call(?MODULE, list_keys). + + -spec add_node(New) -> ok when New :: #node{}. @@ -315,6 +341,9 @@ read_prefs() -> %% The gen_server:handle_call/3 callback. %% See: http://erlang.org/doc/man/gen_server.html#Module:handle_call-3 +handle_call(list_keys, _, State) -> + Response = do_list_keys(State), + {reply, Response, State}; handle_call({nonce, ID}, _, State) -> Response = do_nonce(ID), {reply, Response, State}; @@ -324,9 +353,6 @@ handle_call({save, Module, Prefs}, _, State) -> handle_call({mnemonic, ID}, _, State) -> Response = do_mnemonic(ID, State), {reply, Response, State}; -handle_call({deploy, Build, InitArgs}, _, State) -> - Result = do_deploy(Build, InitArgs, State), - {reply, Result, State}; handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. @@ -384,6 +410,15 @@ handle_cast({sign_mess, Request}, State) -> handle_cast({sign_tx, Request}, State) -> ok = do_sign_tx(Request, State), {noreply, State}; +handle_cast({sign_call, ConID, PubKey, TX}, State) -> + ok = do_sign_call(State, ConID, PubKey, TX), + {noreply, State}; +handle_cast({dry_run, ConID, TX}, State) -> + ok = do_dry_run(ConID, TX), + {noreply, State}; +handle_cast({deploy, CreatorID, Build, InitArgs}, State) -> + ok = do_deploy(CreatorID, Build, InitArgs, State), + {noreply, State}; handle_cast({make_key, Name, Seed, Encoding, Transform}, State) -> NewState = do_make_key(Name, Seed, Encoding, Transform, State), {noreply, NewState}; @@ -613,8 +648,8 @@ do_grids_sig2(WTF) -> do_sign_mess(Request = #{"public_id" := ID, "payload" := Message}, #s{wallet = #wallet{keys = Keys}}) -> case lists:keyfind(ID, #key.id, Keys) of - #key{pair = #{secret := PrivKey}} -> - Sig = base64:encode(sign_message(list_to_binary(Message), PrivKey)), + #key{pair = #{secret := SecKey}} -> + Sig = base64:encode(sign_message(list_to_binary(Message), SecKey)), do_sign_mess2(Request#{"signature" => Sig}); false -> gmc_gui:trouble({bad_key, ID}) @@ -638,13 +673,13 @@ do_sign_mess2(Request = #{"url" := URL}) -> % TODO: Should probably be part of Hakuzaru -sign_message(Message, PrivKey) -> +sign_message(Message, SecKey) -> Prefix = <<"Gajumaru Signed Message:\n">>, {ok, PSize} = vencode(byte_size(Prefix)), {ok, MSize} = vencode(byte_size(Message)), Smashed = iolist_to_binary([PSize, Prefix, MSize, Message]), {ok, Hashed} = eblake2:blake2b(32, Smashed), - ecu_eddsa:sign_detached(Hashed, PrivKey). + ecu_eddsa:sign_detached(Hashed, SecKey). vencode(N) when N < 0 -> @@ -672,9 +707,9 @@ do_sign_tx(Request = #{"public_id" := ID, "payload" := CallData, "network_id" := #s{wallet = #wallet{keys = Keys}}) -> BinNID = list_to_binary(NID), case lists:keyfind(ID, #key.id, Keys) of - #key{pair = #{secret := PrivKey}} -> + #key{pair = #{secret := SecKey}} -> BinaryTX = list_to_binary(CallData), - SignedTX = sign_tx_hash(BinaryTX, PrivKey, BinNID), + SignedTX = sign_tx_hash(BinaryTX, SecKey, BinNID), do_sign_tx2(Request#{"signed" => true, "payload" := SignedTX}); false -> gmc_gui:trouble({bad_key, ID}) @@ -696,11 +731,11 @@ do_sign_tx2(Request = #{"url" := URL}) -> Error -> gmc_gui:trouble(Error) end. -sign_tx_hash(Unsigned, PrivKey, NetworkID) -> - {ok, TX_Data} = aeser_api_encoder:safe_decode(transaction, Unsigned), +sign_tx_hash(Unsigned, SecKey, NetworkID) -> + {ok, TX_Data} = gmser_api_encoder:safe_decode(transaction, Unsigned), {ok, Hash} = eblake2:blake2b(32, TX_Data), NetworkHash = <>, - Signature = ecu_eddsa:sign_detached(NetworkHash, PrivKey), + Signature = ecu_eddsa:sign_detached(NetworkHash, SecKey), SigTxType = signed_tx, SigTxVsn = 1, SigTemplate = @@ -709,19 +744,57 @@ sign_tx_hash(Unsigned, PrivKey, NetworkID) -> TX = [{signatures, [Signature]}, {transaction, TX_Data}], - SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX), - aeser_api_encoder:encode(transaction, SignedTX). + SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX), + gmser_api_encoder:encode(transaction, SignedTX). + + +do_sign_call(#s{wallet = #wallet{keys = Keys, chain_id = ChainID}}, + ConID, + PubKey, + TX) -> + #key{pair = #{secret := SecKey}} = lists:keyfind(PubKey, #key.id, Keys), + SignedTX = sign_tx_hash(TX, SecKey, ChainID), + case hz:post_tx(SignedTX) of + {ok, Data = #{"tx_hash" := TXHash}} -> + ok = tell("Contract deploy TX succeded with: ~p", [TXHash]), + do_sign_call2(ConID, Data); + {ok, WTF} -> + gmc_v_devman:trouble({error, WTF}); + Error -> + gmc_v_devman:trouble(Error) + end; +do_sign_call(_, _, _, _) -> + gmc_v_devman:trouble({error, no_chain}). + +do_sign_call2(ConID, #{"tx_hash" := TXHash}) -> + case hz:tx_info(TXHash) of + {ok, CallInfo = #{"call_info" := #{"return_type" := "ok"}}} -> + gmc_v_devman:call_result(ConID, CallInfo); + {error, "Tx not mined"} -> + gmc_v_devman:trouble({tx_hash, TXHash}); + {ok, Reason = #{"call_info" := #{"return_type" := "revert"}}} -> + gmc_v_devman:trouble({error, Reason}); + Error -> + gmc_v_devman:trouble(Error) + end. + + +do_dry_run(ConID, TX) -> + case hz:dry_run(TX) of + {ok, Result} -> gmc_v_devman:dryrun_result(ConID, Result); + Other -> gmc_v_devmam:trouble({error, ConID, Other}) +end. do_spend(KeyID, TX, State = #s{wallet = #wallet{keys = Keys}}) -> case lists:keyfind(KeyID, #key.id, Keys) of - #key{pair = #{secret := PrivKey}} -> - do_spend2(PrivKey, TX, State); + #key{pair = #{secret := SecKey}} -> + do_spend2(SecKey, TX, State); false -> log(warning, "Tried do_spend with a bad key: ~p", [KeyID]) end. -do_spend2(PrivKey, +do_spend2(SecKey, #spend_tx{sender_id = SenderID, recipient_id = RecipientID, amount = Amount, @@ -751,9 +824,9 @@ do_spend2(PrivKey, {ttl, int}, {nonce, int}, {payload, binary}], - BinaryTX = aeser_chain_objects:serialize(Type, Vsn, Template, Fields), + BinaryTX = gmser_chain_objects:serialize(Type, Vsn, Template, Fields), NetworkTX = <>, - Signature = ecu_eddsa:sign_detached(NetworkTX, PrivKey), + Signature = ecu_eddsa:sign_detached(NetworkTX, SecKey), SigTxType = signed_tx, SigTxVsn = 1, SigTemplate = @@ -762,12 +835,18 @@ do_spend2(PrivKey, TX_Data = [{signatures, [Signature]}, {transaction, BinaryTX}], - SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data), - Encoded = aeser_api_encoder:encode(transaction, SignedTX), + SignedTX = gmser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data), + Encoded = gmser_api_encoder:encode(transaction, SignedTX), Outcome = hz:post_tx(Encoded), tell("Outcome: ~p", [Outcome]). +do_list_keys(#s{selected = Selected, wallet = #wallet{poas = POAs}}) -> + {ok, Selected, [ID || #poa{id = ID} <- POAs]}; +do_list_keys(#s{wallet = none}) -> + error. + + do_nonce(ID) -> hz:next_nonce(ID). @@ -899,37 +978,39 @@ do_mnemonic(ID, #s{wallet = #wallet{keys = Keys}}) -> end. -do_deploy(Build, +do_deploy(CreatorID, + Build, InitArgs, - #s{selected = Index, wallet = #wallet{keys = Keys, chain_id = ChainID}}) -> - #key{pair = #{public := PubKey, secret := SecKey}} = lists:nth(Index, Keys), - case hz:contract_create_built(PubKey, Build, InitArgs) of + #s{wallet = #wallet{keys = Keys, chain_id = ChainID}}) -> + #key{pair = #{secret := SecKey}} = lists:keyfind(CreatorID, #key.id, Keys), + case hz:contract_create_built(CreatorID, Build, InitArgs) of {ok, CreateTX} -> do_deploy2(SecKey, CreateTX, ChainID); - Error -> Error + Error -> gmc_v_devman:trouble(Error) end. do_deploy2(SecKey, CreateTX, ChainID) -> SignedTX = sign_tx_hash(CreateTX, SecKey, ChainID), + tell(info, "SignedTX: ~p", [SignedTX]), case hz:post_tx(SignedTX) of {ok, Data = #{"tx_hash" := TXHash}} -> ok = tell("Contract deploy TX succeded with: ~p", [TXHash]), do_deploy3(Data); {ok, WTF} -> - {error, WTF}; + gmc_v_devman:trouble({error, WTF}); Error -> - Error + gmc_v_devman:trouble(Error) end. do_deploy3(#{"tx_hash" := TXHash}) -> case hz:tx_info(TXHash) of {ok, #{"call_info" := #{"return_type" := "ok", "contract_id" := ConID}}} -> - {contract_id, ConID}; + gmc_v_devman:open_contract(ConID); {error, "Tx not mined"} -> - {tx_hash, TXHash}; + gmc_v_devman:trouble({tx_hash, TXHash}); {ok, Reason = #{"call_info" := #{"return_type" := "revert"}}} -> - {error, Reason}; + gmc_v_devman:trouble({error, Reason}); Error -> - Error + gmc_v_devman:trouble(Error) end. @@ -1136,7 +1217,7 @@ do_close_wallet(State = #s{wallet = Current, wallets = Wallets, pass = Pass}) -> #wallet{name = Name} = Current, RW = lists:keyfind(Name, #wr.name, Wallets), ok = save_wallet(RW, Pass, Current), - State#s{pass = none, wallet = none}. + State#s{selected = 0, pass = none, wallet = none}. save_wallet(#wr{path = Path, pass = false}, none, Wallet) -> diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 7e2ba22..730e93e 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -765,7 +765,7 @@ spend2(#poa{id = ID, name = Name}, Nonce, Height, State = #s{frame = Frame, j = ?wxID_OK -> {ok, PK} = decode_account_id(ID), TX = - #spend_tx{sender_id = aeser_id:create(account, PK), + #spend_tx{sender_id = gmser_id:create(account, PK), recipient_id = wxTextCtrl:getValue(ToTx), amount = wxTextCtrl:getValue(AmtTx), gas_price = wxSlider:getValue(GasSl), @@ -784,7 +784,7 @@ clean_spend(_, #spend_tx{recipient_id = ""}) -> ok; clean_spend(ID, TX = #spend_tx{recipient_id = S}) when is_list(S) -> case decode_account_id(S) of - {ok, PK} -> clean_spend(ID, TX#spend_tx{recipient_id = aeser_id:create(account, PK)}); + {ok, PK} -> clean_spend(ID, TX#spend_tx{recipient_id = gmser_id:create(account, PK)}); Error -> tell("Decode recipient_id failed with: ~tp", [Error]) end; clean_spend(ID, TX = #spend_tx{amount = S}) when is_list(S) -> @@ -811,7 +811,7 @@ decode_account_id(S) when is_list(S) -> decode_account_id(list_to_binary(S)); decode_account_id(B) -> try - {account_pubkey, PK} = aeser_api_encoder:decode(B), + {account_pubkey, PK} = gmser_api_encoder:decode(B), {ok, PK} catch E:R -> {E, R} diff --git a/src/gmc_key_master.erl b/src/gmc_key_master.erl index f32a4bb..b259456 100644 --- a/src/gmc_key_master.erl +++ b/src/gmc_key_master.erl @@ -18,21 +18,21 @@ make_key("", <<>>) -> Pair = #{public := Public} = ecu_eddsa:sign_keypair(), - ID = aeser_api_encoder:encode(account_pubkey, Public), + ID = gmser_api_encoder:encode(account_pubkey, Public), Name = binary_to_list(ID), #key{name = Name, id = ID, pair = Pair}; make_key("", Seed) -> Pair = #{public := Public} = ecu_eddsa:sign_seed_keypair(Seed), - ID = aeser_api_encoder:encode(account_pubkey, Public), + ID = gmser_api_encoder:encode(account_pubkey, Public), Name = binary_to_list(ID), #key{name = Name, id = ID, pair = Pair}; make_key(Name, <<>>) -> Pair = #{public := Public} = ecu_eddsa:sign_keypair(), - ID = aeser_api_encoder:encode(account_pubkey, Public), + ID = gmser_api_encoder:encode(account_pubkey, Public), #key{name = Name, id = ID, pair = Pair}; make_key(Name, Seed) -> Pair = #{public := Public} = ecu_eddsa:sign_seed_keypair(Seed), - ID = aeser_api_encoder:encode(account_pubkey, Public), + ID = gmser_api_encoder:encode(account_pubkey, Public), #key{name = Name, id = ID, pair = Pair}. diff --git a/src/gmc_v_devman.erl b/src/gmc_v_devman.erl index 30ed377..83b6269 100644 --- a/src/gmc_v_devman.erl +++ b/src/gmc_v_devman.erl @@ -8,7 +8,7 @@ %-behavior(gmc_v). -include_lib("wx/include/wx.hrl"). -export([to_front/1]). --export([set_manifest/1, trouble/1]). +-export([set_manifest/1, open_contract/1, call_result/2, dryrun_result/2, trouble/1]). -export([start_link/1]). -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). @@ -30,17 +30,18 @@ % Code book pages -record(p, - {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, - win = none :: none | wx:wx_object(), - code = none :: none | wxStyledTextCtrl:wxStyledTextCtrl()}). + {path = {file, ""} :: {file, file:filename()} | {hash, binary()}, + win = none :: none | wx:wx_object(), + code = none :: none | wxTextCtrl:wxTextCtrl()}). % Contract pages -record(c, - {id = "" :: string(), - win = none :: none | wx:wx_object(), - code = none :: none | wxStyledTextCtrl:wxStyledTextCtrl(), - cons = none :: none | wxStyledTextCtrl:wxStyledTextCtrl(), - funs = {#w{}, []} :: {#w{}, [#f{}]}}). + {id = <<"">> :: binary(), + win = none :: none | wx:wx_object(), + code = none :: none | wxTextCtrl:wxTextCtrl(), + cons = none :: none | wxTextCtrl:wxTextCtrl(), + build = none :: none | map(), + funs = {#w{}, []} :: {#w{}, [#f{}]}}). % State -record(s, @@ -65,8 +66,9 @@ to_front(Win) -> wx_object:cast(Win, to_front). +% TODO: Probably kill this -spec set_manifest(Entries) -> ok - when Entries :: [ael:conf_meta()]. + when Entries :: list(). set_manifest(Entries) -> case is_pid(whereis(?MODULE)) of @@ -75,6 +77,29 @@ set_manifest(Entries) -> end. +-spec open_contract(Address) -> ok + when Address :: string(). + +open_contract(Address) -> + wx_object:cast(?MODULE, {open_contract, Address}). + + +-spec call_result(ConID, CallInfo) -> ok + when ConID :: clutch:id(), + CallInfo :: map(). + +call_result(ConID, CallInfo) -> + wx_object:cast(?MODULE, {call_result, ConID, CallInfo}). + + +-spec dryrun_result(ConID, CallInfo) -> ok + when ConID :: clutch:id(), + CallInfo :: map(). + +dryrun_result(ConID, CallInfo) -> + wx_object:cast(?MODULE, {dryrun_result, ConID, CallInfo}). + + -spec trouble(Info) -> ok when Info :: term(). @@ -189,6 +214,15 @@ handle_call(Unexpected, From, State) -> handle_cast(to_front, State = #s{frame = Frame}) -> ok = wxFrame:raise(Frame), {noreply, State}; +handle_cast({open_contract, Address}, State) -> + NewState = load2(State, Address), + {noreply, NewState}; +handle_cast({call_result, ConID, CallInfo}, State) -> + ok = do_call_result(State, ConID, CallInfo), + {noreply, State}; +handle_cast({dryrun_result, ConID, CallInfo}, State) -> + ok = do_dryrun_result(State, ConID, CallInfo), + {noreply, State}; handle_cast({trouble, Info}, State) -> ok = handle_troubling(State, Info), {noreply, State}; @@ -207,16 +241,16 @@ handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, State = #s{buttons = Buttons}) -> NewState = case maps:get(ID, Buttons, undefined) of - #w{name = new} -> new_file(State); - #w{name = open} -> open(State); - #w{name = save} -> save(State); - #w{name = rename} -> rename(State); - #w{name = deploy} -> deploy(State); - #w{name = close_source} -> close_source(State); - #w{name = load} -> load(State); - #w{name = edit} -> edit(State); - #w{name = close_instance} -> close_instance(State); - #w{name = Name, wx = Button} -> clicked(State, Name, Button); + #w{name = new} -> new_file(State); + #w{name = open} -> open(State); + #w{name = save} -> save(State); + #w{name = rename} -> rename(State); + #w{name = deploy} -> deploy(State); + #w{name = close_source} -> close_source(State); + #w{name = load} -> load(State); + #w{name = edit} -> edit(State); + #w{name = close_instance} -> close_instance(State); + #w{name = Name} -> clicked(State, Name); undefined -> tell("Received message: ~w", [E]), State @@ -257,22 +291,124 @@ terminate(Reason, State) -> %%% Doers -clicked(State = #s{cons = {Consbook, Contracts}}, Name, Button) -> +clicked(State = #s{cons = {Consbook, Contracts}}, Name) -> case wxNotebook:getSelection(Consbook) of ?wxNOT_FOUND -> ok = tell(warning, "Inconcievable! No notebook page is selected!"), State; Index -> Contract = lists:nth(Index + 1, Contracts), - clicked(State, Contract, Name, Button) + clicked2(State, Contract, Name) end. -clicked(State, Contract, Name, Button) -> - ok = tell("Button: ~p ~p~nContract: ~p", [Button, Name, Contract]), +clicked2(State, Contract, Name) -> + case gmc_con:list_keys() of + {ok, 0, []} -> + handle_troubling(State, "No keys exist in the current wallet."); + {ok, Selected, Keys} -> + clicked3(State, Contract, Name, Selected, Keys); + error -> + handle_troubling(State, "No wallet is selected!") + end. + +clicked3(State = #s{frame = Frame, j = J}, Contract, Name, Selected, Keys) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Key")}]), + KeyPicker = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Keys}]), + _ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)), + ok = wxChoice:setSelection(KeyPicker, Selected - 1), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + Affirm = wxButton:new(Dialog, ?wxID_OK), + Cancel = wxButton:new(Dialog, ?wxID_CANCEL), + _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), + _ = wxSizer:add(Sizer, KeySz, [{proportion, 0}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, ButtSz, [{proportion, 1}, {flag, ?wxEXPAND}]), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxDialog:center(Dialog), + Outcome = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)), + BinID = unicode:characters_to_binary(ID), + {ok, BinID}; + ?wxID_CANCEL -> + cancel + end, + ok = wxDialog:destroy(Dialog), + case Outcome of + {ok, CallerID} -> clicked4(State, Contract, Name, CallerID); + cancel -> State + end. + +clicked4(State, + #c{id = ConID, build = #{aci := ACI}, funs = {_, Funs}}, + {Name, Type}, + PK) -> + AACI = hz:prepare_aaci(ACI), + #f{args = ArgFields} = lists:keyfind(Name, #f.name, Funs), + Args = lists:map(fun get_arg/1, ArgFields), + FunName = binary_to_list(Name), + {ok, Nonce} = hz:next_nonce(PK), + {ok, Height} = hz:top_height(), + TTL = Height + 10000, + GasP = hz:min_gas_price(), + Gas = 5000000, + Amount = 0, + case hz:contract_call(PK, Nonce, Gas, GasP, Amount, TTL, AACI, ConID, FunName, Args) of + {ok, UnsignedTX} -> + case Type of + call -> do_call(State, ConID, PK, UnsignedTX); + dryr -> do_dry_run(State, ConID, UnsignedTX) + end; + Error -> + handle_troubling(State, Error), + State + end. + +do_call(State, ConID, CallerID, UnsignedTX) -> + ok = gmc_con:sign_call(ConID, CallerID, UnsignedTX), State. -get_arg({_, TextCtrl, _}) -> - wxTextCtrl:getValue(TextCtrl). +do_dry_run(State, ConID, TX) -> + ok = gmc_con:dry_run(ConID, TX), + State. + + +do_call_result(#s{tabs = TopBook, cons = {Consbook, Contracts}}, ConID, CallInfo) -> + case lookup_contract(ConID, Contracts) of + {#c{cons = Console}, ZeroIndex} -> + _ = wxNotebook:changeSelection(TopBook, 1), + _ = wxNotebook:changeSelection(Consbook, ZeroIndex), + Out = io_lib:format("Call Result:~n~p~n~n", [CallInfo]), + wxTextCtrl:appendText(Console, Out); + error -> + tell(info, "Received result for ~p:~n~p ", [ConID, CallInfo]) + end. + + +do_dryrun_result(#s{tabs = TopBook, cons = {Consbook, Contracts}}, ConID, CallInfo) -> + case lookup_contract(ConID, Contracts) of + {#c{cons = Console}, ZeroIndex} -> + _ = wxNotebook:changeSelection(TopBook, 1), + _ = wxNotebook:changeSelection(Consbook, ZeroIndex), + Out = io_lib:format("Call Result:~n~p~n~n", [CallInfo]), + wxTextCtrl:appendText(Console, Out); + error -> + tell(info, "Received result for ~p:~n~p ", [ConID, CallInfo]) + end. + +lookup_contract(ConID, Contracts) -> + lookup_contract(ConID, Contracts, 0). + +lookup_contract(ConID, [Contract = #c{id = ConID} | _], I) -> + {Contract, I}; +lookup_contract(ConID, [#c{} | T], I) -> + lookup_contract(ConID, T, I + 1); +lookup_contract(_, [], _) -> + error. add_code_page(State = #s{code = {Codebook, Pages}}, File) -> @@ -303,7 +439,7 @@ add_code_page2(State = #s{j = J}, {file, File}) -> add_code_page2(State, {hash, Address}) -> open_hash2(State, Address). -add_code_page(State = #s{j = J, tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> +add_code_page(State = #s{tabs = TopBook, code = {Codebook, Pages}}, Location, Code) -> % FIXME: One of these days we need to define the text area as a wxStyledTextCtrl and will % have to contend with system theme issues (light/dark themese, namely) % Leaving this little thing here to remind myself how any of that works later. @@ -385,24 +521,35 @@ deploy(State = #s{code = {Codebook, Pages}}) -> ?wxNOT_FOUND -> State; Index -> - Page = #p{code = CodeTx} = lists:nth(Index + 1, Pages), + #p{code = CodeTx} = lists:nth(Index + 1, Pages), Source = wxTextCtrl:getValue(CodeTx), deploy2(State, Source) end. deploy2(State, Source) -> - case aeso_compiler:from_string(Source, [{aci, json}]) of - {ok, Build = #{aci := ACI}} -> - FunDefs = {#{functions := Funs}, _} = find_main(ACI), - Init = lom:find(name, <<"init">>, Funs), - ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), - deploy3(State, Init); + case compile(Source) of +% Options = sophia_options(), +% case so_compiler:from_string(Source, Options) of + {ok, Build} -> + deploy3(State, Build); Other -> ok = tell(info, "Compilation Failed!~n~tp", [Other]), State end. -deploy3(State = #s{frame = Frame, j = J}, #{arguments := As}) -> +deploy3(State, Build) -> + case gmc_con:list_keys() of + {ok, 0, []} -> + handle_troubling(State, "No keys exist in the current wallet."); + {ok, Selected, Keys} -> + deploy4(State, Build, Selected, Keys); + error -> + handle_troubling(State, "No wallet is selected!") + end. + +deploy4(State = #s{frame = Frame, j = J}, Build = #{aci := ACI}, Selected, Keys) -> + {#{functions := Funs}, _} = find_main(ACI), + #{arguments := As} = lom:find(name, <<"init">>, Funs), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Deploy Contract")), Sizer = wxBoxSizer:new(?wxVERTICAL), ScrollWin = wxScrolledWindow:new(Dialog), @@ -410,7 +557,15 @@ deploy3(State = #s{frame = Frame, j = J}, #{arguments := As}) -> FunSizer = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSizer), ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), - ButtSz = wxDialog:createButtonSizer(Dialog, ?wxOK bor ?wxCANCEL), + KeySz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Signature Key")}]), + KeyPicker = wxChoice:new(Dialog, ?wxID_ANY, [{choices, Keys}]), + _ = wxStaticBoxSizer:add(KeySz, KeyPicker, zxw:flags(wide)), + ok = wxChoice:setSelection(KeyPicker, Selected - 1), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + Affirm = wxButton:new(Dialog, ?wxID_OK), + Cancel = wxButton:new(Dialog, ?wxID_CANCEL), + _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, Cancel, zxw:flags(wide)), GridSz = wxFlexGridSizer:new(2, 4, 4), ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), @@ -433,117 +588,34 @@ deploy3(State = #s{frame = Frame, j = J}, #{arguments := As}) -> end, ArgFields = lists:map(MakeArgField, As), _ = wxStaticBoxSizer:add(FunSizer, GridSz, zxw:flags(wide)), - _ = wxSizer:add(Sizer, ScrollWin, zxw:flags(wide)), - _ = wxSizer:add(Sizer, ButtSz), + _ = wxSizer:add(Sizer, ScrollWin, [{proportion, 5}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, KeySz, [{proportion, 0}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(Sizer, ButtSz, [{proportion, 1}, {flag, ?wxEXPAND}]), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 300}), ok = wxDialog:center(Dialog), - ok = + Outcome = case wxDialog:showModal(Dialog) of ?wxID_OK -> - tell(info, "DEPLOYING!"); + ID = wxChoice:getString(KeyPicker, wxChoice:getSelection(KeyPicker)), + BinID = unicode:characters_to_binary(ID), + Inputs = lists:map(fun get_arg/1, ArgFields), + {ok, BinID, Inputs}; ?wxID_CANCEL -> - ok + cancel end, ok = wxDialog:destroy(Dialog), + case Outcome of + {ok, SigID, Args} -> deploy5(State, SigID, Build, Args); + cancel -> State + end. + +deploy5(State, SigID, Build, Args) -> + tell(info, "Build: ~p", [Build]), + ok = gmc_con:deploy(SigID, Build, Args), State. -find_main(ACI) -> - find_main(ACI, none, []). - -find_main([#{contract := I = #{kind := contract_interface}} | T], M, Is) -> - find_main(T, M, [I | Is]); -find_main([#{contract := M = #{kind := contract_main}} | T], _, Is) -> - find_main(T, M, Is); -find_main([#{namespace := _} | T], M, Is) -> - find_main(T, M, Is); -find_main([C | T], M, Is) -> - ok = tell("Surprising ACI element: ~p", [C]), - find_main(T, M, Is); -find_main([], M, Is) -> - {M, Is}. - -fun_interfaces(Window, - Buttons, - {OldScrollWin, OldIfaces}, - {#{name := Name, functions := Funs}, _ConIfaces}, - J) -> - ok = wxScrolledWindow:destroy(OldScrollWin), - OldButtonIDs = button_key_list(OldIfaces), - NextButtons = maps:without(OldButtonIDs, Buttons), - ScrollWin = wxScrolledWindow:new(Window), - FSOpts = [{label, J("Function Interfaces")}], - FunSizer = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, FSOpts), - ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSizer), - ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), - ConName = wxStaticText:new(ScrollWin, ?wxID_ANY, Name), - _ = wxSizer:add(FunSizer, ConName), - MakeIface = - fun(#{name := N, arguments := As}) -> - FunName = unicode:characters_to_list([N, "/", integer_to_list(length(As))]), - FN = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), - GridSz = wxFlexGridSizer:new(2, 4, 4), - ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), - ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), - MakeArgField = - fun(#{name := AN, type := T}) -> - Type = - case T of - <<"address">> -> address; - <<"int">> -> integer; - <<"bool">> -> boolean; - L when is_list(L) -> list; % FIXME -% I when is_binary(I) -> iface % FIXME - I when is_binary(I) -> address % FIXME - end, - ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), - TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), - _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), - _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), - {ANT, TCT, Type} - end, - ArgFields = lists:map(MakeArgField, As), - ButtSz = wxBoxSizer:new(?wxHORIZONTAL), - {CallButton, DryRunButton} = - case N =:= <<"init">> of - false -> - CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]), - DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]), - true = wxButton:disable(CallBn), - true = wxButton:disable(DryRBn), - _ = wxBoxSizer:add(ButtSz, CallBn, zxw:flags(wide)), - _ = wxBoxSizer:add(ButtSz, DryRBn, zxw:flags(wide)), - {#w{name = {N, call}, id = wxButton:getId(CallBn), wx = CallBn}, - #w{name = {N, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}}; - true -> - Deploy = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Deploy")}]), - _ = wxBoxSizer:add(ButtSz, Deploy, zxw:flags(wide)), - {#w{name = {N, call}, id = wxButton:getId(Deploy), wx = Deploy}, - none} - end, - _ = wxStaticBoxSizer:add(FN, GridSz, zxw:flags(wide)), - _ = wxStaticBoxSizer:add(FN, ButtSz, zxw:flags(base)), - _ = wxSizer:add(FunSizer, FN, zxw:flags(base)), - #f{name = N, call = CallButton, dryrun = DryRunButton, args = ArgFields} - end, - Ifaces = lists:map(MakeIface, Funs), - NewButtons = lists:foldl(fun map_iface_buttons/2, NextButtons, Ifaces), - ok = wxSizer:layout(FunSizer), - {NewButtons, {ScrollWin, Ifaces}}. - -button_key_list([#f{call = #w{id = C}, dryrun = #w{id = D}} | T]) -> - [C, D | button_key_list(T)]; -button_key_list([#f{call = #w{id = C}, dryrun = none} | T]) -> - [C | button_key_list(T)]; -button_key_list([]) -> - []. - -map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) -> - maps:put(DID, D, maps:put(CID, C, A)); -map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = none}, A) -> - maps:put(CID, C, A). - open(State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Open Contract Source")), @@ -649,7 +721,7 @@ open_hash(State = #s{frame = Frame, j = J}) -> ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 200}), ok = wxDialog:center(Dialog), - ok = wxStyledTextCtrl:setFocus(AddressTx), + ok = wxTextCtrl:setFocus(AddressTx), Choice = case wxDialog:showModal(Dialog) of ?wxID_OK -> @@ -677,7 +749,8 @@ open_hash2(State, Address) -> open_hash3(State, Address, Source) -> % TODO: Compile on load and verify the deployed hash for validity. - case aeso_compiler:from_string(Source, [{aci, json}]) of + Options = sophia_options(), + case so_compiler:from_string(Source, Options) of {ok, Build = #{aci := ACI}} -> {Defs = #{functions := Funs}, ConIfaces} = find_main(ACI), Callable = lom:delete(name, <<"init">>, Funs), @@ -698,7 +771,7 @@ save(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) Index -> case lists:nth(Index + 1, Pages) of #p{path = {file, Path}, code = Widget} -> - Source = wxStyledTextCtrl:getText(Widget), + Source = wxTextCtrl:getValue(Widget), case filelib:ensure_dir(Path) of ok -> case file:write_file(Path, Source) of @@ -864,7 +937,7 @@ load(State = #s{frame = Frame, j = J}) -> ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 200}), ok = wxDialog:center(Dialog), - ok = wxStyledTextCtrl:setFocus(AddressTx), + ok = wxTextCtrl:setFocus(AddressTx), Choice = case wxDialog:showModal(Dialog) of ?wxID_OK -> @@ -890,26 +963,12 @@ load2(State, Address) -> State end. -load3(State, Address, Source) -> - % TODO: Compile on load and verify the deployed hash for validity. - case aeso_compiler:from_string(Source, [{aci, json}]) of - {ok, Build = #{aci := ACI}} -> - {Defs = #{functions := Funs}, ConIfaces} = find_main(ACI), - Callable = lom:delete(name, <<"init">>, Funs), - FunDefs = {maps:put(functions, Callable, Defs), ConIfaces}, - ok = tell(info, "Compilation Succeeded!~n~tp~n~n~tp", [Build, FunDefs]), - add_instance_page(State, Address, Source); - Other -> - ok = tell(info, "Compilation Failed!~n~tp", [Other]), - State - end. - -add_instance_page(State = #s{cons = {Consbook, Pages}, j = J}, Address, Source) -> +load3(State = #s{tabs = TopBook, cons = {Consbook, Pages}, buttons = Buttons, j = J}, + Address, + Source) -> Window = wxWindow:new(Consbook, ?wxID_ANY), PageSz = wxBoxSizer:new(?wxVERTICAL), - ProgSz = wxBoxSizer:new(?wxHORIZONTAL), - CodeSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Contract Source")}]), CodeTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_PROCESS_TAB @@ -922,52 +981,175 @@ add_instance_page(State = #s{cons = {Consbook, Pages}, j = J}, Address, Source) true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), ok = wxTextCtrl:setValue(CodeTx, Source), _ = wxSizer:add(CodeSz, CodeTx, zxw:flags(wide)), - ScrollWin = wxScrolledWindow:new(Window), FunSz = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, J("Function Interfaces")}]), - ok = wxWindow:setSizer(ScrollWin, FunSz), - + ok = wxScrolledWindow:setSizerAndFit(ScrollWin, FunSz), + ok = wxScrolledWindow:setScrollRate(ScrollWin, 5, 5), ConsSz = wxStaticBoxSizer:new(?wxVERTICAL, Window, [{label, J("Console")}]), ConsTxStyle = {style, ?wxTE_MULTILINE bor ?wxTE_READONLY}, ConsTx = wxTextCtrl:new(Window, ?wxID_ANY, [ConsTxStyle]), _ = wxSizer:add(ConsSz, ConsTx, zxw:flags(wide)), - - _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 3}, {flag, ?wxEXPAND}]), + _ = wxSizer:add(ProgSz, CodeSz, [{proportion, 2}, {flag, ?wxEXPAND}]), _ = wxSizer:add(ProgSz, ScrollWin, [{proportion, 1}, {flag, ?wxEXPAND}]), - _ = wxSizer:add(PageSz, ProgSz, [{proportion, 3}, {flag, ?wxEXPAND}]), _ = wxSizer:add(PageSz, ConsSz, [{proportion, 1}, {flag, ?wxEXPAND}]), - + {Out, IFaces, Build, NewButtons} = + case compile(Source) of + {ok, B = #{aci := ACI}} -> + {#{functions := Fs}, _} = find_main(ACI), + Callable = lom:delete(name, <<"init">>, Fs), + {NB, IFs} = fun_interfaces(ScrollWin, FunSz, Buttons, Callable, J), + O = io_lib:format("Compilation Succeeded!~n~tp~n~nDone!~n", [B]), + {O, IFs, B, NB}; + Other -> + O = io_llib:format("Compilation Failed!~n~tp~n", [Other]), + {O, [], none, Buttons} + end, ok = wxWindow:setSizer(Window, PageSz), ok = wxSizer:layout(PageSz), true = wxNotebook:addPage(Consbook, Window, Address, [{bSelect, true}]), - Page = #c{id = Address, win = Window, - code = CodeTx, cons = ConsTx, - funs = {ScrollWin, []}}, + Page = #c{id = Address, win = Window, + code = CodeTx, cons = ConsTx, + build = Build, funs = {ScrollWin, IFaces}}, NewPages = Pages ++ [Page], - State#s{cons = {Consbook, NewPages}}. + ok = wxTextCtrl:appendText(ConsTx, Out), + _ = wxNotebook:changeSelection(TopBook, 1), + % TODO: Verify the deployed hash for validity. + State#s{cons = {Consbook, NewPages}, buttons = NewButtons}. -edit(State) -> - ok = tell(info, "EDIT clicked"), - State. +get_arg({_, TextCtrl, _}) -> + wxTextCtrl:getValue(TextCtrl). + +find_main(ACI) -> + find_main(ACI, none, []). + +find_main([#{contract := I = #{kind := contract_interface}} | T], M, Is) -> + find_main(T, M, [I | Is]); +find_main([#{contract := M = #{kind := contract_main}} | T], _, Is) -> + find_main(T, M, Is); +find_main([#{namespace := _} | T], M, Is) -> + find_main(T, M, Is); +find_main([C | T], M, Is) -> + ok = tell("Surprising ACI element: ~p", [C]), + find_main(T, M, Is); +find_main([], M, Is) -> + {M, Is}. + +fun_interfaces(ScrollWin, FunSz, Buttons, Funs, J) -> + MakeIface = + fun(#{name := N, arguments := As}) -> + FunName = unicode:characters_to_list([N, "/", integer_to_list(length(As))]), + FN = wxStaticBoxSizer:new(?wxVERTICAL, ScrollWin, [{label, FunName}]), + GridSz = wxFlexGridSizer:new(2, 4, 4), + ok = wxFlexGridSizer:setFlexibleDirection(GridSz, ?wxHORIZONTAL), + ok = wxFlexGridSizer:addGrowableCol(GridSz, 1), + MakeArgField = + fun(#{name := AN, type := T}) -> + Type = + case T of + <<"address">> -> address; + <<"int">> -> integer; + <<"bool">> -> boolean; + L when is_list(L) -> list; % FIXME +% I when is_binary(I) -> iface % FIXME + I when is_binary(I) -> address % FIXME + end, + ANT = wxStaticText:new(ScrollWin, ?wxID_ANY, AN), + TCT = wxTextCtrl:new(ScrollWin, ?wxID_ANY), + _ = wxFlexGridSizer:add(GridSz, ANT, zxw:flags(base)), + _ = wxFlexGridSizer:add(GridSz, TCT, zxw:flags(wide)), + {ANT, TCT, Type} + end, + ArgFields = lists:map(MakeArgField, As), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + {CallButton, DryRunButton} = + case N =:= <<"init">> of + false -> + CallBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Call")}]), + DryRBn = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Dry Run")}]), + _ = wxBoxSizer:add(ButtSz, CallBn, zxw:flags(wide)), + _ = wxBoxSizer:add(ButtSz, DryRBn, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(CallBn), wx = CallBn}, + #w{name = {N, dryr}, id = wxButton:getId(DryRBn), wx = DryRBn}}; + true -> + Deploy = wxButton:new(ScrollWin, ?wxID_ANY, [{label, J("Deploy")}]), + _ = wxBoxSizer:add(ButtSz, Deploy, zxw:flags(wide)), + {#w{name = {N, call}, id = wxButton:getId(Deploy), wx = Deploy}, + none} + end, + _ = wxStaticBoxSizer:add(FN, GridSz, zxw:flags(wide)), + _ = wxStaticBoxSizer:add(FN, ButtSz, zxw:flags(base)), + _ = wxSizer:add(FunSz, FN, zxw:flags(base)), + #f{name = N, call = CallButton, dryrun = DryRunButton, args = ArgFields} + end, + IFaces = lists:map(MakeIface, Funs), + NewButtons = lists:foldl(fun map_iface_buttons/2, Buttons, IFaces), + {NewButtons, IFaces}. + +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = D = #w{id = DID}}, A) -> + maps:put(DID, D, maps:put(CID, C, A)); +map_iface_buttons(#f{call = C = #w{id = CID}, dryrun = none}, A) -> + maps:put(CID, C, A). -close_instance(State) -> - ok = tell(info, "CLOSE_INSTANCE clicked"), - State. +edit(State = #s{cons = {Consbook, Pages}}) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + State; + Index -> + #c{code = CodeTx} = lists:nth(Index + 1, Pages), + Address = wxNotebook:getPageText(Consbook, Index), + Source = wxTextCtrl:getValue(CodeTx), + add_code_page(State, {hash, Address}, Source) + end. +close_instance(State = #s{cons = {Consbook, Pages}, buttons = Buttons}) -> + case wxNotebook:getSelection(Consbook) of + ?wxNOT_FOUND -> + State; + Index -> + {#c{funs = {_, IFaces}}, NewPages} = take_nth(Index + 1, Pages), + IDs = list_iface_buttons(IFaces), + NewButtons = maps:without(IDs, Buttons), + true = wxNotebook:deletePage(Consbook, Index), + State#s{cons = {Consbook, NewPages}, buttons = NewButtons} + end. + +list_iface_buttons(IFaces) -> + lists:foldl(fun list_iface_buttons/2, [], IFaces). + +list_iface_buttons(#f{call = #w{id = CID}, dryrun = #w{id = DID}}, A) -> + [CID, DID | A]. + + + +%% Incomplete compiler wrangling + +compile(Source) -> + Options = sophia_options(), + so_compiler:from_string(Source, Options). + +sophia_options() -> + [{aci, json}]. + %% (Somewhat silly) Data operations store_nth(1, E, [_ | T]) -> [E | T]; -store_nth(N, E, [H | T]) -> [H | store_nth(N - 1, T, E)]. +store_nth(N, E, [H | T]) -> [H | store_nth(N - 1, E, T)]. drop_nth(1, [_ | T]) -> T; drop_nth(N, [H | T]) -> [H | drop_nth(N - 1, T)]. +take_nth(N, L) -> + take_nth(N, L, []). + +take_nth(1, [E | T], A) -> {E, lists:reverse(A) ++ T}; +take_nth(N, [H | T], A) -> take_nth(N - 1, T, [H | A]). + keyfind_index(K, E, L) -> keyfind_index(K, E, 1, L). diff --git a/zomp.meta b/zomp.meta index 7a03bb0..4e63379 100644 --- a/zomp.meta +++ b/zomp.meta @@ -2,15 +2,15 @@ {type,gui}. {modules,[]}. {prefix,"gmc"}. -{desc,"A desktop client for the Gajumaru network of blockchain networks"}. {author,"Craig Everett"}. +{desc,"A desktop client for the Gajumaru network of blockchain networks"}. {package_id,{"otpr","clutch",{0,2,0}}}. -{deps,[{"otpr","lom",{1,0,0}}, - {"otpr","hakuzaru",{0,2,0}}, - {"otpr","aesophia",{8,0,1}}, - {"otpr","aeserialization",{0,1,2}}, +{deps,[{"otpr","sophia",{9,0,0}}, + {"otpr","hakuzaru",{0,3,0}}, + {"otpr","gmbytecode",{3,4,1}}, + {"otpr","lom",{1,0,0}}, + {"otpr","gmserialization",{0,1,2}}, {"otpr","zj",{1,1,0}}, - {"otpr","aebytecode",{3,2,1}}, {"otpr","erl_base58",{0,1,0}}, {"otpr","eblake2",{1,0,0}}, {"otpr","ec_utils",{1,0,0}},