Browse Source

Fix many bugs in Send flow

master
Gaëtan Renaudeau 7 years ago
parent
commit
cba5a9e2d4
  1. 97
      src/bridge/EthereumJSBridge.js
  2. 26
      src/bridge/LibcoreBridge.js
  3. 141
      src/bridge/RippleJSBridge.js
  4. 6
      src/bridge/UnsupportedBridge.js
  5. 31
      src/bridge/makeMockBridge.js
  6. 3
      src/bridge/types.js
  7. 150
      src/commands/libcoreSignAndBroadcast.js
  8. 42
      src/components/DeviceSignTransaction.js
  9. 38
      src/components/modals/Send/03-step-verification.js
  10. 13
      src/components/modals/Send/04-step-confirmation.js
  11. 53
      src/components/modals/Send/ConfirmationFooter.js
  12. 98
      src/components/modals/Send/SendModalBody.js
  13. 16
      src/components/modals/Send/index.js
  14. 2
      static/i18n/en/send.yml

97
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<Transaction> = {
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,

26
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<Transaction>) => (
<FeesBitcoinKind
onChange={feePerByte => {
@ -135,20 +138,27 @@ const LibcoreBridge: WalletBridge<Transaction> = {
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) => ({

141
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<Transaction>) => (
/>
)
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<Transaction> = {
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,

6
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

31
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 } })
}),
}
}

3
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<Transaction> {
account: Account,
transaction: Transaction,
deviceId: DeviceId,
): Promise<Operation>;
): 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?)

150
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<OperationRaw>
type Result = { type: 'signed' } | { type: 'broadcasted', operation: OperationRaw }
const cmd: Command<Input, Result> = 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<OperationRaw>) => void,
}): Promise<void> {
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

42
src/components/DeviceSignTransaction.js

@ -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<Props> {
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

38
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) => (
<Container>
<WarnBox>{multiline(t('send:steps.verification.warning'))}</WarnBox>
<Info>{t('send:steps.verification.body')}</Info>
{account &&
bridge &&
transaction &&
device && (
<DeviceSignTransaction
account={account}
device={device}
transaction={transaction}
bridge={bridge}
onOperationBroadcasted={onOperationBroadcasted}
onError={onError}
>
<DeviceConfirm error={hasError} />
</DeviceSignTransaction>
)}
<DeviceConfirm error={hasError} />
</Container>
)

13
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 (
<Container>

53
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,
}) => (
<ModalFooter horizontal alignItems="center" justifyContent="flex-end" flow={2}>
<Button onClick={onClose}>{t('common:close')}</Button>
{optimisticOperation ? (
// TODO: actually go to operations details
<Button
onClick={() => {
const url = account && getAccountOperationExplorer(account, optimisticOperation)
if (url) {
shell.openExternal(url)
}
onClose()
}}
primary
>
{t('send:steps.confirmation.success.cta')}
</Button>
) : (
<Button onClick={onGoToFirstStep} primary>
{t('send:steps.confirmation.error.cta')}
</Button>
)}
</ModalFooter>
)
}) => {
const url =
optimisticOperation && account && getAccountOperationExplorer(account, optimisticOperation)
return (
<ModalFooter horizontal alignItems="center" justifyContent="flex-end" flow={2}>
<Button onClick={onClose}>{t('common:close')}</Button>
{optimisticOperation ? (
// TODO: actually go to operations details
url ? (
<Button
onClick={() => {
shell.openExternal(url)
onClose()
}}
primary
>
{t('send:steps.confirmation.success.cta')}
</Button>
) : null
) : error ? (
<Button onClick={onGoToFirstStep} primary>
{t('send:steps.confirmation.error.cta')}
</Button>
) : null}
</ModalFooter>
)
}

98
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<T> = {
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<Props, State<*>> {
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<Props, State<*>> {
},
{
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<Props, State<*>> {
}
}
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<Props, State<*>> {
}
this.setState({
optimisticOperation,
stepIndex: stepIndex + 1,
stepIndex: 3,
error: null,
})
}
@ -179,7 +247,7 @@ class SendModalBody extends PureComponent<Props, State<*>> {
steps: Step[]
render() {
const { t, onClose } = this.props
const { t } = this.props
const {
stepIndex,
account,
@ -192,8 +260,13 @@ class SendModalBody extends PureComponent<Props, State<*>> {
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 (
<ModalBody onClose={onClose}>
@ -242,6 +315,7 @@ class SendModalBody extends PureComponent<Props, State<*>> {
{stepIndex === 3 ? (
<ConfirmationFooter
t={t}
error={error}
account={account}
optimisticOperation={optimisticOperation}
onClose={onClose}
@ -268,4 +342,10 @@ class SendModalBody extends PureComponent<Props, State<*>> {
}
}
export default compose(connect(mapStateToProps, mapDispatchToProps), translate())(SendModalBody)
export default compose(
connect(
mapStateToProps,
mapDispatchToProps,
),
translate(),
)(SendModalBody)

16
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 (
<Modal
name={MODAL_SEND}
onHide={this.handleReset}
preventBackdropClick
render={({ data, onClose }) => (
<SendModalBody
key={resetId}
{...this.props}
initialAccount={data && data.account}
onClose={onClose}
/>
<SendModalBody {...this.props} initialAccount={data && data.account} onClose={onClose} />
)}
/>
)

2
static/i18n/en/send.yml

@ -31,3 +31,5 @@ steps:
error:
title: Transaction error
cta: Retry operation
pending:
title: Broadcasting transaction...

Loading…
Cancel
Save