From 1495237f5fdfc6ecf10c56f4d09d896e8621a40a Mon Sep 17 00:00:00 2001 From: Sergei Novikov Date: Mon, 23 Oct 2023 03:34:07 +0300 Subject: [PATCH] problem: different components for Ether, ERC20 and doesn't allow to change the blockchain --- .vscode/emerald-wallet.code-workspace | 82 +++ .vscode/extensions.json | 12 - .vscode/settings.json | 11 - packages/core/src/blockchains/blockchains.ts | 4 + packages/core/src/convert.spec.ts | 22 +- packages/core/src/convert.ts | 51 +- packages/core/src/index.ts | 2 +- packages/core/src/transaction/ethereum.ts | 14 +- .../src/workflow/create-tx/CreateBitcoinTx.ts | 4 +- .../create-tx/CreateErc20ApproveTx.spec.ts | 2 +- .../create-tx/CreateErc20ApproveTx.ts | 38 +- .../workflow/create-tx/CreateErc20Tx.spec.ts | 7 +- .../src/workflow/create-tx/CreateErc20Tx.ts | 39 +- .../create-tx/CreateErc20WrappedTx.ts | 26 +- .../workflow/create-tx/CreateEthereumTx.ts | 41 +- .../create-tx/CreateTxConverter.spec.ts | 624 ++++++++++++++++++ .../workflow/create-tx/CreateTxConverter.ts | 181 +++++ packages/core/src/workflow/create-tx/types.ts | 4 +- .../src/workflow/display/DisplayErc20Tx.ts | 5 +- packages/core/src/workflow/index.ts | 9 +- packages/desktop/defaults.json | 5 + packages/desktop/package.json | 4 +- packages/electron-app/package.json | 2 +- packages/react-app/package.json | 3 +- .../src/app/screen/Screen/Screen.tsx | 3 + .../common/EthTxSettings/EthTxSettings.tsx | 39 +- .../src/common/EthTxSettings/index.ts | 1 + .../src/common/NumberField/NumberField.tsx | 88 +++ .../react-app/src/common/NumberField/index.ts | 1 + .../common/SelectAsset/SelectAsset.spec.tsx | 2 +- .../src/common/SelectAsset/SelectAsset.tsx | 10 +- .../src/common/SelectEntry/SelectEntry.tsx | 193 ++++-- .../BroadcastTx/BroadcastTx.spec.tsx | 13 +- .../CreateApproveTransaction.tsx | 7 +- .../SetupApproveTransaction.tsx | 4 +- .../CancelEthereumTransaction.tsx | 40 +- .../CreateConvertTransaction.tsx | 9 +- .../CreateRecoverTransaction.tsx | 7 +- .../SpeedUpEthereumTransaction.tsx | 65 +- .../BroadcastTransaction.tsx | 213 ++++++ .../BroadcastTransaction/index.ts | 1 + .../CreateBitcoinTransaction.tsx | 124 ++++ .../SetupTransaction/SetupTransaction.tsx | 304 +++++++++ .../SetupTransaction/index.ts | 1 + .../SignTransaction/SignTransaction.tsx | 186 ++++++ .../SignTransaction/Summary/Summary.tsx | 38 ++ .../SignTransaction/Summary/index.ts | 1 + .../SignTransaction/index.ts | 1 + .../CreateBitcoinTransaction/index.ts | 1 + .../BroadcastTransaction.tsx | 211 ++++++ .../BroadcastTransaction/index.ts | 1 + .../CreateEthereumTransaction.tsx | 145 ++++ .../SetupTransaction/SetupTransaction.tsx | 221 +++++++ .../SetupTransaction/index.ts | 1 + .../SignTransaction/SignTransaction.tsx | 262 ++++++++ .../SignTransaction/index.ts | 1 + .../CreateEthereumTransaction/index.ts | 1 + .../CreateTransaction.tsx | 108 +++ .../transaction/CreateTransactionNew/index.ts | 1 + .../src/transaction/CreateTx/CreateTx.tsx | 2 +- .../src/transactions/TxDetails/TxDetails.tsx | 17 +- .../src/wallets/WalletList/WalletItem.tsx | 2 +- packages/store/package.json | 2 +- packages/store/src/application/selectors.ts | 4 + packages/store/src/index.ts | 3 + packages/store/src/root-reducer.ts | 2 + packages/store/src/screen/types.ts | 1 + packages/store/src/transaction/actions.ts | 115 ++-- packages/store/src/transaction/types.ts | 28 +- packages/store/src/txstash/actions.ts | 192 ++++++ packages/store/src/txstash/index.ts | 6 + packages/store/src/txstash/reducer.ts | 82 +++ packages/store/src/txstash/selectors.spec.ts | 56 ++ packages/store/src/txstash/selectors.ts | 59 ++ packages/store/src/txstash/types.ts | 126 ++++ packages/store/src/types.ts | 2 + packages/ui/jest.setup.ts | 4 +- packages/ui/package.json | 4 +- .../common/Account/Account.spec.tsx | 45 +- .../src/components/common/Account/Account.tsx | 4 +- .../common/Address/Address.spec.tsx | 20 +- yarn.lock | 224 +------ 82 files changed, 3884 insertions(+), 612 deletions(-) create mode 100644 .vscode/emerald-wallet.code-workspace delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/settings.json create mode 100644 packages/core/src/workflow/create-tx/CreateTxConverter.spec.ts create mode 100644 packages/core/src/workflow/create-tx/CreateTxConverter.ts create mode 100644 packages/react-app/src/common/EthTxSettings/index.ts create mode 100644 packages/react-app/src/common/NumberField/NumberField.tsx create mode 100644 packages/react-app/src/common/NumberField/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/BroadcastTransaction/BroadcastTransaction.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/BroadcastTransaction/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/CreateBitcoinTransaction.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/SetupTransaction/SetupTransaction.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/SetupTransaction/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/SignTransaction/SignTransaction.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/SignTransaction/Summary/Summary.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/SignTransaction/Summary/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/SignTransaction/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateEthereumTransaction/BroadcastTransaction/BroadcastTransaction.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateEthereumTransaction/BroadcastTransaction/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateEthereumTransaction/CreateEthereumTransaction.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateEthereumTransaction/SetupTransaction/SetupTransaction.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateEthereumTransaction/SetupTransaction/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateEthereumTransaction/SignTransaction/SignTransaction.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateEthereumTransaction/SignTransaction/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateEthereumTransaction/index.ts create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/CreateTransaction.tsx create mode 100644 packages/react-app/src/transaction/CreateTransactionNew/index.ts create mode 100644 packages/store/src/txstash/actions.ts create mode 100644 packages/store/src/txstash/index.ts create mode 100644 packages/store/src/txstash/reducer.ts create mode 100644 packages/store/src/txstash/selectors.spec.ts create mode 100644 packages/store/src/txstash/selectors.ts create mode 100644 packages/store/src/txstash/types.ts diff --git a/.vscode/emerald-wallet.code-workspace b/.vscode/emerald-wallet.code-workspace new file mode 100644 index 000000000..4199273f1 --- /dev/null +++ b/.vscode/emerald-wallet.code-workspace @@ -0,0 +1,82 @@ +{ + "extensions": { + "recommendations": [ + "christian-kohler.npm-intellisense", + "dbaeumer.vscode-eslint", + "editorconfig.editorconfig", + "esbenp.prettier-vscode", + "orta.vscode-jest", + "rust-lang.rust-analyzer", + "serayuzgur.crates", + "tamasfe.even-better-toml", + "visualstudioexptteam.vscodeintellicode" + ] + }, + "folders": [ + { + "name": "emerald-wallet", + "path": ".." + }, + { + "name": "@emeraldwallet/core", + "path": "../packages/core" + }, + { + "name": "@emeraldwallet/desktop", + "path": "../packages/desktop" + }, + { + "name": "@emeraldwallet/electron-app", + "path": "../packages/electron-app" + }, + { + "name": "@emeraldwallet/persistent-state", + "path": "../packages/persistent-state" + }, + { + "name": "@emeraldwallet/persistent-state/native", + "path": "../packages/persistent-state/native" + }, + { + "name": "@emeraldwallet/react-app", + "path": "../packages/react-app" + }, + { + "name": "@emeraldwallet/services", + "path": "../packages/services" + }, + { + "name": "@emeraldwallet/store", + "path": "../packages/store" + }, + { + "name": "@emeraldwallet/ui", + "path": "../packages/ui" + } + ], + "settings": { + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "jest.disabledWorkspaceFolders": [ + "emerald-wallet", + "@emeraldwallet/electron-app", + "@emeraldwallet/persistent-state/native" + ], + "npm.packageManager": "yarn", + "search.exclude": { + "**/.emerald-dev": true, + "**/.tests": true, + "**/app": true, + "**/lib": true, + "**/node_modules": true, + "**/target": true, + "*.tsbuildinfo": true, + "yarn.lock": true + }, + "[rust]": { + "editor.defaultFormatter": "rust-lang.rust-analyzer" + } + } +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index e263d09cb..000000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "recommendations": [ - "christian-kohler.npm-intellisense", - "dbaeumer.vscode-eslint", - "editorconfig.editorconfig", - "esbenp.prettier-vscode", - "rust-lang.rust-analyzer", - "serayuzgur.crates", - "tamasfe.even-better-toml", - "visualstudioexptteam.vscodeintellicode" - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 85227d27f..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - }, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "npm.packageManager": "yarn", - "rust-analyzer.linkedProjects": ["packages/persistent-state/native/Cargo.toml"], - "[rust]": { - "editor.defaultFormatter": "rust-lang.rust-analyzer" - } -} diff --git a/packages/core/src/blockchains/blockchains.ts b/packages/core/src/blockchains/blockchains.ts index 57c19a3ed..b9665440a 100644 --- a/packages/core/src/blockchains/blockchains.ts +++ b/packages/core/src/blockchains/blockchains.ts @@ -157,7 +157,11 @@ export function isBitcoin(code: BlockchainCode): boolean { export const WEIS_GOERLI = new Units([ new Unit(0, 'Goerli Wei', 'WeiG'), + new Unit(3, 'Goerli Kwei', 'KWeiG'), + new Unit(6, 'Goerli Mwei', 'MWeiG'), new Unit(9, 'Goerli Gwei', 'GWeiG'), + new Unit(12, 'Goerli Microether', 'μETG'), + new Unit(15, 'Goerli Milliether', 'mETG'), new Unit(18, 'Goerli Ether', 'ETG'), ]); diff --git a/packages/core/src/convert.spec.ts b/packages/core/src/convert.spec.ts index b3495a464..cbb2c7bfc 100644 --- a/packages/core/src/convert.spec.ts +++ b/packages/core/src/convert.spec.ts @@ -1,9 +1,7 @@ import BigNumber from 'bignumber.js'; import * as convert from './convert'; -const { - toNumber, toHex, toBigNumber, quantitiesToHex, -} = convert; +const { toNumber, toHex, toBigNumber } = convert; test('toNumber should convert hex string to number', () => { expect(toNumber('0x01')).toBe(1); @@ -20,29 +18,17 @@ test('toNumber should convert number to number', () => { }); test('toNumber should accept empty', () => { - // @ts-ignore expect(toNumber(null)).toBe(0); - // @ts-ignore expect(toNumber(undefined)).toBe(0); - - // @ts-ignore expect(toNumber(null, -1)).toBe(-1); - // @ts-ignore expect(toNumber(undefined, 10)).toBe(10); }); -describe('quantitiesToHex', () => { - it('converts without leading zeros', () => { - expect(quantitiesToHex(1024)).toEqual('0x400'); - expect(quantitiesToHex(0)).toEqual('0x0'); - }); -}); - describe('toHex', () => { it('convert decimal number to hex', () => { - expect(toHex(10000000000)).toEqual('0x02540be400'); + expect(toHex(10000000000)).toEqual('0x2540be400'); expect(toHex('21000')).toEqual('0x5208'); - expect(toHex('100000000000000000000')).toEqual('0x056bc75e2d63100000'); + expect(toHex('100000000000000000000')).toEqual('0x56bc75e2d63100000'); }); it('convert BigNumber to hex', () => { @@ -50,7 +36,7 @@ describe('toHex', () => { }); it('convert hex to hex', () => { - expect(toHex('0x01')).toEqual('0x01'); + expect(toHex('0x1')).toEqual('0x1'); }); }); diff --git a/packages/core/src/convert.ts b/packages/core/src/convert.ts index daff48f49..3ffb2d998 100644 --- a/packages/core/src/convert.ts +++ b/packages/core/src/convert.ts @@ -1,32 +1,34 @@ import BigNumber from 'bignumber.js'; +type Numeric = number | string | null | undefined; + /** - * Convert hex string to number - * - * @param value - * @param defaultValue - * @returns {number} + * Convert hex to number */ -export function toNumber(value: string | number, defaultValue = 0): number { - if (!value) { +export function toNumber(hex: Numeric, defaultValue = 0): number { + if (hex == null) { return defaultValue; } - if (typeof value === 'number') { - return value; + if (typeof hex === 'number') { + return hex; } - if (value === '0x') { + if (hex === '0x') { return 0; } - return parseInt(value.substring(2), 16); + return parseInt(hex, 16); } /** * Converts number, string or hex string into BigNumber */ -export function toBigNumber(value: BigNumber | number | string): BigNumber { +export function toBigNumber(value: BigNumber | Numeric, defaultValue = new BigNumber(0)): BigNumber { + if (value == null) { + return defaultValue; + } + if (value instanceof BigNumber) { return value; } @@ -36,20 +38,27 @@ export function toBigNumber(value: BigNumber | number | string): BigNumber { return new BigNumber(0); } - if (value.substring(0, 2) === '0x') { - return new BigNumber(value.substring(2), 16); - } + return new BigNumber(value, 16); } return new BigNumber(value, 10); } -export function toHex(val: number | string | BigNumber): string { - const hex = new BigNumber(val).toString(16); +/** + * Converts number, string or BigNumber into hex string + */ +export function toHex(value: BigNumber | Numeric, defaultValue = '0x'): string { + if (value == null) { + return defaultValue; + } - return `0x${hex.length % 2 !== 0 ? `0${hex}` : hex}`; -} + let hex: string; + + if (BigNumber.isBigNumber(value)) { + hex = value.toString(16); + } else { + hex = new BigNumber(value).toString(16); + } -export function quantitiesToHex(val: number | string): string { - return `0x${new BigNumber(val).toString(16)}`; + return `0x${hex}`; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8b94cecc0..385320006 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -76,4 +76,4 @@ export { formatFiatAmountPartial, } from './format'; export { getStandardUnits } from './asset'; -export { quantitiesToHex, toBigNumber, toHex, toNumber } from './convert'; +export { toBigNumber, toHex, toNumber } from './convert'; diff --git a/packages/core/src/transaction/ethereum.ts b/packages/core/src/transaction/ethereum.ts index 6b3270b33..cf240894c 100644 --- a/packages/core/src/transaction/ethereum.ts +++ b/packages/core/src/transaction/ethereum.ts @@ -1,4 +1,4 @@ -import BigNumber from 'bignumber.js'; +import { BigAmount } from '@emeraldpay/bigamount'; import { BlockchainCode } from '../blockchains'; export const DEFAULT_GAS_LIMIT = 21000 as const; @@ -46,16 +46,16 @@ export interface EthereumTransaction { blockchain: BlockchainCode; blockNumber?: number; from: string; - gas: number | string; - gasPrice?: string | BigNumber; - maxGasPrice?: string | BigNumber; - priorityGasPrice?: string | BigNumber; + gas: number; + gasPrice?: BigAmount; + maxGasPrice?: BigAmount; + priorityGasPrice?: BigAmount; hash?: string; data?: string; - nonce?: number | string; + nonce?: number; to?: string; type: EthereumTransactionType; - value: string | BigNumber; + value: BigAmount; } export interface EthereumRawReceipt { diff --git a/packages/core/src/workflow/create-tx/CreateBitcoinTx.ts b/packages/core/src/workflow/create-tx/CreateBitcoinTx.ts index cb7e77fc3..59d1e6972 100644 --- a/packages/core/src/workflow/create-tx/CreateBitcoinTx.ts +++ b/packages/core/src/workflow/create-tx/CreateBitcoinTx.ts @@ -1,4 +1,4 @@ -import { BigAmount, CreateAmount, Units } from '@emeraldpay/bigamount'; +import { BigAmount, CreateAmount } from '@emeraldpay/bigamount'; import { BitcoinEntry, EntryId, UnsignedBitcoinTx } from '@emeraldpay/emerald-vault-core'; import { BlockchainCode, InputUtxo, amountDecoder, amountFactory, blockchainIdToCode } from '../../blockchains'; import { TxTarget, ValidationResult } from './types'; @@ -92,7 +92,6 @@ export class CreateBitcoinTx implements BitcoinTx { private readonly amountDecoder: (value: string) => BigAmount; private readonly amountFactory: CreateAmount; - private readonly amountUnits: Units; private readonly blockchain: BlockchainCode; private readonly utxo: InputUtxo[]; private readonly zero: BigAmount; @@ -110,7 +109,6 @@ export class CreateBitcoinTx implements BitcoinTx { this.amountFactory = amountFactory(this.blockchain); this.zero = this.amountFactory(0); - this.amountUnits = this.zero.units; this.vkbPrice = this.amountFactory(DEFAULT_VKB_FEE); } diff --git a/packages/core/src/workflow/create-tx/CreateErc20ApproveTx.spec.ts b/packages/core/src/workflow/create-tx/CreateErc20ApproveTx.spec.ts index ece0c0743..c3ca520fc 100644 --- a/packages/core/src/workflow/create-tx/CreateErc20ApproveTx.spec.ts +++ b/packages/core/src/workflow/create-tx/CreateErc20ApproveTx.spec.ts @@ -174,6 +174,6 @@ describe('CreateErc20ApproveTx', () => { type: EthereumTransactionType.EIP1559, }); - expect(tx.build().data.length).toBeGreaterThan(2); + expect(tx.build().data?.length).toBeGreaterThan(2); }); }); diff --git a/packages/core/src/workflow/create-tx/CreateErc20ApproveTx.ts b/packages/core/src/workflow/create-tx/CreateErc20ApproveTx.ts index 76beed449..3fe4c90c4 100644 --- a/packages/core/src/workflow/create-tx/CreateErc20ApproveTx.ts +++ b/packages/core/src/workflow/create-tx/CreateErc20ApproveTx.ts @@ -81,12 +81,9 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { this.zeroAmount = amountFactory(details.blockchain)(0); - if (this.type === EthereumTransactionType.EIP1559) { - this.maxGasPrice = details.maxGasPrice ?? this.zeroAmount; - this.priorityGasPrice = details.priorityGasPrice ?? this.zeroAmount; - } else { - this.gasPrice = details.gasPrice ?? this.zeroAmount; - } + this.gasPrice = details.gasPrice ?? this.zeroAmount; + this.maxGasPrice = details.maxGasPrice ?? this.zeroAmount; + this.priorityGasPrice = details.priorityGasPrice ?? this.zeroAmount; } static fromPlain(details: Erc20ApproveTxDetails): CreateErc20ApproveTx { @@ -126,34 +123,24 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { } build(): EthereumTransaction { - const { - amount, - blockchain, - gas, - gasPrice, - maxGasPrice, - priorityGasPrice, - type, - allowFor = '', - approveBy = '', - } = this; + const { blockchain, gas, gasPrice, maxGasPrice, priorityGasPrice, type, allowFor = '', approveBy = '' } = this; const data = this.tokenContract.functionToData('approve', { _spender: allowFor, - _amount: amount.number.toFixed(), + _amount: this.amount.number.toFixed(), }); return { blockchain, - gas, data, + gas, + gasPrice, + maxGasPrice, + priorityGasPrice, type, from: approveBy, - gasPrice: gasPrice?.number, - maxGasPrice: maxGasPrice?.number, - priorityGasPrice: priorityGasPrice?.number, - to: this._token.address, - value: this.zeroAmount.number, + to: this.token.address, + value: this.zeroAmount, }; } @@ -176,7 +163,8 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { } getFees(): BigAmount { - const gasPrice = this.maxGasPrice ?? this.gasPrice ?? this.zeroAmount; + const gasPrice = + (this.type === EthereumTransactionType.EIP1559 ? this.maxGasPrice : this.gasPrice) ?? this.zeroAmount; return gasPrice.multiply(this.gas); } diff --git a/packages/core/src/workflow/create-tx/CreateErc20Tx.spec.ts b/packages/core/src/workflow/create-tx/CreateErc20Tx.spec.ts index 82a7773b9..e93f426d7 100644 --- a/packages/core/src/workflow/create-tx/CreateErc20Tx.spec.ts +++ b/packages/core/src/workflow/create-tx/CreateErc20Tx.spec.ts @@ -97,7 +97,7 @@ describe('CreateErc20Tx', () => { tx.amount = daiToken.getAmount(100); expect(tx.getChange()).toBeDefined(); - expect(tx.getChange().equals(daiToken.getAmount(0))).toBeTruthy(); + expect(tx.getChange()?.equals(daiToken.getAmount(0))).toBeTruthy(); }); it('has change', () => { @@ -109,7 +109,7 @@ describe('CreateErc20Tx', () => { tx.amount = daiToken.getAmount(50); expect(tx.getChange()).toBeDefined(); - expect(tx.getChange().equals(daiToken.getAmount(50))).toBeTruthy(); + expect(tx.getChange()?.equals(daiToken.getAmount(50))).toBeTruthy(); }); it('change is null if total not set', () => { @@ -243,7 +243,6 @@ describe('CreateErc20Tx', () => { amountDecimals: 8, asset: '0x6B175474E89094C44Da98b954EedeAC495271d0F', blockchain: BlockchainCode.ETH, - erc20: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', from: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', gas: 42011, maxGasPrice: '10007000000/WEI', @@ -259,7 +258,7 @@ describe('CreateErc20Tx', () => { expect(tx.from).toEqual('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD'); expect(tx.totalBalance != null ? tx.totalBalance : null).toEqual(new Wei('1000000000057', 'WEI')); - expect(tx.totalTokenBalance.equals(daiToken.getAmount('2000000000015'))).toBeTruthy(); + expect(tx.totalTokenBalance?.equals(daiToken.getAmount('2000000000015'))).toBeTruthy(); expect(tx.to).toEqual('0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'); expect(tx.target).toEqual(TxTarget.SEND_ALL); expect(tx.amount.equals(daiToken.getAmount('999580000000500002'))).toBeTruthy(); diff --git a/packages/core/src/workflow/create-tx/CreateErc20Tx.ts b/packages/core/src/workflow/create-tx/CreateErc20Tx.ts index 9f7b61a65..e31e9d7d8 100644 --- a/packages/core/src/workflow/create-tx/CreateErc20Tx.ts +++ b/packages/core/src/workflow/create-tx/CreateErc20Tx.ts @@ -1,5 +1,6 @@ import { BigAmount } from '@emeraldpay/bigamount'; import { WeiAny } from '@emeraldpay/bigamount-crypto'; +import BigNumber from 'bignumber.js'; import { DisplayErc20Tx, DisplayTx } from '..'; import { BlockchainCode, Token, TokenRegistry, amountDecoder, amountFactory, tokenAbi } from '../../blockchains'; import { Contract } from '../../Contract'; @@ -136,12 +137,9 @@ export class CreateERC20Tx implements ERC20TxDetails, Tx { const zeroAmount = amountFactory(details.blockchain)(0) as WeiAny; - if (details.type === EthereumTransactionType.EIP1559) { - this.maxGasPrice = details.maxGasPrice ?? zeroAmount; - this.priorityGasPrice = details.priorityGasPrice ?? zeroAmount; - } else { - this.gasPrice = details.gasPrice ?? zeroAmount; - } + this.gasPrice = details.gasPrice ?? zeroAmount; + this.maxGasPrice = details.maxGasPrice ?? zeroAmount; + this.priorityGasPrice = details.priorityGasPrice ?? zeroAmount; this.tokenRegistry = tokenRegistry; @@ -161,8 +159,14 @@ export class CreateERC20Tx implements ERC20TxDetails, Tx { return this.asset; } - public setAmount(amount: BigAmount): void { - this.amount = amount; + public setAmount(amount: BigAmount | BigNumber): void { + if (BigAmount.is(amount)) { + this.amount = amount; + } else { + const { units } = this.amount; + + this.amount = new BigAmount(1, units).multiply(units.top.multiplier).multiply(amount); + } } public getChange(): BigAmount | null { @@ -174,7 +178,8 @@ export class CreateERC20Tx implements ERC20TxDetails, Tx { } public getFees(): WeiAny { - const gasPrice = this.maxGasPrice ?? this.gasPrice ?? this.zeroAmount; + const gasPrice = + (this.type === EthereumTransactionType.EIP1559 ? this.maxGasPrice : this.gasPrice) ?? this.zeroAmount; return gasPrice.multiply(this.gas); } @@ -206,9 +211,9 @@ export class CreateERC20Tx implements ERC20TxDetails, Tx { } public build(): EthereumTransaction { - const { amount, blockchain, gas, gasPrice, maxGasPrice, priorityGasPrice, to, type, from = '' } = this; + const { blockchain, gas, gasPrice, maxGasPrice, priorityGasPrice, to, type, from = '' } = this; - const value = amount.number.toFixed(); + const value = this.amount.number.toFixed(); const data = this.transferFrom == null @@ -220,12 +225,12 @@ export class CreateERC20Tx implements ERC20TxDetails, Tx { data, from, gas, + gasPrice, + maxGasPrice, + priorityGasPrice, type, - gasPrice: gasPrice?.number, - maxGasPrice: maxGasPrice?.number, - priorityGasPrice: priorityGasPrice?.number, to: this.token.address, - value: this.zeroAmount.number, + value: this.zeroAmount, }; } @@ -273,7 +278,9 @@ export class CreateERC20Tx implements ERC20TxDetails, Tx { public debug(): string { const change = this.getChange(); - const gasPrice = this.maxGasPrice ?? this.gasPrice ?? this.zeroAmount; + + const gasPrice = + (this.type === EthereumTransactionType.EIP1559 ? this.maxGasPrice : this.gasPrice) ?? this.zeroAmount; return ( `Send ${this.from} -> ${this.to} of ${JSON.stringify(this.amount)} ` + diff --git a/packages/core/src/workflow/create-tx/CreateErc20WrappedTx.ts b/packages/core/src/workflow/create-tx/CreateErc20WrappedTx.ts index 3598170ec..25e6367dc 100644 --- a/packages/core/src/workflow/create-tx/CreateErc20WrappedTx.ts +++ b/packages/core/src/workflow/create-tx/CreateErc20WrappedTx.ts @@ -49,12 +49,9 @@ export class CreateErc20WrappedTx { this.totalTokenBalance = details.totalTokenBalance; this.type = details.type; - if (details.type === EthereumTransactionType.EIP1559) { - this.maxGasPrice = details.maxGasPrice ?? zeroAmount; - this.priorityGasPrice = details.priorityGasPrice ?? zeroAmount; - } else { - this.gasPrice = details.gasPrice ?? zeroAmount; - } + this.gasPrice = details.gasPrice ?? zeroAmount; + this.maxGasPrice = details.maxGasPrice ?? zeroAmount; + this.priorityGasPrice = details.priorityGasPrice ?? zeroAmount; this.token = new Token(details.token); this.zeroAmount = zeroAmount; @@ -65,9 +62,9 @@ export class CreateErc20WrappedTx { } build(): EthereumTransaction { - const { amount, blockchain, gas, gasPrice, maxGasPrice, priorityGasPrice, totalBalance, type, address = '' } = this; + const { amount, blockchain, gas, gasPrice, maxGasPrice, priorityGasPrice, type, address: from = '' } = this; - const isDeposit = amount.units.equals(totalBalance.units); + const isDeposit = amount.units.equals(this.totalBalance.units); const data = isDeposit ? this.tokenContract.functionToData('deposit', {}) @@ -78,12 +75,12 @@ export class CreateErc20WrappedTx { data, gas, type, - from: address, - gasPrice: gasPrice?.number, - maxGasPrice: maxGasPrice?.number, - priorityGasPrice: priorityGasPrice?.number, + from, + gasPrice, + maxGasPrice, + priorityGasPrice, to: this.token.address, - value: isDeposit ? amount.number : this.zeroAmount.number, + value: isDeposit ? amount : this.zeroAmount, }; } @@ -105,7 +102,8 @@ export class CreateErc20WrappedTx { } getFees(): BigAmount { - const gasPrice = this.maxGasPrice ?? this.gasPrice ?? this.zeroAmount; + const gasPrice = + (this.type === EthereumTransactionType.EIP1559 ? this.maxGasPrice : this.gasPrice) ?? this.zeroAmount; return gasPrice.multiply(this.gas); } diff --git a/packages/core/src/workflow/create-tx/CreateEthereumTx.ts b/packages/core/src/workflow/create-tx/CreateEthereumTx.ts index 090dc9a38..16cf1fa1a 100644 --- a/packages/core/src/workflow/create-tx/CreateEthereumTx.ts +++ b/packages/core/src/workflow/create-tx/CreateEthereumTx.ts @@ -1,5 +1,6 @@ import { BigAmount } from '@emeraldpay/bigamount'; import { WeiAny } from '@emeraldpay/bigamount-crypto'; +import BigNumber from 'bignumber.js'; import { DisplayEtherTx, DisplayTx } from '..'; import { BlockchainCode, amountDecoder, amountFactory } from '../../blockchains'; import { DEFAULT_GAS_LIMIT, EthereumTransaction, EthereumTransactionType } from '../../transaction/ethereum'; @@ -99,12 +100,9 @@ export class CreateEthereumTx implements TxDetails, Tx { this.totalBalance = details.totalBalance; this.type = details.type; - if (details.type === EthereumTransactionType.EIP1559) { - this.maxGasPrice = details.maxGasPrice ?? zeroAmount; - this.priorityGasPrice = details.priorityGasPrice ?? zeroAmount; - } else { - this.gasPrice = details.gasPrice ?? zeroAmount; - } + this.gasPrice = details.gasPrice ?? zeroAmount; + this.maxGasPrice = details.maxGasPrice ?? zeroAmount; + this.priorityGasPrice = details.priorityGasPrice ?? zeroAmount; this.zeroAmount = zeroAmount; } @@ -117,8 +115,14 @@ export class CreateEthereumTx implements TxDetails, Tx { return this.amount; } - public setAmount(amount: WeiAny): void { - this.amount = amount; + public setAmount(amount: WeiAny | BigNumber): void { + if (WeiAny.is(amount)) { + this.amount = amount; + } else { + const { units } = this.amount; + + this.amount = new WeiAny(1, units).multiply(units.top.multiplier).multiply(amount); + } } public getAsset(): string { @@ -134,7 +138,8 @@ export class CreateEthereumTx implements TxDetails, Tx { } public getFees(): WeiAny { - const gasPrice = this.maxGasPrice ?? this.gasPrice ?? this.zeroAmount; + const gasPrice = + (this.type === EthereumTransactionType.EIP1559 ? this.maxGasPrice : this.gasPrice) ?? this.zeroAmount; return gasPrice.multiply(this.gas); } @@ -160,19 +165,9 @@ export class CreateEthereumTx implements TxDetails, Tx { } public build(): EthereumTransaction { - const { amount, blockchain, gas, gasPrice, maxGasPrice, priorityGasPrice, to, type, from = '' } = this; + const { blockchain, gas, gasPrice, maxGasPrice, priorityGasPrice, to, type, amount: value, from = '' } = this; - return { - blockchain, - from, - gas, - to, - type, - gasPrice: gasPrice?.number, - maxGasPrice: maxGasPrice?.number, - priorityGasPrice: priorityGasPrice?.number, - value: amount.number, - }; + return { blockchain, from, gas, gasPrice, maxGasPrice, priorityGasPrice, to, type, value }; } public display(): DisplayTx { @@ -233,7 +228,9 @@ export class CreateEthereumTx implements TxDetails, Tx { public debug(): string { const change = this.getChange(); - const gasPrice = this.maxGasPrice ?? this.gasPrice ?? this.zeroAmount; + + const gasPrice = + (this.type === EthereumTransactionType.EIP1559 ? this.maxGasPrice : this.gasPrice) ?? this.zeroAmount; return ( `Send ${this.from} -> ${this.to} of ${this.amount.toString()} ` + diff --git a/packages/core/src/workflow/create-tx/CreateTxConverter.spec.ts b/packages/core/src/workflow/create-tx/CreateTxConverter.spec.ts new file mode 100644 index 000000000..beff54244 --- /dev/null +++ b/packages/core/src/workflow/create-tx/CreateTxConverter.spec.ts @@ -0,0 +1,624 @@ +import { BigAmount } from '@emeraldpay/bigamount'; +import { Wei } from '@emeraldpay/bigamount-crypto'; +import { EthereumEntry } from '@emeraldpay/emerald-vault-core'; +import { TokenData, TokenRegistry, amountFactory, blockchainIdToCode } from '../../blockchains'; +import { DEFAULT_GAS_LIMIT, EthereumTransactionType } from '../../transaction/ethereum'; +import { CreateTxConverter, FeeRange } from './CreateTxConverter'; +import { TxTarget } from './types'; + +describe('CreateTxConverter', () => { + const tokenData: TokenData = { + name: 'Wrapped Ether', + blockchain: 100, + address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + symbol: 'WETH', + decimals: 18, + type: 'ERC20', + }; + + const tokenRegistry = new TokenRegistry([tokenData]); + + const ethEntry1: EthereumEntry = { + id: '50391c5d-a517-4b7a-9c42-1411e0603d30-0', + address: { + type: 'single', + value: '0x0', + }, + key: { + type: 'hd-path', + hdPath: "m/44'", + seedId: 'c782ff2b-ba6e-43e2-9e2d-92d05cc37b03', + }, + blockchain: 100, + createdAt: new Date(), + }; + const ethEntry2: EthereumEntry = { + id: '50391c5d-a517-4b7a-9c42-1411e0603d30-1', + address: { + type: 'single', + value: '0x1', + }, + key: { + type: 'hd-path', + hdPath: "m/44'", + seedId: 'c782ff2b-ba6e-43e2-9e2d-92d05cc37b03', + }, + blockchain: 100, + createdAt: new Date(), + }; + const etcEntry: EthereumEntry = { + id: '50391c5d-a517-4b7a-9c42-1411e0603d30-2', + address: { + type: 'single', + value: '0x2', + }, + key: { + type: 'hd-path', + hdPath: "m/44'", + seedId: 'c782ff2b-ba6e-43e2-9e2d-92d05cc37b03', + }, + blockchain: 101, + createdAt: new Date(), + }; + + const toAddress = '0x3'; + const ownerAddress = '0x4'; + + const feeRange: FeeRange = { + stdMaxGasPrice: new Wei(50), + highMaxGasPrice: new Wei(100), + lowMaxGasPrice: new Wei(10), + stdPriorityGasPrice: new Wei(5), + highPriorityGasPrice: new Wei(10), + lowPriorityGasPrice: new Wei(1), + }; + const zeroFeeRange: FeeRange = { + stdMaxGasPrice: Wei.ZERO, + highMaxGasPrice: Wei.ZERO, + lowMaxGasPrice: Wei.ZERO, + stdPriorityGasPrice: Wei.ZERO, + highPriorityGasPrice: Wei.ZERO, + lowPriorityGasPrice: Wei.ZERO, + }; + + function getBalance(entry: EthereumEntry, asset: string, ownerAddress?: string): BigAmount { + const blockchain = blockchainIdToCode(entry.blockchain); + + if (tokenRegistry.hasAddress(blockchain, asset)) { + const token = tokenRegistry.byAddress(blockchain, asset); + + if (ownerAddress == null) { + switch (entry.id) { + case ethEntry1.id: + return token.getAmount(110_000000); + case ethEntry2.id: + return token.getAmount(210_000000); + case etcEntry.id: + return token.getAmount(310_000000); + default: + return token.getAmount(0); + } + } + + switch (entry.id) { + case ethEntry1.id: + return token.getAmount(111_000000); + case ethEntry2.id: + return token.getAmount(211_000000); + case etcEntry.id: + return token.getAmount(311_000000); + default: + return token.getAmount(0); + } + } + + const factory = amountFactory(blockchain); + + switch (entry.id) { + case ethEntry1.id: + return factory(100_000000); + case ethEntry2.id: + return factory(200_000000); + case etcEntry.id: + return factory(300_000000); + default: + return factory(0); + } + } + + it('create initial ETH tx', () => { + const { createTx } = new CreateTxConverter( + { + feeRange, + asset: 'ETH', + entry: ethEntry1, + }, + tokenRegistry, + getBalance, + ); + + const factory = amountFactory(blockchainIdToCode(ethEntry1.blockchain)); + + expect(createTx.amount.equals(factory(0))).toBeTruthy(); + expect(createTx.from).toEqual(ethEntry1.address?.value); + expect(createTx.target).toEqual(TxTarget.MANUAL); + expect(createTx.totalBalance?.equals(factory(100_000000))).toBeTruthy(); + expect(createTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(createTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(createTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(createTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + }); + + it('create initial ERC20 tx', () => { + const { createTx } = new CreateTxConverter( + { + feeRange, + asset: tokenData.address, + entry: ethEntry1, + }, + tokenRegistry, + getBalance, + ); + + const isErc20 = CreateTxConverter.isErc20CreateTx(createTx, tokenRegistry); + + expect(isErc20).toBeTruthy(); + + if (isErc20) { + const blockchain = blockchainIdToCode(ethEntry1.blockchain); + + const factory = amountFactory(blockchain); + + const token = tokenRegistry.byAddress(blockchain, tokenData.address); + + expect(createTx.amount.equals(token.getAmount(0))).toBeTruthy(); + expect(createTx.from).toEqual(ethEntry1.address?.value); + expect(createTx.target).toEqual(TxTarget.MANUAL); + expect(createTx.totalBalance?.equals(factory(100_000000))).toBeTruthy(); + expect(createTx.totalTokenBalance?.equals(token.getAmount(110_000000))).toBeTruthy(); + expect(createTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(createTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(createTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(createTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + } + }); + + it('create initial ETC tx', () => { + const { createTx } = new CreateTxConverter( + { + feeRange, + asset: 'ETC', + entry: etcEntry, + }, + tokenRegistry, + getBalance, + ); + + const factory = amountFactory(blockchainIdToCode(etcEntry.blockchain)); + + expect(createTx.amount.equals(factory(0))).toBeTruthy(); + expect(createTx.from).toEqual(etcEntry.address?.value); + expect(createTx.target).toEqual(TxTarget.MANUAL); + expect(createTx.totalBalance?.equals(factory(300_000000))).toBeTruthy(); + expect(createTx.type).toEqual(EthereumTransactionType.LEGACY); + + expect(createTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(createTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(createTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + }); + + it('create initial ERC20 tx with allowance', () => { + const { createTx } = new CreateTxConverter( + { + feeRange, + ownerAddress, + asset: tokenData.address, + entry: ethEntry1, + }, + tokenRegistry, + getBalance, + ); + + const isErc20 = CreateTxConverter.isErc20CreateTx(createTx, tokenRegistry); + + expect(isErc20).toBeTruthy(); + + if (isErc20) { + const blockchain = blockchainIdToCode(ethEntry1.blockchain); + + const factory = amountFactory(blockchain); + + const token = tokenRegistry.byAddress(blockchain, tokenData.address); + + expect(createTx.amount.equals(token.getAmount(0))).toBeTruthy(); + expect(createTx.from).toEqual(ethEntry1.address?.value); + expect(createTx.target).toEqual(TxTarget.MANUAL); + expect(createTx.totalBalance?.equals(factory(100_000000))).toBeTruthy(); + expect(createTx.totalTokenBalance?.equals(token.getAmount(111_000000))).toBeTruthy(); + expect(createTx.transferFrom).toEqual(ownerAddress); + expect(createTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(createTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(createTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(createTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + } + }); + + it('change asset from ETH to ERC20 token', () => { + const { createTx: ethCreateTx } = new CreateTxConverter( + { + feeRange, + asset: 'ETH', + entry: ethEntry1, + }, + tokenRegistry, + getBalance, + ); + + ethCreateTx.amount = new Wei(1); + ethCreateTx.to = toAddress; + + const { createTx: erc20CreateTx } = new CreateTxConverter( + { + feeRange, + asset: tokenData.address, + entry: ethEntry1, + transaction: ethCreateTx.dump(), + }, + tokenRegistry, + getBalance, + ); + + const isErc20 = CreateTxConverter.isErc20CreateTx(erc20CreateTx, tokenRegistry); + + expect(isErc20).toBeTruthy(); + + if (isErc20) { + const blockchain = blockchainIdToCode(ethEntry1.blockchain); + + const factory = amountFactory(blockchain); + + const token = tokenRegistry.byAddress(blockchain, tokenData.address); + + expect(erc20CreateTx.amount.equals(token.getAmount(0))).toBeTruthy(); + expect(erc20CreateTx.from).toEqual(ethEntry1.address?.value); + expect(erc20CreateTx.to).toEqual(toAddress); + expect(erc20CreateTx.target).toEqual(TxTarget.MANUAL); + expect(erc20CreateTx.totalBalance?.equals(factory(100_000000))).toBeTruthy(); + expect(erc20CreateTx.totalTokenBalance?.equals(token.getAmount(110_000000))).toBeTruthy(); + expect(erc20CreateTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(erc20CreateTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(erc20CreateTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(erc20CreateTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + } + }); + + it('change asset from ETH to ERC20 token with max amount', () => { + const { createTx: ethCreateTx } = new CreateTxConverter( + { + feeRange, + asset: 'ETH', + entry: ethEntry1, + }, + tokenRegistry, + getBalance, + ); + + ethCreateTx.to = toAddress; + + ethCreateTx.target = TxTarget.SEND_ALL; + ethCreateTx.rebalance(); + + const { createTx: erc20CreateTx } = new CreateTxConverter( + { + feeRange, + asset: tokenData.address, + entry: ethEntry1, + transaction: ethCreateTx.dump(), + }, + tokenRegistry, + getBalance, + ); + + const isErc20 = CreateTxConverter.isErc20CreateTx(erc20CreateTx, tokenRegistry); + + expect(isErc20).toBeTruthy(); + + if (isErc20) { + const blockchain = blockchainIdToCode(ethEntry1.blockchain); + + const factory = amountFactory(blockchain); + + const token = tokenRegistry.byAddress(blockchain, tokenData.address); + + expect(erc20CreateTx.amount.equals(token.getAmount(110_000000))).toBeTruthy(); + expect(erc20CreateTx.from).toEqual(ethEntry1.address?.value); + expect(erc20CreateTx.to).toEqual(toAddress); + expect(erc20CreateTx.target).toEqual(TxTarget.SEND_ALL); + expect(erc20CreateTx.totalBalance?.equals(factory(100_000000))).toBeTruthy(); + expect(erc20CreateTx.totalTokenBalance?.equals(token.getAmount(110_000000))).toBeTruthy(); + expect(erc20CreateTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(erc20CreateTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(erc20CreateTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(erc20CreateTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + } + }); + + it('change asset from ERC20 token to ETH with max amount', () => { + const { createTx: ethCreateTx } = new CreateTxConverter( + { + feeRange, + asset: tokenData.address, + entry: ethEntry1, + }, + tokenRegistry, + getBalance, + ); + + ethCreateTx.to = toAddress; + + ethCreateTx.target = TxTarget.SEND_ALL; + ethCreateTx.rebalance(); + + const { createTx: erc20CreateTx } = new CreateTxConverter( + { + feeRange, + asset: 'ETH', + entry: ethEntry1, + transaction: ethCreateTx.dump(), + }, + tokenRegistry, + getBalance, + ); + + expect(CreateTxConverter.isErc20CreateTx(erc20CreateTx, tokenRegistry)).toBeFalsy(); + + const blockchain = blockchainIdToCode(ethEntry1.blockchain); + + const factory = amountFactory(blockchain); + + const fee = feeRange.stdMaxGasPrice.multiply(DEFAULT_GAS_LIMIT); + + expect(erc20CreateTx.amount.equals(factory(100_000000).minus(fee))).toBeTruthy(); + expect(erc20CreateTx.from).toEqual(ethEntry1.address?.value); + expect(erc20CreateTx.to).toEqual(toAddress); + expect(erc20CreateTx.target).toEqual(TxTarget.SEND_ALL); + expect(erc20CreateTx.totalBalance?.equals(factory(100_000000))).toBeTruthy(); + expect(erc20CreateTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(erc20CreateTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(erc20CreateTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(erc20CreateTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + }); + + it('change entry from ETH to ETC', () => { + const { createTx: ethCreateTx } = new CreateTxConverter( + { + asset: 'ETH', + entry: ethEntry1, + feeRange: zeroFeeRange, + }, + tokenRegistry, + getBalance, + ); + + ethCreateTx.amount = amountFactory(blockchainIdToCode(ethEntry1.blockchain))(1); + ethCreateTx.to = toAddress; + + const { createTx: etcCreateTx } = new CreateTxConverter( + { + feeRange, + asset: 'ETC', + entry: etcEntry, + transaction: ethCreateTx.dump(), + }, + tokenRegistry, + getBalance, + ); + + const factory = amountFactory(blockchainIdToCode(etcEntry.blockchain)); + + expect(etcCreateTx.amount.equals(factory(0))).toBeTruthy(); + expect(etcCreateTx.from).toEqual(etcEntry.address?.value); + expect(etcCreateTx.to).toEqual(toAddress); + expect(etcCreateTx.target).toEqual(TxTarget.MANUAL); + expect(etcCreateTx.totalBalance?.equals(factory(300_000000))).toBeTruthy(); + expect(etcCreateTx.type).toEqual(EthereumTransactionType.LEGACY); + + expect(etcCreateTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(etcCreateTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(etcCreateTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + }); + + it('change entry from ETH to other ETH', () => { + const { createTx: eth1CreateTx } = new CreateTxConverter( + { + asset: 'ETH', + entry: ethEntry1, + feeRange: zeroFeeRange, + }, + tokenRegistry, + getBalance, + ); + + eth1CreateTx.amount = amountFactory(blockchainIdToCode(ethEntry1.blockchain))(1); + eth1CreateTx.to = toAddress; + + const { createTx: eth2CreateTx } = new CreateTxConverter( + { + feeRange, + asset: 'ETH', + entry: ethEntry2, + transaction: eth1CreateTx.dump(), + }, + tokenRegistry, + getBalance, + ); + + const factory = amountFactory(blockchainIdToCode(ethEntry2.blockchain)); + + expect(eth2CreateTx.amount.equals(factory(1))).toBeTruthy(); + expect(eth2CreateTx.from).toEqual(ethEntry2.address?.value); + expect(eth2CreateTx.to).toEqual(toAddress); + expect(eth2CreateTx.target).toEqual(TxTarget.MANUAL); + expect(eth2CreateTx.totalBalance?.equals(factory(200_000000))).toBeTruthy(); + expect(eth2CreateTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(eth2CreateTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(eth2CreateTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(eth2CreateTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + }); + + it('restore ETH tx', () => { + const { createTx: ethCreateTx } = new CreateTxConverter( + { + feeRange, + asset: 'ETH', + entry: ethEntry1, + }, + tokenRegistry, + getBalance, + ); + + ethCreateTx.to = toAddress; + + ethCreateTx.target = TxTarget.SEND_ALL; + ethCreateTx.rebalance(); + + const { createTx: restoredEthCreateTx } = new CreateTxConverter( + { + feeRange, + asset: 'ETH', + entry: ethEntry1, + transaction: ethCreateTx.dump(), + }, + tokenRegistry, + getBalance, + ); + + const factory = amountFactory(blockchainIdToCode(ethEntry1.blockchain)); + + const fee = feeRange.stdMaxGasPrice.multiply(DEFAULT_GAS_LIMIT); + + expect(restoredEthCreateTx.amount.equals(factory(100_000000).minus(fee))).toBeTruthy(); + expect(restoredEthCreateTx.from).toEqual(ethEntry1.address?.value); + expect(restoredEthCreateTx.to).toEqual(toAddress); + expect(restoredEthCreateTx.target).toEqual(TxTarget.SEND_ALL); + expect(restoredEthCreateTx.totalBalance?.equals(factory(100_000000))).toBeTruthy(); + expect(restoredEthCreateTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(restoredEthCreateTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(restoredEthCreateTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(restoredEthCreateTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + }); + + it('restore ERC20 tx', () => { + const { createTx: erc20CreateTx } = new CreateTxConverter( + { + feeRange, + asset: tokenData.address, + entry: ethEntry1, + }, + tokenRegistry, + getBalance, + ); + + erc20CreateTx.to = toAddress; + + erc20CreateTx.target = TxTarget.SEND_ALL; + erc20CreateTx.rebalance(); + + const { createTx: restoredErc20CreateTx } = new CreateTxConverter( + { + feeRange, + asset: tokenData.address, + entry: ethEntry1, + transaction: erc20CreateTx.dump(), + }, + tokenRegistry, + getBalance, + ); + + const isErc20 = CreateTxConverter.isErc20CreateTx(restoredErc20CreateTx, tokenRegistry); + + expect(isErc20).toBeTruthy(); + + if (isErc20) { + const blockchain = blockchainIdToCode(ethEntry1.blockchain); + + const factory = amountFactory(blockchain); + + const token = tokenRegistry.byAddress(blockchain, tokenData.address); + + expect(restoredErc20CreateTx.amount.equals(token.getAmount(110_000000))).toBeTruthy(); + expect(restoredErc20CreateTx.from).toEqual(ethEntry1.address?.value); + expect(restoredErc20CreateTx.to).toEqual(toAddress); + expect(restoredErc20CreateTx.target).toEqual(TxTarget.SEND_ALL); + expect(restoredErc20CreateTx.totalBalance?.equals(factory(100_000000))).toBeTruthy(); + expect(restoredErc20CreateTx.totalTokenBalance?.equals(token.getAmount(110_000000))).toBeTruthy(); + expect(restoredErc20CreateTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(restoredErc20CreateTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(restoredErc20CreateTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(restoredErc20CreateTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + } + }); + + it('restore ERC20 tx with allowance', () => { + const { createTx: erc20CreateTx } = new CreateTxConverter( + { + feeRange, + asset: tokenData.address, + entry: ethEntry1, + }, + tokenRegistry, + getBalance, + ); + + erc20CreateTx.to = toAddress; + + erc20CreateTx.target = TxTarget.SEND_ALL; + erc20CreateTx.rebalance(); + + const { createTx: restoredErc20CreateTx } = new CreateTxConverter( + { + feeRange, + ownerAddress, + asset: tokenData.address, + entry: ethEntry1, + transaction: erc20CreateTx.dump(), + }, + tokenRegistry, + getBalance, + ); + + const isErc20 = CreateTxConverter.isErc20CreateTx(restoredErc20CreateTx, tokenRegistry); + + expect(isErc20).toBeTruthy(); + + if (isErc20) { + const blockchain = blockchainIdToCode(ethEntry1.blockchain); + + const factory = amountFactory(blockchain); + + const token = tokenRegistry.byAddress(blockchain, tokenData.address); + + expect(restoredErc20CreateTx.amount.equals(token.getAmount(111_000000))).toBeTruthy(); + expect(restoredErc20CreateTx.from).toEqual(ethEntry1.address?.value); + expect(restoredErc20CreateTx.to).toEqual(toAddress); + expect(restoredErc20CreateTx.target).toEqual(TxTarget.SEND_ALL); + expect(restoredErc20CreateTx.totalBalance?.equals(factory(100_000000))).toBeTruthy(); + expect(restoredErc20CreateTx.totalTokenBalance?.equals(token.getAmount(111_000000))).toBeTruthy(); + expect(restoredErc20CreateTx.transferFrom).toEqual(ownerAddress); + expect(restoredErc20CreateTx.type).toEqual(EthereumTransactionType.EIP1559); + + expect(restoredErc20CreateTx.gasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(restoredErc20CreateTx.maxGasPrice?.equals(feeRange.stdMaxGasPrice)).toBeTruthy(); + expect(restoredErc20CreateTx.priorityGasPrice?.equals(feeRange.stdPriorityGasPrice)).toBeTruthy(); + } + }); +}); diff --git a/packages/core/src/workflow/create-tx/CreateTxConverter.ts b/packages/core/src/workflow/create-tx/CreateTxConverter.ts new file mode 100644 index 000000000..2e5c2fb03 --- /dev/null +++ b/packages/core/src/workflow/create-tx/CreateTxConverter.ts @@ -0,0 +1,181 @@ +import { BigAmount } from '@emeraldpay/bigamount'; +import { WeiAny } from '@emeraldpay/bigamount-crypto'; +import { EthereumEntry } from '@emeraldpay/emerald-vault-core'; +import { Blockchains, TokenRegistry, amountFactory, blockchainIdToCode } from '../../blockchains'; +import { EthereumTransactionType } from '../../transaction/ethereum'; +import { CreateERC20Tx } from './CreateErc20Tx'; +import { CreateEthereumTx } from './CreateEthereumTx'; +import { TxDetailsPlain, TxTarget } from './types'; + +export type CreateTx = CreateEthereumTx | CreateERC20Tx; + +export interface FeeRange { + stdMaxGasPrice: T; + lowMaxGasPrice: T; + highMaxGasPrice: T; + stdPriorityGasPrice: T; + lowPriorityGasPrice: T; + highPriorityGasPrice: T; +} + +type GetBalance = (entry: EthereumEntry, asset: string, ownerAddress?: string) => BigAmount; + +interface TxOrigin { + asset: string; + entry: EthereumEntry; + feeRange: FeeRange; + ownerAddress?: string; + transaction?: TxDetailsPlain; +} + +export class CreateTxConverter { + private asset: string; + private entry: EthereumEntry; + private feeRange: FeeRange; + private ownerAddress?: string; + private transaction?: TxDetailsPlain; + + private readonly tokenRegistry: TokenRegistry; + + private readonly getBalance: GetBalance; + + constructor( + { asset, entry, feeRange, ownerAddress, transaction }: TxOrigin, + tokenRegistry: TokenRegistry, + getBalance: GetBalance, + ) { + this.asset = asset; + this.entry = entry; + this.feeRange = feeRange; + this.ownerAddress = ownerAddress; + this.transaction = transaction; + + this.tokenRegistry = tokenRegistry; + + this.getBalance = getBalance; + } + + static fromPlain(transaction: TxDetailsPlain, tokenRegistry: TokenRegistry): CreateTx { + if (tokenRegistry.hasAddress(transaction.blockchain, transaction.asset)) { + return CreateERC20Tx.fromPlain(tokenRegistry, transaction); + } + + return CreateEthereumTx.fromPlain(transaction); + } + + static isErc20CreateTx(createTx: CreateTx, tokenRegistry: TokenRegistry): createTx is CreateERC20Tx { + return tokenRegistry.hasAddress(createTx.blockchain, createTx.getAsset()); + } + + get createTx(): CreateTx { + const { asset, entry, feeRange, ownerAddress, tokenRegistry, transaction } = this; + + const blockchain = blockchainIdToCode(entry.blockchain); + const { coinTicker, eip1559: supportEip1559 = false } = Blockchains[blockchain].params; + + let createTx: CreateTx; + + if (transaction == null) { + if (tokenRegistry.hasAddress(blockchain, asset)) { + createTx = new CreateERC20Tx(tokenRegistry, asset, blockchain); + createTx.totalBalance = this.getBalance(entry, coinTicker) as WeiAny; + createTx.totalTokenBalance = this.getBalance(entry, asset, ownerAddress); + createTx.transferFrom = ownerAddress; + } else { + createTx = new CreateEthereumTx(null, blockchain); + createTx.totalBalance = this.getBalance(entry, asset) as WeiAny; + } + + createTx.from = entry.address?.value; + + createTx.gasPrice = feeRange.stdMaxGasPrice; + createTx.maxGasPrice = feeRange.stdMaxGasPrice; + createTx.priorityGasPrice = feeRange.stdPriorityGasPrice; + + createTx.type = supportEip1559 ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY; + } else { + createTx = CreateTxConverter.fromPlain(transaction, tokenRegistry); + + if (asset !== createTx.getAsset() || blockchain !== createTx.blockchain) { + const type = supportEip1559 ? createTx.type : EthereumTransactionType.LEGACY; + + let newCreateTx: CreateTx; + + if (tokenRegistry.hasAddress(blockchain, asset)) { + newCreateTx = new CreateERC20Tx(tokenRegistry, asset, blockchain, type); + newCreateTx.totalBalance = this.getBalance(entry, coinTicker) as WeiAny; + newCreateTx.totalTokenBalance = this.getBalance(entry, asset, newCreateTx.transferFrom); + + newCreateTx.transferFrom = CreateTxConverter.isErc20CreateTx(createTx, tokenRegistry) + ? createTx.transferFrom ?? ownerAddress + : ownerAddress; + } else { + newCreateTx = new CreateEthereumTx(null, blockchain, type); + newCreateTx.totalBalance = this.getBalance(entry, asset) as WeiAny; + } + + newCreateTx.from = entry.address?.value; + newCreateTx.to = createTx.to; + + if (blockchain === createTx.blockchain && (createTx.gasPrice?.isPositive() ?? false)) { + newCreateTx.gasPrice = createTx.gasPrice; + newCreateTx.maxGasPrice = createTx.maxGasPrice; + newCreateTx.priorityGasPrice = createTx.priorityGasPrice; + } else { + newCreateTx.gasPrice = feeRange.stdMaxGasPrice; + newCreateTx.maxGasPrice = feeRange.stdMaxGasPrice; + newCreateTx.priorityGasPrice = feeRange.stdPriorityGasPrice; + } + + if (createTx.target === TxTarget.SEND_ALL) { + newCreateTx.target = TxTarget.SEND_ALL; + + if (!newCreateTx.rebalance()) { + newCreateTx.target = TxTarget.MANUAL; + + newCreateTx.amount = CreateTxConverter.isErc20CreateTx(newCreateTx, tokenRegistry) + ? tokenRegistry.byAddress(blockchain, newCreateTx.getAsset()).getAmount(0) + : amountFactory(blockchain)(0); + } + } + + return newCreateTx; + } + + const gasPrice = createTx.gasPrice ?? createTx.maxGasPrice; + + if (gasPrice?.isZero() ?? true) { + createTx.gasPrice = feeRange.stdMaxGasPrice; + createTx.maxGasPrice = feeRange.stdMaxGasPrice; + createTx.priorityGasPrice = feeRange.stdPriorityGasPrice; + } + + if ( + transaction.from !== entry.address?.value || + transaction.transferFrom !== ownerAddress || + (transaction.transferFrom == null && ownerAddress != null) + ) { + createTx.from = entry.address?.value; + + if (CreateTxConverter.isErc20CreateTx(createTx, tokenRegistry)) { + createTx.transferFrom = ownerAddress; + + createTx.totalBalance = this.getBalance(entry, coinTicker) as WeiAny; + createTx.totalTokenBalance = this.getBalance(entry, asset, createTx.transferFrom); + } else { + createTx.totalBalance = this.getBalance(entry, asset) as WeiAny; + } + + if (createTx.target === TxTarget.SEND_ALL && !createTx.rebalance()) { + createTx.target = TxTarget.MANUAL; + + createTx.amount = CreateTxConverter.isErc20CreateTx(createTx, tokenRegistry) + ? tokenRegistry.byAddress(blockchain, createTx.getAsset()).getAmount(0) + : amountFactory(blockchain)(0); + } + } + } + + return createTx; + } +} diff --git a/packages/core/src/workflow/create-tx/types.ts b/packages/core/src/workflow/create-tx/types.ts index 3290f9dd5..491b3725c 100644 --- a/packages/core/src/workflow/create-tx/types.ts +++ b/packages/core/src/workflow/create-tx/types.ts @@ -1,4 +1,5 @@ import { BigAmount } from '@emeraldpay/bigamount'; +import BigNumber from 'bignumber.js'; import { BlockchainCode } from '../../blockchains'; export enum ValidationResult { @@ -20,7 +21,7 @@ export interface Tx { getAmount(): T; getAsset(): string; getTotalBalance(): T; - setAmount(amount: T, tokenSymbol?: string): void; + setAmount(amount: T | BigNumber, tokenSymbol?: string): void; setTotalBalance(total: T): void; } @@ -29,7 +30,6 @@ export interface TxDetailsPlain { amountDecimals: number; asset: string; blockchain: BlockchainCode; - erc20?: string; from?: string; gas: number; gasPrice?: string; diff --git a/packages/core/src/workflow/display/DisplayErc20Tx.ts b/packages/core/src/workflow/display/DisplayErc20Tx.ts index f6aff043f..ef7f0ed45 100644 --- a/packages/core/src/workflow/display/DisplayErc20Tx.ts +++ b/packages/core/src/workflow/display/DisplayErc20Tx.ts @@ -2,6 +2,7 @@ import { FormatterBuilder, Unit } from '@emeraldpay/bigamount'; import { Wei } from '@emeraldpay/bigamount-crypto'; import { CreateERC20Tx } from '..'; import { TokenRegistry } from '../../blockchains'; +import { EthereumTransactionType } from '../../transaction/ethereum'; import { DisplayTx } from './DisplayTx'; const formatter = new FormatterBuilder().useTopUnit().number(6, true).build(); @@ -44,7 +45,9 @@ export class DisplayErc20Tx implements DisplayTx { topUnit(): Unit { const { transaction } = this; - const gasPrice = transaction.maxGasPrice ?? transaction.gasPrice ?? Wei.ZERO; + const gasPrice = + (transaction.type === EthereumTransactionType.EIP1559 ? transaction.maxGasPrice : transaction.gasPrice) ?? + Wei.ZERO; return gasPrice.units.top; } diff --git a/packages/core/src/workflow/index.ts b/packages/core/src/workflow/index.ts index f20244474..4452150a5 100644 --- a/packages/core/src/workflow/index.ts +++ b/packages/core/src/workflow/index.ts @@ -1,10 +1,13 @@ -export { CreateBitcoinCancelTx } from './create-tx/CreateBitcoinCancelTx'; -export { BitcoinTx, BitcoinTxDetails, CreateBitcoinTx } from './create-tx/CreateBitcoinTx'; +/* eslint sort-exports/sort-exports: error */ + export { ApproveTarget, CreateErc20ApproveTx, Erc20ApproveTxDetails } from './create-tx/CreateErc20ApproveTx'; +export { BitcoinTx, BitcoinTxDetails, CreateBitcoinTx } from './create-tx/CreateBitcoinTx'; +export { CreateBitcoinCancelTx } from './create-tx/CreateBitcoinCancelTx'; export { CreateERC20Tx, ERC20TxDetails } from './create-tx/CreateErc20Tx'; export { CreateErc20WrappedTx, Erc20WrappedTxDetails } from './create-tx/CreateErc20WrappedTx'; export { CreateEthereumTx, TxDetails } from './create-tx/CreateEthereumTx'; -export { ValidationResult, TxTarget, TxDetailsPlain } from './create-tx/types'; +export { CreateTx, CreateTxConverter, FeeRange } from './create-tx/CreateTxConverter'; export { DisplayErc20Tx } from './display/DisplayErc20Tx'; export { DisplayEtherTx } from './display/DisplayEtherTx'; export { DisplayTx } from './display/DisplayTx'; +export { TxDetailsPlain, TxTarget, ValidationResult } from './create-tx/types'; diff --git a/packages/desktop/defaults.json b/packages/desktop/defaults.json index b289d6ad1..d8852fbe8 100644 --- a/packages/desktop/defaults.json +++ b/packages/desktop/defaults.json @@ -10,6 +10,11 @@ "fee_ttl.101": 86400, "fee_ttl.10003": 86400, "fee_ttl.10005": 86400, + "fee_range_ttl.1": 3600, + "fee_range_ttl.100": 3600, + "fee_range_ttl.101": 3600, + "fee_range_ttl.10003": 3600, + "fee_range_ttl.10005": 3600, "ledger_min_version.1": "0.0.0", "ledger_min_version.100": "0.0.0", "ledger_min_version.101": "0.0.0", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 6ea6af5a2..dea134951 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -69,7 +69,6 @@ "@babel/preset-env": "^7.22.9", "@babel/preset-react": "^7.22.5", "@babel/runtime": "^7.22.6", - "@kayahr/jest-electron-runner": "^29.7.0", "@types/jest": "^29.5.3", "@types/node": "^20.4.5", "@types/semver": "^7.5.0", @@ -77,7 +76,7 @@ "copy-webpack-plugin": "^11.0.0", "copyfiles": "^2.4.1", "css-loader": "^6.8.1", - "electron": "22.3.9", + "electron": "22.3.27", "electron-builder": "^24.6.3", "jest": "^29.6.2", "node-notifier-cli": "^2.0.0", @@ -159,7 +158,6 @@ }, "jest": { "preset": "ts-jest", - "runner": "@kayahr/jest-electron-runner/main", "testEnvironment": "node", "testPathIgnorePatterns": [ "/app" diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index d47696563..933347d02 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -27,7 +27,7 @@ "@types/jest": "^29.5.3", "@types/node": "^20.4.5", "@types/uuid": "^9.0.2", - "electron": "22.3.9", + "electron": "22.3.27", "jest": "^29.6.2", "neon-cli": "^0.10.1", "rimraf": "^5.0.1", diff --git a/packages/react-app/package.json b/packages/react-app/package.json index d508bf158..b87628d12 100644 --- a/packages/react-app/package.json +++ b/packages/react-app/package.json @@ -29,6 +29,7 @@ "@material-ui/core": "^4.12.4", "@material-ui/icons": "^4.11.3", "@material-ui/lab": "^4.0.0-alpha.61", + "bignumber.js": "8.0.2", "bip39": "^3.1.0", "bitcoin-address-validation": "^2.2.1", "bitcoinjs-lib": "^6.1.0", @@ -70,7 +71,7 @@ "@types/sortablejs": "^1.15.1", "copyfiles": "^2.4.1", "dotenv-webpack": "^8.0.1", - "electron": "22.3.9", + "electron": "22.3.27", "electron-devtools-installer": "^3.2.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.7", diff --git a/packages/react-app/src/app/screen/Screen/Screen.tsx b/packages/react-app/src/app/screen/Screen/Screen.tsx index c99b74b13..a9bd3398f 100644 --- a/packages/react-app/src/app/screen/Screen/Screen.tsx +++ b/packages/react-app/src/app/screen/Screen/Screen.tsx @@ -28,6 +28,7 @@ import CreateRecoverTransaction from '../../../transaction/CreateRecoverTransact import CreateSpeedUpTransaction from '../../../transaction/CreateSpeedUpTransaction'; import CreateTransaction from '../../../transaction/CreateTransaction'; import SelectAccount from '../../../transaction/CreateTransaction/SelectAccount'; +import CreateTransactionNew from '../../../transaction/CreateTransactionNew'; import WalletInfo from '../../../wallets/WalletInfo'; import GlobalKey from '../../vault/GlobalKey'; import ImportVault from '../../vault/ImportVault'; @@ -69,6 +70,8 @@ const Screen: React.FC = ({ restoreData, screenItem, term return ; case screen.Pages.CREATE_TX: return ; + case screen.Pages.CREATE_TX_NEW: + return ; case screen.Pages.CREATE_TX_APPROVE: return ; case screen.Pages.CREATE_TX_CONVERT: diff --git a/packages/react-app/src/common/EthTxSettings/EthTxSettings.tsx b/packages/react-app/src/common/EthTxSettings/EthTxSettings.tsx index 8ecc2de5b..2b21d7326 100644 --- a/packages/react-app/src/common/EthTxSettings/EthTxSettings.tsx +++ b/packages/react-app/src/common/EthTxSettings/EthTxSettings.tsx @@ -131,14 +131,19 @@ const EthTxSettings: React.FC = ({ const maxGasPriceByUnit = maxGasPrice.getNumberByUnit(gasPriceUnit).toFixed(2); const priorityGasPriceByUnit = priorityGasPrice.getNumberByUnit(gasPriceUnit).toFixed(2); + const showEip1559 = supportEip1559 && useEip1559; + + const showMaxRange = lowMaxGasPrice.isPositive() && highMaxGasPrice.isPositive(); + const showPriorityRange = lowPriorityGasPrice.isPositive() && highPriorityGasPrice.isPositive(); + return ( Settings - {useEip1559 ? 'EIP-1559' : 'Basic Type'} / {maxGasPriceByUnit} {gasPriceUnit.toString()} - {useEip1559 ? ' Max Gas Price' : ' Gas Price'} - {useEip1559 ? ` / ${priorityGasPriceByUnit} ${gasPriceUnit.toString()} Priority Gas Price` : null} + {showEip1559 ? 'EIP-1559' : 'Basic Type'} / {maxGasPriceByUnit} {gasPriceUnit.toString()} + {showEip1559 ? ' Max Gas Price' : ' Gas Price'} + {showEip1559 ? ` / ${priorityGasPriceByUnit} ${gasPriceUnit.toString()} Priority Gas Price` : null} } > @@ -154,14 +159,14 @@ const EthTxSettings: React.FC = ({ )} - {useEip1559 ? 'Max gas price' : 'Gas price'} + {showEip1559 ? 'Max gas price' : 'Gas price'} @@ -169,7 +174,7 @@ const EthTxSettings: React.FC = ({ label={currentUseStdMaxGasPrice ? 'Standard Price' : 'Custom Price'} /> - {!currentUseStdMaxGasPrice && ( + {!currentUseStdMaxGasPrice && showMaxRange && ( = ({ markLabel: styles.gasPriceMarkLabel, valueLabel: styles.gasPriceValueLabel, }} - getAriaValueText={() => `${maxGasPriceByUnit} ${gasPriceUnit.toString()}`} - aria-labelledby="discrete-slider" - valueLabelDisplay="auto" - step={0.01} marks={[ { value: lowMaxGasPrice.getNumberByUnit(gasPriceUnit).toNumber(), label: 'Slow' }, { value: highMaxGasPrice.getNumberByUnit(gasPriceUnit).toNumber(), label: 'Urgent' }, ]} min={lowMaxGasPrice.getNumberByUnit(gasPriceUnit).toNumber()} max={highMaxGasPrice.getNumberByUnit(gasPriceUnit).toNumber()} + step={0.01} value={maxGasPrice.getNumberByUnit(gasPriceUnit).toNumber()} + valueLabelDisplay="auto" onChange={handleMaxGasPriceChange} valueLabelFormat={(value) => value.toFixed(2)} /> @@ -195,12 +198,12 @@ const EthTxSettings: React.FC = ({ )} - {maxGasPrice.getNumberByUnit(gasPriceUnit).toFixed(2)} {gasPriceUnit.toString()} + {maxGasPriceByUnit} {gasPriceUnit.toString()} - {useEip1559 && ( + {showEip1559 && ( Priority gas price @@ -209,7 +212,7 @@ const EthTxSettings: React.FC = ({ control={ @@ -217,7 +220,7 @@ const EthTxSettings: React.FC = ({ label={currentUseStdPriorityGasPrice ? 'Standard Price' : 'Custom Price'} /> - {!currentUseStdPriorityGasPrice && ( + {!currentUseStdPriorityGasPrice && showPriorityRange && ( = ({ markLabel: styles.gasPriceMarkLabel, valueLabel: styles.gasPriceValueLabel, }} - getAriaValueText={() => `${priorityGasPriceByUnit} ${gasPriceUnit.toString()}`} - aria-labelledby="discrete-slider" - valueLabelDisplay="auto" - step={0.01} marks={[ { value: lowPriorityGasPrice.getNumberByUnit(gasPriceUnit).toNumber(), label: 'Slow' }, { value: highPriorityGasPrice.getNumberByUnit(gasPriceUnit).toNumber(), label: 'Urgent' }, ]} min={lowPriorityGasPrice.getNumberByUnit(gasPriceUnit).toNumber()} max={highPriorityGasPrice.getNumberByUnit(gasPriceUnit).toNumber()} + step={0.01} value={priorityGasPrice.getNumberByUnit(gasPriceUnit).toNumber()} + valueLabelDisplay="auto" onChange={handlePriorityGasPriceChange} valueLabelFormat={(value) => value.toFixed(2)} /> @@ -243,7 +244,7 @@ const EthTxSettings: React.FC = ({ )} - {priorityGasPrice.getNumberByUnit(gasPriceUnit).toFixed(2)} {gasPriceUnit.toString()} + {priorityGasPriceByUnit} {gasPriceUnit.toString()} diff --git a/packages/react-app/src/common/EthTxSettings/index.ts b/packages/react-app/src/common/EthTxSettings/index.ts new file mode 100644 index 000000000..729e91956 --- /dev/null +++ b/packages/react-app/src/common/EthTxSettings/index.ts @@ -0,0 +1 @@ +export { default as EthTxSettings } from './EthTxSettings'; diff --git a/packages/react-app/src/common/NumberField/NumberField.tsx b/packages/react-app/src/common/NumberField/NumberField.tsx new file mode 100644 index 000000000..765dc96ca --- /dev/null +++ b/packages/react-app/src/common/NumberField/NumberField.tsx @@ -0,0 +1,88 @@ +import { Input } from '@emeraldwallet/ui'; +import BigNumber from 'bignumber.js'; +import * as React from 'react'; + +interface OwnProps { + decimals?: number; + disabled?: boolean; + initialValue?: BigNumber; + rightIcon?: React.ReactElement; + value?: BigNumber; + width?: number; + onChange(value: BigNumber): void; +} + +const NumberField: React.FC = ({ + initialValue, + rightIcon, + value, + decimals = 9, + disabled = false, + width = 440, + onChange, +}) => { + const [currentValue, setCurrentValue] = React.useState(initialValue?.decimalPlaces(decimals).toString()); + + const [errorText, setErrorText] = React.useState(); + + const handleValueChange = ({ target: { value } }: React.ChangeEvent): void => { + const newValue = value.replace(',', '.').trim(); + + setCurrentValue(newValue); + + const zeroValue = new BigNumber(0); + + if (newValue.length === 0) { + setErrorText('Required'); + + onChange(zeroValue); + } else { + const valid = newValue.match(/^\d*(\.\d+)?$/); + + if (valid) { + try { + const parsed = parseFloat(newValue); + + if (parsed < 0) { + setErrorText('Value must be positive number'); + + onChange(zeroValue); + } else { + setErrorText(undefined); + + onChange(new BigNumber(newValue)); + } + } catch (exception) { + setErrorText('Invalid value'); + + onChange(zeroValue); + } + } else { + setErrorText('Invalid number'); + + onChange(zeroValue); + } + } + }; + + React.useEffect(() => { + if (value != null) { + setCurrentValue(value.decimalPlaces(decimals).toString()); + setErrorText(undefined); + } + }, [decimals, value]); + + return ( +
+ +
+ ); +}; + +export default NumberField; diff --git a/packages/react-app/src/common/NumberField/index.ts b/packages/react-app/src/common/NumberField/index.ts new file mode 100644 index 000000000..9d7552332 --- /dev/null +++ b/packages/react-app/src/common/NumberField/index.ts @@ -0,0 +1 @@ +export { default as NumberField } from './NumberField'; diff --git a/packages/react-app/src/common/SelectAsset/SelectAsset.spec.tsx b/packages/react-app/src/common/SelectAsset/SelectAsset.spec.tsx index 3a548fed9..775a70d97 100644 --- a/packages/react-app/src/common/SelectAsset/SelectAsset.spec.tsx +++ b/packages/react-app/src/common/SelectAsset/SelectAsset.spec.tsx @@ -23,7 +23,7 @@ describe('SelectAsset', () => { it('should not crash without onChange handler', () => { const wrapper = shallow(); - wrapper.instance().onChangeToken({ target: { value: 'ETC' } } as React.ChangeEvent); + wrapper.instance().handleAssetChange({ target: { value: 'ETC' } } as React.ChangeEvent); }); it('should render total balance', async () => { diff --git a/packages/react-app/src/common/SelectAsset/SelectAsset.tsx b/packages/react-app/src/common/SelectAsset/SelectAsset.tsx index 12d7b7a53..c78c857ef 100644 --- a/packages/react-app/src/common/SelectAsset/SelectAsset.tsx +++ b/packages/react-app/src/common/SelectAsset/SelectAsset.tsx @@ -1,5 +1,5 @@ import { BigAmount } from '@emeraldpay/bigamount'; -import { EthereumAddress, formatAmount, formatAmountPartial } from '@emeraldwallet/core'; +import { CurrencyAmount, EthereumAddress, formatAmount, formatAmountPartial } from '@emeraldwallet/core'; import { ListItemText, MenuItem, StyleRulesCallback, TextField, Theme, Tooltip, createStyles } from '@material-ui/core'; import { WithStyles, withStyles } from '@material-ui/styles'; import * as React from 'react'; @@ -18,8 +18,8 @@ interface Props { assets: Asset[]; balance?: BigAmount; disabled?: boolean; - fiatBalance?: BigAmount; - onChangeAsset?(token: string): void; + fiatBalance?: CurrencyAmount; + onChangeAsset?(asset: string): void; } const styles: StyleRulesCallback = (theme) => @@ -35,7 +35,7 @@ const styles: StyleRulesCallback = (theme) => }); export class SelectAsset extends React.Component> { - onChangeToken = ({ target: { value } }: React.ChangeEvent): void => { + handleAssetChange = ({ target: { value } }: React.ChangeEvent): void => { this.props.onChangeAsset?.(value); }; @@ -84,7 +84,7 @@ export class SelectAsset extends React.Component this.renderAsset(asset) }} > {assets.map(({ address, symbol, balance: assetBalance }) => { diff --git a/packages/react-app/src/common/SelectEntry/SelectEntry.tsx b/packages/react-app/src/common/SelectEntry/SelectEntry.tsx index a6676ead3..ac0749adf 100644 --- a/packages/react-app/src/common/SelectEntry/SelectEntry.tsx +++ b/packages/react-app/src/common/SelectEntry/SelectEntry.tsx @@ -1,90 +1,193 @@ import { WalletEntry, isEthereumEntry } from '@emeraldpay/emerald-vault-core'; import { amountFactory, blockchainIdToCode, formatAmount } from '@emeraldwallet/core'; -import { IState, accounts, tokens } from '@emeraldwallet/store'; -import { Account } from '@emeraldwallet/ui'; -import { Menu, MenuItem } from '@material-ui/core'; +import { IState, TokenBalanceBelong, accounts, allowances, tokens } from '@emeraldwallet/store'; +import { Account, Monospace } from '@emeraldwallet/ui'; +import { Menu, MenuItem, Typography } from '@material-ui/core'; import * as React from 'react'; import { connect } from 'react-redux'; interface OwnProps { disabled?: boolean; entries: WalletEntry[]; - selected: WalletEntry; - onSelect(entry: WalletEntry): void; + ownerAddress?: string | null; + selectedEntry: WalletEntry; + withAllowances?: boolean; + onSelect(entry: WalletEntry, ownerAddress?: string): void; +} + +interface EntryAllowances { + [entryId: string]: string[]; } interface StateProps { - getBalances(entry: WalletEntry): string[]; + entryAllowances: EntryAllowances; + getBalances(entry: WalletEntry, ownerAddress?: string): string[]; } -const SelectEntry: React.FC = ({ disabled, entries, selected, getBalances, onSelect }) => { +const SelectEntry: React.FC = ({ + disabled, + entries, + entryAllowances, + ownerAddress, + selectedEntry, + getBalances, + onSelect, +}) => { const [menuElement, setMenuElement] = React.useState(null); - const onOpenMenu = ({ currentTarget }: React.MouseEvent): void => { - setMenuElement(currentTarget); - }; - - const onCloseMenu = (): void => { + const handleMenuClose = (): void => { setMenuElement(null); }; - const onEntryClick = (entry: WalletEntry): void => { - onSelect(entry); - onCloseMenu(); + const handleEntrySelect = (entry: WalletEntry, ownerAddress?: string): void => { + onSelect(entry, ownerAddress); + handleMenuClose(); }; - const renderEntry = (entry: WalletEntry, selected = false): React.ReactNode => { - if (entry.address == null) { - return null; + const renderSelectedEntry = (): React.ReactElement => { + if (selectedEntry.address == null) { + return Please select from entry...; + } + + let description: React.ReactElement | undefined; + + if (ownerAddress != null) { + const { [selectedEntry.id]: allowances = [] } = entryAllowances; + + if (allowances.includes(ownerAddress)) { + description = ( + <> + Owner + + ); + } } return ( onEntryClick(entry)} + onClick={({ currentTarget }) => setMenuElement(currentTarget)} /> ); }; + const renderAccounts = (): React.ReactNode => { + return entries.map((entry) => { + if (entry.address == null) { + return undefined; + } + + const accounts: React.ReactElement[] = [ + + handleEntrySelect(entry)} + /> + , + ]; + + const { [entry.id]: allowances = [] } = entryAllowances; + + allowances.forEach((allowanceAddress) => { + if (entry.address != null) { + accounts.push( + + + Owner + + } + identity={true} + onClick={() => handleEntrySelect(entry, allowanceAddress)} + /> + , + ); + } + }); + + return accounts; + }); + }; + return ( <> - {renderEntry(selected, true)} - - {entries.map((entry) => { - if (entry.address == null) { - return undefined; - } - - return ( - - {renderEntry(entry)} - - ); - })} + {renderSelectedEntry()} + + {renderAccounts()} ); }; -export default connect((state) => ({ - getBalances(entry) { - const blockchain = blockchainIdToCode(entry.blockchain); +export default connect( + (state, { entries, ownerAddress, withAllowances = false }) => { + let entryAllowances: EntryAllowances = {}; - const balance = accounts.selectors.getBalance(state, entry.id, amountFactory(blockchain)(0)); + if (ownerAddress != null || withAllowances !== false) { + entryAllowances = entries.reduce((carry, entry) => { + if (entry.address != null && isEthereumEntry(entry)) { + const { value: address } = entry.address; - if (isEthereumEntry(entry) && entry.address != null) { - const tokenBalances = tokens.selectors.selectBalances(state, blockchain, entry.address.value); + allowances.selectors + .getEntryAllowances(state, entry) + .filter(({ spenderAddress }) => spenderAddress.toLowerCase() === address.toLowerCase()) + .forEach(({ ownerAddress }) => { + if (carry[entry.id] == null) { + carry[entry.id] = [ownerAddress]; + } else { + carry[entry.id].push(ownerAddress); + } + }); + } - return [balance, ...tokenBalances.filter((tokenBalance) => tokenBalance.isPositive())].map((amount) => - formatAmount(amount), - ); + return carry; + }, {}); } - return [balance].map((amount) => formatAmount(amount)); + return { + entryAllowances, + getBalances(entry, ownerAddress) { + const blockchain = blockchainIdToCode(entry.blockchain); + + const balance = accounts.selectors.getBalance(state, entry.id, amountFactory(blockchain)(0)); + + if (isEthereumEntry(entry) && entry.address != null) { + const tokenBalances = tokens.selectors.selectBalances(state, blockchain, entry.address.value, { + belonging: ownerAddress == null ? TokenBalanceBelong.OWN : TokenBalanceBelong.ALLOWED, + belongsTo: ownerAddress, + }); + + return [balance, ...tokenBalances.filter((tokenBalance) => tokenBalance.isPositive())].map((amount) => + formatAmount(amount), + ); + } + + return [balance].map((amount) => formatAmount(amount)); + }, + }; }, -}))(SelectEntry); +)(SelectEntry); diff --git a/packages/react-app/src/transaction/BroadcastTx/BroadcastTx.spec.tsx b/packages/react-app/src/transaction/BroadcastTx/BroadcastTx.spec.tsx index fa0002ef9..5778d5db3 100644 --- a/packages/react-app/src/transaction/BroadcastTx/BroadcastTx.spec.tsx +++ b/packages/react-app/src/transaction/BroadcastTx/BroadcastTx.spec.tsx @@ -4,17 +4,18 @@ import { Theme } from '@emeraldwallet/ui'; import { ThemeProvider } from '@material-ui/core'; import '@testing-library/jest-dom/extend-expect'; import { render } from '@testing-library/react'; -import BigNumber from 'bignumber.js'; import * as React from 'react'; import { Provider } from 'react-redux'; import { createTestStore } from '../../testStore'; import BroadcastTx from './BroadcastTx'; describe('BroadcastTx', () => { + const factory = amountFactory(BlockchainCode.Goerli); + const data: BroadcastData = { blockchain: BlockchainCode.Goerli, entryId: '1022fd13-3431-4f3b-bce8-109fdab15873-1', - fee: amountFactory(BlockchainCode.Goerli)(1 ** 18), + fee: factory('1000000000000000000'), signed: '0x02f8720580845a288bce8502d16b842682520894e7f129f88b57e902cb18ba' + 'eecd43f17449419ae287b1a2bc2ec5000080c001a056663c0965287c9e0e92d6' + @@ -25,12 +26,12 @@ describe('BroadcastTx', () => { data: '', from: '0x65a60f440ed54910d91a0634a45a2294cc807095', gas: 21000, - maxGasPrice: new BigNumber('10000000000'), - nonce: '0x0', - priorityGasPrice: new BigNumber('1000000000'), + maxGasPrice: factory('10000000000'), + nonce: 0, + priorityGasPrice: factory('1000000000'), to: '0xe7f129f88b57e902cb18baeecd43f17449419ae2', type: EthereumTransactionType.EIP1559, - value: new BigNumber('50000000000000000'), + value: factory('50000000000000000'), }, txId: '0xc9c924dd7f4ffe61d653718b204d6aae514736db227fdb0297b76cd8f3f1f203', }; diff --git a/packages/react-app/src/transaction/CreateApproveTransaction/CreateApproveTransaction.tsx b/packages/react-app/src/transaction/CreateApproveTransaction/CreateApproveTransaction.tsx index 4a54d6b3d..53bf5384b 100644 --- a/packages/react-app/src/transaction/CreateApproveTransaction/CreateApproveTransaction.tsx +++ b/packages/react-app/src/transaction/CreateApproveTransaction/CreateApproveTransaction.tsx @@ -1,5 +1,6 @@ import { EthereumEntry, Uuid, isEthereumEntry } from '@emeraldpay/emerald-vault-core'; import { + EthereumTransactionType, MAX_DISPLAY_ALLOWANCE, TokenRegistry, amountFactory, @@ -258,14 +259,16 @@ export default connect( ); if (signed != null) { - const zeroAmount = amountFactory(blockchainIdToCode(entry.blockchain))(0); + const gasPrice = + (tx.type === EthereumTransactionType.EIP1559 ? tx.maxGasPrice : tx.gasPrice) ?? + amountFactory(tx.blockchain)(0); dispatch( screen.actions.gotoScreen( screen.Pages.BROADCAST_TX, { ...signed, - fee: (tx.maxGasPrice ?? tx.gasPrice ?? zeroAmount).multiply(tx.gas), + fee: gasPrice.multiply(tx.gas), }, null, true, diff --git a/packages/react-app/src/transaction/CreateApproveTransaction/SetupApproveTransaction.tsx b/packages/react-app/src/transaction/CreateApproveTransaction/SetupApproveTransaction.tsx index 9c05a67ed..20db0b0dd 100644 --- a/packages/react-app/src/transaction/CreateApproveTransaction/SetupApproveTransaction.tsx +++ b/packages/react-app/src/transaction/CreateApproveTransaction/SetupApproveTransaction.tsx @@ -32,7 +32,7 @@ import { Alert } from '@material-ui/lab'; import * as React from 'react'; import { connect } from 'react-redux'; import { AmountField } from '../../common/AmountField'; -import EthTxSettings from '../../common/EthTxSettings/EthTxSettings'; +import { EthTxSettings } from '../../common/EthTxSettings'; import { Asset, SelectAsset } from '../../common/SelectAsset'; import { SelectEntry } from '../../common/SelectEntry'; import { ToField } from '../../common/ToField'; @@ -400,7 +400,7 @@ const SetupApproveTransaction: React.FC = <> From - + Token diff --git a/packages/react-app/src/transaction/CreateCancelTransaction/CancelEthereumTransaction.tsx b/packages/react-app/src/transaction/CreateCancelTransaction/CancelEthereumTransaction.tsx index edba28ac7..762a6b382 100644 --- a/packages/react-app/src/transaction/CreateCancelTransaction/CancelEthereumTransaction.tsx +++ b/packages/react-app/src/transaction/CreateCancelTransaction/CancelEthereumTransaction.tsx @@ -128,11 +128,13 @@ const CancelEthereumTransaction: React.FC = ({ const [useEip1559, setUseEip1559] = React.useState(ethTx.type === EthereumTransactionType.EIP1559); const factory = React.useMemo(() => amountFactory(ethTx.blockchain), [ethTx.blockchain]); + const zeroAmount = React.useMemo(() => factory(0), [factory]); - const txGasPrice = factory(ethTx.maxGasPrice ?? ethTx.gasPrice ?? 0); + const txGasPrice = + (ethTx.type === EthereumTransactionType.EIP1559 ? ethTx.maxGasPrice : ethTx.gasPrice) ?? zeroAmount; const txGasPriceUnit = txGasPrice.getOptimalUnit(); - const txPriorityGasPrice = factory(ethTx.priorityGasPrice ?? 0); + const txPriorityGasPrice = ethTx.priorityGasPrice ?? zeroAmount; const lowGasPrice = txGasPrice.plus(txGasPrice.multiply(0.1)).getNumberByUnit(txGasPriceUnit).toNumber(); const lowPriorityGasPrice = txPriorityGasPrice.getNumberByUnit(txGasPriceUnit).toNumber(); @@ -150,10 +152,8 @@ const CancelEthereumTransaction: React.FC = ({ const onSignTransaction = async (): Promise => { setPasswordError(undefined); - const zeroAmount = factory(0); - - const newGasPrice = BigAmount.createFor(gasPrice, zeroAmount.units, factory, txGasPriceUnit).number; - const newPriorityGasPrice = BigAmount.createFor(priorityGasPrice, zeroAmount.units, factory, txGasPriceUnit).number; + const newGasPrice = BigAmount.createFor(gasPrice, zeroAmount.units, factory, txGasPriceUnit); + const newPriorityGasPrice = BigAmount.createFor(priorityGasPrice, zeroAmount.units, factory, txGasPriceUnit); let gasPrices: Pick; @@ -281,7 +281,7 @@ const CancelEthereumTransaction: React.FC = ({ )} Amount - {formatAmount(factory(ethTx.value))} + {formatAmount(ethTx.value)} ( return; } - let gasPrice: Wei | undefined; - let maxGasPrice: Wei | undefined; - let priorityGasPrice: Wei | undefined; + const zeroAmount = amountFactory(tx.blockchain)(0); + + let gasPrices: Pick; if (tx.type === EthereumTransactionType.EIP1559) { - maxGasPrice = new Wei(tx.maxGasPrice ?? tx.gasPrice ?? 0); - priorityGasPrice = new Wei(tx.priorityGasPrice ?? 0); + gasPrices = { + maxGasPrice: tx.maxGasPrice ?? zeroAmount, + priorityGasPrice: tx.priorityGasPrice ?? zeroAmount, + }; } else { - gasPrice = new Wei(tx.maxGasPrice ?? tx.gasPrice ?? 0); + gasPrices = { + gasPrice: tx.gasPrice ?? zeroAmount, + }; } const signed: SignData | undefined = await dispatch( @@ -467,24 +471,24 @@ export default connect( entryId, { ...tx, + ...gasPrices, data: '', gas: DEFAULT_GAS_LIMIT, - gasPrice: gasPrice?.number, - maxGasPrice: maxGasPrice?.number, - priorityGasPrice: priorityGasPrice?.number, - value: amountFactory(tx.blockchain)(0).number, + value: zeroAmount, }, password, ), ); if (signed != null) { + const gasPrice = (tx.type === EthereumTransactionType.EIP1559 ? tx.maxGasPrice : tx.gasPrice) ?? zeroAmount; + dispatch( screen.actions.gotoScreen( screen.Pages.BROADCAST_TX, { ...signed, - fee: (maxGasPrice ?? gasPrice ?? Wei.ZERO).multiply(DEFAULT_GAS_LIMIT), + fee: gasPrice.multiply(DEFAULT_GAS_LIMIT), originalAmount: Wei.ZERO, }, null, diff --git a/packages/react-app/src/transaction/CreateConvertTransaction/CreateConvertTransaction.tsx b/packages/react-app/src/transaction/CreateConvertTransaction/CreateConvertTransaction.tsx index 88601a4d4..b49db74cb 100644 --- a/packages/react-app/src/transaction/CreateConvertTransaction/CreateConvertTransaction.tsx +++ b/packages/react-app/src/transaction/CreateConvertTransaction/CreateConvertTransaction.tsx @@ -23,7 +23,7 @@ import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab'; import * as React from 'react'; import { connect } from 'react-redux'; import { AmountField } from '../../common/AmountField'; -import EthTxSettings from '../../common/EthTxSettings/EthTxSettings'; +import { EthTxSettings } from '../../common/EthTxSettings'; import WaitLedger from '../../ledger/WaitLedger'; const useStyles = makeStyles( @@ -546,15 +546,16 @@ export default connect( ); if (signed != null) { - const blockchainCode = blockchainIdToCode(entry.blockchain); - const zeroAmount = amountFactory(blockchainCode)(0); + const gasPrice = + (tx.type === EthereumTransactionType.EIP1559 ? tx.maxGasPrice : tx.gasPrice) ?? + amountFactory(blockchainIdToCode(entry.blockchain))(0); dispatch( screen.actions.gotoScreen( screen.Pages.BROADCAST_TX, { ...signed, - fee: (tx.maxGasPrice ?? tx.gasPrice ?? zeroAmount).multiply(tx.gas), + fee: gasPrice.multiply(tx.gas), originalAmount: tx.amount, tokenAmount: token.getAmount(tx.amount.number), }, diff --git a/packages/react-app/src/transaction/CreateRecoverTransaction/CreateRecoverTransaction.tsx b/packages/react-app/src/transaction/CreateRecoverTransaction/CreateRecoverTransaction.tsx index 90fa68a98..fb23293dc 100644 --- a/packages/react-app/src/transaction/CreateRecoverTransaction/CreateRecoverTransaction.tsx +++ b/packages/react-app/src/transaction/CreateRecoverTransaction/CreateRecoverTransaction.tsx @@ -11,6 +11,7 @@ import { TokenAmount, TokenData, TokenRegistry, + amountFactory, blockchainIdToCode, formatAmount, workflow, @@ -635,12 +636,16 @@ export default connect( ); if (signed != null) { + const gasPrice = + (tx.type === EthereumTransactionType.EIP1559 ? tx.maxGasPrice : tx.gasPrice) ?? + amountFactory(tx.blockchain)(0); + dispatch( screen.actions.gotoScreen( screen.Pages.BROADCAST_TX, { ...signed, - fee: (tx.maxGasPrice ?? tx.gasPrice ?? Wei.ZERO).multiply(tx.gas), + fee: gasPrice.multiply(tx.gas), originalAmount: tx.amount, }, null, diff --git a/packages/react-app/src/transaction/CreateSpeedUpTransaction/SpeedUpEthereumTransaction.tsx b/packages/react-app/src/transaction/CreateSpeedUpTransaction/SpeedUpEthereumTransaction.tsx index 62c38da5e..1850faa05 100644 --- a/packages/react-app/src/transaction/CreateSpeedUpTransaction/SpeedUpEthereumTransaction.tsx +++ b/packages/react-app/src/transaction/CreateSpeedUpTransaction/SpeedUpEthereumTransaction.tsx @@ -8,13 +8,11 @@ import { formatAmount, } from '@emeraldwallet/core'; import { - DefaultFee, GasPrices, IState, SignData, StoredTransaction, accounts, - application, blockchains, screen, transaction, @@ -92,10 +90,6 @@ interface OwnProps { goBack(): void; } -interface StateProps { - defaultFee: DefaultFee | undefined; -} - interface DispatchProps { getTopFee(blockchain: BlockchainCode): Promise; lookupAddress(blockchain: BlockchainCode, address: string): Promise; @@ -103,10 +97,9 @@ interface DispatchProps { verifyGlobalKey(password: string): Promise; } -const SpeedUpEthereumTransaction: React.FC = ({ +const SpeedUpEthereumTransaction: React.FC = ({ entryId, ethTx, - defaultFee, isHardware, tx, getTopFee, @@ -128,8 +121,10 @@ const SpeedUpEthereumTransaction: React.FC(null); const factory = React.useMemo(() => amountFactory(ethTx.blockchain), [ethTx.blockchain]); + const zeroAmount = React.useMemo(() => factory(0), [factory]); - const txGasPrice = factory(ethTx.maxGasPrice ?? ethTx.gasPrice ?? defaultFee?.std ?? 0); + const txGasPrice = + (ethTx.type === EthereumTransactionType.EIP1559 ? ethTx.maxGasPrice : ethTx.gasPrice) ?? zeroAmount; const txGasPriceUnit = txGasPrice.getOptimalUnit(undefined, undefined, 6); const [useEip1559, setUseEip1559] = React.useState(ethTx.type === EthereumTransactionType.EIP1559); @@ -139,7 +134,7 @@ const SpeedUpEthereumTransaction: React.FC(txGasPrice.plus(txGasPrice.multiply(0.5))); - const txPriorityGasPrice = factory(ethTx.priorityGasPrice ?? 0); + const txPriorityGasPrice = ethTx.priorityGasPrice ?? zeroAmount; const minPriorityGasPriceNumber = txPriorityGasPrice.getNumberByUnit(txGasPriceUnit).toNumber(); @@ -154,10 +149,8 @@ const SpeedUpEthereumTransaction: React.FC => { setPasswordError(undefined); - const zeroAmount = factory(0); - - const newGasPrice = BigAmount.createFor(maxGasPrice, zeroAmount.units, factory, txGasPriceUnit).number; - const newPriorityGasPrice = BigAmount.createFor(priorityGasPrice, zeroAmount.units, factory, txGasPriceUnit).number; + const newGasPrice = BigAmount.createFor(maxGasPrice, zeroAmount.units, factory, txGasPriceUnit); + const newPriorityGasPrice = BigAmount.createFor(priorityGasPrice, zeroAmount.units, factory, txGasPriceUnit); let gasPrices: Pick; @@ -296,7 +289,7 @@ const SpeedUpEthereumTransaction: React.FC Amount - {formatAmount(factory(ethTx.value))} + {formatAmount(ethTx.value)} ( - (state, { ethTx: { blockchain } }) => ({ - defaultFee: application.selectors.getDefaultFee(state, blockchain), - }), +export default connect( + null, // eslint-disable-next-line @typescript-eslint/no-explicit-any (dispatch: any) => ({ getTopFee(blockchain) { @@ -471,43 +462,35 @@ export default connect( return; } - const factory = amountFactory(tx.blockchain); + const zeroAmount = amountFactory(tx.blockchain)(0); - let gasPrice: BigAmount | undefined; - let maxGasPrice: BigAmount | undefined; - let priorityGasPrice: BigAmount | undefined; + let gasPrices: Pick; if (tx.type === EthereumTransactionType.EIP1559) { - maxGasPrice = factory(tx.maxGasPrice ?? 0); - priorityGasPrice = factory(tx.priorityGasPrice ?? 0); + gasPrices = { + maxGasPrice: tx.maxGasPrice ?? zeroAmount, + priorityGasPrice: tx.priorityGasPrice ?? zeroAmount, + }; } else { - gasPrice = factory(tx.gasPrice ?? 0); + gasPrices = { + gasPrice: tx.gasPrice ?? zeroAmount, + }; } - const gas = parseInt(tx.gas.toString(), 10); - const value = factory(tx.value); - const signed: SignData | undefined = await dispatch( - transaction.actions.signTransaction( - entryId, - { - ...tx, - gasPrice: gasPrice?.number, - maxGasPrice: maxGasPrice?.number, - priorityGasPrice: priorityGasPrice?.number, - }, - password, - ), + transaction.actions.signTransaction(entryId, { ...tx, ...gasPrices }, password), ); if (signed != null) { + const gasPrice = (tx.type === EthereumTransactionType.EIP1559 ? tx.maxGasPrice : tx.gasPrice) ?? zeroAmount; + dispatch( screen.actions.gotoScreen( screen.Pages.BROADCAST_TX, { ...signed, - fee: (maxGasPrice ?? gasPrice ?? factory(0)).multiply(gas), - originalAmount: value, + fee: gasPrice.multiply(tx.gas), + originalAmount: tx.value, }, null, true, diff --git a/packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/BroadcastTransaction/BroadcastTransaction.tsx b/packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/BroadcastTransaction/BroadcastTransaction.tsx new file mode 100644 index 000000000..db09283f9 --- /dev/null +++ b/packages/react-app/src/transaction/CreateTransactionNew/CreateBitcoinTransaction/BroadcastTransaction/BroadcastTransaction.tsx @@ -0,0 +1,213 @@ +import { BigAmount } from '@emeraldpay/bigamount'; +import { BitcoinEntry, UnsignedBitcoinTx } from '@emeraldpay/emerald-vault-core'; +import { BlockchainCode, amountDecoder, amountFactory, blockchainIdToCode } from '@emeraldwallet/core'; +import { BroadcastData, IState, accounts, transaction } from '@emeraldwallet/store'; +import { Address, Balance, Button, ButtonGroup, FormLabel, FormRow, TxRef } from '@emeraldwallet/ui'; +import { TextField, Typography, createStyles, makeStyles } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import * as bitcoin from 'bitcoinjs-lib'; +import * as React from 'react'; +import { connect } from 'react-redux'; + +const useStyles = makeStyles((theme) => + createStyles({ + balances: { + width: '100%', + }, + balanceItem: { + display: 'flex', + '& + &': { + marginTop: 20, + }, + }, + balanceItemAddress: { + display: 'flex', + flex: 1, + flexDirection: 'column', + }, + buttons: { + display: 'flex', + justifyContent: 'end', + width: '100%', + }, + rawTx: { + ...theme.monotype, + fontSize: 12, + }, + }), +); + +interface OwnProps { + entry: BitcoinEntry; + signed: string; + txId: string; + unsigned: UnsignedBitcoinTx; + onCancel(): void; +} + +interface ParsedInput { + address?: string; + amount: BigAmount; + txid: string; + vout: number; +} + +interface ParsedOutput { + address: string; + amount: BigAmount; +} + +interface StateProps { + blockchain: BlockchainCode; + parsed: { + fee?: BigAmount; + inputs: ParsedInput[]; + outputs: ParsedOutput[]; + }; +} + +interface DispatchProps { + broadcastTx(data: BroadcastData): void; +} + +const BroadcastTransaction: React.FC = ({ + blockchain, + entry, + parsed, + signed, + txId, + unsigned, + broadcastTx, + onCancel, +}) => { + const styles = useStyles(); + + const [showRaw, setShowRaw] = React.useState(false); + + const handleBroadcastTx = (): void => { + const fee = amountFactory(blockchain)(unsigned.fee); + + broadcastTx({ blockchain, fee, signed, txId, entryId: entry.id, tx: unsigned }); + }; + + return ( + <> + + From +
+ {parsed.inputs.map(({ address, amount, txid, vout }) => ( +
+
+ {address == null ? :
} +
+ +
+ ))} +
+
+ + To +
+ {parsed.outputs.map(({ address, amount }, index) => ( +
+
+
+
+ +
+ ))} +
+
+ + Fee + {parsed.fee == null ? ( + Unknown Fee. May be invalid. + ) : ( + {parsed.fee.toString()} + )} + + {showRaw ? ( + <> + + Raw Tx + + + + +