@ -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 { |
||||
font-size: 12px; |
||||
font-weight: bold; |
||||
line-height: 1.2; |
||||
margin: 0 0 12px; |
||||
.transaction-bottom-panel { |
||||
display: flex; |
||||
flex-direction: column; |
||||
@media (min-width: 768px) { |
||||
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 --> |
||||
|
||||
_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). |
@ -0,0 +1,3 @@ |
||||
## Upgrading Guide |
||||
|
||||
**Upgrade instructions are in progress. If you need assistance with an upgrade, please contact us through the [forum](https://forum.poa.network/c/blockscout) or [gitter](https://gitter.im/poanetwork/blockscout) channel.** |