%%% @doc %%% Clutch Controller %%% %%% This process is a in charge of maintaining the program's state. %%% @end -module(gmc_con). -vsn("0.1.4"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). -behavior(gen_server). -export([show_ui/1, open_wallet/2, close_wallet/0, new_wallet/3, import_wallet/3, drop_wallet/2, password/2, refresh/0, nonce/1, spend/2, chain/1, grids/1, sign_mess/1, sign_tx/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([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"). -include("gmc.hrl"). %%% Type and Record Definitions -record(ui, {name = none :: none | ui_name(), pid = none :: none | pid(), wx = none :: none | wx:wx_object(), mon = none :: none | reference()}). -record(s, {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_wallman | gmc_v_devman. %% Interface -spec show_ui(Name) -> ok when Name :: ui_name(). show_ui(Name) -> gen_server:cast(?MODULE, {show_ui, Name}). -spec open_wallet(Path, Phrase) -> ok when Path :: file:filename(), Phrase :: string(). open_wallet(Path, Phrase) -> gen_server:cast(?MODULE, {open_wallet, Path, Phrase}). -spec close_wallet() -> ok. 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 drop_wallet(Path, Delete) -> ok when Path :: file:filename(), Delete :: boolean(). drop_wallet(Path, Delete) -> gen_server:cast(?MODULE, {drop_wallet, Path, Delete}). -spec password(Old, New) -> ok when Old :: none | string(), New :: none | string(). password(Old, New) -> gen_server:cast(?MODULE, {password, Old, New}). -spec refresh() -> ok. refresh() -> gen_server:cast(?MODULE, refresh). -spec nonce(ID) -> {ok, Nonce} | {error, Reason} when ID :: clutch:id(), Nonce :: integer(), Reason :: term(). % FIXME nonce(ID) -> gen_server:call(?MODULE, {nonce, ID}). -spec spend(KeyID, TX) -> ok when KeyID :: clutch:id(), TX :: #spend_tx{}. spend(KeyID, TX) -> gen_server:cast(?MODULE, {spend, KeyID, TX}). -spec chain(ID) -> ok when ID :: string(). chain(ID) -> gen_server:cast(?MODULE, {chain, ID}). -spec grids(string()) -> ok. grids(String) -> gen_server:cast(?MODULE, {grids, String}). -spec sign_mess(Request) -> ok when Request :: map(). sign_mess(Request) -> gen_server:cast(?MODULE, {sign_mess, Request}). -spec sign_tx(Request) -> ok when Request :: map(). sign_tx(Request) -> gen_server:cast(?MODULE, {sign_tx, Request}). -spec make_key(Type, Size, Name, Seed, Encoding, Transform) -> ok when Type :: {eddsa, ed25519}, Size :: 256, Name :: string(), Seed :: string(), Encoding :: utf8 | base64 | base58, Transform :: {Algo, Yugeness}, Algo :: sha3 | sha2 | x_or | pbkdf2, Yugeness :: rand | non_neg_integer(). %% @doc %% Generate a new key. %% The magic first two args are here because `ak_*' basically has no option other than %% to mean a 256 bit key based on Curve 25519. When more key varieties are added to the %% system this will change quite a lot. make_key({eddsa, ed25519}, 256, Name, Seed, Encoding, Transform) -> gen_server:cast(?MODULE, {make_key, Name, Seed, Encoding, Transform}). -spec recover_key(Mnemonic) -> ok when Mnemonic :: string(). recover_key(Mnemonic) -> gen_server:cast(?MODULE, {recover_key, Mnemonic}). -spec mnemonic(ID) -> {ok, Mnemonic} | error when ID :: clutch:id(), Mnemonic :: string(). mnemonic(ID) -> gen_server:call(?MODULE, {mnemonic, ID}). -spec rename_key(ID, NewName) -> ok when ID :: clutch:id(), NewName :: string(). rename_key(ID, NewName) -> gen_server:cast(?MODULE, {rename_key, ID, NewName}). -spec drop_key(ID) -> ok when ID :: clutch:id(). drop_key(ID) -> gen_server:cast(?MODULE, {drop_key, ID}). -spec add_node(New) -> ok when New :: #node{}. add_node(New) -> gen_server:cast(?MODULE, {add_node, New}). -spec set_sole_node(TheOneTrueNode) -> ok when TheOneTrueNode :: #node{}. set_sole_node(TheOneTrueNode) -> gen_server:cast(?MODULE, {set_sole_node, TheOneTrueNode}). %%% Lifecycle functions -spec stop() -> ok. stop() -> gen_server:cast(?MODULE, stop). -spec save(Module, Prefs) -> ok | {error, Reason} when Module :: module(), Prefs :: #{atom() := term()}, Reason :: file:posix(). save(Module, Prefs) -> gen_server:call(?MODULE, {save, Module, Prefs}). %%% Startup Functions -spec start_link() -> Result when Result :: {ok, pid()} | {error, Reason}, Reason :: {already_started, pid()} | {shutdown, term()} | term(). %% @private %% Called by gmc_sup. start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, none, []). -spec init(none) -> {ok, state()}. init(none) -> ok = log(info, "Starting"), Prefs = read_prefs(), GUI_Prefs = maps:get(gmc_gui, Prefs, #{}), Window = gmc_gui:start_link(GUI_Prefs), Wallets = get_prefs(wallets, Prefs, []), State = #s{window = Window, wallets = Wallets, prefs = Prefs}, NewState = do_show_ui(gmc_v_wallman, State), {ok, NewState}. read_prefs() -> case file:consult(prefs_path()) of {ok, Prefs} -> proplists:to_map(Prefs); _ -> #{} end. %%% gen_server Message Handling Callbacks -spec handle_call(Message, From, State) -> Result when Message :: term(), From :: {pid(), reference()}, State :: state(), Result :: {reply, Response, NewState} | {noreply, State}, Response :: ok | {error, {listening, inet:port_number()}}, NewState :: state(). %% @private %% The gen_server:handle_call/3 callback. %% See: http://erlang.org/doc/man/gen_server.html#Module:handle_call-3 handle_call({nonce, ID}, _, State) -> Response = do_nonce(ID), {reply, Response, 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), {reply, Response, State}; handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. -spec handle_cast(Message, State) -> {noreply, NewState} when Message :: term(), State :: state(), NewState :: state(). %% @private %% The gen_server:handle_cast/2 callback. %% See: http://erlang.org/doc/man/gen_server.html#Module:handle_cast-2 handle_cast({show_ui, Name}, State) -> NewState = do_show_ui(Name, State), {noreply, NewState}; 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), ok = gmc_gui:show([]), ok = do_show_ui(gmc_v_wallman, NewState), {noreply, NewState}; handle_cast({new_wallet, Name, Path, Password}, State) -> NewState = do_new_wallet(Name, Path, Password, State), {noreply, NewState}; handle_cast({import_wallet, Name, Path, Password}, State) -> NewState = do_import_wallet(Name, Path, Password, State), {noreply, NewState}; handle_cast({drop_wallet, Path, Delete}, State) -> NewState = do_drop_wallet(Path, Delete, State), {noreply, NewState}; handle_cast({password, Old, New}, State) -> NewState = do_password(Old, New, State), {noreply, NewState}; handle_cast(refresh, State) -> NewState = do_refresh(State), {noreply, NewState}; handle_cast({spend, KeyID, TX}, State) -> ok = do_spend(KeyID, TX, State), {noreply, State}; handle_cast({chain, ID}, State) -> NewState = do_chain(ID, State), {noreply, NewState}; handle_cast({grids, String}, State) -> ok = do_grids(String), {noreply, State}; handle_cast({sign_mess, Request}, State) -> ok = do_sign_mess(Request, State), {noreply, State}; handle_cast({sign_tx, Request}, State) -> ok = do_sign_tx(Request, State), {noreply, State}; handle_cast({make_key, Name, Seed, Encoding, Transform}, State) -> NewState = do_make_key(Name, Seed, Encoding, Transform, State), {noreply, NewState}; handle_cast({recover_key, Mnemonic}, State) -> NewState = do_recover_key(Mnemonic, State), {noreply, NewState}; handle_cast({rename_key, ID, NewName}, State) -> NewState = do_rename_key(ID, NewName, State), {noreply, NewState}; handle_cast({drop_key, ID}, State) -> NewState = do_drop_key(ID, State), {noreply, NewState}; handle_cast({add_node, New}, State) -> NewState = do_add_node(New, State), {noreply, NewState}; 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, NewState}; handle_cast(Unexpected, State) -> ok = tell(warning, "Unexpected cast: ~tp~n", [Unexpected]), {noreply, State}. -spec handle_info(Message, State) -> {noreply, NewState} when Message :: term(), State :: state(), NewState :: state(). %% @private %% The gen_server:handle_info/2 callback. %% See: http://erlang.org/doc/man/gen_server.html#Module:handle_info-2 handle_info({'DOWN', Mon, process, PID, Info}, State) -> NewState = handle_down(Mon, PID, Info, State), {noreply, NewState}; handle_info(Unexpected, State) -> ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), {noreply, State}. handle_down(Mon, PID, Info, State = #s{tasks = Tasks}) -> case lists:keytake(Mon, #ui.mon, Tasks) of {value, #ui{}, NewTasks} -> State#s{tasks = NewTasks}; false -> Unexpected = {'DOWN', Mon, process, PID, Info}, ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), State end. %% @private %% gen_server callback to handle state transformations necessary for hot %% code updates. This template performs no transformation. code_change(_, State, _) -> {ok, State}. terminate(normal, _) -> zx:stop(); terminate(Reason, State) -> ok = log(info, "Reason: ~tp, State: ~tp", [Reason, State]), zx:stop(). %%% GUI doers 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 -> 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; task_data(gmc_v_devman, #s{}) -> []. %%% 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. do_set_sole_node(TOTN = #node{external = none}, State) -> do_set_sole_node(TOTN#node{external = 3013}, State); do_set_sole_node(New = #node{ip = IP, external = Port}, State = #s{wallet = W}) -> ok = hz:chain_nodes([{IP, Port}]), case ensure_hz_set(New) of {ok, ChainID} -> 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); Error -> gmc_gui:trouble(Error), State end. do_refresh(State = #s{wallet = none}) -> State; do_refresh(State = #s{wallet = #wallet{endpoint = none}}) -> State; do_refresh(State = #s{wallet = #wallet{endpoint = Node}}) -> case ensure_hz_set(Node) of {ok, ChainID} -> do_refresh2(ChainID, State); Error -> ok = gmc_gui:trouble({do_refresh, 1, Error}), State end. do_refresh2(ChainID, State = #s{wallet = W = #wallet{poas = POAs}}) -> 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:chain_nodes() of [{IP, Port}] -> case hz:status() of {ok, #{"network_id" := ChainID}} -> ok = hz:network_id(ChainID), ok = gmc_gui:chain(ChainID, Node), {ok, list_to_binary(ChainID)}; {error, no_nodes} -> ok = hz:chain_nodes([{IP, Port}]), ensure_hz_set(Node); Error -> Error end; _ -> ok = hz:chain_nodes([{IP, Port}]), ensure_hz_set(Node) 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); Error -> gmc_gui:trouble(Error) end. do_grids2({{sign, http}, URL}) -> ok = log(info, "Making request to ~p", [URL]), case httpc:request(URL) of {ok, {{_, 200, _}, _, JSON}} -> do_grids_sig(JSON, URL); {error, socket_closed_remotely} -> log(info, "Socket closed remotely."); Error -> gmc_gui:trouble(Error) end; do_grids2({{sign, https}, URL}) -> ok = log(info, "Making request to ~p", [URL]), case httpc:request(URL) of {ok, {{_, 200, _}, _, JSON}} -> do_grids_sig(JSON, URL); {error, socket_closed_remotely} -> log(info, "Socket closed remotely."); Error -> gmc_gui:trouble(Error) end; do_grids2(Instruction) -> tell("GRIDS: ~tp", [Instruction]). do_grids_sig(JSON, URL) -> ok = log(info, "Received: ~p", [JSON]), case zj:decode(JSON) of {ok, GRIDS} -> do_grids_sig2(GRIDS#{"url" => URL}); Error -> gmc_gui:trouble(Error) end. do_grids_sig2(Request = #{"grids" := 1, "type" := "message"}) -> gmc_gui:grids_mess_sig(Request); do_grids_sig2(Request = #{"grids" := 1, "type" := "tx"}) -> gmc_gui:grids_mess_sig(Request); do_grids_sig2(WTF) -> gmc_gui:trouble({trash, 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)), do_sign_mess2(Request#{"signature" => Sig}); false -> gmc_gui:trouble({bad_key, ID}) end. do_sign_mess2(Request = #{"url" := URL}) -> ResponseKeys = ["grids", "chain", "network_id", "type", "public_id", "payload", "signature"], Response = zj:encode(maps:with(ResponseKeys, Request)), case httpc:request(post, {URL, [], "application/json", Response}, [], []) of {ok, {{_, 200, _}, _, JSON}} -> log(info, "Signature posted: ~p", [JSON]); {error, socket_closed_remotely} -> tell("Yep, closed remotely."); Error -> gmc_gui:trouble(Error) end. % TODO: Should probably be part of Hakuzaru sign_message(Message, PrivKey) -> 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). 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), case lists:keyfind(ID, #key.id, Keys) of #key{pair = #{secret := PrivKey}} -> BinaryTX = list_to_binary(CallData), SignedTX = sign_tx_hash(BinaryTX, PrivKey, BinNID), do_sign_tx2(Request#{"signed" => true, "payload" := SignedTX}); false -> gmc_gui:trouble({bad_key, ID}) end. do_sign_tx2(Request = #{"url" := URL}) -> ResponseKeys = ["grids", "chain", "network_id", "type", "public_id", "payload", "signed"], Response = zj:encode(maps:with(ResponseKeys, Request)), case httpc:request(post, {URL, [], "application/json", Response}, [], []) of {ok, {{_, 200, _}, _, JSON}} -> log(info, "Signed TX posted: ~p", [JSON]); {error, socket_closed_remotely} -> log(info, "Socket closed remotely."); Error -> gmc_gui:trouble(Error) end. sign_tx_hash(Unsigned, PrivKey, NetworkID) -> {ok, TX_Data} = aeser_api_encoder:safe_decode(transaction, Unsigned), {ok, Hash} = eblake2:blake2b(32, TX_Data), NetworkHash = <>, Signature = ecu_eddsa:sign_detached(NetworkHash, PrivKey), SigTxType = signed_tx, SigTxVsn = 1, SigTemplate = [{signatures, [binary]}, {transaction, binary}], TX = [{signatures, [Signature]}, {transaction, TX_Data}], SignedTX = aeser_chain_objects:serialize(SigTxType, SigTxVsn, SigTemplate, TX), aeser_api_encoder:encode(transaction, SignedTX). 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); do_make_key(Name, Seed, utf8, Transform, State) -> Bin = unicode:characters_to_binary(Seed), do_make_key2(Name, Bin, Transform, State); do_make_key(Name, Seed, base64, Transform, State) -> case base64_decode(Seed) of {ok, Bin} -> do_make_key2(Name, Bin, Transform, State); {error, Reason} -> ok = gmc_gui:trouble({error, {base64, Reason}}), State end; do_make_key(Name, Seed, base58, Transform, State) -> case base58:check_base58(Seed) of true -> Bin = base58:base58_to_binary(Seed), do_make_key2(Name, Bin, Transform, State); false -> ok = gmc_gui:trouble({error, {base58, badarg}}), State end. do_make_key2(Name, Bin, Transform, State = #s{wallet = Current, wallets = Wallets, pass = Pass}) -> #wallet{name = WalletName, poas = POAs, keys = Keys} = Current, T = transform(Transform), Seed = T(Bin), Key = #key{name = KeyName, id = ID} = gmc_key_master:make_key(Name, Seed), POA = #poa{name = KeyName, id = ID}, NewKeys = [Key | Keys], NewPOAs = [POA | POAs], Updated = Current#wallet{poas = NewPOAs, keys = NewKeys}, RW = lists:keyfind(WalletName, #wr.name, Wallets), ok = save_wallet(RW, Pass, Updated), ok = gmc_gui:show(NewPOAs), State#s{wallet = Updated}. base64_decode(String) -> try {ok, base64:decode(String)} catch E:R -> {E, R} end. transform({sha3, 256}) -> fun(D) -> crypto:hash(sha3_256, D) end; transform({sha2, 256}) -> fun(D) -> crypto:hash(sha256, D) end; transform({x_or, 256}) -> fun t_xor/1. t_xor(Bin) -> t_xor(Bin, <<0:256>>). t_xor(<>, A) -> t_xor(T, crypto:exor(H, A)); t_xor(<<>>, A) -> A; t_xor(B, A) -> H = <<0:(256 - bit_size(B)), B/binary>>, crypto:exor(H, A). do_recover_key(Mnemonic, State) -> case gmc_key_master:decode(Mnemonic) of {ok, Seed} -> do_recover_key2(Seed, State); Error -> ok = gmc_gui:trouble(Error), State end. 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} = gmc_key_master:make_key("", Seed), case lists:keymember(ID, #key.id, Keys) of false -> NewKeys = [Recovered | Keys], POA = #poa{name = AccName, id = ID}, NewPOAs = [POA | POAs], ok = gmc_gui:show(NewPOAs), Updated = Current#wallet{poas = NewPOAs, keys = NewKeys}, RW = lists:keyfind(WalletName, #wr.name, Wallets), ok = save_wallet(RW, Pass, Updated), State#s{wallet = Updated}; true -> State end. 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), {ok, Mnemonic}; false -> {error, bad_key} end. 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), NewPOAs = lists:keystore(ID, #poa.id, POAs, A#poa{name = NewName}), NewKeys = lists:keystore(ID, #key.id, Keys, K#key{name = NewName}), NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys}, ok = gmc_gui:show(NewPOAs), State#s{wallet = NewWallet}. 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), NewWallet = W#wallet{poas = NewPOAs, keys = NewKeys}, ok = gmc_gui:show(NewPOAs), State#s{wallet = NewWallet}. do_open_wallet(Path, Phrase, State) -> Pass = pass(Phrase), case read(Path, Pass) of {ok, Recovered = #wallet{name = Name, poas = POAs, endpoint = Node}} -> ok = gmc_gui:show(POAs), ok = gmc_gui:wallet(Name), ok = case ensure_hz_set(Node) of {ok, ChainID} -> gmc_gui:chain(ChainID, Node); Error -> gmc_gui:trouble(Error) end, State#s{pass = Pass, wallet = Recovered}; Error -> ok = gmc_gui:trouble(Error), New = default_wallet(), State#s{wallet = New} end. default_wallet() -> DevNet = #net{id = <<"devnet">>, chains = [#chain{}]}, % TestChain1 = #chain{id = <<"groot.testnet">>, % nodes = [#node{ip = {1,2,3,4}}, #node{ip = {5,6,7,8}}]}, % TestChain2 = #chain{id = <<"test_ac.testnet">>, % nodes = [#node{ip = {11,12,13,14}}, #node{ip = {15,16,17,18}}]}, % TestNet = #net{id = <<"testnet">>, chains = [TestChain1, TestChain2]}, #wallet{nets = [DevNet]}. do_password(none, none, State) -> State; do_password(none, New, State = #s{pass = none, wallet = #wallet{name = Name}, wallets = Wallets}) -> Pass = pass(New), 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 -> 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 -> 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_stop(State = #s{prefs = Prefs}) -> ok = persist(Prefs), do_close_wallet(State). do_new_wallet(Name, Path, Password, State = #s{wallets = Wallets, prefs = Prefs}) -> 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{name = Name}, ok = save_wallet(Entry, Pass, New), ok = gmc_gui:show([]), ok = gmc_gui:wallet(Name), NewWallets = [Entry | Wallets], NewPrefs = put_prefs(wallets, NewWallets, Prefs), ok = persist(NewPrefs), NextState#s{wallet = New, pass = Pass, wallets = NewWallets, prefs = NewPrefs}; #wr{} -> % FIXME % Need to provide feedback based on where this came from State end. do_import_wallet(Name, Path, Password, State = #s{wallets = Wallets}) -> NameExists = lists:keymember(Name, #wr.name, Wallets), PathExists = lists:keymember(Path, #wr.path, Wallets), case {NameExists, PathExists} of {false, false} -> do_import_wallet2(Name, Path, Password, State); {true, false} -> ok = gmc_gui:trouble({error, name_exists}), State; {false, true} -> ok = gmc_gui:trouble({error, path_exists}), State; {true, true} -> ok = gmc_gui:trouble("Whoa! This exact wallet already exists!"), State end. do_import_wallet2(Name, Path, Password, State = #s{wallets = Wallets, prefs = Prefs}) -> Pass = pass(Password), case read(Path, Pass) of {ok, Recovered = #wallet{poas = POAs, chain_id = ChainID, endpoint = Endpoint}} -> Imported = Recovered#wallet{name = Name}, HasPass = Pass =/= none, Record = #wr{name = Name, path = Path, pass = HasPass}, NewWallets = [Record | Wallets], NewPrefs = put_prefs(wallets, NewWallets, Prefs), ok = save_wallet(Record, Pass, Imported), ok = persist(NewPrefs), ok = gmc_gui:show(POAs), ok = gmc_gui:chain(ChainID, Endpoint), ok = gmc_gui:wallet(Name), State#s{wallet = Imported, wallets = NewWallets, prefs = NewPrefs}; Error -> ok = gmc_gui:trouble(Error), State end. do_drop_wallet(Path, Delete, State = #s{tasks = Tasks, wallet = Wallet, wallets = Wallets, prefs = Prefs}) -> CurrentName = case Wallet of #wallet{name = N} -> N; none -> none end, case lists:keytake(Path, #wr.path, Wallets) of {value, #wr{name = Name}, NewWallets} -> ok = case Name =:= CurrentName of true -> ok = gmc_gui:show([]), ok = gmc_gui:wallet(none), ok = gmc_gui:chain(none, none); false -> ok end, ok = maybe_clean(Delete, Path), NewPrefs = put_prefs(wallets, NewWallets, Prefs), ok = persist(NewPrefs), #ui{wx = WallMan} = lists:keyfind(gmc_v_wallman, #ui.name, Tasks), ok = gmc_v_wallman:show(WallMan, NewWallets), State#s{wallets = NewWallets, prefs = NewPrefs}; false -> State end. maybe_clean(true, Path) -> case file:delete(Path) of ok -> ok; Error -> gmc_gui:trouble(Error) end; maybe_clean(false, _) -> ok. get_prefs(K, M, D) -> P = maps:get(?MODULE, M, #{}), maps:get(K, P, D). put_prefs(K, V, M) -> P = maps:get(?MODULE, M, #{}), NewP = maps:put(K, V, P), maps:put(?MODULE, NewP, M). 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), State#s{pass = none, wallet = none}. save_wallet(#wr{path = Path, pass = false}, none, Wallet) -> ok = filelib:ensure_dir(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(Wallet)), file:write_file(Path, Cipher). read(Path, none) -> case file:read_file(Path) of {ok, Bin} -> read2(Bin); Error -> Error end; read(Path, Pass) -> case file:read_file(Path) of {ok, Cipher} -> try Bin = decrypt(Pass, Cipher), read2(Bin) catch error:{error, L, "Can't finalize"} -> ok = log(info, "Decrypt failed at ~p", [L]), {error, bad_password}; E:R -> {E, R} end; Error -> Error end. read2(Bin) -> case zx_lib:b_to_t(Bin) of {ok, T} -> read3(T); error -> {error, badarg} end. read3(T) -> case element(2, T) of 1 -> {ok, T}; _ -> {error, bad_wallet} end. persist(Prefs) -> Path = prefs_path(), ok = filelib:ensure_dir(Path), zx_lib:write_terms(Path, proplists:from_map(Prefs)). prefs_path() -> filename:join(zx_lib:path(etc, "otpr", "clutch"), "prefs.eterms").