diff --git a/CHANGELOG.md b/CHANGELOG.md
index a847de3b1d..fb0a5f0898 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,23 @@
## Current
### Features
+- [#2109](https://github.com/poanetwork/blockscout/pull/2109) - use bigger updates instead of `Multi` transactions in BlocksTransactionsMismatch
- [#2075](https://github.com/poanetwork/blockscout/pull/2075) - add blocks cache
+- [#2151](https://github.com/poanetwork/blockscout/pull/2151) - hide dropdown menu then other networks list is empty
### Fixes
+- [#2179](https://github.com/poanetwork/blockscout/pull/2179) - fix docker build error
+- [#2165](https://github.com/poanetwork/blockscout/pull/2165) - sort blocks by timestamp when calculating average block time
+- [#2175](https://github.com/poanetwork/blockscout/pull/2175) - fix coinmarketcap response errors
+- [#2164](https://github.com/poanetwork/blockscout/pull/2164) - fix large numbers in balance view card
+- [#2155](https://github.com/poanetwork/blockscout/pull/2155) - fix pending transaction query
+- [#2183](https://github.com/poanetwork/blockscout/pull/2183) - tile content aligning for mobile resolution fix, dai logo fix
+- [#2162](https://github.com/poanetwork/blockscout/pull/2162) - contract creation tile color changed
+- [#2144](https://github.com/poanetwork/blockscout/pull/2144) - 'page not found' images path fixed for goerli
+- [#2142](https://github.com/poanetwork/blockscout/pull/2142) - Removed posdao theme and logo, added 'page not found' image for goerli
+- [#2138](https://github.com/poanetwork/blockscout/pull/2138) - badge colors issue, api titles issue
+- [#2129](https://github.com/poanetwork/blockscout/pull/2129) - Fix for width of explorer elements
+- [#2121](https://github.com/poanetwork/blockscout/pull/2121) - Binding of 404 page
- [#2120](https://github.com/poanetwork/blockscout/pull/2120) - footer links and socials focus color issue
- [#2113](https://github.com/poanetwork/blockscout/pull/2113) - renewed logos for rsk, dai, blockscout; themes color changes for lukso; error images for lukso
- [#2112](https://github.com/poanetwork/blockscout/pull/2112) - themes color improvements, dropdown color issue
@@ -12,7 +26,22 @@
- [#2090](https://github.com/poanetwork/blockscout/pull/2090) - updated some ETC theme colors
- [#2096](https://github.com/poanetwork/blockscout/pull/2096) - RSK theme fixes
- [#2093](https://github.com/poanetwork/blockscout/pull/2093) - detect token transfer type for deprecated erc721 spec
-- [#2108](https://github.com/poanetwork/blockscout/pull/2108) - fixe uncle fetching without full transactions
+- [#2111](https://github.com/poanetwork/blockscout/pull/2111) - improve address transaction controller
+- [#2108](https://github.com/poanetwork/blockscout/pull/2108) - fix uncle fetching without full transactions
+- [#2128](https://github.com/poanetwork/blockscout/pull/2128) - add new function clause for uncle errors
+- [#2123](https://github.com/poanetwork/blockscout/pull/2123) - fix coins percentage view
+- [#2119](https://github.com/poanetwork/blockscout/pull/2119) - fix map logging
+- [#2130](https://github.com/poanetwork/blockscout/pull/2130) - fix navigation
+- [#2147](https://github.com/poanetwork/blockscout/pull/2147) - add rsk format of checksum
+- [#2149](https://github.com/poanetwork/blockscout/pull/2149) - remove pending transaction count
+- [#2169](https://github.com/poanetwork/blockscout/pull/2169) - add more validator reward types for xDai
+- [#2173](https://github.com/poanetwork/blockscout/pull/2173) - handle correctly empty transactions
+- [#2174](https://github.com/poanetwork/blockscout/pull/2174) - fix reward channel joining
+- [#2186](https://github.com/poanetwork/blockscout/pull/2186) - fix net version test
+
+### Chore
+- [#2127](https://github.com/poanetwork/blockscout/pull/2127) - use previouse chromedriver version
+- [#2118](https://github.com/poanetwork/blockscout/pull/2118) - show only the last decompiled contract
### Chore
@@ -41,6 +70,7 @@
- [#2037](https://github.com/poanetwork/blockscout/pull/2037) - add address logs search functionality
- [#2012](https://github.com/poanetwork/blockscout/pull/2012) - make all pages pagination async
- [#2064](https://github.com/poanetwork/blockscout/pull/2064) - feat: add fields to tx apis, small cleanups
+- [#2100](https://github.com/poanetwork/blockscout/pull/2100) - feat: eth_get_balance rpc endpoint
### Fixes
- [#2099](https://github.com/poanetwork/blockscout/pull/2099) - logs search input width
diff --git a/README.md b/README.md
index f53a43e897..e0e9ece96b 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,8 @@ Currently available block explorers (i.e. Etherscan and Etherchain) are closed s
| [xDai Chain](https://blockscout.com/poa/dai) | | [SafeChain](https://explorer.safechain.io) |
| | | [SpringChain](https://explorer.springrole.com/) |
| | | [Kotti Testnet](https://kottiexplorer.ethernode.io/) |
+| | | [Loom](http://plasma-blockexplorer.dappchains.com/) |
+| | | [Tenda](https://tenda.network) |
### Visual Interface
diff --git a/apps/block_scout_web/assets/css/components/_api.scss b/apps/block_scout_web/assets/css/components/_api.scss
index 6fc2bc9a97..820e1e8f01 100644
--- a/apps/block_scout_web/assets/css/components/_api.scss
+++ b/apps/block_scout_web/assets/css/components/_api.scss
@@ -1,7 +1,7 @@
-$api-text-monospace-color: $primary !default;
+$api-text-monospace-color: $secondary !default;
$api-text-monospace-background: rgba($api-text-monospace-color, 0.1) !default;
$api-anchors-list-background-color: #f6f7f9 !default;
-$api-doc-list-item-title-color: $primary !default;
+$api-doc-list-item-title-color: #333 !default;
$api-doc-list-item-view-more-color: $api-doc-list-item-title-color !default;
.api-text-monospace {
@@ -89,8 +89,8 @@ $api-doc-list-item-view-more-color: $api-doc-list-item-title-color !default;
.api-doc-list-item-title {
color: $api-doc-list-item-title-color;
- font-size: 17px;
- font-weight: 700;
+ font-size: 15px;
+ font-weight: 400;
line-height: 1.2;
margin: 0 0 15px;
}
diff --git a/apps/block_scout_web/assets/css/components/_badge.scss b/apps/block_scout_web/assets/css/components/_badge.scss
index 2891a6e4ff..9fad8b41dd 100644
--- a/apps/block_scout_web/assets/css/components/_badge.scss
+++ b/apps/block_scout_web/assets/css/components/_badge.scss
@@ -2,8 +2,8 @@ $badge-success-color: #15bba6 !default;
$badge-success-background-color: rgba($badge-success-color, 0.1) !default;
$badge-danger-color: #ed9966 !default;
$badge-danger-background-color: rgba($badge-danger-color, 0.1) !default;
-$badge-neutral-color: #333 !default;
-$badge-neutral-background-color: #e9e9e9 !default;
+$badge-neutral-color: $secondary !default;
+$badge-neutral-background-color: rgba($secondary, .1) !default;
.badge {
color: $white;
diff --git a/apps/block_scout_web/assets/css/components/_tile.scss b/apps/block_scout_web/assets/css/components/_tile.scss
index d5658647c5..c10fa2f9b6 100644
--- a/apps/block_scout_web/assets/css/components/_tile.scss
+++ b/apps/block_scout_web/assets/css/components/_tile.scss
@@ -4,7 +4,7 @@ $tile-type-reorg-color: $purple !default;
$tile-type-emission-reward-color: $lilac !default;
$tile-type-transaction-color: $blue !default;
$tile-type-contract-call-color: $green !default;
-$tile-type-contract-creation-color: $pink !default;
+$tile-type-contract-creation-color: $dark-purple !default;
$tile-type-token-transfer-color: $orange !default;
$tile-type-unique-token-color: $orange !default;
$tile-type-unique-token-image-color: $orange !default;
@@ -104,6 +104,18 @@ $tile-body-a-color: #5959d8 !default;
padding: 0 5px;
}
+.tile-transaction-type-block {
+ .tile-status-label {
+ padding: 0;
+ }
+}
+
+.tile-bottom {
+ @media (max-width: 767px) {
+ justify-content: flex-start !important;
+ }
+}
+
.tile-bottom-contents {
background-color: #f6f7f9;
font-size: 12px;
diff --git a/apps/block_scout_web/assets/css/components/_verify_other_explorers.scss b/apps/block_scout_web/assets/css/components/_verify_other_explorers.scss
index a3ea681eda..ce8cbdb926 100644
--- a/apps/block_scout_web/assets/css/components/_verify_other_explorers.scss
+++ b/apps/block_scout_web/assets/css/components/_verify_other_explorers.scss
@@ -18,9 +18,13 @@
line-height: 1.25;
display: inline-flex;
margin-bottom: 12px;
- @media (min-width: 1200px) {
+ width: 100%;
+ @media (min-width: 768px) {
margin-right: 10px;
+ }
+ @media (min-width: 1200px) {
margin-bottom: 0;
+ width: auto;
}
}
}
@@ -31,6 +35,8 @@
flex-grow: 2;
@media (min-width: 768px) {
flex-direction: row;
+ position: relative;
+ padding-right: 44px;
}
}
@@ -41,6 +47,7 @@
flex-grow: 2;
@media (min-width: 768px) {
margin-top: 0;
+ max-width: 188px;
}
@media (min-width: 1200px) {
min-width: 145px;
@@ -76,7 +83,7 @@
}
.exp-content {
- padding: 6px 9px 4px 9px;
+ padding: 6px 9px 5px 9px;
h3, div {
font-size: 10px;
line-height: 1;
@@ -117,7 +124,7 @@
display: inline-flex;
align-items: center;
justify-content: center;
- border: 1px solid $secondary;
+ border: 1px solid $btn-line-color;
border-radius: 2px;
margin-top: 10px;
transition: .1s ease-in;
@@ -125,12 +132,15 @@
@media (min-width: 768px) {
margin-left: 10px;
margin-top: 0;
+ position: absolute;
+ top: 0;
+ right: 0;
}
svg path {
- fill: $secondary;
+ fill: $btn-line-color;
}
&:hover {
- background-color: $secondary;
+ background-color: $btn-line-color;
svg path {
fill: #fff;
}
diff --git a/apps/block_scout_web/assets/css/theme/_base_variables.scss b/apps/block_scout_web/assets/css/theme/_base_variables.scss
index 3e97eb61b3..9f1b82c887 100644
--- a/apps/block_scout_web/assets/css/theme/_base_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_base_variables.scss
@@ -47,6 +47,7 @@ $yellow: #ffc107 !default;
$green: #20b760 !default;
$teal: #009097 !default;
$cyan: #90e1d8 !default;
+$dark-purple: #923dc3;
$colors: () !default;
$colors: map-merge(
diff --git a/apps/block_scout_web/assets/css/theme/_dai_variables.scss b/apps/block_scout_web/assets/css/theme/_dai_variables.scss
index 1e19866bbe..50cbcfa290 100644
--- a/apps/block_scout_web/assets/css/theme/_dai_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_dai_variables.scss
@@ -57,4 +57,9 @@ $card-tab-active: $secondary;
$dashboard-banner-gradient-end
);
}
-}
\ No newline at end of file
+}
+
+// Badges
+$badge-neutral-color: #20446e;
+$badge-neutral-background-color: rgba(#20446e, .1);
+$api-text-monospace-color: #20446e;
\ No newline at end of file
diff --git a/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss b/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss
index 9bec5a1243..68feab4dfc 100644
--- a/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss
@@ -70,3 +70,8 @@ $card-tab-active: $tertiary;
filter: brightness(0) invert(1);
}
}
+
+// Badges
+$badge-neutral-color: $tertiary;
+$badge-neutral-background-color: rgba($tertiary, .1);
+$api-text-monospace-color: $tertiary;
\ No newline at end of file
diff --git a/apps/block_scout_web/assets/css/theme/_goerli_variables.scss b/apps/block_scout_web/assets/css/theme/_goerli_variables.scss
index 3ab3e4d089..7900dd4c3b 100644
--- a/apps/block_scout_web/assets/css/theme/_goerli_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_goerli_variables.scss
@@ -73,3 +73,8 @@ $card-tab-active: $sub-accent-color;
);
}
}
+
+// Badges
+$badge-neutral-color: $sub-accent-color;
+$badge-neutral-background-color: rgba($sub-accent-color, .1);
+$api-text-monospace-color: $sub-accent-color;
diff --git a/apps/block_scout_web/assets/css/theme/_kovan_variables.scss b/apps/block_scout_web/assets/css/theme/_kovan_variables.scss
index 821232bbd5..351f046726 100644
--- a/apps/block_scout_web/assets/css/theme/_kovan_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_kovan_variables.scss
@@ -67,4 +67,11 @@ $card-tab-active: $tertiary;
$dashboard-banner-gradient-end
);
}
-}
\ No newline at end of file
+}
+
+// Badges
+$badge-success-color: #15bba6;
+$badge-success-background-color: rgba(#15bba6, .1);
+$badge-neutral-color: $tertiary;
+$badge-neutral-background-color: rgba($tertiary, .1);
+$api-text-monospace-color: $tertiary;
\ No newline at end of file
diff --git a/apps/block_scout_web/assets/css/theme/_lukso_variables.scss b/apps/block_scout_web/assets/css/theme/_lukso_variables.scss
index cfa415b7ae..65f9a73b5f 100644
--- a/apps/block_scout_web/assets/css/theme/_lukso_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_lukso_variables.scss
@@ -146,3 +146,8 @@ $dashboard-banner-network-plain-container-height: 150px;
}
}
}
+
+// Badges
+$badge-neutral-color: $tertiary;
+$badge-neutral-background-color: rgba($tertiary, .1);
+$api-text-monospace-color: $tertiary;
diff --git a/apps/block_scout_web/assets/css/theme/_neutral_variables.scss b/apps/block_scout_web/assets/css/theme/_neutral_variables.scss
index 8b6676a856..8a0a62d26e 100644
--- a/apps/block_scout_web/assets/css/theme/_neutral_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_neutral_variables.scss
@@ -59,4 +59,9 @@ $card-tab-active: $primary;
$dashboard-banner-gradient-end
);
}
-}
\ No newline at end of file
+}
+
+// Badges
+$badge-neutral-color: $primary;
+$badge-neutral-background-color: rgba($primary, .1);
+$api-text-monospace-color: $primary;
\ No newline at end of file
diff --git a/apps/block_scout_web/assets/css/theme/_poa_variables.scss b/apps/block_scout_web/assets/css/theme/_poa_variables.scss
index 8b6676a856..8a0a62d26e 100644
--- a/apps/block_scout_web/assets/css/theme/_poa_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_poa_variables.scss
@@ -59,4 +59,9 @@ $card-tab-active: $primary;
$dashboard-banner-gradient-end
);
}
-}
\ No newline at end of file
+}
+
+// Badges
+$badge-neutral-color: $primary;
+$badge-neutral-background-color: rgba($primary, .1);
+$api-text-monospace-color: $primary;
\ No newline at end of file
diff --git a/apps/block_scout_web/assets/css/theme/_posdao_variables.scss b/apps/block_scout_web/assets/css/theme/_posdao_variables.scss
deleted file mode 100644
index 9d92ca2adc..0000000000
--- a/apps/block_scout_web/assets/css/theme/_posdao_variables.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-$primary: #15bba6;
-$secondary: #17314f;
-$tertiary: #00ff00;
-
-$header-links-color-active: #333;
-$dashboard-banner-gradient-start: $secondary;
-$dashboard-banner-gradient-end: #1e4168;
-
-$dashboard-line-color-market: $primary;
-
-$tile-type-block-border-color: $secondary;
-$tile-type-block-color: #333;
-
-$footer-background-color: #173250;
-$footer-text-color: #909dac;
-
-$navbar-logo-height: auto;
-$navbar-logo-width: 100px;
-
-$footer-logo-height: auto;
-$footer-logo-width: 100px;
-
-$card-background-1: $secondary;
-$card-background-1-text-color: #fff;
-
-$btn-copy-color: $secondary;
-$btn-qr-color: $secondary;
-
-$btn-dropdown-line-color: $secondary;
\ No newline at end of file
diff --git a/apps/block_scout_web/assets/css/theme/_rsk_variables.scss b/apps/block_scout_web/assets/css/theme/_rsk_variables.scss
index e7536db13c..76029e1a34 100644
--- a/apps/block_scout_web/assets/css/theme/_rsk_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_rsk_variables.scss
@@ -61,3 +61,7 @@ $card-tab-active: $secondary;
filter: brightness(0) invert(1);
}
}
+
+// Badges
+$badge-neutral-color: #1a323b;
+$badge-neutral-background-color: rgba(#1a323b, .1);
diff --git a/apps/block_scout_web/assets/css/theme/_sokol_variables.scss b/apps/block_scout_web/assets/css/theme/_sokol_variables.scss
index 2d80e64f03..71822bdc5a 100644
--- a/apps/block_scout_web/assets/css/theme/_sokol_variables.scss
+++ b/apps/block_scout_web/assets/css/theme/_sokol_variables.scss
@@ -65,3 +65,7 @@ $card-tab-active: $sub-accent-color;
);
}
}
+
+// Badges
+$badge-neutral-color: $tertiary;
+$badge-neutral-background-color: rgba($tertiary, .1);
\ No newline at end of file
diff --git a/apps/block_scout_web/assets/js/lib/currency.js b/apps/block_scout_web/assets/js/lib/currency.js
index 99c6cfe1f1..9b7493f1ad 100644
--- a/apps/block_scout_web/assets/js/lib/currency.js
+++ b/apps/block_scout_web/assets/js/lib/currency.js
@@ -18,6 +18,7 @@ function formatCurrencyValue (value, symbol) {
if (value < 0.000001) return `${window.localized['Less than']} ${symbol}0.000001`
if (value < 1) return `${symbol}${numeral(value).format('0.000000')}`
if (value < 100000) return `${symbol}${numeral(value).format('0,0.00')}`
+ if (value > 1000000000) return `${symbol}${numeral(value).format('0.000e+0')}`
return `${symbol}${numeral(value).format('0,0')}`
}
diff --git a/apps/block_scout_web/assets/static/images/dai_logo.svg b/apps/block_scout_web/assets/static/images/dai_logo.svg
index cddb632a3e..3ce9609c84 100644
--- a/apps/block_scout_web/assets/static/images/dai_logo.svg
+++ b/apps/block_scout_web/assets/static/images/dai_logo.svg
@@ -1 +1,11 @@
-
\ No newline at end of file
+
diff --git a/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found.png b/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found.png
new file mode 100644
index 0000000000..8758824715
Binary files /dev/null and b/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found.png differ
diff --git a/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found@2x.png b/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found@2x.png
new file mode 100644
index 0000000000..19557af509
Binary files /dev/null and b/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found@2x.png differ
diff --git a/apps/block_scout_web/assets/static/images/goerli_logo.png b/apps/block_scout_web/assets/static/images/goerli_logo.png
deleted file mode 100644
index 910a7b2362..0000000000
Binary files a/apps/block_scout_web/assets/static/images/goerli_logo.png and /dev/null differ
diff --git a/apps/block_scout_web/assets/static/images/posdao_logo.png b/apps/block_scout_web/assets/static/images/posdao_logo.png
deleted file mode 100755
index 535b0ab6ac..0000000000
Binary files a/apps/block_scout_web/assets/static/images/posdao_logo.png and /dev/null differ
diff --git a/apps/block_scout_web/assets/static/images/posdao_logo_footer.png b/apps/block_scout_web/assets/static/images/posdao_logo_footer.png
deleted file mode 100755
index 1b49d8eff2..0000000000
Binary files a/apps/block_scout_web/assets/static/images/posdao_logo_footer.png and /dev/null differ
diff --git a/apps/block_scout_web/assets/static/images/posdao_logo_footer.svg b/apps/block_scout_web/assets/static/images/posdao_logo_footer.svg
deleted file mode 100644
index 9a26bec27f..0000000000
--- a/apps/block_scout_web/assets/static/images/posdao_logo_footer.svg
+++ /dev/null
@@ -1,21 +0,0 @@
-
diff --git a/apps/block_scout_web/lib/block_scout_web/channels/reward_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/reward_channel.ex
index 5176182d34..6f6ff3a70b 100644
--- a/apps/block_scout_web/lib/block_scout_web/channels/reward_channel.ex
+++ b/apps/block_scout_web/lib/block_scout_web/channels/reward_channel.ex
@@ -11,9 +11,10 @@ defmodule BlockScoutWeb.RewardChannel do
intercept(["new_reward"])
def join("rewards:" <> address_hash, _params, socket) do
- {:ok, hash} = Chain.string_to_address_hash(address_hash)
- {:ok, address} = Chain.hash_to_address(hash)
- {:ok, %{}, assign(socket, :current_address, address)}
+ with {:ok, hash} <- Chain.string_to_address_hash(address_hash),
+ {:ok, address} <- Chain.hash_to_address(hash) do
+ {:ok, %{}, assign(socket, :current_address, address)}
+ end
end
def handle_out("new_reward", %{emission_funds: emission_funds, validator: validator}, socket) do
diff --git a/apps/block_scout_web/lib/block_scout_web/controller.ex b/apps/block_scout_web/lib/block_scout_web/controller.ex
index fe2851c197..cf545f5260 100644
--- a/apps/block_scout_web/lib/block_scout_web/controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controller.ex
@@ -12,8 +12,9 @@ defmodule BlockScoutWeb.Controller do
def not_found(conn) do
conn
|> put_status(:not_found)
- |> put_view(BlockScoutWeb.ErrorView)
- |> render("404.html")
+ |> put_view(BlockScoutWeb.PageNotFoundView)
+ |> render(:index)
+ |> halt()
end
def unprocessable_entity(conn) do
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
index da22b8c042..6742d01b60 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
@@ -28,7 +28,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
- {:ok, address} <- Chain.hash_to_address(address_hash) do
+ {:ok, address} <- Chain.hash_to_address(address_hash, [:names], false) do
options =
@transaction_necessity_by_association
|> put_in([:necessity_by_association, :block], :required)
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
index be10089bad..3150d3bd60 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex
@@ -20,6 +20,35 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
|> render(:listaccounts, %{accounts: accounts})
end
+ def eth_get_balance(conn, params) do
+ with {:address_param, {:ok, address_param}} <- fetch_address(params),
+ {:block_param, {:ok, block}} <- {:block_param, fetch_block_param(params)},
+ {:format, {:ok, address_hash}} <- to_address_hash(address_param),
+ {:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address_hash, block)} do
+ render(conn, :eth_get_balance, %{balance: Wei.hex_format(balance)})
+ else
+ {:address_param, :error} ->
+ conn
+ |> put_status(400)
+ |> render(:eth_get_balance_error, %{message: "Query parameter 'address' is required"})
+
+ {:format, :error} ->
+ conn
+ |> put_status(400)
+ |> render(:eth_get_balance_error, %{error: "Invalid address hash"})
+
+ {:block_param, :error} ->
+ conn
+ |> put_status(400)
+ |> render(:eth_get_balance_error, %{error: "Invalid block"})
+
+ {:balance, {:error, :not_found}} ->
+ conn
+ |> put_status(404)
+ |> render(:eth_get_balance_error, %{error: "Balance not found"})
+ end
+ end
+
def balance(conn, params, template \\ :balance) do
with {:address_param, {:ok, address_param}} <- fetch_address(params),
{:format, {:ok, address_hashes}} <- to_address_hashes(address_param) do
@@ -217,6 +246,20 @@ defmodule BlockScoutWeb.API.RPC.AddressController do
{:required_params, result}
end
+ defp fetch_block_param(%{"block" => "latest"}), do: {:ok, :latest}
+ defp fetch_block_param(%{"block" => "earliest"}), do: {:ok, :earliest}
+ defp fetch_block_param(%{"block" => "pending"}), do: {:ok, :pending}
+
+ defp fetch_block_param(%{"block" => string_integer}) when is_bitstring(string_integer) do
+ case Integer.parse(string_integer) do
+ {integer, ""} -> {:ok, integer}
+ _ -> :error
+ end
+ end
+
+ defp fetch_block_param(%{"block" => _block}), do: :error
+ defp fetch_block_param(_), do: {:ok, :latest}
+
defp to_valid_format(params, :tokenbalance) do
result =
with {:ok, contract_address_hash} <- to_address_hash(params, "contractaddress"),
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/eth_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/eth_controller.ex
new file mode 100644
index 0000000000..693772ed8c
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/eth_controller.ex
@@ -0,0 +1,118 @@
+defmodule BlockScoutWeb.API.RPC.EthController do
+ use BlockScoutWeb, :controller
+
+ alias Explorer.Chain
+ alias Explorer.Chain.Wei
+
+ def eth_request(%{body_params: %{"_json" => requests}} = conn, _) when is_list(requests) do
+ responses = responses(requests)
+
+ conn
+ |> put_status(200)
+ |> render("responses.json", %{responses: responses})
+ end
+
+ def eth_request(%{body_params: %{"_json" => request}} = conn, _) do
+ [response] = responses([request])
+
+ conn
+ |> put_status(200)
+ |> render("response.json", %{response: response})
+ end
+
+ def eth_request(conn, request) do
+ # In the case that the JSON body is sent up w/o a json content type,
+ # Phoenix encodes it as a single key value pair, with the value being
+ # nil and the body being the key (as in a CURL request w/ no content type header)
+ decoded_request =
+ with [{single_key, nil}] <- Map.to_list(request),
+ {:ok, decoded} <- Jason.decode(single_key) do
+ decoded
+ else
+ _ -> request
+ end
+
+ [response] = responses([decoded_request])
+
+ conn
+ |> put_status(200)
+ |> render("response.json", %{response: response})
+ end
+
+ defp responses(requests) do
+ Enum.map(requests, fn request ->
+ with {:id, {:ok, id}} <- {:id, Map.fetch(request, "id")},
+ {:request, {:ok, result}} <- {:request, do_eth_request(request)} do
+ format_success(result, id)
+ else
+ {:id, :error} -> format_error("id is a required field", 0)
+ {:request, {:error, message}} -> format_error(message, Map.get(request, "id"))
+ end
+ end)
+ end
+
+ defp format_success(result, id) do
+ %{result: result, id: id}
+ end
+
+ defp format_error(message, id) do
+ %{error: message, id: id}
+ end
+
+ defp do_eth_request(%{"jsonrpc" => rpc_version}) when rpc_version != "2.0" do
+ {:error, "invalid rpc version"}
+ end
+
+ defp do_eth_request(%{"jsonrpc" => "2.0", "method" => method, "params" => params})
+ when is_list(params) do
+ with {:ok, action} <- get_action(method),
+ true <- :erlang.function_exported(__MODULE__, action, Enum.count(params)) do
+ apply(__MODULE__, action, params)
+ else
+ _ ->
+ {:error, "Action not found."}
+ end
+ end
+
+ defp do_eth_request(%{"params" => _params, "method" => _}) do
+ {:error, "Invalid params. Params must be a list."}
+ end
+
+ defp do_eth_request(_) do
+ {:error, "Method, params, and jsonrpc, are all required parameters."}
+ end
+
+ def eth_get_balance(address_param, block_param \\ nil) do
+ with {:address, {:ok, address}} <- {:address, Chain.string_to_address_hash(address_param)},
+ {:block, {:ok, block}} <- {:block, block_param(block_param)},
+ {:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address, block)} do
+ {:ok, Wei.hex_format(balance)}
+ else
+ {:address, :error} ->
+ {:error, "Query parameter 'address' is invalid"}
+
+ {:block, :error} ->
+ {:error, "Query parameter 'block' is invalid"}
+
+ {:balance, {:error, :not_found}} ->
+ {:error, "Balance not found"}
+ end
+ end
+
+ defp get_action("eth_getBalance"), do: {:ok, :eth_get_balance}
+ defp get_action(_), do: :error
+
+ defp block_param("latest"), do: {:ok, :latest}
+ defp block_param("earliest"), do: {:ok, :earliest}
+ defp block_param("pending"), do: {:ok, :pending}
+
+ defp block_param(string_integer) when is_bitstring(string_integer) do
+ case Integer.parse(string_integer) do
+ {integer, ""} -> {:ok, integer}
+ _ -> :error
+ end
+ end
+
+ defp block_param(nil), do: {:ok, :latest}
+ defp block_param(_), do: :error
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/page_not_found_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/page_not_found_controller.ex
new file mode 100644
index 0000000000..e453e5463b
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/page_not_found_controller.ex
@@ -0,0 +1,8 @@
+defmodule BlockScoutWeb.PageNotFoundController do
+ use BlockScoutWeb, :controller
+
+ def index(conn, _params) do
+ conn
+ |> render("index.html")
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
index 95ffe8aa22..1128f04616 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
@@ -13,7 +13,8 @@ defmodule BlockScoutWeb.PendingTransactionController do
[
necessity_by_association: %{
[from_address: :names] => :optional,
- [to_address: :names] => :optional
+ [to_address: :names] => :optional,
+ [created_contract_address: :names] => :optional
}
],
paging_options(params)
@@ -51,10 +52,7 @@ defmodule BlockScoutWeb.PendingTransactionController do
end
def index(conn, _params) do
- render(conn, "index.html",
- current_path: current_path(conn),
- pending_transaction_count: Chain.pending_transaction_count()
- )
+ render(conn, "index.html", current_path: current_path(conn))
end
defp get_pending_transactions_and_next_page(options) do
diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex
index 74a9fa761c..2191a86fa2 100644
--- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex
+++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex
@@ -100,6 +100,12 @@ defmodule BlockScoutWeb.Etherscan do
"result" => []
}
+ @account_eth_get_balance_example_value %{
+ "jsonrpc" => "2.0",
+ "result" => "0x0234c8a3397aab58",
+ "id" => 1
+ }
+
@account_tokentx_example_value %{
"status" => "1",
"message" => "OK",
@@ -1028,6 +1034,49 @@ defmodule BlockScoutWeb.Etherscan do
}
}
+ @account_eth_get_balance_action %{
+ name: "eth_get_balance",
+ description:
+ "Mimics Ethereum JSON RPC's eth_getBalance. Returns the balance as of the provided block (defaults to latest)",
+ required_params: [
+ %{
+ key: "address",
+ placeholder: "addressHash",
+ type: "string",
+ description: "The address of the account."
+ }
+ ],
+ optional_params: [
+ %{
+ key: "block",
+ placeholder: "block",
+ type: "string",
+ description: """
+ Either the block number as a string, or one of latest, earliest or pending
+
+ latest will be the latest balance in a *consensus* block.
+ earliest will be the first recorded balance for the address.
+ pending will be the latest balance in consensus *or* nonconcensus blocks.
+ """
+ }
+ ],
+ responses: [
+ %{
+ code: "200",
+ description: "successful operation",
+ example_value: Jason.encode!(@account_eth_get_balance_example_value),
+ model: %{
+ name: "Result",
+ fields: %{
+ jsonrpc: @jsonrpc_version_type,
+ id: @id_type,
+ result: @hex_number_type
+ }
+ }
+ }
+ ]
+ }
+
@account_balance_action %{
name: "balance",
description: """
@@ -2203,6 +2252,7 @@ defmodule BlockScoutWeb.Etherscan do
@account_module %{
name: "account",
actions: [
+ @account_eth_get_balance_action,
@account_balance_action,
@account_balancemulti_action,
@account_txlist_action,
diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex
index aa19725811..786c77cdff 100644
--- a/apps/block_scout_web/lib/block_scout_web/router.ex
+++ b/apps/block_scout_web/lib/block_scout_web/router.ex
@@ -32,6 +32,8 @@ defmodule BlockScoutWeb.Router do
alias BlockScoutWeb.API.RPC
+ post("/eth_rpc", EthController, :eth_request)
+
forward("/", RPCTranslator, %{
"block" => RPC.BlockController,
"account" => RPC.AddressController,
@@ -245,5 +247,7 @@ defmodule BlockScoutWeb.Router do
get("/chain_blocks", ChainController, :chain_blocks, as: :chain_blocks)
get("/api_docs", APIDocsController, :index)
+
+ get("/:page", PageNotFoundController, :index)
end
end
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex
index 270450c2c0..c2e62e2004 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex
@@ -39,7 +39,7 @@
- <% if @total_supply do %>
+ <%= if @total_supply do %>
(<%= balance_percentage(@address, @total_supply) %>)
<% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex
index 173744603c..6cb4ccf6ab 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex
@@ -2,7 +2,8 @@
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
+
<%= @transaction |> block_number() |> BlockScoutWeb.RenderHelpers.render_partial() %>
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex
index b92d356479..438f344199 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex
@@ -230,10 +230,8 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do
end)
end
- def sort_contracts_by_version(decompiled_contracts) do
- decompiled_contracts
- |> Enum.sort_by(& &1.decompiler_version)
- |> Enum.reverse()
+ def last_decompiled_contract_version(decompiled_contracts) do
+ Enum.max_by(decompiled_contracts, & &1.decompiler_version)
end
defp add_line_numbers(code) do
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
index 7aaf26dba0..248dc942c7 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex
@@ -1,7 +1,7 @@
defmodule BlockScoutWeb.API.RPC.AddressView do
use BlockScoutWeb, :view
- alias BlockScoutWeb.API.RPC.RPCView
+ alias BlockScoutWeb.API.RPC.{EthRPCView, RPCView}
def render("listaccounts.json", %{accounts: accounts}) do
accounts = Enum.map(accounts, &prepare_account/1)
@@ -51,6 +51,10 @@ defmodule BlockScoutWeb.API.RPC.AddressView do
RPCView.render("show.json", data: data)
end
+ def render("eth_get_balance_error.json", %{error: message}) do
+ EthRPCView.render("error.json", %{error: message, id: 0})
+ end
+
def render("error.json", assigns) do
RPCView.render("error.json", assigns)
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex
index 39eb5ae9d1..5dda92d3d5 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex
@@ -17,16 +17,48 @@ defmodule BlockScoutWeb.API.RPC.EthRPCView do
}
end
+ def render("response.json", %{response: %{error: error, id: id}}) do
+ %__MODULE__{
+ error: error,
+ id: id
+ }
+ end
+
+ def render("response.json", %{response: %{result: result, id: id}}) do
+ %__MODULE__{
+ result: result,
+ id: id
+ }
+ end
+
+ def render("responses.json", %{responses: responses}) do
+ Enum.map(responses, fn
+ %{error: error, id: id} ->
+ %__MODULE__{
+ error: error,
+ id: id
+ }
+
+ %{result: result, id: id} ->
+ %__MODULE__{
+ result: result,
+ id: id
+ }
+ end)
+ end
+
defimpl Poison.Encoder, for: BlockScoutWeb.API.RPC.EthRPCView do
def encode(%BlockScoutWeb.API.RPC.EthRPCView{result: result, id: id, error: error}, _options) when is_nil(error) do
+ result = Poison.encode!(result)
+
"""
- {"jsonrpc":"2.0","result":"#{result}","id":#{id}}
+ {"jsonrpc":"2.0","result":#{result},"id":#{id}}
"""
end
def encode(%BlockScoutWeb.API.RPC.EthRPCView{id: id, error: error}, _options) do
"""
- {"jsonrpc":"2.0","error": #{error},"id": #{id}}
+ {"jsonrpc":"2.0","error": "#{error}","id": #{id}}
"""
end
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_view.ex
new file mode 100644
index 0000000000..739f3ac8a7
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_view.ex
@@ -0,0 +1,13 @@
+defmodule BlockScoutWeb.API.RPC.EthView do
+ use BlockScoutWeb, :view
+
+ alias BlockScoutWeb.API.RPC.EthRPCView
+
+ def render("responses.json", %{responses: responses}) do
+ EthRPCView.render("responses.json", %{responses: responses})
+ end
+
+ def render("response.json", %{response: response}) do
+ EthRPCView.render("response.json", %{response: response})
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex b/apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex
new file mode 100644
index 0000000000..b5a18f0434
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex
@@ -0,0 +1,5 @@
+defmodule BlockScoutWeb.PageNotFoundView do
+ use BlockScoutWeb, :view
+
+ @dialyzer :no_match
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/wei_helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/wei_helpers.ex
index 3488e60522..5cdbff8c0c 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/wei_helpers.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/wei_helpers.ex
@@ -57,13 +57,19 @@ defmodule BlockScoutWeb.WeiHelpers do
converted_value =
wei
|> Wei.to(unit)
- |> Cldr.Number.to_string!(format: "#,##0.##################")
+
+ formatted_value =
+ if Decimal.cmp(converted_value, 1_000_000_000_000) == :gt do
+ Cldr.Number.to_string!(converted_value, format: "0.###E+0")
+ else
+ Cldr.Number.to_string!(converted_value, format: "#,##0.##################")
+ end
if Keyword.get(options, :include_unit_label, true) do
display_unit = display_unit(unit)
- "#{converted_value} #{display_unit}"
+ "#{formatted_value} #{display_unit}"
else
- converted_value
+ formatted_value
end
end
diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot
index e9763e2287..4e05e8266f 100644
--- a/apps/block_scout_web/priv/gettext/default.pot
+++ b/apps/block_scout_web/priv/gettext/default.pot
@@ -378,7 +378,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/_tile.html.eex:30
#: lib/block_scout_web/templates/transaction/overview.html.eex:192
-#: lib/block_scout_web/views/wei_helpers.ex:72
+#: lib/block_scout_web/views/wei_helpers.ex:78
msgid "Ether"
msgstr ""
@@ -436,7 +436,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/views/block_view.ex:20
-#: lib/block_scout_web/views/wei_helpers.ex:71
+#: lib/block_scout_web/views/wei_helpers.ex:77
msgid "Gwei"
msgstr ""
@@ -982,7 +982,7 @@ msgid "Wallet addresses"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/views/wei_helpers.ex:70
+#: lib/block_scout_web/views/wei_helpers.ex:76
msgid "Wei"
msgstr ""
@@ -1490,7 +1490,7 @@ msgid "EVM Version"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:16
+#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:17
msgid "Copy Decompiled Contract Code"
msgstr ""
@@ -1505,12 +1505,12 @@ msgid "Decompiled code"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:14
+#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:15
msgid "Decompiled contract code"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:7
+#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:8
msgid "Decompiler version"
msgstr ""
@@ -1697,3 +1697,8 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:178
msgid " Token Transfer"
msgstr ""
+
+#, elixir-format
+#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:27
+msgid "There is no decompilded contracts for this address."
+msgstr ""
diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
index 654a848278..142aec2b37 100644
--- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
+++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
@@ -378,7 +378,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/_tile.html.eex:30
#: lib/block_scout_web/templates/transaction/overview.html.eex:192
-#: lib/block_scout_web/views/wei_helpers.ex:72
+#: lib/block_scout_web/views/wei_helpers.ex:78
msgid "Ether"
msgstr "POA"
@@ -436,7 +436,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/views/block_view.ex:20
-#: lib/block_scout_web/views/wei_helpers.ex:71
+#: lib/block_scout_web/views/wei_helpers.ex:77
msgid "Gwei"
msgstr ""
@@ -982,7 +982,7 @@ msgid "Wallet addresses"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/views/wei_helpers.ex:70
+#: lib/block_scout_web/views/wei_helpers.ex:76
msgid "Wei"
msgstr ""
@@ -1490,7 +1490,7 @@ msgid "EVM Version"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:16
+#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:17
msgid "Copy Decompiled Contract Code"
msgstr ""
@@ -1505,12 +1505,12 @@ msgid "Decompiled code"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:14
+#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:15
msgid "Decompiled contract code"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:7
+#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:8
msgid "Decompiler version"
msgstr ""
@@ -1693,7 +1693,12 @@ msgstr ""
msgid "New Smart Contract Verification"
msgstr ""
-#, elixir-format, fuzzy
+#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:178
msgid " Token Transfer"
msgstr ""
+
+#, elixir-format, fuzzy
+#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:27
+msgid "There is no decompilded contracts for this address."
+msgstr ""
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs
new file mode 100644
index 0000000000..b26becee2f
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs
@@ -0,0 +1,199 @@
+defmodule BlockScoutWeb.API.RPC.EthControllerTest do
+ use BlockScoutWeb.ConnCase, async: false
+
+ alias Explorer.Counters.{AddressesWithBalanceCounter, AverageBlockTime}
+ alias Indexer.Fetcher.CoinBalanceOnDemand
+
+ setup do
+ mocked_json_rpc_named_arguments = [
+ transport: EthereumJSONRPC.Mox,
+ transport_options: []
+ ]
+
+ start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
+ start_supervised!(AverageBlockTime)
+ start_supervised!({CoinBalanceOnDemand, [mocked_json_rpc_named_arguments, [name: CoinBalanceOnDemand]]})
+ start_supervised!(AddressesWithBalanceCounter)
+
+ Application.put_env(:explorer, AverageBlockTime, enabled: true)
+
+ on_exit(fn ->
+ Application.put_env(:explorer, AverageBlockTime, enabled: false)
+ end)
+
+ :ok
+ end
+
+ defp params(api_params, params), do: Map.put(api_params, "params", params)
+
+ describe "eth_get_balance" do
+ setup do
+ %{
+ api_params: %{
+ "method" => "eth_getBalance",
+ "jsonrpc" => "2.0",
+ "id" => 0
+ }
+ }
+ end
+
+ test "with an invalid address", %{conn: conn, api_params: api_params} do
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, ["badHash"]))
+ |> json_response(200)
+
+ assert %{"error" => "Query parameter 'address' is invalid"} = response
+ end
+
+ test "with a valid address that has no balance", %{conn: conn, api_params: api_params} do
+ address = insert(:address)
+
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, [to_string(address.hash)]))
+ |> json_response(200)
+
+ assert %{"error" => "Balance not found"} = response
+ end
+
+ test "with a valid address that has a balance", %{conn: conn, api_params: api_params} do
+ block = insert(:block)
+ address = insert(:address)
+
+ insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
+
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, [to_string(address.hash)]))
+ |> json_response(200)
+
+ assert %{"result" => "0x1"} = response
+ end
+
+ test "with a valid address that has no earliest balance", %{conn: conn, api_params: api_params} do
+ block = insert(:block, number: 1)
+ address = insert(:address)
+
+ insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
+
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "earliest"]))
+ |> json_response(200)
+
+ assert response["error"] == "Balance not found"
+ end
+
+ test "with a valid address that has an earliest balance", %{conn: conn, api_params: api_params} do
+ block = insert(:block, number: 0)
+ address = insert(:address)
+
+ insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
+
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "earliest"]))
+ |> json_response(200)
+
+ assert response["result"] == "0x1"
+ end
+
+ test "with a valid address and no pending balance", %{conn: conn, api_params: api_params} do
+ block = insert(:block, number: 1, consensus: true)
+ address = insert(:address)
+
+ insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
+
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"]))
+ |> json_response(200)
+
+ assert response["error"] == "Balance not found"
+ end
+
+ test "with a valid address and a pending balance", %{conn: conn, api_params: api_params} do
+ block = insert(:block, number: 1, consensus: false)
+ address = insert(:address)
+
+ insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
+
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"]))
+ |> json_response(200)
+
+ assert response["result"] == "0x1"
+ end
+
+ test "with a valid address and a pending balance after a consensus block", %{conn: conn, api_params: api_params} do
+ insert(:block, number: 1, consensus: true)
+ block = insert(:block, number: 2, consensus: false)
+ address = insert(:address)
+
+ insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1)
+
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"]))
+ |> json_response(200)
+
+ assert response["result"] == "0x1"
+ end
+
+ test "with a block provided", %{conn: conn, api_params: api_params} do
+ address = insert(:address)
+
+ insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1)
+ insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2)
+ insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3)
+
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "2"]))
+ |> json_response(200)
+
+ assert response["result"] == "0x2"
+ end
+
+ test "with a block provided and no balance", %{conn: conn, api_params: api_params} do
+ address = insert(:address)
+
+ insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3)
+
+ assert response =
+ conn
+ |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "2"]))
+ |> json_response(200)
+
+ assert response["error"] == "Balance not found"
+ end
+
+ test "with a batch of requests", %{conn: conn} do
+ address = insert(:address)
+
+ insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1)
+ insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2)
+ insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3)
+
+ params = [
+ %{"id" => 0, "params" => [to_string(address.hash), "1"], "jsonrpc" => "2.0", "method" => "eth_getBalance"},
+ %{"id" => 1, "params" => [to_string(address.hash), "2"], "jsonrpc" => "2.0", "method" => "eth_getBalance"},
+ %{"id" => 2, "params" => [to_string(address.hash), "3"], "jsonrpc" => "2.0", "method" => "eth_getBalance"}
+ ]
+
+ assert response =
+ conn
+ |> put_req_header("content-type", "application/json")
+ |> post("/api/eth_rpc", Jason.encode!(params))
+ |> json_response(200)
+
+ assert [
+ %{"id" => 0, "result" => "0x1"},
+ %{"id" => 1, "result" => "0x2"},
+ %{"id" => 2, "result" => "0x3"}
+ ] = response
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs
index 9dea0841f9..e8c4af7de8 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs
@@ -3,8 +3,8 @@ defmodule BlockScoutWeb.BlockControllerTest do
alias Explorer.Chain.Block
setup do
- Supervisor.terminate_child(Explorer.Supervisor, ConCache)
- Supervisor.restart_child(Explorer.Supervisor, ConCache)
+ Supervisor.terminate_child(Explorer.Supervisor, {ConCache, :blocks})
+ Supervisor.restart_child(Explorer.Supervisor, {ConCache, :blocks})
:ok
end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
index 701a991c55..f902f15ad4 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
@@ -9,8 +9,8 @@ defmodule BlockScoutWeb.ChainControllerTest do
alias Explorer.Counters.AddressesWithBalanceCounter
setup do
- Supervisor.terminate_child(Explorer.Supervisor, ConCache)
- Supervisor.restart_child(Explorer.Supervisor, ConCache)
+ Supervisor.terminate_child(Explorer.Supervisor, {ConCache, :blocks})
+ Supervisor.restart_child(Explorer.Supervisor, {ConCache, :blocks})
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs
index d535b309f8..d632154294 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs
@@ -37,15 +37,6 @@ defmodule BlockScoutWeb.PendingTransactionControllerTest do
refute hd(json_response(conn, 200)["items"]) =~ to_string(dropped_replaced.hash)
end
- test "returns a count of pending transactions", %{conn: conn} do
- insert(:transaction)
-
- conn = get(conn, pending_transaction_path(BlockScoutWeb.Endpoint, :index))
-
- assert html_response(conn, 200)
- assert 1 == conn.assigns.pending_transaction_count
- end
-
test "works when there are no transactions", %{conn: conn} do
conn = get(conn, pending_transaction_path(conn, :index))
diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
index 1add7ed9d5..a32e4f5da3 100644
--- a/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
@@ -10,8 +10,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
alias Explorer.Counters.AddressesWithBalanceCounter
setup do
- Supervisor.terminate_child(Explorer.Supervisor, ConCache)
- Supervisor.restart_child(Explorer.Supervisor, ConCache)
+ Supervisor.terminate_child(Explorer.Supervisor, {ConCache, :blocks})
+ Supervisor.restart_child(Explorer.Supervisor, {ConCache, :blocks})
Enum.map(401..404, &insert(:block, number: &1))
diff --git a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs
index 334ea36baa..abac26beae 100644
--- a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs
@@ -96,15 +96,15 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do
end
end
- describe "sort_contracts_by_version/1" do
- test "sorts contracts in lexicographical order" do
+ describe "last_decompiled_contract_version/1" do
+ test "returns last version" do
contract2 = insert(:decompiled_smart_contract, decompiler_version: "v2")
contract1 = insert(:decompiled_smart_contract, decompiler_version: "v1")
contract3 = insert(:decompiled_smart_contract, decompiler_version: "v3")
- result = AddressDecompiledContractView.sort_contracts_by_version([contract2, contract1, contract3])
+ result = AddressDecompiledContractView.last_decompiled_contract_version([contract2, contract1, contract3])
- assert result == [contract3, contract2, contract1]
+ assert result == contract3
end
end
end
diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
index 24fc593d85..71994da56c 100644
--- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
+++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
@@ -249,6 +249,19 @@ defmodule EthereumJSONRPC do
|> fetch_blocks_by_params(&Block.ByNephew.request/1, json_rpc_named_arguments)
end
+ @spec fetch_net_version(json_rpc_named_arguments) :: {:ok, non_neg_integer()} | {:error, reason :: term}
+ def fetch_net_version(json_rpc_named_arguments) do
+ result =
+ %{id: 0, method: "net_version", params: []}
+ |> request()
+ |> json_rpc(json_rpc_named_arguments)
+
+ case result do
+ {:ok, bin_number} -> {:ok, String.to_integer(bin_number)}
+ other -> other
+ end
+ end
+
@doc """
Fetches block number by `t:tag/0`.
diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex
index 6be34eeee1..1c52a6e6c9 100644
--- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex
+++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex
@@ -319,6 +319,8 @@ defmodule EthereumJSONRPC.Block do
@spec elixir_to_transactions(elixir) :: Transactions.elixir()
def elixir_to_transactions(%{"transactions" => transactions}), do: transactions
+ def elixir_to_transactions(_), do: []
+
@doc """
Get `t:EthereumJSONRPC.Uncles.elixir/0` from `t:elixir/0`.
diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/fetched_beneficiaries.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/fetched_beneficiaries.ex
index 98174be69a..fea1a4c8aa 100644
--- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/fetched_beneficiaries.ex
+++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/parity/fetched_beneficiaries.ex
@@ -174,6 +174,12 @@ defmodule EthereumJSONRPC.Parity.FetchedBeneficiaries do
defp get_address_type(reward_type, index) when reward_type == "external" and index == 2, do: :validator
defp get_address_type(reward_type, index) when reward_type == "external" and index == 3, do: :validator
defp get_address_type(reward_type, index) when reward_type == "external" and index == 4, do: :validator
+ defp get_address_type(reward_type, index) when reward_type == "external" and index == 5, do: :validator
+ defp get_address_type(reward_type, index) when reward_type == "external" and index == 6, do: :validator
+ defp get_address_type(reward_type, index) when reward_type == "external" and index == 7, do: :validator
+ defp get_address_type(reward_type, index) when reward_type == "external" and index == 8, do: :validator
+ defp get_address_type(reward_type, index) when reward_type == "external" and index == 9, do: :validator
+ defp get_address_type(reward_type, index) when reward_type == "external" and index == 10, do: :validator
defp get_address_type(reward_type, _index) when reward_type == "block", do: :validator
defp get_address_type(reward_type, _index) when reward_type == "uncle", do: :uncle
end
diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs
index 1cf16c6f47..23e1e442c5 100644
--- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs
+++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs
@@ -56,4 +56,10 @@ defmodule EthereumJSONRPC.BlockTest do
}
end
end
+
+ describe "elixir_to_transactions/1" do
+ test "converts to empty list if there is not transaction key" do
+ assert Block.elixir_to_transactions(%{}) == []
+ end
+ end
end
diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs
index 5f82b6a43f..3a6bb6f046 100644
--- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs
+++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs
@@ -886,6 +886,24 @@ defmodule EthereumJSONRPCTest do
end
end
+ describe "fetch_net_version/1" do
+ test "fetches net version", %{json_rpc_named_arguments: json_rpc_named_arguments} do
+ expected_version =
+ case Keyword.fetch!(json_rpc_named_arguments, :variant) do
+ EthereumJSONRPC.Parity -> 77
+ _variant -> 1
+ end
+
+ if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do
+ expect(EthereumJSONRPC.Mox, :json_rpc, fn _json, _options ->
+ {:ok, "#{expected_version}"}
+ end)
+ end
+
+ assert {:ok, ^expected_version} = EthereumJSONRPC.fetch_net_version(json_rpc_named_arguments)
+ end
+ end
+
defp clear_mailbox do
receive do
_ -> clear_mailbox()
diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs
index 94a1d79c6d..9a7d9f4296 100644
--- a/apps/explorer/config/config.exs
+++ b/apps/explorer/config/config.exs
@@ -80,7 +80,8 @@ if System.get_env("SOURCE_MODULE") == "TransactionAndLog" do
end
config :explorer,
- solc_bin_api_url: "https://solc-bin.ethereum.org"
+ solc_bin_api_url: "https://solc-bin.ethereum.org",
+ checksum_function: System.get_env("CHECKSUM_FUNCTION") && String.to_atom(System.get_env("CHECKSUM_FUNCTION"))
config :logger, :explorer,
# keep synced with `config/config.exs`
diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex
index 984695b3b4..98a71c7bac 100644
--- a/apps/explorer/lib/explorer/application.ex
+++ b/apps/explorer/lib/explorer/application.ex
@@ -6,7 +6,7 @@ defmodule Explorer.Application do
use Application
alias Explorer.Admin
- alias Explorer.Chain.{BlockCountCache, BlockNumberCache, BlocksCache, TransactionCountCache}
+ alias Explorer.Chain.{BlockCountCache, BlockNumberCache, BlocksCache, NetVersionCache, TransactionCountCache}
alias Explorer.Repo.PrometheusLogger
@impl Application
@@ -31,7 +31,8 @@ defmodule Explorer.Application do
{Admin.Recovery, [[], [name: Admin.Recovery]]},
{TransactionCountCache, [[], []]},
{BlockCountCache, []},
- {ConCache, [name: BlocksCache.cache_name(), ttl_check_interval: false]}
+ con_cache_child_spec(BlocksCache.cache_name()),
+ con_cache_child_spec(NetVersionCache.cache_name())
]
children = base_children ++ configurable_children()
@@ -79,4 +80,17 @@ defmodule Explorer.Application do
http: HTTPoison
]
end
+
+ defp con_cache_child_spec(name) do
+ Supervisor.child_spec(
+ {
+ ConCache,
+ [
+ name: name,
+ ttl_check_interval: false
+ ]
+ },
+ id: {ConCache, name}
+ )
+ end
end
diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex
index 4edc94887a..c40c2a492b 100644
--- a/apps/explorer/lib/explorer/chain.ex
+++ b/apps/explorer/lib/explorer/chain.ex
@@ -227,60 +227,31 @@ defmodule Explorer.Chain do
transaction_hashes_from_token_transfers =
TokenTransfer.where_any_address_fields_match(direction, address_hash, paging_options)
- token_transfers_query =
- transaction_hashes_from_token_transfers
- |> Transaction.where_transaction_hashes_match()
- |> join_associations(necessity_by_association)
- |> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
- |> Transaction.preload_token_transfers(address_hash)
-
- base_query =
+ transactions_list =
paging_options
|> fetch_transactions()
+ |> Transaction.where_transaction_matches(transaction_hashes_from_token_transfers, direction, address_hash)
|> join_associations(necessity_by_association)
|> Transaction.preload_token_transfers(address_hash)
+ |> Repo.all()
- from_address_query =
- base_query
- |> where([t], t.from_address_hash == ^address_hash)
-
- to_address_query =
- base_query
- |> where([t], t.to_address_hash == ^address_hash)
-
- created_contract_query =
- base_query
- |> where([t], t.created_contract_address_hash == ^address_hash)
-
- queries =
- [token_transfers_query] ++
- case direction do
- :from -> [from_address_query]
- :to -> [to_address_query, created_contract_query]
- _ -> [from_address_query, to_address_query, created_contract_query]
+ if Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:has_emission_funds] do
+ address_hash
+ |> Reward.fetch_emission_rewards_tuples(paging_options)
+ |> Enum.concat(transactions_list)
+ |> Enum.sort_by(fn item ->
+ case item do
+ {%Reward{} = emission_reward, _} ->
+ {-emission_reward.block.number, 1}
+
+ item ->
+ {-item.block_number, -item.index}
end
-
- rewards_list =
- if Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:has_emission_funds] do
- Reward.fetch_emission_rewards_tuples(address_hash, paging_options)
- else
- []
- end
-
- queries
- |> Stream.flat_map(&Repo.all/1)
- |> Stream.uniq_by(& &1.hash)
- |> Stream.concat(rewards_list)
- |> Enum.sort_by(fn item ->
- case item do
- {%Reward{} = emission_reward, _} ->
- {-emission_reward.block.number, 1}
-
- item ->
- {-item.block_number, -item.index}
- end
- end)
- |> Enum.take(paging_options.page_size)
+ end)
+ |> Enum.take(paging_options.page_size)
+ else
+ transactions_list
+ end
end
@spec address_to_logs(Address.t(), Keyword.t()) :: [
@@ -703,25 +674,32 @@ defmodule Explorer.Chain do
iex> Explorer.Chain.hash_to_address(hash)
{:error, :not_found}
+ Optionally accepts:
+ - a list of bindings to preload, just like `Ecto.Query.preload/3`
+ - a boolean to also fetch the `has_decompiled_code?` virtual field or not
+
"""
- @spec hash_to_address(Hash.Address.t()) :: {:ok, Address.t()} | {:error, :not_found}
- def hash_to_address(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash) do
- query =
- from(
- address in Address,
- preload: [
+ @spec hash_to_address(Hash.Address.t(), [Macro.t()], boolean()) :: {:ok, Address.t()} | {:error, :not_found}
+ def hash_to_address(
+ %Hash{byte_count: unquote(Hash.Address.byte_count())} = hash,
+ preloads \\ [
:contracts_creation_internal_transaction,
:names,
:smart_contract,
:token,
:contracts_creation_transaction
],
+ query_decompiled_code_flag \\ true
+ ) do
+ query =
+ from(
+ address in Address,
+ preload: ^preloads,
where: address.hash == ^hash
)
- query_with_decompiled_flag = with_decompiled_code_flag(query, hash)
-
- query_with_decompiled_flag
+ query
+ |> with_decompiled_code_flag(hash, query_decompiled_code_flag)
|> Repo.one()
|> case do
nil -> {:error, :not_found}
@@ -824,6 +802,86 @@ defmodule Explorer.Chain do
Repo.all(query)
end
+ @doc """
+ Returns the balance of the given address and block combination.
+
+ Returns `{:error, :not_found}` if there is no address by that hash present.
+ Returns `{:error, :no_balance}` if there is no balance for that address at that block.
+ """
+ @spec get_balance_as_of_block(Hash.Address.t(), integer | :earliest | :latest | :pending) ::
+ {:ok, Wei.t()} | {:error, :no_balance} | {:error, :not_found}
+ def get_balance_as_of_block(address, block) when is_integer(block) do
+ coin_balance_query =
+ from(coin_balance in CoinBalance,
+ where: coin_balance.address_hash == ^address,
+ where: not is_nil(coin_balance.value),
+ where: coin_balance.block_number <= ^block,
+ order_by: [desc: coin_balance.block_number],
+ limit: 1,
+ select: coin_balance.value
+ )
+
+ case Repo.one(coin_balance_query) do
+ nil -> {:error, :not_found}
+ coin_balance -> {:ok, coin_balance}
+ end
+ end
+
+ def get_balance_as_of_block(address, :latest) do
+ case max_consensus_block_number() do
+ {:ok, latest_block_number} ->
+ get_balance_as_of_block(address, latest_block_number)
+
+ {:error, :not_found} ->
+ {:error, :not_found}
+ end
+ end
+
+ def get_balance_as_of_block(address, :earliest) do
+ query =
+ from(coin_balance in CoinBalance,
+ where: coin_balance.address_hash == ^address,
+ where: not is_nil(coin_balance.value),
+ where: coin_balance.block_number == 0,
+ limit: 1,
+ select: coin_balance.value
+ )
+
+ case Repo.one(query) do
+ nil -> {:error, :not_found}
+ coin_balance -> {:ok, coin_balance}
+ end
+ end
+
+ def get_balance_as_of_block(address, :pending) do
+ query =
+ case max_consensus_block_number() do
+ {:ok, latest_block_number} ->
+ from(coin_balance in CoinBalance,
+ where: coin_balance.address_hash == ^address,
+ where: not is_nil(coin_balance.value),
+ where: coin_balance.block_number > ^latest_block_number,
+ order_by: [desc: coin_balance.block_number],
+ limit: 1,
+ select: coin_balance.value
+ )
+
+ {:error, :not_found} ->
+ from(coin_balance in CoinBalance,
+ where: coin_balance.address_hash == ^address,
+ where: not is_nil(coin_balance.value),
+ order_by: [desc: coin_balance.block_number],
+ limit: 1,
+ select: coin_balance.value
+ )
+ end
+
+ case Repo.one(query) do
+ nil -> {:error, :not_found}
+ coin_balance -> {:ok, coin_balance}
+ end
+ end
+
@spec list_ordered_addresses(non_neg_integer(), non_neg_integer()) :: [Address.t()]
def list_ordered_addresses(offset, limit) do
query =
@@ -1907,7 +1965,6 @@ defmodule Explorer.Chain do
|> page_pending_transaction(paging_options)
|> limit(^paging_options.page_size)
|> pending_transactions_query()
- |> where([transaction], is_nil(transaction.error) or transaction.error != "dropped/replaced")
|> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash)
|> join_associations(necessity_by_association)
|> preload([{:token_transfers, [:token, :from_address, :to_address]}])
@@ -1916,7 +1973,7 @@ defmodule Explorer.Chain do
defp pending_transactions_query(query) do
from(transaction in query,
- where: is_nil(transaction.block_hash)
+ where: is_nil(transaction.block_hash) and (is_nil(transaction.error) or transaction.error != "dropped/replaced")
)
end
@@ -3081,7 +3138,11 @@ defmodule Explorer.Chain do
defp staking_pool_filter(query, _), do: query
- defp with_decompiled_code_flag(query, hash) do
+ defp with_decompiled_code_flag(query, hash, use_option \\ true)
+
+ defp with_decompiled_code_flag(query, _hash, false), do: query
+
+ defp with_decompiled_code_flag(query, hash, true) do
has_decompiled_code_query =
from(decompiled_contract in DecompiledSmartContract,
where: decompiled_contract.address_hash == ^hash,
diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex
index a619d70546..28b67f1d66 100644
--- a/apps/explorer/lib/explorer/chain/address.ex
+++ b/apps/explorer/lib/explorer/chain/address.ex
@@ -16,6 +16,7 @@ defmodule Explorer.Chain.Address do
DecompiledSmartContract,
Hash,
InternalTransaction,
+ NetVersionCache,
SmartContract,
Token,
Transaction,
@@ -130,6 +131,21 @@ defmodule Explorer.Chain.Address do
end
def checksum(hash, iodata?) do
+ checksum_formatted =
+ case Application.get_env(:explorer, :checksum_function) || :eth do
+ :eth -> eth_checksum(hash)
+ :rsk -> rsk_checksum(hash)
+ end
+
+ if iodata? do
+ ["0x" | checksum_formatted]
+ else
+ to_string(["0x" | checksum_formatted])
+ end
+ end
+
+ # https://github.com/rsksmart/RSKIPs/blob/master/IPs/RSKIP60.md
+ def eth_checksum(hash) do
string_hash =
hash
|> to_string()
@@ -137,26 +153,46 @@ defmodule Explorer.Chain.Address do
match_byte_stream = stream_every_four_bytes_of_sha256(string_hash)
- checksum_formatted =
- string_hash
- |> stream_binary()
- |> Stream.zip(match_byte_stream)
- |> Enum.map(fn
- {digit, _} when digit in '0123456789' ->
- digit
+ string_hash
+ |> stream_binary()
+ |> Stream.zip(match_byte_stream)
+ |> Enum.map(fn
+ {digit, _} when digit in '0123456789' ->
+ digit
- {alpha, 1} ->
- alpha - 32
+ {alpha, 1} ->
+ alpha - 32
- {alpha, _} ->
- alpha
- end)
+ {alpha, _} ->
+ alpha
+ end)
+ end
- if iodata? do
- ["0x" | checksum_formatted]
- else
- to_string(["0x" | checksum_formatted])
- end
+ def rsk_checksum(hash) do
+ chain_id = NetVersionCache.version()
+
+ string_hash =
+ hash
+ |> to_string()
+ |> String.trim_leading("0x")
+
+ prefix = "#{chain_id}0x"
+
+ match_byte_stream = stream_every_four_bytes_of_sha256("#{prefix}#{string_hash}")
+
+ string_hash
+ |> stream_binary()
+ |> Stream.zip(match_byte_stream)
+ |> Enum.map(fn
+ {digit, _} when digit in '0123456789' ->
+ digit
+
+ {alpha, 1} ->
+ alpha - 32
+
+ {alpha, _} ->
+ alpha
+ end)
end
defp stream_every_four_bytes_of_sha256(value) do
diff --git a/apps/explorer/lib/explorer/chain/net_version_cache.ex b/apps/explorer/lib/explorer/chain/net_version_cache.ex
new file mode 100644
index 0000000000..c3df467e0c
--- /dev/null
+++ b/apps/explorer/lib/explorer/chain/net_version_cache.ex
@@ -0,0 +1,44 @@
+defmodule Explorer.Chain.NetVersionCache do
+ @moduledoc """
+ Caches chain version.
+ """
+
+ @cache_name :net_version
+ @key :version
+
+ @spec version() :: non_neg_integer() | {:error, any()}
+ def version do
+ cached_value = fetch_from_cache()
+
+ if is_nil(cached_value) do
+ fetch_from_node()
+ else
+ cached_value
+ end
+ end
+
+ def cache_name do
+ @cache_name
+ end
+
+ defp fetch_from_cache do
+ ConCache.get(@cache_name, @key)
+ end
+
+ defp cache_value(value) do
+ ConCache.put(@cache_name, @key, value)
+ end
+
+ defp fetch_from_node do
+ json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
+
+ case EthereumJSONRPC.fetch_net_version(json_rpc_named_arguments) do
+ {:ok, value} ->
+ cache_value(value)
+ value
+
+ other ->
+ other
+ end
+ end
+end
diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex
index de99198181..19f04dee2d 100644
--- a/apps/explorer/lib/explorer/chain/transaction.ex
+++ b/apps/explorer/lib/explorer/chain/transaction.ex
@@ -5,7 +5,7 @@ defmodule Explorer.Chain.Transaction do
require Logger
- import Ecto.Query, only: [from: 2, order_by: 3, preload: 3, subquery: 1, where: 3]
+ import Ecto.Query, only: [from: 2, preload: 3, subquery: 1, where: 3]
alias ABI.FunctionSelector
@@ -549,15 +549,40 @@ defmodule Explorer.Chain.Transaction do
end
@doc """
- Builds a query that will check for transactions within the hashes params.
+ Modifies a query to filter for transactions whose hash is in a list or that are
+ linked to the given address_hash through a direction.
Be careful to not pass a large list, because this will lead to performance
problems.
"""
- def where_transaction_hashes_match(transaction_hashes) do
- Transaction
- |> where([t], t.hash == fragment("ANY (?)", ^transaction_hashes))
- |> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
+ def where_transaction_matches(query, transaction_hashes, :from, address_hash) do
+ where(
+ query,
+ [t],
+ t.hash in ^transaction_hashes or
+ t.from_address_hash == ^address_hash
+ )
+ end
+
+ def where_transaction_matches(query, transaction_hashes, :to, address_hash) do
+ where(
+ query,
+ [t],
+ t.hash in ^transaction_hashes or
+ t.to_address_hash == ^address_hash or
+ t.created_contract_address_hash == ^address_hash
+ )
+ end
+
+ def where_transaction_matches(query, transaction_hashes, _direction, address_hash) do
+ where(
+ query,
+ [t],
+ t.hash in ^transaction_hashes or
+ t.from_address_hash == ^address_hash or
+ t.to_address_hash == ^address_hash or
+ t.created_contract_address_hash == ^address_hash
+ )
end
@collated_fields ~w(block_number cumulative_gas_used gas_used index)a
diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex
index 4b12d3deb0..c1c434aa44 100644
--- a/apps/explorer/lib/explorer/chain/wei.ex
+++ b/apps/explorer/lib/explorer/chain/wei.ex
@@ -113,6 +113,17 @@ defmodule Explorer.Chain.Wei do
@wei_per_ether Decimal.new(1_000_000_000_000_000_000)
@wei_per_gwei Decimal.new(1_000_000_000)
+ @spec hex_format(Wei.t()) :: String.t()
+ def hex_format(%Wei{value: decimal}) do
+ hex =
+ decimal
+ |> Decimal.to_integer()
+ |> Integer.to_string(16)
+ |> String.downcase()
+
+ "0x" <> hex
+ end
+
@doc """
Sums two Wei values.
diff --git a/apps/explorer/lib/explorer/counters/average_block_time.ex b/apps/explorer/lib/explorer/counters/average_block_time.ex
index 27a524c22a..6db9caf8f6 100644
--- a/apps/explorer/lib/explorer/counters/average_block_time.ex
+++ b/apps/explorer/lib/explorer/counters/average_block_time.ex
@@ -66,7 +66,7 @@ defmodule Explorer.Counters.AverageBlockTime do
from(block in Block,
limit: 100,
offset: 0,
- order_by: [desc: block.number],
+ order_by: [desc: block.number, desc: block.timestamp],
select: {block.number, block.timestamp}
)
diff --git a/apps/explorer/lib/explorer/exchange_rates/source.ex b/apps/explorer/lib/explorer/exchange_rates/source.ex
index 584f7bf147..41b87c1e29 100644
--- a/apps/explorer/lib/explorer/exchange_rates/source.ex
+++ b/apps/explorer/lib/explorer/exchange_rates/source.ex
@@ -28,7 +28,7 @@ defmodule Explorer.ExchangeRates.Source do
true -> fetch_exchange_rates_from_paginable_source(source, page + 1)
end
- {:ok, %Response{body: body, status_code: status_code}} when status_code in 400..499 ->
+ {:ok, %Response{body: body, status_code: status_code}} when status_code in 400..502 ->
{:error, decode_json(body)["error"]}
{:error, %Error{reason: reason}} ->
@@ -65,6 +65,8 @@ defmodule Explorer.ExchangeRates.Source do
def decode_json(data) do
Jason.decode!(data)
+ rescue
+ _ -> data
end
def to_decimal(nil), do: nil
diff --git a/apps/explorer/priv/repo/migrations/20190613065856_add_tx_hash_inserted_at_index.exs b/apps/explorer/priv/repo/migrations/20190613065856_add_tx_hash_inserted_at_index.exs
new file mode 100644
index 0000000000..4596b1e4d4
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20190613065856_add_tx_hash_inserted_at_index.exs
@@ -0,0 +1,7 @@
+defmodule Explorer.Repo.Migrations.AddTxHashInsertedAtIndex do
+ use Ecto.Migration
+
+ def change do
+ create(index(:transactions, [:hash, :inserted_at]))
+ end
+end
diff --git a/apps/explorer/test/explorer/chain/address_test.exs b/apps/explorer/test/explorer/chain/address_test.exs
index 22710e88bb..213913c766 100644
--- a/apps/explorer/test/explorer/chain/address_test.exs
+++ b/apps/explorer/test/explorer/chain/address_test.exs
@@ -1,6 +1,8 @@
defmodule Explorer.Chain.AddressTest do
use Explorer.DataCase
+ import Mox
+
alias Explorer.Chain.Address
alias Explorer.Repo
@@ -28,6 +30,12 @@ defmodule Explorer.Chain.AddressTest do
end
describe "Phoenix.HTML.Safe.to_iodata/1" do
+ setup do
+ Application.put_env(:explorer, :checksum_function, :eth)
+
+ :ok
+ end
+
defp str(value) do
to_string(insert(:address, hash: value))
end
@@ -39,5 +47,18 @@ defmodule Explorer.Chain.AddressTest do
assert str("0xdbf03b407c01e7cd3cbea99509d93f8dddc8c6fb") == "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB"
assert str("0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb") == "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb"
end
+
+ test "returns the checksum rsk formatted address" do
+ expect(EthereumJSONRPC.Mox, :json_rpc, fn _json, _options ->
+ {:ok, "30"}
+ end)
+
+ Application.put_env(:explorer, :checksum_function, :rsk)
+
+ assert str("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") == "0x5aaEB6053f3e94c9b9a09f33669435E7ef1bEAeD"
+ assert str("0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359") == "0xFb6916095cA1Df60bb79ce92cE3EA74c37c5d359"
+ assert str("0xdbf03b407c01e7cd3cbea99509d93f8dddc8c6fb") == "0xDBF03B407c01E7CD3cBea99509D93F8Dddc8C6FB"
+ assert str("0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb") == "0xD1220A0Cf47c7B9BE7a2e6ba89F429762E7B9adB"
+ end
end
end
diff --git a/apps/explorer/test/explorer/chain/blocks_cache_test.exs b/apps/explorer/test/explorer/chain/blocks_cache_test.exs
index f030502dc3..687d2ff30a 100644
--- a/apps/explorer/test/explorer/chain/blocks_cache_test.exs
+++ b/apps/explorer/test/explorer/chain/blocks_cache_test.exs
@@ -5,8 +5,8 @@ defmodule Explorer.Chain.BlocksCacheTest do
alias Explorer.Repo
setup do
- Supervisor.terminate_child(Explorer.Supervisor, ConCache)
- Supervisor.restart_child(Explorer.Supervisor, ConCache)
+ Supervisor.terminate_child(Explorer.Supervisor, {ConCache, :blocks})
+ Supervisor.restart_child(Explorer.Supervisor, {ConCache, :blocks})
:ok
end
diff --git a/apps/explorer/test/explorer/counters/average_block_time_test.exs b/apps/explorer/test/explorer/counters/average_block_time_test.exs
index 63c47042c7..0f7c328bb3 100644
--- a/apps/explorer/test/explorer/counters/average_block_time_test.exs
+++ b/apps/explorer/test/explorer/counters/average_block_time_test.exs
@@ -3,7 +3,9 @@ defmodule Explorer.Counters.AverageBlockTimeTest do
doctest Explorer.Counters.AverageBlockTimeDurationFormat
+ alias Explorer.Chain.Block
alias Explorer.Counters.AverageBlockTime
+ alias Explorer.Repo
setup do
start_supervised!(AverageBlockTime)
@@ -24,5 +26,37 @@ defmodule Explorer.Counters.AverageBlockTimeTest do
test "without blocks duration is 0" do
assert AverageBlockTime.average_block_time() == Timex.Duration.parse!("PT0S")
end
+
+ test "considers both uncles and consensus blocks" do
+ block_number = 99_999_999
+
+ first_timestamp = Timex.now()
+
+ insert(:block, number: block_number, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 3))
+ insert(:block, number: block_number, consensus: false, timestamp: Timex.shift(first_timestamp, seconds: 9))
+ insert(:block, number: block_number, consensus: false, timestamp: Timex.shift(first_timestamp, seconds: 6))
+
+ assert Repo.aggregate(Block, :count, :hash) == 3
+
+ AverageBlockTime.refresh()
+
+ assert AverageBlockTime.average_block_time() == Timex.Duration.parse!("PT3S")
+ end
+
+ test "when there are no uncles sorts by block number" do
+ block_number = 99_999_999
+
+ first_timestamp = Timex.now()
+
+ insert(:block, number: block_number, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 3))
+ insert(:block, number: block_number + 2, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 9))
+ insert(:block, number: block_number + 1, consensus: true, timestamp: Timex.shift(first_timestamp, seconds: 6))
+
+ assert Repo.aggregate(Block, :count, :hash) == 3
+
+ AverageBlockTime.refresh()
+
+ assert AverageBlockTime.average_block_time() == Timex.Duration.parse!("PT3S")
+ end
end
end
diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex
index ebaf91cf3d..1386a55d36 100644
--- a/apps/explorer/test/support/data_case.ex
+++ b/apps/explorer/test/support/data_case.ex
@@ -40,8 +40,8 @@ defmodule Explorer.DataCase do
end
Explorer.Chain.BlockNumberCache.setup()
- Supervisor.terminate_child(Explorer.Supervisor, ConCache)
- Supervisor.restart_child(Explorer.Supervisor, ConCache)
+ Supervisor.terminate_child(Explorer.Supervisor, {ConCache, :blocks})
+ Supervisor.restart_child(Explorer.Supervisor, {ConCache, :blocks})
:ok
end
diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex
index 8778677f33..8edd87738d 100644
--- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex
+++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex
@@ -140,7 +140,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
%__MODULE__{state | subscription: subscription}
{:error, reason} ->
- Logger.debug(fn -> ["Could not connect to websocket: ", reason, ". Continuing with polling."] end)
+ Logger.debug(fn -> ["Could not connect to websocket: #{inspect(reason)}. Continuing with polling."] end)
state
end
end
@@ -201,6 +201,12 @@ defmodule Indexer.Block.Realtime.Fetcher do
end
end
+ def import(_, _) do
+ Logger.warn("Empty parameters were provided for realtime fetcher")
+
+ {:ok, []}
+ end
+
defp start_fetch_and_import(number, block_fetcher, previous_number, max_number_seen) do
start_at = determine_start_at(number, previous_number, max_number_seen)
diff --git a/apps/indexer/lib/indexer/fetcher/uncle_block.ex b/apps/indexer/lib/indexer/fetcher/uncle_block.ex
index ba36ffb2f9..213604a73e 100644
--- a/apps/indexer/lib/indexer/fetcher/uncle_block.ex
+++ b/apps/indexer/lib/indexer/fetcher/uncle_block.ex
@@ -104,18 +104,18 @@ defmodule Indexer.Fetcher.UncleBlock do
{nephew_hash_bytes, index}
end
- defp run_blocks(%Blocks{blocks_params: []}, _, original_entries), do: {:retry, original_entries}
-
- defp run_blocks(
- %Blocks{
- blocks_params: blocks_params,
- transactions_params: transactions_params,
- block_second_degree_relations_params: block_second_degree_relations_params,
- errors: errors
- },
- block_fetcher,
- original_entries
- ) do
+ def run_blocks(%Blocks{blocks_params: []}, _, original_entries), do: {:retry, original_entries}
+
+ def run_blocks(
+ %Blocks{
+ blocks_params: blocks_params,
+ transactions_params: transactions_params,
+ block_second_degree_relations_params: block_second_degree_relations_params,
+ errors: errors
+ },
+ block_fetcher,
+ original_entries
+ ) do
addresses_params = Addresses.extract_addresses(%{blocks: blocks_params, transactions: transactions_params})
case Block.Fetcher.import(block_fetcher, %{
@@ -235,7 +235,17 @@ defmodule Indexer.Fetcher.UncleBlock do
Enum.map(errors, &error_to_entry/1)
end
- defp error_to_entry(%{data: %{hash: hash}}) when is_binary(hash), do: hash
+ defp error_to_entry(%{data: %{hash: hash, index: index}}) when is_binary(hash) do
+ {:ok, %Hash{bytes: nephew_hash_bytes}} = Hash.Full.cast(hash)
+
+ {nephew_hash_bytes, index}
+ end
+
+ defp error_to_entry(%{data: %{nephew_hash: hash, index: index}}) when is_binary(hash) do
+ {:ok, %Hash{bytes: nephew_hash_bytes}} = Hash.Full.cast(hash)
+
+ {nephew_hash_bytes, index}
+ end
defp errors_to_iodata(errors) when is_list(errors) do
errors_to_iodata(errors, [])
@@ -251,4 +261,9 @@ defmodule Indexer.Fetcher.UncleBlock do
when is_integer(code) and is_binary(message) and is_binary(hash) do
[hash, ": (", to_string(code), ") ", message, ?\n]
end
+
+ defp error_to_iodata(%{code: code, message: message, data: %{nephew_hash: hash}})
+ when is_integer(code) and is_binary(message) and is_binary(hash) do
+ [hash, ": (", to_string(code), ") ", message, ?\n]
+ end
end
diff --git a/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex
index 49b33b7c44..486789f675 100644
--- a/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex
+++ b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex
@@ -13,7 +13,6 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do
import Ecto.Query
- alias Ecto.Multi
alias EthereumJSONRPC.Blocks
alias Explorer.Chain.Block
alias Explorer.Repo
@@ -23,13 +22,14 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do
@defaults [
flush_interval: :timer.seconds(3),
- max_batch_size: 10,
+ max_batch_size: 50,
max_concurrency: 1,
task_supervisor: Indexer.Temporary.BlocksTransactionsMismatch.TaskSupervisor,
metadata: [fetcher: :blocks_transactions_mismatch]
]
@doc false
+ # credo:disable-for-next-line Credo.Check.Design.DuplicatedCode
def child_spec([init_options, gen_server_options]) when is_list(init_options) do
{state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments)
@@ -99,17 +99,26 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do
Map.has_key?(found_blocks_map, to_string(block.hash))
end)
- {:ok, _} =
- found_blocks_data
- |> Enum.reduce(Multi.new(), fn {block, trans_num}, multi ->
- changes = %{
- refetch_needed: false,
- consensus: found_blocks_map[to_string(block.hash)] == trans_num
- }
-
- Multi.update(multi, block.hash, Block.changeset(block, changes))
+ {matching_blocks_data, unmatching_blocks_data} =
+ Enum.split_with(found_blocks_data, fn {block, trans_num} ->
+ found_blocks_map[to_string(block.hash)] == trans_num
end)
- |> Repo.transaction()
+
+ unless Enum.empty?(matching_blocks_data) do
+ hashes = Enum.map(matching_blocks_data, fn {block, _trans_num} -> block.hash end)
+
+ Block
+ |> where([block], block.hash in ^hashes)
+ |> Repo.update_all(set: [refetch_needed: false])
+ end
+
+ unless Enum.empty?(unmatching_blocks_data) do
+ hashes = Enum.map(unmatching_blocks_data, fn {block, _trans_num} -> block.hash end)
+
+ Block
+ |> where([block], block.hash in ^hashes)
+ |> Repo.update_all(set: [refetch_needed: false, consensus: false])
+ end
if Enum.empty?(missing_blocks_data) do
:ok
diff --git a/apps/indexer/test/indexer/fetcher/uncle_block_test.exs b/apps/indexer/test/indexer/fetcher/uncle_block_test.exs
index 224349d781..f3350b8de7 100644
--- a/apps/indexer/test/indexer/fetcher/uncle_block_test.exs
+++ b/apps/indexer/test/indexer/fetcher/uncle_block_test.exs
@@ -6,7 +6,9 @@ defmodule Indexer.Fetcher.UncleBlockTest do
import EthereumJSONRPC, only: [integer_to_quantity: 1]
+ alias EthereumJSONRPC.Blocks
alias Explorer.Chain
+ alias Explorer.Chain.Hash
alias Indexer.Block
alias Indexer.Fetcher.UncleBlock
@@ -138,6 +140,74 @@ defmodule Indexer.Fetcher.UncleBlockTest do
end
end
+ describe "run/2" do
+ test "retries failed request", %{json_rpc_named_arguments: json_rpc_named_arguments} do
+ %Hash{bytes: block_hash_bytes} = block_hash()
+ entries = [{block_hash_bytes, 0}]
+
+ EthereumJSONRPC.Mox
+ |> expect(:json_rpc, fn [
+ %{
+ id: id,
+ method: "eth_getUncleByBlockHashAndIndex"
+ }
+ ],
+ _ ->
+ {:ok,
+ [
+ %{
+ id: id,
+ error: %{
+ code: 404,
+ data: %{index: 0, nephew_hash: "0xa0814f0478fe90c82852f812fd74c96df148654c326d2600d836e6908ebb62b4"},
+ message: "Not Found"
+ }
+ }
+ ]}
+ end)
+
+ assert {:retry, ^entries} =
+ UncleBlock.run(entries, %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments})
+ end
+ end
+
+ describe "run_blocks/2" do
+ test "converts errors to entries for retry", %{json_rpc_named_arguments: json_rpc_named_arguments} do
+ miner_hash =
+ address_hash()
+ |> to_string()
+
+ block_number = 1
+
+ index = 0
+
+ hash = "0xa0814f0478fe90c82852f812fd74c96df148654c326d2600d836e6908ebb62b4"
+
+ params = %Blocks{
+ errors: [
+ %{
+ code: 404,
+ data: %{index: index, nephew_hash: hash},
+ message: "Not Found"
+ }
+ ],
+ blocks_params: [%{miner_hash: miner_hash, number: block_number}]
+ }
+
+ assert {:retry, [{bin_hash, ^index}]} =
+ UncleBlock.run_blocks(
+ params,
+ %Block.Fetcher{
+ json_rpc_named_arguments: json_rpc_named_arguments,
+ callback_module: Indexer.Block.Realtime.Fetcher
+ },
+ []
+ )
+
+ assert Hash.Full.cast(bin_hash) == Hash.Full.cast(hash)
+ end
+ end
+
defp wait(producer) do
producer.()
rescue
diff --git a/bin/install_chrome_headless.sh b/bin/install_chrome_headless.sh
index 9721e84ea3..d27bb6f9d8 100755
--- a/bin/install_chrome_headless.sh
+++ b/bin/install_chrome_headless.sh
@@ -1,6 +1,6 @@
export DISPLAY=:99.0
sh -e /etc/init.d/xvfb start
-export CHROMEDRIVER_VERSION=`curl -s http://chromedriver.storage.googleapis.com/LATEST_RELEASE`
+export CHROMEDRIVER_VERSION=74.0.3729.6
curl -L -O "http://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip"
unzip chromedriver_linux64.zip
sudo chmod +x chromedriver
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 7694358e72..73501a9b9d 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -33,7 +33,7 @@ RUN cd apps/block_scout_web/assets/ && \
RUN cd apps/explorer/ && \
npm install && \
- apk del --force-broken-world alpine-sdk gmp-dev automake libtool inotify-tools autoconf python
+ apk update && apk del --force-broken-world alpine-sdk gmp-dev automake libtool inotify-tools autoconf python
# RUN mix do ecto.drop --force, ecto.create, ecto.migrate