@ -0,0 +1,341 @@ |
||||
$network-selector-overlay-background: $modal-overlay-color !default; |
||||
$network-selector-close-color: $primary !default; |
||||
$network-selector-horizontal-padding: 28px; |
||||
$network-selector-horizontal-mobile-padding: 14px; |
||||
$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-mobile-padding; |
||||
@media (min-width: 375px) { |
||||
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-mobile-padding; |
||||
@media (min-width: 375px) { |
||||
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,46 @@ |
||||
.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 { |
||||
display: inline-flex; |
||||
align-items: center; |
||||
text-decoration: none; |
||||
svg { |
||||
position: relative; |
||||
margin-left: 2px; |
||||
top: -3px; |
||||
left: 3px; |
||||
path { |
||||
fill: $primary; |
||||
} |
||||
} |
||||
&:hover { |
||||
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("Use the search box to find a hosted network, or select from the list of available networks below.") %></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,11 @@ |
||||
defmodule BlockScoutWeb.PageNotFoundControllerTest do |
||||
use BlockScoutWeb.ConnCase |
||||
|
||||
describe "GET index/2" do |
||||
test "returns 404 status", %{conn: conn} do |
||||
conn = get(conn, "/wrong", %{}) |
||||
|
||||
assert html_response(conn, 404) |
||||
end |
||||
end |
||||
end |
@ -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,143 @@ |
||||
defmodule Explorer.Chain.TransactionsCache do |
||||
@moduledoc """ |
||||
Caches the latest imported transactions |
||||
""" |
||||
|
||||
alias Explorer.Chain.Transaction |
||||
alias Explorer.Repo |
||||
|
||||
@transactions_ids_key "transactions_ids" |
||||
@cache_name :transactions |
||||
@max_size 51 |
||||
@preloads [ |
||||
:block, |
||||
created_contract_address: :names, |
||||
from_address: :names, |
||||
to_address: :names, |
||||
token_transfers: :token, |
||||
token_transfers: :from_address, |
||||
token_transfers: :to_address |
||||
] |
||||
|
||||
@spec cache_name :: atom() |
||||
def cache_name, do: @cache_name |
||||
|
||||
@doc """ |
||||
Fetches a transaction from its id ({block_number, index}), returns nil if not found |
||||
""" |
||||
@spec get({non_neg_integer(), non_neg_integer()}) :: Transaction.t() | nil |
||||
def get(id), do: ConCache.get(@cache_name, id) |
||||
|
||||
@doc """ |
||||
Return the current number of transactions stored |
||||
""" |
||||
@spec size :: non_neg_integer() |
||||
def size, do: Enum.count(transactions_ids()) |
||||
|
||||
@doc """ |
||||
Checks if there are enough transactions stored |
||||
""" |
||||
@spec enough?(non_neg_integer()) :: boolean() |
||||
def enough?(amount) do |
||||
amount <= size() |
||||
end |
||||
|
||||
@doc """ |
||||
Checks if the number of transactions stored is already the max allowed |
||||
""" |
||||
@spec full? :: boolean() |
||||
def full? do |
||||
@max_size <= size() |
||||
end |
||||
|
||||
@doc "Returns the list ids of the transactions currently stored" |
||||
@spec transactions_ids :: [{non_neg_integer(), non_neg_integer()}] |
||||
def transactions_ids do |
||||
ConCache.get(@cache_name, @transactions_ids_key) || [] |
||||
end |
||||
|
||||
@doc "Returns all the stored transactions" |
||||
@spec all :: [Transaction.t()] |
||||
def all, do: Enum.map(transactions_ids(), &get(&1)) |
||||
|
||||
@doc "Returns the `n` most recent transactions stored" |
||||
@spec take(integer()) :: [Transaction.t()] |
||||
def take(amount) do |
||||
transactions_ids() |
||||
|> Enum.take(amount) |
||||
|> Enum.map(&get(&1)) |
||||
end |
||||
|
||||
@doc """ |
||||
Returns the `n` most recent transactions, unless there are not as many stored, |
||||
in which case returns `nil` |
||||
""" |
||||
@spec take_enough(integer()) :: [Transaction.t()] | nil |
||||
def take_enough(amount) do |
||||
if enough?(amount), do: take(amount) |
||||
end |
||||
|
||||
@doc """ |
||||
Adds a transaction (or a list of transactions). |
||||
If the cache is already full, the transaction will be only stored if it can take |
||||
the place of a less recent one. |
||||
NOTE: each transaction is inserted atomically |
||||
""" |
||||
@spec update([Transaction.t()] | Transaction.t() | nil) :: :ok |
||||
def update(transactions) when is_nil(transactions), do: :ok |
||||
|
||||
def update(transactions) when is_list(transactions) do |
||||
Enum.map(transactions, &update(&1)) |
||||
end |
||||
|
||||
def update(transaction) do |
||||
ConCache.isolated(@cache_name, @transactions_ids_key, fn -> |
||||
transaction_id = {transaction.block_number, transaction.index} |
||||
ids = transactions_ids() |
||||
|
||||
if full?() do |
||||
{init, [min]} = Enum.split(ids, -1) |
||||
|
||||
cond do |
||||
transaction_id < min -> |
||||
:ok |
||||
|
||||
transaction_id > min -> |
||||
insert_transaction(transaction_id, transaction, init) |
||||
ConCache.delete(@cache_name, min) |
||||
|
||||
transaction_id == min -> |
||||
put_transaction(transaction_id, transaction) |
||||
end |
||||
else |
||||
insert_transaction(transaction_id, transaction, ids) |
||||
end |
||||
end) |
||||
end |
||||
|
||||
defp insert_transaction(transaction_id, transaction, ids) do |
||||
put_transaction(transaction_id, transaction) |
||||
|
||||
ConCache.put(@cache_name, @transactions_ids_key, insert_sorted(transaction_id, ids)) |
||||
end |
||||
|
||||
defp put_transaction(transaction_id, transaction) do |
||||
full_transaction = Repo.preload(transaction, @preloads) |
||||
|
||||
ConCache.put(@cache_name, transaction_id, full_transaction) |
||||
end |
||||
|
||||
defp insert_sorted(id, ids) do |
||||
case ids do |
||||
[] -> |
||||
[id] |
||||
|
||||
[head | tail] -> |
||||
cond do |
||||
head > id -> [head | insert_sorted(id, tail)] |
||||
head < id -> [id | ids] |
||||
head == id -> ids |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,132 @@ |
||||
defmodule Explorer.Repo.Migrations.ReduceTransactionStatusConstraint do |
||||
use Ecto.Migration |
||||
|
||||
def up do |
||||
drop( |
||||
constraint( |
||||
:transactions, |
||||
:status |
||||
) |
||||
) |
||||
|
||||
create( |
||||
constraint( |
||||
:transactions, |
||||
:status, |
||||
# NOTE: all checks on status are lifted except those regarding block_hash |
||||
# This is because of block invalidation, that causes transactions to be |
||||
# refetched while previous internal transactions still exist |
||||
check: """ |
||||
(block_hash IS NULL AND status IS NULL) OR |
||||
(block_hash IS NOT NULL) OR |
||||
(status = 0 and error = 'dropped/replaced') |
||||
""" |
||||
) |
||||
) |
||||
|
||||
drop( |
||||
constraint( |
||||
:transactions, |
||||
:error |
||||
) |
||||
) |
||||
|
||||
create( |
||||
constraint( |
||||
:transactions, |
||||
:error, |
||||
# NOTE: all checks on error are lifted except when status is not 0, for |
||||
# the same reasons as above |
||||
check: """ |
||||
(status = 0) OR |
||||
(status != 0 AND error IS NULL) |
||||
""" |
||||
) |
||||
) |
||||
end |
||||
|
||||
def down do |
||||
drop( |
||||
constraint( |
||||
:transactions, |
||||
:status |
||||
) |
||||
) |
||||
|
||||
create( |
||||
constraint( |
||||
:transactions, |
||||
:status, |
||||
# 0 - NULL |
||||
# 1 - NOT NULL |
||||
# |
||||
# | block_hash | internal_transactions_indexed_at | status | OK | description |
||||
# |------------|----------------------------------|--------|----|------------ |
||||
# | 0 | 0 | 0 | 1 | pending |
||||
# | 0 | 0 | 1 | 0 | pending with status |
||||
# | 0 | 1 | 0 | 0 | pending with internal transactions |
||||
# | 0 | 1 | 1 | 0 | pending with internal transactions and status |
||||
# | 1 | 0 | 0 | 1 | pre-byzantium collated transaction without internal transactions |
||||
# | 1 | 0 | 1 | 1 | post-byzantium collated transaction without internal transactions |
||||
# | 1 | 1 | 0 | 0 | pre-byzantium collated transaction with internal transaction without status |
||||
# | 1 | 1 | 1 | 1 | pre- or post-byzantium collated transaction with internal transactions and status |
||||
# |
||||
# [Karnaugh map](https://en.wikipedia.org/wiki/Karnaugh_map) |
||||
# b \ is | 00 | 01 | 11 | 10 | |
||||
# -------|----|----|----|----| |
||||
# 0 | 1 | 0 | 0 | 0 | |
||||
# 1 | 1 | 1 | 1 | 0 | |
||||
# |
||||
# Simplification: ¬i·¬s + b·¬i + b·s |
||||
check: """ |
||||
(internal_transactions_indexed_at IS NULL AND status IS NULL) OR |
||||
(block_hash IS NOT NULL AND internal_transactions_indexed_at IS NULL) OR |
||||
(block_hash IS NOT NULL AND status IS NOT NULL) OR |
||||
(status = 0 and error = 'dropped/replaced') |
||||
""" |
||||
) |
||||
) |
||||
|
||||
drop( |
||||
constraint( |
||||
:transactions, |
||||
:error |
||||
) |
||||
) |
||||
|
||||
create( |
||||
constraint( |
||||
:transactions, |
||||
:error, |
||||
# | status | internal_transactions_indexed_at | error | OK | description |
||||
# |--------|----------------------------------|----------|------------|------------ |
||||
# | NULL | NULL | NULL | TRUE | pending or pre-byzantium collated |
||||
# | NULL | NULL | NOT NULL | FALSE | error cannot be known before internal transactions are indexed |
||||
# | NULL | NOT NULL | NULL | DON'T CARE | handled by `status` check |
||||
# | NULL | NOT NULL | NOT NULL | FALSE | error cannot be set unless status is known to be error (`0`) |
||||
# | 0 | NULL | NULL | TRUE | post-byzantium before internal transactions indexed |
||||
# | 0 | NULL | NOT NULL | FALSE | error cannot be set unless internal transactions are indexed |
||||
# | 0 | NOT NULL | NULL | FALSE | error MUST be set when status is error |
||||
# | 0 | NOT NULL | NOT NULL | TRUE | error is set when status is error |
||||
# | 1 | NULL | NULL | TRUE | post-byzantium before internal transactions indexed |
||||
# | 1 | NULL | NOT NULL | FALSE | error cannot be set when status is ok |
||||
# | 1 | NOT NULL | NULL | TRUE | error is not set when status is ok |
||||
# | 1 | NOT NULL | NOT NULL | FALSE | error cannot be set when status is ok |
||||
# |
||||
# Karnaugh map |
||||
# s \ ie | NULL, NULL | NULL, NOT NULL | NOT NULL, NOT NULL | NOT NULL, NULL | |
||||
# -------|------------|----------------|--------------------|----------------| |
||||
# NULL | TRUE | FALSE | FALSE | DON'T CARE | |
||||
# 0 | TRUE | FALSE | TRUE | FALSE | |
||||
# 1 | TRUE | FALSE | FALSE | TRUE | |
||||
# |
||||
check: """ |
||||
(internal_transactions_indexed_at IS NULL AND error IS NULL) OR |
||||
(status = 0 AND internal_transactions_indexed_at IS NOT NULL AND error IS NOT NULL) OR |
||||
(status != 0 AND internal_transactions_indexed_at IS NOT NULL AND error IS NULL) OR |
||||
(status = 0 and error = 'dropped/replaced') |
||||
""" |
||||
) |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,11 @@ |
||||
defmodule Explorer.Repo.Migrations.AddAdditionalContractFields do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
alter table(:smart_contracts) do |
||||
add(:optimization_runs, :integer, null: true) |
||||
add(:evm_version, :string, null: true) |
||||
add(:external_libraries, :jsonb, null: true) |
||||
end |
||||
end |
||||
end |