54 Commits

Author SHA1 Message Date
zxq9 75bc52ede3 Doc formatting adjustments 2026-05-27 21:49:30 +09:00
zxq9 29619f08b7 Remove stdout line 2026-05-27 16:50:23 +09:00
zxq9 af46223163 Minor fixes 2026-05-27 16:41:45 +09:00
zxq9 9cafdd2b0f Merge pull request 'Improve specs' (#31) from improve_specs into master
Reviewed-on: #31
2026-05-26 15:38:26 +09:00
zxq9 6d429aa6a4 Merge branch 'master' into improve_specs 2026-05-26 15:38:08 +09:00
zxq9 fcf85077b2 Minor 2026-05-26 15:36:16 +09:00
zxq9 3585dbe534 Merge pull request 'Doc update for hz_sophia and hz_aaci and some minor fixes' (#30) from spivee/docs into master
Reviewed-on: #30
2026-05-26 09:44:53 +09:00
dimitar.p.ivanov 9a7a2a98c4 General polish (#28)
Co-authored-by: Craig Everett <zxq9@zxq9.com>
Co-authored-by: Dimitar Ivanov <dimitar.p.ivanov@gmail.com>
Reviewed-on: #28
2026-05-22 16:54:16 +09:00
dimitar.p.ivanov 4530fd2e93 Merge pull request 'Fix typespec' (#29) from respec into improve_specs
Reviewed-on: #29
2026-05-22 15:57:11 +09:00
zxq9 2a7079129f Fix typespec
Source needs to be defined as a binary.
2026-05-21 17:07:57 +09:00
Dimitar Ivanov 88aeb39d4a Fix a contract create bug 2026-05-20 17:02:36 +03:00
Jarvis Carroll 9fc89c0c22 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.
2026-05-19 12:29:30 +00:00
Jarvis Carroll 23c13f607e 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
2026-05-19 12:29:30 +00:00
Jarvis Carroll 8bc79d3b3f 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
2026-05-19 12:29:30 +00:00
Jarvis Carroll 3fae9a2edd Document hz_aaci types 2026-05-19 12:29:30 +00:00
zxq9 a3b19747b6 Adjust error condition 2026-05-18 12:56:31 +09:00
zxq9 f8e9333b4b Doc update 2026-05-14 11:01:37 +09:00
zxq9 eaccd50764 Fill in the holes in hz.erl docs and make hz_fetcher.erl 2026-05-13 19:48:49 +09:00
zxq9 9fd8dbd1a6 Merge branch 'master' into docs 2026-05-13 19:25:59 +09:00
Dimitar Ivanov f0f86ed36d Improve specs 2026-05-13 10:04:23 +03:00
Jarvis Carroll ed252b4c06 Also note index in record_element
I changed it from noting the index to just noting the field name, but
actually both pieces of information are important, since if there was
a type error, presumably the type information is actually wrong.

Now we put the index first, since that is the part of the FATE tuple
that failed, and then the field name that that would be if the type
information were correct, in case that is useful.
2026-05-12 06:07:58 +00:00
Jarvis Carroll 5dcc05d56a Change fate_to_erlang warning
This warning always confuses me. Usually it is a case I haven't actually implemented,
but I don't need the program to diagnose that for me, I need the program to tell me what the
type was, so that I can work out why it thinks it isn't implemented.

All three terms of the annotated type are relevant, but the annotated version can only differ from the normalized version if
it is a record or variant definition, so we special case those two just to communicate that the fact that it is *some* kind of record
did successfully pass through to the coerce logic, and otherwise we just try and print the opaque and normalized types faithfully.
2026-05-12 06:00:26 +00:00
Jarvis Carroll 2eca3a5338 Handle singleton records in erlang_to_fate
I realized this case needed special handling in hz_sophia, but didn't
get around to covering it properly in the older hz_aaci analogues.

While I was at it, I went and improved the error paths for record elements.
2026-05-12 04:23:21 +00:00
zxq9 e595991616 Correct keyblock() type 2026-05-10 21:02:23 +09:00
zxq9 da92d80334 Merge pull request 'Fix docs' (#24) from contract-spend into master
Reviewed-on: #24
2026-05-10 19:45:08 +09:00
zxq9 f821d57c1c Fix docs 2026-05-10 19:35:20 +09:00
zxq9 e87be689a8 Merge pull request 'Allow spending to contract addresses' (#23) from contract-spend into master
Reviewed-on: #23
2026-05-10 17:57:00 +09:00
zxq9 2a7de4fee1 verup 2026-05-10 17:56:34 +09:00
zxq9 82d08da8ca Allow spending to contract addresses 2026-05-10 16:14:00 +09:00
zxq9 85d0c6fd04 Doc fix 2026-05-10 15:52:39 +09:00
zxq9 d8221e0b25 Doc fix
#20
2026-05-10 15:49:11 +09:00
zxq9 b950bb8a67 Merge pull request 'Make Hakuzaru Great Again' (#22) from parser into master
Reviewed-on: #22
2026-05-10 15:26:44 +09:00
zxq9 a4914c1ad1 Making custom dir. 2026-05-10 15:22:13 +09:00
zxq9 9e6d9ec02e Merge branch 'master' into parser 2026-05-10 15:14:57 +09:00
zxq9 4b9fa65672 Merge down 2026-05-10 15:09:47 +09:00
zxq9 74aaad297a Merge branch 'master' into parser 2026-05-10 15:09:13 +09:00
zxq9 c9ead44aa2 Let non-zx projects call contract_create* 2026-05-10 15:01:50 +09:00
zxq9 c54c0db17a Fix list -> binary arg 2026-05-10 13:16:13 +09:00
zxq9 cd4f6a56a5 Differentiate between source return types 2026-05-09 20:07:52 +09:00
zxq9 fd2158a465 base64 -> bytearray encoding 2026-05-09 15:19:51 +09:00
zxq9 7fc3cd00da WIP 2026-05-08 23:04:56 +09:00
zxq9 02945dd10d derp 2026-05-08 19:47:25 +09:00
zxq9 695e7e4828 WIP 2026-05-08 15:48:05 +09:00
zxq9 9f02f73dbd verup 2026-05-08 08:43:07 +09:00
zxq9 fd8766a249 Unify call arg order between call and create 2026-05-07 19:53:36 +09:00
Jarvis Carroll 540b2c513b Fill AACI and coerce type specs
Any error reasons or paths are just term() still, and ACI doesn't have a defined spec in the compiler, so whatever, but the AACI types, the erlang representation of terms, and the four different kinds of coerce function are all spec'd now.

Also some internal type substitution functions were given types, just in the hopes of catching some errors, but dyalizer doesn't seem to complain at all no matter how badly I break my code. Strange approach to making a type system, but oh well.
2026-02-26 12:57:49 +00:00
zxq9 bda4e89e58 Merge branch 'parser' of ssh://git.qpq.swiss:21203/QPQ-AG/hakuzaru into parser 2026-02-25 16:21:23 +09:00
zxq9 f277e79096 Minor doc and style edits 2026-02-25 16:20:52 +09:00
Jarvis Carroll ddec3bfa74 add more format options to decode_bytearray
I reversed the argument order here, since the Format option is sort of kind of almost optional, but I am not sure if that was a good idea.
2026-02-24 06:12:00 +00:00
Jarvis Carroll a0fbeebcdb Pretty print Sophia expressions.
I think all of the tests roundtrip now, so if my parser was thorough, the pretty printer should be as thorough.
2026-02-17 07:26:42 +00:00
zxq9 48bcccdf23 Merge pull request 'Fix hz_grids:req/2,3 and add req/4' (#18) from grids-fix into master
Reviewed-on: #18
2026-01-19 13:26:50 +09:00
zxq9 03b9756066 Fix hz_grids:req/2,3 and add req/4 2026-01-19 12:53:25 +09:00
zxq9 d65a048409 Updating eDoc style 2026-01-08 22:14:09 +09:00
zxq9 9280495b18 Adjusting eDoc output 2026-01-08 22:12:43 +09:00
16 changed files with 1335 additions and 246 deletions
+2 -2
View File
@@ -8,9 +8,9 @@ cancer
erl_crash.dump
ebin/*.beam
doc/*.html
doc/*.css
doc/edoc-info
doc/erlang.png
doc/stylesheet.css
doc/edoc-info
rel/example_project
.concrete/DEV_MODE
.rebar
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

+75
View File
@@ -0,0 +1,75 @@
/* standard EDoc style sheet */
body {
font-family: Verdana, Arial, Helvetica, sans-serif;
margin-left: .25in;
margin-right: .2in;
margin-top: 0.2in;
margin-bottom: 0.2in;
color: #696969;
background-color: #ffffff;
}
a:link{
color: #000000;
}
a:visited{
color: #000000;
}
a:hover{
color: #d8613c;
}
h1,h2 {
margin-left: -0.2in;
}
div.navbar {
background-color: #000000;
padding: 0.2em;
}
h2.indextitle {
padding: 0.4em;
color: #dfdfdf;
background-color: #000000;
}
div.navbar a:link {
color: #dfdfdf;
}
div.navbar a:visited {
color: #dfdfdf;
}
div.navbar a:hover {
color: #d8613c;
}
h3.function,h3.typedecl {
background-color: #000000;
color: #dfdfdf;
padding-left: 1em;
}
div.spec {
margin-left: 2em;
background-color: #eeeeee;
}
a.module {
text-decoration:none
}
a.module:hover {
background-color: #eeeeee;
}
ul.definitions {
list-style-type: none;
}
ul.index {
list-style-type: none;
background-color: #eeeeee;
}
/*
* Minor style tweaks
*/
ul {
list-style-type: square;
}
table {
border-collapse: collapse;
}
td {
padding: 3
}
+4 -3
View File
@@ -1,5 +1,6 @@
@author Craig Everett <craigeverett@qpq.swiss> [https://git.qpq.swiss/QPQ-AG/hakuzaru]
@version 0.8.0
@author Craig Everett <craigeverett@qpq.swiss> [https://zxq9.com]
@author Jarvis Carrol <jarviscarrol@qpq.swiss> [https://jarviscarroll.net/]
@version 0.9.2
@title Hakuzaru: Gajumaru blockchain bindings for Erlang
@doc
@@ -21,7 +22,7 @@ After startup `hz_man' must be given the address and port of a list of Gajumaru
Note that the service nodes will need to have the dry-run endpoint enabled and the internal service query port made available in order to provide dry-runs and transaction submission.
When configuring chain nodes a list of nodes should be provided.
To avoid sync issues in the case of fast transaction formation/submission to the chain, only one node from the list of chain nodes is used for submitting transactions and querying `next_nonce/1`.
To avoid sync issues in the case of fast transaction formation/submission to the chain, only one node from the list of chain nodes is used for submitting transactions and querying `next_nonce/1'.
This node is called "the sticky node".
The first node in the list of chain nodes provided during configuration is designated as the sticky node.
+3 -3
View File
@@ -3,7 +3,7 @@
{included_applications,[]},
{applications,[stdlib,kernel]},
{description,"Gajumaru interoperation library"},
{vsn,"0.8.2"},
{modules,[hakuzaru,hz,hz_fetcher,hz_format,hz_grids,
hz_key_master,hz_man,hz_sup]},
{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,[]}}]}.
+1 -1
View File
@@ -6,7 +6,7 @@
%%% @end
-module(hakuzaru).
-vsn("0.8.2").
-vsn("0.9.2").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
+219 -92
View File
@@ -23,7 +23,7 @@
%%% @end
-module(hz).
-vsn("0.8.2").
-vsn("0.9.2").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
@@ -45,7 +45,7 @@
acc/1, acc_at_height/2, acc_at_block_id/2,
acc_pending_txs/1,
next_nonce/1,
dry_run/1, dry_run/2, dry_run/3, dry_run_map/1,
dry_run/1, dry_run/2, dry_run/3, % dry_run_map/1,
tx/1, tx_info/1,
post_tx/1,
contract/1, contract_code/1, contract_source/1,
@@ -71,7 +71,7 @@
contract_call/5,
contract_call/6,
contract_call/10,
decode_bytearray_fate/1, decode_bytearray/2,
decode_bytearray/2,
spend/5, spend/10,
sign_tx/2, sign_tx/3,
sign_message/2, verify_signature/3,
@@ -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}.
% </pre>
-type microblock_header() :: #{string() => term()}.
% <pre>
@@ -272,8 +273,7 @@ chain_nodes() ->
%% transactions are submitted is called the "sticky node". This is the first node
%% (head position) in the list of nodes submitted to the chain when `chain_nodes/1'
%% is called. If using multiple nodes but the sticky node should also be used for
%% read-only queries, submit the sticky node at the head of the list and again in
%% the tail.
%% read-only queries, put the sticky node in the list twice.
chain_nodes(List) when is_list(List) ->
hz_man:chain_nodes(List).
@@ -284,7 +284,7 @@ chain_nodes(List) when is_list(List) ->
%% Check whether TLS is in use. The typical situation is to not use TLS as nodes that
%% serve as part of the backend of an application are typically run in the same
%% backend network as the application service. When accessing chain nodes over the WAN
%% however, TLS is strongly recommended to avoid a MITM attack.
%% however, TLS is recommended to avoid a MitM attack.
%%
%% In this version of Hakuzaru TLS is either on or off for all nodes, making a mixed
%% infrastructure complicated to support without two Hakuzaru instances. This will
@@ -299,7 +299,7 @@ tls() ->
-spec tls(boolean()) -> ok.
%% @doc
%% Set TLS true or false. That's what a boolean is, by the way, `true' or `false'.
%% This is a condescending comment. That means I am talking down to you.
%% This is a condescending comment. That means to talk down to someone.
%%
%% TLS defaults to `false'.
@@ -343,7 +343,8 @@ timeout(MS) ->
%% NOTE:
%% This will return the currently synced height, which may be different than the
%% actual current top of the entire chain if the node being queried is still syncing
%% (has not yet caught up with the chain).
%% (has not yet caught up with the chain). More complete information, including
%% whether the node is currently syncing, can be gained from a `status()' query.
top_height() ->
case top_block() of
@@ -353,10 +354,10 @@ top_height() ->
-spec top_block() -> {ok, TopBlock} | {error, Reason}
when TopBlock :: microblock_header(),
when TopBlock :: microblock_header() | keyblock(),
Reason :: chain_error().
%% @doc
%% Returns the current block height as an integer.
%% Returns the header of the current top block.
top_block() ->
request("/v3/headers/top").
@@ -386,7 +387,7 @@ kb_current() ->
kb_current_hash() ->
case request("/v3/key-blocks/current/hash") of
{ok, #{"reason" := Reason}} -> {error, Reason};
{ok, #{"hash" := Hash}} -> {ok, Hash};
{ok, #{"hash" := Hash}} -> {ok, Hash};
Error -> Error
end.
@@ -444,10 +445,6 @@ kb_by_height(Height) ->
result(request(["/v3/key-blocks/height/", StringN])).
%kb_insert(KeyblockData) ->
% request("/v3/key-blocks", KeyblockData).
-spec mb_header(ID) -> {ok, MB_Header} | {error, Reason}
when ID :: microblock_hash(),
MB_Header :: microblock_header(),
@@ -607,12 +604,6 @@ next_nonce(AccountID) ->
{ok, #{"reason" := Reason}} -> {error, Reason};
Error -> Error
end.
% case request_sticky(["/v3/accounts/", AccountID]) of
% {ok, #{"nonce" := Nonce}} -> {ok, Nonce + 1};
% {ok, #{"reason" := "Account not found"}} -> {ok, 1};
% {ok, #{"reason" := Reason}} -> {error, Reason};
% Error -> Error
% end.
-spec dry_run(TX) -> {ok, Result} | {error, Reason}
@@ -671,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}
@@ -691,8 +683,10 @@ decode_bytearray_fate(EncodedStr) ->
Encoded = unicode:characters_to_binary(EncodedStr),
{contract_bytearray, Binary} = gmser_api_encoder:decode(Encoded),
case Binary of
<<>> -> {ok, none};
<<"Out of gas">> -> {error, out_of_gas};
<<>> ->
{ok, none};
<<"Out of gas">> ->
{error, out_of_gas};
_ ->
% FIXME there may be other errors that are encoded directly into
% the byte array. We could try and catch to at least return
@@ -701,8 +695,9 @@ decode_bytearray_fate(EncodedStr) ->
{ok, Object}
end.
-spec decode_bytearray(Type, EncodedStr) -> {ok, Result} | {error, Reason}
when Type :: term(),
-spec decode_bytearray(EncodedStr, Format) -> {ok, Result} | {error, Reason}
when Format :: fate | sophia | {sophia, Type} | {erlang, Type},
Type :: term(),
EncodedStr :: binary() | string(),
Result :: none | term(),
Reason :: term().
@@ -713,13 +708,18 @@ decode_bytearray_fate(EncodedStr) ->
%% must be the result type of the same function in the same AACI that was used
%% to create the transaction that EncodedStr came from.
decode_bytearray(Type, EncodedStr) ->
decode_bytearray(EncodedStr, Format) ->
case decode_bytearray_fate(EncodedStr) of
{ok, none} -> {ok, none};
{ok, Object} -> hz_aaci:fate_to_erlang(Type, Object);
{ok, FATE} -> decode_bytearray2(FATE, Format);
{error, Reason} -> {error, Reason}
end.
decode_bytearray2(FATE, fate) -> FATE;
decode_bytearray2(FATE, sophia) -> hz_sophia:fate_to_list(FATE);
decode_bytearray2(FATE, {sophia, Type}) -> hz_sophia:fate_to_list(Type, FATE);
decode_bytearray2(FATE, {erlang, Type}) -> hz_aaci:fate_to_erlang(Type, FATE).
to_binary(S) when is_binary(S) -> S;
to_binary(S) when is_list(S) -> list_to_binary(S).
@@ -785,20 +785,43 @@ contract_code(ID) ->
end.
-spec contract_source(ID) -> {ok, Bytecode} | {error, Reason}
when ID :: contract_id(),
Bytecode :: contract_byte_array(),
Reason :: chain_error() | string().
-spec contract_source(ID) -> Result
when ID :: contract_id(),
Result :: {ok, Source}
| {project, Bundle}
| {error, Reason},
Source :: binary(),
Bundle :: [{FilePath :: string(), Contents :: binary()}],
Reason :: chain_error() | string().
%% @doc
%% Retrieve the code of a contract as represented on chain.
contract_source(ID) ->
case request(["/v3/contracts/", ID, "/source"]) of
{ok, #{"source" := Source}} -> {ok, Source};
{ok, #{"source" := Blobby}} -> extract(list_to_binary(Blobby));
{ok, #{"reason" := Reason}} -> {error, Reason};
Error -> Error
end.
extract(Blobby) ->
case gmser_api_encoder:safe_decode(bytearray, Blobby) of
{ok, TarBaby} -> extract2(TarBaby);
{error, invalid_encoding} -> {ok, Blobby}
end.
extract2(TarBaby) ->
case erl_tar:extract({binary, TarBaby}, [memory, compressed]) of
{ok, [{_, Source}]} ->
{ok, Source};
{ok, Bundle} ->
{project, Bundle};
{error,invalid_tar_checksum} ->
{ok, TarBaby};
Error ->
ok = io:format("erl_tar:extract/2 error: ~tp~n", [Error]),
{ok, TarBaby}
end.
-spec contract_poi(ID) -> {ok, Bytecode} | {error, Reason}
when ID :: contract_id(),
@@ -878,6 +901,12 @@ 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).
@@ -902,7 +931,7 @@ result(Received) -> Received.
%% @doc
%% This function reads the source of a Sophia contract (an .aes file)
%% and returns the unsigned create contract call data with default values.
%% For more control over exactly what those values are, use create_contract/8.
%% For more control over exactly what those values are, use contract_create/8.
contract_create(CreatorID, Path, InitArgs) ->
case next_nonce(CreatorID) of
@@ -913,7 +942,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
@@ -921,14 +950,14 @@ contract_create(CreatorID, Path, InitArgs) ->
-spec contract_create(CreatorID, Nonce,
Amount, TTL, Gas, GasPrice,
Gas, GasPrice, Amount, TTL,
Path, InitArgs) -> Result
when CreatorID :: pubkey(),
Nonce :: pos_integer(),
Amount :: non_neg_integer(),
TTL :: non_neg_integer(),
Gas :: pos_integer(),
GasPrice :: pos_integer(),
Amount :: non_neg_integer(),
TTL :: non_neg_integer(),
Path :: file:filename(),
InitArgs :: [string()]
| {erlang, [term()]}
@@ -965,24 +994,6 @@ contract_create(CreatorID, Path, InitArgs) ->
%% querying your Gajumaru node (via `hz:next_nonce(CallerID)', for example).
%% </li>
%% <li>
%% <b>Amount:</b>
%% All Gajumaru transactions can carry an "amount" spent from the origin account
%% (in this case the `CallerID') to the destination. In a "Spend" transaction this
%% is the only value that really matters, but in a contract call the utility is
%% quite different, as you can pay money <em>into</em> a contract and have that
%% contract hold it (for future payouts, to be held in escrow, as proof of intent
%% to purchase or engage in an auction, whatever). Typically this value is 0, but
%% of course there are very good reasons why it should be set to a non-zero value
%% in the case of calls related to contract-governed payment systems.
%% </li>
%% <li>
%% <b>TTL:</b>
%% This stands for "Time-To-Live", meaning the height beyond which this element is
%% considered to be eligible for garbage collection (and therefore inaccessible!).
%% The TTL can be extended by a "live extension" transaction (basically pay for the
%% data to remain alive longer).
%% </li>
%% <li>
%% <b>Gas:</b>
%% This number sets a limit on the maximum amount of computation the caller is willing
%% to pay for on the chain.
@@ -1015,6 +1026,24 @@ contract_create(CreatorID, Path, InitArgs) ->
%% transaction, thus making miners more likely to prioritize the high value ones.
%% </li>
%% <li>
%% <b>Amount:</b>
%% All Gajumaru transactions can carry an "amount" spent from the origin account
%% (in this case the `CallerID') to the destination. In a "Spend" transaction this
%% is the only value that really matters, but in a contract call the utility is
%% quite different, as you can pay money <em>into</em> a contract and have that
%% contract hold it (for future payouts, to be held in escrow, as proof of intent
%% to purchase or engage in an auction, whatever). Typically this value is 0, but
%% of course there are very good reasons why it should be set to a non-zero value
%% in the case of calls related to contract-governed payment systems.
%% </li>
%% <li>
%% <b>TTL:</b>
%% This stands for "Time-To-Live", meaning the height beyond which this element is
%% considered to be eligible for garbage collection (and therefore inaccessible!).
%% The TTL can be extended by a "live extension" transaction (basically pay for the
%% data to remain alive longer).
%% </li>
%% <li>
%% <b>ACI:</b>
%% This is the compiled contract's metadata. It provides the information necessary
%% for the contract call data to be formed in a way that the Gajumaru runtime will
@@ -1038,8 +1067,9 @@ contract_create(CreatorID, Path, InitArgs) ->
%% <li>
%% <b>Args:</b>
%% This is a list of the arguments to provide to the function, listed in order
%% according to the function's spec, and represented as strings (that is, an integer
%% argument of `10' must be cast to the textual representation `"10"').
%% according to the function's spec. Arguments can be represented as a list of
%% Sophia literals (a simple list of strings), or alternately as a list of compatible
%% Erlang, FATE or Sophia terms wrapped in a tuple which specifies the representation.
%% </li>
%% </ul>
%% As should be obvious from the above description, it is pretty helpful to have a
@@ -1047,9 +1077,10 @@ contract_create(CreatorID, Path, InitArgs) ->
%% if you do not already have a copy, and can check the spec of a function before
%% trying to form a contract call.
contract_create(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Path, InitArgs) ->
contract_create(CreatorID, Nonce, Gas, GasPrice, Amount, TTL, Path, InitArgs) ->
case file:read_file(Path) of
{ok, Source} ->
Name = filename:basename(Path),
Dir = filename:dirname(Path),
{ok, CWD} = file:get_cwd(),
SrcDir = so_utils:canonical_dir(Path),
@@ -1058,18 +1089,19 @@ contract_create(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Path, InitArgs) ->
{src_file, Path},
{src_dir, SrcDir},
{include, {file_system, [CWD, so_utils:canonical_dir(Dir)]}}],
contract_create2(CreatorID, Nonce, Amount, TTL, Gas, GasPrice,
Source, Options, InitArgs);
contract_create2(CreatorID, Nonce, Gas, GasPrice, Amount, TTL,
Name, Source, Options, InitArgs);
Error ->
Error
end.
contract_create2(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Source, Options, InitArgs) ->
contract_create2(CreatorID, Nonce, Gas, GasPrice, Amount, TTL, Name, Source, Options, InitArgs) ->
case so_compiler:from_string(Source, Options) of
{ok, Compiled} ->
contract_create_built(CreatorID, Nonce, Amount, TTL, Gas, GasPrice,
Compiled, InitArgs);
Named = maps:put(contract_name, Name, Compiled),
contract_create_built(CreatorID, Nonce, Gas, GasPrice, Amount, TTL,
Named, InitArgs);
Error ->
Error
end.
@@ -1088,7 +1120,7 @@ contract_create2(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Source, Options,
%% @doc
%% This function takes the compiler output (instead of starting from source),
%% and returns the unsigned create contract call data with default values.
%% For more control over exactly what those values are, use create_contract/8.
%% For more control over exactly what those values are, use contract_create/8.
contract_create_built(CreatorID, Compiled, InitArgs) ->
case next_nonce(CreatorID) of
@@ -1099,20 +1131,20 @@ contract_create_built(CreatorID, Compiled, InitArgs) ->
Gas = 500000,
GasPrice = min_gas_price(),
contract_create_built(CreatorID, Nonce,
Amount, TTL, Gas, GasPrice,
Gas, GasPrice, Amount, TTL,
Compiled, InitArgs);
Error ->
Error
end.
-spec contract_create_built(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, InitArgs) -> Result
-spec contract_create_built(CreatorID, Nonce, Gas, GasPrice, Amount, TTL, Compiled, InitArgs) -> Result
when CreatorID :: unicode:chardata(),
Nonce :: pos_integer(),
Amount :: non_neg_integer(),
TTL :: non_neg_integer(),
Gas :: pos_integer(),
GasPrice :: pos_integer(),
Amount :: non_neg_integer(),
TTL :: non_neg_integer(),
Compiled :: map(),
InitArgs :: [string()]
| {erlang, [term()]}
@@ -1126,28 +1158,29 @@ contract_create_built(CreatorID, Compiled, InitArgs) ->
%% The `Compiled' argument is the output of contract compilation and replaces the `File'
%% argument in `contract_create/8'.
contract_create_built(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, InitArgs) ->
contract_create_built(CreatorID, Nonce, Gas, GasPrice, Amount, TTL, Compiled, InitArgs) ->
AACI = hz_aaci:prepare(maps:get(aci, Compiled)),
case encode_call_data(AACI, "init", InitArgs) of
{ok, CallData} ->
assemble_calldata(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData);
assemble_calldata(CreatorID, Nonce, Gas, GasPrice, Amount, TTL, Compiled, CallData);
Error ->
Error
end.
assemble_calldata(CreatorID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) ->
assemble_calldata(CreatorID, Nonce, Gas, GasPrice, Amount, TTL, Compiled, CallData) ->
PK = unicode:characters_to_binary(CreatorID),
try
{account_pubkey, OwnerID} = gmser_api_encoder:decode(PK),
assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData)
assemble_calldata2(OwnerID, Nonce, Gas, GasPrice, Amount, TTL, Compiled, CallData)
catch
Error:Reason -> {Error, Reason}
Error:Reason:Stack ->
{Error, {Reason, Stack}}
end.
assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallData) ->
Code = gmser_contract_code:serialize(Compiled),
Source = unicode:characters_to_binary(maps:get(contract_source, Compiled, <<>>)),
assemble_calldata2(OwnerID, Nonce, Gas, GasPrice, Amount, TTL, Compiled, CallData) ->
Compressed = #{contract_source := Bundle} = bundle_source(Compiled),
Code = gmser_contract_code:serialize(Compressed),
VM = 1,
ABI = 1,
<<CTVersion:32>> = <<VM:16, ABI:16>>,
@@ -1157,7 +1190,7 @@ assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallDat
[{owner_id, gmser_id:create(account, OwnerID)},
{nonce, Nonce},
{code, Code},
{source, Source},
{source, Bundle},
{ct_version, CTVersion},
{ttl, TTL},
{deposit, 0},
@@ -1184,6 +1217,43 @@ assemble_calldata2(OwnerID, Nonce, Amount, TTL, Gas, GasPrice, Compiled, CallDat
error:Reason -> {error, Reason}
end.
bundle_source(Compiled) ->
case maps:find(contract_source, Compiled) of
{ok, Source} -> bundle_source2(unicode:characters_to_binary(Source), Compiled);
error -> <<>>
end.
bundle_source2(Source, Compiled) ->
File = unicode:characters_to_list(maps:get(contract_name, Compiled, "contract.aes")),
TempDir = temp_dir(),
TgzName = File ++ ".tgz",
TarGzPath = filename:join(TempDir, TgzName),
ok = filelib:ensure_dir(TarGzPath),
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(TempDir),
ok = erl_tar:create(TarGzPath, [{File, Source}], [compressed]),
{ok, TgzBin} = file:read_file(TarGzPath),
ok = file:set_cwd(CWD),
ok = file:del_dir_r(TempDir),
{ok, Hash} = eblake2:blake2b(32, TgzBin),
Compiled#{contract_source => TgzBin, source_hash => Hash}.
temp_dir() ->
case erlang:function_exported(zx_lib, path, 3) of
true ->
TS = integer_to_list(erlang:system_time()),
filename:join(zx_lib:path(tmp, "otpr", "hakuzaru"), TS);
false ->
temp_dir(os:type())
end.
temp_dir({unix, _}) ->
string:trim(os:cmd("mktemp -d"));
temp_dir({win32, _}) ->
Temp = os:getenv("TEMP"),
TS = integer_to_list(erlang:system_time()),
filename:join([Temp, "hakuzaru", TS]).
-spec read_aci(Path) -> Result
when Path :: file:filename(),
@@ -1399,8 +1469,9 @@ contract_call(CallerID, Gas, AACI, ConID, Fun, Args) ->
%% <li>
%% <b>Args:</b>
%% This is a list of the arguments to provide to the function, listed in order
%% according to the function's spec, and represented as strings (that is, an integer
%% argument of `10' must be cast to the textual representation `"10"').
%% according to the function's spec. Arguments can be represented as a list of
%% Sophia literals (a simple list of strings), or alternately as a list of compatible
%% Erlang, FATE or Sophia terms wrapped in a tuple which specifies the representation.
%% </li>
%% </ul>
%% As should be obvious from the above description, it is pretty helpful to have a
@@ -1583,6 +1654,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
@@ -1590,6 +1669,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),
@@ -1609,10 +1697,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.
@@ -1639,6 +1738,23 @@ 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(),
NetworkID :: unicode:chardata(),
Result :: term(), % FIXME
Reason :: chain_error() | string().
%% @doc
%% Forms a spend transaction and submits it to the chain.
spend(SenderID,
SecKey,
RecipientID,
@@ -1649,7 +1765,7 @@ spend(SenderID,
Nonce,
Payload,
NetworkID) ->
case decode_account_id(unicode:characters_to_binary(SenderID)) of
case gmser_api_encoder:safe_decode(account_pubkey, unicode:characters_to_binary(SenderID)) of
{ok, DSenderID} ->
spend2(gmser_id:create(account, DSenderID),
SecKey,
@@ -1693,11 +1809,10 @@ spend2(DSenderID,
decode_account_id(B) ->
try
{account_pubkey, PK} = gmser_api_encoder:decode(B),
{ok, PK}
catch
E:R -> {E, R}
case gmser_api_encoder:safe_decode(account_pubkey, B) of
{ok, PK} -> {ok, PK};
{error, invalid_prefix} -> gmser_api_encoder:safe_decode(contract_pubkey, B);
Error -> Error
end.
@@ -1752,6 +1867,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(),
@@ -1830,6 +1949,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(),
@@ -1844,6 +1969,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
+553 -65
View File
@@ -10,7 +10,7 @@
%%% @end
-module(hz_aaci).
-vsn("0.8.2").
-vsn("0.9.2").
-author("Jarvis Carroll <spiveehere@gmail.com>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
@@ -25,11 +25,349 @@
%%% Types
-export_type([aaci/0]).
-export_type([aaci/0, annotated_type/0, erlang_repr/0]).
-include_lib("eunit/include/eunit.hrl").
-type aaci() :: {aaci, term(), term(), term()}.
-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().
% 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() :: integer()
%| string()
%| boolean()
%| binary()
%| tuple() % Tuples, variants, or raw addresses
%| [erlang_repr()]
%| #{erlang_repr() => erlang_repr()}.
-type erlang_repr_int() :: integer() | string().
% 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_address() :: unicode:chardata() | {raw, <<_:32*8>>}.
% 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_contract() :: unicode:chardata() | {raw, <<_:32*8>>}.
% 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_signature() :: unicode:chardata() | <<_:64*8>> | {raw, <<_:64*8>>}.
% 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_bool() :: true | false | string().
% 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_string() :: unicode:chardata().
% 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_char() :: integer() | unicode:chardata().
% 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_bytes() :: binary().
% 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_bits() :: bitstring().
% 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_list() :: [erlang_repr()].
% The Erlang representation of a Sophia `list(_)'
% Simply a list. Each element of the list is converted forwards/backwards as
% normal.
-type erlang_repr_map() :: #{erlang_repr() => erlang_repr()}.
% The Erlang representation of a Sophia `map(_, _)'
% Simply a map. Each key and value is converted forwards/backwards as normal.
-type erlang_repr_tuple() :: {}
| {erlang_repr(), erlang_repr()}
| tuple().
% 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_variant() :: string()
| {string()}
| {string(), erlang_repr()}
| tuple().
% 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_record() :: #{string() => erlang_repr()}.
% 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 aaci() :: {aaci, string(), #{string() => function_spec()}, #{string() => typedef()}}.
% 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:
% <ul>
% <li>The name of the contract,</li>
% <li>The 'annotated' entrypoint specs, designed for fast conversion to/from
% the representation used on-chain, see `function_spec/0',</li>
% <li>The 'opaque' type definitions, all the internal type aliases and
% definitions within the contract and its imported namespaces.</li>
% </ul>
-type function_spec() :: {[{string(), annotated_type()}], annotated_type()}.
% 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 annotated_type() :: {opaque_type(), already_normalized | opaque_type(), annotated_type_body()}.
% 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:
% <pre>
% 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)
% </pre>
% If these type aliases appeared in a function spec, the AACI would represent
% them as the following annotated types:
% <pre>
% {"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}}]}}
% </pre>
%
% 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 builtin_type(T) :: {bytes, [integer() | any]}
| {tuple, [T]}
| {list, [T]}
| {map, [T]}
| integer
| boolean
| bits
| char
| string
| address
| signature
| contract
| channel
| unknown_type.
% 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 user_defined_type(T) :: {record, [{string(), T}]}
| {variant, [{string(), [T]}]}.
% 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 opaque_type() :: string()
| {string(), [opaque_type()]}
| builtin_type(opaque_type()).
% 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 annotated_type_body() :: builtin_type(annotated_type())
| user_defined_type(annotated_type()).
% 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 typedef_expression() :: {var, string()}
| string()
| {string(), [typedef_expression()]}
| builtin_type(typedef_expression()).
% 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() :: {[string()], typedef_body()}.
% 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_body() :: typedef_expression()
| user_defined_type(typedef_expression()).
% 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.
%%% ACI/AACI
@@ -38,8 +376,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
@@ -47,6 +387,12 @@ prepare_from_file(Path) ->
Error -> Error
end.
-spec prepare(ACI) -> AACI
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),
@@ -63,9 +409,15 @@ prepare(ACI) ->
% make error messages easier to understand.
InternalTypeDefs = maps:merge(builtin_typedefs(), TypeDefs),
Specs = annotate_function_specs(OpaqueSpecs, InternalTypeDefs, #{}),
{aaci, Name, Specs, TypeDefs}.
-spec convert_aci_types(ACI) -> {Name, OpaqueSpecs, TypeDefs}
when ACI :: term(),
Name :: string(),
OpaqueSpecs :: [{string(), [{string(), opaque_type()}], opaque_type()}],
TypeDefs :: #{string() => typedef()}.
convert_aci_types(ACI) ->
% Find the main contract, so we can get the specifications of its
% entrypoints.
@@ -90,17 +442,20 @@ convert_aci_types(ACI) ->
% just pre-compute and acceleration.
{Name, Specs, TypeDefMap}.
convert_function_spec(#{name := NameBin, arguments := Args, returns := Result}) ->
Name = binary_to_list(NameBin),
ArgTypes = lists:map(fun convert_arg/1, Args),
ResultType = opaque_type([], Result),
{Name, ArgTypes, ResultType}.
convert_arg(#{name := NameBin, type := TypeDef}) ->
Name = binary_to_list(NameBin),
Type = opaque_type([], TypeDef),
{Name, Type}.
convert_namespace_typedefs(#{namespace := NS}) ->
Name = namespace_name(NS),
convert_typedefs(NS, Name);
@@ -134,6 +489,14 @@ convert_typedefs_loop([Next | Rest], NamePrefix, Converted) ->
Def = opaque_type(Params, DefACI),
convert_typedefs_loop(Rest, NamePrefix, [Converted, {Name, Params, Def}]).
-spec collect_opaque_types(Tree, TypeDefs) -> TypeDefs
when Tree :: typedef_tree(),
TypeDefs :: #{string() => typedef()}.
-type typedef_tree() :: {string(), [string()], typedef_body()} | list(typedef_tree()).
collect_opaque_types([], Types) ->
Types;
collect_opaque_types([L | R], Types) ->
@@ -142,10 +505,17 @@ collect_opaque_types([L | R], Types) ->
collect_opaque_types({Name, Params, Def}, Types) ->
maps:put(Name, {Params, Def}, Types).
%%% ACI Type -> Opaque Type
-spec opaque_type(Params, ACIType) -> Opaque
when Params :: [string()],
ACIType :: binary() | map(),
Opaque :: opaque_type().
% Convert an ACI type defintion/spec into the 'opaque type' representation that
% our dereferencing algorithms can reason about.
opaque_type(Params, NameBin) when is_binary(NameBin) ->
Name = opaque_type_name(NameBin),
case not is_atom(Name) and lists:member(Name, Params) of
@@ -171,8 +541,11 @@ opaque_type(Params, Pair) when is_map(Pair) ->
[{Name, TypeArgs}] = maps:to_list(Pair),
{opaque_type_name(Name), [opaque_type(Params, Arg) || Arg <- TypeArgs]}.
-spec opaque_type_name(binary()) -> atom() | string().
% Atoms for any builtins that aren't qualified by a namespace in Sophia.
% Everything else stays as a string, user-defined or not.
opaque_type_name(<<"int">>) -> integer;
opaque_type_name(<<"bool">>) -> boolean;
opaque_type_name(<<"bits">>) -> bits;
@@ -188,6 +561,7 @@ opaque_type_name(<<"map">>) -> map;
opaque_type_name(<<"channel">>) -> channel;
opaque_type_name(Name) -> binary_to_list(Name).
builtin_typedefs() ->
#{"unit" => {[], {tuple, []}},
"void" => {[], {variant, []}},
@@ -245,14 +619,15 @@ builtin_typedefs() ->
"MCL_BLS12_381.fp" => {[], {bytes, [48]}}
}.
%%% Opaque Type -> Accelerated 'Annotated' Type
% Type preparation has two goals. First, we need a data structure that can be
% traversed quickly, to take sophia-esque erlang expressions and turn them into
% fate-esque erlang expressions that gmbytecode can serialize. Second, we need
% partially substituted names, so that error messages can be generated for why
% "foobar" is not valid as the third field of a `bazquux`, because the third
% field is supposed to be `option(integer)`, not `string`.
% "foobar" is not valid as the third field of a `bazquux', because the third
% field is supposed to be `option(integer)', not `string'.
%
% To achieve this we need three representations of each type expression, which
% together form an 'annotated type'. First, we need the fully opaque name,
@@ -268,10 +643,20 @@ builtin_typedefs() ->
%
% In a lot of cases the opaque type given will already be normalized, in which
% case either the normalized field or the non-normalized field of an annotated
% type can simple be the atom `already_normalized`, which means error messages
% type can simple be the atom `already_normalized', which means error messages
% 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) ->
@@ -280,34 +665,30 @@ 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}
when Opaque :: opaque_type(),
Types :: #{string() => typedef()},
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)}.
@@ -318,37 +699,35 @@ 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)}.
% This function evaluates type aliases in a loop, until eventually a usable
% definition is found.
normalize_opaque_type(T, Types) -> normalize_opaque_type(T, Types, true).
@@ -443,8 +822,28 @@ substitute_opaque_types(Bindings, Types) ->
Each = fun(Type) -> substitute_opaque_type(Bindings, Type) end,
lists:map(Each, Types).
%%% Erlang to FATE
-spec erlang_args_to_fate(VarTypes, Terms) -> {ok, FATE} | {error, Errors}
when VarTypes :: [{string(), annotated_type()}],
Terms :: [erlang_repr()],
FATE :: gmb_fate_data:fate_type(),
Errors :: [{Reason, [PathStep]}],
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' 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),
@@ -454,6 +853,23 @@ erlang_args_to_fate(VarTypes, Terms) ->
DefLength < ArgLength -> {error, too_many_args}
end.
-spec erlang_to_fate(Type, Erlang) -> {ok, FATE} | {error, Errors}
when Type :: annotated_type(),
FATE :: gmb_fate_data:fate_type(),
Erlang :: erlang_repr(),
Errors :: [{Reason, [PathStep]}],
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) ->
@@ -720,6 +1136,10 @@ coerce_map_to_record(O, N, MemberTypes, Map) ->
case zip_record_fields(MemberTypes, Map) of
{ok, Zipped} ->
case coerce_zipped_bindings(Zipped, to_fate, field) of
{ok, [SingleElem]} ->
% Singleton records aren't implemented as FATE tuples at
% all.
{ok, SingleElem};
{ok, Converted} ->
{ok, {tuple, list_to_tuple(Converted)}};
Errors ->
@@ -745,10 +1165,18 @@ coerce_record_to_map(O, N, MemberTypes, Tuple) ->
single_error({record_too_few_terms, O, N, Tuple});
{error, too_many_terms} ->
single_error({record_too_many_terms, O, N, Tuple});
Errors ->
Errors
{error, Errors} ->
correct_record_error_paths(Names, Errors)
end.
correct_record_error_paths(Names, Errors) ->
CorrectOne = fun({Error, [{record_element, N} | Path]}) ->
FieldName = lists:nth(N + 1, Names),
{Error, [{record_element, N, FieldName} | Path]}
end,
Corrected = lists:map(CorrectOne, Errors),
{error, Corrected}.
zip_record_fields(Fields, Map) ->
case lists:mapfoldl(fun zip_record_field/2, {Map, []}, Fields) of
{_, {_, Missing = [_|_]}} ->
@@ -786,6 +1214,7 @@ combine_errors(Broken) ->
lists:foldl(F, [], Broken).
%%% FATE to Erlang
% Not sure if this is needed... fate_to_erlang shouldn't fail.
@@ -794,6 +1223,22 @@ coerce_direction(Type, Term, to_fate) ->
coerce_direction(Type, Term, from_fate) ->
fate_to_erlang(Type, Term).
-spec fate_to_erlang(Type, FATE) -> {ok, Erlang} | {error, Errors}
when Type :: annotated_type(),
FATE :: gmb_fate_data:fate_type(),
Erlang :: erlang_repr(),
Errors :: [{Reason, [PathStep]}],
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}) ->
@@ -831,6 +1276,11 @@ fate_to_erlang({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}) ->
Terms = tuple_to_list(Tuple),
{Name, TermTypes} = lists:nth(Tag + 1, Variants),
coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, from_fate);
fate_to_erlang({O, N, {record, [SingleMemberType]}}, Data) ->
% Singleton records aren't implemented as FATE tuples at all.
% Pretend they are, so we can get the full error indexing of the
% non-singletone case.
coerce_record_to_map(O, N, [SingleMemberType], {Data});
fate_to_erlang({O, N, {record, MemberTypes}}, {tuple, Tuple}) ->
coerce_record_to_map(O, N, MemberTypes, Tuple);
fate_to_erlang({O, N, {unknown_type, _}}, Data) ->
@@ -843,15 +1293,31 @@ fate_to_erlang({O, N, {unknown_type, _}}, Data) ->
io:format(Message, [O, N, Data])
end,
{ok, Data};
fate_to_erlang({O, N, _}, Data) ->
case N of
already_normalized ->
io:format("Warning: Unimplemented type ~p.~nUsing term as is:~n~p~n", [O, Data]);
_ ->
io:format("Warning: Unimplemented type ~p (i.e. ~p).~nUsing term as is:~n~p~n", [O, N, Data])
end,
fate_to_erlang(Type, Data) ->
TypeStr = type_to_iolist(Type),
io:format("Warning: Could not coerce term into ~s. Using term as is: ~p~n", [TypeStr, Data]),
{ok, Data}.
type_to_iolist({O, already_normalized, S}) ->
% Already normalized. Example output:
% type {map, [string, integer]}
opaque_type_to_iolist(O, S);
type_to_iolist({O, N, S}) ->
% Type alias. Print the alias, and then print the normalized version in
% parentheses. Example output:
% type "my_alias" (i.e. record type {"my_record_type", [integer]})
io_lib:format("type ~p (i.e. ~s)", [O, opaque_type_to_iolist(N, S)]).
opaque_type_to_iolist(N, {record, _}) ->
% N is the name of a record definition.
io_lib:format("record type ~p", [N]);
opaque_type_to_iolist(N, {variant, _}) ->
% N is the name of a variant definition.
io_lib:format("variant type ~p", [N]);
opaque_type_to_iolist(N, _) ->
% N is some other constructive type.
io_lib:format("type ~p", [N]).
%%% AACI Getters
@@ -861,11 +1327,13 @@ fate_to_erlang({O, N, _}, Data) ->
Fun :: binary() | string(),
Type :: {term(), term()}, % FIXME
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
@@ -966,9 +1434,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}}).
@@ -978,7 +1446,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() ->
@@ -1036,6 +1505,25 @@ record_substitution_test() ->
{ok, {[], Output}} = get_function_signature(AACI, "f"),
check_roundtrip(Output, #{"a" => 123, "b" => 456}, {tuple, {123, 456}}).
singleton_record_substitution_test() ->
Contract = "
contract C =
record single('t) = { it: 't }
entrypoint f(): single(int) = { it = 1 }
entrypoint g(): single(single(int)) = { it = { it = 2 } }
entrypoint h(): single(int * int) = { it = (3, 4) }
",
{ok, AACI} = aaci_from_string(Contract),
{ok, {[], FOutput}} = get_function_signature(AACI, "f"),
check_roundtrip(FOutput, #{"it" => 123}, 123),
{ok, {[], GOutput}} = get_function_signature(AACI, "g"),
check_roundtrip(GOutput, #{"it" => #{"it" => 123}}, 123),
{ok, {[], HOutput}} = get_function_signature(AACI, "h"),
check_roundtrip(HOutput, #{"it" => {123, 456}}, {tuple, {123, 456}}),
% Also check that records have accurate paths, since the implementation for
% record error paths is a bit fiddly.
{error, [{{tuple_too_many_terms, _, _, _}, [{record_element, 0, "it"}]}]} = fate_to_erlang(HOutput, {tuple, {1, 2, 3}}).
tuple_substitution_test() ->
Contract = "
contract C =
+9 -1
View File
@@ -1,5 +1,13 @@
%%% @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.8.2").
-vsn("0.9.2").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("MIT").
+18 -1
View File
@@ -21,7 +21,7 @@
%%% @end
-module(hz_format).
-vsn("0.8.2").
-vsn("0.9.2").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-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.
+45 -9
View File
@@ -37,8 +37,8 @@
%%% @end
-module(hz_grids).
-vsn("0.8.2").
-export([url/2, url/3, url/4, parse/1, req/2, req/3]).
-vsn("0.9.2").
-export([url/2, url/3, url/4, parse/1, req/2, req/3, req/4]).
-spec url(Instruction, HTTP) -> Result
@@ -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,27 +192,61 @@ 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).
req(sign, Message, ID) ->
-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",
"network_id" => hz:network_id(),
"network_id" => NetworkID,
"type" => "message",
"public_id" => ID,
"payload" => Message};
req(tx, Data, ID) ->
req(tx, Data, ID, NetworkID) ->
#{"grids" => 1,
"chain" => "gajumaru",
"network_id" => hz:network_id(),
"network_id" => NetworkID,
"type" => "tx",
"public_id" => ID,
"payload" => Data};
req(ack, Message, ID) ->
req(ack, Message, ID, NetworkID) ->
#{"grids" => 1,
"chain" => "gajumaru",
"network_id" => hz:network_id(),
"network_id" => NetworkID,
"type" => "ack",
"public_id" => ID,
"payload" => Message}.
+1 -2
View File
@@ -8,8 +8,7 @@
%%% @end
-module(hz_key_master).
-vsn("0.8.2").
-vsn("0.9.2").
-export([make_key/1, encode/1, decode/1]).
-export([lcg/1]).
+1 -2
View File
@@ -9,7 +9,7 @@
%%% @end
-module(hz_man).
-vsn("0.8.2").
-vsn("0.9.2").
-behavior(gen_server).
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
@@ -172,7 +172,6 @@ start_link() ->
%% preparatory work necessary for proper function.
init(none) ->
ok = io:format("hz_man starting.~n"),
State = #s{},
{ok, State}.
+401 -62
View File
@@ -1,21 +1,30 @@
-module(hz_sophia).
-vsn("0.8.2").
-vsn("0.9.2").
-author("Jarvis Carroll <spiveehere@gmail.com>").
-copyright("Jarvis Carroll <spiveehere@gmail.com>").
-license("GPL-3.0-or-later").
-export([parse_literal/1, parse_literal/2, check_parser/1]).
-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(Type, Sophia) -> {ok, FATE} | {error, Reason}
when Type :: hz_aaci:annotated_type(),
Sophia :: string(),
FATE :: gmb_fate_data:fate_type(),
Reason :: term().
-spec parse_literal(String) -> Result
when String :: string(),
Result :: {ok, gmb_fate_data:fate_type()}
| {error, Reason :: term()}.
parse_literal(String) ->
parse_literal(unknown_type(), String).
%% @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
@@ -36,6 +45,29 @@ 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))).
@@ -173,7 +205,7 @@ parse_char(_, {Row, Col}, [$\\, $x, A, B | String], SourceChars) when ?IS_HEX(A)
Byte = convert_digit(A) * 16 + convert_digit(B),
{ok, {Byte, [B, A, $x, $\\ | SourceChars], {Row, Col + 4}, String}};
parse_char({Row, Start}, {Row, Col}, [$\\, C | Rest], SourceChars) ->
case escape_char(C) of
case unescape_char(C) of
{ok, ByteVal} ->
{ok, {ByteVal, [C, $\ | SourceChars], {Row, Col + 2}, Rest}};
error ->
@@ -195,19 +227,33 @@ escape_long_hex_code(_, Pos, [], SourceChars, Value) ->
% produce an unclosed string error instead.
{ok, {Value, SourceChars, Pos, []}}.
escape_char($b) -> {ok, $\b};
escape_char($e) -> {ok, $\e};
escape_char($f) -> {ok, $\f};
escape_char($n) -> {ok, $\n};
escape_char($r) -> {ok, $\r};
escape_char($t) -> {ok, $\t};
escape_char($v) -> {ok, $\v};
unescape_char($b) -> {ok, $\b};
unescape_char($e) -> {ok, $\e};
unescape_char($f) -> {ok, $\f};
unescape_char($n) -> {ok, $\n};
unescape_char($r) -> {ok, $\r};
unescape_char($t) -> {ok, $\t};
unescape_char($v) -> {ok, $\v};
% Technically \" and \' are only valid inside their own quote characters, not
% each other, but whatever, we will just be permissive here.
escape_char($") -> {ok, $\"};
escape_char($') -> {ok, $\'};
escape_char($\\) -> {ok, $\\};
escape_char(_) -> error.
unescape_char($") -> {ok, $\"};
unescape_char($') -> {ok, $\'};
unescape_char($\\) -> {ok, $\\};
unescape_char(_) -> error.
% Not needed until later, but we'll put it here for symmetry.
escape_char($\b) -> "\\b";
escape_char($\e) -> "\\e";
escape_char($\f) -> "\\f";
escape_char($\n) -> "\\n";
escape_char($\r) -> "\\r";
escape_char($\t) -> "\\t";
escape_char($\v) -> "\\v";
escape_char($\") -> "\\\"";
escape_char($\\) -> "\\\\";
escape_char(I) -> I.
%%% Sophia Literal Parser
@@ -902,16 +948,293 @@ parse_map5(KeyType, ValueType, Pos, String, Acc) ->
% TODO
wrap_error(Reason, _) -> Reason.
%%% Pretty Printing
-spec fate_to_list(FATE) -> Sophia
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).
-spec fate_to_list(Type, FATE) -> Sophia
when Type :: hz_aaci:annotated_type(),
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().
fate_to_iolist(Term) ->
fate_to_iolist(unknown_type(), Term).
-spec fate_to_iolist(Type, FATE) -> Sophia
when Type :: hz_aaci:annotated_type(),
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);
% Aggregate types, where we should check if there is useful type information to
% act on. Case logic is made explicit so that the default cases stand out.
fate_to_iolist(Type, {tuple, Tuple}) ->
case Type of
{_, _, {record, FieldTypes}} ->
record_to_iolist(FieldTypes, Tuple);
{_, _, {tuple, ElemTypes}} ->
tuple_to_iolist(ElemTypes, Tuple);
_ ->
tuple_to_iolist([], Tuple)
end;
fate_to_iolist(Type, {variant, _, Tag, Tuple}) ->
case Type of
{O, N, {variant, VariantTypes}} when Tag < length(VariantTypes) ->
variant_to_iolist(O, N, VariantTypes, Tag, Tuple);
{O, N, _} ->
% TODO: Make up a special syntax for anonymous variant terms.
erlang:exit({untyped_variant, O, N});
_ ->
erlang:exit({untyped_variant, unknown_type, already_normalized})
end;
fate_to_iolist(Type, List) when is_list(List) ->
case Type of
{_, _, {list, [InnerType]}} ->
list_to_iolist(InnerType, List);
_ ->
list_to_iolist(unknown_type(), List)
end;
fate_to_iolist(Type, Map) when is_map(Map) ->
case Type of
{_, _, {map, [K, V]}} ->
map_to_iolist(K, V, Map);
_ ->
map_to_iolist(unknown_type(), unknown_type(), Map)
end;
% Other FATE types, where no recursion is needed, but type information could
% influence the format that is used.
fate_to_iolist(_, true) ->
"true";
fate_to_iolist(_, false) ->
"false";
fate_to_iolist(_, {bits, 0}) ->
"Bits.none";
fate_to_iolist(_, {bits, -1}) ->
"Bits.all";
fate_to_iolist(_, {bits, I}) when I > 0 ->
["#", integer_to_list(I, 16)];
fate_to_iolist(_, {bits, I}) when I < 0 ->
integer_to_list(I, 10);
fate_to_iolist({_, _, char}, $') ->
% Special case since it needs to be escaped in char literals.
"'\\''";
fate_to_iolist({_, _, char}, $") ->
% Special case since it does NOT need to be escaped in char literals.
"'\"'";
fate_to_iolist({_, _, char}, I) when is_integer(I) ->
[$', escape_char(I), $'];
fate_to_iolist(_, I) when is_integer(I) ->
integer_to_list(I);
fate_to_iolist(_, {address, Addr}) ->
gmser_api_encoder:encode(account_pubkey, Addr);
fate_to_iolist(_, {contract, Addr}) ->
gmser_api_encoder:encode(contract_pubkey, Addr);
fate_to_iolist(_, {bytes, Bytes}) ->
Size = bit_size(Bytes),
<<IntValue:Size>> = Bytes,
["#", integer_to_list(IntValue, 16)];
fate_to_iolist(_, Bytes) when is_binary(Bytes) ->
escape_string(Bytes).
escape_string(Binary) ->
escape_string(Binary, []).
escape_string(<<C/utf8, Rest/binary>>, Acc) ->
NewAcc = [Acc, escape_char(C)],
escape_string(Rest, NewAcc);
escape_string(<<>>, Acc) ->
[$", Acc, $"].
tuple_to_iolist([ElemType], {Elem}) ->
Inner = fate_to_iolist(ElemType, Elem),
["(", Inner, ",)"];
tuple_to_iolist(_, {Elem}) ->
Inner = fate_to_iolist(unknown_type(), Elem),
["(", Inner, ",)"];
tuple_to_iolist(ElemTypes, Tuple) ->
Elems = tuple_to_list(Tuple),
Multivalue = multivalue_to_iolist(ElemTypes, Elems),
["(", Multivalue, ")"].
list_to_iolist(InnerType, Elems) ->
InnerChars = list_elems_to_iolist(InnerType, Elems),
["[", InnerChars, "]"].
variant_to_iolist(O, N, Variants, Tag, Tuple) ->
Prefix = choose_variant_prefix(O, N),
{Name, ElemTypes} = lists:nth(Tag + 1, Variants),
case tuple_size(Tuple) of
0 ->
[Prefix, Name];
_ ->
Elems = tuple_to_list(Tuple),
Multivalue = multivalue_to_iolist(ElemTypes, Elems),
[Prefix, Name, "(", Multivalue, ")"]
end.
choose_variant_prefix(O, N) ->
case get_typename(O, N) of
[Namespace, _] ->
[Namespace, "."];
_ ->
[]
end.
multivalue_to_iolist([FirstType | ElemTypes], [FirstTerm | Elems]) ->
FirstTermChars = fate_to_iolist(FirstType, FirstTerm),
multivalue_to_iolist(ElemTypes, Elems, FirstTermChars);
multivalue_to_iolist(_, Elems) ->
list_elems_to_iolist(unknown_type(), Elems).
multivalue_to_iolist([NextType | RestTypes], [NextTerm | RestTerms], Acc) ->
NextTermChars = fate_to_iolist(NextType, NextTerm),
multivalue_to_iolist(RestTypes, RestTerms, [Acc, ", ", NextTermChars]);
multivalue_to_iolist(_, Elems, Acc) ->
list_elems_to_iolist(unknown_type(), Elems, Acc).
list_elems_to_iolist(Type, [FirstTerm | Rest]) ->
FirstTermChars = fate_to_iolist(Type, FirstTerm),
list_elems_to_iolist(Type, Rest, FirstTermChars);
list_elems_to_iolist(_, []) ->
"".
list_elems_to_iolist(Type, [Next | Rest], Acc) ->
NextChars = fate_to_iolist(Type, Next),
list_elems_to_iolist(Type, Rest, [Acc, ", ", NextChars]);
list_elems_to_iolist(_, [], Acc) ->
Acc.
singleton_record_to_iolist(FieldName, FieldType, Term) ->
FieldChars = fate_to_iolist(FieldType, Term),
["{", FieldName, " = ", FieldChars, "}"].
record_to_iolist(FieldTypes, Tuple) ->
case length(FieldTypes) == tuple_size(Tuple) of
true ->
Chars = record_fields_to_iolist(FieldTypes, tuple_to_list(Tuple)),
["{", Chars, "}"];
false ->
tuple_to_iolist([], Tuple)
end.
record_fields_to_iolist([{Name, Type} | FieldTypes], [Term | Terms]) ->
TermChars = fate_to_iolist(Type, Term),
record_fields_to_iolist(FieldTypes, Terms, [Name, " = ", TermChars]);
record_fields_to_iolist(_, []) ->
"".
record_fields_to_iolist([{Name, Type} | FieldTypes], [Term | Terms], Acc) ->
TermChars = fate_to_iolist(Type, Term),
NewAcc = [Acc, ", ", Name, " = ", TermChars],
record_fields_to_iolist(FieldTypes, Terms, NewAcc);
record_fields_to_iolist(_, [], Acc) ->
Acc.
map_to_iolist(K, V, Map) ->
Iter = maps:iterator(Map),
case maps:next(Iter) of
{KeyTerm, ValTerm, Rest} ->
KChars = fate_to_iolist(K, KeyTerm),
VChars = fate_to_iolist(V, ValTerm),
RestChars = map_to_iolist_inner(K, V, Rest, ["[", KChars, "] = ", VChars]),
["{", RestChars, "}"];
none ->
"{}"
end.
map_to_iolist_inner(K, V, Iter, Acc) ->
case maps:next(Iter) of
{KeyTerm, ValTerm, Rest} ->
KChars = fate_to_iolist(K, KeyTerm),
VChars = fate_to_iolist(V, ValTerm),
map_to_iolist_inner(K, V, Rest, [Acc, ", [", KChars, "] = ", VChars]);
none ->
Acc
end.
%%% Tests
check_sophia_to_fate(Type, Sophia, Fate) ->
case parse_literal(Type, Sophia) of
{ok, Fate} ->
ok;
{ok, FateActual} ->
erlang:error({to_fate_failed, Sophia, Fate, {ok, FateActual}});
{error, Reason} ->
erlang:error({to_fate_failed, Sophia, Fate, {error, Reason}})
Result ->
erlang:error({to_fate_failed, Sophia, Fate, Result})
end.
check_fate_to_sophia(Type, Fate, Sophia) ->
case fate_to_list(Type, Fate) of
Sophia ->
ok;
Result ->
erlang:error({to_sophia_failed, Fate, Sophia, Result})
end.
roundtrip_parser(Type, Sophia, Fate) ->
check_sophia_to_fate(Type, Sophia, Fate),
check_fate_to_sophia(Type, Fate, Sophia),
ok.
% These test function names are getting ridiculous... I might want to optarg
% them or something, but, whatever, it's test code.
roundtrip_parser_lenient(Type, Sophia, Fate) ->
check_sophia_to_fate(Type, Sophia, Fate),
case fate_to_list(Type, Fate) of
Sophia ->
ok;
SophiaActual ->
check_sophia_to_fate(Type, SophiaActual, Fate)
end.
compile_entrypoint_value_and_type(Source, Entrypoint) ->
@@ -943,65 +1266,76 @@ check_parser(Sophia) ->
{Fate, Type} = compile_entrypoint_value_and_type(Source, "f"),
% Check that when we parse the term we get the same value as the Sophia
% compiler.
% compiler. Also check that the pretty printer gives the same string back.
check_sophia_to_fate(unknown_type(), Sophia, Fate),
% Then, once we know that the term is correct, make sure that it is still
% accepted *with* type info.
% accepted *with* type info. Don't bother roundtripping this, since the
% pretty printer doesn't enforce types anyway.
check_sophia_to_fate(Type, Sophia, Fate).
check_parser_roundtrip(Sophia) ->
Source = "contract C = entrypoint f() = " ++ Sophia,
{Fate, Type} = compile_entrypoint_value_and_type(Source, "f"),
roundtrip_parser(Type, Sophia, Fate),
% Without type information we might get a more generic result in Sophia
% syntax. Let's do a lenient test.
roundtrip_parser_lenient(unknown_type(), Sophia, Fate).
check_parser_with_typedef(Typedef, Sophia) ->
% Compile the type definitions alongside the usual literal expression.
Source = "contract C =\n " ++ Typedef ++ "\n entrypoint f() = " ++ Sophia,
{Fate, Type} = compile_entrypoint_value_and_type(Source, "f"),
% Do a typed parse, as usual, but there are probably record/variant
% definitions in the AACI, so untyped parses probably don't work.
check_sophia_to_fate(Type, Sophia, Fate).
% definitions in the AACI, so untyped parses probably don't work, and
% variants often have optional namespaces, so the sophia result might not
% match exactly, but should still be equivalent.
roundtrip_parser_lenient(Type, Sophia, Fate).
anon_types_test() ->
% Integers.
check_parser("123"),
check_parser_roundtrip("123"),
check_parser("1_2_3"),
check_parser("-123"),
check_parser_roundtrip("-123"),
% Booleans.
check_parser("true"),
check_parser("false"),
check_parser("[true, false]"),
check_parser_roundtrip("true"),
check_parser_roundtrip("false"),
check_parser_roundtrip("[true, false]"),
% Bytes.
check_parser("#DEAD000BEEF"),
check_parser_roundtrip("#DEAD000BEEF"),
check_parser("#DE_AD0_00B_EEF"),
% Strings.
check_parser("\"hello world\""),
check_parser_roundtrip("\"hello world\""),
% The Sophia compiler doesn't handle this right, but we should still.
%check_parser("\"ÿ\""),
%check_parser("\"♣\""),
%check_parser_roundtrip("\"ÿ\""),
%check_parser_roundtrip("\"♣\""),
% Characters.
check_parser("'A'"),
check_parser("['a', ' ', '[']"),
%check_parser("'ÿ'"),
%check_parser("'♣'"),
check_parser_roundtrip("'A'"),
check_parser_roundtrip("['a', ' ', '[']"),
%check_parser_roundtrip("'ÿ'"),
%check_parser_roundtrip("'♣'"),
% List of integers.
check_parser("[1, 2, 3]"),
check_parser_roundtrip("[1, 2, 3]"),
% List of lists.
check_parser("[[], [1], [2, 3]]"),
check_parser_roundtrip("[[], [1], [2, 3]]"),
% Tuple.
check_parser("(1, [2, 3], (4, 5))"),
check_parser_roundtrip("(1, [2, 3], (4, 5))"),
% Map.
check_parser("{[1] = 2, [3] = 4}"),
check_parser_roundtrip("{[1] = 2, [3] = 4}"),
ok.
string_escape_codes_test() ->
check_parser("\" \\b\\e\\f\\n\\r\\t\\v\\\"\\\\ \""),
check_parser_roundtrip("\" \\b\\e\\f\\n\\r\\t\\v\\\"\\\\ \""),
check_parser("\"\\x00\\x11\\x77\\x4a\\x4A\""),
check_parser("\"\\x{0}\\x{7}\\x{7F}\\x{07F}\\x{007F}\\x{0007F}\\x{0000007F}\""),
check_parser("\"'\""),
check_parser_roundtrip("\"'\""),
check_parser("['\\b', '\\e', '\\f', '\\n', '\\r', '\\t', '\\v', '\"', '\\'', '\\\\']"),
check_parser_roundtrip("['\\b', '\\e', '\\f', '\\n', '\\r', '\\t', '\\v', '\"', '\\'', '\\\\']"),
check_parser("['\\x00', '\\x11', '\\x77', '\\x4a', '\\x4A']"),
check_parser("['\\x{0}', '\\x{7}', '\\x{7F}', '\\x{07F}', '\\x{007F}', '\\x{0007F}', '\\x{0000007F}']"),
check_parser("'\"'"),
check_parser_roundtrip("'\"'"),
ok.
@@ -1039,15 +1373,17 @@ namespace_variant_test() ->
Term = "[N.A, N.B]",
Source = "namespace N = datatype mytype = A | B\ncontract C = entrypoint f() = " ++ Term,
{Fate, VariantType} = compile_entrypoint_value_and_type(Source, "f"),
check_sophia_to_fate(VariantType, Term, Fate),
roundtrip_parser(VariantType, Term, Fate),
ok.
chain_objects_test() ->
% Address,
check_parser("ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx"),
check_parser_roundtrip("ak_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx"),
% Two different forms of signature,
check_parser("[sg_XDyF8LJC4tpMyAySvpaG1f5V9F2XxAbRx9iuVjvvdNMwVracLhzAuXhRM5kXAFtpwW1DCHuz5jGehUayCah4jub32Ti2n, #00112233445566778899AABBCCDDEEFF_00112233445566778899AABBCCDDEEFF_00112233445566778899AABBCCDDEEFF_00112233445566778899AABBCCDDEEFF]"),
check_parser("sg_XDyF8LJC4tpMyAySvpaG1f5V9F2XxAbRx9iuVjvvdNMwVracLhzAuXhRM5kXAFtpwW1DCHuz5jGehUayCah4jub32Ti2n"),
check_parser("#00112233445566778899AABBCCDDEEFF_00112233445566778899AABBCCDDEEFF_00112233445566778899AABBCCDDEEFF_00112233445566778899AABBCCDDEEFF"),
check_parser_roundtrip("#112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899AABBCCDDEEFF"),
% We have to build a totally custom contract example in order to get an
% AACI and return value for parsing contract addresses. This is because the
@@ -1058,18 +1394,18 @@ chain_objects_test() ->
Contract = "ct_2FTnrGfV8qsfHpaSEHpBrziioCpwwzLqSevHqfxQY3PaAAdARx",
Source = "contract C = entrypoint f(): C = " ++ Contract,
{Fate, ContractType} = compile_entrypoint_value_and_type(Source, "f"),
check_sophia_to_fate(ContractType, Contract, Fate),
check_sophia_to_fate(unknown_type(), Contract, Fate),
roundtrip_parser(ContractType, Contract, Fate),
roundtrip_parser(unknown_type(), Contract, Fate),
ok.
bits_test() ->
check_parser("Bits.all"),
check_parser("Bits.none"),
check_parser_roundtrip("Bits.all"),
check_parser_roundtrip("Bits.none"),
{_, Type} = compile_entrypoint_value_and_type("contract C = entrypoint f() = Bits.all", "f"),
check_sophia_to_fate(Type, "5", {bits, 5}),
check_sophia_to_fate(Type, "-5", {bits, -5}),
check_sophia_to_fate(Type, "#123", {bits, 256 + 32 + 3}),
roundtrip_parser_lenient(Type, "5", {bits, 5}),
roundtrip_parser(Type, "-5", {bits, -5}),
roundtrip_parser(Type, "#123", {bits, 256 + 32 + 3}),
ok.
singleton_records_test() ->
@@ -1104,7 +1440,8 @@ excess_parens_test() ->
% Including multiple nestings of tuples and grouping, interleaved.
check_parser("((((1), ((2, 3)))), 4)"),
% Also empty tuples exist!
check_parser("()"),
check_parser_roundtrip("()"),
check_parser_roundtrip("(((), ()), ((), ()))"),
check_parser("(((((), ())), ()))"),
ok.
@@ -1166,17 +1503,19 @@ singleton_test() ->
% Now let's do some testing with this weird type, to see if we handle it
% correctly.
{ok, {tuple, {1}}} = parse_literal(SingletonType, "(1,)"),
"(1,)" = fate_to_list(SingletonType, {tuple, {1}}),
% Some ambiguous nesting parens, for fun.
{ok, {tuple, {1}}} = parse_literal(SingletonType, "(((1),))"),
% No trailing comma should give an error.
{error, {expected_trailing_comma, 1, 3}} = parse_literal(SingletonType, "(1)"),
% All of the above should behave the same in untyped contexts:
{ok, {tuple, {1}}} = parse_literal(unknown_type(), "(1,)"),
"(1,)" = fate_to_list(unknown_type(), {tuple, {1}}),
{ok, {tuple, {1}}} = parse_literal(unknown_type(), "(((1),))"),
{ok, 1} = parse_literal(unknown_type(), "(1)"),
% Also if we wanted an integer, the singleton is NOT dropped, so is also an
% error.
{error, {expected_close_paren, 1, 3}} = parse_literal({integer, alread_normalized, integer}, "(1,)"),
{error, {expected_close_paren, 1, 3}} = parse_literal({integer, already_normalized, integer}, "(1,)"),
ok.
+1 -1
View File
@@ -9,7 +9,7 @@
%%% @end
-module(hz_sup).
-vsn("0.8.2").
-vsn("0.9.2").
-behaviour(supervisor).
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
+2 -2
View File
@@ -2,9 +2,9 @@
{type,app}.
{modules,[]}.
{prefix,"hz"}.
{desc,"Gajumaru interoperation library"}.
{author,"Craig Everett"}.
{package_id,{"otpr","hakuzaru",{0,8,2}}}.
{desc,"Gajumaru interoperation library"}.
{package_id,{"otpr","hakuzaru",{0,9,2}}}.
{deps,[{"otpr","sophia",{9,0,0}},
{"otpr","gmserialization",{0,1,3}},
{"otpr","gmbytecode",{3,4,1}},