diff --git a/CHANGELOG.md b/CHANGELOG.md index 55161393af..1597306cd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,21 @@ ## Current ### Features +- [#2376](https://github.com/poanetwork/blockscout/pull/2376) - Split API and WebApp routes - [#2477](https://github.com/poanetwork/blockscout/pull/2477) - aggregate token transfers on transaction page - [#2458](https://github.com/poanetwork/blockscout/pull/2458) - Add LAST_BLOCK var to add ability indexing in the range of blocks - [#2456](https://github.com/poanetwork/blockscout/pull/2456) - fetch pending transactions for geth +- [#2403](https://github.com/poanetwork/blockscout/pull/2403) - Return gasPrice field at the result of gettxinfo method ### Fixes - [#2528](https://github.com/poanetwork/blockscout/pull/2528) - fix coin history chart data +- [#2520](https://github.com/poanetwork/blockscout/pull/2520) - Hide loading message when fetching is failed +- [#2523](https://github.com/poanetwork/blockscout/pull/2523) - Avoid importing internal_transactions of pending transactions +- [#2519](https://github.com/poanetwork/blockscout/pull/2519) - enable `First` page button in pagination +- [#2515](https://github.com/poanetwork/blockscout/pull/2515) - do not aggregate NFT token transfers +- [#2512](https://github.com/poanetwork/blockscout/pull/2512) - alert link fix +- [#2508](https://github.com/poanetwork/blockscout/pull/2508) - logs view columns fix +- [#2506](https://github.com/poanetwork/blockscout/pull/2506) - fix two active tab in the top menu - [#2503](https://github.com/poanetwork/blockscout/pull/2503) - Mitigate autocompletion library influence to page loading performance - [#2502](https://github.com/poanetwork/blockscout/pull/2502) - increase reward task timeout - [#2463](https://github.com/poanetwork/blockscout/pull/2463) - dark theme fixes @@ -16,6 +25,7 @@ - [#2425](https://github.com/poanetwork/blockscout/pull/2425) - Force to show address view for checksummed address even if it is not in DB ### Chore +- [#2507](https://github.com/poanetwork/blockscout/pull/2507) - update minor version of ecto, ex_machina, phoenix_live_reload - [#2516](https://github.com/poanetwork/blockscout/pull/2516) - update absinthe plug from fork - [#2473](https://github.com/poanetwork/blockscout/pull/2473) - get rid of cldr warnings - [#2402](https://github.com/poanetwork/blockscout/pull/2402) - bump otp version to 22.0 diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 9b367a8dc8..4f889b7304 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -35,5 +35,5 @@ - [ ] If I added new functionality, I added tests covering it. - [ ] If I fixed a bug, I added a regression test to prevent the bug from silently reappearing again. - [ ] I checked whether I should update the docs and did so if necessary - - [ ] If I added/changed/removed ENV var, I should update the list of env vars in https://github.com/poanetwork/blockscout/blob/master/docs/env-variables.md to reflect changes in the table here https://poanetwork.github.io/blockscout/#/env-variables?id=blockscout-env-variables + - [ ] If I added/changed/removed ENV var, I should update the list of env vars in https://github.com/poanetwork/blockscout/blob/master/docs/env-variables.md to reflect changes in the table here https://poanetwork.github.io/blockscout/#/env-variables?id=blockscout-env-variables. I've set `master` in the `Version` column. - [ ] If I add new indices into DB, I checked, that they don't redundant with PGHero or other tools diff --git a/apps/block_scout_web/assets/__tests__/lib/async_listing_load.js b/apps/block_scout_web/assets/__tests__/lib/async_listing_load.js index 6e86d8a6b5..adae2d6033 100644 --- a/apps/block_scout_web/assets/__tests__/lib/async_listing_load.js +++ b/apps/block_scout_web/assets/__tests__/lib/async_listing_load.js @@ -46,14 +46,12 @@ describe('REQUEST_ERROR', () => { describe('FINISH_REQUEST', () => { test('sets loading status to false', () => { const state = Object.assign({}, asyncInitialState, { - loading: true, - loadingFirstPage: true + loading: true }) const action = { type: 'FINISH_REQUEST' } const output = asyncReducer(state, action) expect(output.loading).toEqual(false) - expect(output.loadingFirstPage).toEqual(false) }) }) diff --git a/apps/block_scout_web/assets/css/theme/_dark-theme.scss b/apps/block_scout_web/assets/css/theme/_dark-theme.scss index c0adb0a84b..fe807aea60 100644 --- a/apps/block_scout_web/assets/css/theme/_dark-theme.scss +++ b/apps/block_scout_web/assets/css/theme/_dark-theme.scss @@ -680,4 +680,9 @@ $labels-dark: #8a8dba; // header nav, labels color: #3f436b !important; border-right-color: #3f436b !important; } + + // alert link + .alert-link { + color: $dark-secondary; + } } \ No newline at end of file diff --git a/apps/block_scout_web/assets/js/lib/async_listing_load.js b/apps/block_scout_web/assets/js/lib/async_listing_load.js index 8477a7d728..1c1fec4baa 100644 --- a/apps/block_scout_web/assets/js/lib/async_listing_load.js +++ b/apps/block_scout_web/assets/js/lib/async_listing_load.js @@ -51,8 +51,6 @@ export const asyncInitialState = { requestError: false, /* if response has no items */ emptyResponse: false, - /* if it is loading the first page */ - loadingFirstPage: true, /* link to the next page */ nextPagePath: null, /* link to the previous page */ @@ -80,8 +78,7 @@ export function asyncReducer (state = asyncInitialState, action) { } case 'FINISH_REQUEST': { return Object.assign({}, state, { - loading: false, - loadingFirstPage: false + loading: false }) } case 'ITEMS_FETCHED': { @@ -134,7 +131,7 @@ export const elements = { }, '[data-async-listing] [data-loading-message]': { render ($el, state) { - if (state.loadingFirstPage) return $el.show() + if (state.loading) return $el.show() $el.hide() } @@ -143,7 +140,7 @@ export const elements = { render ($el, state) { if ( !state.requestError && - (!state.loading || !state.loadingFirstPage) && + (!state.loading) && state.items.length === 0 ) { return $el.show() @@ -201,6 +198,16 @@ export const elements = { $el.attr('href', state.prevPagePath) } }, + '[data-async-listing] [data-first-page-button]': { + render ($el, state) { + if (state.pagesStack.length === 0) { + return $el.hide() + } + $el.show() + $el.attr('disabled', false) + $el.attr('href', window.location.href.split('?')[0]) + } + }, '[data-async-listing] [data-page-number]': { render ($el, state) { if (state.emptyResponse) { @@ -216,7 +223,7 @@ export const elements = { }, '[data-async-listing] [data-loading-button]': { render ($el, state) { - if (!state.loadingFirstPage && state.loading) return $el.show() + if (state.loading) return $el.show() $el.hide() } diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 9dae076638..38f506e854 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -28,7 +28,9 @@ config :block_scout_web, "EtherChain" => "https://www.etherchain.org/", "Bloxy" => "https://bloxy.info/" }, - other_networks: System.get_env("SUPPORTED_CHAINS") + other_networks: System.get_env("SUPPORTED_CHAINS"), + webapp_url: System.get_env("WEBAPP_URL"), + api_url: System.get_env("API_URL") config :block_scout_web, BlockScoutWeb.Counters.BlocksIndexedCounter, enabled: true @@ -85,6 +87,12 @@ config :wobserver, discovery: :none, mode: :plug +config :block_scout_web, BlockScoutWeb.ApiRouter, + writing_enabled: System.get_env("DISABLE_WRITE_API") != "true", + reading_enabled: System.get_env("DISABLE_READ_API") != "true" + +config :block_scout_web, BlockScoutWeb.WebRouter, enabled: System.get_env("DISABLE_WEBAPP") != "true" + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/apps/block_scout_web/lib/block_scout_web.ex b/apps/block_scout_web/lib/block_scout_web.ex index 4e1f857526..20ca4351df 100644 --- a/apps/block_scout_web/lib/block_scout_web.ex +++ b/apps/block_scout_web/lib/block_scout_web.ex @@ -24,6 +24,7 @@ defmodule BlockScoutWeb do import BlockScoutWeb.Controller import BlockScoutWeb.Router.Helpers + import BlockScoutWeb.WebRouter.Helpers, except: [static_path: 2] import BlockScoutWeb.Gettext import BlockScoutWeb.ErrorHelpers import Plug.Conn @@ -55,6 +56,8 @@ defmodule BlockScoutWeb do WeiHelpers } + import BlockScoutWeb.WebRouter.Helpers, except: [static_path: 2] + import PhoenixFormAwesomplete end end 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 new file mode 100644 index 0000000000..944ea56fa4 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -0,0 +1,73 @@ +defmodule RPCTranslatorForwarder do + @moduledoc """ + Phoenix router limits forwarding, + so this module is to forward old paths for backward compatibility + """ + alias BlockScoutWeb.API.RPC.RPCTranslator + defdelegate init(opts), to: RPCTranslator + defdelegate call(conn, opts), to: RPCTranslator +end + +defmodule BlockScoutWeb.ApiRouter do + @moduledoc """ + Router for API + """ + use BlockScoutWeb, :router + + pipeline :api do + plug(:accepts, ["json"]) + end + + scope "/v1", BlockScoutWeb.API.V1, as: :api_v1 do + pipe_through(:api) + get("/health", HealthController, :health) + + if Application.get_env(:block_scout_web, __MODULE__)[:writing_enabled] do + post("/decompiled_smart_contract", DecompiledSmartContractController, :create) + post("/verified_smart_contracts", VerifiedSmartContractController, :create) + end + end + + if Application.get_env(:block_scout_web, __MODULE__)[:reading_enabled] do + scope "/" do + alias BlockScoutWeb.API.{RPC, V1} + pipe_through(:api) + + scope "/v1", as: :api_v1 do + get("/supply", V1.SupplyController, :supply) + post("/eth_rpc", RPC.EthController, :eth_request) + end + + # For backward compatibility. Should be removed + post("/eth_rpc", RPC.EthController, :eth_request) + end + end + + scope "/" do + pipe_through(:api) + alias BlockScoutWeb.API.RPC + + scope "/v1", as: :api_v1 do + forward("/", RPC.RPCTranslator, %{ + "block" => {RPC.BlockController, []}, + "account" => {RPC.AddressController, []}, + "logs" => {RPC.LogsController, []}, + "token" => {RPC.TokenController, []}, + "stats" => {RPC.StatsController, []}, + "contract" => {RPC.ContractController, [:verify]}, + "transaction" => {RPC.TransactionController, []} + }) + end + + # For backward compatibility. Should be removed + forward("/", RPCTranslatorForwarder, %{ + "block" => {RPC.BlockController, []}, + "account" => {RPC.AddressController, []}, + "logs" => {RPC.LogsController, []}, + "token" => {RPC.TokenController, []}, + "stats" => {RPC.StatsController, []}, + "contract" => {RPC.ContractController, [:verify]}, + "transaction" => {RPC.TransactionController, []} + }) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex index a5a4959e8d..b6380fc975 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex @@ -25,8 +25,9 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do def init(opts), do: opts def call(%Conn{params: %{"module" => module, "action" => action}} = conn, translations) do - with {:ok, controller} <- translate_module(translations, module), + with {:ok, {controller, write_actions}} <- translate_module(translations, module), {:ok, action} <- translate_action(action), + true <- action_accessed?(action, write_actions), {:ok, conn} <- call_controller(conn, controller, action) do conn else @@ -64,7 +65,7 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do end @doc false - @spec translate_module(map(), String.t()) :: {:ok, module()} | {:error, :no_action} + @spec translate_module(map(), String.t()) :: {:ok, {module(), list(atom())}} | {:error, :no_action} defp translate_module(translations, module) do module_lowercase = String.downcase(module) @@ -83,6 +84,16 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do ArgumentError -> {:error, :no_action} end + defp action_accessed?(action, write_actions) do + conf = Application.get_env(:block_scout_web, BlockScoutWeb.ApiRouter) + + if action in write_actions do + conf[:writing_enabled] || {:error, :no_action} + else + conf[:reading_enabled] || {:error, :no_action} + end + end + @doc false @spec call_controller(Conn.t(), module(), atom()) :: {:ok, Conn.t()} | {:error, :no_action} | {:error, Exception.t()} defp call_controller(conn, controller, action) do diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 2191a86fa2..73f1967900 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -440,6 +440,7 @@ defmodule BlockScoutWeb.Etherscan do "from" => "0x000000000000000000000000000000000000000c", "gasLimit" => "91966", "gasUsed" => "95123", + "gasPrice" => "100000", "hash" => "0x0000000000000000000000000000000000000000000000000000000000000004", "input" => "0x04", "logs" => [ @@ -986,6 +987,7 @@ defmodule BlockScoutWeb.Etherscan do input: @input_type, gasLimit: @wei_type, gasUsed: @gas_type, + gasPrice: @wei_type, logs: %{ type: "array", array_type: @logs_details diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index 7a1f5b3ed9..e654e1729c 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.Router do use BlockScoutWeb, :router alias BlockScoutWeb.Plug.GraphQL + alias BlockScoutWeb.{ApiRouter, WebRouter} forward("/wobserver", Wobserver.Web.Router) forward("/admin", BlockScoutWeb.AdminRouter) @@ -18,249 +19,54 @@ defmodule BlockScoutWeb.Router do plug(:accepts, ["json"]) end - scope "/api/v1", BlockScoutWeb.API.V1, as: :api_v1 do - pipe_through(:api) + forward("/api", ApiRouter) - get("/supply", SupplyController, :supply) + if Application.get_env(:block_scout_web, ApiRouter)[:reading_enabled] do + # Needs to be 200 to support the schema introspection for graphiql + @max_complexity 200 - get("/health", HealthController, :health) + forward("/graphql", Absinthe.Plug, + schema: BlockScoutWeb.Schema, + analyze_complexity: true, + max_complexity: @max_complexity + ) - resources("/decompiled_smart_contract", DecompiledSmartContractController, only: [:create]) - resources("/verified_smart_contracts", VerifiedSmartContractController, only: [:create]) + forward("/graphiql", Absinthe.Plug.GraphiQL, + schema: BlockScoutWeb.Schema, + interface: :advanced, + default_query: GraphQL.default_query(), + socket: BlockScoutWeb.UserSocket, + analyze_complexity: true, + max_complexity: @max_complexity + ) + else + scope "/", BlockScoutWeb do + pipe_through(:browser) + get("/api_docs", PageNotFoundController, :index) + get("/eth_rpc_api_docs", PageNotFoundController, :index) + end end - scope "/verify_smart_contract" do - pipe_through(:api) + scope "/", BlockScoutWeb do + pipe_through(:browser) - post("/contract_verifications", BlockScoutWeb.AddressContractVerificationController, :create) + get("/api_docs", APIDocsController, :index) + get("/eth_rpc_api_docs", APIDocsController, :eth_rpc) end - scope "/api", BlockScoutWeb.API.RPC do + scope "/verify_smart_contract" do pipe_through(:api) - alias BlockScoutWeb.API.RPC - - post("/eth_rpc", EthController, :eth_request) - - forward("/", RPCTranslator, %{ - "block" => RPC.BlockController, - "account" => RPC.AddressController, - "logs" => RPC.LogsController, - "token" => RPC.TokenController, - "stats" => RPC.StatsController, - "contract" => RPC.ContractController, - "transaction" => RPC.TransactionController - }) - end - - # Needs to be 200 to support the schema introspection for graphiql - @max_complexity 200 - - forward("/graphql", Absinthe.Plug, - schema: BlockScoutWeb.Schema, - analyze_complexity: true, - max_complexity: @max_complexity - ) - - forward("/graphiql", Absinthe.Plug.GraphiQL, - schema: BlockScoutWeb.Schema, - interface: :advanced, - default_query: GraphQL.default_query(), - socket: BlockScoutWeb.UserSocket, - analyze_complexity: true, - max_complexity: @max_complexity - ) - - # Disallows Iframes (write routes) - scope "/", BlockScoutWeb do - pipe_through(:browser) + post("/contract_verifications", BlockScoutWeb.AddressContractVerificationController, :create) end - # Allows Iframes (read-only routes) - scope "/", BlockScoutWeb do - pipe_through([:browser, BlockScoutWeb.Plug.AllowIframe]) - - resources("/", ChainController, only: [:show], singleton: true, as: :chain) - - resources("/market_history_chart", Chain.MarketHistoryChartController, - only: [:show], - singleton: true - ) + if Application.get_env(:block_scout_web, WebRouter)[:enabled] do + forward("/", BlockScoutWeb.WebRouter) + else + scope "/", BlockScoutWeb do + pipe_through(:browser) - resources "/blocks", BlockController, only: [:index, :show], param: "hash_or_number" do - resources("/transactions", BlockTransactionController, only: [:index], as: :transaction) + forward("/", APIDocsController, :index) end - - get("/reorgs", BlockController, :reorg, as: :reorg) - - get("/uncles", BlockController, :uncle, as: :uncle) - - resources("/pending_transactions", PendingTransactionController, only: [:index]) - - resources("/recent_transactions", RecentTransactionsController, only: [:index]) - - get("/txs", TransactionController, :index) - - resources "/tx", TransactionController, only: [:show] do - resources( - "/internal_transactions", - TransactionInternalTransactionController, - only: [:index], - as: :internal_transaction - ) - - resources( - "/raw_trace", - TransactionRawTraceController, - only: [:index], - as: :raw_trace - ) - - resources("/logs", TransactionLogController, only: [:index], as: :log) - - resources("/token_transfers", TransactionTokenTransferController, - only: [:index], - as: :token_transfer - ) - end - - resources("/accounts", AddressController, only: [:index]) - - resources "/address", AddressController, only: [:show] do - resources("/transactions", AddressTransactionController, only: [:index], as: :transaction) - - resources( - "/internal_transactions", - AddressInternalTransactionController, - only: [:index], - as: :internal_transaction - ) - - resources( - "/validations", - AddressValidationController, - only: [:index], - as: :validation - ) - - resources( - "/contracts", - AddressContractController, - only: [:index], - as: :contract - ) - - resources( - "/decompiled_contracts", - AddressDecompiledContractController, - only: [:index], - as: :decompiled_contract - ) - - resources( - "/logs", - AddressLogsController, - only: [:index], - as: :logs - ) - - resources( - "/contract_verifications", - AddressContractVerificationController, - only: [:new], - as: :verify_contract - ) - - resources( - "/read_contract", - AddressReadContractController, - only: [:index, :show], - as: :read_contract - ) - - resources("/tokens", AddressTokenController, only: [:index], as: :token) do - resources( - "/token_transfers", - AddressTokenTransferController, - only: [:index], - as: :transfers - ) - end - - resources( - "/token_balances", - AddressTokenBalanceController, - only: [:index], - as: :token_balance - ) - - resources( - "/coin_balances", - AddressCoinBalanceController, - only: [:index], - as: :coin_balance - ) - - resources( - "/coin_balances/by_day", - AddressCoinBalanceByDayController, - only: [:index], - as: :coin_balance_by_day - ) - end - - resources "/tokens", Tokens.TokenController, only: [:show], as: :token do - resources( - "/token_transfers", - Tokens.TransferController, - only: [:index], - as: :transfer - ) - - resources( - "/read_contract", - Tokens.ReadContractController, - only: [:index], - as: :read_contract - ) - - resources( - "/token_holders", - Tokens.HolderController, - only: [:index], - as: :holder - ) - - resources( - "/inventory", - Tokens.InventoryController, - only: [:index], - as: :inventory - ) - end - - resources( - "/smart_contracts", - SmartContractController, - only: [:index, :show], - as: :smart_contract - ) - - get("/search", ChainController, :search) - - get("/search_logs", AddressLogsController, :search_logs) - - get("/transactions_csv", AddressTransactionController, :transactions_csv) - - get("/token_autocomplete", ChainController, :token_autocomplete) - - get("/token_transfers_csv", AddressTransactionController, :token_transfers_csv) - - get("/chain_blocks", ChainController, :chain_blocks, as: :chain_blocks) - - get("/api_docs", APIDocsController, :index) - get("/eth_rpc_api_docs", APIDocsController, :eth_rpc) - - get("/*path", PageNotFoundController, :index) end end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex index f9992c6d86..dfe7c51f30 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex @@ -14,15 +14,14 @@