diff --git a/README.md b/README.md index 69ba72a..b759e8e 100644 --- a/README.md +++ b/README.md @@ -1157,3 +1157,272 @@ hz status: {ok,#{"difficulty" => 3172644578, "top_key_block_hash" => "kh_2TX5p81WtTX3y82NPdfWwv7yuehDh6aMRh1Uy6GBS5JsdkaGXu"}} ``` + +### Testnet info + +- Explorer: (status) +- Faucet: +- Middleware: (status) +- Endpoint: (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 "). + -copyright("Peter Harpending "). + +- + %% 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/ diff --git a/gex_httpd/.gitignore b/gex_httpd/.gitignore index 20177b4..493c84a 100644 --- a/gex_httpd/.gitignore +++ b/gex_httpd/.gitignore @@ -1,3 +1,4 @@ +priv/keypair.eterms .eunit deps *.o diff --git a/gex_httpd/priv/ct/hello.aes b/gex_httpd/priv/ct/hello.aes new file mode 100644 index 0000000..2597d19 --- /dev/null +++ b/gex_httpd/priv/ct/hello.aes @@ -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" 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 "). -copyright("Peter Harpending "). - %% 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().