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