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.
This commit is contained in:
Jarvis Carroll
2026-06-15 02:21:27 +00:00
parent 6daad4974c
commit 4302ae002c
+131 -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,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