From a286c00783491a2b10e54ed59f894d64dcbef9c7 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Mon, 7 Oct 2024 22:18:54 +0900 Subject: [PATCH] WIP --- include/gmc.hrl | 86 ++++++++++++++--- src/clutch.erl | 4 +- src/gmc_con.erl | 246 ++++++++++++++++++++++++++++++------------------ src/gmc_gui.erl | 56 +++++++++-- 4 files changed, 278 insertions(+), 114 deletions(-) diff --git a/include/gmc.hrl b/include/gmc.hrl index 4c715d3..5bd8985 100644 --- a/include/gmc.hrl +++ b/include/gmc.hrl @@ -1,9 +1,60 @@ --record(ak, - {name = "" :: string(), - id = <<>> :: clutch:id(), - balance = 0 :: non_neg_integer(), - history = [] :: [clutch:tx()], - checked = never :: never | clutch:ts()}). +% Node, Chain and Net represent the physical network. + +-record(node, + {ip = {161,97,102,143} :: inet:ip_address(), + external = 3013 :: inet:port_number(), % 3013 + internal = none :: none | inet:port_number(), % 3113 + rosetta = none :: none | inet:port_number(), % 8080 + channel = none :: none | inet:port_number(), % 3014 + mdw = none :: none | inet:port_number()}). % 4000 + + +-record(chain, + {id = "mint.devnet" :: string(), + coins = ["gaju"] :: [string()], + nodes = [#node{}] :: [#node{}]}). + + +-record(net, + {id = "devnet" :: string(), + chains = [#chain{}] :: [#chain{}]}). + + + +% AC and Coin represent the financial authority graph for a given coin. + +-record(ac, + {id = none :: string(), + acs = [] :: [#ac{}]}). + + +-record(coin, + {id = "gaju" :: string(), + mint = "mint.devnet" :: string(), + acs = [#ac{}] :: [#ac{}]}). + + + +% Balance, POA, Key, TXs, all culminate in capturing a complete Wallet view. +-record(balance, + {coin = "gaju" :: string(), + total = 0 :: non_neg_integer(), + dist = [{"mint.devnet", 0}] :: [{Chain :: string(), non_neg_integer()}]}). + + +-record(poa, + {name = "" :: string(), + id = <<>> :: clutch:id(), + balances = [#balance{}] :: [#balance{}], + history = [] :: [clutch:tx()], + checked = never :: never | clutch:ts()}). + + +-record(key, + {name = "" :: string(), + id = <<>> :: clutch:id(), + pair = #{} :: #{public := binary(), secret := binary()}, + type = {{eddsa, ed25519}, 256} :: {Cipher :: term(), Size :: pos_integer()}}). -record(tx, @@ -12,8 +63,21 @@ type = spend :: spend | atom()}). --record(key, - {name = "" :: string(), - id = <<>> :: clutch:id(), - pair = #{} :: #{public := binary(), secret := binary()}, - type = {{eddsa, ed25519}, 256} :: {Cipher :: term(), Size :: pos_integer()}}). +-record(spend_tx, + {sender_id = <<>> :: clutch:id(), + recipient_id = <<>> :: clutch:id(), + amount = 0 :: non_neg_integer(), + gas_price = 0 :: non_neg_integer(), + gas = 0 :: non_neg_integer(), + ttl = 1 :: pos_integer(), + nonce = 0 :: non_neg_integer(), + payload = <<>> :: binary()}). + + +-record(wallet, + {version = 1 :: integer(), + poas = [] :: [#poa{}], + keys = [] :: [#key{}], + pass = none :: none | binary(), + network_id = <<"mint.devnet">> :: binary(), + chains = [#chain{}] :: [#chain{}]}). diff --git a/src/clutch.erl b/src/clutch.erl index 02ed3f3..6af78ae 100644 --- a/src/clutch.erl +++ b/src/clutch.erl @@ -12,7 +12,7 @@ -export([ts/0]). -export([start/2, stop/1]). --export_type([id/0, key/0, ak/0, tx/0, ts/0]). +-export_type([id/0, key/0, poa/0, tx/0, ts/0]). -include("$zx_include/zx_logger.hrl"). -include("gmc.hrl"). @@ -20,7 +20,7 @@ -type id() :: binary(). -type key() :: #key{}. --type ak() :: #ak{}. +-type poa() :: #poa{}. -type tx() :: #tx{}. -type ts() :: integer(). diff --git a/src/gmc_con.erl b/src/gmc_con.erl index abb91d2..ab681f0 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -11,8 +11,8 @@ -license("GPL-3.0-or-later"). -behavior(gen_server). --export([make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1, - login/1, password/2]). % add_key/1, drop_key/1]). +-export([open_wallet/2, close_wallet/0, password/2, + make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1]). -export([encrypt/2, decrypt/2]). -export([start_link/0, stop/0, save/1]). -export([init/1, terminate/2, code_change/3, @@ -26,26 +26,43 @@ -record(s, - {window = none :: none | wx:wx_object(), - accounts = [] :: [clutch:ak()], - pass = none :: none | binary(), - keys = [] :: [clutch:key()], - chains = [] :: [chain()], - prefs = #{} :: #{atom() := term()}}). + {version = 1 :: integer(), + window = none :: none | wx:wx_object(), + wallet = #wallet{} :: #wallet{}, + prefs = #{} :: #{atom() := term()}}). --record(chain, - {id = "" :: string(), - nodes = [] :: [address()], - mdws = [] :: [address()]}). -type state() :: #s{}. --type chain() :: tuple(). --type address() :: {inet:ip_address(), inet:port_number()}. %% Interface + +-spec open_wallet(Path, Password) -> {ok, Wallet} | {error, Reason} + when Path :: file:filename(), + Password :: string(), + Wallet :: {Accounts :: [clutch:ak()], Selected :: integer()}, + Reason :: bad_password | file:posix(). + +open_wallet(Path, Password) -> + gen_server:call(?MODULE, {open_wallet, Path, Password}). + + +-spec close_wallet() -> ok. + +close_wallet() -> + gen_server:cast(?MODULE, close_wallet). + + +-spec password(Old, New) -> ok + when Old :: none | string(), + New :: none | string(). + +password(Old, New) -> + gen_server:cast(?MODULE, {password, Old, New}). + + -spec make_key(Type, Size, Name, Seed, Encoding, Transform) -> ok when Type :: {eddsa, ed25519}, Size :: 256, @@ -101,21 +118,6 @@ stop() -> gen_server:cast(?MODULE, stop). --spec login(Phrase) -> ok - when Phrase :: string(). - -login(Phrase) -> - gen_server:cast(?MODULE, {login, Phrase}). - - --spec password(Old, New) -> ok - when Old :: none | string(), - New :: none | string(). - -password(Old, New) -> - gen_server:cast(?MODULE, {password, Old, New}). - - -spec save(Prefs) -> ok | {error, Reason} when Prefs :: #{atom() := term()}, Reason :: file:posix(). @@ -147,11 +149,8 @@ init(none) -> ok = log(info, "Starting"), Prefs = read_prefs(), Window = gmc_gui:start_link(Prefs), - ok = log(info, "Window: ~p", [Window]), ok = gmc_gui:ask_password(), - State = #s{window = Window}, - ArgV = zx_daemon:argv(), - ok = gmc_gui:show(ArgV), + State = #s{window = Window, prefs = Prefs}, {ok, State}. @@ -183,6 +182,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({open_wallet, Path, Phrase}, _, State) -> + {Response, NewState} = do_open_wallet(Path, Phrase, State), + {reply, Response, NewState}; handle_call({save, Prefs}, _, State) -> Response = do_save(State#s{prefs = Prefs}), {reply, Response, State}; @@ -214,15 +216,12 @@ handle_cast({rename_key, ID, NewName}, State) -> handle_cast({drop_key, ID}, State) -> NewState = do_drop_key(ID, State), {noreply, NewState}; -handle_cast({login, Phrase}, State) -> - NewState = do_login(Phrase, State), - {noreply, NewState}; handle_cast({password, Old, New}, State) -> NewState = do_password(Old, New, State), {noreply, NewState}; handle_cast(stop, State) -> - ok = log(info, "Received a 'stop' message."), - {stop, normal, State}; + ok = zx:stop(), + {noreply, State}; handle_cast(Unexpected, State) -> ok = tell(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. @@ -284,15 +283,17 @@ do_make_key(Name, Seed, base58, Transform, State) -> State end. -do_make_key2(Name, Bin, Transform, State = #s{accounts = Accounts, keys = Keys}) -> +do_make_key2(Name, Bin, Transform, State = #s{wallet = W}) -> + #wallet{poas = POAs, keys = Keys} = W, T = transform(Transform), Seed = T(Bin), Key = #key{name = KeyName, id = ID} = gmc_key_master:make_key(Name, Seed), - Account = #ak{name = KeyName, id = ID}, + POA = #poa{name = KeyName, id = ID}, NewKeys = [Key | Keys], - NewAccounts = [Account | Accounts], - ok = gmc_gui:show(NewAccounts), - State#s{accounts = NewAccounts, keys = NewKeys}. + NewPOAs = [POA | POAs], + NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys}, + ok = gmc_gui:show(NewPOAs), + State#s{wallet = NewWallet}. base64_decode(String) -> @@ -324,7 +325,7 @@ t_xor(B, A) -> crypto:exor(H, A). -do_recover_key(Mnemonic, State = #s{keys = Keys, accounts = Accounts}) -> +do_recover_key(Mnemonic, State) -> case gmc_key_master:decode(Mnemonic) of {ok, Seed} -> do_recover_key2(Seed, State); @@ -333,21 +334,23 @@ do_recover_key(Mnemonic, State = #s{keys = Keys, accounts = Accounts}) -> State end. -do_recover_key2(Seed, State = #s{keys = Keys, accounts = Accounts}) -> +do_recover_key2(Seed, State = #s{wallet = W}) -> + #wallet{keys = Keys, poas = POAs} = W, Recovered = #key{id = ID, name = Name} = gmc_key_master:make_key("", Seed), case lists:keymember(ID, #key.id, Keys) of false -> NewKeys = [Recovered | Keys], - Account = #ak{name = Name, id = ID}, - NewAccounts = [Account | Accounts], - ok = gmc_gui:show(NewAccounts), - State#s{accounts = NewAccounts, keys = NewKeys}; + POA = #poa{name = Name, id = ID}, + NewPOAs = [POA | POAs], + ok = gmc_gui:show(NewPOAs), + NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys}, + State#s{wallet = NewWallet}; true -> State end. -do_mnemonic(ID, #s{keys = Keys}) -> +do_mnemonic(ID, #s{wallet = #wallet{keys = Keys}}) -> case lists:keyfind(ID, #key.id, Keys) of #key{pair = #{secret := <>}} -> Mnemonic = gmc_key_master:encode(K), @@ -357,20 +360,24 @@ do_mnemonic(ID, #s{keys = Keys}) -> end. -do_rename_key(ID, NewName, State = #s{accounts = Accounts, keys = Keys}) -> - A = lists:keyfind(ID, #ak.id, Accounts), +do_rename_key(ID, NewName, State = #s{wallet = W}) -> + #wallet{poas = POAs, keys = Keys} = W, + A = lists:keyfind(ID, #poa.id, POAs), K = lists:keyfind(ID, #key.id, Keys), - NewAccounts = lists:keystore(ID, #ak.id, Accounts, A#ak{name = NewName}), + NewPOAs = lists:keystore(ID, #poa.id, POAs, A#poa{name = NewName}), NewKeys = lists:keystore(ID, #key.id, Keys, K#key{name = NewName}), - ok = gmc_gui:show(NewAccounts), - State#s{accounts = NewAccounts, keys = NewKeys}. + NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys}, + ok = gmc_gui:show(NewPOAs), + State#s{wallet = NewWallet}. -do_drop_key(ID, State = #s{accounts = Accounts, keys = Keys}) -> - NewAccounts = lists:keydelete(ID, #ak.id, Accounts), +do_drop_key(ID, State = #s{wallet = W}) -> + #wallet{poas = POAs, keys = Keys} = W, + NewPOAs = lists:keydelete(ID, #poa.id, POAs), NewKeys = lists:keydelete(ID, #key.id, Keys), - ok = gmc_gui:show(NewAccounts), - State#s{accounts = NewAccounts, keys = NewKeys}. + NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys}, + ok = gmc_gui:show(NewPOAs), + State#s{wallet = NewWallet}. encrypt(Pass, Binary) -> @@ -387,74 +394,127 @@ pass(Phrase) -> crypto:hash(sha3_256, Phrase). -do_login(none, State) -> +do_open_wallet(Path, none, State) -> case read(none) of - {ok, Recovered = #s{accounts = Accounts}} -> - ok = gmc_gui:show(Accounts), - Recovered; - error -> - State + {ok, Recovered = #wallet{poas = POAs}} -> {POAs, State#s{wallet = Recovered}}; + Error -> {Error, State} end; -do_login(Phrase, State = #s{pass = none}) -> +do_open_wallet(Path, Phrase, State) -> Pass = pass(Phrase), - try - case read(Pass) of - {ok, Recovered = #s{accounts = Accounts}} -> - ok = gmc_gui:show(Accounts), - Recovered; - error -> - State#s{pass = Pass} - end - catch - E:R -> - ok = tell("Problem with loading state: {~p, ~p}", [E, R]), - ok = gmc_gui:ask_password(), - State + case read(Pass) of + {ok, Recovered = #wallet{poas = POAs}} -> {POAs, State#s{wallet = Recovered}}; + Error -> {Error, State} end. do_password(none, none, State) -> State; -do_password(none, New, State = #s{pass = none}) -> +do_password(none, New, State = #s{wallet = #wallet{pass = none}}) -> Pass = pass(New), - State#s{pass = Pass}; -do_password(Old, none, State = #s{pass = Pass}) -> + State#s{wallet = #wallet{pass = Pass}}; +do_password(Old, none, State = #s{wallet = #wallet{pass = Pass}}) -> case pass(Old) =:= Pass of - true -> State#s{pass = none}; + true -> State#s{wallet = #wallet{pass = none}}; false -> State end; -do_password(Old, New, State = #s{pass = Pass}) -> +do_password(Old, New, State = #s{wallet = #wallet{pass = Pass}}) -> case pass(Old) =:= Pass of - true -> State#s{pass = pass(New)}; + true -> State#s{wallet = #wallet{pass = pass(New)}}; false -> State 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); + false -> + log(warning, "Tried do_spend with a bad key: ~p", [KeyID]) + end. + +do_spend2(PrivKey, + #spend_tx{sender_id = SenderID, + recipient_id = RecipientId, + amount = Amount, + gas_price = GasPrice, + gas = Gas, + ttl = TTL, + nonce = Nonce, + payload = Payload}, + #s{wallet = #wallet{network_id = NetworkID}}) -> + Type = spend_tx, + Vsn = 1, + Fields = + [{sender_id, SenderID}, + {recipient_id, RecipientId}, + {amount, Amount}, + {gas_price, GasPrice}, + {gas, Gas}, + {ttl, TTL}, + {nonce, Nonce}, + {payload, Payload}], + Template = + [{sender_id, id}, + {recipient_id, id}, + {amount, int}, + {gas_price, int}, + {gas, int}, + {ttl, int}, + {nonce, int}, + {payload, binary}], + BinaryTX = aeser_chain_objects:serialize(Type, Vsn, Template, Fields), + NetworkTX = <>, + Signature = ecu_eddsa:sign_detached(NetworkTX, PrivKey), + SigTxType = signed_tx, + SigTxVsn = 1, + SigTemplate = + [{signatures, [binary]}, + {transaction, binary}], + TX_Data = + [{signatures, [Signature]}, + {transaction, NetworkTX}], + SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data), + tell("SpendTX: ~p", [SignedTX]). + + do_save(State = #s{prefs = Prefs}) -> ok = persist(Prefs), do_save2(State). -do_save2(State = #s{pass = none}) -> +do_save2(State = #s{wallet = W = #wallet{pass = none}}) -> Path = save_path(), ok = filelib:ensure_dir(Path), - file:write_file(Path, term_to_binary(State)); -do_save2(State = #s{pass = Pass}) -> + file:write_file(Path, term_to_binary(W)); +do_save2(State = #s{wallet = W = #wallet{pass = Pass}}) -> Path = save_path(), ok = filelib:ensure_dir(Path), - Cipher = encrypt(Pass, term_to_binary(State)), + Cipher = encrypt(Pass, term_to_binary(W)), file:write_file(Path, Cipher). read(none) -> case file:read_file(save_path()) of - {ok, Bin} -> {ok, binary_to_term(Bin)}; - {error, enoent} -> error + {ok, Bin} -> read2(Bin); + Error -> Error end; read(Pass) -> case file:read_file(save_path()) of - {ok, Cipher} -> {ok, binary_to_term(decrypt(Pass, Cipher))}; - {error, enoent} -> error + {ok, Cipher} -> + try + Bin = decrypt(Pass, Cipher), + read2(Bin) + catch + E:R -> {E, R} + end; + Error -> + Error + end. + +read2(Bin) -> + case zx_lib:b_to_t(Bin) of + {ok, T} -> {ok, T}; + error -> {error, badarg} end. diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 7aaad43..0860c2a 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -33,7 +33,7 @@ lang = en :: en | jp, j = none :: none | fun(), prefs = #{} :: #{atom() := term()}, - accounts = [] :: [clutch:ak()], + accounts = [] :: [clutch:poa()], picker = none :: none | wx:wx_object(), id = {#w{}, #w{}} :: labeled(), balance = {#w{}, #w{}} :: labeled(), @@ -280,6 +280,7 @@ handle_event(#wx{event = #wxCommand{type = command_button_clicked}, #w{name = drop_key} -> drop_key(State); #w{name = copy} -> copy(State); #w{name = www} -> www(State); + #w{name = send} -> spend(State); #w{name = grids} -> grids_dialogue(State); #w{name = Name} -> handle_button(Name, State); false -> State @@ -437,7 +438,7 @@ show_mnemonic(State = #s{picker = Picker}) -> end. show_mnemonic(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) -> - #ak{id = ID} = lists:nth(Selected, Accounts), + #poa{id = ID} = lists:nth(Selected, Accounts), {ok, Mnemonic} = gmc_con:mnemonic(ID), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Mnemonic")), Sizer = wxBoxSizer:new(?wxVERTICAL), @@ -468,7 +469,7 @@ rename_key(State = #s{picker = Picker}) -> end. rename_key(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) -> - #ak{id = ID, name = Name} = lists:nth(Selected, Accounts), + #poa{id = ID, name = Name} = lists:nth(Selected, Accounts), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key")), Sizer = wxBoxSizer:new(?wxVERTICAL), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name (Optional)")}]), @@ -507,7 +508,7 @@ drop_key(State = #s{picker = Picker}) -> end. drop_key(Selected, State = #s{frame = Frame, j = J, accounts = Accounts}) -> - #ak{id = ID, name = Name} = lists:nth(Selected, Accounts), + #poa{id = ID, name = Name} = lists:nth(Selected, Accounts), Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key")), Sizer = wxBoxSizer:new(?wxVERTICAL), Message = ["REALLY delete key: ", Name, " ?"], @@ -563,6 +564,43 @@ www(State = #s{id = {_, #w{wx = ID_T}}}) -> State. +spend(State) -> + tell("Would be doing a SpendTX"), + State. + +%spend(State = #s{accounts = []}) -> +% State; +%spend(State = #s{picker = Picker}) -> +% case wxListBox:getSelection(Picker) of +% -1 -> State; +% Selected -> spend(Selected + 1, State) +% end. + +%spend(Selected, State = #s{frame = Frame, j = J, }) -> +% #poa{id = ID, name = Name} = lists:nth(Selected, Accounts), +% Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Key")), +% Sizer = wxBoxSizer:new(?wxVERTICAL), +% Account = [J("From: "), Name, " (", ID, ")"], +% MessageT = wxStaticText:new(Dialog, ?wxID_ANY, Message), +% 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)), +% _ = wxBoxSizer:add(Sizer, MessageT, zxw:flags(wide)), +% _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), +% ok = wxDialog:setSizer(Dialog, Sizer), +% ok = wxBoxSizer:layout(Sizer), +% ok = wxFrame:center(Dialog), +% ok = +% case wxDialog:showModal(Dialog) of +% ?wxID_OK -> gmc_con:drop_key(ID); +% ?wxID_CANCEL -> ok +% end, +% ok = wxDialog:destroy(Dialog), +% State State. + + grids_dialogue(State = #s{frame = Frame, j = J}) -> tell("Handle GRIDS URL"), % ok = @@ -582,7 +620,8 @@ handle_button(Name, State) -> do_selection(Selected, State = #s{prefs = Prefs, accounts = Accounts, balance = {_, #w{wx = B}}, id = {_, #w{wx = I}}}) -> - #ak{id = ID, balance = Pucks} = lists:nth(Selected + 1, Accounts), + #poa{id = ID, balances = Balances} = lists:nth(Selected + 1, Accounts), + [#balance{total = Pucks}] = Balances, ok = wxStaticText:setLabel(I, ID), ok = wxStaticText:setLabel(B, price_to_string(Pucks)), NewPrefs = maps:put(selected, Selected, Prefs), @@ -590,7 +629,7 @@ do_selection(Selected, do_show(Accounts, State = #s{prefs = Prefs, picker = Picker}) -> - AKs = [Name || #ak{name = Name} <- Accounts], + AKs = [Name || #poa{name = Name} <- Accounts], ok = wxListBox:set(Picker, AKs), case Accounts of [] -> @@ -622,6 +661,7 @@ do_ask_password(#s{frame = Frame, j = J}) -> ok = wxBoxSizer:layout(Sizer), ok = wxFrame:center(Dialog), ok = wxStyledTextCtrl:setFocus(PassTx), + Path = filename:join(zx_lib:path(var, "otpr", "clutch"), "opaque.data"), ok = case wxDialog:showModal(Dialog) of ?wxID_OK -> @@ -630,9 +670,9 @@ do_ask_password(#s{frame = Frame, j = J}) -> "" -> none; P -> P end, - gmc_con:login(Phrase); + gmc_con:open_wallet(Path, Phrase); ?wxID_CANCEL -> - gmc_con:login(none) + gmc_con:open_wallet(Path, none) end, wxDialog:destroy(Dialog).