From b0f89becfde3a061da134a58a31ffc5894761bd7 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Fri, 18 Aug 2017 16:51:17 +0200 Subject: [PATCH] UI backports (#6332) * Time should not contribue to overall status. (#6276) * Add warning to web browser and fix links. (#6232) * Extension fixes (#6284) * Fix token symbols in extension. * Allow connections from firefox extension. * Add support for ConsenSys multisig wallet (#6153) * First draft of ConsenSys wallet * Fix transfer store // WIP Consensys Wallet * Rename walletABI JSON file * Fix linting * Fix wrong daylimit in wallet modal * Confirm/Revoke ConsensysWallet txs * Linting * Change of settings for the Multisig Wallet --- .../abi/consensys-multisig-wallet.json | 510 ++++++++++++++++++ ...t.json => foundation-multisig-wallet.json} | 0 js/src/contracts/abi/index.js | 2 +- js/src/embed.js | 11 +- js/src/modals/AddContract/types.js | 4 +- .../modals/CreateWallet/createWalletStore.js | 6 +- js/src/modals/Transfer/store.js | 305 ++++------- .../WalletSettings/walletSettingsStore.js | 86 +-- js/src/redux/providers/personalActions.js | 2 +- js/src/redux/providers/status.js | 8 +- js/src/redux/providers/walletActions.js | 260 ++------- js/src/util/tx.js | 74 +-- js/src/util/wallets.js | 426 +++++---------- js/src/util/wallets/consensys-wallet.js | 354 ++++++++++++ js/src/util/wallets/foundation-wallet.js | 500 +++++++++++++++++ js/src/util/wallets/pending-contracts.js | 49 ++ js/src/util/wallets/updates.js | 21 + js/src/views/Home/Urls/urls.css | 1 + js/src/views/Home/Urls/urls.js | 2 +- js/src/views/Web/store.js | 7 +- js/src/views/Web/web.css | 31 +- js/src/views/Web/web.js | 30 ++ parity/cli/mod.rs | 2 +- parity/configuration.rs | 2 +- parity/rpc.rs | 2 +- 25 files changed, 1897 insertions(+), 798 deletions(-) create mode 100644 js/src/contracts/abi/consensys-multisig-wallet.json rename js/src/contracts/abi/{wallet.json => foundation-multisig-wallet.json} (100%) create mode 100644 js/src/util/wallets/consensys-wallet.js create mode 100644 js/src/util/wallets/foundation-wallet.js create mode 100644 js/src/util/wallets/pending-contracts.js create mode 100644 js/src/util/wallets/updates.js diff --git a/js/src/contracts/abi/consensys-multisig-wallet.json b/js/src/contracts/abi/consensys-multisig-wallet.json new file mode 100644 index 000000000..79623637d --- /dev/null +++ b/js/src/contracts/abi/consensys-multisig-wallet.json @@ -0,0 +1,510 @@ +[ + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + } + ], + "name": "owners", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "owner", + "type": "address" + } + ], + "name": "removeOwner", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "transactionId", + "type": "uint256" + } + ], + "name": "revokeConfirmation", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "isOwner", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + }, + { + "name": "", + "type": "address" + } + ], + "name": "confirmations", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "pending", + "type": "bool" + }, + { + "name": "executed", + "type": "bool" + } + ], + "name": "getTransactionCount", + "outputs": [ + { + "name": "count", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "owner", + "type": "address" + } + ], + "name": "addOwner", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "transactionId", + "type": "uint256" + } + ], + "name": "isConfirmed", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "transactionId", + "type": "uint256" + } + ], + "name": "getConfirmationCount", + "outputs": [ + { + "name": "count", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + } + ], + "name": "transactions", + "outputs": [ + { + "name": "destination", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + }, + { + "name": "data", + "type": "bytes" + }, + { + "name": "executed", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getOwners", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "from", + "type": "uint256" + }, + { + "name": "to", + "type": "uint256" + }, + { + "name": "pending", + "type": "bool" + }, + { + "name": "executed", + "type": "bool" + } + ], + "name": "getTransactionIds", + "outputs": [ + { + "name": "_transactionIds", + "type": "uint256[]" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "transactionId", + "type": "uint256" + } + ], + "name": "getConfirmations", + "outputs": [ + { + "name": "_confirmations", + "type": "address[]" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "transactionCount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_required", + "type": "uint256" + } + ], + "name": "changeRequirement", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "transactionId", + "type": "uint256" + } + ], + "name": "confirmTransaction", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "destination", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + }, + { + "name": "data", + "type": "bytes" + } + ], + "name": "submitTransaction", + "outputs": [ + { + "name": "transactionId", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "MAX_OWNER_COUNT", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "required", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "owner", + "type": "address" + }, + { + "name": "newOwner", + "type": "address" + } + ], + "name": "replaceOwner", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "transactionId", + "type": "uint256" + } + ], + "name": "executeTransaction", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "inputs": [ + { + "name": "_owners", + "type": "address[]" + }, + { + "name": "_required", + "type": "uint256" + } + ], + "payable": false, + "type": "constructor" + }, + { + "payable": true, + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "name": "transactionId", + "type": "uint256" + } + ], + "name": "Confirmation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "name": "transactionId", + "type": "uint256" + } + ], + "name": "Revocation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "transactionId", + "type": "uint256" + } + ], + "name": "Submission", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "transactionId", + "type": "uint256" + } + ], + "name": "Execution", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "transactionId", + "type": "uint256" + } + ], + "name": "ExecutionFailure", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + } + ], + "name": "OwnerAddition", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + } + ], + "name": "OwnerRemoval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "name": "required", + "type": "uint256" + } + ], + "name": "RequirementChange", + "type": "event" + } +] diff --git a/js/src/contracts/abi/wallet.json b/js/src/contracts/abi/foundation-multisig-wallet.json similarity index 100% rename from js/src/contracts/abi/wallet.json rename to js/src/contracts/abi/foundation-multisig-wallet.json diff --git a/js/src/contracts/abi/index.js b/js/src/contracts/abi/index.js index 8985d869e..f475cce07 100644 --- a/js/src/contracts/abi/index.js +++ b/js/src/contracts/abi/index.js @@ -28,4 +28,4 @@ export registry2 from './registry2.json'; export signaturereg from './signaturereg.json'; export smsverification from './sms-verification.json'; export tokenreg from './tokenreg.json'; -export wallet from './wallet.json'; +export foundationWallet from './foundation-multisig-wallet.json'; diff --git a/js/src/embed.js b/js/src/embed.js index 5e8bf7ffe..0413fb466 100644 --- a/js/src/embed.js +++ b/js/src/embed.js @@ -64,14 +64,19 @@ class FakeTransport { class FrameSecureApi extends SecureApi { constructor (transport) { - super(transport.uiUrl, null, () => { - return transport; - }); + super( + transport.uiUrl, + null, + () => transport, + () => 'http:' + ); } connect () { // Do nothing - this API does not need connecting this.emit('connecting'); + // Fetch settings + this._fetchSettings(); // Fire connected event with some delay. setTimeout(() => { this.emit('connected'); diff --git a/js/src/modals/AddContract/types.js b/js/src/modals/AddContract/types.js index b229fc7ac..dd1e20fbc 100644 --- a/js/src/modals/AddContract/types.js +++ b/js/src/modals/AddContract/types.js @@ -17,7 +17,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { eip20, wallet } from '~/contracts/abi'; +import { eip20, foundationWallet } from '~/contracts/abi'; const ABI_TYPES = [ { @@ -72,7 +72,7 @@ const ABI_TYPES = [ ), readOnly: true, type: 'multisig', - value: JSON.stringify(wallet) + value: JSON.stringify(foundationWallet) }, { description: ( diff --git a/js/src/modals/CreateWallet/createWalletStore.js b/js/src/modals/CreateWallet/createWalletStore.js index d614e8041..26ed5816c 100644 --- a/js/src/modals/CreateWallet/createWalletStore.js +++ b/js/src/modals/CreateWallet/createWalletStore.js @@ -21,7 +21,7 @@ import { FormattedMessage } from 'react-intl'; import Contract from '~/api/contract'; import Contracts from '~/contracts'; -import { wallet as walletAbi } from '~/contracts/abi'; +import { foundationWallet as walletAbi } from '~/contracts/abi'; import { wallet as walletCode, walletLibrary as walletLibraryCode, walletLibraryRegKey, fullWalletCode } from '~/contracts/code/wallet'; import { validateUint, validateAddress, validateName } from '~/util/validation'; @@ -163,11 +163,11 @@ export default class CreateWalletStore { WalletsUtils.fetchOwners(walletContract), WalletsUtils.fetchDailylimit(walletContract) ]) - .then(([ require, owners, dailylimit ]) => { + .then(([ require, owners, daylimit ]) => { transaction(() => { this.wallet.owners = owners; this.wallet.required = require.toNumber(); - this.wallet.dailylimit = dailylimit.limit; + this.wallet.daylimit = daylimit.limit; this.wallet = this.getWalletWithMeta(this.wallet); }); diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js index 737bab778..eaccf4f40 100644 --- a/js/src/modals/Transfer/store.js +++ b/js/src/modals/Transfer/store.js @@ -18,12 +18,12 @@ import { noop } from 'lodash'; import { observable, computed, action, transaction } from 'mobx'; import BigNumber from 'bignumber.js'; -import { eip20 as tokenAbi, wallet as walletAbi } from '~/contracts/abi'; +import { eip20 as tokenAbi } from '~/contracts/abi'; import { fromWei } from '~/api/util/wei'; -import Contract from '~/api/contract'; import ERRORS from './errors'; -import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; +import { DEFAULT_GAS } from '~/util/constants'; import { ETH_TOKEN } from '~/util/tokens'; +import { getTxOptions } from '~/util/tx'; import GasPriceStore from '~/ui/GasPriceEditor/store'; import { getLogger, LOG_KEYS } from '~/config'; @@ -92,7 +92,6 @@ export default class TransferStore { if (this.isWallet) { this.wallet = props.wallet; - this.walletContract = new Contract(this.api, walletAbi); } if (senders) { @@ -115,19 +114,13 @@ export default class TransferStore { @computed get isValid () { const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError; const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.gasStore.conditionBlockError && !this.totalError; - const verifyValid = !this.passwordError; switch (this.stage) { case 0: return detailsValid; case 1: - return this.extras - ? extrasValid - : verifyValid; - - case 2: - return verifyValid; + return extrasValid; } } @@ -263,16 +256,21 @@ export default class TransferStore { if (this.isWallet && !valueError) { const { last, limit, spent } = this.wallet.dailylimit; - const remains = fromWei(limit.minus(spent)); - const today = Math.round(Date.now() / (24 * 3600 * 1000)); - const isResetable = last.lt(today); - if ((!isResetable && remains.lt(value)) || fromWei(limit).lt(value)) { - // already spent too much today - this.walletWarning = WALLET_WARNING_SPENT_TODAY_LIMIT; - } else if (this.walletWarning) { - // all ok - this.walletWarning = null; + // Don't show a warning if the limit is 0 + // (will always need confirmations) + if (limit.gt(0)) { + const remains = fromWei(limit.minus(spent)); + const today = Math.round(Date.now() / (24 * 3600 * 1000)); + const willResetLimit = last.lt(today); + + if ((!willResetLimit && remains.lt(value)) || fromWei(limit).lt(value)) { + // already spent too much today + this.walletWarning = WALLET_WARNING_SPENT_TODAY_LIMIT; + } else if (this.walletWarning) { + // all ok + this.walletWarning = null; + } } } @@ -312,24 +310,16 @@ export default class TransferStore { }); } - getBalance (forceSender = false) { - if (this.isWallet && !forceSender) { - return this.balance; - } - - const balance = this.senders - ? this.sendersBalances[this.sender] - : this.balance; - - return balance; - } - /** * Return the balance of the selected token * (in WEI for ETH, without formating for other tokens) */ - getTokenBalance (token = this.token, forceSender = false) { - return new BigNumber(this.balance[token.id] || 0); + getTokenBalance (token = this.token, address = this.account.address) { + const balance = address === this.account.address + ? this.balance + : this.sendersBalances[address]; + + return new BigNumber(balance[token.id] || 0); } getTokenValue (token = this.token, value = this.value, inverse = false) { @@ -348,54 +338,30 @@ export default class TransferStore { return _value.mul(token.format); } - getValues (_gasTotal) { - const gasTotal = new BigNumber(_gasTotal || 0); + getValue () { const { valueAll, isEth, isWallet } = this; - log.debug('@getValues', 'gas', gasTotal.toFormat()); - if (!valueAll) { const value = this.getTokenValue(); - // If it's a token or a wallet, eth is the estimated gas, - // and value is the user input - if (!isEth || isWallet) { - return { - eth: gasTotal, - token: value - }; - } - - // Otherwise, eth is the sum of the gas and the user input - const totalEthValue = gasTotal.plus(value); - - return { - eth: totalEthValue, - token: value - }; + return value; } - // If it's the total balance that needs to be sent, send the total balance - // if it's not a proper ETH transfer + const balance = this.getTokenBalance(); + if (!isEth || isWallet) { - const tokenBalance = this.getTokenBalance(); - - return { - eth: gasTotal, - token: tokenBalance - }; + return balance; } - // Otherwise, substract the gas estimate - const availableEth = this.getTokenBalance(ETH_TOKEN); - const totalEthValue = availableEth.gt(gasTotal) - ? availableEth.minus(gasTotal) + // substract the gas estimate + const gasTotal = new BigNumber(this.gasStore.price || 0) + .mul(new BigNumber(this.gasStore.gas || 0)); + + const totalEthValue = balance.gt(gasTotal) + ? balance.minus(gasTotal) : new BigNumber(0); - return { - eth: totalEthValue.plus(gasTotal), - token: totalEthValue - }; + return totalEthValue; } getFormattedTokenValue (tokenValue) { @@ -403,160 +369,125 @@ export default class TransferStore { } @action recalculate = (redo = false) => { - const { account } = this; + const { account, balance } = this; - if (!account || !this.balance) { + if (!account || !balance) { return; } - const balance = this.getBalance(); + return this.getTransactionOptions() + .then((options) => { + const gasTotal = options.gas.mul(options.gasPrice); - if (!balance) { - return; - } + const tokenValue = this.getValue(); + const ethValue = options.value.add(gasTotal); - const gasTotal = new BigNumber(this.gasStore.price || 0).mul(new BigNumber(this.gasStore.gas || 0)); + const tokenBalance = this.getTokenBalance(); + const ethBalance = this.getTokenBalance(ETH_TOKEN, options.from); - const ethBalance = this.getTokenBalance(ETH_TOKEN, true); - const tokenBalance = this.getTokenBalance(); - const { eth, token } = this.getValues(gasTotal); + let totalError = null; + let valueError = null; - let totalError = null; - let valueError = null; + if (tokenValue.gt(tokenBalance)) { + valueError = ERRORS.largeAmount; + } - if (eth.gt(ethBalance)) { - totalError = ERRORS.largeAmount; - } + if (ethValue.gt(ethBalance)) { + totalError = ERRORS.largeAmount; + } - if (token && token.gt(tokenBalance)) { - valueError = ERRORS.largeAmount; - } + log.debug('@recalculate', { + eth: ethValue.toFormat(), + token: tokenValue.toFormat(), + ethBalance: ethBalance.toFormat(), + tokenBalance: tokenBalance.toFormat(), + gasTotal: gasTotal.toFormat() + }); - log.debug('@recalculate', { - eth: eth.toFormat(), - token: token.toFormat(), - ethBalance: ethBalance.toFormat(), - tokenBalance: tokenBalance.toFormat(), - gasTotal: gasTotal.toFormat() - }); + transaction(() => { + this.totalError = totalError; + this.valueError = valueError; + this.gasStore.setErrorTotal(totalError); + this.gasStore.setEthValue(options.value); - transaction(() => { - this.totalError = totalError; - this.valueError = valueError; - this.gasStore.setErrorTotal(totalError); - this.gasStore.setEthValue(eth.sub(gasTotal)); + this.total = fromWei(ethValue).toFixed(); - this.total = this.api.util.fromWei(eth).toFixed(); + const nextValue = this.getFormattedTokenValue(tokenValue); + let prevValue; - const nextValue = this.getFormattedTokenValue(token); - let prevValue; + try { + prevValue = new BigNumber(this.value || 0); + } catch (error) { + prevValue = new BigNumber(0); + } - try { - prevValue = new BigNumber(this.value || 0); - } catch (error) { - prevValue = new BigNumber(0); - } + // Change the input only if necessary + if (!nextValue.eq(prevValue)) { + this.value = nextValue.toString(); + } - // Change the input only if necessary - if (!nextValue.eq(prevValue)) { - this.value = nextValue.toString(); - } - - // Re Calculate gas once more to be sure - if (redo) { - return this.recalculateGas(false); - } - }); - } - - send () { - const { options, values } = this._getTransferParams(); - - log.debug('@send', 'transfer value', options.value && options.value.toFormat()); - - return this._getTransferMethod().postTransaction(options, values); - } - - _estimateGas (forceToken = false) { - const { options, values } = this._getTransferParams(true, forceToken); - - return this._getTransferMethod(true, forceToken).estimateGas(options, values); + // Re Calculate gas once more to be sure + if (redo) { + return this.recalculateGas(false); + } + }); + }); } estimateGas () { - return this._estimateGas(); + return this.getTransactionOptions() + .then((options) => { + return this.api.eth.estimateGas(options); + }); } - _getTransferMethod (gas = false, forceToken = false) { - const { isEth, isWallet } = this; + send () { + return this.getTransactionOptions() + .then((options) => { + log.debug('@send', 'transfer value', options.value && options.value.toFormat()); - if (isEth && !isWallet && !forceToken) { - return gas ? this.api.eth : this.api.parity; - } - - if (isWallet && !forceToken) { - return this.wallet.instance.execute; - } - - return this.tokenContract.at(this.token.address).instance.transfer; + return this.api.parity.postTransaction(options); + }); } - _getData (gas = false) { - const { isEth, isWallet } = this; + getTransactionOptions () { + const [ func, options, values ] = this._getTransactionArgs(); - if (!isWallet || isEth) { - return this.data && this.data.length ? this.data : ''; - } - - const func = this._getTransferMethod(gas, true); - const { options, values } = this._getTransferParams(gas, true); - - return this.tokenContract.at(this.token.address).getCallData(func, options, values); + return getTxOptions(this.api, func, options, values) + .then((_options) => { + delete _options.sender; + return _options; + }); } - _getTransferParams (gas = false, forceToken = false) { - const { isEth, isWallet } = this; - - const to = (isEth && !isWallet) ? this.recipient - : (this.isWallet ? this.wallet.address : this.token.address); + _getTransactionArgs () { + const { isEth } = this; + const value = this.getValue(); const options = this.gasStore.overrideTransaction({ - from: this.sender || this.account.address, - to + from: this.account.address, + sender: this.sender }); - if (gas) { - options.gas = MAX_GAS_ESTIMATION; - } - - const gasTotal = new BigNumber(options.gas || DEFAULT_GAS).mul(options.gasPrice || DEFAULT_GASPRICE); - const { token } = this.getValues(gasTotal); - - if (isEth && !isWallet && !forceToken) { - options.value = token; - options.data = this._getData(gas); - - return { options, values: [] }; - } - - if (isWallet && !forceToken) { - const to = isEth ? this.recipient : this.token.address; - const value = isEth ? token : new BigNumber(0); - - const values = [ - to, value, - this._getData(gas) - ]; - - return { options, values }; + // A simple ETH transfer + if (isEth) { + options.value = value; + options.data = this.data || ''; + options.to = this.recipient; + + return [ null, options ]; } + // A token transfer + const tokenContract = this.tokenContract.at(this.token.address); const values = [ this.recipient, - token.toFixed(0) + value ]; - return { options, values }; + options.to = this.token.address; + + return [ tokenContract.instance.transfer, options, values ]; } _validatePositiveNumber (num) { diff --git a/js/src/modals/WalletSettings/walletSettingsStore.js b/js/src/modals/WalletSettings/walletSettingsStore.js index d31ec9eb2..c3adb812e 100644 --- a/js/src/modals/WalletSettings/walletSettingsStore.js +++ b/js/src/modals/WalletSettings/walletSettingsStore.js @@ -20,6 +20,7 @@ import BigNumber from 'bignumber.js'; import { validateUint, validateAddress } from '~/util/validation'; import { DEFAULT_GAS, MAX_GAS_ESTIMATION } from '~/util/constants'; +import WalletsUtils from '~/util/wallets'; const STEPS = { EDIT: { title: 'wallet settings' }, @@ -220,8 +221,6 @@ export default class WalletSettingsStore { this.api = api; this.step = this.stepsKeys[0]; - this.walletInstance = wallet.instance; - this.initialWallet = { address: wallet.address, owners: wallet.owners, @@ -280,72 +279,43 @@ export default class WalletSettingsStore { @action send = () => { const changes = this.changes; - const walletInstance = this.walletInstance; - Promise.all(changes.map((change) => this.sendChange(change, walletInstance))); + Promise.all(changes.map((change) => this.sendChange(change))); this.onClose(); } - @action sendChange = (change, walletInstance) => { - const { method, values } = this.getChangeMethod(change, walletInstance); + @action sendChange = (change) => { + const { api, initialWallet } = this; - const options = { - from: this.wallet.sender, - to: this.initialWallet.address, - gas: MAX_GAS_ESTIMATION - }; - - return method - .estimateGas(options, values) - .then((gasEst) => { - let gas = gasEst; - - if (gas.gt(DEFAULT_GAS)) { - gas = gas.mul(1.2); + WalletsUtils.getChangeMethod(api, initialWallet.address, change) + .then((changeMethod) => { + if (!changeMethod) { + return; } - options.gas = gas; - return method.postTransaction(options, values); + const { method, values } = changeMethod; + + const options = { + from: this.wallet.sender, + to: initialWallet.address, + gas: MAX_GAS_ESTIMATION + }; + + return method + .estimateGas(options, values) + .then((gasEst) => { + let gas = gasEst; + + if (gas.gt(DEFAULT_GAS)) { + gas = gas.mul(1.2); + } + options.gas = gas; + + return method.postTransaction(options, values); + }); }); } - getChangeMethod = (change, walletInstance) => { - if (change.type === 'require') { - return { - method: walletInstance.changeRequirement, - values: [ change.value ] - }; - } - - if (change.type === 'dailylimit') { - return { - method: walletInstance.setDailyLimit, - values: [ change.value ] - }; - } - - if (change.type === 'add_owner') { - return { - method: walletInstance.addOwner, - values: [ change.value ] - }; - } - - if (change.type === 'change_owner') { - return { - method: walletInstance.changeOwner, - values: [ change.value.from, change.value.to ] - }; - } - - if (change.type === 'remove_owner') { - return { - method: walletInstance.removeOwner, - values: [ change.value ] - }; - } - } - @action validateWallet = (_wallet) => { const senderValidation = validateAddress(_wallet.sender); const requireValidation = validateUint(_wallet.require); diff --git a/js/src/redux/providers/personalActions.js b/js/src/redux/providers/personalActions.js index 6200537c3..f747fa92e 100644 --- a/js/src/redux/providers/personalActions.js +++ b/js/src/redux/providers/personalActions.js @@ -23,7 +23,7 @@ import { attachWallets } from './walletActions'; import Contract from '~/api/contract'; import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore'; import WalletsUtils from '~/util/wallets'; -import { wallet as WalletAbi } from '~/contracts/abi'; +import { foundationWallet as WalletAbi } from '~/contracts/abi'; export function personalAccountsInfo (accountsInfo) { const accounts = {}; diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index fc5dc38ba..e58fcf6c1 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -227,12 +227,12 @@ export default class Status { } _overallStatus = (health) => { - const all = [health.peers, health.sync, health.time].filter(x => x); - const allNoTime = [health.peers, health.sync].filter(x => x); + const allWithTime = [health.peers, health.sync, health.time].filter(x => x); + const all = [health.peers, health.sync].filter(x => x); const statuses = all.map(x => x.status); const bad = statuses.find(x => x === STATUS_BAD); - const needsAttention = allNoTime.map(x => x.status).find(x => x === STATUS_WARN); - const message = all.map(x => x.message).filter(x => x); + const needsAttention = statuses.find(x => x === STATUS_WARN); + const message = allWithTime.map(x => x.message).filter(x => x); if (all.length) { return { diff --git a/js/src/redux/providers/walletActions.js b/js/src/redux/providers/walletActions.js index b31a2b35b..58a8faca6 100644 --- a/js/src/redux/providers/walletActions.js +++ b/js/src/redux/providers/walletActions.js @@ -17,19 +17,17 @@ import { isEqual, uniq } from 'lodash'; import Contract from '~/api/contract'; -import { bytesToHex, toHex } from '~/api/util/format'; import { ERROR_CODES } from '~/api/transport/error'; -import { wallet as WALLET_ABI } from '~/contracts/abi'; -import { MAX_GAS_ESTIMATION } from '~/util/constants'; +import { foundationWallet as WALLET_ABI } from '~/contracts/abi'; import WalletsUtils from '~/util/wallets'; - import { newError } from '~/ui/Errors/actions'; - -const UPDATE_OWNERS = 'owners'; -const UPDATE_REQUIRE = 'require'; -const UPDATE_DAILYLIMIT = 'dailylimit'; -const UPDATE_TRANSACTIONS = 'transactions'; -const UPDATE_CONFIRMATIONS = 'confirmations'; +import { + UPDATE_OWNERS, + UPDATE_REQUIRE, + UPDATE_DAILYLIMIT, + UPDATE_TRANSACTIONS, + UPDATE_CONFIRMATIONS +} from '~/util/wallets/updates'; export function confirmOperation (address, owner, operation) { return modifyOperation('confirm', address, owner, operation); @@ -39,41 +37,25 @@ export function revokeOperation (address, owner, operation) { return modifyOperation('revoke', address, owner, operation); } -function modifyOperation (method, address, owner, operation) { +function modifyOperation (modification, address, owner, operation) { return (dispatch, getState) => { const { api } = getState(); - const contract = new Contract(api, WALLET_ABI).at(address); - - const options = { - from: owner, - gas: MAX_GAS_ESTIMATION - }; - - const values = [ operation ]; dispatch(setOperationPendingState(address, operation, true)); - contract.instance[method] - .estimateGas(options, values) - .then((gas) => { - options.gas = gas.mul(1.2); - return contract.instance[method].postTransaction(options, values); - }) + WalletsUtils.postModifyOperation(api, address, modification, owner, operation) .then((requestId) => { - return api - .pollMethod('parity_checkRequest', requestId) - .catch((e) => { - dispatch(setOperationPendingState(address, operation, false)); - if (e.code === ERROR_CODES.REQUEST_REJECTED) { - return; - } - - throw e; - }); + return api.pollMethod('parity_checkRequest', requestId); }) .catch((error) => { - dispatch(setOperationPendingState(address, operation, false)); + if (error.code === ERROR_CODES.REQUEST_REJECTED) { + return; + } + dispatch(newError(error)); + }) + .then(() => { + dispatch(setOperationPendingState(address, operation, false)); }); }; } @@ -97,14 +79,18 @@ export function attachWallets (_wallets) { return dispatch(updateWallets({ wallets: {}, walletsAddresses: [], filterSubId: null })); } - const filterOptions = { - fromBlock: 0, - toBlock: 'latest', - address: nextAddresses - }; - + // Filter the logs from the current block api.eth - .newFilter(filterOptions) + .blockNumber() + .then((block) => { + const filterOptions = { + fromBlock: block, + toBlock: 'latest', + address: nextAddresses + }; + + return api.eth.newFilter(filterOptions); + }) .then((filterId) => { dispatch(updateWallets({ wallets: _wallets, walletsAddresses: nextAddresses, filterSubId: filterId })); }) @@ -142,7 +128,6 @@ export function load (api) { api.eth .getFilterChanges(filterSubId) - .then((logs) => contract.parseEventLogs(logs)) .then((logs) => { parseLogs(logs)(dispatch, getState); }) @@ -292,202 +277,57 @@ function fetchWalletDailylimit (contract) { } function fetchWalletConfirmations (contract, _operations, _owners = null, _transactions = null, getState) { - const walletInstance = contract.instance; - const wallet = getState().wallet.wallets[contract.address]; const owners = _owners || (wallet && wallet.owners) || null; const transactions = _transactions || (wallet && wallet.transactions) || null; - // Full load if no operations given, or if the one given aren't loaded yet - const fullLoad = !Array.isArray(_operations) || _operations - .filter((op) => !wallet.confirmations.find((conf) => conf.operation === op)) - .length > 0; + const cache = { owners, transactions }; - let promise; - - if (fullLoad) { - promise = walletInstance - .ConfirmationNeeded - .getAllLogs() - .then((logs) => { - return logs.map((log) => ({ - initiator: log.params.initiator.value, - to: log.params.to.value, - data: log.params.data.value, - value: log.params.value.value, - operation: bytesToHex(log.params.operation.value), - transactionIndex: log.transactionIndex, - transactionHash: log.transactionHash, - blockNumber: log.blockNumber, - confirmedBy: [] - })); - }) - .then((logs) => { - return logs.sort((logA, logB) => { - const comp = logA.blockNumber.comparedTo(logB.blockNumber); - - if (comp !== 0) { - return comp; - } - - return logA.transactionIndex.comparedTo(logB.transactionIndex); - }); - }) - .then((confirmations) => { - if (confirmations.length === 0) { - return confirmations; - } - - // Only fetch confirmations for operations not - // yet confirmed (ie. not yet a transaction) - if (transactions) { - const operations = transactions - .filter((t) => t.operation) - .map((t) => t.operation); - - return confirmations.filter((confirmation) => { - return !operations.includes(confirmation.operation); - }); - } - - return confirmations; - }); - } else { - const { confirmations } = wallet; - const nextConfirmations = confirmations - .filter((conf) => _operations.includes(conf.operation)); - - promise = Promise.resolve(nextConfirmations); - } - - return promise + return WalletsUtils.fetchPendingTransactions(contract, cache) .then((confirmations) => { - if (confirmations.length === 0) { - return confirmations; - } - - const uniqConfirmations = Object.values( - confirmations.reduce((confirmations, confirmation) => { - confirmations[confirmation.operation] = confirmation; - return confirmations; - }, {}) - ); - - const operations = uniqConfirmations.map((conf) => conf.operation); - - return Promise - .all(operations.map((op) => fetchOperationConfirmations(contract, op, owners))) - .then((confirmedBys) => { - uniqConfirmations.forEach((_, index) => { - uniqConfirmations[index].confirmedBy = confirmedBys[index]; - }); - - return uniqConfirmations; - }); - }) - .then((confirmations) => { - const prevConfirmations = wallet.confirmations || []; - const nextConfirmations = prevConfirmations - .filter((conA) => !confirmations.find((conB) => conB.operation === conA.operation)) - .concat(confirmations) - .map((conf) => ({ - ...conf, - pending: false - })); - return { key: UPDATE_CONFIRMATIONS, - value: nextConfirmations + value: confirmations }; }); } -function fetchOperationConfirmations (contract, operation, owners = null) { - if (!owners) { - console.warn('[fetchOperationConfirmations] try to provide the owners for the Wallet', contract.address); - } - - const walletInstance = contract.instance; - - const promise = owners - ? Promise.resolve({ value: owners }) - : fetchWalletOwners(contract); - - return promise - .then((result) => { - const owners = result.value; - - return Promise - .all(owners.map((owner) => walletInstance.hasConfirmed.call({}, [ operation, owner ]))) - .then((data) => { - return owners.filter((owner, index) => data[index]); - }); - }); -} - function parseLogs (logs) { return (dispatch, getState) => { if (!logs || logs.length === 0) { return; } - const WalletSignatures = WalletsUtils.getWalletSignatures(); - + const { api } = getState(); const updates = {}; - logs.forEach((log) => { - const { address, topics } = log; - const eventSignature = toHex(topics[0]); - const prev = updates[address] || { - [ UPDATE_DAILYLIMIT ]: true, - address - }; + const promises = logs.map((log) => { + const { address } = log; - switch (eventSignature) { - case WalletSignatures.OwnerChanged: - case WalletSignatures.OwnerAdded: - case WalletSignatures.OwnerRemoved: - updates[address] = { - ...prev, - [ UPDATE_OWNERS ]: true + return WalletsUtils.logToUpdate(api, address, log) + .then((update) => { + const prev = updates[address] || { + [ UPDATE_DAILYLIMIT ]: true, + address }; - return; - case WalletSignatures.RequirementChanged: - updates[address] = { - ...prev, - [ UPDATE_REQUIRE ]: true - }; - return; + if (update[UPDATE_CONFIRMATIONS]) { + const operations = (prev[UPDATE_CONFIRMATIONS] || []).concat(update[UPDATE_CONFIRMATIONS]); - case WalletSignatures.ConfirmationNeeded: - case WalletSignatures.Confirmation: - case WalletSignatures.Revoke: - const operation = bytesToHex(log.params.operation.value); + update[UPDATE_CONFIRMATIONS] = uniq(operations); + } updates[address] = { ...prev, - [ UPDATE_CONFIRMATIONS ]: uniq( - (prev[UPDATE_CONFIRMATIONS] || []).concat(operation) - ) + ...update }; - - return; - - case WalletSignatures.Deposit: - case WalletSignatures.SingleTransact: - case WalletSignatures.MultiTransact: - case WalletSignatures.Old.SingleTransact: - case WalletSignatures.Old.MultiTransact: - updates[address] = { - ...prev, - [ UPDATE_TRANSACTIONS ]: true - }; - return; - } + }); }); - fetchWalletsInfo(updates)(dispatch, getState); + return Promise.all(promises) + .then(() => { + fetchWalletsInfo(updates)(dispatch, getState); + }); }; } diff --git a/js/src/util/tx.js b/js/src/util/tx.js index 9ab8b6599..e325e6024 100644 --- a/js/src/util/tx.js +++ b/js/src/util/tx.js @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import BigNumber from 'bignumber.js'; + import WalletsUtils from '~/util/wallets'; /** @@ -71,11 +73,14 @@ const isValidReceipt = (receipt) => { return receipt && receipt.blockNumber && receipt.blockNumber.gt(0); }; -function getTxArgs (func, options, values = []) { - const { contract } = func; - const { api } = contract; +export function getTxOptions (api, func, _options, values = []) { + const options = { ..._options }; const address = options.from; + if (func && func.contract) { + options.to = options.to || func.contract.address; + } + if (!address) { return Promise.resolve({ func, options, values }); } @@ -87,8 +92,9 @@ function getTxArgs (func, options, values = []) { return { func, options, values }; } - options.data = contract.getCallData(func, options, values); - options.to = options.to || contract.address; + if (func && func.contract) { + options.data = func.contract.getCallData(func, options, values); + } if (!options.to) { return { func, options, values }; @@ -103,24 +109,35 @@ function getTxArgs (func, options, values = []) { return callArgs; }); + }) + .then(({ func, options, values }) => { + if (func) { + options.data = func.contract.getCallData(func, options, values); + } + + if (!options.value) { + options.value = new BigNumber(0); + } + + return options; }); } export function estimateGas (_func, _options, _values = []) { - return getTxArgs(_func, _options, _values) - .then((callArgs) => { - const { func, options, values } = callArgs; + const { api } = _func.contract; - return func._estimateGas(options, values); + return getTxOptions(api, _func, _options, _values) + .then((options) => { + return api.eth.estimateGas(options); }); } export function postTransaction (_func, _options, _values = []) { - return getTxArgs(_func, _options, _values) - .then((callArgs) => { - const { func, options, values } = callArgs; + const { api } = _func.contract; - return func._postTransaction(options, values); + return getTxOptions(api, _func, _options, _values) + .then((options) => { + return api.parity.postTransaction(options); }); } @@ -182,42 +199,35 @@ export function deploy (contract, options, values, skipGasEstimate = false) { } export function parseTransactionReceipt (api, options, receipt) { - const { metadata } = options; - const address = options.from; - if (receipt.gasUsed.eq(options.gas)) { const error = new Error(`Contract not deployed, gasUsed == ${options.gas.toFixed(0)}`); return Promise.reject(error); } - const logs = WalletsUtils.parseLogs(api, receipt.logs || []); + // If regular contract creation, only validate the contract + if (receipt.contractAddress) { + return validateContract(api, receipt.contractAddress); + } - const confirmationLog = logs.find((log) => log.event === 'ConfirmationNeeded'); - const transactionLog = logs.find((log) => log.event === 'SingleTransact'); + // Otherwise, needs to check for a contract deployment + // from a multisig wallet + const walletResult = WalletsUtils.parseTransactionLogs(api, options, receipt.logs || []); - if (!confirmationLog && !transactionLog && !receipt.contractAddress) { + if (!walletResult) { const error = new Error('Something went wrong in the contract deployment...'); return Promise.reject(error); } - // Confirmations are needed from the other owners - if (confirmationLog) { - const operationHash = api.util.bytesToHex(confirmationLog.params.operation.value); - - // Add the contract to pending contracts - WalletsUtils.addPendingContract(address, operationHash, metadata); + if (walletResult.pending) { return Promise.resolve(null); } - if (transactionLog) { - // Set the contract address in the receipt - receipt.contractAddress = transactionLog.params.created.value; - } - - const contractAddress = receipt.contractAddress; + return validateContract(api, walletResult.contractAddress); +} +function validateContract (api, contractAddress) { return api.eth .getCode(contractAddress) .then((code) => { diff --git a/js/src/util/wallets.js b/js/src/util/wallets.js index e90f4115f..6b1c29d01 100644 --- a/js/src/util/wallets.js +++ b/js/src/util/wallets.js @@ -15,95 +15,96 @@ // along with Parity. If not, see . import BigNumber from 'bignumber.js'; -import { intersection, range, uniq } from 'lodash'; -import store from 'store'; +import { intersection } from 'lodash'; -import Abi from '~/abi'; -import Contract from '~/api/contract'; -import { bytesToHex, toHex } from '~/api/util/format'; -import { validateAddress } from '~/util/validation'; -import WalletAbi from '~/contracts/abi/wallet.json'; -import OldWalletAbi from '~/contracts/abi/old-wallet.json'; +import ConsensysWalletUtils from './wallets/consensys-wallet'; +import FoundationWalletUtils from './wallets/foundation-wallet'; -const LS_PENDING_CONTRACTS_KEY = '_parity::wallets::pendingContracts'; +const CONSENSYS_WALLET = 'CONSENSYS_WALLET'; +const FOUNDATION_WALLET = 'FOUNDATION_WALLET'; const _cachedWalletLookup = {}; +const _cachedWalletTypes = {}; let _cachedAccounts = {}; -const walletAbi = new Abi(WalletAbi); -const oldWalletAbi = new Abi(OldWalletAbi); - -const walletEvents = walletAbi.events.reduce((events, event) => { - events[event.name] = event; - return events; -}, {}); - -const oldWalletEvents = oldWalletAbi.events.reduce((events, event) => { - events[event.name] = event; - return events; -}, {}); - -const WalletSignatures = { - OwnerChanged: toHex(walletEvents.OwnerChanged.signature), - OwnerAdded: toHex(walletEvents.OwnerAdded.signature), - OwnerRemoved: toHex(walletEvents.OwnerRemoved.signature), - RequirementChanged: toHex(walletEvents.RequirementChanged.signature), - Confirmation: toHex(walletEvents.Confirmation.signature), - Revoke: toHex(walletEvents.Revoke.signature), - Deposit: toHex(walletEvents.Deposit.signature), - SingleTransact: toHex(walletEvents.SingleTransact.signature), - MultiTransact: toHex(walletEvents.MultiTransact.signature), - ConfirmationNeeded: toHex(walletEvents.ConfirmationNeeded.signature), - - Old: { - SingleTransact: toHex(oldWalletEvents.SingleTransact.signature), - MultiTransact: toHex(oldWalletEvents.MultiTransact.signature) - } -}; - export default class WalletsUtils { - static getWalletSignatures () { - return WalletSignatures; - } - - static getPendingContracts () { - return store.get(LS_PENDING_CONTRACTS_KEY) || {}; - } - - static setPendingContracts (contracts = {}) { - return store.set(LS_PENDING_CONTRACTS_KEY, contracts); - } - - static removePendingContract (operationHash) { - const nextContracts = WalletsUtils.getPendingContracts(); - - delete nextContracts[operationHash]; - WalletsUtils.setPendingContracts(nextContracts); - } - - static addPendingContract (address, operationHash, metadata) { - const nextContracts = { - ...WalletsUtils.getPendingContracts(), - [ operationHash ]: { - address, - metadata, - operationHash - } - }; - - WalletsUtils.setPendingContracts(nextContracts); - } - static cacheAccounts (accounts) { _cachedAccounts = accounts; } - static getCallArgs (api, options, values = []) { - const walletContract = new Contract(api, WalletAbi); - const walletAddress = options.from; + static delegateCall (api, address, method, args = []) { + return WalletsUtils.getWalletType(api, address) + .then((walletType) => { + if (walletType === CONSENSYS_WALLET) { + return ConsensysWalletUtils[method].apply(null, args); + } + + return FoundationWalletUtils[method].apply(null, args); + }); + } + + static fetchDailylimit (walletContract) { + const { api } = walletContract; return WalletsUtils - .fetchOwners(walletContract.at(walletAddress)) + .delegateCall(api, walletContract.address, 'fetchDailylimit', [ walletContract ]); + } + + static fetchOwners (walletContract) { + const { api } = walletContract; + + return WalletsUtils + .delegateCall(api, walletContract.address, 'fetchOwners', [ walletContract ]); + } + + static fetchRequire (walletContract) { + const { api } = walletContract; + + return WalletsUtils + .delegateCall(api, walletContract.address, 'fetchRequire', [ walletContract ]); + } + + static fetchPendingTransactions (walletContract, cache) { + const { api } = walletContract; + + return WalletsUtils + .delegateCall(api, walletContract.address, 'fetchPendingTransactions', [ walletContract, cache ]); + } + + static fetchTransactions (walletContract) { + const { api } = walletContract; + + return WalletsUtils + .delegateCall(api, walletContract.address, 'fetchTransactions', [ walletContract ]) + .then((transactions) => { + return transactions.sort((txA, txB) => { + const comp = txB.blockNumber.comparedTo(txA.blockNumber); + + if (comp !== 0) { + return comp; + } + + return txB.transactionIndex.comparedTo(txA.transactionIndex); + }); + }); + } + + static getCallArgs (api, options, values = []) { + const walletAddress = options.from; + let walletContract; + let submitMethod; + + return Promise + .all([ + WalletsUtils.getWalletContract(api, walletAddress), + WalletsUtils.delegateCall(api, walletAddress, 'getSubmitMethod') + ]) + .then(([ _walletContract, _submitMethod ]) => { + walletContract = _walletContract; + submitMethod = _submitMethod; + + return WalletsUtils.fetchOwners(walletContract); + }) .then((owners) => { const addresses = Object.keys(_cachedAccounts); const ownerAddress = intersection(addresses, owners).pop(); @@ -121,12 +122,12 @@ export default class WalletsUtils { const nextValues = [ to, value, data ]; const nextOptions = { ..._options, - from: ownerAddress, + from: options.sender || ownerAddress, to: walletAddress, value: new BigNumber(0) }; - const execFunc = walletContract.instance.execute; + const execFunc = walletContract.instance[submitMethod]; const callArgs = { func: execFunc, options: nextOptions, values: nextValues }; if (!account.wallet) { @@ -139,6 +140,11 @@ export default class WalletsUtils { }); } + static getChangeMethod (api, address, change) { + return WalletsUtils + .delegateCall(api, address, 'getChangeMethod', [ api, address, change ]); + } + static getDeployArgs (contract, options, values) { const { api } = contract; const func = contract.constructors[0]; @@ -158,10 +164,41 @@ export default class WalletsUtils { }); } - static parseLogs (api, logs = []) { - const walletContract = new Contract(api, WalletAbi); + static getWalletContract (api, address) { + return WalletsUtils + .delegateCall(api, address, 'getWalletContract', [ api ]) + .then((walletContract) => { + return walletContract.at(address); + }); + } - return walletContract.parseEventLogs(logs); + static getWalletType (api, address) { + if (_cachedWalletTypes[address] === undefined) { + _cachedWalletTypes[address] = Promise.resolve(null) + .then((result) => { + if (result) { + return result; + } + + return FoundationWalletUtils.isWallet(api, address) + .then((isWallet) => isWallet && FOUNDATION_WALLET); + }) + .then((result) => { + if (result) { + return result; + } + + return ConsensysWalletUtils.isWallet(api, address) + .then((isWallet) => isWallet && CONSENSYS_WALLET); + }) + .then((result) => { + _cachedWalletTypes[address] = result || null; + + return _cachedWalletTypes[address]; + }); + } + + return Promise.resolve(_cachedWalletTypes[address]); } /** @@ -175,20 +212,8 @@ export default class WalletsUtils { } if (!_cachedWalletLookup[address]) { - const walletContract = new Contract(api, WalletAbi); - - _cachedWalletLookup[address] = walletContract - .at(address) - .instance - .m_numOwners - .call() - .then((result) => { - if (!result || result.equals(0)) { - return false; - } - - return true; - }) + _cachedWalletLookup[address] = WalletsUtils.getWalletType(api, address) + .then((walletType) => walletType !== null) .then((bool) => { _cachedWalletLookup[address] = Promise.resolve(bool); return bool; @@ -198,207 +223,34 @@ export default class WalletsUtils { return _cachedWalletLookup[address]; } - static fetchRequire (walletContract) { - return walletContract.instance.m_required.call(); - } - - static fetchOwners (walletContract) { - const walletInstance = walletContract.instance; - - return walletInstance - .m_numOwners.call() - .then((mNumOwners) => { - const promises = range(mNumOwners.toNumber()) - .map((idx) => walletInstance.getOwner.call({}, [ idx ])); - - return Promise - .all(promises) - .then((owners) => { - const uniqOwners = uniq(owners); - - // If all owners are the zero account : must be Mist wallet contract - if (uniqOwners.length === 1 && /^(0x)?0*$/.test(owners[0])) { - return WalletsUtils.fetchMistOwners(walletContract, mNumOwners.toNumber()); - } - - return owners; - }) - .then((owners) => uniq(owners)); - }); - } - - static fetchMistOwners (walletContract, mNumOwners) { - const walletAddress = walletContract.address; - + static logToUpdate (api, address, log) { return WalletsUtils - .getMistOwnersOffset(walletContract) - .then((result) => { - if (!result || result.offset === -1) { - return []; - } - - const owners = [ result.address ]; - - if (mNumOwners === 1) { - return owners; - } - - const initOffset = result.offset + 1; - let promise = Promise.resolve(); - - range(initOffset, initOffset + mNumOwners - 1).forEach((offset) => { - promise = promise - .then(() => { - return walletContract.api.eth.getStorageAt(walletAddress, offset); - }) - .then((result) => { - const resultAddress = '0x' + (result || '').slice(-40); - const { address } = validateAddress(resultAddress); - - owners.push(address); - }); - }); - - return promise.then(() => owners); - }); + .delegateCall(api, address, 'logToUpdate', [ log ]); } - static getMistOwnersOffset (walletContract, offset = 3) { - return walletContract.api.eth - .getStorageAt(walletContract.address, offset) - .then((result) => { - if (result && !/^(0x)?0*$/.test(result)) { - const resultAddress = '0x' + result.slice(-40); - const { address, addressError } = validateAddress(resultAddress); - - if (!addressError) { - return { offset, address }; - } - } - - if (offset >= 100) { - return { offset: -1 }; - } - - return WalletsUtils.getMistOwnersOffset(walletContract, offset + 1); - }); + static parseTransactionLogs (api, options, rawLogs) { + return WalletsUtils + .delegateCall(api, options.from, 'parseTransactionLogs', [ api, options, rawLogs ]); } - static fetchDailylimit (walletContract) { - const walletInstance = walletContract.instance; + static postModifyOperation (api, walletAddress, modification, owner, operation) { + const options = { from: owner }; + const values = [ operation ]; return Promise .all([ - walletInstance.m_dailyLimit.call(), - walletInstance.m_spentToday.call(), - walletInstance.m_lastDay.call() + WalletsUtils + .getWalletContract(api, walletAddress), + WalletsUtils + .delegateCall(api, walletAddress, 'getModifyOperationMethod', [ modification ]) ]) - .then(([ limit, spent, last ]) => ({ - limit, spent, last - })); - } - - static fetchTransactions (walletContract) { - const { api } = walletContract; - const pendingContracts = WalletsUtils.getPendingContracts(); - - return walletContract - .getAllLogs({ - topics: [ [ - WalletSignatures.SingleTransact, - WalletSignatures.MultiTransact, - WalletSignatures.Deposit, - WalletSignatures.Old.SingleTransact, - WalletSignatures.Old.MultiTransact - ] ] - }) - .then((logs) => { - return logs.sort((logA, logB) => { - const comp = logB.blockNumber.comparedTo(logA.blockNumber); - - if (comp !== 0) { - return comp; - } - - return logB.transactionIndex.comparedTo(logA.transactionIndex); - }); - }) - .then((logs) => { - const transactions = logs.map((log) => { - const signature = toHex(log.topics[0]); - - const value = log.params.value.value; - const from = signature === WalletSignatures.Deposit - ? log.params['_from'].value - : walletContract.address; - - const to = signature === WalletSignatures.Deposit - ? walletContract.address - : log.params.to.value; - - const transaction = { - transactionHash: log.transactionHash, - blockNumber: log.blockNumber, - from, to, value - }; - - if (log.params.created && log.params.created.value && !/^(0x)?0*$/.test(log.params.created.value)) { - transaction.creates = log.params.created.value; - delete transaction.to; - } - - if (log.params.operation) { - const operation = bytesToHex(log.params.operation.value); - - // Add the pending contract to the contracts - if (pendingContracts[operation]) { - const { metadata } = pendingContracts[operation]; - const contractName = metadata.name; - - metadata.blockNumber = log.blockNumber; - - // The contract creation might not be in the same log, - // but must be in the same transaction (eg. Contract creation - // from Wallet within a Wallet) - api.eth - .getTransactionReceipt(log.transactionHash) - .then((transactionReceipt) => { - const transactionLogs = WalletsUtils.parseLogs(api, transactionReceipt.logs); - const creationLog = transactionLogs.find((log) => { - return log.params.created && !/^(0x)?0*$/.test(log.params.created.value); - }); - - if (!creationLog) { - return false; - } - - const contractAddress = creationLog.params.created.value; - - return Promise - .all([ - api.parity.setAccountName(contractAddress, contractName), - api.parity.setAccountMeta(contractAddress, metadata) - ]) - .then(() => { - WalletsUtils.removePendingContract(operation); - }); - }) - .catch((error) => { - console.error('adding wallet contract', error); - }); - } - - transaction.operation = operation; - } - - if (log.params.data) { - transaction.data = log.params.data.value; - } - - return transaction; - }); - - return transactions; + .then(([ wallet, method ]) => { + return wallet.instance[method] + .estimateGas(options, values) + .then((gas) => { + options.gas = gas.mul(1.5); + return wallet.instance[method].postTransaction(options, values); + }); }); } } diff --git a/js/src/util/wallets/consensys-wallet.js b/js/src/util/wallets/consensys-wallet.js new file mode 100644 index 000000000..9f9f9d5fa --- /dev/null +++ b/js/src/util/wallets/consensys-wallet.js @@ -0,0 +1,354 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import BigNumber from 'bignumber.js'; + +import Abi from '~/abi'; +import Contract from '~/api/contract'; +import { toHex } from '~/api/util/format'; + +import WalletAbi from '~/contracts/abi/consensys-multisig-wallet.json'; + +import { + UPDATE_OWNERS, + UPDATE_REQUIRE, + UPDATE_TRANSACTIONS, + UPDATE_CONFIRMATIONS +} from './updates'; + +const WALLET_CONTRACT = new Contract({}, WalletAbi); +const WALLET_ABI = new Abi(WalletAbi); + +const walletEvents = WALLET_ABI.events.reduce((events, event) => { + events[event.name] = event; + return events; +}, {}); + +const WalletSignatures = { + Confirmation: toHex(walletEvents.Confirmation.signature), + Revocation: toHex(walletEvents.Revocation.signature), + Deposit: toHex(walletEvents.Deposit.signature), + Execution: toHex(walletEvents.Execution.signature), + OwnerAddition: toHex(walletEvents.OwnerAddition.signature), + OwnerRemoval: toHex(walletEvents.OwnerRemoval.signature), + RequirementChange: toHex(walletEvents.RequirementChange.signature), + Submission: toHex(walletEvents.Submission.signature) +}; + +export default class ConsensysWalletUtils { + static fetchOwners (inWallet) { + const wallet = new Contract(inWallet.api, WalletAbi).at(inWallet.address); + + return wallet.instance.getOwners.call() + .then((owners) => { + return owners.map((token) => token.value); + }); + } + + static fetchPendingTransactions (inWallet) { + const wallet = new Contract(inWallet.api, WalletAbi).at(inWallet.address); + + let transactions; + let txIds; + + // Get pending and not exectued transactions + return wallet.instance.getTransactionCount + .call({}, [ true, false ]) + .then((txCount) => { + // Get all the pending transactions + const fromId = 0; + const toId = txCount; + + return wallet.instance.getTransactionIds + .call({}, [ fromId, toId, true, false ]); + }) + .then((_txIds) => { + txIds = _txIds.map((token) => token.value); + + const promises = txIds.map((txId) => { + return wallet.instance.transactions + .call({}, [ txId ]); + }); + + return Promise.all(promises); + }) + .then((transactions) => { + return transactions.map((transaction, index) => { + const [ destination, value, data ] = transaction; + const id = toHex(txIds[index]); + + return { + to: destination, + data, + value, + operation: id + }; + }); + }) + .then((_transactions) => { + transactions = _transactions; + + return wallet + .getAllLogs({ + topics: [ + WalletSignatures.Submission, + txIds.map((txId) => toHex(txId)) + ] + }); + }) + .then((logs) => { + transactions.forEach((tx) => { + const log = logs + .find((log) => { + const id = toHex(log.params.transactionId.value); + + return id === tx.operation; + }); + + if (!log) { + console.warn('could not find a Submission log for this operation', tx); + return; + } + + tx.transactionIndex = log.transactionIndex; + tx.transactionHash = log.transactionHash; + tx.blockNumber = log.blockNumber; + }); + + const confirmationsPromises = transactions.map((tx) => { + return wallet.instance.getConfirmations + .call({}, [ tx.operation ]) + .then((owners) => { + return owners.map((token) => token.value); + }); + }); + + return Promise.all(confirmationsPromises); + }) + .then((confirmations) => { + transactions.forEach((tx, index) => { + tx.confirmedBy = confirmations[index]; + }); + + return transactions; + }); + } + + static fetchRequire (inWallet) { + const wallet = new Contract(inWallet.api, WalletAbi).at(inWallet.address); + + return wallet.instance.required.call(); + } + + static fetchTransactions (inWallet) { + const wallet = new Contract(inWallet.api, WalletAbi).at(inWallet.address); + + let transactions; + let txIds; + + return wallet.instance.getTransactionCount + .call({}, [ false, true ]) + .then((txCount) => { + // Get the 20 last transactions + const fromId = Math.max(0, txCount - 20); + const toId = txCount; + + return wallet.instance.getTransactionIds + .call({}, [ fromId, toId, false, true ]); + }) + .then((_txIds) => { + txIds = _txIds.map((token) => token.value); + + const promises = txIds.map((txId) => { + return wallet.instance.transactions + .call({}, [ txId ]); + }); + + return Promise.all(promises); + }) + .then((transactions) => { + return transactions.map((transaction, index) => { + const [ destination, value, data, executed ] = transaction; + const id = toHex(txIds[index]); + + return { + destination, value, data, executed, id + }; + }); + }) + .then((_transactions) => { + transactions = _transactions; + + const depositLogs = wallet + .getAllLogs({ + topics: [ WalletSignatures.Deposit ] + }); + + const executionLogs = wallet + .getAllLogs({ + topics: [ WalletSignatures.Execution, txIds ] + }); + + return Promise.all([ depositLogs, executionLogs ]); + }) + .then(([ depositLogs, executionLogs ]) => { + const logs = [].concat(depositLogs, executionLogs); + + return logs.map((log) => { + const signature = toHex(log.topics[0]); + + const transaction = { + transactionHash: log.transactionHash, + blockNumber: log.blockNumber + }; + + if (signature === WalletSignatures.Deposit) { + transaction.from = log.params.sender.value; + transaction.value = log.params.value.value; + transaction.to = wallet.address; + } else { + const txId = toHex(log.params.transactionId.value); + const tx = transactions.find((tx) => tx.id === txId); + + transaction.from = wallet.address; + transaction.to = tx.destination; + transaction.value = tx.value; + transaction.data = tx.data; + transaction.operation = toHex(tx.id); + } + + return transaction; + }); + }); + } + + static getChangeMethod (api, address, change) { + const wallet = new Contract(api, WalletAbi).at(address); + const walletInstance = wallet.instance; + + let data = ''; + + if (change.type === 'require') { + const func = walletInstance.changeRequirement; + + data = wallet.getCallData(func, {}, [ change.value ]); + } + + if (change.type === 'add_owner') { + const func = walletInstance.addOwner; + + data = wallet.getCallData(func, {}, [ change.value ]); + } + + if (change.type === 'change_owner') { + const func = walletInstance.replaceOwner; + + data = wallet.getCallData(func, {}, [ change.value.from, change.value.to ]); + } + + if (change.type === 'remove_owner') { + const func = walletInstance.removeOwner; + + data = wallet.getCallData(func, {}, [ change.value ]); + } + + const method = walletInstance.submitTransaction; + const values = [ address, 0, data ]; + + return { method, values }; + } + + static getModifyOperationMethod (modification) { + switch (modification) { + case 'confirm': + return 'confirmTransaction'; + + case 'revoke': + return 'revokeConfirmation'; + + default: + return ''; + } + } + + static getSubmitMethod () { + return 'submitTransaction'; + } + + static getWalletContract (api) { + return new Contract(api, WalletAbi); + } + + static getWalletSignatures () { + return WalletSignatures; + } + + static fetchDailylimit () { + return { + last: new BigNumber(0), + limit: new BigNumber(0), + spent: new BigNumber(0) + }; + } + + static isWallet (api, address) { + const wallet = new Contract(api, WalletAbi).at(address); + + return ConsensysWalletUtils.fetchRequire(wallet) + .then((result) => { + if (!result || result.equals(0)) { + return false; + } + + return true; + }); + } + + static logToUpdate (log) { + const eventSignature = toHex(log.topics[0]); + + switch (eventSignature) { + case WalletSignatures.OwnerAddition: + case WalletSignatures.OwnerRemoval: + return { [ UPDATE_OWNERS ]: true }; + + case WalletSignatures.RequirementChange: + return { [ UPDATE_REQUIRE ]: true }; + + case WalletSignatures.Deposit: + case WalletSignatures.Execution: + return { [ UPDATE_TRANSACTIONS ]: true }; + + case WalletSignatures.Submission: + case WalletSignatures.Confirmation: + case WalletSignatures.Revocation: + const parsedLog = WALLET_CONTRACT.parseEventLogs([ log ])[0]; + const operation = toHex(parsedLog.params.transactionId.value); + + return { [ UPDATE_CONFIRMATIONS ]: operation }; + + default: + return {}; + } + } + + /** + * This type of wallet cannot create any contract... + */ + static parseTransactionLogs (api, options, rawLogs) { + return null; + } +} diff --git a/js/src/util/wallets/foundation-wallet.js b/js/src/util/wallets/foundation-wallet.js new file mode 100644 index 000000000..4fb1cfe22 --- /dev/null +++ b/js/src/util/wallets/foundation-wallet.js @@ -0,0 +1,500 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { range, uniq } from 'lodash'; + +import Abi from '~/abi'; +import Contract from '~/api/contract'; +import { bytesToHex, toHex } from '~/api/util/format'; +import { validateAddress } from '~/util/validation'; + +import WalletAbi from '~/contracts/abi/foundation-multisig-wallet.json'; +import OldWalletAbi from '~/contracts/abi/old-wallet.json'; + +import PendingContracts from './pending-contracts'; +import { + UPDATE_OWNERS, + UPDATE_REQUIRE, + UPDATE_TRANSACTIONS, + UPDATE_CONFIRMATIONS +} from './updates'; + +const WALLET_CONTRACT = new Contract({}, WalletAbi); +const WALLET_ABI = new Abi(WalletAbi); +const OLD_WALLET_ABI = new Abi(OldWalletAbi); + +const walletEvents = WALLET_ABI.events.reduce((events, event) => { + events[event.name] = event; + return events; +}, {}); + +const oldWalletEvents = OLD_WALLET_ABI.events.reduce((events, event) => { + events[event.name] = event; + return events; +}, {}); + +const WalletSignatures = { + OwnerChanged: toHex(walletEvents.OwnerChanged.signature), + OwnerAdded: toHex(walletEvents.OwnerAdded.signature), + OwnerRemoved: toHex(walletEvents.OwnerRemoved.signature), + RequirementChanged: toHex(walletEvents.RequirementChanged.signature), + Confirmation: toHex(walletEvents.Confirmation.signature), + Revoke: toHex(walletEvents.Revoke.signature), + Deposit: toHex(walletEvents.Deposit.signature), + SingleTransact: toHex(walletEvents.SingleTransact.signature), + MultiTransact: toHex(walletEvents.MultiTransact.signature), + ConfirmationNeeded: toHex(walletEvents.ConfirmationNeeded.signature), + + Old: { + SingleTransact: toHex(oldWalletEvents.SingleTransact.signature), + MultiTransact: toHex(oldWalletEvents.MultiTransact.signature) + } +}; + +export default class FoundationWalletUtils { + static fetchConfirmations (walletContract, operation, _owners = null) { + const ownersPromise = _owners + ? Promise.resolve(_owners) + : FoundationWalletUtils.fetchOwners(walletContract); + + return ownersPromise + .then((owners) => { + const promises = owners.map((owner) => { + return walletContract.instance.hasConfirmed.call({}, [ operation, owner ]); + }); + + return Promise + .all(promises) + .then((data) => { + return owners.filter((_, index) => data[index]); + }); + }); + } + + static fetchDailylimit (walletContract) { + const walletInstance = walletContract.instance; + + return Promise + .all([ + walletInstance.m_dailyLimit.call(), + walletInstance.m_spentToday.call(), + walletInstance.m_lastDay.call() + ]) + .then(([ limit, spent, last ]) => ({ + limit, spent, last + })); + } + + static fetchOwners (walletContract) { + const walletInstance = walletContract.instance; + + return walletInstance + .m_numOwners.call() + .then((mNumOwners) => { + const promises = range(mNumOwners.toNumber()) + .map((idx) => walletInstance.getOwner.call({}, [ idx ])); + + return Promise + .all(promises) + .then((_owners) => { + const owners = validateOwners(_owners); + + // If all owners are the zero account : must be Mist wallet contract + if (!owners) { + return fetchMistOwners(walletContract, mNumOwners.toNumber()); + } + + return owners; + }); + }); + } + + static fetchPendingTransactions (walletContract, cache = {}) { + const { owners, transactions } = cache; + + return walletContract + .instance + .ConfirmationNeeded + .getAllLogs() + .then((logs) => { + return logs.map((log) => ({ + initiator: log.params.initiator.value, + to: log.params.to.value, + data: log.params.data.value, + value: log.params.value.value, + operation: bytesToHex(log.params.operation.value), + transactionIndex: log.transactionIndex, + transactionHash: log.transactionHash, + blockNumber: log.blockNumber, + confirmedBy: [] + })); + }) + .then((logs) => { + return logs.sort((logA, logB) => { + const comp = logA.blockNumber.comparedTo(logB.blockNumber); + + if (comp !== 0) { + return comp; + } + + return logA.transactionIndex.comparedTo(logB.transactionIndex); + }); + }) + .then((pendingTxs) => { + if (pendingTxs.length === 0) { + return pendingTxs; + } + + // Only fetch confirmations for operations not + // yet confirmed (ie. not yet a transaction) + if (transactions) { + const operations = transactions + .filter((t) => t.operation) + .map((t) => t.operation); + + return pendingTxs.filter((pendingTx) => { + return !operations.includes(pendingTx.operation); + }); + } + + return pendingTxs; + }) + .then((pendingTxs) => { + const promises = pendingTxs.map((tx) => { + return FoundationWalletUtils + .fetchConfirmations(walletContract, tx.operation, owners) + .then((confirmedBy) => { + tx.confirmedBy = confirmedBy; + + return tx; + }); + }); + + return Promise.all(promises); + }); + } + + static fetchRequire (wallet) { + return wallet.instance.m_required.call(); + } + + static fetchTransactions (walletContract) { + const { api } = walletContract; + + return walletContract + .getAllLogs({ + topics: [ [ + WalletSignatures.SingleTransact, + WalletSignatures.MultiTransact, + WalletSignatures.Deposit, + WalletSignatures.Old.SingleTransact, + WalletSignatures.Old.MultiTransact + ] ] + }) + .then((logs) => { + const transactions = logs.map((log) => { + const signature = toHex(log.topics[0]); + + const value = log.params.value.value; + const from = signature === WalletSignatures.Deposit + ? log.params['_from'].value + : walletContract.address; + + const to = signature === WalletSignatures.Deposit + ? walletContract.address + : log.params.to.value; + + const transaction = { + transactionHash: log.transactionHash, + blockNumber: log.blockNumber, + from, to, value + }; + + if (log.params.created && log.params.created.value && !/^(0x)?0*$/.test(log.params.created.value)) { + transaction.creates = log.params.created.value; + delete transaction.to; + } + + if (log.params.operation) { + transaction.operation = bytesToHex(log.params.operation.value); + checkPendingOperation(api, log, transaction.operation); + } + + if (log.params.data) { + transaction.data = log.params.data.value; + } + + return transaction; + }); + + return transactions; + }); + } + + static getChangeMethod (api, address, change) { + const wallet = new Contract(api, WalletAbi).at(address); + const walletInstance = wallet.instance; + + if (change.type === 'require') { + return { + method: walletInstance.changeRequirement, + values: [ change.value ] + }; + } + + if (change.type === 'dailylimit') { + return { + method: walletInstance.setDailyLimit, + values: [ change.value ] + }; + } + + if (change.type === 'add_owner') { + return { + method: walletInstance.addOwner, + values: [ change.value ] + }; + } + + if (change.type === 'change_owner') { + return { + method: walletInstance.changeOwner, + values: [ change.value.from, change.value.to ] + }; + } + + if (change.type === 'remove_owner') { + return { + method: walletInstance.removeOwner, + values: [ change.value ] + }; + } + } + + static getModifyOperationMethod (modification) { + switch (modification) { + case 'confirm': + return 'confirm'; + + case 'revoke': + return 'revoke'; + + default: + return ''; + } + } + + static getSubmitMethod () { + return 'execute'; + } + + static getWalletContract (api) { + return new Contract(api, WalletAbi); + } + + static getWalletSignatures () { + return WalletSignatures; + } + + static isWallet (api, address) { + const walletContract = new Contract(api, WalletAbi); + + return walletContract + .at(address) + .instance + .m_numOwners + .call() + .then((result) => { + if (!result || result.equals(0)) { + return false; + } + + return true; + }); + } + + static logToUpdate (log) { + const eventSignature = toHex(log.topics[0]); + + switch (eventSignature) { + case WalletSignatures.OwnerChanged: + case WalletSignatures.OwnerAdded: + case WalletSignatures.OwnerRemoved: + return { [ UPDATE_OWNERS ]: true }; + + case WalletSignatures.RequirementChanged: + return { [ UPDATE_REQUIRE ]: true }; + + case WalletSignatures.ConfirmationNeeded: + case WalletSignatures.Confirmation: + case WalletSignatures.Revoke: + const parsedLog = WALLET_CONTRACT.parseEventLogs([ log ])[0]; + const operation = bytesToHex(parsedLog.params.operation.value); + + return { [ UPDATE_CONFIRMATIONS ]: operation }; + + case WalletSignatures.Deposit: + case WalletSignatures.SingleTransact: + case WalletSignatures.MultiTransact: + case WalletSignatures.Old.SingleTransact: + case WalletSignatures.Old.MultiTransact: + return { [ UPDATE_TRANSACTIONS ]: true }; + + default: + return {}; + } + } + + static parseLogs (api, logs = []) { + const walletContract = new Contract(api, WalletAbi); + + return walletContract.parseEventLogs(logs); + } + + static parseTransactionLogs (api, options, rawLogs) { + const { metadata } = options; + const address = options.from; + const logs = FoundationWalletUtils.parseLogs(api, rawLogs); + + const confirmationLog = logs.find((log) => log.event === 'ConfirmationNeeded'); + const transactionLog = logs.find((log) => log.event === 'SingleTransact'); + + if (!confirmationLog && !transactionLog) { + return null; + } + + // Confirmations are needed from the other owners + if (confirmationLog) { + const operationHash = bytesToHex(confirmationLog.params.operation.value); + + // Add the contract to pending contracts + PendingContracts.addPendingContract(address, operationHash, metadata); + + return { pending: true }; + } + + return { contractAddress: transactionLog.params.created.value }; + } +} + +function checkPendingOperation (api, log, operation) { + const pendingContracts = PendingContracts.getPendingContracts(); + + // Add the pending contract to the contracts + if (pendingContracts[operation]) { + const { metadata } = pendingContracts[operation]; + const contractName = metadata.name; + + metadata.blockNumber = log.blockNumber; + + // The contract creation might not be in the same log, + // but must be in the same transaction (eg. Contract creation + // from Wallet within a Wallet) + api.eth + .getTransactionReceipt(log.transactionHash) + .then((transactionReceipt) => { + const transactionLogs = FoundationWalletUtils.parseLogs(api, transactionReceipt.logs); + const creationLog = transactionLogs.find((log) => { + return log.params.created && !/^(0x)?0*$/.test(log.params.created.value); + }); + + if (!creationLog) { + return false; + } + + const contractAddress = creationLog.params.created.value; + + return Promise + .all([ + api.parity.setAccountName(contractAddress, contractName), + api.parity.setAccountMeta(contractAddress, metadata) + ]) + .then(() => { + PendingContracts.removePendingContract(operation); + }); + }) + .catch((error) => { + console.error('adding wallet contract', error); + }); + } +} + +function fetchMistOwners (walletContract, mNumOwners) { + const walletAddress = walletContract.address; + + return getMistOwnersOffset(walletContract) + .then((result) => { + if (!result || result.offset === -1) { + return []; + } + + const owners = [ result.address ]; + + if (mNumOwners === 1) { + return owners; + } + + const initOffset = result.offset + 1; + let promise = Promise.resolve(); + + range(initOffset, initOffset + mNumOwners - 1).forEach((offset) => { + promise = promise + .then(() => { + return walletContract.api.eth.getStorageAt(walletAddress, offset); + }) + .then((result) => { + const resultAddress = '0x' + (result || '').slice(-40); + const { address } = validateAddress(resultAddress); + + owners.push(address); + }); + }); + + return promise.then(() => owners); + }); +} + +function getMistOwnersOffset (walletContract, offset = 3) { + return walletContract.api.eth + .getStorageAt(walletContract.address, offset) + .then((result) => { + if (result && !/^(0x)?0*$/.test(result)) { + const resultAddress = '0x' + result.slice(-40); + const { address, addressError } = validateAddress(resultAddress); + + if (!addressError) { + return { offset, address }; + } + } + + if (offset >= 100) { + return { offset: -1 }; + } + + return getMistOwnersOffset(walletContract, offset + 1); + }); +} + +function validateOwners (owners) { + const uniqOwners = uniq(owners); + + // If all owners are the zero account : must be Mist wallet contract + if (uniqOwners.length === 1 && /^(0x)?0*$/.test(owners[0])) { + return null; + } + + return uniqOwners; +} diff --git a/js/src/util/wallets/pending-contracts.js b/js/src/util/wallets/pending-contracts.js new file mode 100644 index 000000000..8eef273e2 --- /dev/null +++ b/js/src/util/wallets/pending-contracts.js @@ -0,0 +1,49 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import store from 'store'; + +const LS_PENDING_CONTRACTS_KEY = '_parity::wallets::pendingContracts'; + +export default class PendingContracts { + static getPendingContracts () { + return store.get(LS_PENDING_CONTRACTS_KEY) || {}; + } + + static setPendingContracts (contracts = {}) { + return store.set(LS_PENDING_CONTRACTS_KEY, contracts); + } + + static removePendingContract (operationHash) { + const nextContracts = PendingContracts.getPendingContracts(); + + delete nextContracts[operationHash]; + PendingContracts.setPendingContracts(nextContracts); + } + + static addPendingContract (address, operationHash, metadata) { + const nextContracts = { + ...PendingContracts.getPendingContracts(), + [ operationHash ]: { + address, + metadata, + operationHash + } + }; + + PendingContracts.setPendingContracts(nextContracts); + } +} diff --git a/js/src/util/wallets/updates.js b/js/src/util/wallets/updates.js new file mode 100644 index 000000000..a739652fc --- /dev/null +++ b/js/src/util/wallets/updates.js @@ -0,0 +1,21 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export const UPDATE_OWNERS = 'owners'; +export const UPDATE_REQUIRE = 'require'; +export const UPDATE_DAILYLIMIT = 'dailylimit'; +export const UPDATE_TRANSACTIONS = 'transactions'; +export const UPDATE_CONFIRMATIONS = 'confirmations'; diff --git a/js/src/views/Home/Urls/urls.css b/js/src/views/Home/Urls/urls.css index 5b5deeb5b..81d3e8dc2 100644 --- a/js/src/views/Home/Urls/urls.css +++ b/js/src/views/Home/Urls/urls.css @@ -47,6 +47,7 @@ color: rgb(0, 151, 167); overflow: hidden; text-overflow: ellipsis; + cursor: pointer; } .timestamp { diff --git a/js/src/views/Home/Urls/urls.js b/js/src/views/Home/Urls/urls.js index 7ded1ecda..acf4ae36f 100644 --- a/js/src/views/Home/Urls/urls.js +++ b/js/src/views/Home/Urls/urls.js @@ -127,7 +127,7 @@ export default class Urls extends Component { this.props.store.gotoUrl(url); if (extensionStore.hasExtension) { - window.open(this.props.store.currentUrl, '_blank'); + window.open(url, '_blank'); } else { router.push('/web'); } diff --git a/js/src/views/Web/store.js b/js/src/views/Web/store.js index 9dd8ea3fe..99b9b4b48 100644 --- a/js/src/views/Web/store.js +++ b/js/src/views/Web/store.js @@ -64,12 +64,9 @@ export default class Store { if (!hasProtocol.test(url)) { url = `https://${url}`; } - + this.setNextUrl(url); return this.generateToken(url).then(() => { - transaction(() => { - this.setNextUrl(url); - this.setCurrentUrl(this.nextUrl); - }); + this.setCurrentUrl(this.nextUrl); }); } diff --git a/js/src/views/Web/web.css b/js/src/views/Web/web.css index 3ea68bb6c..94e9d8e6f 100644 --- a/js/src/views/Web/web.css +++ b/js/src/views/Web/web.css @@ -21,10 +21,39 @@ width: 100%; } +:root .warningClose { + text-align: right; + a { + color: #fff; + text-decoration: underline; + } +} + +.warning { + color: #fff; + background: #f80; + position: fixed; + bottom: 0; + left: 0; + right: 50%; + padding: 1.5em; + z-index: 100; + animation: fadein 0.3s; +} + +@keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + .loading { text-align: center; margin-top: 5em; - color: #999; + color: #444; font-size: 2em; } diff --git a/js/src/views/Web/web.js b/js/src/views/Web/web.js index 8e81b43d5..9e3dbca54 100644 --- a/js/src/views/Web/web.js +++ b/js/src/views/Web/web.js @@ -35,6 +35,10 @@ export default class Web extends Component { store = Store.get(this.context.api); + state = { + isWarningDismissed: false + } + componentDidMount () { this.store.gotoUrl(this.props.params.url); } @@ -83,10 +87,36 @@ export default class Web extends Component { scrolling='auto' src={ encodedPath } /> + { this.renderWarning() } ); } + renderWarning () { + if (this.state.isWarningDismissed) { + return null; + } + + return ( +
+

+ WARNING: The web browser dapp is not safe as a general purpose browser. + Make sure to only visit web3-enabled sites that you trust. + Do not use it to browse web2.0 and never log in to any service - web3 dapps should not require that. +

+
+ Okay! +
+
+ ); + } + + dismissWarning = () => { + this.setState({ + isWarningDismissed: true + }); + }; + iframeOnLoad = () => { this.store.setLoading(false); }; diff --git a/parity/cli/mod.rs b/parity/cli/mod.rs index 3b2b4cd48..b27809970 100644 --- a/parity/cli/mod.rs +++ b/parity/cli/mod.rs @@ -194,7 +194,7 @@ usage! { or |c: &Config| otry!(c.websockets).interface.clone(), flag_ws_apis: String = "web3,eth,pubsub,net,parity,parity_pubsub,traces,rpc,secretstore", or |c: &Config| otry!(c.websockets).apis.as_ref().map(|vec| vec.join(",")), - flag_ws_origins: String = "chrome-extension://*", + flag_ws_origins: String = "chrome-extension://*,moz-extension://*", or |c: &Config| otry!(c.websockets).origins.as_ref().map(|vec| vec.join(",")), flag_ws_hosts: String = "none", or |c: &Config| otry!(c.websockets).hosts.as_ref().map(|vec| vec.join(",")), diff --git a/parity/configuration.rs b/parity/configuration.rs index 09bac053d..5815230dd 100644 --- a/parity/configuration.rs +++ b/parity/configuration.rs @@ -1265,7 +1265,7 @@ mod tests { interface: "127.0.0.1".into(), port: 8546, apis: ApiSet::UnsafeContext, - origins: Some(vec!["chrome-extension://*".into()]), + origins: Some(vec!["chrome-extension://*".into(), "moz-extension://*".into()]), hosts: Some(vec![]), signer_path: expected.into(), ui_address: Some(("127.0.0.1".to_owned(), 8180)), diff --git a/parity/rpc.rs b/parity/rpc.rs index 56d002cae..d919a8496 100644 --- a/parity/rpc.rs +++ b/parity/rpc.rs @@ -159,7 +159,7 @@ impl Default for WsConfiguration { interface: "127.0.0.1".into(), port: 8546, apis: ApiSet::UnsafeContext, - origins: Some(vec!["chrome-extension://*".into()]), + origins: Some(vec!["chrome-extension://*".into(), "moz-extension://*".into()]), hosts: Some(Vec::new()), signer_path: replace_home(&data_dir, "$BASE/signer").into(), support_token_api: true,