Merge branch 'master' into master

pull/594/head
Mulili Nzuki 6 years ago committed by GitHub
commit 1e59dcd371
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      apps/block_scout_web/assets/css/_code.scss
  2. 10
      apps/block_scout_web/assets/css/_elements.scss
  3. 1
      apps/block_scout_web/assets/css/app.scss
  4. 2
      apps/block_scout_web/assets/css/components/_button.scss
  5. 6
      apps/block_scout_web/assets/css/components/_dropdown.scss
  6. 63
      apps/block_scout_web/assets/css/components/_navbar.scss
  7. 9
      apps/block_scout_web/assets/css/components/_tile.scss
  8. 6
      apps/block_scout_web/assets/css/theme/_base_variables.scss
  9. 6
      apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss
  10. 4
      apps/block_scout_web/assets/css/theme/_ethereum_variables.scss
  11. 6
      apps/block_scout_web/assets/css/theme/_kovan_variables.scss
  12. 2
      apps/block_scout_web/assets/css/theme/_poa_variables.scss
  13. 4
      apps/block_scout_web/assets/css/theme/_ropsten_variables.scss
  14. 4
      apps/block_scout_web/assets/css/theme/_sokol_variables.scss
  15. 7
      apps/block_scout_web/assets/js/pages/address.js
  16. 4
      apps/block_scout_web/assets/js/pages/block.js
  17. 4
      apps/block_scout_web/assets/js/pages/transaction.js
  18. 40
      apps/block_scout_web/assets/js/utils.js
  19. 3
      apps/block_scout_web/assets/static/images/icons/accounts.svg
  20. 3
      apps/block_scout_web/assets/static/images/icons/api.svg
  21. 3
      apps/block_scout_web/assets/static/images/icons/blocks.svg
  22. 3
      apps/block_scout_web/assets/static/images/icons/network.svg
  23. 3
      apps/block_scout_web/assets/static/images/icons/search.svg
  24. 3
      apps/block_scout_web/assets/static/images/icons/test-network.svg
  25. 3
      apps/block_scout_web/assets/static/images/icons/transactions.svg
  26. 2
      apps/block_scout_web/config/test.exs
  27. 6
      apps/block_scout_web/lib/block_scout_web/chain.ex
  28. 10
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  29. 13
      apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex
  30. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex
  31. 47
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex
  32. 21
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/stats_controller.ex
  33. 23
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex
  34. 4
      apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
  35. 180
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  36. 10
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  37. 21
      apps/block_scout_web/lib/block_scout_web/router.ex
  38. 56
      apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex
  39. 29
      apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex
  40. 80
      apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex
  41. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex
  42. 33
      apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex
  43. 30
      apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex
  44. 44
      apps/block_scout_web/lib/block_scout_web/templates/address_read_contract/index.html.eex
  45. 15
      apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex
  46. 35
      apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex
  47. 148
      apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex
  48. 15
      apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex
  49. 6
      apps/block_scout_web/lib/block_scout_web/templates/block/_tile.html.eex
  50. 2
      apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex
  51. 6
      apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex
  52. 2
      apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex
  53. 3
      apps/block_scout_web/lib/block_scout_web/templates/icons/_accounts_icon.html.eex
  54. 3
      apps/block_scout_web/lib/block_scout_web/templates/icons/_api_icon.html.eex
  55. 4
      apps/block_scout_web/lib/block_scout_web/templates/icons/_block_icon.html.eex
  56. 3
      apps/block_scout_web/lib/block_scout_web/templates/icons/_network_icon.html.eex
  57. 3
      apps/block_scout_web/lib/block_scout_web/templates/icons/_search_icon.html.eex
  58. 3
      apps/block_scout_web/lib/block_scout_web/templates/icons/_test_network_icon.html.eex
  59. 4
      apps/block_scout_web/lib/block_scout_web/templates/icons/_transaction_icon.html.eex
  60. 37
      apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex
  61. 33
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  62. 4
      apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex
  63. 6
      apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex
  64. 10
      apps/block_scout_web/lib/block_scout_web/templates/tokens/read_contract/index.html.eex
  65. 24
      apps/block_scout_web/lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex
  66. 8
      apps/block_scout_web/lib/block_scout_web/templates/tokens/token/show.html.eex
  67. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
  68. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex
  69. 20
      apps/block_scout_web/lib/block_scout_web/templates/transaction_internal_transaction/_internal_transaction.html.eex
  70. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex
  71. 6
      apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex
  72. 6
      apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/index.html.eex
  73. 2
      apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
  74. 5
      apps/block_scout_web/lib/block_scout_web/views/address_token_transfer_view.ex
  75. 2
      apps/block_scout_web/lib/block_scout_web/views/address_token_view.ex
  76. 71
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  77. 19
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/stats_view.ex
  78. 25
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex
  79. 14
      apps/block_scout_web/lib/block_scout_web/views/internal_transaction_view.ex
  80. 9
      apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
  81. 16
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  82. 2
      apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs
  83. 13
      apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs
  84. 42
      apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs
  85. 114
      apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs
  86. 83
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs
  87. 189
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs
  88. 6
      apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs
  89. 30
      apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex
  90. 2
      apps/block_scout_web/test/block_scout_web/features/pages/contract_verify_page.ex
  91. 70
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  92. 6
      apps/block_scout_web/test/block_scout_web/views/address_token_view_test.exs
  93. 107
      apps/block_scout_web/test/block_scout_web/views/address_view_test.exs
  94. 62
      apps/block_scout_web/test/block_scout_web/views/internal_transaction_view_test.exs
  95. 34
      apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
  96. 8
      apps/block_scout_web/test/support/fake_adapter.ex
  97. 114
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex
  98. 3
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/receipts_test.exs
  99. 192
      apps/explorer/lib/explorer/chain.ex
  100. 106
      apps/explorer/lib/explorer/chain/address/token.ex
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,9 +1,13 @@
.pre-wrap {
white-space: normal;
pre {
white-space: pre-wrap;
}
pre code {
font-size: 13px;
}
code {
word-break: break-all;
}
.pre-wrap code {
white-space: pre-wrap;
}
.pre-scrollable-shorty {

@ -3,3 +3,13 @@ hr {
border-style: solid;
border-color: $gray-300 transparent transparent transparent;
}
svg {
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
}

@ -79,6 +79,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "components/token-balance-dropdown";
@import "components/address-overview";
@import "components/token_tile_view_more";
@import "components/dropdown";
:export {
primary: $primary;

@ -17,6 +17,7 @@
&:hover,
&:focus {
color: $white;
background-color: darken($primary, 10%);
border-color: darken($primary, 10%);
text-decoration: none;
@ -32,6 +33,7 @@
&-secondary {
border: 1px solid $tertiary;
color: $tertiary;
background-color: $white;
font-weight: 400;
&:hover,

@ -0,0 +1,6 @@
// These styles extend the default Bootstrap styles
.dropdown-menu {
width: 100%;
box-shadow: $box-shadow;
}

@ -8,17 +8,17 @@
}
.navbar-brand {
margin-left: 10px;
margin-left: 0;
}
.navbar-logo {
height: 36px;
height: 1.5em;
}
@include media-breakpoint-up(md) {
.navbar-expand-lg .navbar-nav .nav-link {
padding-left: 1.25rem;
padding-right: 1.25rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
}
@ -26,10 +26,29 @@
font-size: 14px;
}
.navbar .nav-link {
.navbar-dark .navbar-nav .nav-link {
color: $white;
}
.navbar-nav .nav-link {
display: flex;
align-items: center;
}
.nav-link-icon {
display: flex;
align-items: center;
position: relative;
top: -2px;
height: 1em;
width: 1em;
margin-right: 0.5em;
path {
fill: $secondary;
}
}
.navbar .form-control {
background: transparent;
width: auto;
@ -70,10 +89,10 @@
.input-group-append {
margin-left: 0px;
}
.fa-search {
color: $white;
path {
fill: $secondary;
}
}
.topnav-nav-link {
@ -84,32 +103,26 @@
content: "";
position: absolute;
display: block;
bottom: 0;
bottom: -10px;
left: 50%;
width: 100%;
width: 75%;
height: 0.2rem;
background-color: $white;
background-color: $secondary;
border-radius: 4px 4px 0 0;
opacity: 0;
transform: translateX(-50%) translateY(-0.5rem);
transition: all 0.3s ease;
transform: translateX(-50%);
transition: all 0.2s ease;
}
&:hover {
color: $white;
&:before {
transform: translateX(-50%) translateY(-0.25rem);
&:before{
opacity: 1;
}
}
}
&:active {
&:before {
transition: all 0.1s ease-out;
transform: translateX(-50%) translateY(-0.5rem);
}
}
.dropdown-toggle {
padding-right: 0 !important;
}
.dropdown-menu {
@ -120,7 +133,7 @@
padding: 10px 20px;
&:hover {
background-color: $secondary;
background-color: $tertiary;
color: $white;
}
}

@ -3,7 +3,6 @@
color: $text-muted;
line-height: 1.4rem;
border: 1px solid $border-color;
border-left: 4px solid $primary;
border-radius: 2px;
padding: 1rem;
box-shadow: 0 1px 5px rgba($black, 0.15);
@ -18,6 +17,10 @@
&-type {
&-block {
border-left: 4px solid $primary;
}
&-transaction {
border-left: 4px solid $blue;
@ -83,7 +86,7 @@
}
&-status {
&--failed {
&--error--reason {
border-top: 1px solid lighten($danger, 10%);
border-right: 1px solid lighten($danger, 10%);
border-bottom: 1px solid lighten($danger, 10%);
@ -92,7 +95,7 @@
color: $danger;
}
}
&--out_of_gas {
&--awaiting-internal-transactions {
border-top: 1px solid lighten($warning, 10%);
border-right: 1px solid lighten($warning, 10%);
border-bottom: 1px solid lighten($warning, 10%);

@ -30,8 +30,8 @@ $grays: map-merge((
), $grays);
$blue: #4786ff !default;
$indigo: #5b33a1 !default;
$purple: #997fdc !default;
$indigo: #5b389f !default;
$purple: #997fdc !default;
$pink: #e83e8c !default;
$red: #c74d39 !default;
$orange: #ef9a60 !default;
@ -58,7 +58,7 @@ $colors: map-merge((
), $colors);
$primary: $indigo !default;
$secondary: $teal !default;
$secondary: #7dd79f !default;
$tertiary: $purple !default;
$success: $green !default;
$info: $cyan !default;

@ -1,3 +1,3 @@
$primary: #1b1b3a;
$secondary: #40ed9e;
$tertiary: #40ed9e;
$primary: #1b1b39;
$secondary: #4beba0;
$tertiary: #4beba0;

@ -1,3 +1,3 @@
$primary: #12455b;
$secondary: #4786cb;
$primary: #16465b;
$secondary: #5ab3ff;
$tertiary: #77a4c5;

@ -1,3 +1,3 @@
$primary: #1BACA4;
$secondary: #6435c9;
$tertiary: #6435c9;
$primary: #28aca4;
$secondary: #89edda;
$tertiary: $purple;

@ -1,3 +1,3 @@
$primary: $indigo;
$secondary: $teal;
$secondary: #7dd79f;
$tertiary: $purple;

@ -1,3 +1,3 @@
$primary: #24a7fb;
$secondary: #f4c500;
$primary: #2fa8f8;
$secondary: #a2daff;
$tertiary: #006aa7;

@ -1,3 +1,3 @@
$primary: #539387;
$secondary: #77a4c5;
$primary: #559387;
$secondary: #add7cf;
$tertiary: #38533d;

@ -3,7 +3,7 @@ import URI from 'urijs'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { batchChannel, initRedux } from '../utils'
import { batchChannel, initRedux, prependWithClingBottom } from '../utils'
import { updateAllAges } from '../lib/from_now'
import { updateAllCalculatedUsdValues } from '../lib/currency.js'
import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown'
@ -152,10 +152,11 @@ if ($addressDetailsPage.length) {
$channelBatching.hide()
}
if (oldState.newInternalTransactions !== state.newInternalTransactions && $internalTransactionsList.length) {
$internalTransactionsList.prepend(state.newInternalTransactions.slice(oldState.newInternalTransactions.length).reverse().join(''))
prependWithClingBottom($internalTransactionsList, state.newInternalTransactions.slice(oldState.newInternalTransactions.length).reverse().join(''))
updateAllAges()
}
if (oldState.newTransactions !== state.newTransactions && $transactionsList.length) {
$transactionsList.prepend(state.newTransactions.slice(oldState.newTransactions.length).reverse().join(''))
prependWithClingBottom($transactionsList, state.newTransactions.slice(oldState.newTransactions.length).reverse().join(''))
updateAllAges()
}
}

@ -3,7 +3,7 @@ import URI from 'urijs'
import humps from 'humps'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { initRedux } from '../utils'
import { initRedux, prependWithClingBottom } from '../utils'
export const initialState = {
beyondPageOne: null,
@ -58,7 +58,7 @@ if ($blockListPage.length) {
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.newBlock !== state.newBlock) {
$blocksList.prepend(state.newBlock)
prependWithClingBottom($blocksList, state.newBlock)
updateAllAges()
}
}

@ -4,7 +4,7 @@ import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { batchChannel, initRedux } from '../utils'
import { batchChannel, initRedux, prependWithClingBottom } from '../utils'
const BATCH_THRESHOLD = 10
@ -125,7 +125,7 @@ if ($transactionListPage.length) {
.children()
.slice($transactionsList.children().length - newTransactionsToInsert.length, $transactionsList.children().length)
.remove()
$transactionsList.prepend(newTransactionsToInsert.reverse().join(''))
prependWithClingBottom($transactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}

@ -1,3 +1,4 @@
import $ from 'jquery'
import _ from 'lodash'
import { createStore } from 'redux'
@ -32,3 +33,42 @@ export function initRedux (reducer, { main, render, debug } = {}) {
}
if (main) main(store)
}
export function prependWithClingBottom ($el, content) {
function userAtTop () {
return window.scrollY < $('[data-selector="navbar"]').outerHeight()
}
if (userAtTop()) return $el.prepend(content)
let isAnimating
function setIsAnimating () {
isAnimating = true
}
$el.on('animationstart', setIsAnimating)
let startingScrollPosition = window.scrollY
let endingScrollPosition = window.scrollY
function userIsScrolling () {
return window.scrollY < startingScrollPosition || endingScrollPosition < window.scrollY
}
const clingDistanceFromBottom = document.body.scrollHeight - window.scrollY
let clingBottomLoop = window.requestAnimationFrame(function clingBottom () {
if (userIsScrolling()) return stopClinging()
startingScrollPosition = window.scrollY
endingScrollPosition = document.body.scrollHeight - clingDistanceFromBottom
$(window).scrollTop(endingScrollPosition)
clingBottomLoop = window.requestAnimationFrame(clingBottom)
})
function stopClinging () {
window.cancelAnimationFrame(clingBottomLoop)
$el.off('animationstart', setIsAnimating)
$el.off('animationend animationcancel', stopClinging)
}
$el.on('animationend animationcancel', stopClinging)
setTimeout(() => !isAnimating && stopClinging(), 100)
return $el.prepend(content)
}

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill="#7DD79F" fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-4a3 3 0 0 1 3-3h1V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v5h1a3 3 0 0 1 3 3v4a1 1 0 0 1-1 1zM10 4a2 2 0 1 0-4 0v3a2 2 0 1 0 4 0V4zm4 8c0-1.105-1.075-2-2.4-2h-.379c-.549.61-1.336 1-2.221 1H7c-.885 0-1.672-.39-2.221-1H4.4c-1.325 0-2.4.895-2.4 2v2h12v-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill="#7DD79F" fill-rule="evenodd" d="M15 9h-1.277c-.347.595-.985 1-1.723 1a1.994 1.994 0 0 1-1.723-1H1a1 1 0 0 1 0-2h9.277c.347-.595.985-1 1.723-1 .738 0 1.376.405 1.723 1H15a1 1 0 0 1 0 2zm0-6H5.723C5.376 3.595 4.738 4 4 4a1.994 1.994 0 0 1-1.723-1H1a1 1 0 0 1 0-2h1.277C2.624.405 3.262 0 4 0c.738 0 1.376.405 1.723 1H15a1 1 0 0 1 0 2zM1 13h1.277c.347-.595.985-1 1.723-1 .738 0 1.376.405 1.723 1H15a1 1 0 0 1 0 2H5.723c-.347.595-.985 1-1.723 1a1.994 1.994 0 0 1-1.723-1H1a1 1 0 0 1 0-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 593 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 14 16" xmlns="http://www.w3.org/2000/svg" width="14" height="16">
<path fill="#7DD79F" fill-rule="evenodd" d="M14 11c0 .026-.013.049-.015.074.004.382-.159.757-.481.962l-5.897 3.743-.004.002-.099.063a.983.983 0 0 1-.299.115c-.01.002-.019.009-.03.011-.013.002-.027.001-.04.003-.046.006-.087.027-.135.027-.048 0-.089-.021-.135-.027-.013-.002-.027-.001-.04-.003-.011-.002-.02-.009-.03-.011a.983.983 0 0 1-.299-.115l-.099-.063-.004-.002-5.897-3.743c-.322-.205-.485-.58-.481-.962C.013 11.049 0 11.026 0 11V5a.98.98 0 0 1 .085-.398c.066-.256.184-.494.411-.638L6.393.221l.004-.002.114-.073c.022-.013.046-.013.068-.025A.924.924 0 0 1 6.825.03.86.86 0 0 1 7 .017a.86.86 0 0 1 .175.013c.087.017.166.05.246.091.022.012.046.012.068.025l.114.073.004.002 5.897 3.743c.227.144.345.382.411.638A.98.98 0 0 1 14 5v6zm-6 2.01l4-2.539V6.99L8 9.529v3.481zm-6-2.539l4 2.539V9.529L2 6.99v3.481zm5-8.116L2.834 5 7 7.645 11.166 5 7 2.355z"/>
</svg>

After

Width:  |  Height:  |  Size: 945 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill="#7DD79F" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm5.906-7h-1.951c-.098 1.559-.408 2.985-.888 4.131A5.996 5.996 0 0 0 13.906 9zM8 14.013c.988 0 1.803-2.171 1.961-5.013H6.039c.158 2.842.973 5.013 1.961 5.013zm-3.067-.882c-.48-1.146-.79-2.572-.888-4.131H2.094a5.996 5.996 0 0 0 2.839 4.131zM2.094 7h1.951c.098-1.559.408-2.985.888-4.131A5.996 5.996 0 0 0 2.094 7zM8 1.987c-.988 0-1.803 2.171-1.961 5.013h3.922C9.803 4.158 8.988 1.987 8 1.987zm3.067.882c.48 1.146.79 2.572.888 4.131h1.951a5.996 5.996 0 0 0-2.839-4.131z"/>
</svg>

After

Width:  |  Height:  |  Size: 643 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 17" xmlns="http://www.w3.org/2000/svg" width="16" height="17">
<path fill="#7DD79F" fill-rule="evenodd" d="M15.713 15.727a.982.982 0 0 1-1.388 0l-2.289-2.29C10.773 14.403 9.213 15 7.5 15A7.5 7.5 0 1 1 15 7.5c0 1.719-.602 3.284-1.575 4.55l2.288 2.288a.983.983 0 0 1 0 1.389zM7.5 2a5.5 5.5 0 1 0 0 11 5.5 5.5 0 1 0 0-11z"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill="#89EDDA" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zM8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm2 5H9v4a1 1 0 0 1-2 0V7H6a1 1 0 0 1 0-2h4a1 1 0 0 1 0 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 269 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 14" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path fill="#7DD79F" fill-rule="evenodd" d="M15 8H7a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2zm0-6H7a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2zM3 14H1a1 1 0 0 1 0-2h2a1 1 0 0 1 0 2zm0-6H1a1 1 0 0 1 0-2h2a1 1 0 0 1 0 2zm0-6H1a1 1 0 0 1 0-2h2a1 1 0 0 1 0 2zm4 10h8a1 1 0 0 1 0 2H7a1 1 0 0 1 0-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 368 B

@ -15,5 +15,3 @@ config :logger, :block_scout_web,
# Configure wallaby
config :wallaby, screenshot_on_failure: true
config :block_scout_web, :fake_adapter, BlockScoutWeb.FakeAdapter

@ -16,10 +16,8 @@ defmodule BlockScoutWeb.Chain do
Address,
Address.TokenBalance,
Block,
Hash,
InternalTransaction,
Log,
Token,
TokenTransfer,
Transaction
}
@ -185,7 +183,7 @@ defmodule BlockScoutWeb.Chain do
%{"inserted_at" => inserted_at_datetime}
end
defp paging_params(%Token{name: name, type: type, inserted_at: inserted_at}) do
defp paging_params(%Address.Token{name: name, type: type, inserted_at: inserted_at}) do
inserted_at_datetime =
inserted_at
|> DateTime.from_naive!("Etc/UTC")
@ -195,7 +193,7 @@ defmodule BlockScoutWeb.Chain do
end
defp paging_params(%TokenBalance{address_hash: address_hash, value: value}) do
%{"address_hash" => Hash.to_string(address_hash), "value" => Decimal.to_integer(value)}
%{"address_hash" => to_string(address_hash), "value" => Decimal.to_integer(value)}
end
defp transaction_from_param(param) do

@ -4,7 +4,7 @@ defmodule BlockScoutWeb.AddressChannel do
"""
use BlockScoutWeb, :channel
alias BlockScoutWeb.{AddressInternalTransactionView, AddressView, TransactionView}
alias BlockScoutWeb.{InternalTransactionView, AddressView, TransactionView}
alias Phoenix.View
intercept(["balance_update", "count", "internal_transaction", "transaction"])
@ -45,9 +45,9 @@ defmodule BlockScoutWeb.AddressChannel do
rendered_internal_transaction =
View.render_to_string(
AddressInternalTransactionView,
"_internal_transaction.html",
address: address,
InternalTransactionView,
"_tile.html",
current_address: address,
internal_transaction: internal_transaction
)
@ -67,7 +67,7 @@ defmodule BlockScoutWeb.AddressChannel do
View.render_to_string(
TransactionView,
"_tile.html",
address: address,
current_address: address,
transaction: transaction
)

@ -1,14 +1,23 @@
defmodule BlockScoutWeb.AddressController do
use BlockScoutWeb, :controller
alias Explorer.Chain
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token
def index(conn, _params) do
render(conn, "index.html",
addresses: Chain.list_top_addresses(),
address_estimated_count: Chain.address_estimated_count(),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
)
end
def show(conn, %{"id" => id}) do
redirect(conn, to: address_transaction_path(conn, :index, id))
end
def transaction_count(%Address{} = address) do
Chain.address_to_transaction_count(address)
Chain.address_to_transactions_estimated_count(address)
end
end

@ -10,7 +10,7 @@ defmodule BlockScoutWeb.AddressTokenController do
def index(conn, %{"address_id" => address_hash_string} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do
tokens_plus_one = Chain.tokens_with_number_of_transfers_from_address(address_hash, paging_options(params))
tokens_plus_one = Chain.address_tokens_with_balance(address_hash, paging_options(params))
{tokens, next_page} = split_list_by_page(tokens_plus_one)
render(

@ -0,0 +1,47 @@
defmodule BlockScoutWeb.AddressTokenTransferController do
use BlockScoutWeb, :controller
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
import BlockScoutWeb.AddressController, only: [transaction_count: 1]
import BlockScoutWeb.Chain,
only: [next_page_params: 3, paging_options: 1, split_list_by_page: 1]
def index(
conn,
%{"address_id" => address_hash_string, "address_token_id" => token_hash_string} = params
) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token_hash} <- Chain.string_to_address_hash(token_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash),
{:ok, token} <- Chain.token_from_address_hash(token_hash) do
transactions =
Chain.address_to_transactions_with_token_tranfers(
address_hash,
token_hash,
paging_options(params)
)
{transactions_paginated, next_page} = split_list_by_page(transactions)
render(
conn,
"index.html",
address: address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
next_page_params: next_page_params(next_page, transactions_paginated, params),
token: token,
transaction_count: transaction_count(address),
transactions: transactions_paginated
)
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
end

@ -1,7 +1,8 @@
defmodule BlockScoutWeb.API.RPC.StatsController do
use BlockScoutWeb, :controller
alias Explorer.Chain
alias Explorer.{Chain, ExchangeRates}
alias Explorer.Chain.Wei
def tokensupply(conn, params) do
with {:contractaddress_param, {:ok, contractaddress_param}} <- fetch_contractaddress(params),
@ -20,6 +21,24 @@ defmodule BlockScoutWeb.API.RPC.StatsController do
end
end
def ethsupply(conn, _params) do
wei_total_supply =
Chain.total_supply()
|> Decimal.new()
|> Wei.from(:ether)
|> Wei.to(:wei)
|> Decimal.to_string()
render(conn, "ethsupply.json", total_supply: wei_total_supply)
end
def ethprice(conn, _params) do
symbol = Application.get_env(:explorer, :coin)
rates = ExchangeRates.lookup(symbol)
render(conn, "ethprice.json", rates: rates)
end
defp fetch_contractaddress(params) do
{:contractaddress_param, Map.fetch(params, "contractaddress")}
end

@ -17,6 +17,20 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do
end
end
def getstatus(conn, params) do
with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params),
{:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param) do
error = to_transaction_error(transaction_hash)
render(conn, :getstatus, %{error: error})
else
{:txhash_param, :error} ->
render(conn, :error, error: "Query parameter txhash is required")
{:format, :error} ->
render(conn, :error, error: "Invalid txhash format")
end
end
defp fetch_txhash(params) do
{:txhash_param, Map.fetch(params, "txhash")}
end
@ -31,4 +45,13 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do
{:ok, transaction} -> transaction.status
end
end
defp to_transaction_error(transaction_hash) do
with {:ok, transaction} <- Chain.hash_to_transaction(transaction_hash),
{:error, error} <- Chain.transaction_to_status(transaction) do
error
else
_ -> ""
end
end
end

@ -7,8 +7,6 @@ defmodule BlockScoutWeb.ChainController do
alias Explorer.Market
def show(conn, _params) do
address_count_module = Application.get_env(:block_scout_web, :fake_adapter) || Chain
blocks =
[paging_options: %PagingOptions{page_size: 4}]
|> Chain.list_blocks()
@ -38,7 +36,7 @@ defmodule BlockScoutWeb.ChainController do
render(
conn,
"show.html",
address_estimated_count: address_count_module.address_estimated_count(),
address_estimated_count: Chain.address_estimated_count(),
average_block_time: Chain.average_block_time(),
blocks: blocks,
exchange_rate: exchange_rate,

@ -215,6 +215,23 @@ defmodule BlockScoutWeb.Etherscan do
"result" => "21265524714464"
}
@stats_ethsupply_example_value %{
"status" => "1",
"message" => "OK",
"result" => "101959776311500000000000000"
}
@stats_ethprice_example_value %{
"status" => "1",
"message" => "OK",
"result" => %{
"ethbtc" => "0.03246",
"ethbtc_timestamp" => "1537212510",
"ethusd" => "204",
"ethusd_timestamp" => "1537212513"
}
}
@block_getblockreward_example_value %{
"status" => "1",
"message" => "OK",
@ -304,6 +321,21 @@ defmodule BlockScoutWeb.Etherscan do
"result" => nil
}
@transaction_getstatus_example_value %{
"status" => "1",
"message" => "OK",
"result" => %{
"isError" => "1",
"errDescription" => "Out of gas"
}
}
@transaction_getstatus_example_value_error %{
"status" => "0",
"message" => "Query parameter txhash is required",
"result" => nil
}
@status_type %{
type: "status",
enum: ~s(["0", "1"]),
@ -674,6 +706,47 @@ defmodule BlockScoutWeb.Etherscan do
}
}
@transaction_status_model %{
name: "TransactionStatus",
fields: %{
isError: %{
type: "isError",
enum: ~s(["0", "1"]),
enum_interpretation: %{"0" => "pass", "1" => "error"}
},
errDescription: %{
type: "string",
example: ~s("Out of gas")
}
}
}
@eth_price_model %{
name: "EthPrice",
fields: %{
ethbtc: %{
type: "ethbtc",
definition: &__MODULE__.ethbtc_type_definition/1,
example: ~s("0.03161")
},
ethbtc_timestamp: %{
type: "timestamp",
definition: "Last updated timestamp.",
example: ~s("1537234460")
},
ethusd: %{
type: "ethusd",
definition: &__MODULE__.ethusd_type_definition/1,
example: ~s("197.57")
},
ethusd_timestamp: %{
type: "timestamp",
definition: "Last updated timestamp.",
example: ~s("1537234460")
}
}
}
@account_balance_action %{
name: "balance",
description: "Get balance for address",
@ -1219,6 +1292,57 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@stats_ethsupply_action %{
name: "ethsupply",
description: "Get total supply in Wei.",
required_params: [],
optional_params: [],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@stats_ethsupply_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "integer",
description: "The total supply.",
example: ~s("101959776311500000000000000")
}
}
}
}
]
}
@stats_ethprice_action %{
name: "ethprice",
description: "Get latest price in USD and BTC.",
required_params: [],
optional_params: [],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@stats_ethprice_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "model",
model: @eth_price_model
}
}
}
}
]
}
@block_getblockreward_action %{
name: "getblockreward",
description: "Get block reward by block number.",
@ -1367,6 +1491,43 @@ defmodule BlockScoutWeb.Etherscan do
]
}
@transaction_getstatus_action %{
name: "getstatus",
description: "Get error status and error message.",
required_params: [
%{
key: "txhash",
placeholder: "transactionHash",
type: "string",
description: "Transaction hash. Hash of contents of the transaction."
}
],
optional_params: [],
responses: [
%{
code: "200",
description: "successful operation",
example_value: Jason.encode!(@transaction_getstatus_example_value),
model: %{
name: "Result",
fields: %{
status: @status_type,
message: @message_type,
result: %{
type: "model",
model: @transaction_status_model
}
}
}
},
%{
code: "200",
description: "error",
example_value: Jason.encode!(@transaction_getstatus_example_value_error)
}
]
}
@account_module %{
name: "account",
actions: [
@ -1392,7 +1553,11 @@ defmodule BlockScoutWeb.Etherscan do
@stats_module %{
name: "stats",
actions: [@stats_tokensupply_action]
actions: [
@stats_tokensupply_action,
@stats_ethsupply_action,
@stats_ethprice_action
]
}
@block_module %{
@ -1410,7 +1575,10 @@ defmodule BlockScoutWeb.Etherscan do
@transaction_module %{
name: "transaction",
actions: [@transaction_gettxreceiptstatus_action]
actions: [
@transaction_gettxreceiptstatus_action,
@transaction_getstatus_action
]
}
@documentation [
@ -1432,4 +1600,12 @@ defmodule BlockScoutWeb.Etherscan do
"and thus the one in which all integer values of the currency are counted, is the Wei. " <>
"One #{coin} is defined as being 10<sup>18</sup> Wei."
end
def ethbtc_type_definition(coin) do
"#{coin} price in Bitcoin."
end
def ethusd_type_definition(coin) do
"#{coin} price in US dollars."
end
end

@ -9,8 +9,7 @@ defmodule BlockScoutWeb.Notifier do
alias BlockScoutWeb.Endpoint
def handle_event({:chain_event, :addresses, addresses}) do
address_count_module = Application.get_env(:block_scout_web, :fake_adapter) || Chain
Endpoint.broadcast("addresses:new_address", "count", %{count: address_count_module.address_estimated_count()})
Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.address_estimated_count()})
addresses
|> Stream.reject(fn %Address{fetched_coin_balance: fetched_coin_balance} -> is_nil(fetched_coin_balance) end)
@ -38,7 +37,11 @@ defmodule BlockScoutWeb.Notifier do
def handle_event({:chain_event, :internal_transactions, internal_transactions}) do
internal_transactions
|> Stream.map(&(InternalTransaction |> Repo.get(&1.id) |> Repo.preload([:from_address, :to_address])))
|> Stream.map(
&(InternalTransaction
|> Repo.get(&1.id)
|> Repo.preload([:from_address, :to_address, transaction: :block]))
)
|> Enum.each(&broadcast_internal_transaction/1)
end
@ -47,6 +50,7 @@ defmodule BlockScoutWeb.Notifier do
|> Chain.hashes_to_transactions(
necessity_by_association: %{
:block => :required,
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
:token_transfers => :optional

@ -58,9 +58,14 @@ defmodule BlockScoutWeb.Router do
resources("/logs", TransactionLogController, only: [:index], as: :log)
resources("/token_transfers", TransactionTokenTransferController, only: [:index], as: :token_transfer)
resources("/token_transfers", TransactionTokenTransferController,
only: [:index],
as: :token_transfer
)
end
resources("/accounts", AddressController, only: [:index])
resources "/address", AddressController, only: [:show] do
resources("/transactions", AddressTransactionController, only: [:index], as: :transaction)
@ -92,12 +97,14 @@ defmodule BlockScoutWeb.Router do
as: :read_contract
)
resources(
"/tokens",
AddressTokenController,
only: [:index],
as: :token
)
resources("/tokens", AddressTokenController, only: [:index], as: :token) do
resources(
"/token_transfers",
AddressTokenTransferController,
only: [:index],
as: :transfers
)
end
resources(
"/token_balances",

@ -0,0 +1,56 @@
<section class="container">
<div class="card">
<div class="card-body">
<h1><%= gettext "Addresses" %></h1>
<p>
<%= gettext "Showing 250 addresses of" %>
<%= Cldr.Number.to_string!(@address_estimated_count, format: "#,###") %>
<%= gettext "total addresses with a balance" %>
</p>
<span data-selector="top-addresses-list">
<%= for {address, index} <- Enum.with_index(@addresses, 1) do %>
<div class="tile">
<div class="row">
<!-- rank -->
<div class="col-2 col-md-1 d-flex justify-content-center align-items-center">
<!-- incremented number by order in the list -->
<span><%= index %></span>
</div>
<div class="col-10 col-md-11">
<div class="row">
<div class="col-md-7 d-flex flex-column mt-3 mt-md-0">
<%= address |> BlockScoutWeb.AddressView.address_partial_selector(nil, nil) |> BlockScoutWeb.AddressView.render_partial() %>
<!-- number of txns for this address -->
<span>
<span data-test="transaction_count"><%= transaction_count(address) %></span>
<%= gettext "Transactions" %>
</span>
</div>
<!-- balance and percentage -->
<div class="col-md-5 d-flex flex-column text-md-right mt-3 mt-md-0">
<!-- address coin balance -->
<span class="tile-title" data-test="address_balance"><%= balance(address) %></span>
<div class="d-flex flex-column flex-md-row justify-content-md-end">
<!-- USD value of the balance -->
<span
data-wei-value="<%= if address.fetched_coin_balance, do: address.fetched_coin_balance.value %>"
data-usd-exchange-rate="<%= @exchange_rate.usd_value %>">
</span>
<!-- percentage of coins from total supply -->
<span class="ml-0 ml-md-2">(<%= balance_percentage(address) %>)</span>
</div>
</div>
</div>
</div>
</div>
</div>
<% end %>
</span>
</div>
</div>
</section>

@ -17,24 +17,23 @@
</div>
<h1 class="card-title"><%= address_title(@address) %> <%= gettext "Details" %> </h1>
<h3 class="<%= if BlockScoutWeb.AddressView.contract?(@address) do %>contract-address<% end %>" data-test="address_detail_hash"><%= @address.hash %></h3>
<div class="d-flex flex-row flex-md-column justify-content-start text-muted">
<div class="d-flex flex-row justify-content-start text-muted mr-4 mb-md-2">
<span class="mr-4 mb-md-2">
<%= if address_name = primary_name(@address) do %>
<strong class="mr-4 mb-md-2 text-primary"><%= address_name %></strong>
<% end %>
<span data-selector="transaction-count"><%= Cldr.Number.to_string!(@transaction_count, format: "#,###") %></span> <%= gettext "Transactions" %>
</span>
<div class="d-flex flex-column flex-lg-row justify-content-start text-muted">
<%= if address_name = primary_name(@address) do %>
<strong class="mr-4 mb-2 text-primary"><%= address_name %></strong>
<% end %>
<span class="mr-4 mb-2">
<span data-selector="transaction-count"><%= Cldr.Number.to_string!(@transaction_count, format: "#,###") %></span>
<%= if @address.token do %>
<span class="mb-md-2">
<%= link(token_title(@address.token), to: token_path(@conn, :show, @address.hash), "data-test": "token_hash_link" ) %>
</span>
<% end %>
</div>
<%= gettext "Transactions" %>
</span>
<%= if @address.token do %>
<span class="mr-4 mb-2">
<%= link(token_title(@address.token), to: token_path(@conn, :show, @address.hash), "data-test": "token_hash_link" ) %>
</span>
<% end %>
<%= if contract?(@address) do %>
<span class="mr-4" data-test="address_contract_creator">
<span data-test="address_contract_creator">
<%= gettext "Contract created by" %>
<%= link(
trimmed_hash(@address.contracts_creation_internal_transaction.from_address_hash),

@ -4,7 +4,9 @@
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<!-- DESKTOP NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Transactions"),
@ -47,12 +49,58 @@
</li>
<% end %>
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
<%= gettext("Code") %>
<%= if smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
</a>
<div class="dropdown-menu">
<%= link(
gettext("Transactions"),
class: "dropdown-item",
to: address_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Tokens"),
class: "dropdown-item",
to: address_token_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item active") do %>
<%= gettext("Code") %>
<%= if smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
<%= if smart_contract_with_read_only_functions?(@address) do %>
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "dropdown-item")%>
<% end %>
</div>
</li>
</ul>
</div>
<div class="card-body">
<%= if !smart_contract_verified?(@address) do %>
<%= link(
gettext("Verify and Publish"),
gettext("Verify & Publish"),
to: address_verify_contract_path(@conn, :new, @address.hash),
class: "button button-primary button-sm float-right ml-3",
"data-test": "verify_and_publish"
@ -78,11 +126,9 @@
<section>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Contract source code" %></h3>
<span class="icon-links" data-clipboard-text="<%= @address.smart_contract.contract_source_code %>">
<button type="button" class="button button-secondary button-sm" id="button" data-toggle="tooltip" data-placement="top">
<%= gettext "Copy Contract Source Code" %>
</button>
</span>
<button type="button" class="button button-secondary button-sm" id="button" data-toggle="tooltip" data-placement="top" data-clipboard-text="<%= @address.smart_contract.contract_source_code %>" aria-label="Copy Contract Source Code">
<%= gettext "Copy Code" %>
</button>
</div>
<div class="tile tile-muted mb-4">
<pre class="pre-scrollable">
@ -96,16 +142,14 @@
<section>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Contract ABI" %></h3>
<span class="icon-links" data-clipboard-text="<%= format_smart_contract_abi(@address.smart_contract.abi) %>">
<button type="button" class="button button-secondary button-sm" id="button">
<%= gettext "Copy Contract ABI" %>
</button>
</span>
<button type="button" class="button button-secondary button-sm" id="button" data-clipboard-text="<%= format_smart_contract_abi(@address.smart_contract.abi) %>" aria-label="Copy Contract ABI">
<%= gettext "Copy Code" %>
</button>
</div>
<div class="tile tile-muted mb-4">
<pre class="pre-scrollable">
<pre class="pre-wrap pre-scrollable">
<code>
<p><%= format_smart_contract_abi(@address.smart_contract.abi) %></p>
<%= format_smart_contract_abi(@address.smart_contract.abi) %>
</code>
</pre>
</div>
@ -116,11 +160,9 @@
<section>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Contract creation code" %></h3>
<span class="icon-links" data-clipboard-text="<%= @address.contract_code %>">
<button type="button" class="button button-secondary button-sm" id="button">
<%= gettext "Copy Contract Creation Code" %>
</button>
</span>
<button type="button" class="button button-secondary button-sm" id="button" data-clipboard-text="<%= @address.contract_code %>" aria-label="copy contract creation code">
<%= gettext "Copy Code" %>
</button>
</div>
<div class="tile tile-muted">
<pre class="pre-wrap pre-scrollable">

@ -56,7 +56,7 @@
class="d-none px-4 position-absolute button button-primary button-sm mr-2">
<i class="fa fa-spinner fa-spin"></i> <%= gettext("loading.....") %>
</button>
<%= submit gettext("Verify and publish"), class: "button button-primary button-sm mr-2", "data-loading": "animation" %>
<%= submit gettext("Verify & publish"), class: "button button-primary button-sm mr-2", "data-loading": "animation" %>
<%= reset gettext("Reset"), class: "button button-secondary button-sm mr-2" %>
<%= link(
gettext("Cancel"),

@ -1,33 +0,0 @@
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-internal-transaction-id="<%= @internal_transaction.id %>">
<div class="row">
<div class="col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center tile-label mb-1 mb-md-0 pl-md-4">
<%= gettext("Internal Transaction") %>
</div>
<div class="col-md-8 col-lg-8 d-flex flex-column text-nowrap pr-2 pr-sm-2 pr-md-0">
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %>
<span class="text-nowrap">
<%= if @address.hash == @internal_transaction.from_address_hash do %>
<%= render BlockScoutWeb.AddressView, "_responsive_hash.html", address: @internal_transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.from_address) %>
<% else %>
<%= render BlockScoutWeb.AddressView, "_link.html", address: @internal_transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.from_address) %>
<% end %>
&rarr;
<%= if @address.hash == BlockScoutWeb.InternalTransactionView.to_address_hash(@internal_transaction) do %>
<%= render BlockScoutWeb.AddressView, "_responsive_hash.html", address: BlockScoutWeb.InternalTransactionView.to_address(@internal_transaction), contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.to_address) %>
<% else %>
<%= render BlockScoutWeb.AddressView, "_link.html", address: BlockScoutWeb.InternalTransactionView.to_address(@internal_transaction), contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.to_address) %>
<% end %>
</span>
<span class="tile-title text-truncate mt-3 mt-md-0">
<%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %> <%= gettext "Ether" %>
</span>
</div>
<div class="col-md-2 d-flex flex-row flex-md-column justify-content-start align-items-end mt-3 mt-md-0">
<%= if @address.hash == @internal_transaction.from_address_hash do %>
<span class="badge badge-danger tile-badge"><%= gettext "OUT" %></span>
<% else %>
<span class="badge badge-success tile-badge"><%= gettext "IN" %></span>
<% end %>
</div>
</div>
</div>

@ -7,7 +7,7 @@
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Transactions"),
@ -54,9 +54,11 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Internal Transactions") %></a>
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
<%= gettext("Internal Transactions") %>
</a>
<div class="dropdown-menu">
<%= link(
gettext("Transactions"),
@ -70,21 +72,23 @@
) %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
class: "dropdown-item active",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
<%= if contract?(@address) do %>
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item") do %>
<%= gettext("Code") %>
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item") do %>
<%= gettext("Code") %>
<%= if smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<%= if smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "dropdown-item")%>
</div>
</li>
</ul>
@ -140,7 +144,7 @@
<%= if Enum.count(@internal_transactions) > 0 do %>
<span data-selector="internal-transactions-list">
<%= for internal_transaction <- @internal_transactions do %>
<%= render "_internal_transaction.html", address: @address, internal_transaction: internal_transaction %>
<%= render BlockScoutWeb.InternalTransactionView, "_tile.html", current_address: @address, internal_transaction: internal_transaction %>
<% end %>
</span>
<% else %>

@ -4,7 +4,9 @@
<div class="card">
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<!-- DESKTOP NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Transactions"),
@ -45,6 +47,46 @@
class: "nav-link active")%>
</li>
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
<%= gettext("Read Contract") %>
</a>
<div class="dropdown-menu">
<%= link(
gettext("Transactions"),
class: "dropdown-item",
to: address_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Tokens"),
class: "dropdown-item",
to: address_token_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item") do %>
<%= gettext("Code") %>
<%= if smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "dropdown-item active")%>
</div>
</li>
</ul>
</div>
<!-- loaded via AJAX -->

@ -1,10 +1,19 @@
<div class="tile tile-type-token">
<div class="row justify-content">
<div class="col-md-12 d-flex flex-column tile-label">
<%= link(to: token_path(@conn, :show, @token.contract_address_hash), class: "tile-title-lg") do %>
<div class="row justify-content align-items-center">
<div class="col-md-7 d-flex flex-column mt-3 mt-md-0">
<%= link(
to: address_token_transfers_path(@conn, :index, @address.hash, @token.contract_address_hash),
class: "tile-title-lg",
"data-test": "token_transfers_#{@token.contract_address_hash}"
) do %>
<%= token_name(@token) %>
<% end %>
<span><%= @token.type %> - <%= number_of_transfers(@token) %></span>
</div>
<div class="col-md-5 d-flex flex-column text-md-right mt-3 mt-md-0">
<span class="tile-title-lg text-md-right align-bottom">
<%= format_according_to_decimals(@token.balance, @token.decimals) %> <%= @token.symbol %>
</span>
</div>
</div>
</div>

@ -5,7 +5,7 @@
<div class="card">
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Transactions"),
@ -63,10 +63,11 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext "Tokens" %></a>
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
<%= gettext("Tokens") %>
</a>
<div class="dropdown-menu">
<%= link(
gettext("Transactions"),
@ -75,7 +76,7 @@
) %>
<%= link(
gettext("Tokens"),
class: "dropdown-item",
class: "dropdown-item active",
to: address_token_path(@conn, :index, @address.hash)
) %>
<%= link(
@ -84,17 +85,21 @@
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
<%= if AddressView.contract?(@address) do %>
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item") do %>
<%= gettext("Code") %>
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item") do %>
<%= gettext("Code") %>
<%= if AddressView.smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<%= if AddressView.smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
<%= if AddressView.smart_contract_with_read_only_functions?(@address) do %>
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "dropdown-item")%>
<% end %>
</div>
</li>
</ul>
@ -104,7 +109,7 @@
<h2 class="card-title"><%= gettext "Tokens" %></h2>
<%= if Enum.any?(@tokens) do %>
<%= for token <- @tokens do %>
<%= render "_tokens.html", conn: @conn, token: token %>
<%= render "_tokens.html", conn: @conn, token: token, address: @address %>
<% end %>
<% else %>
<div class="tile tile-muted text-center">
@ -116,7 +121,7 @@
<%= if @next_page_params do %>
<%= link(
gettext("Next"),
class: "button button-secondary button-sm float-right",
class: "button button-secondary button-sm float-right mt-3",
to: address_token_path(
@conn,
:index,

@ -0,0 +1,148 @@
<section class="container">
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<section>
<div class="card">
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Transactions"),
class: "nav-link",
to: address_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Tokens"),
class: "nav-link active",
to: address_token_path(@conn, :index, @address.hash)
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Internal Transactions"),
class: "nav-link",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
</li>
<%= if AddressView.contract?(@address) do %>
<li class="nav-item">
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "nav-link") do %>
<%= gettext("Code") %>
<%= if AddressView.smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
</li>
<% end %>
<%= if AddressView.smart_contract_with_read_only_functions?(@address) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "nav-link")%>
</li>
<% end %>
<%= if AddressView.smart_contract_with_read_only_functions?(@address) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "nav-link")%>
</li>
<% end %>
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
<%= gettext("Tokens") %>
</a>
<div class="dropdown-menu">
<%= link(
gettext("Transactions"),
class: "dropdown-item",
to: address_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Tokens"),
class: "dropdown-item active",
to: address_token_path(@conn, :index, @address.hash)
) %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
"data-test": "internal_transactions_tab_link",
to: address_internal_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
to: address_contract_path(@conn, :index, @address.hash),
class: "dropdown-item") do %>
<%= gettext("Code") %>
<%= if AddressView.smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
<%= if AddressView.smart_contract_with_read_only_functions?(@address) do %>
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "dropdown-item"
)%>
<% end %>
</div>
</li>
</ul>
</div>
<div class="card-body">
<h2 class="card-title">
<span class="text-muted"><%= gettext "Tokens" %></span> / <%= token_name(@token) %>
</h2>
<%= if Enum.any?(@transactions) do %>
<span data-selector="transactions-list">
<%= for transaction <- @transactions do %>
<%= render(
BlockScoutWeb.TransactionView,
"_tile.html",
transaction: transaction,
current_address: @address
) %>
<% end %>
</span>
<% else %>
<div class="tile tile-muted text-center">
<span><%= gettext "There are no token transfers for this address." %></span>
</div>
<% end %>
<%= if @next_page_params do %>
<%= link(
gettext("Next"),
class: "button button-secondary button-sm float-right mt-3",
to: address_token_transfers_path(
@conn,
:index,
@address.hash,
@token.contract_address_hash,
@next_page_params
)
) %>
<% end %>
</div>
</div>
</section>
</section>

@ -7,7 +7,7 @@
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Transactions"),
@ -19,7 +19,8 @@
<%= link(
gettext("Tokens"),
class: "nav-link",
to: address_token_path(@conn, :index, @address.hash)
to: address_token_path(@conn, :index, @address.hash),
"data-test": "tokens_tab_link"
) %>
</li>
<li class="nav-item">
@ -54,13 +55,13 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext "Transactions" %></a>
<div class="dropdown-menu">
<%= link(
gettext("Transactions"),
class: "dropdown-item",
class: "dropdown-item active",
to: address_transaction_path(@conn, :index, @address.hash)
) %>
<%= link(
@ -85,6 +86,12 @@
<% end %>
<% end %>
<% end %>
<%= if smart_contract_with_read_only_functions?(@address) do %>
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "dropdown-item")%>
<% end %>
</div>
</li>
</ul>

@ -1,4 +1,4 @@
<div class="tile fade-up">
<div class="tile tile-type-block fade-up">
<div class="row">
<div class="col-md-8 col-lg-9">
<!-- block height -->
@ -35,12 +35,12 @@
<!-- Gas Used -->
<div class="mr-3 mr-md-0">
<%= formatted_gas(@block.gas_used) %>
(<%= formatted_gas(@block.gas_used / @block.gas_limit, format: "#.#%") %>)
(<%= formatted_gas(Decimal.to_integer(@block.gas_used) / Decimal.to_integer(@block.gas_limit), format: "#.#%") %>)
<%= gettext "Gas Used" %>
</div>
<!-- Progress bar -->
<div class="progress w-100 mt-3 mt-md-0">
<div class="progress-bar" role="progressbar" style="width: <%= formatted_gas(@block.gas_used / @block.gas_limit, format: "#.#%") %>;" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" role="progressbar" style="width: <%= formatted_gas(Decimal.to_integer(@block.gas_used) / Decimal.to_integer(@block.gas_limit), format: "#.#%") %>;" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100">
</div>
</div>
</div>

@ -86,7 +86,7 @@
<!-- Gas Used -->
<h3>
<%= @block.gas_used |> Cldr.Number.to_string! %>
<span class="text-muted"> (<%= (@block.gas_used / @block.gas_limit) |> Cldr.Number.to_string!(format: "#.#%") %>) </span>
<span class="text-muted"> (<%= (Decimal.to_integer(@block.gas_used) / Decimal.to_integer(@block.gas_limit)) |> Cldr.Number.to_string!(format: "#.#%") %>) </span>
</h3>
<!-- Gas Limit -->
<span class="text-muted"> <%= @block.gas_limit |> Cldr.Number.to_string! %> <%= gettext "Gas Limit" %> </span>

@ -7,7 +7,7 @@
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Transactions"),
@ -18,13 +18,13 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext "Transactions" %></a>
<div class="dropdown-menu">
<%= link(
gettext("Transactions"),
class: "dropdown-item",
class: "dropdown-item active",
to: block_transaction_path(@conn, :index, @conn.params["block_id"])
) %>
</div>

@ -1,5 +1,5 @@
<div class="col-sm-3 fade-up-blocks-chain mb-3 mb-sm-0" data-selector="chain-block" data-block-number="<%= @block.number %>">
<div class="tile d-flex flex-column">
<div class="tile tile-type-block d-flex flex-column">
<%= link(@block, to: block_path(BlockScoutWeb.Endpoint, :show, @block), class: "tile-title") %>
<div>
<span class="mr-2"> <%= Enum.count(@block.transactions) %> Transactions </span>

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill="#7DD79F" fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1v-4a3 3 0 0 1 3-3h1V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v5h1a3 3 0 0 1 3 3v4a1 1 0 0 1-1 1zM10 4a2 2 0 1 0-4 0v3a2 2 0 1 0 4 0V4zm4 8c0-1.105-1.075-2-2.4-2h-.379c-.549.61-1.336 1-2.221 1H7c-.885 0-1.672-.39-2.221-1H4.4c-1.325 0-2.4.895-2.4 2v2h12v-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 410 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill="#7DD79F" fill-rule="evenodd" d="M15 9h-1.277c-.347.595-.985 1-1.723 1a1.994 1.994 0 0 1-1.723-1H1a1 1 0 0 1 0-2h9.277c.347-.595.985-1 1.723-1 .738 0 1.376.405 1.723 1H15a1 1 0 0 1 0 2zm0-6H5.723C5.376 3.595 4.738 4 4 4a1.994 1.994 0 0 1-1.723-1H1a1 1 0 0 1 0-2h1.277C2.624.405 3.262 0 4 0c.738 0 1.376.405 1.723 1H15a1 1 0 0 1 0 2zM1 13h1.277c.347-.595.985-1 1.723-1 .738 0 1.376.405 1.723 1H15a1 1 0 0 1 0 2H5.723c-.347.595-.985 1-1.723 1a1.994 1.994 0 0 1-1.723-1H1a1 1 0 0 1 0-2z"/>
</svg>

After

Width:  |  Height:  |  Size: 593 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="24">
<path fill="#5d6165" fill-rule="evenodd" d="M21.998 17.936c0 .006-.004.011-.004.017.009.185-.013.374-.105.547a.892.892 0 0 1-.816.485c-.019.002-.036.011-.055.011l-9.467 4.887a1.11 1.11 0 0 1-.519.111A.113.113 0 0 1 11 24c-.011 0-.021-.006-.032-.006-.052 0-.104-.009-.156-.014-.018-.004-.038-.001-.056-.005a.984.984 0 0 1-.308-.092l-9.425-4.888c-.008 0-.015.005-.023.005-.03 0-.055-.013-.084-.015a.895.895 0 0 1-.805-.485 1.038 1.038 0 0 1-.105-.547c-.002-.01-.006-.019-.006-.03v-.046c.001-.031-.005-.061 0-.092V6.188c-.005-.031.001-.061 0-.092v-.019l.002-.013c0-.043.004-.086.01-.128a.788.788 0 0 1 .114-.459.98.98 0 0 1 .407-.336.928.928 0 0 1 .18-.079c.038-.01.074-.017.112-.024.053-.01.103-.03.157-.034L10.448.117c.156-.081.323-.103.49-.105C10.96.011 10.978 0 11 0s.04.011.062.012c.167.002.334.024.489.104l9.537 4.902c.179.021.343.09.483.199a.613.613 0 0 1 .058.042c.042.037.08.073.116.116.042.044.095.078.129.131.072.111.098.23.109.349A.826.826 0 0 1 22 6.16v11.652c.005.031-.001.062 0 .093v.018l-.002.013zM2 16.159l1.633-1.025c.44-.276 1.002-.112 1.256.366s.103 1.09-.337 1.366l-1.565.983L10 21.486v-8.645l-1.633 1.025c-.44.276-1.002.112-1.256-.366s-.103-1.09.337-1.366l1.573-.988L2 7.522v8.637zM12 2.492V4a1 1 0 0 1-2 0V2.503L3.226 6 10 9.497V8a1 1 0 0 1 2 0v1.508L18.825 6 12 2.492zm8 5.05l-7.017 3.606 1.569.986c.44.276.591.888.337 1.366-.254.478-.816.642-1.256.366L12 12.841v8.656l7.037-3.633-1.589-.998c-.44-.276-.591-.888-.337-1.366.254-.478.816-.642 1.256-.366L20 16.159V7.542z"/>
<svg viewBox="0 0 14 16" xmlns="http://www.w3.org/2000/svg" width="14" height="16">
<path fill="#7DD79F" fill-rule="evenodd" d="M14 11c0 .026-.013.049-.015.074.004.382-.159.757-.481.962l-5.897 3.743-.004.002-.099.063a.983.983 0 0 1-.299.115c-.01.002-.019.009-.03.011-.013.002-.027.001-.04.003-.046.006-.087.027-.135.027-.048 0-.089-.021-.135-.027-.013-.002-.027-.001-.04-.003-.011-.002-.02-.009-.03-.011a.983.983 0 0 1-.299-.115l-.099-.063-.004-.002-5.897-3.743c-.322-.205-.485-.58-.481-.962C.013 11.049 0 11.026 0 11V5a.98.98 0 0 1 .085-.398c.066-.256.184-.494.411-.638L6.393.221l.004-.002.114-.073c.022-.013.046-.013.068-.025A.924.924 0 0 1 6.825.03.86.86 0 0 1 7 .017a.86.86 0 0 1 .175.013c.087.017.166.05.246.091.022.012.046.012.068.025l.114.073.004.002 5.897 3.743c.227.144.345.382.411.638A.98.98 0 0 1 14 5v6zm-6 2.01l4-2.539V6.99L8 9.529v3.481zm-6-2.539l4 2.539V9.529L2 6.99v3.481zm5-8.116L2.834 5 7 7.645 11.166 5 7 2.355z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 945 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill="#7DD79F" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm5.906-7h-1.951c-.098 1.559-.408 2.985-.888 4.131A5.996 5.996 0 0 0 13.906 9zM8 14.013c.988 0 1.803-2.171 1.961-5.013H6.039c.158 2.842.973 5.013 1.961 5.013zm-3.067-.882c-.48-1.146-.79-2.572-.888-4.131H2.094a5.996 5.996 0 0 0 2.839 4.131zM2.094 7h1.951c.098-1.559.408-2.985.888-4.131A5.996 5.996 0 0 0 2.094 7zM8 1.987c-.988 0-1.803 2.171-1.961 5.013h3.922C9.803 4.158 8.988 1.987 8 1.987zm3.067.882c.48 1.146.79 2.572.888 4.131h1.951a5.996 5.996 0 0 0-2.839-4.131z"/>
</svg>

After

Width:  |  Height:  |  Size: 643 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 17" xmlns="http://www.w3.org/2000/svg" width="16" height="17">
<path fill="#7DD79F" fill-rule="evenodd" d="M15.713 15.727a.982.982 0 0 1-1.388 0l-2.289-2.29C10.773 14.403 9.213 15 7.5 15A7.5 7.5 0 1 1 15 7.5c0 1.719-.602 3.284-1.575 4.55l2.288 2.288a.983.983 0 0 1 0 1.389zM7.5 2a5.5 5.5 0 1 0 0 11 5.5 5.5 0 1 0 0-11z"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" width="16" height="16">
<path fill="#89EDDA" fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zM8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm2 5H9v4a1 1 0 0 1-2 0V7H6a1 1 0 0 1 0-2h4a1 1 0 0 1 0 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 269 B

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
<path fill="#BAC4CB" fill-rule="evenodd" d="M23 19H12.427l3.284 3.284a1.01 1.01 0 0 1-1.427 1.427l-4.995-4.995A1.01 1.01 0 0 1 8.994 18c0-.259.098-.519.295-.716l4.995-4.995a1.01 1.01 0 0 1 1.427 1.427L12.427 17H23a1 1 0 0 1 0 2zM14.711 6.716l-4.995 4.995a1.01 1.01 0 0 1-1.427-1.427L11.573 7H1a1 1 0 0 1 0-2h10.573L8.289 1.716A1.01 1.01 0 0 1 9.716.289l4.995 4.995c.197.197.295.457.295.716 0 .259-.098.519-.295.716z"/>
<svg viewBox="0 0 16 14" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
<path fill="#7DD79F" fill-rule="evenodd" d="M15 8H7a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2zm0-6H7a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2zM3 14H1a1 1 0 0 1 0-2h2a1 1 0 0 1 0 2zm0-6H1a1 1 0 0 1 0-2h2a1 1 0 0 1 0 2zm0-6H1a1 1 0 0 1 0-2h2a1 1 0 0 1 0 2zm4 10h8a1 1 0 0 1 0 2H7a1 1 0 0 1 0-2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 368 B

@ -0,0 +1,37 @@
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-internal-transaction-id="<%= @internal_transaction.id %>">
<div class="row">
<div class="col-md-2 d-flex flex-row flex-md-column align-items-left justify-content-start justify-content-lg-center mb-1 mb-md-0 pl-md-4">
<%= gettext("Internal Transaction") %>
</div>
<div class="col-md-7 col-lg-8 d-flex flex-column text-nowrap pr-2 pr-sm-2 pr-md-0">
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %>
<span class="text-nowrap">
<%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %>
&rarr;
<%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %>
</span>
<span class="tile-title text-truncate mt-3 mt-md-0">
<%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %>
<%= gettext "Ether" %>
</span>
</div>
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-start text-md-right mt-3 mt-md-0">
<span class="mr-2 mr-md-0 order-1">
<%= link(
gettext("Block #%{number}", number: to_string(@internal_transaction.transaction.block_number)),
to: block_path(BlockScoutWeb.Endpoint, :show, @internal_transaction.transaction.block)
) %>
</span>
<span class="mr-2 mr-md-0 order-2" data-from-now="<%= @internal_transaction.transaction.block.timestamp %>"></span>
<%= if assigns[:current_address] do %>
<span class="mr-2 mr-md-0 order-0 order-md-3">
<%= if assigns[:current_address].hash == @internal_transaction.from_address_hash do %>
<span class="badge badge-danger tile-badge"><%= gettext "OUT" %></span>
<% else %>
<span class="badge badge-success tile-badge"><%= gettext "IN" %></span>
<% end %>
</span>
<% end %>
</div>
</div>
</div>

@ -1,4 +1,4 @@
<nav class="navbar navbar-dark navbar-expand-lg navbar-primary">
<nav class="navbar navbar-dark navbar-expand-lg navbar-primary" data-selector="navbar">
<div class="container">
<%= link to: chain_path(@conn, :show), class: "navbar-brand", "data-test": "header_logo" do %>
<img class="navbar-logo" src="<%= Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:logo] %>" />
@ -10,16 +10,33 @@
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<%= link to: block_path(@conn, :index), class: "nav-link topnav-nav-link" do %>
<span class="nav-link-icon">
<%= render BlockScoutWeb.IconsView, "_block_icon.html" %>
</span>
<%= gettext("Blocks") %>
<% end %>
</li>
<li class="nav-item">
<%= link to: transaction_path(@conn, :index), class: "nav-link topnav-nav-link" do %>
<span class="nav-link-icon">
<%= render BlockScoutWeb.IconsView, "_transaction_icon.html" %>
</span>
<%= gettext("Transactions") %>
<% end %>
</li>
<li class="nav-item">
<%= link to: address_path(@conn, :index), class: "nav-link topnav-nav-link" do %>
<span class="nav-link-icon">
<%= render BlockScoutWeb.IconsView, "_accounts_icon.html" %>
</span>
<%= gettext("Accounts") %>
<% end %>
</li>
<li class="nav-item">
<%= link to: api_docs_path(@conn, :index), class: "nav-link topnav-nav-link" do %>
<span class="nav-link-icon">
<%= render BlockScoutWeb.IconsView, "_api_icon.html" %>
</span>
<%= gettext("API") %>
<% end %>
</li>
@ -28,7 +45,9 @@
<div class="input-group">
<%= search_input f, :q, class: 'form-control mr-auto', placeholder: gettext("Search by address, transaction hash, or block number"), "aria-describedby": "search-icon", "aria-label": gettext("Search"), "data-test": "search_input" %>
<div class="input-group-append">
<button class="input-group-text" id="search-icon"><i class="fas fa-search"></i></button>
<button class="input-group-text" id="search-icon">
<%= render BlockScoutWeb.IconsView, "_search_icon.html" %>
</button>
</div>
</div>
<button class="btn btn-outline-success my-2 my-sm-0 sr-only" type="submit"><%= gettext "Search" %></button>
@ -36,7 +55,15 @@
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<%= gettext("Mainnet") %>
<!-- ICON FOR MAINNET -->
<span class="nav-link-icon">
<%= render BlockScoutWeb.IconsView, "_network_icon.html" %>
</span>
<!-- ICON FOR TESTNET -->
<!-- <span class="nav-link-icon">
<%= render BlockScoutWeb.IconsView, "_test_network_icon.html" %>
</span> -->
<%= gettext("POA Core") %>
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="https://sokol.blockscout.com/"><%= gettext("POA Sokol") %></a>

@ -22,7 +22,7 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Pending") %></a>
<div class="dropdown-menu">
@ -33,7 +33,7 @@
) %>
<%= link(
gettext("Pending"),
class: "dropdown-item",
class: "dropdown-item active",
"data-test": "pending_transactions_link",
to: pending_transaction_path(@conn, :index)
) %>

@ -12,7 +12,7 @@
<div class="card">
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Token Transfers"),
@ -41,7 +41,7 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Token Holders") %></a>
<div class="dropdown-menu">
@ -58,7 +58,7 @@
<% end %>
<%= link(
gettext("Token Holders"),
class: "dropdown-item",
class: "dropdown-item active",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</div>

@ -12,7 +12,7 @@
<div class="card">
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Token Transfers"),
@ -39,19 +39,19 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Token Transfers") %></a>
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Read Contract") %></a>
<div class="dropdown-menu">
<%= link(
gettext("Token Transfers"),
class: "nav-link active",
class: "nav-link",
to: token_path(@conn, :show, @token.contract_address_hash)
) %>
<%= link(
gettext("Read Contract"),
to: "#",
class: "nav-link")%>
class: "nav-link active")%>
<%= link(
gettext("Token Holders"),
class: "nav-link",

@ -9,17 +9,25 @@
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @transfer.transaction_hash %>
</p>
<span>
<%= render BlockScoutWeb.AddressView,
"_link.html",
address: @transfer.from_address,
contract: BlockScoutWeb.AddressView.contract?(@transfer.from_address) %>
<%= link to: address_token_transfers_path(@conn, :index, @transfer.from_address, @token.contract_address_hash), "data-test": "address_hash_link" do %>
<%= render(
BlockScoutWeb.AddressView,
"_responsive_hash.html",
address: @transfer.from_address,
contract: BlockScoutWeb.AddressView.contract?(@transfer.from_address)
) %>
<% end %>
&rarr;
<%= render BlockScoutWeb.AddressView,
"_link.html",
address: @transfer.to_address,
contract: BlockScoutWeb.AddressView.contract?(@transfer.to_address) %>
<%= link to: address_token_transfers_path(@conn, :index, @transfer.to_address, @token.contract_address_hash), "data-test": "address_hash_link" do %>
<%= render(
BlockScoutWeb.AddressView,
"_responsive_hash.html",
address: @transfer.to_address,
contract: BlockScoutWeb.AddressView.contract?(@transfer.to_address)
) %>
<% end %>
</span>
<span>

@ -12,7 +12,7 @@
<div class="card">
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Token Transfers"),
@ -41,13 +41,13 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Token Transfers") %></a>
<div class="dropdown-menu">
<%= link(
gettext("Token Transfers"),
class: "dropdown-item",
class: "dropdown-item active",
to: token_path(@conn, :show, @token.contract_address_hash)
) %>
<%= if smart_contract_with_read_only_functions?(@token) do %>
@ -71,7 +71,7 @@
<%= if Enum.any?(@transfers) do %>
<%= for transfer <- @transfers do %>
<%= render("_token_transfer.html", token: @token, transfer: transfer) %>
<%= render("_token_transfer.html", conn: @conn, token: @token, transfer: transfer) %>
<% end %>
<% else %>
<div class="tile tile-muted text-center">

@ -1,4 +1,4 @@
<div class="tile tile-type-<%= type_suffix(@transaction) %> fade-in tile-status--<%= status(@transaction) %>" data-test="<%= type_suffix(@transaction) %>" data-transaction-hash="<%= @transaction.hash %>">
<div class="tile tile-type-<%= type_suffix(@transaction) %> fade-in <%= status_class(@transaction) %>" data-test="<%= type_suffix(@transaction) %>" data-transaction-hash="<%= @transaction.hash %>">
<div class="row" data-selector="token-transfers-toggle" data-test="chain_transaction">
<div class="col-md-2 d-flex flex-row flex-md-column align-items-left justify-content-start justify-content-lg-center mb-1 mb-md-0 pl-md-4">
<span class="tile-label" data-test="transaction_type">
@ -27,7 +27,7 @@
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-start text-md-right mt-3 mt-md-0">
<span class="mr-2 mr-md-0 order-1">
<%= link(
gettext("Block #%{number}", number: to_string(@transaction.block.number)),
gettext("Block #%{number}", number: to_string(@transaction.block_number)),
to: block_path(BlockScoutWeb.Endpoint, :show, @transaction.block)
) %>
</span>

@ -3,7 +3,7 @@
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Validated"),
@ -22,13 +22,13 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Validated") %></a>
<div class="dropdown-menu">
<%= link(
gettext("Validated"),
class: "dropdown-item",
class: "dropdown-item active",
to: transaction_path(@conn, :index)
) %>
<%= link(
@ -53,7 +53,7 @@
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer transactions" %></a>
</div>
</div>
<h2 class="card-title mb-0"><%= gettext "Transactions" %></h2>
<h1><%= gettext "Transactions" %></h1>
<p><%= gettext("Showing") %> <span data-selector="transaction-count"><%= Cldr.Number.to_string!(@transaction_estimated_count, format: "#,###") %></span> <%= gettext("Validated Transactions") %></p>
<span data-selector="transactions-list">
<%= for transaction <- @transactions do %>

@ -1,20 +0,0 @@
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-internal-transaction-id="<%= @internal_transaction.id %>">
<div class="row">
<div class="col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center tile-label">
<%= gettext("Internal Transaction") %>
</div>
<div class="col-md-9 col-lg-10 d-flex flex-column text-nowrap">
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %>
<span class="text-nowrap">
<%= render BlockScoutWeb.AddressView, "_link.html", address: @internal_transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.from_address) %>
&rarr;
<%= render BlockScoutWeb.AddressView, "_link.html", address: BlockScoutWeb.InternalTransactionView.to_address(@internal_transaction), contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.to_address) %>
</span>
<span class="tile-title text-truncate">
<%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %>
<%= gettext "Ether" %>
</span>
</div>
</div>
</div>

@ -5,7 +5,7 @@
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<%= if @show_token_transfers do %>
<li class="nav-item">
<%= link(
@ -33,7 +33,7 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Internal Transactions") %></a>
<div class="dropdown-menu">
@ -47,7 +47,7 @@
<% end %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
class: "dropdown-item active",
to: transaction_internal_transaction_path(@conn, :index, @transaction)
) %>
<%= link(
@ -64,7 +64,7 @@
<h2 class="card-title"><%= gettext "Internal Transactions" %></h2>
<%= if Enum.count(@internal_transactions) > 0 do %>
<%= for internal_transaction <- @internal_transactions do %>
<%= render "_internal_transaction.html", internal_transaction: internal_transaction %>
<%= render BlockScoutWeb.InternalTransactionView, "_tile.html", internal_transaction: internal_transaction %>
<% end %>
<% else %>
<div class="tile tile-muted text-center">

@ -6,7 +6,7 @@
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= if @show_token_transfers do %>
<%= link(
@ -33,7 +33,7 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Logs") %></a>
<div class="dropdown-menu">
@ -52,7 +52,7 @@
) %>
<%= link(
gettext("Logs"),
class: "dropdown-item",
class: "dropdown-item active",
to: transaction_log_path(@conn, :index, @transaction),
"data-test": "transaction_logs_link"
) %>

@ -5,7 +5,7 @@
<div class="card-header">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<ul class="nav nav-tabs card-header-tabs d-none d-lg-inline-flex">
<li class="nav-item">
<%= link(
gettext("Token Transfers"),
@ -30,13 +30,13 @@
</ul>
<!-- MOBILE DROPDOWN NAV -->
<ul class="nav nav-tabs card-header-tabs d-md-none">
<ul class="nav nav-tabs card-header-tabs d-lg-none">
<li class="nav-item dropdown flex-fill text-center">
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Token Transfers") %></a>
<div class="dropdown-menu">
<%= link(
gettext("Token Transfers"),
class: "dropdown-item",
class: "dropdown-item active",
to: transaction_token_transfer_path(@conn, :index, @transaction)
) %>
<%= link(

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.AddressContractView do
import BlockScoutWeb.AddressView, only: [smart_contract_verified?: 1, smart_contract_with_read_only_functions?: 1]
def format_smart_contract_abi(abi), do: Poison.encode!(abi, pretty: true)
def format_smart_contract_abi(abi), do: Poison.encode!(abi, pretty: false)
def format_optimization(true), do: gettext("true")
def format_optimization(false), do: gettext("false")

@ -0,0 +1,5 @@
defmodule BlockScoutWeb.AddressTokenTransferView do
use BlockScoutWeb, :view
alias BlockScoutWeb.AddressView
end

@ -4,6 +4,6 @@ defmodule BlockScoutWeb.AddressTokenView do
alias BlockScoutWeb.AddressView
def number_of_transfers(token) do
ngettext("%{count} transfer", "%{count} transfers", token.number_of_transfers)
ngettext("%{count} transfer", "%{count} transfers", token.transfers_count)
end
end

@ -1,12 +1,43 @@
defmodule BlockScoutWeb.AddressView do
use BlockScoutWeb, :view
alias Explorer.Chain.{Address, Hash, SmartContract, Token, TokenTransfer, Transaction}
alias Explorer.Chain
alias Explorer.Chain.{Address, Hash, InternalTransaction, SmartContract, Token, TokenTransfer, Transaction, Wei}
@dialyzer :no_match
def address_partial_selector(struct_to_render_from, direction, current_address, truncate \\ false)
def address_partial_selector(%Address{} = address, _, current_address, truncate) do
matching_address_check(current_address, address, contract?(address), truncate)
end
def address_partial_selector(
%InternalTransaction{to_address_hash: nil, created_contract_address_hash: nil},
:to,
_current_address,
_truncate
) do
gettext("Contract Address Pending")
end
def address_partial_selector(
%InternalTransaction{to_address: nil, created_contract_address: contract_address},
:to,
current_address,
truncate
) do
matching_address_check(current_address, contract_address, true, truncate)
end
def address_partial_selector(%InternalTransaction{to_address: address}, :to, current_address, truncate) do
matching_address_check(current_address, address, contract?(address), truncate)
end
def address_partial_selector(%InternalTransaction{from_address: address}, :from, current_address, truncate) do
matching_address_check(current_address, address, contract?(address), truncate)
end
def address_partial_selector(%TokenTransfer{to_address: address}, :to, current_address, truncate) do
matching_address_check(current_address, address, contract?(address), truncate)
end
@ -58,6 +89,16 @@ defmodule BlockScoutWeb.AddressView do
format_wei_value(balance, :ether)
end
def balance_percentage(%Address{fetched_coin_balance: balance}) do
balance
|> Wei.to(:ether)
|> Decimal.div(Decimal.new(Chain.total_supply()))
|> Decimal.mult(100)
|> Decimal.round(4)
|> Decimal.to_string(:normal)
|> Kernel.<>("% #{gettext("Market Cap")}")
end
def balance_block_number(%Address{fetched_coin_balance_block_number: nil}), do: ""
def balance_block_number(%Address{fetched_coin_balance_block_number: fetched_coin_balance_block_number}) do
@ -74,6 +115,18 @@ defmodule BlockScoutWeb.AddressView do
to_string(hash)
end
@doc """
Returns the primary name of an address if available.
"""
def primary_name(%Address{names: [_ | _] = address_names}) do
case Enum.find(address_names, &(&1.primary == true)) do
nil -> nil
%Address.Name{name: name} -> name
end
end
def primary_name(%Address{names: _}), do: nil
def qr_code(%Address{hash: hash}) do
hash
|> to_string()
@ -110,6 +163,10 @@ defmodule BlockScoutWeb.AddressView do
def token_title(%Token{name: name, symbol: symbol}), do: "#{name} (#{symbol})"
def transaction_count(%Address{} = address) do
Chain.address_to_transactions_estimated_count(address)
end
def trimmed_hash(%Hash{} = hash) do
string_hash = to_string(hash)
"#{String.slice(string_hash, 0..5)}#{String.slice(string_hash, -6..-1)}"
@ -134,16 +191,4 @@ defmodule BlockScoutWeb.AddressView do
truncate: truncate
}
end
@doc """
Returns the primary name of an address if available.
"""
def primary_name(%Address{names: [_ | _] = address_names}) do
case Enum.find(address_names, &(&1.primary == true)) do
nil -> nil
%Address.Name{name: name} -> name
end
end
def primary_name(%Address{names: _}), do: nil
end

@ -7,7 +7,26 @@ defmodule BlockScoutWeb.API.RPC.StatsView do
RPCView.render("show.json", data: token_supply)
end
def render("ethsupply.json", %{total_supply: total_supply}) do
RPCView.render("show.json", data: total_supply)
end
def render("ethprice.json", %{rates: rates}) do
RPCView.render("show.json", data: prepare_rates(rates))
end
def render("error.json", assigns) do
RPCView.render("error.json", assigns)
end
defp prepare_rates(rates) do
timestamp = rates.last_updated |> DateTime.to_unix() |> to_string()
%{
"ethbtc" => to_string(rates.btc_value),
"ethbtc_timestamp" => timestamp,
"ethusd" => to_string(rates.usd_value),
"ethusd_timestamp" => timestamp
}
end
end

@ -8,6 +8,10 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do
RPCView.render("show.json", data: %{"status" => prepared_status})
end
def render("getstatus.json", %{error: error}) do
RPCView.render("show.json", data: prepare_error(error))
end
def render("error.json", assigns) do
RPCView.render("error.json", assigns)
end
@ -19,4 +23,25 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do
defp prepare_tx_receipt_status(:ok), do: "1"
defp prepare_tx_receipt_status(_), do: "0"
defp prepare_error("") do
%{
"isError" => "0",
"errDescription" => ""
}
end
defp prepare_error(error) when is_binary(error) do
%{
"isError" => "1",
"errDescription" => error
}
end
defp prepare_error(error) when is_atom(error) do
%{
"isError" => "1",
"errDescription" => error |> Atom.to_string() |> String.replace("_", " ")
}
end
end

@ -1,17 +1,3 @@
defmodule BlockScoutWeb.InternalTransactionView do
use BlockScoutWeb, :view
@dialyzer :no_match
alias Explorer.Chain.{Address, InternalTransaction}
def create?(%InternalTransaction{type: :create}), do: true
def create?(_), do: false
# This is the address to be shown in the to field
def to_address_hash(%InternalTransaction{to_address_hash: nil, created_contract_address_hash: hash}), do: hash
def to_address_hash(%InternalTransaction{to_address_hash: hash}), do: hash
def to_address(%InternalTransaction{to_address: nil, created_contract_address: %Address{} = address}), do: address
def to_address(%InternalTransaction{to_address: %Address{} = address}), do: address
end

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Tokens.Helpers do
Helper functions for intereacting with `t:BlockScoutWeb.Chain.Token` attributes.
"""
alias Explorer.Chain.{Token, TokenTransfer}
alias Explorer.Chain.{Token, TokenTransfer, Address}
alias BlockScoutWeb.{CurrencyHelpers}
@doc """
@ -58,11 +58,14 @@ defmodule BlockScoutWeb.Tokens.Helpers do
When the token's name is nil, the function will return the contract address hash.
"""
def token_name(%Token{name: nil, contract_address_hash: address_hash}) do
def token_name(%Token{} = token), do: build_token_name(token)
def token_name(%Address.Token{} = address_token), do: build_token_name(address_token)
defp build_token_name(%{name: nil, contract_address_hash: address_hash}) do
"#{contract_address_hash_truncated(address_hash)}..."
end
def token_name(%Token{name: name}) do
defp build_token_name(%{name: name}) do
name
end

@ -50,10 +50,12 @@ defmodule BlockScoutWeb.TransactionView do
transaction
|> Chain.transaction_to_status()
|> case do
:failed -> gettext("Failed")
:out_of_gas -> gettext("Out of Gas")
:pending -> gettext("Pending")
:awaiting_internal_transactions -> gettext("(Awaiting internal transactions for status)")
:success -> gettext("Success")
{:error, :awaiting_internal_transactions} -> gettext("Error: (Awaiting internal transactions for reason)")
# The pool of possible error reasons is unknown or even if it is enumerable, so we can't translate them
{:error, reason} when is_binary(reason) -> gettext("Error: %{reason}", reason: reason)
end
end
@ -98,8 +100,14 @@ defmodule BlockScoutWeb.TransactionView do
|> Base.encode64()
end
def status(transaction) do
Chain.transaction_to_status(transaction)
def status_class(transaction) do
case Chain.transaction_to_status(transaction) do
:pending -> "tile-status--pending"
:awaiting_internal_transactions -> "tile-status--awaiting-internal-transactions"
:success -> "tile-status--success"
{:error, :awaiting_internal_transactions} -> "tile-status--error--awaiting-internal-transactions"
{:error, reason} when is_binary(reason) -> "tile-status--error--reason"
end
end
# This is the address to be shown in the to field

@ -12,7 +12,7 @@ defmodule BlockScoutWeb.AddressChannelTest do
Notifier.handle_event({:chain_event, :addresses, [address]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "count", payload: %{count: 1}} ->
%Phoenix.Socket.Broadcast{topic: ^topic, event: "count", payload: %{count: _}} ->
assert true
after
5_000 ->

@ -1,6 +1,19 @@
defmodule BlockScoutWeb.AddressControllerTest do
use BlockScoutWeb.ConnCase
describe "GET index/2" do
test "returns top addresses", %{conn: conn} do
address_hashes =
4..1
|> Enum.map(&insert(:address, fetched_coin_balance: &1))
|> Enum.map(& &1.hash)
conn = get(conn, address_path(conn, :index))
assert conn.assigns.addresses |> Enum.map(& &1.hash) == address_hashes
end
end
describe "GET show/3" do
test "redirects to address/:address_id/transactions", %{conn: conn} do
insert(:address, hash: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")

@ -18,7 +18,7 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
assert html_response(conn, 404)
end
test "returns tokens for the address", %{conn: conn} do
test "returns tokens that have balance for the address", %{conn: conn} do
address = insert(:address)
token1 =
@ -29,6 +29,20 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
:token
|> insert(name: "token2")
insert(
:token_balance,
address: address,
token_contract_address_hash: token1.contract_address_hash,
value: 1000
)
insert(
:token_balance,
address: address,
token_contract_address_hash: token2.contract_address_hash,
value: 0
)
insert(
:token_transfer,
token_contract_address: token1.contract_address,
@ -51,7 +65,7 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
assert html_response(conn, 200)
assert Enum.member?(actual_token_hashes, token1.contract_address_hash)
assert Enum.member?(actual_token_hashes, token2.contract_address_hash)
refute Enum.member?(actual_token_hashes, token2.contract_address_hash)
end
test "returns next page of results based on last seen token", %{conn: conn} do
@ -61,12 +75,28 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
1..50
|> Enum.reduce([], fn i, acc ->
token = insert(:token, name: "A Token#{i}", type: "ERC-20")
insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
address: address,
value: 1000
)
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address)
acc ++ [token.name]
end)
|> Enum.sort()
token = insert(:token, name: "Another Token", type: "ERC-721")
insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
address: address,
value: 1000
)
insert(:token_transfer, token: token, from_address: address)
%Token{name: name, type: type} = token
@ -89,6 +119,14 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
Enum.each(1..51, fn i ->
token = insert(:token, name: "A Token#{i}", type: "ERC-20")
insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
address: address,
value: 1000
)
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address)
end)

@ -0,0 +1,114 @@
defmodule BlockScoutWeb.AddressTokenTransferControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.Router.Helpers, only: [address_token_transfers_path: 4]
alias Explorer.Chain.{Address, Token}
describe "GET index/2" do
test "with invalid address hash", %{conn: conn} do
token_hash = "0xc8982771dd50285389c352c175ada74d074427c7"
conn = get(conn, address_token_transfers_path(conn, :index, "invalid_address", token_hash))
assert html_response(conn, 422)
end
test "with invalid token hash", %{conn: conn} do
address_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
conn = get(conn, address_token_transfers_path(conn, :index, address_hash, "invalid_address"))
assert html_response(conn, 422)
end
test "with an address that doesn't exist in our database", %{conn: conn} do
address_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
%Token{contract_address_hash: token_hash} = insert(:token)
conn = get(conn, address_token_transfers_path(conn, :index, address_hash, token_hash))
assert html_response(conn, 404)
end
test "with an token that doesn't exist in our database", %{conn: conn} do
%Address{hash: address_hash} = insert(:address)
token_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
conn = get(conn, address_token_transfers_path(conn, :index, address_hash, token_hash))
assert html_response(conn, 404)
end
test "without token transfers for a token", %{conn: conn} do
%Address{hash: address_hash} = insert(:address)
%Token{contract_address_hash: token_hash} = insert(:token)
conn = get(conn, address_token_transfers_path(conn, :index, address_hash, token_hash))
assert html_response(conn, 200)
assert conn.assigns.transactions == []
end
test "returns the transactions that have token transfers for the given address and token", %{conn: conn} do
address = insert(:address)
token = insert(:token)
transaction =
:transaction
|> insert()
|> with_block()
insert(
:token_transfer,
to_address: address,
transaction: transaction,
token_contract_address: token.contract_address
)
conn = get(conn, address_token_transfers_path(conn, :index, address.hash, token.contract_address_hash))
transaction_hashes = Enum.map(conn.assigns.transactions, & &1.hash)
assert html_response(conn, 200)
assert transaction_hashes == [transaction.hash]
end
test "returns next page of results based on last seen transactions", %{conn: conn} do
address = insert(:address)
token = insert(:token)
second_page_transactions =
1..50
|> Enum.map(fn index ->
block = insert(:block, number: 1000 - index)
transaction =
:transaction
|> insert()
|> with_block(block)
insert(
:token_transfer,
to_address: address,
transaction: transaction,
token_contract_address: token.contract_address
)
transaction
end)
|> Enum.map(& &1.hash)
transaction =
:transaction
|> insert()
|> with_block(insert(:block, number: 1002))
conn =
get(conn, address_token_transfers_path(conn, :index, address.hash, token.contract_address_hash), %{
"block_number" => Integer.to_string(transaction.block_number),
"index" => Integer.to_string(transaction.index)
})
actual_transactions = Enum.map(conn.assigns.transactions, & &1.hash)
assert second_page_transactions == actual_transactions
end
end
end

@ -1,6 +1,12 @@
defmodule BlockScoutWeb.API.RPC.StatsControllerTest do
use BlockScoutWeb.ConnCase
import Mox
alias Explorer.ExchangeRates
alias Explorer.ExchangeRates.Token
alias Explorer.ExchangeRates.Source.TestSource
describe "tokensupply" do
test "with missing contract address", %{conn: conn} do
params = %{
@ -74,4 +80,81 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do
assert response["message"] == "OK"
end
end
describe "ethsupply" do
test "returns total supply", %{conn: conn} do
params = %{
"module" => "stats",
"action" => "ethsupply"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == "252460800000000000000000000"
assert response["status"] == "1"
assert response["message"] == "OK"
end
end
describe "ethprice" do
setup :set_mox_global
setup do
# Use TestSource mock for this test set
configuration = Application.get_env(:explorer, Explorer.ExchangeRates)
Application.put_env(:explorer, Explorer.ExchangeRates, source: TestSource)
ExchangeRates.init([])
:ok
on_exit(fn ->
Application.put_env(:explorer, Explorer.ExchangeRates, configuration)
end)
end
test "returns the configured coin's price information", %{conn: conn} do
symbol = Application.get_env(:explorer, :coin)
eth = %Token{
available_supply: Decimal.new("1000000.0"),
btc_value: Decimal.new("1.000"),
id: "test",
last_updated: DateTime.utc_now(),
market_cap_usd: Decimal.new("1000000.0"),
name: "test",
symbol: symbol,
usd_value: Decimal.new("1.0"),
volume_24h_usd: Decimal.new("1000.0")
}
ExchangeRates.handle_info({nil, {:ok, [eth]}}, %{})
params = %{
"module" => "stats",
"action" => "ethprice"
}
expected_timestamp = eth.last_updated |> DateTime.to_unix() |> to_string()
expected_result = %{
"ethbtc" => to_string(eth.btc_value),
"ethbtc_timestamp" => expected_timestamp,
"ethusd" => to_string(eth.usd_value),
"ethusd_timestamp" => expected_timestamp
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
end
end

@ -121,4 +121,193 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do
assert response["message"] == "OK"
end
end
describe "getstatus" do
test "with missing txhash", %{conn: conn} do
params = %{
"module" => "transaction",
"action" => "getstatus"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "txhash is required"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with an invalid txhash", %{conn: conn} do
params = %{
"module" => "transaction",
"action" => "getstatus",
"txhash" => "badhash"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["message"] =~ "Invalid txhash format"
assert response["status"] == "0"
assert Map.has_key?(response, "result")
refute response["result"]
end
test "with a txhash that doesn't exist", %{conn: conn} do
params = %{
"module" => "transaction",
"action" => "getstatus",
"txhash" => "0x40eb908387324f2b575b4879cd9d7188f69c8fc9d87c901b9e2daaea4b442170"
}
expected_result = %{
"isError" => "0",
"errDescription" => ""
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "with a txhash with ok status", %{conn: conn} do
block = insert(:block)
transaction =
:transaction
|> insert()
|> with_block(block, status: :ok)
params = %{
"module" => "transaction",
"action" => "getstatus",
"txhash" => "#{transaction.hash}"
}
expected_result = %{
"isError" => "0",
"errDescription" => ""
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "with a txhash with error", %{conn: conn} do
error = "some error"
transaction_details = [
status: :error,
error: error,
internal_transactions_indexed_at: DateTime.utc_now()
]
transaction =
:transaction
|> insert()
|> with_block(transaction_details)
internal_transaction_details = [
transaction: transaction,
index: 0,
type: :reward,
error: error
]
insert(:internal_transaction, internal_transaction_details)
params = %{
"module" => "transaction",
"action" => "getstatus",
"txhash" => "#{transaction.hash}"
}
expected_result = %{
"isError" => "1",
"errDescription" => error
}
response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "with a txhash with failed status but awaiting internal transactions", %{conn: conn} do
transaction_details = [
status: :error,
error: nil,
internal_transactions_indexed_at: nil
]
transaction =
:transaction
|> insert()
|> with_block(transaction_details)
params = %{
"module" => "transaction",
"action" => "getstatus",
"txhash" => "#{transaction.hash}"
}
expected_result = %{
"isError" => "1",
"errDescription" => "awaiting internal transactions"
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
test "with a txhash with nil status", %{conn: conn} do
transaction = insert(:transaction, status: nil)
params = %{
"module" => "transaction",
"action" => "getstatus",
"txhash" => "#{transaction.hash}"
}
expected_result = %{
"isError" => "0",
"errDescription" => ""
}
assert response =
conn
|> get("/api", params)
|> json_response(200)
assert response["result"] == expected_result
assert response["status"] == "1"
assert response["message"] == "OK"
end
end
end

@ -35,7 +35,11 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do
end
test "includes internal transactions for the transaction", %{conn: conn} do
transaction = insert(:transaction)
transaction =
:transaction
|> insert()
|> with_block()
expected_internal_transaction = insert(:internal_transaction, transaction: transaction, index: 0)
insert(:internal_transaction, transaction: transaction, index: 1)

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.AddressPage do
use Wallaby.DSL
import Wallaby.Query, only: [css: 1, css: 2]
alias Explorer.Chain.{Address, InternalTransaction, Hash, Transaction}
alias Explorer.Chain.{Address, InternalTransaction, Hash, Transaction, Token}
def apply_filter(session, direction) do
session
@ -15,6 +15,10 @@ defmodule BlockScoutWeb.AddressPage do
css("[data-test='address_balance']")
end
def address(%Address{hash: hash}) do
css("[data-address-hash='#{hash}']", text: to_string(hash))
end
def contract_creator do
css("[data-test='address_contract_creator']")
end
@ -23,6 +27,14 @@ defmodule BlockScoutWeb.AddressPage do
click(session, css("[data-test='internal_transactions_tab_link']"))
end
def click_tokens(session) do
click(session, css("[data-test='tokens_tab_link']"))
end
def click_token_transfers(session, %Token{contract_address_hash: contract_address_hash}) do
click(session, css("[data-test='token_transfers_#{contract_address_hash}']"))
end
def contract_creation(%InternalTransaction{created_contract_address_hash: hash}) do
css("[data-address-hash='#{hash}']", text: to_string(hash))
end
@ -75,12 +87,6 @@ defmodule BlockScoutWeb.AddressPage do
css("[data-transaction-hash='#{transaction_hash}'] [data-test='transaction_status']")
end
def visit_page(session, %Address{hash: address_hash}), do: visit_page(session, address_hash)
def visit_page(session, address_hash) do
visit(session, "/address/#{address_hash}")
end
def token_transfer(%Transaction{hash: transaction_hash}, %Address{hash: address_hash}, count: count) do
css(
"[data-transaction-hash='#{transaction_hash}'] [data-test='token_transfer'] [data-address-hash='#{address_hash}']",
@ -95,4 +101,14 @@ defmodule BlockScoutWeb.AddressPage do
def token_transfers_expansion(%Transaction{hash: transaction_hash}) do
css("[data-transaction-hash='#{transaction_hash}'] [data-test='token_transfers_expansion']")
end
def visit_page(session, %Address{hash: address_hash}), do: visit_page(session, address_hash)
def visit_page(session, address_hash) do
visit(session, "/address/#{address_hash}")
end
def visit_page(session) do
visit(session, "/accounts")
end
end

@ -46,6 +46,6 @@ defmodule BlockScoutWeb.ContractVerifyPage do
end
def verify_and_publish(session) do
click(session, button("Verify and publish"))
click(session, button("Verify & publish"))
end
end

@ -1,15 +1,13 @@
defmodule BlockScoutWeb.ViewingAddressesTest do
use BlockScoutWeb.FeatureCase, async: true
alias Explorer.Chain.Wei
alias BlockScoutWeb.{AddressPage, AddressView, Notifier}
setup do
block = insert(:block)
{:ok, balance} = Wei.cast(5)
lincoln = insert(:address, fetched_coin_balance: balance)
taft = insert(:address)
lincoln = insert(:address, fetched_coin_balance: 5)
taft = insert(:address, fetched_coin_balance: 5)
from_taft =
:transaction
@ -29,6 +27,24 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
}}
end
describe "viewing top addresses" do
setup do
addresses = Enum.map(150..101, &insert(:address, fetched_coin_balance: &1))
{:ok, %{addresses: addresses}}
end
test "lists top addresses", %{session: session, addresses: addresses} do
[first_address | _] = addresses
[last_address | _] = Enum.reverse(addresses)
session
|> AddressPage.visit_page()
|> assert_has(AddressPage.address(first_address))
|> assert_has(AddressPage.address(last_address))
end
end
test "viewing address overview information", %{session: session} do
address = insert(:address, fetched_coin_balance: 500)
@ -241,14 +257,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
end
end
test "viewing transaction count", %{addresses: addresses, session: session} do
insert_list(1000, :transaction, to_address: addresses.lincoln)
session
|> AddressPage.visit_page(addresses.lincoln)
|> assert_text(AddressPage.transaction_count(), "1,002")
end
test "contract creation is shown for to_address on list page", %{
addresses: addresses,
block: block,
@ -414,4 +422,42 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
|> assert_has(AddressPage.token_transfers(transaction, count: 3))
end
end
describe "viewing token transfers from a specific token" do
test "list token transfers related to the address", %{
addresses: addresses,
block: block,
session: session
} do
lincoln = addresses.lincoln
taft = addresses.taft
contract_address = insert(:contract_address)
token = insert(:token, contract_address: contract_address)
transaction =
:transaction
|> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block)
insert(
:token_transfer,
from_address: lincoln,
to_address: taft,
transaction: transaction,
token_contract_address: contract_address
)
insert(:token_balance, address: lincoln, token_contract_address_hash: contract_address.hash)
session
|> AddressPage.visit_page(lincoln)
|> AddressPage.click_tokens()
|> AddressPage.click_token_transfers(token)
|> assert_has(AddressPage.token_transfers(transaction, count: 1))
|> assert_has(AddressPage.token_transfer(transaction, lincoln, count: 1))
|> assert_has(AddressPage.token_transfer(transaction, taft, count: 1))
|> refute_has(AddressPage.token_transfers_expansion(transaction))
end
end
end

@ -5,19 +5,19 @@ defmodule BlockScoutWeb.AddressTokenViewTest do
describe "number_of_transfers/1" do
test "returns the singular form when there is only one transfer" do
token = %{number_of_transfers: 1}
token = %{transfers_count: 1}
assert AddressTokenView.number_of_transfers(token) == "1 transfer"
end
test "returns the plural form when there is more than one transfer" do
token = %{number_of_transfers: 2}
token = %{transfers_count: 2}
assert AddressTokenView.number_of_transfers(token) == "2 transfers"
end
test "returns the plural form when there are 0 transfers" do
token = %{number_of_transfers: 0}
token = %{transfers_count: 0}
assert AddressTokenView.number_of_transfers(token) == "0 transfers"
end

@ -5,11 +5,25 @@ defmodule BlockScoutWeb.AddressViewTest do
alias BlockScoutWeb.AddressView
describe "address_partial_selector/4" do
test "for a pending contract creation to address" do
test "for a pending transaction contract creation to address" do
transaction = insert(:transaction, to_address: nil, created_contract_address_hash: nil)
assert AddressView.address_partial_selector(transaction, :to, nil) == "Contract Address Pending"
end
test "for a pending internal transaction contract creation to address" do
transaction = insert(:transaction, to_address: nil)
internal_transaction =
insert(:internal_transaction,
index: 1,
transaction: transaction,
to_address: nil,
created_contract_address_hash: nil
)
assert AddressView.address_partial_selector(internal_transaction, :to, nil) == "Contract Address Pending"
end
test "will truncate address" do
transaction = %Transaction{to_address: to_address} = insert(:transaction)
@ -101,6 +115,21 @@ defmodule BlockScoutWeb.AddressViewTest do
end
end
describe "balance_block_number/1" do
test "gives empty string with no fetched balance block number present" do
assert AddressView.balance_block_number(%Address{}) == ""
end
test "gives block number when fetched balance block number is non-nil" do
assert AddressView.balance_block_number(%Address{fetched_coin_balance_block_number: 1_000_000}) == "1000000"
end
end
test "balance_percentage/1" do
address = insert(:address, fetched_coin_balance: 2_524_608_000_000_000_000_000_000)
assert "1.0000% Market Cap" = AddressView.balance_percentage(address)
end
describe "contract?/1" do
test "with a smart contract" do
{:ok, code} = Data.cast("0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef")
@ -118,6 +147,39 @@ defmodule BlockScoutWeb.AddressViewTest do
end
end
describe "hash/1" do
test "gives a string version of an address's hash" do
address = %Address{
hash: %Hash{
byte_count: 20,
bytes: <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, 91>>
}
}
assert AddressView.hash(address) == "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
end
end
describe "primary_name/1" do
test "gives an address's primary name when present" do
address = insert(:address)
address_name = insert(:address_name, address: address, primary: true, name: "POA Foundation Wallet")
insert(:address_name, address: address, name: "POA Wallet")
preloaded_address = Explorer.Repo.preload(address, :names)
assert AddressView.primary_name(preloaded_address) == address_name.name
end
test "returns nil when no primary available" do
address_name = insert(:address_name, name: "POA Wallet")
preloaded_address = Explorer.Repo.preload(address_name.address, :names)
refute AddressView.primary_name(preloaded_address)
end
end
describe "qr_code/1" do
test "it returns an encoded value" do
address = build(:address)
@ -227,47 +289,4 @@ defmodule BlockScoutWeb.AddressViewTest do
assert AddressView.token_title(token) == "super token money (ST$)"
end
end
describe "hash/1" do
test "gives a string version of an address's hash" do
address = %Address{
hash: %Hash{
byte_count: 20,
bytes: <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, 91>>
}
}
assert AddressView.hash(address) == "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
end
end
describe "balance_block_number/1" do
test "gives empty string with no fetched balance block number present" do
assert AddressView.balance_block_number(%Address{}) == ""
end
test "gives block number when fetched balance block number is non-nil" do
assert AddressView.balance_block_number(%Address{fetched_coin_balance_block_number: 1_000_000}) == "1000000"
end
end
describe "primary_name/1" do
test "gives an address's primary name when present" do
address = insert(:address)
address_name = insert(:address_name, address: address, primary: true, name: "POA Foundation Wallet")
insert(:address_name, address: address, name: "POA Wallet")
preloaded_address = Explorer.Repo.preload(address, :names)
assert AddressView.primary_name(preloaded_address) == address_name.name
end
test "returns nil when no primary available" do
address_name = insert(:address_name, name: "POA Wallet")
preloaded_address = Explorer.Repo.preload(address_name.address, :names)
refute AddressView.primary_name(preloaded_address)
end
end
end

@ -1,62 +0,0 @@
defmodule BlockScoutWeb.InternalTransactionViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.InternalTransactionView
describe "create?/1" do
test "with internal transaction of type create returns true" do
internal_transaction = build(:internal_transaction_create)
assert InternalTransactionView.create?(internal_transaction)
end
test "with non-create type internal transaction returns false" do
internal_transaction = build(:internal_transaction)
refute InternalTransactionView.create?(internal_transaction)
end
end
describe "to_address_hash/1" do
setup do
transaction = insert(:transaction)
{:ok, transaction: transaction}
end
test "with a contract address", %{transaction: transaction} do
internal_transaction = insert(:internal_transaction_create, transaction: transaction, index: 1)
assert InternalTransactionView.to_address_hash(internal_transaction) ==
internal_transaction.created_contract_address_hash
end
test "without a contract address", %{transaction: transaction} do
internal_transaction = insert(:internal_transaction, transaction: transaction, index: 1)
assert InternalTransactionView.to_address_hash(internal_transaction) == internal_transaction.to_address_hash
end
end
describe "to_address/1" do
setup do
transaction = insert(:transaction)
{:ok, transaction: transaction}
end
test "with a contract address", %{transaction: transaction} do
internal_transaction = insert(:internal_transaction_create, transaction: transaction, index: 1)
preloaded_internal_transaction = Explorer.Repo.preload(internal_transaction, :to_address)
assert InternalTransactionView.to_address(preloaded_internal_transaction) ==
preloaded_internal_transaction.created_contract_address
end
test "without a contract address", %{transaction: transaction} do
internal_transaction = insert(:internal_transaction, transaction: transaction, index: 1)
preloaded_internal_transaction = Explorer.Repo.preload(internal_transaction, :created_contract_address)
assert InternalTransactionView.to_address(preloaded_internal_transaction) ==
preloaded_internal_transaction.to_address
end
end
end

@ -72,38 +72,46 @@ defmodule BlockScoutWeb.TransactionViewTest do
assert TransactionView.formatted_status(transaction) == "Pending"
end
test "with block with status :error with gas_used < gas" do
gas = 2
test "with block without status (pre-Byzantium/Ethereum Class)" do
block = insert(:block)
transaction =
:transaction
|> insert(gas: gas)
|> with_block(block, gas_used: gas - 1, status: :error)
|> insert()
|> with_block(block, status: nil)
assert TransactionView.formatted_status(transaction) == "Failed"
assert TransactionView.formatted_status(transaction) == "(Awaiting internal transactions for status)"
end
test "with block with status :error with gas <= gas_used" do
test "with receipt with status :ok" do
gas = 2
transaction =
:transaction
|> insert(gas: gas)
|> with_block(gas_used: gas, status: :error)
|> with_block(gas_used: gas - 1, status: :ok)
assert TransactionView.formatted_status(transaction) == "Out of Gas"
assert TransactionView.formatted_status(transaction) == "Success"
end
test "with receipt with status :ok" do
gas = 2
test "with block with status :error without internal transactions indexed" do
block = insert(:block)
transaction =
:transaction
|> insert(gas: gas)
|> with_block(gas_used: gas - 1, status: :ok)
|> insert()
|> with_block(block, status: :error)
assert TransactionView.formatted_status(transaction) == "Success"
assert TransactionView.formatted_status(transaction) == "Error: (Awaiting internal transactions for reason)"
end
test "with block with status :error with internal transactions indexed uses `error`" do
transaction =
:transaction
|> insert()
|> with_block(status: :error, internal_transactions_indexed_at: DateTime.utc_now(), error: "Out of Gas")
assert TransactionView.formatted_status(transaction) == "Error: Out of Gas"
end
end

@ -1,8 +0,0 @@
defmodule BlockScoutWeb.FakeAdapter do
alias Explorer.Chain.Address
alias Explorer.Repo
def address_estimated_count do
Repo.aggregate(Address, :count, :hash)
end
end

@ -10,11 +10,6 @@ defmodule EthereumJSONRPC.Receipt do
alias EthereumJSONRPC
alias EthereumJSONRPC.Logs
# > 21000 gas is charged for any transaction as a "base fee". This covers the cost of an elliptic curve operation to
# > recover the sender address from the signature as well as the disk and bandwidth space of storing the transaction.
# -- https://github.com/ethereum/wiki/wiki/Design-Rationale
@base_fee_gas 21_000
@type elixir :: %{String.t() => String.t() | non_neg_integer}
@typedoc """
@ -81,10 +76,8 @@ defmodule EthereumJSONRPC.Receipt do
the [status](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-658.md) as that was a post-Byzantium
[EIP](https://github.com/ethereum/EIPs/tree/master/EIPS).
Pre-Byzantium receipts are given a derived `:status`:
* If `"gas"` (supplied by caller from `EthereumJSONRPC.Transaction.elixir`) `==` `"gasUsed"`, then `:status` is
`:error`
Pre-Byzantium receipts are given a `:status` of `nil`. The `:status` can only be derived from looking at the internal
transactions to see if there was an error.
iex> EthereumJSONRPC.Receipt.elixir_to_params(
...> %{
@ -106,85 +99,11 @@ defmodule EthereumJSONRPC.Receipt do
%{
cumulative_gas_used: 21001,
gas_used: 21001,
status: :error,
transaction_hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
transaction_index: 0
}
* Except, when it is the base transaction fee (21,000 gas)
iex> EthereumJSONRPC.Receipt.elixir_to_params(
...> %{
...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
...> "blockNumber" => 46147,
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => 21000,
...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4",
...> "gas" => 21000,
...> "gasUsed" => 21000,
...> "logs" => [],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957",
...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
...> "transactionIndex" => 0
...> }
...> )
%{
cumulative_gas_used: 21000,
gas_used: 21000,
status: :ok,
transaction_hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
transaction_index: 0
}
* Otherwise, `:status` is `:ok`
iex> EthereumJSONRPC.Receipt.elixir_to_params(
...> %{
...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
...> "blockNumber" => 46147,
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => 21000,
...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4",
...> "gas" => 40000,
...> "gasUsed" => 21000,
...> "logs" => [],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957",
...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
...> "transactionIndex" => 0
...> }
...> )
%{
cumulative_gas_used: 21000,
gas_used: 21000,
status: :ok,
status: nil,
transaction_hash: "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
transaction_index: 0
}
It is a developer error if the budgeted `"gas"` is not supplied for deriving the pre-Byzantium `:status`.
iex> EthereumJSONRPC.Receipt.elixir_to_params(
...> %{
...> "blockHash" => "0x4e3a3754410177e6937ef1f84bba68ea139e8d1a2258c5f85db9f1cd715a1bdd",
...> "blockNumber" => 46147,
...> "contractAddress" => nil,
...> "cumulativeGasUsed" => 21000,
...> "from" => "0xa1e4380a3b1f749673e270229993ee55f35663b4",
...> "gasUsed" => 21000,
...> "logs" => [],
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "root" => "0x96a8e009d2b88b1483e6941e6812e32263b05683fac202abc622a3e31aed1957",
...> "to" => "0x5df9b87991262f6ba471f09758cde1c0fc1de734",
...> "transactionHash" => "0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060",
...> "transactionIndex" => 0
...> }
...> )
** (ArgumentError) Pre-Byzantium transaction receipts require the transaction gas to be given to derive their status
"""
@spec elixir_to_params(elixir) :: %{
cumulative_gas_used: non_neg_integer,
@ -317,31 +236,8 @@ defmodule EthereumJSONRPC.Receipt do
"""
end
defp elixir_to_status(elixir) do
case elixir do
%{"status" => status} ->
status
%{"gas" => gas, "gasUsed" => gas_used} ->
pre_byzantium_status(gas, gas_used)
_ ->
raise ArgumentError,
"Pre-Byzantium transaction receipts require the transaction gas to be given to derive their status"
end
end
# Temporary fix for https://github.com/poanetwork/blockscout/issues/673 as this is the most common gas == gas_used,
# but not failed scenario. Only internal transactions can prove if gas == gas_used means failure pre-Byzantium.
defp pre_byzantium_status(@base_fee_gas, @base_fee_gas), do: :ok
defp pre_byzantium_status(gas, gas_used) when is_integer(gas) and is_integer(gas_used) do
if gas_used < gas do
:ok
else
:error
end
end
defp elixir_to_status(%{"status" => status}), do: status
defp elixir_to_status(_), do: nil
# double check that no new keys are being missed by requiring explicit match for passthrough
# `t:EthereumJSONRPC.address/0` and `t:EthereumJSONRPC.hash/0` pass through as `Explorer.Chain` can verify correct

@ -37,7 +37,7 @@ defmodule EthereumJSONRPC.ReceiptsTest do
index: 12,
first_topic: "0xf6db2bace4ac8277384553ad9603d045220a91fb2448ab6130d7a6f044f9a8cf",
gas_used: 106_025,
status: :error,
status: nil,
type: nil,
transaction_hash: "0xd3efddbbeb6ad8d8bb3f6b8c8fb6165567e9dd868013146bdbeb60953c82822a",
transaction_index: 17
@ -64,6 +64,7 @@ defmodule EthereumJSONRPC.ReceiptsTest do
case status do
:ok -> "0x1"
:error -> "0x0"
nil -> nil
end
expect(EthereumJSONRPC.Mox, :json_rpc, fn _json, _options ->

@ -69,16 +69,18 @@ defmodule Explorer.Chain do
@typep paging_options :: {:paging_options, PagingOptions.t()}
@doc """
Estimated count of `t:Explorer.Chain.Address.t/0`.
Estimated count of addresses
Gets an estimated count of `t:Explorer.Chain.Address.t/0`'s where the `fetched_coin_balance` is > 0
"""
@spec address_estimated_count :: non_neg_integer()
def address_estimated_count do
%Postgrex.Result{rows: [[rows]]} =
SQL.query!(Repo, "SELECT reltuples::BIGINT AS estimate FROM pg_class WHERE relname='addresses'")
{:ok, %Postgrex.Result{rows: result}} =
Repo.query("""
EXPLAIN SELECT COUNT(a0.hash) FROM addresses AS a0 WHERE (a0.fetched_coin_balance > 0)
""")
rows
{[explain], _} = List.pop_at(result, 1)
[[_ | [rows]]] = Regex.scan(~r/rows=(\d+)/, explain)
String.to_integer(rows)
end
@doc """
@ -132,33 +134,35 @@ defmodule Explorer.Chain do
end
@doc """
Counts the number of `t:Explorer.Chain.Transaction.t/0` to or from the `address`.
Gets an estimated count of `t:Explorer.Chain.Transaction.t/0` to or from the `address` based on the estimated rows
resulting in an EXPLAIN of the query plan for the count query.
"""
@spec address_to_transaction_count(Address.t()) :: non_neg_integer()
def address_to_transaction_count(%Address{hash: hash}) do
{:ok, %{rows: [[result]]}} =
SQL.query(
Repo,
@spec address_to_transactions_estimated_count(Address.t()) :: non_neg_integer()
def address_to_transactions_estimated_count(%Address{hash: address_hash}) do
{:ok, %Postgrex.Result{rows: result}} =
Repo.query(
"""
SELECT COUNT(hash) from
(
SELECT t0."hash" address
FROM "transactions" AS t0
LEFT OUTER JOIN "internal_transactions" AS i1 ON (i1."transaction_hash" = t0."hash") AND (i1."type" = 'create')
WHERE (i1."created_contract_address_hash" = $1 AND t0."to_address_hash" IS NULL)
UNION
SELECT t0."hash" address
FROM "transactions" AS t0
WHERE (t0."to_address_hash" = $1)
OR (t0."from_address_hash" = $1)
) AS hash
EXPLAIN SELECT COUNT(DISTINCT t.hash) FROM
(
SELECT t0.hash FROM transactions AS t0 WHERE t0.from_address_hash = $1
UNION
SELECT t0.hash FROM transactions AS t0 WHERE t0.to_address_hash = $1
UNION
SELECT t0.hash FROM transactions AS t0 WHERE t0.created_contract_address_hash = $1
UNION
SELECT tt.transaction_hash AS hash FROM token_transfers AS tt
WHERE tt.from_address_hash = $1
UNION
SELECT tt.transaction_hash AS hash FROM token_transfers AS tt
WHERE tt.to_address_hash = $1
) as t
""",
[hash.bytes]
[address_hash.bytes]
)
result
{[unique_explain], _} = List.pop_at(result, 1)
[[_ | [rows]]] = Regex.scan(~r/rows=(\d+)/, unique_explain)
String.to_integer(rows)
end
@doc """
@ -182,13 +186,60 @@ defmodule Explorer.Chain do
when is_list(options) do
direction = Keyword.get(options, :direction)
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
options
|> Keyword.get(:paging_options, @default_paging_options)
|> fetch_transactions()
|> Transaction.where_address_fields_match(address_hash, direction)
|> join_associations(necessity_by_association)
transaction_matches =
direction
|> case do
:from -> [:from_address_hash]
:to -> [:to_address_hash, :created_contract_address_hash]
_ -> [:from_address_hash, :to_address_hash, :created_contract_address_hash]
end
|> Enum.map(fn address_field ->
paging_options
|> fetch_transactions()
|> Transaction.where_address_fields_match(address_hash, address_field)
|> join_associations(necessity_by_association)
|> Transaction.preload_token_transfers(address_hash)
|> Repo.all()
|> MapSet.new()
end)
token_transfer_matches =
paging_options
|> fetch_transactions()
|> TokenTransfer.where_address_fields_match(address_hash, direction)
|> join_associations(necessity_by_association)
|> Transaction.preload_token_transfers(address_hash)
|> Repo.all()
|> MapSet.new()
transaction_matches
|> Enum.reduce(token_transfer_matches, &MapSet.union/2)
|> MapSet.to_list()
|> Enum.sort_by(& &1.index, &>=/2)
|> Enum.sort_by(& &1.block_number, &>=/2)
|> Enum.slice(0..paging_options.page_size)
end
@doc """
Finds all `t:Explorer.Chain.Transaction.t/0`s given the address_hash and the token contract
address hash.
## Options
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
`:key` (in the form of `%{"inserted_at" => inserted_at}`). Results will be the transactions
older than the `index` that are passed.
"""
@spec address_to_transactions_with_token_tranfers(Hash.t(), Hash.t(), [paging_options]) :: [Transaction.t()]
def address_to_transactions_with_token_tranfers(address_hash, token_hash, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
address_hash
|> Transaction.transactions_with_token_transfers(token_hash)
|> Transaction.preload_token_transfers(address_hash)
|> handle_paging_options(paging_options)
|> Repo.all()
end
@ -712,7 +763,7 @@ defmodule Explorer.Chain do
`:required`, and the `t:Explorer.Chain.Block.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Block.t/0` will not be included in the page `entries`.
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
`:key` (a tuple of the lowest/oldest `{block_number}`) and. Results will be the internal
`:key` (a tuple of the lowest/oldest `{block_number}`). Results will be the internal
transactions older than the `block_number` that are passed.
"""
@ -729,6 +780,19 @@ defmodule Explorer.Chain do
|> Repo.all()
end
@doc """
Lists the top 250 `t:Explorer.Chain.Address.t/0`'s' in descending order based on coin balance.
"""
@spec list_top_addresses :: [Address.t()]
def list_top_addresses do
Address
|> limit(250)
|> order_by(desc: :fetched_coin_balance, asc: :hash)
|> where([address], address.fetched_coin_balance > ^0)
|> Repo.all()
end
@doc """
Returns a stream of unfetched `t:Explorer.Chain.Address.CoinBalance.t/0`.
@ -1263,6 +1327,7 @@ defmodule Explorer.Chain do
|> page_internal_transaction(paging_options)
|> limit(^paging_options.page_size)
|> order_by([internal_transaction], asc: internal_transaction.index)
|> preload(transaction: :block)
|> Repo.all()
end
@ -1337,21 +1402,31 @@ defmodule Explorer.Chain do
## Returns
* `:failed` - the transaction failed without running out of gas
* `:pending` - the transaction has not be confirmed in a block yet
* `:out_of_gas` - the transaction failed because it ran out of gas
* `:pending` - the transaction has not be confirmed in a block yet.
* `:awaiting_internal_transactions` - the transaction happened in a pre-Byzantium block or on a chain like Ethereum
Classic (ETC) that never adopted [EIP-658](https://github.com/Arachnid/EIPs/blob/master/EIPS/eip-658.md), which
add transaction status to transaction receipts, so the status can only be derived whether the first internal
transaction has an error.
* `:success` - the transaction has been confirmed in a block
* `{:error, :awaiting_internal_transactions}` - the transactions happened post-Byzantium, but the error message
requires the internal transactions.
* `{:error, reason}` - the transaction failed due to `reason` in its first internal transaction.
"""
@spec transaction_to_status(Transaction.t()) :: :failed | :pending | :out_of_gas | :success
def transaction_to_status(%Transaction{status: nil}), do: :pending
@spec transaction_to_status(Transaction.t()) ::
:pending
| :awaiting_internal_transactions
| :success
| {:error, :awaiting_internal_transactions}
| {:error, reason :: String.t()}
def transaction_to_status(%Transaction{block_hash: nil, status: nil}), do: :pending
def transaction_to_status(%Transaction{status: nil}), do: :awaiting_internal_transactions
def transaction_to_status(%Transaction{status: :ok}), do: :success
def transaction_to_status(%Transaction{gas: gas, gas_used: gas_used, status: :error}) when gas_used >= gas do
:out_of_gas
end
def transaction_to_status(%Transaction{status: :error, internal_transactions_indexed_at: nil, error: nil}),
do: {:error, :awaiting_internal_transactions}
def transaction_to_status(%Transaction{status: :error}), do: :failed
def transaction_to_status(%Transaction{status: :error, error: error}) when is_binary(error), do: {:error, error}
@doc """
The `t:Explorer.Chain.Transaction.t/0` or `t:Explorer.Chain.InternalTransaction.t/0` `value` of the `transaction` in
@ -1658,38 +1733,13 @@ defmodule Explorer.Chain do
Repo.one(query) != nil
end
@spec tokens_with_number_of_transfers_from_address(Hash.Address.t(), [any()]) :: []
def tokens_with_number_of_transfers_from_address(address_hash, paging_options \\ []) do
@spec address_tokens_with_balance(Hash.Address.t(), [any()]) :: []
def address_tokens_with_balance(address_hash, paging_options \\ []) do
address_hash
|> fetch_tokens_from_address_hash(paging_options)
|> add_number_of_transfers_to_tokens_from_address(address_hash)
end
@spec fetch_tokens_from_address_hash(Hash.Address.t(), [any()]) :: []
def fetch_tokens_from_address_hash(address_hash, paging_options \\ []) do
address_hash
|> Token.with_transfers_by_address(paging_options)
|> Address.Token.list_address_tokens_with_balance(paging_options)
|> Repo.all()
end
@spec add_number_of_transfers_to_tokens_from_address([Token], Hash.Address.t()) :: []
defp add_number_of_transfers_to_tokens_from_address(tokens, address_hash) do
Enum.map(tokens, fn token ->
Map.put(
token,
:number_of_transfers,
count_token_transfers_from_address_hash(token.contract_address_hash, address_hash)
)
end)
end
@spec count_token_transfers_from_address_hash(Hash.Address.t(), Hash.Address.t()) :: []
def count_token_transfers_from_address_hash(token_hash, address_hash) do
token_hash
|> Token.interactions_with_address(address_hash)
|> Repo.aggregate(:count, :name)
end
@doc """
Update a new `t:Token.t/0` record.

@ -0,0 +1,106 @@
defmodule Explorer.Chain.Address.Token do
@moduledoc """
A projection that represents the relation between a Token and a specific Address.
This representation is expressed by the following attributes:
- contract_address_hash - Address of a Token's contract.
- name - Token's name.
- symbol - Token's symbol.
- type - Token's type.
- decimals - Token's decimals.
- balance - how much tokens (TokenBalance) the Address has from the Token.
- transfer_count - a count of how many TokenTransfers of the Token the Address was involved.
"""
@enforce_keys [:contract_address_hash, :inserted_at, :name, :symbol, :balance, :decimals, :type, :transfers_count]
defstruct @enforce_keys
import Ecto.Query
alias Explorer.{PagingOptions, Chain}
alias Explorer.Chain.{Hash, Address, Address.TokenBalance}
@default_paging_options %PagingOptions{page_size: 50}
@typep paging_options :: {:paging_options, PagingOptions.t()}
@doc """
It builds a paginated query of Address.Tokens that have a balance higher than zero ordered by type and name.
"""
@spec list_address_tokens_with_balance(Hash.t(), [paging_options()]) :: %Ecto.Query{}
def list_address_tokens_with_balance(address_hash, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
Chain.Token
|> Chain.Token.join_with_transfers()
|> join_with_last_balance(address_hash)
|> order_filter_and_group(address_hash)
|> page_tokens(paging_options)
|> limit(^paging_options.page_size)
end
defp order_filter_and_group(query, address_hash) do
from(
[token, transfer, balance] in query,
order_by: fragment("? DESC, LOWER(?) ASC NULLS LAST", token.type, token.name),
where:
(transfer.to_address_hash == ^address_hash or transfer.from_address_hash == ^address_hash) and balance.value > 0,
group_by: [token.name, token.symbol, balance.value, token.type, token.contract_address_hash],
select: %Address.Token{
contract_address_hash: token.contract_address_hash,
inserted_at: max(token.inserted_at),
name: token.name,
symbol: token.symbol,
balance: balance.value,
decimals: max(token.decimals),
type: token.type,
transfers_count: count(token.contract_address_hash)
}
)
end
defp join_with_last_balance(queryable, address_hash) do
last_balance_query =
from(
tb in TokenBalance,
where: tb.address_hash == ^address_hash,
distinct: :token_contract_address_hash,
order_by: [desc: :block_number],
select: %{value: tb.value, token_contract_address_hash: tb.token_contract_address_hash}
)
from(
t in queryable,
join: tb in subquery(last_balance_query),
on: tb.token_contract_address_hash == t.contract_address_hash
)
end
@doc """
Builds the pagination according to the given key within `PagingOptions`.
* it just returns the given query when the key is nil.
* it composes another where clause considering the `type`, `name` and `inserted_at`.
"""
def page_tokens(query, %PagingOptions{key: nil}), do: query
def page_tokens(query, %PagingOptions{key: {nil, type, inserted_at}}) do
where(
query,
[token],
token.type < ^type or (token.type == ^type and is_nil(token.name) and token.inserted_at < ^inserted_at)
)
end
def page_tokens(query, %PagingOptions{key: {name, type, inserted_at}}) do
upper_name = String.downcase(name)
where(
query,
[token],
token.type < ^type or
(token.type == ^type and (fragment("LOWER(?)", token.name) > ^upper_name or is_nil(token.name))) or
(token.type == ^type and fragment("LOWER(?)", token.name) == ^upper_name and token.inserted_at < ^inserted_at)
)
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save