Merge pull request #212 from poanetwork/import-multisig

(Feature) Import contract
This commit is contained in:
Victor Baranov 2018-12-12 01:00:02 +03:00 committed by GitHub
commit a8fcea31f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 2600 additions and 646 deletions

View File

@ -179,7 +179,7 @@ jobs:
key: dependency-cache-{{ .Revision }}
- run:
name: Test
command: npx nsp check
command: npm audit
test-e2e-chrome:
docker:

View File

@ -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

View File

@ -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__",

View File

@ -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()

View File

@ -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'),
]),

View File

@ -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)

View File

@ -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)
}

View File

@ -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'})

View File

@ -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 = {

View File

@ -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

View File

@ -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,

View File

@ -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())
})

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -237,6 +237,7 @@ hr.horizontal-line {
padding: 4px;
position: absolute;
right: 20px;
width: 68px;
}
.ether-balance {

View File

@ -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;
}

View File

@ -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 }
}

View File

@ -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'),

695
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
},

View File

@ -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',
},
}

View File

@ -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 () {
}
})

View File

@ -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)
})
})
})

View File

@ -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)
})
})
})

View File

@ -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)
})
})
})

View File

@ -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')
})
})
})

View File

@ -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
})
})

View File

@ -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({}))
})
})

View File

@ -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)
})

View File

@ -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)

View File

@ -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
}

View File

@ -68,5 +68,9 @@ SelectedApp.prototype.render = function () {
}, [
h(I18nProvider, [ h(App) ]),
])
: h(I18nProvider, [ h(OldApp) ])
: h(HashRouter, {
hashType: 'noslash',
}, [
h(I18nProvider, [ h(OldApp) ]),
])
}