diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f916aa01df..f3d2817e91d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,32 @@
# Changelog
+## 11.2.7
+
+### 🚀 Features
+
+- Add /api/v2/transactions/{hash}/preview endpoint ([#14638](https://github.com/blockscout/blockscout/issues/14638), [#14703](https://github.com/blockscout/blockscout/issues/14703), [#14704](https://github.com/blockscout/blockscout/issues/14704))
+
+### 🐛 Bug Fixes
+
+- Fix state changes for sponsored transactions ([#14702](https://github.com/blockscout/blockscout/issues/14702))
+
+### ⚡ Performance
+
+- Reduce query count and payload in transaction API endpoint ([#14705](https://github.com/blockscout/blockscout/issues/14705))
+- Cache empty implementations of verified contracts longer ([#14696](https://github.com/blockscout/blockscout/issues/14696))
+- Fetch address existence checks in a single query ([#14694](https://github.com/blockscout/blockscout/issues/14694))
+- Reuse HTTP connections and parallelize microservice preloads ([#14689](https://github.com/blockscout/blockscout/issues/14689), [#14707](https://github.com/blockscout/blockscout/issues/14707))
+- Fix transform addresses tests for zksync ([#14717](https://github.com/blockscout/blockscout/pull/14717))
+
+### New ENV variables
+
+| Variable | Description | Parameters |
+|-----------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
+| `CONTRACT_PROXY_EMPTY_IMPLEMENTATION_DATA_CACHE_TTL` | Empty probe results ("not a proxy") of smart-contracts verified on the address itself are refreshed with a dedicated, much longer TTL. Unverified contracts, bytecode twins, and real proxies keep the existing TTL behavior. [Time format](/setup/env-variables/backend-env-variables#time-format). Implemented in [#14689](https://github.com/blockscout/blockscout/pull/14689). | Version: v11.2.7+
Default: `1d`
Applications: API |
+| `MICROSERVICE_HTTP_POOL_SIZE` | Total size of the keep-alive connection pools used by `Explorer.MicroserviceInterfaces.HttpClient` for BENS and Metadata requests. This total is split across multiple pools controlled by `MICROSERVICE_HTTP_POOL_COUNT`. Each individual pool size is calculated as `MICROSERVICE_HTTP_POOL_SIZE / MICROSERVICE_HTTP_POOL_COUNT`. Two logical pools are used: `:microservices` for short requests on the critical path and `:microservices_proxy` for requests proxied to a microservice on behalf of an API caller, so a handful of long-running proxied requests can no longer starve the preloads. Both pools are supervised from `Explorer.Application`. Implemented in [#14689](https://github.com/blockscout/blockscout/pull/14689) | Version: v11.2.7+
Default: `1000`
Applications: API |
+| `MICROSERVICE_HTTP_POOL_COUNT` | Number of individual connection pools to split `MICROSERVICE_HTTP_POOL_SIZE` across. Pool splitting allows traffic to spread across multiple processes, reducing contention. Each pool size is calculated as `MICROSERVICE_HTTP_POOL_SIZE / MICROSERVICE_HTTP_POOL_COUNT`. Implemented in [#14707](https://github.com/blockscout/blockscout/pull/14707) | Version: v11.2.7+
Default: `4`
Applications: API, Indexer |
+
+
## 11.2.6
### 🐛 Bug Fixes
@@ -28,6 +55,13 @@
- Bump ex_abi lib version ([#14671](https://github.com/blockscout/blockscout/issues/14671))
+### New ENV variables
+
+| Variable | Description | Parameters |
+|-----------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------|
+| `INDEXER_DISABLE_TOKEN_BALANCE_ON_DEMAND_FETCHER` | If `true`, `Indexer.Fetcher.OnDemand.TokenBalance` fetcher is disabled at runtime. | Version: v11.2.5\+
Default: `false`
Applications: Indexer |
+
+
## 11.2.4
### 🚀 Features
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 640b82feba6..5f9118b36fe 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
@@ -28,15 +28,11 @@ defmodule BlockScoutWeb.API.V2.AddressController do
import Explorer.Helper, only: [safe_parse_non_negative_integer: 1]
- import Explorer.MicroserviceInterfaces.BENS,
- only: [
- maybe_preload_ens: 1,
- maybe_preload_ens_for_token_transfers: 1,
- maybe_preload_ens_for_transactions: 1,
- maybe_preload_ens_to_address: 1
- ]
+ import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens_to_address: 1]
+
+ import Explorer.Chain.Address.MetadataPreloader,
+ only: [maybe_preload_ens_and_metadata: 1, maybe_preload_ens_and_metadata: 2]
- import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1]
import Explorer.Chain.Address.Reputation, only: [reputation_association: 0]
alias BlockScoutWeb.AccessHelper
@@ -424,7 +420,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
|> put_status(200)
|> put_view(TransactionView)
|> render(:transactions, %{
- transactions: transactions |> maybe_preload_ens_for_transactions() |> maybe_preload_metadata(),
+ transactions: transactions |> maybe_preload_ens_and_metadata(:transactions),
next_page_params: next_page_params
})
@@ -554,8 +550,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
token_transfers:
token_transfers
|> Instance.preload_nft(@api_true)
- |> maybe_preload_ens_for_token_transfers()
- |> maybe_preload_metadata(),
+ |> maybe_preload_ens_and_metadata(:token_transfers),
next_page_params: next_page_params
})
@@ -636,7 +631,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
|> put_status(200)
|> put_view(TransactionView)
|> render(:internal_transactions, %{
- internal_transactions: internal_transactions |> maybe_preload_ens() |> maybe_preload_metadata(),
+ internal_transactions: internal_transactions |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
@@ -711,7 +706,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
|> put_status(200)
|> put_view(TransactionView)
|> render(:logs, %{
- logs: logs |> maybe_preload_ens() |> maybe_preload_metadata(),
+ logs: logs |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
@@ -1034,7 +1029,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
|> put_status(200)
|> put_view(WithdrawalView)
|> render(:withdrawals, %{
- withdrawals: withdrawals |> maybe_preload_ens() |> maybe_preload_metadata(),
+ withdrawals: withdrawals |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
@@ -1109,7 +1104,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
conn
|> put_status(200)
|> render(:addresses, %{
- addresses: addresses |> maybe_preload_ens() |> maybe_preload_metadata(),
+ addresses: addresses |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params,
exchange_rate: exchange_rate,
total_supply: total_supply
@@ -1550,7 +1545,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do
|> put_status(200)
|> put_view(DepositView)
|> render(:deposits, %{
- deposits: deposits |> maybe_preload_ens() |> maybe_preload_metadata(),
+ deposits: deposits |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
index e9ad8868b23..d5258d0d0da 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
@@ -28,10 +28,9 @@ defmodule BlockScoutWeb.API.V2.BlockController do
internal_transaction_call_type_options: 1
]
- import Explorer.MicroserviceInterfaces.BENS,
- only: [maybe_preload_ens: 1, maybe_preload_ens_for_blocks: 1, maybe_preload_ens_for_transactions: 1]
+ import Explorer.Chain.Address.MetadataPreloader,
+ only: [maybe_preload_ens_and_metadata: 1, maybe_preload_ens_and_metadata: 2]
- import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1]
import Explorer.Chain.Address.Reputation, only: [reputation_association: 0]
alias BlockScoutWeb.API.V2.{
@@ -227,7 +226,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
conn
|> put_status(200)
|> render(:blocks, %{
- blocks: blocks |> maybe_preload_ens_for_blocks() |> maybe_preload_metadata(),
+ blocks: blocks |> maybe_preload_ens_and_metadata(:blocks),
next_page_params: next_page_params
})
end
@@ -274,7 +273,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
conn
|> put_status(200)
|> render(:blocks, %{
- blocks: blocks |> maybe_preload_ens_for_blocks() |> maybe_preload_metadata(),
+ blocks: blocks |> maybe_preload_ens_and_metadata(:blocks),
next_page_params: next_page_params
})
end
@@ -322,7 +321,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
conn
|> put_status(200)
|> render(:blocks, %{
- blocks: blocks |> maybe_preload_ens_for_blocks() |> maybe_preload_metadata(),
+ blocks: blocks |> maybe_preload_ens_and_metadata(:blocks),
next_page_params: next_page_params
})
end
@@ -370,7 +369,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
conn
|> put_status(200)
|> render(:blocks, %{
- blocks: blocks |> maybe_preload_ens_for_blocks() |> maybe_preload_metadata(),
+ blocks: blocks |> maybe_preload_ens_and_metadata(:blocks),
next_page_params: next_page_params
})
end
@@ -424,7 +423,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
|> put_status(200)
|> put_view(TransactionView)
|> render(:transactions, %{
- transactions: transactions |> maybe_preload_ens_for_transactions() |> maybe_preload_metadata(),
+ transactions: transactions |> maybe_preload_ens_and_metadata(:transactions),
next_page_params: next_page_params
})
end
@@ -546,7 +545,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
|> put_status(200)
|> put_view(WithdrawalView)
|> render(:withdrawals, %{
- withdrawals: withdrawals |> maybe_preload_ens() |> maybe_preload_metadata(),
+ withdrawals: withdrawals |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
@@ -684,7 +683,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
|> put_status(200)
|> put_view(DepositView)
|> render(:deposits, %{
- deposits: deposits |> maybe_preload_ens() |> maybe_preload_metadata(),
+ deposits: deposits |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex
index 827e23ae7ab..1298cfd76d1 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex
@@ -4,8 +4,7 @@ defmodule BlockScoutWeb.API.V2.Ethereum.DepositController do
use OpenApiSpex.ControllerSpecs
import BlockScoutWeb.Chain, only: [next_page_params: 5, split_list_by_page: 1]
- import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1]
- import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1]
+ import Explorer.Chain.Address.MetadataPreloader, only: [maybe_preload_ens_and_metadata: 1]
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.Beacon.Deposit
@@ -74,7 +73,7 @@ defmodule BlockScoutWeb.API.V2.Ethereum.DepositController do
conn
|> put_status(200)
|> render(:deposits, %{
- deposits: deposits |> maybe_preload_ens() |> maybe_preload_metadata(),
+ deposits: deposits |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex
index 151528db3da..b79d0c2f9c3 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex
@@ -14,10 +14,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do
import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
- import Explorer.MicroserviceInterfaces.BENS,
- only: [maybe_preload_ens_for_blocks: 1, maybe_preload_ens_for_transactions: 1]
-
- import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1]
+ import Explorer.Chain.Address.MetadataPreloader, only: [maybe_preload_ens_and_metadata: 2]
import Explorer.Chain.Address.Reputation, only: [reputation_association: 0]
case @chain_identity do
@@ -81,7 +78,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do
conn
|> put_status(200)
|> put_view(BlockView)
- |> render(:blocks, %{blocks: blocks |> maybe_preload_ens_for_blocks() |> maybe_preload_metadata()})
+ |> render(:blocks, %{blocks: blocks |> maybe_preload_ens_and_metadata(:blocks)})
end
operation :transactions,
@@ -110,7 +107,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do
|> put_status(200)
|> put_view(TransactionView)
|> render(:transactions, %{
- transactions: recent_transactions |> maybe_preload_ens_for_transactions() |> maybe_preload_metadata()
+ transactions: recent_transactions |> maybe_preload_ens_and_metadata(:transactions)
})
end
@@ -142,7 +139,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do
|> put_status(200)
|> put_view(TransactionView)
|> render(:transactions_watchlist, %{
- transactions: transactions |> maybe_preload_ens_for_transactions() |> maybe_preload_metadata(),
+ transactions: transactions |> maybe_preload_ens_and_metadata(:transactions),
watchlist_names: watchlist_names
})
end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex
index 60a4886dbc2..9d79b53d57c 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex
@@ -12,8 +12,7 @@ defmodule BlockScoutWeb.API.V2.MudController do
import BlockScoutWeb.PagingHelper, only: [mud_records_sorting: 1]
import Explorer.PagingOptions, only: [default_paging_options: 0]
- import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1]
- import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1]
+ import Explorer.Chain.Address.MetadataPreloader, only: [maybe_preload_ens_and_metadata: 1]
alias BlockScoutWeb.Schemas.Helper, as: SchemasHelper
alias Explorer.Chain
@@ -83,8 +82,7 @@ defmodule BlockScoutWeb.API.V2.MudController do
worlds:
worlds
|> Enum.map(fn world -> Map.get(world_addresses, world, %Address{hash: world}) end)
- |> maybe_preload_ens()
- |> maybe_preload_metadata(),
+ |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
index f9e5924bbcd..e140b16d8fa 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
@@ -35,10 +35,9 @@ defmodule BlockScoutWeb.API.V2.TokenController do
tokens_sorting: 1
]
- import Explorer.MicroserviceInterfaces.BENS,
- only: [maybe_preload_ens: 1, maybe_preload_ens_for_token_transfers: 1]
+ import Explorer.Chain.Address.MetadataPreloader,
+ only: [maybe_preload_ens_and_metadata: 1, maybe_preload_ens_and_metadata: 2]
- import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1]
import Explorer.PagingOptions, only: [default_paging_options: 0]
action_fallback(BlockScoutWeb.API.V2.FallbackController)
@@ -186,8 +185,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
token_transfers:
token_transfers
|> Instance.preload_nft(@api_true)
- |> maybe_preload_ens_for_token_transfers()
- |> maybe_preload_metadata(),
+ |> maybe_preload_ens_and_metadata(:token_transfers),
next_page_params: next_page_params
})
end
@@ -234,7 +232,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
conn
|> put_status(200)
|> render(:token_holders, %{
- token_balances: token_balances |> maybe_preload_ens() |> maybe_preload_metadata(),
+ token_balances: token_balances |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
@@ -304,8 +302,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
token_instances:
token_instances
|> put_owner(holder_address_with_proxy_implementations, holder_address_hash)
- |> maybe_preload_ens()
- |> maybe_preload_metadata(),
+ |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params,
token: token
})
@@ -331,7 +328,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
conn
|> put_status(200)
|> render(:token_instances, %{
- token_instances: token_instances |> maybe_preload_ens() |> maybe_preload_metadata(),
+ token_instances: token_instances |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params,
token: token
})
@@ -445,7 +442,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
|> put_status(200)
|> put_view(TransactionView)
|> render(:token_transfers, %{
- token_transfers: token_transfers |> maybe_preload_ens_for_token_transfers() |> maybe_preload_metadata(),
+ token_transfers: token_transfers |> maybe_preload_ens_and_metadata(:token_transfers),
next_page_params: next_page_params
})
end
@@ -503,7 +500,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
conn
|> put_status(200)
|> render(:token_holders, %{
- token_balances: token_holders |> maybe_preload_ens() |> maybe_preload_metadata(),
+ token_balances: token_holders |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex
index 71dff53a195..57f2c6929d5 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex
@@ -18,10 +18,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do
token_transfers_types_options: 1
]
- import Explorer.MicroserviceInterfaces.BENS,
- only: [maybe_preload_ens_for_token_transfers: 1]
-
- import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1]
+ import Explorer.Chain.Address.MetadataPreloader, only: [maybe_preload_ens_and_metadata: 2]
import Explorer.PagingOptions, only: [default_paging_options: 0]
alias Explorer.Chain.Token.Instance
@@ -110,8 +107,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do
token_transfers:
token_transfers
|> Instance.preload_nft(@api_true)
- |> maybe_preload_ens_for_token_transfers()
- |> maybe_preload_metadata(),
+ |> maybe_preload_ens_and_metadata(:token_transfers),
decoded_transactions_map: decoded_transactions_map,
next_page_params: next_page_params
})
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 d7f5c4e4d14..81f89a4847a 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
@@ -31,17 +31,13 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
type_filter_options: 1
]
- import Explorer.MicroserviceInterfaces.BENS,
+ import Explorer.Chain.Address.MetadataPreloader,
only: [
- maybe_preload_ens: 1,
- maybe_preload_ens_for_token_transfers: 1,
- maybe_preload_ens_for_transactions: 1,
- maybe_preload_ens_to_transaction: 1
+ maybe_preload_ens_and_metadata: 1,
+ maybe_preload_ens_and_metadata: 2,
+ maybe_preload_selected_meta: 2
]
- import Explorer.MicroserviceInterfaces.Metadata,
- only: [maybe_preload_metadata: 1, maybe_preload_metadata_to_transaction: 1]
-
import Explorer.Chain.Address.Reputation, only: [reputation_association: 0]
import Ecto.Query,
@@ -62,7 +58,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit
alias Explorer.Chain.Beacon.Reader, as: BeaconReader
alias Explorer.Chain.Cache.Counters.{NewPendingTransactionsCount, Transactions24hCount}
- alias Explorer.Chain.{FheOperation, Hash, Transaction}
+ alias Explorer.Chain.{FheOperation, Hash, SmartContract, Transaction}
alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch
alias Explorer.Chain.Scroll.Reader, as: ScrollReader
alias Explorer.Chain.Token.Instance
@@ -92,45 +88,34 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
end
# TODO might be redundant to preload blob fields in some of the endpoints
- @transaction_necessity_by_association %{
- :block => :optional,
- [
- created_contract_address: [
- :scam_badge,
- :names,
- :token,
- :smart_contract,
- proxy_implementations_association()
- ]
- ] => :optional,
- [
- from_address: [
- :scam_badge,
- :names,
- :smart_contract,
- proxy_implementations_association()
- ]
- ] => :optional,
- [
- to_address: [
- :scam_badge,
- :names,
- :smart_contract,
- proxy_implementations_association()
- ]
- ] => :optional
- }
+ # Address-info preloads for the transaction participants (from/to/created and
+ # token transfer addresses) are intentionally absent here: the `transaction`
+ # action loads them all in a single deduplicated pass via
+ # `Chain.preload_transaction_participants/3` using
+ # `@transaction_participants_necessity_by_association`.
+ @transaction_necessity_by_association %{:block => :optional}
|> Map.merge(@chain_type_transaction_necessity_by_association)
+ @transaction_participants_necessity_by_association %{
+ :scam_badge => :optional,
+ :names => :optional,
+ :token => :optional,
+ SmartContract.association_without_abi() => :optional,
+ proxy_implementations_association() => :optional
+ }
+
@token_transfers_necessity_by_association %{
- [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional,
- [to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional,
+ [from_address: [:scam_badge, :names, SmartContract.association_without_abi(), proxy_implementations_association()]] =>
+ :optional,
+ [to_address: [:scam_badge, :names, SmartContract.association_without_abi(), proxy_implementations_association()]] =>
+ :optional,
[token: reputation_association()] => :optional
}
+ # Transfer from/to address preloads are handled by
+ # `Chain.preload_transaction_participants/3`, see
+ # `@transaction_participants_necessity_by_association`.
@token_transfers_in_transaction_necessity_by_association %{
- [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional,
- [to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional,
[token: reputation_association()] => :optional
}
@@ -214,6 +199,12 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
transaction,
@token_transfers_in_transaction_necessity_by_association,
@api_true |> fetch_scam_token_toggle(conn)
+ ),
+ preloaded <-
+ Chain.preload_transaction_participants(
+ preloaded,
+ @transaction_participants_necessity_by_association,
+ @api_true
) do
conn
|> put_status(200)
@@ -221,8 +212,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
transaction:
preloaded
|> Instance.preload_nft(@api_true)
- |> maybe_preload_ens_to_transaction()
- |> maybe_preload_metadata_to_transaction()
+ |> maybe_preload_ens_and_metadata()
})
end
end
@@ -273,7 +263,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
conn
|> put_status(200)
|> render(:transactions, %{
- transactions: transactions |> maybe_preload_ens_for_transactions() |> maybe_preload_metadata(),
+ transactions: transactions |> maybe_preload_ens_and_metadata(:transactions),
next_page_params: next_page_params
})
end
@@ -517,7 +507,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
conn
|> put_status(200)
|> render(:transactions, %{
- transactions: transactions |> maybe_preload_ens_for_transactions() |> maybe_preload_metadata(),
+ transactions: transactions |> maybe_preload_ens_and_metadata(:transactions),
next_page_params: next_page_params
})
end
@@ -561,7 +551,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
conn
|> put_status(200)
|> render(:transactions, %{
- transactions: transactions |> maybe_preload_ens_for_transactions() |> maybe_preload_metadata(),
+ transactions: transactions |> maybe_preload_ens_and_metadata(:transactions),
next_page_params: next_page_params
})
end
@@ -608,7 +598,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
conn
|> put_status(200)
|> render(:transactions, %{
- transactions: transactions |> maybe_preload_ens_for_transactions() |> maybe_preload_metadata(),
+ transactions: transactions |> maybe_preload_ens_and_metadata(:transactions),
next_page_params: next_page_params
})
end
@@ -718,8 +708,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
token_transfers:
token_transfers
|> Instance.preload_nft(@api_true)
- |> maybe_preload_ens_for_token_transfers()
- |> maybe_preload_metadata(),
+ |> maybe_preload_ens_and_metadata(:token_transfers),
next_page_params: next_page_params
})
end
@@ -770,7 +759,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
conn
|> put_status(200)
|> render(:internal_transactions, %{
- internal_transactions: internal_transactions |> maybe_preload_ens() |> maybe_preload_metadata(),
+ internal_transactions: internal_transactions |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
@@ -826,7 +815,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
|> put_status(200)
|> render(:logs, %{
transaction_hash: transaction_hash,
- logs: logs |> maybe_preload_ens() |> maybe_preload_metadata(),
+ logs: logs |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
@@ -968,7 +957,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
conn
|> put_status(200)
|> render(:transactions_watchlist, %{
- transactions: transactions |> maybe_preload_ens_for_transactions() |> maybe_preload_metadata(),
+ transactions: transactions |> maybe_preload_ens_and_metadata(:transactions),
next_page_params: next_page_params,
watchlist_names: watchlist_names
})
@@ -1045,6 +1034,72 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
end
end
+ @preview_necessity_by_association %{
+ :block => :optional,
+ [from_address: [:names, :smart_contract]] => :optional,
+ [to_address: [:names, :smart_contract]] => :optional
+ }
+
+ operation :preview,
+ summary: "Get lightweight transaction preview for social media embeds",
+ description: "Returns minimal transaction data (status, timestamp, method, from/to) for rendering OG previews.",
+ parameters:
+ [transaction_hash_param() | base_params()] ++
+ [
+ %OpenApiSpex.Parameter{
+ name: :preload_ens,
+ in: :query,
+ schema: %Schema{type: :boolean},
+ required: false,
+ description: "Preload ENS domain names for addresses (default: false)"
+ },
+ %OpenApiSpex.Parameter{
+ name: :preload_metadata,
+ in: :query,
+ schema: %Schema{type: :boolean},
+ required: false,
+ description: "Preload address metadata/name tags (default: false)"
+ },
+ %OpenApiSpex.Parameter{
+ name: :decode_input,
+ in: :query,
+ schema: %Schema{type: :boolean},
+ required: false,
+ description: "Decode transaction input to resolve method name (default: false)"
+ }
+ ],
+ responses: [
+ ok: {"Lightweight transaction preview.", "application/json", Schemas.Transaction.Preview},
+ not_found: NotFoundResponse.response(),
+ unprocessable_entity: JsonErrorResponse.response()
+ ]
+
+ @doc """
+ Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/preview` endpoint.
+ """
+ @spec preview(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()}
+ def preview(conn, %{transaction_hash_param: transaction_hash_string} = params) do
+ options =
+ [necessity_by_association: @preview_necessity_by_association]
+ |> Keyword.merge(@api_true)
+
+ with {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params, options) do
+ preloaded = maybe_preload_selected_meta(transaction, requested_preview_meta_fields(params))
+
+ conn
+ |> put_status(200)
+ |> render(:preview, %{transaction: preloaded, decode_input: params[:decode_input] == true})
+ end
+ end
+
+ # Both preloads are opt-in per request, so only what the caller asked for is
+ # fetched. When both are requested they are fetched concurrently.
+ defp requested_preview_meta_fields(params) do
+ for {param, field} <- [preload_ens: :ens_domain_name, preload_metadata: :metadata],
+ params[param] == true,
+ do: field
+ end
+
operation :blobs,
summary: "List blobs for a transaction",
description: "Retrieves blobs for a specific transaction (Ethereum only).",
@@ -1204,7 +1259,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
|> put_status(200)
|> put_view(DepositView)
|> render(:deposits, %{
- deposits: deposits |> maybe_preload_ens() |> maybe_preload_metadata(),
+ deposits: deposits |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex
index 3b32c056a80..b9c2715e166 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex
@@ -6,8 +6,7 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do
import BlockScoutWeb.Chain,
only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
- import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1]
- import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1]
+ import Explorer.Chain.Address.MetadataPreloader, only: [maybe_preload_ens_and_metadata: 1]
alias Explorer.Chain
alias Explorer.Chain.Withdrawal
@@ -53,7 +52,7 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do
conn
|> put_status(200)
|> render(:withdrawals, %{
- withdrawals: withdrawals |> maybe_preload_ens() |> maybe_preload_metadata(),
+ withdrawals: withdrawals |> maybe_preload_ens_and_metadata(),
next_page_params: next_page_params
})
end
diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex
index bf1f280112d..5b53275d17e 100644
--- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex
+++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex
@@ -4,6 +4,8 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
Module to interact with Transaction Interpretation Service
"""
+ use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
+
import BlockScoutWeb.Chain, only: [transaction_to_internal_transactions: 2]
alias BlockScoutWeb.API.V2.{Helper, InternalTransactionView, TokenTransferView, TokenView, TransactionView}
@@ -142,12 +144,15 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
defp prepare_request_body(transaction) do
transaction =
- Chain.select_repo(@api_true).preload(transaction, [
- :block,
- to_address: [:scam_badge, :names, :smart_contract],
- from_address: [:scam_badge, :names, :smart_contract],
- created_contract_address: [:scam_badge, :names, :token, :smart_contract]
- ])
+ Chain.select_repo(@api_true).preload(
+ transaction,
+ [
+ :block,
+ to_address: [:scam_badge, :names, :smart_contract],
+ from_address: [:scam_badge, :names, :smart_contract],
+ created_contract_address: [:scam_badge, :names, :token, :smart_contract]
+ ] ++ chain_type_address_preloads()
+ )
token_transfers = transaction |> fetch_token_transfers() |> Enum.reverse()
internal_transactions = transaction |> fetch_internal_transactions() |> Enum.reverse()
@@ -181,32 +186,51 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
decoded_input_data = decoded_input |> Transaction.format_decoded_input() |> TransactionView.decoded_input()
%{
- data: %{
- to:
- Helper.address_with_info(nil, transaction_with_meta.to_address, transaction_with_meta.to_address_hash, true),
- from:
- Helper.address_with_info(
- nil,
- transaction_with_meta.from_address,
- transaction_with_meta.from_address_hash,
- true
- ),
- hash: transaction_with_meta.hash,
- type: transaction_with_meta.type,
- value: transaction_with_meta.value,
- method: Transaction.method_name(transaction_with_meta, Transaction.format_decoded_input(decoded_input)),
- status: transaction_with_meta.status,
- transaction_types: TransactionView.transaction_types(transaction_with_meta),
- raw_input: transaction_with_meta.input,
- decoded_input: decoded_input_data,
- token_transfers: prepare_token_transfers(token_transfers_with_meta, decoded_input),
- internal_transactions: prepare_internal_transactions(internal_transactions_with_meta, transaction_with_meta)
- },
+ data:
+ %{
+ to:
+ Helper.address_with_info(nil, transaction_with_meta.to_address, transaction_with_meta.to_address_hash, true),
+ from:
+ Helper.address_with_info(
+ nil,
+ transaction_with_meta.from_address,
+ transaction_with_meta.from_address_hash,
+ true
+ ),
+ hash: transaction_with_meta.hash,
+ type: transaction_with_meta.type,
+ value: transaction_with_meta.value,
+ method: Transaction.method_name(transaction_with_meta, Transaction.format_decoded_input(decoded_input)),
+ status: transaction_with_meta.status,
+ transaction_types: TransactionView.transaction_types(transaction_with_meta),
+ raw_input: transaction_with_meta.input,
+ decoded_input: decoded_input_data,
+ token_transfers: prepare_token_transfers(token_transfers_with_meta, decoded_input),
+ internal_transactions: prepare_internal_transactions(internal_transactions_with_meta, transaction_with_meta)
+ }
+ |> extend_data_with_chain_type_fields(transaction_with_meta),
logs_data: %{items: prepare_logs(logs_with_meta, transaction_with_meta)},
chain_id: :block_scout_web |> Application.get_env(:chain_id) |> ExplorerHelper.parse_integer()
}
end
+ @spec extend_data_with_chain_type_fields(map(), Transaction.t()) :: map()
+ case @chain_type do
+ :eden ->
+ defp chain_type_address_preloads,
+ do: [fee_payer_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]]
+
+ defp extend_data_with_chain_type_fields(data, transaction) do
+ # credo:disable-for-next-line Credo.Check.Design.AliasUsage
+ BlockScoutWeb.API.V2.EdenView.extend_transaction_interpretation_request(data, transaction)
+ end
+
+ _ ->
+ defp chain_type_address_preloads, do: []
+
+ defp extend_data_with_chain_type_fields(data, _transaction), do: data
+ end
+
defp fetch_token_transfers(transaction) do
full_options =
[
diff --git a/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex b/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex
index e8dde140d33..d97fa17cd61 100644
--- a/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex
+++ b/apps/block_scout_web/lib/block_scout_web/models/get_address_tags.ex
@@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
Get various types of tags associated with the address
"""
- import Ecto.Query, only: [from: 2]
+ import Ecto.Query, only: [from: 2, select_merge: 3, where: 3]
import Explorer.Chain, only: [select_repo: 1]
@@ -27,6 +27,37 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
def get_address_tags(_, _, _), do: %{common_tags: [], personal_tags: [], watchlist_names: []}
+ @doc """
+ Same as `get_address_tags/3` for multiple addresses at once, using a single
+ query per tag type instead of a query per address per tag type.
+
+ Returns a map keyed by address hash with values of the same shape as
+ `get_address_tags/3` returns.
+ """
+ def get_address_tags_batch(address_hashes, current_user, opts \\ [])
+
+ def get_address_tags_batch([], _, _), do: %{}
+
+ def get_address_tags_batch(address_hashes, current_user, opts) do
+ common_tags = address_hashes |> get_tags_on_addresses(opts) |> Enum.group_by(& &1.address_hash)
+
+ personal_tags = address_hashes |> get_personal_tags_on_addresses(current_user) |> Enum.group_by(& &1.address_hash)
+
+ watchlist_names =
+ address_hashes
+ |> get_watchlist_names_on_addresses(current_user)
+ |> Enum.group_by(& &1.address_hash, &Map.delete(&1, :address_hash))
+
+ Map.new(address_hashes, fn address_hash ->
+ {address_hash,
+ %{
+ common_tags: Map.get(common_tags, address_hash, []),
+ personal_tags: Map.get(personal_tags, address_hash, []),
+ watchlist_names: Map.get(watchlist_names, address_hash, [])
+ }}
+ end)
+ end
+
def get_public_tags(address_hash, opts \\ []) when not is_nil(address_hash) do
%{
common_tags: get_tags_on_address(address_hash, opts)
@@ -36,46 +67,82 @@ defmodule BlockScoutWeb.Models.GetAddressTags do
def get_tags_on_address(address_hash, opts \\ [])
def get_tags_on_address(address_hash, opts) when not is_nil(address_hash) do
- query =
- from(
- tt in AddressTag,
- left_join: att in AddressToTag,
- on: tt.id == att.tag_id,
- where: att.address_hash == ^address_hash,
- where: tt.label != ^"validator",
- select: %{label: tt.label, display_name: tt.display_name, address_hash: att.address_hash}
- )
-
- select_repo(opts).all(query)
+ common_tags_base_query()
+ |> where([tt, att], att.address_hash == ^address_hash)
+ |> select_repo(opts).all()
end
def get_tags_on_address(_, _), do: []
+ defp get_tags_on_addresses(address_hashes, opts) do
+ common_tags_base_query()
+ |> where([tt, att], att.address_hash in ^address_hashes)
+ |> select_repo(opts).all()
+ end
+
+ defp common_tags_base_query do
+ from(
+ tt in AddressTag,
+ left_join: att in AddressToTag,
+ on: tt.id == att.tag_id,
+ where: tt.label != ^"validator",
+ select: %{label: tt.label, display_name: tt.display_name, address_hash: att.address_hash}
+ )
+ end
+
def get_personal_tags(address_hash, %{id: id}) when not is_nil(address_hash) do
- query =
- from(
- ta in TagAddress,
- where: ta.address_hash_hash == ^address_hash,
- where: ta.identity_id == ^id,
- select: %{label: ta.name, display_name: ta.name, address_hash: ta.address_hash}
- )
-
- Repo.account_repo().all(query)
+ id
+ |> personal_tags_base_query()
+ |> where([ta], ta.address_hash_hash == ^address_hash)
+ |> Repo.account_repo().all()
end
def get_personal_tags(_, _), do: []
+ defp get_personal_tags_on_addresses(address_hashes, %{id: id}) do
+ id
+ |> personal_tags_base_query()
+ |> where([ta], ta.address_hash_hash in ^address_hashes)
+ |> Repo.account_repo().all()
+ end
+
+ defp get_personal_tags_on_addresses(_, _), do: []
+
+ defp personal_tags_base_query(identity_id) do
+ from(
+ ta in TagAddress,
+ where: ta.identity_id == ^identity_id,
+ select: %{label: ta.name, display_name: ta.name, address_hash: ta.address_hash}
+ )
+ end
+
def get_watchlist_names_on_address(address_hash, %{watchlist_id: watchlist_id}) when not is_nil(address_hash) do
- query =
- from(
- wa in WatchlistAddress,
- where: wa.address_hash_hash == ^address_hash,
- where: wa.watchlist_id == ^watchlist_id,
- select: %{label: wa.name, display_name: wa.name}
- )
-
- Repo.account_repo().all(query)
+ watchlist_id
+ |> watchlist_names_base_query()
+ |> where([wa], wa.address_hash_hash == ^address_hash)
+ |> Repo.account_repo().all()
end
def get_watchlist_names_on_address(_, _), do: []
+
+ defp get_watchlist_names_on_addresses(address_hashes, %{watchlist_id: watchlist_id}) do
+ watchlist_id
+ |> watchlist_names_base_query()
+ |> where([wa], wa.address_hash_hash in ^address_hashes)
+ |> select_merge([wa], %{address_hash: wa.address_hash})
+ |> Repo.account_repo().all()
+ end
+
+ defp get_watchlist_names_on_addresses(_, _), do: []
+
+ # `address_hash` is not selected here on purpose: the per-address variant
+ # returns these maps directly in the API response, so the batch variant
+ # `select_merge`s it in only for grouping.
+ defp watchlist_names_base_query(watchlist_id) do
+ from(
+ wa in WatchlistAddress,
+ where: wa.watchlist_id == ^watchlist_id,
+ select: %{label: wa.name, display_name: wa.name}
+ )
+ end
end
diff --git a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex
index 137c7707534..bf43d1705f6 100644
--- a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex
+++ b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex
@@ -4,6 +4,8 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do
Transaction state changes related functions
"""
+ use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
+
import Explorer.Chain.Address.Reputation, only: [reputation_association: 0]
import Explorer.PagingOptions, only: [default_paging_options: 0]
import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0]
@@ -81,14 +83,16 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do
block_transactions
|> Enum.find(&(&1.hash == transaction.hash))
|> Repo.preload(
- token_transfers: [
- token: reputation_association(),
+ [
+ token_transfers: [
+ token: reputation_association(),
+ from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()],
+ to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]
+ ],
+ block: [miner: [:names, :smart_contract, proxy_implementations_association()]],
from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()],
to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]
- ],
- block: [miner: [:names, :smart_contract, proxy_implementations_association()]],
- from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()],
- to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]
+ ] ++ chain_type_address_preloads()
)
|> Transaction.preload_internal_transactions(
from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()],
@@ -115,8 +119,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do
end
defp transaction_to_coin_balances(transaction, previous_block_number, options) do
- Enum.reduce(
- transaction.internal_transactions,
+ initial_coin_balances =
%{
transaction.from_address_hash =>
{transaction.from_address, coin_balance(transaction.from_address_hash, previous_block_number, options)},
@@ -124,11 +127,82 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do
{transaction.to_address, coin_balance(transaction.to_address_hash, previous_block_number, options)},
transaction.block.miner_hash =>
{transaction.block.miner, coin_balance(transaction.block.miner_hash, previous_block_number, options)}
- },
+ }
+ |> put_fee_payer_coin_balance(transaction, previous_block_number, options)
+ |> put_calls_recipients_coin_balances(transaction, previous_block_number, options)
+
+ Enum.reduce(
+ transaction.internal_transactions,
+ initial_coin_balances,
&internal_transaction_to_coin_balances(&1, previous_block_number, options, &2)
)
end
+ if @chain_type == :eden do
+ defp put_fee_payer_coin_balance(
+ coin_balances,
+ %Transaction{fee_payer_address_hash: fee_payer_address_hash} = transaction,
+ previous_block_number,
+ options
+ )
+ when not is_nil(fee_payer_address_hash) do
+ Map.put_new_lazy(coin_balances, fee_payer_address_hash, fn ->
+ {transaction.fee_payer_address, coin_balance(fee_payer_address_hash, previous_block_number, options)}
+ end)
+ end
+ end
+
+ defp put_fee_payer_coin_balance(coin_balances, _transaction, _previous_block_number, _options), do: coin_balances
+
+ if @chain_type == :eden do
+ alias Explorer.Chain.Address
+
+ defp put_calls_recipients_coin_balances(coin_balances, transaction, previous_block_number, options) do
+ transaction
+ |> Transaction.calls_value_by_recipient()
+ |> Map.keys()
+ |> Enum.reject(&Map.has_key?(coin_balances, &1))
+ |> case do
+ [] ->
+ coin_balances
+
+ address_hashes ->
+ addresses = hashes_to_addresses(address_hashes, options)
+
+ Enum.reduce(address_hashes, coin_balances, fn address_hash, acc ->
+ Map.put(
+ acc,
+ address_hash,
+ {addresses[address_hash] || %Address{hash: address_hash},
+ coin_balance(address_hash, previous_block_number, options)}
+ )
+ end)
+ end
+ end
+
+ defp hashes_to_addresses(address_hashes, options) do
+ address_hashes
+ |> Chain.hashes_to_addresses(options)
+ |> Chain.select_repo(options).preload([
+ :scam_badge,
+ :names,
+ :smart_contract,
+ proxy_implementations_association()
+ ])
+ |> Map.new(&{&1.hash, &1})
+ end
+ else
+ defp put_calls_recipients_coin_balances(coin_balances, _transaction, _previous_block_number, _options),
+ do: coin_balances
+ end
+
+ if @chain_type == :eden do
+ defp chain_type_address_preloads,
+ do: [fee_payer_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]]
+ else
+ defp chain_type_address_preloads, do: []
+ end
+
defp internal_transaction_to_coin_balances(
%InternalTransaction{call_type: call_type, call_type_enum: call_type_enum},
_,
diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
index a94af802da9..95f20f7b514 100644
--- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
+++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
@@ -190,6 +190,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
get("/:transaction_hash_param/raw-trace", V2.TransactionController, :raw_trace)
get("/:transaction_hash_param/state-changes", V2.TransactionController, :state_changes)
get("/:transaction_hash_param/summary", V2.TransactionController, :summary)
+ get("/:transaction_hash_param/preview", V2.TransactionController, :preview)
chain_scope :neon do
get("/:transaction_hash_param/external-transactions", V2.TransactionController, :external_transactions)
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/eden/call.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/eden/call.ex
new file mode 100644
index 00000000000..de04fcad897
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/eden/call.ex
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: LicenseRef-Blockscout
+defmodule BlockScoutWeb.Schemas.API.V2.Eden.Call do
+ @moduledoc false
+ require OpenApiSpex
+ alias BlockScoutWeb.Schemas.API.V2.General
+
+ OpenApiSpex.schema(%{
+ type: :object,
+ nullable: false,
+ properties: %{
+ to: General.AddressHashNullable,
+ value: General.IntegerString,
+ input: General.HexString
+ },
+ required: [:to, :value, :input],
+ additionalProperties: false
+ })
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex
index 0033c5e4cc8..1322be3f59d 100644
--- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex
@@ -3,6 +3,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do
@moduledoc false
alias BlockScoutWeb.API.V2.ZkSyncView
alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token}
+ alias BlockScoutWeb.Schemas.API.V2.Eden.Call, as: EdenCall
alias BlockScoutWeb.Schemas.API.V2.Transaction.Fee
alias BlockScoutWeb.Schemas.Helper
alias OpenApiSpex.Schema
@@ -142,18 +143,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do
additionalProperties: false
}
- @eden_call_schema %Schema{
- type: :object,
- nullable: false,
- properties: %{
- to: General.AddressHashNullable,
- value: General.IntegerString,
- input: General.HexString
- },
- required: [:to, :value, :input],
- additionalProperties: false
- }
-
@doc """
Applies chain-specific field customizations to the given schema based on the configured chain type.
@@ -216,7 +205,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do
|> Helper.extend_schema(
properties: %{
fee_payer: %Schema{allOf: [Address], nullable: true},
- calls: %Schema{type: :array, items: @eden_call_schema, nullable: true}
+ calls: %Schema{type: :array, items: EdenCall, nullable: true}
},
required: [:fee_payer, :calls]
)
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/preview.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/preview.ex
new file mode 100644
index 00000000000..505fe3886f0
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/preview.ex
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: LicenseRef-Blockscout
+defmodule BlockScoutWeb.Schemas.API.V2.Transaction.PreviewAddress do
+ @moduledoc """
+ Lightweight address representation used in the transaction preview endpoint.
+ Contains only the hash, display name, ENS domain name, and metadata tags.
+ """
+ require OpenApiSpex
+
+ alias BlockScoutWeb.Schemas.API.V2.{General, Proxy}
+ alias OpenApiSpex.Schema
+
+ OpenApiSpex.schema(%{
+ description: "Lightweight address in transaction preview",
+ type: :object,
+ nullable: true,
+ properties: %{
+ hash: General.AddressHash,
+ name: %Schema{
+ type: :string,
+ description: "Display name of the address (contract name or address name)",
+ nullable: true
+ },
+ ens_domain_name: %Schema{
+ type: :string,
+ description: "ENS domain name associated with the address. Populated only when preload_ens=true is passed.",
+ nullable: true
+ },
+ metadata: %Schema{
+ allOf: [Proxy.Metadata],
+ description:
+ "Address metadata tags from the Metadata microservice. Populated only when preload_metadata=true is passed.",
+ nullable: true
+ }
+ },
+ required: [:hash, :name, :ens_domain_name, :metadata],
+ additionalProperties: false
+ })
+end
+
+defmodule BlockScoutWeb.Schemas.API.V2.Transaction.Preview do
+ @moduledoc """
+ Schema for the lightweight transaction preview response returned by
+ `GET /api/v2/transactions/:transaction_hash/preview`.
+
+ Used for rendering OG/social-media previews with minimal data.
+ """
+ require OpenApiSpex
+
+ alias BlockScoutWeb.Schemas.API.V2.General
+ alias BlockScoutWeb.Schemas.API.V2.Transaction.PreviewAddress
+ alias OpenApiSpex.Schema
+
+ OpenApiSpex.schema(%{
+ description: "Lightweight transaction preview for social media embeds",
+ type: :object,
+ properties: %{
+ status: %Schema{
+ type: :string,
+ enum: ["ok", "error"],
+ nullable: true,
+ description: "Transaction execution status"
+ },
+ timestamp: General.TimestampNullable,
+ method: General.MethodNameNullable,
+ from: %Schema{allOf: [PreviewAddress], nullable: true},
+ to: %Schema{allOf: [PreviewAddress], nullable: true}
+ },
+ required: [:status, :timestamp, :method, :from, :to],
+ additionalProperties: false
+ })
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex
index ccbf520583a..e9af52cf66b 100644
--- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex
@@ -5,7 +5,11 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.SummaryJustRequestBody do
"""
require OpenApiSpex
+ use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
+
alias BlockScoutWeb.Schemas.API.V2.{Address, InternalTransaction, Log, TokenTransfer}
+ alias BlockScoutWeb.Schemas.API.V2.Eden.Call, as: EdenCall
+ alias BlockScoutWeb.Schemas.Helper
alias OpenApiSpex.Schema
logs_data_schema = %Schema{
@@ -50,6 +54,20 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.SummaryJustRequestBody do
additionalProperties: false
}
+ data_schema =
+ case @chain_type do
+ :eden ->
+ Helper.extend_schema(data_schema,
+ properties: %{
+ fee_payer: %Schema{allOf: [Address], nullable: true},
+ calls: %Schema{type: :array, items: EdenCall, nullable: true}
+ }
+ )
+
+ _ ->
+ data_schema
+ end
+
OpenApiSpex.schema(%{
type: :object,
required: [:data],
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
index 8f62977ef20..3ae83b75cf3 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
@@ -109,6 +109,8 @@ defmodule BlockScoutWeb.API.V2.AddressView do
creation_transaction_hash = creator_hash && AddressView.transaction_hash(address)
token = address.token && TokenView.render("token.json", %{token: address.token})
+ existence_checks = Counters.address_existence_checks(address.hash, @api_true)
+
extended_info =
Map.merge(base_info, %{
"creator_address_hash" => creator_hash && Address.checksum(creator_hash),
@@ -118,12 +120,12 @@ defmodule BlockScoutWeb.API.V2.AddressView do
"coin_balance" => balance,
"exchange_rate" => exchange_rate,
"block_number_balance_updated_at" => address.fetched_coin_balance_block_number,
- "has_validated_blocks" => Counters.check_if_validated_blocks_at_address(address.hash, @api_true),
- "has_logs" => Counters.check_if_logs_at_address(address.hash, @api_true),
- "has_tokens" => Counters.check_if_tokens_at_address(address.hash, @api_true),
- "has_token_transfers" => Counters.check_if_token_transfers_at_address(address.hash, @api_true),
+ "has_validated_blocks" => existence_checks.has_validated_blocks,
+ "has_logs" => existence_checks.has_logs,
+ "has_tokens" => existence_checks.has_tokens,
+ "has_token_transfers" => existence_checks.has_token_transfers,
"watchlist_address_id" => WatchlistAddress.select_watchlist_address_id(get_watchlist_id(conn), address.hash),
- "has_beacon_chain_withdrawals" => Counters.check_if_withdrawals_at_address(address.hash, @api_true)
+ "has_beacon_chain_withdrawals" => existence_checks.has_beacon_chain_withdrawals
})
extended_info
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/eden_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/eden_view.ex
index 7b347627c3d..388963f63eb 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/eden_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/eden_view.ex
@@ -49,12 +49,40 @@ defmodule BlockScoutWeb.API.V2.EdenView do
|> Map.put("calls", prepare_calls(transaction.calls, single_transaction?))
end
+ @doc """
+ Extends the body of the transaction interpretation request with the Eden-specific fields.
+
+ Without the calls the service has no way to tell that the transaction is a batch: its `to` and
+ `value` are the compatibility fields derived from the first call and from the sum of all the
+ calls, and the batched calls produce no internal transactions.
+
+ ## Parameters
+ - `data`: A map defining the `data` part of the request body which will be extended.
+ - `transaction`: The transaction structure.
+
+ ## Returns
+ - A map extended with the data related to Eden.
+ """
+ @spec extend_transaction_interpretation_request(map(), Transaction.t()) :: map()
+ def extend_transaction_interpretation_request(data, %Transaction{} = transaction) do
+ data
+ |> Map.put(
+ :fee_payer,
+ APIHelper.address_with_info(nil, transaction.fee_payer_address, transaction.fee_payer_address_hash, false)
+ )
+ |> Map.put(:calls, prepare_calls(transaction.calls))
+ end
+
@spec prepare_calls(term(), boolean()) :: [map()] | nil
- defp prepare_calls(calls, true = _single_transaction?) when is_list(calls),
- do: Enum.map(calls, &prepare_call/1)
+ defp prepare_calls(calls, true = _single_transaction?), do: prepare_calls(calls)
defp prepare_calls(_calls, _single_transaction?), do: nil
+ @spec prepare_calls(term()) :: [map()] | nil
+ defp prepare_calls(calls) when is_list(calls), do: Enum.map(calls, &prepare_call/1)
+
+ defp prepare_calls(_calls), do: nil
+
# A call is stored as it comes from the JSON RPC response: `to` is a plain address string which
# is `nil` for a contract creation call, `value` is an integer and `input` is a hex string.
# Renders it the way the rest of the API v2 renders those types: a checksummed address and a
@@ -74,5 +102,8 @@ defmodule BlockScoutWeb.API.V2.EdenView do
else
def extend_transaction_json_response(out_json, _, _, _, _),
do: out_json
+
+ def extend_transaction_interpretation_request(data, _),
+ do: data
end
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
index ea98179d65b..bfd51669cd3 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex
@@ -33,6 +33,20 @@ defmodule BlockScoutWeb.API.V2.Helper do
})
end
+ def address_with_info(_conn, address, address_hash, true, {:address_tags, address_tags}) do
+ %{
+ common_tags: public_tags,
+ personal_tags: private_tags,
+ watchlist_names: watchlist_names
+ } = Map.get(address_tags, address_hash, %{common_tags: [], personal_tags: [], watchlist_names: []})
+
+ Map.merge(address_with_info(address, address_hash), %{
+ "private_tags" => private_tags,
+ "watchlist_names" => watchlist_names,
+ "public_tags" => public_tags
+ })
+ end
+
def address_with_info(_conn, address, address_hash, false, nil) do
Map.merge(address_with_info(address, address_hash), %{
"private_tags" => [],
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
index 4edf0f243cf..14418b66410 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
@@ -9,7 +9,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
alias BlockScoutWeb.API.V2.{ApiView, Helper, InternalTransactionView, TokenTransferView, TokenView}
- alias BlockScoutWeb.Models.GetTransactionTags
+ alias BlockScoutWeb.Models.{GetAddressTags, GetTransactionTags}
alias BlockScoutWeb.{TransactionStateView, TransactionView}
alias Ecto.Association.NotLoaded
alias Explorer.{Chain, Market}
@@ -27,6 +27,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
}
alias Explorer.Chain.Block.Reward
+ alias Explorer.Chain.Cache.BlockNumber
alias Explorer.Chain.Cache.Counters.AverageBlockTime
alias Explorer.Chain.SmartContract.Proxy.Models.Implementation, as: ProxyImplementation
alias Explorer.Chain.Transaction.StateChange
@@ -78,7 +79,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
end
def render("transaction.json", %{transaction: transaction, conn: conn}) do
- block_height = Chain.block_height(@api_true)
+ block_height = BlockNumber.get_max()
[decoded_input] = Transaction.decode_transactions([transaction], false, @api_true)
transaction
@@ -86,6 +87,22 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
|> prepare_transaction(conn, true, block_height, nil, decoded_input, nil)
end
+ def render("preview.json", %{transaction: transaction, decode_input: decode_input}) do
+ decoded_input =
+ if decode_input do
+ [decoded] = Transaction.decode_transactions([transaction], true, @api_true)
+ decoded
+ end
+
+ %{
+ "status" => transaction.status,
+ "timestamp" => block_timestamp(transaction),
+ "method" => Transaction.method_name(transaction, decoded_input),
+ "from" => preview_address(transaction.from_address, transaction.from_address_hash),
+ "to" => preview_address(transaction.to_address, transaction.to_address_hash)
+ }
+ end
+
def render("raw_trace.json", %{raw_traces: raw_traces}) do
raw_traces
end
@@ -409,7 +426,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
end
defp prepare_transactions(transactions, conn, watchlist_names) do
- block_height = Chain.block_height(@api_true)
+ block_height = BlockNumber.get_max()
decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
historic_exchange_rates = historic_exchange_rates(transactions)
@@ -492,6 +509,13 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
block_timestamp = block_timestamp(transaction)
+ tags_data =
+ if single_transaction? do
+ batched_address_tags(transaction, conn)
+ else
+ watchlist_names
+ end
+
result = %{
"hash" => transaction.hash,
"result" => status,
@@ -504,7 +528,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
transaction.from_address,
transaction.from_address_hash,
single_transaction?,
- watchlist_names
+ tags_data
),
"to" =>
Helper.address_with_info(
@@ -512,7 +536,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
transaction.to_address,
transaction.to_address_hash,
single_transaction?,
- watchlist_names
+ tags_data
),
"created_contract" =>
Helper.address_with_info(
@@ -520,7 +544,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
transaction.created_contract_address,
transaction.created_contract_address_hash,
single_transaction?,
- watchlist_names
+ tags_data
),
"confirmations" => transaction.block |> Chain.confirmations(block_height: block_height) |> format_confirmations(),
"confirmation_duration" => processing_time_duration(transaction),
@@ -562,6 +586,17 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
transaction.block && transaction.block.base_fee_per_gas
end
+ # Fetches tags for from/to/created addresses with one query per tag type
+ # instead of up to three queries per address.
+ defp batched_address_tags(transaction, conn) do
+ address_hashes =
+ [transaction.from_address_hash, transaction.to_address_hash, transaction.created_contract_address_hash]
+ |> Enum.reject(&is_nil/1)
+ |> Enum.uniq()
+
+ {:address_tags, GetAddressTags.get_address_tags_batch(address_hashes, current_user(conn), @api_true)}
+ end
+
defp gas_price_for_display(transaction) do
transaction.gas_price || Transaction.effective_gas_price(transaction)
end
@@ -900,6 +935,26 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
def block_timestamp(%Block{} = block), do: block.timestamp
def block_timestamp(_), do: nil
+ defp preview_address(%Address{} = address, _hash) do
+ %{
+ "hash" => Address.checksum(address),
+ "name" => Helper.address_name(address),
+ "ens_domain_name" => address.ens_domain_name,
+ "metadata" => address.metadata
+ }
+ end
+
+ defp preview_address(_, nil), do: nil
+
+ defp preview_address(_, hash) do
+ %{
+ "hash" => Address.checksum(hash),
+ "name" => nil,
+ "ens_domain_name" => nil,
+ "metadata" => nil
+ }
+ end
+
defp prepare_state_change(%StateChange{} = state_change) do
coin_or_transfer =
if state_change.coin_or_token_transfers == :coin,
diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_state_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_state_view.ex
index 2c6db6e6d91..e4a20d908d8 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/transaction_state_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_state_view.ex
@@ -5,7 +5,7 @@ defmodule BlockScoutWeb.TransactionStateView do
alias Explorer.Chain
alias Explorer.Chain.{Address, Wei}
- import Explorer.Chain.Transaction.StateChange, only: [from_loss: 1, has_diff?: 1, to_profit: 1]
+ import Explorer.Chain.Transaction.StateChange, only: [fee_payer_loss: 1, from_loss: 1, has_diff?: 1, to_profit: 1]
def not_negative?(%Wei{value: val}) do
not Decimal.negative?(val)
@@ -24,7 +24,7 @@ defmodule BlockScoutWeb.TransactionStateView do
end
def has_state_changes?(transaction) do
- has_diff?(from_loss(transaction)) or has_diff?(to_profit(transaction))
+ has_diff?(from_loss(transaction)) or has_diff?(to_profit(transaction)) or has_diff?(fee_payer_loss(transaction))
end
def display_value(balance, :coin, _token_id) do
diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs
index fa41e768ee7..2f744302126 100644
--- a/apps/block_scout_web/mix.exs
+++ b/apps/block_scout_web/mix.exs
@@ -20,7 +20,7 @@ defmodule BlockScoutWeb.Mixfile do
lockfile: "../../mix.lock",
package: package(),
start_permanent: Mix.env() == :prod,
- version: "11.2.6",
+ version: "11.2.7",
xref: [
exclude: [
Explorer.Chain.Beacon.Reader,
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
index af47396f174..8c4338e39cf 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
@@ -3447,6 +3447,174 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
assert item["calls"] == nil
assert "sponsored_transaction" in item["transaction_types"]
end
+
+ test "attributes the fee state change to the fee payer instead of the sender", %{conn: conn} do
+ block_before = insert(:block)
+ fee_payer = insert(:address)
+ recipient = insert(:address)
+
+ transaction =
+ :transaction
+ |> insert(
+ type: 118,
+ fee_payer_address_hash: fee_payer.hash,
+ calls: [%{"to" => to_string(recipient.hash), "value" => 0, "input" => "0xdeadbeef"}],
+ to_address: recipient,
+ gas_price: 1_000,
+ value: 0
+ )
+ |> with_block(status: :ok, gas_used: 147)
+
+ insert_coin_balances_before(transaction, [fee_payer, recipient], block_before)
+
+ request = get(conn, "/api/v2/transactions/#{transaction.hash}/state-changes")
+
+ assert response = json_response(request, 200)
+
+ changes =
+ Map.new(response["items"], fn item -> {item["address"]["hash"], item["change"]} end)
+
+ assert changes[Address.checksum(fee_payer.hash)] == "-147000"
+
+ refute Map.has_key?(changes, Address.checksum(transaction.from_address_hash))
+
+ assert changes[Address.checksum(transaction.block.miner_hash)] == "147000"
+ end
+
+ test "credits every recipient of the batched calls with its own value", %{conn: conn} do
+ block_before = insert(:block)
+ fee_payer = insert(:address)
+ recipient_a = insert(:address)
+ recipient_b = insert(:address)
+
+ transaction =
+ :transaction
+ |> insert(
+ type: 118,
+ fee_payer_address_hash: fee_payer.hash,
+ calls: [
+ %{"to" => to_string(recipient_a.hash), "value" => 1, "input" => "0x"},
+ %{"to" => to_string(recipient_b.hash), "value" => 2, "input" => "0x"}
+ ],
+ to_address: recipient_a,
+ value: 3,
+ gas_price: 1_000
+ )
+ |> with_block(status: :ok, gas_used: 147)
+
+ insert_coin_balances_before(transaction, [fee_payer, recipient_a, recipient_b], block_before)
+
+ request = get(conn, "/api/v2/transactions/#{transaction.hash}/state-changes")
+
+ assert response = json_response(request, 200)
+
+ changes =
+ Map.new(response["items"], fn item -> {item["address"]["hash"], item["change"]} end)
+
+ assert changes[Address.checksum(fee_payer.hash)] == "-147000"
+ assert changes[Address.checksum(transaction.from_address_hash)] == "-3"
+ assert changes[Address.checksum(recipient_a.hash)] == "1"
+ assert changes[Address.checksum(recipient_b.hash)] == "2"
+ end
+
+ test "sums up the batched calls sharing the same recipient", %{conn: conn} do
+ block_before = insert(:block)
+ fee_payer = insert(:address)
+ recipient = insert(:address)
+
+ transaction =
+ :transaction
+ |> insert(
+ type: 118,
+ fee_payer_address_hash: fee_payer.hash,
+ calls: [
+ %{"to" => to_string(recipient.hash), "value" => 1, "input" => "0x"},
+ %{"to" => to_string(recipient.hash), "value" => 2, "input" => "0x"}
+ ],
+ to_address: recipient,
+ value: 3,
+ gas_price: 1_000
+ )
+ |> with_block(status: :ok, gas_used: 147)
+
+ insert_coin_balances_before(transaction, [fee_payer, recipient], block_before)
+
+ request = get(conn, "/api/v2/transactions/#{transaction.hash}/state-changes")
+
+ assert response = json_response(request, 200)
+
+ changes =
+ Map.new(response["items"], fn item -> {item["address"]["hash"], item["change"]} end)
+
+ assert changes[Address.checksum(transaction.from_address_hash)] == "-3"
+ assert changes[Address.checksum(recipient.hash)] == "3"
+ end
+
+ test "credits the recipients of the calls which have no address indexed yet", %{conn: conn} do
+ block_before = insert(:block)
+ fee_payer = insert(:address)
+ recipient_hash = "0x11f60a633dd30a8d1a26dd6e20167a9293fb4647"
+
+ transaction =
+ :transaction
+ |> insert(
+ type: 118,
+ fee_payer_address_hash: fee_payer.hash,
+ calls: [%{"to" => recipient_hash, "value" => 3, "input" => "0x"}],
+ value: 3,
+ gas_price: 1_000
+ )
+ |> with_block(status: :ok, gas_used: 147)
+
+ insert_coin_balances_before(transaction, [fee_payer], block_before)
+
+ request = get(conn, "/api/v2/transactions/#{transaction.hash}/state-changes")
+
+ assert response = json_response(request, 200)
+
+ changes =
+ Map.new(response["items"], fn item -> {item["address"]["hash"], item["change"]} end)
+
+ assert changes[Address.checksum(recipient_hash)] == "3"
+ end
+
+ test "charges the sender for both the value and the fee for regular transactions", %{conn: conn} do
+ block_before = insert(:block)
+
+ transaction =
+ :transaction
+ |> insert(type: 2, gas_price: 1_000, value: 3)
+ |> with_block(status: :ok, gas_used: 147)
+
+ insert_coin_balances_before(transaction, [], block_before)
+
+ request = get(conn, "/api/v2/transactions/#{transaction.hash}/state-changes")
+
+ assert response = json_response(request, 200)
+
+ changes =
+ Map.new(response["items"], fn item -> {item["address"]["hash"], item["change"]} end)
+
+ assert changes[Address.checksum(transaction.from_address_hash)] == "-147003"
+ assert changes[Address.checksum(transaction.to_address_hash)] == "3"
+ end
+ end
+
+ defp insert_coin_balances_before(transaction, extra_addresses, block_before) do
+ [
+ {transaction.from_address, transaction.from_address_hash},
+ {transaction.to_address, transaction.to_address_hash},
+ {transaction.block.miner, transaction.block.miner_hash}
+ ]
+ |> Enum.concat(Enum.map(extra_addresses, &{&1, &1.hash}))
+ |> Enum.uniq_by(fn {_address, address_hash} -> address_hash end)
+ |> Enum.each(fn {address, address_hash} ->
+ insert(:address_coin_balance,
+ address: address,
+ address_hash: address_hash,
+ block_number: block_before.number
+ )
+ end)
end
end
@@ -3498,4 +3666,110 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
end
end
end
+
+ describe "/transactions/{transaction_hash}/preview" do
+ setup do
+ bypass = Bypass.open()
+ old_bens_env = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.BENS, [])
+ old_metadata_env = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, [])
+ old_chain_id = Application.get_env(:block_scout_web, :chain_id)
+
+ Application.put_env(:block_scout_web, :chain_id, 1)
+
+ Application.put_env(
+ :explorer,
+ Explorer.MicroserviceInterfaces.BENS,
+ Keyword.merge(old_bens_env || [],
+ service_url: "http://localhost:#{bypass.port}",
+ enabled: true
+ )
+ )
+
+ Application.put_env(
+ :explorer,
+ Explorer.MicroserviceInterfaces.Metadata,
+ Keyword.merge(old_metadata_env || [],
+ service_url: "http://localhost:#{bypass.port}",
+ enabled: true
+ )
+ )
+
+ on_exit(fn ->
+ Bypass.down(bypass)
+ Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, old_bens_env)
+ Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, old_metadata_env)
+ Application.put_env(:block_scout_web, :chain_id, old_chain_id)
+ end)
+
+ {:ok, bypass: bypass}
+ end
+
+ test "preloads ENS names when preload_ens is requested", %{conn: conn, bypass: bypass} do
+ transaction = :transaction |> insert() |> with_block(status: :ok)
+ from_hash = Address.checksum(transaction.from_address_hash)
+
+ Bypass.expect_once(bypass, "POST", "/api/v1/1/addresses:batch_resolve_names", fn conn ->
+ Plug.Conn.resp(conn, 200, Jason.encode!(%{"names" => %{from_hash => "preview.eth"}}))
+ end)
+
+ request =
+ get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/preview", %{"preload_ens" => "true"})
+
+ assert %{"from" => %{"ens_domain_name" => "preview.eth"}} = json_response(request, 200)
+ end
+
+ test "preloads metadata when preload_metadata is requested", %{conn: conn, bypass: bypass} do
+ transaction = :transaction |> insert() |> with_block(status: :ok)
+ from_hash = Address.checksum(transaction.from_address_hash)
+ to_hash = Address.checksum(transaction.to_address_hash)
+
+ metadata_tag = %{
+ "slug" => "test-1",
+ "name" => "Test 1",
+ "tagType" => "name",
+ "ordinal" => 0,
+ "meta" => Jason.encode!(%{})
+ }
+
+ Bypass.expect_once(bypass, "GET", "/api/v1/metadata", fn conn ->
+ Plug.Conn.resp(
+ conn,
+ 200,
+ Jason.encode!(%{
+ "addresses" => %{
+ from_hash => %{"tags" => [metadata_tag]},
+ to_hash => %{"tags" => [metadata_tag]}
+ }
+ })
+ )
+ end)
+
+ request =
+ get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/preview", %{"preload_metadata" => "true"})
+
+ response = json_response(request, 200)
+
+ assert %{"from" => %{"metadata" => %{"tags" => [%{"name" => "Test 1"}]}}} = response
+ assert %{"to" => %{"metadata" => %{"tags" => [%{"name" => "Test 1"}]}}} = response
+ end
+
+ test "returns nil metadata when preload_metadata is not requested", %{conn: conn} do
+ transaction = :transaction |> insert() |> with_block(status: :ok)
+
+ request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/preview")
+
+ response = json_response(request, 200)
+ assert %{"from" => %{"metadata" => nil}} = response
+ assert %{"to" => %{"metadata" => nil}} = response
+ end
+
+ test "queries no microservice when neither preload is requested", %{conn: conn} do
+ transaction = :transaction |> insert() |> with_block(status: :ok)
+
+ # any request to the bypass would fail the test, since nothing is expected
+ request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/preview")
+
+ assert %{"from" => %{"ens_domain_name" => nil}} = json_response(request, 200)
+ end
+ end
end
diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs
index 7433a6b65eb..969822e6ebc 100644
--- a/apps/ethereum_jsonrpc/mix.exs
+++ b/apps/ethereum_jsonrpc/mix.exs
@@ -20,7 +20,7 @@ defmodule EthereumJSONRPC.MixProject do
elixirc_paths: elixirc_paths(Mix.env()),
lockfile: "../../mix.lock",
start_permanent: Mix.env() == :prod,
- version: "11.2.6"
+ version: "11.2.7"
]
end
@@ -76,7 +76,7 @@ defmodule EthereumJSONRPC.MixProject do
{:spandex, "~> 3.0"},
# `:spandex` integration with Datadog
{:spandex_datadog, "~> 1.0"},
- {:tesla, "~> 1.20.0"},
+ {:tesla, "~> 1.21.0"},
# Convert unix timestamps in JSONRPC to DateTimes
{:timex, "~> 3.7.1"},
# Encode/decode function names and arguments
diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs
index 9bbc6129523..eedd6b54845 100644
--- a/apps/explorer/config/config.exs
+++ b/apps/explorer/config/config.exs
@@ -23,6 +23,8 @@ config :explorer, Explorer.ChainSpec.GenesisData, enabled: true
config :explorer, Explorer.Chain.Cache.BlockNumber, enabled: true
+config :explorer, Explorer.Chain.Cache.ContractMethods, enabled: true
+
config :explorer, Explorer.Chain.Cache.Counters.AddressesCoinBalanceSum,
enabled: true,
ttl_check_interval: :timer.seconds(1)
diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs
index 965d593c62c..1a6aaa300a2 100644
--- a/apps/explorer/config/test.exs
+++ b/apps/explorer/config/test.exs
@@ -49,6 +49,7 @@ config :explorer, :proxy,
caching_implementation_data_enabled: true,
implementation_data_ttl_via_avg_block_time: false,
fallback_cached_implementation_data_ttl: :timer.seconds(20),
+ empty_cached_implementation_data_ttl: :timer.seconds(20),
implementation_data_fetching_timeout: :timer.seconds(20)
account_database_url = System.get_env("TEST_DATABASE_READ_ONLY_API_URL") || database_url
@@ -102,6 +103,7 @@ end
config :logger, :explorer, path: Path.absname("logs/test/explorer.log")
+config :explorer, Explorer.Chain.Cache.ContractMethods, enabled: false
config :explorer, Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand, enabled: false
config :explorer, Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand, enabled: false
config :explorer, Explorer.Tags.AddressTag.Cataloger, enabled: false
diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex
index 50e8ee90a26..c2ad1da0359 100644
--- a/apps/explorer/lib/explorer/application.ex
+++ b/apps/explorer/lib/explorer/application.ex
@@ -32,11 +32,12 @@ defmodule Explorer.Application do
TransactionsCount
}
+ alias Explorer.Chain.Cache.ContractMethods, as: ContractMethodsCache
alias Explorer.Chain.Optimism.InteropMessage, as: OptimismInteropMessage
alias Explorer.Chain.Supply.RSK
alias Explorer.Market.MarketHistoryCache
- alias Explorer.MicroserviceInterfaces.MultichainSearch
+ alias Explorer.MicroserviceInterfaces.{HttpClient, MultichainSearch}
alias Explorer.Prometheus.Instrumenter
alias Explorer.Repo.PrometheusLogger
alias Explorer.Stats.HotSmartContractsCache
@@ -61,54 +62,63 @@ defmodule Explorer.Application do
)
# Children to start in all environments
- base_children = [
- Explorer.Repo,
- Explorer.Repo.Replica1,
- Explorer.Vault,
- Supervisor.child_spec({SpandexDatadog.ApiServer, datadog_opts()}, id: SpandexDatadog.ApiServer),
- Supervisor.child_spec({Task.Supervisor, name: Explorer.HistoryTaskSupervisor},
- id: Explorer.HistoryTaskSupervisor
- ),
- Supervisor.child_spec({Task.Supervisor, name: Explorer.MarketTaskSupervisor}, id: Explorer.MarketTaskSupervisor),
- Supervisor.child_spec({Task.Supervisor, name: Explorer.GenesisDataTaskSupervisor}, id: GenesisDataTaskSupervisor),
- Supervisor.child_spec({Task.Supervisor, name: Explorer.TaskSupervisor}, id: Explorer.TaskSupervisor),
- Supervisor.child_spec({Task.Supervisor, name: Explorer.LookUpSmartContractSourcesTaskSupervisor},
- id: LookUpSmartContractSourcesTaskSupervisor
- ),
- Supervisor.child_spec({Task.Supervisor, name: Explorer.WETHMigratorSupervisor}, id: WETHMigratorSupervisor),
- {Registry, keys: :duplicate, name: Registry.ChainEvents, id: Registry.ChainEvents},
- Accounts,
- AddressesCoinBalanceSum,
- AddressesCoinBalanceSumMinusBurnt,
- BackgroundMigrations,
- BlocksCount,
- BlockNumber,
- Blocks,
- ChainId,
- GasPriceOracle,
- GasUsageSum,
- PendingBlockOperationCount,
- PendingTransactionOperationCount,
- TransactionsCount,
- StateChanges,
- Transactions,
- Uncles,
- AddressTabsElementsCount,
- con_cache_child_spec(MarketHistoryCache.cache_name()),
- con_cache_child_spec(HotSmartContractsCache.cache_name(),
- ttl_check_interval: :timer.seconds(1),
- global_ttl: :infinity
- ),
- con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)),
- {Redix, redix_opts()},
- {Explorer.Utility.ReplicaAccessibilityManager, []},
- :hackney_pool.child_spec(:default,
- recv_timeout: 60_000,
- timeout: 60_000,
- max_connections: Application.get_env(:explorer, :hackney_default_pool_size)
- ),
- Explorer.Promo.Autoscout
- ]
+ base_children =
+ [
+ Explorer.Repo,
+ Explorer.Repo.Replica1,
+ Explorer.Vault,
+ Supervisor.child_spec({SpandexDatadog.ApiServer, datadog_opts()}, id: SpandexDatadog.ApiServer),
+ Supervisor.child_spec({Task.Supervisor, name: Explorer.HistoryTaskSupervisor},
+ id: Explorer.HistoryTaskSupervisor
+ ),
+ Supervisor.child_spec({Task.Supervisor, name: Explorer.MarketTaskSupervisor},
+ id: Explorer.MarketTaskSupervisor
+ ),
+ Supervisor.child_spec({Task.Supervisor, name: Explorer.GenesisDataTaskSupervisor},
+ id: GenesisDataTaskSupervisor
+ ),
+ Supervisor.child_spec({Task.Supervisor, name: Explorer.TaskSupervisor}, id: Explorer.TaskSupervisor),
+ Supervisor.child_spec({Task.Supervisor, name: Explorer.LookUpSmartContractSourcesTaskSupervisor},
+ id: LookUpSmartContractSourcesTaskSupervisor
+ ),
+ Supervisor.child_spec({Task.Supervisor, name: Explorer.WETHMigratorSupervisor}, id: WETHMigratorSupervisor),
+ {Registry, keys: :duplicate, name: Registry.ChainEvents, id: Registry.ChainEvents},
+ Accounts,
+ AddressesCoinBalanceSum,
+ AddressesCoinBalanceSumMinusBurnt,
+ BackgroundMigrations,
+ BlocksCount,
+ BlockNumber,
+ Blocks,
+ ChainId,
+ GasPriceOracle,
+ GasUsageSum,
+ PendingBlockOperationCount,
+ PendingTransactionOperationCount,
+ TransactionsCount,
+ StateChanges,
+ Transactions,
+ Uncles,
+ AddressTabsElementsCount,
+ con_cache_child_spec(MarketHistoryCache.cache_name()),
+ con_cache_child_spec(HotSmartContractsCache.cache_name(),
+ ttl_check_interval: :timer.seconds(1),
+ global_ttl: :infinity
+ ),
+ con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)),
+ con_cache_child_spec(ContractMethodsCache.cache_name(),
+ ttl_check_interval: :timer.minutes(1),
+ global_ttl: :infinity
+ ),
+ {Redix, redix_opts()},
+ {Explorer.Utility.ReplicaAccessibilityManager, []},
+ :hackney_pool.child_spec(:default,
+ recv_timeout: 60_000,
+ timeout: 60_000,
+ max_connections: Application.get_env(:explorer, :hackney_default_pool_size)
+ ),
+ Explorer.Promo.Autoscout
+ ] ++ HttpClient.pool_child_specs()
children = base_children ++ configurable_children()
diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex
index 736df749574..a127a18c07f 100644
--- a/apps/explorer/lib/explorer/chain.ex
+++ b/apps/explorer/lib/explorer/chain.ex
@@ -1147,6 +1147,68 @@ defmodule Explorer.Chain do
def get_token_transfers_per_transaction_preview_count, do: @token_transfers_per_transaction_preview
+ @doc """
+ Loads address-info associations for every address participating in the
+ transaction — `from`/`to`/`created_contract` plus each preloaded token
+ transfer's `from`/`to` — in a single query pass, deduplicating addresses
+ shared between the transaction and its token transfers.
+
+ All participants share `address_necessity_by_association` (typically with the
+ ABI-less smart-contract preload, see
+ `Explorer.Chain.SmartContract.association_without_abi/0`). The `to_address`
+ additionally gets the full `:smart_contract` association when it is a
+ contract, since transaction input and revert-reason decoding need its `abi`.
+ """
+ @spec preload_transaction_participants(Transaction.t(), %{any() => :optional | :required}, [api?]) ::
+ Transaction.t()
+ def preload_transaction_participants(%Transaction{} = transaction, address_necessity_by_association, options) do
+ token_transfers = if is_list(transaction.token_transfers), do: transaction.token_transfers, else: []
+
+ participant_hashes =
+ [
+ transaction.from_address_hash,
+ transaction.to_address_hash,
+ transaction.created_contract_address_hash
+ | Enum.flat_map(token_transfers, &[&1.from_address_hash, &1.to_address_hash])
+ ]
+ |> Enum.reject(&is_nil/1)
+ |> Enum.uniq()
+
+ addresses =
+ Address
+ |> where([address], address.hash in ^participant_hashes)
+ |> join_associations(address_necessity_by_association)
+ |> select_repo(options).all()
+ |> Map.new(&{&1.hash, &1})
+
+ to_address =
+ addresses
+ |> Map.get(transaction.to_address_hash)
+ |> preload_full_smart_contract(options)
+
+ %Transaction{
+ transaction
+ | from_address: Map.get(addresses, transaction.from_address_hash),
+ to_address: to_address,
+ created_contract_address: Map.get(addresses, transaction.created_contract_address_hash),
+ token_transfers:
+ Enum.map(token_transfers, fn %TokenTransfer{} = token_transfer ->
+ %TokenTransfer{
+ token_transfer
+ | from_address: Map.get(addresses, token_transfer.from_address_hash),
+ to_address: Map.get(addresses, token_transfer.to_address_hash)
+ }
+ end)
+ }
+ end
+
+ defp preload_full_smart_contract(%Address{contract_code: contract_code} = address, options)
+ when not is_nil(contract_code) do
+ select_repo(options).preload(address, :smart_contract, force: true)
+ end
+
+ defp preload_full_smart_contract(address, _options), do: address
+
@doc """
Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for
those `hashes`.
diff --git a/apps/explorer/lib/explorer/chain/address/counters.ex b/apps/explorer/lib/explorer/chain/address/counters.ex
index 9d663193a48..4624da788cf 100644
--- a/apps/explorer/lib/explorer/chain/address/counters.ex
+++ b/apps/explorer/lib/explorer/chain/address/counters.ex
@@ -80,10 +80,43 @@ defmodule Explorer.Chain.Address.Counters do
@spec check_if_withdrawals_at_address(Hash.Address.t()) :: boolean()
def check_if_withdrawals_at_address(address_hash, options \\ []) do
address_hash
- |> Withdrawal.address_hash_to_withdrawals_unordered_query()
+ |> Withdrawal.address_hash_to_withdrawals_existence_query()
|> select_repo(options).exists?()
end
+ @doc """
+ Performs all existence checks needed by the address view in a single
+ database round trip: `SELECT exists(...), exists(...), ...`.
+ """
+ @spec address_existence_checks(Hash.Address.t(), Keyword.t()) :: %{
+ has_validated_blocks: boolean(),
+ has_logs: boolean(),
+ has_tokens: boolean(),
+ has_token_transfers: boolean(),
+ has_beacon_chain_withdrawals: boolean()
+ }
+ def address_existence_checks(address_hash, options \\ []) do
+ validated_blocks_query = address_hash |> address_hash_to_validated_blocks_query() |> select([_], 1)
+ logs_query = address_hash |> address_hash_to_logs_query() |> select([_], 1)
+ token_balances_query = address_hash |> address_hash_to_token_balances_query() |> select([_], 1)
+ token_transfers_from_query = from(tt in TokenTransfer, where: tt.from_address_hash == ^address_hash, select: 1)
+ token_transfers_to_query = from(tt in TokenTransfer, where: tt.to_address_hash == ^address_hash, select: 1)
+ withdrawals_query = Withdrawal.address_hash_to_withdrawals_existence_query(address_hash)
+
+ query =
+ from(f in fragment("SELECT 1"),
+ select: %{
+ has_validated_blocks: exists(validated_blocks_query),
+ has_logs: exists(logs_query),
+ has_tokens: exists(token_balances_query),
+ has_token_transfers: exists(token_transfers_from_query) or exists(token_transfers_to_query),
+ has_beacon_chain_withdrawals: exists(withdrawals_query)
+ }
+ )
+
+ select_repo(options).one(query)
+ end
+
def address_hash_to_transaction_count_query(address_hash) do
dynamic = Transaction.where_transactions_to_from(address_hash)
diff --git a/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex b/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex
index ea8291cbba6..85dad4aead6 100644
--- a/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex
+++ b/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex
@@ -33,6 +33,74 @@ defmodule Explorer.Chain.Address.MetadataPreloader do
@type supported_input :: [supported_types] | supported_types
+ @typedoc """
+ Kind of entity being preloaded, used to pick the `DISABLE_*_BENS_PRELOAD` flag
+ that applies to it. `:any` means that no flag applies.
+ """
+ @type entity_kind :: :any | :blocks | :token_transfers | :transactions
+
+ @typedoc """
+ Field a microservice preload writes to: `:ens_domain_name` is served by BENS,
+ `:metadata` by the Metadata microservice.
+ """
+ @type meta_field :: :ens_domain_name | :metadata
+
+ @all_meta_fields [:ens_domain_name, :metadata]
+
+ # Backstop for the concurrent microservice requests. Both BENS and Metadata
+ # enforce their own receive timeout, so this only fires if a request hangs
+ # outside of it.
+ @concurrent_preload_timeout :timer.seconds(10)
+
+ @doc """
+ Preloads ENS names and metadata to supported entities, querying the BENS and
+ Metadata microservices concurrently.
+
+ Both microservices are asked about the same set of address hashes, so their
+ requests are independent and there is nothing to gain from issuing them one
+ after another: sequentially the entity waits for the sum of both round trips,
+ concurrently only for the slower one.
+
+ `entity_kind` selects the `DISABLE_*_BENS_PRELOAD` flag that applies to the
+ input. The metadata preload is not flag-gated.
+ """
+ @spec maybe_preload_ens_and_metadata(supported_input(), entity_kind()) :: supported_input()
+ def maybe_preload_ens_and_metadata(input, entity_kind \\ :any) do
+ fields =
+ if BENS.ens_preload_disabled?(entity_kind),
+ do: @all_meta_fields -- [:ens_domain_name],
+ else: @all_meta_fields
+
+ maybe_preload_selected_meta(input, fields)
+ end
+
+ @doc """
+ Preloads only the requested `fields` to supported entities, querying the
+ microservices that serve them concurrently.
+
+ Use this where the caller decides per request which preloads to pay for, such
+ as the `preload_ens` and `preload_metadata` parameters of the transaction
+ preview endpoint. Unlike `maybe_preload_ens_and_metadata/2`, no
+ `DISABLE_*_BENS_PRELOAD` flag is consulted: those flags exist to keep the
+ latency out of list endpoints that always preload, whereas here nothing is
+ requested unless the caller asks for it.
+ """
+ @spec maybe_preload_selected_meta(supported_input(), [meta_field()]) :: supported_input()
+ def maybe_preload_selected_meta(input, fields)
+
+ def maybe_preload_selected_meta(input, []), do: input
+
+ def maybe_preload_selected_meta(nil, _fields), do: nil
+
+ def maybe_preload_selected_meta(items, fields) when is_list(items) do
+ preload_selected_meta(items, fields)
+ end
+
+ def maybe_preload_selected_meta(item, fields) do
+ [item_with_meta] = preload_selected_meta([item], fields)
+ item_with_meta
+ end
+
@doc """
Preloads ENS/metadata to supported entities
"""
@@ -77,13 +145,7 @@ defmodule Explorer.Chain.Address.MetadataPreloader do
"""
@spec preload_ens_to_list([supported_types]) :: [supported_types]
def preload_ens_to_list(items) do
- address_hash_strings =
- items
- |> Enum.reduce([], fn item, acc ->
- item_to_address_hash_strings(item) ++ acc
- end)
- |> Enum.filter(&(&1 != ""))
- |> Enum.uniq()
+ address_hash_strings = address_hash_strings(items)
case BENS.ens_names_batch_request(address_hash_strings) do
{:ok, result} ->
@@ -99,11 +161,7 @@ defmodule Explorer.Chain.Address.MetadataPreloader do
"""
@spec preload_metadata_to_list([supported_types]) :: [supported_types]
def preload_metadata_to_list(items) do
- address_hash_strings =
- items
- |> Enum.flat_map(&item_to_address_hash_strings/1)
- |> Enum.filter(&(&1 != ""))
- |> Enum.uniq()
+ address_hash_strings = address_hash_strings(items)
case Metadata.get_addresses_tags(address_hash_strings) do
{:ok, result} ->
@@ -150,6 +208,76 @@ defmodule Explorer.Chain.Address.MetadataPreloader do
end)
end
+ defp preload_selected_meta(items, fields) do
+ case address_hash_strings(items) do
+ [] ->
+ items
+
+ address_hash_strings ->
+ address_hash_strings
+ |> meta_fetchers(fields)
+ |> fetch_meta()
+ |> Enum.reduce(items, fn {field, meta}, acc -> put_meta_to_items(acc, meta, field) end)
+ end
+ end
+
+ defp address_hash_strings(items) do
+ items
+ |> Enum.flat_map(&item_to_address_hash_strings/1)
+ |> Enum.reject(&(&1 == ""))
+ |> Enum.uniq()
+ end
+
+ defp meta_fetchers(address_hash_strings, fields) do
+ [
+ {:ens_domain_name, BENS.enabled?(), fn -> ens_names(address_hash_strings) end},
+ {:metadata, Metadata.enabled?(), fn -> metadata_tags(address_hash_strings) end}
+ ]
+ |> Enum.filter(fn {field, enabled?, _fetcher} -> enabled? and field in fields end)
+ end
+
+ defp fetch_meta([]), do: []
+
+ # A single request needs no task: running it in the caller keeps the logger
+ # metadata and the stacktrace of the calling process.
+ defp fetch_meta([{field, _enabled?, fetcher}]), do: fetched_meta(field, fetcher.())
+
+ defp fetch_meta(fetchers) do
+ Explorer.TaskSupervisor
+ |> Task.Supervisor.async_stream_nolink(
+ fetchers,
+ fn {field, _enabled?, fetcher} -> {field, fetcher.()} end,
+ timeout: @concurrent_preload_timeout,
+ on_timeout: :kill_task,
+ ordered: false
+ )
+ |> Enum.flat_map(fn
+ {:ok, {field, result}} -> fetched_meta(field, result)
+ _other -> []
+ end)
+ end
+
+ defp fetched_meta(field, {:ok, meta}), do: [{field, meta || %{}}]
+ defp fetched_meta(_field, _error), do: []
+
+ defp ens_names(address_hash_strings) do
+ case BENS.ens_names_batch_request(address_hash_strings) do
+ {:ok, result} -> {:ok, result["names"]}
+ _error -> :error
+ end
+ end
+
+ defp metadata_tags(address_hash_strings) do
+ case Metadata.get_addresses_tags(address_hash_strings) do
+ {:ok, result} -> {:ok, result["addresses"]}
+ _error -> :error
+ end
+ end
+
+ defp put_meta_to_items(items, meta, field) do
+ Enum.map(items, &put_meta_to_item(&1, meta, field))
+ end
+
defp item_to_address_hash_strings(nil), do: []
defp item_to_address_hash_strings(%Transaction{
diff --git a/apps/explorer/lib/explorer/chain/cache/contract_methods.ex b/apps/explorer/lib/explorer/chain/cache/contract_methods.ex
new file mode 100644
index 00000000000..7b1132f1790
--- /dev/null
+++ b/apps/explorer/lib/explorer/chain/cache/contract_methods.ex
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: LicenseRef-Blockscout
+defmodule Explorer.Chain.Cache.ContractMethods do
+ @moduledoc """
+ Cache for `Explorer.Chain.ContractMethod` lookups by method id and for ABI
+ candidates returned by the sig-provider microservice.
+
+ Candidates for a 4-byte selector change only when new methods are inserted on
+ contract verification, so found entries are cached with a long TTL and empty
+ results with a short one, keeping transaction input decoding off the DB and
+ HTTP paths for hot method ids.
+ """
+
+ alias Explorer.Chain.ContractMethod
+
+ @cache_name :contract_methods
+ @found_ttl :timer.hours(1)
+ @not_found_ttl :timer.minutes(5)
+
+ @spec cache_name() :: atom()
+ def cache_name, do: @cache_name
+
+ @doc """
+ Same as `Explorer.Chain.ContractMethod.find_contract_methods/2`, but serves
+ repeated lookups from cache. Ids missing from the cache are fetched from the
+ DB in a single query and cached, including ids with no known methods.
+ """
+ @spec find_contract_methods([Explorer.Chain.MethodIdentifier.t()], keyword()) :: [ContractMethod.t()]
+ def find_contract_methods(method_ids, options) do
+ if enabled?() do
+ find_contract_methods_with_cache(method_ids, options)
+ else
+ ContractMethod.find_contract_methods(method_ids, options)
+ end
+ end
+
+ @doc """
+ Fetches the sig-provider ABI candidates for a method id, calling `fallback_fn`
+ and caching its result (including an empty one) on a cache miss.
+ """
+ @spec fetch_sig_provider_abi(binary(), (-> [map()])) :: [map()]
+ def fetch_sig_provider_abi(method_id, fallback_fn) when is_function(fallback_fn, 0) do
+ if enabled?() do
+ {:ok, abi} =
+ ConCache.fetch_or_store(@cache_name, {:sig_provider_abi, method_id}, fn ->
+ {:ok, item(fallback_fn.())}
+ end)
+
+ abi
+ else
+ fallback_fn.()
+ end
+ end
+
+ defp find_contract_methods_with_cache(method_ids, options) do
+ {cached_methods, missing_ids} =
+ Enum.reduce(method_ids, {[], []}, fn method_id, {cached, missing} ->
+ case ConCache.get(@cache_name, {:contract_method, method_id}) do
+ nil -> {cached, [method_id | missing]}
+ methods -> {methods ++ cached, missing}
+ end
+ end)
+
+ fetched_methods = ContractMethod.find_contract_methods(missing_ids, options)
+ fetched_by_id = Enum.group_by(fetched_methods, & &1.identifier)
+
+ Enum.each(missing_ids, fn method_id ->
+ methods = Map.get(fetched_by_id, method_id, [])
+ ConCache.put(@cache_name, {:contract_method, method_id}, item(methods))
+ end)
+
+ cached_methods ++ fetched_methods
+ end
+
+ defp enabled? do
+ Application.get_env(:explorer, __MODULE__)[:enabled]
+ end
+
+ defp item(value) do
+ %ConCache.Item{value: value, ttl: if(value == [], do: @not_found_ttl, else: @found_ttl)}
+ end
+end
diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex
index 10d7f622ab9..590a414530e 100644
--- a/apps/explorer/lib/explorer/chain/smart_contract.ex
+++ b/apps/explorer/lib/explorer/chain/smart_contract.ex
@@ -260,6 +260,21 @@ defmodule Explorer.Chain.SmartContract do
@dead_address_hash_string
end
+ @doc """
+ Returns a preload spec for the `:smart_contract` association that selects all
+ fields except `abi`.
+
+ ABIs of popular contracts reach hundreds of kilobytes, so preloads that only
+ need address info (name, verification flags) should use this spec. Note that
+ `abi` is `nil` (not `%NotLoaded{}`) on structs loaded this way, so it must not
+ be used on preload paths feeding transaction input decoding.
+ """
+ @spec association_without_abi() :: {:smart_contract, Ecto.Query.t()}
+ def association_without_abi do
+ {:smart_contract,
+ from(smart_contract in __MODULE__, select: struct(smart_contract, ^(__schema__(:fields) -- [:abi])))}
+ end
+
@typedoc """
The name of a parameter to a function or event.
"""
diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex
index 9700f14ea6a..56287dcbdc9 100644
--- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex
+++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex
@@ -168,10 +168,11 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
} = smart_contract,
options
) do
- implementation_updated_at = get_proxy_implementation_updated_at(address_hash, options)
+ proxy_implementations = get_proxy_implementations(address_hash, options)
+ implementation_updated_at = proxy_implementations && proxy_implementations.updated_at
{updated_smart_contract, implementation_address_fetched?} =
- if check_implementation_refetch_necessity(implementation_updated_at) do
+ if implementation_refetch_necessary?(proxy_implementations, smart_contract) do
{smart_contract_with_bytecode_twin, implementation_address_fetched?} =
SmartContract.address_hash_to_smart_contract_with_bytecode_twin(address_hash, options)
@@ -202,10 +203,8 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
def get_implementation(
%{
- updated: %SmartContract{
- address_hash: address_hash
- },
- implementation_updated_at: implementation_updated_at,
+ updated: %SmartContract{address_hash: address_hash} = smart_contract,
+ implementation_updated_at: _implementation_updated_at,
implementation_address_fetched?: implementation_address_fetched?,
refetch_necessity_checked?: refetch_necessity_checked?
},
@@ -213,9 +212,12 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
) do
proxy_implementations = get_proxy_implementations(address_hash, options)
- implementation_updated_at = implementation_updated_at || (proxy_implementations && proxy_implementations.updated_at)
-
- if fetch_implementation?(implementation_address_fetched?, refetch_necessity_checked?, implementation_updated_at) do
+ if fetch_implementation?(
+ implementation_address_fetched?,
+ refetch_necessity_checked?,
+ proxy_implementations,
+ smart_contract
+ ) do
get_implementation_address_hash_task =
Task.async(fn ->
# Here and only here we fetch implementations for the given address
@@ -254,11 +256,43 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
def get_implementation(_, _), do: nil
- defp fetch_implementation?(implementation_address_fetched?, refetch_necessity_checked?, implementation_updated_at) do
+ defp fetch_implementation?(
+ implementation_address_fetched?,
+ refetch_necessity_checked?,
+ proxy_implementations,
+ smart_contract
+ ) do
(!implementation_address_fetched? || !refetch_necessity_checked?) &&
- check_implementation_refetch_necessity(implementation_updated_at)
+ implementation_refetch_necessary?(proxy_implementations, smart_contract)
+ end
+
+ # Decides whether the cached implementations row needs to be refetched. Empty
+ # cached results ("not a proxy") for smart-contracts verified on the address
+ # itself are kept for a much longer TTL than positive ones: a verified
+ # non-proxy contract is unlikely to become a proxy, so re-probing all known
+ # proxy patterns via JSON-RPC on every regular cache expiry is wasted work on
+ # the hot path of API requests.
+ @spec implementation_refetch_necessary?(__MODULE__.t() | nil, SmartContract.t() | nil) :: boolean()
+ defp implementation_refetch_necessary?(nil, _smart_contract), do: true
+
+ defp implementation_refetch_necessary?(%__MODULE__{} = proxy_implementations, smart_contract) do
+ if Enum.empty?(proxy_implementations.address_hashes) && verified_on_this_address?(smart_contract) do
+ ttl = Application.get_env(:explorer, :proxy)[:empty_cached_implementation_data_ttl] || get_fresh_time_distance()
+
+ implementation_data_stale?(proxy_implementations.updated_at, ttl)
+ else
+ check_implementation_refetch_necessity(proxy_implementations.updated_at)
+ end
end
+ # Smart-contract verified on the address itself: loaded from the DB and not a
+ # bytecode twin substitute built for an unverified address.
+ defp verified_on_this_address?(%SmartContract{__meta__: %Ecto.Schema.Metadata{state: :loaded}} = smart_contract) do
+ !smart_contract.metadata_from_verified_bytecode_twin
+ end
+
+ defp verified_on_this_address?(_), do: false
+
@doc """
Function checks by timestamp if new implementation fetching needed
"""
@@ -266,13 +300,15 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
def check_implementation_refetch_necessity(nil), do: true
def check_implementation_refetch_necessity(timestamp) do
+ implementation_data_stale?(timestamp, get_fresh_time_distance())
+ end
+
+ defp implementation_data_stale?(timestamp, ttl) do
if Application.get_env(:explorer, :proxy)[:caching_implementation_data_enabled] do
now = DateTime.utc_now()
- fresh_time_distance = get_fresh_time_distance()
-
timestamp
- |> DateTime.add(fresh_time_distance, :millisecond)
+ |> DateTime.add(ttl, :millisecond)
|> DateTime.compare(now) != :gt
else
true
@@ -365,6 +401,10 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
|> Repo.insert(on_conflict: on_conflict(), conflict_target: [:proxy_address_hash], allow_stale: true)
end
+ # The update deliberately runs even when the data is unchanged: a successful
+ # re-probe must bump `updated_at`, otherwise cached results (including empty
+ # "not a proxy" ones) are considered stale forever after the first TTL window
+ # and every subsequent request re-probes the proxy patterns via JSON-RPC.
defp on_conflict do
from(
proxy_implementations in __MODULE__,
@@ -378,16 +418,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", proxy_implementations.inserted_at),
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", proxy_implementations.updated_at)
]
- ],
- where:
- fragment("EXCLUDED.proxy_type <> ?", proxy_implementations.proxy_type) or
- fragment("EXCLUDED.address_hashes <> ?", proxy_implementations.address_hashes) or
- fragment("EXCLUDED.names <> ?", proxy_implementations.names) or
- fragment("EXCLUDED.conflicting_proxy_types IS DISTINCT FROM ?", proxy_implementations.conflicting_proxy_types) or
- fragment(
- "EXCLUDED.conflicting_address_hashes IS DISTINCT FROM ?",
- proxy_implementations.conflicting_address_hashes
- )
+ ]
)
end
diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex
index adfc1933ca6..8ee5a01ffa4 100644
--- a/apps/explorer/lib/explorer/chain/transaction.ex
+++ b/apps/explorer/lib/explorer/chain/transaction.ex
@@ -345,6 +345,7 @@ defmodule Explorer.Chain.Transaction do
alias Explorer.Chain.Block.Reader.General, as: BlockReaderGeneral
+ alias Explorer.Chain.Cache.ContractMethods, as: ContractMethodsCache
alias Explorer.Chain.Cache.Transactions
alias Explorer.Chain.SmartContract.Proxy.Models.Implementation
@@ -1092,13 +1093,15 @@ defmodule Explorer.Chain.Transaction do
end
end
- defp decode_function_call_via_sig_provider(%{bytes: data} = input, hash, skip_sig_provider?) do
+ defp decode_function_call_via_sig_provider(
+ %{bytes: <> = data} = input,
+ hash,
+ skip_sig_provider?
+ ) do
with true <- SigProviderInterface.enabled?(),
false <- skip_sig_provider?,
- {:ok, result} <- SigProviderInterface.decode_function_call(input),
- true <- is_list(result),
- false <- Enum.empty?(result),
- abi <- [result |> List.first() |> Map.put("outputs", []) |> Map.put("type", "function")],
+ [_ | _] = abi <-
+ ContractMethodsCache.fetch_sig_provider_abi(method_id, fn -> request_abi_from_sig_provider(input) end),
{:ok, _, _, _} = candidate <- do_decoded_input_data(data, abi, hash) do
[candidate]
else
@@ -1107,6 +1110,19 @@ defmodule Explorer.Chain.Transaction do
end
end
+ # inputs shorter than a 4-byte method id cannot be a function call
+ defp decode_function_call_via_sig_provider(_input, _hash, _skip_sig_provider?), do: []
+
+ defp request_abi_from_sig_provider(input) do
+ with {:ok, result} <- SigProviderInterface.decode_function_call(input),
+ true <- is_list(result),
+ false <- Enum.empty?(result) do
+ [result |> List.first() |> Map.put("outputs", []) |> Map.put("type", "function")]
+ else
+ _ -> []
+ end
+ end
+
defp check_methods_cache(method_id, methods_map, options) do
Map.get_lazy(methods_map, method_id, fn ->
method_id
@@ -2247,6 +2263,40 @@ defmodule Explorer.Chain.Transaction do
|> Decimal.mult(gas_used)
end
+ @doc """
+ Aggregates the values of the calls batched in an Eden sponsored transaction by their recipients.
+
+ The `to_address_hash` and the `value` of such a transaction are the compatibility fields derived
+ from the first call and from the sum of all the calls respectively, so the calls are the only
+ source of the actual recipients and of the amounts they receive.
+
+ The calls without a recipient (the contract creations) and the malformed ones are skipped.
+
+ ## Parameters
+ - `transaction`: The transaction entity.
+
+ ## Returns
+ - A map of the recipient address hashes to the total value each of them receives within the
+ transaction. Empty for the transactions which are not the sponsored ones.
+ """
+ @spec calls_value_by_recipient(__MODULE__.t()) :: %{Hash.Address.t() => Wei.t()}
+ if @chain_type == :eden do
+ def calls_value_by_recipient(%__MODULE__{calls: calls}) when is_list(calls) do
+ Enum.reduce(calls, %{}, fn call, acc ->
+ with {:ok, address_hash} <- call |> Map.get("to") |> Hash.Address.cast(),
+ {:ok, value} <- call |> Map.get("value") |> Wei.cast() do
+ Map.update(acc, address_hash, value, &Wei.sum(&1, value))
+ else
+ _ -> acc
+ end
+ end)
+ end
+
+ def calls_value_by_recipient(%__MODULE__{}), do: %{}
+ else
+ def calls_value_by_recipient(%__MODULE__{}), do: %{}
+ end
+
@doc """
Calculates burnt fees for a transaction as `base_fee_per_gas * gas_used`.
@@ -2387,7 +2437,7 @@ defmodule Explorer.Chain.Transaction do
_ -> []
end)
|> Enum.uniq()
- |> ContractMethod.find_contract_methods(opts)
+ |> ContractMethodsCache.find_contract_methods(opts)
|> Enum.into(empty_methods_map, &{&1.identifier, [&1]})
# decode remaining transaction using methods map
diff --git a/apps/explorer/lib/explorer/chain/transaction/state_change.ex b/apps/explorer/lib/explorer/chain/transaction/state_change.ex
index e1d878e41a9..8fe2e18bb6d 100644
--- a/apps/explorer/lib/explorer/chain/transaction/state_change.ex
+++ b/apps/explorer/lib/explorer/chain/transaction/state_change.ex
@@ -4,6 +4,8 @@ defmodule Explorer.Chain.Transaction.StateChange do
Helper functions and struct for storing state changes
"""
+ use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
+
use Utils.RuntimeEnvHelper,
miner_gets_burnt_fees?: [:explorer, [Explorer.Chain.Transaction, :block_miner_gets_burnt_fees?]]
@@ -59,7 +61,8 @@ defmodule Explorer.Chain.Transaction.StateChange do
coin_balances =
coin_balances
|> update_balance(transaction.from_address_hash, &Wei.sub(&1, from_loss(transaction)))
- |> update_balance(transaction.to_address_hash, &Wei.sum(&1, to_profit(transaction)))
+ |> update_balance(fee_payer_address_hash(transaction), &Wei.sub(&1, fee_payer_loss(transaction)))
+ |> credit_recipients(transaction)
|> update_balance(block.miner_hash, &Wei.sum(&1, miner_profit(transaction, block)))
if error?(transaction) do
@@ -70,6 +73,30 @@ defmodule Explorer.Chain.Transaction.StateChange do
end
end
+ # Credits the addresses which receive the value of the transaction.
+ #
+ # An Eden sponsored transaction carries an ordered list of the batched calls, each one with its
+ # own recipient and value, so every one of them is credited separately. The `to_address_hash` and
+ # the `value` of such a transaction are just the compatibility fields derived from the first call
+ # and from the sum of all the calls, hence they are not used here.
+ if @chain_type == :eden do
+ defp credit_recipients(coin_balances, %Transaction{calls: [_ | _]} = transaction) do
+ if error?(transaction) do
+ coin_balances
+ else
+ transaction
+ |> Transaction.calls_value_by_recipient()
+ |> Enum.reduce(coin_balances, fn {address_hash, value}, acc ->
+ update_balance(acc, address_hash, &Wei.sum(&1, value))
+ end)
+ end
+ end
+ end
+
+ defp credit_recipients(coin_balances, transaction) do
+ update_balance(coin_balances, transaction.to_address_hash, &Wei.sum(&1, to_profit(transaction)))
+ end
+
defp update_coin_balances_from_internal_transaction(
%InternalTransaction{call_type: call_type, call_type_enum: call_type_enum},
coin_balances
@@ -199,12 +226,12 @@ defmodule Explorer.Chain.Transaction.StateChange do
"""
@spec from_loss(Transaction.t() | InternalTransaction.t()) :: Wei.t()
def from_loss(%Transaction{} = transaction) do
- {_, fee} = Transaction.fee(transaction, :wei)
+ fee = sender_fee(transaction)
if error?(transaction) do
- %Wei{value: fee}
+ fee
else
- Wei.sum(transaction.value, %Wei{value: fee})
+ Wei.sum(transaction.value, fee)
end
end
@@ -212,6 +239,42 @@ defmodule Explorer.Chain.Transaction.StateChange do
transaction.value || Wei.zero()
end
+ @doc """
+ Returns the balance change of the fee payer (sponsor) of a transaction.
+
+ Equals to the transaction fee for the transactions which fee is paid by an address other than
+ the transaction sender (the Eden sponsored transactions), zero otherwise.
+ """
+ @spec fee_payer_loss(Transaction.t()) :: Wei.t()
+ if @chain_type == :eden do
+ def fee_payer_loss(%Transaction{fee_payer_address_hash: nil}), do: Wei.zero()
+
+ def fee_payer_loss(%Transaction{} = transaction), do: transaction_fee(transaction)
+ else
+ def fee_payer_loss(%Transaction{}), do: Wei.zero()
+ end
+
+ if @chain_type == :eden do
+ defp sender_fee(%Transaction{fee_payer_address_hash: nil} = transaction), do: transaction_fee(transaction)
+
+ defp sender_fee(%Transaction{}), do: Wei.zero()
+ else
+ defp sender_fee(%Transaction{} = transaction), do: transaction_fee(transaction)
+ end
+
+ if @chain_type == :eden do
+ defp fee_payer_address_hash(%Transaction{fee_payer_address_hash: fee_payer_address_hash}),
+ do: fee_payer_address_hash
+ else
+ defp fee_payer_address_hash(%Transaction{}), do: nil
+ end
+
+ defp transaction_fee(transaction) do
+ {_, fee} = Transaction.fee(transaction, :wei)
+
+ %Wei{value: fee}
+ end
+
@doc """
Returns the balance change of to address of a transaction
or an internal transaction.
diff --git a/apps/explorer/lib/explorer/chain/withdrawal.ex b/apps/explorer/lib/explorer/chain/withdrawal.ex
index 02af2fcceb7..3ad99d805f9 100644
--- a/apps/explorer/lib/explorer/chain/withdrawal.ex
+++ b/apps/explorer/lib/explorer/chain/withdrawal.ex
@@ -86,6 +86,16 @@ defmodule Explorer.Chain.Withdrawal do
)
end
+ @spec address_hash_to_withdrawals_existence_query(Hash.Address.t()) :: Ecto.Query.t()
+ def address_hash_to_withdrawals_existence_query(address_hash) do
+ from(withdrawal in __MODULE__,
+ left_join: block in assoc(withdrawal, :block),
+ where: withdrawal.address_hash == ^address_hash,
+ where: block.consensus == true,
+ select: 1
+ )
+ end
+
@spec blocks_without_withdrawals_query(non_neg_integer()) :: Ecto.Query.t()
def blocks_without_withdrawals_query(from_block) do
from(withdrawal in __MODULE__,
diff --git a/apps/explorer/lib/explorer/microservice_interfaces/bens.ex b/apps/explorer/lib/explorer/microservice_interfaces/bens.ex
index e63da5a6d2b..90f0c5913af 100644
--- a/apps/explorer/lib/explorer/microservice_interfaces/bens.ex
+++ b/apps/explorer/lib/explorer/microservice_interfaces/bens.ex
@@ -4,18 +4,23 @@ defmodule Explorer.MicroserviceInterfaces.BENS do
Interface to interact with Blockscout ENS microservice
"""
- alias Explorer.{Chain, HttpClient}
+ alias Explorer.Chain
alias Explorer.Chain.Address.MetadataPreloader
alias Explorer.Chain.{Address, Block, Transaction}
+ alias Explorer.MicroserviceInterfaces.HttpClient
alias Explorer.Utility.Microservice
require Logger
import Explorer.Chain.Address.MetadataPreloader, only: [maybe_preload_meta: 3]
- @post_timeout :timer.seconds(5)
+ # The batch resolve is only used to decorate API responses with ENS names, so
+ # it is kept short on purpose: a slow BENS should cost the response its names,
+ # not its latency. A longer timeout also keeps a pooled connection held for
+ # longer, which multiplies into pool pressure under load.
+ @preload_timeout :timer.seconds(1)
@request_error_msg "Error while sending request to BENS microservice"
@doc """
@@ -115,7 +120,7 @@ defmodule Explorer.MicroserviceInterfaces.BENS do
defp http_post_request(url, body) do
headers = [{"Content-Type", "application/json"}]
- case HttpClient.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do
+ case HttpClient.post(url, Jason.encode!(body), headers, recv_timeout: @preload_timeout) do
{:ok, %{body: body, status_code: 200}} ->
Jason.decode(body)
@@ -361,20 +366,33 @@ defmodule Explorer.MicroserviceInterfaces.BENS do
@spec maybe_preload_ens_for_blocks(MetadataPreloader.supported_input()) ::
MetadataPreloader.supported_input()
def maybe_preload_ens_for_blocks(blocks) do
- if Application.get_env(:explorer, __MODULE__, [])[:disable_blocks_bens_preload] do
+ if ens_preload_disabled?(:blocks) do
blocks
else
maybe_preload_ens(blocks)
end
end
+ @doc """
+ Returns `true` if the ENS preload is disabled for the given entity kind via the
+ corresponding `DISABLE_*_BENS_PRELOAD` environment variable.
+ """
+ @spec ens_preload_disabled?(MetadataPreloader.entity_kind()) :: boolean()
+ def ens_preload_disabled?(:blocks), do: !!config()[:disable_blocks_bens_preload]
+
+ def ens_preload_disabled?(:token_transfers), do: !!config()[:disable_token_transfers_bens_preload]
+
+ def ens_preload_disabled?(:transactions), do: !!config()[:disable_transactions_bens_preload]
+
+ def ens_preload_disabled?(:any), do: false
+
@doc """
Preloads ENS data to the list of token transfers unless disabled via DISABLE_TOKEN_TRANSFERS_BENS_PRELOAD
"""
@spec maybe_preload_ens_for_token_transfers(MetadataPreloader.supported_input()) ::
MetadataPreloader.supported_input()
def maybe_preload_ens_for_token_transfers(token_transfers) do
- if Application.get_env(:explorer, __MODULE__, [])[:disable_token_transfers_bens_preload] do
+ if ens_preload_disabled?(:token_transfers) do
token_transfers
else
maybe_preload_ens(token_transfers)
@@ -387,10 +405,12 @@ defmodule Explorer.MicroserviceInterfaces.BENS do
@spec maybe_preload_ens_for_transactions(MetadataPreloader.supported_input()) ::
MetadataPreloader.supported_input()
def maybe_preload_ens_for_transactions(transactions) do
- if Application.get_env(:explorer, __MODULE__, [])[:disable_transactions_bens_preload] do
+ if ens_preload_disabled?(:transactions) do
transactions
else
maybe_preload_ens(transactions)
end
end
+
+ defp config, do: Application.get_env(:explorer, __MODULE__, [])
end
diff --git a/apps/explorer/lib/explorer/microservice_interfaces/http_client.ex b/apps/explorer/lib/explorer/microservice_interfaces/http_client.ex
new file mode 100644
index 00000000000..6822700bd72
--- /dev/null
+++ b/apps/explorer/lib/explorer/microservice_interfaces/http_client.ex
@@ -0,0 +1,140 @@
+# SPDX-License-Identifier: LicenseRef-Blockscout
+defmodule Explorer.MicroserviceInterfaces.HttpClient do
+ @moduledoc """
+ HTTP client for requests to Blockscout microservices.
+
+ Microservice requests sit on the critical path of API responses and are made
+ over and over to the same handful of hosts. The default client configured in
+ `:explorer, :http_client` uses `Tesla.Adapter.Mint`, which opens a connection
+ for every request and closes it once the response is read, so every call pays
+ for DNS resolution and a TCP + TLS handshake. Measured against a microservice
+ answering in ~30ms at the origin, that overhead dominates the call.
+
+ This module routes microservice requests through Finch (mint connections kept
+ alive in a `NimblePool`), so connections are reused across requests. hackney
+ was avoided deliberately: it leaked binaries in production before. The Finch
+ instances are started in `Explorer.Application`.
+
+ Two Finch instances are used, because the workloads have very different
+ shapes:
+
+ * `#{inspect(__MODULE__)}.Finch` for short requests on the critical path of
+ an API response, such as the ENS and metadata preloads. These get a short
+ pool (checkout) timeout: when the pool is saturated it is better to answer
+ without the decorative data than to make the caller queue for a
+ connection.
+
+ * `#{inspect(__MODULE__)}.ProxyFinch` for requests proxied to a microservice
+ on behalf of an API caller, which are allowed to run for much longer (see
+ `MICROSERVICE_METADATA_PROXY_REQUESTS_TIMEOUT`). Sharing a pool with the
+ preloads would let a handful of these hold connections for tens of seconds
+ and starve them.
+ """
+
+ @finch_name __MODULE__.Finch
+ @proxy_finch_name __MODULE__.ProxyFinch
+
+ # Milliseconds a request waits for a free connection. A preload that has to
+ # queue has already lost the latency it was trying to save, so it gives up
+ # quickly and the response is rendered without the extra data.
+ @checkout_timeout 500
+ @proxy_checkout_timeout :timer.seconds(5)
+
+ @doc """
+ Sends a pooled GET request. Accepts the same options as `Explorer.HttpClient.get/3`.
+ """
+ @spec get(binary(), list(), keyword()) :: {:ok, map()} | {:error, any()}
+ def get(url, headers \\ [], options \\ []) do
+ request(:get, url, nil, headers, options, @finch_name, @checkout_timeout)
+ end
+
+ @doc """
+ Sends a pooled POST request. Accepts the same options as `Explorer.HttpClient.post/4`.
+ """
+ @spec post(binary(), iodata(), list(), keyword()) :: {:ok, map()} | {:error, any()}
+ def post(url, body, headers \\ [], options \\ []) do
+ request(:post, url, body, headers, options, @finch_name, @checkout_timeout)
+ end
+
+ @doc """
+ Sends a GET request proxied on behalf of an API caller, through the Finch
+ instance reserved for long-running microservice requests.
+ """
+ @spec proxy_get(binary(), list(), keyword()) :: {:ok, map()} | {:error, any()}
+ def proxy_get(url, headers \\ [], options \\ []) do
+ request(:get, url, nil, headers, options, @proxy_finch_name, @proxy_checkout_timeout)
+ end
+
+ @doc """
+ Returns the child specs of the Finch instances used for microservice requests.
+ """
+ @spec pool_child_specs() :: [Supervisor.child_spec()]
+ def pool_child_specs do
+ total_size = Application.get_env(:explorer, :microservice_http_pool_size)
+ pool_count = Application.get_env(:explorer, :microservice_http_pool_count)
+
+ pools = %{
+ default: [
+ # NimblePool is built for small pools, and per-pool size is the knob
+ # that keeps its process responsive. Two costs scale with it: every
+ # checkout/checkin is a message through the single pool process, and -
+ # much worse - every message an idle socket sends it (e.g. the remote
+ # closing an idle keep-alive connection) makes NimblePool run
+ # handle_info over EVERY idle worker, O(size) per message. A batch of
+ # idle-connection closes against a big pool can occupy its process for
+ # long enough that checkout replies miss the pool timeout even though
+ # almost all connections are free. Keep per-pool size around Finch's
+ # default of 50 by raising MICROSERVICE_HTTP_POOL_COUNT rather than
+ # letting pools grow.
+ size: max(div(total_size, pool_count), 1),
+ count: pool_count,
+ # metrics make the pools observable in a remote console via
+ # Finch.get_pool_status/2, e.g. when debugging checkout timeouts
+ start_pool_metrics?: true
+ ]
+ }
+
+ Enum.map([@finch_name, @proxy_finch_name], &Finch.child_spec(name: &1, pools: pools))
+ end
+
+ defp request(method, url, body, headers, options, finch_name, default_checkout_timeout) do
+ # :finch_instance is a test seam: it lets tests exercise this function
+ # against a small pool they control instead of the app-wide instances
+ adapter_options =
+ [
+ name: options[:finch_instance] || finch_name,
+ pool_timeout: options[:checkout_timeout] || default_checkout_timeout
+ ]
+ |> put_receive_timeout(options[:recv_timeout])
+
+ [method: method, url: url, body: body, headers: headers, query: options[:params] || []]
+ |> then(&Tesla.request(client(adapter_options), &1))
+ |> parse_response()
+ rescue
+ # Finch raises when no connection frees up within the pool timeout (and for
+ # little else). Callers expect the hackney-era contract where a saturated
+ # pool is an {:error, _} to log and degrade on - a response without ENS
+ # names or tags - not an exception that kills the API request.
+ exception in RuntimeError -> {:error, exception}
+ catch
+ # other pool checkout failures (e.g. the pool process going down) exit
+ # instead of raising; degrade the same way
+ :exit, reason -> {:error, {:exit, reason}}
+ end
+
+ defp client(adapter_options) do
+ Tesla.client([], {Tesla.Adapter.Finch, adapter_options})
+ end
+
+ defp put_receive_timeout(adapter_options, nil), do: adapter_options
+
+ defp put_receive_timeout(adapter_options, recv_timeout) do
+ Keyword.put(adapter_options, :receive_timeout, recv_timeout)
+ end
+
+ defp parse_response({:ok, %Tesla.Env{body: body, status: status_code, headers: headers}}) do
+ {:ok, %{body: body, status_code: status_code, headers: headers}}
+ end
+
+ defp parse_response(error), do: error
+end
diff --git a/apps/explorer/lib/explorer/microservice_interfaces/metadata.ex b/apps/explorer/lib/explorer/microservice_interfaces/metadata.ex
index 60576810820..c613c7ea65c 100644
--- a/apps/explorer/lib/explorer/microservice_interfaces/metadata.ex
+++ b/apps/explorer/lib/explorer/microservice_interfaces/metadata.ex
@@ -4,15 +4,22 @@ defmodule Explorer.MicroserviceInterfaces.Metadata do
Module to interact with Metadata microservice
"""
- alias Explorer.{Chain, HttpClient}
+ alias Explorer.Chain
alias Explorer.Chain.{Address.MetadataPreloader, Block, Transaction}
+ alias Explorer.MicroserviceInterfaces.HttpClient
alias Explorer.Utility.Microservice
- import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1]
- import Explorer.Chain.Address.MetadataPreloader, only: [maybe_preload_meta: 3]
+ import Explorer.Chain.Address.MetadataPreloader,
+ only: [maybe_preload_ens_and_metadata: 1, maybe_preload_meta: 3]
+
import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0]
require Logger
+
+ # Preloads only decorate API responses, so a slow Metadata service should cost
+ # the response its tags, not its latency. A longer timeout also keeps a pooled
+ # connection held for longer, which multiplies into pool pressure under load.
+ @preload_timeout :timer.seconds(1)
@request_timeout :timer.seconds(5)
@tags_per_address_limit 5
@@ -47,7 +54,7 @@ defmodule Explorer.MicroserviceInterfaces.Metadata do
chain_id: Application.get_env(:block_scout_web, :chain_id)
}
- http_get_request(addresses_metadata_url(), params)
+ http_get_request(addresses_metadata_url(), params, @preload_timeout)
end
end
@@ -93,17 +100,17 @@ defmodule Explorer.MicroserviceInterfaces.Metadata do
tag_types: "protocol,name"
})
- http_get_request(tags_search_url(), params, &prepare_search_results/1)
+ http_get_request(tags_search_url(), params, @request_timeout, &prepare_search_results/1)
_ ->
:disabled
end
end
- defp http_get_request(url, params, parsing_function \\ &decode_meta/1) do
+ defp http_get_request(url, params, recv_timeout, parsing_function \\ &decode_meta/1) do
headers = []
- case HttpClient.get(url, headers, params: params, recv_timeout: @request_timeout) do
+ case HttpClient.get(url, headers, params: params, recv_timeout: recv_timeout) do
{:ok, %{body: body, status_code: 200}} ->
body |> Jason.decode() |> parsing_function.()
@@ -120,7 +127,7 @@ defmodule Explorer.MicroserviceInterfaces.Metadata do
end
defp http_get_request_for_proxy_method(url, params, parsing_function) do
- case HttpClient.get(url, [], params: params, recv_timeout: config()[:proxy_requests_timeout]) do
+ case HttpClient.proxy_get(url, [], params: params, recv_timeout: config()[:proxy_requests_timeout]) do
{:ok, %{body: body, status_code: 200}} ->
{200, body |> Jason.decode() |> parsing_function.()}
@@ -216,8 +223,7 @@ defmodule Explorer.MicroserviceInterfaces.Metadata do
proxy_implementations_association() => :optional
}
)
- |> maybe_preload_ens()
- |> maybe_preload_metadata()
+ |> maybe_preload_ens_and_metadata()
)}
end
diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex
index 51d26ead08a..0d97ce22b91 100644
--- a/apps/explorer/lib/explorer/token/metadata_retriever.ex
+++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex
@@ -15,7 +15,7 @@ defmodule Explorer.Token.MetadataRetriever do
@vm_execution_error "VM execution error"
@invalid_base64_data "invalid data:application/json;base64"
@invalid_ipfs_path "invalid ipfs path"
- @default_headers [{"User-Agent", "blockscout-11.2.6"}]
+ @default_headers [{"User-Agent", "blockscout-11.2.7"}]
# https://eips.ethereum.org/EIPS/eip-1155#metadata
@erc1155_token_id_placeholder "{id}"
diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs
index 3382654152a..70b87a8fddd 100644
--- a/apps/explorer/mix.exs
+++ b/apps/explorer/mix.exs
@@ -21,7 +21,7 @@ defmodule Explorer.Mixfile do
lockfile: "../../mix.lock",
package: package(),
start_permanent: Mix.env() == :prod,
- version: "11.2.6",
+ version: "11.2.7",
xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]]
]
end
@@ -116,7 +116,7 @@ defmodule Explorer.Mixfile do
# `Timex.Duration` for `Explorer.Chain.Cache.Counters.AverageBlockTime.average_block_time/0`
{:timex, "~> 3.7.1"},
{:con_cache, "~> 1.0"},
- {:tesla, "~> 1.20.0"},
+ {:tesla, "~> 1.21.0"},
{:cbor, "~> 1.0"},
{:cloak_ecto, "~> 1.3.0"},
{:redix, "~> 1.1"},
@@ -137,6 +137,8 @@ defmodule Explorer.Mixfile do
{:hammer, "~> 7.0"},
{:ton, "~> 0.5.0"},
{:mint, "~> 1.0"},
+ # pooled HTTP client on top of mint, used for microservice requests
+ {:finch, "~> 0.18"},
{:oban, "~> 2.19"}
]
end
diff --git a/apps/explorer/test/explorer/chain/address/metadata_preloader_test.exs b/apps/explorer/test/explorer/chain/address/metadata_preloader_test.exs
new file mode 100644
index 00000000000..5bbdf43b602
--- /dev/null
+++ b/apps/explorer/test/explorer/chain/address/metadata_preloader_test.exs
@@ -0,0 +1,185 @@
+# SPDX-License-Identifier: LicenseRef-Blockscout
+defmodule Explorer.Chain.Address.MetadataPreloaderTest do
+ use ExUnit.Case, async: false
+
+ alias Explorer.Chain
+ alias Explorer.Chain.Address
+ alias Explorer.Chain.Address.MetadataPreloader
+ alias Explorer.MicroserviceInterfaces.{BENS, Metadata}
+ alias Plug.Conn
+
+ @address_hash_string "0x000000000000000000000000000000000000000a"
+ @ens_name "test.eth"
+ @metadata_tag %{"name" => "Test", "tagType" => "name", "meta" => %{}}
+
+ setup do
+ bypass = Bypass.open()
+ old_bens_env = Application.get_env(:explorer, BENS, [])
+ old_metadata_env = Application.get_env(:explorer, Metadata, [])
+ old_chain_id = Application.get_env(:block_scout_web, :chain_id)
+
+ Application.put_env(:block_scout_web, :chain_id, 1)
+
+ Application.put_env(
+ :explorer,
+ BENS,
+ Keyword.merge(old_bens_env || [],
+ service_url: "http://localhost:#{bypass.port}",
+ enabled: true,
+ # legacy, chain-id-based URLs, so the expected paths are deterministic
+ protocols: []
+ )
+ )
+
+ Application.put_env(
+ :explorer,
+ Metadata,
+ Keyword.merge(old_metadata_env || [], service_url: "http://localhost:#{bypass.port}", enabled: true)
+ )
+
+ on_exit(fn ->
+ Bypass.down(bypass)
+ Application.put_env(:explorer, BENS, old_bens_env)
+ Application.put_env(:explorer, Metadata, old_metadata_env)
+ Application.put_env(:block_scout_web, :chain_id, old_chain_id)
+ end)
+
+ {:ok, bypass: bypass}
+ end
+
+ describe "maybe_preload_ens_and_metadata/2" do
+ test "preloads ENS names and metadata from both microservices", %{bypass: bypass} do
+ expect_both_microservices(bypass)
+
+ [address] = MetadataPreloader.maybe_preload_ens_and_metadata([address()])
+
+ assert address.ens_domain_name == @ens_name
+ assert address.metadata == %{"tags" => [@metadata_tag]}
+ end
+
+ test "preloads metadata when the ENS preload is disabled for the entity kind", %{bypass: bypass} do
+ Application.put_env(
+ :explorer,
+ BENS,
+ Keyword.put(Application.get_env(:explorer, BENS), :disable_transactions_bens_preload, true)
+ )
+
+ expect_both_microservices(bypass)
+
+ [address] = MetadataPreloader.maybe_preload_ens_and_metadata([address()], :transactions)
+
+ assert address.ens_domain_name == nil
+ assert address.metadata == %{"tags" => [@metadata_tag]}
+ end
+
+ test "preloads ENS names when the metadata microservice fails", %{bypass: bypass} do
+ Bypass.expect(bypass, fn conn ->
+ case conn.request_path do
+ "/api/v1/1/addresses:batch_resolve_names" ->
+ Conn.resp(conn, 200, Jason.encode!(%{"names" => %{checksummed_address_hash() => @ens_name}}))
+
+ _metadata_path ->
+ Conn.resp(conn, 500, "")
+ end
+ end)
+
+ [address] = MetadataPreloader.maybe_preload_ens_and_metadata([address()])
+
+ assert address.ens_domain_name == @ens_name
+ assert address.metadata == nil
+ end
+
+ test "returns a single entity as-is, not wrapped in a list", %{bypass: bypass} do
+ expect_both_microservices(bypass)
+
+ address = MetadataPreloader.maybe_preload_ens_and_metadata(address())
+
+ assert %Address{ens_domain_name: @ens_name} = address
+ end
+
+ test "skips both microservices when there are no address hashes" do
+ # any request to the bypass would fail the test, since nothing is expected
+ assert MetadataPreloader.maybe_preload_ens_and_metadata([]) == []
+ end
+ end
+
+ describe "maybe_preload_selected_meta/2" do
+ test "queries both microservices concurrently when both fields are requested", %{bypass: bypass} do
+ expect_both_microservices(bypass)
+
+ address = MetadataPreloader.maybe_preload_selected_meta(address(), [:ens_domain_name, :metadata])
+
+ assert address.ens_domain_name == @ens_name
+ assert address.metadata == %{"tags" => [@metadata_tag]}
+ end
+
+ test "queries only BENS when just the ENS field is requested", %{bypass: bypass} do
+ expect_only(bypass, "/api/v1/1/addresses:batch_resolve_names")
+
+ address = MetadataPreloader.maybe_preload_selected_meta(address(), [:ens_domain_name])
+
+ assert address.ens_domain_name == @ens_name
+ assert address.metadata == nil
+ end
+
+ test "queries only Metadata when just the metadata field is requested", %{bypass: bypass} do
+ expect_only(bypass, "/api/v1/metadata")
+
+ address = MetadataPreloader.maybe_preload_selected_meta(address(), [:metadata])
+
+ assert address.ens_domain_name == nil
+ assert address.metadata == %{"tags" => [@metadata_tag]}
+ end
+
+ test "queries nothing when no field is requested" do
+ # any request to the bypass would fail the test, since nothing is expected
+ assert MetadataPreloader.maybe_preload_selected_meta(address(), []) == address()
+ end
+
+ test "ignores DISABLE_TRANSACTIONS_BENS_PRELOAD, since the caller opted in explicitly", %{bypass: bypass} do
+ Application.put_env(
+ :explorer,
+ BENS,
+ Keyword.put(Application.get_env(:explorer, BENS), :disable_transactions_bens_preload, true)
+ )
+
+ expect_only(bypass, "/api/v1/1/addresses:batch_resolve_names")
+
+ address = MetadataPreloader.maybe_preload_selected_meta(address(), [:ens_domain_name])
+
+ assert address.ens_domain_name == @ens_name
+ end
+ end
+
+ defp address do
+ {:ok, hash} = Chain.string_to_address_hash(@address_hash_string)
+ %Address{hash: hash}
+ end
+
+ defp checksummed_address_hash, do: Address.checksum(@address_hash_string)
+
+ # fails the test if any path other than `expected_path` is requested
+ defp expect_only(bypass, expected_path) do
+ Bypass.expect(bypass, fn %{request_path: ^expected_path} = conn ->
+ Conn.resp(conn, 200, response_body(expected_path))
+ end)
+ end
+
+ defp response_body("/api/v1/1/addresses:batch_resolve_names") do
+ Jason.encode!(%{"names" => %{checksummed_address_hash() => @ens_name}})
+ end
+
+ defp response_body("/api/v1/metadata") do
+ Jason.encode!(%{
+ "addresses" => %{
+ checksummed_address_hash() => %{"tags" => [Map.put(@metadata_tag, "meta", Jason.encode!(%{}))]}
+ }
+ })
+ end
+
+ defp expect_both_microservices(bypass) do
+ Bypass.expect(bypass, fn conn ->
+ Conn.resp(conn, 200, response_body(conn.request_path))
+ end)
+ end
+end
diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs
index 804d8670aee..66a26f04ea5 100644
--- a/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs
+++ b/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs
@@ -13,7 +13,47 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do
setup :set_mox_global
describe "fetching implementation" do
+ test "get_implementation/1 does not re-probe verified non-proxy within empty cache TTL" do
+ initial_proxy_config = Application.get_env(:explorer, :proxy)
+ on_exit(fn -> Application.put_env(:explorer, :proxy, initial_proxy_config) end)
+
+ smart_contract = insert(:smart_contract)
+
+ proxy =
+ :explorer
+ |> Application.get_env(:proxy)
+ |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20))
+ |> Keyword.replace(:implementation_data_fetching_timeout, :timer.seconds(20))
+ |> Keyword.put(:empty_cached_implementation_data_ttl, :timer.hours(24))
+
+ Application.put_env(:explorer, :proxy, proxy)
+
+ EthereumJSONRPC.Mox
+ |> TestHelper.mock_generic_proxy_requests()
+
+ assert %Implementation{address_hashes: [], names: [], proxy_type: nil} =
+ Implementation.get_implementation(smart_contract)
+
+ verify!(EthereumJSONRPC.Mox)
+
+ # expire the regular TTL: the cached empty result of a verified non-proxy
+ # must still be served without re-probing via JSON RPC (no Mox
+ # expectations are set, so any request would raise)
+ proxy =
+ :explorer
+ |> Application.get_env(:proxy)
+ |> Keyword.replace(:fallback_cached_implementation_data_ttl, 0)
+
+ Application.put_env(:explorer, :proxy, proxy)
+
+ assert %Implementation{address_hashes: [], names: [], proxy_type: nil} =
+ Implementation.get_implementation(smart_contract)
+ end
+
test "get_implementation/1" do
+ initial_proxy_config = Application.get_env(:explorer, :proxy)
+ on_exit(fn -> Application.put_env(:explorer, :proxy, initial_proxy_config) end)
+
smart_contract = insert(:smart_contract)
implementation_smart_contract = insert(:smart_contract, name: "implementation")
@@ -42,6 +82,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do
:explorer
|> Application.get_env(:proxy)
|> Keyword.replace(:fallback_cached_implementation_data_ttl, 0)
+ |> Keyword.put(:empty_cached_implementation_data_ttl, 0)
Application.put_env(:explorer, :proxy, proxy)
diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs
index bad2b162233..dfa62c3b68b 100644
--- a/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs
+++ b/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs
@@ -350,6 +350,9 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do
end
test "check proxy_contract?/1 function" do
+ initial_proxy_config = Application.get_env(:explorer, :proxy)
+ on_exit(fn -> Application.put_env(:explorer, :proxy, initial_proxy_config) end)
+
smart_contract = insert(:smart_contract)
proxy =
@@ -374,6 +377,7 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do
:explorer
|> Application.get_env(:proxy)
|> Keyword.replace(:fallback_cached_implementation_data_ttl, 0)
+ |> Keyword.put(:empty_cached_implementation_data_ttl, 0)
Application.put_env(:explorer, :proxy, proxy)
diff --git a/apps/explorer/test/explorer/chain/transaction_test.exs b/apps/explorer/test/explorer/chain/transaction_test.exs
index 469dd8daf34..22e4faacae5 100644
--- a/apps/explorer/test/explorer/chain/transaction_test.exs
+++ b/apps/explorer/test/explorer/chain/transaction_test.exs
@@ -923,6 +923,38 @@ defmodule Explorer.Chain.TransactionTest do
end
end
+ if Application.compile_env(:explorer, :chain_type) == :eden do
+ describe "calls_value_by_recipient/1" do
+ test "sums up the values of the batched calls per recipient" do
+ first_recipient = "0x11f60a633dd30a8d1a26dd6e20167a9293fb4647"
+ second_recipient = "0xcfc096e58b1f858e5a3ee88ecaeccb2b464625b5"
+
+ transaction = %Transaction{
+ calls: [
+ %{"to" => first_recipient, "value" => 1, "input" => "0x"},
+ %{"to" => second_recipient, "value" => 2, "input" => "0x"},
+ %{"to" => first_recipient, "value" => 3, "input" => "0x"}
+ ]
+ }
+
+ assert %{} = values = Transaction.calls_value_by_recipient(transaction)
+
+ assert Map.new(values, fn {address_hash, value} -> {to_string(address_hash), to_string(value.value)} end) ==
+ %{first_recipient => "4", second_recipient => "2"}
+ end
+
+ test "skips the calls without a recipient" do
+ transaction = %Transaction{calls: [%{"to" => nil, "value" => 1, "input" => "0x"}]}
+
+ assert Transaction.calls_value_by_recipient(transaction) == %{}
+ end
+
+ test "returns an empty map for the transactions which are not sponsored ones" do
+ assert Transaction.calls_value_by_recipient(%Transaction{calls: nil}) == %{}
+ end
+ end
+ end
+
describe "get_method_name/1" do
test "returns method name for transaction with input data starting with 0x" do
transaction =
diff --git a/apps/explorer/test/explorer/microservice_interfaces/http_client_test.exs b/apps/explorer/test/explorer/microservice_interfaces/http_client_test.exs
new file mode 100644
index 00000000000..4a58e27a532
--- /dev/null
+++ b/apps/explorer/test/explorer/microservice_interfaces/http_client_test.exs
@@ -0,0 +1,115 @@
+# SPDX-License-Identifier: LicenseRef-Blockscout
+defmodule Explorer.MicroserviceInterfaces.HttpClientTest do
+ use ExUnit.Case, async: false
+
+ alias Explorer.MicroserviceInterfaces.HttpClient
+ alias Plug.Conn
+
+ @reuse_event [:finch, :reused_connection]
+
+ setup do
+ bypass = Bypass.open()
+ test_process = self()
+
+ # a stub rather than an expectation: not every test here makes a request,
+ # and the ones that do assert on :request_received themselves
+ Bypass.stub(bypass, "GET", "/api/v1/metadata", fn conn ->
+ conn = Conn.fetch_query_params(conn)
+
+ if conn.query_params["sleep"] do
+ send(test_process, :slow_request_started)
+ Process.sleep(300)
+ else
+ send(test_process, :request_received)
+ end
+
+ Conn.resp(conn, 200, "{}")
+ end)
+
+ handler_id = {__MODULE__, self()}
+
+ :telemetry.attach(
+ handler_id,
+ @reuse_event,
+ fn @reuse_event, _measurements, metadata, _config -> send(test_process, {:connection_reused, metadata.name}) end,
+ nil
+ )
+
+ on_exit(fn ->
+ :telemetry.detach(handler_id)
+ Bypass.down(bypass)
+ end)
+
+ {:ok, url: "http://localhost:#{bypass.port}/api/v1/metadata"}
+ end
+
+ describe "get/3" do
+ test "reuses pooled connections instead of opening one per request", %{url: url} do
+ # one more sequential request than there are pools, so at least one pool
+ # must serve a second request over the connection its first one opened -
+ # which is what makes reuse skip DNS and the TCP + TLS handshake
+ for _ <- 1..(pool_count() + 1) do
+ assert {:ok, %{status_code: 200}} = HttpClient.get(url)
+ assert_received :request_received
+ end
+
+ assert_received {:connection_reused, HttpClient.Finch}
+ end
+ end
+
+ describe "get/3 on a saturated pool" do
+ test "returns an error tuple instead of raising, so preloads can degrade", %{url: url} do
+ # a Finch instance with exactly one connection, so a single in-flight
+ # request saturates it
+ start_supervised!({Finch, name: __MODULE__.TinyFinch, pools: %{default: [size: 1, count: 1]}})
+
+ slow_response =
+ Task.async(fn -> HttpClient.get(url <> "?sleep=true", [], finch_instance: __MODULE__.TinyFinch) end)
+
+ # wait until the slow request holds the pool's only connection
+ assert_receive :slow_request_started, 1_000
+
+ assert {:error, %RuntimeError{message: message}} =
+ HttpClient.get(url, [], finch_instance: __MODULE__.TinyFinch, checkout_timeout: 50)
+
+ assert message =~ "excess queuing"
+
+ assert {:ok, %{status_code: 200}} = Task.await(slow_response)
+ end
+ end
+
+ describe "proxy_get/3" do
+ test "uses the Finch instance reserved for long-running requests", %{url: url} do
+ for _ <- 1..(pool_count() + 1) do
+ assert {:ok, %{status_code: 200}} = HttpClient.proxy_get(url)
+ end
+
+ assert_received {:connection_reused, HttpClient.ProxyFinch}
+
+ # nothing went through the instance that serves the latency-critical
+ # preloads
+ refute_received {:connection_reused, HttpClient.Finch}
+ end
+ end
+
+ describe "pool_child_specs/0" do
+ test "starts both Finch instances, with MICROSERVICE_HTTP_POOL_SIZE connections split across MICROSERVICE_HTTP_POOL_COUNT pools",
+ %{url: url} do
+ total_size = Application.get_env(:explorer, :microservice_http_pool_size)
+ pool_count = pool_count()
+ per_pool_size = div(total_size, pool_count)
+
+ # pools are started lazily per origin, so make one request through each
+ assert {:ok, %{status_code: 200}} = HttpClient.get(url)
+ assert {:ok, %{status_code: 200}} = HttpClient.proxy_get(url)
+
+ for finch_name <- [HttpClient.Finch, HttpClient.ProxyFinch] do
+ assert {:ok, pool_metrics} = Finch.get_pool_status(finch_name, url)
+ assert length(pool_metrics) == pool_count
+ assert Enum.all?(pool_metrics, &(&1.pool_size == per_pool_size))
+ end
+ end
+ end
+
+ defp pool_count, do: Application.get_env(:explorer, :microservice_http_pool_count)
+end
diff --git a/apps/indexer/lib/indexer/transform/address_coin_balances.ex b/apps/indexer/lib/indexer/transform/address_coin_balances.ex
index 50175ec99dd..2d33b2a7218 100644
--- a/apps/indexer/lib/indexer/transform/address_coin_balances.ex
+++ b/apps/indexer/lib/indexer/transform/address_coin_balances.ex
@@ -4,7 +4,9 @@ defmodule Indexer.Transform.AddressCoinBalances do
Extracts `Explorer.Chain.Address.CoinBalance` params from other schema's params.
"""
- use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity]
+ use Utils.CompileTimeEnvHelper,
+ chain_identity: [:explorer, :chain_identity],
+ chain_type: [:explorer, :chain_type]
use Utils.RuntimeEnvHelper,
chain_type: [:explorer, :chain_type],
@@ -241,5 +243,41 @@ defmodule Indexer.Transform.AddressCoinBalances do
end
end
+ if @chain_type == :eden do
+ defp transactions_params_chain_type_fields_reducer(
+ %{block_number: block_number} = transaction_params,
+ initial
+ )
+ when is_integer(block_number) do
+ initial
+ |> put_fee_payer(transaction_params, block_number)
+ |> put_calls_recipients(transaction_params, block_number)
+ end
+ end
+
defp transactions_params_chain_type_fields_reducer(_, acc), do: acc
+
+ if @chain_type == :eden do
+ alias Explorer.Chain
+
+ defp put_fee_payer(acc, %{fee_payer_address_hash: fee_payer_address_hash}, block_number)
+ when is_binary(fee_payer_address_hash) do
+ MapSet.put(acc, %{address_hash: fee_payer_address_hash, block_number: block_number})
+ end
+
+ defp put_fee_payer(acc, _transaction_params, _block_number), do: acc
+
+ defp put_calls_recipients(acc, %{calls: calls}, block_number) when is_list(calls) do
+ Enum.reduce(calls, acc, fn call, inner_acc ->
+ address_hash = Map.get(call, "to")
+
+ case Chain.string_to_address_hash(address_hash) do
+ {:ok, _} -> MapSet.put(inner_acc, %{address_hash: address_hash, block_number: block_number})
+ :error -> inner_acc
+ end
+ end)
+ end
+
+ defp put_calls_recipients(acc, _transaction_params, _block_number), do: acc
+ end
end
diff --git a/apps/indexer/lib/indexer/transform/addresses.ex b/apps/indexer/lib/indexer/transform/addresses.ex
index 48ddf8b8dc4..bd9b7192ade 100644
--- a/apps/indexer/lib/indexer/transform/addresses.ex
+++ b/apps/indexer/lib/indexer/transform/addresses.ex
@@ -232,6 +232,128 @@ defmodule Indexer.Transform.Addresses do
defstruct pending: false
+ if @chain_type == :zksync do
+ @created_contract_doc """
+ A contract created in an internal transaction has its `created_contract_address_hash` extracted.
+ The `created_contract_code` is ignored as it isn't the deployed bytecode on zksync.
+
+ iex> Indexer.Transform.Addresses.extract_addresses(
+ ...> %{
+ ...> internal_transactions: [
+ ...> %{
+ ...> block_number: 3,
+ ...> created_contract_address_hash: "0x0000000000000000000000000000000000000003",
+ ...> created_contract_code: "0x"
+ ...> }
+ ...> ]
+ ...> }
+ ...> )
+ [
+ %{
+ fetched_coin_balance_block_number: 3,
+ hash: "0x0000000000000000000000000000000000000003"
+ }
+ ]
+ """
+
+ @merged_contract_code_doc """
+ When a contract's code is fetched and the contract is used in transactions in the same fetched data, the
+ `contract_code` is merged with the greatest `block_number`
+
+ iex> Indexer.Transform.Addresses.extract_addresses(
+ ...> %{
+ ...> codes: [
+ ...> %{
+ ...> address: "0x0000000000000000000000000000000000000001",
+ ...> code: "0x"
+ ...> }
+ ...> ],
+ ...> transactions: [
+ ...> %{
+ ...> block_number: 2,
+ ...> from_address_hash: "0x0000000000000000000000000000000000000001",
+ ...> nonce: 4
+ ...> },
+ ...> %{
+ ...> block_number: 3,
+ ...> to_address_hash: "0x0000000000000000000000000000000000000001",
+ ...> nonce: 5
+ ...> }
+ ...> ]
+ ...> }
+ ...> )
+ [
+ %{
+ contract_code: "0x",
+ fetched_coin_balance_block_number: 3,
+ hash: "0x0000000000000000000000000000000000000001",
+ nonce: 4
+ }
+ ]
+ """
+ else
+ @created_contract_doc """
+ A contract created in an internal transaction has its `created_contract_address_hash` and
+ `created_contract_code` extracted.
+
+ iex> Indexer.Transform.Addresses.extract_addresses(
+ ...> %{
+ ...> internal_transactions: [
+ ...> %{
+ ...> block_number: 3,
+ ...> created_contract_address_hash: "0x0000000000000000000000000000000000000003",
+ ...> created_contract_code: "0x"
+ ...> }
+ ...> ]
+ ...> }
+ ...> )
+ [
+ %{
+ contract_code: "0x",
+ fetched_coin_balance_block_number: 3,
+ hash: "0x0000000000000000000000000000000000000003"
+ }
+ ]
+ """
+
+ @merged_contract_code_doc """
+ When a contract is created and then used in internal transactions and transaction in the same fetched data, the
+ `created_contract_code` is merged with the greatest `block_number`
+
+ iex> Indexer.Transform.Addresses.extract_addresses(
+ ...> %{
+ ...> internal_transactions: [
+ ...> %{
+ ...> block_number: 1,
+ ...> created_contract_code: "0x",
+ ...> created_contract_address_hash: "0x0000000000000000000000000000000000000001"
+ ...> }
+ ...> ],
+ ...> transactions: [
+ ...> %{
+ ...> block_number: 2,
+ ...> from_address_hash: "0x0000000000000000000000000000000000000001",
+ ...> nonce: 4
+ ...> },
+ ...> %{
+ ...> block_number: 3,
+ ...> to_address_hash: "0x0000000000000000000000000000000000000001",
+ ...> nonce: 5
+ ...> }
+ ...> ]
+ ...> }
+ ...> )
+ [
+ %{
+ contract_code: "0x",
+ fetched_coin_balance_block_number: 3,
+ hash: "0x0000000000000000000000000000000000000001",
+ nonce: 4
+ }
+ ]
+ """
+ end
+
@doc """
Extract addresses from block, internal transaction, transaction, and log parameters.
@@ -254,8 +376,7 @@ defmodule Indexer.Transform.Addresses do
}
]
- Internal transactions can have their `from_address_hash`, `to_address_hash` and/or `created_contract_address_hash`
- extracted.
+ Internal transactions can have their `from_address_hash` and/or `to_address_hash` extracted.
iex> Indexer.Transform.Addresses.extract_addresses(
...> %{
@@ -267,11 +388,6 @@ defmodule Indexer.Transform.Addresses do
...> %{
...> block_number: 2,
...> to_address_hash: "0x0000000000000000000000000000000000000002"
- ...> },
- ...> %{
- ...> block_number: 3,
- ...> created_contract_address_hash: "0x0000000000000000000000000000000000000003",
- ...> created_contract_code: "0x"
...> }
...> ]
...> }
@@ -284,14 +400,10 @@ defmodule Indexer.Transform.Addresses do
%{
fetched_coin_balance_block_number: 2,
hash: "0x0000000000000000000000000000000000000002"
- },
- %{
- contract_code: "0x",
- fetched_coin_balance_block_number: 3,
- hash: "0x0000000000000000000000000000000000000003"
}
]
+ #{@created_contract_doc}
Transactions can have their `from_address_hash` and/or `to_address_hash` extracted.
iex> Indexer.Transform.Addresses.extract_addresses(
@@ -399,41 +511,7 @@ defmodule Indexer.Transform.Addresses do
}
]
- When a contract is created and then used in internal transactions and transaction in the same fetched data, the
- `created_contract_code` is merged with the greatest `block_number`
-
- iex> Indexer.Transform.Addresses.extract_addresses(
- ...> %{
- ...> internal_transactions: [
- ...> %{
- ...> block_number: 1,
- ...> created_contract_code: "0x",
- ...> created_contract_address_hash: "0x0000000000000000000000000000000000000001"
- ...> }
- ...> ],
- ...> transactions: [
- ...> %{
- ...> block_number: 2,
- ...> from_address_hash: "0x0000000000000000000000000000000000000001",
- ...> nonce: 4
- ...> },
- ...> %{
- ...> block_number: 3,
- ...> to_address_hash: "0x0000000000000000000000000000000000000001",
- ...> nonce: 5
- ...> }
- ...> ]
- ...> }
- ...> )
- [
- %{
- contract_code: "0x",
- fetched_coin_balance_block_number: 3,
- hash: "0x0000000000000000000000000000000000000001",
- nonce: 4
- }
- ]
-
+ #{@merged_contract_code_doc}
All data must have some way of extracting the `fetched_coin_balance_block_number` or an `ArgumentError` will be raised when
none of the supported extract formats matches the params.
@@ -569,6 +647,7 @@ defmodule Indexer.Transform.Addresses do
addresses
|> List.flatten()
+ |> Enum.concat(chain_type_addresses(fetched_data, state))
|> merge_addresses()
end
@@ -577,6 +656,46 @@ defmodule Indexer.Transform.Addresses do
def extract_addresses_from_item(item, fields, state), do: Enum.flat_map(fields, &extract_fields(&1, item, state))
+ if @chain_type == :eden do
+ alias Explorer.Chain
+
+ @eden_call_fields [
+ [
+ %{from: :block_number, to: :fetched_coin_balance_block_number},
+ %{from: :to_address_hash, to: :hash}
+ ]
+ ]
+
+ # The recipients of the calls batched in an Eden sponsored transaction are stored in the `calls`
+ # JSON field, so they can't be declared in `@entity_to_address_map`, which supports the plain
+ # fields only. The calls are flattened into the items of the shape the declarations expect
+ # instead, so that the pending transactions keep being handled the same way.
+ defp chain_type_addresses(fetched_data, state) do
+ fetched_data
+ |> Map.get(:transactions)
+ |> Kernel.||([])
+ |> Enum.flat_map(&transaction_to_call_items/1)
+ |> extract_addresses_from_collection(@eden_call_fields, state)
+ end
+
+ defp transaction_to_call_items(%{calls: calls} = transaction) when is_list(calls) do
+ block_number = Map.take(transaction, [:block_number])
+
+ Enum.flat_map(calls, fn call ->
+ to_address_hash = Map.get(call, "to")
+
+ case Chain.string_to_address_hash(to_address_hash) do
+ {:ok, _} -> [Map.put(block_number, :to_address_hash, to_address_hash)]
+ :error -> []
+ end
+ end)
+ end
+
+ defp transaction_to_call_items(_transaction), do: []
+ else
+ defp chain_type_addresses(_fetched_data, _state), do: []
+ end
+
def merge_addresses(addresses) when is_list(addresses) do
addresses
|> Enum.group_by(fn address -> address.hash end)
diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs
index 94ce337f3bd..cfc7f0a0659 100644
--- a/apps/indexer/mix.exs
+++ b/apps/indexer/mix.exs
@@ -15,7 +15,7 @@ defmodule Indexer.MixProject do
elixirc_paths: elixirc_paths(Mix.env()),
lockfile: "../../mix.lock",
start_permanent: Mix.env() == :prod,
- version: "11.2.6",
+ version: "11.2.7",
xref: [
exclude: [
Explorer.Chain.Optimism.Deposit,
diff --git a/apps/indexer/test/indexer/transform/address_coin_balances_test.exs b/apps/indexer/test/indexer/transform/address_coin_balances_test.exs
index 836789bc565..372d41295c7 100644
--- a/apps/indexer/test/indexer/transform/address_coin_balances_test.exs
+++ b/apps/indexer/test/indexer/transform/address_coin_balances_test.exs
@@ -247,6 +247,80 @@ defmodule Indexer.Transform.AddressCoinBalancesTest do
end
end
+ if Application.compile_env(:explorer, :chain_type) == :eden do
+ describe "params_set/1 transactions_params on Eden chain" do
+ test "with sponsored transaction extracts fee_payer_address_hash" do
+ block_number = 1
+
+ from_address_hash = to_string(Factory.address_hash())
+ fee_payer_address_hash = to_string(Factory.address_hash())
+
+ transaction_params =
+ :transaction
+ |> Factory.params_for()
+ |> Map.put(:block_number, block_number)
+ |> Map.put(:from_address_hash, from_address_hash)
+ |> Map.put(:fee_payer_address_hash, fee_payer_address_hash)
+
+ params_set = AddressCoinBalances.params_set(%{transactions_params: [transaction_params]})
+
+ assert MapSet.size(params_set) == 2
+ assert MapSet.member?(params_set, %{address_hash: from_address_hash, block_number: block_number})
+ assert MapSet.member?(params_set, %{address_hash: fee_payer_address_hash, block_number: block_number})
+ end
+
+ test "with sponsored transaction extracts the recipients of the batched calls" do
+ block_number = 1
+
+ from_address_hash = to_string(Factory.address_hash())
+ fee_payer_address_hash = to_string(Factory.address_hash())
+ first_call_address_hash = to_string(Factory.address_hash())
+ second_call_address_hash = to_string(Factory.address_hash())
+
+ transaction_params =
+ :transaction
+ |> Factory.params_for()
+ |> Map.put(:block_number, block_number)
+ |> Map.put(:from_address_hash, from_address_hash)
+ |> Map.put(:fee_payer_address_hash, fee_payer_address_hash)
+ |> Map.put(:calls, [
+ %{"to" => first_call_address_hash, "value" => 1, "input" => "0x"},
+ %{"to" => second_call_address_hash, "value" => 2, "input" => "0x"},
+ %{"to" => nil, "value" => 3, "input" => "0xc0ffee"},
+ %{"to" => "0xdeadbeef", "value" => 4, "input" => "0x"},
+ %{"to" => 12_345, "value" => 5, "input" => "0x"}
+ ])
+
+ params_set = AddressCoinBalances.params_set(%{transactions_params: [transaction_params]})
+
+ assert MapSet.size(params_set) == 4
+ assert MapSet.member?(params_set, %{address_hash: from_address_hash, block_number: block_number})
+ assert MapSet.member?(params_set, %{address_hash: fee_payer_address_hash, block_number: block_number})
+ assert MapSet.member?(params_set, %{address_hash: first_call_address_hash, block_number: block_number})
+ assert MapSet.member?(params_set, %{address_hash: second_call_address_hash, block_number: block_number})
+ end
+
+ test "with regular transaction extracts nothing extra" do
+ block_number = 1
+
+ from_address_hash = to_string(Factory.address_hash())
+
+ transaction_params =
+ :transaction
+ |> Factory.params_for()
+ |> Map.put(:block_number, block_number)
+ |> Map.put(:from_address_hash, from_address_hash)
+ |> Map.put(:fee_payer_address_hash, nil)
+ |> Map.put(:calls, nil)
+
+ params_set = AddressCoinBalances.params_set(%{transactions_params: [transaction_params]})
+
+ assert MapSet.size(params_set) == 1
+ assert MapSet.member?(params_set, %{address_hash: from_address_hash, block_number: block_number})
+ end
+ end
+ end
+
if Application.compile_env(:explorer, :chain_type) == :arc do
describe "params_set/1 logs_params on Arc chain" do
test "with EIP-7708 Transfer queues non-burn from_address and to_address for coin balances" do
diff --git a/apps/indexer/test/indexer/transform/addresses.exs b/apps/indexer/test/indexer/transform/addresses_test.exs
similarity index 76%
rename from apps/indexer/test/indexer/transform/addresses.exs
rename to apps/indexer/test/indexer/transform/addresses_test.exs
index df5eb0a2c7e..2c1daa8e809 100644
--- a/apps/indexer/test/indexer/transform/addresses.exs
+++ b/apps/indexer/test/indexer/transform/addresses_test.exs
@@ -4,6 +4,10 @@ defmodule Indexer.Transform.AddressesTest do
alias Indexer.Transform.Addresses
+ @chain_type Application.compile_env(:explorer, :chain_type)
+
+ @created_contract_code_extracted? @chain_type != :zksync
+
doctest Addresses
describe "extract_addresses/1" do
@@ -60,6 +64,22 @@ defmodule Indexer.Transform.AddressesTest do
end
test "differing contract code is ignored" do
+ expected_address =
+ if @created_contract_code_extracted? do
+ %{
+ fetched_coin_balance_block_number: 2,
+ contract_code: "0x1",
+ hash: "0x0000000000000000000000000000000000000001",
+ nonce: nil
+ }
+ else
+ %{
+ fetched_coin_balance_block_number: 2,
+ hash: "0x0000000000000000000000000000000000000001",
+ nonce: nil
+ }
+ end
+
assert Addresses.extract_addresses(%{
internal_transactions: [
%{
@@ -73,14 +93,7 @@ defmodule Indexer.Transform.AddressesTest do
created_contract_address_hash: "0x0000000000000000000000000000000000000001"
}
]
- }) == [
- %{
- fetched_coin_balance_block_number: 2,
- contract_code: "0x1",
- hash: "0x0000000000000000000000000000000000000001",
- nonce: nil
- }
- ]
+ }) == [expected_address]
end
test "extracts address params from code params" do
@@ -154,6 +167,16 @@ defmodule Indexer.Transform.AddressesTest do
block_reward_contract_beneficiaries: [beneficiary]
}
+ created_contract_address = %{
+ hash: internal_transaction.created_contract_address_hash,
+ fetched_coin_balance_block_number: internal_transaction.block_number
+ }
+
+ created_contract_address =
+ if @created_contract_code_extracted?,
+ do: Map.put(created_contract_address, :contract_code, internal_transaction.created_contract_code),
+ else: created_contract_address
+
assert Addresses.extract_addresses(blockchain_data) == [
%{hash: block.miner_hash, fetched_coin_balance_block_number: block.number},
%{
@@ -164,11 +187,7 @@ defmodule Indexer.Transform.AddressesTest do
hash: internal_transaction.to_address_hash,
fetched_coin_balance_block_number: internal_transaction.block_number
},
- %{
- hash: internal_transaction.created_contract_address_hash,
- contract_code: internal_transaction.created_contract_code,
- fetched_coin_balance_block_number: internal_transaction.block_number
- },
+ created_contract_address,
%{
hash: transaction.from_address_hash,
fetched_coin_balance_block_number: transaction.block_number,
@@ -223,10 +242,14 @@ defmodule Indexer.Transform.AddressesTest do
]
}
- assert Addresses.extract_addresses(blockchain_data) ==
- [
- %{hash: hash, fetched_coin_balance_block_number: 34, contract_code: "code", nonce: 12}
- ]
+ expected_address =
+ if @created_contract_code_extracted? do
+ %{hash: hash, fetched_coin_balance_block_number: 34, contract_code: "code", nonce: 12}
+ else
+ %{hash: hash, fetched_coin_balance_block_number: 34, nonce: 12}
+ end
+
+ assert Addresses.extract_addresses(blockchain_data) == [expected_address]
end
test "only entities data defined in @entity_to_address_map are collected" do
@@ -272,6 +295,59 @@ defmodule Indexer.Transform.AddressesTest do
]
}) == []
end
+
+ if @chain_type == :eden do
+ test "recipients of the batched calls are extracted with the coin balance block number" do
+ first_call_hash = "0x11f60a633dd30a8d1a26dd6e20167a9293fb4647"
+ second_call_hash = "0xcfc096e58b1f858e5a3ee88ecaeccb2b464625b5"
+
+ addresses =
+ Addresses.extract_addresses(%{
+ transactions: [
+ %{
+ block_number: 34,
+ from_address_hash: "0x0000000000000000000000000000000000000001",
+ nonce: 12,
+ calls: [
+ %{"to" => first_call_hash, "value" => 1, "input" => "0x"},
+ %{"to" => second_call_hash, "value" => 2, "input" => "0x"},
+ %{"to" => nil, "value" => 3, "input" => "0xc0ffee"}
+ ]
+ }
+ ]
+ })
+
+ assert Enum.find(addresses, &(&1.hash == first_call_hash)) ==
+ %{hash: first_call_hash, fetched_coin_balance_block_number: 34}
+
+ assert Enum.find(addresses, &(&1.hash == second_call_hash)) ==
+ %{hash: second_call_hash, fetched_coin_balance_block_number: 34}
+
+ refute Enum.any?(addresses, &is_nil(&1.hash))
+ end
+
+ test "recipients of the batched calls without a `block_number` aren't extracted" do
+ assert Addresses.extract_addresses(%{
+ transactions: [
+ %{calls: [%{"to" => "0x11f60a633dd30a8d1a26dd6e20167a9293fb4647", "value" => 1, "input" => "0x"}]}
+ ]
+ }) == []
+ end
+
+ test "recipients of the batched calls which aren't valid address hashes aren't extracted" do
+ assert Addresses.extract_addresses(%{
+ transactions: [
+ %{
+ block_number: 34,
+ calls: [
+ %{"to" => "0xdeadbeef", "value" => 1, "input" => "0x"},
+ %{"to" => 12_345, "value" => 2, "input" => "0x"}
+ ]
+ }
+ ]
+ }) == []
+ end
+ end
end
describe "extract_addresses_from_collection/2" do
diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs
index b220b5e0af0..5e61955cc88 100644
--- a/apps/nft_media_handler/mix.exs
+++ b/apps/nft_media_handler/mix.exs
@@ -5,7 +5,7 @@ defmodule NFTMediaHandler.MixProject do
def project do
[
app: :nft_media_handler,
- version: "11.2.6",
+ version: "11.2.7",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs
index fc8d69df8be..b3604f8506e 100644
--- a/apps/utils/mix.exs
+++ b/apps/utils/mix.exs
@@ -5,7 +5,7 @@ defmodule Utils.MixProject do
def project do
[
app: :utils,
- version: "11.2.6",
+ version: "11.2.7",
build_path: "../../_build",
# config_path: "../../config/config.exs",
deps_path: "../../deps",
@@ -34,7 +34,7 @@ defmodule Utils.MixProject do
{:credo, "~> 1.5", only: [:test, :dev], runtime: false},
{:httpoison, "~> 2.0"},
{:mime, "~> 2.0"},
- {:tesla, "~> 1.20.0"}
+ {:tesla, "~> 1.21.0"}
]
end
diff --git a/config/runtime.exs b/config/runtime.exs
index 2c1e75525d9..333588105cf 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -349,7 +349,9 @@ config :explorer,
csv_export_limit: ConfigHelper.parse_integer_env_var("CSV_EXPORT_LIMIT", 10_000),
shrink_internal_transactions_enabled: ConfigHelper.parse_bool_env_var("SHRINK_INTERNAL_TRANSACTIONS_ENABLED"),
replica_max_lag: ConfigHelper.parse_time_env_var("REPLICA_MAX_LAG", "5m"),
- hackney_default_pool_size: ConfigHelper.parse_integer_env_var("HACKNEY_DEFAULT_POOL_SIZE", 1_000)
+ hackney_default_pool_size: ConfigHelper.parse_integer_env_var("HACKNEY_DEFAULT_POOL_SIZE", 1_000),
+ microservice_http_pool_size: ConfigHelper.parse_integer_env_var("MICROSERVICE_HTTP_POOL_SIZE", 1_000),
+ microservice_http_pool_count: ConfigHelper.parse_integer_env_var("MICROSERVICE_HTTP_POOL_COUNT", 20, min: 1)
config :explorer, Explorer.Chain.Health.Monitor,
check_interval: ConfigHelper.parse_time_env_var("HEALTH_MONITOR_CHECK_INTERVAL", "1m"),
@@ -363,6 +365,8 @@ config :explorer, :proxy,
implementation_data_ttl_via_avg_block_time:
ConfigHelper.parse_bool_env_var("CONTRACT_PROXY_IMPLEMENTATION_TTL_VIA_AVG_BLOCK_TIME", "true"),
fallback_cached_implementation_data_ttl: :timer.seconds(4),
+ empty_cached_implementation_data_ttl:
+ ConfigHelper.parse_time_env_var("CONTRACT_PROXY_EMPTY_IMPLEMENTATION_DATA_CACHE_TTL", "1d"),
implementation_data_fetching_timeout: :timer.seconds(2)
config :explorer, Explorer.Chain.Events.Listener,
diff --git a/cspell.json b/cspell.json
index 07ad111581c..e890a65a37c 100644
--- a/cspell.json
+++ b/cspell.json
@@ -140,6 +140,7 @@
"chakra",
"chardata",
"chartjs",
+ "checkin",
"checkmarks",
"checkproxyverification",
"checksummed",
diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env
index 42e982b8d40..faec6984bc9 100644
--- a/docker-compose/envs/common-blockscout.env
+++ b/docker-compose/envs/common-blockscout.env
@@ -163,7 +163,12 @@ RELEASE_LINK=
# CONTRACT_AUDIT_REPORTS_AIRTABLE_URL=
# CONTRACT_AUDIT_REPORTS_AIRTABLE_API_KEY=
# CONTRACT_CERTIFIED_LIST=
+<<<<<<< HEAD
CONTRACT_ENABLE_PARTIAL_REVERIFICATION=true
+=======
+# CONTRACT_ENABLE_PARTIAL_REVERIFICATION=
+# CONTRACT_PROXY_EMPTY_IMPLEMENTATION_DATA_CACHE_TTL=1d
+>>>>>>> v11.2.7
# UNCLES_IN_AVERAGE_BLOCK_TIME=false
# DISABLE_BLOCK_BROADCAST_ENRICHMENT=false
# BLOCK_BROADCAST_TYPE=block
@@ -732,4 +737,6 @@ REOWN_PROJECT_ID=4b6b77d337588564379b0ef8f3a5c479
# RE_CAPTCHA_CLIENT_KEY=
# RE_CAPTCHA_V3_CLIENT_KEY=
# HACKNEY_DEFAULT_POOL_SIZE=1000
+# MICROSERVICE_HTTP_POOL_SIZE=1000
+# MICROSERVICE_HTTP_POOL_COUNT=20
# UNIVERSAL_PROXY_CONFIG_URL=
diff --git a/docker/Makefile b/docker/Makefile
index 4895bbfbf07..c481f1399ed 100644
--- a/docker/Makefile
+++ b/docker/Makefile
@@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats
STATS_DB_CONTAINER_NAME := stats-db
PROXY_CONTAINER_NAME := proxy
PG_CONTAINER_NAME := postgres
-RELEASE_VERSION ?= '11.2.6'
+RELEASE_VERSION ?= '11.2.7'
TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h")
STABLE_TAG := $(RELEASE_VERSION)
diff --git a/mix.exs b/mix.exs
index 6f0a72c7c54..9221e1e8b40 100644
--- a/mix.exs
+++ b/mix.exs
@@ -8,7 +8,7 @@ defmodule BlockScout.Mixfile do
[
# app: :block_scout,
# aliases: aliases(config_env()),
- version: "11.2.6",
+ version: "11.2.7",
apps_path: "apps",
deps: deps(),
dialyzer: dialyzer(),
@@ -106,7 +106,7 @@ defmodule BlockScout.Mixfile do
[
{:prometheus_ex, "~> 5.1.0", override: true},
{:absinthe_plug, git: "https://github.com/blockscout/absinthe_plug.git", tag: "1.5.8", override: true},
- {:tesla, "~> 1.20.0"},
+ {:tesla, "~> 1.21.0"},
{:mint, "~> 1.9.0"},
# Documentation
{:ex_doc, "~> 0.40.1", only: :dev, runtime: false},
diff --git a/mix.lock b/mix.lock
index 2c96a20673c..b1b70a74290 100644
--- a/mix.lock
+++ b/mix.lock
@@ -66,7 +66,7 @@
"ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"},
"ex_json_schema": {:hex, :ex_json_schema, "0.11.3", "0f9128fde2a46976c21d1ed9506bf8462776ac9e333e7b42ececcdbf7433a1da", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "0e8e16866bc14339b1bf1e441f1d9231031c8fe2cf45fa5a988a8b685de8c002"},
"ex_keccak": {:hex, :ex_keccak, "0.7.8", "be1cf194d3158f0a305eaed0334e478d0d0f2c827e7c1f8f0e1e2a667da5a8ac", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "52de5b42b718df2534fb9a55780d8a05bbaea539f867c3e7c0a8e7e1d5f149d9"},
- "ex_machina": {:hex, :ex_machina, "2.8.1", "be50262d132107c2e3f9ccbbe0124118b944e3c2480459b59a712befc59f0d52", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "f25b8aab1c2765507a595f8fa1bbd3c180357fae49f8cdc720747f7ce5128cf8"},
+ "ex_machina": {:hex, :ex_machina, "2.8.2", "f0a46a6aeb1198577bf8e34625c69ee0f24bf3230e79e3e17b27fcb1d682a70d", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "42f551b2a3a26d138788160d44e391a6bf31266af48125c2b935f24521ebd644"},
"ex_pbkdf2": {:hex, :ex_pbkdf2, "0.8.5", "583733a1134a0c8b4df70c6b3e874d1896bf48f2aaafdf332ec5480c7b5117bf", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "f138b2acdc5a4d87880e42742e9bead4d1235fd9b1b2b29be0d1da1e01c3a312"},
"ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"},
"ex_secp256k1": {:hex, :ex_secp256k1, "0.8.0", "aade42e790638de82a2b951a83f55b9cc6545b8c105b20a0112ff6b78e1800cd", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "87257ef7110c45ac396a110eb93023371db96b6f65ed0676046a11e866d1e3a0"},
@@ -78,7 +78,7 @@
"ezstd": {:hex, :ezstd, "1.2.4", "7ab3ed4bf5ed93e249c936f457a060cf99487f392a41b2b3fd0f93e2c792a7d6", [:rebar3], [], "hexpm", "c79a63c8f1706ca5402d4d97347a1934f41fd8dd055c7af4ca92b9bcbef6d1c0"},
"file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
- "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
+ "finch": {:hex, :finch, "0.23.0", "e3f9287ac25a8832f848b144c2b57346aac65b205e2e0629a52adfe6507fd837", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "80e58d3f936f57e3fdf404f83a3642897ae6d9fb642934e46da4d8fe761b99d5"},
"floki": {:hex, :floki, "0.38.4", "10f98971e892aed2c2f1b3a0f928e488e3797e1c6dd3dfd98db40b14e9a78bcf", [:mix], [], "hexpm", "bdb34645eee8e79845c7edaca2d4099a52804ee4d4a3ecc683a69451f0244973"},
"flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"},
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
@@ -127,7 +127,11 @@
"numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"},
"nx": {:hex, :nx, "0.13.0", "43c57f529f17139821f6ed01a8aba6e045fef6722700c928cec2e2e3d2ee9560", [:mix], [{:complex, "~> 0.7", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "38d2f37ee7e2f3aaa86b96f125643ffbca56a89576cd24d83b452597397b4fbd"},
"oauth2": {:hex, :oauth2, "2.1.1", "3bec9bf49e88e1b0c0ddf9f44c393d71fd0f88e905766f2e2cc5d57e6bcc5352", [:mix], [{:tesla, "~> 1.18", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "1d5997cb1ff1643dac17076b6c00c91e4381d8389f3c49a8c390984606dad439"},
+<<<<<<< HEAD
"oban": {:hex, :oban, "2.23.0", "1867d0fa4e8c7685217b02cc2632e3ee86c93da770e9029ff71304d9e62e53d7", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e5f0cec5abecce78dd08cb14dc5438db90ec3884987b44773ce76fe60dd3f81"},
+=======
+ "oban": {:hex, :oban, "2.23.1", "0b9495e28a236ca0478c80d666c58a8b2b55182731c3603c6e766e298a08342a", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a9855b9f5d87e31de3e2f46731b163f372e329613892a56c5b2aacceb50ed508"},
+>>>>>>> v11.2.7
"open_api_spex": {:hex, :open_api_spex, "3.22.3", "0e383bf23cc3a060bffaebbcd09fc06bfc908d948c00e518aed36bbf8a2fe473", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "5f74f1878fdc38f8e961b0b943ac7af88dcf3a82a0c0ef6680ddfd3d161aecbd"},
"optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"},
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
@@ -146,7 +150,7 @@
"plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
"poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
- "postgrex": {:hex, :postgrex, "0.22.3", "bf65941737ee7a9adbe4a64c91080310d11703da343e8ac9188aacb9eb9f6f02", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "f018c13752b2b46e8d35d7e2d84c3276557cbfd880769109021a1d0ee36c1cfe"},
+ "postgrex": {:hex, :postgrex, "0.22.4", "d271f595dfd25230b6398354e19d17bb5e2d20130fd2d9bdca7e15f125d43552", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "4aae45a2d60e35b04eea2602440be152fae332901f1fc7a60fc7cb7f0f9a9c5a"},
"prometheus": {:hex, :prometheus, "6.1.3", "67c85683a265f36d35559bdddd49a34d857d4827888ba987c8c8b7954a3df80e", [:rebar3], [{:ddskerl, "0.4.3", [hex: :ddskerl, repo: "hexpm", optional: false]}], "hexpm", "bd9522b4dd21cf6670f18a647d1ef91ab2f633f02f9275b583fad1eb11f880bf"},
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"},
"prometheus_ex": {:hex, :prometheus_ex, "5.1.0", "a978945b4be5923b87edae3537e3fd61f8d04d755fae865ec4cbb1be7b5ec2e5", [:mix], [{:prometheus, "~> 6.1", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "15d3cd752063a1b51ffcba727fc0276f62d2a4387b523963883d125ea5e0d9e7"},
@@ -163,7 +167,7 @@
"rustler_precompiled": {:hex, :rustler_precompiled, "0.9.0", "3a052eda09f3d2436364645cc1f13279cf95db310eb0c17b0d8f25484b233aa0", [:mix], [{:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "471d97315bd3bf7b64623418b3693eedd8e47de3d1cb79a0ac8f9da7d770d94c"},
"siwe": {:git, "https://github.com/royal-markets/siwe-ex.git", "51c9c08240eb7eea3c35693011f8d260cd9bb3be", [ref: "51c9c08240eb7eea3c35693011f8d260cd9bb3be"]},
"sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"},
- "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"},
+ "sobelow": {:hex, :sobelow, "0.15.0", "b067d7f8522a9d758fa89cb2bfcbab7ad72c45a0993cb958c989c6fd956fdd56", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "24a800e2d7fa8c3bd21561b6ad8ad4745ed726a09fd606598981d9048708da98"},
"spandex": {:hex, :spandex, "3.2.0", "f8cd40146ea988c87f3c14054150c9a47ba17e53cd4515c00e1f93c29c45404d", [:mix], [{:decorator, "~> 1.2", [hex: :decorator, repo: "hexpm", optional: true]}, {:optimal, "~> 0.3.3", [hex: :optimal, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d0a7d5aef4c5af9cf5467f2003e8a5d8d2bdae3823a6cc95d776b9a2251d4d03"},
"spandex_datadog": {:hex, :spandex_datadog, "1.4.0", "0594b9655b0af00ab9137122616bc0208b68ceec01e9916ab13d6fbb33dcce35", [:mix], [{:msgpax, "~> 2.2.1 or ~> 2.3", [hex: :msgpax, repo: "hexpm", optional: false]}, {:spandex, "~> 3.2", [hex: :spandex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "360f8e1b4db238c1749c4872b1697b096429927fa42b8858d0bb782067380123"},
"spandex_ecto": {:hex, :spandex_ecto, "0.7.0", "259ad2feb7c834e774ec623f99c0fbacca8d60a73be212f92b75e37f853c81be", [:mix], [{:spandex, "~> 2.2 or ~> 3.0", [hex: :spandex, repo: "hexpm", optional: false]}], "hexpm", "c64784be79d95538013b7c60828830411c5c7aff1f4e8d66dfe564b3c83b500e"},
@@ -172,7 +176,7 @@
"statistex": {:hex, :statistex, "1.1.1", "73612aa7f79e53c30569be065fd121e380f1cf57bc4c2da5b41be9246da18df9", [:mix], [], "hexpm", "310c4b49b34adf683de3103639006bed233ab54c08a4add65a531448e653857c"},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
- "tesla": {:hex, :tesla, "1.20.0", "922b504e186da2ee82990d44e7422435f41b67403908a00340c3a0b23a17aa90", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21 or >= 4.0.2 and < 5.0.0-0", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "3ecb41cb458772332752c3acdfe983e23abb991f5a43cfd69a64e9ea3f4b0061"},
+ "tesla": {:hex, :tesla, "1.21.0", "de3dc7b0ddbbd72a2fdab02decc977c0fa910aa04cbfb92aaca5e93a839ad2a5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21 or >= 4.0.2 and < 5.0.0-0", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "1a0b8c9a7c3676beafd33db5e13f5e486af936c1a9a9f56b037e7861d916d333"},
"timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"},
"ton": {:hex, :ton, "0.5.1", "79745434a93e5f7de3572fdcf04feb048620f0edab9794fc412a73528672927d", [:mix], [{:cafezinho, "~> 0.4.4", [hex: :cafezinho, repo: "hexpm", optional: false]}, {:evil_crc32c, "~> 0.2.9", [hex: :evil_crc32c, repo: "hexpm", optional: false]}, {:ex_pbkdf2, "~> 0.8.4", [hex: :ex_pbkdf2, repo: "hexpm", optional: false]}, {:mnemoniac, "~> 0.1.4", [hex: :mnemoniac, repo: "hexpm", optional: false]}], "hexpm", "916f656c870902a61690347da9500c5ce27f04c02e02441363bac7b128030f07"},
"typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.3", "1e5f3b6c763f9b5725975d3ab7f1554525f1f1399b966f2425acf04f9d8dd4fe", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "dcbd9b35b9fda5fa9258e0ae629a99cf4473bd7adfb85785d3f71dfe7a9b2bc0"},
diff --git a/rel/config.exs b/rel/config.exs
index c6a5fba2c84..f44c8d5f20e 100644
--- a/rel/config.exs
+++ b/rel/config.exs
@@ -72,7 +72,7 @@ end
# will be used by default
release :blockscout do
- set version: "11.2.6"
+ set version: "11.2.7"
set applications: [
:runtime_tools,
block_scout_web: :permanent,
diff --git a/types-package/package-lock.json b/types-package/package-lock.json
index 5c8b3edcb21..b118b8c4658 100644
--- a/types-package/package-lock.json
+++ b/types-package/package-lock.json
@@ -213,9 +213,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
- "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
"dev": true,
"funding": [
{