32 Commits

Author SHA1 Message Date
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
Jarvis Carroll 78c9c67f38 typecheck bits
Sophia bitstrings aren't really something you initialize manually, so we have to make up a literal format for them. Failing that, we just accept arbitrary integers and bytearrays as bitstrings.
2026-02-13 06:25:24 +00:00
Jarvis Carroll 9bc0ffafd1 bool/char literals
Character literals were the main complexity here, but I threw booleans in as well, since that covers all the major literals.
2026-02-13 06:25:24 +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 786 additions and 182 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
}
+1 -1
View File
@@ -1,5 +1,5 @@
@author Craig Everett <craigeverett@qpq.swiss> [https://git.qpq.swiss/QPQ-AG/hakuzaru]
@version 0.8.0
@version 0.9.1
@title Hakuzaru: Gajumaru blockchain bindings for Erlang
@doc
+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.1"},
{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.1").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
+139 -81
View File
@@ -23,7 +23,7 @@
%%% @end
-module(hz).
-vsn("0.8.2").
-vsn("0.9.1").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
@@ -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,
@@ -272,8 +272,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 +283,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 +298,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 +342,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
@@ -356,7 +356,7 @@ top_height() ->
when TopBlock :: microblock_header(),
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 +386,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 +444,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 +603,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}
@@ -691,8 +681,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 +693,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 +706,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 +783,39 @@ contract_code(ID) ->
end.
-spec contract_source(ID) -> {ok, Bytecode} | {error, Reason}
when ID :: contract_id(),
Bytecode :: contract_byte_array(),
-spec contract_source(ID) -> Result
when ID :: contract_id(),
Result :: {ok, Source}
| {project, Bundle}
| {error, Reason},
Source :: string(),
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, Bundle} ->
{project, Bundle};
Error ->
io:format("Dis chit happen: ~tp~n", [Error]),
{ok, TarBaby}
end.
-spec contract_poi(ID) -> {ok, Bytecode} | {error, Reason}
when ID :: contract_id(),
@@ -902,7 +919,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
@@ -921,14 +938,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 +982,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 +1014,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 +1055,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 +1065,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 +1077,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 +1108,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 +1119,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 +1146,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 +1178,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 +1205,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 +1457,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
@@ -1649,7 +1708,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 +1752,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.
+87 -3
View File
@@ -10,7 +10,7 @@
%%% @end
-module(hz_aaci).
-vsn("0.8.2").
-vsn("0.9.1").
-author("Jarvis Carroll <spiveehere@gmail.com>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
@@ -25,11 +25,43 @@
%%% 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 aaci() :: {aaci, string(), #{string() => function_spec()}, #{string() => typedef()}}.
-type function_spec() :: {[{string(), annotated_type()}], annotated_type()}.
-type typedef() :: {[string()], typedef_rhs()}.
-type annotated_type() :: {opaque_type(), already_normalized | opaque_type(), builtin_type(annotated_type())}.
-type builtin_type(T) :: {bytes, [integer() | any]}
| {record, [{string(), T}]}
| {variant, [{string(), [T]}]}
| {tuple, [T]}
| {list, [T]}
| {map, [T]}
| integer
| boolean
| bits
| char
| string
| address
| signature
| contract
| channel
| unknown_type.
-type opaque_type() :: string() | {string(), [opaque_type()]} | builtin_type(opaque_type()).
-type typedef_rhs() :: {var, string()} | string() | {string(), [opaque_type()]} | builtin_type(typedef_rhs()).
-type erlang_repr() :: integer()
| string()
| boolean()
| binary()
| tuple() % Tuples, variants, or raw addresses
| [erlang_repr()]
| #{erlang_repr() => erlang_repr()}.
%%% ACI/AACI
@@ -47,6 +79,9 @@ prepare_from_file(Path) ->
Error -> Error
end.
-spec prepare(ACI) -> AACI
when ACI :: term(),
AACI :: aaci().
prepare(ACI) ->
% We want to take the types represented by the ACI, things like N1.T(N2.T),
@@ -66,6 +101,12 @@ prepare(ACI) ->
{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.
@@ -134,6 +175,12 @@ 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_rhs()} | list(typedef_tree()).
collect_opaque_types([], Types) ->
Types;
collect_opaque_types([L | R], Types) ->
@@ -144,6 +191,11 @@ collect_opaque_types({Name, Params, Def}, Types) ->
%%% ACI Type -> Opaque Type
-spec opaque_type(Params, ACIType) -> Opaque
when Params :: [string()],
ACIType :: binary() | map(),
Opaque :: typedef_rhs().
% 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) ->
@@ -171,6 +223,8 @@ 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;
@@ -280,6 +334,12 @@ annotate_function_specs([{Name, ArgsOpaque, ResultOpaque} | Rest], Types, Specs)
NewSpecs = maps:put(Name, {Args, Result}, Specs),
annotate_function_specs(Rest, Types, NewSpecs).
-spec annotate_type(Opaque, Types) -> {ok, Annotated} | {error, Reason}
when Opaque :: opaque_type(),
Types :: #{string() => typedef()},
Annotated :: annotated_type(),
Reason :: none().
annotate_type(T, Types) ->
case normalize_opaque_type(T, Types) of
{ok, AlreadyNormalized, NOpaque, NExpanded} ->
@@ -445,6 +505,14 @@ substitute_opaque_types(Bindings, 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().
erlang_args_to_fate(VarTypes, Terms) ->
DefLength = length(VarTypes),
ArgLength = length(Terms),
@@ -454,6 +522,14 @@ 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().
erlang_to_fate({_, _, integer}, S) when is_integer(S) ->
{ok, S};
erlang_to_fate({O, N, integer}, S) when is_list(S) ->
@@ -794,6 +870,14 @@ 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().
fate_to_erlang({_, _, integer}, S) when is_integer(S) ->
{ok, S};
fate_to_erlang({_, _, address}, {address, Bin}) ->
+1 -1
View File
@@ -1,5 +1,5 @@
-module(hz_fetcher).
-vsn("0.8.2").
-vsn("0.9.1").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("MIT").
+1 -1
View File
@@ -21,7 +21,7 @@
%%% @end
-module(hz_format).
-vsn("0.8.2").
-vsn("0.9.1").
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
-license("GPL-3.0-or-later").
+12 -8
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.1").
-export([url/2, url/3, url/4, parse/1, req/2, req/3, req/4]).
-spec url(Instruction, HTTP) -> Result
@@ -193,24 +193,28 @@ l_to_i(S) ->
req(Type, Message) ->
req(Type, Message, false).
req(sign, Message, ID) ->
req(Type, Message, ID) ->
{ok, NetworkID} = hz:network_id(),
req(Type, Message, ID, NetworkID).
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.1").
-export([make_key/1, encode/1, decode/1]).
-export([lcg/1]).
+1 -1
View File
@@ -9,7 +9,7 @@
%%% @end
-module(hz_man).
-vsn("0.8.2").
-vsn("0.9.1").
-behavior(gen_server).
-author("Craig Everett <ceverett@tsuriai.jp>").
-copyright("Craig Everett <ceverett@tsuriai.jp>").
+459 -75
View File
@@ -1,22 +1,29 @@
-module(hz_sophia).
-vsn("0.8.2").
-vsn("0.9.1").
-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/1, parse_literal/2]).
-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(String) -> Result
when String :: string(),
Result :: {ok, gmb_fate_data:fate_type()}
| {error, Reason :: term()}.
-spec parse_literal(Sophia) -> {ok, FATE} | {error, Reason}
when Sophia :: string(),
FATE :: gmb_fate_data:fate_type(),
Reason :: term().
parse_literal(String) ->
parse_literal(unknown_type(), String).
-spec parse_literal(Type, Sophia) -> {ok, FATE} | {error, Reason}
when Type :: hz_aaci:annotated_type(),
Sophia :: string(),
FATE :: gmb_fate_data:fate_type(),
Reason :: term().
parse_literal(Type, String) ->
case parse_expression(Type, {1, 1}, String) of
{ok, {Result, NewPos, NewString}} ->
@@ -65,6 +72,8 @@ next_token({Row, Col}, [$#, C | Rest]) when ?IS_HEX(C) ->
bytes_token({Row, Col}, {Row, Col + 1}, [C | Rest], "#", []);
next_token({Row, Col}, "\"" ++ Rest) ->
string_token({Row, Col}, {Row, Col + 1}, Rest, "\"", <<>>);
next_token({Row, Col}, "'" ++ Rest) ->
character_token({Row, Col}, {Row, Col + 1}, Rest, "'");
next_token({Row, Col}, [Char | Rest]) ->
Token = {character, [Char], Char, Row, Col, Col},
{ok, {Token, {Row, Col + 1}, Rest}}.
@@ -115,41 +124,70 @@ reverse_combine_nibbles([D1], Acc) ->
reverse_combine_nibbles([], Acc) ->
Acc.
string_token(Start, {Row, Col}, "\\x" ++ String, SourceChars, Value) ->
case escape_hex_code({Row, Col}, {Row, Col + 2}, String, "x\\" ++ SourceChars) of
{ok, {Codepoint, NewSourceChars, NewPos, NewString}} ->
NewValue = <<Value/binary, Codepoint/utf8>>,
string_token(Start, NewPos, NewString, NewSourceChars, NewValue);
{error, Reason} ->
{error, Reason}
end;
string_token(Start, {Row, Col}, [$\\, C | Rest], SourceChars, Value) ->
case escape_char(C) of
{ok, ByteVal} ->
string_token(Start, {Row, Col + 2}, Rest, [C, $\ | SourceChars], <<Value/binary, ByteVal>>);
error ->
{error, {invalid_escape_code, [C], Row, Col}}
end;
string_token({_, Start}, {Row, Col}, [$" | Rest], SourceChars, Value) ->
SourceStr = lists:reverse([$" | SourceChars]),
Token = {string, SourceStr, Value, Row, Start, Col},
{ok, {Token, {Row, Col + 1}, Rest}};
string_token(Start, {Row, Col}, [C | Rest], SourceChars, Value) ->
% TODO: ERTS probably had to convert this FROM utf8 at some point, so why
% bother, if we need to convert it back? I guess we could accept iolists if
% we really wanted to waste time on this point...
string_token(Start, {Row, Col + 1}, Rest, [C | SourceChars], <<Value/binary, C/utf8>>).
string_token({_, Start}, {Row, Col}, [], SourceChars, _) ->
SourceStr = lists:reverse(SourceChars),
{error, {unclosed_string_literal, SourceStr, Start, Row, Col - 1}};
string_token({_, Start}, {Row, Col}, [$\r | _], SourceChars, _) ->
SourceStr = lists:reverse(SourceChars),
{error, {unclosed_string_literal, SourceStr, Start, Row, Col - 1}};
string_token({_, Start}, {Row, Col}, [$\n | _], SourceChars, _) ->
SourceStr = lists:reverse(SourceChars),
{error, {unclosed_string_literal, SourceStr, Start, Row, Col - 1}};
string_token(Start, Pos, String, SourceChars, Value) ->
case parse_char(Start, Pos, String, SourceChars) of
{ok, {Char, NewSourceChars, NewPos, NewString}} ->
% TODO: ERTS probably had to convert this FROM utf8 at some point,
% so why bother, if we need to convert it back? I guess we could
% accept iolists if we really wanted to waste time on this point...
NewValue = <<Value/binary, Char/utf8>>,
string_token(Start, NewPos, NewString, NewSourceChars, NewValue);
{error, Reason} ->
{error, Reason}
end.
escape_hex_code(Start, {Row, Col}, "{" ++ String, SourceChars) ->
escape_long_hex_code(Start, {Row, Col + 1}, String, "{" ++ SourceChars, 0);
escape_hex_code(_, {Row, Col}, [A, B | String], SourceChars) when ?IS_HEX(A), ?IS_HEX(B) ->
% As of writing this, the Sophia compiler will convert this byte from
% extended ASCII to unicode... But it really shouldn't. The literal parser
% does what the compiler should do.
character_token({_, Start}, {Row, Col}, [], SourceChars) ->
SourceStr = lists:reverse(SourceChars),
{error, {unclosed_character_literal, SourceStr, Start, Row, Col - 1}};
character_token({_, Start}, {Row, Col}, [$\r | _], SourceChars) ->
SourceStr = lists:reverse(SourceChars),
{error, {unclosed_character_literal, SourceStr, Start, Row, Col - 1}};
character_token({_, Start}, {Row, Col}, [$\n | _], SourceChars) ->
SourceStr = lists:reverse(SourceChars),
{error, {unclosed_character_literal, SourceStr, Start, Row, Col - 1}};
character_token(Start, Pos, String, SourceChars) ->
case parse_char(Start, Pos, String, SourceChars) of
{ok, {Char, NewSourceChars, NewPos, NewString}} ->
character_token2(Start, NewPos, NewString, NewSourceChars, Char);
{error, Reason} ->
{error, Reason}
end.
character_token2({_, Start}, {Row, Col}, [$' | Rest], SourceChars, Value) ->
SourceStr = lists:reverse([$' | SourceChars]),
Token = {char_literal, SourceStr, Value, Row, Start, Col},
{ok, {Token, {Row, Col + 1}, Rest}};
character_token2({_, Start}, {Row, Col}, _, SourceChars, _) ->
SourceStr = lists:reverse(SourceChars),
{error, {unclosed_character_literal, SourceStr, Start, Row, Col - 1}}.
parse_char(Start, {Row, Col}, "\\x{" ++ String, SourceChars) ->
escape_long_hex_code(Start, {Row, Col + 3}, String, "{x\\" ++ SourceChars, 0);
parse_char(_, {Row, Col}, [$\\, $x, A, B | String], SourceChars) when ?IS_HEX(A), ?IS_HEX(B) ->
Byte = convert_digit(A) * 16 + convert_digit(B),
{ok, {Byte, [B, A | SourceChars], {Row, Col + 2}, String}};
escape_hex_code({Row1, Col1}, _, _, _) ->
{error, {invalid_escape_code, "\\x", Row1, Col1}}.
{ok, {Byte, [B, A, $x, $\\ | SourceChars], {Row, Col + 4}, String}};
parse_char({Row, Start}, {Row, Col}, [$\\, C | Rest], SourceChars) ->
case unescape_char(C) of
{ok, ByteVal} ->
{ok, {ByteVal, [C, $\ | SourceChars], {Row, Col + 2}, Rest}};
error ->
{error, {invalid_escape_code, [$\\, C], Row, Start, Col + 1}}
end;
parse_char(_, {Row, Col}, [C | Rest], SourceChars) ->
{ok, {C, [C | SourceChars], {Row, Col + 1}, Rest}}.
escape_long_hex_code(_, {Row, Col}, "}" ++ String, SourceChars, Value) ->
{ok, {Value, "}" ++ SourceChars, {Row, Col + 1}, String}};
@@ -164,16 +202,31 @@ 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};
escape_char($") -> {ok, $\"};
escape_char($\\) -> {ok, $\\};
escape_char(_) -> error.
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.
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
@@ -202,13 +255,13 @@ parse_expression(Type, Pos, String) ->
end.
parse_expression2(Type, Pos, String, {integer, _, Value, Row, Start, End}) ->
case Type of
{_, _, integer} ->
{ok, {Value, Pos, String}};
{_, _, unknown_type} ->
{ok, {Value, Pos, String}};
{O, N, _} ->
{error, {wrong_type, O, N, integer, Row, Start, End}}
typecheck_integer(Type, Pos, String, Value, Row, Start, End);
parse_expression2(Type, Pos, String, {character, "-", _, _, _, _}) ->
case next_token(Pos, String) of
{ok, {{integer, _, Value, Row, Start, End}, NewPos, NewString}} ->
typecheck_integer(Type, NewPos, NewString, -Value, Row, Start, End);
{error, Reason} ->
{error, Reason}
end;
parse_expression2(Type, Pos, String, {bytes, _, Value, Row, Start, End}) ->
Len = byte_size(Value),
@@ -220,6 +273,10 @@ parse_expression2(Type, Pos, String, {bytes, _, Value, Row, Start, End}) ->
{ok, {Result, Pos, String}};
{_, _, {bytes, [ExpectedLen]}} ->
{error, {bytes_wrong_size, ExpectedLen, Len, Row, Start, End}};
{_, _, bits} ->
Size = bit_size(Value),
<<IntValue:Size>> = Value,
{ok, {{bits, IntValue}, Pos, String}};
{_, _, unknown_type} ->
{ok, {Result, Pos, String}};
{O, N, _} ->
@@ -234,6 +291,15 @@ parse_expression2(Type, Pos, String, {string, _, Value, Row, Start, End}) ->
{O, N, _} ->
{error, {wrong_type, O, N, string, Row, Start, End}}
end;
parse_expression2(Type, Pos, String, {char_literal, _, Value, Row, Start, End}) ->
case Type of
{_, _, char} ->
{ok, {Value, Pos, String}};
{_, _, unknown_type} ->
{ok, {Value, Pos, String}};
{O, N, _} ->
{error, {wrong_type, O, N, char, Row, Start, End}}
end;
parse_expression2(Type, Pos, String, {character, "[", _, Row, Start, _}) ->
parse_list(Type, Pos, String, Row, Start);
parse_expression2(Type, Pos, String, {character, "(", _, _, _, _}) ->
@@ -276,6 +342,14 @@ unexpected_token({_, S, _, Row, Start, End}) ->
%%% Ambiguous Chain Object vs Identifier Parsing
parse_alphanum(Type, Pos, String, ["true"], Row, Start, End) ->
typecheck_bool(Type, Pos, String, true, Row, Start, End);
parse_alphanum(Type, Pos, String, ["false"], Row, Start, End) ->
typecheck_bool(Type, Pos, String, false, Row, Start, End);
parse_alphanum(Type, Pos, String, ["Bits", "all"], Row, Start, End) ->
typecheck_bits(Type, Pos, String, -1, Row, Start, End);
parse_alphanum(Type, Pos, String, ["Bits", "none"], Row, Start, End) ->
typecheck_bits(Type, Pos, String, 0, Row, Start, End);
parse_alphanum(Type, Pos, String, [[C | _] = S], Row, Start, End) when ?IS_LATIN_LOWER(C) ->
% From a programming perspective, we are trying to parse a constant, so
% an alphanum token can really only be a constructor, or a chain object.
@@ -303,6 +377,29 @@ parse_alphanum(Type, Pos, String, Path, Row, Start, End) ->
% must be a variant constructor, or invalid.
parse_variant(Type, Pos, String, Path, Row, Start, End).
typecheck_integer({_, _, integer}, Pos, String, Value, _, _, _) ->
{ok, {Value, Pos, String}};
typecheck_integer({_, _, unknown_type}, Pos, String, Value, _, _, _) ->
{ok, {Value, Pos, String}};
typecheck_integer({_, _, bits}, Pos, String, Value, _, _, _) ->
{ok, {{bits, Value}, Pos, String}};
typecheck_integer({O, N, _}, _, _, _, Row, Start, End) ->
{error, {wrong_type, O, N, integer, Row, Start, End}}.
typecheck_bool({_, _, unknown_type}, Pos, String, Value, _, _, _) ->
{ok, {Value, Pos, String}};
typecheck_bool({_, _, boolean}, Pos, String, Value, _, _, _) ->
{ok, {Value, Pos, String}};
typecheck_bool({O, N, _}, _, _, _, Row, Start, End) ->
{error, {wrong_type, O, N, boolean, Row, Start, End}}.
typecheck_bits({_, _, unknown_type}, Pos, String, Value, _, _, _) ->
{ok, {{bits, Value}, Pos, String}};
typecheck_bits({_, _, bits}, Pos, String, Value, _, _, _) ->
{ok, {{bits, Value}, Pos, String}};
typecheck_bits({O, N, _}, _, _, _, Row, Start, End) ->
{error, {wrong_type, O, N, bits, Row, Start, End}}.
typecheck_address({_, _, address}, Pos, String, Data, _, _, _) ->
{ok, {{address, Data}, Pos, String}};
typecheck_address({_, _, contract}, Pos, String, Data, _, _, _) ->
@@ -824,16 +921,258 @@ 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().
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().
fate_to_list(Type, Term) ->
IOList = fate_to_iolist(Type, Term),
unicode:characters_to_list(IOList).
-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().
% 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) ->
@@ -865,46 +1204,77 @@ 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_roundtrip("-123"),
% Booleans.
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_roundtrip("\"ÿ\""),
%check_parser_roundtrip("\"\""),
% Characters.
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_roundtrip("\"'\""),
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_roundtrip("'\"'"),
ok.
records_test() ->
@@ -941,15 +1311,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
@@ -960,11 +1332,20 @@ 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_roundtrip("Bits.all"),
check_parser_roundtrip("Bits.none"),
{_, Type} = compile_entrypoint_value_and_type("contract C = entrypoint f() = Bits.all", "f"),
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() ->
TypeDef = "record singleton('a) = {it: 'a}",
check_parser_with_typedef(TypeDef, "{it = 123}"),
@@ -997,7 +1378,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.
@@ -1059,17 +1441,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.1").
-behaviour(supervisor).
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
+2 -2
View File
@@ -1,10 +1,10 @@
{name,"Hakuzaru"}.
{type,app}.
{modules,[]}.
{author,"Craig Everett"}.
{prefix,"hz"}.
{desc,"Gajumaru interoperation library"}.
{author,"Craig Everett"}.
{package_id,{"otpr","hakuzaru",{0,8,2}}}.
{package_id,{"otpr","hakuzaru",{0,9,1}}}.
{deps,[{"otpr","sophia",{9,0,0}},
{"otpr","gmserialization",{0,1,3}},
{"otpr","gmbytecode",{3,4,1}},