Skip to content

Commit 80d4b58

Browse files
authored
feat: add contract calls/logs nested routes (#1812)
1 parent f064217 commit 80d4b58

File tree

8 files changed

+292
-1096
lines changed

8 files changed

+292
-1096
lines changed

lib/ae_mdw/contracts.ex

Lines changed: 261 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule AeMdw.Contracts do
44
"""
55

66
alias :aeser_api_encoder, as: Enc
7+
alias AeMdw.AexnContracts
78
alias AeMdw.Collection
89
alias AeMdw.Contract
910
alias AeMdw.Db.Contract, as: DbContract
@@ -38,6 +39,7 @@ defmodule AeMdw.Contracts do
3839
@typep reason() :: binary()
3940
@typep pagination() :: Collection.direction_limit()
4041
@typep range() :: {:gen, Range.t()} | {:txi, Range.t()} | nil
42+
@typep logs_opt() :: {:v3?, boolean()}
4143

4244
@contract_log_table Model.ContractLog
4345
@idx_contract_log_table Model.IdxContractLog
@@ -85,33 +87,93 @@ defmodule AeMdw.Contracts do
8587
end
8688
end
8789

88-
@spec fetch_logs(State.t(), pagination(), range(), query(), cursor()) ::
90+
@spec fetch_logs(State.t(), pagination(), range(), query(), cursor(), [logs_opt()]) ::
8991
{:ok, {cursor(), [log()], cursor()}} | {:error, Error.t()}
90-
def fetch_logs(state, pagination, range, query, cursor) do
92+
def fetch_logs(state, pagination, range, query, cursor, opts) do
9193
cursor = deserialize_logs_cursor(cursor)
9294
scope = deserialize_scope(state, range)
9395

94-
with {:ok, filters} <- Util.convert_params(query, &convert_param(state, &1)) do
96+
with {:ok, filters} <- Util.convert_params(query, &convert_logs_param(state, &1)) do
97+
encode_args = %{
98+
aexn_args?: Map.get(filters, :aexn_args, false),
99+
custom_args?: Map.get(filters, :custom_args, false)
100+
}
101+
95102
paginated_logs =
96103
filters
97104
|> build_logs_pagination(state, scope, cursor)
98-
|> Collection.paginate(pagination, & &1, &serialize_logs_cursor/1)
105+
|> Collection.paginate(
106+
pagination,
107+
&render_log(state, &1, encode_args, opts),
108+
&serialize_logs_cursor/1
109+
)
99110

100111
{:ok, paginated_logs}
101112
end
102113
end
103114

104-
@spec fetch_calls(State.t(), pagination(), range(), query(), cursor()) ::
115+
@spec fetch_contract_logs(State.t(), binary(), pagination(), range(), query(), cursor()) ::
116+
{:ok, {cursor(), [log()], cursor()}} | {:error, Error.t()}
117+
def fetch_contract_logs(state, contract_id, pagination, range, query, cursor) do
118+
with {:ok, contract_pk} <- Validate.id(contract_id, [:contract_pubkey]),
119+
{:ok, filters} <- Util.convert_params(query, &convert_logs_param(state, &1)),
120+
{:ok, create_txi} <- create_txi(state, contract_pk) do
121+
cursor = deserialize_logs_cursor(cursor)
122+
scope = deserialize_scope(state, range)
123+
124+
encode_args = %{
125+
aexn_args?: Map.get(filters, :aexn_args, false),
126+
custom_args?: Map.get(filters, :custom_args, false)
127+
}
128+
129+
filters
130+
|> Map.put(:create_txi, create_txi)
131+
|> build_logs_pagination(state, scope, cursor)
132+
|> Collection.paginate(
133+
pagination,
134+
&render_log(state, &1, encode_args, v3?: true),
135+
&serialize_logs_cursor/1
136+
)
137+
|> then(&{:ok, &1})
138+
end
139+
end
140+
141+
@spec fetch_contract_calls(State.t(), binary(), pagination(), range(), query(), cursor()) ::
142+
{:ok, {cursor(), [call()], cursor()}} | {:error, Error.t()}
143+
def fetch_contract_calls(state, contract_id, pagination, range, query, cursor) do
144+
with {:ok, contract_pk} <- Validate.id(contract_id, [:contract_pubkey]),
145+
{:ok, filters} <- Util.convert_params(query, &convert_param(state, &1)),
146+
{:ok, create_txi} <- create_txi(state, contract_pk) do
147+
cursor = deserialize_calls_cursor(cursor)
148+
scope = deserialize_scope(state, range)
149+
150+
filters
151+
|> Map.put(:create_txi, create_txi)
152+
|> build_calls_pagination(state, scope, cursor)
153+
|> Collection.paginate(
154+
pagination,
155+
&render_call(state, &1, v3?: true),
156+
&serialize_calls_cursor/1
157+
)
158+
|> then(&{:ok, &1})
159+
end
160+
end
161+
162+
@spec fetch_calls(State.t(), pagination(), range(), query(), cursor(), Keyword.t()) ::
105163
{:ok, {cursor(), [call()], cursor()}} | {:error, Error.t()}
106-
def fetch_calls(state, pagination, range, query, cursor) do
164+
def fetch_calls(state, pagination, range, query, cursor, opts) do
107165
cursor = deserialize_calls_cursor(cursor)
108166
scope = deserialize_scope(state, range)
109167

110168
with {:ok, filters} <- Util.convert_params(query, &convert_param(state, &1)) do
111169
paginated_calls =
112170
filters
113171
|> build_calls_pagination(state, scope, cursor)
114-
|> Collection.paginate(pagination, &render_call(state, &1), &serialize_calls_cursor/1)
172+
|> Collection.paginate(
173+
pagination,
174+
&render_call(state, &1, opts),
175+
&serialize_calls_cursor/1
176+
)
115177

116178
{:ok, paginated_calls}
117179
end
@@ -492,11 +554,26 @@ defmodule AeMdw.Contracts do
492554
end)
493555
end
494556

557+
defp convert_logs_param(_state, {"aexn-args", value}) when value in ~w(true false),
558+
do: {:ok, {:aexn_args, value == "true"}}
559+
560+
defp convert_logs_param(_state, {"aexn-args", _val}),
561+
do: {:error, ErrInput.Query.exception(value: "aexn-args should be either true or false")}
562+
563+
defp convert_logs_param(_state, {"custom-args", value}) when value in ~w(true false),
564+
do: {:ok, {:custom_args, value == "true"}}
565+
566+
defp convert_logs_param(_state, {"custom-args", _val}),
567+
do: {:error, ErrInput.Query.exception(value: "custom-args should be either true or false")}
568+
569+
defp convert_logs_param(state, arg), do: convert_param(state, arg)
570+
495571
defp convert_param(state, {"contract_id", contract_id}),
496572
do: convert_param(state, {"contract", contract_id})
497573

498574
defp convert_param(state, {"contract", contract_id}) do
499-
with {:ok, create_txi} <- create_txi(state, contract_id) do
575+
with {:ok, contract_pk} <- Validate.id(contract_id),
576+
{:ok, create_txi} <- create_txi(state, contract_pk) do
500577
{:ok, {:create_txi, create_txi}}
501578
end
502579
end
@@ -536,13 +613,9 @@ defmodule AeMdw.Contracts do
536613

537614
defp deserialize_scope(_state, {:txi, first_txi..last_txi}), do: {first_txi, last_txi}
538615

539-
defp create_txi(state, contract_id) do
540-
with {:ok, pk} <- Validate.id(contract_id),
541-
{:ok, txi} <- Origin.tx_index(state, {:contract, pk}) do
542-
{:ok, txi}
543-
else
544-
{:error, reason} -> {:error, reason}
545-
:not_found -> {:error, ErrInput.Id.exception(value: contract_id)}
616+
defp create_txi(state, contract_pk) do
617+
with :not_found <- Origin.tx_index(state, {:contract, contract_pk}) do
618+
{:error, ErrInput.Id.exception(value: Enc.encode(:contract_pubkey, contract_pk))}
546619
end
547620
end
548621

@@ -556,9 +629,16 @@ defmodule AeMdw.Contracts do
556629
tx_type
557630
end
558631

559-
defp render_call(state, {call_txi, local_idx, _create_txi, _pk, _fname, _pos}) do
632+
defp render_call(state, {call_txi, local_idx, _create_txi, _pk, _fname, _pos}, opts) do
560633
call_key = {call_txi, local_idx}
561-
Format.to_map(state, call_key, @int_contract_call_table)
634+
635+
call = Format.to_map(state, call_key, @int_contract_call_table)
636+
637+
if Keyword.get(opts, :v3?, true) do
638+
Map.drop(call, ~w(call_txi contract_txi)a)
639+
else
640+
call
641+
end
562642
end
563643

564644
defp render_contract(state, create_txi_idx) do
@@ -593,6 +673,170 @@ defmodule AeMdw.Contracts do
593673
}
594674
end
595675

676+
defp render_log(state, {create_txi, call_txi, log_idx} = index, encode_args, opts) do
677+
{contract_tx_hash, ct_pk} =
678+
if create_txi == -1 do
679+
{nil, Origin.pubkey(state, {:contract_call, call_txi})}
680+
else
681+
tx_hash = Enc.encode(:tx_hash, Txs.txi_to_hash(state, create_txi))
682+
683+
{tx_hash, Origin.pubkey(state, {:contract, create_txi})}
684+
end
685+
686+
v3? = Keyword.get(opts, :v3?, true)
687+
688+
Model.tx(id: call_tx_hash, block_index: {height, micro_index}) =
689+
State.fetch!(state, Model.Tx, call_txi)
690+
691+
Model.block(hash: block_hash) = DBUtil.read_block!(state, {height, micro_index})
692+
693+
Model.contract_log(args: args, data: data, ext_contract: ext_contract, hash: event_hash) =
694+
State.fetch!(state, Model.ContractLog, index)
695+
696+
event_name = AexnContracts.event_name(event_hash) || get_custom_event_name(event_hash)
697+
698+
state
699+
|> render_remote_log_fields(ext_contract)
700+
|> Map.merge(%{
701+
contract_txi: create_txi,
702+
contract_tx_hash: contract_tx_hash,
703+
contract_id: encode_contract(ct_pk),
704+
call_txi: call_txi,
705+
call_tx_hash: Enc.encode(:tx_hash, call_tx_hash),
706+
block_time: DBUtil.block_time(block_hash),
707+
args: format_args(event_name, args, encode_args),
708+
data: maybe_encode_base64(data),
709+
event_hash: Base.hex_encode32(event_hash),
710+
event_name: event_name,
711+
height: height,
712+
micro_index: micro_index,
713+
block_hash: Enc.encode(:micro_block_hash, block_hash),
714+
log_idx: log_idx
715+
})
716+
|> maybe_remove_logs_txis(v3?)
717+
end
718+
719+
defp maybe_remove_logs_txis(log, true) do
720+
Map.drop(log, [:contract_txi, :call_txi, :ext_caller_contract_txi])
721+
end
722+
723+
defp maybe_remove_logs_txis(log, false) do
724+
log
725+
end
726+
727+
defp render_remote_log_fields(_state, nil) do
728+
%{
729+
ext_caller_contract_tx_hash: nil,
730+
ext_caller_contract_id: nil,
731+
parent_contract_id: nil
732+
}
733+
end
734+
735+
defp render_remote_log_fields(_state, {:parent_contract_pk, parent_pk}) do
736+
%{
737+
ext_caller_contract_txi: -1,
738+
ext_caller_contract_tx_hash: nil,
739+
ext_caller_contract_id: nil,
740+
parent_contract_id: encode_contract(parent_pk)
741+
}
742+
end
743+
744+
defp render_remote_log_fields(state, ext_ct_pk) do
745+
ext_ct_txi = Origin.tx_index!(state, {:contract, ext_ct_pk})
746+
ext_ct_tx_hash = Enc.encode(:tx_hash, Txs.txi_to_hash(state, ext_ct_txi))
747+
748+
%{
749+
ext_caller_contract_txi: ext_ct_txi,
750+
ext_caller_contract_tx_hash: ext_ct_tx_hash,
751+
ext_caller_contract_id: encode_contract(ext_ct_pk),
752+
parent_contract_id: nil
753+
}
754+
end
755+
756+
defp maybe_encode_base64(data) do
757+
if String.valid?(data), do: data, else: Base.encode64(data)
758+
end
759+
760+
defp format_args("Allowance", [account1, account2, <<amount::256>>], %{aexn_args?: true}) do
761+
[encode_account(account1), encode_account(account2), amount]
762+
end
763+
764+
defp format_args("Approval", [account1, account2, <<token_id::256>>, enable], %{
765+
aexn_args?: true
766+
})
767+
when enable in ["true", "false"] do
768+
[encode_account(account1), encode_account(account2), token_id, enable]
769+
end
770+
771+
defp format_args("ApprovalForAll", [account1, account2, enable], %{aexn_args?: true})
772+
when enable in ["true", "false"] do
773+
[encode_account(account1), encode_account(account2), enable]
774+
end
775+
776+
defp format_args(event_name, [account, <<token_id::256>>], %{aexn_args?: true})
777+
when event_name in ["Burn", "Mint", "Swap"] do
778+
[encode_account(account), token_id]
779+
end
780+
781+
defp format_args("PairCreated", [pair_pk, token1, token2], %{aexn_args?: true}) do
782+
[encode_contract(pair_pk), encode_contract(token1), encode_contract(token2)]
783+
end
784+
785+
defp format_args("Transfer", [from, to, <<token_id::256>>], %{aexn_args?: true}) do
786+
[encode_account(from), encode_account(to), token_id]
787+
end
788+
789+
defp format_args(
790+
"TemplateMint",
791+
[account, <<template_id::256>>, <<token_id::256>>],
792+
%{aexn_args?: true}
793+
) do
794+
[encode_account(account), template_id, token_id]
795+
end
796+
797+
defp format_args(
798+
"TemplateMint",
799+
[account, <<template_id::256>>, <<token_id::256>>, edition_serial],
800+
%{aexn_args?: true}
801+
) do
802+
[encode_account(account), template_id, token_id, edition_serial]
803+
end
804+
805+
defp format_args(event_name, args, %{custom_args?: true}) do
806+
case :persistent_term.get({__MODULE__, event_name}, nil) do
807+
nil ->
808+
Enum.map(args, fn <<topic::256>> -> to_string(topic) end)
809+
810+
custom_args_config ->
811+
encode_custom_args(args, custom_args_config)
812+
end
813+
end
814+
815+
defp format_args(_event_name, args, _format_opts) do
816+
Enum.map(args, fn <<topic::256>> -> to_string(topic) end)
817+
end
818+
819+
defp encode_custom_args(args, custom_args_config) do
820+
Enum.with_index(args, fn arg, i ->
821+
case Map.get(custom_args_config, i) do
822+
nil ->
823+
<<topic::256>> = arg
824+
to_string(topic)
825+
826+
type ->
827+
Enc.encode(type, arg)
828+
end
829+
end)
830+
end
831+
832+
defp get_custom_event_name(event_hash) do
833+
:persistent_term.get({__MODULE__, event_hash}, nil)
834+
end
835+
836+
defp encode_contract(pk), do: Enc.encode(:contract_pubkey, pk)
837+
838+
defp encode_account(pk), do: Enc.encode(:account_pubkey, pk)
839+
596840
defp serialize_logs_cursor({create_txi, call_txi, log_idx}),
597841
do: Base.hex_encode32("#{create_txi}$#{call_txi}$#{log_idx}", padding: false)
598842

0 commit comments

Comments
 (0)