Merge pull request #212 from poanetwork/import-multisig
(Feature) Import contract
This commit is contained in:
commit
a8fcea31f0
|
@ -179,7 +179,7 @@ jobs:
|
|||
key: dependency-cache-{{ .Revision }}
|
||||
- run:
|
||||
name: Test
|
||||
command: npx nsp check
|
||||
command: npm audit
|
||||
|
||||
test-e2e-chrome:
|
||||
docker:
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
## Current Master
|
||||
|
||||
## 4.9.1 Tue Nov 27 2018
|
||||
## 4.10.0 Tue Dec 04 2018
|
||||
|
||||
- [#212](https://github.com/poanetwork/nifty-wallet/pull/212): (Feature) interact with smart-contracts
|
||||
- [#209](https://github.com/poanetwork/nifty-wallet/pull/209): (Fix) Enhance custom RPC validation
|
||||
|
||||
## 4.9.0 Mon Nov 26 2018
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "__MSG_appName__",
|
||||
"short_name": "__MSG_appName__",
|
||||
"version": "4.9.1",
|
||||
"version": "4.10.0",
|
||||
"manifest_version": 2,
|
||||
"author": "POA Network",
|
||||
"description": "__MSG_appDescription__",
|
||||
|
|
|
@ -369,6 +369,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
resetAccount: nodeify(this.resetAccount, this),
|
||||
changePassword: nodeify(this.changePassword, this),
|
||||
removeAccount: nodeify(this.removeAccount, this),
|
||||
getContract: nodeify(this.getContract, this),
|
||||
importAccountWithStrategy: nodeify(this.importAccountWithStrategy, this),
|
||||
|
||||
// hardware wallets
|
||||
|
@ -407,6 +408,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
createNewVaultAndKeychain: nodeify(this.createNewVaultAndKeychain, this),
|
||||
createNewVaultAndRestore: nodeify(this.createNewVaultAndRestore, this),
|
||||
addNewKeyring: nodeify(keyringController.addNewKeyring, keyringController),
|
||||
addNewMultisig: nodeify(keyringController.addNewMultisig, keyringController),
|
||||
exportAccount: nodeify(keyringController.exportAccount, keyringController),
|
||||
|
||||
// txController
|
||||
|
@ -558,7 +560,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
const accounts = await this.keyringController.getAccounts()
|
||||
|
||||
// verify keyrings
|
||||
const nonSimpleKeyrings = this.keyringController.keyrings.filter(keyring => keyring.type !== 'Simple Key Pair')
|
||||
const nonSimpleKeyrings = this.keyringController.keyrings.filter(keyring => keyring.type !== 'Simple Key Pair' && keyring.type !== 'Simple Address')
|
||||
if (nonSimpleKeyrings.length > 1 && this.diagnostics) {
|
||||
await this.diagnostics.reportMultipleKeyrings(nonSimpleKeyrings)
|
||||
}
|
||||
|
@ -804,6 +806,14 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
return selectedAddress
|
||||
}
|
||||
|
||||
async getContract (address) {
|
||||
let props
|
||||
if (this.keyringController.getProps) {
|
||||
props = this.keyringController.getProps(address)
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
async changePassword (oldPassword, newPassword) {
|
||||
await this.keyringController.changePassword(oldPassword, newPassword)
|
||||
}
|
||||
|
@ -814,7 +824,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {string[]} address A hex address
|
||||
*
|
||||
*/
|
||||
async removeAccount (address) {
|
||||
async removeAccount (address, network) {
|
||||
// Remove account from the preferences controller
|
||||
this.preferencesController.removeAddress(address)
|
||||
// Remove account from the account tracker controller
|
||||
|
@ -822,7 +832,7 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
|
||||
// Remove account from the keyring
|
||||
try {
|
||||
await this.keyringController.removeAccount(address)
|
||||
await this.keyringController.removeAccount(address, network)
|
||||
} catch (e) {
|
||||
log.error(e)
|
||||
}
|
||||
|
@ -840,8 +850,13 @@ module.exports = class MetamaskController extends EventEmitter {
|
|||
* @param {Function} cb - A callback function called with a state update on success.
|
||||
*/
|
||||
async importAccountWithStrategy (strategy, args) {
|
||||
const privateKey = await accountImporter.importAccount(strategy, args)
|
||||
const keyring = await this.keyringController.addNewKeyring('Simple Key Pair', [ privateKey ])
|
||||
let keyring
|
||||
if (strategy === 'Contract') {
|
||||
keyring = await this.keyringController.addNewKeyring('Simple Address', args)
|
||||
} else {
|
||||
const privateKey = await accountImporter.importAccount(strategy, args)
|
||||
keyring = await this.keyringController.addNewKeyring('Simple Key Pair', [ privateKey ])
|
||||
}
|
||||
const accounts = await keyring.getAccounts()
|
||||
// update accounts in preferences controller
|
||||
const allAccounts = await this.keyringController.getAccounts()
|
||||
|
|
|
@ -4,7 +4,7 @@ const Component = require('react').Component
|
|||
const h = require('react-hyperscript')
|
||||
const connect = require('react-redux').connect
|
||||
const actions = require('../../ui/app/actions')
|
||||
const valuesFor = require('./util').valuesFor
|
||||
const { getCurrentKeyring, ifContractAcc, valuesFor } = require('./util')
|
||||
const Identicon = require('./components/identicon')
|
||||
const EthBalance = require('./components/eth-balance')
|
||||
const TransactionList = require('./components/transaction-list')
|
||||
|
@ -56,6 +56,8 @@ AccountDetailScreen.prototype.render = function () {
|
|||
this.props.dispatch(actions.showAddSuggestedTokenPage())
|
||||
}
|
||||
|
||||
const currentKeyring = getCurrentKeyring(props.address, network, props.keyrings, props.identities)
|
||||
|
||||
return (
|
||||
|
||||
h('.account-detail-section.full-flex-height', [
|
||||
|
@ -219,7 +221,13 @@ AccountDetailScreen.prototype.render = function () {
|
|||
}, 'Buy'),
|
||||
|
||||
h('button', {
|
||||
onClick: () => props.dispatch(actions.showSendPage()),
|
||||
onClick: () => {
|
||||
if (ifContractAcc(currentKeyring)) {
|
||||
return props.dispatch(actions.showSendContractPage({}))
|
||||
} else {
|
||||
return props.dispatch(actions.showSendPage())
|
||||
}
|
||||
},
|
||||
}, 'Send'),
|
||||
|
||||
]),
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import actions from '../../../../ui/app/actions'
|
||||
import Web3 from 'web3'
|
||||
import log from 'loglevel'
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
|
||||
class ContractImportView extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const web3 = new Web3(global.ethereumProvider)
|
||||
this.state = {
|
||||
contractAddr: '',
|
||||
abi: '',
|
||||
abiInputDisabled: false,
|
||||
importDisabled: true,
|
||||
web3,
|
||||
}
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
error: PropTypes.string,
|
||||
network: PropTypes.string,
|
||||
displayWarning: PropTypes.func,
|
||||
importNewAccount: PropTypes.func,
|
||||
hideWarning: PropTypes.func,
|
||||
showLoadingIndication: PropTypes.func,
|
||||
hideLoadingIndication: PropTypes.func,
|
||||
}
|
||||
|
||||
addressOnChange (contractAddr) {
|
||||
this.setState({
|
||||
contractAddr,
|
||||
}, () => {
|
||||
this.autodetectContractAbi()
|
||||
})
|
||||
}
|
||||
|
||||
abiOnChange (abi) {
|
||||
this.props.hideWarning()
|
||||
try {
|
||||
if (abi && JSON.parse(abi)) {
|
||||
this.setState({
|
||||
abi,
|
||||
abiInputDisabled: true,
|
||||
importDisabled: false,
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.clearAbi()
|
||||
log.debug('ABI can not be parsed')
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { error } = this.props
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
padding: '5px 0px 0px 0px',
|
||||
}}>
|
||||
<span>Paste address of contract here</span>
|
||||
<input
|
||||
className="large-input"
|
||||
id="address-box"
|
||||
value={this.state.contractAddr}
|
||||
onChange={(e) => this.addressOnChange(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
marginTop: '12px',
|
||||
}}
|
||||
/>
|
||||
<span style={{ marginTop: '20px' }}>Paste ABI of contract here
|
||||
<i
|
||||
className="clipboard cursor-pointer"
|
||||
style={{ marginLeft: '10px' }}
|
||||
onClick={(e) => { copyToClipboard(this.state.abi) }}
|
||||
/>
|
||||
</span>
|
||||
<textarea
|
||||
id="abi-box"
|
||||
disabled={this.state.abiInputDisabled}
|
||||
value={this.state.abi}
|
||||
onChange={(e) => this.abiOnChange(e.target.value) }
|
||||
style={{
|
||||
marginTop: '12px',
|
||||
width: '100%',
|
||||
height: '50px',
|
||||
}}
|
||||
onKeyPress={(e) => this.createKeyringOnEnter(e)}
|
||||
/>
|
||||
<button
|
||||
disabled={this.state.importDisabled}
|
||||
onClick={(e) => this.createNewKeychain(e)}
|
||||
style={{ margin: '20px' }}
|
||||
>Import</button>
|
||||
{error ? <span className="error">{error}</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
autodetectContractAbi = () => {
|
||||
const { contractAddr, web3 } = this.state
|
||||
if (!contractAddr || !web3.isAddress(contractAddr)) {
|
||||
this.clearAbi()
|
||||
return
|
||||
}
|
||||
|
||||
const networkName = this.getBlockscoutApiNetworkSuffix()
|
||||
const bloscoutApiLink = `https://blockscout.com/poa/${networkName}/api`
|
||||
const bloscoutApiContractPath = '?module=contract'
|
||||
const blockscoutApiGetAbiPath = `&action=getabi&address=${this.state.contractAddr}`
|
||||
const apiLink = `${bloscoutApiLink}${bloscoutApiContractPath}${blockscoutApiGetAbiPath}`
|
||||
fetch(apiLink)
|
||||
.then(response => {
|
||||
return response.json()
|
||||
})
|
||||
.then(responseJson => {
|
||||
this.abiOnChange(responseJson && responseJson.result)
|
||||
})
|
||||
.catch((e) => {
|
||||
this.clearAbi()
|
||||
log.debug(e)
|
||||
})
|
||||
}
|
||||
|
||||
createKeyringOnEnter (event) {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
this.createNewKeychain()
|
||||
}
|
||||
}
|
||||
|
||||
async getContractCode () {
|
||||
this.props.showLoadingIndication()
|
||||
const { contractAddr, web3 } = this.state
|
||||
return new Promise((resolve, reject) => {
|
||||
web3.eth.getCode(contractAddr, (err, addrCode) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(addrCode)
|
||||
}
|
||||
this.props.hideLoadingIndication()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async createNewKeychain () {
|
||||
const { contractAddr, web3 } = this.state
|
||||
|
||||
if (!contractAddr || !web3.isAddress(contractAddr)) {
|
||||
this.clearAbi()
|
||||
return this.props.displayWarning('Invalid contract address')
|
||||
}
|
||||
|
||||
const contractAddrCode = await this.getContractCode()
|
||||
if (contractAddrCode === '0x') {
|
||||
this.clearAbi()
|
||||
return this.props.displayWarning('This is not a contract address')
|
||||
}
|
||||
|
||||
let abi
|
||||
try {
|
||||
abi = JSON.parse(this.state.abi)
|
||||
} catch (e) {
|
||||
this.clearAbi()
|
||||
this.props.displayWarning('Invalid ABI')
|
||||
}
|
||||
|
||||
if (!abi) {
|
||||
this.clearAbi()
|
||||
return this.props.displayWarning('Invalid contract ABI')
|
||||
}
|
||||
|
||||
this.props.importNewAccount('Contract', { addr: contractAddr, network: this.props.network, abi })
|
||||
// JS runtime requires caught rejections but failures are handled by Redux
|
||||
.catch()
|
||||
}
|
||||
|
||||
getBlockscoutApiNetworkSuffix () {
|
||||
const { network } = this.props
|
||||
switch (Number(network)) {
|
||||
case 1:
|
||||
return 'mainnet'
|
||||
case 99:
|
||||
return 'core'
|
||||
case 77:
|
||||
return 'sokol'
|
||||
case 100:
|
||||
return 'dai'
|
||||
case 42:
|
||||
return 'kovan'
|
||||
case 3:
|
||||
return 'ropsten'
|
||||
case 4:
|
||||
return 'rinkeby'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
clearAbi () {
|
||||
this.setState({
|
||||
abi: '',
|
||||
abiInputDisabled: false,
|
||||
importDisabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const warning = state.appState.warning
|
||||
const result = {
|
||||
error: warning && (warning || warning.message),
|
||||
network: state.metamask.network,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
showLoadingIndication: () => dispatch(actions.showLoadingIndication()),
|
||||
hideLoadingIndication: () => dispatch(actions.hideLoadingIndication()),
|
||||
hideWarning: () => dispatch(actions.hideWarning()),
|
||||
displayWarning: (msg) => dispatch(actions.displayWarning(msg)),
|
||||
importNewAccount: (strategy, args) => dispatch(actions.importNewAccount(strategy, args)),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(ContractImportView)
|
|
@ -8,10 +8,12 @@ import Select from 'react-select'
|
|||
// Subviews
|
||||
const JsonImportView = require('./json.js')
|
||||
const PrivateKeyImportView = require('./private-key.js')
|
||||
const ContractImportView = require('./contract.js')
|
||||
|
||||
const menuItems = [
|
||||
'Private Key',
|
||||
'JSON File',
|
||||
'Contract',
|
||||
]
|
||||
|
||||
module.exports = connect(mapStateToProps)(AccountImportSubview)
|
||||
|
@ -127,6 +129,8 @@ AccountImportSubview.prototype.renderImportView = function () {
|
|||
return h(PrivateKeyImportView)
|
||||
case 'JSON File':
|
||||
return h(JsonImportView)
|
||||
case 'Contract':
|
||||
return h(ContractImportView)
|
||||
default:
|
||||
return h(JsonImportView)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
const inherits = require('util').inherits
|
||||
const Component = require('react').Component
|
||||
const connect = require('react-redux').connect
|
||||
const { withRouter } = require('react-router-dom')
|
||||
const { compose } = require('recompose')
|
||||
const h = require('react-hyperscript')
|
||||
const actions = require('../../ui/app/actions')
|
||||
const log = require('loglevel')
|
||||
|
@ -15,8 +17,10 @@ const UnlockScreen = require('./unlock')
|
|||
// accounts
|
||||
const AccountDetailScreen = require('./account-detail')
|
||||
const AccountQrScreen = require('./account-qr')
|
||||
const SendTransactionScreen = require('./send')
|
||||
const SendTokenScreen = require('./send-token')
|
||||
const SendTransactionScreen = require('./components/send/send')
|
||||
const SendTokenScreen = require('./components/send/send-token')
|
||||
const SendContractScreen = require('./components/send/send-contract')
|
||||
const ChooseContractExecutorScreen = require('./components/send/choose-contract-executor')
|
||||
const ConfirmTxScreen = require('./conf-tx')
|
||||
// notice
|
||||
const NoticeScreen = require('./components/notice')
|
||||
|
@ -41,7 +45,10 @@ const DeleteImportedAccount = require('./components/delete-imported-account')
|
|||
const ConfirmChangePassword = require('./components/confirm-change-password')
|
||||
const ethNetProps = require('eth-net-props')
|
||||
|
||||
module.exports = connect(mapStateToProps)(App)
|
||||
module.exports = compose(
|
||||
withRouter,
|
||||
connect(mapStateToProps)
|
||||
)(App)
|
||||
|
||||
inherits(App, Component)
|
||||
function App () { Component.call(this) }
|
||||
|
@ -231,9 +238,17 @@ App.prototype.renderPrimary = function () {
|
|||
return h(SendTransactionScreen, {key: 'send-transaction'})
|
||||
|
||||
case 'sendToken':
|
||||
log.debug('rendering send tx screen')
|
||||
log.debug('rendering send token tx screen')
|
||||
return h(SendTokenScreen, {key: 'send-token'})
|
||||
|
||||
case 'sendContract':
|
||||
log.debug('rendering send contract tx screen')
|
||||
return h(SendContractScreen, {key: 'send-contract'})
|
||||
|
||||
case 'show-choose-contract-executor-page':
|
||||
log.debug('rendering choose contract executor screen')
|
||||
return h(ChooseContractExecutorScreen, {key: 'show-choose-contract-executor-page'})
|
||||
|
||||
case 'newKeychain':
|
||||
log.debug('rendering new keychain screen')
|
||||
return h(NewKeyChainScreen, {key: 'new-keychain'})
|
||||
|
|
|
@ -9,6 +9,7 @@ const Identicon = require('./identicon')
|
|||
const ethUtil = require('ethereumjs-util')
|
||||
const copyToClipboard = require('copy-to-clipboard')
|
||||
const ethNetProps = require('eth-net-props')
|
||||
const { getCurrentKeyring, ifLooseAcc, ifContractAcc } = require('../util')
|
||||
|
||||
class AccountDropdowns extends Component {
|
||||
constructor (props) {
|
||||
|
@ -22,19 +23,40 @@ class AccountDropdowns extends Component {
|
|||
}
|
||||
|
||||
renderAccounts () {
|
||||
const { identities, selected, keyrings } = this.props
|
||||
const accountOrder = keyrings.reduce((list, keyring) => list.concat(keyring.accounts), [])
|
||||
const { identities, selected, keyrings, network } = this.props
|
||||
const accountOrder = keyrings.reduce((list, keyring) => {
|
||||
if (ifContractAcc(keyring) && keyring.network === network) {
|
||||
list = list.concat(keyring.accounts)
|
||||
} else if (!ifContractAcc(keyring)) {
|
||||
list = list.concat(keyring.accounts)
|
||||
}
|
||||
return list
|
||||
}, [])
|
||||
|
||||
return accountOrder.map((address, index) => {
|
||||
const identity = identities[address]
|
||||
if (!identity) {
|
||||
return null
|
||||
}
|
||||
const isSelected = identity.address === selected
|
||||
return accountOrder.map((address, index) => {
|
||||
const identity = identities[address]
|
||||
if (!identity) {
|
||||
return null
|
||||
}
|
||||
const isSelected = identity.address === selected
|
||||
|
||||
const keyring = this.getCurrentKeyring(address)
|
||||
const keyring = getCurrentKeyring(address, network, keyrings, identities)
|
||||
|
||||
return h(
|
||||
// display contract acc only for network where it was created
|
||||
if (ifContractAcc(keyring)) {
|
||||
if (keyring.network !== network) {
|
||||
return null
|
||||
} else {
|
||||
return this.accountsDropdownItemView(index, isSelected, keyring, identity)
|
||||
}
|
||||
} else {
|
||||
return this.accountsDropdownItemView(index, isSelected, keyring, identity)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
accountsDropdownItemView (index, isSelected, keyring, identity) {
|
||||
return h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => {},
|
||||
|
@ -80,7 +102,7 @@ class AccountDropdowns extends Component {
|
|||
},
|
||||
}, identity.name || ''),
|
||||
this.indicateIfLoose(keyring),
|
||||
this.ifLooseAcc(keyring) ? h('.remove', {
|
||||
ifLooseAcc(keyring) ? h('.remove', {
|
||||
onClick: (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
@ -93,39 +115,28 @@ class AccountDropdowns extends Component {
|
|||
}) : null,
|
||||
]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
ifLooseAcc (keyring) {
|
||||
try { // Sometimes keyrings aren't loaded yet:
|
||||
const type = keyring.type
|
||||
const isLoose = type !== 'HD Key Tree'
|
||||
return isLoose
|
||||
} catch (e) { return }
|
||||
}
|
||||
|
||||
ifHardwareAcc (address) {
|
||||
const keyring = this.getCurrentKeyring(address)
|
||||
const keyring = getCurrentKeyring(address, this.props.network, this.props.keyrings, this.props.identities)
|
||||
if (keyring && keyring.type.search('Hardware') !== -1) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
getCurrentKeyring (address) {
|
||||
const { identities, keyrings } = this.props
|
||||
const identity = identities[address]
|
||||
const simpleAddress = identity.address.substring(2).toLowerCase()
|
||||
const keyring = keyrings && keyrings.find((kr) => {
|
||||
return kr.accounts.includes(simpleAddress) ||
|
||||
kr.accounts.includes(address)
|
||||
})
|
||||
|
||||
return keyring
|
||||
}
|
||||
|
||||
indicateIfLoose (keyring) {
|
||||
return this.ifLooseAcc(keyring) ? h('.keyring-label', 'IMPORTED') : null
|
||||
if (ifLooseAcc(keyring)) {
|
||||
let label
|
||||
if (ifContractAcc(keyring)) {
|
||||
label = 'CONTRACT'
|
||||
} else {
|
||||
label = 'IMPORTED'
|
||||
}
|
||||
return h('.keyring-label', label)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
renderAccountSelector () {
|
||||
|
@ -214,9 +225,11 @@ class AccountDropdowns extends Component {
|
|||
}
|
||||
|
||||
renderAccountOptions () {
|
||||
const { actions, selected } = this.props
|
||||
const { actions, selected, network, keyrings, identities } = this.props
|
||||
const { optionsMenuActive } = this.state
|
||||
|
||||
const keyring = getCurrentKeyring(selected, network, keyrings, identities)
|
||||
|
||||
return h(
|
||||
Dropdown,
|
||||
{
|
||||
|
@ -274,7 +287,7 @@ class AccountDropdowns extends Component {
|
|||
},
|
||||
'Copy address to clipboard',
|
||||
),
|
||||
!this.ifHardwareAcc(selected) ? h(
|
||||
(!this.ifHardwareAcc(selected) && !(ifContractAcc(keyring))) ? h(
|
||||
DropdownMenuItem,
|
||||
{
|
||||
closeMenu: () => {},
|
||||
|
@ -333,6 +346,21 @@ class AccountDropdowns extends Component {
|
|||
]
|
||||
)
|
||||
}
|
||||
|
||||
// switch to the first account in the list on network switch, if unlocked account was contract before change
|
||||
componentDidUpdate (prevProps) {
|
||||
if (!isNaN(this.props.network)) {
|
||||
const { selected, network, keyrings, identities } = this.props
|
||||
if (network !== prevProps.network) {
|
||||
const keyring = getCurrentKeyring(selected, this.props.network, keyrings, identities)
|
||||
const firstKeyring = keyrings && keyrings[0]
|
||||
const firstKeyRingAcc = firstKeyring && firstKeyring.accounts && firstKeyring.accounts[0]
|
||||
if (!keyring || (ifContractAcc(keyring) && firstKeyRingAcc)) {
|
||||
return this.props.actions.showAccountDetail(firstKeyRingAcc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AccountDropdowns.defaultProps = {
|
||||
|
|
|
@ -4,6 +4,7 @@ const h = require('react-hyperscript')
|
|||
const Tooltip = require('../tooltip.js')
|
||||
const TabBar = require('../tab-bar')
|
||||
const { checkExistingAddresses } = require('./util')
|
||||
const { getCurrentKeyring, ifContractAcc } = require('../../util')
|
||||
const TokenList = require('./token-list')
|
||||
const TokenSearch = require('./token-search')
|
||||
const { tokenInfoGetter } = require('../../../../ui/app/token-util')
|
||||
|
@ -32,6 +33,7 @@ class AddTokenScreen extends Component {
|
|||
displayWarning: PropTypes.func,
|
||||
tokens: PropTypes.array,
|
||||
identities: PropTypes.object,
|
||||
keyrings: PropTypes.array,
|
||||
address: PropTypes.string,
|
||||
dispatch: PropTypes.func,
|
||||
network: PropTypes.string,
|
||||
|
@ -340,6 +342,7 @@ class AddTokenScreen extends Component {
|
|||
|
||||
validateInputs () {
|
||||
let msg = ''
|
||||
const { network, keyrings, identities } = this.props
|
||||
const state = this.state
|
||||
const identitiesList = Object.keys(this.props.identities)
|
||||
const { customAddress: address, customSymbol: symbol, customDecimals: decimals } = state
|
||||
|
@ -361,9 +364,14 @@ class AddTokenScreen extends Component {
|
|||
msg += 'Symbol must be between 0 and 10 characters.'
|
||||
}
|
||||
|
||||
const ownAddress = identitiesList.includes(standardAddress)
|
||||
let ownAddress = identitiesList.includes(standardAddress)
|
||||
if (ownAddress) {
|
||||
msg = 'Personal address detected. Input the token contract address.'
|
||||
const keyring = getCurrentKeyring(standardAddress, network, keyrings, identities)
|
||||
if (!ifContractAcc(keyring)) {
|
||||
msg = 'Personal address detected. Input the token contract address.'
|
||||
} else {
|
||||
ownAddress = false
|
||||
}
|
||||
}
|
||||
|
||||
const isValid = validAddress && validDecimals && validSymbol && !ownAddress
|
||||
|
@ -448,6 +456,7 @@ class AddTokenScreen extends Component {
|
|||
}
|
||||
|
||||
handleCustomAddressChange = (value) => {
|
||||
const { identities, keyrings, tokens, network } = this.props
|
||||
const customAddress = value.trim()
|
||||
this.setState({
|
||||
customAddress,
|
||||
|
@ -459,10 +468,13 @@ class AddTokenScreen extends Component {
|
|||
const isValidAddress = ethUtil.isValidAddress(customAddress)
|
||||
const standardAddress = ethUtil.addHexPrefix(customAddress).toLowerCase()
|
||||
|
||||
let warning
|
||||
switch (true) {
|
||||
case !isValidAddress:
|
||||
warning = 'Invalid address'
|
||||
this.setState({
|
||||
customAddressError: 'Invalid address' /* this.context.t('invalidAddress')*/,
|
||||
warning,
|
||||
customAddressError: warning /* this.context.t('invalidAddress')*/,
|
||||
customSymbol: '',
|
||||
customDecimals: null,
|
||||
customSymbolError: null,
|
||||
|
@ -470,15 +482,23 @@ class AddTokenScreen extends Component {
|
|||
})
|
||||
|
||||
break
|
||||
case Boolean(this.props.identities[standardAddress]):
|
||||
this.setState({
|
||||
customAddressError: 'Personal address detected. Input the token contract address.' /* this.context.t('personalAddressDetected')*/,
|
||||
})
|
||||
|
||||
case Boolean(identities[standardAddress]):
|
||||
const keyring = getCurrentKeyring(standardAddress, network, keyrings, identities)
|
||||
if (!ifContractAcc(keyring)) {
|
||||
warning = 'Personal address detected. Input the token contract address.' /* this.context.t('personalAddressDetected')*/
|
||||
this.setState({
|
||||
warning,
|
||||
customAddressError: warning /* this.context.t('personalAddressDetected')*/,
|
||||
})
|
||||
} else {
|
||||
this.attemptToAutoFillTokenParams(customAddress)
|
||||
}
|
||||
break
|
||||
case checkExistingAddresses(customAddress, this.props.tokens):
|
||||
case checkExistingAddresses(customAddress, tokens):
|
||||
warning = 'Token has already been added.'
|
||||
this.setState({
|
||||
customAddressError: 'Token has already been added.' /* this.context.t('tokenAlreadyAdded')*/,
|
||||
warning,
|
||||
customAddressError: warning /* this.context.t('tokenAlreadyAdded')*/,
|
||||
})
|
||||
|
||||
break
|
||||
|
|
|
@ -4,9 +4,10 @@ import AddToken from './add-token.component'
|
|||
const { setPendingTokens, clearPendingTokens, displayWarning, goHome, addToken, showConfirmAddTokensPage } = require('../../../../ui/app/actions')
|
||||
|
||||
const mapStateToProps = ({ metamask }) => {
|
||||
const { identities, tokens, pendingTokens, network } = metamask
|
||||
const { identities, keyrings, tokens, pendingTokens, network } = metamask
|
||||
return {
|
||||
identities,
|
||||
keyrings,
|
||||
tokens,
|
||||
network,
|
||||
pendingTokens,
|
||||
|
|
|
@ -68,7 +68,7 @@ DeleteImportedAccount.prototype.render = function () {
|
|||
h('button',
|
||||
{
|
||||
onClick: () => {
|
||||
this.props.dispatch(actions.removeAccount(this.props.identity.address))
|
||||
this.props.dispatch(actions.removeAccount(this.props.identity.address, this.props.metamask.network))
|
||||
.then(() => {
|
||||
this.props.dispatch(actions.showConfigPage())
|
||||
})
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import SendProfile from './send-profile'
|
||||
import ExecutorCell from './executor-cell'
|
||||
import SendHeader from './send-header'
|
||||
import SendError from './send-error'
|
||||
import actions from '../../../../ui/app/actions'
|
||||
import { ifContractAcc } from '../../util'
|
||||
|
||||
class ChooseContractExecutor extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
selectedExecutor: '',
|
||||
accountsCells: [],
|
||||
nextDisabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
methodSelected: PropTypes.string,
|
||||
methodABI: PropTypes.object,
|
||||
inputValues: PropTypes.object,
|
||||
hideWarning: PropTypes.func,
|
||||
signTx: PropTypes.func,
|
||||
setSelectedAddress: PropTypes.func,
|
||||
showSendContractPage: PropTypes.func,
|
||||
txParams: PropTypes.object,
|
||||
identities: PropTypes.object,
|
||||
keyrings: PropTypes.array,
|
||||
error: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
const { error } = this.props
|
||||
return (
|
||||
<div className="send-screen flex-column flex-grow">
|
||||
<SendProfile />
|
||||
<SendHeader title="Choose contract executor" back={() => this.back()} />
|
||||
<SendError
|
||||
error={error}
|
||||
onClose={() => {
|
||||
this.props.hideWarning()
|
||||
}}
|
||||
/>
|
||||
<div style={{ padding: '0 30px' }}>
|
||||
<span className="hw-connect__header__msg">Contract transaction will be executed from selected account</span>
|
||||
</div>
|
||||
<div style={{
|
||||
padding: '0 30px',
|
||||
maxHeight: '220px',
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
{this.state.accountsCells}
|
||||
</div>
|
||||
<div style={{ padding: '0 30px' }}>
|
||||
{this.buttonsSection()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.generateListOfAccounts()
|
||||
}
|
||||
|
||||
buttonsSection () {
|
||||
const nextButton = (
|
||||
<button
|
||||
disabled={this.state.nextDisabled}
|
||||
className="choose-contract-next-button"
|
||||
onClick={() => this.onSubmit() }
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
)
|
||||
|
||||
const buttonContainer = (
|
||||
<div
|
||||
className="section flex-row flex-right"
|
||||
style={{ margin: '20px 0' }}
|
||||
>
|
||||
{ nextButton }
|
||||
</div>
|
||||
)
|
||||
|
||||
return buttonContainer
|
||||
}
|
||||
|
||||
generateListOfAccounts () {
|
||||
const { keyrings, identities } = this.props
|
||||
const accountsCells = []
|
||||
keyrings.forEach((keyring) => {
|
||||
if (!ifContractAcc(keyring)) {
|
||||
keyring.accounts.forEach((address) => {
|
||||
const identity = identities[address]
|
||||
accountsCells.push(
|
||||
<ExecutorCell
|
||||
key={Math.random()}
|
||||
address={address}
|
||||
identity={identity}
|
||||
isAccountSelected={this.isAccountSelected(address)}
|
||||
onClick={(e, isSelected) => this.selectExecutor(e, isSelected, address)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.setState({
|
||||
accountsCells,
|
||||
})
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (prevState.selectedExecutor !== this.state.selectedExecutor) {
|
||||
this.generateListOfAccounts()
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const { txParams } = this.props
|
||||
const { selectedExecutor } = this.state
|
||||
this.props.setSelectedAddress(selectedExecutor)
|
||||
txParams.from = selectedExecutor
|
||||
this.props.signTx(txParams)
|
||||
}
|
||||
|
||||
selectExecutor (e, isSelected, address) {
|
||||
if (isSelected) {
|
||||
this.setState({
|
||||
selectedExecutor: address,
|
||||
nextDisabled: false,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
selectedExecutor: '',
|
||||
nextDisabled: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
isAccountSelected (address) {
|
||||
return address === this.state.selectedExecutor
|
||||
}
|
||||
|
||||
back () {
|
||||
const { methodSelected, methodABI, inputValues } = this.props
|
||||
this.props.showSendContractPage({methodSelected, methodABI, inputValues})
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const result = {
|
||||
selected: state.metamask.selectedAddress,
|
||||
accounts: state.metamask.accounts,
|
||||
keyrings: state.metamask.keyrings,
|
||||
identities: state.metamask.identities,
|
||||
warning: state.appState.warning,
|
||||
txParams: state.appState.txParams,
|
||||
methodSelected: state.appState.contractAcc && state.appState.contractAcc.methodSelected,
|
||||
methodABI: state.appState.contractAcc && state.appState.contractAcc.methodABI,
|
||||
inputValues: state.appState.contractAcc && state.appState.contractAcc.inputValues,
|
||||
}
|
||||
|
||||
result.error = result.warning && result.warning.message
|
||||
return result
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
hideWarning: () => dispatch(actions.hideWarning()),
|
||||
signTx: (txParams) => dispatch(actions.signTx(txParams)),
|
||||
setSelectedAddress: (address) => dispatch(actions.setSelectedAddress(address)),
|
||||
showSendContractPage: ({ methodSelected, methodABI, inputValues }) => dispatch(actions.showSendContractPage({methodSelected, methodABI, inputValues})),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(ChooseContractExecutor)
|
|
@ -0,0 +1,83 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import Identicon from '../identicon'
|
||||
import { addressSummary } from '../../util'
|
||||
|
||||
class ExecutorCell extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isSelected: props.isAccountSelected,
|
||||
}
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
isAccountSelected: PropTypes.bool,
|
||||
address: PropTypes.string,
|
||||
identity: PropTypes.object,
|
||||
onClick: PropTypes.func,
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
address,
|
||||
identity,
|
||||
} = this.props
|
||||
const { isSelected } = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
className={isSelected ? 'executor-cell-container-selected' : 'executor-cell-container'}
|
||||
onClick={(e) => {
|
||||
this.setState({
|
||||
isSelected: !isSelected,
|
||||
})
|
||||
this.props.onClick(e, !isSelected)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="account-data-subsection flex-row flex-grow"
|
||||
style={{
|
||||
background: 'linear-gradient(rgb(84, 36, 147), rgb(104, 45, 182))',
|
||||
padding: '20px',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
{/* header - identicon + nav */}
|
||||
<div className="flex-row flex-space-between">
|
||||
{/* large identicon*/}
|
||||
<div
|
||||
className="identicon-wrapper flex-column flex-center select-none"
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<Identicon diameter={40} address={address} />
|
||||
</div>
|
||||
{/* invisible place holder */}
|
||||
<i className="fa fa-users fa-lg invisible" style={{ marginTop: '28px' }} />
|
||||
</div>
|
||||
{/* account label */}
|
||||
<div className="flex-column" style={{ alignItems: 'flex-start' }} >
|
||||
<h2
|
||||
className="font-medium flex-center"
|
||||
style={{
|
||||
color: '#ffffff',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>{identity && identity.name}</h2>
|
||||
{/* address and getter actions */}
|
||||
<div
|
||||
className="flex-row flex-center"
|
||||
style={{ color: 'rgba(255, 255, 255, 0.7)' }}
|
||||
>
|
||||
<div style={{ lineHeight: '16px', fontSize: '14px' }}>
|
||||
{addressSummary(address)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ExecutorCell
|
|
@ -0,0 +1,376 @@
|
|||
import React, { Component } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import PersistentForm from '../../../lib/persistent-form'
|
||||
import SendProfile from './send-profile'
|
||||
import SendHeader from './send-header'
|
||||
import SendError from './send-error'
|
||||
import Select from 'react-select'
|
||||
import actions from '../../../../ui/app/actions'
|
||||
import abiEncoder from 'web3-eth-abi'
|
||||
import Web3 from 'web3'
|
||||
import copyToClipboard from 'copy-to-clipboard'
|
||||
|
||||
class SendTransactionInput extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
inputVal: props.defaultValue,
|
||||
}
|
||||
this.timerID = null
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
placeholder: PropTypes.string,
|
||||
defaultValue: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
className="input large-input"
|
||||
placeholder={this.props.placeholder}
|
||||
value={this.state.inputVal}
|
||||
onChange={e => {
|
||||
this.setState({
|
||||
inputVal: e.target.value,
|
||||
})
|
||||
this.props.onChange(e)
|
||||
}
|
||||
}
|
||||
style={{ marginTop: '5px' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SendTransactionScreen extends PersistentForm {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
options: [],
|
||||
abi: [],
|
||||
methodSelected: props.methodSelected,
|
||||
methodABI: props.methodABI,
|
||||
methodInputs: [],
|
||||
methodInputsView: [],
|
||||
methodOutput: null,
|
||||
isConstantMethod: false,
|
||||
inputValues: props.inputValues || {},
|
||||
output: '',
|
||||
copyDisabled: true,
|
||||
}
|
||||
PersistentForm.call(this)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.hideToast()
|
||||
clearTimeout(this.timerID)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.getContractMethods()
|
||||
}
|
||||
|
||||
render () {
|
||||
this.persistentFormParentId = 'send-contract-tx-form'
|
||||
|
||||
const {
|
||||
error,
|
||||
} = this.props
|
||||
return (
|
||||
<div className="send-screen flex-column flex-grow">
|
||||
<SendProfile />
|
||||
<SendHeader title="Execute Method" />
|
||||
<SendError
|
||||
error={error}
|
||||
onClose={() => { this.props.hideWarning() }}
|
||||
/>
|
||||
{this.props.toastMsg ? <div className="toast">{this.props.toastMsg}</div> : null}
|
||||
<div style={{ padding: '0 30px' }}>
|
||||
<Select
|
||||
clearable={false}
|
||||
value={this.state.methodSelected}
|
||||
options={this.state.options}
|
||||
style={{ marginBottom: '10px' }}
|
||||
onChange={(opt) => {
|
||||
this.setState({
|
||||
methodSelected: opt.value,
|
||||
isConstantMethod: opt.metadata.constant,
|
||||
methodABI: opt.metadata,
|
||||
output: '',
|
||||
inputValues: {},
|
||||
})
|
||||
this.generateMethodInputsView(opt.metadata)
|
||||
}}
|
||||
/>
|
||||
<div style={{ overflow: 'auto', maxHeight: this.state.isConstantMethod ? '120px' : '210px' }}>
|
||||
{this.state.methodInputsView}
|
||||
</div>
|
||||
{this.state.isConstantMethod && this.methodOutput()}
|
||||
{this.buttonsSection()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.methodSelected) {
|
||||
this.generateMethodInputsView(this.props.methodABI)
|
||||
}
|
||||
}
|
||||
|
||||
async getContractMethods () {
|
||||
const contractProps = await this.props.getContract(this.props.address)
|
||||
const abi = contractProps && contractProps.abi
|
||||
const options = abi && abi.reduce((filtered, obj) => {
|
||||
if (obj.type === 'function') {
|
||||
filtered.push({ label: obj.name, value: obj.name, metadata: obj })
|
||||
}
|
||||
return filtered
|
||||
}, [])
|
||||
options.sort((option1, option2) => (option1.label).localeCompare(option2.label))
|
||||
this.setState({
|
||||
options,
|
||||
abi,
|
||||
})
|
||||
}
|
||||
|
||||
generateMethodInput (params, ind) {
|
||||
const label = (
|
||||
<h3
|
||||
key={`method_label_${ind}`}
|
||||
style={{ marginTop: '10px' }}
|
||||
>
|
||||
{params.name || `Input ${ind + 1}`}
|
||||
</h3>
|
||||
)
|
||||
const input = (
|
||||
<SendTransactionInput
|
||||
key={Math.random()}
|
||||
ind={ind}
|
||||
placeholder={params.type}
|
||||
defaultValue={(this.props.inputValues && this.props.inputValues[ind]) || ''}
|
||||
onChange={e => this.handleInputChange(e, ind)}
|
||||
/>
|
||||
)
|
||||
const inputObj = (
|
||||
<div key={`method_label_container_${ind}`}>
|
||||
{label}
|
||||
{input}
|
||||
</div>
|
||||
)
|
||||
return inputObj
|
||||
}
|
||||
|
||||
handleInputChange (e, ind) {
|
||||
const { inputValues } = this.state
|
||||
if (e.target.value) {
|
||||
inputValues[ind] = e.target.value
|
||||
} else {
|
||||
delete inputValues[ind]
|
||||
}
|
||||
this.setState({
|
||||
inputValues,
|
||||
})
|
||||
}
|
||||
|
||||
generateMethodInputsView (metadata) {
|
||||
this.setState({
|
||||
methodInputs: [],
|
||||
methodInputsView: [],
|
||||
})
|
||||
const methodInputsView = []
|
||||
const methodInputs = metadata && metadata.inputs
|
||||
methodInputs.forEach((input, ind) => {
|
||||
methodInputsView.push(this.generateMethodInput(input, ind))
|
||||
})
|
||||
this.setState({
|
||||
methodInputs,
|
||||
methodInputsView,
|
||||
})
|
||||
}
|
||||
|
||||
methodOutput () {
|
||||
const label = (
|
||||
<h2
|
||||
key="method_output_label"
|
||||
style={{
|
||||
marginTop: '10px',
|
||||
}}
|
||||
>
|
||||
Output
|
||||
</h2>
|
||||
)
|
||||
const output = (
|
||||
<textarea
|
||||
key="method_output_value"
|
||||
className="input large-input"
|
||||
disabled={true}
|
||||
value={this.state.output}
|
||||
style={{
|
||||
marginTop: '5px',
|
||||
width: '100%',
|
||||
height: '50px',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
const outputObj = (
|
||||
<div>
|
||||
{label}
|
||||
{output}
|
||||
</div>
|
||||
)
|
||||
return outputObj
|
||||
}
|
||||
|
||||
buttonsSection () {
|
||||
const { isConstantMethod } = this.state
|
||||
const callButton = (
|
||||
<button disabled={this.buttonDisabled()} onClick={() => this.callData()}>Call data</button>
|
||||
)
|
||||
const nextButton = (
|
||||
<div>
|
||||
<button
|
||||
disabled={this.buttonDisabled()}
|
||||
style={{ marginRight: '20px' }}
|
||||
className="btn-violet"
|
||||
onClick={() => this.copyAbiEncoded()}
|
||||
>Copy ABI encoded
|
||||
</button>
|
||||
<button disabled={this.buttonDisabled()} onClick={() => this.onSubmit()}>Next</button>
|
||||
</div>
|
||||
)
|
||||
const executeButton = isConstantMethod ? callButton : nextButton
|
||||
|
||||
const buttonContainer = (
|
||||
<div
|
||||
className="section flex-row flex-right"
|
||||
style={{ margin: '20px 0' }}
|
||||
>
|
||||
{ executeButton }
|
||||
</div>
|
||||
)
|
||||
|
||||
return buttonContainer
|
||||
}
|
||||
|
||||
buttonDisabled = () => {
|
||||
const { methodSelected, methodInputs, inputValues } = this.state
|
||||
return !methodSelected || (methodInputs.length !== Object.keys(inputValues).length)
|
||||
}
|
||||
|
||||
callData = () => {
|
||||
this.props.showLoadingIndication()
|
||||
const { abi, methodSelected, inputValues } = this.state
|
||||
const { address } = this.props
|
||||
const web3 = new Web3(global.ethereumProvider)
|
||||
|
||||
const inputValuesArray = Object.keys(inputValues).map(key => inputValues[key])
|
||||
try {
|
||||
web3.eth.contract(abi).at(address)[methodSelected].call(...inputValuesArray, (err, output) => {
|
||||
this.props.hideLoadingIndication()
|
||||
if (err) {
|
||||
this.props.hideToast()
|
||||
return this.props.displayWarning(err)
|
||||
}
|
||||
if (output) {
|
||||
this.setState({
|
||||
output,
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
this.props.hideToast()
|
||||
return this.props.displayWarning(e)
|
||||
}
|
||||
}
|
||||
|
||||
encodeFunctionCall = () => {
|
||||
const { inputValues, methodABI } = this.state
|
||||
const inputValuesArray = Object.keys(inputValues).map(key => inputValues[key])
|
||||
let txData
|
||||
try {
|
||||
txData = abiEncoder.encodeFunctionCall(methodABI, inputValuesArray)
|
||||
this.props.hideWarning()
|
||||
} catch (e) {
|
||||
this.props.hideToast()
|
||||
this.props.displayWarning(e)
|
||||
}
|
||||
|
||||
return txData
|
||||
}
|
||||
|
||||
copyAbiEncoded = () => {
|
||||
const txData = this.encodeFunctionCall()
|
||||
if (txData) {
|
||||
copyToClipboard(txData)
|
||||
this.props.displayToast('Contract ABI encoded method call has been successfully copied to clipboard')
|
||||
this.timerID = setTimeout(() => {
|
||||
this.props.hideToast()
|
||||
}, 4000)
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const { inputValues, methodABI, methodSelected } = this.state
|
||||
const { address } = this.props
|
||||
const txData = this.encodeFunctionCall()
|
||||
|
||||
if (txData) {
|
||||
this.props.hideWarning()
|
||||
|
||||
const txParams = {
|
||||
value: '0x',
|
||||
data: txData,
|
||||
to: address,
|
||||
}
|
||||
|
||||
this.props.showChooseContractExecutorPage({methodSelected, methodABI, inputValues, txParams})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const contractAcc = state.appState.contractAcc
|
||||
const result = {
|
||||
address: state.metamask.selectedAddress,
|
||||
warning: state.appState.warning,
|
||||
toastMsg: state.appState.toastMsg,
|
||||
methodSelected: contractAcc && contractAcc.methodSelected,
|
||||
methodABI: contractAcc && contractAcc.methodABI,
|
||||
inputValues: contractAcc && contractAcc.inputValues,
|
||||
}
|
||||
|
||||
result.error = result.warning && result.warning.message
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function mapDispatchToProps (dispatch) {
|
||||
return {
|
||||
showLoadingIndication: () => dispatch(actions.showLoadingIndication()),
|
||||
hideLoadingIndication: () => dispatch(actions.hideLoadingIndication()),
|
||||
getContract: (addr) => dispatch(actions.getContract(addr)),
|
||||
displayToast: (msg) => dispatch(actions.displayToast(msg)),
|
||||
hideToast: () => dispatch(actions.hideToast()),
|
||||
displayWarning: (msg) => dispatch(actions.displayWarning(msg)),
|
||||
hideWarning: () => dispatch(actions.hideWarning()),
|
||||
showChooseContractExecutorPage: ({
|
||||
methodSelected,
|
||||
methodABI,
|
||||
inputValues,
|
||||
txParams,
|
||||
}) => dispatch(actions.showChooseContractExecutorPage({
|
||||
methodSelected,
|
||||
methodABI,
|
||||
inputValues,
|
||||
txParams,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps, mapDispatchToProps)(SendTransactionScreen)
|
|
@ -0,0 +1,50 @@
|
|||
import React, {Component} from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
class SendError extends Component {
|
||||
static propTypes = {
|
||||
error: PropTypes.string,
|
||||
onClose: PropTypes.func,
|
||||
}
|
||||
|
||||
render () {
|
||||
return this.props.error ? (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
background: 'rgba(255, 255, 255, 0.85)',
|
||||
width: '100%',
|
||||
paddingLeft: '30px',
|
||||
paddingRight: '30px',
|
||||
paddingTop: '70%',
|
||||
zIndex: '100',
|
||||
height: '100%',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url('../images/close.svg')`,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={(e) => this.props.onClose(e)}
|
||||
/>
|
||||
<div style={{
|
||||
marginLeft: '30px',
|
||||
marginRight: '30px',
|
||||
}} >
|
||||
<div
|
||||
className="error"
|
||||
style={{
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>{this.props.error}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SendError
|
|
@ -0,0 +1,50 @@
|
|||
import React, {Component} from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import PropTypes from 'prop-types'
|
||||
import actions from '../../../../ui/app/actions'
|
||||
|
||||
class SendHeader extends Component {
|
||||
static propTypes = {
|
||||
back: PropTypes.func,
|
||||
dispatch: PropTypes.func,
|
||||
address: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<h3
|
||||
className="flex-center send-header"
|
||||
style={{
|
||||
marginTop: '18px',
|
||||
marginBottom: '14px',
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className="fa fa-arrow-left fa-lg cursor-pointer"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '30px',
|
||||
}}
|
||||
onClick={() => { this.props.back ? this.props.back() : this.back() }}
|
||||
/>
|
||||
{ this.props.title }
|
||||
</h3>
|
||||
)
|
||||
}
|
||||
|
||||
back () {
|
||||
const address = this.props.address
|
||||
this.props.dispatch(actions.backToAccountDetail(address))
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const result = {
|
||||
address: state.metamask.selectedAddress,
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(SendHeader)
|
|
@ -0,0 +1,95 @@
|
|||
import React, {Component} from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import Identicon from '../identicon'
|
||||
import { addressSummary } from '../../util'
|
||||
import EthBalance from '../eth-balance'
|
||||
import TokenBalance from '../token-balance'
|
||||
|
||||
class SendProfile extends Component {
|
||||
render () {
|
||||
const props = this.props
|
||||
const {
|
||||
address,
|
||||
account,
|
||||
identity,
|
||||
network,
|
||||
conversionRate,
|
||||
currentCurrency,
|
||||
isToken,
|
||||
token,
|
||||
} = props
|
||||
return (
|
||||
<div
|
||||
className="account-data-subsection flex-row flex-grow"
|
||||
style={{
|
||||
background: 'linear-gradient(rgb(84, 36, 147), rgb(104, 45, 182))',
|
||||
padding: '30px',
|
||||
minHeight: '178px',
|
||||
}}
|
||||
>
|
||||
{/* header - identicon + nav */}
|
||||
<div className="flex-row flex-space-between">
|
||||
{/* large identicon*/}
|
||||
<div
|
||||
className="identicon-wrapper flex-column flex-center select-none"
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
<Identicon diameter={62} address={address} />
|
||||
</div>
|
||||
{/* invisible place holder */}
|
||||
<i className="fa fa-users fa-lg invisible" style={{ marginTop: '28px' }} />
|
||||
</div>
|
||||
{/* account label */}
|
||||
<div className="flex-column" style={{ alignItems: 'flex-start' }} >
|
||||
<h2
|
||||
className="send-profile-identity-name font-medium flex-center"
|
||||
style={{
|
||||
color: '#ffffff',
|
||||
paddingTop: '8px',
|
||||
marginBottom: '8px',
|
||||
}}
|
||||
>{identity && identity.name}</h2>
|
||||
{/* address and getter actions */}
|
||||
<div
|
||||
className="flex-row flex-center"
|
||||
style={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
marginBottom: '30px',
|
||||
}}
|
||||
>
|
||||
<div className="send-profile-address" style={{ lineHeight: '16px', fontSize: '14px' }}>
|
||||
{addressSummary(address)}
|
||||
</div>
|
||||
</div>
|
||||
{/* balance */}
|
||||
<div className="flex-row flex-center">
|
||||
{isToken ? <TokenBalance token={token} /> : <EthBalance {...{
|
||||
value: account && account.balance,
|
||||
conversionRate,
|
||||
currentCurrency,
|
||||
network,
|
||||
}} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps (state) {
|
||||
var result = {
|
||||
address: state.metamask.selectedAddress,
|
||||
accounts: state.metamask.accounts,
|
||||
identities: state.metamask.identities,
|
||||
network: state.metamask.network,
|
||||
conversionRate: state.metamask.conversionRate,
|
||||
currentCurrency: state.metamask.currentCurrency,
|
||||
}
|
||||
|
||||
result.account = result.accounts[result.address]
|
||||
result.identity = result.identities[result.address]
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
module.exports = connect(mapStateToProps)(SendProfile)
|
|
@ -1,21 +1,24 @@
|
|||
const inherits = require('util').inherits
|
||||
const PersistentForm = require('../lib/persistent-form')
|
||||
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 actions = require('../../../../ui/app/actions')
|
||||
const {
|
||||
numericBalance,
|
||||
isInvalidChecksumAddress,
|
||||
isValidAddress,
|
||||
} = require('../../util')
|
||||
const EnsInput = require('../ens-input')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
const { tokenInfoGetter, calcTokenAmountWithDec } = require('../../ui/app/token-util')
|
||||
const { tokenInfoGetter, calcTokenAmountWithDec } = require('../../../../ui/app/token-util')
|
||||
const TokenTracker = require('eth-token-watcher')
|
||||
const Loading = require('./components/loading')
|
||||
const Loading = require('../loading')
|
||||
const BigNumber = require('bignumber.js')
|
||||
BigNumber.config({ ERRORS: false })
|
||||
const log = require('loglevel')
|
||||
import SendProfile from './send-profile'
|
||||
import SendHeader from './send-header'
|
||||
import SendError from './send-error'
|
||||
|
||||
module.exports = connect(mapStateToProps)(SendTransactionScreen)
|
||||
|
||||
|
@ -33,7 +36,6 @@ function mapStateToProps (state) {
|
|||
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
|
||||
|
@ -65,11 +67,10 @@ SendTransactionScreen.prototype.render = function () {
|
|||
|
||||
const props = this.props
|
||||
const {
|
||||
address,
|
||||
identity,
|
||||
network,
|
||||
identities,
|
||||
addressBook,
|
||||
error,
|
||||
} = props
|
||||
|
||||
return (
|
||||
|
@ -80,109 +81,23 @@ SendTransactionScreen.prototype.render = function () {
|
|||
// 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,
|
||||
}),
|
||||
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
h(SendProfile, {
|
||||
isToken: true,
|
||||
token,
|
||||
}),
|
||||
|
||||
//
|
||||
// Required Fields
|
||||
// Send Header
|
||||
//
|
||||
|
||||
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`,
|
||||
]),
|
||||
h(SendHeader, {
|
||||
title: `Send ${this.state.token.symbol} Tokens`,
|
||||
}),
|
||||
|
||||
// error message
|
||||
props.error && h('div', {style: {
|
||||
marginLeft: '30px',
|
||||
marginRight: '30px',
|
||||
}}, [
|
||||
h('div.error.flex-center', props.error),
|
||||
]),
|
||||
h(SendError, {
|
||||
error,
|
||||
}),
|
||||
|
||||
// 'to' field
|
||||
h('section.flex-row.flex-center', [
|
||||
|
@ -255,7 +170,7 @@ SendTransactionScreen.prototype.componentWillUnmount = function () {
|
|||
SendTransactionScreen.prototype.createFreshTokenTracker = function () {
|
||||
this.setState({isLoading: true})
|
||||
const { address, tokenAddress } = this.props
|
||||
if (!util.isValidAddress(tokenAddress)) return
|
||||
if (!isValidAddress(tokenAddress)) return
|
||||
if (this.tracker) {
|
||||
// Clean up old trackers when refreshing:
|
||||
this.tracker.stop()
|
||||
|
@ -303,11 +218,6 @@ SendTransactionScreen.prototype.navigateToAccounts = function (event) {
|
|||
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,
|
||||
|
@ -352,12 +262,12 @@ SendTransactionScreen.prototype.onSubmit = async function () {
|
|||
return this.props.dispatch(actions.displayWarning(message))
|
||||
}
|
||||
|
||||
if ((util.isInvalidChecksumAddress(recipient))) {
|
||||
if ((isInvalidChecksumAddress(recipient))) {
|
||||
message = 'Recipient address checksum is invalid.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
}
|
||||
|
||||
if (!util.isValidAddress(recipient) || (!recipient)) {
|
||||
if (!isValidAddress(recipient) || (!recipient)) {
|
||||
message = 'Recipient address is invalid.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
}
|
|
@ -1,16 +1,20 @@
|
|||
const inherits = require('util').inherits
|
||||
const PersistentForm = require('../lib/persistent-form')
|
||||
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 isHex = require('./util').isHex
|
||||
const EthBalance = require('./components/eth-balance')
|
||||
const EnsInput = require('./components/ens-input')
|
||||
const actions = require('../../../../ui/app/actions')
|
||||
const {
|
||||
numericBalance,
|
||||
isHex,
|
||||
normalizeEthStringToWei,
|
||||
isInvalidChecksumAddress,
|
||||
isValidAddress,
|
||||
} = require('../../util')
|
||||
const EnsInput = require('../ens-input')
|
||||
const ethUtil = require('ethereumjs-util')
|
||||
import SendProfile from './send-profile'
|
||||
import SendHeader from './send-header'
|
||||
import SendError from './send-error'
|
||||
module.exports = connect(mapStateToProps)(SendTransactionScreen)
|
||||
|
||||
function mapStateToProps (state) {
|
||||
|
@ -21,14 +25,10 @@ function mapStateToProps (state) {
|
|||
warning: state.appState.warning,
|
||||
network: state.metamask.network,
|
||||
addressBook: state.metamask.addressBook,
|
||||
conversionRate: state.metamask.conversionRate,
|
||||
currentCurrency: state.metamask.currentCurrency,
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -44,14 +44,10 @@ SendTransactionScreen.prototype.render = function () {
|
|||
|
||||
const props = this.props
|
||||
const {
|
||||
address,
|
||||
account,
|
||||
identity,
|
||||
network,
|
||||
identities,
|
||||
addressBook,
|
||||
conversionRate,
|
||||
currentCurrency,
|
||||
error,
|
||||
} = props
|
||||
|
||||
return (
|
||||
|
@ -62,112 +58,23 @@ SendTransactionScreen.prototype.render = function () {
|
|||
// 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(EthBalance, {
|
||||
value: account && account.balance,
|
||||
conversionRate,
|
||||
currentCurrency,
|
||||
network,
|
||||
}),
|
||||
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
h(SendProfile),
|
||||
|
||||
//
|
||||
// Required Fields
|
||||
// Send Header
|
||||
//
|
||||
|
||||
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 Transaction',
|
||||
]),
|
||||
h(SendHeader, {
|
||||
title: 'Send Transaction',
|
||||
}),
|
||||
|
||||
// error message
|
||||
props.error && h('div', {style: {
|
||||
marginLeft: '30px',
|
||||
marginRight: '30px',
|
||||
}}, [
|
||||
h('div.error.flex-center', props.error),
|
||||
]),
|
||||
h(SendError, {
|
||||
error,
|
||||
onClose: () => {
|
||||
this.props.dispatch(actions.hideWarning())
|
||||
},
|
||||
}),
|
||||
|
||||
// 'to' field
|
||||
h('section.flex-row.flex-center', [
|
||||
|
@ -243,11 +150,6 @@ SendTransactionScreen.prototype.navigateToAccounts = function (event) {
|
|||
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,
|
||||
|
@ -277,7 +179,7 @@ SendTransactionScreen.prototype.onSubmit = function () {
|
|||
}
|
||||
}
|
||||
|
||||
const value = util.normalizeEthStringToWei(input)
|
||||
const value = normalizeEthStringToWei(input)
|
||||
const txData = document.querySelector('input[name="txData"]').value
|
||||
const balance = this.props.balance
|
||||
|
||||
|
@ -291,12 +193,12 @@ SendTransactionScreen.prototype.onSubmit = function () {
|
|||
return this.props.dispatch(actions.displayWarning(message))
|
||||
}
|
||||
|
||||
if ((util.isInvalidChecksumAddress(recipient))) {
|
||||
if ((isInvalidChecksumAddress(recipient))) {
|
||||
message = 'Recipient address checksum is invalid.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
}
|
||||
|
||||
if ((!util.isValidAddress(recipient) && !txData) || (!recipient && !txData)) {
|
||||
if ((!isValidAddress(recipient) && !txData) || (!recipient && !txData)) {
|
||||
message = 'Recipient address is invalid.'
|
||||
return this.props.dispatch(actions.displayWarning(message))
|
||||
}
|
|
@ -279,6 +279,31 @@ app sections
|
|||
}
|
||||
|
||||
/* unlock */
|
||||
.toast {
|
||||
border: 1px solid #60db97 !important;
|
||||
color: #ffffff !important;
|
||||
font-size: 12px;
|
||||
background: #60db97;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
width: 357px;
|
||||
line-height: 14px;
|
||||
position: fixed;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
z-index: 100;
|
||||
animation: 500ms ease-out 0s move;
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
from { top: -50px; }
|
||||
to {
|
||||
top: 0px;
|
||||
transform: translateY(1);
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-bottom: 9px;
|
||||
border: 1px solid #ff1345 !important;
|
||||
|
@ -1162,6 +1187,6 @@ div.message-container > div:first-child {
|
|||
visibility: hidden;
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
input[disabled],textarea[disabled] {
|
||||
background: #FFFADE
|
||||
}
|
||||
|
|
|
@ -237,6 +237,7 @@ hr.horizontal-line {
|
|||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
width: 68px;
|
||||
}
|
||||
|
||||
.ether-balance {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
.executor-cell-container {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
border: 3px solid white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.executor-cell-container-selected {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
border: 3px solid #60db97;
|
||||
border-radius: 5px;
|
||||
}
|
|
@ -41,6 +41,9 @@ module.exports = {
|
|||
exportAsFile: exportAsFile,
|
||||
isInvalidChecksumAddress,
|
||||
countSignificantDecimals,
|
||||
getCurrentKeyring,
|
||||
ifLooseAcc,
|
||||
ifContractAcc,
|
||||
}
|
||||
|
||||
function valuesFor (obj) {
|
||||
|
@ -255,8 +258,10 @@ function exportAsFile (filename, data) {
|
|||
|
||||
/**
|
||||
* returns the length of truncated significant decimals for fiat value
|
||||
*
|
||||
* @param {float} val The float value to be truncated
|
||||
* @param {number} len The length of significant decimals
|
||||
*
|
||||
* returns {number} The length of truncated significant decimals
|
||||
**/
|
||||
function countSignificantDecimals (val, len) {
|
||||
|
@ -278,3 +283,57 @@ function countSignificantDecimals (val, len) {
|
|||
decimalsLen = parseFloat(valWithSignificantDecimals).toString().split('.')[1].length
|
||||
return decimalsLen || 0
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieves the current unlocked keyring
|
||||
*
|
||||
* @param {string} address The current unlocked address
|
||||
* @param {array} keyrings The array of keyrings
|
||||
* @param {array} identities The array of identities
|
||||
*
|
||||
* returns {object} keyring object corresponding to unlocked address
|
||||
**/
|
||||
function getCurrentKeyring (address, network, keyrings, identities) {
|
||||
const identity = identities[address]
|
||||
const simpleAddress = identity && identity.address.substring(2).toLowerCase()
|
||||
const keyring = keyrings && keyrings.find((kr) => {
|
||||
const isAddressIncluded = kr.accounts.includes(simpleAddress) || kr.accounts.includes(address)
|
||||
if (ifContractAcc(kr)) {
|
||||
return kr.network === network && isAddressIncluded
|
||||
} else {
|
||||
return isAddressIncluded
|
||||
}
|
||||
})
|
||||
|
||||
return keyring
|
||||
}
|
||||
|
||||
/**
|
||||
* checks, if keyring is imported account
|
||||
*
|
||||
* @param {object} keyring
|
||||
*
|
||||
* returns {boolean} true, if keyring is importec and false, if it is not
|
||||
**/
|
||||
function ifLooseAcc (keyring) {
|
||||
try { // Sometimes keyrings aren't loaded yet:
|
||||
const type = keyring.type
|
||||
const isLoose = type !== 'HD Key Tree'
|
||||
return isLoose
|
||||
} catch (e) { return }
|
||||
}
|
||||
|
||||
/**
|
||||
* checks, if keyring is contract
|
||||
*
|
||||
* @param {object} keyring
|
||||
*
|
||||
* returns {boolean} true, if keyring is contract and false, if it is not
|
||||
**/
|
||||
function ifContractAcc (keyring) {
|
||||
try { // Sometimes keyrings aren't loaded yet:
|
||||
const type = keyring.type
|
||||
const isContract = type === 'Simple Address'
|
||||
return isContract
|
||||
} catch (e) { return }
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ var cssFiles = {
|
|||
'lib.css': fs.readFileSync(path.join(__dirname, '/app/css/lib.css'), 'utf8'),
|
||||
'search-token.css': fs.readFileSync(path.join(__dirname, '/app/css/search-token.css'), 'utf8'),
|
||||
'hw.css': fs.readFileSync(path.join(__dirname, '/app/css/hw.css'), 'utf8'),
|
||||
'send-contract.css': fs.readFileSync(path.join(__dirname, '/app/css/send-contract.css'), 'utf8'),
|
||||
'confirm-add-token.css': fs.readFileSync(path.join(__dirname, '/app/css/confirm-add-token.css'), 'utf8'),
|
||||
'page-container.css': fs.readFileSync(path.join(__dirname, '/app/css/page-container.css'), 'utf8'),
|
||||
'index.css': fs.readFileSync(path.join(__dirname, '/app/css/index.css'), 'utf8'),
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -115,7 +115,7 @@
|
|||
"eth-ens-namehash": "^2.0.8",
|
||||
"eth-json-rpc-filters": "github:poanetwork/eth-json-rpc-filters#3.0.2",
|
||||
"eth-json-rpc-infura": "^3.0.0",
|
||||
"eth-keychain-controller": "^5.0.0",
|
||||
"eth-keychain-controller": "github:vbaranov/KeyringController#simple-address",
|
||||
"eth-ledger-bridge-keyring": "^0.1.0",
|
||||
"eth-method-registry": "^1.0.0",
|
||||
"eth-net-props": "^1.0.10",
|
||||
|
@ -208,6 +208,7 @@
|
|||
"valid-url": "^1.0.9",
|
||||
"vreme": "^3.0.2",
|
||||
"web3": "^0.20.1",
|
||||
"web3-eth-abi": "^1.0.0-beta.36",
|
||||
"web3-stream-provider": "^3.0.1",
|
||||
"xtend": "^4.0.1"
|
||||
},
|
||||
|
|
|
@ -2,7 +2,10 @@ const webdriver = require('selenium-webdriver')
|
|||
const { By } = webdriver
|
||||
module.exports = {
|
||||
elements: {
|
||||
errorClose: By.css('.send-screen > div:nth-child(3) > div:nth-child(1)'),
|
||||
error: By.className('error'),
|
||||
loader: By.css('#app-content > div > div.full-flex-height > img'),
|
||||
poaABI: '[{"type":"function","stateMutability":"view","payable":false,"outputs":[{"type":"address","name":""}],"name":"abstractStorageAddr","inputs":[],"constant":true},{"type":"function","stateMutability":"view","payable":false,"outputs":[{"type":"address[]","name":""}],"name":"getCrowdsalesForUser","inputs":[{"type":"address","name":"deployer"}],"constant":true},{"type":"function","stateMutability":"view","payable":false,"outputs":[{"type":"uint256","name":""}],"name":"countCrowdsalesForUser","inputs":[{"type":"address","name":"deployer"}],"constant":true},{"type":"function","stateMutability":"nonpayable","payable":false,"outputs":[],"name":"renounceOwnership","inputs":[],"constant":false},{"type":"function","stateMutability":"nonpayable","payable":false,"outputs":[],"name":"changeMintedCappedIdx","inputs":[{"type":"address","name":"newMintedCappedIdxAddr"}],"constant":false},{"type":"function","stateMutability":"nonpayable","payable":false,"outputs":[],"name":"changeDutchIdxAddr","inputs":[{"type":"address","name":"newDutchIdxAddr"}],"constant":false},{"type":"function","stateMutability":"view","payable":false,"outputs":[{"type":"address","name":""}],"name":"owner","inputs":[],"constant":true},{"type":"function","stateMutability":"view","payable":false,"outputs":[{"type":"address","name":""}],"name":"dutchIdxAddr","inputs":[],"constant":true},{"type":"function","stateMutability":"view","payable":false,"outputs":[{"type":"address","name":""}],"name":"mintedCappedIdxAddr","inputs":[],"constant":true},{"type":"function","stateMutability":"nonpayable","payable":false,"outputs":[],"name":"changeAbstractStorage","inputs":[{"type":"address","name":"newAbstractStorageAddr"}],"constant":false},{"type":"function","stateMutability":"nonpayable","payable":false,"outputs":[],"name":"trackCrowdsale","inputs":[{"type":"address","name":"proxyAddress"}],"constant":false},{"type":"function","stateMutability":"nonpayable","payable":false,"outputs":[],"name":"transferOwnership","inputs":[{"type":"address","name":"_newOwner"}],"constant":false},{"type":"constructor","stateMutability":"nonpayable","payable":false,"inputs":[{"type":"address","name":"_abstractStorage"},{"type":"address","name":"_mintedCappedIdx"},{"type":"address","name":"_dutchIdx"}]},{"type":"event","name":"Added","inputs":[{"type":"address","name":"sender","indexed":true},{"type":"address","name":"proxyAddress","indexed":true},{"type":"bytes32","name":"appExecID","indexed":false}],"anonymous":false},{"type":"event","name":"OwnershipRenounced","inputs":[{"type":"address","name":"previousOwner","indexed":true}],"anonymous":false},{"type":"event","name":"OwnershipTransferred","inputs":[{"type":"address","name":"previousOwner","indexed":true},{"type":"address","name":"newOwner","indexed":true}],"anonymous":false}]',
|
||||
},
|
||||
menus: {
|
||||
token: {
|
||||
|
@ -31,7 +34,8 @@ module.exports = {
|
|||
menu: By.css('#app-content > div > div.full-width > div.full-width > div > div:nth-child(2) > span > div'),
|
||||
delete: By.css('#app-content > div > div.full-width > 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.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(3) > span'),
|
||||
import: By.css('#app-content > div > div.full-width > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(5) > span'),
|
||||
// import: By.css('#app-content > div > div.full-width > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(5) > span'),
|
||||
import: By.css('li.dropdown-menu-item:nth-child(5) > span:nth-child(1)'),
|
||||
labelImported: By.css('#app-content > div > div.full-width > div.full-width > div > div:nth-child(2) > span > div > div > span > div > li:nth-child(4) > div.keyring-label'),
|
||||
},
|
||||
dot: {
|
||||
|
@ -46,6 +50,30 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
screens: {
|
||||
chooseContractExecutor: {
|
||||
title: By.className('flex-center send-header'),
|
||||
titleText: 'Choose contract executor',
|
||||
buttonNext: By.css('.choose-contract-next-button'),
|
||||
account: By.className('account-data-subsection flex-row flex-grow'),
|
||||
selectedAccount: By.className('executor-cell-container-selected'),
|
||||
buttonArrow: By.className('fa fa-arrow-left fa-lg cursor-pointer'),
|
||||
|
||||
},
|
||||
executeMethod: {
|
||||
title: By.className('flex-center send-header'),
|
||||
titleText: 'Execute Method',
|
||||
selectArrow: By.className('Select-arrow-zone'),
|
||||
item0: By.css('.Select-input > input:nth-child(1)'),
|
||||
item1: By.className('Select-option'),
|
||||
item11: By.css('#react-select-2--option-11'),
|
||||
buttonCall: By.css('.section > button:nth-child(1)'),
|
||||
fieldOutput: By.css('.input'),
|
||||
fieldParametr1: By.css('.input'),
|
||||
buttonNext: By.css('.section > div:nth-child(1) > button:nth-child(2)'),
|
||||
buttonArrow: By.className('fa fa-arrow-left fa-lg cursor-pointer'),
|
||||
buttonCopyABI: By.className('btn-violet'),
|
||||
},
|
||||
|
||||
eventsEmitter: {
|
||||
button: By.className('btn btn-default'),
|
||||
event: By.className('Toastify__toast-body'),
|
||||
|
@ -59,9 +87,9 @@ module.exports = {
|
|||
error: By.className('error'),
|
||||
accountName: By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(3) > div.identity-panel.flex-row.flex-space-between > div.identity-data.flex-column.flex-justify-center.flex-grow.select-none > h2'),
|
||||
message: By.css('#app-content > div > div.app-primary.from-right > div > div > div:nth-child(3) > div.tx-data.flex-column.flex-justify-center.flex-grow.select-none > div > span'),
|
||||
},
|
||||
},
|
||||
sendTokens: {
|
||||
error: By.className('error flex-center'),
|
||||
error: By.className('error'),
|
||||
errorText: {
|
||||
invalidAmount: 'Invalid token\'s amount',
|
||||
address: 'Recipient address is invalid',
|
||||
|
@ -159,11 +187,13 @@ module.exports = {
|
|||
titleText: 'Delete Custom RPC',
|
||||
},
|
||||
confirmTransaction: {
|
||||
titleText: 'Confirm Transaction',
|
||||
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'),
|
||||
reject: By.css('.cancel'),
|
||||
},
|
||||
fields: {
|
||||
gasLimit: By.css('#pending-tx-form > div:nth-child(1) > div.table-box > div:nth-child(3) > div.cell.value > div > div > input'),
|
||||
|
@ -192,10 +222,19 @@ module.exports = {
|
|||
titleText: 'Delete Imported Account',
|
||||
buttons: {
|
||||
no: By.css('#app-content > div > div.app-primary.from-left > div > div.flex-row.flex-right > button.btn-violet'),
|
||||
yes: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-row.flex-right > button:nth-child(2)'),
|
||||
yes: By.css('div.flex-row:nth-child(4) > button:nth-child(2)'),
|
||||
arrow: By.className('fa fa-arrow-left fa-lg cursor-pointer'),
|
||||
},
|
||||
},
|
||||
importAccounts: {
|
||||
iconCopy: By.className('clipboard cursor-pointer'),
|
||||
warning: By.className('error'),
|
||||
error: By.css('span.error'),
|
||||
selectArrow: By.className('Select-arrow-zone'),
|
||||
selectType: By.name('import-type-select'),
|
||||
itemContract: By.id('react-select-2--option-2'),
|
||||
contractAddress: By.id('address-box'),
|
||||
contractABI: By.id('abi-box'),
|
||||
title: By.css('#app-content > div > div.app-primary.from-right > div > div:nth-child(2) > div.flex-row.flex-center > h2'),
|
||||
textTitle: 'Import Accounts',
|
||||
fieldPrivateKey: By.id('private-key-box'),
|
||||
|
@ -213,6 +252,7 @@ module.exports = {
|
|||
titleText: 'Settings',
|
||||
title: By.css('#app-content > div > div.app-primary.from-right > div > div.section-title.flex-row.flex-center > h2'),
|
||||
buttons: {
|
||||
arrow: By.className('fa fa-arrow-left fa-lg cursor-pointer'),
|
||||
changePassword: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > div:nth-child(10) > button:nth-child(5)'),
|
||||
delete: By.css('#app-content > div > div.app-primary.from-right > div > div.flex-column.flex-justify-center.flex-grow.select-none > div > div:nth-child(1) > button'),
|
||||
},
|
||||
|
@ -241,8 +281,8 @@ module.exports = {
|
|||
menu: By.className('wallet-view__tab-history'),
|
||||
tokens: By.className('activeForm right'),
|
||||
},
|
||||
// balance: By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > div.ether-balance.ether-balance-amount > div > div > div:nth-child(1) > div:nth-child(1)'),
|
||||
balance: By.xpath('//*[@id="app-content"]/div/div[2]/div/div/div[2]/div[1]/div/div/div[1]/div[1]'),
|
||||
// balance: By.css('#app-content > div > div.app-primary.from-right > div > div > div.flex-row > div.ether-balance.ether-balance-amount > div > div > div:nth-child(1) > div:nth-child(1)'),
|
||||
balance: By.xpath('//*[@id="app-content"]/div/div[2]/div/div/div[2]/div[1]/div/div/div[1]/div[1]'),
|
||||
address: By.css('#app-content > div > div.app-primary.from-left > div > div > div:nth-child(1) > flex-column > div.flex-row > div'),
|
||||
tokens: {
|
||||
menu: By.id('wallet-view__tab-tokens'),
|
||||
|
@ -334,3 +374,4 @@ module.exports = {
|
|||
CUSTOM: 'http://test.com',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ describe('Metamask popup page', async function () {
|
|||
})
|
||||
|
||||
after(async function () {
|
||||
await driver.quit()
|
||||
// await driver.quit()
|
||||
})
|
||||
|
||||
describe('Setup', async function () {
|
||||
|
@ -244,9 +244,336 @@ describe('Metamask popup page', async function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Import Contract account', async () => {
|
||||
const poaContract = '0xc6468767214c577013a904900ada0a0dd6653bc3'
|
||||
const wrongAddress = '0xB87b6077D59B01Ab9fa8cd5A1A21D02a4d60D35'
|
||||
const notContractAddress = '0x56B2e3C3cFf7f3921Dc2e0F8B8e20d1eEc29216b'
|
||||
describe('Import Contract', async () => {
|
||||
it('opens import account menu', async function () {
|
||||
await setProvider(NETWORKS.ROPSTEN)
|
||||
const menu = await waitUntilShowUp(menus.account.menu)
|
||||
await menu.click()
|
||||
const item = await waitUntilShowUp(menus.account.import)
|
||||
await item.click()
|
||||
const importAccountTitle = await waitUntilShowUp(screens.importAccounts.title)
|
||||
assert.equal(await importAccountTitle.getText(), screens.importAccounts.textTitle)
|
||||
})
|
||||
|
||||
it("Warning's text is correct", async function () {
|
||||
const field = await waitUntilShowUp(screens.importAccounts.warning)
|
||||
assert.equal(await field.getText(), 'Imported accounts will not be associated with your originally created Nifty Wallet account seedphrase.', "incorrect warning's text")
|
||||
})
|
||||
|
||||
it("Select type 'Contract'", async function () {
|
||||
const field = await waitUntilShowUp(screens.importAccounts.selectArrow)
|
||||
await field.click()
|
||||
const item = await waitUntilShowUp(screens.importAccounts.itemContract)
|
||||
await item.click()
|
||||
})
|
||||
|
||||
it("Field 'Address' is displayed", async function () {
|
||||
await delay(2000)
|
||||
const field = await waitUntilShowUp(screens.importAccounts.contractAddress)
|
||||
assert.notEqual(field, false, "field 'Address' isn't displayed")
|
||||
await field.sendKeys(wrongAddress)
|
||||
})
|
||||
|
||||
it("Button 'Import' is displayed", async function () {
|
||||
const button = await waitUntilShowUp(screens.importAccounts.buttonImport)
|
||||
assert.notEqual(button, false, "button 'Import' isn't displayed")
|
||||
assert.equal(await button.getText(), 'Import', 'wrong name of button')
|
||||
})
|
||||
|
||||
it("Button 'Import' is disabled if incorrect address", async function () {
|
||||
const button = await waitUntilShowUp(screens.importAccounts.buttonImport)
|
||||
assert.equal(await button.isEnabled(), false, 'button enabled')
|
||||
})
|
||||
|
||||
it("Field 'ABI' is displayed", async function () {
|
||||
const field = await waitUntilShowUp(screens.importAccounts.contractABI)
|
||||
assert.notEqual(field, false, "field 'ABI' isn't displayed")
|
||||
})
|
||||
|
||||
it('icon copy is displayed for ABI ', async function () {
|
||||
const field = await waitUntilShowUp(screens.importAccounts.iconCopy)
|
||||
assert.notEqual(field, false, "icon copy isn't displayed")
|
||||
})
|
||||
|
||||
it("Field 'ABI' is empty if contract isn't verified in current network", async function () {
|
||||
const field = await waitUntilShowUp(screens.importAccounts.contractABI)
|
||||
assert.equal(await field.getText(), '', "field 'ABI' isn't displayed")
|
||||
})
|
||||
it("Fill 'Address' with not contract address , POA core", async function () {
|
||||
await setProvider(NETWORKS.POA)
|
||||
const field = await waitUntilShowUp(screens.importAccounts.contractAddress)
|
||||
await clearField(field, 100)
|
||||
await field.sendKeys(notContractAddress)
|
||||
})
|
||||
|
||||
it("Button 'Import' is disabled if not contract address", async function () {
|
||||
const button = await waitUntilShowUp(screens.importAccounts.buttonImport)
|
||||
assert.equal(await button.isEnabled(), false, 'button enabled')
|
||||
})
|
||||
|
||||
it("Fill 'Address' with valid contract , POA core", async function () {
|
||||
const field = await waitUntilShowUp(screens.importAccounts.contractAddress)
|
||||
await clearField(field, 100)
|
||||
await field.sendKeys(poaContract)
|
||||
})
|
||||
|
||||
it("Button 'Import' is enabled if contract address is correct", async function () {
|
||||
await delay(5000)
|
||||
const button = await waitUntilShowUp(screens.importAccounts.buttonImport)
|
||||
assert.equal(await button.isEnabled(), true, 'button enabled')
|
||||
})
|
||||
|
||||
it('ABI is fetched ', async function () {
|
||||
const field = await waitUntilShowUp(screens.importAccounts.contractABI)
|
||||
const abi = await field.getText()
|
||||
assert.equal(abi.length, 2800, "ABI isn't fetched")
|
||||
})
|
||||
|
||||
it("Click button 'Import', main screen opens", async function () {
|
||||
const button = await waitUntilShowUp(screens.importAccounts.buttonImport)
|
||||
await click(button)
|
||||
const identicon = await waitUntilShowUp(screens.main.identicon, 20)
|
||||
assert.notEqual(identicon, false, "main screen isn't opened")
|
||||
})
|
||||
|
||||
it("Click button 'Send', 'Execute Method' screen opens", async function () {
|
||||
const button = await waitUntilShowUp(screens.main.buttons.send)
|
||||
await click(button)
|
||||
const identicon = await waitUntilShowUp(screens.main.identicon, 40)
|
||||
assert.notEqual(identicon, false, "main screen isn't opened")
|
||||
})
|
||||
})
|
||||
describe('Execute Method', () => {
|
||||
const outputData = '0xd70befce3cf1cc88119c8f4eb583ccd4c39d06e2'
|
||||
const notContractAddress = '0x56B2e3C3cFf7f3921Dc2e0F8B8e20d1eEc29216b'
|
||||
it("Click button 'Send', 'Execute Method' screen opens", async function () {
|
||||
await driver.navigate().refresh()
|
||||
await delay(2000)
|
||||
const button = await waitUntilShowUp(screens.main.buttons.send)
|
||||
await click(button)
|
||||
})
|
||||
|
||||
it('title is displayed and correct', async function () {
|
||||
const title = await waitUntilShowUp(screens.executeMethod.title)
|
||||
assert.notEqual(title, false, 'title isn\'t displayed')
|
||||
assert.equal(await title.getText(), screens.executeMethod.titleText, 'incorrect text')
|
||||
})
|
||||
|
||||
it("Select method 'abstractStorageAddr'", async function () {
|
||||
const field = await waitUntilShowUp(screens.executeMethod.selectArrow)
|
||||
await field.click()
|
||||
const item = await waitUntilShowUp(screens.executeMethod.item0)
|
||||
assert.notEqual(item, false, 'no drop down menu')
|
||||
await click(item)
|
||||
})
|
||||
|
||||
it("Button 'Call data' is displayed", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonCall)
|
||||
assert.notEqual(button, false, "button 'Call data' isn't displayed")
|
||||
await button.click()
|
||||
})
|
||||
|
||||
it('method returns correct value', async function () {
|
||||
const field = await waitUntilShowUp(screens.executeMethod.fieldOutput)
|
||||
assert.notEqual(field, false, "field 'Output' isn't displayed")
|
||||
const text = await waitUntilHasText(field)
|
||||
assert.equal(text, outputData, 'incorrect value was returned')
|
||||
})
|
||||
|
||||
it("2nd call doesn't throw the error", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonCall)
|
||||
assert.notEqual(button, false, "button 'Call data' isn't displayed")
|
||||
await button.click()
|
||||
const field = await waitUntilShowUp(screens.executeMethod.fieldOutput)
|
||||
assert.notEqual(field, false, "field 'Output' isn't displayed")
|
||||
const text = await waitUntilHasText(field)
|
||||
assert.equal(text, outputData, 'incorrect value was returned')
|
||||
})
|
||||
it('Click arrow button leads to main screen', async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonArrow)
|
||||
await click(button)
|
||||
const identicon = await waitUntilShowUp(screens.main.identicon, 40)
|
||||
assert.notEqual(identicon, false, "main screen isn't opened")
|
||||
})
|
||||
it("Click button 'Send', 'Execute Method' screen opens", async function () {
|
||||
await driver.navigate().refresh()
|
||||
await delay(2000)
|
||||
const button = await waitUntilShowUp(screens.main.buttons.send)
|
||||
await click(button)
|
||||
})
|
||||
it("Select method 'changeAbstractStorage'", async function () {
|
||||
const field = await waitUntilShowUp(screens.executeMethod.selectArrow)
|
||||
await field.click()
|
||||
const items = await waitUntilShowUp(screens.executeMethod.item1)
|
||||
assert.notEqual(items, false, 'no drop down menu')
|
||||
const item = (await driver.findElements(screens.executeMethod.item1))[1]
|
||||
// await click(item)
|
||||
await item.click()
|
||||
})
|
||||
|
||||
it("Button 'Copy ABI encoded' is displayed", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonCopyABI)
|
||||
assert.notEqual(button, false, "button 'Copy ABI encoded' isn't displayed")
|
||||
|
||||
})
|
||||
|
||||
it("Button 'Copy ABI encoded' is disabled", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonCopyABI)
|
||||
assert.equal(await button.isEnabled(), false, "button 'Copy ABI encoded' enabled")
|
||||
})
|
||||
|
||||
it("Button 'Next' is disabled", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonNext)
|
||||
assert.equal(await button.isEnabled(), false, "button 'Next' enabled")
|
||||
})
|
||||
|
||||
it("Fill out parameter '_newAbstractStorageAddr with wrong data'", async function () {
|
||||
const field = await waitUntilShowUp(screens.executeMethod.fieldParametr1)
|
||||
assert.notEqual(field, false, "field address isn't displayed")
|
||||
await field.sendKeys(wrongAddress)
|
||||
})
|
||||
|
||||
it.skip("Button 'Copy ABI encoded' is disabled", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonCopyABI)
|
||||
assert.equal(await button.isEnabled(), false, "button 'Copy ABI encoded' enabled")
|
||||
})
|
||||
|
||||
it.skip("Button 'Next' is disabled", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonNext)
|
||||
assert.equal(await button.isEnabled(), false, "button 'Next' enabled")
|
||||
})
|
||||
|
||||
it('Error message if wrong parameter', async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonCopyABI)
|
||||
await button.click()
|
||||
const error = await waitUntilShowUp(elements.error)
|
||||
assert.notEqual(error, false, 'no error message')
|
||||
})
|
||||
|
||||
it('Close error message', async function () {
|
||||
const button = await waitUntilShowUp(elements.errorClose)
|
||||
await button.click()
|
||||
const title = await waitUntilShowUp(screens.executeMethod.title)
|
||||
assert.notEqual(title, false, "error message isn't closed")
|
||||
})
|
||||
|
||||
it("Fill out parameter '_newAbstractStorageAddr'", async function () {
|
||||
const field = await waitUntilShowUp(screens.executeMethod.fieldParametr1)
|
||||
await clearField(field, 100)
|
||||
await field.sendKeys(notContractAddress)
|
||||
assert.notEqual(field, false, "field address isn't displayed")
|
||||
})
|
||||
|
||||
it("Button 'Next' is enabled", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonNext)
|
||||
assert.equal(await button.isEnabled(), true, "button 'Next' disabled")
|
||||
})
|
||||
it("Button 'Copy ABI encoded' is enabled", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonCopyABI)
|
||||
assert.equal(await button.isEnabled(), true, "button 'Copy ABI encoded' disabled")
|
||||
await button.click()
|
||||
})
|
||||
|
||||
it("Click button 'Next'", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonNext)
|
||||
assert.notEqual(button, false, "button 'Next' isn't displayed")
|
||||
await button.click()
|
||||
})
|
||||
|
||||
})
|
||||
describe('Choose Contract Executor', () => {
|
||||
|
||||
it('title is displayed and correct', async function () {
|
||||
await delay(5000)
|
||||
const title = await waitUntilShowUp(screens.chooseContractExecutor.title)
|
||||
assert.notEqual(title, false, 'title isn\'t displayed')
|
||||
assert.equal(await title.getText(), screens.chooseContractExecutor.titleText, 'incorrect text')
|
||||
})
|
||||
|
||||
it('two accounts displayed', async function () {
|
||||
const accs = await waitUntilShowUp(screens.chooseContractExecutor.account)
|
||||
assert.notEqual(accs, false, 'accounts aren\'t displayed')
|
||||
const accounts = await driver.findElements(screens.chooseContractExecutor.account)
|
||||
assert.equal(accounts.length, 3, "number of accounts isn't 2")
|
||||
})
|
||||
|
||||
it("Click arrow button leads to 'Execute Method' screen ", async function () {
|
||||
const button = await waitUntilShowUp(screens.chooseContractExecutor.buttonArrow)
|
||||
assert.notEqual(button, false, 'button isn\'t displayed')
|
||||
await button.click()
|
||||
await delay(2000)
|
||||
|
||||
const title = await waitUntilShowUp(screens.executeMethod.title)
|
||||
assert.notEqual(title, false, 'title isn\'t displayed')
|
||||
assert.equal(await title.getText(), screens.executeMethod.titleText, "'Execute Method' screen isn't opened")
|
||||
})
|
||||
|
||||
it("Return back to 'Choose Contract Executor' screen", async function () {
|
||||
const button = await waitUntilShowUp(screens.executeMethod.buttonNext)
|
||||
assert.notEqual(button, false, "button 'Next' isn't displayed")
|
||||
await button.click()
|
||||
})
|
||||
|
||||
it("Button 'Next' is disabled by default", async function () {
|
||||
const button = await waitUntilShowUp(screens.chooseContractExecutor.buttonNext)
|
||||
assert.notEqual(button, false, 'button isn\'t displayed')
|
||||
assert.equal(await button.isEnabled(), false, 'button enabled by default')
|
||||
})
|
||||
|
||||
it('User is able to select account', async function () {
|
||||
await waitUntilShowUp(screens.chooseContractExecutor.account)
|
||||
const accounts = await driver.findElements(screens.chooseContractExecutor.account)
|
||||
const account = accounts[1]
|
||||
await account.click()
|
||||
const selected = await driver.findElements(screens.chooseContractExecutor.selectedAccount)
|
||||
assert.equal(selected.length, 1, "account isn't selected")
|
||||
})
|
||||
|
||||
it('User is able to select only one account', async function () {
|
||||
const account = (await driver.findElements(screens.chooseContractExecutor.account))[2]
|
||||
await account.click()
|
||||
const selected = await driver.findElements(screens.chooseContractExecutor.selectedAccount)
|
||||
assert.equal(selected.length, 1, 'more than one accounts are selected')
|
||||
})
|
||||
|
||||
it("Click button 'Next' open 'Confirm transaction' screen", async function () {
|
||||
const button = await waitUntilShowUp(screens.chooseContractExecutor.buttonNext)
|
||||
await button.click()
|
||||
await delay(5000)
|
||||
const reject = await waitUntilShowUp(screens.confirmTransaction.button.reject)
|
||||
assert.notEqual(reject, false, "button reject isn't displayed")
|
||||
await click(reject)
|
||||
const identicon = await waitUntilShowUp(screens.main.identicon)
|
||||
assert.notEqual(identicon, false, 'main screen didn\'t opened')
|
||||
})
|
||||
it("Label 'CONTRACT' present", async function () {
|
||||
const menu = await waitUntilShowUp(menus.account.menu)
|
||||
await menu.click()
|
||||
await waitUntilShowUp(menus.account.labelImported)
|
||||
const label = (await driver.findElements(menus.account.labelImported))[0]
|
||||
assert.equal(await label.getText(), 'CONTRACT', 'label incorrect')
|
||||
})
|
||||
it('Delete imported account', async function () {
|
||||
const item = await waitUntilShowUp(menus.account.delete)
|
||||
await item.click()
|
||||
const button = await waitUntilShowUp(screens.deleteImportedAccount.buttons.yes)
|
||||
await button.click()
|
||||
const buttonArrow = await waitUntilShowUp(screens.settings.buttons.arrow)
|
||||
await buttonArrow.click()
|
||||
const identicon = await waitUntilShowUp(screens.main.identicon)
|
||||
assert.notEqual(identicon, false, 'main screen didn\'t opened')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Sign Data', () => {
|
||||
|
||||
it('simulate sign request ', async function () {
|
||||
await setProvider(NETWORKS.LOCALHOST)
|
||||
await driver.get('https://danfinlay.github.io/js-eth-personal-sign-examples/')
|
||||
const button = await waitUntilShowUp(By.id('ethSignButton'))
|
||||
await button.click()
|
||||
|
@ -305,6 +632,7 @@ describe('Metamask popup page', async function () {
|
|||
|
||||
it('opens import account menu', async function () {
|
||||
await setProvider(NETWORKS.POA)
|
||||
await delay(2000)
|
||||
const menu = await waitUntilShowUp(menus.account.menu)
|
||||
await menu.click()
|
||||
const item = await waitUntilShowUp(menus.account.import)
|
||||
|
@ -314,7 +642,6 @@ describe('Metamask popup page', async function () {
|
|||
})
|
||||
|
||||
it('imports account', async function () {
|
||||
await delay(2000)
|
||||
const privateKeyBox = await waitUntilShowUp(screens.importAccounts.fieldPrivateKey)
|
||||
await privateKeyBox.sendKeys('76bd0ced0a47055bb5d060e1ae4a8cb3ece658d668823e250dae6e79d3ab4435')// 0xf4702CbA917260b2D6731Aea6385215073e8551b
|
||||
const button = await waitUntilShowUp(screens.importAccounts.buttonImport)
|
||||
|
@ -322,9 +649,9 @@ describe('Metamask popup page', async function () {
|
|||
assert.equal(await button.getText(), 'Import', 'button has incorrect name')
|
||||
const menu = await waitUntilShowUp(menus.account.menu)
|
||||
await menu.click()
|
||||
const importedLabel = await waitUntilShowUp(menus.account.labelImported)
|
||||
assert.equal(await importedLabel.getText(), 'IMPORTED')
|
||||
|
||||
await waitUntilShowUp(menus.account.labelImported)
|
||||
const label = (await driver.findElements(menus.account.labelImported))[0]
|
||||
assert.equal(await label.getText(), 'IMPORTED')
|
||||
await menu.click()
|
||||
})
|
||||
|
||||
|
@ -461,6 +788,7 @@ describe('Metamask popup page', async function () {
|
|||
await driver.navigate().refresh()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Import Ganache seed phrase', function () {
|
||||
|
||||
it('logs out', async function () {
|
||||
|
@ -544,7 +872,7 @@ describe('Metamask popup page', async function () {
|
|||
const balance = await balanceField.getText()
|
||||
console.log('Account = ' + account)
|
||||
console.log('Balance = ' + balance)
|
||||
assert.equal(parseInt(balance) > 0.001, true, 'Balance of account ' + account + ' TOO LOW !!! Please refill with Sokol eth!!!!')
|
||||
assert.equal(parseFloat(balance) > 0.001, true, 'Balance of account ' + account + ' TOO LOW !!! Please refill with Sokol eth!!!!')
|
||||
await driver.get(eventsEmitter)
|
||||
const button = await waitUntilShowUp(screens.eventsEmitter.button)
|
||||
await button.click()
|
||||
|
@ -565,7 +893,7 @@ describe('Metamask popup page', async function () {
|
|||
const windowHandles = await driver.getAllWindowHandles()
|
||||
await driver.switchTo().window(windowHandles[0])
|
||||
await delay(5000)
|
||||
const event = await waitUntilShowUp(screens.eventsEmitter.event, 1200)
|
||||
const event = await waitUntilShowUp(screens.eventsEmitter.event, 600)
|
||||
const events = await driver.findElements(screens.eventsEmitter.event)
|
||||
console.log('number of events = ' + events.length)
|
||||
if (!event) console.log("event wasn't created or transaction failed".toUpperCase())
|
||||
|
@ -590,7 +918,7 @@ describe('Metamask popup page', async function () {
|
|||
|
||||
describe('Add Token: Custom', function () {
|
||||
|
||||
describe('Add token to LOCALHOST', function () {
|
||||
describe('Add token to LOCALHOST', function () {
|
||||
|
||||
it('Create custom token in LOCALHOST', async function () {
|
||||
await setProvider(NETWORKS.LOCALHOST)
|
||||
|
@ -656,8 +984,8 @@ describe('Metamask popup page', async function () {
|
|||
console.log('allHandles.length ' + allHandles.length)
|
||||
assert.equal(allHandles.length, 2, 'etherscan wasn\'t opened')
|
||||
await switchToLastPage()
|
||||
await delay(2000)
|
||||
const title = await waitUntilCurrentUrl()
|
||||
|
||||
console.log(title)
|
||||
assert.equal(title.includes('https://etherscan.io/token/'), true, 'etherscan wasn\'t opened')
|
||||
await switchToFirstPage()
|
||||
|
@ -1762,6 +2090,17 @@ describe('Metamask popup page', async function () {
|
|||
return false
|
||||
}
|
||||
|
||||
async function waitUntilHasText (element, Twait) {
|
||||
if (Twait === undefined) Twait = 200
|
||||
let text
|
||||
do {
|
||||
await delay(100)
|
||||
text = await element.getText()
|
||||
if (text !== '') return text
|
||||
} while (Twait-- > 0)
|
||||
return false
|
||||
}
|
||||
|
||||
async function isElementDisplayed (by) {
|
||||
try {
|
||||
return await driver.findElement(by).isDisplayed()
|
||||
|
@ -2226,3 +2565,5 @@ describe('Metamask popup page', async function () {
|
|||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import ChooseContractExecutor from '../../../../../../old-ui/app/components/send/choose-contract-executor'
|
||||
import { mount } from 'enzyme'
|
||||
import thunk from 'redux-thunk'
|
||||
import configureMockStore from 'redux-mock-store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
describe('ChooseContractExecutor component', () => {
|
||||
describe('render() function', () => {
|
||||
const state = {
|
||||
metamask: {
|
||||
selectedAddress: '0x99a22ce737b6a48f44cad6331432ce98693cad07',
|
||||
accounts: ['0x99a22ce737b6a48f44cad6331432ce98693cad07'],
|
||||
keyrings: [
|
||||
{
|
||||
'type': 'HD Key Tree',
|
||||
'accounts': [
|
||||
'0x99a22ce737b6a48f44cad6331432ce98693cad07',
|
||||
],
|
||||
},
|
||||
],
|
||||
identities: {
|
||||
'0x99a22ce737b6a48f44cad6331432ce98693cad07': {
|
||||
name: 'Account 1',
|
||||
address: '0x99a22ce737b6a48f44cad6331432ce98693cad07',
|
||||
},
|
||||
},
|
||||
txParams: {},
|
||||
methodSelected: '',
|
||||
methodABI: [],
|
||||
inputValues: [],
|
||||
},
|
||||
appState: {},
|
||||
}
|
||||
|
||||
const middlewares = [thunk]
|
||||
const mockStore = configureMockStore(middlewares)
|
||||
const store = mockStore(state)
|
||||
let wrapper
|
||||
|
||||
beforeEach(function () {
|
||||
wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<ChooseContractExecutor
|
||||
methodSelected={false}
|
||||
methodABI={[]}
|
||||
inputValues={[]}
|
||||
txParams={{}}
|
||||
/>
|
||||
</Provider>
|
||||
)
|
||||
})
|
||||
|
||||
it('shows correct title', () => {
|
||||
assert.equal(wrapper.find('.send-header').text(), 'Choose contract executor')
|
||||
})
|
||||
|
||||
it('shows correct profile', () => {
|
||||
assert.equal(wrapper.find('.send-profile-identity-name').text(), 'Account 1')
|
||||
assert.equal(wrapper.find('.send-profile-address').text(), '0x99a22cE7...Ad07')
|
||||
})
|
||||
|
||||
it('doesn\'t show error', () => {
|
||||
assert.equal(wrapper.find('.error').exists(), false)
|
||||
})
|
||||
|
||||
it('shows correct description', () => {
|
||||
assert.equal(wrapper.find('.hw-connect__header__msg').text(), 'Contract transaction will be executed from selected account')
|
||||
})
|
||||
|
||||
it('shows Next button', () => {
|
||||
assert.equal(wrapper.find('.choose-contract-next-button').text(), 'Next')
|
||||
})
|
||||
|
||||
it('the number of accounts is equal to number of keyrings', () => {
|
||||
assert.equal(wrapper.find('.executor-cell-container').length, state.metamask.keyrings.length)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import ExecutorCell from '../../../../../../old-ui/app/components/send/executor-cell'
|
||||
import Identicon from '../../../../../../old-ui/app/components/identicon'
|
||||
import { mount } from 'enzyme'
|
||||
import thunk from 'redux-thunk'
|
||||
import configureMockStore from 'redux-mock-store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
describe('ExecutorCell component', () => {
|
||||
describe('render() function', () => {
|
||||
const state = {
|
||||
metamask: {},
|
||||
appState: {},
|
||||
}
|
||||
|
||||
const middlewares = [thunk]
|
||||
const mockStore = configureMockStore(middlewares)
|
||||
const store = mockStore(state)
|
||||
let wrapper
|
||||
|
||||
beforeEach(function () {
|
||||
wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<ExecutorCell
|
||||
isAccountSelected={false}
|
||||
address={'0x99a22ce737b6a48f44cad6331432ce98693cad07'}
|
||||
identity={{
|
||||
name: 'Account 1',
|
||||
address: '0x99a22ce737b6a48f44cad6331432ce98693cad07',
|
||||
}}
|
||||
onClick={() => {}}
|
||||
/>
|
||||
</Provider>
|
||||
)
|
||||
})
|
||||
|
||||
it('renders Identicon', () => {
|
||||
assert.equal(wrapper.find(Identicon).prop('address'), '0x99a22ce737b6a48f44cad6331432ce98693cad07')
|
||||
assert.equal(wrapper.find(Identicon).prop('network'), undefined)
|
||||
})
|
||||
|
||||
it('renders correct identity name', () => {
|
||||
assert.equal(wrapper.find('.font-medium.flex-center').text(), 'Account 1')
|
||||
})
|
||||
|
||||
it('renders correct compressed address', () => {
|
||||
assert.equal(wrapper.find('.flex-row.flex-center').text(), '0x99a22cE7...Ad07')
|
||||
})
|
||||
|
||||
it('changes class on click', () => {
|
||||
assert.equal(wrapper.find('.executor-cell-container').exists(), true)
|
||||
assert.equal(wrapper.find('.executor-cell-container-selected').exists(), false)
|
||||
wrapper.find('.executor-cell-container').simulate('click')
|
||||
assert.equal(wrapper.find('.executor-cell-container').exists(), false)
|
||||
assert.equal(wrapper.find('.executor-cell-container-selected').exists(), true)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import SendContractError from '../../../../../../old-ui/app/components/send/send-error'
|
||||
import { mount } from 'enzyme'
|
||||
import thunk from 'redux-thunk'
|
||||
import configureMockStore from 'redux-mock-store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
const state = {
|
||||
metamask: {},
|
||||
appState: {},
|
||||
}
|
||||
|
||||
const middlewares = [thunk]
|
||||
const mockStore = configureMockStore(middlewares)
|
||||
const store = mockStore(state)
|
||||
let wrapper
|
||||
|
||||
describe('SendContractError component', () => {
|
||||
describe('renders SendContractError component', () => {
|
||||
beforeEach(function () {
|
||||
wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<SendContractError error="Error!"/>
|
||||
</Provider>
|
||||
)
|
||||
})
|
||||
it('shows error', () => {
|
||||
assert.equal(wrapper.find('.error').text(), 'Error!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('doesn\'t render SendContractError component', () => {
|
||||
beforeEach(function () {
|
||||
wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<SendContractError/>
|
||||
</Provider>
|
||||
)
|
||||
})
|
||||
|
||||
it('doesn\'t show error', () => {
|
||||
assert.equal(wrapper.find('.error').isEmpty(), true)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import SendHeader from '../../../../../../old-ui/app/components/send/send-header'
|
||||
import { mount } from 'enzyme'
|
||||
import thunk from 'redux-thunk'
|
||||
import configureMockStore from 'redux-mock-store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
describe('SendHeader component', () => {
|
||||
describe('render() function', () => {
|
||||
const state = {
|
||||
metamask: {},
|
||||
appState: {},
|
||||
}
|
||||
|
||||
const middlewares = [thunk]
|
||||
const mockStore = configureMockStore(middlewares)
|
||||
const store = mockStore(state)
|
||||
let wrapper
|
||||
|
||||
beforeEach(function () {
|
||||
wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<SendHeader
|
||||
title={'Execute Method'}
|
||||
/>
|
||||
</Provider>
|
||||
)
|
||||
})
|
||||
it('renders correct title', () => {
|
||||
assert.equal(wrapper.find('.send-header').text(), 'Execute Method')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,53 @@
|
|||
import React from 'react'
|
||||
import assert from 'assert'
|
||||
import SendProfile from '../../../../../../old-ui/app/components/send/send-profile'
|
||||
import Identicon from '../../../../../../old-ui/app/components/identicon'
|
||||
import { mount } from 'enzyme'
|
||||
import thunk from 'redux-thunk'
|
||||
import configureMockStore from 'redux-mock-store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
const state = {
|
||||
metamask: {
|
||||
selectedAddress: '0x99a22ce737b6a48f44cad6331432ce98693cad07',
|
||||
accounts: ['0x99a22ce737b6a48f44cad6331432ce98693cad07'],
|
||||
identities: {
|
||||
'0x99a22ce737b6a48f44cad6331432ce98693cad07': {
|
||||
name: 'Account 1',
|
||||
address: '0x99a22ce737b6a48f44cad6331432ce98693cad07',
|
||||
},
|
||||
},
|
||||
},
|
||||
appState: {},
|
||||
}
|
||||
|
||||
const middlewares = [thunk]
|
||||
const mockStore = configureMockStore(middlewares)
|
||||
const store = mockStore(state)
|
||||
let wrapper
|
||||
|
||||
describe('SendProfile component', () => {
|
||||
describe('renders SendProfile component', () => {
|
||||
beforeEach(function () {
|
||||
wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<SendProfile/>
|
||||
</Provider>
|
||||
)
|
||||
})
|
||||
it('shows identity name', () => {
|
||||
assert.equal(wrapper.find('.send-profile-identity-name').text(), 'Account 1')
|
||||
})
|
||||
|
||||
it('shows identicon', () => {
|
||||
assert.equal(wrapper.find(Identicon).prop('address'), '0x99a22ce737b6a48f44cad6331432ce98693cad07')
|
||||
assert.equal(wrapper.find(Identicon).prop('network'), undefined)
|
||||
})
|
||||
|
||||
it('shows address', () => {
|
||||
assert.equal(wrapper.find('.send-profile-address').text(), '0x99a22cE7...Ad07')
|
||||
})
|
||||
|
||||
// todo: add check for Balance
|
||||
})
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
const assert = require('assert')
|
||||
const { countSignificantDecimals } = require('../../../../old-ui/app/util')
|
||||
const { countSignificantDecimals, getCurrentKeyring, ifLooseAcc, ifContractAcc } = require('../../../../old-ui/app/util')
|
||||
|
||||
describe('countSignificantDecimals(val, len) function', () => {
|
||||
it('returns correct significant decimals', () => {
|
||||
|
@ -10,3 +10,62 @@ describe('countSignificantDecimals(val, len) function', () => {
|
|||
assert.equal(3, countSignificantDecimals('2.03243', 2))
|
||||
})
|
||||
})
|
||||
|
||||
const keyrings = [
|
||||
{
|
||||
'type': 'HD Key Tree',
|
||||
'accounts': [
|
||||
'0xb55e278d3e8ff77ec95749b51b526a236502b6fe',
|
||||
'0x99a22ce737b6a48f44cad6331432ce98693cad07',
|
||||
'0xa37bd195eebfc4ccd02529125b3e691fb6fe3a53',
|
||||
],
|
||||
},
|
||||
{
|
||||
'type': 'Simple Address',
|
||||
'accounts': [
|
||||
'0x2aE8025dECA9d0d0f985eC6666174FdF6546CC85',
|
||||
],
|
||||
'network': '1',
|
||||
},
|
||||
]
|
||||
|
||||
describe('getCurrentKeyring(address, keyrings, network, identities) function', () => {
|
||||
const address = '0xb55e278d3e8ff77ec95749b51b526a236502b6fe'
|
||||
const identities = {
|
||||
'0x99a22ce737b6a48f44cad6331432ce98693cad07': {name: 'Account 1', address: '0x99a22ce737b6a48f44cad6331432ce98693cad07'},
|
||||
'0xb55e278d3e8ff77ec95749b51b526a236502b6fe': {name: 'Account 2', address: '0xb55e278d3e8ff77ec95749b51b526a236502b6fe'},
|
||||
'0xa37bd195eebfc4ccd02529125b3e691fb6fe3a53': {name: 'Account 3', address: '0xa37bd195eebfc4ccd02529125b3e691fb6fe3a53'},
|
||||
}
|
||||
it('returns keyring matched to address', () => {
|
||||
assert.deepEqual({
|
||||
'type': 'HD Key Tree',
|
||||
'accounts': [
|
||||
'0xb55e278d3e8ff77ec95749b51b526a236502b6fe',
|
||||
'0x99a22ce737b6a48f44cad6331432ce98693cad07',
|
||||
'0xa37bd195eebfc4ccd02529125b3e691fb6fe3a53',
|
||||
],
|
||||
}, getCurrentKeyring(address, 1, keyrings, identities))
|
||||
})
|
||||
|
||||
it('doesn\'t return keyring matched to address', () => {
|
||||
assert.deepEqual(null, getCurrentKeyring('0x9053a0Fe25fc45367d06B2e04528BDb4c1A03eaB', 1, keyrings, identities))
|
||||
})
|
||||
})
|
||||
|
||||
describe('ifLooseAcc(keyring) function', () => {
|
||||
it('Checks if keyring is looseAcc', () => {
|
||||
assert.equal(false, ifLooseAcc(keyrings[0]))
|
||||
assert.equal(true, ifLooseAcc(keyrings[1]))
|
||||
assert.equal(null, ifLooseAcc())
|
||||
assert.equal(true, ifLooseAcc({}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('ifContractAcc(keyring) function', () => {
|
||||
it('Checks if keyring is contract', () => {
|
||||
assert.equal(false, ifContractAcc(keyrings[0]))
|
||||
assert.equal(true, ifContractAcc(keyrings[1]))
|
||||
assert.equal(null, ifContractAcc())
|
||||
assert.equal(false, ifContractAcc({}))
|
||||
})
|
||||
})
|
||||
|
|
|
@ -471,7 +471,7 @@ describe('Actions', () => {
|
|||
|
||||
removeAccountSpy = sinon.spy(background, 'removeAccount')
|
||||
|
||||
return store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc'))
|
||||
return store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc', '1'))
|
||||
.then(() => {
|
||||
assert(removeAccountSpy.calledOnce)
|
||||
assert.deepEqual(store.getActions(), expectedActions)
|
||||
|
@ -486,11 +486,11 @@ describe('Actions', () => {
|
|||
{ type: 'DISPLAY_WARNING', value: 'error' },
|
||||
]
|
||||
removeAccountSpy = sinon.stub(background, 'removeAccount')
|
||||
removeAccountSpy.callsFake((address, callback) => {
|
||||
removeAccountSpy.callsFake((address, network, callback) => {
|
||||
callback(new Error('error'))
|
||||
})
|
||||
|
||||
return store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc'))
|
||||
return store.dispatch(actions.removeAccount('0xe18035bf8712672935fdb4e5e431b1a0183d2dfc', '1'))
|
||||
.catch(() => {
|
||||
assert.deepEqual(store.getActions(), expectedActions)
|
||||
})
|
||||
|
|
|
@ -98,6 +98,7 @@ var actions = {
|
|||
navigateToNewAccountScreen,
|
||||
resetAccount,
|
||||
changePassword,
|
||||
getContract,
|
||||
removeAccount,
|
||||
showNewVaultSeed: showNewVaultSeed,
|
||||
showInfoPage: showInfoPage,
|
||||
|
@ -118,7 +119,11 @@ var actions = {
|
|||
lockMetamask: lockMetamask,
|
||||
unlockInProgress: unlockInProgress,
|
||||
// error handling
|
||||
displayToast: displayToast,
|
||||
displayWarning: displayWarning,
|
||||
DISPLAY_TOAST: 'DISPLAY_TOAST',
|
||||
HIDE_TOAST: 'HIDE_TOAST',
|
||||
hideToast: hideToast,
|
||||
DISPLAY_WARNING: 'DISPLAY_WARNING',
|
||||
HIDE_WARNING: 'HIDE_WARNING',
|
||||
hideWarning: hideWarning,
|
||||
|
@ -139,6 +144,8 @@ var actions = {
|
|||
showSendPage: showSendPage,
|
||||
SHOW_SEND_TOKEN_PAGE: 'SHOW_SEND_TOKEN_PAGE',
|
||||
showSendTokenPage,
|
||||
SHOW_SEND_CONTRACT_PAGE: 'SHOW_SEND_CONTRACT_PAGE',
|
||||
showSendContractPage,
|
||||
ADD_TO_ADDRESS_BOOK: 'ADD_TO_ADDRESS_BOOK',
|
||||
addToAddressBook: addToAddressBook,
|
||||
REQUEST_ACCOUNT_EXPORT: 'REQUEST_ACCOUNT_EXPORT',
|
||||
|
@ -217,6 +224,8 @@ var actions = {
|
|||
setSelectedAddress,
|
||||
gasLoadingStarted,
|
||||
gasLoadingFinished,
|
||||
SHOW_CHOOSE_CONTRACT_EXECUTOR_PAGE: 'SHOW_CHOOSE_CONTRACT_EXECUTOR_PAGE',
|
||||
showChooseContractExecutorPage,
|
||||
// app messages
|
||||
confirmSeedWords: confirmSeedWords,
|
||||
showAccountDetail: showAccountDetail,
|
||||
|
@ -615,12 +624,27 @@ function changePassword (oldPassword, newPassword) {
|
|||
}
|
||||
}
|
||||
|
||||
function removeAccount (address) {
|
||||
function getContract (address) {
|
||||
return dispatch => {
|
||||
return new Promise((resolve, reject) => {
|
||||
background.getContract(address, (err, props) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
return reject(err)
|
||||
}
|
||||
log.info('Contract retrieved: ' + JSON.stringify(props))
|
||||
resolve(props)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function removeAccount (address, network) {
|
||||
return dispatch => {
|
||||
dispatch(actions.showLoadingIndication())
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
background.removeAccount(address, (err, account) => {
|
||||
background.removeAccount(address, network, (err, account) => {
|
||||
dispatch(actions.hideLoadingIndication())
|
||||
if (err) {
|
||||
dispatch(actions.displayWarning(err.message))
|
||||
|
@ -1017,6 +1041,16 @@ function gasLoadingFinished () {
|
|||
}
|
||||
}
|
||||
|
||||
function showChooseContractExecutorPage ({methodSelected, methodABI, inputValues, txParams}) {
|
||||
return {
|
||||
type: actions.SHOW_CHOOSE_CONTRACT_EXECUTOR_PAGE,
|
||||
methodSelected,
|
||||
methodABI,
|
||||
inputValues,
|
||||
txParams,
|
||||
}
|
||||
}
|
||||
|
||||
function updateSendTokenBalance ({
|
||||
selectedToken,
|
||||
tokenContract,
|
||||
|
@ -2112,6 +2146,19 @@ function hideWarning () {
|
|||
}
|
||||
}
|
||||
|
||||
function displayToast (text) {
|
||||
return {
|
||||
type: actions.DISPLAY_TOAST,
|
||||
value: text,
|
||||
}
|
||||
}
|
||||
|
||||
function hideToast () {
|
||||
return {
|
||||
type: actions.HIDE_TOAST,
|
||||
}
|
||||
}
|
||||
|
||||
function requestExportAccount () {
|
||||
return {
|
||||
type: actions.REQUEST_ACCOUNT_EXPORT,
|
||||
|
@ -2205,6 +2252,15 @@ function showSendTokenPage (address) {
|
|||
}
|
||||
}
|
||||
|
||||
function showSendContractPage ({methodSelected, methodABI, inputValues}) {
|
||||
return {
|
||||
type: actions.SHOW_SEND_CONTRACT_PAGE,
|
||||
methodSelected,
|
||||
methodABI,
|
||||
inputValues,
|
||||
}
|
||||
}
|
||||
|
||||
function buyEth (opts) {
|
||||
return (dispatch) => {
|
||||
const url = getBuyEthUrl(opts)
|
||||
|
|
|
@ -294,7 +294,7 @@ function reduceApp (state, action) {
|
|||
transForward: true,
|
||||
})
|
||||
|
||||
case actions.CREATE_NEW_VAULT_IN_PROGRESS:
|
||||
case actions.CREATE_NEW_VAULT_IN_PROGRESS:
|
||||
return extend(appState, {
|
||||
currentView: {
|
||||
name: 'createVault',
|
||||
|
@ -344,6 +344,21 @@ function reduceApp (state, action) {
|
|||
warning: null,
|
||||
})
|
||||
|
||||
case actions.SHOW_SEND_CONTRACT_PAGE:
|
||||
return extend(appState, {
|
||||
currentView: {
|
||||
name: 'sendContract',
|
||||
context: appState.currentView.context,
|
||||
},
|
||||
transForward: true,
|
||||
warning: null,
|
||||
contractAcc: {
|
||||
methodSelected: action.methodSelected,
|
||||
methodABI: action.methodABI,
|
||||
inputValues: action.inputValues,
|
||||
},
|
||||
})
|
||||
|
||||
case actions.SHOW_NEW_KEYCHAIN:
|
||||
return extend(appState, {
|
||||
currentView: {
|
||||
|
@ -621,6 +636,17 @@ function reduceApp (state, action) {
|
|||
},
|
||||
})
|
||||
|
||||
case actions.DISPLAY_TOAST:
|
||||
return extend(appState, {
|
||||
toastMsg: action.value,
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
case actions.HIDE_TOAST:
|
||||
return extend(appState, {
|
||||
toastMsg: undefined,
|
||||
})
|
||||
|
||||
case actions.DISPLAY_WARNING:
|
||||
return extend(appState, {
|
||||
warning: action.value,
|
||||
|
@ -806,6 +832,20 @@ function reduceApp (state, action) {
|
|||
networkNonce: action.value,
|
||||
})
|
||||
|
||||
case actions.SHOW_CHOOSE_CONTRACT_EXECUTOR_PAGE:
|
||||
return extend(appState, {
|
||||
currentView: {
|
||||
name: 'show-choose-contract-executor-page',
|
||||
context: appState.currentView.context,
|
||||
},
|
||||
txParams: action.txParams,
|
||||
contractAcc: {
|
||||
methodSelected: action.methodSelected,
|
||||
methodABI: action.methodABI,
|
||||
inputValues: action.inputValues,
|
||||
},
|
||||
})
|
||||
|
||||
default:
|
||||
return appState
|
||||
}
|
||||
|
|
|
@ -68,5 +68,9 @@ SelectedApp.prototype.render = function () {
|
|||
}, [
|
||||
h(I18nProvider, [ h(App) ]),
|
||||
])
|
||||
: h(I18nProvider, [ h(OldApp) ])
|
||||
: h(HashRouter, {
|
||||
hashType: 'noslash',
|
||||
}, [
|
||||
h(I18nProvider, [ h(OldApp) ]),
|
||||
])
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue