Skip to content

Commit

Permalink
feat: add /oracles/:id/queries to list an oracle queries (#1240)
Browse files Browse the repository at this point in the history
  • Loading branch information
sborrazas authored Mar 24, 2023
1 parent dfe86b5 commit f8f2b7d
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 14 deletions.
75 changes: 66 additions & 9 deletions lib/ae_mdw/oracles.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ defmodule AeMdw.Oracles do
Context module for dealing with Oracles.
"""

require AeMdw.Db.Model

alias :aeser_api_encoder, as: Enc
alias AeMdw.Blocks
alias AeMdw.Collection
alias AeMdw.Db.Format
alias AeMdw.Db.Model
alias AeMdw.Db.Oracle
alias AeMdw.Db.State
Expand All @@ -16,10 +16,14 @@ defmodule AeMdw.Oracles do
alias AeMdw.Node.Db
alias AeMdw.Txs
alias AeMdw.Util
alias AeMdw.Validate

require Model

@type cursor :: binary()
# This needs to be an actual type like AeMdw.Db.Oracle.t()
@type oracle :: term()
@type oracle_query() :: map()
@type pagination :: Collection.direction_limit()
@type opts() :: Util.opts()
@type query_id() :: binary()
Expand All @@ -33,6 +37,7 @@ defmodule AeMdw.Oracles do
@table_active_expiration Model.ActiveOracleExpiration
@table_inactive AeMdw.Db.Model.InactiveOracle
@table_inactive_expiration Model.InactiveOracleExpiration
@table_query Model.OracleQuery

@pagination_params ~w(limit cursor rev direction scope expand tx_hash)
@states ~w(active inactive)
Expand Down Expand Up @@ -60,6 +65,59 @@ defmodule AeMdw.Oracles do
end
end

@spec fetch_oracle_queries(state(), pubkey(), pagination(), range(), cursor() | nil) ::
{:ok, {cursor() | nil, [oracle_query()], cursor() | nil}} | {:error, Error.t()}
def fetch_oracle_queries(state, oracle_id, pagination, nil, cursor) do
with {:ok, oracle_pk} <- Validate.id(oracle_id, [:oracle_pubkey]),
{:ok, cursor} <- deserialize_queries_cursor(cursor, oracle_pk) do
key_boundary = {{oracle_pk, Util.min_bin()}, {oracle_pk, Util.max_256bit_bin()}}

{prev_cursor, query_ids, next_cursor} =
fn direction ->
Collection.stream(state, @table_query, direction, key_boundary, cursor)
end
|> Collection.paginate(pagination)

queries = Enum.map(query_ids, &render_query(state, &1))

{:ok,
{serialize_queries_cursor(prev_cursor), queries, serialize_queries_cursor(next_cursor)}}
end
end

def fetch_oracle_queries(_state, _oracle_id, _pagination, _range, _cursor),
do: {:error, ErrInput.Query.exception(value: "cannot filter by range on this endpoint")}

defp deserialize_queries_cursor(nil, _oracle_pk), do: {:ok, nil}

defp deserialize_queries_cursor(query_id, oracle_pk) do
case Enc.safe_decode(:oracle_query_id, query_id) do
{:ok, query_id} -> {:ok, {oracle_pk, query_id}}
{:error, _reason} -> {:error, ErrInput.Cursor.exception(value: query_id)}
end
end

defp serialize_queries_cursor(nil), do: nil

defp serialize_queries_cursor({{_oracle_pk, query_id}, is_reversed?}),
do: {Enc.encode(:oracle_query_id, query_id), is_reversed?}

defp render_query(state, {oracle_pk, query_id}) do
Model.oracle_query(txi_idx: txi_idx) =
State.fetch!(state, Model.OracleQuery, {oracle_pk, query_id})

{query_tx, tx_hash, tx_type, block_hash} = DBUtil.read_node_tx_details(state, txi_idx)

%{
block_hash: Enc.encode(:micro_block_hash, block_hash),
source_tx_hash: Enc.encode(:tx_hash, tx_hash),
source_tx_type: Format.type_to_swagger_name(tx_type),
query_id: Enc.encode(:oracle_query_id, query_id)
}
|> Map.merge(:aeo_query_tx.for_client(query_tx))
|> update_in(["query"], &Base.encode64(&1, padding: false))
end

defp convert_param({"state", state}) when state in @states, do: {:state, state}

defp convert_param(other_param),
Expand Down Expand Up @@ -125,8 +183,7 @@ defmodule AeMdw.Oracles do
{:ok, render(state, m_oracle, last_gen, source == Model.ActiveOracle, opts)}

nil ->
{:error,
ErrInput.NotFound.exception(value: :aeser_api_encoder.encode(:oracle_pubkey, oracle_pk))}
{:error, ErrInput.NotFound.exception(value: Enc.encode(:oracle_pubkey, oracle_pk))}
end
end

Expand Down Expand Up @@ -177,12 +234,12 @@ defmodule AeMdw.Oracles do
query_fee = :aeo_oracles.query_fee(oracle_rec)

%{
oracle: :aeser_api_encoder.encode(:oracle_pubkey, pk),
oracle: Enc.encode(:oracle_pubkey, pk),
active: is_active?,
active_from: register_height,
expire_height: expire_height,
register: expand_bi_txi_idx(state, register_bi_txi_idx, opts),
register_tx_hash: :aeser_api_encoder.encode(:tx_hash, Txs.txi_to_hash(state, register_txi)),
register_tx_hash: Enc.encode(:tx_hash, Txs.txi_to_hash(state, register_txi)),
extends: Enum.map(extends, &expand_bi_txi_idx(state, &1, opts)),
query_fee: query_fee,
format: %{
Expand All @@ -198,13 +255,13 @@ defmodule AeMdw.Oracles do
do: serialize_cursor({{exp_height, oracle_pk}, is_reversed?})

defp serialize_cursor({{exp_height, oracle_pk}, is_reversed?}),
do: {"#{exp_height}-#{:aeser_api_encoder.encode(:oracle_pubkey, oracle_pk)}", is_reversed?}
do: {"#{exp_height}-#{Enc.encode(:oracle_pubkey, oracle_pk)}", is_reversed?}

defp deserialize_cursor(nil), do: nil

defp deserialize_cursor(cursor_bin) do
with [_match0, exp_height, encoded_pk] <- Regex.run(~r/(\d+)-(ok_\w+)/, cursor_bin),
{:ok, pk} <- :aeser_api_encoder.safe_decode(:oracle_pubkey, encoded_pk) do
{:ok, pk} <- Enc.safe_decode(:oracle_pubkey, encoded_pk) do
{String.to_integer(exp_height), pk}
else
_nil_or_error -> nil
Expand All @@ -217,7 +274,7 @@ defmodule AeMdw.Oracles do
Txs.fetch!(state, txi)

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

true ->
txi
Expand Down
13 changes: 13 additions & 0 deletions lib/ae_mdw_web/controllers/oracle_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,17 @@ defmodule AeMdwWeb.OracleController do
{:error, reason}
end
end

@spec oracle_queries(Conn.t(), map()) :: Conn.t()
def oracle_queries(%Conn{assigns: assigns} = conn, %{"id" => oracle_id}) do
%{state: state, pagination: pagination, cursor: cursor, scope: scope} = assigns

case Oracles.fetch_oracle_queries(state, oracle_id, pagination, scope, cursor) do
{:ok, {prev_cursor, oracles, next_cursor}} ->
Util.paginate(conn, prev_cursor, oracles, next_cursor)

{:error, reason} ->
{:error, reason}
end
end
end
3 changes: 2 additions & 1 deletion lib/ae_mdw_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ defmodule AeMdwWeb.Router do
AexnTokenController,
:aex9_token_balance_history

get "/oracles/:id", OracleController, :oracle
get "/oracles", OracleController, :oracles
get "/oracles/:id", OracleController, :oracle
get "/oracles/:id/queries", OracleController, :oracle_queries

get "/channels", ChannelController, :channels
get "/channels/:id", ChannelController, :channel
Expand Down
163 changes: 159 additions & 4 deletions test/ae_mdw_web/controllers/oracle_controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
defmodule AeMdwWeb.OracleControllerTest do
use AeMdwWeb.ConnCase, async: false

import Mock

require AeMdw.Db.Model

alias :aeser_api_encoder, as: Enc
alias AeMdw.Blocks
alias AeMdw.Db.Model
Expand All @@ -13,9 +9,14 @@ defmodule AeMdwWeb.OracleControllerTest do
alias AeMdw.Db.Model.Block
alias AeMdw.Db.Oracle
alias AeMdw.Db.Store
alias AeMdw.Db.Util, as: DbUtil
alias AeMdw.Database
alias AeMdw.TestSamples, as: TS

import Mock

require Model

describe "oracles" do
test "it retrieves active oracles first", %{conn: conn} do
Model.oracle(index: pk) = oracle = TS.oracle()
Expand Down Expand Up @@ -234,4 +235,158 @@ defmodule AeMdwWeb.OracleControllerTest do
end
end
end

describe "oracle_queries" do
test "it retrieves all oracle queries", %{conn: conn, store: store} do
oracle_pk = <<1::256>>
oracle_pk2 = <<2::256>>
query_id1 = <<3::256>>
query_id2 = <<4::256>>
query_id3 = <<5::256>>
query_id4 = <<6::256>>
account_pk1 = <<7::256>>
account_pk2 = <<8::256>>
oracle_id = :aeser_id.create(:oracle, oracle_pk)
account_id1 = :aeser_id.create(:account, account_pk1)
account_id2 = :aeser_id.create(:account, account_pk2)
encoded_account_id1 = Enc.encode(:account_pubkey, account_pk1)
encoded_account_id2 = Enc.encode(:account_pubkey, account_pk2)
encoded_oracle_pk = Enc.encode(:oracle_pubkey, oracle_pk)
encoded_query_id1 = Enc.encode(:oracle_query_id, query_id1)
encoded_query_id2 = Enc.encode(:oracle_query_id, query_id2)
encoded_query_id3 = Enc.encode(:oracle_query_id, query_id3)
txi_idx1 = {789, -1}
tx_hash1 = <<10::256>>
txi_idx2 = {791, 3}
tx_hash2 = <<11::256>>
txi_idx3 = {799, -1}
tx_hash3 = <<12::256>>
block_hash = <<13::256>>

{:ok, oracle_query_aetx1} =
:aeo_query_tx.new(%{
sender_id: account_id1,
nonce: 1,
oracle_id: oracle_id,
query: "query-1",
query_fee: 11,
query_ttl: {:delta, 111},
response_ttl: {:delta, 1_111},
fee: 11_111
})

{:ok, oracle_query_aetx2} =
:aeo_query_tx.new(%{
sender_id: account_id2,
nonce: 2,
oracle_id: oracle_id,
query: "query-2",
query_fee: 22,
query_ttl: {:delta, 222},
response_ttl: {:delta, 2_222},
fee: 22_222
})

{:ok, oracle_query_aetx3} =
:aeo_query_tx.new(%{
sender_id: account_id1,
nonce: 3,
oracle_id: oracle_id,
query: <<0, 2, 2>>,
query_fee: 33,
query_ttl: {:delta, 333},
response_ttl: {:delta, 3_333},
fee: 33_333
})

{:oracle_query_tx, oracle_query_tx1} = :aetx.specialize_type(oracle_query_aetx1)
{:oracle_query_tx, oracle_query_tx2} = :aetx.specialize_type(oracle_query_aetx2)
{:oracle_query_tx, oracle_query_tx3} = :aetx.specialize_type(oracle_query_aetx3)

store =
store
|> Store.put(
Model.OracleQuery,
Model.oracle_query(index: {oracle_pk, query_id1}, txi_idx: txi_idx1)
)
|> Store.put(Model.Tx, Model.tx(index: 789, id: tx_hash1))
|> Store.put(
Model.OracleQuery,
Model.oracle_query(index: {oracle_pk, query_id2}, txi_idx: txi_idx2)
)
|> Store.put(Model.Tx, Model.tx(index: 791, id: tx_hash2))
|> Store.put(
Model.OracleQuery,
Model.oracle_query(index: {oracle_pk, query_id3}, txi_idx: txi_idx3)
)
|> Store.put(Model.Tx, Model.tx(index: 799, id: tx_hash3))
|> Store.put(Model.OracleQuery, Model.oracle_query(index: {oracle_pk2, query_id4}))

with_mocks [
{DbUtil, [:passthrough],
[
read_node_tx_details: fn
_state, ^txi_idx1 -> {oracle_query_tx1, tx_hash1, :oracle_query_tx, block_hash}
_state, ^txi_idx2 -> {oracle_query_tx2, tx_hash2, :contract_call_tx, block_hash}
_state, ^txi_idx3 -> {oracle_query_tx3, tx_hash3, :oracle_query_tx, block_hash}
end
]}
] do
assert %{"data" => [oracle1, oracle2], "next" => next_url} =
conn
|> with_store(store)
|> get("/v2/oracles/#{encoded_oracle_pk}/queries", direction: "forward", limit: 2)
|> json_response(200)

assert %{
"oracle_id" => ^encoded_oracle_pk,
"query_id" => ^encoded_query_id1,
"nonce" => 1,
"query_fee" => 11,
"sender_id" => ^encoded_account_id1,
"source_tx_type" => "OracleQueryTx",
"query" => "cXVlcnktMQ",
"fee" => 11_111
} = oracle1

assert %{
"oracle_id" => ^encoded_oracle_pk,
"query_id" => ^encoded_query_id2,
"nonce" => 2,
"query_fee" => 22,
"sender_id" => ^encoded_account_id2,
"source_tx_type" => "ContractCallTx",
"query" => "cXVlcnktMg",
"fee" => 22_222
} = oracle2

assert %{"data" => [oracle3], "next" => nil} =
conn
|> with_store(store)
|> get(next_url)
|> json_response(200)

assert %{
"oracle_id" => ^encoded_oracle_pk,
"query_id" => ^encoded_query_id3,
"nonce" => 3,
"query_fee" => 33,
"sender_id" => ^encoded_account_id1,
"source_tx_type" => "OracleQueryTx",
"query" => "AAIC",
"fee" => 33_333
} = oracle3
end
end

test "cursor is invalid, it displays error", %{conn: conn} do
oracle_pk = <<1::256>>
encoded_oracle_pk = Enc.encode(:oracle_pubkey, oracle_pk)

assert %{"error" => "invalid cursor: foo"} =
conn
|> get("/v2/oracles/#{encoded_oracle_pk}/queries", cursor: "foo")
|> json_response(400)
end
end
end

0 comments on commit f8f2b7d

Please sign in to comment.