deploy hello world contract to testnet
This commit is contained in:
parent
d99fefcd15
commit
7299dbc9f1
269
README.md
269
README.md
@ -1157,3 +1157,272 @@ hz status: {ok,#{"difficulty" => 3172644578,
|
|||||||
"top_key_block_hash" =>
|
"top_key_block_hash" =>
|
||||||
"kh_2TX5p81WtTX3y82NPdfWwv7yuehDh6aMRh1Uy6GBS5JsdkaGXu"}}
|
"kh_2TX5p81WtTX3y82NPdfWwv7yuehDh6aMRh1Uy6GBS5JsdkaGXu"}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Testnet info
|
||||||
|
|
||||||
|
- Explorer: <http://84.46.242.9:5001> (status)
|
||||||
|
- Faucet: <http://84.46.242.9:5000/>
|
||||||
|
- Middleware: <http://84.46.242.9:4000> (status)
|
||||||
|
- Endpoint: <http://84.46.242.9:3013> (status)
|
||||||
|
|
||||||
|
### Deploying a contract to testnet
|
||||||
|
|
||||||
|
- Deployed to: `ct_2PbZyDvyECxnwVvL5Y1ryHciY9J1EJmNmGWtP1uEJW3dn73MEv`
|
||||||
|
- [Explorer link](http://84.46.242.9:5001/contract/ct_2PbZyDvyECxnwVvL5Y1ryHciY9J1EJmNmGWtP1uEJW3dn73MEv)
|
||||||
|
|
||||||
|
#### Goal
|
||||||
|
|
||||||
|
Take this contract:
|
||||||
|
|
||||||
|
```c
|
||||||
|
/**
|
||||||
|
* Hello world contract in sophia
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025, QPQ AG
|
||||||
|
*/
|
||||||
|
|
||||||
|
@compiler 9.0.0
|
||||||
|
|
||||||
|
contract Hello =
|
||||||
|
type state = ()
|
||||||
|
|
||||||
|
entrypoint
|
||||||
|
init : () => state
|
||||||
|
init() = ()
|
||||||
|
|
||||||
|
entrypoint
|
||||||
|
hello : () => string
|
||||||
|
hello() = "hello"
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy it to the testnet, call `hello` entrypoint, and get the result back
|
||||||
|
|
||||||
|
#### Big picture
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Need a keypair (someone who owns the contract) -> [`ec_utils` library](https://github.com/hanssv/ec_utils)
|
||||||
|
2. Give our public key to the [faucet][tn-faucet] and get some gas money
|
||||||
|
3. Use [`hz:contract_create/3`](https://git.qpq.swiss/QPQ-AG/hakuzaru/src/commit/b13af3d0822762df167ac56da89e30cf8372c673/src/hz.erl#L859-L884)
|
||||||
|
to make a `ContractCreateTx`
|
||||||
|
4. Use [`hz:prepare_contract/1`](https://git.qpq.swiss/QPQ-AG/hakuzaru/src/commit/b13af3d0822762df167ac56da89e30cf8372c673/src/hz.erl#L1395-L1407)
|
||||||
|
to get an "AACI", which is a data structure that maps Erlang types
|
||||||
|
(integers, strings, tuples, lists, etc) to FATE types.
|
||||||
|
|
||||||
|
Essentially when we go from Sophia (source language) to FATE (VM bytecode),
|
||||||
|
a lot of information is lost. The AACI is the information that is lost,
|
||||||
|
which is precisely what is needed to translate back and forth between Erlang
|
||||||
|
and FATE data.
|
||||||
|
|
||||||
|
#### Deploying diff
|
||||||
|
|
||||||
|
Start point: `d99fefcd1540d5ded0c000c5608992805217bd25`
|
||||||
|
|
||||||
|
```diff
|
||||||
|
diff --git a/gex_httpd/src/gex_httpd.erl b/gex_httpd/src/gex_httpd.erl
|
||||||
|
index 0cb09bd..f9d7a4f 100644
|
||||||
|
--- a/gex_httpd/src/gex_httpd.erl
|
||||||
|
+++ b/gex_httpd/src/gex_httpd.erl
|
||||||
|
@@ -8,7 +8,6 @@
|
||||||
|
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
|
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
|
|
||||||
|
-
|
||||||
|
%% for our edification
|
||||||
|
-export([listen/1, ignore/0]).
|
||||||
|
-export([start/0]).
|
||||||
|
@@ -17,6 +16,12 @@
|
||||||
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
|
|
||||||
|
+-include("$zx_include/zx_logger.hrl").
|
||||||
|
+
|
||||||
|
+%------------------------------------------------------
|
||||||
|
+% API
|
||||||
|
+%------------------------------------------------------
|
||||||
|
+
|
||||||
|
|
||||||
|
-spec listen(PortNum) -> Result
|
||||||
|
when PortNum :: inet:port_num(),
|
||||||
|
@@ -77,7 +82,7 @@ start(normal, _Args) ->
|
||||||
|
hz() ->
|
||||||
|
ok = application:ensure_started(hakuzaru),
|
||||||
|
ok = hz:chain_nodes([testnet_node()]),
|
||||||
|
- ok = zx:tell("hz status: ~tp", [hz:status()]),
|
||||||
|
+ ok = tell("hz status: ~tp", [hz:status()]),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
testnet_ip() ->
|
||||||
|
diff --git a/gex_httpd/src/gh_ct.erl b/gex_httpd/src/gh_ct.erl
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000..212a679
|
||||||
|
--- /dev/null
|
||||||
|
+++ b/gex_httpd/src/gh_ct.erl
|
||||||
|
@@ -0,0 +1,164 @@
|
||||||
|
+% @doc miscellaneous contract functions
|
||||||
|
+%
|
||||||
|
+% mostly wrappers for ec_utils and hakuzaru
|
||||||
|
+-module(gh_ct).
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-export_type([
|
||||||
|
+ keypair/0
|
||||||
|
+]).
|
||||||
|
+
|
||||||
|
+-export([
|
||||||
|
+ deploy/2,
|
||||||
|
+ get_pubkey_akstr/0, get_keypair/0,
|
||||||
|
+ keypair_file/0,
|
||||||
|
+ read_keypair_from_file/1, write_keypair_to_file/2, fmt_keypair/1,
|
||||||
|
+ fmt_pubkey_api/1,
|
||||||
|
+ gen_keypair/0
|
||||||
|
+]).
|
||||||
|
+
|
||||||
|
+-include("$zx_include/zx_logger.hrl").
|
||||||
|
+
|
||||||
|
+%------------------------------------------------------
|
||||||
|
+% API: types
|
||||||
|
+%------------------------------------------------------
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-type keypair() :: #{public := binary(),
|
||||||
|
+ secret := binary()}.
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+%------------------------------------------------------
|
||||||
|
+% API: functions
|
||||||
|
+%------------------------------------------------------
|
||||||
|
+
|
||||||
|
+-spec deploy(ContractSrcPath, InitArgs) -> Result
|
||||||
|
+ when ContractSrcPath :: string(),
|
||||||
|
+ InitArgs :: term(),
|
||||||
|
+ Result :: {ok, term()}
|
||||||
|
+ | {error, term()}. %% FIXME
|
||||||
|
+
|
||||||
|
+deploy(ContractSrcPath, InitArgs) ->
|
||||||
|
+ CreatorId = get_pubkey_akstr(),
|
||||||
|
+ case hz:contract_create(CreatorId, ContractSrcPath, InitArgs) of
|
||||||
|
+ {ok, ContractCreateTx} ->
|
||||||
|
+ push(ContractCreateTx);
|
||||||
|
+ Error ->
|
||||||
|
+ tell(error, "gh_ct:deploy(~tp, ~tp) error: ~tp", [ContractSrcPath, InitArgs, Error]),
|
||||||
|
+ Error
|
||||||
|
+ end.
|
||||||
|
+
|
||||||
|
+push(ContractCreateTx) ->
|
||||||
|
+ #{secret := SecretKey} = get_keypair(),
|
||||||
|
+ SignedTx = hz:sign_tx(ContractCreateTx, SecretKey),
|
||||||
|
+ tell(info, "pushing signed tx: ~tp", [SignedTx]),
|
||||||
|
+ hz:post_tx(SignedTx).
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-spec get_pubkey_akstr() -> string().
|
||||||
|
+% @doc
|
||||||
|
+% get our pubkey as an ak_... string
|
||||||
|
+
|
||||||
|
+get_pubkey_akstr() ->
|
||||||
|
+ #{public := PK} = get_keypair(),
|
||||||
|
+ unicode:characters_to_list(fmt_pubkey_api(PK)).
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-spec get_keypair() -> keypair().
|
||||||
|
+% @doc
|
||||||
|
+% if can read keypair from `keypair_file()`, do so
|
||||||
|
+% otherwise generate one
|
||||||
|
+%
|
||||||
|
+% prints warnings if IO ops fail
|
||||||
|
+
|
||||||
|
+get_keypair() ->
|
||||||
|
+ case read_keypair_from_file(keypair_file()) of
|
||||||
|
+ {ok, KP} ->
|
||||||
|
+ KP;
|
||||||
|
+ % probably file
|
||||||
|
+ ReadError ->
|
||||||
|
+ tell(warning, "gh_ct:get_keypair(): read error: ~tp", [ReadError]),
|
||||||
|
+ KP = gen_keypair(),
|
||||||
|
+ % try writing to file
|
||||||
|
+ %tell(info, "gh_ct:get_keypair(): attempting to write keypair to file...", []),
|
||||||
|
+ %case write_keypair_to_file(keypair_file(), KP) of
|
||||||
|
+ % ok -> tell(info, "gh_ct:get_keypair(): write successful!", []);
|
||||||
|
+ % Error -> tell(warning, "gh_ct:get_keypair(): write error: ~tp", [Error])
|
||||||
|
+ %end,
|
||||||
|
+ KP
|
||||||
|
+ end.
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-spec keypair_file() -> string().
|
||||||
|
+% @doc
|
||||||
|
+% normal file where operating keypair is stored
|
||||||
|
+
|
||||||
|
+keypair_file() ->
|
||||||
|
+ filename:join([zx:get_home(), "priv", "keypair.eterms"]).
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-spec read_keypair_from_file(FilePath) -> Result
|
||||||
|
+ when FilePath :: string(),
|
||||||
|
+ Result :: {ok, keypair()}
|
||||||
|
+ | {error, Reason :: term()}.
|
||||||
|
+% @doc
|
||||||
|
+% try to read keypair from file in `file:consult/1` format.
|
||||||
|
+
|
||||||
|
+read_keypair_from_file(FilePath) ->
|
||||||
|
+ case file:consult(FilePath) of
|
||||||
|
+ {ok, [{public, PK}, {secret, SK}]} ->
|
||||||
|
+ {ok, #{public => PK, secret => SK}};
|
||||||
|
+ {ok, [{secret, SK}, {public, PK}]} ->
|
||||||
|
+ {ok, #{public => PK, secret => SK}};
|
||||||
|
+ {ok, Bad} ->
|
||||||
|
+ tell(warning, "read malformed keypair from file ~tp: ~tp", [FilePath, Bad]),
|
||||||
|
+ {error, bad_keypair};
|
||||||
|
+ Error ->
|
||||||
|
+ Error
|
||||||
|
+ end.
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-spec write_keypair_to_file(FilePath, Keypair) -> Result
|
||||||
|
+ when FilePath :: string(),
|
||||||
|
+ Keypair :: keypair(),
|
||||||
|
+ Result :: ok
|
||||||
|
+ | {error, Reason :: term()}.
|
||||||
|
+% @doc
|
||||||
|
+% Write keypair to file as
|
||||||
|
+%
|
||||||
|
+% ```
|
||||||
|
+% {public, <<...>>}.
|
||||||
|
+% {secret, <<..>>}.
|
||||||
|
+% ```
|
||||||
|
+
|
||||||
|
+write_keypair_to_file(FP, KP) ->
|
||||||
|
+ file:write_file(FP, fmt_keypair(KP)).
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-spec fmt_pubkey_api(binary()) -> binary().
|
||||||
|
+
|
||||||
|
+fmt_pubkey_api(Bin) ->
|
||||||
|
+ gmser_api_encoder:encode(account_pubkey, Bin).
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-spec fmt_keypair(keypair()) -> iolist().
|
||||||
|
+% @doc
|
||||||
|
+% format keypair in `file:consult/1` format
|
||||||
|
+
|
||||||
|
+fmt_keypair(#{public := PK, secret := SK}) ->
|
||||||
|
+ io_lib:format("{public, ~tp}.~n"
|
||||||
|
+ "{secret, ~tp}.~n",
|
||||||
|
+ [PK, SK]).
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+
|
||||||
|
+-spec gen_keypair() -> keypair().
|
||||||
|
+% @doc
|
||||||
|
+% Generate a keypair
|
||||||
|
+
|
||||||
|
+gen_keypair() ->
|
||||||
|
+ ecu_eddsa:sign_keypair().
|
||||||
|
```
|
||||||
|
|
||||||
|
[tn-explorer]: http://84.46.242.9:5001/
|
||||||
|
[tn-faucet]: http://84.46.242.9:5000/
|
||||||
|
|||||||
1
gex_httpd/.gitignore
vendored
1
gex_httpd/.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
|
priv/keypair.eterms
|
||||||
.eunit
|
.eunit
|
||||||
deps
|
deps
|
||||||
*.o
|
*.o
|
||||||
|
|||||||
16
gex_httpd/priv/ct/hello.aes
Normal file
16
gex_httpd/priv/ct/hello.aes
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Hello world contract in sophia
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025, QPQ AG
|
||||||
|
*/
|
||||||
|
|
||||||
|
@compiler == 9.0.0
|
||||||
|
|
||||||
|
contract Hello =
|
||||||
|
type state = unit
|
||||||
|
|
||||||
|
entrypoint init(): state =
|
||||||
|
()
|
||||||
|
|
||||||
|
entrypoint hello(): string =
|
||||||
|
"hello"
|
||||||
@ -8,7 +8,6 @@
|
|||||||
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
-author("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
-copyright("Peter Harpending <peterharpending@qpq.swiss>").
|
||||||
|
|
||||||
|
|
||||||
%% for our edification
|
%% for our edification
|
||||||
-export([listen/1, ignore/0]).
|
-export([listen/1, ignore/0]).
|
||||||
-export([start/0]).
|
-export([start/0]).
|
||||||
@ -17,6 +16,12 @@
|
|||||||
-export([start/2, stop/1]).
|
-export([start/2, stop/1]).
|
||||||
|
|
||||||
|
|
||||||
|
-include("$zx_include/zx_logger.hrl").
|
||||||
|
|
||||||
|
%------------------------------------------------------
|
||||||
|
% API
|
||||||
|
%------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
-spec listen(PortNum) -> Result
|
-spec listen(PortNum) -> Result
|
||||||
when PortNum :: inet:port_num(),
|
when PortNum :: inet:port_num(),
|
||||||
@ -77,7 +82,7 @@ start(normal, _Args) ->
|
|||||||
hz() ->
|
hz() ->
|
||||||
ok = application:ensure_started(hakuzaru),
|
ok = application:ensure_started(hakuzaru),
|
||||||
ok = hz:chain_nodes([testnet_node()]),
|
ok = hz:chain_nodes([testnet_node()]),
|
||||||
ok = zx:tell("hz status: ~tp", [hz:status()]),
|
ok = tell("hz status: ~tp", [hz:status()]),
|
||||||
ok.
|
ok.
|
||||||
|
|
||||||
testnet_ip() ->
|
testnet_ip() ->
|
||||||
|
|||||||
164
gex_httpd/src/gh_ct.erl
Normal file
164
gex_httpd/src/gh_ct.erl
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
% @doc miscellaneous contract functions
|
||||||
|
%
|
||||||
|
% mostly wrappers for ec_utils and hakuzaru
|
||||||
|
-module(gh_ct).
|
||||||
|
|
||||||
|
|
||||||
|
-export_type([
|
||||||
|
keypair/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([
|
||||||
|
deploy/2,
|
||||||
|
get_pubkey_akstr/0, get_keypair/0,
|
||||||
|
keypair_file/0,
|
||||||
|
read_keypair_from_file/1, write_keypair_to_file/2, fmt_keypair/1,
|
||||||
|
fmt_pubkey_api/1,
|
||||||
|
gen_keypair/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
-include("$zx_include/zx_logger.hrl").
|
||||||
|
|
||||||
|
%------------------------------------------------------
|
||||||
|
% API: types
|
||||||
|
%------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
-type keypair() :: #{public := binary(),
|
||||||
|
secret := binary()}.
|
||||||
|
|
||||||
|
|
||||||
|
%------------------------------------------------------
|
||||||
|
% API: functions
|
||||||
|
%------------------------------------------------------
|
||||||
|
|
||||||
|
-spec deploy(ContractSrcPath, InitArgs) -> Result
|
||||||
|
when ContractSrcPath :: string(),
|
||||||
|
InitArgs :: term(),
|
||||||
|
Result :: {ok, term()}
|
||||||
|
| {error, term()}. %% FIXME
|
||||||
|
|
||||||
|
deploy(ContractSrcPath, InitArgs) ->
|
||||||
|
CreatorId = get_pubkey_akstr(),
|
||||||
|
case hz:contract_create(CreatorId, ContractSrcPath, InitArgs) of
|
||||||
|
{ok, ContractCreateTx} ->
|
||||||
|
push(ContractCreateTx);
|
||||||
|
Error ->
|
||||||
|
tell(error, "gh_ct:deploy(~tp, ~tp) error: ~tp", [ContractSrcPath, InitArgs, Error]),
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
push(ContractCreateTx) ->
|
||||||
|
#{secret := SecretKey} = get_keypair(),
|
||||||
|
SignedTx = hz:sign_tx(ContractCreateTx, SecretKey),
|
||||||
|
tell(info, "pushing signed tx: ~tp", [SignedTx]),
|
||||||
|
hz:post_tx(SignedTx).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-spec get_pubkey_akstr() -> string().
|
||||||
|
% @doc
|
||||||
|
% get our pubkey as an ak_... string
|
||||||
|
|
||||||
|
get_pubkey_akstr() ->
|
||||||
|
#{public := PK} = get_keypair(),
|
||||||
|
unicode:characters_to_list(fmt_pubkey_api(PK)).
|
||||||
|
|
||||||
|
|
||||||
|
-spec get_keypair() -> keypair().
|
||||||
|
% @doc
|
||||||
|
% if can read keypair from `keypair_file()`, do so
|
||||||
|
% otherwise generate one
|
||||||
|
%
|
||||||
|
% prints warnings if IO ops fail
|
||||||
|
|
||||||
|
get_keypair() ->
|
||||||
|
case read_keypair_from_file(keypair_file()) of
|
||||||
|
{ok, KP} ->
|
||||||
|
KP;
|
||||||
|
% probably file
|
||||||
|
ReadError ->
|
||||||
|
tell(warning, "gh_ct:get_keypair(): read error: ~tp", [ReadError]),
|
||||||
|
KP = gen_keypair(),
|
||||||
|
% try writing to file
|
||||||
|
%tell(info, "gh_ct:get_keypair(): attempting to write keypair to file...", []),
|
||||||
|
%case write_keypair_to_file(keypair_file(), KP) of
|
||||||
|
% ok -> tell(info, "gh_ct:get_keypair(): write successful!", []);
|
||||||
|
% Error -> tell(warning, "gh_ct:get_keypair(): write error: ~tp", [Error])
|
||||||
|
%end,
|
||||||
|
KP
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
-spec keypair_file() -> string().
|
||||||
|
% @doc
|
||||||
|
% normal file where operating keypair is stored
|
||||||
|
|
||||||
|
keypair_file() ->
|
||||||
|
filename:join([zx:get_home(), "priv", "keypair.eterms"]).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-spec read_keypair_from_file(FilePath) -> Result
|
||||||
|
when FilePath :: string(),
|
||||||
|
Result :: {ok, keypair()}
|
||||||
|
| {error, Reason :: term()}.
|
||||||
|
% @doc
|
||||||
|
% try to read keypair from file in `file:consult/1` format.
|
||||||
|
|
||||||
|
read_keypair_from_file(FilePath) ->
|
||||||
|
case file:consult(FilePath) of
|
||||||
|
{ok, [{public, PK}, {secret, SK}]} ->
|
||||||
|
{ok, #{public => PK, secret => SK}};
|
||||||
|
{ok, [{secret, SK}, {public, PK}]} ->
|
||||||
|
{ok, #{public => PK, secret => SK}};
|
||||||
|
{ok, Bad} ->
|
||||||
|
tell(warning, "read malformed keypair from file ~tp: ~tp", [FilePath, Bad]),
|
||||||
|
{error, bad_keypair};
|
||||||
|
Error ->
|
||||||
|
Error
|
||||||
|
end.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-spec write_keypair_to_file(FilePath, Keypair) -> Result
|
||||||
|
when FilePath :: string(),
|
||||||
|
Keypair :: keypair(),
|
||||||
|
Result :: ok
|
||||||
|
| {error, Reason :: term()}.
|
||||||
|
% @doc
|
||||||
|
% Write keypair to file as
|
||||||
|
%
|
||||||
|
% ```
|
||||||
|
% {public, <<...>>}.
|
||||||
|
% {secret, <<..>>}.
|
||||||
|
% ```
|
||||||
|
|
||||||
|
write_keypair_to_file(FP, KP) ->
|
||||||
|
file:write_file(FP, fmt_keypair(KP)).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-spec fmt_pubkey_api(binary()) -> binary().
|
||||||
|
|
||||||
|
fmt_pubkey_api(Bin) ->
|
||||||
|
gmser_api_encoder:encode(account_pubkey, Bin).
|
||||||
|
|
||||||
|
|
||||||
|
-spec fmt_keypair(keypair()) -> iolist().
|
||||||
|
% @doc
|
||||||
|
% format keypair in `file:consult/1` format
|
||||||
|
|
||||||
|
fmt_keypair(#{public := PK, secret := SK}) ->
|
||||||
|
io_lib:format("{public, ~tp}.~n"
|
||||||
|
"{secret, ~tp}.~n",
|
||||||
|
[PK, SK]).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-spec gen_keypair() -> keypair().
|
||||||
|
% @doc
|
||||||
|
% Generate a keypair
|
||||||
|
|
||||||
|
gen_keypair() ->
|
||||||
|
ecu_eddsa:sign_keypair().
|
||||||
Loading…
x
Reference in New Issue
Block a user