diff --git a/.gitignore b/.gitignore index 248e513622..3d4cf941e9 100644 --- a/.gitignore +++ b/.gitignore @@ -69,9 +69,14 @@ data/ # local test setup localchain_contract_addresses.env + +# the famous mac .DS_Store +.DS_Store + # IntelliJ files .idea/ *.iml #vs code -.tool_versions \ No newline at end of file +.tool_versions + diff --git a/Makefile b/Makefile index 20e289f9d5..49b96161c9 100644 --- a/Makefile +++ b/Makefile @@ -27,9 +27,11 @@ help: @echo "DOCKER DEVELOPMENT" @echo "------------------" @echo "" - @echo " - \`make docker-build-cluster\`: build child_chain, watcher and watcher_info images \c" + @echo " - \`make docker-build-start-cluster\`: build child_chain, watcher and watcher_info images \c" @echo "from your current code base, then start a cluster with these freshly built images." @echo "" + @echo " - \`make docker-build\`" build child_chain, watcher and watcher_info images from your current code base + @echo "" @echo " - \`make docker-update-watcher\`, \`make docker-update-watcher_info\` or \c" @echo "\`make docker-update-child_chain\`: replaces containers with your code changes\c" @echo "for rapid development." @@ -305,6 +307,8 @@ docker-watcher: docker-watcher-prod docker-watcher-build docker-watcher_info: docker-watcher_info-prod docker-watcher_info-build docker-child_chain: docker-child_chain-prod docker-child_chain-build +docker-build: docker-watcher docker-watcher_info docker-child_chain + docker-push: docker docker push $(CHILD_CHAIN_IMAGE_NAME) docker push $(WATCHER_IMAGE_NAME) @@ -315,7 +319,8 @@ docker-start-cluster: SNAPSHOT=SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_120 make init_test && \ docker-compose build --no-cache && docker-compose up -docker-build-cluster: docker-child_chain docker-watcher docker-watcher_info +docker-build-start-cluster: + $(MAKE) docker-build SNAPSHOT=SNAPSHOT_MIX_EXIT_PERIOD_SECONDS_120 make init_test && \ docker-compose build --no-cache && docker-compose up diff --git a/apps/omg_utils/lib/omg_utils/http_rpc/validators/base.ex b/apps/omg_utils/lib/omg_utils/http_rpc/validators/base.ex index 3119ef8a3d..f25b714fa5 100644 --- a/apps/omg_utils/lib/omg_utils/http_rpc/validators/base.ex +++ b/apps/omg_utils/lib/omg_utils/http_rpc/validators/base.ex @@ -145,6 +145,12 @@ defmodule OMG.Utils.HttpRPC.Validator.Base do def greater({val, []}, _b) when not is_integer(val), do: {val, [:integer]} def greater({val, []}, bound), do: {val, greater: bound} + @spec lesser({any(), list()}, integer()) :: {any(), list()} + def lesser({_, [_ | _]} = err, _b), do: err + def lesser({val, []}, bound) when is_integer(val) and val < bound, do: {val, []} + def lesser({val, []}, _b) when not is_integer(val), do: {val, [:integer]} + def lesser({val, []}, bound), do: {val, lesser: bound} + @spec list({any(), list()}, function() | nil) :: {any(), list()} def list(tuple, mapper \\ nil) def list({_, [_ | _]} = err, _), do: err diff --git a/apps/omg_utils/lib/omg_utils/paginator.ex b/apps/omg_utils/lib/omg_utils/paginator.ex index c97594a642..e2c88150f1 100644 --- a/apps/omg_utils/lib/omg_utils/paginator.ex +++ b/apps/omg_utils/lib/omg_utils/paginator.ex @@ -19,8 +19,8 @@ defmodule OMG.Utils.Paginator do @default_limit 200 @first_page 1 - @type t() :: %__MODULE__{ - data: list(), + @type t(data_type) :: %__MODULE__{ + data: list(data_type), data_paging: %{limit: pos_integer(), page: pos_integer()} } @@ -45,6 +45,6 @@ defmodule OMG.Utils.Paginator do %__MODULE__{data: [], data_paging: data_paging} end - @spec set_data(list(), t()) :: t() + @spec set_data(list(), t(%__MODULE__{})) :: t(%__MODULE__{}) def set_data(data, paginator) when is_list(data), do: %__MODULE__{paginator | data: data} end diff --git a/apps/omg_watcher/test/omg_watcher/integration/block_getter_test.exs b/apps/omg_watcher/test/omg_watcher/integration/block_getter_test.exs index cf32518ae7..3961efe1f9 100644 --- a/apps/omg_watcher/test/omg_watcher/integration/block_getter_test.exs +++ b/apps/omg_watcher/test/omg_watcher/integration/block_getter_test.exs @@ -77,7 +77,7 @@ defmodule OMG.Watcher.Integration.BlockGetterTest do |> Enum.map(fn utxo -> Map.take(utxo, fields) end) utxos = - alice.addr + %{address: alice.addr} |> WatcherHelper.get_utxos() |> Enum.map(fn utxo -> Map.take(utxo, fields) end) diff --git a/apps/omg_watcher/test/support/watcher_helper.ex b/apps/omg_watcher/test/support/watcher_helper.ex index 1f575463df..57d7d88283 100644 --- a/apps/omg_watcher/test/support/watcher_helper.ex +++ b/apps/omg_watcher/test/support/watcher_helper.ex @@ -125,8 +125,16 @@ defmodule Support.WatcherHelper do |> Map.get("amount") end - def get_utxos(address) do - success?("/account.get_utxos", %{"address" => Encoding.to_hex(address)}) + def get_utxos(params) when is_map(params) do + hex_string_address = Encoding.to_hex(params.address) + success?("/account.get_utxos", %{params | address: hex_string_address}) + end + + @doc """ + shortcut helper for get_utxos that inject pagination data for you + """ + def get_utxos(address, page \\ 1, limit \\ 100) do + success?("/account.get_utxos", %{"address" => Encoding.to_hex(address), "page" => page, "limit" => limit}) end def get_exitable_utxos(address) do diff --git a/apps/omg_watcher_info/lib/omg_watcher_info/api/account.ex b/apps/omg_watcher_info/lib/omg_watcher_info/api/account.ex index 60d344aafe..e9d6b628a8 100644 --- a/apps/omg_watcher_info/lib/omg_watcher_info/api/account.ex +++ b/apps/omg_watcher_info/lib/omg_watcher_info/api/account.ex @@ -16,7 +16,7 @@ defmodule OMG.WatcherInfo.API.Account do @moduledoc """ Module provides operations related to plasma accounts. """ - + alias OMG.Utils.Paginator alias OMG.WatcherInfo.DB @doc """ @@ -30,8 +30,8 @@ defmodule OMG.WatcherInfo.API.Account do @doc """ Gets all utxos belonging to the given address. """ - @spec get_utxos(OMG.Crypto.address_t()) :: list(%DB.TxOutput{}) - def get_utxos(address) do - DB.TxOutput.get_utxos(address) + @spec get_utxos(Keyword.t()) :: Paginator.t(%DB.TxOutput{}) + def get_utxos(params) do + DB.TxOutput.get_utxos(params) end end diff --git a/apps/omg_watcher_info/lib/omg_watcher_info/api/block.ex b/apps/omg_watcher_info/lib/omg_watcher_info/api/block.ex index 2c00b535e3..7534b690fb 100644 --- a/apps/omg_watcher_info/lib/omg_watcher_info/api/block.ex +++ b/apps/omg_watcher_info/lib/omg_watcher_info/api/block.ex @@ -37,7 +37,7 @@ defmodule OMG.WatcherInfo.API.Block do Retrieves a list of blocks. Length of the list is limited by `limit` and `page` arguments. """ - @spec get_blocks(Keyword.t()) :: Paginator.t() + @spec get_blocks(Keyword.t()) :: Paginator.t(%DB.Block{}) def get_blocks(constraints) do paginator = Paginator.from_constraints(constraints, @default_blocks_limit) diff --git a/apps/omg_watcher_info/lib/omg_watcher_info/api/transaction.ex b/apps/omg_watcher_info/lib/omg_watcher_info/api/transaction.ex index bfb34d9f76..d4236f33e7 100644 --- a/apps/omg_watcher_info/lib/omg_watcher_info/api/transaction.ex +++ b/apps/omg_watcher_info/lib/omg_watcher_info/api/transaction.ex @@ -23,7 +23,6 @@ defmodule OMG.WatcherInfo.API.Transaction do alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.HttpRPC.Client alias OMG.WatcherInfo.UtxoSelection - require Utxo @default_transactions_limit 200 @@ -46,7 +45,7 @@ defmodule OMG.WatcherInfo.API.Transaction do Length of the list is limited by `limit` argument """ - @spec get_transactions(Keyword.t()) :: Paginator.t() + @spec get_transactions(Keyword.t()) :: Paginator.t(%DB.Transaction{}) def get_transactions(constraints) do paginator = Paginator.from_constraints(constraints, @default_transactions_limit) diff --git a/apps/omg_watcher_info/lib/omg_watcher_info/db/block.ex b/apps/omg_watcher_info/lib/omg_watcher_info/db/block.ex index 2d57250c44..fc50abee30 100644 --- a/apps/omg_watcher_info/lib/omg_watcher_info/db/block.ex +++ b/apps/omg_watcher_info/lib/omg_watcher_info/db/block.ex @@ -87,7 +87,7 @@ defmodule OMG.WatcherInfo.DB.Block do @doc """ Returns a list of blocks """ - @spec get_blocks(Paginator.t()) :: Paginator.t() + @spec get_blocks(Paginator.t(%DB.Block{})) :: Paginator.t(%DB.Block{}) def get_blocks(paginator) do query_get_last(paginator.data_paging) |> DB.Repo.all() diff --git a/apps/omg_watcher_info/lib/omg_watcher_info/db/transaction.ex b/apps/omg_watcher_info/lib/omg_watcher_info/db/transaction.ex index cf11b565b3..aa14c32f72 100644 --- a/apps/omg_watcher_info/lib/omg_watcher_info/db/transaction.ex +++ b/apps/omg_watcher_info/lib/omg_watcher_info/db/transaction.ex @@ -66,7 +66,7 @@ defmodule OMG.WatcherInfo.DB.Transaction do Returns transactions possibly filtered by constraints * constraints - accepts keyword in the form of [schema_field: value] """ - @spec get_by_filters(Keyword.t(), Paginator.t()) :: Paginator.t() + @spec get_by_filters(Keyword.t(), Paginator.t(%__MODULE__{})) :: Paginator.t(%__MODULE__{}) def get_by_filters(constraints, paginator) do allowed_constraints = [:address, :blknum, :txindex, :txtypes, :metadata] diff --git a/apps/omg_watcher_info/lib/omg_watcher_info/db/txoutput.ex b/apps/omg_watcher_info/lib/omg_watcher_info/db/txoutput.ex index 71264ce97f..86f79c5755 100644 --- a/apps/omg_watcher_info/lib/omg_watcher_info/db/txoutput.ex +++ b/apps/omg_watcher_info/lib/omg_watcher_info/db/txoutput.ex @@ -21,6 +21,7 @@ defmodule OMG.WatcherInfo.DB.TxOutput do use Ecto.Schema alias OMG.State.Transaction + alias OMG.Utils.Paginator alias OMG.Utxo alias OMG.WatcherInfo.DB alias OMG.WatcherInfo.DB.Repo @@ -29,6 +30,8 @@ defmodule OMG.WatcherInfo.DB.TxOutput do import Ecto.Query, only: [from: 2, where: 2] + @default_get_utxos_limit 200 + @type balance() :: %{ currency: binary(), amount: non_neg_integer() @@ -79,24 +82,18 @@ defmodule OMG.WatcherInfo.DB.TxOutput do ) end - def get_utxos(owner) do - query = - from( - txoutput in __MODULE__, - preload: [:ethevents], - left_join: ethevent in assoc(txoutput, :ethevents), - # select txoutputs by owner that have neither been spent nor have a corresponding ethevents exit events - where: txoutput.owner == ^owner and is_nil(txoutput.spending_txhash) and (is_nil(ethevent) or fragment(" - NOT EXISTS (SELECT 1 - FROM ethevents_txoutputs AS etfrag - JOIN ethevents AS efrag ON - etfrag.root_chain_txhash_event=efrag.root_chain_txhash_event - AND efrag.event_type IN (?) - AND etfrag.child_chain_utxohash = ?)", "standard_exit", txoutput.child_chain_utxohash)), - order_by: [asc: :blknum, asc: :txindex, asc: :oindex] - ) - - Repo.all(query) + @spec get_utxos(keyword) :: OMG.Utils.Paginator.t(%__MODULE__{}) + def get_utxos(params) do + address = Keyword.fetch!(params, :address) + paginator = Paginator.from_constraints(params, @default_get_utxos_limit) + %{limit: limit, page: page} = paginator.data_paging + offset = (page - 1) * limit + + address + |> query_get_utxos() + |> from(limit: ^limit, offset: ^offset) + |> Repo.all() + |> Paginator.set_data(paginator) end @spec get_balance(OMG.Crypto.address_t()) :: list(balance()) @@ -192,9 +189,33 @@ defmodule OMG.WatcherInfo.DB.TxOutput do @spec get_sorted_grouped_utxos(OMG.Crypto.address_t()) :: %{OMG.Crypto.address_t() => list(%__MODULE__{})} def get_sorted_grouped_utxos(owner) do # TODO: use clever DB query to get following out of DB - get_utxos(owner) + owner + |> get_all_utxos() |> Enum.group_by(& &1.currency) |> Enum.map(fn {k, v} -> {k, Enum.sort_by(v, & &1.amount, &>=/2)} end) |> Map.new() end + + defp query_get_utxos(address) do + from( + txoutput in __MODULE__, + preload: [:ethevents], + left_join: ethevent in assoc(txoutput, :ethevents), + # select txoutputs by owner that have neither been spent nor have a corresponding ethevents exit events + where: txoutput.owner == ^address and is_nil(txoutput.spending_txhash) and (is_nil(ethevent) or fragment(" +NOT EXISTS (SELECT 1 + FROM ethevents_txoutputs AS etfrag + JOIN ethevents AS efrag ON + etfrag.root_chain_txhash_event=efrag.root_chain_txhash_event + AND efrag.event_type IN (?) + AND etfrag.child_chain_utxohash = ?)", "standard_exit", txoutput.child_chain_utxohash)), + order_by: [asc: :blknum, asc: :txindex, asc: :oindex] + ) + end + + @spec get_all_utxos(OMG.Crypto.address_t()) :: list() + defp get_all_utxos(address) do + query = query_get_utxos(address) + Repo.all(query) + end end diff --git a/apps/omg_watcher_info/test/omg_watcher_info/db/eth_event_test.exs b/apps/omg_watcher_info/test/omg_watcher_info/db/eth_event_test.exs index 51a8a4d85c..21aad59753 100644 --- a/apps/omg_watcher_info/test/omg_watcher_info/db/eth_event_test.exs +++ b/apps/omg_watcher_info/test/omg_watcher_info/db/eth_event_test.exs @@ -189,9 +189,10 @@ defmodule OMG.WatcherInfo.DB.EthEventTest do root_chain_txhash_event: ^expected_root_chain_txhash_event_3 } = DB.EthEvent.get(expected_root_chain_txhash_event_3) + %{data: alice_utxos} = DB.TxOutput.get_utxos(address: alice.addr) + assert [^expected_root_chain_txhash_1, ^expected_root_chain_txhash_2, ^expected_root_chain_txhash_3] = - DB.TxOutput.get_utxos(alice.addr) - |> Enum.map(fn txoutput -> + Enum.map(alice_utxos, fn txoutput -> [head | _tail] = txoutput.ethevents head.root_chain_txhash end) @@ -225,7 +226,8 @@ defmodule OMG.WatcherInfo.DB.EthEventTest do } ]) - assert length(DB.TxOutput.get_utxos(expected_owner)) == 1 + %{data: utxos} = DB.TxOutput.get_utxos(address: expected_owner) + assert length(utxos) == 1 assert :ok = DB.EthEvent.insert_exits!([ @@ -236,7 +238,8 @@ defmodule OMG.WatcherInfo.DB.EthEventTest do } ]) - assert Enum.empty?(DB.TxOutput.get_utxos(expected_owner)) + %{data: utxos_after_exit} = DB.TxOutput.get_utxos(address: expected_owner) + assert Enum.empty?(utxos_after_exit) end @tag fixtures: [:alice, :initial_blocks] diff --git a/apps/omg_watcher_rpc/lib/web/controllers/account.ex b/apps/omg_watcher_rpc/lib/web/controllers/account.ex index 13d752e580..e8ed29030e 100644 --- a/apps/omg_watcher_rpc/lib/web/controllers/account.ex +++ b/apps/omg_watcher_rpc/lib/web/controllers/account.ex @@ -21,6 +21,7 @@ defmodule OMG.WatcherRPC.Web.Controller.Account do alias OMG.Watcher.API, as: SecurityAPI alias OMG.WatcherInfo.API, as: InfoAPI + alias OMG.WatcherRPC.Web.Validator.AccountConstraints @doc """ Gets plasma account balance @@ -34,8 +35,8 @@ defmodule OMG.WatcherRPC.Web.Controller.Account do end def get_utxos(conn, params) do - with {:ok, address} <- expect(params, "address", :address) do - address + with {:ok, constraints} <- AccountConstraints.parse(params) do + constraints |> InfoAPI.Account.get_utxos() |> api_response(conn, :utxos) end @@ -45,7 +46,7 @@ defmodule OMG.WatcherRPC.Web.Controller.Account do with {:ok, address} <- expect(params, "address", :address) do address |> SecurityAPI.Account.get_exitable_utxos() - |> api_response(conn, :utxos) + |> api_response(conn, :exitable_utxos) end end end diff --git a/apps/omg_watcher_rpc/lib/web/validators/account_constraints.ex b/apps/omg_watcher_rpc/lib/web/validators/account_constraints.ex new file mode 100644 index 0000000000..991d16d58a --- /dev/null +++ b/apps/omg_watcher_rpc/lib/web/validators/account_constraints.ex @@ -0,0 +1,35 @@ +# Copyright 2019-2020 OmiseGO Pte Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defmodule OMG.WatcherRPC.Web.Validator.AccountConstraints do + @moduledoc """ + Validates `/account.get_utxos` query parameters + """ + + alias OMG.WatcherRPC.Web.Validator.Helpers + + @doc """ + Validates possible query constraints, stops on first error. + """ + @spec parse(%{binary() => any()}) :: {:ok, Keyword.t()} | {:error, any()} + def parse(params) do + constraints = [ + {"limit", [pos_integer: true, lesser: 1000, optional: true], :limit}, + {"page", [:pos_integer, :optional], :page}, + {"address", [:address], :address} + ] + + Helpers.validate_constraints(params, constraints) + end +end diff --git a/apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex b/apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex index 4f481033c9..fd6a11460d 100644 --- a/apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex +++ b/apps/omg_watcher_rpc/lib/web/validators/block_constraints.ex @@ -16,8 +16,7 @@ defmodule OMG.WatcherRPC.Web.Validator.BlockConstraints do @moduledoc """ Validates `/block.all` query parameters """ - - import OMG.Utils.HttpRPC.Validator.Base, only: [expect: 3] + alias OMG.WatcherRPC.Web.Validator.Helpers @doc """ Validates possible query constraints, stops on first error. @@ -25,16 +24,10 @@ defmodule OMG.WatcherRPC.Web.Validator.BlockConstraints do @spec parse(%{binary() => any()}) :: {:ok, Keyword.t()} | {:error, any()} def parse(params) do constraints = [ - {"limit", [:pos_integer, :optional]}, - {"page", [:pos_integer, :optional]} + {"limit", [pos_integer: true, lesser: 1000, optional: true], :limit}, + {"page", [:pos_integer, :optional], :page} ] - Enum.reduce_while(constraints, {:ok, []}, fn {key, validators}, {:ok, list} -> - case expect(params, key, validators) do - {:ok, nil} -> {:cont, {:ok, list}} - {:ok, value} -> {:cont, {:ok, [{String.to_existing_atom(key), value} | list]}} - error -> {:halt, error} - end - end) + Helpers.validate_constraints(params, constraints) end end diff --git a/apps/omg_watcher_rpc/lib/web/validators/helpers.ex b/apps/omg_watcher_rpc/lib/web/validators/helpers.ex new file mode 100644 index 0000000000..9e8bea4a3d --- /dev/null +++ b/apps/omg_watcher_rpc/lib/web/validators/helpers.ex @@ -0,0 +1,39 @@ +# Copyright 2019-2020 OmiseGO Pte Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defmodule OMG.WatcherRPC.Web.Validator.Helpers do + @moduledoc """ + helper for validators + """ + import OMG.Utils.HttpRPC.Validator.Base, only: [expect: 3] + + @doc """ + Validates possible params with query constraints, stops on first error. + """ + @spec validate_constraints(%{binary() => any()}, list()) :: {:ok, Keyword.t()} | {:error, any()} + def validate_constraints(params, constraints) do + Enum.reduce_while(constraints, {:ok, []}, fn {key, validators, atom}, {:ok, list} -> + case expect(params, key, validators) do + {:ok, nil} -> + {:cont, {:ok, list}} + + {:ok, value} -> + {:cont, {:ok, [{atom, value} | list]}} + + error -> + {:halt, error} + end + end) + end +end diff --git a/apps/omg_watcher_rpc/lib/web/validators/transaction_constraints.ex b/apps/omg_watcher_rpc/lib/web/validators/transaction_constraints.ex index ca45430f85..26a0ccb6b9 100644 --- a/apps/omg_watcher_rpc/lib/web/validators/transaction_constraints.ex +++ b/apps/omg_watcher_rpc/lib/web/validators/transaction_constraints.ex @@ -16,9 +16,8 @@ defmodule OMG.WatcherRPC.Web.Validator.TransactionConstraints do @moduledoc """ Validates `/transaction.all` query parameters """ - - import OMG.Utils.HttpRPC.Validator.Base - + import OMG.Utils.HttpRPC.Validator.Base, only: [expect: 3] + alias OMG.WatcherRPC.Web.Validator.Helpers @max_tx_types 16 @doc """ @@ -31,22 +30,11 @@ defmodule OMG.WatcherRPC.Web.Validator.TransactionConstraints do {"blknum", [:pos_integer, :optional], :blknum}, {"metadata", [:hash, :optional], :metadata}, {"txtypes", [list: &to_tx_type/1, max_length: @max_tx_types, optional: true], :txtypes}, - {"limit", [:pos_integer, :optional], :limit}, + {"limit", [pos_integer: true, lesser: 1000, optional: true], :limit}, {"page", [:pos_integer, :optional], :page} ] - Enum.reduce_while(constraints, {:ok, []}, fn {key, validators, atom}, {:ok, list} -> - case expect(params, key, validators) do - {:ok, nil} -> - {:cont, {:ok, list}} - - {:ok, value} -> - {:cont, {:ok, [{atom, value} | list]}} - - error -> - {:halt, error} - end - end) + Helpers.validate_constraints(params, constraints) end defp to_tx_type(tx_type_str) do diff --git a/apps/omg_watcher_rpc/lib/web/views/account.ex b/apps/omg_watcher_rpc/lib/web/views/account.ex index d7c128b602..76d992e462 100644 --- a/apps/omg_watcher_rpc/lib/web/views/account.ex +++ b/apps/omg_watcher_rpc/lib/web/views/account.ex @@ -19,6 +19,7 @@ defmodule OMG.WatcherRPC.Web.View.Account do use OMG.WatcherRPC.Web, :view alias OMG.Utils.HttpRPC.Response + alias OMG.Utils.Paginator alias OMG.Utxo alias OMG.WatcherRPC.Web.Response, as: WatcherRPCResponse @@ -30,7 +31,14 @@ defmodule OMG.WatcherRPC.Web.View.Account do |> WatcherRPCResponse.add_app_infos() end - def render("utxos.json", %{response: utxos}) do + def render("utxos.json", %{response: %Paginator{data: utxos, data_paging: data_paging}}) do + utxos + |> Enum.map(&to_utxo/1) + |> Response.serialize_page(data_paging) + |> WatcherRPCResponse.add_app_infos() + end + + def render("exitable_utxos.json", %{response: utxos}) do utxos |> Enum.map(&to_utxo/1) |> Response.serialize() diff --git a/apps/omg_watcher_rpc/priv/swagger/info_api_specs.yaml b/apps/omg_watcher_rpc/priv/swagger/info_api_specs.yaml index 5e14c23bff..a6995afba9 100644 --- a/apps/omg_watcher_rpc/priv/swagger/info_api_specs.yaml +++ b/apps/omg_watcher_rpc/priv/swagger/info_api_specs.yaml @@ -307,7 +307,7 @@ paths: summary: Returns the balance of each currency for the given account address. operationId: account_get_balance requestBody: - description: HEX-encoded address of the account + description: HEX-encoded address of the account and pagination fields required: true content: application/json: @@ -317,10 +317,20 @@ paths: properties: address: type: string + page: + type: integer + format: int32 + default: 1 + limit: + type: integer + format: int32 + default: 200 required: - address example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' + limit: 100 + page: 2 responses: '200': description: Account balance successful response @@ -431,7 +441,7 @@ paths: summary: Gets all utxos belonging to the given address. operationId: account_get_utxos requestBody: - description: HEX-encoded address of the account + description: HEX-encoded address of the account and pagination fields required: true content: application/json: @@ -441,10 +451,20 @@ paths: properties: address: type: string + page: + type: integer + format: int32 + default: 1 + limit: + type: integer + format: int32 + default: 200 required: - address example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' + limit: 100 + page: 2 responses: '200': description: Account utxos succcessful response @@ -510,6 +530,17 @@ paths: type: string updated_at: type: string + data_paging: + type: object + properties: + page: + type: integer + format: int32 + default: 1 + limit: + type: integer + format: int32 + default: 200 example: data: - amount: 10 @@ -524,6 +555,9 @@ paths: txindex: 111 updated_at: '2020-02-15T04:07:57Z' utxo_pos: 123000001110000 + data_paging: + page: 1 + limit: 200 '500': description: Returns an internal server error content: diff --git a/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/request_bodies.yaml b/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/request_bodies.yaml index bd8522b3c1..50a7a0f949 100644 --- a/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/request_bodies.yaml +++ b/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/request_bodies.yaml @@ -1,5 +1,5 @@ AddressBodySchema: - description: HEX-encoded address of the account + description: HEX-encoded address of the account and pagination fields required: true content: application/json: @@ -9,8 +9,18 @@ AddressBodySchema: properties: address: type: string + page: + type: integer + format: int32 + default: 1 + limit: + type: integer + format: int32 + default: 200 required: - address example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' + limit: 100 + page: 2 diff --git a/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/response_schemas.yaml b/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/response_schemas.yaml index 6141c56c01..9226d33731 100644 --- a/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/response_schemas.yaml +++ b/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/response_schemas.yaml @@ -25,6 +25,17 @@ AccountUtxoResponseSchema: type: array items: $ref: 'schemas.yaml#/AccountUtxoSchema' + data_paging: + type: object + properties: + page: + type: integer + format: int32 + default: 1 + limit: + type: integer + format: int32 + default: 200 example: data: - @@ -40,3 +51,6 @@ AccountUtxoResponseSchema: txindex: 111 updated_at: '2020-02-15T04:07:57Z' utxo_pos: 123000001110000 + data_paging: + page: 1 + limit: 200 \ No newline at end of file diff --git a/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/responses.yaml b/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/responses.yaml index 9905aeec4c..2d6d0c9a44 100644 --- a/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/responses.yaml +++ b/apps/omg_watcher_rpc/priv/swagger/info_api_specs/account/responses.yaml @@ -4,7 +4,6 @@ AccountBalanceResponse: application/json: schema: $ref: 'response_schemas.yaml#/AccountBalanceResponseSchema' - AccountUtxoResponse: description: Account utxos succcessful response content: diff --git a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/account/request_bodies.yaml b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/account/request_bodies.yaml index bd8522b3c1..5eb7181276 100644 --- a/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/account/request_bodies.yaml +++ b/apps/omg_watcher_rpc/priv/swagger/security_critical_api_specs/account/request_bodies.yaml @@ -14,3 +14,4 @@ AddressBodySchema: - address example: address: '0xb3256026863eb6ae5b06fa396ab09069784ea8ea' + \ No newline at end of file diff --git a/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/account_contraints_test.exs b/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/account_contraints_test.exs new file mode 100644 index 0000000000..68db12ad13 --- /dev/null +++ b/apps/omg_watcher_rpc/test/omg_watcher_rpc/web/validators/account_contraints_test.exs @@ -0,0 +1,74 @@ +# Copyright 2019-2020 OmiseGO Pte Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defmodule OMG.WatcherRPC.Web.Validator.AccountConstraintsTest do + @moduledoc """ + Account constraints validate test + """ + use ExUnit.Case, async: true + + alias OMG.Eth.Encoding + alias OMG.WatcherRPC.Web.Validator.AccountConstraints + + @fake_address_hex_string "0x7977fe798feef376b74b6c1c5ebce8a2ccf02afd" + + describe("parse/1") do + test "returns page, limit and adress constraints when given page, limit and adress" do + request_data = %{ + "page" => 1, + "limit" => 100, + "address" => @fake_address_hex_string + } + + {:ok, constraints} = AccountConstraints.parse(request_data) + + assert constraints == [ + address: Encoding.from_hex(@fake_address_hex_string), + page: 1, + limit: 100 + ] + end + + test "return error if does not provide address" do + request_data = %{ + "page" => 1, + "limit" => 100 + } + + assert AccountConstraints.parse(request_data) == {:error, {:validation_error, "address", :hex}} + end + + test "return error if limit exceed 1000" do + request_data = %{ + "address" => @fake_address_hex_string, + "page" => 1, + "limit" => 2200 + } + + assert AccountConstraints.parse(request_data) == {:error, {:validation_error, "limit", {:lesser, 1000}}} + end + + test "return address if only address is provided" do + request_data = %{ + "address" => @fake_address_hex_string + } + + {:ok, constraints} = AccountConstraints.parse(request_data) + + assert constraints == [ + address: Encoding.from_hex(@fake_address_hex_string) + ] + end + end +end diff --git a/mix.lock b/mix.lock index e4518ab3fb..f6ddecb5bf 100644 --- a/mix.lock +++ b/mix.lock @@ -56,7 +56,7 @@ "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, "phoenix": {:hex, :phoenix, "1.4.16", "2cbbe0c81e6601567c44cc380c33aa42a1372ac1426e3de3d93ac448a7ec4308", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "856cc1a032fa53822737413cf51aa60e750525d7ece7d1c0576d90d7c0f05c24"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm", "1f13f9f0f3e769a667a6b6828d29dec37497a082d195cc52dbef401a9b69bf38"}, - "plug": {:hex, :plug, "1.9.0", "8d7c4e26962283ff9f8f3347bd73838e2413fbc38b7bb5467d5924f68f3a5a4a", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "9902eda2c52ada2a096434682e99a2493f5d06a94d6ac6bcfff9805f952350f1"}, + "plug": {:hex, :plug, "1.10.0", "6508295cbeb4c654860845fb95260737e4a8838d34d115ad76cd487584e2fc4d", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "422a9727e667be1bf5ab1de03be6fa0ad67b775b2d84ed908f3264415ef29d4a"}, "plug_cowboy": {:hex, :plug_cowboy, "1.0.0", "2e2a7d3409746d335f451218b8bb0858301c3de6d668c3052716c909936eb57a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "01d201427a8a1f4483be2465a98b45f5e82263327507fe93404a61c51eb9e9a8"}, "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, diff --git a/priv/cabbage/apps/itest/lib/client.ex b/priv/cabbage/apps/itest/lib/client.ex index 239c77cb90..f282de8973 100644 --- a/priv/cabbage/apps/itest/lib/client.ex +++ b/priv/cabbage/apps/itest/lib/client.ex @@ -15,7 +15,6 @@ defmodule Itest.Client do @moduledoc """ An interface to Watcher API. """ - alias Itest.ApiModel.Utxo alias Itest.Transactions.Currency alias Itest.Transactions.Deposit alias Itest.Transactions.Encoding @@ -96,10 +95,15 @@ defmodule Itest.Client do submit_typed(typed_data_signed) end - def get_utxos(address) do - payload = %AddressBodySchema1{address: address} - {:ok, response} = Account.account_get_utxos(WatcherInfo.new(), payload) - Poison.decode!(response.body, as: %{"data" => [%Utxo{}]})["data"] + def get_utxos(params) do + default_paging = %{page: 1, limit: 200} + %{address: address, page: page, limit: limit} = Map.merge(default_paging, params) + + {:ok, response} = + Account.account_get_utxos(WatcherInfo.new(), %AddressBodySchema1{address: address, page: page, limit: limit}) + + data = Jason.decode!(response.body) + {:ok, data} end def get_gas_used(receipt_hash), do: Itest.Gas.get_gas_used(receipt_hash) diff --git a/priv/cabbage/apps/itest/test/features/watcher_info_api.feature b/priv/cabbage/apps/itest/test/features/watcher_info_api.feature new file mode 100644 index 0000000000..814bba79c0 --- /dev/null +++ b/priv/cabbage/apps/itest/test/features/watcher_info_api.feature @@ -0,0 +1,7 @@ +Feature: Watcher info + + Scenario: Alice wants to use retrieve her UTXO information after deposit + When Alice deposits "1" ETH to the root chain creating 1 UTXO + Then Alice is able to paginate her single UTXO + When Alice deposits another "2" ETH to the root chain creating second UTXO + Then Alice is able to paginate 2 UTXOs correctly diff --git a/priv/cabbage/apps/itest/test/itest/watcher_info_api_test.exs b/priv/cabbage/apps/itest/test/itest/watcher_info_api_test.exs new file mode 100644 index 0000000000..57de5cf063 --- /dev/null +++ b/priv/cabbage/apps/itest/test/itest/watcher_info_api_test.exs @@ -0,0 +1,102 @@ +# Copyright 2019-2020 OmiseGO Pte Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +defmodule WatcherInfoApiTest do + use Cabbage.Feature, async: true, file: "watcher_info_api.feature" + + require Logger + + alias Itest.Account + alias Itest.ApiModel.WatcherSecurityCriticalConfiguration + alias Itest.Client + alias Itest.Transactions.Currency + + @geth_block_every 1 + @to_milliseconds 1000 + + setup do + accounts = Account.take_accounts(1) + alice_account = Enum.at(accounts, 0) + %{alice_account: alice_account} + end + + defgiven ~r/^Alice deposits "(?[^"]+)" ETH to the root chain creating 1 UTXO$/, + %{amount: amount}, + %{alice_account: alice_account} = state do + {alice_addr, _alice_priv} = alice_account + + {:ok, _} = Client.deposit(Currency.to_wei(amount), alice_addr, Itest.PlasmaFramework.vault(Currency.ether())) + wait_for_balance_equal(alice_addr, Currency.to_wei(Currency.to_wei(1))) + {:ok, state} + end + + defthen ~r/^Alice is able to paginate her single UTXO$/, + _, + %{alice_account: alice_account} = state do + {alice_addr, _alice_priv} = alice_account + + {:ok, data} = Client.get_utxos(%{address: alice_addr, page: 1, limit: 10}) + %{"data" => utxos, "data_paging" => data_paging} = data + assert_equal(1, length(utxos), "for depositing 1 tx") + assert_equal(Currency.to_wei(1), Enum.at(utxos, 0)["amount"], "for first utxo") + assert_equal(true, Map.equal?(data_paging, %{"page" => 1, "limit" => 10}), "as data_paging") + {:ok, state} + end + + defthen ~r/^Alice deposits another "(?[^"]+)" ETH to the root chain creating second UTXO$/, + _, + %{alice_account: alice_account} = state do + {alice_addr, _alice_priv} = alice_account + {:ok, _} = Client.deposit(Currency.to_wei(2), alice_addr, Itest.PlasmaFramework.vault(Currency.ether())) + wait_for_balance_equal(alice_addr, Currency.to_wei(1 + 2)) + {:ok, state} + end + + defthen ~r/^Alice is able to paginate 2 UTXOs correctly$/, + _, + %{alice_account: alice_account} do + {alice_addr, _alice_priv} = alice_account + {:ok, data} = Client.get_utxos(%{address: alice_addr, page: 1, limit: 2}) + %{"data" => utxos, "data_paging" => data_paging} = data + assert_equal(2, length(utxos), "for depositing 2 tx") + assert_equal(Currency.to_wei(1), Enum.at(utxos, 0)["amount"], "for first utxo") + assert_equal(Currency.to_wei(2), Enum.at(utxos, 1)["amount"], "for second utxo") + assert_equal(true, Map.equal?(data_paging, %{"page" => 1, "limit" => 2}), "as data_paging") + end + + defp assert_equal(left, right, message) do + assert(left == right, "Expected #{left}, but have #{right}." <> message) + end + + defp wait_for_balance_equal(address, amount) do + {:ok, response} = + WatcherSecurityCriticalAPI.Api.Configuration.configuration_get(WatcherSecurityCriticalAPI.Connection.new()) + + watcher_security_critical_config = + WatcherSecurityCriticalConfiguration.to_struct(Jason.decode!(response.body)["data"]) + + finality_margin_blocks = watcher_security_critical_config.deposit_finality_margin + wait_finality_margin_blocks(finality_margin_blocks) + Itest.Poller.pull_balance_until_amount(address, amount) + end + + defp wait_finality_margin_blocks(finality_margin_blocks) do + # sometimes waiting just 1 margin blocks is not enough + finality_margin_blocks + |> Kernel.*(@geth_block_every) + |> Kernel.*(@to_milliseconds) + |> Kernel.round() + |> Process.sleep() + end +end