From 5f570edf3b431f2ef4b720d91660ad73502717dd Mon Sep 17 00:00:00 2001 From: Jannis Redmann Date: Mon, 28 Nov 2016 17:39:55 +0100 Subject: [PATCH] update SMS verification (#3579) * add isTestnet helper * sms verification: use different port on testnet * subscribeToEvent helper * sms verification: await Puzzled event * sms verification: bugfixes :bug:, move awaitPuzzle * sms verification: check upfront if code is valid * sms verification: more helpful phone input label * isTestnet helper -> redux state --- js/package.json | 1 + js/src/3rdparty/sms-verification/index.js | 5 +- js/src/contracts/abi/sms-verification.json | 2 +- js/src/contracts/sms-verification.js | 35 +++++++++ .../SMSVerification/GatherData/gatherData.js | 2 +- .../modals/SMSVerification/SMSVerification.js | 4 +- .../SendRequest/sendRequest.js | 2 +- js/src/modals/SMSVerification/store.js | 47 +++++++---- js/src/util/is-testnet.js | 19 +++++ js/src/util/subscribe-to-event.js | 77 +++++++++++++++++++ js/src/views/Account/account.js | 6 +- 11 files changed, 177 insertions(+), 23 deletions(-) create mode 100644 js/src/util/is-testnet.js create mode 100644 js/src/util/subscribe-to-event.js diff --git a/js/package.json b/js/package.json index 8a3f6e421..912bff007 100644 --- a/js/package.json +++ b/js/package.json @@ -128,6 +128,7 @@ "es6-error": "^4.0.0", "es6-promise": "^3.2.1", "ethereumjs-tx": "^1.1.2", + "eventemitter3": "^2.0.2", "file-saver": "^1.3.3", "format-json": "^1.0.3", "format-number": "^2.0.1", diff --git a/js/src/3rdparty/sms-verification/index.js b/js/src/3rdparty/sms-verification/index.js index 9b113f364..c50b2331a 100644 --- a/js/src/3rdparty/sms-verification/index.js +++ b/js/src/3rdparty/sms-verification/index.js @@ -27,9 +27,10 @@ export const termsOfService = ( ); -export const postToServer = (query) => { +export const postToServer = (query, isTestnet = false) => { + const port = isTestnet ? 8443 : 443; query = stringify(query); - return fetch('https://sms-verification.parity.io/?' + query, { + return fetch(`https://sms-verification.parity.io:${port}/?` + query, { method: 'POST', mode: 'cors', cache: 'no-store' }) .then((res) => { diff --git a/js/src/contracts/abi/sms-verification.json b/js/src/contracts/abi/sms-verification.json index 400d22b44..d6852b182 100644 --- a/js/src/contracts/abi/sms-verification.json +++ b/js/src/contracts/abi/sms-verification.json @@ -1 +1 @@ -[{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"certify","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"request","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"},{"name":"_puzzle","type":"bytes32"}],"name":"puzzle","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"revoke","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes32"}],"name":"confirm","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"delegate","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setDelegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"}],"name":"certified","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Requested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"puzzle","type":"bytes32"}],"name":"Puzzled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Confirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Revoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}] +[{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"certify","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"request","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"},{"name":"_puzzle","type":"bytes32"}],"name":"puzzle","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"revoke","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes32"}],"name":"confirm","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"delegate","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setDelegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"}],"name":"certified","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Requested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"puzzle","type":"bytes32"}],"name":"Puzzled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Confirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Revoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}] diff --git a/js/src/contracts/sms-verification.js b/js/src/contracts/sms-verification.js index 2d32556ea..34a6bad76 100644 --- a/js/src/contracts/sms-verification.js +++ b/js/src/contracts/sms-verification.js @@ -14,6 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import subscribeToEvent from '../util/subscribe-to-event'; + export const checkIfVerified = (contract, account) => { return contract.instance.certified.call({}, [account]); }; @@ -50,3 +52,36 @@ export const checkIfRequested = (contract, account) => { }); }); }; + +const blockNumber = (api) => { + return new Promise((resolve, reject) => { + api.subscribe('eth_blockNumber', (err, block) => { + if (err) { + return reject(err); + } + resolve(block); + }) + .then((subscription) => { + api.unsubscribe(subscription); + }) + .catch(reject); + }); +}; + +export const awaitPuzzle = (api, contract, account) => { + return blockNumber(api) + .then((block) => { + return new Promise((resolve, reject) => { + const subscription = subscribeToEvent(contract, 'Puzzled', { + from: block.toNumber(), + filter: (log) => log.params.who.value === account + }); + subscription.once('error', reject); + subscription.once('log', subscription.unsubscribe); + subscription.once('log', resolve); + subscription.once('timeout', () => { + reject(new Error('Timed out waiting for the puzzle.')); + }); + }); + }); +}; diff --git a/js/src/modals/SMSVerification/GatherData/gatherData.js b/js/src/modals/SMSVerification/GatherData/gatherData.js index 3620de904..3d90fa2ba 100644 --- a/js/src/modals/SMSVerification/GatherData/gatherData.js +++ b/js/src/modals/SMSVerification/GatherData/gatherData.js @@ -53,7 +53,7 @@ export default class GatherData extends Component { { this.renderCertified() } { this.renderRequested() } Requesting an SMS from the Parity server.

+

Requesting an SMS from the Parity server and waiting for the puzzle to be put into the contract.

); default: diff --git a/js/src/modals/SMSVerification/store.js b/js/src/modals/SMSVerification/store.js index 8c4db373a..76045d814 100644 --- a/js/src/modals/SMSVerification/store.js +++ b/js/src/modals/SMSVerification/store.js @@ -20,19 +20,16 @@ import { sha3 } from '../../api/util/sha3'; import Contracts from '../../contracts'; -import { checkIfVerified, checkIfRequested } from '../../contracts/sms-verification'; +import { checkIfVerified, checkIfRequested, awaitPuzzle } from '../../contracts/sms-verification'; import { postToServer } from '../../3rdparty/sms-verification'; import checkIfTxFailed from '../../util/check-if-tx-failed'; import waitForConfirmations from '../../util/wait-for-block-confirmations'; -const validCode = /^[A-Z\s]+$/i; - export const LOADING = 'fetching-contract'; export const QUERY_DATA = 'query-data'; export const POSTING_REQUEST = 'posting-request'; export const POSTED_REQUEST = 'posted-request'; export const REQUESTING_SMS = 'requesting-sms'; -export const REQUESTED_SMS = 'requested-sms'; export const QUERY_CODE = 'query-code'; export const POSTING_CONFIRMATION = 'posting-confirmation'; export const POSTED_CONFIRMATION = 'posted-confirmation'; @@ -50,11 +47,9 @@ export default class VerificationStore { @observable number = ''; @observable requestTx = null; @observable code = ''; + @observable isCodeValid = null; @observable confirmationTx = null; - @computed get isCodeValid () { - return validCode.test(this.code); - } @computed get isNumberValid () { return phone.isValidNumber(this.number); } @@ -72,20 +67,19 @@ export default class VerificationStore { return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null; case QUERY_DATA: return this.isNumberValid && this.consentGiven; - case REQUESTED_SMS: - return this.requestTx; case QUERY_CODE: - return this.isCodeValid; + return this.requestTx && this.isCodeValid === true; case POSTED_CONFIRMATION: - return this.confirmationTx; + return !!this.confirmationTx; default: return false; } } - constructor (api, account) { + constructor (api, account, isTestnet) { this.api = api; this.account = account; + this.isTestnet = isTestnet; this.step = LOADING; Contracts.create(api).registry.getContract('smsverification') @@ -151,7 +145,26 @@ export default class VerificationStore { } @action setCode = (code) => { + const { contract, account } = this; + if (!contract || !account || code.length === 0) return; + + const confirm = contract.functions.find((fn) => fn.name === 'confirm'); + const options = { from: account }; + const values = [ sha3(code) ]; + this.code = code; + this.isCodeValid = null; + confirm.estimateGas(options, values) + .then((gas) => { + options.gas = gas.mul(1.2).toFixed(0); + return confirm.call(options, values); + }) + .then((result) => { + this.isCodeValid = result === true; + }) + .catch((err) => { + this.error = 'Failed to check if the code is valid: ' + err.message; + }); } @action sendRequest = () => { @@ -188,11 +201,15 @@ export default class VerificationStore { chain .then(() => { - this.step = REQUESTING_SMS; - return postToServer({ number, address: account }); + return api.parity.netChain(); }) + .then((chain) => { + this.step = REQUESTING_SMS; + return postToServer({ number, address: account }, this.isTestnet); + }) + .then(() => awaitPuzzle(api, contract, account)) .then(() => { - this.step = REQUESTED_SMS; + this.step = QUERY_CODE; }) .catch((err) => { this.error = 'Failed to request a confirmation SMS: ' + err.message; diff --git a/js/src/util/is-testnet.js b/js/src/util/is-testnet.js new file mode 100644 index 000000000..c2bf2c450 --- /dev/null +++ b/js/src/util/is-testnet.js @@ -0,0 +1,19 @@ +// Copyright 2015, 2016 Ethcore (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 default (chain) => { + return chain === 'morden' || chain === 'ropsten' || chain === 'testnet'; +}; diff --git a/js/src/util/subscribe-to-event.js b/js/src/util/subscribe-to-event.js new file mode 100644 index 000000000..3313404c5 --- /dev/null +++ b/js/src/util/subscribe-to-event.js @@ -0,0 +1,77 @@ +// Copyright 2015, 2016 Ethcore (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 EventEmitter from 'eventemitter3'; + +const defaults = { + from: 0, // TODO + to: 'latest', + timeout: null, + filter: () => true +}; + +const subscribeToEvent = (contract, name, opt = {}) => { + opt = Object.assign({}, defaults, opt); + + let subscription = null; + let timeout = null; + + const unsubscribe = () => { + if (subscription) { + contract.unsubscribe(subscription); + subscription = null; + } + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + }; + + const emitter = new EventEmitter(); + emitter.unsubscribe = unsubscribe; + + if (typeof opt.timeout === 'number') { + timeout = setTimeout(() => { + unsubscribe(); + emitter.emit('timeout'); + }, opt.timeout); + } + + const callback = (err, logs) => { + if (err) { + return emitter.emit('error', err); + } + for (let log of logs) { + if (opt.filter(log)) { + emitter.emit('log', log); + } + } + }; + + contract.subscribe(name, { + fromBlock: opt.from, toBlock: opt.to + }, callback) + .then((_subscription) => { + subscription = _subscription; + }) + .catch((err) => { + emitter.emit('error', err); + }); + + return emitter; +}; + +export default subscribeToEvent; diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index 1181b7f73..e27333cbf 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -47,6 +47,7 @@ class Account extends Component { params: PropTypes.object, accounts: PropTypes.object, + isTestnet: PropTypes.bool, balances: PropTypes.object } @@ -65,8 +66,9 @@ class Account extends Component { componentDidMount () { const { api } = this.context; const { address } = this.props.params; + const { isTestnet } = this.props; - const verificationStore = new VerificationStore(api, address); + const verificationStore = new VerificationStore(api, address, isTestnet); this.setState({ verificationStore }); this.setVisibleAccounts(); } @@ -326,11 +328,13 @@ class Account extends Component { function mapStateToProps (state) { const { accounts } = state.personal; + const { isTest } = state.nodeStatus; const { balances } = state.balances; const { images } = state; return { accounts, + isTestnet: isTest, balances, images };