From 9825192aba24d88dd53d90093a79f11ee628b402 Mon Sep 17 00:00:00 2001 From: Viktor Baranov Date: Mon, 25 Dec 2023 18:51:55 +0300 Subject: [PATCH] Noves.fi API proxy --- CHANGELOG.md | 1 + .../lib/block_scout_web/api_router.ex | 8 +++ .../controllers/api/v2/address_controller.ex | 8 ++- .../api/v2/proxy/noves_fi_conroller.ex | 61 +++++++++++++++++ .../api/v2/transaction_controller.ex | 7 +- .../third_party_integrations/noves_fi.ex | 66 +++++++++++++++++++ config/runtime.exs | 5 ++ cspell.json | 2 + docker-compose/envs/common-blockscout.env | 63 ++++++++++-------- 9 files changed, 189 insertions(+), 32 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_conroller.ex create mode 100644 apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index b807cbefc0..b96216dbee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - [#9072](https://github.com/blockscout/blockscout/pull/9072) - Add tracing by block logic for geth +- [#9056](https://github.com/blockscout/blockscout/pull/9056) - Noves.fi API proxy ### Fixes diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index 7880bd15af..5f8415014a 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -300,6 +300,14 @@ defmodule BlockScoutWeb.ApiRouter do get("/batches/:batch_number", V2.ZkevmController, :batch) end end + + scope "/proxy" do + scope "/noves-fi" do + get("/transactions/:transaction_hash_param", V2.Proxy.NovesFiController, :transaction) + get("/transactions/:transaction_hash_param/describe", V2.Proxy.NovesFiController, :describe_transaction) + get("/addresses/:address_hash_param/transactions", V2.Proxy.NovesFiController, :address_transactions) + end + end end scope "/v1", as: :api_v1 do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 3a78dcdb67..6794eeb6ad 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -25,7 +25,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.{BlockView, TransactionView, WithdrawalView} alias Explorer.{Chain, Market} - alias Explorer.Chain.{Address, Transaction} + alias Explorer.Chain.{Address, Hash, Transaction} alias Explorer.Chain.Address.Counters alias Explorer.Chain.Token.Instance alias Indexer.Fetcher.{CoinBalanceOnDemand, TokenBalanceOnDemand} @@ -497,7 +497,11 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end - defp validate_address(address_hash_string, params, options \\ @api_true) do + @doc """ + Checks if this valid address hash string, and this address is not prohibited address + """ + @spec validate_address(String.t(), any(), list()) :: {:ok, Hash.Address.t(), Address.t()} + def validate_address(address_hash_string, params, options \\ @api_true) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, address}} <- {:not_found, Chain.hash_to_address(address_hash, options, false)} do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_conroller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_conroller.ex new file mode 100644 index 0000000000..7c0e6a3277 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_conroller.ex @@ -0,0 +1,61 @@ +defmodule BlockScoutWeb.API.V2.Proxy.NovesFiController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.API.V2.{AddressController, TransactionController} + alias Explorer.ThirdPartyIntegrations.NovesFi + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + @doc """ + Function to handle GET requests to `/api/v2/proxy/noves-fi/transactions/:transaction_hash_param` endpoint. + """ + @spec transaction(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def transaction(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + with {:ok, _transaction, _transaction_hash} <- + TransactionController.validate_transaction(transaction_hash_string, params, + necessity_by_association: %{}, + api?: true + ), + url = NovesFi.tx_url(transaction_hash_string), + {response, status} <- NovesFi.noves_fi_api_request(url, conn), + {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do + conn + |> put_status(status) + |> json(response) + end + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/noves-fi/transactions/:transaction_hash_param/describe` endpoint. + """ + @spec describe_transaction(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def describe_transaction(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + with {:ok, _transaction, _transaction_hash} <- + TransactionController.validate_transaction(transaction_hash_string, params, + necessity_by_association: %{}, + api?: true + ), + url = NovesFi.describe_tx_url(transaction_hash_string), + {response, status} <- NovesFi.noves_fi_api_request(url, conn), + {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do + conn + |> put_status(status) + |> json(response) + end + end + + @doc """ + Function to handle GET requests to `/api/v2/proxy/noves-fi/transactions/:transaction_hash_param/describe` endpoint. + """ + @spec address_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def address_transactions(conn, %{"address_hash_param" => address_hash_string} = params) do + with {:ok, _address_hash, _address} <- AddressController.validate_address(address_hash_string, params), + url = NovesFi.address_txs_url(address_hash_string), + {response, status} <- NovesFi.noves_fi_api_request(url, conn), + {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do + conn + |> put_status(status) + |> json(response) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index a1e5134f4e..aabbc21ecb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -28,6 +28,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do alias BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, as: TransactionInterpretationService alias BlockScoutWeb.Models.TransactionStateHelper alias Explorer.Chain + alias Explorer.Chain.{Hash, Transaction} alias Explorer.Chain.Zkevm.Reader alias Indexer.Fetcher.FirstTraceOnDemand @@ -388,7 +389,11 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end - defp validate_transaction(transaction_hash_string, params, options \\ @api_true) do + @doc """ + Checks if this valid transaction hash string, and this transaction doesn't belong to prohibited address + """ + @spec validate_transaction(String.t(), any(), list()) :: {:ok, Transaction.t(), Hash.t()} + def validate_transaction(transaction_hash_string, params, options \\ @api_true) do with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, {:not_found, {:ok, transaction}} <- {:not_found, Chain.hash_to_transaction(transaction_hash, options)}, diff --git a/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex b/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex new file mode 100644 index 0000000000..fef14a2bfd --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex @@ -0,0 +1,66 @@ +defmodule Explorer.ThirdPartyIntegrations.NovesFi do + @moduledoc """ + Module for Noves.Fi API integration https://blockscout.noves.fi/swagger/index.html + """ + + alias Explorer.Helper + + @recv_timeout 60_000 + + @doc """ + Proxy request to noves.fi API endpoints + """ + @spec noves_fi_api_request(String.t(), Plug.Conn.t()) :: any() + def noves_fi_api_request(url, conn) do + headers = [{"apiKey", api_key()}] + url_with_params = url <> "?" <> conn.query_string + + case HTTPoison.get(url_with_params, headers, recv_timeout: @recv_timeout) do + {:ok, %HTTPoison.Response{status_code: status, body: body}} -> + {Helper.decode_json(body), status} + + _ -> + nil + end + end + + @doc """ + Noves.fi /evm/{chain}/tx/{txHash} endpoint + """ + @spec tx_url(String.t()) :: String.t() + def tx_url(transaction_hash_string) do + "#{base_url()}/evm/#{chain_name()}/tx/#{transaction_hash_string}" + end + + @doc """ + Noves.fi /evm/{chain}/describeTx/{txHash} endpoint + """ + @spec describe_tx_url(String.t()) :: String.t() + def describe_tx_url(transaction_hash_string) do + "#{base_url()}/evm/#{chain_name()}/describeTx/#{transaction_hash_string}" + end + + @doc """ + Noves.fi /evm/{chain}/txs/{accountAddress} endpoint + """ + @spec address_txs_url(String.t()) :: String.t() + def address_txs_url(address_hash_string) do + "#{base_url()}/evm/#{chain_name()}/txs/#{address_hash_string}" + end + + defp base_url do + api_base_url() || "https://blockscout.noves.fi" + end + + defp api_base_url do + Application.get_env(:explorer, __MODULE__)[:api_base_url] + end + + defp chain_name do + Application.get_env(:explorer, __MODULE__)[:chain_name] + end + + defp api_key do + Application.get_env(:explorer, __MODULE__)[:api_key] + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 2cd105ac72..d506839dfd 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -378,6 +378,11 @@ config :explorer, Explorer.ThirdPartyIntegrations.SolidityScan, chain_id: System.get_env("SOLIDITYSCAN_CHAIN_ID"), api_key: System.get_env("SOLIDITYSCAN_API_TOKEN") +config :explorer, Explorer.ThirdPartyIntegrations.NovesFi, + api_base_url: System.get_env("NOVES_FI_BASE_API_URL"), + chain_name: System.get_env("NOVES_FI_CHAIN_NAME"), + api_key: System.get_env("NOVES_FI_API_TOKEN") + enabled? = ConfigHelper.parse_bool_env_var("MICROSERVICE_SC_VERIFIER_ENABLED") # or "eth_bytecode_db" type = System.get_env("MICROSERVICE_SC_VERIFIER_TYPE", "sc_verifier") diff --git a/cspell.json b/cspell.json index ddad179c8d..aa6c88ac85 100644 --- a/cspell.json +++ b/cspell.json @@ -312,6 +312,8 @@ "noproc", "noreferrer", "noreply", + "noves", + "NovesFi", "nowarn", "nowrap", "ntoa", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 511cd2ebfc..ce0c19d13c 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -99,8 +99,20 @@ CONTRACT_MAX_STRING_LENGTH_WITHOUT_TRIMMING=2040 # CONTRACT_DISABLE_INTERACTION= UNCLES_IN_AVERAGE_BLOCK_TIME=false DISABLE_WEBAPP=false +API_V2_ENABLED=true API_V1_READ_METHODS_DISABLED=false API_V1_WRITE_METHODS_DISABLED=false +#API_RATE_LIMIT_DISABLED=true +# API_SENSITIVE_ENDPOINTS_KEY= +API_RATE_LIMIT_TIME_INTERVAL=1s +API_RATE_LIMIT_BY_IP_TIME_INTERVAL=5m +API_RATE_LIMIT=50 +API_RATE_LIMIT_BY_KEY=50 +API_RATE_LIMIT_BY_WHITELISTED_IP=50 +API_RATE_LIMIT_WHITELISTED_IPS= +API_RATE_LIMIT_STATIC_API_KEY= +API_RATE_LIMIT_UI_V2_WITH_TOKEN=5 +API_RATE_LIMIT_BY_IP=3000 DISABLE_INDEXER=false DISABLE_REALTIME_INDEXER=false DISABLE_CATCHUP_INDEXER=false @@ -119,10 +131,16 @@ INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=false # INDEXER_BLOCK_REWARD_BATCH_SIZE= # INDEXER_BLOCK_REWARD_CONCURRENCY= # INDEXER_TOKEN_INSTANCE_RETRY_REFETCH_INTERVAL= +# INDEXER_TOKEN_INSTANCE_RETRY_BATCH_SIZE=10 # INDEXER_TOKEN_INSTANCE_RETRY_CONCURRENCY= +# INDEXER_TOKEN_INSTANCE_REALTIME_BATCH_SIZE=1 # INDEXER_TOKEN_INSTANCE_REALTIME_CONCURRENCY= +# INDEXER_TOKEN_INSTANCE_SANITIZE_BATCH_SIZE=10 # INDEXER_TOKEN_INSTANCE_SANITIZE_CONCURRENCY= +# INDEXER_TOKEN_INSTANCE_LEGACY_SANITIZE_BATCH_SIZE=10 # INDEXER_TOKEN_INSTANCE_LEGACY_SANITIZE_CONCURRENCY=10 +# TOKEN_INSTANCE_OWNER_MIGRATION_CONCURRENCY=5 +# TOKEN_INSTANCE_OWNER_MIGRATION_BATCH_SIZE=50 # INDEXER_COIN_BALANCES_BATCH_SIZE= # INDEXER_COIN_BALANCES_CONCURRENCY= # INDEXER_RECEIPTS_BATCH_SIZE= @@ -194,31 +212,16 @@ EXTERNAL_APPS=[] # RESTRICTED_LIST_KEY= SHOW_MAINTENANCE_ALERT=false MAINTENANCE_ALERT_MESSAGE= -SOURCIFY_INTEGRATION_ENABLED=false -SOURCIFY_SERVER_URL= -SOURCIFY_REPO_URL= CHAIN_ID= MAX_SIZE_UNLESS_HIDE_ARRAY=50 HIDE_BLOCK_MINER=false DISPLAY_TOKEN_ICONS=false -SHOW_TENDERLY_LINK=false -TENDERLY_CHAIN_PATH= RE_CAPTCHA_SECRET_KEY= RE_CAPTCHA_CLIENT_KEY= RE_CAPTCHA_V3_SECRET_KEY= RE_CAPTCHA_V3_CLIENT_KEY= RE_CAPTCHA_DISABLED=false JSON_RPC= -#API_RATE_LIMIT_DISABLED=true -API_RATE_LIMIT_TIME_INTERVAL=1s -API_RATE_LIMIT_BY_IP_TIME_INTERVAL=5m -API_RATE_LIMIT=50 -API_RATE_LIMIT_BY_KEY=50 -API_RATE_LIMIT_BY_WHITELISTED_IP=50 -API_RATE_LIMIT_WHITELISTED_IPS= -API_RATE_LIMIT_STATIC_API_KEY= -API_RATE_LIMIT_UI_V2_WITH_TOKEN=5 -API_RATE_LIMIT_BY_IP=3000 # API_RATE_LIMIT_HAMMER_REDIS_URL=redis://redis_db:6379/1 # API_RATE_LIMIT_IS_BLOCKSCOUT_BEHIND_PROXY=false API_RATE_LIMIT_UI_V2_TOKEN_TTL_IN_SECONDS=18000 @@ -234,6 +237,8 @@ MICROSERVICE_VISUALIZE_SOL2UML_ENABLED=true MICROSERVICE_VISUALIZE_SOL2UML_URL=http://visualizer:8050/ MICROSERVICE_SIG_PROVIDER_ENABLED=true MICROSERVICE_SIG_PROVIDER_URL=http://sig-provider:8050/ +# MICROSERVICE_BENS_URL= +# MICROSERVICE_BENS_ENABLED= DECODE_NOT_A_CONTRACT_CALLS=true # DATABASE_READ_ONLY_API_URL= # ACCOUNT_DATABASE_URL= @@ -246,28 +251,28 @@ DECODE_NOT_A_CONTRACT_CALLS=true # ACCOUNT_SENDGRID_API_KEY= # ACCOUNT_SENDGRID_SENDER= # ACCOUNT_SENDGRID_TEMPLATE= +# ACCOUNT_VERIFICATION_EMAIL_RESEND_INTERVAL= +# ACCOUNT_PRIVATE_TAGS_LIMIT=2000 +# ACCOUNT_WATCHLIST_ADDRESSES_LIMIT=15 ACCOUNT_CLOAK_KEY= ACCOUNT_ENABLED=false ACCOUNT_REDIS_URL=redis://redis_db:6379 +EIP_1559_ELASTICITY_MULTIPLIER=2 # MIXPANEL_TOKEN= # MIXPANEL_URL= # AMPLITUDE_API_KEY= # AMPLITUDE_URL= -EIP_1559_ELASTICITY_MULTIPLIER=2 -# API_SENSITIVE_ENDPOINTS_KEY= -# ACCOUNT_VERIFICATION_EMAIL_RESEND_INTERVAL= -# INDEXER_TOKEN_INSTANCE_RETRY_BATCH_SIZE=10 -# INDEXER_TOKEN_INSTANCE_REALTIME_BATCH_SIZE=1 -# INDEXER_TOKEN_INSTANCE_SANITIZE_BATCH_SIZE=10 -# INDEXER_TOKEN_INSTANCE_LEGACY_SANITIZE_BATCH_SIZE=10 -# TOKEN_INSTANCE_OWNER_MIGRATION_CONCURRENCY=5 -# TOKEN_INSTANCE_OWNER_MIGRATION_BATCH_SIZE=50 # IPFS_GATEWAY_URL= -API_V2_ENABLED=true # ADDRESSES_TABS_COUNTERS_TTL=10m -# ACCOUNT_PRIVATE_TAGS_LIMIT=2000 -# ACCOUNT_WATCHLIST_ADDRESSES_LIMIT=15 -# MICROSERVICE_BENS_URL= -# MICROSERVICE_BENS_ENABLED= # DENORMALIZATION_MIGRATION_BATCH_SIZE= # DENORMALIZATION_MIGRATION_CONCURRENCY= +SOURCIFY_INTEGRATION_ENABLED=false +SOURCIFY_SERVER_URL= +SOURCIFY_REPO_URL= +SHOW_TENDERLY_LINK=false +TENDERLY_CHAIN_PATH= +# SOLIDITYSCAN_CHAIN_ID= +# SOLIDITYSCAN_API_TOKEN= +# NOVES_FI_BASE_API_URL= +# NOVES_FI_CHAIN_NAME= +# NOVES_FI_API_TOKEN=