From e59599161628b0317cd3520097f7e9edc28c993e Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Sun, 10 May 2026 21:02:23 +0900 Subject: [PATCH 1/9] Correct keyblock() type --- src/hz.erl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/hz.erl b/src/hz.erl index 0e2271b..8756608 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -125,13 +125,14 @@ % "info" => contract_byte_array(), % "miner" => account_id(), % "nonce" => non_neg_integer(), -% "pow" => [non_neg_integer()], % "prev_hash" => microblock_hash(), % "prev_key_hash" => keyblock_hash(), +% "seal" => #{"data" => [int()], +% "signature" => signature()} % "state_hash" => block_state_hash(), % "target" => non_neg_integer(), % "time" => non_neg_integer(), -% "version" => 5}. +% "version" => 1}. % -type microblock_header() :: #{string() => term()}. %
@@ -353,7 +354,7 @@ top_height() ->
 
 
 -spec top_block() -> {ok, TopBlock} | {error, Reason}
-    when TopBlock :: microblock_header(),
+    when TopBlock :: microblock_header() | keyblock(),
          Reason   :: chain_error().
 %% @doc
 %% Returns the header of the current top block.

From eaccd50764e7837cfff47780fc475205ed1e7b12 Mon Sep 17 00:00:00 2001
From: Craig Everett 
Date: Wed, 13 May 2026 19:48:49 +0900
Subject: [PATCH 2/9] Fill in the holes in hz.erl docs and make hz_fetcher.erl

---
 src/hz.erl         | 67 ++++++++++++++++++++++++++++++++++++++++++----
 src/hz_fetcher.erl |  8 ++++++
 2 files changed, 70 insertions(+), 5 deletions(-)

diff --git a/src/hz.erl b/src/hz.erl
index 8756608..7a72fac 100644
--- a/src/hz.erl
+++ b/src/hz.erl
@@ -662,9 +662,10 @@ dry_run(TX, Accounts, KBHash) ->
     request("/v3/dry_run", JSON).
 
 
-dry_run_map(Map) ->
-    JSON = zj:binary_encode(Map),
-    request("/v3/dry_run", JSON).
+% TODO
+%dry_run_map(Map) ->
+%    JSON = zj:binary_encode(Map),
+%    request("/v3/dry_run", JSON).
 
 
 -spec decode_bytearray_fate(EncodedStr) -> {ok, Result} | {error, Reason}
@@ -1643,6 +1644,14 @@ convert([], [], _, Terms, []) ->
 convert([], [], _, _, Errors) ->
     {error, Errors}.
 
+-spec sign_tx(Unsigned, SecKey) -> Result
+    when Unsigned :: string(),
+         SecKey   :: binary(),
+         Result   :: {ok, SignedTX} | {error, Reason}
+         SignedTX :: binary(),
+         Reason   :: chain_error().
+%% @doc
+%% Signs transaction data with the provided secret key for the currently selected network.
 
 sign_tx(Unsigned, SecKey) ->
     case network_id() of
@@ -1650,6 +1659,15 @@ sign_tx(Unsigned, SecKey) ->
         Error           -> Error
     end.
 
+
+-spec sign_tx(Unsigned, SecKey, NetworkID) -> SignedTX
+    when Unsigned  :: string(),
+         SecKey    :: binary(),
+         NetworkID :: string(),
+         SignedTX  :: binary().
+%% @doc
+%% Signs transaction data with the provided secret key using the provided network ID.
+
 sign_tx(Unsigned, SecKey, MNetworkID) ->
     UnsignedBin = unicode:characters_to_binary(Unsigned),
     NetworkID   = unicode:characters_to_binary(MNetworkID),
@@ -1669,10 +1687,21 @@ sign_tx(Unsigned, SecKey, MNetworkID) ->
     gmser_api_encoder:encode(transaction, SignedTX).
 
 
-spend(SenderID, SecKey, ReceipientID, Amount, Payload) ->
+-spec spend(SenderID, SecKey, RecipientID, Amount, Payload) -> {ok, Result} | {error, Reason}
+    when SenderID    :: string(),
+         SecKey      :: binary(),
+         RecipientID :: string(),
+         Amount      :: non_neg_integer(),
+         Payload     :: binary(),
+         Result      :: term(), % FIXME
+         Reason      :: chain_error() | string().
+%% @doc
+%% Forms a spend transaction and submits it to the chain.
+
+spend(SenderID, SecKey, RecipientID, Amount, Payload) ->
     case status() of
         {ok, #{"top_block_height" := Height, "network_id" := NetworkID}} ->
-            spend(SenderID, SecKey, ReceipientID, Amount, Payload, Height, NetworkID);
+            spend(SenderID, SecKey, RecipientID, Amount, Payload, Height, NetworkID);
         Error  ->
             Error
     end.
@@ -1699,6 +1728,22 @@ spend(SenderID, SecKey, RecipientID, Amount, Payload, Height, NetworkID) ->
     end.
 
 
+-spec spend(SenderID, SecKey, RecipientID, Amount,
+            GasPrice, Gas, TTL, Nonce, Payload, NetworkID) -> {ok, Result} | {error, Reason}
+    when SenderID    :: string(),
+         SecKey      :: binary(),
+         RecipientID :: string(),
+         Amount      :: non_neg_integer(),
+         GasPrice    :: pos_integer(),
+         Gas         :: pos_integer(),
+         TTL         :: non_neg_integer(),
+         Nonce       :: non_neg_integer(),
+         Payload     :: binary(),
+         Result      :: term(), % FIXME
+         Reason      :: chain_error() | string().
+%% @doc
+%% Forms a spend transaction and submits it to the chain.
+
 spend(SenderID,
       SecKey,
       RecipientID,
@@ -1811,6 +1856,10 @@ spend3(DSenderID,
     when Message :: binary(),
          SecKey  :: binary(),
          Sig     :: binary().
+%% @doc
+%% Accepts a string to be signed, prepends the prefix `"Gajumaru Signed Message:\n"',
+%% encodes the string with `vencode/1', then hashes the encoded message and signs the
+%% hash.
 
 sign_message(Message, SecKey) ->
     Prefix = message_sig_prefix(),
@@ -1889,6 +1938,12 @@ eu(N, Size) ->
     when Binary :: binary(),
          SecKey :: binary(),
          Sig    :: binary().
+%% @doc
+%% This procedure signs an arbitrary binary blob with a special binary prefix
+%% attached. The reason for the binary prefix is to prevent signing of dangerous
+%% binaries which could be used to authorized dangerous actions on chain.
+%% The signature target becomes: `<<"Gajumaru Signed Binary:", Binary/binary>>'
+%% before being hashed, and then the resulting hash is signed.
 
 sign_binary(Binary, SecKey) ->
     Prefix = binary_sig_prefix(),
@@ -1903,6 +1958,8 @@ sign_binary(Binary, SecKey) ->
          PubKey  :: pubkey(),
          Result  :: {ok, Outcome :: boolean()}
                   | {error, Reason :: term()}.
+%% @doc
+%% Verifies a signature created with the `sign_binary/2' function.
 
 verify_bin_signature(Sig, Binary, PubKey) ->
     case gmser_api_encoder:decode(PubKey) of
diff --git a/src/hz_fetcher.erl b/src/hz_fetcher.erl
index 0cf8414..37f931e 100644
--- a/src/hz_fetcher.erl
+++ b/src/hz_fetcher.erl
@@ -1,3 +1,11 @@
+%%% @private
+%%% Hakuzaru Request Fetcher
+%%%
+%%% This module defines the request workers.
+%%% Each request to a remote chain node is handled by a worker that is spawned
+%%% to handle it and terminates on completion.
+%%% @end
+
 -module(hz_fetcher).
 -vsn("0.9.1").
 -author("Craig Everett ").

From f8e9333b4b511bea03943daff5ebe00dd2a15e7d Mon Sep 17 00:00:00 2001
From: Craig Everett 
Date: Thu, 14 May 2026 11:01:37 +0900
Subject: [PATCH 3/9] Doc update

---
 doc/overview.edoc     |  2 +-
 ebin/hakuzaru.app     |  2 +-
 src/hakuzaru.erl      |  2 +-
 src/hz.erl            |  4 ++--
 src/hz_aaci.erl       |  2 +-
 src/hz_fetcher.erl    |  2 +-
 src/hz_format.erl     | 19 ++++++++++++++++++-
 src/hz_grids.erl      | 36 ++++++++++++++++++++++++++++++++++--
 src/hz_key_master.erl |  2 +-
 src/hz_man.erl        |  2 +-
 src/hz_sophia.erl     |  2 +-
 src/hz_sup.erl        |  2 +-
 zomp.meta             |  4 ++--
 13 files changed, 65 insertions(+), 16 deletions(-)

diff --git a/doc/overview.edoc b/doc/overview.edoc
index b52c71a..61eb6f3 100644
--- a/doc/overview.edoc
+++ b/doc/overview.edoc
@@ -1,5 +1,5 @@
 @author Craig Everett  [https://git.qpq.swiss/QPQ-AG/hakuzaru]
-@version 0.9.1
+@version 0.9.2
 @title Hakuzaru: Gajumaru blockchain bindings for Erlang
 
 @doc
diff --git a/ebin/hakuzaru.app b/ebin/hakuzaru.app
index 4df4bd1..30da878 100644
--- a/ebin/hakuzaru.app
+++ b/ebin/hakuzaru.app
@@ -3,7 +3,7 @@
               {included_applications,[]},
               {applications,[stdlib,kernel]},
               {description,"Gajumaru interoperation library"},
-              {vsn,"0.9.1"},
+              {vsn,"0.9.2"},
               {modules,[hakuzaru,hz,hz_aaci,hz_fetcher,hz_format,hz_grids,
                         hz_key_master,hz_man,hz_sophia,hz_sup]},
               {mod,{hakuzaru,[]}}]}.
diff --git a/src/hakuzaru.erl b/src/hakuzaru.erl
index a06da60..b0eb0a0 100644
--- a/src/hakuzaru.erl
+++ b/src/hakuzaru.erl
@@ -6,7 +6,7 @@
 %%% @end
 
 -module(hakuzaru).
--vsn("0.9.1").
+-vsn("0.9.2").
 -author("Craig Everett ").
 -copyright("Craig Everett ").
 -license("GPL-3.0-or-later").
diff --git a/src/hz.erl b/src/hz.erl
index 7a72fac..9aa3999 100644
--- a/src/hz.erl
+++ b/src/hz.erl
@@ -23,7 +23,7 @@
 %%% @end
 
 -module(hz).
--vsn("0.9.1").
+-vsn("0.9.2").
 -author("Craig Everett ").
 -copyright("Craig Everett ").
 -license("GPL-3.0-or-later").
@@ -1647,7 +1647,7 @@ convert([], [], _, _, Errors) ->
 -spec sign_tx(Unsigned, SecKey) -> Result
     when Unsigned :: string(),
          SecKey   :: binary(),
-         Result   :: {ok, SignedTX} | {error, Reason}
+         Result   :: {ok, SignedTX} | {error, Reason},
          SignedTX :: binary(),
          Reason   :: chain_error().
 %% @doc
diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl
index ebf1c6a..7b588e8 100644
--- a/src/hz_aaci.erl
+++ b/src/hz_aaci.erl
@@ -10,7 +10,7 @@
 %%% @end
 
 -module(hz_aaci).
--vsn("0.9.1").
+-vsn("0.9.2").
 -author("Jarvis Carroll ").
 -copyright("Craig Everett ").
 -license("GPL-3.0-or-later").
diff --git a/src/hz_fetcher.erl b/src/hz_fetcher.erl
index 37f931e..dd2b5c9 100644
--- a/src/hz_fetcher.erl
+++ b/src/hz_fetcher.erl
@@ -7,7 +7,7 @@
 %%% @end
 
 -module(hz_fetcher).
--vsn("0.9.1").
+-vsn("0.9.2").
 -author("Craig Everett ").
 -copyright("Craig Everett ").
 -license("MIT").
diff --git a/src/hz_format.erl b/src/hz_format.erl
index 2716989..dcd60b8 100644
--- a/src/hz_format.erl
+++ b/src/hz_format.erl
@@ -21,7 +21,7 @@
 %%% @end
 
 -module(hz_format).
--vsn("0.9.1").
+-vsn("0.9.2").
 -author("Craig Everett ").
 -copyright("Craig Everett ").
 -license("GPL-3.0-or-later").
@@ -462,9 +462,26 @@ ranks(heresy) ->
     ["k ", "m ", "b ", "t ", "q ", "e ", "z ", "y ", "r ", "Q "].
 
 
+-spec mark(Unit) -> Mark
+    when Unit :: gaju | puck,
+         Mark :: $木 | $本.
+%% @doc
+%% Retrieve the unicode codepoint for the `gaju' mark (木) or the `puck' mark (本).
+
 mark(gaju) -> $木;
 mark(puck) -> $本.
 
+
+-spec one(Unit) -> Pucks
+    when Unit  :: gaju | puck,
+         Pucks :: 1_000_000_000_000_000_000 | 1.
+%% @doc
+%% Quickly resolve the number of pucks in a given unit.
+%%
+%% The number of pucks in a gaju is so large that it can be a little bit annoying
+%% to remember the exact amount. This is a helper to simplify this when writing
+%% an app against the hakuzaru library when dealing in either unit.
+
 one(gaju) -> 1_000_000_000_000_000_000;
 one(puck) -> 1.
 
diff --git a/src/hz_grids.erl b/src/hz_grids.erl
index a8233b8..1eb9f0a 100644
--- a/src/hz_grids.erl
+++ b/src/hz_grids.erl
@@ -37,7 +37,7 @@
 %%% @end
 
 -module(hz_grids).
--vsn("0.9.1").
+-vsn("0.9.2").
 -export([url/2, url/3, url/4, parse/1, req/2, req/3, req/4]).
 
 
@@ -47,7 +47,7 @@
          Result      :: {ok, GRIDS} | uri_string:uri_error(),
          GRIDS       :: uri_string:uri_string().
 %% @doc
-%% Takes 
+%% Takes an instruction and an HTTP endpoint location and forms a GRIDS URL.
 
 url(Instruction, HTTP) ->
     case uri_string:parse(HTTP) of
@@ -134,6 +134,8 @@ qwargs(Amount, Payload) ->
          Amount      :: non_neg_integer(),
          Payload     :: binary(),
          URL         :: string().
+%% @doc
+%% Translate a GRIDS URL into an Erlang terms instruction.
 
 parse(GRIDS) ->
     case uri_string:parse(GRIDS) of
@@ -190,13 +192,43 @@ l_to_i(S) ->
     end.
 
 
+-spec req(Type, Message) -> Format
+    when Type    :: sign | tx | ack,
+         Message :: string() | binary(),
+         Format  :: map().
+%% @doc
+%% @equiv req(Type, Message, false)
+
 req(Type, Message) ->
     req(Type, Message, false).
 
+
+-spec req(Type, Message, ID) -> Format
+    when Type    :: sign | tx | ack,
+         Message :: string() | binary(),
+         ID      :: false | string() | binary(),
+         Format  :: map().
+%% @doc
+%% Creates a GRIDS message format with the current `NetworkID'.
+%%
+%% The `ID' parameter indicates which key the requestee should sign with or
+%% is `false' to indicate that which key to sign with is up to the requestee.
+%% @equiv req(Type, Message, ID, CurrentNetworkID)
+
 req(Type, Message, ID) ->
     {ok, NetworkID} = hz:network_id(),
     req(Type, Message, ID, NetworkID).
 
+
+-spec req(Type, Message, ID, NetworkID) -> Format
+    when Type      :: sign | tx | ack,
+         Message   :: string() | binary(),
+         ID        :: false | string() | binary(),
+         NetworkID :: string() | binary(),
+         Format    :: map().
+%% @doc
+%% Creates a GRIDS message format.
+
 req(sign, Message, ID, NetworkID) ->
     #{"grids"      => 1,
       "chain"      => "gajumaru",
diff --git a/src/hz_key_master.erl b/src/hz_key_master.erl
index 5113ee6..85246a5 100644
--- a/src/hz_key_master.erl
+++ b/src/hz_key_master.erl
@@ -8,7 +8,7 @@
 %%% @end
 
 -module(hz_key_master).
--vsn("0.9.1").
+-vsn("0.9.2").
 
 -export([make_key/1, encode/1, decode/1]).
 -export([lcg/1]).
diff --git a/src/hz_man.erl b/src/hz_man.erl
index e38e305..06491ed 100644
--- a/src/hz_man.erl
+++ b/src/hz_man.erl
@@ -9,7 +9,7 @@
 %%% @end
 
 -module(hz_man).
--vsn("0.9.1").
+-vsn("0.9.2").
 -behavior(gen_server).
 -author("Craig Everett ").
 -copyright("Craig Everett ").
diff --git a/src/hz_sophia.erl b/src/hz_sophia.erl
index 76a950d..9254635 100644
--- a/src/hz_sophia.erl
+++ b/src/hz_sophia.erl
@@ -1,5 +1,5 @@
 -module(hz_sophia).
--vsn("0.9.1").
+-vsn("0.9.2").
 -author("Jarvis Carroll ").
 -copyright("Jarvis Carroll ").
 -license("GPL-3.0-or-later").
diff --git a/src/hz_sup.erl b/src/hz_sup.erl
index a15e1e9..47ad92b 100644
--- a/src/hz_sup.erl
+++ b/src/hz_sup.erl
@@ -9,7 +9,7 @@
 %%% @end
 
 -module(hz_sup).
--vsn("0.9.1").
+-vsn("0.9.2").
 -behaviour(supervisor).
 -author("Craig Everett ").
 -copyright("Craig Everett ").
diff --git a/zomp.meta b/zomp.meta
index ded02f1..17eed7e 100644
--- a/zomp.meta
+++ b/zomp.meta
@@ -1,10 +1,10 @@
 {name,"Hakuzaru"}.
 {type,app}.
 {modules,[]}.
-{author,"Craig Everett"}.
 {prefix,"hz"}.
+{author,"Craig Everett"}.
 {desc,"Gajumaru interoperation library"}.
-{package_id,{"otpr","hakuzaru",{0,9,1}}}.
+{package_id,{"otpr","hakuzaru",{0,9,2}}}.
 {deps,[{"otpr","sophia",{9,0,0}},
        {"otpr","gmserialization",{0,1,3}},
        {"otpr","gmbytecode",{3,4,1}},

From a3b19747b6d6b55c211dfbeba8586821fd65e972 Mon Sep 17 00:00:00 2001
From: Craig Everett 
Date: Mon, 18 May 2026 12:56:31 +0900
Subject: [PATCH 4/9] Adjust error condition

---
 src/hz.erl | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/hz.erl b/src/hz.erl
index 9aa3999..5e51ca1 100644
--- a/src/hz.erl
+++ b/src/hz.erl
@@ -813,8 +813,10 @@ extract2(TarBaby) ->
     case erl_tar:extract({binary, TarBaby}, [memory, compressed]) of
         {ok, Bundle} ->
             {project, Bundle};
+        {error,invalid_tar_checksum} ->
+            {ok, TarBaby};
         Error ->
-            io:format("Dis chit happen: ~tp~n", [Error]),
+            ok = io:format("erl_tar:extract/2 error: ~tp~n", [Error]),
             {ok, TarBaby}
     end.
 

From 3fae9a2eddb2930dd503515131bbfd52f2414407 Mon Sep 17 00:00:00 2001
From: Jarvis Carroll 
Date: Thu, 14 May 2026 06:34:51 +0000
Subject: [PATCH 5/9] Document hz_aaci types

---
 src/hz_aaci.erl | 365 +++++++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 345 insertions(+), 20 deletions(-)

diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl
index 7b588e8..de3d8ea 100644
--- a/src/hz_aaci.erl
+++ b/src/hz_aaci.erl
@@ -29,14 +29,281 @@
 
 -include_lib("eunit/include/eunit.hrl").
 
--type aaci() :: {aaci, string(), #{string() => function_spec()}, #{string() => typedef()}}.
--type function_spec() :: {[{string(), annotated_type()}], annotated_type()}.
--type typedef() :: {[string()], typedef_rhs()}.
+%% @doc
+%% The Sophia-flavored 'Erlang representation' of on-chain data.
+%% Data is stored and manipulated on the chain without knowledge of Sophia
+%% types, which leads to a specialized representation that is confusing to
+%% manipulate directly. If you want to form contract arguments using an Erlang
+%% program, or pattern match the outputs of a contract call using an Erlang
+%% program, this Sophia-flavored representation is much more convenient. It
+%% de-anonymizes variant types and record types, and is more lenient in how it
+%% interprets a variety of cryptographic, binary, and string data types.
+%%
+%% When calling functions that manipulate this erlang representation, AACI type
+%% information representing the Sophia type of that term must be provided. The
+%% Sophia type used to produce that AACI type will determine what Erlang terms
+%% are actually accepted without producing errors.
+%%
+-type erlang_repr() ::   erlang_repr_int()
+                       | erlang_repr_address()
+                       | erlang_repr_contract()
+                       | erlang_repr_signature()
+                       | erlang_repr_bool()
+                       | erlang_repr_string()
+                       | erlang_repr_char()
+                       | erlang_repr_bytes()
+                       | erlang_repr_bits()
+                       | erlang_repr_list()
+                       | erlang_repr_map()
+                       | erlang_repr_tuple()
+                       | erlang_repr_variant()
+                       | erlang_repr_record().
+
+
+%-type erlang_repr() ::   integer()
+                       %| string()
+                       %| boolean()
+                       %| binary()
+                       %| tuple() % Tuples, variants, or raw addresses
+                       %| [erlang_repr()]
+                       %| #{erlang_repr() => erlang_repr()}.
+
+%% @doc
+%% The Erlang representation of a Sophia `int`
+%% Integers will be used as-is. Strings will be parsed using list_to_integer/1.
+%% fate_to_erlang/2 always produces the integer representation.
+
+-type erlang_repr_int() :: integer() | string().
+
+%% @doc
+%% The Erlang representation of a Sophia `address`
+%% This can either be the "ak_..." string produced by gmserialization,
+%% GajuDesk, etc. or a 'raw' binary of 32 bytes. fate_to_erlang/2 always
+%% produces the "ak_..." string as an Erlang list. The Sophia-flavored Erlang
+%% representation should not be used if this is undesirable.
+
+-type erlang_repr_address() :: unicode:chardata() | {raw, <<_:32*8>>}.
+
+%% @doc
+%% The Erlang representation of a Sophia `contract`
+%% This can either be the "ct_..." string produced by gmserialization,
+%% GajuDesk, etc. or a 'raw' binary of 32 bytes. fate_to_erlang/2 always
+%% produces the "ct_..." string as an Erlang list.
+
+-type erlang_repr_contract() :: unicode:chardata() | {raw, <<_:32*8>>}.
+
+%% @doc
+%% The Erlang representation of a Sophia `signature`
+%% This can either be the "sg_..." string produced by gmserialization,
+%% GajuDesk, etc. or a 'raw' binary of 64 bytes. (Not 32.) Unlike addresses and
+%% contracts, 'raw' binaries can be wrapped or unwrapped when representing a
+%% signature. fate_to_erlang/2 always produces the "sg_..." string as an Erlang
+%% list.
+
+-type erlang_repr_signature() :: unicode:chardata() | <<_:64*8>> | {raw, <<_:64*8>>}.
+
+%% @doc
+%% The Erlang representation of a Sophia `bool`
+%% fate_to_erlang/2 always produces atoms, but erlang_to_fate/2 also accepts
+%% the lists "true" and "false".
+
+-type erlang_repr_bool() :: true | false | string().
+
+%% @doc
+%% The Erlang representation of a Sophia `string`
+%% The conversion uses unicode:characters_to_binary/1, so a list, a UTF8
+%% binary, or an iolist mixing both are all acceptable inputs. fate_to_erlang/2
+%% always produces a list.
+
+-type erlang_repr_string() :: unicode:chardata().
+
+%% @doc
+%% The Erlang representation of a Sophia `char`
+%% On-chain a `char` means one unicode code point, and is just a FATE integer.
+%% fate_to_erlang/2 will provide this integer as-is, but erlang_to_fate/2 can
+%% be passed an arbitrary unicode string, as long as it decodes to a single
+%% unicode code point.
+
+-type erlang_repr_char() :: integer() | unicode:chardata().
+
+%% @doc
+%% The Erlang representation of Sophia `bytes()`
+%% Sophia has fixed-length `bytes(10)` etc. and variable length `bytes()`.
+%% These are treated the same in the Erlang representation, but
+%% erlang_to_fate/2 will check the length of the binary in the fixed length
+%% case, and provide errors if it doesn't agree.
+
+-type erlang_repr_bytes() :: binary().
+
+%% @doc
+%% The Erlang representation of Sophia `bits()`
+%% FATE has a representation of bitstrings that one might call novel. A
+%% FATE/Sophia bitstring is actually represented as an integer, so there is no
+%% concept of bitstring 'length', all bitstrings have infinitely many leading
+%% zeroes, if the integer is positive, and, surprisingly, infinitely many
+%% leading ones, if the integer is negative! To represent this in the general
+%% case, erlang_to_fate/2 accepts arbitrary integers, positive or negative, and
+%% fate_to_erlang/2 always produces integers, but for convenience,
+%% erlang_to_fate/2 also accepts arbitrary Erlang bitstrings, which are
+%% converted into positive integers, i.e. '0 by default' FATE bitstrings.
+
+-type erlang_repr_bits() :: bitstring().
+
+%% @doc
+%% The Erlang representation of a Sophia `list(_)`
+%% Simply a list. Each element of the list is converted forwards/backwards as
+%% normal.
+
+-type erlang_repr_list() :: [erlang_repr()].
+
+%% @doc
+%% The Erlang representation of a Sophia `map(_, _)`
+%% Simply a map. Each key and value is converted forwards/backwards as normal.
+
+-type erlang_repr_map() :: #{erlang_repr() => erlang_repr()}.
+
+%% @doc
+%% The Erlang representation of a Sophia tuple
+%% In Sophia these types are written `a * b`, `a * b * c`, and so on. Despite
+%% the binary infix notation, a product of more than two types gives a single
+%% tuple type with that many elements, so (1, 2, 3) is an int * int * int.
+%% gmbytecode requires FATE tuples to be wrapped in {tuple, {X, Y}}, etc. but
+%% the Erlang representation specifically requires that the tuple be provided
+%% without any wrappers, so {X, Y}, etc. These representations cannot be mixed,
+%% since at the highest level they are both just tuples. Each element of the
+%% tuple is also converted forwards/backwards as normal. Although FATE has
+%% singleton tuples, Sophia doesn't, so an ACI/AACI will never produce a
+%% singleton tuple in an interface; if your contract takes singleton tuples,
+%% these Sophia representations will probably still work, but you won't be able
+%% to generate the AACI that makes them work, so it is likely simpler to just
+%% use the FATE representation.
+
+-type erlang_repr_tuple() :: {} | {erlang_repr(), erlang_repr()} | tuple().
+
+%% @doc
+%% The Erlang representation of a Sophia ADT
+%% Sophia has a `datatype` keyword that allows the definition of algebraic data
+%% types, also known as variants, tagged unions, sum types, coproduct types,
+%% etc. In Erlang these are normally represented as an atom, or as a tuple
+%% whose first term is an atom, so for familiarity, erlang_to_fate/2 accepts
+%% lists in place of atoms, or tuples whose first term is a list. Note that
+%% constructors in Sophia have to be capitalized, so actual atoms wouldn't be
+%% that convenient. fate_to_erlang/2 always produces a tuple whose first term
+%% is a list, even if that tuple is a singleton. This allows the user to
+%% blindly call element(0) or tuple_to_list(_) without annoying special cases.
+%%
+%% Sophia also has a few built-in algebraic data types, for building its
+%% standard library, and for exposing certain FATE primitives, which will
+%% therefore also use this representation, e.g. "None", {"None"}, or
+%% {"Some", Datum} for the `option(_)` type.
+
+-type erlang_repr_variant() :: string() | {string()} | {string(), erlang_repr()} | tuple().
+
+%% @doc
+%% The Erlang representation of a Sophia record type
+%% Sophia has a `record` keyword, that allows the definition of new record
+%% types. Sophia records are meant to be reminiscent of Sophia maps, so in the
+%% Erlang representation of Sophia records, we use a map, with strings as keys,
+%% and arbitrary erlang_repr() terms as values.
+
+-type erlang_repr_record() :: #{string() => erlang_repr()}.
+
+%% @doc
+%% The Accelerated Aeternity Contract Interface
+%% Sophia tooling was originally written around a javascript use-case, but
+%% hakuzaru is written for Erlang, so we don't really want to walk through big
+%% JSON trees every time we do an on-chain action, so the AACI exists to
+%% accelerate these actions, so that interacting with contract entrypoints from
+%% within a pure Erlang environment is convenient and fast.
+%%
+%% The layout may change, but an AACI basically consists of three parts:
+%% - The name of the contract,
+%% - The 'annotated' entrypoint specs, designed for fast conversion to/from
+%%   the representation used on-chain, see function_spec/0,
+%% - The 'opaque' type definitions, all the internal type aliases and
+%%   definitions within the contract and its imported namespaces.
+
+-type aaci() :: {aaci, string(), #{string() => function_spec()}, #{string() => typedef()}}.
+
+%% @doc
+%% The fully annotated spec of a contract entrypoint, for fast call formation
+%% The first term is a list of parameter names and their types, as expected by
+%% erlang_args_to_fate/2, and the second term is a single type, as expected by
+%% fate_to_erlang/2. See annotated_type/0 for the details of how these types
+%% are represented and why, but for most purposes it is fine to just store and
+%% pass these type terms around without looking at their contents.
+-type function_spec() :: {[{string(), annotated_type()}], annotated_type()}.
+%% @doc
+%% A fully annotated Sophia type
+%% Sophia allows for arbitrary nesting of type aliases, each with parameters,
+%% and each potentially substituting for another arbitrarily complex type
+%% alias, so there is a potentially indefinite amount of work converting the
+%% type `my_type_alias` as it would appear in Sophia/in the ACI, into the
+%% actual variant/record/list/map/tuple type expression that it ultimately
+%% represents. To overcome this, we 'annotate' a type, recording what its
+%% aliased name was, along with its actual definition.
+%%
+%% Normally you can extract the annotated types from a function_spec(), and
+%% pass them into the conversion function that needs them, but it can also be
+%% useful to walk through the annotated types yourself. Confusingly, if you
+%% want to recursively descend down an annotated type, you want to recurse on
+%% the third element in the tuple, not the first two, as the first two
+%% represent incomplete levels of normalization, which can be more descriptive
+%% for users, but aren't as actionable as the fully normalized third element.
+%%
+%% Despite the third term being the most important, it is kept at the end,
+%% because that is what is most memorable, since each element of the triple is
+%% more normalized than the last, and because that is what is easiest to read,
+%% since the third term is usually an explosion of nested braces and brackets,
+%% making anything written after it basically unreadable.
+%%
+%% If you look at examples of annotated types produced in your own programs,
+%% you will tend to see things like {integer, alread_normalized, integer},
+%% making it even less clear that the third element is the important one, or
+%% why that is. For some fairly simple but informative examples, consider these
+%% type aliases:
+%%   contract C =
+%%     record my_record('t) = {x: 't, y: 't}
+%%     type my_alias1 = int
+%%     type my_alias2 = list(my_alias1)
+%%     type my_alias3 = my_record(my_alias1)
+%% If these type aliases appeared in a function spec, the AACI would represent
+%% them as the following annotated types:
+%%    {"my_alias1", integer, integer}
+%%    {"my_alias2", {list, ["my_alias1"]}, {list, [{"my_alias1", integer, integer}]}}
+%%    {"my_alias3", {"my_record", ["my_alias1"]}, {record, [{"x", {"my_alias1", integer, integer}}, {"y", {"my_alias1", integer, integer}}]}}
+%%
+%% The first term is the type roughly as it appeared in the ACI, see
+%% opaque_type/0 for more information.
+%%
+%% The second term is that same type but 'head normalized', chasing type
+%% aliases iteratively, until it is some built in type like an integer, or some
+%% user-defined record type or ADT. If the alias reduces to a list or map or
+%% tuple with more aliased types nested inside, these nested type
+%% subexpressions are not normalized any further, as the 'list' or 'map'
+%% connective is considered the 'head' of the type expression, and is
+%% normalized. Record type names and ADT names are not considered aliases, and
+%% so are considered head normalized, but both can take parameters, which can
+%% also stay un-normalized, as with lists or maps. If the head normalized type
+%% is the same as the opaque type, then the atom 'already_normalized' is placed
+%% instead, as a hint that instead of printing messages like
+%% "my_alias1 (i.e.  int)", a simple message like "list(my_record)" will do.
+%%
+%% The third term is the head normalized type with two changes, first, record
+%% and variant definitions are subtituted in as well, giving a list of field
+%% names or constructor names in full, and second, each subexpression is
+%% recursively annotated, meaning its opaque, head-normalized, and fully
+%% normalized parts also appear as triples.
+
+-type annotated_type() :: {opaque_type(), already_normalized | opaque_type(), annotated_type_body()}.
+
+%% @doc
+%% The primitive connectives that complex type expressions can be built out of.
+%% It takes a parameter, since builtin_type(opaque_type()),
+%% builtin_type(annotated_type()), and builtin_type(typedef_expression()) are
+%% all useful recursive applications of these connectives.
 
--type annotated_type() :: {opaque_type(), already_normalized | opaque_type(), builtin_type(annotated_type())}.
 -type builtin_type(T) ::   {bytes, [integer() | any]}
-                         | {record, [{string(), T}]}
-                         | {variant, [{string(), [T]}]}
                          | {tuple, [T]}
                          | {list, [T]}
                          | {map, [T]}
@@ -51,17 +318,55 @@
                          | channel
                          | unknown_type.
 
+%% @doc
+%% The connectives for defining new records and ADTs.
+%% Record types and ADTs can both appear in the original type definitions in
+%% the body of a contract, as well as in the recursively normalized 'annotated
+%% types' that the AACI stores. We use the same layout in both cases.
+-type user_defined_type(T) ::   {record, [{string(), T}]} | {variant, [{string(), [T]}]}.
+
+%% @doc
+%% An opaque type as it originally appeared in a function spec.
+%% The Sophia compiler may have a different representation for these type
+%% expressions, but we make a simple representation here as well.
+%% These type expressions are really function applications, in a limited sort
+%% of rewrite calculus without higher order functions. After performing some
+%% rewrites, the format actually stays the same, so the second term in a type
+%% triple is also this 'opaque type', but that is a coincidence; this type is
+%% primarily designed to represent types that haven't been head-normalized at
+%% all % yet.
 -type opaque_type() :: string() | {string(), [opaque_type()]} | builtin_type(opaque_type()).
 
--type typedef_rhs() :: {var, string()} | string() | {string(), [opaque_type()]} | builtin_type(typedef_rhs()).
+%% @doc
+%% The recursively annotated part of an annotated type triple
+%% This can be any anonymous type connective, with annotated types inside, or
+%% it can be a record definition, with annotated types for fields, or it can be
+%% an ADT definition, with annotated types for each constructor input.
+-type annotated_type_body() :: builtin_type(annotated_type()) | user_defined_type(annotated_type()).
 
--type erlang_repr() ::   integer()
-                       | string()
-                       | boolean()
-                       | binary()
-                       | tuple() % Tuples, variants, or raw addresses
-                       | [erlang_repr()]
-                       | #{erlang_repr() => erlang_repr()}.
+%% @doc
+%% The recursive type expressions that can appear in the definitions of type aliases.
+%% Similar to opaque_type(), but type aliases can take parameters as well,
+%% which means those parameters can also appear anywhere within the recursive
+%% type expression that defines the type alias.
+-type typedef_expression() ::   {var, string()}
+                              | string()
+                              | {string(), [typedef_expression()]}
+                              | builtin_type(typedef_expression()).
+%% @doc
+%% A type definition as it appears in the AACI.
+%% A type definition has a list of parameter names, and then some body defined
+%% using builtin type connectives, other defined types, and those parameters.
+-type typedef() :: {[string()], typedef_body()}.
+
+%% @doc
+%% The possible right-hand-sides of a type definition
+%% A type definition means a type alias, a record definition, or an ADT
+%% definition. Aliases are just some type expression, possibly with type
+%% parameters, and records and variants are already defined above in
+%% user_defined_type/1, with arbitrary type expressions in each one, but again,
+%% they could contain type parameters as well.
+-type typedef_body() :: typedef_expression() | user_defined_type(typedef_expression()).
 
 %%% ACI/AACI
 
@@ -179,7 +484,7 @@ convert_typedefs_loop([Next | Rest], NamePrefix, Converted) ->
     when Tree     :: typedef_tree(),
          TypeDefs :: #{string() => typedef()}.
 
--type typedef_tree() :: {string(), [string()], typedef_rhs()} | list(typedef_tree()).
+-type typedef_tree() :: {string(), [string()], typedef_body()} | list(typedef_tree()).
 
 collect_opaque_types([], Types) ->
     Types;
@@ -194,7 +499,7 @@ collect_opaque_types({Name, Params, Def}, Types) ->
 -spec opaque_type(Params, ACIType) -> Opaque
     when Params  :: [string()],
          ACIType :: binary() | map(),
-         Opaque  :: typedef_rhs().
+         Opaque  :: opaque_type().
 
 % Convert an ACI type defintion/spec into the 'opaque type' representation that
 % our dereferencing algorithms can reason about.
@@ -513,6 +818,16 @@ substitute_opaque_types(Bindings, Types) ->
          Reason   :: term(),
          PathStep :: term().
 
+%% @doc
+%% Call erlang_to_fate/2 on a list of named values.
+%% See the documentation for the erlang_repr/0 type for more information on the
+%% format required.
+%% This is mainly used by hz.erl to form contract calls. The parameter names
+%% and parameter types are provided in one zipped list, exactly as they appear
+%% in the AACI datatype, and then a second list of concrete arguments are
+%% provided in the format that erlang_to_fate/2 expects. The parameter names
+%% are used to provide slightly more informative errors.
+
 erlang_args_to_fate(VarTypes, Terms) ->
     DefLength = length(VarTypes),
     ArgLength = length(Terms),
@@ -530,6 +845,15 @@ erlang_args_to_fate(VarTypes, Terms) ->
          Reason   :: term(),
          PathStep :: term().
 
+%% @doc
+%% Convert one Sophia-flavored Erlang term into one FATE-flavored Erlang terms.
+%% This is not usually used on its own, since if you need to form a contract
+%% call, you have a list of arguments, not a single argument. Nonetheless, if
+%% for some reason you want to use a mix of FATE-flavored Erlang terms and
+%% Sophia-flavored Erlang terms in one function call, it may be useful to
+%% convert the Sophia-flavored terms individually, to form a single
+%% FATE-flavored list for call formation.
+
 erlang_to_fate({_, _, integer}, S) when is_integer(S) ->
     {ok, S};
 erlang_to_fate({O, N, integer},  S) when is_list(S) ->
@@ -1082,9 +1406,9 @@ coerce_tuple_test() ->
     check_roundtrip(Type, {123, "456"}, {tuple, {123, <<"456">>}}).
 
 coerce_variant_test() ->
-    {ok, Type} = annotate_type({variant, [{"A", [integer]},
-                                                {"B", [integer, integer]}]},
-                                     #{}),
+    Definition = {variant, [{"A", [integer]},
+                            {"B", [integer, integer]}]},
+    {ok, Type} = annotate_type("t", #{"t" => {[], Definition}}),
     check_roundtrip(Type, {"A", 123}, {variant, [1, 2], 0, {123}}),
     check_roundtrip(Type, {"B", 456, 789}, {variant, [1, 2], 1, {456, 789}}).
 
@@ -1094,7 +1418,8 @@ coerce_option_test() ->
     check_roundtrip(Type, {"Some", 1}, {variant, [0, 1], 1, {1}}).
 
 coerce_record_test() ->
-    {ok, Type} = annotate_type({record, [{"a", integer}, {"b", integer}]}, #{}),
+    Definition = {record, [{"a", integer}, {"b", integer}]},
+    {ok, Type} = annotate_type("t", #{"t" => {[], Definition}}),
     check_roundtrip(Type, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
 
 coerce_bytes_test() ->

From 8bc79d3b3fa83c258ae62c48803a7b41b273a72b Mon Sep 17 00:00:00 2001
From: Jarvis Carroll 
Date: Fri, 15 May 2026 05:47:48 +0000
Subject: [PATCH 6/9] Fix dialyzer warnings for hz_aaci

A while ago I tried dialyzer and discovered that actually a lot of the AACI generation process
never fails, and whatever the one failure was that was possible, I think I decided was unnecessary,
and made that produce {ok, unknown_type} instead. I then set the types of these functions to be
{ok, Result} | {error, none()}, since there were in fact no errors, but dialyzer still spewed out
warnings for all the case blocks that redundantly check for these impossible error conditions.

Anyway now that is fixed! The behavior and external interface are all the same still, there are just fewer warnings
now.

Also added specs for a couple more internal functions, just because.
So noperhaps realized that actually none of it should fail. I never went in and
removed all the {ok, _} wrappers, but I did at least type the functions with the correct error list
---
 src/hz_aaci.erl | 79 +++++++++++++++++++++++--------------------------
 1 file changed, 37 insertions(+), 42 deletions(-)

diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl
index de3d8ea..a18f142 100644
--- a/src/hz_aaci.erl
+++ b/src/hz_aaci.erl
@@ -631,6 +631,16 @@ builtin_typedefs() ->
 % can simply render the normalized type expression and know that the error will
 % make sense.
 
+-spec annotate_function_specs(OpaqueSpecs, Types, Acc) -> Specs
+    when OpaqueSpecs     :: [{string(), ArgsOpaque, ResultOpaque}],
+         ArgsOpaque      :: [{string(), opaque_type()}],
+         ResultOpaque    :: opaque_type(),
+         Types           :: #{string() => typedef()},
+         Acc             :: #{string() => {ArgsAnnotated, ResultAnnotated}},
+         Specs           :: #{string() => {ArgsAnnotated, ResultAnnotated}},
+         ArgsAnnotated   :: [{string(), annotated_type()}],
+         ResultAnnotated :: annotated_type().
+
 annotate_function_specs([], _Types, Specs) ->
     Specs;
 annotate_function_specs([{Name, ArgsOpaque, ResultOpaque} | Rest], Types, Specs) ->
@@ -639,40 +649,29 @@ annotate_function_specs([{Name, ArgsOpaque, ResultOpaque} | Rest], Types, Specs)
     NewSpecs = maps:put(Name, {Args, Result}, Specs),
     annotate_function_specs(Rest, Types, NewSpecs).
 
--spec annotate_type(Opaque, Types) -> {ok, Annotated} | {error, Reason}
+-spec annotate_type(Opaque, Types) -> {ok, Annotated}
     when Opaque    :: opaque_type(),
          Types     :: #{string() => typedef()},
-         Annotated :: annotated_type(),
-         Reason    :: none().
+         Annotated :: annotated_type().
 
 annotate_type(T, Types) ->
     case normalize_opaque_type(T, Types) of
+        {ok, _, _, unknown_type} ->
+            {ok, {T, unknown_type, unknown_type}};
         {ok, AlreadyNormalized, NOpaque, NExpanded} ->
-            annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types);
-        Error ->
-            Error
+            annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types)
     end.
 
-annotate_type2(T, _, _, unknown_type, _) ->
-    % If a type is unknown, then it should not be reported as the normalized
-    % name.
-    {ok, {T, unknown_type, unknown_type}};
 annotate_type2(T, AlreadyNormalized, NOpaque, NExpanded, Types) ->
-    case annotate_type_subexpressions(NExpanded, Types) of
-        {ok, Flat} ->
-            case AlreadyNormalized of
-                true -> {ok, {T, already_normalized, Flat}};
-                false -> {ok, {T, NOpaque, Flat}}
-            end;
-        Error ->
-            Error
+    {ok, Flat} = annotate_type_subexpressions(NExpanded, Types),
+    case AlreadyNormalized of
+        true -> {ok, {T, already_normalized, Flat}};
+        false -> {ok, {T, NOpaque, Flat}}
     end.
 
 annotate_types([T | Rest], Types, Acc) ->
-    case annotate_type(T, Types) of
-        {ok, Type} -> annotate_types(Rest, Types, [Type | Acc]);
-        Error      -> Error
-    end;
+    {ok, Type} = annotate_type(T, Types),
+    annotate_types(Rest, Types, [Type | Acc]);
 annotate_types([], _Types, Acc) ->
     {ok, lists:reverse(Acc)}.
 
@@ -683,34 +682,30 @@ annotate_type_subexpressions({bytes, [Count]}, _Types) ->
     % opaque type.
     {ok, {bytes, [Count]}};
 annotate_type_subexpressions({variant, VariantsOpaque}, Types) ->
-    case annotate_variants(VariantsOpaque, Types, []) of
-        {ok, Variants} -> {ok, {variant, Variants}};
-        Error          -> Error
-    end;
+    {ok, Variants} = annotate_variants(VariantsOpaque, Types, []),
+    {ok, {variant, Variants}};
 annotate_type_subexpressions({record, FieldsOpaque}, Types) ->
-    case annotate_bindings(FieldsOpaque, Types, []) of
-        {ok, Fields} -> {ok, {record, Fields}};
-        Error        -> Error
-    end;
+    {ok, Fields} = annotate_bindings(FieldsOpaque, Types, []),
+    {ok, {record, Fields}};
 annotate_type_subexpressions({T, ElemsOpaque}, Types) ->
-    case annotate_types(ElemsOpaque, Types, []) of
-        {ok, Elems} -> {ok, {T, Elems}};
-        Error       -> Error
-    end.
+    {ok, Elems} = annotate_types(ElemsOpaque, Types, []),
+    {ok, {T, Elems}}.
+
+-spec annotate_bindings(Bindings, Types, Acc) -> {ok, Annotated}
+    when Bindings  :: [{string(), opaque_type()}],
+         Types     :: #{string() => typedef()},
+         Acc       :: [{string(), annotated_type()}],
+         Annotated :: [{string(), annotated_type()}].
 
 annotate_bindings([{Name, T} | Rest], Types, Acc) ->
-    case annotate_type(T, Types) of
-        {ok, Type} -> annotate_bindings(Rest, Types, [{Name, Type} | Acc]);
-        Error      -> Error
-    end;
+    {ok, Next} = annotate_type(T, Types),
+    annotate_bindings(Rest, Types, [{Name, Next} | Acc]);
 annotate_bindings([], _Types, Acc) ->
     {ok, lists:reverse(Acc)}.
 
 annotate_variants([{Name, Elems} | Rest], Types, Acc) ->
-    case annotate_types(Elems, Types, []) of
-        {ok, ElemsFlat} -> annotate_variants(Rest, Types, [{Name, ElemsFlat} | Acc]);
-        Error           -> Error
-    end;
+    {ok, ElemsFlat} = annotate_types(Elems, Types, []),
+    annotate_variants(Rest, Types, [{Name, ElemsFlat} | Acc]);
 annotate_variants([], _Types, Acc) ->
     {ok, lists:reverse(Acc)}.
 

From 23c13f607e44089caad8c0350ee963a511cc1f3c Mon Sep 17 00:00:00 2001
From: Jarvis Carroll 
Date: Fri, 15 May 2026 06:15:15 +0000
Subject: [PATCH 7/9] Document hz_aaci functions

Once the types were documented, the functions were easy to document. Just say "see erlang_expr/0 for details" over and over! ;p
---
 src/hz_aaci.erl | 27 ++++++++++++++++++++++-----
 1 file changed, 22 insertions(+), 5 deletions(-)

diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl
index a18f142..47d659d 100644
--- a/src/hz_aaci.erl
+++ b/src/hz_aaci.erl
@@ -375,8 +375,10 @@
          AACI   :: aaci(),
          Reason :: term().
 %% @doc
-%% Compile a contract and extract the function spec meta for use in future formation
-%% of calldata
+%% Compile a contract and extract the contract type information for forming contract calls
+%% This is the simplest (but slowest) way of getting access to the AACI
+%% structure for a contract. Having the AACI is not strictly necessary, but
+%% makes it much more convenient to form contract calls and view their results.
 
 prepare_from_file(Path) ->
     case so_compiler:file(Path, [{aci, json}]) of
@@ -388,6 +390,10 @@ prepare_from_file(Path) ->
     when ACI  :: term(),
          AACI :: aaci().
 
+%% @doc
+%% Convert the ACI structure produced by the compiler into the AACI format used by Hakuzaru
+%% See the documentation for the aaci/0 type for more information.
+
 prepare(ACI) ->
     % We want to take the types represented by the ACI, things like N1.T(N2.T),
     % and dereference them down to concrete types like
@@ -1209,6 +1215,14 @@ coerce_direction(Type, Term, from_fate) ->
          Reason   :: term(),
          PathStep :: term().
 
+%% @doc
+%% Convert a FATE-flavored Erlang term into a Sophia-flavored Erlang term
+%% Typically this is called by hakuzaru for you when decoding results from the
+%% chain, if you ask for the 'erlang' format, but you can call this function
+%% manually if you have a result in the 'fate' format, and need the 'erlang'
+%% format now. See the documentation of the erlang_repr/0 type for more
+%% information.
+
 fate_to_erlang({_, _, integer}, S) when is_integer(S) ->
     {ok, S};
 fate_to_erlang({_, _, address}, {address, Bin}) ->
@@ -1298,9 +1312,12 @@ opaque_type_to_iolist(N, _) ->
          Reason :: bad_fun_name.
 
 %% @doc
-%% Look up the type information of a given function, in the AACI provided by
-%% prepare_contract/1. This type information, particularly the return type, is
-%% useful for calling decode_bytearray/2.
+%% Extract the type information for a particular function from the AACI
+%% If you want to manually convert a FATE result into the Sophia-flavored
+%% Erlang representation, or manually convert some or all of the inputs for a
+%% contract call yourself, this function gives you all of the annotated types
+%% associated with a contract entrypoint. For more information, see the
+%% documentation for the annotated_type/0 type.
 
 get_function_signature({aaci, _, FunDefs, _}, Fun) ->
     case maps:find(Fun, FunDefs) of

From 9fc89c0c2224d9151e89e3963d55ceffc492e275 Mon Sep 17 00:00:00 2001
From: Jarvis Carroll 
Date: Tue, 19 May 2026 12:21:17 +0000
Subject: [PATCH 8/9] Add type information for hz_sophia

This one was much simpler to do than hz_aaci, since it doesn't introduce any new types.

I could have made note of some of the conventions, where a type can be represented in multiple
ways in Sophia syntax, or where these functions are actually more lenient than the compiler, but
it isn't as easy to break those notes up from the basic function usage, like it was in hz_aaci,
where those aforementioned new types are used.
---
 src/hz_sophia.erl | 79 +++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 69 insertions(+), 10 deletions(-)

diff --git a/src/hz_sophia.erl b/src/hz_sophia.erl
index 9254635..f81d377 100644
--- a/src/hz_sophia.erl
+++ b/src/hz_sophia.erl
@@ -4,26 +4,28 @@
 -copyright("Jarvis Carroll ").
 -license("GPL-3.0-or-later").
 
--export([parse_literal/1, parse_literal/2]).
+-export([parse_literal/2, parse_literal/1]).
 -export([fate_to_list/1, fate_to_list/2, fate_to_iolist/1, fate_to_iolist/2]).
 
 -include_lib("eunit/include/eunit.hrl").
 
-
--spec parse_literal(Sophia) -> {ok, FATE} | {error, Reason}
-    when Sophia :: string(),
-         FATE   :: gmb_fate_data:fate_type(),
-         Reason :: term().
-
-parse_literal(String) ->
-    parse_literal(unknown_type(), String).
-
 -spec parse_literal(Type, Sophia) -> {ok, FATE} | {error, Reason}
     when Type :: hz_aaci:annotated_type(),
          Sophia :: string(),
          FATE   :: gmb_fate_data:fate_type(),
          Reason :: term().
 
+%% @doc
+%% Parse a typed Sophia expression into a FATE term
+%% The Sophia expression must consist only of literals, thus making a 'Sophia
+%% term', which means no arithmetic, no function calls, no variables, etc.
+%% The FATE term is in the format that gmbytecode expects as input, for forming
+%% contract calls, etc. Used by the hz module to implement the 'sophia' format.
+%%
+%% The function takes type information retrieved from the AACI data structure,
+%% which is used to interpret record types and variant types, but is also used
+%% to check inputs and generate errors.
+
 parse_literal(Type, String) ->
     case parse_expression(Type, {1, 1}, String) of
         {ok, {Result, NewPos, NewString}} ->
@@ -43,6 +45,28 @@ parse_literal2(Result, Pos, String) ->
             {error, Reason}
     end.
 
+
+-spec parse_literal(Sophia) -> {ok, FATE} | {error, Reason}
+    when Sophia :: string(),
+         FATE   :: gmb_fate_data:fate_type(),
+         Reason :: term().
+
+%% @doc
+%% Parse an untyped Sophia expression into a FATE term
+%% Like parse_literal/2, but will not produce type errors. This function can
+%% still produce parsing errors, and can produce errors when variants or
+%% records are encountered, since they can't be parsed unless you have type
+%% information.
+%%
+%% Note that since records are implemented as tuples, if you are trying to call
+%a function that you know takes a record, but you don't have type information
+%% available in the context where the expression is being passed, then tuples
+%% can be used instead. This does not work if you have type information,
+%% though, as tuples and records are different Sophia/AACI types.
+
+parse_literal(String) ->
+    parse_literal(unknown_type(), String).
+
 %%% Tokenizer
 
 -define(IS_LATIN_UPPER(C), (((C) >= $A) and ((C) =< $Z))).
@@ -927,6 +951,19 @@ wrap_error(Reason, _) -> Reason.
     when FATE   :: gmb_fate_data:fate_type(),
          Sophia :: string().
 
+%% @doc
+%% Print a FATE term from gmbytecode in Sophia syntax
+%% FATE terms usually come from using gmbytecode to decode the result of an
+%% on-chain transaction.
+%%
+%% This function does not use any type information to interpret the data, and
+%% so can make mistakes. It's okay for interpreting tuples, lists, maps,
+%% integers, and strings, but it will misinterpret the types of records and
+%% unicode characters, and will crash the process if variants are encountered.
+%%
+%% fate_to_list/2 should be used whenever possible, especially since
+%% transaction results are type checked by nodes at runtime.
+
 fate_to_list(Term) ->
     fate_to_list(unknown_type(), Term).
 
@@ -935,10 +972,27 @@ fate_to_list(Term) ->
          FATE   :: gmb_fate_data:fate_type(),
          Sophia :: string().
 
+
+%% @doc
+%% Print a FATE term from gmbytecode in Sophia syntax
+%% Like fate_to_list/1, but now type information from the AACI data structure
+%% can be provided, in order to correctly interpret types like records,
+%% variants, and unicode characters.  If the type information you provide is
+%% incorrect for the FATE term provided, then the function will fall back to
+%% untyped pretty printing like in fate_to_list/1, but this is not recommended,
+%% as correct type information should always be available.
+
 fate_to_list(Type, Term) ->
     IOList = fate_to_iolist(Type, Term),
     unicode:characters_to_list(IOList).
 
+%% @doc
+%% Print a FATE term in Sophia syntax, without concatenating
+%% The fate_to_list/1 function builds an iolist, and then concatenates it into
+%% a list. If you are going to put the term into a bigger iolist directly
+%% after, or write it to a streaming device, then it can save effort and memory
+%% to just use the iolist directly.
+
 -spec fate_to_iolist(FATE) -> Sophia
     when FATE   :: gmb_fate_data:fate_type(),
          Sophia :: iolist().
@@ -951,6 +1005,11 @@ fate_to_iolist(Term) ->
          FATE   :: gmb_fate_data:fate_type(),
          Sophia :: iolist().
 
+%% @doc
+%% Print a FATE term in Sophia syntax, without concatenating
+%% Prints using type information, like fate_to_list/2, but without spending
+%% time or memory concatenating the result into a list, like fate_to_iolist/1.
+
 % Special case for singleton records, since they are erased during compilation.
 fate_to_iolist({_, _, {record, [{FieldName, FieldType}]}}, Term) ->
     singleton_record_to_iolist(FieldName, FieldType, Term);

From 9a7a2a98c497b69de18cf58f22a0f0933d422f96 Mon Sep 17 00:00:00 2001
From: "dimitar.p.ivanov" 
Date: Fri, 22 May 2026 16:54:16 +0900
Subject: [PATCH 9/9] General polish (#28)

Co-authored-by: Craig Everett 
Co-authored-by: Dimitar Ivanov 
Reviewed-on: https://git.qpq.swiss/QPQ-AG/hakuzaru/pulls/28
---
 src/hz.erl | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/hz.erl b/src/hz.erl
index 0e2271b..9200e89 100644
--- a/src/hz.erl
+++ b/src/hz.erl
@@ -788,9 +788,9 @@ contract_code(ID) ->
          Result :: {ok,      Source}
                  | {project, Bundle}
                  | {error,   Reason},
-         Source :: string(),
+         Source :: binary(),
          Bundle :: [{FilePath :: string(), Contents :: binary()}],
-         Reason   :: chain_error() | string().
+         Reason :: chain_error() | string().
 %% @doc
 %% Retrieve the code of a contract as represented on chain.
 
@@ -809,6 +809,8 @@ extract(Blobby) ->
 
 extract2(TarBaby) ->
     case erl_tar:extract({binary, TarBaby}, [memory, compressed]) of
+        {ok, [{_File, Source}]} ->
+            {ok, Source};
         {ok, Bundle} ->
             {project, Bundle};
         Error ->
@@ -895,6 +897,11 @@ request(Path) ->
     hz_man:request(unicode:characters_to_list(Path)).
 
 
+-spec request(Path, Payload) -> {ok, Value} | {error, Reason}
+    when Path    :: unicode:charlist(),
+         Payload :: unicode:charlist(),
+         Value   :: map(),
+         Reason  :: hz:chain_error().
 request(Path, Payload) ->
     hz_man:request(unicode:characters_to_list(Path), Payload).
 
@@ -930,7 +937,7 @@ contract_create(CreatorID, Path, InitArgs) ->
             Gas = 500000,
             GasPrice = min_gas_price(),
             contract_create(CreatorID, Nonce,
-                            Amount, TTL, Gas, GasPrice,
+                            Gas, GasPrice, Amount, TTL,
                             Path, InitArgs);
         Error ->
             Error