parent
f77ff79a36
commit
fe78cbfe39
@ -1,75 +1,186 @@ |
||||
import $ from 'jquery' |
||||
|
||||
$(function () { |
||||
$('.js-become-candidate').on('click', function () { |
||||
$('#becomeCandidateModal').modal() |
||||
}) |
||||
|
||||
$('.js-validator-info-modal').on('click', function () { |
||||
$('#validatorInfoModal').modal() |
||||
}) |
||||
|
||||
$('.js-move-stake').on('click', function () { |
||||
$('#errorStatusModal').modal() |
||||
}) |
||||
|
||||
$('.js-remove-pool').on('click', function () { |
||||
$('#warningStatusModal').modal() |
||||
}) |
||||
|
||||
$('.js-copy-address').on('click', function () { |
||||
$('#successStatusModal').modal() |
||||
}) |
||||
|
||||
$('.js-stake-stake').on('click', function () { |
||||
const modal = '#stakeModal' |
||||
const progress = parseInt($(`${modal} .js-stakes-progress-data-progress`).text()) |
||||
const total = parseInt($(`${modal} .js-stakes-progress-data-total`).text()) |
||||
|
||||
$(modal).modal() |
||||
|
||||
setupStakesProgress(progress, total, modal) |
||||
}) |
||||
|
||||
$('.js-withdraw-stake').on('click', function () { |
||||
const modal = '#withdrawModal' |
||||
const progress = parseInt($(`${modal} .js-stakes-progress-data-progress`).text()) |
||||
const total = parseInt($(`${modal} .js-stakes-progress-data-total`).text()) |
||||
|
||||
$(modal).modal() |
||||
|
||||
setupStakesProgress(progress, total, modal) |
||||
}) |
||||
|
||||
function setupStakesProgress (progress, total, modal) { |
||||
// const stakeProgress = $(`${modal} .js-stakes-progress`)
|
||||
// const primaryColor = $('.btn-full-primary').css('background-color')
|
||||
// const backgroundColors = [
|
||||
// primaryColor,
|
||||
// 'rgba(202, 199, 226, 0.5)'
|
||||
// ]
|
||||
// const progressBackground = total - progress
|
||||
|
||||
// // eslint-disable-next-line no-unused-vars
|
||||
// const myChart = new window.Chart(stakeProgress, {
|
||||
// type: 'doughnut',
|
||||
// data: {
|
||||
// datasets: [{
|
||||
// data: [progress, progressBackground],
|
||||
// backgroundColor: backgroundColors,
|
||||
// hoverBackgroundColor: backgroundColors,
|
||||
// borderWidth: 0
|
||||
// }]
|
||||
// },
|
||||
// options: {
|
||||
// cutoutPercentage: 80,
|
||||
// legend: {
|
||||
// display: false
|
||||
// },
|
||||
// tooltips: {
|
||||
// enabled: false
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
let $currentModal = null |
||||
let modalLocked = false |
||||
|
||||
const spinner = |
||||
` |
||||
<span class="loading-spinner-small mr-2"> |
||||
<span class="loading-spinner-block-1"></span> |
||||
<span class="loading-spinner-block-2"></span> |
||||
</span> |
||||
` |
||||
|
||||
$(document.body).on('hide.bs.modal', e => { |
||||
if (modalLocked) { |
||||
e.preventDefault() |
||||
e.stopPropagation() |
||||
return false |
||||
} |
||||
|
||||
$currentModal = null |
||||
}) |
||||
|
||||
export function currentModal () { |
||||
return $currentModal |
||||
} |
||||
|
||||
export function openModal ($modal, unclosable) { |
||||
// Hide all tooltips before showing a modal,
|
||||
// since they are sticking on top of modal
|
||||
$('.tooltip').tooltip('hide') |
||||
|
||||
if (unclosable) { |
||||
$('.close-modal, .modal-status-button-wrapper', $modal).addClass('hidden') |
||||
$('.modal-status-text', $modal).addClass('m-b-0') |
||||
} |
||||
|
||||
if ($currentModal) { |
||||
modalLocked = false |
||||
|
||||
$currentModal |
||||
.one('hidden.bs.modal', () => { |
||||
$modal.modal('show') |
||||
$currentModal = $modal |
||||
if (unclosable) { |
||||
modalLocked = true |
||||
} |
||||
}) |
||||
.modal('hide') |
||||
} else { |
||||
$modal.modal('show') |
||||
$currentModal = $modal |
||||
if (unclosable) { |
||||
modalLocked = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
export function lockModal ($modal, $submitButton = null, spinnerText = '') { |
||||
$modal.find('.close-modal').attr('disabled', true) |
||||
|
||||
const $button = $submitButton || $modal.find('.btn-add-full') |
||||
|
||||
$button |
||||
.attr('data-text', $button.text()) |
||||
.attr('disabled', true) |
||||
|
||||
const $span = $('span', $button) |
||||
const waitHtml = spinner + (spinnerText ? ` ${spinnerText}` : '') |
||||
|
||||
if ($span.length) { |
||||
$('svg', $button).hide() |
||||
$span.html(waitHtml) |
||||
} else { |
||||
$button.html(waitHtml) |
||||
} |
||||
|
||||
modalLocked = true |
||||
} |
||||
|
||||
export function unlockModal ($modal, $submitButton = null) { |
||||
$modal.find('.close-modal').attr('disabled', false) |
||||
|
||||
const $button = $submitButton || $modal.find('.btn-add-full') |
||||
const buttonText = $button.attr('data-text') |
||||
|
||||
$button.attr('disabled', false) |
||||
|
||||
const $span = $('span', $button) |
||||
if ($span.length) { |
||||
$('svg', $button).show() |
||||
$span.text(buttonText) |
||||
} else { |
||||
$button.text(buttonText) |
||||
} |
||||
|
||||
modalLocked = false |
||||
} |
||||
|
||||
export function openErrorModal (title, text, unclosable) { |
||||
const $modal = $('#errorStatusModal') |
||||
$modal.find('.modal-status-title').text(title) |
||||
$modal.find('.modal-status-text').html(text) |
||||
openModal($modal, unclosable) |
||||
} |
||||
|
||||
export function openWarningModal (title, text) { |
||||
const $modal = $('#warningStatusModal') |
||||
$modal.find('.modal-status-title').text(title) |
||||
$modal.find('.modal-status-text').html(text) |
||||
openModal($modal) |
||||
} |
||||
|
||||
export function openSuccessModal (title, text) { |
||||
const $modal = $('#successStatusModal') |
||||
$modal.find('.modal-status-title').text(title) |
||||
$modal.find('.modal-status-text').html(text) |
||||
openModal($modal) |
||||
} |
||||
|
||||
export function openQuestionModal (title, text, acceptCallback = null, exceptCallback = null, acceptText = 'Yes', exceptText = 'No') { |
||||
const $modal = $('#questionStatusModal') |
||||
const $closeButton = $modal.find('.close-modal') |
||||
|
||||
$closeButton.attr('disabled', false) |
||||
|
||||
$modal.find('.modal-status-title').text(title) |
||||
$modal.find('.modal-status-text').text(text) |
||||
|
||||
const $accept = $modal.find('.btn-line.accept') |
||||
const $except = $modal.find('.btn-line.except') |
||||
|
||||
$accept |
||||
.removeAttr('data-dismiss') |
||||
.removeAttr('disabled') |
||||
.unbind('click') |
||||
.find('.btn-line-text').text(acceptText) |
||||
|
||||
$except |
||||
.removeAttr('data-dismiss') |
||||
.removeAttr('disabled') |
||||
.unbind('click') |
||||
.find('.btn-line-text').text(exceptText) |
||||
|
||||
if (acceptCallback) { |
||||
$accept.on('click', event => { |
||||
$closeButton.attr('disabled', true) |
||||
|
||||
$accept |
||||
.unbind('click') |
||||
.attr('disabled', true) |
||||
.find('.btn-line-text').html(spinner) |
||||
$except |
||||
.unbind('click') |
||||
.removeAttr('data-dismiss') |
||||
.attr('disabled', true) |
||||
|
||||
modalLocked = true |
||||
acceptCallback($modal, event) |
||||
}) |
||||
} else { |
||||
$accept.attr('data-dismiss', 'modal') |
||||
} |
||||
|
||||
if (exceptCallback) { |
||||
$except.on('click', event => { |
||||
$closeButton.attr('disabled', true) |
||||
|
||||
$except |
||||
.unbind('click') |
||||
.attr('disabled', true) |
||||
.find('.btn-line-text').html(spinner) |
||||
$accept |
||||
.unbind('click') |
||||
.attr('disabled', true) |
||||
.removeAttr('data-dismiss') |
||||
|
||||
modalLocked = true |
||||
exceptCallback($modal, event) |
||||
}) |
||||
} else { |
||||
$except.attr('data-dismiss', 'modal') |
||||
} |
||||
|
||||
openModal($modal) |
||||
} |
||||
|
@ -0,0 +1,99 @@ |
||||
import $ from 'jquery' |
||||
import { walletEnabled, getCurrentAccount } from './write.js' |
||||
import { openErrorModal, openWarningModal, openSuccessModal } from '../modals.js' |
||||
|
||||
const loadFunctions = (element) => { |
||||
const $element = $(element) |
||||
const url = $element.data('url') |
||||
const hash = $element.data('hash') |
||||
const type = $element.data('type') |
||||
const action = $element.data('action') |
||||
|
||||
$.get( |
||||
url, |
||||
{ hash: hash, type: type, action: action }, |
||||
response => $element.html(response) |
||||
) |
||||
.done(function () { |
||||
$('[data-function]').each((_, element) => { |
||||
readWriteFunction(element) |
||||
}) |
||||
}) |
||||
.fail(function (response) { |
||||
$element.html(response.statusText) |
||||
}) |
||||
} |
||||
|
||||
const readWriteFunction = (element) => { |
||||
const $element = $(element) |
||||
const $form = $element.find('[data-function-form]') |
||||
|
||||
const $responseContainer = $element.find('[data-function-response]') |
||||
|
||||
$form.on('submit', (event) => { |
||||
const action = $form.data('action') |
||||
event.preventDefault() |
||||
|
||||
if (action === 'read') { |
||||
const url = $form.data('url') |
||||
const $functionName = $form.find('input[name=function_name]') |
||||
const $functionInputs = $form.find('input[name=function_input]') |
||||
|
||||
const args = $.map($functionInputs, element => { |
||||
return $(element).val() |
||||
}) |
||||
|
||||
const data = { |
||||
function_name: $functionName.val(), |
||||
args |
||||
} |
||||
|
||||
$.get(url, data, response => $responseContainer.html(response)) |
||||
} else { |
||||
walletEnabled() |
||||
.then((isWalletEnabled) => { |
||||
if (isWalletEnabled) { |
||||
const functionName = $form.find('input[name=function_name]').val() |
||||
|
||||
const $functionInputs = $form.find('input[name=function_input]') |
||||
const args = $.map($functionInputs, element => { |
||||
return $(element).val() |
||||
}) |
||||
|
||||
const contractAddress = $form.data('contract-address') |
||||
const contractAbi = $form.data('contract-abi') |
||||
|
||||
getCurrentAccount() |
||||
.then(currentAccount => { |
||||
const TargetContract = new window.web3.eth.Contract(contractAbi, contractAddress) |
||||
|
||||
TargetContract.methods[functionName](...args).send({ from: currentAccount }) |
||||
.on('error', function (error) { |
||||
openErrorModal(`Error in sending transaction for method "${functionName}"`, error, false) |
||||
}) |
||||
.on('transactionHash', function (txHash) { |
||||
const getTxReceipt = (txHash) => { |
||||
window.web3.eth.getTransactionReceipt(txHash) |
||||
.then(txReceipt => { |
||||
if (txReceipt) { |
||||
openSuccessModal('Success', `Successfully sent <a href="/tx/${txHash}">transaction</a> for method "${functionName}"`) |
||||
clearInterval(txReceiptPollingIntervalId) |
||||
} |
||||
}) |
||||
} |
||||
const txReceiptPollingIntervalId = setInterval(() => { getTxReceipt(txHash) }, 5 * 1000) |
||||
}) |
||||
}) |
||||
} else { |
||||
openWarningModal('Unauthorized', 'You haven\'t approved the reading of account list from your MetaMask/Nifty wallet or MetaMask/Nifty wallet is not installed.') |
||||
} |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
|
||||
const container = $('[data-smart-contract-functions]') |
||||
|
||||
if (container.length) { |
||||
loadFunctions(container) |
||||
} |
@ -1,3 +1,3 @@ |
||||
import './read_only_functions' |
||||
import './functions' |
||||
import './wei_ether_converter' |
||||
import '../../app' |
||||
|
@ -1,54 +0,0 @@ |
||||
import $ from 'jquery' |
||||
|
||||
const loadFunctions = (element) => { |
||||
const $element = $(element) |
||||
const url = $element.data('url') |
||||
const hash = $element.data('hash') |
||||
const type = $element.data('type') |
||||
|
||||
$.get( |
||||
url, |
||||
{ hash: hash, type: type }, |
||||
response => $element.html(response) |
||||
) |
||||
.done(function () { |
||||
$('[data-function]').each((_, element) => { |
||||
readFunction(element) |
||||
}) |
||||
}) |
||||
.fail(function (response) { |
||||
$element.html(response.statusText) |
||||
}) |
||||
} |
||||
|
||||
const readFunction = (element) => { |
||||
const $element = $(element) |
||||
const $form = $element.find('[data-function-form]') |
||||
|
||||
const $responseContainer = $element.find('[data-function-response]') |
||||
|
||||
$form.on('submit', (event) => { |
||||
event.preventDefault() |
||||
|
||||
const url = $form.data('url') |
||||
const $functionName = $form.find('input[name=function_name]') |
||||
const $functionInputs = $form.find('input[name=function_input]') |
||||
|
||||
const args = $.map($functionInputs, element => { |
||||
return $(element).val() |
||||
}) |
||||
|
||||
const data = { |
||||
function_name: $functionName.val(), |
||||
args |
||||
} |
||||
|
||||
$.get(url, data, response => $responseContainer.html(response)) |
||||
}) |
||||
} |
||||
|
||||
const container = $('[data-smart-contract-functions]') |
||||
|
||||
if (container.length) { |
||||
loadFunctions(container) |
||||
} |
@ -0,0 +1,29 @@ |
||||
import Web3 from 'web3' |
||||
|
||||
export const walletEnabled = () => { |
||||
if (window.ethereum) { |
||||
window.web3 = new Web3(window.ethereum) |
||||
if (window.ethereum._state && window.ethereum._state.isUnlocked) { // Nifty Wallet
|
||||
window.web3 = new Web3(window.web3.currentProvider) |
||||
return Promise.resolve(true) |
||||
} else if (window.ethereum._state && window.ethereum._state.isUnlocked === false) { // Nifty Wallet
|
||||
return Promise.resolve(false) |
||||
} else { |
||||
window.ethereum.enable() |
||||
window.web3 = new Web3(window.web3.currentProvider) |
||||
return Promise.resolve(true) |
||||
} |
||||
} else if (window.web3) { |
||||
window.web3 = new Web3(window.web3.currentProvider) |
||||
return Promise.resolve(true) |
||||
} else { |
||||
return Promise.resolve(false) |
||||
} |
||||
} |
||||
|
||||
export const getCurrentAccount = async () => { |
||||
const accounts = await window.web3.eth.getAccounts() |
||||
const account = accounts[0] ? accounts[0].toLowerCase() : null |
||||
|
||||
return account |
||||
} |
@ -0,0 +1 @@ |
||||
import '../lib/smart_contract/write' |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@ |
||||
# credo:disable-for-this-file |
||||
# |
||||
# When moving the calls to ajax, this controller became very similar to the |
||||
# `address_contract_controller`, but both are necessary until we are able to |
||||
# address a better way to organize the controllers. |
||||
# |
||||
# So, for now, I'm adding this comment to disable the credo check for this file. |
||||
defmodule BlockScoutWeb.AddressWriteContractController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
alias Explorer.{Chain, Market} |
||||
alias Explorer.Chain.Address |
||||
alias Explorer.ExchangeRates.Token |
||||
alias Indexer.Fetcher.CoinBalanceOnDemand |
||||
|
||||
def index(conn, %{"address_id" => address_hash_string}) do |
||||
address_options = [ |
||||
necessity_by_association: %{ |
||||
:contracts_creation_internal_transaction => :optional, |
||||
:names => :optional, |
||||
:smart_contract => :optional, |
||||
:token => :optional, |
||||
:contracts_creation_transaction => :optional |
||||
} |
||||
] |
||||
|
||||
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), |
||||
{:ok, address} <- Chain.find_contract_address(address_hash, address_options, true), |
||||
false <- is_nil(address.smart_contract) do |
||||
render( |
||||
conn, |
||||
"index.html", |
||||
address: address, |
||||
type: :regular, |
||||
action: :write, |
||||
coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), |
||||
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), |
||||
counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}) |
||||
) |
||||
else |
||||
_ -> |
||||
not_found(conn) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1 @@ |
||||
<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> |
@ -0,0 +1,20 @@ |
||||
<section class="container"> |
||||
|
||||
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %> |
||||
|
||||
<div class="card"> |
||||
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %> |
||||
<!-- loaded via AJAX --> |
||||
<div class="card-body" data-smart-contract-functions data-hash="<%= to_string(@address.hash) %>" data-type="<%= @type %>" data-action="<%= @action %>" data-url="<%= smart_contract_path(@conn, :index) %>"> |
||||
<div> |
||||
<span class="loading-spinner-small mr-2"> |
||||
<span class="loading-spinner-block-1"></span> |
||||
<span class="loading-spinner-block-2"></span> |
||||
</span> |
||||
<%= gettext("Loading...") %> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/smart-contract-helpers.js") %>"></script> |
||||
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/write_contract.js") %>"></script> |
||||
</section> |
@ -1,7 +1,13 @@ |
||||
defmodule BlockScoutWeb.AddressReadContractView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
def queryable?(inputs), do: Enum.any?(inputs) |
||||
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs) |
||||
|
||||
def queryable?(inputs) when is_nil(inputs), do: false |
||||
|
||||
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs) |
||||
|
||||
def outputs?(outputs) when is_nil(outputs), do: false |
||||
|
||||
def address?(type), do: type == "address" |
||||
end |
||||
|
@ -1,7 +1,13 @@ |
||||
defmodule BlockScoutWeb.AddressReadProxyView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
def queryable?(inputs), do: Enum.any?(inputs) |
||||
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs) |
||||
|
||||
def queryable?(inputs) when is_nil(inputs), do: false |
||||
|
||||
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs) |
||||
|
||||
def outputs?(outputs) when is_nil(outputs), do: false |
||||
|
||||
def address?(type), do: type == "address" |
||||
end |
||||
|
@ -0,0 +1,13 @@ |
||||
defmodule BlockScoutWeb.AddressWriteContractView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
def queryable?(inputs) when not is_nil(inputs), do: Enum.any?(inputs) |
||||
|
||||
def queryable?(inputs) when is_nil(inputs), do: false |
||||
|
||||
def outputs?(outputs) when not is_nil(outputs), do: Enum.any?(outputs) |
||||
|
||||
def outputs?(outputs) when is_nil(outputs), do: false |
||||
|
||||
def address?(type), do: type == "address" |
||||
end |
@ -0,0 +1,28 @@ |
||||
defmodule Explorer.SmartContract.Writer do |
||||
@moduledoc """ |
||||
Generates smart-contract transactions |
||||
""" |
||||
|
||||
alias Explorer.Chain |
||||
|
||||
@spec write_functions(Hash.t()) :: [%{}] |
||||
def write_functions(contract_address_hash) do |
||||
abi = |
||||
contract_address_hash |
||||
|> Chain.address_hash_to_smart_contract() |
||||
|> Map.get(:abi) |
||||
|
||||
case abi do |
||||
nil -> |
||||
[] |
||||
|
||||
_ -> |
||||
abi |
||||
|> Enum.filter( |
||||
&(&1["type"] !== "event" && |
||||
(&1["stateMutability"] == "nonpayable" || &1["stateMutability"] == "payable" || &1["payable"] || |
||||
(!&1["payable"] && !&1["constant"] && !&1["stateMutability"]))) |
||||
) |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue