3 Commits

Author SHA1 Message Date
Jarvis Carroll cf66548443 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.
2026-06-15 04:52:19 +00:00
Jarvis Carroll 4302ae002c 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.
2026-06-15 02:21:27 +00:00
Jarvis Carroll 6daad4974c 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.
2026-06-08 07:23:34 +00:00
2 changed files with 250 additions and 205 deletions
+130 -34
View File
@@ -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,31 @@ 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, #{"owner_id" := CallerID}} ->
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 +1707,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
+117 -168
View File
@@ -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) ->
<<IntValue:Size>> = 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;
{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 = "