diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b506a7978..f757d65cb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,49 @@ Changelog ========= +## 4.1.0 + +### Features + +- Enabled Catalyst voting registration on Mainnet and Flight builds + +## 4.1.0-FC1 + +### Features + +- Integrated cardano-wallet Api native tokens decimal places value ([PR 2577](https://github.com/input-output-hk/daedalus/pull/2577)) +- Added address verification for "Trezor" hardware wallet devices ([PR 2558](https://github.com/input-output-hk/daedalus/pull/2558)) +- Added address verification for "Ledger" hardware wallet devices ([PR 2282](https://github.com/input-output-hk/daedalus/pull/2282)) +- Implemented manually configurable decimal places for native tokens ([PR 2533](https://github.com/input-output-hk/daedalus/pull/2533)) +- Added warning message when pasting an address of the same wallet in the send form ([PR 2506](https://github.com/input-output-hk/daedalus/pull/2506)) +- Implemented select search style variables ([PR 2512](https://github.com/input-output-hk/daedalus/pull/2512)) +- Enabled pasting of wallet recovery phrase ([PR 2459](https://github.com/input-output-hk/daedalus/pull/2459)) + +### Fixes + +- Fixed pool description scrolling and styling ([PR 2556](https://github.com/input-output-hk/daedalus/pull/2564), [PR 2568](https://github.com/input-output-hk/daedalus/pull/2568)) +- Fixed external currencies not appearing when not connected ([PR 2556](https://github.com/input-output-hk/daedalus/pull/2556)) +- Fixed hardware wallets transactions issue with native tokens ([PR 2543](https://github.com/input-output-hk/daedalus/pull/2543)) +- Fixed hardware wallets transaction initialization issue on device reconnect ([PR 2541](https://github.com/input-output-hk/daedalus/pull/2541)) +- Fixed blank currency selection after updating to 4.0.5 ([PR 2529](https://github.com/input-output-hk/daedalus/pull/2529)) +- Fixed hardware wallets confirmation crashing issue when device is in "Busy" state ([PR 2522](https://github.com/input-output-hk/daedalus/pull/2522)) +- Fixed hardware wallets spending password issue ([PR 2519](https://github.com/input-output-hk/daedalus/pull/2519)) + +### Chores + +- Updated wallet importer to extract private keys from 'WalletUserSecret' field ([PR 2445](https://github.com/input-output-hk/daedalus/pull/2445)) +- Changed the assets identifiers from fingerprint to policyId+name ([PR 2562](https://github.com/input-output-hk/daedalus/pull/2562)) +- Implemented the Items Dropdown component and simplified the Wallets and Assets Dropdowns ([PR 2540](https://github.com/input-output-hk/daedalus/pull/2540)) +- Included wallet names in dialog windows ([PR 2552](https://github.com/input-output-hk/daedalus/pull/2552)) +- Updated `cardano-cli` version to always match the one from `cardano-wallet` ([PR 2561](https://github.com/input-output-hk/daedalus/pull/2561)) +- Updated `react-polymorph` to version `1.0.0` ([PR 2549](https://github.com/input-output-hk/daedalus/pull/2549)) +- Updated `cardano-wallet` to version `2021-05-26` ([PR 2577](https://github.com/input-output-hk/daedalus/pull/2577), [PR 2580](https://github.com/input-output-hk/daedalus/pull/2580)) +- Updated `cardano-wallet` to version `2021-04-28` and `cardano-node` to version `1.26.2` ([PR 2551](https://github.com/input-output-hk/daedalus/pull/2551)) +- Updated Catalyst voting registration to include rewards address instead of the payment one ([PR 2550](https://github.com/input-output-hk/daedalus/pull/2550)) +- Moved currency related code into a dedicated store ([PR 2546](https://github.com/input-output-hk/daedalus/pull/2546)) +- Updated `trezor-connect` dependency to version `8.1.26` ([PR 2534](https://github.com/input-output-hk/daedalus/pull/2534)) +- Updated `@cardano-foundation/ledgerjs-hw-app-cardano` package to version `3.0.0` ([PR 2530](https://github.com/input-output-hk/daedalus/pull/2530)) + ## 4.0.5 ### Features @@ -89,9 +132,6 @@ Changelog ### Features - Added native token support [PR 2292](https://github.com/input-output-hk/daedalus/pull/2292) - -### Features - - Enabled rewards withdrawals on hardware wallets ([PR 2408](https://github.com/input-output-hk/daedalus/pull/2408)) ### Fixes diff --git a/default.nix b/default.nix index f5d96af3d5..7d3fe111d3 100644 --- a/default.nix +++ b/default.nix @@ -87,8 +87,7 @@ let cardano-address = (import self.sources.cardano-wallet { inherit system; gitrev = self.sources.cardano-wallet.rev; crossSystem = crossSystem walletPkgs.lib; }).cardano-address; mock-token-metadata-server = (import self.sources.cardano-wallet { inherit system; gitrev = self.sources.cardano-wallet.rev; crossSystem = crossSystem walletPkgs.lib; }).mock-token-metadata-server; cardano-shell = import self.sources.cardano-shell { inherit system; crossSystem = crossSystem shellPkgs.lib; }; - cardano-cli = (import self.sources.cardano-node { inherit system; crossSystem = crossSystem nodePkgs.lib; }).cardano-cli; - local-cluster = if cluster == "selfnode" then (import self.sources.cardano-wallet { inherit system; gitrev = self.sources.cardano-wallet.rev; crossSystem = crossSystem walletPkgs.lib; }).project.hsPkgs.cardano-wallet.components.exes.local-cluster else null; + local-cluster = if cluster == "selfnode" then (import self.sources.cardano-wallet { inherit system; gitrev = self.sources.cardano-wallet.rev; crossSystem = crossSystem walletPkgs.lib; }).local-cluster else null; cardano-node-cluster = let # Test wallets with known mnemonics walletTestGenesisYaml = (self.sources.cardano-wallet + "/lib/shelley/test/data/cardano-node-shelley/genesis.yaml"); @@ -103,8 +102,11 @@ let }; in (import self.sources.cardano-node { inherit system customConfig; crossSystem = crossSystem nodePkgs.lib; }).cluster; cardano-node = if useLocalNode - then (import self.sources.cardano-node { inherit system; crossSystem = crossSystem nodePkgs.lib; }).haskellPackages.cardano-node.components.exes.cardano-node + then (import self.sources.cardano-node { inherit system; crossSystem = crossSystem nodePkgs.lib; }).cardano-node else self.cardano-wallet.cardano-node; + cardano-cli = if useLocalNode + then (import self.sources.cardano-node { inherit system; crossSystem = crossSystem nodePkgs.lib; }).haskellPackages.cardano-cli + else self.cardano-wallet.cardano-cli; darwin-launcher = self.callPackage ./nix/darwin-launcher.nix {}; # a cross-compiled fastlist for the ps-list package diff --git a/nix/sources.json b/nix/sources.json index e1b9105dc7..4d12263502 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -29,10 +29,10 @@ "homepage": null, "owner": "input-output-hk", "repo": "cardano-wallet", - "rev": "7df30796be8bab3351467ecdee5e803f0969b177", - "sha256": "158w18wgxmqh2l224llw9vmyk0677k1ainm2qw3ja6mkdi37xns9", + "rev": "7426ccc17ecffcc112abf5e8382bcb89cb24f771", + "sha256": "0ladc7bpy6fwvlsp2zjw3h8rm384w3qf93gvb1s6nn2n912sp7ck", "type": "tarball", - "url": "https://github.com/input-output-hk/cardano-wallet/archive/7df30796be8bab3351467ecdee5e803f0969b177.tar.gz", + "url": "https://github.com/input-output-hk/cardano-wallet/archive/7426ccc17ecffcc112abf5e8382bcb89cb24f771.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, "gitignore": { diff --git a/package.json b/package.json index 42c684fe3b..202027d2e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "daedalus", "productName": "Daedalus", - "version": "4.0.5", + "version": "4.1.0", "description": "Cryptocurrency Wallet", "main": "./dist/main/index.js", "scripts": { @@ -86,6 +86,7 @@ "@babel/preset-flow": "7.8.3", "@babel/preset-react": "7.0.0", "@babel/register": "7.0.0", + "@dump247/storybook-state": "1.6.1", "@storybook/addon-actions": "5.3.14", "@storybook/addon-knobs": "5.3.14", "@storybook/addon-links": "5.3.14", @@ -173,7 +174,7 @@ "yarn-lockfile": "1.1.1" }, "dependencies": { - "@cardano-foundation/ledgerjs-hw-app-cardano": "2.2.0", + "@cardano-foundation/ledgerjs-hw-app-cardano": "3.0.0", "@iohk-jormungandr/wallet-js": "0.5.0-pre7", "aes-js": "3.1.2", "bech32": "2.0.0", @@ -233,7 +234,7 @@ "react-intl": "2.7.2", "react-lottie": "1.2.3", "react-markdown": "4.3.1", - "react-polymorph": "0.9.8-rc.11", + "react-polymorph": "1.0.0-next.1", "react-router": "5.2.0", "react-router-dom": "5.2.0", "react-svg-inline": "2.1.1", @@ -250,7 +251,7 @@ "source-map-support": "0.5.19", "spectron-fake-dialog": "0.0.1", "tcp-port-used": "1.0.1", - "trezor-connect": "8.1.25-extended", + "trezor-connect": "8.1.26-extended", "unorm": "1.6.0", "validator": "13.1.17" }, diff --git a/source/common/config/electron-store.config.js b/source/common/config/electron-store.config.js index 3fdd16196a..ec9c7bd4b4 100644 --- a/source/common/config/electron-store.config.js +++ b/source/common/config/electron-store.config.js @@ -13,25 +13,27 @@ export const STORAGE_TYPES: { export const STORAGE_KEYS: { [key: string]: StorageKey, } = { - RESET: 'RESET', - USER_LOCALE: 'USER-LOCALE', - USER_NUMBER_FORMAT: 'USER-NUMBER-FORMAT', - USER_DATE_FORMAT_ENGLISH: 'USER-DATE-FORMAT-ENGLISH', - USER_DATE_FORMAT_JAPANESE: 'USER-DATE-FORMAT-JAPANESE', - USER_TIME_FORMAT: 'USER-TIME-FORMAT', - TERMS_OF_USE_ACCEPTANCE: 'TERMS-OF-USE-ACCEPTANCE', - THEME: 'THEME', - DATA_LAYER_MIGRATION_ACCEPTANCE: 'DATA-LAYER-MIGRATION-ACCEPTANCE', - READ_NEWS: 'READ-NEWS', - WALLETS: 'WALLETS', - HARDWARE_WALLETS: 'HARDWARE-WALLETS', - HARDWARE_WALLET_DEVICES: 'HARDWARE-WALLET-DEVICES', - WALLET_MIGRATION_STATUS: 'WALLET-MIGRATION-STATUS', - DOWNLOAD_MANAGER: 'DOWNLOAD-MANAGER', APP_AUTOMATIC_UPDATE_FAILED: 'APP-AUTOMATIC-UPDATE-FAILED', APP_UPDATE_COMPLETED: 'APP-UPDATE-COMPLETED', - CURRENCY_SELECTED: 'CURRENCY-SELECTED', + ASSET_DATA: 'ASSET-DATA', + ASSET_SETTINGS_DIALOG_WAS_OPENED: 'ASSET-SETTINGS-DIALOG-WAS-OPENED', CURRENCY_ACTIVE: 'CURRENCY-ACTIVE', + CURRENCY_SELECTED: 'CURRENCY-SELECTED', + DATA_LAYER_MIGRATION_ACCEPTANCE: 'DATA-LAYER-MIGRATION-ACCEPTANCE', + DOWNLOAD_MANAGER: 'DOWNLOAD-MANAGER', + HARDWARE_WALLET_DEVICES: 'HARDWARE-WALLET-DEVICES', + HARDWARE_WALLETS: 'HARDWARE-WALLETS', + READ_NEWS: 'READ-NEWS', + RESET: 'RESET', SMASH_SERVER: 'SMASH-SERVER', STAKING_INFO_WAS_OPEN: 'STAKING-INFO-WAS-OPEN', + TERMS_OF_USE_ACCEPTANCE: 'TERMS-OF-USE-ACCEPTANCE', + THEME: 'THEME', + USER_DATE_FORMAT_ENGLISH: 'USER-DATE-FORMAT-ENGLISH', + USER_DATE_FORMAT_JAPANESE: 'USER-DATE-FORMAT-JAPANESE', + USER_LOCALE: 'USER-LOCALE', + USER_NUMBER_FORMAT: 'USER-NUMBER-FORMAT', + USER_TIME_FORMAT: 'USER-TIME-FORMAT', + WALLET_MIGRATION_STATUS: 'WALLET-MIGRATION-STATUS', + WALLETS: 'WALLETS', }; diff --git a/source/common/ipc/api.js b/source/common/ipc/api.js index e623cbf221..ecb473231b 100644 --- a/source/common/ipc/api.js +++ b/source/common/ipc/api.js @@ -414,6 +414,32 @@ export type DeriveXpubRendererRequestType = { derivationScheme: number, }; +export type StakingBlockchainPointer = { + blockIndex: number, + txIndex: number, + certificateIndex: number, +}; + +export type deriveAddressRendererRequestType = { + devicePath: ?string, + addressType: number, + networkId: number, + protocolMagic: number, + spendingPathStr: string, + stakingPathStr: ?string, + isTrezor: boolean, +}; + +export type showAddressRendererRequestType = { + devicePath: ?string, + addressType: number, + networkId: number, + protocolMagic: number, + spendingPathStr: string, + stakingPathStr: ?string, + isTrezor: boolean, +}; + /** * Channel for introspecting an address */ @@ -477,3 +503,11 @@ export type deriveXpubMainResponse = string; export const RESET_ACTION_TREZOR_CHANNEL = 'RESET_ACTION_TREZOR_CHANNEL'; export type resetTrezorActionRendererRequest = void; export type resetTrezorActionMainResponse = void; + +export const DERIVE_ADDRESS_CHANNEL = 'DERIVE_ADDRESS_CHANNEL'; +export type deriveAddressRendererRequest = deriveAddressRendererRequestType; +export type deriveAddressMainResponse = string; + +export const SHOW_ADDRESS_CHANNEL = 'SHOW_ADDRESS_CHANNEL'; +export type showAddressRendererRequest = showAddressRendererRequestType; +export type showAddressMainResponse = void; diff --git a/source/common/types/electron-store.types.js b/source/common/types/electron-store.types.js index 081a290d3e..d994107786 100644 --- a/source/common/types/electron-store.types.js +++ b/source/common/types/electron-store.types.js @@ -3,27 +3,29 @@ export type StorageType = 'get' | 'set' | 'delete' | 'reset'; export type StorageKey = + | 'APP-AUTOMATIC-UPDATE-FAILED' + | 'APP-UPDATE-COMPLETED' + | 'ASSET-DATA' + | 'ASSET-SETTINGS-DIALOG-WAS-OPENED' + | 'CURRENCY-ACTIVE' + | 'CURRENCY-SELECTED' + | 'DATA-LAYER-MIGRATION-ACCEPTANCE' + | 'DOWNLOAD-MANAGER' + | 'HARDWARE-WALLET-DEVICES' + | 'HARDWARE-WALLETS' + | 'READ-NEWS' | 'RESET' - | 'USER-LOCALE' - | 'USER-NUMBER-FORMAT' + | 'SMASH-SERVER' + | 'STAKING-INFO-WAS-OPEN' + | 'TERMS-OF-USE-ACCEPTANCE' + | 'THEME' | 'USER-DATE-FORMAT-ENGLISH' | 'USER-DATE-FORMAT-JAPANESE' + | 'USER-LOCALE' + | 'USER-NUMBER-FORMAT' | 'USER-TIME-FORMAT' - | 'TERMS-OF-USE-ACCEPTANCE' - | 'THEME' - | 'DATA-LAYER-MIGRATION-ACCEPTANCE' - | 'READ-NEWS' - | 'WALLETS' - | 'HARDWARE-WALLETS' - | 'HARDWARE-WALLET-DEVICES' | 'WALLET-MIGRATION-STATUS' - | 'DOWNLOAD-MANAGER' - | 'APP-AUTOMATIC-UPDATE-FAILED' - | 'APP-UPDATE-COMPLETED' - | 'CURRENCY-SELECTED' - | 'CURRENCY-ACTIVE' - | 'SMASH-SERVER' - | 'STAKING-INFO-WAS-OPEN'; + | 'WALLETS'; export type StoreMessage = { type: StorageType, diff --git a/source/common/types/hardware-wallets.types.js b/source/common/types/hardware-wallets.types.js index a6a195be4b..e20ca52c8c 100644 --- a/source/common/types/hardware-wallets.types.js +++ b/source/common/types/hardware-wallets.types.js @@ -118,12 +118,6 @@ export type TransportDevice = { firmwareVersion: ?string, }; -/* export type Certificate = {| - type: CertificateType, - path: BIP32Path, - poolKeyHashHex: ?string, -|}; */ - export type Certificate = { address: string, type: string, @@ -236,10 +230,11 @@ export type LedgerSignTransactionRequest = { protocolMagic: number, certificates: Array, withdrawals: Array, - metadataHashHex: ?string, + auxiliaryData: ?string, reset?: boolean, devicePath: ?string, validityIntervalStartStr?: string, + signingMode: string, }; export type TrezorSignTransactionRequest = { diff --git a/source/main/environment.js b/source/main/environment.js index 0f5aa75c39..f6089c6e9d 100644 --- a/source/main/environment.js +++ b/source/main/environment.js @@ -61,7 +61,7 @@ const isDevelopment = checkIsDevelopment(NETWORK); const isWatchMode = process.env.IS_WATCH_MODE; const keepLocalClusterRunning = process.env.KEEP_LOCAL_CLUSTER_RUNNING; const API_VERSION = process.env.API_VERSION || 'dev'; -const NODE_VERSION = '1.25.1'; // TODO: pick up this value from process.env +const NODE_VERSION = '1.26.2'; // TODO: pick up this value from process.env const mainProcessID = get(process, 'ppid', '-'); const rendererProcessID = process.pid; const PLATFORM = os.platform(); diff --git a/source/main/ipc/getHardwareWalletChannel.js b/source/main/ipc/getHardwareWalletChannel.js index bf86b54343..85c2706fcf 100644 --- a/source/main/ipc/getHardwareWalletChannel.js +++ b/source/main/ipc/getHardwareWalletChannel.js @@ -26,6 +26,8 @@ import { GET_INIT_LEDGER_CONNECT_CHANNEL, DERIVE_XPUB_CHANNEL, RESET_ACTION_TREZOR_CHANNEL, + DERIVE_ADDRESS_CHANNEL, + SHOW_ADDRESS_CHANNEL, } from '../../common/ipc/api'; import { logger } from '../utils/logging'; @@ -51,6 +53,10 @@ import type { resetTrezorActionMainResponse, deriveXpubRendererRequest, deriveXpubMainResponse, + deriveAddressRendererRequest, + deriveAddressMainResponse, + showAddressRendererRequest, + showAddressMainResponse, } from '../../common/ipc/api'; const getHardwareWalletTransportChannel: MainIpcChannel< @@ -103,6 +109,16 @@ const deriveXpubChannel: MainIpcChannel< deriveXpubMainResponse > = new MainIpcChannel(DERIVE_XPUB_CHANNEL); +const deriveAddressChannel: MainIpcChannel< + deriveAddressRendererRequest, + deriveAddressMainResponse +> = new MainIpcChannel(DERIVE_ADDRESS_CHANNEL); + +const showAddressChannel: MainIpcChannel< + showAddressRendererRequest, + showAddressMainResponse +> = new MainIpcChannel(SHOW_ADDRESS_CHANNEL); + let devicesMemo = {}; class EventObserver { constructor(props) { @@ -397,6 +413,116 @@ export const handleHardwareWalletRequests = async ( } }); + deriveAddressChannel.onRequest(async (params) => { + const { + addressType, + spendingPathStr, + stakingPathStr, + devicePath, + isTrezor, + networkId, + protocolMagic, + } = params; + const spendingPath = utils.str_to_path(spendingPathStr); + const stakingPath = stakingPathStr + ? utils.str_to_path(stakingPathStr) + : null; + + try { + deviceConnection = get(devicesMemo, [devicePath, 'AdaConnection']); + logger.info('[HW-DEBUG] DERIVE ADDRESS'); + if (isTrezor) { + const result = await TrezorConnect.cardanoGetAddress({ + device: { + path: devicePath, + showOnTrezor: true, + }, + addressParameters: { + addressType, + path: `m/${spendingPathStr}`, + stakingPath: stakingPathStr ? `m/${stakingPathStr}` : null, + }, + protocolMagic, + networkId, + }); + return result.payload.address; + } + + // Check if Ledger instantiated + if (!deviceConnection) { + throw new Error('Ledger device not connected'); + } + + const { addressHex } = await deviceConnection.deriveAddress({ + network: { + networkId, + protocolMagic, + }, + address: { + type: addressType, + params: { + spendingPath, + stakingPath, + }, + }, + }); + + const encodedAddress = utils.bech32_encodeAddress( + utils.hex_to_buf(addressHex) + ); + return encodedAddress; + } catch (e) { + throw e; + } + }); + + showAddressChannel.onRequest(async (params) => { + const { + addressType, + spendingPathStr, + stakingPathStr, + devicePath, + isTrezor, + networkId, + protocolMagic, + } = params; + const spendingPath = utils.str_to_path(spendingPathStr); + const stakingPath = stakingPathStr + ? utils.str_to_path(stakingPathStr) + : null; + + try { + deviceConnection = get(devicesMemo, [devicePath, 'AdaConnection']); + logger.info('[HW-DEBUG] SHOW ADDRESS'); + + if (isTrezor) { + throw new Error('Address verification not supported on Trezor devices'); + } + + // Check if Ledger instantiated + if (!deviceConnection) { + throw new Error('Ledger device not connected'); + } + + await deviceConnection.showAddress({ + network: { + networkId, + protocolMagic, + }, + address: { + type: addressType, + params: { + spendingPath, + stakingPath, + }, + }, + }); + return; + } catch (e) { + throw e; + } + }); + getCardanoAdaAppChannel.onRequest(async (request) => { const { path } = request; try { @@ -407,16 +533,16 @@ export const handleHardwareWalletRequests = async ( } logger.info('[HW-DEBUG] GET CARDANO APP'); deviceConnection = devicesMemo[path].AdaConnection; - const appVersion = await deviceConnection.getVersion(); + const { version } = await deviceConnection.getVersion(); logger.info('[HW-DEBUG] getCardanoAdaAppChannel:: appVersion'); - const deviceSerial = await deviceConnection.getSerial(); + const { serial } = await deviceConnection.getSerial(); logger.info('[HW-DEBUG] getCardanoAdaAppChannel:: deviceSerial'); - const { minor, major, patch } = appVersion; + const { minor, major, patch } = version; return Promise.resolve({ minor, major, patch, - deviceId: deviceSerial.serial, + deviceId: serial, }); } catch (error) { const errorCode = error.code || ''; @@ -541,9 +667,10 @@ export const handleHardwareWalletRequests = async ( if (!deviceConnection) { throw new Error('Ledger device not connected'); } - const extendedPublicKey = await deviceConnection.getExtendedPublicKey( - utils.str_to_path(path) - ); + + const extendedPublicKey = await deviceConnection.getExtendedPublicKey({ + path: utils.str_to_path(path), + }); const deviceSerial = await deviceConnection.getSerial(); return Promise.resolve({ publicKeyHex: extendedPublicKey.publicKeyHex, @@ -566,9 +693,10 @@ export const handleHardwareWalletRequests = async ( networkId, certificates, withdrawals, - metadataHashHex, + auxiliaryData, devicePath, // validityIntervalStartStr, + signingMode, } = params; logger.info('[HW-DEBUG] SIGN Ledger transaction'); deviceConnection = devicePath @@ -579,19 +707,23 @@ export const handleHardwareWalletRequests = async ( if (!deviceConnection) { throw new Error('Device not connected!'); } - - const signedTransaction = await deviceConnection.signTransaction( - networkId, - protocolMagic, - inputs, - outputs, - fee, - ttl, - certificates, - withdrawals, - metadataHashHex - // validityIntervalStartStr - ); + const signedTransaction = await deviceConnection.signTransaction({ + signingMode, + tx: { + network: { + networkId, + protocolMagic, + }, + inputs, + outputs, + fee, + ttl, + certificates, + withdrawals, + auxiliaryData, + // validityIntervalStart, + }, + }); return Promise.resolve(signedTransaction); } catch (e) { throw e; diff --git a/source/main/utils/restoreKeystore.js b/source/main/utils/restoreKeystore.js index 0d8461a35b..57dad82f94 100644 --- a/source/main/utils/restoreKeystore.js +++ b/source/main/utils/restoreKeystore.js @@ -17,9 +17,41 @@ export type WalletId = string; export const decodeKeystore = async ( bytes: Buffer ): Promise => { - return cbor - .decodeAll(bytes) - .then((obj) => obj[0][2].map(toEncryptedSecretKey)); + return cbor.decodeAll(bytes).then((obj) => { + /** + * The original 'UserSecret' from cardano-sl looks like this: + * + * ```hs + * data UserSecret = UserSecret + * { _usVss :: Maybe VssKeyPair + * , _usPrimKey :: Maybe SecretKey + * , _usKeys :: [EncryptedSecretKey] + * , _usWalletSet :: Maybe WalletUserSecret + * , _usPath :: FilePath + * , _usLock :: Maybe FileLock + * } + * + * data WalletUserSecret = WalletUserSecret + * { _wusRootKey :: EncryptedSecretKey + * , _wusWalletName :: Text + * , _wusAccounts :: [(Word32, Text)] + * , _wusAddrs :: [(Word32, Word32)] + * } + * ``` + * + * We are interested in: + * - usKeys: + * which is where keys have been stored since ~2018 + * + * - usWalletSet + * which seems to have been used in earlier version; at least the + * wallet from the time did allow to restore so-called 'wallets' + * from keys coming from that 'WalletUserSecret' + */ + const usKeys = obj[0][2].map(toEncryptedSecretKey); + const usWalletSet = obj[0][3].map((x) => toEncryptedSecretKey(x[0])); + return usKeys.concat(usWalletSet); + }); }; const toEncryptedSecretKey = ([encryptedPayload, passphraseHash]: [ diff --git a/source/renderer/app/actions/assets-actions.js b/source/renderer/app/actions/assets-actions.js new file mode 100644 index 0000000000..db00875f2d --- /dev/null +++ b/source/renderer/app/actions/assets-actions.js @@ -0,0 +1,14 @@ +// @flow +import Action from './lib/Action'; +import type { AssetToken } from '../api/assets/types'; + +// ======= ASSETS ACTIONS ======= + +export default class AssetsActions { + onAssetSettingsOpen: Action<{ asset: AssetToken }> = new Action(); + onAssetSettingsSubmit: Action<{ + asset: AssetToken, + decimals: number, + }> = new Action(); + onAssetSettingsCancel: Action = new Action(); +} diff --git a/source/renderer/app/actions/currency-actions.js b/source/renderer/app/actions/currency-actions.js new file mode 100644 index 0000000000..af128c426c --- /dev/null +++ b/source/renderer/app/actions/currency-actions.js @@ -0,0 +1,9 @@ +// @flow +import Action from './lib/Action'; + +// ======= CURRENCY ACTIONS ======= + +export default class CurrencyActions { + setCurrencySelected: Action<{ code: string }> = new Action(); + toggleCurrencyIsActive: Action = new Action(); +} diff --git a/source/renderer/app/actions/index.js b/source/renderer/app/actions/index.js index a2d6649434..254fc4952c 100644 --- a/source/renderer/app/actions/index.js +++ b/source/renderer/app/actions/index.js @@ -2,6 +2,8 @@ import AddressesActions from './addresses-actions'; import AppActions from './app-actions'; import AppUpdateActions from './app-update-actions'; +import AssetsActions from './assets-actions'; +import CurrencyActions from './currency-actions'; import DialogsActions from './dialogs-actions'; import HardwareWalletsActions from './hardware-wallets-actions'; import NetworkStatusActions from './network-status-actions'; @@ -23,7 +25,9 @@ export type ActionsMap = { addresses: AddressesActions, app: AppActions, appUpdate: AppUpdateActions, + assets: AssetsActions, dialogs: DialogsActions, + currency: CurrencyActions, hardwareWallets: HardwareWalletsActions, networkStatus: NetworkStatusActions, notifications: NotificationsActions, @@ -45,6 +49,8 @@ const actionsMap: ActionsMap = { addresses: new AddressesActions(), app: new AppActions(), appUpdate: new AppUpdateActions(), + assets: new AssetsActions(), + currency: new CurrencyActions(), dialogs: new DialogsActions(), hardwareWallets: new HardwareWalletsActions(), networkStatus: new NetworkStatusActions(), diff --git a/source/renderer/app/actions/wallets-actions.js b/source/renderer/app/actions/wallets-actions.js index 43c74c22c5..9c6e6735b9 100644 --- a/source/renderer/app/actions/wallets-actions.js +++ b/source/renderer/app/actions/wallets-actions.js @@ -7,7 +7,7 @@ import type { } from '../../../common/types/hardware-wallets.types'; import type { CsvFileContent } from '../../../common/types/csv-request.types'; import type { QuitStakePoolRequest } from '../api/staking/types'; -import type { WalletSummaryAsset } from '../api/assets/types'; +import type { AssetToken } from '../api/assets/types'; export type WalletImportFromFileParams = { filePath: string, @@ -55,7 +55,7 @@ export default class WalletsActions { receiver: string, amount: string, passphrase: string, - assets?: Array, + assets?: Array, assetsAmounts?: Array, }> = new Action(); chooseWalletExportType: Action<{ @@ -87,13 +87,10 @@ export default class WalletsActions { setCertificateTemplate: Action<{ selectedTemplate: string }> = new Action(); finishCertificate: Action = new Action(); finishRewardsCsv: Action = new Action(); - setCurrencySelected: Action<{ currencyCode: string }> = new Action(); - toggleCurrencyIsActive: Action = new Action(); /* ---------- Transfer Funds ---------- */ - setActiveAssetFingerprint: Action<{ - fingerprint: ?string, - }> = new Action(); + setActiveAsset: Action = new Action(); + unsetActiveAsset: Action = new Action(); transferFundsNextStep: Action = new Action(); transferFundsPrevStep: Action = new Action(); transferFundsSetSourceWalletId: Action<{ diff --git a/source/renderer/app/api/addresses/types.js b/source/renderer/app/api/addresses/types.js index 1a35aff0ed..26446e6ed7 100644 --- a/source/renderer/app/api/addresses/types.js +++ b/source/renderer/app/api/addresses/types.js @@ -12,6 +12,7 @@ export type GetAddressesRequestQueryParams = { export type Address = { id: string, state: AddressState, + derivation_path: Array, }; export type Addresses = Array
; diff --git a/source/renderer/app/api/api.js b/source/renderer/app/api/api.js index 854677da52..6a1b3437ac 100644 --- a/source/renderer/app/api/api.js +++ b/source/renderer/app/api/api.js @@ -100,6 +100,7 @@ import { generateAdditionalMnemonics, } from './utils/mnemonics'; import { filterLogData } from '../../../common/utils/logging'; +import { derivationPathToAddressPath } from '../utils/hardwareWalletUtils'; // Config constants import { LOVELACES_PER_ADA } from '../config/numbersConfig'; @@ -234,7 +235,9 @@ import type { GetAssetsRequest, GetAssetsResponse, ApiAsset, + StoredAssetMetadata, } from './assets/types'; +import type { AssetLocalData } from './utils/localStorage'; import Asset from '../domains/Asset'; import { getAssets } from './assets/requests/getAssets'; import { getAccountPublicKey } from './wallets/requests/getAccountPublicKey'; @@ -244,6 +247,10 @@ const { isIncentivizedTestnet } = global; export default class AdaApi { config: RequestConfig; + // We need to preserve all asset metadata during single runtime in order + // to avoid losing it in case of Token Metadata Registry server unvailability + storedAssetMetadata: StoredAssetMetadata = {}; + constructor(isTest: boolean, config: RequestConfig) { this.setRequestConfig(config); if (isTest) patchAdaApi(this); @@ -255,12 +262,21 @@ export default class AdaApi { getWallets = async (): Promise> => { logger.debug('AdaApi::getWallets called'); + const { + getHardwareWalletLocalData, + getHardwareWalletsLocalData, + } = global.daedalus.api.localStorage; try { const wallets: AdaWallets = await getWallets(this.config); const legacyWallets: LegacyAdaWallets = await getLegacyWallets( this.config ); - logger.debug('AdaApi::getWallets success', { wallets, legacyWallets }); + const hwLocalData = await getHardwareWalletsLocalData(); + logger.debug('AdaApi::getWallets success', { + wallets, + legacyWallets, + hwLocalData: filterLogData(hwLocalData), + }); map(legacyWallets, (legacyAdaWallet) => { const extraLegacyWalletProps = { address_pool_gap: 0, // Not needed for legacy wallets @@ -281,9 +297,6 @@ export default class AdaApi { return await Promise.all( wallets.map(async (wallet) => { const { id } = wallet; - const { - getHardwareWalletLocalData, - } = global.daedalus.api.localStorage; const walletData = await getHardwareWalletLocalData(id); return _createWalletFromServerData({ ...wallet, @@ -629,7 +642,17 @@ export default class AdaApi { logger.debug('AdaApi::getAssets success', { assets: response, }); - const assets = response.map((asset) => _createAssetFromServerData(asset)); + const assetsLocaldata = await global.daedalus.api.localStorage.getAssetsLocalData(); + logger.debug('AdaApi::getAssetsLocalData success', { + assetsLocaldata, + }); + const assets = response.map((asset) => + _createAssetFromServerData( + asset, + assetsLocaldata[asset.policy_id + asset.asset_name] || {}, + this.storedAssetMetadata + ) + ); return new Promise((resolve) => resolve({ assets, total: response.length }) ); @@ -2730,10 +2753,11 @@ const _createWalletFromServerData = action( const _createAddressFromServerData = action( 'AdaApi::_createAddressFromServerData', (address: Address) => { - const { id, state } = address; + const { id, state, derivation_path: derivationPath } = address; return new WalletAddress({ id, used: state === 'used', + spendingPath: derivationPathToAddressPath(derivationPath), // E.g. "1852'/1815'/0'/0/19", }); } ); @@ -2825,18 +2849,33 @@ const _createTransactionFromServerData = action( const _createAssetFromServerData = action( 'AdaApi::_createAssetFromServerData', - (data: ApiAsset) => { + ( + data: ApiAsset, + localData: AssetLocalData, + storedAssetMetadata: StoredAssetMetadata + ) => { const { policy_id: policyId, asset_name: assetName, fingerprint, metadata, } = data; + const uniqueId = `${policyId}${assetName}`; + const storedMetadata = storedAssetMetadata[uniqueId]; + const { decimals } = localData; + const { decimals: recommendedDecimals = null } = + metadata || storedMetadata || {}; + if (metadata) { + storedAssetMetadata[uniqueId] = metadata; + } return new Asset({ policyId, assetName, fingerprint, - metadata, + metadata: metadata || storedMetadata, + decimals, + recommendedDecimals, + uniqueId, }); } ); diff --git a/source/renderer/app/api/assets/requests/getAssets.js b/source/renderer/app/api/assets/requests/getAssets.js index d75fa3f973..a72538c023 100644 --- a/source/renderer/app/api/assets/requests/getAssets.js +++ b/source/renderer/app/api/assets/requests/getAssets.js @@ -1,13 +1,13 @@ // @flow import type { RequestConfig } from '../../common/types'; import { request } from '../../utils/request'; -import type { Assets, GetAssetsRequest } from '../types'; +import type { ApiAssets, GetAssetsRequest } from '../types'; import { getRawWalletId, isLegacyWalletId } from '../../utils'; export const getAssets = ( config: RequestConfig, { walletId }: GetAssetsRequest -): Promise => +): Promise => request({ method: 'GET', path: `/v2/${ diff --git a/source/renderer/app/api/assets/types.js b/source/renderer/app/api/assets/types.js index d97a050c33..c2f335289f 100644 --- a/source/renderer/app/api/assets/types.js +++ b/source/renderer/app/api/assets/types.js @@ -1,72 +1,89 @@ // @flow import BigNumber from 'bignumber.js'; -import Asset from '../../domains/Asset'; - -export type SingleAsset = { - policy_id: string, - asset_name: string, - metadata?: ?AssetMetadata, -}; +import AssetDomain from '../../domains/Asset'; +/** + * + * ASSET + * Fetched from the Assets API endpoint + * It's not attached to a particular wallet or transaction + * Therefore, it doesn't have `quantity` nor `address` + * + * Exclusive data: fingerprint, metadata + * Missing data: quantity, address + * + */ export type ApiAsset = { policy_id: string, asset_name: string, fingerprint: string, metadata?: ?AssetMetadata, }; - -export type AssetMetadata = { - name: string, - ticker: string, - description: string, - unit?: { - decimals: number, - name: string, - }, - url?: string, - logo?: string, +export type ApiAssets = Array; +export type Asset = { + assetName: string, + decimals: ?number, + fingerprint: string, + metadata?: ?AssetMetadata, + policyId: string, + recommendedDecimals: ?number, + uniqueId: string, }; -export type AssetItem = { +/** + * + * TOKEN + * Asset that is attached to a particular wallet and/or transaction + * It doesn't have the Asset details (fingerprint, metadata) + * + * Exclusive data: quantity, address + * Missing data: fingerprint, metadata + * + */ +export type ApiToken = { policy_id: string, asset_name: string, quantity: number, + address?: ?string, }; - -export type WalletAssetItem = { +export type ApiTokens = Array; +export type Token = { policyId: string, assetName: string, quantity: BigNumber, address?: ?string, }; +export type Tokens = Array; +export type WalletTokens = { + available: Tokens, + total: Tokens, +}; -export type Assets = Array; - -export type AssetItems = Array; - -export type WalletAssetItems = Array; - -export type WalletAssets = { - available: WalletAssetItems, - total: WalletAssetItems, +/** + * + * ASSET TOKEN + * Merged object from a Token and its relative Asset details + * + * It has all the data combined: quantity, address, fingerprint, metadata, etc. + * + */ +export type AssetToken = { + ...$Exact, + ...$Exact, }; -export type WalletSummaryAsset = { - policyId: string, - assetName: string, - fingerprint: string, - quantity: BigNumber, - metadata: ?AssetMetadata, +export type AssetMetadata = { + name: string, + ticker: string, + description: string, + decimals?: number, // [0 .. 255] + url?: string, + logo?: string, }; -export type WalletTransactionAsset = { - policyId: string, - assetName: string, - quantity: BigNumber, - fingerprint: string, - metadata: ?AssetMetadata, - address?: ?string, +export type StoredAssetMetadata = { + [uniqueId: string]: AssetMetadata, }; export type GetUnknownAssetRequest = { @@ -79,6 +96,6 @@ export type GetAssetsRequest = { }; export type GetAssetsResponse = { - assets: Array, + assets: Array, total: number, }; diff --git a/source/renderer/app/api/transactions/types.js b/source/renderer/app/api/transactions/types.js index ce54234d4a..d2015a3a16 100644 --- a/source/renderer/app/api/transactions/types.js +++ b/source/renderer/app/api/transactions/types.js @@ -3,7 +3,7 @@ import BigNumber from 'bignumber.js'; import { WalletTransaction } from '../../domains/WalletTransaction'; import { WalletUnits } from '../../domains/Wallet'; import type { DelegationAction } from '../../types/stakingTypes'; -import type { AssetItems } from '../assets/types'; +import type { ApiTokens } from '../assets/types'; import type { TransactionMetadata } from '../../types/TransactionMetadata'; export type TransactionAmount = { @@ -60,7 +60,7 @@ export type Transactions = Array; export type TransactionInputs = { address: string, amount?: TransactionAmount, - assets?: AssetItems, + assets?: ApiTokens, id: string, index: number, }; @@ -68,7 +68,7 @@ export type TransactionInputs = { export type TransactionOutputs = { address: string, amount: TransactionAmount, - assets?: AssetItems, + assets?: ApiTokens, }; export type TransactionWithdrawals = { @@ -114,7 +114,7 @@ export type GetTransactionFeeRequest = { walletId: string, address: string, amount: number, - assets?: AssetItems, + assets?: ApiTokens, walletBalance: BigNumber, availableBalance: BigNumber, rewardsBalance: BigNumber, @@ -133,7 +133,7 @@ export type CreateTransactionRequest = { amount: number, passphrase: string, isLegacy: boolean, - assets?: AssetItems, + assets?: ApiTokens, withdrawal?: 'self' | Array, }; @@ -171,7 +171,7 @@ export type GetTransactionFeeParams = { export type TransactionPaymentData = { address: string, amount: TransactionFeeAmount, - assets?: AssetItems, + assets?: ApiTokens, }; export type TransactionFee = { @@ -238,7 +238,7 @@ export type CoinSelectionsPaymentRequestType = { walletId: string, address: string, amount: number, - assets?: AssetItems, + assets?: ApiTokens, }; export type CoinSelectionsRequest = diff --git a/source/renderer/app/api/utils/localStorage.js b/source/renderer/app/api/utils/localStorage.js index 546daa058c..9294b03571 100644 --- a/source/renderer/app/api/utils/localStorage.js +++ b/source/renderer/app/api/utils/localStorage.js @@ -2,7 +2,7 @@ /* eslint-disable consistent-return */ -import { includes, without } from 'lodash'; +import { includes, without, get } from 'lodash'; import { electronStoreConversation } from '../../ipc/electronStoreConversation'; import { WalletMigrationStatuses } from '../../stores/WalletMigrationStore'; import { @@ -18,7 +18,7 @@ import type { DeviceType, } from '../../../../common/types/hardware-wallets.types'; import type { StorageKey } from '../../../../common/types/electron-store.types'; -import type { Currency } from '../../types/currencyTypes'; +import type { Currency, DeprecatedCurrency } from '../../types/currencyTypes'; import { CURRENCY_IS_ACTIVE_BY_DEFAULT, CURRENCY_DEFAULT_SELECTED, @@ -71,6 +71,10 @@ export type HardwareWalletDevicesType = { [key: string]: TransportDevice, }; +export type AssetLocalData = { + decimals: number, +}; + /** * This api layer provides access to the electron local storage * for user settings that are not synced with any coin backend. @@ -188,11 +192,33 @@ export default class LocalStorageApi { unsetDataLayerMigrationAcceptance = (): Promise => LocalStorageApi.unset(keys.DATA_LAYER_MIGRATION_ACCEPTANCE); - getCurrencySelected = (): Promise => - LocalStorageApi.get(keys.CURRENCY_SELECTED, CURRENCY_DEFAULT_SELECTED); + getCurrencySelected = async (): Promise => { + const localCurrencySelected: Promise< + Currency | DeprecatedCurrency | string + > = await LocalStorageApi.get( + keys.CURRENCY_SELECTED, + CURRENCY_DEFAULT_SELECTED + ); + if (typeof localCurrencySelected === 'string') return localCurrencySelected; + /** + * + * Prior versions were storing the whole Currency object, + * which could lead different formats (e.g. currency.code or currency.symbol) + * It now stores only the currency code string, + * but we need to account for users storing old formats + * + * In this case, we also set the correct local code value + * + */ + const localCurrencyCode: string = + get(localCurrencySelected, 'code') || + get(localCurrencySelected, 'symbol'); + this.setCurrencySelected(localCurrencyCode); + return localCurrencyCode; + }; - setCurrencySelected = (currency: Currency): Promise => - LocalStorageApi.set(keys.CURRENCY_SELECTED, currency); + setCurrencySelected = (currencyCode: string): Promise => + LocalStorageApi.set(keys.CURRENCY_SELECTED, currencyCode); unsetCurrencySelected = (): Promise => LocalStorageApi.unset(keys.CURRENCY_SELECTED); @@ -300,6 +326,38 @@ export default class LocalStorageApi { unsetAppUpdateCompleted = (): Promise => LocalStorageApi.unset(keys.APP_UPDATE_COMPLETED); + getAssetsLocalData = (): Promise => + LocalStorageApi.get(keys.ASSET_DATA, []); + + unsetAssetsLocalData = (): Promise => + LocalStorageApi.unset(keys.ASSET_DATA); + + getAssetLocalData = ( + policyId: string, + assetName: string + ): Promise => + LocalStorageApi.get(keys.ASSET_DATA, {}, `${policyId}${assetName}`); + + setAssetLocalData = ( + policyId: string, + assetName: string, + assetLocalData: AssetLocalData + ): Promise => + LocalStorageApi.set( + keys.ASSET_DATA, + assetLocalData, + `${policyId}${assetName}` + ); + + getAssetSettingsDialogWasOpened = (): Promise => + LocalStorageApi.get(keys.ASSET_SETTINGS_DIALOG_WAS_OPENED, false); + + setAssetSettingsDialogWasOpened = (): Promise => + LocalStorageApi.set(keys.ASSET_SETTINGS_DIALOG_WAS_OPENED, true); + + unsetAssetSettingsDialogWasOpened = (): Promise => + LocalStorageApi.unset(keys.ASSET_SETTINGS_DIALOG_WAS_OPENED); + getSmashServer = (): Promise => LocalStorageApi.get(keys.SMASH_SERVER); diff --git a/source/renderer/app/api/wallets/requests/getAccountPublicKey.js b/source/renderer/app/api/wallets/requests/getAccountPublicKey.js index c8c967eb07..8d3dff9594 100644 --- a/source/renderer/app/api/wallets/requests/getAccountPublicKey.js +++ b/source/renderer/app/api/wallets/requests/getAccountPublicKey.js @@ -14,5 +14,5 @@ export const getAccountPublicKey = ( ...config, }, {}, - { passphrase, extended } + { passphrase, format: extended ? 'extended' : 'non_extended' } ); diff --git a/source/renderer/app/api/wallets/types.js b/source/renderer/app/api/wallets/types.js index 169a1222bb..041ccf937a 100644 --- a/source/renderer/app/api/wallets/types.js +++ b/source/renderer/app/api/wallets/types.js @@ -3,7 +3,7 @@ import BigNumber from 'bignumber.js'; import { WalletUnits } from '../../domains/Wallet'; import type { ExportedByronWallet } from '../../types/walletExportTypes'; import type { Currency, LocalizedCurrency } from '../../types/currencyTypes'; -import type { AssetItems } from '../assets/types'; +import type { ApiTokens } from '../assets/types'; export type Block = { slot_number: number, @@ -41,8 +41,8 @@ export type AdaWallet = { reward: WalletBalance, }, assets: { - available: AssetItems, - total: AssetItems, + available: ApiTokens, + total: ApiTokens, }, delegation: { active: WalletDelegation, diff --git a/source/renderer/app/assets/images/asset-token-settings-ic.inline.svg b/source/renderer/app/assets/images/asset-token-settings-ic.inline.svg new file mode 100644 index 0000000000..5c5698a1fc --- /dev/null +++ b/source/renderer/app/assets/images/asset-token-settings-ic.inline.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/asset-token-warning-ic.inline.svg b/source/renderer/app/assets/images/asset-token-warning-ic.inline.svg new file mode 100644 index 0000000000..e1b65d4e39 --- /dev/null +++ b/source/renderer/app/assets/images/asset-token-warning-ic.inline.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/source/renderer/app/components/widgets/AssetToken.js b/source/renderer/app/components/assets/Asset.js similarity index 58% rename from source/renderer/app/components/widgets/AssetToken.js rename to source/renderer/app/components/assets/Asset.js index e5f331c0ef..c7f5993d0a 100644 --- a/source/renderer/app/components/widgets/AssetToken.js +++ b/source/renderer/app/components/assets/Asset.js @@ -6,73 +6,97 @@ import { PopOver } from 'react-polymorph/lib/components/PopOver'; import { defineMessages, intlShape } from 'react-intl'; import CopyToClipboard from 'react-copy-to-clipboard'; import { observer } from 'mobx-react'; -import styles from './AssetToken.scss'; +import styles from './Asset.scss'; import { ellipsis, hexToString } from '../../utils/strings'; -import type { WalletSummaryAsset } from '../../api/assets/types'; + import copyIcon from '../../assets/images/copy-asset.inline.svg'; +import settingsIcon from '../../assets/images/asset-token-settings-ic.inline.svg'; +import warningIcon from '../../assets/images/asset-token-warning-ic.inline.svg'; import copyCheckmarkIcon from '../../assets/images/check-w.inline.svg'; import { ASSET_TOKEN_ID_COPY_FEEDBACK, ASSET_TOKEN_DISPLAY_DELAY, } from '../../config/timingConfig'; +import type { Asset as AssetProps } from '../../api/assets/types'; const messages = defineMessages({ fingerprintItem: { - id: 'widgets.assetToken.item.fingerprint', + id: 'assets.assetToken.item.fingerprint', defaultMessage: '!!!Fingerprint', description: '"fingerprint" item.', }, policyIdItem: { - id: 'widgets.assetToken.item.policyId', + id: 'assets.assetToken.item.policyId', defaultMessage: '!!!Policy Id', description: '"policyId" item.', }, assetNameItem: { - id: 'widgets.assetToken.item.assetName', + id: 'assets.assetToken.item.assetName', defaultMessage: '!!!Asset name', description: '"assetName" item.', }, nameItem: { - id: 'widgets.assetToken.item.name', + id: 'assets.assetToken.item.name', defaultMessage: '!!!Name', description: '"name" item.', }, tickerItem: { - id: 'widgets.assetToken.item.ticker', + id: 'assets.assetToken.item.ticker', defaultMessage: '!!!Ticker', description: '"ticker" item.', }, descriptionItem: { - id: 'widgets.assetToken.item.description', + id: 'assets.assetToken.item.description', defaultMessage: '!!!Description', description: '"description" item.', }, blank: { - id: 'widgets.assetToken.item.blank', + id: 'assets.assetToken.item.blank', defaultMessage: '!!!Blank', description: '"Blank" item value.', }, + settingsCogPopOver: { + id: 'assets.assetToken.settings.cogPopOver', + defaultMessage: + '!!!You can configure the number of decimal places for this native token.', + description: 'Asset settings pop over content', + }, + settingsWarningPopOverAvailable: { + id: 'assets.warning.available', + defaultMessage: + '!!!Recommended configuration for decimal places for this native token is available.', + description: 'Asset settings recommended pop over content', + }, + settingsWarningPopOverNotUsing: { + id: 'assets.warning.notUsing', + defaultMessage: + '!!!You are not using the recommended decimal place configuration for this native token.', + description: 'Asset settings recommended pop over content', + }, }); type Props = { - asset: WalletSummaryAsset, + asset: AssetProps, small?: boolean, - hideTooltip?: boolean, + hidePopOver?: boolean, onCopyAssetItem?: Function, - componentClassName?: string, - contentClassName?: string, + onClickSettings?: Function, + assetSettingsDialogWasOpened?: ?boolean, + anyAssetWasHovered?: ?boolean, + className?: string, // In case it's not possible to calculate the container width // this props defines after how many characters the `metadata.name` text will cut off metadataNameChars?: number, }; type State = { - isTooltipVisible: boolean, + isPillPopOverVisible: boolean, itemCopied: ?string, + isHoveringSettingsIcon: boolean, }; @observer -export default class AssetToken extends Component { +export default class Asset extends Component { static contextTypes = { intl: intlShape.isRequired, }; @@ -81,8 +105,9 @@ export default class AssetToken extends Component { displayDelayTimeout: TimeoutID; state = { - isTooltipVisible: false, + isPillPopOverVisible: false, itemCopied: null, + isHoveringSettingsIcon: false, }; // We need to track the mounted state in order to avoid calling @@ -98,28 +123,55 @@ export default class AssetToken extends Component { this._isMounted = false; } - handleShowTooltip = () => { + handleShowPillPopOver = () => { clearTimeout(this.displayDelayTimeout); this.displayDelayTimeout = setTimeout(() => { if (this._isMounted) { this.setState({ - isTooltipVisible: true, + isPillPopOverVisible: true, }); } }, ASSET_TOKEN_DISPLAY_DELAY); }; - handleHideTooltip = () => { + handleHidePillPopOver = () => { clearTimeout(this.displayDelayTimeout); this.displayDelayTimeout = setTimeout(() => { if (this._isMounted) { this.setState({ - isTooltipVisible: false, + isPillPopOverVisible: false, }); } }, ASSET_TOKEN_DISPLAY_DELAY); }; + handleSettingsMouseEnter = () => { + this.setState({ + isHoveringSettingsIcon: true, + }); + }; + + handleSettingsMouseLeave = () => { + this.setState({ + isHoveringSettingsIcon: false, + }); + }; + + get isSettingsPopOverVisible() { + const { assetSettingsDialogWasOpened, anyAssetWasHovered } = this.props; + const { isHoveringSettingsIcon } = this.state; + if (isHoveringSettingsIcon) { + return true; + } + if ( + assetSettingsDialogWasOpened === false && + anyAssetWasHovered === false + ) { + return true; + } + return false; + } + handleCopyItem = (itemCopied: string, assetItem: string, value: string) => { const { onCopyAssetItem } = this.props; if (onCopyAssetItem) { @@ -136,14 +188,13 @@ export default class AssetToken extends Component { }, ASSET_TOKEN_ID_COPY_FEEDBACK); }; - contentRender() { - const { asset, metadataNameChars, small, contentClassName } = this.props; + renderPillContent() { + const { asset, metadataNameChars, small } = this.props; const { fingerprint, metadata } = asset; const { name } = metadata || {}; const contentStyles = classnames([ - styles.content, + styles.pill, small ? styles.small : null, - contentClassName, ]); return (
@@ -159,7 +210,7 @@ export default class AssetToken extends Component { ); } - assetItemRenderer = (assetId: string, assetItem: string, value: string) => { + renderAssetItem = (assetId: string, assetItem: string, value: string) => { const { itemCopied } = this.state; const icon = itemCopied === assetId ? copyCheckmarkIcon : copyIcon; const iconClassnames = classnames([ @@ -186,14 +237,14 @@ export default class AssetToken extends Component { ); }; - tooltipRender() { + renderPillPopOverContent() { const { intl } = this.context; const { asset } = this.props; const { fingerprint, policyId, assetName, metadata } = asset; const { name, ticker, description } = metadata || {}; - const item = this.assetItemRenderer; + const item = this.renderAssetItem; return ( -
+
{item( 'fingerprint', @@ -261,18 +312,15 @@ export default class AssetToken extends Component { ); } - render() { - const children = this.contentRender(); - const { hideTooltip, componentClassName } = this.props; - if (hideTooltip) return children; - const { isTooltipVisible } = this.state; - const tooltipContent = this.tooltipRender(); - const componentStyles = classnames([styles.component, componentClassName]); + renderPillPopOverContainer = () => { + const pillContent = this.renderPillContent(); + const popOverContent = this.renderPillPopOverContent(); + const { isPillPopOverVisible } = this.state; return (
{ '0 5px 20px 0 var(--theme-widgets-asset-token-box-shadow)', }} contentClassName={styles.popOver} - content={tooltipContent} - visible={isTooltipVisible} + content={popOverContent} + visible={isPillPopOverVisible} appendTo="parent" maxWidth={376} allowHTML interactive > - {children} + {pillContent}
); + }; + + renderSettingsContent = () => { + const { intl } = this.context; + const { asset, onClickSettings } = this.props; + if (!onClickSettings) return null; + const { + isSettingsPopOverVisible, + handleSettingsMouseEnter, + handleSettingsMouseLeave, + } = this; + const onClickSettingsBind = () => onClickSettings && onClickSettings(asset); + const { decimals, recommendedDecimals } = asset; + const hasWarning = + typeof recommendedDecimals === 'number' && + decimals !== recommendedDecimals; + let warningPopOverMessage; + if (hasWarning) { + warningPopOverMessage = + typeof decimals === 'number' + ? messages.settingsWarningPopOverNotUsing + : messages.settingsWarningPopOverAvailable; + } + return ( + + ); + }; + + render() { + const { hidePopOver, className } = this.props; + + const content = hidePopOver + ? this.renderPillContent() + : this.renderPillPopOverContainer(); + const settingsContent = this.renderSettingsContent(); + + const componenClassnames = classnames([styles.component, className]); + return ( +
+ {content} + {settingsContent} +
+ ); } } diff --git a/source/renderer/app/components/widgets/AssetToken.scss b/source/renderer/app/components/assets/Asset.scss similarity index 80% rename from source/renderer/app/components/widgets/AssetToken.scss rename to source/renderer/app/components/assets/Asset.scss index 33d0343fa9..30c3d9c753 100644 --- a/source/renderer/app/components/widgets/AssetToken.scss +++ b/source/renderer/app/components/assets/Asset.scss @@ -1,7 +1,12 @@ .component { display: inline-flex; - min-width: 0; width: 100%; +} + +.popOverContainer { + display: inline-flex; + min-width: 0; + text-transform: initial; > span { min-width: 0; @@ -14,7 +19,7 @@ } } -.content { +.pill { display: flex; height: 22px; min-width: 0; @@ -49,7 +54,7 @@ display: none; } -.tooltipContent { +.popOverContent { align-items: center; display: flex; flex-direction: column; @@ -101,7 +106,7 @@ max-height: 48px; overflow-y: auto; padding-right: 20px; - width: calc(100% - 77px); + width: calc(100% - 87px); &::-webkit-scrollbar-button { display: none; @@ -177,3 +182,42 @@ } } } + +.settingsButton { + cursor: pointer; + display: flex; + position: relative; + z-index: 1; + &:hover { + .settingsIcon { + opacity: 1; + } + } + .settingsIcon, + .warningIcon { + opacity: 0.6; + &:hover { + opacity: 1; + } + } + > span { + display: inline-flex; + margin-left: 6px; + } + .settingsIcon { + svg { + height: 10px; + width: 10px; + > g > g { + fill: var(--theme-widgets-asset-token-text-color); + } + } + } + .warningIcon { + margin-top: -1px; + svg { + height: 9px; + width: 11px; + } + } +} diff --git a/source/renderer/app/components/assets/AssetAmount.js b/source/renderer/app/components/assets/AssetAmount.js new file mode 100644 index 0000000000..a569a321b3 --- /dev/null +++ b/source/renderer/app/components/assets/AssetAmount.js @@ -0,0 +1,64 @@ +// @flow +import React, { Component } from 'react'; +import BigNumber from 'bignumber.js'; +import classnames from 'classnames'; +import { PopOver } from 'react-polymorph/lib/components/PopOver'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import { observer } from 'mobx-react'; +import styles from './AssetAmount.scss'; +import { formattedTokenWalletAmount } from '../../utils/formatters'; +import type { AssetMetadata } from '../../api/assets/types'; + +const messages = defineMessages({ + unformattedAmount: { + id: 'assets.assetAmount.unformattedAmount', + defaultMessage: '!!!Unformatted amount {amount}', + description: 'Unformatted amount', + }, +}); + +type Props = { + amount: BigNumber, + metadata?: ?AssetMetadata, + decimals: ?number, + isLoading?: boolean, + className?: string, +}; + +@observer +export default class AssetAmount extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + render() { + const { amount, metadata, decimals, isLoading, className } = this.props; + if (isLoading) return '-'; + const componentStyles = classnames([styles.component, className]); + const content = !isLoading + ? formattedTokenWalletAmount(amount, metadata, decimals) + : '-'; + return ( +
+ {decimals ? ( + + } + visible={decimals ? undefined : false} + className={styles.unformattedAmount} + > + {content} + + ) : ( + {content} + )} +
+ ); + } +} diff --git a/source/renderer/app/components/assets/AssetAmount.scss b/source/renderer/app/components/assets/AssetAmount.scss new file mode 100644 index 0000000000..efeaa9765c --- /dev/null +++ b/source/renderer/app/components/assets/AssetAmount.scss @@ -0,0 +1,11 @@ +.component { + span { + user-select: text; + word-break: break-all; + } +} +.unformattedAmount { + em { + opacity: 0.7; + } +} diff --git a/source/renderer/app/components/assets/AssetSettingsDialog.js b/source/renderer/app/components/assets/AssetSettingsDialog.js new file mode 100644 index 0000000000..51d8e920f7 --- /dev/null +++ b/source/renderer/app/components/assets/AssetSettingsDialog.js @@ -0,0 +1,223 @@ +// @flow +import React, { Component } from 'react'; +import SVGInline from 'react-svg-inline'; +import { range } from 'lodash'; +import { observer } from 'mobx-react'; +import { Select } from 'react-polymorph/lib/components/Select'; +import { PopOver } from 'react-polymorph/lib/components/PopOver'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import Asset from './Asset'; +import DialogCloseButton from '../widgets/DialogCloseButton'; +import Dialog from '../widgets/Dialog'; +import styles from './AssetSettingsDialog.scss'; +import globalMessages from '../../i18n/global-messages'; +import type { AssetToken } from '../../api/assets/types'; +import { formattedTokenWalletAmount } from '../../utils/formatters'; +import warningIcon from '../../assets/images/asset-token-warning-ic.inline.svg'; +import { + DEFAULT_DECIMAL_PRECISION, + MAX_DECIMAL_PRECISION, +} from '../../config/assetsConfig'; + +const messages = defineMessages({ + title: { + id: 'assets.settings.dialog.title', + defaultMessage: '!!!Native token settings', + description: '"title" for the Asset settings dialog', + }, + description: { + id: 'assets.settings.dialog.description', + defaultMessage: + '!!!Updates made here will be applied in other wallets containing this token too.', + description: '"description" for the Asset settings dialog', + }, + formattedBalanceLabel: { + id: 'assets.settings.dialog.formattedAmount.label', + defaultMessage: '!!!Unformated amount', + description: '"formattedBalanceLabel" for the Asset settings dialog', + }, + unformattedBalanceLabel: { + id: 'assets.settings.dialog.unformattedAmount.label', + defaultMessage: '!!!Formated amount', + description: '"unformattedBalanceLabel" for the Asset settings dialog', + }, + decimalPrecisionLabel: { + id: 'assets.settings.dialog.decimalPrecision.label', + defaultMessage: '!!!Number of decimal places', + description: '"decimalPrecisionLabel" for the Asset settings dialog', + }, + recommended: { + id: 'assets.settings.dialog.recommended', + defaultMessage: '!!!(recommended)', + description: '"recommended" for the Asset settings dialog', + }, + default: { + id: 'assets.settings.dialog.default', + defaultMessage: '!!!(default)', + description: '"default" for the Asset settings dialog', + }, + warningPopOverAvailable: { + id: 'assets.warning.available', + defaultMessage: + '!!!Recommended configuration for decimal places for this native token is available.', + description: 'Asset settings recommended pop over content', + }, + warningPopOverNotUsing: { + id: 'assets.warning.notUsing', + defaultMessage: + '!!!You are not using the recommended decimal place configuration for this native token.', + description: 'Asset settings recommended pop over content', + }, +}); + +type Props = { + asset: AssetToken, + onSubmit: Function, + onCancel: Function, +}; + +type State = { + decimals: ?number, +}; + +@observer +export default class AssetSettingsDialog extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + constructor(props: Props) { + super(props); + const { asset } = props; + const { decimals: savedDecimals, recommendedDecimals } = asset; + const hasSavedDecimals = typeof savedDecimals === 'number'; + this.state = { + decimals: hasSavedDecimals + ? savedDecimals + : recommendedDecimals || DEFAULT_DECIMAL_PRECISION, + }; + } + + onSetDecimalPrecision = (decimals: number) => { + this.setState({ decimals }); + }; + + optionRenderer = (props: { value: number }) => { + const { value } = props; + const { intl } = this.context; + const { recommendedDecimals } = this.props.asset; + let extraLabel; + if ( + typeof recommendedDecimals === 'number' && + recommendedDecimals === value + ) { + extraLabel = messages.recommended; + } else if (value === DEFAULT_DECIMAL_PRECISION) { + extraLabel = messages.default; + } + if (extraLabel) { + return ( +
+ {value} {intl.formatMessage(extraLabel)} +
+ ); + } + return value; + }; + + selectionRenderer = (props: { value: number }) => ( +
{this.optionRenderer(props)}
+ ); + + render() { + const { intl } = this.context; + const { onCancel, onSubmit, asset } = this.props; + const { decimals: savedDecimals, recommendedDecimals } = asset; + const { decimals } = this.state; + const hasSavedDecimals = typeof savedDecimals === 'number'; + const hasRecommendedDecimals = typeof recommendedDecimals === 'number'; + const options = range(MAX_DECIMAL_PRECISION + 1).map((value) => ({ + value, + })); + const actions = [ + { + label: intl.formatMessage(globalMessages.cancel), + onClick: onCancel, + }, + { + label: intl.formatMessage(globalMessages.save), + primary: true, + disabled: + (hasSavedDecimals && decimals === savedDecimals) || + (!hasSavedDecimals && decimals === DEFAULT_DECIMAL_PRECISION), + onClick: () => onSubmit(asset, decimals), + }, + ]; + const hasWarning = + hasRecommendedDecimals && savedDecimals !== recommendedDecimals; + let warningPopOverMessage; + if (hasWarning) { + warningPopOverMessage = hasSavedDecimals + ? messages.warningPopOverNotUsing + : messages.warningPopOverAvailable; + } + + return ( + } + actions={actions} + closeOnOverlayClick + onClose={onCancel} + closeButton={} + className1={styles.dialog} + > +
+
+ +
+
+ {intl.formatMessage(messages.unformattedBalanceLabel)} +
+

{formattedTokenWalletAmount(asset.quantity, null, 0)}

+
+ {intl.formatMessage(messages.formattedBalanceLabel)} +
+

+ {formattedTokenWalletAmount( + asset.quantity, + asset.metadata, + decimals + )} +

+ { this.addFocusableField(field); }} className="receiver" - error={receiverField.error} + error={receiverFieldError} onChange={(value) => { receiverField.onChange(value || ''); this.setState({ @@ -721,6 +742,7 @@ export default class WalletSendForm extends Component { }); }} onKeyPress={this.handleSubmitOnEnter} + themeVariables={receiverFieldThemeVars} /> {this.hasReceiverValue() && (
@@ -731,6 +753,7 @@ export default class WalletSendForm extends Component {
diff --git a/source/renderer/app/components/wallet/WalletSendForm.scss b/source/renderer/app/components/wallet/WalletSendForm.scss index e13628a372..08c7e02538 100755 --- a/source/renderer/app/components/wallet/WalletSendForm.scss +++ b/source/renderer/app/components/wallet/WalletSendForm.scss @@ -128,6 +128,12 @@ } } } + + &.sameRecieverInput { + input { + border-color: var(--rp-password-input-warning-score-color) !important; + } + } } .estimatedFeeInput, diff --git a/source/renderer/app/components/wallet/receive/WalletReceiveDialog.js b/source/renderer/app/components/wallet/receive/WalletReceiveDialog.js index 3afda16469..5672bb74f2 100644 --- a/source/renderer/app/components/wallet/receive/WalletReceiveDialog.js +++ b/source/renderer/app/components/wallet/receive/WalletReceiveDialog.js @@ -1,19 +1,34 @@ // @flow import React, { Component } from 'react'; import { observer } from 'mobx-react'; -import { defineMessages, intlShape } from 'react-intl'; +import { utils } from '@cardano-foundation/ledgerjs-hw-app-cardano'; +import { map, filter } from 'lodash'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import classnames from 'classnames'; import CopyToClipboard from 'react-copy-to-clipboard'; import SVGInline from 'react-svg-inline'; import { TextArea } from 'react-polymorph/lib/components/TextArea'; import { TextAreaSkin } from 'react-polymorph/lib/skins/simple/TextAreaSkin'; +import { PopOver } from 'react-polymorph/lib/components/PopOver'; import QRCode from 'qrcode.react'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; +import RadioSet from '../../widgets/RadioSet'; import Dialog from '../../widgets/Dialog'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import WalletAddress from '../../../domains/WalletAddress'; import globalMessages from '../../../i18n/global-messages'; import styles from './WalletReceiveDialog.scss'; import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; +import HardwareWalletStatus from '../../hardware-wallet/HardwareWalletStatus'; import iconCopy from '../../../assets/images/clipboard-ic.inline.svg'; +import { HW_SHELLEY_CONFIG } from '../../../config/hardwareWalletsConfig'; +import { hardenedPathToDerivationPath } from '../../../utils/hardwareWalletUtils'; +import { AddressVerificationCheckStatuses } from '../../../stores/HardwareWalletsStore'; +import LoadingSpinner from '../../widgets/LoadingSpinner'; + +import type { AddressVerificationCheckStatus } from '../../../stores/HardwareWalletsStore'; +import type { HwDeviceStatus } from '../../../domains/Wallet'; const messages = defineMessages({ inputLabel: { @@ -46,24 +61,118 @@ const messages = defineMessages({ defaultMessage: '!!!Copy address', description: 'Label for "Copy address" link on the wallet "Receive page"', }, + spendingPathTooltip: { + id: 'wallet.receive.dialog.spendingPathTooltip', + defaultMessage: '!!!Receiving address path', + description: 'Tooltip for the receiving address path', + }, + stakingPathTooltip: { + id: 'wallet.receive.dialog.stakingPathTooltip', + defaultMessage: '!!!Rewards address path', + description: 'Tooltip for the rewards address path', + }, + supportRequestButtonLabel: { + id: 'wallet.receive.dialog.supportRequestButtonLabel', + defaultMessage: '!!!Submit a request to IOHK Support', + description: 'Support request button label', + }, + supportRequestLinkUrl: { + id: 'wallet.receive.dialog.supportRequestLinkUrl', + defaultMessage: '!!!https://iohk.zendesk.com/hc/en-us/requests/new/', + description: 'Support request link URL', + }, + invalidAddressConfirmationLabel: { + id: 'wallet.receive.dialog.invalidAddressConfirmationLabel', + defaultMessage: + '!!!Yes, I am sure I have compared the address displayed in Daedalus with the address displayed on the {deviceType} device by comparing the beginning and ending of the address.', + description: 'Invalid address confirmation checkbox label', + }, + verificationCheckOptionsLabel: { + id: 'wallet.receive.dialog.verificationCheckOptionsLabel', + defaultMessage: '!!!Is the address you have verified correct?', + description: 'Verification options section label', + }, + verificationCheckOptionValid: { + id: 'wallet.receive.dialog.verificationCheckOptionValid', + defaultMessage: '!!!Yes', + description: 'Verification option "Valid" label', + }, + verificationCheckOptionInvalid: { + id: 'wallet.receive.dialog.verificationCheckOptionInvalid', + defaultMessage: + '!!!No, I am sure that address displayed in Daedalus is different from the address displayed on my {deviceType} device', + description: 'Verification option "Invalid" label', + }, + verificationCheckOptionReverify: { + id: 'wallet.receive.dialog.verificationCheckOptionReverify', + defaultMessage: '!!!No, reverify', + description: 'Verification option "Reverify" label', + }, + softwareCheckLabel: { + id: 'wallet.receive.dialog.softwareCheckLabel', + defaultMessage: '!!!Daedalus verified the address', + description: 'Daedalus verification status check label', + }, + confirmationCheckLabel: { + id: 'wallet.receive.dialog.confirmationCheckLabel', + defaultMessage: '!!!You have verified the address', + description: 'User verification status check label', + }, + addressVerificationInstructions: { + id: 'wallet.receive.dialog.addressVerificationInstructions', + defaultMessage: + '!!!Please compare the address displayed here on the screen with the address displayed on the {deviceType} device by comparing at least the first 5 characters at the start of the address after the "addr" part and at least 5 characters at the end of the address.', + description: 'Address verification instructions', + }, + invalidAddressWarningTitle: { + id: 'wallet.receive.dialog.invalidAddressWarningTitle', + defaultMessage: '!!!Warning, your copy of Daedalus may be hacked!', + description: 'Invalid address "Warning" title', + }, + invalidAddressWarningDescription: { + id: 'wallet.receive.dialog.invalidAddressWarningDescription', + defaultMessage: + '!!!You have manually compared the address shown in Daedalus with the address shown on the hardware wallet device and reported that they are different. If this is the case, please contact support and make sure you download and attach logs.', + description: 'Invalid address "Warning" description', + }, }); messages.fieldIsRequired = globalMessages.fieldIsRequired; type Props = { address: WalletAddress, + isHardwareWallet: boolean, + walletName: string, + hwDeviceStatus: HwDeviceStatus, + isAddressDerived: boolean, + isAddressChecked: boolean, onCopyAddress: Function, onDownloadPDF: Function, onSaveQRCodeImage: Function, onClose: Function, + onChangeVerificationStatus: Function, + onSupportRequestClick: Function, + isTrezor: boolean, +}; + +type State = { + selectedVerificationStatus: ?AddressVerificationCheckStatus, + isInvalidAddressConfirmed: boolean, + isReverifying: boolean, }; @observer -export default class WalletReceiveDialog extends Component { +export default class WalletReceiveDialog extends Component { static contextTypes = { intl: intlShape.isRequired, }; + state = { + selectedVerificationStatus: null, + isInvalidAddressConfirmed: false, + isReverifying: false, + }; + form = new ReactToolboxMobxForm({ fields: { noteInput: { @@ -91,23 +200,127 @@ export default class WalletReceiveDialog extends Component { field.value = field.value.replace(/\n/g, ''); }; + constructPaths = (address: WalletAddress) => { + const hardenedSpendingPath = utils.str_to_path(address.spendingPath); + const derivationSpendingPath = hardenedPathToDerivationPath( + hardenedSpendingPath + ); + const spendingPath = map( + derivationSpendingPath.constructed, + (constructeSpendingPathChunk, index) => { + const isChangeablePart = + index >= derivationSpendingPath.constructed.length - 2; + if (isChangeablePart) { + return /{constructeSpendingPathChunk}; + } + return index === 0 + ? constructeSpendingPathChunk + : `/${constructeSpendingPathChunk}`; + } + ); + + const derivationStakingPath = hardenedPathToDerivationPath( + HW_SHELLEY_CONFIG.DEFAULT_DERIVATION_PATH + ); + const stakingPath = map( + derivationStakingPath.constructed, + (constructeStakingPathChunk, index) => { + const isLastIndex = + index === derivationStakingPath.constructed.length - 1; + if (isLastIndex) { + return /{constructeStakingPathChunk}; + } + return index === 0 + ? constructeStakingPathChunk + : `/${constructeStakingPathChunk}`; + } + ); + + return { + stakingPath, + spendingPath, + }; + }; + + onChangeVerificationStatus = (status: AddressVerificationCheckStatus) => { + this.setState({ + selectedVerificationStatus: + status === AddressVerificationCheckStatuses.REVERIFY ? null : status, + isInvalidAddressConfirmed: false, + isReverifying: status === AddressVerificationCheckStatuses.REVERIFY, + }); + this.props.onChangeVerificationStatus(status); + }; + + handleConfirmInvalidAddress = (isConfirmed: boolean) => { + this.setState({ + isInvalidAddressConfirmed: isConfirmed, + }); + }; + render() { - const { address, onCopyAddress, onSaveQRCodeImage, onClose } = this.props; + const { + address, + onCopyAddress, + onSaveQRCodeImage, + onClose, + walletName, + hwDeviceStatus, + isHardwareWallet, + isAddressDerived, + isAddressChecked, + onSupportRequestClick, + isTrezor, + } = this.props; + const { + selectedVerificationStatus, + isInvalidAddressConfirmed, + isReverifying, + } = this.state; const { intl } = this.context; const noteInputField = this.form.$('noteInput'); + const deviceType = isHardwareWallet && isTrezor ? 'Trezor' : 'Ledger'; + const isSubmitting = false; + const buttonClasses = classnames([ + 'attention', + isSubmitting ? styles.isSubmitting : null, + ]); + const supportButtonLabel = !isSubmitting ? ( + intl.formatMessage(messages.supportRequestButtonLabel) + ) : ( + + ); + const isSupportRequestButton = + selectedVerificationStatus === AddressVerificationCheckStatuses.INVALID; - const actions = [ - { - label: intl.formatMessage(messages.saveQRCodeImage), - onClick: () => onSaveQRCodeImage(), - }, - { - className: 'downloadPDFButton', - label: intl.formatMessage(messages.downloadPDFButton), - onClick: this.submit, - primary: true, - }, - ]; + let actions; + if (isSupportRequestButton) { + const supportRequestLinkUrl = intl.formatMessage( + messages.supportRequestLinkUrl + ); + actions = [ + { + className: buttonClasses, + label: supportButtonLabel, + onClick: onSupportRequestClick.bind(this, supportRequestLinkUrl), + disabled: !isInvalidAddressConfirmed, + primary: true, + }, + ]; + } else { + actions = [ + { + label: intl.formatMessage(messages.saveQRCodeImage), + onClick: () => onSaveQRCodeImage(), + }, + { + className: 'downloadPDFButton', + label: intl.formatMessage(messages.downloadPDFButton), + onClick: this.submit, + primary: true, + }, + ]; + } // Get QRCode color value from active theme's CSS variable const qrCodeBackgroundColor = document.documentElement @@ -121,11 +334,61 @@ export default class WalletReceiveDialog extends Component { ) : '#000'; + const constructedPaths = this.constructPaths(address); + + const verificationOptions = [ + { + status: AddressVerificationCheckStatuses.VALID, + label: intl.formatMessage(messages.verificationCheckOptionValid), + }, + { + status: AddressVerificationCheckStatuses.REVERIFY, + label: intl.formatMessage(messages.verificationCheckOptionReverify), + }, + { + status: AddressVerificationCheckStatuses.INVALID, + label: intl.formatMessage(messages.verificationCheckOptionInvalid, { + deviceType, + }), + }, + ]; + + const filteredVerificationOptions = filter( + verificationOptions, + (option) => { + const isInvalidOption = + option.status === AddressVerificationCheckStatuses.INVALID; + if ( + (!selectedVerificationStatus && + (!isInvalidOption || (isInvalidOption && isReverifying))) || + (selectedVerificationStatus && + selectedVerificationStatus === option.status) + ) { + return option; + } + return null; + } + ); + + const showActions = + !isHardwareWallet || + (isHardwareWallet && + (selectedVerificationStatus === + AddressVerificationCheckStatuses.INVALID || + selectedVerificationStatus === + AddressVerificationCheckStatuses.VALID)); + + const isAddressConfirmed = + isAddressChecked && + isAddressDerived && + selectedVerificationStatus !== null; + return ( } > @@ -139,6 +402,20 @@ export default class WalletReceiveDialog extends Component { />
+
+ +
+ {constructedPaths.spendingPath} +
+
+ + +
+ {constructedPaths.stakingPath} +
+
+
+
{address.id}
{ -