From ea3a5453f2455b67c631b0837c2d2c4f03c26aed Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Thu, 28 May 2026 00:41:51 +0000 Subject: [PATCH 1/5] fix bytes coerce logic --- src/hz_aaci.erl | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl index 5fa642a..67f5021 100644 --- a/src/hz_aaci.erl +++ b/src/hz_aaci.erl @@ -926,7 +926,10 @@ erlang_to_fate({O, N, char}, Str) -> single_error({invalid, O, N, Str}) end; erlang_to_fate({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) -> - coerce_bytes(O, N, Count, Bytes); + case check_bytes(O, N, Count, Bytes) of + ok -> {ok, {bytes, Bytes}}; + {error, Reason} -> {error, Reason} + end; erlang_to_fate({_, _, bits}, Num) when is_integer(Num) -> {ok, {bits, Num}}; erlang_to_fate({_, _, bits}, Bits) when is_bitstring(Bits) -> @@ -988,14 +991,14 @@ decode_chain_object(Tag, S) -> error:incorrect_size -> {error, incorrect_size} end. -coerce_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 -> +check_bytes(O, N, _, Bytes) when bit_size(Bytes) rem 8 /= 0 -> single_error({partial_bytes, O, N, bit_size(Bytes)}); -coerce_bytes(_, _, any, Bytes) -> - {ok, Bytes}; -coerce_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> +check_bytes(_, _, any, _) -> + ok; +check_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> single_error({incorrect_size, O, N, Bytes}); -coerce_bytes(_, _, _, Bytes) -> - {ok, Bytes}. +check_bytes(_, _, _, _) -> + ok. coerce_zipped_bindings(Bindings, Direction, Tag) -> coerce_zipped_bindings(Bindings, Direction, Tag, [], []). @@ -1261,8 +1264,11 @@ fate_to_erlang({_, _, string}, Bin) -> {ok, Str}; fate_to_erlang({_, _, char}, Val) -> {ok, Val}; -fate_to_erlang({O, N, {bytes, [Count]}}, Bytes) when is_bitstring(Bytes) -> - coerce_bytes(O, N, Count, Bytes); +fate_to_erlang({O, N, {bytes, [Count]}}, {bytes, Bytes}) when is_bitstring(Bytes) -> + case check_bytes(O, N, Count, Bytes) of + ok -> {ok, Bytes}; + {error, Reason} -> {error, Reason} + end; fate_to_erlang({_, _, bits}, {bits, Num}) -> {ok, Num}; fate_to_erlang({_, _, {list, [Type]}}, Data) when is_list(Data) -> @@ -1452,7 +1458,7 @@ coerce_record_test() -> coerce_bytes_test() -> {ok, Type} = annotate_type({tuple, [{bytes, [4]}, {bytes, [any]}]}, #{}), - check_roundtrip(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {<<"abcd">>, <<"efghi">>}}). + check_roundtrip(Type, {<<"abcd">>, <<"efghi">>}, {tuple, {{bytes, <<"abcd">>}, {bytes, <<"efghi">>}}}). coerce_bits_test() -> {ok, Type} = annotate_type(bits, #{}), @@ -1471,7 +1477,7 @@ coerce_unicode_test() -> coerce_hash_test() -> {ok, Type} = annotate_type("hash", builtin_typedefs()), Hash = list_to_binary(lists:seq(1,32)), - check_roundtrip(Type, Hash, Hash), + check_roundtrip(Type, Hash, {bytes, Hash}), ok. -- 2.30.2 From d323fb0f52e3f8a4bfe6c1dc7a7df62089026e0f Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Fri, 5 Jun 2026 03:08:38 +0000 Subject: [PATCH 2/5] Add special anonymous variant syntax This is outside of the scope of the sophia parser, but is a simple generalization to 'sophia terms' to make them able to represent any FATE term anonymously. We also parse these anonymous variant expressions without type info, since it is convenient for users to copy the output of one call into another call. Anonymous parsing of None and Some was also added, since new users would be shocked if this doesn't work, and advanced users will greatly appreciate that it does. The resulting FATE terms are still rendered as variant([0, 1], ...), since user defined types can also have [0, 1] as their arity list, and since automation and tooling programmers hate special case exceptions like that. Anonymous parsing of other Chain and AENS terms are not added, since anonymous variants already cover those types, so very little is gained by hard-coding such complex types into the term parser. Complex, version-specific compiler types are already supported by hakuzaru, in the form of the ACI/AACI; parsing without AACI, on the other hand, is intended to support language-agnostic communication using the primitives of FATE, and in general, variants in FATE are anonymous. --- src/hz_sophia.erl | 207 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 173 insertions(+), 34 deletions(-) diff --git a/src/hz_sophia.erl b/src/hz_sophia.erl index 2a9892e..da7dd5f 100644 --- a/src/hz_sophia.erl +++ b/src/hz_sophia.erl @@ -343,6 +343,12 @@ parse_expression2(_, _, _, Token) -> unknown_type() -> {unknown_type, already_normalized, unknown_type}. +int_type() -> + {integer, already_normalized, integer}. + +int_list_type() -> + {{list, [integer]}, alread_normalized, {list, [int_type()]}}. + expect_tokens([], Pos, String) -> {ok, {Pos, String}}; expect_tokens([Str | Rest], Pos, String) -> @@ -377,11 +383,14 @@ 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, ["variant"], Row, Start, End) -> + parse_anonymous_variant(Type, Pos, String, 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. - % Constructors start with uppercase characters, so lowercase can only be a - % chain object. + % Constructors start with uppercase characters, and we have handled our + % made-up 'variant' case explicitly, so the only other lowercase constants + % are serialized chain objects. try case gmser_api_encoder:decode(unicode:characters_to_binary(S)) of {account_pubkey, Data} -> @@ -400,8 +409,8 @@ parse_alphanum(Type, Pos, String, [[C | _] = S], Row, Start, End) when ?IS_LATIN _:_ -> {error, {unexpected_identifier, S, Row, Start, End}} end; parse_alphanum(Type, Pos, String, Path, Row, Start, End) -> - % Inversely, chain object prefixes are always lowercase, so any other path - % must be a variant constructor, or invalid. + % Now having handled all lowercase terms, anything else must be uppercase, + % which is either a variant constructor, or totally invalid. parse_variant(Type, Pos, String, Path, Row, Start, End). typecheck_integer({_, _, integer}, Pos, String, Value, _, _, _) -> @@ -731,6 +740,12 @@ parse_variant({O, N, {variant, Variants}}, Pos, String, [Namespace, Constructor] _ -> {error, {invalid_constructor, O, N, Namespace ++ "." ++ Constructor, Row, Start, End}} end; +parse_variant({_, _, unknown_type}, Pos, String, ["None"], _, _, _) -> + % Special case for None without type info. + parse_variant3([0, 1], 0, [], Pos, String); +parse_variant({_, _, unknown_type}, Pos, String, ["Some"], _, _, _) -> + % Also a special case for Some. + parse_variant3([0, 1], 1, [unknown_type()], Pos, String); parse_variant({_, _, unknown_type}, _, _, _, Row, Start, End) -> {error, {unresolved_variant, Row, Start, End}}; parse_variant({O, N, _}, _, _, _, Row, Start, End) -> @@ -753,8 +768,7 @@ get_typename(Name) -> parse_variant2(O, N, Variants, Pos, String, Prefix, Constructor, Row, Start, End) -> case lookup_variant(Constructor, Variants, 0) of {ok, {Tag, ElemTypes}} -> - GetArity = fun({_, OtherElemTypes}) -> length(OtherElemTypes) end, - Arities = lists:map(GetArity, Variants), + Arities = get_arities(Variants), parse_variant3(Arities, Tag, ElemTypes, Pos, String); error -> {error, {invalid_constructor, O, N, Prefix ++ Constructor, Row, Start, End}} @@ -790,6 +804,112 @@ lookup_variant(Ident, [{Ident, ElemTypes} | _], Tag) -> lookup_variant(Ident, [_ | Rest], Tag) -> lookup_variant(Ident, Rest, Tag + 1). +get_arities(Variants) -> + GetArity = fun({_, OtherElemTypes}) -> length(OtherElemTypes) end, + lists:map(GetArity, Variants). + +parse_anonymous_variant({O, N, {variant, Variants}}, Pos, String, _, _, _) -> + parse_anonymous_variant2({O, N, {variant, Variants}}, Pos, String); +parse_anonymous_variant({O, N, unknown_type}, Pos, String, _, _, _) -> + parse_anonymous_variant2({O, N, unknown_type}, Pos, String); +parse_anonymous_variant({O, N, _}, _, _, Row, Start, End) -> + {error, {wrong_type, O, N, variant, Row, Start, End}}. + +parse_anonymous_variant2(Type, Pos, String) -> + case expect_tokens(["("], Pos, String) of + {ok, {NewPos, NewString}} -> + parse_anonymous_variant3(Type, NewPos, NewString); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_variant3(Type, Pos, String) -> + case parse_arities(Type, Pos, String) of + {ok, {Arities, NewPos, NewString}} -> + parse_anonymous_variant4(Type, NewPos, NewString, Arities); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_variant4(Type, Pos, String, Arities) -> + case expect_tokens([","], Pos, String) of + {ok, {NewPos, NewString}} -> + parse_anonymous_variant5(Type, NewPos, NewString, Arities); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_variant5(Type, Pos, String, Arities) -> + case parse_anonymous_tag(Pos, String, Arities) of + {ok, {Tag, NewPos, NewString}} -> + parse_anonymous_variant6(Type, NewPos, NewString, Arities, Tag); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_variant6(Type, Pos, String, Arities, Tag) -> + ElemTypes = infer_anonymous_variant_elem_types(Type, Arities, Tag), + case parse_multivalue3(ElemTypes, Pos, String, []) of + {ok, {Terms, NewPos, NewString}} -> + Result = {variant, Arities, Tag, list_to_tuple(Terms)}, + {ok, {Result, NewPos, NewString}}; + {error, Reason} -> + {error, Reason} + end. + +parse_arities(Type, Pos, String) -> + case next_token(Pos, String) of + {ok, {Token, NewPos, NewString}} -> + parse_arities2(Type, NewPos, NewString, Token); + {error, Reason} -> + {error, Reason} + end. + +parse_arities2(Type, Pos, String, Token = {_, _, _, Row, Start, _}) -> + case parse_expression2(int_list_type(), Pos, String, Token) of + {ok, {Arities, NewPos, NewString}} -> + parse_arities3(Type, NewPos, NewString, Arities, Row, Start); + {error, Reason} -> + {error, Reason} + end. + +parse_arities3({O, N, {variant, Variants}}, Pos, String, Arities, Row, Start) -> + ExpectedArities = get_arities(Variants), + case Arities == ExpectedArities of + true -> + {ok, {Arities, Pos, String}}; + false -> + {error, {wrong_arities, O, N, Arities, Row, Start}} + end; +parse_arities3(_, Pos, String, Arities, _, _) -> + {ok, {Arities, Pos, String}}. + +parse_anonymous_tag(Pos, String, Arities) -> + case next_token(Pos, String) of + {ok, {Token, NewPos, NewString}} -> + parse_anonymous_tag2(NewPos, NewString, Arities, Token); + {error, Reason} -> + {error, Reason} + end. + +parse_anonymous_tag2(Pos, String, Arities, Token = {_, _, _, Row, Start, End}) -> + TagCount = length(Arities), + case parse_expression2(int_type(), Pos, String, Token) of + {ok, {Tag, _, _}} when Tag < 0 -> + {error, {negative_tag, Tag, Row, Start, End}}; + {ok, {Tag, _, _}} when Tag >= TagCount -> + {error, {invalid_tag, Tag, TagCount, Row, Start, End}}; + Result -> + Result + end. + +infer_anonymous_variant_elem_types({_, _, {variant, Variants}}, _, Tag) -> + {_Name, ElemTypes} = lists:nth(Tag + 1, Variants), + ElemTypes; +infer_anonymous_variant_elem_types({_, _, unknown_type}, Arities, Tag) -> + Arity = lists:nth(Tag + 1, Arities), + lists:duplicate(Arity, unknown_type()). + %%% Record parsing parse_record_or_map({_, _, {map, [KeyType, ValueType]}}, Pos, String, _, _) -> @@ -1027,15 +1147,12 @@ fate_to_iolist(Type, {tuple, Tuple}) -> _ -> tuple_to_iolist([], Tuple) end; -fate_to_iolist(Type, {variant, _, Tag, Tuple}) -> +fate_to_iolist(Type, {variant, Arities, 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}) + {_, _, _} -> + anonymous_variant_to_iolist(Arities, Tag, Tuple) end; fate_to_iolist(Type, List) when is_list(List) -> case Type of @@ -1130,6 +1247,22 @@ choose_variant_prefix(O, N) -> [] end. +% We don't have type information, but the Sophia programming language doesn't +% have syntax for anonymous variants, so we have to make a syntax up. This +% syntax is also supported when parsing terms, so that the output of one +% contract call can be fed easily into another contract call. +anonymous_variant_to_iolist(Arities, Tag, Tuple) -> + % Extract the elements of the tuple. + Elems = tuple_to_list(Tuple), + + % Turn the arities, tag, and elements into an iolist. + AritiesStr = list_to_iolist(int_type(), Arities), + TagStr = integer_to_list(Tag), + FullTermsStr = list_elems_to_iolist(unknown_type(), Elems, [AritiesStr, ", ", TagStr]), + + % Wrap that iolist in the anonymous 'variant' constructor. + ["variant(", FullTermsStr, ")"]. + multivalue_to_iolist([FirstType | ElemTypes], [FirstTerm | Elems]) -> FirstTermChars = fate_to_iolist(FirstType, FirstTerm), multivalue_to_iolist(ElemTypes, Elems, FirstTermChars); @@ -1282,16 +1415,18 @@ check_parser_roundtrip(Sophia) -> % syntax. Let's do a lenient test. roundtrip_parser_lenient(unknown_type(), Sophia, Fate). -check_parser_with_typedef(Typedef, Sophia) -> +check_parser_with_typedef(Typedef, Sophia, UntypedSophia) -> % 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, 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). + % Do a typed parse, as usual. Variant namespaces can make pretty printing + % ambiguous, so make the roundtrip lenient. + roundtrip_parser_lenient(Type, Sophia, Fate), + % Do an untyped parse, but using a second special Sophia expression that + % doesn't require type info to parse. This one *doesn't* need to be + % lenient, since we are specifying a distinct sophia expression. + roundtrip_parser(unknown_type(), UntypedSophia, Fate). anon_types_test() -> % Integers. @@ -1323,6 +1458,10 @@ anon_types_test() -> check_parser_roundtrip("(1, [2, 3], (4, 5))"), % Map. check_parser_roundtrip("{[1] = 2, [3] = 4}"), + % Option. + check_parser_roundtrip("None"), + check_parser_roundtrip("Some(1)"), + check_parser_roundtrip("Some([1, 2, 3])"), ok. @@ -1342,7 +1481,7 @@ string_escape_codes_test() -> records_test() -> TypeDef = "record pair = {x: int, y: int}", Sophia = "{x = 1, y = 2}", - check_parser_with_typedef(TypeDef, Sophia), + check_parser_with_typedef(TypeDef, Sophia, "(1, 2)"), % The above won't run an untyped parse on the expression, but we can. It % will error, though. {error, {unresolved_record, _, _, _}} = parse_literal(unknown_type(), Sophia). @@ -1350,11 +1489,11 @@ records_test() -> variant_test() -> TypeDef = "datatype multi('a) = Zero | One('a) | Two('a, 'a)", - check_parser_with_typedef(TypeDef, "Zero"), - check_parser_with_typedef(TypeDef, "One(0)"), - check_parser_with_typedef(TypeDef, "Two(0, 1)"), - check_parser_with_typedef(TypeDef, "Two([], [1, 2, 3])"), - check_parser_with_typedef(TypeDef, "C.Zero"), + check_parser_with_typedef(TypeDef, "Zero", "variant([0, 1, 2], 0)"), + check_parser_with_typedef(TypeDef, "One(0)", "variant([0, 1, 2], 1, 0)"), + check_parser_with_typedef(TypeDef, "Two(0, 1)", "variant([0, 1, 2], 2, 0, 1)"), + check_parser_with_typedef(TypeDef, "Two([], [1, 2, 3])", "variant([0, 1, 2], 2, [], [1, 2, 3])"), + check_parser_with_typedef(TypeDef, "C.Zero", "variant([0, 1, 2], 0)"), {error, {unresolved_variant, _, _, _}} = parse_literal(unknown_type(), "Zero"), @@ -1362,10 +1501,10 @@ variant_test() -> ambiguous_variant_test() -> TypeDef = "datatype mytype = C | D", - check_parser_with_typedef(TypeDef, "C"), - check_parser_with_typedef(TypeDef, "D"), - check_parser_with_typedef(TypeDef, "C.C"), - check_parser_with_typedef(TypeDef, "C.D"), + check_parser_with_typedef(TypeDef, "C", "variant([0, 0], 0)"), + check_parser_with_typedef(TypeDef, "D", "variant([0, 0], 1)"), + check_parser_with_typedef(TypeDef, "C.C", "variant([0, 0], 0)"), + check_parser_with_typedef(TypeDef, "C.D", "variant([0, 0], 1)"), ok. @@ -1410,9 +1549,9 @@ bits_test() -> singleton_records_test() -> TypeDef = "record singleton('a) = {it: 'a}", - check_parser_with_typedef(TypeDef, "{it = 123}"), - check_parser_with_typedef(TypeDef, "{it = {it = {it = 5}}}"), - check_parser_with_typedef(TypeDef, "[{it = 1}, {it = 2}, {it = 3}]"), + check_parser_with_typedef(TypeDef, "{it = 123}", "123"), + check_parser_with_typedef(TypeDef, "{it = {it = {it = 5}}}", "5"), + check_parser_with_typedef(TypeDef, "[{it = 1}, {it = 2}, {it = 3}]", "[1, 2, 3]"), ok. @@ -1421,9 +1560,9 @@ singleton_variants_test() -> % actually a special case; singleton variants are in fact wrapped in the % FATE too. TypeDef = "datatype wrapped('a) = Wrap('a)", - check_parser_with_typedef(TypeDef, "Wrap(123)"), - check_parser_with_typedef(TypeDef, "Wrap(Wrap(123))"), - check_parser_with_typedef(TypeDef, "[Wrap(1), Wrap(2), Wrap(3)]"), + check_parser_with_typedef(TypeDef, "Wrap(123)", "variant([1], 0, 123)"), + check_parser_with_typedef(TypeDef, "Wrap(Wrap(123))", "variant([1], 0, variant([1], 0, 123))"), + check_parser_with_typedef(TypeDef, "[Wrap(1), Wrap(2), Wrap(3)]", "[variant([1], 0, 1), variant([1], 0, 2), variant([1], 0, 3)]"), ok. -- 2.30.2 From 6daad4974ce6632690946f56a92fb3d355b37624 Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Mon, 8 Jun 2026 07:23:34 +0000 Subject: [PATCH 3/5] unwrap fate_to_erlang results fate_to_erlang can only really fail at runtime if the wrong AACI is provided, in which case the details of how failure occured are not helpful, or recoverable. Anything else will be so broken that dialyzer will catch it, or is a bug in hakuzaru, that we want to know about. --- src/hz_aaci.erl | 291 ++++++++++++++++++++---------------------------- 1 file changed, 120 insertions(+), 171 deletions(-) diff --git a/src/hz_aaci.erl b/src/hz_aaci.erl index 67f5021..447fd02 100644 --- a/src/hz_aaci.erl +++ b/src/hz_aaci.erl @@ -527,10 +527,7 @@ opaque_type(Params, #{record := FieldDefs}) -> || #{name := Name, type := Type} <- FieldDefs], {record, Fields}; opaque_type(Params, #{variant := VariantDefs}) -> - ConvertVariant = fun(Pair) -> - [{Name, Types}] = maps:to_list(Pair), - {binary_to_list(Name), [opaque_type(Params, Type) || Type <- Types]} - end, + ConvertVariant = fun(Pair) -> opaque_variant_each(Params, Pair) end, Variants = lists:map(ConvertVariant, VariantDefs), {variant, Variants}; opaque_type(Params, #{tuple := TypeDefs}) -> @@ -541,6 +538,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]}. +opaque_variant_each(Params, Pair) -> + [{Name, Types}] = maps:to_list(Pair), + ElemTypes = [opaque_type(Params, Type) || Type <- Types], + {binary_to_list(Name), ElemTypes}. + -spec opaque_type_name(binary()) -> atom() | string(). % Atoms for any builtins that aren't qualified by a namespace in Sophia. @@ -848,7 +850,7 @@ erlang_args_to_fate(VarTypes, Terms) -> DefLength = length(VarTypes), ArgLength = length(Terms), if - DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), to_fate, arg); + DefLength =:= ArgLength -> coerce_zipped_bindings(lists:zip(VarTypes, Terms), arg); DefLength > ArgLength -> {error, too_few_args}; DefLength < ArgLength -> {error, too_many_args} end. @@ -937,19 +939,19 @@ erlang_to_fate({_, _, bits}, Bits) when is_bitstring(Bits) -> <> = Bits, {ok, {bits, IntValue}}; erlang_to_fate({_, _, {list, [Type]}}, Data) when is_list(Data) -> - coerce_list(Type, Data, to_fate); + coerce_list(Type, Data); erlang_to_fate({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) -> - coerce_map(KeyType, ValType, Data, to_fate); + coerce_map(KeyType, ValType, Data); erlang_to_fate({O, N, {tuple, ElementTypes}}, Data) when is_tuple(Data) -> ElementList = tuple_to_list(Data), - coerce_tuple(O, N, ElementTypes, ElementList, to_fate); + coerce_tuple(O, N, ElementTypes, ElementList); erlang_to_fate({O, N, {variant, Variants}}, Name) when is_list(Name) -> erlang_to_fate({O, N, {variant, Variants}}, {Name}); erlang_to_fate({O, N, {variant, Variants}}, Data) when is_tuple(Data), tuple_size(Data) > 0 -> [Name | Terms] = tuple_to_list(Data), case lookup_variant(Name, Variants) of {Tag, TermTypes} -> - coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, to_fate); + coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms); not_found -> ValidNames = [Valid || {Valid, _} <- Variants], single_error({invalid_variant, O, N, Name, ValidNames}) @@ -957,17 +959,15 @@ erlang_to_fate({O, N, {variant, Variants}}, Data) when is_tuple(Data), tuple_siz erlang_to_fate({O, N, {record, MemberTypes}}, Map) when is_map(Map) -> coerce_map_to_record(O, N, MemberTypes, Map); erlang_to_fate({O, N, {unknown_type, _}}, Data) -> - case N of - already_normalized -> - Message = "Warning: Unknown type ~p. Using term ~p as is.~n", - io:format(Message, [O, Data]); - _ -> - Message = "Warning: Unknown type ~p (i.e. ~p). Using term ~p as is.~n", - io:format(Message, [O, N, Data]) - end, + warn_unknown_type(O, N, Data), {ok, Data}; erlang_to_fate({O, N, _}, Data) -> single_error({invalid, O, N, Data}). +warn_unknown_type(O, already_normalized, Data) -> + io:format("Warning: Unknown type ~p. Using term ~p as is.~n", [O, Data]); +warn_unknown_type(O, N, Data) -> + io:format("Warning: Unknown type ~p (i.e. ~p). Using term ~p as is.~n", [O, N, Data]). + coerce_chain_object(_, _, _, _, {raw, Binary}) -> {ok, Binary}; coerce_chain_object(O, N, T, Tag, S) -> @@ -1000,69 +1000,69 @@ check_bytes(O, N, Count, Bytes) when byte_size(Bytes) /= Count -> check_bytes(_, _, _, _) -> ok. -coerce_zipped_bindings(Bindings, Direction, Tag) -> - coerce_zipped_bindings(Bindings, Direction, Tag, [], []). +coerce_zipped_bindings(Bindings, Tag) -> + coerce_zipped_bindings(Bindings, Tag, [], []). -coerce_zipped_bindings([Next | Rest], Direction, Tag, Good, Broken) -> +coerce_zipped_bindings([Next | Rest], Tag, Good, Broken) -> {{ArgName, Type}, Term} = Next, - case coerce_direction(Type, Term, Direction) of + case erlang_to_fate(Type, Term) of {ok, NewTerm} -> - coerce_zipped_bindings(Rest, Direction, Tag, [NewTerm | Good], Broken); + coerce_zipped_bindings(Rest, Tag, [NewTerm | Good], Broken); {error, Errors} -> Wrapped = wrap_errors({Tag, ArgName}, Errors), - coerce_zipped_bindings(Rest, Direction, Tag, Good, [Wrapped | Broken]) + coerce_zipped_bindings(Rest, Tag, Good, [Wrapped | Broken]) end; -coerce_zipped_bindings([], _, _, Good, []) -> +coerce_zipped_bindings([], _, Good, []) -> {ok, lists:reverse(Good)}; -coerce_zipped_bindings([], _, _, _, Broken) -> +coerce_zipped_bindings([], _, _, Broken) -> {error, combine_errors(Broken)}. -coerce_list(Type, Elements, Direction) -> +coerce_list(Type, Elements) -> % 0 index since it represents a sophia list - coerce_list(Type, Elements, Direction, 0, [], []). + coerce_list(Type, Elements, 0, [], []). -coerce_list(Type, [Next | Rest], Direction, Index, Good, Broken) -> - case coerce_direction(Type, Next, Direction) of - {ok, Coerced} -> coerce_list(Type, Rest, Direction, Index + 1, [Coerced | Good], Broken); +coerce_list(Type, [Next | Rest], Index, Good, Broken) -> + case erlang_to_fate(Type, Next) of + {ok, Coerced} -> coerce_list(Type, Rest, Index + 1, [Coerced | Good], Broken); {error, Errors} -> Wrapped = wrap_errors({index, Index}, Errors), - coerce_list(Type, Rest, Direction, Index + 1, Good, [Wrapped | Broken]) + coerce_list(Type, Rest, Index + 1, Good, [Wrapped | Broken]) end; -coerce_list(_Type, [], _, _, Good, []) -> +coerce_list(_Type, [], _, Good, []) -> {ok, lists:reverse(Good)}; -coerce_list(_, [], _, _, _, Broken) -> +coerce_list(_, [], _, _, Broken) -> {error, combine_errors(Broken)}. -coerce_map(KeyType, ValType, Data, Direction) -> - coerce_map(KeyType, ValType, maps:iterator(Data), Direction, #{}, []). +coerce_map(KeyType, ValType, Data) -> + coerce_map(KeyType, ValType, maps:iterator(Data), #{}, []). -coerce_map(KeyType, ValType, Remaining, Direction, Good, Broken) -> +coerce_map(KeyType, ValType, Remaining, Good, Broken) -> case maps:next(Remaining) of {K, V, RemainingAfter} -> - coerce_map2(KeyType, ValType, RemainingAfter, Direction, Good, Broken, K, V); + coerce_map2(KeyType, ValType, RemainingAfter, Good, Broken, K, V); none -> coerce_map_finish(Good, Broken) end. -coerce_map2(KeyType, ValType, Remaining, Direction, Good, Broken, K, V) -> - case coerce_direction(KeyType, K, Direction) of +coerce_map2(KeyType, ValType, Remaining, Good, Broken, K, V) -> + case erlang_to_fate(KeyType, K) of {ok, KFATE} -> - coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE); + coerce_map3(KeyType, ValType, Remaining, Good, Broken, K, V, KFATE); {error, Errors} -> Wrapped = wrap_errors(map_key, Errors), % Continue as if the key coerced successfully, so that we can give % errors for both the key and the value. - coerce_map3(KeyType, ValType, Remaining, Direction, Good, [Wrapped | Broken], K, V, error) + coerce_map3(KeyType, ValType, Remaining, Good, [Wrapped | Broken], K, V, error) end. -coerce_map3(KeyType, ValType, Remaining, Direction, Good, Broken, K, V, KFATE) -> - case coerce_direction(ValType, V, Direction) of +coerce_map3(KeyType, ValType, Remaining, Good, Broken, K, V, KFATE) -> + case erlang_to_fate(ValType, V) of {ok, VFATE} -> NewGood = Good#{KFATE => VFATE}, - coerce_map(KeyType, ValType, Remaining, Direction, NewGood, Broken); + coerce_map(KeyType, ValType, Remaining, NewGood, Broken); {error, Errors} -> Wrapped = wrap_errors({map_value, K}, Errors), - coerce_map(KeyType, ValType, Remaining, Direction, Good, [Wrapped | Broken]) + coerce_map(KeyType, ValType, Remaining, Good, [Wrapped | Broken]) end. coerce_map_finish(Good, []) -> @@ -1079,13 +1079,10 @@ lookup_variant(Name, [_ | Rest], Tag) -> lookup_variant(_Name, [], _Tag) -> not_found. -coerce_tuple(O, N, TermTypes, Terms, Direction) -> - case coerce_tuple_elements(TermTypes, Terms, Direction, tuple_element) of +coerce_tuple(O, N, TermTypes, Terms) -> + case coerce_elems_to_fate(TermTypes, Terms, tuple_element) of {ok, Converted} -> - case Direction of - to_fate -> {ok, {tuple, list_to_tuple(Converted)}}; - from_fate -> {ok, list_to_tuple(Converted)} - end; + {ok, {tuple, list_to_tuple(Converted)}}; {error, too_few_terms} -> single_error({tuple_too_few_terms, O, N, list_to_tuple(Terms)}); {error, too_many_terms} -> @@ -1093,19 +1090,14 @@ coerce_tuple(O, N, TermTypes, Terms, Direction) -> Errors -> Errors end. -coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) -> +coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms) -> % FIXME: we could go through and add the variant tag to the adt_element % paths? - case coerce_tuple_elements(TermTypes, Terms, Direction, adt_element) of + case coerce_elems_to_fate(TermTypes, Terms, adt_element) of {ok, Converted} -> - case Direction of - to_fate -> - Arities = [length(VariantTerms) - || {_, VariantTerms} <- Variants], - {ok, {variant, Arities, Tag, list_to_tuple(Converted)}}; - from_fate -> - {ok, list_to_tuple([Name | Converted])} - end; + Arities = [length(VariantTerms) + || {_, VariantTerms} <- Variants], + {ok, {variant, Arities, Tag, list_to_tuple(Converted)}}; {error, too_few_terms} -> single_error({adt_too_few_terms, O, N, Name, TermTypes, Terms}); {error, too_many_terms} -> @@ -1113,32 +1105,32 @@ coerce_variant2(O, N, Variants, Name, Tag, TermTypes, Terms, Direction) -> Errors -> Errors end. -coerce_tuple_elements(Types, Terms, Direction, Tag) -> +coerce_elems_to_fate(Types, Terms, Tag) -> % The sophia standard library uses 0 indexing for lists, and fst/snd/thd % for tuples... Not sure how we should report errors in tuples, then. - coerce_tuple_elements(Types, Terms, Direction, Tag, 0, [], []). + coerce_elems_to_fate(Types, Terms, Tag, 0, [], []). -coerce_tuple_elements([Type | Types], [Term | Terms], Direction, Tag, Index, Good, Broken) -> - case coerce_direction(Type, Term, Direction) of +coerce_elems_to_fate([Type | Types], [Term | Terms], Tag, Index, Good, Broken) -> + case erlang_to_fate(Type, Term) of {ok, Value} -> - coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, [Value | Good], Broken); + coerce_elems_to_fate(Types, Terms, Tag, Index + 1, [Value | Good], Broken); {error, Errors} -> Wrapped = wrap_errors({Tag, Index}, Errors), - coerce_tuple_elements(Types, Terms, Direction, Tag, Index + 1, Good, [Wrapped | Broken]) + coerce_elems_to_fate(Types, Terms, Tag, Index + 1, Good, [Wrapped | Broken]) end; -coerce_tuple_elements([], [], _, _, _, Good, []) -> +coerce_elems_to_fate([], [], _, _, Good, []) -> {ok, lists:reverse(Good)}; -coerce_tuple_elements([], [], _, _, _, _, Broken) -> +coerce_elems_to_fate([], [], _, _, _, Broken) -> {error, combine_errors(Broken)}; -coerce_tuple_elements(_, [], _, _, _, _, _) -> +coerce_elems_to_fate(_, [], _, _, _, _) -> {error, too_few_terms}; -coerce_tuple_elements([], _, _, _, _, _, _) -> +coerce_elems_to_fate([], _, _, _, _, _) -> {error, too_many_terms}. 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 + case coerce_zipped_bindings(Zipped, field) of {ok, [SingleElem]} -> % Singleton records aren't implemented as FATE tuples at % all. @@ -1155,31 +1147,6 @@ coerce_map_to_record(O, N, MemberTypes, Map) -> single_error({unexpected_fields, O, N, Names}) end. -coerce_record_to_map(O, N, MemberTypes, Tuple) -> - {Names, Types} = lists:unzip(MemberTypes), - Terms = tuple_to_list(Tuple), - % FIXME: We could go through and change the record_element paths into field - % paths? - case coerce_tuple_elements(Types, Terms, from_fate, record_element) of - {ok, Converted} -> - Map = maps:from_list(lists:zip(Names, Converted)), - {ok, Map}; - {error, too_few_terms} -> - single_error({record_too_few_terms, O, N, Tuple}); - {error, too_many_terms} -> - single_error({record_too_many_terms, O, N, Tuple}); - {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 = [_|_]}} -> @@ -1220,20 +1187,10 @@ combine_errors(Broken) -> %%% FATE to Erlang -% Not sure if this is needed... fate_to_erlang shouldn't fail. -coerce_direction(Type, Term, to_fate) -> - erlang_to_fate(Type, Term); -coerce_direction(Type, Term, from_fate) -> - fate_to_erlang(Type, Term). - - --spec fate_to_erlang(Type, FATE) -> {ok, Erlang} | {error, Errors} +-spec fate_to_erlang(Type, FATE) -> Erlang when Type :: annotated_type(), FATE :: gmb_fate_data:fate_type(), - Erlang :: erlang_repr(), - Errors :: [{Reason, [PathStep]}], - Reason :: term(), - PathStep :: term(). + Erlang :: erlang_repr(). %% @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 @@ -1243,86 +1200,81 @@ coerce_direction(Type, Term, from_fate) -> %% information. fate_to_erlang({_, _, integer}, S) when is_integer(S) -> - {ok, S}; + S; fate_to_erlang({_, _, address}, {address, Bin}) -> Address = gmser_api_encoder:encode(account_pubkey, Bin), - {ok, unicode:characters_to_list(Address)}; + unicode:characters_to_list(Address); fate_to_erlang({_, _, contract}, {contract, Bin}) -> Address = gmser_api_encoder:encode(contract_pubkey, Bin), - {ok, unicode:characters_to_list(Address)}; + unicode:characters_to_list(Address); fate_to_erlang({_, _, signature}, Bin) -> Address = gmser_api_encoder:encode(signature, Bin), - {ok, unicode:characters_to_list(Address)}; + unicode:characters_to_list(Address); %fate_to_erlang({_, _, channel}, {channel, S}) when is_binary(S) -> - %{ok, S}; + %S; fate_to_erlang({_, _, boolean}, true) -> - {ok, true}; + true; fate_to_erlang({_, _, boolean}, false) -> - {ok, false}; + false; fate_to_erlang({_, _, string}, Bin) -> - Str = binary_to_list(Bin), - {ok, Str}; + binary_to_list(Bin); fate_to_erlang({_, _, char}, Val) -> - {ok, Val}; + Val; fate_to_erlang({O, N, {bytes, [Count]}}, {bytes, Bytes}) when is_bitstring(Bytes) -> case check_bytes(O, N, Count, Bytes) of - ok -> {ok, Bytes}; - {error, Reason} -> {error, Reason} + ok -> Bytes; + {error, Reason} -> erlang:exit(Reason) end; fate_to_erlang({_, _, bits}, {bits, Num}) -> - {ok, Num}; + Num; fate_to_erlang({_, _, {list, [Type]}}, Data) when is_list(Data) -> - coerce_list(Type, Data, from_fate); + Each = fun(Elem) -> fate_to_erlang(Type, Elem) end, + lists:map(Each, Data); fate_to_erlang({_, _, {map, [KeyType, ValType]}}, Data) when is_map(Data) -> - coerce_map(KeyType, ValType, Data, from_fate); -fate_to_erlang({O, N, {tuple, ElementTypes}}, {tuple, Data}) -> + coerce_map_to_erlang(KeyType, ValType, maps:iterator(Data), #{}); +fate_to_erlang({_, _, {tuple, ElementTypes}}, {tuple, Data}) -> ElementList = tuple_to_list(Data), - coerce_tuple(O, N, ElementTypes, ElementList, from_fate); -fate_to_erlang({O, N, {variant, Variants}}, {variant, _, Tag, Tuple}) -> + Elems = coerce_elems_to_erlang(ElementTypes, ElementList), + list_to_tuple(Elems); +fate_to_erlang({_, _, {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) -> + {Name, Types} = lists:nth(Tag + 1, Variants), + Elems = coerce_elems_to_erlang(Types, Terms), + list_to_tuple([Name | Elems]); +fate_to_erlang({_, _, {record, [SingleField]}}, 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); + coerce_record_to_map([SingleField], [Data], #{}); +fate_to_erlang({_, _, {record, MemberTypes}}, {tuple, Tuple}) -> + Terms = tuple_to_list(Tuple), + coerce_record_to_map(MemberTypes, Terms, #{}); fate_to_erlang({O, N, {unknown_type, _}}, Data) -> - case N of - already_normalized -> - Message = "Warning: Unknown type ~p. Using term ~p as is.~n", - io:format(Message, [O, Data]); - _ -> - Message = "Warning: Unknown type ~p (i.e. ~p). Using term ~p as is.~n", - io:format(Message, [O, N, Data]) - end, - {ok, Data}; -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}. + warn_unknown_type(O, N, Data), + Data; +fate_to_erlang({O, N, _}, Data) -> + erlang:exit({invalid, O, N, 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)]). +coerce_elems_to_erlang(Types, Elems) -> + Zipped = lists:zip(Types, Elems), + Each = fun({Type, Elem}) -> fate_to_erlang(Type, Elem) end, + lists:map(Each, Zipped). -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]). +coerce_record_to_map([{Name, Type} | Types], [Term | Terms], Acc) -> + Coerced = fate_to_erlang(Type, Term), + NewAcc = maps:put(Name, Coerced, Acc), + coerce_record_to_map(Types, Terms, NewAcc); +coerce_record_to_map([], [], Acc) -> + Acc. + +coerce_map_to_erlang(KeyType, ValType, Iter, Acc) -> + case maps:next(Iter) of + {KeyFATE, ValFATE, Rest} -> + Key = fate_to_erlang(KeyType, KeyFATE), + Val = fate_to_erlang(ValType, ValFATE), + NewAcc = maps:put(Key, Val, Acc), + coerce_map_to_erlang(KeyType, ValType, Rest, NewAcc); + none -> + Acc + end. @@ -1360,7 +1312,7 @@ check_erlang_to_fate(Type, Sophia, Fate) -> end. check_fate_to_erlang(Type, Fate, Sophia) -> - {ok, SophiaActual} = fate_to_erlang(Type, Fate), + SophiaActual = fate_to_erlang(Type, Fate), % Now check that the results were what we expected. case SophiaActual of Sophia -> @@ -1525,10 +1477,7 @@ singleton_record_substitution_test() -> {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}}). + check_roundtrip(HOutput, #{"it" => {123, 456}}, {tuple, {123, 456}}). tuple_substitution_test() -> Contract = " -- 2.30.2 From 4302ae002c44fa1396498838e2e3b36ea7bdf3f8 Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Mon, 15 Jun 2026 02:21:27 +0000 Subject: [PATCH 4/5] add parse_tx_info and read_contract_getter parse_tx_info takes the output of tx_info OR dry_run and strips it down to a cb_ encoded binary, and then passes that cb_ encoded binary to decode_bytearray, using the Format specified. read_contract_getter combines contract_call and dry_run, but automatically identifies the owner of the contract, and uses that as the caller, and gives the caller a huge amount of gajus for the purpose of the dry run, so that the call always succeeds. This operation should be available in the node itself, rather than requiring us to do this huge back and forth for something as simple as reading the contents of the blockchain, but at least we can abstract over this in the tooling, and save the user from having to think about these steps. --- src/hz.erl | 165 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 131 insertions(+), 34 deletions(-) diff --git a/src/hz.erl b/src/hz.erl index 37e590c..bd4085e 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -46,6 +46,7 @@ acc_pending_txs/1, next_nonce/1, dry_run/1, dry_run/2, dry_run/3, % dry_run_map/1, + read_contract_getter/4, read_contract_getter/5, tx/1, tx_info/1, post_tx/1, contract/1, contract_code/1, contract_source/1, @@ -71,6 +72,7 @@ contract_call/5, contract_call/6, contract_call/10, + parse_tx_info/2, decode_bytearray/2, spend/5, spend/10, sign_tx/2, sign_tx/3, @@ -627,7 +629,8 @@ dry_run(TX) -> -spec dry_run(TX, Accounts) -> {ok, Result} | {error, Reason} when TX :: binary() | string(), - Accounts :: [pubkey()], + Accounts :: [Account], + Account :: {pubkey(), integer()} | #{string() => term()}, Result :: term(), % FIXME Reason :: term(). % FIXME %% @doc @@ -643,7 +646,8 @@ dry_run(TX, Accounts) -> -spec dry_run(TX, Accounts, KBHash) -> {ok, Result} | {error, Reason} when TX :: binary() | string(), - Accounts :: [pubkey()], + Accounts :: [Account], + Account :: {pubkey(), integer()} | #{string() => term()}, KBHash :: binary() | string(), Result :: term(), % FIXME Reason :: term(). % FIXME @@ -652,21 +656,85 @@ dry_run(TX, Accounts) -> %% hash provided. dry_run(TX, Accounts, KBHash) -> + NAccounts = lists:map(fun normalize_account/1, Accounts), KBB = to_binary(KBHash), TXB = to_binary(TX), DryData = #{top => KBB, - accounts => Accounts, + accounts => NAccounts, txs => [#{tx => TXB}], tx_events => true}, JSON = zj:binary_encode(DryData), request("/v3/dry_run", JSON). +normalize_account({Pubkey, Amount}) -> + PubkeyBin = unicode:characters_to_binary(Pubkey), + #{"pub_key" => PubkeyBin, "amount" => Amount}; +normalize_account(Val) -> + Val. + % TODO %dry_run_map(Map) -> % JSON = zj:binary_encode(Map), % request("/v3/dry_run", JSON). +parse_tx_info({error, Reason}, _) -> + {error, Reason}; +parse_tx_info({ok, Result}, Format) -> + parse_tx_info(Result, Format); +parse_tx_info(#{"call_info" := #{"contract_id" := Contract}}, deploy) -> + % TODO: What happens if a contract deploy goes wrong? + {ok, Contract}; +parse_tx_info(#{"call_info" := #{"return_type" := Status, + "return_value" := Value}}, + Format) -> + parse_tx_value(Status, Value, Format); +parse_tx_info(#{"reason" := Reason, + "parameter" := Parameter, + "info" := #{"error" := Reason2, + "path" := Path, + "data" := Data}}, + _)-> + % Overall dry run error. Informative, but annoyingly inconsistent with all + % other cases. + {error, {Reason, Reason2, [Parameter | Path], Data}}; +parse_tx_info(#{"results" := Results}, Format) -> + % Dry run result, could be multiple results or one, and each could be a + % success or an error. + parse_tx_info(Results, Format); +parse_tx_info([Next, Then | Rest], Format) -> + case Next of + #{"call_obj" := #{"return_type" := "ok"}} -> + % Success. Assume this transaction was just setting up conditions + % for later transactions, and move on. + parse_tx_info([Then | Rest], Format); + _ -> + % Some error. Stop here and parse the error out. + parse_tx_info(Next, Format) + end; +parse_tx_info([Last], Format) -> + parse_tx_info(Last, Format); +parse_tx_info(#{"reason" := Message}, _) -> + % Dry run error for individual tx. + {error, Message}; +parse_tx_info(#{"call_obj" := #{"return_type" := Status, + "return_value" := Value}}, + Format) -> + % Dry run result. At this point we can parse it the same way we parse + % tx_info. + parse_tx_value(Status, Value, Format). + +parse_tx_value("revert", Value, _) -> + Message = decode_bytearray(Value, fate), + {error, {abort, Message}}; +parse_tx_value("error", Value, _) -> + % gmser takes binary inputs and gives binary outputs + EncodedBinary = list_to_binary(Value), + {contract_bytearray, Binary} = gmser_api_encoder:decode(EncodedBinary), + Message = binary_to_list(Binary), + {error, {contract_error, Message}}; +parse_tx_value("ok", Value, Format) -> + decode_bytearray(Value, Format). -spec decode_bytearray_fate(EncodedStr) -> {ok, Result} | {error, Reason} when EncodedStr :: binary() | string(), @@ -720,6 +788,32 @@ 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). +read_contract_getter(AACI, ConID, Fun, Args) -> + case contract(ConID) of + {ok, {}} -> + CallerID = ConID, + read_contract_getter(CallerID, AACI, ConID, Fun, Args); + {error, Reason} -> + {error, Reason} + end. + +read_contract_getter(CallerID, AACI, ConID, Fun, Args) -> + case convert_args(AACI, Fun, Args) of + {ok, {ArgsFATE, ReturnFormat}} -> + read_contract_getter2(CallerID, ConID, Fun, ArgsFATE, ReturnFormat); + {error, Reason} -> + {error, Reason} + end. + +read_contract_getter2(CallerID, ConID, Fun, Args, ReturnFormat) -> + case contract_call(CallerID, {}, ConID, Fun, {fate, Args}) of + {ok, TX} -> + Result = dry_run(TX, [{CallerID, 1 bsl 80}]), + parse_tx_info(Result, ReturnFormat); + {error, Reason} -> + {error, Reason} + end. + to_binary(S) when is_binary(S) -> S; to_binary(S) when is_list(S) -> list_to_binary(S). @@ -1614,44 +1708,47 @@ min_gas() -> 200_000. -encode_call_data({aaci, _ContractName, FunDefs, _TypeDefs}, Fun, Args) -> - case maps:find(Fun, FunDefs) of - {ok, {ArgDef, _ResultDef}} -> encode_call_data2(ArgDef, Fun, Args); - error -> {error, bad_fun_name} - end; -encode_call_data({aaci, Label}, Fun, Args) -> - case hz_man:lookup_aaci(Label) of - {ok, AACI} -> encode_call_data(AACI, Fun, Args); - error -> {error, aaci_not_found} +encode_call_data(AACI, Fun, Args) -> + case convert_args(AACI, Fun, Args) of + {ok, {ArgsFATE, _}} -> + gmb_fate_abi:create_calldata(Fun, ArgsFATE); + {error, Reason} -> + {error, Reason} end. -encode_call_data2(ArgDef, Fun, {sophia, Args}) -> - case convert(ArgDef, Args) of - {ok, Converted} -> gmb_fate_abi:create_calldata(Fun, Converted); - Errors -> Errors - end; -encode_call_data2(ArgDef, Fun, {erlang, Args}) -> - case hz_aaci:erlang_args_to_fate(ArgDef, Args) of - {ok, Coerced} -> gmb_fate_abi:create_calldata(Fun, Coerced); - Errors -> Errors - end; -encode_call_data2(_, Fun, {fate, Args}) -> - % TODO: This should probably be moved back closer to the initiating call. - % 2026-02-13: Craig - gmb_fate_abi:create_calldata(Fun, Args); -encode_call_data2(ArgDef, Fun, Args) -> - encode_call_data2(ArgDef, Fun, {sophia, Args}). +convert_args(_, _, {fate, Args}) -> + {ok, {Args, fate}}; +convert_args(AACI, Fun, Args) -> + case aaci_lookup_spec(AACI, Fun) of + {ok, {ArgTypes, ReturnType}} -> + convert_args2(ArgTypes, Args, ReturnType); + {error, Reason} -> + {error, Reason} + end. -convert(Defs, Args) -> convert(Defs, Args, 1, [], []). +convert_args2(ArgTypes, {erlang, Args}, ReturnType) -> + case hz_aaci:erlang_args_to_fate(ArgTypes, Args) of + {ok, Converted} -> {ok, {Converted, {erlang, ReturnType}}}; + {error, Reason} -> {error, Reason} + end; +convert_args2(ArgTypes, {sophia, Args}, ReturnType) -> + case sophia_args_to_fate(ArgTypes, Args) of + {ok, Converted} -> {ok, {Converted, {sophia, ReturnType}}}; + {error, Reason} -> {error, Reason} + end; +convert_args2(ArgTypes, Args, ReturnType) -> + convert_args2(ArgTypes, {sophia, Args}, ReturnType). -convert([{Name, Def} | Defs], [Arg | Args], Nth, Terms, Errors) -> +sophia_args_to_fate(Defs, Args) -> sophia_args_to_fate(Defs, Args, 1, [], []). + +sophia_args_to_fate([{Name, Def} | Defs], [Arg | Args], Nth, Terms, Errors) -> case hz_sophia:parse_literal(Def, Arg) of - {ok, Term} -> convert(Defs, Args, Nth + 1, [Term | Terms], Errors); - {error, Reason} -> convert(Defs, Args, Nth + 1, Terms, [{Nth, Name, Reason} | Errors]) + {ok, Term} -> sophia_args_to_fate(Defs, Args, Nth + 1, [Term | Terms], Errors); + {error, Reason} -> sophia_args_to_fate(Defs, Args, Nth + 1, Terms, [{Nth, Name, Reason} | Errors]) end; -convert([], [], _, Terms, []) -> +sophia_args_to_fate([], [], _, Terms, []) -> {ok, lists:reverse(Terms)}; -convert([], [], _, _, Errors) -> +sophia_args_to_fate([], [], _, _, Errors) -> {error, Errors}. -spec sign_tx(Unsigned, SecKey) -> Result -- 2.30.2 From cf6654844325352ba4b4d718cb92cdfc783e9e39 Mon Sep 17 00:00:00 2001 From: Jarvis Carroll Date: Mon, 15 Jun 2026 04:52:19 +0000 Subject: [PATCH 5/5] Fix read_contract_getter/4 This was meant to be a placeholder that I would catch and fix, but my test case never hit it! Whoops. --- src/hz.erl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hz.erl b/src/hz.erl index bd4085e..98ba4f4 100644 --- a/src/hz.erl +++ b/src/hz.erl @@ -790,8 +790,7 @@ decode_bytearray2(FATE, {erlang, Type}) -> hz_aaci:fate_to_erlang(Type, FATE). read_contract_getter(AACI, ConID, Fun, Args) -> case contract(ConID) of - {ok, {}} -> - CallerID = ConID, + {ok, #{"owner_id" := CallerID}} -> read_contract_getter(CallerID, AACI, ConID, Fun, Args); {error, Reason} -> {error, Reason} -- 2.30.2