From 9c053399053fcc27ec56d34b8ff898253a814308 Mon Sep 17 00:00:00 2001 From: Sergei Novikov Date: Tue, 19 Dec 2023 11:52:35 +0300 Subject: [PATCH 1/3] solution: new ui for recovery tx --- .../src/transaction/workflow/TxBuilder.ts | 71 +++++++- .../create-tx/CreateErc20RecoveryTx.ts | 11 ++ .../create-tx/CreateEtherRecoveryTx.ts | 10 ++ .../transaction/workflow/create-tx/index.ts | 5 + .../transaction/workflow/create-tx/types.ts | 19 +- .../core/src/transaction/workflow/index.ts | 5 + .../core/src/transaction/workflow/types.ts | 1 + .../src/transaction/CreateTransaction.tsx | 4 + .../SetupTransaction/SetupTransaction.tsx | 18 +- .../blockchain/ethereum/erc20/approve.tsx | 9 +- .../blockchain/ethereum/erc20/convert.tsx | 1 + .../flow/blockchain/ethereum/index.ts | 1 + .../flow/blockchain/ethereum/recovery.tsx | 167 ++++++++++++++++++ .../SetupTransaction/flow/blockchain/index.ts | 1 + .../SetupTransaction/flow/common/transfer.tsx | 2 +- .../SetupTransaction/flow/flow.tsx | 13 +- .../SetupTransaction/flow/types.ts | 2 + .../wallets/WalletDetails/WalletBalance.tsx | 142 ++++++++------- .../wallets/WalletDetails/WalletDetails.tsx | 25 ++- .../WalletDetails/entry/BitcoinEntryItem.tsx | 12 +- .../WalletDetails/entry/EthereumEntryItem.tsx | 45 +++-- .../src/wallets/WalletList/WalletItem.tsx | 18 +- packages/store/src/tokens/selectors.ts | 16 -- packages/store/src/txstash/actions.ts | 4 +- .../handler/blockchain/ethereum/index.ts | 2 +- .../handler/blockchain/ethereum/prepare.ts | 70 +++++++- .../src/txstash/handler/blockchain/index.ts | 8 +- packages/store/src/txstash/handler/handler.ts | 25 ++- packages/store/src/txstash/handler/types.ts | 1 + packages/store/src/txstash/types.ts | 1 + 30 files changed, 566 insertions(+), 143 deletions(-) create mode 100644 packages/core/src/transaction/workflow/create-tx/CreateErc20RecoveryTx.ts create mode 100644 packages/core/src/transaction/workflow/create-tx/CreateEtherRecoveryTx.ts create mode 100644 packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/recovery.tsx diff --git a/packages/core/src/transaction/workflow/TxBuilder.ts b/packages/core/src/transaction/workflow/TxBuilder.ts index ce1d84d3b..f129403b0 100644 --- a/packages/core/src/transaction/workflow/TxBuilder.ts +++ b/packages/core/src/transaction/workflow/TxBuilder.ts @@ -18,19 +18,30 @@ import { isEthereum, } from '../../blockchains'; import { EthereumTransactionType } from '../ethereum'; -import { CreateBitcoinTx, CreateErc20ApproveTx, CreateErc20ConvertTx, CreateErc20Tx, CreateEtherTx } from './create-tx'; +import { + CreateBitcoinTx, + CreateErc20ApproveTx, + CreateErc20ConvertTx, + CreateErc20RecoveryTx, + CreateErc20Tx, + CreateEtherRecoveryTx, + CreateEtherTx, +} from './create-tx'; import { AnyCreateTx, AnyErc20CreateTx, AnyEtherCreateTx, AnyEthereumCreateTx, + AnyEthereumRecoveryTx, fromBitcoinPlainTx, fromEthereumPlainTx, isAnyErc20CreateTx, isErc20ApproveCreateTx, isErc20ConvertCreateTx, isErc20CreateTx, + isErc20RecoveryCreateTx, isEtherCreateTx, + isEtherRecoveryCreateTx, } from './create-tx/types'; import { AnyPlainTx, @@ -141,23 +152,75 @@ export class TxBuilder implements BuilderOrigin { this.mergeEthereumTx(transaction, createTx); } - if (isChanged) { - if (isErc20ApproveCreateTx(createTx)) { + if (isErc20ApproveCreateTx(createTx)) { + if (isChanged) { createTx = this.transformErc20ApproveTx(createTx); } - if (isErc20ConvertCreateTx(createTx)) { + this.mergeEthereumFee(createTx); + } + + if (isErc20ConvertCreateTx(createTx)) { + if (isChanged) { createTx = this.transformErc20ConvertTx(createTx); } this.mergeEthereumFee(createTx); } + + if (isEtherRecoveryCreateTx(createTx) || isErc20RecoveryCreateTx(createTx)) { + if (isChanged) { + return this.convertEthereumRecoveryTx(createTx); + } + + this.mergeEthereumFee(createTx); + } } } return createTx; } + private convertEthereumRecoveryTx(oldCreateTx: AnyEthereumRecoveryTx): AnyEthereumRecoveryTx { + const { asset, entry, feeRange, tokenRegistry } = this; + const { getBalance } = this.dataProvider; + + const blockchain = blockchainIdToCode(entry.blockchain); + + const { coinTicker, eip1559: supportEip1559 = false } = Blockchains[blockchain].params; + + const type = supportEip1559 ? oldCreateTx.type : EthereumTransactionType.LEGACY; + + let newCreateTx: AnyEthereumRecoveryTx; + + if (tokenRegistry.hasAddress(blockchain, asset)) { + newCreateTx = new CreateErc20RecoveryTx(asset, tokenRegistry, blockchain, type); + newCreateTx.totalBalance = getBalance(entry, coinTicker) as WeiAny; + newCreateTx.totalTokenBalance = getBalance(entry, asset, newCreateTx.transferFrom); + } else { + newCreateTx = new CreateEtherRecoveryTx(null, blockchain, type); + newCreateTx.totalBalance = getBalance(entry, asset) as WeiAny; + } + + newCreateTx.from = entry.address?.value; + newCreateTx.to = oldCreateTx.to; + + if (blockchain === oldCreateTx.blockchain && (oldCreateTx.gasPrice?.isPositive() ?? false)) { + newCreateTx.gasPrice = oldCreateTx.gasPrice; + newCreateTx.maxGasPrice = oldCreateTx.maxGasPrice; + newCreateTx.priorityGasPrice = oldCreateTx.priorityGasPrice; + } else if (isEthereumFeeRange(feeRange)) { + newCreateTx.gasPrice = feeRange.stdMaxGasPrice; + newCreateTx.maxGasPrice = feeRange.stdMaxGasPrice; + newCreateTx.priorityGasPrice = feeRange.stdPriorityGasPrice; + } + + newCreateTx.target = TxTarget.SEND_ALL; + newCreateTx.rebalance(); + + return newCreateTx; + } + private convertEthereumTx(oldCreateTx: EthereumBasicCreateTx): EthereumBasicCreateTx { const { asset, entry, feeRange, ownerAddress, tokenRegistry } = this; const { getBalance } = this.dataProvider; diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20RecoveryTx.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20RecoveryTx.ts new file mode 100644 index 000000000..25ab15b19 --- /dev/null +++ b/packages/core/src/transaction/workflow/create-tx/CreateErc20RecoveryTx.ts @@ -0,0 +1,11 @@ +import { TokenRegistry } from '../../../blockchains'; +import { EthereumBasicPlainTx, TxMetaType } from '../types'; +import { CreateErc20Tx, fromPlainDetails } from './CreateErc20Tx'; + +export class CreateErc20RecoveryTx extends CreateErc20Tx { + meta = { type: TxMetaType.ERC20_RECOVERY }; + + static fromPlain(details: EthereumBasicPlainTx, tokenRegistry: TokenRegistry): CreateErc20RecoveryTx { + return new CreateErc20RecoveryTx(fromPlainDetails(details, tokenRegistry), tokenRegistry); + } +} diff --git a/packages/core/src/transaction/workflow/create-tx/CreateEtherRecoveryTx.ts b/packages/core/src/transaction/workflow/create-tx/CreateEtherRecoveryTx.ts new file mode 100644 index 000000000..c0e1fa0e9 --- /dev/null +++ b/packages/core/src/transaction/workflow/create-tx/CreateEtherRecoveryTx.ts @@ -0,0 +1,10 @@ +import { EthereumBasicPlainTx, TxMetaType } from '../types'; +import { CreateEtherTx, fromPlainDetails } from './CreateEtherTx'; + +export class CreateEtherRecoveryTx extends CreateEtherTx { + meta = { type: TxMetaType.ETHER_RECOVERY }; + + static fromPlain(details: EthereumBasicPlainTx): CreateEtherRecoveryTx { + return new CreateEtherRecoveryTx(fromPlainDetails(details)); + } +} diff --git a/packages/core/src/transaction/workflow/create-tx/index.ts b/packages/core/src/transaction/workflow/create-tx/index.ts index 21bcf3cbd..30cc50423 100644 --- a/packages/core/src/transaction/workflow/create-tx/index.ts +++ b/packages/core/src/transaction/workflow/create-tx/index.ts @@ -7,6 +7,7 @@ export { AnyErc20CreateTx, AnyEtherCreateTx, AnyEthereumCreateTx, + AnyEthereumRecoveryTx, EthereumTx, fromBitcoinPlainTx, fromErc20PlainTx, @@ -24,9 +25,11 @@ export { isErc20CancelCreateTx, isErc20ConvertCreateTx, isErc20CreateTx, + isErc20RecoveryCreateTx, isErc20SpeedUpCreateTx, isEtherCancelCreateTx, isEtherCreateTx, + isEtherRecoveryCreateTx, isEtherSpeedUpCreateTx, } from './types'; @@ -36,8 +39,10 @@ export { CreateBitcoinCancelTx } from './CreateBitcoinCancelTx'; export { CreateBitcoinSpeedUpTx } from './CreateBitcoinSpeedUpTx'; export { CreateErc20CancelTx } from './CreateErc20CancelTx'; export { CreateErc20ConvertTx, Erc20ConvertTxDetails } from './CreateErc20ConvertTx'; +export { CreateErc20RecoveryTx } from './CreateErc20RecoveryTx'; export { CreateErc20SpeedUpTx } from './CreateErc20SpeedUpTx'; export { CreateErc20Tx, Erc20TxDetails } from './CreateErc20Tx'; export { CreateEtherCancelTx } from './CreateEtherCancelTx'; +export { CreateEtherRecoveryTx } from './CreateEtherRecoveryTx'; export { CreateEtherSpeedUpTx } from './CreateEtherSpeedUpTx'; export { CreateEtherTx, TxDetails } from './CreateEtherTx'; diff --git a/packages/core/src/transaction/workflow/create-tx/types.ts b/packages/core/src/transaction/workflow/create-tx/types.ts index b17daae40..96dc3c717 100644 --- a/packages/core/src/transaction/workflow/create-tx/types.ts +++ b/packages/core/src/transaction/workflow/create-tx/types.ts @@ -17,9 +17,11 @@ import { BitcoinTxOrigin, CreateBitcoinTx } from './CreateBitcoinTx'; import { CreateErc20ApproveTx } from './CreateErc20ApproveTx'; import { CreateErc20CancelTx } from './CreateErc20CancelTx'; import { CreateErc20ConvertTx } from './CreateErc20ConvertTx'; +import { CreateErc20RecoveryTx } from './CreateErc20RecoveryTx'; import { CreateErc20SpeedUpTx } from './CreateErc20SpeedUpTx'; import { CreateErc20Tx } from './CreateErc20Tx'; import { CreateEtherCancelTx } from './CreateEtherCancelTx'; +import { CreateEtherRecoveryTx } from './CreateEtherRecoveryTx'; import { CreateEtherSpeedUpTx } from './CreateEtherSpeedUpTx'; import { CreateEtherTx } from './CreateEtherTx'; @@ -36,7 +38,8 @@ export type AnyBitcoinCreateTx = CreateBitcoinTx | CreateBitcoinCancelTx | Creat export type AnyEtherCreateTx = CreateEtherTx | CreateEtherCancelTx | CreateEtherSpeedUpTx; export type AnyErc20CreateTx = CreateErc20Tx | CreateErc20CancelTx | CreateErc20SpeedUpTx; export type AnyContractCreateTx = AnyErc20CreateTx | CreateErc20ApproveTx | CreateErc20ConvertTx; -export type AnyEthereumCreateTx = AnyEtherCreateTx | AnyContractCreateTx; +export type AnyEthereumRecoveryTx = CreateEtherRecoveryTx | CreateErc20RecoveryTx; +export type AnyEthereumCreateTx = AnyEtherCreateTx | AnyContractCreateTx | AnyEthereumRecoveryTx; export type AnyCreateTx = AnyBitcoinCreateTx | AnyEthereumCreateTx; const bitcoinTxMetaTypes: Readonly = [ @@ -47,12 +50,14 @@ const bitcoinTxMetaTypes: Readonly = [ const etherTxMetaTypes: Readonly = [ TxMetaType.ETHER_CANCEL, + TxMetaType.ETHER_RECOVERY, TxMetaType.ETHER_SPEEDUP, TxMetaType.ETHER_TRANSFER, ]; const erc20TxMetaTypes: Readonly = [ TxMetaType.ERC20_CANCEL, + TxMetaType.ERC20_RECOVERY, TxMetaType.ERC20_SPEEDUP, TxMetaType.ERC20_TRANSFER, ]; @@ -91,6 +96,10 @@ export function isEtherCancelCreateTx(createTx: AnyCreateTx): createTx is Create return createTx.meta.type === TxMetaType.ETHER_CANCEL; } +export function isEtherRecoveryCreateTx(createTx: AnyCreateTx): createTx is CreateEtherRecoveryTx { + return createTx.meta.type === TxMetaType.ETHER_RECOVERY; +} + export function isEtherSpeedUpCreateTx(createTx: AnyCreateTx): createTx is CreateEtherSpeedUpTx { return createTx.meta.type === TxMetaType.ETHER_SPEEDUP; } @@ -119,6 +128,10 @@ export function isErc20ConvertCreateTx(createTx: AnyCreateTx): createTx is Creat return createTx.meta.type === TxMetaType.ERC20_CONVERT; } +export function isErc20RecoveryCreateTx(createTx: AnyCreateTx): createTx is CreateErc20RecoveryTx { + return createTx.meta.type === TxMetaType.ERC20_RECOVERY; +} + export function isErc20SpeedUpCreateTx(createTx: AnyCreateTx): createTx is CreateErc20SpeedUpTx { return createTx.meta.type === TxMetaType.ERC20_SPEEDUP; } @@ -140,6 +153,8 @@ export function fromEtherPlainTx(transaction: EthereumPlainTx): AnyEtherCreateTx switch (transaction.meta.type) { case TxMetaType.ETHER_CANCEL: return CreateEtherCancelTx.fromPlain(transaction); + case TxMetaType.ETHER_RECOVERY: + return CreateEtherRecoveryTx.fromPlain(transaction); case TxMetaType.ETHER_SPEEDUP: return CreateEtherSpeedUpTx.fromPlain(transaction); case TxMetaType.ETHER_TRANSFER: @@ -157,6 +172,8 @@ export function fromErc20PlainTx(transaction: EthereumPlainTx, tokenRegistry: To return CreateErc20CancelTx.fromPlain(transaction, tokenRegistry); case TxMetaType.ERC20_CONVERT: return CreateErc20ConvertTx.fromPlain(transaction, tokenRegistry); + case TxMetaType.ERC20_RECOVERY: + return CreateErc20RecoveryTx.fromPlain(transaction, tokenRegistry); case TxMetaType.ERC20_SPEEDUP: return CreateErc20SpeedUpTx.fromPlain(transaction, tokenRegistry); case TxMetaType.ERC20_TRANSFER: diff --git a/packages/core/src/transaction/workflow/index.ts b/packages/core/src/transaction/workflow/index.ts index 549e8cc42..06277533f 100644 --- a/packages/core/src/transaction/workflow/index.ts +++ b/packages/core/src/transaction/workflow/index.ts @@ -7,6 +7,7 @@ export { AnyErc20CreateTx, AnyEtherCreateTx, AnyEthereumCreateTx, + AnyEthereumRecoveryTx, ApproveTarget, BitcoinTx, BitcoinTxDetails, @@ -17,9 +18,11 @@ export { CreateErc20ApproveTx, CreateErc20CancelTx, CreateErc20ConvertTx, + CreateErc20RecoveryTx, CreateErc20SpeedUpTx, CreateErc20Tx, CreateEtherCancelTx, + CreateEtherRecoveryTx, CreateEtherSpeedUpTx, CreateEtherTx, Erc20ApproveTxDetails, @@ -43,9 +46,11 @@ export { isErc20CancelCreateTx, isErc20ConvertCreateTx, isErc20CreateTx, + isErc20RecoveryCreateTx, isErc20SpeedUpCreateTx, isEtherCancelCreateTx, isEtherCreateTx, + isEtherRecoveryCreateTx, isEtherSpeedUpCreateTx, } from './create-tx'; diff --git a/packages/core/src/transaction/workflow/types.ts b/packages/core/src/transaction/workflow/types.ts index 0a1ae7868..5ec97673b 100644 --- a/packages/core/src/transaction/workflow/types.ts +++ b/packages/core/src/transaction/workflow/types.ts @@ -17,6 +17,7 @@ export enum TxMetaType { ERC20_APPROVE, ERC20_CANCEL, ERC20_CONVERT, + ERC20_RECOVERY, ERC20_SPEEDUP, ERC20_TRANSFER, } diff --git a/packages/react-app/src/transaction/CreateTransaction.tsx b/packages/react-app/src/transaction/CreateTransaction.tsx index 1d9bc6148..d62bfb05a 100644 --- a/packages/react-app/src/transaction/CreateTransaction.tsx +++ b/packages/react-app/src/transaction/CreateTransaction.tsx @@ -50,6 +50,10 @@ const CreateTransaction: React.FC = ({ st case TxAction.CONVERT: title = 'Create Convert Transaction'; + break; + case TxAction.RECOVERY: + title = 'Create Recovery Transaction'; + break; case TxAction.SPEEDUP: title = 'Speed Up Transaction'; diff --git a/packages/react-app/src/transaction/SetupTransaction/SetupTransaction.tsx b/packages/react-app/src/transaction/SetupTransaction/SetupTransaction.tsx index 194e5fb0e..0545e9413 100644 --- a/packages/react-app/src/transaction/SetupTransaction/SetupTransaction.tsx +++ b/packages/react-app/src/transaction/SetupTransaction/SetupTransaction.tsx @@ -54,8 +54,14 @@ interface StateProps { getFiatBalance(asset: string): CurrencyAmount | undefined; } +interface TxOrigin { + action: TxAction; + entries: WalletEntry[]; + entry: WalletEntry; +} + interface DispatchProps { - prepareTransaction(action: TxAction, entry: WalletEntry): void; + prepareTransaction(origin: TxOrigin): void; setAsset(asset: string): void; setEntry(entry: WalletEntry, ownerAddress?: string): void; setStage(stage: CreateTxStage): void; @@ -88,8 +94,8 @@ const SetupTransaction: React.FC = ({ const mounted = React.useRef(true); React.useEffect(() => { - prepareTransaction(action, entry); - }, [action, entry, storedTx, prepareTransaction]); + prepareTransaction({ action, entries, entry }); + }, [action, entries, entry, storedTx, prepareTransaction]); React.useEffect(() => { return () => { @@ -132,7 +138,7 @@ export default connect( walletId = EntryIdOp.of(entryId).extractWalletId(); } - const entries = accounts.selectors.findWallet(state, walletId)?.entries.filter((entry) => !entry.receiveDisabled); + const { entries } = accounts.selectors.findWallet(state, walletId) ?? {}; if (entries == null || entries.length === 0) { throw new Error('Something went wrong while getting entries from wallet'); @@ -250,8 +256,8 @@ export default connect( }, // eslint-disable-next-line @typescript-eslint/no-explicit-any (dispatch: any, { initialAllowance, storedTx }) => ({ - prepareTransaction(action, entry) { - dispatch(txStash.actions.prepareTransaction({ action, entry, initialAllowance, storedTx })); + prepareTransaction({ action, entries, entry }) { + dispatch(txStash.actions.prepareTransaction({ action, entries, entry, initialAllowance, storedTx })); }, setAsset(asset) { dispatch(txStash.actions.setAsset(asset)); diff --git a/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/erc20/approve.tsx b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/erc20/approve.tsx index 0e56e2e34..5fe32d3af 100644 --- a/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/erc20/approve.tsx +++ b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/erc20/approve.tsx @@ -28,13 +28,14 @@ export class Erc20ApproveFlow implements CommonFlow { const { setEntry } = this.handler; const approvingEntries = entries - .filter((item): item is EthereumEntry => isEthereumEntry(item)) - .filter((item) => { - const blockchain = blockchainIdToCode(item.blockchain); + .filter((entry): entry is EthereumEntry => isEthereumEntry(entry)) + .filter((entry) => { + const blockchain = blockchainIdToCode(entry.blockchain); return ( + !entry.receiveDisabled && tokenRegistry.hasAnyToken(blockchain) && - getBalance(item, Blockchains[blockchain].params.coinTicker).isPositive() + getBalance(entry, Blockchains[blockchain].params.coinTicker).isPositive() ); }); diff --git a/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/erc20/convert.tsx b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/erc20/convert.tsx index b6c081ab8..57e172158 100644 --- a/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/erc20/convert.tsx +++ b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/erc20/convert.tsx @@ -53,6 +53,7 @@ export class Erc20ConvertFlow implements CommonFlow { const blockchain = blockchainIdToCode(item.blockchain); return ( + !entry.receiveDisabled && tokenRegistry.hasWrappedToken(blockchain) && getBalance(item, Blockchains[blockchain].params.coinTicker).isPositive() ); diff --git a/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/index.ts b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/index.ts index 2f09001ee..bfb641230 100644 --- a/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/index.ts +++ b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/index.ts @@ -2,4 +2,5 @@ export { Erc20ApproveFlow, Erc20ConvertFlow } from './erc20'; export { EthereumCancelFlow, EthereumSpeedUpFlow } from './modify'; +export { EthereumRecoveryFlow } from './recovery'; export { EthereumTransferFlow } from './transfer'; diff --git a/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/recovery.tsx b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/recovery.tsx new file mode 100644 index 000000000..974b483c2 --- /dev/null +++ b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/ethereum/recovery.tsx @@ -0,0 +1,167 @@ +import { EthereumEntry, WalletEntry, isEthereumEntry } from '@emeraldpay/emerald-vault-core'; +import { Blockchains, blockchainIdToCode, formatAmount, workflow } from '@emeraldwallet/core'; +import { CreateTxStage } from '@emeraldwallet/store'; +import { FormLabel, FormRow } from '@emeraldwallet/ui'; +import { Typography } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import * as React from 'react'; +import { SelectAsset } from '../../../../../common/SelectAsset'; +import { SelectEntry } from '../../../../../common/SelectEntry'; +import { Actions, EthereumFee } from '../../components'; +import { CommonFlow, Data, DataProvider, Handler } from '../../types'; + +type EthereumData = Data; + +export class EthereumRecoveryFlow implements CommonFlow { + readonly data: EthereumData; + readonly dataProvider: DataProvider; + readonly handler: Handler; + + constructor(data: EthereumData, dataProvider: DataProvider, handler: Handler) { + this.data = data; + this.dataProvider = dataProvider; + this.handler = handler; + } + + private getToEntries(): EthereumEntry[] { + const { entry, entries } = this.data; + + return entries + .filter((entry): entry is EthereumEntry => isEthereumEntry(entry)) + .filter(({ blockchain, receiveDisabled }) => !receiveDisabled && blockchain === entry.blockchain); + } + + private renderTo(entries: EthereumEntry[]): React.ReactElement { + const { createTx } = this.data; + const { setTransaction } = this.handler; + + const handleSelectTo = (entry: WalletEntry): void => { + createTx.to = entry.address?.value; + + setTransaction(createTx.dump()); + }; + + const { to: toAddress } = createTx; + + let [entry] = entries; + + if (toAddress != null) { + entry = + entries.find( + ({ address, blockchain }) => + address?.value === toAddress && blockchainIdToCode(blockchain) === createTx.blockchain, + ) ?? entry; + } + + return ( + + To + + + ); + } + + private renderToken(): React.ReactNode { + const { asset, assets, entry } = this.data; + const { getBalance, getFiatBalance } = this.dataProvider; + const { setAsset } = this.handler; + + return ( + + Token + balance.isPositive())} + balance={getBalance(entry, asset)} + fiatBalance={getFiatBalance(asset)} + onChangeAsset={setAsset} + /> + + ); + } + + private renderPreview(): React.ReactElement { + const { entries, entry, createTx } = this.data; + + const wrongEntry = entries.find( + ({ address, blockchain, receiveDisabled }) => + !receiveDisabled && + address != null && + address.value === entry.address?.value && + blockchain !== entry.blockchain, + ); + + const recoveryBlockchain = Blockchains[blockchainIdToCode(entry.blockchain)]; + const wrongBlockchain = wrongEntry == null ? null : Blockchains[blockchainIdToCode(wrongEntry.blockchain)]; + + return ( + <> + + Amount + {formatAmount(createTx.amount)} + + + Reason + + {recoveryBlockchain.getTitle()} coins/tokens on {wrongBlockchain?.getTitle() ?? 'wrong'} address + + + + ); + } + + private renderFee(): React.ReactElement { + const { + createTx, + fee: { loading, range }, + } = this.data; + + if (!workflow.isEthereumFeeRange(range)) { + throw new Error('Bitcoin transaction or fee provided for Ethereum transaction'); + } + + const { setTransaction } = this.handler; + + return ; + } + + private renderActions(): React.ReactElement { + const { createTx, entry, fee } = this.data; + const { onCancel, setEntry, setStage, setTransaction } = this.handler; + + const handleCreateTx = (): void => { + setEntry(entry); + setTransaction(createTx.dump()); + + setStage(CreateTxStage.SIGN); + }; + + return ; + } + + render(): React.ReactElement { + const { entry } = this.data; + + const toEntries = this.getToEntries(); + + if (toEntries.length === 0) { + const blockchainTitle = Blockchains[blockchainIdToCode(entry.blockchain)].getTitle(); + + return ( + + Address for recovery not found. Please add new address on {blockchainTitle} blockchain and try again. + + ); + } + + return ( + <> + {this.renderTo(toEntries)} + {this.renderToken()} + {this.renderPreview()} + {this.renderFee()} + {this.renderActions()} + + ); + } +} diff --git a/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/index.ts b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/index.ts index b455e42cb..42b2e5ce4 100644 --- a/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/index.ts +++ b/packages/react-app/src/transaction/SetupTransaction/flow/blockchain/index.ts @@ -5,6 +5,7 @@ export { Erc20ApproveFlow, Erc20ConvertFlow, EthereumCancelFlow, + EthereumRecoveryFlow, EthereumSpeedUpFlow, EthereumTransferFlow, } from './ethereum'; diff --git a/packages/react-app/src/transaction/SetupTransaction/flow/common/transfer.tsx b/packages/react-app/src/transaction/SetupTransaction/flow/common/transfer.tsx index dd832c924..81f434189 100644 --- a/packages/react-app/src/transaction/SetupTransaction/flow/common/transfer.tsx +++ b/packages/react-app/src/transaction/SetupTransaction/flow/common/transfer.tsx @@ -31,7 +31,7 @@ export abstract class TransferFlow implements CommonFlow { From !entry.receiveDisabled)} ownerAddress={ownerAddress} selectedEntry={entry} onSelect={setEntry} diff --git a/packages/react-app/src/transaction/SetupTransaction/flow/flow.tsx b/packages/react-app/src/transaction/SetupTransaction/flow/flow.tsx index fd1a0c133..ad04e24b6 100644 --- a/packages/react-app/src/transaction/SetupTransaction/flow/flow.tsx +++ b/packages/react-app/src/transaction/SetupTransaction/flow/flow.tsx @@ -6,6 +6,7 @@ import { BitcoinTransferFlow, Erc20ConvertFlow, EthereumCancelFlow, + EthereumRecoveryFlow, EthereumSpeedUpFlow, EthereumTransferFlow, } from './blockchain'; @@ -29,11 +30,15 @@ export class Flow { throw new Error('Unsupported Bitcoin transaction action'); } } else if (isEthereumEntry(entry)) { - if (workflow.isEtherCancelCreateTx(createTx)) { + if (workflow.isEtherCreateTx(createTx)) { + this._flow = new EthereumTransferFlow({ ...data, entry, createTx }, dataProvider, handler); + } else if (workflow.isEtherCancelCreateTx(createTx)) { this._flow = new EthereumCancelFlow({ ...data, entry, createTx }, dataProvider, handler); + } else if (workflow.isEtherRecoveryCreateTx(createTx)) { + this._flow = new EthereumRecoveryFlow({ ...data, entry, createTx }, dataProvider, handler); } else if (workflow.isEtherSpeedUpCreateTx(createTx)) { this._flow = new EthereumSpeedUpFlow({ ...data, entry, createTx }, dataProvider, handler); - } else if (workflow.isEtherCreateTx(createTx)) { + } else if (workflow.isErc20CreateTx(createTx)) { this._flow = new EthereumTransferFlow({ ...data, entry, createTx }, dataProvider, handler); } else if (workflow.isErc20ApproveCreateTx(createTx)) { this._flow = new Erc20ApproveFlow({ ...data, entry, createTx }, dataProvider, handler); @@ -41,10 +46,10 @@ export class Flow { this._flow = new EthereumCancelFlow({ ...data, entry, createTx }, dataProvider, handler); } else if (workflow.isErc20ConvertCreateTx(createTx)) { this._flow = new Erc20ConvertFlow({ ...data, entry, createTx }, dataProvider, handler); + } else if (workflow.isErc20RecoveryCreateTx(createTx)) { + this._flow = new EthereumRecoveryFlow({ ...data, entry, createTx }, dataProvider, handler); } else if (workflow.isErc20SpeedUpCreateTx(createTx)) { this._flow = new EthereumSpeedUpFlow({ ...data, entry, createTx }, dataProvider, handler); - } else if (workflow.isErc20CreateTx(createTx)) { - this._flow = new EthereumTransferFlow({ ...data, entry, createTx }, dataProvider, handler); } else { throw new Error('Unsupported Ethereum transaction action'); } diff --git a/packages/react-app/src/transaction/SetupTransaction/flow/types.ts b/packages/react-app/src/transaction/SetupTransaction/flow/types.ts index 828dfd120..3b64df656 100644 --- a/packages/react-app/src/transaction/SetupTransaction/flow/types.ts +++ b/packages/react-app/src/transaction/SetupTransaction/flow/types.ts @@ -11,6 +11,7 @@ import { Erc20ApproveFlow, Erc20ConvertFlow, EthereumCancelFlow, + EthereumRecoveryFlow, EthereumSpeedUpFlow, EthereumTransferFlow, } from './blockchain'; @@ -22,6 +23,7 @@ export type BlockchainFlow = | Erc20ApproveFlow | Erc20ConvertFlow | EthereumCancelFlow + | EthereumRecoveryFlow | EthereumSpeedUpFlow | EthereumTransferFlow; diff --git a/packages/react-app/src/wallets/WalletDetails/WalletBalance.tsx b/packages/react-app/src/wallets/WalletDetails/WalletBalance.tsx index 66b4109dd..452e240db 100644 --- a/packages/react-app/src/wallets/WalletDetails/WalletBalance.tsx +++ b/packages/react-app/src/wallets/WalletDetails/WalletBalance.tsx @@ -89,10 +89,13 @@ interface OwnProps { } interface StateProps { - balance?: CurrencyAmount; + blockchains: string | undefined; + fiatBalance: CurrencyAmount | undefined; + groupedEntries: WalletEntry[][]; + hasAnyBalance: boolean; hasEthereumEntry: boolean; - wallet?: Wallet; - walletIcon?: string | null; + wallet: Wallet | undefined; + walletIcon: string | null | undefined; isHardware(wallet: Wallet | undefined): boolean; } @@ -103,7 +106,10 @@ interface DispatchProps { } const WalletBalance: React.FC = ({ - balance, + blockchains, + fiatBalance, + groupedEntries, + hasAnyBalance, hasEthereumEntry, wallet, walletIcon, @@ -114,68 +120,30 @@ const WalletBalance: React.FC = ({ }) => { const styles = useStyles(); - const blockchains = React.useMemo( - () => - wallet?.entries - .reduce((carry, entry) => { - const blockchainCode = blockchainIdToCode(entry.blockchain); - - if (carry.includes(blockchainCode)) { - return carry; - } - - return [...carry, blockchainCode]; - }, []) - .map((blockchain) => Blockchains[blockchain].getTitle()) - .join(', '), - [wallet?.entries], - ); - - const entriesByBlockchain = React.useMemo( - () => - Object.values( - wallet?.entries - .filter((entry) => !entry.receiveDisabled) - .reduce>( - (carry, entry) => ({ - ...carry, - [entry.blockchain]: [...(carry[entry.blockchain] ?? []), entry], - }), - {}, - ) ?? {}, - ), - [wallet?.entries], - ); - - const receiveDisabledEntries = React.useMemo( - () => wallet?.entries.filter((entry) => entry.receiveDisabled) ?? [], - [wallet?.entries], - ); - - const renderEntry = (entries: WalletEntry[]): React.ReactNode => { - if (wallet != null) { - const [entry] = entries; - - if (isBitcoinEntry(entry)) { - return ( -
- -
- ); - } + const renderEntries = (entries: WalletEntry[]): React.ReactNode => { + if (wallet == null) { + return null; + } - const ethereumEntries = entries.filter((item): item is EthereumEntry => isEthereumEntry(item)); + const [entry] = entries; - if (ethereumEntries.length > 0) { - return ( -
- -
- ); - } + if (isBitcoinEntry(entry)) { + return ( +
+ +
+ ); } - return null; + const ethereumEntries = entries + .filter(({ blockchain }) => blockchain === entry.blockchain) + .filter((item): item is EthereumEntry => isEthereumEntry(item)); + + return ( +
+ +
+ ); }; return ( @@ -196,9 +164,9 @@ const WalletBalance: React.FC = ({ {wallet.name} )} - {balance?.isPositive() && ( + {fiatBalance?.isPositive() && ( - {formatFiatAmount(balance)} + {formatFiatAmount(fiatBalance)} )} @@ -221,13 +189,12 @@ const WalletBalance: React.FC = ({ - } @@ -106,13 +109,14 @@ const EthereumEntryItem: React.FC = ({ `${carry}${array.length === index + 1 ? ' and ' : ', '}${formatAmount(balance)}`, '', )}{' '} - on {blockchainTitle} blockchain + on {blockchainTitle} blockchain. + {balance.isZero() ? " Unfortunately you can't recovery them without positive balance." : null} ); } const { commonBalances: tokenCommonBalances, wrappedBalances: tokenWrappedBalances } = - tokenBalances.reduce( + tokenBalances.reduce( (carry, tokenBalance) => { if (isWrappedToken(tokenBalance.balance.token)) { carry.wrappedBalances.push(tokenBalance); @@ -225,9 +229,24 @@ const EthereumEntryItem: React.FC = ({ ); }; +function aggregateBalances(balances: TokenAmount[]): TokenAmount[] { + return balances.reduce((carry, balance) => { + const index = carry.findIndex( + ({ token: { address } }) => address.toLowerCase() === balance.token.address.toLowerCase(), + ); + + if (index > -1) { + carry[index] = carry[index].plus(balance); + } else { + carry.push(balance); + } + + return carry; + }, []); +} + export default connect( - (state, ownProps) => { - const { entries } = ownProps; + (state, { entries }) => { const [entry] = entries; const blockchainCode = blockchainIdToCode(entry.blockchain); @@ -249,14 +268,14 @@ export default connect( const balances = tokens.selectors.selectBalances(state, blockchainCode, address.value); - return tokens.selectors.aggregateBalances([...carry, ...balances]); + return aggregateBalances([...carry, ...balances]); }, []), ); return { balance, blockchainCode, fiatBalance, tokenBalances }; }, // eslint-disable-next-line @typescript-eslint/no-explicit-any - (dispatch: Dispatch, { entries: [entry], walletId }) => ({ + (dispatch: Dispatch, { walletId, entries: [entry] }) => ({ gotoConvert() { dispatch( screen.actions.gotoScreen( diff --git a/packages/react-app/src/wallets/WalletList/WalletItem.tsx b/packages/react-app/src/wallets/WalletList/WalletItem.tsx index 21a8aa670..cf3c08be2 100644 --- a/packages/react-app/src/wallets/WalletList/WalletItem.tsx +++ b/packages/react-app/src/wallets/WalletList/WalletItem.tsx @@ -75,6 +75,7 @@ interface OwnProps { interface StateProps { balances: BigAmount[]; + hasAnyBalance: boolean; total: BigAmount | undefined; walletIcon?: string | null; } @@ -86,6 +87,7 @@ interface DispatchProps { const WalletItem: React.FC = ({ balances, + hasAnyBalance, total, wallet, walletIcon, @@ -141,7 +143,13 @@ const WalletItem: React.FC = ({ -