Merge remote-tracking branch 'origin/develop' into add-tokens-validation

This commit is contained in:
Victor Baranov 2018-10-16 18:06:02 +03:00
commit 53b3802396
16 changed files with 959 additions and 87 deletions

View File

@ -15,6 +15,7 @@ const UnlockScreen = require('./unlock')
// accounts
const AccountDetailScreen = require('./account-detail')
const SendTransactionScreen = require('./send')
const SendTokenScreen = require('./send-token')
const ConfirmTxScreen = require('./conf-tx')
// notice
const NoticeScreen = require('./components/notice')
@ -577,6 +578,10 @@ App.prototype.renderPrimary = function () {
log.debug('rendering send tx screen')
return h(SendTransactionScreen, {key: 'send-transaction'})
case 'sendToken':
log.debug('rendering send tx screen')
return h(SendTokenScreen, {key: 'send-token'})
case 'newKeychain':
log.debug('rendering new keychain screen')
return h(NewKeyChainScreen, {key: 'new-keychain'})

View File

@ -16,9 +16,9 @@ function EthBalanceComponent () {
EthBalanceComponent.prototype.render = function () {
var props = this.props
let { value } = props
const { style, width, network } = props
const { style, width, network, isToken, tokenSymbol } = props
var needsParse = this.props.needsParse !== undefined ? this.props.needsParse : true
value = value ? formatBalance(value, 6, needsParse, network) : '...'
value = value ? formatBalance(value, 6, needsParse, network, isToken, tokenSymbol) : '...'
return (

View File

@ -12,6 +12,7 @@ const util = require('../util')
const MiniAccountPanel = require('./mini-account-panel')
const Copyable = require('./copyable')
const EthBalance = require('./eth-balance')
const TokenBalance = require('./token-balance')
const addressSummary = util.addressSummary
const accountSummary = util.accountSummary
const nameForAddress = require('../../lib/contract-namer')
@ -20,6 +21,9 @@ const { getEnvironmentType } = require('../../../app/scripts/lib/util')
const NetworkIndicator = require('../components/network')
const { ENVIRONMENT_TYPE_NOTIFICATION } = require('../../../app/scripts/lib/enums')
const connect = require('react-redux').connect
const abiDecoder = require('abi-decoder')
const { tokenInfoGetter, calcTokenAmount } = require('../../../ui/app/token-util')
const BigNumber = require('bignumber.js')
const MIN_GAS_PRICE_BN = new BN('0')
const MIN_GAS_LIMIT_BN = new BN('21000')
@ -32,7 +36,11 @@ function PendingTx () {
valid: true,
txData: null,
submitting: false,
tokenSymbol: '',
tokenDecimals: 0,
tokenDataRetrieved: false,
}
this.tokenInfoGetter = tokenInfoGetter()
}
function mapStateToProps (state) {
@ -57,12 +65,32 @@ function mapStateToProps (state) {
}
PendingTx.prototype.render = function () {
const state = this.state
if (!state.tokenDataRetrieved) return null
const props = this.props
const { currentCurrency, blockGasLimit, network, provider, isUnlocked } = props
const conversionRate = props.conversionRate
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
let { isToken, tokensToSend, tokensTransferTo } = props
let token = {
address: txParams.to,
}
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
if (decodedData && decodedData.name === 'transfer') {
isToken = true
const tokenValBN = new BigNumber(calcTokenAmount(decodedData.params[1].value, state.tokenDecimals))
const multiplier = Math.pow(10, 18)
tokensToSend = tokenValBN.mul(multiplier).toString(16)
tokensTransferTo = decodedData.params[0].value
token = {
address: txParams.to,
decimals: state.tokenDecimals,
symbol: state.tokenSymbol,
}
}
// Allow retry txs
const { lastGasPrice } = txMeta
@ -217,7 +245,10 @@ PendingTx.prototype.render = function () {
fontFamily: 'Nunito Regular',
},
}, [
h(EthBalance, {
isToken ? h(TokenBalance, {
token,
fontSize: '12px',
}) : h(EthBalance, {
fontSize: '12px',
value: balance,
conversionRate,
@ -231,7 +262,7 @@ PendingTx.prototype.render = function () {
forwardCarrat(),
this.miniAccountPanelForRecipient(),
this.miniAccountPanelForRecipient(isToken, tokensTransferTo),
]),
h('style', `
@ -326,7 +357,17 @@ PendingTx.prototype.render = function () {
// in the way that gas and gasLimit currently are.
h('.row', [
h('.cell.label', 'Amount'),
h(EthBalance, { valueStyle, dimStyle, value: txParams.value, currentCurrency, conversionRate, network }),
h(EthBalance, {
valueStyle,
dimStyle,
value: isToken ? tokensToSend/* (new BN(tokensToSend)).mul(1e18)*/ : txParams.value,
currentCurrency,
conversionRate,
network,
isToken,
tokenSymbol: this.state.tokenSymbol,
showFiat: !isToken,
}),
]),
// Gas Limit (customizable)
@ -381,7 +422,14 @@ PendingTx.prototype.render = function () {
// Max Transaction Fee (calculated)
h('.cell.row', [
h('.cell.label', 'Max Transaction Fee'),
h(EthBalance, { valueStyle, dimStyle, value: txFeeBn.toString(16), currentCurrency, conversionRate, network }),
h(EthBalance, {
valueStyle,
dimStyle,
value: txFeeBn.toString(16),
currentCurrency,
conversionRate,
network,
}),
]),
h('.cell.row', {
@ -482,11 +530,12 @@ PendingTx.prototype.render = function () {
)
}
PendingTx.prototype.miniAccountPanelForRecipient = function () {
PendingTx.prototype.miniAccountPanelForRecipient = function (isToken, tokensTransferTo) {
const props = this.props
const txData = props.txData
const txParams = txData.txParams || {}
const isContractDeploy = !('to' in txParams)
const to = isToken ? tokensTransferTo : txParams.to
// If it's not a contract deploy, send to the account
if (!isContractDeploy) {
@ -506,17 +555,17 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () {
display: 'inline-block',
whiteSpace: 'nowrap',
},
}, accountSummary(nameForAddress(txParams.to, props.identities)), 6, 4),
}, accountSummary(nameForAddress(to, props.identities)), 6, 4),
h(Copyable, {
value: ethUtil.toChecksumAddress(txParams.to),
value: ethUtil.toChecksumAddress(to),
}, [
h('span.font-small', {
style: {
fontFamily: 'Nunito Regular',
color: 'rgba(255, 255, 255, 0.7)',
},
}, addressSummary(txParams.to, 6, 4, false)),
}, addressSummary(to, 6, 4, false)),
]),
]),
])
@ -536,6 +585,29 @@ PendingTx.prototype.miniAccountPanelForRecipient = function () {
}
}
PendingTx.prototype.componentWillMount = function () {
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
this.updateTokenInfo(txParams)
}
PendingTx.prototype.componentWillUnmount = function () {
this.setState({
tokenSymbol: '',
tokenDecimals: 0,
tokenDataRetrieved: false,
})
}
PendingTx.prototype.updateTokenInfo = async function (txParams) {
const tokenParams = await this.tokenInfoGetter(txParams.to)
this.setState({
tokenSymbol: tokenParams.symbol,
tokenDecimals: tokenParams.decimals,
tokenDataRetrieved: true,
})
}
PendingTx.prototype.gasPriceChanged = function (newBN, valid) {
log.info(`Gas price changed to: ${newBN.toString(10)}`)
const txMeta = this.gatherTxMeta()

View File

@ -0,0 +1,143 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
const TokenTracker = require('eth-token-watcher')
const connect = require('react-redux').connect
const selectors = require('../../../ui/app/selectors')
const log = require('loglevel')
function mapStateToProps (state) {
return {
userAddress: selectors.getSelectedAddress(state),
}
}
module.exports = connect(mapStateToProps)(TokenBalance)
inherits(TokenBalance, Component)
function TokenBalance () {
this.state = {
string: '',
symbol: '',
isLoading: true,
error: null,
}
Component.call(this)
}
TokenBalance.prototype.render = function () {
const state = this.state
const props = this.props
const { symbol, string, isLoading } = state
const { balanceOnly } = this.props
const valueStyle = props.valueStyle ? props.valueStyle : {
color: '#ffffff',
width: '100%',
fontSize: props.fontSize || '14px',
textAlign: 'right',
}
const dimStyle = props.dimStyle ? props.dimStyle : {
color: ' #60db97',
fontSize: props.fontSize || '14px',
marginLeft: '5px',
}
return isLoading
? h('div', '')
: h('.flex-row', {
style: {
alignItems: 'flex-end',
lineHeight: '20px',
textRendering: 'geometricPrecision',
},
}, [
h('div.hide-text-overflow.token-balance__amount', {
style: valueStyle,
}, string),
!balanceOnly && h('span.token-balance__symbol', {
style: dimStyle,
}, symbol),
])
}
TokenBalance.prototype.componentDidMount = function () {
this.createFreshTokenTracker()
}
TokenBalance.prototype.createFreshTokenTracker = function () {
if (this.tracker) {
// Clean up old trackers when refreshing:
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
}
if (!global.ethereumProvider) return
const { userAddress, token } = this.props
this.tracker = new TokenTracker({
userAddress,
provider: global.ethereumProvider,
tokens: [token],
pollingInterval: 8000,
})
// Set up listener instances for cleaning up
this.balanceUpdater = this.updateBalance.bind(this)
this.showError = error => {
this.setState({ error, isLoading: false })
}
this.tracker.on('update', this.balanceUpdater)
this.tracker.on('error', this.showError)
this.tracker.updateBalances()
.then(() => {
this.updateBalance(this.tracker.serialize())
})
.catch((reason) => {
log.error(`Problem updating balances`, reason)
this.setState({ isLoading: false })
})
}
TokenBalance.prototype.componentDidUpdate = function (nextProps) {
const {
userAddress: oldAddress,
token: { address: oldTokenAddress },
} = this.props
const {
userAddress: newAddress,
token: { address: newTokenAddress },
} = nextProps
if ((!oldAddress || !newAddress) && (!oldTokenAddress || !newTokenAddress)) return
if ((oldAddress === newAddress) && (oldTokenAddress === newTokenAddress)) return
this.setState({ isLoading: true })
this.createFreshTokenTracker()
}
TokenBalance.prototype.updateBalance = function (tokens = []) {
if (!this.tracker.running) {
return
}
const [{ string, symbol }] = tokens
this.setState({
string,
symbol,
isLoading: false,
})
}
TokenBalance.prototype.componentWillUnmount = function () {
if (!this.tracker) return
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
}

View File

@ -7,11 +7,11 @@ const Dropdown = require('./dropdown').Dropdown
const DropdownMenuItem = require('./dropdown').DropdownMenuItem
const ethUtil = require('ethereumjs-util')
const copyToClipboard = require('copy-to-clipboard')
const actions = require('../../../ui/app/actions')
const connect = require('react-redux').connect
const tokenCellDropDownPrefix = 'token-cell_dropdown_'
module.exports = TokenCell
inherits(TokenCell, Component)
function TokenCell () {
Component.call(this)
@ -76,7 +76,7 @@ TokenCell.prototype.render = function () {
}
TokenCell.prototype.renderTokenOptions = function (menuToTop, ind) {
const { address, symbol, string, network, userAddress } = this.props
const { address, symbol, string, network, userAddress, showSendTokenPage } = this.props
const { optionsMenuActive } = this.state
return h(
@ -84,9 +84,9 @@ TokenCell.prototype.renderTokenOptions = function (menuToTop, ind) {
{
style: {
position: 'relative',
marginLeft: '-263px',
marginLeft: menuToTop ? '-273px' : '-263px',
minWidth: '180px',
marginTop: menuToTop ? '-200px' : '30px',
marginTop: menuToTop ? '-214px' : '30px',
width: '280px',
},
isOpen: optionsMenuActive,
@ -100,6 +100,16 @@ TokenCell.prototype.renderTokenOptions = function (menuToTop, ind) {
},
},
[
h(
DropdownMenuItem,
{
closeMenu: () => {},
onClick: () => {
showSendTokenPage(address)
},
},
`Send`,
),
h(
DropdownMenuItem,
{
@ -159,3 +169,11 @@ function tokenFactoryFor (tokenAddress) {
return `https://tokenfactory.surge.sh/#/token/${tokenAddress}`
}
const mapDispatchToProps = dispatch => {
return {
showSendTokenPage: (tokenAddress) => dispatch(actions.showSendTokenPage(tokenAddress)),
}
}
module.exports = connect(null, mapDispatchToProps)(TokenCell)

View File

@ -16,22 +16,28 @@ const Loading = require('./components/loading')
module.exports = connect(mapStateToProps)(ConfirmTxScreen)
function mapStateToProps (state) {
const { metamask, appState } = state
const { screenParams, pendingTxIndex } = appState.currentView
return {
identities: state.metamask.identities,
accounts: state.metamask.accounts,
selectedAddress: state.metamask.selectedAddress,
unapprovedTxs: state.metamask.unapprovedTxs,
unapprovedMsgs: state.metamask.unapprovedMsgs,
unapprovedPersonalMsgs: state.metamask.unapprovedPersonalMsgs,
unapprovedTypedMessages: state.metamask.unapprovedTypedMessages,
index: state.appState.currentView.pendingTxIndex || 0,
warning: state.appState.warning,
network: state.metamask.network,
provider: state.metamask.provider,
conversionRate: state.metamask.conversionRate,
currentCurrency: state.metamask.currentCurrency,
blockGasLimit: state.metamask.currentBlockGasLimit,
computedBalances: state.metamask.computedBalances,
identities: metamask.identities,
accounts: metamask.accounts,
selectedAddress: metamask.selectedAddress,
unapprovedTxs: metamask.unapprovedTxs,
unapprovedMsgs: metamask.unapprovedMsgs,
unapprovedPersonalMsgs: metamask.unapprovedPersonalMsgs,
unapprovedTypedMessages: metamask.unapprovedTypedMessages,
index: pendingTxIndex || 0,
warning: appState.warning,
network: metamask.network,
provider: metamask.provider,
conversionRate: metamask.conversionRate,
currentCurrency: metamask.currentCurrency,
blockGasLimit: metamask.currentBlockGasLimit,
computedBalances: metamask.computedBalances,
isToken: (screenParams && screenParams.isToken),
tokenSymbol: (screenParams && screenParams.tokenSymbol),
tokensToSend: (screenParams && screenParams.tokensToSend),
tokensTransferTo: (screenParams && screenParams.tokensTransferTo),
}
}
@ -95,6 +101,10 @@ ConfirmTxScreen.prototype.render = function () {
unconfTxListLength,
computedBalances,
network,
isToken: props.isToken,
tokenSymbol: props.tokenSymbol,
tokensToSend: props.tokensToSend,
tokensTransferTo: props.tokensTransferTo,
// Actions
buyEth: this.buyEth.bind(this, txParams.from || props.selectedAddress),
sendTransaction: this.sendTransaction.bind(this),

399
old-ui/app/send-token.js Normal file
View File

@ -0,0 +1,399 @@
const inherits = require('util').inherits
const PersistentForm = require('../lib/persistent-form')
const h = require('react-hyperscript')
const connect = require('react-redux').connect
const Identicon = require('./components/identicon')
const actions = require('../../ui/app/actions')
const util = require('./util')
const numericBalance = require('./util').numericBalance
const addressSummary = require('./util').addressSummary
const TokenBalance = require('./components/token-balance')
const EnsInput = require('./components/ens-input')
const ethUtil = require('ethereumjs-util')
const { tokenInfoGetter, calcTokenAmountWithDec } = require('../../ui/app/token-util')
const TokenTracker = require('eth-token-watcher')
const Loading = require('./components/loading')
const BigNumber = require('bignumber.js')
BigNumber.config({ ERRORS: false })
const log = require('loglevel')
module.exports = connect(mapStateToProps)(SendTransactionScreen)
function mapStateToProps (state) {
var result = {
address: state.metamask.selectedAddress,
accounts: state.metamask.accounts,
identities: state.metamask.identities,
warning: state.appState.warning,
network: state.metamask.network,
addressBook: state.metamask.addressBook,
tokenAddress: state.appState.currentView.tokenAddress,
}
result.error = result.warning && result.warning.split('.')[0]
result.account = result.accounts[result.address]
result.identity = result.identities[result.address]
result.balance = result.account ? numericBalance(result.account.balance) : null
return result
}
inherits(SendTransactionScreen, PersistentForm)
function SendTransactionScreen () {
this.state = {
token: {
address: '',
symbol: '',
balance: 0,
decimals: 0,
},
isLoading: true,
}
PersistentForm.call(this)
}
SendTransactionScreen.prototype.render = function () {
const { isLoading, token } = this.state
if (isLoading) {
return h(Loading, {
isLoading: isLoading,
loadingMessage: 'Loading...',
})
}
this.persistentFormParentId = 'send-tx-form'
const props = this.props
const {
address,
identity,
network,
identities,
addressBook,
} = props
return (
h('.send-screen.flex-column.flex-grow', [
//
// Sender Profile
//
h('.account-data-subsection.flex-row.flex-grow', {
style: {
background: 'linear-gradient(rgb(84, 36, 147), rgb(104, 45, 182))',
padding: '30px',
},
}, [
// header - identicon + nav
h('.flex-row.flex-space-between', [
// large identicon
h('.identicon-wrapper.flex-column.flex-center.select-none', {
style: {
display: 'inline-block',
},
}, [
h(Identicon, {
diameter: 62,
address: address,
}),
]),
// invisible place holder
h('i.fa.fa-users.fa-lg.invisible', {
style: {
marginTop: '28px',
},
}),
]),
// account label
h('.flex-column', {
style: {
alignItems: 'flex-start',
},
}, [
h('h2.font-medium.flex-center', {
style: {
color: '#ffffff',
paddingTop: '8px',
marginBottom: '8px',
},
}, identity && identity.name),
// address and getter actions
h('.flex-row.flex-center', {
style: {
color: 'rgba(255, 255, 255, 0.7)',
marginBottom: '30px',
},
}, [
h('div', {
style: {
lineHeight: '16px',
fontSize: '14px',
},
}, addressSummary(address)),
]),
// balance
h('.flex-row.flex-center', [
h(TokenBalance, {
token,
}),
]),
]),
]),
//
// Required Fields
//
h('h3.flex-center', {
style: {
color: '#333333',
marginTop: '18px',
marginBottom: '14px',
},
}, [
// back button
h('i.fa.fa-arrow-left.fa-lg.cursor-pointer', {
style: {
position: 'absolute',
left: '30px',
},
onClick: this.back.bind(this),
}),
`Send ${this.state.token.symbol} Tokens`,
]),
// error message
props.error && h('div', {style: {
marginLeft: '30px',
marginRight: '30px',
}}, [
h('div.error.flex-center', props.error),
]),
// 'to' field
h('section.flex-row.flex-center', [
h(EnsInput, {
name: 'address',
placeholder: 'Recipient Address',
onChange: this.recipientDidChange.bind(this),
network,
identities,
addressBook,
}),
]),
// 'amount' and send button
h('section.flex-row.flex-center', [
h('input.large-input', {
name: 'amount',
placeholder: 'Amount',
type: 'number',
style: {
marginRight: '6px',
},
dataset: {
persistentFormId: 'tx-amount',
},
}),
h('button', {
onClick: this.onSubmit.bind(this),
}, 'Next'),
]),
])
)
}
SendTransactionScreen.prototype.componentDidMount = function () {
this.getTokensMetadata()
.then(() => {
this.createFreshTokenTracker()
})
}
SendTransactionScreen.prototype.getTokensMetadata = async function () {
this.setState({isLoading: true})
this.tokenInfoGetter = tokenInfoGetter()
const { tokenAddress, network } = this.props
const { symbol = '', decimals = 0 } = await this.tokenInfoGetter(tokenAddress)
this.setState({
token: {
address: tokenAddress,
network,
symbol,
decimals,
},
})
return Promise.resolve()
}
SendTransactionScreen.prototype.componentDidUnmount = function () {
this.props.dispatch(actions.displayWarning(''))
if (!this.tracker) return
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
}
SendTransactionScreen.prototype.createFreshTokenTracker = function () {
this.setState({isLoading: true})
const { address, tokenAddress } = this.props
if (!util.isValidAddress(tokenAddress)) return
if (this.tracker) {
// Clean up old trackers when refreshing:
this.tracker.stop()
this.tracker.removeListener('update', this.balanceUpdater)
this.tracker.removeListener('error', this.showError)
}
if (!global.ethereumProvider) return
this.tracker = new TokenTracker({
userAddress: address,
provider: global.ethereumProvider,
tokens: [this.state.token],
pollingInterval: 8000,
})
// Set up listener instances for cleaning up
this.balanceUpdater = this.updateBalances.bind(this)
this.showError = (error) => {
this.setState({ error, isLoading: false })
}
this.tracker.on('update', this.balanceUpdater)
this.tracker.on('error', this.showError)
this.tracker.updateBalances()
.then(() => {
this.updateBalances(this.tracker.serialize())
})
.catch((reason) => {
log.error(`Problem updating balances`, reason)
this.setState({ isLoading: false })
})
}
SendTransactionScreen.prototype.updateBalances = function (tokens) {
if (!this.tracker.running) {
return
}
this.setState({ token: (tokens && tokens[0]), isLoading: false })
}
SendTransactionScreen.prototype.navigateToAccounts = function (event) {
event.stopPropagation()
this.props.dispatch(actions.showAccountsPage())
}
SendTransactionScreen.prototype.back = function () {
var address = this.props.address
this.props.dispatch(actions.backToAccountDetail(address))
}
SendTransactionScreen.prototype.recipientDidChange = function (recipient, nickname) {
this.setState({
recipient: recipient,
nickname: nickname,
})
}
SendTransactionScreen.prototype.onSubmit = async function () {
const state = this.state || {}
const { token } = state
const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '')
const nickname = state.nickname || ' '
const input = document.querySelector('input[name="amount"]').value
const parts = input.split('.')
let message
if (isNaN(input) || input === '') {
message = 'Invalid token\'s amount.'
return this.props.dispatch(actions.displayWarning(message))
}
if (parts[1]) {
var decimal = parts[1]
if (decimal.length > 18) {
message = 'Token\'s amount is too precise.'
return this.props.dispatch(actions.displayWarning(message))
}
}
const tokenAddress = ethUtil.addHexPrefix(token.address)
const tokensValueWithoutDec = new BigNumber(input)
const tokensValueWithDec = new BigNumber(calcTokenAmountWithDec(input, token.decimals))
if (tokensValueWithDec.gt(token.balance)) {
message = 'Insufficient token\'s balance.'
return this.props.dispatch(actions.displayWarning(message))
}
if (input < 0) {
message = 'Can not send negative amounts of ETH.'
return this.props.dispatch(actions.displayWarning(message))
}
if ((util.isInvalidChecksumAddress(recipient))) {
message = 'Recipient address checksum is invalid.'
return this.props.dispatch(actions.displayWarning(message))
}
if (!util.isValidAddress(recipient) || (!recipient)) {
message = 'Recipient address is invalid.'
return this.props.dispatch(actions.displayWarning(message))
}
this.props.dispatch(actions.hideWarning())
this.props.dispatch(actions.addToAddressBook(recipient, nickname))
var txParams = {
from: this.props.address,
value: '0x',
}
const toAddress = ethUtil.addHexPrefix(recipient)
txParams.to = tokenAddress
const tokensAmount = `0x${input.toString(16)}`
const encoded = this.generateTokenTransferData({toAddress, amount: tokensAmount})
txParams.data = encoded
const confTxScreenParams = {
isToken: true,
tokenSymbol: token.symbol,
tokensToSend: tokensValueWithoutDec,
tokensTransferTo: toAddress,
}
this.props.dispatch(actions.signTokenTx(tokenAddress, toAddress, tokensValueWithDec, txParams, confTxScreenParams))
}
SendTransactionScreen.prototype.generateTokenTransferData = function ({ toAddress = '0x0', amount = '0x0' }) {
const TOKEN_TRANSFER_FUNCTION_SIGNATURE = '0xa9059cbb'
const abi = require('ethereumjs-abi')
return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
abi.rawEncode(['address', 'uint256'], [toAddress, ethUtil.addHexPrefix(amount)]),
x => ('00' + x.toString(16)).slice(-2)
).join('')
}

View File

@ -260,7 +260,7 @@ SendTransactionScreen.prototype.onSubmit = function () {
const recipient = state.recipient || document.querySelector('input[name="address"]').value.replace(/^[.\s]+|[.\s]+$/g, '')
const nickname = state.nickname || ' '
const input = document.querySelector('input[name="amount"]').value
const parts = input.split('')
const parts = input.split('.')
let message

View File

@ -111,10 +111,11 @@ function parseBalance (balance) {
// Takes wei hex, returns an object with three properties.
// Its "formatted" property is what we generally use to render values.
function formatBalance (balance, decimalsToKeep, needsParse = true, network) {
function formatBalance (balance, decimalsToKeep, needsParse = true, network, isToken, tokenSymbol) {
const isSokol = parseInt(network) === 77
const isPOA = parseInt(network) === 99
const coinName = isPOA ? 'POA' : isSokol ? 'SPOA' : 'ETH'
const assetName = isToken ? tokenSymbol : coinName
var parsed = needsParse ? parseBalance(balance) : balance.split('.')
var beforeDecimal = parsed[0]
var afterDecimal = parsed[1]
@ -124,14 +125,14 @@ function formatBalance (balance, decimalsToKeep, needsParse = true, network) {
if (afterDecimal !== '0') {
var sigFigs = afterDecimal.match(/^0*(.{2})/) // default: grabs 2 most significant digits
if (sigFigs) { afterDecimal = sigFigs[0] }
formatted = '0.' + afterDecimal + ` ${coinName}`
formatted = '0.' + afterDecimal + ` ${assetName}`
}
} else {
formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${coinName}`
formatted = beforeDecimal + '.' + afterDecimal.slice(0, 3) + ` ${assetName}`
}
} else {
afterDecimal += Array(decimalsToKeep).join('0')
formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${coinName}`
formatted = beforeDecimal + '.' + afterDecimal.slice(0, decimalsToKeep) + ` ${assetName}`
}
return formatted
}

View File

@ -8,9 +8,11 @@ module.exports = {
token: {
menu: By.id('token-cell_dropdown_0'),
items: By.className('dropdown-menu-item'),
view: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(2)'),
copy: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(3)'),
remove: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(4)'),
send: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(2)'),
view: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(3)'),
copy: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(4)'),
remove: By.css('#token-cell_dropdown_0 > div > div > li:nth-child(5)'),
sendText: 'Send',
viewText: 'View token on block explorer',
copyText: 'Copy address to clipboard',
removeText: 'Remove',
@ -25,6 +27,7 @@ module.exports = {
},
account: {
account1: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(2) > span'),
account2: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span'),
menu: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div'),
delete: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(4) > div.remove'),
createAccount: By.css('#app-content > div > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span'),
@ -43,6 +46,29 @@ module.exports = {
},
},
screens: {
sendTokens: {
error: By.className('error flex-center'),
errorText: {
invalidAmount: 'Invalid token\'s amount',
address: 'Recipient address is invalid',
largeAmount: 'Insufficient token\'s balance',
tooPrecise: 'Token\'s amount is too precise',
negativeAmount: 'Can not send negative amounts of ETH',
},
title: By.className('flex-center'),
balance: By.className('hide-text-overflow token-balance__amount'),
symbol: By.className('token-balance__symbol'),
field: {
address: By.name('address'),
addressPlaceholder: 'Recipient Address',
amount: By.name('amount'),
amountPlaceholder: 'Amount',
},
button: {
next: By.xpath('//*[@id="app-content"]/div/div[4]/div/section[2]/button'),
arrow: By.className('fa fa-arrow-left fa-lg cursor-pointer'),
},
},
yourPR: {
key: By.css('#app-content > div > div.app-primary.from-right > div > div.privateKey > div.flex-row > p'),
copy: By.className('clipboard cursor-pointer'),
@ -120,14 +146,17 @@ module.exports = {
titleText: 'Delete Custom RPC',
},
confirmTransaction: {
buttons: {
title: By.className('flex-row flex-center'),
amount: By.css('#pending-tx-form > div:nth-child(1) > div.table-box > div:nth-child(2) > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(1)'),
symbol: By.css('#pending-tx-form > div:nth-child(1) > div.table-box > div:nth-child(2) > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(2)'),
button: {
submit: By.css('#pending-tx-form > div.flex-row.flex-space-around.conf-buttons > input'),
},
},
sendTransaction: {
title: By.css('#app-content > div > div.app-primary.from-right > div > h3:nth-child(2)'),
titleText: 'Send Transaction',
fields: {
field: {
address: By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(3) > div > input'),
amount: By.css('#app-content > div > div.app-primary.from-right > div > section:nth-child(4) > input'),
},
@ -179,7 +208,8 @@ module.exports = {
},
main: {
identicon: By.className('identicon-wrapper select-none'),
accountName: By.className('sizing-input'),
fieldAccountName: By.className('sizing-input'),
accountName: By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.name-label > div > div > h2'),
edit: By.className('edit-text'),
iconCopy: By.className('clipboard cursor-pointer white'),
transactionList: By.css('#app-content > div > div.app-primary.from-left > div > section > section > div > div > div > div.ether-balance.ether-balance-amount > div > div > div > div:nth-child(1)'),
@ -199,7 +229,7 @@ module.exports = {
tokens: {
menu: By.className('inactiveForm pointer'),
token: By.className('token-cell'),
balance: By.css('#app-content > div > div.app-primary.from-left > div > section > div.full-flex-height > ol > li:nth-child(2) > h3'),
balance: By.css('#token-cell_0 > h3'),
amount: By.css('#app-content > div > div.app-primary.from-left > div > section > div.full-flex-height > div > span'),
textNoTokens: 'No tokens found',
textYouOwn1token: 'You own 1 token',

View File

@ -134,13 +134,13 @@ describe('Metamask popup page', async function () {
await menu.click()
const field = await waitUntilShowUp(screens.main.edit)
await field.click()
const accountName = await waitUntilShowUp(screens.main.accountName)
const accountName = await waitUntilShowUp(screens.main.fieldAccountName)
assert.notEqual(accountName, false, '\'Account name\' change dialog isn\'t opened')
assert.equal(await accountName.getAttribute('value'), 'Account 1', 'incorrect account name')
})
it('fill out new account\'s name', async () => {
const field = await waitUntilShowUp(screens.main.accountName)
const field = await waitUntilShowUp(screens.main.fieldAccountName)
await field.clear()
await field.sendKeys(newAccountName)
})
@ -150,7 +150,7 @@ describe('Metamask popup page', async function () {
assert.equal(await button.getText(), 'Save', 'button has incorrect name')
assert.notEqual(button, true, 'button \'Save\' does not present')
await click(button)
const accountName = await waitUntilShowUp(screens.main.accountName, 10)
const accountName = await waitUntilShowUp(screens.main.fieldAccountName, 10)
assert.equal(accountName, false, '\'Account name\' change dialog isn\'t opened')
})
@ -587,8 +587,8 @@ describe('Metamask popup page', async function () {
it('adds recipient address and amount', async function () {
const sendTranscationScreen = await waitUntilShowUp(screens.sendTransaction.title)
assert.equal(await sendTranscationScreen.getText(), screens.sendTransaction.titleText, 'Transaction screen has incorrect titlr')
const inputAddress = await waitUntilShowUp(screens.sendTransaction.fields.address)
const inputAmmount = await waitUntilShowUp(screens.sendTransaction.fields.amount)
const inputAddress = await waitUntilShowUp(screens.sendTransaction.field.address)
const inputAmmount = await waitUntilShowUp(screens.sendTransaction.field.amount)
await inputAddress.sendKeys('0x2f318C334780961FB129D2a6c30D0763d9a5C970')
await inputAmmount.sendKeys('10')
const button = await waitUntilShowUp(screens.sendTransaction.buttonNext)
@ -597,7 +597,7 @@ describe('Metamask popup page', async function () {
})
it('confirms transaction', async function () {
const button = await waitUntilShowUp(screens.confirmTransaction.buttons.submit)
const button = await waitUntilShowUp(screens.confirmTransaction.button.submit)
assert.equal(await button.getAttribute('value'), 'Submit', 'button has incorrect name')
await click(button)
})
@ -924,7 +924,7 @@ describe('Metamask popup page', async function () {
it('confirms transaction in MetaMask popup', async function () {
const windowHandles = await driver.getAllWindowHandles()
await driver.switchTo().window(windowHandles[windowHandles.length - 1])
const button = await waitUntilShowUp(screens.confirmTransaction.buttons.submit)
const button = await waitUntilShowUp(screens.confirmTransaction.button.submit)
await click(button)
})
@ -949,6 +949,9 @@ describe('Metamask popup page', async function () {
it('navigates to the add token screen', async function () {
await waitUntilShowUp(screens.main.identicon)
const tab = await waitUntilShowUp(screens.main.tokens.menu)
await tab.click()
const addTokenButton = await waitUntilShowUp(screens.main.tokens.buttonAdd)
assert.equal(await addTokenButton.getText(), screens.main.tokens.buttonAddText)
await click(addTokenButton)
@ -1009,6 +1012,50 @@ describe('Metamask popup page', async function () {
await switchToFirstPage()
})
})
describe('Token menu', function () {
it('token menu is displayed and clickable ', async function () {
const menu = await waitUntilShowUp(menus.token.menu)
await menu.click()
})
it('link \'View on blockexplorer...\' leads to correct page ', async function () {
const menu = await waitUntilShowUp(menus.token.view)
assert.notEqual(menu, false, 'item isn\'t displayed')
assert.equal(await menu.getText(), menus.token.viewText, 'incorrect name')
await menu.click()
await switchToLastPage()
const title = await driver.getCurrentUrl()
assert.equal(title.includes('https://etherscan.io/token/'), true, 'link leads to wrong page')
await switchToFirstPage()
})
it('item \'Copy\' is displayed and clickable ', async function () {
let menu = await waitUntilShowUp(menus.token.menu)
await menu.click()
const item = await waitUntilShowUp(menus.token.copy)
assert.notEqual(item, false, 'item isn\'t displayed')
assert.equal(await item.getText(), menus.token.copyText, 'incorrect name')
await item.click()
menu = await waitUntilShowUp(menus.token.menu, 10)
assert.notEqual(menu, false, 'menu wasn\'t closed')
})
it('item \'Remove\' is displayed', async function () {
const menu = await waitUntilShowUp(menus.token.menu)
await menu.click()
const item = await waitUntilShowUp(menus.token.remove)
assert.notEqual(item, false, 'item isn\'t displayed')
assert.equal(await item.getText(), menus.token.removeText, 'incorrect name')
})
it('item \'Send \' is displayed', async function () {
const item = await waitUntilShowUp(menus.token.send)
assert.notEqual(item, false, 'item isn\'t displayed')
assert.equal(await item.getText(), menus.token.sendText, 'incorrect name')
await waitUntilShowUp(menus.token.menu)
})
})
describe('Check support of token per network basis ', async function () {
@ -1092,44 +1139,178 @@ describe('Metamask popup page', async function () {
})
})
describe('Token menu', function () {
describe('Transfer tokens', function () {
it('token menu is displayed and clickable ', async function () {
const menu = await waitUntilShowUp(menus.token.menu)
await menu.click()
})
it('link \'View on blockexplorer...\' leads to correct page ', async function () {
const menu = await waitUntilShowUp(menus.token.view)
assert.notEqual(menu, false, 'item isn\'t displayed')
assert.equal(await menu.getText(), menus.token.viewText, 'incorrect name')
await menu.click()
await switchToLastPage()
const title = await driver.getCurrentUrl()
assert.equal(title.includes('https://etherscan.io/token/'), true, 'link leads to wrong page')
await switchToFirstPage()
})
it('item \'Copy\' is displayed and clickable ', async function () {
let menu = await waitUntilShowUp(menus.token.menu)
await menu.click()
const item = await waitUntilShowUp(menus.token.copy)
assert.notEqual(item, false, 'item isn\'t displayed')
assert.equal(await item.getText(), menus.token.copyText, 'incorrect name')
const account2 = '0x2f318C334780961FB129D2a6c30D0763d9a5C970'
const invalidAddress = '0xkqjefwblknnecwe'
const invalidAmount = 'eeeee'
const largeAmount = '123'
const preciseAmount = '0.123456789123456789123'
const negativeAmount = '-1'
it('switch to account 1 ', async function () {
const accountMenu = await waitUntilShowUp(menus.account.menu)
await accountMenu.click()
const item = await waitUntilShowUp(menus.account.account1)
await item.click()
menu = await waitUntilShowUp(menus.token.menu, 10)
assert.notEqual(menu, false, 'menu wasn\'t closed')
await delay(2000)
const accountName = await waitUntilShowUp(screens.main.accountName)
assert.equal(await accountName.getText(), 'Account 1', 'account name incorrect')
})
it('item \'Remove\' is displayed', async function () {
it('open screen \'Transfer tokens\' ', async function () {
const menu = await waitUntilShowUp(menus.token.menu)
await menu.click()
const item = await waitUntilShowUp(menus.token.remove)
assert.notEqual(item, false, 'item isn\'t displayed')
assert.equal(await item.getText(), menus.token.removeText, 'incorrect name')
const item = await waitUntilShowUp(menus.token.send)
await item.click()
})
it('field \'Amount\' is displayed and has correct placeholder ', async function () {
const item = await waitUntilShowUp(screens.sendTokens.field.amount)
assert.equal(await item.getAttribute('placeholder'), screens.sendTokens.field.amountPlaceholder, 'placeholder is incorrect')
})
it('field \'Address\' is displayed and has correct placeholder ', async function () {
const item = await waitUntilShowUp(screens.sendTokens.field.address)
assert.equal(await item.getAttribute('placeholder'), screens.sendTokens.field.addressPlaceholder, 'placeholder is incorrect')
})
it('token\'s balance is correct ', async function () {
const item = await waitUntilShowUp(screens.sendTokens.balance)
assert.equal(await item.getText(), '100', 'token\'s balance is incorrect')
})
it('token\'s symbol is correct ', async function () {
const item = await waitUntilShowUp(screens.sendTokens.symbol)
assert.equal(await item.getText(), 'TST', 'token\'s symbol is incorrect')
})
it('error message if invalid token\'s amount', async function () {
const button = await waitUntilShowUp(screens.sendTokens.button.next)
assert.equal(await button.getText(), 'Next', 'button \'Next\' has incorrect name')
await click(button)
const error = await waitUntilShowUp(screens.sendTokens.error)
assert.equal(await error.getText(), screens.sendTokens.errorText.invalidAmount, ' error message is incorrect')
})
it('error message if invalid address', async function () {
const amount = await waitUntilShowUp(screens.sendTokens.field.amount)
await amount.sendKeys('1')
const address = await waitUntilShowUp(screens.sendTokens.field.address)
await address.sendKeys(invalidAddress)
const button = await waitUntilShowUp(screens.sendTokens.button.next)
await click(button)
await click(button)
await delay(2000)
const error = await waitUntilShowUp(screens.sendTokens.error)
assert.equal(await error.getText(), screens.sendTokens.errorText.address, ' error message is incorrect')
})
it('error message if amount is large', async function () {
const amount = await waitUntilShowUp(screens.sendTokens.field.amount)
await amount.sendKeys(largeAmount)
const address = await waitUntilShowUp(screens.sendTokens.field.address)
await clearField(address)
await address.sendKeys(account2)
const button = await waitUntilShowUp(screens.sendTokens.button.next)
await click(button)
await click(button)
await delay(2000)
const error = await waitUntilShowUp(screens.sendTokens.error)
assert.equal(await error.getText(), screens.sendTokens.errorText.largeAmount, ' error message is incorrect')
})
it('error message if amount is invalid', async function () {
const amount = await waitUntilShowUp(screens.sendTokens.field.amount)
await clearField(amount)
await amount.sendKeys(invalidAmount)
const button = await waitUntilShowUp(screens.sendTokens.button.next)
await click(button)
await click(button)
await delay(2000)
const error = await waitUntilShowUp(screens.sendTokens.error)
assert.equal(await error.getText(), screens.sendTokens.errorText.invalidAmount, ' error message is incorrect')
})
it.skip('error message if amount is too precise', async function () {
const amount = await waitUntilShowUp(screens.sendTokens.field.amount)
await clearField(amount)
await amount.sendKeys(preciseAmount)
const button = await waitUntilShowUp(screens.sendTokens.button.next)
await click(button)
await click(button)
await delay(2000)
const error = await waitUntilShowUp(screens.sendTokens.error)
assert.equal(await error.getText(), screens.sendTokens.errorText.tooPrecise, ' error message is incorrect')
})
it('error message if amount is negative', async function () {
const amount = await waitUntilShowUp(screens.sendTokens.field.amount)
await clearField(amount)
await amount.sendKeys(negativeAmount)
const button = await waitUntilShowUp(screens.sendTokens.button.next)
await click(button)
await click(button)
await delay(2000)
const error = await waitUntilShowUp(screens.sendTokens.error)
assert.equal(await error.getText(), screens.sendTokens.errorText.negativeAmount, ' error message is incorrect')
})
it('\'Confirm transaction\' screen is opened if address and amount are correct', async function () {
const amount = await waitUntilShowUp(screens.sendTokens.field.amount)
await clearField(amount)
await amount.sendKeys('5')
const button = await waitUntilShowUp(screens.sendTokens.button.next)
await click(button)
const buttonSubmit = await waitUntilShowUp(screens.confirmTransaction.button.submit)
assert.notEqual(buttonSubmit, false, 'incorrect screen was opened')
})
it('\'Confirm transaction\' screen: token\'s amount is correct', async function () {
const amount = await waitUntilShowUp(screens.confirmTransaction.amount)
assert.equal(await amount.getText(), '5.000', ' amount is incorrect')
})
it('\'Confirm transaction\' screen: token\'s symbol is correct', async function () {
const symbol = await waitUntilShowUp(screens.confirmTransaction.symbol)
assert.equal(await symbol.getText(), 'TST', ' symbol is incorrect')
})
it('submit transaction', async function () {
await driver.navigate().refresh()
const button = await waitUntilShowUp(screens.confirmTransaction.button.submit)
await click(button)
const list = await waitUntilShowUp(screens.main.transactionList)
assert.notEqual(list, false, ' main screen isn\'t opened')
})
it('correct amount substracted from sender\'s tokens balance', async function () {
const tab = await waitUntilShowUp(screens.main.tokens.menu)
await tab.click()
await driver.navigate().refresh()
await delay(5000)
await driver.navigate().refresh()
await delay(5000)
await driver.navigate().refresh()
await delay(5000)
const balance = await waitUntilShowUp(screens.main.tokens.balance)
assert.equal(await balance.getText(), '95 TST', 'balance is incorrect')
})
it('switch to account 2 ', async function () {
const accountMenu = await waitUntilShowUp(menus.account.menu)
await accountMenu.click()
const item = await waitUntilShowUp(menus.account.account2)
await item.click()
await delay(2000)
const accountName = await waitUntilShowUp(screens.main.accountName)
assert.equal(await accountName.getText(), 'Account 2', 'account name incorrect')
})
it('receiver got correct amount of tokens', async function () {
const balance = await waitUntilShowUp(screens.main.tokens.balance)
assert.equal(await balance.getText(), '5 TST', 'balance is incorrect')
})
})
describe('Remove token , provider is localhost', function () {
it('remove option opens \'Remove token\' screen ', async function () {

View File

@ -1074,7 +1074,7 @@ function sendTx (txData) {
}
}
function signTokenTx (tokenAddress, toAddress, amount, txData) {
function signTokenTx (tokenAddress, toAddress, amount, txData, confTxScreenParams) {
return dispatch => {
dispatch(actions.showLoadingIndication())
const token = global.eth.contract(abi).at(tokenAddress)
@ -1083,7 +1083,7 @@ function signTokenTx (tokenAddress, toAddress, amount, txData) {
dispatch(actions.hideLoadingIndication())
dispatch(actions.displayWarning(err.message))
})
dispatch(actions.showConfTxPage({}))
dispatch(actions.showConfTxPage(confTxScreenParams || {}))
}
}
@ -1109,7 +1109,7 @@ function updateTransaction (txData) {
.then(() => updateMetamaskStateFromBackground())
.then(newState => dispatch(actions.updateMetamaskState(newState)))
.then(() => {
dispatch(actions.showConfTxPage({ id: txData.id }))
dispatch(actions.showConfTxPage({ id: txData.id}))
dispatch(actions.hideLoadingIndication())
return txData
})
@ -1143,6 +1143,7 @@ function updateAndApproveTx (txData) {
dispatch(actions.clearSend())
dispatch(actions.completedTx(txData.id))
dispatch(actions.hideLoadingIndication())
dispatch(actions.setCurrentAccountTab('history'))
if (!hasUnconfirmedTransactions(getState())) {
return global.platform.closeNotificationWindow()
@ -1542,11 +1543,12 @@ function showAccountsPage () {
}
}
function showConfTxPage ({transForward = true, id}) {
function showConfTxPage (screenParams) {
return {
type: actions.SHOW_CONF_TX_PAGE,
transForward,
id,
transForward: (screenParams.transForward || true),
id: screenParams.id,
value: screenParams,
}
}
@ -1994,9 +1996,11 @@ function showSendPage () {
}
}
function showSendTokenPage () {
function showSendTokenPage (address) {
return {
type: actions.SHOW_SEND_TOKEN_PAGE,
value: address,
}
}

View File

@ -122,7 +122,7 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa
if (isUnapproved) {
opts.onClick = () => {
this.props.showConfTxPage({ id: transactionId })
this.props.showConfTxPage({ id: transactionId})
history.push(CONFIRM_TRANSACTION_ROUTE)
}
opts.transactionStatus = this.context.t('notStarted')

View File

@ -297,6 +297,7 @@ function reduceApp (state, action) {
currentView: {
name: 'sendToken',
context: appState.currentView.context,
tokenAddress: action.value,
},
transForward: true,
warning: null,
@ -438,6 +439,7 @@ function reduceApp (state, action) {
currentView: {
name: 'confTx',
pendingTxIndex: action.id ? indexForPending(state, action.id) : 0,
screenParams: action.value,
},
transForward: action.transForward,
warning: null,

View File

@ -47,9 +47,15 @@ function calcTokenAmount (value, decimals) {
return new BigNumber(value).div(multiplier).toNumber()
}
function calcTokenAmountWithDec (valueWithoutDec, decimals) {
const multiplier = Math.pow(10, Number(decimals || 0))
return new BigNumber(valueWithoutDec).mul(multiplier).toNumber()
}
module.exports = {
tokenInfoGetter,
calcTokenAmount,
calcTokenAmountWithDec,
getSymbolAndDecimals,
}

View File

@ -54,6 +54,7 @@ async function startApp (metamaskState, accountManager, opts) {
const unapprovedTxsAll = txHelper(metamaskState.unapprovedTxs, metamaskState.unapprovedMsgs, metamaskState.unapprovedPersonalMsgs, metamaskState.unapprovedTypedMessages, metamaskState.network)
const numberOfUnapprivedTx = unapprovedTxsAll.length
if (numberOfUnapprivedTx > 0) {
store.dispatch(actions.showConfTxPage({
id: unapprovedTxsAll[numberOfUnapprivedTx - 1].id,
}))