diff --git a/include/gmc.hrl b/include/gmc.hrl index a0d8e15..c2cab3f 100644 --- a/include/gmc.hrl +++ b/include/gmc.hrl @@ -1,4 +1,4 @@ -% Node, Chain and Net represent the physical network. +%% Node, Chain and Net represent the physical network. -record(node, {ip = {161,97,102,143} :: string() | inet:ip_address(), @@ -21,7 +21,7 @@ -% AC and Coin represent the financial authority graph for a given coin. +%% AC and Coin represent the financial authority graph for a given coin. -record(ac, {id = none :: string(), @@ -35,7 +35,7 @@ -% Balance, POA, Key, TXs, all culminate in capturing a complete Wallet view. +%% Balance, POA, Key, TXs, all culminate in capturing a complete Wallet view. -record(balance, {coin = "gaju" :: string(), total = 0 :: non_neg_integer(), @@ -76,9 +76,19 @@ -record(wallet, {version = 1 :: integer(), + name = "" :: string(), poas = [] :: [#poa{}], keys = [] :: [#key{}], - pass = none :: none | binary(), chain_id = <<"groot.devnet">> :: binary(), endpoint = #node{} :: #node{}, nets = [#net{}] :: [#net{}]}). + + + +%% Niche registered elements + +% WR: Wallet Registration +-record(wr, + {name = "" :: string(), + path = "" :: file:filename(), + pass = false :: boolean()}). diff --git a/src/gmc_con.erl b/src/gmc_con.erl index e457d87..39b1cd3 100644 --- a/src/gmc_con.erl +++ b/src/gmc_con.erl @@ -12,12 +12,14 @@ -behavior(gen_server). -export([show_ui/1, - open_wallet/2, close_wallet/0, password/2, refresh/0, + open_wallet/2, close_wallet/0, new_wallet/3, import_wallet/3, password/2, + refresh/0, nonce/1, spend/2, chain/1, grids/1, make_key/6, recover_key/1, mnemonic/1, rename_key/2, drop_key/1, add_node/1, set_sole_node/1]). -export([encrypt/2, decrypt/2]). --export([start_link/0, stop/0, save/1, save/2]). +-export([save/2]). +-export([start_link/0, stop/0]). -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2]). -include("$zx_include/zx_logger.hrl"). @@ -35,16 +37,18 @@ mon = none :: none | reference()}). -record(s, - {version = 1 :: integer(), - window = none :: none | wx:wx_object(), - tasks = [] :: [#ui{}], - wallet = #wallet{} :: #wallet{}, - prefs = #{} :: #{atom() := term()}}). + {version = 1 :: integer(), + window = none :: none | wx:wx_object(), + tasks = [] :: [#ui{}], + wallet = none :: none | #wallet{}, + pass = none :: none | binary(), + prefs = #{} :: #{module() := term()}, + wallets = [] :: [#wr{}]}). -type state() :: #s{}. -type ui_name() :: gmc_v_netman - | gmc_v_conman. + | gmc_v_wallman. @@ -72,6 +76,24 @@ close_wallet() -> gen_server:cast(?MODULE, close_wallet). +-spec new_wallet(Name, Path, Password) -> ok + when Name :: string(), + Path :: string(), + Password :: string(). + +new_wallet(Name, Path, Password) -> + gen_server:cast(?MODULE, {new_wallet, Name, Path, Password}). + + +-spec import_wallet(Name, Path, Password) -> ok + when Name :: string(), + Path :: string(), + Password :: string(). + +import_wallet(Name, Path, Password) -> + gen_server:cast(?MODULE, {import_wallet, Name, Path, Password}). + + -spec password(Old, New) -> ok when Old :: none | string(), New :: none | string(). @@ -186,20 +208,13 @@ stop() -> gen_server:cast(?MODULE, stop). --spec save(Prefs) -> ok | {error, Reason} - when Prefs :: #{atom() := term()}, +-spec save(Module, Prefs) -> ok | {error, Reason} + when Module :: module(), + Prefs :: #{atom() := term()}, Reason :: file:posix(). -save(Prefs) -> - gen_server:call(?MODULE, {save, Prefs}). - - --spec save(Label, Pref) -> ok - when Label :: term(), - Pref :: term(). - -save(Label, Pref) -> - gen_server:call(?MODULE, {save, Label, Pref}). +save(Module, Prefs) -> + gen_server:call(?MODULE, {save, Module, Prefs}). @@ -224,10 +239,11 @@ start_link() -> init(none) -> ok = log(info, "Starting"), Prefs = read_prefs(), - Window = gmc_gui:start_link(Prefs), - ok = gmc_gui:ask_password(), + GUI_Prefs = maps:get(gmc_gui, Prefs, #{}), + Window = gmc_gui:start_link(GUI_Prefs), State = #s{window = Window, prefs = Prefs}, - {ok, State}. + NewState = do_show_ui(gmc_v_wallman, State), + {ok, NewState}. read_prefs() -> @@ -262,11 +278,8 @@ read_prefs() -> handle_call({nonce, ID}, _, State) -> Response = do_nonce(ID), {reply, Response, State}; -handle_call({save, Prefs}, _, State) -> - Response = do_save(State#s{prefs = Prefs}), - {reply, Response, State}; -handle_call({save, Label, Pref}, _, State) -> - NewState = do_save(Label, Pref, State), +handle_call({save, Module, Prefs}, _, State) -> + NewState = do_save(Module, Prefs, State), {reply, ok, NewState}; handle_call({mnemonic, ID}, _, State) -> Response = do_mnemonic(ID, State), @@ -290,6 +303,12 @@ handle_cast({show_ui, Name}, State) -> handle_cast({open_wallet, Path, Phrase}, State) -> NewState = do_open_wallet(Path, Phrase, State), {noreply, NewState}; +handle_cast(close_wallet, State) -> + NewState = do_close_wallet(State), + {noreply, NewState}; +handle_cast({new_wallet, Name, Path, Password}, State) -> + NewState = do_new_wallet(Name, Path, Password, State), + {noreply, NewState}; handle_cast({password, Old, New}, State) -> NewState = do_password(Old, New, State), {noreply, NewState}; @@ -324,8 +343,9 @@ handle_cast({set_sole_node, TheOneTrueNode}, State) -> NewState = do_set_sole_node(TheOneTrueNode, State), {noreply, NewState}; handle_cast(stop, State) -> + NewState = do_stop(State), ok = zx:stop(), - {noreply, State}; + {noreply, NewState}; handle_cast(Unexpected, State) -> ok = tell(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. @@ -375,29 +395,97 @@ terminate(Reason, State) -> -%%% +%%% GUI doers -do_show_ui(Name, State = #s{tasks = Tasks, prefs = Prefs, wallet = Wallet}) -> - #wallet{nets = Nets} = Wallet, +do_show_ui(Name, State = #s{tasks = Tasks, prefs = Prefs}) -> case lists:keyfind(Name, #ui.name, Tasks) of #ui{wx = Win} -> ok = Name:to_front(Win), State; false -> - Win = Name:start_link({Prefs, Nets}), + TaskPrefs = maps:get(Name, Prefs, #{}), + TaskData = task_data(Name, State), + Win = Name:start_link({TaskPrefs, TaskData}), PID = wx_object:get_pid(Win), Mon = monitor(process, PID), UI = #ui{name = Name, pid = PID, wx = Win, mon = Mon}, State#s{tasks = [UI | Tasks]} end. +task_data(gmc_v_netman, #s{wallet = #wallet{nets = Nets}}) -> + Nets; +task_data(gmc_v_netman, #s{wallet = none}) -> + []; +task_data(gmc_v_wallman, #s{wallets = Wallets}) -> + Wallets. + + + +%%% Network operations do_chain(_, State) -> tell("Would be doing chain in do_chain/2 here"), State. +do_add_node(New, State) -> + tell("New node: ~p", [New]), + State. + + +% FIXME: This will, of course, totally explode if anything goes wrong +do_set_sole_node(TOTN = #node{external = none}, State) -> + do_set_sole_node(TOTN#node{external = 3013}, State); +do_set_sole_node(New, State = #s{wallet = W}) -> + ChainID = ensure_hz_set(New), + Net = #net{id = ChainID, chains = [#chain{id = ChainID, nodes = [New]}]}, + NewWallet = W#wallet{chain_id = ChainID, endpoint = New, nets = [Net]}, + NewState = State#s{wallet = NewWallet}, + do_refresh(NewState). + + +do_refresh(State = #s{wallet = W = #wallet{poas = POAs, endpoint = Node}}) -> + ChainID = ensure_hz_set(Node), + CheckBalance = + fun(This = #poa{id = ID}) -> + Pucks = + case hz:acc(ID) of + {ok, #{"balance" := P}} -> P; + {error, "Account not found"} -> 0 + end, + Dist = [{ChainID, Pucks}], + Gaju = #balance{coin = "gaju", total = Pucks, dist = Dist}, + This#poa{balances = [Gaju]} + end, + NewPOAs = lists:map(CheckBalance, POAs), + ok = gmc_gui:show(NewPOAs), + NewW = W#wallet{chain_id = ChainID, poas = NewPOAs}, + State#s{wallet = NewW}. + + +ensure_hz_set(Node = #node{ip = IP, external = Port}) -> + case hz:status() of + {ok, #{"network_id" := ChainID}} -> + list_to_binary(ChainID); + {error, no_nodes} -> + ok = hz:chain_nodes([{IP, Port}]), + {ok, #{"network_id" := C}} = hz:status(), + ChainID = list_to_binary(C), + ok = hz:network_id(ChainID), + ok = gmc_gui:chain(ChainID, Node), + ChainID + end. + + +%ensure_connected(ChainID, IP, Port) -> +% ok = hz:chain_nodes([{IP, Port}]), +% ok = hz:network_id(ChainID). + + + +%%% Chain operations + do_grids(String) -> case gmc_grids:parse(String) of {ok, Instruction} -> do_grids2(Instruction); @@ -408,6 +496,84 @@ do_grids2(Instruction) -> tell("GRIDS: ~tp", [Instruction]). +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{chain_id = ChainID}}) -> + 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, BinaryTX}], + SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data), + Encoded = aeser_api_encoder:encode(transaction, SignedTX), + Outcome = hz:post_tx(Encoded), + tell("Outcome: ~p", [Outcome]). + + +do_nonce(ID) -> + hz:next_nonce(ID). + + + +%%% State Operations + +encrypt(Pass, Binary) -> + Flags = [{encrypt, true}, {padding, pkcs_padding}], + crypto:crypto_one_time(aes_256_ecb, Pass, Binary, Flags). + + +decrypt(Pass, Binary) -> + Flags = [{encrypt, false}, {padding, pkcs_padding}], + crypto:crypto_one_time(aes_256_ecb, Pass, Binary, Flags). + + +pass(none) -> + none; +pass(Phrase) -> + crypto:hash(sha3_256, Phrase). + + do_make_key(Name, <<>>, _, Transform, State) -> Bin = crypto:strong_rand_bytes(32), do_make_key2(Name, Bin, Transform, State); @@ -432,6 +598,7 @@ do_make_key(Name, Seed, base58, Transform, State) -> State end. + do_make_key2(Name, Bin, Transform, State = #s{wallet = W}) -> #wallet{poas = POAs, keys = Keys} = W, T = transform(Transform), @@ -529,74 +696,6 @@ do_drop_key(ID, State = #s{wallet = W}) -> State#s{wallet = NewWallet}. -do_add_node(New, State) -> - tell("New node: ~p", [New]), - State. - - -% FIXME: This will, of course, totally explode if anything goes wrong -do_set_sole_node(TOTN = #node{external = none}, State) -> - do_set_sole_node(TOTN#node{external = 3013}, State); -do_set_sole_node(New, State = #s{wallet = W}) -> - ChainID = ensure_hz_set(New), - Net = #net{id = ChainID, chains = [#chain{id = ChainID, nodes = [New]}]}, - NewWallet = W#wallet{chain_id = ChainID, endpoint = New, nets = [Net]}, - NewState = State#s{wallet = NewWallet}, - do_refresh(NewState). - - -do_refresh(State = #s{wallet = W = #wallet{poas = POAs, endpoint = Node}}) -> - ChainID = ensure_hz_set(Node), - CheckBalance = - fun(This = #poa{id = ID}) -> - Pucks = - case hz:acc(ID) of - {ok, #{"balance" := P}} -> P; - {error, "Account not found"} -> 0 - end, - Dist = [{ChainID, Pucks}], - Gaju = #balance{coin = "gaju", total = Pucks, dist = Dist}, - This#poa{balances = [Gaju]} - end, - NewPOAs = lists:map(CheckBalance, POAs), - ok = gmc_gui:show(NewPOAs), - NewW = W#wallet{chain_id = ChainID, poas = NewPOAs}, - State#s{wallet = NewW}. - - -ensure_hz_set(Node = #node{ip = IP, external = Port}) -> - case hz:status() of - {ok, #{"network_id" := ChainID}} -> - list_to_binary(ChainID); - {error, no_nodes} -> - ok = hz:chain_nodes([{IP, Port}]), - {ok, #{"network_id" := C}} = hz:status(), - ChainID = list_to_binary(C), - ok = hz:network_id(ChainID), - ok = gmc_gui:chain(ChainID, Node), - ChainID - end. - - -%ensure_connected(ChainID, IP, Port) -> -% ok = hz:chain_nodes([{IP, Port}]), -% ok = hz:network_id(ChainID). - - -encrypt(Pass, Binary) -> - Flags = [{encrypt, true}, {padding, pkcs_padding}], - crypto:crypto_one_time(aes_256_ecb, Pass, Binary, Flags). - - -decrypt(Pass, Binary) -> - Flags = [{encrypt, false}, {padding, pkcs_padding}], - crypto:crypto_one_time(aes_256_ecb, Pass, Binary, Flags). - - -pass(Phrase) -> - crypto:hash(sha3_256, Phrase). - - do_open_wallet(Path, none, State) -> case read(Path, none) of {ok, Recovered = #wallet{poas = POAs, chain_id = ChainID, endpoint = Node}} -> @@ -621,6 +720,7 @@ do_open_wallet(Path, Phrase, State) -> State#s{wallet = New} end. + default_wallet() -> DevNet = #net{id = <<"devnet">>, chains = [#chain{}]}, TestChain1 = #chain{id = <<"groot.testnet">>, @@ -633,104 +733,79 @@ default_wallet() -> do_password(none, none, State) -> State; -do_password(none, New, State = #s{wallet = #wallet{pass = none}}) -> +do_password(none, New, State = #s{pass = none, + wallet = #wallet{name = Name}, + wallets = Wallets}) -> Pass = pass(New), - State#s{wallet = #wallet{pass = Pass}}; -do_password(Old, none, State = #s{wallet = #wallet{pass = Pass}}) -> + Selected = lists:keyfind(Name, #wr.name, Wallets), + Updated = Selected#wr{pass = true}, + NewWallets = lists:keystore(Name, #wr.name, Wallets, Updated), + State#s{wallet = pass = Pass, wallets = NewWallets}; +do_password(Old, none, State = #s{pass = Pass, + wallet = #wallet{name = Name}, + wallets = Wallets}) -> case pass(Old) =:= Pass of - true -> State#s{wallet = #wallet{pass = none}}; - false -> State - end; -do_password(Old, New, State = #s{wallet = #wallet{pass = Pass}}) -> - case pass(Old) =:= Pass of - true -> State#s{wallet = #wallet{pass = pass(New)}}; - false -> State - end. - - -do_spend(KeyID, TX, State = #s{wallet = #wallet{keys = Keys}}) -> - tell("SpendTX: ~p", [TX]), - case lists:keyfind(KeyID, #key.id, Keys) of - #key{pair = #{secret := PrivKey}} -> - do_spend2(PrivKey, TX, State); + true -> + Selected = lists:keyfind(Name, #wr.name, Wallets), + Updated = Selected#wr{pass = false}, + NewWallets = lists:keystore(Name, #wr.name, Wallets, Updated), + State#s{pass = none, wallets = NewWallets}; false -> - log(warning, "Tried do_spend with a bad key: ~p", [KeyID]) + State + end; +do_password(Old, New, State = #s{pass = Pass}) -> + case pass(Old) =:= Pass of + true -> State#s{pass = pass(New)}; + false -> State 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{chain_id = ChainID}}) -> - 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), - tell("BinaryTX: ~p", [BinaryTX]), - NetworkTX = <>, - tell("NetworkTX: ~p", [NetworkTX]), - Signature = ecu_eddsa:sign_detached(NetworkTX, PrivKey), - SigTxType = signed_tx, - SigTxVsn = 1, - SigTemplate = - [{signatures, [binary]}, - {transaction, binary}], - TX_Data = - [{signatures, [Signature]}, - {transaction, BinaryTX}], - SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX_Data), - tell("SignedTX: ~p", [SignedTX]), - Encoded = aeser_api_encoder:encode(transaction, SignedTX), - tell("Encoded: ~p", [Encoded]), - Outcome = hz:post_tx(Encoded), - tell("Outcome: ~p", [Outcome]). - -do_nonce(ID) -> - hz:next_nonce(ID). - - -do_save(Label, Pref, State = #s{prefs = Prefs}) -> - NewPrefs = maps:put(Label, Pref, Prefs), +do_stop(State = #s{prefs = Prefs}) -> ok = persist(Prefs), - State#s{prefs = NewPrefs}. + do_close_wallet(State). -do_save(State = #s{prefs = Prefs}) -> - ok = persist(Prefs), - do_save2(State). +do_new_wallet(Name, Path, Password, State = #s{wallets = Wallets}) -> + case lists:keyfind(Name, #wr.name, Wallets) of + false -> + NextState = do_close_wallet(State), + Pass = pass(Password), + HasPass = Pass =/= none, + Entry = #wr{name = Name, path = Path, pass = HasPass}, + New = #wallet{}, + ok = save_wallet(Entry, Pass, New), + ok = gmc_gui:show([]), + NextState#s{wallet = New, pass = Pass, wallets = [Entry | Wallets]}; + #wr{} -> + % FIXME + % Need to provide feedback based on where this came from + State + end. -do_save2(#s{wallet = W = #wallet{pass = none}}) -> - Path = save_path(), +do_save(Module, Prefs, State = #s{prefs = Cached}) -> + Updated = maps:put(Module, Prefs, Cached), + ok = persist(Updated), + State#s{prefs = Updated}. + + +do_close_wallet(State = #s{wallet = none}) -> + State; +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), + ok = gmc_gui:show([]), + ok = do_show_ui(gmc_v_wallman, State), + State#s{pass = false, wallet = none}. + + +save_wallet(#wr{path = Path, pass = false}, none, Wallet) -> ok = filelib:ensure_dir(Path), - file:write_file(Path, term_to_binary(W)); -do_save2(#s{wallet = W = #wallet{pass = Pass}}) -> - Path = save_path(), + file:write_file(Path, term_to_binary(Wallet)); +save_wallet(#wr{path = Path, pass = true}, Pass, Wallet) -> ok = filelib:ensure_dir(Path), - Cipher = encrypt(Pass, term_to_binary(W)), + Cipher = encrypt(Pass, term_to_binary(Wallet)), file:write_file(Path, Cipher). diff --git a/src/gmc_gui.erl b/src/gmc_gui.erl index 50bbf3e..11ba598 100644 --- a/src/gmc_gui.erl +++ b/src/gmc_gui.erl @@ -82,6 +82,8 @@ init(Prefs) -> MainSz = wxBoxSizer:new(?wxVERTICAL), Picker = wxListBox:new(Frame, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]), + WallB = wxButton:new(Frame, ?wxID_ANY, [{label, "[none]"}, {style, ?wxBORDER_NONE}]), + WallW = #w{name = wallet, id = wxButton:getId(WallB), wx = WallB}, ChainB = wxButton:new(Frame, ?wxID_ANY, [{label, "[ChainID]"}, {style, ?wxBORDER_NONE}]), ChainW = #w{name = chain, id = wxButton:getId(ChainB), wx = ChainB}, NodeB = wxButton:new(Frame, ?wxID_ANY, [{label, "[Node]"}, {style, ?wxBORDER_NONE}]), @@ -128,7 +130,7 @@ init(Prefs) -> #w{name = Name, id = wxButton:getId(B), wx = B} end, - Buttons = [ChainW, NodeW | lists:map(MakeButton, ButtonTemplates)], + Buttons = [WallW, ChainW, NodeW | lists:map(MakeButton, ButtonTemplates)], ChainSz = wxBoxSizer:new(?wxHORIZONTAL), AccountSz = wxBoxSizer:new(?wxHORIZONTAL), @@ -136,6 +138,7 @@ init(Prefs) -> ActionsSz = wxBoxSizer:new(?wxHORIZONTAL), HistorySz = wxBoxSizer:new(?wxVERTICAL), + _ = wxSizer:add(ChainSz, WallB, zxw:flags(wide)), _ = wxSizer:add(ChainSz, ChainB, zxw:flags(wide)), _ = wxSizer:add(ChainSz, NodeB, zxw:flags(wide)), @@ -199,6 +202,7 @@ init(Prefs) -> {Frame, State}. +% Put this in a gmc bevhavior safe_size(Prefs) -> Display = wxDisplay:new(), GSize = wxDisplay:getGeometry(Display), @@ -290,6 +294,7 @@ handle_event(#wx{event = #wxCommand{type = command_button_clicked}, State = #s{buttons = Buttons}) -> NewState = case lists:keyfind(ID, #w.id, Buttons) of + #w{name = wallet} -> wallman(State); #w{name = chain} -> netman(State); #w{name = node} -> set_node(State); #w{name = make_key} -> make_key(State); @@ -322,7 +327,7 @@ handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) {X, Y, W, H} end, NewPrefs = maps:put(geometry, Geometry, Prefs), - ok = gmc_con:save(NewPrefs), + ok = gmc_con:save(?MODULE, NewPrefs), ok = gmc_con:stop(), ok = wxWindow:destroy(Frame), {noreply, State}; @@ -350,6 +355,11 @@ refresh(State) -> State. +wallman(State) -> + ok = gmc_con:show_ui(gmc_v_wallman), + State. + + netman(State) -> ok = gmc_con:show_ui(gmc_v_netman), State. @@ -688,13 +698,14 @@ spend(Selected, State = #s{accounts = Accounts}) -> POA = #poa{id = ID} = lists:nth(Selected, Accounts), case gmc_con:nonce(ID) of {ok, Nonce} -> - spend2(POA, Nonce, State); + {ok, #{"top_block_height" := Height}} = hz:status(), + spend2(POA, Nonce, Height, State); {error, Reason} -> tell("spend/2 failed to get nonce with ~tp", [Reason]), State end. -spend2(#poa{id = ID, name = Name}, Nonce, State = #s{frame = Frame, j = J}) -> +spend2(#poa{id = ID, name = Name}, Nonce, Height, State = #s{frame = Frame, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Transfer"), [{size, {500, 400}}]), Sizer = wxBoxSizer:new(?wxVERTICAL), @@ -716,10 +727,14 @@ spend2(#poa{id = ID, name = Name}, Nonce, State = #s{frame = Frame, j = J}) -> _ = wxStaticBoxSizer:add(DataSz, DataTx, zxw:flags(wide)), Style = [{style, ?wxSL_HORIZONTAL bor ?wxSL_LABELS}], - Preset = 1_000, + + TTL_Sl = wxSlider:new(Dialog, ?wxID_ANY, 100, 10, 1000, Style), + TTL_Sz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("TTL")}]), + _ = wxStaticBoxSizer:add(TTL_Sz, TTL_Sl, zxw:flags(wide)), + Min = hz:min_gas_price(), Max = Min * 2, - GasSl = wxSlider:new(Dialog, ?wxID_ANY, Preset, Min, Max, Style), + GasSl = wxSlider:new(Dialog, ?wxID_ANY, Min, Min, Max, Style), GasSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Gas Price")}]), _ = wxStaticBoxSizer:add(GasSz, GasSl, zxw:flags(wide)), @@ -733,10 +748,12 @@ spend2(#poa{id = ID, name = Name}, Nonce, State = #s{frame = Frame, j = J}) -> _ = wxBoxSizer:add(Sizer, ToSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, AmtSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, DataSz, zxw:flags(wide)), + _ = wxBoxSizer:add(Sizer, TTL_Sz, zxw:flags(wide)), _ = wxBoxSizer:add(Sizer, GasSz, zxw:flags(base)), _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), + ok = wxFrame:setSize(Dialog, {500, 450}), ok = wxFrame:center(Dialog), ok = case wxDialog:showModal(Dialog) of @@ -748,7 +765,7 @@ spend2(#poa{id = ID, name = Name}, Nonce, State = #s{frame = Frame, j = J}) -> amount = wxTextCtrl:getValue(AmtTx), gas_price = wxSlider:getValue(GasSl), gas = 20000, - ttl = 1, + ttl = Height + wxSlider:getValue(TTL_Sl), nonce = Nonce, payload = wxTextCtrl:getValue(DataTx)}, clean_spend(ID, TX); @@ -879,7 +896,7 @@ do_ask_password(#s{frame = Frame, prefs = Prefs, j = J}) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Password")), Sizer = wxBoxSizer:new(?wxVERTICAL), Label = J("Password (leave blank for no password)"), - PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J(Label)}]), + PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label}]), PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxStaticBoxSizer:add(PassSz, PassTx, zxw:flags(wide)), ButtSz = wxBoxSizer:new(?wxHORIZONTAL), @@ -915,7 +932,7 @@ do_ask_password(#s{frame = Frame, prefs = Prefs, j = J}) -> price_to_string(Pucks) -> - Gaju = 1000000000000000000, + Gaju = 1_000_000_000_000_000_000, H = integer_to_list(Pucks div Gaju), R = Pucks rem Gaju, case string:strip(lists:flatten(io_lib:format("~18..0w", [R])), right, $0) of diff --git a/src/gmc_v_netman.erl b/src/gmc_v_netman.erl index dab337d..8d43769 100644 --- a/src/gmc_v_netman.erl +++ b/src/gmc_v_netman.erl @@ -6,7 +6,8 @@ -behavior(wx_object). -include_lib("wx/include/wx.hrl"). --export([to_front/1, set_manifest/1]). +-export([to_front/1]). +-export([set_manifest/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]). @@ -209,6 +210,9 @@ handle_call(Unexpected, From, State) -> {noreply, State}. +handle_cast(to_front, State = #s{frame = Frame}) -> + ok = wxFrame:raise(Frame), + {noreply, State}; handle_cast(Unexpected, State) -> ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. @@ -231,7 +235,7 @@ handle_event(E = #wx{event = #wxCommand{type = command_button_clicked}, State end, {noreply, NewState}; -handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame}) -> +handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame, prefs = Prefs}) -> Geometry = case wxTopLevelWindow:isMaximized(Frame) of true -> @@ -241,7 +245,8 @@ handle_event(#wx{event = #wxClose{}}, State = #s{frame = Frame}) -> {W, H} = wxWindow:getSize(Frame), {X, Y, W, H} end, - ok = gmc_con:save({?MODULE, geometry}, Geometry), + NewPrefs = maps:put(geometry, Geometry, Prefs), + ok = gmc_con:save(?MODULE, NewPrefs), ok = wxWindow:destroy(Frame), {noreply, State}; handle_event(Event, State) -> diff --git a/src/gmc_v_wallman.erl b/src/gmc_v_wallman.erl new file mode 100644 index 0000000..1a20d63 --- /dev/null +++ b/src/gmc_v_wallman.erl @@ -0,0 +1,424 @@ +-module(gmc_v_wallman). +-vsn("0.1.0"). +-author("Craig Everett "). +-copyright("QPQ AG "). +-license("GPL-3.0-or-later"). + +-behavior(wx_object). +-include_lib("wx/include/wx.hrl"). +-export([to_front/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"). + + +-record(w, + {name = none :: atom(), + id = 0 :: integer(), + wx = none :: none | wx:wx_object()}). + +-record(s, + {wx = none :: none | wx:wx_object(), + frame = none :: none | wx:wx_object(), + lang = en :: en | jp, + j = none :: none | fun(), + prefs = #{} :: map(), + wallets = [] :: [#wr{}], + picker = none :: none | wx:wx_object(), + buttons = [] :: [#w{}]}). + + +%%% Interface + +-spec to_front(Win) -> ok + when Win :: wx:wx_object(). + +to_front(Win) -> + wx_object:cast(Win, to_front). + + + +%%% Startup + +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("Wallets")), + + MainSz = wxBoxSizer:new(?wxVERTICAL), + + Picker = wxListBox:new(Frame, ?wxID_ANY, [{style, ?wxLC_SINGLE_SEL}]), + Names = [Name || #wr{name = Name} <- Manifest], + ok = wxListBox:set(Picker, Names), + + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + ButtonTemplates = + [{open, J("Open")}, + {new, J("New")}, + {move, J("Move")}, + {import, J("Import")}, + {drop, J("Drop")}], + MakeButton = + fun({Name, Label}) -> + B = wxButton:new(Frame, ?wxID_ANY, [{label, Label}]), + _ = wxSizer:add(ButtSz, B, zxw:flags(wide)), + #w{name = Name, id = wxButton:getId(B), wx = B} + end, + Buttons = lists:map(MakeButton, ButtonTemplates), + + _ = wxSizer:add(MainSz, Picker, zxw:flags(wide)), + _ = wxSizer:add(MainSz, ButtSz, zxw:flags(base)), + + ok = wxFrame:setSizer(Frame, MainSz), + ok = wxSizer:layout(MainSz), + + ok = + case safe_size(Prefs) of + {pref, max} -> + wxTopLevelWindow:maximize(Frame); + {pref, WSize} -> + wxFrame:setSize(Frame, WSize); + {center, WSize} -> + ok = wxFrame:setSize(Frame, WSize), + wxFrame:center(Frame) + end, + + ok = wxFrame:connect(Frame, command_button_clicked), + ok = wxFrame:connect(Frame, close_window), + true = wxFrame:show(Frame), + ok = wxListBox:connect(Picker, command_listbox_selected), + ok = wxListBox:connect(Picker, command_listbox_doubleclicked), + State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs, + wallets = Manifest, + picker = Picker, + buttons = Buttons}, + {Frame, State}. + + +safe_size(Prefs) -> + Display = wxDisplay:new(), + GSize = wxDisplay:getGeometry(Display), + CSize = wxDisplay:getClientArea(Display), + PPI = + try + wxDisplay:getPPI(Display) + catch + Class:Exception -> {Class, Exception} + end, + ok = log(info, "Geometry: ~p", [GSize]), + ok = log(info, "ClientArea: ~p", [CSize]), + ok = log(info, "PPI: ~p", [PPI]), + Geometry = + case maps:find(geometry, Prefs) of + {ok, none} -> + {X, Y, _, _} = GSize, + {center, {X, Y, 420, 520}}; + {ok, G} -> + {pref, G}; + error -> + {X, Y, _, _} = GSize, + {center, {X, Y, 420, 520}} + end, + ok = wxDisplay:destroy(Display), + Geometry. + + + +%%% wx_object + +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(Unexpected, State) -> + ok = log(warning, "Unexpected cast: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_info(Unexpected, State) -> + ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), + {noreply, State}. + + +handle_event(#wx{event = #wxCommand{type = command_button_clicked}, + id = ID}, + State = #s{buttons = Buttons}) -> + NewState = + case lists:keyfind(ID, #w.id, Buttons) of + #w{name = open} -> do_open(State); + #w{name = new} -> do_new(State); + #w{name = import} -> do_import(State); + #w{name = Name} -> handle_button(Name, State); + false -> State + end, + {noreply, NewState}; +handle_event(#wx{event = #wxClose{}}, State) -> + ok = do_close(State), + {noreply, State}; +handle_event(Event, State) -> + ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), + {noreply, State}. + + +code_change(_, State, _) -> + {ok, State}. + + +terminate(wx_deleted, _) -> + wx:destroy(); +terminate(Reason, State) -> + ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), + wx:destroy(). + + + +%%% doers + +do_close(#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). + + +handle_button(Name, State) -> + ok = tell("Button Click: ~p", [Name]), + State. + + +do_open(State = #s{wallets = []}) -> + State; +do_open(State = #s{picker = Picker}) -> + case wxListBox:getSelection(Picker) of + -1 -> State; + Selected -> do_open2(Selected + 1, State) + end. + +do_open2(Selected, State = #s{wallets = Wallets}) -> + case lists:nth(Selected, Wallets) of + #wr{pass = true, path = Path} -> + do_open3(Path, State); + #wr{pass = false, path = Path} -> + ok = gmc_con:open_wallet(Path, none), + ok = do_close(State), + State + end. + +do_open3(Path, State = #s{frame = Frame, j = J}) -> + Label = J("Password"), + Dialog = wxDialog:new(Frame, ?wxID_ANY, Label), + Sizer = wxBoxSizer:new(?wxVERTICAL), + PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, Label}]), + PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxStaticBoxSizer:add(PassSz, PassTx, zxw:flags(wide)), + ButtSz = wxBoxSizer:new(?wxHORIZONTAL), + Affirm = wxButton:new(Dialog, ?wxID_OK), + _ = wxBoxSizer:add(ButtSz, Affirm, zxw:flags(wide)), + _ = wxBoxSizer:add(Sizer, PassSz, zxw:flags(base)), + _ = wxBoxSizer:add(Sizer, ButtSz, zxw:flags(base)), + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxFrame:center(Dialog), + ok = wxStyledTextCtrl:setFocus(PassTx), + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + ok = wxFileDialog:destroy(Dialog), + case wxTextCtrl:getValue(PassTx) of + "" -> + State; + Password -> + ok = gmc_con:open_wallet(Path, Password), + ok = do_close(State), + State + end; + ?wxID_CANCEL -> + ok = wxFileDialog:destroy(Dialog), + State + end. + + +do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) -> + DefaultDir = maps:get(dir, Prefs, zx_lib:path(var, "otpr", "clutch")), + Options = + [{message, J("Save Location")}, + {defaultDir, DefaultDir}, + {defaultFile, "default.gaju"}, + {wildCard, "*.gaju"}, + {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], + Dialog = wxFileDialog:new(Frame, Options), + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + File = wxFileDialog:getFilename(Dialog), + Path = filename:join(Dir, File), + ok = wxFileDialog:destroy(Dialog), + case do_new2(Path, J, Frame) of + ok -> + NewPrefs = maps:put(dir, Dir, Prefs), + do_close(State#s{prefs = NewPrefs}); + abort -> + State + end; + ?wxID_CANCEL -> + ok = wxFileDialog:destroy(Dialog), + State + end. + +do_new2(Path, J, Frame) -> + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Set Node")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + + NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), + NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)), + PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Passphrase")}]), + PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(PassSz, PassTx, zxw:flags(wide)), + Label = "Passphrase Confirmation", + PassConSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J(Label)}]), + PassConTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(PassConSz, PassConTx, 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, NameSz, zxw:flags(base)), + _ = wxSizer:add(Sizer, PassSz, zxw:flags(base)), + _ = wxSizer:add(Sizer, PassConSz, zxw:flags(base)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxFrame:setSize(Dialog, {500, 200}), + ok = wxFrame:center(Dialog), + ok = wxStyledTextCtrl:setFocus(NameTx), + + Result = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + Name = + case wxTextCtrl:getValue(NameTx) of + "" -> Path; + N -> N + end, + Pass = + case {wxTextCtrl:getValue(PassTx), wxTextCtrl:getValue(PassConTx)} of + {"", ""} -> none; + {P, P} -> P; + {_, _} -> bad + end, + do_new3(Name, Path, Pass); + ?wxID_CANCEL -> + abort + end, + ok = wxDialog:destroy(Dialog), + Result. + +do_new3(_, _, bad) -> + abort; +do_new3(Name, Path, Pass) -> + gmc_con:new_wallet(Name, Path, Pass). + + +do_import(State = #s{frame = Frame, j = J, prefs = Prefs}) -> + DefaultDir = maps:get(dir, Prefs, zx_lib:path(var, "otpr", "clutch")), + Options = + [{message, J("Select Wallet File")}, + {defaultDir, DefaultDir}, + {wildCard, "*.gaju"}, + {style, ?wxFD_OPEN}], + Dialog = wxFileDialog:new(Frame, Options), + case wxFileDialog:showModal(Dialog) of + ?wxID_OK -> + Dir = wxFileDialog:getDirectory(Dialog), + File = wxFileDialog:getFilename(Dialog), + ok = wxFileDialog:destroy(Dialog), + case do_import2(Dir, File, J, Frame) of + ok -> + NewPrefs = maps:put(dir, Dir, Prefs), + do_close(State#s{prefs = NewPrefs}); + abort -> + State + end; + ?wxID_CANCEL -> + ok = wxFileDialog:destroy(Dialog), + State + end. + +do_import2(_, "", _, _) -> + abort; +do_import2(Dir, File, J, Frame) -> + Path = filename:join(Dir, File), + Dialog = wxDialog:new(Frame, ?wxID_ANY, J("Set Node")), + Sizer = wxBoxSizer:new(?wxVERTICAL), + + NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), + NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)), + PassSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Passphrase")}]), + PassTx = wxTextCtrl:new(Dialog, ?wxID_ANY), + _ = wxSizer:add(PassSz, PassTx, 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, NameSz, zxw:flags(base)), + _ = wxSizer:add(Sizer, PassSz, zxw:flags(base)), + _ = wxSizer:add(Sizer, ButtSz, zxw:flags(wide)), + + ok = wxDialog:setSizer(Dialog, Sizer), + ok = wxBoxSizer:layout(Sizer), + ok = wxFrame:setSize(Dialog, {500, 200}), + ok = wxFrame:center(Dialog), + ok = wxStyledTextCtrl:setFocus(NameTx), + + Result = + case wxDialog:showModal(Dialog) of + ?wxID_OK -> + Name = + case wxTextCtrl:getValue(NameTx) of + "" -> Path; + N -> N + end, + Pass = + case wxTextCtrl:getValue(PassTx) of + "" -> none; + P -> P + end, + gmc_con:import_wallet(Name, Path, Pass); + ?wxID_CANCEL -> + abort + end, + ok = wxDialog:destroy(Dialog), + Result. + + +do_selection(Selected, State = #s{prefs = Prefs}) -> + NewPrefs = maps:put(selected, Selected, Prefs), + State#s{prefs = NewPrefs}.