@ -1,20 +0,0 @@ |
|||||||
# Upgrading Guide |
|
||||||
|
|
||||||
### Migration scripts |
|
||||||
|
|
||||||
There is in the project a `scripts` folder that contains `SQL` files responsible to migrate data from the database. |
|
||||||
|
|
||||||
This script should be used if you already have an indexed database with a large amount of data. |
|
||||||
|
|
||||||
#### `address_current_token_balances_in_batches.sql` |
|
||||||
|
|
||||||
Is responsible to populate a new table using the `token_balances` table information. |
|
||||||
|
|
||||||
#### `internal_transaction_update_in_batches.sql` |
|
||||||
|
|
||||||
Is responsible to migrate data from the `transactions` table to the `internal_transactions` one in order to improve the application listing performance; |
|
||||||
|
|
||||||
#### `transaction_update_in_baches.sql` |
|
||||||
|
|
||||||
Parity call traces contain the input, but it was not put in the internal_transactions_params. |
|
||||||
Enforce input and call_type being non-NULL for calls in new constraints on internal_transactions. |
|
@ -0,0 +1,334 @@ |
|||||||
|
$network-selector-overlay-background: $modal-overlay-color !default; |
||||||
|
$network-selector-close-color: $primary !default; |
||||||
|
$network-selector-horizontal-padding: 28px; |
||||||
|
$btn-network-selector-load-more-background: #fff !default; |
||||||
|
$btn-network-selector-load-more-color: $primary !default; |
||||||
|
$network-selector-search-input-color: #a3a9b5 !default; |
||||||
|
$network-selector-tab-active-border-color: $primary !default; |
||||||
|
$network-selector-item-icon-dimensions: 30px !default; |
||||||
|
|
||||||
|
.network-selector-visible { |
||||||
|
bottom: 0; |
||||||
|
left: 0; |
||||||
|
position: fixed; |
||||||
|
right: 0; |
||||||
|
top: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-overlay { |
||||||
|
background-color: rgba($network-selector-overlay-background, 0.9); |
||||||
|
bottom: 0; |
||||||
|
display: none; |
||||||
|
left: 0; |
||||||
|
position: fixed; |
||||||
|
right: 0; |
||||||
|
top: 0; |
||||||
|
z-index: 123; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-overlay-close { |
||||||
|
position: absolute; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
right: 0; |
||||||
|
bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-wrapper { |
||||||
|
display: flex; |
||||||
|
height: 100%; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector { |
||||||
|
background-color: #fff; |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
flex-grow: 1; |
||||||
|
flex-shrink: 1; |
||||||
|
margin-left: auto; |
||||||
|
max-width: 398px; |
||||||
|
min-width: 0; |
||||||
|
padding-top: 28px; |
||||||
|
position: relative; |
||||||
|
transition: right 0.25s ease-out; |
||||||
|
z-index: 2; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-close { |
||||||
|
flex-shrink: 0; |
||||||
|
padding: 0 $network-selector-horizontal-padding; |
||||||
|
margin: 0 0 8px; |
||||||
|
|
||||||
|
svg { |
||||||
|
cursor: pointer; |
||||||
|
display: block; |
||||||
|
margin-left: auto; |
||||||
|
} |
||||||
|
|
||||||
|
path { |
||||||
|
fill: $network-selector-close-color; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-text-container { |
||||||
|
flex-shrink: 0; |
||||||
|
margin: 0 0 15px; |
||||||
|
padding: 0 $network-selector-horizontal-padding; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-title { |
||||||
|
color: #333; |
||||||
|
font-size: 18px; |
||||||
|
font-weight: normal; |
||||||
|
line-height: 1.2; |
||||||
|
margin: 0 0 10px; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-text { |
||||||
|
color: #a3a9b5; |
||||||
|
font-size: 12px; |
||||||
|
font-weight: normal; |
||||||
|
line-height: 1.67; |
||||||
|
margin: 0; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-search-container { |
||||||
|
align-items: center; |
||||||
|
background-color: #f5f6fa; |
||||||
|
display: flex; |
||||||
|
flex-shrink: 0; |
||||||
|
height: 62px; |
||||||
|
margin: 0; |
||||||
|
padding: 0 $network-selector-horizontal-padding; |
||||||
|
|
||||||
|
path { |
||||||
|
flex-grow: 0; |
||||||
|
flex-shrink: 0; |
||||||
|
fill: $network-selector-search-input-color; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-search-input { |
||||||
|
background-color: transparent; |
||||||
|
border-color: transparent; |
||||||
|
color: #333; |
||||||
|
flex-grow: 1; |
||||||
|
font-size: 14px; |
||||||
|
font-weight: 600; |
||||||
|
height: 100%; |
||||||
|
outline: none; |
||||||
|
padding: 0 20px 0 10px; |
||||||
|
|
||||||
|
&[placeholder]{ |
||||||
|
color: $network-selector-search-input-color !important; |
||||||
|
} |
||||||
|
&::-webkit-input-placeholder { /* Chrome/Opera/Safari */ |
||||||
|
color: $network-selector-search-input-color !important; |
||||||
|
} |
||||||
|
&::-moz-placeholder { /* Firefox 19+ */ |
||||||
|
color: $network-selector-search-input-color !important; |
||||||
|
} |
||||||
|
&:-ms-input-placeholder { /* IE 10+ */ |
||||||
|
color: $network-selector-search-input-color !important; |
||||||
|
} |
||||||
|
&:-moz-placeholder { /* Firefox 18- */ |
||||||
|
color: $network-selector-search-input-color !important; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-tabs-container { |
||||||
|
border-bottom: 1px solid $base-border-color; |
||||||
|
display: flex; |
||||||
|
flex-shrink: 0; |
||||||
|
margin: 0 $network-selector-horizontal-padding; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-tab { |
||||||
|
color: #a3a9b5; |
||||||
|
cursor: pointer; |
||||||
|
flex-shrink: 1; |
||||||
|
font-size: 14px; |
||||||
|
font-weight: 600; |
||||||
|
line-height: 1.2; |
||||||
|
min-width: 0; |
||||||
|
padding: 20px 18px 15px; |
||||||
|
position: relative; |
||||||
|
text-align: center; |
||||||
|
user-select: none; |
||||||
|
white-space: nowrap; |
||||||
|
|
||||||
|
&:hover { |
||||||
|
color: #333; |
||||||
|
} |
||||||
|
|
||||||
|
&.active { |
||||||
|
color: #333; |
||||||
|
cursor: default; |
||||||
|
|
||||||
|
&::after { |
||||||
|
background-color: $network-selector-tab-active-border-color; |
||||||
|
border-top-left-radius: 4px; |
||||||
|
border-top-right-radius: 4px; |
||||||
|
bottom: 0; |
||||||
|
content: ""; |
||||||
|
height: 4px; |
||||||
|
left: 0; |
||||||
|
position: absolute; |
||||||
|
right: 0; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-tab-content { |
||||||
|
display: none; |
||||||
|
|
||||||
|
&.active { |
||||||
|
display: block; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-item { |
||||||
|
border-bottom: 1px solid $base-border-color; |
||||||
|
display: flex; |
||||||
|
position: relative; |
||||||
|
|
||||||
|
.radio { |
||||||
|
cursor: pointer; |
||||||
|
margin: 0 15px 0 0; |
||||||
|
|
||||||
|
input[type="radio"] { |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.radio-icon { |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
|
||||||
|
&:last-child { |
||||||
|
border-bottom: none; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-item-url { |
||||||
|
align-items: center; |
||||||
|
cursor: pointer; |
||||||
|
display: flex; |
||||||
|
flex-grow: 1; |
||||||
|
margin: 0; |
||||||
|
padding: 20px 0; |
||||||
|
|
||||||
|
&:hover { |
||||||
|
.network-selector-item-type { |
||||||
|
color: #333; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-item-icon { |
||||||
|
background-color: #dfdfdf; |
||||||
|
background-position: 50% 50%; |
||||||
|
background-repeat: no-repeat; |
||||||
|
background-size: contain; |
||||||
|
border-radius: 50%; |
||||||
|
flex-grow: 0; |
||||||
|
flex-shrink: 0; |
||||||
|
height: $network-selector-item-icon-dimensions; |
||||||
|
margin: 0 15px 0 0; |
||||||
|
width: $network-selector-item-icon-dimensions; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-item-title { |
||||||
|
color: #333; |
||||||
|
flex-grow: 1; |
||||||
|
font-size: 14px; |
||||||
|
font-weight: normal; |
||||||
|
line-height: 1.2; |
||||||
|
overflow: hidden; |
||||||
|
text-align: left; |
||||||
|
text-overflow: ellipsis; |
||||||
|
user-select: none; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-item-type { |
||||||
|
color: #a3a9b5; |
||||||
|
flex-shrink: 0; |
||||||
|
font-size: 14px; |
||||||
|
font-weight: normal; |
||||||
|
line-height: 1.2; |
||||||
|
padding-left: 10px; |
||||||
|
text-align: right; |
||||||
|
user-select: none; |
||||||
|
white-space: nowrap; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-item-content { |
||||||
|
align-items: center; |
||||||
|
display: flex; |
||||||
|
flex-grow: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-networks-container { |
||||||
|
flex-grow: 1; |
||||||
|
flex-shrink: 1; |
||||||
|
min-height: 100px; |
||||||
|
overflow: auto; |
||||||
|
padding: 0 $network-selector-horizontal-padding; |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-load-more-container { |
||||||
|
flex-shrink: 1; |
||||||
|
padding: 0 $network-selector-horizontal-padding; |
||||||
|
|
||||||
|
.btn-network-selector-load-more { |
||||||
|
@include btn-line($btn-network-selector-load-more-background, $btn-network-selector-load-more-color); |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-item-favorite { |
||||||
|
align-items: center; |
||||||
|
cursor: pointer; |
||||||
|
display: flex; |
||||||
|
flex-grow: 1; |
||||||
|
flex-shrink: 0; |
||||||
|
margin: 0; |
||||||
|
max-width: 36px; |
||||||
|
padding-left: 20px; |
||||||
|
position: relative; |
||||||
|
|
||||||
|
input[type="checkbox"] { |
||||||
|
cursor: pointer; |
||||||
|
height: 100%; |
||||||
|
opacity: 0; |
||||||
|
position: absolute; |
||||||
|
width: 100%; |
||||||
|
z-index: 5; |
||||||
|
|
||||||
|
&:checked + svg { |
||||||
|
position: relative; |
||||||
|
z-index: 1; |
||||||
|
|
||||||
|
path { |
||||||
|
fill: #ffb20d; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
&:hover { |
||||||
|
path { |
||||||
|
fill: rgba(#ffb20d, 0.4); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.network-selector-tab-content-empty { |
||||||
|
font-size: 16px; |
||||||
|
font-weight: 600; |
||||||
|
padding: 40px; |
||||||
|
text-align: center; |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
$radio-color: $primary !default; |
||||||
|
$radio-dimensions: 20px !default; |
||||||
|
|
||||||
|
.radio { |
||||||
|
align-items: center; |
||||||
|
display: flex; |
||||||
|
position: relative; |
||||||
|
|
||||||
|
input[type="radio"] { |
||||||
|
height: 100%; |
||||||
|
opacity: 0; |
||||||
|
position: absolute; |
||||||
|
width: 100%; |
||||||
|
z-index: 5; |
||||||
|
|
||||||
|
&:checked + .radio-icon::before { |
||||||
|
background-color: $radio-color; |
||||||
|
border-radius: 50%; |
||||||
|
content: ""; |
||||||
|
height: 12px; |
||||||
|
left: 50%; |
||||||
|
position: absolute; |
||||||
|
top: 50%; |
||||||
|
transform: translateX(-50%) translateY(-50%); |
||||||
|
width: 12px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.radio-icon { |
||||||
|
border: 1px solid $base-border-color; |
||||||
|
border-radius: 50%; |
||||||
|
flex-grow: 0; |
||||||
|
flex-shrink: 0; |
||||||
|
height: $radio-dimensions; |
||||||
|
margin: 0 10px 0 0; |
||||||
|
position: relative; |
||||||
|
width: $radio-dimensions; |
||||||
|
z-index: 1; |
||||||
|
} |
||||||
|
|
||||||
|
.radio-text { |
||||||
|
font-size: 14px; |
||||||
|
font-weight: normal; |
||||||
|
line-height: 1.2; |
||||||
|
position: relative; |
||||||
|
white-space: nowrap; |
||||||
|
z-index: 1; |
||||||
|
} |
||||||
|
} |
@ -1,6 +1,45 @@ |
|||||||
.transaction-details-address { |
.transaction-bottom-panel { |
||||||
font-size: 12px; |
display: flex; |
||||||
font-weight: bold; |
flex-direction: column; |
||||||
line-height: 1.2; |
@media (min-width: 768px) { |
||||||
margin: 0 0 12px; |
flex-direction: row; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: flex-end; |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
|
.transaction-bottom-panel { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
@media (min-width: 768px) { |
||||||
|
flex-direction: row; |
||||||
|
justify-content: space-between; |
||||||
|
align-items: flex-end; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.download-all-transactions { |
||||||
|
text-align: center; |
||||||
|
color: #a3a9b5; |
||||||
|
font-size: 13px; |
||||||
|
margin-top: 10px; |
||||||
|
@media (min-width: 768px) { |
||||||
|
margin-top: 30px; |
||||||
|
} |
||||||
|
.download-all-transactions-link { |
||||||
|
text-decoration: none; |
||||||
|
svg { |
||||||
|
position: relative; |
||||||
|
margin-left: 2px; |
||||||
|
top: -3px; |
||||||
|
path { |
||||||
|
fill: $primary; |
||||||
|
} |
||||||
|
} |
||||||
|
&:hover { |
||||||
|
span { |
||||||
|
text-decoration: underline; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,77 @@ |
|||||||
|
import $ from 'jquery' |
||||||
|
|
||||||
|
$(function () { |
||||||
|
const mainBody = $('body') |
||||||
|
const showNetworkSelector = $('.js-show-network-selector') |
||||||
|
const hideNetworkSelector = $('.js-network-selector-close') |
||||||
|
const hideNetworkSelectorOverlay = $('.js-network-selector-overlay-close') |
||||||
|
const networkSelector = $('.js-network-selector') |
||||||
|
const networkSelectorOverlay = $('.js-network-selector-overlay') |
||||||
|
const networkSelectorTab = $('.js-network-selector-tab') |
||||||
|
const networkSelectorTabContent = $('.js-network-selector-tab-content') |
||||||
|
const networkSelectorItemURL = $('.js-network-selector-item-url') |
||||||
|
const FADE_IN_DELAY = 250 |
||||||
|
|
||||||
|
showNetworkSelector.on('click', (e) => { |
||||||
|
e.preventDefault() |
||||||
|
openNetworkSelector() |
||||||
|
}) |
||||||
|
|
||||||
|
hideNetworkSelector.on('click', (e) => { |
||||||
|
e.preventDefault() |
||||||
|
closeNetworkSelector() |
||||||
|
}) |
||||||
|
|
||||||
|
hideNetworkSelectorOverlay.on('click', (e) => { |
||||||
|
e.preventDefault() |
||||||
|
closeNetworkSelector() |
||||||
|
}) |
||||||
|
|
||||||
|
networkSelectorTab.on('click', function (e) { |
||||||
|
e.preventDefault() |
||||||
|
setNetworkTab($(this)) |
||||||
|
}) |
||||||
|
|
||||||
|
networkSelectorItemURL.on('click', function (e) { |
||||||
|
window.location = $(this).attr('network-selector-item-url') |
||||||
|
}) |
||||||
|
|
||||||
|
let setNetworkTab = (currentTab) => { |
||||||
|
if (currentTab.hasClass('active')) return |
||||||
|
|
||||||
|
networkSelectorTab.removeClass('active') |
||||||
|
currentTab.addClass('active') |
||||||
|
networkSelectorTabContent.removeClass('active') |
||||||
|
$(`[network-selector-tab="${currentTab.attr('network-selector-tab-filter')}"]`).addClass('active') |
||||||
|
} |
||||||
|
|
||||||
|
let openNetworkSelector = () => { |
||||||
|
mainBody.addClass('network-selector-visible') |
||||||
|
networkSelectorOverlay.fadeIn(FADE_IN_DELAY) |
||||||
|
setNetworkSelectorVisiblePosition() |
||||||
|
} |
||||||
|
|
||||||
|
let closeNetworkSelector = () => { |
||||||
|
mainBody.removeClass('network-selector-visible') |
||||||
|
networkSelectorOverlay.fadeOut(FADE_IN_DELAY) |
||||||
|
setNetworkSelectorHiddenPosition() |
||||||
|
} |
||||||
|
|
||||||
|
let getNetworkSelectorWidth = () => { |
||||||
|
return parseInt(networkSelector.css('width')) || parseInt(networkSelector.css('max-width')) |
||||||
|
} |
||||||
|
|
||||||
|
let setNetworkSelectorHiddenPosition = () => { |
||||||
|
return networkSelector.css({ 'right': `-${getNetworkSelectorWidth()}px` }) |
||||||
|
} |
||||||
|
|
||||||
|
let setNetworkSelectorVisiblePosition = () => { |
||||||
|
return networkSelector.css({ 'right': '0' }) |
||||||
|
} |
||||||
|
|
||||||
|
let init = () => { |
||||||
|
setNetworkSelectorHiddenPosition() |
||||||
|
} |
||||||
|
|
||||||
|
init() |
||||||
|
}) |
@ -0,0 +1,58 @@ |
|||||||
|
import $ from 'jquery' |
||||||
|
|
||||||
|
var favoritesContainer = $('.js-favorites-tab') |
||||||
|
var favoritesNetworksUrls = [] |
||||||
|
|
||||||
|
if (localStorage.getItem('favoritesNetworksUrls') === null) { |
||||||
|
localStorage.setItem('favoritesNetworksUrls', JSON.stringify(favoritesNetworksUrls)) |
||||||
|
} else { |
||||||
|
favoritesNetworksUrls = JSON.parse(localStorage.getItem('favoritesNetworksUrls')) |
||||||
|
} |
||||||
|
|
||||||
|
$(document).on('change', ".network-selector-item-favorite input[type='checkbox']", function () { |
||||||
|
var networkUrl = $(this).attr('data-url') |
||||||
|
var thisStatus = $(this).is(':checked') |
||||||
|
var workWith = $(".network-selector-item[data-url='" + networkUrl + "'") |
||||||
|
|
||||||
|
// Add new checkbox status to same network in another tabs
|
||||||
|
$(".network-selector-item-favorite input[data-url='" + networkUrl + "']").prop('checked', thisStatus) |
||||||
|
|
||||||
|
// Clone
|
||||||
|
var parent = $(".network-selector-item[data-url='" + networkUrl + "'").clone() |
||||||
|
|
||||||
|
// Push or remove favorite networks to array
|
||||||
|
var found = $.inArray(networkUrl, favoritesNetworksUrls) |
||||||
|
if (found < 0 && thisStatus === true) { |
||||||
|
favoritesNetworksUrls.push(networkUrl) |
||||||
|
} else { |
||||||
|
var index = favoritesNetworksUrls.indexOf(networkUrl) |
||||||
|
if (index !== -1) { |
||||||
|
favoritesNetworksUrls.splice(index, 1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Push to localstorage
|
||||||
|
var willBePushed = JSON.stringify(favoritesNetworksUrls) |
||||||
|
localStorage.setItem('favoritesNetworksUrls', willBePushed) |
||||||
|
|
||||||
|
// Append or remove item from 'favorites' tab
|
||||||
|
if (thisStatus === true) { |
||||||
|
favoritesContainer.append(parent[0]) |
||||||
|
$('.js-favorites-tab .network-selector-tab-content-empty').hide() |
||||||
|
} else { |
||||||
|
var willRemoved = favoritesContainer.find(workWith) |
||||||
|
willRemoved.remove() |
||||||
|
if (favoritesNetworksUrls.length === 0) { |
||||||
|
$('.js-favorites-tab .network-selector-tab-content-empty').show() |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
if (favoritesNetworksUrls.length > 0) { |
||||||
|
$('.js-favorites-tab .network-selector-tab-content-empty').hide() |
||||||
|
for (var i = 0; i < favoritesNetworksUrls.length + 1; i++) { |
||||||
|
$(".network-selector-item[data-url='" + favoritesNetworksUrls[i] + "'").find('input[data-url]').prop('checked', true) |
||||||
|
var parent = $(".network-selector-item[data-url='" + favoritesNetworksUrls[i] + "'").clone() |
||||||
|
favoritesContainer.append(parent[0]) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
import $ from 'jquery' |
||||||
|
|
||||||
|
$(document).click(function (event) { |
||||||
|
var clickover = $(event.target) |
||||||
|
var _opened = $('.navbar-collapse').hasClass('show') |
||||||
|
if (_opened === true && $('.navbar').find(clickover).length < 1) { |
||||||
|
$('.navbar-toggler').click() |
||||||
|
} |
||||||
|
}) |
@ -0,0 +1,21 @@ |
|||||||
|
import $ from 'jquery' |
||||||
|
|
||||||
|
var networkSearchInput = $('.network-selector-search-input') |
||||||
|
var networkSearchInputVal = '' |
||||||
|
|
||||||
|
$(networkSearchInput).on('input', function () { |
||||||
|
networkSearchInputVal = $(this).val() |
||||||
|
|
||||||
|
$.expr[':'].Contains = $.expr.createPseudo(function (arg) { |
||||||
|
return function (elem) { |
||||||
|
return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0 |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
if (networkSearchInputVal === '') { |
||||||
|
$('.network-selector-item').show() |
||||||
|
} else { |
||||||
|
$('.network-selector-item').hide() |
||||||
|
$(".network-selector-item:Contains('" + networkSearchInputVal + "')").show() |
||||||
|
} |
||||||
|
}) |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 868 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 6.6 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 916 B |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 916 B |
After Width: | Height: | Size: 2.7 KiB |
@ -0,0 +1,53 @@ |
|||||||
|
<div class="network-selector-overlay js-network-selector-overlay"> |
||||||
|
<div class="network-selector-overlay-close js-network-selector-overlay-close"></div> |
||||||
|
<div class="network-selector-wrapper"> |
||||||
|
<div class="network-selector js-network-selector"> |
||||||
|
<div class="network-selector-close js-network-selector-close"> |
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13"> |
||||||
|
<path fill-rule="evenodd" d="M7.881 6.5l4.834 4.834a.977.977 0 0 1-1.381 1.381L6.5 7.881l-4.834 4.834a.977.977 0 0 1-1.381-1.381L5.119 6.5.285 1.666A.977.977 0 0 1 1.666.285L6.5 5.119 11.334.285a.977.977 0 0 1 1.381 1.381L7.881 6.5z"/> |
||||||
|
</svg> |
||||||
|
</div> |
||||||
|
<div class="network-selector-text-container"> |
||||||
|
<h1 class="network-selector-title"><%= gettext("Change Network") %></h1> |
||||||
|
<p class="network-selector-text"><%= gettext("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore.") %></p> |
||||||
|
</div> |
||||||
|
<form class="network-selector-search-container"> |
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="17"> |
||||||
|
<path 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.285-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> |
||||||
|
<input class="network-selector-search-input" type="text" placeholder='<%= gettext("Search network") %>' /> |
||||||
|
</form> |
||||||
|
<div class="network-selector-tabs-container"> |
||||||
|
<div class="network-selector-tab js-network-selector-tab active" network-selector-tab-filter="all"><%= gettext("All") %></div> |
||||||
|
<div class="network-selector-tab js-network-selector-tab" network-selector-tab-filter="mainnet"><%= gettext("Mainnet") %></div> |
||||||
|
<div class="network-selector-tab js-network-selector-tab" network-selector-tab-filter="testnet"><%= gettext("Testnet") %></div> |
||||||
|
<div class="network-selector-tab js-network-selector-tab" network-selector-tab-filter="favorites"><%= gettext("Favorites") %></div> |
||||||
|
</div> |
||||||
|
<div class="network-selector-networks-container"> |
||||||
|
<% main_nets = dropdown_main_nets() %> |
||||||
|
<% test_nets = dropdown_test_nets() %> |
||||||
|
<div class="network-selector-tab-content js-network-selector-tab-content active" network-selector-tab="all"> |
||||||
|
<%= for %{url: url, title: title} <- main_nets do %> |
||||||
|
<%= render BlockScoutWeb.LayoutView, "_network_selector_item.html", title: title, url: url, tab_type: "Mainnet" %> |
||||||
|
<% end %> |
||||||
|
<%= for %{url: url, title: title} <- test_nets do %> |
||||||
|
<%= render BlockScoutWeb.LayoutView, "_network_selector_item.html", title: title, url: url, tab_type: "Testnet" %> |
||||||
|
<% end %> |
||||||
|
</div> |
||||||
|
<div class="network-selector-tab-content js-network-selector-tab-content" network-selector-tab="mainnet"> |
||||||
|
<%= for %{url: url, title: title} <- main_nets do %> |
||||||
|
<%= render BlockScoutWeb.LayoutView, "_network_selector_item.html", title: title, url: url, tab_type: "Mainnet" %> |
||||||
|
<% end %> |
||||||
|
</div> |
||||||
|
<div class="network-selector-tab-content js-network-selector-tab-content" network-selector-tab="testnet"> |
||||||
|
<%= for %{url: url, title: title} <- test_nets do %> |
||||||
|
<%= render BlockScoutWeb.LayoutView, "_network_selector_item.html", title: title, url: url, tab_type: "Testnet" %> |
||||||
|
<% end %> |
||||||
|
</div> |
||||||
|
<div class="network-selector-tab-content js-network-selector-tab-content js-favorites-tab" network-selector-tab="favorites"> |
||||||
|
<div class="network-selector-tab-content-empty">No content.</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
@ -0,0 +1,25 @@ |
|||||||
|
<div class="network-selector-item" data-url="<%= @url %>" data-name="<%= @title %>"> |
||||||
|
<label class="network-selector-item-url js-network-selector-item-url" network-selector-item-url="<%= @url %>"> |
||||||
|
<span class="radio"> |
||||||
|
<input type="radio" name="networkSelectorItem" <%= if @title == subnetwork_title() do %> checked="true" <% end %> /> |
||||||
|
<span class="radio-icon"></span> |
||||||
|
</span> |
||||||
|
<span class="network-selector-item-content"> |
||||||
|
<span class='network-selector-item-icon network-selector-item-icon-<%= String.downcase(String.replace(@title, " ", "-")) %>' style="background-image: url('/images/network-selector-icons/<%= String.downcase(String.replace(@title, " ", "-")) %>.png');"></span> |
||||||
|
<span class="network-selector-item-title"> |
||||||
|
<%= @title %> |
||||||
|
</span> |
||||||
|
<%= if @tab_type do %> |
||||||
|
<span class="network-selector-item-type"> |
||||||
|
<%= @tab_type %> |
||||||
|
</span> |
||||||
|
<% end %> |
||||||
|
</span> |
||||||
|
</label> |
||||||
|
<label class="network-selector-item-favorite"> |
||||||
|
<input type="checkbox" data-url="<%= @url %>" /> |
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15"> |
||||||
|
<path fill="#E2E5EC" fill-rule="evenodd" d="M15.647 6.795c.315-.3.426-.741.29-1.151a1.135 1.135 0 0 0-.926-.764l-3.871-.551a.501.501 0 0 1-.381-.271L9.028.624A1.143 1.143 0 0 0 8-.001c-.44 0-.834.24-1.028.625L5.24 4.059a.506.506 0 0 1-.381.271l-3.871.55c-.435.062-.79.355-.926.765-.136.409-.025.85.29 1.15l2.801 2.673a.492.492 0 0 1 .146.439l-.661 3.774c-.058.333.031.656.25.911.342.397.937.518 1.414.272l3.462-1.782a.53.53 0 0 1 .471 0l3.463 1.782a1.16 1.16 0 0 0 1.413-.272c.22-.255.309-.579.25-.911L12.7 9.907a.489.489 0 0 1 .146-.439l2.801-2.673z"/> |
||||||
|
</svg> |
||||||
|
</label> |
||||||
|
</div> |
@ -0,0 +1,119 @@ |
|||||||
|
defmodule Explorer.Chain.AddressTokenTransferCsvExporter do |
||||||
|
@moduledoc """ |
||||||
|
Exports token transfers to a csv file. |
||||||
|
""" |
||||||
|
|
||||||
|
alias Explorer.{Chain, PagingOptions} |
||||||
|
alias Explorer.Chain.{Address, TokenTransfer, Transaction} |
||||||
|
alias NimbleCSV.RFC4180 |
||||||
|
|
||||||
|
@necessity_by_association [ |
||||||
|
necessity_by_association: %{ |
||||||
|
[created_contract_address: :names] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
[token_transfers: :token] => :optional, |
||||||
|
[token_transfers: :to_address] => :optional, |
||||||
|
[token_transfers: :from_address] => :optional, |
||||||
|
[token_transfers: :token_contract_address] => :optional, |
||||||
|
:block => :required |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
@page_size 150 |
||||||
|
@paging_options %PagingOptions{page_size: @page_size + 1} |
||||||
|
|
||||||
|
def export(address) do |
||||||
|
address |
||||||
|
|> fetch_all_transactions(@paging_options) |
||||||
|
|> to_token_transfers() |
||||||
|
|> to_csv_format(address) |
||||||
|
|> dump_to_stream() |
||||||
|
end |
||||||
|
|
||||||
|
defp fetch_all_transactions(address, paging_options, acc \\ []) do |
||||||
|
options = Keyword.merge(@necessity_by_association, paging_options: paging_options) |
||||||
|
|
||||||
|
transactions = |
||||||
|
address |
||||||
|
|> Chain.address_to_transactions_with_rewards(options) |
||||||
|
|> Enum.filter(fn transaction -> Enum.count(transaction.token_transfers) > 0 end) |
||||||
|
|
||||||
|
new_acc = transactions ++ acc |
||||||
|
|
||||||
|
case Enum.split(transactions, @page_size) do |
||||||
|
{_transactions, [%Transaction{block_number: block_number, index: index}]} -> |
||||||
|
new_paging_options = %{@paging_options | key: {block_number, index}} |
||||||
|
fetch_all_transactions(address, new_paging_options, new_acc) |
||||||
|
|
||||||
|
{_, []} -> |
||||||
|
new_acc |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp to_token_transfers(transactions) do |
||||||
|
transactions |
||||||
|
|> Enum.flat_map(fn transaction -> |
||||||
|
transaction.token_transfers |
||||||
|
|> Enum.map(fn transfer -> %{transfer | transaction: transaction} end) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
defp dump_to_stream(transactions) do |
||||||
|
transactions |
||||||
|
|> RFC4180.dump_to_stream() |
||||||
|
end |
||||||
|
|
||||||
|
defp to_csv_format(token_transfers, address) do |
||||||
|
row_names = [ |
||||||
|
"TxHash", |
||||||
|
"BlockNumber", |
||||||
|
"UnixTimestamp", |
||||||
|
"FromAddress", |
||||||
|
"ToAddress", |
||||||
|
"TokenContractAddress", |
||||||
|
"Type", |
||||||
|
"TokenSymbol", |
||||||
|
"TokensTransferred", |
||||||
|
"TransactionFee", |
||||||
|
"Status", |
||||||
|
"ErrCode" |
||||||
|
] |
||||||
|
|
||||||
|
token_transfer_lists = |
||||||
|
token_transfers |
||||||
|
|> Stream.map(fn token_transfer -> |
||||||
|
[ |
||||||
|
to_string(token_transfer.transaction_hash), |
||||||
|
token_transfer.transaction.block_number, |
||||||
|
token_transfer.transaction.block.timestamp, |
||||||
|
token_transfer.from_address |> to_string() |> String.downcase(), |
||||||
|
token_transfer.to_address |> to_string() |> String.downcase(), |
||||||
|
token_transfer.token_contract_address |> to_string() |> String.downcase(), |
||||||
|
type(token_transfer, address), |
||||||
|
token_transfer.token.symbol, |
||||||
|
token_transfer.amount, |
||||||
|
fee(token_transfer.transaction), |
||||||
|
token_transfer.transaction.status, |
||||||
|
token_transfer.transaction.error |
||||||
|
] |
||||||
|
end) |
||||||
|
|
||||||
|
Stream.concat([row_names], token_transfer_lists) |
||||||
|
end |
||||||
|
|
||||||
|
defp type(%TokenTransfer{from_address_hash: from_address}, %Address{hash: from_address}), do: "OUT" |
||||||
|
|
||||||
|
defp type(%TokenTransfer{to_address_hash: to_address}, %Address{hash: to_address}), do: "IN" |
||||||
|
|
||||||
|
defp type(_, _), do: "" |
||||||
|
|
||||||
|
defp fee(transaction) do |
||||||
|
transaction |
||||||
|
|> Chain.fee(:wei) |
||||||
|
|> case do |
||||||
|
{:actual, value} -> value |
||||||
|
{:maximum, value} -> "Max of #{value}" |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,139 @@ |
|||||||
|
defmodule Explorer.Chain.AddressTransactionCsvExporter do |
||||||
|
@moduledoc """ |
||||||
|
Exports transactions to a csv file. |
||||||
|
""" |
||||||
|
|
||||||
|
import Ecto.Query, |
||||||
|
only: [ |
||||||
|
from: 2 |
||||||
|
] |
||||||
|
|
||||||
|
alias Explorer.{Chain, Market, PagingOptions, Repo} |
||||||
|
alias Explorer.Market.MarketHistory |
||||||
|
alias Explorer.Chain.{Address, Transaction, Wei} |
||||||
|
alias Explorer.ExchangeRates.Token |
||||||
|
alias NimbleCSV.RFC4180 |
||||||
|
|
||||||
|
@necessity_by_association [ |
||||||
|
necessity_by_association: %{ |
||||||
|
[created_contract_address: :names] => :optional, |
||||||
|
[from_address: :names] => :optional, |
||||||
|
[to_address: :names] => :optional, |
||||||
|
[token_transfers: :token] => :optional, |
||||||
|
[token_transfers: :to_address] => :optional, |
||||||
|
[token_transfers: :from_address] => :optional, |
||||||
|
[token_transfers: :token_contract_address] => :optional, |
||||||
|
:block => :required |
||||||
|
} |
||||||
|
] |
||||||
|
|
||||||
|
@page_size 150 |
||||||
|
|
||||||
|
@paging_options %PagingOptions{page_size: @page_size + 1} |
||||||
|
|
||||||
|
@spec export(Address.t()) :: Enumerable.t() |
||||||
|
def export(address) do |
||||||
|
exchange_rate = Market.get_exchange_rate(Explorer.coin()) || Token.null() |
||||||
|
|
||||||
|
address |
||||||
|
|> fetch_all_transactions(@paging_options) |
||||||
|
|> to_csv_format(address, exchange_rate) |
||||||
|
|> dump_to_stream() |
||||||
|
end |
||||||
|
|
||||||
|
defp fetch_all_transactions(address, paging_options, acc \\ []) do |
||||||
|
options = Keyword.merge(@necessity_by_association, paging_options: paging_options) |
||||||
|
|
||||||
|
transactions = Chain.address_to_transactions_with_rewards(address, options) |
||||||
|
|
||||||
|
new_acc = transactions ++ acc |
||||||
|
|
||||||
|
case Enum.split(transactions, @page_size) do |
||||||
|
{_transactions, [%Transaction{block_number: block_number, index: index}]} -> |
||||||
|
new_paging_options = %{@paging_options | key: {block_number, index}} |
||||||
|
fetch_all_transactions(address, new_paging_options, new_acc) |
||||||
|
|
||||||
|
{_, []} -> |
||||||
|
new_acc |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp dump_to_stream(transactions) do |
||||||
|
transactions |
||||||
|
|> RFC4180.dump_to_stream() |
||||||
|
end |
||||||
|
|
||||||
|
defp to_csv_format(transactions, address, exchange_rate) do |
||||||
|
row_names = [ |
||||||
|
"TxHash", |
||||||
|
"BlockNumber", |
||||||
|
"UnixTimestamp", |
||||||
|
"FromAddress", |
||||||
|
"ToAddress", |
||||||
|
"ContractAddress", |
||||||
|
"Type", |
||||||
|
"Value", |
||||||
|
"Fee", |
||||||
|
"Status", |
||||||
|
"ErrCode", |
||||||
|
"CurrentPrice", |
||||||
|
"TxDateOpeningPrice", |
||||||
|
"TxDateClosingPrice" |
||||||
|
] |
||||||
|
|
||||||
|
transaction_lists = |
||||||
|
transactions |
||||||
|
|> Stream.map(fn transaction -> |
||||||
|
{opening_price, closing_price} = price_at_date(transaction.block.timestamp) |
||||||
|
|
||||||
|
[ |
||||||
|
to_string(transaction.hash), |
||||||
|
transaction.block_number, |
||||||
|
transaction.block.timestamp, |
||||||
|
to_string(transaction.from_address), |
||||||
|
to_string(transaction.to_address), |
||||||
|
to_string(transaction.created_contract_address), |
||||||
|
type(transaction, address), |
||||||
|
Wei.to(transaction.value, :wei), |
||||||
|
fee(transaction), |
||||||
|
transaction.status, |
||||||
|
transaction.error, |
||||||
|
exchange_rate.usd_value, |
||||||
|
opening_price, |
||||||
|
closing_price |
||||||
|
] |
||||||
|
end) |
||||||
|
|
||||||
|
Stream.concat([row_names], transaction_lists) |
||||||
|
end |
||||||
|
|
||||||
|
defp type(%Transaction{from_address_hash: from_address}, %Address{hash: from_address}), do: "OUT" |
||||||
|
|
||||||
|
defp type(%Transaction{to_address_hash: to_address}, %Address{hash: to_address}), do: "IN" |
||||||
|
|
||||||
|
defp type(_, _), do: "" |
||||||
|
|
||||||
|
defp fee(transaction) do |
||||||
|
transaction |
||||||
|
|> Chain.fee(:wei) |
||||||
|
|> case do |
||||||
|
{:actual, value} -> value |
||||||
|
{:maximum, value} -> "Max of #{value}" |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp price_at_date(datetime) do |
||||||
|
date = DateTime.to_date(datetime) |
||||||
|
|
||||||
|
query = |
||||||
|
from( |
||||||
|
mh in MarketHistory, |
||||||
|
where: mh.date == ^date |
||||||
|
) |
||||||
|
|
||||||
|
case Repo.one(query) do |
||||||
|
nil -> {nil, nil} |
||||||
|
price -> {price.opening_price, price.closing_price} |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,103 @@ |
|||||||
|
defmodule Explorer.Chain.Supply.RSK do |
||||||
|
@moduledoc """ |
||||||
|
Defines the supply API for calculating supply for coins from RSK. |
||||||
|
""" |
||||||
|
|
||||||
|
use Explorer.Chain.Supply |
||||||
|
|
||||||
|
import Ecto.Query, only: [from: 2] |
||||||
|
|
||||||
|
alias Explorer.Chain.Address.CoinBalance |
||||||
|
alias Explorer.Chain.{Block, Wei} |
||||||
|
alias Explorer.ExchangeRates.Token |
||||||
|
alias Explorer.{Market, Repo} |
||||||
|
|
||||||
|
def market_cap(exchange_rate) do |
||||||
|
circulating() * exchange_rate.usd_value |
||||||
|
end |
||||||
|
|
||||||
|
@doc "Equivalent to getting the circulating value " |
||||||
|
def supply_for_days(days) do |
||||||
|
now = Timex.now() |
||||||
|
|
||||||
|
balances_query = |
||||||
|
from(balance in CoinBalance, |
||||||
|
join: block in Block, |
||||||
|
on: block.number == balance.block_number, |
||||||
|
where: block.consensus == true, |
||||||
|
where: balance.address_hash == ^"0x0000000000000000000000000000000001000006", |
||||||
|
where: block.timestamp > ^Timex.shift(now, days: -days), |
||||||
|
distinct: fragment("date_trunc('day', ?)", block.timestamp), |
||||||
|
select: {block.timestamp, balance.value} |
||||||
|
) |
||||||
|
|
||||||
|
balance_before_query = |
||||||
|
from(balance in CoinBalance, |
||||||
|
join: block in Block, |
||||||
|
on: block.number == balance.block_number, |
||||||
|
where: block.consensus == true, |
||||||
|
where: balance.address_hash == ^"0x0000000000000000000000000000000001000006", |
||||||
|
where: block.timestamp <= ^Timex.shift(Timex.now(), days: -days), |
||||||
|
order_by: [desc: block.timestamp], |
||||||
|
limit: 1, |
||||||
|
select: balance.value |
||||||
|
) |
||||||
|
|
||||||
|
by_day = |
||||||
|
balances_query |
||||||
|
|> Repo.all() |
||||||
|
|> Enum.into(%{}, fn {timestamp, value} -> |
||||||
|
{Timex.to_date(timestamp), value} |
||||||
|
end) |
||||||
|
|
||||||
|
starting = Repo.one(balance_before_query) || wei!(0) |
||||||
|
|
||||||
|
result = |
||||||
|
-days..0 |
||||||
|
|> Enum.reduce({%{}, starting.value}, fn i, {days, last} -> |
||||||
|
date = |
||||||
|
now |
||||||
|
|> Timex.shift(days: i) |
||||||
|
|> Timex.to_date() |
||||||
|
|
||||||
|
case Map.get(by_day, date) do |
||||||
|
nil -> |
||||||
|
{Map.put(days, date, last), last} |
||||||
|
|
||||||
|
value -> |
||||||
|
{Map.put(days, date, value.value), value.value} |
||||||
|
end |
||||||
|
end) |
||||||
|
|> elem(0) |
||||||
|
|
||||||
|
{:ok, result} |
||||||
|
end |
||||||
|
|
||||||
|
def circulating do |
||||||
|
query = |
||||||
|
from(balance in CoinBalance, |
||||||
|
join: block in Block, |
||||||
|
on: block.number == balance.block_number, |
||||||
|
where: block.consensus == true, |
||||||
|
where: balance.address_hash == ^"0x0000000000000000000000000000000001000006", |
||||||
|
order_by: [desc: block.timestamp], |
||||||
|
limit: 1, |
||||||
|
select: balance.value |
||||||
|
) |
||||||
|
|
||||||
|
Repo.one(query) || wei!(0) |
||||||
|
end |
||||||
|
|
||||||
|
defp wei!(value) do |
||||||
|
{:ok, wei} = Wei.cast(value) |
||||||
|
wei |
||||||
|
end |
||||||
|
|
||||||
|
def total do |
||||||
|
21_000_000 |
||||||
|
end |
||||||
|
|
||||||
|
def exchange_rate do |
||||||
|
Market.get_exchange_rate(Explorer.coin()) || Token.null() |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,79 @@ |
|||||||
|
defmodule Explorer.Market.MarketHistoryCache do |
||||||
|
@moduledoc """ |
||||||
|
Caches recent market history. |
||||||
|
""" |
||||||
|
|
||||||
|
import Ecto.Query, only: [from: 2] |
||||||
|
|
||||||
|
alias Explorer.Market.MarketHistory |
||||||
|
alias Explorer.Repo |
||||||
|
|
||||||
|
@cache_name :market_history |
||||||
|
@last_update_key :last_update |
||||||
|
@history_key :history |
||||||
|
# 6 hours |
||||||
|
@cache_period 1_000 * 60 * 60 * 6 |
||||||
|
@recent_days 30 |
||||||
|
|
||||||
|
def fetch do |
||||||
|
if cache_expired?() do |
||||||
|
update_cache() |
||||||
|
else |
||||||
|
fetch_from_cache(@history_key) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def cache_name, do: @cache_name |
||||||
|
|
||||||
|
def data_key, do: @history_key |
||||||
|
|
||||||
|
def updated_at_key, do: @last_update_key |
||||||
|
|
||||||
|
def recent_days_count, do: @recent_days |
||||||
|
|
||||||
|
defp cache_expired? do |
||||||
|
updated_at = fetch_from_cache(@last_update_key) |
||||||
|
|
||||||
|
cond do |
||||||
|
is_nil(updated_at) -> true |
||||||
|
current_time() - updated_at > @cache_period -> true |
||||||
|
true -> false |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp update_cache do |
||||||
|
new_data = fetch_from_db() |
||||||
|
|
||||||
|
put_into_cache(@last_update_key, current_time()) |
||||||
|
put_into_cache(@history_key, new_data) |
||||||
|
|
||||||
|
new_data |
||||||
|
end |
||||||
|
|
||||||
|
defp fetch_from_db do |
||||||
|
day_diff = @recent_days * -1 |
||||||
|
|
||||||
|
query = |
||||||
|
from( |
||||||
|
mh in MarketHistory, |
||||||
|
where: mh.date > date_add(^Date.utc_today(), ^day_diff, "day"), |
||||||
|
order_by: [desc: mh.date] |
||||||
|
) |
||||||
|
|
||||||
|
Repo.all(query) |
||||||
|
end |
||||||
|
|
||||||
|
defp fetch_from_cache(key) do |
||||||
|
ConCache.get(@cache_name, key) |
||||||
|
end |
||||||
|
|
||||||
|
defp put_into_cache(key, value) do |
||||||
|
ConCache.put(@cache_name, key, value) |
||||||
|
end |
||||||
|
|
||||||
|
defp current_time do |
||||||
|
utc_now = DateTime.utc_now() |
||||||
|
|
||||||
|
DateTime.to_unix(utc_now, :millisecond) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,72 @@ |
|||||||
|
defmodule Explorer.Chain.AddressTokenTransferCsvExporterTest do |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
alias Explorer.Chain.AddressTokenTransferCsvExporter |
||||||
|
|
||||||
|
describe "export/1" do |
||||||
|
test "exports token transfers to csv" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
transaction = |
||||||
|
:transaction |
||||||
|
|> insert(from_address: address) |
||||||
|
|> with_block() |
||||||
|
|
||||||
|
token_transfer = insert(:token_transfer, transaction: transaction, from_address: address) |
||||||
|
|
||||||
|
[result] = |
||||||
|
address |
||||||
|
|> AddressTokenTransferCsvExporter.export() |
||||||
|
|> Enum.to_list() |
||||||
|
|> Enum.drop(1) |
||||||
|
|> Enum.map(fn [ |
||||||
|
tx_hash, |
||||||
|
_, |
||||||
|
block_number, |
||||||
|
_, |
||||||
|
timestamp, |
||||||
|
_, |
||||||
|
from_address, |
||||||
|
_, |
||||||
|
to_address, |
||||||
|
_, |
||||||
|
token_contract_address, |
||||||
|
_, |
||||||
|
type, |
||||||
|
_, |
||||||
|
token_symbol, |
||||||
|
_, |
||||||
|
tokens_transferred, |
||||||
|
_, |
||||||
|
transaction_fee, |
||||||
|
_, |
||||||
|
status, |
||||||
|
_, |
||||||
|
err_code, |
||||||
|
_ |
||||||
|
] -> |
||||||
|
%{ |
||||||
|
tx_hash: tx_hash, |
||||||
|
block_number: block_number, |
||||||
|
timestamp: timestamp, |
||||||
|
from_address: from_address, |
||||||
|
to_address: to_address, |
||||||
|
token_contract_address: token_contract_address, |
||||||
|
type: type, |
||||||
|
token_symbol: token_symbol, |
||||||
|
tokens_transferred: tokens_transferred, |
||||||
|
transaction_fee: transaction_fee, |
||||||
|
status: status, |
||||||
|
err_code: err_code |
||||||
|
} |
||||||
|
end) |
||||||
|
|
||||||
|
assert result.block_number == to_string(transaction.block_number) |
||||||
|
assert result.tx_hash == to_string(transaction.hash) |
||||||
|
assert result.from_address == token_transfer.from_address_hash |> to_string() |> String.downcase() |
||||||
|
assert result.to_address == token_transfer.to_address_hash |> to_string() |> String.downcase() |
||||||
|
assert result.timestamp == to_string(transaction.block.timestamp) |
||||||
|
assert result.type == "OUT" |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,105 @@ |
|||||||
|
defmodule Explorer.Chain.AddressTransactionCsvExporterTest do |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
alias Explorer.Chain.{AddressTransactionCsvExporter, Wei} |
||||||
|
|
||||||
|
describe "export/1" do |
||||||
|
test "exports address transactions to csv" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
transaction = |
||||||
|
:transaction |
||||||
|
|> insert(from_address: address) |
||||||
|
|> with_block() |
||||||
|
|> Repo.preload(:token_transfers) |
||||||
|
|
||||||
|
[result] = |
||||||
|
address |
||||||
|
|> AddressTransactionCsvExporter.export() |
||||||
|
|> Enum.to_list() |
||||||
|
|> Enum.drop(1) |
||||||
|
|> Enum.map(fn [ |
||||||
|
hash, |
||||||
|
_, |
||||||
|
block_number, |
||||||
|
_, |
||||||
|
timestamp, |
||||||
|
_, |
||||||
|
from_address, |
||||||
|
_, |
||||||
|
to_address, |
||||||
|
_, |
||||||
|
created_address, |
||||||
|
_, |
||||||
|
type, |
||||||
|
_, |
||||||
|
value, |
||||||
|
_, |
||||||
|
fee, |
||||||
|
_, |
||||||
|
status, |
||||||
|
_, |
||||||
|
error, |
||||||
|
_, |
||||||
|
cur_price, |
||||||
|
_, |
||||||
|
op_price, |
||||||
|
_, |
||||||
|
cl_price, |
||||||
|
_ |
||||||
|
] -> |
||||||
|
%{ |
||||||
|
hash: hash, |
||||||
|
block_number: block_number, |
||||||
|
timestamp: timestamp, |
||||||
|
from_address: from_address, |
||||||
|
to_address: to_address, |
||||||
|
created_address: created_address, |
||||||
|
type: type, |
||||||
|
value: value, |
||||||
|
fee: fee, |
||||||
|
status: status, |
||||||
|
error: error, |
||||||
|
current_price: cur_price, |
||||||
|
opening_price: op_price, |
||||||
|
closing_price: cl_price |
||||||
|
} |
||||||
|
end) |
||||||
|
|
||||||
|
assert result.block_number == to_string(transaction.block_number) |
||||||
|
assert result.timestamp |
||||||
|
assert result.created_address == to_string(transaction.created_contract_address_hash) |
||||||
|
assert result.from_address == to_string(transaction.from_address) |
||||||
|
assert result.to_address == to_string(transaction.to_address) |
||||||
|
assert result.hash == to_string(transaction.hash) |
||||||
|
assert result.type == "OUT" |
||||||
|
assert result.value == transaction.value |> Wei.to(:wei) |> to_string() |
||||||
|
assert result.fee |
||||||
|
assert result.status == to_string(transaction.status) |
||||||
|
assert result.error == to_string(transaction.error) |
||||||
|
assert result.current_price |
||||||
|
assert result.opening_price |
||||||
|
assert result.closing_price |
||||||
|
end |
||||||
|
|
||||||
|
test "fetches all transactions" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
1..200 |
||||||
|
|> Enum.map(fn _ -> |
||||||
|
:transaction |
||||||
|
|> insert(from_address: address) |
||||||
|
|> with_block() |
||||||
|
end) |
||||||
|
|> Enum.count() |
||||||
|
|
||||||
|
result = |
||||||
|
address |
||||||
|
|> AddressTransactionCsvExporter.export() |
||||||
|
|> Enum.to_list() |
||||||
|
|> Enum.drop(1) |
||||||
|
|
||||||
|
assert Enum.count(result) == 200 |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,139 @@ |
|||||||
|
defmodule Explorer.Chain.Supply.RSKTest do |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
alias Explorer.Chain.Supply.RSK |
||||||
|
alias Explorer.Chain.Wei |
||||||
|
|
||||||
|
@coin_address "0x0000000000000000000000000000000001000006" |
||||||
|
|
||||||
|
defp wei!(value) do |
||||||
|
{:ok, wei} = Wei.cast(value) |
||||||
|
wei |
||||||
|
end |
||||||
|
|
||||||
|
test "total is 21_000_000" do |
||||||
|
assert RSK.total() == 21_000_000 |
||||||
|
end |
||||||
|
|
||||||
|
describe "circulating/0" do |
||||||
|
test "with no balance" do |
||||||
|
assert RSK.circulating() == wei!(0) |
||||||
|
end |
||||||
|
|
||||||
|
test "with a balance" do |
||||||
|
address = insert(:address, hash: @coin_address) |
||||||
|
insert(:block, number: 0) |
||||||
|
|
||||||
|
insert(:fetched_balance, value: 10, address_hash: address.hash, block_number: 0) |
||||||
|
|
||||||
|
assert RSK.circulating() == wei!(10) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp date(now, shift \\ []) do |
||||||
|
now |
||||||
|
|> Timex.shift(shift) |
||||||
|
|> Timex.to_date() |
||||||
|
end |
||||||
|
|
||||||
|
defp dec(number) do |
||||||
|
Decimal.new(number) |
||||||
|
end |
||||||
|
|
||||||
|
describe "supply_for_days/1" do |
||||||
|
test "when there is no balance" do |
||||||
|
now = Timex.now() |
||||||
|
|
||||||
|
assert RSK.supply_for_days(2) == |
||||||
|
{:ok, |
||||||
|
%{ |
||||||
|
date(now, days: -2) => dec(0), |
||||||
|
date(now, days: -1) => dec(0), |
||||||
|
date(now) => dec(0) |
||||||
|
}} |
||||||
|
end |
||||||
|
|
||||||
|
test "when there is a single balance before the days, that balance is used" do |
||||||
|
address = insert(:address, hash: @coin_address) |
||||||
|
now = Timex.now() |
||||||
|
|
||||||
|
insert(:block, number: 0, timestamp: Timex.shift(now, days: -10)) |
||||||
|
|
||||||
|
insert(:fetched_balance, value: 10, address_hash: address.hash, block_number: 0) |
||||||
|
|
||||||
|
assert RSK.supply_for_days(2) == |
||||||
|
{:ok, |
||||||
|
%{ |
||||||
|
date(now, days: -2) => dec(10), |
||||||
|
date(now, days: -1) => dec(10), |
||||||
|
date(now) => dec(10) |
||||||
|
}} |
||||||
|
end |
||||||
|
|
||||||
|
test "when there is a balance for one of the days, days after it use that balance" do |
||||||
|
address = insert(:address, hash: @coin_address) |
||||||
|
now = Timex.now() |
||||||
|
|
||||||
|
insert(:block, number: 0, timestamp: Timex.shift(now, days: -10)) |
||||||
|
insert(:block, number: 1, timestamp: Timex.shift(now, days: -1)) |
||||||
|
|
||||||
|
insert(:fetched_balance, value: 10, address_hash: address.hash, block_number: 0) |
||||||
|
|
||||||
|
insert(:fetched_balance, value: 20, address_hash: address.hash, block_number: 1) |
||||||
|
|
||||||
|
assert RSK.supply_for_days(2) == |
||||||
|
{:ok, |
||||||
|
%{ |
||||||
|
date(now, days: -2) => dec(10), |
||||||
|
date(now, days: -1) => dec(20), |
||||||
|
date(now) => dec(20) |
||||||
|
}} |
||||||
|
end |
||||||
|
|
||||||
|
test "when there is a balance for the first day, that balance is used" do |
||||||
|
address = insert(:address, hash: @coin_address) |
||||||
|
now = Timex.now() |
||||||
|
|
||||||
|
insert(:block, number: 0, timestamp: Timex.shift(now, days: -10)) |
||||||
|
insert(:block, number: 1, timestamp: Timex.shift(now, days: -2)) |
||||||
|
insert(:block, number: 2, timestamp: Timex.shift(now, days: -1)) |
||||||
|
|
||||||
|
insert(:fetched_balance, value: 5, address_hash: address.hash, block_number: 0) |
||||||
|
|
||||||
|
insert(:fetched_balance, value: 10, address_hash: address.hash, block_number: 1) |
||||||
|
|
||||||
|
insert(:fetched_balance, value: 20, address_hash: address.hash, block_number: 2) |
||||||
|
|
||||||
|
assert RSK.supply_for_days(2) == |
||||||
|
{:ok, |
||||||
|
%{ |
||||||
|
date(now, days: -2) => dec(10), |
||||||
|
date(now, days: -1) => dec(20), |
||||||
|
date(now) => dec(20) |
||||||
|
}} |
||||||
|
end |
||||||
|
|
||||||
|
test "when there is a balance for all days, they are each used correctly" do |
||||||
|
address = insert(:address, hash: @coin_address) |
||||||
|
now = Timex.now() |
||||||
|
|
||||||
|
insert(:block, number: 0, timestamp: Timex.shift(now, days: -10)) |
||||||
|
insert(:block, number: 1, timestamp: Timex.shift(now, days: -2)) |
||||||
|
insert(:block, number: 2, timestamp: Timex.shift(now, days: -1)) |
||||||
|
insert(:block, number: 3, timestamp: now) |
||||||
|
|
||||||
|
insert(:fetched_balance, value: 5, address_hash: address.hash, block_number: 0) |
||||||
|
insert(:fetched_balance, value: 10, address_hash: address.hash, block_number: 1) |
||||||
|
insert(:fetched_balance, value: 20, address_hash: address.hash, block_number: 2) |
||||||
|
insert(:fetched_balance, value: 30, address_hash: address.hash, block_number: 3) |
||||||
|
|
||||||
|
assert RSK.supply_for_days(2) == |
||||||
|
{:ok, |
||||||
|
%{ |
||||||
|
date(now, days: -2) => dec(10), |
||||||
|
date(now, days: -1) => dec(20), |
||||||
|
date(now) => dec(30) |
||||||
|
}} |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,90 @@ |
|||||||
|
defmodule Explorer.Market.MarketHistoryCacheTest do |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
alias Explorer.Market |
||||||
|
alias Explorer.Market.MarketHistoryCache |
||||||
|
|
||||||
|
setup do |
||||||
|
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, MarketHistoryCache.cache_name()}) |
||||||
|
Supervisor.restart_child(Explorer.Supervisor, {ConCache, MarketHistoryCache.cache_name()}) |
||||||
|
|
||||||
|
on_exit(fn -> |
||||||
|
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Chain.BlocksCache.cache_name()}) |
||||||
|
Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Chain.BlocksCache.cache_name()}) |
||||||
|
end) |
||||||
|
|
||||||
|
:ok |
||||||
|
end |
||||||
|
|
||||||
|
describe "fetch/1" do |
||||||
|
test "caches data on the first call" do |
||||||
|
today = Date.utc_today() |
||||||
|
|
||||||
|
records = |
||||||
|
for i <- 0..29 do |
||||||
|
%{ |
||||||
|
date: Timex.shift(today, days: i * -1), |
||||||
|
closing_price: Decimal.new(1), |
||||||
|
opening_price: Decimal.new(1) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
Market.bulk_insert_history(records) |
||||||
|
|
||||||
|
refute fetch_data() |
||||||
|
|
||||||
|
assert Enum.count(MarketHistoryCache.fetch()) == 30 |
||||||
|
|
||||||
|
assert fetch_data() == records |
||||||
|
end |
||||||
|
|
||||||
|
test "updates cache if cache is stale" do |
||||||
|
today = Date.utc_today() |
||||||
|
|
||||||
|
stale_records = |
||||||
|
for i <- 0..29 do |
||||||
|
%{ |
||||||
|
date: Timex.shift(today, days: i * -1), |
||||||
|
closing_price: Decimal.new(1), |
||||||
|
opening_price: Decimal.new(1) |
||||||
|
} |
||||||
|
end |
||||||
|
|
||||||
|
Market.bulk_insert_history(stale_records) |
||||||
|
|
||||||
|
MarketHistoryCache.fetch() |
||||||
|
|
||||||
|
stale_updated_at = fetch_updated_at() |
||||||
|
|
||||||
|
assert fetch_data() == stale_records |
||||||
|
|
||||||
|
ConCache.put(MarketHistoryCache.cache_name(), MarketHistoryCache.updated_at_key(), 1) |
||||||
|
|
||||||
|
fetch_data() |
||||||
|
|
||||||
|
assert stale_updated_at != fetch_updated_at() |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp fetch_updated_at do |
||||||
|
ConCache.get(MarketHistoryCache.cache_name(), MarketHistoryCache.updated_at_key()) |
||||||
|
end |
||||||
|
|
||||||
|
defp fetch_data do |
||||||
|
MarketHistoryCache.cache_name() |
||||||
|
|> ConCache.get(MarketHistoryCache.data_key()) |
||||||
|
|> case do |
||||||
|
nil -> |
||||||
|
nil |
||||||
|
|
||||||
|
records -> |
||||||
|
Enum.map(records, fn record -> |
||||||
|
%{ |
||||||
|
date: record.date, |
||||||
|
closing_price: record.closing_price, |
||||||
|
opening_price: record.opening_price |
||||||
|
} |
||||||
|
end) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,76 @@ |
|||||||
|
pragma solidity ^0.5.9; |
||||||
|
contract Token { |
||||||
|
function totalSupply() public view returns (uint256 supply) {} |
||||||
|
function balanceOf(address _owner) public view returns (uint256 balance) {} |
||||||
|
function transfer(address _to, uint256 _value) public returns (bool success) {} |
||||||
|
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {} |
||||||
|
function approve(address _spender, uint256 _value) public returns (bool success) {} |
||||||
|
function allowance(address _owner, address _spender) public view returns (uint256 remaining) {} |
||||||
|
event Transfer(address indexed _from, address indexed _to, uint256 _value); |
||||||
|
event Approval(address indexed _owner, address indexed _spender, uint256 _value); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
contract StandardToken is Token { |
||||||
|
function transfer(address _to, uint256 _value) public returns (bool success) { |
||||||
|
//if (balances[msg.sender] >= _value && balances[_to] + _value > balances[_to]) { |
||||||
|
if (balances[msg.sender] >= _value && _value > 0) { |
||||||
|
balances[msg.sender] -= _value; |
||||||
|
balances[_to] += _value; |
||||||
|
emit Transfer(msg.sender, _to, _value); |
||||||
|
return true; |
||||||
|
} else { return false; } |
||||||
|
} |
||||||
|
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { |
||||||
|
//if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value > balances[_to]) { |
||||||
|
if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && _value > 0) { |
||||||
|
balances[_to] += _value; |
||||||
|
balances[_from] -= _value; |
||||||
|
allowed[_from][msg.sender] -= _value; |
||||||
|
emit Transfer(_from, _to, _value); |
||||||
|
return true; |
||||||
|
} else { return false; } |
||||||
|
} |
||||||
|
function balanceOf(address _owner) public view returns (uint256 balance) { |
||||||
|
return balances[_owner]; |
||||||
|
} |
||||||
|
function approve(address _spender, uint256 _value) public returns (bool success) { |
||||||
|
allowed[msg.sender][_spender] = _value; |
||||||
|
emit Approval(msg.sender, _spender, _value); |
||||||
|
return true; |
||||||
|
} |
||||||
|
function allowance(address _owner, address _spender) public view returns (uint256 remaining) { |
||||||
|
return allowed[_owner][_spender]; |
||||||
|
} |
||||||
|
mapping (address => uint256) balances; |
||||||
|
mapping (address => mapping (address => uint256)) allowed; |
||||||
|
uint256 totalTokenSupply; |
||||||
|
} |
||||||
|
|
||||||
|
contract TestToken is StandardToken { |
||||||
|
|
||||||
|
/* Public variables */ |
||||||
|
string public name; |
||||||
|
uint8 public decimals; |
||||||
|
string public symbol; |
||||||
|
string public version = '0.1'; |
||||||
|
|
||||||
|
constructor( |
||||||
|
uint256 _initialAmount, |
||||||
|
string memory _tokenName, |
||||||
|
uint8 _decimalUnits, |
||||||
|
string memory _tokenSymbol |
||||||
|
) public { |
||||||
|
balances[msg.sender] = _initialAmount; |
||||||
|
totalTokenSupply = _initialAmount; |
||||||
|
name = _tokenName; |
||||||
|
decimals = _decimalUnits; |
||||||
|
symbol = _tokenSymbol; |
||||||
|
} |
||||||
|
|
||||||
|
function approveAndCall(address _spender, uint256 _value) public returns (bool success) { |
||||||
|
allowed[msg.sender][_spender] = _value; |
||||||
|
emit Approval(msg.sender, _spender, _value); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
@ -1,3 +1,3 @@ |
|||||||
<!-- faq.md --> |
<!-- faq.md --> |
||||||
|
|
||||||
_Coming Soon_ |
FAQs are located in the [BlockScout forum](https://forum.poa.network/c/blockscout/wiki). |
@ -1,20 +0,0 @@ |
|||||||
<!-- projects.md --> |
|
||||||
|
|
||||||
### Supported Projects |
|
||||||
|
|
||||||
| **Hosted Mainnets** | **Hosted Testnets** | **Additional Chains using BlockScout** | |
|
||||||
|--------------------------------------------------------|-------------------------------------------------------|----------------------------------------------------| |
|
||||||
| [Aerum](https://blockscout.com/aerum/mainnet) | [Goerli Testnet](https://blockscout.com/eth/goerli) | [ARTIS](https://explorer.sigma1.artis.network) | |
|
||||||
| [Callisto](https://blockscout.com/callisto/mainnet) | [Kovan Testnet](https://blockscout.com/eth/kovan) | [Ether-1](https://blocks.ether1.wattpool.net/) | |
|
||||||
| [Ethereum Classic](https://blockscout.com/etc/mainnet) | [POA Sokol Testnet](https://blockscout.com/poa/sokol) | [Fuse Network](https://explorer.fuse.io/) | |
|
||||||
| [Ethereum Mainnet](https://blockscout.com/eth/mainnet) | [Rinkeby Testnet](https://blockscout.com/eth/rinkeby) | [Oasis Labs](https://blockexplorer.oasiscloud.io/) | |
|
||||||
| [POA Core Network](https://blockscout.com/poa/core) | [Ropsten Testnet](https://blockscout.com/eth/ropsten) | [Petrichor](https://explorer.petrachor.com/) | |
|
||||||
| [RSK](https://blockscout.com/rsk/mainnet) | | [PIRL](http://pirl.es/) | |
|
||||||
| [xDai Chain](https://blockscout.com/poa/dai) | | [SafeChain](https://explorer.safechain.io) | |
|
||||||
| | | [SpringChain](https://explorer.springrole.com/) | |
|
||||||
| | | [Kotti Testnet](https://kottiexplorer.ethernode.io/) | |
|
||||||
| | | [Loom](http://plasma-blockexplorer.dappchains.com/) | |
|
||||||
| | | [Tenda](https://tenda.network) | |
|
||||||
|
|
||||||
|
|
||||||
Current BlockScout versions for hosted projects are available [on the forum](https://forum.poa.network/t/deployed-instances-on-blockscout-com/1938). |
|