diff --git a/src/bridge/EthereumJSBridge.js b/src/bridge/EthereumJSBridge.js index 04844351..20be763d 100644 --- a/src/bridge/EthereumJSBridge.js +++ b/src/bridge/EthereumJSBridge.js @@ -1,4 +1,5 @@ // @flow +import { Observable } from 'rxjs' import React from 'react' import FeesField from 'components/FeesField/EthereumKind' import AdvancedOptions from 'components/AdvancedOptions/EthereumKind' @@ -93,6 +94,49 @@ function mergeOps(existing: Operation[], newFetched: Operation[]) { return uniqBy(all.sort((a, b) => b.date - a.date), 'id') } +const signAndBroadcast = async ({ + a, + t, + deviceId, + isCancelled, + onSigned, + onOperationBroadcasted, +}) => { + const api = apiForCurrency(a.currency) + + const nonce = await api.getAccountNonce(a.freshAddress) + + const transaction = await signTransactionCommand + .send({ + currencyId: a.currency.id, + devicePath: deviceId, + path: a.freshAddressPath, + transaction: { ...t, nonce }, + }) + .toPromise() + + if (!isCancelled()) { + onSigned() + + const hash = await api.broadcastTransaction(transaction) + + onOperationBroadcasted({ + id: `${a.id}-${hash}-OUT`, + hash, + type: 'OUT', + value: t.amount, + fee: t.gasPrice * t.gasLimit, + blockHeight: null, + blockHash: null, + accountId: a.id, + senders: [a.freshAddress], + recipients: [t.recipient], + transactionSequenceNumber: nonce, + date: new Date(), + }) + } +} + const SAFE_REORG_THRESHOLD = 80 const fetchCurrentBlock = (perCurrencyId => currency => { @@ -324,37 +368,28 @@ const EthereumBridge: WalletBridge = { getTotalSpent: (a, t) => Promise.resolve(t.amount + t.gasPrice * t.gasLimit), getMaxAmount: (a, t) => Promise.resolve(a.balance - t.gasPrice * t.gasLimit), - signAndBroadcast: async (a, t, deviceId) => { - const api = apiForCurrency(a.currency) - - const nonce = await api.getAccountNonce(a.freshAddress) - - const transaction = await signTransactionCommand - .send({ - currencyId: a.currency.id, - devicePath: deviceId, - path: a.freshAddressPath, - transaction: { ...t, nonce }, - }) - .toPromise() - - const hash = await api.broadcastTransaction(transaction) - - return { - id: `${a.id}-${hash}-OUT`, - hash, - type: 'OUT', - value: t.amount, - fee: t.gasPrice * t.gasLimit, - blockHeight: null, - blockHash: null, - accountId: a.id, - senders: [a.freshAddress], - recipients: [t.recipient], - transactionSequenceNumber: nonce, - date: new Date(), - } - }, + signAndBroadcast: (a, t, deviceId) => + Observable.create(o => { + let cancelled = false + const isCancelled = () => cancelled + const onSigned = () => { + o.next({ type: 'signed' }) + } + const onOperationBroadcasted = operation => { + o.next({ type: 'broadcasted', operation }) + } + signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }).then( + () => { + o.complete() + }, + e => { + o.error(e) + }, + ) + return () => { + cancelled = true + } + }), addPendingOperation: (account, operation) => ({ ...account, diff --git a/src/bridge/LibcoreBridge.js b/src/bridge/LibcoreBridge.js index 960ef946..30b4102a 100644 --- a/src/bridge/LibcoreBridge.js +++ b/src/bridge/LibcoreBridge.js @@ -17,6 +17,9 @@ type Transaction = { recipient: string, } +const decodeOperation = (encodedAccount, rawOp) => + decodeAccount({ ...encodedAccount, operations: [rawOp] }).operations[0] + const EditFees = ({ account, onChange, value }: EditProps) => ( { @@ -135,20 +138,27 @@ const LibcoreBridge: WalletBridge = { getMaxAmount: (a, _t) => Promise.resolve(a.balance), // FIXME - signAndBroadcast: async (account, transaction, deviceId) => { + signAndBroadcast: (account, transaction, deviceId) => { const encodedAccount = encodeAccount(account) - const rawOp = await libcoreSignAndBroadcast + return libcoreSignAndBroadcast .send({ account: encodedAccount, transaction, deviceId, }) - .toPromise() - - // quick HACK - const [op] = decodeAccount({ ...encodedAccount, operations: [rawOp] }).operations - - return op + .pipe( + map(e => { + switch (e.type) { + case 'broadcasted': + return { + type: 'broadcasted', + operation: decodeOperation(encodedAccount, e.operation), + } + default: + return e + } + }), + ) }, addPendingOperation: (account, operation) => ({ diff --git a/src/bridge/RippleJSBridge.js b/src/bridge/RippleJSBridge.js index 4fad0371..57463f2b 100644 --- a/src/bridge/RippleJSBridge.js +++ b/src/bridge/RippleJSBridge.js @@ -1,4 +1,5 @@ // @flow +import { Observable } from 'rxjs' import React from 'react' import bs58check from 'ripple-bs58check' import { computeBinaryTransactionHash } from 'ripple-hashes' @@ -43,6 +44,70 @@ const EditAdvancedOptions = ({ onChange, value }: EditProps) => ( /> ) +async function signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }) { + const api = apiForCurrency(a.currency) + try { + await api.connect() + const amount = formatAPICurrencyXRP(t.amount) + const payment = { + source: { + address: a.freshAddress, + amount, + }, + destination: { + address: t.recipient, + minAmount: amount, + tag: t.tag, + }, + } + const instruction = { + fee: formatAPICurrencyXRP(t.fee).value, + } + + const prepared = await api.preparePayment(a.freshAddress, payment, instruction) + + const transaction = await signTransaction + .send({ + currencyId: a.currency.id, + devicePath: deviceId, + path: a.freshAddressPath, + transaction: JSON.parse(prepared.txJSON), + }) + .toPromise() + + if (!isCancelled()) { + onSigned() + const submittedPayment = await api.submit(transaction) + + if (submittedPayment.resultCode !== 'tesSUCCESS') { + throw new Error(submittedPayment.resultMessage) + } + + const hash = computeBinaryTransactionHash(transaction) + + onOperationBroadcasted({ + id: `${a.id}-${hash}-OUT`, + hash, + accountId: a.id, + type: 'OUT', + value: t.amount, + fee: t.fee, + blockHash: null, + blockHeight: null, + senders: [a.freshAddress], + recipients: [t.recipient], + date: new Date(), + // we probably can't get it so it's a predictive value + transactionSequenceNumber: + (a.operations.length > 0 ? a.operations[0].transactionSequenceNumber : 0) + + a.pendingOperations.length, + }) + } + } finally { + api.disconnect() + } +} + function isRecipientValid(currency, recipient) { try { bs58check.decode(recipient) @@ -394,66 +459,28 @@ const RippleJSBridge: WalletBridge = { getMaxAmount: (a, t) => Promise.resolve(a.balance - t.fee), - signAndBroadcast: async (a, t, deviceId) => { - const api = apiForCurrency(a.currency) - try { - await api.connect() - const amount = formatAPICurrencyXRP(t.amount) - const payment = { - source: { - address: a.freshAddress, - amount, - }, - destination: { - address: t.recipient, - minAmount: amount, - tag: t.tag, - }, + signAndBroadcast: (a, t, deviceId) => + Observable.create(o => { + let cancelled = false + const isCancelled = () => cancelled + const onSigned = () => { + o.next({ type: 'signed' }) } - const instruction = { - fee: formatAPICurrencyXRP(t.fee).value, - } - - const prepared = await api.preparePayment(a.freshAddress, payment, instruction) - - const transaction = await signTransaction - .send({ - currencyId: a.currency.id, - devicePath: deviceId, - path: a.freshAddressPath, - transaction: JSON.parse(prepared.txJSON), - }) - .toPromise() - - const submittedPayment = await api.submit(transaction) - - if (submittedPayment.resultCode !== 'tesSUCCESS') { - throw new Error(submittedPayment.resultMessage) + const onOperationBroadcasted = operation => { + o.next({ type: 'broadcasted', operation }) } - - const hash = computeBinaryTransactionHash(transaction) - - return { - id: `${a.id}-${hash}-OUT`, - hash, - accountId: a.id, - type: 'OUT', - value: t.amount, - fee: t.fee, - blockHash: null, - blockHeight: null, - senders: [a.freshAddress], - recipients: [t.recipient], - date: new Date(), - // we probably can't get it so it's a predictive value - transactionSequenceNumber: - (a.operations.length > 0 ? a.operations[0].transactionSequenceNumber : 0) + - a.pendingOperations.length, + signAndBroadcast({ a, t, deviceId, isCancelled, onSigned, onOperationBroadcasted }).then( + () => { + o.complete() + }, + e => { + o.error(e) + }, + ) + return () => { + cancelled = true } - } finally { - api.disconnect() - } - }, + }), addPendingOperation: (account, operation) => ({ ...account, diff --git a/src/bridge/UnsupportedBridge.js b/src/bridge/UnsupportedBridge.js index 0a6b26b9..2f1bbce5 100644 --- a/src/bridge/UnsupportedBridge.js +++ b/src/bridge/UnsupportedBridge.js @@ -1,4 +1,5 @@ // @flow +import { Observable } from 'rxjs' import type { WalletBridge } from './types' const genericError = new Error('UnsupportedBridge') @@ -36,7 +37,10 @@ const UnsupportedBridge: WalletBridge<*> = { getMaxAmount: () => Promise.resolve(0), - signAndBroadcast: () => Promise.reject(genericError), + signAndBroadcast: () => + Observable.create(o => { + o.error(genericError) + }), } export default UnsupportedBridge diff --git a/src/bridge/makeMockBridge.js b/src/bridge/makeMockBridge.js index eb41396d..1358e01f 100644 --- a/src/bridge/makeMockBridge.js +++ b/src/bridge/makeMockBridge.js @@ -1,4 +1,5 @@ // @flow +import { Observable } from 'rxjs' import { genAccount, genAddingOperationsInAccount, @@ -149,20 +150,22 @@ function makeMockBridge(opts?: Opts): WalletBridge<*> { getMaxAmount, - signAndBroadcast: async (account, t) => { - const rng = new Prando() - const op = genOperation(account, account.operations, account.currency, rng) - op.type = 'OUT' - op.value = t.amount - op.blockHash = null - op.blockHeight = null - op.senders = [account.freshAddress] - op.recipients = [t.recipient] - op.blockHeight = account.blockHeight - op.date = new Date() - broadcasted[account.id] = (broadcasted[account.id] || []).concat(op) - return { ...op } - }, + signAndBroadcast: (account, t) => + Observable.create(o => { + const rng = new Prando() + const op = genOperation(account, account.operations, account.currency, rng) + op.type = 'OUT' + op.value = t.amount + op.blockHash = null + op.blockHeight = null + op.senders = [account.freshAddress] + op.recipients = [t.recipient] + op.blockHeight = account.blockHeight + op.date = new Date() + broadcasted[account.id] = (broadcasted[account.id] || []).concat(op) + o.next({ type: 'signed' }) + o.next({ type: 'broadcasted', operation: { ...op } }) + }), } } diff --git a/src/bridge/types.js b/src/bridge/types.js index 3b9d5e0c..c84758de 100644 --- a/src/bridge/types.js +++ b/src/bridge/types.js @@ -1,5 +1,6 @@ // @flow +import type { Observable } from 'rxjs' import type { Account, Operation, Currency } from '@ledgerhq/live-common/lib/types' // a WalletBridge is implemented on renderer side. @@ -104,7 +105,7 @@ export interface WalletBridge { account: Account, transaction: Transaction, deviceId: DeviceId, - ): Promise; + ): Observable<{ type: 'signed' } | { type: 'broadcasted', operation: Operation }>; // Implement an optimistic response for signAndBroadcast. // you likely should add the operation in account.pendingOperations but maybe you want to clean it (because maybe some are replaced / cancelled by this one?) diff --git a/src/commands/libcoreSignAndBroadcast.js b/src/commands/libcoreSignAndBroadcast.js index 79ef35ec..b5a92aad 100644 --- a/src/commands/libcoreSignAndBroadcast.js +++ b/src/commands/libcoreSignAndBroadcast.js @@ -2,9 +2,8 @@ import type { AccountRaw, OperationRaw } from '@ledgerhq/live-common/lib/types' import Btc from '@ledgerhq/hw-app-btc' -import { fromPromise } from 'rxjs/observable/fromPromise' +import { Observable } from 'rxjs' import { getCryptoCurrencyById } from '@ledgerhq/live-common/lib/helpers/currencies' -import type Transport from '@ledgerhq/hw-transport' import withLibcore from 'helpers/withLibcore' import { createCommand, Command } from 'helpers/ipc' @@ -23,24 +22,37 @@ type Input = { deviceId: string, } -type Result = $Exact +type Result = { type: 'signed' } | { type: 'broadcasted', operation: OperationRaw } const cmd: Command = createCommand( 'libcoreSignAndBroadcast', ({ account, transaction, deviceId }) => - fromPromise( - withDevice(deviceId)(transport => - withLibcore(core => - doSignAndBroadcast({ - account, - transaction, - deviceId, - core, - transport, - }), - ), - ), - ), + Observable.create(o => { + let unsubscribed = false + const isCancelled = () => unsubscribed + withLibcore(core => + doSignAndBroadcast({ + account, + transaction, + deviceId, + core, + isCancelled, + onSigned: () => { + o.next({ type: 'signed' }) + }, + onOperationBroadcasted: operation => { + o.next({ + type: 'broadcasted', + operation, + }) + }, + }), + ).then(() => o.complete(), e => o.error(e)) + + return () => { + unsubscribed = true + } + }), ) export async function doSignAndBroadcast({ @@ -48,60 +60,74 @@ export async function doSignAndBroadcast({ transaction, deviceId, core, - transport, + isCancelled, + onSigned, + onOperationBroadcasted, }: { account: AccountRaw, transaction: BitcoinLikeTransaction, deviceId: string, core: *, - transport: Transport<*>, -}) { - const hwApp = new Btc(transport) - - const WALLET_IDENTIFIER = await getWalletIdentifier({ - hwApp, - isSegwit: !!account.isSegwit, - currencyId: account.currencyId, - devicePath: deviceId, + isCancelled: () => boolean, + onSigned: () => void, + onOperationBroadcasted: (optimisticOp: $Exact) => void, +}): Promise { + let njsAccount + + const signedTransaction: ?string = await withDevice(deviceId)(async transport => { + const hwApp = new Btc(transport) + + const WALLET_IDENTIFIER = await getWalletIdentifier({ + hwApp, + isSegwit: !!account.isSegwit, + currencyId: account.currencyId, + devicePath: deviceId, + }) + + const njsWallet = await core.getWallet(WALLET_IDENTIFIER) + if (isCancelled()) return null + njsAccount = await njsWallet.getAccount(account.index) + if (isCancelled()) return null + const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() + const njsWalletCurrency = njsWallet.getCurrency() + const amount = core.createAmount(njsWalletCurrency, transaction.amount) + const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) + const transactionBuilder = bitcoinLikeAccount.buildTransaction() + + // TODO: check if is valid address. if not, it will fail silently on invalid + + transactionBuilder.sendToAddress(amount, transaction.recipient) + // TODO: don't use hardcoded value for sequence (and first also maybe) + transactionBuilder.pickInputs(0, 0xffffff) + transactionBuilder.setFeesPerByte(fees) + + const builded = await transactionBuilder.build() + if (isCancelled()) return null + const sigHashType = core.helpers.bytesToHex( + njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash, + ) + + const hasTimestamp = njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction + // TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay + + const currency = getCryptoCurrencyById(account.currencyId) + + return core.signTransaction({ + hwApp, + transaction: builded, + sigHashType: parseInt(sigHashType, 16).toString(), + supportsSegwit: !!currency.supportsSegwit, + isSegwit: account.isSegwit, + hasTimestamp, + }) }) - const njsWallet = await core.getWallet(WALLET_IDENTIFIER) - const njsAccount = await njsWallet.getAccount(account.index) - const bitcoinLikeAccount = njsAccount.asBitcoinLikeAccount() - const njsWalletCurrency = njsWallet.getCurrency() - const amount = core.createAmount(njsWalletCurrency, transaction.amount) - const fees = core.createAmount(njsWalletCurrency, transaction.feePerByte) - const transactionBuilder = bitcoinLikeAccount.buildTransaction() - - // TODO: check if is valid address. if not, it will fail silently on invalid - - transactionBuilder.sendToAddress(amount, transaction.recipient) - // TODO: don't use hardcoded value for sequence (and first also maybe) - transactionBuilder.pickInputs(0, 0xffffff) - transactionBuilder.setFeesPerByte(fees) - - const builded = await transactionBuilder.build() - const sigHashType = core.helpers.bytesToHex( - njsWalletCurrency.bitcoinLikeNetworkParameters.SigHash, - ) - - const hasTimestamp = njsWalletCurrency.bitcoinLikeNetworkParameters.UsesTimestampedTransaction - // TODO: const timestampDelay = njsWalletCurrency.bitcoinLikeNetworkParameters.TimestampDelay - - const currency = getCryptoCurrencyById(account.currencyId) - const signedTransaction = await core.signTransaction({ - hwApp, - transaction: builded, - sigHashType: parseInt(sigHashType, 16).toString(), - supportsSegwit: !!currency.supportsSegwit, - isSegwit: account.isSegwit, - hasTimestamp, - }) + if (!signedTransaction || isCancelled() || !njsAccount) return + onSigned() const txHash = await njsAccount.asBitcoinLikeAccount().broadcastRawTransaction(signedTransaction) - - // optimistic operation - return { + // NB we don't check isCancelled() because the broadcast is not cancellable now! + onOperationBroadcasted({ id: txHash, hash: txHash, type: 'OUT', @@ -113,7 +139,7 @@ export async function doSignAndBroadcast({ recipients: [transaction.recipient], accountId: account.id, date: new Date().toISOString(), - } + }) } export default cmd diff --git a/src/components/DeviceSignTransaction.js b/src/components/DeviceSignTransaction.js deleted file mode 100644 index fe1f35e0..00000000 --- a/src/components/DeviceSignTransaction.js +++ /dev/null @@ -1,42 +0,0 @@ -// @flow -import { PureComponent } from 'react' -import type { Account, Operation } from '@ledgerhq/live-common/lib/types' -import type { Device } from 'types/common' -import type { WalletBridge } from 'bridge/types' - -type Props = { - children: *, - onOperationBroadcasted: (op: Operation) => void, - onError: Error => void, - device: Device, - account: Account, - bridge: WalletBridge<*>, - transaction: *, -} - -class DeviceSignTransaction extends PureComponent { - componentDidMount() { - this.sign() - } - - componentWillUnmount() { - this.unmount = true - } - unmount = false - - sign = async () => { - const { device, account, transaction, bridge, onOperationBroadcasted, onError } = this.props - try { - const optimisticOperation = await bridge.signAndBroadcast(account, transaction, device.path) - onOperationBroadcasted(optimisticOperation) - } catch (error) { - onError(error) - } - } - - render() { - return this.props.children - } -} - -export default DeviceSignTransaction diff --git a/src/components/modals/Send/03-step-verification.js b/src/components/modals/Send/03-step-verification.js index d94677d4..e40c1fad 100644 --- a/src/components/modals/Send/03-step-verification.js +++ b/src/components/modals/Send/03-step-verification.js @@ -6,12 +6,9 @@ import styled from 'styled-components' import Box from 'components/base/Box' import WarnBox from 'components/WarnBox' import { multiline } from 'styles/helpers' -import DeviceSignTransaction from 'components/DeviceSignTransaction' import DeviceConfirm from 'components/DeviceConfirm' -import type { WalletBridge } from 'bridge/types' -import type { Account, Operation } from '@ledgerhq/live-common/lib/types' -import type { Device, T } from 'types/common' +import type { T } from 'types/common' const Container = styled(Box).attrs({ alignItems: 'center', @@ -30,43 +27,14 @@ const Info = styled(Box).attrs({ ` type Props = { - account: ?Account, - device: ?Device, - bridge: ?WalletBridge<*>, - transaction: *, - onOperationBroadcasted: (op: Operation) => void, - onError: (e: Error) => void, hasError: boolean, t: T, } -export default ({ - account, - device, - bridge, - transaction, - onOperationBroadcasted, - t, - onError, - hasError, -}: Props) => ( +export default ({ t, hasError }: Props) => ( {multiline(t('send:steps.verification.warning'))} {t('send:steps.verification.body')} - {account && - bridge && - transaction && - device && ( - - - - )} + ) diff --git a/src/components/modals/Send/04-step-confirmation.js b/src/components/modals/Send/04-step-confirmation.js index 50301761..dff95ee2 100644 --- a/src/components/modals/Send/04-step-confirmation.js +++ b/src/components/modals/Send/04-step-confirmation.js @@ -3,6 +3,7 @@ import React from 'react' import styled from 'styled-components' import type { Operation } from '@ledgerhq/live-common/lib/types' +import Spinner from 'components/base/Spinner' import IconCheckCircle from 'icons/CheckCircle' import IconExclamationCircleThin from 'icons/ExclamationCircleThin' import Box from 'components/base/Box' @@ -45,11 +46,17 @@ type Props = { function StepConfirmation(props: Props) { const { t, optimisticOperation, error } = props - const Icon = optimisticOperation ? IconCheckCircle : IconExclamationCircleThin - const iconColor = optimisticOperation ? colors.positiveGreen : colors.alertRed + const Icon = optimisticOperation ? IconCheckCircle : error ? IconExclamationCircleThin : Spinner + const iconColor = optimisticOperation + ? colors.positiveGreen + : error + ? colors.alertRed + : colors.grey const tPrefix = optimisticOperation ? 'send:steps.confirmation.success' - : 'send:steps.confirmation.error' + : error + ? 'send:steps.confirmation.error' + : 'send:steps.confirmation.pending' return ( diff --git a/src/components/modals/Send/ConfirmationFooter.js b/src/components/modals/Send/ConfirmationFooter.js index 99abd760..13863786 100644 --- a/src/components/modals/Send/ConfirmationFooter.js +++ b/src/components/modals/Send/ConfirmationFooter.js @@ -9,37 +9,42 @@ import type { T } from 'types/common' export default ({ t, + error, account, optimisticOperation, onClose, onGoToFirstStep, }: { t: T, + error: ?Error, account: ?Account, optimisticOperation: ?Operation, onClose: () => void, onGoToFirstStep: () => void, -}) => ( - - - {optimisticOperation ? ( - // TODO: actually go to operations details - - ) : ( - - )} - -) +}) => { + const url = + optimisticOperation && account && getAccountOperationExplorer(account, optimisticOperation) + return ( + + + {optimisticOperation ? ( + // TODO: actually go to operations details + url ? ( + + ) : null + ) : error ? ( + + ) : null} + + ) +} diff --git a/src/components/modals/Send/SendModalBody.js b/src/components/modals/Send/SendModalBody.js index c6343d0a..cba8561e 100644 --- a/src/components/modals/Send/SendModalBody.js +++ b/src/components/modals/Send/SendModalBody.js @@ -1,5 +1,6 @@ // @flow +import invariant from 'invariant' import React, { PureComponent } from 'react' import { translate } from 'react-i18next' import { connect } from 'react-redux' @@ -27,6 +28,8 @@ import StepAmount from './01-step-amount' import StepVerification from './03-step-verification' import StepConfirmation from './04-step-confirmation' +const noop = () => {} + type Props = { initialAccount: ?Account, onClose: () => void, @@ -48,7 +51,9 @@ type State = { type Step = { label: string, - canNext?: (State<*>) => boolean, + canNext: (State<*>) => boolean, + canPrev: (State<*>) => boolean, + canClose: (State<*>) => boolean, prevStep?: number, } @@ -81,6 +86,8 @@ class SendModalBody extends PureComponent> { this.steps = [ { label: t('send:steps.amount.title'), + canClose: () => true, + canPrev: () => false, canNext: ({ bridge, account, transaction }) => bridge && account && transaction ? bridge.isValidTransaction(account, transaction) @@ -88,28 +95,83 @@ class SendModalBody extends PureComponent> { }, { label: t('send:steps.connectDevice.title'), + canClose: () => true, canNext: ({ deviceSelected, appStatus }) => deviceSelected !== null && appStatus === 'success', prevStep: 0, + canPrev: () => true, }, { label: t('send:steps.verification.title'), + canClose: ({ error }) => !!error, canNext: () => true, + canPrev: ({ error }) => !!error, prevStep: 1, }, { label: t('send:steps.confirmation.title'), prevStep: 0, + canClose: () => true, + canPrev: () => true, + canNext: () => false, }, ] } + componentWillUnmount() { + const { signTransactionSub } = this + if (signTransactionSub) { + signTransactionSub.unsubscribe() + } + } + + signTransactionSub: * + + signTransaction = async () => { + const { deviceSelected, account, transaction, bridge } = this.state + invariant( + deviceSelected && account && transaction && bridge, + 'signTransaction invalid conditions', + ) + this.signTransactionSub = bridge + .signAndBroadcast(account, transaction, deviceSelected.path) + .subscribe({ + next: e => { + switch (e.type) { + case 'signed': { + this.onSigned() + break + } + case 'broadcasted': { + this.onOperationBroadcasted(e.operation) + break + } + default: + } + }, + error: error => { + this.onOperationError(error) + }, + }) + } + onNextStep = () => - this.setState(({ stepIndex }) => { + this.setState(state => { + let { stepIndex, error } = state if (stepIndex >= this.steps.length - 1) { return null } - return { stepIndex: stepIndex + 1 } + if (!this.steps[stepIndex].canNext(state)) { + console.warn('tried to next step without a valid state!', state, stepIndex) + return null + } + stepIndex++ + if (stepIndex < 2) { + error = null + } else if (stepIndex === 2) { + this.signTransaction() + } + return { stepIndex, error } }) onChangeDevice = (deviceSelected: ?Device) => { @@ -133,8 +195,14 @@ class SendModalBody extends PureComponent> { } } + onSigned = () => { + this.setState({ + stepIndex: 3, + }) + } + onOperationBroadcasted = (optimisticOperation: Operation) => { - const { stepIndex, account, bridge } = this.state + const { account, bridge } = this.state if (!account || !bridge) return const { addPendingOperation } = bridge if (addPendingOperation) { @@ -144,7 +212,7 @@ class SendModalBody extends PureComponent> { } this.setState({ optimisticOperation, - stepIndex: stepIndex + 1, + stepIndex: 3, error: null, }) } @@ -179,7 +247,7 @@ class SendModalBody extends PureComponent> { steps: Step[] render() { - const { t, onClose } = this.props + const { t } = this.props const { stepIndex, account, @@ -192,8 +260,13 @@ class SendModalBody extends PureComponent> { const step = this.steps[stepIndex] if (!step) return null - const canNext = step.canNext && step.canNext(this.state) - const canPrev = 'prevStep' in step + const canClose = step.canClose(this.state) + const canNext = step.canNext(this.state) + const canPrev = step.canPrev(this.state) + let { onClose } = this.props + if (!canClose) { + onClose = noop + } return ( @@ -242,6 +315,7 @@ class SendModalBody extends PureComponent> { {stepIndex === 3 ? ( > { } } -export default compose(connect(mapStateToProps, mapDispatchToProps), translate())(SendModalBody) +export default compose( + connect( + mapStateToProps, + mapDispatchToProps, + ), + translate(), +)(SendModalBody) diff --git a/src/components/modals/Send/index.js b/src/components/modals/Send/index.js index 743bd5f3..eaff72cf 100644 --- a/src/components/modals/Send/index.js +++ b/src/components/modals/Send/index.js @@ -4,24 +4,14 @@ import { MODAL_SEND } from 'config/constants' import Modal from 'components/base/Modal' import SendModalBody from './SendModalBody' -class SendModal extends PureComponent<{}, { resetId: number }> { - state = { resetId: 0 } - handleReset = () => { - this.setState(({ resetId }) => ({ resetId: resetId + 1 })) - } +class SendModal extends PureComponent<{}> { render() { - const { resetId } = this.state return ( ( - + )} /> ) diff --git a/static/i18n/en/send.yml b/static/i18n/en/send.yml index 3a8d25f9..c586daa1 100644 --- a/static/i18n/en/send.yml +++ b/static/i18n/en/send.yml @@ -31,3 +31,5 @@ steps: error: title: Transaction error cta: Retry operation + pending: + title: Broadcasting transaction...