diff --git a/.buildkite/hooks/pre-command b/.buildkite/hooks/pre-command new file mode 100644 index 0000000000..fdcca27174 --- /dev/null +++ b/.buildkite/hooks/pre-command @@ -0,0 +1,2 @@ +NIX_PATH="nixpkgs=$(nix-build fetch-nixpkgs.nix -o nixpkgs)" +export NIX_PATH diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 4a8e8ec66d..52cd06691b 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -2,16 +2,16 @@ env: ARTIFACT_BUCKET: s3://ci-output-sink steps: - label: 'daedalus-x86_64-darwin' - command: 'scripts/nix-shell.sh --run "scripts/build-installer-unix.sh --build-id $BUILDKITE_BUILD_NUMBER"' + command: 'scripts/build-installer-unix.sh --build-id $BUILDKITE_BUILD_NUMBER' env: NIX_SSL_CERT_FILE: /nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt agents: system: x86_64-darwin - label: 'daedalus-x86_64-linux' - command: 'scripts/nix-shell.sh --run "scripts/build-installer-unix.sh --build-id $BUILDKITE_BUILD_NUMBER"' + command: 'scripts/build-installer-unix.sh --build-id $BUILDKITE_BUILD_NUMBER' agents: system: x86_64-linux - label: 'daedalus-x86_64-linux-nix' - command: 'scripts/nix-shell.sh --run "scripts/build-installer-nix.sh $BUILDKITE_BUILD_NUMBER"' + command: 'scripts/build-installer-nix.sh $BUILDKITE_BUILD_NUMBER' agents: system: x86_64-linux diff --git a/.eslintrc b/.eslintrc index 3b86f7de67..2e6e754735 100755 --- a/.eslintrc +++ b/.eslintrc @@ -77,6 +77,7 @@ "API_VERSION": true, "NETWORK": true, "MOBX_DEV_TOOLS": true, - "BUILD_NUMBER": true + "BUILD_NUMBER": true, + "Process": true // TODO: remove after fix } } diff --git a/.flowconfig b/.flowconfig index 703b15c187..d05c078fa9 100755 --- a/.flowconfig +++ b/.flowconfig @@ -3,6 +3,7 @@ /node_modules/electron-packager/.* /node_modules/npm/.* /node_modules/oboe/.* +/node_modules/jsonlint/.* /dist/.* /release/.* /git/.* diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4b6e36293b..d7744936c2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,16 +19,16 @@ This PR CHANGES. - [ ] PR is updated to the most recent version of target branch (and there are no conflicts) - [ ] PR has good description that summarizes all changes and shows some screenshots or animated GIFs of important UI changes - [ ] CHANGELOG entry has been added and is linked to the correct PR on GitHub -- [ ] Automated tests: All acceptance tests are passing (`npm run test`) -- [ ] Manual tests (minimum tests should cover newly added feature/fix): App works correctly in *development* build (`npm run dev`) -- [ ] Manual tests (minimum tests should cover newly added feature/fix): App works correctly in *production* build (`npm run package` / CI builds) -- [ ] There are no *flow* errors or warnings (`npm run flow:test`) -- [ ] There are no *lint* errors or warnings (`npm run lint`) +- [ ] Automated tests: All acceptance tests are passing (`yarn run test`) +- [ ] Manual tests (minimum tests should cover newly added feature/fix): App works correctly in *development* build (`yarn run dev`) +- [ ] Manual tests (minimum tests should cover newly added feature/fix): App works correctly in *production* build (`yarn run package` / CI builds) +- [ ] There are no *flow* errors or warnings (`yarn run flow:test`) +- [ ] There are no *lint* errors or warnings (`yarn run lint`) - [ ] Text changes are proofread and approved (Jane Wild) -- [ ] There are no missing translations (running `npm run manage:translations` produces no changes) +- [ ] There are no missing translations (running `yarn run manage:translations` produces no changes) - [ ] UI changes look good in all themes (Alexander Rukin) -- [ ] Storybook works and no stories are broken (`npm run storybook`) -- [ ] In case of npm dependency changes both `package-lock.json` and `yarn.lock` files are updated +- [ ] Storybook works and no stories are broken (`yarn run storybook`) +- [ ] In case of dependency changes `yarn.lock` file is updated ### Code Quality - [ ] Important parts of the code are properly documented and commented diff --git a/.gitignore b/.gitignore index ac7daa21ce..7ede4dae70 100755 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ Logs # Runtime data pids +icon.png *.exe *.pid *.seed @@ -44,6 +45,10 @@ installers/tls/server installers/DLLs installers/libressl +# temporary certs for daedalus dev +tls/client +tls/server + # App packaged dist release @@ -59,8 +64,9 @@ release translations/messages translations/reports -# Paper wallet ceritifcate PDF file generated by acceptance tests +# 'Screenshots' and 'Paper wallet ceritifcate PDF file' generated by acceptance tests features/support/paper_wallet_certificates/paper-wallet-certificate.pdf +features/screenshots # Webpack .cache @@ -69,9 +75,20 @@ features/support/paper_wallet_certificates/paper-wallet-certificate.pdf build-id ci-url commit-id +mainnet +staging +cardano-node +configuration.yaml +log-config-prod.yaml +mainnet-genesis-dryrun-with-stakeholders.json +wallet-topology.yaml # nix-build results result* # Npm package-lock.json + +# cardano public keys +public.key +public.key.lock diff --git a/CHANGELOG.md b/CHANGELOG.md index 827399fc68..f5df8d5289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,70 @@ + Changelog ========= +## 0.12.0 +======= + +### Features + +- Implemented "Forbidden mnemonic" error message ([PR 1093](https://github.com/input-output-hk/daedalus/pull/1093), [PR 1100](https://github.com/input-output-hk/daedalus/pull/1100)) +- Implemented extended error messages for transaction fee calculation failures ([PR 1111](https://github.com/input-output-hk/daedalus/pull/1111)) +- Implemented forms submission on "Enter" key press ([PR 981](https://github.com/input-output-hk/daedalus/pull/981)) +- Implemented Japanese "Terms of use" for the "Testnet" network ([PR 1097](https://github.com/input-output-hk/daedalus/pull/1097)) +- Implemented lock-file mechanism which prevents multiple Daedalus instances from running against the same state directory and network ([PR 1113](https://github.com/input-output-hk/daedalus/pull/1113), [PR 1114](https://github.com/input-output-hk/daedalus/pull/1114), [PR 1121](https://github.com/input-output-hk/daedalus/pull/1121), [PR 1166](https://github.com/input-output-hk/daedalus/pull/1166)) +- Implemented "New data layer migration" screen ([PR 1096](https://github.com/input-output-hk/daedalus/pull/1096)) +- Implemented sending of `cert` and `key` with API requests in order to enable 2-way TLS authentication ([PR 1072](https://github.com/input-output-hk/daedalus/pull/1072)) +- Implemented support for Cardano node "structured logging" ([PR 1092](https://github.com/input-output-hk/daedalus/pull/1092), [PR 1122](https://github.com/input-output-hk/daedalus/pull/1122)) +- Implemented the IPC driven Cardano node / Daedalus communication ([PR 1075](https://github.com/input-output-hk/daedalus/pull/1075), [PR 1107](https://github.com/input-output-hk/daedalus/pull/1107), [PR 1109](https://github.com/input-output-hk/daedalus/pull/1109), [PR 1115](https://github.com/input-output-hk/daedalus/pull/1115), [PR 1118](https://github.com/input-output-hk/daedalus/pull/1118), [PR 1119](https://github.com/input-output-hk/daedalus/pull/1119), [PR 1162](https://github.com/input-output-hk/daedalus/pull/1162)) +- Improved the loading UX ([PR 723](https://github.com/input-output-hk/daedalus/pull/723)) +- Improved the NTP check handling ([PR 1086](https://github.com/input-output-hk/daedalus/pull/1086), [PR 1149](https://github.com/input-output-hk/daedalus/pull/1149), [PR 1158](https://github.com/input-output-hk/daedalus/pull/1158), [PR 1194](https://github.com/input-output-hk/daedalus/pull/1194), [PR 1213](https://github.com/input-output-hk/daedalus/pull/1213)) +- Improved the transaction details text selection ([PR 1073](https://github.com/input-output-hk/daedalus/pull/1073), [PR 1095](https://github.com/input-output-hk/daedalus/pull/1095)) +- Integrated Cardano V1 API endpoints ([PR 1018](https://github.com/input-output-hk/daedalus/pull/1018), [PR 1031](https://github.com/input-output-hk/daedalus/pull/1031), [PR 1037](https://github.com/input-output-hk/daedalus/pull/1037), [PR 1042](https://github.com/input-output-hk/daedalus/pull/1042), [PR 1045](https://github.com/input-output-hk/daedalus/pull/1045), [PR 1070](https://github.com/input-output-hk/daedalus/pull/1070), [PR 1078](https://github.com/input-output-hk/daedalus/pull/1078), [PR 1079](https://github.com/input-output-hk/daedalus/pull/1079), [PR 1080](https://github.com/input-output-hk/daedalus/pull/1080), [PR 1088](https://github.com/input-output-hk/daedalus/pull/1088), [PR 1220](https://github.com/input-output-hk/daedalus/pull/1220)) +- Refactored and improved `NetworkStatus` store to use V1 API data ([PR 1081](https://github.com/input-output-hk/daedalus/pull/1081)) + +### Fixes + +- Added support for non-Latin characters in spending password ([PR 1197](https://github.com/input-output-hk/daedalus/pull/1197)) +- Fixed an issue which allowed to submit invalid form on the "Send" screen using an "enter" key ([PR 1002](https://github.com/input-output-hk/daedalus/pull/1002)) +- Fixed an issue which caused the send bug report form to hang and not send the support request if the logs were downloaded before attempting to send the request ([PR 1176](https://github.com/input-output-hk/daedalus/pull/1176)) +- Fixed an issue which would show a runtime JavaScript error in case Daedalus is not started using Launcher ([PR 1169](https://github.com/input-output-hk/daedalus/pull/1169)) +- Fixed an issue which would trigger submission of the "Generate address" button on the "Receive" screen on right-mouse click ([PR 1082](https://github.com/input-output-hk/daedalus/pull/1082)) +- Fixed an issue with the "Having Trouble Connecting" notification not showing up on the "Connection lost. Reconnecting..." screen ([PR 1112](https://github.com/input-output-hk/daedalus/pull/1112)) +- Fixed broken "Export wallet to file" dialog and improved "Wallet settings" screen dialogs file structure and namings ([PR 998](https://github.com/input-output-hk/daedalus/pull/998)) +- Fixed special buttons outline color ([PR 990](https://github.com/input-output-hk/daedalus/pull/990)) +- Fixed the close button's hover state within the AntivirusRestaurationSlowdownNotification so it's full height ([PR 1131](https://github.com/input-output-hk/daedalus/pull/1131)) +- Fixed uninstaller unicode issue ([PR 1116](https://github.com/input-output-hk/daedalus/pull/1116)) +- Fixed window icon quality issue on Linux ([PR 1170](https://github.com/input-output-hk/daedalus/pull/1170)) +- Implement design review fixes and improvements ([PR 1090](https://github.com/input-output-hk/daedalus/pull/1090)) +- Pinned eslint-scope version via the yarn resolutions feature ([PR 1017](https://github.com/input-output-hk/daedalus/pull/1017)) +- Prevented wallet data polling during wallet deletion ([PR 996](https://github.com/input-output-hk/daedalus/pull/996)) +- Removed transaction status and number of confirmation during wallet restoration ([PR 1189](https://github.com/input-output-hk/daedalus/pull/1189)) + +### Chores + +- Added acceptance tests for node update notification with apply and postpone update scenarios ([PR 977](https://github.com/input-output-hk/daedalus/pull/977)) +- Added acceptance tests for maximum wallets limit ([PR 979](https://github.com/input-output-hk/daedalus/pull/979)) +- Added acceptance tests for the "About" dialog ([PR 975](https://github.com/input-output-hk/daedalus/pull/975)) +- Added acceptance tests for wallets, transactions and addresses ordering ([PR 976](https://github.com/input-output-hk/daedalus/pull/976)) +- Added Daedalus and Cardano version to Daedalus log ([PR 1094](https://github.com/input-output-hk/daedalus/pull/1094), [PR 1137](https://github.com/input-output-hk/daedalus/pull/1137)) +- Added dynamic prefix derivation to local storage keys used to store previous Cardano node PID and added dynamic derivation of Cardano node process names based on the current platform ([PR 1109](https://github.com/input-output-hk/daedalus/pull/1109)) +- Added logging of "GPU-crashed" events ([PR 1083](https://github.com/input-output-hk/daedalus/pull/1083)) +- Added `NETWORK` info to the application title bar ([PR 1174](https://github.com/input-output-hk/daedalus/pull/1174)) +- Added screenshot recording of failed acceptance tests ([PR 1103](https://github.com/input-output-hk/daedalus/pull/1103)) +- Added Storybook stories for the wallet screens ([942](https://github.com/input-output-hk/daedalus/pull/942)) +- Disabled logging to `cardano-node.log` since it was not needed for the support and it was impacting performance ([PR 1027](https://github.com/input-output-hk/daedalus/pull/1027)) +- Enabled Cardano node EKG for non-mainnet networks and made it accessible from the "Network status" screen ([PR 1188](https://github.com/input-output-hk/daedalus/pull/1188)) +- Fixated all `npm` dependencies and update script names ([PR 1014](https://github.com/input-output-hk/daedalus/pull/1014)) +- Fixed broken storybook ([PR 1041](https://github.com/input-output-hk/daedalus/pull/1041)) +- Improved compress/download logs handling ([PR 995](https://github.com/input-output-hk/daedalus/pull/995)) +- Integrated latest React-Polymorph features: render props architecture, theme composition, and a ThemeProvider HOC ([PR 950](https://github.com/input-output-hk/daedalus/pull/950)) +- Integrated latest React-Polymorph with a fix for `NumericInput` component carrot positioning issues ([PR 1172](https://github.com/input-output-hk/daedalus/pull/1172)) +- Made `nodelLogPath` entry in `launcher-config.yaml` optional ([PR 1027](https://github.com/input-output-hk/daedalus/pull/1027)) +- Made `port` and `ca` of Ada Api configurable during runtime ([PR 1067](https://github.com/input-output-hk/daedalus/pull/1067)) +- Removed all ETC specific files ([PR 1068](https://github.com/input-output-hk/daedalus/pull/1068), [PR 1108](https://github.com/input-output-hk/daedalus/pull/1108)) +- Removed Wallet `export` and `import` features for the "Testnet" network ([PR 1168](https://github.com/input-output-hk/daedalus/pull/1168)) +- Switched from `npm` to `yarn` ([PR 989](https://github.com/input-output-hk/daedalus/pull/989)) + ## 0.11.2 ======= @@ -18,27 +82,27 @@ Changelog ### Fixes +- Changed the information we are sending on support requests to the reporting server ([PR 1036](https://github.com/input-output-hk/daedalus/pull/1036)) +- Fixed an issue on Windows where Daedalus couldn't start if the Windows username contained non-ASCII characters ([PR 1057](https://github.com/input-output-hk/daedalus/pull/1057)) +- Fixed a issue with Electron which results in blank/white screen rendering on some OS/Graphics-card/Drivers combinations ([PR 1007](https://github.com/input-output-hk/daedalus/pull/1007)) +- Fixed Daedalus icon scaling issues on Windows ([PR 1064](https://github.com/input-output-hk/daedalus/pull/1064)) - Implemented error dialog shown in case Daedalus is not started using Launcher ([PR 1054](https://github.com/input-output-hk/daedalus/pull/1054)) - Improved paper wallet certificate QR code compatibility ([PR 999](https://github.com/input-output-hk/daedalus/pull/999)) -- Fixed a bug in the Electron which results in blank/white screen rendering on some OS/Graphics-card/Drivers combinations ([PR 1007](https://github.com/input-output-hk/daedalus/pull/1007)) -- Changed the information we are sending on support requests to the reporting server. ([PR 1036](https://github.com/input-output-hk/daedalus/pull/1036)) -- Fixed the bug on Windows where Daedalus couldn't start if the Windows username contained non-ASCII characters. ([PR 1057](https://github.com/input-output-hk/daedalus/pull/1057)) -- Fixed Daedalus icon scaling issues on Windows ([PR 1064](https://github.com/input-output-hk/daedalus/pull/1064)) - Updated to `electon@1.7.16` to avoid the known vulnarability [CVE-2018-15685](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-15685) ([PR 1066](https://github.com/input-output-hk/daedalus/pull/1066)) ### Chores -- Implemented `NETWORK` specific Cardano Blockchain Explorer links (Mainnet, Testnet, Staging) [PR 1051](https://github.com/input-output-hk/daedalus/pull/1051) +- Implemented `NETWORK` specific Cardano Blockchain Explorer links (Mainnet, Testnet, Staging) ([PR 1051](https://github.com/input-output-hk/daedalus/pull/1051)) - Improved network label in the Top bar ([PR 988](https://github.com/input-output-hk/daedalus/pull/988)) -- Added "testnet" label to paper wallet certificates which are not generated on the mainnet ([PR 1055](https://github.com/input-output-hk/daedalus/pull/1055)) +- Added "Testnet" label to paper wallet certificates which are not generated on the mainnet ([PR 1055](https://github.com/input-output-hk/daedalus/pull/1055)) ## 0.11.0 +======= ### Features - Implemented a switch instead of a link for "hide used" addresses on the Receive screen ([PR 935](https://github.com/input-output-hk/daedalus/pull/935)) -- Added a notification for Windows users that using antivirus software might slow down wallet - restoration ([PR 1020](https://github.com/input-output-hk/daedalus/pull/1020)) +- Added a notification for Windows users that using antivirus software might slow down wallet restoration ([PR 1020](https://github.com/input-output-hk/daedalus/pull/1020)) ### Fixes @@ -81,7 +145,7 @@ Changelog - Fixed reporting server URL used for submitting support requests for the Linux build of Daedalus ([PR 959](https://github.com/input-output-hk/daedalus/pull/959)) - Fixed missing launcher log file ([PR 963](https://github.com/input-output-hk/daedalus/pull/963)) - Fixed issue with multiple cardano-node processes running and windows allowing skipping of files in installer ([PR 953](https://github.com/input-output-hk/daedalus/pull/953)) -- Limit maximum number of wallets to 20 ([PR 966](https://github.com/input-output-hk/daedalus/pull/966)) +- Limited maximum number of wallets to 20 ([PR 966](https://github.com/input-output-hk/daedalus/pull/966)) ## 0.10.0 ======= @@ -100,7 +164,7 @@ Changelog ### Fixes -- Fixed color of ADA logo color on the loading screen (logo was too transparent) ([PR 850](https://github.com/input-output-hk/daedalus/pull/850)) +- Fixed color of Ada logo color on the loading screen (logo was too transparent) ([PR 850](https://github.com/input-output-hk/daedalus/pull/850)) - Fixed styling of "click to upload" text on Ada redemption screen (text was too bold) ([PR 850](https://github.com/input-output-hk/daedalus/pull/850)) - Fixed node syncing state bubble background color in Dark blue theme ([PR 850](https://github.com/input-output-hk/daedalus/pull/850)) - Fixed cropped log list within the support request dialog ([PR 850](https://github.com/input-output-hk/daedalus/pull/850)) @@ -144,10 +208,10 @@ Changelog ### Fixes -- A presentation bug has been fixed that caused only five recent transactions to be shown on the transaction list, even though there were more than five transactions in the wallet. ([PR 778](https://github.com/input-output-hk/daedalus/pull/778)) -- An issue has been fixed that stopped copy and paste operations from working when initiated using the right-click context menu. ([PR 817](https://github.com/input-output-hk/daedalus/pull/817)) -- On Windows, the desktop icon was not showing the Daedalus image; this has now been fixed. ([837](https://github.com/input-output-hk/daedalus/pull/837)) -- An error has been fixed that in some cases prevented users creating a wallet with a name containing non-latin characters, like Japanese Kanji or Chinese. ([PR 840](https://github.com/input-output-hk/daedalus/pull/840)) +- A presentation bug has been fixed that caused only five recent transactions to be shown on the transaction list, even though there were more than five transactions in the wallet ([PR 778](https://github.com/input-output-hk/daedalus/pull/778)) +- An issue has been fixed that stopped copy and paste operations from working when initiated using the right-click context menu ([PR 817](https://github.com/input-output-hk/daedalus/pull/817)) +- On Windows, the desktop icon was not showing the Daedalus image; this has now been fixed ([837](https://github.com/input-output-hk/daedalus/pull/837)) +- An error has been fixed that in some cases prevented users creating a wallet with a name containing non-latin characters, like Japanese Kanji or Chinese ([PR 840](https://github.com/input-output-hk/daedalus/pull/840)) ## 0.9.0 ======= @@ -156,7 +220,7 @@ Changelog - Do not block the UI while wallet is being restored/imported ([PR 457](https://github.com/input-output-hk/daedalus/pull/457)) - Start and stop Mantis Client from Daedalus main process ([PR 568](https://github.com/input-output-hk/daedalus/pull/568)) -- Add ADA and Mantis logo to loader screens ([PR 584](https://github.com/input-output-hk/daedalus/pull/584)) +- Add Ada and Mantis logo to loader screens ([PR 584](https://github.com/input-output-hk/daedalus/pull/584)) - New Edit section in system menu with copy & paste and related actions ([PR 817](https://github.com/input-output-hk/daedalus/pull/817)) ### Fixes @@ -168,13 +232,13 @@ Changelog - Remove transactions sorting ([PR 587](https://github.com/input-output-hk/daedalus/pull/587)) - Improve ETC transaction amount validation ([PR 590](https://github.com/input-output-hk/daedalus/pull/590)) - Fixed storybook and resolver issues ([PR 617](https://github.com/input-output-hk/daedalus/pull/617)) -- Time of your machine is different from global time. You are 0 seconds behind. ([PR 678](https://github.com/input-output-hk/daedalus/pull/678)) +- Time of your machine is different from global time. You are 0 seconds behind ([PR 678](https://github.com/input-output-hk/daedalus/pull/678)) - Updated copy for the sync error screen ([PR 657](https://github.com/input-output-hk/daedalus/pull/657)) - Detect when wallet is disconnected ([PR 689](https://github.com/input-output-hk/daedalus/pull/689)) - Improve connecting/reconnecting messages on Loading screen ([PR 696](https://github.com/input-output-hk/daedalus/pull/696)) - Send bug reports with logs from Daedalus ([PR 691](https://github.com/input-output-hk/daedalus/pull/691)) - Poll local time difference every 1 hour, only when connected ([PR 719](https://github.com/input-output-hk/daedalus/pull/719)) -- Fixed various styling issues and updated to react-polymorph 0.6.2 ([PR 726](https://github.com/input-output-hk/daedalus/pull/726)) +- Fixed various styling issues and updated to React-Polymorph 0.6.2 ([PR 726](https://github.com/input-output-hk/daedalus/pull/726)) - Fixed async restore/import dialogs logic ([PR 735](https://github.com/input-output-hk/daedalus/pull/735)) - Fixed minor UI issue on receive screen when generating wallet addresses with spending password ([PR 738](https://github.com/input-output-hk/daedalus/pull/738)) - Fixed `Time sync error notification` not showing up in case blocks syncing has not started ([PR 752](https://github.com/input-output-hk/daedalus/pull/752)) @@ -210,7 +274,7 @@ Changelog ### Fixes - Fix error message text for A to A transaction error ([PR 484](https://github.com/input-output-hk/daedalus/pull/484)) -- Fix "label prop type" checkbox issue in react-polymorph ([PR 487](https://github.com/input-output-hk/daedalus/pull/487)) +- Fix "label prop type" checkbox issue in React-Polymorph ([PR 487](https://github.com/input-output-hk/daedalus/pull/487)) - Remove all drag and drop instructions from UI ([PR 495](https://github.com/input-output-hk/daedalus/pull/495)) - Preferences saved to local storage prefixed with network ([PR 501](https://github.com/input-output-hk/daedalus/pull/501)) - Disable wallet import and export features for the mainnet ([PR 503](https://github.com/input-output-hk/daedalus/pull/503)) @@ -230,7 +294,7 @@ Changelog - Update flow-bin to version `0.59.0` ([PR 544](https://github.com/input-output-hk/daedalus/pull/544)) - Cleanup and standardization of ETC Api calls ([PR 549](https://github.com/input-output-hk/daedalus/pull/549)) - Unused vendor dependencies cleanup and DLL file optimization ([PR 555](https://github.com/input-output-hk/daedalus/pull/555)) -- Introduce `testReset` endpoint for ETC api ([PR 558](https://github.com/input-output-hk/daedalus/pull/558)) +- Introduce `testReset` endpoint for ETC API ([PR 558](https://github.com/input-output-hk/daedalus/pull/558)) - Update acceptance tests suite dependencies ([PR 561](https://github.com/input-output-hk/daedalus/pull/561)) - Refactor Cardano type declarations to type folder ([PR 557](https://github.com/input-output-hk/daedalus/pull/557)) @@ -241,7 +305,7 @@ Changelog - Fix all features eslint warnings ([PR 468](https://github.com/input-output-hk/daedalus/pull/468)) - Fix for disabled buttons on dark-blue theme ([PR 473](https://github.com/input-output-hk/daedalus/pull/473)) - Remove maximum screen width and height in full-screen mode ([PR 472](https://github.com/input-output-hk/daedalus/pull/472)) -- Fix "label click" dropdown issue in react-polymorph ([PR 479](https://github.com/input-output-hk/daedalus/pull/479)) +- Fix "label click" dropdown issue in React-Polymorph ([PR 479](https://github.com/input-output-hk/daedalus/pull/479)) ## 0.8.0 @@ -274,7 +338,7 @@ Changelog - Fixed the issue of dialogs being closable while wallet import/creation/restoring is happening ([PR 393](https://github.com/input-output-hk/daedalus/pull/414)) - Fixed calculation and display of transaction assurance levels ([PR 390](https://github.com/input-output-hk/daedalus/pull/416)) - Fixes Transaction additional info showing/hiding affects all user's wallets ([PR 411](https://github.com/input-output-hk/daedalus/pull/411)) -- Fixed promise handling for unmounted wallet send form [PR 412](https://github.com/input-output-hk/daedalus/pull/412) +- Fixed promise handling for unmounted wallet send form ([PR 412](https://github.com/input-output-hk/daedalus/pull/412)) - Fixes Transaction toggle issue on active wallet change ([PR 421](https://github.com/input-output-hk/daedalus/pull/421)) - Fixed missing 'used address' styling for default wallet address on wallet receive screen ([PR 422](https://github.com/input-output-hk/daedalus/pull/422)) - Terms of service for the mainnet ([PR 425](https://github.com/input-output-hk/daedalus/pull/425)) @@ -300,7 +364,7 @@ Changelog - Improved webpack build performance ([PR 402](https://github.com/input-output-hk/daedalus/pull/402)) - Updated README file "Development - network options" section ([PR 410](https://github.com/input-output-hk/daedalus/pull/410)) - All CSS hardcoded values replaced with variables ([PR 370](https://github.com/input-output-hk/daedalus/pull/398)) -- Implemented sync progress and payment requests with new JS api as first step to remove the purescript api ([PR 437](https://github.com/input-output-hk/daedalus/pull/437)) +- Implemented sync progress and payment requests with new JS API as first step to remove the purescript API ([PR 437](https://github.com/input-output-hk/daedalus/pull/437)) - Speed optimizations for page reloads while running tests by loading bundled up for one-time tests runs ([PR 448](https://github.com/input-output-hk/daedalus/pull/445)) - Optimized structure and naming of theming files ([PR 453](https://github.com/input-output-hk/daedalus/pull/453)) - Added testnet label, loaded from translations for release candidate ([PR 460](https://github.com/input-output-hk/daedalus/pull/460)) @@ -460,7 +524,7 @@ Changelog ### Chores -- Fixed and improved first acceptance test, made api data configurable for future test cases +- Fixed and improved first acceptance test, made API data configurable for future test cases ## 0.4.0 @@ -482,7 +546,7 @@ Changelog ### Chores -- First version of the backend api +- First version of the backend API - Improved and simplified mobx state management ## 0.3.0 diff --git a/README.md b/README.md index d25526831b..f7d6491525 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -Author: [Nikola Glumac](https://github.com/nikolaglumac)
Status: Active
+
+Document maintainer: Nikola Glumac
Document status: Active
+
# Daedalus [![Build status](https://badge.buildkite.com/e173494257519752d79bb52c7859df6277c6d759b217b68384.svg?branch=master)](https://buildkite.com/input-output-hk/daedalus) @@ -14,7 +16,7 @@ Daedalus - cryptocurrency wallet Platform-specific build scripts facilitate building Daedalus the way it is built by the IOHK CI: -# Linux/macOS +#### Linux/macOS This script requires [Nix](https://nixos.org/nix/), (optionally) configured with the [IOHK binary cache][cache]. @@ -25,7 +27,7 @@ The result can be found at `installers/csl-daedalus/daedalus-*.pkg`. [cache]: https://github.com/input-output-hk/cardano-sl/blob/3dbe220ae108fa707b55c47e689ed794edf5f4d4/docs/how-to/build-cardano-sl-and-daedalus-from-source-code.md#nix-build-mode-recommended -# Pure Nix installer build +#### Pure Nix installer build This will use nix to build a Linux installer. Using the [IOHK binary cache][cache] will speed things up. @@ -34,92 +36,74 @@ cache][cache] will speed things up. The result can be found at `./result/daedalus-*.bin`. -# Windows - -This batch file requires [Node.js](https://nodejs.org/en/download/) and -[7zip](https://www.7-zip.org/download.html). - - scripts/build-installer-win64.bat - -The result will can be found at `.\daedalus-*.exe`. +# Development -## Stepwise build - -### Install Node.js dependencies. - -```bash -$ npm install -``` - -## Development - -Run with: - -```bash -$ export CARDANO_TLS_PATH={path-to-cardano-sl}/run/tls-files/ -$ npm run dev -``` +`shell.nix` provides a way to load a shell with all the correct versions of all the +required dependencies for development. -*Note: requires a node version >= 8 and an npm version >= 5. This project defaults to 8.x* +## Connect to staging cluster: -### Development - with Cardano Wallet +1. Start the nix-shell with staging environment `yarn nix:staging` +2. Within the nix-shell run any command like `yarn dev` -Build and run [Cardano SL](https://github.com/input-output-hk/cardano-sl) +## Connect to Local Demo Cluster: -Build with: +### Build and Run cardano-sl Demo Cluster -```bash -$ brew install haskell-stack # OR curl -ssl https://get.haskellstack.org/ | sh -$ stack setup -$ stack install cpphs -$ brew install xz # OR sudo apt-get install xz-utils -$ brew install rocksdb # OR sudo apt-get install librocksdb-dev -$ git clone git@github.com:input-output-hk/cardano-sl.git -$ cd cardano-sl/ -$ ./scripts/build/cardano-sl.sh -``` +1. Install nix: `curl https://nixos.org/nix/install | sh` +2. Employ the signed IOHK binary cache: + ```bash + $ sudo mkdir -p /etc/nix + $ sudo vi /etc/nix/nix.conf # ..or any other editor, if you prefer + ``` + and then add the following lines: + ``` + substituters = https://hydra.iohk.io https://cache.nixos.org/ + trusted-substituters = + trusted-public-keys = hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspc + ``` +3. Build and run demo cluster: `scripts/launch/demo-nix.sh` -Run with: +### Start Daedalus Using Demo Cluster -```bash -$ tmux new-session -s cardano -$ WALLET_CLIENT_AUTH_DISABLE=1 ./scripts/launch/demo-with-wallet-api.sh -``` +1. Start local cardano-sl demo cluster (`./scripts/launch/demo-nix.sh`) +2. Inspect the terminal output of cardano-sl and copy the timestamp from the message + `system start: 1537184804` +3. Start the nix-shell with development environment `yarn nix:dev 1537184804` (timestamp is +different each time you restart the cardano-sl demo cluster) +4. Within the nix-shell run any command like `yarn dev` -Stop with: +## Notes: -```bash -$ tmux kill-session -t cardano -``` +`shell.nix` also provides a script for updating yarn.lock. Run `nix-shell -A fixYarnLock` +to update `yarn.lock` file. -### Development - network options +### Configuring the Network There are three different network options you can run Daedalus in: `mainnet`, `testnet` and `development` (default). To set desired network option use `NETWORK` environment variable: ```bash -$ NETWORK=testnet npm run dev +$ export NETWORK=testnet +$ yarn dev ``` -### Testing +# Testing -You can run the test suite in two different modes: +You can find more details regarding tests setup within +[Running Daedalus acceptance tests](https://github.com/input-output-hk/daedalus/blob/master/features/README.md) README file. -**One-time run:** -For running tests once using the application in production mode: +**Notes:** Be aware that only a single Daedalus instance can run per state directory. +So you have to exit any development instances before running tests! -```bash -$ npm run test -``` +# Windows -**Watch & Rerun on file changes:** -For development purposes run the tests continuously in watch mode which will re-run tests when source code changes: +This batch file requires [Node.js](https://nodejs.org/en/download/) and +[7zip](https://www.7-zip.org/download.html). -```bash -$ npm run test:watch -``` + scripts/build-installer-win64.bat -You can find more details regarding tests setup within [Running Daedalus acceptance tests](https://github.com/input-output-hk/daedalus/blob/master/features/README.md) README file. +The result will can be found at `.\daedalus-*.exe`. ### CSS Modules @@ -140,7 +124,7 @@ externals: [ ] ``` -For a common example, to install Bootstrap, `npm i --save bootstrap` and link them in the head of app.html +For a common example, to install Bootstrap, `yarn install --save bootstrap` and link them in the head of app.html ```html @@ -156,19 +140,19 @@ externals: ['bootstrap'] ## Packaging ```bash -$ npm run package +$ yarn run package ``` To package apps for all platforms: ```bash -$ npm run package:all +$ yarn run package:all ``` To package apps with options: ```bash -$ npm run package -- --[option] +$ yarn run package -- --[option] ``` ### Options diff --git a/bors.toml b/bors.toml new file mode 100644 index 0000000000..5755d16ba2 --- /dev/null +++ b/bors.toml @@ -0,0 +1,17 @@ +status = [ + # Buildkite: osx/linux installers + "buildkite/daedalus", + + # Appveyor: windows installers + "continuous-integration/appveyor/branch", + + # Hydra: we just care about tests attribute set + "ci/hydra:serokell:daedalus:tests.runFlow", + "ci/hydra:serokell:daedalus:tests.runLint", + "ci/hydra:serokell:daedalus:tests.runShellcheck" +] +timeout_sec = 7200 +required_approvals = 1 +cut_body_after = "## Type of change" +delete_merged_branches = true +block_labels = [ "DO NOT MERGE", "WIP" ] diff --git a/cardano-sl-src.json b/cardano-sl-src.json index 2ee4218b67..a1ffe2f594 100644 --- a/cardano-sl-src.json +++ b/cardano-sl-src.json @@ -1,6 +1,6 @@ { "url": "https://github.com/input-output-hk/cardano-sl", - "rev": "a956b5cfaf9165651a1ed7c9e9dc1c790e5b0d95", - "sha256": "1qg8s99l1wvch1qyw48scg6j6bf7f68xjplpw5gg3np86gxk55za", + "rev": "6a13344867b285b93e27d9ae0b1dedef2b202ddd", + "sha256": "11cql2f3j33wc5ljp5r3r4nvn3s8kcy7578n11k1ynx04dwb2c9l", "fetchSubmodules": false } diff --git a/default.nix b/default.nix index 0d62e8d9fb..33046094b7 100644 --- a/default.nix +++ b/default.nix @@ -55,8 +55,8 @@ let nix-bundle = import (pkgs.fetchFromGitHub { owner = "matthewbauer"; repo = "nix-bundle"; - rev = "496f2b524743da67717e4533745394575c6aab1f"; - sha256 = "0p9hsrbc1b0i4aipwnl4vxjsayc5m865xhp8q139ggaxq7xd0lps"; + rev = "7f12322399fd87d937355d0fc263d37d798496fc"; + sha256 = "07wnmdadchf73p03wk51abzgd3zm2xz5khwadz1ypbvv3cqlzp5m"; }) { nixpkgs = pkgs; }; desktopItem = pkgs.makeDesktopItem { name = "Daedalus${if cluster != "mainnet" then "-${cluster}" else ""}"; @@ -68,9 +68,22 @@ let }; iconPath = { # the target of these paths must not be a symlink - mainnet = ./installers/icons/mainnet/1024x1024.png; - staging = ./installers/icons/staging.iconset/icon_512x512.png; - testnet = ./installers/icons/testnet.iconset/icon_512x512.png; + demo = { + small = ./installers/icons/mainnet/64x64.png; + large = ./installers/icons/mainnet/1024x1024.png; + }; + mainnet = { + small = ./installers/icons/mainnet/64x64.png; + large = ./installers/icons/mainnet/1024x1024.png; + }; + staging = { + small = ./installers/icons/staging/64x64.png; + large = ./installers/icons/staging/1024x1024.png; + }; + testnet = { + small = ./installers/icons/testnet/64x64.png; + large = ./installers/icons/testnet/1024x1024.png; + }; }; namespaceHelper = pkgs.writeScriptBin "namespaceHelper" '' #!/usr/bin/env bash @@ -83,7 +96,12 @@ let cat /etc/nsswitch.conf > etc/nsswitch.conf cat /etc/machine-id > etc/machine-id cat /etc/resolv.conf > etc/resolv.conf - exec .${self.nix-bundle.nix-user-chroot}/bin/nix-user-chroot -n ./nix -c -m /home:/home -m /etc:/host-etc -m etc:/etc -p DISPLAY -p HOME -p XAUTHORITY -- /nix/var/nix/profiles/profile-${cluster}/bin/enter-phase2 daedalus + + if [ "x$DEBUG_SHELL" == x ]; then + exec .${self.nix-bundle.nix-user-chroot}/bin/nix-user-chroot -n ./nix -c -e -m /home:/home -m /etc:/host-etc -m etc:/etc -p DISPLAY -p HOME -p XAUTHORITY -- /nix/var/nix/profiles/profile-${cluster}/bin/enter-phase2 daedalus + else + exec .${self.nix-bundle.nix-user-chroot}/bin/nix-user-chroot -n ./nix -c -e -m /home:/home -m /etc:/host-etc -m etc:/etc -p DISPLAY -p HOME -p XAUTHORITY -- /nix/var/nix/profiles/profile-${cluster}/bin/enter-phase2 bash + fi ''; postInstall = pkgs.writeScriptBin "post-install" '' #!${pkgs.stdenv.shell} @@ -99,17 +117,23 @@ let echo "in post-install hook" - cp -f ${self.iconPath.${cluster}} $DAEDALUS_DIR/icon.png + cp -f ${self.iconPath.${cluster}.large} $DAEDALUS_DIR/icon_large.png + cp -f ${self.iconPath.${cluster}.small} $DAEDALUS_DIR/icon.png cp -Lf ${self.namespaceHelper}/bin/namespaceHelper $DAEDALUS_DIR/namespaceHelper mkdir -pv ~/.local/bin ''${XDG_DATA_HOME}/applications - cp -Lf ${self.namespaceHelper}/bin/namespaceHelper ~/.local/bin/daedalus + ${pkgs.lib.optionalString (cluster == "mainnet") "cp -Lf ${self.namespaceHelper}/bin/namespaceHelper ~/.local/bin/daedalus"} cp -Lf ${self.namespaceHelper}/bin/namespaceHelper ~/.local/bin/daedalus-${cluster} cat ${self.desktopItem}/share/applications/Daedalus*.desktop | sed \ -e "s+INSERT_PATH_HERE+''${DAEDALUS_DIR}/namespaceHelper+g" \ - -e "s+INSERT_ICON_PATH_HERE+''${DAEDALUS_DIR}/icon.png+g" \ + -e "s+INSERT_ICON_PATH_HERE+''${DAEDALUS_DIR}/icon_large.png+g" \ > "''${XDG_DATA_HOME}/applications/Daedalus${if cluster != "mainnet" then "-${cluster}" else ""}.desktop" ''; + xdg-open = pkgs.writeScriptBin "xdg-open" '' + #!${pkgs.stdenv.shell} + + echo -n "xdg-open \"$1\"" > /escape-hatch + ''; preInstall = pkgs.writeText "pre-install" '' if grep sse4 /proc/cpuinfo -q; then echo 'SSE4 check pass' @@ -123,7 +147,7 @@ let in (import ./installers/nix/nix-installer.nix { inherit (self) postInstall preInstall cluster; installationSlug = installPath; - installedPackages = [ daedalus' self.postInstall self.namespaceHelper daedalus'.cfg self.daedalus-bridge daedalus'.daedalus-frontend ]; + installedPackages = [ daedalus' self.postInstall self.namespaceHelper daedalus'.cfg self.daedalus-bridge daedalus'.daedalus-frontend self.xdg-open ]; nix-bundle = self.nix-bundle; }).installerBundle; }; diff --git a/features/README.md b/features/README.md index 76ab7e5ac2..9cc4290818 100644 --- a/features/README.md +++ b/features/README.md @@ -1,22 +1,36 @@ -Author: [Nikola Glumac](https://github.com/nikolaglumac)
Status: Active
+
+Document maintainer: Nikola Glumac
Document status: Active
+
# Running Daedalus acceptance tests -1. Make sure you have the correct node/npm versions installed on your machine (node v8.x and npm v5.x) -2. Clone Daedalus repository to your machine (`git clone git@github.com:input-output-hk/daedalus.git` - use **master** branch) -3. Install npm dependencies from within Daedalus directory: +1. Make sure you have node and yarn installed on your machine +2. Clone Daedalus repository to your machine (`git clone git@github.com:input-output-hk/daedalus.git`) +3. Install dependencies from within Daedalus directory: ```bash $ cd daedalus/ -$ npm install +$ yarn install ``` + 4. Build and run the backend (Cardano SL) following the instructions from [Daedalus](https://github.com/input-output-hk/daedalus/blob/master/README.md#development---with-cardano-wallet) README file. 5. Run Daedalus frontend tests: ```bash $ cd daedalus/ -$ npm run test +$ nix-shell --arg autoStartBackend true --arg systemStart XXX # XXX = cardano system startup time +$ yarn test ``` Once tests are complete you will get a summary of passed/failed tests in the Terminal window. + +## Keeping Daedalus Alive After Tests + +While working on the tests it's often useful to keep Daedalus alive after the tests have run +(e.g: to inspect the app state). You can pass a special environment var to tell the test script +not to close the app: + +````bash +$ KEEP_APP_AFTER_TESTS=true yarn test +```` diff --git a/features/about-dialog.feature b/features/about-dialog.feature new file mode 100644 index 0000000000..6fb0e83857 --- /dev/null +++ b/features/about-dialog.feature @@ -0,0 +1,11 @@ +Feature: About Dialog + + Background: + Given I have completed the basic setup + + Scenario: Open/close the About dialog and compare its version to the package.json + Given I open the About dialog + Then the About dialog is visible + And the About dialog and package.json show the same Daedalus version + When I close the About dialog + Then the About dialog is hidden diff --git a/features/add-wallet-via-sidebar.feature b/features/add-wallet-via-sidebar.feature index 363afe1b27..f7b10aa039 100644 --- a/features/add-wallet-via-sidebar.feature +++ b/features/add-wallet-via-sidebar.feature @@ -14,8 +14,8 @@ Feature: Add Wallet via Sidebar And I see the create wallet dialog And I toggle "Spending password" switch on the create wallet dialog And I submit the create wallet dialog with the following inputs: - | walletName | - | New wallet | + | walletName | + | New wallet | And I see the create wallet privacy dialog And I click on "Please make sure nobody looks your screen" checkbox And I submit the create wallet privacy dialog @@ -37,8 +37,8 @@ Feature: Add Wallet via Sidebar And I click on the create wallet button on the add wallet page And I see the create wallet dialog And I submit the create wallet with spending password dialog with the following inputs: - | walletName | password | repeatedPassword | - | New wallet | Secret123 | Secret123 | + | walletName | password | repeatedPassword | + | New wallet | Secret123 | Secret123 | And I see the create wallet privacy dialog And I click on "Please make sure nobody looks your screen" checkbox And I submit the create wallet privacy dialog diff --git a/features/data-layer-migration.feature b/features/data-layer-migration.feature new file mode 100644 index 0000000000..b5d75de34f --- /dev/null +++ b/features/data-layer-migration.feature @@ -0,0 +1,19 @@ +Feature: Data Layer Migration + + Background: + Given I haven't accepted the data layer migration + And I have selected English language + And I have accepted "Terms of use" + + Scenario: I don't have any wallets + Then I should not see the Data Layer Migration screen + + Scenario: I do have wallets + Given I have the following wallets: + | name | password | + | Wallet | | + Then I should see the Data Layer Migration screen + When I click the migration button + Then I should see the initial screen + When I refresh the application + Then I should not see the Data Layer Migration screen diff --git a/features/generate-wallet-address.feature b/features/generate-wallet-address.feature index 75ebc97c04..719328f007 100644 --- a/features/generate-wallet-address.feature +++ b/features/generate-wallet-address.feature @@ -5,8 +5,8 @@ Feature: Generate Wallet Address Scenario: Generating wallet address Given I have the following wallets: - | name | - | first | + | name | + | first | And I am on the "first" wallet "receive" screen And I have one wallet address And I click on the "Generate new address" button @@ -14,8 +14,8 @@ Feature: Generate Wallet Address Scenario: Generating wallet address for a wallet with spending password Given I have the following wallets: - | name | password | - | first | Secret123 | + | name | password | + | first | Secret123 | And I am on the "first" wallet "receive" screen And I have one wallet address And I enter spending password "Secret123" diff --git a/features/hide-show-used-addresses.feature b/features/hide-show-used-addresses.feature deleted file mode 100644 index e6d9df90fc..0000000000 --- a/features/hide-show-used-addresses.feature +++ /dev/null @@ -1,18 +0,0 @@ -Feature: Hide/show used addresses - - Background: - Given I have completed the basic setup - And I have a "Genesis wallet" with funds - And I have the following wallets: - | name | - | TargetWallet | - - Scenario: After 1 transaction - Given I am on the "TargetWallet" wallet "receive" screen - And I generate 1 addresses - And I have made the following transactions: - | sender | receiver | amount | - | Genesis wallet | TargetWallet | 1 | - Then I should see 2 addresses - When I click the ShowUsed switch - Then I should see 1 addresses diff --git a/features/import-wallet-via-sidebar.feature b/features/import-wallet-via-sidebar.feature index 7ff807fac4..1cf1615df1 100644 --- a/features/import-wallet-via-sidebar.feature +++ b/features/import-wallet-via-sidebar.feature @@ -15,13 +15,13 @@ Feature: Import Wallet via Sidebar And I select a valid wallet import key file And I click on the import wallet button in import wallet dialog Then I should not see the import wallet dialog anymore - And I should have newly created "Genesis wallet" wallet loaded - And I should be on the "Genesis wallet" wallet "summary" screen + And I should have newly created "Imported Wallet" wallet loaded + And I should be on the "Imported Wallet" wallet "summary" screen And I should see the restore status notification while import is running And I should not see the restore status notification once import is finished Scenario: Wallet Already Imported Error - Given I have a "Genesis wallet" with funds + Given I have a "Imported Wallet" with funds When I try to import the wallet with funds again Then I see the import wallet dialog with an error that the wallet already exists @@ -36,11 +36,11 @@ Feature: Import Wallet via Sidebar And I toggle "Activate to create password" switch on the import wallet key dialog And I should see wallet spending password inputs And I enter wallet spending password: - | password | repeatedPassword | - | Secret123 | Secret123 | + | password | repeatedPassword | + | Secret123 | Secret123 | And I click on the import wallet button in import wallet dialog Then I should not see the import wallet dialog anymore - And I should have newly created "Genesis wallet" wallet loaded - And I should be on the "Genesis wallet" wallet "summary" screen + And I should have newly created "Imported Wallet" wallet loaded + And I should be on the "Imported Wallet" wallet "summary" screen And I should see the restore status notification while import is running And I should not see the restore status notification once import is finished diff --git a/features/node-update-notification.feature b/features/node-update-notification.feature new file mode 100644 index 0000000000..d6d8faa91b --- /dev/null +++ b/features/node-update-notification.feature @@ -0,0 +1,25 @@ +Feature: Node Update Notification + + Background: + Given I have completed the basic setup + And I have the following wallets: + | name | + | Test wallet | + When I am on the "Test wallet" wallet "summary" screen + When I make a node update available + Then I should see the node update notification component + Then I should see the notification's title bar + Then I should see the expected update version in the notification's title bar + Then I should see the notification's toggle button + Then I should see the notification's update message + Then I should see the notification's accept button + Then I should see the notification's postpone button + + Scenario: User postpones a node update notification + When I click the notification's postpone button + Then I should not see the notification component anymore + + @restartApp @skip + Scenario: User accepts a node update notification + When I click the notification's accept button + Then I should see the Daedalus window close diff --git a/features/paper-wallets-certificate.feature b/features/paper-wallets-certificate.feature index 60648fbcda..ad7a3c5f05 100644 --- a/features/paper-wallets-certificate.feature +++ b/features/paper-wallets-certificate.feature @@ -2,7 +2,7 @@ Feature: Paper Wallets Certificate generation Background: Given I have completed the basic setup - And I have a "Genesis wallet" with funds + And I have a "Imported Wallet" with funds Scenario: Paper wallets certificate success generation Given The sidebar shows the "wallets" category @@ -24,8 +24,8 @@ Feature: Paper Wallets Certificate generation And Cardano explorer link and wallet address should be valid And I click on the finish button And I should not see the create paper wallet certificate dialog anymore - When I click on the "Genesis wallet" wallet in the sidebar - And I am on the "Genesis wallet" wallet "send" screen + When I click on the "Imported Wallet" wallet in the sidebar + And I am on the "Imported Wallet" wallet "send" screen And I fill out the send form: | amount | | 0.000010 | @@ -33,7 +33,7 @@ Feature: Paper Wallets Certificate generation And I click on the next button in the wallet send form And I see send money confirmation dialog And I submit the wallet send form - Then I should be on the "Genesis wallet" wallet "summary" screen + Then I should be on the "Imported Wallet" wallet "summary" screen And the latest transaction should show: | title | amountWithoutFees | | wallet.transaction.sent | -0.000010 | diff --git a/features/receive-money.feature b/features/receive-money.feature new file mode 100644 index 0000000000..9d6d57dc69 --- /dev/null +++ b/features/receive-money.feature @@ -0,0 +1,29 @@ +Feature: Receive money + + Background: + Given I have completed the basic setup + And I have a "Imported Wallet" with funds + And I have the following wallets: + | name | + | TargetWallet | + + Scenario: Hide/show used addresses + Given I am on the "TargetWallet" wallet "receive" screen + And I generate 1 addresses + And I have made the following transactions: + | source | destination | amount | + | Imported Wallet | TargetWallet | 1 | + Then I should see 2 addresses + And I should see 1 used addresses + When I click the ShowUsed switch + Then I should see 1 addresses + + Scenario: Addresses ordering + Given I am on the "TargetWallet" wallet "receive" screen + And I generate 2 addresses + Then I should see the following addresses: + | ClassName | + | generatedAddress-1 | + | generatedAddress-2 | + | generatedAddress-3 | + And The active address should be the newest one diff --git a/features/restore-wallet-via-sidebar.feature b/features/restore-wallet-via-sidebar.feature index fd42f1df88..70ee129242 100644 --- a/features/restore-wallet-via-sidebar.feature +++ b/features/restore-wallet-via-sidebar.feature @@ -14,8 +14,8 @@ Feature: Add Wallet via Sidebar And I see the restore wallet dialog And I enter wallet name "Restored wallet" in restore wallet dialog And I enter recovery phrase in restore wallet dialog: - | recoveryPhrase | - | marriage glide need gold actress grant judge eager spawn plug sister whip | + | recoveryPhrase | + | marriage glide need gold actress grant judge eager spawn plug sister whip | And I toggle "Spending password" switch on the restore wallet dialog And I submit the restore wallet dialog Then I should not see the restore wallet dialog anymore @@ -32,11 +32,11 @@ Feature: Add Wallet via Sidebar And I see the restore wallet dialog And I enter wallet name "Restored wallet" in restore wallet dialog And I enter recovery phrase in restore wallet dialog: - | recoveryPhrase | - | marriage glide need gold actress grant judge eager spawn plug sister whip | + | recoveryPhrase | + | marriage glide need gold actress grant judge eager spawn plug sister whip | And I enter wallet password in restore wallet dialog: - | password | repeatedPassword | - | Secret123 | Secret123 | + | password | repeatedPassword | + | Secret123 | Secret123 | And I submit the restore wallet dialog Then I should not see the restore wallet dialog anymore And I should have newly created "Restored wallet" wallet loaded diff --git a/features/send-money-to-receiver.feature b/features/send-money-to-receiver.feature index 622ee2a60c..283cd57321 100644 --- a/features/send-money-to-receiver.feature +++ b/features/send-money-to-receiver.feature @@ -7,8 +7,8 @@ Feature: Send Money to Receiver | first | Scenario: User Sends Money to Receiver - Given I have a "Genesis wallet" with funds - And I am on the "Genesis wallet" wallet "send" screen + Given I have a "Imported Wallet" with funds + And I am on the "Imported Wallet" wallet "send" screen And I can see the send form When I fill out the send form with a transaction to "first" wallet: | amount | @@ -17,7 +17,7 @@ Feature: Send Money to Receiver And I click on the next button in the wallet send form And I see send money confirmation dialog And I submit the wallet send form - Then I should be on the "Genesis wallet" wallet "summary" screen + Then I should be on the "Imported Wallet" wallet "summary" screen And the latest transaction should show: | title | amountWithoutFees | | wallet.transaction.sent | -0.000010 | @@ -26,8 +26,8 @@ Feature: Send Money to Receiver | 0.000010 | Scenario: User Sends Money from wallet with spending password to Receiver - Given I have a "Genesis wallet" with funds and password - And I am on the "Genesis wallet" wallet "send" screen + Given I have a "Imported Wallet" with funds and password + And I am on the "Imported Wallet" wallet "send" screen And I can see the send form When I fill out the send form with a transaction to "first" wallet: | amount | @@ -37,7 +37,7 @@ Feature: Send Money to Receiver And I see send money confirmation dialog And I enter wallet spending password in confirmation dialog "Secret123" And I submit the wallet send form - Then I should be on the "Genesis wallet" wallet "summary" screen + Then I should be on the "Imported Wallet" wallet "summary" screen And the latest transaction should show: | title | amountWithoutFees | | wallet.transaction.sent | -0.000010 | diff --git a/features/step_definitions/about-dialog.js b/features/step_definitions/about-dialog.js new file mode 100644 index 0000000000..a3077aaff1 --- /dev/null +++ b/features/step_definitions/about-dialog.js @@ -0,0 +1,22 @@ +import { Given, When, Then } from 'cucumber'; +import { expect } from 'chai'; +import packageJson from '../../package.json'; + +Given(/^I open the About dialog$/, async function () { + this.client.execute(() => daedalus.actions.app.openAboutDialog.trigger()); +}); + +When(/^I close the About dialog$/, function () { + this.client.execute(() => daedalus.actions.app.closeAboutDialog.trigger()); +}); + +Then(/^the About dialog is (hidden|visible)/, async function (state) { + const isVisible = state === 'visible'; + return this.client.waitForVisible('.About_container', null, !isVisible); +}); + +Then(/^the About dialog and package.json show the same Daedalus version/, async function () { + const { version: packageJsonVersion } = packageJson; + const aboutVersion = await this.client.getText('.About_daedalusVersion'); + expect(aboutVersion).to.equal(packageJsonVersion); +}); diff --git a/features/step_definitions/ada-redemption-steps.js b/features/step_definitions/ada-redemption-steps.js index 49f9529e7d..f5f848af56 100644 --- a/features/step_definitions/ada-redemption-steps.js +++ b/features/step_definitions/ada-redemption-steps.js @@ -13,7 +13,7 @@ const REDEMPTION_SUBMIT_BUTTON = '.AdaRedemptionForm_component .AdaRedemptionFor Given(/^I have accepted "Daedalus Redemption Disclaimer"$/, async function () { await this.client.execute(() => { - daedalus.actions.ada.adaRedemption.acceptRedemptionDisclaimer.trigger(); + daedalus.actions.adaRedemption.acceptRedemptionDisclaimer.trigger(); }); }); diff --git a/features/step_definitions/data-layer-migration.js b/features/step_definitions/data-layer-migration.js new file mode 100644 index 0000000000..ef1abb0169 --- /dev/null +++ b/features/step_definitions/data-layer-migration.js @@ -0,0 +1,22 @@ +import { Given, When, Then } from 'cucumber'; + +const DATA_LAYER_MIGRATION_ACCEPTANCE_COMPONENT = '.DataLayerMigrationForm_component'; + +Given(/^I haven't accepted the data layer migration$/, async function () { + await this.client.execute(() => { + daedalus.api.localStorage.unsetDataLayerMigrationAcceptance(); + }); +}); + +Then(/^I should see the Data Layer Migration screen$/, function () { + return this.client.waitForVisible(DATA_LAYER_MIGRATION_ACCEPTANCE_COMPONENT); +}); + +Then(/^I should not see the Data Layer Migration screen$/, function () { + return this.client.waitForVisible(DATA_LAYER_MIGRATION_ACCEPTANCE_COMPONENT, null, true); +}); + +When(/^I click the migration button$/, function () { + return this.waitAndClick(`${DATA_LAYER_MIGRATION_ACCEPTANCE_COMPONENT} .DataLayerMigrationForm_submitButton`); +}); + diff --git a/features/step_definitions/helper-steps.js b/features/step_definitions/helper-steps.js index d9eb70ae78..35925a75be 100644 --- a/features/step_definitions/helper-steps.js +++ b/features/step_definitions/helper-steps.js @@ -1,4 +1,5 @@ -import { When } from 'cucumber'; +import { When, Then } from 'cucumber'; +import { generateScreenshotFilePath, saveScreenshot } from '../support/helpers/screenshot'; const oneHour = 60 * 60 * 1000; // Helper step to pause execution for up to an hour ;) @@ -6,3 +7,15 @@ When(/^I freeze$/, { timeout: oneHour }, (callback) => { setTimeout(callback, oneHour); }); +When(/^I refresh the application$/, async function () { + return this.client.refresh(); +}); + +Then(/^I should see the initial screen$/, function () { + return this.client.waitForVisible('.SidebarLayout_component'); +}); + +When(/^I take a screenshot named "([^"]*)"$/, async function (testName) { + const file = generateScreenshotFilePath(testName); + await saveScreenshot(this, file); +}); diff --git a/features/step_definitions/local-time-difference-steps.js b/features/step_definitions/local-time-difference-steps.js index 64030ce56a..cbe864526d 100644 --- a/features/step_definitions/local-time-difference-steps.js +++ b/features/step_definitions/local-time-difference-steps.js @@ -3,7 +3,7 @@ import { Given, Then } from 'cucumber'; Given(/^I set wrong local time difference$/, async function () { await this.client.executeAsync((timeDifference, done) => { daedalus.api.ada.setLocalTimeDifference(timeDifference) - .then(() => daedalus.stores.networkStatus._updateLocalTimeDifference()) + .then(() => daedalus.stores.networkStatus._updateNetworkStatus()) .then(done) .catch((error) => done(error)); }, 1511823600000); diff --git a/features/step_definitions/node-update-notification-steps.js b/features/step_definitions/node-update-notification-steps.js new file mode 100644 index 0000000000..041c064c00 --- /dev/null +++ b/features/step_definitions/node-update-notification-steps.js @@ -0,0 +1,73 @@ +import { When, Then } from 'cucumber'; +import { expect } from 'chai'; + +const NODE_UPDATE_COMPONENT = '.NodeUpdateNotification_component'; +const TITLE_BAR = '.NodeUpdateNotification_titleBar'; +const TOGGLE_BUTTON = '.NodeUpdateNotification_toggleButton'; +const UPDATE_MESSAGE = '.NodeUpdateNotification_message'; +const ACTIONS = '.NodeUpdateNotification_actions'; +const ACCEPT_BTN = '.NodeUpdateNotification_acceptButton'; +const DENY_BTN = '.NodeUpdateNotification_denyButton'; + +const UPDATE_VERSION = 50; + +When(/^I make a node update available$/, async function () { + await this.client.executeAsync((nextVersion, done) => { + daedalus.api.ada.setNextUpdate(nextVersion) + .then(() => daedalus.stores.NodeUpdateStore.refreshNextUpdate()) + .then(done) + .catch((error) => done(error)); + }, { version: UPDATE_VERSION }); +}); + +Then(/^I should see the node update notification component$/, async function () { + await this.client.waitForVisible(`${NODE_UPDATE_COMPONENT}`); +}); + +Then(/^I should see the notification's title bar$/, async function () { + await this.client.waitForVisible(`${NODE_UPDATE_COMPONENT} ${TITLE_BAR}`); +}); + +Then(/^I should see the expected update version in the notification's title bar$/, async function () { + const titleBarSelector = `${NODE_UPDATE_COMPONENT} ${TITLE_BAR}`; + await this.client.waitForText(titleBarSelector); + const versionText = await this.client.getText(titleBarSelector); + const expectedVersionText = await this.intl('cardano.node.update.notification.titleWithVersion', { version: UPDATE_VERSION }); + expect(versionText).to.equal(expectedVersionText); +}); + +Then(/^I should see the notification's toggle button$/, async function () { + await this.client.waitForVisible(`${NODE_UPDATE_COMPONENT} ${TITLE_BAR} ${TOGGLE_BUTTON}`); +}); + +Then(/^I should see the notification's update message$/, async function () { + await this.client.waitForVisible(`${NODE_UPDATE_COMPONENT} ${UPDATE_MESSAGE}`); +}); + +Then(/^I should see the notification's accept button/, async function () { + await this.client.waitForVisible(`${NODE_UPDATE_COMPONENT} ${ACTIONS} ${ACCEPT_BTN}`); +}); + +Then(/^I should see the notification's postpone button$/, async function () { + await this.client.waitForVisible(`${NODE_UPDATE_COMPONENT} ${ACTIONS} ${DENY_BTN}`); +}); + +When(/^I click the notification's postpone button$/, async function () { + await this.waitAndClick(`${NODE_UPDATE_COMPONENT} ${ACTIONS} ${DENY_BTN}`); +}); + +When(/^I click the notification's accept button$/, async function () { + await this.waitAndClick(`${NODE_UPDATE_COMPONENT} ${ACTIONS} ${ACCEPT_BTN}`); +}); + +Then(/^I should not see the notification component anymore$/, async function () { + await this.client.waitForVisible(NODE_UPDATE_COMPONENT, null, true); +}); + +Then(/^I should see the Daedalus window close$/, async function () { + // there is latency between the window closing and this test running, so setTimeout + await setTimeout(async () => { + const windowCount = await this.client.getWindowCount(); + expect(windowCount).to.equal(0); + }, 1500); +}); diff --git a/features/step_definitions/paper-wallets-certificate-steps.js b/features/step_definitions/paper-wallets-certificate-steps.js index e8245a215c..0d3f29ee97 100644 --- a/features/step_definitions/paper-wallets-certificate-steps.js +++ b/features/step_definitions/paper-wallets-certificate-steps.js @@ -20,7 +20,7 @@ When(/^I click on the print button$/, async function () { }; await this.client.execute(params => { - daedalus.actions.ada.wallets.generateCertificate.trigger({ + daedalus.actions.wallets.generateCertificate.trigger({ filePath: params.filePath, }); }, data); @@ -54,8 +54,8 @@ When(/^I see the "Verify Certificate" dialog$/, function () { When(/^I enter paper wallet recovery phrase$/, async function () { const fields = await this.client.execute(() => ({ - walletCertificateRecoveryPhrase: daedalus.stores.ada.wallets.walletCertificateRecoveryPhrase, - additionalMnemonicWords: daedalus.stores.ada.wallets.additionalMnemonicWords, + walletCertificateRecoveryPhrase: daedalus.stores.wallets.walletCertificateRecoveryPhrase, + additionalMnemonicWords: daedalus.stores.wallets.additionalMnemonicWords, })); const walletCertificateRecoveryPhrase = `${fields.value.walletCertificateRecoveryPhrase} ${fields.value.additionalMnemonicWords}`; @@ -128,7 +128,7 @@ When(/^I see the "Paper Wallet Certificate" dialog$/, function () { When(/^Cardano explorer link and wallet address should be valid$/, async function () { const visibleCardanoExplorerLink = await this.client.getText('.CompletionDialog_linkInstructionsWrapper .CompletionDialog_infoBox .CompletionDialog_link'); const walletCertificateAddress = await this.client.execute(() => ( - daedalus.stores.ada.wallets.walletCertificateAddress + daedalus.stores.wallets.walletCertificateAddress )); const cardanoExplorerLink = `https://cardanoexplorer.com/address/${walletCertificateAddress.value}`; this.certificateWalletAddress = walletCertificateAddress.value; diff --git a/features/step_definitions/receive-steps.js b/features/step_definitions/receive-steps.js index acdfacf778..b6814afc43 100644 --- a/features/step_definitions/receive-steps.js +++ b/features/step_definitions/receive-steps.js @@ -12,7 +12,28 @@ When('I click the ShowUsed switch', async function () { await waitAndClick(this.client, '.SimpleSwitch_switch'); }); +Then('I should see {int} used addresses', { timeout: 40000 }, async function (numberOfAddresses) { + const addressesFound = await getVisibleElementsCountForSelector(this.client, '.WalletReceive_usedWalletAddress', '.WalletReceive_usedWalletAddress', 40000); + expect(addressesFound).to.equal(numberOfAddresses); +}); + Then('I should see {int} addresses', async function (numberOfAddresses) { - const addressesFound = await getVisibleElementsCountForSelector(this.client, '.WalletReceive_walletAddress', `.generatedAddress-${numberOfAddresses}`); + const addressesFound = await getVisibleElementsCountForSelector(this.client, '.WalletReceive_walletAddress'); expect(addressesFound).to.equal(numberOfAddresses); }); + +Then('I should see the following addresses:', async function (table) { + const expectedAdresses = table.hashes(); + let addresses; + await this.client.waitUntil(async () => { + addresses = await this.client.getAttribute('.WalletReceive_walletAddress', 'class'); + return addresses.length === expectedAdresses.length; + }); + addresses.forEach((address, index) => expect(address).to.include(expectedAdresses[index].ClassName)); +}); + +Then('The active address should be the newest one', async function () { + const { value: { id: lastGeneratedAddress } } = await this.client.execute(() => daedalus.stores.addresses.lastGeneratedAddress); + const activeAddress = await this.client.getText('.WalletReceive_hash'); + expect(lastGeneratedAddress).to.equal(activeAddress); +}); diff --git a/features/step_definitions/select-language-steps.js b/features/step_definitions/select-language-steps.js index 0908795dfd..91b7e89432 100644 --- a/features/step_definitions/select-language-steps.js +++ b/features/step_definitions/select-language-steps.js @@ -23,7 +23,7 @@ When(/^I open language selection dropdown$/, function () { }); When(/^I select Japanese language$/, function () { - return this.waitAndClick('//li[contains(text(), "Japanese")]'); + return this.waitAndClick('//*[@class="SimpleOptions_label"][contains(text(), "Japanese")]'); }); When(/^I submit the language selection form$/, function () { diff --git a/features/step_definitions/settings-steps.js b/features/step_definitions/settings-steps.js index 63c2153e98..7e03e5886a 100644 --- a/features/step_definitions/settings-steps.js +++ b/features/step_definitions/settings-steps.js @@ -69,18 +69,18 @@ When(/^I open "Transaction assurance security level" selection dropdown$/, funct }); When(/^I select "Strict" assurance level$/, function () { - return this.waitAndClick('//li[contains(text(), "Strict")]'); + return this.waitAndClick('//*[@class="SimpleOptions_label"][contains(text(), "Strict")]'); }); Then(/^I should have wallet with "Strict" assurance level set$/, async function () { const activeWalletName = await getNameOfActiveWalletInSidebar.call(this); const wallets = await this.client.executeAsync((done) => { - daedalus.stores.ada.wallets.walletsRequest.execute() + daedalus.stores.wallets.walletsRequest.execute() .then(done) .catch((error) => done(error)); }); const activeWallet = wallets.value.find((w) => w.name === activeWalletName); - expect(activeWallet.assurance).to.equal('CWAStrict'); + expect(activeWallet.assurance).to.equal('strict'); }); Then(/^I should see new wallet name "([^"]*)"$/, async function (walletName) { @@ -94,7 +94,7 @@ Then(/^I should see "([^"]*)" label in password field$/, function (label) { Then(/^I should see the following error messages:$/, async function (data) { const error = data.hashes()[0]; - const errorSelector = '.ChangeWalletPasswordDialog_newPassword .SimpleFormField_error'; + const errorSelector = '.ChangeSpendingPasswordDialog_newPassword .SimpleFormField_error'; await this.client.waitForText(errorSelector); const errorsOnScreen = await this.client.getText(errorSelector); const expectedError = await this.intl(error.message); diff --git a/features/step_definitions/setup-steps.js b/features/step_definitions/setup-steps.js index 3025e0edbf..bd9dde34db 100644 --- a/features/step_definitions/setup-steps.js +++ b/features/step_definitions/setup-steps.js @@ -1,8 +1,10 @@ import { Given } from 'cucumber'; import termsOfUse from '../support/helpers/terms-of-use-helpers'; import languageSelection from '../support/helpers/language-selection-helpers'; +import dataLayerMigration from '../support/helpers/data-layer-migration-helpers'; Given(/^I have completed the basic setup$/, async function () { await languageSelection.ensureLanguageIsSelected(this.client, { language: 'en-US' }); await termsOfUse.acceptTerms(this.client); + await dataLayerMigration.acceptMigration(this.client); }); diff --git a/features/step_definitions/transactions-steps.js b/features/step_definitions/transactions-steps.js index 8a29a664d8..8192b5fd8d 100644 --- a/features/step_definitions/transactions-steps.js +++ b/features/step_definitions/transactions-steps.js @@ -9,10 +9,10 @@ import { getWalletByName } from '../support/helpers/wallets-helpers'; // use only when the order is important because it's slower! Given(/^I have made the following transactions:$/, { timeout: 40000 }, async function (table) { const txData = table.hashes().map((t) => ({ - sender: getWalletByName.call(this, t.sender).id, - receiver: getWalletByName.call(this, t.receiver).id, - amount: new BigNumber(t.amount).times(LOVELACES_PER_ADA), - password: t.password || null, + walletId: getWalletByName.call(this, t.source).id, + destinationWalletId: getWalletByName.call(this, t.destination).id, + amount: parseInt(new BigNumber(t.amount).times(LOVELACES_PER_ADA), 10), + spendingPassword: t.password || null, })); this.transactions = []; // Sequentially (and async) create transactions with for loop @@ -21,12 +21,12 @@ Given(/^I have made the following transactions:$/, { timeout: 40000 }, async fun new window.Promise((resolve) => ( // Need to fetch the wallets data async and wait for all results window.Promise.all([ - daedalus.stores.ada.addresses.getAccountIdByWalletId(transaction.sender), - daedalus.stores.ada.addresses.getAddressesByWalletId(transaction.receiver) + daedalus.stores.addresses.getAccountIndexByWalletId(transaction.walletId), + daedalus.stores.addresses.getAddressesByWalletId(transaction.destinationWalletId) ]).then(results => ( daedalus.api.ada.createTransaction(window.Object.assign(transaction, { - sender: results[0], // Account id of sender wallet - receiver: results[1][0].id // First address of receiving wallet + accountIndex: results[0], // Account index of sender wallet + address: results[1][0].id // First address of receiving wallet })).then(resolve) )) )).then(done) diff --git a/features/step_definitions/wallets-limit.js b/features/step_definitions/wallets-limit.js new file mode 100644 index 0000000000..ddefa8ada6 --- /dev/null +++ b/features/step_definitions/wallets-limit.js @@ -0,0 +1,55 @@ +import { Given, When, Then } from 'cucumber'; +import { expect } from 'chai'; +import { createWallets, getWalletByName } from '../support/helpers/wallets-helpers'; +import { MAX_ADA_WALLETS_COUNT } from '../../source/renderer/app/config/numbersConfig'; +import sidebar from '../support/helpers/sidebar-helpers'; + +Given('I create wallets until I reach the maximum number permitted', async function () { + const wallets = [...Array(MAX_ADA_WALLETS_COUNT)].map((x, i) => ({ + name: `Wallet ${i + 1}`, + password: '' + })); + await createWallets(wallets, this); +}); + +When('I should see maximum number of wallets in the wallets list', async function () { + const wallets = await this.client.elements('.SidebarWalletMenuItem_component'); + expect(wallets.value.length).to.equal(MAX_ADA_WALLETS_COUNT); +}); + +When('I delete the last wallet', async function () { + const wallet = getWalletByName.call(this, 'Wallet 20'); + await this.client.execute( + walletId => daedalus.actions.wallets.deleteWallet.trigger({ walletId }), + wallet.id + ); + await this.client.waitUntil(async () => { + const wallets = await this.client.elements('.SidebarWalletMenuItem_component'); + return wallets.value.length < MAX_ADA_WALLETS_COUNT; + }); +}); + +Then(/^the buttons in the Add Wallet screen should be (disabled|enabled)/, async function (state) { + const isDisabled = state === 'disabled' ? 'true' : null; + + sidebar.clickAddWalletButton(this.client); + + await this.client.waitForVisible('.WalletAdd_buttonsContainer .BigButtonForDialogs_component'); + + const buttonsAreDisabled = await this.client + .getAttribute('.WalletAdd_buttonsContainer .BigButtonForDialogs_component', 'disabled'); + + // Excludes the "Join shared wallet" button + buttonsAreDisabled.splice(1, 1); + + expect(buttonsAreDisabled) + .to.be.an('array') + .that.include(isDisabled) + .and.not.include(!isDisabled); + +}); + +Then('I should see a disclaimer saying I have reached the maximum number of wallets', async function () { + const disclaimer = await this.client.getText('.WalletAdd_notification'); + expect(disclaimer.replace(/\n/, ' ')).to.equal(`You have reached the maximum of ${MAX_ADA_WALLETS_COUNT} wallets. No more wallets can be added.`); +}); diff --git a/features/step_definitions/wallets-steps.js b/features/step_definitions/wallets-steps.js index 60abf6bac6..f21edf755f 100644 --- a/features/step_definitions/wallets-steps.js +++ b/features/step_definitions/wallets-steps.js @@ -3,6 +3,7 @@ import { expect } from 'chai'; import path from 'path'; import BigNumber from 'bignumber.js'; import { + createWallets, fillOutWalletSendForm, getWalletByName, waitUntilWalletIsLoaded, @@ -24,52 +25,52 @@ import { } from '../support/helpers/notifications-helpers'; const defaultWalletKeyFilePath = path.resolve(__dirname, '../support/default-wallet.key'); -const defaultWalletJSONFilePath = path.resolve(__dirname, '../support/default-wallet.json'); +// const defaultWalletJSONFilePath = path.resolve(__dirname, '../support/default-wallet.json'); +// ^^ JSON wallet file import is currently not working due to missing JSON import V1 API endpoint -Given(/^I have a "Genesis wallet" with funds$/, async function () { +Given(/^I have a "Imported Wallet" with funds$/, async function () { await importWalletWithFunds(this.client, { keyFilePath: defaultWalletKeyFilePath, password: null, }); - const wallet = await waitUntilWalletIsLoaded.call(this, 'Genesis wallet'); + const wallet = await waitUntilWalletIsLoaded.call(this, 'Imported Wallet'); addOrSetWalletsForScenario.call(this, wallet); }); -Given(/^I have a "Genesis wallet" with funds and password$/, async function () { +// V1 API endpoint for importing a wallet with a spending-password is currently broken +// As a temporary workaround we import the wallet without a spending-password +// and then create a spending-password in a separate call +Given(/^I have a "Imported Wallet" with funds and password$/, async function () { await importWalletWithFunds(this.client, { keyFilePath: defaultWalletKeyFilePath, - password: 'Secret123', + password: null, // 'Secret123', }); - const wallet = await waitUntilWalletIsLoaded.call(this, 'Genesis wallet'); + const wallet = await waitUntilWalletIsLoaded.call(this, 'Imported Wallet'); addOrSetWalletsForScenario.call(this, wallet); -}); -Given(/^I have the following wallets:$/, async function (table) { - const result = await this.client.executeAsync((wallets, done) => { - window.Promise.all(wallets.map((wallet) => ( - daedalus.api.ada.createWallet({ - name: wallet.name, - mnemonic: daedalus.utils.crypto.generateMnemonic(), - password: wallet.password || null, - }) - ))) + // Create a spending-password in a separate call + await this.client.executeAsync((walletId, done) => { + daedalus.api.ada.updateSpendingPassword({ + walletId, + oldPassword: null, + newPassword: 'Secret123', + }) .then(() => ( - daedalus.stores.ada.wallets.walletsRequest.execute() - .then((storeWallets) => ( - daedalus.stores.ada.wallets.refreshWalletsData() - .then(() => done(storeWallets)) - .catch((error) => done(error)) - )) + daedalus.stores.wallets.refreshWalletsData() + .then(done) .catch((error) => done(error)) )) - .catch((error) => done(error.stack)); - }, table.hashes()); - // Add or set the wallets for this scenario - if (this.wallets != null) { - this.wallets.push(...result.value); - } else { - this.wallets = result.value; - } + .catch((error) => done(error)); + }, wallet.id); +}); + +Given(/^I have the following wallets:$/, async function (table) { + await createWallets(table.hashes(), this); +}); + +// Creates them sequentially +Given(/^I have created the following wallets:$/, async function (table) { + await createWallets(table.hashes(), this, { sequentially: true }); }); Given(/^I am on the "([^"]*)" wallet "([^"]*)" screen$/, async function (walletName, screen) { @@ -117,7 +118,10 @@ When(/^I see the import wallet dialog$/, function () { }); When(/^I select a valid wallet import key file$/, function () { - return importWalletDialog.selectFile(this.client, { filePath: defaultWalletJSONFilePath }); + // return importWalletDialog.selectFile(this.client, { filePath: defaultWalletJSONFilePath }); + // ^^ JSON wallet file import is currently not working due to missing JSON import V1 API endpoint + // so we have to use the KEY wallet file instead: + return importWalletDialog.selectFile(this.client, { filePath: defaultWalletKeyFilePath }); }); When(/^I toggle "Activate to create password" switch on the import wallet key dialog$/, function () { @@ -126,7 +130,7 @@ When(/^I toggle "Activate to create password" switch on the import wallet key di When(/^I enter wallet spending password:$/, async function (table) { const fields = table.hashes()[0]; - await this.client.setValue('.WalletFileImportDialog .walletPassword input', fields.password); + await this.client.setValue('.WalletFileImportDialog .spendingPassword input', fields.password); await this.client.setValue('.WalletFileImportDialog .repeatedPassword input', fields.repeatedPassword); }); @@ -135,7 +139,7 @@ When(/^I click on the import wallet button in import wallet dialog$/, function ( }); When(/^I should see wallet spending password inputs$/, function () { - return this.client.waitForVisible('.WalletFileImportDialog .walletPassword input'); + return this.client.waitForVisible('.WalletFileImportDialog .spendingPassword input'); }); When(/^I have one wallet address$/, function () { @@ -202,7 +206,7 @@ When(/^I see send money confirmation dialog$/, function () { }); When(/^I enter wallet spending password in confirmation dialog "([^"]*)"$/, async function (password) { - await this.client.setValue('.WalletSendConfirmationDialog_walletPassword input', password); + await this.client.setValue('.WalletSendConfirmationDialog_spendingPassword input', password); }); When(/^I submit the wallet send form$/, async function () { @@ -227,7 +231,7 @@ When(/^I submit the create wallet dialog with the following inputs:$/, async fun When(/^I submit the create wallet with spending password dialog with the following inputs:$/, async function (table) { const fields = table.hashes()[0]; await this.client.setValue('.WalletCreateDialog .walletName input', fields.walletName); - await this.client.setValue('.WalletCreateDialog .walletPassword input', fields.password); + await this.client.setValue('.WalletCreateDialog .spendingPassword input', fields.password); await this.client.setValue('.WalletCreateDialog .repeatedPassword input', fields.repeatedPassword); return this.client.click('.WalletCreateDialog .primary'); }); @@ -250,7 +254,7 @@ When(/^I enter recovery phrase in restore wallet dialog:$/, async function (tabl When(/^I enter wallet password in restore wallet dialog:$/, async function (table) { const fields = table.hashes()[0]; - await this.client.setValue('.WalletRestoreDialog .walletPassword input', fields.password); + await this.client.setValue('.WalletRestoreDialog .spendingPassword input', fields.password); await this.client.setValue('.WalletRestoreDialog .repeatedPassword input', fields.repeatedPassword); }); @@ -290,7 +294,7 @@ When(/^I see the create wallet recovery phrase entry dialog$/, function () { When(/^I click on recovery phrase mnemonics in correct order$/, async function () { for (let i = 0; i < this.recoveryPhrase.length; i++) { const recoveryPhraseMnemonic = this.recoveryPhrase[i]; - await this.waitAndClick(`//button[contains(text(), "${recoveryPhraseMnemonic}") and @class="flat MnemonicWord_component MnemonicWord_active SimpleButton_root"]`); + await this.waitAndClick(`//button[contains(text(), "${recoveryPhraseMnemonic}") and @class="flat SimpleButton_root MnemonicWord_root"]`); } }); @@ -328,7 +332,10 @@ When(/^I try to import the wallet with funds again$/, async function () { await addWalletPage.waitForVisible(this.client); await addWalletPage.clickImportButton(this.client); await importWalletDialog.waitForDialog(this.client); - await importWalletDialog.selectFile(this.client, { filePath: defaultWalletJSONFilePath }); + // await importWalletDialog.selectFile(this.client, { filePath: defaultWalletJSONFilePath }); + // ^^ JSON wallet file import is currently not working due to missing JSON import V1 API endpoint + // so we have to use the KEY wallet file instead: + await importWalletDialog.selectFile(this.client, { filePath: defaultWalletKeyFilePath }); return importWalletDialog.clickImport(this.client); }); @@ -378,7 +385,7 @@ Then(/^I should not see the restore status notification once restore is finished Then(/^I should have newly created "([^"]*)" wallet loaded$/, async function (walletName) { const result = await this.client.executeAsync((done) => { - daedalus.stores.ada.wallets.walletsRequest.execute() + daedalus.stores.wallets.walletsRequest.execute() .then(done) .catch((error) => done(error)); }); @@ -437,13 +444,13 @@ Then(/^the latest transaction should show:$/, async function (table) { // Extended timeout is used for this step as it takes more than DEFAULT_TIMEOUT // for the receiver wallet's balance to be updated on the backend after creating transactions -Then(/^the balance of "([^"]*)" wallet should be:$/, { timeout: 40000 }, async function (walletName, table) { +Then(/^the balance of "([^"]*)" wallet should be:$/, { timeout: 60000 }, async function (walletName, table) { const expectedData = table.hashes()[0]; const receiverWallet = getWalletByName.call(this, walletName); return this.client.waitUntil(async () => { const receiverWalletBalance = await this.client.getText(`.SidebarWalletsMenu_wallets .Wallet_${receiverWallet.id} .SidebarWalletMenuItem_info`); return receiverWalletBalance === `${expectedData.balance} ADA`; - }); + }, 60000); }); Then(/^I should see newly generated address as active address on the wallet receive screen$/, async function () { @@ -453,3 +460,9 @@ Then(/^I should see newly generated address as active address on the wallet rece return generatedAddress === activeAddress; }); }); + +Then(/^I should see the wallets in the following order:$/, async function (table) { + const expectedWallets = table.hashes(); + const wallets = await this.client.getText('.SidebarWalletMenuItem_title'); + wallets.forEach((wallet, index) => expect(wallet).to.equal(expectedWallets[index].name)); +}); diff --git a/features/support/default-wallet.json b/features/support/default-wallet.json index 777a1778ff..23cfc7af1b 100644 --- a/features/support/default-wallet.json +++ b/features/support/default-wallet.json @@ -1 +1 @@ -{"wallet":{"accounts":[{"name":"Genesis account","index":2147483648}],"walletSecretKey":"WIAwbsQgbz9X0WhvOnVeH+yRs7Ri93ESTdMspBHzeLnPUR6hLZL/NazfB40z2x8FZhLwNIt83DCuMR1nGG+ZqvsD/ouyzg3ec729fnrqEMO4A+qPTJmpiRgQZfYO2KDJDRxLtMyofXl90VVZOEke/QddnZ8CGHoR/lCemJgZuvzBpw==","walletMeta":{"name":"Genesis wallet","assurance":"normal","unit":"ADA"},"passwordHash":"WGQxNHw4fDF8V0NERGRHY0JGcThzelVyeFdza00wM1VjYnloeVBBQXBvdWtwdWFsUTExNGVFdz09fFJXMk5kUmVJYmg2REtsa2lsWG8rQ1lvTStRZmJkMzRmRVd0MG4rSy82YUU9"},"fileType":"WALLETS_EXPORT","fileVersion":"1.0.0"} \ No newline at end of file +{"wallet":{"accounts":[{"name":"Genesis account","index":2147483648}],"walletSecretKey":"WIAwbsQgbz9X0WhvOnVeH+yRs7Ri93ESTdMspBHzeLnPUR6hLZL/NazfB40z2x8FZhLwNIt83DCuMR1nGG+ZqvsD/ouyzg3ec729fnrqEMO4A+qPTJmpiRgQZfYO2KDJDRxLtMyofXl90VVZOEke/QddnZ8CGHoR/lCemJgZuvzBpw==","walletMeta":{"name":"Imported Wallet","assurance":"normal","unit":"ADA"},"passwordHash":"WGQxNHw4fDF8V0NERGRHY0JGcThzelVyeFdza00wM1VjYnloeVBBQXBvdWtwdWFsUTExNGVFdz09fFJXMk5kUmVJYmg2REtsa2lsWG8rQ1lvTStRZmJkMzRmRVd0MG4rSy82YUU9"},"fileType":"WALLETS_EXPORT","fileVersion":"1.0.0"} \ No newline at end of file diff --git a/features/support/electron.js b/features/support/electron.js index d10471b304..e76d4f934e 100644 --- a/features/support/electron.js +++ b/features/support/electron.js @@ -2,6 +2,7 @@ import { Application } from 'spectron'; import { defineSupportCode } from 'cucumber'; import electronPath from 'electron'; import environment from '../../source/common/environment'; +import { generateScreenshotFilePath, getTestNameFromTestFile, saveScreenshot } from './helpers/screenshot'; const context = {}; const DEFAULT_TIMEOUT = 20000; @@ -17,6 +18,20 @@ const printMainProcessLogs = () => ( }) ); +const startApp = async () => { + const app = new Application({ + path: electronPath, + args: ['./dist/main/index.js'], + env: Object.assign({}, process.env, { + NODE_ENV: environment.TEST, + }), + waitTimeout: DEFAULT_TIMEOUT + }); + await app.start(); + await app.client.waitUntilWindowLoaded(); + return app; +}; + defineSupportCode(({ BeforeAll, Before, After, AfterAll, setDefaultTimeout }) => { // The cucumber timeout should be high (and never reached in best case) // because the errors thrown by webdriver.io timeouts are more descriptive @@ -25,17 +40,7 @@ defineSupportCode(({ BeforeAll, Before, After, AfterAll, setDefaultTimeout }) => // Boot up the electron app before all features BeforeAll({ timeout: 5 * 60 * 1000 }, async () => { - const app = new Application({ - path: electronPath, - args: ['./dist/main/index.js'], - env: { - NODE_ENV: environment.TEST, - }, - waitTimeout: DEFAULT_TIMEOUT - }); - await app.start(); - await app.client.waitUntilWindowLoaded(); - context.app = app; + context.app = await startApp(); }); // Make the electron app accessible in each scenario context @@ -85,19 +90,35 @@ defineSupportCode(({ BeforeAll, Before, After, AfterAll, setDefaultTimeout }) => }); }); + // this ensures that the spectron instance of the app restarts + // after the node update acceptance test shuts it down via 'kill-process' + // eslint-disable-next-line prefer-arrow-callback + After({ tags: '@restartApp' }, async function () { + context.app = await startApp(); + }); + // eslint-disable-next-line prefer-arrow-callback - After(async function ({ result }) { + After(async function ({ sourceLocation, result }) { scenariosCount++; if (result.status === 'failed') { + const testName = getTestNameFromTestFile(sourceLocation.uri); + const file = generateScreenshotFilePath(testName); + await saveScreenshot(context.app, file); await printMainProcessLogs(); } }); // eslint-disable-next-line prefer-arrow-callback AfterAll(async function () { + if (!context.app.running) return; + if (scenariosCount === 0) { await printMainProcessLogs(); } + if (process.env.KEEP_APP_AFTER_TESTS === 'true') { + console.log('Keeping the app running since KEEP_APP_AFTER_TESTS env var is true'); + return; + } return context.app.stop(); }); }); diff --git a/features/support/helpers/data-layer-migration-helpers.js b/features/support/helpers/data-layer-migration-helpers.js new file mode 100644 index 0000000000..16365291b7 --- /dev/null +++ b/features/support/helpers/data-layer-migration-helpers.js @@ -0,0 +1,15 @@ +const DATA_LAYER_MIGRATION_ACCEPTANCE_COMPONENT = '.DataLayerMigrationForm_component'; + +const dataLayerMigration = { + waitForVisible: async (client, { isHidden } = {}) => ( + client.waitForVisible(DATA_LAYER_MIGRATION_ACCEPTANCE_COMPONENT, null, isHidden) + ), + acceptMigration: async (client) => { + await client.execute(() => { + daedalus.actions.profile.acceptDataLayerMigration.trigger(); + }); + await dataLayerMigration.waitForVisible(client, { isHidden: true }); + } +}; + +export default dataLayerMigration; diff --git a/features/support/helpers/notifications-helpers.js b/features/support/helpers/notifications-helpers.js index 3034d82e1a..84c91a2511 100644 --- a/features/support/helpers/notifications-helpers.js +++ b/features/support/helpers/notifications-helpers.js @@ -1,9 +1,9 @@ -import { syncStateTags } from '../../../source/renderer/app/domains/Wallet'; +import { WalletSyncStateTags } from '../../../source/renderer/app/domains/Wallet'; export const isActiveWalletBeingRestored = async (client) => { const result = await client.execute((expectedSyncTag) => ( - daedalus.stores.ada.wallets.active.syncState.tag === expectedSyncTag - ), syncStateTags.RESTORING); + daedalus.stores.wallets.active.syncState.tag === expectedSyncTag + ), WalletSyncStateTags.RESTORING); return result.value; }; diff --git a/features/support/helpers/screenshot.js b/features/support/helpers/screenshot.js new file mode 100644 index 0000000000..193299e115 --- /dev/null +++ b/features/support/helpers/screenshot.js @@ -0,0 +1,23 @@ +import fs from 'fs'; +import path from 'path'; +import { generateFileNameWithTimestamp } from '../../../source/common/fileName'; +import ensureDirectoryExists from '../../../source/main/utils/ensureDirectoryExists'; + +export const generateScreenshotFilePath = (testName) => { + const filePath = path.resolve(__dirname, '../../screenshots', testName); + const fileName = generateFileNameWithTimestamp(testName, 'png'); + ensureDirectoryExists(filePath); + return `${filePath}/${fileName}`; +}; + +export const getTestNameFromTestFile = (testFile) => testFile + .replace('features/', '') + .replace('.feature', ''); + +export const saveScreenshot = async (context, file) => { + await context.browserWindow.capturePage() + .then((imageBuffer) => fs.writeFile(file, imageBuffer)) + .catch((err) => { + console.log(err); + }); +}; diff --git a/features/support/helpers/wallets-helpers.js b/features/support/helpers/wallets-helpers.js index b280bccb96..4326d2b913 100644 --- a/features/support/helpers/wallets-helpers.js +++ b/features/support/helpers/wallets-helpers.js @@ -14,8 +14,8 @@ export const fillOutWalletSendForm = async function (values) { const formSelector = '.WalletSendForm_component'; await this.client.setValue(`${formSelector} .receiver .SimpleInput_input`, values.address); await this.client.setValue(`${formSelector} .amount .SimpleInput_input`, values.amount); - if (values.walletPassword) { - await this.client.setValue(`${formSelector} .walletPassword .SimpleInput_input`, values.walletPassword); + if (values.spendingPassword) { + await this.client.setValue(`${formSelector} .spendingPassword .SimpleInput_input`, values.spendingPassword); } this.walletSendFormValues = values; }; @@ -37,7 +37,7 @@ export const waitUntilWalletIsLoaded = async function (walletName) { const context = this; await context.client.waitUntil(async () => { const result = await context.client.execute((name) => ( - daedalus.stores.ada.wallets.getWalletByName(name) + daedalus.stores.wallets.getWalletByName(name) ), walletName); if (result.value) { wallet = result.value; @@ -58,13 +58,71 @@ export const addOrSetWalletsForScenario = function (wallet) { }; export const importWalletWithFunds = async (client, { keyFilePath, password }) => ( - await client.executeAsync((filePath, walletPassword, done) => { - daedalus.api.ada.importWalletFromKey({ filePath, walletPassword }) + await client.executeAsync((filePath, spendingPassword, done) => { + daedalus.api.ada.importWalletFromKey({ filePath, spendingPassword }) .then(() => ( - daedalus.stores.ada.wallets.refreshWalletsData() + daedalus.stores.wallets.refreshWalletsData() .then(done) .catch((error) => done(error)) )) .catch((error) => done(error)); }, keyFilePath, password) ); + +const createWalletsAsync = async (table, context) => { + const result = await context.client.executeAsync((wallets, done) => { + window.Promise.all(wallets.map((wallet) => ( + daedalus.api.ada.createWallet({ + name: wallet.name, + mnemonic: daedalus.utils.crypto.generateMnemonic(), + spendingPassword: wallet.password || null, + }) + ))) + .then(() => ( + daedalus.stores.wallets.walletsRequest.execute() + .then((storeWallets) => ( + daedalus.stores.wallets.refreshWalletsData() + .then(() => done(storeWallets)) + .catch((error) => done(error)) + )) + .catch((error) => done(error)) + )) + .catch((error) => done(error.stack)); + }, table); + // Add or set the wallets for this scenario + if (context.wallets != null) { + context.wallets.push(...result.value); + } else { + context.wallets = result.value; + } +}; + +const createWalletsSequentially = async (wallets, context) => { + context.wallets = []; + for (const walletData of wallets) { + const result = await context.client.executeAsync((wallet, done) => { + daedalus.api.ada.createWallet({ + name: wallet.name, + mnemonic: daedalus.utils.crypto.generateMnemonic(), + spendingPassword: wallet.password || null, + }).then(() => ( + daedalus.stores.wallets.walletsRequest.execute() + .then((storeWallets) => ( + daedalus.stores.wallets.refreshWalletsData() + .then(() => done(storeWallets)) + .catch((error) => done(error)) + )) + .catch((error) => done(error)) + )).catch((error) => done(error.stack)); + }, walletData); + context.wallets = result.value; + } +}; + +export const createWallets = async (wallets, context, options = {}) => { + if (options.sequentially === true) { + await createWalletsSequentially(wallets, context); + } else { + await createWalletsAsync(wallets, context); + } +}; diff --git a/features/switching-between-wallets.feature b/features/switching-between-wallets.feature index 668ececae8..7d9f43bc48 100644 --- a/features/switching-between-wallets.feature +++ b/features/switching-between-wallets.feature @@ -5,10 +5,10 @@ Feature: Switching Between Wallets Scenario Outline: Using the Sidebar to Switch Wallets Given I have the following wallets: - | name | - | first | - | second | - | third | + | name | + | first | + | second | + | third | And I am on the "" wallet "summary" screen And The sidebar shows the "wallets" category And the sidebar submenu is visible diff --git a/features/transactions-display.feature b/features/transactions-display.feature index cf599fa529..748cc7bc1e 100644 --- a/features/transactions-display.feature +++ b/features/transactions-display.feature @@ -7,9 +7,9 @@ Feature: Display wallet transactions Background: Given I have completed the basic setup - And I have a "Genesis wallet" with funds + And I have a "Imported Wallet" with funds And I have the following wallets: - | name | + | name | | TargetWallet | Scenario: No recent transactions @@ -20,15 +20,16 @@ Feature: Display wallet transactions Then I should not see any transactions And I should see the no recent transactions message + @skip Scenario: More than five transactions Given I have made the following transactions: - | sender | receiver | amount | - | Genesis wallet | TargetWallet | 1 | - | Genesis wallet | TargetWallet | 2 | - | Genesis wallet | TargetWallet | 3 | - | Genesis wallet | TargetWallet | 4 | - | Genesis wallet | TargetWallet | 5 | - | Genesis wallet | TargetWallet | 6 | + | source | destination | amount | + | Imported Wallet | TargetWallet | 1 | + | Imported Wallet | TargetWallet | 2 | + | Imported Wallet | TargetWallet | 3 | + | Imported Wallet | TargetWallet | 4 | + | Imported Wallet | TargetWallet | 5 | + | Imported Wallet | TargetWallet | 6 | When I am on the "TargetWallet" wallet "summary" screen Then I should see the following transactions: | type | amount | diff --git a/features/transactions-grouping.feature b/features/transactions-grouping.feature deleted file mode 100644 index 618b53b6d6..0000000000 --- a/features/transactions-grouping.feature +++ /dev/null @@ -1,20 +0,0 @@ -# TODO: Fix this test case! -@skip -Feature: Transactions Grouping - In order to see clearly when transactions have been executed - As a User - I want to see them grouped by date - - Background: - Given I have selected English language - And I have accepted "Terms of use" - And I have a "Genesis wallet" with funds - - Scenario: Transactions are Grouped by Date - Given I have made the following transactions: - | title | date | - | First | 2016-01-01 | - | Second | 2016-01-02 | - | Third | 2016-01-03 | - When I am on the wallet summary screen - Then I should see the transactions grouped by their date diff --git a/features/wallet-settings.feature b/features/wallet-settings.feature index 28908d55be..b2e5e3c73d 100644 --- a/features/wallet-settings.feature +++ b/features/wallet-settings.feature @@ -3,17 +3,17 @@ Feature: Wallet Settings Background: Given I have completed the basic setup And I have the following wallets: - | name | password | - | first | | - | second | Secret123 | + | name | password | + | first | | + | second | Secret123 | Scenario: User sets Wallet password Given I am on the "first" wallet "settings" screen And I click on the "create" password label And I should see the "create" wallet password dialog And I enter wallet password: - | password | repeatedPassword | - | Secret123 | Secret123 | + | password | repeatedPassword | + | Secret123 | Secret123 | And I submit the wallet password dialog Then I should see "change" label in password field @@ -22,20 +22,39 @@ Feature: Wallet Settings And I click on the "create" password label And I should see the "create" wallet password dialog And I enter wallet password: - | password | repeatedPassword | - | secret | secret | + | password | repeatedPassword | + | secret | secret | And I submit the wallet password dialog Then I should see the following error messages: - | message | - | global.errors.invalidWalletPassword | + | message | + | global.errors.invalidSpendingPassword | Scenario: User changes Wallet password Given I am on the "second" wallet "settings" screen And I click on the "change" password label And I should see the "change" wallet password dialog And I change wallet password: - | currentPassword | password | repeatedPassword | - | Secret123 | newSecret123 | newSecret123 | + | currentPassword | password | repeatedPassword | + | Secret123 | newSecret123 | newSecret123 | + And I submit the wallet password dialog + Then I should not see the change password dialog anymore + + Scenario: User changes wallet password to one which contains only cyrillic characters and numbers + Given I am on the "second" wallet "settings" screen + And I click on the "change" password label + And I should see the "change" wallet password dialog + And I change wallet password: + | currentPassword | password | repeatedPassword | + | Secret123 | ЬнЫгзукЗфыыцщкв123 | ЬнЫгзукЗфыыцщкв123 | + And I submit the wallet password dialog + Then I should not see the change password dialog anymore + Scenario: User changes wallet password to one which contains only japanese characters and numbers + Given I am on the "second" wallet "settings" screen + And I click on the "change" password label + And I should see the "change" wallet password dialog + And I change wallet password: + | currentPassword | password | repeatedPassword | + | Secret123 | 新しい秘密123 | 新しい秘密123 | And I submit the wallet password dialog Then I should not see the change password dialog anymore @@ -45,8 +64,8 @@ Feature: Wallet Settings And I should see the "change" wallet password dialog And I toggle "Check to deactivate password" switch on the change wallet password dialog And I enter current wallet password: - | currentPassword | - | Secret123 | + | currentPassword | + | Secret123 | And I submit the wallet password dialog Then I should see "create" label in password field @@ -54,8 +73,8 @@ Feature: Wallet Settings Given I am on the "first" wallet "settings" screen And I click on "name" input field And I enter new wallet name: - | name | - | first Edited | + | name | + | first Edited | And I click outside "name" input field Then I should see new wallet name "first Edited" @@ -63,8 +82,8 @@ Feature: Wallet Settings Given I am on the "first" wallet "settings" screen And I click on "name" input field And I enter new wallet name: - | name | - | キュビズム | + | name | + | キュビズム | And I click outside "name" input field Then I should see new wallet name "キュビズム" diff --git a/features/wallets-limit.feature b/features/wallets-limit.feature new file mode 100644 index 0000000000..e164fd1ac0 --- /dev/null +++ b/features/wallets-limit.feature @@ -0,0 +1,14 @@ +Feature: Wallet Settings + + Background: + Given I have completed the basic setup + Given I create wallets until I reach the maximum number permitted + + Scenario: User reaches the maximum number of wallets + Then I should see maximum number of wallets in the wallets list + And the buttons in the Add Wallet screen should be disabled + And I should see a disclaimer saying I have reached the maximum number of wallets + + Scenario: User deletes one wallet and re-enable its Adding new wallets + Given I delete the last wallet + Then the buttons in the Add Wallet screen should be enabled diff --git a/features/wallets-ordering.feature b/features/wallets-ordering.feature new file mode 100644 index 0000000000..fc1a1f4aef --- /dev/null +++ b/features/wallets-ordering.feature @@ -0,0 +1,28 @@ +Feature: Wallet Odering + + Background: + Given I have completed the basic setup + + Scenario: Wallets ordering + Given I have created the following wallets: + | name | + | Wallet 1 | + | Wallet 2 | + | Wallet 3 | + | Wallet 4 | + | Wallet 5 | + | Wallet 6 | + | Wallet 7 | + | Wallet 8 | + | Wallet 9 | + Then I should see the wallets in the following order: + | name | + | Wallet 1 | + | Wallet 2 | + | Wallet 3 | + | Wallet 4 | + | Wallet 5 | + | Wallet 6 | + | Wallet 7 | + | Wallet 8 | + | Wallet 9 | diff --git a/gulpfile.js b/gulpfile.js index 9f6d5b65e3..02518e6105 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -6,20 +6,24 @@ const rendererWebpackConfig = require('./source/renderer/webpack.config'); const shell = require('gulp-shell'); const electronConnect = require('electron-connect'); -const mainOutputDestination = () => gulp.dest('dist/main'); -const rendererOutputDestination = () => gulp.dest('dist/renderer'); - -const buildMain = (config, done) => gulp.src('source/main/index.js') - .pipe(webpackStream(Object.assign({}, mainWebpackConfig, config), webpack, done)) - .pipe(mainOutputDestination()); - -const buildRenderer = (config, done) => gulp.src('source/renderer/index.js') - .pipe(webpackStream(Object.assign({}, rendererWebpackConfig, config), webpack, done)) - .pipe(rendererOutputDestination()); - // Setup electron-connect server to start the app in development mode let electronServer; +// Gulp input sources for main and renderer compilation +const mainInputSource = () => gulp.src('source/main/index.js'); +const rendererInputSource = () => gulp.src('source/renderer/index.js'); +// Webpack watch configs +const mainWebpackWatchConfig = Object.assign({}, mainWebpackConfig, { watch: true }); +const rendererWebpackWatchConfig = Object.assign({}, rendererWebpackConfig, { watch: true }); +// Gulp output destinations for main and renderer compilation +const mainOutputDestination = () => gulp.dest('dist/main'); +const rendererOutputDestination = () => gulp.dest('dist/renderer'); +/** + * Creates an electron-connect server instance that enables + * us to control our app (restarting / reloading) + * @param env - electron app environment + * @param args - additional spawn options + */ const createElectronServer = (env, args = []) => { electronServer = electronConnect.server.create({ spawnOpt: { @@ -29,31 +33,84 @@ const createElectronServer = (env, args = []) => { }); }; -const startElectronInWatchMode = () => { +const buildMain = () => ( + () => ( + mainInputSource() + .pipe(webpackStream(mainWebpackConfig, webpack)) + .pipe(mainOutputDestination()) + ) +); + +const buildMainWatch = () => ( + (done) => ( + mainInputSource() + .pipe(webpackStream(mainWebpackWatchConfig, webpack, () => { + // Restart app everytime after main script has been re-compiled + electronServer.restart(); + done(); + })) + .pipe(mainOutputDestination()) + ) +); + +const buildRenderer = () => ( + () => ( + rendererInputSource() + .pipe(webpackStream(rendererWebpackConfig, webpack)) + .pipe(rendererOutputDestination()) + ) +); + +const buildRendererWatch = () => ( + (done) => ( + rendererInputSource() + .pipe(webpackStream(rendererWebpackWatchConfig, webpack, () => { + // Reload app everytime after renderer script has been re-compiled + electronServer.reload(); + done(); + })) + .pipe(rendererOutputDestination()) + ) +); + +gulp.task('clear:cache', shell.task('rimraf ./node_modules/.cache')); + +gulp.task('clean:dist', shell.task('rimraf ./dist')); + +gulp.task('server:start', (done) => { electronServer.start(); - gulp.watch('dist/main/index.js', gulp.series('electron:restart')); - gulp.watch('dist/renderer/*', gulp.series('electron:reload')); -}; + done(); +}); -gulp.task('clear-cache', shell.task('rimraf ./node_modules/.cache')); +gulp.task('server:create:dev', (done) => { + createElectronServer({ NODE_ENV: process.env.NODE_ENV || 'development' }); + done(); +}); -gulp.task('clean:dist', shell.task('rimraf ./dist')); +gulp.task('server:create:debug', (done) => { + createElectronServer({ NODE_ENV: 'development' }, ['--inspect', '--inspect-brk']); + done(); +}); -gulp.task('build:main', (done) => buildMain({}, done)); +gulp.task('build:main', buildMain()); -gulp.task('build:main:watch', (done) => buildMain({ watch: true }, done)); +gulp.task('build:main:watch', buildMainWatch()); -gulp.task('build:renderer:html', () => gulp.src('source/renderer/index.html').pipe(gulp.dest('dist/renderer/'))); +gulp.task('build:renderer:html', () => ( + gulp.src('source/renderer/index.html').pipe(gulp.dest('dist/renderer/')) +)); -gulp.task('build:renderer:assets', (done) => buildRenderer({}, done)); +gulp.task('build:renderer:assets', buildRenderer()); gulp.task('build:renderer', gulp.series('build:renderer:html', 'build:renderer:assets')); -gulp.task('build:renderer:watch', (done) => buildRenderer({ watch: true }, done)); +gulp.task('build:renderer:watch', buildRendererWatch()); gulp.task('build', gulp.series('clean:dist', 'build:main', 'build:renderer')); -gulp.task('build:watch', gulp.series('clean:dist', 'build:renderer:html', 'build:main:watch', 'build:renderer:watch')); +gulp.task('build:watch', gulp.series( + 'clean:dist', 'server:create:dev', 'build:renderer:html', 'build:main:watch', 'build:renderer:watch' +)); gulp.task('cucumber', shell.task('npm run cucumber --')); @@ -67,28 +124,8 @@ gulp.task('purge:translations', shell.task('rimraf ./translations/messages/sourc gulp.task('electron:inspector', shell.task('npm run electron:inspector')); -gulp.task('electron:restart', (done) => { - electronServer.restart(); - done(); -}); - -gulp.task('electron:reload', (done) => { - electronServer.reload(); - done(); -}); - -gulp.task('start:watch', () => { - createElectronServer({ NODE_ENV: process.env.NODE_ENV || 'development' }); - startElectronInWatchMode(); -}); - -gulp.task('start:debug', () => { - createElectronServer({ NODE_ENV: 'development' }, ['--inspect', '--inspect-brk']); - startElectronInWatchMode(); -}); - gulp.task('start', shell.task(`cross-env NODE_ENV=${process.env.NODE_ENV || 'production'} electron ./`)); -gulp.task('dev', gulp.series('build:watch', 'start:watch')); +gulp.task('dev', gulp.series('server:create:dev', 'build:watch', 'server:start')); -gulp.task('debug', gulp.series('build:watch', 'start:debug', 'electron:inspector')); +gulp.task('debug', gulp.series('server:create:debug', 'build:watch', 'server:start', 'electron:inspector')); diff --git a/installers/README.md b/installers/README.md index fd9576262c..efb48a6789 100644 --- a/installers/README.md +++ b/installers/README.md @@ -17,7 +17,7 @@ The Dhall expressions that comprise the runtime configuration are thus composed - `launcher.dhall` -- top level expression defining the launcher configuration YAML file - `topology.dhall` -- top level expression defining the wallet topology YAML file - `{linux64,macos64,win64}.dhall` - - `{mainnet,staging}.dhall` + - `{mainnet,staging,testnet}.dhall` The set of clusters (currently `mainnet` and `staging`) that the build scripts (`scripts/build-installer-*`) will build installers for is enumerated in diff --git a/installers/Spec.hs b/installers/Spec.hs index 2f41a7f390..8c7b3cb154 100644 --- a/installers/Spec.hs +++ b/installers/Spec.hs @@ -45,7 +45,10 @@ macBuildSpec = do } liftIO $ do - Mac.withDir installersDir $ Mac.main opts + withDir installersDir $ do + mktree "../release/darwin-x64/Daedalus-darwin-x64/Daedalus.app/Contents/Resources/app" + writeFile "../release/darwin-x64/Daedalus-darwin-x64/Daedalus.app/Contents/Resources/app/package.json" "{}" + Mac.main opts -- there should be an installer file at the end fold (ls out) Fold.length `shouldReturn` 1 diff --git a/installers/common/Config.hs b/installers/common/Config.hs index 4f714791a1..b541dd6e3f 100644 --- a/installers/common/Config.hs +++ b/installers/common/Config.hs @@ -119,7 +119,7 @@ optionsParser detectedOS = Options <*> (fromMaybe detectedOS <$> (optional $ optReadLower "os" 's' "OS, defaults to host OS. One of: linux64 macos64 win64")) <*> (fromMaybe Mainnet <$> (optional $ - optReadLower "cluster" 'c' "Cluster the resulting installer will target: mainnet or staging")) + optReadLower "cluster" 'c' "Cluster the resulting installer will target: mainnet, staging, or testnet")) <*> (fromMaybe "daedalus" <$> (optional $ (AppName <$> optText "appname" 'n' "Application name: daedalus or.."))) <*> optPath "out-dir" 'o' "Installer output directory" diff --git a/installers/common/MacInstaller.hs b/installers/common/MacInstaller.hs index dfdf66abf6..a370c6c1ba 100644 --- a/installers/common/MacInstaller.hs +++ b/installers/common/MacInstaller.hs @@ -13,7 +13,6 @@ module MacInstaller , run , run' , readCardanoVersionFile - , withDir ) where --- @@ -39,7 +38,7 @@ import Turtle.Line (unsafeTextToLine) import Config import RewriteLibs (chain) import Types -import Util (exportBuildVars) +import Util (exportBuildVars, rewritePackageJson) data DarwinConfig = DarwinConfig { dcAppNameApp :: Text -- ^ Daedalus.app for example @@ -69,7 +68,7 @@ main opts@Options{..} = do exportBuildVars opts installerConfig ver buildIcons oCluster - appRoot <- buildElectronApp darwinConfig + appRoot <- buildElectronApp darwinConfig installerConfig makeComponentRoot opts appRoot darwinConfig daedalusVer <- getDaedalusVersion "../package.json" @@ -122,14 +121,16 @@ buildIcons cluster = do -- component root path. -- NB: If webpack scripts are changed then this function may need to -- be updated. -buildElectronApp :: DarwinConfig -> IO FilePath -buildElectronApp darwinConfig@DarwinConfig{..} = do +buildElectronApp :: DarwinConfig -> InstallerConfig -> IO FilePath +buildElectronApp darwinConfig@DarwinConfig{..} installerConfig = do withDir ".." . sh $ npmPackage darwinConfig let formatter :: Format r (Text -> Text -> r) formatter = "../release/darwin-x64/" % s % "-darwin-x64/" % s - pure $ fromString $ T.unpack $ format formatter dcAppName dcAppNameApp + pathtoapp = format formatter dcAppName dcAppNameApp + rewritePackageJson (T.unpack $ pathtoapp <> "/Contents/Resources/app/package.json") (installDirectory installerConfig) + pure $ fromString $ T.unpack $ pathtoapp npmPackage :: DarwinConfig -> Shell () npmPackage DarwinConfig{..} = do diff --git a/installers/common/Types.hs b/installers/common/Types.hs index 34360121c6..697e502db8 100644 --- a/installers/common/Types.hs +++ b/installers/common/Types.hs @@ -57,6 +57,7 @@ data Cluster = Mainnet | Staging | Testnet + | Demo deriving (Bounded, Enum, Eq, Read, Show) -- | The wallet backend to include in the installer. @@ -112,6 +113,7 @@ clusterNetwork :: Cluster -> Text clusterNetwork Mainnet = "mainnet" clusterNetwork Staging = "staging" clusterNetwork Testnet = "testnet" +clusterNetwork Demo = "demo" packageFileName :: OS -> Cluster -> Version -> Backend -> Text -> Maybe BuildJob -> FilePath packageFileName os cluster ver backend backendVer build = fromText name <.> ext diff --git a/installers/common/Util.hs b/installers/common/Util.hs index 434e2b24fd..6ac9dd2b5a 100644 --- a/installers/common/Util.hs +++ b/installers/common/Util.hs @@ -4,8 +4,12 @@ module Util where import Control.Monad (mapM_) import Data.Text (Text) +import qualified Data.ByteString as BS +import qualified Data.ByteString.Lazy as BSL import System.Directory (listDirectory, withCurrentDirectory, removeDirectory, removeFile, doesDirectoryExist) import Turtle (export, format, d) +import Data.Aeson (Value, decodeStrict', FromJSON, Value(Object, String), ToJSON, encode) +import qualified Data.HashMap.Strict as HM import Config (Options(..), Backend(..)) import Types (InstallerConfig(walletPort), fromBuildJob, clusterNetwork) @@ -37,3 +41,18 @@ exportBuildVars Options{oBackend, oBuildJob, oCluster} cfg backendVersion = do where apiName (Cardano _) = "ada" apiName Mantis = "etc" + +decodeFileStrict' :: FromJSON a => FilePath -> IO (Maybe a) +decodeFileStrict' = fmap decodeStrict' . BS.readFile + +encodeFile :: ToJSON a => FilePath -> a -> IO () +encodeFile fp = BSL.writeFile fp . encode + +rewritePackageJson :: FilePath -> Text -> IO () +rewritePackageJson path name = do + rawObject <- (decodeFileStrict' path :: IO (Maybe Value)) + newObject <- case rawObject of + Just (Object hashmap) -> do + pure $ Object $ HM.insert "productName" (String name) hashmap + _ -> error "invalid package.json detected" + encodeFile path newObject diff --git a/installers/common/WindowsInstaller.hs b/installers/common/WindowsInstaller.hs index 3afceffe22..b7d6c36719 100644 --- a/installers/common/WindowsInstaller.hs +++ b/installers/common/WindowsInstaller.hs @@ -51,6 +51,7 @@ writeUninstallerNSIS (Version fullVersion) installerConfig = do _ <- constantStr "InstallDir" (str $ unpack $ installDirectory installerConfig) name "$InstallDir Uninstaller $Version" outFile . str . encodeString $ tempDir "tempinstaller.exe" + unsafeInjectGlobal "Unicode true" unsafeInjectGlobal "!addplugindir \"nsis_plugins\\liteFirewall\\bin\"" unsafeInjectGlobal "SetCompress off" _ <- section "" [Required] $ do @@ -73,6 +74,10 @@ writeUninstallerNSIS (Version fullVersion) installerConfig = do -- See non-INNER blocks at http://nsis.sourceforge.net/Signing_an_Uninstaller signUninstaller :: Options -> IO SigningResult signUninstaller opts = do + rawnsi <- readFile "uninstaller.nsi" + putStr rawnsi + IO.hFlush IO.stdout + procs "C:\\Program Files (x86)\\NSIS\\makensis" ["uninstaller.nsi"] mempty tempDir <- getTempDir writeTextFile "runtempinstaller.bat" $ format (fp%" /S") (tempDir "tempinstaller.exe") @@ -192,11 +197,12 @@ writeInstallerNSIS outName (Version fullVersion') installerConfig clusterName = lshow :: Show a => a -> String lshow = T.unpack . lshowText -packageFrontend :: Cluster -> IO () -packageFrontend cluster = do +packageFrontend :: Cluster -> InstallerConfig -> IO () +packageFrontend cluster installerConfig = do let icon = format ("installers/icons/"%s%"/"%s) (lshowText cluster) (lshowText cluster) export "NODE_ENV" "production" shells ("npm run package -- --icon " <> icon) empty + rewritePackageJson "../release/win32-x64/Daedalus-win32-x64/resources/app/package.json" (installDirectory installerConfig) -- | The contract of `main` is not to produce unsigned installer binaries. main :: Options -> IO () @@ -214,7 +220,7 @@ main opts@Options{..} = do echo "Packaging frontend" exportBuildVars opts installerConfig ver - packageFrontend oCluster + packageFrontend oCluster installerConfig let fullName = packageFileName Win64 oCluster fullVersion oBackend ver oBuildJob diff --git a/installers/daedalus-installer.cabal b/installers/daedalus-installer.cabal index 679ada3c86..93c969bf62 100644 --- a/installers/daedalus-installer.cabal +++ b/installers/daedalus-installer.cabal @@ -27,6 +27,7 @@ library , Glob , base , bytestring + , unordered-containers , containers , dhall , dhall-json diff --git a/installers/daedalus-installer.nix b/installers/daedalus-installer.nix index fea74187ba..d1f13b59ab 100644 --- a/installers/daedalus-installer.nix +++ b/installers/daedalus-installer.nix @@ -2,8 +2,8 @@ , dhall-json, directory, filepath, foldl, github, Glob, hspec , lens-aeson, managed, megaparsec, microlens, network-uri, nsis , optional-args, optparse-applicative, optparse-generic, split -, stdenv, system-filepath, temporary, text, turtle, universum, wreq -, yaml, zip-archive +, stdenv, system-filepath, temporary, text, turtle, universum +, unordered-containers, wreq, yaml, zip-archive }: mkDerivation { pname = "daedalus-installer"; @@ -14,7 +14,8 @@ mkDerivation { libraryHaskellDepends = [ aeson base bytestring containers dhall dhall-json directory github Glob lens-aeson megaparsec microlens network-uri nsis optional-args - system-filepath text turtle universum wreq yaml zip-archive + system-filepath text turtle universum unordered-containers wreq + yaml zip-archive ]; executableHaskellDepends = [ aeson base bytestring containers dhall dhall-json directory diff --git a/installers/dhall/cluster.type b/installers/dhall/cluster.type index 784e437e3c..72faf41c0a 100644 --- a/installers/dhall/cluster.type +++ b/installers/dhall/cluster.type @@ -6,4 +6,5 @@ , installDirectorySuffix : Text , macPackageSuffix : Text , walletPort : Integer +, extraNodeArgs : List Text } diff --git a/installers/dhall/demo.dhall b/installers/dhall/demo.dhall new file mode 100644 index 0000000000..bf920a3ca7 --- /dev/null +++ b/installers/dhall/demo.dhall @@ -0,0 +1,10 @@ +{ name = "demo" +, keyPrefix = "TODO, default without the suffix" +, relays = "TODO, ip support missing" +, updateServer = "https://disabled.iohkdev.io" +, reportServer = "http://staging-report-server.awstest.iohkdev.io:8080" +, installDirectorySuffix = " Demo" +, macPackageSuffix = "Demo" +, walletPort = 8092 +, extraNodeArgs = [ "--metrics", "--ekg-server", "localhost:8083", "+RTS", "-T", "-RTS" ] : List Text +} diff --git a/installers/dhall/launcher.dhall b/installers/dhall/launcher.dhall index bd89fa8ce5..f7888c49d0 100644 --- a/installers/dhall/launcher.dhall +++ b/installers/dhall/launcher.dhall @@ -17,13 +17,14 @@ , "--tlscert", "${os.nodeArgs.tlsPath}/server/server.crt" , "--tlskey", "${os.nodeArgs.tlsPath}/server/server.key" , "--no-client-auth" + , "--log-console-off" , "--update-server", cluster.updateServer , "--keyfile", os.nodeArgs.keyfile , "--topology", os.nodeArgs.topology , "--wallet-db-path", os.nodeArgs.walletDBPath , "--update-latest-path", os.nodeArgs.updateLatestPath - , "--wallet-address", "127.0.0.1:${Integer/show cluster.walletPort}" + , "--wallet-address", "127.0.0.1:0" -- XXX: this is a workaround for Linux , "--update-with-package" - ] + ] # cluster.extraNodeArgs } // os.pass diff --git a/installers/dhall/linux64.dhall b/installers/dhall/linux64.dhall index 6cb2f3459f..fa0015ccd3 100644 --- a/installers/dhall/linux64.dhall +++ b/installers/dhall/linux64.dhall @@ -11,7 +11,7 @@ in , logsPrefix = "${dataDir}/Logs" , topology = "\${DAEDALUS_CONFIG}/wallet-topology.yaml" , updateLatestPath = "${dataDir}/installer.sh" - , walletDBPath = "${dataDir}/Wallet/" + , walletDBPath = "${dataDir}/Wallet" , tlsPath = "${dataDir}/tls" } , pass = @@ -21,10 +21,9 @@ in , nodeDbPath = "${dataDir}/DB/" , nodeLogConfig = "\${DAEDALUS_CONFIG}/log-config-prod.yaml" , nodeLogPath = [] : Optional Text - , walletPath = "daedalus-frontend" , walletLogging = False - , frontendOnlyMode = False + , frontendOnlyMode = True -- todo, find some way to disable updates when unsandboxed? , updaterPath = "/bin/update-runner" diff --git a/installers/dhall/macos64.dhall b/installers/dhall/macos64.dhall index 99e5e6b1e5..614234d992 100644 --- a/installers/dhall/macos64.dhall +++ b/installers/dhall/macos64.dhall @@ -26,7 +26,7 @@ in , walletPath = "\${DAEDALUS_INSTALL_DIRECTORY}/Frontend" , walletLogging = True - , frontendOnlyMode = False + , frontendOnlyMode = True , updaterPath = "/usr/bin/open" , updaterArgs = ["-FW"] diff --git a/installers/dhall/mainnet.dhall b/installers/dhall/mainnet.dhall index fc7bb1a92f..132a57daf1 100644 --- a/installers/dhall/mainnet.dhall +++ b/installers/dhall/mainnet.dhall @@ -6,4 +6,5 @@ , installDirectorySuffix = "" , macPackageSuffix = "" , walletPort = 8090 +, extraNodeArgs = [] : List Text } diff --git a/installers/dhall/staging.dhall b/installers/dhall/staging.dhall index 529cea61fc..ab10a3d31f 100644 --- a/installers/dhall/staging.dhall +++ b/installers/dhall/staging.dhall @@ -6,4 +6,5 @@ , installDirectorySuffix = " Staging" , macPackageSuffix = "Staging" , walletPort = 8092 +, extraNodeArgs = [ "--metrics", "--ekg-server", "localhost:8082", "+RTS", "-T", "-RTS" ] : List Text } diff --git a/installers/dhall/testnet.dhall b/installers/dhall/testnet.dhall index d3cdd88bdf..c451c60350 100644 --- a/installers/dhall/testnet.dhall +++ b/installers/dhall/testnet.dhall @@ -6,4 +6,5 @@ , installDirectorySuffix = " Testnet" , macPackageSuffix = "Testnet" , walletPort = 8094 +, extraNodeArgs = [ "--metrics", "--ekg-server", "localhost:8081", "+RTS", "-T", "-RTS" ] : List Text } diff --git a/installers/dhall/win64.dhall b/installers/dhall/win64.dhall index 1b1981898f..7a104cc14b 100644 --- a/installers/dhall/win64.dhall +++ b/installers/dhall/win64.dhall @@ -27,7 +27,7 @@ in , walletPath = "\${DAEDALUS_DIR}\\Daedalus.exe" , walletLogging = True - , frontendOnlyMode = False + , frontendOnlyMode = True , updaterPath = "Installer.exe" , updaterArgs = [] : List Text diff --git a/installers/nix/linux.nix b/installers/nix/linux.nix index 8bf1b8ca27..752a18f988 100644 --- a/installers/nix/linux.nix +++ b/installers/nix/linux.nix @@ -24,7 +24,7 @@ let cd "''${DAEDALUS_DIR}/${cluster}/" - exec ${electron}/bin/electron ${rawapp}/share/daedalus/main/ "$@" + exec ${electron}/bin/electron ${rawapp}/share/daedalus "$@" ''; daedalus = writeScriptBin "daedalus" '' #!${stdenv.shell} diff --git a/installers/nix/nix-installer.nix b/installers/nix/nix-installer.nix index 9f9aa1c1d4..4060a5e68c 100644 --- a/installers/nix/nix-installer.nix +++ b/installers/nix/nix-installer.nix @@ -40,7 +40,7 @@ let bash "$1" --extract ls -ltrh dat/nix/store/*-tarball/tarball/tarball.tar.xz UNPACK2=$(mktemp -d) - tar -C $UNPACK2 -xf dat/nix/store/*-tarball/tarball/tarball.tar.xz + tar --delay-directory-restore -C $UNPACK2 -xf dat/nix/store/*-tarball/tarball/tarball.tar.xz cd rmrf $UNPACK ls -ltrh $UNPACK2 @@ -125,7 +125,7 @@ let UNPACK=$(mktemp -d) - pv $TARPATH | unxz | tar -x -C $UNPACK + pv $TARPATH | unxz | tar --delay-directory-restore -x -C $UNPACK NIX_REMOTE=local?root=$UNPACK nix-store --load-db < $UNPACK/nix-path-registration pwd diff --git a/package.json b/package.json index 38f152fe55..078e3777a1 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "daedalus", "productName": "Daedalus", - "version": "0.11.2", + "version": "0.12.0", "description": "Cryptocurrency Wallet", "main": "./dist/main/index.js", "scripts": { "build": "gulp build", "start": "gulp start", - "start:watch": "gulp start:watch", + "start:dev": "NODE_ENV=development gulp start", "dev": "gulp dev", "test": "gulp test", "test:watch": "gulp test:watch", @@ -20,16 +20,22 @@ "cucumber:watch": "npm run cucumber -- --tags @watch", "lint": "eslint --format=node_modules/eslint-formatter-pretty source features storybook *.js", "flow:test": "flow; test $? -eq 0 -o $? -eq 2", - "manage:translations": "gulp purge:translations && gulp clear-cache && gulp build && node ./translations/translation-runner.js", + "manage:translations": "gulp purge:translations && gulp clear:cache && gulp build && node ./translations/translation-runner.js", "storybook": "start-storybook -p 6006 -c storybook", - "clear:cache": "gulp clear-cache" + "clear:cache": "gulp clear:cache", + "nix:dev": "nix-shell --arg autoStartBackend true --arg systemStart", + "nix:staging": "nix-shell --arg autoStartBackend true --argstr cluster staging" }, "bin": { "electron": "./node_modules/.bin/electron" }, "devDependencies": { - "@storybook/addon-actions": "3.2.18", - "@storybook/react": "3.2.18", + "@storybook/addon-actions": "3.4.7", + "@storybook/addon-knobs": "3.4.7", + "@storybook/addon-links": "3.4.7", + "@storybook/addon-notes": "3.4.7", + "@storybook/addons": "3.4.7", + "@storybook/react": "3.4.7", "asar": "0.14.0", "autodll-webpack-plugin": "0.3.8", "babel-cli": "6.26.0", @@ -75,7 +81,8 @@ "file-loader": "1.1.5", "flow-bin": "0.60.1", "gulp-shell": "0.6.5", - "hard-source-webpack-plugin": "0.6.4", + "hard-source-webpack-plugin": "0.8.1", + "hash.js": "1.1.3", "html-loader": "0.5.1", "json-loader": "0.5.7", "markdown-loader": "2.0.1", @@ -120,6 +127,7 @@ "form-data": "2.3.1", "gulp": "4.0.0", "humanize-duration": "3.12.0", + "lockfile": "1.0.4", "lodash": "4.17.10", "mkdirp": "0.5.1", "mobx": "3.1.7", @@ -131,18 +139,18 @@ "pdf.js-extract": "0.0.9", "pdfkit": "0.8.3", "prop-types": "15.6.1", + "ps-list": "^5.0.0", "qr-image": "3.2.0", "qrcode.react": "0.6.1", "react": "15.6.2", "react-addons-css-transition-group": "15.6.2", "react-copy-to-clipboard": "5.0.1", - "react-css-themr": "2.1.2", "react-dom": "15.6.2", "react-dropzone": "4.2.3", "react-intl": "2.4.0", "react-markdown": "3.1.0", "react-number-format": "3.0.3", - "react-polymorph": "0.6.5", + "react-polymorph": "0.7.2", "react-router": "3.0.3", "react-svg-inline": "2.1.0", "recharts": "1.0.0-beta.10", @@ -153,10 +161,15 @@ "split-file": "2.1.0", "unorm": "1.4.1", "validator": "9.1.2", - "web3-utils": "1.0.0-beta.30" + "web3-utils": "1.0.0-beta.30", + "graceful-fs": "4.1.11", + "retry": "0.12.0" + }, + "resolutions": { + "eslint-scope": "3.7.3" }, "devEngines": { "node": "8.x", - "npm": "5.x || 6.x" + "yarn": "^1.7.0" } } diff --git a/scripts/build-installer-unix.sh b/scripts/build-installer-unix.sh index f3f206b2c1..d31938106a 100755 --- a/scripts/build-installer-unix.sh +++ b/scripts/build-installer-unix.sh @@ -18,7 +18,7 @@ usage() { Options: --clusters "[CLUSTER-NAME...]" - Build installers for CLUSTERS. Defaults to "mainnet staging" + Build installers for CLUSTERS. Defaults to "mainnet staging testnet" --fast-impure Fast, impure, incremental build --build-id BUILD-NO Identifier of the build; defaults to '0' diff --git a/scripts/nix-shell.sh b/scripts/nix-shell.sh deleted file mode 100755 index b0d0f8028e..0000000000 --- a/scripts/nix-shell.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/sh -# sh is really bash in this case -# shellcheck disable=SC2039 - -set -x - -case $(uname -s | tr '[:upper:]' '[:lower:]') in - linux ) NIX_PROFILE_BINPATH=/run/current-system/sw/bin/;; - darwin ) NIX_PROFILE_BINPATH=/nix/var/nix/profiles/default/bin;; - * ) echo "Unsupported OS: $OS_NAME" >&2; exit 1;; -esac - -# Bootstrap on OS X/NixOS: - -NIX_BUILD="$(type -P nix-build)" -NIX_BUILD="${NIX_BUILD:-$NIX_PROFILE_BINPATH/nix-build}" -NIX_SHELL="$(type -P nix-shell)" -NIX_SHELL="${NIX_SHELL:-$NIX_PROFILE_BINPATH/nix-shell}" -NIX_BUILD_SHELL="$(type -P bash)" -NIX_BUILD_SHELL="${NIX_BUILD_SHELL:-$NIX_PROFILE_BINPATH/bash}" - -export NIX_REMOTE=daemon -NIX_PATH="nixpkgs=$(${NIX_BUILD} fetch-nixpkgs.nix -o nixpkgs)" -export NIX_PATH -export NIX_BUILD_SHELL - -${NIX_SHELL} -p nix bash binutils coreutils curl "$@" diff --git a/shell.nix b/shell.nix index 66044579b6..83cff9f7eb 100644 --- a/shell.nix +++ b/shell.nix @@ -4,31 +4,156 @@ in { system ? builtins.currentSystem , config ? {} , pkgs ? (import (localLib.fetchNixPkgs) { inherit system config; }) +, cluster ? "demo" +, systemStart ? null +, autoStartBackend ? systemStart != null +, walletExtraArgs ? [] +, allowFaultInjection ? false +, purgeNpmCache ? false }: let - daedalusShell = pkgs.stdenv.mkDerivation { + yaml2json = pkgs.haskell.lib.disableCabalFlag pkgs.haskellPackages.yaml "no-exe"; + daedalusPkgs = import ./. { inherit cluster; }; + yarn = pkgs.yarn.override { inherit nodejs; }; + nodejs = pkgs.nodejs-8_x; + launcher-json = pkgs.runCommand "read-launcher-config.json" { buildInputs = [ yaml2json ]; } "yaml2json ${daedalusPkgs.daedalus.cfg}/etc/launcher-config.yaml > $out"; + launcher-config = builtins.fromJSON (builtins.readFile launcher-json); + fullExtraArgs = walletExtraArgs ++ pkgs.lib.optional allowFaultInjection "--allow-fault-injection"; + patches = builtins.concatLists [ + (pkgs.lib.optional (systemStart != null) ".configuration.systemStart = ${toString systemStart}") + (pkgs.lib.optional (cluster == "demo") ''.configuration.key = "default"'') + (pkgs.lib.optional (fullExtraArgs != []) ''.nodeArgs += ${builtins.toJSON fullExtraArgs}'') + ]; + patchesString = pkgs.lib.concatStringsSep " | " patches; + launcherYamlWithStartTime = pkgs.runCommand "launcher-config.yaml" { buildInputs = [ pkgs.jq yaml2json ]; } '' + jq '${patchesString}' < ${launcher-json} | json2yaml > $out + echo "Launcher config: $out" + ''; + launcherConfig' = if (patches == []) then "${daedalusPkgs.daedalus.cfg}/etc/launcher-config.yaml" else launcherYamlWithStartTime; + fixYarnLock = pkgs.stdenv.mkDerivation { + name = "fix-yarn-lock"; + buildInputs = [ nodejs yarn pkgs.git ]; + shellHook = '' + git diff > pre-yarn.diff + yarn + git diff > post-yarn.diff + diff pre-yarn.diff post-yarn.diff > /dev/null + if [ $? != 0 ] + then + echo "Changes by yarn have been made. Please commit them." + else + echo "No changes were made." + fi + rm pre-yarn.diff post-yarn.diff + exit + ''; + }; + demoTopology = { + wallet = { + fallbacks = 7; + valency = 1; + relays = [ + [ { addr = "127.0.0.1"; port = 3100; } ] + ]; + }; + }; + demoTopologyYaml = pkgs.runCommand "wallet-topology.yaml" { buildInputs = [ pkgs.jq yaml2json ]; } '' + cat ${builtins.toFile "wallet-topology.json" (builtins.toJSON demoTopology)} | json2yaml > $out + ''; + demoConfig = pkgs.runCommand "new-config" {} '' + mkdir $out + cp ${daedalusPkgs.daedalus.cfg}/etc/* $out/ + rm $out/wallet-topology.yaml + cp ${demoTopologyYaml} $out/wallet-topology.yaml + ''; + daedalusShell = pkgs.stdenv.mkDerivation (rec { name = "daedalus"; - passthru = { inherit daedalus; }; - - buildInputs = with pkgs; [ + buildInputs = [ nodejs yarn ] ++ (with pkgs; [ nix bash binutils coreutils curl gnutar - git python27 curl electron nodejs-8_x + git python27 curl electron nodePackages.node-gyp nodePackages.node-pre-gyp - gnumake yarn - ]; - shellHook = '' + gnumake + chromedriver + ] ++ (localLib.optionals autoStartBackend [ + daedalusPkgs.daedalus-bridge + ])); + LAUNCHER_CONFIG = launcherConfig'; + DAEDALUS_CONFIG = if (cluster == "demo") then demoConfig else "${daedalusPkgs.daedalus.cfg}/etc/"; + DAEDALUS_INSTALL_DIRECTORY = "./"; + DAEDALUS_DIR = DAEDALUS_INSTALL_DIRECTORY; + CLUSTER = cluster; + shellHook = let + secretsDir = if pkgs.stdenv.isLinux then "Secrets" else "Secrets-1.0"; + systemStartString = builtins.toString systemStart; + in '' + warn() { + (echo "###"; echo "### WARNING: $*"; echo "###") >&2 + } + if ! test -x "$(type -P cardano-node)" + then warn "cardano-node not in $PATH"; fi + if test -z "${systemStartString}" + then warn "--arg systemStart wasn't passed, cardano won't be able to connect to the demo cluster!" + elif test "${systemStartString}" -gt $(date +%s) + then warn "--arg systemStart is in future, cardano PROBABLY won't be able to connect to the demo cluster!" + elif test "${systemStartString}" -lt $(date -d '12 hours ago' +%s) + then warn "--arg systemStart is in 12 hours in the past, unless there is a cluster running with this systemStart cardano won't be able to connect to the demo cluster!" + fi + ${localLib.optionalString pkgs.stdenv.isLinux "export XDG_DATA_HOME=$HOME/.local/share"} + cp -f ${daedalusPkgs.iconPath.${cluster}.small} $DAEDALUS_INSTALL_DIRECTORY/icon.png + ln -svf $(type -P cardano-node) + ${pkgs.lib.optionalString autoStartBackend '' + for x in wallet-topology.yaml log-config-prod.yaml configuration.yaml mainnet-genesis-dryrun-with-stakeholders.json ; do + ln -svf ${daedalusPkgs.daedalus.cfg}/etc/$x + done + ${pkgs.lib.optionalString (cluster == "demo") '' + ln -svf ${demoTopologyYaml} wallet-topology.yaml + if [[ -f "${launcher-config.statePath}/system-start" && "${systemStartString}" == $(cat "${launcher-config.statePath}/system-start") ]] + then + echo "running pre-existing demo cluster matching system start: ${systemStartString}" + else + echo "removing pre-existing demo cluster because system-start differs or doesn't exist" + rm -rf "${launcher-config.statePath}" + mkdir -p "${launcher-config.statePath}" + echo -n ${systemStartString} > "${launcher-config.statePath}/system-start" + fi + ''} + mkdir -p "${launcher-config.statePath}/${secretsDir}" + ''} + ${localLib.optionalString autoStartBackend '' + mkdir -p "${launcher-config.tlsPath}/server" "${launcher-config.tlsPath}/client" + cardano-x509-certificates \ + --server-out-dir "${launcher-config.tlsPath}/server" \ + --clients-out-dir "${launcher-config.tlsPath}/client" \ + --configuration-file ${daedalusPkgs.daedalus.cfg}/etc/configuration.yaml \ + --configuration-key mainnet_dryrun_full + echo ${launcher-config.tlsPath} + '' + } + export DAEDALUS_INSTALL_DIRECTORY + export NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -I${nodejs}/include/node" + ${localLib.optionalString purgeNpmCache '' + warn "purging all NPM/Yarn caches" + rm -rf node_modules + yarn cache clean + npm cache clean --force + '' + } yarn install - ln -svf ${pkgs.electron}/bin/electron ./node_modules/electron/dist/electron - echo "Instructions:" - echo "In cardano repo run scripts/launch/demo-nix.sh" - echo "export CARDANO_TLS_PATH=/path/to/cardano-sl/state-demo/tls/client" + ln -svf ${pkgs.electron}/bin/electron ./node_modules/electron/dist/electron + ln -svf ${pkgs.chromedriver}/bin/chromedriver ./node_modules/electron-chromedriver/bin/chromedriver + ${localLib.optionalString (! autoStartBackend) '' + echo "Instructions for manually running cardano-node:" + echo "DEPRECATION NOTICE: This should only be used for debugging a specific revision of cardano. Use --autoStartBackend --system-start SYSTEM_START_TIME as parameters to this script to auto-start the wallet" + echo "In cardano repo run scripts/launch/demo-nix.sh -w" + echo "export CARDANO_TLS_PATH=/path/to/cardano-sl/state-demo/tls/wallet" echo "yarn dev" + ''} ''; - }; + }); daedalus = daedalusShell.overrideAttrs (oldAttrs: { shellHook = '' - if [ ! -f "$CARDANO_TLS_PATH/ca.crt" ] + if [ ! -f "$CARDANO_TLS_PATH/ca.crt" ] || [ ! -f "tls/client/ca.crt" ] then echo "CARDANO_TLS_PATH must be set" exit 1 @@ -38,4 +163,4 @@ let exit 0 ''; }); -in daedalusShell +in daedalusShell // { inherit fixYarnLock; } diff --git a/source/common/environment.js b/source/common/environment.js index c71a6e29e9..bb249ddcfd 100644 --- a/source/common/environment.js +++ b/source/common/environment.js @@ -1,12 +1,12 @@ // @flow import os from 'os'; -import { uniq } from 'lodash'; +import { uniq, upperFirst } from 'lodash'; import { version } from '../../package.json'; // Only require electron / remote if we are in a node.js environment let remote; -if (module && module.require) { - remote = module.require('electron').remote; +if (process.version !== '' && process.release !== undefined && process.release.name === 'node') { + remote = require('electron').remote; } const osNames = { @@ -24,24 +24,23 @@ const environment = Object.assign({ TEST: 'test', PRODUCTION: 'production', NETWORK: process.env.NETWORK || 'development', - API: process.env.API || 'ada', API_VERSION, MOBX_DEV_TOOLS: process.env.MOBX_DEV_TOOLS, current: process.env.NODE_ENV || 'development', REPORT_URL: process.env.REPORT_URL || 'http://staging-report-server.awstest.iohkdev.io:8080/', - WALLET_PORT: parseInt(process.env.WALLET_PORT || '8090', 10), isDev: () => environment.current === environment.DEVELOPMENT, isTest: () => environment.current === environment.TEST, isProduction: () => environment.current === environment.PRODUCTION, isMainnet: () => environment.NETWORK === 'mainnet', isStaging: () => environment.NETWORK === 'staging', isTestnet: () => environment.NETWORK === 'testnet', - isAdaApi: () => environment.API === 'ada', - isEtcApi: () => environment.API === 'etc', + isDevelopment: () => environment.NETWORK === 'development', build, buildNumber: uniq([API_VERSION, build]).join('.'), getBuildLabel: () => { - let buildLabel = `Daedalus (${environment.version}#${environment.buildNumber})`; + const networkLabel = !(environment.isMainnet() || environment.isDevelopment()) ? + ` ${upperFirst(environment.NETWORK)}` : ''; + let buildLabel = `Daedalus${networkLabel} (${environment.version}#${environment.buildNumber})`; if (!environment.isProduction()) buildLabel += ` ${environment.current}`; return buildLabel; }, diff --git a/source/common/fileName.js b/source/common/fileName.js new file mode 100644 index 0000000000..a6f8926414 --- /dev/null +++ b/source/common/fileName.js @@ -0,0 +1,13 @@ +// @flow +import moment from 'moment'; + +export const generateFileNameWithTimestamp = (prefix: string = 'logs', fileType: string = 'zip') => + `${prefix}-${moment.utc().format('YYYY-MM-DDTHHmmss.0SSS')}Z.${fileType}`; + +export const isFileNameWithTimestamp = (prefix: string = 'logs', fileType: string = 'zip') => (fileName: string) => + fileName.match(RegExp(`(${prefix}-)([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{6}.0[0-9]{3}Z)(.${fileType})`)); + +export const getPathSlash = (path: string) => ((path.indexOf('/') > -1) ? '/' : '\\'); + +export const extractFileNameFromPath = (path: string) => + path.substr(path.lastIndexOf(getPathSlash(path)) + 1); diff --git a/source/common/ipc-api.js b/source/common/ipc-api.js index 5e9f321220..699604246f 100644 --- a/source/common/ipc-api.js +++ b/source/common/ipc-api.js @@ -20,13 +20,6 @@ export const COMPRESS_LOGS = { ERROR: `${COMPRESS_LOGS_CHANNEL}-error`, }; -const DELETE_COMPRESSED_LOGS_CHANNEL = 'delete-compressed-logs'; -export const DELETE_COMPRESSED_LOGS = { - REQUEST: DELETE_COMPRESSED_LOGS_CHANNEL, - SUCCESS: `${DELETE_COMPRESSED_LOGS_CHANNEL}-success`, - ERROR: `${DELETE_COMPRESSED_LOGS_CHANNEL}-error`, -}; - const DOWNLOAD_LOGS_CHANNEL = 'download-logs'; export const DOWNLOAD_LOGS = { REQUEST: DOWNLOAD_LOGS_CHANNEL, @@ -40,3 +33,4 @@ export const GET_GPU_STATUS = { SUCCESS: `${GET_GPU_STATUS_CHANNEL}-success`, ERROR: `${GET_GPU_STATUS_CHANNEL}-error`, }; + diff --git a/source/common/ipc/cardano.ipc.js b/source/common/ipc/cardano.ipc.js new file mode 100644 index 0000000000..123f603e09 --- /dev/null +++ b/source/common/ipc/cardano.ipc.js @@ -0,0 +1,5 @@ +// @flow +export const CARDANO_AWAIT_UPDATE_CHANNEL = 'CARDANO_AWAIT_UPDATE_CHANNEL'; +export const CARDANO_STATE_CHANGE_CHANNEL = 'CARDANO_STATE_CHANGE_CHANNEL'; +export const CARDANO_TLS_CONFIG_CHANNEL = 'CARDANO_TLS_CONFIG_CHANNEL'; +export const CARDANO_RESTART_CHANNEL = 'CARDANO_RESTART_CHANNEL'; diff --git a/source/common/ipc-api/go-to-ada-redemption-screen.js b/source/common/ipc/go-to-ada-redemption-screen.js similarity index 100% rename from source/common/ipc-api/go-to-ada-redemption-screen.js rename to source/common/ipc/go-to-ada-redemption-screen.js diff --git a/source/common/ipc/go-to-network-status-screen.js b/source/common/ipc/go-to-network-status-screen.js new file mode 100644 index 0000000000..98a672170a --- /dev/null +++ b/source/common/ipc/go-to-network-status-screen.js @@ -0,0 +1 @@ +export const GO_TO_NETWORK_STATUS_SCREEN_CHANNEL = 'GO_TO_NETWORK_STATUS_SCREEN'; diff --git a/source/common/ipc/lib/IpcChannel.js b/source/common/ipc/lib/IpcChannel.js new file mode 100644 index 0000000000..4b7bfc5c0a --- /dev/null +++ b/source/common/ipc/lib/IpcChannel.js @@ -0,0 +1,101 @@ +// @flow +import { isString } from 'lodash'; + +export type IpcSender = { + send: (channel: string, ...args: Array) => void +}; + +export type IpcEvent = { + sender: IpcSender, +} + +export type IpcReceiver = { + on: (channel: string, (event: IpcEvent, ...args: Array) => Promise) => void, + once: (channel: string, (event: IpcEvent, isOk: boolean, ...args: Array) => void) => void +}; + +/** + * Provides a coherent, typed api for working with electron + * ipc messages over named channels. Where possible it uses + * promises to reduce the necessary boilerplate for request + * and response cycles. + */ +export class IpcChannel { + + /** + * Each ipc channel should be a singleton (based on the channelName) + * Here we track the created instances. + */ + static _instances = {}; + /** + * The public broadcast channel (any process will receive these messages) + * @private + */ + _broadcastChannel: string; + /** + * The response channel between a main and render process + * @private + */ + _responseChannel: string; + /** + * Sets up the ipc channel and checks that its name is valid. + * Ensures that only one instance per channel name can exist. + * + * @param channelName {String} + */ + constructor(channelName: string) { + if (!isString(channelName) || channelName === '') { + throw new Error(`Invalid channel name ${channelName} provided`); + } + // Enforce the singleton pattern based on the channel name + const existingChannel = IpcChannel._instances[channelName]; + if (existingChannel) return existingChannel; + IpcChannel._instances[channelName] = this; + + this._broadcastChannel = channelName + '-broadcast'; + this._responseChannel = channelName + '-response'; + } + + /** + * Sends a request over ipc to the receiver and waits for the next response on the + * same channel. It returns a promise which is resolved or rejected with the response + * depending on the `isOk` flag set by the respondant. + * + * @param message {Outgoing} + * @param sender {IpcSender} + * @param receiver {IpcReceiver} + * @returns {Promise} + */ + async send(message: Outgoing, sender: IpcSender, receiver: IpcReceiver): Promise { + return new Promise((resolve, reject) => { + sender.send(this._broadcastChannel, message); + // Handle response to the sent request once + receiver.once(this._responseChannel, (event, isOk: boolean, response: Incoming) => { + if (isOk) { + resolve(response); + } else { + reject(response); + } + }); + }); + } + + /** + * Sets up a permanent handler for receiving messages on this channel. + * This should be used to receive messages that are broadcasted by the other end of + * the ipc channel and are not responses to requests sent by this party. + * + * @param receiver {IpcReceiver} + * @param handler + */ + onReceive(handler: (message: Incoming) => Promise, receiver: IpcReceiver): void { + receiver.on(this._broadcastChannel, async (event: IpcEvent, message: Incoming) => { + try { + const response = await handler(message); + event.sender.send(this._responseChannel, true, response); + } catch (error) { + event.sender.send(this._responseChannel, false, error); + } + }); + } +} diff --git a/source/common/ipc-api/load-asset.js b/source/common/ipc/load-asset.js similarity index 100% rename from source/common/ipc-api/load-asset.js rename to source/common/ipc/load-asset.js diff --git a/source/common/ipc-api/open-about-dialog.js b/source/common/ipc/open-about-dialog.js similarity index 100% rename from source/common/ipc-api/open-about-dialog.js rename to source/common/ipc/open-about-dialog.js diff --git a/source/common/logging.js b/source/common/logging.js index 1a38cdfc07..dcda29d54c 100644 --- a/source/common/logging.js +++ b/source/common/logging.js @@ -1,24 +1,26 @@ // @flow import log from 'electron-log'; -export const Logger = { - - debug: (data: string) => { - log.debug(data); - }, - - info: (data: string) => { - log.info(data); - }, +const isRenderer = () => { + const envProcess: Process & { type?: ?string } = process; + // running in a web browser + if (typeof envProcess === 'undefined') return true; + // node-integration is disabled + if (!envProcess) return true; + // We're in node.js somehow + if (!envProcess.type) return false; + return envProcess.type === 'renderer'; +}; - error: (data: string) => { - log.error(data); - }, +const prefixProcessType = (str: string) => (isRenderer() ? '[renderer] ' : '[main] ') + str; - warn: (data: string) => { - log.info(data); - }, +const logToLevel = (level) => (message: string) => log[level](prefixProcessType(message)); +export const Logger = { + debug: logToLevel('debug'), + info: logToLevel('info'), + error: logToLevel('error'), + warn: logToLevel('warn'), }; // ========== STRINGIFY ========= diff --git a/source/common/types/cardanoNode.types.js b/source/common/types/cardanoNode.types.js new file mode 100644 index 0000000000..2afe41460a --- /dev/null +++ b/source/common/types/cardanoNode.types.js @@ -0,0 +1,81 @@ +// @flow +export type TlsConfig = { + port: number, + ca: Uint8Array, + cert: Uint8Array, + key: Uint8Array, +}; + +export type NetworkNames = ( + 'mainnet' | 'staging' | 'testnet' | 'development' | string +); + +export type PlatformNames = ( + 'win32' | 'linux' | 'darwin' | string +); + +export const NetworkNameOptions = { + mainnet: 'mainnet', + staging: 'staging', + testnet: 'testnet', + development: 'development' +}; + +export type CardanoNodeState = ( + 'stopped' | 'starting' | 'running' | 'stopping' | 'updating' | + 'updated' | 'crashed' | 'errored' | 'exiting' | 'unrecoverable' +); + +export const CardanoNodeStates: { + STARTING: CardanoNodeState, + RUNNING: CardanoNodeState; + EXITING: CardanoNodeState; + STOPPING: CardanoNodeState; + STOPPED: CardanoNodeState; + UPDATING: CardanoNodeState; + UPDATED: CardanoNodeState; + CRASHED: CardanoNodeState; + ERRORED: CardanoNodeState; + UNRECOVERABLE: CardanoNodeState; +} = { + STARTING: 'starting', + RUNNING: 'running', + EXITING: 'exiting', + STOPPING: 'stopping', + STOPPED: 'stopped', + UPDATING: 'updating', + UPDATED: 'updated', + CRASHED: 'crashed', + ERRORED: 'errored', + UNRECOVERABLE: 'unrecoverable', +}; + +export type CardanoPidOptions = ( + 'mainnet-PREVIOUS-CARDANO-PID' | + 'staging-PREVIOUS-CARDANO-PID' | + 'testnet-PREVIOUS-CARDANO-PID' | + 'development-PREVIOUS-CARDANO-PID' | + string +); + +export type CardanoNodeStorageKeys = { + PREVIOUS_CARDANO_PID: CardanoPidOptions +}; + +export type CardanoNodeProcessNames = ( + 'cardano-node' | 'cardano-node.exe' +); + +export type ProcessNames = { + CARDANO_PROCESS_NAME: CardanoNodeProcessNames +}; + +export const CardanoProcessNameOptions: { + win32: CardanoNodeProcessNames, + linux: CardanoNodeProcessNames, + darwin: CardanoNodeProcessNames, +} = { + win32: 'cardano-node.exe', + linux: 'cardano-node', + darwin: 'cardano-node' +}; diff --git a/source/main/cardano/CardanoNode.js b/source/main/cardano/CardanoNode.js new file mode 100644 index 0000000000..9b9a2e62ec --- /dev/null +++ b/source/main/cardano/CardanoNode.js @@ -0,0 +1,625 @@ +// @flow +import Store from 'electron-store'; +import type { ChildProcess, spawn, exec } from 'child_process'; +import type { WriteStream } from 'fs'; +import { toInteger } from 'lodash'; +import environment from '../../common/environment'; +import type { CardanoNodeState, TlsConfig } from '../../common/types/cardanoNode.types'; +import { CardanoNodeStates } from '../../common/types/cardanoNode.types'; +import { deriveProcessNames, deriveStorageKeys, getProcess, promisedCondition } from './utils'; + +type Logger = { + debug: (string) => void, + info: (string) => void, + error: (string) => void, +}; + +type Actions = { + spawn: spawn, + exec: exec, + readFileSync: (path: string) => Buffer, + createWriteStream: (path: string, options?: Object) => WriteStream, + broadcastTlsConfig: (config: ?TlsConfig) => void, + broadcastStateChange: (state: CardanoNodeState) => void, +}; + +type StateTransitions = { + onStarting: () => void, + onRunning: () => void, + onStopping: () => void, + onStopped: () => void, + onUpdating: () => void, + onUpdated: () => void, + onCrashed: (code: number, signal: string) => void, + onError: (error: Error) => void, + onUnrecoverable: () => void, +} + +type CardanoNodeIpcMessage = { + Started?: Array, + ReplyPort?: number, +} + +type NodeArgs = Array; + +export type CardanoNodeConfig = { + nodePath: string, // Path to cardano-node executable + logFilePath: string, // Log file path for cardano-sl + tlsPath: string, // Path to cardano-node TLS folder + nodeArgs: NodeArgs, // Arguments that are used to spwan cardano-node + startupTimeout: number, // Milliseconds to wait for cardano-node to startup + startupMaxRetries: number, // Maximum number of retries for re-starting then ode + shutdownTimeout: number, // Milliseconds to wait for cardano-node to gracefully shutdown + killTimeout: number, // Milliseconds to wait for cardano-node to be killed + updateTimeout: number, // Milliseconds to wait for cardano-node to update itself +}; + +const CARDANO_UPDATE_EXIT_CODE = 20; +// grab the current network on which Daedalus is running +const network = String(environment.NETWORK); +const platform = String(environment.platform); +// derive storage keys based on current network +const { PREVIOUS_CARDANO_PID } = deriveStorageKeys(network); +// derive Cardano process name based on current platform +const { CARDANO_PROCESS_NAME } = deriveProcessNames(platform); +// create store for persisting CardanoNode and Daedalus PID's in fs +const store = new Store(); + +export class CardanoNode { + /** + * The config used to spawn cardano-node + * @private + */ + _config: CardanoNodeConfig; + /** + * The managed cardano-node child process + * @private + */ + _node: ?ChildProcess; + + /** + * The ipc channel used for broadcasting messages to the outside world + * @private + */ + _actions: Actions; + + /** + * The ipc channel used for broadcasting messages to the outside world + * @private + */ + _transitionListeners: StateTransitions; + + /** + * Logger instance to print debug messages to + * @private + */ + _log: Logger; + + /** + * Log file stream for cardano-sl + * @private + */ + _cardanoLogFile: WriteStream; + + /** + * The TLS config that is generated by the cardano-node + * on each startup and is broadcasted over ipc channel + * @private + */ + _tlsConfig: ?TlsConfig = null; + + /** + * The current state of the node, used for making decisions + * when events like process crashes happen. + * @type {CardanoNodeState} + * @private + */ + _state: CardanoNodeState = CardanoNodeStates.STOPPED; + + /** + * Number of retries to startup the node (without ever reaching running state) + */ + _startupTries: number = 0; + + /** + * Getter which copies and returns the internal tls config. + * @returns {TlsConfig} + */ + get tlsConfig(): TlsConfig { + return Object.assign({}, this._tlsConfig); + } + + /** + * Getter which returns the PID of the child process of cardano-node + * @returns {TlsConfig} // I think this returns a number... + */ + get pid(): ?number { + return this._node ? this._node.pid : null; + } + + /** + * Getter for the current internal state of the node. + * @returns {CardanoNodeState} + */ + get state(): CardanoNodeState { + return this._state; + } + + /** + * Getter for the number of tried (and failed) startups + * @returns {number} + */ + get startupTries(): number { + return this._startupTries; + } + + /** + * Constructs and prepares the CardanoNode instance for life. + * @param log + * @param actions + * @param transitions + */ + constructor(log: Logger, actions: Actions, transitions: StateTransitions) { + this._log = log; + this._actions = actions; + this._transitionListeners = transitions; + } + + /** + * Starts cardano-node as child process with given config and log file stream. + * Waits up to `startupTimeout` for the process to connect. + * Registers ipc listeners for any necessary process lifecycle events. + * Asks the node to reply with the current port. + * Transitions into STARTING state. + * + * @param config {CardanoNodeConfig} + * @param isForced {boolean} + * @returns {Promise} resolves if the node could be started, rejects with error otherwise. + */ + start = async (config: CardanoNodeConfig, isForced: boolean = false): Promise => { + // Guards + const nodeCanBeStarted = await this._canBeStarted(); + + if (!nodeCanBeStarted) { + return Promise.reject('CardanoNode: Cannot be started.'); + } + if (this._isUnrecoverable(config) && !isForced) { + return Promise.reject('CardanoNode: Too many startup retries.'); + } + // Setup + const { _log } = this; + const { nodePath, nodeArgs, startupTimeout } = config; + const { createWriteStream } = this._actions; + this._config = config; + + this._startupTries++; + this._changeToState(CardanoNodeStates.STARTING); + _log.info(`CardanoNode#start: trying to start cardano-node for the ${this._startupTries}. time.`); + + return new Promise((resolve, reject) => { + const logFile = createWriteStream(config.logFilePath, { flags: 'a' }); + logFile.on('open', async () => { + this._cardanoLogFile = logFile; + // Spawning cardano-node + const jsonArgs = JSON.stringify(nodeArgs); + _log.debug(`from path: ${nodePath} with args: ${jsonArgs}.`); + const node = this._spawnNode(nodePath, nodeArgs, logFile); + this._node = node; + try { + await promisedCondition(() => node.connected, startupTimeout); + // Setup livecycle event handlers + node.on('message', this._handleCardanoNodeMessage); + node.on('exit', this._handleCardanoNodeExit); + node.on('error', this._handleCardanoNodeError); + // Request cardano-node to reply with port + node.send({ QueryPort: [] }); + _log.info(`CardanoNode#start: cardano-node child process spawned with PID ${node.pid}`); + resolve(); + } catch (_) { + reject('CardanoNode#start: Error while spawning cardano-node.'); + } + }); + }); + }; + + /** + * Stops cardano-node, first by disconnecting and waiting up to `shutdownTimeout` + * for the node to shutdown itself properly. If that doesn't work as expected the + * node is killed. + * + * @returns {Promise} resolves if the node could be stopped, rejects with error otherwise. + */ + async stop(): Promise { + const { _node, _log, _config } = this; + if (await this._isDead()) { + _log.info('CardanoNode#stop: process is not running anymore.'); + return Promise.resolve(); + } + _log.info('CardanoNode#stop: disconnecting from cardano-node process.'); + try { + if (_node) _node.disconnect(); + this._changeToState(CardanoNodeStates.STOPPING); + await this._waitForNodeProcessToExit(_config.shutdownTimeout); + await this._storeProcessStates(); + this._reset(); + return Promise.resolve(); + } catch (error) { + _log.info(`CardanoNode#stop: cardano-node did not stop correctly: ${error}`); + try { + await this.kill(); + } catch (killError) { + return Promise.reject(killError); + } + } + } + + /** + * Kills cardano-node and waitsup to `killTimeout` for the node to + * report the exit message. + * + * @returns {Promise} resolves if the node could be killed, rejects with error otherwise. + */ + kill(): Promise { + const { _node, _log } = this; + return new Promise(async (resolve, reject) => { + if (await this._isDead()) { + _log.info('CardanoNode#kill: process is already dead.'); + return Promise.resolve(); + } + try { + _log.info('CardanoNode#kill: killing cardano-node process.'); + if (_node) _node.kill(); + await this._waitForCardanoToExitOrKillIt(); + await this._storeProcessStates(); + this._reset(); + resolve(); + } catch (_) { + _log.info('CardanoNode#kill: could not kill cardano-node.'); + await this._storeProcessStates(); + this._reset(); + reject('Could not kill cardano-node.'); + } + }); + } + + /** + * Stops cardano-node if necessary and starts it again with current config. + * Optionally the restart can be forced, so that the `maxRestartTries` is ignored. + * + * @param isForced {boolean} + * @returns {Promise} resolves if the node could be restarted, rejects with error otherwise. + */ + async restart(isForced: boolean = false): Promise { + const { _log, _config } = this; + try { + // Stop cardano nicely if it is still awake + if (await this._isConnected()) { + _log.info('CardanoNode#restart: stopping current node.'); + await this.stop(); + } + _log.info(`CardanoNode#restart: restarting node with previous config (isForced: ${isForced.toString()}).`); + await this._waitForCardanoToExitOrKillIt(); + await this.start(_config, isForced); + } catch (error) { + _log.info(`CardanoNode#restart: Could not restart cardano-node "${error}"`); + this._changeToState(CardanoNodeStates.ERRORED); + return Promise.reject(error); + } + } + + /** + * Uses the configured action to broadcast the tls config + */ + broadcastTlsConfig() { + this._actions.broadcastTlsConfig(this._tlsConfig); + } + + /** + * Changes the internal state to UPDATING. + * Waits up to the configured `updateTimeout` for the UPDATED state. + * Kills cardano-node if it didn't properly update. + * + * @returns {Promise} resolves if the node updated, rejects with error otherwise. + */ + async expectNodeUpdate(): Promise { + const { _log, _config } = this; + this._changeToState(CardanoNodeStates.UPDATING); + _log.info('CardanoNode: waiting for node to apply update.'); + try { + await promisedCondition(() => ( + this._state === CardanoNodeStates.UPDATED + ), _config.updateTimeout); + await this._waitForNodeProcessToExit(_config.updateTimeout); + } catch (error) { + _log.info('CardanoNode: did not apply update as expected. Killing it.'); + return this.kill(); + } + } + + // ================================= PRIVATE =================================== + + /** + * Spawns cardano-node as child_process in ipc mode writing to given log file + * @param nodePath {string} + * @param args {NodeArgs} + * @param logFile {WriteStream} + * @returns {ChildProcess} + * @private + */ + _spawnNode(nodePath: string, args: NodeArgs, logFile: WriteStream) { + return this._actions.spawn( + nodePath, args, { stdio: ['inherit', logFile, logFile, 'ipc'] } + ); + } + + /** + * Handles node ipc messages sent by the cardano-node process. + * Updates the tls config where possible and broadcasts it to + * the outside if it is complete. Transitions into RUNNING state + * after it broadcasted the tls config (that's the difference between + * STARTING and RUNNING). + * + * @param msg + * @private + */ + _handleCardanoNodeMessage = (msg: CardanoNodeIpcMessage) => { + const { _log, _actions } = this; + const { tlsPath } = this._config; + _log.info(`CardanoNode: received message: ${JSON.stringify(msg)}`); + if (msg != null && msg.ReplyPort != null) { + const port: number = msg.ReplyPort; + this._tlsConfig = { + ca: _actions.readFileSync(tlsPath + '/client/ca.crt'), + key: _actions.readFileSync(tlsPath + '/client/client.key'), + cert: _actions.readFileSync(tlsPath + '/client/client.pem'), + port, + }; + if (this._state === CardanoNodeStates.STARTING) { + this._changeToState(CardanoNodeStates.RUNNING); + this.broadcastTlsConfig(); + // Reset the startup tries when we managed to get the node running + this._startupTries = 0; + } + } + }; + + _handleCardanoNodeError = async (error: Error) => { + const { _log } = this; + _log.info(`CardanoNode: error: ${error.toString()}`); + this._changeToState(CardanoNodeStates.ERRORED); + this._transitionListeners.onError(error); + await this.restart(); + }; + + _handleCardanoNodeExit = async (code: number, signal: string) => { + const { _log, _config, _node } = this; + _log.info(`CardanoNode: says it exited with [${code}, ${signal}]`); + // We don't know yet what happened but we can be sure cardano-node is exiting + if (this._state === CardanoNodeStates.RUNNING) { + this._changeToState(CardanoNodeStates.EXITING); + } + try { + // Before proceeding with exit procedures, wait until the node is really dead. + await this._waitForNodeProcessToExit(_config.shutdownTimeout); + } catch (_) { + _log.error(`CardanoNode: sent exit code ${code} but was still running after ${_config.shutdownTimeout}ms. Killing it now.`); + try { + if (_node) await this._ensureProcessIsNotRunning(_node.pid, CARDANO_PROCESS_NAME); + } catch (e) { + _log.info('CardanoNode: did not exit correctly.'); + } + } + _log.info(`CardanoNode: process really exited with [${code}, ${signal}]}`); + // Handle various exit scenarios + if (this._state === CardanoNodeStates.STOPPING) { + this._changeToState(CardanoNodeStates.STOPPED); + } else if (this._state === CardanoNodeStates.UPDATING && code === CARDANO_UPDATE_EXIT_CODE) { + this._changeToState(CardanoNodeStates.UPDATED); + } else if (this._isUnrecoverable(_config)) { + this._changeToState(CardanoNodeStates.UNRECOVERABLE); + } else { + this._changeToState(CardanoNodeStates.CRASHED, code, signal); + } + this._reset(); + }; + + _reset = () => { + if (this._cardanoLogFile) this._cardanoLogFile.end(); + if (this._node) this._node.removeAllListeners(); + this._tlsConfig = null; + }; + + _changeToState(state: CardanoNodeState, ...args: Array) { + const { _log, _transitionListeners } = this; + _log.info(`CardanoNode: transitions to <${state}>`); + this._state = state; + this._actions.broadcastStateChange(state); + switch (state) { + case CardanoNodeStates.STARTING: return _transitionListeners.onStarting(); + case CardanoNodeStates.RUNNING: return _transitionListeners.onRunning(); + case CardanoNodeStates.STOPPING: return _transitionListeners.onStopping(); + case CardanoNodeStates.STOPPED: return _transitionListeners.onStopped(); + case CardanoNodeStates.UPDATING: return _transitionListeners.onUpdating(); + case CardanoNodeStates.UPDATED: return _transitionListeners.onUpdated(); + case CardanoNodeStates.CRASHED: return _transitionListeners.onCrashed(...args); + case CardanoNodeStates.UNRECOVERABLE: return _transitionListeners.onUnrecoverable(); + default: + } + } + + /** + * Checks if cardano-node child_process is connected and can be interacted with + * @returns {boolean} + */ + _isConnected = (): boolean => this._node != null && this._node.connected; + + /** + * Checks if cardano-node child_process is not running anymore + * @returns {boolean} + */ + _isDead = async (): Promise => ( + !this._isConnected() && await this._isNodeProcessNotRunningAnymore() + ); + + /** + * Checks if current cardano-node child_process is "awake" (created, connected, stateful) + * If node is already awake, returns false. + * Kills process with PID that matches PID of the previously running + * cardano-node child_process that didn't shut down properly + * @returns {boolean} + * @private + */ + _canBeStarted = async (): Promise => { + if (this._isConnected()) { return false; } + try { + await this._ensurePreviousCardanoNodeIsNotRunning(); + return true; + } catch (error) { + return false; + } + }; + + _ensureProcessIsNotRunning = async (pid: number, name: string) => { + const { _log } = this; + _log.info(`CardanoNode: checking if ${name} process (PID: ${pid}) is still running`); + if (await this._isProcessRunning(pid, name)) { + _log.info(`CardanoNode: killing ${name} process (PID: ${pid})`); + try { + await this._killProcessWithName(pid, name); + return Promise.resolve(); + } catch (error) { + _log.info(`CardanoNode: could not kill ${name} process (PID: ${pid})`); + return Promise.reject(); + } + } + this._log.info(`No ${name} process (PID: ${pid}) is running.`); + }; + + _ensureCurrentCardanoNodeIsNotRunning = async (): Promise => { + const { _log, _node } = this; + _log.info('CardanoNode: checking if current cardano-node process is still running'); + if (_node == null) { return Promise.resolve(); } + return await this._ensureProcessIsNotRunning(_node.pid, CARDANO_PROCESS_NAME); + }; + + _ensurePreviousCardanoNodeIsNotRunning = async (): Promise => { + const { _log } = this; + _log.info('CardanoNode: checking if previous cardano-node process is still running'); + const previousPID: ?number = await this._retrieveData(PREVIOUS_CARDANO_PID); + if (previousPID == null) { return Promise.resolve(); } + return await this._ensureProcessIsNotRunning(previousPID, CARDANO_PROCESS_NAME); + }; + + _isProcessRunning = async (previousPID: number, processName: string): Promise => { + const { _log } = this; + try { + const previousProcess = await getProcess(previousPID, processName); + if (!previousProcess) { + _log.debug(`CardanoNode: No previous ${processName} process is running anymore.`); + return false; + } + _log.debug(`CardanoNode: previous ${processName} process found: ${JSON.stringify(previousProcess)}`); + return true; + } catch (error) { + return false; + } + }; + + // kills running process which did not shut down properly between sessions + _killProcessWithName = async (pid: number, name: string): Promise => { + const { _config } = this; + try { + if (!environment.isWindows()) { + this._log.info('CardanoNode: using "process.kill(pid)" to kill.'); + process.kill(pid); + } else { + // https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/taskkill + const windowsKillCmd = `taskkill /pid ${pid} /t /f`; + this._log.info(`CardanoNode (Windows): using "${windowsKillCmd}" to kill.`); + this._actions.exec(windowsKillCmd); + } + await promisedCondition(async () => ( + (await this._isProcessRunning(pid, name)) === false + ), _config.killTimeout); + + this._log.info(`CardanoNode: successfuly killed ${name} process (PID: ${pid})`); + return Promise.resolve(); + } catch (error) { + this._log.info( + `CardanoNode: _killProcessWithName returned an error attempting to kill ${name} + process (PID: ${pid}). Error: ${JSON.stringify(error)}` + ); + return Promise.reject(error); + } + }; + + async _storeProcessStates() { + const { _log } = this; + if (this._node != null) { + const { pid } = this._node; + _log.info(`CardanoNode: storing last cardano-node PID: ${pid}`); + await this._storeData(PREVIOUS_CARDANO_PID, pid); + } + } + + // stores the current port/pid on which cardano-node or Daedalus is running + _storeData = (identifier: string, data: number): Promise => ( + new Promise((resolve, reject) => { + try { + // saves current port/pid in file system + store.set(identifier, data); + this._log.info(`CardanoNode: ${identifier} stored successfuly`); + resolve(); + } catch (error) { + this._log.info(`CardanoNode: failed to store ${identifier}. Error: ${JSON.stringify(error)}`); + reject(error); + } + }) + ); + + // retrieves the last known port/pid on which cardano-node or Daedalus was running + _retrieveData = (identifier: string): Promise => ( + new Promise((resolve, reject) => { + try { + // retrieves previous port/pid from file system + const data: ?number = store.get(identifier); + + if (!data) { + this._log.info(`CardanoNode: get ${identifier} returned null`); + resolve(null); + } + + this._log.info(`CardanoNode: get ${identifier} success: ${JSON.stringify(data)}`); + resolve(toInteger(data)); + } catch (error) { + this._log.info(`CardanoNode: get ${identifier} failed. Error: ${JSON.stringify(error)}`); + reject(error); + } + }) + ); + + _isNodeProcessStillRunning = async (): Promise => ( + this._node != null && await this._isProcessRunning(this._node.pid, CARDANO_PROCESS_NAME) + ); + + _isNodeProcessNotRunningAnymore = async () => await this._isNodeProcessStillRunning() === false; + + _waitForNodeProcessToExit = async (timeout: number) => ( + await promisedCondition(this._isNodeProcessNotRunningAnymore, timeout) + ); + + _waitForCardanoToExitOrKillIt = async () => { + const { _config } = this; + if (this._isNodeProcessNotRunningAnymore()) return Promise.resolve(); + try { + await this._waitForNodeProcessToExit(_config.shutdownTimeout); + } catch (_) { + await this._ensureCurrentCardanoNodeIsNotRunning(); + } + }; + + _isUnrecoverable = (config: CardanoNodeConfig) => ( + this._startupTries >= config.startupMaxRetries + ); + +} diff --git a/source/main/cardano/config.js b/source/main/cardano/config.js new file mode 100644 index 0000000000..72e9a70d04 --- /dev/null +++ b/source/main/cardano/config.js @@ -0,0 +1,31 @@ +// @flow +import type { LauncherConfig } from '../config'; + +export const ensureXDGDataIsSet = () => { + if (process.env.HOME && process.env.XDG_DATA_HOME === undefined) { + process.env.XDG_DATA_HOME = process.env.HOME + '/.local/share/'; + } +}; + +/** + * Transforms the launcher config to an array of string args + * which can be passed to the cardano-node process. + * + * @param config + * @returns {NodeArgs} + * @private + */ +export const prepareArgs = (config: LauncherConfig) => { + const args: Array = Array.from(config.nodeArgs); + if (config.reportServer) args.push('--report-server', config.reportServer); + if (config.nodeDbPath) args.push('--db-path', config.nodeDbPath); + if (config.nodeLogConfig) args.push('--log-config', config.nodeLogConfig); + if (config.logsPrefix) args.push('--logs-prefix', config.logsPrefix); + if (config.configuration) { + if (config.configuration.filePath) args.push('--configuration-file', config.configuration.filePath); + if (config.configuration.key) args.push('--configuration-key', config.configuration.key); + if (config.configuration.systemStart) args.push('--system-start', config.configuration.systemStart); + if (config.configuration.seed) args.push('--configuration-seed', config.configuration.seed); + } + return args; +}; diff --git a/source/main/cardano/setup.js b/source/main/cardano/setup.js new file mode 100644 index 0000000000..5a70cec220 --- /dev/null +++ b/source/main/cardano/setup.js @@ -0,0 +1,108 @@ +// @flow +import { createWriteStream, readFileSync } from 'fs'; +import { spawn, exec } from 'child_process'; +import { BrowserWindow } from 'electron'; +import { Logger } from '../../common/logging'; +import { prepareArgs } from './config'; +import { CardanoNode } from './CardanoNode'; +import { + cardanoTlsConfigChannel, + cardanoRestartChannel, + cardanoAwaitUpdateChannel, + cardanoStateChangeChannel +} from '../ipc/cardano.ipc'; +import { safeExitWithCode } from '../utils/safeExitWithCode'; +import type { TlsConfig, CardanoNodeState } from '../../common/types/cardanoNode.types'; +import type { LauncherConfig } from '../config'; + +const startCardanoNode = (node: CardanoNode, launcherConfig: Object) => { + const { nodePath, tlsPath, logsPrefix } = launcherConfig; + const nodeArgs = prepareArgs(launcherConfig); + const logFilePath = logsPrefix + '/cardano-node.log'; + const config = { + nodePath, + logFilePath, + tlsPath, + nodeArgs, + startupTimeout: 5000, + startupMaxRetries: 5, + shutdownTimeout: 10000, + killTimeout: 10000, + updateTimeout: 60000, + }; + return node.start(config); +}; + +const restartCardanoNode = async (node: CardanoNode) => { + try { + await node.restart(); + } catch (error) { + Logger.info(`Could not restart CardanoNode: ${error}`); + } +}; + +/** + * Configures, starts and manages the CardanoNode responding to node + * state changes, app events and IPC messages coming from the renderer. + * + * @param launcherConfig {LauncherConfig} + * @param mainWindow + */ +export const setupCardano = ( + launcherConfig: LauncherConfig, mainWindow: BrowserWindow +): CardanoNode => { + + const cardanoNode = new CardanoNode(Logger, { + // Dependencies on node.js apis are passed as props to ease testing + spawn, + exec, + readFileSync, + createWriteStream, + broadcastTlsConfig: (config: ?TlsConfig) => { + if (!mainWindow.isDestroyed()) cardanoTlsConfigChannel.send(config, mainWindow); + }, + broadcastStateChange: (state: CardanoNodeState) => { + if (!mainWindow.isDestroyed()) cardanoStateChangeChannel.send(state, mainWindow); + }, + }, { + // CardanoNode lifecycle hooks + onStarting: () => {}, + onRunning: () => {}, + onStopping: () => {}, + onStopped: () => {}, + onUpdating: () => {}, + onUpdated: () => {}, + onCrashed: (code) => { + const restartTimeout = cardanoNode.startupTries > 0 ? 30000 : 0; + Logger.info(`CardanoNode crashed with code ${code}. Restarting in ${restartTimeout}ms …`); + setTimeout(() => restartCardanoNode(cardanoNode), restartTimeout); + }, + onError: () => {}, + onUnrecoverable: () => {} + }); + startCardanoNode(cardanoNode, launcherConfig); + + cardanoStateChangeChannel.onReceive(() => { + Logger.info('ipcMain: Received request from renderer for node state.'); + return Promise.resolve(cardanoNode.state); + }); + cardanoTlsConfigChannel.onReceive(() => { + Logger.info('ipcMain: Received request from renderer for tls config.'); + return Promise.resolve(cardanoNode.tlsConfig); + }); + cardanoAwaitUpdateChannel.onReceive(() => { + Logger.info('ipcMain: Received request from renderer to await update.'); + setTimeout(async () => { + await cardanoNode.expectNodeUpdate(); + Logger.info('CardanoNode applied an update. Exiting Daedalus with code 20.'); + safeExitWithCode(20); + }); + return Promise.resolve(); + }); + cardanoRestartChannel.onReceive(() => { + Logger.info('ipcMain: Received request from renderer to restart node.'); + return cardanoNode.restart(true); // forced restart + }); + + return cardanoNode; +}; diff --git a/source/main/cardano/utils.js b/source/main/cardano/utils.js new file mode 100644 index 0000000000..18d1dde0c1 --- /dev/null +++ b/source/main/cardano/utils.js @@ -0,0 +1,80 @@ +// @flow +import psList from 'ps-list'; +import { isObject } from 'lodash'; +import type { + CardanoNodeStorageKeys, + NetworkNames, + PlatformNames, + ProcessNames +} from '../../common/types/cardanoNode.types'; +import { + CardanoProcessNameOptions, + NetworkNameOptions +} from '../../common/types/cardanoNode.types'; + +export type Process = { + pid: number, + name: string, + cmd: string, + ppid?: number, + cpu: number, + memore: number, +}; + +const checkCondition = async ( + condition: () => boolean, + resolve: Function, + reject: Function, + timeout: number, + retryEvery: number, + timeWaited: number = 0 +): Promise => { + const result = await condition(); + if (result) { + resolve(); + } else if (timeWaited >= timeout) { + reject(`Condition not met within ${timeout}ms: ${condition.toString()}`); + } else { + setTimeout(() => checkCondition( + condition, resolve, reject, timeout, retryEvery, timeWaited + retryEvery + ), retryEvery); + } +}; + +export const promisedCondition = ( + cond: Function, timeout: number = 5000, retryEvery: number = 1000 +): Promise => new Promise((resolve, reject) => { + checkCondition(cond, resolve, reject, timeout, retryEvery); +}); + +const getNetworkName = (network: NetworkNames): string => ( + NetworkNameOptions[network] || NetworkNameOptions.development +); + +export const deriveStorageKeys = (network: NetworkNames): CardanoNodeStorageKeys => ({ + PREVIOUS_CARDANO_PID: `${getNetworkName(network)}-PREVIOUS-CARDANO-PID` +}); + +export const deriveProcessNames = (platform: PlatformNames): ProcessNames => ({ + CARDANO_PROCESS_NAME: CardanoProcessNameOptions[platform] || 'cardano-node' +}); + +export const getProcess = async (processId: number, processName: string): Promise => { + try { + // retrieves all running processes + const runningProcesses: Array = await psList(); + // filters running processes against given pid + const matchingProcesses: Array = runningProcesses.filter(({ pid }) => ( + pid === processId + )); + // no processes exist with a matching PID + if (!matchingProcesses.length) return null; + // Return first matching process if names match + const previousProcess: Process = matchingProcesses[0]; + if (isObject(previousProcess) && previousProcess.name === processName) { + return previousProcess; + } + } catch (error) { + return null; + } +}; diff --git a/source/main/config.js b/source/main/config.js index bf49027ef6..b911213d81 100644 --- a/source/main/config.js +++ b/source/main/config.js @@ -1,8 +1,59 @@ +// @flow import path from 'path'; -import getRuntimeFolderPath from './utils/getRuntimeFolderPath'; -import { launcherConfig } from './utils/launcherConfig'; +import { app, dialog } from 'electron'; +import { readLauncherConfig } from './utils/config'; + +// Make sure Daedalus is started with required configuration +const { NODE_ENV, LAUNCHER_CONFIG } = process.env; +const isProd = NODE_ENV === 'production'; +const isStartedByLauncher = !!LAUNCHER_CONFIG; +if (!isStartedByLauncher) { + const isWindows = process.platform === 'win32'; + const dialogTitle = 'Daedalus improperly started!'; + let dialogMessage; + if (isProd) { + dialogMessage = isWindows ? + 'Please start Daedalus using the icon in the Windows start menu or using Daedalus icon on your desktop.' : + 'Daedalus was launched without needed configuration. Please start Daedalus using the shortcut provided by the installer.'; + } else { + dialogMessage = 'Daedalus should be started using nix-shell. Find more details here: https://github.com/input-output-hk/daedalus/blob/develop/README.md'; + } + try { + // app may not be available at this moment so we need to use try-catch + dialog.showErrorBox(dialogTitle, dialogMessage); + app.exit(1); + } catch (e) { + throw new Error(`${dialogTitle}\n\n${dialogMessage}\n`); + } +} + +/** + * The shape of the config params, usually provided to the cadano-node launcher + */ +export type LauncherConfig = { + statePath: string, + nodePath: string, + nodeArgs: Array, + tlsPath: string, + reportServer?: string, + nodeDbPath: string, + logsPrefix: string, + nodeLogConfig: string, + nodeTimeoutSec: number, + configuration: { + filePath: string, + key: string, + systemStart: string, + seed: string, + } +}; export const APP_NAME = 'Daedalus'; -export const runtimeFolderPath = getRuntimeFolderPath(process.platform, process.env, APP_NAME); -export const appLogsFolderPath = launcherConfig.logsPrefix || path.join(runtimeFolderPath, 'Logs'); +export const launcherConfig: LauncherConfig = readLauncherConfig(LAUNCHER_CONFIG); +export const appLogsFolderPath = launcherConfig.logsPrefix; export const pubLogsFolderPath = path.join(appLogsFolderPath, 'pub'); +export const ALLOWED_LOGS = ['Daedalus.log']; +export const ALLOWED_NODE_LOGS = new RegExp(/(node.json-)(\d{14}$)/); +export const ALLOWED_LAUNCHER_LOGS = new RegExp(/(launcher-)(\d{14}$)/); +export const MAX_NODE_LOGS_ALLOWED = 3; +export const MAX_LAUNCHER_LOGS_ALLOWED = 3; diff --git a/source/main/index.js b/source/main/index.js index a0a53e6a3f..6379c66e51 100644 --- a/source/main/index.js +++ b/source/main/index.js @@ -1,31 +1,31 @@ +// @flow import os from 'os'; -import { app, globalShortcut, Menu, dialog } from 'electron'; -import log from 'electron-log'; +import { app, BrowserWindow, globalShortcut, Menu, dialog } from 'electron'; import { client } from 'electron-connect'; import { includes } from 'lodash'; +import { Logger } from '../common/logging'; import { setupLogging } from './utils/setupLogging'; -import { setupTls } from './utils/setupTls'; import { makeEnvironmentGlobal } from './utils/makeEnvironmentGlobal'; import { createMainWindow } from './windows/main'; import { winLinuxMenu } from './menus/win-linux'; import { osxMenu } from './menus/osx'; import { installChromeExtensions } from './utils/installChromeExtensions'; import environment from '../common/environment'; -import { OPEN_ABOUT_DIALOG_CHANNEL } from '../common/ipc-api/open-about-dialog'; -import { GO_TO_ADA_REDEMPTION_SCREEN_CHANNEL } from '../common/ipc-api/go-to-ada-redemption-screen'; +import { OPEN_ABOUT_DIALOG_CHANNEL } from '../common/ipc/open-about-dialog'; +import { GO_TO_ADA_REDEMPTION_SCREEN_CHANNEL } from '../common/ipc/go-to-ada-redemption-screen'; +import { GO_TO_NETWORK_STATUS_SCREEN_CHANNEL } from '../common/ipc/go-to-network-status-screen'; import mainErrorHandler from './utils/mainErrorHandler'; - -setupLogging(); -mainErrorHandler(); - -log.info(`========== Daedalus is starting at ${new Date()} ==========`); - -log.info(`!!! Daedalus is running on ${os.platform()} version ${os.release()} - with CPU: ${JSON.stringify(os.cpus(), null, 2)} with - ${JSON.stringify(os.totalmem(), null, 2)} total RAM !!!`); +import { launcherConfig } from './config'; +import { setupCardano } from './cardano/setup'; +import { CardanoNode } from './cardano/CardanoNode'; +import { safeExitWithCode } from './utils/safeExitWithCode'; +import { ensureXDGDataIsSet } from './cardano/config'; +import { acquireDaedalusInstanceLock } from './utils/lockFiles'; +import { CardanoNodeStates } from '../common/types/cardanoNode.types'; // Global references to windows to prevent them from being garbage collected -let mainWindow; +let mainWindow: BrowserWindow; +let cardanoNode: CardanoNode; const openAbout = () => { if (mainWindow) mainWindow.webContents.send(OPEN_ABOUT_DIALOG_CHANNEL); @@ -35,35 +35,66 @@ const goToAdaRedemption = () => { if (mainWindow) mainWindow.webContents.send(GO_TO_ADA_REDEMPTION_SCREEN_CHANNEL); }; -const restartInSafeMode = () => { - app.exit(21); +const goToNetworkStatus = () => { + if (mainWindow) mainWindow.webContents.send(GO_TO_NETWORK_STATUS_SCREEN_CHANNEL); +}; + +const restartInSafeMode = async () => { + Logger.info('restarting in SafeMode …'); + if (cardanoNode) await cardanoNode.stop(); + Logger.info('Exiting Daedalus with code 21.'); + safeExitWithCode(21); }; -const restartWithoutSafeMode = () => { - app.exit(22); +const restartWithoutSafeMode = async () => { + Logger.info('restarting without SafeMode …'); + if (cardanoNode) await cardanoNode.stop(); + Logger.info('Exiting Daedalus with code 22.'); + safeExitWithCode(22); }; const menuActions = { openAbout, goToAdaRedemption, + goToNetworkStatus, restartInSafeMode, - restartWithoutSafeMode + restartWithoutSafeMode, +}; + +const safeExit = async () => { + if (cardanoNode.state === CardanoNodeStates.STOPPING) return; + try { + Logger.info(`Daedalus:safeExit: stopping cardano-node with PID ${cardanoNode.pid || 'null'}`); + await cardanoNode.stop(); + Logger.info('Daedalus:safeExit: exiting Daedalus with code 0.'); + safeExitWithCode(0); + } catch (stopError) { + Logger.info(`Daedalus:safeExit: cardano-node did not exit correctly: ${stopError}`); + safeExitWithCode(0); + } }; app.on('ready', async () => { - const isProd = process.env.NODE_ENV === 'production'; - const isStartedByLauncher = !!process.env.LAUNCHER_CONFIG; - if (isProd && !isStartedByLauncher) { - const isWindows = process.platform === 'win32'; - const dialogTitle = 'Daedalus improperly started!'; - const dialogMessage = isWindows ? - 'Please start Daedalus using the icon in the Windows start menu or using Daedalus icon on your desktop.' : - 'Daedalus was launched without needed configuration. Please start Daedalus using the shortcut provided by the installer.'; + // Make sure this is the only Daedalus instance running per cluster before doing anything else + try { + await acquireDaedalusInstanceLock(); + } catch (e) { + const dialogTitle = 'Daedalus is unable to start!'; + const dialogMessage = 'Another Daedalus instance is already running.'; dialog.showErrorBox(dialogTitle, dialogMessage); - app.quit(); + app.exit(1); } - setupTls(); + setupLogging(); + mainErrorHandler(); + + Logger.info(`========== Daedalus is starting at ${new Date().toString()} ==========`); + + Logger.debug(`!!! ${environment.getBuildLabel()} is running on ${os.platform()} version ${os.release()} + with CPU: ${JSON.stringify(os.cpus(), null, 2)} with + ${JSON.stringify(os.totalmem(), null, 2)} total RAM !!!`); + + ensureXDGDataIsSet(); makeEnvironmentGlobal(process.env); await installChromeExtensions(environment.isDev()); @@ -71,6 +102,7 @@ app.on('ready', async () => { const isInSafeMode = includes(process.argv.slice(1), '--safe-mode'); mainWindow = createMainWindow(isInSafeMode); + cardanoNode = setupCardano(launcherConfig, mainWindow); if (environment.isDev()) { // Connect to electron-connect server which restarts / reloads windows on file changes @@ -101,8 +133,17 @@ app.on('ready', async () => { globalShortcut.unregister('CommandOrControl+H'); }); } -}); -app.on('window-all-closed', () => { - app.quit(); + mainWindow.on('close', async (event) => { + Logger.info('mainWindow received event. Safe exiting Daedalus now.'); + event.preventDefault(); + await safeExit(); + }); + + // Wait for controlled cardano-node shutdown before quitting the app + app.on('before-quit', async (event) => { + Logger.info('app received event. Safe exiting Daedalus now.'); + event.preventDefault(); // prevent Daedalus from quitting immediately + await safeExit(); + }); }); diff --git a/source/main/ipc-api/delete-compressed-logs.js b/source/main/ipc-api/delete-compressed-logs.js deleted file mode 100644 index deb19117c3..0000000000 --- a/source/main/ipc-api/delete-compressed-logs.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import { ipcMain } from 'electron'; -import fs from 'fs'; -import { Logger, stringifyError } from '../../common/logging'; -import { DELETE_COMPRESSED_LOGS } from '../../common/ipc-api'; - -export default () => { - ipcMain.on(DELETE_COMPRESSED_LOGS.REQUEST, (event, file) => { - const sender = event.sender; - try { - fs.unlinkSync(file); - Logger.info('DELETE_COMPRESSED_LOGS.SUCCESS'); - return sender.send(DELETE_COMPRESSED_LOGS.SUCCESS); - } catch (error) { - Logger.error('DELETE_COMPRESSED_LOGS.ERROR: ' + stringifyError(error)); - return sender.send(DELETE_COMPRESSED_LOGS.ERROR, error); - } - }); -}; diff --git a/source/main/ipc-api/kill-process.js b/source/main/ipc-api/kill-process.js deleted file mode 100644 index c03b88faef..0000000000 --- a/source/main/ipc-api/kill-process.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import { ipcMain } from 'electron'; - -export default () => { - ipcMain.on('kill-process', () => { - // if (event.sender !== mainWindow.webContents) return; - // TODO: fix this - process.exit(20); - }); -}; diff --git a/source/main/ipc-api/load-asset.js b/source/main/ipc-api/load-asset.js deleted file mode 100644 index 2c214fbcfb..0000000000 --- a/source/main/ipc-api/load-asset.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import { ipcMain } from 'electron'; -import fs from 'fs'; -import path from 'path'; -import { LOAD_ASSET_CHANNEL } from '../../common/ipc-api/load-asset'; -import type { LoadAssetRequest } from '../../common/ipc-api/load-asset'; - -export default () => { - ipcMain.on(LOAD_ASSET_CHANNEL, ({ sender }, request: LoadAssetRequest) => { - const assetPath = path.resolve(__dirname, `../renderer/${request.fileName}`); - fs.readFile(assetPath, 'base64', (error, data) => { - if (error) { - sender.send(LOAD_ASSET_CHANNEL, false, error); - } else { - sender.send(LOAD_ASSET_CHANNEL, true, data); - } - }); - }); -}; diff --git a/source/main/ipc/cardano.ipc.js b/source/main/ipc/cardano.ipc.js new file mode 100644 index 0000000000..5c993ba7be --- /dev/null +++ b/source/main/ipc/cardano.ipc.js @@ -0,0 +1,27 @@ +// @flow +import { + CARDANO_AWAIT_UPDATE_CHANNEL, + CARDANO_RESTART_CHANNEL, + CARDANO_TLS_CONFIG_CHANNEL, + CARDANO_STATE_CHANGE_CHANNEL +} from '../../common/ipc/cardano.ipc'; +import type { CardanoNodeState, TlsConfig } from '../../common/types/cardanoNode.types'; +import { MainIpcChannel } from './lib/MainIpcChannel'; + +// IpcChannel + +export const cardanoRestartChannel: MainIpcChannel = ( + new MainIpcChannel(CARDANO_RESTART_CHANNEL) +); + +export const cardanoTlsConfigChannel: MainIpcChannel = ( + new MainIpcChannel(CARDANO_TLS_CONFIG_CHANNEL) +); + +export const cardanoAwaitUpdateChannel: MainIpcChannel = ( + new MainIpcChannel(CARDANO_AWAIT_UPDATE_CHANNEL) +); + +export const cardanoStateChangeChannel: MainIpcChannel = ( + new MainIpcChannel(CARDANO_STATE_CHANGE_CHANNEL) +); diff --git a/source/main/ipc-api/compress-logs.js b/source/main/ipc/compress-logs.js similarity index 93% rename from source/main/ipc-api/compress-logs.js rename to source/main/ipc/compress-logs.js index d36a5236c0..eb40eb6e72 100644 --- a/source/main/ipc-api/compress-logs.js +++ b/source/main/ipc/compress-logs.js @@ -9,10 +9,8 @@ import { Logger, stringifyError } from '../../common/logging'; import { COMPRESS_LOGS } from '../../common/ipc-api'; export default () => { - ipcMain.on(COMPRESS_LOGS.REQUEST, (event, logs) => { + ipcMain.on(COMPRESS_LOGS.REQUEST, (event, logs, compressedFileName) => { const sender = event.sender; - const compressedFileName = 'logs.zip'; - const outputPath = path.join(appLogsFolderPath, compressedFileName); const output = fs.createWriteStream(outputPath); const archive = archiver('zip', { diff --git a/source/main/ipc-api/download-logs.js b/source/main/ipc/download-logs.js similarity index 100% rename from source/main/ipc-api/download-logs.js rename to source/main/ipc/download-logs.js diff --git a/source/main/ipc-api/get-gpu-status.js b/source/main/ipc/get-gpu-status.js similarity index 100% rename from source/main/ipc-api/get-gpu-status.js rename to source/main/ipc/get-gpu-status.js diff --git a/source/main/ipc-api/get-logs.js b/source/main/ipc/get-logs.js similarity index 50% rename from source/main/ipc-api/get-logs.js rename to source/main/ipc/get-logs.js index 359aabea18..555a3af5e7 100644 --- a/source/main/ipc-api/get-logs.js +++ b/source/main/ipc/get-logs.js @@ -3,34 +3,21 @@ import { ipcMain } from 'electron'; import { includes, sortBy } from 'lodash'; import fs from 'fs'; import path from 'path'; -import { pubLogsFolderPath } from '../config'; +import { + pubLogsFolderPath, + MAX_NODE_LOGS_ALLOWED, + ALLOWED_LOGS, + ALLOWED_NODE_LOGS, + ALLOWED_LAUNCHER_LOGS, + MAX_LAUNCHER_LOGS_ALLOWED, +} from '../config'; import { GET_LOGS } from '../../common/ipc-api'; -const ALLOWED_LOGS = [ - 'Daedalus.log', - 'launcher', - 'node.pub', - 'node.pub.0', - 'node.pub.1', - 'node.pub.2', - 'node.pub.3', - 'node.pub.4', - 'node.pub.5', - 'node.pub.6', - 'node.pub.7', - 'node.pub.8', - 'node.pub.9', - 'node.pub.10', - 'node.pub.11', - 'node.pub.12', - 'node.pub.13', - 'node.pub.14', - 'node.pub.15', - 'node.pub.16', - 'node.pub.17', - 'node.pub.18', - 'node.pub.19', -]; +const isFileAllowed = (fileName: string) => includes(ALLOWED_LOGS, fileName); +const isFileNodeLog = (fileName: string, nodeLogsIncluded: number) => + ALLOWED_NODE_LOGS.test(fileName) && nodeLogsIncluded < MAX_NODE_LOGS_ALLOWED; +const isFileLauncherLog = (fileName: string, nodeLogsIncluded: number) => + ALLOWED_LAUNCHER_LOGS.test(fileName) && nodeLogsIncluded < MAX_LAUNCHER_LOGS_ALLOWED; export default () => { ipcMain.on(GET_LOGS.REQUEST, (event) => { @@ -39,14 +26,26 @@ export default () => { // check if pub folder exists and create array of log file names const logFiles = []; if (fs.existsSync(pubLogsFolderPath)) { - const files = fs.readdirSync(pubLogsFolderPath); + + const files = fs + .readdirSync(pubLogsFolderPath) + .sort() + .reverse(); + + let nodeLogsIncluded = 0; + let launcherLogsIncluded = 0; for (let i = 0; i < files.length; i++) { const currentFile = path.join(pubLogsFolderPath, files[i]); if (fs.statSync(currentFile).isFile()) { const fileName = path.basename(currentFile); - const isFileAllowed = includes(ALLOWED_LOGS, fileName); - if (isFileAllowed) { + if (isFileAllowed(fileName)) { + logFiles.push(fileName); + } else if (isFileNodeLog(fileName, nodeLogsIncluded)) { + logFiles.push(fileName); + nodeLogsIncluded++; + } else if (isFileLauncherLog(fileName, launcherLogsIncluded)) { logFiles.push(fileName); + launcherLogsIncluded++; } } } diff --git a/source/main/ipc-api/index.js b/source/main/ipc/index.js similarity index 68% rename from source/main/ipc-api/index.js rename to source/main/ipc/index.js index 7aa2e9b1fe..e5ba2d1aca 100644 --- a/source/main/ipc-api/index.js +++ b/source/main/ipc/index.js @@ -1,22 +1,19 @@ // @flow +import type { BrowserWindow } from 'electron'; import compressLogsApi from './compress-logs'; -import deleteCompressedLogsApi from './delete-compressed-logs'; import downloadLogsApi from './download-logs'; import getLogsApi from './get-logs'; import parseRedemptionCodeApi from './parse-redemption-code-from-pdf'; import resizeWindowApi from './resize-window'; -import killProcess from './kill-process'; import loadAsset from './load-asset'; import getGpuStatus from './get-gpu-status'; -export default (params: any) => { +export default (window: BrowserWindow) => { compressLogsApi(); - deleteCompressedLogsApi(); downloadLogsApi(); getLogsApi(); parseRedemptionCodeApi(); - resizeWindowApi(params); - killProcess(); + resizeWindowApi(window); loadAsset(); getGpuStatus(); }; diff --git a/source/main/ipc/lib/MainIpcChannel.js b/source/main/ipc/lib/MainIpcChannel.js new file mode 100644 index 0000000000..6bff16728c --- /dev/null +++ b/source/main/ipc/lib/MainIpcChannel.js @@ -0,0 +1,25 @@ +// @flow +import { ipcMain } from 'electron'; +import { IpcChannel } from '../../../common/ipc/lib/IpcChannel'; +import type { IpcReceiver, IpcSender } from '../../../common/ipc/lib/IpcChannel'; + +/** + * Subclass of IpcChannel that uses ipcMain to receive messages. + */ +export class MainIpcChannel extends IpcChannel { + + async send( + message: Outgoing, sender: IpcSender, receiver: IpcReceiver = ipcMain + ): Promise { + return super.send(message, sender, receiver); + } + + onReceive( + handler: (message: Incoming) => Promise, + receiver: IpcReceiver = ipcMain + ): void { + super.onReceive(handler, receiver); + } + +} + diff --git a/source/main/ipc/load-asset.js b/source/main/ipc/load-asset.js new file mode 100644 index 0000000000..8cf358cbee --- /dev/null +++ b/source/main/ipc/load-asset.js @@ -0,0 +1,20 @@ +// @flow +import fs from 'fs'; +import path from 'path'; +import { LOAD_ASSET_CHANNEL } from '../../common/ipc/load-asset'; +import type { LoadAssetRequest, LoadAssetResponse } from '../../common/ipc/load-asset'; +import { MainIpcChannel } from './lib/MainIpcChannel'; + +// IpcChannel + +export default () => { + const loadAssetChannel: MainIpcChannel = ( + new MainIpcChannel(LOAD_ASSET_CHANNEL) + ); + loadAssetChannel.onReceive((request: LoadAssetRequest) => { + const asset = path.resolve(__dirname, `../renderer/${request.fileName}`); + return new Promise((resolve, reject) => ( + fs.readFile(asset, 'base64', (error, data) => { error ? reject(error) : resolve(data); }) + )); + }); +}; diff --git a/source/main/ipc-api/parse-redemption-code-from-pdf.js b/source/main/ipc/parse-redemption-code-from-pdf.js similarity index 96% rename from source/main/ipc-api/parse-redemption-code-from-pdf.js rename to source/main/ipc/parse-redemption-code-from-pdf.js index e698b1a71a..f0bc107a3e 100644 --- a/source/main/ipc-api/parse-redemption-code-from-pdf.js +++ b/source/main/ipc/parse-redemption-code-from-pdf.js @@ -2,12 +2,12 @@ import { PDFExtract } from 'pdf.js-extract'; import { ipcMain } from 'electron'; import fs from 'fs'; -import log from 'electron-log'; import { decryptRegularVend, decryptForceVend, decryptRecoveryRegularVend, decryptRecoveryForceVend, } from '../../common/decrypt'; import { PARSE_REDEMPTION_CODE } from '../../common/ipc-api'; +import { Logger } from '../../common/logging'; export default () => { ipcMain.on(PARSE_REDEMPTION_CODE.REQUEST, (event, filePath, decryptionKey, redemptionType) => { @@ -38,7 +38,7 @@ export default () => { fs.writeFileSync(pdfPath, decryptedFile); isTemporaryDecryptedPdf = true; } catch (error) { - log.warn('ERROR!', error); + Logger.warn(`Error while parsing redemption code: ${error}`); sender.send(PARSE_REDEMPTION_CODE.ERROR, error.message); } } else { diff --git a/source/main/ipc-api/resize-window.js b/source/main/ipc/resize-window.js similarity index 71% rename from source/main/ipc-api/resize-window.js rename to source/main/ipc/resize-window.js index 60af485145..6367451b0c 100644 --- a/source/main/ipc-api/resize-window.js +++ b/source/main/ipc/resize-window.js @@ -1,7 +1,8 @@ // @flow import { ipcMain } from 'electron'; +import type { BrowserWindow } from 'electron'; -export default ({ window }: { window: any }) => { +export default (window: BrowserWindow) => { ipcMain.on('resize-window', (event, { width, height, animate }) => { if (event.sender !== window.webContents) return; window.setSize(width, height, animate); diff --git a/source/main/menus/osx.js b/source/main/menus/osx.js index 6d21f10c37..5fc0e80dfc 100644 --- a/source/main/menus/osx.js +++ b/source/main/menus/osx.js @@ -1,8 +1,7 @@ import { compact } from 'lodash'; -import environment from '../../common/environment'; export const osxMenu = (app, window, { - openAbout, goToAdaRedemption, restartInSafeMode, restartWithoutSafeMode + openAbout, goToAdaRedemption, goToNetworkStatus, restartInSafeMode, restartWithoutSafeMode }, isInSafeMode) => ( [{ label: 'Daedalus', @@ -11,7 +10,7 @@ export const osxMenu = (app, window, { click() { openAbout(); }, - }, environment.API === 'ada' && { + }, { label: 'Ada redemption', click() { goToAdaRedemption(); @@ -25,6 +24,12 @@ export const osxMenu = (app, window, { restartWithoutSafeMode() : restartInSafeMode(); }, + }, { + label: 'Network status', + accelerator: 'Command+S', + click() { + goToNetworkStatus(); + }, }, { label: 'Quit', accelerator: 'Command+Q', diff --git a/source/main/menus/win-linux.js b/source/main/menus/win-linux.js index 3c68f66233..5f96c39f07 100644 --- a/source/main/menus/win-linux.js +++ b/source/main/menus/win-linux.js @@ -2,7 +2,7 @@ import { compact } from 'lodash'; import environment from '../../common/environment'; export const winLinuxMenu = (app, window, { - openAbout, goToAdaRedemption, restartInSafeMode, restartWithoutSafeMode + openAbout, goToAdaRedemption, goToNetworkStatus, restartInSafeMode, restartWithoutSafeMode }, isInSafeMode) => ( [{ label: 'Daedalus', @@ -11,7 +11,7 @@ export const winLinuxMenu = (app, window, { click() { openAbout(); } - }, environment.API === 'ada' && { + }, { label: 'Ada redemption', click() { goToAdaRedemption(); @@ -25,6 +25,12 @@ export const winLinuxMenu = (app, window, { restartWithoutSafeMode() : restartInSafeMode(); }, + }, { + label: 'Network status', + accelerator: 'Ctrl+S', + click() { + goToNetworkStatus(); + }, }, { label: 'Close', accelerator: 'Ctrl+W', @@ -69,10 +75,20 @@ export const winLinuxMenu = (app, window, { accelerator: 'Ctrl+R', click() { window.webContents.reload(); } }, - { + environment.isWindows() ? { label: 'Toggle Full Screen', accelerator: 'F11', click() { window.setFullScreen(!window.isFullScreen()); } + } : { + label: 'Toggle Maximum Window Size', + accelerator: 'F11', + click() { + if (window.isMaximized()) { + window.unmaximize(); + } else { + window.maximize(); + } + } }, { label: 'Toggle Developer Tools', diff --git a/source/main/utils/config.js b/source/main/utils/config.js new file mode 100644 index 0000000000..b64cdb3b89 --- /dev/null +++ b/source/main/utils/config.js @@ -0,0 +1,23 @@ +// @flow +import { readFileSync } from 'fs'; +import yamljs from 'yamljs'; +import type { LauncherConfig } from '../config'; + +/** + * Reads and parses the launcher config yaml file on given path. + * @param configPath {String} + * @returns {LauncherConfig} + */ +export const readLauncherConfig = (configPath: ?string): LauncherConfig => { + const inputYaml = configPath ? readFileSync(configPath, 'utf8') : ''; + const finalYaml = inputYaml.replace(/\${([^}]+)}/g, + (a, b) => { + if (process.env[b]) { + return process.env[b]; + } + console.log('readLauncherConfig: warning var undefined:', b); + return ''; + } + ); + return yamljs.parse(finalYaml); +}; diff --git a/source/main/utils/getRuntimeFolderPath.js b/source/main/utils/getRuntimeFolderPath.js deleted file mode 100644 index b4f6e8be78..0000000000 --- a/source/main/utils/getRuntimeFolderPath.js +++ /dev/null @@ -1,29 +0,0 @@ -import path from 'path'; - -const isProd = process.env.NODE_ENV === 'production'; - -export default (platform, env, appName) => { - if (!isProd) { - return './'; - } - - switch (platform) { - case 'darwin': { - return path.join(env.HOME, 'Library', 'Application Support', appName); - } - case 'win32': { - return path.join(env.APPDATA, appName); - } - case 'linux': { - const { DAEDALUS_DIR, CLUSTER } = env; - if (!!DAEDALUS_DIR && !!CLUSTER) { - return DAEDALUS_DIR + '/' + CLUSTER; - } - return path.join(env.HOME, '.config', appName); - } - default: { - console.log('Unsupported platform'); - process.exit(1); - } - } -}; diff --git a/source/main/utils/launcherConfig.js b/source/main/utils/launcherConfig.js deleted file mode 100644 index 73ce5aa46d..0000000000 --- a/source/main/utils/launcherConfig.js +++ /dev/null @@ -1,27 +0,0 @@ -import { readFileSync } from 'fs'; - -const yamljs = require('yamljs'); - -function getLauncherConfig() { - if (process.env.LAUNCHER_CONFIG) { - const inputYaml = readFileSync(process.env.LAUNCHER_CONFIG, 'utf8'); - - // Linux usage refers to $XDG_DATA_HOME which needs a default value when not set - if (process.env.XDG_DATA_HOME === undefined) { - process.env.XDG_DATA_HOME = process.env.HOME + '/.local/share/'; - } - - const finalYaml = inputYaml.replace(/\${([^}]+)}/g, - (a, b) => { - const res = process.env[b]; - if (res === undefined) { - return ''; - } - return res; - }); - return yamljs.parse(finalYaml); - } - return {}; -} - -export const launcherConfig = getLauncherConfig(); diff --git a/source/main/utils/lock-files/adapter.js b/source/main/utils/lock-files/adapter.js new file mode 100644 index 0000000000..51f13dc62a --- /dev/null +++ b/source/main/utils/lock-files/adapter.js @@ -0,0 +1,77 @@ +import fs from 'graceful-fs'; + +function createSyncFs(fileSystem) { + const methods = ['mkdir', 'realpath', 'stat', 'rmdir', 'utimes']; + const newFs = { ...fileSystem }; + + methods.forEach((method) => { + newFs[method] = (...args) => { + const callback = args.pop(); + let ret; + + try { + ret = fileSystem[`${method}Sync`](...args); + } catch (err) { + return callback(err); + } + + callback(null, ret); + }; + }); + + return newFs; +} + +// ---------------------------------------------------------- + +export function toPromise(method) { + return (...args) => new Promise((resolve, reject) => { + args.push((err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + + method(...args); + }); +} + +export function toSync(method) { + return (...args) => { + let err; + let result; + + args.push((_err, _result) => { + err = _err; + result = _result; + }); + + method(...args); + + if (err) { + throw err; + } + + return result; + }; +} + +export function toSyncOptions(options) { + // Shallow clone options because we are oging to mutate them + options = { ...options }; + + // Transform fs to use the sync methods instead + options.fs = createSyncFs(options.fs || fs); + + // Retries are not allowed because it requires the flow to be sync + if ( + (typeof options.retries === 'number' && options.retries > 0) || + (options.retries && typeof options.retries.retries === 'number' && options.retries.retries > 0) + ) { + throw Object.assign(new Error('Cannot use retries with the sync api'), { code: 'ESYNC' }); + } + + return options; +} diff --git a/source/main/utils/lock-files/index.js b/source/main/utils/lock-files/index.js new file mode 100644 index 0000000000..b8bb99c6d6 --- /dev/null +++ b/source/main/utils/lock-files/index.js @@ -0,0 +1,30 @@ +import lockfile from './lockfile'; +import { toPromise, toSync, toSyncOptions } from './adapter'; + +export async function lock(file, options) { + const release = await toPromise(lockfile.lock)(file, options); + + return toPromise(release); +} + +export function lockSync(file, options) { + const release = toSync(lockfile.lock)(file, toSyncOptions(options)); + + return toSync(release); +} + +export function unlock(file, options) { + return toPromise(lockfile.unlock)(file, options); +} + +export function unlockSync(file, options) { + return toSync(lockfile.unlock)(file, toSyncOptions(options)); +} + +export function check(file, options) { + return toPromise(lockfile.check)(file, options); +} + +export function checkSync(file, options) { + return toSync(lockfile.check)(file, toSyncOptions(options)); +} diff --git a/source/main/utils/lock-files/lockfile.js b/source/main/utils/lock-files/lockfile.js new file mode 100644 index 0000000000..7d0a35e3ff --- /dev/null +++ b/source/main/utils/lock-files/lockfile.js @@ -0,0 +1,314 @@ +import path from 'path'; +import fs from 'graceful-fs'; +import retry from 'retry'; + +const locks = {}; + +function getLockFile(file) { + return `${file}.lock`; +} + +function resolveCanonicalPath(file, options, callback) { + if (!options.realpath) { + return callback(null, path.resolve(file)); + } + + // Use realpath to resolve symlinks + // It also resolves relative paths + options.fs.realpath(file, callback); +} + +function acquireLock(file, options, callback) { + // Use mkdir to create the lockfile (atomic operation) + options.fs.mkdir(getLockFile(file), (err) => { + // If successful, we are done + if (!err) { + return callback(); + } + + // If error is not EEXIST then some other error occurred while locking + if (err.code !== 'EEXIST') { + return callback(err); + } + + // Otherwise, check if lock is stale by analyzing the file mtime + if (options.stale <= 0) { + return callback(Object.assign( + new Error('Lock file is already being hold'), { code: 'ELOCKED', file } + )); + } + + options.fs.stat(getLockFile(file), (statError, stat) => { + if (statError) { + // Retry if the lockfile has been removed (meanwhile) + // Skip stale check to avoid recursiveness + if (statError.code === 'ENOENT') { + return acquireLock(file, { ...options, stale: 0 }, callback); + } + + return callback(statError); + } + + if (!isLockStale(stat, options)) { + return callback(Object.assign( + new Error('Lock file is already being hold'), { code: 'ELOCKED', file } + )); + } + + // If it's stale, remove it and try again! + // Skip stale check to avoid recursiveness + removeLock(file, options, (removeLockError) => { + if (removeLockError) { + return callback(removeLockError); + } + + acquireLock(file, { ...options, stale: 0 }, callback); + }); + }); + }); +} + +function isLockStale(stat, options) { + return stat.mtime.getTime() < Date.now() - options.stale; +} + +function removeLock(file, options, callback) { + // Remove lockfile, ignoring ENOENT errors + options.fs.rmdir(getLockFile(file), (err) => { + if (err && err.code !== 'ENOENT') { + return callback(err); + } + + callback(); + }); +} + +function updateLock(file, options) { + const _lock = locks[file]; + + // Just for safety, should never happen + /* istanbul ignore if */ + if (_lock.updateTimeout) { + return; + } + + _lock.updateDelay = _lock.updateDelay || options.update; + _lock.updateTimeout = setTimeout(() => { + const mtime = Date.now() / 1000; + + _lock.updateTimeout = null; + + options.fs.utimes(getLockFile(file), mtime, mtime, (utimesError) => { + // Ignore if the lock was released + if (_lock.released) { + return; + } + + // Verify if we are within the stale threshold + const isCompromised = ( + _lock.lastUpdate <= Date.now() - options.stale + ) && ( + _lock.lastUpdate > Date.now() - (options.stale * 2) + ); + + if (isCompromised) { + const error = Object.assign( + new Error(_lock.updateError || 'Unable to update lock within the stale threshold'), + { code: 'ECOMPROMISED' } + ); + + return setLockAsCompromised(file, _lock, error); + } + + // If the file is older than (stale * 2), we assume the clock is moved manually, + // which we consider a valid case + + // If it failed to update the lockfile, keep trying unless + // the lockfile was deleted! + if (utimesError) { + if (utimesError.code === 'ENOENT') { + return setLockAsCompromised(file, _lock, Object.assign(utimesError, { code: 'ECOMPROMISED' })); + } + + _lock.updateError = utimesError; + _lock.updateDelay = 1000; + + return updateLock(file, options); + } + + // All ok, keep updating.. + _lock.lastUpdate = Date.now(); + _lock.updateError = null; + _lock.updateDelay = null; + updateLock(file, options); + }); + }, _lock.updateDelay); + + // Unref the timer so that the nodejs process can exit freely + // This is safe because all acquired locks will be automatically released + // on process exit + + // We first check that `lock.updateTimeout.unref` exists because some users + // may be using this module outside of NodeJS (e.g., in an electron app), + // and in those cases `setTimeout` return an integer. + /* istanbul ignore else */ + if (_lock.updateTimeout.unref) { + _lock.updateTimeout.unref(); + } +} + +function setLockAsCompromised(file, _lock, err) { + // Signal the lock has been released + _lock.released = true; + + // Cancel lock mtime update + // Just for safety, at this point updateTimeout should be null + /* istanbul ignore if */ + if (_lock.updateTimeout) { + clearTimeout(_lock.updateTimeout); + } + + if (locks[file] === _lock) { + delete locks[file]; + } + + _lock.options.onCompromised(err); +} + +// ---------------------------------------------------------- + +function lock(filePath, options, callback) { + /* istanbul ignore next */ + options = { + stale: 10000, + update: null, + realpath: true, + retries: 0, + fs, + onCompromised: (err) => { throw err; }, + ...options, + }; + + options.retries = options.retries || 0; + options.retries = typeof options.retries === 'number' ? { retries: options.retries } : options.retries; + options.stale = Math.max(options.stale || 0, 2000); + options.update = options.update == null ? options.stale / 2 : options.update || 0; + options.update = Math.max(Math.min(options.update, options.stale / 2), 1000); + + // Resolve to a canonical file path + resolveCanonicalPath(filePath, options, (err, file) => { + if (err) { + return callback(err); + } + + // Attempt to acquire the lock + const operation = retry.operation(options.retries); + + operation.attempt(() => { + acquireLock(file, options, (acquireError) => { + if (operation.retry(acquireError)) { + return; + } + + if (acquireError) { + return callback(operation.mainError()); + } + + // We now own the lock + const _lock = { + options, + lastUpdate: Date.now(), + }; + locks[file] = _lock; + + // We must keep the lock fresh to avoid staleness + updateLock(file, options); + + callback(null, (releasedCallback) => { + if (_lock.released) { + return releasedCallback && + releasedCallback(Object.assign(new Error('Lock is already released'), { code: 'ERELEASED' })); + } + + // Not necessary to use realpath twice when unlocking + unlock(file, { ...options, realpath: false }, releasedCallback); + }); + }); + }); + }); +} + +function unlock(filePath, options, callback) { + options = { + fs, + realpath: true, + ...options, + }; + + // Resolve to a canonical file path + resolveCanonicalPath(filePath, options, (err, file) => { + if (err) { + return callback(err); + } + + // Skip if the lock is not acquired + const _lock = locks[file]; + + if (!_lock) { + return callback(Object.assign(new Error('Lock is not acquired/owned by you'), { code: 'ENOTACQUIRED' })); + } + + _lock.updateTimeout && clearTimeout(_lock.updateTimeout); // Cancel lock mtime update + _lock.released = true; // Signal the lock has been released + delete locks[file]; // Delete from locks + + removeLock(file, options, callback); + }); +} + +function check(file, options, callback) { + options = { + stale: 10000, + realpath: true, + fs, + ...options, + }; + + options.stale = Math.max(options.stale || 0, 2000); + + // Resolve to a canonical file path + resolveCanonicalPath(file, options, (error, resolvedFile) => { + if (error) { + return callback(error); + } + + // Check if lockfile exists + options.fs.stat(getLockFile(resolvedFile), (err, stat) => { + if (err) { + // If does not exist, file is not locked. Otherwise, callback with error + return err.code === 'ENOENT' ? callback(null, false) : callback(err); + } + + // Otherwise, check if lock is stale by analyzing the file mtime + return callback(null, !isLockStale(stat, options)); + }); + }); +} + +function getLocks() { + return locks; +} + +// Remove acquired locks on exit +/* istanbul ignore next */ +process.on('exit', () => { + for (const file in locks) { + if (Object.hasOwnProperty.call(locks, file)) { + try { + locks[file].options.fs.rmdirSync(getLockFile(file)); + } catch (e) { /* Empty */ } + } + } +}); + +export default { lock, unlock, check, getLocks }; diff --git a/source/main/utils/lockFiles.js b/source/main/utils/lockFiles.js new file mode 100644 index 0000000000..8ce749bd37 --- /dev/null +++ b/source/main/utils/lockFiles.js @@ -0,0 +1,42 @@ +// @flow +import path from 'path'; +import { lockSync, unlockSync, checkSync } from './lock-files'; +import { launcherConfig } from '../config'; +import { getProcessName } from './processes'; + +const OPTIONS = { + realpath: false, // Resolve symlinks (note that if true, the file must exist previously) +}; + +const getLockFilePath = async (): Promise => { + const processName = await getProcessName(process.pid); + return path.join(launcherConfig.statePath, processName); +}; + +const isLockfileActive = (lockFilePath) => { + try { + return checkSync(lockFilePath, OPTIONS); + } catch (error) { + return false; + } +}; + +export const acquireDaedalusInstanceLock = async () => { + const lockFilePath = await getLockFilePath(); + const isOtherInstanceActive = isLockfileActive(lockFilePath); + if (isOtherInstanceActive) { + return Promise.reject(new Error('Another Daedalus instance is already running.')); + } + lockSync(lockFilePath, OPTIONS); +}; + +export const releaseDaedalusInstanceLock = async () => ( + unlockSync(await getLockFilePath(), OPTIONS) +); + +// Map SIGINT & SIGTERM to process exit +// so that lockfile removes the lockfile automatically +// https://www.npmjs.com/package/proper-lockfile +process + .once('SIGINT', () => process.exit(1)) + .once('SIGTERM', () => process.exit(1)); diff --git a/source/main/utils/mainErrorHandler.js b/source/main/utils/mainErrorHandler.js index 23be9f8b67..7653f2eab2 100644 --- a/source/main/utils/mainErrorHandler.js +++ b/source/main/utils/mainErrorHandler.js @@ -1,4 +1,5 @@ // @flow +import { app } from 'electron'; import unhandled from 'electron-unhandled'; import { Logger, stringifyError } from '../../common/logging'; @@ -11,4 +12,8 @@ export default () => { process.on('uncaughtException', (error: any) => { Logger.error(`uncaughtException: ${stringifyError(error)}`); }); + + app.on('gpu-process-crashed', (event: any, killed: boolean) => { + Logger.error(`uncaughtException::gpu-process-crashed: ${killed ? 'killed' : 'not-killed'} ${stringifyError(event)}`); + }); }; diff --git a/source/main/utils/processes.js b/source/main/utils/processes.js new file mode 100644 index 0000000000..0f835db20f --- /dev/null +++ b/source/main/utils/processes.js @@ -0,0 +1,30 @@ +// @flow +import psList from 'ps-list'; + +export type Process = { + pid: number, + name: string, + cmd: string, + ppid?: number, + cpu: number, + memore: number, +}; + +export const getProcessById = async (processId: number): Promise => { + // retrieves all running processes + const processes: Array = await psList(); + // filters running processes against previous PID + const matches: Array = processes.filter(({ pid }) => processId === pid); + return matches.length > 0 ? matches[0] : Promise.reject(); +}; + +export const getProcessName = async (processId: number) => ( + (await getProcessById(processId)).name +); + +export const getProcessesByName = async (processName: string): Promise> => { + // retrieves all running processes + const processes: Array = await psList(); + // filters running processes against previous PID + return processes.filter(({ name }) => processName === name); +}; diff --git a/source/main/utils/safeExitWithCode.js b/source/main/utils/safeExitWithCode.js new file mode 100644 index 0000000000..ad680862cf --- /dev/null +++ b/source/main/utils/safeExitWithCode.js @@ -0,0 +1,20 @@ +// @flow +import { app } from 'electron'; +import log from 'electron-log'; +import { releaseDaedalusInstanceLock } from './lockFiles'; + +export const safeExitWithCode = (exitCode: number) => { + const { file } = log.transports; + // Prevent electron-log from writing to stream + file.level = false; + // Flush the stream to the log file and exit afterwards. + // https://nodejs.org/api/stream.html#stream_writable_end_chunk_encoding_callback + file.stream.end( + `========== Flushing logs and exiting with code ${exitCode} ===========`, + 'utf8', + () => { + releaseDaedalusInstanceLock(); + app.exit(exitCode); + } + ); +}; diff --git a/source/main/utils/setupLogging.js b/source/main/utils/setupLogging.js index ab00008413..31a6da06b0 100644 --- a/source/main/utils/setupLogging.js +++ b/source/main/utils/setupLogging.js @@ -1,17 +1,21 @@ +// @flow +import fs from 'fs'; import path from 'path'; import log from 'electron-log'; import moment from 'moment'; import ensureDirectoryExists from './ensureDirectoryExists'; -import { pubLogsFolderPath, APP_NAME } from '../config'; +import { pubLogsFolderPath, appLogsFolderPath, APP_NAME } from '../config'; +import { isFileNameWithTimestamp } from '../../common/fileName'; const isTest = process.env.NODE_ENV === 'test'; +const isDev = process.env.NODE_ENV === 'development'; export const setupLogging = () => { const logFilePath = path.join(pubLogsFolderPath, APP_NAME + '.log'); ensureDirectoryExists(pubLogsFolderPath); - log.transports.console.level = isTest ? 'error' : false; - log.transports.rendererConsole.level = 'error'; + log.transports.console.level = isTest ? 'error' : 'info'; + log.transports.rendererConsole.level = isDev ? 'info' : 'error'; log.transports.file.level = 'debug'; log.transports.file.maxSize = 20 * 1024 * 1024; log.transports.file.file = logFilePath; @@ -19,4 +23,18 @@ export const setupLogging = () => { const formattedDate = moment.utc(msg.date).format('YYYY-MM-DDTHH:mm:ss.0SSS'); return `[${formattedDate}Z] [${msg.level}] ${msg.data}`; }; + + // Removes existing compressed logs + fs.readdir(appLogsFolderPath, (err, files) => { + files + .filter(isFileNameWithTimestamp()) + .forEach((logFileName) => { + const logFile = path.join(appLogsFolderPath, logFileName); + try { + fs.unlinkSync(logFile); + } catch (error) { + console.error(`Compressed log file "${logFile}" deletion failed: ${error}`); + } + }); + }); }; diff --git a/source/main/utils/setupTls.js b/source/main/utils/setupTls.js deleted file mode 100644 index 76b2663fef..0000000000 --- a/source/main/utils/setupTls.js +++ /dev/null @@ -1,32 +0,0 @@ -import path from 'path'; -import fs from 'fs'; -import log from 'electron-log'; -import { launcherConfig } from './launcherConfig'; -import { runtimeFolderPath } from '../config'; - -const isProd = process.env.NODE_ENV === 'production'; -const caDevelopmentPath = process.env.CARDANO_TLS_PATH || ''; - -if (!isProd && !caDevelopmentPath) { - throw new Error('Environment variable missing: CARDANO_TLS_PATH'); -} - -/** - * Here we are reading the TLS certificate from the file system - * and make it available to render processes via a global variable - * so that it can be used in HTTP and Websocket connections. - */ -export const setupTls = () => { - const tlsBasePath = launcherConfig.tlsPath || path.join(runtimeFolderPath, 'tls'); - const caProductionPath = path.join(tlsBasePath, 'client', 'ca.crt'); - const pathToCertificate = isProd ? caProductionPath : path.join(caDevelopmentPath, 'ca.crt'); - - try { - log.info('Using certificates from: ' + pathToCertificate); - Object.assign(global, { - ca: fs.readFileSync(pathToCertificate), - }); - } catch (error) { - log.error(`Error while loading ca.crt: ${error}`); - } -}; diff --git a/source/main/webpack.config.js b/source/main/webpack.config.js index 3d62c80c12..720157e260 100644 --- a/source/main/webpack.config.js +++ b/source/main/webpack.config.js @@ -1,4 +1,3 @@ -const path = require('path'); const webpack = require('webpack'); const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); const lodash = require('lodash'); @@ -14,7 +13,6 @@ module.exports = { devtool: 'cheap-module-source-map', entry: './source/main/index.js', output: { - path: path.join(__dirname, './dist/main'), filename: 'index.js' }, /** @@ -46,18 +44,14 @@ module.exports = { }, plugins: [ new webpack.DefinePlugin(Object.assign({ - 'process.env.API': JSON.stringify(process.env.API || 'ada'), 'process.env.API_VERSION': JSON.stringify(process.env.API_VERSION || 'dev'), 'process.env.NETWORK': JSON.stringify(process.env.NETWORK || 'development'), 'process.env.MOBX_DEV_TOOLS': process.env.MOBX_DEV_TOOLS || 0, 'process.env.BUILD_NUMBER': JSON.stringify(process.env.BUILD_NUMBER || 'dev'), 'process.env.REPORT_URL': JSON.stringify(reportUrl), }, process.env.NODE_ENV === 'production' ? { - // Only bake in NODE_ENV and WALLET_PORT values for production builds. - // This is so that the test suite based on the webpack build will - // choose the correct path to ca.crt (see setupTls.js). + // Only bake in NODE_ENV value for production builds. 'process.env.NODE_ENV': '"production"', - 'process.env.WALLET_PORT': JSON.stringify(process.env.WALLET_PORT || ''), } : {})), !isCi && ( new HardSourceWebpackPlugin({ @@ -66,7 +60,7 @@ module.exports = { require('node-object-hash')({ sort: false }).hash(lodash.omit(webpackConfig, 'watch')) ), environmentPaths: { - files: ['.babelrc', 'package-lock.json', 'yarn.lock'], + files: ['.babelrc', 'yarn.lock'], }, }) ) diff --git a/source/main/windows/main.js b/source/main/windows/main.js index 46ee7b998b..0c7e83cb66 100644 --- a/source/main/windows/main.js +++ b/source/main/windows/main.js @@ -1,9 +1,9 @@ import path from 'path'; import { app, BrowserWindow, ipcMain, Menu } from 'electron'; import environment from '../../common/environment'; -import ipcApi from '../ipc-api'; -import { runtimeFolderPath } from '../config'; +import ipcApi from '../ipc'; import RendererErrorHandler from '../utils/rendererErrorHandler'; +import { launcherConfig } from '../config'; const rendererErrorHandler = new RendererErrorHandler(); @@ -18,7 +18,7 @@ export const createMainWindow = (isInSafeMode) => { }; if (process.platform === 'linux') { - windowOptions.icon = path.join(runtimeFolderPath, 'icon.png'); + windowOptions.icon = path.join(launcherConfig.statePath, 'icon.png'); } // Construct new BrowserWindow diff --git a/source/renderer/app/App.js b/source/renderer/app/App.js index d3aa5d9382..6bdba66d6f 100755 --- a/source/renderer/app/App.js +++ b/source/renderer/app/App.js @@ -1,7 +1,7 @@ // @flow import React, { Component } from 'react'; import { Provider, observer } from 'mobx-react'; -import { ThemeProvider } from 'react-css-themr'; +import { ThemeProvider } from 'react-polymorph/lib/components/ThemeProvider'; import DevTools from 'mobx-react-devtools'; import { Router } from 'react-router'; import { IntlProvider } from 'react-intl'; @@ -26,11 +26,11 @@ export default class App extends Component<{ const locale = stores.profile.currentLocale; const mobxDevTools = environment.MOBX_DEV_TOOLS ? : null; const currentTheme = stores.profile.currentTheme; - const theme = require(`./themes/daedalus/${currentTheme}.js`); // eslint-disable-line + const themeVars = require(`./themes/daedalus/${currentTheme}.js`); // eslint-disable-line return (
- + diff --git a/source/renderer/app/Routes.js b/source/renderer/app/Routes.js index 948203c17d..068a7d22c5 100644 --- a/source/renderer/app/Routes.js +++ b/source/renderer/app/Routes.js @@ -2,11 +2,11 @@ import React from 'react'; import { Route, IndexRedirect } from 'react-router'; import { ROUTES } from './routes-config'; -import resolver from './utils/imports'; // PAGES -// import StakingPage from './containers/staking/StakingPage'; +import Root from './containers/Root'; import AdaRedemptionPage from './containers/wallet/AdaRedemptionPage'; +import NetworkStatusPage from './containers/status/NetworkStatusPage'; import WalletAddPage from './containers/wallet/WalletAddPage'; import LanguageSelectionPage from './containers/profile/LanguageSelectionPage'; import Settings from './containers/settings/Settings'; @@ -14,25 +14,26 @@ import GeneralSettingsPage from './containers/settings/categories/GeneralSetting import SupportSettingsPage from './containers/settings/categories/SupportSettingsPage'; import TermsOfUseSettingsPage from './containers/settings/categories/TermsOfUseSettingsPage'; import TermsOfUsePage from './containers/profile/TermsOfUsePage'; +import DataLayerMigrationPage from './containers/profile/DataLayerMigrationPage'; import DisplaySettingsPage from './containers/settings/categories/DisplaySettingsPage'; import PaperWalletCreateCertificatePage from './containers/wallet/PaperWalletCreateCertificatePage'; - -// Dynamic container loading - resolver loads file relative to '/app/' directory -const LoadingPage = resolver('containers/LoadingPage'); -const Wallet = resolver('containers/wallet/Wallet'); -const WalletSummaryPage = resolver('containers/wallet/WalletSummaryPage'); -const WalletSendPage = resolver('containers/wallet/WalletSendPage'); -const WalletReceivePage = resolver('containers/wallet/WalletReceivePage'); -const WalletTransactionsPage = resolver('containers/wallet/WalletTransactionsPage'); -const WalletSettingsPage = resolver('containers/wallet/WalletSettingsPage'); +import Wallet from './containers/wallet/Wallet'; +import WalletSummaryPage from './containers/wallet/WalletSummaryPage'; +import WalletSendPage from './containers/wallet/WalletSendPage'; +import WalletReceivePage from './containers/wallet/WalletReceivePage'; +import WalletTransactionsPage from './containers/wallet/WalletTransactionsPage'; +import WalletSettingsPage from './containers/wallet/WalletSettingsPage'; +// import StakingPage from './containers/staking/StakingPage'; export const Routes = ( -
- + + + {/* */} + @@ -52,5 +53,5 @@ export const Routes = ( path={ROUTES.PAPER_WALLET_CREATE_CERTIFICATE} component={PaperWalletCreateCertificatePage} /> -
+ ); diff --git a/source/renderer/app/ThemeManager.js b/source/renderer/app/ThemeManager.js index 0916bcb09b..1c06a37233 100644 --- a/source/renderer/app/ThemeManager.js +++ b/source/renderer/app/ThemeManager.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import _ from 'lodash'; +import { map } from 'lodash'; export default class ThemeManager extends Component { componentDidMount() { @@ -13,7 +13,7 @@ export default class ThemeManager extends Component { } updateCSSVariables(variables) { - _.map(variables, (value, prop) => { + map(variables, (value, prop) => { document.documentElement.style.setProperty(prop, value); }); } diff --git a/source/renderer/app/actions/ada/ada-redemption-actions.js b/source/renderer/app/actions/ada-redemption-actions.js similarity index 74% rename from source/renderer/app/actions/ada/ada-redemption-actions.js rename to source/renderer/app/actions/ada-redemption-actions.js index 34ea796d5c..cac04faadc 100644 --- a/source/renderer/app/actions/ada/ada-redemption-actions.js +++ b/source/renderer/app/actions/ada-redemption-actions.js @@ -1,6 +1,6 @@ // @flow -import Action from '../lib/Action'; -import type { RedemptionTypeChoices } from '../../types/redemptionTypes'; +import Action from './lib/Action'; +import type { RedemptionTypeChoices } from '../types/redemptionTypes'; // ======= ADA REDEMPTION ACTIONS ======= @@ -14,10 +14,10 @@ export default class AdaRedemptionActions { setAdaPasscode: Action<{ adaPasscode: string }> = new Action(); setAdaAmount: Action<{ adaAmount: string }> = new Action(); setDecryptionKey: Action<{ decryptionKey: string }> = new Action(); - redeemAda: Action<{ walletId: string, walletPassword: ?string }> = new Action(); + redeemAda: Action<{ walletId: string, spendingPassword: ?string }> = new Action(); // eslint-disable-next-line max-len - redeemPaperVendedAda: Action<{ walletId: string, shieldedRedemptionKey: string, walletPassword: ?string }> = new Action(); - adaSuccessfullyRedeemed: Action = new Action(); + redeemPaperVendedAda: Action<{ walletId: string, shieldedRedemptionKey: string, spendingPassword: ?string }> = new Action(); + adaSuccessfullyRedeemed: Action<{ walletId: string, amount: number }> = new Action(); acceptRedemptionDisclaimer: Action = new Action(); // TODO: refactor dialog toggles to use dialog-actions instead closeAdaRedemptionSuccessOverlay: Action = new Action(); diff --git a/source/renderer/app/actions/ada/index.js b/source/renderer/app/actions/ada/index.js deleted file mode 100644 index 61e6908838..0000000000 --- a/source/renderer/app/actions/ada/index.js +++ /dev/null @@ -1,27 +0,0 @@ -// @flow -import WalletsActions from './wallets-actions'; -import AdaRedemptionActions from './ada-redemption-actions'; -import TransactionsActions from './transactions-actions'; -import NodeUpdateActions from './node-update-actions'; -import WalletSettingsActions from './wallet-settings-actions'; -import AddressesActions from './addresses-actions'; - -export type AdaActionsMap = { - wallets: WalletsActions, - adaRedemption: AdaRedemptionActions, - transactions: TransactionsActions, - nodeUpdate: NodeUpdateActions, - walletSettings: WalletSettingsActions, - addresses: AddressesActions, -}; - -const adaActionsMap: AdaActionsMap = { - wallets: new WalletsActions(), - adaRedemption: new AdaRedemptionActions(), - transactions: new TransactionsActions(), - nodeUpdate: new NodeUpdateActions(), - walletSettings: new WalletSettingsActions(), - addresses: new AddressesActions(), -}; - -export default adaActionsMap; diff --git a/source/renderer/app/actions/ada/addresses-actions.js b/source/renderer/app/actions/addresses-actions.js similarity index 51% rename from source/renderer/app/actions/ada/addresses-actions.js rename to source/renderer/app/actions/addresses-actions.js index 834297777d..60eafdf2b8 100644 --- a/source/renderer/app/actions/ada/addresses-actions.js +++ b/source/renderer/app/actions/addresses-actions.js @@ -1,9 +1,9 @@ // @flow -import Action from '../lib/Action'; +import Action from './lib/Action'; // ======= ADDRESSES ACTIONS ======= export default class AddressesActions { - createAddress: Action<{ walletId: string, password: ?string }> = new Action(); + createAddress: Action<{ walletId: string, spendingPassword: ?string }> = new Action(); resetErrors: Action = new Action(); } diff --git a/source/renderer/app/actions/etc/index.js b/source/renderer/app/actions/etc/index.js deleted file mode 100644 index 37866ae1b8..0000000000 --- a/source/renderer/app/actions/etc/index.js +++ /dev/null @@ -1,15 +0,0 @@ -// @flow -import WalletsActions from './wallets-actions'; -import WalletSettingsActions from './wallet-settings-actions'; - -export type EtcActionsMap = { - wallets: WalletsActions, - walletSettings: WalletSettingsActions, -}; - -const etcActionsMap: EtcActionsMap = { - wallets: new WalletsActions(), - walletSettings: new WalletSettingsActions(), -}; - -export default etcActionsMap; diff --git a/source/renderer/app/actions/etc/wallet-settings-actions.js b/source/renderer/app/actions/etc/wallet-settings-actions.js deleted file mode 100644 index 7cd44c755b..0000000000 --- a/source/renderer/app/actions/etc/wallet-settings-actions.js +++ /dev/null @@ -1,11 +0,0 @@ -// @flow -import Action from '../lib/Action'; - -export default class WalletSettingsActions { - cancelEditingWalletField: Action = new Action(); - startEditingWalletField: Action<{ field: string }> = new Action(); - stopEditingWalletField: Action = new Action(); - updateWalletField: Action<{ field: string, value: string }> = new Action(); - // eslint-disable-next-line max-len - updateWalletPassword: Action<{ walletId: string, oldPassword: ?string, newPassword: ?string }> = new Action(); -} diff --git a/source/renderer/app/actions/etc/wallets-actions.js b/source/renderer/app/actions/etc/wallets-actions.js deleted file mode 100644 index 8901b7206a..0000000000 --- a/source/renderer/app/actions/etc/wallets-actions.js +++ /dev/null @@ -1,12 +0,0 @@ -// @flow -import Action from '../lib/Action'; - -// ======= WALLET ACTIONS ======= - -export default class WalletsActions { - createWallet: Action<{ name: string, password: ?string }> = new Action(); - // eslint-disable-next-line max-len - restoreWallet: Action<{recoveryPhrase: string, walletName: string, walletPassword: ?string }> = new Action(); - deleteWallet: Action<{ walletId: string }> = new Action(); - sendMoney: Action<{ receiver: string, amount: string, password: ?string }> = new Action(); -} diff --git a/source/renderer/app/actions/index.js b/source/renderer/app/actions/index.js index d69775afbd..348c1672da 100644 --- a/source/renderer/app/actions/index.js +++ b/source/renderer/app/actions/index.js @@ -1,44 +1,54 @@ // @flow +import AdaRedemptionActions from './ada-redemption-actions'; +import AddressesActions from './addresses-actions'; +import AppActions from './app-actions'; +import DialogsActions from './dialogs-actions'; +import NetworkStatusActions from './network-status-actions'; +import NodeUpdateActions from './node-update-actions'; +import NotificationsActions from './notifications-actions'; +import ProfileActions from './profile-actions'; import RouterActions from './router-actions'; import SidebarActions from './sidebar-actions'; -import WindowActions from './window-actions'; -import NetworkStatusActions from './network-status-actions'; +import TransactionsActions from './transactions-actions'; +import WalletsActions from './wallets-actions'; import WalletBackupActions from './wallet-backup-actions'; -import ProfileActions from './profile-actions'; -import DialogsActions from './dialogs-actions'; -import NotificationsActions from './notifications-actions'; -import adaActionsMap from './ada/index'; -import etcActionsMap from './etc/index'; -import type { AdaActionsMap } from './ada/index'; -import type { EtcActionsMap } from './etc/index'; -import AppActions from './app-actions'; +import WalletSettingsActions from './wallet-settings-actions'; +import WindowActions from './window-actions'; export type ActionsMap = { + adaRedemption: AdaRedemptionActions, + addresses: AddressesActions, app: AppActions, + dialogs: DialogsActions, + networkStatus: NetworkStatusActions, + nodeUpdate: NodeUpdateActions, + notifications: NotificationsActions, + profile: ProfileActions, router: RouterActions, sidebar: SidebarActions, - window: WindowActions, - networkStatus: NetworkStatusActions, + transactions: TransactionsActions, + wallets: WalletsActions, walletBackup: WalletBackupActions, - profile: ProfileActions, - dialogs: DialogsActions, - notifications: NotificationsActions, - ada: AdaActionsMap, - etc: EtcActionsMap, + walletSettings: WalletSettingsActions, + window: WindowActions, }; const actionsMap: ActionsMap = { + adaRedemption: new AdaRedemptionActions(), + addresses: new AddressesActions(), app: new AppActions(), + dialogs: new DialogsActions(), + networkStatus: new NetworkStatusActions(), + nodeUpdate: new NodeUpdateActions(), + notifications: new NotificationsActions(), + profile: new ProfileActions(), router: new RouterActions(), sidebar: new SidebarActions(), - window: new WindowActions(), - networkStatus: new NetworkStatusActions(), + transactions: new TransactionsActions(), + wallets: new WalletsActions(), walletBackup: new WalletBackupActions(), - profile: new ProfileActions(), - dialogs: new DialogsActions(), - notifications: new NotificationsActions(), - ada: adaActionsMap, - etc: etcActionsMap, + walletSettings: new WalletSettingsActions(), + window: new WindowActions(), }; export default actionsMap; diff --git a/source/renderer/app/actions/ada/node-update-actions.js b/source/renderer/app/actions/node-update-actions.js similarity index 87% rename from source/renderer/app/actions/ada/node-update-actions.js rename to source/renderer/app/actions/node-update-actions.js index 1d70d28c12..6f0abc44cd 100644 --- a/source/renderer/app/actions/ada/node-update-actions.js +++ b/source/renderer/app/actions/node-update-actions.js @@ -1,5 +1,5 @@ // @flow -import Action from '../lib/Action'; +import Action from './lib/Action'; // ======= NODE UPDATE ACTIONS ======= diff --git a/source/renderer/app/actions/profile-actions.js b/source/renderer/app/actions/profile-actions.js index ca8e53fd3a..4d1cd11bef 100644 --- a/source/renderer/app/actions/profile-actions.js +++ b/source/renderer/app/actions/profile-actions.js @@ -5,14 +5,14 @@ import Action from './lib/Action'; export default class ProfileActions { acceptTermsOfUse: Action = new Action(); - compressLogs: Action<{ logs: Object }> = new Action(); + acceptDataLayerMigration: Action = new Action(); getLogs: Action = new Action(); - downloadLogs: Action<{ destination: string, fresh?: boolean }> = new Action(); - deleteCompressedLogs: Action = new Action(); - resetBugReportDialog: Action = new Action(); + getLogsAndCompress: Action = new Action(); sendBugReport: Action<{ - email: string, subject: string, problem: string, compressedLog: ?string, + email: string, subject: string, problem: string, compressedLogsFile: ?string, }> = new Action(); + resetBugReportDialog: Action = new Action(); + downloadLogs: Action<{ fileName: string, destination: string, fresh?: boolean }> = new Action(); updateLocale: Action<{ locale: string }> = new Action(); updateTheme: Action<{ theme: string }> = new Action(); } diff --git a/source/renderer/app/actions/ada/transactions-actions.js b/source/renderer/app/actions/transactions-actions.js similarity index 85% rename from source/renderer/app/actions/ada/transactions-actions.js rename to source/renderer/app/actions/transactions-actions.js index 75467b04fd..5a9bfc407a 100644 --- a/source/renderer/app/actions/ada/transactions-actions.js +++ b/source/renderer/app/actions/transactions-actions.js @@ -1,5 +1,5 @@ // @flow -import Action from '../lib/Action'; +import Action from './lib/Action'; // ======= TRANSACTIONS ACTIONS ======= diff --git a/source/renderer/app/actions/ada/wallet-settings-actions.js b/source/renderer/app/actions/wallet-settings-actions.js similarity index 78% rename from source/renderer/app/actions/ada/wallet-settings-actions.js rename to source/renderer/app/actions/wallet-settings-actions.js index 9aaa7ba9b7..92780fa907 100644 --- a/source/renderer/app/actions/ada/wallet-settings-actions.js +++ b/source/renderer/app/actions/wallet-settings-actions.js @@ -1,5 +1,5 @@ // @flow -import Action from '../lib/Action'; +import Action from './lib/Action'; export type WalletExportToFileParams = { walletId: string, @@ -14,6 +14,6 @@ export default class WalletSettingsActions { stopEditingWalletField: Action = new Action(); updateWalletField: Action<{ field: string, value: string }> = new Action(); // eslint-disable-next-line max-len - updateWalletPassword: Action<{ walletId: string, oldPassword: ?string, newPassword: ?string }> = new Action(); + updateSpendingPassword: Action<{ walletId: string, oldPassword: ?string, newPassword: ?string }> = new Action(); exportToFile: Action = new Action(); } diff --git a/source/renderer/app/actions/ada/wallets-actions.js b/source/renderer/app/actions/wallets-actions.js similarity index 72% rename from source/renderer/app/actions/ada/wallets-actions.js rename to source/renderer/app/actions/wallets-actions.js index 04daf72403..b52ffebfe9 100644 --- a/source/renderer/app/actions/ada/wallets-actions.js +++ b/source/renderer/app/actions/wallets-actions.js @@ -1,19 +1,19 @@ // @flow -import Action from '../lib/Action'; -import type { walletExportTypeChoices } from '../../types/walletExportTypes'; +import Action from './lib/Action'; +import type { walletExportTypeChoices } from '../types/walletExportTypes'; export type WalletImportFromFileParams = { filePath: string, walletName: ?string, - walletPassword: ?string, + spendingPassword: ?string, }; // ======= WALLET ACTIONS ======= export default class WalletsActions { - createWallet: Action<{ name: string, password: ?string }> = new Action(); + createWallet: Action<{ name: string, spendingPassword: ?string }> = new Action(); // eslint-disable-next-line max-len - restoreWallet: Action<{recoveryPhrase: string, walletName: string, walletPassword: ?string, type?: string }> = new Action(); + restoreWallet: Action<{ recoveryPhrase: string, walletName: string, spendingPassword: ?string, type?: string }> = new Action(); importWalletFromFile: Action = new Action(); deleteWallet: Action<{ walletId: string }> = new Action(); sendMoney: Action<{ receiver: string, amount: string, password: ?string }> = new Action(); diff --git a/source/renderer/app/api/accounts/errors.js b/source/renderer/app/api/accounts/errors.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/renderer/app/api/accounts/requests/getAccounts.js b/source/renderer/app/api/accounts/requests/getAccounts.js new file mode 100644 index 0000000000..e6f7180f9e --- /dev/null +++ b/source/renderer/app/api/accounts/requests/getAccounts.js @@ -0,0 +1,20 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Accounts } from '../types'; +import { request } from '../../utils/request'; + +export type GetAccountsParams = { + walletId: string, +}; + +export const getAccounts = ( + config: RequestConfig, + { walletId }: GetAccountsParams +): Promise => ( + request({ + hostname: 'localhost', + method: 'GET', + path: `/api/v1/wallets/${walletId}/accounts`, + ...config, + }) +); diff --git a/source/renderer/app/api/accounts/types.js b/source/renderer/app/api/accounts/types.js new file mode 100644 index 0000000000..d2036c5a3a --- /dev/null +++ b/source/renderer/app/api/accounts/types.js @@ -0,0 +1,12 @@ +// @flow +import type { Addresses } from '../addresses/types'; + +export type Account = { + amount: number, + addresses: Addresses, + name: string, + walletId: string, + index: number +}; + +export type Accounts = Array; diff --git a/source/renderer/app/api/ada/adaTestReset.js b/source/renderer/app/api/ada/adaTestReset.js deleted file mode 100644 index 45b4b6753a..0000000000 --- a/source/renderer/app/api/ada/adaTestReset.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type AdaTestResetParams = { - ca: string, -}; - -export const adaTestReset = ( - { ca }: AdaTestResetParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/test/reset', - port: environment.WALLET_PORT, - ca, - }) -); diff --git a/source/renderer/app/api/ada/adaTxFee.js b/source/renderer/app/api/ada/adaTxFee.js deleted file mode 100644 index 47241980e0..0000000000 --- a/source/renderer/app/api/ada/adaTxFee.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow -import type { AdaTransactionFee } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type AdaTxFeeParams = { - ca: string, - sender: string, - receiver: string, - amount: string, - // "groupingPolicy" - Spend everything from the address - // "OptimizeForSize" for no grouping - groupingPolicy: ?'OptimizeForSecurity' | 'OptimizeForSize', -}; - -export const adaTxFee = ( - { ca, sender, receiver, amount, groupingPolicy }: AdaTxFeeParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: `/api/txs/fee/${sender}/${receiver}/${amount}`, - port: environment.WALLET_PORT, - ca, - }, {}, { groupingPolicy }) -); diff --git a/source/renderer/app/api/ada/applyAdaUpdate.js b/source/renderer/app/api/ada/applyAdaUpdate.js deleted file mode 100644 index 2600c38e0d..0000000000 --- a/source/renderer/app/api/ada/applyAdaUpdate.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type ApplyAdaUpdateParams = { - ca: string, -}; - -export const applyAdaUpdate = ( - { ca }: ApplyAdaUpdateParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/update/apply', - port: environment.WALLET_PORT, - ca, - }) -); diff --git a/source/renderer/app/api/ada/changeAdaWalletPassphrase.js b/source/renderer/app/api/ada/changeAdaWalletPassphrase.js deleted file mode 100644 index 1d9741fccf..0000000000 --- a/source/renderer/app/api/ada/changeAdaWalletPassphrase.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow -import type { AdaWallet } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; -import { encryptPassphrase } from './lib/encryptPassphrase'; - -export type ChangeAdaWalletPassphraseParams = { - ca: string, - walletId: string, - oldPassword: ?string, - newPassword: ?string, -}; - -export const changeAdaWalletPassphrase = ( - { ca, walletId, oldPassword, newPassword }: ChangeAdaWalletPassphraseParams -): Promise => { - const encryptedOldPassphrase = oldPassword ? encryptPassphrase(oldPassword) : null; - const encryptedNewPassphrase = newPassword ? encryptPassphrase(newPassword) : null; - return request({ - hostname: 'localhost', - method: 'POST', - path: `/api/wallets/password/${walletId}`, - port: environment.WALLET_PORT, - ca, - }, { old: encryptedOldPassphrase, new: encryptedNewPassphrase }); -}; diff --git a/source/renderer/app/api/ada/deleteAdaWallet.js b/source/renderer/app/api/ada/deleteAdaWallet.js deleted file mode 100644 index 47e97f9383..0000000000 --- a/source/renderer/app/api/ada/deleteAdaWallet.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type DeleteAdaWalletParams = { - ca: string, - walletId: string, -}; - -export const deleteAdaWallet = ( - { ca, walletId }: DeleteAdaWalletParams -): Promise<[]> => ( - request({ - hostname: 'localhost', - method: 'DELETE', - path: `/api/wallets/${walletId}`, - port: environment.WALLET_PORT, - ca, - }) -); diff --git a/source/renderer/app/api/ada/exportAdaBackupJSON.js b/source/renderer/app/api/ada/exportAdaBackupJSON.js deleted file mode 100644 index 007096b5a0..0000000000 --- a/source/renderer/app/api/ada/exportAdaBackupJSON.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type ExportAdaBackupJSONParams = { - ca: string, - walletId: string, - filePath: string, -}; - -export const exportAdaBackupJSON = ( - { ca, walletId, filePath }: ExportAdaBackupJSONParams, -): Promise<[]> => ( - request({ - hostname: 'localhost', - method: 'POST', - path: `/api/backup/export/${walletId}`, - port: environment.WALLET_PORT, - ca, - }, {}, filePath) -); diff --git a/source/renderer/app/api/ada/getAdaAccountRecoveryPhrase.js b/source/renderer/app/api/ada/getAdaAccountRecoveryPhrase.js deleted file mode 100644 index 00be1e1d40..0000000000 --- a/source/renderer/app/api/ada/getAdaAccountRecoveryPhrase.js +++ /dev/null @@ -1,7 +0,0 @@ -// @flow -import type { AdaWalletRecoveryPhraseResponse } from './types'; -import { generateMnemonic } from '../../utils/crypto'; - -export const getAdaAccountRecoveryPhrase = (): AdaWalletRecoveryPhraseResponse => ( - generateMnemonic().split(' ') -); diff --git a/source/renderer/app/api/ada/getAdaAccounts.js b/source/renderer/app/api/ada/getAdaAccounts.js deleted file mode 100644 index 0da0e38354..0000000000 --- a/source/renderer/app/api/ada/getAdaAccounts.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import type { AdaAccounts } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type GetAdaAccountsParams = { - ca: string, -}; - -export const getAdaAccounts = ( - { ca }: GetAdaAccountsParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/accounts', - port: environment.WALLET_PORT, - ca, - }) -); diff --git a/source/renderer/app/api/ada/getAdaAddressHistory.js b/source/renderer/app/api/ada/getAdaAddressHistory.js deleted file mode 100644 index 3643cedd11..0000000000 --- a/source/renderer/app/api/ada/getAdaAddressHistory.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow -import type { AdaTransactions } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type GetAdaAddressHistoryParams = { - ca: string, - accountId: string, - address: string, - skip: number, - limit: number, -}; - -export const getAdaAddressHistory = ( - { ca, accountId, address, skip, limit }: GetAdaAddressHistoryParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/txs/histories', - port: environment.WALLET_PORT, - ca, - }, { accountId, address, skip, limit }) -); diff --git a/source/renderer/app/api/ada/getAdaHistory.js b/source/renderer/app/api/ada/getAdaHistory.js deleted file mode 100644 index 9986eb504d..0000000000 --- a/source/renderer/app/api/ada/getAdaHistory.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow -import type { AdaTransactions } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type GetAdaHistoryParams = { - ca: string, - walletId: ?string, - accountId: ?string, - address: ?string, - skip: number, - limit: number, -}; - -export const getAdaHistory = ( - { ca, walletId, accountId, address, skip, limit }: GetAdaHistoryParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/txs/histories', - port: environment.WALLET_PORT, - ca, - }, { walletId, accountId, address, skip, limit }) -); diff --git a/source/renderer/app/api/ada/getAdaHistoryByAccount.js b/source/renderer/app/api/ada/getAdaHistoryByAccount.js deleted file mode 100644 index d88b028faa..0000000000 --- a/source/renderer/app/api/ada/getAdaHistoryByAccount.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import type { AdaTransactions } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type GetAdaHistoryByAccountParams = { - ca: string, - accountId: string, - skip: number, - limit: number, -}; - -export const getAdaHistoryByAccount = ( - { ca, accountId, skip, limit }: GetAdaHistoryByAccountParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/txs/histories', - port: environment.WALLET_PORT, - ca, - }, { accountId, skip, limit }) -); diff --git a/source/renderer/app/api/ada/getAdaHistoryByWallet.js b/source/renderer/app/api/ada/getAdaHistoryByWallet.js deleted file mode 100644 index 02402f3dd3..0000000000 --- a/source/renderer/app/api/ada/getAdaHistoryByWallet.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import type { AdaTransactions } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type GetAdaHistoryByWalletParams = { - ca: string, - walletId: string, - skip: number, - limit: number, -}; - -export const getAdaHistoryByWallet = ( - { ca, walletId, skip, limit }: GetAdaHistoryByWalletParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/txs/histories', - port: environment.WALLET_PORT, - ca, - }, { walletId, skip, limit }) -); diff --git a/source/renderer/app/api/ada/getAdaLocalTimeDifference.js b/source/renderer/app/api/ada/getAdaLocalTimeDifference.js deleted file mode 100644 index 01bbd989b3..0000000000 --- a/source/renderer/app/api/ada/getAdaLocalTimeDifference.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import type { AdaLocalTimeDifference } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type GetAdaLocalTimeDifferenceParams = { - ca: string, -}; - -export const getAdaLocalTimeDifference = ( - { ca }: GetAdaLocalTimeDifferenceParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/settings/time/difference', - port: environment.WALLET_PORT, - ca, - }) -); diff --git a/source/renderer/app/api/ada/getAdaSyncProgress.js b/source/renderer/app/api/ada/getAdaSyncProgress.js deleted file mode 100644 index a698d210a1..0000000000 --- a/source/renderer/app/api/ada/getAdaSyncProgress.js +++ /dev/null @@ -1,20 +0,0 @@ -// @flow -import type { AdaSyncProgressResponse } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type GetAdaSyncProgressParams = { - ca: string, -}; - -export const getAdaSyncProgress = ( - { ca }: GetAdaSyncProgressParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/settings/sync/progress', - port: environment.WALLET_PORT, - ca, - }) -); diff --git a/source/renderer/app/api/ada/getAdaWalletAccounts.js b/source/renderer/app/api/ada/getAdaWalletAccounts.js deleted file mode 100644 index 997e7b9a7a..0000000000 --- a/source/renderer/app/api/ada/getAdaWalletAccounts.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow -import type { AdaAccounts } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - - -export type GetAdaWalletAccountsParams = { - ca: string, - walletId: string, -}; - -export const getAdaWalletAccounts = ( - { ca, walletId }: GetAdaWalletAccountsParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/accounts', - port: environment.WALLET_PORT, - ca, - }, { accountId: walletId }) -); diff --git a/source/renderer/app/api/ada/getAdaWalletCertificateAdditionalMnemonics.js b/source/renderer/app/api/ada/getAdaWalletCertificateAdditionalMnemonics.js deleted file mode 100644 index 365fe98fcf..0000000000 --- a/source/renderer/app/api/ada/getAdaWalletCertificateAdditionalMnemonics.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import type { AdaWalletCertificateAdditionalMnemonicsResponse } from './types'; -import { generateMnemonic } from '../../utils/crypto'; -import { PAPER_WALLET_WRITTEN_WORDS_COUNT } from '../../config/cryptoConfig'; - -// eslint-disable-next-line -export const getAdaWalletCertificateAdditionalMnemonics = (): AdaWalletCertificateAdditionalMnemonicsResponse => ( - generateMnemonic(PAPER_WALLET_WRITTEN_WORDS_COUNT).split(' ') -); diff --git a/source/renderer/app/api/ada/getAdaWalletCertificateRecoveryPhrase.js b/source/renderer/app/api/ada/getAdaWalletCertificateRecoveryPhrase.js deleted file mode 100644 index 460c1dae8d..0000000000 --- a/source/renderer/app/api/ada/getAdaWalletCertificateRecoveryPhrase.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow -import type { AdaWalletCertificateRecoveryPhraseResponse } from './types'; -import { scramblePaperWalletMnemonic } from '../../utils/crypto'; - -export type GetAdaWalletCertificateRecoveryPhraseParams = { - passphrase: string, - scrambledInput: string, -}; - -export const getAdaWalletCertificateRecoveryPhrase = ( - { passphrase, scrambledInput }: GetAdaWalletCertificateRecoveryPhraseParams -): AdaWalletCertificateRecoveryPhraseResponse => ( - scramblePaperWalletMnemonic(passphrase, scrambledInput) -); diff --git a/source/renderer/app/api/ada/getAdaWalletRecoveryPhraseFromCertificate.js b/source/renderer/app/api/ada/getAdaWalletRecoveryPhraseFromCertificate.js deleted file mode 100644 index afde8cf1c9..0000000000 --- a/source/renderer/app/api/ada/getAdaWalletRecoveryPhraseFromCertificate.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow -import type { AdaWalletRecoveryPhraseFromCertificateResponse } from './types'; -import { unscramblePaperWalletMnemonic } from '../../utils/crypto'; - -export type GetAdaWalletRecoveryPhraseFromCertificateParams = { - passphrase: string, // 9-word mnemonic - scrambledInput: string, // 18-word scrambled mnemonic -}; - -export const getAdaWalletRecoveryPhraseFromCertificate = ( - { passphrase, scrambledInput }: GetAdaWalletRecoveryPhraseFromCertificateParams -): AdaWalletRecoveryPhraseFromCertificateResponse => ( - unscramblePaperWalletMnemonic(passphrase, scrambledInput) -); diff --git a/source/renderer/app/api/ada/getAdaWallets.js b/source/renderer/app/api/ada/getAdaWallets.js deleted file mode 100644 index 1c3fc0604c..0000000000 --- a/source/renderer/app/api/ada/getAdaWallets.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow -import type { AdaV1Wallets } from './types'; -import { request } from './lib/v1/request'; -import { MAX_ADA_WALLETS_COUNT } from '../../config/numbersConfig'; -import environment from '../../../../common/environment'; - - -export type GetAdaWalletParams = { - ca: string, -}; - -export const getAdaWallets = ( - { ca }: GetAdaWalletParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/v1/wallets', - port: environment.WALLET_PORT, - ca, - }, { - per_page: MAX_ADA_WALLETS_COUNT, // 50 is the max per_page value - sort_by: 'ASC[created_at]', - }) -); diff --git a/source/renderer/app/api/ada/importAdaBackupJSON.js b/source/renderer/app/api/ada/importAdaBackupJSON.js deleted file mode 100644 index e1e5ed15b1..0000000000 --- a/source/renderer/app/api/ada/importAdaBackupJSON.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import type { AdaWallet } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type ImportAdaBackupJSONParams = { - ca: string, - filePath: string, -}; - -export const importAdaBackupJSON = ( - { ca, filePath }: ImportAdaBackupJSONParams, -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/backup/import', - port: environment.WALLET_PORT, - ca, - }, {}, filePath) -); diff --git a/source/renderer/app/api/ada/importAdaWallet.js b/source/renderer/app/api/ada/importAdaWallet.js deleted file mode 100644 index b9118df62b..0000000000 --- a/source/renderer/app/api/ada/importAdaWallet.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow -import type { AdaWallet } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type ImportAdaWalletParams = { - ca: string, - filePath: string, - walletPassword: ?string, -}; - -export const importAdaWallet = ( - { ca, walletPassword, filePath }: ImportAdaWalletParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/wallets/keys', - port: environment.WALLET_PORT, - ca, - }, { passphrase: walletPassword }, filePath) -); diff --git a/source/renderer/app/api/ada/index.js b/source/renderer/app/api/ada/index.js deleted file mode 100644 index ae5fed56e8..0000000000 --- a/source/renderer/app/api/ada/index.js +++ /dev/null @@ -1,874 +0,0 @@ -// @flow -import { split, get } from 'lodash'; -import { action } from 'mobx'; -import { ipcRenderer, remote } from 'electron'; -import BigNumber from 'bignumber.js'; -import { Logger, stringifyData, stringifyError } from '../../../../common/logging'; -import { unixTimestampToDate } from './lib/utils'; -import Wallet from '../../domains/Wallet'; -import WalletTransaction, { transactionTypes } from '../../domains/WalletTransaction'; -import WalletAddress from '../../domains/WalletAddress'; -import { isValidMnemonic } from '../../../../common/decrypt'; -import { isValidRedemptionKey, isValidPaperVendRedemptionKey } from '../../../../common/redemption-key-validation'; -import { LOVELACES_PER_ADA } from '../../config/numbersConfig'; -import { getAdaSyncProgress } from './getAdaSyncProgress'; -import environment from '../../../../common/environment'; -import patchAdaApi from './mocks/patchAdaApi'; - -import { getAdaWallets } from './getAdaWallets'; -import { changeAdaWalletPassphrase } from './changeAdaWalletPassphrase'; -import { deleteAdaWallet } from './deleteAdaWallet'; -import { newAdaWallet } from './newAdaWallet'; -import { newAdaWalletAddress } from './newAdaWalletAddress'; -import { restoreAdaWallet } from './restoreAdaWallet'; -import { updateAdaWallet } from './updateAdaWallet'; -import { exportAdaBackupJSON } from './exportAdaBackupJSON'; -import { importAdaBackupJSON } from './importAdaBackupJSON'; -import { importAdaWallet } from './importAdaWallet'; -import { getAdaWalletAccounts } from './getAdaWalletAccounts'; -import { isValidAdaAddress } from './isValidAdaAddress'; -import { adaTxFee } from './adaTxFee'; -import { newAdaPayment } from './newAdaPayment'; -import { redeemAda } from './redeemAda'; -import { redeemAdaPaperVend } from './redeemAdaPaperVend'; -import { nextAdaUpdate } from './nextAdaUpdate'; -import { postponeAdaUpdate } from './postponeAdaUpdate'; -import { applyAdaUpdate } from './applyAdaUpdate'; -import { adaTestReset } from './adaTestReset'; -import { getAdaHistoryByWallet } from './getAdaHistoryByWallet'; -import { getAdaAccountRecoveryPhrase } from './getAdaAccountRecoveryPhrase'; -import { getAdaWalletCertificateAdditionalMnemonics } from './getAdaWalletCertificateAdditionalMnemonics'; -import { getAdaWalletCertificateRecoveryPhrase } from './getAdaWalletCertificateRecoveryPhrase'; -import { getAdaWalletRecoveryPhraseFromCertificate } from './getAdaWalletRecoveryPhraseFromCertificate'; -import { getAdaLocalTimeDifference } from './getAdaLocalTimeDifference'; -import { sendAdaBugReport } from './sendAdaBugReport'; - -import type { - AdaLocalTimeDifference, - AdaSyncProgressResponse, - AdaAddress, - AdaAccounts, - AdaTransaction, - AdaTransactionFee, - AdaTransactions, - AdaWallet, - AdaV1Wallet, - AdaV1Wallets, - AdaWalletRecoveryPhraseResponse, - AdaWalletCertificateAdditionalMnemonicsResponse, - AdaWalletCertificateRecoveryPhraseResponse, - GetWalletCertificateAdditionalMnemonicsResponse, - GetWalletCertificateRecoveryPhraseResponse, - GetWalletRecoveryPhraseFromCertificateResponse, -} from './types'; - -import type { - CreateWalletRequest, - CreateWalletResponse, - CreateTransactionResponse, - DeleteWalletRequest, - DeleteWalletResponse, - GetLocalTimeDifferenceResponse, - GetSyncProgressResponse, - GetTransactionsRequest, - GetTransactionsResponse, - GetWalletRecoveryPhraseResponse, - GetWalletsResponse, - RestoreWalletRequest, - RestoreWalletResponse, - SendBugReportRequest, - SendBugReportResponse, - UpdateWalletResponse, - UpdateWalletPasswordRequest, - UpdateWalletPasswordResponse, -} from '../common'; - -import { - GenericApiError, - IncorrectWalletPasswordError, - WalletAlreadyRestoredError, - ReportRequestError, InvalidMnemonicError, -} from '../common'; - -import { - AllFundsAlreadyAtReceiverAddressError, - NotAllowedToSendMoneyToRedeemAddressError, - NotAllowedToSendMoneyToSameAddressError, - NotEnoughFundsForTransactionFeesError, - NotEnoughMoneyToSendError, - RedeemAdaError, - WalletAlreadyImportedError, - WalletFileImportError, -} from './errors'; - -import { - ADA_CERTIFICATE_MNEMONIC_LENGHT, - ADA_REDEMPTION_PASSPHRASE_LENGHT, - WALLET_RECOVERY_PHRASE_WORD_COUNT -} from '../../config/cryptoConfig'; - -import { AdaV1AssuranceOptions } from './types'; -import { assuranceModeOptions } from '../../types/transactionAssuranceTypes'; - -/** - * The api layer that is used for all requests to the - * cardano backend when working with the ADA coin. - */ - -const ca = remote.getGlobal('ca'); - -// ADA specific Request / Response params -export type GetAddressesResponse = { - accountId: ?string, - addresses: Array, -}; -export type GetAddressesRequest = { - walletId: string, -}; -export type CreateAddressResponse = WalletAddress; -export type CreateAddressRequest = { - accountId: string, - password: ?string, -}; - -export type CreateTransactionRequest = { - sender: string, - receiver: string, - amount: string, - password?: ?string, -}; -export type UpdateWalletRequest = { - walletId: string, - name: string, - assurance: string, -}; -export type RedeemAdaRequest = { - redemptionCode: string, - accountId: string, - walletPassword: ?string, -}; -export type RedeemAdaResponse = Wallet; -export type RedeemPaperVendedAdaRequest = { - shieldedRedemptionKey: string, - mnemonics: string, - accountId: string, - walletPassword: ?string, -}; -export type RedeemPaperVendedAdaResponse = RedeemPaperVendedAdaRequest; -export type ImportWalletFromKeyRequest = { - filePath: string, - walletPassword: ?string, -}; -export type ImportWalletFromKeyResponse = Wallet; -export type ImportWalletFromFileRequest = { - filePath: string, - walletPassword: ?string, - walletName: ?string, -}; -export type ImportWalletFromFileResponse = Wallet; -export type NextUpdateResponse = ?{ - version: ?string, -}; -export type PostponeUpdateResponse = Promise; -export type ApplyUpdateResponse = Promise; - -export type TransactionFeeRequest = { - sender: string, - receiver: string, - amount: string, -}; -export type TransactionFeeResponse = BigNumber; -export type ExportWalletToFileRequest = { - walletId: string, - filePath: string, - password: ?string -}; -export type ExportWalletToFileResponse = []; -export type GetWalletCertificateRecoveryPhraseRequest = { - passphrase: string, - input: string, -}; -export type GetWalletRecoveryPhraseFromCertificateRequest = { - passphrase: string, - scrambledInput: string, -}; - -// const notYetImplemented = () => new Promise((_, reject) => { -// reject(new ApiMethodNotYetImplementedError()); -// }); - -// Commented out helper code for testing async APIs -// (async () => { -// const result = await ClientApi.nextUpdate(); -// console.log('nextUpdate', result); -// })(); - -// Commented out helper code for testing sync APIs -// (() => { -// const result = ClientApi.isValidRedeemCode('HSoXEnt9X541uHvtzBpy8vKfTo1C9TkAX3wat2c6ikg='); -// console.log('isValidRedeemCode', result); -// })(); - - -export default class AdaApi { - - constructor() { - if (environment.isTest()) { - patchAdaApi(this); - } - } - - async getWallets(): Promise { - Logger.debug('AdaApi::getWallets called'); - try { - const response: AdaV1Wallets = await getAdaWallets({ ca }); - Logger.debug('AdaApi::getWallets success: ' + stringifyData(response)); - return response.map(data => _createWalletFromServerV1Data(data)); - } catch (error) { - Logger.error('AdaApi::getWallets error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async getAddresses(request: GetAddressesRequest): Promise { - Logger.debug('AdaApi::getAddresses called: ' + stringifyData(request)); - const { walletId } = request; - try { - const response: AdaAccounts = await getAdaWalletAccounts({ ca, walletId }); - Logger.debug('AdaApi::getAddresses success: ' + stringifyData(response)); - if (!response.length) { - return new Promise((resolve) => resolve({ accountId: null, addresses: [] })); - } - // For now only the first wallet account is used - const firstAccount = response[0]; - const firstAccountId = firstAccount.caId; - const firstAccountAddresses = firstAccount.caAddresses; - - return new Promise((resolve) => resolve({ - accountId: firstAccountId, - addresses: firstAccountAddresses.map(data => _createAddressFromServerData(data)), - })); - } catch (error) { - Logger.error('AdaApi::getAddresses error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async getTransactions(request: GetTransactionsRequest): Promise { - Logger.debug('AdaApi::searchHistory called: ' + stringifyData(request)); - const { walletId, skip, limit } = request; - try { - const history: AdaTransactions = await getAdaHistoryByWallet({ ca, walletId, skip, limit }); - Logger.debug('AdaApi::searchHistory success: ' + stringifyData(history)); - return new Promise((resolve) => resolve({ - transactions: history[0].map(data => _createTransactionFromServerData(data)), - total: history[1] - })); - } catch (error) { - Logger.error('AdaApi::searchHistory error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async createWallet(request: CreateWalletRequest): Promise { - Logger.debug('AdaApi::createWallet called'); - const { name, mnemonic, password } = request; - const assurance = 'CWANormal'; - const unit = 0; - try { - const walletInitData = { - cwInitMeta: { - cwName: name, - cwAssurance: assurance, - cwUnit: unit, - }, - cwBackupPhrase: { - bpToList: split(mnemonic), // array of mnemonic words - } - }; - const wallet: AdaWallet = await newAdaWallet({ ca, password, walletInitData }); - Logger.debug('AdaApi::createWallet success'); - return _createWalletFromServerData(wallet); - } catch (error) { - Logger.error('AdaApi::createWallet error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async deleteWallet(request: DeleteWalletRequest): Promise { - Logger.debug('AdaApi::deleteWallet called: ' + stringifyData(request)); - try { - const { walletId } = request; - await deleteAdaWallet({ ca, walletId }); - Logger.debug('AdaApi::deleteWallet success: ' + stringifyData(request)); - return true; - } catch (error) { - Logger.error('AdaApi::deleteWallet error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async createTransaction(request: CreateTransactionRequest): Promise { - Logger.debug('AdaApi::createTransaction called'); - const { sender, receiver, amount, password } = request; - // sender must be set as accountId (account.caId) and not walletId - try { - // default value. Select (OptimizeForSecurity | OptimizeForSize) will be implemented - const groupingPolicy = 'OptimizeForSecurity'; - const response: AdaTransaction = await newAdaPayment( - { ca, sender, receiver, amount, groupingPolicy, password } - ); - Logger.debug('AdaApi::createTransaction success: ' + stringifyData(response)); - return _createTransactionFromServerData(response); - } catch (error) { - Logger.debug('AdaApi::createTransaction error: ' + stringifyError(error)); - // eslint-disable-next-line max-len - if (error.message.includes('It\'s not allowed to send money to the same address you are sending from')) { - throw new NotAllowedToSendMoneyToSameAddressError(); - } - if (error.message.includes('Destination address can\'t be redeem address')) { - throw new NotAllowedToSendMoneyToRedeemAddressError(); - } - if (error.message.includes('Not enough money')) { - throw new NotEnoughMoneyToSendError(); - } - if (error.message.includes('Passphrase doesn\'t match')) { - throw new IncorrectWalletPasswordError(); - } - throw new GenericApiError(); - } - } - - async calculateTransactionFee(request: TransactionFeeRequest): Promise { - Logger.debug('AdaApi::calculateTransactionFee called'); - const { sender, receiver, amount } = request; - try { - // default value. Select (OptimizeForSecurity | OptimizeForSize) will be implemented - const groupingPolicy = 'OptimizeForSecurity'; - const response: adaTxFee = await adaTxFee( - { ca, sender, receiver, amount, groupingPolicy } - ); - Logger.debug('AdaApi::calculateTransactionFee success: ' + stringifyData(response)); - return _createTransactionFeeFromServerData(response); - } catch (error) { - Logger.debug('AdaApi::calculateTransactionFee error: ' + stringifyError(error)); - // eslint-disable-next-line max-len - if (error.message.includes('not enough money on addresses which are not included in output addresses set')) { - throw new AllFundsAlreadyAtReceiverAddressError(); - } - if (error.message.includes('not enough money')) { - throw new NotEnoughFundsForTransactionFeesError(); - } - throw new GenericApiError(); - } - } - - async createAddress(request: CreateAddressRequest): Promise { - Logger.debug('AdaApi::createAddress called'); - const { accountId, password } = request; - try { - const response: AdaAddress = await newAdaWalletAddress( - { ca, password, accountId } - ); - Logger.debug('AdaApi::createAddress success: ' + stringifyData(response)); - return _createAddressFromServerData(response); - } catch (error) { - Logger.debug('AdaApi::createAddress error: ' + stringifyError(error)); - if (error.message.includes('Passphrase doesn\'t match')) { - throw new IncorrectWalletPasswordError(); - } - throw new GenericApiError(); - } - } - - isValidAddress(address: string): Promise { - return isValidAdaAddress({ ca, address }); - } - - isValidMnemonic(mnemonic: string): Promise { - return isValidMnemonic(mnemonic, WALLET_RECOVERY_PHRASE_WORD_COUNT); - } - - isValidRedemptionKey(mnemonic: string): Promise { - return isValidRedemptionKey(mnemonic); - } - - isValidPaperVendRedemptionKey(mnemonic: string): Promise { - return isValidPaperVendRedemptionKey(mnemonic); - } - - isValidRedemptionMnemonic(mnemonic: string): Promise { - return isValidMnemonic(mnemonic, ADA_REDEMPTION_PASSPHRASE_LENGHT); - } - - isValidCertificateMnemonic(mnemonic: string): boolean { - return mnemonic.split(' ').length === ADA_CERTIFICATE_MNEMONIC_LENGHT; - } - - getWalletRecoveryPhrase(): Promise { - Logger.debug('AdaApi::getWalletRecoveryPhrase called'); - try { - const response: Promise = new Promise( - (resolve) => resolve(getAdaAccountRecoveryPhrase()) - ); - Logger.debug('AdaApi::getWalletRecoveryPhrase success'); - return response; - } catch (error) { - Logger.error('AdaApi::getWalletRecoveryPhrase error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - // eslint-disable-next-line max-len - getWalletCertificateAdditionalMnemonics(): Promise { - Logger.debug('AdaApi::getWalletCertificateAdditionalMnemonics called'); - try { - const response: Promise = new Promise( - (resolve) => resolve(getAdaWalletCertificateAdditionalMnemonics()) - ); - Logger.debug('AdaApi::getWalletCertificateAdditionalMnemonics success'); - return response; - } catch (error) { - Logger.error('AdaApi::getWalletCertificateAdditionalMnemonics error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - getWalletCertificateRecoveryPhrase( - request: GetWalletCertificateRecoveryPhraseRequest - ): Promise { - Logger.debug('AdaApi::getWalletCertificateRecoveryPhrase called'); - const { passphrase, input } = request; - try { - const response: Promise = new Promise( - (resolve) => resolve(getAdaWalletCertificateRecoveryPhrase({ - passphrase, - scrambledInput: input, - })) - ); - Logger.debug('AdaApi::getWalletCertificateRecoveryPhrase success'); - return response; - } catch (error) { - Logger.error('AdaApi::getWalletCertificateRecoveryPhrase error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - getWalletRecoveryPhraseFromCertificate( - request: GetWalletRecoveryPhraseFromCertificateRequest - ): Promise { - Logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate called'); - const { passphrase, scrambledInput } = request; - try { - const response = getAdaWalletRecoveryPhraseFromCertificate({ passphrase, scrambledInput }); - Logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate success'); - return Promise.resolve(response); - } catch (error) { - Logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate error: ' + stringifyError(error)); - return Promise.reject(new InvalidMnemonicError()); - } - } - - async restoreWallet(request: RestoreWalletRequest): Promise { - Logger.debug('AdaApi::restoreWallet called'); - const { recoveryPhrase, walletName, walletPassword } = request; - const assurance = 'CWANormal'; - const unit = 0; - - const walletInitData = { - cwInitMeta: { - cwName: walletName, - cwAssurance: assurance, - cwUnit: unit, - }, - cwBackupPhrase: { - bpToList: split(recoveryPhrase), // array of mnemonic words - } - }; - - try { - const wallet: AdaWallet = await restoreAdaWallet( - { ca, walletPassword, walletInitData } - ); - Logger.debug('AdaApi::restoreWallet success'); - return _createWalletFromServerData(wallet); - } catch (error) { - Logger.debug('AdaApi::restoreWallet error: ' + stringifyError(error)); - // TODO: backend will return something different here, if multiple wallets - // are restored from the key and if there are duplicate wallets we will get - // some kind of error and present the user with message that some wallets - // where not imported/restored if some where. if no wallets are imported - // we will error out completely with throw block below - if (error.message.includes('Wallet with that mnemonics already exists')) { - throw new WalletAlreadyRestoredError(); - } - // We don't know what the problem was -> throw generic error - throw new GenericApiError(); - } - } - - async importWalletFromKey( - request: ImportWalletFromKeyRequest - ): Promise { - Logger.debug('AdaApi::importWalletFromKey called'); - const { filePath, walletPassword } = request; - try { - const importedWallet: AdaWallet = await importAdaWallet( - { ca, walletPassword, filePath } - ); - Logger.debug('AdaApi::importWalletFromKey success'); - return _createWalletFromServerData(importedWallet); - } catch (error) { - Logger.debug('AdaApi::importWalletFromKey error: ' + stringifyError(error)); - if (error.message.includes('already exists')) { - throw new WalletAlreadyImportedError(); - } - throw new WalletFileImportError(); - } - } - - async importWalletFromFile( - request: ImportWalletFromFileRequest - ): Promise { - Logger.debug('AdaApi::importWalletFromFile called'); - const { filePath, walletPassword } = request; - const isKeyFile = filePath.split('.').pop().toLowerCase() === 'key'; - try { - const importedWallet: AdaWallet = isKeyFile ? ( - await importAdaWallet({ ca, walletPassword, filePath }) - ) : ( - await importAdaBackupJSON({ ca, filePath }) - ); - Logger.debug('AdaApi::importWalletFromFile success'); - return _createWalletFromServerData(importedWallet); - } catch (error) { - Logger.debug('AdaApi::importWalletFromFile error: ' + stringifyError(error)); - if (error.message.includes('already exists')) { - throw new WalletAlreadyImportedError(); - } - throw new WalletFileImportError(); - } - } - - async redeemAda(request: RedeemAdaRequest): Promise { - Logger.debug('AdaApi::redeemAda called'); - const { redemptionCode, accountId, walletPassword } = request; - try { - const walletRedeemData = { - crWalletId: accountId, - crSeed: redemptionCode, - }; - - const response: AdaTransaction = await redeemAda( - { ca, walletPassword, walletRedeemData } - ); - - Logger.debug('AdaApi::redeemAda success'); - return _createTransactionFromServerData(response); - } catch (error) { - Logger.debug('AdaApi::redeemAda error: ' + stringifyError(error)); - if (error.message.includes('Passphrase doesn\'t match')) { - throw new IncorrectWalletPasswordError(); - } - throw new RedeemAdaError(); - } - } - - async redeemPaperVendedAda( - request: RedeemPaperVendedAdaRequest - ): Promise { - Logger.debug('AdaApi::redeemAdaPaperVend called'); - const { shieldedRedemptionKey, mnemonics, accountId, walletPassword } = request; - try { - const redeemPaperVendedData = { - pvWalletId: accountId, - pvSeed: shieldedRedemptionKey, - pvBackupPhrase: { - bpToList: split(mnemonics), - } - }; - - const response: AdaTransaction = await redeemAdaPaperVend( - { ca, walletPassword, redeemPaperVendedData } - ); - - Logger.debug('AdaApi::redeemAdaPaperVend success'); - return _createTransactionFromServerData(response); - } catch (error) { - Logger.debug('AdaApi::redeemAdaPaperVend error: ' + stringifyError(error)); - if (error.message.includes('Passphrase doesn\'t match')) { - throw new IncorrectWalletPasswordError(); - } - throw new RedeemAdaError(); - } - } - - async sendBugReport(requestFormData: SendBugReportRequest): Promise { - Logger.debug('AdaApi::sendBugReport called: ' + stringifyData(requestFormData)); - try { - await sendAdaBugReport({ requestFormData }); - Logger.debug('AdaApi::sendBugReport success'); - return true; - } catch (error) { - Logger.error('AdaApi::sendBugReport error: ' + stringifyError(error)); - throw new ReportRequestError(); - } - } - - async nextUpdate(): Promise { - Logger.debug('AdaApi::nextUpdate called'); - let nextUpdate = null; - try { - // TODO: add flow type definitions for nextUpdate response - const response: Promise = await nextAdaUpdate({ ca }); - Logger.debug('AdaApi::nextUpdate success: ' + stringifyData(response)); - if (response && response.cuiSoftwareVersion) { - nextUpdate = { - version: get(response, ['cuiSoftwareVersion', 'svNumber'], null) - }; - } - } catch (error) { - if (error.message.includes('No updates available')) { - Logger.debug('AdaApi::nextUpdate success: No updates available'); - } else { - Logger.error('AdaApi::nextUpdate error: ' + stringifyError(error)); - } - // throw new GenericApiError(); - } - return nextUpdate; - // TODO: remove hardcoded response after node update is tested - // nextUpdate = { - // cuiSoftwareVersion: { - // svAppName: { - // getApplicationName: 'cardano' - // }, - // svNumber: 1 - // }, - // cuiBlockVesion: { - // bvMajor: 0, - // bvMinor: 1, - // bvAlt: 0 - // }, - // cuiScriptVersion: 1, - // cuiImplicit: false, - // cuiVotesFor: 2, - // cuiVotesAgainst: 0, - // cuiPositiveStake: { - // getCoin: 66666 - // }, - // cuiNegativeStake: { - // getCoin: 0 - // } - // }; - // if (nextUpdate && nextUpdate.cuiSoftwareVersion && nextUpdate.cuiSoftwareVersion.svNumber) { - // return { version: nextUpdate.cuiSoftwareVersion.svNumber }; - // } else if (nextUpdate) { - // return { version: null }; - // } - // return null; - } - - async postponeUpdate(): PostponeUpdateResponse { - Logger.debug('AdaApi::postponeUpdate called'); - try { - const response: Promise = await postponeAdaUpdate({ ca }); - Logger.debug('AdaApi::postponeUpdate success: ' + stringifyData(response)); - } catch (error) { - Logger.error('AdaApi::postponeUpdate error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async applyUpdate(): ApplyUpdateResponse { - Logger.debug('AdaApi::applyUpdate called'); - try { - const response: Promise = await applyAdaUpdate({ ca }); - Logger.debug('AdaApi::applyUpdate success: ' + stringifyData(response)); - ipcRenderer.send('kill-process'); - } catch (error) { - Logger.error('AdaApi::applyUpdate error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - getSyncProgress = async (): Promise => { - Logger.debug('AdaApi::syncProgress called'); - try { - const response: AdaSyncProgressResponse = await getAdaSyncProgress({ ca }); - Logger.debug('AdaApi::syncProgress success: ' + stringifyData(response)); - const localDifficulty = response._spLocalCD.getChainDifficulty.getBlockCount; - // In some cases we dont get network difficulty & we need to wait for it from the notify API - let networkDifficulty = null; - if (response._spNetworkCD) { - networkDifficulty = response._spNetworkCD.getChainDifficulty.getBlockCount; - } - return { localDifficulty, networkDifficulty }; - } catch (error) { - Logger.debug('AdaApi::syncProgress error: ' + stringifyError(error)); - throw new GenericApiError(); - } - }; - - async updateWallet(request: UpdateWalletRequest): Promise { - Logger.debug('AdaApi::updateWallet called: ' + stringifyData(request)); - const { walletId, name, assurance } = request; - const unit = 0; - - const walletMeta = { - cwName: name, - cwAssurance: assurance, - cwUnit: unit, - }; - - try { - const wallet: AdaWallet = await updateAdaWallet({ ca, walletId, walletMeta }); - Logger.debug('AdaApi::updateWallet success: ' + stringifyData(wallet)); - return _createWalletFromServerData(wallet); - } catch (error) { - Logger.error('AdaApi::updateWallet error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async updateWalletPassword( - request: UpdateWalletPasswordRequest - ): Promise { - Logger.debug('AdaApi::updateWalletPassword called'); - const { walletId, oldPassword, newPassword } = request; - try { - await changeAdaWalletPassphrase({ ca, walletId, oldPassword, newPassword }); - Logger.debug('AdaApi::updateWalletPassword success'); - return true; - } catch (error) { - Logger.debug('AdaApi::updateWalletPassword error: ' + stringifyError(error)); - if (error.message.includes('Invalid old passphrase given')) { - throw new IncorrectWalletPasswordError(); - } - throw new GenericApiError(); - } - } - - async exportWalletToFile( - request: ExportWalletToFileRequest - ): Promise { - const { walletId, filePath } = request; - Logger.debug('AdaApi::exportWalletToFile called'); - try { - const response: Promise<[]> = await exportAdaBackupJSON({ ca, walletId, filePath }); - Logger.debug('AdaApi::exportWalletToFile success: ' + stringifyData(response)); - return response; - } catch (error) { - Logger.error('AdaApi::exportWalletToFile error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async testReset(): Promise { - Logger.debug('AdaApi::testReset called'); - try { - const response: Promise = await adaTestReset({ ca }); - Logger.debug('AdaApi::testReset success: ' + stringifyData(response)); - return response; - } catch (error) { - Logger.error('AdaApi::testReset error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async getLocalTimeDifference(): Promise { - Logger.debug('AdaApi::getLocalTimeDifference called'); - try { - const response: AdaLocalTimeDifference = await getAdaLocalTimeDifference({ ca }); - Logger.debug('AdaApi::getLocalTimeDifference success: ' + stringifyData(response)); - return Math.abs(response); // time offset direction is irrelevant to the UI - } catch (error) { - Logger.error('AdaApi::getLocalTimeDifference error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } -} - -// ========== TRANSFORM SERVER DATA INTO FRONTEND MODELS ========= - -const _createWalletFromServerData = action( - 'AdaApi::_createWalletFromServerData', (data: AdaWallet) => ( - new Wallet({ - id: data.cwId, - amount: new BigNumber(data.cwAmount.getCCoin).dividedBy(LOVELACES_PER_ADA), - name: data.cwMeta.cwName, - assurance: data.cwMeta.cwAssurance, - hasPassword: data.cwHasPassphrase, - passwordUpdateDate: unixTimestampToDate(data.cwPassphraseLU), - }) - ) -); - -const _createAddressFromServerData = action( - 'AdaApi::_createAddressFromServerData', (data: AdaAddress) => ( - new WalletAddress({ - id: data.cadId, - amount: new BigNumber(data.cadAmount.getCCoin).dividedBy(LOVELACES_PER_ADA), - isUsed: data.cadIsUsed, - }) - ) -); - -const _conditionToTxState = (condition: string) => { - switch (condition) { - case 'CPtxApplying': return 'pending'; - case 'CPtxWontApply': return 'failed'; - default: return 'ok'; // CPtxInBlocks && CPtxNotTracked - } -}; - -const _createTransactionFromServerData = action( - 'AdaApi::_createTransactionFromServerData', (data: AdaTransaction) => { - const coins = data.ctAmount.getCCoin; - const { ctmTitle, ctmDescription, ctmDate } = data.ctMeta; - return new WalletTransaction({ - id: data.ctId, - title: ctmTitle || data.ctIsOutgoing ? 'Ada sent' : 'Ada received', - type: data.ctIsOutgoing ? transactionTypes.EXPEND : transactionTypes.INCOME, - amount: new BigNumber(data.ctIsOutgoing ? -1 * coins : coins).dividedBy(LOVELACES_PER_ADA), - date: unixTimestampToDate(ctmDate), - description: ctmDescription || '', - numberOfConfirmations: data.ctConfirmations, - addresses: { - from: data.ctInputs.map(address => address[0]), - to: data.ctOutputs.map(address => address[0]), - }, - state: _conditionToTxState(data.ctCondition), - }); - } -); - -const _createTransactionFeeFromServerData = action( - 'AdaApi::_createTransactionFeeFromServerData', (data: AdaTransactionFee) => { - const coins = data.getCCoin; - return new BigNumber(coins).dividedBy(LOVELACES_PER_ADA); - } -); - - -// ========== V1 API ========= - -const _createWalletFromServerV1Data = action( - 'AdaApi::_createWalletFromServerV1Data', (data: AdaV1Wallet) => { - const { - id, balance, name, assuranceLevel, - hasSpendingPassword, spendingPasswordLastUpdate, - syncState, - } = data; - return new Wallet({ - id, - amount: new BigNumber(balance).dividedBy(LOVELACES_PER_ADA), - name, - assurance: (assuranceLevel === AdaV1AssuranceOptions.NORMAL ? - assuranceModeOptions.NORMAL : assuranceModeOptions.STRICT - ), - hasPassword: hasSpendingPassword, - passwordUpdateDate: new Date(`${spendingPasswordLastUpdate}Z`), - syncState, - }); - } -); diff --git a/source/renderer/app/api/ada/isValidAdaAddress.js b/source/renderer/app/api/ada/isValidAdaAddress.js deleted file mode 100644 index d32237de2b..0000000000 --- a/source/renderer/app/api/ada/isValidAdaAddress.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type IsValidAdaAddressParams = { - ca: string, - address: string, -}; - -export const isValidAdaAddress = ( - { ca, address }: IsValidAdaAddressParams -): Promise => { - const encodedAddress = encodeURIComponent(address); - return request({ - hostname: 'localhost', - method: 'GET', - path: `/api/addresses/${encodedAddress}`, - port: environment.WALLET_PORT, - ca, - }); -}; diff --git a/source/renderer/app/api/ada/lib/encryptPassphrase.js b/source/renderer/app/api/ada/lib/encryptPassphrase.js deleted file mode 100644 index 178d31ce08..0000000000 --- a/source/renderer/app/api/ada/lib/encryptPassphrase.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import blakejs from 'blakejs'; - -const bytesToB16 = (bytes) => Buffer.from(bytes).toString('hex'); -const blake2b = (data) => blakejs.blake2b(data, null, 32); - -export const encryptPassphrase = (passphrase: string) => ( - bytesToB16(blake2b(passphrase)) -); diff --git a/source/renderer/app/api/ada/lib/utils.js b/source/renderer/app/api/ada/lib/utils.js deleted file mode 100644 index d2478daddb..0000000000 --- a/source/renderer/app/api/ada/lib/utils.js +++ /dev/null @@ -1,2 +0,0 @@ -// @flow -export const unixTimestampToDate = (timestamp: number) => new Date(timestamp * 1000); diff --git a/source/renderer/app/api/ada/mocks/patchAdaApi.js b/source/renderer/app/api/ada/mocks/patchAdaApi.js deleted file mode 100644 index 991d940389..0000000000 --- a/source/renderer/app/api/ada/mocks/patchAdaApi.js +++ /dev/null @@ -1,49 +0,0 @@ -import BigNumber from 'bignumber.js'; -import { Logger } from '../../../../../common/logging'; -import { RedeemAdaError } from '../errors'; -import AdaApi from '../index'; -import type { - RedeemPaperVendedAdaRequest, - RedeemAdaRequest -} from '../index'; - -// ========== LOGGING ========= - -let LOCAL_TIME_DIFFERENCE = 0; - -const stringifyData = (data) => JSON.stringify(data, null, 2); - -export default (api: AdaApi) => { - // Since we cannot test ada redemption in dev mode, just resolve the requests - api.redeemAda = async (request: RedeemAdaRequest) => { - Logger.debug('AdaApi::redeemAda (PATCHED) called: ' + stringifyData(request)); - const { redemptionCode } = request; - const isValidRedemptionCode = await api.isValidRedemptionKey(redemptionCode); - if (!isValidRedemptionCode) { - Logger.debug('AdaApi::redeemAda failed: not a valid redemption key!'); - throw new RedeemAdaError(); - } - return { amount: new BigNumber(1000) }; - }; - - api.redeemPaperVendedAda = async (request: RedeemPaperVendedAdaRequest) => { - Logger.debug('AdaApi::redeemPaperVendedAda (PATCHED) called: ' + stringifyData(request)); - const { shieldedRedemptionKey, mnemonics } = request; - const isValidKey = await api.isValidPaperVendRedemptionKey(shieldedRedemptionKey); - const isValidMnemonic = await api.isValidRedemptionMnemonic(mnemonics); - if (!isValidKey) Logger.debug('AdaApi::redeemPaperVendedAda failed: not a valid redemption key!'); - if (!isValidMnemonic) Logger.debug('AdaApi::redeemPaperVendedAda failed: not a valid mnemonic!'); - if (!isValidKey || !isValidMnemonic) { - throw new RedeemAdaError(); - } - return { amount: new BigNumber(1000) }; - }; - - api.getLocalTimeDifference = async () => ( - Promise.resolve(LOCAL_TIME_DIFFERENCE) - ); - - api.setLocalTimeDifference = async (timeDifference) => { - LOCAL_TIME_DIFFERENCE = timeDifference; - }; -}; diff --git a/source/renderer/app/api/ada/newAdaAccount.js b/source/renderer/app/api/ada/newAdaAccount.js deleted file mode 100644 index cddb9183ef..0000000000 --- a/source/renderer/app/api/ada/newAdaAccount.js +++ /dev/null @@ -1,33 +0,0 @@ -// @flow -import type { AdaAccount } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type NewAdaAccountQueryParams = { - passphrase: ?string, -}; - -export type NewAdaAccountRawBodyParams = { - accountInitData: { - caInitMeta: { - caName: string, - }, - caInitWId: string, - } -}; - -export const newAdaAccount = ( - ca: string, - pathParams: {}, - queryParams: NewAdaAccountQueryParams, - rawBodyParams: NewAdaAccountRawBodyParams, -): Promise => { - const { accountInitData } = rawBodyParams; - return request({ - hostname: 'localhost', - method: 'POST', - path: '/api/accounts', - port: environment.WALLET_PORT, - ca, - }, queryParams, accountInitData); -}; diff --git a/source/renderer/app/api/ada/newAdaPayment.js b/source/renderer/app/api/ada/newAdaPayment.js deleted file mode 100644 index f968941b3a..0000000000 --- a/source/renderer/app/api/ada/newAdaPayment.js +++ /dev/null @@ -1,28 +0,0 @@ -// @flow -import type { AdaTransaction } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type NewAdaPaymentParams = { - ca: string, - sender: string, - receiver: string, - amount: string, - password: ?string, - // "groupingPolicy" - Spend everything from the address - // "OptimizeForSize" for no grouping - groupingPolicy: ?'OptimizeForSecurity' | 'OptimizeForSize', -}; - - -export const newAdaPayment = ( - { ca, sender, receiver, amount, groupingPolicy, password }: NewAdaPaymentParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: `/api/txs/payments/${sender}/${receiver}/${amount}`, - port: environment.WALLET_PORT, - ca, - }, { passphrase: password }, { groupingPolicy }) -); diff --git a/source/renderer/app/api/ada/newAdaWallet.js b/source/renderer/app/api/ada/newAdaWallet.js deleted file mode 100644 index 1f95b68dbb..0000000000 --- a/source/renderer/app/api/ada/newAdaWallet.js +++ /dev/null @@ -1,22 +0,0 @@ -// @flow -import type { AdaWallet, AdaWalletInitData } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type NewAdaWalletParams = { - ca: string, - password: ?string, - walletInitData: AdaWalletInitData -}; - -export const newAdaWallet = ( - { ca, password, walletInitData }: NewAdaWalletParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/wallets/new', - port: environment.WALLET_PORT, - ca, - }, { passphrase: password }, walletInitData) -); diff --git a/source/renderer/app/api/ada/newAdaWalletAddress.js b/source/renderer/app/api/ada/newAdaWalletAddress.js deleted file mode 100644 index 0bb0eb1f43..0000000000 --- a/source/renderer/app/api/ada/newAdaWalletAddress.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import type { AdaAddress } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type NewAdaWalletAddressParams = { - ca: string, - password: ?string, - accountId: string, -}; - -export const newAdaWalletAddress = ( - { ca, password, accountId }: NewAdaWalletAddressParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/addresses', - port: environment.WALLET_PORT, - ca, - }, { passphrase: password }, accountId) -); - diff --git a/source/renderer/app/api/ada/nextAdaUpdate.js b/source/renderer/app/api/ada/nextAdaUpdate.js deleted file mode 100644 index 3bacc1bc5e..0000000000 --- a/source/renderer/app/api/ada/nextAdaUpdate.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type NextAdaUpdateParams = { - ca: string, -}; - -export const nextAdaUpdate = ( - { ca }: NextAdaUpdateParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'GET', - path: '/api/update', - port: environment.WALLET_PORT, - ca, - }) -); diff --git a/source/renderer/app/api/ada/postponeAdaUpdate.js b/source/renderer/app/api/ada/postponeAdaUpdate.js deleted file mode 100644 index f456bf17b4..0000000000 --- a/source/renderer/app/api/ada/postponeAdaUpdate.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type PostponeAdaUpdateParams = { - ca: string, -}; - -export const postponeAdaUpdate = ( - { ca }: PostponeAdaUpdateParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/update/postpone', - port: environment.WALLET_PORT, - ca, - }) -); diff --git a/source/renderer/app/api/ada/redeemAda.js b/source/renderer/app/api/ada/redeemAda.js deleted file mode 100644 index 0f836b6d1b..0000000000 --- a/source/renderer/app/api/ada/redeemAda.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow -import type { AdaTransaction } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type RedeemAdaParams = { - ca: string, - walletPassword: ?string, - walletRedeemData: { - crWalletId: string, - crSeed: string, - } -}; - -export const redeemAda = ( - { ca, walletPassword, walletRedeemData }: RedeemAdaParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/redemptions/ada', - port: environment.WALLET_PORT, - ca, - }, { passphrase: walletPassword }, walletRedeemData) -); diff --git a/source/renderer/app/api/ada/redeemAdaPaperVend.js b/source/renderer/app/api/ada/redeemAdaPaperVend.js deleted file mode 100644 index d91c9b2d42..0000000000 --- a/source/renderer/app/api/ada/redeemAdaPaperVend.js +++ /dev/null @@ -1,28 +0,0 @@ -// @flow -import type { AdaTransaction } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type RedeemAdaPaperVendParams = { - ca: string, - walletPassword: ?string, - redeemPaperVendedData: { - pvWalletId: string, - pvSeed: string, - pvBackupPhrase: { - bpToList: [], - } - } -}; - -export const redeemAdaPaperVend = ( - { ca, walletPassword, redeemPaperVendedData }: RedeemAdaPaperVendParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/papervend/redemptions/ada', - port: environment.WALLET_PORT, - ca, - }, { passphrase: walletPassword }, redeemPaperVendedData) -); diff --git a/source/renderer/app/api/ada/restoreAdaWallet.js b/source/renderer/app/api/ada/restoreAdaWallet.js deleted file mode 100644 index 640d70ff23..0000000000 --- a/source/renderer/app/api/ada/restoreAdaWallet.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import type { AdaWallet, AdaWalletInitData } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type RestoreAdaWalletParams = { - ca: string, - walletPassword: ?string, - walletInitData: AdaWalletInitData -}; - -export const restoreAdaWallet = ( - { ca, walletPassword, walletInitData }: RestoreAdaWalletParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'POST', - path: '/api/wallets/restore', - port: environment.WALLET_PORT, - ca, - }, { passphrase: walletPassword }, walletInitData) -); - diff --git a/source/renderer/app/api/ada/types.js b/source/renderer/app/api/ada/types.js deleted file mode 100644 index 4b78a62b69..0000000000 --- a/source/renderer/app/api/ada/types.js +++ /dev/null @@ -1,147 +0,0 @@ -// @flow - -// ========= Response Types ========= -export type AdaAssurance = 'CWANormal' | 'CWAStrict'; -export type AdaTransactionCondition = 'CPtxApplying' | 'CPtxInBlocks' | 'CPtxWontApply' | 'CPtxNotTracked'; -export type AdaWalletRecoveryPhraseResponse = Array; -export type AdaWalletCertificateAdditionalMnemonicsResponse = Array; -export type AdaWalletCertificateRecoveryPhraseResponse = Array; -export type AdaWalletRecoveryPhraseFromCertificateResponse = Array; -export type GetWalletCertificateAdditionalMnemonicsResponse = Array; -export type GetWalletCertificateRecoveryPhraseResponse = Array; -export type GetWalletRecoveryPhraseFromCertificateResponse = Array; - -export type AdaSyncProgressResponse = { - _spLocalCD: { - getChainDifficulty: { - getBlockCount: number, - } - }, - _spNetworkCD: { - getChainDifficulty: { - getBlockCount: number, - } - }, - _spPeers: number, -}; - -export type AdaWalletInitData = { - cwInitMeta: { - cwName: string, - cwAssurance: AdaAssurance, - cwUnit: number, - }, - cwBackupPhrase: { - bpToList: [], - } -}; - -export type AdaAmount = { - getCCoin: number, -}; -export type AdaTransactionTag = 'CTIn' | 'CTOut'; - -export type AdaAddress = { - cadAmount: AdaAmount, - cadId: string, - cadIsUsed: boolean, -}; - -export type AdaAddresses = Array; - -export type AdaAccount = { - caAddresses: AdaAddresses, - caAmount: AdaAmount, - caId: string, - caMeta: { - caName: string, - }, -}; - -export type AdaAccounts = Array; - -export type AdaTransaction = { - ctAmount: AdaAmount, - ctConfirmations: number, - ctId: string, - ctInputs: AdaTransactionInputOutput, - ctIsOutgoing: boolean, - ctMeta: { - ctmDate: Date, - ctmDescription: ?string, - ctmTitle: ?string, - }, - ctOutputs: AdaTransactionInputOutput, - ctCondition: AdaTransactionCondition, -}; - -export type AdaTransactions = [ - Array, - number, -]; - -export type AdaTransactionInputOutput = [ - [string, AdaAmount], -]; - -export type AdaTransactionFee = AdaAmount; - -export type AdaWallet = { - cwAccountsNumber: number, - cwAmount: AdaAmount, - cwHasPassphrase: boolean, - cwId: string, - cwMeta: { - cwAssurance: AdaAssurance, - cwName: string, - csUnit: number, - }, - cwPassphraseLU: Date, -}; - -export type AdaWallets = Array; - -export type AdaLocalTimeDifference = number; - - -// ========== V1 API ========= - -export type AdaV1Assurance = 'normal' | 'strict'; -export type AdaV1WalletSyncStateTag = 'restoring' | 'synced'; - -export type AdaV1WalletSyncState = { - data: ?{ - estimatedCompletionTime: { - quantity: number, - unit: 'milliseconds', - }, - percentage: { - quantity: number, - unit: 'percenage', - }, - throughput: { - quantity: number, - unit: 'blocksPerSecond', - }, - }, - tag: AdaV1WalletSyncStateTag, -}; - -export type AdaV1Wallet = { - assuranceLevel: AdaV1Assurance, - balance: number, - createdAt: string, - hasSpendingPassword: boolean, - id: string, - name: string, - spendingPasswordLastUpdate: string, - syncState: AdaV1WalletSyncState, -}; - -export type AdaV1Wallets = Array; - -export const AdaV1AssuranceOptions: { - NORMAL: AdaV1Assurance, STRICT: AdaV1Assurance, -} = { - NORMAL: 'normal', STRICT: 'strict', -}; diff --git a/source/renderer/app/api/ada/updateAdaWallet.js b/source/renderer/app/api/ada/updateAdaWallet.js deleted file mode 100644 index 8e120fd90c..0000000000 --- a/source/renderer/app/api/ada/updateAdaWallet.js +++ /dev/null @@ -1,26 +0,0 @@ -// @flow -import type { AdaWallet } from './types'; -import { request } from './lib/request'; -import environment from '../../../../common/environment'; - -export type UpdateAdaWalletParams = { - ca: string, - walletId: string, - walletMeta: { - cwName: string, - cwAssurance: string, - cwUnit: number, - } -}; - -export const updateAdaWallet = ( - { ca, walletId, walletMeta }: UpdateAdaWalletParams -): Promise => ( - request({ - hostname: 'localhost', - method: 'PUT', - path: `/api/wallets/${walletId}`, - port: environment.WALLET_PORT, - ca, - }, {}, walletMeta) -); diff --git a/source/renderer/app/api/addresses/errors.js b/source/renderer/app/api/addresses/errors.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/renderer/app/api/addresses/requests/createAddress.js b/source/renderer/app/api/addresses/requests/createAddress.js new file mode 100644 index 0000000000..ef9e1ec83c --- /dev/null +++ b/source/renderer/app/api/addresses/requests/createAddress.js @@ -0,0 +1,22 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Address } from '../types'; +import { request } from '../../utils/request'; + +export type CreateAddressParams = { + spendingPassword?: string, + accountIndex: number, + walletId: string, +}; + +export const createAddress = ( + config: RequestConfig, + { spendingPassword, accountIndex, walletId }: CreateAddressParams +): Promise
=> ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/v1/addresses', + ...config, + }, {}, { spendingPassword, accountIndex, walletId }) +); diff --git a/source/renderer/app/api/addresses/requests/getAddress.js b/source/renderer/app/api/addresses/requests/getAddress.js new file mode 100644 index 0000000000..5831c55a0f --- /dev/null +++ b/source/renderer/app/api/addresses/requests/getAddress.js @@ -0,0 +1,21 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Address } from '../types'; +import { request } from '../../utils/request'; + +export type GetAddressParams = { + address: string, +}; + +export const getAddress = ( + config: RequestConfig, + { address }: GetAddressParams +): Promise
=> { + const encodedAddress = encodeURIComponent(address); + return request({ + hostname: 'localhost', + method: 'GET', + path: `/api/v1/addresses/${encodedAddress}`, + ...config, + }); +}; diff --git a/source/renderer/app/api/addresses/types.js b/source/renderer/app/api/addresses/types.js new file mode 100644 index 0000000000..76f394d4bb --- /dev/null +++ b/source/renderer/app/api/addresses/types.js @@ -0,0 +1,24 @@ +// @flow +export type Address = { + id: string, + used: boolean, + changeAddress: boolean +}; + +export type Addresses = Array
; + +// req/res Address types +export type GetAddressesResponse = { + accountIndex: ?number, + addresses: Addresses, +}; + +export type GetAddressesRequest = { + walletId: string, +}; + +export type CreateAddressRequest = { + spendingPassword: ?string, + accountIndex: number, + walletId: string, +}; diff --git a/source/renderer/app/api/api.js b/source/renderer/app/api/api.js new file mode 100644 index 0000000000..e33c1e9f52 --- /dev/null +++ b/source/renderer/app/api/api.js @@ -0,0 +1,832 @@ +// @flow +import { split, get } from 'lodash'; +import { action } from 'mobx'; +import BigNumber from 'bignumber.js'; + +// domains +import Wallet from '../domains/Wallet'; +import WalletTransaction, { transactionTypes } from '../domains/WalletTransaction'; +import WalletAddress from '../domains/WalletAddress'; + +// Accounts requests +import { getAccounts } from './accounts/requests/getAccounts'; + +// Addresses requests +import { getAddress } from './addresses/requests/getAddress'; +import { createAddress } from './addresses/requests/createAddress'; + +// Common requests +import { sendBugReport } from './common/requests/sendBugReport'; + +// Nodes requests +import { applyNodeUpdate } from './nodes/requests/applyNodeUpdate'; +import { getNodeInfo } from './nodes/requests/getNodeInfo'; +import { getNextNodeUpdate } from './nodes/requests/getNextNodeUpdate'; +import { postponeNodeUpdate } from './nodes/requests/postponeNodeUpdate'; + +// Transactions requests +import { getTransactionFee } from './transactions/requests/getTransactionFee'; +import { getTransactionHistory } from './transactions/requests/getTransactionHistory'; +import { createTransaction } from './transactions/requests/createTransaction'; +import { redeemAda } from './transactions/requests/redeemAda'; +import { redeemPaperVendedAda } from './transactions/requests/redeemPaperVendedAda'; + +// Wallets requests +import { resetWalletState } from './wallets/requests/resetWalletState'; +import { changeSpendingPassword } from './wallets/requests/changeSpendingPassword'; +import { deleteWallet } from './wallets/requests/deleteWallet'; +import { exportWalletAsJSON } from './wallets/requests/exportWalletAsJSON'; +import { importWalletAsJSON } from './wallets/requests/importWalletAsJSON'; +import { getWallets } from './wallets/requests/getWallets'; +import { importWalletAsKey } from './wallets/requests/importWalletAsKey'; +import { createWallet } from './wallets/requests/createWallet'; +import { restoreWallet } from './wallets/requests/restoreWallet'; +import { updateWallet } from './wallets/requests/updateWallet'; + +// utility functions +import { awaitUpdateChannel } from '../ipc/cardano.ipc'; +import patchAdaApi from './utils/patchAdaApi'; +import { isValidMnemonic } from '../../../common/decrypt'; +import { utcStringToDate, encryptPassphrase } from './utils'; +import { + Logger, + stringifyData, + stringifyError +} from '../../../common/logging'; +import { + isValidRedemptionKey, + isValidPaperVendRedemptionKey +} from '../utils/redemption-key-validation'; +import { + unscrambleMnemonics, + scrambleMnemonics, + generateAccountMnemonics, + generateAdditionalMnemonics +} from './utils/mnemonics'; + +// config constants +import { + LOVELACES_PER_ADA, + MAX_TRANSACTIONS_PER_PAGE +} from '../config/numbersConfig'; +import { + ADA_CERTIFICATE_MNEMONIC_LENGTH, + ADA_REDEMPTION_PASSPHRASE_LENGTH, + WALLET_RECOVERY_PHRASE_WORD_COUNT +} from '../config/cryptoConfig'; + +// Accounts types +import type { Accounts } from './accounts/types'; + +// Addresses Types +import type { + Address, + GetAddressesRequest, + CreateAddressRequest, + GetAddressesResponse +} from './addresses/types'; + +// Common Types +import type { + RequestConfig, + SendBugReportRequest +} from './common/types'; + +// Nodes Types +import type { + NodeInfo, + NodeSoftware, + GetNetworkStatusResponse +} from './nodes/types'; +import type { NodeQueryParams } from './nodes/requests/getNodeInfo'; + +// Transactions Types +import type { RedeemAdaParams } from './transactions/requests/redeemAda'; +import type { RedeemPaperVendedAdaParams } from './transactions/requests/redeemPaperVendedAda'; +import type { + Transaction, + Transactions, + TransactionFee, + TransactionRequest, + GetTransactionsRequest, + GetTransactionsResponse +} from './transactions/types'; + +// Wallets Types +import type { + AdaWallet, + AdaWallets, + CreateWalletRequest, + DeleteWalletRequest, + RestoreWalletRequest, + UpdateSpendingPasswordRequest, + ExportWalletToFileRequest, + GetWalletCertificateRecoveryPhraseRequest, + GetWalletRecoveryPhraseFromCertificateRequest, + ImportWalletFromKeyRequest, + ImportWalletFromFileRequest, + UpdateWalletRequest +} from './wallets/types'; + +// Common errors +import { + GenericApiError, + IncorrectSpendingPasswordError, + ReportRequestError, + InvalidMnemonicError, + ForbiddenMnemonicError +} from './common/errors'; + +// Wallets errors +import { + WalletAlreadyRestoredError, + WalletAlreadyImportedError, + WalletFileImportError +} from './wallets/errors'; + +// Transactions errors +import { + CanNotCalculateTransactionFeesError, + NotAllowedToSendMoneyToRedeemAddressError, + NotEnoughFundsForTransactionFeesError, + NotEnoughFundsForTransactionError, + NotEnoughMoneyToSendError, + RedeemAdaError +} from './transactions/errors'; + +export default class AdaApi { + + config: RequestConfig; + + constructor(isTest: boolean, config: RequestConfig) { + this.setRequestConfig(config); + if (isTest) patchAdaApi(this); + } + + setRequestConfig(config: RequestConfig) { + this.config = config; + } + + getWallets = async (): Promise> => { + Logger.debug('AdaApi::getWallets called'); + try { + const response: AdaWallets = await getWallets(this.config); + Logger.debug('AdaApi::getWallets success: ' + stringifyData(response)); + return response.map(data => _createWalletFromServerData(data)); + } catch (error) { + Logger.error('AdaApi::getWallets error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + getAddresses = async (request: GetAddressesRequest): Promise => { + Logger.debug('AdaApi::getAddresses called: ' + stringifyData(request)); + const { walletId } = request; + try { + const accounts: Accounts = await getAccounts(this.config, { walletId }); + Logger.debug('AdaApi::getAddresses success: ' + stringifyData(accounts)); + + if (!accounts || !accounts.length) { + return new Promise(resolve => resolve({ accountIndex: null, addresses: [] })); + } + + // For now only the first wallet account is used + const firstAccount = accounts[0]; + const { index: accountIndex, addresses } = firstAccount; + + return new Promise(resolve => resolve({ accountIndex, addresses })); + } catch (error) { + Logger.error('AdaApi::getAddresses error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + getTransactions = async (request: GetTransactionsRequest): Promise => { + Logger.debug('AdaApi::searchHistory called: ' + stringifyData(request)); + const { walletId, skip, limit } = request; + const accounts: Accounts = await getAccounts(this.config, { walletId }); + + if (!accounts.length || !accounts[0].index) { + return new Promise(resolve => resolve({ transactions: [], total: 0 })); + } + + let perPage = limit; + if (limit === null || limit > MAX_TRANSACTIONS_PER_PAGE) { + perPage = MAX_TRANSACTIONS_PER_PAGE; + } + + const params = { + accountIndex: accounts[0].index, + page: skip === 0 ? 1 : (skip / limit) + 1, + per_page: perPage, + wallet_id: walletId, + sort_by: 'DES[created_at]', + }; + const pagesToBeLoaded = Math.ceil(limit / params.per_page); + + try { + const response: Transactions = await getTransactionHistory(this.config, params); + const { meta, data: txnHistory } = response; + const { totalPages } = meta.pagination; + const hasMultiplePages = (totalPages > 1 && limit > MAX_TRANSACTIONS_PER_PAGE); + + if (hasMultiplePages) { + let page = 2; + const hasNextPage = () => page < totalPages + 1; + const shouldLoadNextPage = () => limit === null || page <= pagesToBeLoaded; + + for (page; (hasNextPage() && shouldLoadNextPage()); page++) { + const { data: pageHistory } = + await getTransactionHistory(this.config, Object.assign(params, { page })); + txnHistory.push(...pageHistory); + } + if (limit !== null) txnHistory.splice(limit); + } + + const transactions = txnHistory.map(txn => _createTransactionFromServerData(txn)); + const total = transactions.length; + Logger.debug('AdaApi::searchHistory success: ' + stringifyData(txnHistory)); + return new Promise(resolve => resolve({ transactions, total })); + } catch (error) { + Logger.error('AdaApi::searchHistory error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + createWallet = async (request: CreateWalletRequest): Promise => { + Logger.debug('AdaApi::createWallet called'); + const { name, mnemonic, spendingPassword: passwordString } = request; + const spendingPassword = passwordString ? encryptPassphrase(passwordString) : ''; + const assuranceLevel = 'normal'; + try { + const walletInitData = { + operation: 'create', + backupPhrase: split(mnemonic, ' '), + assuranceLevel, + name, + spendingPassword, + }; + const wallet: AdaWallet = await createWallet(this.config, { walletInitData }); + Logger.debug('AdaApi::createWallet success'); + return _createWalletFromServerData(wallet); + } catch (error) { + Logger.error('AdaApi::createWallet error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + deleteWallet = async (request: DeleteWalletRequest): Promise => { + Logger.debug('AdaApi::deleteWallet called: ' + stringifyData(request)); + try { + const { walletId } = request; + const response = await deleteWallet(this.config, { walletId }); + Logger.debug('AdaApi::deleteWallet success: ' + stringifyData(response)); + return true; + } catch (error) { + Logger.error('AdaApi::deleteWallet error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + createTransaction = async ( + request: TransactionRequest + ): Promise => { + Logger.debug('AdaApi::createTransaction called'); + const { accountIndex, walletId, address, amount, spendingPassword: passwordString } = request; + const spendingPassword = passwordString ? encryptPassphrase(passwordString) : ''; + try { + const data = { + source: { + accountIndex, + walletId, + }, + destinations: [ + { + address, + amount, + }, + ], + groupingPolicy: 'OptimizeForSecurity', + spendingPassword, + }; + const response: Transaction = await createTransaction(this.config, { data }); + Logger.debug('AdaApi::createTransaction success: ' + stringifyData(response)); + return _createTransactionFromServerData(response); + } catch (error) { + Logger.debug('AdaApi::createTransaction error: ' + stringifyError(error)); + if (error.message === 'OutputIsRedeem') { + throw new NotAllowedToSendMoneyToRedeemAddressError(); + } + if (error.message === 'NotEnoughMoney') { + throw new NotEnoughMoneyToSendError(); + } + if (error.message === 'CannotCreateAddress') { + throw new IncorrectSpendingPasswordError(); + } + throw new GenericApiError(); + } + }; + + calculateTransactionFee = async ( + request: TransactionRequest + ): Promise => { + Logger.debug('AdaApi::calculateTransactionFee called'); + const { + accountIndex, + walletId, walletBalance, + address, amount, + spendingPassword: passwordString, + } = request; + const spendingPassword = passwordString ? encryptPassphrase(passwordString) : ''; + try { + const data = { + source: { + accountIndex, + walletId, + }, + destinations: [ + { + address, + amount, + }, + ], + groupingPolicy: 'OptimizeForSecurity', + spendingPassword, + }; + const response: TransactionFee = await getTransactionFee(this.config, { data }); + Logger.debug('AdaApi::calculateTransactionFee success: ' + stringifyData(response)); + return _createTransactionFeeFromServerData(response); + } catch (error) { + Logger.debug('AdaApi::calculateTransactionFee error: ' + stringifyError(error)); + if (error.message === 'NotEnoughMoney') { + const errorMessage = get(error, 'diagnostic.details.msg', ''); + if (errorMessage.includes('Not enough coins to cover fee')) { + // Amount + fees exceeds walletBalance: + // - error.diagnostic.details.msg === 'Not enough coins to cover fee.' + // = show "Not enough Ada for fees. Try sending a smaller amount." + throw new NotEnoughFundsForTransactionFeesError(); + } else if (errorMessage.includes('Not enough available coins to proceed')) { + const availableBalance = new BigNumber( + get(error, 'diagnostic.details.availableBalance', 0) + ).dividedBy(LOVELACES_PER_ADA); + if (walletBalance.gt(availableBalance)) { + // Amount exceeds availableBalance due to pending transactions: + // - error.diagnostic.details.msg === 'Not enough available coins to proceed.' + // - total walletBalance > error.diagnostic.details.availableBalance + // = show "Cannot calculate fees while there are pending transactions." + throw new CanNotCalculateTransactionFeesError(); + } else { + // Amount exceeds walletBalance: + // - error.diagnostic.details.msg === 'Not enough available coins to proceed.' + // - total walletBalance === error.diagnostic.details.availableBalance + // = show "Not enough Ada. Try sending a smaller amount." + throw new NotEnoughFundsForTransactionError(); + } + } else { + // Amount exceeds walletBalance: + // = show "Not enough Ada. Try sending a smaller amount." + throw new NotEnoughFundsForTransactionError(); + } + } + throw new GenericApiError(); + } + }; + + createAddress = async (request: CreateAddressRequest): Promise
=> { + Logger.debug('AdaApi::createAddress called'); + const { accountIndex, walletId, spendingPassword: passwordString } = request; + const spendingPassword = passwordString ? encryptPassphrase(passwordString) : ''; + try { + const address: Address = await createAddress( + this.config, { spendingPassword, accountIndex, walletId } + ); + Logger.debug('AdaApi::createAddress success: ' + stringifyData(address)); + return _createAddressFromServerData(address); + } catch (error) { + Logger.debug('AdaApi::createAddress error: ' + stringifyError(error)); + if (error.message === 'CannotCreateAddress') { + throw new IncorrectSpendingPasswordError(); + } + throw new GenericApiError(); + } + }; + + async isValidAddress(address: string): Promise { + Logger.debug('AdaApi::isValidAdaAddress called'); + try { + const response: Address = await getAddress(this.config, { address }); + Logger.debug(`AdaApi::isValidAdaAddress success: ${stringifyData(response)}`); + return true; + } catch (error) { + Logger.debug(`AdaApi::isValidAdaAddress error: ${stringifyError(error)}`); + return false; + } + } + + isValidMnemonic = (mnemonic: string): boolean => ( + isValidMnemonic(mnemonic, WALLET_RECOVERY_PHRASE_WORD_COUNT) + ); + + isValidRedemptionKey = (mnemonic: string): boolean => (isValidRedemptionKey(mnemonic)); + + isValidPaperVendRedemptionKey = (mnemonic: string): boolean => ( + isValidPaperVendRedemptionKey(mnemonic) + ); + + isValidRedemptionMnemonic = (mnemonic: string): boolean => ( + isValidMnemonic(mnemonic, ADA_REDEMPTION_PASSPHRASE_LENGTH) + ); + + isValidCertificateMnemonic = (mnemonic: string): boolean => ( + mnemonic.split(' ').length === ADA_CERTIFICATE_MNEMONIC_LENGTH + ); + + getWalletRecoveryPhrase(): Promise> { + Logger.debug('AdaApi::getWalletRecoveryPhrase called'); + try { + const response: Promise> = new Promise( + (resolve) => resolve(generateAccountMnemonics()) + ); + Logger.debug('AdaApi::getWalletRecoveryPhrase success'); + return response; + } catch (error) { + Logger.error('AdaApi::getWalletRecoveryPhrase error: ' + stringifyError(error)); + throw new GenericApiError(); + } + } + + // eslint-disable-next-line max-len + getWalletCertificateAdditionalMnemonics(): Promise> { + Logger.debug('AdaApi::getWalletCertificateAdditionalMnemonics called'); + try { + const response: Promise> = new Promise( + (resolve) => resolve(generateAdditionalMnemonics()) + ); + Logger.debug('AdaApi::getWalletCertificateAdditionalMnemonics success'); + return response; + } catch (error) { + Logger.error('AdaApi::getWalletCertificateAdditionalMnemonics error: ' + stringifyError(error)); + throw new GenericApiError(); + } + } + + getWalletCertificateRecoveryPhrase( + request: GetWalletCertificateRecoveryPhraseRequest + ): Promise> { + Logger.debug('AdaApi::getWalletCertificateRecoveryPhrase called'); + const { passphrase, input: scrambledInput } = request; + try { + const response: Promise> = new Promise( + (resolve) => resolve(scrambleMnemonics({ passphrase, scrambledInput })) + ); + Logger.debug('AdaApi::getWalletCertificateRecoveryPhrase success'); + return response; + } catch (error) { + Logger.error('AdaApi::getWalletCertificateRecoveryPhrase error: ' + stringifyError(error)); + throw new GenericApiError(); + } + } + + getWalletRecoveryPhraseFromCertificate( + request: GetWalletRecoveryPhraseFromCertificateRequest + ): Promise> { + Logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate called'); + const { passphrase, scrambledInput } = request; + try { + const response = unscrambleMnemonics({ passphrase, scrambledInput }); + Logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate success'); + return Promise.resolve(response); + } catch (error) { + Logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate error: ' + stringifyError(error)); + return Promise.reject(new InvalidMnemonicError()); + } + } + + restoreWallet = async (request: RestoreWalletRequest): Promise => { + Logger.debug('AdaApi::restoreWallet called'); + const { recoveryPhrase, walletName, spendingPassword: passwordString } = request; + const spendingPassword = passwordString ? encryptPassphrase(passwordString) : ''; + const assuranceLevel = 'normal'; + const walletInitData = { + operation: 'restore', + backupPhrase: split(recoveryPhrase, ' '), + assuranceLevel, + name: walletName, + spendingPassword + }; + + try { + const wallet: AdaWallet = await restoreWallet(this.config, { walletInitData }); + Logger.debug('AdaApi::restoreWallet success'); + return _createWalletFromServerData(wallet); + } catch (error) { + Logger.debug('AdaApi::restoreWallet error: ' + stringifyError(error)); + if (error.message === 'WalletAlreadyExists') { + throw new WalletAlreadyRestoredError(); + } + if (error.message === 'JSONValidationFailed') { + const validationError = get(error, 'diagnostic.validationError', ''); + if (validationError.includes('Forbidden Mnemonic: an example Mnemonic has been submitted')) { + throw new ForbiddenMnemonicError(); + } + } + throw new GenericApiError(); + } + }; + + importWalletFromKey = async ( + request: ImportWalletFromKeyRequest + ): Promise => { + Logger.debug('AdaApi::importWalletFromKey called'); + const { filePath, spendingPassword: passwordString } = request; + const spendingPassword = passwordString ? encryptPassphrase(passwordString) : ''; + try { + const importedWallet: AdaWallet = await importWalletAsKey( + this.config, { filePath, spendingPassword } + ); + Logger.debug('AdaApi::importWalletFromKey success'); + return _createWalletFromServerData(importedWallet); + } catch (error) { + Logger.debug('AdaApi::importWalletFromKey error: ' + stringifyError(error)); + if (error.message === 'WalletAlreadyExists') { + throw new WalletAlreadyImportedError(); + } + throw new WalletFileImportError(); + } + }; + + importWalletFromFile = async ( + request: ImportWalletFromFileRequest + ): Promise => { + Logger.debug('AdaApi::importWalletFromFile called'); + const { filePath, spendingPassword: passwordString } = request; + const spendingPassword = passwordString ? encryptPassphrase(passwordString) : ''; + const isKeyFile = filePath.split('.').pop().toLowerCase() === 'key'; + try { + const importedWallet: AdaWallet = isKeyFile ? ( + await importWalletAsKey(this.config, { filePath, spendingPassword }) + ) : ( + await importWalletAsJSON(this.config, filePath) + ); + Logger.debug('AdaApi::importWalletFromFile success'); + return _createWalletFromServerData(importedWallet); + } catch (error) { + Logger.debug('AdaApi::importWalletFromFile error: ' + stringifyError(error)); + if (error.message === 'WalletAlreadyExists') { + throw new WalletAlreadyImportedError(); + } + throw new WalletFileImportError(); + } + }; + + redeemAda = async ( + request: RedeemAdaParams + ): Promise => { + Logger.debug('AdaApi::redeemAda called'); + const { spendingPassword: passwordString } = request; + const spendingPassword = passwordString ? encryptPassphrase(passwordString) : ''; + try { + const transaction: Transaction = await redeemAda( + this.config, { ...request, spendingPassword } + ); + Logger.debug('AdaApi::redeemAda success'); + return _createTransactionFromServerData(transaction); + } catch (error) { + Logger.debug('AdaApi::redeemAda error: ' + stringifyError(error)); + if (error.message === 'CannotCreateAddress') { + throw new IncorrectSpendingPasswordError(); + } + throw new RedeemAdaError(); + } + }; + + redeemPaperVendedAda = async ( + request: RedeemPaperVendedAdaParams + ): Promise => { + Logger.debug('AdaApi::redeemAdaPaperVend called'); + const { spendingPassword: passwordString } = request; + const spendingPassword = passwordString ? encryptPassphrase(passwordString) : ''; + try { + const transaction: Transaction = await redeemPaperVendedAda( + this.config, { ...request, spendingPassword } + ); + Logger.debug('AdaApi::redeemAdaPaperVend success'); + return _createTransactionFromServerData(transaction); + } catch (error) { + Logger.debug('AdaApi::redeemAdaPaperVend error: ' + stringifyError(error)); + if (error.message === 'CannotCreateAddress') { + throw new IncorrectSpendingPasswordError(); + } + throw new RedeemAdaError(); + } + }; + + async sendBugReport(requestFormData: SendBugReportRequest): Promise { + Logger.debug('AdaApi::sendBugReport called: ' + stringifyData(requestFormData)); + try { + await sendBugReport({ requestFormData }); + Logger.debug('AdaApi::sendBugReport success'); + return true; + } catch (error) { + Logger.error('AdaApi::sendBugReport error: ' + stringifyError(error)); + throw new ReportRequestError(); + } + } + + nextUpdate = async (): Promise => { + Logger.debug('AdaApi::nextUpdate called'); + try { + const nodeUpdate = await getNextNodeUpdate(this.config); + if (nodeUpdate && nodeUpdate.version) { + Logger.debug('AdaApi::nextUpdate success: ' + stringifyData(nodeUpdate)); + return nodeUpdate; + } + Logger.debug('AdaApi::nextUpdate success: No Update Available'); + } catch (error) { + Logger.error('AdaApi::nextUpdate error: ' + stringifyError(error)); + throw new GenericApiError(); + } + return null; + }; + + postponeUpdate = async (): Promise => { + Logger.debug('AdaApi::postponeUpdate called'); + try { + const response: Promise = await postponeNodeUpdate(this.config); + Logger.debug('AdaApi::postponeUpdate success: ' + stringifyData(response)); + } catch (error) { + Logger.error('AdaApi::postponeUpdate error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + applyUpdate = async (): Promise => { + Logger.debug('AdaApi::applyUpdate called'); + try { + await awaitUpdateChannel.send(); + const response: Promise = await applyNodeUpdate(this.config); + Logger.debug('AdaApi::applyUpdate success: ' + stringifyData(response)); + } catch (error) { + Logger.error('AdaApi::applyUpdate error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + updateWallet = async (request: UpdateWalletRequest): Promise => { + Logger.debug('AdaApi::updateWallet called: ' + stringifyData(request)); + const { walletId, assuranceLevel, name } = request; + try { + const wallet: AdaWallet = await updateWallet( + this.config, { walletId, assuranceLevel, name } + ); + Logger.debug('AdaApi::updateWallet success: ' + stringifyData(wallet)); + return _createWalletFromServerData(wallet); + } catch (error) { + Logger.error('AdaApi::updateWallet error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + updateSpendingPassword = async ( + request: UpdateSpendingPasswordRequest + ): Promise => { + Logger.debug('AdaApi::updateSpendingPassword called'); + const { walletId, oldPassword, newPassword } = request; + try { + await changeSpendingPassword(this.config, { walletId, oldPassword, newPassword }); + Logger.debug('AdaApi::updateSpendingPassword success'); + return true; + } catch (error) { + Logger.debug('AdaApi::updateSpendingPassword error: ' + stringifyError(error)); + const errorMessage = get(error, 'diagnostic.msg', ''); + if (errorMessage.includes('UpdateWalletPasswordOldPasswordMismatch')) { + throw new IncorrectSpendingPasswordError(); + } + throw new GenericApiError(); + } + }; + + exportWalletToFile = async ( + request: ExportWalletToFileRequest + ): Promise<[]> => { + const { walletId, filePath } = request; + Logger.debug('AdaApi::exportWalletToFile called'); + try { + const response: Promise<[]> = await exportWalletAsJSON( + this.config, { walletId, filePath } + ); + Logger.debug('AdaApi::exportWalletToFile success: ' + stringifyData(response)); + return response; + } catch (error) { + Logger.error('AdaApi::exportWalletToFile error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + testReset = async (): Promise => { + Logger.debug('AdaApi::testReset called'); + try { + const response: Promise = await resetWalletState(this.config); + Logger.debug('AdaApi::testReset success: ' + stringifyData(response)); + return response; + } catch (error) { + Logger.error('AdaApi::testReset error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + getNetworkStatus = async ( + queryParams?: NodeQueryParams + ): Promise => { + const isForceNTPCheck = !!queryParams; + const loggerText = `AdaApi::getNetworkStatus${isForceNTPCheck ? ' (FORCE-NTP-CHECK)' : ''}`; + Logger.debug(`${loggerText} called`); + try { + const status: NodeInfo = await getNodeInfo(this.config, queryParams); + Logger.debug(`${loggerText} success: ${stringifyData(status)}`); + + const { + blockchainHeight, + subscriptionStatus, + syncProgress, + localBlockchainHeight, + localTimeInformation, + } = status; + + // extract relevant data before sending to NetworkStatusStore + return { + subscriptionStatus, + syncProgress: syncProgress.quantity, + blockchainHeight: get(blockchainHeight, 'quantity', 0), + localBlockchainHeight: localBlockchainHeight.quantity, + localTimeDifference: get(localTimeInformation, 'differenceFromNtpServer.quantity', null), + }; + } catch (error) { + Logger.error(`${loggerText} error: ${stringifyError(error)}`); + throw new GenericApiError(error); + } + }; +} + +// ========== TRANSFORM SERVER DATA INTO FRONTEND MODELS ========= + +const _createWalletFromServerData = action( + 'AdaApi::_createWalletFromServerData', (data: AdaWallet) => { + const { + id, balance, name, assuranceLevel, + hasSpendingPassword, spendingPasswordLastUpdate, + syncState, + } = data; + + return new Wallet({ + id, + amount: new BigNumber(balance).dividedBy(LOVELACES_PER_ADA), + name, + assurance: assuranceLevel, + hasPassword: hasSpendingPassword, + passwordUpdateDate: new Date(`${spendingPasswordLastUpdate}Z`), + syncState, + }); + } +); + +const _createAddressFromServerData = action( + 'AdaApi::_createAddressFromServerData', + (address: Address) => new WalletAddress(address) +); + +const _conditionToTxState = (condition: string) => { + switch (condition) { + case 'applying': + case 'creating': return 'pending'; + case 'wontApply': return 'failed'; + default: return 'ok'; + // Others V0: CPtxInBlocks && CPtxNotTracked + // Others V1: "inNewestBlocks" "persisted" "creating" + } +}; + +const _createTransactionFromServerData = action( + 'AdaApi::_createTransactionFromServerData', (data: Transaction) => { + const { id, direction, amount, confirmations, creationTime, inputs, outputs, status } = data; + return new WalletTransaction({ + id, + title: direction === 'outgoing' ? 'Ada sent' : 'Ada received', + type: direction === 'outgoing' ? transactionTypes.EXPEND : transactionTypes.INCOME, + amount: new BigNumber(direction === 'outgoing' ? (amount * -1) : amount).dividedBy(LOVELACES_PER_ADA), + date: utcStringToDate(creationTime), + description: '', + numberOfConfirmations: confirmations, + addresses: { + from: inputs.map(({ address }) => address), + to: outputs.map(({ address }) => address), + }, + state: _conditionToTxState(status.tag), + }); + } +); + +const _createTransactionFeeFromServerData = action( + 'AdaApi::_createTransactionFeeFromServerData', (data: TransactionFee) => + new BigNumber(data.estimatedAmount).dividedBy(LOVELACES_PER_ADA) +); diff --git a/source/renderer/app/api/common.js b/source/renderer/app/api/common.js deleted file mode 100644 index f0702c548c..0000000000 --- a/source/renderer/app/api/common.js +++ /dev/null @@ -1,129 +0,0 @@ -import { defineMessages } from 'react-intl'; -import LocalizableError from '../i18n/LocalizableError'; -import { WalletTransaction, Wallet } from '../domains/WalletTransaction'; -import globalMessages from '../i18n/global-messages'; - -const messages = defineMessages({ - genericApiError: { - id: 'api.errors.GenericApiError', - defaultMessage: '!!!An error occurred.', - description: 'Generic error message.' - }, - incorrectWalletPasswordError: { - id: 'api.errors.IncorrectPasswordError', - defaultMessage: '!!!Incorrect wallet password.', - description: '"Incorrect wallet password." error message.' - }, - walletAlreadyRestoredError: { - id: 'api.errors.WalletAlreadyRestoredError', - defaultMessage: '!!!Wallet you are trying to restore already exists.', - description: '"Wallet you are trying to restore already exists." error message.' - }, - reportRequestError: { - id: 'api.errors.ReportRequestError', - defaultMessage: '!!!There was a problem sending the support request.', - description: '"There was a problem sending the support request." error message' - }, -}); - -export class GenericApiError extends LocalizableError { - constructor() { - super({ - id: messages.genericApiError.id, - defaultMessage: messages.genericApiError.defaultMessage, - }); - } -} - -export class IncorrectWalletPasswordError extends LocalizableError { - constructor() { - super({ - id: messages.incorrectWalletPasswordError.id, - defaultMessage: messages.incorrectWalletPasswordError.defaultMessage, - }); - } -} - -export class WalletAlreadyRestoredError extends LocalizableError { - constructor() { - super({ - id: messages.walletAlreadyRestoredError.id, - defaultMessage: messages.walletAlreadyRestoredError.defaultMessage, - }); - } -} - -export class ReportRequestError extends LocalizableError { - constructor() { - super({ - id: messages.reportRequestError.id, - defaultMessage: messages.reportRequestError.defaultMessage, - }); - } -} - -export class InvalidMnemonicError extends LocalizableError { - constructor() { - super({ - id: globalMessages.invalidMnemonic.id, - defaultMessage: globalMessages.invalidMnemonic.defaultMessage, - }); - } -} - -export type CreateTransactionResponse = WalletTransaction; -export type CreateWalletResponse = Wallet; -export type DeleteWalletResponse = boolean; -export type GetLocalTimeDifferenceResponse = number; -export type GetWalletsResponse = Array; -export type GetWalletRecoveryPhraseResponse = Array; -export type RestoreWalletResponse = Wallet; -export type UpdateWalletResponse = Wallet; -export type UpdateWalletPasswordResponse = boolean; - -export type CreateWalletRequest = { - name: string, - mnemonic: string, - password: ?string, -}; - -export type UpdateWalletPasswordRequest = { - walletId: string, - oldPassword: ?string, - newPassword: ?string, -}; - -export type DeleteWalletRequest = { - walletId: string, -}; - -export type RestoreWalletRequest = { - recoveryPhrase: string, - walletName: string, - walletPassword: ?string, -}; - -export type GetSyncProgressResponse = { - localDifficulty: ?number, - networkDifficulty: ?number, -}; - -export type GetTransactionsRequest = { - walletId: string, - searchTerm: string, - skip: number, - limit: number, -}; - -export type GetTransactionsResponse = { - transactions: Array, - total: number, -}; - -export type SendBugReportRequest = { - email: string, - subject: string, - problem: string, - logs: Array, -}; -export type SendBugReportResponse = any; diff --git a/source/renderer/app/api/common/errors.js b/source/renderer/app/api/common/errors.js new file mode 100644 index 0000000000..b372361928 --- /dev/null +++ b/source/renderer/app/api/common/errors.js @@ -0,0 +1,86 @@ +import { defineMessages } from 'react-intl'; +import LocalizableError from '../../i18n/LocalizableError'; +import globalMessages from '../../i18n/global-messages'; + +const messages = defineMessages({ + genericApiError: { + id: 'api.errors.GenericApiError', + defaultMessage: '!!!An error occurred.', + description: 'Generic error message.' + }, + incorrectSpendingPasswordError: { + id: 'api.errors.IncorrectPasswordError', + defaultMessage: '!!!Incorrect wallet password.', + description: '"Incorrect wallet password." error message.' + }, + reportRequestError: { + id: 'api.errors.ReportRequestError', + defaultMessage: '!!!There was a problem sending the support request.', + description: '"There was a problem sending the support request." error message' + }, + apiMethodNotYetImplementedError: { + id: 'api.errors.ApiMethodNotYetImplementedError', + defaultMessage: '!!!This API method is not yet implemented.', + description: '"This API method is not yet implemented." error message.' + }, + forbiddenMnemonicError: { + id: 'api.errors.ForbiddenMnemonicError', + defaultMessage: '!!!Invalid recovery phrase. Submitted recovery phrase is one of the example recovery phrases from the documentation and should not be used for wallets holding funds.', + description: '"Forbidden Mnemonic: an example Mnemonic has been submitted." error message', + }, +}); + +export class GenericApiError extends LocalizableError { + constructor(values?: Object = {}) { + super({ + id: messages.genericApiError.id, + defaultMessage: messages.genericApiError.defaultMessage, + values, + }); + } +} + +export class IncorrectSpendingPasswordError extends LocalizableError { + constructor() { + super({ + id: messages.incorrectSpendingPasswordError.id, + defaultMessage: messages.incorrectSpendingPasswordError.defaultMessage, + }); + } +} + +export class ReportRequestError extends LocalizableError { + constructor() { + super({ + id: messages.reportRequestError.id, + defaultMessage: messages.reportRequestError.defaultMessage, + }); + } +} + +export class ForbiddenMnemonicError extends LocalizableError { + constructor() { + super({ + id: messages.forbiddenMnemonicError.id, + defaultMessage: messages.forbiddenMnemonicError.defaultMessage, + }); + } +} + +export class InvalidMnemonicError extends LocalizableError { + constructor() { + super({ + id: globalMessages.invalidMnemonic.id, + defaultMessage: globalMessages.invalidMnemonic.defaultMessage, + }); + } +} + +export class ApiMethodNotYetImplementedError extends LocalizableError { + constructor() { + super({ + id: messages.apiMethodNotYetImplementedError.id, + defaultMessage: messages.apiMethodNotYetImplementedError.defaultMessage, + }); + } +} diff --git a/source/renderer/app/api/ada/sendAdaBugReport.js b/source/renderer/app/api/common/requests/sendBugReport.js similarity index 66% rename from source/renderer/app/api/ada/sendAdaBugReport.js rename to source/renderer/app/api/common/requests/sendBugReport.js index bd65cf360d..58a385e03c 100644 --- a/source/renderer/app/api/ada/sendAdaBugReport.js +++ b/source/renderer/app/api/common/requests/sendBugReport.js @@ -1,33 +1,34 @@ // @flow import moment from 'moment'; import url from 'url'; -import { request } from '../lib/reportRequest'; -import environment from '../../../../common/environment'; +import { request } from '../../utils/reportRequest'; +import environment from '../../../../../common/environment'; -export type SendAdaBugReportRequestParams = { +export type SendBugReportParams = { requestFormData: { email: string, subject: string, problem: string, - compressedLog: string, + compressedLogsFile: string, }, }; -export const sendAdaBugReport = ( - { requestFormData }: SendAdaBugReportRequestParams -): Promise<{}> => { - const { email, subject, problem, compressedLog } = requestFormData; +export const sendBugReport = ( + { requestFormData }: SendBugReportParams +) => { + const { email, subject, problem, compressedLogsFile } = requestFormData; const { version, os, API_VERSION, NETWORK, build, getInstallerVersion, REPORT_URL } = environment; const reportUrl = url.parse(REPORT_URL); + const { hostname, port } = reportUrl; // Report server recognizes the following networks: mainnet, staging and testnet const network = NETWORK === 'development' ? 'staging' : NETWORK; return request({ - hostname: reportUrl.hostname, + hostname, method: 'POST', path: '/api/v1/report', - port: reportUrl.port, + port, }, { product: 'Daedalus Wallet', frontendVersion: version, @@ -36,7 +37,7 @@ export const sendAdaBugReport = ( build, installerVersion: getInstallerVersion(), os, - compressedLog, + compressedLogsFile, date: moment().format('YYYY-MM-DDTHH:mm:ss'), magic: 2000000000, type: { diff --git a/source/renderer/app/api/common/types.js b/source/renderer/app/api/common/types.js new file mode 100644 index 0000000000..220bae40dc --- /dev/null +++ b/source/renderer/app/api/common/types.js @@ -0,0 +1,29 @@ +export type RequestConfig = { + port: number, + ca: Uint8Array, + cert: Uint8Array, + key: Uint8Array, +}; + +export type ResponseBase = { + status: ResponseStatus, + meta: Pagination +}; + +export type ResponseStatus = 'success' | 'fail' | 'error'; + +export type Pagination = { + pagination: { + totalPages: number, + page: number, + perPage: number, + totalEntries: number + } +}; + +export type SendBugReportRequest = { + email: string, + subject: string, + problem: string, + logs: Array, +}; diff --git a/source/renderer/app/api/etc/changeEtcAccountPassphrase.js b/source/renderer/app/api/etc/changeEtcAccountPassphrase.js deleted file mode 100644 index c85cfc55be..0000000000 --- a/source/renderer/app/api/etc/changeEtcAccountPassphrase.js +++ /dev/null @@ -1,31 +0,0 @@ -// @flow -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcAccountPassphrase } from './types'; - -export type ChangeEtcAccountPassphraseParams = { - ca: string, - walletId: string, - oldPassword: ?string, - newPassword: ?string, -}; - -export const changeEtcAccountPassphrase = ( - { ca, walletId, oldPassword, newPassword }: ChangeEtcAccountPassphraseParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'daedalus_changePassphrase', - params: [ - walletId, - oldPassword || '', - newPassword || '', - ] - }) -); diff --git a/source/renderer/app/api/etc/createEtcAccount.js b/source/renderer/app/api/etc/createEtcAccount.js deleted file mode 100644 index a8e0d25147..0000000000 --- a/source/renderer/app/api/etc/createEtcAccount.js +++ /dev/null @@ -1,29 +0,0 @@ -// @flow -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcWalletId } from './types'; - -export type CreateEtcAccountParams = { - ca: string, - privateKey: string, - password: ?string, -}; - -export const createEtcAccount = ( - { ca, privateKey, password }: CreateEtcAccountParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'personal_importRawKey', - params: [ - privateKey, - password || '', - ] - }) -); diff --git a/source/renderer/app/api/etc/deleteEtcAccount.js b/source/renderer/app/api/etc/deleteEtcAccount.js deleted file mode 100644 index 3bd9e00db7..0000000000 --- a/source/renderer/app/api/etc/deleteEtcAccount.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; - -export type DeleteEtcAccountBalanceParams = { - ca: string, - walletId: string, -}; - -export const deleteEtcAccount = ( - { ca, walletId }: DeleteEtcAccountBalanceParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'daedalus_deleteWallet', - params: [walletId] - }) -); diff --git a/source/renderer/app/api/etc/etcLocalStorage.js b/source/renderer/app/api/etc/etcLocalStorage.js deleted file mode 100644 index 4dd0e46942..0000000000 --- a/source/renderer/app/api/etc/etcLocalStorage.js +++ /dev/null @@ -1,201 +0,0 @@ -// @flow -import Store from 'electron-store'; -import type { AssuranceModeOption } from '../../types/transactionAssuranceTypes'; -import environment from '../../../../common/environment'; - -const store = new Store(); - -const networkForLocalStorage = String(environment.NETWORK); -const storageKeys = { - WALLETS: networkForLocalStorage + '-ETC-WALLETS', -}; - -/** - * This api layer provides access to the electron local storage - * for account/wallet properties that are not synced with ETC backend. - */ - -export type EtcWalletData = { - id: string, - name: string, - assurance: AssuranceModeOption, - hasPassword: boolean, - passwordUpdateDate: ?Date, -}; - -export const getEtcWalletData = ( - walletId: string -): Promise => new Promise((resolve, reject) => { - try { - const walletData = store.get(`${storageKeys.WALLETS}.${walletId}`); - resolve(walletData); - } catch (error) { - return reject(error); - } -}); - -export const setEtcWalletData = ( - walletData: EtcWalletData -): Promise => new Promise((resolve, reject) => { - try { - const walletId = walletData.id; - store.set(`${storageKeys.WALLETS}.${walletId}`, walletData); - resolve(); - } catch (error) { - return reject(error); - } -}); - -export const updateEtcWalletData = ( - updatedWalletData: { - id: string, - name?: string, - assurance?: AssuranceModeOption, - hasPassword?: boolean, - passwordUpdateDate?: ?Date, - } -): Promise => new Promise(async (resolve, reject) => { - const walletId = updatedWalletData.id; - const walletData = await getEtcWalletData(walletId); - Object.assign(walletData, updatedWalletData); - try { - store.set(`${storageKeys.WALLETS}.${walletId}`, walletData); - resolve(); - } catch (error) { - return reject(error); - } -}); - -export const unsetEtcWalletData = ( - walletId: string -): Promise => new Promise((resolve, reject) => { - try { - store.delete(`${storageKeys.WALLETS}.${walletId}`); - resolve(); - } catch (error) { - return reject(error); - } -}); - -export const unsetEtcWalletsData = (): Promise => new Promise((resolve) => { - try { - store.delete(storageKeys.WALLETS); - resolve(); - } catch (error) {} // eslint-disable-line -}); - -// ======= DUMMY DATA ======= - -export const ETC_WALLETS_DATA = [ - { - id: '0xafe149dce151dc829008779820cc4a947ab2257e', - name: 'Wallet 1', - assurance: 'CWANormal', - hasPassword: true, - passwordUpdateDate: new Date('2017-10-01'), - }, - { - id: '0x6f71adcdb471af7f7623d7e4ff27d9e5de4fd3b1', - name: 'Wallet 2', - assurance: 'CWANormal', - hasPassword: true, - passwordUpdateDate: new Date('2017-10-01'), - }, - { - id: '0xde81f296a5c102ce0a4ac2b98f203f6763895185', - name: 'Wallet 3', - assurance: 'CWANormal', - hasPassword: true, - passwordUpdateDate: new Date('2017-10-01'), - }, - { - id: '0xbe1f71291cb65c6aadd025427e9f60e8f9c95ffc', - name: 'Wallet 4', - assurance: 'CWANormal', - hasPassword: true, - passwordUpdateDate: new Date('2017-10-01'), - }, - { - id: '0x31d3500eaff3f8e6a3cfa1ab0523abe2e7910424', - name: 'Wallet 5', - assurance: 'CWANormal', - hasPassword: false, - passwordUpdateDate: null, - }, - { - id: '0xf5c12ec7f63d6a134366c886bcf9e70777107fd8', - name: 'Wallet 6', - assurance: 'CWANormal', - hasPassword: true, - passwordUpdateDate: new Date('2017-10-01'), - }, - { - id: '0xa492e4770faf26399df9cfb3f1c07f08cd69a3f1', - name: 'Wallet 7', - assurance: 'CWANormal', - hasPassword: true, - passwordUpdateDate: new Date('2017-10-01'), - }, - { - id: '0x37c0762a7b9dad03d7934353abf28daa31947e4e', - name: 'Wallet 8', - assurance: 'CWANormal', - hasPassword: true, - passwordUpdateDate: new Date('2017-10-01'), - }, - { - id: '0x7864deb4fe7808eb09cb11c4b22effc21b75dc05', - name: 'Wallet 9', - assurance: 'CWANormal', - hasPassword: true, - passwordUpdateDate: new Date('2017-10-01'), - }, - { - id: '0x0a61652996de1b8051cc2b0dfc6e671ac5f4e624', - name: 'Wallet 10', - assurance: 'CWANormal', - hasPassword: false, - passwordUpdateDate: null, - }, - { - id: '0x2ba43ca4dc0788ae2b09a3e75e4c1ff191a84279', - name: 'Wallet 11', - assurance: 'CWANormal', - hasPassword: false, - passwordUpdateDate: null, - }, - { - id: '0x9a62039f33d6ade11d00a127a30ba5fee4c969cb', - name: 'Wallet 12', - assurance: 'CWANormal', - hasPassword: false, - passwordUpdateDate: null, - }, - { - id: '0xf39c9ed4abd09e18abc3ba40a53d7c65e2e6c25f', - name: 'Wallet 13', - assurance: 'CWANormal', - hasPassword: false, - passwordUpdateDate: null, - }, - { - id: '0x2fecc6a10ab19719f7a4cd493e018742d119f945', - name: 'Wallet 14', - assurance: 'CWANormal', - hasPassword: false, - passwordUpdateDate: null, - }, -]; - -export const initEtcWalletsDummyData = (): Promise => new Promise(async (resolve, reject) => { - try { - ETC_WALLETS_DATA.forEach(async (dummyWalletData) => { - const walletId = dummyWalletData.id; - const walletData = await getEtcWalletData(walletId); - if (!walletData) await setEtcWalletData(dummyWalletData); - }); - resolve(); - } catch (error) { - return reject(error); - } -}); diff --git a/source/renderer/app/api/etc/getEtcAccountBalance.js b/source/renderer/app/api/etc/getEtcAccountBalance.js deleted file mode 100644 index 4fcb3ffd67..0000000000 --- a/source/renderer/app/api/etc/getEtcAccountBalance.js +++ /dev/null @@ -1,29 +0,0 @@ -// @flow -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcWalletBalance } from './types'; - -export type GetEtcAccountBalanceParams = { - ca: string, - walletId: string, - status: 'latest' | 'earliest' | 'pending', -}; - -export const getEtcAccountBalance = ( - { ca, walletId, status }: GetEtcAccountBalanceParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'eth_getBalance', - params: [ - walletId, - status - ] - }) -); diff --git a/source/renderer/app/api/etc/getEtcAccountRecoveryPhrase.js b/source/renderer/app/api/etc/getEtcAccountRecoveryPhrase.js deleted file mode 100644 index eb66fd5658..0000000000 --- a/source/renderer/app/api/etc/getEtcAccountRecoveryPhrase.js +++ /dev/null @@ -1,7 +0,0 @@ -// @flow -import { generateMnemonic } from '../../utils/crypto'; -import type { EtcRecoveryPassphrase } from './types'; - -export const getEtcAccountRecoveryPhrase = (): EtcRecoveryPassphrase => ( - generateMnemonic().split(' ') -); diff --git a/source/renderer/app/api/etc/getEtcAccounts.js b/source/renderer/app/api/etc/getEtcAccounts.js deleted file mode 100644 index 0af21e380f..0000000000 --- a/source/renderer/app/api/etc/getEtcAccounts.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcAccounts } from './types'; - -export type GetEtcAccountsParams = { - ca: string, -}; - -export const getEtcAccounts = ( - { ca }: GetEtcAccountsParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'personal_listAccounts', - }) -); diff --git a/source/renderer/app/api/etc/getEtcBlock.js b/source/renderer/app/api/etc/getEtcBlock.js deleted file mode 100644 index 2f79c5e524..0000000000 --- a/source/renderer/app/api/etc/getEtcBlock.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcBlock } from './types'; - -export type GetEtcBlockByHashParams = { - ca: string, - blockHash: string, -}; - -export const getEtcBlockByHash = ( - { ca, blockHash }: GetEtcBlockByHashParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'eth_getBlockByHash', - params: [blockHash, true] // returns the full transaction objects - }) -); diff --git a/source/renderer/app/api/etc/getEtcBlockNumber.js b/source/renderer/app/api/etc/getEtcBlockNumber.js deleted file mode 100644 index 5a0056e829..0000000000 --- a/source/renderer/app/api/etc/getEtcBlockNumber.js +++ /dev/null @@ -1,31 +0,0 @@ -// @flow -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcBlockNumber } from './types'; - -export type GetEtcBlockNumberParams = { - ca: string, -}; - -/** - * Returns the number of most recent block. - * @param ca - * @returns {Promise} integer of the current block number the client is on. - */ - -export const getEtcBlockNumber = async ( - { ca }: GetEtcBlockNumberParams -): Promise => { - const response = await request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'eth_blockNumber', - params: [] - }); - return parseInt(response, 16); -}; diff --git a/source/renderer/app/api/etc/getEtcEstimatedGas.js b/source/renderer/app/api/etc/getEtcEstimatedGas.js deleted file mode 100644 index 443c1b9b96..0000000000 --- a/source/renderer/app/api/etc/getEtcEstimatedGas.js +++ /dev/null @@ -1,35 +0,0 @@ -// @flow -import BigNumber from 'bignumber.js'; -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcGas } from './types'; - -export type GetEtcEstimatedGasParams = { - ca: string, - from: string, - to: string, - value: BigNumber, // QUANTITY in WEI with base 10 - gasPrice: BigNumber, // QUANTITY in WEI with base 10 -}; - -export const getEtcEstimatedGas = ( - { ca, from, to, value, gasPrice }: GetEtcEstimatedGasParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'eth_estimateGas', - params: [{ - from, - to, - // Convert quantities to HEX for the ETC api - value: new BigNumber(value).toString(16), - gasPrice: new BigNumber(gasPrice).toString(16), - }] - }) -); diff --git a/source/renderer/app/api/etc/getEtcSyncProgress.js b/source/renderer/app/api/etc/getEtcSyncProgress.js deleted file mode 100644 index 401a8398f0..0000000000 --- a/source/renderer/app/api/etc/getEtcSyncProgress.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcSyncProgress } from './types'; - -export type GetEtcSyncProgressParams = { - ca: string, -}; - -export const getEtcSyncProgress = ( - { ca }: GetEtcSyncProgressParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'eth_syncing', - }) -); diff --git a/source/renderer/app/api/etc/getEtcTransaction.js b/source/renderer/app/api/etc/getEtcTransaction.js deleted file mode 100644 index f127be16fa..0000000000 --- a/source/renderer/app/api/etc/getEtcTransaction.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcTransaction } from './types'; - -export type GetEtcTransactionByHashParams = { - ca: string, - txHash: string -}; - -export const getEtcTransactionByHash = ( - { ca, txHash }: GetEtcTransactionByHashParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'eth_getTransactionByHash', - params: [txHash] - }) -); diff --git a/source/renderer/app/api/etc/getEtcTransactions.js b/source/renderer/app/api/etc/getEtcTransactions.js deleted file mode 100644 index 0421c4b158..0000000000 --- a/source/renderer/app/api/etc/getEtcTransactions.js +++ /dev/null @@ -1,41 +0,0 @@ -// @flow -import BigNumber from 'bignumber.js'; -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcTransactions } from './types'; - -export type GetEtcTransactionsParams = { - ca: string, - walletId: string, - fromBlock: number, - toBlock: number, -}; - -/** - * Returns account transactions (both sent and received) from a range of blocks. - * The response also includes pending transactions. - * @param ca (the TLS certificate) - * @param walletId - * @param fromBlock (in the past) - * @param toBlock (more recent) - * @returns {*} - */ -export const getEtcTransactions = ( - { ca, walletId, fromBlock, toBlock }: GetEtcTransactionsParams -): Promise => ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'daedalus_getAccountTransactions', - params: [ - walletId, - new BigNumber(fromBlock).toString(16), - new BigNumber(toBlock).toString(16), - ], - }) -); diff --git a/source/renderer/app/api/etc/index.js b/source/renderer/app/api/etc/index.js deleted file mode 100644 index 345f554b98..0000000000 --- a/source/renderer/app/api/etc/index.js +++ /dev/null @@ -1,417 +0,0 @@ -// @flow -import BigNumber from 'bignumber.js'; -import { remote } from 'electron'; -import { isAddress } from 'web3-utils/src/utils'; -import { getEtcSyncProgress } from './getEtcSyncProgress'; -import { Logger, stringifyData, stringifyError } from '../../../../common/logging'; -import environment from '../../../../common/environment'; -import { - GenericApiError, IncorrectWalletPasswordError, - WalletAlreadyRestoredError, -} from '../common'; -import { mnemonicToSeedHex, quantityToBigNumber, unixTimestampToDate } from './lib/utils'; -import { - getEtcWalletData, setEtcWalletData, unsetEtcWalletData, updateEtcWalletData, - initEtcWalletsDummyData, -} from './etcLocalStorage'; -import { ETC_DEFAULT_GAS_PRICE, WEI_PER_ETC } from '../../config/numbersConfig'; -import Wallet from '../../domains/Wallet'; -import WalletTransaction, { transactionStates, transactionTypes } from '../../domains/WalletTransaction'; - -import { getEtcAccounts } from './getEtcAccounts'; -import { getEtcAccountBalance } from './getEtcAccountBalance'; -import { getEtcAccountRecoveryPhrase } from './getEtcAccountRecoveryPhrase'; -import { createEtcAccount } from './createEtcAccount'; -import { getEtcBlockByHash } from './getEtcBlock'; -import { sendEtcTransaction } from './sendEtcTransaction'; -import { deleteEtcAccount } from './deleteEtcAccount'; -import { getEtcTransactionByHash } from './getEtcTransaction'; -import { changeEtcAccountPassphrase } from './changeEtcAccountPassphrase'; -import { getEtcEstimatedGas } from './getEtcEstimatedGas'; -import { getEtcTransactions } from './getEtcTransactions'; -import { getEtcBlockNumber } from './getEtcBlockNumber'; -import { isValidMnemonic } from '../../../../common/decrypt'; -import { sendEtcBugReport } from './sendEtcBugReport'; - -import type { - GetSyncProgressResponse, GetWalletRecoveryPhraseResponse, - GetTransactionsRequest, GetTransactionsResponse, GetWalletsResponse, - CreateWalletRequest, CreateWalletResponse, UpdateWalletResponse, - UpdateWalletPasswordRequest, UpdateWalletPasswordResponse, - DeleteWalletRequest, DeleteWalletResponse, - RestoreWalletRequest, RestoreWalletResponse, - CreateTransactionResponse, SendBugReportRequest, - SendBugReportResponse, -} from '../common'; -import type { - EtcSyncProgress, EtcAccounts, EtcWalletBalance, - EtcTransactions, EtcBlockNumber, EtcWalletId, - EtcRecoveryPassphrase, EtcTxHash, EtcGas, - EtcBlock, EtcTransaction, -} from './types'; - -// Load Dummy ETC Wallets into Local Storage -if (environment.isDev() && environment.isEtcApi()) { - (async () => { - await initEtcWalletsDummyData(); - })(); -} - -/** - * The ETC api layer that handles all requests to the - * mantis client which is used as backend for ETC blockchain. - */ - -const ca = remote.getGlobal('ca'); - -// export const ETC_API_HOST = 'ec2-52-30-28-57.eu-west-1.compute.amazonaws.com'; -export const ETC_API_HOST = 'localhost'; -export const ETC_API_PORT = 8546; - -// ETC specific Request / Response params -export type ImportWalletResponse = Wallet; -export type UpdateWalletRequest = Wallet; - -export type ImportWalletRequest = { - name: string, - privateKey: string, - password: ?string, -}; - -export type CreateTransactionRequest = { - from: string, - to: string, - value: BigNumber, - password: string, -}; - -export type GetEstimatedGasPriceRequest = { - ca: string, - from: string, - to: string, - value: BigNumber, - gasPrice: BigNumber, -}; -export type GetEstimatedGasPriceResponse = Promise; - -export default class EtcApi { - - async getSyncProgress(): Promise { - Logger.debug('EtcApi::getSyncProgress called'); - try { - const response: EtcSyncProgress = await getEtcSyncProgress({ ca }); - Logger.debug('EtcApi::getSyncProgress success: ' + stringifyData(response)); - return { - localDifficulty: response ? parseInt(response.currentBlock, 16) : 100, - networkDifficulty: response ? parseInt(response.highestBlock, 16) : 100, - }; - } catch (error) { - Logger.debug('EtcApi::getSyncProgress error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - getWallets = async (): Promise => { - Logger.debug('EtcApi::getWallets called'); - try { - const accounts: EtcAccounts = await getEtcAccounts({ ca }); - Logger.debug('EtcApi::getWallets success: ' + stringifyData(accounts)); - return await Promise.all(accounts.map(async (id) => { - const amount = await this.getAccountBalance(id); - try { - // use wallet data from local storage - const walletData = await getEtcWalletData(id); // fetch wallet data from local storage - const { name, assurance, hasPassword, passwordUpdateDate } = walletData; - return new Wallet({ id, name, amount, assurance, hasPassword, passwordUpdateDate }); - } catch (error) { - // there is no wallet data in local storage - use fallback data - const fallbackWalletData = { - id, - name: 'Untitled Wallet (*)', - assurance: 'CWANormal', - hasPassword: true, - passwordUpdateDate: new Date(), - }; - const { name, assurance, hasPassword, passwordUpdateDate } = fallbackWalletData; - return new Wallet({ id, name, amount, assurance, hasPassword, passwordUpdateDate }); - } - })); - } catch (error) { - Logger.error('EtcApi::getWallets error: ' + stringifyError(error)); - throw new GenericApiError(); - } - }; - - async getAccountBalance(walletId: string): Promise { - Logger.debug('EtcApi::getAccountBalance called'); - try { - const status = 'latest'; - const response: EtcWalletBalance = await getEtcAccountBalance({ - ca, walletId, status, - }); - Logger.debug('EtcApi::getAccountBalance success: ' + stringifyData(response)); - return quantityToBigNumber(response).dividedBy(WEI_PER_ETC); - } catch (error) { - Logger.error('EtcApi::getAccountBalance error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - getTransactions = async (request: GetTransactionsRequest): Promise => { - Logger.debug('EtcApi::getTransactions called: ' + stringifyData(request)); - try { - const walletId = request.walletId; - const mostRecentBlockNumber: EtcBlockNumber = await getEtcBlockNumber({ ca }); - const response: EtcTransactions = await getEtcTransactions({ - ca, - walletId, - fromBlock: Math.max(mostRecentBlockNumber - 10000, 0), - toBlock: mostRecentBlockNumber, - }); - Logger.debug('EtcApi::getTransactions success: ' + stringifyData(response)); - const transactions = await Promise.all( - response.transactions.map(async (txData: EtcTransaction) => ( - _createWalletTransactionFromServerData(txData) - )) - ); - return { - transactions, - total: transactions.length, - }; - } catch (error) { - Logger.debug('EtcApi::getTransactions error: ' + stringifyError(error)); - throw new GenericApiError(); - } - }; - - async importWallet(request: ImportWalletRequest): Promise { - Logger.debug('EtcApi::importWallet called'); - const { name, privateKey, password } = request; - try { - const response: EtcWalletId = await createEtcAccount({ - ca, privateKey, password, - }); - Logger.debug('EtcApi::importWallet success: ' + stringifyData(response)); - const id = response; - const amount = quantityToBigNumber('0'); - const assurance = 'CWANormal'; - const hasPassword = password !== null; - const passwordUpdateDate = hasPassword ? new Date() : null; - await setEtcWalletData({ - id, name, assurance, hasPassword, passwordUpdateDate, - }); - return new Wallet({ id, name, amount, assurance, hasPassword, passwordUpdateDate }); - } catch (error) { - Logger.error('EtcApi::importWallet error: ' + stringifyError(error)); - throw error; // Error is handled in parent method (e.g. createWallet/restoreWallet) - } - } - - createWallet = async (request: CreateWalletRequest): Promise => { - Logger.debug('EtcApi::createWallet called'); - const { name, mnemonic, password } = request; - const privateKey = mnemonicToSeedHex(mnemonic); - try { - const response: ImportWalletResponse = await this.importWallet({ - name, privateKey, password, - }); - Logger.debug('EtcApi::createWallet success: ' + stringifyData(response)); - return response; - } catch (error) { - Logger.error('EtcApi::createWallet error: ' + stringifyError(error)); - throw new GenericApiError(); - } - }; - - getWalletRecoveryPhrase(): Promise { - Logger.debug('EtcApi::getWalletRecoveryPhrase called'); - try { - const response: Promise = new Promise( - (resolve) => resolve(getEtcAccountRecoveryPhrase()) - ); - Logger.debug('EtcApi::getWalletRecoveryPhrase success'); - return response; - } catch (error) { - Logger.error('EtcApi::getWalletRecoveryPhrase error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async createTransaction(params: CreateTransactionRequest): CreateTransactionResponse { - Logger.debug('EtcApi::createTransaction called'); - try { - const senderAccount = params.from; - const { from, to, value, password } = params; - const gasPrice = ETC_DEFAULT_GAS_PRICE; - const gas = quantityToBigNumber( - await getEtcEstimatedGas({ ca, from, to, value, gasPrice }) - ); - const txHash: EtcTxHash = await sendEtcTransaction({ - ca, from, to, value, password, gasPrice, gas, - }); - Logger.debug('EtcApi::createTransaction success: ' + stringifyData(txHash)); - return _createTransaction(senderAccount, txHash); - } catch (error) { - Logger.debug('EtcApi::createTransaction error: ' + stringifyError(error)); - if (error.message.includes('Could not decrypt key with given passphrase')) { - throw new IncorrectWalletPasswordError(); - } - throw new GenericApiError(); - } - } - - async updateWallet(request: UpdateWalletRequest): Promise { - Logger.debug('EtcApi::updateWallet called: ' + stringifyData(request)); - const { id, name, amount, assurance, hasPassword, passwordUpdateDate } = request; - try { - await setEtcWalletData({ - id, name, assurance, hasPassword, passwordUpdateDate, - }); - Logger.debug('EtcApi::updateWallet success: ' + stringifyData(request)); - return new Wallet({ id, name, amount, assurance, hasPassword, passwordUpdateDate }); - } catch (error) { - Logger.error('EtcApi::updateWallet error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async updateWalletPassword( - request: UpdateWalletPasswordRequest - ): Promise { - Logger.debug('EtcApi::updateWalletPassword called'); - const { walletId, oldPassword, newPassword } = request; - try { - await changeEtcAccountPassphrase({ - ca, walletId, oldPassword, newPassword, - }); - Logger.debug('EtcApi::updateWalletPassword success'); - const hasPassword = newPassword !== null; - const passwordUpdateDate = hasPassword ? new Date() : null; - await updateEtcWalletData({ - id: walletId, hasPassword, passwordUpdateDate - }); - return true; - } catch (error) { - Logger.debug('EtcApi::updateWalletPassword error: ' + stringifyError(error)); - if (error.message.includes('Could not decrypt key with given passphrase')) { - throw new IncorrectWalletPasswordError(); - } - throw new GenericApiError(); - } - } - - async deleteWallet(request: DeleteWalletRequest): Promise { - Logger.debug('EtcApi::deleteWallet called: ' + stringifyData(request)); - const { walletId } = request; - try { - await deleteEtcAccount({ ca, walletId }); - Logger.debug('EtcApi::deleteWallet success: ' + stringifyData(request)); - await unsetEtcWalletData(walletId); // remove wallet data from local storage - return true; - } catch (error) { - Logger.error('EtcApi::deleteWallet error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - restoreWallet = async (request: RestoreWalletRequest): Promise => { - Logger.debug('EtcApi::restoreWallet called'); - const { recoveryPhrase: mnemonic, walletName: name, walletPassword: password } = request; - const privateKey = mnemonicToSeedHex(mnemonic); - try { - const wallet: ImportWalletResponse = await this.importWallet({ name, privateKey, password }); - Logger.debug('EtcApi::restoreWallet success: ' + stringifyData(wallet)); - return wallet; - } catch (error) { - Logger.debug('EtcApi::restoreWallet error: ' + stringifyError(error)); - if (error.message.includes('account already exists')) { - throw new WalletAlreadyRestoredError(); - } - throw new GenericApiError(); - } - }; - - isValidMnemonic(mnemonic: string): Promise { - return isValidMnemonic(mnemonic, 12); - } - - isValidAddress(address: string): Promise { - return Promise.resolve(isAddress(address)); - } - - async getEstimatedGasPriceResponse( - request: GetEstimatedGasPriceRequest - ): GetEstimatedGasPriceResponse { - Logger.debug('EtcApi::getEstimatedGasPriceResponse called'); - try { - const { from, to, value, gasPrice } = request; - const estimatedGas: EtcGas = await getEtcEstimatedGas({ - ca, from, to, value, gasPrice, - }); - Logger.debug('EtcApi::getEstimatedGasPriceResponse success: ' + stringifyData(estimatedGas)); - return quantityToBigNumber(estimatedGas).times(request.gasPrice).dividedBy(WEI_PER_ETC); - } catch (error) { - Logger.error('EtcApi::getEstimatedGasPriceResponse error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - async sendBugReport(request: SendBugReportRequest): Promise { - Logger.debug('EtcApi::sendBugReport called: ' + stringifyData(request)); - try { - await sendEtcBugReport({ requestFormData: request, application: 'mantis-node' }); - Logger.debug('EtcApi::sendBugReport success'); - return true; - } catch (error) { - Logger.error('EtcApi::sendBugReport error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } - - testReset = async (): Promise => { - Logger.debug('EtcApi::testReset called'); - try { - const accounts: EtcAccounts = await getEtcAccounts({ ca }); - await Promise.all(accounts.map(async (id) => this.deleteWallet({ walletId: id }))); - Logger.debug('EtcApi::testReset success'); - return true; - } catch (error) { - Logger.error('EtcApi::testReset error: ' + stringifyError(error)); - throw new GenericApiError(); - } - } -} - -// ========== TRANSFORM SERVER DATA INTO FRONTEND MODELS ========= - -const _createWalletTransactionFromServerData = async ( - txData: EtcTransaction, -): Promise => { - const { hash, blockHash, value, from, to, pending, isOutgoing, } = txData; - const txBlock: ?EtcBlock = blockHash ? await getEtcBlockByHash({ - ca, blockHash, - }) : null; - const blockDate = txBlock ? unixTimestampToDate(txBlock.timestamp) : new Date(); - return new WalletTransaction({ - id: hash, - type: isOutgoing ? transactionTypes.EXPEND : transactionTypes.INCOME, - title: '', - description: '', - amount: quantityToBigNumber(value).dividedBy(WEI_PER_ETC), - date: blockDate, - numberOfConfirmations: 0, - addresses: { - from: [from], - to: [to], - }, - state: pending ? transactionStates.PENDING : transactionStates.OK, - }); -}; - -const _createTransaction = async (senderAccount: EtcWalletId, txHash: EtcTxHash) => { - const txData: EtcTransaction = await getEtcTransactionByHash({ - ca, txHash, - }); - txData.isOutgoing = senderAccount === txData.from; - return _createWalletTransactionFromServerData(txData); -}; diff --git a/source/renderer/app/api/etc/lib/request.js b/source/renderer/app/api/etc/lib/request.js deleted file mode 100644 index c71b594471..0000000000 --- a/source/renderer/app/api/etc/lib/request.js +++ /dev/null @@ -1,61 +0,0 @@ -// @flow -import https from 'https'; -import { getContentLength } from '../../lib/utils'; - -export type RequestOptions = { - hostname: string, - method: string, - path: string, - port: number, - ca: string, - headers?: { - 'Content-Type': string, - 'Content-Length': number, - }, -}; - -function typedRequest( - httpOptions: RequestOptions, queryParams?: {} -): Promise { - return new Promise((resolve, reject) => { - // Prepare request with http options and (optional) query params - const options: RequestOptions = Object.assign({}, httpOptions); - let requestBody = ''; - if (queryParams) requestBody = JSON.stringify(queryParams); - options.headers = Object.assign(options.headers || {}, { - 'Content-Type': 'application/json', - 'Content-Length': getContentLength(requestBody), - }); - const httpsRequest = https.request(options, (response) => { - let body = ''; - // Cardano-sl returns chunked requests, so we need to concat them - response.on('data', (chunk) => (body += chunk)); - // Reject errors - response.on('error', (error) => reject(error)); - // Resolve JSON results and handle weird backend behavior - response.on('end', () => { - try { - const parsedBody = JSON.parse(body); - if (parsedBody.result != null) { - resolve(parsedBody.result); - } else if (parsedBody.error) { - reject(new Error(parsedBody.error.message)); - } else { - // TODO: investigate if that can happen! (no Right or Left in a response) - reject(new Error('Unknown response from backend.')); - } - } catch (error) { - // Handle internal server errors (e.g. HTTP 500 - 'Something went wrong') - reject(new Error(error)); - } - }); - }); - httpsRequest.on('error', (error) => reject(error)); - if (queryParams) { - httpsRequest.write(requestBody); - } - httpsRequest.end(); - }); -} - -export const request = typedRequest; diff --git a/source/renderer/app/api/etc/lib/utils.js b/source/renderer/app/api/etc/lib/utils.js deleted file mode 100644 index b78a1d4d3b..0000000000 --- a/source/renderer/app/api/etc/lib/utils.js +++ /dev/null @@ -1,34 +0,0 @@ -// @flow -import { Buffer } from 'safe-buffer'; -import { pbkdf2Sync as pbkdf2 } from 'pbkdf2'; -import * as unorm from 'unorm'; -import BigNumber from 'bignumber.js'; -import { isString } from 'lodash'; - -/** - * Takes an input and transforms it into an bignumber - * - * @method toBigNumber - * @param number {Number|String|BigNumber} string, HEX string - * @return {BigNumber} BigNumber - */ -export const quantityToBigNumber = (number: string) => { - number = number || '0'; - if (isString(number) && (number.indexOf('0x') === 0 || number.indexOf('-0x') === 0)) { - return new BigNumber(number, 16); - } - return new BigNumber(number, 10); -}; - -export const mnemonicToSeedHex = (mnemonic: string, password: ?string) => { - const mnemonicBuffer = Buffer.from(unorm.nfkd(mnemonic), 'utf8'); - const salt = 'mnemonic' + (unorm.nfkd(password) || ''); - const saltBuffer = Buffer.from(salt, 'utf8'); - return pbkdf2(mnemonicBuffer, saltBuffer, 2048, 32, 'sha512').toString('hex'); -}; - -export const unixTimestampToDate = (rawTimestamp: string) => ( - // We have to convert unix timestamp (seconds since …) to - // JS date (milliseconds since …) by multiplying it with 1000 - new Date(quantityToBigNumber(rawTimestamp).times(1000).toNumber()) -); diff --git a/source/renderer/app/api/etc/sendEtcBugReport.js b/source/renderer/app/api/etc/sendEtcBugReport.js deleted file mode 100644 index f0e1e20bed..0000000000 --- a/source/renderer/app/api/etc/sendEtcBugReport.js +++ /dev/null @@ -1,49 +0,0 @@ -// @flow -import moment from 'moment'; -import url from 'url'; -import { request } from '../lib/reportRequest'; -import environment from '../../../../common/environment'; - -export type SendEtcBugReportRequestParams = { - requestFormData: { - email: string, - subject: string, - problem: string, - compressedLog: string, - }, -}; - -export const sendEtcBugReport = ( - { requestFormData }: SendEtcBugReportRequestParams -): Promise<{}> => { - const { email, subject, problem, compressedLog } = requestFormData; - const { version, os, API_VERSION, NETWORK, build, getInstallerVersion, REPORT_URL } = environment; - const reportUrl = url.parse(REPORT_URL); - - // Report server recognizes the following networks: mainnet, staging and testnet - const network = NETWORK === 'development' ? 'staging' : NETWORK; - - return request({ - hostname: reportUrl.hostname, - method: 'POST', - path: '/api/v1/report', - port: reportUrl.port, - }, { - product: 'Mantis Wallet', - frontendVersion: version, - backendVersion: API_VERSION, - network, - build, - installerVersion: getInstallerVersion(), - os, - compressedLog, - date: moment().format('YYYY-MM-DDTHH:mm:ss'), - magic: 2000000000, - type: { - type: 'customreport', - email, - subject, - problem, - } - }); -}; diff --git a/source/renderer/app/api/etc/sendEtcTransaction.js b/source/renderer/app/api/etc/sendEtcTransaction.js deleted file mode 100644 index 3120a43555..0000000000 --- a/source/renderer/app/api/etc/sendEtcTransaction.js +++ /dev/null @@ -1,43 +0,0 @@ -// @flow -import BigNumber from 'bignumber.js'; -import { request } from './lib/request'; -import { ETC_API_HOST, ETC_API_PORT } from './index'; -import type { EtcTxHash } from './types'; - -export type SendEtcTransactionParams = { - ca: string, - from: string, - to: string, - value: BigNumber, - password: string, - gasPrice: BigNumber, - gas: BigNumber, -}; - -export const sendEtcTransaction = ( - { ca, from, to, value, password, gasPrice, gas }: SendEtcTransactionParams -): Promise => { - const txParams = { - from, - to, - value: value.toString(16), - gasPrice: gasPrice.toString(16), - gas: gas.toString(16), - }; - return ( - request({ - hostname: ETC_API_HOST, - method: 'POST', - path: '/', - port: ETC_API_PORT, - ca, - }, { - jsonrpc: '2.0', - method: 'personal_sendTransaction', - params: [ - txParams, - password, - ] - }) - ); -}; diff --git a/source/renderer/app/api/etc/types.js b/source/renderer/app/api/etc/types.js deleted file mode 100644 index 00fbdde77c..0000000000 --- a/source/renderer/app/api/etc/types.js +++ /dev/null @@ -1,44 +0,0 @@ -// @flow -import BigNumber from 'bignumber.js'; - -export type EtcAccountPassphrase = string; -export type EtcWalletId = string; -export type EtcWalletBalance = BigNumber; -export type EtcBlockNumber = number; -export type EtcGas = string; -export type EtcGasPrice = BigNumber; -export type EtcTxHash = string; - -export type EtcRecoveryPassphrase = Array; - -export type EtcAccounts = Array; - -export type EtcBlock = { - timestamp: string -}; - -export type EtcSyncProgress = ?{ - startingBlock: EtcBlock, - currentBlock: EtcBlock, - highestBlock: EtcBlock -}; - -export type EtcTransaction = { - hash: EtcTxHash, - nonce: string, - blockHash: string, - blockNumber: EtcBlockNumber, - transactionIndex: string, - from: EtcWalletId, - to: EtcWalletId, - value: string, - gasPrice: EtcGasPrice, - gas: EtcGas, - input: string, - pending: boolean, - isOutgoing: boolean, -}; - -export type EtcTransactions = { - transactions: Array, -}; diff --git a/source/renderer/app/api/index.js b/source/renderer/app/api/index.js index ae3e85ad6c..c86091066a 100644 --- a/source/renderer/app/api/index.js +++ b/source/renderer/app/api/index.js @@ -1,16 +1,19 @@ // @flow -import AdaApi from './ada/index'; -import EtcApi from './etc/index'; -import LocalStorageApi from './localStorage/index'; +import AdaApi from './api'; +import LocalStorageApi from './utils/localStorage'; +import environment from '../../../common/environment'; export type Api = { ada: AdaApi, - etc: EtcApi, localStorage: LocalStorageApi, }; export const setupApi = (): Api => ({ - ada: new AdaApi(), - etc: new EtcApi(), + ada: new AdaApi(environment.isTest(), { + port: 8090, + ca: Uint8Array.from([]), + key: Uint8Array.from([]), + cert: Uint8Array.from([]), + }), localStorage: new LocalStorageApi(), }); diff --git a/source/renderer/app/api/lib/utils.js b/source/renderer/app/api/lib/utils.js deleted file mode 100644 index 8a4d5ab73c..0000000000 --- a/source/renderer/app/api/lib/utils.js +++ /dev/null @@ -1,6 +0,0 @@ -// @flow - -// 'TextEncoder' is used to measure correct length of UTF-8 strings -export const getContentLength = (content: string) => ( - (new TextEncoder()).encode(content).length -); diff --git a/source/renderer/app/api/nodes/errors.js b/source/renderer/app/api/nodes/errors.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/renderer/app/api/nodes/requests/applyNodeUpdate.js b/source/renderer/app/api/nodes/requests/applyNodeUpdate.js new file mode 100644 index 0000000000..bdc1f1154f --- /dev/null +++ b/source/renderer/app/api/nodes/requests/applyNodeUpdate.js @@ -0,0 +1,14 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; + +export const applyNodeUpdate = ( + config: RequestConfig, +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/internal/apply-update', + ...config, + }) +); diff --git a/source/renderer/app/api/nodes/requests/getNextNodeUpdate.js b/source/renderer/app/api/nodes/requests/getNextNodeUpdate.js new file mode 100644 index 0000000000..04e45e97ca --- /dev/null +++ b/source/renderer/app/api/nodes/requests/getNextNodeUpdate.js @@ -0,0 +1,14 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; + +export const getNextNodeUpdate = ( + config: RequestConfig +): Promise => ( + request({ + hostname: 'localhost', + method: 'GET', + path: '/api/internal/next-update', + ...config, + }) +); diff --git a/source/renderer/app/api/nodes/requests/getNodeInfo.js b/source/renderer/app/api/nodes/requests/getNodeInfo.js new file mode 100644 index 0000000000..8c5f932523 --- /dev/null +++ b/source/renderer/app/api/nodes/requests/getNodeInfo.js @@ -0,0 +1,20 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { NodeInfo } from '../types'; +import { request } from '../../utils/request'; + +export type NodeQueryParams = { + force_ntp_check: boolean, +}; + +export const getNodeInfo = ( + config: RequestConfig, + queryParams?: NodeQueryParams, +): Promise => ( + request({ + hostname: 'localhost', + method: 'GET', + path: '/api/v1/node-info', + ...config, + }, queryParams) +); diff --git a/source/renderer/app/api/nodes/requests/postponeNodeUpdate.js b/source/renderer/app/api/nodes/requests/postponeNodeUpdate.js new file mode 100644 index 0000000000..3254082265 --- /dev/null +++ b/source/renderer/app/api/nodes/requests/postponeNodeUpdate.js @@ -0,0 +1,14 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; + +export const postponeNodeUpdate = ( + config: RequestConfig +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/internal/postpone-update', + ...config, + }) +); diff --git a/source/renderer/app/api/nodes/types.js b/source/renderer/app/api/nodes/types.js new file mode 100644 index 0000000000..afed2d758b --- /dev/null +++ b/source/renderer/app/api/nodes/types.js @@ -0,0 +1,45 @@ +// @flow +export type NodeInfo = { + syncProgress: { + quantity: number, + unit: 'percent' + }, + blockchainHeight: ?{ + quantity: number, + unit: ?'blocks' + }, + localBlockchainHeight: { + quantity: number, + unit: ?'blocks' + }, + localTimeInformation: { + differenceFromNtpServer: ?{ + quantity: number, + unit: ?'microseconds' + } + }, + subscriptionStatus: Object +}; + +export type NodeSettings = { + slotDuration: { + quantity: number, + unit: ?'milliseconds' + }, + softwareInfo: NodeSoftware, + projectVersion: string, + gitRevision: string +}; + +export type NodeSoftware = { + applicationName: string, + version: number +}; + +// req/res Node Types +export type GetNetworkStatusResponse = { + subscriptionStatus: Object, + syncProgress: number, + blockchainHeight: number, + localBlockchainHeight: number +}; diff --git a/source/renderer/app/api/ada/errors.js b/source/renderer/app/api/transactions/errors.js similarity index 71% rename from source/renderer/app/api/ada/errors.js rename to source/renderer/app/api/transactions/errors.js index e1a29dba36..44595de024 100644 --- a/source/renderer/app/api/ada/errors.js +++ b/source/renderer/app/api/transactions/errors.js @@ -2,31 +2,6 @@ import { defineMessages } from 'react-intl'; import LocalizableError from '../../i18n/LocalizableError'; const messages = defineMessages({ - apiMethodNotYetImplementedError: { - id: 'api.errors.ApiMethodNotYetImplementedError', - defaultMessage: '!!!This API method is not yet implemented.', - description: '"This API method is not yet implemented." error message.' - }, - walletAlreadyImportedError: { - id: 'api.errors.WalletAlreadyImportedError', - defaultMessage: '!!!Wallet you are trying to import already exists.', - description: '"Wallet you are trying to import already exists." error message.' - }, - redeemAdaError: { - id: 'api.errors.RedeemAdaError', - defaultMessage: '!!!Your ADA could not be redeemed correctly.', - description: '"Your ADA could not be redeemed correctly." error message.' - }, - walletFileImportError: { - id: 'api.errors.WalletFileImportError', - defaultMessage: '!!!Wallet could not be imported, please make sure you are providing a correct file.', - description: '"Wallet could not be imported, please make sure you are providing a correct file." error message.' - }, - notEnoughMoneyToSendError: { - id: 'api.errors.NotEnoughMoneyToSendError', - defaultMessage: '!!!Not enough money to make this transaction.', - description: '"Not enough money to make this transaction." error message.' - }, notAllowedToSendMoneyToSameAddressError: { id: 'api.errors.NotAllowedToSendMoneyToSameAddressError', defaultMessage: '!!!It\'s not allowed to send money to the same address you are sending from. Make sure you have enough addresses with money in this account or send to a different address.', @@ -37,6 +12,16 @@ const messages = defineMessages({ defaultMessage: '!!!It is not allowed to send money to Ada redemption address.', description: '"It is not allowed to send money to Ada redemption address." error message.' }, + notEnoughMoneyToSendError: { + id: 'api.errors.NotEnoughMoneyToSendError', + defaultMessage: '!!!Not enough money to make this transaction.', + description: '"Not enough money to make this transaction." error message.' + }, + redeemAdaError: { + id: 'api.errors.RedeemAdaError', + defaultMessage: '!!!Your ADA could not be redeemed correctly.', + description: '"Your ADA could not be redeemed correctly." error message.' + }, allFundsAlreadyAtReceiverAddressError: { id: 'api.errors.AllFundsAlreadyAtReceiverAddressError', defaultMessage: '!!!All your funds are already at the address you are trying send money to.', @@ -47,85 +32,86 @@ const messages = defineMessages({ defaultMessage: '!!!Not enough Ada for fees. Try sending a smaller amount.', description: '"Not enough Ada for fees. Try sending a smaller amount." error message' }, + notEnoughFundsForTransactionError: { + id: 'api.errors.NotEnoughFundsForTransactionError', + defaultMessage: '!!!Not enough Ada. Try sending a smaller amount.', + description: '"Not enough Ada. Try sending a smaller amount." error message' + }, + canNotCalculateTransactionFeesError: { + id: 'api.errors.CanNotCalculateTransactionFeesError', + defaultMessage: '!!!Cannot calculate fees while there are pending transactions.', + description: '"Cannot calculate fees while there are pending transactions." error message' + }, }); -export class ApiMethodNotYetImplementedError extends LocalizableError { - constructor() { - super({ - id: messages.apiMethodNotYetImplementedError.id, - defaultMessage: messages.apiMethodNotYetImplementedError.defaultMessage, - }); - } -} - -export class WalletAlreadyImportedError extends LocalizableError { +export class NotAllowedToSendMoneyToSameAddressError extends LocalizableError { constructor() { super({ - id: messages.walletAlreadyImportedError.id, - defaultMessage: messages.walletAlreadyImportedError.defaultMessage, + id: messages.notAllowedToSendMoneyToSameAddressError.id, + defaultMessage: messages.notAllowedToSendMoneyToSameAddressError.defaultMessage, }); } } -export class RedeemAdaError extends LocalizableError { +export class NotAllowedToSendMoneyToRedeemAddressError extends LocalizableError { constructor() { super({ - id: messages.redeemAdaError.id, - defaultMessage: messages.redeemAdaError.defaultMessage, + id: messages.notAllowedToSendMoneyToRedeemAddressError.id, + defaultMessage: messages.notAllowedToSendMoneyToRedeemAddressError.defaultMessage, }); } } -export class WalletFileImportError extends LocalizableError { +export class NotEnoughMoneyToSendError extends LocalizableError { constructor() { super({ - id: messages.walletFileImportError.id, - defaultMessage: messages.walletFileImportError.defaultMessage, + id: messages.notEnoughMoneyToSendError.id, + defaultMessage: messages.notEnoughMoneyToSendError.defaultMessage, }); } } -export class NotEnoughMoneyToSendError extends LocalizableError { +export class RedeemAdaError extends LocalizableError { constructor() { super({ - id: messages.notEnoughMoneyToSendError.id, - defaultMessage: messages.notEnoughMoneyToSendError.defaultMessage, + id: messages.redeemAdaError.id, + defaultMessage: messages.redeemAdaError.defaultMessage, }); } } -export class NotAllowedToSendMoneyToSameAddressError extends LocalizableError { +export class AllFundsAlreadyAtReceiverAddressError extends LocalizableError { constructor() { super({ - id: messages.notAllowedToSendMoneyToSameAddressError.id, - defaultMessage: messages.notAllowedToSendMoneyToSameAddressError.defaultMessage, + id: messages.allFundsAlreadyAtReceiverAddressError.id, + defaultMessage: messages.allFundsAlreadyAtReceiverAddressError.defaultMessage, }); } } -export class NotAllowedToSendMoneyToRedeemAddressError extends LocalizableError { +export class NotEnoughFundsForTransactionFeesError extends LocalizableError { constructor() { super({ - id: messages.notAllowedToSendMoneyToRedeemAddressError.id, - defaultMessage: messages.notAllowedToSendMoneyToRedeemAddressError.defaultMessage, + id: messages.notEnoughFundsForTransactionFeesError.id, + defaultMessage: messages.notEnoughFundsForTransactionFeesError.defaultMessage, }); } } -export class AllFundsAlreadyAtReceiverAddressError extends LocalizableError { +export class NotEnoughFundsForTransactionError extends LocalizableError { constructor() { super({ - id: messages.allFundsAlreadyAtReceiverAddressError.id, - defaultMessage: messages.allFundsAlreadyAtReceiverAddressError.defaultMessage, + id: messages.notEnoughFundsForTransactionError.id, + defaultMessage: messages.notEnoughFundsForTransactionError.defaultMessage, }); } } -export class NotEnoughFundsForTransactionFeesError extends LocalizableError { +export class CanNotCalculateTransactionFeesError extends LocalizableError { constructor() { super({ - id: messages.notEnoughFundsForTransactionFeesError.id, - defaultMessage: messages.notEnoughFundsForTransactionFeesError.defaultMessage, + id: messages.canNotCalculateTransactionFeesError.id, + defaultMessage: messages.canNotCalculateTransactionFeesError.defaultMessage, }); } } diff --git a/source/renderer/app/api/transactions/requests/createTransaction.js b/source/renderer/app/api/transactions/requests/createTransaction.js new file mode 100644 index 0000000000..8b655b1171 --- /dev/null +++ b/source/renderer/app/api/transactions/requests/createTransaction.js @@ -0,0 +1,28 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Transaction, PaymentDistribution } from '../types'; +import { request } from '../../utils/request'; + +export type TransactionParams = { + data: { + source: { + accountIndex: number, + walletId: string, + }, + destinations: Array, + groupingPolicy: ?'OptimizeForSecurity' | 'OptimizeForSize', + spendingPassword?: string, + }, +}; + +export const createTransaction = ( + config: RequestConfig, + { data }: TransactionParams +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/v1/transactions', + ...config, + }, {}, data) +); diff --git a/source/renderer/app/api/transactions/requests/getTransactionFee.js b/source/renderer/app/api/transactions/requests/getTransactionFee.js new file mode 100644 index 0000000000..c43c2ee764 --- /dev/null +++ b/source/renderer/app/api/transactions/requests/getTransactionFee.js @@ -0,0 +1,17 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { TransactionParams } from './createTransaction'; +import type { TransactionFee } from '../types'; +import { request } from '../../utils/request'; + +export const getTransactionFee = ( + config: RequestConfig, + { data }: TransactionParams +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/v1/transactions/fees', + ...config, + }, {}, data) +); diff --git a/source/renderer/app/api/transactions/requests/getTransactionHistory.js b/source/renderer/app/api/transactions/requests/getTransactionHistory.js new file mode 100644 index 0000000000..0aa093b4a9 --- /dev/null +++ b/source/renderer/app/api/transactions/requests/getTransactionHistory.js @@ -0,0 +1,28 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Transactions } from '../types'; +import { request } from '../../utils/request'; + +export type GetTxnHistoryParams = { + wallet_id: string, + page: number, + per_page: number, + accountIndex: number, + sort_by: string, +}; + +const requestOptions = { + returnMeta: true, +}; + +export const getTransactionHistory = ( + config: RequestConfig, + { ...requestParams }: GetTxnHistoryParams +): Promise => ( + request({ + hostname: 'localhost', + method: 'GET', + path: '/api/v1/transactions', + ...config, + }, requestParams, null, requestOptions) +); diff --git a/source/renderer/app/api/transactions/requests/redeemAda.js b/source/renderer/app/api/transactions/requests/redeemAda.js new file mode 100644 index 0000000000..ba56d409d4 --- /dev/null +++ b/source/renderer/app/api/transactions/requests/redeemAda.js @@ -0,0 +1,24 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Transaction } from '../types'; +import { request } from '../../utils/request'; + +export type RedeemAdaParams = { + redemptionCode: string, + mnemonic: ?Array, + spendingPassword?: string, + walletId: string, + accountIndex: number +}; + +export const redeemAda = ( + config: RequestConfig, + redemptionParams: RedeemAdaParams +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/v1/transactions/certificates', + ...config + }, {}, redemptionParams) +); diff --git a/source/renderer/app/api/transactions/requests/redeemPaperVendedAda.js b/source/renderer/app/api/transactions/requests/redeemPaperVendedAda.js new file mode 100644 index 0000000000..a7beca2d5e --- /dev/null +++ b/source/renderer/app/api/transactions/requests/redeemPaperVendedAda.js @@ -0,0 +1,22 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Transaction } from '../types'; +import type { RedeemAdaParams } from './redeemAda'; +import { request } from '../../utils/request'; + +export type RedeemPaperVendedAdaParams = { + ...RedeemAdaParams, + mnemonic: Array, +}; + +export const redeemPaperVendedAda = ( + config: RequestConfig, + redemptionParams: RedeemPaperVendedAdaParams +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/v1/transactions/certificates', + ...config + }, {}, redemptionParams) +); diff --git a/source/renderer/app/api/transactions/types.js b/source/renderer/app/api/transactions/types.js new file mode 100644 index 0000000000..1cdcb97370 --- /dev/null +++ b/source/renderer/app/api/transactions/types.js @@ -0,0 +1,63 @@ +// @flow +import BigNumber from 'bignumber.js'; +import WalletTransaction from '../../domains/WalletTransaction'; +import type { ResponseBase } from '../common/types'; + +export type Transactions = { + data: Array, + ...ResponseBase +}; + +export type Transaction = { + amount: number, + confirmations: number, + creationTime: string, + direction: 'outgoing' | 'incoming', + id: string, + type: 'local' | 'foreign', + inputs: Array, + outputs: Array, + status: { + tag: 'applying' | 'inNewestBlocks' | 'persisted' | 'wontApply' | 'creating', + data: {}, + }, +}; + +export type PaymentDistribution = { + address: string, + amount: number +}; + +export type TxnAssuranceLevel = 'low' | 'medium' | 'high'; + +export type TransactionState = 'pending' | 'failed' | 'ok'; + +export type TransactionFee = { + estimatedAmount: number, + ...ResponseBase +}; + +export type TrasactionAddresses = { from: Array, to: Array }; +export type TransactionType = 'card' | 'expend' | 'income' | 'exchange'; + +// req/res Transaction Types +export type GetTransactionsRequest = { + walletId: string, + searchTerm: string, + skip: number, + limit: number, +}; + +export type TransactionRequest = { + accountIndex: number, + walletId: string, + walletBalance: BigNumber, + address: string, + amount: number, + spendingPassword?: ?string, +}; + +export type GetTransactionsResponse = { + transactions: Array, + total: number, +}; diff --git a/source/renderer/app/api/utils/apiHelpers.js b/source/renderer/app/api/utils/apiHelpers.js new file mode 100644 index 0000000000..23025016ff --- /dev/null +++ b/source/renderer/app/api/utils/apiHelpers.js @@ -0,0 +1,22 @@ +// @flow +import { ApiMethodNotYetImplementedError } from '../common/errors'; + +export const notYetImplemented = async () => ( + new Promise((resolve, reject) => { + reject(new ApiMethodNotYetImplementedError()); + }) +); + +// helper code for testing async APIs +export const testAsync = async (apiMethod: Function) => { + const result = await apiMethod(); + console.log(`testAsync result: ${result}`); + return result; +}; + +// helper code for testing sync APIs +export const testSync = (apiMethod: Function) => { + const result = apiMethod(); + console.log(`testSync result: ${result}`); + return result; +}; diff --git a/source/renderer/app/api/utils/index.js b/source/renderer/app/api/utils/index.js new file mode 100644 index 0000000000..d82ca58d71 --- /dev/null +++ b/source/renderer/app/api/utils/index.js @@ -0,0 +1,21 @@ +// @flow +import moment from 'moment'; +import blakejs from 'blakejs'; + +// time utils +export const unixTimestampToDate = (timestamp: number) => new Date(timestamp * 1000); +export const utcStringToDate = (createDate: string) => moment.utc(createDate).toDate(); + +// passphrase utils +const bytesToB16 = (bytes) => Buffer.from(bytes).toString('hex'); +const blake2b = (data) => blakejs.blake2b(data, null, 32); + +export const encryptPassphrase = (passphrase: ?string) => ( + bytesToB16(blake2b(passphrase)) +); + +// string utils +export const getContentLength = (content: string) => ( + // 'TextEncoder' is used to measure correct length of UTF-8 strings + (new TextEncoder()).encode(content).length +); diff --git a/source/renderer/app/api/localStorage/index.js b/source/renderer/app/api/utils/localStorage.js similarity index 73% rename from source/renderer/app/api/localStorage/index.js rename to source/renderer/app/api/utils/localStorage.js index 829f609edd..64bb2ae63b 100644 --- a/source/renderer/app/api/localStorage/index.js +++ b/source/renderer/app/api/utils/localStorage.js @@ -7,7 +7,8 @@ const networkForLocalStorage = String(environment.NETWORK); const storageKeys = { USER_LOCALE: networkForLocalStorage + '-USER-LOCALE', TERMS_OF_USE_ACCEPTANCE: networkForLocalStorage + '-TERMS-OF-USE-ACCEPTANCE', - THEME: networkForLocalStorage + '-THEME' + THEME: networkForLocalStorage + '-THEME', + DATA_LAYER_MIGRATION_ACCEPTANCE: networkForLocalStorage + '-DATA-LAYER-MIGRATION-ACCEPTANCE', }; /** @@ -95,10 +96,37 @@ export default class LocalStorageApi { } catch (error) {} // eslint-disable-line }); + getDataLayerMigrationAcceptance = () => new Promise((resolve, reject) => { + try { + const accepted = store.get(storageKeys.DATA_LAYER_MIGRATION_ACCEPTANCE); + if (!accepted) return resolve(false); + resolve(accepted); + } catch (error) { + return reject(error); + } + }); + + setDataLayerMigrationAcceptance = () => new Promise((resolve, reject) => { + try { + store.set(storageKeys.DATA_LAYER_MIGRATION_ACCEPTANCE, true); + resolve(); + } catch (error) { + return reject(error); + } + }); + + unsetDataLayerMigrationAcceptance = () => new Promise((resolve) => { + try { + store.delete(storageKeys.DATA_LAYER_MIGRATION_ACCEPTANCE); + resolve(); + } catch (error) {} // eslint-disable-line + }); + async reset() { await this.unsetUserLocale(); await this.unsetTermsOfUseAcceptance(); await this.unsetUserTheme(); + await this.unsetDataLayerMigrationAcceptance(); } } diff --git a/source/renderer/app/api/utils/mnemonics.js b/source/renderer/app/api/utils/mnemonics.js new file mode 100644 index 0000000000..a3bf0dd5c8 --- /dev/null +++ b/source/renderer/app/api/utils/mnemonics.js @@ -0,0 +1,32 @@ +import { + unscramblePaperWalletMnemonic, + scramblePaperWalletMnemonic, + generateMnemonic +} from '../../utils/crypto'; +import { PAPER_WALLET_WRITTEN_WORDS_COUNT } from '../../config/cryptoConfig'; + +type MnemonicsParams = { + passphrase: string, // 9-word mnemonic + scrambledInput: string, // 18-word scrambled mnemonic +}; + +export const unscrambleMnemonics = ( + { passphrase, scrambledInput }: MnemonicsParams +): Array => ( + unscramblePaperWalletMnemonic(passphrase, scrambledInput) +); + +export const scrambleMnemonics = ( + { passphrase, scrambledInput }: MnemonicsParams +): Array => ( + scramblePaperWalletMnemonic(passphrase, scrambledInput) +); + +export const generateAccountMnemonics = (): Array => ( + generateMnemonic().split(' ') +); + +// eslint-disable-next-line +export const generateAdditionalMnemonics = (): Array => ( + generateMnemonic(PAPER_WALLET_WRITTEN_WORDS_COUNT).split(' ') +); diff --git a/source/renderer/app/api/utils/patchAdaApi.js b/source/renderer/app/api/utils/patchAdaApi.js new file mode 100644 index 0000000000..ef5aa4ada6 --- /dev/null +++ b/source/renderer/app/api/utils/patchAdaApi.js @@ -0,0 +1,100 @@ +import BigNumber from 'bignumber.js'; +import { get } from 'lodash'; +import AdaApi from '../api'; +import { getNodeInfo } from '../nodes/requests/getNodeInfo'; +import { GenericApiError } from '../common/errors'; +import { Logger, stringifyData, stringifyError } from '../../../../common/logging'; +import { RedeemAdaError } from '../transactions/errors'; +import type { RedeemAdaParams } from '../transactions/requests/redeemAda'; +import type { RedeemPaperVendedAdaParams } from '../transactions/requests/redeemPaperVendedAda'; +import type { NodeQueryParams } from '../nodes/requests/getNodeInfo'; +import type { NodeInfo, GetNetworkStatusResponse } from '../nodes/types'; + +// ========== LOGGING ========= + +let LOCAL_TIME_DIFFERENCE = 0; +let NEXT_ADA_UPDATE = null; + +export default (api: AdaApi) => { + // Since we cannot test ada redemption in dev mode, just resolve the requests + api.redeemAda = (request: RedeemAdaParams) => new Promise((resolve) => { + try { + Logger.debug('AdaApi::redeemAda (PATCHED) called: ' + stringifyData(request)); + const { redemptionCode } = request; + const isValidRedemptionCode = api.isValidRedemptionKey(redemptionCode); + if (!isValidRedemptionCode) { + Logger.debug('AdaApi::redeemAda (PATCHED) failed: not a valid redemption key!'); + throw new RedeemAdaError(); + } + Logger.debug('AdaApi::redeemAda (PATCHED) success'); + resolve({ amount: new BigNumber(1000) }); + } catch (error) { + Logger.debug('AdaApi::redeemAda (PATCHED) error: ' + stringifyError(error)); + throw new RedeemAdaError(); + } + }); + + api.redeemPaperVendedAda = (request: RedeemPaperVendedAdaParams) => new Promise((resolve) => { + try { + Logger.debug('AdaApi::redeemPaperVendedAda (PATCHED) called: ' + stringifyData(request)); + const { redemptionCode, mnemonics } = request; + const isValidKey = api.isValidPaperVendRedemptionKey(redemptionCode); + const isValidMnemonic = api.isValidRedemptionMnemonic(mnemonics.join(' ')); + if (!isValidKey) Logger.debug('AdaApi::redeemPaperVendedAda (PATCHED) failed: not a valid redemption key!'); + if (!isValidMnemonic) Logger.debug('AdaApi::redeemPaperVendedAda (PATCHED) failed: not a valid mnemonic!'); + if (!isValidKey || !isValidMnemonic) { + throw new RedeemAdaError(); + } + Logger.debug('AdaApi::redeemPaperVendedAda (PATCHED) success'); + resolve({ amount: new BigNumber(1000) }); + } catch (error) { + Logger.debug('AdaApi::redeemPaperVendedAda (PATCHED) error: ' + stringifyError(error)); + throw new RedeemAdaError(); + } + }); + + api.getLocalTimeDifference = async () => ( + Promise.resolve(LOCAL_TIME_DIFFERENCE) + ); + + api.getNetworkStatus = async ( + queryParams?: NodeQueryParams + ): Promise => { + Logger.debug('AdaApi::getNetworkStatus (PATCHED) called'); + try { + const status: NodeInfo = await getNodeInfo(api.config, queryParams); + Logger.debug('AdaApi::getNetworkStatus (PATCHED) success: ' + stringifyData(status)); + + const { + blockchainHeight, + subscriptionStatus, + syncProgress, + localBlockchainHeight, + } = status; + + // extract relevant data before sending to NetworkStatusStore + return { + subscriptionStatus, + syncProgress: syncProgress.quantity, + blockchainHeight: get(blockchainHeight, 'quantity', 0), + localBlockchainHeight: localBlockchainHeight.quantity, + localTimeDifference: LOCAL_TIME_DIFFERENCE, + }; + } catch (error) { + Logger.error('AdaApi::getNetworkStatus (PATCHED) error: ' + stringifyError(error)); + throw new GenericApiError(); + } + }; + + api.setLocalTimeDifference = async (timeDifference) => { + LOCAL_TIME_DIFFERENCE = timeDifference; + }; + + api.nextUpdate = async () => ( + Promise.resolve(NEXT_ADA_UPDATE) + ); + + api.setNextUpdate = async (nextUpdate) => { + NEXT_ADA_UPDATE = nextUpdate; + }; +}; diff --git a/source/renderer/app/api/lib/reportRequest.js b/source/renderer/app/api/utils/reportRequest.js similarity index 71% rename from source/renderer/app/api/lib/reportRequest.js rename to source/renderer/app/api/utils/reportRequest.js index abb91b0b9d..a4bd904d6e 100644 --- a/source/renderer/app/api/lib/reportRequest.js +++ b/source/renderer/app/api/utils/reportRequest.js @@ -1,22 +1,28 @@ +// @flow import http from 'http'; import FormData from 'form-data/lib/form_data'; import fs from 'fs'; +import { extractFileNameFromPath } from '../../../../common/fileName'; export type RequestOptions = { - hostname: string, + hostname: ?string, method: string, - port: number, + path: string, + port: ?string, headers?: { 'Content-Type': string, }, }; export type RequestPayload = { - application: string, - version: string, + product: string, + frontendVersion: string, + backendVersion: string, + network: string, build: string, + installerVersion: string, os: string, - logs: Array, + compressedLogsFile: string, date: string, magic: number, type: { @@ -27,9 +33,9 @@ export type RequestPayload = { } }; -function typedHttpRequest( +function typedHttpRequest( httpOptions: RequestOptions, requestPayload?: RequestPayload -): Promise { +): Promise { return new Promise((resolve, reject) => { const options: RequestOptions = Object.assign({}, httpOptions); const payload: RequestPayload = Object.assign({}, requestPayload); @@ -38,9 +44,10 @@ function typedHttpRequest( formData.append('payload', JSON.stringify(payload)); // prepare file stream (attachment) - if (payload.compressedLog) { - const stream = fs.createReadStream(payload.compressedLog); - formData.append('logs.zip', stream); + if (payload.compressedLogsFile) { + const stream = fs.createReadStream(payload.compressedLogsFile); + const fileName = extractFileNameFromPath(payload.compressedLogsFile); + formData.append(fileName, stream); } options.headers = formData.getHeaders(); diff --git a/source/renderer/app/api/ada/lib/v1/request.js b/source/renderer/app/api/utils/request.js similarity index 69% rename from source/renderer/app/api/ada/lib/v1/request.js rename to source/renderer/app/api/utils/request.js index 50ae27894b..8d7bdb8216 100644 --- a/source/renderer/app/api/ada/lib/v1/request.js +++ b/source/renderer/app/api/utils/request.js @@ -2,15 +2,16 @@ import https from 'https'; import { size, has, get, omit } from 'lodash'; import querystring from 'querystring'; -import { encryptPassphrase } from '../encryptPassphrase'; -import { getContentLength } from '../../../lib/utils'; +import { encryptPassphrase, getContentLength } from './'; export type RequestOptions = { hostname: string, method: string, path: string, port: number, - ca: string, + ca: Uint8Array, + cert: Uint8Array, + key: Uint8Array, headers?: { 'Content-Type': string, 'Content-Length': number, @@ -18,10 +19,14 @@ export type RequestOptions = { }; function typedRequest( - httpOptions: RequestOptions, queryParams?: {}, rawBodyParams?: any + httpOptions: RequestOptions, + queryParams?: {}, + rawBodyParams?: any, + requestOptions?: { returnMeta: boolean }, ): Promise { return new Promise((resolve, reject) => { const options: RequestOptions = Object.assign({}, httpOptions); + const { returnMeta } = Object.assign({}, requestOptions); let hasRequestBody = false; let requestBody = ''; @@ -56,7 +61,8 @@ function typedRequest( requestBody = JSON.stringify(rawBodyParams); options.headers = { 'Content-Length': getContentLength(requestBody), - 'Content-Type': 'application/json', + 'Content-Type': 'application/json; charset=utf-8', + Accept: 'application/json; charset=utf-8', }; } @@ -73,13 +79,35 @@ function typedRequest( // Resolve JSON results and handle backend errors response.on('end', () => { try { + // When deleting a wallet, the API does not return any data in body + // even if it was successful + const { statusCode, statusMessage } = response; + + if (!body && statusCode >= 200 && statusCode <= 206) { + // adds status and data properties so JSON.parse doesn't throw an error + body = `{ + "status": "success", + "data": "statusCode: ${statusCode} -- statusMessage: ${statusMessage}" + }`; + } else if ( + options.path === '/api/internal/next-update' && + statusCode === 404 + ) { + // when nextAdaUpdate receives a 404, it isn't an error + // it means no updates are available + body = `{ + "status": "success", + "data": null + }`; + } + const parsedBody = JSON.parse(body); const status = get(parsedBody, 'status', false); if (status) { if (status === 'success') { - resolve(parsedBody.data); + resolve(returnMeta ? parsedBody : parsedBody.data); } else if (status === 'error' || status === 'fail') { - reject(new Error(parsedBody.message)); + reject(parsedBody); } else { // TODO: find a way to record this case and report to the backend team reject(new Error('Unknown response from backend.')); diff --git a/source/renderer/app/api/ada/lib/request.js b/source/renderer/app/api/utils/requestV0.js similarity index 96% rename from source/renderer/app/api/ada/lib/request.js rename to source/renderer/app/api/utils/requestV0.js index 12faf329fb..928b7524fc 100644 --- a/source/renderer/app/api/ada/lib/request.js +++ b/source/renderer/app/api/utils/requestV0.js @@ -2,15 +2,16 @@ import https from 'https'; import { size, has, get, omit } from 'lodash'; import querystring from 'querystring'; -import { encryptPassphrase } from './encryptPassphrase'; -import { getContentLength } from '../../lib/utils'; +import { encryptPassphrase, getContentLength } from './'; export type RequestOptions = { hostname: string, method: string, path: string, port: number, - ca: string, + ca: Uint8Array, + cert: Uint8Array, + key: Uint8Array, headers?: { 'Content-Type': string, 'Content-Length': number, diff --git a/source/renderer/app/api/wallets/errors.js b/source/renderer/app/api/wallets/errors.js new file mode 100644 index 0000000000..b22f5501fc --- /dev/null +++ b/source/renderer/app/api/wallets/errors.js @@ -0,0 +1,47 @@ +import { defineMessages } from 'react-intl'; +import LocalizableError from '../../i18n/LocalizableError'; + +const messages = defineMessages({ + walletAlreadyRestoredError: { + id: 'api.errors.WalletAlreadyRestoredError', + defaultMessage: '!!!Wallet you are trying to restore already exists.', + description: '"Wallet you are trying to restore already exists." error message.' + }, + walletAlreadyImportedError: { + id: 'api.errors.WalletAlreadyImportedError', + defaultMessage: '!!!Wallet you are trying to import already exists.', + description: '"Wallet you are trying to import already exists." error message.' + }, + walletFileImportError: { + id: 'api.errors.WalletFileImportError', + defaultMessage: '!!!Wallet could not be imported, please make sure you are providing a correct file.', + description: '"Wallet could not be imported, please make sure you are providing a correct file." error message.' + }, +}); + +export class WalletAlreadyRestoredError extends LocalizableError { + constructor() { + super({ + id: messages.walletAlreadyRestoredError.id, + defaultMessage: messages.walletAlreadyRestoredError.defaultMessage, + }); + } +} + +export class WalletAlreadyImportedError extends LocalizableError { + constructor() { + super({ + id: messages.walletAlreadyImportedError.id, + defaultMessage: messages.walletAlreadyImportedError.defaultMessage, + }); + } +} + +export class WalletFileImportError extends LocalizableError { + constructor() { + super({ + id: messages.walletFileImportError.id, + defaultMessage: messages.walletFileImportError.defaultMessage, + }); + } +} diff --git a/source/renderer/app/api/wallets/requests/changeSpendingPassword.js b/source/renderer/app/api/wallets/requests/changeSpendingPassword.js new file mode 100644 index 0000000000..507b6d0422 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/changeSpendingPassword.js @@ -0,0 +1,25 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallet } from '../types'; +import { encryptPassphrase } from '../../utils'; +import { request } from '../../utils/request'; + +export type ChangeSpendingPasswordParams = { + walletId: string, + oldPassword: ?string, + newPassword: ?string, +}; + +export const changeSpendingPassword = ( + config: RequestConfig, + { walletId, oldPassword, newPassword }: ChangeSpendingPasswordParams +): Promise => { + const encryptedOldPassphrase = oldPassword ? encryptPassphrase(oldPassword) : ''; + const encryptedNewPassphrase = newPassword ? encryptPassphrase(newPassword) : ''; + return request({ + hostname: 'localhost', + method: 'PUT', + path: `/api/v1/wallets/${walletId}/password`, + ...config, + }, {}, { old: encryptedOldPassphrase, new: encryptedNewPassphrase }); +}; diff --git a/source/renderer/app/api/wallets/requests/createWallet.js b/source/renderer/app/api/wallets/requests/createWallet.js new file mode 100644 index 0000000000..1104ddcecf --- /dev/null +++ b/source/renderer/app/api/wallets/requests/createWallet.js @@ -0,0 +1,24 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallet, WalletAssuranceLevel } from '../types'; +import { request } from '../../utils/request'; + +export type WalletInitData = { + operation: 'create' | 'restore', + backupPhrase: [string], + assuranceLevel: WalletAssuranceLevel, + name: string, + spendingPassword?: string, +}; + +export const createWallet = ( + config: RequestConfig, + { walletInitData }: { walletInitData: WalletInitData } +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/v1/wallets', + ...config, + }, {}, walletInitData) +); diff --git a/source/renderer/app/api/wallets/requests/deleteWallet.js b/source/renderer/app/api/wallets/requests/deleteWallet.js new file mode 100644 index 0000000000..3c9a53f7fd --- /dev/null +++ b/source/renderer/app/api/wallets/requests/deleteWallet.js @@ -0,0 +1,19 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; + +export type DeleteWalletParams = { + walletId: string, +}; + +export const deleteWallet = ( + config: RequestConfig, + { walletId }: DeleteWalletParams +): Promise<*> => ( + request({ + hostname: 'localhost', + method: 'DELETE', + path: `/api/v1/wallets/${walletId}`, + ...config, + }) +); diff --git a/source/renderer/app/api/wallets/requests/exportWalletAsJSON.js b/source/renderer/app/api/wallets/requests/exportWalletAsJSON.js new file mode 100644 index 0000000000..d5f6ec5bbe --- /dev/null +++ b/source/renderer/app/api/wallets/requests/exportWalletAsJSON.js @@ -0,0 +1,20 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/requestV0'; + +export type ExportWalletAsJSONParams = { + walletId: string, + filePath: string, +}; + +export const exportWalletAsJSON = ( + config: RequestConfig, + { walletId, filePath }: ExportWalletAsJSONParams, +): Promise<[]> => ( + request({ + hostname: 'localhost', + method: 'POST', + path: `/api/backup/export/${walletId}`, + ...config, + }, {}, filePath) +); diff --git a/source/renderer/app/api/wallets/requests/getWallets.js b/source/renderer/app/api/wallets/requests/getWallets.js new file mode 100644 index 0000000000..4031584a80 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/getWallets.js @@ -0,0 +1,19 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallets } from '../types'; +import { request } from '../../utils/request'; +import { MAX_ADA_WALLETS_COUNT } from '../../../config/numbersConfig'; + +export const getWallets = ( + config: RequestConfig +): Promise => ( + request({ + hostname: 'localhost', + method: 'GET', + path: '/api/v1/wallets', + ...config, + }, { + per_page: MAX_ADA_WALLETS_COUNT, // 50 is the max per_page value + sort_by: 'ASC[created_at]', + }) +); diff --git a/source/renderer/app/api/wallets/requests/importWalletAsJSON.js b/source/renderer/app/api/wallets/requests/importWalletAsJSON.js new file mode 100644 index 0000000000..66434b765a --- /dev/null +++ b/source/renderer/app/api/wallets/requests/importWalletAsJSON.js @@ -0,0 +1,16 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallet } from '../types'; +import { request } from '../../utils/requestV0'; + +export const importWalletAsJSON = ( + config: RequestConfig, + filePath: string, +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/backup/import', + ...config, + }, {}, filePath) +); diff --git a/source/renderer/app/api/wallets/requests/importWalletAsKey.js b/source/renderer/app/api/wallets/requests/importWalletAsKey.js new file mode 100644 index 0000000000..bb828114a3 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/importWalletAsKey.js @@ -0,0 +1,21 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallet } from '../types'; +import { request } from '../../utils/request'; + +export type ImportWalletAsKey = { + filePath: string, + spendingPassword?: string, +}; + +export const importWalletAsKey = ( + config: RequestConfig, + walletImportData: ImportWalletAsKey +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/internal/import-wallet', + ...config, + }, {}, walletImportData) +); diff --git a/source/renderer/app/api/wallets/requests/resetWalletState.js b/source/renderer/app/api/wallets/requests/resetWalletState.js new file mode 100644 index 0000000000..49627115a8 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/resetWalletState.js @@ -0,0 +1,14 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; + +export const resetWalletState = ( + config: RequestConfig +): Promise => ( + request({ + hostname: 'localhost', + method: 'DELETE', + path: '/api/internal/reset-wallet-state', + ...config, + }) +); diff --git a/source/renderer/app/api/wallets/requests/restoreWallet.js b/source/renderer/app/api/wallets/requests/restoreWallet.js new file mode 100644 index 0000000000..462e553f02 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/restoreWallet.js @@ -0,0 +1,17 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { WalletInitData } from './createWallet'; +import type { AdaWallet } from '../types'; +import { request } from '../../utils/request'; + +export const restoreWallet = ( + config: RequestConfig, + { walletInitData }: { walletInitData: WalletInitData } +): Promise => ( + request({ + hostname: 'localhost', + method: 'POST', + path: '/api/v1/wallets', + ...config, + }, {}, walletInitData) +); diff --git a/source/renderer/app/api/wallets/requests/updateWallet.js b/source/renderer/app/api/wallets/requests/updateWallet.js new file mode 100644 index 0000000000..34fbd67039 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/updateWallet.js @@ -0,0 +1,22 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallet, WalletAssuranceLevel } from '../types'; +import { request } from '../../utils/request'; + +export type UpdateWalletParams = { + walletId: string, + assuranceLevel: WalletAssuranceLevel, + name: string +}; + +export const updateWallet = ( + config: RequestConfig, + { walletId, assuranceLevel, name }: UpdateWalletParams +): Promise => ( + request({ + hostname: 'localhost', + method: 'PUT', + path: `/api/v1/wallets/${walletId}`, + ...config, + }, {}, { assuranceLevel, name }) +); diff --git a/source/renderer/app/api/wallets/types.js b/source/renderer/app/api/wallets/types.js new file mode 100644 index 0000000000..eaf6c15c86 --- /dev/null +++ b/source/renderer/app/api/wallets/types.js @@ -0,0 +1,92 @@ +// @flow +export type AdaWallet = { + createdAt: string, + syncState: WalletSyncState, + balance: number, + hasSpendingPassword: boolean, + assuranceLevel: WalletAssuranceLevel, + name: string, + id: string, + spendingPasswordLastUpdate: string, +}; + +export type AdaWallets = Array; + +export type WalletAssuranceLevel = 'normal' | 'strict'; + +export type WalletAssuranceMode = { low: number, medium: number }; + +export type SyncStateTag = 'restoring' | 'synced'; + +export type WalletSyncState = { + data: ?{ + estimatedCompletionTime: { + quantity: number, + unit: 'milliseconds', + }, + percentage: { + quantity: number, + unit: 'percent', + }, + throughput: { + quantity: number, + unit: 'blocksPerSecond', + }, + }, + tag: SyncStateTag, +}; + +// req/res Wallet types +export type CreateWalletRequest = { + name: string, + mnemonic: string, + spendingPassword: ?string, +}; + +export type UpdateSpendingPasswordRequest = { + walletId: string, + oldPassword?: string, + newPassword: ?string, +}; + +export type DeleteWalletRequest = { + walletId: string, +}; + +export type RestoreWalletRequest = { + recoveryPhrase: string, + walletName: string, + spendingPassword?: ?string, +}; + +export type UpdateWalletRequest = { + walletId: string, + assuranceLevel: WalletAssuranceLevel, + name: string +}; +export type ImportWalletFromKeyRequest = { + filePath: string, + spendingPassword: ?string, +}; + +export type ImportWalletFromFileRequest = { + filePath: string, + spendingPassword: ?string, + walletName: ?string, +}; + +export type ExportWalletToFileRequest = { + walletId: string, + filePath: string, + password: ?string +}; + +export type GetWalletCertificateRecoveryPhraseRequest = { + passphrase: string, + input: string, +}; + +export type GetWalletRecoveryPhraseFromCertificateRequest = { + passphrase: string, + scrambledInput: string, +}; diff --git a/source/renderer/app/assets/images/ada-symbol-big-dark.inline.svg b/source/renderer/app/assets/images/ada-symbol-big-dark.inline.svg index 7d4a79c69e..2f5aa74c63 100644 --- a/source/renderer/app/assets/images/ada-symbol-big-dark.inline.svg +++ b/source/renderer/app/assets/images/ada-symbol-big-dark.inline.svg @@ -1,14 +1,14 @@ - - - 696A6278-807F-494A-9EA6-81F015340A36 - Created with sketchtool. + + + ada-symbol-big-dark-1.5px + Created with Sketch. - - + + - + diff --git a/source/renderer/app/assets/images/collapse-arrow.inline.svg b/source/renderer/app/assets/images/collapse-arrow.inline.svg new file mode 100644 index 0000000000..f9c038cd4f --- /dev/null +++ b/source/renderer/app/assets/images/collapse-arrow.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/etc-logo.inline.svg b/source/renderer/app/assets/images/etc-logo.inline.svg deleted file mode 100644 index d0ae4f0189..0000000000 --- a/source/renderer/app/assets/images/etc-logo.inline.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/source/renderer/app/assets/images/etc-symbol.inline.svg b/source/renderer/app/assets/images/etc-symbol.inline.svg deleted file mode 100644 index 9ba1108813..0000000000 --- a/source/renderer/app/assets/images/etc-symbol.inline.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/source/renderer/app/assets/images/mantis-logo.inline.svg b/source/renderer/app/assets/images/mantis-logo.inline.svg deleted file mode 100644 index ddb3bbbe08..0000000000 --- a/source/renderer/app/assets/images/mantis-logo.inline.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/source/renderer/app/components/layout/TopBar.js b/source/renderer/app/components/layout/TopBar.js index 75cf5fdd74..44eca045bb 100644 --- a/source/renderer/app/components/layout/TopBar.js +++ b/source/renderer/app/components/layout/TopBar.js @@ -41,7 +41,7 @@ export default class TopBar extends Component {
{activeWallet.name}
{ - // show currency and use long format (e.g. in ETC show all decimal places) + // show currency and use long format formattedWalletAmount(activeWallet.amount, true, true) }
diff --git a/source/renderer/app/components/loading/Loading.js b/source/renderer/app/components/loading/Loading.js index 250581bc9d..e03cb4e1aa 100644 --- a/source/renderer/app/components/loading/Loading.js +++ b/source/renderer/app/components/loading/Loading.js @@ -1,33 +1,65 @@ // @flow import React, { Component } from 'react'; +import { includes } from 'lodash'; import SVGInline from 'react-svg-inline'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; import classNames from 'classnames'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; import SystemTimeErrorOverlay from './SystemTimeErrorOverlay'; import LoadingSpinner from '../widgets/LoadingSpinner'; import daedalusLogo from '../../assets/images/daedalus-logo-loading-grey.inline.svg'; +import { CardanoNodeStates } from '../../../../common/types/cardanoNode.types'; import styles from './Loading.scss'; import type { ReactIntlMessage } from '../../types/i18nTypes'; -import environment from '../../../../common/environment'; +import type { CardanoNodeState } from '../../../../common/types/cardanoNode.types'; import { REPORT_ISSUE_TIME_TRIGGER } from '../../config/timingConfig'; let connectingInterval = null; let syncingInterval = null; const messages = defineMessages({ + starting: { + id: 'loading.screen.startingCardanoMessage', + defaultMessage: '!!!Starting Cardano node', + description: 'Message "Starting Cardano node" on the loading screen.' + }, + stopping: { + id: 'loading.screen.stoppingCardanoMessage', + defaultMessage: '!!!Stopping Cardano node', + description: 'Message "Stopping Cardano node" on the loading screen.' + }, + stopped: { + id: 'loading.screen.stoppedCardanoMessage', + defaultMessage: '!!!Cardano node stopped', + description: 'Message "Cardano node stopped" on the loading screen.' + }, + updating: { + id: 'loading.screen.updatingCardanoMessage', + defaultMessage: '!!!Updating Cardano node', + description: 'Message "Updating Cardano node" on the loading screen.' + }, + updated: { + id: 'loading.screen.updatedCardanoMessage', + defaultMessage: '!!!Cardano node updated', + description: 'Message "Cardano node updated" on the loading screen.' + }, + crashed: { + id: 'loading.screen.crashedCardanoMessage', + defaultMessage: '!!!Cardano node crashed', + description: 'Message "Cardano node crashed" on the loading screen.' + }, + unrecoverable: { + id: 'loading.screen.unrecoverableCardanoMessage', + defaultMessage: '!!!Unable to start Cardano node. Please submit a support request.', + description: 'Message "Unable to start Cardano node. Please submit a support request." on the loading screen.' + }, connecting: { id: 'loading.screen.connectingToNetworkMessage', defaultMessage: '!!!Connecting to network', description: 'Message "Connecting to network" on the loading screen.' }, - waitingForSyncToStart: { - id: 'loading.screen.waitingForSyncToStart', - defaultMessage: '!!!Connected - waiting for block syncing to start', - description: 'Message "Connected - waiting for block syncing to start" on the loading screen.' - }, reconnecting: { id: 'loading.screen.reconnectingToNetworkMessage', defaultMessage: '!!!Network connection lost - reconnecting', @@ -51,7 +83,7 @@ const messages = defineMessages({ reportIssueButtonLabel: { id: 'loading.screen.reportIssue.buttonLabel', defaultMessage: '!!!Report an issue', - description: 'Report an issue button label on the loading .' + description: 'Report an issue button label on the loading.' }, }); @@ -64,90 +96,230 @@ type State = { type Props = { currencyIcon: string, apiIcon: string, - isConnecting: boolean, + cardanoNodeState: ?CardanoNodeState, hasBeenConnected: boolean, - hasBlockSyncingStarted: boolean, - isSyncing: boolean, + isConnected: boolean, + isSynced: boolean, syncPercentage: number, - isLoadingDataForNextScreen: boolean, loadingDataForNextScreenMessage: ReactIntlMessage, hasLoadedCurrentLocale: boolean, hasLoadedCurrentTheme: boolean, - localTimeDifference: number, + localTimeDifference: ?number, isSystemTimeCorrect: boolean, + isCheckingSystemTime: boolean, currentLocale: string, handleReportIssue: Function, onProblemSolutionClick: Function, + onCheckTheTimeAgain: Function, + onContinueWithoutClockSyncCheck: Function, }; @observer export default class Loading extends Component { - constructor() { - super(); - this.state = { - connectingTime: 0, - syncingTime: 0, - syncPercentage: '0', - }; + static contextTypes = { + intl: intlShape.isRequired, + }; + + state = { + connectingTime: 0, + syncingTime: 0, + syncPercentage: '0', + }; + + componentDidMount() { + this._defensivelyStartTimers(this.props.isConnected, this.props.isSynced); } componentWillReceiveProps(nextProps: Props) { - const startConnectingTimer = nextProps.isConnecting && (connectingInterval === null); - const stopConnectingTimer = ( - this.props.isConnecting && - !nextProps.isConnecting && - (connectingInterval !== null) - ); - - if (startConnectingTimer) { - connectingInterval = setInterval(this.connectingTimer, 1000); - } else if (stopConnectingTimer) { - this.resetConnectingTimer(); - } + this._defensivelyStartTimers(nextProps.isConnected, nextProps.isSynced); + } - const startSyncingTimer = nextProps.isSyncing && (syncingInterval === null); - const stopSyncingTimer = ( - this.props.isSyncing && - !nextProps.isSyncing && - (syncingInterval !== null) - ); + componentDidUpdate() { + const canResetSyncing = this._syncingTimerShouldStop(this.props.isSynced); + const canResetConnecting = this._connectingTimerShouldStop(this.props.isConnected); - if (startSyncingTimer) { - syncingInterval = setInterval(this.syncingTimer, 1000); - } else if (stopSyncingTimer) { - this.resetSyncingTimer(); - } + if (canResetSyncing) { this._resetSyncingTime(); } + if (canResetConnecting) { this._resetConnectingTime(); } } componentWillUnmount() { - this.resetConnectingTimer(); - this.resetSyncingTimer(); + this._resetConnectingTime(); + this._resetSyncingTime(); } - static contextTypes = { - intl: intlShape.isRequired, + _connectingTimerShouldStart = (isConnected: boolean): boolean => ( + !isConnected && connectingInterval === null + ); + + _syncingTimerShouldStart = (isConnected: boolean, isSynced: boolean): boolean => ( + isConnected && !isSynced && syncingInterval === null + ); + + _syncingTimerShouldStop = (isSynced: boolean): boolean => ( + isSynced && syncingInterval !== null + ); + + _connectingTimerShouldStop = (isConnected: boolean): boolean => ( + isConnected && connectingInterval !== null + ); + + _defensivelyStartTimers = (isConnected: boolean, isSynced: boolean) => { + const needConnectingTimer = this._connectingTimerShouldStart(isConnected); + const needSyncingTimer = this._syncingTimerShouldStart(isConnected, isSynced); + + if (needConnectingTimer) { + connectingInterval = setInterval(this._incrementConnectingTime, 1000); + } else if (needSyncingTimer) { + syncingInterval = setInterval(this._incrementSyncingTime, 1000); + } + }; + + _resetSyncingTime = () => { + if (syncingInterval !== null) { + clearInterval(syncingInterval); + syncingInterval = null; + } + this.setState({ syncingTime: 0 }); + }; + + _resetConnectingTime = () => { + if (connectingInterval !== null) { + clearInterval(connectingInterval); + connectingInterval = null; + } + this.setState({ connectingTime: 0 }); + }; + + _incrementConnectingTime = () => { + this.setState({ connectingTime: this.state.connectingTime + 1 }); + }; + + _incrementSyncingTime = () => { + const syncPercentage = this.props.syncPercentage.toFixed(2); + if (syncPercentage === this.state.syncPercentage) { + // syncPercentage not increased, increase syncing time + this.setState({ syncingTime: this.state.syncingTime + 1 }); + } else { + // reset syncingTime and set new max percentage + this.setState({ syncingTime: 0, syncPercentage }); + } + }; + + _getConnectingMessage = () => { + const { cardanoNodeState, hasBeenConnected } = this.props; + let connectingMessage; + switch (cardanoNodeState) { + case null: + case CardanoNodeStates.STARTING: + connectingMessage = messages.starting; + break; + case CardanoNodeStates.STOPPING: + case CardanoNodeStates.EXITING: + connectingMessage = messages.stopping; + break; + case CardanoNodeStates.STOPPED: + connectingMessage = messages.stopped; + break; + case CardanoNodeStates.UPDATING: + connectingMessage = messages.updating; + break; + case CardanoNodeStates.UPDATED: + connectingMessage = messages.updated; + break; + case CardanoNodeStates.CRASHED: + case CardanoNodeStates.ERRORED: + connectingMessage = messages.crashed; + break; + case CardanoNodeStates.UNRECOVERABLE: + connectingMessage = messages.unrecoverable; + break; + default: // also covers CardanoNodeStates.RUNNING state + connectingMessage = hasBeenConnected ? messages.reconnecting : messages.connecting; + } + return connectingMessage; + }; + + _renderLoadingScreen = () => { + const { intl } = this.context; + const { + cardanoNodeState, + isConnected, + isSystemTimeCorrect, + isSynced, + localTimeDifference, + currentLocale, + onProblemSolutionClick, + onCheckTheTimeAgain, + onContinueWithoutClockSyncCheck, + isCheckingSystemTime, + syncPercentage, + loadingDataForNextScreenMessage + } = this.props; + + if (!isSystemTimeCorrect) { + return ( + + ); + } else if (!isConnected) { + const finalCardanoNodeStates = [ + CardanoNodeStates.STOPPED, + CardanoNodeStates.UPDATED, + CardanoNodeStates.CRASHED, + CardanoNodeStates.ERRORED, + CardanoNodeStates.UNRECOVERABLE + ]; + const headlineClasses = classNames([ + styles.headline, + includes(finalCardanoNodeStates, cardanoNodeState) ? styles.withoutAnimation : null, + ]); + return ( +
+

+ {intl.formatMessage(this._getConnectingMessage())} +

+
+ ); + } else if (!isSynced) { + return ( +
+

+ {intl.formatMessage(messages.syncing)} {syncPercentage.toFixed(2)}% +

+
+ ); + } + + return ( +
+
+

+ {intl.formatMessage(loadingDataForNextScreenMessage)} +

+ +
+
+ ); }; render() { const { intl } = this.context; const { + cardanoNodeState, currencyIcon, apiIcon, - isConnecting, - isSyncing, - syncPercentage, - isLoadingDataForNextScreen, - loadingDataForNextScreenMessage, - hasBeenConnected, - hasBlockSyncingStarted, + isConnected, + isSynced, hasLoadedCurrentLocale, hasLoadedCurrentTheme, - localTimeDifference, - isSystemTimeCorrect, - currentLocale, handleReportIssue, - onProblemSolutionClick, } = this.props; const { connectingTime, syncingTime } = this.state; @@ -155,37 +327,35 @@ export default class Loading extends Component { const componentStyles = classNames([ styles.component, hasLoadedCurrentTheme ? null : styles['is-loading-theme'], - isConnecting ? styles['is-connecting'] : null, - isSyncing ? styles['is-syncing'] : null, + !isConnected ? styles['is-connecting'] : null, + isConnected && !isSynced ? styles['is-syncing'] : null, ]); const daedalusLogoStyles = classNames([ styles.daedalusLogo, - isConnecting ? styles.connectingLogo : styles.syncingLogo, + !isConnected ? styles.connectingLogo : styles.syncingLogo, ]); const currencyLogoStyles = classNames([ - styles[`${environment.API}-logo`], - isConnecting ? styles.connectingLogo : styles.syncingLogo, + styles['ada-logo'], + !isConnected ? styles.connectingLogo : styles.syncingLogo, ]); const apiLogoStyles = classNames([ - styles[`${environment.API}-apiLogo`], - isConnecting ? styles.connectingLogo : styles.syncingLogo, + styles['ada-apiLogo'], + !isConnected ? styles.connectingLogo : styles.syncingLogo, ]); const daedalusLoadingLogo = daedalusLogo; const currencyLoadingLogo = currencyIcon; const apiLoadingLogo = apiIcon; - let connectingMessage; - if (hasBeenConnected) { - connectingMessage = messages.reconnecting; - } else { - connectingMessage = ( - hasBlockSyncingStarted ? messages.waitingForSyncToStart : messages.connecting - ); - } - - const canReportConnectingIssue = isConnecting && connectingTime >= REPORT_ISSUE_TIME_TRIGGER; - const canReportSyncingIssue = isSyncing && syncingTime >= REPORT_ISSUE_TIME_TRIGGER; + const canReportConnectingIssue = ( + !isConnected && ( + connectingTime >= REPORT_ISSUE_TIME_TRIGGER || + cardanoNodeState === CardanoNodeStates.UNRECOVERABLE + ) + ); + const canReportSyncingIssue = ( + isConnected && !isSynced && syncingTime >= REPORT_ISSUE_TIME_TRIGGER + ); const showReportIssue = canReportConnectingIssue || canReportSyncingIssue; const buttonClasses = classNames([ @@ -198,7 +368,7 @@ export default class Loading extends Component { {showReportIssue && (

- {isConnecting ? + {!isConnected ? intl.formatMessage(messages.reportConnectingIssueText) : intl.formatMessage(messages.reportSyncingIssueText) } @@ -207,7 +377,7 @@ export default class Loading extends Component { className={buttonClasses} label={intl.formatMessage(messages.reportIssueButtonLabel)} onClick={handleReportIssue} - skin={} + skin={ButtonSkin} />

)} @@ -216,71 +386,9 @@ export default class Loading extends Component {
- {hasLoadedCurrentLocale && ( -
- {isConnecting && ( -
-

- {intl.formatMessage(connectingMessage)} -

-
- )} - {isSyncing && ( -
-

- {intl.formatMessage(messages.syncing)} {syncPercentage.toFixed(2)}% -

-
- )} - {!isSyncing && !isConnecting && isLoadingDataForNextScreen && ( -
-

- {intl.formatMessage(loadingDataForNextScreenMessage)} -

- -
- )} - {!isSystemTimeCorrect && ( - - )} -
- )} + {hasLoadedCurrentLocale ? this._renderLoadingScreen() : null} ); } - connectingTimer = () => { - this.setState({ connectingTime: this.state.connectingTime + 1 }); - }; - - resetConnectingTimer = () => { - if (connectingInterval !== null) { - clearInterval(connectingInterval); - connectingInterval = null; - } - this.setState({ connectingTime: 0 }); - }; - - syncingTimer = () => { - const syncPercentage = this.props.syncPercentage.toFixed(2); - if (syncPercentage === this.state.syncPercentage) { - // syncPercentage not increased, increase syncing time - this.setState({ syncingTime: this.state.syncingTime + 1 }); - } else { - // reset syncingTime and set new max percentage - this.setState({ syncingTime: 0, syncPercentage }); - } - }; - - resetSyncingTimer = () => { - if (syncingInterval !== null) { - clearInterval(syncingInterval); - syncingInterval = null; - } - this.setState({ syncingTime: 0 }); - }; } diff --git a/source/renderer/app/components/loading/Loading.scss b/source/renderer/app/components/loading/Loading.scss index 2a61a2b7f3..e66814081f 100644 --- a/source/renderer/app/components/loading/Loading.scss +++ b/source/renderer/app/components/loading/Loading.scss @@ -76,9 +76,7 @@ } .ada-logo, -.ada-apiLogo, -.etc-logo, -.etc-apiLogo { +.ada-apiLogo { & > svg { display: block; width: 48px; @@ -97,10 +95,6 @@ $adaLogoWidth: 43px; margin: 3px ($cardanoLogoWidth - $adaLogoWidth) / 2 0; // Visually align in the middle + Needs to be the same dimension as the apiLogo } -.etc-apiLogo > svg { - margin-top: 7px; // aligning the icon visually in the middle -} - .daedalusLogo { & > svg { display: block; @@ -128,7 +122,9 @@ $adaLogoWidth: 43px; color: var(--theme-connecting-text-color); .headline { overflow: visible; - @include animated-ellipsis($width: 16px); + &:not(.withoutAnimation) { + @include animated-ellipsis($width: 16px); + } } } diff --git a/source/renderer/app/components/loading/SystemTimeErrorOverlay.js b/source/renderer/app/components/loading/SystemTimeErrorOverlay.js index 498473ab34..c370adcc6d 100644 --- a/source/renderer/app/components/loading/SystemTimeErrorOverlay.js +++ b/source/renderer/app/components/loading/SystemTimeErrorOverlay.js @@ -3,10 +3,9 @@ import React, { Component } from 'react'; import humanizeDuration from 'humanize-duration'; import SVGInline from 'react-svg-inline'; import { observer } from 'mobx-react'; -import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; +import { defineMessages, intlShape, FormattedMessage } from 'react-intl'; import attentionIcon from '../../assets/images/attention-big-light.inline.svg'; +import { ALLOWED_TIME_DIFFERENCE } from '../../config/timingConfig'; import styles from './SystemTimeErrorOverlay.scss'; const messages = defineMessages({ @@ -15,22 +14,55 @@ const messages = defineMessages({ defaultMessage: '!!!Unable to sync - incorrect time', description: 'Title of Sync error overlay' }, - overlayText: { - id: 'systemTime.error.overlayText', - defaultMessage: '!!!Attention, Daedalus is unable to sync with the blockchain because the time on your machine is different from the global time. Your time is off by 2 hours 12 minutes 54 seconds.
To synchronize the time and fix this issue, please visit the FAQ section of Daedalus website:', - description: 'Text of Sync error overlay' + overlayTextP1: { + id: 'systemTime.error.overlayTextP1', + defaultMessage: '!!!Attention, Daedalus is unable to sync with the blockchain because the time on your machine is different from the global time. Your time is off by 2 hours 12 minutes 54 seconds.', + description: 'First paragraph of Sync error overlay' + }, + overlayTextP2: { + id: 'systemTime.error.overlayTextP2', + defaultMessage: '!!!To synchronise the time and fix the issue, please read our {supportPortalLink} article.', + description: 'Second paragraph of Sync error overlay' + }, + ntpUnreachableTextP1: { + id: 'systemTime.error.ntpUnreachableTextP1', + defaultMessage: '!!!Attention, Daedalus is unable to check if the clock on your computer is synchronized with global time because NTP (Network Time Protocol) servers are unreachable, possibly due to firewalls on your network.', + description: 'Text of Sync error overlay when NTP service is unreachable' + }, + ntpUnreachableTextP2: { + id: 'systemTime.error.ntpUnreachableTextP2', + defaultMessage: '!!!If your computer clock is off by more than 15 seconds, Daedalus will be unable to connect to the network. If you have this issue, please read our Support Portal article to synchronize the time on your machine.', + description: 'Text of Sync error overlay when NTP service is unreachable' + }, + supportPortalLink: { + id: 'systemTime.error.supportPortalLink', + defaultMessage: '!!!Support Portal', + description: '"Support Portal" link text' + }, + supportPortalLinkUrl: { + id: 'systemTime.error.supportPortalLinkUrl', + defaultMessage: '!!!https://iohk.zendesk.com/hc/en-us/articles/360010230873', + description: 'Link to "Machine clock out of sync with Cardano network" support page' + }, + onCheckTheTimeAgainLink: { + id: 'systemTime.error.onCheckTheTimeAgainLink', + defaultMessage: '!!!Check the time again', + description: 'Text of Check the time again button' + }, + onContinueWithoutClockSyncCheckLink: { + id: 'systemTime.error.onContinueWithoutClockSyncCheckLink', + defaultMessage: '!!!Continue without clock synchronization checks', + description: 'Text of "Continue without clock synchronization checks" button' }, - problemSolutionLink: { - id: 'systemTime.error.problemSolutionLink', - defaultMessage: '!!!daedaluswallet.io/faq', - description: 'Link to Daedalus website FAQ page' - } }); type Props = { - localTimeDifference: number, + localTimeDifference: ?number, currentLocale: string, onProblemSolutionClick: Function, + onCheckTheTimeAgain: Function, + onContinueWithoutClockSyncCheck: Function, + isCheckingSystemTime: boolean, }; @observer @@ -42,8 +74,19 @@ export default class SystemTimeErrorOverlay extends Component { render() { const { intl } = this.context; - const { localTimeDifference, currentLocale } = this.props; - const problemSolutionLink = intl.formatMessage(messages.problemSolutionLink); + const { + localTimeDifference, currentLocale, isCheckingSystemTime, + onCheckTheTimeAgain, onContinueWithoutClockSyncCheck, + } = this.props; + + const supportPortalLink = ( + this.onProblemSolutionClick(event)} + > + {intl.formatMessage(messages.supportPortalLink)} + + ); let humanizedDurationLanguage; switch (currentLocale) { @@ -63,7 +106,10 @@ export default class SystemTimeErrorOverlay extends Component { humanizedDurationLanguage = 'en'; } - const timeOffset = humanizeDuration(localTimeDifference / 1000, { + const isNTPServiceReachable = !!localTimeDifference; + const allowedTimeDifferenceInSeconds = ALLOWED_TIME_DIFFERENCE / 1000000; + + const timeOffset = humanizeDuration((localTimeDifference || 0) / 1000, { round: true, // round seconds to prevent e.g. 1 day 3 hours *11,56 seconds* language: humanizedDurationLanguage, }).replace(/,/g, ''); // replace 1 day, 3 hours, 12 seconds* to clean period without comma @@ -73,19 +119,62 @@ export default class SystemTimeErrorOverlay extends Component { -

+ {isNTPServiceReachable ? ( +
+

+ +

+ +

+ +

+ + +
+ ) : ( +
+

+ +

- +
+ )} ); } - onProblemSolutionClick = (link: string) => { - this.props.onProblemSolutionClick(link); - } + onProblemSolutionClick = (event: MouseEvent) => { + event.preventDefault(); + if (event.target.href) this.props.onProblemSolutionClick(event.target.href); + }; + } diff --git a/source/renderer/app/components/loading/SystemTimeErrorOverlay.scss b/source/renderer/app/components/loading/SystemTimeErrorOverlay.scss index 06f60de301..3d18dd63c2 100644 --- a/source/renderer/app/components/loading/SystemTimeErrorOverlay.scss +++ b/source/renderer/app/components/loading/SystemTimeErrorOverlay.scss @@ -1,3 +1,5 @@ +@import "../../themes/mixins/animations"; + .component { align-items: center; background-color: var(--theme-system-error-overlay-background-color); @@ -45,28 +47,30 @@ p { font-size: 16px; line-height: 1.38; - margin-bottom: 30px; + margin-bottom: 20px; opacity: 0.8; } - button { - background-color: var(--theme-system-error-overlay-button-background-color); - border: solid 1px var(--theme-system-error-overlay-button-border-color); - border-radius: 5px; - color: var(--theme-system-error-overlay-button-text-color); + a { + border-bottom: 1px solid var(--theme-system-error-overlay-text-color); + color: var(--theme-system-error-overlay-text-color); + text-decoration: none; + } + + .checkLink { + border-bottom: 1px solid var(--theme-system-error-overlay-text-color); + color: var(--theme-system-error-overlay-text-color); cursor: pointer; font-size: 14px; - height: 50px; - letter-spacing: 1px; - min-height: 50px; - transition: box-shadow 0.2s cubic-bezier(0.4, 0, 1, 1), background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), color 0.2s cubic-bezier(0.4, 0, 0.2, 1); - width: 360px; + line-height: 1.36; + margin-top: 30px; + opacity: 0.8; - &:not(.disabled) { - &:hover { - background-color: var(--theme-system-error-overlay-button-background-color-hover); - color: var(--theme-system-error-overlay-button-text-color-hover); - } + &:disabled { + text-decoration: none; + cursor: default; + @include animated-ellipsis($width: 16px); } } + } diff --git a/source/renderer/app/components/notifications/AntivirusRestaurationSlowdownNotification.scss b/source/renderer/app/components/notifications/AntivirusRestaurationSlowdownNotification.scss index d941fde661..ad4f91025d 100644 --- a/source/renderer/app/components/notifications/AntivirusRestaurationSlowdownNotification.scss +++ b/source/renderer/app/components/notifications/AntivirusRestaurationSlowdownNotification.scss @@ -1,40 +1,42 @@ @import "../../themes/simple/config"; .component { - width: 100%; + align-items: center; background-color: #b2311d; - box-shadow: 0 -5px 20px 0 rgba(0, 0, 0, 0.25); + bottom: 0; + box-shadow: 0 -5px 20px 0 rgba(0,0,0,0.25); display: flex; position: absolute; - bottom: 0; - align-items: center; - padding: 10px 0; -} + width: 100%; -.text { - color: white; - font-family: $theme-font-light; - text-align: center; - font-size: 12px; - flex-grow: 1; - line-height: 1.2em; + .text { + color: #fff; + font-family: $theme-font-light; + font-size: 12px; + flex-grow: 1; + line-height: 1.2em; + padding: 10px 0; + text-align: center; - a:link, a:visited { - color: white; + a:link, a:visited { + color: #fff; + } } -} -.closeButton { - width: 50px; - &:hover { - cursor: pointer; - background-color: #951604; - } - display: flex; - align-items: center; - justify-content: center; -} + .closeButton { + align-items: center; + display: flex; + justify-content: center; + padding: 25px 0; + width: 50px; + + &:hover { + cursor: pointer; + background-color: #951604; + } -.closeCross { - width: 10px; + .closeCross { + width: 10px; + } + } } diff --git a/source/renderer/app/components/notifications/NodeUpdateNotification.js b/source/renderer/app/components/notifications/NodeUpdateNotification.js index d4046edeae..13fe9f2dbe 100644 --- a/source/renderer/app/components/notifications/NodeUpdateNotification.js +++ b/source/renderer/app/components/notifications/NodeUpdateNotification.js @@ -4,8 +4,8 @@ import SVGInline from 'react-svg-inline'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; import classnames from 'classnames'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; import styles from './NodeUpdateNotification.scss'; import arrowIcon from '../../assets/images/arrow.inline.svg'; @@ -91,14 +91,14 @@ export default class NodeUpdateNotification extends Component { className={styles.acceptButton} label={intl.formatMessage(messages.acceptLabel)} onClick={onAccept} - skin={} + skin={ButtonSkin} /> diff --git a/source/renderer/app/components/sidebar/wallets/SidebarWalletsMenu.scss b/source/renderer/app/components/sidebar/wallets/SidebarWalletsMenu.scss index f1836e8226..e79b5424ba 100644 --- a/source/renderer/app/components/sidebar/wallets/SidebarWalletsMenu.scss +++ b/source/renderer/app/components/sidebar/wallets/SidebarWalletsMenu.scss @@ -4,13 +4,13 @@ .wallets { height: calc(100% - #{$sidebar-button-height}); overflow-x: hidden; - overflow-y: auto; + overflow-y: overlay; } .addWalletButton { @include button; align-items: center; - background-color: var(--theme-sidebar-menu-add-button-background-color-active); + background-color: var(--theme-sidebar-menu-add-button-background-color); bottom: 0; color: var(--theme-sidebar-menu-add-button-text-color); display: flex; @@ -38,4 +38,10 @@ font-family: var(--font-regular); font-size: 18px; } + + &.active { + cursor: default; + background-color: var(--theme-sidebar-menu-add-button-background-color-active); + } } + diff --git a/source/renderer/app/components/staking/StakingChartTooltip.scss b/source/renderer/app/components/staking/StakingChartTooltip.scss index a2a9133065..a31c362535 100644 --- a/source/renderer/app/components/staking/StakingChartTooltip.scss +++ b/source/renderer/app/components/staking/StakingChartTooltip.scss @@ -25,7 +25,3 @@ word-wrap: break-word; margin-bottom: 6px; } - - - - diff --git a/source/renderer/app/components/staking/StakingSwitch.js b/source/renderer/app/components/staking/StakingSwitch.js index 2c0c42901e..a774110089 100644 --- a/source/renderer/app/components/staking/StakingSwitch.js +++ b/source/renderer/app/components/staking/StakingSwitch.js @@ -1,8 +1,9 @@ // @flow import React, { Component } from 'react'; import { observer } from 'mobx-react'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import SimpleSwitchSkin from 'react-polymorph/lib/skins/simple/raw/SwitchSkin'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { SwitchSkin } from 'react-polymorph/lib/skins/simple/SwitchSkin'; +import { IDENTIFIERS } from 'react-polymorph/lib/themes/API'; import styles from './StakingSwitch.scss'; type Props = { @@ -20,9 +21,10 @@ export default class StakingSwitch extends Component {
Staking
} + skin={SwitchSkin} />
); diff --git a/source/renderer/app/components/static/About.js b/source/renderer/app/components/static/About.js index 0bd8f14857..1aa936d367 100644 --- a/source/renderer/app/components/static/About.js +++ b/source/renderer/app/components/static/About.js @@ -2,11 +2,10 @@ import React, { Component } from 'react'; import SVGInline from 'react-svg-inline'; import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; -import { environmentSpecificMessages } from '../../i18n/global-messages'; +import globalMessages from '../../i18n/global-messages'; import styles from './About.scss'; import daedalusIcon from '../../assets/images/daedalus-logo-loading-grey.inline.svg'; import cardanoIcon from '../../assets/images/cardano-logo.inline.svg'; -import mantisIcon from '../../assets/images/mantis-logo.inline.svg'; import environment from '../../../../common/environment'; const messages = defineMessages({ @@ -25,11 +24,6 @@ const messages = defineMessages({ defaultMessage: '!!!Cardano Team:', description: 'About page cardano team headline', }, - aboutContentMantisHeadline: { - id: 'static.about.content.mantis.headline', - defaultMessage: '!!!Mantis Team:', - description: 'About page mantis team headline', - }, aboutContentDaedalusMembers: { id: 'static.about.content.daedalus.members', defaultMessage: '!!!Alexander Rukin, Charles Hoskinson, Clemens Helm, Darko Mijić, Dominik Guzei, Jeremy Wood, Nikola Glumac, Richard Wild, Stefan Malzner, Tomislav Horaček', @@ -40,11 +34,6 @@ const messages = defineMessages({ defaultMessage: '!!!Alexander Sukhoverkhov, Alexander Vieth, Alexandre Rodrigues Baldé, Alfredo Di Napoli, Anastasiya Besman, Andrzej Rybczak, Ante Kegalj, Anton Belyy, Anupam Jain, Arseniy Seroka, Artyom Kazak, Carlos D\'Agostino, Charles Hoskinson, Dan Friedman, Denis Shevchenko, Dmitry Kovanikov, Dmitry Mukhutdinov, Dmitry Nikulin, Domen Kožar, Duncan Coutts, Edsko de Vries, Eileen Fitzgerald, George Agapov, Hiroto Shioi, Ilya Lubimov, Ilya Peresadin, Ivan Gromakovskii, Jake Mitchell, Jane Wild, Jens Krause, Jeremy Wood, Joel Mislov Kunst, Jonn Mostovoy, Konstantin Ivanov, Kristijan Šarić, Lars Brünjes, Laurie Wang, Lionel Miller, Michael Bishop, Mikhail Volkhov, Niklas Hambüchen, Peter Gaži, Philipp Kant, Serge Kosyrev, Vincent Hanquez', description: 'About page cardano team members', }, - aboutContentMantisMembers: { - id: 'static.about.content.mantis.members', - defaultMessage: '!!!Adam Smolarek, Alan McSherry, Alan Verbner, Alejandro Garcia, Charles Hoskinson, Domen Kožar, Eileen Fitzgerald, Hiroto Shioi, Jane Wild, Jan Ziniewicz, Javier Diaz, Jeremy Wood, Laurie Wang, Łukasz Gąsior, Konrad Staniec, Michael Bishop, Mirko Alić, Nicolás Tallar, Radek Tkaczyk, Serge Kosyrev', - description: 'About page mantis team members', - }, aboutCopyright: { id: 'static.about.copyright', defaultMessage: '!!!Input Output HK Limited. Licensed under', @@ -77,19 +66,13 @@ export default class About extends Component { const { onOpenExternalLink } = this.props; const { version, build, os, - API, API_VERSION, isAdaApi, + API_VERSION, } = environment; - const apiName = intl.formatMessage(environmentSpecificMessages[API].apiName); - const apiIcon = isAdaApi() ? cardanoIcon : mantisIcon; - - const apiHeadline = isAdaApi() - ? intl.formatMessage(messages.aboutContentCardanoHeadline) - : intl.formatMessage(messages.aboutContentMantisHeadline); - - const apiMembers = isAdaApi() - ? intl.formatMessage(messages.aboutContentCardanoMembers) - : intl.formatMessage(messages.aboutContentMantisMembers); + const apiName = intl.formatMessage(globalMessages.apiName); + const apiIcon = cardanoIcon; + const apiHeadline = intl.formatMessage(messages.aboutContentCardanoHeadline); + const apiMembers = intl.formatMessage(messages.aboutContentCardanoMembers); return (
diff --git a/source/renderer/app/components/status/NetworkStatus.js b/source/renderer/app/components/status/NetworkStatus.js new file mode 100644 index 0000000000..3a94ac7b76 --- /dev/null +++ b/source/renderer/app/components/status/NetworkStatus.js @@ -0,0 +1,383 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { get, includes, upperFirst } from 'lodash'; +import moment from 'moment'; +import classNames from 'classnames'; +import SVGInline from 'react-svg-inline'; +import { + LineChart, YAxis, XAxis, Line, + CartesianGrid, Tooltip, Legend, + ResponsiveContainer, +} from 'recharts'; +import { + ALLOWED_TIME_DIFFERENCE, + MAX_ALLOWED_STALL_DURATION, +} from '../../config/timingConfig'; +import { UNSYNCED_BLOCKS_ALLOWED } from '../../config/numbersConfig'; +import { getNetworkEkgUrl } from '../../utils/network'; +import closeCross from '../../assets/images/close-cross.inline.svg'; +import LocalizableError from '../../i18n/LocalizableError'; +import { CardanoNodeStates } from '../../../../common/types/cardanoNode.types'; +import environment from '../../../../common/environment'; +import styles from './NetworkStatus.scss'; +import type { CardanoNodeState } from '../../../../common/types/cardanoNode.types'; + +let syncingInterval = null; + +type Props = { + cardanoNodeState: ?CardanoNodeState, + isNodeResponding: boolean, + isNodeSubscribed: boolean, + isNodeSyncing: boolean, + isNodeInSync: boolean, + isNodeTimeCorrect: boolean, + nodeConnectionError: ?LocalizableError, + isConnected: boolean, + isSynced: boolean, + syncPercentage: number, + hasBeenConnected: boolean, + localTimeDifference: ?number, + isSystemTimeIgnored: boolean, + isSystemTimeCorrect: boolean, + isForceCheckingNodeTime: boolean, + mostRecentBlockTimestamp: number, + localBlockHeight: number, + networkBlockHeight: number, + onForceCheckLocalTimeDifference: Function, + onOpenExternalLink: Function, + onRestartNode: Function, + onClose: Function, +}; + +type State = { + data: Array<{ + localBlockHeight: ?number, + networkBlockHeight: ?number, + time: number, + }>, + isNodeRestarting: boolean, +}; + +@observer +export default class NetworkStatus extends Component { + + constructor(props: Props) { + super(props); + let { localBlockHeight, networkBlockHeight } = props; + localBlockHeight = localBlockHeight || null; + networkBlockHeight = networkBlockHeight || null; + this.state = { + data: [ + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 20000).format('HH:mm:ss') }, + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 18000).format('HH:mm:ss') }, + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 16000).format('HH:mm:ss') }, + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 14000).format('HH:mm:ss') }, + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 12000).format('HH:mm:ss') }, + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 10000).format('HH:mm:ss') }, + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 8000).format('HH:mm:ss') }, + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 6000).format('HH:mm:ss') }, + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 4000).format('HH:mm:ss') }, + { localBlockHeight, networkBlockHeight, time: moment(Date.now() - 2000).format('HH:mm:ss') }, + ], + isNodeRestarting: false, + }; + } + + componentWillMount() { + syncingInterval = setInterval(this.syncingTimer, 2000); + } + + componentWillReceiveProps(nextProps: Props) { + const { cardanoNodeState } = this.props; + const { cardanoNodeState: nextCardanoNodeState } = nextProps; + const { isNodeRestarting } = this.state; + const finalCardanoNodeStates = [ + CardanoNodeStates.RUNNING, + CardanoNodeStates.STOPPED, + CardanoNodeStates.UPDATED, + CardanoNodeStates.CRASHED, + CardanoNodeStates.ERRORED, + CardanoNodeStates.UNRECOVERABLE + ]; + if ( + isNodeRestarting && + cardanoNodeState === CardanoNodeStates.STARTING && + includes(finalCardanoNodeStates, nextCardanoNodeState) + ) { + this.setState({ isNodeRestarting: false }); + } + } + + componentWillUnmount() { + this.resetSyncingTimer(); + } + + render() { + const { + cardanoNodeState, isNodeResponding, isNodeSubscribed, isNodeSyncing, isNodeInSync, + isNodeTimeCorrect, isConnected, isSynced, syncPercentage, hasBeenConnected, + localTimeDifference, isSystemTimeCorrect, isForceCheckingNodeTime, + mostRecentBlockTimestamp, localBlockHeight, networkBlockHeight, + onForceCheckLocalTimeDifference, onClose, nodeConnectionError, isSystemTimeIgnored, + onOpenExternalLink, + } = this.props; + const { data, isNodeRestarting } = this.state; + const isNTPServiceReachable = !!localTimeDifference; + const connectionError = get(nodeConnectionError, 'values', '{}'); + const { message, code } = connectionError; + + const localTimeDifferenceClasses = classNames([ + ( + !isNTPServiceReachable || + (localTimeDifference && (localTimeDifference > ALLOWED_TIME_DIFFERENCE)) + ) ? styles.red : styles.green, + ]); + + const remainingUnsyncedBlocks = networkBlockHeight - localBlockHeight; + const remainingUnsyncedBlocksClasses = classNames([ + ( + remainingUnsyncedBlocks < 0 || + remainingUnsyncedBlocks > UNSYNCED_BLOCKS_ALLOWED + ) ? styles.red : styles.green, + ]); + + const timeSinceLastBlock = moment(Date.now()).diff(moment(mostRecentBlockTimestamp)); + const isBlockchainHeightStalling = timeSinceLastBlock > MAX_ALLOWED_STALL_DURATION; + const timeSinceLastBlockClasses = classNames([ + mostRecentBlockTimestamp > 0 && !isBlockchainHeightStalling ? styles.green : styles.red, + ]); + + // Cardano Node EKG server is not enabled for the Mainnet! + const cardanoNodeEkgLink = environment.isMainnet() ? false : getNetworkEkgUrl(); + + return ( +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ DAEDALUS STATUS
+
isConnected: + {isConnected ? 'YES' : 'NO'} +
hasBeenConnected: + {hasBeenConnected ? 'YES' : 'NO'} +
isSynced: + {isSynced ? 'YES' : 'NO'} +
syncPercentage:{syncPercentage.toFixed(2)}%
localBlockHeight:{localBlockHeight}
networkBlockHeight:{networkBlockHeight}
remainingUnsyncedBlocks: + {remainingUnsyncedBlocks >= 0 ? remainingUnsyncedBlocks : '-'} +
timeSinceLastNetworkBlockChange: + {mostRecentBlockTimestamp > 0 ? `${timeSinceLastBlock} ms` : '-'} +
localTimeDifference: + + {isNTPServiceReachable ? ( + `${localTimeDifference || 0} μs` + ) : ( + 'NTP service unreachable' + )} + |  + +
isSystemTimeCorrect: + {isSystemTimeCorrect ? 'YES' : 'NO'} +
isSystemTimeIgnored: + {isSystemTimeIgnored ? 'YES' : 'NO'} +
isForceCheckingNodeTime: + {isForceCheckingNodeTime ? 'YES' : 'NO'} +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {cardanoNodeEkgLink ? ( + + + + + ) : null} + {!isConnected && nodeConnectionError ? ( + + + + ) : null} + +
+ CARDANO NODE STATUS
+
cardanoNodeState: + {upperFirst(cardanoNodeState != null ? cardanoNodeState : 'unknown')} +
isNodeResponding: + {isNodeResponding ? 'YES' : 'NO'} +
isNodeSubscribed: + {isNodeSubscribed ? 'YES' : 'NO'} +
isNodeTimeCorrect: + {isNodeTimeCorrect ? 'YES' : 'NO'} +
isNodeSyncing: + {isNodeSyncing ? 'YES' : 'NO'} +
isNodeInSync: + {isNodeInSync ? 'YES' : 'NO'} +
Cardano Node actions: + +
Cardano Node diagnostics: + +
+ Connection error:
+
+ message: {message || '-'}
+ code: {code || '-'} +
+
+
+ + + + + (Math.max(0, dataMin - 20)), dataMax => (dataMax + 20)]} + orientation="right" + type="number" + width={100} + /> + + + + + + + + + +
+ ); + } + + restartNode = () => { + this.setState({ isNodeRestarting: true }); + this.props.onRestartNode(); + }; + + getClass = (isTrue: boolean) => ( + classNames([ + isTrue ? styles.green : styles.red, + ]) + ); + + syncingTimer = () => { + const { localBlockHeight, networkBlockHeight } = this.props; + const { data } = this.state; + data.push({ + localBlockHeight, + networkBlockHeight, + time: moment().format('HH:mm:ss'), + }); + this.setState({ data: data.slice(-10) }); + }; + + resetSyncingTimer = () => { + if (syncingInterval !== null) { + clearInterval(syncingInterval); + syncingInterval = null; + } + }; + +} diff --git a/source/renderer/app/components/status/NetworkStatus.scss b/source/renderer/app/components/status/NetworkStatus.scss new file mode 100644 index 0000000000..5ad6bf2d83 --- /dev/null +++ b/source/renderer/app/components/status/NetworkStatus.scss @@ -0,0 +1,99 @@ +.component { + align-items: center; + background: #202225; + height: 100%; + display: flex; + flex-direction: column; + font-family: var(--font-medium); + font-size: 14px; + line-height: 1.5; + justify-content: space-between; + padding: 30px; + width: 100%; +} + +.tables { + color: #fff; + display: flex; + justify-content: space-between; + width: 100%; +} + +.table { + width: calc(50% - 15px); + + th { + text-align: left; + } + + td { + &.topPadding { + padding-top: 10px; + } + + & + td { + text-align: right; + } + } + + .red { + color: #cd3100; + } + + .green { + color: #1cac63; + } + + button { + color: #fff; + cursor: pointer; + font-size: 14px; + line-height: 1.5; + position: relative; + + &::after { + border-top: 1px solid #fff; + bottom: 1px; + content: ''; + left: 0; + position: absolute; + right: 1px; + } + + &:disabled { + cursor: default; + + &::after { + display: none; + } + } + } + + hr { + border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.25); + } + + .error { + color: #cd3100; + font-size: 12px; + font-style: italic; + line-height: 1.36; + padding: 2px 0; + } +} + +.closeButton { + cursor: pointer; + position: fixed; + right: 20px; + top: 20px; + + svg { + width: 16px; + height: 16px; + polygon { + fill: #fff; + } + } +} diff --git a/source/renderer/app/components/wallet/WalletAdd.js b/source/renderer/app/components/wallet/WalletAdd.js index b26f952536..89476821e0 100644 --- a/source/renderer/app/components/wallet/WalletAdd.js +++ b/source/renderer/app/components/wallet/WalletAdd.js @@ -97,10 +97,6 @@ export default class WalletAdd extends Component { isRestoreActive, isMaxNumberOfWalletsReached, } = this.props; - const restoreButtonDescription = environment.isAdaApi() - ? messages.restoreWithCertificateDescription - : messages.restoreWithoutCertificateDescription; - const componentClasses = classnames([styles.component, 'WalletAdd']); let activeNotification = null; @@ -136,7 +132,7 @@ export default class WalletAdd extends Component { onClick={onRestore} icon={restoreIcon} label={intl.formatMessage(messages.restoreLabel)} - description={intl.formatMessage(restoreButtonDescription)} + description={intl.formatMessage(messages.restoreWithCertificateDescription)} isDisabled={isMaxNumberOfWalletsReached || isRestoreActive} /> { isDisabled={ isMaxNumberOfWalletsReached || isRestoreActive || - environment.isEtcApi() || - (environment.isAdaApi() && environment.isMainnet()) + environment.isMainnet() || + environment.isTestnet() } />
diff --git a/source/renderer/app/components/wallet/WalletCreateDialog.js b/source/renderer/app/components/wallet/WalletCreateDialog.js index 2463b0d20a..6fdbc1ffac 100644 --- a/source/renderer/app/components/wallet/WalletCreateDialog.js +++ b/source/renderer/app/components/wallet/WalletCreateDialog.js @@ -2,18 +2,20 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import SimpleSwitchSkin from 'react-polymorph/lib/skins/simple/raw/SwitchSkin'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { SwitchSkin } from 'react-polymorph/lib/skins/simple/SwitchSkin'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; +import { IDENTIFIERS } from 'react-polymorph/lib/themes/API'; import { defineMessages, intlShape } from 'react-intl'; import ReactToolboxMobxForm from '../../utils/ReactToolboxMobxForm'; import DialogCloseButton from '../widgets/DialogCloseButton'; import Dialog from '../widgets/Dialog'; -import { isValidWalletName, isValidWalletPassword, isValidRepeatPassword } from '../../utils/validations'; +import { isValidWalletName, isValidSpendingPassword, isValidRepeatPassword } from '../../utils/validations'; import globalMessages from '../../i18n/global-messages'; import styles from './WalletCreateDialog.scss'; import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../config/timingConfig'; +import { submitOnEnter } from '../../utils/form'; const messages = defineMessages({ dialogTitle: { @@ -46,8 +48,8 @@ const messages = defineMessages({ defaultMessage: '!!!Spending password', description: 'Label for the "Activate to create password" switch in the create wallet dialog.', }, - walletPasswordLabel: { - id: 'wallet.create.dialog.walletPasswordLabel', + spendingPasswordLabel: { + id: 'wallet.create.dialog.spendingPasswordLabel', defaultMessage: '!!!Enter password', description: 'Label for the "Wallet password" input in the create wallet dialog.', }, @@ -86,7 +88,7 @@ export default class WalletCreateDialog extends Component { }; componentDidMount() { - setTimeout(() => { this.walletNameInput.focus(); }); + setTimeout(() => { this.walletNameInput.getRef().focus(); }); } walletNameInput: Input; @@ -104,9 +106,9 @@ export default class WalletCreateDialog extends Component { ] )], }, - walletPassword: { + spendingPassword: { type: 'password', - label: this.context.intl.formatMessage(messages.walletPasswordLabel), + label: this.context.intl.formatMessage(messages.spendingPasswordLabel), placeholder: this.context.intl.formatMessage(messages.passwordFieldPlaceholder), value: '', validators: [({ field, form }) => { @@ -116,8 +118,8 @@ export default class WalletCreateDialog extends Component { repeatPasswordField.validate({ showErrors: true }); } return [ - isValidWalletPassword(field.value), - this.context.intl.formatMessage(globalMessages.invalidWalletPassword) + isValidSpendingPassword(field.value), + this.context.intl.formatMessage(globalMessages.invalidSpendingPassword) ]; }], }, @@ -128,10 +130,10 @@ export default class WalletCreateDialog extends Component { value: '', validators: [({ field, form }) => { if (!this.state.createPassword) return [true]; - const walletPassword = form.$('walletPassword').value; - if (walletPassword.length === 0) return [true]; + const spendingPassword = form.$('spendingPassword').value; + if (spendingPassword.length === 0) return [true]; return [ - isValidRepeatPassword(walletPassword, field.value), + isValidRepeatPassword(spendingPassword, field.value), this.context.intl.formatMessage(globalMessages.invalidRepeatPassword) ]; }], @@ -149,10 +151,10 @@ export default class WalletCreateDialog extends Component { onSuccess: (form) => { this.setState({ isSubmitting: true }); const { createPassword } = this.state; - const { walletName, walletPassword } = form.values(); + const { walletName, spendingPassword } = form.values(); const walletData = { name: walletName, - password: createPassword ? walletPassword : null, + spendingPassword: createPassword ? spendingPassword : null, }; this.props.onSubmit(walletData); }, @@ -162,12 +164,6 @@ export default class WalletCreateDialog extends Component { }); }; - checkForEnterKey(event: KeyboardEvent) { - if (event.key === 'Enter') { - this.submit(); - } - } - handlePasswordSwitchToggle = (value: boolean) => { this.setState({ createPassword: value }); }; @@ -181,8 +177,8 @@ export default class WalletCreateDialog extends Component { styles.component, 'WalletCreateDialog', ]); - const walletPasswordFieldsClasses = classnames([ - styles.walletPasswordFields, + const spendingPasswordFieldsClasses = classnames([ + styles.spendingPasswordFields, createPassword ? styles.show : null, ]); @@ -196,7 +192,7 @@ export default class WalletCreateDialog extends Component { ]; const walletNameField = form.$('walletName'); - const walletPasswordField = form.$('walletPassword'); + const spendingPasswordField = form.$('spendingPassword'); const repeatedPasswordField = form.$('repeatPassword'); return ( @@ -211,38 +207,41 @@ export default class WalletCreateDialog extends Component { { this.walletNameInput = input; }} {...walletNameField.bind()} error={walletNameField.error} - skin={} + skin={InputSkin} /> -
-
+
+
{intl.formatMessage(messages.passwordSwitchLabel)}
} + skin={SwitchSkin} />
-
+
} + className="spendingPassword" + onKeyPress={submitOnEnter.bind(this, this.submit)} + {...spendingPasswordField.bind()} + error={spendingPasswordField.error} + skin={InputSkin} /> } + skin={InputSkin} />

{intl.formatMessage(globalMessages.passwordInstructions)} diff --git a/source/renderer/app/components/wallet/WalletCreateDialog.scss b/source/renderer/app/components/wallet/WalletCreateDialog.scss index 972328c0fc..e82743350b 100644 --- a/source/renderer/app/components/wallet/WalletCreateDialog.scss +++ b/source/renderer/app/components/wallet/WalletCreateDialog.scss @@ -1,8 +1,8 @@ @import '../../themes/mixins/loading-spinner'; @import '../../themes/mixins/place-form-field-error-below-input'; -.walletPassword { - .walletPasswordSwitch { +.spendingPassword { + .spendingPasswordSwitch { border-top: 1px solid var(--theme-separation-border-color); margin-top: 30px; padding-top: 20px; @@ -22,7 +22,7 @@ } } - .walletPasswordFields { + .spendingPasswordFields { display: flex; flex-wrap: wrap; justify-content: space-between; @@ -55,5 +55,6 @@ } .isSubmitting { + box-shadow: none !important; @include loading-spinner("../../assets/images/spinner-light.svg"); } diff --git a/source/renderer/app/components/wallet/WalletReceive.js b/source/renderer/app/components/wallet/WalletReceive.js index 25833065b4..b4eb6b5830 100644 --- a/source/renderer/app/components/wallet/WalletReceive.js +++ b/source/renderer/app/components/wallet/WalletReceive.js @@ -6,15 +6,16 @@ import SVGInline from 'react-svg-inline'; import classnames from 'classnames'; import CopyToClipboard from 'react-copy-to-clipboard'; import QRCode from 'qrcode.react'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import ReactToolboxMobxForm from '../../utils/ReactToolboxMobxForm'; +import { submitOnEnter } from '../../utils/form'; import BorderedBox from '../widgets/BorderedBox'; import TinySwitch from '../widgets/forms/TinySwitch'; import iconCopy from '../../assets/images/clipboard-ic.inline.svg'; -import WalletAddress from '../../domains/WalletAddress'; +import type { Addresses } from '../../api/addresses/types'; import globalMessages from '../../i18n/global-messages'; import LocalizableError from '../../i18n/LocalizableError'; import styles from './WalletReceive.scss'; @@ -62,7 +63,7 @@ messages.fieldIsRequired = globalMessages.fieldIsRequired; type Props = { walletAddress: string, isWalletAddressUsed: boolean, - walletAddresses: Array, + walletAddresses: Addresses, onGenerateAddress: Function, onCopyAddress: Function, isSidebarExpanded: boolean, @@ -86,6 +87,8 @@ export default class WalletReceive extends Component { showUsed: true, }; + passwordField: Input; + toggleUsedAddresses = () => { this.setState({ showUsed: !this.state.showUsed }); }; @@ -113,7 +116,7 @@ export default class WalletReceive extends Component { }, }); - submit() { + submit = () => { this.form.submit({ onSuccess: (form) => { const { walletHasPassword } = this.props; @@ -124,6 +127,8 @@ export default class WalletReceive extends Component { }, onError: () => {} }); + + this.passwordField && this.passwordField.getRef().focus(); } render() { @@ -161,16 +166,18 @@ export default class WalletReceive extends Component { { this.passwordField = input; }} error={passwordField.error} - skin={} + skin={InputSkin} + onKeyPress={submitOnEnter.bind(this, this.submit)} /> }

); @@ -236,13 +243,13 @@ export default class WalletReceive extends Component { {walletAddresses.map((address, index) => { - const isAddressVisible = !address.isUsed || showUsed; + const isAddressVisible = !address.used || showUsed; if (!isAddressVisible) return null; const addressClasses = classnames([ 'generatedAddress-' + (index + 1), styles.walletAddress, - address.isUsed ? styles.usedWalletAddress : null, + address.used ? styles.usedWalletAddress : null, ]); return (
diff --git a/source/renderer/app/components/wallet/WalletReceive.scss b/source/renderer/app/components/wallet/WalletReceive.scss index 2265066362..9ef32dccc0 100644 --- a/source/renderer/app/components/wallet/WalletReceive.scss +++ b/source/renderer/app/components/wallet/WalletReceive.scss @@ -47,9 +47,9 @@ position: relative; .qrCode { - align-items: center; + align-items: flex-start; display: flex; - margin-right: 30px; + margin-right: 20px; canvas { border: 4px solid var(--theme-receive-qr-code-background-color); diff --git a/source/renderer/app/components/wallet/WalletRestoreDialog.js b/source/renderer/app/components/wallet/WalletRestoreDialog.js index b32c46f2dd..42cb42cba0 100644 --- a/source/renderer/app/components/wallet/WalletRestoreDialog.js +++ b/source/renderer/app/components/wallet/WalletRestoreDialog.js @@ -3,22 +3,24 @@ import React, { Component } from 'react'; import { join } from 'lodash'; import { observer } from 'mobx-react'; import classnames from 'classnames'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import Autocomplete from 'react-polymorph/lib/components/Autocomplete'; -import SimpleAutocompleteSkin from 'react-polymorph/lib/skins/simple/raw/AutocompleteSkin'; -import SimpleSwitchSkin from 'react-polymorph/lib/skins/simple/raw/SwitchSkin'; +import { Autocomplete } from 'react-polymorph/lib/components/Autocomplete'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { AutocompleteSkin } from 'react-polymorph/lib/skins/simple/AutocompleteSkin'; +import { SwitchSkin } from 'react-polymorph/lib/skins/simple/SwitchSkin'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; +import { IDENTIFIERS } from 'react-polymorph/lib/themes/API'; import { defineMessages, intlShape } from 'react-intl'; import ReactToolboxMobxForm from '../../utils/ReactToolboxMobxForm'; import DialogCloseButton from '../widgets/DialogCloseButton'; import Dialog from '../widgets/Dialog'; -import { isValidWalletName, isValidWalletPassword, isValidRepeatPassword } from '../../utils/validations'; +import { isValidWalletName, isValidSpendingPassword, isValidRepeatPassword } from '../../utils/validations'; import globalMessages from '../../i18n/global-messages'; import LocalizableError from '../../i18n/LocalizableError'; import { PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT, WALLET_RECOVERY_PHRASE_WORD_COUNT } from '../../config/cryptoConfig'; import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../config/timingConfig'; import styles from './WalletRestoreDialog.scss'; +import { submitOnEnter } from '../../utils/form'; const RESTORE_TYPES = { REGULAR: 'regular', @@ -76,8 +78,8 @@ const messages = defineMessages({ defaultMessage: '!!!Spending password', description: 'Label for the "Spending password" switch in the wallet restore dialog.', }, - walletPasswordLabel: { - id: 'wallet.restore.dialog.walletPasswordLabel', + spendingPasswordLabel: { + id: 'wallet.restore.dialog.spendingPasswordLabel', defaultMessage: '!!!Enter password', description: 'Label for the "Wallet password" input in the wallet restore dialog.', }, @@ -122,7 +124,6 @@ type Props = { mnemonicValidator: Function, error?: ?LocalizableError, suggestedMnemonics: Array, - showCertificateRestore: boolean, onChoiceChange: ?Function, }; @@ -183,9 +184,9 @@ export default class WalletRestoreDialog extends Component { ]; }, }, - walletPassword: { + spendingPassword: { type: 'password', - label: this.context.intl.formatMessage(messages.walletPasswordLabel), + label: this.context.intl.formatMessage(messages.spendingPasswordLabel), placeholder: this.context.intl.formatMessage(messages.passwordFieldPlaceholder), value: '', validators: [({ field, form }) => { @@ -195,8 +196,8 @@ export default class WalletRestoreDialog extends Component { repeatPasswordField.validate({ showErrors: true }); } return [ - isValidWalletPassword(field.value), - this.context.intl.formatMessage(globalMessages.invalidWalletPassword) + isValidSpendingPassword(field.value), + this.context.intl.formatMessage(globalMessages.invalidSpendingPassword) ]; }], }, @@ -207,10 +208,10 @@ export default class WalletRestoreDialog extends Component { value: '', validators: [({ field, form }) => { if (!this.state.createPassword) return [true]; - const walletPassword = form.$('walletPassword').value; - if (walletPassword.length === 0) return [true]; + const spendingPassword = form.$('spendingPassword').value; + if (spendingPassword.length === 0) return [true]; return [ - isValidRepeatPassword(walletPassword, field.value), + isValidRepeatPassword(spendingPassword, field.value), this.context.intl.formatMessage(globalMessages.invalidRepeatPassword) ]; }], @@ -231,22 +232,20 @@ export default class WalletRestoreDialog extends Component { this.form.submit({ onSuccess: (form) => { const { createPassword } = this.state; - const { showCertificateRestore, onSubmit } = this.props; + const { onSubmit } = this.props; const { recoveryPhrase, walletName, - walletPassword, + spendingPassword, } = form.values(); const walletData: Object = { recoveryPhrase: join(recoveryPhrase, ' '), walletName, - walletPassword: createPassword ? walletPassword : null, + spendingPassword: createPassword ? spendingPassword : null, }; - if (showCertificateRestore) { - walletData.type = this.state.activeChoice; - } + walletData.type = this.state.activeChoice; onSubmit(walletData); }, @@ -262,7 +261,7 @@ export default class WalletRestoreDialog extends Component { form.showErrors(false); // Autocomplete has to be reset manually - this.recoveryPhraseAutocomplete.clear(); + this.recoveryPhraseAutocomplete.getRef().clear(); }; render() { @@ -270,7 +269,6 @@ export default class WalletRestoreDialog extends Component { const { form } = this; const { suggestedMnemonics, - showCertificateRestore, isSubmitting, error, onCancel, @@ -279,7 +277,7 @@ export default class WalletRestoreDialog extends Component { const dialogClasses = classnames([ styles.component, - showCertificateRestore ? styles.dialogWithCertificateRestore : null, + styles.dialogWithCertificateRestore, 'WalletRestoreDialog', ]); @@ -288,14 +286,14 @@ export default class WalletRestoreDialog extends Component { styles.walletName, ]); - const walletPasswordFieldsClasses = classnames([ - styles.walletPasswordFields, + const spendingPasswordFieldsClasses = classnames([ + styles.spendingPasswordFields, createPassword ? styles.show : null, ]); const walletNameField = form.$('walletName'); const recoveryPhraseField = form.$('recoveryPhrase'); - const walletPasswordField = form.$('walletPassword'); + const spendingPasswordField = form.$('spendingPassword'); const repeatedPasswordField = form.$('repeatPassword'); const actions = [ @@ -308,7 +306,6 @@ export default class WalletRestoreDialog extends Component { }, ]; - const regularTabClasses = classnames([ 'regularTab', this.isRegular() ? styles.activeButton : '', @@ -328,28 +325,27 @@ export default class WalletRestoreDialog extends Component { onClose={onCancel} closeButton={} > - {showCertificateRestore && -
- - -
- } +
+ + +
} + skin={InputSkin} /> { error={recoveryPhraseField.error} maxVisibleOptions={5} noResultsMessage={intl.formatMessage(messages.recoveryPhraseNoResults)} - skin={} + skin={AutocompleteSkin} /> -
-
+
+
{intl.formatMessage(messages.passwordSwitchLabel)}
} + skin={SwitchSkin} />
-
+
} + className="spendingPassword" + onKeyPress={submitOnEnter.bind(this, this.submit)} + {...spendingPasswordField.bind()} + error={spendingPasswordField.error} + skin={InputSkin} /> } + skin={InputSkin} />

{intl.formatMessage(globalMessages.passwordInstructions)} diff --git a/source/renderer/app/components/wallet/WalletRestoreDialog.scss b/source/renderer/app/components/wallet/WalletRestoreDialog.scss index 1e639e8cc5..6d7f06ee6b 100644 --- a/source/renderer/app/components/wallet/WalletRestoreDialog.scss +++ b/source/renderer/app/components/wallet/WalletRestoreDialog.scss @@ -2,7 +2,6 @@ @import '../../themes/mixins/error-message'; @import '../../themes/mixins/place-form-field-error-below-input'; - .component { &.dialogWithCertificateRestore { :global { @@ -16,8 +15,8 @@ margin-bottom: 20px; } - .walletPassword { - .walletPasswordSwitch { + .spendingPassword { + .spendingPasswordSwitch { border-top: 1px solid var(--theme-separation-border-color); margin-top: 30px; padding-top: 20px; @@ -37,7 +36,7 @@ } } - .walletPasswordFields { + .spendingPasswordFields { display: flex; flex-wrap: wrap; justify-content: space-between; @@ -101,6 +100,7 @@ } .isSubmitting { + box-shadow: none !important; @include loading-spinner("../../assets/images/spinner-light.svg"); } } diff --git a/source/renderer/app/components/wallet/WalletSendConfirmationDialog.js b/source/renderer/app/components/wallet/WalletSendConfirmationDialog.js index 3d2bf81583..40ed68daff 100644 --- a/source/renderer/app/components/wallet/WalletSendConfirmationDialog.js +++ b/source/renderer/app/components/wallet/WalletSendConfirmationDialog.js @@ -2,8 +2,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import { defineMessages, intlShape } from 'react-intl'; import ReactToolboxMobxForm from '../../utils/ReactToolboxMobxForm'; import Dialog from '../widgets/Dialog'; @@ -12,6 +12,7 @@ import globalMessages from '../../i18n/global-messages'; import LocalizableError from '../../i18n/LocalizableError'; import styles from './WalletSendConfirmationDialog.scss'; import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../config/timingConfig'; +import { submitOnEnter } from '../../utils/form'; export const messages = defineMessages({ dialogTitle: { @@ -19,8 +20,8 @@ export const messages = defineMessages({ defaultMessage: '!!!Confirm transaction', description: 'Title for the "Confirm transaction" dialog.' }, - walletPasswordLabel: { - id: 'wallet.send.confirmationDialog.walletPasswordLabel', + spendingPasswordLabel: { + id: 'wallet.send.confirmationDialog.spendingPasswordLabel', defaultMessage: '!!!Spending password', description: 'Label for the "Spending password" input in the wallet send confirmation dialog.', }, @@ -44,8 +45,8 @@ export const messages = defineMessages({ defaultMessage: '!!!Total', description: 'Label for the "Total" in the wallet send confirmation dialog.', }, - walletPasswordFieldPlaceholder: { - id: 'wallet.send.confirmationDialog.walletPasswordFieldPlaceholder', + spendingPasswordFieldPlaceholder: { + id: 'wallet.send.confirmationDialog.spendingPasswordFieldPlaceholder', defaultMessage: '!!!Type your spending password', description: 'Placeholder for the "Spending password" inputs in the wallet send confirmation dialog.', }, @@ -64,11 +65,11 @@ export const messages = defineMessages({ messages.fieldIsRequired = globalMessages.fieldIsRequired; type Props = { - isWalletPasswordSet: boolean, + isSpendingPasswordSet: boolean, amount: string, receiver: string, - totalAmount: string, - transactionFee: string, + totalAmount: ?string, + transactionFee: ?string, onSubmit: Function, amountToNaturalUnits: (amountWithFractions: string) => string, onCancel: Function, @@ -86,13 +87,13 @@ export default class WalletSendConfirmationDialog extends Component { form = new ReactToolboxMobxForm({ fields: { - walletPassword: { + spendingPassword: { type: 'password', - label: this.context.intl.formatMessage(messages.walletPasswordLabel), - placeholder: this.context.intl.formatMessage(messages.walletPasswordFieldPlaceholder), + label: this.context.intl.formatMessage(messages.spendingPasswordLabel), + placeholder: this.context.intl.formatMessage(messages.spendingPasswordFieldPlaceholder), value: '', validators: [({ field }) => { - if (this.props.isWalletPasswordSet && field.value === '') { + if (this.props.isSpendingPasswordSet && field.value === '') { return [false, this.context.intl.formatMessage(messages.fieldIsRequired)]; } return [true]; @@ -106,29 +107,31 @@ export default class WalletSendConfirmationDialog extends Component { }, }); - submit() { + submit = () => { this.form.submit({ onSuccess: (form) => { - const { isWalletPasswordSet, receiver, amount, amountToNaturalUnits } = this.props; - const { walletPassword } = form.values(); + const { isSpendingPasswordSet, receiver, amount, amountToNaturalUnits } = this.props; + const { spendingPassword } = form.values(); const transactionData = { receiver, amount: amountToNaturalUnits(amount), - password: isWalletPasswordSet ? walletPassword : null, + password: isSpendingPasswordSet ? spendingPassword : null, }; this.props.onSubmit(transactionData); }, onError: () => {} }); - } + }; + + submitOnEnter = (event: {}) => this.form.$('spendingPassword').isValid && submitOnEnter(this.submit, event); render() { const { form } = this; const { intl } = this.context; - const walletPasswordField = form.$('walletPassword'); + const spendingPasswordField = form.$('spendingPassword'); const { onCancel, - isWalletPasswordSet, + isSpendingPasswordSet, amount, receiver, totalAmount, @@ -150,10 +153,10 @@ export default class WalletSendConfirmationDialog extends Component { }, { label: intl.formatMessage(messages.sendButtonLabel), - onClick: this.submit.bind(this), + onClick: this.submit, primary: true, className: confirmButtonClasses, - disabled: !walletPasswordField.isValid, + disabled: !spendingPasswordField.isValid, }, ]; @@ -162,11 +165,12 @@ export default class WalletSendConfirmationDialog extends Component { title={intl.formatMessage(messages.dialogTitle)} actions={actions} closeOnOverlayClick + primaryButtonAutoFocus onClose={!isSubmitting ? onCancel : null} className={styles.dialog} closeButton={} > -

+
{intl.formatMessage(messages.addressToLabel)} @@ -197,13 +201,15 @@ export default class WalletSendConfirmationDialog extends Component {
- {isWalletPasswordSet ? ( + {isSpendingPasswordSet ? ( } + className={styles.spendingPassword} + {...spendingPasswordField.bind()} + error={spendingPasswordField.error} + skin={InputSkin} + onKeyPress={this.submitOnEnter} + autoFocus /> ) : null}
diff --git a/source/renderer/app/components/wallet/WalletSendConfirmationDialog.scss b/source/renderer/app/components/wallet/WalletSendConfirmationDialog.scss index ecde30a126..363d458c77 100644 --- a/source/renderer/app/components/wallet/WalletSendConfirmationDialog.scss +++ b/source/renderer/app/components/wallet/WalletSendConfirmationDialog.scss @@ -77,7 +77,7 @@ word-break: break-word; } - .walletPassword { + .spendingPassword { margin-top: 20px; } @@ -89,5 +89,6 @@ } .submitButtonSpinning { + box-shadow: none !important; @include loading-spinner("../../assets/images/spinner-light.svg"); } diff --git a/source/renderer/app/components/wallet/WalletSendForm.js b/source/renderer/app/components/wallet/WalletSendForm.js index ae70d852cc..8d56edffc1 100755 --- a/source/renderer/app/components/wallet/WalletSendForm.js +++ b/source/renderer/app/components/wallet/WalletSendForm.js @@ -2,14 +2,15 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; -import Input from 'react-polymorph/lib/components/Input'; -import NumericInput from 'react-polymorph/lib/components/NumericInput'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { NumericInput } from 'react-polymorph/lib/components/NumericInput'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import { defineMessages, intlShape } from 'react-intl'; import BigNumber from 'bignumber.js'; import ReactToolboxMobxForm from '../../utils/ReactToolboxMobxForm'; +import { submitOnEnter } from '../../utils/form'; import AmountInputSkin from './skins/AmountInputSkin'; import BorderedBox from '../widgets/BorderedBox'; import LoadingSpinner from '../widgets/LoadingSpinner'; @@ -17,7 +18,7 @@ import styles from './WalletSendForm.scss'; import globalMessages from '../../i18n/global-messages'; import WalletSendConfirmationDialog from './WalletSendConfirmationDialog'; import WalletSendConfirmationDialogContainer from '../../containers/wallet/dialogs/WalletSendConfirmationDialogContainer'; -import { formattedAmountToBigNumber, formattedAmountToNaturalUnits } from '../../utils/formatters'; +import { formattedAmountToBigNumber, formattedAmountToNaturalUnits, formattedAmountToLovelace } from '../../utils/formatters'; import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../config/timingConfig'; export const messages = defineMessages({ @@ -81,11 +82,6 @@ export const messages = defineMessages({ defaultMessage: '!!!Please enter a title with at least 3 characters.', description: 'Error message shown when invalid transaction title was entered.', }, - transactionFeeError: { - id: 'wallet.send.form.transactionFeeError', - defaultMessage: '!!!Not enough Ada for fees. Try sending a smaller amount.', - description: '"Not enough Ada for fees. Try sending a smaller amount." error message', - }, syncingTransactionsMessage: { id: 'wallet.send.form.syncingTransactionsMessage', defaultMessage: '!!!This wallet is currently being synced with the blockchain. While synchronisation is in progress transacting is not possible and transaction history is not complete.', @@ -100,7 +96,7 @@ type Props = { currencyMaxIntegerDigits?: number, currencyMaxFractionalDigits: number, validateAmount: (amountInNaturalUnits: string) => Promise, - calculateTransactionFee: (receiver: string, amount: string) => Promise, + calculateTransactionFee: (address: string, amount: number) => Promise, addressValidator: Function, openDialogAction: Function, isDialogOpen: Function, @@ -146,6 +142,17 @@ export default class WalletSendForm extends Component { this._isMounted = false; } + handleOnSubmit = () => { + if (this.isDisabled()) { + return false; + } + this.props.openDialogAction({ + dialog: WalletSendConfirmationDialog, + }); + }; + + isDisabled = () => this._isCalculatingFee || !this.state.isTransactionFeeCalculated; + // FORM VALIDATION form = new ReactToolboxMobxForm({ fields: { @@ -159,16 +166,16 @@ export default class WalletSendForm extends Component { this._resetTransactionFee(); return [false, this.context.intl.formatMessage(messages.fieldIsRequired)]; } - const isValid = await this.props.addressValidator(value); + const isValidAddress = await this.props.addressValidator(value); const amountField = form.$('amount'); const amountValue = amountField.value; const isAmountValid = amountField.isValid; - if (isValid && isAmountValid) { + if (isValidAddress && isAmountValid) { await this._calculateTransactionFee(value, amountValue); } else { this._resetTransactionFee(); } - return [isValid, this.context.intl.formatMessage(messages.invalidAddress)]; + return [isValidAddress, this.context.intl.formatMessage(messages.invalidAddress)]; }], }, amount: { @@ -209,7 +216,7 @@ export default class WalletSendForm extends Component { const { intl } = this.context; const { currencyUnit, currencyMaxIntegerDigits, currencyMaxFractionalDigits, - openDialogAction, isDialogOpen, isRestoreActive, + isDialogOpen, isRestoreActive, } = this.props; const { isTransactionFeeCalculated, transactionFee, transactionFeeError } = this.state; const amountField = form.$('amount'); @@ -246,13 +253,15 @@ export default class WalletSendForm extends Component {
{ this._isCalculatingFee = true; receiverField.onChange(value || ''); }} - skin={} + skin={InputSkin} + onKeyPress={submitOnEnter.bind(this, this.handleOnSubmit)} />
@@ -272,18 +281,17 @@ export default class WalletSendForm extends Component { currency={currencyUnit} fees={fees} total={total} - skin={} + skin={AmountInputSkin} + onKeyPress={submitOnEnter.bind(this, this.handleOnSubmit)} />
@@ -314,10 +322,10 @@ export default class WalletSendForm extends Component { } } - async _calculateTransactionFee(receiver: string, amountValue: string) { - const amount = formattedAmountToNaturalUnits(amountValue); + async _calculateTransactionFee(address: string, amountValue: string) { + const amount = formattedAmountToLovelace(amountValue); try { - const fee = await this.props.calculateTransactionFee(receiver, amount); + const fee = await this.props.calculateTransactionFee(address, amount); if (this._isMounted) { this._isCalculatingFee = false; this.setState({ @@ -332,7 +340,7 @@ export default class WalletSendForm extends Component { this.setState({ isTransactionFeeCalculated: false, transactionFee: new BigNumber(0), - transactionFeeError: this.context.intl.formatMessage(error) + transactionFeeError: this.context.intl.formatMessage(error), }); } } diff --git a/source/renderer/app/components/wallet/WalletSettings.js b/source/renderer/app/components/wallet/WalletSettings.js index 5461fed796..0cd2ff8ef7 100644 --- a/source/renderer/app/components/wallet/WalletSettings.js +++ b/source/renderer/app/components/wallet/WalletSettings.js @@ -1,5 +1,6 @@ // @flow import React, { Component } from 'react'; +import type { Node } from 'react'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; import moment from 'moment'; @@ -11,12 +12,9 @@ import InlineEditingDropdown from '../widgets/forms/InlineEditingDropdown'; import ReadOnlyInput from '../widgets/forms/ReadOnlyInput'; import DeleteWalletButton from './settings/DeleteWalletButton'; import DeleteWalletConfirmationDialog from './settings/DeleteWalletConfirmationDialog'; -import DeleteWalletDialogContainer from '../../containers/wallet/dialogs/DeleteWalletDialogContainer'; -import WalletExportDialog from './settings/export-to-file/WalletExportToFileDialog'; -import WalletExportToFileDialogContainer from '../../containers/wallet/settings/WalletExportToFileDialogContainer'; +import ExportWalletToFileDialog from './settings/ExportWalletToFileDialog'; import type { ReactIntlMessage } from '../../types/i18nTypes'; -import ChangeWalletPasswordDialog from './settings/ChangeWalletPasswordDialog'; -import ChangeWalletPasswordDialogContainer from '../../containers/wallet/dialogs/ChangeWalletPasswordDialogContainer'; +import ChangeSpendingPasswordDialog from './settings/ChangeSpendingPasswordDialog'; import globalMessages from '../../i18n/global-messages'; import styles from './WalletSettings.scss'; @@ -57,8 +55,8 @@ type Props = { assuranceLevels: Array<{ value: string, label: ReactIntlMessage }>, walletName: string, walletAssurance: string, - isWalletPasswordSet: boolean, - walletPasswordUpdateDate: ?Date, + isSpendingPasswordSet: boolean, + spendingPasswordUpdateDate: ?Date, error?: ?LocalizableError, openDialogAction: Function, isDialogOpen: Function, @@ -71,6 +69,9 @@ type Props = { isSubmitting: boolean, isInvalid: boolean, lastUpdatedField: ?string, + changeSpendingPasswordDialog: Node, + deleteWalletDialogContainer: Node, + exportWalletDialogContainer: Node, }; @observer @@ -89,14 +90,17 @@ export default class WalletSettings extends Component { const { intl } = this.context; const { assuranceLevels, walletAssurance, - walletName, isWalletPasswordSet, - walletPasswordUpdateDate, error, + walletName, isSpendingPasswordSet, + spendingPasswordUpdateDate, error, openDialogAction, isDialogOpen, onFieldValueChange, onStartEditing, onStopEditing, onCancelEditing, nameValidator, activeField, isSubmitting, isInvalid, lastUpdatedField, + changeSpendingPasswordDialog, + deleteWalletDialogContainer, + exportWalletDialogContainer, } = this.props; const assuranceLevelOptions = assuranceLevels.map(assurance => ({ @@ -104,12 +108,14 @@ export default class WalletSettings extends Component { label: intl.formatMessage(assurance.label), })); - const passwordMessage = isWalletPasswordSet ? ( + const passwordMessage = isSpendingPasswordSet ? ( intl.formatMessage(messages.passwordLastUpdated, { - lastUpdated: moment(walletPasswordUpdateDate).fromNow(), + lastUpdated: moment(spendingPasswordUpdateDate).fromNow(), }) ) : intl.formatMessage(messages.passwordNotSet); + const showExportLink = !environment.isMainnet() && !environment.isTestnet(); + return (
@@ -144,25 +150,25 @@ export default class WalletSettings extends Component { openDialogAction({ - dialog: ChangeWalletPasswordDialog, + dialog: ChangeSpendingPasswordDialog, })} /> {error &&

{intl.formatMessage(error)}

}
- {!environment.isMainnet() ? ( + {showExportLink ? ( - ) : null} + ) : false} openDialogAction({ @@ -173,17 +179,17 @@ export default class WalletSettings extends Component { - {isDialogOpen(ChangeWalletPasswordDialog) ? ( - - ) : null} + {isDialogOpen(ChangeSpendingPasswordDialog) ? ( + changeSpendingPasswordDialog + ) : false} {isDialogOpen(DeleteWalletConfirmationDialog) ? ( - - ) : null} + deleteWalletDialogContainer + ) : false} - {isDialogOpen(WalletExportDialog) ? ( - - ) : null} + {isDialogOpen(ExportWalletToFileDialog) ? ( + exportWalletDialogContainer + ) : false}
); diff --git a/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.js b/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.js index 66bc835e83..35866840e8 100644 --- a/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.js +++ b/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.js @@ -3,11 +3,10 @@ import React, { Component } from 'react'; import SVGInline from 'react-svg-inline'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; -import classnames from 'classnames'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import SimpleCheckboxSkin from 'react-polymorph/lib/skins/simple/raw/CheckboxSkin'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; +import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; import attentionIcon from '../../../assets/images/attention-big-light.inline.svg'; import styles from './AdaRedemptionDisclaimer.scss'; @@ -62,10 +61,6 @@ export default class AdaRedemptionDisclaimer extends Component { const { onSubmit } = this.props; const { isAccepted } = this.state; - const submitButtonStyles = classnames([ - !isAccepted ? styles.disabled : null, - ]); - return (
@@ -80,16 +75,16 @@ export default class AdaRedemptionDisclaimer extends Component { label={intl.formatMessage(messages.checkboxLabel)} onChange={this.onAcceptToggle} checked={isAccepted} - skin={} + skin={CheckboxSkin} />
diff --git a/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.scss b/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.scss index 078172d690..62b526e1a2 100644 --- a/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.scss +++ b/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionDisclaimer.scss @@ -48,32 +48,6 @@ opacity: 0.8; } - button { - background-color: var(--theme-ada-redemption-disclaimer-button-background-color); - border: solid 1px var(--theme-ada-redemption-disclaimer-button-border-color); - border-radius: 5px; - color: var(--theme-ada-redemption-disclaimer-button-text-color); - cursor: pointer; - height: 50px; - font-size: 14px; - letter-spacing: 1px; - min-height: 50px; - transition: box-shadow 0.2s cubic-bezier(0.4, 0, 1, 1), background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1), color 0.2s cubic-bezier(0.4, 0, 0.2, 1); - width: 360px; - - &:not(.disabled) { - &:hover { - background-color: var(--theme-ada-redemption-disclaimer-button-background-color-hover); - color: var(--theme-ada-redemption-disclaimer-button-text-color-hover); - } - } - - &.disabled { - cursor: default; - opacity: 0.3; - } - } - :global .adaRedemptionDisclaimerCheckbox { .SimpleCheckbox_root { margin-bottom: 30px; diff --git a/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionForm.js b/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionForm.js index c2c50656e1..9a99dc129c 100644 --- a/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionForm.js +++ b/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionForm.js @@ -4,15 +4,15 @@ import { observer } from 'mobx-react'; import { join } from 'lodash'; import { isEmail, isEmpty } from 'validator'; import classnames from 'classnames'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; +import { Autocomplete } from 'react-polymorph/lib/components/Autocomplete'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { Select } from 'react-polymorph/lib/components/Select'; +import { AutocompleteSkin } from 'react-polymorph/lib/skins/simple/AutocompleteSkin'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; +import { SelectSkin } from 'react-polymorph/lib/skins/simple/SelectSkin'; import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; -import Select from 'react-polymorph/lib/components/Select'; -import SelectSkin from 'react-polymorph/lib/skins/simple/raw/SelectSkin'; -import Autocomplete from 'react-polymorph/lib/components/Autocomplete'; -import SimpleAutocompleteSkin from 'react-polymorph/lib/skins/simple/raw/AutocompleteSkin'; import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; import AdaCertificateUploadWidget from '../../widgets/forms/AdaCertificateUploadWidget'; import AdaRedemptionChoices from './AdaRedemptionChoices'; @@ -23,9 +23,10 @@ import { InvalidMnemonicError, InvalidEmailError, FieldRequiredError } from '../ import globalMessages from '../../../i18n/global-messages'; import styles from './AdaRedemptionForm.scss'; import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../../config/timingConfig'; -import { ADA_REDEMPTION_PASSPHRASE_LENGHT } from '../../../config/cryptoConfig'; +import { ADA_REDEMPTION_PASSPHRASE_LENGTH } from '../../../config/cryptoConfig'; import { ADA_REDEMPTION_TYPES } from '../../../types/redemptionTypes'; import type { RedemptionTypeChoices } from '../../../types/redemptionTypes'; +import { submitOnEnter } from '../../../utils/form'; const messages = defineMessages({ headline: { @@ -181,13 +182,13 @@ where Ada should be redeemed and enter {adaRedemptionPassphraseLength} word mnem defaultMessage: '!!!Enter your Ada amount', description: 'Hint for the Ada amount input field.' }, - walletPasswordPlaceholder: { - id: 'wallet.redeem.dialog.walletPasswordPlaceholder', + spendingPasswordPlaceholder: { + id: 'wallet.redeem.dialog.spendingPasswordPlaceholder', defaultMessage: '!!!Password', description: 'Placeholder for "spending password"', }, - walletPasswordLabel: { - id: 'wallet.redeem.dialog.walletPasswordLabel', + spendingPasswordLabel: { + id: 'wallet.redeem.dialog.spendingPasswordLabel', defaultMessage: '!!!Password', description: 'Label for "spending password"', }, @@ -243,7 +244,7 @@ export default class AdaRedemptionForm extends Component { passPhrase: { label: this.context.intl.formatMessage(messages.passphraseLabel), placeholder: this.context.intl.formatMessage(messages.passphraseHint, { - length: ADA_REDEMPTION_PASSPHRASE_LENGHT + length: ADA_REDEMPTION_PASSPHRASE_LENGTH }), value: [], validators: [({ field }) => { @@ -331,10 +332,10 @@ export default class AdaRedemptionForm extends Component { ]; }], }, - walletPassword: { + spendingPassword: { type: 'password', - label: this.context.intl.formatMessage(messages.walletPasswordLabel), - placeholder: this.context.intl.formatMessage(messages.walletPasswordPlaceholder), + label: this.context.intl.formatMessage(messages.spendingPasswordLabel), + placeholder: this.context.intl.formatMessage(messages.spendingPasswordPlaceholder), value: '', validators: [({ field, form }) => { const password = field.value; @@ -371,11 +372,11 @@ export default class AdaRedemptionForm extends Component { submit = () => { this.form.submit({ onSuccess: (form) => { - const { walletId, shieldedRedemptionKey, walletPassword } = form.values(); + const { walletId, shieldedRedemptionKey, spendingPassword } = form.values(); this.props.onSubmit({ walletId, shieldedRedemptionKey, - walletPassword: walletPassword || null, + spendingPassword: spendingPassword || null, }); }, onError: () => {}, @@ -390,7 +391,7 @@ export default class AdaRedemptionForm extends Component { // We can not user form.reset() call here as it would reset selected walletId // which is a bad UX since we are calling resetForm on certificate add/remove - form.$('walletPassword').reset(); + form.$('spendingPassword').reset(); form.$('adaAmount').reset(); form.$('adaPasscode').reset(); form.$('certificate').reset(); @@ -406,7 +407,7 @@ export default class AdaRedemptionForm extends Component { onWalletChange = (walletId: string) => { const { form } = this; form.$('walletId').value = walletId; - form.$('walletPassword').value = ''; + form.$('spendingPassword').value = ''; } render() { @@ -428,7 +429,7 @@ export default class AdaRedemptionForm extends Component { const emailField = form.$('email'); const adaPasscodeField = form.$('adaPasscode'); const adaAmountField = form.$('adaAmount'); - const walletPasswordField = form.$('walletPassword'); + const spendingPasswordField = form.$('spendingPassword'); const decryptionKeyField = form.$('decryptionKey'); const componentClasses = classnames([ styles.component, @@ -444,7 +445,7 @@ export default class AdaRedemptionForm extends Component { redemptionType === ADA_REDEMPTION_TYPES.RECOVERY_FORCE_VENDED ); - const passwordSubmittable = !walletHasPassword || walletPasswordField.value !== ''; + const passwordSubmittable = !walletHasPassword || spendingPasswordField.value !== ''; let canSubmit = false; if (( @@ -471,18 +472,18 @@ export default class AdaRedemptionForm extends Component { switch (redemptionType) { case ADA_REDEMPTION_TYPES.REGULAR: instructionMessage = messages.instructionsRegular; - instructionValues = { adaRedemptionPassphraseLength: ADA_REDEMPTION_PASSPHRASE_LENGHT }; + instructionValues = { adaRedemptionPassphraseLength: ADA_REDEMPTION_PASSPHRASE_LENGTH }; break; case ADA_REDEMPTION_TYPES.FORCE_VENDED: instructionMessage = messages.instructionsForceVended; break; case ADA_REDEMPTION_TYPES.PAPER_VENDED: instructionMessage = messages.instructionsPaperVended; - instructionValues = { adaRedemptionPassphraseLength: ADA_REDEMPTION_PASSPHRASE_LENGHT }; + instructionValues = { adaRedemptionPassphraseLength: ADA_REDEMPTION_PASSPHRASE_LENGTH }; break; case ADA_REDEMPTION_TYPES.RECOVERY_REGULAR: instructionMessage = messages.instructionsRecoveryRegular; - instructionValues = { adaRedemptionPassphraseLength: ADA_REDEMPTION_PASSPHRASE_LENGHT }; + instructionValues = { adaRedemptionPassphraseLength: ADA_REDEMPTION_PASSPHRASE_LENGTH }; break; case ADA_REDEMPTION_TYPES.RECOVERY_FORCE_VENDED: instructionMessage = messages.instructionsRecoveryForceVended; @@ -522,6 +523,7 @@ export default class AdaRedemptionForm extends Component {
{redemptionType !== ADA_REDEMPTION_TYPES.PAPER_VENDED ? ( { }} disabled={isRecovery || isCertificateSelected} error={redemptionKeyField.error} - skin={} + skin={InputSkin} /> ) : ( } + skin={InputSkin} /> )} @@ -554,7 +557,7 @@ export default class AdaRedemptionForm extends Component { {...walletId.bind()} onChange={this.onWalletChange} isOpeningUpward - skin={} + skin={SelectSkin} />
@@ -583,10 +586,11 @@ export default class AdaRedemptionForm extends Component { {walletHasPassword ? (
} + onKeyPress={submitOnEnter.bind(this, submit)} + className="spendingPassword" + {...spendingPasswordField.bind()} + error={spendingPasswordField.error} + skin={InputSkin} />
) : null} @@ -596,13 +600,13 @@ export default class AdaRedemptionForm extends Component { } + skin={AutocompleteSkin} />
) : null} @@ -610,10 +614,11 @@ export default class AdaRedemptionForm extends Component { {showInputForDecryptionKey ? (
} + skin={InputSkin} />
) : null} @@ -621,10 +626,11 @@ export default class AdaRedemptionForm extends Component { {showInputsForDecryptingForceVendedCertificate ? (
} + skin={InputSkin} />
) : null} @@ -632,10 +638,11 @@ export default class AdaRedemptionForm extends Component { {showInputsForDecryptingForceVendedCertificate ? (
} + skin={InputSkin} />
) : null} @@ -643,10 +650,11 @@ export default class AdaRedemptionForm extends Component { {showInputsForDecryptingForceVendedCertificate ? (
} + skin={InputSkin} />
) : null} @@ -656,9 +664,9 @@ export default class AdaRedemptionForm extends Component {
diff --git a/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionSuccessOverlay.js b/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionSuccessOverlay.js index eddb7e9388..d231370b41 100644 --- a/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionSuccessOverlay.js +++ b/source/renderer/app/components/wallet/ada-redemption/AdaRedemptionSuccessOverlay.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; import SVGInline from 'react-svg-inline'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import styles from './AdaRedemptionSuccessOverlay.scss'; import successIcon from '../../../assets/images/success-big.inline.svg'; @@ -49,7 +49,7 @@ export default class AdaRedemptionSuccessOverlay extends Component { className={styles.confirmButton} label={intl.formatMessage(messages.confirmButton)} onClick={onClose} - skin={} + skin={ButtonSkin} />
diff --git a/source/renderer/app/components/wallet/backup-recovery/MnemonicWord.js b/source/renderer/app/components/wallet/backup-recovery/MnemonicWord.js index ed0d99ff18..b75620e4fe 100644 --- a/source/renderer/app/components/wallet/backup-recovery/MnemonicWord.js +++ b/source/renderer/app/components/wallet/backup-recovery/MnemonicWord.js @@ -1,9 +1,8 @@ // @flow import React, { Component } from 'react'; import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; import styles from './MnemonicWord.scss'; type Props = { @@ -18,17 +17,16 @@ export default class MnemonicWord extends Component { render() { const { word, index, isActive, onClick } = this.props; - const componentClassNames = classnames([ - 'flat', - styles.component, - isActive ? styles.active : styles.inactive - ]); + const handleClick = onClick.bind(null, { word, index }); + return (
diff --git a/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js b/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js index 90ccee5bf2..ebc360cd2f 100644 --- a/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js +++ b/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js @@ -2,8 +2,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import SimpleCheckboxSkin from 'react-polymorph/lib/skins/simple/raw/CheckboxSkin'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; import WalletRecoveryPhraseMnemonic from './WalletRecoveryPhraseMnemonic'; import DialogCloseButton from '../../widgets/DialogCloseButton'; @@ -130,15 +130,19 @@ export default class WalletRecoveryPhraseEntryDialog extends Component { {!isValid && (
- {recoveryPhraseShuffled.map(({ word, isActive }, index) => ( - isActive && onAddWord(value)} - /> - ))} + {recoveryPhraseShuffled.map(({ word, isActive }, index) => { + const handleClick = value => isActive && onAddWord(value); + + return ( + + ); + })}
)} @@ -149,7 +153,7 @@ export default class WalletRecoveryPhraseEntryDialog extends Component { label={} onChange={onAcceptTermDevice} checked={isTermDeviceAccepted} - skin={} + skin={CheckboxSkin} />
@@ -157,7 +161,7 @@ export default class WalletRecoveryPhraseEntryDialog extends Component { label={intl.formatMessage(messages.termRecovery)} onChange={onAcceptTermRecovery} checked={isTermRecoveryAccepted} - skin={} + skin={CheckboxSkin} />
diff --git a/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.scss b/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.scss index a5b3aa922b..52cad0705c 100644 --- a/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.scss +++ b/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.scss @@ -25,5 +25,6 @@ } .isSubmitting { + box-shadow: none !important; @include loading-spinner("../../../assets/images/spinner-light.svg"); } diff --git a/source/renderer/app/components/wallet/etc/WalletReceive.js b/source/renderer/app/components/wallet/etc/WalletReceive.js deleted file mode 100644 index c5b926b250..0000000000 --- a/source/renderer/app/components/wallet/etc/WalletReceive.js +++ /dev/null @@ -1,90 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { defineMessages, intlShape } from 'react-intl'; -import SVGInline from 'react-svg-inline'; -import CopyToClipboard from 'react-copy-to-clipboard'; -import QRCode from 'qrcode.react'; -import BorderedBox from '../../widgets/BorderedBox'; -import iconCopy from '../../../assets/images/clipboard-ic.inline.svg'; -import styles from './WalletReceiveEtc.scss'; - -const messages = defineMessages({ - walletAddressLabel: { - id: 'wallet.receive.page.etc.walletAddressLabel', - defaultMessage: '!!!Your wallet address', - description: 'Label for wallet address on the ETC wallet "Receive page"', - }, - walletReceiveInstructions: { - id: 'wallet.receive.page.etc.walletReceiveInstructions', - defaultMessage: '!!!Share this wallet address to receive payments. To protect your privacy generate new addresses for receiving payments instead of reusing existing ones.', - description: 'Wallet receive payments instructions on the ETC wallet "Receive page"', - }, -}); - -type Props = { - walletAddress: string, - onCopyAddress: Function, -}; - -@observer -export default class WalletReceive extends Component { - - static contextTypes = { - intl: intlShape.isRequired, - }; - - render() { - const { walletAddress, onCopyAddress } = this.props; - const { intl } = this.context; - - // Get QRCode color value from active theme's CSS variable - const qrCodeBackgroundColor = document.documentElement ? - document.documentElement.style.getPropertyValue('--theme-receive-qr-code-background-color') : 'transparent'; - const qrCodeForegroundColor = document.documentElement ? - document.documentElement.style.getPropertyValue('--theme-receive-qr-code-foreground-color') : '#000'; - - return ( -
- - - -
-
- -
- -
-
- {walletAddress} - - - -
- -
- {intl.formatMessage(messages.walletAddressLabel)} -
- -
- {intl.formatMessage(messages.walletReceiveInstructions)} -
- -
-
- -
- -
- ); - } - -} diff --git a/source/renderer/app/components/wallet/etc/WalletReceiveEtc.scss b/source/renderer/app/components/wallet/etc/WalletReceiveEtc.scss deleted file mode 100644 index 9b5be4afa3..0000000000 --- a/source/renderer/app/components/wallet/etc/WalletReceiveEtc.scss +++ /dev/null @@ -1,71 +0,0 @@ -.component { - flex: 1; - overflow-x: hidden; - overflow-y: overlay; - padding: 20px; - &::-webkit-scrollbar-button { - height: 7px; - display: block; - } -} - -.copyIcon { - cursor: pointer; - margin-left: 6px; - object-fit: contain; - & > svg { - height: 14.5px; - width: 11.5px; - path { - fill: var(--theme-icon-copy-address-color); - } - } -} - -.qrCodeAndInstructions { - display: flex; - flex-direction: row; - margin: 10px 0; - - .qrCode { - align-items: center; - display: flex; - margin-right: 20px; - - canvas { - border: 4px solid var(--theme-receive-qr-code-background-color); - box-sizing: content-box; - } - } - - .instructions { - color: var(--theme-bordered-box-text-color); - font-family: var(--font-regular); - font-size: 14px; - line-height: 19px; - - .hash { - font-size: 19px; - font-family: var(--font-medium); - line-height: 23px; - margin-bottom: 6px; - user-select: auto; - word-break: break-all; - } - - .usedHash { - opacity: 0.4; - } - - .hashLabel { - margin-bottom: 6px; - opacity: 0.5; - } - - .instructionsText { - font-size: 16px; - line-height: 22px; - word-break: break-word; - } - } -} diff --git a/source/renderer/app/components/wallet/etc/WalletSendConfirmationDialog.js b/source/renderer/app/components/wallet/etc/WalletSendConfirmationDialog.js deleted file mode 100644 index 8173a3fe25..0000000000 --- a/source/renderer/app/components/wallet/etc/WalletSendConfirmationDialog.js +++ /dev/null @@ -1,172 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import classnames from 'classnames'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; -import { intlShape } from 'react-intl'; -import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; -import Dialog from '../../widgets/Dialog'; -import DialogCloseButton from '../../widgets/DialogCloseButton'; -import LocalizableError from '../../../i18n/LocalizableError'; -import styles from '../WalletSendConfirmationDialog.scss'; -import { formattedAmountWithoutTrailingZeros } from '../../../utils/formatters'; -import { messages } from '../WalletSendConfirmationDialog'; -import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../../config/timingConfig'; - -type Props = { - isWalletPasswordSet: boolean, - amount: string, - receiver: string, - totalAmount: ?string, - transactionFee: ?string, - onSubmit: Function, - amountToNaturalUnits: (amountWithFractions: string) => string, - onCancel: Function, - isSubmitting: boolean, - error: ?LocalizableError, - currencyUnit: string, -}; - -@observer -export default class WalletSendConfirmationDialog extends Component { - - static contextTypes = { - intl: intlShape.isRequired, - }; - - form = new ReactToolboxMobxForm({ - fields: { - walletPassword: { - type: 'password', - label: this.context.intl.formatMessage(messages.walletPasswordLabel), - placeholder: this.context.intl.formatMessage(messages.walletPasswordFieldPlaceholder), - value: '', - validators: [({ field }) => { - if (this.props.isWalletPasswordSet && field.value === '') { - return [false, this.context.intl.formatMessage(messages.fieldIsRequired)]; - } - return [true]; - }], - }, - } - }, { - options: { - validateOnChange: true, - validationDebounceWait: FORM_VALIDATION_DEBOUNCE_WAIT, - }, - }); - - submit() { - this.form.submit({ - onSuccess: (form) => { - const { isWalletPasswordSet, receiver, amount, amountToNaturalUnits } = this.props; - const { walletPassword } = form.values(); - const transactionData = { - receiver, - amount: amountToNaturalUnits(amount), - password: isWalletPasswordSet ? walletPassword : null, - }; - this.props.onSubmit(transactionData); - }, - onError: () => {} - }); - } - - render() { - const { form } = this; - const { intl } = this.context; - const walletPasswordField = form.$('walletPassword'); - const { - onCancel, - isWalletPasswordSet, - amount, - receiver, - totalAmount, - transactionFee, - isSubmitting, - error, - currencyUnit - } = this.props; - - const confirmButtonClasses = classnames([ - 'confirmButton', - isSubmitting ? styles.submitButtonSpinning : null, - ]); - - const actions = [ - { - label: intl.formatMessage(messages.backButtonLabel), - onClick: !isSubmitting && onCancel, - }, - { - label: intl.formatMessage(messages.sendButtonLabel), - onClick: this.submit.bind(this), - primary: true, - className: confirmButtonClasses, - disabled: !walletPasswordField.isValid, - }, - ]; - - const formattedAmount = formattedAmountWithoutTrailingZeros(amount); - const formattedTransactionFee = formattedAmountWithoutTrailingZeros(transactionFee || ''); - const formattedTotalAmount = formattedAmountWithoutTrailingZeros(totalAmount || ''); - - return ( - } - > -
-
-
- {intl.formatMessage(messages.addressToLabel)} -
-
{receiver}
-
- -
-
-
{intl.formatMessage(messages.amountLabel)}
-
{formattedAmount} -  {currencyUnit} -
-
- -
-
{intl.formatMessage(messages.feesLabel)}
-
+{formattedTransactionFee} -  {currencyUnit} -
-
-
- -
-
{intl.formatMessage(messages.totalLabel)}
-
{formattedTotalAmount} -  {currencyUnit} -
-
- - {isWalletPasswordSet ? ( - } - /> - ) : null} -
- - {error ?

{intl.formatMessage(error)}

: null} - -
- ); - } - -} diff --git a/source/renderer/app/components/wallet/etc/WalletSendForm.js b/source/renderer/app/components/wallet/etc/WalletSendForm.js deleted file mode 100644 index b56d530458..0000000000 --- a/source/renderer/app/components/wallet/etc/WalletSendForm.js +++ /dev/null @@ -1,255 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { intlShape } from 'react-intl'; -import BigNumber from 'bignumber.js'; -import classnames from 'classnames'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; -import Input from 'react-polymorph/lib/components/Input'; -import NumericInput from 'react-polymorph/lib/components/NumericInput'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; -import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; -import AmountInputSkin from '../skins/etc/AmountInputSkin'; -import BorderedBox from '../../widgets/BorderedBox'; -import styles from '../WalletSendForm.scss'; -import WalletSendConfirmationDialog from './WalletSendConfirmationDialog'; -import WalletSendConfirmationDialogContainer from '../../../containers/wallet/dialogs/WalletSendConfirmationDialogContainer'; -import { formattedAmountToBigNumber, formattedAmountToNaturalUnits } from '../../../utils/formatters'; -import { messages } from '../WalletSendForm'; -import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../../config/timingConfig'; - -type Props = { - currencyUnit: string, - currencyMaxIntegerDigits?: number, - currencyMaxFractionalDigits: number, - validateAmount: (amountInNaturalUnits: string) => Promise, - calculateTransactionFee: (receiver: string, amount: string) => Promise, - addressValidator: Function, - openDialogAction: Function, - isDialogOpen: Function, -}; - -type State = { - isTransactionFeeCalculated: boolean, - transactionFee: BigNumber, - transactionFeeError: ?string, -}; - -@observer -export default class WalletSendForm extends Component { - - static contextTypes = { - intl: intlShape.isRequired, - }; - - state = { - isTransactionFeeCalculated: false, - transactionFee: new BigNumber(0), - transactionFeeError: null, - }; - - // We need to track the fee calculation state in order to disable - // the "Submit" button as soon as either receiver or amount field changes. - // This is required as we are using debounced validation and we need to - // disable the "Submit" button as soon as the value changes and then wait for - // the validation to end in order to see if the button should be enabled or not. - _isCalculatingFee = false; - - // 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; - } - - // FORM VALIDATION - form = new ReactToolboxMobxForm({ - fields: { - receiver: { - label: this.context.intl.formatMessage(messages.receiverLabel), - placeholder: this.context.intl.formatMessage(messages.receiverHint), - value: '', - validators: [({ field, form }) => { - const value = field.value; - if (value === '') { - this._resetTransactionFee(); - return [false, this.context.intl.formatMessage(messages.fieldIsRequired)]; - } - return this.props.addressValidator(value) - .then(isValid => { - const amountField = form.$('amount'); - const amountValue = amountField.value; - const isAmountValid = amountField.isValid; - if (isValid && isAmountValid) { - this._calculateTransactionFee(value, amountValue); - } else { - this._resetTransactionFee(); - } - return [isValid, this.context.intl.formatMessage(messages.invalidAddress)]; - }); - }], - }, - amount: { - label: this.context.intl.formatMessage(messages.amountLabel), - placeholder: `0.${'0'.repeat(this.props.currencyMaxFractionalDigits)}`, - value: '', - validators: [({ field, form }) => { - const amountValue = field.value; - if (amountValue === '') { - this._resetTransactionFee(); - return [false, this.context.intl.formatMessage(messages.fieldIsRequired)]; - } - const isValid = this.props.validateAmount(formattedAmountToNaturalUnits(amountValue)); - const receiverField = form.$('receiver'); - const receiverValue = receiverField.value; - const isReceiverValid = receiverField.isValid; - if (isValid && isReceiverValid) { - this._calculateTransactionFee(receiverValue, amountValue); - } else { - this._resetTransactionFee(); - } - return [isValid, this.context.intl.formatMessage(messages.invalidAmount)]; - }], - }, - }, - }, { - options: { - validateOnBlur: false, - validateOnChange: true, - validationDebounceWait: FORM_VALIDATION_DEBOUNCE_WAIT, - }, - }); - - render() { - const { form } = this; - const { intl } = this.context; - const { - currencyUnit, currencyMaxIntegerDigits, currencyMaxFractionalDigits, - openDialogAction, isDialogOpen - } = this.props; - const { isTransactionFeeCalculated, transactionFee, transactionFeeError } = this.state; - const amountField = form.$('amount'); - const receiverField = form.$('receiver'); - const receiverFieldProps = receiverField.bind(); - const amountFieldProps = amountField.bind(); - const amount = formattedAmountToBigNumber(amountFieldProps.value); - - let fees = null; - let total = null; - if (isTransactionFeeCalculated) { - fees = transactionFee.toFormat(currencyMaxFractionalDigits); - total = amount.add(transactionFee).toFormat(currencyMaxFractionalDigits); - } - - const buttonClasses = classnames([ - 'primary', - styles.nextButton, - ]); - - return ( -
- - - -
- { - this._isCalculatingFee = true; - receiverField.onChange(value || ''); - }} - skin={} - /> -
- -
- { - this._isCalculatingFee = true; - amountField.onChange(value || ''); - }} - // AmountInputSkin props - currency={currencyUnit} - fees={fees} - total={total} - skin={} - /> -
- -
- ); - } - - _resetTransactionFee() { - if (this._isMounted) { - this.setState({ - isTransactionFeeCalculated: false, - transactionFee: new BigNumber(0), - transactionFeeError: null, - }); - } - } - - async _calculateTransactionFee(receiver: string, amountValue: string) { - const amount = formattedAmountToNaturalUnits(amountValue); - try { - const fee = await this.props.calculateTransactionFee(receiver, amount); - if (this._isMounted) { - this._isCalculatingFee = false; - this.setState({ - isTransactionFeeCalculated: true, - transactionFee: fee, - transactionFeeError: null, - }); - } - } catch (error) { - if (this._isMounted) { - this._isCalculatingFee = false; - this.setState({ - isTransactionFeeCalculated: false, - transactionFee: new BigNumber(0), - transactionFeeError: this.context.intl.formatMessage(error) - }); - } - } - } -} diff --git a/source/renderer/app/components/wallet/etc/WalletSettings.js b/source/renderer/app/components/wallet/etc/WalletSettings.js deleted file mode 100644 index 3a5a281730..0000000000 --- a/source/renderer/app/components/wallet/etc/WalletSettings.js +++ /dev/null @@ -1,120 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { intlShape } from 'react-intl'; -import moment from 'moment'; -import LocalizableError from '../../../i18n/LocalizableError'; -import BorderedBox from '../../widgets/BorderedBox'; -import InlineEditingInput from '../../widgets/forms/InlineEditingInput'; -import ReadOnlyInput from '../../widgets/forms/ReadOnlyInput'; -import DeleteWalletButton from '../settings/DeleteWalletButton'; -import DeleteWalletConfirmationDialog from '../settings/DeleteWalletConfirmationDialog'; -import DeleteWalletDialogContainer from '../../../containers/wallet/dialogs/DeleteWalletDialogContainer'; -import ChangeWalletPasswordDialog from '../settings/ChangeWalletPasswordDialog'; -import ChangeWalletPasswordDialogContainer from '../../../containers/wallet/dialogs/ChangeWalletPasswordDialogContainer'; -import globalMessages from '../../../i18n/global-messages'; -import styles from '../WalletSettings.scss'; -import { messages } from '../WalletSettings'; - -type Props = { - walletName: string, - isWalletPasswordSet: boolean, - walletPasswordUpdateDate: ?Date, - error?: ?LocalizableError, - openDialogAction: Function, - isDialogOpen: Function, - onFieldValueChange: Function, - onStartEditing: Function, - onStopEditing: Function, - onCancelEditing: Function, - nameValidator: Function, - activeField: ?string, - isSubmitting: boolean, - isInvalid: boolean, - lastUpdatedField: ?string, -}; - -@observer -export default class WalletSettings extends Component { - - static contextTypes = { - intl: intlShape.isRequired, - }; - - componentWillUnmount() { - // This call is used to prevent display of old successfully-updated messages - this.props.onCancelEditing(); - } - - render() { - const { intl } = this.context; - const { - walletName, isWalletPasswordSet, - walletPasswordUpdateDate, error, - openDialogAction, isDialogOpen, - onFieldValueChange, onStartEditing, - onStopEditing, onCancelEditing, - nameValidator, activeField, - isSubmitting, isInvalid, - lastUpdatedField, - } = this.props; - - const passwordMessage = isWalletPasswordSet ? ( - intl.formatMessage(messages.passwordLastUpdated, { - lastUpdated: moment(walletPasswordUpdateDate).fromNow(), - }) - ) : intl.formatMessage(messages.passwordNotSet); - - return ( -
- - - - onStartEditing('name')} - onStopEditing={onStopEditing} - onCancelEditing={onCancelEditing} - onSubmit={(value) => onFieldValueChange('name', value)} - isValid={nameValidator} - validationErrorMessage={intl.formatMessage(globalMessages.invalidWalletName)} - successfullyUpdated={!isSubmitting && lastUpdatedField === 'name' && !isInvalid} - /> - - openDialogAction({ - dialog: ChangeWalletPasswordDialog, - })} - /> - - {error &&

{intl.formatMessage(error)}

} - -
- openDialogAction({ - dialog: DeleteWalletConfirmationDialog, - })} - /> -
- -
- - {isDialogOpen(ChangeWalletPasswordDialog) ? ( - - ) : null} - - {isDialogOpen(DeleteWalletConfirmationDialog) ? ( - - ) : null} - -
- ); - } - -} diff --git a/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.js b/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.js index 3f0b13c916..c1d28b11c5 100644 --- a/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.js +++ b/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.js @@ -3,15 +3,16 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; import { defineMessages, intlShape } from 'react-intl'; -// import Input from 'react-polymorph/lib/components/Input'; -// import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; -// import Checkbox from 'react-polymorph/lib/components/Checkbox'; -// import SimpleSwitchSkin from 'react-polymorph/lib/skins/simple/raw/SwitchSkin'; +// import { Input } from 'react-polymorph/lib/components/Input'; +// import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +// import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; +// import { SwitchSkin } from 'react-polymorph/lib/skins/simple/SwitchSkin'; +// import { IDENTIFIERS } from 'react-polymorph/lib/themes/API'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import Dialog from '../../widgets/Dialog'; import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; import FileUploadWidget from '../../widgets/forms/FileUploadWidget'; -import { isValidWalletName, isValidWalletPassword, isValidRepeatPassword } from '../../../utils/validations'; +import { isValidWalletName, isValidSpendingPassword, isValidRepeatPassword } from '../../../utils/validations'; import globalMessages from '../../../i18n/global-messages'; import LocalizableError from '../../../i18n/LocalizableError'; import styles from './WalletFileImportDialog.scss'; @@ -58,8 +59,8 @@ const messages = defineMessages({ defaultMessage: '!!!Password', description: 'Label for the "Activate to create password" switch in the wallet file import dialog.', }, - walletPasswordLabel: { - id: 'wallet.file.import.dialog.walletPasswordLabel', + spendingPasswordLabel: { + id: 'wallet.file.import.dialog.spendingPasswordLabel', defaultMessage: '!!!Wallet password', description: 'Label for the "Wallet password" input in the wallet file import dialog.', }, @@ -101,7 +102,6 @@ export default class WalletFileImportDialog extends Component { this.setState({ createPassword: value }); }; - form = new ReactToolboxMobxForm({ fields: { walletFile: { @@ -121,9 +121,9 @@ export default class WalletFileImportDialog extends Component { ]; }], }, - walletPassword: { + spendingPassword: { type: 'password', - label: this.context.intl.formatMessage(messages.walletPasswordLabel), + label: this.context.intl.formatMessage(messages.spendingPasswordLabel), placeholder: this.context.intl.formatMessage(messages.passwordFieldPlaceholder), value: '', validators: [({ field, form }) => { @@ -133,8 +133,8 @@ export default class WalletFileImportDialog extends Component { repeatPasswordField.validate({ showErrors: true }); } return [ - isValidWalletPassword(field.value), - this.context.intl.formatMessage(globalMessages.invalidWalletPassword) + isValidSpendingPassword(field.value), + this.context.intl.formatMessage(globalMessages.invalidSpendingPassword) ]; }], }, @@ -145,10 +145,10 @@ export default class WalletFileImportDialog extends Component { value: '', validators: [({ field, form }) => { if (!this.state.createPassword) return [true]; - const walletPassword = form.$('walletPassword').value; - if (walletPassword.length === 0) return [true]; + const spendingPassword = form.$('spendingPassword').value; + if (spendingPassword.length === 0) return [true]; return [ - isValidRepeatPassword(walletPassword, field.value), + isValidRepeatPassword(spendingPassword, field.value), this.context.intl.formatMessage(globalMessages.invalidRepeatPassword) ]; }], @@ -165,10 +165,10 @@ export default class WalletFileImportDialog extends Component { this.form.submit({ onSuccess: (form) => { const { createPassword } = this.state; - const { walletFile, walletPassword, walletName } = form.values(); + const { walletFile, spendingPassword, walletName } = form.values(); const walletData = { filePath: walletFile.path, - walletPassword: createPassword ? walletPassword : null, + spendingPassword: createPassword ? spendingPassword : null, walletName: (walletName.length > 0) ? walletName : null, }; this.props.onSubmit(walletData); @@ -189,8 +189,8 @@ export default class WalletFileImportDialog extends Component { 'WalletFileImportDialog', ]); - // const walletPasswordFieldsClasses = classnames([ - // styles.walletPasswordFields, + // const spendingPasswordFieldsClasses = classnames([ + // styles.spendingPasswordFields, // createPassword ? styles.show : null, // ]); @@ -205,7 +205,7 @@ export default class WalletFileImportDialog extends Component { ]; // const walletNameField = form.$('walletName'); - // const walletPasswordField = form.$('walletPassword'); + // const spendingPasswordField = form.$('spendingPassword'); // const repeatedPasswordField = form.$('repeatPassword'); return ( @@ -232,44 +232,45 @@ export default class WalletFileImportDialog extends Component { {/* TODO: re-enable when wallet-name and wallet-password support is added to the API endpoint - } - /> + -
-
-
- {intl.formatMessage(messages.passwordSwitchLabel)} +
+
+
+ {intl.formatMessage(messages.passwordSwitchLabel)} +
+
- } - /> -
-
- } - /> - } - /> -

- {intl.formatMessage(globalMessages.passwordInstructions)} -

+
+ + +

+ {intl.formatMessage(globalMessages.passwordInstructions)} +

+
-
*/} {error &&

{intl.formatMessage(error)}

} diff --git a/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.scss b/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.scss index e2a7411381..370c80c560 100644 --- a/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.scss +++ b/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.scss @@ -2,8 +2,16 @@ @import '../../../themes/mixins/error-message'; @import '../../../themes/mixins/place-form-field-error-below-input'; -.walletPassword { - .walletPasswordSwitch { +.component { + :global { + .Dialog_actions { + margin-top: 10px; + } + } +} + +.spendingPassword { + .spendingPasswordSwitch { border-top: 1px solid var(--theme-separation-border-color); margin-top: 30px; padding-top: 20px; @@ -23,7 +31,7 @@ } } - .walletPasswordFields { + .spendingPasswordFields { display: flex; flex-wrap: wrap; justify-content: space-between; @@ -61,10 +69,11 @@ .error { @include error-message; - margin-top: 30px; + margin: 20px 0 10px; text-align: center; } .isSubmitting { + box-shadow: none !important; @include loading-spinner("../../../assets/images/spinner-light.svg"); } diff --git a/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.js b/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.js index 63e81e1743..8971f60d5a 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.js +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.js @@ -4,9 +4,13 @@ import { observer } from 'mobx-react'; import QRCode from 'qrcode.react'; import classnames from 'classnames'; import { defineMessages, intlShape } from 'react-intl'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import SVGInline from 'react-svg-inline'; import Dialog from '../../widgets/Dialog'; -import { getNetworkExplorerUrl } from '../../../utils/ada/network'; +import { getNetworkExplorerUrl } from '../../../utils/network'; import styles from './CompletionDialog.scss'; +import iconCopy from '../../../assets/images/clipboard-ic.inline.svg'; +import InlineNotification from '../../widgets/InlineNotification'; const messages = defineMessages({ headline: { @@ -35,12 +39,16 @@ const messages = defineMessages({ defaultMessage: '!!!Cardano explorer link', description: '"Paper wallet create certificate completion dialog" cardano link label.' }, + addressCopiedLabel: { + id: 'paper.wallet.create.certificate.completion.dialog.addressCopiedLabel', + defaultMessage: '!!!copied', + description: '"Paper wallet create certificate completion dialog" address copied.' + }, addressLabel: { id: 'paper.wallet.create.certificate.completion.dialog.addressLabel', defaultMessage: '!!!Wallet address', description: '"Paper wallet create certificate completion dialog" wallet address label.' }, - finishButtonLabel: { id: 'paper.wallet.create.certificate.completion.dialog.finishButtonLabel', defaultMessage: '!!!Finish', @@ -52,18 +60,40 @@ type Props = { walletCertificateAddress: string, onClose: Function, onOpenExternalLink: Function, + copyAddressNotificationDuration: number, }; +type State = { + showCopyNotification: boolean, +} + @observer -export default class CompletionDialog extends Component { +export default class CompletionDialog extends Component { static contextTypes = { intl: intlShape.isRequired, }; + state = { + showCopyNotification: false, + } + + copyNotificationTimeout: number; + + onShowCopyNotification = () => { + const { copyAddressNotificationDuration } = this.props; + const timeInSeconds = copyAddressNotificationDuration * 1000; + clearTimeout(this.copyNotificationTimeout); + + this.setState({ showCopyNotification: true }); + this.copyNotificationTimeout = setTimeout(() => + this.setState({ showCopyNotification: false }), timeInSeconds); + } + render() { const { intl } = this.context; const { onClose, walletCertificateAddress, onOpenExternalLink } = this.props; + const { showCopyNotification } = this.state; const dialogClasses = classnames([ styles.component, 'completionDialog', @@ -117,8 +147,20 @@ export default class CompletionDialog extends Component {

{intl.formatMessage(messages.addressLabel)}

+ + {intl.formatMessage(messages.addressCopiedLabel)} + +
{walletCertificateAddress} + + +
diff --git a/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.scss b/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.scss index ed6f144762..fbbefda5a7 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.scss +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.scss @@ -48,4 +48,33 @@ margin: auto; } } + + .copyIconBig, + .copyIcon { + cursor: pointer; + object-fit: contain; + } + + .copyIconBig { + margin-left: 6px; + & > svg { + height: 14.5px; + width: 11.5px; + path { + fill: var(--theme-icon-copy-address-color); + } + } + } + + .copyIcon { + margin-left: 4px; + & > svg { + height: 12px; + width: 10px; + path { + fill: var(--theme-icon-copy-address-color); + } + } + } + } diff --git a/source/renderer/app/components/wallet/paper-wallet-certificate/ConfirmationDialog.js b/source/renderer/app/components/wallet/paper-wallet-certificate/ConfirmationDialog.js index 4457016854..92b0815f44 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/ConfirmationDialog.js +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/ConfirmationDialog.js @@ -57,6 +57,7 @@ export default class ConfirmationDialog extends Component { const confirmButtonClasses = classnames([ 'confirmButton', + 'attention', styles.confirmButton, ]); diff --git a/source/renderer/app/components/wallet/paper-wallet-certificate/ConfirmationDialog.scss b/source/renderer/app/components/wallet/paper-wallet-certificate/ConfirmationDialog.scss index d4ad7cbc5a..5f22e08d36 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/ConfirmationDialog.scss +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/ConfirmationDialog.scss @@ -10,22 +10,4 @@ } } - .confirmButton { - &[disabled] { - background: var(--theme-input-remove-color-lightest); - &:hover, - &:active { - background: var(--theme-input-remove-color-lightest); - } - } - &:not([disabled]) { - background: var(--theme-input-remove-color-light); - &:hover { - background: var(--theme-input-remove-color-lighter); - } - &:active { - background: var(--theme-input-remove-color-dark); - } - } - } } diff --git a/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.js b/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.js index ffddbec016..b9a3070696 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.js +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.js @@ -5,7 +5,7 @@ import classnames from 'classnames'; import { defineMessages, intlShape, FormattedMessage } from 'react-intl'; import Dialog from '../../widgets/Dialog'; import DialogCloseButton from '../../widgets/DialogCloseButton'; -import { getNetworkExplorerUrl } from '../../../utils/ada/network'; +import { getNetworkExplorerUrl } from '../../../utils/network'; import styles from './InstructionsDialog.scss'; import { PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT, diff --git a/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.scss b/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.scss index 01ebc4e9cc..c38749522f 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.scss +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.scss @@ -42,7 +42,7 @@ .printingInstructions { border-top: 1px solid var(--theme-separation-border-color); margin-top: 20px; - padding-bottom: 20px; + padding-bottom: 0; padding-top: 20px; line-height: 1.38; @@ -53,5 +53,6 @@ } .submitButtonSpinning { + box-shadow: none !important; @include loading-spinner("../../../assets/images/spinner-light.svg"); } diff --git a/source/renderer/app/components/wallet/paper-wallet-certificate/PrintDialog.js b/source/renderer/app/components/wallet/paper-wallet-certificate/PrintDialog.js index 68f842c654..c927c28fe5 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/PrintDialog.js +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/PrintDialog.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; import { defineMessages, intlShape } from 'react-intl'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import SimpleCheckboxSkin from 'react-polymorph/lib/skins/simple/raw/CheckboxSkin'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; import Dialog from '../../widgets/Dialog'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import globalMessages from '../../../i18n/global-messages'; @@ -22,15 +22,15 @@ const messages = defineMessages({ }, subtitle: { id: 'paper.wallet.create.certificate.print.dialog.subtitle', - defaultMessage: `!!!Check your paper wallet certificate and make sure everything - is readable and correctly printed. You can test this by scanning the QR code with + defaultMessage: `!!!Check your paper wallet certificate and make sure everything + is readable and correctly printed. You can test this by scanning the QR code with a QR scanner application on your mobile phone.`, description: '"Paper wallet create certificate print dialog" subtitle.' }, info: { id: 'paper.wallet.create.certificate.print.dialog.info', - defaultMessage: `!!!Your certificate is not yet complete and does not contain all - the data needed to restore your paper wallet. In the next step, you will need to + defaultMessage: `!!!Your certificate is not yet complete and does not contain all + the data needed to restore your paper wallet. In the next step, you will need to write down an additional {paperWalletWrittenWordsCount} words to your paper wallet recovery phrase.`, description: '"Paper wallet create certificate print dialog" info.' }, @@ -138,7 +138,7 @@ export default class PrintDialog extends Component { label={intl.formatMessage(messages.certificatePrintedConfirmationLabel)} onChange={this.onConfirmCorrectPrinting.bind(this)} checked={isPrintedCorrectly} - skin={} + skin={CheckboxSkin} /> { })} onChange={this.onConfirmReadable.bind(this)} checked={isReadable} - skin={} + skin={CheckboxSkin} /> { label={intl.formatMessage(messages.qrScannableConfirmationLabel)} onChange={this.onConfirmScannable.bind(this)} checked={isScannable} - skin={} + skin={CheckboxSkin} />
diff --git a/source/renderer/app/components/wallet/paper-wallet-certificate/SecuringPasswordDialog.js b/source/renderer/app/components/wallet/paper-wallet-certificate/SecuringPasswordDialog.js index ab0446699f..2c5afe95f1 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/SecuringPasswordDialog.js +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/SecuringPasswordDialog.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; import { defineMessages, intlShape } from 'react-intl'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import SimpleCheckboxSkin from 'react-polymorph/lib/skins/simple/raw/CheckboxSkin'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; import Dialog from '../../widgets/Dialog'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import paperWalletImage from '../../../assets/images/paper-wallet-certificate/certificate.png'; @@ -20,8 +20,8 @@ const messages = defineMessages({ }, infoLabel1: { id: 'paper.wallet.create.certificate.securingPassword.dialog.infoLabel1', - defaultMessage: `!!!To complete your paper wallet certificate you will need to - write the remaining {paperWalletWrittenWordsCount} words of your paper wallet recovery + defaultMessage: `!!!To complete your paper wallet certificate you will need to + write the remaining {paperWalletWrittenWordsCount} words of your paper wallet recovery phrase on your certificate.`, description: '"Paper wallet create certificate securing password dialog" first info label.' }, @@ -112,7 +112,7 @@ export default class SecuringPasswordDialog extends Component { })} onChange={this.onSecurePasswordConfirmationChange.bind(this)} checked={securePasswordConfirmed} - skin={} + skin={CheckboxSkin} />
diff --git a/source/renderer/app/components/wallet/paper-wallet-certificate/VerificationDialog.js b/source/renderer/app/components/wallet/paper-wallet-certificate/VerificationDialog.js index bc6b5b12e5..4fd272973e 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/VerificationDialog.js +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/VerificationDialog.js @@ -4,10 +4,10 @@ import { join } from 'lodash'; import { observer } from 'mobx-react'; import classnames from 'classnames'; import { defineMessages, intlShape } from 'react-intl'; -import Autocomplete from 'react-polymorph/lib/components/Autocomplete'; -import SimpleAutocompleteSkin from 'react-polymorph/lib/skins/simple/raw/AutocompleteSkin'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import SimpleCheckboxSkin from 'react-polymorph/lib/skins/simple/raw/CheckboxSkin'; +import { Autocomplete } from 'react-polymorph/lib/components/Autocomplete'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { AutocompleteSkin } from 'react-polymorph/lib/skins/simple/AutocompleteSkin'; +import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; import Dialog from '../../widgets/Dialog'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; @@ -154,13 +154,19 @@ export default class VerificationDialog extends Component { resetForm = () => { const { form } = this; + const autocomplete = this.recoveryPhraseAutocomplete.getRef(); + // Cancel all debounced field validations form.each((field) => { field.debouncedValidation.cancel(); }); form.reset(); form.showErrors(false); // Autocomplete has to be reset manually - this.recoveryPhraseAutocomplete.clear(); + this.recoveryPhraseAutocomplete.getRef().clear(); + + if (autocomplete && autocomplete.focus) { + autocomplete.focus(); + } this.setState({ storingConfirmed: false, @@ -237,7 +243,7 @@ export default class VerificationDialog extends Component { error={recoveryPhraseField.error} maxVisibleOptions={5} noResultsMessage={intl.formatMessage(messages.recoveryPhraseNoResults)} - skin={} + skin={AutocompleteSkin} /> { onChange={this.onStoringConfirmationChange.bind(this)} checked={storingConfirmed} disabled={!isRecoveryPhraseValid} - skin={} + skin={CheckboxSkin} /> { onChange={this.onRecoveringConfirmationChange.bind(this)} checked={recoveringConfirmed} disabled={!isRecoveryPhraseValid} - skin={} + skin={CheckboxSkin} /> diff --git a/source/renderer/app/components/wallet/settings/ChangeWalletPasswordDialog.js b/source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.js similarity index 78% rename from source/renderer/app/components/wallet/settings/ChangeWalletPasswordDialog.js rename to source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.js index 62b2d0b23a..3f8914799b 100644 --- a/source/renderer/app/components/wallet/settings/ChangeWalletPasswordDialog.js +++ b/source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.js @@ -2,19 +2,21 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import SimpleSwitchSkin from 'react-polymorph/lib/skins/simple/raw/SwitchSkin'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { SwitchSkin } from 'react-polymorph/lib/skins/simple/SwitchSkin'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; +import { IDENTIFIERS } from 'react-polymorph/lib/themes/API'; import { defineMessages, intlShape } from 'react-intl'; import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import Dialog from '../../widgets/Dialog'; -import { isValidWalletPassword, isValidRepeatPassword } from '../../../utils/validations'; +import { isValidSpendingPassword, isValidRepeatPassword } from '../../../utils/validations'; import globalMessages from '../../../i18n/global-messages'; import LocalizableError from '../../../i18n/LocalizableError'; -import styles from './ChangeWalletPasswordDialog.scss'; +import styles from './ChangeSpendingPasswordDialog.scss'; import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../../config/timingConfig'; +import { submitOnEnter } from '../../../utils/form'; const messages = defineMessages({ dialogTitleSetPassword: { @@ -75,7 +77,7 @@ const messages = defineMessages({ }); type Props = { - isWalletPasswordSet: boolean, + isSpendingPasswordSet: boolean, currentPasswordValue: string, newPasswordValue: string, repeatedPasswordValue: string, @@ -92,7 +94,7 @@ type State = { }; @observer -export default class ChangeWalletPasswordDialog extends Component { +export default class ChangeSpendingPasswordDialog extends Component { static defaultProps = { currentPasswordValue: '', @@ -116,17 +118,17 @@ export default class ChangeWalletPasswordDialog extends Component placeholder: this.context.intl.formatMessage(messages.currentPasswordFieldPlaceholder), value: '', validators: [({ field }) => { - if (!this.props.isWalletPasswordSet) return [true]; + if (!this.props.isSpendingPasswordSet) return [true]; return [ - isValidWalletPassword(field.value), - this.context.intl.formatMessage(globalMessages.invalidWalletPassword) + isValidSpendingPassword(field.value), + this.context.intl.formatMessage(globalMessages.invalidSpendingPassword) ]; }], }, - walletPassword: { + spendingPassword: { type: 'password', label: this.context.intl.formatMessage(messages[ - this.props.isWalletPasswordSet ? 'newPasswordLabel' : 'spendingPasswordLabel' + this.props.isSpendingPasswordSet ? 'newPasswordLabel' : 'spendingPasswordLabel' ]), placeholder: this.context.intl.formatMessage(messages.newPasswordFieldPlaceholder), value: '', @@ -137,8 +139,8 @@ export default class ChangeWalletPasswordDialog extends Component repeatPasswordField.validate({ showErrors: true }); } return [ - isValidWalletPassword(field.value), - this.context.intl.formatMessage(globalMessages.invalidWalletPassword) + isValidSpendingPassword(field.value), + this.context.intl.formatMessage(globalMessages.invalidSpendingPassword) ]; }], }, @@ -149,10 +151,10 @@ export default class ChangeWalletPasswordDialog extends Component value: '', validators: [({ field, form }) => { if (this.state.removePassword) return [true]; - const walletPassword = form.$('walletPassword').value; - if (walletPassword.length === 0) return [true]; + const spendingPassword = form.$('spendingPassword').value; + if (spendingPassword.length === 0) return [true]; return [ - isValidRepeatPassword(walletPassword, field.value), + isValidRepeatPassword(spendingPassword, field.value), this.context.intl.formatMessage(globalMessages.invalidRepeatPassword) ]; }], @@ -169,10 +171,10 @@ export default class ChangeWalletPasswordDialog extends Component this.form.submit({ onSuccess: (form) => { const { removePassword } = this.state; - const { currentPassword, walletPassword } = form.values(); + const { currentPassword, spendingPassword } = form.values(); const passwordData = { oldPassword: currentPassword || null, - newPassword: removePassword ? null : walletPassword, + newPassword: removePassword ? null : spendingPassword, }; this.props.onSave(passwordData); }, @@ -193,7 +195,7 @@ export default class ChangeWalletPasswordDialog extends Component const { form } = this; const { intl } = this.context; const { - isWalletPasswordSet, + isSpendingPasswordSet, onCancel, currentPasswordValue, newPasswordValue, @@ -204,18 +206,18 @@ export default class ChangeWalletPasswordDialog extends Component const { removePassword } = this.state; const dialogClasses = classnames([ - isWalletPasswordSet ? 'changePasswordDialog' : 'createPasswordDialog', + isSpendingPasswordSet ? 'changePasswordDialog' : 'createPasswordDialog', styles.dialog, ]); - const walletPasswordFieldsClasses = classnames([ - styles.walletPasswordFields, + const spendingPasswordFieldsClasses = classnames([ + styles.spendingPasswordFields, removePassword ? styles.hidden : null ]); const confirmButtonClasses = classnames([ 'confirmButton', - removePassword ? styles.removeButton : null, + removePassword ? 'attention' : null, isSubmitting ? styles.isSubmitting : null, ]); @@ -234,13 +236,13 @@ export default class ChangeWalletPasswordDialog extends Component ]; const currentPasswordField = form.$('currentPassword'); - const newPasswordField = form.$('walletPassword'); + const newPasswordField = form.$('spendingPassword'); const repeatedPasswordField = form.$('repeatPassword'); return ( closeButton={} > - {isWalletPasswordSet ? ( -
-
+ {isSpendingPasswordSet ? ( +
+
{intl.formatMessage(messages.passwordSwitchLabel)}
@@ -259,41 +261,48 @@ export default class ChangeWalletPasswordDialog extends Component onChange={this.handlePasswordSwitchToggle} label={intl.formatMessage(messages.passwordSwitchPlaceholder)} checked={removePassword} - skin={} + themeId={IDENTIFIERS.SWITCH} + skin={SwitchSkin} />
this.handleDataChange('currentPasswordValue', value)} {...currentPasswordField.bind()} error={currentPasswordField.error} - skin={} + skin={InputSkin} />
) : null} -
+
this.handleDataChange('newPasswordValue', value)} {...newPasswordField.bind()} error={newPasswordField.error} - skin={} + skin={InputSkin} /> this.handleDataChange('repeatedPasswordValue', value)} {...repeatedPasswordField.bind()} error={repeatedPasswordField.error} - skin={} + skin={InputSkin} />

diff --git a/source/renderer/app/components/wallet/settings/ChangeWalletPasswordDialog.scss b/source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.scss similarity index 80% rename from source/renderer/app/components/wallet/settings/ChangeWalletPasswordDialog.scss rename to source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.scss index c541db77cf..4df1c2a8ef 100644 --- a/source/renderer/app/components/wallet/settings/ChangeWalletPasswordDialog.scss +++ b/source/renderer/app/components/wallet/settings/ChangeSpendingPasswordDialog.scss @@ -3,8 +3,8 @@ .dialog { font-family: var(--font-light); - .walletPassword { - .walletPasswordSwitch { + .spendingPassword { + .spendingPasswordSwitch { & .passwordLabel { color: var(--theme-wallet-password-switch-label-color); font-family: var(--font-semibold); @@ -13,14 +13,13 @@ margin-bottom: 10px; } } - & + .walletPasswordFields { + & + .spendingPasswordFields { .newPassword { margin-top: 20px; } } } - :global .SimpleSwitch_root > .SimpleSwitch_checked { background: var(--theme-input-remove-color-light) !important; .SimpleSwitch_thumb { @@ -33,7 +32,7 @@ } } - .walletPasswordFields { + .spendingPasswordFields { max-height: 300px; overflow: hidden; transition: all 400ms ease; @@ -56,16 +55,6 @@ } } - .removeButton { - background: var(--theme-input-remove-color-light) !important; - &:hover { - background: var(--theme-input-remove-color-lighter) !important; - } - &:active { - background: var(--theme-input-remove-color-dark) !important; - } - } - .error { @include error-message; text-align: center; @@ -74,5 +63,6 @@ } .isSubmitting { + box-shadow: none !important; @include loading-spinner("../../../assets/images/spinner-light.svg"); } diff --git a/source/renderer/app/components/wallet/settings/DeleteWalletButton.scss b/source/renderer/app/components/wallet/settings/DeleteWalletButton.scss index 7c8dc45e68..28a2ad23f7 100644 --- a/source/renderer/app/components/wallet/settings/DeleteWalletButton.scss +++ b/source/renderer/app/components/wallet/settings/DeleteWalletButton.scss @@ -1,5 +1,5 @@ .button { - color: var(--theme-input-remove-color-light); + color: var(--theme-button-attention-background-color); font-family: var(--font-medium); font-size: 16px; &:hover { diff --git a/source/renderer/app/components/wallet/settings/DeleteWalletConfirmationDialog.js b/source/renderer/app/components/wallet/settings/DeleteWalletConfirmationDialog.js index ead2fb43a6..5999f75c9a 100644 --- a/source/renderer/app/components/wallet/settings/DeleteWalletConfirmationDialog.js +++ b/source/renderer/app/components/wallet/settings/DeleteWalletConfirmationDialog.js @@ -2,10 +2,10 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; -import Checkbox from 'react-polymorph/lib/components/Checkbox'; -import SimpleCheckboxSkin from 'react-polymorph/lib/skins/simple/raw/CheckboxSkin'; +import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import Dialog from '../../widgets/Dialog'; @@ -13,6 +13,7 @@ import styles from './DeleteWalletConfirmationDialog.scss'; import globalMessages from '../../../i18n/global-messages'; import environment from '../../../../../common/environment'; import { DELETE_WALLET_COUNTDOWN } from '../../../config/timingConfig'; +import { submitOnEnter } from '../../../utils/form'; const messages = defineMessages({ dialogTitle: { @@ -84,10 +85,13 @@ export default class DeleteWalletConfirmationDialog extends Component { const countdownDisplay = countdownRemaining > 0 ? ` (${countdownRemaining})` : ''; const isCountdownFinished = countdownRemaining <= 0; const isWalletNameConfirmationCorrect = confirmationValue === walletName; + const isDisabled = ( + !isCountdownFinished || !isBackupNoticeAccepted || !isWalletNameConfirmationCorrect + ); + const handleSubmit = () => !isDisabled && onContinue(); const buttonClasses = classnames([ - 'deleteButton', - styles.deleteButton, + 'attention', isSubmitting ? styles.isSubmitting : null ]); @@ -100,9 +104,7 @@ export default class DeleteWalletConfirmationDialog extends Component { className: buttonClasses, label: intl.formatMessage(messages.confirmButtonLabel) + countdownDisplay, onClick: onContinue, - disabled: ( - !isCountdownFinished || !isBackupNoticeAccepted || !isWalletNameConfirmationCorrect - ), + disabled: isDisabled, primary: true, }, ]; @@ -124,15 +126,16 @@ export default class DeleteWalletConfirmationDialog extends Component { label={intl.formatMessage(messages.confirmBackupNotice)} onChange={onAcceptBackupNotice} checked={isBackupNoticeAccepted} - skin={} + skin={CheckboxSkin} /> {isBackupNoticeAccepted ? ( } + skin={InputSkin} /> ) : null}

diff --git a/source/renderer/app/components/wallet/settings/DeleteWalletConfirmationDialog.scss b/source/renderer/app/components/wallet/settings/DeleteWalletConfirmationDialog.scss index 11ff5034bb..96ef298d85 100644 --- a/source/renderer/app/components/wallet/settings/DeleteWalletConfirmationDialog.scss +++ b/source/renderer/app/components/wallet/settings/DeleteWalletConfirmationDialog.scss @@ -2,6 +2,7 @@ .dialog { font-family: var(--font-light); + :global { .SimpleCheckbox_root { margin-top: 20px; @@ -21,25 +22,7 @@ } .isSubmitting { + box-shadow: none !important; @include loading-spinner("../../../assets/images/spinner-light.svg"); } - - .deleteButton { - &[disabled] { - background: var(--theme-input-remove-color-lightest); - &:hover, - &:active { - background: var(--theme-input-remove-color-lightest); - } - } - &:not([disabled]) { - background: var(--theme-input-remove-color-light); - &:hover { - background: var(--theme-input-remove-color-lighter); - } - &:active { - background: var(--theme-input-remove-color-dark); - } - } - } } diff --git a/source/renderer/app/components/wallet/settings/export-to-file/WalletExportToFileDialog.js b/source/renderer/app/components/wallet/settings/ExportWalletToFileDialog.js similarity index 83% rename from source/renderer/app/components/wallet/settings/export-to-file/WalletExportToFileDialog.js rename to source/renderer/app/components/wallet/settings/ExportWalletToFileDialog.js index 0f08b6ffe6..bb6b685817 100644 --- a/source/renderer/app/components/wallet/settings/export-to-file/WalletExportToFileDialog.js +++ b/source/renderer/app/components/wallet/settings/ExportWalletToFileDialog.js @@ -3,29 +3,30 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; -import DialogCloseButton from '../../../widgets/DialogCloseButton'; -import ReactToolboxMobxForm from '../../../../utils/ReactToolboxMobxForm'; -import globalMessages from '../../../../i18n/global-messages'; -import Dialog from '../../../widgets/Dialog'; -import LocalizableError from '../../../../i18n/LocalizableError'; -import styles from './WalletExportToFileDialog.scss'; -import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../../../config/timingConfig'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; +import DialogCloseButton from '../../widgets/DialogCloseButton'; +import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; +import globalMessages from '../../../i18n/global-messages'; +import Dialog from '../../widgets/Dialog'; +import LocalizableError from '../../../i18n/LocalizableError'; +import styles from './ExportWalletToFileDialog.scss'; +import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../../config/timingConfig'; +import { submitOnEnter } from '../../../utils/form'; const messages = defineMessages({ headline: { - id: 'wallet.exportToFile.dialog.headline', + id: 'wallet.settings.exportToFile.dialog.headline', defaultMessage: '!!!Export Wallet', description: 'headline for "export wallet to file" dialog.' }, introduction: { - id: 'wallet.exportToFile.dialog.introduction', + id: 'wallet.settings.exportToFile.dialog.introduction', defaultMessage: '!!!You are exporting {walletName} to a file.', description: 'headline for "export wallet to file" dialog.' }, exportButtonLabel: { - id: 'wallet.exportToFile.dialog.submit.label', + id: 'wallet.settings.exportToFile.dialog.submit.label', defaultMessage: '!!!Export', description: 'Label for export wallet to file submit button.' }, @@ -68,7 +69,7 @@ type State = { }; @observer -export default class WalletExportToFileDialog extends Component { +export default class ExportWalletToFileDialog extends Component { static contextTypes = { intl: intlShape.isRequired, @@ -187,7 +188,8 @@ export default class WalletExportToFileDialog extends Component { className={styles.spendingPassword} {...spendingPasswordField.bind()} error={spendingPasswordField.error} - skin={} + skin={InputSkin} + onKeyPress={submitOnEnter.bind(this, this.submit)} /> ) : null} diff --git a/source/renderer/app/components/wallet/settings/export-to-file/WalletExportToFileDialog.scss b/source/renderer/app/components/wallet/settings/ExportWalletToFileDialog.scss similarity index 80% rename from source/renderer/app/components/wallet/settings/export-to-file/WalletExportToFileDialog.scss rename to source/renderer/app/components/wallet/settings/ExportWalletToFileDialog.scss index 6e05e240c1..ba187b7730 100644 --- a/source/renderer/app/components/wallet/settings/export-to-file/WalletExportToFileDialog.scss +++ b/source/renderer/app/components/wallet/settings/ExportWalletToFileDialog.scss @@ -1,5 +1,5 @@ -@import '../../../../themes/mixins/loading-spinner'; -@import '../../../../themes/mixins/error-message'; +@import '../../../themes/mixins/loading-spinner'; +@import '../../../themes/mixins/error-message'; .component { :global { @@ -11,6 +11,7 @@ .introduction { font-family: var(--font-light); + line-height: 1.38; overflow: hidden; text-align: center; } @@ -20,7 +21,8 @@ } .isSubmitting { - @include loading-spinner("../../../../assets/images/spinner-light.svg"); + box-shadow: none !important; + @include loading-spinner("../../../assets/images/spinner-light.svg"); } .error { diff --git a/source/renderer/app/components/wallet/skins/AmountInputSkin.js b/source/renderer/app/components/wallet/skins/AmountInputSkin.js index 7e737b9920..2064f4d06f 100644 --- a/source/renderer/app/components/wallet/skins/AmountInputSkin.js +++ b/source/renderer/app/components/wallet/skins/AmountInputSkin.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { defineMessages, intlShape } from 'react-intl'; import BigNumber from 'bignumber.js'; -import InputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import styles from './AmountInputSkin.scss'; export const messages = defineMessages({ diff --git a/source/renderer/app/components/wallet/skins/AmountInputSkin.scss b/source/renderer/app/components/wallet/skins/AmountInputSkin.scss index 6a58bca653..d63c749a72 100644 --- a/source/renderer/app/components/wallet/skins/AmountInputSkin.scss +++ b/source/renderer/app/components/wallet/skins/AmountInputSkin.scss @@ -14,7 +14,7 @@ .total { position: absolute; - bottom: 17px; + bottom: 16px; right: 20px; color: var(--theme-input-right-floating-text-color); font-family: var(--font-light); diff --git a/source/renderer/app/components/wallet/skins/etc/AmountInputSkin.js b/source/renderer/app/components/wallet/skins/etc/AmountInputSkin.js deleted file mode 100644 index 94712ce82a..0000000000 --- a/source/renderer/app/components/wallet/skins/etc/AmountInputSkin.js +++ /dev/null @@ -1,44 +0,0 @@ -import React, { Component } from 'react'; -import { intlShape } from 'react-intl'; -import BigNumber from 'bignumber.js'; -import InputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; -import styles from './AmountInputSkinEtc.scss'; -import { formattedAmountWithoutTrailingZeros } from '../../../../utils/formatters'; -import { messages } from '../AmountInputSkin'; - -type Props = { - currency: string, - fees: ?BigNumber, - total: ?BigNumber, - error: boolean, -}; - -export default class AmountInputSkin extends Component { - - static contextTypes = { - intl: intlShape.isRequired, - }; - - render() { - const { error, fees, total, currency } = this.props; - const { intl } = this.context; - - const formattedFees = formattedAmountWithoutTrailingZeros(fees || ''); - const formattedTotal = formattedAmountWithoutTrailingZeros(total || ''); - - return ( -
- - {!error && ( - - {intl.formatMessage(messages.feesLabel, { amount: formattedFees })} - - )} - - {total && !error && `= ${formattedTotal} ${currency}`} - -
- ); - } - -} diff --git a/source/renderer/app/components/wallet/skins/etc/AmountInputSkinEtc.scss b/source/renderer/app/components/wallet/skins/etc/AmountInputSkinEtc.scss deleted file mode 100644 index 4bf41c98ce..0000000000 --- a/source/renderer/app/components/wallet/skins/etc/AmountInputSkinEtc.scss +++ /dev/null @@ -1,26 +0,0 @@ -.root { - position: relative; -} - -.fees, -.total { - color: var(--theme-input-right-floating-text-color); - font-family: var(--font-light); - font-size: 14px; - user-select: auto; -} - -.fees { - position: absolute; - right: 19px; - top: 7px; -} - -.total { - display: block; - line-height: 1.38; - margin: 9px 19px 0; - text-align: right; - text-transform: uppercase; - word-break: break-all; -} diff --git a/source/renderer/app/components/wallet/summary/WalletSummary.js b/source/renderer/app/components/wallet/summary/WalletSummary.js index 39416e6339..519ff79e1f 100644 --- a/source/renderer/app/components/wallet/summary/WalletSummary.js +++ b/source/renderer/app/components/wallet/summary/WalletSummary.js @@ -34,6 +34,7 @@ type Props = { numberOfTransactions: number, pendingAmount: UnconfirmedAmount, isLoadingTransactions: boolean, + isRestoreActive: boolean, }; @observer @@ -49,7 +50,8 @@ export default class WalletSummary extends Component { amount, pendingAmount, numberOfTransactions, - isLoadingTransactions + isLoadingTransactions, + isRestoreActive, } = this.props; const { intl } = this.context; return ( @@ -60,20 +62,26 @@ export default class WalletSummary extends Component { {amount} - {pendingAmount.incoming.greaterThan(0) && -
- {`${intl.formatMessage(messages.pendingIncomingConfirmationLabel)}`} - : {pendingAmount.incoming.toFormat(DECIMAL_PLACES_IN_ADA)} - -
- } - {pendingAmount.outgoing.greaterThan(0) && -
- {`${intl.formatMessage(messages.pendingOutgoingConfirmationLabel)}`} - : {pendingAmount.outgoing.toFormat(DECIMAL_PLACES_IN_ADA)} - + + {!isRestoreActive ? ( +
+ {pendingAmount.incoming.greaterThan(0) && +
+ {`${intl.formatMessage(messages.pendingIncomingConfirmationLabel)}`} + : {pendingAmount.incoming.toFormat(DECIMAL_PLACES_IN_ADA)} + +
+ } + {pendingAmount.outgoing.greaterThan(0) && +
+ {`${intl.formatMessage(messages.pendingOutgoingConfirmationLabel)}`} + : {pendingAmount.outgoing.toFormat(DECIMAL_PLACES_IN_ADA)} + +
+ }
- } + ) : null} + {!isLoadingTransactions ? (
{intl.formatMessage(messages.transactionsLabel)}: {numberOfTransactions} diff --git a/source/renderer/app/components/wallet/summary/etc/WalletSummary.js b/source/renderer/app/components/wallet/summary/etc/WalletSummary.js deleted file mode 100644 index 2302dd4ac7..0000000000 --- a/source/renderer/app/components/wallet/summary/etc/WalletSummary.js +++ /dev/null @@ -1,36 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import SVGInline from 'react-svg-inline'; -import etcSymbol from '../../../../assets/images/etc-logo.inline.svg'; -import BorderedBox from '../../../widgets/BorderedBox'; -import styles from '../WalletSummary.scss'; - -type Props = { - walletName: string, - amountInteger: string, - amountDecimal: string, -}; - -@observer -export default class WalletSummary extends Component { - - render() { - const { - walletName, amountInteger, amountDecimal, - } = this.props; - return ( -
- -
{walletName}
-
- {amountInteger} - .{amountDecimal} - -
-
-
- ); - } - -} diff --git a/source/renderer/app/components/wallet/transactions/Transaction.js b/source/renderer/app/components/wallet/transactions/Transaction.js index eb8959de30..eba9ac2169 100644 --- a/source/renderer/app/components/wallet/transactions/Transaction.js +++ b/source/renderer/app/components/wallet/transactions/Transaction.js @@ -6,13 +6,16 @@ import classNames from 'classnames'; import styles from './Transaction.scss'; import TransactionTypeIcon from './TransactionTypeIcon'; import adaSymbol from '../../../assets/images/ada-symbol.inline.svg'; -import etcSymbol from '../../../assets/images/etc-symbol.inline.svg'; -import WalletTransaction, { transactionStates, transactionTypes } from '../../../domains/WalletTransaction'; -import { assuranceLevels } from '../../../types/transactionAssuranceTypes'; -import { environmentSpecificMessages } from '../../../i18n/global-messages'; -import type { TransactionState } from '../../../domains/WalletTransaction'; -import environment from '../../../../../common/environment'; -import { getNetworkExplorerUrl } from '../../../utils/ada/network'; +import arrow from '../../../assets/images/collapse-arrow.inline.svg'; +import WalletTransaction, +{ + TxnAssuranceLevelOptions, + transactionStates, + transactionTypes +} from '../../../domains/WalletTransaction'; +import globalMessages from '../../../i18n/global-messages'; +import type { TransactionState } from '../../../api/transactions/types'; +import { getNetworkExplorerUrl } from '../../../utils/network'; const messages = defineMessages({ card: { @@ -88,17 +91,17 @@ const messages = defineMessages({ }); const assuranceLevelTranslations = defineMessages({ - [assuranceLevels.LOW]: { + [TxnAssuranceLevelOptions.LOW]: { id: 'wallet.transaction.assuranceLevel.low', defaultMessage: '!!!low', description: 'Transaction assurance level "low".', }, - [assuranceLevels.MEDIUM]: { + [TxnAssuranceLevelOptions.MEDIUM]: { id: 'wallet.transaction.assuranceLevel.medium', defaultMessage: '!!!medium', description: 'Transaction assurance level "medium".', }, - [assuranceLevels.HIGH]: { + [TxnAssuranceLevelOptions.HIGH]: { id: 'wallet.transaction.assuranceLevel.high', defaultMessage: '!!!high', description: 'Transaction assurance level "high".', @@ -122,6 +125,7 @@ type Props = { data: WalletTransaction, state: TransactionState, assuranceLevel: string, + isRestoreActive: boolean, isLastInList: boolean, formattedWalletAmount: Function, onOpenExternalLink: ?Function, @@ -146,7 +150,7 @@ export default class Transaction extends Component { } handleOpenExplorer(type, param, e) { - if (this.props.onOpenExternalLink && environment.isAdaApi()) { + if (this.props.onOpenExternalLink) { e.stopPropagation(); const link = `${getNetworkExplorerUrl()}/${type}/${param}`; this.props.onOpenExternalLink(link); @@ -157,11 +161,12 @@ export default class Transaction extends Component { const { data, isLastInList, state, assuranceLevel, formattedWalletAmount, onOpenExternalLink, + isRestoreActive, } = this.props; const { isExpanded } = this.state; const { intl } = this.context; - const canOpenExplorer = onOpenExternalLink && environment.isAdaApi(); + const canOpenExplorer = onOpenExternalLink; const hasConfirmations = data.numberOfConfirmations > 0; const isFailedTransaction = state === transactionStates.FAILED; @@ -178,28 +183,48 @@ export default class Transaction extends Component { const contentStyles = classNames([ styles.content, - isLastInList ? styles.last : null + isLastInList ? styles.last : null, + isExpanded ? styles.contentExpanded : null ]); const detailsStyles = classNames([ styles.details, canOpenExplorer ? styles.clickable : null, - isExpanded ? styles.expanded : styles.closed + isExpanded ? styles.detailsExpanded : styles.detailsClosed + ]); + + const arrowStyles = classNames([ + styles.arrow, + isExpanded ? styles.arrowExpanded : null ]); const status = intl.formatMessage(assuranceLevelTranslations[assuranceLevel]); - const currency = intl.formatMessage(environmentSpecificMessages[environment.API].currency); - const symbol = environment.isAdaApi() ? adaSymbol : etcSymbol; + const currency = intl.formatMessage(globalMessages.currency); + const symbol = adaSymbol; + + const transactionStateTag = () => { + if (isRestoreActive) return; + return ( + (transactionState === transactionStates.OK) ? ( +
{status}
+ ) : ( +
+ {intl.formatMessage(stateTranslations[transactionState])} +
+ ) + ); + }; return (
- -
+
@@ -226,21 +251,21 @@ export default class Transaction extends Component { {intl.formatMessage(messages.type, { currency })} , {moment(data.date).format('hh:mm:ss A')}
- - {(transactionState === transactionStates.OK) ? ( -
{status}
- ) : ( -
- {intl.formatMessage(stateTranslations[transactionState])} -
- )} + {transactionStateTag()}
{/* ==== Toggleable Transaction Details ==== */} -
-
+
+
event.stopPropagation()} + role="presentation" + aria-hidden + > {data.exchange && data.conversionRate && (
@@ -255,9 +280,7 @@ export default class Transaction extends Component { )}

- {intl.formatMessage(messages[ - environment.isEtcApi() ? 'fromAddress' : 'fromAddresses' - ])} + {intl.formatMessage(messages.fromAddresses)}

{data.addresses.from.map((address, addressIndex) => ( { ))}

- {intl.formatMessage(messages[ - environment.isEtcApi() ? 'toAddress' : 'toAddresses' - ])} + {intl.formatMessage(messages.toAddresses)}

{data.addresses.to.map((address, addressIndex) => ( { ))} - {environment.isAdaApi() ? ( -
-

{intl.formatMessage(messages.assuranceLevel)}

- {(transactionState === transactionStates.OK) ? ( - - {status} - . {data.numberOfConfirmations} {intl.formatMessage(messages.confirmations)}. - - ) : null} -
- ) : null} - - {environment.isEtcApi() ? ( -
-

{intl.formatMessage(messages.transactionAmount)}

+
+

{intl.formatMessage(messages.assuranceLevel)}

+ {!isRestoreActive && (transactionState === transactionStates.OK) ? ( - { - // show currency and use long format (e.g. in ETC show all decimal places) - formattedWalletAmount(data.amount, true, true) - } + {status}.  + {data.numberOfConfirmations.toLocaleString()}  + {intl.formatMessage(messages.confirmations)}. -
- ) : null} + ) : null} +

{intl.formatMessage(messages.transactionId)}

{
*/}
+
-
); } diff --git a/source/renderer/app/components/wallet/transactions/Transaction.scss b/source/renderer/app/components/wallet/transactions/Transaction.scss index 8f04df3d70..1094d58d1b 100644 --- a/source/renderer/app/components/wallet/transactions/Transaction.scss +++ b/source/renderer/app/components/wallet/transactions/Transaction.scss @@ -1,5 +1,7 @@ .component { color: var(--theme-transactions-list-item-details-color); + position: relative; + cursor: pointer; &.failed { .title, .amount, .details { @@ -22,6 +24,39 @@ -webkit-user-select: none; } +.arrow { + bottom: 10px; + display: none; + float: left; + margin-left: -70px; + margin-top: -20px; + opacity: .1; + position: sticky; + text-align: center; + width: 74px; + z-index: 1; + > svg { + height: 8px; + width: 25px; + path { + stroke: var(--theme-transactions-arrow-stroke-color); + } + } + .component:hover & { + opacity: .2; + } + .content:hover & { + opacity: .1; + &:hover { + opacity: .2; + } + } +} + +.arrowExpanded { + display: block; +} + .togglerContent { flex: 1; margin-left: 20px; @@ -37,18 +72,18 @@ .title, .amount { color: var(--theme-transactions-list-item-details-color); - font-size: 16px; font-family: var(--font-semibold); + font-size: 16px; height: 22px; line-height: 1.38; text-align: left; } .amount { - margin-left: auto; font-family: var(--font-medium); font-size: 16px; letter-spacing: 1px; + margin-left: auto; user-select: auto; } @@ -124,8 +159,13 @@ .content { border-bottom: 1px solid var(--theme-separation-border-color); - margin-left: 84px; - margin-right: 20px; + margin-left: 74px; + margin-right: 10px; +} + +.contentExpanded { + padding-bottom: 20px; + margin-top: -10px; } .last { @@ -134,6 +174,7 @@ .details { height: auto; + cursor: default; * + h2, * + .row { margin-top: 20px; @@ -166,15 +207,15 @@ } } -.closed { +.detailsClosed { max-height: 0; overflow: hidden; padding-bottom: 0; } -.expanded { +.detailsExpanded { max-height: 100%; - padding-bottom: 20px; + padding: 10px 10px 0; } .conversion { diff --git a/source/renderer/app/components/wallet/transactions/WalletTransactionsList.js b/source/renderer/app/components/wallet/transactions/WalletTransactionsList.js index b148f5f5b3..0e9581feb3 100644 --- a/source/renderer/app/components/wallet/transactions/WalletTransactionsList.js +++ b/source/renderer/app/components/wallet/transactions/WalletTransactionsList.js @@ -2,15 +2,15 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; import { defineMessages, intlShape } from 'react-intl'; import moment from 'moment'; import styles from './WalletTransactionsList.scss'; import Transaction from './Transaction'; import WalletTransaction from '../../../domains/WalletTransaction'; import LoadingSpinner from '../../widgets/LoadingSpinner'; -import type { AssuranceMode } from '../../../types/transactionAssuranceTypes'; +import type { WalletAssuranceMode } from '../../../api/wallets/types'; const messages = defineMessages({ today: { @@ -42,7 +42,7 @@ type Props = { isLoadingTransactions: boolean, isRestoreActive?: boolean, hasMoreToLoad: boolean, - assuranceMode: AssuranceMode, + assuranceMode: WalletAssuranceMode, walletId: string, formattedWalletAmount: Function, showMoreTransactionsButton?: boolean, @@ -150,6 +150,7 @@ export default class WalletTransactionsList extends Component {
{ className={buttonClasses} label={intl.formatMessage(messages.showMoreTransactionsButtonLabel)} onClick={this.onShowMoreTransactions.bind(this, walletId)} - skin={} + skin={ButtonSkin} /> }
diff --git a/source/renderer/app/components/wallet/transactions/WalletTransactionsSearch.js b/source/renderer/app/components/wallet/transactions/WalletTransactionsSearch.js index 7d63bd3669..604382bf55 100644 --- a/source/renderer/app/components/wallet/transactions/WalletTransactionsSearch.js +++ b/source/renderer/app/components/wallet/transactions/WalletTransactionsSearch.js @@ -1,8 +1,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import styles from './WalletTransactionsSearch.scss'; const messages = defineMessages({ @@ -36,7 +36,7 @@ export default class WalletTransactionsSearch extends Component { placeholder={intl.formatMessage(messages.searchHint)} value={searchTerm} onChange={onChange} - skin={} + skin={InputSkin} />
); diff --git a/source/renderer/app/components/widgets/BigButtonForDialogs.scss b/source/renderer/app/components/widgets/BigButtonForDialogs.scss index 8377ff69ad..32b1d84c46 100644 --- a/source/renderer/app/components/widgets/BigButtonForDialogs.scss +++ b/source/renderer/app/components/widgets/BigButtonForDialogs.scss @@ -1,13 +1,14 @@ .component { - border-radius: 10px; + align-items: center; background-color: var(--theme-dialog-big-button-background-color); - border: solid 4px var(--theme-dialog-big-button-border-color); + border-radius: 10px; + border: solid 1px var(--theme-dialog-big-button-border-color); + cursor: pointer; display: flex; - flex: 1; flex-direction: column; - align-items: center; + flex: 1; + height: 200px; justify-content: center; - cursor: pointer; margin: 10px; padding: 30px; } diff --git a/source/renderer/app/components/widgets/Dialog.js b/source/renderer/app/components/widgets/Dialog.js index c343ac6dd3..5c63531497 100644 --- a/source/renderer/app/components/widgets/Dialog.js +++ b/source/renderer/app/components/widgets/Dialog.js @@ -1,11 +1,11 @@ import React, { Component } from 'react'; -import _ from 'lodash'; +import { map } from 'lodash'; import classnames from 'classnames'; import type { Node } from 'react'; -import Modal from 'react-polymorph/lib/components/Modal'; -import Button from 'react-polymorph/lib/components/Button'; -import SimpleButtonSkin from 'react-polymorph/lib/skins/simple/raw/ButtonSkin'; -import SimpleModalSkin from 'react-polymorph/lib/skins/simple/raw/ModalSkin'; +import { Modal } from 'react-polymorph/lib/components/Modal'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; +import { ModalSkin } from 'react-polymorph/lib/skins/simple/ModalSkin'; import styles from './Dialog.scss'; type Props = { @@ -17,6 +17,7 @@ type Props = { className?: string, onClose?: Function, closeOnOverlayClick?: boolean, + primaryButtonAutoFocus?: boolean, }; export default class Dialog extends Component { @@ -31,6 +32,7 @@ export default class Dialog extends Component { className, closeButton, backButton, + primaryButtonAutoFocus, } = this.props; return ( @@ -38,7 +40,7 @@ export default class Dialog extends Component { isOpen triggerCloseOnOverlayClick={closeOnOverlayClick} onClose={onClose} - skin={} + skin={ModalSkin} >
@@ -56,7 +58,7 @@ export default class Dialog extends Component { {actions &&
- {_.map(actions, (action, key) => { + {map(actions, (action, key) => { const buttonClasses = classnames([ action.className ? action.className : null, action.primary ? 'primary' : 'flat', @@ -68,7 +70,8 @@ export default class Dialog extends Component { label={action.label} onClick={action.onClick} disabled={action.disabled} - skin={} + skin={ButtonSkin} + autoFocus={action.primary ? primaryButtonAutoFocus : false} /> ); })} diff --git a/source/renderer/app/components/widgets/Dialog.scss b/source/renderer/app/components/widgets/Dialog.scss index 8a3224cfbd..eb2ae1c340 100644 --- a/source/renderer/app/components/widgets/Dialog.scss +++ b/source/renderer/app/components/widgets/Dialog.scss @@ -41,4 +41,5 @@ } } } + } diff --git a/source/renderer/app/components/widgets/DialogCloseButton.js b/source/renderer/app/components/widgets/DialogCloseButton.js index bbac06db58..f532d53219 100644 --- a/source/renderer/app/components/widgets/DialogCloseButton.js +++ b/source/renderer/app/components/widgets/DialogCloseButton.js @@ -6,14 +6,19 @@ import styles from './DialogCloseButton.scss'; type Props = { onClose: Function, icon?: string, + disabled?: boolean }; export default class DialogCloseButton extends Component { render() { - const { onClose, icon } = this.props; + const { onClose, icon, disabled } = this.props; return ( - ); diff --git a/source/renderer/app/components/widgets/DialogCloseButton.scss b/source/renderer/app/components/widgets/DialogCloseButton.scss index cc238043f7..1bb5384482 100644 --- a/source/renderer/app/components/widgets/DialogCloseButton.scss +++ b/source/renderer/app/components/widgets/DialogCloseButton.scss @@ -11,3 +11,9 @@ } } } + +.disabled { + @extend .component; + cursor: default; + opacity: .5; +} diff --git a/source/renderer/app/components/widgets/InlineNotification.js b/source/renderer/app/components/widgets/InlineNotification.js new file mode 100644 index 0000000000..22e6645488 --- /dev/null +++ b/source/renderer/app/components/widgets/InlineNotification.js @@ -0,0 +1,27 @@ +// @flow +import React, { Component } from 'react'; +import classNames from 'classnames'; +import styles from './InlineNotification.scss'; + +type Props = { + show: boolean, + children?: string, +}; + +export default class InlineNotification extends Component { + + render() { + const { show, children } = this.props; + + const notificationMessageStyles = classNames([ + styles.component, + show ? styles.show : null, + ]); + + return ( +
+ {children} +
+ ); + } +} diff --git a/source/renderer/app/components/widgets/InlineNotification.scss b/source/renderer/app/components/widgets/InlineNotification.scss new file mode 100644 index 0000000000..8c5e7e3fd4 --- /dev/null +++ b/source/renderer/app/components/widgets/InlineNotification.scss @@ -0,0 +1,20 @@ +.component { + background-color: var(--theme-notification-message-background-color); + border-radius: 4px; + color: var(--theme-notification-message-text-color); + cursor: default; + float: right; + font-family: var(--font-regular); + font-size: 13px; + height: 22px; + line-height: 21px; + margin-top: -22px; + opacity: 0; + padding: 0 6px; + text-align: center; + transition: opacity .3s ease-out; +} + +.show { + opacity: .7; +} diff --git a/source/renderer/app/components/widgets/forms/AdaCertificateUploadWidget.scss b/source/renderer/app/components/widgets/forms/AdaCertificateUploadWidget.scss index 600e16a71f..9515d02d79 100644 --- a/source/renderer/app/components/widgets/forms/AdaCertificateUploadWidget.scss +++ b/source/renderer/app/components/widgets/forms/AdaCertificateUploadWidget.scss @@ -90,5 +90,3 @@ } } } - - diff --git a/source/renderer/app/components/widgets/forms/InlineEditingDropdown.js b/source/renderer/app/components/widgets/forms/InlineEditingDropdown.js index db044e3d94..5da7079ddd 100644 --- a/source/renderer/app/components/widgets/forms/InlineEditingDropdown.js +++ b/source/renderer/app/components/widgets/forms/InlineEditingDropdown.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; import classnames from 'classnames'; -import Select from 'react-polymorph/lib/components/Select'; -import SelectSkin from 'react-polymorph/lib/skins/simple/raw/SelectSkin'; +import { Select } from 'react-polymorph/lib/components/Select'; +import { SelectSkin } from 'react-polymorph/lib/skins/simple/SelectSkin'; import styles from './InlineEditingDropdown.scss'; const messages = defineMessages({ @@ -63,7 +63,7 @@ export default class InlineEditingDropdown extends Component { value={value} onChange={this.onChange} disabled={!isActive} - skin={} + skin={SelectSkin} /> {successfullyUpdated && ( diff --git a/source/renderer/app/components/widgets/forms/InlineEditingInput.js b/source/renderer/app/components/widgets/forms/InlineEditingInput.js index b04f787d23..7efe8aac23 100644 --- a/source/renderer/app/components/widgets/forms/InlineEditingInput.js +++ b/source/renderer/app/components/widgets/forms/InlineEditingInput.js @@ -3,8 +3,8 @@ import React, { Component, } from 'react'; import { observer } from 'mobx-react'; import { defineMessages, intlShape } from 'react-intl'; import classnames from 'classnames'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; +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'; @@ -119,7 +119,7 @@ export default class InlineEditingInput extends Component { componentDidUpdate() { if (this.props.isActive) { - this.inputField.focus(); + this.inputField && this.inputField.getRef().focus(); } } @@ -154,9 +154,9 @@ export default class InlineEditingInput extends Component { role="presentation" aria-hidden > - { error={isActive ? inputField.error : null} disabled={!isActive} ref={(input) => { this.inputField = input; }} - skin={} + skin={InputSkin} /> {isActive && ( diff --git a/source/renderer/app/components/widgets/forms/InlineEditingInput.scss b/source/renderer/app/components/widgets/forms/InlineEditingInput.scss index c185a5c920..2484f96c45 100644 --- a/source/renderer/app/components/widgets/forms/InlineEditingInput.scss +++ b/source/renderer/app/components/widgets/forms/InlineEditingInput.scss @@ -2,14 +2,10 @@ margin-bottom: 20px; position: relative; - :global { - .SimpleInput_disabled { - .SimpleInput_input { - background-color: var(--theme-input-background-color); - border-color: var(--theme-input-border-color); - color: var(--theme-input-text-color); - } - } + .disabled { + background-color: var(--theme-input-background-color); + border-color: var(--theme-input-border-color); + color: var(--theme-input-text-color); } &.inactive { @@ -21,7 +17,7 @@ } .button { - bottom: 15px; + bottom: 14px; color: var(--theme-label-button-color); cursor: pointer; font-family: var(--font-light); diff --git a/source/renderer/app/components/widgets/forms/MnemonicInputWidget.js b/source/renderer/app/components/widgets/forms/MnemonicInputWidget.js index 7dbf99cbc2..67d2d75f0f 100644 --- a/source/renderer/app/components/widgets/forms/MnemonicInputWidget.js +++ b/source/renderer/app/components/widgets/forms/MnemonicInputWidget.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import styles from './MnemonicInputWidget.scss'; type Props = { @@ -29,7 +29,7 @@ export default class MnemonicInputWidget extends Component { className={styles.input} value={token} onChange={(value) => onTokenChanged(index, value)} - skin={} + skin={InputSkin} /> ))}
diff --git a/source/renderer/app/components/widgets/forms/ReadOnlyInput.js b/source/renderer/app/components/widgets/forms/ReadOnlyInput.js index b75e79a355..078870bc6c 100644 --- a/source/renderer/app/components/widgets/forms/ReadOnlyInput.js +++ b/source/renderer/app/components/widgets/forms/ReadOnlyInput.js @@ -3,8 +3,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; import { intlShape } from 'react-intl'; -import Input from 'react-polymorph/lib/components/Input'; -import SimpleInputSkin from 'react-polymorph/lib/skins/simple/raw/InputSkin'; +import { Input } from 'react-polymorph/lib/components/Input'; +import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; import globalMessages from '../../../i18n/global-messages'; import styles from './ReadOnlyInput.scss'; @@ -41,12 +41,12 @@ export default class ReadOnlyInput extends Component {
} + skin={InputSkin} />