diff --git a/CHANGELOG.md b/CHANGELOG.md index 281def2d9f..20f4cfcb0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,57 @@ Changelog ========= +## 3.3.0 + +### Fixes + +- Fixed issue with hardware wallet delegation ([PR 2369](https://github.com/input-output-hk/daedalus/pull/2369)) + +### Chores + +- Updated `cardano-launcher` to version `0.20210215.0` ([PR 2363](https://github.com/input-output-hk/daedalus/pull/2363)) +- Updated `cardano-wallet` to version `2021-02-15` ([PR 2363](https://github.com/input-output-hk/daedalus/pull/2363)) +- Updated `cardano-wallet` to version `2021-02-12` ([PR 2358](https://github.com/input-output-hk/daedalus/pull/2358)) +- Improved the error messages for the custom SMASH server url input ([PR 2355](https://github.com/input-output-hk/daedalus/pull/2355)) + +## 3.3.0-FC1 + +### Features + +- Added display of wallet balance in other currencies ([PR 2290](https://github.com/input-output-hk/daedalus/pull/2290)) +- Implemented alternate Ledger wallet handling ([PR 2342](https://github.com/input-output-hk/daedalus/pull/2342)) +- Re-enabled "Wallet import" feature ([PR 2308](https://github.com/input-output-hk/daedalus/pull/2308)) +- Configured "Staking" sidebar icon to always be shown and added a "Staking Syncing" screen to be shown instead of the "Delegation center" until Daedalus fully syncs ([PR 2315](https://github.com/input-output-hk/daedalus/pull/2315)) +- Implemented "Voting Center" ([PR 2315](https://github.com/input-output-hk/daedalus/pull/2315), [PR 2353](https://github.com/input-output-hk/daedalus/pull/2353), [PR 2354](https://github.com/input-output-hk/daedalus/pull/2354)) +- Implemented transaction metadata display ([PR 2338](https://github.com/input-output-hk/daedalus/pull/2338)) +- Displayed fee and deposit info in transaction details and in the delegation wizard ([PR 2339](https://github.com/input-output-hk/daedalus/pull/2339)) +- Added SMASH server configuration options ([PR 2259](https://github.com/input-output-hk/daedalus/pull/2259)) + +### Fixes + +- Fixed issues with downloading logs and exporting transaction CSV history on Linux platform +- Fixed an automatic update failure ([PR 2352](https://github.com/input-output-hk/daedalus/pull/2352)) +- Fixed logging issue with too few `cardano-wallet` logs being packed into logs zip archive ([PR 2341](https://github.com/input-output-hk/daedalus/pull/2341)) +- Fixed misalignment of the "i" icon on the "Set password" dialog ([PR 2337](https://github.com/input-output-hk/daedalus/pull/2337)) +- Removed steps counter from the "Success" wallet restoration dialog step ([PR 2335](https://github.com/input-output-hk/daedalus/pull/2335)) + +### Chores + +- Disabled "Voting Center" for Flight builds +- Updated `cardano-wallet` to revision `1ea5e882` ([PR 2356](https://github.com/input-output-hk/daedalus/pull/2356)) +- Force public key export on every interaction with hardware wallet device ([PR 2342](https://github.com/input-output-hk/daedalus/pull/2342)) +- Updated Hardware Wallets delegation deposit calculation ([PR 2332](https://github.com/input-output-hk/daedalus/pull/2332)) +- Implemented dynamic TTL calculation for hardware wallets transactions ([PR 2331](https://github.com/input-output-hk/daedalus/pull/2331)) +- Added link to connecting issues support article on the hardware wallet "Pairing" dialog ([PR 2336](https://github.com/input-output-hk/daedalus/pull/2336)) +- Updated recovery phrase entry ([PR 2334](https://github.com/input-output-hk/daedalus/pull/2334)) +- Adjusted sorting of table values on the "Rewards" screen ([PR 2333](https://github.com/input-output-hk/daedalus/pull/2333)) +- Fixed error thrown when closing delegation wizard while transaction fees are being calculated ([PR 2330](https://github.com/input-output-hk/daedalus/pull/2330)) +- Fixed number format for syncing percentage and stake pools count ([PR 2313](https://github.com/input-output-hk/daedalus/pull/2313)) +- Updated `cardano-wallet` to version `2021-01-28` and `cardano-node` to version `1.25.1` ([PR 2270](https://github.com/input-output-hk/daedalus/pull/2270)) +- Updated `react-polymorph` package ([PR 2318](https://github.com/input-output-hk/daedalus/pull/2318)) +- Updated `bignumber.js` package ([PR 2305](https://github.com/input-output-hk/daedalus/pull/2305)) +- Disabled application menu navigation before the "Terms of use" have been accepted ([PR 2304](https://github.com/input-output-hk/daedalus/pull/2304)) + ## 3.2.0 ### Chores diff --git a/README.md b/README.md index c5c69dc16d..cb2342f672 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,38 @@ Daedalus - Cryptocurrency Wallet 1. Run `yarn nix:shelley_qa` from `daedalus`. 2. Run `yarn dev` from the subsequent `nix-shell` +#### Native token metadata server + +Daedalus, by default, uses the following metadata server for all networks except for the mainnet: `https://metadata.cardano-testnet.iohkdev.io/`. + +It's also possible to use a mock server locally by running the following command in `nix-shell` prior to starting Daedalus: + +``` +$ mock-token-metadata-server ./utils/cardano/native-tokens/registry.json +Mock metadata server running with url http://localhost:65432/ +``` + +Then proceed to launch Daedalus and make sure to provide the mock token metadata server port: + +``` +$ MOCK_TOKEN_METADATA_SERVER_PORT=65432 yarn dev +``` + +This enables you to modify the metadata directly by modifying the registry file directly: + +``` +$ vi ./utils/cardano/native-tokens/registry.json # ..or any other editor, if you prefer +``` + +Use the following command to check if the mock server is working correctly: + +``` +$ curl -i -H "Content-type: application/json" --data '{"subjects":["789ef8ae89617f34c07f7f6a12e4d65146f958c0bc15a97b4ff169f1"],"properties":["name","description","acronym","unit","logo"]}' +http://localhost:65432/metadata/query +``` +... and expect a "200 OK" response. + + ### Running Daedalus with Jormungandr #### ITN Selfnode diff --git a/default.nix b/default.nix index 9d605e2f02..fb04ce156d 100644 --- a/default.nix +++ b/default.nix @@ -85,6 +85,7 @@ let cardano-wallet = import self.sources.cardano-wallet { inherit system; gitrev = self.sources.cardano-wallet.rev; crossSystem = crossSystem walletPkgs.lib; }; cardano-wallet-native = import self.sources.cardano-wallet { inherit system; gitrev = self.sources.cardano-wallet.rev; }; 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; cardano-node-cluster = let diff --git a/installers/common/MacInstaller.hs b/installers/common/MacInstaller.hs index 7851c083ff..af17638bd5 100644 --- a/installers/common/MacInstaller.hs +++ b/installers/common/MacInstaller.hs @@ -279,6 +279,7 @@ buildElectronApp darwinConfig@DarwinConfig{dcAppName, dcAppNameApp} installerCon , "cross-fetch" , "trezor-connect" , "js-chain-libs-node" + , "bignumber.js" ] mapM_ (\lib -> do cptree ("../node_modules" lib) ((fromText pathtoapp) "Contents/Resources/app/node_modules" lib) diff --git a/installers/nix/linux.nix b/installers/nix/linux.nix index ae477c3a14..7e243ddd75 100644 --- a/installers/nix/linux.nix +++ b/installers/nix/linux.nix @@ -6,6 +6,10 @@ , jormungandrLib , launcherConfigs , linuxClusterBinName +, gsettings-desktop-schemas +, gtk3 +, hicolor-icon-theme +, xfce }: let @@ -41,6 +45,7 @@ let export CLUSTER=${cluster'} export DAEDALUS_DIR="''${XDG_DATA_HOME}/Daedalus" export DAEDALUS_CONFIG=${if sandboxed then "/nix/var/nix/profiles/profile-${linuxClusterBinName}/etc" else daedalus-config} + export XDG_DATA_DIRS=${gsettings-desktop-schemas}/share/gsettings-schemas/${gsettings-desktop-schemas.name}:${gtk3}/share/gsettings-schemas/${gtk3.name}:${hicolor-icon-theme}/share:${xfce.xfce4-icon-theme}/share mkdir -p "''${DAEDALUS_DIR}/${cluster}/"{Logs/pub,Secrets} cd "''${DAEDALUS_DIR}/${cluster}/" diff --git a/nix/sources.json b/nix/sources.json index 2a1700070f..35e11f8451 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -24,17 +24,16 @@ "url_template": "https://github.com///archive/.tar.gz" }, "cardano-wallet": { - "branch": "tags/v2020-12-08", + "branch": "master", "description": "Official Wallet Backend & API for Cardano decentralized", "homepage": null, "owner": "input-output-hk", "repo": "cardano-wallet", - "rev": "4e49b1f12ae7d653eca149b60d67f2a1a578104a", - "sha256": "1ypkyn2s12nxwrk1ims8vrhhyy1xl3v5y35yxrwvqp8y8m2sac2x", + "rev": "a37c9856b4d96286e3f01a95026c63b986ebbba0", + "sha256": "1mg8n58j2mjqhhzjb4p5yp8z06b9arh40pagi9rddil2f3vxzihm", "type": "tarball", - "url": "https://github.com/input-output-hk/cardano-wallet/archive/4e49b1f12ae7d653eca149b60d67f2a1a578104a.tar.gz", - "url_template": "https://github.com///archive/.tar.gz", - "version": "v2021-01-12" + "url": "https://github.com/input-output-hk/cardano-wallet/archive/a37c9856b4d96286e3f01a95026c63b986ebbba0.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" }, "gitignore": { "branch": "master", @@ -61,15 +60,15 @@ "url_template": "https://github.com///archive/.tar.gz" }, "iohk-nix": { - "branch": "nixpkgs-bump", + "branch": "master", "description": "nix scripts shared across projects", "homepage": null, "owner": "input-output-hk", "repo": "iohk-nix", - "rev": "940f3b7c16b9e7d4493d400280eedf2c936aa5c0", - "sha256": "1m5c8abw3bi5ij99vbcb5bpjywljy72yig5bbm4158r1mck7hljd", + "rev": "4efc38924c64c23a582c84950c8c25f72ff049cc", + "sha256": "0nhwyrd0xc72yj5q3jqa2wl4khp4g7n72i45cxy2rgn9nrp8wqh0", "type": "tarball", - "url": "https://github.com/input-output-hk/iohk-nix/archive/940f3b7c16b9e7d4493d400280eedf2c936aa5c0.tar.gz", + "url": "https://github.com/input-output-hk/iohk-nix/archive/4efc38924c64c23a582c84950c8c25f72ff049cc.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, "js-chain-libs": { diff --git a/package.json b/package.json index fc3a49b1a6..da70ad2805 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "daedalus", "productName": "Daedalus", - "version": "3.2.0", + "version": "3.3.0", "description": "Cryptocurrency Wallet", "main": "./dist/main/index.js", "scripts": { @@ -173,8 +173,9 @@ }, "dependencies": { "@cardano-foundation/ledgerjs-hw-app-cardano": "2.1.0", + "@iohk-jormungandr/wallet-js": "0.5.0-pre7", "aes-js": "3.1.2", - "bignumber.js": "5.0.0", + "bignumber.js": "9.0.1", "bip39": "2.3.0", "blake2b": "2.1.3", "blakejs": "1.1.0", @@ -182,7 +183,7 @@ "bs58": "4.0.1", "cardano-crypto.js": "5.3.6-rc.6", "cardano-js": "0.4.5", - "cardano-launcher": "0.20201014.0", + "cardano-launcher": "0.20210215.0", "cbor": "5.0.2", "check-disk-space": "2.1.0", "chroma-js": "2.1.0", @@ -201,6 +202,7 @@ "history": "4.10.1", "humanize-duration": "3.23.1", "inquirer": "7.3.3", + "json-bigint": "1.0.0", "lodash": "4.17.20", "lodash-es": "4.17.15", "mime-types": "2.1.27", @@ -228,7 +230,7 @@ "react-intl": "2.7.2", "react-lottie": "1.2.3", "react-markdown": "4.3.1", - "react-polymorph": "0.9.7-rc.11", + "react-polymorph": "0.9.7-rc.17", "react-router": "5.2.0", "react-router-dom": "5.2.0", "react-svg-inline": "2.1.1", diff --git a/shell.nix b/shell.nix index 4bd9b769fc..1aaec9ff3b 100644 --- a/shell.nix +++ b/shell.nix @@ -51,6 +51,7 @@ let daedalusPkgs.daedalus-bridge daedalusPkgs.daedalus-installer daedalusPkgs.darwin-launcher + daedalusPkgs.mock-token-metadata-server ] ++ (with pkgs; [ nix bash binutils coreutils curl gnutar git python27 curl jq diff --git a/source/common/config/electron-store.config.js b/source/common/config/electron-store.config.js index 665d1e8150..633617002e 100644 --- a/source/common/config/electron-store.config.js +++ b/source/common/config/electron-store.config.js @@ -30,4 +30,7 @@ export const STORAGE_KEYS: { DOWNLOAD_MANAGER: 'DOWNLOAD-MANAGER', APP_AUTOMATIC_UPDATE_FAILED: 'APP-AUTOMATIC-UPDATE-FAILED', APP_UPDATE_COMPLETED: 'APP-UPDATE-COMPLETED', + CURRENCY_SELECTED: 'CURRENCY-SELECTED', + CURRENCY_ACTIVE: 'CURRENCY-ACTIVE', + SMASH_SERVER: 'SMASH-SERVER', }; diff --git a/source/common/ipc/api.js b/source/common/ipc/api.js index c03b899ac3..e1401eb494 100644 --- a/source/common/ipc/api.js +++ b/source/common/ipc/api.js @@ -11,6 +11,7 @@ import type { SaveFileDialogResponseParams, } from '../types/file-dialog.types'; import type { GenerateAddressPDFParams } from '../types/address-pdf-request.types'; +import type { GenerateVotingPDFParams } from '../types/voting-pdf-request.types'; import type { GenerateCsvParams } from '../types/csv-request.types'; import type { GenerateQRCodeParams } from '../types/save-qrCode.types'; import type { @@ -177,7 +178,10 @@ export type SubmitBugReportRequestMainResponse = void; * Channel to rebuild the electron application menu after the language setting changes */ export const REBUILD_APP_MENU_CHANNEL = 'REBUILD_APP_MENU_CHANNEL'; -export type RebuildAppMenuRendererRequest = { isUpdateAvailable: boolean }; +export type RebuildAppMenuRendererRequest = { + isUpdateAvailable: boolean, + isNavigationEnabled: boolean, +}; export type RebuildAppMenuMainResponse = void; /** @@ -201,6 +205,13 @@ export const GENERATE_ADDRESS_PDF_CHANNEL = 'GENERATE_ADDRESS_PDF_CHANNEL'; export type GenerateAddressPDFRendererRequest = GenerateAddressPDFParams; export type GenerateAddressPDFMainResponse = void; +/** + * Channel to generate and save a share voting PDF + */ +export const GENERATE_VOTING_PDF_CHANNEL = 'GENERATE_VOTING_PDF_CHANNEL'; +export type GenerateVotingPDFRendererRequest = GenerateVotingPDFParams; +export type GenerateVotingPDFMainResponse = void; + /** * Channel to generate and save a csv file */ @@ -295,6 +306,14 @@ export const GENERATE_WALLET_MIGRATION_REPORT_CHANNEL = export type GenerateWalletMigrationReportRendererRequest = WalletMigrationReportData; export type GenerateWalletMigrationReportMainResponse = void; +/** + * Channel for enabling application menu navigation + */ +export const ENABLE_APPLICATION_MENU_NAVIGATION_CHANNEL = + 'ENABLE_APPLICATION_MENU_NAVIGATION_CHANNEL'; +export type EnableApplicationMenuNavigationRendererRequest = void; +export type EnableApplicationMenuNavigationMainResponse = void; + /** * Channel for generating wallet migration report */ diff --git a/source/common/types/electron-store.types.js b/source/common/types/electron-store.types.js index 8315462143..09d9c5c13f 100644 --- a/source/common/types/electron-store.types.js +++ b/source/common/types/electron-store.types.js @@ -19,7 +19,10 @@ export type StorageKey = | 'WALLET-MIGRATION-STATUS' | 'DOWNLOAD-MANAGER' | 'APP-AUTOMATIC-UPDATE-FAILED' - | 'APP-UPDATE-COMPLETED'; + | 'APP-UPDATE-COMPLETED' + | 'CURRENCY-SELECTED' + | 'CURRENCY-ACTIVE' + | 'SMASH-SERVER'; export type StoreMessage = { type: StorageType, diff --git a/source/common/types/voting-pdf-request.types.js b/source/common/types/voting-pdf-request.types.js new file mode 100644 index 0000000000..ffe4f953b7 --- /dev/null +++ b/source/common/types/voting-pdf-request.types.js @@ -0,0 +1,14 @@ +// @flow +export type GenerateVotingPDFParams = { + title: string, + currentLocale: string, + creationDate: string, + qrCode: string, + walletNameLabel: string, + walletName: string, + isMainnet: boolean, + networkLabel: string, + networkName: string, + filePath: string, + author: string, +}; diff --git a/source/common/utils/logging.js b/source/common/utils/logging.js index c04d628f28..d9d61addd7 100644 --- a/source/common/utils/logging.js +++ b/source/common/utils/logging.js @@ -29,6 +29,9 @@ export const filterLogData = (data: Object): Object => { 'recoveryPhrase', 'passphrase', 'password', + 'votingKey', + 'stakeKey', + 'signature', 'accountPublicKey', 'extendedPublicKey', 'publicKeyHex', diff --git a/source/main/cardano/CardanoNode.js b/source/main/cardano/CardanoNode.js index f22d3d3dbe..643e6497da 100644 --- a/source/main/cardano/CardanoNode.js +++ b/source/main/cardano/CardanoNode.js @@ -81,7 +81,6 @@ export type CardanoNodeConfig = { syncTolerance: string, cliBin: string, // Path to cardano-cli executable isStaging: boolean, - smashUrl?: string, }; const CARDANO_UPDATE_EXIT_CODE = 20; @@ -297,7 +296,6 @@ export class CardanoNode { syncTolerance, cliBin, isStaging, - smashUrl, } = config; this._config = config; @@ -356,7 +354,6 @@ export class CardanoNode { walletLogFile, cliBin, isStaging, - smashUrl, }); this._node = node; diff --git a/source/main/cardano/CardanoWalletLauncher.js b/source/main/cardano/CardanoWalletLauncher.js index 7f8ab36bd9..eb07fb9e4e 100644 --- a/source/main/cardano/CardanoWalletLauncher.js +++ b/source/main/cardano/CardanoWalletLauncher.js @@ -7,7 +7,12 @@ import * as cardanoLauncher from 'cardano-launcher'; import type { Launcher } from 'cardano-launcher'; import type { NodeConfig } from '../config'; import { environment } from '../environment'; -import { STAKE_POOL_REGISTRY_URL } from '../config'; +import { + STAKE_POOL_REGISTRY_URL, + TOKEN_METADATA_SERVER_URL, + MOCK_TOKEN_METADATA_SERVER_URL, + MOCK_TOKEN_METADATA_SERVER_PORT, +} from '../config'; import { MAINNET, STAGING, @@ -38,7 +43,6 @@ export type WalletOpts = { walletLogFile: WriteStream, cliBin: string, isStaging: boolean, - smashUrl?: string, }; export async function CardanoWalletLauncher(walletOpts: WalletOpts): Launcher { @@ -57,7 +61,6 @@ export async function CardanoWalletLauncher(walletOpts: WalletOpts): Launcher { walletLogFile, cliBin, isStaging, - smashUrl, } = walletOpts; // TODO: Update launcher config to pass number const syncToleranceSeconds = parseInt(syncTolerance.replace('s', ''), 10); @@ -97,6 +100,8 @@ export async function CardanoWalletLauncher(walletOpts: WalletOpts): Launcher { await fs.copy('tls', tlsPath); } + let tokenMetadataServer; + // This switch statement handles any node specific // configuration, prior to spawning the child process logger.info('Node implementation', { nodeImplementation }); @@ -130,13 +135,16 @@ export async function CardanoWalletLauncher(walletOpts: WalletOpts): Launcher { launcherConfig.networkName = TESTNET; logger.info('Launching Wallet with --testnet flag'); } - if (smashUrl) { - logger.info('Launching Wallet with --pool-metadata-fetching flag', { - poolMetadataSource: { smashUrl }, - }); - merge(launcherConfig, { - poolMetadataSource: { smashUrl }, + if (MOCK_TOKEN_METADATA_SERVER_PORT) { + tokenMetadataServer = `${MOCK_TOKEN_METADATA_SERVER_URL}:${MOCK_TOKEN_METADATA_SERVER_PORT}/`; + } else if (cluster !== MAINNET) { + tokenMetadataServer = TOKEN_METADATA_SERVER_URL; + } + if (tokenMetadataServer) { + logger.info('Launching Wallet with --token-metadata-server flag', { + tokenMetadataServer, }); + merge(launcherConfig, { tokenMetadataServer }); } merge(launcherConfig, { nodeConfig, tlsConfiguration }); break; diff --git a/source/main/cardano/setup.js b/source/main/cardano/setup.js index 80ae28098b..cd8c770f4f 100644 --- a/source/main/cardano/setup.js +++ b/source/main/cardano/setup.js @@ -49,7 +49,6 @@ const startCardanoNode = ( syncTolerance, cliBin, isStaging, - smashUrl, } = launcherConfig; const logFilePath = `${logsPrefix}/pub/`; const config = { @@ -66,7 +65,6 @@ const startCardanoNode = ( syncTolerance, cliBin, isStaging, - smashUrl, startupTimeout: NODE_STARTUP_TIMEOUT, startupMaxRetries: NODE_STARTUP_MAX_RETRIES, shutdownTimeout: NODE_SHUTDOWN_TIMEOUT, diff --git a/source/main/cardano/utils.js b/source/main/cardano/utils.js index 4194f8e851..e983a51793 100644 --- a/source/main/cardano/utils.js +++ b/source/main/cardano/utils.js @@ -6,6 +6,7 @@ import { spawnSync } from 'child_process'; import { logger } from '../utils/logging'; import { getTranslation } from '../utils/getTranslation'; import ensureDirectoryExists from '../utils/ensureDirectoryExists'; +import { decodeKeystore } from '../utils/restoreKeystore'; import type { LauncherConfig } from '../config'; import type { ExportWalletsMainResponse } from '../../common/ipc/api'; import type { @@ -19,7 +20,6 @@ import { CardanoProcessNameOptions, CardanoNodeImplementationOptions, NetworkNameOptions, - TESTNET_MAGIC, } from '../../common/types/cardano-node.types'; export type Process = { @@ -176,7 +176,6 @@ export const exportWallets = async ( locale: string ): Promise => { const { - exportWalletsBin, legacySecretKey, legacyWalletDB, stateDir, @@ -186,7 +185,6 @@ export const exportWallets = async ( logger.info('ipcMain: Starting wallets export...', { exportSourcePath, - exportWalletsBin, legacySecretKey, legacyWalletDB, stateDir, @@ -226,41 +224,38 @@ export const exportWallets = async ( } } - // Export tool flags - const exportWalletsBinFlags = []; - - // Cluster flags - if (cluster === 'testnet') { - exportWalletsBinFlags.push('--testnet', TESTNET_MAGIC.toString()); - } else { - exportWalletsBinFlags.push('--mainnet'); - } - - // Secret key flags - exportWalletsBinFlags.push('--keyfile', legacySecretKeyPath); - - // Wallet DB flags const legacyWalletDBPathExists = await fs.pathExists( `${legacyWalletDBPath}-acid` ); - if (legacyWalletDBPathExists) { - exportWalletsBinFlags.push('--wallet-db-path', legacyWalletDBPath); - } logger.info('ipcMain: Exporting wallets...', { - exportWalletsBin, - exportWalletsBinFlags, + legacySecretKeyPath, + legacyWalletDBPath, + legacyWalletDBPathExists, }); - const { stdout, stderr } = spawnSync(exportWalletsBin, exportWalletsBinFlags); - const wallets = JSON.parse(stdout.toString() || '[]'); - const errors = stderr.toString(); + let wallets = []; + let errors = ''; + try { + const legacySecretKeyFile = fs.readFileSync(legacySecretKeyPath); + // $FlowFixMe + const rawWallets = await decodeKeystore(legacySecretKeyFile); + wallets = rawWallets.map((w) => ({ + name: null, + id: w.walletId, + isEmptyPassphrase: w.isEmptyPassphrase, + passphrase_hash: w.passphraseHash.toString('hex'), + encrypted_root_private_key: w.encryptedPayload.toString('hex'), + })); + } catch (error) { + errors = error.toString(); + } logger.info(`ipcMain: Exported ${wallets.length} wallets`, { walletsData: wallets.map((w) => ({ name: w.name, id: w.id, - hasPassword: w.is_passphrase_empty, + hasPassword: !w.isEmptyPassphrase, })), errors, }); diff --git a/source/main/config.js b/source/main/config.js index 23631d0eb4..254276c62a 100644 --- a/source/main/config.js +++ b/source/main/config.js @@ -69,7 +69,6 @@ export type LauncherConfig = { configPath: string, syncTolerance: string, cliBin: string, - exportWalletsBin: string, legacyStateDir: string, legacySecretKey: string, legacyWalletDB: string, @@ -121,6 +120,7 @@ export const { legacyStateDir, logsPrefix, isFlight, + smashUrl, } = launcherConfig; export const appLogsFolderPath = logsPrefix; export const pubLogsFolderPath = path.join(appLogsFolderPath, 'pub'); @@ -144,8 +144,10 @@ export const ALLOWED_LOGS = [ 'node.log', ]; export const ALLOWED_NODE_LOGS = new RegExp(/(node.log-)(\d{14}$)/); +export const ALLOWED_WALLET_LOGS = new RegExp(/(cardano-wallet.log-)(\d{14}$)/); export const ALLOWED_LAUNCHER_LOGS = new RegExp(/(launcher-)(\d{14}$)/); export const MAX_NODE_LOGS_ALLOWED = 3; +export const MAX_WALLET_LOGS_ALLOWED = 3; export const MAX_LAUNCHER_LOGS_ALLOWED = 3; // CardanoNode config @@ -173,3 +175,12 @@ export const STAKE_POOL_REGISTRY_URL = { qa: 'https://explorer.qa.jormungandr-testnet.iohkdev.io/stakepool-registry/registry.zip', }; + +// Used for all non mainnet networks +export const TOKEN_METADATA_SERVER_URL = + 'https://metadata.cardano-testnet.iohkdev.io/'; + +// Used by mock-token-metadata-server +export const MOCK_TOKEN_METADATA_SERVER_URL = 'http://localhost'; +export const MOCK_TOKEN_METADATA_SERVER_PORT = + process.env.MOCK_TOKEN_METADATA_SERVER_PORT || 0; diff --git a/source/main/environment.js b/source/main/environment.js index f0497432a9..9fc7e0e576 100644 --- a/source/main/environment.js +++ b/source/main/environment.js @@ -60,7 +60,7 @@ const isIncentivizedTestnetSelfnode = checkIsIncentivizedTestnetSelfnode( const isDevelopment = checkIsDevelopment(NETWORK); const isWatchMode = process.env.IS_WATCH_MODE; const API_VERSION = process.env.API_VERSION || 'dev'; -const NODE_VERSION = '1.24.2'; // TODO: pick up this value from process.env +const NODE_VERSION = '1.25.1'; // 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/index.js b/source/main/index.js index 0edc4a8680..6826650256 100644 --- a/source/main/index.js +++ b/source/main/index.js @@ -37,6 +37,7 @@ import type { CheckDiskSpaceResponse } from '../common/types/no-disk-space.types import { logUsedVersion } from './utils/logUsedVersion'; import { setStateSnapshotLogChannel } from './ipc/set-log-state-snapshot'; import { generateWalletMigrationReportChannel } from './ipc/generateWalletMigrationReportChannel'; +import { enableApplicationMenuNavigationChannel } from './ipc/enableApplicationMenuNavigationChannel'; import { pauseActiveDownloads } from './ipc/downloadManagerChannel'; // import { isHardwareWalletSupportEnabled, isLedgerEnabled } from '../renderer/app/config/hardwareWalletsConfig'; @@ -202,7 +203,21 @@ const onAppReady = async () => { await safeExit(); }); - buildAppMenus(mainWindow, cardanoNode, locale, { isUpdateAvailable: false }); + buildAppMenus(mainWindow, cardanoNode, locale, { + isUpdateAvailable: false, + isNavigationEnabled: false, + }); + + await enableApplicationMenuNavigationChannel.onReceive( + () => + new Promise((resolve) => { + buildAppMenus(mainWindow, cardanoNode, locale, { + isUpdateAvailable: false, + isNavigationEnabled: true, + }); + resolve(); + }) + ); await rebuildApplicationMenu.onReceive( (data) => @@ -210,6 +225,7 @@ const onAppReady = async () => { locale = getLocale(network); buildAppMenus(mainWindow, cardanoNode, locale, { isUpdateAvailable: data.isUpdateAvailable, + isNavigationEnabled: data.isNavigationEnabled, }); mainWindow.updateTitle(locale); resolve(); diff --git a/source/main/ipc/enableApplicationMenuNavigationChannel.js b/source/main/ipc/enableApplicationMenuNavigationChannel.js new file mode 100644 index 0000000000..4bf790e972 --- /dev/null +++ b/source/main/ipc/enableApplicationMenuNavigationChannel.js @@ -0,0 +1,13 @@ +// @flow +import { MainIpcChannel } from './lib/MainIpcChannel'; +import { ENABLE_APPLICATION_MENU_NAVIGATION_CHANNEL } from '../../common/ipc/api'; +import type { + EnableApplicationMenuNavigationMainResponse, + EnableApplicationMenuNavigationRendererRequest, +} from '../../common/ipc/api'; + +export const enableApplicationMenuNavigationChannel: // IpcChannel +MainIpcChannel< + EnableApplicationMenuNavigationRendererRequest, + EnableApplicationMenuNavigationMainResponse +> = new MainIpcChannel(ENABLE_APPLICATION_MENU_NAVIGATION_CHANNEL); diff --git a/source/main/ipc/generateVotingPDFChannel.js b/source/main/ipc/generateVotingPDFChannel.js new file mode 100644 index 0000000000..62b9040011 --- /dev/null +++ b/source/main/ipc/generateVotingPDFChannel.js @@ -0,0 +1,142 @@ +// @flow +import fs from 'fs'; +import path from 'path'; +import PDFDocument from 'pdfkit'; +import qr from 'qr-image'; +import { MainIpcChannel } from './lib/MainIpcChannel'; +import { GENERATE_VOTING_PDF_CHANNEL } from '../../common/ipc/api'; +import type { + GenerateVotingPDFRendererRequest, + GenerateVotingPDFMainResponse, +} from '../../common/ipc/api'; +import fontRegularEn from '../../common/assets/pdf/NotoSans-Regular.ttf'; +import fontMediumEn from '../../common/assets/pdf/NotoSans-Medium.ttf'; +import fontUnicode from '../../common/assets/pdf/arial-unicode.ttf'; + +export const generateVotingPDFChannel: // IpcChannel +MainIpcChannel< + GenerateVotingPDFRendererRequest, + GenerateVotingPDFMainResponse +> = new MainIpcChannel(GENERATE_VOTING_PDF_CHANNEL); + +export const handleVotingPDFRequests = () => { + generateVotingPDFChannel.onReceive( + (request: GenerateVotingPDFRendererRequest) => + new Promise((resolve, reject) => { + // Prepare params + const { + title, + currentLocale, + creationDate, + qrCode, + walletNameLabel, + walletName, + isMainnet, + networkLabel, + networkName, + filePath, + author, + } = request; + + const readAssetSync = (p) => fs.readFileSync(path.join(__dirname, p)); + let fontRegular; + let fontMedium; + + if (currentLocale === 'ja-JP') { + fontRegular = fontUnicode; + fontMedium = fontUnicode; + } else { + fontRegular = fontRegularEn; + fontMedium = fontMediumEn; + } + + // Generate QR image for wallet voting + const qrCodeImage = qr.imageSync(qrCode, { + type: 'png', + size: 10, + ec_level: 'L', + margin: 0, + }); + + try { + const fontBufferMedium = readAssetSync(fontMedium); + const fontBufferRegular = readAssetSync(fontRegular); + + const textColor = '#5e6066'; + const textColorRed = '#ea4c5b'; + const width = 640; + const height = 450; + const doc = new PDFDocument({ + size: [width, height], + margins: { + bottom: 20, + left: 30, + right: 30, + top: 20, + }, + info: { + Title: title, + Author: author, + }, + }).fillColor(textColor); + + // Title + doc.font(fontBufferMedium).fontSize(18).text(title.toUpperCase(), { + align: 'center', + characterSpacing: 2, + }); + + // Creation date + doc + .font(fontBufferRegular) + .fontSize(12) + .text(creationDate.toUpperCase(), { + align: 'center', + characterSpacing: 0.6, + }); + + doc.moveDown(); + + // QR Code + doc.image(qrCodeImage, { + fit: [width - 60, 192], + align: 'center', + }); + + doc.moveDown(); + + // Wallet name + doc.font(fontBufferMedium).fontSize(14).text(walletNameLabel, { + align: 'center', + characterSpacing: 0.6, + }); + doc.font(fontBufferRegular).text(walletName, { + align: 'center', + characterSpacing: 0.6, + }); + + doc.moveDown(); + + // Footer + if (!isMainnet) { + doc + .fontSize(12) + .font(fontBufferMedium) + .fillColor(textColorRed) + .text(`${networkLabel} ${networkName}`, { + align: 'center', + }); + } + + // Write file to disk + const writeStream = fs.createWriteStream(filePath); + doc.pipe(writeStream); + doc.end(); + writeStream.on('close', resolve); + writeStream.on('error', reject); + } catch (error) { + reject(error); + } + }) + ); +}; diff --git a/source/main/ipc/get-logs.js b/source/main/ipc/get-logs.js index f52f6c1628..fd69be8969 100644 --- a/source/main/ipc/get-logs.js +++ b/source/main/ipc/get-logs.js @@ -4,10 +4,12 @@ import fs from 'fs'; import path from 'path'; import { pubLogsFolderPath, - MAX_NODE_LOGS_ALLOWED, ALLOWED_LOGS, ALLOWED_NODE_LOGS, + ALLOWED_WALLET_LOGS, ALLOWED_LAUNCHER_LOGS, + MAX_NODE_LOGS_ALLOWED, + MAX_WALLET_LOGS_ALLOWED, MAX_LAUNCHER_LOGS_ALLOWED, } from '../config'; import { MainIpcChannel } from './lib/MainIpcChannel'; @@ -50,6 +52,10 @@ const isFileAllowed = (fileName: string) => const isFileNodeLog = (fileName: string, nodeLogsIncluded: number) => ALLOWED_NODE_LOGS.test(fileName) && nodeLogsIncluded < MAX_NODE_LOGS_ALLOWED; +const isFileWalletLog = (fileName: string, walletLogsIncluded: number) => + ALLOWED_WALLET_LOGS.test(fileName) && + walletLogsIncluded < MAX_WALLET_LOGS_ALLOWED; + const isFileLauncherLog = (fileName: string, nodeLogsIncluded: number) => ALLOWED_LAUNCHER_LOGS.test(fileName) && nodeLogsIncluded < MAX_LAUNCHER_LOGS_ALLOWED; @@ -62,6 +68,7 @@ export default () => { const files = fs.readdirSync(pubLogsFolderPath).sort().reverse(); let nodeLogsIncluded = 0; + let walletLogsIncluded = 0; let launcherLogsIncluded = 0; for (let i = 0; i < files.length; i++) { const currentFile = path.join(pubLogsFolderPath, files[i]); @@ -72,6 +79,9 @@ export default () => { } else if (isFileNodeLog(fileName, nodeLogsIncluded)) { logFiles.push(fileName); nodeLogsIncluded++; + } else if (isFileWalletLog(fileName, walletLogsIncluded)) { + logFiles.push(fileName); + walletLogsIncluded++; } else if (isFileLauncherLog(fileName, launcherLogsIncluded)) { logFiles.push(fileName); launcherLogsIncluded++; diff --git a/source/main/ipc/index.js b/source/main/ipc/index.js index e73376a6ef..f638aaca28 100644 --- a/source/main/ipc/index.js +++ b/source/main/ipc/index.js @@ -14,6 +14,7 @@ import { handleBugReportRequests } from './bugReportRequestChannel'; import { handleFileMetaRequests } from './generateFileMetaChannel'; import { handlePaperWalletRequests } from './generatePaperWalletChannel'; import { handleAddressPDFRequests } from './generateAddressPDFChannel'; +import { handleVotingPDFRequests } from './generateVotingPDFChannel'; import { saveQRCodeImageRequests } from './saveQRCodeImageChannel'; import { handleRewardsCsvRequests } from './generateCsvChannel'; import { handleFileDialogRequests } from './show-file-dialog-channels'; @@ -33,6 +34,7 @@ export default (window: BrowserWindow) => { handleFileMetaRequests(); handlePaperWalletRequests(); handleAddressPDFRequests(); + handleVotingPDFRequests(); saveQRCodeImageRequests(); handleRewardsCsvRequests(); handleFileDialogRequests(window); diff --git a/source/main/menus/osx.js b/source/main/menus/osx.js index 6817e8b3f6..db2a1d554c 100644 --- a/source/main/menus/osx.js +++ b/source/main/menus/osx.js @@ -19,6 +19,7 @@ export const osxMenu = ( translations: {}, locale: string, isUpdateAvailable: boolean, + isNavigationEnabled: boolean, translation: Function = getTranslation(translations, id) ) => [ { @@ -29,7 +30,7 @@ export const osxMenu = ( click() { actions.openAboutDialog(); }, - enabled: !isUpdateAvailable, + enabled: !isUpdateAvailable && isNavigationEnabled, }, { type: 'separator' }, { @@ -38,7 +39,7 @@ export const osxMenu = ( click() { actions.openSettingsPage(); }, - enabled: !isUpdateAvailable, + enabled: !isUpdateAvailable && isNavigationEnabled, }, { label: translation('daedalus.walletSettings'), @@ -46,7 +47,7 @@ export const osxMenu = ( click() { actions.openWalletSettingsPage(); }, - enabled: !isUpdateAvailable, + enabled: !isUpdateAvailable && isNavigationEnabled, }, { type: 'separator' }, { @@ -190,7 +191,7 @@ export const osxMenu = ( click() { actions.openDaedalusDiagnosticsDialog(); }, - enabled: !isUpdateAvailable, + enabled: !isUpdateAvailable && isNavigationEnabled, }, ]), }, diff --git a/source/main/menus/win-linux.js b/source/main/menus/win-linux.js index e6954eeda2..ebbca67db5 100644 --- a/source/main/menus/win-linux.js +++ b/source/main/menus/win-linux.js @@ -19,6 +19,7 @@ export const winLinuxMenu = ( translations: {}, locale: string, isUpdateAvailable: boolean, + isNavigationEnabled: boolean, translation: Function = getTranslation(translations, id) ) => [ { @@ -29,7 +30,7 @@ export const winLinuxMenu = ( click() { actions.openAboutDialog(); }, - enabled: !isUpdateAvailable, + enabled: !isUpdateAvailable && isNavigationEnabled, }, { label: translation('daedalus.close'), @@ -97,7 +98,7 @@ export const winLinuxMenu = ( click() { actions.openSettingsPage(); }, - enabled: !isUpdateAvailable, + enabled: !isUpdateAvailable && isNavigationEnabled, }, { label: translation('daedalus.walletSettings'), @@ -105,7 +106,7 @@ export const winLinuxMenu = ( click() { actions.openWalletSettingsPage(); }, - enabled: !isUpdateAvailable, + enabled: !isUpdateAvailable && isNavigationEnabled, }, { type: 'separator', @@ -201,7 +202,7 @@ export const winLinuxMenu = ( click() { actions.openDaedalusDiagnosticsDialog(); }, - enabled: !isUpdateAvailable, + enabled: !isUpdateAvailable && isNavigationEnabled, }, ]), }, diff --git a/source/main/preload.js b/source/main/preload.js index af13cf98af..a3777e4bf7 100644 --- a/source/main/preload.js +++ b/source/main/preload.js @@ -11,6 +11,7 @@ import { legacyStateDir, nodeImplementation, isFlight, + smashUrl, } from './config'; import { SHELLEY_LOCAL, @@ -58,7 +59,9 @@ process.once('loaded', () => { isIncentivizedTestnet: _isIncentivizedTestnet, isFlight, legacyStateDir, + smashUrl, }); + // Expose require for Spectron! if (_process.env.NODE_ENV === 'test') { // $FlowFixMe diff --git a/source/main/utils/buildAppMenus.js b/source/main/utils/buildAppMenus.js index 055477e4ca..8b7a23ce9a 100644 --- a/source/main/utils/buildAppMenus.js +++ b/source/main/utils/buildAppMenus.js @@ -16,11 +16,12 @@ export const buildAppMenus = async ( locale: string, data: { isUpdateAvailable: boolean, + isNavigationEnabled: boolean, } ) => { const { ABOUT, DAEDALUS_DIAGNOSTICS } = DIALOGS; const { SETTINGS, WALLET_SETTINGS } = PAGES; - const { isUpdateAvailable } = data; + const { isUpdateAvailable, isNavigationEnabled } = data; const { isMacOS, isBlankScreenFixActive } = environment; const translations = require(`../locales/${locale}`); @@ -106,7 +107,8 @@ export const buildAppMenus = async ( menuActions, translations, locale, - isUpdateAvailable + isUpdateAvailable, + isNavigationEnabled ) ); Menu.setApplicationMenu(menu); @@ -118,7 +120,8 @@ export const buildAppMenus = async ( menuActions, translations, locale, - isUpdateAvailable + isUpdateAvailable, + isNavigationEnabled ) ); mainWindow.setMenu(menu); diff --git a/source/main/utils/restoreKeystore.js b/source/main/utils/restoreKeystore.js new file mode 100644 index 0000000000..0d8461a35b --- /dev/null +++ b/source/main/utils/restoreKeystore.js @@ -0,0 +1,52 @@ +// @flow +import * as cbor from 'cbor'; +import * as blake2b from 'blake2b'; +import * as crypto from 'crypto'; + +export type EncryptedSecretKeys = Array; + +export type EncryptedSecretKey = { + encryptedPayload: Buffer, + passphraseHash: Buffer, + isEmptyPassphrase: boolean, + walletId: WalletId, +}; + +export type WalletId = string; + +export const decodeKeystore = async ( + bytes: Buffer +): Promise => { + return cbor + .decodeAll(bytes) + .then((obj) => obj[0][2].map(toEncryptedSecretKey)); +}; + +const toEncryptedSecretKey = ([encryptedPayload, passphraseHash]: [ + Buffer, + Buffer +]): EncryptedSecretKey => { + const isEmptyPassphrase = $isEmptyPassphrase(passphraseHash); + return { + walletId: mkWalletId(encryptedPayload), + encryptedPayload, + passphraseHash, + isEmptyPassphrase, + }; +}; + +const mkWalletId = (xprv: Buffer): WalletId => { + const xpub = xprv.slice(64); + return blake2b(20).update(xpub).digest('hex'); +}; + +const $isEmptyPassphrase = (pwd: Buffer): boolean => { + const cborEmptyBytes = Buffer.from('40', 'hex'); + const [logN, r, p, salt, hashA] = pwd.toString('utf8').split('|'); + const opts = { N: 2 ** Number(logN), r: Number(r), p: Number(p) }; + // $FlowFixMe + const hashB = crypto + .scryptSync(cborEmptyBytes, Buffer.from(salt, 'base64'), 32, opts) + .toString('base64'); + return hashA === hashB; +}; diff --git a/source/main/webpack.config.js b/source/main/webpack.config.js index d87734f976..d63c68a538 100644 --- a/source/main/webpack.config.js +++ b/source/main/webpack.config.js @@ -72,6 +72,8 @@ module.exports = { 'process.env.NETWORK': JSON.stringify( process.env.NETWORK || 'development' ), + 'process.env.MOCK_TOKEN_METADATA_SERVER_PORT': + process.env.MOCK_TOKEN_METADATA_SERVER_PORT || 0, 'process.env.MOBX_DEV_TOOLS': process.env.MOBX_DEV_TOOLS || 0, 'process.env.BUILD_NUMBER': JSON.stringify( process.env.BUILD_NUMBER || 'dev' diff --git a/source/main/windows/main.js b/source/main/windows/main.js index 24a13fe40b..676abb21e9 100644 --- a/source/main/windows/main.js +++ b/source/main/windows/main.js @@ -130,7 +130,7 @@ export const createMainWindow = (locale: string) => { }); window.webContents.on('did-finish-load', () => { - if (isTest) { + if (isTest || isDev) { window.showInactive(); // show without focusing the window } else { window.show(); // show also focuses the window diff --git a/source/renderer/app/Routes.js b/source/renderer/app/Routes.js index c82d059099..9e519ec3b5 100644 --- a/source/renderer/app/Routes.js +++ b/source/renderer/app/Routes.js @@ -8,6 +8,8 @@ import Root from './containers/Root'; import InitialSettingsPage from './containers/profile/InitialSettingsPage'; import Settings from './containers/settings/Settings'; import GeneralSettingsPage from './containers/settings/categories/GeneralSettingsPage'; +import WalletsSettingsPage from './containers/settings/categories/WalletsSettingsPage'; +import StakePoolsSettingsPage from './containers/settings/categories/StakePoolsSettingsPage'; import SupportSettingsPage from './containers/settings/categories/SupportSettingsPage'; import TermsOfUseSettingsPage from './containers/settings/categories/TermsOfUseSettingsPage'; import TermsOfUsePage from './containers/profile/TermsOfUsePage'; @@ -30,6 +32,7 @@ import WalletReceivePage from './containers/wallet/WalletReceivePage'; import WalletTransactionsPage from './containers/wallet/WalletTransactionsPage'; import WalletSettingsPage from './containers/wallet/WalletSettingsPage'; import WalletUtxoPage from './containers/wallet/WalletUtxoPage'; +import VotingRegistrationPage from './containers/voting/VotingRegistrationPage'; export const Routes = withRouter(() => ( @@ -88,6 +91,14 @@ export const Routes = withRouter(() => ( path={ROUTES.SETTINGS.GENERAL} component={GeneralSettingsPage} /> + + ( component={RedeemItnRewardsContainer} /> + diff --git a/source/renderer/app/actions/index.js b/source/renderer/app/actions/index.js index dc3fc7ce2c..a2d6649434 100644 --- a/source/renderer/app/actions/index.js +++ b/source/renderer/app/actions/index.js @@ -1,16 +1,17 @@ // @flow import AddressesActions from './addresses-actions'; import AppActions from './app-actions'; +import AppUpdateActions from './app-update-actions'; import DialogsActions from './dialogs-actions'; import HardwareWalletsActions from './hardware-wallets-actions'; import NetworkStatusActions from './network-status-actions'; -import AppUpdateActions from './app-update-actions'; import NotificationsActions from './notifications-actions'; import ProfileActions from './profile-actions'; import RouterActions from './router-actions'; import SidebarActions from './sidebar-actions'; import StakingActions from './staking-actions'; import TransactionsActions from './transactions-actions'; +import VotingActions from './voting-actions'; import WalletsActions from './wallets-actions'; import WalletsLocalAction from './wallets-local-actions'; import WalletBackupActions from './wallet-backup-actions'; @@ -21,16 +22,17 @@ import WindowActions from './window-actions'; export type ActionsMap = { addresses: AddressesActions, app: AppActions, + appUpdate: AppUpdateActions, dialogs: DialogsActions, hardwareWallets: HardwareWalletsActions, networkStatus: NetworkStatusActions, - appUpdate: AppUpdateActions, notifications: NotificationsActions, profile: ProfileActions, router: RouterActions, sidebar: SidebarActions, staking: StakingActions, transactions: TransactionsActions, + voting: VotingActions, wallets: WalletsActions, walletsLocal: WalletsLocalAction, walletBackup: WalletBackupActions, @@ -42,16 +44,17 @@ export type ActionsMap = { const actionsMap: ActionsMap = { addresses: new AddressesActions(), app: new AppActions(), + appUpdate: new AppUpdateActions(), dialogs: new DialogsActions(), hardwareWallets: new HardwareWalletsActions(), networkStatus: new NetworkStatusActions(), - appUpdate: new AppUpdateActions(), notifications: new NotificationsActions(), profile: new ProfileActions(), router: new RouterActions(), sidebar: new SidebarActions(), staking: new StakingActions(), transactions: new TransactionsActions(), + voting: new VotingActions(), wallets: new WalletsActions(), walletsLocal: new WalletsLocalAction(), walletBackup: new WalletBackupActions(), diff --git a/source/renderer/app/actions/staking-actions.js b/source/renderer/app/actions/staking-actions.js index d69eb1610e..f31faa723b 100644 --- a/source/renderer/app/actions/staking-actions.js +++ b/source/renderer/app/actions/staking-actions.js @@ -21,6 +21,8 @@ export default class StakingActions { filenamePrefix: string, }> = new Action(); requestCSVFileSuccess: Action = new Action(); + selectSmashServerUrl: Action<{ smashServerUrl: string }> = new Action(); + resetSmashServerError: Action = new Action(); /* ---------- Redeem ITN Rewards ---------- */ onRedeemStart: Action = new Action(); onConfigurationContinue: Action = new Action(); diff --git a/source/renderer/app/actions/voting-actions.js b/source/renderer/app/actions/voting-actions.js new file mode 100644 index 0000000000..09d5cb5147 --- /dev/null +++ b/source/renderer/app/actions/voting-actions.js @@ -0,0 +1,18 @@ +// @flow +import Action from './lib/Action'; + +export default class VotingActions { + selectWallet: Action = new Action(); + sendTransaction: Action<{ + amount: number, + passphrase: string, + }> = new Action(); + generateQrCode: Action = new Action(); + saveAsPDF: Action = new Action(); + saveAsPDFSuccess: Action = new Action(); + nextRegistrationStep: Action = new Action(); + previousRegistrationStep: Action = new Action(); + resetRegistration: Action = new Action(); + showConfirmationDialog: Action = new Action(); + closeConfirmationDialog: Action = new Action(); +} diff --git a/source/renderer/app/actions/wallets-actions.js b/source/renderer/app/actions/wallets-actions.js index 3c91a00dd0..f31b5d9598 100644 --- a/source/renderer/app/actions/wallets-actions.js +++ b/source/renderer/app/actions/wallets-actions.js @@ -84,6 +84,8 @@ export default class WalletsActions { setCertificateTemplate: Action<{ selectedTemplate: string }> = new Action(); finishCertificate: Action = new Action(); finishRewardsCsv: Action = new Action(); + setCurrencySelected: Action<{ currencySymbol: string }> = new Action(); + toggleCurrencyIsActive: Action = new Action(); /* ---------- Transfer Funds ---------- */ transferFundsNextStep: Action = new Action(); diff --git a/source/renderer/app/api/api.js b/source/renderer/app/api/api.js index 4c5b0328ee..8eb19aa137 100644 --- a/source/renderer/app/api/api.js +++ b/source/renderer/app/api/api.js @@ -31,6 +31,7 @@ import { getNetworkParameters } from './network/requests/getNetworkParameters'; // Transactions requests import { getTransactionFee } from './transactions/requests/getTransactionFee'; import { getByronWalletTransactionFee } from './transactions/requests/getByronWalletTransactionFee'; +import { getTransaction } from './transactions/requests/getTransaction'; import { getTransactionHistory } from './transactions/requests/getTransactionHistory'; import { getLegacyWalletTransactionHistory } from './transactions/requests/getLegacyWalletTransactionHistory'; import { getWithdrawalHistory } from './transactions/requests/getWithdrawalHistory'; @@ -41,6 +42,9 @@ import { selectCoins } from './transactions/requests/selectCoins'; import { createExternalTransaction } from './transactions/requests/createExternalTransaction'; import { getPublicKey } from './transactions/requests/getPublicKey'; +// Voting requests +import { createWalletSignature } from './voting/requests/createWalletSignature'; + // Wallets requests import { updateSpendingPassword } from './wallets/requests/updateSpendingPassword'; import { updateByronSpendingPassword } from './wallets/requests/updateByronSpendingPassword'; @@ -66,6 +70,8 @@ import { getLegacyWallet } from './wallets/requests/getLegacyWallet'; import { transferFundsCalculateFee } from './wallets/requests/transferFundsCalculateFee'; import { transferFunds } from './wallets/requests/transferFunds'; import { createHardwareWallet } from './wallets/requests/createHardwareWallet'; +import { getCurrencyList } from './wallets/requests/getCurrencyList'; +import { getCurrencyRate } from './wallets/requests/getCurrencyRate'; // Staking import StakePool from '../domains/StakePool'; @@ -78,6 +84,9 @@ import { getStakePools } from './staking/requests/getStakePools'; import { getDelegationFee } from './staking/requests/getDelegationFee'; import { joinStakePool } from './staking/requests/joinStakePool'; import { quitStakePool } from './staking/requests/quitStakePool'; +import { getSmashSettings } from './staking/requests/getSmashSettings'; +import { checkSmashServerHealth } from './staking/requests/checkSmashServerHealth'; +import { updateSmashSettings } from './staking/requests/updateSmashSettings'; // Utility functions import { cardanoFaultInjectionChannel } from '../ipc/cardano.ipc'; @@ -95,7 +104,8 @@ import { filterLogData } from '../../../common/utils/logging'; // Config constants import { LOVELACES_PER_ADA } from '../config/numbersConfig'; import { - DELEGATION_DEPOSIT, + SMASH_SERVER_STATUSES, + SMASH_SERVERS_LIST, MIN_REWARDS_REDEMPTION_RECEIVER_BALANCE, REWARDS_REDEMPTION_FEE_CALCULATION_AMOUNT, } from '../config/stakingConfig'; @@ -103,6 +113,7 @@ import { ADA_CERTIFICATE_MNEMONIC_LENGTH, WALLET_RECOVERY_PHRASE_WORD_COUNT, } from '../config/cryptoConfig'; +import { currencyConfig } from '../config/currencyConfig'; // Addresses Types import type { @@ -133,6 +144,7 @@ import type { GetTransactionFeeRequest, CreateTransactionRequest, DeleteTransactionRequest, + GetTransactionRequest, GetTransactionsRequest, GetTransactionsResponse, CoinSelectionsPaymentRequestType, @@ -172,6 +184,9 @@ import type { TransferFundsRequest, TransferFundsResponse, UpdateWalletRequest, + GetCurrencyListResponse, + GetCurrencyRateRequest, + GetCurrencyRateResponse, } from './wallets/types'; import type { WalletProps } from '../domains/Wallet'; @@ -182,6 +197,7 @@ import type { GetNewsResponse } from './news/types'; import type { JoinStakePoolRequest, GetDelegationFeeRequest, + DelegationCalculateFeeResponse, AdaApiStakePools, AdaApiStakePool, QuitStakePoolRequest, @@ -189,7 +205,17 @@ import type { GetRedeemItnRewardsFeeResponse, RequestRedeemItnRewardsRequest, RequestRedeemItnRewardsResponse, + GetSmashSettingsApiResponse, + CheckSmashServerHealthApiResponse, + PoolMetadataSource, } from './staking/types'; + +// Voting Types +import type { + CreateVotingRegistrationRequest, + CreateWalletSignatureRequest, +} from './voting/types'; + import type { StakePoolProps } from '../domains/StakePool'; import type { FaultInjectionIpcRequest } from '../../../common/types/cardano-node.types'; @@ -304,7 +330,7 @@ export default class AdaApi { }); try { const { walletId, role, index } = request; - const walletPublicKey = await getWalletPublicKey(this.config, { + const walletPublicKey: string = await getWalletPublicKey(this.config, { walletId, role, index, @@ -313,10 +339,7 @@ export default class AdaApi { return walletPublicKey; } catch (error) { logger.error('AdaApi::getWalletPublicKey error', { error }); - // @TODO: Uncomment this when api is ready - // throw new ApiError(error); - // @TODO: Delete this when api is ready - return '8edd9c9b73873ce8826cbe3e2e08534d35f1ba64cc94c063c0525865aa28e35527be51bb72ee9983d173f5617493bc6804a6750b359538c79cd5b43ccbbd48e5'; + throw new ApiError(error); } }; @@ -348,6 +371,26 @@ export default class AdaApi { } }; + getTransaction = async ( + request: GetTransactionRequest + ): Promise => { + logger.debug('AdaApi::getTransaction called', { parameters: request }); + const { walletId, transactionId } = request; + + try { + const response = await getTransaction( + this.config, + walletId, + transactionId + ); + logger.debug('AdaApi::getTransaction success', { response }); + return _createTransactionFromServerData(response); + } catch (error) { + logger.error('AdaApi::getTransaction error', { error }); + throw new ApiError(error); + } + }; + getTransactions = async ( request: GetTransactionsRequest ): Promise => { @@ -383,9 +426,7 @@ export default class AdaApi { const transactions = response.map((tx) => _createTransactionFromServerData(tx) ); - return new Promise((resolve) => - resolve({ transactions, total: response.length }) - ); + return Promise.resolve({ transactions, total: response.length }); } catch (error) { logger.error('AdaApi::getTransactions error', { error }); throw new ApiError(error); @@ -562,7 +603,7 @@ export default class AdaApi { const withdrawal = new BigNumber(w.amount.quantity).dividedBy( LOVELACES_PER_ADA ); - withdrawals = withdrawals.add(withdrawal); + withdrawals = withdrawals.plus(withdrawal); }); }); return { withdrawals }; @@ -883,7 +924,6 @@ export default class AdaApi { parameters: filterLogData(request), }); const { walletId, payments, delegation } = request; - try { let data; if (delegation) { @@ -917,13 +957,15 @@ export default class AdaApi { const outputs = concat(response.outputs, response.change); // Calculate fee from inputs and outputs - let totalInputs = 0; - let totalOutputs = 0; const inputsData = []; const outputsData = []; const certificatesData = []; + let totalInputs = new BigNumber(0); + let totalOutputs = new BigNumber(0); + map(response.inputs, (input) => { - totalInputs += input.amount.quantity; + const inputAmount = new BigNumber(input.amount.quantity); + totalInputs = totalInputs.plus(inputAmount); const inputData = { address: input.address, amount: input.amount, @@ -933,8 +975,10 @@ export default class AdaApi { }; inputsData.push(inputData); }); + map(outputs, (output) => { - totalOutputs += output.amount.quantity; + const outputAmount = new BigNumber(output.amount.quantity); + totalOutputs = totalOutputs.plus(outputAmount); const outputData = { address: output.address, amount: output.amount, @@ -942,6 +986,7 @@ export default class AdaApi { }; outputsData.push(outputData); }); + if (response.certificates) { map(response.certificates, (certificate) => { const certificateData = { @@ -952,28 +997,21 @@ export default class AdaApi { certificatesData.push(certificateData); }); } - const fee = new BigNumber(totalInputs - totalOutputs).dividedBy( - LOVELACES_PER_ADA - ); - let transactionFee; - if (delegation && delegation.delegationAction) { - const delegationDeposit = new BigNumber(DELEGATION_DEPOSIT); - const isDepositIncluded = fee.gt(delegationDeposit); - transactionFee = isDepositIncluded ? fee.minus(delegationDeposit) : fee; - } else { - transactionFee = fee; - } + const deposits = map(response.deposits, (deposit) => deposit.quantity); + const totalDeposits = deposits.length + ? BigNumber.sum.apply(null, deposits) + : new BigNumber(0); + const feeWithDeposits = totalInputs.minus(totalOutputs); + const fee = feeWithDeposits.minus(totalDeposits); - // On first wallet delegation deposit is included in fee const extendedResponse = { inputs: inputsData, outputs: outputsData, certificates: certificatesData, - feeWithDelegationDeposit: fee, - fee: transactionFee, + feeWithDeposits: feeWithDeposits.dividedBy(LOVELACES_PER_ADA), + fee: fee.dividedBy(LOVELACES_PER_ADA), }; - logger.debug('AdaApi::selectCoins success', { extendedResponse }); return extendedResponse; } catch (error) { @@ -1242,6 +1280,36 @@ export default class AdaApi { } }; + getCurrencyList = async (): Promise => { + try { + const apiResponse = await getCurrencyList(); + const response: GetCurrencyListResponse = currencyConfig.responses.list( + apiResponse + ); + logger.debug('AdaApi::getCurrencyList success', { response }); + return response; + } catch (error) { + logger.error('AdaApi::getCurrencyList error', { error }); + throw new ApiError(error); + } + }; + + getCurrencyRate = async ( + currency: GetCurrencyRateRequest + ): Promise => { + try { + const apiResponse = await getCurrencyRate(currency); + const response: GetCurrencyRateResponse = currencyConfig.responses.rate( + apiResponse + ); + logger.debug('AdaApi::getCurrencyRate success', { response }); + return response; + } catch (error) { + logger.error('AdaApi::getCurrencyRate error', { error }); + throw new ApiError(error); + } + }; + restoreLegacyWallet = async ( request: RestoreLegacyWalletRequest ): Promise => { @@ -1678,6 +1746,80 @@ export default class AdaApi { } }; + getSmashSettings = async (): Promise => { + logger.debug('AdaApi::getSmashSettings called'); + try { + const { + pool_metadata_source: poolMetadataSource, + } = await getSmashSettings(this.config); + logger.debug('AdaApi::getSmashSettings success', { poolMetadataSource }); + return poolMetadataSource; + } catch (error) { + logger.error('AdaApi::getSmashSettings error', { error }); + throw new ApiError(error); + } + }; + + checkSmashServerIsValid = async (url: string): Promise => { + logger.debug('AdaApi::checkSmashServerIsValid called', { + parameters: { url }, + }); + try { + if (url === SMASH_SERVERS_LIST.direct.url) { + return true; + } + const { + health, + }: CheckSmashServerHealthApiResponse = await checkSmashServerHealth( + this.config, + url + ); + const isValid = health === SMASH_SERVER_STATUSES.AVAILABLE; + logger.debug('AdaApi::checkSmashServerIsValid success', { isValid }); + return isValid; + } catch (error) { + logger.error('AdaApi::checkSmashServerIsValid error', { error }); + throw new ApiError(error); + } + }; + + updateSmashSettings = async ( + poolMetadataSource: PoolMetadataSource + ): Promise => { + logger.debug('AdaApi::updateSmashSettings called', { + parameters: { poolMetadataSource }, + }); + try { + const isSmashServerValid = await this.checkSmashServerIsValid( + poolMetadataSource + ); + if (!isSmashServerValid) { + const error = { + code: 'invalid_smash_server', + }; + throw new ApiError(error); + } + await updateSmashSettings(this.config, poolMetadataSource); + logger.debug('AdaApi::updateSmashSettings success', { + poolMetadataSource, + }); + } catch (error) { + const id = get(error, 'id'); + const message = get(error, 'values.message'); + if ( + id === 'api.errors.GenericApiError' && + message === + 'Error parsing query parameter url failed: URI must not contain a path/query/fragment.' + ) { + throw new ApiError({ + code: 'invalid_smash_server', + }); + } + logger.error('AdaApi::updateSmashSettings error', { error }); + throw new ApiError(error); + } + }; + getRedeemItnRewardsFee = async ( request: GetRedeemItnRewardsFeeRequest ): Promise => { @@ -1691,7 +1833,7 @@ export default class AdaApi { MIN_REWARDS_REDEMPTION_RECEIVER_BALANCE ); // Amount is set to either wallet's balance in case balance is less than 3 ADA or 1 ADA in order to avoid min UTXO affecting transaction fees calculation - const amount = walletBalance.lessThan( + const amount = walletBalance.isLessThan( minRewardsReceiverBalance.times( MIN_REWARDS_REDEMPTION_RECEIVER_BALANCE * 3 ) @@ -1928,11 +2070,13 @@ export default class AdaApi { localTip: { epoch: get(nodeTip, 'epoch_number', 0), slot: get(nodeTip, 'slot_number', 0), + absoluteSlotNumber: get(nodeTip, 'absolute_slot_number', 0), }, networkTip: networkTip ? { - epoch: get(networkTip, 'epoch_number', null), - slot: get(networkTip, 'slot_number', null), + epoch: get(networkTip, 'epoch_number', 0), + slot: get(networkTip, 'slot_number', 0), + absoluteSlotNumber: get(networkTip, 'absolute_slot_number', 0), } : null, nextEpoch: nextEpoch @@ -1999,7 +2143,7 @@ export default class AdaApi { decentralization_level: decentralizationLevel, desired_pool_number: desiredPoolNumber, minimum_utxo_value: minimumUtxoValue, - hardfork_at: hardforkAt, + eras, } = networkParameters; const blockchainStartTime = moment(blockchain_start_time).valueOf(); @@ -2013,7 +2157,7 @@ export default class AdaApi { decentralizationLevel, desiredPoolNumber, minimumUtxoValue, - hardforkAt: hardforkAt || null, + hardforkAt: eras.shelley || null, }; } catch (error) { logger.error('AdaApi::getNetworkParameters error', { error }); @@ -2059,7 +2203,7 @@ export default class AdaApi { calculateDelegationFee = async ( request: GetDelegationFeeRequest - ): Promise => { + ): Promise => { logger.debug('AdaApi::calculateDelegationFee called', { parameters: filterLogData(request), }); @@ -2068,8 +2212,7 @@ export default class AdaApi { walletId: request.walletId, }); logger.debug('AdaApi::calculateDelegationFee success', { response }); - const delegationFee = _createDelegationFeeFromServerData(response); - return delegationFee; + return _createDelegationFeeFromServerData(response); } catch (error) { logger.error('AdaApi::calculateDelegationFee error', { error }); throw new ApiError(error); @@ -2103,6 +2246,169 @@ export default class AdaApi { } }; + createWalletSignature = async ( + request: CreateWalletSignatureRequest + ): Promise => { + logger.debug('AdaApi::createWalletSignature called', { + parameters: filterLogData(request), + }); + const { + walletId, + role, + index, + passphrase, + votingKey, + stakeKey, + addressHex, + } = request; + + try { + const data = { + passphrase, + metadata: { + [61284]: { + map: [ + { + k: { + int: 1, + }, + v: { + bytes: votingKey, + }, + }, + { + k: { + int: 2, + }, + v: { + bytes: stakeKey, + }, + }, + { + k: { + int: 3, + }, + v: { + bytes: addressHex, + }, + }, + ], + }, + }, + }; + const response = await createWalletSignature(this.config, { + walletId, + role, + index, + data, + }); + logger.debug('AdaApi::createWalletSignature success', { response }); + return response; + } catch (error) { + logger.error('AdaApi::createWalletSignature error', { error }); + throw new ApiError(error); + } + }; + + createVotingRegistrationTransaction = async ( + request: CreateVotingRegistrationRequest + ): Promise => { + logger.debug('AdaApi::createVotingRegistrationTransaction called', { + parameters: filterLogData(request), + }); + const { + walletId, + address, + addressHex, + amount, + passphrase, + votingKey, + stakeKey, + signature, + } = request; + + try { + const data = { + payments: [ + { + address, + amount: { + quantity: amount, + unit: WalletUnits.LOVELACE, + }, + }, + ], + passphrase, + metadata: { + [61284]: { + map: [ + { + k: { + int: 1, + }, + v: { + bytes: votingKey, + }, + }, + { + k: { + int: 2, + }, + v: { + bytes: stakeKey, + }, + }, + { + k: { + int: 3, + }, + v: { + bytes: addressHex, + }, + }, + ], + }, + [61285]: { + map: [ + { + k: { + int: 1, + }, + v: { + bytes: signature, + }, + }, + ], + }, + }, + }; + const response: Transaction = await createTransaction(this.config, { + walletId, + data: { ...data }, + }); + + logger.debug('AdaApi::createVotingRegistrationTransaction success', { + transaction: response, + }); + + return _createTransactionFromServerData(response); + } catch (error) { + logger.error('AdaApi::createVotingRegistrationTransaction error', { + error, + }); + throw new ApiError(error) + .set('wrongEncryptionPassphrase') + .where('code', 'bad_request') + .inc('message', 'passphrase is too short') + .set('transactionIsTooBig', true, { + linkLabel: 'tooBigTransactionErrorLinkLabel', + linkURL: 'tooBigTransactionErrorLinkURL', + }) + .where('code', 'transaction_is_too_big') + .result(); + } + }; + setCardanoNodeFault = async (fault: FaultInjectionIpcRequest) => { await cardanoFaultInjectionChannel.send(fault); }; @@ -2225,6 +2531,8 @@ const _createTransactionFromServerData = action( const { id, amount, + fee, + deposit, inserted_at, // eslint-disable-line camelcase pending_since, // eslint-disable-line camelcase depth, @@ -2233,6 +2541,7 @@ const _createTransactionFromServerData = action( outputs, withdrawals, status, + metadata, } = data; const state = _conditionToTxState(status); const stateInfo = @@ -2240,9 +2549,10 @@ const _createTransactionFromServerData = action( const date = get(stateInfo, 'time'); const slotNumber = get(stateInfo, ['block', 'slot_number'], null); const epochNumber = get(stateInfo, ['block', 'epoch_number'], null); + const confirmations = get(depth, 'quantity', 0); return new WalletTransaction({ id, - depth, + confirmations, slotNumber, epochNumber, title: direction === 'outgoing' ? 'Ada sent' : 'Ada received', @@ -2253,6 +2563,8 @@ const _createTransactionFromServerData = action( amount: new BigNumber( direction === 'outgoing' ? amount.quantity * -1 : amount.quantity ).dividedBy(LOVELACES_PER_ADA), + fee: new BigNumber(fee.quantity).dividedBy(LOVELACES_PER_ADA), + deposit: new BigNumber(deposit.quantity).dividedBy(LOVELACES_PER_ADA), date: utcStringToDate(date), description: '', addresses: { @@ -2261,6 +2573,7 @@ const _createTransactionFromServerData = action( withdrawals: withdrawals.map(({ stake_address: address }) => address), }, state, + metadata, }); } ); @@ -2289,8 +2602,13 @@ const _createMigrationFeeFromServerData = action( const _createDelegationFeeFromServerData = action( 'AdaApi::_createDelegationFeeFromServerData', (data: TransactionFee) => { - const amount = get(data, ['estimated_max', 'quantity'], 0); - return new BigNumber(amount).dividedBy(LOVELACES_PER_ADA); + const fee = new BigNumber( + get(data, ['estimated_max', 'quantity'], 0) + ).dividedBy(LOVELACES_PER_ADA); + const deposit = new BigNumber( + get(data, ['deposit', 'quantity'], 0) + ).dividedBy(LOVELACES_PER_ADA); + return { fee, deposit }; } ); diff --git a/source/renderer/app/api/errors.js b/source/renderer/app/api/errors.js index ddca71f329..21e2a37a06 100644 --- a/source/renderer/app/api/errors.js +++ b/source/renderer/app/api/errors.js @@ -106,4 +106,9 @@ export const messages = defineMessages({ description: '"Funds cannot be transferred from this wallet because it contains some unspent transaction outputs (UTXOs), with amounts of ada that are too small to be migrated." error message', }, + invalidSmashServer: { + id: 'api.errors.invalidSmashServer', + defaultMessage: '!!!This URL is not a valid SMASH server', + description: '"This URL is not a valid SMASH server" error message', + }, }); diff --git a/source/renderer/app/api/network/types.js b/source/renderer/app/api/network/types.js index 82ad11bdad..513e09c473 100644 --- a/source/renderer/app/api/network/types.js +++ b/source/renderer/app/api/network/types.js @@ -1,7 +1,8 @@ // @flow export type TipInfo = { - epoch: ?number, - slot: ?number, + epoch: number, + slot: number, + absoluteSlotNumber: number, }; export type NextEpoch = { @@ -120,5 +121,10 @@ export type GetNetworkParametersApiResponse = { decentralization_level: DecentralizationLevel, desired_pool_number: number, minimum_utxo_value: MinimumUtxoValue, - hardfork_at?: HardforkAt, + eras: { + byron?: HardforkAt, + shelley?: HardforkAt, + allegra?: HardforkAt, + mary?: HardforkAt, + }, }; diff --git a/source/renderer/app/api/staking/requests/checkSmashServerHealth.js b/source/renderer/app/api/staking/requests/checkSmashServerHealth.js new file mode 100644 index 0000000000..c4010a526d --- /dev/null +++ b/source/renderer/app/api/staking/requests/checkSmashServerHealth.js @@ -0,0 +1,17 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { CheckSmashServerHealthApiResponse } from '../types'; +import { request } from '../../utils/request'; + +export const checkSmashServerHealth = ( + config: RequestConfig, + url?: string +): Promise => + request( + { + method: 'GET', + path: '/v2/smash/health', + ...config, + }, + { url } + ); diff --git a/source/renderer/app/api/staking/requests/getSmashSettings.js b/source/renderer/app/api/staking/requests/getSmashSettings.js new file mode 100644 index 0000000000..390c8a9001 --- /dev/null +++ b/source/renderer/app/api/staking/requests/getSmashSettings.js @@ -0,0 +1,13 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { GetSmashSettingsResponse } from '../types'; +import { request } from '../../utils/request'; + +export const getSmashSettings = ( + config: RequestConfig +): Promise => + request({ + method: 'GET', + path: '/v2/settings', + ...config, + }); diff --git a/source/renderer/app/api/staking/requests/updateSmashSettings.js b/source/renderer/app/api/staking/requests/updateSmashSettings.js new file mode 100644 index 0000000000..c12078e85f --- /dev/null +++ b/source/renderer/app/api/staking/requests/updateSmashSettings.js @@ -0,0 +1,22 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { PoolMetadataSource } from '../types'; +import { request } from '../../utils/request'; + +export const updateSmashSettings = ( + config: RequestConfig, + poolMetadataSource: PoolMetadataSource +): Promise => + request( + { + method: 'PUT', + path: '/v2/settings', + ...config, + }, + {}, + { + settings: { + pool_metadata_source: poolMetadataSource, + }, + } + ); diff --git a/source/renderer/app/api/staking/types.js b/source/renderer/app/api/staking/types.js index a10166e21d..380d1d1858 100644 --- a/source/renderer/app/api/staking/types.js +++ b/source/renderer/app/api/staking/types.js @@ -94,6 +94,11 @@ export type GetDelegationFeeRequest = { walletId: string, }; +export type DelegationCalculateFeeResponse = { + fee: BigNumber, + deposit: BigNumber, +}; + export type QuitStakePoolRequest = { walletId: string, passphrase: string, @@ -115,3 +120,29 @@ export type RequestRedeemItnRewardsRequest = { }; export type RequestRedeemItnRewardsResponse = BigNumber; + +export type PoolMetadataSource = 'none' | 'direct' | string; + +export type UpdateSmashSettingsRequest = { + settings: { + pool_metadata_source: PoolMetadataSource, + }, +}; + +export type GetSmashSettingsResponse = { + pool_metadata_source: PoolMetadataSource, +}; + +export type GetSmashSettingsApiResponse = PoolMetadataSource; + +export type SmashServerStatuses = + | 'available' + | 'unavailable' + | 'unreachable' + | 'no_smash_configured'; + +export type CheckSmashServerHealthApiResponse = { + health: SmashServerStatuses, +}; + +export type CheckSmashServerHealthResponse = boolean; diff --git a/source/renderer/app/api/transactions/requests/createExternalTransaction.js b/source/renderer/app/api/transactions/requests/createExternalTransaction.js index fe75c92d53..b689b52f17 100644 --- a/source/renderer/app/api/transactions/requests/createExternalTransaction.js +++ b/source/renderer/app/api/transactions/requests/createExternalTransaction.js @@ -13,10 +13,10 @@ export const createExternalTransaction = ( request( { method: 'POST', - path: `/v2/proxy/transactions`, + path: '/v2/proxy/transactions', ...config, }, {}, signedTransactionBlob, - { isOctetStream: true } + { isOctetStreamRequest: true } ); diff --git a/source/renderer/app/api/transactions/requests/getTransaction.js b/source/renderer/app/api/transactions/requests/getTransaction.js new file mode 100644 index 0000000000..3b69d9e5b9 --- /dev/null +++ b/source/renderer/app/api/transactions/requests/getTransaction.js @@ -0,0 +1,15 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Transaction } from '../types'; +import { request } from '../../utils/request'; + +export const getTransaction = ( + config: RequestConfig, + walletId: string, + transactionId: string +): Promise => + request({ + method: 'GET', + path: `/v2/wallets/${walletId}/transactions/${transactionId}`, + ...config, + }); diff --git a/source/renderer/app/api/transactions/types.js b/source/renderer/app/api/transactions/types.js index a09d253b80..d161ae0f8e 100644 --- a/source/renderer/app/api/transactions/types.js +++ b/source/renderer/app/api/transactions/types.js @@ -3,6 +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 { TransactionMetadata } from '../../types/TransactionMetadata'; export type TransactionAmount = { quantity: number, @@ -50,6 +51,7 @@ export type Transaction = { outputs: Array, withdrawals: Array, status: TransactionState, + metadata?: TransactionMetadata, }; export type Transactions = Array; @@ -100,6 +102,11 @@ export type GetTransactionsRequest = { // cachedTransactions: Array, }; +export type GetTransactionRequest = { + walletId: string, + transactionId: string, +}; + export type GetTransactionFeeRequest = { walletId: string, address: string, @@ -216,7 +223,7 @@ export type CoinSelectionsResponse = { inputs: Array, outputs: Array, certificates: CoinSelectionCertificates, - feeWithDelegationDeposit: BigNumber, + feeWithDeposits: BigNumber, fee: BigNumber, }; diff --git a/source/renderer/app/api/utils/localStorage.js b/source/renderer/app/api/utils/localStorage.js index 673dd4519e..ec8bbbedee 100644 --- a/source/renderer/app/api/utils/localStorage.js +++ b/source/renderer/app/api/utils/localStorage.js @@ -18,6 +18,11 @@ 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 { + CURRENCY_IS_ACTIVE_BY_DEFAULT, + CURRENCY_DEFAULT_SELECTED, +} from '../../config/currencyConfig'; export type WalletLocalData = { id: string, @@ -80,7 +85,7 @@ export default class LocalStorageApi { key, id, }); - if (!value) return fallbackValue || ''; + if (value === undefined) return fallbackValue || ''; return value; }; @@ -181,6 +186,24 @@ export default class LocalStorageApi { unsetDataLayerMigrationAcceptance = (): Promise => LocalStorageApi.unset(keys.DATA_LAYER_MIGRATION_ACCEPTANCE); + getCurrencySelected = (): Promise => + LocalStorageApi.get(keys.CURRENCY_SELECTED, CURRENCY_DEFAULT_SELECTED); + + setCurrencySelected = (currency: Currency): Promise => + LocalStorageApi.set(keys.CURRENCY_SELECTED, currency); + + unsetCurrencySelected = (): Promise => + LocalStorageApi.unset(keys.CURRENCY_SELECTED); + + getCurrencyIsActive = (): Promise => + LocalStorageApi.get(keys.CURRENCY_ACTIVE, CURRENCY_IS_ACTIVE_BY_DEFAULT); + + setCurrencyIsActive = async (isActive: boolean): Promise => + LocalStorageApi.set(keys.CURRENCY_ACTIVE, isActive); + + unsetCurrencyIsActive = (): Promise => + LocalStorageApi.unset(keys.CURRENCY_ACTIVE); + getWalletsLocalData = (): Promise => LocalStorageApi.get(keys.WALLETS, {}); @@ -275,6 +298,15 @@ export default class LocalStorageApi { unsetAppUpdateCompleted = (): Promise => LocalStorageApi.unset(keys.APP_UPDATE_COMPLETED); + getSmashServer = (): Promise => + LocalStorageApi.get(keys.SMASH_SERVER); + + setSmashServer = (smashServerUrl: string): Promise => + LocalStorageApi.set(keys.SMASH_SERVER, smashServerUrl); + + unsetSmashServer = (): Promise => + LocalStorageApi.unset(keys.SMASH_SERVER); + // Paired Hardware wallets (software <-> hardware wallet / device) getHardwareWalletsLocalData = (): Promise => LocalStorageApi.get(keys.HARDWARE_WALLETS, {}); diff --git a/source/renderer/app/api/utils/patchAdaApi.js b/source/renderer/app/api/utils/patchAdaApi.js index 4460f6fa9b..7cb62e7098 100644 --- a/source/renderer/app/api/utils/patchAdaApi.js +++ b/source/renderer/app/api/utils/patchAdaApi.js @@ -49,10 +49,12 @@ export default (api: AdaApi) => { localTip: { epoch: get(node_tip, 'epoch_number', 0), slot: get(node_tip, 'slot_number', 0), + absoluteSlotNumber: get(node_tip, 'absolute_slot_number', 0), }, networkTip: { epoch: get(network_tip, 'epoch_number', null), slot: get(network_tip, 'slot_number', null), + absoluteSlotNumber: get(network_tip, 'absolute_slot_number', 0), }, nextEpoch: { // N+1 epoch diff --git a/source/renderer/app/api/utils/request.js b/source/renderer/app/api/utils/request.js index 19ad6d251e..bee0883f2f 100644 --- a/source/renderer/app/api/utils/request.js +++ b/source/renderer/app/api/utils/request.js @@ -1,5 +1,6 @@ // @flow import { includes, omit, size } from 'lodash'; +import JSONBigInt from 'json-bigint'; import querystring from 'querystring'; import { getContentLength } from '.'; @@ -27,12 +28,16 @@ function typedRequest( rawBodyParams?: any, requestOptions?: { returnMeta?: boolean, - isOctetStream?: boolean, + isOctetStreamRequest?: boolean, + isOctetStreamResponse?: boolean, } ): Promise { return new Promise((resolve, reject) => { const options: RequestOptions = Object.assign({}, httpOptions); - // const { returnMeta } = Object.assign({}, requestOptions); + const { isOctetStreamRequest, isOctetStreamResponse } = Object.assign( + {}, + requestOptions + ); let hasRequestBody = false; let requestBody = ''; @@ -41,26 +46,27 @@ function typedRequest( } // Handle raw body params - if ( - requestOptions && - requestOptions.isOctetStream && - rawBodyParams && - typeof rawBodyParams === 'string' - ) { + if (rawBodyParams) { hasRequestBody = true; - requestBody = rawBodyParams; - options.headers = { - 'Content-Length': requestBody.length / 2, - 'Content-Type': 'application/octet-stream', - Accept: 'application/json; charset=utf-8', - }; - } else if (rawBodyParams) { - hasRequestBody = true; - requestBody = JSON.stringify(rawBodyParams); + if (isOctetStreamRequest) { + requestBody = rawBodyParams; + options.headers = { + 'Content-Length': requestBody.length / 2, + 'Content-Type': 'application/octet-stream', + }; + } else { + requestBody = JSON.stringify(rawBodyParams); + options.headers = { + 'Content-Length': getContentLength(requestBody), + 'Content-Type': 'application/json; charset=utf-8', + }; + } + options.headers = { - 'Content-Length': getContentLength(requestBody), - 'Content-Type': 'application/json; charset=utf-8', - Accept: 'application/json; charset=utf-8', + ...options.headers, + Accept: isOctetStreamResponse + ? 'application/octet-stream' + : 'application/json; charset=utf-8', }; } @@ -70,17 +76,23 @@ function typedRequest( : global.https.request(options); if (hasRequestBody) { - if (requestOptions && requestOptions.isOctetStream) { + if (isOctetStreamRequest) { httpsRequest.write(requestBody, 'hex'); } else { httpsRequest.write(requestBody); } } + httpsRequest.on('response', (response) => { let body = ''; - // Cardano-sl returns chunked requests, so we need to concat them + let stream; + // cardano-wallet returns chunked requests, so we need to concat them response.on('data', (chunk) => { - body += chunk; + if (isOctetStreamResponse) { + stream = chunk; + } else { + body += chunk; + } }); // Reject errors response.on('error', (error) => reject(error)); @@ -94,29 +106,37 @@ function typedRequest( includes(ALLOWED_ERROR_EXCEPTION_PATHS, options.path)); if (isSuccessResponse) { - const data = - statusCode === 404 - ? 'null' - : `"statusCode: ${statusCode} -- statusMessage: ${statusMessage}"`; - // When deleting a wallet, the API does not return any data in body - // even if it was successful - if (!body) { - body = `{ - "status": ${statusCode}, - "data": ${data} - }`; + if (isOctetStreamResponse) { + resolve(stream); + } else { + const data = + statusCode === 404 + ? 'null' + : `"statusCode: ${statusCode} -- statusMessage: ${statusMessage}"`; + // When deleting a wallet, the API does not return any data in body + // even if it was successful + if (!body) { + body = `{ + "status": ${statusCode}, + "data": ${data} + }`; + } + resolve(JSONBigInt.parse(body)); } - resolve(JSON.parse(body)); + } else if (stream) { + // Error response with a stream + const parsedStream = JSONBigInt.parse(stream.toString()); + reject(parsedStream); } else if (body) { // Error response with a body - const parsedBody = JSON.parse(body); + const parsedBody = JSONBigInt.parse(body); if (parsedBody.code && parsedBody.message) { reject(parsedBody); } else { reject(new Error('Unknown API response')); } } else { - // Error response without a body + // Error response without a stream or body reject(new Error('Unknown API response')); } } catch (error) { diff --git a/source/renderer/app/api/voting/requests/createWalletSignature.js b/source/renderer/app/api/voting/requests/createWalletSignature.js new file mode 100644 index 0000000000..fb1b854594 --- /dev/null +++ b/source/renderer/app/api/voting/requests/createWalletSignature.js @@ -0,0 +1,19 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { SignatureParams } from '../types'; +import { request } from '../../utils/request'; + +export const createWalletSignature = ( + config: RequestConfig, + { walletId, role, index, data }: SignatureParams +): Promise => + request( + { + method: 'POST', + path: `/v2/wallets/${walletId}/signatures/${role}/${index}`, + ...config, + }, + {}, + data, + { isOctetStreamResponse: true } + ); diff --git a/source/renderer/app/api/voting/types.js b/source/renderer/app/api/voting/types.js new file mode 100644 index 0000000000..e599251c73 --- /dev/null +++ b/source/renderer/app/api/voting/types.js @@ -0,0 +1,30 @@ +// @flow +export type CreateVotingRegistrationRequest = { + walletId: string, + address: string, + addressHex: string, + amount: number, + passphrase: string, + votingKey: string, + stakeKey: string, + signature: string, +}; + +export type CreateWalletSignatureRequest = { + walletId: string, + role: string, + index: string, + passphrase: string, + votingKey: string, + stakeKey: string, + addressHex: string, +}; + +export type SignatureParams = { + walletId: string, + role: string, + index: string, + data: { + passphrase: string, + }, +}; diff --git a/source/renderer/app/api/wallets/requests/getCurrencyList.js b/source/renderer/app/api/wallets/requests/getCurrencyList.js new file mode 100644 index 0000000000..1822a271b3 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/getCurrencyList.js @@ -0,0 +1,9 @@ +// @flow +import { + genericCurrencyRequest, + REQUESTS, +} from '../../../config/currencyConfig'; + +const requestName = REQUESTS.LIST; + +export const getCurrencyList = genericCurrencyRequest(requestName); diff --git a/source/renderer/app/api/wallets/requests/getCurrencyRate.js b/source/renderer/app/api/wallets/requests/getCurrencyRate.js new file mode 100644 index 0000000000..a3c2a06df4 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/getCurrencyRate.js @@ -0,0 +1,11 @@ +// @flow +import { + genericCurrencyRequest, + REQUESTS, +} from '../../../config/currencyConfig'; +import type { GetCurrencyRateRequest } from '../types'; + +const requestName = REQUESTS.RATE; + +export const getCurrencyRate = (currency: GetCurrencyRateRequest) => + genericCurrencyRequest(requestName)(currency); diff --git a/source/renderer/app/api/wallets/types.js b/source/renderer/app/api/wallets/types.js index ac6495aef9..e4a8492a6a 100644 --- a/source/renderer/app/api/wallets/types.js +++ b/source/renderer/app/api/wallets/types.js @@ -2,6 +2,7 @@ import BigNumber from 'bignumber.js'; import { WalletUnits } from '../../domains/Wallet'; import type { ExportedByronWallet } from '../../types/walletExportTypes'; +import type { Currency } from '../../types/currencyTypes'; export type Block = { slot_number: number, @@ -301,3 +302,8 @@ export type CreateHardwareWalletRequest = { walletName: string, accountPublicKey: string, }; + +export type GetCurrencyListResponse = Array; + +export type GetCurrencyRateRequest = Currency; +export type GetCurrencyRateResponse = number; diff --git a/source/renderer/app/assets/images/currency-settings-ic.inline.svg b/source/renderer/app/assets/images/currency-settings-ic.inline.svg new file mode 100644 index 0000000000..66fd33eb38 --- /dev/null +++ b/source/renderer/app/assets/images/currency-settings-ic.inline.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/sidebar/voting-ic.inline.svg b/source/renderer/app/assets/images/sidebar/voting-ic.inline.svg new file mode 100644 index 0000000000..398c5fd3f6 --- /dev/null +++ b/source/renderer/app/assets/images/sidebar/voting-ic.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/smash-settings-ic.inline.svg b/source/renderer/app/assets/images/smash-settings-ic.inline.svg new file mode 100644 index 0000000000..8c51e1fecc --- /dev/null +++ b/source/renderer/app/assets/images/smash-settings-ic.inline.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/spinner-ic.inline.svg b/source/renderer/app/assets/images/spinner-ic.inline.svg new file mode 100644 index 0000000000..da22dc634c --- /dev/null +++ b/source/renderer/app/assets/images/spinner-ic.inline.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/spinner-tiny.inline.svg b/source/renderer/app/assets/images/spinner-tiny.inline.svg new file mode 100644 index 0000000000..0ca7caf5d9 --- /dev/null +++ b/source/renderer/app/assets/images/spinner-tiny.inline.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/voting/confirm-step-message-ic.inline.svg b/source/renderer/app/assets/images/voting/confirm-step-message-ic.inline.svg new file mode 100644 index 0000000000..40f0c1d11a --- /dev/null +++ b/source/renderer/app/assets/images/voting/confirm-step-message-ic.inline.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/voting/download-app-ic.inline.svg b/source/renderer/app/assets/images/voting/download-app-ic.inline.svg new file mode 100644 index 0000000000..edaa882958 --- /dev/null +++ b/source/renderer/app/assets/images/voting/download-app-ic.inline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/source/renderer/app/assets/images/voting/download-app-store-icon-ic.inline.svg b/source/renderer/app/assets/images/voting/download-app-store-icon-ic.inline.svg new file mode 100644 index 0000000000..318ed8dcb1 --- /dev/null +++ b/source/renderer/app/assets/images/voting/download-app-store-icon-ic.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/voting/download-play-store-icon-ic.inline.svg b/source/renderer/app/assets/images/voting/download-play-store-icon-ic.inline.svg new file mode 100644 index 0000000000..8be9b2ff2e --- /dev/null +++ b/source/renderer/app/assets/images/voting/download-play-store-icon-ic.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/voting/open-app-ic.inline.svg b/source/renderer/app/assets/images/voting/open-app-ic.inline.svg new file mode 100644 index 0000000000..b334a7c28b --- /dev/null +++ b/source/renderer/app/assets/images/voting/open-app-ic.inline.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/voting/wait-ic.inline.svg b/source/renderer/app/assets/images/voting/wait-ic.inline.svg new file mode 100644 index 0000000000..f91772cf38 --- /dev/null +++ b/source/renderer/app/assets/images/voting/wait-ic.inline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/source/renderer/app/components/appUpdate/AppUpdateOverlay.js b/source/renderer/app/components/appUpdate/AppUpdateOverlay.js index 3918d57517..89c90dd81b 100644 --- a/source/renderer/app/components/appUpdate/AppUpdateOverlay.js +++ b/source/renderer/app/components/appUpdate/AppUpdateOverlay.js @@ -158,24 +158,19 @@ export default class AppUpdateOverlay extends Component { downloadProgress, } = this.props; return ( -
-
-

- {intl.formatMessage(messages.downloadProgressLabel)} -

-

- - {intl.formatMessage(messages.downloadTimeLeft, { - downloadTimeLeft, - })} - {' '} - {intl.formatMessage(messages.downloadProgressData, { - totalDownloaded, - totalDownloadSize, - })} -

-
- +
+
); }; @@ -234,14 +229,13 @@ export default class AppUpdateOverlay extends Component { /> )} {isLinux && isWaitingToQuitDaedalus ? ( - <> -
-

- {intl.formatMessage(messages.installingUpdateLabel)} -

-
- - +
+ +
) : ( <>
- {hasLegacyNotification && activeWallet && ( - - )} + {IS_BYRON_WALLET_MIGRATION_ENABLED && + hasLegacyNotification && + activeWallet && ( + + )} ); } diff --git a/source/renderer/app/components/notifications/RestoreNotification.js b/source/renderer/app/components/notifications/RestoreNotification.js index 237d71c7a3..ab929cfc48 100644 --- a/source/renderer/app/components/notifications/RestoreNotification.js +++ b/source/renderer/app/components/notifications/RestoreNotification.js @@ -5,6 +5,7 @@ import { observer } from 'mobx-react'; import SVGInline from 'react-svg-inline'; import { defineMessages, intlShape } from 'react-intl'; import spinnerIcon from '../../assets/images/spinner-dark.inline.svg'; +import { formattedNumber } from '../../utils/formatters'; import styles from './RestoreNotification.scss'; const messages = defineMessages({ @@ -40,7 +41,7 @@ export default class RestoreNotification extends Component {
{intl.formatMessage(messages.activeRestoreMessage, { - percentage: restoreProgress, + percentage: formattedNumber(restoreProgress), })} diff --git a/source/renderer/app/components/settings/categories/StakePoolsSettings.js b/source/renderer/app/components/settings/categories/StakePoolsSettings.js new file mode 100644 index 0000000000..6891062652 --- /dev/null +++ b/source/renderer/app/components/settings/categories/StakePoolsSettings.js @@ -0,0 +1,349 @@ +// @flow +import React, { Component } from 'react'; +import { map } from 'lodash'; +import { Select } from 'react-polymorph/lib/components/Select'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { Link } from 'react-polymorph/lib/components/Link'; +import SVGInline from 'react-svg-inline'; +import { observer } from 'mobx-react'; +import { + defineMessages, + intlShape, + FormattedMessage, + FormattedHTMLMessage, +} from 'react-intl'; +import { getSmashServerIdFromUrl, getUrlParts } from '../../../utils/staking'; +import InlineEditingInput from '../../widgets/forms/InlineEditingInput'; +import styles from './StakePoolsSettings.scss'; +import { + SMASH_SERVERS_LIST, + SMASH_SERVER_TYPES, + SMASH_URL_VALIDATOR, +} from '../../../config/stakingConfig'; +import type { SmashServerType } from '../../../types/stakingTypes'; +import spinningIcon from '../../../assets/images/spinner-ic.inline.svg'; + +import LocalizableError from '../../../i18n/LocalizableError'; + +const messages = defineMessages({ + description: { + id: 'settings.stakePools.smash.description', + defaultMessage: + '!!!The {link} is an off-chain metadata server that enables the fast loading of stake pool details. Stake pools are also curated and each server has a different curation policy.', + description: 'description for the Stake Pools settings page.', + }, + descriptionLinkLabel: { + id: 'settings.stakePools.smash.descriptionLinkLabel', + defaultMessage: '!!!Stakepool Metadata Aggregation Server (SMASH)', + description: 'description for the Stake Pools settings page.', + }, + descriptionLinkUrl: { + id: 'settings.stakePools.smash.descriptionLinkUrl', + defaultMessage: + '!!!https://iohk.io/en/blog/posts/2020/11/17/in-pools-we-trust/', + description: 'description for the Stake Pools settings page.', + }, + descriptionIOHKContent1: { + id: 'settings.stakePools.smash.descriptionIOHKContent1', + defaultMessage: + '!!!The IOHK server ensures that registered stake pools are valid, helps to avoid duplicated ticker names or trademarks, and checks that the pools do not feature potentially offensive or harmful information.', + description: 'description for the Stake Pools settings page.', + }, + descriptionIOHKContent2: { + id: 'settings.stakePools.smash.descriptionIOHKContent2', + defaultMessage: + '!!!This allows us to deal with any scams, trolls, or abusive behavior by filtering out potentially problematic actors. {link} about the IOHK SMASH server.', + description: 'description for the Stake Pools settings page.', + }, + descriptionIOHKLinkLabel: { + id: 'settings.stakePools.smash.descriptionIOHKLinkLabel', + defaultMessage: '!!!Read more', + description: 'description for the Stake Pools settings page.', + }, + descriptionIOHKLinkUrl: { + id: 'settings.stakePools.smash.descriptionIOHKLinkUrl', + defaultMessage: + '!!!https://iohk.io/en/blog/posts/2020/11/17/in-pools-we-trust/', + description: 'description for the Stake Pools settings page.', + }, + descriptionNone: { + id: 'settings.stakePools.smash.descriptionNone', + defaultMessage: + '!!!This option is not recommended! Without the off-chain metadata server your Daedalus client will fetch this data by contacting every stake pool individually, which is a very slow and resource-consuming process. The list of stake pools received is not curated, so Daedalus will receive legitimate pools, duplicates, and fake pools. An added risk to this process is that your antivirus or antimalware software could recognize the thousands of network requests as malicious behavior by the Daedalus client.', + description: 'description for the Stake Pools settings page.', + }, + smashSelectLabel: { + id: 'settings.stakePools.smash.select.label', + defaultMessage: '!!!Off-chain metadata server (SMASH)', + description: + 'smashSelectLabel for the "Smash" selection on the Stake Pools settings page.', + }, + smashSelectIOHKServer: { + id: 'settings.stakePools.smash.select.IOHKServer', + defaultMessage: '!!!IOHK (Recommended)', + description: + 'smashSelectCustomServer option for the "Smash" selection on the Stake Pools settings page.', + }, + smashSelectDirect: { + id: 'settings.stakePools.smash.select.direct', + defaultMessage: '!!!None - let my Daedalus client fetch the data', + description: + 'smashSelectCustomServer option for the "Smash" selection on the Stake Pools settings page.', + }, + smashSelectCustomServer: { + id: 'settings.stakePools.smash.select.customServer', + defaultMessage: '!!!Custom server', + description: + 'smashSelectCustomServer option for the "Smash" selection on the Stake Pools settings page.', + }, + smashURLInputLabel: { + id: 'settings.stakePools.smashUrl.input.label', + defaultMessage: '!!!SMASH server URL', + description: + 'smashURLInputLabel for the "Smash Custom Server" selection on the Stake Pools settings page.', + }, + smashUrlInputPlaceholder: { + id: 'settings.stakePools.smashUrl.input.placeholder', + defaultMessage: '!!!Enter custom server URL', + description: + 'smashUrlInputPlaceholder for the "Smash Custom Server" selection on the Stake Pools settings page.', + }, + changesSaved: { + id: 'inline.editing.input.changesSaved', + defaultMessage: '!!!Your changes have been saved', + description: + 'Message "Your changes have been saved" for inline editing (eg. on Profile Settings page).', + }, + invalidUrl: { + id: 'settings.stakePools.smashUrl.input.invalidUrl', + defaultMessage: '!!!Invalid URL', + description: + 'invalidUrl for the "Smash Custom Server" selection on the Stake Pools settings page.', + }, + invalidUrlPrefix: { + id: 'settings.stakePools.smashUrl.input.invalidUrlPrefix', + defaultMessage: '!!!The URL should start with "https://"', + description: + 'invalidUrlPrefix for the "Smash Custom Server" selection on the Stake Pools settings page.', + }, + invalidUrlParameter: { + id: 'settings.stakePools.smashUrl.input.invalidUrlParameter', + defaultMessage: + '!!!Only "https://" protocol and hostname (e.g. domain.com) are allowed', + description: + 'invalidUrlParameter for the "Smash Custom Server" selection on the Stake Pools settings page.', + }, +}); + +type Props = { + smashServerUrl: string, + smashServerUrlError?: ?LocalizableError, + onSelectSmashServerUrl: Function, + onResetSmashServerError: Function, + isLoading: boolean, + onOpenExternalLink: Function, +}; + +type State = { + editingSmashServerUrl: string, + successfullyUpdated: boolean, + wasLoading: boolean, +}; + +@observer +export default class StakePoolsSettings extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + /* eslint-disable react/no-unused-state */ + // Disabling eslint due to a [known issue](https://github.com/yannickcr/eslint-plugin-react/issues/2061) + // `wasLoading` is actually used in the `getDerivedStateFromProps` method + static getDerivedStateFromProps( + { isLoading, smashServerUrlError }: Props, + { wasLoading }: State + ) { + const successfullyUpdated = + wasLoading && !isLoading && !smashServerUrlError; + return { + successfullyUpdated, + wasLoading: isLoading, + }; + } + + state = { + editingSmashServerUrl: this.props.smashServerUrl, + successfullyUpdated: false, + wasLoading: false, + }; + + componentWillUnmount() { + this.props.onResetSmashServerError(); + } + + handleSubmit = (url: string) => { + if (this.handleIsValid(url)) { + this.setState({ + editingSmashServerUrl: url, + }); + this.props.onSelectSmashServerUrl(url); + } + }; + + handleOnSelectSmashServerType = (smashServerType: SmashServerType) => { + const { onSelectSmashServerUrl, onResetSmashServerError } = this.props; + onResetSmashServerError(); + let editingSmashServerUrl = ''; + if (smashServerType !== SMASH_SERVER_TYPES.CUSTOM) { + editingSmashServerUrl = SMASH_SERVERS_LIST[smashServerType].url; + onSelectSmashServerUrl(editingSmashServerUrl); + } + this.setState({ + editingSmashServerUrl, + }); + }; + + handleIsValid = (url: string) => url === '' || SMASH_URL_VALIDATOR.test(url); + + handleErrorMessage = (value: string) => { + const { intl } = this.context; + let errorMessage = messages.invalidUrl; + const { pathname, search } = getUrlParts(value); + if (!/^https:\/\//i.test(value)) errorMessage = messages.invalidUrlPrefix; + else if (search || (pathname && pathname.slice(1))) + errorMessage = messages.invalidUrlParameter; + return intl.formatMessage(errorMessage); + }; + + smashSelectMessages = { + iohk: , + direct: this.context.intl.formatMessage(messages.smashSelectDirect), + custom: this.context.intl.formatMessage(messages.smashSelectCustomServer), + none: null, + }; + + render() { + const { smashServerUrlError, isLoading, onOpenExternalLink } = this.props; + const { intl } = this.context; + const { editingSmashServerUrl, successfullyUpdated } = this.state; + const smashServerType = getSmashServerIdFromUrl(editingSmashServerUrl); + + const selectedLabel = + this.smashSelectMessages[smashServerType] || smashServerType; + + const smashSelectOptions = map(SMASH_SERVER_TYPES, (value) => ({ + label: this.smashSelectMessages[value] || value, + value, + })); + + const errorMessage = smashServerUrlError + ? intl.formatMessage(smashServerUrlError) + : null; + + return ( +
+
+ + onOpenExternalLink( + intl.formatMessage(messages.descriptionLinkUrl) + ) + } + label={intl.formatMessage(messages.descriptionLinkLabel)} + /> + ), + }} + /> +
+ + {!isLoading ? ( + ( +
+ {label} + +
+ )} + selectedOption={selectedLabel} + disabled + /> + )} + + {smashServerType === SMASH_SERVER_TYPES.CUSTOM && ( + + )} + {smashServerType === SMASH_SERVER_TYPES.IOHK && ( +
+

{intl.formatMessage(messages.descriptionIOHKContent1)}

+

+ + onOpenExternalLink( + intl.formatMessage(messages.descriptionIOHKLinkUrl) + ) + } + label={intl.formatMessage( + messages.descriptionIOHKLinkLabel + )} + /> + ), + }} + /> +

+
+ )} + + {smashServerType === SMASH_SERVER_TYPES.DIRECT && ( +
+ +
+ )} +
+ ); + } +} diff --git a/source/renderer/app/components/settings/categories/StakePoolsSettings.scss b/source/renderer/app/components/settings/categories/StakePoolsSettings.scss new file mode 100644 index 0000000000..da0a50fc00 --- /dev/null +++ b/source/renderer/app/components/settings/categories/StakePoolsSettings.scss @@ -0,0 +1,145 @@ +@import '../../../themes/mixins/error-message'; + +.component { + margin-bottom: 20px; + + :global { + .RadioSet_radiosContainer { + flex-direction: column; + } + .SimpleOptions_option em { + opacity: 0.5; + } + } +} +.description { + color: var(--theme-support-settings-text-color); + display: block; + font-family: var(--font-light); + font-size: 16px; + line-height: 1.38; + margin-bottom: 20px; + p { + margin-bottom: 12px; + } + b { + font-family: var(--font-regular); + } + li { + list-style: decimal; + margin-left: 20px; + } + em { + color: var(--theme-delegation-steps-intro-link-color); + } + .link { + font-size: 16px; + } +} +.smashServerUrl { + margin-top: 20px; + :global { + .SimpleInput_errored { + border-color: var(--rp-input-border-color-errored); + } + } +} +.smashServerUrlError { + @include error-message; + margin-bottom: 1rem; + text-align: center; +} + +.optionDescription { + color: var(--theme-support-settings-text-color); + display: block; + font-family: var(--font-light); + font-size: 14px; + line-height: 1.38; + margin-top: 20px; + p { + margin-bottom: 12px; + } + b { + font-family: var(--font-bold); + } + :global { + .SimpleLink_root { + font-family: var(--font-regular); + word-break: break-word; + } + } +} + +.selectionRenderer { + color: var(--theme-support-settings-text-color); + font-family: var(--rp-theme-font-regular); + line-height: var(--rp-input-line-height); + padding: 14px 20px 0; + position: relative; + em { + opacity: 0.5; + } + .icon { + display: inline-block; + position: absolute; + right: 20px; + svg { + animation: spinner 1.5s linear infinite; + height: 12px; + position: relative; + top: 1px; + width: 12px; + g { + stroke: var(--rp-select-arrow-bg-color); + } + } + } +} + +@keyframes spinner { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.disabledInput { + input { + background-color: var(--rp-input-bg-color, #fafbfc); + border: 1px solid var(--rp-input-border-color, #c6cdd6); + color: var(--rp-input-text-color); + height: 50px; + } + :global { + .SimpleFormField_inputWrapper { + height: 50px; + } + } +} + +@keyframes animateSavingResultLabel { + 0% { + opacity: 0.5; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 0; + } +} + +.savingResultLabel { + animation: animateSavingResultLabel 3s; + color: var(--theme-input-right-floating-text-success-color); + font-family: var(--font-regular); + font-size: 13px; + opacity: 0; + position: absolute; + right: 0; + text-align: right; + top: 5px; +} diff --git a/source/renderer/app/components/settings/categories/WalletsSettings.js b/source/renderer/app/components/settings/categories/WalletsSettings.js new file mode 100644 index 0000000000..514f3ba94e --- /dev/null +++ b/source/renderer/app/components/settings/categories/WalletsSettings.js @@ -0,0 +1,122 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { map } from 'lodash'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import { Select } from 'react-polymorph/lib/components/Select'; +import { Link } from 'react-polymorph/lib/components/Link'; +import NormalSwitch from '../../widgets/forms/NormalSwitch'; +import styles from './WalletsSettings.scss'; +import { currencyConfig } from '../../../config/currencyConfig'; +import type { Currency } from '../../../types/currencyTypes'; + +const messages = defineMessages({ + currencyTitleLabel: { + id: 'settings.wallets.currency.titleLabel', + defaultMessage: '!!!Display ada balances in other currency', + description: + 'titleLabel for the Currency settings in the Wallets settings page.', + }, + currencyDescription: { + id: 'settings.wallets.currency.description', + defaultMessage: + '!!!Select a conversion currency for displaying your ada balances.', + description: + 'currencyDescription for the Currency settings in the Wallets settings page.', + }, + currencySelectLabel: { + id: 'settings.wallets.currency.selectLabel', + defaultMessage: '!!!Select currency', + description: + 'currencySelectLabel for the Currency settings in the Wallets settings page.', + }, + currencyDisclaimer: { + id: 'settings.wallets.currency.disclaimer', + defaultMessage: + '!!!Conversion rates are provided by CoinGecko without any warranty. Please use the calculated conversion value only as a reference. Converted balances reflect the current global average price of ada on active cryptocurrency exchanges, as tracked by CoinGecko. Ada conversion is available only to fiat and cryptocurrencies that are supported by CoinGecko, other local currency conversions may not be available.', + description: + 'currencyDisclaimer for the Currency settings in the Wallets settings page.', + }, + currencyPoweredByLabel: { + id: 'settings.wallets.currency.poweredBy.label', + defaultMessage: '!!!Powered by ', + description: + 'currencyPoweredByLabel for the Currency settings in the Wallets settings page.', + }, +}); + +type Props = { + currencySelected: ?Currency, + currencyList: Array, + currencyIsActive: boolean, + onSelectCurrency: Function, + onToggleCurrencyIsActive: Function, + onOpenExternalLink: Function, +}; + +@observer +export default class WalletSettings extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + render() { + const { intl } = this.context; + const { + currencySelected, + currencyList, + currencyIsActive, + onSelectCurrency, + onToggleCurrencyIsActive, + onOpenExternalLink, + } = this.props; + + const currencyOptions = map(currencyList, ({ symbol, name }) => { + return { + label: `${symbol.toUpperCase()} - ${name}`, + value: symbol, + }; + }); + + return ( +
+
+ {intl.formatMessage(messages.currencyTitleLabel)} +
+
+

{intl.formatMessage(messages.currencyDescription)}

+ +
+ {currencyIsActive && ( +
+
+
+ {intl.formatMessage(messages.currencyPoweredByLabel)} + onOpenExternalLink(currencyConfig.website)} + label={currencyConfig.name} + /> +
+ + + {transactionFeeError ? ( +
+

{transactionFeeError}

+
+ ) : null} + + {transactionError ? ( +
+

{intl.formatMessage(transactionError)}

+
+ ) : null} + + ); + } +} diff --git a/source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.scss b/source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.scss new file mode 100644 index 0000000000..e762b884e8 --- /dev/null +++ b/source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.scss @@ -0,0 +1,61 @@ +@import '../../../themes/mixins/animations'; +@import '../../../themes/mixins/loading-spinner'; +@import '../../../themes/mixins/error-message'; + +.component { + .description { + color: var(--theme-voting-registration-steps-description-color); + } + + .feesWrapper { + font-family: var(--font-medium); + font-weight: 500; + line-height: 1.38; + margin-bottom: 20px; + margin-top: 20px; + + .feesLabel { + color: var(--theme-voting-registration-steps-deposit-fees-label-color); + margin-bottom: 6px; + } + + .calculatingFeesLabel { + @include animated-ellipsis($width: 20px); + --webkit-backface-visibility: hidden; + } + + .feesAmount { + color: var(--theme-voting-registration-steps-deposit-fees-amount-color); + user-select: text; + + .feesAmountLabel { + font-family: var(--font-light); + } + } + } + + .errorMessage { + margin-top: 20px; + text-align: center; + + p { + color: var(--theme-color-error); + font-family: var(--font-medium); + font-weight: 500; + } + } + + .learnMoreWrapper { + margin-top: 10px; + + .externalLink { + font-family: var(--font-regular); + font-size: 16px; + } + } +} + +.isSubmitting { + box-shadow: none !important; + @include loading-spinner('../../../assets/images/spinner-light.svg'); +} diff --git a/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.js b/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.js new file mode 100644 index 0000000000..b184f3fe8a --- /dev/null +++ b/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.js @@ -0,0 +1,88 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import { defineMessages, intlShape } from 'react-intl'; +import Dialog from '../../../widgets/Dialog'; +import styles from './ConfirmationDialog.scss'; + +const messages = defineMessages({ + headline: { + id: 'voting.votingRegistration.dialog.confirmation.headline', + defaultMessage: '!!!Cancel Fund3 voting registration?', + description: + 'Headline for the voting registration cancellation confirmation dialog.', + }, + content: { + id: 'voting.votingRegistration.dialog.confirmation.content', + defaultMessage: + '!!!Are you sure that you want to cancel Fund3 voting registration? The transaction fee you paid for the voting registration transaction will be lost and you will need to repeat the registration from the beginning.', + description: + 'Content for the voting registration cancellation confirmation dialog.', + }, + cancelButtonLabel: { + id: + 'voting.votingRegistration.dialog.confirmation.button.cancelButtonLabel', + defaultMessage: '!!!Cancel registration', + description: + '"Cancel registration" button label for the voting registration cancellation confirmation dialog.', + }, + confirmButtonLabel: { + id: + 'voting.votingRegistration.dialog.confirmation.button.confirmButtonLabel', + defaultMessage: '!!!Continue registration', + description: + '"Continue registration" button label for the voting registration cancellation confirmation dialog.', + }, +}); + +type Props = { + onConfirm: Function, + onCancel: Function, +}; + +@observer +export default class ConfirmationDialog extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + render() { + const { intl } = this.context; + const { onConfirm, onCancel } = this.props; + + const dialogClasses = classnames([styles.component, 'ConfirmDialog']); + + const confirmButtonClasses = classnames([ + 'confirmButton', + // 'attention', + styles.confirmButton, + ]); + + const actions = [ + { + className: 'cancelButton', + label: intl.formatMessage(messages.cancelButtonLabel), + onClick: onCancel, + }, + { + className: confirmButtonClasses, + label: intl.formatMessage(messages.confirmButtonLabel), + primary: true, + onClick: onConfirm, + }, + ]; + + return ( + +

{intl.formatMessage(messages.content)}

+
+ ); + } +} diff --git a/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.scss b/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.scss new file mode 100644 index 0000000000..df4e48efb9 --- /dev/null +++ b/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.scss @@ -0,0 +1,12 @@ +.component { + color: var(--theme-dialog-text-color); + font-family: var(--font-light); + width: 440px; + + p { + line-height: 1.38; + &:not(:last-child) { + padding-bottom: 10px; + } + } +} diff --git a/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/VotingRegistrationDialog.js b/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/VotingRegistrationDialog.js new file mode 100644 index 0000000000..e65bbf02c1 --- /dev/null +++ b/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/VotingRegistrationDialog.js @@ -0,0 +1,102 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import type { Node } from 'react'; +import classnames from 'classnames'; +import { Stepper } from 'react-polymorph/lib/components/Stepper'; +import { StepperSkin } from 'react-polymorph/lib/skins/simple/StepperSkin'; +import { defineMessages, FormattedMessage, intlShape } from 'react-intl'; +import styles from './VotingRegistrationDialog.scss'; +import Dialog from '../../../widgets/Dialog'; +import DialogCloseButton from '../../../widgets/DialogCloseButton'; +import DialogBackButton from '../../../widgets/DialogBackButton'; +import type { DialogActions } from '../../../widgets/Dialog'; + +const messages = defineMessages({ + dialogTitle: { + id: 'voting.votingRegistration.dialog.dialogTitle', + defaultMessage: '!!!Register for Fund3 voting', + description: 'Tile "Register to vote" for voting registration', + }, + subtitle: { + id: 'voting.votingRegistration.dialog.subtitle', + defaultMessage: '!!!Step {step} of {stepCount}', + description: 'Sub title for voting registration', + }, +}); + +type Props = { + children: Node, + stepsList: Array, + activeStep: number, + actions: DialogActions, + onClose: Function, + onBack?: Function, + containerClassName?: ?string, + contentClassName?: ?string, + hideCloseButton?: boolean, + hideSteps?: boolean, +}; + +@observer +export default class VotingRegistrationDialog extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + static defaultProps = { + children: null, + }; + + render() { + const { intl } = this.context; + const { + children, + activeStep, + stepsList, + actions, + onClose, + onBack, + containerClassName, + contentClassName, + hideCloseButton, + hideSteps, + } = this.props; + const containerStyles = classnames([styles.container, containerClassName]); + const contentStyles = classnames([styles.content, contentClassName]); + + const stepsIndicatorLabel = ( + + ); + + return ( + } + backButton={onBack && } + actions={actions} + > + {!hideSteps && ( +
+ +
+ )} +
+
{children}
+
+
+ ); + } +} diff --git a/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/VotingRegistrationDialog.scss b/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/VotingRegistrationDialog.scss new file mode 100644 index 0000000000..399c752e11 --- /dev/null +++ b/source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/VotingRegistrationDialog.scss @@ -0,0 +1,44 @@ +@import '../../votingConfig'; + +.component { + text-align: center; + + :global { + .Dialog_title { + margin-bottom: 0; + } + + .Dialog_subtitle { + text-transform: uppercase; + } + } + + .votingRegistrationStepsIndicatorWrapper { + margin: 4px auto 0; + width: 450px; + + .stepIndicatorLabel { + color: var( + --theme-voting-registration-steps-activation-steps-indicator-color + ); + font-family: var(--font-medium); + font-size: 12px; + font-weight: 500; + line-height: 1.83; + margin-bottom: 10px; + text-align: center; + } + } + + .container { + font-family: var(--font-regular); + text-align: center; + } + + .content { + font-size: 16px; + line-height: 1.38; + margin-top: 20px; + text-align: left; + } +} diff --git a/source/renderer/app/components/wallet/WalletAdd.js b/source/renderer/app/components/wallet/WalletAdd.js index 19138248b3..f169c327e8 100644 --- a/source/renderer/app/components/wallet/WalletAdd.js +++ b/source/renderer/app/components/wallet/WalletAdd.js @@ -191,7 +191,6 @@ export default class WalletAdd extends Component { label={intl.formatMessage(messages.importLabel)} description={intl.formatMessage(messages.importDescription)} isDisabled={ - true || // This feature is currently unavailable as export tool is disabled isMaxNumberOfWalletsReached || (isProduction && !(isMainnet || isTestnet)) } diff --git a/source/renderer/app/components/wallet/WalletConnectDialog.js b/source/renderer/app/components/wallet/WalletConnectDialog.js index fa90285d1d..7f7072211b 100644 --- a/source/renderer/app/components/wallet/WalletConnectDialog.js +++ b/source/renderer/app/components/wallet/WalletConnectDialog.js @@ -3,8 +3,15 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; -import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import { + defineMessages, + intlShape, + FormattedHTMLMessage, + FormattedMessage, +} from 'react-intl'; import SVGInline from 'react-svg-inline'; +import { Link } from 'react-polymorph/lib/components/Link'; +import { LinkSkin } from 'react-polymorph/lib/skins/simple/LinkSkin'; import { get } from 'lodash'; import ledgerIcon from '../../assets/images/hardware-wallet/ledger-cropped.inline.svg'; import ledgerXIcon from '../../assets/images/hardware-wallet/ledger-x-cropped.inline.svg'; @@ -52,6 +59,22 @@ const messages = defineMessages({ '!!!

Daedalus currently supports only Trezor Model T hardware wallet devices.

If you are pairing your device with Daedalus for the first time, please follow the instructions below.

If you have already paired your device with Daedalus, you don’t need to repeat this step. Just connect your device when you need to confirm a transaction.

', description: 'Follow instructions label', }, + connectingIssueSupportLabel: { + id: 'wallet.connect.dialog.connectingIssueSupportLabel', + defaultMessage: + '!!!If you are experiencing issues pairing your hardware wallet device, please {supportLink}', + description: 'Connecting issue support description', + }, + connectingIssueSupportLink: { + id: 'wallet.connect.dialog.connectingIssueSupportLink', + defaultMessage: '!!!read the instructions.', + description: 'Connecting issue support link', + }, + connectingIssueSupportLinkUrl: { + id: 'wallet.connect.dialog.connectingIssueSupportLinkUrl', + defaultMessage: 'https://support.ledger.com/hc/en-us/articles/115005165269', + description: 'Link to support article', + }, }); type Props = { @@ -133,6 +156,19 @@ export default class WalletConnectDialog extends Component { ? messages.instructions : messages.instructionsTrezorOnly; + const supportLink = ( + + onExternalLinkClick( + intl.formatMessage(messages.connectingIssueSupportLinkUrl) + ) + } + label={intl.formatMessage(messages.connectingIssueSupportLink)} + skin={LinkSkin} + /> + ); + return ( { isTransactionStatus={false} />
+
+

+ +

+
)}
diff --git a/source/renderer/app/components/wallet/WalletConnectDialog.scss b/source/renderer/app/components/wallet/WalletConnectDialog.scss index 17e528889f..7a2007e016 100644 --- a/source/renderer/app/components/wallet/WalletConnectDialog.scss +++ b/source/renderer/app/components/wallet/WalletConnectDialog.scss @@ -78,6 +78,24 @@ width: 100%; } + .hardwareWalletIssueArticleWrapper { + margin-top: 20px; + + p { + color: var(--theme-hardware-wallet-message-color); + font-family: var(--font-light); + font-size: 16px; + line-height: 22px; + } + + .externalLink { + font-family: var(--font-regular); + font-size: 16px; + margin-right: 4px; + word-break: break-word; + } + } + .error { @include error-message; margin-top: 27px; diff --git a/source/renderer/app/components/wallet/WalletRestoreDialog.js b/source/renderer/app/components/wallet/WalletRestoreDialog.js index 924edc2b5f..a9d7a8c90c 100644 --- a/source/renderer/app/components/wallet/WalletRestoreDialog.js +++ b/source/renderer/app/components/wallet/WalletRestoreDialog.js @@ -34,7 +34,6 @@ import { } from '../../config/walletsConfig'; import { LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT, - PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT, WALLET_RECOVERY_PHRASE_WORD_COUNT, YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT, } from '../../config/cryptoConfig'; @@ -176,6 +175,12 @@ const messages = defineMessages({ description: 'Hint "Enter your 27-word paper wallet recovery phrase." for the recovery phrase input on the wallet restore dialog.', }, + shieldedRecoveryPhraseInputPlaceholder: { + id: 'wallet.restore.dialog.shielded.recovery.phrase.input.placeholder', + defaultMessage: '!!!Enter word #{wordNumber}', + description: + 'Placeholder "Enter word #" for the recovery phrase input on the wallet restore dialog.', + }, restorePaperWalletButtonLabel: { id: 'wallet.restore.dialog.paper.wallet.button.label', defaultMessage: '!!!Restore paper wallet', @@ -387,9 +392,7 @@ export default class WalletRestoreDialog extends Component { const label = this.isCertificate() ? this.context.intl.formatMessage(messages.restorePaperWalletButtonLabel) : this.context.intl.formatMessage(messages.importButtonLabel); - const buttonLabel = !isSubmitting ? label : ; - const actions = [ { label: buttonLabel, @@ -576,9 +579,12 @@ export default class WalletRestoreDialog extends Component { placeholder={ !this.isCertificate() ? intl.formatMessage(messages.recoveryPhraseInputHint) - : intl.formatMessage(messages.shieldedRecoveryPhraseInputHint, { - numberOfWords: PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT, - }) + : intl.formatMessage( + messages.shieldedRecoveryPhraseInputPlaceholder, + { + wordNumber: recoveryPhraseField.value.length + 1, + } + ) } options={suggestedMnemonics} requiredSelections={[RECOVERY_PHRASE_WORD_COUNT_OPTIONS[walletType]]} diff --git a/source/renderer/app/components/wallet/WalletSendForm.js b/source/renderer/app/components/wallet/WalletSendForm.js index 7cda63534b..52281fa44b 100755 --- a/source/renderer/app/components/wallet/WalletSendForm.js +++ b/source/renderer/app/components/wallet/WalletSendForm.js @@ -213,7 +213,7 @@ export default class WalletSendForm extends Component { placeholder: `0${ this.getCurrentNumberFormat().decimalSeparator }${'0'.repeat(this.props.currencyMaxFractionalDigits)}`, - value: null, + value: '', validators: [ async ({ field, form }) => { if (field.value === null) { @@ -349,14 +349,13 @@ export default class WalletSendForm extends Component { const receiverField = form.$('receiver'); const receiverFieldProps = receiverField.bind(); const amountFieldProps = amountField.bind(); - - const amount = new BigNumber(amountFieldProps.value || 0); + const amount = new BigNumber(amountFieldProps.value); let fees = null; let total = null; if (isTransactionFeeCalculated) { fees = transactionFee.toFormat(currencyMaxFractionalDigits); - total = amount.add(transactionFee).toFormat(currencyMaxFractionalDigits); + total = amount.plus(transactionFee).toFormat(currencyMaxFractionalDigits); } const buttonClasses = classnames(['primary', styles.nextButton]); @@ -391,12 +390,11 @@ export default class WalletSendForm extends Component {
{ this._isCalculatingTransactionFee = true; diff --git a/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js b/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js index 42d6ccc313..3e145634e9 100644 --- a/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js +++ b/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js @@ -42,6 +42,12 @@ const messages = defineMessages({ recoveryPhraseInputHint: { id: 'wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputHint', defaultMessage: '!!!Enter your {numberOfWords}-word recovery phrase', + description: 'Placeholder hint for the mnemonics autocomplete.', + }, + recoveryPhraseInputPlaceholder: { + id: + 'wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputPlaceholder', + defaultMessage: '!!!Enter word #{wordNumber}', description: 'Placeholder for the mnemonics autocomplete.', }, recoveryPhraseNoResults: { @@ -169,7 +175,6 @@ export default class WalletRecoveryPhraseEntryDialog extends Component { ]); const wordCount = WALLET_RECOVERY_PHRASE_WORD_COUNT; const enteredPhraseString = enteredPhrase.join(' '); - const buttonLabel = !isSubmitting ? ( intl.formatMessage(messages.buttonLabelConfirm) ) : ( @@ -212,9 +217,9 @@ export default class WalletRecoveryPhraseEntryDialog extends Component { {...recoveryPhraseField.bind()} label={intl.formatMessage(messages.recoveryPhraseInputLabel)} placeholder={intl.formatMessage( - messages.recoveryPhraseInputHint, + messages.recoveryPhraseInputPlaceholder, { - numberOfWords: wordCount, + wordNumber: enteredPhrase.length + 1, } )} options={suggestedMnemonics} diff --git a/source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.js b/source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.js index 6de7491388..0750aafc6d 100644 --- a/source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.js +++ b/source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.js @@ -299,7 +299,7 @@ export default class ChangeSpendingPasswordDialog extends Component { )}
-
+
span { height: 12px; - left: 145px; + left: 158px; outline: none; position: absolute; top: 4px; @@ -35,6 +35,16 @@ &.jpLangTooltipIcon { > span { + left: 134px !important; + } + } + + .currentPassword + span { + left: 145px !important; + } + + &.jpLangTooltipIcon { + .currentPassword + span { left: 135px !important; } } diff --git a/source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStep2Dialog.js b/source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStep2Dialog.js index 214263d89a..10f3eeadeb 100644 --- a/source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStep2Dialog.js +++ b/source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStep2Dialog.js @@ -41,11 +41,11 @@ export const messages = defineMessages({ defaultMessage: '!!!Verify', description: 'Label for the recoveryPhraseStep2Button on wallet settings.', }, - recoveryPhraseInputHint: { - id: 'wallet.settings.recoveryPhraseInputHint', - defaultMessage: '!!!Enter recovery phrase', + recoveryPhraseInputPlaceholder: { + id: 'wallet.settings.recoveryPhraseInputPlaceholder', + defaultMessage: '!!!Enter word #{wordNumber}', description: - 'Hint "Enter recovery phrase" for the recovery phrase input on the wallet restore dialog.', + 'Placeholder "Enter word #{wordNumber}" for the recovery phrase input on the verification dialog.', }, recoveryPhraseNoResults: { id: 'wallet.settings.recoveryPhraseInputNoResults', @@ -155,11 +155,15 @@ export default class WalletRecoveryPhraseStep2Dialog extends Component<

{intl.formatMessage(messages.recoveryPhraseStep2Description)}

- { componentWillUnmount() { // This call is used to prevent display of old successfully-updated messages - this.props.onCancelEditing(); + this.props.onCancel(); } onBlockForm = () => { @@ -222,15 +218,11 @@ export default class WalletSettings extends Component { onFieldValueChange, onStartEditing, onStopEditing, - onCancelEditing, + onCancel, onVerifyRecoveryPhrase, nameValidator, - activeField, - isSubmitting, isIncentivizedTestnet, - isInvalid, isLegacy, - lastUpdatedField, changeSpendingPasswordDialog, recoveryPhraseVerificationDate, recoveryPhraseVerificationStatus, @@ -268,22 +260,18 @@ export default class WalletSettings extends Component { onStartEditing('name')} - onStopEditing={onStopEditing} - onCancelEditing={onCancelEditing} + onFocus={() => onStartEditing('name')} + onBlur={onStopEditing} + onCancel={onCancel} onSubmit={(value) => onFieldValueChange('name', value)} isValid={nameValidator} - validationErrorMessage={intl.formatMessage( + valueErrorMessage={intl.formatMessage( globalMessages.invalidWalletName )} - successfullyUpdated={ - !isSubmitting && !isInvalid && lastUpdatedField === 'name' - } - inputBlocked={isFormBlocked} + readOnly={isFormBlocked} /> {!isHardwareWallet && ( diff --git a/source/renderer/app/components/wallet/summary/WalletSummary.js b/source/renderer/app/components/wallet/summary/WalletSummary.js index 8218ae15b8..b2f68dd12b 100644 --- a/source/renderer/app/components/wallet/summary/WalletSummary.js +++ b/source/renderer/app/components/wallet/summary/WalletSummary.js @@ -1,14 +1,20 @@ // @flow import React, { Component } from 'react'; import { observer } from 'mobx-react'; +import moment from 'moment'; import { defineMessages, intlShape } from 'react-intl'; import SVGInline from 'react-svg-inline'; import classnames from 'classnames'; -import adaSymbolBig from '../../../assets/images/ada-symbol-big-dark.inline.svg'; +import currencySettingsIcon from '../../../assets/images/currency-settings-ic.inline.svg'; +import globalMessages from '../../../i18n/global-messages'; import BorderedBox from '../../widgets/BorderedBox'; -import { DECIMAL_PLACES_IN_ADA } from '../../../config/numbersConfig'; import styles from './WalletSummary.scss'; import Wallet from '../../../domains/Wallet'; +import { + formattedWalletAmount, + formattedWalletCurrencyAmount, +} from '../../../utils/formatters'; +import type { Currency } from '../../../types/currencyTypes'; const messages = defineMessages({ transactionsLabel: { @@ -22,6 +28,21 @@ const messages = defineMessages({ description: '"Number of pending transactions" label on Wallet summary page', }, + currencyTitle: { + id: 'wallet.summary.page.currency.title', + defaultMessage: '!!!Converts as', + description: '"Currency - title" label on Wallet summary page', + }, + currencyLastFetched: { + id: 'wallet.summary.page.currency.lastFetched', + defaultMessage: '!!!converted {fetchedTimeAgo}', + description: '"Currency - last fetched" label on Wallet summary page', + }, + currencyIsFetchingRate: { + id: 'wallet.summary.page.currency.isFetchingRate', + defaultMessage: '!!!fetching conversion rates', + description: '"Currency - Fetching" label on Wallet summary page', + }, }); type Props = { @@ -30,6 +51,13 @@ type Props = { numberOfTransactions?: number, numberOfPendingTransactions: number, isLoadingTransactions: boolean, + currencyIsFetchingRate: boolean, + currencyIsAvailable: boolean, + currencyIsActive: boolean, + currencySelected: ?Currency, + currencyRate: ?number, + currencyLastFetched: ?Date, + onCurrencySettingClick: Function, }; @observer @@ -45,6 +73,13 @@ export default class WalletSummary extends Component { numberOfRecentTransactions, numberOfTransactions, isLoadingTransactions, + currencyIsActive, + currencyIsAvailable, + currencyIsFetchingRate, + currencyLastFetched, + currencyRate, + currencySelected, + onCurrencySettingClick, } = this.props; const { intl } = this.context; const isLoadingAllTransactions = @@ -55,33 +90,99 @@ export default class WalletSummary extends Component { ]); const isRestoreActive = wallet.isRestoring; + const hasCurrency = + currencyIsActive && + currencyIsAvailable && + !!currencySelected && + (!!currencyRate || currencyIsFetchingRate); + + const walletAmount = isRestoreActive + ? '-' + : formattedWalletAmount(wallet.amount, false); + + const { decimalDigits } = currencySelected || {}; + + let currencyWalletAmount; + if (isRestoreActive) currencyWalletAmount = '- '; + else if (hasCurrency && currencyRate) + currencyWalletAmount = formattedWalletCurrencyAmount( + wallet.amount, + currencyRate, + decimalDigits + ); + const currencyWalletAmountSymbol = currencySelected + ? currencySelected.symbol.toUpperCase() + : ''; + const fetchedTimeAgo = moment(currencyLastFetched) + .locale(intl.locale) + .fromNow(); + + const buttonClasses = classnames([ + styles.currencyLastFetched, + currencyIsFetchingRate ? styles.currencyIsFetchingRate : null, + ]); return (
-
{wallet.name}
-
- {isRestoreActive - ? '-' - : wallet.amount.toFormat(DECIMAL_PLACES_IN_ADA)} - -
- - {!isLoadingTransactions ? ( -
-
- {intl.formatMessage(messages.pendingTransactionsLabel)}:  - {numberOfPendingTransactions} -
-
- {intl.formatMessage(messages.transactionsLabel)}:  - {numberOfTransactions || numberOfRecentTransactions} +
+
+
{wallet.name}
+
+ {walletAmount} + + {intl.formatMessage(globalMessages.unitAda)} +
+ {!isLoadingTransactions ? ( +
+
+ {intl.formatMessage(messages.pendingTransactionsLabel)} + :  + {numberOfPendingTransactions} +
+
+ {intl.formatMessage(messages.transactionsLabel)}:  + {numberOfTransactions || numberOfRecentTransactions} +
+
+ ) : null}
- ) : null} + + {hasCurrency && ( +
+
+ {intl.formatMessage(messages.currencyTitle)} +
+
+ {currencyWalletAmount} + + {currencyWalletAmountSymbol} + +
+
+ 1 {intl.formatMessage(globalMessages.unitAda)} ={' '} + {currencyRate} {currencyWalletAmountSymbol} +
+ +
+ )} +
); diff --git a/source/renderer/app/components/wallet/summary/WalletSummary.scss b/source/renderer/app/components/wallet/summary/WalletSummary.scss index 3d1fb6e724..e820b4a842 100644 --- a/source/renderer/app/components/wallet/summary/WalletSummary.scss +++ b/source/renderer/app/components/wallet/summary/WalletSummary.scss @@ -13,10 +13,17 @@ padding: 20px; } +.walletContent { + display: flex; + flex-direction: row; + justify-content: space-between; +} + .walletName { color: var(--theme-bordered-box-text-color); - font-family: var(--font-bold); + font-family: var(--font-regular); font-size: 16px; + font-weight: 600; letter-spacing: 0.5px; line-height: 1.38; margin-bottom: 11px; @@ -25,13 +32,19 @@ .walletAmount { color: var(--theme-bordered-box-text-color); - font-family: var(--font-ultralight); - font-size: 40px; + font-family: var(--font-bold); + font-size: 22px; line-height: 1; margin-bottom: 11px; user-select: text; word-break: break-all; + span { + font-size: 16px; + margin-left: 6px; + opacity: 0.7; + } + & > .decimal { font-family: var(--font-medium); font-size: 18px; @@ -49,6 +62,99 @@ } } } +.walletAmountSymbol { + font-size: 16px; + opacity: 0.7; +} + +.currency { + color: var(--theme-bordered-box-text-color); + font-family: var(--font-bold); + font-size: 22px; + line-height: 1; + text-align: right; + user-select: none; + word-break: break-all; + + .currencySymbol { + font-size: 12px; + margin-left: 0; + } +} +.currencyTitle { + color: var(--theme-bordered-box-text-color); + font-family: var(--font-bold); + font-size: 12px; + font-stretch: normal; + font-style: normal; + letter-spacing: normal; + line-height: 1.67; + margin: 15px 0 5px; + opacity: 0.7; + text-align: right; +} + +.currencyWalletAmount { + @extend .walletAmount; + font-size: 16px; + margin-bottom: 5px; + text-align: right; +} + +.currencyRate { + font-family: var(--font-bold); + font-size: 12px; + line-height: 1.67; + margin-bottom: -6px; + opacity: 0.7; + text-align: right; +} + +.currencyLastFetched { + color: var(--theme-bordered-box-text-color); + cursor: pointer; + font-family: var(--font-bold); + font-size: 12px; + line-height: 1.67; + text-align: right; + + em { + opacity: 0.5; + } + + &:hover { + em { + opacity: 0.8; + } + + .currencySettingsIcon { + opacity: 1; + } + } + + &.currencyIsFetchingRate em { + margin-right: 13px; + @include animated-ellipsis($width: 16px); + } + + svg g > g { + fill: var(--theme-bordered-box-text-color); + } +} + +.currencySettingsIcon { + margin-left: 4px; + opacity: 0.6; + vertical-align: middle; + + svg { + height: 10px; + width: 10px; + g { + opacity: 1; + } + } +} .transactionsCountWrapper { .numberOfTransactions, diff --git a/source/renderer/app/components/wallet/transactions/FilterDialog.js b/source/renderer/app/components/wallet/transactions/FilterDialog.js index abbc3cc0c5..37abc17123 100644 --- a/source/renderer/app/components/wallet/transactions/FilterDialog.js +++ b/source/renderer/app/components/wallet/transactions/FilterDialog.js @@ -233,12 +233,12 @@ export default class FilterDialog extends Component { fromAmount: { type: 'number', label: '', - value: fromAmount ? Number(fromAmount) : '', + value: fromAmount, }, toAmount: { type: 'number', label: '', - value: toAmount ? Number(toAmount) : '', + value: toAmount, }, }, }); @@ -287,8 +287,8 @@ export default class FilterDialog extends Component { this.form.select('dateRange').set(dateRange); this.form.select('fromDate').set(fromDate); this.form.select('toDate').set(toDate); - this.form.select('fromAmount').set(fromAmount ? Number(fromAmount) : ''); - this.form.select('toAmount').set(toAmount ? Number(toAmount) : ''); + this.form.select('fromAmount').set(fromAmount); + this.form.select('toAmount').set(toAmount); this.form.select('incomingChecked').set(incomingChecked); this.form.select('outgoingChecked').set(outgoingChecked); }; @@ -489,24 +489,22 @@ export default class FilterDialog extends Component {
diff --git a/source/renderer/app/components/wallet/transactions/Transaction.js b/source/renderer/app/components/wallet/transactions/Transaction.js index 72959e5a4a..271addc092 100644 --- a/source/renderer/app/components/wallet/transactions/Transaction.js +++ b/source/renderer/app/components/wallet/transactions/Transaction.js @@ -8,6 +8,7 @@ import classNames from 'classnames'; import { Link } from 'react-polymorph/lib/components/Link'; import { LinkSkin } from 'react-polymorph/lib/skins/simple/LinkSkin'; import CancelTransactionButton from './CancelTransactionButton'; +import { TransactionMetadataView } from './metadata/TransactionMetadataView'; import styles from './Transaction.scss'; import TransactionTypeIcon from './TransactionTypeIcon'; import adaSymbol from '../../../assets/images/ada-symbol.inline.svg'; @@ -46,6 +47,22 @@ const messages = defineMessages({ defaultMessage: '!!!Transaction ID', description: 'Transaction ID.', }, + metadataLabel: { + id: 'wallet.transaction.metadataLabel', + defaultMessage: '!!!Transaction metadata', + description: 'Transaction metadata label', + }, + metadataDisclaimer: { + id: 'wallet.transaction.metadataDisclaimer', + defaultMessage: + '!!!Transaction metadata is not moderated and may contain inappropriate content.', + description: 'Transaction metadata disclaimer', + }, + metadataConfirmationLabel: { + id: 'wallet.transaction.metadataConfirmationLabel', + defaultMessage: '!!!Show unmoderated content', + description: 'Transaction metadata confirmation toggle', + }, conversionRate: { id: 'wallet.transaction.conversion.rate', defaultMessage: '!!!Conversion rate', @@ -86,6 +103,16 @@ const messages = defineMessages({ defaultMessage: '!!!To addresses', description: 'To addresses', }, + transactionFee: { + id: 'wallet.transaction.transactionFee', + defaultMessage: '!!!Transaction fee', + description: 'Transaction fee', + }, + deposit: { + id: 'wallet.transaction.deposit', + defaultMessage: '!!!Deposit', + description: 'Deposit', + }, transactionAmount: { id: 'wallet.transaction.transactionAmount', defaultMessage: '!!!Transaction amount', @@ -161,9 +188,11 @@ type Props = { isExpanded: boolean, isRestoreActive: boolean, isLastInList: boolean, + isShowingMetadata: boolean, formattedWalletAmount: Function, onDetailsToggled: ?Function, onOpenExternalLink: Function, + onShowMetadata: () => void, getUrlByType: Function, currentTimeFormat: string, walletId: string, @@ -172,6 +201,7 @@ type Props = { type State = { showConfirmationDialog: boolean, + showUnmoderatedMetadata: boolean, }; export default class Transaction extends Component { @@ -181,8 +211,20 @@ export default class Transaction extends Component { state = { showConfirmationDialog: false, + showUnmoderatedMetadata: false, }; + componentDidUpdate(prevProps: Props, prevState: State) { + // Tell parent components that meta data was toggled + if ( + !prevState.showUnmoderatedMetadata && + this.state.showUnmoderatedMetadata && + this.props.onShowMetadata + ) { + this.props.onShowMetadata(); + } + } + toggleDetails() { const { onDetailsToggled } = this.props; if (onDetailsToggled) onDetailsToggled(); @@ -300,6 +342,7 @@ export default class Transaction extends Component { const { data, isLastInList, + isShowingMetadata, state, formattedWalletAmount, onOpenExternalLink, @@ -486,6 +529,30 @@ export default class Transaction extends Component {
))} + {data.type === TransactionTypes.EXPEND && ( + <> +

{intl.formatMessage(messages.transactionFee)}

+
+
+ {formattedWalletAmount(data.fee, false)} + ADA +
+
+ + )} + + {!data.deposit.isZero() && ( + <> +

{intl.formatMessage(messages.deposit)}

+
+
+ {formattedWalletAmount(data.deposit, false)} + ADA +
+
+ + )} +

{intl.formatMessage(messages.transactionId)}

{ />
{this.renderCancelPendingTxnContent()} + + {data.metadata != null && ( +
+

{intl.formatMessage(messages.metadataLabel)}

+ {data.metadata && + (this.state.showUnmoderatedMetadata || + isShowingMetadata) ? ( + + ) : ( + <> +

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

+ { + e.preventDefault(); + this.setState({ showUnmoderatedMetadata: true }); + }} + /> + + )} +
+ )}
diff --git a/source/renderer/app/components/wallet/transactions/Transaction.scss b/source/renderer/app/components/wallet/transactions/Transaction.scss index a250e71bd6..00fed49804 100644 --- a/source/renderer/app/components/wallet/transactions/Transaction.scss +++ b/source/renderer/app/components/wallet/transactions/Transaction.scss @@ -179,6 +179,18 @@ } } + .transactionFeeRow, + .depositRow { + .amount { + color: var(--theme-transactions-list-item-highlight-color); + + .currency { + font-family: var(--font-light); + margin-left: 5px; + } + } + } + span { font-family: var(--font-light); font-size: 15px; @@ -244,3 +256,18 @@ } } } + +.metadataDisclaimer { + font-family: var(--font-light); + font-size: 16px; + line-height: 22px; +} + +.metadata { + margin-top: 20px; + + pre { + white-space: pre-wrap; + word-break: break-all; + } +} diff --git a/source/renderer/app/components/wallet/transactions/WalletTransactionsList.js b/source/renderer/app/components/wallet/transactions/WalletTransactionsList.js index 747fcf71b2..154c3d4b4f 100644 --- a/source/renderer/app/components/wallet/transactions/WalletTransactionsList.js +++ b/source/renderer/app/components/wallet/transactions/WalletTransactionsList.js @@ -83,6 +83,7 @@ export default class WalletTransactionsList extends Component { }; expandedTransactionIds: Map = new Map(); + transactionsShowingMetadata: Map = new Map(); virtualList: ?VirtualTransactionList; simpleList: ?SimpleTransactionList; loadingSpinner: ?LoadingSpinner; @@ -136,6 +137,9 @@ export default class WalletTransactionsList extends Component { isTxExpanded = (tx: WalletTransaction) => this.expandedTransactionIds.has(tx.id); + isTxShowingMetadata = (tx: WalletTransaction) => + this.transactionsShowingMetadata.has(tx.id); + toggleTransactionExpandedState = (tx: WalletTransaction) => { const isExpanded = this.isTxExpanded(tx); if (isExpanded) { @@ -150,6 +154,19 @@ export default class WalletTransactionsList extends Component { } }; + /** + * Update the height of the transaction when metadata is shown + * @param tx + */ + onShowMetadata = (tx: WalletTransaction) => { + this.transactionsShowingMetadata.set(tx.id, tx); + if (this.virtualList) { + this.virtualList.updateTxRowHeight(tx, true, true); + } else if (this.simpleList) { + this.simpleList.forceUpdate(); + } + }; + onShowMoreTransactions = (walletId: string) => { if (this.props.onShowMoreTransactions) { this.props.onShowMoreTransactions(walletId); @@ -186,10 +203,12 @@ export default class WalletTransactionsList extends Component { deletePendingTransaction={deletePendingTransaction} formattedWalletAmount={formattedWalletAmount} isExpanded={this.isTxExpanded(tx)} + isShowingMetadata={this.isTxShowingMetadata(tx)} isLastInList={isLastInGroup} isRestoreActive={isRestoreActive} onDetailsToggled={() => this.toggleTransactionExpandedState(tx)} onOpenExternalLink={onOpenExternalLink} + onShowMetadata={() => this.onShowMetadata(tx)} getUrlByType={getUrlByType} state={tx.state} walletId={walletId} diff --git a/source/renderer/app/components/wallet/transactions/metadata/MetadataValueView.js b/source/renderer/app/components/wallet/transactions/metadata/MetadataValueView.js new file mode 100644 index 0000000000..cafa248597 --- /dev/null +++ b/source/renderer/app/components/wallet/transactions/metadata/MetadataValueView.js @@ -0,0 +1,76 @@ +// @flow +import React from 'react'; +import type { + MetadataBytes, + MetadataInteger, + MetadataList, + MetadataMap, + MetadataString, + MetadataValue, +} from '../../../../types/TransactionMetadata'; +import styles from './TransactionMetadataView.scss'; + +/** + * NOTE: These components are currently not used because we simply + * JSON.stringify the metadata for transactions. This can be + * used as the basis for a more sophisticated implementation + * later on: + */ + +function IntegerView({ value }: { value: MetadataInteger }) { + return

{value.int}

; +} + +function StringView({ value }: { value: MetadataString }) { + return

{value.string}

; +} + +function BytesView({ value }: { value: MetadataBytes }) { + return

{value.bytes}

; +} + +function ListView({ value }: { value: MetadataList }) { + return ( +
    + {value.list.map((v, index) => ( + // eslint-disable-next-line react/no-array-index-key +
  1. + +
  2. + ))} +
+ ); +} + +function MapView({ value }: { value: MetadataMap }) { + return ( +
    + {value.map.map((v, index) => ( + // eslint-disable-next-line react/no-array-index-key +
  1. + : +
  2. + ))} +
+ ); +} + +function MetadataValueView(props: { value: MetadataValue }) { + const { value } = props; + if (value.int) { + return ; + } + if (value.string) { + return ; + } + if (value.bytes) { + return ; + } + if (value.list) { + return ; + } + if (value.map) { + return ; + } + return null; +} diff --git a/source/renderer/app/components/wallet/transactions/metadata/TransactionMetadataView.js b/source/renderer/app/components/wallet/transactions/metadata/TransactionMetadataView.js new file mode 100644 index 0000000000..85ca2ed830 --- /dev/null +++ b/source/renderer/app/components/wallet/transactions/metadata/TransactionMetadataView.js @@ -0,0 +1,57 @@ +// @flow +import React from 'react'; +import JSONBigInt from 'json-bigint'; +import type { + MetadataMapValue, + MetadataValue, + TransactionMetadata, +} from '../../../../types/TransactionMetadata'; +import styles from './TransactionMetadataView.scss'; + +function flattenMetadata(data: MetadataValue) { + if (data.int) { + return data.int; + } + if (data.string) { + return data.string; + } + if (data.bytes) { + return `0x${data.bytes}`; + } + if (data.list) { + return data.list.map((v: MetadataValue) => flattenMetadata(v)); + } + if (data.map) { + return data.map.map((v: MetadataMapValue) => { + if (v.k.list || v.k.map) { + return { + key: flattenMetadata(v.k), + value: flattenMetadata(v.v), + }; + } + if (v.k.int) { + return { [v.k.int]: flattenMetadata(v.v) }; + } + if (v.k.string) { + return { [v.k.string]: flattenMetadata(v.v) }; + } + if (v.k.bytes) { + return { [v.k.bytes]: flattenMetadata(v.v) }; + } + return null; + }); + } + return null; +} + +export function TransactionMetadataView(props: { data: TransactionMetadata }) { + return ( +
+ {Object.keys(props.data).map((key: string) => ( +
+          {JSONBigInt.stringify(flattenMetadata(props.data[key]), null, 2)}
+        
+ ))} +
+ ); +} diff --git a/source/renderer/app/components/wallet/transactions/metadata/TransactionMetadataView.scss b/source/renderer/app/components/wallet/transactions/metadata/TransactionMetadataView.scss new file mode 100644 index 0000000000..499195c6c3 --- /dev/null +++ b/source/renderer/app/components/wallet/transactions/metadata/TransactionMetadataView.scss @@ -0,0 +1,65 @@ +.root { + font-family: var(--font-light); + & > * { + margin-bottom: 5px; + &:last-child { + margin-bottom: 0; + } + } +} + +/** + * NOTE: These styles are currently not used because we simply + * JSON.stringify the metadata for transactions. This can be + * used as the basis for a more sophisticated implementation + * later on: + */ + +//.list { +// &::before { +// content: '['; +// display: inline; +// } +// &::after { +// content: ']'; +// display: inline; +// } +// li::after { +// content: ','; +// display: inline; +// } +// li:last-child::after { +// content: ''; +// } +// li { +// padding-left: 10px; +// } +// li > * { +// display: inline-block; +// } +//} +// +//.map { +// vertical-align: top; +// &::before { +// content: '{'; +// display: inline; +// } +// &::after { +// content: '}'; +// display: inline; +// } +// li::after { +// content: ','; +// display: inline; +// } +// li:last-child::after { +// content: ''; +// } +// li { +// padding-left: 10px; +// } +// li > * { +// display: inline-block; +// } +//} diff --git a/source/renderer/app/components/wallet/transactions/render-strategies/VirtualTransactionList.js b/source/renderer/app/components/wallet/transactions/render-strategies/VirtualTransactionList.js index 76f9f1794c..33982a19ed 100644 --- a/source/renderer/app/components/wallet/transactions/render-strategies/VirtualTransactionList.js +++ b/source/renderer/app/components/wallet/transactions/render-strategies/VirtualTransactionList.js @@ -128,7 +128,6 @@ export class VirtualTransactionList extends Component { ? this.estimateHeightOfTxExpandedRow(row, tx) : this.estimateHeightOfTxContractedRow(row); this.recomputeVirtualRowHeights(); - // In case transaction has just been manually expanded we need to schedule // another row height calculation if the transaction still isn't fully // expanded in the moment of the initial execution of this method diff --git a/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.js b/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.js index 4fd86244be..9c11f2004e 100644 --- a/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.js +++ b/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.js @@ -149,7 +149,7 @@ export default class WalletSelectImportDialog extends Component { walletStatus = alreadyExistsStatus; } else if (wallet.import.status === WalletImportStatuses.COMPLETED) { walletStatus = walletImportedStatus; - } else if (wallet.is_passphrase_empty) { + } else if (wallet.isEmptyPassphrase) { walletStatus = noPasswordStatus; } else { walletStatus = hasPasswordStatus; diff --git a/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.scss b/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.scss index 3c330e782b..f4838aead9 100644 --- a/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.scss +++ b/source/renderer/app/components/wallet/wallet-import/WalletSelectImportDialog.scss @@ -184,6 +184,12 @@ display: flex; padding-right: 20px; + :global { + .InlineEditingSmallInput_component { + margin-bottom: 0 !important; + } + } + .walletsInputFieldInner { input { color: var(--theme-wallet-import-button-text-color); @@ -258,6 +264,14 @@ margin-top: -3px; } } + + .LoadingSpinner_component { + margin: 0 !important; + + .LoadingSpinner_icon svg path { + fill: var(--theme-wallet-import-button-text-color) !important; + } + } } .walletsStatusIconCheckmark { diff --git a/source/renderer/app/components/wallet/wallet-restore/MnemonicsDialog.js b/source/renderer/app/components/wallet/wallet-restore/MnemonicsDialog.js index 6128fdf96b..144df13db0 100644 --- a/source/renderer/app/components/wallet/wallet-restore/MnemonicsDialog.js +++ b/source/renderer/app/components/wallet/wallet-restore/MnemonicsDialog.js @@ -23,7 +23,7 @@ import type { const messages = defineMessages({ autocompletePlaceholder: { id: 'wallet.restore.dialog.step.mnemonics.autocomplete.placeholder', - defaultMessage: '!!!Enter your {numberOfWords}-word recovery phrase', + defaultMessage: '!!!Enter word #{wordNumber}', description: 'Placeholder for the mnemonics autocomplete.', }, autocompleteMultiLengthPhrase: { @@ -138,7 +138,7 @@ export default class MnemonicsDialog extends Component { Array.isArray(expectedWordCount) ? intl.formatMessage(messages.autocompleteMultiLengthPhrase) : intl.formatMessage(messages.autocompletePlaceholder, { - numberOfWords: expectedWordCount, + wordNumber: mnemonics.length + 1, }) } options={validWords} diff --git a/source/renderer/app/components/wallet/wallet-restore/widgets/ConfirmationDialog.scss b/source/renderer/app/components/wallet/wallet-restore/widgets/ConfirmationDialog.scss index df4e48efb9..cb44a6933f 100644 --- a/source/renderer/app/components/wallet/wallet-restore/widgets/ConfirmationDialog.scss +++ b/source/renderer/app/components/wallet/wallet-restore/widgets/ConfirmationDialog.scss @@ -5,6 +5,7 @@ p { line-height: 1.38; + &:not(:last-child) { padding-bottom: 10px; } diff --git a/source/renderer/app/components/wallet/wallet-restore/widgets/WalletRestoreDialog.js b/source/renderer/app/components/wallet/wallet-restore/widgets/WalletRestoreDialog.js index 9449bfe399..e16fe2c590 100644 --- a/source/renderer/app/components/wallet/wallet-restore/widgets/WalletRestoreDialog.js +++ b/source/renderer/app/components/wallet/wallet-restore/widgets/WalletRestoreDialog.js @@ -69,7 +69,7 @@ export default class WalletRestoreDialog extends Component { , backButton?: Node, className?: string, diff --git a/source/renderer/app/components/widgets/NodeSyncStatusIcon.js b/source/renderer/app/components/widgets/NodeSyncStatusIcon.js index e8cade34ac..f40cb23e16 100644 --- a/source/renderer/app/components/widgets/NodeSyncStatusIcon.js +++ b/source/renderer/app/components/widgets/NodeSyncStatusIcon.js @@ -3,6 +3,7 @@ import React, { Component } from 'react'; import SVGInline from 'react-svg-inline'; import { defineMessages, intlShape } from 'react-intl'; import classNames from 'classnames'; +import { formattedNumber } from '../../utils/formatters'; import spinnerIcon from '../../assets/images/top-bar/node-sync-spinner.inline.svg'; import syncedIcon from '../../assets/images/top-bar/node-sync-synced.inline.svg'; import styles from './NodeSyncStatusIcon.scss'; @@ -41,7 +42,7 @@ export default class NodeSyncStatusIcon extends Component {
{intl.formatMessage(messages.blocksSynced, { - percentage, + percentage: formattedNumber(percentage), })}
diff --git a/source/renderer/app/components/widgets/ProgressBarLarge.js b/source/renderer/app/components/widgets/ProgressBarLarge.js index 065ee3ec4d..592032c9a7 100644 --- a/source/renderer/app/components/widgets/ProgressBarLarge.js +++ b/source/renderer/app/components/widgets/ProgressBarLarge.js @@ -1,11 +1,17 @@ // @flow import React, { Component } from 'react'; +import classnames from 'classnames'; import { observer } from 'mobx-react'; import styles from './ProgressBarLarge.scss'; type Props = { progress: number, showProgressLabel?: boolean, + leftLabel?: string, + rightLabel1?: string, + rightLabel2?: string, + isDarkMode?: boolean, + loading?: boolean, }; @observer @@ -15,15 +21,46 @@ export default class ProgressBarLarge extends Component { }; render() { - const { progress, showProgressLabel } = this.props; + const { + progress, + showProgressLabel, + leftLabel, + rightLabel1, + rightLabel2, + isDarkMode, + loading, + } = this.props; + + const isComplete = progress >= 100; + + const progressStyles = classnames([ + styles.progress, + isComplete ? styles.isComplete : null, + isDarkMode ? styles.progressDarkMode : styles.progressLightMode, + loading ? styles.loading : null, + ]); + + const progressBarContainerStyles = classnames([ + styles.progressBarContainer, + loading ? styles.loading : null, + ]); + return (
-
-
- {showProgressLabel && ( -
{progress}%
- )} -
+
+

{leftLabel}

+

+ {rightLabel1} {rightLabel2} +

+
+
+ {!loading && ( +
+ {showProgressLabel && ( +
{progress}%
+ )} +
+ )}
); diff --git a/source/renderer/app/components/widgets/ProgressBarLarge.scss b/source/renderer/app/components/widgets/ProgressBarLarge.scss index 50453f6fd4..151a6169ce 100644 --- a/source/renderer/app/components/widgets/ProgressBarLarge.scss +++ b/source/renderer/app/components/widgets/ProgressBarLarge.scss @@ -1,29 +1,74 @@ .component { - height: 24px; + width: 100%; +} + +.content { + display: flex; + justify-content: space-between; + margin-bottom: 4px; + p { + font-size: 14px; + } +} + +.rightLabel { + opacity: 0.5; + b { + font-family: var(--font-bold); + } } .progressBarContainer { background-color: var(--theme-progress-bar-large-background-color); border-radius: 5px; - height: 100%; + height: 24px; position: relative; width: 100%; + + &.loading { + animation: animatedStripesBackground 2s linear infinite; + background: repeating-linear-gradient( + -63deg, + var(--theme-staking-progress-stripe-dark-1-background-color), + var(--theme-staking-progress-stripe-dark-1-background-color) 10px, + var(--theme-staking-progress-stripe-dark-2-background-color) 10px, + var(--theme-staking-progress-stripe-dark-2-background-color) 20px + ); + opacity: 0.3; + } } .progress { align-items: center; - background: repeating-linear-gradient( - -63deg, - var(--theme-progress-bar-large-progress-stripe1), - var(--theme-progress-bar-large-progress-stripe1) 10px, - var(--theme-progress-bar-large-progress-stripe2) 10px, - var(--theme-progress-bar-large-progress-stripe2) 20px - ); border-radius: 5px; display: flex; height: 100%; justify-content: flex-end; transition: width 0.24s; + + &:not(.isComplete) { + animation: animatedStripesBackground 2s linear infinite; + } +} + +.progressDarkMode { + background: repeating-linear-gradient( + -63deg, + var(--theme-progress-bar-large-progress-dark-stripe1), + var(--theme-progress-bar-large-progress-dark-stripe1) 10px, + var(--theme-progress-bar-large-progress-dark-stripe2) 10px, + var(--theme-progress-bar-large-progress-dark-stripe2) 20px + ); +} + +.progressLightMode { + background: repeating-linear-gradient( + -63deg, + var(--theme-staking-progress-stripe-dark-1-background-color), + var(--theme-staking-progress-stripe-dark-1-background-color) 10px, + var(--theme-staking-progress-stripe-dark-2-background-color) 10px, + var(--theme-staking-progress-stripe-dark-2-background-color) 20px + ); } .progressLabel { @@ -34,3 +79,12 @@ color: var(--theme-staking-progress-label-light); transform: translateX(-8px); } + +@keyframes animatedStripesBackground { + 0% { + background-position-x: 0; + } + 100% { + background-position-x: 67px; + } +} diff --git a/source/renderer/app/components/widgets/forms/InlineEditingInput.js b/source/renderer/app/components/widgets/forms/InlineEditingInput.js index e09eddea07..d07b0a0bb9 100644 --- a/source/renderer/app/components/widgets/forms/InlineEditingInput.js +++ b/source/renderer/app/components/widgets/forms/InlineEditingInput.js @@ -1,14 +1,22 @@ // @flow +/* eslint-disable react/no-did-update-set-state */ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; +import { get } from 'lodash'; +import { Button } from 'react-polymorph/lib/components/Button'; import vjf from 'mobx-react-form/lib/validators/VJF'; +import SVGInline from 'react-svg-inline'; import classnames from 'classnames'; import { Input } from 'react-polymorph/lib/components/Input'; -import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; import styles from './InlineEditingInput.scss'; import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../../config/timingConfig'; +import penIcon from '../../../assets/images/pen.inline.svg'; +import crossIcon from '../../../assets/images/close-cross.inline.svg'; +import arrowIcon from '../../../assets/images/arrow-right.inline.svg'; +import spinningIcon from '../../../assets/images/spinner-ic.inline.svg'; +import { ENTER_KEY_CODE, ESCAPE_KEY_CODE } from '../../../config/numbersConfig'; const messages = defineMessages({ change: { @@ -31,44 +39,65 @@ const messages = defineMessages({ type Props = { className?: string, - isActive: boolean, - inputFieldLabel: string, - inputFieldValue: string, - onStartEditing: Function, - onStopEditing: Function, - onCancelEditing: Function, + label: string, + value: string, + placeholder?: string, + onFocus?: Function, + onCancel?: Function, + onBlur?: Function, onSubmit: Function, isValid: Function, - validationErrorMessage: string, - successfullyUpdated: boolean, - inputBlocked?: boolean, + valueErrorMessage?: string | Function, + errorMessage?: ?string, + disabled?: boolean, + readOnly?: boolean, maxLength?: number, + isLoading?: boolean, + validateOnChange?: boolean, + successfullyUpdated?: boolean, }; type State = { isActive: boolean, + hasChanged: boolean, + successfullyUpdated: boolean, }; @observer export default class InlineEditingInput extends Component { - state = { - isActive: false, + static defaultProps = { + validateOnChange: true, + valueErrorMessage: '', }; static contextTypes = { intl: intlShape.isRequired, }; + state = { + isActive: false, + hasChanged: false, + successfullyUpdated: false, + }; + validator = new ReactToolboxMobxForm( { fields: { inputField: { - value: this.props.inputFieldValue, + value: this.props.value, validators: [ - ({ field }) => [ - this.props.isValid(field.value), - this.props.validationErrorMessage, - ], + ({ field }) => { + const { value } = field; + const { valueErrorMessage } = this.props; + const errorMessage = + typeof valueErrorMessage === 'function' + ? valueErrorMessage(value) + : valueErrorMessage; + return [ + this.props.isValid(value) && this.state.isActive, + errorMessage || null, + ]; + }, ], }, }, @@ -76,7 +105,7 @@ export default class InlineEditingInput extends Component { { plugins: { vjf: vjf() }, options: { - validateOnChange: true, + validateOnChange: this.props.validateOnChange, validationDebounceWait: FORM_VALIDATION_DEBOUNCE_WAIT, }, } @@ -84,120 +113,247 @@ export default class InlineEditingInput extends Component { submit = () => { this.validator.submit({ - onSuccess: (form) => { + onSuccess: async (form) => { + this.setInputBlur(); const { inputField } = form.values(); - if (inputField !== this.props.inputFieldValue) { - this.props.onSubmit(inputField); - this.props.onStopEditing(); - } else { - this.props.onCancelEditing(); + const { onSubmit, errorMessage } = this.props; + if (!!inputField && (inputField !== this.props.value || errorMessage)) { + this.setState({ + hasChanged: true, + successfullyUpdated: false, + }); + await onSubmit(inputField); + this.setState({ + hasChanged: false, + }); } - this.setState({ isActive: false }); }, }); }; handleInputKeyDown = (event: KeyboardEvent) => { - if (event.which === 13) { - // ENTER key - this.onBlur(); - } - if (event.which === 27) { - // ESCAPE key + if (event.which === ENTER_KEY_CODE) { + this.submit(); + } else if (event.which === ESCAPE_KEY_CODE) { this.onCancel(); } }; onFocus = () => { - this.setState({ isActive: true }); - this.props.onStartEditing(); + const { disabled, onFocus, readOnly } = this.props; + if (!disabled && !readOnly) { + this.setState({ + isActive: true, + }); + if (onFocus) onFocus(); + } }; - onBlur = () => { - if (this.state.isActive) { - this.submit(); + onBlur = (event: InputEvent) => { + event.stopPropagation(); + event.preventDefault(); + const { disabled, readOnly, onBlur } = this.props; + this.setState({ + isActive: false, + }); + if (!disabled && !readOnly && onBlur) { + onBlur(); } }; onCancel = () => { + const { value, onCancel, errorMessage } = this.props; + const inputField = this.validator.$('inputField'); + const newValue = !errorMessage ? value : ''; + inputField.set(newValue); + if (onCancel) onCancel(); + this.setInputFocus(); + this.setState({ + hasChanged: true, + }); + }; + + setInputFocus = () => { + const input = this.inputElement; + if (input instanceof HTMLElement) input.focus(); + }; + + setInputBlur = () => { + const input = this.inputElement; + if (input instanceof HTMLElement) input.blur(); + }; + + onChange = (...props: KeyboardEvent) => { + this.setState({ + hasChanged: true, + }); const inputField = this.validator.$('inputField'); - inputField.value = this.props.inputFieldValue; - this.setState({ isActive: false }); - this.props.onCancelEditing(); + inputField.onChange(...props); }; - componentDidUpdate() { - if (this.props.isActive) { - const { inputBlocked } = this.props; - // eslint-disable-next-line no-unused-expressions - this.inputField && !inputBlocked && this.inputField.focus(); + componentDidUpdate({ value: prevValue, errorMessage: prevError }: Props) { + const { value: nextValue, errorMessage: nextError } = this.props; + const inputField = this.validator.$('inputField'); + + // If there's an error, we focus the input again + if (nextError) { + this.setInputFocus(); + } else if (prevError && !nextError) { + // else we blur it + this.setInputBlur(); + } + + // In case the `value` prop was updated + // we need to manually update the ReactToolboxMobxForm input field + if (prevValue !== nextValue) { + inputField.set(nextValue); + if (nextValue === '') { + this.setState({ + hasChanged: false, + }); + } + } + + // If the `value` props was updated + // after a submit action + // we show the `success` message + const successfullyUpdated = !!nextValue && prevValue !== nextValue; + if (successfullyUpdated) { + this.setState({ + successfullyUpdated, + }); } } - inputField: Input; + inputElement: HTMLElement; + + preventDefaultHelper = (event: KeyboardEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; render() { const { validator } = this; const { className, - inputFieldLabel, - isActive, - inputBlocked, + label, maxLength, + placeholder, + disabled, + readOnly, + isLoading, + errorMessage, } = this.props; + const { isActive, hasChanged } = this.state; let { successfullyUpdated } = this.props; + if (successfullyUpdated === undefined) { + ({ successfullyUpdated } = this.state); + } const { intl } = this.context; const inputField = validator.$('inputField'); + let error; + if (inputField.error) error = inputField.error; + else if (!hasChanged) error = !!errorMessage; + + const showEditButton = + !isActive && !isLoading && !hasChanged && label.length && !readOnly; + const showFocusButtons = + !isLoading && !disabled && !readOnly && (isActive || hasChanged); + const showLoadingButton = isLoading; + const componentStyles = classnames([ className, styles.component, isActive ? null : styles.inactive, + readOnly ? styles.readOnly : null, + isLoading ? styles.isLoading : null, + showEditButton || showLoadingButton ? styles.twoButtons : null, + showFocusButtons ? styles.twoButtons : null, ]); const inputStyles = classnames([ successfullyUpdated ? 'input_animateSuccess' : null, isActive ? null : 'input_cursorPointer', ]); - - if (isActive) successfullyUpdated = false; + const buttonsWrapperStyles = classnames([ + styles.buttonsWrapper, + readOnly ? styles.readOnly : null, + ]); + const editButtonStyles = classnames([styles.button, styles.editButton]); + const cancelButtonStyles = classnames([styles.button, styles.cancelButton]); + const okButtonStyles = classnames([styles.button, styles.okButton]); + const submittingButtonStyles = classnames([ + styles.button, + styles.submittingButton, + ]); return ( -
+
this.handleInputKeyDown(event)} - error={isActive || inputBlocked ? inputField.error : null} - disabled={!isActive} + error={isActive ? error : !!error} + disabled={disabled} + readOnly={readOnly} ref={(input) => { - this.inputField = input; + if (!this.inputElement) { + this.inputElement = get(input, 'inputElement.current'); + } }} - skin={InputSkin} /> - {isActive && ( - - )} +
+ {showEditButton && ( +
{successfullyUpdated && (
{intl.formatMessage(messages.changesSaved)}
)} + + {errorMessage && !hasChanged && ( +
{errorMessage}
+ )}
); } diff --git a/source/renderer/app/components/widgets/forms/InlineEditingInput.scss b/source/renderer/app/components/widgets/forms/InlineEditingInput.scss index 2342b15b08..bfc00d3f04 100644 --- a/source/renderer/app/components/widgets/forms/InlineEditingInput.scss +++ b/source/renderer/app/components/widgets/forms/InlineEditingInput.scss @@ -1,3 +1,5 @@ +@import '../../../themes/mixins/error-message'; + .component { margin-bottom: 20px; position: relative; @@ -8,6 +10,12 @@ color: var(--theme-input-text-color); } + &:hover { + .editButton { + opacity: 1; + } + } + &.inactive { &:hover { input { @@ -16,20 +24,127 @@ } } + &.readOnly { + input:read-only { + background: var(--rp-input-bg-color-disabled); + border: none; + } + &:hover { + input { + cursor: text; + } + } + } + + &.oneButton { + :global { + input { + padding-right: 49px; + } + } + } + + &.twoButtons { + :global { + input { + padding-right: 88px; + } + } + } + + .buttonsWrapper { + cursor: text; + height: 28px; + margin-right: 11px; + position: absolute; + right: 1px; + top: 42px; + } + .button { - bottom: 14px; - color: var(--theme-label-button-color); + background-color: var(--theme-button-flat-background-color); + border-radius: 3px; cursor: pointer; - font-family: var(--font-light); - font-size: 16px; - line-height: 1.38; - opacity: 0.5; - position: absolute; - right: 22px; - text-transform: lowercase; + height: 28px; + width: 28px; + &:hover { + background-color: var(--theme-button-flat-background-color-hover); + } + &:active { + background-color: var(--theme-button-flat-background-color-active); + } + .icon { + svg { + height: 12px; + position: relative; + top: 1px; + width: 12px; + } + } + } + .editButton { + opacity: 0; + transition: opacity 0.25s; &:hover { opacity: 1; } + .icon { + svg path { + stroke: var(--theme-button-flat-text-color); + } + } + } + .cancelButton { + .icon { + svg { + height: 10px; + width: 10px; + g { + fill: var(--theme-button-flat-text-color); + } + } + } + } + .okButton { + background-color: var(--theme-button-primary-background-color); + margin-left: 11px; + &:hover { + background-color: var(--theme-button-primary-background-color-hover); + } + &:active { + background-color: var(--theme-button-primary-background-color-active); + } + .icon { + svg path { + stroke: var(--theme-button-primary-text-color); + } + } + } + .submittingButton { + &:hover { + background-color: var(--theme-button-flat-background-color); + cursor: default; + } + &:active { + background-color: var(--theme-button-flat-background-color); + } + .icon { + animation: spinner 1.5s linear; + animation-iteration-count: infinite; + display: inline-block; + svg g { + stroke: var(--theme-button-flat-text-color); + } + } + } + + @keyframes spinner { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } @keyframes animateSavingResultLabel { @@ -55,4 +170,16 @@ text-align: right; top: 5px; } + + :global { + .SimpleFormField_label { + display: inline-block; + } + } +} + +.errorMessage { + @include error-message; + margin-top: 20px; + text-align: center; } diff --git a/source/renderer/app/components/widgets/forms/PinCode.js b/source/renderer/app/components/widgets/forms/PinCode.js new file mode 100644 index 0000000000..aeaaf4c383 --- /dev/null +++ b/source/renderer/app/components/widgets/forms/PinCode.js @@ -0,0 +1,149 @@ +// @flow +import React, { Component } from 'react'; +import { map } from 'lodash'; +import { NumericInput } from 'react-polymorph/lib/components/NumericInput'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; +import { IDENTIFIERS } from 'react-polymorph/lib/themes/API'; +import { PopOver } from 'react-polymorph/lib/components/PopOver'; +import classNames from 'classnames'; +import styles from './PinCode.scss'; + +type Props = $Exact<{ + id: string, + name: string, + type: string, + autoFocus: boolean, + onChange?: Function, + label: string, + length: number, + disabled: boolean, + value: Array, + error: string | null, +}>; + +export default class PinCode extends Component { + static defaultProps = { + length: 4, + disabled: false, + value: [], + }; + + inputsRef = []; + focusKey = 0; + add = false; + + onChange = (inputValue: ?number, key: number) => { + const { value, onChange } = this.props; + const inputNewValue = + inputValue !== null && inputValue !== undefined + ? inputValue.toString() + : ''; + + if ( + !Object.prototype.hasOwnProperty.call(value, key) || + value[key] === '' || + inputNewValue === '' + ) { + const newValue = value; + newValue[key] = inputNewValue; + if (onChange) { + onChange(newValue); + } + this.focusKey = key; + this.add = inputValue !== null && inputValue !== undefined; + } + }; + + componentDidUpdate() { + const { value, length } = this.props; + const key = value.join('').length; + if (key > 0 && key < length) { + const inputFocusKey = this.add ? this.focusKey + 1 : this.focusKey - 1; + if ( + Object.prototype.hasOwnProperty.call(this.inputsRef, inputFocusKey) && + this.inputsRef[inputFocusKey] + ) + this.inputsRef[inputFocusKey].focus(); + } + } + + generatePinCodeInput = () => { + const { + id, + name, + type, + autoFocus, + length, + error, + value, + disabled, + } = this.props; + + const pinCodeClasses = classNames([ + styles.pinCode, + error ? styles.error : null, + ]); + + return ( +
+ {map(Array(length).fill(), (action, key) => { + return ( + { + if ( + !Object.prototype.hasOwnProperty.call(this.inputsRef, key) || + this.inputsRef[key] !== input + ) + this.inputsRef[key] = input; + }} + id={id} + name={name} + type={type} + className={pinCodeClasses} + label={null} + key={key} + themeId={IDENTIFIERS.INPUT} + skin={InputSkin} + onChange={(number) => this.onChange(number, key)} + value={value ? value[key] : undefined} + autoFocus={autoFocus && key === 0} + disabled={ + disabled || + (key !== 0 && + (!value || + !Object.prototype.hasOwnProperty.call(value, key - 1))) + } + /> + ); + })} +
+ ); + }; + + render() { + const { label, error } = this.props; + + const pinCode = this.generatePinCodeInput(); + + return ( +
+ + {error ? ( + + {pinCode} + + ) : ( + <>{pinCode} + )} +
+ ); + } +} diff --git a/source/renderer/app/components/widgets/forms/PinCode.scss b/source/renderer/app/components/widgets/forms/PinCode.scss new file mode 100644 index 0000000000..9b85eb9e29 --- /dev/null +++ b/source/renderer/app/components/widgets/forms/PinCode.scss @@ -0,0 +1,33 @@ +.component { + width: 240px; + + :global { + .SimpleFormField_label { + width: 100%; + } + + .SimpleInput_input { + font-family: var(--font-regular); + font-size: 14px; + height: 50px; + padding: 10px 10px; + text-align: center; + width: 50px; + } + } + + .pinCodeInput { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + width: 240px; + + .pinCode { + &.error { + input { + border-color: var(--theme-color-error); + } + } + } + } +} diff --git a/source/renderer/app/config/currenciesList.json b/source/renderer/app/config/currenciesList.json new file mode 100644 index 0000000000..8db0c2fcf1 --- /dev/null +++ b/source/renderer/app/config/currenciesList.json @@ -0,0 +1,814 @@ +{ + "aed": { + "symbol": "aed", + "name": "United Arab Emirates Dirham", + "decimalDigits": 2, + "symbolNative": "د.إ.‏" + }, + "afn": { + "symbol": "afn", + "name": "Afghan Afghani", + "decimalDigits": 0, + "symbolNative": "؋" + }, + "all": { + "symbol": "all", + "name": "Albanian Lek", + "decimalDigits": 0, + "symbolNative": "Lek" + }, + "amd": { + "symbol": "amd", + "name": "Armenian Dram", + "decimalDigits": 0, + "symbolNative": "դր." + }, + "ars": { + "symbol": "ars", + "name": "Argentine Peso", + "decimalDigits": 2, + "symbolNative": "$" + }, + "aud": { + "symbol": "aud", + "name": "Australian Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "azn": { + "symbol": "azn", + "name": "Azerbaijani Manat", + "decimalDigits": 2, + "symbolNative": "ман." + }, + "bam": { + "symbol": "bam", + "name": "Bosnia-Herzegovina Convertible Mark", + "decimalDigits": 2, + "symbolNative": "KM" + }, + "bch": { + "symbol": "bch", + "name": "Bitcoin Cash", + "decimalDigits": 8, + "id": "bitcoin-cash" + }, + "bdt": { + "symbol": "bdt", + "name": "Bangladeshi Taka", + "decimalDigits": 2, + "symbolNative": "৳" + }, + "bgn": { + "symbol": "bgn", + "name": "Bulgarian Lev", + "decimalDigits": 2, + "symbolNative": "лв." + }, + "bhd": { + "symbol": "bhd", + "name": "Bahraini Dinar", + "decimalDigits": 3, + "symbolNative": "د.ب.‏" + }, + "bif": { + "symbol": "bif", + "name": "Burundian Franc", + "decimalDigits": 0, + "symbolNative": "FBu" + }, + "bits": { + "symbol": "bits", + "name": "Bitcoinus", + "decimalDigits": 10, + "id": "bitcoinus" + }, + "bmd": { + "symbol": "bmd", + "name": "Bermudian dollar", + "decimalDigits": 2 + }, + "bnb": { + "symbol": "bnb", + "name": "Binance Coin", + "decimalDigits": 2, + "id": "binancecoin" + }, + "bnd": { + "symbol": "bnd", + "name": "Brunei Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "bob": { + "symbol": "bob", + "name": "Bolivian Boliviano", + "decimalDigits": 2, + "symbolNative": "Bs" + }, + "brl": { + "symbol": "brl", + "name": "Brazilian Real", + "decimalDigits": 2, + "symbolNative": "R$" + }, + "btc": { + "symbol": "btc", + "name": "Bitcoin", + "decimalDigits": 8, + "id": "bitcoin" + }, + "bwp": { + "symbol": "bwp", + "name": "Botswanan Pula", + "decimalDigits": 2, + "symbolNative": "P" + }, + "byn": { + "symbol": "byn", + "name": "Belarusian Ruble", + "decimalDigits": 2, + "symbolNative": "руб." + }, + "bzd": { + "symbol": "bzd", + "name": "Belize Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "cad": { + "symbol": "cad", + "name": "Canadian Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "cdf": { + "symbol": "cdf", + "name": "Congolese Franc", + "decimalDigits": 2, + "symbolNative": "FrCD" + }, + "chf": { + "symbol": "chf", + "name": "Swiss Franc", + "decimalDigits": 2, + "symbolNative": "CHF" + }, + "clp": { + "symbol": "clp", + "name": "Chilean Peso", + "decimalDigits": 0, + "symbolNative": "$" + }, + "cny": { + "symbol": "cny", + "name": "Chinese Yuan", + "decimalDigits": 2, + "symbolNative": "CN¥" + }, + "cop": { + "symbol": "cop", + "name": "Colombian Peso", + "decimalDigits": 0, + "symbolNative": "$" + }, + "crc": { + "symbol": "crc", + "name": "Costa Rican Colón", + "decimalDigits": 0, + "symbolNative": "₡" + }, + "cve": { + "symbol": "cve", + "name": "Cape Verdean Escudo", + "decimalDigits": 2, + "symbolNative": "CV$" + }, + "czk": { + "symbol": "czk", + "name": "Czech Republic Koruna", + "decimalDigits": 2, + "symbolNative": "Kč" + }, + "djf": { + "symbol": "djf", + "name": "Djiboutian Franc", + "decimalDigits": 0, + "symbolNative": "Fdj" + }, + "dkk": { + "symbol": "dkk", + "name": "Danish Krone", + "decimalDigits": 2, + "symbolNative": "kr" + }, + "dop": { + "symbol": "dop", + "name": "Dominican Peso", + "decimalDigits": 2, + "symbolNative": "RD$" + }, + "dot": { + "symbol": "dot", + "name": "Polkadot", + "decimalDigits": 2, + "id": "polkadot" + }, + "dzd": { + "symbol": "dzd", + "name": "Algerian Dinar", + "decimalDigits": 2, + "symbolNative": "د.ج.‏" + }, + "eek": { + "symbol": "eek", + "name": "Estonian Kroon", + "decimalDigits": 2, + "symbolNative": "kr" + }, + "egp": { + "symbol": "egp", + "name": "Egyptian Pound", + "decimalDigits": 2, + "symbolNative": "ج.م.‏" + }, + "eos": { + "symbol": "eos", + "name": "EOS", + "decimalDigits": 2, + "id": "eos" + }, + "ern": { + "symbol": "ern", + "name": "Eritrean Nakfa", + "decimalDigits": 2, + "symbolNative": "Nfk" + }, + "etb": { + "symbol": "etb", + "name": "Ethiopian Birr", + "decimalDigits": 2, + "symbolNative": "Br" + }, + "eth": { + "symbol": "eth", + "name": "Ethereum", + "decimalDigits": 18, + "id": "ethereum" + }, + "eur": { + "symbol": "eur", + "name": "Euro", + "decimalDigits": 2, + "symbolNative": "€" + }, + "gbp": { + "symbol": "gbp", + "name": "British Pound Sterling", + "decimalDigits": 2, + "symbolNative": "£" + }, + "gel": { + "symbol": "gel", + "name": "Georgian Lari", + "decimalDigits": 2, + "symbolNative": "GEL" + }, + "ghs": { + "symbol": "ghs", + "name": "Ghanaian Cedi", + "decimalDigits": 2, + "symbolNative": "GH₵" + }, + "gnf": { + "symbol": "gnf", + "name": "Guinean Franc", + "decimalDigits": 0, + "symbolNative": "FG" + }, + "gtq": { + "symbol": "gtq", + "name": "Guatemalan Quetzal", + "decimalDigits": 2, + "symbolNative": "Q" + }, + "hkd": { + "symbol": "hkd", + "name": "Hong Kong Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "hnl": { + "symbol": "hnl", + "name": "Honduran Lempira", + "decimalDigits": 2, + "symbolNative": "L" + }, + "hrk": { + "symbol": "hrk", + "name": "Croatian Kuna", + "decimalDigits": 2, + "symbolNative": "kn" + }, + "huf": { + "symbol": "huf", + "name": "Hungarian Forint", + "decimalDigits": 0, + "symbolNative": "Ft" + }, + "idr": { + "symbol": "idr", + "name": "Indonesian Rupiah", + "decimalDigits": 0, + "symbolNative": "Rp" + }, + "ils": { + "symbol": "ils", + "name": "Israeli New Sheqel", + "decimalDigits": 2, + "symbolNative": "₪" + }, + "inr": { + "symbol": "inr", + "name": "Indian Rupee", + "decimalDigits": 2, + "symbolNative": "টকা" + }, + "iqd": { + "symbol": "iqd", + "name": "Iraqi Dinar", + "decimalDigits": 0, + "symbolNative": "د.ع.‏" + }, + "irr": { + "symbol": "irr", + "name": "Iranian Rial", + "decimalDigits": 0, + "symbolNative": "﷼" + }, + "isk": { + "symbol": "isk", + "name": "Icelandic Króna", + "decimalDigits": 0, + "symbolNative": "kr" + }, + "jmd": { + "symbol": "jmd", + "name": "Jamaican Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "jod": { + "symbol": "jod", + "name": "Jordanian Dinar", + "decimalDigits": 3, + "symbolNative": "د.أ.‏" + }, + "jpy": { + "symbol": "jpy", + "name": "Japanese Yen", + "decimalDigits": 0, + "symbolNative": "¥" + }, + "kes": { + "symbol": "kes", + "name": "Kenyan Shilling", + "decimalDigits": 2, + "symbolNative": "Ksh" + }, + "khr": { + "symbol": "khr", + "name": "Cambodian Riel", + "decimalDigits": 2, + "symbolNative": "៛" + }, + "kmf": { + "symbol": "kmf", + "name": "Comorian Franc", + "decimalDigits": 0, + "symbolNative": "FC" + }, + "krw": { + "symbol": "krw", + "name": "South Korean Won", + "decimalDigits": 0, + "symbolNative": "₩" + }, + "kwd": { + "symbol": "kwd", + "name": "Kuwaiti Dinar", + "decimalDigits": 3, + "symbolNative": "د.ك.‏" + }, + "kzt": { + "symbol": "kzt", + "name": "Kazakhstani Tenge", + "decimalDigits": 2, + "symbolNative": "тңг." + }, + "lbp": { + "symbol": "lbp", + "name": "Lebanese Pound", + "decimalDigits": 0, + "symbolNative": "ل.ل.‏" + }, + "link": { + "symbol": "link", + "name": "Chainlink", + "id": "chainlink", + "decimalDigits": 2 + }, + "lkr": { + "symbol": "lkr", + "name": "Sri Lankan Rupee", + "decimalDigits": 2, + "symbolNative": "SL Re" + }, + "ltc": { + "symbol": "ltc", + "name": "Litecoin", + "decimalDigits": 8, + "id": "litecoin" + }, + "ltl": { + "symbol": "ltl", + "name": "Lithuanian Litas", + "decimalDigits": 2, + "symbolNative": "Lt" + }, + "lvl": { + "symbol": "lvl", + "name": "Latvian Lats", + "decimalDigits": 2, + "symbolNative": "Ls" + }, + "lyd": { + "symbol": "lyd", + "name": "Libyan Dinar", + "decimalDigits": 3, + "symbolNative": "د.ل.‏" + }, + "mad": { + "symbol": "mad", + "name": "Moroccan Dirham", + "decimalDigits": 2, + "symbolNative": "د.م.‏" + }, + "mdl": { + "symbol": "mdl", + "name": "Moldovan Leu", + "decimalDigits": 2, + "symbolNative": "MDL" + }, + "mga": { + "symbol": "mga", + "name": "Malagasy Ariary", + "decimalDigits": 0, + "symbolNative": "MGA" + }, + "mkd": { + "symbol": "mkd", + "name": "Macedonian Denar", + "decimalDigits": 2, + "symbolNative": "MKD" + }, + "mmk": { + "symbol": "mmk", + "name": "Myanma Kyat", + "decimalDigits": 0, + "symbolNative": "K" + }, + "mop": { + "symbol": "mop", + "name": "Macanese Pataca", + "decimalDigits": 2, + "symbolNative": "MOP$" + }, + "mur": { + "symbol": "mur", + "name": "Mauritian Rupee", + "decimalDigits": 0, + "symbolNative": "MURs" + }, + "mxn": { + "symbol": "mxn", + "name": "Mexican Peso", + "decimalDigits": 2, + "symbolNative": "$" + }, + "myr": { + "symbol": "myr", + "name": "Malaysian Ringgit", + "decimalDigits": 2, + "symbolNative": "RM" + }, + "mzn": { + "symbol": "mzn", + "name": "Mozambican Metical", + "decimalDigits": 2, + "symbolNative": "MTn" + }, + "nad": { + "symbol": "nad", + "name": "Namibian Dollar", + "decimalDigits": 2, + "symbolNative": "N$" + }, + "ngn": { + "symbol": "ngn", + "name": "Nigerian Naira", + "decimalDigits": 2, + "symbolNative": "₦" + }, + "nio": { + "symbol": "nio", + "name": "Nicaraguan Córdoba", + "decimalDigits": 2, + "symbolNative": "C$" + }, + "nok": { + "symbol": "nok", + "name": "Norwegian Krone", + "decimalDigits": 2, + "symbolNative": "kr" + }, + "npr": { + "symbol": "npr", + "name": "Nepalese Rupee", + "decimalDigits": 2, + "symbolNative": "नेरू" + }, + "nzd": { + "symbol": "nzd", + "name": "New Zealand Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "omr": { + "symbol": "omr", + "name": "Omani Rial", + "decimalDigits": 3, + "symbolNative": "ر.ع.‏" + }, + "pab": { + "symbol": "pab", + "name": "Panamanian Balboa", + "decimalDigits": 2, + "symbolNative": "B/." + }, + "pen": { + "symbol": "pen", + "name": "Peruvian Nuevo Sol", + "decimalDigits": 2, + "symbolNative": "S/." + }, + "php": { + "symbol": "php", + "name": "Philippine Peso", + "decimalDigits": 2, + "symbolNative": "₱" + }, + "pkr": { + "symbol": "pkr", + "name": "Pakistani Rupee", + "decimalDigits": 0, + "symbolNative": "₨" + }, + "pln": { + "symbol": "pln", + "name": "Polish Zloty", + "decimalDigits": 2, + "symbolNative": "zł" + }, + "pyg": { + "symbol": "pyg", + "name": "Paraguayan Guarani", + "decimalDigits": 0, + "symbolNative": "₲" + }, + "qar": { + "symbol": "qar", + "name": "Qatari Rial", + "decimalDigits": 2, + "symbolNative": "ر.ق.‏" + }, + "ron": { + "symbol": "ron", + "name": "Romanian Leu", + "decimalDigits": 2, + "symbolNative": "RON" + }, + "rsd": { + "symbol": "rsd", + "name": "Serbian Dinar", + "decimalDigits": 0, + "symbolNative": "дин." + }, + "rub": { + "symbol": "rub", + "name": "Russian Ruble", + "decimalDigits": 2, + "symbolNative": "₽." + }, + "rwf": { + "symbol": "rwf", + "name": "Rwandan Franc", + "decimalDigits": 0, + "symbolNative": "FR" + }, + "sar": { + "symbol": "sar", + "name": "Saudi Riyal", + "decimalDigits": 2, + "symbolNative": "ر.س.‏" + }, + "sats": { + "symbol": "sats", + "decimalDigits": 2, + "name": "Satoshi" + }, + "sdg": { + "symbol": "sdg", + "name": "Sudanese Pound", + "decimalDigits": 2, + "symbolNative": "SDG" + }, + "sek": { + "symbol": "sek", + "name": "Swedish Krona", + "decimalDigits": 2, + "symbolNative": "kr" + }, + "sgd": { + "symbol": "sgd", + "name": "Singapore Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "sos": { + "symbol": "sos", + "name": "Somali Shilling", + "decimalDigits": 0, + "symbolNative": "Ssh" + }, + "syp": { + "symbol": "syp", + "name": "Syrian Pound", + "decimalDigits": 0, + "symbolNative": "ل.س.‏" + }, + "thb": { + "symbol": "thb", + "name": "Thai Baht", + "decimalDigits": 2, + "symbolNative": "฿" + }, + "tnd": { + "symbol": "tnd", + "name": "Tunisian Dinar", + "decimalDigits": 3, + "symbolNative": "د.ت.‏" + }, + "top": { + "symbol": "top", + "name": "Tongan Paʻanga", + "decimalDigits": 2, + "symbolNative": "T$" + }, + "try": { + "symbol": "try", + "name": "Turkish Lira", + "decimalDigits": 2, + "symbolNative": "TL" + }, + "ttd": { + "symbol": "ttd", + "name": "Trinidad and Tobago Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "twd": { + "symbol": "twd", + "name": "New Taiwan Dollar", + "decimalDigits": 2, + "symbolNative": "NT$" + }, + "tzs": { + "symbol": "tzs", + "name": "Tanzanian Shilling", + "decimalDigits": 0, + "symbolNative": "TSh" + }, + "uah": { + "symbol": "uah", + "name": "Ukrainian Hryvnia", + "decimalDigits": 2, + "symbolNative": "₴" + }, + "ugx": { + "symbol": "ugx", + "name": "Ugandan Shilling", + "decimalDigits": 0, + "symbolNative": "USh" + }, + "usd": { + "symbol": "usd", + "name": "US Dollar", + "decimalDigits": 2, + "symbolNative": "$" + }, + "uyu": { + "symbol": "uyu", + "name": "Uruguayan Peso", + "decimalDigits": 2, + "symbolNative": "$" + }, + "uzs": { + "symbol": "uzs", + "name": "Uzbekistan Som", + "decimalDigits": 0, + "symbolNative": "UZS" + }, + "vef": { + "symbol": "vef", + "name": "Venezuelan Bolívar", + "decimalDigits": 2, + "symbolNative": "Bs.F." + }, + "vnd": { + "symbol": "vnd", + "name": "Vietnamese Dong", + "decimalDigits": 0, + "symbolNative": "₫" + }, + "xaf": { + "symbol": "xaf", + "name": "CFA Franc BEAC", + "decimalDigits": 0, + "symbolNative": "FCFA" + }, + "xag": { + "symbol": "xag", + "name": "Xrpalike Gene", + "decimalDigits": 5, + "id": "xrpalike-gene" + }, + "xau": { + "symbol": "xau", + "name": "Xaucoin", + "decimalDigits": 2 + }, + "xdr": { + "symbol": "xdr", + "name": "Xandereum", + "decimalDigits": 2 + }, + "xlm": { + "symbol": "xlm", + "name": "Stellar", + "decimalDigits": 4, + "id": "stellar" + }, + "xof": { + "symbol": "xof", + "name": "CFA Franc BCEAO", + "decimalDigits": 0, + "symbolNative": "CFA" + }, + "xrp": { + "symbol": "xrp", + "name": "XRP", + "decimalDigits": 5, + "id": "ripple" + }, + "yer": { + "symbol": "yer", + "name": "Yemeni Rial", + "decimalDigits": 0, + "symbolNative": "ر.ي.‏" + }, + "yfi": { + "symbol": "yfi", + "name": "yearn.finance", + "decimalDigits": 2, + "id": "yearn-finance" + }, + "zar": { + "symbol": "zar", + "name": "South African Rand", + "decimalDigits": 2, + "symbolNative": "R" + }, + "zmk": { + "symbol": "zmk", + "name": "Zambian Kwacha", + "decimalDigits": 0, + "symbolNative": "ZK" + }, + "zwl": { + "symbol": "zwl", + "name": "Zimbabwean Dollar", + "decimalDigits": 0, + "symbolNative": "ZWL$" + } +} diff --git a/source/renderer/app/config/currencyConfig.coinapi.js b/source/renderer/app/config/currencyConfig.coinapi.js new file mode 100644 index 0000000000..7c18264bed --- /dev/null +++ b/source/renderer/app/config/currencyConfig.coinapi.js @@ -0,0 +1,73 @@ +// @flow + +/** + * + * CoinAPI API + * + * https://www.coinapi.io/ + * + * check `currencyConfig.js` for more info + * + */ +import { get, values } from 'lodash'; +import { logger } from '../utils/logging'; +import type { Currency, CurrencyApiConfig } from '../types/currencyTypes.js'; +import type { + GetCurrencyListResponse, + GetCurrencyRateResponse, +} from '../api/wallets/types'; +import currenciesList from './currenciesList.json'; + +// For the complete response, check +// https://docs.coinapi.io/#get-specific-rate +type CurrencyRateCoinApiResponse = { + rate: string, +}; + +const id = 'coinapi'; +const name = 'CoinAPI'; +const website = 'https://www.coinapi.io/'; +const hostname = 'rest.coinapi.io'; +const version = 'v1'; +// If we need to use COINAPI, we will need to get a valid key +const apiKey = 'API_KEY'; + +const requests = { + rate: ({ symbol }: Currency) => ({ + hostname, + method: 'GET', + path: `/${version}/exchangerate/ADA/${symbol.toUpperCase()}?apikey=${apiKey}`, + }), +}; + +const responses = { + list: (): GetCurrencyListResponse => { + try { + const list = values(currenciesList); + logger.debug('Currency::CoinAPI::List success', { list }); + return list; + } catch (error) { + logger.error('Currency::CoinAPI::List error', { error }); + throw new Error(error); + } + }, + rate: (apiResponse: CurrencyRateCoinApiResponse): GetCurrencyRateResponse => { + try { + const rate = get(apiResponse, 'rate', 0); + logger.debug('Currency::CoinAPI::Rate success', { rate }); + return rate; + } catch (error) { + logger.error('Currency::CoinAPI::Rate error', { error }); + throw new Error(error); + } + }, +}; + +export default ({ + id, + name, + hostname, + website, + requests, + responses, +}: CurrencyApiConfig); diff --git a/source/renderer/app/config/currencyConfig.coingecko.js b/source/renderer/app/config/currencyConfig.coingecko.js new file mode 100644 index 0000000000..2c706046c5 --- /dev/null +++ b/source/renderer/app/config/currencyConfig.coingecko.js @@ -0,0 +1,94 @@ +// @flow + +/** + * + * CoingGecko API + * + * https://www.coingecko.com/en/api + * + * check `currencyConfig.js` for more info + * + */ +import { get } from 'lodash'; +import { logger } from '../utils/logging'; +import type { Currency, CurrencyApiConfig } from '../types/currencyTypes.js'; +import type { + GetCurrencyListResponse, + GetCurrencyRateResponse, +} from '../api/wallets/types'; +import currenciesList from './currenciesList.json'; + +// For the complete response, check +// https://api.coingecko.com/api/v3/coins/markets?ids=cardano&vs_currency=usd +type CurrencyRateGeckoResponse = Array<{ + current_price: number, +}>; + +const id = 'coingecko'; +const name = 'CoinGecko'; +const website = 'https://www.coingecko.com/en/api'; +const hostname = 'api.coingecko.com'; +const version = 'v3'; +const pathBase = `api/${version}`; + +const requests = { + list: [ + { + hostname, + method: 'GET', + path: `/${pathBase}/coins/list`, + }, + { + hostname, + method: 'GET', + path: `/${pathBase}/simple/supported_vs_currencies`, + }, + ], + rate: ({ symbol }: Currency) => ({ + hostname, + method: 'GET', + path: `/${pathBase}/coins/markets?ids=cardano&vs_currency=${symbol}`, + }), +}; + +const responses = { + list: (apiResponse: Array): GetCurrencyListResponse => { + try { + if (!Array.isArray(apiResponse) || apiResponse.length < 2) { + throw new Error('unexpected API response'); + } + const [completeList, vsCurrencies] = apiResponse; + const list = vsCurrencies + .map( + (symbol) => + currenciesList[symbol] || + completeList.find((currency) => currency.symbol === symbol) + ) + .filter((item) => !!item); + logger.debug('Currency::CoingGecko::List success', { list }); + return list; + } catch (error) { + logger.error('Currency::CoingGecko::List error', { error }); + throw new Error(error); + } + }, + rate: (apiResponse: CurrencyRateGeckoResponse): GetCurrencyRateResponse => { + try { + const rate = get(apiResponse, '[0].current_price', 0); + logger.debug('Currency::CoingGecko::Rate success', { rate }); + return rate; + } catch (error) { + logger.error('Currency::CoingGecko::Rate error', { error }); + throw new Error(error); + } + }, +}; + +export default ({ + id, + name, + hostname, + website, + requests, + responses, +}: CurrencyApiConfig); diff --git a/source/renderer/app/config/currencyConfig.js b/source/renderer/app/config/currencyConfig.js new file mode 100644 index 0000000000..4b6c482ce5 --- /dev/null +++ b/source/renderer/app/config/currencyConfig.js @@ -0,0 +1,48 @@ +// @flow + +/** + * + * This file imports the external currency API used + * + */ + +// Available APIS +import coingeckoConfig from './currencyConfig.coingecko'; + +import { externalRequest } from '../api/utils/externalRequest'; +import currenciesList from './currenciesList.json'; +import type { RequestName } from '../types/currencyTypes'; + +export const REQUESTS: { + [key: string]: RequestName, +} = { + LIST: 'list', + RATE: 'rate', +}; + +// Definitions +export const currencyConfig = coingeckoConfig; +export const CURRENCY_IS_ACTIVE_BY_DEFAULT = true; +export const CURRENCY_DEFAULT_SELECTED = currenciesList.usd; +export const CURRENCY_REQUEST_RATE_INTERVAL = 60 * 1000; // 1 minute | unit: milliseconds + +// Generic function for all the Currency requests +export const genericCurrencyRequest = ( + requestName: RequestName +): Function => async (payload?: any): any => { + const request = currencyConfig.requests[requestName]; + let response; + if (Array.isArray(request)) { + response = []; + for (const req of request) { + const responseItem = await externalRequest(req); + response.push(responseItem); + } + } else if (typeof request === 'function') { + const req = request(payload); + response = await externalRequest(req); + } else if (request) { + response = await externalRequest(request); + } + return response; +}; diff --git a/source/renderer/app/config/currencyConfig.nomics.js b/source/renderer/app/config/currencyConfig.nomics.js new file mode 100644 index 0000000000..b30b20f8a5 --- /dev/null +++ b/source/renderer/app/config/currencyConfig.nomics.js @@ -0,0 +1,73 @@ +// @flow + +/** + * + * Nomics API + * + * https://nomics.com/docs/ + * + * check `currencyConfig.js` for more info + * + */ +import { get, values } from 'lodash'; +import { logger } from '../utils/logging'; +import type { Currency, CurrencyApiConfig } from '../types/currencyTypes.js'; +import type { + GetCurrencyListResponse, + GetCurrencyRateResponse, +} from '../api/wallets/types'; +import currenciesList from './currenciesList.json'; + +// For the complete response, check +// https://nomics.com/docs/#operation/getCurrenciesTicker +type CurrencyRateNomicsResponse = Array<{ + price: string, +}>; + +const id = 'nomics'; +const name = 'Nomics'; +const website = 'https://nomics.com/docs/'; +const hostname = 'api.nomics.com'; +const version = 'v1'; +// If we need to use NOMICS, we will need to get a valid key +const apiKey = 'API_KEY'; + +const requests = { + rate: ({ symbol }: Currency) => ({ + hostname, + method: 'GET', + path: `/${version}/currencies/ticker?key=${apiKey}&ids=ADA&interval=1d,30d&&per-page=100&page=1&convert=${symbol.toUpperCase()}`, + }), +}; + +const responses = { + list: (): GetCurrencyListResponse => { + try { + const list = values(currenciesList); + logger.debug('Currency::Nomics::List success', { list }); + return list; + } catch (error) { + logger.error('Currency::Nomics::List error', { error }); + throw new Error(error); + } + }, + rate: (apiResponse: CurrencyRateNomicsResponse): GetCurrencyRateResponse => { + try { + const rate = parseFloat(get(apiResponse, '[0].price', 0)); + logger.debug('Currency::Nomics::Rate success', { rate }); + return rate; + } catch (error) { + logger.error('Currency::Nomics::Rate error', { error }); + throw new Error(error); + } + }, +}; + +export default ({ + id, + name, + hostname, + website, + requests, + responses, +}: CurrencyApiConfig); diff --git a/source/renderer/app/config/numbersConfig.js b/source/renderer/app/config/numbersConfig.js index 58113d95f1..f83b0fae34 100644 --- a/source/renderer/app/config/numbersConfig.js +++ b/source/renderer/app/config/numbersConfig.js @@ -9,3 +9,7 @@ export const LOVELACES_PER_ADA = 1000000; export const MAX_INTEGER_PLACES_IN_ADA = 11; export const DECIMAL_PLACES_IN_ADA = 6; export const TX_AGE_POLLING_THRESHOLD = 15 * 60 * 1000; // 15 minutes | unit: milliseconds + +// Keyboard events +export const ENTER_KEY_CODE = 13; +export const ESCAPE_KEY_CODE = 27; diff --git a/source/renderer/app/config/sidebarConfig.js b/source/renderer/app/config/sidebarConfig.js index 479b19bb82..e820034747 100644 --- a/source/renderer/app/config/sidebarConfig.js +++ b/source/renderer/app/config/sidebarConfig.js @@ -7,6 +7,7 @@ import paperWalletCertificateIcon from '../assets/images/sidebar/paper-certifica import delegationIcon from '../assets/images/sidebar/delegation-ic.inline.svg'; import delegationProgressIcon from '../assets/images/sidebar/delegation-progress-ic.inline.svg'; import networkInfoLogo from '../assets/images/sidebar/network-info-logo-cardano-ic.inline.svg'; +import votingIcon from '../assets/images/sidebar/voting-ic.inline.svg'; export type SidebarCategoryInfo = { name: string, @@ -50,6 +51,11 @@ export const CATEGORIES_BY_NAME = { icon: networkInfoLogo, route: ROUTES.NETWORK_INFO, }, + VOTING: { + name: 'VOTING', + icon: votingIcon, + route: ROUTES.VOTING.REGISTRATION, + }, }; export const CATEGORIES_WITH_DELEGATION_COUNTDOWN = [ @@ -71,6 +77,7 @@ export const CATEGORIES_LIST = [ CATEGORIES_BY_NAME.STAKING_DELEGATION_COUNTDOWN, CATEGORIES_BY_NAME.STAKING, CATEGORIES_BY_NAME.REDEEM_ITN_REWARDS, + CATEGORIES_BY_NAME.VOTING, CATEGORIES_BY_NAME.SETTINGS, CATEGORIES_BY_NAME.NETWORK_INFO, ]; diff --git a/source/renderer/app/config/stakingConfig.js b/source/renderer/app/config/stakingConfig.js index 6c232bd424..c04b2ad06f 100644 --- a/source/renderer/app/config/stakingConfig.js +++ b/source/renderer/app/config/stakingConfig.js @@ -1,9 +1,58 @@ // @flow import type { RedeemItnRewardsStep, + SmashServerType, DelegationAction, } from '../types/stakingTypes'; +import type { SmashServerStatuses } from '../api/staking/types'; + +const { smashUrl } = global; + +export const SMASH_SERVERS_LIST: { + [key: SmashServerType]: { + name: string, + url: string, + }, +} = { + iohk: { + name: 'IOHK', + url: smashUrl, + }, + // Metadata is fetched directly in URLs registered on chain, + direct: { + name: 'direct', + url: 'direct', + }, +}; + +export const SMASH_SERVER_TYPES: { + [key: string]: SmashServerType, +} = { + IOHK: 'iohk', + CUSTOM: 'custom', + DIRECT: 'direct', +}; + +export const SMASH_SERVER_INVALID_TYPES: { + [key: string]: SmashServerType, +} = { + NONE: 'none', +}; + +export const SMASH_SERVER_STATUSES: { + [key: string]: SmashServerStatuses, +} = { + AVAILABLE: 'available', + UNAVAILABLE: 'unavailable', + UNREACHABLE: 'unreachable', + NO_SMASH_CONFIGURED: 'no_smash_configured', +}; + +export const SMASH_URL_VALIDATOR = new RegExp( + '^(direct|https://[a-zA-Z0-9-_~.]+(:[0-9]+)?/?)$' +); + export const RANKING_SLIDER_RATIO = 60; export const MIN_DELEGATION_FUNDS = 10; export const MIN_DELEGATION_FUNDS_LOG = Math.log(MIN_DELEGATION_FUNDS); @@ -36,10 +85,12 @@ export const RECENT_STAKE_POOLS_COUNT = 6; // Timers -export const STAKE_POOL_TRANSACTION_CHECK_INTERVAL = 1 * 1000; // 1 second | unit: milliseconds; -export const STAKE_POOL_TRANSACTION_CHECKER_TIMEOUT = 30 * 1000; // 30 seconds | unit: milliseconds; -export const STAKE_POOLS_INTERVAL = 1 * 60 * 1000; // 1 minute | unit: milliseconds; -export const STAKE_POOLS_FAST_INTERVAL = 1 * 1000; // 1 second | unit: milliseconds; +export const STAKE_POOL_TRANSACTION_CHECK_INTERVAL = 1 * 1000; // 1 second | unit: milliseconds +export const STAKE_POOL_TRANSACTION_CHECKER_TIMEOUT = 30 * 1000; // 30 seconds | unit: milliseconds +export const STAKE_POOLS_INTERVAL = 1 * 60 * 1000; // 1 minute | unit: milliseconds +export const STAKE_POOLS_FAST_INTERVAL = 1 * 1000; // 1 second | unit: milliseconds +export const STAKE_POOLS_FETCH_TRACKER_INTERVAL = 30 * 1000; // 30 seconds | unit: milliseconds +export const STAKE_POOLS_FETCH_TRACKER_CYCLES = 6; // Redeem ITN Rewards @@ -53,8 +104,6 @@ export const REDEEM_ITN_REWARDS_STEPS: { RESULT: 'result', }; -export const DELEGATION_DEPOSIT = 2; // 2 ADA | unit: lovelace - export const DELEGATION_ACTIONS: { [key: string]: DelegationAction, } = { @@ -66,4 +115,4 @@ export const IS_RANKING_DATA_AVAILABLE = true; export const IS_SATURATION_DATA_AVAILABLE = true; -export const EPOCH_COUNTDOWN_INTERVAL = 1 * 1000; // 1 second | unit: milliseconds; +export const EPOCH_COUNTDOWN_INTERVAL = 1 * 1000; // 1 second | unit: milliseconds diff --git a/source/renderer/app/config/txnsConfig.js b/source/renderer/app/config/txnsConfig.js index c4f3c73edc..e37b71ecc4 100644 --- a/source/renderer/app/config/txnsConfig.js +++ b/source/renderer/app/config/txnsConfig.js @@ -1 +1,2 @@ export const PENDING_TIME_LIMIT = 10 * 60 * 1000; // 10 minutes | unit: milliseconds +export const TIME_TO_LIVE = 2 * 60 * 60; // 2 hours | unit: seconds (7200) diff --git a/source/renderer/app/config/urlsConfig.js b/source/renderer/app/config/urlsConfig.js index 1d7c73d77a..428492ffdb 100644 --- a/source/renderer/app/config/urlsConfig.js +++ b/source/renderer/app/config/urlsConfig.js @@ -1,4 +1,6 @@ // @flow +import { currencyConfig } from './currencyConfig'; + export const MAINNET_EXPLORER_URL = 'explorer.cardano.org'; export const STAGING_EXPLORER_URL = 'explorer.awstest.iohkdev.io'; export const TESTNET_EXPLORER_URL = 'explorer.cardano-testnet.iohkdev.io'; @@ -51,4 +53,5 @@ export const ALLOWED_EXTERNAL_HOSTNAMES = [ MAINNET_NEWS_HASH_URL, TESTNET_NEWS_HASH_URL, STAGING_NEWS_HASH_URL, + currencyConfig.hostname, ]; diff --git a/source/renderer/app/config/votingConfig.js b/source/renderer/app/config/votingConfig.js new file mode 100644 index 0000000000..2ac5be980c --- /dev/null +++ b/source/renderer/app/config/votingConfig.js @@ -0,0 +1,9 @@ +// @flow +const { isDev } = global.environment; + +export const VOTING_FUND_NUMBER = 3; +export const VOTING_REGISTRATION_MIN_WALLET_FUNDS = 2950; // 2950 ADA | unit: ADA +export const VOTING_REGISTRATION_FEE_CALCULATION_AMOUNT = 1; // 1 ADA | unit: ADA +export const VOTING_REGISTRATION_PIN_CODE_LENGTH = 4; +export const VOTING_REGISTRATION_MIN_TRANSACTION_CONFIRMATIONS = isDev ? 2 : 10; +export const VOTING_REGISTRATION_TRANSACTION_POLLING_INTERVAL = 5000; // 5 seconds | unit: milliseconds diff --git a/source/renderer/app/config/walletsConfig.js b/source/renderer/app/config/walletsConfig.js index 5ac9457ba0..5291f1eb17 100644 --- a/source/renderer/app/config/walletsConfig.js +++ b/source/renderer/app/config/walletsConfig.js @@ -33,3 +33,9 @@ export const RECOVERY_PHRASE_WORD_COUNT_OPTIONS = { export const WALLET_PUBLIC_KEY_NOTIFICATION_SEGMENT_LENGTH = 15; export const WALLET_PUBLIC_KEY_SHARING_ENABLED = false; + +// Automatic wallet migration from pre Daedalus 1.0.0 versions has been disabled +export const IS_AUTOMATIC_WALLET_MIGRATION_ENABLED = false; + +// Byron wallet migration has been temporarily disabled due to missing Api support after Mary HF +export const IS_BYRON_WALLET_MIGRATION_ENABLED = false; diff --git a/source/renderer/app/config/walletsConfigCurrenciesList.dummy.json b/source/renderer/app/config/walletsConfigCurrenciesList.dummy.json new file mode 100644 index 0000000000..b13e5f8623 --- /dev/null +++ b/source/renderer/app/config/walletsConfigCurrenciesList.dummy.json @@ -0,0 +1,97 @@ +[ + { + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin" + }, + { + "id": "ethereum", + "symbol": "eth", + "name": "Ethereum" + }, + { + "id": "litecoin", + "symbol": "ltc", + "name": "Litecoin" + }, + { + "id": "bitcoin-cash", + "symbol": "bch", + "name": "Bitcoin Cash" + }, + { + "id": "binancecoin", + "symbol": "bnb", + "name": "Binance Coin" + }, + { + "id": "eos", + "symbol": "eos", + "name": "EOS" + }, + { + "id": "ripple", + "symbol": "xrp", + "name": "XRP" + }, + { + "id": "stellar", + "symbol": "xlm", + "name": "Stellar" + }, + { + "id": "chainlink", + "symbol": "link", + "name": "Chainlink" + }, + { + "id": "polkadot", + "symbol": "dot", + "name": "Polkadot" + }, + { + "id": "yearn-finance", + "symbol": "yfi", + "name": "yearn.finance" + }, + { + "id": "uniswap-state-dollar", + "symbol": "usd", + "name": "unified Stable Dollar" + }, + { + "id": "block-duelers", + "symbol": "bdt", + "name": "Block Duelers" + }, + { + "id": "bitcoin-hd", + "symbol": "bhd", + "name": "Bitcoin HD" + }, + { + "id": "good-boy-points", + "symbol": "gbp", + "name": "Good Boy Points" + }, + { + "id": "lkr-coin", + "symbol": "lkr", + "name": "LKR Coin" + }, + { + "id": "trias", + "symbol": "try", + "name": "Trias" + }, + { + "id": "xrpalike-gene", + "symbol": "xag", + "name": "Xrpalike Gene" + }, + { + "id": "bitcoinus", + "symbol": "bits", + "name": "Bitcoinus" + } +] diff --git a/source/renderer/app/containers/Root.js b/source/renderer/app/containers/Root.js index ee7f7bfc16..afbd312e3d 100644 --- a/source/renderer/app/containers/Root.js +++ b/source/renderer/app/containers/Root.js @@ -24,9 +24,11 @@ export default class Root extends Component { networkStatus, profile, staking, + voting, uiDialogs, wallets, } = stores; + const { isVotingPage } = voting; const { isStakingPage, redeemStep } = staking; const { isProfilePage, isSettingsPage } = profile; const { displayAppUpdateOverlay } = appUpdate; @@ -43,7 +45,9 @@ export default class Root extends Component { const isWalletImportDialogOpen = uiDialogs.isOpen(WalletImportFileDialog); const isPageThatDoesntNeedWallets = - (isStakingPage || isSettingsPage) && hasLoadedWallets && isConnected; + (isStakingPage || isSettingsPage || isVotingPage) && + hasLoadedWallets && + isConnected; // In case node is in stopping sequence we must show the "Connecting" screen // with the "Stopping Cardano node..." and "Cardano node stopped" messages diff --git a/source/renderer/app/containers/notifications/NotificationsContainer.js b/source/renderer/app/containers/notifications/NotificationsContainer.js index 3dea6f8f9d..76b79115c7 100644 --- a/source/renderer/app/containers/notifications/NotificationsContainer.js +++ b/source/renderer/app/containers/notifications/NotificationsContainer.js @@ -61,6 +61,12 @@ const messages = defineMessages({ description: 'Notification for the wallet address PDF download success in the Wallet Receive page.', }, + downloadVotingPDFSuccess: { + id: 'notification.downloadVotingPDFSuccess', + defaultMessage: '!!!PDF successfully downloaded', + description: + 'Notification for the wallet voting PDF download success in the Voting Registration dialog.', + }, downloadQRCodeImageSuccess: { id: 'notification.downloadQRCodeImageSuccess', defaultMessage: @@ -122,6 +128,11 @@ export default class NotificationsContainer extends Component { .generateAddressPDFSuccess, actionToListenAndClose: this.props.actions.wallets.generateAddressPDF, }, + { + id: 'downloadVotingPDFSuccess', + actionToListenAndOpen: this.props.actions.voting.saveAsPDFSuccess, + actionToListenAndClose: this.props.actions.voting.saveAsPDF, + }, { id: 'downloadQRCodeImageSuccess', actionToListenAndOpen: this.props.actions.wallets.saveQRCodeImageSuccess, diff --git a/source/renderer/app/containers/profile/InitialSettingsPage.js b/source/renderer/app/containers/profile/InitialSettingsPage.js index 1db67991d2..cab493c7e6 100644 --- a/source/renderer/app/containers/profile/InitialSettingsPage.js +++ b/source/renderer/app/containers/profile/InitialSettingsPage.js @@ -23,8 +23,13 @@ export default class InitialSettingsPage extends Component { const { updateUserLocalSetting } = actions.profile; updateUserLocalSetting.trigger({ param, value }); const { isUpdateAvailable } = stores.appUpdate; + const { areTermsOfUseAccepted: isNavigationEnabled } = stores.profile; + if (param === 'locale') { - await rebuildApplicationMenu.send({ isUpdateAvailable }); + await rebuildApplicationMenu.send({ + isUpdateAvailable, + isNavigationEnabled, + }); } }; diff --git a/source/renderer/app/containers/settings/Settings.js b/source/renderer/app/containers/settings/Settings.js index 20a190eabd..9b143384d0 100644 --- a/source/renderer/app/containers/settings/Settings.js +++ b/source/renderer/app/containers/settings/Settings.js @@ -27,11 +27,16 @@ export default class Settings extends Component { render() { const { isFlight } = global; - const { actions, children, stores } = this.props; - const { location } = stores.router; + const { actions, stores, children } = this.props; + const { networkStatus, app, router } = stores; + const { isSynced } = networkStatus; + const { currentRoute } = app; + const { location } = router; const menu = ( actions.router.goToRoute.trigger({ route })} isActiveItem={this.isActivePage} /> diff --git a/source/renderer/app/containers/settings/categories/GeneralSettingsPage.js b/source/renderer/app/containers/settings/categories/GeneralSettingsPage.js index bd5387e7e3..82abd5c216 100644 --- a/source/renderer/app/containers/settings/categories/GeneralSettingsPage.js +++ b/source/renderer/app/containers/settings/categories/GeneralSettingsPage.js @@ -13,10 +13,15 @@ export default class GeneralSettingsPage extends Component { handleSelectItem = async (param: string, value: string) => { const { actions, stores } = this.props; const { isUpdateAvailable } = stores.appUpdate; + const { areTermsOfUseAccepted: isNavigationEnabled } = stores.profile; const { updateUserLocalSetting } = actions.profile; + updateUserLocalSetting.trigger({ param, value }); if (param === 'locale') { - await rebuildApplicationMenu.send({ isUpdateAvailable }); + await rebuildApplicationMenu.send({ + isUpdateAvailable, + isNavigationEnabled, + }); } }; diff --git a/source/renderer/app/containers/settings/categories/StakePoolsSettingsPage.js b/source/renderer/app/containers/settings/categories/StakePoolsSettingsPage.js new file mode 100644 index 0000000000..41d24f4665 --- /dev/null +++ b/source/renderer/app/containers/settings/categories/StakePoolsSettingsPage.js @@ -0,0 +1,38 @@ +// @flow +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import StakePoolsSettings from '../../../components/settings/categories/StakePoolsSettings'; +import type { InjectedProps } from '../../../types/injectedPropsType'; + +@inject('stores', 'actions') +@observer +export default class StakePoolsSettingsPage extends Component { + static defaultProps = { actions: null, stores: null }; + + handleSelectSmashServerUrl = (smashServerUrl: string) => { + this.props.actions.staking.selectSmashServerUrl.trigger({ smashServerUrl }); + }; + + render() { + const { stores, actions } = this.props; + const { + smashServerUrl, + smashServerUrlError, + smashServerLoading, + } = stores.staking; + const { openExternalLink } = stores.app; + const { resetSmashServerError } = actions.staking; + // If `smashServerUrl` is null, waits for it to be set + if (!smashServerUrl) return false; + return ( + + ); + } +} diff --git a/source/renderer/app/containers/settings/categories/WalletsSettingsPage.js b/source/renderer/app/containers/settings/categories/WalletsSettingsPage.js new file mode 100644 index 0000000000..7916bbb014 --- /dev/null +++ b/source/renderer/app/containers/settings/categories/WalletsSettingsPage.js @@ -0,0 +1,39 @@ +// @flow +import React, { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import WalletsSettings from '../../../components/settings/categories/WalletsSettings'; +import type { InjectedProps } from '../../../types/injectedPropsType'; + +@inject('stores', 'actions') +@observer +export default class WalletsSettingsPage extends Component { + static defaultProps = { actions: null, stores: null }; + + handleSelectCurrency = (currencySymbol: string) => + this.props.actions.wallets.setCurrencySelected.trigger({ currencySymbol }); + + handleToggleCurrencyIsActive = () => + this.props.actions.wallets.toggleCurrencyIsActive.trigger(); + + render() { + const { stores } = this.props; + const { + currencySelected, + currencyRate, + currencyList, + currencyIsActive, + } = stores.wallets; + const { openExternalLink } = stores.app; + return ( + + ); + } +} diff --git a/source/renderer/app/containers/staking/StakePoolsListPage.js b/source/renderer/app/containers/staking/StakePoolsListPage.js index 8cc1015fb3..895ab08d4d 100644 --- a/source/renderer/app/containers/staking/StakePoolsListPage.js +++ b/source/renderer/app/containers/staking/StakePoolsListPage.js @@ -5,6 +5,7 @@ import StakePools from '../../components/staking/stake-pools/StakePools'; import StakePoolsRankingLoader from '../../components/staking/stake-pools/StakePoolsRankingLoader'; import DelegationSetupWizardDialogContainer from './dialogs/DelegationSetupWizardDialogContainer'; import DelegationSetupWizardDialog from '../../components/staking/delegation-setup-wizard/DelegationSetupWizardDialog'; +import { ROUTES } from '../../routes-config'; import type { InjectedProps } from '../../types/injectedPropsType'; type Props = InjectedProps; @@ -38,6 +39,12 @@ export default class StakePoolsListPage extends Component { stakingActions.rankStakePools.trigger(); }; + handleSmashSettingsClick = () => { + this.props.actions.router.goToRoute.trigger({ + route: ROUTES.SETTINGS.STAKE_POOLS, + }); + }; + render() { const { uiDialogs, @@ -57,11 +64,12 @@ export default class StakePoolsListPage extends Component { fetchingStakePoolsFailed, recentStakePools, getStakePoolById, + smashServerUrl, maxDelegationFunds, + isFetchingStakePools, } = staking; const { all } = wallets; - const isLoading = - !isSynced || fetchingStakePoolsFailed || stakePools.length === 0; + const isLoading = !isSynced || fetchingStakePoolsFailed; const isRanking = !isLoading && staking.isRanking && stakePoolsRequest.isExecuting; @@ -80,8 +88,11 @@ export default class StakePoolsListPage extends Component { stake={stake} onDelegate={this.handleDelegate} isLoading={isLoading} + isFetching={isFetchingStakePools} isRanking={isRanking} getStakePoolById={getStakePoolById} + smashServerUrl={smashServerUrl} + onSmashSettingsClick={this.handleSmashSettingsClick} maxDelegationFunds={maxDelegationFunds} /> {isRanking && } diff --git a/source/renderer/app/containers/staking/Staking.js b/source/renderer/app/containers/staking/Staking.js index 6865c0d687..5333271842 100644 --- a/source/renderer/app/containers/staking/Staking.js +++ b/source/renderer/app/containers/staking/Staking.js @@ -2,8 +2,12 @@ import React, { Component } from 'react'; import { observer, inject } from 'mobx-react'; import MainLayout from '../MainLayout'; +import VerticalFlexContainer from '../../components/layout/VerticalFlexContainer'; +import StakingUnavailable from '../../components/staking/StakingUnavailable'; import StakingWithNavigation from '../../components/staking/layouts/StakingWithNavigation'; import ExperimentalDataOverlay from '../../components/notifications/ExperimentalDataOverlay'; +import DelegationSetupWizardDialog from '../../components/staking/delegation-setup-wizard/DelegationSetupWizardDialog'; +import UndelegateConfirmationDialog from '../../components/staking/delegation-center/UndelegateConfirmationDialog'; import { ROUTES } from '../../routes-config'; import { buildRoute } from '../../utils/routing'; import type { InjectedContainerProps } from '../../types/injectedPropsType'; @@ -75,12 +79,31 @@ export default class Staking extends Component { render() { const { - stores: { app, staking }, + stores: { app, staking, networkStatus, uiDialogs }, children, } = this.props; const { isIncentivizedTestnet } = global; + const { isSynced, syncPercentage } = networkStatus; const { isStakingExperimentRead, isStakingDelegationCountdown } = staking; + const isDelegationWizardOpen = uiDialogs.isOpen( + DelegationSetupWizardDialog + ); + + const isUndelegationWizardOpen = uiDialogs.isOpen( + UndelegateConfirmationDialog + ); + + if (!isSynced && !(isDelegationWizardOpen || isUndelegationWizardOpen)) { + return ( + + + + + + ); + } + return ( {isIncentivizedTestnet && !isStakingExperimentRead && ( diff --git a/source/renderer/app/containers/staking/StakingRewardsPage.js b/source/renderer/app/containers/staking/StakingRewardsPage.js index 9b6ce9588d..2c3a7b41ed 100644 --- a/source/renderer/app/containers/staking/StakingRewardsPage.js +++ b/source/renderer/app/containers/staking/StakingRewardsPage.js @@ -39,11 +39,17 @@ export default class StakingRewardsPage extends Component { wallets, } = this.props.stores; const { isIncentivizedTestnet, isShelleyTestnet } = global; - const { isMainnet, isTestnet, isTest } = networkStatus.environment; + const { + isMainnet, + isStaging, + isTestnet, + isTest, + } = networkStatus.environment; const { requestCSVFile } = this.props.actions.staking; if ( isMainnet || + isStaging || isTestnet || isIncentivizedTestnet || isShelleyTestnet || diff --git a/source/renderer/app/containers/staking/dialogs/DelegationSetupWizardDialogContainer.js b/source/renderer/app/containers/staking/dialogs/DelegationSetupWizardDialogContainer.js index fe1ea0a06a..8b7b6f6915 100644 --- a/source/renderer/app/containers/staking/dialogs/DelegationSetupWizardDialogContainer.js +++ b/source/renderer/app/containers/staking/dialogs/DelegationSetupWizardDialogContainer.js @@ -10,6 +10,7 @@ import { RECENT_STAKE_POOLS_COUNT, DELEGATION_ACTIONS, } from '../../../config/stakingConfig'; +import type { DelegationCalculateFeeResponse } from '../../../api/staking/types'; import type { InjectedDialogContainerProps } from '../../../types/injectedPropsType'; const messages = defineMessages({ @@ -40,7 +41,7 @@ type State = { activeStep: number, selectedWalletId: string, selectedPoolId: string, - stakePoolJoinFee: ?BigNumber, + stakePoolJoinFee: ?DelegationCalculateFeeResponse, }; type Props = InjectedDialogContainerProps; @@ -62,13 +63,26 @@ export default class DelegationSetupWizardDialogContainer extends Component< onClose: () => {}, }; + // We need to track the mounted state in order to avoid calling + // setState promise handling code after the component was already unmounted: + // Read more: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html + _isMounted = false; + + componentDidMount() { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; + } + handleIsWalletAcceptable = ( walletAmount?: BigNumber, walletReward?: BigNumber = 0 ) => walletAmount && walletAmount.gte(new BigNumber(MIN_DELEGATION_FUNDS)) && - !walletAmount.equals(walletReward); + !walletAmount.isEqualTo(walletReward); get selectedWalletId() { return get( @@ -247,7 +261,11 @@ export default class DelegationSetupWizardDialogContainer extends Component< poolId, delegationAction: DELEGATION_ACTIONS.JOIN, }); - stakePoolJoinFee = coinsSelection.feeWithDelegationDeposit; + const { feeWithDeposits, fee } = coinsSelection; + stakePoolJoinFee = { + fee, + deposit: feeWithDeposits.minus(fee), + }; // Initiate Transaction (Delegation) hardwareWallets.initiateTransaction({ walletId: selectedWalletId }); } else { @@ -256,9 +274,13 @@ export default class DelegationSetupWizardDialogContainer extends Component< }); } - // Update state only if DelegationSetupWizardDialog is still active + // Update state only if DelegationSetupWizardDialog is still mounted and active // and fee calculation was successful - if (isOpen(DelegationSetupWizardDialog) && stakePoolJoinFee) { + if ( + this._isMounted && + isOpen(DelegationSetupWizardDialog) && + stakePoolJoinFee + ) { this.setState({ stakePoolJoinFee }); } } diff --git a/source/renderer/app/containers/staking/dialogs/redeem-itn-rewards/Step1ConfigurationContainer.js b/source/renderer/app/containers/staking/dialogs/redeem-itn-rewards/Step1ConfigurationContainer.js index 1934fc43e5..7326b01f3b 100644 --- a/source/renderer/app/containers/staking/dialogs/redeem-itn-rewards/Step1ConfigurationContainer.js +++ b/source/renderer/app/containers/staking/dialogs/redeem-itn-rewards/Step1ConfigurationContainer.js @@ -45,14 +45,19 @@ export default class Step1ConfigurationContainer extends Component { }; render() { - const { onClose, onBack, stores, actions } = this.props; - const { wallets, staking } = stores; + const { actions, stores, onBack, onClose } = this.props; + const { app, staking, wallets } = stores; const { allWallets } = wallets; const { redeemWallet, isCalculatingReedemFees, redeemRecoveryPhrase, } = staking; + const { openExternalLink } = app; + const { + onConfigurationContinue, + onCalculateRedeemWalletFees, + } = actions.staking; const selectedWalletId = get(redeemWallet, 'id', null); const selectedWallet: ?Wallet = allWallets.find( @@ -63,14 +68,10 @@ export default class Step1ConfigurationContainer extends Component { if (selectedWallet && !this.onWalletAcceptable(amount)) { // Wallet is restoring if (isRestoring) errorMessage = messages.errorRestoringWallet; - // Wallet balance < min delegation funds + // Wallet balance < min rewards redemption funds else errorMessage = messages.errorMinRewardFunds; } - const { openExternalLink } = stores.app; - const { - onConfigurationContinue, - onCalculateRedeemWalletFees, - } = actions.staking; + return ( { redeemWallet, transactionFees, redeemedRewards, - stakingSuccess, + redeemSuccess, } = stores.staking; const { onResultContinue } = actions.staking; if (!redeemWallet) throw new Error('Redeem wallet required'); - if (stakingSuccess) { + if (redeemSuccess) { return ( { + static defaultProps = { actions: null, stores: null }; + + handleGoToCreateWalletClick = () => { + this.props.actions.router.goToRoute.trigger({ route: ROUTES.WALLETS.ADD }); + }; + + render() { + const { actions, stores } = this.props; + const { app, networkStatus, uiDialogs, wallets } = stores; + const { openExternalLink } = app; + const { isSynced, syncPercentage } = networkStatus; + + const isVotingRegistrationDialogOpen = uiDialogs.isOpen( + VotingRegistrationDialog + ); + + if (!isSynced && !isVotingRegistrationDialogOpen) { + return ( + + + + + + ); + } + + if (!wallets.allWallets.length) { + return ( + + + + ); + } + + return ( + + + + actions.dialogs.open.trigger({ + dialog: VotingRegistrationDialog, + }) + } + onExternalLinkClick={openExternalLink} + /> + + + {isVotingRegistrationDialogOpen && ( + + )} + + ); + } +} diff --git a/source/renderer/app/containers/voting/dialogs/VotingRegistrationDialogContainer.js b/source/renderer/app/containers/voting/dialogs/VotingRegistrationDialogContainer.js new file mode 100644 index 0000000000..715a7485bd --- /dev/null +++ b/source/renderer/app/containers/voting/dialogs/VotingRegistrationDialogContainer.js @@ -0,0 +1,274 @@ +// @flow +import React, { Component } from 'react'; +import type { Node } from 'react'; +import { observer, inject } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +import { find, get } from 'lodash'; +import BigNumber from 'bignumber.js'; +import { + VOTING_REGISTRATION_MIN_WALLET_FUNDS, + VOTING_REGISTRATION_FEE_CALCULATION_AMOUNT, +} from '../../../config/votingConfig'; +import VotingRegistrationDialogWizard from '../../../components/voting/VotingRegistrationDialogWizard'; +import ConfirmationDialog from '../../../components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog'; +import { FormattedHTMLMessageWithLink } from '../../../components/widgets/FormattedHTMLMessageWithLink'; +import { formattedAmountToLovelace } from '../../../utils/formatters'; +import type { InjectedDialogContainerProps } from '../../../types/injectedPropsType'; + +const messages = defineMessages({ + votingRegistrationStep1Label: { + id: 'voting.votingRegistration.steps.step.1.label', + defaultMessage: '!!!Wallet', + description: 'Step 1 label text on voting registration.', + }, + votingRegistrationStep2Label: { + id: 'voting.votingRegistration.steps.step.2.label', + defaultMessage: '!!!Register', + description: 'Step 2 label text on voting registration.', + }, + votingRegistrationStep3Label: { + id: 'voting.votingRegistration.steps.step.3.label', + defaultMessage: '!!!Confirm', + description: 'Step 3 label text on voting registration.', + }, + votingRegistrationStep4Label: { + id: 'voting.votingRegistration.steps.step.4.label', + defaultMessage: '!!!PIN', + description: 'Step 4 label text on voting registration.', + }, + votingRegistrationStep5Label: { + id: 'voting.votingRegistration.steps.step.5.label', + defaultMessage: 'QR code', + description: 'Step 5 label text on voting registration.', + }, +}); + +type Props = InjectedDialogContainerProps; + +type State = { + selectedWalletId: string, + transactionFee: BigNumber, + transactionFeeError: string | Node | null, +}; + +@inject('stores', 'actions') +@observer +export default class VotingRegistrationDialogContainer extends Component< + Props, + State +> { + static contextTypes = { + intl: intlShape.isRequired, + }; + + static defaultProps = { + actions: null, + stores: null, + children: null, + onClose: () => {}, + }; + + // We need to track the mounted state in order to avoid calling + // setState promise handling code after the component was already unmounted: + // Read more: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html + _isMounted = false; + + componentDidMount() { + this._isMounted = true; + } + + componentWillUnmount() { + this._isMounted = false; + this.props.actions.voting.resetRegistration.trigger(); + } + + handleIsWalletAcceptable = ( + isLegacy?: boolean, + isHardwareWallet?: boolean, + isRestoring?: boolean, + walletAmount?: BigNumber, + walletReward?: BigNumber = 0 + ) => + !isLegacy && + !isHardwareWallet && + !isRestoring && + walletAmount && + walletAmount.gte(new BigNumber(VOTING_REGISTRATION_MIN_WALLET_FUNDS)) && + !walletAmount.isEqualTo(walletReward); + + get selectedWalletId() { + return get(this.props, ['stores', 'voting', 'selectedWalletId'], null); + } + + state = { + selectedWalletId: this.selectedWalletId, + transactionFee: null, + transactionFeeError: null, + }; + + STEPS_LIST = [ + this.context.intl.formatMessage(messages.votingRegistrationStep1Label), + this.context.intl.formatMessage(messages.votingRegistrationStep2Label), + this.context.intl.formatMessage(messages.votingRegistrationStep3Label), + this.context.intl.formatMessage(messages.votingRegistrationStep4Label), + this.context.intl.formatMessage(messages.votingRegistrationStep5Label), + ]; + + handleClose = (showConfirmationDialog?: boolean) => { + if (showConfirmationDialog) { + this.props.actions.voting.showConfirmationDialog.trigger(); + } else { + this.props.actions.dialogs.closeActiveDialog.trigger(); + } + }; + + handleRestart = () => { + this.props.actions.voting.resetRegistration.trigger(); + }; + + handleContinue = () => { + this.props.actions.voting.nextRegistrationStep.trigger(); + }; + + handleBack = () => { + this.props.actions.voting.previousRegistrationStep.trigger(); + }; + + handleSelectWallet = (walletId: string) => { + this.setState({ selectedWalletId: walletId }); + this.props.actions.voting.selectWallet.trigger(walletId); + this._handleCalculateTransactionFee(); + this.handleContinue(); + }; + + handleSetPinCode = (code: number) => { + this.props.actions.voting.generateQrCode.trigger(code); + }; + + handleSendTransaction = (spendingPassword: string) => { + const amount = formattedAmountToLovelace( + `${VOTING_REGISTRATION_FEE_CALCULATION_AMOUNT}` + ); + this.props.actions.voting.sendTransaction.trigger({ + amount, + passphrase: spendingPassword, + }); + }; + + render() { + const { + selectedWalletId, + transactionFee, + transactionFeeError, + } = this.state; + const { wallets, staking, voting, app } = this.props.stores; + const { closeConfirmationDialog, saveAsPDF } = this.props.actions.voting; + const { all } = wallets; + const { stakePools, getStakePoolById } = staking; + const { + isConfirmationDialogOpen, + registrationStep, + getWalletPublicKeyRequest, + createVotingRegistrationTransactionRequest, + signMetadataRequest, + isTransactionPending, + isTransactionConfirmed, + transactionConfirmations, + qrCode, + } = voting; + const { openExternalLink } = app; + + const selectedWallet = find( + all, + (wallet) => wallet.id === selectedWalletId + ); + + return ( + <> + + {isConfirmationDialogOpen && ( + { + this.props.actions.dialogs.closeActiveDialog.trigger(); + }} + /> + )} + + ); + } + + async _handleCalculateTransactionFee() { + const { transactions, addresses, app } = this.props.stores; + const { calculateTransactionFee } = transactions; + const { getAddressesByWalletId } = addresses; + const amount = formattedAmountToLovelace( + `${VOTING_REGISTRATION_FEE_CALCULATION_AMOUNT}` + ); + this.setState({ + transactionFee: null, + transactionFeeError: null, + }); + try { + const [address] = await getAddressesByWalletId(this.selectedWalletId); + + const fee = await calculateTransactionFee({ + walletId: this.selectedWalletId, + address: address.id, + amount, + }); + if (this._isMounted) { + this.setState({ + transactionFee: fee, + transactionFeeError: null, + }); + } + } catch (error) { + const errorHasLink = !!get(error, ['values', 'linkLabel']); + const transactionFeeError = errorHasLink ? ( + + ) : ( + this.context.intl.formatMessage(error) + ); + + if (this._isMounted) { + this.setState({ + transactionFee: new BigNumber(0), + transactionFeeError, + }); + } + } + } +} diff --git a/source/renderer/app/containers/wallet/WalletSettingsPage.js b/source/renderer/app/containers/wallet/WalletSettingsPage.js index d952e241b4..6690ee6e68 100644 --- a/source/renderer/app/containers/wallet/WalletSettingsPage.js +++ b/source/renderer/app/containers/wallet/WalletSettingsPage.js @@ -124,7 +124,7 @@ export default class WalletSettingsPage extends Component { } onStartEditing={(field) => startEditingWalletField.trigger({ field })} onStopEditing={stopEditingWalletField.trigger} - onCancelEditing={cancelEditingWalletField.trigger} + onCancel={cancelEditingWalletField.trigger} onVerifyRecoveryPhrase={recoveryPhraseVerificationContinue.trigger} onCopyWalletPublicKey={this.handleCopyWalletPublicKey} getWalletPublicKey={this.handleGetWalletPublicKey} diff --git a/source/renderer/app/containers/wallet/WalletSummaryPage.js b/source/renderer/app/containers/wallet/WalletSummaryPage.js index e19b527913..3e71cef3f8 100755 --- a/source/renderer/app/containers/wallet/WalletSummaryPage.js +++ b/source/renderer/app/containers/wallet/WalletSummaryPage.js @@ -40,6 +40,12 @@ export default class WalletSummaryPage extends Component { }); }; + handleCurrencySettingsClick = () => { + this.props.actions.router.goToRoute.trigger({ + route: ROUTES.SETTINGS.WALLETS, + }); + }; + render() { const { intl } = this.context; const { stores } = this.props; @@ -57,7 +63,15 @@ export default class WalletSummaryPage extends Component { deleteTransactionRequest, pendingTransactionsCount, } = transactions; - const wallet = wallets.active; + const { + active: wallet, + currencyIsActive, + currencyIsAvailable, + currencyIsFetchingRate, + currencyLastFetched, + currencyRate, + currencySelected, + } = wallets; const { currentTimeFormat, currentDateFormat, currentLocale } = profile; // Guard against potential null values if (!wallet) @@ -114,6 +128,13 @@ export default class WalletSummaryPage extends Component { numberOfTransactions={totalAvailable} numberOfPendingTransactions={pendingTransactionsCount} isLoadingTransactions={recentTransactionsRequest.isExecutingFirstTime} + currencyIsActive={currencyIsActive} + currencyIsAvailable={currencyIsAvailable} + currencyIsFetchingRate={currencyIsFetchingRate} + currencyLastFetched={currencyLastFetched} + currencyRate={currencyRate} + currencySelected={currencySelected} + onCurrencySettingClick={this.handleCurrencySettingsClick} /> {walletTransactions} diff --git a/source/renderer/app/containers/wallet/dialogs/WalletConnectDialogContainer.js b/source/renderer/app/containers/wallet/dialogs/WalletConnectDialogContainer.js index 58dbc6ce51..5a9ba082e0 100644 --- a/source/renderer/app/containers/wallet/dialogs/WalletConnectDialogContainer.js +++ b/source/renderer/app/containers/wallet/dialogs/WalletConnectDialogContainer.js @@ -26,6 +26,7 @@ export default class WalletConnectDialogContainer extends Component { const { hardwareWallets, wallets, app } = stores; const { hwDeviceStatus, transportDevice } = hardwareWallets; const { createHardwareWalletRequest } = wallets; + return ( = { PENDING: 'pending', OK: 'in_ledger', - IN_LEDGER: 'in_ledger', FAILED: 'expired', }; @@ -30,6 +29,8 @@ export class WalletTransaction { @observable type: TransactionType; @observable title: string = ''; @observable amount: BigNumber; + @observable fee: BigNumber; + @observable deposit: BigNumber; @observable date: ?Date; @observable description: string = ''; @observable addresses: TrasactionAddresses = { @@ -38,22 +39,26 @@ export class WalletTransaction { withdrawals: [], }; @observable state: TransactionState; - @observable depth: TransactionDepth; + @observable confirmations: number; @observable slotNumber: ?number; @observable epochNumber: ?number; + @observable metadata: ?TransactionMetadata; constructor(data: { id: string, type: TransactionType, title: string, amount: BigNumber, + fee: BigNumber, + deposit: BigNumber, date: ?Date, description: string, addresses: TrasactionAddresses, state: TransactionState, - depth: TransactionDepth, + confirmations: number, slotNumber: ?number, epochNumber: ?number, + metadata: ?TransactionMetadata, }) { Object.assign(this, data); } diff --git a/source/renderer/app/i18n/global-messages.js b/source/renderer/app/i18n/global-messages.js index 7819ffd0bb..8f24f0faa8 100644 --- a/source/renderer/app/i18n/global-messages.js +++ b/source/renderer/app/i18n/global-messages.js @@ -323,4 +323,11 @@ export default defineMessages({ defaultMessage: '!!!Copy', description: 'Copy label.', }, + featureUnavailableWhileSyncing: { + id: 'global.info.featureUnavailableWhileSyncing', + defaultMessage: + '!!!Daedalus is synchronizing with the Cardano blockchain, and the process is currently {syncPercentage}% complete. This feature will become available once Daedalus is fully synchronized.', + description: + 'Info message displayed for features which are unavailable while Daedalus is syncing', + }, }); diff --git a/source/renderer/app/i18n/locales/defaultMessages.json b/source/renderer/app/i18n/locales/defaultMessages.json index e2cc12dffa..7bdb32bf8d 100644 --- a/source/renderer/app/i18n/locales/defaultMessages.json +++ b/source/renderer/app/i18n/locales/defaultMessages.json @@ -271,6 +271,20 @@ "column": 20, "line": 102 } + }, + { + "defaultMessage": "!!!This URL is not a valid SMASH server", + "description": "\"This URL is not a valid SMASH server\" error message", + "end": { + "column": 3, + "line": 113 + }, + "file": "source/renderer/app/api/errors.js", + "id": "api.errors.invalidSmashServer", + "start": { + "column": 22, + "line": 109 + } } ], "path": "source/renderer/app/api/errors.json" @@ -1882,13 +1896,13 @@ "description": "Status message \"Wallet restore in progress\" shown while wallet is being restored.", "end": { "column": 3, - "line": 17 + "line": 18 }, "file": "source/renderer/app/components/notifications/RestoreNotification.js", "id": "wallet.statusMessages.activeRestore", "start": { "column": 24, - "line": 11 + "line": 12 } } ], @@ -2161,6 +2175,263 @@ ], "path": "source/renderer/app/components/settings/categories/DisplaySettings.json" }, + { + "descriptors": [ + { + "defaultMessage": "!!!The {link} is an off-chain metadata server that enables the fast loading of stake pool details. Stake pools are also curated and each server has a different curation policy.", + "description": "description for the Stake Pools settings page.", + "end": { + "column": 3, + "line": 34 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.description", + "start": { + "column": 15, + "line": 29 + } + }, + { + "defaultMessage": "!!!Stakepool Metadata Aggregation Server (SMASH)", + "description": "description for the Stake Pools settings page.", + "end": { + "column": 3, + "line": 39 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.descriptionLinkLabel", + "start": { + "column": 24, + "line": 35 + } + }, + { + "defaultMessage": "!!!https://iohk.io/en/blog/posts/2020/11/17/in-pools-we-trust/", + "description": "description for the Stake Pools settings page.", + "end": { + "column": 3, + "line": 45 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.descriptionLinkUrl", + "start": { + "column": 22, + "line": 40 + } + }, + { + "defaultMessage": "!!!The IOHK server ensures that registered stake pools are valid, helps to avoid duplicated ticker names or trademarks, and checks that the pools do not feature potentially offensive or harmful information.", + "description": "description for the Stake Pools settings page.", + "end": { + "column": 3, + "line": 51 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.descriptionIOHKContent1", + "start": { + "column": 27, + "line": 46 + } + }, + { + "defaultMessage": "!!!This allows us to deal with any scams, trolls, or abusive behavior by filtering out potentially problematic actors. {link} about the IOHK SMASH server.", + "description": "description for the Stake Pools settings page.", + "end": { + "column": 3, + "line": 57 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.descriptionIOHKContent2", + "start": { + "column": 27, + "line": 52 + } + }, + { + "defaultMessage": "!!!Read more", + "description": "description for the Stake Pools settings page.", + "end": { + "column": 3, + "line": 62 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.descriptionIOHKLinkLabel", + "start": { + "column": 28, + "line": 58 + } + }, + { + "defaultMessage": "!!!https://iohk.io/en/blog/posts/2020/11/17/in-pools-we-trust/", + "description": "description for the Stake Pools settings page.", + "end": { + "column": 3, + "line": 68 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.descriptionIOHKLinkUrl", + "start": { + "column": 26, + "line": 63 + } + }, + { + "defaultMessage": "!!!This option is not recommended! Without the off-chain metadata server your Daedalus client will fetch this data by contacting every stake pool individually, which is a very slow and resource-consuming process. The list of stake pools received is not curated, so Daedalus will receive legitimate pools, duplicates, and fake pools. An added risk to this process is that your antivirus or antimalware software could recognize the thousands of network requests as malicious behavior by the Daedalus client.", + "description": "description for the Stake Pools settings page.", + "end": { + "column": 3, + "line": 74 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.descriptionNone", + "start": { + "column": 19, + "line": 69 + } + }, + { + "defaultMessage": "!!!Off-chain metadata server (SMASH)", + "description": "smashSelectLabel for the \"Smash\" selection on the Stake Pools settings page.", + "end": { + "column": 3, + "line": 80 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.select.label", + "start": { + "column": 20, + "line": 75 + } + }, + { + "defaultMessage": "!!!IOHK (Recommended)", + "description": "smashSelectCustomServer option for the \"Smash\" selection on the Stake Pools settings page.", + "end": { + "column": 3, + "line": 86 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.select.IOHKServer", + "start": { + "column": 25, + "line": 81 + } + }, + { + "defaultMessage": "!!!None - let my Daedalus client fetch the data", + "description": "smashSelectCustomServer option for the \"Smash\" selection on the Stake Pools settings page.", + "end": { + "column": 3, + "line": 92 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.select.direct", + "start": { + "column": 21, + "line": 87 + } + }, + { + "defaultMessage": "!!!Custom server", + "description": "smashSelectCustomServer option for the \"Smash\" selection on the Stake Pools settings page.", + "end": { + "column": 3, + "line": 98 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smash.select.customServer", + "start": { + "column": 27, + "line": 93 + } + }, + { + "defaultMessage": "!!!SMASH server URL", + "description": "smashURLInputLabel for the \"Smash Custom Server\" selection on the Stake Pools settings page.", + "end": { + "column": 3, + "line": 104 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smashUrl.input.label", + "start": { + "column": 22, + "line": 99 + } + }, + { + "defaultMessage": "!!!Enter custom server URL", + "description": "smashUrlInputPlaceholder for the \"Smash Custom Server\" selection on the Stake Pools settings page.", + "end": { + "column": 3, + "line": 110 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smashUrl.input.placeholder", + "start": { + "column": 28, + "line": 105 + } + }, + { + "defaultMessage": "!!!Your changes have been saved", + "description": "Message \"Your changes have been saved\" for inline editing (eg. on Profile Settings page).", + "end": { + "column": 3, + "line": 116 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "inline.editing.input.changesSaved", + "start": { + "column": 16, + "line": 111 + } + }, + { + "defaultMessage": "!!!Invalid URL", + "description": "invalidUrl for the \"Smash Custom Server\" selection on the Stake Pools settings page.", + "end": { + "column": 3, + "line": 122 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smashUrl.input.invalidUrl", + "start": { + "column": 14, + "line": 117 + } + }, + { + "defaultMessage": "!!!The URL should start with \"https://\"", + "description": "invalidUrlPrefix for the \"Smash Custom Server\" selection on the Stake Pools settings page.", + "end": { + "column": 3, + "line": 128 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smashUrl.input.invalidUrlPrefix", + "start": { + "column": 20, + "line": 123 + } + }, + { + "defaultMessage": "!!!Only \"https://\" protocol and hostname (e.g. domain.com) are allowed", + "description": "invalidUrlParameter for the \"Smash Custom Server\" selection on the Stake Pools settings page.", + "end": { + "column": 3, + "line": 135 + }, + "file": "source/renderer/app/components/settings/categories/StakePoolsSettings.js", + "id": "settings.stakePools.smashUrl.input.invalidUrlParameter", + "start": { + "column": 23, + "line": 129 + } + } + ], + "path": "source/renderer/app/components/settings/categories/StakePoolsSettings.json" + }, { "descriptors": [ { @@ -2309,72 +2580,175 @@ { "descriptors": [ { - "defaultMessage": "!!!General", - "description": "Label for the \"General\" link in the settings menu.", + "defaultMessage": "!!!Display ada balances in other currency", + "description": "titleLabel for the Currency settings in the Wallets settings page.", "end": { "column": 3, + "line": 19 + }, + "file": "source/renderer/app/components/settings/categories/WalletsSettings.js", + "id": "settings.wallets.currency.titleLabel", + "start": { + "column": 22, "line": 14 + } + }, + { + "defaultMessage": "!!!Select a conversion currency for displaying your ada balances.", + "description": "currencyDescription for the Currency settings in the Wallets settings page.", + "end": { + "column": 3, + "line": 26 }, - "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", - "id": "settings.menu.general.link.label", + "file": "source/renderer/app/components/settings/categories/WalletsSettings.js", + "id": "settings.wallets.currency.description", "start": { - "column": 11, - "line": 10 + "column": 23, + "line": 20 } }, { - "defaultMessage": "!!!Support", - "description": "Label for the \"Support\" link in the settings menu.", + "defaultMessage": "!!!Select currency", + "description": "currencySelectLabel for the Currency settings in the Wallets settings page.", "end": { "column": 3, - "line": 19 + "line": 32 }, - "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", - "id": "settings.menu.support.link.label", + "file": "source/renderer/app/components/settings/categories/WalletsSettings.js", + "id": "settings.wallets.currency.selectLabel", "start": { - "column": 11, - "line": 15 + "column": 23, + "line": 27 } }, { - "defaultMessage": "!!!Terms of service", - "description": "Label for the \"Terms of service\" link in the settings menu.", + "defaultMessage": "!!!Conversion rates are provided by CoinGecko without any warranty. Please use the calculated conversion value only as a reference. Converted balances reflect the current global average price of ada on active cryptocurrency exchanges, as tracked by CoinGecko. Ada conversion is available only to fiat and cryptocurrencies that are supported by CoinGecko, other local currency conversions may not be available.", + "description": "currencyDisclaimer for the Currency settings in the Wallets settings page.", "end": { "column": 3, - "line": 24 + "line": 39 }, - "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", - "id": "settings.menu.termsOfUse.link.label", + "file": "source/renderer/app/components/settings/categories/WalletsSettings.js", + "id": "settings.wallets.currency.disclaimer", "start": { - "column": 14, - "line": 20 + "column": 22, + "line": 33 } }, { - "defaultMessage": "!!!Themes", - "description": "Label for the \"Themes\" link in the settings menu.", + "defaultMessage": "!!!Powered by", + "description": "currencyPoweredByLabel for the Currency settings in the Wallets settings page.", "end": { "column": 3, - "line": 29 + "line": 45 }, - "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", - "id": "settings.menu.display.link.label", + "file": "source/renderer/app/components/settings/categories/WalletsSettings.js", + "id": "settings.wallets.currency.poweredBy.label", "start": { - "column": 11, - "line": 25 + "column": 26, + "line": 40 } } ], - "path": "source/renderer/app/components/settings/menu/SettingsMenu.json" + "path": "source/renderer/app/components/settings/categories/WalletsSettings.json" }, { "descriptors": [ { - "defaultMessage": "!!!Mainnet vx", - "description": "Label for mainnet network with version.", + "defaultMessage": "!!!General", + "description": "Label for the \"General\" link in the settings menu.", "end": { "column": 3, - "line": 12 + "line": 14 + }, + "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", + "id": "settings.menu.general.link.label", + "start": { + "column": 11, + "line": 10 + } + }, + { + "defaultMessage": "!!!Wallets", + "description": "Label for the \"Wallets\" link in the settings menu.", + "end": { + "column": 3, + "line": 19 + }, + "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", + "id": "settings.menu.wallets.link.label", + "start": { + "column": 11, + "line": 15 + } + }, + { + "defaultMessage": "!!!Stake Pools", + "description": "Label for the \"Support\" link in the settings menu.", + "end": { + "column": 3, + "line": 24 + }, + "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", + "id": "settings.menu.stakePools.link.label", + "start": { + "column": 14, + "line": 20 + } + }, + { + "defaultMessage": "!!!Support", + "description": "Label for the \"Support\" link in the settings menu.", + "end": { + "column": 3, + "line": 29 + }, + "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", + "id": "settings.menu.support.link.label", + "start": { + "column": 11, + "line": 25 + } + }, + { + "defaultMessage": "!!!Terms of service", + "description": "Label for the \"Terms of service\" link in the settings menu.", + "end": { + "column": 3, + "line": 34 + }, + "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", + "id": "settings.menu.termsOfUse.link.label", + "start": { + "column": 14, + "line": 30 + } + }, + { + "defaultMessage": "!!!Themes", + "description": "Label for the \"Themes\" link in the settings menu.", + "end": { + "column": 3, + "line": 39 + }, + "file": "source/renderer/app/components/settings/menu/SettingsMenu.js", + "id": "settings.menu.display.link.label", + "start": { + "column": 11, + "line": 35 + } + } + ], + "path": "source/renderer/app/components/settings/menu/SettingsMenu.json" + }, + { + "descriptors": [ + { + "defaultMessage": "!!!Mainnet vx", + "description": "Label for mainnet network with version.", + "end": { + "column": 3, + "line": 12 }, "file": "source/renderer/app/components/sidebar/SidebarCategoryNetworkInfo.js", "id": "test.environment.mainnetLabel", @@ -3114,13 +3488,13 @@ "description": "\"No wallets\" headLine on the Delegation centre Page.", "end": { "column": 3, - "line": 16 + "line": 17 }, "file": "source/renderer/app/components/staking/delegation-center/DelegationCenterNoWallets.js", "id": "staking.delegationCenter.noWallets.headLine", "start": { "column": 12, - "line": 11 + "line": 12 } }, { @@ -3128,27 +3502,27 @@ "description": "\"No wallets\" instructions on the Delegation centre Page.", "end": { "column": 3, - "line": 22 + "line": 23 }, "file": "source/renderer/app/components/staking/delegation-center/DelegationCenterNoWallets.js", "id": "staking.delegationCenter.noWallets.instructions", "start": { "column": 16, - "line": 17 + "line": 18 } }, { - "defaultMessage": "!!!Create a wallet", + "defaultMessage": "!!!Create wallet", "description": "Label for \"Create New Wallet\" button on the Delegation centre Page.", "end": { "column": 3, - "line": 28 + "line": 29 }, "file": "source/renderer/app/components/staking/delegation-center/DelegationCenterNoWallets.js", "id": "staking.delegationCenter.noWallets.createWalletButtonLabel", "start": { "column": 27, - "line": 23 + "line": 24 } } ], @@ -3909,18 +4283,32 @@ "line": 62 } }, + { + "defaultMessage": "!!!Deposit", + "description": "Deposit label on the delegation setup \"confirmation\" step dialog.", + "end": { + "column": 3, + "line": 73 + }, + "file": "source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.js", + "id": "staking.delegationSetup.confirmation.step.dialog.depositLabel", + "start": { + "column": 16, + "line": 68 + } + }, { "defaultMessage": "!!!Spending password", "description": "Placeholder for \"spending password\"", "end": { "column": 3, - "line": 73 + "line": 79 }, "file": "source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.js", "id": "staking.delegationSetup.confirmation.step.dialog.spendingPasswordPlaceholder", "start": { "column": 31, - "line": 68 + "line": 74 } }, { @@ -3928,13 +4316,13 @@ "description": "Label for \"spending password\"", "end": { "column": 3, - "line": 79 + "line": 85 }, "file": "source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.js", "id": "staking.delegationSetup.confirmation.step.dialog.spendingPasswordLabel", "start": { "column": 25, - "line": 74 + "line": 80 } }, { @@ -3942,13 +4330,13 @@ "description": "Label for continue button on the delegation setup \"confirmation\" step dialog.", "end": { "column": 3, - "line": 85 + "line": 91 }, "file": "source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.js", "id": "staking.delegationSetup.confirmation.step.dialog.confirmButtonLabel", "start": { "column": 22, - "line": 80 + "line": 86 } }, { @@ -3956,27 +4344,41 @@ "description": "Label for \"Cancel\" button on the delegation setup \"confirmation\" step dialog.", "end": { "column": 3, - "line": 91 + "line": 97 }, "file": "source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.js", "id": "staking.delegationSetup.confirmation.step.dialog.cancelButtonLabel", "start": { "column": 21, - "line": 86 + "line": 92 } }, { "defaultMessage": "!!!Calculating fees", - "description": "\"Calculating fees\" message in the \"Undelegate\" dialog.", + "description": "\"Calculating fees\" message in the \"confirmation\" dialog.", "end": { "column": 3, - "line": 96 + "line": 102 }, "file": "source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.js", "id": "staking.delegationSetup.confirmation.step.dialog.calculatingFees", "start": { "column": 19, - "line": 92 + "line": 98 + } + }, + { + "defaultMessage": "!!!Calculating deposit", + "description": "\"Calculating deposit\" message in the \"confirmation\" dialog.", + "end": { + "column": 3, + "line": 107 + }, + "file": "source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.js", + "id": "staking.delegationSetup.confirmation.step.dialog.calculatingDeposit", + "start": { + "column": 22, + "line": 103 } } ], @@ -4557,27 +4959,13 @@ "description": "Title for Redeem Incentivized Testnet - redemptionUnavailable", "end": { "column": 3, - "line": 15 + "line": 18 }, "file": "source/renderer/app/components/staking/redeem-itn-rewards/RedemptionUnavailableDialog.js", "id": "staking.redeemItnRewards.redemptionUnavailable.title", "start": { "column": 9, - "line": 10 - } - }, - { - "defaultMessage": "!!!Before you can redeem your Incentivized Testnet rewards, Daedalus first needs to synchronize with the blockchain. The synchronization process is now underway and is currently {syncPercentage}% complete. As soon as this process is fully complete, you’ll be able to redeem your Incentivized Testnet rewards. Please wait for this process to complete before returning here to redeem your rewards.", - "description": "description for Redeem Incentivized Testnet - redemptionUnavailable", - "end": { - "column": 3, - "line": 22 - }, - "file": "source/renderer/app/components/staking/redeem-itn-rewards/RedemptionUnavailableDialog.js", - "id": "staking.redeemItnRewards.redemptionUnavailable.description", - "start": { - "column": 15, - "line": 16 + "line": 13 } }, { @@ -4585,13 +4973,13 @@ "description": "closeButtonLabel for Redeem Incentivized Testnet - redemptionUnavailable", "end": { "column": 3, - "line": 28 + "line": 24 }, "file": "source/renderer/app/components/staking/redeem-itn-rewards/RedemptionUnavailableDialog.js", "id": "staking.redeemItnRewards.redemptionUnavailable.closeButton.label", "start": { "column": 20, - "line": 23 + "line": 19 } } ], @@ -5100,13 +5488,13 @@ "description": "Title \"Earned delegation rewards\" label on the staking rewards page.", "end": { "column": 3, - "line": 24 + "line": 28 }, "file": "source/renderer/app/components/staking/rewards/StakingRewards.js", "id": "staking.rewards.title", "start": { "column": 9, - "line": 19 + "line": 23 } }, { @@ -5114,13 +5502,13 @@ "description": "Label for the \"Export CSV\" button on the staking rewards page.", "end": { "column": 3, - "line": 30 + "line": 34 }, "file": "source/renderer/app/components/staking/rewards/StakingRewards.js", "id": "staking.rewards.exportButtonLabel", "start": { "column": 21, - "line": 25 + "line": 29 } }, { @@ -5128,13 +5516,13 @@ "description": "\"No rewards\" rewards label on staking rewards page.", "end": { "column": 3, - "line": 35 + "line": 39 }, "file": "source/renderer/app/components/staking/rewards/StakingRewards.js", "id": "staking.rewards.no.rewards", "start": { "column": 13, - "line": 31 + "line": 35 } }, { @@ -5142,13 +5530,13 @@ "description": "Table header \"Date\" label on staking rewards page", "end": { "column": 3, - "line": 40 + "line": 44 }, "file": "source/renderer/app/components/staking/rewards/StakingRewards.js", "id": "staking.rewards.tableHeader.date", "start": { "column": 19, - "line": 36 + "line": 40 } }, { @@ -5156,13 +5544,13 @@ "description": "Table header \"Stake pool\" label on staking rewards page", "end": { "column": 3, - "line": 45 + "line": 49 }, "file": "source/renderer/app/components/staking/rewards/StakingRewards.js", "id": "staking.rewards.tableHeader.pool", "start": { "column": 19, - "line": 41 + "line": 45 } }, { @@ -5170,13 +5558,13 @@ "description": "Table header \"Wallet\" label on staking rewards page", "end": { "column": 3, - "line": 50 + "line": 54 }, "file": "source/renderer/app/components/staking/rewards/StakingRewards.js", "id": "staking.rewards.tableHeader.wallet", "start": { "column": 21, - "line": 46 + "line": 50 } }, { @@ -5184,13 +5572,13 @@ "description": "Table header \"Reward\" label on staking rewards page", "end": { "column": 3, - "line": 55 + "line": 59 }, "file": "source/renderer/app/components/staking/rewards/StakingRewards.js", "id": "staking.rewards.tableHeader.reward", "start": { "column": 21, - "line": 51 + "line": 55 } }, { @@ -5198,13 +5586,13 @@ "description": "Label for \"Learn more\" button on staking rewards page", "end": { "column": 3, - "line": 60 + "line": 64 }, "file": "source/renderer/app/components/staking/rewards/StakingRewards.js", "id": "staking.rewards.learnMore.ButtonLabel", "start": { "column": 24, - "line": 56 + "line": 60 } }, { @@ -5212,13 +5600,13 @@ "description": "Rewards description text on staking rewards page", "end": { "column": 3, - "line": 66 + "line": 70 }, "file": "source/renderer/app/components/staking/rewards/StakingRewards.js", "id": "staking.rewards.note", "start": { "column": 8, - "line": 61 + "line": 65 } } ], @@ -5231,13 +5619,13 @@ "description": "Title \"Earned delegation rewards\" label on the staking rewards page.", "end": { "column": 3, - "line": 27 + "line": 31 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.rewards.title", "start": { "column": 9, - "line": 22 + "line": 26 } }, { @@ -5245,13 +5633,13 @@ "description": "Filename prefix for the \"Export CSV\" on the staking rewards page.", "end": { "column": 3, - "line": 33 + "line": 37 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.rewards.csvFilenamePrefix", "start": { "column": 21, - "line": 28 + "line": 32 } }, { @@ -5259,13 +5647,13 @@ "description": "Label for the \"Export CSV\" button on the staking rewards page.", "end": { "column": 3, - "line": 39 + "line": 43 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.rewards.exportButtonLabel", "start": { "column": 21, - "line": 34 + "line": 38 } }, { @@ -5273,13 +5661,13 @@ "description": "\"No rewards\" rewards label on staking rewards page.", "end": { "column": 3, - "line": 44 + "line": 48 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.rewards.no.rewards", "start": { "column": 13, - "line": 40 + "line": 44 } }, { @@ -5287,13 +5675,13 @@ "description": "Table header \"Wallet\" label on staking rewards page", "end": { "column": 3, - "line": 49 + "line": 53 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.rewards.tableHeader.wallet", "start": { "column": 21, - "line": 45 + "line": 49 } }, { @@ -5301,13 +5689,13 @@ "description": "Table header \"Reward\" label on staking rewards page", "end": { "column": 3, - "line": 54 + "line": 58 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.rewards.tableHeader.reward", "start": { "column": 21, - "line": 50 + "line": 54 } }, { @@ -5315,13 +5703,13 @@ "description": "Table header \"Date\" label in exported csv file", "end": { "column": 3, - "line": 59 + "line": 63 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.rewards.tableHeader.date", "start": { "column": 19, - "line": 55 + "line": 59 } }, { @@ -5329,13 +5717,13 @@ "description": "Label for \"Learn more\" button on staking rewards page", "end": { "column": 3, - "line": 64 + "line": 68 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.rewards.learnMore.ButtonLabel", "start": { "column": 24, - "line": 60 + "line": 64 } }, { @@ -5343,13 +5731,13 @@ "description": "Rewards description text on staking rewards page", "end": { "column": 3, - "line": 70 + "line": 74 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.rewards.note", "start": { "column": 8, - "line": 65 + "line": 69 } }, { @@ -5357,13 +5745,13 @@ "description": "unknown stake pool label on staking rewards page.", "end": { "column": 3, - "line": 75 + "line": 79 }, "file": "source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js", "id": "staking.delegationCenter.syncingTooltipLabel", "start": { "column": 23, - "line": 71 + "line": 75 } } ], @@ -5376,55 +5764,111 @@ "description": "\"delegatingListTitle\" for the Stake Pools page.", "end": { "column": 3, - "line": 23 + "line": 31 }, "file": "source/renderer/app/components/staking/stake-pools/StakePools.js", "id": "staking.stakePools.delegatingListTitle", "start": { "column": 23, - "line": 19 + "line": 27 } }, { - "defaultMessage": "!!!Stake pools ({pools})", + "defaultMessage": "!!!Stake pools", "description": "\"listTitle\" for the Stake Pools page.", "end": { "column": 3, - "line": 28 + "line": 36 }, "file": "source/renderer/app/components/staking/stake-pools/StakePools.js", "id": "staking.stakePools.listTitle", "start": { "column": 13, - "line": 24 + "line": 32 } }, { - "defaultMessage": "!!!Stake pools. Search results: ({pools})", - "description": "\"listTitle\" for the Stake Pools page.", + "defaultMessage": "!!!Loading stake pools", + "description": "\"listTitleLoading\" for the Stake Pools page.", "end": { "column": 3, - "line": 33 + "line": 41 }, "file": "source/renderer/app/components/staking/stake-pools/StakePools.js", - "id": "staking.stakePools.listTitleWithSearch", + "id": "staking.stakePools.listTitleLoading", "start": { - "column": 23, - "line": 29 + "column": 20, + "line": 37 } }, { - "defaultMessage": "!!!Loading stake pools", - "description": "Loading stake pool message for the Delegation center body section.", + "defaultMessage": "!!!Stake pools. Search results:", + "description": "\"listTitleSearch\" for the Stake Pools page.", "end": { "column": 3, - "line": 39 + "line": 46 + }, + "file": "source/renderer/app/components/staking/stake-pools/StakePools.js", + "id": "staking.stakePools.listTitleSearch", + "start": { + "column": 19, + "line": 42 + } + }, + { + "defaultMessage": "!!!({pools})", + "description": "\"listTitleStakePools\" for the Stake Pools page.", + "end": { + "column": 3, + "line": 51 + }, + "file": "source/renderer/app/components/staking/stake-pools/StakePools.js", + "id": "staking.stakePools.listTitleStakePools", + "start": { + "column": 23, + "line": 47 + } + }, + { + "defaultMessage": "!!!Loading stake pools", + "description": "Loading stake pool message for the Delegation center body section.", + "end": { + "column": 3, + "line": 57 }, "file": "source/renderer/app/components/staking/stake-pools/StakePools.js", "id": "staking.stakePools.loadingStakePoolsMessage", "start": { "column": 28, - "line": 34 + "line": 52 + } + }, + { + "defaultMessage": "!!!Moderated by", + "description": "moderatedBy message for the Delegation center body section.", + "end": { + "column": 3, + "line": 62 + }, + "file": "source/renderer/app/components/staking/stake-pools/StakePools.js", + "id": "staking.stakePools.moderatedBy", + "start": { + "column": 15, + "line": 58 + } + }, + { + "defaultMessage": "!!!Unmoderated", + "description": "unmoderated message for the Delegation center body section.", + "end": { + "column": 3, + "line": 67 + }, + "file": "source/renderer/app/components/staking/stake-pools/StakePools.js", + "id": "staking.stakePools.unmoderated", + "start": { + "column": 15, + "line": 63 } } ], @@ -6386,13 +6830,13 @@ "description": "System info", "end": { "column": 3, - "line": 33 + "line": 37 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.system.info", "start": { "column": 14, - "line": 29 + "line": 33 } }, { @@ -6400,13 +6844,13 @@ "description": "Platform", "end": { "column": 3, - "line": 38 + "line": 42 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.platform", "start": { "column": 12, - "line": 34 + "line": 38 } }, { @@ -6414,13 +6858,13 @@ "description": "Platform version", "end": { "column": 3, - "line": 43 + "line": 47 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.platform.version", "start": { "column": 19, - "line": 39 + "line": 43 } }, { @@ -6428,13 +6872,13 @@ "description": "CPU", "end": { "column": 3, - "line": 48 + "line": 52 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cpu", "start": { "column": 7, - "line": 44 + "line": 48 } }, { @@ -6442,13 +6886,13 @@ "description": "RAM", "end": { "column": 3, - "line": 53 + "line": 57 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.ram", "start": { "column": 7, - "line": 49 + "line": 53 } }, { @@ -6456,13 +6900,13 @@ "description": "Available disk space", "end": { "column": 3, - "line": 58 + "line": 62 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.availableDiskSpace", "start": { "column": 22, - "line": 54 + "line": 58 } }, { @@ -6470,13 +6914,13 @@ "description": "Unknown amount of disk space", "end": { "column": 3, - "line": 63 + "line": 67 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.unknownDiskSpace", "start": { "column": 20, - "line": 59 + "line": 63 } }, { @@ -6484,13 +6928,13 @@ "description": "\"Support\" link URL while disk space is unknown", "end": { "column": 3, - "line": 68 + "line": 72 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.unknownDiskSpaceSupportUrl", "start": { "column": 30, - "line": 64 + "line": 68 } }, { @@ -6498,13 +6942,13 @@ "description": "CORE INFO", "end": { "column": 3, - "line": 73 + "line": 77 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.coreInfo", "start": { "column": 12, - "line": 69 + "line": 73 } }, { @@ -6512,13 +6956,13 @@ "description": "Daedalus version", "end": { "column": 3, - "line": 78 + "line": 82 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.daedalusVersion", "start": { "column": 19, - "line": 74 + "line": 78 } }, { @@ -6526,13 +6970,13 @@ "description": "Daedalus build number", "end": { "column": 3, - "line": 83 + "line": 87 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.daedalusBuildNumber", "start": { "column": 23, - "line": 79 + "line": 83 } }, { @@ -6540,13 +6984,13 @@ "description": "Daedalus main process ID", "end": { "column": 3, - "line": 88 + "line": 92 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.daedalusMainProcessID", "start": { "column": 25, - "line": 84 + "line": 88 } }, { @@ -6554,13 +6998,13 @@ "description": "Daedalus renderer process ID", "end": { "column": 3, - "line": 93 + "line": 97 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.daedalusProcessID", "start": { "column": 21, - "line": 89 + "line": 93 } }, { @@ -6568,13 +7012,13 @@ "description": "Daedalus 'Blank Screen Fix' active", "end": { "column": 3, - "line": 98 + "line": 102 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.blankScreenFix", "start": { "column": 18, - "line": 94 + "line": 98 } }, { @@ -6582,13 +7026,13 @@ "description": "Cardano node version", "end": { "column": 3, - "line": 103 + "line": 107 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoNodeVersion", "start": { "column": 22, - "line": 99 + "line": 103 } }, { @@ -6596,13 +7040,13 @@ "description": "Cardano node process ID", "end": { "column": 3, - "line": 108 + "line": 112 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoNodePID", "start": { "column": 18, - "line": 104 + "line": 108 } }, { @@ -6610,13 +7054,13 @@ "description": "Cardano node port", "end": { "column": 3, - "line": 113 + "line": 117 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoNodeApiPort", "start": { "column": 22, - "line": 109 + "line": 113 } }, { @@ -6624,13 +7068,13 @@ "description": "Cardano wallet process ID", "end": { "column": 3, - "line": 118 + "line": 122 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoWalletPID", "start": { "column": 20, - "line": 114 + "line": 118 } }, { @@ -6638,13 +7082,13 @@ "description": "Cardano wallet version", "end": { "column": 3, - "line": 123 + "line": 127 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoWalletVersion", "start": { "column": 24, - "line": 119 + "line": 123 } }, { @@ -6652,13 +7096,13 @@ "description": "Cardano wallet port", "end": { "column": 3, - "line": 128 + "line": 132 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoWalletApiPort", "start": { "column": 24, - "line": 124 + "line": 128 } }, { @@ -6666,13 +7110,13 @@ "description": "Cardano network", "end": { "column": 3, - "line": 133 + "line": 137 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoNetwork", "start": { "column": 18, - "line": 129 + "line": 133 } }, { @@ -6680,13 +7124,13 @@ "description": "Daedalus state directory", "end": { "column": 3, - "line": 138 + "line": 142 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.stateDirectory", "start": { "column": 22, - "line": 134 + "line": 138 } }, { @@ -6694,13 +7138,13 @@ "description": "Open", "end": { "column": 3, - "line": 143 + "line": 147 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.stateDirectoryPathOpenBtn", "start": { "column": 29, - "line": 139 + "line": 143 } }, { @@ -6708,13 +7152,13 @@ "description": "CONNECTION ERROR", "end": { "column": 3, - "line": 148 + "line": 152 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.connectionError", "start": { "column": 19, - "line": 144 + "line": 148 } }, { @@ -6722,13 +7166,13 @@ "description": "DAEDALUS STATUS", "end": { "column": 3, - "line": 153 + "line": 157 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.daedalusStatus", "start": { "column": 18, - "line": 149 + "line": 153 } }, { @@ -6736,13 +7180,13 @@ "description": "Connected", "end": { "column": 3, - "line": 158 + "line": 162 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.connected", "start": { "column": 13, - "line": 154 + "line": 158 } }, { @@ -6750,13 +7194,13 @@ "description": "Synced", "end": { "column": 3, - "line": 163 + "line": 167 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.synced", "start": { "column": 10, - "line": 159 + "line": 163 } }, { @@ -6764,13 +7208,13 @@ "description": "Sync percentage", "end": { "column": 3, - "line": 168 + "line": 172 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.syncPercentage", "start": { "column": 18, - "line": 164 + "line": 168 } }, { @@ -6778,13 +7222,13 @@ "description": "Local time difference", "end": { "column": 3, - "line": 173 + "line": 177 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.localTimeDifference", "start": { "column": 23, - "line": 169 + "line": 173 } }, { @@ -6792,13 +7236,13 @@ "description": "System time correct", "end": { "column": 3, - "line": 178 + "line": 182 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.systemTimeCorrect", "start": { "column": 21, - "line": 174 + "line": 178 } }, { @@ -6806,13 +7250,13 @@ "description": "System time ignored", "end": { "column": 3, - "line": 183 + "line": 187 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.systemTimeIgnored", "start": { "column": 21, - "line": 179 + "line": 183 } }, { @@ -6820,13 +7264,13 @@ "description": "Checking system time", "end": { "column": 3, - "line": 188 + "line": 192 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.checkingNodeTime", "start": { "column": 20, - "line": 184 + "line": 188 } }, { @@ -6834,13 +7278,13 @@ "description": "CARDANO NODE STATUS", "end": { "column": 3, - "line": 193 + "line": 197 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoNodeStatus", "start": { "column": 21, - "line": 189 + "line": 193 } }, { @@ -6848,13 +7292,13 @@ "description": "Restarting Cardano node...", "end": { "column": 3, - "line": 198 + "line": 202 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoNodeStatusRestarting", "start": { "column": 31, - "line": 194 + "line": 198 } }, { @@ -6862,13 +7306,13 @@ "description": "Restart Cardano node", "end": { "column": 3, - "line": 203 + "line": 207 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoNodeStatusRestart", "start": { "column": 28, - "line": 199 + "line": 203 } }, { @@ -6876,13 +7320,13 @@ "description": "Cardano node state", "end": { "column": 3, - "line": 208 + "line": 212 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.cardanoNodeState", "start": { "column": 20, - "line": 204 + "line": 208 } }, { @@ -6890,13 +7334,13 @@ "description": "Updated", "end": { "column": 3, - "line": 213 + "line": 217 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.nodeHasBeenUpdated", "start": { "column": 22, - "line": 209 + "line": 213 } }, { @@ -6904,13 +7348,13 @@ "description": "Crashed", "end": { "column": 3, - "line": 218 + "line": 222 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.nodeHasCrashed", "start": { "column": 18, - "line": 214 + "line": 218 } }, { @@ -6918,13 +7362,13 @@ "description": "Errored", "end": { "column": 3, - "line": 223 + "line": 227 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.nodeHasErrored", "start": { "column": 18, - "line": 219 + "line": 223 } }, { @@ -6932,13 +7376,13 @@ "description": "Stopped", "end": { "column": 3, - "line": 228 + "line": 232 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.nodeHasStopped", "start": { "column": 18, - "line": 224 + "line": 228 } }, { @@ -6946,13 +7390,13 @@ "description": "Exiting", "end": { "column": 3, - "line": 233 + "line": 237 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.nodeIsExiting", "start": { "column": 17, - "line": 229 + "line": 233 } }, { @@ -6960,13 +7404,13 @@ "description": "Running", "end": { "column": 3, - "line": 238 + "line": 242 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.nodeIsRunning", "start": { "column": 17, - "line": 234 + "line": 238 } }, { @@ -6974,13 +7418,13 @@ "description": "Starting", "end": { "column": 3, - "line": 243 + "line": 247 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.nodeIsStarting", "start": { "column": 18, - "line": 239 + "line": 243 } }, { @@ -6988,13 +7432,13 @@ "description": "Stopping", "end": { "column": 3, - "line": 248 + "line": 252 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.nodeIsStopping", "start": { "column": 18, - "line": 244 + "line": 248 } }, { @@ -7002,13 +7446,13 @@ "description": "Unrecoverable", "end": { "column": 3, - "line": 253 + "line": 257 }, "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", "id": "daedalus.diagnostics.dialog.nodeIsUnrecoverable", "start": { "column": 23, - "line": 249 + "line": 253 } }, { @@ -7016,241 +7460,1126 @@ "description": "Updating", "end": { "column": 3, + "line": 262 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.nodeIsUpdating", + "start": { + "column": 18, "line": 258 + } + }, + { + "defaultMessage": "!!!Cardano node responding", + "description": "Cardano node responding", + "end": { + "column": 3, + "line": 267 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.cardanoNodeResponding", + "start": { + "column": 25, + "line": 263 + } + }, + { + "defaultMessage": "!!!Cardano node subscribed", + "description": "Cardano node subscribed", + "end": { + "column": 3, + "line": 272 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.cardanoNodeSubscribed", + "start": { + "column": 25, + "line": 268 + } + }, + { + "defaultMessage": "!!!Cardano node time correct", + "description": "Cardano node time correct", + "end": { + "column": 3, + "line": 277 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.cardanoNodeTimeCorrect", + "start": { + "column": 26, + "line": 273 + } + }, + { + "defaultMessage": "!!!Cardano node syncing", + "description": "Cardano node syncing", + "end": { + "column": 3, + "line": 282 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.cardanoNodeSyncing", + "start": { + "column": 22, + "line": 278 + } + }, + { + "defaultMessage": "!!!Cardano node in sync", + "description": "Cardano node in sync", + "end": { + "column": 3, + "line": 287 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.cardanoNodeInSync", + "start": { + "column": 21, + "line": 283 + } + }, + { + "defaultMessage": "!!!Checking...", + "description": "Checking...", + "end": { + "column": 3, + "line": 292 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.localTimeDifferenceChecking", + "start": { + "column": 31, + "line": 288 + } + }, + { + "defaultMessage": "!!!Check time", + "description": "Check time", + "end": { + "column": 3, + "line": 297 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.localTimeDifferenceCheckTime", + "start": { + "column": 32, + "line": 293 + } + }, + { + "defaultMessage": "!!!YES", + "description": "YES", + "end": { + "column": 3, + "line": 302 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.statusOn", + "start": { + "column": 12, + "line": 298 + } + }, + { + "defaultMessage": "!!!NO", + "description": "NO", + "end": { + "column": 3, + "line": 307 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.statusOff", + "start": { + "column": 13, + "line": 303 + } + }, + { + "defaultMessage": "!!!NTP service unreachable", + "description": "NTP service unreachable", + "end": { + "column": 3, + "line": 312 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.serviceUnreachable", + "start": { + "column": 22, + "line": 308 + } + }, + { + "defaultMessage": "!!!message", + "description": "message", + "end": { + "column": 3, + "line": 317 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.message", + "start": { + "column": 11, + "line": 313 + } + }, + { + "defaultMessage": "!!!code", + "description": "code", + "end": { + "column": 3, + "line": 322 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.code", + "start": { + "column": 8, + "line": 318 + } + }, + { + "defaultMessage": "!!!Last network block", + "description": "Last network block", + "end": { + "column": 3, + "line": 327 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.lastNetworkBlock", + "start": { + "column": 20, + "line": 323 + } + }, + { + "defaultMessage": "!!!Last synchronized block", + "description": "Last synchronized block", + "end": { + "column": 3, + "line": 332 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.lastSynchronizedBlock", + "start": { + "column": 25, + "line": 328 + } + }, + { + "defaultMessage": "!!!epoch", + "description": "epoch", + "end": { + "column": 3, + "line": 337 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.epoch", + "start": { + "column": 9, + "line": 333 + } + }, + { + "defaultMessage": "!!!slot", + "description": "slot", + "end": { + "column": 3, + "line": 342 + }, + "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", + "id": "daedalus.diagnostics.dialog.slot", + "start": { + "column": 8, + "line": 338 + } + } + ], + "path": "source/renderer/app/components/status/DaedalusDiagnostics.json" + }, + { + "descriptors": [ + { + "defaultMessage": "!!!You can only use one wallet when registering. To maximize rewards and voting power, choose the wallet with the largest balance.", + "description": "Description on the voting registration \"choose wallet\" step.", + "end": { + "column": 3, + "line": 17 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.js", + "id": "voting.votingRegistration.chooseWallet.step.description", + "start": { + "column": 15, + "line": 12 + } + }, + { + "defaultMessage": "!!!Select a wallet", + "description": "Label \"Wallet\" for select input on the voting registration \"choose wallet\" step.", + "end": { + "column": 3, + "line": 23 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.js", + "id": "voting.votingRegistration.chooseWallet.step.selectWalletInputLabel", + "start": { + "column": 26, + "line": 18 + } + }, + { + "defaultMessage": "!!!Select a wallet", + "description": "Placeholder \"Select Wallet\" for select input on the voting registration \"choose wallet\" step.", + "end": { + "column": 3, + "line": 30 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.js", + "id": "voting.votingRegistration.chooseWallet.step.selectWalletInputPlaceholder", + "start": { + "column": 32, + "line": 24 + } + }, + { + "defaultMessage": "!!!This wallet does not contain the minimum required amount of {minVotingRegistrationFunds} ADA. Please select a different wallet with a minimum balance of {minVotingRegistrationFunds} ADA.", + "description": "errorMinVotingFunds Error Label on the voting registration \"choose wallet\" step.", + "end": { + "column": 3, + "line": 37 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.js", + "id": "voting.votingRegistration.chooseWallet.step.errorMinVotingFunds", + "start": { + "column": 23, + "line": 31 + } + }, + { + "defaultMessage": "!!!This wallet cannot be registered for voting as it contains rewards balance only.", + "description": "errorMinVotingFundsRewardsOnly Error Label on the voting registration \"choose wallet\" step.", + "end": { + "column": 3, + "line": 45 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.js", + "id": "voting.votingRegistration.chooseWallet.step.errorMinVotingFundsRewardsOnly", + "start": { + "column": 34, + "line": 38 + } + }, + { + "defaultMessage": "!!!This wallet cannot be registered for voting as it is a legacy Byron wallet.", + "description": "Byron wallet error message on the voting registration \"choose wallet\" step.", + "end": { + "column": 3, + "line": 52 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.js", + "id": "voting.votingRegistration.chooseWallet.step.errorLegacyWallet", + "start": { + "column": 21, + "line": 46 + } + }, + { + "defaultMessage": "!!!This wallet cannot be registered for voting as it is a hardware wallet. Hardware wallets will be supported in the future.", + "description": "Hardware wallet error message on the voting registration \"choose wallet\" step.", + "end": { + "column": 3, + "line": 59 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.js", + "id": "voting.votingRegistration.chooseWallet.step.errorHardwareWallet", + "start": { + "column": 23, + "line": 53 + } + }, + { + "defaultMessage": "!!!The wallet cannot be registered for voting while it is being synced with the blockchain.", + "description": "Restoring wallet error message on the voting registration \"choose wallet\" step.", + "end": { + "column": 3, + "line": 66 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.js", + "id": "voting.votingRegistration.chooseWallet.step.errorRestoringWallet", + "start": { + "column": 24, + "line": 60 + } + }, + { + "defaultMessage": "!!!Continue", + "description": "Label for continue button on the voting registration \"choose wallet\" step.", + "end": { + "column": 3, + "line": 72 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.js", + "id": "voting.votingRegistration.chooseWallet.step.continueButtonLabel", + "start": { + "column": 23, + "line": 67 + } + } + ], + "path": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet.json" + }, + { + "descriptors": [ + { + "defaultMessage": "!!!Confirmation of voting registration requires approximately 5 minutes. Please leave Daedalus running.", + "description": "Description voting registration \"confirm\" step.", + "end": { + "column": 3, + "line": 19 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.js", + "id": "voting.votingRegistration.confirm.step.description", + "start": { + "column": 15, + "line": 14 + } + }, + { + "defaultMessage": "!!!Please restart the voting registration process by clicking Restart voting registration.", + "description": "Message for restart voting registration on the voting registration \"confirm\" step.", + "end": { + "column": 3, + "line": 26 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.js", + "id": "voting.votingRegistration.confirm.step.descriptionRestart", + "start": { + "column": 22, + "line": 20 + } + }, + { + "defaultMessage": "!!!The voting registration process was not completed correctly.", + "description": "Error message on the voting registration \"confirm\" step.", + "end": { + "column": 3, + "line": 32 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.js", + "id": "voting.votingRegistration.confirm.step.errorMessage", + "start": { + "column": 16, + "line": 27 + } + }, + { + "defaultMessage": "!!!Continue", + "description": "Label for continue button on the voting registration \"confirm\" step.", + "end": { + "column": 3, + "line": 38 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.js", + "id": "voting.votingRegistration.confirm.step.continueButtonLabel", + "start": { + "column": 23, + "line": 33 + } + }, + { + "defaultMessage": "!!!Restart voting registration", + "description": "Label for restart button on the voting registration \"confirm\" step.", + "end": { + "column": 3, + "line": 44 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.js", + "id": "voting.votingRegistration.confirm.step.restartButtonLabel", + "start": { + "column": 22, + "line": 39 + } + }, + { + "defaultMessage": "!!!Transaction pending...", + "description": "Label for pending transaction state on the voting registration \"confirm\" step.", + "end": { + "column": 3, + "line": 50 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.js", + "id": "voting.votingRegistration.confirm.step.transactionPendingLabel", + "start": { + "column": 27, + "line": 45 + } + }, + { + "defaultMessage": "!!!Transaction confirmed", + "description": "Label for confirmed transaction state on the voting registration \"confirm\" step.", + "end": { + "column": 3, + "line": 56 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.js", + "id": "voting.votingRegistration.confirm.step.transactionConfirmedLabel", + "start": { + "column": 29, + "line": 51 + } + }, + { + "defaultMessage": "!!!Waiting for confirmation...", + "description": "Label for confirming transaction state on the voting registration \"confirm\" step.", + "end": { + "column": 3, + "line": 62 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.js", + "id": "voting.votingRegistration.confirm.step.waitingForConfirmationsLabel", + "start": { + "column": 32, + "line": 57 + } + }, + { + "defaultMessage": "!!!{currentCount} of {expectedCount}", + "description": "Label for number of confirmations on the voting registration \"confirm\" step.", + "end": { + "column": 3, + "line": 68 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.js", + "id": "voting.votingRegistration.confirm.step.confirmationsCountLabel", + "start": { + "column": 27, + "line": 63 + } + } + ], + "path": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm.json" + }, + { + "descriptors": [ + { + "defaultMessage": "!!!Please enter a PIN for your Fund3 voting registration. The PIN you set here, and the QR code which you will get in the next step, will be required for you to vote using the Catalyst Voting app on your smartphone.", + "description": "Description on the voting registration \"enter pin code\" step.", + "end": { + "column": 3, + "line": 24 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsEnterPinCode.js", + "id": "voting.votingRegistration.enterPinCode.step.description", + "start": { + "column": 15, + "line": 18 + } + }, + { + "defaultMessage": "!!!It is important to remember your PIN. If you forget your PIN, you will not be able to use this registration for voting, and you will need to repeat the registration process.", + "description": "Reminder on the voting registration \"enter pin code\" step.", + "end": { + "column": 3, + "line": 30 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsEnterPinCode.js", + "id": "voting.votingRegistration.enterPinCode.step.reminder", + "start": { + "column": 12, + "line": 25 + } + }, + { + "defaultMessage": "!!!Enter PIN", + "description": "Label for pin code input on the voting registration \"enter pin code\" step.", + "end": { + "column": 3, + "line": 36 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsEnterPinCode.js", + "id": "voting.votingRegistration.enterPinCode.step.enterPinCodeLabel", + "start": { + "column": 21, + "line": 31 + } + }, + { + "defaultMessage": "!!!Repeat PIN", + "description": "Label for repeat pin code on the voting registration \"enter pin code\" step.", + "end": { + "column": 3, + "line": 42 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsEnterPinCode.js", + "id": "voting.votingRegistration.enterPinCode.step.repeatPinCodeLabel", + "start": { + "column": 22, + "line": 37 + } + }, + { + "defaultMessage": "!!!Invalid PIN", + "description": "Error message shown when repeat pin code is invalid.", + "end": { + "column": 3, + "line": 47 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsEnterPinCode.js", + "id": "voting.votingRegistration.enterPinCode.step.errors.invalidPinCode", + "start": { + "column": 18, + "line": 43 + } + }, + { + "defaultMessage": "!!!PIN doesn’t match", + "description": "Error message shown when repeat pin code is invalid.", + "end": { + "column": 3, + "line": 53 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsEnterPinCode.js", + "id": "voting.votingRegistration.enterPinCode.step.errors.invalidRepeatPinCode", + "start": { + "column": 24, + "line": 48 + } + }, + { + "defaultMessage": "!!!Continue", + "description": "Label for continue button on the voting registration \"enter pin code\" step.", + "end": { + "column": 3, + "line": 59 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsEnterPinCode.js", + "id": "voting.votingRegistration.enterPinCode.step.continueButtonLabel", + "start": { + "column": 23, + "line": 54 + } + } + ], + "path": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsEnterPinCode.json" + }, + { + "descriptors": [ + { + "defaultMessage": "!!!Please complete your registration now.", + "description": "Qr code title on the voting registration \"qr code\" step.", + "end": { + "column": 3, + "line": 16 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsQrCode.js", + "id": "voting.votingRegistration.qrCode.step.qrCodeTitle", + "start": { + "column": 15, + "line": 12 + } + }, + { + "defaultMessage": "!!!Open the Catalyst Voting app on your smartphone, scan the QR code, and use the PIN to complete the voting registration process.", + "description": "Qr code description of use on the voting registration \"qr code\" step.", + "end": { + "column": 3, + "line": 23 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsQrCode.js", + "id": "voting.votingRegistration.qrCode.step.qrCodeDescription", + "start": { + "column": 21, + "line": 17 + } + }, + { + "defaultMessage": "!!!Warning: After closing this window the QR code will no longer be available. If you do not keep a PDF copy of the QR code, you might not be able to participate in voting.", + "description": "Qr code warning on the voting registration \"qr code\" step.", + "end": { + "column": 3, + "line": 29 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsQrCode.js", + "id": "voting.votingRegistration.qrCode.step.qrCodeWarning", + "start": { + "column": 17, + "line": 24 + } + }, + { + "defaultMessage": "!!!I understand that I will not be able to retrieve this QR code again after closing this window.", + "description": "First checkbox label on the voting registration \"qr code\" step.", + "end": { + "column": 3, + "line": 36 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsQrCode.js", + "id": "voting.votingRegistration.qrCode.step.checkbox1Label", + "start": { + "column": 18, + "line": 30 + } + }, + { + "defaultMessage": "!!!I acknowledge that I must have the downloaded PDF with the QR code, to vote with Fund3.", + "description": "Second checkbox label on the voting registration \"qr code\" step.", + "end": { + "column": 3, + "line": 43 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsQrCode.js", + "id": "voting.votingRegistration.qrCode.step.checkbox2Label", + "start": { + "column": 18, + "line": 37 + } + }, + { + "defaultMessage": "!!!Close", + "description": "\"Close\" button label on the voting registration \"qr code\" step.", + "end": { + "column": 3, + "line": 49 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsQrCode.js", + "id": "voting.votingRegistration.qrCode.step.closeButtonLabel", + "start": { + "column": 20, + "line": 44 + } + }, + { + "defaultMessage": "!!!Save as PDF", + "description": "\"Save as PDF\" button label on the voting registration \"qr code\" step.", + "end": { + "column": 3, + "line": 55 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsQrCode.js", + "id": "voting.votingRegistration.qrCode.step.saveAsPdfButtonLabel", + "start": { + "column": 24, + "line": 50 + } + } + ], + "path": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsQrCode.json" + }, + { + "descriptors": [ + { + "defaultMessage": "!!!Please sign the voting registration transaction. This transaction links your wallet balance with your Fund3 voting registration, as a proof of your voting power. Funds will not leave your wallet, but registration requires paying transaction fees, as displayed on-screen.", + "description": "Description on the voting registration \"sign\" step.", + "end": { + "column": 3, + "line": 27 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.js", + "id": "voting.votingRegistration.register.step.description", + "start": { + "column": 15, + "line": 22 + } + }, + { + "defaultMessage": "!!!Submit registration transaction", + "description": "Label for continue button on the voting registration \"sign\" step.", + "end": { + "column": 3, + "line": 33 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.js", + "id": "voting.votingRegistration.register.step.continueButtonLabel", + "start": { + "column": 23, + "line": 28 + } + }, + { + "defaultMessage": "!!!Fees", + "description": "Fees label on the voting registration \"sign\" step.", + "end": { + "column": 3, + "line": 38 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.js", + "id": "voting.votingRegistration.register.step.feesLabel", + "start": { + "column": 13, + "line": 34 + } + }, + { + "defaultMessage": "!!!Spending password", + "description": "Placeholder for \"spending password\"", + "end": { + "column": 3, + "line": 43 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.js", + "id": "voting.votingRegistration.register.step.spendingPasswordPlaceholder", + "start": { + "column": 31, + "line": 39 + } + }, + { + "defaultMessage": "!!!Spending password", + "description": "Label for \"spending password\"", + "end": { + "column": 3, + "line": 48 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.js", + "id": "voting.votingRegistration.register.step.spendingPasswordLabel", + "start": { + "column": 25, + "line": 44 + } + }, + { + "defaultMessage": "!!!Calculating fees", + "description": "\"Calculating fees\" message in the \"sign\" step.", + "end": { + "column": 3, + "line": 53 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.js", + "id": "voting.votingRegistration.register.step.calculatingFees", + "start": { + "column": 19, + "line": 49 + } + }, + { + "defaultMessage": "!!!Learn more", + "description": "\"Learn more\" link on the \"sign\" step.", + "end": { + "column": 3, + "line": 58 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.js", + "id": "voting.votingRegistration.register.step.learnMoreLink", + "start": { + "column": 17, + "line": 54 + } + }, + { + "defaultMessage": "!!!https://cardano.ideascale.com/a/index", + "description": "Learn more\" link URL on the \"sign\" step.", + "end": { + "column": 3, + "line": 63 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.js", + "id": "voting.votingRegistration.register.step.learntMoreLinkUrl", + "start": { + "column": 21, + "line": 59 + } + } + ], + "path": "source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister.json" + }, + { + "descriptors": [ + { + "defaultMessage": "!!!Cancel Fund3 voting registration?", + "description": "Headline for the voting registration cancellation confirmation dialog.", + "end": { + "column": 3, + "line": 15 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.js", + "id": "voting.votingRegistration.dialog.confirmation.headline", + "start": { + "column": 12, + "line": 10 + } + }, + { + "defaultMessage": "!!!Are you sure that you want to cancel Fund3 voting registration? The transaction fee you paid for the voting registration transaction will be lost and you will need to repeat the registration from the beginning.", + "description": "Content for the voting registration cancellation confirmation dialog.", + "end": { + "column": 3, + "line": 22 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.js", + "id": "voting.votingRegistration.dialog.confirmation.content", + "start": { + "column": 11, + "line": 16 + } + }, + { + "defaultMessage": "!!!Cancel registration", + "description": "\"Cancel registration\" button label for the voting registration cancellation confirmation dialog.", + "end": { + "column": 3, + "line": 29 + }, + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.js", + "id": "voting.votingRegistration.dialog.confirmation.button.cancelButtonLabel", + "start": { + "column": 21, + "line": 23 + } + }, + { + "defaultMessage": "!!!Continue registration", + "description": "\"Continue registration\" button label for the voting registration cancellation confirmation dialog.", + "end": { + "column": 3, + "line": 36 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.nodeIsUpdating", + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.js", + "id": "voting.votingRegistration.dialog.confirmation.button.confirmButtonLabel", "start": { - "column": 18, - "line": 254 + "column": 22, + "line": 30 } - }, + } + ], + "path": "source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/ConfirmationDialog.json" + }, + { + "descriptors": [ { - "defaultMessage": "!!!Cardano node responding", - "description": "Cardano node responding", + "defaultMessage": "!!!Register for Fund3 voting", + "description": "Tile \"Register to vote\" for voting registration", "end": { "column": 3, - "line": 263 + "line": 20 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.cardanoNodeResponding", + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/VotingRegistrationDialog.js", + "id": "voting.votingRegistration.dialog.dialogTitle", "start": { - "column": 25, - "line": 259 + "column": 15, + "line": 16 } }, { - "defaultMessage": "!!!Cardano node subscribed", - "description": "Cardano node subscribed", + "defaultMessage": "!!!Step {step} of {stepCount}", + "description": "Sub title for voting registration", "end": { "column": 3, - "line": 268 + "line": 25 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.cardanoNodeSubscribed", + "file": "source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/VotingRegistrationDialog.js", + "id": "voting.votingRegistration.dialog.subtitle", "start": { - "column": 25, - "line": 264 + "column": 12, + "line": 21 } - }, + } + ], + "path": "source/renderer/app/components/voting/voting-registration-wizard-steps/widgets/VotingRegistrationDialog.json" + }, + { + "descriptors": [ { - "defaultMessage": "!!!Cardano node time correct", - "description": "Cardano node time correct", + "defaultMessage": "!!!Register to vote on Fund3", + "description": "Headline for voting registration steps", "end": { "column": 3, - "line": 273 + "line": 20 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.cardanoNodeTimeCorrect", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.heading", "start": { - "column": 26, - "line": 269 + "column": 11, + "line": 16 } }, { - "defaultMessage": "!!!Cardano node syncing", - "description": "Cardano node syncing", + "defaultMessage": "!!!Decide which innovative ideas for Cardano will receive funding.", + "description": "Info step title 2 for voting registration steps", "end": { "column": 3, - "line": 278 + "line": 26 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.cardanoNodeSyncing", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.stepTitle1", "start": { - "column": 22, - "line": 274 + "column": 14, + "line": 21 } }, { - "defaultMessage": "!!!Cardano node in sync", - "description": "Cardano node in sync", + "defaultMessage": "!!!$70.000 worth of ada rewards will be distributed between ada holders who register their vote.", + "description": "Info step title 2 for voting registration steps", "end": { "column": 3, - "line": 283 + "line": 32 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.cardanoNodeInSync", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.stepTitle2", "start": { - "column": 21, - "line": 279 + "column": 14, + "line": 27 } }, { - "defaultMessage": "!!!Checking...", - "description": "Checking...", + "defaultMessage": "!!!Learn more", + "description": "learnMoreLinkLabel for voting registration steps", "end": { "column": 3, - "line": 288 + "line": 37 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.localTimeDifferenceChecking", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.learnMoreLinkLabel", "start": { - "column": 31, - "line": 284 + "column": 22, + "line": 33 } }, { - "defaultMessage": "!!!Check time", - "description": "Check time", + "defaultMessage": "!!!https://cardano.ideascale.com/a/index", + "description": "learnMoreLinkUrl for voting registration steps", "end": { "column": 3, - "line": 293 + "line": 42 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.localTimeDifferenceCheckTime", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.learnMoreLinkUrl", "start": { - "column": 32, - "line": 289 + "column": 20, + "line": 38 } }, { - "defaultMessage": "!!!YES", - "description": "YES", + "defaultMessage": "!!!Download the Catalyst Voting app on your smartphone", + "description": "bottomContentTitle for voting registration steps", "end": { "column": 3, - "line": 298 + "line": 47 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.statusOn", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.bottomContentTitle", "start": { - "column": 12, - "line": 294 + "column": 22, + "line": 43 } }, { - "defaultMessage": "!!!NO", - "description": "NO", + "defaultMessage": "!!!To register to vote for Catalyst Fund3 you first need to download the Catalyst Voting app on your Android or iOS smartphone.", + "description": "bottomContentDescription for voting registration steps", "end": { "column": 3, - "line": 303 + "line": 53 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.statusOff", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.bottomContentDescription", "start": { - "column": 13, - "line": 299 + "column": 28, + "line": 48 } }, { - "defaultMessage": "!!!NTP service unreachable", - "description": "NTP service unreachable", + "defaultMessage": "!!!I have installed the Catalyst Voting app", + "description": "checkboxLabel for voting registration steps", "end": { "column": 3, - "line": 308 + "line": 58 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.serviceUnreachable", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.checkboxLabel", "start": { - "column": 22, - "line": 304 + "column": 17, + "line": 54 } }, { - "defaultMessage": "!!!message", - "description": "message", + "defaultMessage": "!!!Register to vote", + "description": "Button Label for voting registration steps", "end": { "column": 3, - "line": 313 + "line": 63 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.message", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.buttonLabel", "start": { - "column": 11, - "line": 309 + "column": 15, + "line": 59 } }, { - "defaultMessage": "!!!code", - "description": "code", + "defaultMessage": "!!!https://play.google.com/store/apps/details?id=io.iohk.vitvoting", + "description": "\"androidAppButtonUrl\" for the Catalyst voting app", "end": { "column": 3, - "line": 318 + "line": 69 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.code", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.androidAppButtonUrl", "start": { - "column": 8, - "line": 314 + "column": 23, + "line": 64 } }, { - "defaultMessage": "!!!Last network block", - "description": "Last network block", + "defaultMessage": "!!!https://apps.apple.com/in/app/catalyst-voting/id1517473397", + "description": "\"appleAppButtonUrl\" for the Catalyst voting app", "end": { "column": 3, - "line": 323 + "line": 75 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.lastNetworkBlock", + "file": "source/renderer/app/components/voting/VotingInfo.js", + "id": "voting.info.appleAppButtonUrl", "start": { - "column": 20, - "line": 319 + "column": 21, + "line": 70 } - }, + } + ], + "path": "source/renderer/app/components/voting/VotingInfo.json" + }, + { + "descriptors": [ { - "defaultMessage": "!!!Last synchronized block", - "description": "Last synchronized block", + "defaultMessage": "!!!Voting registration for Fund3 is not available as you currently do not have any Shelley-compatible wallets.", + "description": "\"No wallets\" headLine on the voting info page.", "end": { "column": 3, - "line": 328 + "line": 17 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.lastSynchronizedBlock", + "file": "source/renderer/app/components/voting/VotingNoWallets.js", + "id": "voting.info.noWallets.headLine", "start": { - "column": 25, - "line": 324 + "column": 12, + "line": 12 } }, { - "defaultMessage": "!!!epoch", - "description": "epoch", + "defaultMessage": "!!!Create a new wallet and transfer a minimum of {minVotingFunds} ADA (or restore an existing wallet with funds), then return here to register for voting.", + "description": "\"No wallets\" instructions on the voting info page.", "end": { "column": 3, - "line": 333 + "line": 23 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.epoch", + "file": "source/renderer/app/components/voting/VotingNoWallets.js", + "id": "voting.info.noWallets.instructions", "start": { - "column": 9, - "line": 329 + "column": 16, + "line": 18 } }, { - "defaultMessage": "!!!slot", - "description": "slot", + "defaultMessage": "!!!Create wallet", + "description": "Label for \"Create New Wallet\" button on the voting info page.", "end": { "column": 3, - "line": 338 + "line": 29 }, - "file": "source/renderer/app/components/status/DaedalusDiagnostics.js", - "id": "daedalus.diagnostics.dialog.slot", + "file": "source/renderer/app/components/voting/VotingNoWallets.js", + "id": "voting.info.noWallets.createWalletButtonLabel", "start": { - "column": 8, - "line": 334 + "column": 27, + "line": 24 } } ], - "path": "source/renderer/app/components/status/DaedalusDiagnostics.json" + "path": "source/renderer/app/components/voting/VotingNoWallets.json" }, { "descriptors": [ @@ -7406,7 +8735,7 @@ }, { "defaultMessage": "!!!Enter your {numberOfWords}-word recovery phrase", - "description": "Placeholder for the mnemonics autocomplete.", + "description": "Placeholder hint for the mnemonics autocomplete.", "end": { "column": 3, "line": 46 @@ -7418,18 +8747,32 @@ "line": 42 } }, + { + "defaultMessage": "!!!Enter word #{wordNumber}", + "description": "Placeholder for the mnemonics autocomplete.", + "end": { + "column": 3, + "line": 52 + }, + "file": "source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js", + "id": "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputPlaceholder", + "start": { + "column": 34, + "line": 47 + } + }, { "defaultMessage": "!!!No results", "description": "\"No results\" message for the recovery phrase input search results.", "end": { "column": 3, - "line": 53 + "line": 59 }, "file": "source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js", "id": "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputNoResults", "start": { "column": 27, - "line": 47 + "line": 53 } }, { @@ -7437,13 +8780,13 @@ "description": "Error message shown when invalid recovery phrase was entered.", "end": { "column": 3, - "line": 60 + "line": 66 }, "file": "source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js", "id": "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInvalidMnemonics", "start": { "column": 34, - "line": 54 + "line": 60 } }, { @@ -7451,13 +8794,13 @@ "description": "Label for button \"Confirm\" on wallet backup dialog", "end": { "column": 3, - "line": 65 + "line": 71 }, "file": "source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js", "id": "wallet.recovery.phrase.show.entry.dialog.button.labelConfirm", "start": { "column": 22, - "line": 61 + "line": 67 } }, { @@ -7465,13 +8808,13 @@ "description": "Term on wallet creation to store recovery phrase offline", "end": { "column": 3, - "line": 72 + "line": 78 }, "file": "source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js", "id": "wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.offline", "start": { "column": 15, - "line": 66 + "line": 72 } }, { @@ -7479,13 +8822,13 @@ "description": "Term and condition on wallet backup dialog describing that wallet can only be recovered with a security phrase", "end": { "column": 3, - "line": 80 + "line": 86 }, "file": "source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js", "id": "wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.recovery", "start": { "column": 16, - "line": 73 + "line": 79 } }, { @@ -7493,13 +8836,13 @@ "description": "Term and condition on wallet backup dialog describing that wallet can only be recovered with a security phrase", "end": { "column": 3, - "line": 87 + "line": 93 }, "file": "source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js", "id": "wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.rewards", "start": { "column": 15, - "line": 81 + "line": 87 } } ], @@ -9204,16 +10547,16 @@ } }, { - "defaultMessage": "!!!Enter recovery phrase", - "description": "Hint \"Enter recovery phrase\" for the recovery phrase input on the wallet restore dialog.", + "defaultMessage": "!!!Enter word #{wordNumber}", + "description": "Placeholder \"Enter word #{wordNumber}\" for the recovery phrase input on the verification dialog.", "end": { "column": 3, "line": 49 }, "file": "source/renderer/app/components/wallet/settings/WalletRecoveryPhraseStep2Dialog.js", - "id": "wallet.settings.recoveryPhraseInputHint", + "id": "wallet.settings.recoveryPhraseInputPlaceholder", "start": { - "column": 27, + "column": 34, "line": 44 } }, @@ -9751,46 +11094,88 @@ "description": "Label for the \"Calculating fees\" message above amount input field.", "end": { "column": 3, - "line": 20 + "line": 20 + }, + "file": "source/renderer/app/components/wallet/skins/AmountInputSkin.js", + "id": "wallet.amountInput.calculatingFeesLabel", + "start": { + "column": 24, + "line": 15 + } + } + ], + "path": "source/renderer/app/components/wallet/skins/AmountInputSkin.json" + }, + { + "descriptors": [ + { + "defaultMessage": "!!!Number of transactions", + "description": "\"Number of transactions\" label on Wallet summary page", + "end": { + "column": 3, + "line": 24 + }, + "file": "source/renderer/app/components/wallet/summary/WalletSummary.js", + "id": "wallet.summary.page.transactionsLabel", + "start": { + "column": 21, + "line": 20 + } + }, + { + "defaultMessage": "!!!Number of pending transactions", + "description": "\"Number of pending transactions\" label on Wallet summary page", + "end": { + "column": 3, + "line": 30 + }, + "file": "source/renderer/app/components/wallet/summary/WalletSummary.js", + "id": "wallet.summary.page.pendingTransactionsLabel", + "start": { + "column": 28, + "line": 25 + } + }, + { + "defaultMessage": "!!!Converts as", + "description": "\"Currency - title\" label on Wallet summary page", + "end": { + "column": 3, + "line": 35 }, - "file": "source/renderer/app/components/wallet/skins/AmountInputSkin.js", - "id": "wallet.amountInput.calculatingFeesLabel", + "file": "source/renderer/app/components/wallet/summary/WalletSummary.js", + "id": "wallet.summary.page.currency.title", "start": { - "column": 24, - "line": 15 + "column": 17, + "line": 31 } - } - ], - "path": "source/renderer/app/components/wallet/skins/AmountInputSkin.json" - }, - { - "descriptors": [ + }, { - "defaultMessage": "!!!Number of transactions", - "description": "\"Number of transactions\" label on Wallet summary page", + "defaultMessage": "!!!converted {fetchedTimeAgo}", + "description": "\"Currency - last fetched\" label on Wallet summary page", "end": { "column": 3, - "line": 18 + "line": 40 }, "file": "source/renderer/app/components/wallet/summary/WalletSummary.js", - "id": "wallet.summary.page.transactionsLabel", + "id": "wallet.summary.page.currency.lastFetched", "start": { - "column": 21, - "line": 14 + "column": 23, + "line": 36 } }, { - "defaultMessage": "!!!Number of pending transactions", - "description": "\"Number of pending transactions\" label on Wallet summary page", + "defaultMessage": "!!!fetching conversion rates", + "description": "\"Currency - Fetching\" label on Wallet summary page", "end": { "column": 3, - "line": 24 + "line": 45 }, "file": "source/renderer/app/components/wallet/summary/WalletSummary.js", - "id": "wallet.summary.page.pendingTransactionsLabel", + "id": "wallet.summary.page.currency.isFetchingRate", "start": { - "column": 28, - "line": 19 + "column": 26, + "line": 41 } } ], @@ -10117,13 +11502,13 @@ "description": "Transaction type shown for credit card payments.", "end": { "column": 3, - "line": 32 + "line": 33 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.type.card", "start": { "column": 8, - "line": 28 + "line": 29 } }, { @@ -10131,13 +11516,13 @@ "description": "Transaction type shown for {currency} transactions.", "end": { "column": 3, - "line": 37 + "line": 38 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.type", "start": { "column": 8, - "line": 33 + "line": 34 } }, { @@ -10145,13 +11530,13 @@ "description": "Transaction type shown for money exchanges between currencies.", "end": { "column": 3, - "line": 43 + "line": 44 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.type.exchange", "start": { "column": 12, - "line": 38 + "line": 39 } }, { @@ -10159,13 +11544,55 @@ "description": "Transaction ID.", "end": { "column": 3, - "line": 48 + "line": 49 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.transactionId", "start": { "column": 17, - "line": 44 + "line": 45 + } + }, + { + "defaultMessage": "!!!Transaction metadata", + "description": "Transaction metadata label", + "end": { + "column": 3, + "line": 54 + }, + "file": "source/renderer/app/components/wallet/transactions/Transaction.js", + "id": "wallet.transaction.metadataLabel", + "start": { + "column": 17, + "line": 50 + } + }, + { + "defaultMessage": "!!!Transaction metadata is not moderated and may contain inappropriate content.", + "description": "Transaction metadata disclaimer", + "end": { + "column": 3, + "line": 60 + }, + "file": "source/renderer/app/components/wallet/transactions/Transaction.js", + "id": "wallet.transaction.metadataDisclaimer", + "start": { + "column": 22, + "line": 55 + } + }, + { + "defaultMessage": "!!!Show unmoderated content", + "description": "Transaction metadata confirmation toggle", + "end": { + "column": 3, + "line": 65 + }, + "file": "source/renderer/app/components/wallet/transactions/Transaction.js", + "id": "wallet.transaction.metadataConfirmationLabel", + "start": { + "column": 29, + "line": 61 } }, { @@ -10173,13 +11600,13 @@ "description": "Conversion rate.", "end": { "column": 3, - "line": 53 + "line": 70 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.conversion.rate", "start": { "column": 18, - "line": 49 + "line": 66 } }, { @@ -10187,13 +11614,13 @@ "description": "Label \"{currency} sent\" for the transaction.", "end": { "column": 3, - "line": 58 + "line": 75 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.sent", "start": { "column": 8, - "line": 54 + "line": 71 } }, { @@ -10201,13 +11628,13 @@ "description": "Label \"{currency} received\" for the transaction.", "end": { "column": 3, - "line": 63 + "line": 80 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.received", "start": { "column": 12, - "line": 59 + "line": 76 } }, { @@ -10215,13 +11642,13 @@ "description": "From address", "end": { "column": 3, - "line": 68 + "line": 85 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.address.from", "start": { "column": 15, - "line": 64 + "line": 81 } }, { @@ -10229,13 +11656,13 @@ "description": "From addresses", "end": { "column": 3, - "line": 73 + "line": 90 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.addresses.from", "start": { "column": 17, - "line": 69 + "line": 86 } }, { @@ -10243,13 +11670,13 @@ "description": "From rewards", "end": { "column": 3, - "line": 78 + "line": 95 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.rewards.from", "start": { "column": 15, - "line": 74 + "line": 91 } }, { @@ -10257,13 +11684,13 @@ "description": "To address", "end": { "column": 3, - "line": 83 + "line": 100 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.address.to", "start": { "column": 13, - "line": 79 + "line": 96 } }, { @@ -10271,13 +11698,41 @@ "description": "To addresses", "end": { "column": 3, - "line": 88 + "line": 105 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.addresses.to", "start": { "column": 15, - "line": 84 + "line": 101 + } + }, + { + "defaultMessage": "!!!Transaction fee", + "description": "Transaction fee", + "end": { + "column": 3, + "line": 110 + }, + "file": "source/renderer/app/components/wallet/transactions/Transaction.js", + "id": "wallet.transaction.transactionFee", + "start": { + "column": 18, + "line": 106 + } + }, + { + "defaultMessage": "!!!Deposit", + "description": "Deposit", + "end": { + "column": 3, + "line": 115 + }, + "file": "source/renderer/app/components/wallet/transactions/Transaction.js", + "id": "wallet.transaction.deposit", + "start": { + "column": 11, + "line": 111 } }, { @@ -10285,13 +11740,13 @@ "description": "Transaction amount.", "end": { "column": 3, - "line": 93 + "line": 120 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.transactionAmount", "start": { "column": 21, - "line": 89 + "line": 116 } }, { @@ -10299,13 +11754,13 @@ "description": "Note to cancel a transaction that has been pending too long", "end": { "column": 3, - "line": 99 + "line": 126 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.pending.cancelPendingTxnNote", "start": { "column": 24, - "line": 94 + "line": 121 } }, { @@ -10313,13 +11768,13 @@ "description": "Link to support article for canceling a pending transaction", "end": { "column": 3, - "line": 104 + "line": 131 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.pending.cancelPendingTxnSupportArticle", "start": { "column": 34, - "line": 100 + "line": 127 } }, { @@ -10327,13 +11782,13 @@ "description": "Url to support article for canceling a pending transaction", "end": { "column": 3, - "line": 110 + "line": 137 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.pending.supportArticleUrl", "start": { "column": 21, - "line": 105 + "line": 132 } }, { @@ -10341,13 +11796,13 @@ "description": "Input Addresses label.", "end": { "column": 3, - "line": 115 + "line": 142 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.noInputAddressesLabel", "start": { "column": 25, - "line": 111 + "line": 138 } }, { @@ -10355,13 +11810,13 @@ "description": "Unresolved Input Addresses link label.", "end": { "column": 3, - "line": 120 + "line": 147 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.unresolvedInputAddressesLinkLabel", "start": { "column": 37, - "line": 116 + "line": 143 } }, { @@ -10369,13 +11824,13 @@ "description": "Unresolved Input Addresses additional label.", "end": { "column": 3, - "line": 125 + "line": 152 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.unresolvedInputAddressesAdditionalLabel", "start": { "column": 43, - "line": 121 + "line": 148 } }, { @@ -10383,13 +11838,13 @@ "description": "Note to cancel a transaction that has been failed", "end": { "column": 3, - "line": 131 + "line": 158 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.failed.cancelFailedTxnNote", "start": { "column": 23, - "line": 126 + "line": 153 } }, { @@ -10397,13 +11852,13 @@ "description": "Link to support article for removing a failed transaction", "end": { "column": 3, - "line": 136 + "line": 163 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.failed.cancelFailedTxnSupportArticle", "start": { "column": 33, - "line": 132 + "line": 159 } }, { @@ -10411,13 +11866,13 @@ "description": "Transaction state \"confirmed\"", "end": { "column": 3, - "line": 144 + "line": 171 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.state.confirmed", "start": { "column": 26, - "line": 140 + "line": 167 } }, { @@ -10425,13 +11880,13 @@ "description": "Transaction state \"pending\"", "end": { "column": 3, - "line": 149 + "line": 176 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.state.pending", "start": { "column": 31, - "line": 145 + "line": 172 } }, { @@ -10439,13 +11894,13 @@ "description": "Transaction state \"failed\"", "end": { "column": 3, - "line": 154 + "line": 181 }, "file": "source/renderer/app/components/wallet/transactions/Transaction.js", "id": "wallet.transaction.state.failed", "start": { "column": 30, - "line": 150 + "line": 177 } } ], @@ -11620,7 +13075,7 @@ { "descriptors": [ { - "defaultMessage": "!!!Enter your {numberOfWords}-word recovery phrase", + "defaultMessage": "!!!Enter word #{wordNumber}", "description": "Placeholder for the mnemonics autocomplete.", "end": { "column": 3, @@ -12443,13 +13898,13 @@ "description": "Title \"Connect a hardware wallet device\" in the connect wallet dialog.", "end": { "column": 3, - "line": 37 + "line": 44 }, "file": "source/renderer/app/components/wallet/WalletConnectDialog.js", "id": "wallet.connect.dialog.title", "start": { "column": 15, - "line": 32 + "line": 39 } }, { @@ -12457,13 +13912,13 @@ "description": "Label for the \"Cancel\" button in the connect wallet dialog", "end": { "column": 3, - "line": 42 + "line": 49 }, "file": "source/renderer/app/components/wallet/WalletConnectDialog.js", "id": "wallet.connect.dialog.button.cancel", "start": { "column": 16, - "line": 38 + "line": 45 } }, { @@ -12471,13 +13926,13 @@ "description": "Follow instructions label", "end": { "column": 3, - "line": 48 + "line": 55 }, "file": "source/renderer/app/components/wallet/WalletConnectDialog.js", "id": "wallet.connect.dialog.instructions", "start": { "column": 16, - "line": 43 + "line": 50 } }, { @@ -12485,13 +13940,55 @@ "description": "Follow instructions label", "end": { "column": 3, - "line": 54 + "line": 61 }, "file": "source/renderer/app/components/wallet/WalletConnectDialog.js", "id": "wallet.connect.dialog.instructionsTrezorOnly", "start": { "column": 26, - "line": 49 + "line": 56 + } + }, + { + "defaultMessage": "!!!If you are experiencing issues pairing your hardware wallet device, please {supportLink}", + "description": "Connecting issue support description", + "end": { + "column": 3, + "line": 67 + }, + "file": "source/renderer/app/components/wallet/WalletConnectDialog.js", + "id": "wallet.connect.dialog.connectingIssueSupportLabel", + "start": { + "column": 31, + "line": 62 + } + }, + { + "defaultMessage": "!!!read the instructions.", + "description": "Connecting issue support link", + "end": { + "column": 3, + "line": 72 + }, + "file": "source/renderer/app/components/wallet/WalletConnectDialog.js", + "id": "wallet.connect.dialog.connectingIssueSupportLink", + "start": { + "column": 30, + "line": 68 + } + }, + { + "defaultMessage": "https://support.ledger.com/hc/en-us/articles/115005165269", + "description": "Link to support article", + "end": { + "column": 3, + "line": 77 + }, + "file": "source/renderer/app/components/wallet/WalletConnectDialog.js", + "id": "wallet.connect.dialog.connectingIssueSupportLinkUrl", + "start": { + "column": 33, + "line": 73 } } ], @@ -12649,13 +14146,13 @@ "description": "Label \"Restore wallet\" on the wallet restore dialog.", "end": { "column": 3, - "line": 49 + "line": 48 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.title.label", "start": { "column": 9, - "line": 45 + "line": 44 } }, { @@ -12663,13 +14160,13 @@ "description": "Label for the wallet name input on the wallet restore dialog.", "end": { "column": 3, - "line": 55 + "line": 54 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.wallet.name.input.label", "start": { "column": 24, - "line": 50 + "line": 49 } }, { @@ -12677,13 +14174,13 @@ "description": "Hint \"Name the wallet you are restoring\" for the wallet name input on the wallet restore dialog.", "end": { "column": 3, - "line": 61 + "line": 60 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.wallet.name.input.hint", "start": { "column": 23, - "line": 56 + "line": 55 } }, { @@ -12691,13 +14188,13 @@ "description": "Label for the recovery phrase type options on the wallet restore dialog.", "end": { "column": 3, - "line": 67 + "line": 66 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.recovery.phrase.type.options.label", "start": { "column": 27, - "line": 62 + "line": 61 } }, { @@ -12705,13 +14202,13 @@ "description": "Word for the recovery phrase type on the wallet restore dialog.", "end": { "column": 3, - "line": 73 + "line": 72 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.recovery.phrase.type.word", "start": { "column": 32, - "line": 68 + "line": 67 } }, { @@ -12719,13 +14216,13 @@ "description": "Label for the recovery phrase type 15-word option on the wallet restore dialog.", "end": { "column": 3, - "line": 79 + "line": 78 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.recovery.phrase.type.15word.option", "start": { "column": 34, - "line": 74 + "line": 73 } }, { @@ -12733,13 +14230,13 @@ "description": "Label for the recovery phrase type 12-word option on the wallet restore dialog.", "end": { "column": 3, - "line": 85 + "line": 84 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.recovery.phrase.type.12word.option", "start": { "column": 34, - "line": 80 + "line": 79 } }, { @@ -12747,13 +14244,13 @@ "description": "Label for the recovery phrase input on the wallet restore dialog.", "end": { "column": 3, - "line": 91 + "line": 90 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.recovery.phrase.input.label", "start": { "column": 28, - "line": 86 + "line": 85 } }, { @@ -12761,13 +14258,13 @@ "description": "Hint \"Enter recovery phrase\" for the recovery phrase input on the wallet restore dialog.", "end": { "column": 3, - "line": 97 + "line": 96 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.recovery.phrase.input.hint", "start": { "column": 27, - "line": 92 + "line": 91 } }, { @@ -12775,13 +14272,13 @@ "description": "Label \"new\" on the wallet restore dialog.", "end": { "column": 3, - "line": 102 + "line": 101 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.recovery.phrase.newLabel", "start": { "column": 12, - "line": 98 + "line": 97 } }, { @@ -12789,13 +14286,13 @@ "description": "\"No results\" message for the recovery phrase input search results.", "end": { "column": 3, - "line": 108 + "line": 107 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.recovery.phrase.input.noResults", "start": { "column": 27, - "line": 103 + "line": 102 } }, { @@ -12803,13 +14300,13 @@ "description": "Label for the \"Restore wallet\" button on the wallet restore dialog.", "end": { "column": 3, - "line": 114 + "line": 113 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.restore.wallet.button.label", "start": { "column": 21, - "line": 109 + "line": 108 } }, { @@ -12817,13 +14314,13 @@ "description": "Error message shown when invalid recovery phrase was entered.", "end": { "column": 3, - "line": 120 + "line": 119 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.form.errors.invalidRecoveryPhrase", "start": { "column": 25, - "line": 115 + "line": 114 } }, { @@ -12831,13 +14328,13 @@ "description": "Password creation label.", "end": { "column": 3, - "line": 125 + "line": 124 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.passwordSectionLabel", "start": { "column": 24, - "line": 121 + "line": 120 } }, { @@ -12845,13 +14342,13 @@ "description": "Password creation description.", "end": { "column": 3, - "line": 131 + "line": 130 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.passwordSectionDescription", "start": { "column": 30, - "line": 126 + "line": 125 } }, { @@ -12859,13 +14356,13 @@ "description": "Label for the \"Wallet password\" input in the wallet restore dialog.", "end": { "column": 3, - "line": 137 + "line": 136 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.spendingPasswordLabel", "start": { "column": 25, - "line": 132 + "line": 131 } }, { @@ -12873,13 +14370,13 @@ "description": "Label for the \"Repeat password\" input in the wallet restore dialog.", "end": { "column": 3, - "line": 143 + "line": 142 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.repeatPasswordLabel", "start": { "column": 23, - "line": 138 + "line": 137 } }, { @@ -12887,13 +14384,13 @@ "description": "Placeholder for the \"Password\" inputs in the wallet restore dialog.", "end": { "column": 3, - "line": 149 + "line": 148 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.passwordFieldPlaceholder", "start": { "column": 28, - "line": 144 + "line": 143 } }, { @@ -12901,13 +14398,13 @@ "description": "Tab title \"Daedalus wallet\" in the wallet restore dialog.", "end": { "column": 3, - "line": 154 + "line": 153 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.tab.title.recoveryPhrase", "start": { "column": 26, - "line": 150 + "line": 149 } }, { @@ -12915,13 +14412,13 @@ "description": "Tab title \"Daedalus paper wallet\" in the wallet restore dialog.", "end": { "column": 3, - "line": 160 + "line": 159 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.tab.title.certificate", "start": { "column": 23, - "line": 155 + "line": 154 } }, { @@ -12929,13 +14426,13 @@ "description": "Tab title \"Yoroi wallet\" in the wallet restore dialog.", "end": { "column": 3, - "line": 165 + "line": 164 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.tab.title.yoroi", "start": { "column": 17, - "line": 161 + "line": 160 } }, { @@ -12943,13 +14440,13 @@ "description": "Label for the shielded recovery phrase input on the wallet restore dialog.", "end": { "column": 3, - "line": 171 + "line": 170 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.shielded.recovery.phrase.input.label", "start": { "column": 36, - "line": 166 + "line": 165 } }, { @@ -12957,13 +14454,27 @@ "description": "Hint \"Enter your 27-word paper wallet recovery phrase.\" for the recovery phrase input on the wallet restore dialog.", "end": { "column": 3, - "line": 178 + "line": 177 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.shielded.recovery.phrase.input.hint", "start": { "column": 35, - "line": 172 + "line": 171 + } + }, + { + "defaultMessage": "!!!Enter word #{wordNumber}", + "description": "Placeholder \"Enter word #\" for the recovery phrase input on the wallet restore dialog.", + "end": { + "column": 3, + "line": 183 + }, + "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", + "id": "wallet.restore.dialog.shielded.recovery.phrase.input.placeholder", + "start": { + "column": 42, + "line": 178 } }, { @@ -12971,13 +14482,13 @@ "description": "Label for the \"Restore paper wallet\" button on the wallet restore dialog.", "end": { "column": 3, - "line": 184 + "line": 189 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.restore.dialog.paper.wallet.button.label", "start": { "column": 33, - "line": 179 + "line": 184 } }, { @@ -12985,13 +14496,13 @@ "description": "Tooltip for the password input in the wallet dialog.", "end": { "column": 3, - "line": 190 + "line": 195 }, "file": "source/renderer/app/components/wallet/WalletRestoreDialog.js", "id": "wallet.dialog.passwordTooltip", "start": { "column": 19, - "line": 185 + "line": 190 } } ], @@ -13355,13 +14866,13 @@ "description": "Label \"change\" on inline editing inputs in inactive state.", "end": { "column": 3, - "line": 18 + "line": 26 }, "file": "source/renderer/app/components/widgets/forms/InlineEditingInput.js", "id": "inline.editing.input.change.label", "start": { "column": 10, - "line": 14 + "line": 22 } }, { @@ -13369,13 +14880,13 @@ "description": "Label \"cancel\" on inline editing inputs in inactive state.", "end": { "column": 3, - "line": 23 + "line": 31 }, "file": "source/renderer/app/components/widgets/forms/InlineEditingInput.js", "id": "inline.editing.input.cancel.label", "start": { "column": 10, - "line": 19 + "line": 27 } }, { @@ -13383,13 +14894,13 @@ "description": "Message \"Your changes have been saved\" for inline editing (eg. on Profile Settings page).", "end": { "column": 3, - "line": 29 + "line": 37 }, "file": "source/renderer/app/components/widgets/forms/InlineEditingInput.js", "id": "inline.editing.input.changesSaved", "start": { "column": 16, - "line": 24 + "line": 32 } } ], @@ -13477,13 +14988,13 @@ "description": "Label for the blocks synced info overlay on node sync status icon.", "end": { "column": 3, - "line": 16 + "line": 17 }, "file": "source/renderer/app/components/widgets/NodeSyncStatusIcon.js", "id": "cardano.node.sync.status.blocksSynced", "start": { "column": 16, - "line": 11 + "line": 12 } } ], @@ -13748,18 +15259,32 @@ "line": 57 } }, + { + "defaultMessage": "!!!PDF successfully downloaded", + "description": "Notification for the wallet voting PDF download success in the Voting Registration dialog.", + "end": { + "column": 3, + "line": 69 + }, + "file": "source/renderer/app/containers/notifications/NotificationsContainer.js", + "id": "notification.downloadVotingPDFSuccess", + "start": { + "column": 28, + "line": 64 + } + }, { "defaultMessage": "!!!Address: {walletAddress} QR code image successfully downloaded", "description": "Notification for the wallet address PDF download success in the Wallet Receive page.", "end": { "column": 3, - "line": 70 + "line": 76 }, "file": "source/renderer/app/containers/notifications/NotificationsContainer.js", "id": "notification.downloadQRCodeImageSuccess", "start": { "column": 30, - "line": 64 + "line": 70 } }, { @@ -13767,13 +15292,13 @@ "description": "Notification for the state directory copy success in the Diagnostics page.", "end": { "column": 3, - "line": 76 + "line": 82 }, "file": "source/renderer/app/containers/notifications/NotificationsContainer.js", "id": "notification.copyStateDirectoryPath", "start": { "column": 26, - "line": 71 + "line": 77 } } ], @@ -13805,13 +15330,13 @@ "description": "\"Learn more\" link URL on the delegation setup \"intro\" dialog.", "end": { "column": 3, - "line": 21 + "line": 22 }, "file": "source/renderer/app/containers/staking/dialogs/DelegationSetupWizardDialogContainer.js", "id": "staking.delegationSetup.intro.step.dialog.learnMore.url", "start": { "column": 20, - "line": 16 + "line": 17 } }, { @@ -13819,13 +15344,13 @@ "description": "Step 1 label text on delegation steps dialog.", "end": { "column": 3, - "line": 26 + "line": 27 }, "file": "source/renderer/app/containers/staking/dialogs/DelegationSetupWizardDialogContainer.js", "id": "staking.delegationSetup.steps.step.1.label", "start": { "column": 29, - "line": 22 + "line": 23 } }, { @@ -13833,13 +15358,13 @@ "description": "Step 2 label text on delegation steps dialog.", "end": { "column": 3, - "line": 31 + "line": 32 }, "file": "source/renderer/app/containers/staking/dialogs/DelegationSetupWizardDialogContainer.js", "id": "staking.delegationSetup.steps.step.2.label", "start": { "column": 29, - "line": 27 + "line": 28 } }, { @@ -13847,13 +15372,13 @@ "description": "Step 3 label text on delegation steps dialog.", "end": { "column": 3, - "line": 36 + "line": 37 }, "file": "source/renderer/app/containers/staking/dialogs/DelegationSetupWizardDialogContainer.js", "id": "staking.delegationSetup.steps.step.3.label", "start": { "column": 29, - "line": 32 + "line": 33 } } ], @@ -13949,6 +15474,81 @@ ], "path": "source/renderer/app/containers/staking/StakingRewardsPage.json" }, + { + "descriptors": [ + { + "defaultMessage": "!!!Wallet", + "description": "Step 1 label text on voting registration.", + "end": { + "column": 3, + "line": 23 + }, + "file": "source/renderer/app/containers/voting/dialogs/VotingRegistrationDialogContainer.js", + "id": "voting.votingRegistration.steps.step.1.label", + "start": { + "column": 32, + "line": 19 + } + }, + { + "defaultMessage": "!!!Register", + "description": "Step 2 label text on voting registration.", + "end": { + "column": 3, + "line": 28 + }, + "file": "source/renderer/app/containers/voting/dialogs/VotingRegistrationDialogContainer.js", + "id": "voting.votingRegistration.steps.step.2.label", + "start": { + "column": 32, + "line": 24 + } + }, + { + "defaultMessage": "!!!Confirm", + "description": "Step 3 label text on voting registration.", + "end": { + "column": 3, + "line": 33 + }, + "file": "source/renderer/app/containers/voting/dialogs/VotingRegistrationDialogContainer.js", + "id": "voting.votingRegistration.steps.step.3.label", + "start": { + "column": 32, + "line": 29 + } + }, + { + "defaultMessage": "!!!PIN", + "description": "Step 4 label text on voting registration.", + "end": { + "column": 3, + "line": 38 + }, + "file": "source/renderer/app/containers/voting/dialogs/VotingRegistrationDialogContainer.js", + "id": "voting.votingRegistration.steps.step.4.label", + "start": { + "column": 32, + "line": 34 + } + }, + { + "defaultMessage": "QR code", + "description": "Step 5 label text on voting registration.", + "end": { + "column": 3, + "line": 43 + }, + "file": "source/renderer/app/containers/voting/dialogs/VotingRegistrationDialogContainer.js", + "id": "voting.votingRegistration.steps.step.5.label", + "start": { + "column": 32, + "line": 39 + } + } + ], + "path": "source/renderer/app/containers/voting/dialogs/VotingRegistrationDialogContainer.json" + }, { "descriptors": [ { @@ -14828,6 +16428,20 @@ "column": 8, "line": 321 } + }, + { + "defaultMessage": "!!!Daedalus is synchronizing with the Cardano blockchain, and the process is currently {syncPercentage}% complete. This feature will become available once Daedalus is fully synchronized.", + "description": "Info message displayed for features which are unavailable while Daedalus is syncing", + "end": { + "column": 3, + "line": 332 + }, + "file": "source/renderer/app/i18n/global-messages.js", + "id": "global.info.featureUnavailableWhileSyncing", + "start": { + "column": 34, + "line": 326 + } } ], "path": "source/renderer/app/i18n/global-messages.json" @@ -15182,5 +16796,80 @@ } ], "path": "source/renderer/app/utils/transactionsCsvGenerator.json" + }, + { + "descriptors": [ + { + "defaultMessage": "!!!Fund{fundNumber} Voting Registration", + "description": "PDF title", + "end": { + "column": 3, + "line": 16 + }, + "file": "source/renderer/app/utils/votingPDFGenerator.js", + "id": "voting.votingRegistration.pdf.title", + "start": { + "column": 9, + "line": 12 + } + }, + { + "defaultMessage": "!!!Wallet name", + "description": "PDF wallet name title", + "end": { + "column": 3, + "line": 21 + }, + "file": "source/renderer/app/utils/votingPDFGenerator.js", + "id": "voting.votingRegistration.pdf.walletNameLabel", + "start": { + "column": 19, + "line": 17 + } + }, + { + "defaultMessage": "!!!voting-registration", + "description": "PDF filename title", + "end": { + "column": 3, + "line": 26 + }, + "file": "source/renderer/app/utils/votingPDFGenerator.js", + "id": "voting.votingRegistration.pdf.filename", + "start": { + "column": 12, + "line": 22 + } + }, + { + "defaultMessage": "!!!Cardano network:", + "description": "PDF networkLabel label", + "end": { + "column": 3, + "line": 31 + }, + "file": "source/renderer/app/utils/votingPDFGenerator.js", + "id": "voting.votingRegistration.pdf.networkLabel", + "start": { + "column": 16, + "line": 27 + } + }, + { + "defaultMessage": "!!!Daedalus wallet", + "description": "PDF author", + "end": { + "column": 3, + "line": 36 + }, + "file": "source/renderer/app/utils/votingPDFGenerator.js", + "id": "voting.votingRegistration.pdf.author", + "start": { + "column": 10, + "line": 32 + } + } + ], + "path": "source/renderer/app/utils/votingPDFGenerator.json" } ] \ No newline at end of file diff --git a/source/renderer/app/i18n/locales/en-US.json b/source/renderer/app/i18n/locales/en-US.json index 7d5ffdc666..70c68aa5ef 100755 --- a/source/renderer/app/i18n/locales/en-US.json +++ b/source/renderer/app/i18n/locales/en-US.json @@ -15,6 +15,7 @@ "api.errors.WalletFileImportError": "Wallet could not be imported, please make sure you are providing a correct file.", "api.errors.inputsDepleted": "Cannot send from a wallet that contains only rewards balances.", "api.errors.invalidAddress": "Please enter a valid address.", + "api.errors.invalidSmashServer": "This URL is not a valid SMASH server", "api.errors.nothingToMigrate": "Funds cannot be transferred from this wallet because it contains some unspent transaction outputs (UTXOs), with amounts of ada that are too small to be migrated.", "api.errors.utxoTooSmall": "Invalid transaction.", "appUpdate.overlay.button.installUpdate.label": "Install the update and restart Daedalus", @@ -134,6 +135,7 @@ "global.errors.rewardsOpenCsvError": "The file you are trying to replace is open. Please close it and try again.", "global.errors.strongSpendingPassword": "Strong", "global.errors.weakSpendingPassword": "Weak", + "global.info.featureUnavailableWhileSyncing": "Daedalus is synchronizing with the Cardano blockchain, and the process is currently {syncPercentage}% complete. This feature will become available once Daedalus is fully synchronized.", "global.info.knownMnemonicWordCount": "{actual} of {required} {required, plural, one {word} other {words}} entered", "global.info.unknownMnemonicWordCount": "{actual} {actual, plural, one {word} other {words}} entered", "global.labels.all": "All", @@ -201,6 +203,7 @@ "notification.downloadQRCodeImageSuccess": "Address: {walletAddress} QR code image successfully downloaded", "notification.downloadRewardsCSVSuccess": "CSV file successfully downloaded", "notification.downloadTransactionsCSVSuccess": "CSV file successfully downloaded", + "notification.downloadVotingPDFSuccess": "PDF successfully downloaded", "paper.wallet.create.certificate.completion.dialog.addressCopiedLabel": "copied", "paper.wallet.create.certificate.completion.dialog.addressInstructions": "To receive funds to your paper wallet simply share your wallet address with others.", "paper.wallet.create.certificate.completion.dialog.addressLabel": "Wallet address", @@ -275,8 +278,27 @@ "settings.display.themeNames.yellow": "Yellow", "settings.menu.display.link.label": "Themes", "settings.menu.general.link.label": "General", + "settings.menu.stakePools.link.label": "Stake pools", "settings.menu.support.link.label": "Support", "settings.menu.termsOfUse.link.label": "Terms of service", + "settings.menu.wallets.link.label": "Wallets", + "settings.stakePools.smash.description": "The {link} is an off-chain metadata server that enables the fast loading of stake pool details. Stake pools are also curated and each server has a different curation policy.", + "settings.stakePools.smash.descriptionIOHKContent1": "The IOHK server ensures that registered stake pools are valid, helps to avoid duplicated ticker names or trademarks, and checks that the pools do not feature potentially offensive or harmful information.", + "settings.stakePools.smash.descriptionIOHKContent2": "This allows us to deal with any scams, trolls, or abusive behavior by filtering out potentially problematic actors. {link} about the IOHK SMASH server.", + "settings.stakePools.smash.descriptionIOHKLinkLabel": "Read more", + "settings.stakePools.smash.descriptionIOHKLinkUrl": "https://iohk.io/en/blog/posts/2020/11/17/in-pools-we-trust/", + "settings.stakePools.smash.descriptionLinkLabel": "Stakepool Metadata Aggregation Server (SMASH)", + "settings.stakePools.smash.descriptionLinkUrl": "https://docs.cardano.org/en/latest/getting-started/stake-pool-operators/SMASH-metadata-management.html", + "settings.stakePools.smash.descriptionNone": "This option is not recommended! Without the off-chain metadata server your Daedalus client will fetch this data by contacting every stake pool individually, which is a very slow and resource-consuming process. The list of stake pools received is not curated, so Daedalus will receive legitimate pools, duplicates, and fake pools. An added risk to this process is that your antivirus or antimalware software could recognize the thousands of network requests as malicious behavior by the Daedalus client.", + "settings.stakePools.smash.select.IOHKServer": "IOHK (recommended)", + "settings.stakePools.smash.select.customServer": "Custom server", + "settings.stakePools.smash.select.direct": "None - fetch the data directly", + "settings.stakePools.smash.select.label": "Off-chain metadata server (SMASH)", + "settings.stakePools.smashUrl.input.invalidUrl": "Invalid URL", + "settings.stakePools.smashUrl.input.invalidUrlParameter": "Only \"https://\" protocol and hostname (e.g. domain.com) are allowed", + "settings.stakePools.smashUrl.input.invalidUrlPrefix": "The URL should start with \"https://\"", + "settings.stakePools.smashUrl.input.label": "SMASH server URL", + "settings.stakePools.smashUrl.input.placeholder": "Enter custom server URL", "settings.support.faq.content": "If you are experiencing a problem, please look for guidance using the list of {faqLink} on the support pages. If you can’t find a solution, please submit a support ticket.", "settings.support.faq.faqLink": "Known Issues", "settings.support.faq.faqLinkURL": "https://daedaluswallet.io/known-issues/", @@ -289,6 +311,11 @@ "settings.support.steps.reportProblem.link": "Open the support request form in your browser", "settings.support.steps.reportProblem.title": "Report a problem", "settings.support.steps.title": "Steps for creating a support request:", + "settings.wallets.currency.description": "Select a conversion currency for displaying your ada balances.", + "settings.wallets.currency.disclaimer": "Conversion rates are provided by {currencyApiName} without any warranty. Please use the calculated conversion value only as a reference.
Converted balances reflect the current global average price of ada on active cryptocurrency exchanges, as tracked by {currencyApiName}. Ada conversion is available only to fiat and cryptocurrencies that are supported by {currencyApiName}, other local currency conversions may not be available.", + "settings.wallets.currency.poweredBy.label": "Powered by ", + "settings.wallets.currency.selectLabel": "Select currency", + "settings.wallets.currency.titleLabel": "Display ada balances in other currency", "sidebar.wallets.addWallet": "Add wallet", "staking.countdown.learnMore.linkUrl": "https://iohk.zendesk.com/hc/en-us", "staking.delegationCenter.bodyTitle": "Wallets", @@ -300,7 +327,7 @@ "staking.delegationCenter.headingLeft": "Next Cardano epoch starts in", "staking.delegationCenter.headingRight": "Current Cardano epoch", "staking.delegationCenter.loadingStakePoolsMessage": "Loading stake pools", - "staking.delegationCenter.noWallets.createWalletButtonLabel": "Create a wallet", + "staking.delegationCenter.noWallets.createWalletButtonLabel": "Create wallet", "staking.delegationCenter.noWallets.headLine": "The delegation center is not available because you currently do not have any Shelley-compatible wallets.", "staking.delegationCenter.noWallets.instructions": "Create a new wallet and transfer in a minimum of {minDelegationFunds} ADA (or restore an existing wallet with funds), then return here to delegate your stake.", "staking.delegationCenter.notDelegated": "Undelegated", @@ -354,9 +381,11 @@ "staking.delegationSetup.chooseWallet.step.dialog.stepIndicatorLabel": "STEP {currentStep} OF {totalSteps}", "staking.delegationSetup.chooseWallet.step.dialog.syncingWallet": "syncing", "staking.delegationSetup.chooseWallet.step.dialog.title": "Delegate wallet", + "staking.delegationSetup.confirmation.step.dialog.calculatingDeposit": "Calculating deposit", "staking.delegationSetup.confirmation.step.dialog.calculatingFees": "Calculating fees", "staking.delegationSetup.confirmation.step.dialog.cancelButtonLabel": "Cancel", "staking.delegationSetup.confirmation.step.dialog.confirmButtonLabel": "Confirm", + "staking.delegationSetup.confirmation.step.dialog.depositLabel": "Deposit", "staking.delegationSetup.confirmation.step.dialog.description": "Confirm your delegation choice to [{selectedPoolTicker}] stake pool for your {selectedWalletName} wallet.", "staking.delegationSetup.confirmation.step.dialog.feesLabel": "Fees", "staking.delegationSetup.confirmation.step.dialog.spendingPasswordLabel": "Spending password", @@ -407,7 +436,6 @@ "staking.redeemItnRewards.noWallets.addWalletButtonLabel": "Add wallet", "staking.redeemItnRewards.noWallets.description": "Redemption of Incentivized Testnet rewards is not available as you currently do not have any Shelley-compatible wallets.", "staking.redeemItnRewards.redemptionUnavailable.closeButton.label": "Close", - "staking.redeemItnRewards.redemptionUnavailable.description": "Before you can redeem your Incentivized Testnet rewards, Daedalus first needs to synchronize with the blockchain. The synchronization process is now underway and is currently {syncPercentage}% complete. As soon as this process is fully complete, you’ll be able to redeem your Incentivized Testnet rewards. Please wait for this process to complete before returning here to redeem your rewards.", "staking.redeemItnRewards.redemptionUnavailable.title": "Redeem Incentivized Testnet rewards", "staking.redeemItnRewards.step1.checkbox1Label": "I understand that redeeming rewards from the Incentivized Testnet requires paying transaction fees.", "staking.redeemItnRewards.step1.checkbox2Label": "I understand that fees will be paid from the wallet I am redeeming my rewards to.", @@ -457,9 +485,12 @@ "staking.rewards.title": "Earned delegation rewards", "staking.stakePools.delegatingListTitle": "Staking pools you are delegating to", "staking.stakePools.learnMore": "Learn more", - "staking.stakePools.listTitle": "Stake pools ({pools})", - "staking.stakePools.listTitleWithSearch": "Stake pools. Search results: ({pools})", + "staking.stakePools.listTitle": "Stake pools", + "staking.stakePools.listTitleLoading": "Loading stake pools", + "staking.stakePools.listTitleSearch": ". Search results:", + "staking.stakePools.listTitleStakePools": " ({pools})", "staking.stakePools.loadingStakePoolsMessage": "Loading stake pools", + "staking.stakePools.moderatedBy": "Moderated by {smashServer}", "staking.stakePools.noDataDashTooltip": "Data not available yet", "staking.stakePools.rankingAllWallets": "all your wallets", "staking.stakePools.rankingAllWalletsEnd": ".", @@ -509,6 +540,7 @@ "staking.stakePools.tooltip.retirement": "Retirement in {retirementFromNow}", "staking.stakePools.tooltip.saturation": "Saturation:", "staking.stakePools.tooltip.saturationTooltip": "Saturation measures the stake in the pool and indicates the point at which rewards stop increasing with increases in stake. This capping mechanism encourages decentralization by discouraging users from delegating to oversaturated stake pools.", + "staking.stakePools.unmoderated": "Unmoderated", "static.about.buildInfo": "{platform} build {build}, {apiName} Node {nodeVersion}, {apiName} Wallet {apiVersion}", "static.about.buildInfoForITN": "{platform} build {build}, with {apiName} {apiVersion}", "static.about.content.cardano.headline": "Cardano Team:", @@ -580,6 +612,76 @@ "test.environment.shelleyTestnetLabel": "Shelley Testnet", "test.environment.stagingLabel": "Staging", "test.environment.testnetLabel": "Testnet", + "voting.info.androidAppButtonUrl": "https://play.google.com/store/apps/details?id=io.iohk.vitvoting", + "voting.info.appleAppButtonUrl": "https://apps.apple.com/in/app/catalyst-voting/id1517473397", + "voting.info.bottomContentDescription": "To register to vote for Catalyst Fund3 you first need to download the Catalyst Voting app on your Android or iOS smartphone.", + "voting.info.bottomContentTitle": "Download the Catalyst Voting app on your smartphone", + "voting.info.buttonLabel": "Register to vote", + "voting.info.checkboxLabel": "I have installed the Catalyst Voting app", + "voting.info.heading": "Register to vote on Fund3", + "voting.info.learnMoreLinkLabel": "Learn more", + "voting.info.learnMoreLinkUrl": "https://cardano.ideascale.com/a/index", + "voting.info.noWallets.createWalletButtonLabel": "Create wallet", + "voting.info.noWallets.headLine": "Voting registration for Fund3 is not available as you currently do not have any Shelley-compatible wallets.", + "voting.info.noWallets.instructions": "Create a new wallet and transfer a minimum of {minVotingFunds} ADA (or restore an existing wallet with funds), then return here to register for voting.", + "voting.info.stepTitle1": "Decide which innovative ideas for Cardano will receive funding.", + "voting.info.stepTitle2": "$70.000 worth of ada rewards will be distributed between ada holders who register their vote.", + "voting.votingRegistration.chooseWallet.step.continueButtonLabel": "Continue", + "voting.votingRegistration.chooseWallet.step.description": "You can only use one wallet when registering. To maximize rewards and voting power, choose the wallet with the largest balance.", + "voting.votingRegistration.chooseWallet.step.errorHardwareWallet": "This wallet cannot be registered for voting as it is a hardware wallet. Hardware wallets will be supported in the future.", + "voting.votingRegistration.chooseWallet.step.errorLegacyWallet": "This wallet cannot be registered for voting as it is a legacy Byron wallet.", + "voting.votingRegistration.chooseWallet.step.errorMinVotingFunds": "This wallet does not contain the minimum required amount of {minVotingRegistrationFunds} ADA. Please select a different wallet with a minimum balance of {minVotingRegistrationFunds} ADA.", + "voting.votingRegistration.chooseWallet.step.errorMinVotingFundsRewardsOnly": "This wallet cannot be registered for voting as it contains rewards balance only.", + "voting.votingRegistration.chooseWallet.step.errorRestoringWallet": "The wallet cannot be registered for voting while it is being synced with the blockchain.", + "voting.votingRegistration.chooseWallet.step.selectWalletInputLabel": "Select a wallet", + "voting.votingRegistration.chooseWallet.step.selectWalletInputPlaceholder": "Select a wallet", + "voting.votingRegistration.confirm.step.confirmationsCountLabel": "{currentCount} of {expectedCount}", + "voting.votingRegistration.confirm.step.continueButtonLabel": "Continue", + "voting.votingRegistration.confirm.step.description": "Confirmation of voting registration requires approximately 5 minutes. Please leave Daedalus running.", + "voting.votingRegistration.confirm.step.descriptionRestart": "Please restart the voting registration process by clicking Restart voting registration.", + "voting.votingRegistration.confirm.step.errorMessage": "The voting registration process was not completed correctly.", + "voting.votingRegistration.confirm.step.restartButtonLabel": "Restart voting registration", + "voting.votingRegistration.confirm.step.transactionConfirmedLabel": "Transaction confirmed", + "voting.votingRegistration.confirm.step.transactionPendingLabel": "Transaction pending...", + "voting.votingRegistration.confirm.step.waitingForConfirmationsLabel": "Waiting for confirmation...", + "voting.votingRegistration.dialog.confirmation.button.cancelButtonLabel": "Cancel registration", + "voting.votingRegistration.dialog.confirmation.button.confirmButtonLabel": "Continue registration", + "voting.votingRegistration.dialog.confirmation.content": "Are you sure that you want to cancel Fund3 voting registration? The transaction fee you paid for the voting registration transaction will be lost and you will need to repeat the registration from the beginning.", + "voting.votingRegistration.dialog.confirmation.headline": "Cancel Fund3 voting registration?", + "voting.votingRegistration.dialog.dialogTitle": "Register for Fund3 voting", + "voting.votingRegistration.dialog.subtitle": "Step {step} of {stepCount}", + "voting.votingRegistration.enterPinCode.step.continueButtonLabel": "Continue", + "voting.votingRegistration.enterPinCode.step.description": "Please enter a PIN for your Fund3 voting registration. The PIN you set here, and the QR code which you will get in the next step, will be required for you to vote using the Catalyst Voting app on your smartphone.", + "voting.votingRegistration.enterPinCode.step.enterPinCodeLabel": "Enter PIN", + "voting.votingRegistration.enterPinCode.step.errors.invalidPinCode": "Invalid PIN", + "voting.votingRegistration.enterPinCode.step.errors.invalidRepeatPinCode": "PIN doesn't match", + "voting.votingRegistration.enterPinCode.step.reminder": "It is important to remember your PIN. If you forget your PIN, you will not be able to use this registration for voting, and you will need to repeat the registration process.", + "voting.votingRegistration.enterPinCode.step.repeatPinCodeLabel": "Repeat PIN", + "voting.votingRegistration.pdf.author": "Daedalus wallet", + "voting.votingRegistration.pdf.filename": "voting-registration", + "voting.votingRegistration.pdf.networkLabel": "Cardano network:", + "voting.votingRegistration.pdf.title": "Fund{fundNumber} Voting Registration", + "voting.votingRegistration.pdf.walletNameLabel": "Wallet name", + "voting.votingRegistration.qrCode.step.checkbox1Label": "I understand that I will not be able to retrieve this QR code again after closing this window.", + "voting.votingRegistration.qrCode.step.checkbox2Label": "I acknowledge that I must have the downloaded PDF with the QR code, to vote with Fund3.", + "voting.votingRegistration.qrCode.step.closeButtonLabel": "Close", + "voting.votingRegistration.qrCode.step.qrCodeDescription": "Open the Catalyst Voting app on your smartphone, scan the QR code, and use the PIN to complete the voting registration process.", + "voting.votingRegistration.qrCode.step.qrCodeTitle": "Please complete your registration now.", + "voting.votingRegistration.qrCode.step.qrCodeWarning": "Warning: After closing this window the QR code will no longer be available. If you do not keep a PDF copy of the QR code, you might not be able to participate in voting.", + "voting.votingRegistration.qrCode.step.saveAsPdfButtonLabel": "Save as PDF", + "voting.votingRegistration.register.step.calculatingFees": "Calculating fees", + "voting.votingRegistration.register.step.continueButtonLabel": "Submit registration transaction", + "voting.votingRegistration.register.step.description": "Please sign the voting registration transaction. This transaction links your wallet balance with your Fund3 voting registration, as a proof of your voting power. Funds will not leave your wallet, but registration requires paying transaction fees, as displayed on-screen.", + "voting.votingRegistration.register.step.feesLabel": "Fees", + "voting.votingRegistration.register.step.learnMoreLink": "Learn more", + "voting.votingRegistration.register.step.learntMoreLinkUrl": "https://cardano.ideascale.com/a/index", + "voting.votingRegistration.register.step.spendingPasswordLabel": "Spending password", + "voting.votingRegistration.register.step.spendingPasswordPlaceholder": "Spending password", + "voting.votingRegistration.steps.step.1.label": "Wallet", + "voting.votingRegistration.steps.step.2.label": "Register", + "voting.votingRegistration.steps.step.3.label": "Confirm", + "voting.votingRegistration.steps.step.4.label": "PIN", + "voting.votingRegistration.steps.step.5.label": "QR code", "wallet.add.dialog.connect.description": "Pair a hardware wallet device", "wallet.add.dialog.connect.label": "Pair", "wallet.add.dialog.create.description": "Create a new wallet", @@ -609,6 +711,7 @@ "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputHint": "Enter your {numberOfWords}-word recovery phrase", "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputLabel": "Verify your recovery phrase", "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputNoResults": "No results", + "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputPlaceholder": "Enter word #{wordNumber}", "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInvalidMnemonics": "Invalid recovery phrase", "wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.offline": "I understand that the simplest way to keep my wallet recovery phrase secure is to never store it digitally or online. If I decide to use an online service, such as a password manager with an encrypted database, it is my responsibility to make sure that I use it correctly.", "wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.recovery": "I understand that the only way to recover my wallet if my computer is lost, broken, stolen, or stops working is to use my wallet recovery phrase.", @@ -627,6 +730,9 @@ "wallet.byron.notification.moveFundsDescription.line2.link.label": "brand new wallet", "wallet.byron.notification.moveFundsTitle": "Move funds from \"{activeWalletName}\"", "wallet.connect.dialog.button.cancel": "Cancel", + "wallet.connect.dialog.connectingIssueSupportLabel": "If you are experiencing issues pairing your hardware wallet device, please {supportLink}.", + "wallet.connect.dialog.connectingIssueSupportLink": "read the instructions", + "wallet.connect.dialog.connectingIssueSupportLinkUrl": "https://iohk.zendesk.com/hc/en-us/articles/900004722083-How-to-use-Ledger-and-Trezor-HW-with-Daedalus", "wallet.connect.dialog.instructions": "

Daedalus supports Ledger Nano S, Ledger Nano X, and Trezor Model T hardware wallet devices.

If you are pairing your device with Daedalus for the first time, please follow the instructions below.

If you have already paired your device with Daedalus, you don’t need to repeat this step. Just connect your device when you need to confirm a transaction.

", "wallet.connect.dialog.instructionsTrezorOnly": "

Daedalus currently supports only Trezor Model T hardware wallet devices.

If you are pairing your device with Daedalus for the first time, please follow the instructions below.

If you have already paired your device with Daedalus, you don’t need to repeat this step. Just connect your device when you need to confirm a transaction.

", "wallet.connect.dialog.title": "Pair a hardware wallet device", @@ -669,7 +775,7 @@ "wallet.hardware.deviceStatus.wrong_firmware.link.label": "Firmware update instructions", "wallet.hardware.deviceStatus.wrong_firmware.link.url": "https://trezor.io/start/", "wallet.import.file.dialog.buttonLabel": "Import wallets", - "wallet.import.file.dialog.description": "

This feature enables you to import wallets from the previous version of Daedalus, from the Daedalus state directory, or from a ‘secret.key’ file.

It can be used to import wallets quickly without entering the wallet recovery phrase for each wallet, or to import wallets for which you have lost your wallet recovery phrase.

After importing a wallet for which you have lost your wallet recovery phrase, please create a new wallet and transfer all funds from the old wallet to the new wallet. Keep the wallet recovery phrase for your new wallet secure.

", + "wallet.import.file.dialog.description": "

This feature enables you to import wallets from ‘secret.key’ files of old versions of Daedalus (Daedalus version 0.15.1 and previous). Importing wallets from state directories of version Daedalus 1.0 onwards is currently not supported.

It can be used to import wallets quickly without entering the wallet recovery phrase for each wallet, or to import wallets for which you have lost your wallet recovery phrase.

After importing a wallet for which you have lost your wallet recovery phrase, please create a new wallet and transfer all funds from the old wallet to the new wallet. Keep the wallet recovery phrase for your new wallet secure.

", "wallet.import.file.dialog.importFromLabel": "Import from:", "wallet.import.file.dialog.linkLabel": "Learn more", "wallet.import.file.dialog.linkUrl": "https://iohk.zendesk.com/hc/en-us/articles/900000623463", @@ -680,7 +786,7 @@ "wallet.import.file.dialog.stateDirOptionLabel": "Daedalus state directory", "wallet.import.file.dialog.stateFolderLabel": "Select Daedalus state directory:", "wallet.import.file.dialog.title": "Import wallets", - "wallet.legacy.notification.descriptionWithFunds": "!!!\"{transferWalletName}\"\" is a legacy wallet. It does not support Shelley delegation features. To earn ada from delegating your stake, please move all funds from this wallet to a new, Shelley-compatible wallet. You can create a brand new wallet or move funds to one of the existing wallets.", + "wallet.legacy.notification.descriptionWithFunds": "\"{transferWalletName}\"\" is a legacy wallet. It does not support Shelley delegation features. To earn ada from delegating your stake, please move all funds from this wallet to a new, Shelley-compatible wallet. You can create a brand new wallet or move funds to one of the existing wallets.", "wallet.navigation.more": "More", "wallet.navigation.receive": "Receive", "wallet.navigation.send": "Send", @@ -740,6 +846,7 @@ "wallet.restore.dialog.restore.wallet.button.label": "Restore wallet", "wallet.restore.dialog.shielded.recovery.phrase.input.hint": "Enter your {numberOfWords}-word paper wallet recovery phrase", "wallet.restore.dialog.shielded.recovery.phrase.input.label": "27-word paper wallet recovery phrase", + "wallet.restore.dialog.shielded.recovery.phrase.input.placeholder": "Enter word #{wordNumber}", "wallet.restore.dialog.spendingPasswordLabel": "Enter password", "wallet.restore.dialog.step.configuration.continueButtonLabel": "Continue", "wallet.restore.dialog.step.configuration.description1": "Name your restored wallet and set a spending password to keep your wallet secure.", @@ -753,7 +860,7 @@ "wallet.restore.dialog.step.mnemonics.autocomplete.invalidRecoveryPhrase": "Invalid recovery phrase", "wallet.restore.dialog.step.mnemonics.autocomplete.multiLengthPhrase.placeholder": "Enter your 12, 18 or 24-word recovery phrase", "wallet.restore.dialog.step.mnemonics.autocomplete.noResults": "No results", - "wallet.restore.dialog.step.mnemonics.autocomplete.placeholder": "Enter your {numberOfWords}-word recovery phrase", + "wallet.restore.dialog.step.mnemonics.autocomplete.placeholder": "Enter word #{wordNumber}", "wallet.restore.dialog.step.success.dialog.close": "Close", "wallet.restore.dialog.step.success.dialog.description.line1": "Your wallet has been successfully restored.", "wallet.restore.dialog.step.success.dialog.description.line2": "Restored wallets should have all the funds and transaction history of the original wallet. If your restored wallet does not have the funds and transaction history you were expecting, please check that you have the correct wallet recovery phrase for the wallet you were intending to restore.", @@ -829,7 +936,7 @@ "wallet.send.form.syncingTransactionsMessage": "The balance and transaction history of this wallet is being synced with the blockchain.", "wallet.send.form.title.hint": "E.g: Money for Frank", "wallet.send.form.title.label": "Title", - "wallet.settings.assurance": "!!!Transaction assurance security level", + "wallet.settings.assurance": "Transaction assurance security level", "wallet.settings.changePassword.dialog.currentPasswordFieldPlaceholder": "Type current password", "wallet.settings.changePassword.dialog.currentPasswordLabel": "Current password", "wallet.settings.changePassword.dialog.newPasswordFieldPlaceholder": "Type new password", @@ -856,8 +963,8 @@ "wallet.settings.password": "Password", "wallet.settings.passwordLastUpdated": "Last updated {lastUpdated}", "wallet.settings.passwordNotSet": "You still don't have password", - "wallet.settings.recoveryPhraseInputHint": "Enter recovery phrase", "wallet.settings.recoveryPhraseInputNoResults": "No results", + "wallet.settings.recoveryPhraseInputPlaceholder": "Enter word #{wordNumber}", "wallet.settings.recoveryPhraseStep1Button": "Continue", "wallet.settings.recoveryPhraseStep1Paragraph1": "To verify that you have the correct recovery phrase for this wallet, you can enter it on the following screen.", "wallet.settings.recoveryPhraseStep1Paragraph2": "Are you being watched? Please make sure that nobody can see your screen while you are entering your wallet recovery phrase.", @@ -911,6 +1018,9 @@ "wallet.settings.walletPublicKeyShowInstruction": "To show wallet public key click \"Reveal\" button", "wallet.statusMessages.activeRestore": "The balance and transaction history of this wallet is {percentage}% synced with the blockchain.", "wallet.summary.no.transactions": "No recent transactions", + "wallet.summary.page.currency.isFetchingRate": "fetching conversion rates", + "wallet.summary.page.currency.lastFetched": "converted {fetchedTimeAgo}", + "wallet.summary.page.currency.title": "Converts as", "wallet.summary.page.pendingTransactionsLabel": "Number of pending transactions", "wallet.summary.page.showMoreTransactionsButtonLabel": "Show more transactions", "wallet.summary.page.syncingTransactionsMessage": "Your transaction history for this wallet is being synced with the blockchain.", @@ -922,6 +1032,7 @@ "wallet.transaction.addresses.from": "From addresses", "wallet.transaction.addresses.to": "To addresses", "wallet.transaction.conversion.rate": "Conversion rate", + "wallet.transaction.deposit": "Deposit", "wallet.transaction.failed.cancelFailedTxnNote": "This transaction was submitted to the Cardano network, but it failed as it had expired. Transactions on the Cardano network have a ‘time to live’ attribute, which had passed before the network processed the transaction. You need to remove it to release the funds (UTXOs) used by this transaction for use in another transaction.", "wallet.transaction.failed.cancelFailedTxnSupportArticle": "Why should I cancel failed transactions?", "wallet.transaction.failed.removeTransactionButton": "Remove failed transaction", @@ -939,6 +1050,9 @@ "wallet.transaction.filter.resultInfo": "{filtered} out of {total} transactions match your filter.", "wallet.transaction.filter.selectTimeRange": "Select time range", "wallet.transaction.filter.thisYear": "This year", + "wallet.transaction.metadataConfirmationLabel": "Show unmoderated content", + "wallet.transaction.metadataDisclaimer": "Transaction metadata is not moderated and may contain inappropriate content.", + "wallet.transaction.metadataLabel": "Transaction metadata", "wallet.transaction.noInputAddressesLabel": "No addresses", "wallet.transaction.pending.cancelPendingTxnNote": "This transaction has been pending for a long time. To release the funds used by this transaction, you can try canceling it.", "wallet.transaction.pending.cancelPendingTxnSupportArticle": "Why should I cancel this transaction?", @@ -951,6 +1065,7 @@ "wallet.transaction.state.failed": "Transaction failed", "wallet.transaction.state.pending": "Transaction pending", "wallet.transaction.transactionAmount": "Transaction amount", + "wallet.transaction.transactionFee": "Transaction fee", "wallet.transaction.transactionId": "Transaction ID", "wallet.transaction.type": "{currency} transaction", "wallet.transaction.type.card": "Card Payment", diff --git a/source/renderer/app/i18n/locales/ja-JP.json b/source/renderer/app/i18n/locales/ja-JP.json index e7659088aa..1d9422f0d0 100755 --- a/source/renderer/app/i18n/locales/ja-JP.json +++ b/source/renderer/app/i18n/locales/ja-JP.json @@ -15,6 +15,7 @@ "api.errors.WalletFileImportError": "ウォレットをインポートできませんでした。有効なファイルを指定していることを確認してください。", "api.errors.inputsDepleted": "報酬残高のみを含むウォレットからの送信はできません。", "api.errors.invalidAddress": "有効なアドレスを入力してください。", + "api.errors.invalidSmashServer": "このURLは有効なSMASHサーバーではありません", "api.errors.nothingToMigrate": "このウォレットに保有されている未使用トランザクションアウトプット(UTXO)の一部に、移行するために十分なADAが入っていないため、このウォレットから資金を移すことはできません。", "api.errors.utxoTooSmall": "無効なトランザクションです。", "appUpdate.overlay.button.installUpdate.label": "更新をインストールしてDaedalusを再起動する", @@ -23,7 +24,7 @@ "appUpdate.overlay.downloadProgressData": "({totalDownloaded}/{totalDownloadSize}をダウンロード済み)", "appUpdate.overlay.downloadProgressLabel": "ダウンロードしています", "appUpdate.overlay.downloadTimeLeft": "残り{downloadTimeLeft}", - "appUpdate.overlay.installingUpdate.link.label": "更新をインストールしています…", + "appUpdate.overlay.installingUpdate.link.label": "更新をインストールしています...", "appUpdate.overlay.manualUpdate.button.label": "指示に従い手動で更新してください", "appUpdate.overlay.manualUpdate.button.url": "https://daedaluswallet.io/ja/download/", "appUpdate.overlay.manualUpdate.description.action": "手動でDaedalusを最新バージョンに更新してください。", @@ -49,7 +50,7 @@ "daedalus.diagnostics.dialog.cardanoNodeState": "Cardanoノードステータス", "daedalus.diagnostics.dialog.cardanoNodeStatus": "CARDANOノードステータス", "daedalus.diagnostics.dialog.cardanoNodeStatusRestart": "Cardanoノードを再起動する", - "daedalus.diagnostics.dialog.cardanoNodeStatusRestarting": "Cardanoノードを再起動しています…", + "daedalus.diagnostics.dialog.cardanoNodeStatusRestarting": "Cardanoノードを再起動しています...", "daedalus.diagnostics.dialog.cardanoNodeSubscribed": "Cardanoノード受信", "daedalus.diagnostics.dialog.cardanoNodeSyncing": "Cardanoノード同期中", "daedalus.diagnostics.dialog.cardanoNodeTimeCorrect": "Cardanoノード時刻正常", @@ -73,7 +74,7 @@ "daedalus.diagnostics.dialog.lastSynchronizedBlock": "最新の同期済みブロック", "daedalus.diagnostics.dialog.localTimeDifference": "時差", "daedalus.diagnostics.dialog.localTimeDifferenceCheckTime": "時刻を確認する", - "daedalus.diagnostics.dialog.localTimeDifferenceChecking": "確認しています…", + "daedalus.diagnostics.dialog.localTimeDifferenceChecking": "確認しています...", "daedalus.diagnostics.dialog.message": "メッセージ", "daedalus.diagnostics.dialog.nodeHasBeenUpdated": "更新済み", "daedalus.diagnostics.dialog.nodeHasCrashed": "クラッシュ", @@ -134,6 +135,7 @@ "global.errors.rewardsOpenCsvError": "置き換えようとしているファイルが開いています。ファイルを閉じてからもう一度お試しください。", "global.errors.strongSpendingPassword": "強い", "global.errors.weakSpendingPassword": "弱い", + "global.info.featureUnavailableWhileSyncing": "Daedalusは現在Cardanoブロックチェーンと同期中であり、{syncPercentage}%完了しています。この機能はDaedalusが完全に同期すると有効になります。", "global.info.knownMnemonicWordCount": "{actual}/{required}語入力済み", "global.info.unknownMnemonicWordCount": "{actual}語入力済み", "global.labels.all": "すべて", @@ -188,7 +190,7 @@ "loading.screen.updatingCardanoMessage": "Cardanoノードを更新しています", "loading.screen.verifyingBlockchainMessage": "ブロックチェーンを検証しています({verificationProgress}%完了)", "news.newsfeed.empty": "ニュースフィードは空です", - "news.newsfeed.noFetch": "ニュースフィードを読み込んでいます…", + "news.newsfeed.noFetch": "ニュースフィードを読み込んでいます...", "news.newsfeed.title": "ニュースフィード", "noDiskSpace.error.overlayContent": "Daedalusを動作させるには、ハードディスクに{diskSpaceRequired}以上の空き容量が必要です。ご使用のコンピューターには空き容量が{diskSpaceMissing}不足しています。Daedalusのご利用を続けるためには、ファイルをいくつか削除して空き容量を増やしてください。

オペレーティングシステムとインストール済みプログラムを正常かつ安定した状態で動作させるには、ハードディスクに少なくとも15%(ご使用のコンピューターの場合は{diskSpaceRecommended})の空き容量が必要です。ハードディスクにこれ以上の空き容量を確保してください。", "noDiskSpace.error.overlayTitle": "ハードディスクの空き容量が不足しているためDaedalusを作動できません", @@ -201,6 +203,7 @@ "notification.downloadQRCodeImageSuccess": "アドレス:{walletAddress} QRコードの画像をダウンロードしました", "notification.downloadRewardsCSVSuccess": "CSVファイルのダウンロードが完了しました", "notification.downloadTransactionsCSVSuccess": "CSVファイルのダウンロードが完了しました", + "notification.downloadVotingPDFSuccess": "PDFをダウンロードしました", "paper.wallet.create.certificate.completion.dialog.addressCopiedLabel": "コピーされました", "paper.wallet.create.certificate.completion.dialog.addressInstructions": "ペーパーウォレットを用いて資金を受け取るには、ウォレットアドレスを共有してください。", "paper.wallet.create.certificate.completion.dialog.addressLabel": "ウォレットアドレス", @@ -275,8 +278,27 @@ "settings.display.themeNames.yellow": "イエロー", "settings.menu.display.link.label": "テーマ", "settings.menu.general.link.label": "一般", + "settings.menu.stakePools.link.label": "ステークプール", "settings.menu.support.link.label": "ユーザーサポート", "settings.menu.termsOfUse.link.label": "サービス利用規約", + "settings.menu.wallets.link.label": "ウォレット", + "settings.stakePools.smash.description": "{link}は、ステークプールの詳細を素早くロードすることを可能にする、オフチェーンのメタデータサーバーです。ステークプールは、サーバーごとに異なるポリシーに基づいて調整されます。", + "settings.stakePools.smash.descriptionIOHKContent1": "IOHKサーバーは、登録されたステークプールが有効であることを確認し、ティッカー名や商標の重複を回避し、プールが攻撃的または害となる可能性のある情報を有していないかチェックします。", + "settings.stakePools.smash.descriptionIOHKContent2": "これで問題を起こす可能性のあるアクターをフィルタリングすることにより、詐欺や荒らし、虐待行為に対処することができます。IOHKのSMASHサーバーの詳細は{link}。", + "settings.stakePools.smash.descriptionIOHKLinkLabel": "こちらをご覧ください", + "settings.stakePools.smash.descriptionIOHKLinkUrl": "https://forum.cardano.org/t/iohk-smash/42230", + "settings.stakePools.smash.descriptionLinkLabel": "ステークプールメタデータアグリゲーションサーバー(SMASH)(英語のみ)", + "settings.stakePools.smash.descriptionLinkUrl": "https://docs.cardano.org/en/latest/getting-started/stake-pool-operators/SMASH-metadata-management.html", + "settings.stakePools.smash.descriptionNone": "このオプションは推奨されません。オフチェーンメタデータサーバーを使用しない場合、Daedalusクライアントはすべてのステークプールと個別に接触してこのデータをフェッチすることになります。このプロセスは大量の時間とリソースを消費します。受信したステークプールのリストは調整されていないため、Daedalusは合法的なプールも、重複やフェイクのプールもすべて取得します。このプロセスの追加的リスクとして、ウィルス対策ソフトウェアが何千ものネットワークリクエストを、Daedalusクライアントによる悪意のある行動と見なす可能性があります。", + "settings.stakePools.smash.select.IOHKServer": "IOHK(推奨)", + "settings.stakePools.smash.select.customServer": "カスタムサーバー", + "settings.stakePools.smash.select.direct": "使用しない(直接データをフェッチする)", + "settings.stakePools.smash.select.label": "オフチェーンメタデータサーバー(SMASH)", + "settings.stakePools.smashUrl.input.invalidUrl": "無効なURL", + "settings.stakePools.smashUrl.input.invalidUrlParameter": "「https://」プロトコルおよびホスト名(例:domain.com)のみ使用できます", + "settings.stakePools.smashUrl.input.invalidUrlPrefix": "URLは「https://」から始めてください", + "settings.stakePools.smashUrl.input.label": "SMASHサーバーURL", + "settings.stakePools.smashUrl.input.placeholder": "カスタムサーバーを入力してください", "settings.support.faq.content": "問題が発生している場合は、サポートページにある{faqLink}リストを参照して解決方法を探してください。解決方法が見つからない場合はサポートリクエストを送信してください。", "settings.support.faq.faqLink": "既知の問題", "settings.support.faq.faqLinkURL": "https://daedaluswallet.io/ja/known-issues/", @@ -289,6 +311,11 @@ "settings.support.steps.reportProblem.link": "ご使用のブラウザーでサポートリクエストフォームを開き", "settings.support.steps.reportProblem.title": "問題を報告する", "settings.support.steps.title": "サポートリクエストの作成方法:", + "settings.wallets.currency.description": "ADA残高を表示する通貨を選択してください。", + "settings.wallets.currency.disclaimer": "換金レートは{currencyApiName}により保証なしで提供されています。換算値は参考としてのみ使用してください。
換金後残高は、{currencyApiName}が追跡した、アクティブな暗号通貨取引所における現在の世界的なADA平均価格を反映しています。ADAの換金は、{currencyApiName}がサポートする法定通貨および暗号通貨にのみ可能です。その他の現地通貨への換金は不可能な場合があります。", + "settings.wallets.currency.poweredBy.label": "提供:", + "settings.wallets.currency.selectLabel": "通貨を選択してください", + "settings.wallets.currency.titleLabel": "ADA残高を別の通貨で表示する", "sidebar.wallets.addWallet": "ウォレット追加", "staking.countdown.learnMore.linkUrl": "https://iohk.zendesk.com/hc/ja", "staking.delegationCenter.bodyTitle": "ウォレット", @@ -354,9 +381,11 @@ "staking.delegationSetup.chooseWallet.step.dialog.stepIndicatorLabel": "ステップ{currentStep}/{totalSteps}", "staking.delegationSetup.chooseWallet.step.dialog.syncingWallet": "同期", "staking.delegationSetup.chooseWallet.step.dialog.title": "ウォレットを委任する", + "staking.delegationSetup.confirmation.step.dialog.calculatingDeposit": "デポジットを計算しています", "staking.delegationSetup.confirmation.step.dialog.calculatingFees": "手数料を計算しています", "staking.delegationSetup.confirmation.step.dialog.cancelButtonLabel": "キャンセル", "staking.delegationSetup.confirmation.step.dialog.confirmButtonLabel": "確認", + "staking.delegationSetup.confirmation.step.dialog.depositLabel": "デポジット", "staking.delegationSetup.confirmation.step.dialog.description": "{selectedWalletName}ウォレットの委任先が「{selectedPoolTicker}」ステークプールであることを確認してください。", "staking.delegationSetup.confirmation.step.dialog.feesLabel": "手数料", "staking.delegationSetup.confirmation.step.dialog.spendingPasswordLabel": "送信時パスワード", @@ -407,7 +436,6 @@ "staking.redeemItnRewards.noWallets.addWalletButtonLabel": "ウォレット追加", "staking.redeemItnRewards.noWallets.description": "インセンティブ付きテストネットの還元機能はShelley対応のウォレットがないため利用できません。", "staking.redeemItnRewards.redemptionUnavailable.closeButton.label": "閉じる", - "staking.redeemItnRewards.redemptionUnavailable.description": "インセンティブ付きテストネットの報酬を還元するには、Daedalusがブロックチェーンと同期している必要があります。現在同期プロセスは進行中で、{syncPercentage}%完了しています。このプロセスが完了したら、インセンティブ付きテストネットの報酬を還元することができるようになります。ここで報酬を還元するのは、このプロセスが終了するまでお待ちください。", "staking.redeemItnRewards.redemptionUnavailable.title": "インセンティブ付きテストネットの報酬を還元する", "staking.redeemItnRewards.step1.checkbox1Label": "インセンティブ付きテストネットで獲得した報酬の還元には、トランザクション手数料を支払う必要があることを理解しました。", "staking.redeemItnRewards.step1.checkbox2Label": "手数料は報酬の振込先ウォレットから支払われることを理解しました。", @@ -457,9 +485,12 @@ "staking.rewards.title": "獲得した委任報酬", "staking.stakePools.delegatingListTitle": "現在委任しているステークプール", "staking.stakePools.learnMore": "もっと知る", - "staking.stakePools.listTitle": "ステークプール ({pools})", - "staking.stakePools.listTitleWithSearch": "ステークプール検索結果:({pools})", + "staking.stakePools.listTitle": "ステークプール", + "staking.stakePools.listTitleLoading": "ステークプールをロード中", + "staking.stakePools.listTitleSearch": "。検索結果:", + "staking.stakePools.listTitleStakePools": "({pools})", "staking.stakePools.loadingStakePoolsMessage": "ステークプールをロードしています", + "staking.stakePools.moderatedBy": "{smashServer}が調整済み", "staking.stakePools.noDataDashTooltip": "まだ利用できるデータがありません", "staking.stakePools.rankingAllWallets": "すべてのウォレット", "staking.stakePools.rankingAllWalletsEnd": "の合計額を基にランク付けされています。", @@ -509,6 +540,7 @@ "staking.stakePools.tooltip.retirement": "あと{retirementFromNow}で終了", "staking.stakePools.tooltip.saturation": "飽和:", "staking.stakePools.tooltip.saturationTooltip": "プールのステーク数を計測し、プール数が増加しても、報酬増加に反映されなくなる時点を示すもので、この上限を定めるメカニズムにより、ユーザーが飽和状態のステークプールに委任することを抑制することで、分散化を促進する", + "staking.stakePools.unmoderated": "未調整", "static.about.buildInfo": "{platform} ビルド {build}, {apiName} Node {nodeVersion}, {apiName} Wallet {apiVersion}", "static.about.buildInfoForITN": "{platform} ビルド {build}, {apiName} {apiVersion}", "static.about.content.cardano.headline": "Cardanoチーム:", @@ -580,6 +612,76 @@ "test.environment.shelleyTestnetLabel": "Shelleyテストネット", "test.environment.stagingLabel": "ステージング", "test.environment.testnetLabel": "テストネット", + "voting.info.androidAppButtonUrl": "https://play.google.com/store/apps/details?id=io.iohk.vitvoting", + "voting.info.appleAppButtonUrl": "https://apps.apple.com/in/app/catalyst-voting/id1517473397", + "voting.info.bottomContentDescription": "Catalyst Fund3に有権者登録を行うためには、まずAndroidまたはiOSのスマートフォンにCatalyst Votingアプリをダウンロードする必要があります。", + "voting.info.bottomContentTitle": "スマートフォンにCatalyst Votingアプリをダウンロードします", + "voting.info.buttonLabel": "有権者登録をする", + "voting.info.checkboxLabel": "Catalyst Votingアプリをインストールしました", + "voting.info.heading": "Fund3の有権者登録をする", + "voting.info.learnMoreLinkLabel": "もっと知る", + "voting.info.learnMoreLinkUrl": "https://cardano.ideascale.com/a/index", + "voting.info.noWallets.createWalletButtonLabel": "ウォレットを作成する", + "voting.info.noWallets.headLine": "現在Shelley対応のウォレットがないため、Fund3の有権者登録はできません。", + "voting.info.noWallets.instructions": "ウォレットを作成し、そこに{minVotingFunds}ADA以上を移してから(または既存の資金入りウォレットを復元してから)、改めてここで有権者登録を行ってください。", + "voting.info.stepTitle1": "Cardanoの革新的なアイデアの中で、どのアイデアに資金を提供するか決定します。", + "voting.info.stepTitle2": "70,000米ドル相当のADA報酬が、有権者登録を行ったADA保有者に分配されます。", + "voting.votingRegistration.chooseWallet.step.continueButtonLabel": "続ける", + "voting.votingRegistration.chooseWallet.step.description": "投票に使用できるウォレットは1つだけです。報酬と議決権を最大にするには、残高の一番大きなウォレットを選択してください。", + "voting.votingRegistration.chooseWallet.step.errorHardwareWallet": "このウォレットはハードウェアウォレットであるため、有権者登録に使用できません。ハードウェアウォレットは今後サポートされる予定です。", + "voting.votingRegistration.chooseWallet.step.errorLegacyWallet": "このウォレットはByronレガシーウォレットであるため、有権者登録に使用できません。", + "voting.votingRegistration.chooseWallet.step.errorMinVotingFunds": "このウォレットには必要最低額{minVotingRegistrationFunds}ADAが入っていません。必要最低額{minVotingRegistrationFunds}ADAが入った別のウォレットを選択してください。", + "voting.votingRegistration.chooseWallet.step.errorMinVotingFundsRewardsOnly": "このウォレットに含まれているのは報酬残高のみであり、投票用には使用できません。", + "voting.votingRegistration.chooseWallet.step.errorRestoringWallet": "このウォレットはブロックチェーンと同期中のため、投票用に使用することはできません。", + "voting.votingRegistration.chooseWallet.step.selectWalletInputLabel": "ウォレットを選択してください", + "voting.votingRegistration.chooseWallet.step.selectWalletInputPlaceholder": "ウォレットを選択してください", + "voting.votingRegistration.confirm.step.confirmationsCountLabel": "{currentCount}/{expectedCount}", + "voting.votingRegistration.confirm.step.continueButtonLabel": "続ける", + "voting.votingRegistration.confirm.step.description": "有権者登録の確認にはおよそ5分かかります。Daedalusを実行させたままにしておいてください。", + "voting.votingRegistration.confirm.step.descriptionRestart": "[有権者登録をやり直す]をクリックして、有権者登録手続きをやり直してください。", + "voting.votingRegistration.confirm.step.errorMessage": "有権者登録手続きは正しく完了しませんでした。", + "voting.votingRegistration.confirm.step.restartButtonLabel": "有権者登録をやり直す", + "voting.votingRegistration.confirm.step.transactionConfirmedLabel": "トランザクションが確認されました", + "voting.votingRegistration.confirm.step.transactionPendingLabel": "トランザクション保留中...", + "voting.votingRegistration.confirm.step.waitingForConfirmationsLabel": "確認を待っています...", + "voting.votingRegistration.dialog.confirmation.button.cancelButtonLabel": "登録をキャンセルする", + "voting.votingRegistration.dialog.confirmation.button.confirmButtonLabel": "登録を続ける", + "voting.votingRegistration.dialog.confirmation.content": "Fund3の有権者登録をキャンセルしてもよろしいですか。有権者登録トランザクション用に支払ったトランザクション手数料は失われ、登録手続きは最初からやり直す必要があります。", + "voting.votingRegistration.dialog.confirmation.headline": "Fund3の有権者登録をキャンセルしますか", + "voting.votingRegistration.dialog.dialogTitle": "Fund3の有権者登録をする", + "voting.votingRegistration.dialog.subtitle": "ステップ{step}/{stepCount}", + "voting.votingRegistration.enterPinCode.step.continueButtonLabel": "続ける", + "voting.votingRegistration.enterPinCode.step.description": "Fund3有権者登録のPINコードを入力してください。ここで設定したPINコードと、次のステップで表示されるQRコードは、スマートフォンでCatalyst Votingアプリを使用して投票するときに必要となります。", + "voting.votingRegistration.enterPinCode.step.enterPinCodeLabel": "PINコードを入力してください", + "voting.votingRegistration.enterPinCode.step.errors.invalidPinCode": "!!!Invalid PIN", + "voting.votingRegistration.enterPinCode.step.errors.invalidRepeatPinCode": "PINコードが一致しません", + "voting.votingRegistration.enterPinCode.step.reminder": "PINコードを忘れないでください。PINを忘れると本登録で投票することができなくなり、登録手続きをやり直すことになります。", + "voting.votingRegistration.enterPinCode.step.repeatPinCodeLabel": "PINコードを再入力してください", + "voting.votingRegistration.pdf.author": "Daedalusウォレット", + "voting.votingRegistration.pdf.filename": "有権者登録", + "voting.votingRegistration.pdf.networkLabel": "Cardanoネットワーク:", + "voting.votingRegistration.pdf.title": "FUND{fundNumber}有権者登録", + "voting.votingRegistration.pdf.walletNameLabel": "ウォレット名", + "voting.votingRegistration.qrCode.step.checkbox1Label": "ウィンドウを閉じるとQRコードを表示できなくなることを理解しました。", + "voting.votingRegistration.qrCode.step.checkbox2Label": "Fund3に投票するためには、QRコードをPDFでダウンロードする必要があることを認識しています。", + "voting.votingRegistration.qrCode.step.closeButtonLabel": "閉じる", + "voting.votingRegistration.qrCode.step.qrCodeDescription": "スマートフォンでCatalyst Votingアプリを開き、QRコードをスキャンし、PINコードを使用して有権者登録手続きを完了してください。", + "voting.votingRegistration.qrCode.step.qrCodeTitle": "登録を完了してください", + "voting.votingRegistration.qrCode.step.qrCodeWarning": "警告:このウィンドウを閉じるとQRコードを再び表示することはできません。QRコードのPDFコピーを保存していない場合、投票に参加できなくなる可能性があります。", + "voting.votingRegistration.qrCode.step.saveAsPdfButtonLabel": "PDFで保存する", + "voting.votingRegistration.register.step.calculatingFees": "手数料を計算しています", + "voting.votingRegistration.register.step.continueButtonLabel": "登録トランザクションを送信する", + "voting.votingRegistration.register.step.description": "有権者登録トランザクションに署名してください。このトランザクションは、ウォレット残高とFund3有権者登録を結びつけ、議決権を証明するものとなります。資金はウォレットに残されますが、登録には画面に表示されているトランザクション手数料が必要となります。", + "voting.votingRegistration.register.step.feesLabel": "手数料", + "voting.votingRegistration.register.step.learnMoreLink": "もっと知る", + "voting.votingRegistration.register.step.learntMoreLinkUrl": "https://cardano.ideascale.com/a/index", + "voting.votingRegistration.register.step.spendingPasswordLabel": "送信時パスワード", + "voting.votingRegistration.register.step.spendingPasswordPlaceholder": "送信時パスワード", + "voting.votingRegistration.steps.step.1.label": "ウォレット", + "voting.votingRegistration.steps.step.2.label": "登録", + "voting.votingRegistration.steps.step.3.label": "確認", + "voting.votingRegistration.steps.step.4.label": "PINコード", + "voting.votingRegistration.steps.step.5.label": "QRコード", "wallet.add.dialog.connect.description": "ハードウェアウォレットデバイスをペアリングする", "wallet.add.dialog.connect.label": "ペアリング", "wallet.add.dialog.create.description": "ウォレットを新規作成する", @@ -609,6 +711,7 @@ "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputHint": "{numberOfWords}語のウォレット復元フレーズを入力してください", "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputLabel": "復元フレーズを検証してください", "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputNoResults": "該当するフレーズは見つかりませんでした", + "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInputPlaceholder": "{wordNumber}番目の単語を入力", "wallet.backup.recovery.phrase.entry.dialog.recoveryPhraseInvalidMnemonics": "無効な復元フレーズ", "wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.offline": "ウォレット復元フレーズを安全に保管する最もシンプルな方法はデジタルまたはオンラインに保存しないことであることを理解しました。暗号化データベース付きパスワードマネージャーなどオンラインサービスを使用する場合は、自己責任で適切に使用します。", "wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.recovery": "コンピューターが喪失、故障、盗難、作動不能などとなった場合、ウォレットは復元フレーズを使用することによってのみ復元することができることを理解しました。", @@ -627,6 +730,9 @@ "wallet.byron.notification.moveFundsDescription.line2.link.label": "ウォレットを新規作成する", "wallet.byron.notification.moveFundsTitle": "「{activeWalletName}」の資金を移してください", "wallet.connect.dialog.button.cancel": "キャンセル", + "wallet.connect.dialog.connectingIssueSupportLabel": "ハードウェアウォレットデバイスのペアリングで問題が生じた場合は、{supportLink}してください。", + "wallet.connect.dialog.connectingIssueSupportLink": "ガイドを参照", + "wallet.connect.dialog.connectingIssueSupportLinkUrl": "https://iohk.zendesk.com/hc/ja/articles/900004722083-How-to-use-Ledger-and-Trezor-HW-with-Daedalus", "wallet.connect.dialog.instructions": "

DaedalusはLedger Nano S、Ledger Nano X、Trezor Model Tハードウェアウォレットデバイスをサポートしています。

デバイスを初めてDaedalusにペアリングする場合は、以下の指示に従ってください。

Daedalusとペアリング済みであるデバイスの場合には、この手順を繰り返す必要はありません。トランザクションを確認する必要があるときに、デバイスを接続してください。

", "wallet.connect.dialog.instructionsTrezorOnly": "

Daedalusが現在サポートするハードウェアウォレットデバイスはTrezor Model Tのみです。

デバイスを初めてDaedalusにペアリングする場合は、以下の指示に従ってください。

Daedalusとペアリング済みであるデバイスの場合には、この手順を繰り返す必要はありません。トランザクションを確認する必要があるときに、デバイスを接続してください。

", "wallet.connect.dialog.title": "ハードウェアウォレットを接続する", @@ -669,7 +775,7 @@ "wallet.hardware.deviceStatus.wrong_firmware.link.label": "ファームウェア更新ガイド", "wallet.hardware.deviceStatus.wrong_firmware.link.url": "https://trezor.io/start/", "wallet.import.file.dialog.buttonLabel": "ウォレットをインポートする", - "wallet.import.file.dialog.description": "

この機能により、Daedalusの旧バージョン、Daedalusステータスディレクトリー、またはsecret.keyファイルからウォレットをインポートすることができます。

各ウォレットの復元フレーズを入力せずに素早くウォレットをインポートできるほか、復元フレーズを紛失したウォレットのインポートも可能です。

復元フレーズを紛失したウォレットをインポートした場合は、インポート後に新規ウォレットを作成してすべての資金を旧ウォレットから移し、新しいウォレットの復元フレーズを安全な場所に保管してください。

", + "wallet.import.file.dialog.description": "

この機能により、Daedalus旧バージョン(Daedalus 0.15.1以前)の「secret.key」からウォレットをインポートすることができます。Daedalus 1.0以降のステータスディレクトリーからのウォレットインポートは現在サポートされていません。

各ウォレットの復元フレーズを入力せずに素早くウォレットをインポートできるほか、復元フレーズを紛失したウォレットのインポートも可能です。

復元フレーズを紛失したウォレットをインポートした場合は、インポート後に新規ウォレットを作成してすべての資金を旧ウォレットから移し、新しいウォレットの復元フレーズを安全な場所に保管してください。

", "wallet.import.file.dialog.importFromLabel": "インポート元:", "wallet.import.file.dialog.linkLabel": "もっと知る", "wallet.import.file.dialog.linkUrl": "https://iohk.zendesk.com/hc/ja/articles/900000623463", @@ -740,6 +846,7 @@ "wallet.restore.dialog.restore.wallet.button.label": "ウォレットを復元する", "wallet.restore.dialog.shielded.recovery.phrase.input.hint": "{numberOfWords}語のペーパーウォレット復元フレーズを入力してください。", "wallet.restore.dialog.shielded.recovery.phrase.input.label": "27語のペーパーウォレット復元フレーズ", + "wallet.restore.dialog.shielded.recovery.phrase.input.placeholder": "{wordNumber}番目の単語を入力", "wallet.restore.dialog.spendingPasswordLabel": "パスワードを入力してください", "wallet.restore.dialog.step.configuration.continueButtonLabel": "続ける", "wallet.restore.dialog.step.configuration.description1": "復元したウォレットに名前を付け、ウォレットを安全に保つために送信時パスワードを設定してください。", @@ -753,7 +860,7 @@ "wallet.restore.dialog.step.mnemonics.autocomplete.invalidRecoveryPhrase": "無効な復元フレーズ", "wallet.restore.dialog.step.mnemonics.autocomplete.multiLengthPhrase.placeholder": "12語、18語、または24語のウォレット復元フレーズを入力してください", "wallet.restore.dialog.step.mnemonics.autocomplete.noResults": "該当するフレーズは見つかりませんでした", - "wallet.restore.dialog.step.mnemonics.autocomplete.placeholder": "{numberOfWords}語のウォレット復元フレーズを入力してください", + "wallet.restore.dialog.step.mnemonics.autocomplete.placeholder": "{wordNumber}番目の単語を入力", "wallet.restore.dialog.step.success.dialog.close": "閉じる", "wallet.restore.dialog.step.success.dialog.description.line1": "ウォレットは無事に復元されました。", "wallet.restore.dialog.step.success.dialog.description.line2": "復元されたウォレットにはオリジナルウォレットの資金およびトランザクション履歴がすべて含まれているはずです。復元されたウォレットに期待していた資金やトランザクション履歴が入っていない場合には, 復元対象となるウォレットの正しい復元フレーズを使用しているか確認してください。", @@ -794,7 +901,7 @@ "wallet.select.import.dialog.closeWindow": "ウィンドウを閉じる", "wallet.select.import.dialog.description": "Daedalusステータスディレクトリーに以下のウォレットが見つかりました。

インポートするウォレットを選択してください。

", "wallet.select.import.dialog.enterWalletNameTooltip": "先にウォレット名を入力してください", - "wallet.select.import.dialog.importingWallet": "ウォレットをインポートしています…", + "wallet.select.import.dialog.importingWallet": "ウォレットをインポートしています...", "wallet.select.import.dialog.linkLabel": "もっと知る", "wallet.select.import.dialog.linkUrl": "https://iohk.zendesk.com/hc/ja/articles/900000623463", "wallet.select.import.dialog.maxWalletsReachedTooltip": "Daedalusがサポートするウォレット数は最大{maxWalletsCount}です。このウォレットをインポートする前に、いずれかのウォレットを削除してください。", @@ -856,8 +963,8 @@ "wallet.settings.password": "パスワード", "wallet.settings.passwordLastUpdated": "最終更新 {lastUpdated}", "wallet.settings.passwordNotSet": "パスワードが設定されていません", - "wallet.settings.recoveryPhraseInputHint": "復元フレーズを入力してください", "wallet.settings.recoveryPhraseInputNoResults": "該当するフレーズは見つかりませんでした", + "wallet.settings.recoveryPhraseInputPlaceholder": "{wordNumber}番目の単語を入力", "wallet.settings.recoveryPhraseStep1Button": "続ける", "wallet.settings.recoveryPhraseStep1Paragraph1": "お持ちの復元フレーズが正しいかどうか確認するには、次の画面で復元フレーズを入力してください。", "wallet.settings.recoveryPhraseStep1Paragraph2": "誰かに見られていませんか。ウォレット復元フレーズを入力する際には、画面を誰にも見られないように注意してください。", @@ -911,6 +1018,9 @@ "wallet.settings.walletPublicKeyShowInstruction": "ウォレット公開鍵を表示するには[表示]ボタンをクリックしてください", "wallet.statusMessages.activeRestore": "ウォレットの残高とトランザクション履歴はブロックチェーンと{percentage}%同期されました。", "wallet.summary.no.transactions": "最近のトランザクションはありません", + "wallet.summary.page.currency.isFetchingRate": "フェッチ時の換金レート", + "wallet.summary.page.currency.lastFetched": "換金済み:{fetchedTimeAgo}", + "wallet.summary.page.currency.title": "換金通貨:", "wallet.summary.page.pendingTransactionsLabel": "処理中のトランザクション数", "wallet.summary.page.showMoreTransactionsButtonLabel": "もっと表示", "wallet.summary.page.syncingTransactionsMessage": "ウォレットのトランザクション履歴は現在ブロックチェーンと同期中です。", @@ -922,6 +1032,7 @@ "wallet.transaction.addresses.from": "送信元アドレス", "wallet.transaction.addresses.to": "送信先", "wallet.transaction.conversion.rate": "両替率", + "wallet.transaction.deposit": "デポジット", "wallet.transaction.failed.cancelFailedTxnNote": "トランザクションはCardanoネットワークに送信されましたが、期限切れのため失敗しました。Cardanoネットワークのトランザクションには「time to live(有効期限)」属性があり、ネットワークがトランザクションを処理する前にこれを経過しました。このトランザクションに使用されたファンド(UTXO)をリリースして別のトランザクションで使用可能にするために、このトランザクションを削除してください。", "wallet.transaction.failed.cancelFailedTxnSupportArticle": "このトランザクションをキャンセルする理由", "wallet.transaction.failed.removeTransactionButton": "失敗したトランザクションを削除する", @@ -939,6 +1050,9 @@ "wallet.transaction.filter.resultInfo": "フィルター条件と一致するのは{total}トランザクション中{filtered}件です。", "wallet.transaction.filter.selectTimeRange": "期間を選択してください", "wallet.transaction.filter.thisYear": "今年", + "wallet.transaction.metadataConfirmationLabel": "調整されていないコンテンツを表示する", + "wallet.transaction.metadataDisclaimer": "トランザクションメタデータは承認制ではなく、不適切な内容を含む場合があります。", + "wallet.transaction.metadataLabel": "トランザクションメタデータ", "wallet.transaction.noInputAddressesLabel": "アドレスなし", "wallet.transaction.pending.cancelPendingTxnNote": "このトランザクションは長時間処理中となっています。このトランザクションに使用されている資金を解放するために、トランザクションのキャンセルを試みることができます。", "wallet.transaction.pending.cancelPendingTxnSupportArticle": "このトランザクションをキャンセルする理由", @@ -951,6 +1065,7 @@ "wallet.transaction.state.failed": "トランザクション失敗", "wallet.transaction.state.pending": "トランザクション処理中", "wallet.transaction.transactionAmount": "トランザクション額", + "wallet.transaction.transactionFee": "トランザクション手数料", "wallet.transaction.transactionId": "トランザクションID", "wallet.transaction.type": "{currency}トランザクション", "wallet.transaction.type.card": "カード支払い", diff --git a/source/renderer/app/ipc/enableApplicationMenuNavigationChannel.js b/source/renderer/app/ipc/enableApplicationMenuNavigationChannel.js new file mode 100644 index 0000000000..2373f8ce40 --- /dev/null +++ b/source/renderer/app/ipc/enableApplicationMenuNavigationChannel.js @@ -0,0 +1,13 @@ +// @flow +import { ENABLE_APPLICATION_MENU_NAVIGATION_CHANNEL } from '../../../common/ipc/api'; +import type { + EnableApplicationMenuNavigationMainResponse, + EnableApplicationMenuNavigationRendererRequest, +} from '../../../common/ipc/api'; +import { RendererIpcChannel } from './lib/RendererIpcChannel'; + +export const enableApplicationMenuNavigationChannel: // IpcChannel +RendererIpcChannel< + EnableApplicationMenuNavigationMainResponse, + EnableApplicationMenuNavigationRendererRequest +> = new RendererIpcChannel(ENABLE_APPLICATION_MENU_NAVIGATION_CHANNEL); diff --git a/source/renderer/app/ipc/generateVotingPDFChannel.js b/source/renderer/app/ipc/generateVotingPDFChannel.js new file mode 100644 index 0000000000..342eb5d476 --- /dev/null +++ b/source/renderer/app/ipc/generateVotingPDFChannel.js @@ -0,0 +1,13 @@ +// @flow +import { GENERATE_VOTING_PDF_CHANNEL } from '../../../common/ipc/api'; +import type { + GenerateVotingPDFMainResponse, + GenerateVotingPDFRendererRequest, +} from '../../../common/ipc/api'; +import { RendererIpcChannel } from './lib/RendererIpcChannel'; + +export const generateVotingPDFChannel: // IpcChannel +RendererIpcChannel< + GenerateVotingPDFMainResponse, + GenerateVotingPDFRendererRequest +> = new RendererIpcChannel(GENERATE_VOTING_PDF_CHANNEL); diff --git a/source/renderer/app/routes-config.js b/source/renderer/app/routes-config.js index 08447d8bf1..d31d1b549c 100644 --- a/source/renderer/app/routes-config.js +++ b/source/renderer/app/routes-config.js @@ -30,9 +30,14 @@ export const ROUTES = { SETTINGS: '/wallets/:id/settings', UTXO: '/wallets/:id/utxo', }, + VOTING: { + REGISTRATION: '/voting/registration', + }, SETTINGS: { ROOT: '/settings', + WALLETS: '/settings/wallets', GENERAL: '/settings/general', + STAKE_POOLS: '/settings/stake-pools', TERMS_OF_USE: '/settings/terms-of-service', SUPPORT: '/settings/support', DISPLAY: '/settings/display', diff --git a/source/renderer/app/stores/AppUpdateStore.js b/source/renderer/app/stores/AppUpdateStore.js index 36bbdbf9ff..189547f4eb 100644 --- a/source/renderer/app/stores/AppUpdateStore.js +++ b/source/renderer/app/stores/AppUpdateStore.js @@ -156,7 +156,7 @@ export default class AppUpdateStore extends Store { // =================== PRIVATE ================== _checkNewAppUpdate = async (update: News) => { - const { version } = this.getUpdateInfo(update); + const { version, url } = this.getUpdateInfo(update); const appUpdateCompleted = await this.getAppUpdateCompletedRequest.execute(); /* @@ -198,6 +198,11 @@ export default class AppUpdateStore extends Store { const downloadLocalData = await this._getUpdateDownloadLocalData(); const { info, data } = downloadLocalData; if (info && data) { + // The download is outdated + if (info.fileUrl !== url) { + await this._removeLocalDataInfo(); + return this._requestUpdateDownload(update); + } // The user reopened Daedalus without installing the update if (data.state === DOWNLOAD_STATES.FINISHED && data.progress === 100) { // Does the file still exist? diff --git a/source/renderer/app/stores/HardwareWalletsStore.js b/source/renderer/app/stores/HardwareWalletsStore.js index 9363ef4f74..0284b196cd 100644 --- a/source/renderer/app/stores/HardwareWalletsStore.js +++ b/source/renderer/app/stores/HardwareWalletsStore.js @@ -14,6 +14,7 @@ import { isTrezorEnabled, isLedgerEnabled, } from '../config/hardwareWalletsConfig'; +import { TIME_TO_LIVE } from '../config/txnsConfig'; import { getHardwareWalletTransportChannel, getExtendedPublicKeyChannel, @@ -158,6 +159,8 @@ export default class HardwareWalletsStore extends Store { @observable unfinishedWalletTxSigning: ?string = null; @observable isListeningForDevice: boolean = false; @observable isConnectInitiated: boolean = false; + @observable isExportKeyAborted: boolean = false; + @observable activeDelegationWalletId: ?string = null; cardanoAdaAppPollingInterval: ?IntervalID = null; checkTransactionTimeInterval: ?IntervalID = null; @@ -296,14 +299,12 @@ export default class HardwareWalletsStore extends Store { ); } else { this.setTransactionPendingState(false); - // this._resetTransaction(); } this.stores.wallets.refreshWalletsData(); this.sendMoneyRequest.reset(); return transaction; } catch (e) { this.setTransactionPendingState(false); - // this._resetTransaction(); runInAction('HardwareWalletsStore:: reset Transaction verifying', () => { this.txBody = null; this.activeDevicePath = null; @@ -426,25 +427,33 @@ export default class HardwareWalletsStore extends Store { this.hwDeviceStatus = HwDeviceStatuses.CONNECTING; }); const { hardwareWalletDevices, hardwareWalletsConnectionData } = this; - const activeWallet = this.stores.wallets.active; logger.debug('[HW-DEBUG] HWStore - establishHardwareWalletConnection'); - try { // Check if active wallet exist - this means that hw exist but we need to check if relevant device connected to it let recognizedPairedHardwareWallet; let relatedConnectionData; - if (activeWallet) { + + let activeWalletId; + if (this.activeDelegationWalletId && this.isTransactionInitiated) { + // Active wallet can be different that wallet we want to delegate + activeWalletId = this.activeDelegationWalletId; + } else { + // For regular tx we are using active wallet + activeWalletId = get(this.stores.wallets, ['active', 'id']); + } + + if (activeWalletId) { // Check if device connected to wallet logger.debug('[HW-DEBUG] HWStore - active wallet exists'); recognizedPairedHardwareWallet = find( hardwareWalletDevices, - (recognizedDevice) => recognizedDevice.paired === activeWallet.id + (recognizedDevice) => recognizedDevice.paired === activeWalletId ); relatedConnectionData = find( hardwareWalletsConnectionData, - (connection) => connection.id === activeWallet.id + (connection) => connection.id === activeWalletId ); } @@ -474,6 +483,35 @@ export default class HardwareWalletsStore extends Store { logger.debug( '[HW-DEBUG] HWStore - Establish connection:: Transaction initiated - Recognized device found' ); + runInAction('HardwareWalletsStore:: Set transport device', () => { + this.transportDevice = recognizedPairedHardwareWallet; + }); + + // Special case when Pub key export rejected by the user and then device reconnected + // Force export again and proceed (continue) with last action + const isTrezor = + recognizedPairedHardwareWallet.deviceType === DeviceTypes.TREZOR; + if (this.isExportKeyAborted) { + if (isTrezor) { + await this._getExtendedPublicKey( + recognizedPairedHardwareWallet.path, + activeWalletId + ); + } else { + this.cardanoAdaAppPollingInterval = setInterval( + (devicePath, txWalletId) => + this.getCardanoAdaApp({ + path: devicePath, + walletId: txWalletId, + }), + CARDANO_ADA_APP_POLLING_INTERVAL, + recognizedPairedHardwareWallet.path, + activeWalletId + ); + } + } + // End of special case + return recognizedPairedHardwareWallet; } @@ -482,6 +520,7 @@ export default class HardwareWalletsStore extends Store { 'device', 'deviceType', ]); + const isTrezor = relatedConnectionDataDeviceType === DeviceTypes.TREZOR; let lastDeviceTransport = null; if (relatedConnectionDataDeviceType) { @@ -491,11 +530,36 @@ export default class HardwareWalletsStore extends Store { lastDeviceTransport = await getHardwareWalletTransportChannel.request( { devicePath: null, // Use last plugged device - isTrezor: relatedConnectionDataDeviceType === DeviceTypes.TREZOR, + isTrezor, } ); - } + runInAction('HardwareWalletsStore:: Set transport device', () => { + this.transportDevice = lastDeviceTransport; + }); + // Special case when Pub key export rejected by the user and then device reconnected + // Force export again and proceed (continue) with last action + if (this.isExportKeyAborted) { + if (isTrezor) { + await this._getExtendedPublicKey( + lastDeviceTransport.path, + activeWalletId + ); + } else { + this.cardanoAdaAppPollingInterval = setInterval( + (devicePath, txWalletId) => + this.getCardanoAdaApp({ + path: devicePath, + walletId: txWalletId, + }), + CARDANO_ADA_APP_POLLING_INTERVAL, + lastDeviceTransport.path, + activeWalletId + ); + } + } + // End of special case + } return lastDeviceTransport; } // End of Tx Special cases! @@ -543,7 +607,6 @@ export default class HardwareWalletsStore extends Store { logger.debug( '[HW-DEBUG] HWStore - establishHardwareWalletConnection:: start process with known transport' ); - if (transportDevice) { const { deviceType, firmwareVersion } = transportDevice; // Check if device is supported @@ -680,116 +743,7 @@ export default class HardwareWalletsStore extends Store { `Cardano app must be ${MINIMAL_CARDANO_APP_VERSION} or greater!` ); } - - // Check if I have Software wallet with this deviceId - const recognizedWallet = find( - this.hardwareWalletsConnectionData, - (hardwareWalletData) => - hardwareWalletData.device.deviceId === cardanoAdaApp.deviceId - ); - - let isDisconnected = true; - if (recognizedWallet) { - isDisconnected = false; - logger.debug( - '[HW-DEBUG] HWStore - cardanoAdaApp - SET Software wallet as connected', - { recognizedWalletID: recognizedWallet.id } - ); - this._setHardwareWalletLocalData({ - walletId: recognizedWallet.id, - data: { - disconnected: false, - path, - device: { - ...recognizedWallet.device, - path, - }, - }, - }); - - // Delete initiate (pending) device with this path - const recognizedDevice = find( - this.hardwareWalletDevices, - (device) => device.path === path - ); - if (recognizedDevice) { - logger.debug( - '[HW-DEBUG] HWStore - cardanoAdaApp - UNSET Device with path: ', - { recognizedDevice: recognizedDevice.id } - ); - await this._unsetHardwareWalletDevice({ - deviceId: recognizedDevice.id, - }); - } - - // Add PAIRED device to LC - if (recognizedDevice) { - await this._setHardwareWalletDevice({ - deviceId: cardanoAdaApp.deviceId, - data: { - ...recognizedDevice, - id: cardanoAdaApp.deviceId, - path, - paired: recognizedWallet.id, // device paired with software wallet - disconnected: isDisconnected, // device physically disconnected - isPending: false, - }, - }); - } - } - - if (this.isTransactionInitiated) { - // Check if sender wallet match transaction initialization - if ( - !walletId || - !recognizedWallet || - (recognizedWallet && recognizedWallet.id !== walletId) - ) { - logger.debug( - '[HW-DEBUG] HWStore - Device not belongs to this wallet' - ); - // Stop poller - this.stopCardanoAdaAppFetchPoller(); - // Keep isTransactionInitiated active & Set new device listener by initiating transaction - // Show message to reconnect proper software wallet device pair - logger.debug( - '[HW-DEBUG] unfinishedWalletTxSigning SET: ', - walletId - ); - runInAction( - 'HardwareWalletsStore:: set HW device CONNECTING FAILED', - () => { - this.hwDeviceStatus = HwDeviceStatuses.CONNECTING_FAILED; - this.activeDevicePath = null; - this.unfinishedWalletTxSigning = walletId; - } - ); - } else { - logger.debug( - '[HW-DEBUG] HWStore - Transaction Initiated - Close: ', - walletId - ); - logger.debug('[HW-DEBUG] unfinishedWalletTxSigning UNSET'); - runInAction('HardwareWalletsStore:: Initiate transaction', () => { - this.isTransactionInitiated = false; - this.unfinishedWalletTxSigning = null; - }); - this._signTransactionLedger(walletId, path); - } - } else if (recognizedWallet) { - // While Cardano ADA app recognized & existing wallet mathes device ID, set wallet as active - this.stores.wallets.goToWalletRoute(recognizedWallet.id); - this.actions.dialogs.closeActiveDialog.trigger(); - runInAction( - 'HardwareWalletsStore:: set HW device to initial state', - () => { - this.hwDeviceStatus = HwDeviceStatuses.CONNECTING; - } - ); - } else { - // While Cardano ADA app recognized on Ledger, proceed to exporting public key - await this._getExtendedPublicKey(path); - } + await this._getExtendedPublicKey(path, walletId); } } catch (error) { logger.debug('[HW-DEBUG] HWStore - Cardano app fetching error', { @@ -878,15 +832,20 @@ export default class HardwareWalletsStore extends Store { } }; - @action _getExtendedPublicKey = async (forcedPath: ?string) => { + @action _getExtendedPublicKey = async ( + forcedPath: ?string, + walletId?: string + ) => { logger.debug('[HW-DEBUG] - extendedPublicKey'); this.hwDeviceStatus = HwDeviceStatuses.EXPORTING_PUBLIC_KEY; const { transportDevice } = this; + if (!transportDevice) { throw new Error( 'Can not export extended public key: Device not recognized!' ); } + const { deviceType, path, deviceName, deviceModel } = transportDevice; const isTrezor = deviceType === DeviceTypes.TREZOR; @@ -907,10 +866,10 @@ export default class HardwareWalletsStore extends Store { const recognizedStoredWallet = find( this.hardwareWalletsConnectionData, (hardwareWalletData) => - hardwareWalletData.extendedPublicKey.chainCodeHex === - extendedPublicKey.chainCodeHex && - hardwareWalletData.extendedPublicKey.publicKeyHex === - extendedPublicKey.publicKeyHex + extendedPublicKey.chainCodeHex === + hardwareWalletData.extendedPublicKey.chainCodeHex && + extendedPublicKey.publicKeyHex === + hardwareWalletData.extendedPublicKey.publicKeyHex ); const recognizedWallet = recognizedStoredWallet @@ -938,7 +897,21 @@ export default class HardwareWalletsStore extends Store { }, }); - // @TODO - guard against Ledger - logic needs to be changed and deviceId (serial) applied + // Delete initiated (pending) device with this path since now is paired to wallet + const recognizedDevice = find( + this.hardwareWalletDevices, + (device) => device.path === forcedPath + ); + if (recognizedDevice) { + logger.debug( + '[HW-DEBUG] HWStore - _getExtendedPublicKey - UNSET Device with path: ', + { recognizedDevice: recognizedDevice.id } + ); + await this._unsetHardwareWalletDevice({ + deviceId: recognizedDevice.id, + }); + } + logger.debug('[HW-DEBUG] HWStore - SET device from key export: ', { deviceId, }); @@ -946,25 +919,88 @@ export default class HardwareWalletsStore extends Store { this._setHardwareWalletDevice({ deviceId, data: { + deviceId, deviceType, deviceModel, deviceName, path: devicePath, paired: recognizedWallet.id, // device paired with software wallet disconnected: false, // device physically disconnected + isPending: false, }, }); } + // Prevent redirect / check if device is valid / proceed with tx + if (this.isTransactionInitiated) { + // Check if sender wallet match transaction initialization + if (!walletId || recognizedWallet.id !== walletId) { + logger.debug( + '[HW-DEBUG] HWStore - Device not belongs to this wallet' + ); + // Keep isTransactionInitiated active & Set new device listener by initiating transaction + // Show message to reconnect proper software wallet device pair + logger.debug( + '[HW-DEBUG] unfinishedWalletTxSigning SET: ', + walletId + ); + runInAction( + 'HardwareWalletsStore:: set HW device CONNECTING FAILED', + () => { + this.hwDeviceStatus = HwDeviceStatuses.CONNECTING_FAILED; + this.activeDevicePath = null; + this.unfinishedWalletTxSigning = walletId; + this.isExportKeyAborted = false; + } + ); + } else { + logger.debug( + '[HW-DEBUG] HWStore - Transaction Initiated - Close: ', + walletId + ); + logger.debug('[HW-DEBUG] unfinishedWalletTxSigning UNSET'); + runInAction('HardwareWalletsStore:: Initiate transaction', () => { + this.isTransactionInitiated = false; + this.unfinishedWalletTxSigning = null; + this.isExportKeyAborted = false; + }); + if (isTrezor) { + this._signTransactionTrezor(walletId, deviceId); + } else { + this._signTransactionLedger(walletId, devicePath); + } + } + return; + } + + // --> Else this.stores.wallets.goToWalletRoute(recognizedStoredWallet.id); this.actions.dialogs.closeActiveDialog.trigger(); return; } logger.debug( - '[HW-DEBUG] HWStore - I don not have recognized wallet - create new one: ', + '[HW-DEBUG] HWStore - I don not have recognized wallet - create new one or reject TX: ', { deviceId } ); + + // Software Wallet not recognized and TX initiated. Show error + if (this.isTransactionInitiated) { + logger.debug('[HW-DEBUG] HWStore - Device not belongs to this wallet'); + // Keep isTransactionInitiated active & Set new device listener by initiating transaction + // Show message to reconnect proper software wallet device pair + runInAction( + 'HardwareWalletsStore:: set HW device CONNECTING FAILED', + () => { + this.hwDeviceStatus = HwDeviceStatuses.CONNECTING_FAILED; + this.activeDevicePath = null; + this.unfinishedWalletTxSigning = walletId; + this.isExportKeyAborted = false; + } + ); + return; + } + // Software Wallet not recognized, create new one with default name await this.actions.wallets.createHardwareWallet.trigger({ walletName: deviceName || DEFAULT_HW_NAME, @@ -1035,6 +1071,7 @@ export default class HardwareWalletsStore extends Store { ? HwDeviceStatuses.CONNECTING : HwDeviceStatuses.EXPORTING_PUBLIC_KEY_FAILED; this.isListeningForDevice = true; + this.isExportKeyAborted = true; } ); }, 2000); @@ -1047,6 +1084,7 @@ export default class HardwareWalletsStore extends Store { ? HwDeviceStatuses.CONNECTING : HwDeviceStatuses.EXPORTING_PUBLIC_KEY_FAILED; this.isListeningForDevice = true; + this.isExportKeyAborted = true; } ); } @@ -1066,7 +1104,7 @@ export default class HardwareWalletsStore extends Store { // Trezor - Shelley only @action _signTransactionTrezor = async ( walletId: string, - deviceId?: string + deviceId?: ?string ) => { const { coinSelection } = this.txSignRequest; runInAction('HardwareWalletsStore:: set Transaction verifying', () => { @@ -1107,12 +1145,6 @@ export default class HardwareWalletsStore extends Store { ); const recognizedDevicePath = get(recognizedDevice, 'path', null); - // Check if I have Software wallet with this deviceId - // const recognizedWallet = find( - // this.hardwareWalletsConnectionData, - // (hardwareWalletData) => - // hardwareWalletData.device.deviceId === recognizedDevice.id - // ); logger.debug('[HW-DEBUG] sign Trezor:: recognizedDevicePath and walelt: ', { walletId, deviceId, @@ -1151,13 +1183,14 @@ export default class HardwareWalletsStore extends Store { } const { isMainnet } = this.environment; + const ttl = this._getTtl(); try { const signedTransaction = await signTransactionTrezorChannel.request({ inputs: inputsData, outputs: outputsData, fee: formattedAmountToLovelace(fee.toString()).toString(), - ttl: '150000000', + ttl: ttl.toString(), networkId: isMainnet ? HW_SHELLEY_CONFIG.NETWORK.MAINNET.networkId : HW_SHELLEY_CONFIG.NETWORK.TESTNET.networkId, @@ -1290,9 +1323,8 @@ export default class HardwareWalletsStore extends Store { }); const certificatesData = await Promise.all(_certificatesData); - const fee = formattedAmountToLovelace(flatFee.toString()); - const ttl = 150000000; + const ttl = this._getTtl(); const withdrawals = []; const metadataHashHex = null; const { isMainnet } = this.environment; @@ -1353,11 +1385,12 @@ export default class HardwareWalletsStore extends Store { }; initiateTransaction = async (params: { walletId: ?string }) => { + const { walletId } = params; runInAction('HardwareWalletsStore:: Initiate Transaction', () => { this.isTransactionInitiated = true; this.hwDeviceStatus = HwDeviceStatuses.CONNECTING; + this.activeDelegationWalletId = walletId; }); - const { walletId } = params; const hardwareWalletConnectionData = get( this.hardwareWalletsConnectionData, walletId @@ -1375,7 +1408,6 @@ export default class HardwareWalletsStore extends Store { const { deviceType } = device; let devicePath = hardwareWalletConnectionData.device.path; - let deviceId; if (disconnected) { logger.debug('[HW-DEBUG] HWStore - initiateTransaction - DISCONNECTED'); // Wait for connection to be established and continue to signing process @@ -1402,7 +1434,6 @@ export default class HardwareWalletsStore extends Store { }); } logger.debug('[HW-DEBUG] INITIATE tx - I have transport'); - // throw new Error('Signing device not recognized!'); } else { transportDevice = await this.establishHardwareWalletConnection(); } @@ -1413,7 +1444,6 @@ export default class HardwareWalletsStore extends Store { } devicePath = transportDevice.path; - deviceId = transportDevice.id || transportDevice.deviceId; } catch (e) { logger.debug( '[HW-DEBUG] HWStore - initiateTransaction - DISCONNECTED - ERROR' @@ -1425,7 +1455,6 @@ export default class HardwareWalletsStore extends Store { throw e; } } - runInAction( 'HardwareWalletsStore:: Set active device path for Transaction send', () => { @@ -1436,10 +1465,16 @@ export default class HardwareWalletsStore extends Store { // Add more cases / edge cases if needed if (deviceType === DeviceTypes.TREZOR && walletId) { logger.debug('[HW-DEBUG] Sign Trezor: ', { id }); - this._signTransactionTrezor(walletId, deviceId); - runInAction('HardwareWalletsStore:: Initiate transaction', () => { - this.isTransactionInitiated = false; - }); + const transportDevice = await this.establishHardwareWalletConnection(); + if (transportDevice) { + runInAction( + 'HardwareWalletsStore:: Set transport device fomr tx init', + () => { + this.transportDevice = transportDevice; + } + ); + await this._getExtendedPublicKey(transportDevice.path, walletId); + } } else { logger.debug( '[HW-DEBUG] HWStore - getCardanoAdaApp - from initiateTransaction', @@ -1479,6 +1514,7 @@ export default class HardwareWalletsStore extends Store { this.txBody = null; this.activeDevicePath = null; this.unfinishedWalletTxSigning = null; + this.activeDelegationWalletId = null; }); }; @@ -1496,9 +1532,7 @@ export default class HardwareWalletsStore extends Store { } = params; logger.debug('[HW-DEBUG] HWStore - CHANGE status: ', { - disconnected, - deviceId, - deviceType, + params, }); // Handle Trezor Bridge instance checker @@ -1599,7 +1633,7 @@ export default class HardwareWalletsStore extends Store { deviceType, deviceModel, deviceName, - disconnected, + disconnected: true, // Always reset connecting state to force re-connect path, }, }); @@ -1630,9 +1664,10 @@ export default class HardwareWalletsStore extends Store { deviceModel, deviceName, path, - paired: recognizedPairedHardwareWallet - ? recognizedPairedHardwareWallet.id - : null, // device paired with software wallet + // paired: (recognizedPairedHardwareWallet && deviceType === DeviceTypes.LEDGER) + // ? recognizedPairedHardwareWallet.id + // : null, // Always reset pairing indication on Trezor to force re-connect and set if exist for Ledger + paired: null, // Always reset pairing indication to force re-connect disconnected, // device physically disconnected isPending: !deviceId && !recognizedPairedHardwareWallet, }, @@ -1694,6 +1729,7 @@ export default class HardwareWalletsStore extends Store { this.extendedPublicKey = null; this.transportDevice = {}; this.isListeningForDevice = false; + this.isExportKeyAborted = false; }; @action _refreshHardwareWalletsLocalData = async () => { @@ -1738,6 +1774,13 @@ export default class HardwareWalletsStore extends Store { return type; }; + _getTtl = (): number => { + const { networkTip } = this.stores.networkStatus; + const absoluteSlotNumber = get(networkTip, 'absoluteSlotNumber', 0); + const ttl = absoluteSlotNumber + TIME_TO_LIVE; + return ttl; + }; + _setHardwareWalletLocalData = async ({ walletId, data, diff --git a/source/renderer/app/stores/ProfileStore.js b/source/renderer/app/stores/ProfileStore.js index d1c6200a83..dac935fa04 100644 --- a/source/renderer/app/stores/ProfileStore.js +++ b/source/renderer/app/stores/ProfileStore.js @@ -14,6 +14,7 @@ import { logger } from '../utils/logging'; import { setStateSnapshotLogChannel } from '../ipc/setStateSnapshotLogChannel'; import { getDesktopDirectoryPathChannel } from '../ipc/getDesktopDirectoryPathChannel'; import { getSystemLocaleChannel } from '../ipc/getSystemLocaleChannel'; +import { enableApplicationMenuNavigationChannel } from '../ipc/enableApplicationMenuNavigationChannel'; import { LOCALES } from '../../../common/types/locales.types'; import { compressLogsChannel, @@ -302,10 +303,14 @@ export default class ProfileStore extends Store { _acceptTermsOfUse = async () => { await this.setTermsOfUseAcceptanceRequest.execute(); await this.getTermsOfUseAcceptanceRequest.execute(); + await enableApplicationMenuNavigationChannel.send(); }; - _getTermsOfUseAcceptance = () => { - this.getTermsOfUseAcceptanceRequest.execute(); + _getTermsOfUseAcceptance = async () => { + await this.getTermsOfUseAcceptanceRequest.execute(); + if (this.getTermsOfUseAcceptanceRequest.result) { + await enableApplicationMenuNavigationChannel.send(); + } }; _acceptDataLayerMigration = async () => { diff --git a/source/renderer/app/stores/SidebarStore.js b/source/renderer/app/stores/SidebarStore.js index b2248336d3..9bc00434c2 100644 --- a/source/renderer/app/stores/SidebarStore.js +++ b/source/renderer/app/stores/SidebarStore.js @@ -64,9 +64,12 @@ export default class SidebarStore extends Store { } @action _configureCategories = () => { - const { isFlight, isIncentivizedTestnet, isShelleyTestnet } = global; - - const { isShelleyActivated, isShelleyPending } = this.stores.networkStatus; + const { + isFlight, + isIncentivizedTestnet, + isShelleyTestnet, + environment: { isDev, isMainnet }, + } = global; const { CATEGORIES_BY_NAME: categories, @@ -78,10 +81,11 @@ export default class SidebarStore extends Store { } = { [categories.WALLETS.name]: true, [categories.PAPER_WALLET_CREATE_CERTIFICATE.name]: false, - [categories.STAKING_DELEGATION_COUNTDOWN.name]: isShelleyPending, - [categories.STAKING.name]: isShelleyActivated, + [categories.STAKING_DELEGATION_COUNTDOWN.name]: false, + [categories.STAKING.name]: true, [categories.REDEEM_ITN_REWARDS.name]: true, [categories.SETTINGS.name]: true, + [categories.VOTING.name]: isMainnet || isDev, [categories.NETWORK_INFO.name]: isFlight || isIncentivizedTestnet || isShelleyTestnet, }; diff --git a/source/renderer/app/stores/StakingStore.js b/source/renderer/app/stores/StakingStore.js index 41d26a4d98..b91ae99cc3 100644 --- a/source/renderer/app/stores/StakingStore.js +++ b/source/renderer/app/stores/StakingStore.js @@ -12,8 +12,13 @@ import { STAKE_POOL_TRANSACTION_CHECKER_TIMEOUT, STAKE_POOLS_INTERVAL, STAKE_POOLS_FAST_INTERVAL, + STAKE_POOLS_FETCH_TRACKER_INTERVAL, + STAKE_POOLS_FETCH_TRACKER_CYCLES, REDEEM_ITN_REWARDS_STEPS as steps, INITIAL_DELEGATION_FUNDS, + SMASH_SERVERS_LIST, + SMASH_SERVER_TYPES, + SMASH_SERVER_INVALID_TYPES, CIRCULATING_SUPPLY, } from '../config/stakingConfig'; import type { @@ -21,7 +26,9 @@ import type { RewardForIncentivizedTestnet, JoinStakePoolRequest, GetDelegationFeeRequest, + DelegationCalculateFeeResponse, QuitStakePoolRequest, + PoolMetadataSource, } from '../api/staking/types'; import Wallet from '../domains/Wallet'; import StakePool from '../domains/StakePool'; @@ -40,6 +47,9 @@ export default class StakingStore extends Store { @observable selectedDelegationWalletId = null; @observable stake = INITIAL_DELEGATION_FUNDS; @observable isRanking = false; + @observable smashServerUrl: ?string = null; + @observable smashServerUrlError: ?LocalizableError = null; + @observable smashServerLoading: boolean = false; /* ---------- Redeem ITN Rewards ---------- */ @observable redeemStep: ?RedeemItnRewardsStep = null; @@ -50,20 +60,29 @@ export default class StakingStore extends Store { @observable redeemedRewards: ?BigNumber = null; @observable isSubmittingReedem: boolean = false; @observable isCalculatingReedemFees: boolean = false; - @observable stakingSuccess: ?boolean = null; + @observable redeemSuccess: ?boolean = null; @observable configurationStepError: ?LocalizableError = null; @observable confirmationStepError: ?LocalizableError = null; + /* ---------- Stake Pools Fetching Tracker ---------- */ + @observable isFetchingStakePools: boolean = false; + @observable numberOfStakePoolsFetched: number = 0; + @observable cyclesWithoutIncreasingStakePools: number = 0; + pollingStakePoolsInterval: ?IntervalID = null; refreshPolling: ?IntervalID = null; delegationCheckTimeInterval: ?IntervalID = null; adaValue: BigNumber = new BigNumber(82650.15); percentage: number = 14; + stakePoolsFetchTrackerInterval: ?IntervalID = null; _delegationFeeCalculationWalletId: ?string = null; setup() { - const { staking: stakingActions } = this.actions; + const { + staking: stakingActions, + networkStatus: networkStatusActions, + } = this.actions; this.refreshPolling = setInterval( this.getStakePoolsData, @@ -91,10 +110,13 @@ export default class StakingStore extends Store { stakingActions.fakeStakePoolsLoading.listen(this._setFakePoller); stakingActions.updateDelegatingStake.listen(this._setStake); stakingActions.rankStakePools.listen(this._rankStakePools); + stakingActions.selectSmashServerUrl.listen(this._selectSmashServerUrl); + stakingActions.resetSmashServerError.listen(this._resetSmashServerError); stakingActions.selectDelegationWallet.listen( this._setSelectedDelegationWalletId ); stakingActions.requestCSVFile.listen(this._requestCSVFile); + networkStatusActions.isSyncedAndReady.listen(this._getSmashSettingsRequest); // ========== MOBX REACTIONS =========== // this.registerReactions([this._pollOnSync]); @@ -110,7 +132,8 @@ export default class StakingStore extends Store { @observable stakePoolsRequest: Request> = new Request( this.api.ada.getStakePools ); - @observable calculateDelegationFeeRequest: Request = new Request( + @observable + calculateDelegationFeeRequest: Request = new Request( this.api.ada.calculateDelegationFee ); // @REDEEM TODO: Proper type it when the API endpoint is implemented. @@ -120,9 +143,39 @@ export default class StakingStore extends Store { @observable requestRedeemItnRewardsRequest: Request = new Request( this.api.ada.requestRedeemItnRewards ); + @observable getSmashSettingsRequest: Request = new Request( + this.api.ada.getSmashSettings + ); + @observable + updateSmashSettingsRequest: Request = new Request( + this.api.ada.updateSmashSettings + ); // =================== PUBLIC API ==================== // + @action _getSmashSettingsRequest = async () => { + this.smashServerLoading = true; + let smashServerUrl: string = await this.getSmashSettingsRequest.execute(); + + const localSmashServer = await this.api.localStorage.getSmashServer(); + + // If the server wasn't set, sets it for IOHK + if ( + !smashServerUrl || + smashServerUrl === SMASH_SERVER_INVALID_TYPES.NONE || + (smashServerUrl === SMASH_SERVER_TYPES.DIRECT && + localSmashServer !== SMASH_SERVER_TYPES.DIRECT) + ) { + smashServerUrl = SMASH_SERVERS_LIST.iohk.url; + await this.updateSmashSettingsRequest.execute(smashServerUrl); + } + + runInAction(() => { + this.smashServerUrl = smashServerUrl; + this.smashServerLoading = false; + }); + }; + @action _setSelectedDelegationWalletId = (walletId: string) => { this.selectedDelegationWalletId = walletId; }; @@ -136,6 +189,77 @@ export default class StakingStore extends Store { this.getStakePoolsData(); }; + @action _selectSmashServerUrl = async ({ + smashServerUrl, + }: { + smashServerUrl: string, + }) => { + if (smashServerUrl && smashServerUrl !== this.smashServerUrl) { + try { + this.smashServerUrlError = null; + // Retrieves the API update + this.smashServerLoading = true; + await this.updateSmashSettingsRequest.execute(smashServerUrl); + // Refreshes the Stake Pools list + this.getStakePoolsData(); + // Starts the SPs fetch tracker + this._startStakePoolsFetchTracker(); + // Updates the Smash Server URL + runInAction(() => { + this.smashServerUrl = smashServerUrl; + this.smashServerUrlError = null; + this.smashServerLoading = false; + }); + // Update + await this.api.localStorage.setSmashServer(smashServerUrl); + } catch (error) { + runInAction(() => { + this.smashServerUrlError = error; + this.smashServerLoading = false; + }); + } + } + }; + + @action _startStakePoolsFetchTracker = () => { + this._stopStakePoolsFetchTracker(); + this.isFetchingStakePools = true; + this.stakePoolsFetchTrackerInterval = setInterval( + this._stakePoolsFetchTracker, + STAKE_POOLS_FETCH_TRACKER_INTERVAL + ); + this.getStakePoolsData(true); + }; + + @action _stakePoolsFetchTracker = () => { + const lastNumberOfStakePoolsFetched = this.numberOfStakePoolsFetched; + this.numberOfStakePoolsFetched = this.stakePools.length; + if ( + lastNumberOfStakePoolsFetched === this.numberOfStakePoolsFetched && + this.numberOfStakePoolsFetched > 0 + ) { + this.cyclesWithoutIncreasingStakePools++; + } + if ( + this.cyclesWithoutIncreasingStakePools >= STAKE_POOLS_FETCH_TRACKER_CYCLES + ) { + this._stopStakePoolsFetchTracker(); + } + }; + + @action _stopStakePoolsFetchTracker = () => { + clearInterval(this.stakePoolsFetchTrackerInterval); + this.numberOfStakePoolsFetched = 0; + this.cyclesWithoutIncreasingStakePools = 0; + this.isFetchingStakePools = false; + this.getStakePoolsData(); + }; + + @action _resetSmashServerError = () => { + this.smashServerUrlError = null; + this.smashServerLoading = false; + }; + @action _joinStakePool = async (request: JoinStakePoolRequest) => { const { walletId, stakePoolId, passphrase, isHardwareWallet } = request; @@ -206,11 +330,11 @@ export default class StakingStore extends Store { walletId: string, }) => { const { transactionId, walletId } = request; - const recenttransactionsResponse = this.stores.transactions._getTransactionsRecentRequest( + const recentTransactionsResponse = this.stores.transactions._getTransactionsRecentRequest( walletId ).result; - const recentTransactions = recenttransactionsResponse - ? recenttransactionsResponse.transactions + const recentTransactions = recentTransactionsResponse + ? recentTransactionsResponse.transactions : []; // Return stake pool transaction when state is not "PENDING" @@ -278,7 +402,7 @@ export default class StakingStore extends Store { calculateDelegationFee = async ( delegationFeeRequest: GetDelegationFeeRequest - ): ?BigNumber => { + ): Promise => { const { walletId } = delegationFeeRequest; const wallet = this.stores.wallets.getWalletById(walletId); this._delegationFeeCalculationWalletId = walletId; @@ -294,7 +418,7 @@ export default class StakingStore extends Store { } try { - const delegationFee: BigNumber = await this.calculateDelegationFeeRequest.execute( + const delegationFee: DelegationCalculateFeeResponse = await this.calculateDelegationFeeRequest.execute( { ...delegationFeeRequest } ).promise; @@ -377,7 +501,7 @@ export default class StakingStore extends Store { return isShelleyPending; } - @action getStakePoolsData = async () => { + @action getStakePoolsData = async (isSmash?: boolean) => { const { isConnected, isSynced, @@ -395,16 +519,16 @@ export default class StakingStore extends Store { 10 ); await this.stakePoolsRequest.execute(stakeInLovelace).promise; - this._resetPolling(false); + this._resetPolling(isSmash ? 'smash' : 'regular'); } catch (error) { - this._resetPolling(true); + this._resetPolling('failed'); } this._resetIsRanking(); }; - @action _resetPolling = (fetchFailed: boolean, kill?: boolean) => { - if (kill) { - this.fetchingStakePoolsFailed = fetchFailed; + @action _resetPolling = (type?: 'regular' | 'failed' | 'kill' | 'smash') => { + if (type === 'kill') { + this.fetchingStakePoolsFailed = true; if (this.pollingStakePoolsInterval) { clearInterval(this.pollingStakePoolsInterval); this.pollingStakePoolsInterval = null; @@ -413,9 +537,7 @@ export default class StakingStore extends Store { clearInterval(this.refreshPolling); this.refreshPolling = null; } - return; - } - if (fetchFailed) { + } else if (type === 'failed') { this.fetchingStakePoolsFailed = true; if (this.pollingStakePoolsInterval) { clearInterval(this.pollingStakePoolsInterval); @@ -433,12 +555,15 @@ export default class StakingStore extends Store { clearInterval(this.refreshPolling); this.refreshPolling = null; } - if (!this.pollingStakePoolsInterval) { - this.pollingStakePoolsInterval = setInterval( - this.getStakePoolsData, - STAKE_POOLS_INTERVAL - ); - } + clearInterval(this.pollingStakePoolsInterval); + const isSmash = type === 'smash'; + const interval = isSmash + ? STAKE_POOLS_FETCH_TRACKER_INTERVAL + : STAKE_POOLS_INTERVAL; + this.pollingStakePoolsInterval = setInterval( + () => this.getStakePoolsData(isSmash), + interval + ); } }; @@ -471,7 +596,7 @@ export default class StakingStore extends Store { // Regular fetching way with faked response that throws error. if ((_pollingBlocked || !isConnected) && !this.refreshPolling) { - this._resetPolling(true); + this._resetPolling('failed'); return; } @@ -479,7 +604,7 @@ export default class StakingStore extends Store { throw new Error('Faked "Stake pools" fetch error'); } catch (error) { if (!this.refreshPolling) { - this._resetPolling(true); + this._resetPolling('failed'); } } } @@ -589,6 +714,9 @@ export default class StakingStore extends Store { this.redeemStep = steps.CONFIRMATION; this.confirmationStepError = null; this.configurationStepError = null; + } else { + this.redeemSuccess = false; + this.redeemStep = steps.RESULT; } }; @@ -616,7 +744,7 @@ export default class StakingStore extends Store { ); runInAction(() => { this.redeemedRewards = redeemedRewards; - this.stakingSuccess = true; + this.redeemSuccess = true; this.redeemStep = steps.RESULT; this.confirmationStepError = null; this.isSubmittingReedem = false; @@ -626,7 +754,7 @@ export default class StakingStore extends Store { this.confirmationStepError = error; this.isSubmittingReedem = false; if (error.id !== 'api.errors.IncorrectPasswordError') { - this.stakingSuccess = false; + this.redeemSuccess = false; this.redeemStep = steps.RESULT; } }); @@ -644,7 +772,7 @@ export default class StakingStore extends Store { @action _resetRedeemItnRewards = () => { this.isSubmittingReedem = false; this.isCalculatingReedemFees = false; - this.stakingSuccess = null; + this.redeemSuccess = null; this.redeemWallet = null; this.transactionFees = null; this.redeemedRewards = null; @@ -666,7 +794,7 @@ export default class StakingStore extends Store { this.getStakePoolsData(); } else { this._resetIsRanking(); - this._resetPolling(true, true); + this._resetPolling('kill'); } }; @@ -693,7 +821,7 @@ export default class StakingStore extends Store { syncState, } = inputWallet; const { withdrawals } = this.stores.transactions; - const reward = rewards.add(withdrawals[walletId]); + const reward = rewards.plus(withdrawals[walletId]); const syncingProgress = get(syncState, 'progress.quantity', ''); return { wallet, reward, isRestoring, syncingProgress }; }; diff --git a/source/renderer/app/stores/TransactionsStore.js b/source/renderer/app/stores/TransactionsStore.js index acacbe988d..34531860cf 100644 --- a/source/renderer/app/stores/TransactionsStore.js +++ b/source/renderer/app/stores/TransactionsStore.js @@ -395,6 +395,10 @@ export default class TransactionsStore extends Store { return new Request(this.api.ada.getWithdrawals); }; + _getTransactionRequest = (): Request => { + return new Request(this.api.ada.getTransaction); + }; + // ======================= REACTIONS ========================== // /** diff --git a/source/renderer/app/stores/VotingStore.js b/source/renderer/app/stores/VotingStore.js new file mode 100644 index 0000000000..b9836ddc34 --- /dev/null +++ b/source/renderer/app/stores/VotingStore.js @@ -0,0 +1,324 @@ +// @flow +import { action, computed, observable } from 'mobx'; +import Store from './lib/Store'; +import Request from './lib/LocalizedRequest'; +import { ROUTES } from '../routes-config'; +import { + TransactionStates, + WalletTransaction, +} from '../domains/WalletTransaction'; +import { formattedArrayBufferToHexString } from '../utils/formatters'; +import walletUtils from '../utils/walletUtils'; +import { + VOTING_REGISTRATION_TRANSACTION_POLLING_INTERVAL, + VOTING_REGISTRATION_MIN_TRANSACTION_CONFIRMATIONS, + VOTING_FUND_NUMBER, +} from '../config/votingConfig'; +import { votingPDFGenerator } from '../utils/votingPDFGenerator'; +import { i18nContext } from '../utils/i18nContext'; + +export type VotingRegistrationKeyType = { bytes: Function, public: Function }; + +export default class VotingStore extends Store { + @observable registrationStep: number = 1; + @observable selectedWalletId: ?string = null; + @observable transactionId: ?string = null; + @observable transactionConfirmations: number = 0; + @observable isTransactionPending: boolean = false; + @observable isTransactionConfirmed: boolean = false; + @observable votingRegistrationKey: ?VotingRegistrationKeyType = null; + @observable qrCode: ?string = null; + @observable isConfirmationDialogOpen: boolean = false; + + transactionPollingInterval: ?IntervalID = null; + + setup() { + const { voting: votingActions } = this.actions; + votingActions.selectWallet.listen(this._setSelectedWalletId); + votingActions.sendTransaction.listen(this._sendTransaction); + votingActions.generateQrCode.listen(this._generateQrCode); + votingActions.saveAsPDF.listen(this._saveAsPDF); + votingActions.nextRegistrationStep.listen(this._nextRegistrationStep); + votingActions.previousRegistrationStep.listen( + this._previousRegistrationStep + ); + votingActions.resetRegistration.listen(this._resetRegistration); + votingActions.showConfirmationDialog.listen(this._showConfirmationDialog); + votingActions.closeConfirmationDialog.listen(this._closeConfirmationDialog); + } + + // REQUESTS + + @observable getWalletPublicKeyRequest: Request = new Request( + this.api.ada.getWalletPublicKey + ); + + @observable + createVotingRegistrationTransactionRequest: Request = new Request( + this.api.ada.createVotingRegistrationTransaction + ); + + @observable + signMetadataRequest: Request = new Request( + this.api.ada.createWalletSignature + ); + + // ACTIONS + + @action _showConfirmationDialog = () => { + this.isConfirmationDialogOpen = true; + }; + + @action _closeConfirmationDialog = () => { + this.isConfirmationDialogOpen = false; + }; + + @action _setSelectedWalletId = (walletId: string) => { + this.selectedWalletId = walletId; + }; + + @action _nextRegistrationStep = () => { + this.registrationStep++; + }; + + @action _previousRegistrationStep = () => { + this.registrationStep--; + }; + + @action _resetRegistration = () => { + this.isConfirmationDialogOpen = false; + this.registrationStep = 1; + this.selectedWalletId = null; + this.transactionId = null; + this.transactionConfirmations = 0; + this.isTransactionPending = false; + this.isTransactionConfirmed = false; + this.votingRegistrationKey = null; + this.qrCode = null; + this.getWalletPublicKeyRequest.reset(); + this.createVotingRegistrationTransactionRequest.reset(); + this.signMetadataRequest.reset(); + if (this.transactionPollingInterval) + clearInterval(this.transactionPollingInterval); + }; + + @action _startTransactionPolling = () => { + if (this.transactionPollingInterval) + clearInterval(this.transactionPollingInterval); + this.transactionPollingInterval = setInterval(() => { + this._checkVotingRegistrationTransaction(); + }, VOTING_REGISTRATION_TRANSACTION_POLLING_INTERVAL); + }; + + @action _setVotingRegistrationKey = (value: VotingRegistrationKeyType) => { + this.votingRegistrationKey = value; + }; + + @action _setTransactionId = (transactionId: string) => { + this.transactionId = transactionId; + }; + + @action _setTransactionConfirmations = (confirmations: number) => { + this.transactionConfirmations = confirmations; + }; + + @action _setIsTransactionPending = (value: boolean) => { + this.isTransactionPending = value; + }; + + @action _setIsTransactionConfirmed = (value: boolean) => { + this.isTransactionConfirmed = value; + }; + + @action _setQrCode = (value: ?string) => { + this.qrCode = value; + }; + + _sendTransaction = async ({ + amount, + passphrase, + }: { + amount: number, + passphrase: string, + }) => { + const walletId = this.selectedWalletId; + if (!walletId) + throw new Error( + 'Selected wallet required before send voting registration.' + ); + const [address] = await this.stores.addresses.getAddressesByWalletId( + walletId + ); + + // Reset voting registration transaction state + this._setIsTransactionPending(true); + this._setIsTransactionConfirmed(false); + + // Reset voting registration requests + this.getWalletPublicKeyRequest.reset(); + this.createVotingRegistrationTransactionRequest.reset(); + this.signMetadataRequest.reset(); + + try { + const addressHex = await this._getHexFromBech32(address.id); + + await this._generateVotingRegistrationKey(); + if (!this.votingRegistrationKey) + throw new Error('Failed to generate voting registration key.'); + const votingKey = formattedArrayBufferToHexString( + this.votingRegistrationKey.public().bytes() + ); + + let stakeKey = await this.getWalletPublicKeyRequest.execute({ + walletId, + role: 'mutable_account', + index: '0', + }); + stakeKey = await this._getHexFromBech32(stakeKey); + + const signature = await this.signMetadataRequest.execute({ + addressHex, + walletId, + passphrase, + votingKey, + stakeKey, + role: 'mutable_account', + index: '0', + }); + + const transaction = await this.createVotingRegistrationTransactionRequest.execute( + { + address: address.id, + addressHex, + amount, + passphrase, + walletId, + votingKey, + stakeKey, + signature: signature.toString('hex'), + } + ); + + this._setTransactionId(transaction.id); + this._startTransactionPolling(); + this._nextRegistrationStep(); + } catch (error) { + if (error.code === 'wrong_encryption_passphrase') { + // In case of a invalid spending password we stay on the same screen + this._setIsTransactionPending(false); + } else { + // For any other error code we proceed to the next screen + this._nextRegistrationStep(); + } + throw error; + } + }; + + _generateQrCode = async (pinCode: number) => { + const { symmetric_encrypt: symmetricEncrypt } = await walletUtils; + const password = new Uint8Array(4); + pinCode + .toString() + .split('') + .forEach((value: string, index: number) => { + password[index] = parseInt(value, 10); + }); + if (!this.votingRegistrationKey) + throw new Error( + 'Failed to generate QR code due to missing voting registration key.' + ); + const encrypt = symmetricEncrypt( + password, + this.votingRegistrationKey.bytes() + ); + this._setQrCode(formattedArrayBufferToHexString(encrypt)); + this._nextRegistrationStep(); + }; + + _saveAsPDF = async () => { + const { qrCode, selectedWalletId } = this; + if (!qrCode || !selectedWalletId) return; + const selectedWallet = this.stores.wallets.getWalletById(selectedWalletId); + if (!selectedWallet) return; + const { name: walletName } = selectedWallet; + const { desktopDirectoryPath } = this.stores.profile; + const { + currentLocale, + currentDateFormat, + currentTimeFormat, + } = this.stores.profile; + const fundNumber = VOTING_FUND_NUMBER; + const { network, isMainnet } = this.environment; + const intl = i18nContext(currentLocale); + + try { + await votingPDFGenerator({ + fundNumber, + qrCode, + walletName, + currentLocale, + currentDateFormat, + currentTimeFormat, + desktopDirectoryPath, + network, + isMainnet, + intl, + }); + this.actions.voting.saveAsPDFSuccess.trigger(); + } catch (error) { + throw new Error(error); + } + }; + + _checkVotingRegistrationTransaction = async () => { + const { + confirmations, + state, + }: WalletTransaction = await this.stores.transactions + ._getTransactionRequest() + .execute({ + walletId: this.selectedWalletId, + transactionId: this.transactionId, + }); + + // Update voting registration confirmations count + if (this.transactionConfirmations !== confirmations) { + this._setTransactionConfirmations(confirmations); + } + + // Update voting registration pending state + if (this.isTransactionPending && state === TransactionStates.OK) { + this._setIsTransactionPending(false); + } + + // Update voting registration confirmed state + if ( + !this.isTransactionConfirmed && + confirmations >= VOTING_REGISTRATION_MIN_TRANSACTION_CONFIRMATIONS + ) { + this._setIsTransactionConfirmed(true); + if (this.transactionPollingInterval) + clearInterval(this.transactionPollingInterval); + } + }; + + _generateVotingRegistrationKey = async () => { + const { Ed25519ExtendedPrivate: extendedPrivateKey } = await walletUtils; + this._setVotingRegistrationKey(extendedPrivateKey.generate()); + }; + + _getHexFromBech32 = async (key: string): Promise => { + const { bech32_decode_to_bytes: decodeBech32ToBytes } = await walletUtils; + return formattedArrayBufferToHexString(decodeBech32ToBytes(key)); + }; + + // GETTERS + + @computed get currentRoute(): string { + return this.stores.router.location.pathname; + } + + @computed get isVotingPage(): boolean { + return this.currentRoute.indexOf(ROUTES.VOTING.REGISTRATION) > -1; + } +} diff --git a/source/renderer/app/stores/WalletMigrationStore.js b/source/renderer/app/stores/WalletMigrationStore.js index f8e49a1eb0..5f06be05ee 100644 --- a/source/renderer/app/stores/WalletMigrationStore.js +++ b/source/renderer/app/stores/WalletMigrationStore.js @@ -28,6 +28,7 @@ import { ImportFromOptions, } from '../types/walletExportTypes'; import { IMPORT_WALLET_STEPS } from '../config/walletRestoreConfig'; +import { IS_AUTOMATIC_WALLET_MIGRATION_ENABLED } from '../config/walletsConfig'; import type { ImportWalletStep } from '../types/walletRestoreTypes'; export type WalletMigrationStatus = @@ -245,7 +246,7 @@ export default class WalletMigrationStore extends Store { : WalletImportStatuses.UNSTARTED; return { ...wallet, hasName, import: { status, error: null } }; }), - ['hasName', 'id', 'name', 'is_passphrase_empty'], + ['hasName', 'id', 'name', 'isEmptyPassphrase'], ['desc', 'asc', 'asc', 'asc'] ); @@ -286,7 +287,7 @@ export default class WalletMigrationStore extends Store { { id: wallet.id, name: wallet.name, - hasPassword: wallet.is_passphrase_empty, + hasPassword: !wallet.isEmptyPassphrase, } ); return this._restoreWallet(wallet); @@ -333,7 +334,7 @@ export default class WalletMigrationStore extends Store { }); } catch (error) { runInAction('update restorationErrors', () => { - const { name, is_passphrase_empty: hasPassword } = exportedWallet; + const { name, isEmptyPassphrase } = exportedWallet; this._updateWalletImportStatus( index, WalletImportStatuses.ERRORED, @@ -341,7 +342,7 @@ export default class WalletMigrationStore extends Store { ); this.restorationErrors.push({ error, - wallet: { id, name, hasPassword }, + wallet: { id, name, hasPassword: !isEmptyPassphrase }, }); }); } @@ -381,8 +382,7 @@ export default class WalletMigrationStore extends Store { }; @action _startMigration = async () => { - // eslint-disable-next-line - if (true) return; // This feature is currently unavailable as export tool is disabled + if (!IS_AUTOMATIC_WALLET_MIGRATION_ENABLED) return; const { isMainnet, isTestnet, isTest } = this.environment; if (isMainnet || isTestnet || (isTest && this.isTestMigrationEnabled)) { @@ -514,7 +514,7 @@ export default class WalletMigrationStore extends Store { return this.exportedWallets.map((wallet) => ({ id: wallet.id, name: wallet.name, - hasPassword: wallet.is_passphrase_empty, + hasPassword: !wallet.isEmptyPassphrase, import: wallet.import, })); } diff --git a/source/renderer/app/stores/WalletsStore.js b/source/renderer/app/stores/WalletsStore.js index 125f4ddc8f..cea8237c9a 100644 --- a/source/renderer/app/stores/WalletsStore.js +++ b/source/renderer/app/stores/WalletsStore.js @@ -29,6 +29,7 @@ import { WALLET_HARDWARE_KINDS, RESTORE_WALLET_STEPS, } from '../config/walletRestoreConfig'; +import { CURRENCY_REQUEST_RATE_INTERVAL } from '../config/currencyConfig'; import { WALLET_PUBLIC_KEY_SHARING_ENABLED } from '../config/walletsConfig'; import type { WalletKind, @@ -36,6 +37,7 @@ import type { WalletYoroiKind, WalletHardwareKind, } from '../types/walletRestoreTypes'; +import type { Currency } from '../types/currencyTypes.js'; import type { CsvFileContent } from '../../../common/types/csv-request.types'; import type { WalletExportTypeChoices } from '../types/walletExportTypes'; import type { WalletImportFromFileParams } from '../actions/wallets-actions'; @@ -140,6 +142,17 @@ export default class WalletsStore extends Store { @observable activeValue: ?BigNumber = null; @observable activePublicKey: ?string = null; + /* ------------ Currencies ----------- */ + @observable currencyIsFetchingList: boolean = false; + @observable currencyIsFetchingRate: boolean = false; + @observable currencyIsAvailable: boolean = false; + @observable currencyIsActive: boolean = false; + + @observable currencyList: Array = []; + @observable currencySelected: ?Currency = null; + @observable currencyRate: ?number = null; + @observable currencyLastFetched: ?Date = null; + /* ---------- Create Wallet ---------- */ @observable createWalletStep = null; @observable createWalletShowAbortConfirmation = false; @@ -204,6 +217,7 @@ export default class WalletsStore extends Store { spendingPassword: '', }; _pollingBlocked = false; + _getCurrencyRateInterval: ?IntervalID = null; setup() { setInterval(this._pollRefresh, this.WALLET_REFRESH_INTERVAL); @@ -286,6 +300,10 @@ export default class WalletsStore extends Store { walletsActions.transferFundsCalculateFee.listen( this._transferFundsCalculateFee ); + walletsActions.setCurrencySelected.listen(this._setCurrencySelected); + walletsActions.toggleCurrencyIsActive.listen(this._toggleCurrencyIsActive); + + this.setupCurrency(); } @action _getWalletPublicKey = async () => { @@ -312,6 +330,91 @@ export default class WalletsStore extends Store { } }; + @action setupCurrency = async () => { + // CURRENCY_REQUEST_RATE_INTERVAL + + // Check if the user has enabled currencies + // Otherwise applies the default config + const currencyIsActive = await this.api.localStorage.getCurrencyIsActive(); + + // Check if the user has already selected a currency + // Otherwise applies the default currency + const currencySelected = await this.api.localStorage.getCurrencySelected(); + + runInAction(() => { + this.currencyIsActive = currencyIsActive; + this.currencySelected = currencySelected; + }); + + clearInterval(this._getCurrencyRateInterval); + this._getCurrencyRateInterval = setInterval( + this.getCurrencyRate, + CURRENCY_REQUEST_RATE_INTERVAL + ); + + // Fetch the currency list and rate + this.getCurrencyList(); + this.getCurrencyRate(); + }; + + @action getCurrencyList = async () => { + this.currencyIsFetchingList = true; + const currencyList = await this.api.ada.getCurrencyList(); + runInAction(() => { + this.currencyList = currencyList; + this.currencyIsFetchingList = false; + }); + }; + + @action getCurrencyRate = async () => { + const { currencySelected } = this; + if (currencySelected && currencySelected.symbol) { + try { + this.currencyIsFetchingRate = true; + const currencyRate = await this.api.ada.getCurrencyRate( + currencySelected + ); + runInAction(() => { + this.currencyIsFetchingRate = false; + this.currencyLastFetched = new Date(); + if (currencyRate) { + this.currencyRate = currencyRate; + this.currencyIsAvailable = true; + } else { + throw new Error('Error fetching the Currency rate'); + } + }); + } catch (error) { + runInAction(() => { + this.currencyRate = null; + this.currencyIsAvailable = false; + }); + clearInterval(this._getCurrencyRateInterval); + } + } + }; + + @action _setCurrencySelected = async ({ + currencySymbol, + }: { + currencySymbol: string, + }) => { + const { currencyList } = this; + const currencySelected = currencyList.find( + ({ symbol }) => currencySymbol === symbol + ); + if (currencySelected) { + this.currencySelected = currencySelected; + this.getCurrencyRate(); + await this.api.localStorage.setCurrencySelected(currencySelected); + } + }; + + @action _toggleCurrencyIsActive = () => { + this.currencyIsActive = !this.currencyIsActive; + this.api.localStorage.setCurrencyIsActive(this.currencyIsActive); + }; + _create = async (params: { name: string, spendingPassword: string }) => { Object.assign(this._newWalletDetails, params); try { diff --git a/source/renderer/app/stores/index.js b/source/renderer/app/stores/index.js index 671a1a066b..1934ab01a5 100644 --- a/source/renderer/app/stores/index.js +++ b/source/renderer/app/stores/index.js @@ -3,8 +3,8 @@ import { observable, action } from 'mobx'; import type Store from './lib/Store'; import AddressesStore from './AddressesStore'; import AppStore from './AppStore'; -import HardwareWalletsStore from './HardwareWalletsStore'; import AppUpdateStore from './AppUpdateStore'; +import HardwareWalletsStore from './HardwareWalletsStore'; import NetworkStatusStore from './NetworkStatusStore'; import NewsFeedStore from './NewsFeedStore'; import ProfileStore from './ProfileStore'; @@ -13,6 +13,7 @@ import StakingStore from './StakingStore'; import TransactionsStore from './TransactionsStore'; import UiDialogsStore from './UiDialogsStore'; import UiNotificationsStore from './UiNotificationsStore'; +import VotingStore from './VotingStore'; import WalletsStore from './WalletsStore'; import WalletsLocalStore from './WalletsLocalStore'; import WalletBackupStore from './WalletBackupStore'; @@ -23,8 +24,8 @@ import WindowStore from './WindowStore'; export const storeClasses = { addresses: AddressesStore, app: AppStore, - hardwareWallets: HardwareWalletsStore, appUpdate: AppUpdateStore, + hardwareWallets: HardwareWalletsStore, networkStatus: NetworkStatusStore, newsFeed: NewsFeedStore, profile: ProfileStore, @@ -33,6 +34,7 @@ export const storeClasses = { transactions: TransactionsStore, uiDialogs: UiDialogsStore, uiNotifications: UiNotificationsStore, + voting: VotingStore, wallets: WalletsStore, walletsLocal: WalletsLocalStore, walletBackup: WalletBackupStore, @@ -44,9 +46,8 @@ export const storeClasses = { export type StoresMap = { addresses: AddressesStore, app: AppStore, - - hardwareWallets: HardwareWalletsStore, appUpdate: AppUpdateStore, + hardwareWallets: HardwareWalletsStore, networkStatus: NetworkStatusStore, newsFeed: NewsFeedStore, profile: ProfileStore, @@ -56,6 +57,7 @@ export type StoresMap = { transactions: TransactionsStore, uiDialogs: UiDialogsStore, uiNotifications: UiNotificationsStore, + voting: VotingStore, wallets: WalletsStore, walletsLocal: WalletsLocalStore, walletBackup: WalletBackupStore, @@ -85,19 +87,20 @@ export default action((api, actions, router): StoresMap => { // Create fresh instances of all stores stores = observable({ - uiNotifications: createStoreInstanceOf(UiNotificationsStore), addresses: createStoreInstanceOf(AddressesStore), app: createStoreInstanceOf(AppStore), + appUpdate: createStoreInstanceOf(AppUpdateStore), hardwareWallets: createStoreInstanceOf(HardwareWalletsStore), networkStatus: createStoreInstanceOf(NetworkStatusStore), newsFeed: createStoreInstanceOf(NewsFeedStore), - appUpdate: createStoreInstanceOf(AppUpdateStore), profile: createStoreInstanceOf(ProfileStore), router, sidebar: createStoreInstanceOf(SidebarStore), staking: createStoreInstanceOf(StakingStore), transactions: createStoreInstanceOf(TransactionsStore), uiDialogs: createStoreInstanceOf(UiDialogsStore), + uiNotifications: createStoreInstanceOf(UiNotificationsStore), + voting: createStoreInstanceOf(VotingStore), wallets: createStoreInstanceOf(WalletsStore), walletsLocal: createStoreInstanceOf(WalletsLocalStore), walletBackup: createStoreInstanceOf(WalletBackupStore), diff --git a/source/renderer/app/themes/daedalus/cardano.js b/source/renderer/app/themes/daedalus/cardano.js index 806854442f..04115f37a9 100644 --- a/source/renderer/app/themes/daedalus/cardano.js +++ b/source/renderer/app/themes/daedalus/cardano.js @@ -506,6 +506,11 @@ export const CARDANO_THEME_OUTPUT = { '--theme-progress-bar-large-progress-stripe1': '#e0e5ea', '--theme-progress-bar-large-progress-stripe2': '#fafbfc', '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-progress-bar-large-progress-dark-stripe1': '#e0e5ea', + '--theme-progress-bar-large-progress-dark-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-light-stripe-1': '#259c59', + '--theme-progress-bar-large-progress-light-stripe-2-background-color': + '#2cbb69', }, receiveQRCode: { '--theme-receive-qr-code-background-color': 'transparent', @@ -923,6 +928,7 @@ export const CARDANO_THEME_OUTPUT = { '--theme-transactions-list-border-color': '#d2d3d3', '--theme-transactions-list-group-date-color': '#5e6066', '--theme-transactions-list-item-details-color': '#5e6066', + '--theme-transactions-list-item-highlight-color': '#ea4c5b', '--theme-transactions-state-ok-background-color': '#007600', '--theme-transactions-state-pending-background-color': 'rgba(94, 96, 102, 0.5)', @@ -977,6 +983,26 @@ export const CARDANO_THEME_OUTPUT = { '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': '#ffffff', }, + voting: { + '--theme-voting-font-color-accent': '#5e6066', + '--theme-voting-font-color-light': 'rgba(94, 96, 102, 0.7)', + '--theme-voting-font-color-regular': '#5e6066', + '--theme-voting-info-background-color': 'rgba(94, 96, 102, 0.1)', + '--theme-voting-info-font-color': '#5e6066', + '--theme-voting-registration-steps-activation-steps-indicator-color': + '#5e6066', + '--theme-voting-registration-steps-choose-wallet-error-message-color': + '#ea4c5b', + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': + 'rgba(234, 76, 91, 0.7)', + '--theme-voting-registration-steps-deposit-fees-amount-color': '#ea4c5b', + '--theme-voting-registration-steps-deposit-fees-label-color': '#5e6066', + '--theme-voting-registration-steps-description-color': + 'rgba(94, 96, 102, 0.8)', + '--theme-voting-registration-steps-description-highlighted-color': + '#5e6066', + '--theme-voting-separator-color': 'rgba(94, 96, 102, 0.15)', + }, recoveryPhrase: { '--theme-recovery-phrase-normal-background-color': 'transparent', '--theme-recovery-phrase-normal-border-color': 'rgba(32, 34, 37, .07)', diff --git a/source/renderer/app/themes/daedalus/dark-blue.js b/source/renderer/app/themes/daedalus/dark-blue.js index 9601994f02..775d1115af 100644 --- a/source/renderer/app/themes/daedalus/dark-blue.js +++ b/source/renderer/app/themes/daedalus/dark-blue.js @@ -509,6 +509,11 @@ export const DARK_BLUE_THEME_OUTPUT = { '--theme-progress-bar-large-progress-stripe1': '#e0e5eb', '--theme-progress-bar-large-progress-stripe2': '#fafbfc', '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-progress-bar-large-progress-dark-stripe1': '#e0e5eb', + '--theme-progress-bar-large-progress-dark-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-light-stripe-1': '#3c4852', + '--theme-progress-bar-large-progress-light-stripe-2-background-color': + '#536370', }, receiveQRCode: { '--theme-receive-qr-code-background-color': '#fff', @@ -929,6 +934,7 @@ export const DARK_BLUE_THEME_OUTPUT = { '--theme-transactions-list-border-color': '#263345', '--theme-transactions-list-group-date-color': '#7a8691', '--theme-transactions-list-item-details-color': '#e9f4fe', + '--theme-transactions-list-item-highlight-color': '#ea4c5b', '--theme-transactions-state-ok-background-color': '#274c2d', '--theme-transactions-state-pending-background-color': 'rgba(233, 244, 254, 0.3)', @@ -983,6 +989,26 @@ export const DARK_BLUE_THEME_OUTPUT = { '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': '#fafbfc', }, + voting: { + '--theme-voting-font-color-accent': '#cecfd1', + '--theme-voting-font-color-light': 'rgba(233, 244, 254, 0.7)', + '--theme-voting-font-color-regular': '#cecfd1', + '--theme-voting-info-background-color': 'rgba(233, 244, 254, 0.1)', + '--theme-voting-info-font-color': '#e9f4fe', + '--theme-voting-registration-steps-activation-steps-indicator-color': + '#e9f4fe', + '--theme-voting-registration-steps-choose-wallet-error-message-color': + '#ea4c5b', + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': + 'rgba(234, 76, 91, 0.7)', + '--theme-voting-registration-steps-deposit-fees-amount-color': '#ea4c5b', + '--theme-voting-registration-steps-deposit-fees-label-color': '#e9f4fe', + '--theme-voting-registration-steps-description-color': + 'rgba(233, 244, 254, 0.8)', + '--theme-voting-registration-steps-description-highlighted-color': + '#e9f4fe', + '--theme-voting-separator-color': 'rgba(233, 244, 254, 0.15)', + }, recoveryPhrase: { '--theme-recovery-phrase-normal-background-color': 'rgba(83, 99, 112, .3)', '--theme-recovery-phrase-normal-border-color': 'transparent', diff --git a/source/renderer/app/themes/daedalus/dark-cardano.js b/source/renderer/app/themes/daedalus/dark-cardano.js index 82d5af0342..8f69668f5f 100644 --- a/source/renderer/app/themes/daedalus/dark-cardano.js +++ b/source/renderer/app/themes/daedalus/dark-cardano.js @@ -488,6 +488,13 @@ export const DARK_CARDANO_THEME_OUTPUT = { '--theme-progress-bar-large-progress-stripe1': '#e0e5eb', '--theme-progress-bar-large-progress-stripe2': '#fafbfc', '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-progress-bar-large-progress-dark-stripe1': '#e0e5eb', + '--theme-progress-bar-large-progress-dark-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-light-stripe1': '#e0e5eb', + '--theme-progress-bar-large-progress-light-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-light-stripe-1': '#0da2a4', + '--theme-progress-bar-large-progress-light-stripe-2-background-color': + '#1fc1c3', }, receiveQRCode: { '--theme-receive-qr-code-background-color': '#fff', @@ -909,6 +916,7 @@ export const DARK_CARDANO_THEME_OUTPUT = { '--theme-transactions-list-border-color': '1e1f31', '--theme-transactions-list-group-date-color': '#ffffff', '--theme-transactions-list-item-details-color': '#ffffff', + '--theme-transactions-list-item-highlight-color': '#ea4c5b', '--theme-transactions-state-ok-background-color': '#2cbb69', '--theme-transactions-state-pending-background-color': 'rgba(255, 255, 255, 0.5)', @@ -964,6 +972,25 @@ export const DARK_CARDANO_THEME_OUTPUT = { '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': '#fff', }, + voting: { + '--theme-voting-font-color-accent': '#ffffff', + '--theme-voting-font-color-light': '#ffffffb3', + '--theme-voting-font-color-regular': '#ffffff', + '--theme-voting-info-background-color': 'rgba(255, 255, 255, 0.1)', + '--theme-voting-info-font-color': '#ffffff', + '--theme-voting-registration-steps-activation-steps-indicator-color': + '#ffffff', + '--theme-voting-registration-steps-choose-wallet-error-message-color': + '#ea4c5b', + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': + '#ea4c5bb3', + '--theme-voting-registration-steps-deposit-fees-amount-color': '#ea4c5b', + '--theme-voting-registration-steps-deposit-fees-label-color': '#ffffff', + '--theme-voting-registration-steps-description-color': '#ffffffcc', + '--theme-voting-registration-steps-description-highlighted-color': + '#ffffff', + '--theme-voting-separator-color': 'rgba(255, 255, 255, 0.15)', + }, recoveryPhrase: { '--theme-recovery-phrase-normal-background-color': 'rgba(255, 255, 255, .1)', diff --git a/source/renderer/app/themes/daedalus/flight-candidate.js b/source/renderer/app/themes/daedalus/flight-candidate.js index 43862dd107..d1553196c4 100644 --- a/source/renderer/app/themes/daedalus/flight-candidate.js +++ b/source/renderer/app/themes/daedalus/flight-candidate.js @@ -488,6 +488,11 @@ export const FLIGHT_CANDIDATE_THEME_OUTPUT = { '--theme-progress-bar-large-progress-stripe1': '#e0e5eb', '--theme-progress-bar-large-progress-stripe2': '#fafbfc', '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-progress-bar-large-progress-dark-stripe1': '#e0e5eb', + '--theme-progress-bar-large-progress-dark-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-light-stripe-1': '#e6a009', + '--theme-progress-bar-large-progress-light-stripe-2-background-color': + '#ffc64d', }, receiveQRCode: { '--theme-receive-qr-code-background-color': '#fff', @@ -909,6 +914,7 @@ export const FLIGHT_CANDIDATE_THEME_OUTPUT = { '--theme-transactions-list-border-color': '1e1f31', '--theme-transactions-list-group-date-color': '#ffffff', '--theme-transactions-list-item-details-color': '#ffffff', + '--theme-transactions-list-item-highlight-color': '#ea4c5b', '--theme-transactions-state-ok-background-color': '#2cbb69', '--theme-transactions-state-pending-background-color': 'rgba(255, 255, 255, 0.5)', @@ -964,6 +970,25 @@ export const FLIGHT_CANDIDATE_THEME_OUTPUT = { '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': '#fff', }, + voting: { + '--theme-voting-font-color-accent': '#ffffff', + '--theme-voting-font-color-light': '#ffffffb3', + '--theme-voting-font-color-regular': '#ffffff', + '--theme-voting-info-background-color': 'rgba(255, 255, 255, 0.1)', + '--theme-voting-info-font-color': '#ffffff', + '--theme-voting-registration-steps-activation-steps-indicator-color': + '#ffffff', + '--theme-voting-registration-steps-choose-wallet-error-message-color': + '#ea4c5b', + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': + '#ea4c5bb3', + '--theme-voting-registration-steps-deposit-fees-amount-color': '#ea4c5b', + '--theme-voting-registration-steps-deposit-fees-label-color': '#ffffff', + '--theme-voting-registration-steps-description-color': '#ffffffcc', + '--theme-voting-registration-steps-description-highlighted-color': + '#ffffff', + '--theme-voting-separator-color': 'rgba(255, 255, 255, 0.15)', + }, recoveryPhrase: { '--theme-recovery-phrase-normal-background-color': 'rgba(255, 255, 255, .1)', diff --git a/source/renderer/app/themes/daedalus/incentivized-testnet.js b/source/renderer/app/themes/daedalus/incentivized-testnet.js index 1fcf9a6ec6..9bfbe3ddb9 100644 --- a/source/renderer/app/themes/daedalus/incentivized-testnet.js +++ b/source/renderer/app/themes/daedalus/incentivized-testnet.js @@ -488,6 +488,11 @@ export const INCENTIVIZED_TESTNET_THEME_OUTPUT = { '--theme-progress-bar-large-progress-stripe1': '#e0e5eb', '--theme-progress-bar-large-progress-stripe2': '#fafbfc', '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-progress-bar-large-progress-dark-stripe1': '#e0e5eb', + '--theme-progress-bar-large-progress-dark-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-light-stripe-1': '#bf0535', + '--theme-progress-bar-large-progress-light-stripe-2-background-color': + '#eb2256', }, receiveQRCode: { '--theme-receive-qr-code-background-color': '#fff', @@ -923,6 +928,7 @@ export const INCENTIVIZED_TESTNET_THEME_OUTPUT = { '--theme-transactions-list-border-color': '1e1f31', '--theme-transactions-list-group-date-color': '#ffffff', '--theme-transactions-list-item-details-color': '#ffffff', + '--theme-transactions-list-item-highlight-color': '#eb4a22', '--theme-transactions-search-background-color': '#121326', '--theme-transactions-state-ok-background-color': '#2cbb69', '--theme-transactions-state-pending-background-color': @@ -967,6 +973,25 @@ export const INCENTIVIZED_TESTNET_THEME_OUTPUT = { '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': '#fff', }, + voting: { + '--theme-voting-font-color-accent': '#ffffff', + '--theme-voting-font-color-light': '#ffffffb3', + '--theme-voting-font-color-regular': '#ffffff', + '--theme-voting-info-background-color': 'rgba(255, 255, 255, 0.1)', + '--theme-voting-info-font-color': '#ffffff', + '--theme-voting-registration-steps-activation-steps-indicator-color': + '#ffffff', + '--theme-voting-registration-steps-choose-wallet-error-message-color': + '#eb4a22', + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': + '#eb4a22b3', + '--theme-voting-registration-steps-deposit-fees-amount-color': '#ea4c5b', + '--theme-voting-registration-steps-deposit-fees-label-color': '#ffffff', + '--theme-voting-registration-steps-description-color': '#ffffffcc', + '--theme-voting-registration-steps-description-highlighted-color': + '#ffffff', + '--theme-voting-separator-color': 'rgba(255, 255, 255, 0.15)', + }, recoveryPhrase: { '--theme-recovery-phrase-normal-background-color': 'rgba(255, 255, 255, .1)', diff --git a/source/renderer/app/themes/daedalus/light-blue.js b/source/renderer/app/themes/daedalus/light-blue.js index 03ab6921ae..d91c8b46de 100644 --- a/source/renderer/app/themes/daedalus/light-blue.js +++ b/source/renderer/app/themes/daedalus/light-blue.js @@ -505,6 +505,11 @@ export const LIGHT_BLUE_THEME_OUTPUT = { '--theme-progress-bar-large-progress-stripe1': '#e0e5eb', '--theme-progress-bar-large-progress-stripe2': '#fafbfc', '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-progress-bar-large-progress-dark-stripe1': '#e0e5eb', + '--theme-progress-bar-large-progress-dark-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-light-stripe-1': '#34465e', + '--theme-progress-bar-large-progress-light-stripe-2-background-color': + '#445b7c', }, receiveQRCode: { '--theme-receive-qr-code-background-color': 'transparent', @@ -921,6 +926,7 @@ export const LIGHT_BLUE_THEME_OUTPUT = { '--theme-transactions-list-border-color': '#c6cdd6', '--theme-transactions-list-group-date-color': '#5e6066', '--theme-transactions-list-item-details-color': '#5e6066', + '--theme-transactions-list-item-highlight-color': '#ea4c5b', '--theme-transactions-state-ok-background-color': '#007600', '--theme-transactions-state-pending-background-color': 'rgba(94, 96, 102, 0.5)', @@ -975,6 +981,26 @@ export const LIGHT_BLUE_THEME_OUTPUT = { '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': '#fafbfc', }, + voting: { + '--theme-voting-font-color-accent': '#5e6066', + '--theme-voting-font-color-light': 'rgba(94, 96, 102, 0.7)', + '--theme-voting-font-color-regular': '#5e6066', + '--theme-voting-info-background-color': 'rgba(94, 96, 102, 0.1)', + '--theme-voting-info-font-color': '#5e6066', + '--theme-voting-registration-steps-activation-steps-indicator-color': + '#5e6066', + '--theme-voting-registration-steps-choose-wallet-error-message-color': + '#ea4c5b', + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': + 'rgba(234, 76, 91, 0.7)', + '--theme-voting-registration-steps-deposit-fees-amount-color': '#ea4c5b', + '--theme-voting-registration-steps-deposit-fees-label-color': '#5e6066', + '--theme-voting-registration-steps-description-color': + 'rgba(94, 96, 102, 0.8)', + '--theme-voting-registration-steps-description-highlighted-color': + '#5e6066', + '--theme-voting-separator-color': 'rgba(94, 96, 102, 0.15)', + }, recoveryPhrase: { '--theme-recovery-phrase-normal-background-color': 'transparent', '--theme-recovery-phrase-normal-border-color': 'rgba(68, 91, 124, .07)', diff --git a/source/renderer/app/themes/daedalus/shelley-testnet.js b/source/renderer/app/themes/daedalus/shelley-testnet.js index addcc842f4..176b2595fa 100644 --- a/source/renderer/app/themes/daedalus/shelley-testnet.js +++ b/source/renderer/app/themes/daedalus/shelley-testnet.js @@ -488,6 +488,11 @@ export const SHELLEY_TESTNET_THEME_OUTPUT = { '--theme-progress-bar-large-progress-stripe1': '#e0e5eb', '--theme-progress-bar-large-progress-stripe2': '#fafbfc', '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-progress-bar-large-progress-dark-stripe1': '#e0e5eb', + '--theme-progress-bar-large-progress-dark-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-light-stripe-1': '#676ddf', + '--theme-progress-bar-large-progress-light-stripe-2-background-color': + '#898ee6', }, receiveQRCode: { '--theme-receive-qr-code-background-color': '#fff', @@ -908,6 +913,7 @@ export const SHELLEY_TESTNET_THEME_OUTPUT = { '--theme-transactions-list-border-color': '1e1f31', '--theme-transactions-list-group-date-color': '#ffffff', '--theme-transactions-list-item-details-color': '#ffffff', + '--theme-transactions-list-item-highlight-color': '#ea4c5b', '--theme-transactions-state-ok-background-color': '#2cbb69', '--theme-transactions-state-pending-background-color': 'rgba(255, 255, 255, 0.5)', @@ -963,6 +969,25 @@ export const SHELLEY_TESTNET_THEME_OUTPUT = { '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': '#fff', }, + voting: { + '--theme-voting-font-color-accent': '#ffffff', + '--theme-voting-font-color-light': '#ffffffb3', + '--theme-voting-font-color-regular': '#ffffff', + '--theme-voting-info-background-color': 'rgba(255, 255, 255, 0.1)', + '--theme-voting-info-font-color': '#ffffff', + '--theme-voting-registration-steps-activation-steps-indicator-color': + '#ffffff', + '--theme-voting-registration-steps-choose-wallet-error-message-color': + '#ea4c5b', + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': + '#ea4c5bb3', + '--theme-voting-registration-steps-deposit-fees-amount-color': '#ea4c5b', + '--theme-voting-registration-steps-deposit-fees-label-color': '#ffffff', + '--theme-voting-registration-steps-description-color': '#ffffffcc', + '--theme-voting-registration-steps-description-highlighted-color': + '#ffffff', + '--theme-voting-separator-color': 'rgba(255, 255, 255, 0.15)', + }, recoveryPhrase: { '--theme-recovery-phrase-normal-background-color': 'rgba(255, 255, 255, .1)', diff --git a/source/renderer/app/themes/daedalus/white.js b/source/renderer/app/themes/daedalus/white.js index 9de36ad520..0f80ccdff0 100644 --- a/source/renderer/app/themes/daedalus/white.js +++ b/source/renderer/app/themes/daedalus/white.js @@ -494,6 +494,11 @@ export const WHITE_THEME_OUTPUT = { '--theme-progress-bar-large-progress-stripe1': '#29b595', '--theme-progress-bar-large-progress-stripe2': '#69cbb4', '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-progress-bar-large-progress-dark-stripe1': '#29b595', + '--theme-progress-bar-large-progress-dark-stripe2': '#69cbb4', + '--theme-progress-bar-large-progress-light-stripe-1': '#29b595', + '--theme-progress-bar-large-progress-light-stripe-2-background-color': + '#69cbb4', }, receiveQRCode: { '--theme-receive-qr-code-background-color': 'transparent', @@ -914,6 +919,7 @@ export const WHITE_THEME_OUTPUT = { '--theme-transactions-list-border-color': 'transparent', '--theme-transactions-list-group-date-color': '#2d2d2d', '--theme-transactions-list-item-details-color': '#2d2d2d', + '--theme-transactions-list-item-highlight-color': '#ea4c5b', '--theme-transactions-state-ok-background-color': 'rgba(0, 118, 0, 1);', '--theme-transactions-state-pending-background-color': 'rgba(45, 45, 45, 0.5)', @@ -968,6 +974,25 @@ export const WHITE_THEME_OUTPUT = { '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': '#fafbfc', }, + voting: { + '--theme-voting-font-color-accent': '#2d2d2d', + '--theme-voting-font-color-light': '#2d2d2db3', + '--theme-voting-font-color-regular': '#2d2d2d', + '--theme-voting-info-background-color': 'rgba(45, 45, 45, 0.1)', + '--theme-voting-info-font-color': '#2d2d2d', + '--theme-voting-registration-steps-activation-steps-indicator-color': + '#2d2d2d', + '--theme-voting-registration-steps-choose-wallet-error-message-color': + '#ea4c5b', + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': + '#ea4c5bb3', + '--theme-voting-registration-steps-deposit-fees-amount-color': '#ea4c5b', + '--theme-voting-registration-steps-deposit-fees-label-color': '#2d2d2d', + '--theme-voting-registration-steps-description-color': '#2d2d2dcc', + '--theme-voting-registration-steps-description-highlighted-color': + '#2d2d2d', + '--theme-voting-separator-color': 'rgba(45, 45, 45, 0.15)', + }, recoveryPhrase: { '--theme-recovery-phrase-normal-background-color': 'rgba(45, 45, 45, .1)', '--theme-recovery-phrase-normal-border-color': 'transparent', diff --git a/source/renderer/app/themes/daedalus/yellow.js b/source/renderer/app/themes/daedalus/yellow.js index 55a00ebbca..1a5d9f9ece 100644 --- a/source/renderer/app/themes/daedalus/yellow.js +++ b/source/renderer/app/themes/daedalus/yellow.js @@ -493,6 +493,11 @@ export const YELLOW_THEME_OUTPUT = { '--theme-progress-bar-large-progress-stripe1': '#2d2d2d', '--theme-progress-bar-large-progress-stripe2': '#3f3e3e', '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', + '--theme-progress-bar-large-progress-dark-stripe1': '#2d2d2d', + '--theme-progress-bar-large-progress-dark-stripe2': '#3f3e3e', + '--theme-progress-bar-large-progress-light-stripe-1': '#000000', + '--theme-progress-bar-large-progress-light-stripe-2-background-color': + '#2d2d2d', }, receiveQRCode: { '--theme-receive-qr-code-background-color': 'transparent', @@ -913,6 +918,7 @@ export const YELLOW_THEME_OUTPUT = { '--theme-transactions-list-border-color': '#e1dac6', '--theme-transactions-list-group-date-color': '#2d2d2d', '--theme-transactions-list-item-details-color': '#2d2d2d', + '--theme-transactions-list-item-highlight-color': '#ea4c5b', '--theme-transactions-state-ok-background-color': '#007600', '--theme-transactions-state-pending-background-color': 'rgba(45, 45, 45, 0.5)', @@ -967,6 +973,25 @@ export const YELLOW_THEME_OUTPUT = { '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': '#fff', }, + voting: { + '--theme-voting-font-color-accent': '#2d2d2d', + '--theme-voting-font-color-light': '#2d2d2db3', + '--theme-voting-font-color-regular': '#2d2d2d', + '--theme-voting-info-background-color': 'rgba(45, 45, 45, 0.1)', + '--theme-voting-info-font-color': '#2d2d2d', + '--theme-voting-registration-steps-activation-steps-indicator-color': + '#2d2d2d', + '--theme-voting-registration-steps-choose-wallet-error-message-color': + '#ea4c5b', + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': + '#ea4c5bb3', + '--theme-voting-registration-steps-deposit-fees-amount-color': '#ea4c5b', + '--theme-voting-registration-steps-deposit-fees-label-color': '#2d2d2d', + '--theme-voting-registration-steps-description-color': '#2d2d2dcc', + '--theme-voting-registration-steps-description-highlighted-color': + '#2d2d2d', + '--theme-voting-separator-color': 'rgba(45, 45, 45, 0.15)', + }, recoveryPhrase: { '--theme-recovery-phrase-normal-background-color': 'rgba(45, 45, 45, .1)', '--theme-recovery-phrase-normal-border-color': 'transparent', diff --git a/source/renderer/app/themes/utils/createTheme.js b/source/renderer/app/themes/utils/createTheme.js index 4d92e5660b..1d96d831cc 100644 --- a/source/renderer/app/themes/utils/createTheme.js +++ b/source/renderer/app/themes/utils/createTheme.js @@ -830,6 +830,10 @@ export const createDaedalusComponentsTheme = ( ).alpha(0.7)}`, '--theme-progress-bar-large-progress-stripe1': '#e0e5eb', '--theme-progress-bar-large-progress-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-dark-stripe1': '#e0e5eb', + '--theme-progress-bar-large-progress-dark-stripe2': '#fafbfc', + '--theme-progress-bar-large-progress-light-stripe-1': `${background.secondary.dark}`, + '--theme-progress-bar-large-progress-light-stripe-2': `${background.secondary.regular}`, '--theme-progress-bar-large-background-color': 'rgba(0, 0, 0, 0.1)', }, receiveQRCode: { @@ -1154,6 +1158,7 @@ export const createDaedalusComponentsTheme = ( '--theme-transactions-list-border-color': `${border}`, '--theme-transactions-list-group-date-color': `${text.primary}`, '--theme-transactions-list-item-details-color': `${text.primary}`, + '--theme-transactions-list-item-highlight-color': `${error.regular}`, '--theme-transactions-state-ok-background-color': '#007600', '--theme-transactions-state-pending-background-color': `${background.primary.dark}`, '--theme-transactions-state-pending-warning-background-color': '#ec5d6b', @@ -1218,6 +1223,27 @@ export const createDaedalusComponentsTheme = ( '--theme-utxo-tooltip-shadow-color': 'rgba(0, 0, 0, 0.18)', '--theme-utxo-tooltip-text-color': `${text.secondary}`, }, + voting: { + '--theme-voting-font-color-accent': `${focus}`, + '--theme-voting-font-color-light': `${chroma(text.primary).alpha(0.7)}`, + '--theme-voting-font-color-regular': `${text.primary}`, + '--theme-voting-info-background-color': `${chroma( + background.primary.darkest + )}`, + '--theme-voting-info-font-color': `${chroma(background.primary.darkest)}`, + '--theme-voting-registration-steps-activation-steps-indicator-color': `${text.primary}`, + '--theme-voting-registration-steps-choose-wallet-error-message-color': `${error.regular}`, + '--theme-voting-registration-steps-choose-wallet-error-message-light-color': `${chroma( + error.regular + ).alpha(0.7)}`, + '--theme-voting-registration-steps-deposit-fees-amount-color': `${error.regular}`, + '--theme-voting-registration-steps-deposit-fees-label-color': `${text.primary}`, + '--theme-voting-registration-steps-description-color': `${chroma( + text.primary + ).alpha(0.8)}`, + '--theme-voting-registration-steps-description-highlighted-color': `${text.primary}`, + '--theme-voting-separator-color': `${chroma(text.primary).alpha(0.15)}`, + }, walletRestoreDialog: { '--theme-wallet-restore-dialog-new-label-background-color': `${chroma( background.primary.regular diff --git a/source/renderer/app/types/TransactionMetadata.js b/source/renderer/app/types/TransactionMetadata.js new file mode 100644 index 0000000000..ee5455754d --- /dev/null +++ b/source/renderer/app/types/TransactionMetadata.js @@ -0,0 +1,14 @@ +// @flow +export type MetadataInteger = {| int: number |}; +export type MetadataString = {| string: string |}; +export type MetadataBytes = {| bytes: string |}; +export type MetadataList = {| list: MetadataValue[] |}; +export type MetadataMapValue = {| k: MetadataValue, v: MetadataValue |}; +export type MetadataMap = {| map: MetadataMapValue[] |}; +export type MetadataValue = + | MetadataInteger + | MetadataString + | MetadataBytes + | MetadataList + | MetadataMap; +export type TransactionMetadata = { [string]: MetadataValue }; diff --git a/source/renderer/app/types/currencyTypes.js b/source/renderer/app/types/currencyTypes.js new file mode 100644 index 0000000000..8eefa232e8 --- /dev/null +++ b/source/renderer/app/types/currencyTypes.js @@ -0,0 +1,28 @@ +// @flow +import type { HttpOptions } from '../api/utils/externalRequest'; + +export type Currency = { + symbol: string, + name: string, + decimalDigits?: number, + id?: string, +}; + +export type Request = HttpOptions | Function; + +export type RequestName = 'list' | 'rate'; + +export type CurrencyApiConfig = { + id: string, + name: string, + hostname: string, + website: string, + requests: { + list?: Request, + rate: Request, + }, + responses: { + list: Function, + rate: Function, + }, +}; diff --git a/source/renderer/app/types/notificationTypes.js b/source/renderer/app/types/notificationTypes.js index 64ed611293..e1ae91f179 100644 --- a/source/renderer/app/types/notificationTypes.js +++ b/source/renderer/app/types/notificationTypes.js @@ -9,6 +9,7 @@ export type NotificationId = | 'downloadLogsProgress' | 'downloadLogsSuccess' | 'downloadQRCodeImageSuccess' + | 'downloadVotingPDFSuccess' | 'downloadRewardsCSVSuccess' | 'downloadTransactionsCSVSuccess'; diff --git a/source/renderer/app/types/stakingTypes.js b/source/renderer/app/types/stakingTypes.js index a3a2a8e3b8..48d90e0874 100644 --- a/source/renderer/app/types/stakingTypes.js +++ b/source/renderer/app/types/stakingTypes.js @@ -1,4 +1,5 @@ // @flow export type RedeemItnRewardsStep = 'configuration' | 'confirmation' | 'result'; +export type SmashServerType = 'iohk' | 'custom' | 'direct' | 'none'; export type DelegationAction = 'join' | 'quit'; diff --git a/source/renderer/app/types/walletExportTypes.js b/source/renderer/app/types/walletExportTypes.js index fc94449019..6163455184 100644 --- a/source/renderer/app/types/walletExportTypes.js +++ b/source/renderer/app/types/walletExportTypes.js @@ -33,7 +33,7 @@ export type ExportedByronWallet = { name: ?string, id: string, passphrase_hash: string, - is_passphrase_empty: boolean, + isEmptyPassphrase: boolean, // Daedalus derived wallet props hasName: boolean, diff --git a/source/renderer/app/utils/formatters.js b/source/renderer/app/utils/formatters.js index bc31bf11c8..d2d7dff228 100644 --- a/source/renderer/app/utils/formatters.js +++ b/source/renderer/app/utils/formatters.js @@ -30,6 +30,16 @@ export const formattedWalletAmount = ( return formattedAmount.toString(); }; +export const formattedWalletCurrencyAmount = ( + amount: BigNumber, + currencyRate: number, + decimalDigits?: ?number, + currencySymbol?: ?string +) => + `${amount ? amount.times(currencyRate).toFormat(decimalDigits || 2) : 0} ${ + currencySymbol || '' + }`; + // Symbol Name Scientific Notation // K Thousand 1.00E+03 // M Million 1.00E+06 @@ -41,31 +51,31 @@ export const shortNumber = (value: number | BigNumber): string => { let formattedAmount = ''; if (amount.isZero()) { formattedAmount = '0'; - } else if (amount.lessThan(1000)) { - formattedAmount = `${amount.round( + } else if (amount.isLessThan(1000)) { + formattedAmount = `${amount.decimalPlaces( DECIMAL_PLACES_IN_ADA, BigNumber.ROUND_DOWN )}`; - } else if (amount.lessThan(1000000)) { + } else if (amount.isLessThan(1000000)) { formattedAmount = `${amount .dividedBy(1000) - .round(1, BigNumber.ROUND_DOWN)}K`; - } else if (amount.lessThan(1000000000)) { + .decimalPlaces(1, BigNumber.ROUND_DOWN)}K`; + } else if (amount.isLessThan(1000000000)) { formattedAmount = `${amount .dividedBy(1000000) - .round(1, BigNumber.ROUND_DOWN)}M`; - } else if (amount.lessThan(1000000000000)) { + .decimalPlaces(1, BigNumber.ROUND_DOWN)}M`; + } else if (amount.isLessThan(1000000000000)) { formattedAmount = `${amount .dividedBy(1000000000) - .round(1, BigNumber.ROUND_DOWN)}B`; - } else if (amount.lessThan(1000000000000000)) { + .decimalPlaces(1, BigNumber.ROUND_DOWN)}B`; + } else if (amount.isLessThan(1000000000000000)) { formattedAmount = `${amount .dividedBy(1000000000000) - .round(1, BigNumber.ROUND_DOWN)}T`; + .decimalPlaces(1, BigNumber.ROUND_DOWN)}T`; } else { formattedAmount = `${amount .dividedBy(1000000000000000) - .round(1, BigNumber.ROUND_DOWN)}Q`; + .decimalPlaces(1, BigNumber.ROUND_DOWN)}Q`; } return formattedAmount; }; @@ -79,12 +89,12 @@ export const formattedAmountToNaturalUnits = (amount: string): string => { return cleanedAmount === '' ? '0' : cleanedAmount; }; -export const formattedAmountToBigNumber = (amount: string) => { +export const formattedAmountToBigNumber = (amount: string): BigNumber => { const cleanedAmount = amount.replace(/,/g, ''); return new BigNumber(cleanedAmount !== '' ? cleanedAmount : 0); }; -export const toFixedUserFormat = (number: number, digits: number) => { +export const toFixedUserFormat = (number: number, digits: number): string => { // This is necessary, because the BigNumber version we use // can't receive numbers with more than 15 digits const parsedNumber = parseFloat(number).toFixed(digits); @@ -107,7 +117,7 @@ export const formattedBytesToSize = (bytes: number): string => { 10 ); if (i === 0) return `${bytes} ${sizes[i]})`; - return `${(bytes / 1024 ** i).toFixed(1)} ${sizes[i]}`; + return `${formattedNumber(bytes / 1024 ** i, 1)} ${sizes[i]}`; }; export type FormattedDownloadData = { @@ -148,10 +158,59 @@ export const formattedDownloadData = ( }; }; -export const generateThousands = (value: number) => { +export const generateThousands = (value: number): number => { if (value <= 1000) { return Math.round(value); } return Math.round(value / 1000) * 1000; }; + +export const formattedArrayBufferToHexString = ( + arrayBuffer: Uint8Array +): string => { + const buff = new Uint8Array(arrayBuffer); + const byteToHex = []; + const hexOctets = []; + + for (let n = 0; n <= 0xff; ++n) { + const hexOctet = `0${n.toString(16)}`.slice(-2); + byteToHex.push(hexOctet); + } + + for (let i = 0; i < buff.length; ++i) { + hexOctets.push(byteToHex[buff[i]]); + } + + return hexOctets.join(''); +}; + +export const formattedNumber = (value: number | string, dp?: number): string => + new BigNumber(value).toFormat(dp); + +export const formattedCpuModel = (model: string): string => { + const atCharPosition = model.indexOf('@'); + const speedSection = model.substring(atCharPosition); + const speedNumbers = speedSection.match(/[\d,.]+/g); + const speedNumber = speedNumbers ? speedNumbers[0] : ''; + const formattedSpeedNumber = formattedNumber(speedNumber, 2); + const formattedSpeedSection = speedSection.replace( + /[\d,.]+/, + formattedSpeedNumber + ); + const formattedModel = `${model.substring( + 0, + atCharPosition + )}${formattedSpeedSection}`; + + return formattedModel; +}; + +export const formattedSize = (size: string): string => { + const sizeNumbers = size.match(/[\d,.]+/g); + const sizeNumber = sizeNumbers ? sizeNumbers[0] : ''; + const formattedSizeNumber = formattedNumber(sizeNumber); + const formattedResult = size.replace(/[\d,.]+/, formattedSizeNumber); + + return formattedResult; +}; diff --git a/source/renderer/app/utils/sortComparators.js b/source/renderer/app/utils/sortComparators.js new file mode 100644 index 0000000000..cd0e19a440 --- /dev/null +++ b/source/renderer/app/utils/sortComparators.js @@ -0,0 +1,51 @@ +// @flow +import BigNumber from 'bignumber.js'; +import moment from 'moment'; + +export const bigNumberComparator = ( + numberA: BigNumber, + numberB: BigNumber, + isAscending: boolean = true +): number => { + if (numberA.isLessThan(numberB)) { + return isAscending ? -1 : 1; + } + + if (numberA.isGreaterThan(numberB)) { + return isAscending ? 1 : -1; + } + + return 0; +}; + +export const stringComparator = ( + stringA: string, + stringB: string, + isAscending: boolean = true +): number => { + if (stringA < stringB) { + return isAscending ? -1 : 1; + } + + if (stringA > stringB) { + return isAscending ? 1 : -1; + } + + return 0; +}; + +export const dateComparator = ( + dateA: string, + dateB: string, + isAscending: boolean = true +): number => { + if (moment(dateA).unix() < moment(dateB).unix()) { + return isAscending ? -1 : 1; + } + + if (moment(dateA).unix() > moment(dateB).unix()) { + return isAscending ? 1 : -1; + } + + return 0; +}; diff --git a/source/renderer/app/utils/staking.js b/source/renderer/app/utils/staking.js new file mode 100644 index 0000000000..8d9116ba1e --- /dev/null +++ b/source/renderer/app/utils/staking.js @@ -0,0 +1,52 @@ +// @flow +import { reduce } from 'lodash'; +import { + SMASH_SERVERS_LIST, + SMASH_SERVER_TYPES, +} from '../config/stakingConfig'; +import type { SmashServerType } from '../types/stakingTypes'; + +export const getSmashServerNameFromUrl = (smashServerUrl: string): string => + reduce( + SMASH_SERVERS_LIST, + (result, { name, url }) => { + if (url === smashServerUrl) result = name; + return result; + }, + smashServerUrl + ); + +export const getSmashServerIdFromUrl = ( + smashServerUrl: string +): SmashServerType => + reduce( + SMASH_SERVERS_LIST, + (result, { url }, id) => { + if (url === smashServerUrl) result = id; + return result; + }, + SMASH_SERVER_TYPES.CUSTOM + ); + +export const getUrlParts = ( + url: string +): { + hash: string, + host: string, + hostname: string, + href: string, + origin: string, + password: string, + pathname: string, + port: string, + protocol: string, + search: string, + searchParams: URLSearchParams, + username: string, +} => { + try { + return new URL(url); + } catch (error) { + return {}; + } +}; diff --git a/source/renderer/app/utils/transaction.js b/source/renderer/app/utils/transaction.js index 6387874c1b..a18534449e 100644 --- a/source/renderer/app/utils/transaction.js +++ b/source/renderer/app/utils/transaction.js @@ -1,19 +1,19 @@ // @flow -import React from 'react'; -import moment from 'moment'; import BigNumber from 'bignumber.js'; +import moment from 'moment'; +import React from 'react'; +import type { CoinSelectionsResponse } from '../api/transactions/types'; import { - WalletTransaction, TransactionTypes, + WalletTransaction, } from '../domains/WalletTransaction'; -import { formattedWalletAmount } from './formatters'; -import { DateRangeTypes } from '../stores/TransactionsStore'; -import type { TransactionFilterOptionsType } from '../stores/TransactionsStore'; import type { ByronEncodeSignedTransactionRequest, ByronSignedTransactionWitnesses, } from '../stores/HardwareWalletsStore'; -import type { CoinSelectionsResponse } from '../api/transactions/types'; +import type { TransactionFilterOptionsType } from '../stores/TransactionsStore'; +import { DateRangeTypes } from '../stores/TransactionsStore'; +import { formattedWalletAmount } from './formatters'; const cbor = require('cbor'); const bs58 = require('bs58'); @@ -85,10 +85,10 @@ export const isTransactionAmountInFilterRange = ( ? new BigNumber(0) : new BigNumber(toAmount); const compareFrom = fromAmount - ? amount.absoluteValue().greaterThanOrEqualTo(min) + ? amount.absoluteValue().isGreaterThanOrEqualTo(min) : true; const compareTo = toAmount - ? amount.absoluteValue().lessThanOrEqualTo(max) + ? amount.absoluteValue().isLessThanOrEqualTo(max) : true; return compareFrom && compareTo; @@ -268,7 +268,7 @@ export const validateFilterForm = (values: { if ( fromAmount && toAmount && - new BigNumber(fromAmount).greaterThan(new BigNumber(toAmount)) + new BigNumber(fromAmount).isGreaterThan(new BigNumber(toAmount)) ) { invalidFields.toAmount = true; } diff --git a/source/renderer/app/utils/validations.js b/source/renderer/app/utils/validations.js index b5e65063b1..5abe21639d 100644 --- a/source/renderer/app/utils/validations.js +++ b/source/renderer/app/utils/validations.js @@ -111,3 +111,13 @@ export function validateMnemonics(params: ValidateMnemonicsParams) { export function errorOrIncompleteMarker(error: string) { return error === INCOMPLETE_MNEMONIC_MARKER ? null : error; } + +/** + * Voting PIN code validation + */ +export const isValidPinCode = (pinCode: string, length: number): boolean => { + return pinCode.length === length; +}; + +export const isValidRepeatPinCode = (pinCode: string, repeatPinCode: string) => + pinCode === repeatPinCode; diff --git a/source/renderer/app/utils/votingPDFGenerator.js b/source/renderer/app/utils/votingPDFGenerator.js new file mode 100644 index 0000000000..ca0c0e0b41 --- /dev/null +++ b/source/renderer/app/utils/votingPDFGenerator.js @@ -0,0 +1,112 @@ +// @flow +import moment from 'moment'; +import path from 'path'; +import { defineMessages } from 'react-intl'; +import { generateVotingPDFChannel } from '../ipc/generateVotingPDFChannel'; +import type { Network } from '../../../common/types/environment.types'; +import { generateFileNameWithTimestamp } from '../../../common/utils/files'; +import { showSaveDialogChannel } from '../ipc/show-file-dialog-channels'; +import globalMessages from '../i18n/global-messages'; + +const messages = defineMessages({ + title: { + id: 'voting.votingRegistration.pdf.title', + defaultMessage: '!!!Fund{fundNumber} Voting Registration', + description: 'PDF title', + }, + walletNameLabel: { + id: 'voting.votingRegistration.pdf.walletNameLabel', + defaultMessage: '!!!Wallet name', + description: 'PDF wallet name title', + }, + filename: { + id: 'voting.votingRegistration.pdf.filename', + defaultMessage: '!!!voting-registration', + description: 'PDF filename title', + }, + networkLabel: { + id: 'voting.votingRegistration.pdf.networkLabel', + defaultMessage: '!!!Cardano network:', + description: 'PDF networkLabel label', + }, + author: { + id: 'voting.votingRegistration.pdf.author', + defaultMessage: '!!!Daedalus wallet', + description: 'PDF author', + }, +}); + +type Params = { + fundNumber: number, + qrCode: string, + walletName: string, + currentLocale: string, + currentDateFormat: string, + currentTimeFormat: string, + desktopDirectoryPath: string, + network: Network, + isMainnet: boolean, + intl: Object, +}; + +export const votingPDFGenerator = async ({ + fundNumber, + qrCode, + walletName, + currentLocale, + currentDateFormat, + currentTimeFormat, + desktopDirectoryPath, + network, + isMainnet, + intl, +}: Params) => { + // Consolidate data + const title = intl.formatMessage(messages.title, { fundNumber }); + const creationDate = moment().format( + `${currentDateFormat} ${currentTimeFormat}` + ); + const walletNameLabel = intl.formatMessage(messages.walletNameLabel); + const networkLabel = intl.formatMessage(messages.networkLabel); + const networkName = intl.formatMessage(globalMessages[`network_${network}`]); + const author = intl.formatMessage(messages.author); + + // Generate the filePath + const localizedFileName = intl.formatMessage(messages.filename); + const prefix = `fund${fundNumber}-${localizedFileName}-${walletName}`; + const name = generateFileNameWithTimestamp({ + prefix, + extension: '', + isUTC: false, + }); + const fileExtension = 'pdf'; + const defaultPath = path.join( + desktopDirectoryPath, + `${name}.${fileExtension}` + ); + const params = { + defaultPath, + filters: [ + { + name, + extensions: [fileExtension], + }, + ], + }; + const dialogPath = await showSaveDialogChannel.send(params); + const filePath = dialogPath.filePath || ''; + + await generateVotingPDFChannel.send({ + title, + currentLocale, + creationDate, + qrCode, + walletNameLabel, + walletName, + isMainnet, + networkLabel, + networkName, + filePath, + author, + }); +}; diff --git a/source/renderer/app/utils/walletUtils.js b/source/renderer/app/utils/walletUtils.js new file mode 100644 index 0000000000..cce471c62a --- /dev/null +++ b/source/renderer/app/utils/walletUtils.js @@ -0,0 +1 @@ +export default import('@iohk-jormungandr/wallet-js').then((modules) => modules); diff --git a/source/renderer/app/utils/walletsForStakePoolsRanking.js b/source/renderer/app/utils/walletsForStakePoolsRanking.js index 6a5debb161..0bd5b60cf5 100644 --- a/source/renderer/app/utils/walletsForStakePoolsRanking.js +++ b/source/renderer/app/utils/walletsForStakePoolsRanking.js @@ -6,7 +6,7 @@ import { MIN_DELEGATION_FUNDS } from '../config/stakingConfig'; export const getFilteredWallets = (wallets: Array): Array => { return wallets.filter( (w: Wallet) => - w.amount.greaterThanOrEqualTo(new BigNumber(MIN_DELEGATION_FUNDS)) && + w.amount.isGreaterThanOrEqualTo(new BigNumber(MIN_DELEGATION_FUNDS)) && !w.isLegacy ); }; diff --git a/storybook/stories/_support/utils.js b/storybook/stories/_support/utils.js index d4a934f3ed..b3c402d9db 100644 --- a/storybook/stories/_support/utils.js +++ b/storybook/stories/_support/utils.js @@ -1,6 +1,7 @@ // @flow import hash from 'hash.js'; import faker from 'faker'; +import JSONBigInt from 'json-bigint'; import moment from 'moment'; import { random, get } from 'lodash'; import BigNumber from 'bignumber.js'; @@ -24,6 +25,52 @@ import type { TransactionState, } from '../../../source/renderer/app/api/transactions/types'; import type { SyncStateStatus } from '../../../source/renderer/app/api/wallets/types'; +import type { TransactionMetadata } from '../../../source/renderer/app/types/TransactionMetadata'; + +export const EXAMPLE_METADATA = JSONBigInt.parse(`{ + "0": { + "string": "some string" + }, + "1": { + "int": 99999999999999999999999 + }, + "2": { + "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" + }, + "3": { + "list": [ + { "int": 14 }, + { "int": 42 }, + { "string": "1337" }, + { "list": [ + { "string": "nested list" } + ]} + ] + }, + "4": { + "map": [ + { + "k": { "int": "5" }, + "v": { "bytes": "2512a00e9653fe49a44a5886202e24d77eeb998f" } + }, + { + "k": { "map": [ + { + "k": { "int": 14 }, + "v": { "int": 42 } + } + ]}, + "v": { "string": "nested" } + }, + { + "k": { "string": "key" }, + "v": { "list": [ + { "string": "nested list" } + ] } + } + ] + } + }`); export const generateHash = () => { const now = new Date().valueOf().toString(); @@ -75,22 +122,24 @@ export const generateTransaction = ( type: TransactionType = TransactionTypes.INCOME, date: Date = faker.date.past(), amount: BigNumber = new BigNumber(faker.finance.amount()), + fee: BigNumber = new BigNumber(faker.finance.amount()), + deposit: BigNumber = new BigNumber(faker.finance.amount()), state: TransactionState = TransactionStates.OK, hasUnresolvedIncomeAddresses: boolean = false, noIncomeAddresses: boolean = false, - noWithdrawals: boolean = true + noWithdrawals: boolean = true, + metadata?: TransactionMetadata = EXAMPLE_METADATA ) => new WalletTransaction({ id: faker.random.uuid(), title: '', type, amount, + fee, + deposit, date, state, - depth: { - quantity: 0, - unit: 'block', - }, + confirmations: 0, epochNumber: 0, slotNumber: 0, description: '', @@ -113,6 +162,7 @@ export const generateTransaction = ( faker.random.alphaNumeric(Math.round(Math.random() * 10) + 100), ], }, + metadata, }); export const generateRandomTransaction = (index: number) => diff --git a/storybook/stories/common/Widgets.stories.js b/storybook/stories/common/Widgets.stories.js index 8d5aa10af8..688bcb0e61 100644 --- a/storybook/stories/common/Widgets.stories.js +++ b/storybook/stories/common/Widgets.stories.js @@ -4,6 +4,7 @@ import { defineMessages, IntlProvider } from 'react-intl'; import { storiesOf } from '@storybook/react'; import { observable, action as mobxAction } from 'mobx'; import { action } from '@storybook/addon-actions'; +import { withKnobs, boolean, number, text } from '@storybook/addon-knobs'; import StoryDecorator from '../_support/StoryDecorator'; import StoryProvider from '../_support/StoryProvider'; import StoryLayout from '../_support/StoryLayout'; @@ -11,6 +12,7 @@ import enMessages from '../../../source/renderer/app/i18n/locales/en-US.json'; import jpMessages from '../../../source/renderer/app/i18n/locales/ja-JP.json'; import BigButtonForDialogs from '../../../source/renderer/app/components/widgets/BigButtonForDialogs'; import MnemonicInputWidget from '../../../source/renderer/app/components/widgets/forms/MnemonicInputWidget'; +import InlineEditingInput from '../../../source/renderer/app/components/widgets/forms/InlineEditingInput'; import createIcon from '../../../source/renderer/app/assets/images/create-ic.inline.svg'; import importIcon from '../../../source/renderer/app/assets/images/import-ic.inline.svg'; import joinSharedIcon from '../../../source/renderer/app/assets/images/join-shared-ic.inline.svg'; @@ -95,8 +97,32 @@ storiesOf('Common|Widgets', module) ); }) + .addDecorator(withKnobs) + // ====== Stories ====== + .add('InlineEditingInput', () => ( +
+
+ value && value.length > 3 && value !== 'error'} + validationErrorMessage={text('validationErrorMessage', 'Error!')} + successfullyUpdated={boolean('successfullyUpdated', true)} + isActive={boolean('isActive', true)} + isSubmitting={boolean('isSubmitting', false)} + inputBlocked={boolean('inputBlocked', false)} + disabled={boolean('disabled', false)} + readOnly={boolean('readOnly', false)} + maxLength={number('maxLength')} + /> +
+
+ )) + .add('BigButtonForDialogs', (props: { locale: string }) => (
diff --git a/storybook/stories/index.js b/storybook/stories/index.js index 4598c7c5fc..1b9b6f8b30 100644 --- a/storybook/stories/index.js +++ b/storybook/stories/index.js @@ -11,6 +11,9 @@ import './nodes'; // Staking import './staking/Staking.stories'; +// Voting +import './voting/Voting.stories'; + // Settings import './settings'; diff --git a/storybook/stories/nodes/status/Diagnostics.stories.js b/storybook/stories/nodes/status/Diagnostics.stories.js index 4f2eaafbc6..9cf35e5778 100644 --- a/storybook/stories/nodes/status/Diagnostics.stories.js +++ b/storybook/stories/nodes/status/Diagnostics.stories.js @@ -67,8 +67,8 @@ storiesOf('Nodes|Status', module) 280719 )} nodeConnectionError={null} - localTip={{ epoch: 123, slot: 13400 }} - networkTip={{ epoch: 123, slot: 13400 }} + localTip={{ epoch: 123, slot: 13400, absoluteSlotNumber: 15000000 }} + networkTip={{ epoch: 123, slot: 13400, absoluteSlotNumber: 15000000 }} localBlockHeight={number('localBlockHeight', 280719)} networkBlockHeight={number('networkBlockHeight', 42539)} isCheckingSystemTime={boolean('isCheckingSystemTime', true)} @@ -109,7 +109,7 @@ storiesOf('Nodes|Status', module) 280719 )} nodeConnectionError={null} - localTip={{ epoch: 123, slot: 13400 }} + localTip={{ epoch: 123, slot: 13400, absoluteSlotNumber: 15000000 }} networkTip={null} localBlockHeight={number('localBlockHeight', 280719)} networkBlockHeight={number('networkBlockHeight', 42539)} diff --git a/storybook/stories/settings/general/General.stories.js b/storybook/stories/settings/general/General.stories.js index 1aa702b7cc..c6fb9e2d14 100644 --- a/storybook/stories/settings/general/General.stories.js +++ b/storybook/stories/settings/general/General.stories.js @@ -16,9 +16,11 @@ import { locales, themesIds } from '../../_support/config'; // Screens import ProfileSettingsForm from '../../../../source/renderer/app/components/widgets/forms/ProfileSettingsForm'; +import StakePoolsSettings from '../../../../source/renderer/app/components/settings/categories/StakePoolsSettings'; import DisplaySettings from '../../../../source/renderer/app/components/settings/categories/DisplaySettings'; import SupportSettings from '../../../../source/renderer/app/components/settings/categories/SupportSettings'; import TermsOfUseSettings from '../../../../source/renderer/app/components/settings/categories/TermsOfUseSettings'; +import WalletsSettings from '../../../../source/renderer/app/components/settings/categories/WalletsSettings'; const getParamName = (obj, itemName): any => Object.entries(obj).find((entry: [any, any]) => itemName === entry[1]); @@ -47,6 +49,30 @@ storiesOf('Settings|General', module) currentTimeFormat={TIME_OPTIONS[0].value} /> )) + .add('Wallets', () => ( + + )) + .add('Stake Pools', () => ( + + )) .add('Themes', () => ( { const menu = ( pageNames[item])} isActiveItem={(item) => { const itemName = context.story diff --git a/storybook/stories/staking/DelegationCenter.stories.js b/storybook/stories/staking/DelegationCenter.stories.js index cda492dc2b..15974fba53 100644 --- a/storybook/stories/staking/DelegationCenter.stories.js +++ b/storybook/stories/staking/DelegationCenter.stories.js @@ -31,6 +31,7 @@ const walletSyncedStateRestoring = { const networkTip: TipInfo = { epoch: 1232, slot: 123, + absoluteSlotNumber: 15000000, }; const nextEpochDate = new Date(); diff --git a/storybook/stories/staking/StakePools.stories.js b/storybook/stories/staking/StakePools.stories.js index e541a5bad1..4f31f55363 100644 --- a/storybook/stories/staking/StakePools.stories.js +++ b/storybook/stories/staking/StakePools.stories.js @@ -1,6 +1,6 @@ // @flow import React from 'react'; -import { number } from '@storybook/addon-knobs'; +import { number, boolean } from '@storybook/addon-knobs'; import { action } from '@storybook/addon-actions'; import StakePools from '../../../source/renderer/app/components/staking/stake-pools/StakePools'; @@ -43,6 +43,7 @@ export const StakePoolsStory = (props: Props) => ( STAKE_POOLS[20], STAKE_POOLS[36], ]} + isFetching={boolean('isFetching', false)} onOpenExternalLink={action('onOpenExternalLink')} currentTheme={props.currentTheme} currentLocale={props.locale} @@ -53,6 +54,8 @@ export const StakePoolsStory = (props: Props) => ( rankStakePools={() => null} wallets={dummyWallets} getStakePoolById={() => null} + onSmashSettingsClick={action('onSmashSettingsClick')} + smashServerUrl="https://smash.cardano-mainnet.iohk.io" maxDelegationFunds={maxDelegationFunds} /> ); diff --git a/storybook/stories/voting/Voting.stories.js b/storybook/stories/voting/Voting.stories.js new file mode 100644 index 0000000000..dde39246a3 --- /dev/null +++ b/storybook/stories/voting/Voting.stories.js @@ -0,0 +1,116 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { withKnobs, boolean, number } from '@storybook/addon-knobs'; +import BigNumber from 'bignumber.js'; +import StoryDecorator from '../_support/StoryDecorator'; +import VotingRegistrationStepsChooseWallet from '../../../source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsChooseWallet'; +import VotingRegistrationStepsRegister from '../../../source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsRegister'; +import VotingRegistrationStepsConfirm from '../../../source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsConfirm'; +import VotingRegistrationStepsEnterPinCode from '../../../source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsEnterPinCode'; +import VotingRegistrationStepsQrCode from '../../../source/renderer/app/components/voting/voting-registration-wizard-steps/VotingRegistrationStepsQrCode'; +import VotingInfo from '../../../source/renderer/app/components/voting/VotingInfo'; + +import { + VOTING_REGISTRATION_MIN_TRANSACTION_CONFIRMATIONS, + VOTING_REGISTRATION_MIN_WALLET_FUNDS, +} from '../../../source/renderer/app/config/votingConfig'; +import { generateWallet } from '../_support/utils'; + +const WALLETS = [ + generateWallet('Wallet 1', '100000000000', 0), + generateWallet('Wallet 2', '100', 0), +]; + +const stepsList = ['Wallet', 'Sign', 'Confirm', 'PIN code', 'QR code']; + +storiesOf('Voting|Voting Registration Wizard', module) + .addDecorator((story) => {story()}) + .addDecorator(withKnobs) + + // ====== Stories ====== + + .add('Voting Registration - Step 1', () => ( + + )) + + .add('Voting Registration - Step 2', () => ( + + )) + + .add('Voting Registration - Step 3', () => ( + + )) + + .add('Voting Registration - Step 4', () => ( + + )) + + .add('Voting Registration - Step 5', () => ( + + )); + +storiesOf('Voting|Voting Info', module) + .addDecorator((story) => {story()}) + .addDecorator(withKnobs) + + // ====== Stories ====== + + .add('Voting Info', () => ( + + )); diff --git a/storybook/stories/wallets/_utils/WalletsTransactionsWrapper.js b/storybook/stories/wallets/_utils/WalletsTransactionsWrapper.js index 2dbe297f77..3a7798b257 100644 --- a/storybook/stories/wallets/_utils/WalletsTransactionsWrapper.js +++ b/storybook/stories/wallets/_utils/WalletsTransactionsWrapper.js @@ -117,6 +117,8 @@ export default class WalletsTransactionsWrapper extends Component< TransactionTypes.INCOME, new Date(), new BigNumber(1), + new BigNumber(1), + new BigNumber(1), TransactionStates.OK, false, true @@ -125,6 +127,8 @@ export default class WalletsTransactionsWrapper extends Component< TransactionTypes.INCOME, new Date(), new BigNumber(1), + new BigNumber(1), + new BigNumber(1), TransactionStates.OK, false, true @@ -135,6 +139,8 @@ export default class WalletsTransactionsWrapper extends Component< TransactionTypes.INCOME, new Date(), new BigNumber(1), + new BigNumber(1), + new BigNumber(1), TransactionStates.OK, false, false, @@ -144,6 +150,8 @@ export default class WalletsTransactionsWrapper extends Component< TransactionTypes.INCOME, new Date(), new BigNumber(1), + new BigNumber(1), + new BigNumber(1), TransactionStates.OK, false, false, diff --git a/storybook/stories/wallets/_utils/currencies.json b/storybook/stories/wallets/_utils/currencies.json new file mode 100644 index 0000000000..b13e5f8623 --- /dev/null +++ b/storybook/stories/wallets/_utils/currencies.json @@ -0,0 +1,97 @@ +[ + { + "id": "bitcoin", + "symbol": "btc", + "name": "Bitcoin" + }, + { + "id": "ethereum", + "symbol": "eth", + "name": "Ethereum" + }, + { + "id": "litecoin", + "symbol": "ltc", + "name": "Litecoin" + }, + { + "id": "bitcoin-cash", + "symbol": "bch", + "name": "Bitcoin Cash" + }, + { + "id": "binancecoin", + "symbol": "bnb", + "name": "Binance Coin" + }, + { + "id": "eos", + "symbol": "eos", + "name": "EOS" + }, + { + "id": "ripple", + "symbol": "xrp", + "name": "XRP" + }, + { + "id": "stellar", + "symbol": "xlm", + "name": "Stellar" + }, + { + "id": "chainlink", + "symbol": "link", + "name": "Chainlink" + }, + { + "id": "polkadot", + "symbol": "dot", + "name": "Polkadot" + }, + { + "id": "yearn-finance", + "symbol": "yfi", + "name": "yearn.finance" + }, + { + "id": "uniswap-state-dollar", + "symbol": "usd", + "name": "unified Stable Dollar" + }, + { + "id": "block-duelers", + "symbol": "bdt", + "name": "Block Duelers" + }, + { + "id": "bitcoin-hd", + "symbol": "bhd", + "name": "Bitcoin HD" + }, + { + "id": "good-boy-points", + "symbol": "gbp", + "name": "Good Boy Points" + }, + { + "id": "lkr-coin", + "symbol": "lkr", + "name": "LKR Coin" + }, + { + "id": "trias", + "symbol": "try", + "name": "Trias" + }, + { + "id": "xrpalike-gene", + "symbol": "xag", + "name": "Xrpalike Gene" + }, + { + "id": "bitcoinus", + "symbol": "bits", + "name": "Bitcoinus" + } +] diff --git a/storybook/stories/wallets/index.js b/storybook/stories/wallets/index.js index 27347e811b..c92a98a28d 100644 --- a/storybook/stories/wallets/index.js +++ b/storybook/stories/wallets/index.js @@ -5,6 +5,7 @@ import './summary/WalletSummary.stories'; import './send/WalletSend.stories'; import './receive/WalletReceive.stories'; import './transactions/WalletTransactions.stories'; +import './transactions/TransactionMetadata.stories'; import './settings/WalletSettings.stories'; import './addWallet/AddWallet.stories'; import './import/WalletImportFile.stories'; diff --git a/storybook/stories/wallets/settings/WalletSettingsScreen.stories.js b/storybook/stories/wallets/settings/WalletSettingsScreen.stories.js index 1e385deb8d..67ee42051c 100644 --- a/storybook/stories/wallets/settings/WalletSettingsScreen.stories.js +++ b/storybook/stories/wallets/settings/WalletSettingsScreen.stories.js @@ -140,7 +140,7 @@ export default (props: { currentTheme: string, locale: Locale }) => { isSubmitting={false} lastUpdatedField={null} nameValidator={() => true} - onCancelEditing={() => {}} + onCancel={() => {}} onFieldValueChange={() => {}} onStartEditing={() => {}} onStopEditing={() => {}} diff --git a/storybook/stories/wallets/summary/WalletSummary.stories.js b/storybook/stories/wallets/summary/WalletSummary.stories.js index 19d0788938..90d6818652 100644 --- a/storybook/stories/wallets/summary/WalletSummary.stories.js +++ b/storybook/stories/wallets/summary/WalletSummary.stories.js @@ -1,11 +1,13 @@ // @flow import React from 'react'; import { storiesOf } from '@storybook/react'; -import { boolean, number } from '@storybook/addon-knobs'; +import { boolean, number, select } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; // Assets and helpers import { generateWallet } from '../../_support/utils'; import WalletsWrapper from '../_utils/WalletsWrapper'; +import currencyList from '../_utils/currencies.json'; // Screens import WalletSummary from '../../../../source/renderer/app/components/wallet/summary/WalletSummary'; @@ -13,12 +15,60 @@ import WalletSummary from '../../../../source/renderer/app/components/wallet/sum /* eslint-disable consistent-return */ storiesOf('Wallets|Summary', module) .addDecorator(WalletsWrapper) - .add('Wallet Summary', () => ( - - )); + .add('Wallet Summary', () => { + const currencyState = select( + 'Currency state', + { + Fetched: 'fetched', + 'Fetching rate': 'loading', + 'Disabled or unavailable': 'off', + }, + 'fetched' + ); + + let currencyIsFetchingRate = false; + let currencyIsAvailable = true; + let currencyIsActive = true; + let currencyLastFetched = new Date(); + + if (currencyState === 'loading') { + currencyIsFetchingRate = true; + currencyLastFetched = null; + } else if (currencyState === 'off') { + currencyIsAvailable = false; + currencyIsActive = false; + } + + const currencySelected = select( + 'currencySelected', + currencyList.reduce((obj, currency) => { + obj[`${currency.id} - ${currency.name}`] = currency; + return obj; + }, {}), + { + id: 'uniswap-state-dollar', + symbol: 'usd', + name: 'unified Stable Dollar', + } + ); + + return ( + + ); + }); diff --git a/storybook/stories/wallets/transactions/TransactionMetadata.stories.js b/storybook/stories/wallets/transactions/TransactionMetadata.stories.js new file mode 100644 index 0000000000..f84dca2c6d --- /dev/null +++ b/storybook/stories/wallets/transactions/TransactionMetadata.stories.js @@ -0,0 +1,10 @@ +// @flow +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { TransactionMetadataView } from '../../../../source/renderer/app/components/wallet/transactions/metadata/TransactionMetadataView'; +import { EXAMPLE_METADATA } from '../../_support/utils'; + +storiesOf('Wallets|Transactions', module) + // ====== Stories ====== + + .add('Metadata', () => ); diff --git a/storybook/stories/wallets/transactions/TransactionsList.stories.js b/storybook/stories/wallets/transactions/TransactionsList.stories.js index e81b500d04..db4416ff6f 100644 --- a/storybook/stories/wallets/transactions/TransactionsList.stories.js +++ b/storybook/stories/wallets/transactions/TransactionsList.stories.js @@ -3,7 +3,6 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { withKnobs, select } from '@storybook/addon-knobs'; - // Assets and helpers import { generateWallet } from '../../_support/utils'; import { formattedWalletAmount } from '../../../../source/renderer/app/utils/formatters'; @@ -17,7 +16,6 @@ import { } from '../../../../source/renderer/app/config/profileConfig'; import { WalletTransaction } from '../../../../source/renderer/app/domains/WalletTransaction'; import type { TransactionFilterOptionsType } from '../../../../source/renderer/app/stores/TransactionsStore'; - // Screens import WalletTransactions from '../../../../source/renderer/app/components/wallet/transactions/WalletTransactions'; diff --git a/tests/transactions/e2e/steps/transactions.js b/tests/transactions/e2e/steps/transactions.js index 944b5f6f96..dedb408c12 100644 --- a/tests/transactions/e2e/steps/transactions.js +++ b/tests/transactions/e2e/steps/transactions.js @@ -123,7 +123,7 @@ When(/^the transaction fees are calculated$/, async function() { '.AmountInputSkin_fees' ); const transactionFeeAmount = new BigNumber(transactionFeeText.substr(2, 8)); - return transactionFeeAmount.greaterThan(0) ? transactionFeeAmount : false; + return transactionFeeAmount.isGreaterThan(0) ? transactionFeeAmount : false; }); }); @@ -234,7 +234,7 @@ Then(/^the latest transaction should show:$/, async function(table) { // NOTE: we use "add()" as this is outgoing transaction and amount is a negative value! const transactionAmount = new BigNumber(transactionAmounts[0]); const transactionAmountWithoutFees = transactionAmount - .add(this.fees) + .plus(this.fees) .toFormat(DECIMAL_PLACES_IN_ADA); expect(expectedData.amountWithoutFees).to.equal(transactionAmountWithoutFees); }); diff --git a/tests/wallets/e2e/steps/transfer-funds-wizard.js b/tests/wallets/e2e/steps/transfer-funds-wizard.js index a20ac37114..003b7dae29 100644 --- a/tests/wallets/e2e/steps/transfer-funds-wizard.js +++ b/tests/wallets/e2e/steps/transfer-funds-wizard.js @@ -95,7 +95,7 @@ Then(/^I should see increased rewards wallet balance and 0 ADA in Daedalus Balan async function() { const rewardsSelector = '.SidebarWalletsMenu_wallets button:nth-child(1) .SidebarWalletMenuItem_info'; const balanceSelector = '.SidebarWalletsMenu_wallets button:nth-child(2) .SidebarWalletMenuItem_info'; - const transferSumWithoutFees = this.rewardsWalletAmount.add(this.balanceWalletAmount); + const transferSumWithoutFees = this.rewardsWalletAmount.plus(this.balanceWalletAmount); const transferSumWithFees = transferSumWithoutFees.minus(this.transferFee); const initialRewardsFormattedAmount = formattedWalletAmount(this.rewardsWalletAmount, true, false); const initialBallanceFormattedAmount = formattedWalletAmount(this.balanceWalletAmount, true, false); diff --git a/utils/cardano/native-tokens/registry.json b/utils/cardano/native-tokens/registry.json new file mode 100644 index 0000000000..4fda046e1c --- /dev/null +++ b/utils/cardano/native-tokens/registry.json @@ -0,0 +1,13 @@ +{ + "subjects": [ + { + "subject": "789ef8ae89617f34c07f7f6a12e4d65146f958c0bc15a97b4ff169f1", + "name": { "value": "NiceCoin" }, + "description": { "value": "A nice coin" }, + "acronym": { "value": "NCN" }, + "unit": { "value": { "decimals": 9, "name": "GigaNice" } }, + "url": { "value": "https://iohk.io" }, + "logo": { "value": "QWxtb3N0IGEgbG9nbw==" } + } + ] +} diff --git a/yarn.lock b/yarn.lock index 1c16f6723e..7574dbe07a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1150,6 +1150,10 @@ version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" +"@iohk-jormungandr/wallet-js@0.5.0-pre7": + version "0.5.0-pre7" + resolved "https://registry.yarnpkg.com/@iohk-jormungandr/wallet-js/-/wallet-js-0.5.0-pre7.tgz#a004a6c9d19c28f902076f538c40aa67fd819a30" + "@ledgerhq/devices@^5.26.0": version "5.26.0" resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-5.26.0.tgz#6c25ee48d0d2f49a8fa1abc11f3efd888f3fea68" @@ -3326,11 +3330,7 @@ bigi@^1.1.0, bigi@^1.4.0, bigi@^1.4.1: version "1.4.2" resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825" -bignumber.js@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-5.0.0.tgz#fbce63f09776b3000a83185badcde525daf34833" - -bignumber.js@^9.0.0: +bignumber.js@9.0.1, bignumber.js@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" @@ -3921,9 +3921,9 @@ cardano-js@0.4.5: js-chain-libs-node "^0.3.0" ts-custom-error "^3.1.1" -cardano-launcher@0.20201014.0: - version "0.20201014.0" - resolved "https://registry.yarnpkg.com/cardano-launcher/-/cardano-launcher-0.20201014.0.tgz#97c2de764303f833e9acc37a30f9ef22f5459346" +cardano-launcher@0.20210215.0: + version "0.20210215.0" + resolved "https://registry.yarnpkg.com/cardano-launcher/-/cardano-launcher-0.20210215.0.tgz#c6dc8a602c69ff002433eddb9659d988eb2db5d0" dependencies: get-port "5.1.1" lodash "4.17.20" @@ -9115,6 +9115,12 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +json-bigint@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + dependencies: + bignumber.js "^9.0.0" + json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -12554,9 +12560,9 @@ react-modal@3.1.12: prop-types "^15.5.10" warning "^3.0.0" -react-polymorph@0.9.7-rc.11: - version "0.9.7-rc.11" - resolved "https://registry.yarnpkg.com/react-polymorph/-/react-polymorph-0.9.7-rc.11.tgz#e297dd5cbe0dc359dff10e881c5558c2080b7d38" +react-polymorph@0.9.7-rc.17: + version "0.9.7-rc.17" + resolved "https://registry.yarnpkg.com/react-polymorph/-/react-polymorph-0.9.7-rc.17.tgz#febe8d146e110118afa9ee0cee128af04a86397b" dependencies: "@tippyjs/react" "4.2.0" create-react-context "0.2.2" diff --git a/yarn2nix.nix b/yarn2nix.nix index 7e3b51e86f..9a9ad6fc78 100644 --- a/yarn2nix.nix +++ b/yarn2nix.nix @@ -97,7 +97,7 @@ yarn2nix.mkYarnPackage { # the webpack utils embed the original source paths into map files, so backtraces from the 1 massive index.js can be converted back to multiple files # but that causes the derivation to depend on the original inputs at the nix layer, and double the size of the linux installs # nuke-refs will just replace all storepaths with an invalid one - for x in {main,renderer}/index.js{,.map} main/preload.js{,.map} main/0.js{,.map} renderer/styles.css.map; do + for x in {main,renderer}/{0.,}index.js{,.map} main/preload.js{,.map} main/0.js{,.map} renderer/styles.css.map; do nuke-refs $x done ''; @@ -121,7 +121,7 @@ yarn2nix.mkYarnPackage { rm -rf $out/resources/app/{installers,launcher-config.yaml,gulpfile.js,home} mkdir -pv $out/resources/app/node_modules - cp -rv $node_modules/{\@babel,regenerator-runtime,node-fetch,\@trezor,runtypes,parse-uri,randombytes,safe-buffer,bip66,pushdata-bitcoin,bitcoin-ops,typeforce,varuint-bitcoin,bigi,create-hash,merkle-lib,blake2b,nanoassert,blake2b-wasm,bs58check,bs58,base-x,create-hmac,ecurve,wif,ms,keccak,trezor-link,semver-compare,protobufjs-old-fixed-webpack,bytebuffer-old-fixed-webpack,long,object.values,define-properties,object-keys,has,function-bind,es-abstract,has-symbols,json-stable-stringify,tiny-worker,hd-wallet,cashaddrjs,big-integer,queue,inherits,bchaddrjs,cross-fetch,trezor-connect,js-chain-libs-node} $out/resources/app/node_modules + cp -rv $node_modules/{\@babel,regenerator-runtime,node-fetch,\@trezor,runtypes,parse-uri,randombytes,safe-buffer,bip66,pushdata-bitcoin,bitcoin-ops,typeforce,varuint-bitcoin,bigi,create-hash,merkle-lib,blake2b,nanoassert,blake2b-wasm,bs58check,bs58,base-x,create-hmac,ecurve,wif,ms,keccak,trezor-link,semver-compare,protobufjs-old-fixed-webpack,bytebuffer-old-fixed-webpack,long,object.values,define-properties,object-keys,has,function-bind,es-abstract,has-symbols,json-stable-stringify,tiny-worker,hd-wallet,cashaddrjs,big-integer,queue,inherits,bchaddrjs,cross-fetch,trezor-connect,js-chain-libs-node,bignumber.js} $out/resources/app/node_modules cd $out/resources/app/ unzip ${./nix/windows-usb-libs.zip} @@ -164,7 +164,7 @@ yarn2nix.mkYarnPackage { mkdir -p $out/share/fonts ln -sv $out/share/daedalus/renderer/assets $out/share/fonts/daedalus mkdir -pv $out/share/daedalus/node_modules - cp -rv $node_modules/{\@babel,regenerator-runtime,node-fetch,\@trezor,runtypes,parse-uri,randombytes,safe-buffer,bip66,pushdata-bitcoin,bitcoin-ops,typeforce,varuint-bitcoin,bigi,create-hash,merkle-lib,blake2b,nanoassert,blake2b-wasm,bs58check,bs58,base-x,create-hmac,ecurve,wif,ms,keccak,trezor-link,semver-compare,protobufjs-old-fixed-webpack,bytebuffer-old-fixed-webpack,long,object.values,define-properties,object-keys,has,function-bind,es-abstract,has-symbols,json-stable-stringify,tiny-worker,hd-wallet,cashaddrjs,big-integer,queue,inherits,bchaddrjs,cross-fetch,trezor-connect,js-chain-libs-node} $out/share/daedalus/node_modules/ + cp -rv $node_modules/{\@babel,regenerator-runtime,node-fetch,\@trezor,runtypes,parse-uri,randombytes,safe-buffer,bip66,pushdata-bitcoin,bitcoin-ops,typeforce,varuint-bitcoin,bigi,create-hash,merkle-lib,blake2b,nanoassert,blake2b-wasm,bs58check,bs58,base-x,create-hmac,ecurve,wif,ms,keccak,trezor-link,semver-compare,protobufjs-old-fixed-webpack,bytebuffer-old-fixed-webpack,long,object.values,define-properties,object-keys,has,function-bind,es-abstract,has-symbols,json-stable-stringify,tiny-worker,hd-wallet,cashaddrjs,big-integer,queue,inherits,bchaddrjs,cross-fetch,trezor-connect,js-chain-libs-node,bignumber.js} $out/share/daedalus/node_modules/ find $out $NIX_BUILD_TOP -name '*.node' mkdir -pv $out/share/daedalus/build