From df2286abdd1fb381fc74758f2fc336e31409ea5a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 28 Nov 2018 20:19:20 +0300 Subject: [PATCH 01/41] Import multisig account option --- app/scripts/metamask-controller.js | 12 +- old-ui/app/account-detail.js | 12 +- old-ui/app/accounts/import/index.js | 4 + old-ui/app/accounts/import/multisig.js | 69 ++++++++ old-ui/app/app.js | 11 +- old-ui/app/components/account-dropdowns.js | 39 ++--- old-ui/app/components/send/send-error.js | 21 +++ old-ui/app/components/send/send-header.js | 49 ++++++ old-ui/app/components/send/send-multisig.js | 103 ++++++++++++ old-ui/app/components/send/send-profile.js | 94 +++++++++++ .../app/{ => components/send}/send-token.js | 146 ++++------------- old-ui/app/{ => components/send}/send.js | 152 +++--------------- old-ui/app/util.js | 55 +++++++ package-lock.json | 46 +----- package.json | 2 +- ui/app/actions.js | 9 ++ ui/app/reducers/app.js | 10 ++ 17 files changed, 514 insertions(+), 320 deletions(-) create mode 100644 old-ui/app/accounts/import/multisig.js create mode 100644 old-ui/app/components/send/send-error.js create mode 100644 old-ui/app/components/send/send-header.js create mode 100644 old-ui/app/components/send/send-multisig.js create mode 100644 old-ui/app/components/send/send-profile.js rename old-ui/app/{ => components/send}/send-token.js (70%) rename old-ui/app/{ => components/send}/send.js (57%) diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 9ac972b76..022ef3236 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -407,6 +407,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 +559,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) } @@ -840,8 +841,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 === 'Multisig') { + 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() diff --git a/old-ui/app/account-detail.js b/old-ui/app/account-detail.js index ecbdc1849..fd05bd4a0 100644 --- a/old-ui/app/account-detail.js +++ b/old-ui/app/account-detail.js @@ -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, ifMultisigAcc, 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, 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 (ifMultisigAcc(currentKeyring)) { + return props.dispatch(actions.showSendMultisigPage()) + } else { + return props.dispatch(actions.showSendPage()) + } + }, }, 'Send'), ]), diff --git a/old-ui/app/accounts/import/index.js b/old-ui/app/accounts/import/index.js index d06e2dd84..9cbed06f7 100644 --- a/old-ui/app/accounts/import/index.js +++ b/old-ui/app/accounts/import/index.js @@ -8,10 +8,12 @@ import Select from 'react-select' // Subviews const JsonImportView = require('./json.js') const PrivateKeyImportView = require('./private-key.js') +const MultisugImportView = require('./multisig.js') const menuItems = [ 'Private Key', 'JSON File', + 'Multisig', ] module.exports = connect(mapStateToProps)(AccountImportSubview) @@ -127,6 +129,8 @@ AccountImportSubview.prototype.renderImportView = function () { return h(PrivateKeyImportView) case 'JSON File': return h(JsonImportView) + case 'Multisig': + return h(MultisugImportView) default: return h(JsonImportView) } diff --git a/old-ui/app/accounts/import/multisig.js b/old-ui/app/accounts/import/multisig.js new file mode 100644 index 000000000..eacb98dcd --- /dev/null +++ b/old-ui/app/accounts/import/multisig.js @@ -0,0 +1,69 @@ +const inherits = require('util').inherits +const Component = require('react').Component +const h = require('react-hyperscript') +const connect = require('react-redux').connect +const actions = require('../../../../ui/app/actions') + +module.exports = connect(mapStateToProps)(MultisigImportView) + +function mapStateToProps (state) { + return { + error: state.appState.warning, + } +} + +inherits(MultisigImportView, Component) +function MultisigImportView () { + Component.call(this) +} + +MultisigImportView.prototype.render = function () { + const { error } = this.props + + return ( + h('div', { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: '5px 0px 0px 0px', + }, + }, [ + h('span', 'Paste address of multisig here'), + + h('input.large-input', { + id: 'address-box', + onKeyPress: this.createKeyringOnEnter.bind(this), + style: { + width: '100%', + marginTop: 12, + border: '1px solid #e2e2e2', + }, + }), + + h('button', { + onClick: this.createNewKeychain.bind(this), + style: { + margin: 20, + }, + }, 'Import'), + + error ? h('span.error', error) : null, + ]) + ) +} + +MultisigImportView.prototype.createKeyringOnEnter = function (event) { + if (event.key === 'Enter') { + event.preventDefault() + this.createNewKeychain() + } +} + +MultisigImportView.prototype.createNewKeychain = function () { + const input = document.getElementById('address-box') + const addr = input.value + this.props.dispatch(actions.importNewAccount('Multisig', [ addr ])) + // JS runtime requires caught rejections but failures are handled by Redux + .catch() +} diff --git a/old-ui/app/app.js b/old-ui/app/app.js index f5adf89ba..90ac7acd5 100644 --- a/old-ui/app/app.js +++ b/old-ui/app/app.js @@ -15,8 +15,9 @@ 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 SendMultisigScreen = require('./components/send/send-multisig') const ConfirmTxScreen = require('./conf-tx') // notice const NoticeScreen = require('./components/notice') @@ -231,9 +232,13 @@ 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 'sendMultisig': + log.debug('rendering send multisig tx screen') + return h(SendMultisigScreen, {key: 'send-multisig'}) + case 'newKeychain': log.debug('rendering new keychain screen') return h(NewKeyChainScreen, {key: 'new-keychain'}) diff --git a/old-ui/app/components/account-dropdowns.js b/old-ui/app/components/account-dropdowns.js index 88d1f16e6..79472ab0c 100644 --- a/old-ui/app/components/account-dropdowns.js +++ b/old-ui/app/components/account-dropdowns.js @@ -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, ifMultisigAcc } = require('../util') class AccountDropdowns extends Component { constructor (props) { @@ -32,7 +33,7 @@ class AccountDropdowns extends Component { } const isSelected = identity.address === selected - const keyring = this.getCurrentKeyring(address) + const keyring = getCurrentKeyring(address, keyrings, identities) return h( DropdownMenuItem, @@ -80,7 +81,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() @@ -96,36 +97,26 @@ class AccountDropdowns extends Component { }) } - 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.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 (ifMultisigAcc(keyring)) { + label = 'MULTISIG' + } else { + label = 'IMPORTED' + } + return h('.keyring-label', label) + } + + return null } renderAccountSelector () { diff --git a/old-ui/app/components/send/send-error.js b/old-ui/app/components/send/send-error.js new file mode 100644 index 000000000..21ce981b1 --- /dev/null +++ b/old-ui/app/components/send/send-error.js @@ -0,0 +1,21 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' + +class SendError extends Component { + static propTypes = { + error: PropTypes.string, + } + + render () { + return this.props.error ? ( +
+
{this.props.error}
+
+ ) : null + } +} + +module.exports = SendError diff --git a/old-ui/app/components/send/send-header.js b/old-ui/app/components/send/send-header.js new file mode 100644 index 000000000..eb715cae5 --- /dev/null +++ b/old-ui/app/components/send/send-header.js @@ -0,0 +1,49 @@ +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 = { + dispatch: PropTypes.func, + address: PropTypes.string, + title: PropTypes.string, + } + + render () { + return ( +

+ this.back()} + /> + { this.props.title } +

+ ) + } + + 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, null)(SendHeader) diff --git a/old-ui/app/components/send/send-multisig.js b/old-ui/app/components/send/send-multisig.js new file mode 100644 index 000000000..c88d49c42 --- /dev/null +++ b/old-ui/app/components/send/send-multisig.js @@ -0,0 +1,103 @@ +import React from 'react' +import { connect } from 'react-redux' +import PersistentForm from '../../../lib/persistent-form' +import { numericBalance } from '../../util' +import SendProfile from './send-profile' +import SendHeader from './send-header' +import SendError from './send-error' +import Select from 'react-select' + +class SendTransactionScreen extends PersistentForm { + constructor (props) { + super(props) + this.state = { + options: [], + methodSelected: '', + } + PersistentForm.call(this) + } + + componentWillMount () { + this.getContractMethods() + } + + render () { + this.persistentFormParentId = 'send-multisig-tx-form' + + const props = this.props + const { + address, + account, + identity, + network, + identities, + addressBook, + conversionRate, + currentCurrency, + error, + } = props + return ( +
+ + + +
+ this.props.onChange(e)} + style={{ marginTop: '5px' }} + /> + ) + } +} class SendTransactionScreen extends PersistentForm { constructor (props) { super(props) this.state = { options: [], + abi: [], methodSelected: '', + methodABI: {}, + methodInputs: null, + methodInputsView: null, + methodOutput: null, + isConstantMethod: false, + inputValues: {}, + output: '', } PersistentForm.call(this) } componentWillMount () { - this.getContractMethods() + this.getMultisigMethods() } render () { @@ -26,72 +59,222 @@ class SendTransactionScreen extends PersistentForm { const props = this.props const { - address, - account, - identity, - network, - identities, - addressBook, - conversionRate, - currentCurrency, error, } = props return (
- -
- { + this.setState({ + methodSelected: opt.value, + isConstantMethod: opt.metadata.constant, + methodABI: opt.metadata, + output: '', + }) + this.generateMethodInputsView(opt.metadata) + }} + /> +
+ {this.state.methodInputsView} +
+ {this.state.isConstantMethod && this.methodOutput()} + {this.buttonsSection()}
- ) + ) } - getContractMethods () { - console.log('###getContractMethods') - console.log(this.props) - const apiLink = `https://api.etherscan.io/api?module=contract&action=getabi&address=${this.props.address}&apikey=` - fetch(apiLink) - .then(res => res.json()) - .then(json => { - console.log(json) - const abiString = json && json.result - const abi = JSON.parse(abiString) - console.log(abi) - const options = abi.map((obj) => { - if (abi.type === 'function') { - return { label: abi.name, value: abi.name } - } - console.log(obj) - }) + async getMultisigMethods () { + const multisigProps = await this.props.getMultisig(this.props.address) + const abi = multisigProps && multisigProps.abi + const options = abi && abi.reduce((filtered, obj) => { + if (obj.type === 'function') { + filtered.push({ label: obj.name, value: obj.name, metadata: obj }) + } + return filtered + }, []) + this.setState({ + options, + abi, + }) + } + + generateMethodInput (params, ind) { + const label = ( +

+ {params.name || `Input ${ind + 1}`} +

+ ) + const inputKey = `method_input_${ind}` + const input = ( + this.handleInputChange(e, ind)} + /> + ) + const inputObj = ( +
+ {label} + {input} +
+ ) + return inputObj + } + + handleInputChange (e, ind) { + const { inputValues } = this.state + inputValues[ind] = e.target.value + this.setState({ + inputValues, + }) + } + + generateMethodInputsView (metadata) { + const methodInputsView = [] + console.log(metadata) + const methodInputs = metadata && metadata.inputs + methodInputs.forEach((input, ind) => { + methodInputsView.push(this.generateMethodInput(input, ind)) + }) + this.setState({ + methodInputs: methodInputs, + methodInputsView: methodInputsView, + }) + } + + methodOutput () { + const label = ( +

+ Output +

+ ) + const output = ( +