Skip to content

Commit

Permalink
feat: display tx hash instead of txi when tx_hash=true (#789)
Browse files Browse the repository at this point in the history
* feat: display tx hash instead of txi when tx_hash=true

* feat: validate tx_hash and expand aren't sent together

* test: add oracle tx_hash tests
  • Loading branch information
sborrazas committed Jul 19, 2022
1 parent dde85c0 commit 295da57
Show file tree
Hide file tree
Showing 11 changed files with 262 additions and 109 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [HTTP v2 (latest) endpoints](#http-v2-latest-endpoints)
- [HTTP v1 (deprecated) endpoints](#http-v1-endpoints-deprecated)
- [Pagination](#pagination)
- [Additional endpoint options](#additional-endpoint-options)
- [Transactions](#transactions)
- [Blocks](#blocks)
- [Naming System](#naming-system)
Expand Down Expand Up @@ -321,6 +322,28 @@ All paginated endpoints support a `direction` parameter that specifies the order

It can be either `forward` or `backward` (default).

## Additional endpoint options

In many of the endpoints there's some additional query parameters that can be sent to change the endpoint behavior.

### `expand`

When `expand=true` is sent on the URL, it displays additional information.

Most oracle and name endpoints allow this option.

### `top`

When `top=true`, it displays the latest state of the changes by querying the node directly. This is allowed on some of the `AEx9` endpoints to obtain the latest balances state for a given contract.

### `tx_hash`

Currently, in the endpoints created from the beginning, transactions were identified by a "transaction index". Since recently, we've decided this shouldn't really be the case: transactions are to be identified exclusively via the transaction hash. This is because forks caused different transactions to have the same (consecutive and sequentially calculated) transaction indexes.

For the new endpoints, though, we **highly recommend not to use transaction indexes**, instead, use `tx_hash=true` for all endpoints and use exclusively the transaction hashes.

In the newer versions of the API, the transaction index will stop being exposed, regardless of the `tx_hash`query parameter.

----

## Transactions
Expand Down
25 changes: 17 additions & 8 deletions lib/ae_mdw/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -469,31 +469,40 @@ defmodule AeMdw.Contracts do
m_log = State.fetch!(state, @contract_log_table, {create_txi, call_txi, event_hash, log_idx})
ct_id = &:aeser_id.create(:contract, &1)

ct_pk =
{contract_tx_hash, ct_pk} =
if create_txi == -1 do
Origin.pubkey(state, {:contract_call, call_txi})
{nil, Origin.pubkey(state, {:contract_call, call_txi})}
else
Origin.pubkey(state, {:contract, create_txi})
{Txs.txi_to_hash(state, create_txi), Origin.pubkey(state, {:contract, create_txi})}
end

{parent_contract_pk, ext_ct_pk, ext_ct_txi} =
{parent_contract_pk, ext_ct_pk, ext_ct_txi, ext_ct_tx_hash} =
case Model.contract_log(m_log, :ext_contract) do
{:parent_contract_pk, pct_pk} -> {pct_pk, nil, -1}
ext_ct_pk -> {nil, ext_ct_pk, Origin.tx_index!(state, {:contract, ext_ct_pk})}
{:parent_contract_pk, pct_pk} ->
{pct_pk, nil, -1, nil}

ext_ct_pk ->
ext_ct_txi = Origin.tx_index!(state, {:contract, ext_ct_pk})

{nil, ext_ct_pk, ext_ct_txi, Txs.txi_to_hash(state, ext_ct_txi)}
end

Model.tx(block_index: {height, micro_index}) = m_tx = State.fetch!(state, Model.Tx, call_txi)
Model.tx(id: call_tx_hash, block_index: {height, micro_index}) =
State.fetch!(state, Model.Tx, call_txi)

block_hash = Model.block(DBUtil.read_block!(state, {height, micro_index}), :hash)

%{
contract_txi: create_txi,
contract_tx_hash: Enc.encode(:tx_hash, contract_tx_hash),
contract_id: Format.enc_id(ct_id.(ct_pk)),
ext_caller_contract_txi: ext_ct_txi,
ext_caller_contract_tx_hash: Enc.encode(:tx_hash, ext_ct_tx_hash),
ext_caller_contract_id: Format.enc_id((ext_ct_pk != nil && ct_id.(ext_ct_pk)) || nil),
parent_contract_id:
Format.enc_id((parent_contract_pk && ct_id.(parent_contract_pk)) || nil),
call_txi: call_txi,
call_tx_hash: Enc.encode(:tx_hash, Model.tx(m_tx, :id)),
call_tx_hash: Enc.encode(:tx_hash, call_tx_hash),
args: Enum.map(Model.contract_log(m_log, :args), fn <<topic::256>> -> to_string(topic) end),
data: Model.contract_log(m_log, :data),
event_hash: Base.hex_encode32(event_hash),
Expand Down
141 changes: 67 additions & 74 deletions lib/ae_mdw/db/format.ex
Original file line number Diff line number Diff line change
Expand Up @@ -79,35 +79,6 @@ defmodule AeMdw.Db.Format do
}
end

def to_raw_map(state, {call_txi, local_idx}, Model.IntContractCall) do
m_call = State.fetch!(state, Model.IntContractCall, {call_txi, local_idx})
create_txi = Model.int_contract_call(m_call, :create_txi)
fname = Model.int_contract_call(m_call, :fname)

ct_pk =
case Origin.pubkey(state, {:contract, create_txi}) do
nil -> nil
pk -> :aeser_id.create(:contract, pk)
end

m_tx = State.fetch!(state, Model.Tx, call_txi)
{height, micro_index} = Model.tx(m_tx, :block_index)
block_hash = Model.block(DbUtil.read_block!(state, {height, micro_index}), :hash)

%{
contract_txi: (create_txi != -1 && create_txi) || nil,
contract_id: ct_pk,
call_txi: call_txi,
call_tx_hash: Model.tx(m_tx, :id),
function: fname,
internal_tx: Model.int_contract_call(m_call, :tx),
height: height,
micro_index: micro_index,
block_hash: block_hash,
local_idx: local_idx
}
end

def to_raw_map(_state, ae_tx, tx_type) do
AeMdw.Node.tx_fields(tx_type)
|> Stream.with_index(1)
Expand Down Expand Up @@ -192,7 +163,7 @@ defmodule AeMdw.Db.Format do
|> put_in(["micro_index"], mb_index)
|> put_in(["micro_time"], mb_time)

custom_encode(state, type, enc_tx, tx_rec, signed_tx, block_hash)
custom_encode(state, type, enc_tx, tx_rec, signed_tx, index, block_hash)
end

def to_map(state, auction_bid, Model.AuctionBid),
Expand Down Expand Up @@ -223,33 +194,55 @@ defmodule AeMdw.Db.Format do
end

def to_map(state, {call_txi, local_idx}, Model.IntContractCall) do
raw_map = to_raw_map(state, {call_txi, local_idx}, Model.IntContractCall)
m_call = State.fetch!(state, Model.IntContractCall, {call_txi, local_idx})
create_txi = Model.int_contract_call(m_call, :create_txi)
fname = Model.int_contract_call(m_call, :fname)

ct_pk =
case Origin.pubkey(state, {:contract, create_txi}) do
nil -> nil
pk -> :aeser_id.create(:contract, pk)
end

Model.tx(id: call_tx_hash, block_index: {height, micro_index}) =
State.fetch!(state, Model.Tx, call_txi)

block_hash = Model.block(DbUtil.read_block!(state, {height, micro_index}), :hash)

{contract_txi, contract_tx_hash} =
if create_txi == -1 do
{nil, nil}
else
{create_txi, Enc.encode(:tx_hash, Txs.txi_to_hash(state, create_txi))}
end

int_tx = fn tx ->
{tx_type, tx_rec} = :aetx.specialize_type(tx)
serialized_tx = :aetx.serialize_for_client(tx)
tx = Model.int_contract_call(m_call, :tx)
{tx_type, tx_rec} = :aetx.specialize_type(tx)
serialized_tx = :aetx.serialize_for_client(tx)

encoded_tx =
case tx_type do
:contact_call_tx ->
serialized_tx

_ ->
wrapped_tx = %{"tx" => serialized_tx}
_tx_type ->
signed_tx = :aetx_sign.new(tx, [])

%{"tx" => enc_tx} =
custom_encode(state, tx_type, wrapped_tx, tx_rec, signed_tx, raw_map.block_hash)

enc_tx
custom_encode(state, tx_type, serialized_tx, tx_rec, signed_tx, call_txi, block_hash)
end
end

raw_map
|> update_in([:contract_id], &enc_id/1)
|> update_in([:call_tx_hash], &Enc.encode(:tx_hash, &1))
|> update_in([:block_hash], &Enc.encode(:micro_block_hash, &1))
# &:aetx.serialize_for_client/1)
|> update_in([:internal_tx], int_tx)
%{
contract_txi: contract_txi,
contract_tx_hash: contract_tx_hash,
contract_id: enc_id(ct_pk),
call_txi: call_txi,
call_tx_hash: Enc.encode(:tx_hash, call_tx_hash),
function: fname,
internal_tx: encoded_tx,
height: height,
micro_index: micro_index,
block_hash: Enc.encode(:micro_block_hash, block_hash),
local_idx: local_idx
}
end

def to_map(state, data, source, false = _expand),
Expand All @@ -269,36 +262,36 @@ defmodule AeMdw.Db.Format do
|> update_in(["previous"], fn prevs -> Enum.map(prevs, &expand_name_info(state, &1)) end)
end

defp custom_encode(_state, :oracle_response_tx, tx, _tx_rec, _signed_tx, _block_hash),
do: update_in(tx, ["tx", "response"], &maybe_base64/1)
defp custom_encode(_state, :oracle_response_tx, tx, _tx_rec, _signed_tx, _txi, _block_hash),
do: update_in(tx, ["response"], &maybe_base64/1)

defp custom_encode(_state, :oracle_query_tx, tx, tx_rec, _signed_tx, _block_hash) do
defp custom_encode(_state, :oracle_query_tx, tx, tx_rec, _signed_tx, _txi, _block_hash) do
query_id = :aeo_query_tx.query_id(tx_rec)
query_id = Enc.encode(:oracle_query_id, query_id)

tx
|> update_in(["tx", "query"], &Base.encode64/1)
|> put_in(["tx", "query_id"], query_id)
|> update_in(["query"], &Base.encode64/1)
|> put_in(["query_id"], query_id)
end

defp custom_encode(_state, :ga_attach_tx, tx, tx_rec, _signed_tx, _block_hash) do
defp custom_encode(_state, :ga_attach_tx, tx, tx_rec, _signed_tx, _txi, _block_hash) do
contract_pk = :aega_attach_tx.contract_pubkey(tx_rec)
put_in(tx, ["tx", "contract_id"], Enc.encode(:contract_pubkey, contract_pk))
put_in(tx, ["contract_id"], Enc.encode(:contract_pubkey, contract_pk))
end

defp custom_encode(_state, :contract_create_tx, tx, tx_rec, _, block_hash) do
defp custom_encode(_state, :contract_create_tx, tx, tx_rec, _signed_tx, _txi, block_hash) do
init_call_details = Contract.get_init_call_details(tx_rec, block_hash)

update_in(tx, ["tx"], fn tx_details -> Map.merge(tx_details, init_call_details) end)
Map.merge(tx, init_call_details)
end

defp custom_encode(state, :contract_call_tx, tx, tx_rec, _signed_tx, block_hash) do
defp custom_encode(state, :contract_call_tx, tx, tx_rec, _signed_tx, txi, block_hash) do
contract_pk = :aect_call_tx.contract_pubkey(tx_rec)
call_rec = Contract.call_rec(tx_rec, contract_pk, block_hash)

fun_arg_res =
state
|> AeMdw.Db.Contract.call_fun_arg_res(contract_pk, tx["tx_index"])
|> AeMdw.Db.Contract.call_fun_arg_res(contract_pk, txi)
|> map_raw_values(fn
x when is_number(x) -> x
x -> to_string(x)
Expand All @@ -309,34 +302,34 @@ defmodule AeMdw.Db.Format do
|> Map.drop(["return_value", "gas_price", "height", "contract_id", "caller_nonce"])
|> Map.update("log", [], &Contract.stringfy_log_topics/1)

update_in(tx, ["tx"], &Map.merge(&1, Map.merge(fun_arg_res, call_ser)))
Map.merge(tx, Map.merge(fun_arg_res, call_ser))
end

defp custom_encode(_state, :channel_create_tx, tx, _tx_rec, signed_tx, _block_hash) do
channel_pk = :aesc_utils.channel_pubkey(signed_tx) |> ok!
put_in(tx, ["tx", "channel_id"], Enc.encode(:channel, channel_pk))
defp custom_encode(_state, :channel_create_tx, tx, _tx_rec, signed_tx, _txi, _block_hash) do
{:ok, channel_pk} = :aesc_utils.channel_pubkey(signed_tx)
put_in(tx, ["channel_id"], Enc.encode(:channel, channel_pk))
end

defp custom_encode(_state, :oracle_register_tx, tx, tx_rec, _signed_tx, _block_hash) do
defp custom_encode(_state, :oracle_register_tx, tx, tx_rec, _signed_tx, _txi, _block_hash) do
oracle_pk = :aeo_register_tx.account_pubkey(tx_rec)
put_in(tx, ["tx", "oracle_id"], Enc.encode(:oracle_pubkey, oracle_pk))
put_in(tx, ["oracle_id"], Enc.encode(:oracle_pubkey, oracle_pk))
end

defp custom_encode(_state, :name_claim_tx, tx, tx_rec, _signed_tx, _block_hash) do
defp custom_encode(_state, :name_claim_tx, tx, tx_rec, _signed_tx, _txi, _block_hash) do
{:ok, name_id} = :aens.get_name_hash(:aens_claim_tx.name(tx_rec))
put_in(tx, ["tx", "name_id"], Enc.encode(:name, name_id))
put_in(tx, ["name_id"], Enc.encode(:name, name_id))
end

defp custom_encode(state, :name_update_tx, tx, tx_rec, _signed_tx, _block_hash),
do: put_in(tx, ["tx", "name"], Name.plain_name!(state, :aens_update_tx.name_hash(tx_rec)))
defp custom_encode(state, :name_update_tx, tx, tx_rec, _signed_tx, _txi, _block_hash),
do: put_in(tx, ["name"], Name.plain_name!(state, :aens_update_tx.name_hash(tx_rec)))

defp custom_encode(state, :name_transfer_tx, tx, tx_rec, _signed_tx, _block_hash),
do: put_in(tx, ["tx", "name"], Name.plain_name!(state, :aens_transfer_tx.name_hash(tx_rec)))
defp custom_encode(state, :name_transfer_tx, tx, tx_rec, _signed_tx, _txi, _block_hash),
do: put_in(tx, ["name"], Name.plain_name!(state, :aens_transfer_tx.name_hash(tx_rec)))

defp custom_encode(state, :name_revoke_tx, tx, tx_rec, _signed_tx, _block_hash),
do: put_in(tx, ["tx", "name"], Name.plain_name!(state, :aens_revoke_tx.name_hash(tx_rec)))
defp custom_encode(state, :name_revoke_tx, tx, tx_rec, _signed_tx, _txi, _block_hash),
do: put_in(tx, ["name"], Name.plain_name!(state, :aens_revoke_tx.name_hash(tx_rec)))

defp custom_encode(_state, _tx_type, tx, _tx_rec, _signed_tx, _block_hash),
defp custom_encode(_state, _tx_type, tx, _tx_rec, _signed_tx, _txi, _block_hash),
do: tx

defp maybe_base64(bin) do
Expand Down
25 changes: 16 additions & 9 deletions lib/ae_mdw/names.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ defmodule AeMdw.Names do
@table_inactive_expiration Model.InactiveNameExpiration
@table_inactive_owner Model.InactiveNameOwner

@pagination_params ~w(limit cursor rev direction scope)
@pagination_params ~w(limit cursor rev direction scope tx_hash expand)
@states ~w(active inactive)
@all_lifecycles ~w(active inactive auction)a

Expand Down Expand Up @@ -342,10 +342,10 @@ defmodule AeMdw.Names do
%{
active_from: active,
expire_height: expire,
claims: Enum.map(claims, &expand_txi(state, Format.bi_txi_txi(&1), opts)),
updates: Enum.map(updates, &expand_txi(state, Format.bi_txi_txi(&1), opts)),
transfers: Enum.map(transfers, &expand_txi(state, Format.bi_txi_txi(&1), opts)),
revoke: (revoke && expand_txi(state, Format.bi_txi_txi(revoke), opts)) || nil,
claims: Enum.map(claims, &expand_txi(state, &1, opts)),
updates: Enum.map(updates, &expand_txi(state, &1, opts)),
transfers: Enum.map(transfers, &expand_txi(state, &1, opts)),
revoke: (revoke && expand_txi(state, revoke, opts)) || nil,
auction_timeout: auction_timeout,
pointers: render_pointers(state, name),
ownership: render_ownership(state, name)
Expand Down Expand Up @@ -408,10 +408,17 @@ defmodule AeMdw.Names do
end

defp expand_txi(state, bi_txi, opts) do
if Keyword.get(opts, :expand?, false) do
Txs.fetch!(state, Format.bi_txi_txi(bi_txi))
else
bi_txi
txi = Format.bi_txi_txi(bi_txi)

cond do
Keyword.get(opts, :expand?, false) ->
Txs.fetch!(state, txi)

Keyword.get(opts, :tx_hash?, false) ->
Enc.encode(:tx_hash, Txs.txi_to_hash(state, txi))

true ->
txi
end
end

Expand Down
18 changes: 12 additions & 6 deletions lib/ae_mdw/oracles.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ defmodule AeMdw.Oracles do
@table_inactive AeMdw.Db.Model.InactiveOracle
@table_inactive_expiration Model.InactiveOracleExpiration

@pagination_params ~w(limit cursor rev direction scope expand tx_hashes)
@pagination_params ~w(limit cursor rev direction scope expand tx_hash)
@states ~w(active inactive)

@spec fetch_oracles(state(), pagination(), range(), query(), cursor() | nil, opts()) ::
Expand Down Expand Up @@ -159,7 +159,7 @@ defmodule AeMdw.Oracles do
Model.oracle(
index: pk,
expire: expire_height,
register: {{register_height, _mbi}, _register_txi} = register_bi_txi,
register: {{register_height, _mbi}, register_txi} = register_bi_txi,
extends: extends,
previous: _previous
),
Expand All @@ -179,6 +179,7 @@ defmodule AeMdw.Oracles do
active_from: register_height,
expire_height: expire_height,
register: expand_txi(state, register_bi_txi, opts),
register_tx_hash: :aeser_api_encoder.encode(:tx_hash, Txs.txi_to_hash(state, register_txi)),
extends: Enum.map(extends, &expand_txi(state, &1, opts)),
query_fee: Node.Oracle.get!(oracle_rec, :query_fee),
format: %{
Expand Down Expand Up @@ -210,10 +211,15 @@ defmodule AeMdw.Oracles do
defp expand_txi(state, bi_txi, opts) do
txi = Format.bi_txi_txi(bi_txi)

if Keyword.get(opts, :expand?, false) do
Txs.fetch!(state, txi)
else
txi
cond do
Keyword.get(opts, :expand?, false) ->
Txs.fetch!(state, txi)

Keyword.get(opts, :tx_hash?, false) ->
:aeser_api_encoder.encode(:tx_hash, Txs.txi_to_hash(state, txi))

true ->
txi
end
end

Expand Down
Loading

0 comments on commit 295da57

Please sign in to comment.