pull/8050/head
commit
86220e9dae
@ -0,0 +1,180 @@ |
||||
import { prepareMethodArgs } from '../../../js/lib/smart_contract/common_helpers' |
||||
import $ from 'jquery' |
||||
|
||||
const oneFieldHTML = |
||||
'<form data-function-form>' + |
||||
' <input type="hidden" name="function_name" value="convertMultiple">' +
|
||||
' <input type="hidden" name="method_id" value="">' +
|
||||
' <div>' +
|
||||
' <input type="text" name="function_input" id="first">' +
|
||||
' </div>' +
|
||||
' <input type="submit" value="Write">' +
|
||||
'</form>' |
||||
|
||||
const twoFieldHTML = |
||||
'<form data-function-form>' + |
||||
' <input type="hidden" name="function_name" value="convertMultiple">' +
|
||||
' <input type="hidden" name="method_id" value="">' +
|
||||
' <div>' +
|
||||
' <input type="text" name="function_input" id="first">' +
|
||||
' </div>' +
|
||||
' <div>' +
|
||||
' <input type="text" name="function_input" id="second">' +
|
||||
' </div>' +
|
||||
' <input type="submit" value="Write">' +
|
||||
'</form>' |
||||
|
||||
test('prepare contract args | type: address[]*2', () => { |
||||
document.body.innerHTML = twoFieldHTML |
||||
|
||||
var inputs = [ |
||||
{ |
||||
"type": "address[]", |
||||
"name": "arg1", |
||||
"internalType": "address[]" |
||||
}, |
||||
{ |
||||
"type": "address[]", |
||||
"name": "arg2", |
||||
"internalType": "address[]" |
||||
} |
||||
] |
||||
|
||||
document.getElementById('first').value = ' 0x0000000000000000000000000000000000000000 , 0x0000000000000000000000000000000000000001 ' |
||||
document.getElementById('second').value = ' 0x0000000000000000000000000000000000000002 , 0x0000000000000000000000000000000000000003 ' |
||||
const expectedValue = [ |
||||
[ |
||||
'0x0000000000000000000000000000000000000000', |
||||
'0x0000000000000000000000000000000000000001' |
||||
], |
||||
[ |
||||
'0x0000000000000000000000000000000000000002', |
||||
'0x0000000000000000000000000000000000000003' |
||||
] |
||||
] |
||||
const $functionInputs = $('[data-function-form]').find('input[name=function_input]') |
||||
|
||||
expect(prepareMethodArgs($functionInputs, inputs)).toEqual(expectedValue) |
||||
}) |
||||
|
||||
test('prepare contract args | type: address', () => { |
||||
document.body.innerHTML = oneFieldHTML |
||||
|
||||
var inputs = [ |
||||
{ |
||||
"type": "address", |
||||
"name": "arg1", |
||||
"internalType": "address" |
||||
} |
||||
] |
||||
|
||||
document.getElementById('first').value = ' 0x000000000000000000 0000000000000000000000 ' |
||||
const expectedValue = ['0x0000000000000000000000000000000000000000'] |
||||
const $functionInputs = $('[data-function-form]').find('input[name=function_input]') |
||||
|
||||
expect(prepareMethodArgs($functionInputs, inputs)).toEqual(expectedValue) |
||||
}) |
||||
|
||||
test('prepare contract args | type: string', () => { |
||||
document.body.innerHTML = oneFieldHTML |
||||
|
||||
var inputs = [ |
||||
{ |
||||
"type": "string", |
||||
"name": "arg1", |
||||
"internalType": "string" |
||||
} |
||||
] |
||||
|
||||
document.getElementById('first').value = ' 0x0000000000000000000000000000000000000000 , 0x0000000000000000000000000000000000000001 ' |
||||
const expectedValue = ['0x0000000000000000000000000000000000000000 , 0x0000000000000000000000000000000000000001'] |
||||
const $functionInputs = $('[data-function-form]').find('input[name=function_input]') |
||||
|
||||
expect(prepareMethodArgs($functionInputs, inputs)).toEqual(expectedValue) |
||||
}) |
||||
|
||||
test('prepare contract args | type: string[]', () => { |
||||
document.body.innerHTML = oneFieldHTML |
||||
|
||||
var inputs = [ |
||||
{ |
||||
"type": "string[]", |
||||
"name": "arg1", |
||||
"internalType": "string[]" |
||||
} |
||||
] |
||||
|
||||
document.getElementById('first').value = ' " 0x0000000000000000000000000000000000000000 " , " 0x0000000000000000000000000000000000000001 " ' |
||||
const expectedValue = [['0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000001']] |
||||
const $functionInputs = $('[data-function-form]').find('input[name=function_input]') |
||||
expect(prepareMethodArgs($functionInputs, inputs)).toEqual(expectedValue) |
||||
}) |
||||
|
||||
test('prepare contract args | type: bool[]', () => { |
||||
document.body.innerHTML = oneFieldHTML |
||||
|
||||
var inputs = [ |
||||
{ |
||||
"type": "bool[]", |
||||
"name": "arg1", |
||||
"internalType": "bool[]" |
||||
} |
||||
] |
||||
|
||||
document.getElementById('first').value = ' true , false ' |
||||
const expectedValue = [[true, false]] |
||||
const $functionInputs = $('[data-function-form]').find('input[name=function_input]') |
||||
expect(prepareMethodArgs($functionInputs, inputs)).toEqual(expectedValue) |
||||
}) |
||||
|
||||
test('prepare contract args | type: bool', () => { |
||||
document.body.innerHTML = oneFieldHTML |
||||
|
||||
var inputs = [ |
||||
{ |
||||
"type": "bool", |
||||
"name": "arg1", |
||||
"internalType": "bool" |
||||
} |
||||
] |
||||
|
||||
document.getElementById('first').value = ' fals e ' |
||||
const expectedValue = [false] |
||||
const $functionInputs = $('[data-function-form]').find('input[name=function_input]') |
||||
expect(prepareMethodArgs($functionInputs, inputs)).toEqual(expectedValue) |
||||
}) |
||||
|
||||
|
||||
test('prepare contract args | type: uint256', () => { |
||||
document.body.innerHTML = oneFieldHTML |
||||
|
||||
var inputs = [ |
||||
{ |
||||
"type": "uint256", |
||||
"name": "arg1", |
||||
"internalType": "uint256" |
||||
} |
||||
] |
||||
|
||||
document.getElementById('first').value = ' 9 876 543 210 ' |
||||
const expectedValue = ['9876543210'] |
||||
const $functionInputs = $('[data-function-form]').find('input[name=function_input]') |
||||
expect(prepareMethodArgs($functionInputs, inputs)).toEqual(expectedValue) |
||||
}) |
||||
|
||||
test('prepare contract args | type: uint256[]', () => { |
||||
document.body.innerHTML = oneFieldHTML |
||||
|
||||
var inputs = [ |
||||
{ |
||||
"type": "uint256[]", |
||||
"name": "arg1", |
||||
"internalType": "uint256[]" |
||||
} |
||||
] |
||||
|
||||
document.getElementById('first').value = ' 156 000 , 10 690 000 , 59874 ' |
||||
const expectedValue = [['156000', '10690000', '59874']] |
||||
const $functionInputs = $('[data-function-form]').find('input[name=function_input]') |
||||
expect(prepareMethodArgs($functionInputs, inputs)).toEqual(expectedValue) |
||||
}) |
@ -0,0 +1,10 @@ |
||||
$btn-wallet-color: $btn-line-color !default; |
||||
$btn-wallet-dimensions: 31px !default; |
||||
|
||||
.btn-wallet-icon { |
||||
@include square-icon-button($btn-wallet-color, $btn-wallet-dimensions); |
||||
color: $btn-wallet-color; |
||||
&:hover { |
||||
color: #fff; |
||||
} |
||||
} |
@ -0,0 +1,16 @@ |
||||
<div |
||||
class="address-current-balance" |
||||
data-token-balance-dropdown |
||||
data-api_path="<%= AccessHelpers.get_path(@conn, :address_token_balance_path, :index, Address.checksum(@address.hash)) %>" |
||||
> |
||||
<span data-loading class="mb-0"> |
||||
<span class="loading-spinner-small mr-2"> |
||||
<span class="loading-spinner-block-1"></span> |
||||
<span class="loading-spinner-block-2"></span> |
||||
</span> |
||||
<%= gettext("Fetching tokens...") %> |
||||
</span> |
||||
<span data-error-message class="mb-0" style="display: none;"> |
||||
<%= gettext("Error trying to fetch balances.") %> |
||||
</span> |
||||
</div> |
@ -1,22 +1,69 @@ |
||||
<div class="tile tile-type-token"> |
||||
<div class="row justify-content align-items-center"> |
||||
<div class="col-md-7 d-flex flex-column mt-3 mt-md-0"> |
||||
<tr data-identifier-hash="<%= @token.contract_address_hash %>"> |
||||
<td class="stakes-td"> |
||||
<!-- incremented number by order in the list --> |
||||
<span class="color-lighten"> |
||||
</span> |
||||
</td> |
||||
<td class="stakes-td"> |
||||
<span |
||||
class="token-icon mr-1" |
||||
> |
||||
<%= if System.get_env("DISPLAY_TOKEN_ICONS") === "true" do %> |
||||
<% chain_id_for_token_icon = if @bridged_token && @bridged_token.foreign_chain_id, do: @bridged_token.foreign_chain_id |> Decimal.to_integer() |> to_string(), else: System.get_env("CHAIN_ID") %> |
||||
<% address_hash = if @bridged_token && @bridged_token.foreign_token_contract_address_hash, do: @bridged_token.foreign_token_contract_address_hash, else: @token.contract_address_hash %> |
||||
<% token_icon_url = Chain.get_token_icon_url_by(chain_id_for_token_icon, Address.checksum(address_hash)) %> |
||||
|
||||
<%= if token_icon_url do %> |
||||
<img heigth=15 width=15 src="<%= token_icon_url %>" style="margin-top: -2px;"/> |
||||
<% end %> |
||||
<% end %> |
||||
</span> |
||||
<%= link( |
||||
to: address_token_transfers_path(@conn, :index, to_string(@address.hash), to_string(@token.contract_address_hash)), |
||||
class: "tile-title-lg", |
||||
"data-test": "token_transfers_#{@token.contract_address_hash}" |
||||
"data-test": "token_transfers_#{@token_balance.token.contract_address_hash}" |
||||
) do %> |
||||
<%= token_name(@token) %> |
||||
<% end %> |
||||
<span><%= @token.type %> - <%= @token.contract_address_hash %></span> |
||||
<%= if @token.usd_value do %> |
||||
<span data-selector="token-price" data-usd-value="<%= @token.usd_value %>"></span> |
||||
</td> |
||||
<td class="stakes-td"> |
||||
<%= @token.type %> |
||||
</td> |
||||
<td class="stakes-td"> |
||||
<%= format_according_to_decimals(@token_balance.value, @token.decimals) %> |
||||
</td> |
||||
<td class="stakes-td"> |
||||
<%= @token.symbol %> |
||||
</td> |
||||
<td class="stakes-td"> |
||||
<p class="mb-0 col-md-6 text-right text-muted"> |
||||
<% token_price = if @token_balance.token.usd_value, do: @token_balance.token.usd_value, else: nil %> |
||||
<span data-selector="token-price" data-token-usd-value="<%= @token_balance.token.usd_value %>"><%= ChainView.format_currency_value(token_price, "@") %></span> |
||||
</p> |
||||
</td> |
||||
<td class="stakes-td"> |
||||
<%= if @bridged_token && @bridged_token.lp_token && @bridged_token.custom_cap do %> |
||||
<% lp_token_balance_usd = @token_balance.value |> Decimal.div(@token_balance.token.total_supply) |> Decimal.mult(@bridged_token.custom_cap) |> Decimal.round(4) %> |
||||
<p class="mb-0 col-md-6 text-right"> |
||||
<span data-selector="token-balance-usd" data-usd-value="<%= lp_token_balance_usd %>"><%= ChainView.format_usd_value(lp_token_balance_usd) %></span> |
||||
</p> |
||||
<% else %> |
||||
<%= if @token_balance.token.usd_value do %> |
||||
<p class="mb-0 col-md-6 text-right"> |
||||
<span data-selector="token-balance-usd" data-usd-value="<%= Chain.balance_in_usd(@token_balance) %>"><%= ChainView.format_usd_value(Chain.balance_in_usd(@token_balance)) %></span> |
||||
</p> |
||||
<% end %> |
||||
</div> |
||||
<div class="col-md-5 d-flex flex-column text-md-right mt-3 mt-md-0"> |
||||
<span class="tile-title-lg text-md-right align-bottom"> |
||||
<%= format_according_to_decimals(@token.balance, @token.decimals) %> <%= @token.symbol %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<% end %> |
||||
</td> |
||||
<td class="stakes-td"> |
||||
<%= with {:ok, address} <- Chain.hash_to_address(@token.contract_address_hash) do |
||||
render BlockScoutWeb.AddressView, |
||||
"_link.html", |
||||
address: address, |
||||
contract: false, |
||||
use_custom_tooltip: false, |
||||
no_tooltip: true |
||||
end |
||||
%> |
||||
</td> |
||||
</tr> |
@ -0,0 +1,64 @@ |
||||
<% native_coin = gettext("ETH") %> |
||||
<!-- USD value of the balance --> |
||||
<% wei_value = if @address.fetched_coin_balance, do: @address.fetched_coin_balance.value %> |
||||
<% raw_usd_value = |
||||
case @exchange_rate.usd_value do |
||||
%Decimal{} -> |
||||
if wei_value do |
||||
%Wei{value: Decimal.new(wei_value)} |
||||
|> Wei.to(:ether) |
||||
|> Decimal.mult(@exchange_rate.usd_value) |
||||
else |
||||
Decimal.new(0) |
||||
end |
||||
_ -> Decimal.new(0) |
||||
end |
||||
%> |
||||
<% data_usd_exchange_rate = |
||||
unless AddressView.empty_exchange_rate?(@exchange_rate) do |
||||
"data-usd-exchange-rate='#{@exchange_rate.usd_value}' data-raw-usd-value='#{raw_usd_value}'" |
||||
end |
||||
%> |
||||
<% native_coin_balance_token = AddressView.balance(@address) |
||||
%> |
||||
<% native_coin_balance_usd = |
||||
if AddressView.empty_exchange_rate?(@exchange_rate) do |
||||
nil |
||||
else |
||||
"<span |
||||
data-wei-value='#{wei_value}' |
||||
#{data_usd_exchange_rate} |
||||
> |
||||
</span>" |
||||
end |
||||
%> |
||||
<% native_coin_balance = |
||||
if native_coin_balance_usd do |
||||
native_coin_balance_usd <> " | " <> native_coin_balance_token |
||||
else |
||||
native_coin_balance_token |
||||
end |
||||
%> |
||||
<div class="d-md-flex"> |
||||
<%= render BlockScoutWeb.AddressTokenView, "overview_item.html", |
||||
title: gettext("Net Worth"), |
||||
tooltip: gettext("Shows total assets held in the address"), |
||||
value: "N/A", |
||||
data_test: "address-tokens-panel-net-worth", |
||||
classes: ["fs-14"] |
||||
%> |
||||
<%= render BlockScoutWeb.AddressTokenView, "overview_item.html", |
||||
title: "#{native_coin} #{gettext("Balance")}", |
||||
tooltip: "#{gettext("Shows the current")} #{native_coin} #{gettext("balance of the address")}", |
||||
value: raw(native_coin_balance), |
||||
data_test: "address-tokens-panel-native-worth", |
||||
classes: ["fs-14"] |
||||
%> |
||||
<%= render BlockScoutWeb.AddressTokenView, "overview_item.html", |
||||
title: gettext("Tokens"), |
||||
tooltip: gettext("Shows the tokens held in the address (includes ERC-20, ERC-721 and ERC-1155)."), |
||||
value: "N/A", |
||||
data_test: "address-tokens-panel-tokens-worth", |
||||
classes: ["fs-14"] |
||||
%> |
||||
</div> |
@ -0,0 +1,14 @@ |
||||
<div class="d-flex mr-4" style="padding: 10px;"> |
||||
<div> |
||||
<div class="d-flex"> |
||||
<h1 class="card-title mb-2"><%= @title %></h1> |
||||
<%= render BlockScoutWeb.CommonComponentsView, "_i_tooltip.html", |
||||
text: @tooltip, |
||||
additional_classes: ["ml-2"] |
||||
%> |
||||
</div> |
||||
<div data-test="<%= if assigns[:data_test], do: @data_test %>" class="<%= if assigns[:classes] do @classes |> Enum.join(" ") end %>""> |
||||
<%= @value %> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,7 @@ |
||||
<div csv-download class="download-all-transactions"> |
||||
Download <a class="download-all-transactions-link" href=<%= csv_export_path(@conn, :index, %{"address" => @address, "type" => @type }) %>><%= gettext("CSV") %></span> |
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="16"> |
||||
<path fill="#333333" fill-rule="evenodd" d="M13 16H1c-.999 0-1-1-1-1V1s-.004-1 1-1h6l7 7v8s-.032 1-1 1zm-1-8c0-.99-1-1-1-1H8s-1 .001-1-1V3c0-.999-1-1-1-1H2v12h10V8z"/> |
||||
</svg> |
||||
</a> |
||||
</div> |
@ -1,5 +1,7 @@ |
||||
defmodule BlockScoutWeb.AddressTokenView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
alias Explorer.Chain.Address |
||||
alias BlockScoutWeb.{AddressView, ChainView} |
||||
alias Explorer.Chain |
||||
alias Explorer.Chain.{Address, Wei} |
||||
end |
||||
|
@ -0,0 +1,119 @@ |
||||
defmodule Explorer.Counters.AddressTokenTransfersCounter do |
||||
@moduledoc """ |
||||
Caches Address token transfers counter. |
||||
""" |
||||
use GenServer |
||||
|
||||
alias Explorer.Chain |
||||
|
||||
@cache_name :address_token_transfers_counter |
||||
@last_update_key "last_update" |
||||
|
||||
@ets_opts [ |
||||
:set, |
||||
:named_table, |
||||
:public, |
||||
read_concurrency: true |
||||
] |
||||
|
||||
config = Application.get_env(:explorer, Explorer.Counters.AddressTokenTransfersCounter) |
||||
@enable_consolidation Keyword.get(config, :enable_consolidation) |
||||
|
||||
@spec start_link(term()) :: GenServer.on_start() |
||||
def start_link(_) do |
||||
GenServer.start_link(__MODULE__, :ok, name: __MODULE__) |
||||
end |
||||
|
||||
@impl true |
||||
def init(_args) do |
||||
create_cache_table() |
||||
|
||||
{:ok, %{consolidate?: enable_consolidation?()}, {:continue, :ok}} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_continue(:ok, %{consolidate?: true} = state) do |
||||
{:noreply, state} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_continue(:ok, state) do |
||||
{:noreply, state} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info(:consolidate, state) do |
||||
{:noreply, state} |
||||
end |
||||
|
||||
def fetch(address) do |
||||
if cache_expired?(address) do |
||||
Task.start_link(fn -> |
||||
update_cache(address) |
||||
end) |
||||
end |
||||
|
||||
address_hash_string = get_address_hash_string(address) |
||||
fetch_from_cache("hash_#{address_hash_string}") |
||||
end |
||||
|
||||
def cache_name, do: @cache_name |
||||
|
||||
defp cache_expired?(address) do |
||||
cache_period = address_token_transfers_counter_cache_period() |
||||
address_hash_string = get_address_hash_string(address) |
||||
updated_at = fetch_from_cache("hash_#{address_hash_string}_#{@last_update_key}") |
||||
|
||||
cond do |
||||
is_nil(updated_at) -> true |
||||
current_time() - updated_at > cache_period -> true |
||||
true -> false |
||||
end |
||||
end |
||||
|
||||
defp update_cache(address) do |
||||
address_hash_string = get_address_hash_string(address) |
||||
put_into_cache("hash_#{address_hash_string}_#{@last_update_key}", current_time()) |
||||
new_data = Chain.address_to_token_transfer_count(address) |
||||
put_into_cache("hash_#{address_hash_string}", new_data) |
||||
end |
||||
|
||||
defp fetch_from_cache(key) do |
||||
case :ets.lookup(@cache_name, key) do |
||||
[{_, value}] -> |
||||
value |
||||
|
||||
[] -> |
||||
0 |
||||
end |
||||
end |
||||
|
||||
defp put_into_cache(key, value) do |
||||
:ets.insert(@cache_name, {key, value}) |
||||
end |
||||
|
||||
defp get_address_hash_string(address) do |
||||
Base.encode16(address.hash.bytes, case: :lower) |
||||
end |
||||
|
||||
defp current_time do |
||||
utc_now = DateTime.utc_now() |
||||
|
||||
DateTime.to_unix(utc_now, :millisecond) |
||||
end |
||||
|
||||
def create_cache_table do |
||||
if :ets.whereis(@cache_name) == :undefined do |
||||
:ets.new(@cache_name, @ets_opts) |
||||
end |
||||
end |
||||
|
||||
def enable_consolidation?, do: @enable_consolidation |
||||
|
||||
defp address_token_transfers_counter_cache_period do |
||||
case Integer.parse(System.get_env("ADDRESS_TOKEN_TRANSFERS_COUNTER_CACHE_PERIOD", "")) do |
||||
{secs, ""} -> :timer.seconds(secs) |
||||
_ -> :timer.hours(1) |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue