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