diff --git a/src/gd_con.erl b/src/gd_con.erl index 172f705..57ade09 100644 --- a/src/gd_con.erl +++ b/src/gd_con.erl @@ -652,7 +652,7 @@ 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 := SecKey}} -> - Sig = base64:encode(sign_message(list_to_binary(Message), SecKey)), + Sig = base64:encode(hz:sign_message(list_to_binary(Message), SecKey)), do_sign_mess2(Request#{"signature" => Sig}); false -> gd_gui:trouble({bad_key, ID}) @@ -675,37 +675,6 @@ do_sign_mess2(Request = #{"url" := URL}) -> end. -% TODO: Should probably be part of Hakuzaru -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, SecKey). - - -vencode(N) when N < 0 -> - {error, {negative_N, N}}; -vencode(N) when N < 16#FD -> - {ok, <>}; -vencode(N) when N =< 16#FFFF -> - NBytes = eu(N, 2), - {ok, <<16#FD, NBytes/binary>>}; -vencode(N) when N =< 16#FFFF_FFFF -> - NBytes = eu(N, 4), - {ok, <<16#FE, NBytes/binary>>}; -vencode(N) when N < (2 bsl 64) -> - NBytes = eu(N, 8), - {ok, <<16#FF, NBytes/binary>>}. - -eu(N, Size) -> - Bytes = binary:encode_unsigned(N, little), - NExtraZeros = Size - byte_size(Bytes), - ExtraZeros = << <<0>> || _ <- lists:seq(1, NExtraZeros) >>, - <>. - - do_sign_tx(Request = #{"public_id" := ID, "payload" := CallData, "network_id" := NID}, #s{wallet = #wallet{keys = Keys}}) -> BinNID = list_to_binary(NID), @@ -862,7 +831,9 @@ do_make_key2(Name, Bin, Transform, #wallet{name = WalletName, poas = POAs, keys = Keys} = Current, T = transform(Transform), Seed = T(Bin), - Key = #key{name = KeyName, id = ID} = gd_key_master:make_key(Name, Seed), + {ID, Pair} = hz_key_master:make_key(Seed), + KeyName = case Name =:= "" of true -> ID; false -> Name end, + Key = #key{name = KeyName, id = ID, pair = Pair}, POA = #poa{name = KeyName, id = ID}, NewKeys = [Key | Keys], NewPOAs = [POA | POAs], @@ -901,7 +872,7 @@ t_xor(B, A) -> do_recover_key(Mnemonic, State) -> - case gd_key_master:decode(Mnemonic) of + case hz_key_master:decode(Mnemonic) of {ok, Seed} -> do_recover_key2(Seed, State); Error -> @@ -911,7 +882,7 @@ do_recover_key(Mnemonic, State) -> do_recover_key2(Seed, State = #s{wallet = Current, wallets = Wallets, pass = Pass}) -> #wallet{name = WalletName, keys = Keys, poas = POAs} = Current, - Recovered = #key{id = ID, name = AccName} = gd_key_master:make_key("", Seed), + Recovered = #key{id = ID, name = AccName} = hz_key_master:make_key("", Seed), case lists:keymember(ID, #key.id, Keys) of false -> NewKeys = [Recovered | Keys], @@ -930,7 +901,7 @@ do_recover_key2(Seed, State = #s{wallet = Current, wallets = Wallets, pass = Pas do_mnemonic(ID, #s{wallet = #wallet{keys = Keys}}) -> case lists:keyfind(ID, #key.id, Keys) of #key{pair = #{secret := <>}} -> - Mnemonic = gd_key_master:encode(K), + Mnemonic = hz_key_master:encode(K), {ok, Mnemonic}; false -> {error, bad_key} diff --git a/src/gd_key_master.erl b/src/gd_key_master.erl deleted file mode 100644 index ff89b07..0000000 --- a/src/gd_key_master.erl +++ /dev/null @@ -1,164 +0,0 @@ -%%% @doc -%%% Key functions go here. -%%% -%%% The main reason this is a module of its own is that in the original architecture -%%% it was a process rather than just a library of functions. Now that it exists, though, -%%% there is little motivation to cram everything here into the controller process's -%%% code. -%%% @end - --module(gd_key_master). --vsn("0.5.4"). - - --export([make_key/2, encode/1, decode/1]). --export([lcg/1]). --include("gd.hrl"). - - -make_key("", <<>>) -> - Pair = #{public := Public} = ecu_eddsa:sign_keypair(), - 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 = 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 = 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 = gmser_api_encoder:encode(account_pubkey, Public), - #key{name = Name, id = ID, pair = Pair}. - - --spec encode(Secret) -> Phrase - when Secret :: binary(), - Phrase :: string(). -%% @doc -%% The encoding and decoding procesures are written to be able to handle any -%% width of bitstring or binary and a variable size dictionary. The magic numbers -%% 32, 4096 and 12 have been dropped in because currently these are known, but that -%% will change in the future if the key size or type changes. - -encode(Bin) -> - <> = Bin, - DictSize = 4096, - Words = read_words(), -% Width = chunksize(DictSize - 1, 2), - Width = 12, - Chunks = chunksize(Number, DictSize), - Binary = <>, - encode(Width, Binary, Words). - -encode(Width, Bits, Words) -> - CheckSum = checksum(Width, Bits), - encode(Width, <>, Words, []). - -encode(_, <<>>, _, Acc) -> - unicode:characters_to_list(lists:join(" ", lists:reverse(Acc))); -encode(Width, Bits, Words, Acc) -> - <> = Bits, - Word = lists:nth(I + 1, Words), - encode(Width, Rest, Words, [Word | Acc]). - - --spec decode(Phrase) -> {ok, Secret} | {error, Reason} - when Phrase :: string(), - Secret :: binary(), - Reason :: bad_phrase | bad_word. -%% @doc -%% Reverses the encoded secret string back into its binary representation. - -decode(Encoded) -> - DictSize = 4096, - Words = read_words(), - Width = chunksize(DictSize - 1, 2), - decode(Width, Words, Encoded). - -decode(Width, Words, Encoded) when is_list(Encoded) -> - decode(Width, Words, list_to_binary(Encoded)); -decode(Width, Words, Encoded) -> - Split = string:lexemes(Encoded, " "), - decode(Width, Words, Split, <<>>). - -decode(Width, Words, [Word | Rest], Acc) -> - case find(Word, Words) of - {ok, N} -> decode(Width, Words, Rest, <>); - Error -> Error - end; -decode(Width, _, [], Acc) -> - sumcheck(Width, Acc). - - -chunksize(N, C) -> - chunksize(N, C, 0). - -chunksize(0, _, A) -> A; -chunksize(N, C, A) -> chunksize(N div C, C, A + 1). - - -read_words() -> - Path = filename:join([zx:get_home(), "priv", "words4096.txt"]), - {ok, Bin} = file:read_file(Path), - string:lexemes(Bin, "\n"). - - -find(Word, Words) -> - find(Word, Words, 0). - -find(Word, [Word | _], N) -> {ok, N}; -find(Word, [_ | Rest], N) -> find(Word, Rest, N + 1); -find(Word, [], _) -> {error, {bad_word, Word}}. - - -checksum(Width, Bits) -> - checksum(Width, Bits, 0). - -checksum(_, <<>>, Sum) -> - Sum; -checksum(Width, Bits, Sum) -> - <> = Bits, - checksum(Width, Rest, N bxor Sum). - - -sumcheck(Width, Bits) -> - <> = Bits, - case checksum(Width, Binary) =:= CheckSum of - true -> - <> = Binary, - {ok, <>}; - false -> - {error, bad_phrase} - end. - - - --spec lcg(integer()) -> integer(). -%% A simple PRNG that fits into 32 bits and is easy to implement anywhere (Kotlin). -%% Specifically, it is a "linear congruential generator" of the Lehmer variety. -%% The constants used are based on recommendations from Park, Miller and Stockmeyer: -%% https://www.firstpr.com.au/dsp/rand31/p105-crawford.pdf#page=4 -%% -%% The input value should be between 1 and 2^31-1. -%% -%% The purpose of this PRNG is for password-based dictionary shuffling. - -lcg(N) -> - M = 16#7FFFFFFF, - A = 48271, - Q = 44488, % M div A - R = 3399, % M rem A - Div = N div Q, - Rem = N rem Q, - S = Rem * A, - T = Div * R, - Result = S - T, - case Result < 0 of - false -> Result; - true -> Result + M - end. diff --git a/src/gd_sophia_editor.erl b/src/gd_sophia_editor.erl new file mode 100644 index 0000000..33e8987 --- /dev/null +++ b/src/gd_sophia_editor.erl @@ -0,0 +1,193 @@ +-module(gd_sophia_editor). +-export([new/1, update/2, + get_text/1, set_text/2]). + +-include("$zx_include/zx_logger.hrl"). +-include_lib("wx/include/wx.hrl"). + + +%%% Formatting Constants + +%% Style labels +-define(DEFAULT, 0). +-define(KEYWORD, 1). +-define(IDENTIFIER, 2). +-define(COMMENT, 3). +-define(STRING, 4). +-define(NUMBER, 5). +-define(OPERATOR, 6). + + + +%% Color palette + +% Intensities: +-define(H, 255). % High +-define(M, 192). % Medium +-define(L, 128). % Low +-define(X, 32). % X-Low +-define(Z, 0). % Zilch + + +% RGB values +% R G B +-define(black, {?Z, ?Z, ?Z}). +-define(light_red, {?H, ?Z, ?Z}). +-define(light_green, {?Z, ?H, ?Z}). +-define(light_blue, {?Z, ?Z, ?H}). +-define(yellow, {?H, ?H, ?Z}). +-define(light_magenta, {?H, ?Z, ?H}). +-define(light_cyan, {?Z, ?H, ?H}). +-define(high_white, {?H, ?H, ?H}). +-define(red, {?L, ?Z, ?Z}). +-define(green, {?Z, ?L, ?Z}). +-define(blue, {?Z, ?Z, ?L}). +-define(brown, {?L, ?L, ?Z}). +-define(magenta, {?L, ?Z, ?L}). +-define(cyan, {?Z, ?L, ?L}). +-define(not_black, {?X, ?X, ?X}). +-define(grey, {?L, ?L, ?L}). +-define(white, {?M, ?M, ?M}). + +styles() -> + [?DEFAULT, + ?KEYWORD, + ?IDENTIFIER, + ?COMMENT, + ?STRING, + ?NUMBER, + ?OPERATOR]. + +palette(light) -> + #{?DEFAULT => ?black, + ?KEYWORD => ?blue, + ?IDENTIFIER => ?cyan, + ?COMMENT => ?grey, + ?STRING => ?red, + ?NUMBER => ?magenta, + ?OPERATOR => ?brown, + bg => ?high_white}; +palette(dark) -> + #{?DEFAULT => ?white, + ?KEYWORD => ?light_cyan, + ?IDENTIFIER => ?green, + ?COMMENT => ?grey, + ?STRING => ?light_red, + ?NUMBER => ?light_magenta, + ?OPERATOR => ?yellow, + bg => ?not_black}. + +color_mode() -> + {R, G, B, _} = wxSystemSettings:getColour(?wxSYS_COLOUR_WINDOW), + case (lists:sum([R, G, B]) div 3) > 128 of + true -> light; + false -> dark + end. + + +new(Parent) -> + STC = wxStyledTextCtrl:new(Parent), + ok = wxStyledTextCtrl:setLexer(STC, ?wxSTC_LEX_CONTAINER), + FontSize = 13, + Mono = wxFont:new(FontSize, + ?wxFONTFAMILY_TELETYPE, + ?wxFONTSTYLE_NORMAL, + ?wxFONTWEIGHT_NORMAL, + [{face, "Monospace"}]), + SetMonospace = fun(Style) -> wxStyledTextCtrl:styleSetFont(STC, Style, Mono) end, + ok = lists:foreach(SetMonospace, styles()), + ok = wxStyledTextCtrl:styleSetFont(STC, ?wxSTC_STYLE_DEFAULT, Mono), + ok = set_colors(STC), + STC. + +get_text(STC) -> + wxStyledTextCtrl:getText(STC). + +set_text(STC, Text) -> + ok = wxStyledTextCtrl:setText(STC, Text), + %% Force Scintilla to request styling for the entire text + wxStyledTextCtrl:colourise(STC, 0, -1). + + +set_colors(STC) -> + ok = wxStyledTextCtrl:styleClearAll(STC), + Palette = #{bg := BGC} = palette(color_mode()), + Colorize = + fun(Style) -> + Color = maps:get(Style, Palette), + ok = wxStyledTextCtrl:styleSetForeground(STC, Style, Color), + ok = wxStyledTextCtrl:styleSetBackground(STC, Style, BGC) + end, + ok = wxStyledTextCtrl:styleSetBackground(STC, ?wxSTC_STYLE_DEFAULT, BGC), + lists:foreach(Colorize, styles()). + + +update(_Event, STC) -> + Text = wxStyledTextCtrl:getText(STC), + case so_scan:scan(Text) of + {ok, Tokens} -> + ok = wxStyledTextCtrl:startStyling(STC, 0), + apply_styles(STC, Tokens); + {error, _Reason} -> + ok + end. + + +apply_styles(STC, Tokens) -> + lists:foreach(fun(Token) -> style_token(STC, Token) end, Tokens). + + +% FIXME: 'qid' is not properly handled. If there are multi-dot qids, they will break +style_token(STC, Token) -> + {Type, Value} = type_and_value(Token), + {StartOffset, LengthOffset} = offset(Type), + {Line, Col} = element(2, Token), + Length = byte_size(to_binary(Value)) + LengthOffset, + Style = classify_style(Type, Value), + Start = wxStyledTextCtrl:positionFromLine(STC, Line - 1) + Col + StartOffset, + wxStyledTextCtrl:startStyling(STC, Start), + wxStyledTextCtrl:setStyling(STC, Length, Style). + +offset(string) -> { 0, 0}; +offset(qid) -> {-1, 1}; +offset(_) -> {-1, 0}. + +to_binary(S) when is_list(S) -> + unicode:characters_to_binary(S); +to_binary(S) when is_binary(S) -> + S; +to_binary(I) when is_integer(I) -> + integer_to_binary(I). + + +classify_style(Type, Value) -> + case Type of + symbol -> + case lists:member(Value, keywords()) of + true -> ?KEYWORD; + false -> ?OPERATOR + end; + id -> ?IDENTIFIER; + qid -> ?IDENTIFIER; + con -> ?IDENTIFIER; + qcon -> ?IDENTIFIER; + tvar -> ?IDENTIFIER; + string -> ?STRING; + char -> ?STRING; + int -> ?NUMBER; + hex -> ?NUMBER; + bytes -> ?NUMBER; + skip -> ?COMMENT; + _ -> ?DEFAULT + end. + + +type_and_value({Type, _Line, Value}) -> {Type, Value}; +type_and_value({Type, _}) -> {Type, atom_to_list(Type)}. + + +keywords() -> + ["contract", "include", "let", "switch", "type", "record", "datatype", "if", + "elif", "else", "function", "stateful", "payable", "true", "false", "mod", + "public", "entrypoint", "private", "indexed", "namespace", "interface", + "main", "using", "as", "for", "hiding", "band", "bor", "bxor", "bnot"]. diff --git a/src/gd_v_devman.erl b/src/gd_v_devman.erl index 4459516..1a0f110 100644 --- a/src/gd_v_devman.erl +++ b/src/gd_v_devman.erl @@ -15,6 +15,8 @@ -include("$zx_include/zx_logger.hrl"). -include("gd.hrl"). +-define(editorMode, sophia). + % Widgets -record(w, {name = none :: atom() | {FunName :: binary(), call | dryr}, @@ -256,6 +258,9 @@ handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, 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 @@ -291,6 +296,14 @@ terminate(Reason, State) -> %%% 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 -> @@ -503,22 +516,13 @@ 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]), + 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), + CodeTx = gd_sophia_editor:new(Window), + ok = gd_sophia_editor:set_text(CodeTx, Code), _ = wxSizer:add(PageSz, CodeTx, zxw:flags(wide)), @@ -530,6 +534,7 @@ add_code_page(State = #s{tabs = TopBook, code = {Codebook, Pages}}, Location, Co {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], @@ -898,7 +903,7 @@ save(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) _ -> Name ++ ".aes" end, Path = filename:join(Dir, File), - Source = wxTextCtrl:getValue(Widget), + Source = get_source(Widget), case filelib:ensure_dir(Path) of ok -> case file:write_file(Path, Source) of @@ -926,6 +931,19 @@ save(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}}) 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 @@ -956,7 +974,7 @@ rename(State = #s{frame = Frame, j = J, prefs = Prefs, code = {Codebook, Pages}} _ -> Name ++ ".aes" end, NewPath = filename:join(Dir, File), - Source = wxTextCtrl:getValue(Widget), + Source = get_source(Widget), case filelib:ensure_dir(NewPath) of ok -> case file:write_file(NewPath, Source) of @@ -1072,7 +1090,7 @@ load3(State = #s{tabs = TopBook, cons = {Consbook, Pages}, buttons = Buttons, j TextAt = wxTextAttr:new(), ok = wxTextAttr:setFont(TextAt, Mono), true = wxTextCtrl:setDefaultStyle(CodeTx, TextAt), - ok = wxTextCtrl:setValue(CodeTx, Source), + 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")}]), diff --git a/zomp.meta b/zomp.meta index 924eea2..c253603 100644 --- a/zomp.meta +++ b/zomp.meta @@ -1,11 +1,11 @@ -{name,"Clutch"}. +{name,"GajuDesk"}. {type,gui}. {modules,[]}. {prefix,"gd"}. {author,"Craig Everett"}. {desc,"A desktop client for the Gajumaru network of blockchain networks"}. {package_id,{"otpr","gajudesk",{0,5,4}}}. -{deps,[{"otpr","hakuzaru",{0,5,1}}, +{deps,[{"otpr","hakuzaru",{0,6,0}}, {"otpr","gmserialization",{0,1,3}}, {"otpr","sophia",{9,0,0}}, {"otpr","gmbytecode",{3,4,1}},