diff --git a/ebin/gajudesk.app b/ebin/gajudesk.app index 1f70874..676fc99 100644 --- a/ebin/gajudesk.app +++ b/ebin/gajudesk.app @@ -3,7 +3,7 @@ {registered,[]}, {included_applications,[]}, {applications,[stdlib,kernel,sasl,ssl]}, - {vsn,"0.6.6"}, + {vsn,"0.7.0"}, {modules,[gajudesk,gd_con,gd_grids,gd_gui,gd_jt, gd_sophia_editor,gd_sup,gd_v,gd_v_devman,gd_v_netman, gd_v_wallman]}, diff --git a/src/gajudesk.erl b/src/gajudesk.erl index 995acf3..6ae95a4 100644 --- a/src/gajudesk.erl +++ b/src/gajudesk.erl @@ -3,7 +3,7 @@ %%% @end -module(gajudesk). --vsn("0.6.6"). +-vsn("0.7.0"). -behavior(application). -author("Craig Everett "). -copyright("QPQ AG "). diff --git a/src/gd_con.erl b/src/gd_con.erl index 75c20e4..6bb2e63 100644 --- a/src/gd_con.erl +++ b/src/gd_con.erl @@ -3,7 +3,7 @@ %%% @end -module(gd_con). --vsn("0.6.6"). +-vsn("0.7.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). @@ -38,15 +38,15 @@ mon = none :: none | reference()}). -record(s, - {version = 1 :: integer(), - window = none :: none | wx:wx_object(), - timer = none :: none | reference(), - tasks = [] :: [#ui{}], - selected = 0 :: non_neg_integer(), - wallet = none :: none | #wallet{}, - pass = none :: none | binary(), - prefs = #{} :: #{module() := term()}, - wallets = [] :: [#wr{}]}). + {version = 1 :: integer(), + window = none :: none | wx:wx_object(), + timer = none :: none | reference(), + tasks = [] :: [#ui{}], + selected = 0 :: non_neg_integer(), + wallet = none :: none | #wallet{}, + pass = none :: none | binary(), + prefs = #{} :: #{module() := term()}, + wallets = [] :: [#wr{}]}). -type state() :: #s{}. @@ -63,15 +63,16 @@ when Name :: ui_name(). show_ui(Name) -> - gen_server:cast(?MODULE, {show_ui, Name}). + gen_server:call(?MODULE, {show_ui, Name}). --spec open_wallet(Path, Phrase) -> ok +-spec open_wallet(Path, Phrase) -> Result when Path :: file:filename(), - Phrase :: string(). + Phrase :: string(), + Result :: ok | {error, Reason :: term()}. open_wallet(Path, Phrase) -> - gen_server:cast(?MODULE, {open_wallet, Path, Phrase}). + gen_server:call(?MODULE, {open_wallet, Path, Phrase}). -spec close_wallet() -> ok. @@ -217,7 +218,7 @@ deploy(Build, Params, InitArgs) -> Size :: 256, Name :: string(), Seed :: string(), - Encoding :: utf8 | base64 | base58, + Encoding :: utf8 | base64 | base58 | none, Transform :: {Algo, Yugeness}, Algo :: sha3 | sha2 | x_or | pbkdf2, Yugeness :: rand | non_neg_integer(). @@ -328,6 +329,7 @@ init(none) -> T = erlang:send_after(tic(), self(), tic), State = #s{window = Window, timer = T, wallets = Wallets, prefs = Prefs}, NewState = do_show_ui(gd_v_wallman, State), + ok = gd_v_wallman:first_run(), {ok, NewState}. @@ -365,6 +367,9 @@ handle_call(list_keys, _, State) -> handle_call({nonce, ID}, _, State) -> Response = do_nonce(ID), {reply, Response, State}; +handle_call({open_wallet, Path, Phrase}, _, State) -> + {Response, NewState} = do_open_wallet(Path, Phrase, State), + {reply, Response, NewState}; handle_call(network, _, State) -> Response = do_network(State), {reply, Response, State}; @@ -374,6 +379,9 @@ handle_call({save, Module, Prefs}, _, State) -> handle_call({mnemonic, ID}, _, State) -> Response = do_mnemonic(ID, State), {reply, Response, State}; +handle_call({show_ui, Name}, _, State) -> + NewState = do_show_ui(Name, State), + {reply, ok, NewState}; handle_call(Unexpected, From, State) -> ok = log(warning, "Unexpected call from ~tp: ~tp~n", [From, Unexpected]), {noreply, State}. @@ -387,16 +395,11 @@ handle_call(Unexpected, From, State) -> %% 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) -> NextState = do_close_wallet(State), ok = gd_gui:show([]), NewState = do_show_ui(gd_v_wallman, NextState), + ok = gd_v_wallman:to_front(), {noreply, NewState}; handle_cast({new_wallet, Net, Name, Path, Password}, State) -> NewState = do_new_wallet(Net, Name, Path, Password, State), @@ -477,6 +480,9 @@ handle_cast(Unexpected, State) -> handle_info(tic, State) -> NewState = do_tic(State), {noreply, NewState}; +handle_info({show_ui, Name}, State) -> + NewState = do_show_ui(Name, State), + {noreply, NewState}; handle_info({'DOWN', Mon, process, PID, Info}, State) -> NewState = handle_down(Mon, PID, Info, State), {noreply, NewState}; @@ -531,7 +537,6 @@ do_show_ui(Name, State = #s{tasks = Tasks, prefs = Prefs}) -> Win = Name:start_link({TaskPrefs, TaskData}), PID = wx_object:get_pid(Win), Mon = monitor(process, PID), - ok = Name:to_front(Win), UI = #ui{name = Name, pid = PID, wx = Win, mon = Mon}, State#s{tasks = [UI | Tasks]} end. @@ -863,7 +868,9 @@ do_make_key(Name, Seed, base58, Transform, State) -> do_make_key2(_, _, _, State = #s{wallet = none}) -> ok = gd_gui:trouble("No wallet selected!"), - do_show_ui(gd_v_wallman, State); + NewState = do_show_ui(gd_v_wallman, State), + ok = gd_v_wallman:to_front(), + NewState; do_make_key2(Name, Bin, Transform, State = #s{wallet = Current, wallets = Wallets, pass = Pass}) -> #wallet{name = WalletName, poas = POAs, keys = Keys} = Current, @@ -1020,17 +1027,12 @@ do_open_wallet(Path, Phrase, State) -> {ok, ChainID} -> gd_gui:chain(ChainID, Node); Error -> gd_gui:trouble(Error) end, - State#s{pass = Pass, wallet = Recovered}; + {ok, State#s{pass = Pass, wallet = Recovered}}; Error -> - ok = gd_gui:trouble(Error), - New = default_wallet(), - State#s{wallet = New} + {Error, State} end. -default_wallet() -> - default_wallet(testnet). - default_wallet(mainnet) -> Node = #node{ip = "groot.mainnet.gajumaru.io"}, Groot = #chain{id = <<"groot.mainnet">>, diff --git a/src/gd_grids.erl b/src/gd_grids.erl index 7a5f3f0..a639e30 100644 --- a/src/gd_grids.erl +++ b/src/gd_grids.erl @@ -37,7 +37,7 @@ %%% @end -module(gd_grids). --vsn("0.6.6"). +-vsn("0.7.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). @@ -59,7 +59,7 @@ Reason :: bad_url. parse(URL) -> - case uri_string:parse(URL) of + case uri_string:parse(string:trim(URL)) of #{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grids"} -> spend(R, chain, list_to_binary(H), Q); #{path := "/1/s/" ++ R, host := H, query := Q, scheme := "grid"} -> diff --git a/src/gd_gui.erl b/src/gd_gui.erl index 0e91dae..3b583aa 100644 --- a/src/gd_gui.erl +++ b/src/gd_gui.erl @@ -3,7 +3,7 @@ %%% @end -module(gd_gui). --vsn("0.6.6"). +-vsn("0.7.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). @@ -361,6 +361,7 @@ refresh(State) -> wallman(State) -> ok = gd_con:show_ui(gd_v_wallman), + ok = gd_v_wallman:to_front(), State. diff --git a/src/gd_jt.erl b/src/gd_jt.erl index d0e4858..6293b29 100644 --- a/src/gd_jt.erl +++ b/src/gd_jt.erl @@ -15,7 +15,7 @@ %%% translation library is retained). -module(gd_jt). --vsn("0.6.6"). +-vsn("0.7.0"). -export([read_translations/1, j/2, oneshot_j/2]). diff --git a/src/gd_sophia_editor.erl b/src/gd_sophia_editor.erl index 1529b60..448bb7c 100644 --- a/src/gd_sophia_editor.erl +++ b/src/gd_sophia_editor.erl @@ -1,5 +1,5 @@ -module(gd_sophia_editor). --vsn("0.6.6"). +-vsn("0.7.0"). -export([new/1, update/2, get_text/1, set_text/2]). diff --git a/src/gd_sup.erl b/src/gd_sup.erl index f3c9810..5f92335 100644 --- a/src/gd_sup.erl +++ b/src/gd_sup.erl @@ -12,7 +12,7 @@ %%% @end -module(gd_sup). --vsn("0.6.6"). +-vsn("0.7.0"). -behaviour(supervisor). -author("Craig Everett "). -copyright("QPQ AG "). diff --git a/src/gd_v.erl b/src/gd_v.erl index 5908de5..c93be9b 100644 --- a/src/gd_v.erl +++ b/src/gd_v.erl @@ -1,5 +1,5 @@ -module(gd_v). --vsn("0.6.6"). +-vsn("0.7.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gd_v_devman.erl b/src/gd_v_devman.erl index c2fa0a8..a5dad28 100644 --- a/src/gd_v_devman.erl +++ b/src/gd_v_devman.erl @@ -1,5 +1,5 @@ -module(gd_v_devman). --vsn("0.6.6"). +-vsn("0.7.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gd_v_netman.erl b/src/gd_v_netman.erl index 4de8e5e..000688a 100644 --- a/src/gd_v_netman.erl +++ b/src/gd_v_netman.erl @@ -1,5 +1,5 @@ -module(gd_v_netman). --vsn("0.6.6"). +-vsn("0.7.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). diff --git a/src/gd_v_wallman.erl b/src/gd_v_wallman.erl index 49f007e..cdf8514 100644 --- a/src/gd_v_wallman.erl +++ b/src/gd_v_wallman.erl @@ -1,5 +1,19 @@ +%%% @doc +%%% The GajuDesk Wallet Manager +%%% +%%% This is an interface for managing multiple wallets. +%%% A wallet is defined as a collection of accounts/keys. +%%% +%%% NOTE: +%%% Any call to `gd_con:show_ui(gd_v_wallman)' must be followed by a call to +%%% either `gd_v_wallman:to_front()' or `gd_v_wallman:first_run()' or else +%%% this UI process will just sit in the background (invisible) until told to +%%% do something. +%%% @end + + -module(gd_v_wallman). --vsn("0.6.6"). +-vsn("0.7.0"). -author("Craig Everett "). -copyright("QPQ AG "). -license("GPL-3.0-or-later"). @@ -7,7 +21,7 @@ -behavior(wx_object). %-behavior(gd_v). -include_lib("wx/include/wx.hrl"). --export([to_front/1]). +-export([to_front/0, to_front/1, first_run/0, trouble/1]). -export([show/2]). -export([start_link/1]). -export([init/1, terminate/2, code_change/3, @@ -29,11 +43,18 @@ prefs = #{} :: map(), wallets = [] :: [#wr{}], picker = none :: none | wx:wx_object(), - buttons = [] :: [#w{}]}). + buttons = [] :: [#w{}], + wiz = none :: none | {wx:wx_object(), Buttons :: [#w{}]}}). %%% Interface +-spec to_front() -> ok. + +to_front() -> + wx_object:cast(?MODULE, to_front). + + -spec to_front(Win) -> ok when Win :: wx:wx_object(). @@ -41,6 +62,19 @@ to_front(Win) -> wx_object:cast(Win, to_front). +-spec first_run() -> ok. + +first_run() -> + wx_object:cast(?MODULE, first_run). + + +-spec trouble(Info) -> ok + when Info :: term(). + +trouble(Info) -> + wx_object:cast(?MODULE, {trouble, Info}). + + -spec show(Win, Manifest) -> ok when Win :: wx:xw_object(), Manifest :: [#wr{}]. @@ -107,34 +141,32 @@ init({Prefs, Manifest}) -> ok = wxFrame:connect(Frame, command_button_clicked), ok = wxFrame:connect(Frame, close_window), - true = wxFrame:show(Frame), ok = wxListBox:connect(Picker, command_listbox_doubleclicked), - ok = - case length(Manifest) =:= 0 of - false -> - ok; - true -> - self() ! new, - ok - end, State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs, - wallets = Manifest, - picker = Picker, - buttons = Buttons}, + wallets = Manifest, + picker = Picker, + buttons = Buttons}, {Frame, State}. + %%% 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 = ensure_shown(Frame), ok = wxFrame:raise(Frame), {noreply, State}; +handle_cast(first_run, State) -> + NewState = do_first_run(State), + {noreply, NewState}; +handle_cast({trouble, Info}, State) -> + ok = handle_troubling(State, Info), + {noreply, State}; handle_cast({show, Manifest}, State) -> NewState = do_show(Manifest, State), {noreply, NewState}; @@ -143,9 +175,6 @@ handle_cast(Unexpected, State) -> {noreply, State}. -handle_info(new, State) -> - NewState = do_new(State), - {noreply, NewState}; handle_info(Unexpected, State) -> ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), {noreply, State}. @@ -153,7 +182,7 @@ handle_info(Unexpected, State) -> handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, - State = #s{buttons = Buttons}) -> + State = #s{buttons = Buttons, wiz = none}) -> NewState = case lists:keyfind(ID, #w.id, Buttons) of #w{name = open} -> do_open(State); @@ -166,17 +195,34 @@ handle_event(#wx{event = #wxCommand{type = command_button_clicked}, {noreply, NewState}; handle_event(#wx{event = #wxCommand{type = command_listbox_doubleclicked, commandInt = Selected}}, - State) -> - NewState = do_open2(Selected + 1, State), - {noreply, NewState}; -handle_event(#wx{event = #wxClose{}}, State) -> + State = #s{wiz = none}) -> + ok = do_open2(Selected + 1, State), + {noreply, State}; +handle_event(#wx{event = #wxClose{}}, State = #s{wiz = none}) -> ok = do_close(State), {noreply, State}; +handle_event(#wx{event = #wxCommand{type = command_button_clicked}, + id = ID}, + State = #s{wiz = {_, WizButtons}}) -> + NewState = + case lists:keyfind(ID, #w.id, WizButtons) of + #w{name = noob} -> wiz_noob_assist(State); + #w{name = l33t} -> close_wiz(State); + false -> State + end, + {noreply, NewState}; +handle_event(#wx{event = #wxClose{}}, State = #s{wiz = {_, _}}) -> + NewState = close_wiz(State), + {noreply, NewState}; handle_event(Event, State) -> ok = tell(info, "Unexpected event ~tp State: ~tp~n", [Event, State]), {noreply, State}. +handle_troubling(#s{frame = Frame}, Info) -> + zxw:show_message(Frame, Info). + + code_change(_, State, _) -> {ok, State}. @@ -197,6 +243,26 @@ do_show(Manifest, State = #s{picker = Picker}) -> State#s{wallets = Manifest}. +do_first_run(State = #s{frame = Frame, wallets = Manifest}) -> + Count = length(Manifest), + if + Count =:= 0 -> + do_wiz(State); + Count =:= 1 -> + do_open(State); + Count > 1 -> + true = wxFrame:show(Frame), + wxFrame:raise(Frame), + State + end. + + +close_wiz(State = #s{frame = Frame, wiz = {Wiz, _}}) -> + ok = wxWindow:destroy(Wiz), + true = wxFrame:show(Frame), + State#s{wiz = none}. + + do_close(#s{frame = Frame, prefs = Prefs}) -> Geometry = case wxTopLevelWindow:isMaximized(Frame) of @@ -219,10 +285,26 @@ handle_button(Name, State) -> do_open(State = #s{wallets = []}) -> State; +do_open(State = #s{wallets = [#wr{pass = true, path = Path}]}) -> + ok = do_open3(Path, State), + State; +do_open(State = #s{wallets = [#wr{pass = false, path = Path}], frame = Frame}) -> + ok = + case gd_con:open_wallet(Path, none) of + ok -> + do_close(State); + Error -> + ok = ensure_shown(Frame), + trouble(Error) + end, + State; do_open(State = #s{picker = Picker}) -> case wxListBox:getSelection(Picker) of - -1 -> State; - Selected -> do_open2(Selected + 1, State) + -1 -> + State; + Selected -> + ok = do_open2(Selected + 1, State), + State end. do_open2(Selected, State = #s{wallets = Wallets}) -> @@ -231,8 +313,7 @@ do_open2(Selected, State = #s{wallets = Wallets}) -> do_open3(Path, State); #wr{pass = false, path = Path} -> ok = gd_con:open_wallet(Path, none), - ok = do_close(State), - State + do_close(State) end. do_open3(Path, State = #s{frame = Frame, j = J}) -> @@ -250,32 +331,90 @@ do_open3(Path, State = #s{frame = Frame, j = J}) -> ok = wxDialog:setSizer(Dialog, Sizer), ok = wxBoxSizer:layout(Sizer), ok = wxDialog:setSize(Dialog, {500, 130}), - ok = wxFrame:center(Dialog), + ok = wxDialog:center(Dialog), ok = wxStyledTextCtrl:setFocus(PassTx), case wxDialog:showModal(Dialog) of ?wxID_OK -> case wxTextCtrl:getValue(PassTx) of "" -> ok = wxDialog:destroy(Dialog), - State; + ensure_shown(Frame); Phrase -> ok = wxDialog:destroy(Dialog), - ok = gd_con:open_wallet(Path, Phrase), - ok = do_close(State), - State + case gd_con:open_wallet(Path, Phrase) of + ok -> + do_close(State); + Error -> + ok = ensure_shown(Frame), + trouble(Error) + end end; ?wxID_CANCEL -> ok = wxDialog:destroy(Dialog), + ensure_shown(Frame) + end. + + +do_wiz(State = #s{wx = WX, lang = Lang, wiz = none}) -> + Trans = gd_jt:read_translations(?MODULE), + J = gd_jt:j(Lang, Trans), + + Wiz = wxFrame:new(WX, ?wxID_ANY, J("Initializing Wallet")), + MainSz = wxBoxSizer:new(?wxVERTICAL), + + ButtonTemplates = + [{noob, J("I'm new!\nCreate a new account for me.")}, + {l33t, J("Open the wallet manager.")}], + + MakeButton = + fun({Name, Label}) -> + B = wxButton:new(Wiz, ?wxID_ANY, [{label, Label}]), + #w{name = Name, id = wxButton:getId(B), wx = B} + end, + + Buttons = lists:map(MakeButton, ButtonTemplates), + + Add = fun(#w{wx = Button}) -> wxBoxSizer:add(MainSz, Button, zxw:flags(wide)) end, + ok = lists:foreach(Add, Buttons), + + ok = wxFrame:setSizer(Wiz, MainSz), + ok = wxSizer:layout(MainSz), + + ok = wxFrame:connect(Wiz, command_button_clicked), + ok = wxFrame:connect(Wiz, close_window), + ok = wxFrame:setSize(Wiz, {300, 300}), + ok = wxFrame:center(Wiz), + true = wxFrame:show(Wiz), + State#s{wiz = {Wiz, Buttons}}. + + +wiz_noob_assist(State = #s{j = J, wiz = {Wiz, _}}) -> + DefaultDir = zx_lib:path(var, "otpr", "gajudesk"), + Name = default_name(), + Path = filename:join(DefaultDir, Name), + case do_new2(Path, J, Wiz) of + ok -> + Label = J("Account 1"), + ok = gd_con:make_key({eddsa, ed25519}, 256, Label, <<>>, none, {sha3, 256}), + ok = do_close(State), + State; + abort -> State end. +default_name() -> + {{YY, MM, DD}, {Hr, Mn, Sc}} = calendar:local_time(), + Form = "~4.10.0B-~2.10.0B-~2.10.0B_~2.10.0B-~2.10.0B-~2.10.0B", + Name = io_lib:format(Form, [YY, MM, DD, Hr, Mn, Sc]), + unicode:characters_to_list(Name ++ ".gaju"). + do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) -> DefaultDir = maps:get(dir, Prefs, zx_lib:path(var, "otpr", "gajudesk")), Options = [{message, J("Save Location")}, {defaultDir, DefaultDir}, - {defaultFile, "default.gaju"}, + {defaultFile, default_name()}, {wildCard, "*.gaju"}, {sz, {300, 270}}, {style, ?wxFD_SAVE bor ?wxFD_OVERWRITE_PROMPT}], @@ -289,7 +428,9 @@ do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) -> case do_new2(Path, J, Frame) of ok -> NewPrefs = maps:put(dir, Dir, Prefs), - do_close(State#s{prefs = NewPrefs}); + NewState = State#s{prefs = NewPrefs}, + ok = do_close(NewState), + NewState; abort -> State end; @@ -301,8 +442,8 @@ do_new(State = #s{frame = Frame, j = J, prefs = Prefs}) -> do_new2(Path, J, Frame) -> Dialog = wxDialog:new(Frame, ?wxID_ANY, J("New Wallet"), [{size, {400, 250}}]), Sizer = wxBoxSizer:new(?wxVERTICAL), - - Network = wxRadioBox:new(Dialog, ?wxID_ANY, J("Network"), {0, 0}, {50, 50}, [J("Mainnet"), J("Testnet")]), + NetworkOptions = [J("Mainnet"), J("Testnet")], + Network = wxRadioBox:new(Dialog, ?wxID_ANY, J("Network"), {0, 0}, {50, 50}, NetworkOptions), NameSz = wxStaticBoxSizer:new(?wxVERTICAL, Dialog, [{label, J("Name")}]), NameTx = wxTextCtrl:new(Dialog, ?wxID_ANY), _ = wxSizer:add(NameSz, NameTx, zxw:flags(wide)), @@ -382,7 +523,9 @@ do_import(State = #s{frame = Frame, j = J, prefs = Prefs}) -> case do_import2(Dir, File, J, Frame) of ok -> NewPrefs = maps:put(dir, Dir, Prefs), - do_close(State#s{prefs = NewPrefs}); + NewState = State#s{prefs = NewPrefs}, + ok = do_close(NewState), + NewState; abort -> State end; @@ -484,3 +627,13 @@ do_drop(Selected, State = #s{j = J, frame = Frame, wallets = Wallets}) -> ok end, State. + + +ensure_shown(Frame) -> + case wxWindow:isShown(Frame) of + true -> + ok; + false -> + true = wxFrame:show(Frame), + ok + end. diff --git a/zomp.meta b/zomp.meta index 8f43620..7e49fef 100644 --- a/zomp.meta +++ b/zomp.meta @@ -4,7 +4,7 @@ {prefix,"gd"}. {author,"Craig Everett"}. {desc,"A desktop client for the Gajumaru network of blockchain networks"}. -{package_id,{"otpr","gajudesk",{0,6,6}}}. +{package_id,{"otpr","gajudesk",{0,7,0}}}. {deps,[{"otpr","hakuzaru",{0,6,1}}, {"otpr","eblake2",{1,0,1}}, {"otpr","base58",{0,1,1}},