// 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 { observable, computed, autorun, action } from 'mobx'; import phone from 'phoneformat.js'; import { sha3 } from '../../api/util/sha3'; import ABI from '../../contracts/abi/sms-verification.json'; // TODO: move this to a better place const contract = '0xcE381B876A85A72303f7cA7b3a012f58F4CEEEeB'; import checkIfVerified from './check-if-verified'; import checkIfRequested from './check-if-requested'; import checkIfTxFailed from './check-if-tx-failed'; import waitForConfirmations from './wait-for-confirmations'; import postToVerificationServer from './post-to-verification-server'; const validCode = /^[A-Z\s]+$/i; export const GATHERING_DATA = 'gathering-data'; export const GATHERED_DATA = 'gathered-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'; export const DONE = 'done'; export default class VerificationStore { @observable step = null; @observable error = null; @observable fee = null; @observable isVerified = null; @observable hasRequested = null; @observable consentGiven = false; @observable number = ''; @observable requestTx = null; @observable code = ''; @observable confirmationTx = null; @computed get isCodeValid () { return validCode.test(this.code); } @computed get isNumberValid () { return phone.isValidNumber(this.number); } @computed get isStepValid () { if (this.step === DONE) { return true; } if (this.error) { return false; } switch (this.step) { case GATHERED_DATA: return this.fee && this.isVerified === false && this.isNumberValid && this.consentGiven; case REQUESTED_SMS: return this.requestTx; case QUERY_CODE: return this.isCodeValid; case POSTED_CONFIRMATION: return this.confirmationTx; default: return false; } } constructor (api, account) { this.api = api; this.account = account; this.contract = api.newContract(ABI, contract); autorun(() => { if (this.error) { console.error('sms verification: ' + this.error); } }); } @action setNumber = (number) => { this.number = number; } @action setConsentGiven = (consentGiven) => { this.consentGiven = consentGiven; } @action setCode = (code) => { this.code = code; } @action gatherData = () => { const { contract, account } = this; this.step = GATHERING_DATA; const fee = contract.instance.fee.call() .then((fee) => { this.fee = fee; }) .catch((err) => { this.error = 'Failed to fetch the fee: ' + err.message; }); const isVerified = checkIfVerified(contract, account) .then((isVerified) => { this.isVerified = isVerified; }) .catch((err) => { this.error = 'Failed to check if verified: ' + err.message; }); const hasRequested = checkIfRequested(contract, account) .then((txHash) => { this.hasRequested = !!txHash; if (txHash) { this.requestTx = txHash; } }) .catch((err) => { this.error = 'Failed to check if requested: ' + err.message; }); Promise .all([ fee, isVerified, hasRequested ]) .then(() => { this.step = GATHERED_DATA; }); } @action sendRequest = () => { const { api, account, contract, fee, number, hasRequested } = this; const request = contract.functions.find((fn) => fn.name === 'request'); const options = { from: account, value: fee.toString() }; let chain = Promise.resolve(); if (!hasRequested) { this.step = POSTING_REQUEST; chain = request.estimateGas(options, []) .then((gas) => { options.gas = gas.mul(1.2).toFixed(0); return request.postTransaction(options, []); }) .then((handle) => { // TODO: The "request rejected" error doesn't have any property to // distinguish it from other errors, so we can't give a meaningful error here. return api.pollMethod('parity_checkRequest', handle); }) .then((txHash) => { this.requestTx = txHash; return checkIfTxFailed(api, txHash, options.gas) .then((hasFailed) => { if (hasFailed) { throw new Error('Transaction failed, all gas used up.'); } this.step = POSTED_REQUEST; return waitForConfirmations(api, txHash, 1); }); }); } chain .then(() => { this.step = REQUESTING_SMS; return postToVerificationServer({ number, address: account }); }) .then(() => { this.step = REQUESTED_SMS; }) .catch((err) => { this.error = 'Failed to request a confirmation SMS: ' + err.message; }); } @action queryCode = () => { this.step = QUERY_CODE; } @action sendConfirmation = () => { const { api, account, contract, code } = this; const token = sha3(code); const confirm = contract.functions.find((fn) => fn.name === 'confirm'); const options = { from: account }; const values = [ token ]; this.step = POSTING_CONFIRMATION; confirm.estimateGas(options, values) .then((gas) => { options.gas = gas.mul(1.2).toFixed(0); return confirm.postTransaction(options, values); }) .then((handle) => { // TODO: The "request rejected" error doesn't have any property to // distinguish it from other errors, so we can't give a meaningful error here. return api.pollMethod('parity_checkRequest', handle); }) .then((txHash) => { this.confirmationTx = txHash; return checkIfTxFailed(api, txHash, options.gas) .then((hasFailed) => { if (hasFailed) { throw new Error('Transaction failed, all gas used up.'); } this.step = POSTED_CONFIRMATION; return waitForConfirmations(api, txHash, 1); }); }) .then(() => { this.step = DONE; }) .catch((err) => { this.error = 'Failed to send the verification code: ' + err.message; }); } @action done = () => { this.step = DONE; } }