diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 07071d8a06..4c2cb75d02 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -8,10 +8,6 @@ steps: agents: queue: daedalus system: x86_64-darwin - - label: 'daedalus-x86_64-linux' - command: 'scripts/build-installer-unix.sh --build-id $BUILDKITE_BUILD_NUMBER' - agents: - system: x86_64-linux - label: 'daedalus-x86_64-linux-nix' command: 'scripts/build-installer-nix.sh $BUILDKITE_BUILD_NUMBER' agents: @@ -21,7 +17,8 @@ steps: agents: system: x86_64-linux - - label: 'release.nix' - command: 'ci/check-hydra.sh' - agents: - system: x86_64-linux + # TODO: Re-enable once script is fixed + # - label: 'release.nix' + # command: 'ci/check-hydra.sh' + # agents: + # system: x86_64-linux diff --git a/.eslintignore b/.eslintignore index 23fdd0c543..10c9718f18 100755 --- a/.eslintignore +++ b/.eslintignore @@ -5,8 +5,9 @@ main.js logs node_modules translations -./tests -features/tests/e2e/documents/* +tests +tests/paper-wallets/e2e/documents/* +tests/wallets/e2e/documents/* mainnet-genesis-dryrun-with-stakeholders.json source/renderer/app/i18n/locales nodemon.json diff --git a/.eslintrc b/.eslintrc index c8dc3cfe50..532027a40c 100755 --- a/.eslintrc +++ b/.eslintrc @@ -31,6 +31,10 @@ "import/no-dynamic-require": 0, "import/no-extraneous-dependencies": 0, "import/prefer-default-export": 0, + "jsx-a11y/anchor-is-valid": 0, + "jsx-a11y/control-has-associated-label": 0, + "max-classes-per-file": 0, + "no-async-promise-executor": 0, "no-await-in-loop": 0, "no-bitwise": 0, "no-param-reassign": 0, @@ -40,22 +44,31 @@ "no-underscore-dangle": 0, "no-unused-expressions": "warn", "no-use-before-define": 0, + "no-useless-catch": 0, "no-restricted-syntax": 0, "prefer-destructuring": "warn", + "prefer-object-spread": 0, "prefer-promise-reject-errors": "warn", "prefer-template": "warn", "react/button-has-type": 0, + "react/default-props-match-prop-types": 0, "react/destructuring-assignment": 0, + "react/display-name": 0, + "react/jsx-curly-brace-presence": 0, + "react/jsx-filename-extension": 0, + "react/jsx-fragments": 0, "react/jsx-no-bind": "warn", + "react/jsx-props-no-spreading": 0, "react/jsx-wrap-multilines": 0, - "react/jsx-filename-extension": 0, "react/no-access-state-in-setstate": "warn", "react/no-array-index-key": "warn", "react/no-unused-prop-types": "warn", "react/prefer-stateless-function": 0, "react/prop-types": "warn", "react/require-default-props": 0, - "react/sort-comp": 0 + "react/sort-comp": 0, + "react/state-in-constructor": 0, + "react/static-property-placement": 0 }, "plugins": [ "flowtype", diff --git a/.flowconfig b/.flowconfig index 23194b55ff..291c60775a 100755 --- a/.flowconfig +++ b/.flowconfig @@ -8,6 +8,8 @@ /release/.* /git/.* /.cache-loader/.* +/utils/js-launcher/.* +/tests-report/.* [include] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 4f33dc46d1..159c681a70 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -23,7 +23,10 @@ This PR CHANGES. - [ ] PR has been assigned and has appropriate labels (`feature`/`bug`/`chore`, `release-x.x.x`) - [ ] PR is updated to the most recent version of the target branch (and there are no conflicts) -- [ ] PR has a good description that summarizes all changes and shows some screenshots or animated GIFs of important UI changes +- [ ] PR has a good description that summarizes all changes +- [ ] PR has default-sized Daedalus window screenshots or animated GIFs of important UI changes: + - [ ] In English + - [ ] In Japanese - [ ] CHANGELOG entry has been added to the top of the appropriate section (*Features*, *Fixes*, *Chores*) and is linked to the correct PR on GitHub - [ ] Automated tests: All acceptance and unit tests are passing (`yarn test`) - [ ] Manual tests (minimum tests should cover newly added feature/fix): App works correctly in *development* build (`yarn dev`) diff --git a/.gitignore b/.gitignore index 889975bc75..90aaec5750 100755 --- a/.gitignore +++ b/.gitignore @@ -62,8 +62,13 @@ translations/messages translations/reports # 'Screenshots' and 'Paper wallet ceritifcate PDF file' generated by acceptance tests -features/tests/e2e/documents/paper_wallet_certificates/paper-wallet-certificate.pdf -features/tests/e2e/screenshots +tests/paper-wallets/e2e/documents/paper-wallet-certificate.pdf +tests-report/ +-tests-report/.gitkeep + +# Old ignored files +tests/screenshots/ +tests/@rerun.txt # Webpack .cache @@ -75,11 +80,9 @@ 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* @@ -90,3 +93,14 @@ package-lock.json # cardano public keys public.key public.key.lock + +# Development executables +cardano-wallet-jormungandr +cardano-wallet-http-bridge +jcli +/jormungandr +cardano-http-bridge +cardano-node + +# JS Launcher +utils/js-launcher/state diff --git a/.prettierignore b/.prettierignore index 583be1c748..cd386dc5fa 100644 --- a/.prettierignore +++ b/.prettierignore @@ -20,4 +20,6 @@ # Ignore the following special folders and files source/renderer/app/i18n/locales/ -features/tests/e2e/documents/* +source/renderer/app/config/newsfeed-files/ +tests/paper-wallets/e2e/documents/* +tests/wallets/e2e/documents/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 5af4f0987b..f55e0b9f25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,366 @@ Changelog ========= +## 1.0.0 + +### Features + +- Implemented NTP force check ([PR 1996](https://github.com/input-output-hk/daedalus/pull/1996)) + +### Fixes + +- Fixed UI issues on wallet import overlays ([PR 1983](https://github.com/input-output-hk/daedalus/pull/1983)) + +### Chores + +- Rename "Daedalus" to "Daedalus Mainnet" and update state directory path ([PR 1986](https://github.com/input-output-hk/daedalus/pull/1986)) + +## 1.0.0-FC5 + +### Fixes + +- Handle duplicate wallets in import process ([PR 1985](https://github.com/input-output-hk/daedalus/pull/1985), [PR 1989](https://github.com/input-output-hk/daedalus/pull/1989), [PR 1991](https://github.com/input-output-hk/daedalus/pull/1991)) +- Treat wallets with 100% syncing progress as synced wallets ([PR 1984](https://github.com/input-output-hk/daedalus/pull/1984)) +- Fixed `cardano-node` / `jormungandr` and `cardano-wallet` info on the "Diagnostics" screen ([PR 1980](https://github.com/input-output-hk/daedalus/pull/1980)) +- Persist "Blank screen fix" / "--safe-mode" flag between Daedalus restarts ([PR 1979](https://github.com/input-output-hk/daedalus/pull/1979)) +- Track `cardano-node` / `jormungandr` PID and use it for safe shutdowns and improve cardano-launcher error handling ([PR 1972](https://github.com/input-output-hk/daedalus/pull/1972)) +- Fixed "Wallet import" UI/UX issues ([PR 1968](https://github.com/input-output-hk/daedalus/pull/1968)) + +### Chores + +- Updated Daedalus installer file names to contain only Daedalus version, cluster name and build number ([PR 1971](https://github.com/input-output-hk/daedalus/pull/1971)) +- Restore the paragraph about "Automatic wallet migration" on the Daedalus Flight "Splash" screen ([PR 1967](https://github.com/input-output-hk/daedalus/pull/1967)) +- Updated `Electron` and it's related dependencies ([PR 1887](https://github.com/input-output-hk/daedalus/pull/1887)) + +## 1.0.0-FC4 + +### Fixes + +- Fixed active address handling ([PR 1969](https://github.com/input-output-hk/daedalus/pull/1969)) + +## 1.0.0-FC3 + +### Features + +- Implemented "Wallet Import" feature ([PR 1956](https://github.com/input-output-hk/daedalus/pull/1956)) +- Force setting spending password on passwordless wallets ([PR 1957](https://github.com/input-output-hk/daedalus/pull/1957)) + +### Chores + +- Implemented smart error handler ([PR 1962](https://github.com/input-output-hk/daedalus/pull/1962)) +- Separated handling of `cardano-wallet` and `cardano-node` logs ([PR 1960](https://github.com/input-output-hk/daedalus/pull/1960)) +- Refactored and improved automated tests setup ([PR 1912](https://github.com/input-output-hk/daedalus/pull/1912)) +- Updated README ([PR 1953](https://github.com/input-output-hk/daedalus/pull/1953)) +- Updated `cardano-wallet` to revision `7140ff08` which includes `cardano-node` 1.10.1 ([PR 1960](https://github.com/input-output-hk/daedalus/pull/1960)) +- Updated `cardano-wallet` to revision `745aaad67004855a84c51e400c6fa1d10aedb910` with fee estimation fix ([PR 1964](https://github.com/input-output-hk/daedalus/pull/1964)) + +## 1.0.0-FC2 + +### Features + +- Removed "Syncing" screen ([PR 1952](https://github.com/input-output-hk/daedalus/pull/1952)) + +### Fixes + +- Disabled address generation for Yoroi wallets ([PR 1961](https://github.com/input-output-hk/daedalus/pull/1961)) +- Fixed "Restoration" dialog validation ([PR 1951](https://github.com/input-output-hk/daedalus/pull/1951)) +- Fixed the `EPERM` errors thrown in the console upon changing wallet name on the "Wallet Settings" screen ([PR 1944](https://github.com/input-output-hk/daedalus/pull/1944)) + +### Chores + +- Removed counter from Alert dialog if we have only one newsfeed alert to show ([PR 1949](https://github.com/input-output-hk/daedalus/pull/1949)) +- Integrated react-polymorph ScrollBar CSS vars into each theme ([PR 1827](https://github.com/input-output-hk/daedalus/pull/1827)) +- Implemented generation of an Byron wallet address after creating a Byron wallet ([PR 1943](https://github.com/input-output-hk/daedalus/pull/1943)) +- Disabled "Force resync" feature for Byron wallets ([PR 1946](https://github.com/input-output-hk/daedalus/pull/1946)) + +## 1.0.0-FC1 + +### Features + +- Implemented NTP checks ([PR 1925](https://github.com/input-output-hk/daedalus/pull/1925)) +- Disabled "Recovery phrase verification" feature ([PR 1931](https://github.com/input-output-hk/daedalus/pull/1931)) +- Implemented automated wallet migration ([PR 1905](https://github.com/input-output-hk/daedalus/pull/1905), [PR 1922](https://github.com/input-output-hk/daedalus/pull/1922), [PR 1930](https://github.com/input-output-hk/daedalus/pull/1930), [PR 1935](https://github.com/input-output-hk/daedalus/pull/1935), ([PR 1936](https://github.com/input-output-hk/daedalus/pull/1936)) +- Integrated network clock api endpoint ([PR 1918](https://github.com/input-output-hk/daedalus/pull/1918)) +- Implements the Flight release information window ([PR 1917](https://github.com/input-output-hk/daedalus/pull/1917)) +- Added new icon for Daedalus Flight ([PR 1909](https://github.com/input-output-hk/daedalus/pull/1909)) +- Integrated network parameters api endpoint ([PR 1915](https://github.com/input-output-hk/daedalus/pull/1915)) +- Implemented flight candidates changes ([PR 1908](https://github.com/input-output-hk/daedalus/pull/1908)) +- Implemented new "Flight candidate" theme ([PR 1914](https://github.com/input-output-hk/daedalus/pull/1914)) +- Implemented non-responding wallets handling ([PR 1901](https://github.com/input-output-hk/daedalus/pull/1901)) +- Added support for building both `cardano-node` and `jormungandr` installers ([PR 1894](https://github.com/input-output-hk/daedalus/pull/1894)) +- Added support for `cardano-node` "Selfnode" network ([PR 1897](https://github.com/input-output-hk/daedalus/pull/1897)) +- Enabled Byron wallet creation for Haskell node builds ([PR 1895](https://github.com/input-output-hk/daedalus/pull/1895)) +- Enabled all Mainnet Daedalus features for Byron wallets for Haskell node builds ([PR 1895](https://github.com/input-output-hk/daedalus/pull/1895)) +- Enabled changing spending password in "Wallet settings" for Haskell node builds ([PR 1902](https://github.com/input-output-hk/daedalus/pull/1902)) +- Enabled "Send" feature for Haskell node builds ([PR 1902](https://github.com/input-output-hk/daedalus/pull/1902)) +- Disabled transfer funds notification for Haskell node builds ([PR 1902](https://github.com/input-output-hk/daedalus/pull/1902)) +- Disabled Shelley wallets and delegation features for Haskell node builds ([PR 1895](https://github.com/input-output-hk/daedalus/pull/1895)) +- Disabled "Data layer migration" notification for Haskell node builds ([PR 1895](https://github.com/input-output-hk/daedalus/pull/1895)) +- Disabled "Paper wallet certificate" creation for Haskell node builds ([PR 1895](https://github.com/input-output-hk/daedalus/pull/1895)) +- Removed hardware and Shelley wallet restoration options for Haskel node builds ([PR 1895](https://github.com/input-output-hk/daedalus/pull/1895)) +- Integrated new `cardano-launcher` ([PR 1886](https://github.com/input-output-hk/daedalus/pull/1886)) + +### Fixes + +- Fixed Cardano Node version shown on the About screen ([PR 1941](https://github.com/input-output-hk/daedalus/pull/1941)) +- Fixed UI issues across all themes ([PR 1916](https://github.com/input-output-hk/daedalus/pull/1916)) +- Removed the click and mark from spinner component ([PR 1885](https://github.com/input-output-hk/daedalus/pull/1885)) +- Removed locale specific rules from the CSS files ([PR 1871](https://github.com/input-output-hk/daedalus/pull/1871)) + +### Chores + +- Added new "Terms of use" ([PR 1934](https://github.com/input-output-hk/daedalus/pull/1934)) +- Updated About screen ([PR 1928](https://github.com/input-output-hk/daedalus/pull/1928)) +- Configured Daedalus Flight to use it's own newsfeed ([PR 1927](https://github.com/input-output-hk/daedalus/pull/1927)) +- Reduced "connection timeout limit" for Byron Reboot builds to 5 minutes ([PR 1926](https://github.com/input-output-hk/daedalus/pull/1926)) +- Updated Daedalus name and version for the Daedalus Flight 1.0.0-FC1 release ([PR 1910](https://github.com/input-output-hk/daedalus/pull/1910)) +- Enabled "Wallet UTXO distribution" feature for Byron wallets([PR 1913](https://github.com/input-output-hk/daedalus/pull/1913)) +- Enabled Byron wallet name editing ([PR 1911](https://github.com/input-output-hk/daedalus/pull/1911)) +- Updated UI copy ([PR 1907](https://github.com/input-output-hk/daedalus/pull/1907)) +- Updated Byron Haskell address validation ([PR 1902](https://github.com/input-output-hk/daedalus/pull/1902)) +- Updated test configuration to cover Byron features only ([PR 1895](https://github.com/input-output-hk/daedalus/pull/1895)) +- Optimized e2e tests ([PR 1874](https://github.com/input-output-hk/daedalus/pull/1874)) +- Updated `react-polymorph` dependency ([PR 1882](https://github.com/input-output-hk/daedalus/pull/1882)) +- Updated small 3rd party dependencies ([PR 1877](https://github.com/input-output-hk/daedalus/pull/1877), [PR 1924](https://github.com/input-output-hk/daedalus/pull/1924)) +- Updated React dependencies ([PR 1873](https://github.com/input-output-hk/daedalus/pull/1873)) +- Updated Storybook dependencies ([PR 1873](https://github.com/input-output-hk/daedalus/pull/1873)) +- Updated node and yarn dependencies ([PR 1883](https://github.com/input-output-hk/daedalus/pull/1883)) +- Re-enabled theme selection on the "Settings" screen ([PR 1872](https://github.com/input-output-hk/daedalus/pull/1872)) +- Implemented acceptance tests for custom number, date and time formats ([PR 1868](https://github.com/input-output-hk/daedalus/pull/1868)) + +## 2.3.1-ITN1 + +### Fixes + +- Fixed stake pool rank coloring within the stake pool tooltip ([PR 1891](https://github.com/input-output-hk/daedalus/pull/1891)) +- Fixed missing notarization on macOS installers ([PR 1890](https://github.com/input-output-hk/daedalus/pull/1890)) +- Fixed issues with loading stake pools on the "Stake pools" screen ([PR 1888](https://github.com/input-output-hk/daedalus/pull/1888)) + +### Chores + +- Updated `cardano-wallet` to version `2020-03-16` which includes Jormungandr 0.8.14 ([PR 1888](https://github.com/input-output-hk/daedalus/pull/1888)) + +## 2.3.0-ITN1 + +### Fixes + +- Fixed "Decentralization countdown" Storybook story ([PR 1863](https://github.com/input-output-hk/daedalus/pull/1863)) + +### Chores + +- Implemented acceptance tests for "Rewards" screen ([PR 1861](https://github.com/input-output-hk/daedalus/pull/1861)) +- Updated test environment dependencies ([PR 1867](https://github.com/input-output-hk/daedalus/pull/1867)) +- Updated `Flow` and `ESLint` dependencies ([PR 1866](https://github.com/input-output-hk/daedalus/pull/1866)) +- Updated `husky` dependency ([PR 1865](https://github.com/input-output-hk/daedalus/pull/1865)) +- Updated `stylelint` and `stylelint-order` dependencies ([PR 1864](https://github.com/input-output-hk/daedalus/pull/1864)) +- Updated `cardano-wallet` to version `2020-03-03` which includes Jormungandr 0.8.13 ([PR 1870](https://github.com/input-output-hk/daedalus/pull/1870)) + +## 2.2.0-ITN1 + +### Features + +- Added "block-0" to the installers in order to speed up the initial bootstrap phase ([PR 1857](https://github.com/input-output-hk/daedalus/pull/1857)) +- Implemented transactions filtering dialog on wallet "Transactions" screen ([PR 1815](https://github.com/input-output-hk/daedalus/pull/1815)) + +### Fixes + +- Fixed pending delegation preferences handling ([PR 1856](https://github.com/input-output-hk/daedalus/pull/1856)) + +### Chores + +- Implemented unit tests for scrambling and unscrambling mnemonics ([PR 1849](https://github.com/input-output-hk/daedalus/pull/1849)) +- Implemented acceptance tests for wallet delegation ([PR 1814](https://github.com/input-output-hk/daedalus/pull/1814)) +- Improved loading state of the "Stake pools" screen ([PR 1814](https://github.com/input-output-hk/daedalus/pull/1814)) +- Updated `cardano-wallet` to version `2020-02-17` ([PR 1856](https://github.com/input-output-hk/daedalus/pull/1856)) +- Updated `cardano-wallet` to revision `573a7038` ([PR 1862](https://github.com/input-output-hk/daedalus/pull/1862)) + +## 2.1.0-ITN1 + +### Features + +- Added stake pools saturation info and ordering based on desirability ([PR 1826](https://github.com/input-output-hk/daedalus/pull/1826)) +- Implemented "Resync wallet" feature ([PR 1822](https://github.com/input-output-hk/daedalus/pull/1822)) +- Implemented "Hardware wallets" restoration ([PR 1801](https://github.com/input-output-hk/daedalus/pull/1801)) +- Implemented "Yoroi wallets" restoration ([PR 1740](https://github.com/input-output-hk/daedalus/pull/1740)) +- Implemented new menu shortcuts ([PR 1780](https://github.com/input-output-hk/daedalus/pull/1780)) +- Implemented React-Polymorph "Link" component ([PR 1799](https://github.com/input-output-hk/daedalus/pull/1799)) +- Implemented a spinner on Wallet delegation screens for wallets in the restoration process ([PR 1847](https://github.com/input-output-hk/daedalus/pull/1847)) +- Implemented experimental data UI ([PR 1845](https://github.com/input-output-hk/daedalus/pull/1845), [PR 1850](https://github.com/input-output-hk/daedalus/pull/1850)) +- Implemented pending delegation preferences ([PR 1842](https://github.com/input-output-hk/daedalus/pull/1842)) +- Renamed "Profit margin" into "Pool margin" on the stake pool tooltip ([PR 1841](https://github.com/input-output-hk/daedalus/pull/1841)) + +### Fixes + +- Fixed a routing issue after wallet deletion ([PR 1823](https://github.com/input-output-hk/daedalus/pull/1823)) +- Fixed a typo on the "Staking pools" screen ([PR 1785](https://github.com/input-output-hk/daedalus/pull/1785)) +- Fixed a typo in the Daedalus ITN "Terms of Service" ([PR 1809](https://github.com/input-output-hk/daedalus/pull/1809)) +- Fixed handling of duplicated wallet IDs when restoring Yoroi Balance and Rewards wallets from the same wallet recovery phrase ([PR 1805](https://github.com/input-output-hk/daedalus/pull/1805)) +- Fixed stake pool descriptions text clipping on stake pool tooltip ([PR 1832](https://github.com/input-output-hk/daedalus/pull/1832)) +- Fixed "Low disk space" notification not being shown for Incentivized testnet ([PR 1833](https://github.com/input-output-hk/daedalus/pull/1833)) +- Fixed download logs link underline color ([PR 1831](https://github.com/input-output-hk/daedalus/pull/1831)) + +### Chores + +- Improved notification display ([PR 1748](https://github.com/input-output-hk/daedalus/pull/1748)) +- Improved delete wallet text copy ([PR 1819](https://github.com/input-output-hk/daedalus/pull/1819)) +- Improved the paper wallet recovery phrase validation ([PR 1818](https://github.com/input-output-hk/daedalus/pull/1818)) +- Improved network screen with responsive main copy box ([PR 1797](https://github.com/input-output-hk/daedalus/pull/1797)) +- Updated checkboxes, radio buttons and switchers sizes and borders ([PR 1793](https://github.com/input-output-hk/daedalus/pull/1793)) +- Updated `cardano-wallet` to revision `b89cfa19` which includes Jormungandr 0.8.9 ([PR 1834](https://github.com/input-output-hk/daedalus/pull/1834)) +- Updated `cardano-wallet` to revision `23e12d1a` which includes Jormungandr 0.8.7 ([PR 1828](https://github.com/input-output-hk/daedalus/pull/1828)) +- Updated `cardano-wallet` to revision `d188a5fc` ([PR 1825](https://github.com/input-output-hk/daedalus/pull/1825)) +- Updated `cardano-wallet` to revision `e6316404` ([PR 1826](https://github.com/input-output-hk/daedalus/pull/1826)) +- Updated `cardano-wallet` to revision `254575e4` which includes Jormungandr 0.8.6 ([PR 1821](https://github.com/input-output-hk/daedalus/pull/1821)) +- Updated `cardano-wallet` to revision `132a5faf` ([PR 1740](https://github.com/input-output-hk/daedalus/pull/1740)) +- Improved GitHub pull request template ([PR 1843](https://github.com/input-output-hk/daedalus/pull/1843)) +- Removed unused locales and translation files ([PR 1840](https://github.com/input-output-hk/daedalus/pull/1840)) +- Improved acceptance tests setup with "rerun" feature ([PR 1835](https://github.com/input-output-hk/daedalus/pull/1835) +- Implemented acceptance tests for Daedalus Balance wallets ([PR 1816](https://github.com/input-output-hk/daedalus/pull/1816), [PR 1828](https://github.com/input-output-hk/daedalus/pull/1828)) +- Implemented acceptance tests for stake pools loading ([PR 1820](https://github.com/input-output-hk/daedalus/pull/1820)) +- Bumped cardano-wallet dependecy to 4ea622c694768bf61bd5c9d04a6e59fe1de3fd53 ([PR 1851](https://github.com/input-output-hk/daedalus/pull/1851)) + +## 2.0.0-ITN1 + +### Features + +- Added stake pool metadata registry for "SelfNode" network ([PR 1771](https://github.com/input-output-hk/daedalus/pull/1771/)) +- Added list stake pools API endpoint errors handlers ([PR 1765](https://github.com/input-output-hk/daedalus/pull/1765)) +- Added "Terms of use" for the Incentivized Testnet v1 - Rewards network ([PR 1741](https://github.com/input-output-hk/daedalus/pull/1741)) +- Updated stake pool ranking logic to use `apparent_performance` from the Api response ([PR 1757](https://github.com/input-output-hk/daedalus/pull/1757)) +- Integrated V2 API endpoint for join/quit fee estimation ([PR 1752](https://github.com/input-output-hk/daedalus/pull/1752)) +- Integrated stake pool join V2 API endpoint and "Delegation" wizard updated ([PR 1744](https://github.com/input-output-hk/daedalus/pull/1744)) +- Integrated the V2 API endpoint for fetching stake pool data ([PR 1733](https://github.com/input-output-hk/daedalus/pull/1733)) +- Integrated stake pool Quit V2 API endpoint and added "Undelegate" dialog ([PR 1737](https://github.com/input-output-hk/daedalus/pull/1737)) +- Added support for new number formats to the React-Polymorph "Numeric input" on the "Send" screen ([PR 1735](https://github.com/input-output-hk/daedalus/pull/1735)) +- Added script which checks for "integrity" lines in yarn.lock file ([PR 1715](https://github.com/input-output-hk/daedalus/pull/1715)) +- Added new Mainnet and Testnet icons ([PR 1716](https://github.com/input-output-hk/daedalus/pull/1716)) +- Added number of pending transactions to “Wallet Summary” screen ([PR 1705](https://github.com/input-output-hk/daedalus/pull/1705)) + +### Fixes + +- Fixed undelegation issue ([PR 1774](https://github.com/input-output-hk/daedalus/pull/1774)) +- Fixed the display of headlines on the "Network" splash screen ([PR 1770](https://github.com/input-output-hk/daedalus/pull/1770)) +- Fixed spending password error handling ([PR 1767](https://github.com/input-output-hk/daedalus/pull/1767)) +- Fixed an issue where "Balance" wallet notification wasn't offering an option to create a "Rewards" wallet in case none are available ([PR 1761](https://github.com/input-output-hk/daedalus/pull/1761)) +- Fixed the middle ellipsis for different addresses lengths ([PR 1736](https://github.com/input-output-hk/daedalus/pull/1736)) +- Fixed missing "hamburger" icon on wallet menu while there are only legacy wallets in the UI ([PR 1730](https://github.com/input-output-hk/daedalus/pull/1730)) +- Fixed issues with wrong transaction amounts when new number formats are used ([PR 1726](https://github.com/input-output-hk/daedalus/pull/1726)) +- Fixed naming of labels on "Daedalus Diagnostics" screen and status icons on "Loading" screen from "Node..." to "Cardano node..." ([PR 1723](https://github.com/input-output-hk/daedalus/pull/1723)) +- Adjusted the Stake pool logic for ranking color to account for the total number of stake pools ([PR 1719](https://github.com/input-output-hk/daedalus/pull/1719)) +- Fixed styling issues on the "Network info" overlay on all the themes ([PR 1708](https://github.com/input-output-hk/daedalus/pull/1708)) +- Fixed read newsfeed items ID duplication in local storage ([PR 1710](https://github.com/input-output-hk/daedalus/pull/1710)) +- Fixed the `themes:check:createTheme` script and updated the `createTheme` object ([PR 1709](https://github.com/input-output-hk/daedalus/pull/1709)) +- Fixed disabled button background color on all of the themes ([PR 1707](https://github.com/input-output-hk/daedalus/pull/1707)) +- Fixed select language e2e tests ([PR 1702](https://github.com/input-output-hk/daedalus/pull/1702)) +- Fixed yellow frame around the "Diagnostics" dialog ([PR 1699](https://github.com/input-output-hk/daedalus/pull/1699)) +- Fixed the Japanese translation for `timeAgo` in "Wallet settings" stories ([PR 1701](https://github.com/input-output-hk/daedalus/pull/1701)) + +### Chores + +- Updated newsfeed ([PR 1786](https://github.com/input-output-hk/daedalus/pull/1786)) +- Changed Incentivized TestNet "Having trouble syncing" URL to point to one specific to TestNet ([PR 1795](https://github.com/input-output-hk/daedalus/pull/1795)) +- Updated `cardano-wallet` to revision `d3d93ba3` ([PR 1784](https://github.com/input-output-hk/daedalus/pull/1784)) +- Updated `cardano-wallet` to revision `e341d288` ([PR 1779](https://github.com/input-output-hk/daedalus/pull/1779)) +- Reduced stake pools fetching interval to 1 minute ([PR 1779](https://github.com/input-output-hk/daedalus/pull/1779)) +- Updated `cardano-wallet` to version `2019.12.13` ([PR 1775](https://github.com/input-output-hk/daedalus/pull/1775)) +- Updated stake pool details tooltip with profit margin & cost ([PR 1773](https://github.com/input-output-hk/daedalus/pull/1773), [PR 1779](https://github.com/input-output-hk/daedalus/pull/1779)) +- Updated `react-polymorph` to version `0.9.0-rc.26` ([PR 1772](https://github.com/input-output-hk/daedalus/pull/1772)) +- Updated wallet delegation / undelegation ([PR 1766](https://github.com/input-output-hk/daedalus/pull/1766)) +- Updated `webpack` package to version `4.39.1` ([PR 1769](https://github.com/input-output-hk/daedalus/pull/1769)) +- Improved "Delegration Center" epoch countdown to use epoch info from the Api ([PR 1751](https://github.com/input-output-hk/daedalus/pull/1751)) +- Updated `cardano-wallet` to revision `555b5e82` ([PR 1764](https://github.com/input-output-hk/daedalus/pull/1764)) +- Updated rewards screen note and learn more button style ([PR 1760](https://github.com/input-output-hk/daedalus/pull/1760)) +- Updated message for current stake pool being selected ([PR 1758](https://github.com/input-output-hk/daedalus/pull/1758)) +- Updated "Delegation" UI Support portal article URLs ([PR 1759](https://github.com/input-output-hk/daedalus/pull/1759)) +- Updated `serialize-javascript` package dependency ([PR 1756](https://github.com/input-output-hk/daedalus/pull/1756)) +- Updated minimum amount of ada for delegation to be available ([PR 1753](https://github.com/input-output-hk/daedalus/pull/1753)) +- Enabled "Delegation" UI in Daedalus builds ([PR 1750](https://github.com/input-output-hk/daedalus/pull/1750)) +- Updated `cardano-wallet` to revision `d4571952` which includes Jormungandr 0.8.0-rc8 ([PR 1749](https://github.com/input-output-hk/daedalus/pull/1749)) +- Improved the unit and e2e test setup ([PR 1743](https://github.com/input-output-hk/daedalus/pull/1743)) +- Updated to react-polymorph@0.9.0-rc.25 which includes a theme var for checkbox icon color (([PR 1742](https://github.com/input-output-hk/daedalus/pull/1742))) +- Updated `cardano-wallet` to revision `833f9d4e` which includes Jormungandr 0.8.0-rc7 ([PR 1739](https://github.com/input-output-hk/daedalus/pull/1739)) +- Updated stake header info component and adds new information with the countdown timer for next epoch ([PR 1729](https://github.com/input-output-hk/daedalus/pull/1729)) +- Removed "StakePool" from Wallet domain ([PR 1738](https://github.com/input-output-hk/daedalus/pull/1738)) +- Refactored wallet navigation to use new React-Polymorph Dropdown component ([PR 1593](https://github.com/input-output-hk/daedalus/pull/1593)) +- Improved error messages on the "Send" screen ([PR 1724](https://github.com/input-output-hk/daedalus/pull/1724)) +- Improved menu items while adding external link icon on menu items which open external links ([PR 1727](https://github.com/input-output-hk/daedalus/pull/1727), [PR 1728](https://github.com/input-output-hk/daedalus/pull/1728)) +- Updated `cardano-js` package to version 0.2.2 and improved address validation ([PR 1712](https://github.com/input-output-hk/daedalus/pull/1712)) +- Removed unused dependencies ([PR 1706](https://github.com/input-output-hk/daedalus/pull/1706)) +- Improved the error message shown on the "Change password" dialog when too short current password is submitted ([PR 1703](https://github.com/input-output-hk/daedalus/pull/1703)) + +## 1.1.0-ITN0 + +### Features + +- Implemented the new Wallet Receive screen ([PR 1700](https://github.com/input-output-hk/daedalus/pull/1700)) +- Added Japanese "Terms of use" text ([PR 1691](https://github.com/input-output-hk/daedalus/pull/1691)) + +### Fixes + +- Fixed Wallet name length UI issues on smaller screen sizes ([PR 1689](https://github.com/input-output-hk/daedalus/pull/1689)) +- Fixed "Number of words in your recovery phrase" default state on "Restore a wallet" dialog ([PR 1692](https://github.com/input-output-hk/daedalus/pull/1692)) +- Fixed "Verify wallet recovery phrase" button text vertical centering ([PR 1693](https://github.com/input-output-hk/daedalus/pull/1693)) +- Disabled "Latest version check" call and fixed "Legacy" wallet ordering after wallet restoration ([PR 1690](https://github.com/input-output-hk/daedalus/pull/1690)) +- Increased Sync and Report sync-issue threshold to 10 mins ([PR 1697](https://github.com/input-output-hk/daedalus/pull/1697)) + +### Chores + +- Bumped Daedalus version to "1.1.0-ITN0" ([PR 1695](https://github.com/input-output-hk/daedalus/pull/1695)) + +## 1.0.0-ITN0 + +### Features + +- Added the Cardano Explorer URL's for ITN ([PR 1674](https://github.com/input-output-hk/daedalus/pull/1674)) +- Hide legacy wallet notification during wallet restoration / syncing ([PR 1667](https://github.com/input-output-hk/daedalus/pull/1667)) +- Implemented the necessary UI changes for the Incentivized Testnet network ([PR 1657](https://github.com/input-output-hk/daedalus/pull/1657)) +- Implemented syncing and connecting screens for the Incentivized Testnet network ([PR 1673](https://github.com/input-output-hk/daedalus/pull/1673)) +- Implemented cancel pending transaction V2 API endpoint and UI for legacy wallets ([PR 1651](https://github.com/input-output-hk/daedalus/pull/1651)) +- Implemented cancel pending transaction V2 API endpoint and UI ([PR 1633](https://github.com/input-output-hk/daedalus/pull/1633)) +- Implemented "Transfer funds" wizard for Incentivized Testnet version of Daedalus ([PR 1634](https://github.com/input-output-hk/daedalus/pull/1634), [PR 1659](https://github.com/input-output-hk/daedalus/pull/1659), [PR 1660](https://github.com/input-output-hk/daedalus/pull/1660)) +- Implemented "Network info" overlay ([PR 1655](https://github.com/input-output-hk/daedalus/pull/1655), [PR 1676](https://github.com/input-output-hk/daedalus/pull/1676)) +- Disable "Manual update" notification for Incentivized Testnet version of Daedalus ([PR 1652](https://github.com/input-output-hk/daedalus/pull/1652)) +- Update rewards screen for incentivized testnet ([PR 1643](https://github.com/input-output-hk/daedalus/pull/1643)) +- Implement restoration of both 12 and 15 mnemonic words phrases in Wallet Restore dialog ([PR 1629](https://github.com/input-output-hk/daedalus/pull/1629)) +- Replace sidebar "Bug-report" icon with a "Network" badge ([PR 1622](https://github.com/input-output-hk/daedalus/pull/1622)) +- Implemented "Spending password" as required parameter for Cardano V2 API ([PR 1631](https://github.com/input-output-hk/daedalus/pull/1631)) +- Implemented "Incentivized Testnet" theme for Incentivized Testnet version of Daedalus ([PR 1620](https://github.com/input-output-hk/daedalus/pull/1620)) +- Removed "Decentralization countdown", "Decentralization info", and "Staking epochs" screens for Incentivized Testnet Daedalus version ([PR 1625](https://github.com/input-output-hk/daedalus/pull/1625)) +- Implemented address validator ([PR 1609](https://github.com/input-output-hk/daedalus/pull/1609), [PR 1618](https://github.com/input-output-hk/daedalus/pull/1618)) +- Add "frontend-only" mode utility that has no dependency on nix ([PR 1583](https://github.com/input-output-hk/daedalus/pull/1583)) +- Added Jormungandr support for Cardano V2 API ([PR 1567](https://github.com/input-output-hk/daedalus/pull/1567) +- Integrated Cardano V2 API endpoints ([PR 1548](https://github.com/input-output-hk/daedalus/pull/1548), [PR 1551](https://github.com/input-output-hk/daedalus/pull/1551), [PR 1552](https://github.com/input-output-hk/daedalus/pull/1552), [PR 1553](https://github.com/input-output-hk/daedalus/pull/1553), [PR 1555](https://github.com/input-output-hk/daedalus/pull/1555), [PR 1556](https://github.com/input-output-hk/daedalus/pull/1556), [PR 1557](https://github.com/input-output-hk/daedalus/pull/1557), [PR 1558](https://github.com/input-output-hk/daedalus/pull/1558), [PR 1559](https://github.com/input-output-hk/daedalus/pull/1559), [PR 1560](https://github.com/input-output-hk/daedalus/pull/1560), [PR 1575](https://github.com/input-output-hk/daedalus/pull/1575), [PR 1577](https://github.com/input-output-hk/daedalus/pull/1577), [PR 1579](https://github.com/input-output-hk/daedalus/pull/1579), [PR 1604](https://github.com/input-output-hk/daedalus/pull/1604), [PR 1613](https://github.com/input-output-hk/daedalus/pull/1613), [PR 1637](https://github.com/input-output-hk/daedalus/pull/1637), [PR 1639](https://github.com/input-output-hk/daedalus/pull/1639), [PR 1641](https://github.com/input-output-hk/daedalus/pull/1641)) +- Add internal link support in newsfeed items and verification hash generator script ([PR 1617](https://github.com/input-output-hk/daedalus/pull/1617)) +- Implemented date, time and number format user options ([PR 1611](https://github.com/input-output-hk/daedalus/pull/1611)) + +### Chores + +- Daedalus copy updates for Incentivized Testnet - Balance check ([PR 1680](https://github.com/input-output-hk/daedalus/pull/1680)) +- Added "Terms of use" for Incentivized Testnet version of Daedalus ([PR 1664](https://github.com/input-output-hk/daedalus/pull/1664)) +- Added Legacy Wallet UI changes ([PR 1647](https://github.com/input-output-hk/daedalus/pull/1647)) +- Removed wallet recovery phrase verification feature for Incentivized Testnet version of Daedalus ([PR 1645](https://github.com/input-output-hk/daedalus/pull/1645)) +- Changed wallet restoration notification message ([PR 1644](https://github.com/input-output-hk/daedalus/pull/1644)) +- Removed parallel wallet restoration limitation ([PR 1638](https://github.com/input-output-hk/daedalus/pull/1638)) +- Disabled create a paper wallet certificate feature ([PR 1640](https://github.com/input-output-hk/daedalus/pull/1640)) +- Removed all notions of account indexes from the codebase ([PR 1614](https://github.com/input-output-hk/daedalus/pull/1614)) +- Removed "Block consolidation status" dialog ([PR 1610](https://github.com/input-output-hk/daedalus/pull/1610)) +- Fixed build mode of webpack auto dll plugin ([PR 1606](https://github.com/input-output-hk/daedalus/pull/1606)) +- Changed delete wallet button layout for emphasized location/importance and removed export wallet feature ([PR 1612](https://github.com/input-output-hk/daedalus/pull/1612), [PR 1619](https://github.com/input-output-hk/daedalus/pull/1619)) +- Speedup storybook builds in development ([PR 1607](https://github.com/input-output-hk/daedalus/pull/1607)) +- Added note to UTXO screen showing pending transactions ([PR 1589](https://github.com/input-output-hk/daedalus/pull/1589)) +- Fixed broken source maps ([PR 1594](https://github.com/input-output-hk/daedalus/pull/1594)) +- Reorganized Storybook by domain ([PR 1537](https://github.com/input-output-hk/daedalus/pull/1537)) +- Reorganized Tests by domain ([PR 1540](https://github.com/input-output-hk/daedalus/pull/1540)) + +### Fixes + +- Fixed incentivized testnet theme ([PR 1677](https://github.com/input-output-hk/daedalus/pull/1677), [PR 1684](https://github.com/input-output-hk/daedalus/pull/1684)) +- Fixed wrong "Jormugandr" process name ([PR 1669](https://github.com/input-output-hk/daedalus/pull/1669)) +- Fixed paper wallet certificate restoration ([PR 1055](https://github.com/input-output-hk/daedalus/pull/1055)) +- Reduce layout re-renderings ([PR 1595](https://github.com/input-output-hk/daedalus/pull/1595)) +- Fixed green Cardano theme white color and borders color ([PR 1584](https://github.com/input-output-hk/daedalus/pull/1584)) +- Fixed flat button color ([PR 1586](https://github.com/input-output-hk/daedalus/pull/1586)) + ## 0.15.1 ### Fixes @@ -11,8 +371,8 @@ Changelog ### Features -- Implemented "Newsfeed" feature ([PR 1570](https://github.com/input-output-hk/daedalus/pull/1570), [PR 1578](https://github.com/input-output-hk/daedalus/pull/1578), [PR 1580](https://github.com/input-output-hk/daedalus/pull/1580)) -- Implemented "Wallet recovery phrase verification" feature ([PR 1565](https://github.com/input-output-hk/daedalus/pull/1565), [PR 1623](https://github.com/input-output-hk/daedalus/pull/1623) +- Implemented "Newsfeed" feature ([PR 1570](https://github.com/input-output-hk/daedalus/pull/1570)) +- Implemented "Wallet recovery phrase verification" feature ([PR 1565](https://github.com/input-output-hk/daedalus/pull/1565)) - Implemented new "Automated update" notification design ([PR 1491](https://github.com/input-output-hk/daedalus/pull/1491)) - Improved "Wallets" list scrollbar UX ([PR 1475](https://github.com/input-output-hk/daedalus/pull/1475)) - Removed "Ada Redemption" feature ([PR 1510](https://github.com/input-output-hk/daedalus/pull/1510)) diff --git a/README.md b/README.md index 0e71e156f3..bcb0c9a811 100644 --- a/README.md +++ b/README.md @@ -7,48 +7,20 @@ [![Windows build status](https://ci.appveyor.com/api/projects/status/github/input-output-hk/daedalus?branch=master&svg=true)](https://ci.appveyor.com/project/input-output/daedalus) [![Release](https://img.shields.io/github/release/input-output-hk/daedalus.svg)](https://github.com/input-output-hk/daedalus/releases) -Daedalus - cryptocurrency wallet +Daedalus - Cryptocurrency Wallet -## Automated build +## Installation -### CI/dev build scripts - -Platform-specific build scripts facilitate building Daedalus the way it is built -by the IOHK CI: - -#### Linux/macOS - -This script requires [Nix](https://nixos.org/nix/), (optionally) -configured with the [IOHK binary cache][cache]. +### Yarn - scripts/build-installer-unix.sh [OPTIONS..] - -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 - -This will use nix to build a Linux installer. Using the [IOHK binary -cache][cache] will speed things up. - - nix build -f ./release.nix mainnet.installer - -The result can be found at `./result/daedalus-*.bin`. +[Yarn](https://yarnpkg.com/lang/en/docs/install) is required to install `npm` dependencies to build Daedalus. -# Development +### Nix -`shell.nix` provides a way to load a shell with all the correct versions of all the -required dependencies for development. +[Nix](https://nixos.org/nix/) is needed to run Daedalus in `nix-shell`. -## Connect to staging cluster: - -1. Start the nix-shell with staging environment `yarn nix:staging` -2. Within the nix-shell run any command like `yarn dev` - -## Connect to Local Demo Cluster: - -### Build and Run cardano-sl Demo Cluster +**Note:** There are special instructions for +[installing Nix on macOS Catalina](https://github.com/NixOS/nix/issues/2925#issuecomment-564149154). 1. Install nix: `curl https://nixos.org/nix/install | sh` 2. Employ the signed IOHK binary cache: @@ -58,105 +30,89 @@ required dependencies for development. ``` and then add the following lines: ``` - sandbox = true - extra-sandbox-paths = /System/Library/Frameworks 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:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= max-jobs = 2 # run at most two builds at once cores = 0 # the builder will use all available CPU cores + extra-sandbox-paths = /System/Library/Frameworks ``` -3. Build and run demo cluster: `scripts/launch/demo-nix.sh` +3. Run `nix-shell` with correct list of arguments or by using existing `package.json` scripts to load a shell with all the correct versions of all the required dependencies for development. -### Start Daedalus Using Demo Cluster +## Development -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` +### Running Daedalus with Cardano Node -## "Frontend only" mode +#### Selfnode -The `frontendOnlyMode` makes it possible to connect to manually started instances of cardano-node for advanced debugging purposes. +1. Run `yarn nix:selfnode` from `daedalus`. +2. Run `yarn dev` from the subsequent `nix-shell` +3. Once Daedalus has started, and has gotten past the loading screen, run `yarn byron:wallet:importer` from a new terminal window. This is only required if you wish to import some funded wallets. It is also possible to import funded Yoroi wallets by running `yarn yoroi:wallet:importer` script. -### How to connect: -1. Within the [cardano-sl repository](https://github.com/input-output-hk/cardano-sl), build a script for a certain network. E.g. for testnet: `nix-build -A connectScripts.testnet.wallet -o launch_testnet` -2. Launch this cluster + node with `./launch_testnet` -3. You should now have a `state-wallet-testnet` folder inside the cardano-sl repo. Copy the full path to the sub folder `tls` in there. -4. Within the Daedalus repo checkout this branch and run: `CARDANO_TLS_PATH=/path/to/tls CARDANO_HOST=localhost CARDANO_PORT=8090 nix-shell` +#### Mainnet -Now you should have a pre-configured nix-shell session where you can `yarn dev` as usual and Daedalus connects itself to the manually started cardano node. +1. Run `yarn nix:mainnet` from `daedalus`. +2. Run `yarn dev` from the subsequent `nix-shell` -### Parameters: +#### Flight -| Param | Mandatory | Default | -|--------------------|-----------|-------------| -| `CARDANO_TLS_PATH` | Yes | | -| `CARDANO_HOST` | No | `localhost` | -| `CARDANO_PORT` | No | `8090` | +1. Run `yarn nix:flight` from `daedalus`. +2. Run `yarn dev` from the subsequent `nix-shell` -So if you just start the default cardano node (which runs on localhost:8090) you can also start nix-shell with `CARDANO_TLS_PATH=/path/to/tls nix-shell` +#### Testnet +1. Run `yarn nix:testnet` from `daedalus`. +2. Run `yarn dev` from the subsequent `nix-shell` -## Notes: +#### Staging -`shell.nix` also provides a script for updating yarn.lock. Run `nix-shell -A fixYarnLock` -to update `yarn.lock` file. - -### 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 -$ export NETWORK=testnet -$ yarn dev -``` +1. Run `yarn nix:staging` from `daedalus`. +2. Run `yarn dev` from the subsequent `nix-shell` -### Cardano Wallet API documentation +### Running Daedalus with Jormungandr -While running Daedalus in development mode you can access Cardano Wallet API documentation on the following URL: https://localhost:8091/docs/v1/index/. +#### ITN Selfnode -# Testing +1. Run `yarn nix:itn_selfnode` from `daedalus`. +2. Run `yarn dev` from the subsequent `nix-shell` +3. Once Daedalus has started, and has gotten past the loading screen, run `yarn itn:shelley:wallet:importer` from a new terminal window. This is only required if you wish to import some funded wallets. It is also possible to import funded legacy wallets by running `yarn itn:byron:wallet:importer` script. -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. +#### ITN Rewards V1 -**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! +1. Run `yarn nix:itn` from `daedalus`. +2. Run `yarn dev` from the subsequent `nix-shell` -## Wallet fault injection +#### QA Testnet -General information about wallet fault injection can be found in the [Cardano's wallet-new README file](https://github.com/input-output-hk/cardano-sl/tree/develop/wallet-new#fault-injection). +1. Run `yarn nix:qa` from `daedalus`. +2. Run `yarn dev` from the subsequent `nix-shell` -`shell.nix` has support for passing the necessary flags: +#### Nightly Testnet -- `--arg allowFaultInjection true` is necessary to enable any processing of faults, and -- `--arg walletExtraArgs '[ "--somefault" ]'` can be used for enabling certain fault types at startup. +1. Run `yarn nix:nightly` from `daedalus`. +2. Run `yarn dev` from the subsequent `nix-shell` -# Windows +### Updating upstream dependencies (cardano-wallet, cardano-node & Jormungandr) -This batch file requires [Node.js](https://nodejs.org/en/download/) and -[7zip](https://www.7-zip.org/download.html). +`Niv` is used to manage the version of upstream dependencies. The versions of these dependencies can be seen in `nix/sources.json`. - scripts/build-installer-win64.bat +Dependencies are updated with the follow nix commands: +- Update to the latest master: `nix-shell -A devops --run "niv update cardano-wallet"` +- Update to a specific revision: `nix-shell -A devops --run "niv update cardano-wallet -a rev=1988f22895c45e12506ec83da0496ebdcdd17719"` -The result will can be found at `.\daedalus-*.exe`. +#### Notes -### CSS Modules +`nix-shell` also provides a script for updating `yarn.lock` file: -This boilerplate out of the box is configured to use [css-modules](https://github.com/css-modules/css-modules). + nix-shell -A fixYarnLock -All `.css` file extensions will use css-modules unless it has `.global.css`. +### Cardano Wallet Api documentation -If you need global styles, stylesheets with `.global.css` will not go through the -css-modules loader. e.g. `app.global.css` +Api documentation for edge `cardano-wallet` version: https://input-output-hk.github.io/cardano-wallet/api/edge/ ### Externals -If you use any 3rd party libraries which can't or won't be built with webpack, you must list them in your `webpack.config.base.js`: +If you use any 3rd party libraries which can't or won't be built with webpack, you must list them in your `source/main/webpack.config.js` and/or `source/renderer/webpack.config.js`: ```javascript externals: [ @@ -177,6 +133,14 @@ Make sure to list bootstrap in externals in `webpack.config.base.js` or the app externals: ['bootstrap'] ``` +## Testing + +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. + +**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! + ## Packaging ```bash @@ -197,10 +161,44 @@ $ yarn run package -- --[option] ### Options -- --name, -n: Application name (default: ElectronReact) +- --name, -n: Application name (default: Electron) - --version, -v: Electron version (default: latest version) - --asar, -a: [asar](https://github.com/atom/asar) support (default: false) - --icon, -i: Application icon - --all: pack for all platforms -Use `electron-packager` to pack your app with `--all` options for darwin (osx), linux and win32 (windows) platform. After build, you will find them in `release` folder. Otherwise, you will only find one for your os. +Use `electron-packager` to pack your app with `--all` options for macOS, Linux and Windows platform. After build, you will find them in `release` folder. Otherwise, you will only find one for your OS. + +## Automated builds + +### CI/dev build scripts + +Platform-specific build scripts facilitate building Daedalus the way it is built by the IOHK CI: + +#### Linux/macOS + +This script requires [Nix](https://nixos.org/nix/), (optionally) configured with the [IOHK binary cache][cache]. + + scripts/build-installer-unix.sh [OPTIONS..] + +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 + +#### 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`. + +#### Pure Nix installer build + +This will use nix to build a Linux installer. Using the [IOHK binary +cache][cache] will speed things up. + + nix build -f ./release.nix mainnet.installer + +The result can be found at `./result/daedalus-*.bin`. diff --git a/default.nix b/default.nix index 68d135fb8a..8c681328d4 100644 --- a/default.nix +++ b/default.nix @@ -1,11 +1,11 @@ let - localLib = import ./lib.nix; - system = builtins.currentSystem; # todo + itn_clusters = [ "itn_rewards_v1" "qa" "nightly" "itn_selfnode" ]; + getDefaultBackend = cluster: if (builtins.elem cluster itn_clusters) then "jormungandr" else "cardano"; in { target ? builtins.currentSystem -#system ? builtins.currentSystem +, nodeImplementation ? (getDefaultBackend cluster) +, localLib ? import ./lib.nix { inherit nodeImplementation; } , config ? {} -, pkgs ? localLib.iohkNix.getPkgs { inherit system config; } , cluster ? "mainnet" , version ? "versionNotSet" , buildNum ? null @@ -13,61 +13,77 @@ in , signingKeys ? null , HSMServer ? null , fudgeConfig ? null +, devShell ? false +, useLocalNode ? false }: let + systemTable = { + x86_64-windows = builtins.currentSystem; + }; + crossSystemTable = lib: { + x86_64-windows = lib.systems.examples.mingwW64; + }; + system = systemTable.${target} or target; + pkgs = localLib.iohkNix.getPkgsDefault { inherit system config; }; + pkgsNative = localLib.iohkNix.getPkgsDefault {}; + sources = localLib.sources; + walletPkgs = import "${sources.cardano-wallet}/nix" {}; + # only used for CLI, to be removed when upgraded to next node version + nodePkgs = import "${sources.cardano-node}/nix" {}; + shellPkgs = (import "${sources.cardano-shell}/nix/iohk-common.nix").getPkgs {}; + inherit (pkgs.lib) optionalString optional concatStringsSep; + inherit (pkgs) writeTextFile; + crossSystem = lib: (crossSystemTable lib).${target} or null; # TODO, nsis cant cross-compile with the nixpkgs daedalus currently uses - nsisNixPkgs = import (pkgs.fetchFromGitHub { - owner = "nixos"; - repo = "nixpkgs"; - rev = "be445a9074f"; - sha256 = "15dc7gdspimavcwyw9nif4s59v79gk18rwsafylffs9m1ld2dxwa"; - }) {}; + nsisNixPkgs = import localLib.sources.nixpkgs-nsis {}; installPath = ".daedalus"; - lib = pkgs.lib; - cardanoSL = localLib.cardanoSL { inherit target config; }; - cardanoJSON = builtins.fromJSON (builtins.readFile ./cardano-sl-src.json); - cardanoSrc = pkgs.fetchFromGitHub { - owner = "input-output-hk"; - repo = "cardano-sl"; - rev = cardanoJSON.rev; - sha256 = cardanoJSON.sha256; - }; needSignedBinaries = (signingKeys != null) || (HSMServer != null); buildNumSuffix = if buildNum == null then "" else ("-${builtins.toString buildNum}"); - cleanSourceFilter = with pkgs.stdenv; - name: type: let baseName = baseNameOf (toString name); in ! ( - # Filter out .git repo - (type == "directory" && baseName == ".git") || - # Filter out editor backup / swap files. - lib.hasSuffix "~" baseName || - builtins.match "^\\.sw[a-z]$" baseName != null || - builtins.match "^\\..*\\.sw[a-z]$" baseName != null || - - # Filter out locally generated/downloaded things. - baseName == "dist" || - baseName == "node_modules" || - - # Filter out the files which I'm editing often. - lib.hasSuffix ".nix" baseName || - lib.hasSuffix ".dhall" baseName || - lib.hasSuffix ".hs" baseName || - # Filter out nix-build result symlinks - (type == "symlink" && lib.hasPrefix "result" baseName) - ); throwSystem = throw "Unsupported system: ${pkgs.stdenv.hostPlatform.system}"; - ghcWithCardano = cardanoSL.haskellPackages.ghcWithPackages (ps: [ ps.cardano-sl ps.cardano-sl-x509 ]); - daedalus-bridge = cardanoSL.daedalus-bridge.overrideAttrs (oldAttrs: { - buildCommand = '' - ${oldAttrs.buildCommand} - cp ${./installers/cardano-configuration.yaml} $out/config/configuration.yaml - ''; - }); + ostable.x86_64-windows = "windows"; + ostable.x86_64-linux = "linux"; + ostable.x86_64-darwin = "macos64"; packages = self: { - inherit cluster pkgs version daedalus-bridge; + inherit cluster pkgs version target nodeImplementation; + jormungandrLib = localLib.iohkNix.jormungandrLib; + cardanoLib = localLib.iohkNix.cardanoLib; + daedalus-bridge = self.bridgeTable.${nodeImplementation}; + export-wallets = self.cardano-sl.nix-tools.cexes.cardano-wallet.export-wallets; + + nodejs = pkgs.nodejs-12_x; + yarnInfo = { + version = "1.22.4"; + hash = "1l3sv30g61dcn7ls213prcja2y3dqdi5apq9r7yyick295w25npq"; + }; + yarn = (pkgs.yarn.override { inherit (self) nodejs; }) /*.overrideAttrs (old: { + version = self.yarnInfo.version; + src = pkgs.fetchFromGitHub { + owner = "yarnpkg"; + repo = "yarn"; + rev = "v${self.yarnInfo.version}"; + sha256 = self.yarnInfo.hash; + }; + })*/; + + sources = localLib.sources; + bridgeTable = { + jormungandr = self.callPackage ./nix/jormungandr-bridge.nix {}; + cardano = self.callPackage ./nix/cardano-bridge.nix {}; + }; + cardano-wallet = import self.sources.cardano-wallet { inherit system; gitrev = self.sources.cardano-wallet.rev; crossSystem = crossSystem walletPkgs.lib; }; + cardano-wallet-native = import self.sources.cardano-wallet { inherit system; gitrev = self.sources.cardano-wallet.rev; }; + cardano-shell = import self.sources.cardano-shell { inherit system; crossSystem = crossSystem shellPkgs.lib; }; + cardano-cli = (import self.sources.cardano-node { inherit system; crossSystem = crossSystem nodePkgs.lib; }).haskellPackages.cardano-node.components.exes.cardano-cli; + cardano-node = if useLocalNode + then (import self.sources.cardano-node { inherit system; crossSystem = crossSystem nodePkgs.lib; }).haskellPackages.cardano-node.components.exes.cardano-node + else self.cardano-wallet.cardano-node; + cardano-sl = import self.sources.cardano-sl { inherit target; gitrev = self.sources.cardano-sl.rev; }; # a cross-compiled fastlist for the ps-list package - fastlist = pkgs.pkgsCross.mingwW64.callPackage ./fastlist.nix {}; + fastlist = pkgs.pkgsCross.mingwW64.callPackage ./nix/fastlist.nix {}; + wine = pkgs.wine.override { wineBuild = "wine32"; }; + wine64 = pkgs.wine.override { wineBuild = "wineWow"; }; dlls = pkgs.fetchurl { url = "https://s3.eu-central-1.amazonaws.com/daedalus-ci-binaries/DLLs.zip"; @@ -75,9 +91,23 @@ let }; # the native makensis binary, with cross-compiled windows stubs - nsis = nsisNixPkgs.callPackage ./nsis.nix {}; + nsis = nsisNixPkgs.callPackage ./nix/nsis.nix {}; + + launcherConfigs = self.callPackage ./nix/launcher-config.nix { + inherit (self) jormungandrLib; + inherit devShell; + network = cluster; + os = ostable.${target}; + backend = nodeImplementation; + runCommandNative = pkgsNative.runCommand; + }; - unsignedUnpackedCardano = daedalus-bridge; + itnClustersFile = writeTextFile { + name = "itn-clusters"; + text = concatStringsSep " " itn_clusters; + }; + + unsignedUnpackedCardano = self.daedalus-bridge; # TODO unpackedCardano = if dummyInstaller then self.dummyUnpacked else (if needSignedBinaries then self.signedCardano else self.unsignedUnpackedCardano); signFile = file: let localSigningScript = pkgs.writeScript "signing-script" '' @@ -128,15 +158,18 @@ let # requires --allow-unsafe-native-code-during-evaluation res = builtins.exec [ signingScript ]; in res; - signedCardano = pkgs.runCommand "signed-daedalus-bridge" {} '' + signedCardano = let + copySignedBinaries = let + signAndCopy = bin: '' + cp ${self.signFile "${self.unsignedUnpackedCardano}/bin/${bin}"} bin/${bin} + ''; + in __concatStringsSep "\n" (map signAndCopy self.launcherConfigs.installerConfig.installerWinBinaries); + in pkgs.runCommand "signed-daedalus-bridge" {} '' cp -r ${self.unsignedUnpackedCardano} $out chmod -R +w $out cd $out rm bin/*.exe - cp ${self.signFile "${self.unsignedUnpackedCardano}/bin/cardano-launcher.exe"} bin/cardano-launcher.exe - cp ${self.signFile "${self.unsignedUnpackedCardano}/bin/cardano-node.exe"} bin/cardano-node.exe - cp ${self.signFile "${self.unsignedUnpackedCardano}/bin/cardano-x509-certificates.exe"} bin/cardano-x509-certificates.exe - cp ${self.signFile "${self.unsignedUnpackedCardano}/bin/wallet-extractor.exe"} bin/wallet-extractor.exe + ${copySignedBinaries} ''; dummyUnpacked = pkgs.runCommand "dummy-unpacked-cardano" {} '' mkdir $out @@ -144,21 +177,26 @@ let touch cardano-launcher.exe cardano-node.exe cardano-x509-certificates.exe log-config-prod.yaml configuration.yaml mainnet-genesis.json ''; - nsisFiles = pkgs.runCommand "nsis-files" { buildInputs = [ self.daedalus-installer pkgs.glibcLocales ]; } '' + nsisFiles = pkgs.runCommand "nsis-files" { + buildInputs = [ self.daedalus-installer pkgs.glibcLocales ]; + } '' mkdir installers cp -vir ${./package.json} package.json - cp -vir ${./installers/dhall} installers/dhall cd installers - cp -vi ${self.unpackedCardano}/version version + + echo ${self.daedalus-bridge.wallet-version} > version export LANG=en_US.UTF-8 - make-installer --os win64 -o $out --cluster ${cluster} ${lib.optionalString (buildNum != null) "--build-job ${buildNum}"} buildkite-cross + cp -v ${self.launcherConfigs.configFiles}/* . + make-installer --${nodeImplementation} dummy --os win64 -o $out --cluster ${cluster} ${optionalString (buildNum != null) "--build-job ${buildNum}"} buildkite-cross mkdir $out - cp daedalus.nsi uninstaller.nsi launcher-config.yaml wallet-topology.yaml $out/ + cp -v daedalus.nsi uninstaller.nsi $out/ + cp -v ${self.launcherConfigs.configFiles}/* $out/ + ls -lR $out ''; - unsignedUninstaller = pkgs.runCommand "uninstaller" { buildInputs = [ self.nsis pkgs.winePackages.minimal ]; } '' + unsignedUninstaller = pkgs.runCommand "uninstaller" { buildInputs = [ self.nsis self.wine ]; } '' mkdir home export HOME=$(realpath home) @@ -178,30 +216,25 @@ let uninstaller = if needSignedBinaries then self.signedUninstaller else self.unsignedUninstaller; unsigned-windows-installer = let - mapping = { - mainnet = "Daedalus"; - staging = "Daedalus Staging"; - testnet = "Daedalus Testnet"; - }; - installDir = mapping.${cluster}; + installDir = self.launcherConfigs.installerConfig.spacedName; in pkgs.runCommand "win64-installer-${cluster}" { buildInputs = [ self.daedalus-installer self.nsis pkgs.unzip pkgs.jq self.yaml2json - ] ++ lib.optional (fudgeConfig != null) self.configMutator; + ]; } '' + echo '~~~ Preparing files for installer' mkdir home export HOME=$(realpath home) mkdir -p $out/{nix-support,cfg-files} mkdir installers - cp ${./cardano-sl-src.json} cardano-sl-src.json cp -vir ${./installers/dhall} installers/dhall cp -vir ${./installers/icons} installers/icons cp -vir ${./package.json} package.json chmod -R +w installers cd installers mkdir -pv ../release/win32-x64/ - ${if dummyInstaller then ''mkdir -pv "../release/win32-x64/${installDir}-win32-x64/resources/app/dist/main/"'' else ''cp -r ${self.rawapp-win64} "../release/win32-x64/${installDir}-win32-x64"''} + ${if dummyInstaller then ''mkdir -pv "../release/win32-x64/${installDir}-win32-x64/resources/app/dist/main/"'' else ''cp -rv ${self.rawapp-win64} "../release/win32-x64/${installDir}-win32-x64"''} chmod -R +w "../release/win32-x64/${installDir}-win32-x64" cp -v ${self.fastlist}/bin/fastlist.exe "../release/win32-x64/${installDir}-win32-x64/resources/app/dist/main/fastlist.exe" ln -s ${./installers/nsis_plugins} nsis_plugins @@ -210,11 +243,14 @@ let pushd dlls ${if dummyInstaller then "touch foo" else "unzip ${self.dlls}"} popd - cp -v ${self.unpackedCardano}/{bin,config}/* . + cp -v ${self.unpackedCardano}/bin/* . + cp -v ${self.nsisFiles}/{*.yaml,*.json,daedalus.nsi,*.key,*.cert} . cp ${self.uninstaller}/uninstall.exe ../uninstall.exe - cp -v ${self.nsisFiles}/{daedalus.nsi,wallet-topology.yaml,launcher-config.yaml} . + if [ -f ${self.nsisFiles}/block-0.bin ]; then + cp -v ${self.nsisFiles}/block-0.bin . + fi chmod -R +w . - ${lib.optionalString (fudgeConfig != null) '' + ${optionalString (fudgeConfig != null) '' set -x KEY=$(yaml2json launcher-config.yaml | jq .configuration.key -r) config-mutator configuration.yaml ''${KEY} ${toString fudgeConfig.applicationVersion} > temp @@ -222,16 +258,18 @@ let set +x ''} + echo '~~~ Generating installer' makensis daedalus.nsi -V4 - cp daedalus-*-cardano-sl-*-windows*.exe $out/ + echo '~~~ Copying to $out' + cp daedalus-*-*.exe $out/ cp *.yaml $out/cfg-files/ echo file installer $out/*.exe > $out/nix-support/hydra-build-products ''; signed-windows-installer = let - backend_version = lib.removeSuffix "\n" (builtins.readFile "${self.unpackedCardano}/version"); # TODO, get from a nix expr + backend_version = self.daedalus-bridge.wallet-version; frontend_version = (builtins.fromJSON (builtins.readFile ./package.json)).version; - fullName = "daedalus-${frontend_version}-cardano-sl-${backend_version}-${cluster}-windows${buildNumSuffix}.exe"; # must match to packageFileName in make-installer + fullName = "daedalus-${frontend_version}-${cluster}${buildNumSuffix}.exe"; # must match to packageFileName in make-installer in pkgs.runCommand "signed-windows-installer-${cluster}" {} '' mkdir $out cp -v ${self.signFile "${self.unsigned-windows-installer}/${fullName}"} $out/${fullName} @@ -240,32 +278,31 @@ let ## TODO: move to installers/nix hsDaedalusPkgs = import ./installers { - inherit localLib system daedalus-bridge; + inherit (self) daedalus-bridge; + inherit localLib system; }; daedalus-installer = pkgs.haskell.lib.justStaticExecutables self.hsDaedalusPkgs.daedalus-installer; daedalus = self.callPackage ./installers/nix/linux.nix {}; - configMutator = pkgs.runCommand "configMutator" { buildInputs = [ ghcWithCardano ]; } '' - cp ${./ConfigMutator.hs} ConfigMutator.hs - mkdir -p $out/bin/ - ghc ConfigMutator.hs -o $out/bin/config-mutator - ''; rawapp = self.callPackage ./yarn2nix.nix { inherit buildNum; api = "ada"; - apiVersion = daedalus-bridge.version; + apiVersion = self.daedalus-bridge.wallet-version; + inherit (self.launcherConfigs.installerConfig) spacedName; + inherit (self.launcherConfigs) launcherConfig; + inherit cluster; }; rawapp-win64 = self.rawapp.override { win64 = true; }; - source = builtins.filterSource cleanSourceFilter ./.; + source = builtins.filterSource localLib.cleanSourceFilter ./.; yaml2json = pkgs.haskell.lib.disableCabalFlag pkgs.haskellPackages.yaml "no-exe"; electron4 = pkgs.callPackage ./installers/nix/electron.nix {}; - electron3 = self.electron4.overrideAttrs (old: rec { + electron8 = self.electron4.overrideAttrs (old: rec { name = "electron-${version}"; - version = "3.0.14"; + version = "8.2.2"; src = { x86_64-linux = pkgs.fetchurl { url = "https://github.com/electron/electron/releases/download/v${version}/electron-v${version}-linux-x64.zip"; - sha256 = "0wha13dbb8553h9c7kvpnrjj5c6wizr441s81ynmkfbfybg697p7"; + sha256 = "0sk63i72kg7xixqgdkq4z80ia3ya9cyc15pak8shg4qi605jdnr7"; }; }.${pkgs.stdenv.hostPlatform.system} or throwSystem; }); @@ -281,33 +318,9 @@ let rev = "7f12322399fd87d937355d0fc263d37d798496fc"; sha256 = "07wnmdadchf73p03wk51abzgd3zm2xz5khwadz1ypbvv3cqlzp5m"; }) { nixpkgs = pkgs; }; - desktopItem = pkgs.makeDesktopItem { - name = "Daedalus${if cluster != "mainnet" then "-${cluster}" else ""}"; - exec = "INSERT_PATH_HERE"; - desktopName = "Daedalus${if cluster != "mainnet" then " ${cluster}" else ""}"; - genericName = "Crypto-Currency Wallet"; - categories = "Application;Network;"; - icon = "INSERT_ICON_PATH_HERE"; - }; - iconPath = { - # the target of these paths must not be a symlink - 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; - }; - }; + iconPath = self.launcherConfigs.installerConfig.iconPath; + # used for name of profile, binary and the desktop shortcut + linuxClusterBinName = cluster; namespaceHelper = pkgs.writeScriptBin "namespaceHelper" '' #!/usr/bin/env bash @@ -322,11 +335,19 @@ let cat /etc/resolv.conf > etc/resolv.conf 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 -p LANG -p LANGUAGE -p LC_ALL -p LC_MESSAGES -- /nix/var/nix/profiles/profile-${cluster}/bin/enter-phase2 daedalus + 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 -p LANG -p LANGUAGE -p LC_ALL -p LC_MESSAGES -- /nix/var/nix/profiles/profile-${self.linuxClusterBinName}/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 -p LANG -p LANGUAGE -p LC_ALL -p LC_MESSAGES -- /nix/var/nix/profiles/profile-${cluster}/bin/enter-phase2 bash + 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 -p LANG -p LANGUAGE -p LC_ALL -p LC_MESSAGES -- /nix/var/nix/profiles/profile-${self.linuxClusterBinName}/bin/enter-phase2 bash fi ''; + desktopItem = pkgs.makeDesktopItem { + name = "Daedalus-${self.linuxClusterBinName}"; + exec = "INSERT_PATH_HERE"; + desktopName = "Daedalus ${self.linuxClusterBinName}"; + genericName = "Crypto-Currency Wallet"; + categories = "Application;Network;"; + icon = "INSERT_ICON_PATH_HERE"; + }; postInstall = pkgs.writeScriptBin "post-install" '' #!${pkgs.stdenv.shell} @@ -341,17 +362,16 @@ let echo "in post-install hook" - cp -f ${self.iconPath.${cluster}.large} $DAEDALUS_DIR/icon_large.png - cp -f ${self.iconPath.${cluster}.small} $DAEDALUS_DIR/icon.png + cp -f ${self.iconPath.large} $DAEDALUS_DIR/icon_large.png + cp -f ${self.iconPath.small} $DAEDALUS_DIR/icon.png cp -Lf ${self.namespaceHelper}/bin/namespaceHelper $DAEDALUS_DIR/namespaceHelper mkdir -pv ~/.local/bin ''${XDG_DATA_HOME}/applications - ${pkgs.lib.optionalString (cluster == "mainnet") "cp -Lf ${self.namespaceHelper}/bin/namespaceHelper ~/.local/bin/daedalus"} - cp -Lf ${self.namespaceHelper}/bin/namespaceHelper ~/.local/bin/daedalus-${cluster} + cp -Lf ${self.namespaceHelper}/bin/namespaceHelper ~/.local/bin/daedalus-${self.linuxClusterBinName} 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_large.png+g" \ - > "''${XDG_DATA_HOME}/applications/Daedalus${if cluster != "mainnet" then "-${cluster}" else ""}.desktop" + > "''${XDG_DATA_HOME}/applications/Daedalus-${self.linuxClusterBinName}.desktop" ''; xdg-open = pkgs.writeScriptBin "xdg-open" '' #!${pkgs.stdenv.shell} @@ -369,11 +389,20 @@ let newBundle = let daedalus' = self.daedalus.override { sandboxed = true; }; in (import ./installers/nix/nix-installer.nix { - inherit (self) postInstall preInstall cluster rawapp; + inherit (self) postInstall preInstall linuxClusterBinName rawapp; inherit pkgs; installationSlug = installPath; - installedPackages = [ daedalus' self.postInstall self.namespaceHelper daedalus'.cfg daedalus-bridge daedalus'.daedalus-frontend self.xdg-open ]; + installedPackages = [ daedalus' self.postInstall self.namespaceHelper daedalus'.cfg self.daedalus-bridge daedalus'.daedalus-frontend self.xdg-open ]; nix-bundle = self.nix-bundle; }).installerBundle; - }; + wrappedBundle = let + version = (builtins.fromJSON (builtins.readFile ./package.json)).version; + backend = "cardano-wallet-${nodeImplementation}"; + suffix = if buildNum == null then "" else "-${toString buildNum}"; + fn = "daedalus-${version}-${self.linuxClusterBinName}${suffix}.bin"; + in pkgs.runCommand fn {} '' + mkdir -p $out + cp ${self.newBundle} $out/${fn} + ''; + }; in pkgs.lib.makeScope pkgs.newScope packages diff --git a/features/.eslintrc b/features/.eslintrc deleted file mode 100644 index 0eed3f6a3f..0000000000 --- a/features/.eslintrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "rules": { - "func-names": 0, - "max-len": 0, - "new-cap": [2, { - "capIsNewExceptions": [ - "After", - "AfterAll", - "BeforeAll", - "Before", - "Given", - "When", - "Then" - ] - }] - }, - "globals": { - "daedalus": true - } -} diff --git a/features/README.md b/features/README.md deleted file mode 100644 index 14523556d2..0000000000 --- a/features/README.md +++ /dev/null @@ -1,62 +0,0 @@ -
-Document maintainer: Nikola Glumac
Document status: Active
-
- -# Install Daedalus - -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 -$ yarn install -``` - -# Run unit tests - -Make sure Daedalus is properly installed (see above). - -```bash -$ yarn test:unit -``` - -## Unbound tests - -Unbound tests run as long as you keep them running -(never end except if an error occurs). - -Example: -`yarn test:unit:unbound --tags @mnemonics` -generates and validates mnemonics as long as you keep it -running (the number of executions is updated in the terminal) - -# Run end-to-end tests - -1. Make sure Daedalus is properly installed (see above). -2. 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. -3. Run Daedalus frontend tests: - -```bash -$ cd daedalus/ -$ yarn nix:dev XXX # XXX = cardano system startup time -$ yarn build -$ yarn test:e2e -``` - -# Run all tests - -```bash -$ yarn test -``` - -Once tests are complete you will get a summary of passed/failed tests in the Terminal window. - -## Keeping Daedalus alive after end-to-end 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:e2e -```` diff --git a/features/add-wallet-via-sidebar.feature b/features/add-wallet-via-sidebar.feature deleted file mode 100644 index f9eb4c0559..0000000000 --- a/features/add-wallet-via-sidebar.feature +++ /dev/null @@ -1,55 +0,0 @@ -@e2e -Feature: Add Wallet via Sidebar - - Background: - Given I have completed the basic setup - And I have the following wallets: - | name | - | Test wallet | - - Scenario: Successfully Adding a Wallet - Given The sidebar shows the "wallets" category - When I click on the add wallet button in the sidebar - And I see the add wallet page - And I click on the create wallet button on the add wallet page - 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 | - 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 - And I see the create wallet recovery phrase display dialog - And I note down the recovery phrase - And I submit the create wallet recovery phrase display dialog - And I see the create wallet recovery phrase entry dialog - And I click on recovery phrase mnemonics in correct order - And I click on the "Accept terms" checkboxes - And I submit the create wallet recovery phrase entry dialog - Then I should not see the create wallet recovery phrase entry dialog anymore - And I should have newly created "New wallet" wallet loaded - And I should be on the "New wallet" wallet "summary" screen - - Scenario: Successfully Adding a Wallet with spending password - Given The sidebar shows the "wallets" category - When I click on the add wallet button in the sidebar - And I see the add wallet page - 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 | - 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 - And I see the create wallet recovery phrase display dialog - And I note down the recovery phrase - And I submit the create wallet recovery phrase display dialog - And I see the create wallet recovery phrase entry dialog - And I click on recovery phrase mnemonics in correct order - And I click on the "Accept terms" checkboxes - And I submit the create wallet recovery phrase entry dialog - Then I should not see the create wallet recovery phrase entry dialog anymore - And I should have newly created "New wallet" wallet loaded - And I should be on the "New wallet" wallet "summary" screen diff --git a/features/block-consolidation-page.feature b/features/block-consolidation-page.feature deleted file mode 100644 index e0339ede2a..0000000000 --- a/features/block-consolidation-page.feature +++ /dev/null @@ -1,44 +0,0 @@ -@e2e -Feature: Display Block Consolidation Page - - Background: - Given I have completed the basic setup - - Scenario: Open/close the Block Consolidation Page and ensure epoch consolidation data is rendered correctly - When I open the Block Consolidation Status Dialog - Then the Block Consolidation Status Page is visible - And the page accurately renders the follow explanation of how block consolidation works in file storage: - | message | - | blockConsolidationStatus.description2 | - And the page accurately renders epochs consolidated out of the total in the main blocks graphic - And the page accurately renders epochs consolidated above the progress bar: - | message | - | blockConsolidationStatus.epochsConsolidated | - And the page accurately renders the epoch trailing 2 behind the current epoch above the progress bar: - | message | - | blockConsolidationStatus.epoch | - And the page accurately renders the current epoch signifying the max end of the progress bar: - | message | - | blockConsolidationStatus.epoch | - And the page accurately renders the node's sync progress as a percentage below the progress bar: - | message | - | blockConsolidationStatus.synced | - When I close the Block Consolidation Status Dialog - Then the Block Consolidation Status Page is hidden - - Scenario: Fetch current epoch from Cardano Explorer and ensure epoch consolidation data is rendered correctly - When I set the Node Setting Api Request to return faulty response - And I open the Block Consolidation Status Dialog - Then the Block Consolidation Status Page is visible - And the page hides the node's sync progress as a percentage below the progress bar - And the page renders the progress bar in loading state - When the fallback function returns the current epoch - And the page accurately renders the follow explanation of how block consolidation works in file storage: - | message | - | blockConsolidationStatus.description2 | - And the page accurately renders epochs consolidated out of the total in the main blocks graphic - And the page accurately renders epochs consolidated above the progress bar: - | message | - | blockConsolidationStatus.epochsConsolidated | - When I close the Block Consolidation Status Dialog - Then the Block Consolidation Status Page is hidden diff --git a/features/import-wallet-via-sidebar.feature b/features/import-wallet-via-sidebar.feature deleted file mode 100644 index 00c772d0d3..0000000000 --- a/features/import-wallet-via-sidebar.feature +++ /dev/null @@ -1,47 +0,0 @@ -@e2e -Feature: Import Wallet via Sidebar - - Background: - Given I have completed the basic setup - And I have the following wallets: - | name | - | Test wallet | - - Scenario: Successfully Importing a Wallet - Given The sidebar shows the "wallets" category - When I click on the add wallet button in the sidebar - And I see the add wallet page - And I click on the import wallet button on the add wallet page - And I see the import wallet dialog - 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 "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 "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 - - @skip - Scenario: Successfully Importing a Wallet with spending password - Given The sidebar shows the "wallets" category - When I click on the add wallet button in the sidebar - And I see the add wallet page - And I click on the import wallet button on the add wallet page - And I see the import wallet dialog - And I select a valid wallet import key file - 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 | - 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 "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/navigate-wallet-tabs.feature b/features/navigate-wallet-tabs.feature deleted file mode 100644 index 1ff73a12d0..0000000000 --- a/features/navigate-wallet-tabs.feature +++ /dev/null @@ -1,22 +0,0 @@ -@e2e -Feature: Navigate Wallet Tabs - - Background: - Given I have completed the basic setup - And I have the following wallets: - | name | - | Test wallet | - - Scenario Outline: Switching Between Wallet Tabs - Given I am on the "Test wallet" wallet "" screen - When I click the wallet button - Then I should be on the "Test wallet" wallet "" screen - - Examples: - | FROM | TO | - | summary | send | - | summary | receive | - | send | summary | - | send | receive | - | receive | summary | - | receive | send | diff --git a/features/receive-money.feature b/features/receive-money.feature deleted file mode 100644 index 6f7e71222a..0000000000 --- a/features/receive-money.feature +++ /dev/null @@ -1,30 +0,0 @@ -@e2e -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 deleted file mode 100644 index 6510def383..0000000000 --- a/features/restore-wallet-via-sidebar.feature +++ /dev/null @@ -1,46 +0,0 @@ -@e2e -Feature: Add Wallet via Sidebar - - Background: - Given I have completed the basic setup - And I have the following wallets: - | name | - | Test wallet | - - Scenario: Successfully Restoring a Wallet - Given The sidebar shows the "wallets" category - When I click on the add wallet button in the sidebar - And I see the add wallet page - And I click on the restore wallet button on the add wallet page - 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 | - 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 - And I should have newly created "Restored wallet" wallet loaded - And I should be on the "Restored wallet" wallet "summary" screen - And I should see the restore status notification while restore is running - And I should not see the restore status notification once restore is finished - - Scenario: Successfully Restoring a Wallet with spending password - Given The sidebar shows the "wallets" category - When I click on the add wallet button in the sidebar - And I see the add wallet page - And I click on the restore wallet button on the add wallet page - 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 | - And I enter wallet password in restore wallet dialog: - | 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 - And I should be on the "Restored wallet" wallet "summary" screen - And I should see the restore status notification while restore is running - And I should not see the restore status notification once restore is finished diff --git a/features/tests/e2e/documents/default-wallet.json b/features/tests/e2e/documents/default-wallet.json deleted file mode 100644 index bad1f6d5b4..0000000000 --- a/features/tests/e2e/documents/default-wallet.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "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" -} diff --git a/features/tests/e2e/helpers/add-wallet-page-helpers.js b/features/tests/e2e/helpers/add-wallet-page-helpers.js deleted file mode 100644 index 58a956403b..0000000000 --- a/features/tests/e2e/helpers/add-wallet-page-helpers.js +++ /dev/null @@ -1,11 +0,0 @@ -import { waitAndClick } from './shared-helpers'; - -const ADD_WALLET = '.WalletAdd'; -const IMPORT_WALLET_BUTTON = '.importWalletButton'; - -export default { - waitForVisible: (client, { isHidden } = {}) => - client.waitForVisible(ADD_WALLET, null, isHidden), - clickImportButton: client => - waitAndClick(client, `${ADD_WALLET} ${IMPORT_WALLET_BUTTON}`), -}; diff --git a/features/tests/e2e/helpers/app-helpers.js b/features/tests/e2e/helpers/app-helpers.js deleted file mode 100644 index 0adca54379..0000000000 --- a/features/tests/e2e/helpers/app-helpers.js +++ /dev/null @@ -1,19 +0,0 @@ -// @flow -import type { WebdriverClient } from '../setup/global-types'; -import { getProcessesByName } from '../../../../source/main/utils/processes'; - -export const waitForDaedalusToExit = async ( - client: WebdriverClient, - timeout: number = 61000 -) => { - const daedalusProcessName = - process.platform === 'linux' ? 'electron' : 'Electron'; - return client.waitUntil( - async () => (await getProcessesByName(daedalusProcessName)).length === 0, - timeout - ); -}; - -export const refreshClient = async (client: WebdriverClient) => { - await client.url(`file://${__dirname}/../../../../dist/renderer/index.html`); -}; diff --git a/features/tests/e2e/helpers/cardano-node-helpers.js b/features/tests/e2e/helpers/cardano-node-helpers.js deleted file mode 100644 index c4075450aa..0000000000 --- a/features/tests/e2e/helpers/cardano-node-helpers.js +++ /dev/null @@ -1,15 +0,0 @@ -// @flow -import type { Daedalus, WebdriverClient } from '../setup/global-types'; -import { getProcessesByName } from '../../../../source/main/utils/processes'; - -declare var daedalus: Daedalus; - -export const getCardanoNodeState = async (client: WebdriverClient) => - (await client.execute(() => daedalus.stores.networkStatus.cardanoNodeState)) - .value; - -export const waitForCardanoNodeToExit = async (client: WebdriverClient) => - client.waitUntil( - async () => (await getProcessesByName('cardano-node')).length === 0, - 61000 - ); diff --git a/features/tests/e2e/helpers/data-layer-migration-helpers.js b/features/tests/e2e/helpers/data-layer-migration-helpers.js deleted file mode 100644 index 0981959da1..0000000000 --- a/features/tests/e2e/helpers/data-layer-migration-helpers.js +++ /dev/null @@ -1,19 +0,0 @@ -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/tests/e2e/helpers/dialogs/import-wallet-dialog-helpers.js b/features/tests/e2e/helpers/dialogs/import-wallet-dialog-helpers.js deleted file mode 100644 index f8d9afa72e..0000000000 --- a/features/tests/e2e/helpers/dialogs/import-wallet-dialog-helpers.js +++ /dev/null @@ -1,20 +0,0 @@ -import { expectTextInSelector, waitAndClick } from '../shared-helpers'; - -const IMPORT_WALLET_DIALOG = '.WalletFileImportDialog'; - -export default { - waitForDialog: (client, { isHidden } = {}) => - client.waitForVisible(IMPORT_WALLET_DIALOG, null, isHidden), - selectFile: (client, { filePath }) => - client.chooseFile( - `${IMPORT_WALLET_DIALOG} .FileUploadWidget_dropZone input`, - filePath - ), - clickImport: client => - waitAndClick(client, `${IMPORT_WALLET_DIALOG} .primary`), - expectError: (client, { error }) => - expectTextInSelector(client, { - selector: `${IMPORT_WALLET_DIALOG}_error`, - text: error, - }), -}; diff --git a/features/tests/e2e/helpers/i18n-helpers.js b/features/tests/e2e/helpers/i18n-helpers.js deleted file mode 100644 index d331cf4e17..0000000000 --- a/features/tests/e2e/helpers/i18n-helpers.js +++ /dev/null @@ -1,27 +0,0 @@ -const DEFAULT_LANGUAGE = 'en-US'; - -export default { - formatMessage: async (client, { id, values }) => { - const translation = await client.execute( - (translationId, translationValues) => { - const IntlProvider = require('react-intl').IntlProvider; // eslint-disable-line - const locale = daedalus.stores.profile.currentLocale; - const messages = daedalus.translations; - const intlProvider = new IntlProvider( - { locale, messages: messages[locale] }, - {} - ); - return intlProvider - .getChildContext() - .intl.formatMessage({ id: translationId }, translationValues); - }, - id, - values || {} - ); - return translation.value; - }, - setActiveLanguage: async (client, { language } = {}) => - client.execute(locale => { - daedalus.actions.profile.updateLocale.trigger({ locale }); - }, language || DEFAULT_LANGUAGE), -}; diff --git a/features/tests/e2e/helpers/language-selection-helpers.js b/features/tests/e2e/helpers/language-selection-helpers.js deleted file mode 100644 index bc53148a9d..0000000000 --- a/features/tests/e2e/helpers/language-selection-helpers.js +++ /dev/null @@ -1,14 +0,0 @@ -import i18n from './i18n-helpers'; - -const LANGUAGE_SELECTION_FORM = '.LanguageSelectionForm_component'; - -const languageSelection = { - waitForVisible: async (client, { isHidden } = {}) => - client.waitForVisible(LANGUAGE_SELECTION_FORM, null, isHidden), - ensureLanguageIsSelected: async (client, { language } = {}) => { - await i18n.setActiveLanguage(client, { language }); - await languageSelection.waitForVisible(client, { isHidden: true }); - }, -}; - -export default languageSelection; diff --git a/features/tests/e2e/helpers/notifications-helpers.js b/features/tests/e2e/helpers/notifications-helpers.js deleted file mode 100644 index 70aea2d1b9..0000000000 --- a/features/tests/e2e/helpers/notifications-helpers.js +++ /dev/null @@ -1,13 +0,0 @@ -import { WalletSyncStateTags } from '../../../../source/renderer/app/domains/Wallet'; - -export const isActiveWalletBeingRestored = async client => { - const result = await client.execute( - expectedSyncTag => - daedalus.stores.wallets.active.syncState.tag === expectedSyncTag, - WalletSyncStateTags.RESTORING - ); - return result.value; -}; - -export const waitForActiveRestoreNotification = (client, { isHidden } = {}) => - client.waitForVisible('.ActiveRestoreNotification', null, isHidden); diff --git a/features/tests/e2e/helpers/route-helpers.js b/features/tests/e2e/helpers/route-helpers.js deleted file mode 100644 index 4da5b90c22..0000000000 --- a/features/tests/e2e/helpers/route-helpers.js +++ /dev/null @@ -1,18 +0,0 @@ -export const getCurrentAppRoute = async function() { - const url = (await this.client.url()).value; - return url.substring(url.indexOf('#/') + 1); // return without the hash -}; - -export const waitUntilUrlEquals = function(expectedUrl) { - const context = this; - return context.client.waitUntil(async () => { - const url = await getCurrentAppRoute.call(context); - return url === expectedUrl; - }); -}; - -export const navigateTo = function(requestedRoute) { - return this.client.execute(route => { - daedalus.actions.router.goToRoute.trigger({ route }); - }, requestedRoute); -}; diff --git a/features/tests/e2e/helpers/screenshot.js b/features/tests/e2e/helpers/screenshot.js deleted file mode 100644 index ab615a1599..0000000000 --- a/features/tests/e2e/helpers/screenshot.js +++ /dev/null @@ -1,25 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { generateFileNameWithTimestamp } from '../../../../source/common/utils/files'; -import ensureDirectoryExists from '../../../../source/main/utils/ensureDirectoryExists'; - -export const generateScreenshotFilePath = prefix => { - const filePath = path.resolve(__dirname, '../screenshots', prefix); - const extension = 'png'; - const fileName = generateFileNameWithTimestamp({ prefix, extension }); - 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 => { - // eslint-disable-next-line no-console - console.log(err); - }); -}; diff --git a/features/tests/e2e/helpers/shared-helpers.js b/features/tests/e2e/helpers/shared-helpers.js deleted file mode 100644 index 5e4813b619..0000000000 --- a/features/tests/e2e/helpers/shared-helpers.js +++ /dev/null @@ -1,57 +0,0 @@ -import { expect } from 'chai'; - -export const waitAndClick = async (client, selector, ...waitArgs) => { - await client.waitForVisible(selector, ...waitArgs); - await client.waitForEnabled(selector, ...waitArgs); - return client.click(selector); -}; - -export const expectTextInSelector = async (client, { selector, text }) => { - await client.waitForText(selector); - let textOnScreen = await client.getText(selector); - // The selector could exist multiple times in the DOM - if (typeof textOnScreen === 'string') textOnScreen = [textOnScreen]; - // We only compare the first result - expect(textOnScreen[0]).to.equal(text); -}; - -export const waitUntilTextInSelector = async (client, { selector, text }) => - client.waitUntil(async () => { - await client.waitForText(selector); - let textOnScreen = await client.getText(selector); - // The selector could exist multiple times in the DOM - if (typeof textOnScreen === 'string') textOnScreen = [textOnScreen]; - // We only compare the first result - return textOnScreen[0] === text; - }); - -export const getVisibleElementsForSelector = async ( - client, - selectSelector, - waitSelector = selectSelector, - ...waitArgs -) => { - await client.waitForVisible(waitSelector, ...waitArgs); - return client.elements(selectSelector); -}; - -export const getVisibleElementsCountForSelector = async ( - client, - selectSelector, - waitSelector = selectSelector, - ...waitArgs -) => { - const elements = await getVisibleElementsForSelector( - client, - selectSelector, - waitSelector, - ...waitArgs - ); - return elements.value ? elements.value.length : 0; -}; - -export const getVisibleTextsForSelector = async (client, selector) => { - await client.waitForVisible(selector); - const texts = await client.getText(selector); - return [].concat(texts); -}; diff --git a/features/tests/e2e/helpers/sidebar-helpers.js b/features/tests/e2e/helpers/sidebar-helpers.js deleted file mode 100644 index 4981919339..0000000000 --- a/features/tests/e2e/helpers/sidebar-helpers.js +++ /dev/null @@ -1,15 +0,0 @@ -import { waitAndClick } from './shared-helpers'; - -export default { - activateCategory: async (client, { category }) => { - await client.execute(cat => { - daedalus.actions.sidebar.activateSidebarCategory.trigger({ - category: cat, - showSubMenu: true, - }); - }, `/${category}`); - return client.waitForVisible(`.SidebarCategory_active.${category}`); - }, - clickAddWalletButton: client => - waitAndClick(client, '.SidebarWalletsMenu_addWalletButton'), -}; diff --git a/features/tests/e2e/helpers/terms-of-use-helpers.js b/features/tests/e2e/helpers/terms-of-use-helpers.js deleted file mode 100644 index e387b7c923..0000000000 --- a/features/tests/e2e/helpers/terms-of-use-helpers.js +++ /dev/null @@ -1,14 +0,0 @@ -const TERMS_OF_USE_FORM = '.TermsOfUseForm_component'; - -const termsOfUse = { - waitForVisible: async (client, { isHidden } = {}) => - client.waitForVisible(TERMS_OF_USE_FORM, null, isHidden), - acceptTerms: async client => { - await client.execute(() => { - daedalus.actions.profile.acceptTermsOfUse.trigger(); - }); - await termsOfUse.waitForVisible(client, { isHidden: true }); - }, -}; - -export default termsOfUse; diff --git a/features/tests/e2e/helpers/wallets-helpers.js b/features/tests/e2e/helpers/wallets-helpers.js deleted file mode 100644 index 4718d893bd..0000000000 --- a/features/tests/e2e/helpers/wallets-helpers.js +++ /dev/null @@ -1,169 +0,0 @@ -import { expect } from 'chai'; - -export const getNameOfActiveWalletInSidebar = async function() { - await this.client.waitForVisible('.SidebarWalletMenuItem_active'); - return this.client.getText( - '.SidebarWalletMenuItem_active .SidebarWalletMenuItem_title' - ); -}; - -export const expectActiveWallet = async function(walletName) { - const displayedWalletName = await getNameOfActiveWalletInSidebar.call(this); - expect(displayedWalletName.toLowerCase().trim()).to.equal( - walletName.toLowerCase().trim() - ); -}; - -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.spendingPassword) { - await this.client.setValue( - `${formSelector} .spendingPassword .SimpleInput_input`, - values.spendingPassword - ); - } - this.walletSendFormValues = values; -}; - -export const getWalletByName = function(walletName) { - return this.wallets.find(w => w.name === walletName); -}; - -export const waitUntilWaletNamesEqual = function(walletName) { - const context = this; - return context.client.waitUntil(async () => { - const currentWalletName = await getNameOfActiveWalletInSidebar.call( - context - ); - return currentWalletName === walletName; - }); -}; - -export const waitUntilWalletIsLoaded = async function(walletName) { - let wallet = null; - const context = this; - await context.client.waitUntil(async () => { - const result = await context.client.execute( - name => daedalus.stores.wallets.getWalletByName(name), - walletName - ); - if (result.value) { - wallet = result.value; - return true; - } - return false; - }); - return wallet; -}; - -export const addOrSetWalletsForScenario = function(wallet) { - this.wallet = wallet; - if (this.wallets != null) { - this.wallets.push(this.wallet); - } else { - this.wallets = [this.wallet]; - } -}; - -export const importWalletWithFunds = async ( - client, - { keyFilePath, password } -) => - client.executeAsync( - (filePath, spendingPassword, done) => { - daedalus.api.ada - .importWalletFromKey({ filePath, spendingPassword }) - .then(() => - 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) => { - const mnemonics = {}; - window.Promise.all( - wallets.map(wallet => { - const mnemonic = daedalus.utils.crypto.generateMnemonic(); - mnemonics[wallet.name] = mnemonic.split(' '); - return daedalus.api.ada.createWallet({ - name: wallet.name, - mnemonic, - spendingPassword: wallet.password || null, - }); - }) - ) - .then(() => - daedalus.stores.wallets.walletsRequest - .execute() - .then(storeWallets => - daedalus.stores.wallets - .refreshWalletsData() - .then(() => done({ storeWallets, mnemonics })) - .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.storeWallets); - } else { - context.wallets = result.value.storeWallets; - } - if (context.mnemonics != null) { - context.mnemonics.push(...result.value.mnemonics); - } else { - context.mnemonics = result.value.mnemonics; - } -}; - -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/tests/e2e/setup/global-types.js b/features/tests/e2e/setup/global-types.js deleted file mode 100644 index e573d4a542..0000000000 --- a/features/tests/e2e/setup/global-types.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import type { Api } from '../../../../source/renderer/app/api'; -import type { ActionsMap } from '../../../../source/renderer/app/actions'; -import type { StoresMap } from '../../../../source/renderer/app/stores'; - -export type Daedalus = { - api: Api, - environment: Object, - actions: ActionsMap, - stores: StoresMap, - translations: Object, - reset: Function, -}; - -export type WebdriverExecuteResult = { value: T }; - -export type WebdriverClient = { - execute: (script: Function) => WebdriverExecuteResult, - waitUntil: (script: Function, timeout?: number) => Promise, - url: (url: string) => Promise, -}; diff --git a/features/tests/e2e/setup/i18n.js b/features/tests/e2e/setup/i18n.js deleted file mode 100644 index f9afc7b852..0000000000 --- a/features/tests/e2e/setup/i18n.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Before } from 'cucumber'; - -Before(function() { - this.intl = async (translationId, translationValues = {}) => { - const translation = await this.client.execute( - (id, values) => { - const IntlProvider = require('react-intl').IntlProvider; // eslint-disable-line - const locale = daedalus.stores.profile.currentLocale; - const messages = daedalus.translations; - const intlProvider = new IntlProvider( - { locale, messages: messages[locale] }, - {} - ); - return intlProvider - .getChildContext() - .intl.formatMessage({ id }, values); - }, - translationId, - translationValues - ); - return translation.value; - }; -}); diff --git a/features/tests/e2e/setup/webdriver.js b/features/tests/e2e/setup/webdriver.js deleted file mode 100644 index 33d9b85225..0000000000 --- a/features/tests/e2e/setup/webdriver.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Before } from 'cucumber'; - -Before(function() { - this.waitAndClick = async (selector, ...waitArgs) => { - await this.client.waitForVisible(selector, ...waitArgs); - return this.client.click(selector); - }; -}); diff --git a/features/tests/e2e/steps/block-consolidation-page-steps.js b/features/tests/e2e/steps/block-consolidation-page-steps.js deleted file mode 100644 index 7807b861aa..0000000000 --- a/features/tests/e2e/steps/block-consolidation-page-steps.js +++ /dev/null @@ -1,238 +0,0 @@ -import { When, Then } from 'cucumber'; -import { expect } from 'chai'; -import { getVisibleTextsForSelector } from '../helpers/shared-helpers'; -import i18n from '../helpers/i18n-helpers'; - -const SELECTORS = { - BLOCK_CONSOLIDATION_COMPONENT: '.BlockConsolidationStatus_component', - BLOCK_CONSOLIDATION_EXPLANATION: - '.BlockConsolidationStatus_content p:nth-child(3)', - EPOCHS_CONSOLIDATION_STATUS: '.BlockConsolidationStatus_epochs p span b', - EPOCHS_CONSOLIDATED: - '.BlockConsolidationStatus_indicatorEpochsConsolidated p', - TRAILING_BY_2_EPOCH: '.BlockConsolidationStatus_indicatorEpochsBehind p', - MAXIMUM_EPOCH: '.BlockConsolidationStatus_fullEpoch', - SYNC_PROGRESS: '.BlockConsolidationStatus_indicatorEpochsSynced p span', - SYNC_PROGRESS_LOADING_STATE: - '.BlockConsolidationStatus_indicatorContainerNoCurrentEpochs', -}; - -When(/^I open the Block Consolidation Status Dialog$/, async function() { - await this.client.execute(() => - daedalus.actions.app.openBlockConsolidationStatusDialog.trigger() - ); -}); - -When(/^I close the Block Consolidation Status Dialog$/, async function() { - await this.client.execute(() => - daedalus.actions.app.closeBlockConsolidationStatusDialog.trigger() - ); -}); - -Then(/^the Block Consolidation Status Page is (hidden|visible)/, async function( - state -) { - const isVisible = state === 'visible'; - await this.client.waitForVisible( - SELECTORS.BLOCK_CONSOLIDATION_COMPONENT, - null, - !isVisible - ); -}); - -Then( - /^the page accurately renders the follow explanation of how block consolidation works in file storage:$/, - async function(data) { - const [consolidationText] = data.hashes(); - - let [renderedText] = await getVisibleTextsForSelector( - this.client, - SELECTORS.BLOCK_CONSOLIDATION_EXPLANATION - ); - - const { - value: { currentEpochValue }, - } = await this.client.executeAsync(done => - done({ - currentEpochValue: daedalus.stores.blockConsolidation.currentEpoch, - }) - ); - - let currentEpoch = ''; - let currentEpochBehind = ''; - - if (currentEpochValue && currentEpochValue > 0) { - currentEpoch = `(${currentEpochValue})`; - currentEpochBehind = `(${Math.max(currentEpochValue - 1, 0)})`; - } - - let expectedText = await i18n.formatMessage(this.client, { - id: consolidationText.message, - values: { - currentEpoch, - currentEpochBehind, - }, - }); - - // Removes double spaces caused by missing currentEpoch - renderedText = renderedText.replace(/\s+/g, ' '); - expectedText = expectedText.replace(/\s+/g, ' '); - expect(renderedText).to.equal(expectedText); - } -); - -Then( - /^the page accurately renders epochs consolidated out of the total in the main blocks graphic$/, - async function() { - const [ - renderedEpochsConsolidated, - renderedCurrentEpoch, - ] = await getVisibleTextsForSelector( - this.client, - SELECTORS.EPOCHS_CONSOLIDATION_STATUS - ); - - const { - value: { expectedEpochsConsolidated, expectedCurrentEpochValue }, - } = await this.client.executeAsync(done => - done({ - expectedEpochsConsolidated: - daedalus.stores.blockConsolidation.epochsConsolidated, - expectedCurrentEpochValue: - daedalus.stores.blockConsolidation.currentEpoch, - }) - ); - - expect(parseInt(renderedEpochsConsolidated, 10)).to.equal( - expectedEpochsConsolidated - ); - expect(parseInt(renderedCurrentEpoch, 10)).to.equal( - expectedCurrentEpochValue - ); - } -); - -Then( - /^the page accurately renders epochs consolidated above the progress bar:$/, - async function(data) { - const { - value: { epochsConsolidated }, - } = await this.client.executeAsync(done => - done({ - epochsConsolidated: - daedalus.stores.blockConsolidation.epochsConsolidated, - }) - ); - const [expectedTextData] = data.hashes(); - const expectedTextMessage = await this.intl(expectedTextData.message); - const expectedText = `${epochsConsolidated} ${expectedTextMessage}`; - - const [renderedText] = await getVisibleTextsForSelector( - this.client, - SELECTORS.EPOCHS_CONSOLIDATED - ); - - expect(renderedText).to.equal(expectedText); - } -); - -Then( - /^the page accurately renders the epoch trailing 2 behind the current epoch above the progress bar:$/, - async function(data) { - const { - value: { currentEpoch }, - } = await this.client.executeAsync(done => - done({ currentEpoch: daedalus.stores.blockConsolidation.currentEpoch }) - ); - const epochBehind = Math.max(currentEpoch - 2, 0); - const [expectedTextData] = data.hashes(); - const expectedTextMessage = await this.intl(expectedTextData.message); - const expectedText = `${expectedTextMessage} ${epochBehind}`; - - const [renderedText] = await getVisibleTextsForSelector( - this.client, - SELECTORS.TRAILING_BY_2_EPOCH - ); - - expect(renderedText).to.equal(expectedText); - } -); - -Then( - /^the page accurately renders the current epoch signifying the max end of the progress bar:$/, - async function(data) { - const { - value: { currentEpoch }, - } = await this.client.executeAsync(done => - done({ currentEpoch: daedalus.stores.blockConsolidation.currentEpoch }) - ); - const [expectedTextData] = data.hashes(); - const expectedTextMessage = await this.intl(expectedTextData.message); - const expectedText = `${expectedTextMessage} ${currentEpoch}`; - - const [renderedText] = await getVisibleTextsForSelector( - this.client, - SELECTORS.MAXIMUM_EPOCH - ); - - expect(renderedText).to.equal(expectedText); - } -); - -Then( - /^the page accurately renders the node's sync progress as a percentage below the progress bar:$/, - async function(data) { - const { - value: { epochsSynced }, - } = await this.client.executeAsync(done => { - daedalus.stores.networkStatus - ._updateNetworkStatus() - .then(() => - done({ - epochsSynced: daedalus.stores.networkStatus.syncProgress, - }) - ) - .catch(error => done(error)); - }); - const [expectedTextData] = data.hashes(); - const expectedText = await i18n.formatMessage(this.client, { - id: expectedTextData.message, - values: { epochsSynced }, - }); - - const [renderedText] = await getVisibleTextsForSelector( - this.client, - SELECTORS.SYNC_PROGRESS - ); - - expect(renderedText).to.equal(expectedText); - } -); - -Then( - /^the page hides the node's sync progress as a percentage below the progress bar$/, - async function() { - return this.client.waitForVisible(SELECTORS.SYNC_PROGRESS, null, true); - } -); - -Then(/^the page renders the progress bar in loading state$/, async function() { - return this.client.waitForVisible( - SELECTORS.SYNC_PROGRESS_LOADING_STATE, - null, - true - ); -}); - -When(/^the fallback function returns the current epoch$/, async function() { - return this.client.waitForVisible(SELECTORS.MAXIMUM_EPOCH); -}); - -When( - /^I set the Node Setting Api Request to return faulty response$/, - function() { - return this.client.execute(() => { - daedalus.api.setFaultyNodeSettingsApi = true; - }); - } -); diff --git a/features/tests/e2e/steps/helper-steps.js b/features/tests/e2e/steps/helper-steps.js deleted file mode 100644 index d0141ace1a..0000000000 --- a/features/tests/e2e/steps/helper-steps.js +++ /dev/null @@ -1,51 +0,0 @@ -import { When } from 'cucumber'; -import { - generateScreenshotFilePath, - saveScreenshot, -} from '../helpers/screenshot'; - -const oneHour = 60 * 60 * 1000; -// Helper step to pause execution for up to an hour ;) -When(/^I freeze$/, { timeout: oneHour }, callback => { - setTimeout(callback, oneHour); -}); - -When(/^I take a screenshot named "([^"]*)"$/, async function(testName) { - const file = generateScreenshotFilePath(testName); - await saveScreenshot(this, file); -}); - -When(/^I inject fault named "([^"]*)"$/, async function(faultName) { - await this.client.executeAsync((name, done) => { - daedalus.api.ada - .setCardanoNodeFault([name, true]) - .then(done) - .catch(e => { - throw e; - }); - }, faultName); -}); - -When(/^I trigger the apply-update endpoint$/, async function() { - await this.client.executeAsync(done => { - daedalus.api.ada - .applyUpdate() - .then(done) - .catch(e => { - throw e; - }); - }); -}); - -When(/^I set next update version to "([^"]*)"$/, async function( - applicationVersion -) { - await this.client.executeAsync((version, done) => { - daedalus.api.ada - .setNextUpdate(parseInt(version, 10)) - .then(done) - .catch(e => { - throw e; - }); - }, applicationVersion); -}); diff --git a/features/tests/e2e/steps/receive-steps.js b/features/tests/e2e/steps/receive-steps.js deleted file mode 100644 index 51d1c15f9c..0000000000 --- a/features/tests/e2e/steps/receive-steps.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Given, When, Then } from 'cucumber'; -import { expect } from 'chai'; -import { - waitAndClick, - getVisibleElementsCountForSelector, -} from '../helpers/shared-helpers'; - -Given('I generate {int} addresses', async function(numberOfAddresses) { - for (let i = 0; i < numberOfAddresses; i++) { - await waitAndClick( - this.client, - '.generateAddressButton:not(.WalletReceive_spinning)' - ); - } -}); - -When('I click the ShowUsed switch', async function() { - await waitAndClick(this.client, '.SimpleSwitch_switch'); -}); - -Then('I should see {int} used addresses', { timeout: 60000 }, async function( - numberOfAddresses -) { - const addressesFound = await getVisibleElementsCountForSelector( - this.client, - '.Address_usedWalletAddress', - '.Address_usedWalletAddress', - 60000 - ); - expect(addressesFound).to.equal(numberOfAddresses); -}); - -Then('I should see {int} addresses', async function(numberOfAddresses) { - const addressesFound = await getVisibleElementsCountForSelector( - this.client, - '.Address_component' - ); - 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('.Address_component', '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/tests/e2e/steps/setup-steps.js b/features/tests/e2e/steps/setup-steps.js deleted file mode 100644 index 370a5ebccc..0000000000 --- a/features/tests/e2e/steps/setup-steps.js +++ /dev/null @@ -1,12 +0,0 @@ -import { Given } from 'cucumber'; -import termsOfUse from '../helpers/terms-of-use-helpers'; -import languageSelection from '../helpers/language-selection-helpers'; -import dataLayerMigration from '../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/tests/e2e/steps/transactions-steps.js b/features/tests/e2e/steps/transactions-steps.js deleted file mode 100644 index cea7a72da8..0000000000 --- a/features/tests/e2e/steps/transactions-steps.js +++ /dev/null @@ -1,104 +0,0 @@ -import { Given, When, Then } from 'cucumber'; -import { expect } from 'chai'; -import BigNumber from 'bignumber.js/bignumber'; -import { - DECIMAL_PLACES_IN_ADA, - LOVELACES_PER_ADA, -} from '../../../../source/renderer/app/config/numbersConfig'; -import { getVisibleTextsForSelector } from '../helpers/shared-helpers'; -import { getWalletByName } from '../helpers/wallets-helpers'; - -// This step ensures sequential creation of given transactions -// 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 => ({ - 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 - for (const tx of txData) { - const txResponse = await this.client.executeAsync( - (transaction, done) => - new window.Promise(resolve => - // Need to fetch the wallets data async and wait for all results - window.Promise.all([ - daedalus.stores.addresses.getAccountIndexByWalletId( - transaction.walletId - ), - daedalus.stores.addresses.getAddressesByWalletId( - transaction.destinationWalletId - ), - ]).then(results => - daedalus.api.ada - .createTransaction( - window.Object.assign(transaction, { - accountIndex: results[0], // Account index of sender wallet - address: results[1][0].id, // First address of receiving wallet - }) - ) - .then(resolve) - ) - ).then(done), - tx - ); - this.transactions.push(txResponse); - } - } -); - -Then(/^I should not see any transactions$/, async function() { - await this.client.waitForVisible('.Transaction_component', null, true); -}); - -Then(/^I should see the no recent transactions message$/, async function() { - await this.client.waitForVisible('.WalletNoTransactions_label'); -}); - -Then(/^I should see the following transactions:$/, async function(table) { - // Prepare expected transaction data - const expectedTxs = await Promise.all( - table.hashes().map(async tx => { - let title; - switch (tx.type) { - case 'income': - title = 'wallet.transaction.received'; - break; - case 'expend': - title = 'wallet.transaction.sent'; - break; - default: - throw new Error('unknown transaction type'); - } - return { - title: await this.intl(title, { currency: 'ADA' }), - amount: new BigNumber(tx.amount).toFormat(DECIMAL_PLACES_IN_ADA), - }; - }) - ); - - // Collect data of visible transactions on screen - const txTitles = await getVisibleTextsForSelector( - this.client, - '.Transaction_title' - ); - const txAmounts = await getVisibleTextsForSelector( - this.client, - '.Transaction_amount' - ); - const visibleTxs = txTitles.map((title, index) => ({ - title, - amount: txAmounts[index], - })); - - expect(expectedTxs).to.deep.equal(visibleTxs); -}); - -When(/^I click on the show more transactions button$/, async function() { - await this.waitAndClick('.WalletTransactionsList_showMoreTransactionsButton'); -}); diff --git a/features/tests/e2e/steps/trouble-connecting-notification-steps.js b/features/tests/e2e/steps/trouble-connecting-notification-steps.js deleted file mode 100644 index 0b1867d48c..0000000000 --- a/features/tests/e2e/steps/trouble-connecting-notification-steps.js +++ /dev/null @@ -1,82 +0,0 @@ -import { Then, When } from 'cucumber'; -import { waitUntilTextInSelector } from '../helpers/shared-helpers'; - -const SELECTORS = { - SYNCING_CONNECTING_COMPONENT: '.SyncingConnecting_component', - REPORT_ISSUE_TEXT_H1: '.ReportIssue_reportIssueText', - REPORT_ISSUE_BUTTON: '.ReportIssue_actionButton.reportIssueButton', -}; - -Then(/^I should not see the loading screen$/, async function() { - await this.client.waitForVisible( - SELECTORS.SYNCING_CONNECTING_COMPONENT, - null, - true - ); -}); - -Then( - /^I should see the report issue notification displaying "([^"]*)"$/, - async function(text) { - await waitUntilTextInSelector(this.client, { - selector: SELECTORS.REPORT_ISSUE_TEXT_H1, - text, - }); - } -); - -Then(/^I should not see the report issue notification$/, async function() { - await this.client.waitForVisible(SELECTORS.REPORT_ISSUE_TEXT_H1, null, true); -}); - -Then(/^The report issue button should be (hidden|visible)$/, async function( - state -) { - const waitForHidden = state === 'hidden'; - await this.client.waitForVisible( - SELECTORS.REPORT_ISSUE_BUTTON, - null, - waitForHidden - ); -}); - -When( - /^I set both local and network block heights to a static, equal number$/, - async function() { - await this.client.executeAsync(done => { - daedalus.api.ada - .setNetworkBlockHeight(150) - .then(() => daedalus.api.ada.setLocalBlockHeight(150)) - .then(done) - .catch(error => done(error)); - }); - } -); - -When( - /^I set the node subscription status to (subscribing|subscribed)$/, - async function(state) { - const subscriptionState = - state === 'subscribed' ? { status: 'subscribed' } : {}; - await this.client.executeAsync((subscriptionStatus, done) => { - daedalus.api.ada - .setSubscriptionStatus(subscriptionStatus) - .then(done) - .catch(error => done(error)); - }, subscriptionState); - } -); - -When( - /^I reconnect local and network block heights to the node$/, - async function() { - await this.client.executeAsync(done => { - daedalus.api.ada - .setNetworkBlockHeight(null) - .then(() => daedalus.api.ada.setLocalBlockHeight(null)) - .then(() => daedalus.stores.networkStatus._updateNetworkStatus()) - .then(done) - .catch(error => done(error)); - }); - } -); diff --git a/features/tests/e2e/steps/trouble-syncing-notification-steps.js b/features/tests/e2e/steps/trouble-syncing-notification-steps.js deleted file mode 100644 index f68180537b..0000000000 --- a/features/tests/e2e/steps/trouble-syncing-notification-steps.js +++ /dev/null @@ -1,22 +0,0 @@ -import { When, Then } from 'cucumber'; -import { waitUntilTextInSelector } from '../helpers/shared-helpers'; - -When( - /^I arbitrarily set the local block height to half the network block height$/, - async function() { - await this.client.executeAsync(done => { - daedalus.api.ada - .setLocalBlockHeight(150) - .then(() => daedalus.api.ada.setNetworkBlockHeight(300)) - .then(done) - .catch(error => done(error)); - }); - } -); - -Then(/^I should see the syncing status with "([^"]*)"$/, async function(text) { - await waitUntilTextInSelector(this.client, { - selector: '.SyncingConnectingTitle_syncing h1', - text, - }); -}); diff --git a/features/tests/e2e/steps/wallets-steps.js b/features/tests/e2e/steps/wallets-steps.js deleted file mode 100644 index 76ec20d611..0000000000 --- a/features/tests/e2e/steps/wallets-steps.js +++ /dev/null @@ -1,624 +0,0 @@ -import { Given, When, Then } from 'cucumber'; -import { expect } from 'chai'; -import path from 'path'; -import BigNumber from 'bignumber.js'; -import { - createWallets, - fillOutWalletSendForm, - getWalletByName, - waitUntilWalletIsLoaded, - addOrSetWalletsForScenario, - importWalletWithFunds, -} from '../helpers/wallets-helpers'; -import { waitUntilUrlEquals, navigateTo } from '../helpers/route-helpers'; -import { DECIMAL_PLACES_IN_ADA } from '../../../../source/renderer/app/config/numbersConfig'; -import sidebar from '../helpers/sidebar-helpers'; -import addWalletPage from '../helpers/add-wallet-page-helpers'; -import importWalletDialog from '../helpers/dialogs/import-wallet-dialog-helpers'; -import i18n from '../helpers/i18n-helpers'; -import { - isActiveWalletBeingRestored, - waitForActiveRestoreNotification, -} from '../helpers/notifications-helpers'; - -const defaultWalletKeyFilePath = path.resolve( - __dirname, - '../documents/default-wallet.key' -); -// 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 "Imported Wallet" with funds$/, async function() { - await importWalletWithFunds(this.client, { - keyFilePath: defaultWalletKeyFilePath, - password: null, - }); - const wallet = await waitUntilWalletIsLoaded.call(this, 'Imported Wallet'); - addOrSetWalletsForScenario.call(this, wallet); -}); - -// 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: null, // 'Secret123', - }); - const wallet = await waitUntilWalletIsLoaded.call(this, 'Imported Wallet'); - addOrSetWalletsForScenario.call(this, wallet); - - // 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.wallets - .refreshWalletsData() - .then(done) - .catch(error => done(error)) - ) - .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 -) { - const wallet = getWalletByName.call(this, walletName); - await navigateTo.call(this, `/wallets/${wallet.id}/${screen}`); -}); - -Given(/^I see the add wallet page/, function() { - return addWalletPage.waitForVisible(this.client); -}); - -Given(/^I see delete wallet dialog$/, function() { - return this.client.waitForVisible('.DeleteWalletConfirmationDialog_dialog'); -}); - -Given(/^I see the create wallet dialog$/, function() { - return this.client.waitForVisible('.WalletCreateDialog'); -}); - -Given(/^I see the restore wallet dialog$/, function() { - return this.client.waitForVisible('.WalletRestoreDialog'); -}); - -Given(/^I dont see the create wallet dialog(?: anymore)?$/, function() { - return this.client.waitForVisible('.WalletCreateDialog', null, true); -}); - -Given(/^the active wallet is "([^"]*)"$/, function(walletName) { - const wallet = getWalletByName.call(this, walletName); - this.client.execute(walletId => { - daedalus.actions.setActiveWallet.trigger({ walletId }); - }, wallet.id); -}); - -When(/^I click on the create wallet button on the add wallet page/, function() { - return this.waitAndClick('.WalletAdd .createWalletButton'); -}); - -When(/^I click on the import wallet button on the add wallet page/, function() { - return addWalletPage.clickImportButton(this.client); -}); - -When(/^I see the import wallet dialog$/, function() { - return importWalletDialog.waitForDialog(this.client); -}); - -When(/^I select a valid wallet import key file$/, function() { - this.waitAndClick('.WalletFileImportDialog .FileUploadWidget_dropZone'); -}); - -When( - /^I toggle "Activate to create password" switch on the import wallet key dialog$/, - function() { - return this.waitAndClick('.WalletFileImportDialog .SimpleSwitch_switch'); - } -); - -When(/^I enter wallet spending password:$/, async function(table) { - const fields = table.hashes()[0]; - await this.client.setValue( - '.WalletFileImportDialog .spendingPassword input', - fields.password - ); - await this.client.setValue( - '.WalletFileImportDialog .repeatedPassword input', - fields.repeatedPassword - ); -}); - -When( - /^I click on the import wallet button in import wallet dialog$/, - function() { - return importWalletDialog.clickImport(this.client); - } -); - -When(/^I should see wallet spending password inputs$/, function() { - return this.client.waitForVisible( - '.WalletFileImportDialog .spendingPassword input' - ); -}); - -When(/^I have one wallet address$/, function() { - return this.client.waitForVisible('.generatedAddress-1'); -}); - -When(/^I enter spending password "([^"]*)"$/, function(password) { - return this.client.setValue( - '.WalletReceive_spendingPassword input', - password - ); -}); - -When(/^I click on the "Generate new address" button$/, function() { - return this.client.click('.generateAddressButton'); -}); - -When( - /^I click on the restore wallet button on the add wallet page$/, - function() { - return this.waitAndClick('.WalletAdd .restoreWalletButton'); - } -); - -When(/^I click the wallet (.*) button$/, async function(buttonName) { - const buttonSelector = `.NavButton_component.${buttonName}`; - await this.client.waitForVisible(buttonSelector); - await this.client.click(buttonSelector); -}); - -When(/^I can see the send form$/, function() { - return this.client.waitForVisible('.WalletSendForm'); -}); - -When(/^I fill out the wallet send form with:$/, function(table) { - return fillOutWalletSendForm.call(this, table.hashes()[0]); -}); - -When( - /^I fill out the send form with a transaction to "([^"]*)" wallet:$/, - async function(walletName, table) { - const values = table.hashes()[0]; - const walletId = getWalletByName.call(this, walletName).id; - const walletAddress = await this.client.executeAsync((id, done) => { - daedalus.api.ada - .getAddresses({ walletId: id }) - .then(response => done(response.addresses[0].id)) - .catch(error => done(error)); - }, walletId); - values.address = walletAddress.value; - return fillOutWalletSendForm.call(this, values); - } -); - -When(/^the transaction fees are calculated$/, async function() { - this.fees = await this.client.waitUntil(async () => { - // Expected transactionFeeText format "+ 0.000001 of fees" - const transactionFeeText = await this.client.getText( - '.AmountInputSkin_fees' - ); - const transactionFeeAmount = new BigNumber(transactionFeeText.substr(2, 8)); - return transactionFeeAmount.greaterThan(0) ? transactionFeeAmount : false; - }); -}); - -When(/^I click on the next button in the wallet send form$/, async function() { - const submitButton = '.WalletSendForm_nextButton'; - await this.client.waitForVisible(submitButton); - return this.client.click(submitButton); -}); - -When(/^I see send money confirmation dialog$/, function() { - return this.client.waitForVisible('.WalletSendConfirmationDialog_dialog'); -}); - -When( - /^I enter wallet spending password in confirmation dialog "([^"]*)"$/, - async function(password) { - await this.client.setValue( - '.WalletSendConfirmationDialog_spendingPassword input', - password - ); - } -); - -When(/^I submit the wallet send form$/, async function() { - await this.client.waitForEnabled( - '.WalletSendConfirmationDialog_dialog .confirmButton' - ); - return this.client.click( - '.WalletSendConfirmationDialog_dialog .confirmButton' - ); -}); - -When( - /^I toggle "Spending password" switch on the create wallet dialog$/, - function() { - return this.waitAndClick('.WalletCreateDialog .SimpleSwitch_switch'); - } -); - -When( - /^I toggle "Spending password" switch on the restore wallet dialog$/, - function() { - return this.waitAndClick('.WalletRestoreDialog .SimpleSwitch_switch'); - } -); - -When( - /^I submit the create wallet dialog with the following inputs:$/, - async function(table) { - const fields = table.hashes()[0]; - await this.client.setValue( - '.WalletCreateDialog .walletName input', - fields.walletName - ); - return this.client.click('.WalletCreateDialog .primary'); - } -); - -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 .spendingPassword input', - fields.password - ); - await this.client.setValue( - '.WalletCreateDialog .repeatedPassword input', - fields.repeatedPassword - ); - return this.client.click('.WalletCreateDialog .primary'); - } -); - -When(/^I enter wallet name "([^"]*)" in restore wallet dialog$/, async function( - walletName -) { - return this.client.setValue( - '.WalletRestoreDialog .walletName input', - walletName - ); -}); - -When(/^I enter recovery phrase in restore wallet dialog:$/, async function( - table -) { - const fields = table.hashes()[0]; - const recoveryPhrase = fields.recoveryPhrase.split(' '); - for (let i = 0; i < recoveryPhrase.length; i++) { - const word = recoveryPhrase[i]; - await this.client.setValue( - '.AutocompleteOverrides_autocompleteWrapper input', - word - ); - await this.client.waitForVisible(`//li[text()="${word}"]`); - await this.waitAndClick(`//li[text()="${word}"]`); - await this.client.waitForVisible(`//span[text()="${word}"]`); - } -}); - -When(/^I enter wallet password in restore wallet dialog:$/, async function( - table -) { - const fields = table.hashes()[0]; - await this.client.setValue( - '.WalletRestoreDialog .spendingPassword input', - fields.password - ); - await this.client.setValue( - '.WalletRestoreDialog .repeatedPassword input', - fields.repeatedPassword - ); -}); - -When(/^I submit the restore wallet dialog$/, function() { - return this.client.click('.WalletRestoreDialog .primary'); -}); - -When(/^I see the create wallet privacy dialog$/, function() { - return this.client.waitForVisible('.WalletBackupPrivacyWarningDialog'); -}); - -When( - /^I click on "Please make sure nobody looks your screen" checkbox$/, - function() { - return this.waitAndClick( - '.WalletBackupPrivacyWarningDialog .SimpleCheckbox_root' - ); - } -); - -When(/^I submit the create wallet privacy dialog$/, function() { - return this.waitAndClick('.WalletBackupPrivacyWarningDialog .primary'); -}); - -When(/^I see the create wallet recovery phrase display dialog$/, function() { - return this.client.waitForVisible('.WalletRecoveryPhraseDisplayDialog'); -}); - -When(/^I note down the recovery phrase$/, async function() { - const recoveryPhrase = await this.client.getText( - '.WalletRecoveryPhraseMnemonic_component' - ); - this.recoveryPhrase = recoveryPhrase.split(' '); -}); - -When(/^I submit the create wallet recovery phrase display dialog$/, function() { - return this.waitAndClick('.WalletRecoveryPhraseDisplayDialog .primary'); -}); - -When(/^I see the create wallet recovery phrase entry dialog$/, function() { - return this.client.waitForVisible('.WalletRecoveryPhraseEntryDialog'); -}); - -When( - /^I click on recovery phrase mnemonics in correct order$/, - async function() { - for (let i = 0; i < this.recoveryPhrase.length; i++) { - const word = this.recoveryPhrase[i]; - const selector = 'MnemonicWord_root'; - const disabledSelector = 'MnemonicWord_disabled'; - await this.waitAndClick( - `//button[contains(@class,'${selector}') and not(contains(@class, '${disabledSelector}')) and text()="${word}"]` - ); - } - } -); - -When(/^I click on the "Accept terms" checkboxes$/, async function() { - const termsCheckboxes = await this.client.elements('.SimpleCheckbox_root'); - for (let i = 0; i < termsCheckboxes.value.length; i++) { - const termsCheckbox = termsCheckboxes.value[i].ELEMENT; - await this.client.elementIdClick(termsCheckbox); - } -}); - -When(/^I submit the create wallet recovery phrase entry dialog$/, function() { - return this.waitAndClick('.WalletRecoveryPhraseEntryDialog .primary'); -}); - -When(/^I click on delete wallet button$/, async function() { - return this.client.click('.DeleteWalletButton_button'); -}); - -When(/^I enter "([^"]*)" as name of the wallet to confirm$/, async function( - walletName -) { - return this.client.setValue( - '.DeleteWalletConfirmationDialog_confirmationInput input', - walletName - ); -}); - -When( - /^I click on the "Make sure you have access to backup before continuing" checkbox$/, - function() { - return this.waitAndClick( - '.DeleteWalletConfirmationDialog_dialog .SimpleCheckbox_root' - ); - } -); - -When(/^I submit the delete wallet dialog$/, function() { - return this.client.click('.DeleteWalletConfirmationDialog_dialog .primary'); -}); - -When(/^I try to import the wallet with funds again$/, async function() { - await sidebar.activateCategory(this.client, { category: 'wallets' }); - await sidebar.clickAddWalletButton(this.client); - await addWalletPage.waitForVisible(this.client); - await addWalletPage.clickImportButton(this.client); - this.waitAndClick('.WalletFileImportDialog .FileUploadWidget_dropZone'); - this.waitAndClick('.Dialog_actions button'); -}); - -Then( - /^I see the import wallet dialog with an error that the wallet already exists$/, - async function() { - return importWalletDialog.expectError(this.client, { - error: await i18n.formatMessage(this.client, { - id: 'api.errors.WalletAlreadyImportedError', - }), - }); - } -); - -Then( - /^I should not see the create wallet recovery phrase entry dialog anymore$/, - function() { - return this.client.waitForVisible( - '.WalletRecoveryPhraseEntryDialog', - null, - true - ); - } -); - -Then(/^I should not see the delete wallet dialog anymore$/, function() { - return this.client.waitForVisible( - '.DeleteWalletConfirmationDialog_dialog', - null, - true - ); -}); - -Then(/^I should not see the import wallet dialog anymore$/, function() { - return importWalletDialog.waitForDialog(this.client, { isHidden: true }); -}); - -Then(/^I should not see the restore wallet dialog anymore$/, function() { - return this.client.waitForVisible('.WalletRestoreDialog', null, true); -}); - -Then( - /^I should see the restore status notification while import is running$/, - async function() { - // Only check the rendered DOM if the restore is still in progress - if (await isActiveWalletBeingRestored(this.client)) { - await waitForActiveRestoreNotification(this.client); - } - } -); - -Then( - /^I should not see the restore status notification once import is finished$/, - async function() { - await waitForActiveRestoreNotification(this.client, { isHidden: true }); - } -); - -Then( - /^I should see the restore status notification while restore is running$/, - async function() { - // Only check the rendered DOM if the restore is still in progress - if (await isActiveWalletBeingRestored(this.client)) { - await waitForActiveRestoreNotification(this.client); - } - } -); - -Then( - /^I should not see the restore status notification once restore is finished$/, - async function() { - await waitForActiveRestoreNotification(this.client, { isHidden: true }); - } -); - -Then(/^I should have newly created "([^"]*)" wallet loaded$/, async function( - walletName -) { - const result = await this.client.executeAsync(done => { - daedalus.stores.wallets.walletsRequest - .execute() - .then(done) - .catch(error => done(error)); - }); - // Add or set the wallets for this scenario - if (this.wallets != null) { - this.wallets.push(...result.value); - } else { - this.wallets = result.value; - } - const wallet = getWalletByName.call(this, walletName); - expect(wallet).to.be.an('object'); -}); - -Then(/^I should be on some wallet page$/, async function() { - return this.client.waitForVisible('.Navigation_component'); -}); - -Then(/^I should be on the "([^"]*)" wallet "([^"]*)" screen$/, async function( - walletName, - screenName -) { - const wallet = getWalletByName.call(this, walletName); - return waitUntilUrlEquals.call(this, `/wallets/${wallet.id}/${screenName}`); -}); - -Then(/^I should be on the "([^"]*)" screen$/, async function(screenName) { - return waitUntilUrlEquals.call(this, `/${screenName}`); -}); - -Then( - /^I should see the following error messages on the wallet send form:$/, - async function(data) { - const errorSelector = '.WalletSendForm_component .SimpleFormField_error'; - await this.client.waitForText(errorSelector); - let errorsOnScreen = await this.client.getText(errorSelector); - if (typeof errorsOnScreen === 'string') errorsOnScreen = [errorsOnScreen]; - const errors = data.hashes(); - for (let i = 0; i < errors.length; i++) { - const expectedError = await this.intl(errors[i].message); - expect(errorsOnScreen[i]).to.equal(expectedError); - } - } -); - -// TODO: refactor this to a less hackish solution (fees cannot easily be calculated atm) -Then(/^the latest transaction should show:$/, async function(table) { - const expectedData = table.hashes()[0]; - await this.client.waitForVisible('.Transaction_title'); - let transactionTitles = await this.client.getText('.Transaction_title'); - transactionTitles = [].concat(transactionTitles); - const expectedTransactionTitle = await this.intl(expectedData.title, { - currency: 'Ada', - }); - expect(expectedTransactionTitle).to.equal(transactionTitles[0]); - let transactionAmounts = await this.client.getText('.Transaction_amount'); - transactionAmounts = [].concat(transactionAmounts); - // Transaction amount includes transaction fees so we need to - // substract them in order to get a match with expectedData.amountWithoutFees. - // NOTE: we use "add()" as this is outgoing transaction and amount is a negative value! - const transactionAmount = new BigNumber(transactionAmounts[0]); - const transactionAmountWithoutFees = transactionAmount - .add(this.fees) - .toFormat(DECIMAL_PLACES_IN_ADA); - expect(expectedData.amountWithoutFees).to.equal(transactionAmountWithoutFees); -}); - -// 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: 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() { - return this.client.waitUntil(async () => { - const activeAddress = await this.client.getText('.WalletReceive_hash'); - const generatedAddress = await this.client.getText( - '.generatedAddress-1 .Address_addressId' - ); - 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/tests/unit/setup/parameter-types.js b/features/tests/unit/setup/parameter-types.js deleted file mode 100644 index 9aad9bd47f..0000000000 --- a/features/tests/unit/setup/parameter-types.js +++ /dev/null @@ -1,8 +0,0 @@ -import { defineParameterType } from 'cucumber'; - -// Add {bool} parameter type -defineParameterType({ - name: 'bool', - regexp: /true|false/, - transformer: b => b === 'true', -}); diff --git a/features/tests/unit/steps/mnemonics-steps.js b/features/tests/unit/steps/mnemonics-steps.js deleted file mode 100644 index e81c01c2dd..0000000000 --- a/features/tests/unit/steps/mnemonics-steps.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Given, Then } from 'cucumber'; -import { range } from 'lodash'; -import { generateAccountMnemonics } from '../../../../source/renderer/app/api/utils/mnemonics'; -import { isValidMnemonic } from '../../../../source/common/crypto/decrypt'; -import { WALLET_RECOVERY_PHRASE_WORD_COUNT } from '../../../../source/renderer/app/config/cryptoConfig'; - -const isValidWalletRecoveryPhrase = mnemonic => - isValidMnemonic(mnemonic, WALLET_RECOVERY_PHRASE_WORD_COUNT); - -Given('I generate {int} wallet recovery mnemonics', function( - numberOfMnemonics -) { - this.context.mnemonics = range(numberOfMnemonics).map(() => - generateAccountMnemonics().join(' ') - ); -}); - -Then('all generated wallet recovery mnemonics should be valid', function() { - for (const mnemonic of this.context.mnemonics) { - if (!isValidWalletRecoveryPhrase(mnemonic)) { - throw new Error(`"${mnemonic}" is not valid`); - } - } -}); - -Given( - 'I generate and validate an unbound number of wallet recovery mnemonics', - function() { - let numberOfTestsExecuted = 0; - let generated = true; - while (generated) { - const mnemonic = generateAccountMnemonics().join(' '); - if (!isValidWalletRecoveryPhrase(mnemonic)) { - generated = false; - throw new Error(`"${mnemonic}" is not valid`); - } - numberOfTestsExecuted++; - process.stdout.clearLine(); - process.stdout.cursorTo(0); - process.stdout.write(`${numberOfTestsExecuted} mnemonics validated.`); - } - } -); diff --git a/features/wallet-address-generate.feature b/features/wallet-address-generate.feature deleted file mode 100644 index e40a59010d..0000000000 --- a/features/wallet-address-generate.feature +++ /dev/null @@ -1,24 +0,0 @@ -@e2e -Feature: Generate Wallet Address - - Background: - Given I have completed the basic setup - - Scenario: Generating wallet address - Given I have the following wallets: - | 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 - Then I should see newly generated address as active address on the wallet receive screen - - Scenario: Generating wallet address for a wallet with spending password - Given I have the following wallets: - | 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" - And I click on the "Generate new address" button - Then I should see newly generated address as active address on the wallet receive screen diff --git a/features/wallets-ordering.feature b/features/wallets-ordering.feature deleted file mode 100644 index f9d606e3c8..0000000000 --- a/features/wallets-ordering.feature +++ /dev/null @@ -1,29 +0,0 @@ -@e2e -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/flow/declarations/EnumMap.js b/flow/declarations/EnumMap.js new file mode 100644 index 0000000000..35b0bf059d --- /dev/null +++ b/flow/declarations/EnumMap.js @@ -0,0 +1 @@ +declare type EnumMap = O & { [K]: V & $ElementType }; \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 3ceec80513..c563ce6118 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -138,6 +138,8 @@ gulp.task('prepare:themes:daedalus', () => 'source/renderer/app/themes/daedalus/cardano.js', 'source/renderer/app/themes/daedalus/dark-blue.js', 'source/renderer/app/themes/daedalus/dark-cardano.js', + 'source/renderer/app/themes/daedalus/flight-candidate.js', + 'source/renderer/app/themes/daedalus/incentivized-testnet.js', 'source/renderer/app/themes/daedalus/index.js', 'source/renderer/app/themes/daedalus/light-blue.js', 'source/renderer/app/themes/daedalus/white.js', @@ -182,7 +184,7 @@ gulp.task('build:themes', gulp.series('clean:dist', 'prepare:themes')); gulp.task( 'test:e2e:nodemon', shell.task( - 'nodemon --watch dist --watch features --exec "yarn test:e2e --tags \'@e2e and @watch\'"' + 'nodemon --watch dist --watch tests --exec "yarn test:e2e --tags \'@e2e and @watch\'"' ) ); diff --git a/installer-clusters.cfg b/installer-clusters.cfg index 474498aabb..19707b2acc 100644 --- a/installer-clusters.cfg +++ b/installer-clusters.cfg @@ -1 +1 @@ -mainnet staging testnet +mainnet_flight selfnode testnet mainnet staging itn_selfnode itn_rewards_v1 qa nightly diff --git a/installers/.gitignore b/installers/.gitignore index ef82266568..c5700857c5 100644 --- a/installers/.gitignore +++ b/installers/.gitignore @@ -4,7 +4,6 @@ dist-newstyle/ configuration.yaml launcher-config.yaml -wallet-topology.yaml *genesis*.json *.windows diff --git a/installers/DarwinLauncher.hs b/installers/DarwinLauncher.hs new file mode 100644 index 0000000000..3b3b980776 --- /dev/null +++ b/installers/DarwinLauncher.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Main (main) where + +import System.Environment (getExecutablePath) +import Turtle (FilePath, Text, parent, decodeString, encodeString, (), procs, format, fp) +import System.Posix.Process (executeFile) + +main :: IO () +main = do + self <- getExecutablePath + let + installDir = parent (decodeString self) + launcherConfig :: String + launcherConfig = encodeString $ installDir "../Resources/launcher-config.yaml" + launcher = installDir "cardano-launcher" + procs (tt $ installDir "../Resources/helper") [] mempty + executeFile (encodeString launcher) False [ "--config", launcherConfig ] Nothing + +tt :: Turtle.FilePath -> Text +tt = format fp diff --git a/installers/Installer.hs b/installers/Installer.hs index 51cbd12a98..44339375b7 100644 --- a/installers/Installer.hs +++ b/installers/Installer.hs @@ -15,6 +15,7 @@ import Data.Maybe (fromJust) import System.Directory import qualified Data.Text.IO as T import qualified Data.Text as T +import Data.Yaml (decodeFileThrow) import Types import Config @@ -33,20 +34,15 @@ main = do (,) <$> optionsParser os <*> commandParser case command of - GenConfig{..} -> - generateOSClusterConfigs cfDhallRoot cfOutdir options' - CheckConfigs{..} -> - checkAllConfigs cfDhallRoot GenInstaller -> do genSignedInstaller (oOS options') options' BuildkiteCrossWin -> do fullVersion <- getDaedalusVersion "../package.json" - ver <- T.strip <$> T.readFile "version" + ver <- T.strip <$> T.readFile "version" -- TODO let fullName = packageFileName Win64 (oCluster options') fullVersion (oBackend options') ver (oBuildJob options') - installerConfig <- getInstallerConfig "./dhall" Win64 (oCluster options') - WindowsInstaller.writeInstallerNSIS fullName fullVersion installerConfig (oCluster options') + installerConfig <- decodeFileThrow "installer-config.json" + WindowsInstaller.writeInstallerNSIS fullName fullVersion installerConfig options' (oCluster options') WindowsInstaller.writeUninstallerNSIS fullVersion installerConfig - generateOSClusterConfigs "./dhall" "." options' Appveyor -> do buildNumber <- getEnv "APPVEYOR_BUILD_NUMBER" let @@ -56,9 +52,10 @@ main = do go :: String -> IO () go cluster' = do let - getAppName Mainnet = "Daedalus" - getAppName Staging = "DaedalusStaging" - getAppName Testnet = "DaedalusTestnet" + getAppName ITN_Rewards_v1 = "DaedalusRewardsV1" + getAppName Nightly = "DaedalusNightly" + getAppName QA = "DaedalusQA"; + getAppName Selfnode = "DaedalusSelfnode" cluster = fromJust $ diagReadCaseInsensitive cluster' opts'' = opts' { oCluster = cluster @@ -73,7 +70,6 @@ main = do putStr banner genSignedInstaller os opts'' copyFile "launcher-config.yaml" ("launcher-config-" <> cluster' <> ".win64.yaml") - copyFile "wallet-topology.yaml" ("wallet-topology-" <> cluster' <> ".win64.yaml") clusters' <- getEnv "CLUSTERS" let clusters = splitOn " " clusters' print clusters diff --git a/installers/Spec.hs b/installers/Spec.hs index a4792f0c13..0d9b1fe66f 100644 --- a/installers/Spec.hs +++ b/installers/Spec.hs @@ -7,10 +7,9 @@ import qualified Data.Text as T import Filesystem.Path (FilePath, ()) import Filesystem.Path.CurrentOS (fromText, decodeString) import System.IO.Temp (getCanonicalTemporaryDirectory) -import Turtle (mktempdir, inproc, strict, ls, fold, writeTextFile, mktree, mkdir, cptree, format) +import Turtle (mktempdir, inproc, strict, ls, fold, writeTextFile, mktree, mkdir, cptree) import Control.Monad.Managed (MonadManaged, runManaged) import Data.Aeson.Types (Value) -import Data.Aeson.Lens import qualified Control.Foldl as Fold import System.Directory import qualified Data.ByteString.Lazy as BL @@ -28,7 +27,6 @@ main :: IO () main = hspec $ do describe "Utility functions" utilSpec describe "MacInstaller build" macBuildSpec - describe "Config generation" configSpec describe "recursive directory deletion" deleteSpec describe "Hydra downloads for AppVeyor" hydraSpec @@ -44,10 +42,11 @@ macBuildSpec = do { oOS = Win64 , oBackend = Cardano daedalusBridge , oBuildJob = Just (BuildJob "test") - , oCluster = Mainnet + , oCluster = Nightly , oAppName = "Daedalus" , oOutputDir = out , oTestInstaller = testInstaller False + , oSigningConfigPath = Nothing } liftIO $ do @@ -87,21 +86,6 @@ makeTestInstallersDir = do getDaedalusBridge :: IO FilePath getDaedalusBridge = fromText . T.stripEnd <$> strict (inproc "daedalus-bridge" [] empty) -configSpec :: Spec -configSpec = do - describe "Config file generation" $ do - it "Generates something" $ do - dhallTest Win64 Staging Launcher "./dhall" $ \val -> do - val^.key "configuration".key "key"._String `shouldBe` "mainnet_dryrun_wallet_win64" - describe "installer config generation" $ do - it "gets the right mainnet port" $ do - mainnetCfg <- getInstallerConfig "./dhall" Macos64 Mainnet - walletPort mainnetCfg `shouldBe` 8090 - it "gets the right testnet port" $ do - stagingCfg <- getInstallerConfig "./dhall" Win64 Testnet - walletPort stagingCfg `shouldBe` 8094 - - deleteSpec :: Spec deleteSpec = do describe "deleting a path over 256 chars long" $ do @@ -123,11 +107,6 @@ deleteSpec = do type Yuck = Value -> IO () -dhallTest :: OS -> Cluster -> Config -> FilePath -> Yuck -> IO () -dhallTest os cluster cfg root yuck = - forConfigValues (format dfp root) os cluster - (\cfg' val -> when (cfg == cfg') (yuck val)) - getTempDir :: MonadManaged io => Text -> io FilePath getTempDir template = do tmp <- liftIO . fmap decodeString $ getCanonicalTemporaryDirectory @@ -146,8 +125,8 @@ utilSpec = do describe "Package filename generation" $ do it "generates a good filename for windows" $ do - let f = packageFileName Win64 Mainnet (Version "0.4.2") (Cardano "") "9.9" (Just "job.id") - f `shouldBe` (fromText "daedalus-0.4.2-cardano-sl-9.9-mainnet-windows-job.id.exe") + let f = packageFileName Win64 Nightly (Version "0.4.2") (Cardano "") "9.9" (Just "job.id") + f `shouldBe` (fromText "daedalus-0.4.2-cardano-sl-9.9-nightly-windows-job.id.exe") ---------------------------------------------------------------------------- -- Tests for Hydra downloading (yes it's in the AppVeyor module) diff --git a/installers/cabal.project b/installers/cabal.project new file mode 100644 index 0000000000..694d0846b0 --- /dev/null +++ b/installers/cabal.project @@ -0,0 +1,9 @@ +packages: . + +write-ghc-environment-files: never + +source-repository-package + type: git + location: https://github.com/input-output-hk/haskell-nsis + tag: 016a122d845849b14711e0f86805a4748ec44584 + --sha256: 14phxmmzncq63aahpw6f4sfmkf3cy075zjfyzhv2689csjwivm8y diff --git a/installers/common/Config.hs b/installers/common/Config.hs index 1455135cdd..8233661f1c 100644 --- a/installers/common/Config.hs +++ b/installers/common/Config.hs @@ -7,44 +7,24 @@ {-# LANGUAGE TypeOperators #-} {-# LANGUAGE DataKinds #-} module Config - ( checkAllConfigs - , generateOSClusterConfigs - , forConfigValues - , OS(..), Cluster(..), Config(..), Backend(..) + ( OS(..), Cluster(..), Config(..), Backend(..) , optReadLower, argReadLower , Options(..), optionsParser , Command(..), commandParser , dfp -- Re-export Turtle: , options - , getInstallerConfig - , dhallTopExpr , diagReadCaseInsensitive ) where -import qualified Control.Exception as Ex - -import Data.Bool (bool) -import qualified Data.ByteString as BS -import qualified Data.ByteString.Char8 as BS8 import qualified Data.Map as Map import Data.Maybe import Data.Optional (Optional) -import Data.Semigroup ((<>)) import qualified Data.Text as T -import qualified Data.Text.Lazy as LT -import qualified Data.Yaml as YAML - -import qualified Dhall.JSON as Dhall -import qualified Dhall as Dhall -import Filesystem.Path (FilePath, ()) -import Filesystem.Path.CurrentOS (fromText, encodeString) +import Filesystem.Path (FilePath) import qualified Filesystem.Path.Rules as FP -import qualified GHC.IO.Encoding as GHC -import qualified System.IO as Sys -import qualified System.Exit as Sys import Turtle (optional, (<|>), format, (%), s, Format, makeFormat) import Turtle.Options @@ -73,40 +53,27 @@ argReadLower :: (Bounded a, Enum a, Read a, Show a) => ArgName -> Optional HelpM argReadLower = arg (diagReadCaseInsensitive . T.unpack) data Command - = GenConfig - { cfDhallRoot :: Text - , cfOutdir :: FilePath - } - | CheckConfigs - { cfDhallRoot :: Text - } - | GenInstaller + = GenInstaller | Appveyor | BuildkiteCrossWin deriving (Eq, Show) data Options = Options - { oBackend :: Backend - , oBuildJob :: Maybe BuildJob - , oOS :: OS - , oCluster :: Cluster - , oAppName :: AppName - , oOutputDir :: FilePath - , oTestInstaller :: TestInstaller - , oSigningConfigPath :: Maybe FilePath + { oBackend :: Backend + , oBuildJob :: Maybe BuildJob + , oOS :: OS + , oCluster :: Cluster + , oAppName :: AppName + , oOutputDir :: FilePath + , oTestInstaller :: TestInstaller + , oCodeSigningConfigPath :: Maybe FilePath + , oSigningConfigPath :: Maybe FilePath } deriving Show commandParser :: Parser Command commandParser = (fromMaybe GenInstaller <$>) . optional $ subcommandGroup "Subcommands:" - [ ("config", "Build configs for an OS / cluster (see: --os, --cluster top-level options)", - GenConfig - <$> argText "INDIR" "Directory containing Dhall config files" - <*> (fromText <$> argText "OUTDIR" "Target directory for generated YAML config files")) - , ("check-configs", "Verify all Dhall-defined config components", - CheckConfigs - <$> argText "DIR" "Directory containing Dhall config files") - , ("installer", "Build an installer", + [ ("installer", "Build an installer", pure GenInstaller) , ("appveyor", "do an appveroy build", pure Appveyor) , ("buildkite-cross", "cross-compile windows from linux", pure BuildkiteCrossWin) @@ -119,67 +86,24 @@ optionsParser detectedOS = Options (BuildJob <$> optText "build-job" 'b' "CI Build Job/ID")) <*> (fromMaybe detectedOS <$> (optional $ optReadLower "os" 's' "OS, defaults to host OS. One of: linux64 macos64 win64")) - <*> (fromMaybe Mainnet <$> (optional $ + <*> (fromMaybe Selfnode <$> (optional $ 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" <*> (testInstaller <$> switch "test-installer" 't' "Test installers after building") - <*> (optional $ optPath "signing-config" 'k' "the path to the json file describing the signing config") + <*> (optional $ optPath "code-signing-config" 's' "the path to the json file describing the code signing config") + <*> (optional $ optPath "signing-config" 'k' "the path to the json file describing the product signing config") backendOptionParser :: Parser Backend -backendOptionParser = cardano <|> bool (Cardano "") Mantis <$> enableMantis +backendOptionParser = enableJormungandr <|> cardano where cardano = Cardano <$> optPath "cardano" 'C' "Use Cardano backend with given Daedalus bridge path" - enableMantis = switch "mantis" 'M' "Use Mantis (ETC) backend" - + enableJormungandr = Jormungandr <$> optPath "jormungandr" 'j' "use Jormungandr backend" -- | Render a FilePath with POSIX-style forward slashes, which is the -- Dhall syntax. dfp :: Format r (FilePath -> r) dfp = makeFormat (\fpath -> either id id (FP.toText FP.posix fpath)) - -dhallTopExpr :: Text -> Config -> OS -> Cluster -> Text -dhallTopExpr dhallRoot cfg os cluster - | Launcher <- cfg = format (s%" "%s%" ("%s%" "%s%" )") (comp Launcher) (comp cluster) (comp os) (comp cluster) - | Topology <- cfg = format (s%" "%s) (comp Topology) (comp cluster) - where comp x = dhallRoot <>"/"<> lshowText x <>".dhall" - -getInstallerConfig :: Text -> OS -> Cluster -> IO InstallerConfig -getInstallerConfig dhallRoot os cluster = Dhall.input Dhall.auto (LT.fromStrict topexpr) - where - topexpr = format (s%" "%s%" ("%s%" "%s%")") (dhallRoot <> "/installer.dhall") (comp cluster) (comp os) (comp cluster) - comp x = dhallRoot <>"/"<> lshowText x <>".dhall" - - -forConfigValues :: Text -> OS -> Cluster -> (Config -> YAML.Value -> IO a) -> IO () -forConfigValues dhallRoot os cluster action = do - sequence_ [ let topExpr = dhallTopExpr dhallRoot cfg os cluster - in action cfg =<< - (handle $ Dhall.codeToValue (BS8.pack $ T.unpack topExpr) topExpr) - | cfg <- enumFromTo minBound maxBound ] - -checkAllConfigs :: Text -> IO () -checkAllConfigs dhallRoot = - sequence_ [ forConfigValues dhallRoot os cluster (\_ _ -> pure ()) - | os <- enumFromTo minBound maxBound - , cluster <- enumFromTo minBound maxBound ] - -generateOSClusterConfigs :: Text -> FilePath -> Options -> IO () -generateOSClusterConfigs dhallRoot outDir Options{..} = do - GHC.setLocaleEncoding GHC.utf8 - forConfigValues dhallRoot oOS oCluster $ - \config val -> - BS.writeFile (encodeString $ outDir configFilename config) $ YAML.encode val - --- | Generic error handler: be it encoding/decoding, file IO, parsing or type-checking. -handle :: IO a -> IO a -handle = Ex.handle handler - where - handler :: Ex.SomeException -> IO a - handler e = do - Sys.hPutStrLn Sys.stderr "" - Sys.hPrint Sys.stderr e - Sys.exitFailure diff --git a/installers/common/MacInstaller.hs b/installers/common/MacInstaller.hs index 592abbd7f2..eff05fe678 100644 --- a/installers/common/MacInstaller.hs +++ b/installers/common/MacInstaller.hs @@ -2,10 +2,14 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE QuasiQuotes #-} -module MacInstaller (main) where +module MacInstaller + ( main + , readCardanoVersionFile + ) where --- --- An overview of Mac .pkg internals: http://www.peachpit.com/articles/article.aspx?p=605381&seqNum=2 @@ -17,18 +21,21 @@ import Control.Exception (handle) import Control.Monad (unless) import Data.Text (Text) import qualified Data.Text as T -import Data.Aeson (FromJSON(parseJSON), decodeFileStrict', genericParseJSON, defaultOptions) +import Data.Aeson (FromJSON(parseJSON), genericParseJSON, defaultOptions, decodeFileStrict') +import Data.Yaml (decodeFileThrow) +import Text.RawString.QQ import Filesystem.Path (FilePath, dropExtension, (<.>), ()) import Filesystem.Path.CurrentOS (encodeString) -import System.FilePath.Glob (glob) import System.IO (BufferMode (NoBuffering), hSetBuffering) import System.IO.Error (IOError, isDoesNotExistError) import System.Environment (getEnv) +import System.Posix.Files import Turtle hiding (e, prefix, stdout) import Turtle.Line (unsafeTextToLine) + import Config import RewriteLibs (chain) import Types @@ -38,43 +45,55 @@ data DarwinConfig = DarwinConfig { dcAppNameApp :: Text -- ^ Daedalus.app for example , dcAppName :: Text -- ^ the Daedalus from Daedalus.app , dcPkgName :: Text -- ^ org.daedalus.pkg for example + , dcDataDir :: Text -- ^ ${HOME}/Library/Application Support/Daedalus/qa } deriving (Show) -- | The contract of `main` is not to produce unsigned installer binaries. main :: Options -> IO () -main opts@Options{oSigningConfigPath,oCluster,oBackend,oBuildJob,oOutputDir,oTestInstaller} = do +main opts@Options{oCodeSigningConfigPath,oSigningConfigPath,oCluster,oBackend,oBuildJob,oOutputDir,oTestInstaller} = do + + installerConfig <- decodeFileThrow "installer-config.json" + hSetBuffering stdout NoBuffering - mSigningConfig <- case oSigningConfigPath of + mCodeSigningConfig <- case oCodeSigningConfigPath of Just path -> do decodeFileStrict' $ encodeString path Nothing -> do pure Nothing - generateOSClusterConfigs "./dhall" "." opts - cp "launcher-config.yaml" "../launcher-config.yaml" - - installerConfig <- getInstallerConfig "./dhall" Macos64 oCluster + mSigningConfig <- case oSigningConfigPath of + Just path -> do + decodeFileStrict' $ encodeString path + Nothing -> pure Nothing let darwinConfig = DarwinConfig { - dcAppNameApp = (installDirectory installerConfig) <> ".app" - , dcAppName = installDirectory installerConfig + dcAppNameApp = (spacedName installerConfig) <> ".app" + , dcAppName = spacedName installerConfig , dcPkgName = "org." <> (macPackageName installerConfig) <> ".pkg" + , dcDataDir = dataDir installerConfig } print darwinConfig ver <- getBackendVersion oBackend - exportBuildVars opts installerConfig ver + exportBuildVars opts ver buildIcons oCluster appRoot <- buildElectronApp darwinConfig installerConfig - makeComponentRoot opts appRoot darwinConfig + makeComponentRoot opts appRoot darwinConfig installerConfig daedalusVer <- getDaedalusVersion "../package.json" let pkg = packageFileName Macos64 oCluster daedalusVer oBackend ver oBuildJob opkg = oOutputDir pkg + print "appRoot:" + print (tt appRoot) + + case mCodeSigningConfig of + Just codeSigningConfig -> codeSignComponent codeSigningConfig appRoot + Nothing -> pure () + tempInstaller <- makeInstaller opts darwinConfig appRoot pkg case mSigningConfig of @@ -96,6 +115,60 @@ main opts@Options{oSigningConfigPath,oCluster,oBackend,oBuildJob,oOutputDir,oTes NotSigned -> rm opkg Nothing -> pure () +-- | Define the code signing script to be used for code signing +codeSignScriptContents :: String +codeSignScriptContents = [r|#!/run/current-system/sw/bin/bash +set -x +SIGN_ID="$1" +KEYCHAIN="$2" +REL_PATH="$3" +XML_PATH="$4" +ABS_PATH="$(pwd)/$REL_PATH" +SIGN_CMD="codesign --verbose=4 --deep --strict --timestamp --options=runtime --entitlements $XML_PATH --sign \"$SIGN_ID\"" +VERIFY_CMD="codesign --verbose=4 --verify --deep --strict" +ENTITLEMENT_CMD="codesign -d --entitlements :-" +TS="$(date +%Y-%m-%d_%H-%M-%S)" +LOG="2>&1 | tee -a /tmp/codesign-output-${TS}.txt" + +# Remove symlinks pointing outside of the project build folder: +rm -f "$ABS_PATH/Contents/Resources/app/result" + +# Ensure the code signing identity is found and set the keychain search path: +eval "security show-keychain-info \"$KEYCHAIN\" $LOG" +eval "security find-identity -v -p codesigning \"$KEYCHAIN\" $LOG" +eval "security list-keychains -d user -s \"$KEYCHAIN\" $LOG" + +# Sign framework executables not signed by the deep sign command: +eval "$SIGN_CMD \"$ABS_PATH/Contents/Frameworks/Squirrel.framework/Versions/A/Resources/ShipIt\" $LOG" +eval "$SIGN_CMD \"$ABS_PATH/Contents/Frameworks/Electron Framework.framework/Versions/Current/Resources/crashpad_handler\" $LOG" +eval "$SIGN_CMD \"$ABS_PATH/Contents/Frameworks/Electron Framework.framework/Versions/Current/Libraries/libnode.dylib\" $LOG" +eval "$SIGN_CMD \"$ABS_PATH/Contents/Frameworks/Electron Framework.framework/Versions/Current/Libraries/libffmpeg.dylib\" $LOG" + +eval "$SIGN_CMD \"$ABS_PATH/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libEGL.dylib\" $LOG" +eval "$SIGN_CMD \"$ABS_PATH/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libGLESv2.dylib\" $LOG" +eval "$SIGN_CMD \"$ABS_PATH/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libswiftshader_libEGL.dylib\" $LOG" +eval "$SIGN_CMD \"$ABS_PATH/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libswiftshader_libGLESv2.dylib\" $LOG" + +# Sign the whole component deeply +eval "$SIGN_CMD \"$ABS_PATH\" $LOG" + +# Verify the signing +eval "$VERIFY_CMD \"$ABS_PATH\" $LOG" +eval "$VERIFY_CMD --display -r- \"$ABS_PATH\"" "$LOG" +eval "$ENTITLEMENT_CMD \"$ABS_PATH\"" "$LOG" +set +x|] + +-- | Define the code signing entitlements to be used for code signing +codeSignEntitlements :: String +codeSignEntitlements = [r| + + + + com.apple.security.cs.allow-unsigned-executable-memory + + +|] + makePostInstall :: Format a (Text -> a) makePostInstall = "#!/usr/bin/env bash\n" % "#\n" % @@ -106,15 +179,30 @@ makePostInstall = "#!/usr/bin/env bash\n" % makeScriptsDir :: Options -> DarwinConfig -> Managed T.Text makeScriptsDir Options{oBackend} DarwinConfig{dcAppNameApp} = case oBackend of - Cardano _ -> do + Cardano _ -> common + Jormungandr _ -> common + where + common = do + tmp <- fromString <$> (liftIO $ getEnv "TMP") + tempdir <- mktempdir tmp "scripts" + liftIO $ do + cp "data/scripts/dockutil" (tempdir "dockutil") + writeTextFile (tempdir "postinstall") (format makePostInstall dcAppNameApp) + chmod executable (tempdir "postinstall") + pure $ tt tempdir + +makeSigningDir :: Managed (T.Text, T.Text) +makeSigningDir = do tmp <- fromString <$> (liftIO $ getEnv "TMP") - tempdir <- mktempdir tmp "scripts" + tempdir <- mktempdir tmp "codeScripts" + let + codesignScriptPath = tempdir "codesignFnGen.sh" + entitlementsPath = tempdir "entitlements.xml" liftIO $ do - cp "data/scripts/dockutil" (tempdir "dockutil") - writeTextFile (tempdir "postinstall") (format makePostInstall dcAppNameApp) - run "chmod" ["+x", tt (tempdir "postinstall")] - pure $ tt tempdir - Mantis -> pure "[DEVOPS-533]" + writeTextFile codesignScriptPath $ T.pack codeSignScriptContents + chmod executable codesignScriptPath + writeTextFile entitlementsPath $ T.pack codeSignEntitlements + pure $ (tt codesignScriptPath, tt entitlementsPath) buildIcons :: Cluster -> IO () buildIcons cluster = do @@ -128,72 +216,113 @@ buildIcons cluster = do -- NB: If webpack scripts are changed then this function may need to -- be updated. buildElectronApp :: DarwinConfig -> InstallerConfig -> IO FilePath -buildElectronApp darwinConfig@DarwinConfig{dcAppNameApp,dcAppName} installerConfig = do +buildElectronApp darwinConfig@DarwinConfig{dcAppName, dcAppNameApp} installerConfig = do withDir ".." . sh $ npmPackage darwinConfig let formatter :: Format r (Text -> Text -> r) formatter = "../release/darwin-x64/" % s % "-darwin-x64/" % s pathtoapp = format formatter dcAppName dcAppNameApp - rewritePackageJson (T.unpack $ pathtoapp <> "/Contents/Resources/app/package.json") (installDirectory installerConfig) + rewritePackageJson (T.unpack $ pathtoapp <> "/Contents/Resources/app/package.json") (spacedName installerConfig) pure $ fromString $ T.unpack $ pathtoapp npmPackage :: DarwinConfig -> Shell () npmPackage DarwinConfig{dcAppName} = do mktree "release" - echo "~~~ Installing nodejs dependencies..." + echo "Installing nodejs dependencies..." procs "yarn" ["install"] empty - echo "~~~ Running electron packager script..." + echo "Running electron packager script..." export "NODE_ENV" "production" procs "yarn" ["run", "package", "--", "--name", dcAppName ] empty size <- inproc "du" ["-sh", "release"] empty printf ("Size of Electron app is " % l % "\n") size getBackendVersion :: Backend -> IO Text -getBackendVersion (Cardano bridge) = readCardanoVersionFile bridge -getBackendVersion Mantis = pure "DEVOPS-533" +getBackendVersion (Cardano bridge) = readCardanoVersionFile bridge +getBackendVersion (Jormungandr bridge) = readCardanoVersionFile bridge -makeComponentRoot :: Options -> FilePath -> DarwinConfig -> IO () -makeComponentRoot Options{oBackend} appRoot darwinConfig@DarwinConfig{dcAppName} = do +makeComponentRoot :: Options -> FilePath -> DarwinConfig -> InstallerConfig -> IO () +makeComponentRoot Options{oBackend,oCluster} appRoot darwinConfig@DarwinConfig{dcAppName} InstallerConfig{hasBlock0,genesisPath,secretPath} = do let dir = appRoot "Contents/MacOS" + dataDir = appRoot "Contents/Resources" + maybeCopyToResources (maybePath,name) = maybe (pure ()) (\path -> cp (fromText path) (dataDir name)) maybePath - echo "~~~ Preparing files ..." + echo "Preparing files ..." + let + common :: FilePath -> IO () + common bridge = do + -- Executables (from daedalus-bridge) + forM_ ["cardano-launcher" ] $ \f -> + cp (bridge "bin" f) (dir f) + + -- Config yaml + cp "launcher-config.yaml" (dataDir "launcher-config.yaml") case oBackend of Cardano bridge -> do + common bridge -- Executables (from daedalus-bridge) - forM ["cardano-launcher", "cardano-node", "cardano-x509-certificates"] $ \f -> + forM_ ["cardano-wallet-byron", "cardano-node", "cardano-cli", "export-wallets" ] $ \f -> cp (bridge "bin" f) (dir f) + forM_ ["config.yaml", "genesis.json", "topology.yaml" ] $ \f -> + cp f (dataDir f) - -- Config files (from daedalus-bridge) - cp (bridge "config/configuration.yaml") (dir "configuration.yaml") - cp (bridge "config/log-config-prod.yaml") (dir "log-config-prod.yaml") + when (oCluster == Selfnode) $ do + cp "signing.key" (dataDir "signing.key") + cp "delegation.cert" (dataDir "delegation.cert") - -- Genesis (from daedalus-bridge) - genesisFiles <- glob . encodeString $ bridge "config" "*genesis*.json" - when (null genesisFiles) $ - error "Cardano package carries no genesis files." - procs "cp" (map T.pack genesisFiles ++ [tt dir]) mempty + procs "chmod" ["-R", "+w", tt dir] empty + + rmtree $ dataDir "app/installers" + + -- Rewrite libs paths and bundle them + void $ chain (encodeString dir) $ fmap tt [dir "cardano-launcher", dir "cardano-wallet-byron", dir "cardano-node", dir "cardano-cli", dir "export-wallets" ] + Jormungandr bridge -> do + common bridge + -- Executables (from daedalus-bridge) + forM_ ["cardano-wallet-jormungandr", "jormungandr" ] $ \f -> + cp (bridge "bin" f) (dir f) + + -- Config files (from launcherConfig.configFiles) + cp "config.yaml" (dataDir "config.yaml") + + when hasBlock0 $ + cp "block-0.bin" (dataDir "block-0.bin") - -- Config yaml (generated from dhall files) - cp "launcher-config.yaml" (dir "launcher-config.yaml") - cp "wallet-topology.yaml" (dir "wallet-topology.yaml") + mapM_ maybeCopyToResources [ (genesisPath,"genesis.yaml"), (secretPath,"secret.yaml") ] + + -- Genesis (from daedalus-bridge) + --genesisFiles <- glob . encodeString $ bridge "config" "*genesis*.json" + --when (null genesisFiles) $ + -- error "Cardano package carries no genesis files." + --procs "cp" (map T.pack genesisFiles ++ [tt dir]) mempty procs "chmod" ["-R", "+w", tt dir] empty - -- Rewrite libs paths and bundle them - void $ chain (encodeString dir) $ fmap tt [dir "cardano-launcher", dir "cardano-node", dir "cardano-x509-certificates"] + rmtree $ dataDir "app/installers" - Mantis -> pure () -- DEVOPS-533 + -- Rewrite libs paths and bundle them + void $ chain (encodeString dir) $ fmap tt [dir "cardano-launcher", dir "cardano-wallet-jormungandr", dir "jormungandr" ] -- Prepare launcher de <- testdir (dir "Frontend") unless de $ mv (dir (fromString $ T.unpack $ dcAppName)) (dir "Frontend") - run "chmod" ["+x", tt (dir "Frontend")] - void $ writeLauncherFile dir darwinConfig - + chmod executable (dir "Frontend") + void $ writeLauncherFile dataDir darwinConfig + maybeDarwinLauncher <- which "darwin-launcher" + case maybeDarwinLauncher of + Just darwinLauncher -> do + let + dest = dir (fromString $ T.unpack $ dcAppName) + cp darwinLauncher dest + chmod writable dest + void $ chain (encodeString dir) [ tt dest ] + Nothing -> do + print "darwin-launcher was not found in $PATH" + exit $ ExitFailure 1 makeInstaller :: Options -> DarwinConfig -> FilePath -> FilePath -> IO FilePath makeInstaller opts@Options{oOutputDir} darwinConfig@DarwinConfig{dcPkgName} componentRoot pkg = do + echo "Making installer ..." let tempPkg1 = format fp (oOutputDir pkg) tempPkg2 = oOutputDir (dropExtension pkg <.> "unsigned" <.> "pkg") @@ -233,31 +362,49 @@ readCardanoVersionFile bridge = prefix <$> handle handler (readTextFile verFile) | otherwise = throwM e writeLauncherFile :: FilePath -> DarwinConfig -> IO FilePath -writeLauncherFile dir DarwinConfig{dcAppName} = do +writeLauncherFile dir DarwinConfig{dcDataDir} = do writeTextFile path $ T.unlines contents - run "chmod" ["+x", tt path] + chmod executable path + setFileMode (encodeString path) anyReadExecute pure path where - path = dir (fromString $ T.unpack dcAppName) - dataDir = "$HOME/Library/Application Support/" <> (dcAppName) + anyReadExecute = foldl unionFileModes nullFileMode [ ownerExecuteMode, ownerReadMode, groupExecuteMode, groupReadMode, otherExecuteMode, otherReadMode ] + path = dir "helper" contents = [ "#!/usr/bin/env bash" - , "mkdir -p \"" <> dataDir <> "/Secrets-1.0\"" - , "mkdir -p \"" <> dataDir <> "/Logs/pub\"" - , "\"$(dirname \"$0\")/cardano-launcher\"" + , "mkdir -p \"" <> dcDataDir <> "/Secrets-1.0\"" + , "mkdir -p \"" <> dcDataDir <> "/Logs/pub\"" ] +data CodeSigningConfig = CodeSigningConfig + { codeSigningIdentity :: T.Text + , codeSigningKeyChain :: T.Text + } deriving (Show, Eq, Generic) + data SigningConfig = SigningConfig { signingIdentity :: T.Text , signingKeyChain :: Maybe T.Text + , signingKeyChainPassword :: Maybe T.Text } deriving (Show, Eq, Generic) +instance FromJSON CodeSigningConfig where + parseJSON = genericParseJSON defaultOptions + instance FromJSON SigningConfig where parseJSON = genericParseJSON defaultOptions +-- | Code sign a component. +codeSignComponent :: CodeSigningConfig -> FilePath -> IO () +codeSignComponent CodeSigningConfig{codeSigningIdentity,codeSigningKeyChain} component = do + with makeSigningDir $ \(codesignScriptPath, entitlementsPath) -> do + run codesignScriptPath [ codeSigningIdentity + , codeSigningKeyChain + , (tt component) + , entitlementsPath ] + -- | Creates a new installer package with signature added. signInstaller :: SigningConfig -> FilePath -> FilePath -> IO () -signInstaller SigningConfig{signingIdentity,signingKeyChain} src dst = +signInstaller SigningConfig{signingKeyChain, signingIdentity} src dst = run "productsign" $ sign ++ keychain ++ map tt [src, dst] where sign = [ "--sign", signingIdentity ] diff --git a/installers/common/RewriteLibs.hs b/installers/common/RewriteLibs.hs index b6e75a2d25..6dc58e32bf 100755 --- a/installers/common/RewriteLibs.hs +++ b/installers/common/RewriteLibs.hs @@ -1,6 +1,3 @@ -#!/usr/bin/env nix-shell -#! nix-shell -j 4 -i runhaskell -p 'pkgs.haskellPackages.ghcWithPackages (hp: with hp; [ turtle megaparsec text directory universum ])' - {-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} @@ -13,8 +10,8 @@ import Universum hiding (isPrefixOf, last) import Data.List (last) import Data.Text (isPrefixOf, isSuffixOf, splitOn) import System.Directory (copyFile, getPermissions, setOwnerWritable, setPermissions) -import Text.Megaparsec (Parsec, eof, manyTill, parse, someTill) -import Text.Megaparsec.Char (anyChar, eol, spaceChar) +import Text.Megaparsec (Parsec, anySingle, eof, manyTill, parse, someTill) +import Text.Megaparsec.Char (eol, spaceChar) import Turtle (procStrict, procs) @@ -78,12 +75,12 @@ type Parser = Parsec Void Text parseLibLine :: Parser Text parseLibLine = do _ <- many spaceChar - path <- someTill anyChar spaceChar - _ <- someTill anyChar eol + path <- someTill anySingle spaceChar + _ <- someTill anySingle eol return (toText path) parseOTool :: Parser [Text] parseOTool = do - _ <- manyTill anyChar eol + _ <- manyTill anySingle eol manyTill parseLibLine eof diff --git a/installers/common/Types.hs b/installers/common/Types.hs index 5c24ea2527..7106cdf844 100644 --- a/installers/common/Types.hs +++ b/installers/common/Types.hs @@ -41,9 +41,8 @@ import Filesystem.Path import Filesystem.Path.CurrentOS (fromText, encodeString) import Turtle (pwd, cd) import Turtle.Format (format, fp) -import Data.Aeson (FromJSON(..), withObject, eitherDecode, (.:)) +import Data.Aeson (FromJSON(..), withObject, eitherDecode, (.:), genericParseJSON, defaultOptions) import qualified Data.ByteString.Lazy.Char8 as L8 -import qualified Dhall as Dhall data OS = Linux64 @@ -52,16 +51,21 @@ data OS deriving (Bounded, Enum, Eq, Read, Show) data Cluster - = Mainnet + = Nightly + | ITN_Rewards_v1 + | QA + | Selfnode + | ITN_Selfnode + | Mainnet + | Mainnet_Flight | Staging | Testnet - | Demo deriving (Bounded, Enum, Eq, Read, Show) -- | The wallet backend to include in the installer. data Backend = Cardano FilePath -- ^ Cardano SL with the given daedalus-bridge. - | Mantis -- ^ Mantis, to be implemented in DEVOPS-533. + | Jormungandr FilePath -- ^ Rust node with haskell wallet deriving (Eq, Show) data SigningResult @@ -71,12 +75,10 @@ data SigningResult data Config = Launcher - | Topology deriving (Bounded, Enum, Eq, Show) configFilename :: Config -> FilePath configFilename Launcher = "launcher-config.yaml" -configFilename Topology = "wallet-topology.yaml" -- | What runtime config file to generate. data ConfigRequest = ConfigRequest @@ -102,24 +104,29 @@ tt = format fp -- | Value of the NETWORK variable used by the npm build. -- See also: the cluster argument in default.nix. clusterNetwork :: Cluster -> Text +clusterNetwork Nightly = "nightly" +clusterNetwork ITN_Rewards_v1 = "itn_rewards_v1" +clusterNetwork QA = "qa" +clusterNetwork ITN_Selfnode = "itn_selfnode" +clusterNetwork Selfnode = "selfnode" clusterNetwork Mainnet = "mainnet" +clusterNetwork Mainnet_Flight = "mainnet_flight" 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 +packageFileName _os cluster ver backend _backendVer build = fromText name <.> ext where name = T.intercalate "-" parts - parts = ["daedalus", fromVer ver, backend', backendVer, lshowText cluster, os'] ++ build' - backend' = case backend of - Cardano _ -> "cardano-sl" - Mantis -> "mantis" - ext = case os of + parts = ["daedalus", fromVer ver, lshowText cluster] ++ build' + _backend' = case backend of + Cardano _ -> "cardano-wallet" + Jormungandr _ -> "jormungandr-wallet" + ext = case _os of Win64 -> "exe" Macos64 -> "pkg" Linux64 -> "bin" - os' = case os of + _os' = case _os of Win64 -> "windows" Macos64 -> "macos" Linux64 -> "linux" @@ -142,8 +149,14 @@ withDir path = bracket (pwd >>= \old -> (cd path >> pure old)) cd . const data InstallerConfig = InstallerConfig { installDirectory :: Text + , spacedName :: Text , macPackageName :: Text - , walletPort :: Integer + , dataDir :: Text + , hasBlock0 :: Bool + , genesisPath :: Maybe Text + , secretPath :: Maybe Text + , configPath :: Maybe Text } deriving (Generic, Show) -instance Dhall.Interpret InstallerConfig +instance FromJSON InstallerConfig where + parseJSON = genericParseJSON defaultOptions diff --git a/installers/common/Util.hs b/installers/common/Util.hs index dda7f3470a..e3d475dd2e 100644 --- a/installers/common/Util.hs +++ b/installers/common/Util.hs @@ -5,12 +5,12 @@ module Util where import Control.Monad (mapM_) import Data.Text (Text) import System.Directory (listDirectory, withCurrentDirectory, removeDirectory, removeFile, doesDirectoryExist) -import Turtle (export, format, d) -import Data.Aeson (Value, Value(Object, String), decodeFileStrict', encodeFile) +import Turtle (export) +import Data.Aeson (Value, Value(Object, String), encodeFile, decodeFileStrict') import qualified Data.HashMap.Strict as HM import Config (Options(..), Backend(..)) -import Types (InstallerConfig(walletPort), fromBuildJob, clusterNetwork) +import Types (fromBuildJob, clusterNetwork) windowsRemoveDirectoryRecursive :: FilePath -> IO () windowsRemoveDirectoryRecursive path = do @@ -27,18 +27,17 @@ windowsRemoveDirectoryRecursive path = do -- "yarn package" build. -- When updating this, check that all variables are baked in with both -- webpack.config.js files. -exportBuildVars :: Options -> InstallerConfig -> Text -> IO () -exportBuildVars Options{oBackend, oBuildJob, oCluster} cfg backendVersion = do +exportBuildVars :: Options -> Text -> IO () +exportBuildVars Options{oBackend, oBuildJob, oCluster} backendVersion = do mapM_ (uncurry export) [ ("API", apiName oBackend) , ("API_VERSION", backendVersion) , ("BUILD_NUMBER", maybe "" fromBuildJob oBuildJob) , ("NETWORK", clusterNetwork oCluster) - , ("WALLET_PORT", format d (walletPort cfg)) ] where apiName (Cardano _) = "ada" - apiName Mantis = "etc" + apiName (Jormungandr _) = "ada" rewritePackageJson :: FilePath -> Text -> IO () rewritePackageJson path name = do diff --git a/installers/common/WindowsInstaller.hs b/installers/common/WindowsInstaller.hs index 751f7a43ec..82f82659f5 100644 --- a/installers/common/WindowsInstaller.hs +++ b/installers/common/WindowsInstaller.hs @@ -1,5 +1,7 @@ {-# LANGUAGE RecordWildCards, LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE NamedFieldPuns #-} + module WindowsInstaller ( main , writeInstallerNSIS @@ -12,6 +14,7 @@ import Control.Monad (unless) import qualified Data.List as L import Data.Text (Text, unpack) import qualified Data.Text as T +import Data.Yaml (decodeFileThrow) import Development.NSIS (Attrib (IconFile, IconIndex, RebootOK, Recursive, Required, StartOptions, Target), HKEY (HKLM), Level (Highest), Page (Directory, InstFiles), abort, constant, constantStr, createDirectory, createShortcut, delete, @@ -51,6 +54,7 @@ writeUninstallerNSIS (Version fullVersion) installerConfig = do IO.writeFile "uninstaller.nsi" $ nsis $ do _ <- constantStr "Version" (str $ unpack fullVersion) _ <- constantStr "InstallDir" (str $ unpack $ installDirectory installerConfig) + _ <- constantStr "SpacedName" (str $ unpack $ spacedName installerConfig) unsafeInjectGlobal "Unicode true" loadLanguage "English" @@ -61,7 +65,7 @@ writeUninstallerNSIS (Version fullVersion) installerConfig = do -- , "LangString UninstallName ${LANG_JAPANESE} \"アンインストーラー\"" -- ] - name "$InstallDir Uninstaller $Version" + name "$SpacedName Uninstaller $Version" -- TODO, the nsis library doesnt support translation vars -- name "$InstallDir $(UninstallName) $Version" --unsafeInjectGlobal $ unpack ( "Name \"" <> (installDirectory installerConfig) <> " $(UninstallName) " <> (fullVersion) <> "\"") @@ -74,11 +78,11 @@ writeUninstallerNSIS (Version fullVersion) installerConfig = do uninstall $ do -- Remove registry keys - deleteRegKey HKLM "Software/Microsoft/Windows/CurrentVersion/Uninstall/$InstallDir" - deleteRegKey HKLM "Software/$InstallDir" + deleteRegKey HKLM "Software/Microsoft/Windows/CurrentVersion/Uninstall/$SpacedName" + deleteRegKey HKLM "Software/$SpacedName" rmdir [Recursive,RebootOK] "$INSTDIR" - delete [] "$SMPROGRAMS/$InstallDir/*.*" - delete [] "$DESKTOP\\$InstallDir.lnk" + delete [] "$SMPROGRAMS/$SpacedName/*.*" + delete [] "$DESKTOP\\$SpacedName.lnk" mapM_ unsafeInject [ "liteFirewall::RemoveRule \"$INSTDIR\\cardano-node.exe\" \"Cardano Node\"" , "Pop $0" @@ -128,8 +132,8 @@ parseVersion ver = v@[_, _, _, _] -> map toString v _ -> ["0", "0", "0", "0"] -writeInstallerNSIS :: FilePath -> Version -> InstallerConfig -> Cluster -> IO () -writeInstallerNSIS outName (Version fullVersion') installerConfig clusterName = do +writeInstallerNSIS :: FilePath -> Version -> InstallerConfig -> Options -> Cluster -> IO () +writeInstallerNSIS outName (Version fullVersion') InstallerConfig{hasBlock0,installDirectory,spacedName} Options{oBackend} clusterName = do tempDir <- getTempDir let fullVersion = unpack fullVersion' viProductVersion = L.intercalate "." $ parseVersion fullVersion' @@ -138,8 +142,9 @@ writeInstallerNSIS outName (Version fullVersion') installerConfig clusterName = IO.writeFile "daedalus.nsi" $ nsis $ do _ <- constantStr "Version" (str fullVersion) _ <- constantStr "Cluster" (str $ lshow clusterName) - _ <- constantStr "InstallDir" (str $ unpack $ installDirectory installerConfig) - name "$InstallDir ($Version)" -- The name of the installer + _ <- constantStr "InstallDir" (str $ unpack installDirectory) + _ <- constantStr "SpacedName" (str $ unpack spacedName) + name "$SpacedName ($Version)" -- The name of the installer outFile $ str $ encodeString outName -- Where to produce the installer unsafeInjectGlobal $ "!define MUI_ICON \"icons\\" ++ lshow clusterName ++ "\\" ++ lshow clusterName ++ ".ico\"" unsafeInjectGlobal $ "!define MUI_HEADERIMAGE" @@ -151,11 +156,11 @@ writeInstallerNSIS outName (Version fullVersion') installerConfig clusterName = requestExecutionLevel Highest unsafeInjectGlobal "!addplugindir \"nsis_plugins\\liteFirewall\\bin\"" - installDir "$PROGRAMFILES64\\$InstallDir" -- Default installation directory... - installDirRegKey HKLM "Software/$InstallDir" "Install_Dir" -- ...except when already installed. + installDir "$PROGRAMFILES64\\$SpacedName" -- Default installation directory... + installDirRegKey HKLM "Software/$SpacedName" "Install_Dir" -- ...except when already installed. page Directory -- Pick where to install - _ <- constant "INSTALLEDAT" $ readRegStr HKLM "Software/$InstallDir" "Install_Dir" + _ <- constant "INSTALLEDAT" $ readRegStr HKLM "Software/$SpacedName" "Install_Dir" onPagePre Directory (iff_ (strLength "$INSTALLEDAT" %/= 0) $ abort "") page InstFiles -- Give a progress bar while installing @@ -170,25 +175,46 @@ writeInstallerNSIS outName (Version fullVersion') installerConfig clusterName = _ <- section "" [Required] $ do setOutPath "$INSTDIR" -- Where to install files in this section unsafeInject "AllowSkipFiles off" - writeRegStr HKLM "Software/$InstallDir" "Install_Dir" "$INSTDIR" -- Used by launcher batch script + writeRegStr HKLM "Software/$SpacedName" "Install_Dir" "$INSTDIR" -- Used by launcher batch script createDirectory "$APPDATA\\$InstallDir\\Secrets-1.0" createDirectory "$APPDATA\\$InstallDir\\Logs" createDirectory "$APPDATA\\$InstallDir\\Logs\\pub" - onError (delete [] "$APPDATA\\$InstallDir\\launcher.lock") $ - --abort "$InstallDir $(AlreadyRunning)" - unsafeInject $ unpack $ "Abort \" " <> (installDirectory installerConfig) <> "$(AlreadyRunning)\"" + onError (delete [] "$APPDATA\\$InstallDir\\daedalus_lockfile") $ + --abort "$SpacedName $(AlreadyRunning)" + unsafeInject $ unpack $ "Abort \" " <> installDirectory <> "$(AlreadyRunning)\"" iff_ (fileExists "$APPDATA\\$InstallDir\\Wallet-1.0\\open\\*.*") $ rmdir [] "$APPDATA\\$InstallDir\\Wallet-1.0\\open" - file [] "cardano-node.exe" + case oBackend of + Jormungandr _ -> do + file [] "jormungandr.exe" + file [] "cardano-wallet-jormungandr.exe" + file [] "config.yaml" + Cardano _ -> do + file [] "cardano-node.exe" + file [] "cardano-wallet-byron.exe" + file [] "export-wallets.exe" + file [] "cardano-cli.exe" + file [] "config.yaml" + file [] "topology.yaml" + file [] "genesis.json" + when (clusterName == Selfnode) $ do + file [] "signing.key" + file [] "delegation.cert" file [] "cardano-launcher.exe" - file [] "cardano-x509-certificates.exe" - file [] "log-config-prod.yaml" - file [] "wallet-topology.yaml" - file [] "configuration.yaml" - file [] "*genesis*.json" + file [] "libffi-6.dll" + --file [] "cardano-x509-certificates.exe" + --file [] "log-config-prod.yaml" + --file [] "wallet-topology.yaml" + --file [] "configuration.yaml" + --file [] "*genesis*.json" file [] "launcher-config.yaml" + when hasBlock0 $ + file [] "block-0.bin" + when (clusterName == ITN_Selfnode) $ do + file [] "genesis.yaml" + file [] "secret.yaml" file [Recursive] "dlls\\" - file [Recursive] "..\\release\\win32-x64\\$InstallDir-win32-x64\\" + file [Recursive] "..\\release\\win32-x64\\$SpacedName-win32-x64\\" mapM_ unsafeInject [ "liteFirewall::AddRule \"$INSTDIR\\cardano-node.exe\" \"Cardano Node\"" @@ -196,18 +222,18 @@ writeInstallerNSIS outName (Version fullVersion') installerConfig clusterName = , "DetailPrint \"liteFirewall::AddRule: $0\"" ] - createShortcut "$DESKTOP\\$InstallDir.lnk" (daedalusShortcut $ installDirectory installerConfig) + createShortcut "$DESKTOP\\$SpacedName.lnk" (daedalusShortcut spacedName) -- Uninstaller let - uninstallKey = "Software/Microsoft/Windows/CurrentVersion/Uninstall/$InstallDir" + uninstallKey = "Software/Microsoft/Windows/CurrentVersion/Uninstall/$SpacedName" do writeRegStr HKLM uninstallKey "InstallLocation" "$INSTDIR" writeRegStr HKLM uninstallKey "Publisher" "IOHK" writeRegStr HKLM uninstallKey "ProductVersion" (str fullVersion) writeRegStr HKLM uninstallKey "VersionMajor" (str . (!! 0). parseVersion $ fullVersion') writeRegStr HKLM uninstallKey "VersionMinor" (str . (!! 1). parseVersion $ fullVersion') - writeRegStr HKLM uninstallKey "DisplayName" "$InstallDir" + writeRegStr HKLM uninstallKey "DisplayName" "$SpacedName" writeRegStr HKLM uninstallKey "DisplayVersion" (str fullVersion) writeRegStr HKLM uninstallKey "UninstallString" "\"$INSTDIR/uninstall.exe\"" writeRegStr HKLM uninstallKey "QuietUninstallString" "\"$INSTDIR/uninstall.exe\" /S" @@ -217,10 +243,10 @@ writeInstallerNSIS outName (Version fullVersion') installerConfig clusterName = -- this string never appears in the UI _ <- section "Start Menu Shortcuts" [] $ do - createDirectory "$SMPROGRAMS/$InstallDir" - createShortcut "$SMPROGRAMS/$InstallDir/Uninstall $InstallDir.lnk" + createDirectory "$SMPROGRAMS/$SpacedName" + createShortcut "$SMPROGRAMS/$SpacedName/Uninstall $SpacedName.lnk" [Target "$INSTDIR/uninstall.exe", IconFile "$INSTDIR/uninstall.exe", IconIndex 0] - createShortcut "$SMPROGRAMS/$InstallDir/$InstallDir.lnk" (daedalusShortcut $ installDirectory installerConfig) + createShortcut "$SMPROGRAMS/$SpacedName/$SpacedName.lnk" (daedalusShortcut installDirectory) return () lshow :: Show a => a -> String @@ -243,10 +269,9 @@ packageFrontend cluster installerConfig = do -- | The contract of `main` is not to produce unsigned installer binaries. main :: Options -> IO () main opts@Options{..} = do - generateOSClusterConfigs "./dhall" "." opts cp (fromText "launcher-config.yaml") (fromText "../launcher-config.yaml") - installerConfig <- getInstallerConfig "./dhall" Win64 oCluster + installerConfig <- decodeFileThrow "installer-config.json" fetchCardanoSL "." printCardanoBuildInfo "." @@ -255,7 +280,7 @@ main opts@Options{..} = do ver <- getCardanoVersion echo "Packaging frontend" - exportBuildVars opts installerConfig ver + exportBuildVars opts ver packageFrontend oCluster installerConfig let fullName = packageFileName Win64 oCluster fullVersion oBackend ver oBuildJob @@ -273,7 +298,7 @@ main opts@Options{..} = do signUninstaller opts echo "Writing daedalus.nsi" - writeInstallerNSIS fullName fullVersion installerConfig oCluster + writeInstallerNSIS fullName fullVersion installerConfig opts oCluster rawnsi <- readFile "daedalus.nsi" putStr rawnsi diff --git a/installers/daedalus-installer.cabal b/installers/daedalus-installer.cabal index 2a5f34c8c7..c34786d380 100644 --- a/installers/daedalus-installer.cabal +++ b/installers/daedalus-installer.cabal @@ -2,10 +2,10 @@ name: daedalus-installer version: 0.1.0.0 synopsis: Daedalus Installer Builder description: Please see README.md -license: MIT -author: Serokell -maintainer: Serokell -copyright: 2016 IOHK +license: Apache-2.0 +author: IOHK +maintainer: DevOps +copyright: 2019 IOHK build-type: Simple extra-source-files: README.md cabal-version: >=1.10 @@ -29,8 +29,6 @@ library , bytestring , unordered-containers , containers - , dhall - , dhall-json , directory , github , lens-aeson @@ -39,10 +37,12 @@ library , network-uri , nsis , optional-args + , raw-strings-qq , system-filepath , text , turtle , universum + , unix , wreq , yaml , zip-archive @@ -52,19 +52,20 @@ executable make-installer build-depends: base , bytestring , containers + , daedalus-installer , directory - , foldl , filepath + , foldl + , optional-args , optparse-applicative , optparse-generic - , optional-args , split , system-filepath , temporary , text , turtle , universum - , daedalus-installer + , yaml default-language: Haskell2010 ghc-options: -threaded -rtsopts @@ -76,6 +77,13 @@ executable make-installer default-extensions: NoImplicitPrelude OverloadedStrings +executable darwin-launcher + main-is: DarwinLauncher.hs + default-language: Haskell2010 + build-depends: base + , turtle + , unix + ghc-options: -Weverything test-suite test-make-installer type: exitcode-stdio-1.0 @@ -85,8 +93,6 @@ test-suite test-make-installer , aeson , bytestring , containers - , dhall - , dhall-json , directory , filepath , foldl @@ -98,6 +104,7 @@ test-suite test-make-installer , optional-args , optparse-applicative , optparse-generic + , raw-strings-qq , split , system-filepath , temporary diff --git a/installers/daedalus-installer.nix b/installers/daedalus-installer.nix index f5cd64646a..41771d00a5 100644 --- a/installers/daedalus-installer.nix +++ b/installers/daedalus-installer.nix @@ -1,8 +1,8 @@ -{ mkDerivation, aeson, base, bytestring, containers, dhall -, 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 +{ mkDerivation, aeson, base, bytestring, containers, 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, raw-strings-qq , unordered-containers, wreq, yaml, zip-archive }: mkDerivation { @@ -13,23 +13,22 @@ mkDerivation { isExecutable = true; doCheck = false; libraryHaskellDepends = [ - aeson base bytestring containers dhall dhall-json directory github - Glob lens-aeson megaparsec microlens network-uri nsis optional-args + aeson base bytestring containers directory github Glob lens-aeson + megaparsec microlens network-uri nsis optional-args raw-strings-qq system-filepath text turtle universum unordered-containers wreq yaml zip-archive ]; executableHaskellDepends = [ - aeson base bytestring containers dhall dhall-json directory - filepath foldl megaparsec optional-args optparse-applicative - optparse-generic split system-filepath temporary text turtle - universum yaml + base bytestring containers directory filepath foldl optional-args + optparse-applicative optparse-generic raw-strings-qq split + system-filepath temporary text turtle universum yaml ]; testHaskellDepends = [ - aeson base bytestring containers dhall dhall-json directory - filepath foldl hspec lens-aeson managed megaparsec optional-args - optparse-applicative optparse-generic split system-filepath - temporary text turtle universum yaml + aeson base bytestring containers directory filepath foldl github + hspec lens-aeson managed megaparsec optional-args + optparse-applicative optparse-generic raw-strings-qq split + system-filepath temporary text turtle universum yaml ]; description = "Daedalus Installer Builder"; - license = stdenv.lib.licenses.mit; + license = stdenv.lib.licenses.asl20; } diff --git a/installers/default.nix b/installers/default.nix index 1a40c9c923..aa6b72f8eb 100644 --- a/installers/default.nix +++ b/installers/default.nix @@ -1,54 +1,14 @@ { system ? builtins.currentSystem , config ? {} -, pkgs ? localLib.iohkNix.getPkgs { inherit system config; } - -# Disable running of tests for all local packages. -, forceDontCheck ? false - -# Enable profiling for all haskell packages. -# Profiling slows down performance by 50% so we don't enable it by default. -, enableProfiling ? false - -# Enable separation of build/check derivations. -, enableSplitCheck ? false - -# Keeps the debug information for all haskell packages. -, enableDebugging ? false - -# Build (but don't run) benchmarks for all local packages. -, enableBenchmarks ? false - -# Overrides all nix derivations to add build timing information in -# their build output. -, enablePhaseMetrics ? true - -# Overrides all nix derivations to add haddock hydra output. -, enableHaddockHydra ? false - -# Disables optimization in the build for all local packages. -, fasterBuild ? false +, localLib ? import ../lib.nix {} +, pkgs ? import localLib.sources.nixpkgs { inherit system config; } , daedalus-bridge -, localLib }: -with pkgs; -with haskell.lib; - let - inherit daedalus-bridge; - addTestStubsOverlay = import ./overlays/add-test-stubs.nix { - inherit pkgs daedalus-bridge; + haskellPackages = pkgs.haskellPackages.override { + overrides = import ./overlays/required.nix { inherit pkgs; }; }; - # We use GHC 8.2.2 because too many changes are needed to get to build with 8.4.3 - haskellPackages = callPackage localLib.iohkNix.haskellPackages { - inherit forceDontCheck enableProfiling enablePhaseMetrics enableHaddockHydra - enableBenchmarks fasterBuild enableDebugging enableSplitCheck; - pkgsGenerated = haskell.packages.ghc822; - ghc = haskell.compiler.ghc822; - filter = localLib.isDaedalus; - requiredOverlay = ./overlays/required.nix; - customOverlays = [ addTestStubsOverlay ]; - }; - + #haskellPackages = haskellPackages1.extend (import ./overlays/add-test-stubs.nix { inherit pkgs daedalus-bridge; }); -in haskellPackages +in haskellPackages // { inherit pkgs; } diff --git a/installers/dhall/cluster.type b/installers/dhall/cluster.type index 17b19c27f1..12dacd5d0d 100644 --- a/installers/dhall/cluster.type +++ b/installers/dhall/cluster.type @@ -1,9 +1,9 @@ -{ name : Text -, keyPrefix : Text -, relays : Text -, updateServer : Text +{ name : Text +, keyPrefix : Text +, relays : Text +, updateServer : Text , installDirectorySuffix : Text , macPackageSuffix : Text -, walletPort : Integer +, walletPort : Natural , extraNodeArgs : List Text } diff --git a/installers/dhall/demo.dhall b/installers/dhall/demo.dhall index 491380f5da..47af5f4672 100644 --- a/installers/dhall/demo.dhall +++ b/installers/dhall/demo.dhall @@ -5,5 +5,5 @@ , installDirectorySuffix = " Demo" , macPackageSuffix = "Demo" , walletPort = 8092 -, extraNodeArgs = [ "--metrics", "--ekg-server", "localhost:8085", "+RTS", "-T", "-RTS" ] : List Text +, extraNodeArgs = [ "--network", "local" ] : List Text } diff --git a/installers/dhall/launcher.dhall b/installers/dhall/launcher.dhall index c7914883aa..26a7bf33db 100644 --- a/installers/dhall/launcher.dhall +++ b/installers/dhall/launcher.dhall @@ -1,29 +1,12 @@ \(cluster : ./cluster.type) -> \(os : ./os.type) -> -{ configuration = - { filePath = os.configurationYaml - , key = "${cluster.keyPrefix}_${os.name}" - , systemStart = [] : Optional Integer - , seed = [] : Optional Integer - } -, nodeTimeoutSec = 60 +{ nodeTimeoutSec = 60 , walletArgs = [] : List Text , logsPrefix = os.nodeArgs.logsPrefix , tlsPath = os.nodeArgs.tlsPath , x509ToolPath = os.x509ToolPath +, nodeImplementation = "jormungandr" , nodeArgs = - [ "--tlsca", "${os.nodeArgs.tlsPath}/server/ca.crt" - , "--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:0" - -- XXX: this is a workaround for Linux - , "--update-with-package" - ] # cluster.extraNodeArgs + [ + ] : List Text } // os.pass diff --git a/installers/dhall/linux64.dhall b/installers/dhall/linux64.dhall index fa0015ccd3..93b65a6e70 100644 --- a/installers/dhall/linux64.dhall +++ b/installers/dhall/linux64.dhall @@ -1,35 +1,34 @@ \(cluster : ./cluster.type) -> let dataDir = "\${XDG_DATA_HOME}/Daedalus/${cluster.name}" in -{ name = "linux64" -, configurationYaml = "\${DAEDALUS_CONFIG}/configuration.yaml" +{ name = "linux64" , installDirectory = "" , macPackageName = "unused" -, x509ToolPath = "cardano-x509-certificates" +, x509ToolPath = None Text , nodeArgs = - { keyfile = "${dataDir}/Secrets/secret.key" - , logsPrefix = "${dataDir}/Logs" - , topology = "\${DAEDALUS_CONFIG}/wallet-topology.yaml" - , updateLatestPath = "${dataDir}/installer.sh" - , walletDBPath = "${dataDir}/Wallet" - , tlsPath = "${dataDir}/tls" + { logsPrefix = "${dataDir}/Logs" + , topology = None Text + , updateLatestPath = None Text + , statePath = "${dataDir}/state" + , tlsPath = None Text } , pass = { statePath = dataDir , workingDir = dataDir - , nodePath = "cardano-node" - , nodeDbPath = "${dataDir}/DB/" - , nodeLogConfig = "\${DAEDALUS_CONFIG}/log-config-prod.yaml" - , nodeLogPath = [] : Optional Text - , walletPath = "daedalus-frontend" + , nodeBin = "jormungandr" + , walletBin = "cardano-wallet-jormungandr" + , daedalusBin = "daedalus-frontend" + , cliPath = "jcli" + , nodeLogConfig = None Text + , nodeLogPath = None Text , walletLogging = False , frontendOnlyMode = True -- todo, find some way to disable updates when unsandboxed? - , updaterPath = "/bin/update-runner" + , updaterPath = None Text , updaterArgs = [] : List Text - , updateArchive = [ "${dataDir}/installer.sh" ] : Optional Text - , updateWindowsRunner = [] : Optional Text + , updateArchive = None Text + , updateWindowsRunner = None Text , launcherLogsPrefix = "${dataDir}/Logs/" } diff --git a/installers/dhall/macos64.dhall b/installers/dhall/macos64.dhall index 614234d992..13d9715770 100644 --- a/installers/dhall/macos64.dhall +++ b/installers/dhall/macos64.dhall @@ -4,34 +4,33 @@ let dataDir = "\${HOME}/Library/Application Support/Daedalus${cluster.installDir -- in { name = "macos64" -, configurationYaml = "\${DAEDALUS_INSTALL_DIRECTORY}/configuration.yaml" , installDirectory = "Daedalus${cluster.installDirectorySuffix}" , macPackageName = "Daedalus${cluster.macPackageSuffix}" -, x509ToolPath = "\${DAEDALUS_INSTALL_DIRECTORY}/cardano-x509-certificates" +, x509ToolPath = None Text , nodeArgs = - { keyfile = "${dataDir}/Secrets-1.0/secret.key" - , logsPrefix = "${dataDir}/Logs" - , topology = "\${DAEDALUS_INSTALL_DIRECTORY}/wallet-topology.yaml" - , updateLatestPath = "${dataDir}/installer.pkg" - , walletDBPath = "${dataDir}/Wallet-1.0" - , tlsPath = "${dataDir}/tls" + { logsPrefix = "${dataDir}/Logs" + , topology = None Text + , updateLatestPath = None Text + , statePath = "${dataDir}/state" + , tlsPath = None Text } , pass = { statePath = dataDir , workingDir = dataDir - , nodePath = "\${DAEDALUS_INSTALL_DIRECTORY}/cardano-node" - , nodeDbPath = "${dataDir}/DB-1.0" - , nodeLogConfig = "\${DAEDALUS_INSTALL_DIRECTORY}/log-config-prod.yaml" - , nodeLogPath = [] : Optional Text + , nodeBin = "\${DAEDALUS_INSTALL_DIRECTORY}/jormungandr" + , walletBin = "\${DAEDALUS_INSTALL_DIRECTORY}/cardano-wallet-jormungandr.exe" + , daedalusBin = "\${DAEDALUS_INSTALL_DIRECTORY}/Frontend" + , cliPath = "\${DAEDALUS_INSTALL_DIRECTORY}/jcli" + , nodeLogConfig = None Text + , nodeLogPath = None Text - , walletPath = "\${DAEDALUS_INSTALL_DIRECTORY}/Frontend" , walletLogging = True , frontendOnlyMode = True - , updaterPath = "/usr/bin/open" - , updaterArgs = ["-FW"] - , updateArchive = ["${dataDir}/installer.pkg"] : Optional Text - , updateWindowsRunner = [] : Optional Text + , updaterPath = None Text + , updaterArgs = [] : List Text + , updateArchive = None Text + , updateWindowsRunner = None Text , launcherLogsPrefix = "${dataDir}/Logs/pub/" } diff --git a/installers/dhall/mainnet.dhall b/installers/dhall/mainnet.dhall index ddde8553f7..a46ec9c127 100644 --- a/installers/dhall/mainnet.dhall +++ b/installers/dhall/mainnet.dhall @@ -5,5 +5,5 @@ , installDirectorySuffix = "" , macPackageSuffix = "" , walletPort = 8090 -, extraNodeArgs = [] : List Text +, extraNodeArgs = [ "--network", "mainnet" ] : List Text } diff --git a/installers/dhall/os.type b/installers/dhall/os.type index c0e44e3363..eed4ebec44 100644 --- a/installers/dhall/os.type +++ b/installers/dhall/os.type @@ -1,27 +1,26 @@ { name : Text -, configurationYaml : Text , installDirectory : Text , macPackageName : Text -, x509ToolPath : Text +, x509ToolPath : Optional Text , nodeArgs : - { keyfile : Text - , logsPrefix : Text - , topology : Text - , updateLatestPath : Text - , walletDBPath : Text - , tlsPath : Text + { logsPrefix : Text + , topology : Optional Text + , updateLatestPath : Optional Text + , statePath : Text + , tlsPath : Optional Text } , pass : { statePath : Text - , nodePath : Text - , nodeDbPath : Text - , nodeLogConfig : Text + , nodeBin : Text + , daedalusBin : Text + , walletBin : Text + , cliPath : Text + , nodeLogConfig : Optional Text , nodeLogPath : Optional Text - , walletPath : Text , walletLogging : Bool , workingDir : Text , frontendOnlyMode : Bool - , updaterPath : Text + , updaterPath : Optional Text , updaterArgs : List Text , updateArchive : Optional Text , updateWindowsRunner : Optional Text diff --git a/installers/dhall/staging.dhall b/installers/dhall/staging.dhall index 6aca3c1aa5..4c6831d1f5 100644 --- a/installers/dhall/staging.dhall +++ b/installers/dhall/staging.dhall @@ -5,5 +5,5 @@ , installDirectorySuffix = " Staging" , macPackageSuffix = "Staging" , walletPort = 8092 -, extraNodeArgs = [ "--metrics", "--ekg-server", "localhost:8082", "+RTS", "-T", "-RTS" ] : List Text +, extraNodeArgs = [ "--network", "staging" ] : List Text } diff --git a/installers/dhall/testnet.dhall b/installers/dhall/testnet.dhall index 7260f13ebe..d9ee5b86f0 100644 --- a/installers/dhall/testnet.dhall +++ b/installers/dhall/testnet.dhall @@ -5,5 +5,5 @@ , installDirectorySuffix = " Testnet" , macPackageSuffix = "Testnet" , walletPort = 8094 -, extraNodeArgs = [ "--metrics", "--ekg-server", "localhost:8081", "+RTS", "-T", "-RTS" ] : List Text +, extraNodeArgs = [ "--network", "testnet" ] : List Text } diff --git a/installers/dhall/win64.dhall b/installers/dhall/win64.dhall index c17507ca03..a33aa687ee 100644 --- a/installers/dhall/win64.dhall +++ b/installers/dhall/win64.dhall @@ -4,35 +4,34 @@ in let dataDir = "\${APPDATA}\\${installDir}" -- -- in -{ name = "win64" -, configurationYaml = "\${DAEDALUS_INSTALL_DIRECTORY}\\configuration.yaml" +{ name = "win64" , installDirectory = installDir , macPackageName = "unused" -, x509ToolPath = "\${DAEDALUS_DIR}\\cardano-x509-certificates.exe" +, x509ToolPath = None Text , nodeArgs = - { keyfile = "Secrets-1.0\\secret.key" - , logsPrefix = "Logs" - , topology = "\${DAEDALUS_DIR}\\wallet-topology.yaml" - , updateLatestPath = "Installer.exe" - , walletDBPath = "Wallet-1.0" - , tlsPath = "tls" + { logsPrefix = "Logs" + , topology = None Text + , updateLatestPath = None Text + , statePath = "state" + , tlsPath = None Text } , pass = { statePath = dataDir , workingDir = dataDir - , nodePath = "\${DAEDALUS_DIR}\\cardano-node.exe" - , nodeDbPath = "DB-1.0" - , nodeLogConfig = "\${DAEDALUS_INSTALL_DIRECTORY}\\log-config-prod.yaml" - , nodeLogPath = [] : Optional Text + , nodeBin = "\${DAEDALUS_INSTALL_DIRECTORY}\\jormungandr.exe" + , walletBin = "\${DAEDALUS_INSTALL_DIRECTORY}\\cardano-wallet-jormungandr.exe" + , daedalusBin = "\${DAEDALUS_INSTALL_DIRECTORY}\\${installDir}.exe" + , cliPath = "\${DAEDALUS_INSTALL_DIRECTORY}\\jcli.exe" + , nodeLogConfig = None Text + , nodeLogPath = None Text - , walletPath = "\${DAEDALUS_DIR}\\${installDir}.exe" , walletLogging = True , frontendOnlyMode = True - , updaterPath = "Installer.exe" + , updaterPath = None Text , updaterArgs = [] : List Text - , updateArchive = [] : Optional Text - , updateWindowsRunner = ["Installer.bat"] : Optional Text + , updateArchive = None Text + , updateWindowsRunner = Some "Installer.bat" , launcherLogsPrefix = "Logs\\pub" } diff --git a/installers/icons/itn_rewards_v1.iconset/icon_1024x1024.png b/installers/icons/itn_rewards_v1.iconset/icon_1024x1024.png new file mode 100644 index 0000000000..9b7bbe6bf3 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_1024x1024.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_1024x1024@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_1024x1024@2x.png new file mode 100644 index 0000000000..79e6fe941a Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_1024x1024@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_128x128.png b/installers/icons/itn_rewards_v1.iconset/icon_128x128.png new file mode 100644 index 0000000000..a5ea7d200b Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_128x128.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_128x128@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_128x128@2x.png new file mode 100644 index 0000000000..a085ce36df Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_128x128@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_16x16.png b/installers/icons/itn_rewards_v1.iconset/icon_16x16.png new file mode 100644 index 0000000000..ab2bbad5b8 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_16x16.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_16x16@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000..0c37aba948 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_16x16@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_18x18.png b/installers/icons/itn_rewards_v1.iconset/icon_18x18.png new file mode 100644 index 0000000000..3b40a9bc8d Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_18x18.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_18x18@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_18x18@2x.png new file mode 100644 index 0000000000..27817a5625 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_18x18@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_19x19.png b/installers/icons/itn_rewards_v1.iconset/icon_19x19.png new file mode 100644 index 0000000000..9df12b672f Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_19x19.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_19x19@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_19x19@2x.png new file mode 100644 index 0000000000..af23901ac8 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_19x19@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_22x22.png b/installers/icons/itn_rewards_v1.iconset/icon_22x22.png new file mode 100644 index 0000000000..1612f54431 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_22x22.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_22x22@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_22x22@2x.png new file mode 100644 index 0000000000..613b628db1 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_22x22@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_24x24.png b/installers/icons/itn_rewards_v1.iconset/icon_24x24.png new file mode 100644 index 0000000000..936e1cfb3d Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_24x24.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_24x24@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_24x24@2x.png new file mode 100644 index 0000000000..3b888304c7 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_24x24@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_256x256.png b/installers/icons/itn_rewards_v1.iconset/icon_256x256.png new file mode 100644 index 0000000000..9256e02e84 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_256x256.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_256x256@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000..ff5423daf5 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_256x256@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_32x32.png b/installers/icons/itn_rewards_v1.iconset/icon_32x32.png new file mode 100644 index 0000000000..eca87127ff Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_32x32.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_32x32@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_32x32@2x.png new file mode 100644 index 0000000000..02747700f0 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_32x32@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_40x40.png b/installers/icons/itn_rewards_v1.iconset/icon_40x40.png new file mode 100644 index 0000000000..891bb287a6 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_40x40.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_40x40@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_40x40@2x.png new file mode 100644 index 0000000000..7d3d3c3a38 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_40x40@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_48x48.png b/installers/icons/itn_rewards_v1.iconset/icon_48x48.png new file mode 100644 index 0000000000..62a9f7f2ec Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_48x48.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_48x48@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_48x48@2x.png new file mode 100644 index 0000000000..2e1d664925 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_48x48@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_512x512.png b/installers/icons/itn_rewards_v1.iconset/icon_512x512.png new file mode 100644 index 0000000000..acb0e703bc Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_512x512.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_512x512@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000..c6e24604eb Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_512x512@2x.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_64x64.png b/installers/icons/itn_rewards_v1.iconset/icon_64x64.png new file mode 100644 index 0000000000..e6e930fac2 Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_64x64.png differ diff --git a/installers/icons/itn_rewards_v1.iconset/icon_64x64@2x.png b/installers/icons/itn_rewards_v1.iconset/icon_64x64@2x.png new file mode 100644 index 0000000000..d59de7370b Binary files /dev/null and b/installers/icons/itn_rewards_v1.iconset/icon_64x64@2x.png differ diff --git a/installers/icons/itn_rewards_v1/1024x1024.png b/installers/icons/itn_rewards_v1/1024x1024.png new file mode 100644 index 0000000000..9b7bbe6bf3 Binary files /dev/null and b/installers/icons/itn_rewards_v1/1024x1024.png differ diff --git a/installers/icons/itn_rewards_v1/128x128.ico b/installers/icons/itn_rewards_v1/128x128.ico new file mode 100644 index 0000000000..3ae2615e34 Binary files /dev/null and b/installers/icons/itn_rewards_v1/128x128.ico differ diff --git a/installers/icons/itn_rewards_v1/128x128.png b/installers/icons/itn_rewards_v1/128x128.png new file mode 100644 index 0000000000..a5ea7d200b Binary files /dev/null and b/installers/icons/itn_rewards_v1/128x128.png differ diff --git a/installers/icons/itn_rewards_v1/16x16.ico b/installers/icons/itn_rewards_v1/16x16.ico new file mode 100644 index 0000000000..10bc3c93a6 Binary files /dev/null and b/installers/icons/itn_rewards_v1/16x16.ico differ diff --git a/installers/icons/itn_rewards_v1/16x16.png b/installers/icons/itn_rewards_v1/16x16.png new file mode 100644 index 0000000000..ab2bbad5b8 Binary files /dev/null and b/installers/icons/itn_rewards_v1/16x16.png differ diff --git a/installers/icons/itn_rewards_v1/18x18.ico b/installers/icons/itn_rewards_v1/18x18.ico new file mode 100644 index 0000000000..b747d287cd Binary files /dev/null and b/installers/icons/itn_rewards_v1/18x18.ico differ diff --git a/installers/icons/itn_rewards_v1/18x18.png b/installers/icons/itn_rewards_v1/18x18.png new file mode 100644 index 0000000000..3b40a9bc8d Binary files /dev/null and b/installers/icons/itn_rewards_v1/18x18.png differ diff --git a/installers/icons/itn_rewards_v1/19x19.ico b/installers/icons/itn_rewards_v1/19x19.ico new file mode 100644 index 0000000000..aab78223fa Binary files /dev/null and b/installers/icons/itn_rewards_v1/19x19.ico differ diff --git a/installers/icons/itn_rewards_v1/19x19.png b/installers/icons/itn_rewards_v1/19x19.png new file mode 100644 index 0000000000..9df12b672f Binary files /dev/null and b/installers/icons/itn_rewards_v1/19x19.png differ diff --git a/installers/icons/itn_rewards_v1/22x22.ico b/installers/icons/itn_rewards_v1/22x22.ico new file mode 100644 index 0000000000..fc76a53eef Binary files /dev/null and b/installers/icons/itn_rewards_v1/22x22.ico differ diff --git a/installers/icons/itn_rewards_v1/22x22.png b/installers/icons/itn_rewards_v1/22x22.png new file mode 100644 index 0000000000..1612f54431 Binary files /dev/null and b/installers/icons/itn_rewards_v1/22x22.png differ diff --git a/installers/icons/itn_rewards_v1/24x24.ico b/installers/icons/itn_rewards_v1/24x24.ico new file mode 100644 index 0000000000..e048aec592 Binary files /dev/null and b/installers/icons/itn_rewards_v1/24x24.ico differ diff --git a/installers/icons/itn_rewards_v1/24x24.png b/installers/icons/itn_rewards_v1/24x24.png new file mode 100644 index 0000000000..936e1cfb3d Binary files /dev/null and b/installers/icons/itn_rewards_v1/24x24.png differ diff --git a/installers/icons/itn_rewards_v1/256x256.ico b/installers/icons/itn_rewards_v1/256x256.ico new file mode 100644 index 0000000000..672decbc30 Binary files /dev/null and b/installers/icons/itn_rewards_v1/256x256.ico differ diff --git a/installers/icons/itn_rewards_v1/256x256.png b/installers/icons/itn_rewards_v1/256x256.png new file mode 100644 index 0000000000..9256e02e84 Binary files /dev/null and b/installers/icons/itn_rewards_v1/256x256.png differ diff --git a/installers/icons/itn_rewards_v1/32x32.ico b/installers/icons/itn_rewards_v1/32x32.ico new file mode 100644 index 0000000000..8cb6dba844 Binary files /dev/null and b/installers/icons/itn_rewards_v1/32x32.ico differ diff --git a/installers/icons/itn_rewards_v1/32x32.png b/installers/icons/itn_rewards_v1/32x32.png new file mode 100644 index 0000000000..eca87127ff Binary files /dev/null and b/installers/icons/itn_rewards_v1/32x32.png differ diff --git a/installers/icons/itn_rewards_v1/40x40.ico b/installers/icons/itn_rewards_v1/40x40.ico new file mode 100644 index 0000000000..95ba1f2c33 Binary files /dev/null and b/installers/icons/itn_rewards_v1/40x40.ico differ diff --git a/installers/icons/itn_rewards_v1/40x40.png b/installers/icons/itn_rewards_v1/40x40.png new file mode 100644 index 0000000000..891bb287a6 Binary files /dev/null and b/installers/icons/itn_rewards_v1/40x40.png differ diff --git a/installers/icons/itn_rewards_v1/48x48.ico b/installers/icons/itn_rewards_v1/48x48.ico new file mode 100644 index 0000000000..196a00bf48 Binary files /dev/null and b/installers/icons/itn_rewards_v1/48x48.ico differ diff --git a/installers/icons/itn_rewards_v1/48x48.png b/installers/icons/itn_rewards_v1/48x48.png new file mode 100644 index 0000000000..62a9f7f2ec Binary files /dev/null and b/installers/icons/itn_rewards_v1/48x48.png differ diff --git a/installers/icons/itn_rewards_v1/512x512.png b/installers/icons/itn_rewards_v1/512x512.png new file mode 100644 index 0000000000..acb0e703bc Binary files /dev/null and b/installers/icons/itn_rewards_v1/512x512.png differ diff --git a/installers/icons/itn_rewards_v1/64x64.ico b/installers/icons/itn_rewards_v1/64x64.ico new file mode 100644 index 0000000000..4db26d2f42 Binary files /dev/null and b/installers/icons/itn_rewards_v1/64x64.ico differ diff --git a/installers/icons/itn_rewards_v1/64x64.png b/installers/icons/itn_rewards_v1/64x64.png new file mode 100644 index 0000000000..e6e930fac2 Binary files /dev/null and b/installers/icons/itn_rewards_v1/64x64.png differ diff --git a/installers/icons/itn_rewards_v1/itn_rewards_v1.ico b/installers/icons/itn_rewards_v1/itn_rewards_v1.ico new file mode 100644 index 0000000000..1f95354083 Binary files /dev/null and b/installers/icons/itn_rewards_v1/itn_rewards_v1.ico differ diff --git a/installers/icons/itn_selfnode.iconset/icon_1024x1024.png b/installers/icons/itn_selfnode.iconset/icon_1024x1024.png new file mode 100644 index 0000000000..1bf9f6e6d1 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_1024x1024.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_1024x1024@2x.png b/installers/icons/itn_selfnode.iconset/icon_1024x1024@2x.png new file mode 100644 index 0000000000..49eba42eec Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_1024x1024@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_128x128.png b/installers/icons/itn_selfnode.iconset/icon_128x128.png new file mode 100644 index 0000000000..40d5d43702 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_128x128.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_128x128@2x.png b/installers/icons/itn_selfnode.iconset/icon_128x128@2x.png new file mode 100644 index 0000000000..0047e51d6f Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_128x128@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_16x16.png b/installers/icons/itn_selfnode.iconset/icon_16x16.png new file mode 100644 index 0000000000..c9d740ee1f Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_16x16.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_16x16@2x.png b/installers/icons/itn_selfnode.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000..3a9033c203 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_16x16@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_18x18.png b/installers/icons/itn_selfnode.iconset/icon_18x18.png new file mode 100644 index 0000000000..836e4d6364 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_18x18.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_18x18@2x.png b/installers/icons/itn_selfnode.iconset/icon_18x18@2x.png new file mode 100644 index 0000000000..8f094407eb Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_18x18@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_19x19.png b/installers/icons/itn_selfnode.iconset/icon_19x19.png new file mode 100644 index 0000000000..9943ddf6fa Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_19x19.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_19x19@2x.png b/installers/icons/itn_selfnode.iconset/icon_19x19@2x.png new file mode 100644 index 0000000000..2a63ef81a5 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_19x19@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_22x22.png b/installers/icons/itn_selfnode.iconset/icon_22x22.png new file mode 100644 index 0000000000..485ebd97be Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_22x22.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_22x22@2x.png b/installers/icons/itn_selfnode.iconset/icon_22x22@2x.png new file mode 100644 index 0000000000..879f57396e Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_22x22@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_24x24.png b/installers/icons/itn_selfnode.iconset/icon_24x24.png new file mode 100644 index 0000000000..320730ff56 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_24x24.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_24x24@2x.png b/installers/icons/itn_selfnode.iconset/icon_24x24@2x.png new file mode 100644 index 0000000000..eeb134ced0 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_24x24@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_256x256.png b/installers/icons/itn_selfnode.iconset/icon_256x256.png new file mode 100644 index 0000000000..d34553f83e Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_256x256.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_256x256@2x.png b/installers/icons/itn_selfnode.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000..7d7cfcffee Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_256x256@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_32x32.png b/installers/icons/itn_selfnode.iconset/icon_32x32.png new file mode 100644 index 0000000000..bfb1f9059d Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_32x32.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_32x32@2x.png b/installers/icons/itn_selfnode.iconset/icon_32x32@2x.png new file mode 100644 index 0000000000..890680bc93 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_32x32@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_40x40.png b/installers/icons/itn_selfnode.iconset/icon_40x40.png new file mode 100644 index 0000000000..cefebd807d Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_40x40.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_40x40@2x.png b/installers/icons/itn_selfnode.iconset/icon_40x40@2x.png new file mode 100644 index 0000000000..224cdea1d1 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_40x40@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_48x48.png b/installers/icons/itn_selfnode.iconset/icon_48x48.png new file mode 100644 index 0000000000..7af6d6edfd Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_48x48.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_48x48@2x.png b/installers/icons/itn_selfnode.iconset/icon_48x48@2x.png new file mode 100644 index 0000000000..103574fb4d Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_48x48@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_512x512.png b/installers/icons/itn_selfnode.iconset/icon_512x512.png new file mode 100644 index 0000000000..68e08d4bf9 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_512x512.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_512x512@2x.png b/installers/icons/itn_selfnode.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000..ffaf6cf0f6 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_512x512@2x.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_64x64.png b/installers/icons/itn_selfnode.iconset/icon_64x64.png new file mode 100644 index 0000000000..cea916fc4a Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_64x64.png differ diff --git a/installers/icons/itn_selfnode.iconset/icon_64x64@2x.png b/installers/icons/itn_selfnode.iconset/icon_64x64@2x.png new file mode 100644 index 0000000000..1ebe6df1a2 Binary files /dev/null and b/installers/icons/itn_selfnode.iconset/icon_64x64@2x.png differ diff --git a/installers/icons/itn_selfnode/1024x1024.png b/installers/icons/itn_selfnode/1024x1024.png new file mode 100644 index 0000000000..1bf9f6e6d1 Binary files /dev/null and b/installers/icons/itn_selfnode/1024x1024.png differ diff --git a/installers/icons/itn_selfnode/128x128.ico b/installers/icons/itn_selfnode/128x128.ico new file mode 100644 index 0000000000..ed9f67aa17 Binary files /dev/null and b/installers/icons/itn_selfnode/128x128.ico differ diff --git a/installers/icons/itn_selfnode/128x128.png b/installers/icons/itn_selfnode/128x128.png new file mode 100644 index 0000000000..40d5d43702 Binary files /dev/null and b/installers/icons/itn_selfnode/128x128.png differ diff --git a/installers/icons/itn_selfnode/16x16.ico b/installers/icons/itn_selfnode/16x16.ico new file mode 100644 index 0000000000..10bc3c93a6 Binary files /dev/null and b/installers/icons/itn_selfnode/16x16.ico differ diff --git a/installers/icons/itn_selfnode/16x16.png b/installers/icons/itn_selfnode/16x16.png new file mode 100644 index 0000000000..c9d740ee1f Binary files /dev/null and b/installers/icons/itn_selfnode/16x16.png differ diff --git a/installers/icons/itn_selfnode/18x18.ico b/installers/icons/itn_selfnode/18x18.ico new file mode 100644 index 0000000000..b747d287cd Binary files /dev/null and b/installers/icons/itn_selfnode/18x18.ico differ diff --git a/installers/icons/itn_selfnode/18x18.png b/installers/icons/itn_selfnode/18x18.png new file mode 100644 index 0000000000..836e4d6364 Binary files /dev/null and b/installers/icons/itn_selfnode/18x18.png differ diff --git a/installers/icons/itn_selfnode/19x19.ico b/installers/icons/itn_selfnode/19x19.ico new file mode 100644 index 0000000000..aab78223fa Binary files /dev/null and b/installers/icons/itn_selfnode/19x19.ico differ diff --git a/installers/icons/itn_selfnode/19x19.png b/installers/icons/itn_selfnode/19x19.png new file mode 100644 index 0000000000..9943ddf6fa Binary files /dev/null and b/installers/icons/itn_selfnode/19x19.png differ diff --git a/installers/icons/itn_selfnode/22x22.ico b/installers/icons/itn_selfnode/22x22.ico new file mode 100644 index 0000000000..fc76a53eef Binary files /dev/null and b/installers/icons/itn_selfnode/22x22.ico differ diff --git a/installers/icons/itn_selfnode/22x22.png b/installers/icons/itn_selfnode/22x22.png new file mode 100644 index 0000000000..485ebd97be Binary files /dev/null and b/installers/icons/itn_selfnode/22x22.png differ diff --git a/installers/icons/itn_selfnode/24x24.ico b/installers/icons/itn_selfnode/24x24.ico new file mode 100644 index 0000000000..e048aec592 Binary files /dev/null and b/installers/icons/itn_selfnode/24x24.ico differ diff --git a/installers/icons/itn_selfnode/24x24.png b/installers/icons/itn_selfnode/24x24.png new file mode 100644 index 0000000000..320730ff56 Binary files /dev/null and b/installers/icons/itn_selfnode/24x24.png differ diff --git a/installers/icons/itn_selfnode/256x256.ico b/installers/icons/itn_selfnode/256x256.ico new file mode 100644 index 0000000000..a14e2189e9 Binary files /dev/null and b/installers/icons/itn_selfnode/256x256.ico differ diff --git a/installers/icons/itn_selfnode/256x256.png b/installers/icons/itn_selfnode/256x256.png new file mode 100644 index 0000000000..d34553f83e Binary files /dev/null and b/installers/icons/itn_selfnode/256x256.png differ diff --git a/installers/icons/itn_selfnode/32x32.ico b/installers/icons/itn_selfnode/32x32.ico new file mode 100644 index 0000000000..8cb6dba844 Binary files /dev/null and b/installers/icons/itn_selfnode/32x32.ico differ diff --git a/installers/icons/itn_selfnode/32x32.png b/installers/icons/itn_selfnode/32x32.png new file mode 100644 index 0000000000..bfb1f9059d Binary files /dev/null and b/installers/icons/itn_selfnode/32x32.png differ diff --git a/installers/icons/itn_selfnode/40x40.ico b/installers/icons/itn_selfnode/40x40.ico new file mode 100644 index 0000000000..25d374d53b Binary files /dev/null and b/installers/icons/itn_selfnode/40x40.ico differ diff --git a/installers/icons/itn_selfnode/40x40.png b/installers/icons/itn_selfnode/40x40.png new file mode 100644 index 0000000000..cefebd807d Binary files /dev/null and b/installers/icons/itn_selfnode/40x40.png differ diff --git a/installers/icons/itn_selfnode/48x48.ico b/installers/icons/itn_selfnode/48x48.ico new file mode 100644 index 0000000000..25ee38aefe Binary files /dev/null and b/installers/icons/itn_selfnode/48x48.ico differ diff --git a/installers/icons/itn_selfnode/48x48.png b/installers/icons/itn_selfnode/48x48.png new file mode 100644 index 0000000000..7af6d6edfd Binary files /dev/null and b/installers/icons/itn_selfnode/48x48.png differ diff --git a/installers/icons/itn_selfnode/512x512.png b/installers/icons/itn_selfnode/512x512.png new file mode 100644 index 0000000000..68e08d4bf9 Binary files /dev/null and b/installers/icons/itn_selfnode/512x512.png differ diff --git a/installers/icons/itn_selfnode/64x64.ico b/installers/icons/itn_selfnode/64x64.ico new file mode 100644 index 0000000000..e743af029f Binary files /dev/null and b/installers/icons/itn_selfnode/64x64.ico differ diff --git a/installers/icons/itn_selfnode/64x64.png b/installers/icons/itn_selfnode/64x64.png new file mode 100644 index 0000000000..cea916fc4a Binary files /dev/null and b/installers/icons/itn_selfnode/64x64.png differ diff --git a/installers/icons/itn_selfnode/itn_selfnode.ico b/installers/icons/itn_selfnode/itn_selfnode.ico new file mode 100644 index 0000000000..c6c63a5685 Binary files /dev/null and b/installers/icons/itn_selfnode/itn_selfnode.ico differ diff --git a/installers/icons/mainnet.iconset/icon_1024x1024.png b/installers/icons/mainnet.iconset/icon_1024x1024.png new file mode 100644 index 0000000000..7ffbccdff5 Binary files /dev/null and b/installers/icons/mainnet.iconset/icon_1024x1024.png differ diff --git a/installers/icons/mainnet.iconset/icon_1024x1024@2x.png b/installers/icons/mainnet.iconset/icon_1024x1024@2x.png new file mode 100644 index 0000000000..14c0aa2d0f Binary files /dev/null and b/installers/icons/mainnet.iconset/icon_1024x1024@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_128x128.png b/installers/icons/mainnet.iconset/icon_128x128.png index 03e5a1b9f1..91a7863453 100644 Binary files a/installers/icons/mainnet.iconset/icon_128x128.png and b/installers/icons/mainnet.iconset/icon_128x128.png differ diff --git a/installers/icons/mainnet.iconset/icon_128x128@2x.png b/installers/icons/mainnet.iconset/icon_128x128@2x.png index 88844b9cbb..455c18dc67 100644 Binary files a/installers/icons/mainnet.iconset/icon_128x128@2x.png and b/installers/icons/mainnet.iconset/icon_128x128@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_16x16.png b/installers/icons/mainnet.iconset/icon_16x16.png index 6fbc82229b..ae53c1f6c4 100644 Binary files a/installers/icons/mainnet.iconset/icon_16x16.png and b/installers/icons/mainnet.iconset/icon_16x16.png differ diff --git a/installers/icons/mainnet.iconset/icon_16x16@2x.png b/installers/icons/mainnet.iconset/icon_16x16@2x.png index ec06686fb8..fdf49e74f2 100644 Binary files a/installers/icons/mainnet.iconset/icon_16x16@2x.png and b/installers/icons/mainnet.iconset/icon_16x16@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_18x18.png b/installers/icons/mainnet.iconset/icon_18x18.png new file mode 100644 index 0000000000..a36a25fd31 Binary files /dev/null and b/installers/icons/mainnet.iconset/icon_18x18.png differ diff --git a/installers/icons/mainnet.iconset/icon_18x18@2x.png b/installers/icons/mainnet.iconset/icon_18x18@2x.png new file mode 100644 index 0000000000..c64d578db8 Binary files /dev/null and b/installers/icons/mainnet.iconset/icon_18x18@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_19x19.png b/installers/icons/mainnet.iconset/icon_19x19.png new file mode 100644 index 0000000000..dee726ec13 Binary files /dev/null and b/installers/icons/mainnet.iconset/icon_19x19.png differ diff --git a/installers/icons/mainnet.iconset/icon_19x19@2x.png b/installers/icons/mainnet.iconset/icon_19x19@2x.png new file mode 100644 index 0000000000..741e599a3d Binary files /dev/null and b/installers/icons/mainnet.iconset/icon_19x19@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_22x22.png b/installers/icons/mainnet.iconset/icon_22x22.png new file mode 100644 index 0000000000..799f44af63 Binary files /dev/null and b/installers/icons/mainnet.iconset/icon_22x22.png differ diff --git a/installers/icons/mainnet.iconset/icon_22x22@2x.png b/installers/icons/mainnet.iconset/icon_22x22@2x.png new file mode 100644 index 0000000000..91141526e7 Binary files /dev/null and b/installers/icons/mainnet.iconset/icon_22x22@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_24x24.png b/installers/icons/mainnet.iconset/icon_24x24.png index 724b81a04f..70e119a751 100644 Binary files a/installers/icons/mainnet.iconset/icon_24x24.png and b/installers/icons/mainnet.iconset/icon_24x24.png differ diff --git a/installers/icons/mainnet.iconset/icon_24x24@2x.png b/installers/icons/mainnet.iconset/icon_24x24@2x.png index 653d3743ee..128b0fdb72 100644 Binary files a/installers/icons/mainnet.iconset/icon_24x24@2x.png and b/installers/icons/mainnet.iconset/icon_24x24@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_256x256.png b/installers/icons/mainnet.iconset/icon_256x256.png index b0da8d983b..0afefbc621 100644 Binary files a/installers/icons/mainnet.iconset/icon_256x256.png and b/installers/icons/mainnet.iconset/icon_256x256.png differ diff --git a/installers/icons/mainnet.iconset/icon_256x256@2x.png b/installers/icons/mainnet.iconset/icon_256x256@2x.png index 2233043f0c..2f56ad5e26 100644 Binary files a/installers/icons/mainnet.iconset/icon_256x256@2x.png and b/installers/icons/mainnet.iconset/icon_256x256@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_32x32.png b/installers/icons/mainnet.iconset/icon_32x32.png index 7bacd1118f..f25ae486d9 100644 Binary files a/installers/icons/mainnet.iconset/icon_32x32.png and b/installers/icons/mainnet.iconset/icon_32x32.png differ diff --git a/installers/icons/mainnet.iconset/icon_32x32@2x.png b/installers/icons/mainnet.iconset/icon_32x32@2x.png index 58e169f814..4fa7ec9961 100644 Binary files a/installers/icons/mainnet.iconset/icon_32x32@2x.png and b/installers/icons/mainnet.iconset/icon_32x32@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_40x40.png b/installers/icons/mainnet.iconset/icon_40x40.png index 58d8c76696..bd6918f079 100644 Binary files a/installers/icons/mainnet.iconset/icon_40x40.png and b/installers/icons/mainnet.iconset/icon_40x40.png differ diff --git a/installers/icons/mainnet.iconset/icon_40x40@2x.png b/installers/icons/mainnet.iconset/icon_40x40@2x.png index 6501fc4979..5246374cbf 100644 Binary files a/installers/icons/mainnet.iconset/icon_40x40@2x.png and b/installers/icons/mainnet.iconset/icon_40x40@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_48x48.png b/installers/icons/mainnet.iconset/icon_48x48.png index efcb86db38..b41e6f1041 100644 Binary files a/installers/icons/mainnet.iconset/icon_48x48.png and b/installers/icons/mainnet.iconset/icon_48x48.png differ diff --git a/installers/icons/mainnet.iconset/icon_48x48@2x.png b/installers/icons/mainnet.iconset/icon_48x48@2x.png index 75f76c761a..ddd3899251 100644 Binary files a/installers/icons/mainnet.iconset/icon_48x48@2x.png and b/installers/icons/mainnet.iconset/icon_48x48@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_512x512.png b/installers/icons/mainnet.iconset/icon_512x512.png index ea81a4ba0f..839cde5954 100644 Binary files a/installers/icons/mainnet.iconset/icon_512x512.png and b/installers/icons/mainnet.iconset/icon_512x512.png differ diff --git a/installers/icons/mainnet.iconset/icon_512x512@2x.png b/installers/icons/mainnet.iconset/icon_512x512@2x.png index d73ec7451d..68b2911ce7 100644 Binary files a/installers/icons/mainnet.iconset/icon_512x512@2x.png and b/installers/icons/mainnet.iconset/icon_512x512@2x.png differ diff --git a/installers/icons/mainnet.iconset/icon_64x64.png b/installers/icons/mainnet.iconset/icon_64x64.png index 03f986e164..6e4fd7ab47 100644 Binary files a/installers/icons/mainnet.iconset/icon_64x64.png and b/installers/icons/mainnet.iconset/icon_64x64.png differ diff --git a/installers/icons/mainnet.iconset/icon_64x64@2x.png b/installers/icons/mainnet.iconset/icon_64x64@2x.png index 29d18ed049..254ce64d59 100644 Binary files a/installers/icons/mainnet.iconset/icon_64x64@2x.png and b/installers/icons/mainnet.iconset/icon_64x64@2x.png differ diff --git a/installers/icons/mainnet/1024x1024.png b/installers/icons/mainnet/1024x1024.png index da34c5be9c..7ffbccdff5 100644 Binary files a/installers/icons/mainnet/1024x1024.png and b/installers/icons/mainnet/1024x1024.png differ diff --git a/installers/icons/mainnet/128x128.ico b/installers/icons/mainnet/128x128.ico index 32c01abc83..8b225ae1cb 100644 Binary files a/installers/icons/mainnet/128x128.ico and b/installers/icons/mainnet/128x128.ico differ diff --git a/installers/icons/mainnet/128x128.png b/installers/icons/mainnet/128x128.png index 02a88e939c..91a7863453 100644 Binary files a/installers/icons/mainnet/128x128.png and b/installers/icons/mainnet/128x128.png differ diff --git a/installers/icons/mainnet/16x16.ico b/installers/icons/mainnet/16x16.ico index 8c1bcad743..0df611f296 100644 Binary files a/installers/icons/mainnet/16x16.ico and b/installers/icons/mainnet/16x16.ico differ diff --git a/installers/icons/mainnet/16x16.png b/installers/icons/mainnet/16x16.png index cb63ce313e..ae53c1f6c4 100644 Binary files a/installers/icons/mainnet/16x16.png and b/installers/icons/mainnet/16x16.png differ diff --git a/installers/icons/mainnet/18x18.ico b/installers/icons/mainnet/18x18.ico index 43db2c26d7..ddc41e38cd 100644 Binary files a/installers/icons/mainnet/18x18.ico and b/installers/icons/mainnet/18x18.ico differ diff --git a/installers/icons/mainnet/18x18.png b/installers/icons/mainnet/18x18.png index b2149b8628..a36a25fd31 100644 Binary files a/installers/icons/mainnet/18x18.png and b/installers/icons/mainnet/18x18.png differ diff --git a/installers/icons/mainnet/19x19.ico b/installers/icons/mainnet/19x19.ico index cf0cb0021b..b2106743c4 100644 Binary files a/installers/icons/mainnet/19x19.ico and b/installers/icons/mainnet/19x19.ico differ diff --git a/installers/icons/mainnet/19x19.png b/installers/icons/mainnet/19x19.png index 18b1ab90ad..dee726ec13 100644 Binary files a/installers/icons/mainnet/19x19.png and b/installers/icons/mainnet/19x19.png differ diff --git a/installers/icons/mainnet/22x22.ico b/installers/icons/mainnet/22x22.ico index 801abe8cc5..2221dc4cfc 100644 Binary files a/installers/icons/mainnet/22x22.ico and b/installers/icons/mainnet/22x22.ico differ diff --git a/installers/icons/mainnet/22x22.png b/installers/icons/mainnet/22x22.png index 3f32ee940b..799f44af63 100644 Binary files a/installers/icons/mainnet/22x22.png and b/installers/icons/mainnet/22x22.png differ diff --git a/installers/icons/mainnet/24x24.ico b/installers/icons/mainnet/24x24.ico index 3a6ce682dd..15dac5d179 100644 Binary files a/installers/icons/mainnet/24x24.ico and b/installers/icons/mainnet/24x24.ico differ diff --git a/installers/icons/mainnet/24x24.png b/installers/icons/mainnet/24x24.png index d2fe86fee1..70e119a751 100644 Binary files a/installers/icons/mainnet/24x24.png and b/installers/icons/mainnet/24x24.png differ diff --git a/installers/icons/mainnet/256x256.ico b/installers/icons/mainnet/256x256.ico index 673c0faa4a..1d37456a14 100644 Binary files a/installers/icons/mainnet/256x256.ico and b/installers/icons/mainnet/256x256.ico differ diff --git a/installers/icons/mainnet/256x256.png b/installers/icons/mainnet/256x256.png index 5024773884..0afefbc621 100644 Binary files a/installers/icons/mainnet/256x256.png and b/installers/icons/mainnet/256x256.png differ diff --git a/installers/icons/mainnet/32x32.ico b/installers/icons/mainnet/32x32.ico index 768c9fef4f..2720222288 100644 Binary files a/installers/icons/mainnet/32x32.ico and b/installers/icons/mainnet/32x32.ico differ diff --git a/installers/icons/mainnet/32x32.png b/installers/icons/mainnet/32x32.png index 29a1e0e479..f25ae486d9 100644 Binary files a/installers/icons/mainnet/32x32.png and b/installers/icons/mainnet/32x32.png differ diff --git a/installers/icons/mainnet/40x40.ico b/installers/icons/mainnet/40x40.ico index 168e5be403..93d3a49125 100644 Binary files a/installers/icons/mainnet/40x40.ico and b/installers/icons/mainnet/40x40.ico differ diff --git a/installers/icons/mainnet/40x40.png b/installers/icons/mainnet/40x40.png index 58d8c76696..bd6918f079 100644 Binary files a/installers/icons/mainnet/40x40.png and b/installers/icons/mainnet/40x40.png differ diff --git a/installers/icons/mainnet/48x48.ico b/installers/icons/mainnet/48x48.ico index 9e6c7ba993..b0d246700e 100644 Binary files a/installers/icons/mainnet/48x48.ico and b/installers/icons/mainnet/48x48.ico differ diff --git a/installers/icons/mainnet/48x48.png b/installers/icons/mainnet/48x48.png index efcb86db38..b41e6f1041 100644 Binary files a/installers/icons/mainnet/48x48.png and b/installers/icons/mainnet/48x48.png differ diff --git a/installers/icons/mainnet/512x512.png b/installers/icons/mainnet/512x512.png index 89f84b8024..839cde5954 100644 Binary files a/installers/icons/mainnet/512x512.png and b/installers/icons/mainnet/512x512.png differ diff --git a/installers/icons/mainnet/64x64.ico b/installers/icons/mainnet/64x64.ico index 752e6611f4..1ff6082884 100644 Binary files a/installers/icons/mainnet/64x64.ico and b/installers/icons/mainnet/64x64.ico differ diff --git a/installers/icons/mainnet/64x64.png b/installers/icons/mainnet/64x64.png index 03f986e164..6e4fd7ab47 100644 Binary files a/installers/icons/mainnet/64x64.png and b/installers/icons/mainnet/64x64.png differ diff --git a/installers/icons/mainnet/mainnet.ico b/installers/icons/mainnet/mainnet.ico index c0b9c3fa8e..68ca6d09ff 100644 Binary files a/installers/icons/mainnet/mainnet.ico and b/installers/icons/mainnet/mainnet.ico differ diff --git a/installers/icons/mainnet_flight.iconset/icon_1024x1024.png b/installers/icons/mainnet_flight.iconset/icon_1024x1024.png new file mode 100644 index 0000000000..ab5c0bf952 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_1024x1024.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_1024x1024@2x.png b/installers/icons/mainnet_flight.iconset/icon_1024x1024@2x.png new file mode 100644 index 0000000000..c08107c4e6 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_1024x1024@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_128x128.png b/installers/icons/mainnet_flight.iconset/icon_128x128.png new file mode 100644 index 0000000000..89fe7ac13e Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_128x128.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_128x128@2x.png b/installers/icons/mainnet_flight.iconset/icon_128x128@2x.png new file mode 100644 index 0000000000..00ded39f6a Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_128x128@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_16x16.png b/installers/icons/mainnet_flight.iconset/icon_16x16.png new file mode 100644 index 0000000000..ed4c9c2512 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_16x16.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_16x16@2x.png b/installers/icons/mainnet_flight.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000..34ea6dec34 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_16x16@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_18x18.png b/installers/icons/mainnet_flight.iconset/icon_18x18.png new file mode 100644 index 0000000000..87306019ac Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_18x18.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_18x18@2x.png b/installers/icons/mainnet_flight.iconset/icon_18x18@2x.png new file mode 100644 index 0000000000..c8534e57ce Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_18x18@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_19x19.png b/installers/icons/mainnet_flight.iconset/icon_19x19.png new file mode 100644 index 0000000000..2596cf59f0 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_19x19.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_19x19@2x.png b/installers/icons/mainnet_flight.iconset/icon_19x19@2x.png new file mode 100644 index 0000000000..cad230f3b6 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_19x19@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_22x22.png b/installers/icons/mainnet_flight.iconset/icon_22x22.png new file mode 100644 index 0000000000..13232da6c6 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_22x22.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_22x22@2x.png b/installers/icons/mainnet_flight.iconset/icon_22x22@2x.png new file mode 100644 index 0000000000..bb0ad519f1 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_22x22@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_24x24.png b/installers/icons/mainnet_flight.iconset/icon_24x24.png new file mode 100644 index 0000000000..af00c8e307 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_24x24.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_24x24@2x.png b/installers/icons/mainnet_flight.iconset/icon_24x24@2x.png new file mode 100644 index 0000000000..49dfd9cc20 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_24x24@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_256x256.png b/installers/icons/mainnet_flight.iconset/icon_256x256.png new file mode 100644 index 0000000000..4e7683c626 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_256x256.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_256x256@2x.png b/installers/icons/mainnet_flight.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000..b1f723f1ed Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_256x256@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_32x32.png b/installers/icons/mainnet_flight.iconset/icon_32x32.png new file mode 100644 index 0000000000..06bd4d944f Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_32x32.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_32x32@2x.png b/installers/icons/mainnet_flight.iconset/icon_32x32@2x.png new file mode 100644 index 0000000000..ff1eb1400e Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_32x32@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_40x40.png b/installers/icons/mainnet_flight.iconset/icon_40x40.png new file mode 100644 index 0000000000..17d71ad601 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_40x40.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_40x40@2x.png b/installers/icons/mainnet_flight.iconset/icon_40x40@2x.png new file mode 100644 index 0000000000..1fc3d0bc54 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_40x40@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_48x48.png b/installers/icons/mainnet_flight.iconset/icon_48x48.png new file mode 100644 index 0000000000..45317ddf82 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_48x48.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_48x48@2x.png b/installers/icons/mainnet_flight.iconset/icon_48x48@2x.png new file mode 100644 index 0000000000..1039f57238 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_48x48@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_512x512.png b/installers/icons/mainnet_flight.iconset/icon_512x512.png new file mode 100644 index 0000000000..e2786f4e68 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_512x512.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_512x512@2x.png b/installers/icons/mainnet_flight.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000..57e44e0bde Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_512x512@2x.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_64x64.png b/installers/icons/mainnet_flight.iconset/icon_64x64.png new file mode 100644 index 0000000000..20c70c1785 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_64x64.png differ diff --git a/installers/icons/mainnet_flight.iconset/icon_64x64@2x.png b/installers/icons/mainnet_flight.iconset/icon_64x64@2x.png new file mode 100644 index 0000000000..cce5e7c203 Binary files /dev/null and b/installers/icons/mainnet_flight.iconset/icon_64x64@2x.png differ diff --git a/installers/icons/mainnet_flight/1024x1024.png b/installers/icons/mainnet_flight/1024x1024.png new file mode 100644 index 0000000000..ab5c0bf952 Binary files /dev/null and b/installers/icons/mainnet_flight/1024x1024.png differ diff --git a/installers/icons/mainnet_flight/128x128.ico b/installers/icons/mainnet_flight/128x128.ico new file mode 100644 index 0000000000..89310db8f3 Binary files /dev/null and b/installers/icons/mainnet_flight/128x128.ico differ diff --git a/installers/icons/mainnet_flight/128x128.png b/installers/icons/mainnet_flight/128x128.png new file mode 100644 index 0000000000..89fe7ac13e Binary files /dev/null and b/installers/icons/mainnet_flight/128x128.png differ diff --git a/installers/icons/mainnet_flight/16x16.ico b/installers/icons/mainnet_flight/16x16.ico new file mode 100644 index 0000000000..96f64d1361 Binary files /dev/null and b/installers/icons/mainnet_flight/16x16.ico differ diff --git a/installers/icons/mainnet_flight/16x16.png b/installers/icons/mainnet_flight/16x16.png new file mode 100644 index 0000000000..ed4c9c2512 Binary files /dev/null and b/installers/icons/mainnet_flight/16x16.png differ diff --git a/installers/icons/mainnet_flight/18x18.ico b/installers/icons/mainnet_flight/18x18.ico new file mode 100644 index 0000000000..4aa4575cc3 Binary files /dev/null and b/installers/icons/mainnet_flight/18x18.ico differ diff --git a/installers/icons/mainnet_flight/18x18.png b/installers/icons/mainnet_flight/18x18.png new file mode 100644 index 0000000000..87306019ac Binary files /dev/null and b/installers/icons/mainnet_flight/18x18.png differ diff --git a/installers/icons/mainnet_flight/19x19.ico b/installers/icons/mainnet_flight/19x19.ico new file mode 100644 index 0000000000..9a4c08fb20 Binary files /dev/null and b/installers/icons/mainnet_flight/19x19.ico differ diff --git a/installers/icons/mainnet_flight/19x19.png b/installers/icons/mainnet_flight/19x19.png new file mode 100644 index 0000000000..2596cf59f0 Binary files /dev/null and b/installers/icons/mainnet_flight/19x19.png differ diff --git a/installers/icons/mainnet_flight/22x22.ico b/installers/icons/mainnet_flight/22x22.ico new file mode 100644 index 0000000000..e86ac3dc28 Binary files /dev/null and b/installers/icons/mainnet_flight/22x22.ico differ diff --git a/installers/icons/mainnet_flight/22x22.png b/installers/icons/mainnet_flight/22x22.png new file mode 100644 index 0000000000..13232da6c6 Binary files /dev/null and b/installers/icons/mainnet_flight/22x22.png differ diff --git a/installers/icons/mainnet_flight/24x24.ico b/installers/icons/mainnet_flight/24x24.ico new file mode 100644 index 0000000000..8991674cd0 Binary files /dev/null and b/installers/icons/mainnet_flight/24x24.ico differ diff --git a/installers/icons/mainnet_flight/24x24.png b/installers/icons/mainnet_flight/24x24.png new file mode 100644 index 0000000000..af00c8e307 Binary files /dev/null and b/installers/icons/mainnet_flight/24x24.png differ diff --git a/installers/icons/mainnet_flight/256x256.ico b/installers/icons/mainnet_flight/256x256.ico new file mode 100644 index 0000000000..7ab22bf5ce Binary files /dev/null and b/installers/icons/mainnet_flight/256x256.ico differ diff --git a/installers/icons/mainnet_flight/256x256.png b/installers/icons/mainnet_flight/256x256.png new file mode 100644 index 0000000000..4e7683c626 Binary files /dev/null and b/installers/icons/mainnet_flight/256x256.png differ diff --git a/installers/icons/mainnet_flight/32x32.ico b/installers/icons/mainnet_flight/32x32.ico new file mode 100644 index 0000000000..c44e0bc650 Binary files /dev/null and b/installers/icons/mainnet_flight/32x32.ico differ diff --git a/installers/icons/mainnet_flight/32x32.png b/installers/icons/mainnet_flight/32x32.png new file mode 100644 index 0000000000..06bd4d944f Binary files /dev/null and b/installers/icons/mainnet_flight/32x32.png differ diff --git a/installers/icons/mainnet_flight/40x40.ico b/installers/icons/mainnet_flight/40x40.ico new file mode 100644 index 0000000000..fd100b804f Binary files /dev/null and b/installers/icons/mainnet_flight/40x40.ico differ diff --git a/installers/icons/mainnet_flight/40x40.png b/installers/icons/mainnet_flight/40x40.png new file mode 100644 index 0000000000..17d71ad601 Binary files /dev/null and b/installers/icons/mainnet_flight/40x40.png differ diff --git a/installers/icons/mainnet_flight/48x48.ico b/installers/icons/mainnet_flight/48x48.ico new file mode 100644 index 0000000000..31be88d116 Binary files /dev/null and b/installers/icons/mainnet_flight/48x48.ico differ diff --git a/installers/icons/mainnet_flight/48x48.png b/installers/icons/mainnet_flight/48x48.png new file mode 100644 index 0000000000..45317ddf82 Binary files /dev/null and b/installers/icons/mainnet_flight/48x48.png differ diff --git a/installers/icons/mainnet_flight/512x512.png b/installers/icons/mainnet_flight/512x512.png new file mode 100644 index 0000000000..e2786f4e68 Binary files /dev/null and b/installers/icons/mainnet_flight/512x512.png differ diff --git a/installers/icons/mainnet_flight/64x64.ico b/installers/icons/mainnet_flight/64x64.ico new file mode 100644 index 0000000000..699d6cb212 Binary files /dev/null and b/installers/icons/mainnet_flight/64x64.ico differ diff --git a/installers/icons/mainnet_flight/64x64.png b/installers/icons/mainnet_flight/64x64.png new file mode 100644 index 0000000000..20c70c1785 Binary files /dev/null and b/installers/icons/mainnet_flight/64x64.png differ diff --git a/installers/icons/mainnet_flight/mainnet_flight.ico b/installers/icons/mainnet_flight/mainnet_flight.ico new file mode 100644 index 0000000000..1add653ada Binary files /dev/null and b/installers/icons/mainnet_flight/mainnet_flight.ico differ diff --git a/installers/icons/nightly.iconset/icon_1024x1024.png b/installers/icons/nightly.iconset/icon_1024x1024.png new file mode 100644 index 0000000000..c73013521e Binary files /dev/null and b/installers/icons/nightly.iconset/icon_1024x1024.png differ diff --git a/installers/icons/nightly.iconset/icon_1024x1024@2x.png b/installers/icons/nightly.iconset/icon_1024x1024@2x.png new file mode 100644 index 0000000000..c4c3cf2165 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_1024x1024@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_128x128.png b/installers/icons/nightly.iconset/icon_128x128.png new file mode 100644 index 0000000000..b3c4ee64ec Binary files /dev/null and b/installers/icons/nightly.iconset/icon_128x128.png differ diff --git a/installers/icons/nightly.iconset/icon_128x128@2x.png b/installers/icons/nightly.iconset/icon_128x128@2x.png new file mode 100644 index 0000000000..31fd5f1391 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_128x128@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_16x16.png b/installers/icons/nightly.iconset/icon_16x16.png new file mode 100644 index 0000000000..ab2bbad5b8 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_16x16.png differ diff --git a/installers/icons/nightly.iconset/icon_16x16@2x.png b/installers/icons/nightly.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000..0c37aba948 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_16x16@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_18x18.png b/installers/icons/nightly.iconset/icon_18x18.png new file mode 100644 index 0000000000..3b40a9bc8d Binary files /dev/null and b/installers/icons/nightly.iconset/icon_18x18.png differ diff --git a/installers/icons/nightly.iconset/icon_18x18@2x.png b/installers/icons/nightly.iconset/icon_18x18@2x.png new file mode 100644 index 0000000000..27817a5625 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_18x18@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_19x19.png b/installers/icons/nightly.iconset/icon_19x19.png new file mode 100644 index 0000000000..9df12b672f Binary files /dev/null and b/installers/icons/nightly.iconset/icon_19x19.png differ diff --git a/installers/icons/nightly.iconset/icon_19x19@2x.png b/installers/icons/nightly.iconset/icon_19x19@2x.png new file mode 100644 index 0000000000..af23901ac8 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_19x19@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_22x22.png b/installers/icons/nightly.iconset/icon_22x22.png new file mode 100644 index 0000000000..1612f54431 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_22x22.png differ diff --git a/installers/icons/nightly.iconset/icon_22x22@2x.png b/installers/icons/nightly.iconset/icon_22x22@2x.png new file mode 100644 index 0000000000..613b628db1 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_22x22@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_24x24.png b/installers/icons/nightly.iconset/icon_24x24.png new file mode 100644 index 0000000000..936e1cfb3d Binary files /dev/null and b/installers/icons/nightly.iconset/icon_24x24.png differ diff --git a/installers/icons/nightly.iconset/icon_24x24@2x.png b/installers/icons/nightly.iconset/icon_24x24@2x.png new file mode 100644 index 0000000000..3b888304c7 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_24x24@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_256x256.png b/installers/icons/nightly.iconset/icon_256x256.png new file mode 100644 index 0000000000..9cec95cd03 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_256x256.png differ diff --git a/installers/icons/nightly.iconset/icon_256x256@2x.png b/installers/icons/nightly.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000..bd2f85af24 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_256x256@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_32x32.png b/installers/icons/nightly.iconset/icon_32x32.png new file mode 100644 index 0000000000..eca87127ff Binary files /dev/null and b/installers/icons/nightly.iconset/icon_32x32.png differ diff --git a/installers/icons/nightly.iconset/icon_32x32@2x.png b/installers/icons/nightly.iconset/icon_32x32@2x.png new file mode 100644 index 0000000000..02747700f0 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_32x32@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_40x40.png b/installers/icons/nightly.iconset/icon_40x40.png new file mode 100644 index 0000000000..1b0f8949e2 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_40x40.png differ diff --git a/installers/icons/nightly.iconset/icon_40x40@2x.png b/installers/icons/nightly.iconset/icon_40x40@2x.png new file mode 100644 index 0000000000..250376d6b5 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_40x40@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_48x48.png b/installers/icons/nightly.iconset/icon_48x48.png new file mode 100644 index 0000000000..18bd4584d7 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_48x48.png differ diff --git a/installers/icons/nightly.iconset/icon_48x48@2x.png b/installers/icons/nightly.iconset/icon_48x48@2x.png new file mode 100644 index 0000000000..d15255c1d0 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_48x48@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_512x512.png b/installers/icons/nightly.iconset/icon_512x512.png new file mode 100644 index 0000000000..f7cbba2255 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_512x512.png differ diff --git a/installers/icons/nightly.iconset/icon_512x512@2x.png b/installers/icons/nightly.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000..a1576d5d21 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_512x512@2x.png differ diff --git a/installers/icons/nightly.iconset/icon_64x64.png b/installers/icons/nightly.iconset/icon_64x64.png new file mode 100644 index 0000000000..cf3e79947b Binary files /dev/null and b/installers/icons/nightly.iconset/icon_64x64.png differ diff --git a/installers/icons/nightly.iconset/icon_64x64@2x.png b/installers/icons/nightly.iconset/icon_64x64@2x.png new file mode 100644 index 0000000000..c694c33fb6 Binary files /dev/null and b/installers/icons/nightly.iconset/icon_64x64@2x.png differ diff --git a/installers/icons/nightly/1024x1024.png b/installers/icons/nightly/1024x1024.png new file mode 100644 index 0000000000..c73013521e Binary files /dev/null and b/installers/icons/nightly/1024x1024.png differ diff --git a/installers/icons/nightly/128x128.ico b/installers/icons/nightly/128x128.ico new file mode 100644 index 0000000000..0450781c06 Binary files /dev/null and b/installers/icons/nightly/128x128.ico differ diff --git a/installers/icons/nightly/128x128.png b/installers/icons/nightly/128x128.png new file mode 100644 index 0000000000..b3c4ee64ec Binary files /dev/null and b/installers/icons/nightly/128x128.png differ diff --git a/installers/icons/nightly/16x16.ico b/installers/icons/nightly/16x16.ico new file mode 100644 index 0000000000..10bc3c93a6 Binary files /dev/null and b/installers/icons/nightly/16x16.ico differ diff --git a/installers/icons/nightly/16x16.png b/installers/icons/nightly/16x16.png new file mode 100644 index 0000000000..ab2bbad5b8 Binary files /dev/null and b/installers/icons/nightly/16x16.png differ diff --git a/installers/icons/nightly/18x18.ico b/installers/icons/nightly/18x18.ico new file mode 100644 index 0000000000..b747d287cd Binary files /dev/null and b/installers/icons/nightly/18x18.ico differ diff --git a/installers/icons/nightly/18x18.png b/installers/icons/nightly/18x18.png new file mode 100644 index 0000000000..3b40a9bc8d Binary files /dev/null and b/installers/icons/nightly/18x18.png differ diff --git a/installers/icons/nightly/19x19.ico b/installers/icons/nightly/19x19.ico new file mode 100644 index 0000000000..aab78223fa Binary files /dev/null and b/installers/icons/nightly/19x19.ico differ diff --git a/installers/icons/nightly/19x19.png b/installers/icons/nightly/19x19.png new file mode 100644 index 0000000000..9df12b672f Binary files /dev/null and b/installers/icons/nightly/19x19.png differ diff --git a/installers/icons/nightly/22x22.ico b/installers/icons/nightly/22x22.ico new file mode 100644 index 0000000000..fc76a53eef Binary files /dev/null and b/installers/icons/nightly/22x22.ico differ diff --git a/installers/icons/nightly/22x22.png b/installers/icons/nightly/22x22.png new file mode 100644 index 0000000000..1612f54431 Binary files /dev/null and b/installers/icons/nightly/22x22.png differ diff --git a/installers/icons/nightly/24x24.ico b/installers/icons/nightly/24x24.ico new file mode 100644 index 0000000000..e048aec592 Binary files /dev/null and b/installers/icons/nightly/24x24.ico differ diff --git a/installers/icons/nightly/24x24.png b/installers/icons/nightly/24x24.png new file mode 100644 index 0000000000..936e1cfb3d Binary files /dev/null and b/installers/icons/nightly/24x24.png differ diff --git a/installers/icons/nightly/256x256.ico b/installers/icons/nightly/256x256.ico new file mode 100644 index 0000000000..6088ac2b2f Binary files /dev/null and b/installers/icons/nightly/256x256.ico differ diff --git a/installers/icons/nightly/256x256.png b/installers/icons/nightly/256x256.png new file mode 100644 index 0000000000..9cec95cd03 Binary files /dev/null and b/installers/icons/nightly/256x256.png differ diff --git a/installers/icons/nightly/32x32.ico b/installers/icons/nightly/32x32.ico new file mode 100644 index 0000000000..8cb6dba844 Binary files /dev/null and b/installers/icons/nightly/32x32.ico differ diff --git a/installers/icons/nightly/32x32.png b/installers/icons/nightly/32x32.png new file mode 100644 index 0000000000..eca87127ff Binary files /dev/null and b/installers/icons/nightly/32x32.png differ diff --git a/installers/icons/nightly/40x40.ico b/installers/icons/nightly/40x40.ico new file mode 100644 index 0000000000..f819ea0378 Binary files /dev/null and b/installers/icons/nightly/40x40.ico differ diff --git a/installers/icons/nightly/40x40.png b/installers/icons/nightly/40x40.png new file mode 100644 index 0000000000..1b0f8949e2 Binary files /dev/null and b/installers/icons/nightly/40x40.png differ diff --git a/installers/icons/nightly/48x48.ico b/installers/icons/nightly/48x48.ico new file mode 100644 index 0000000000..7f2cbd87dd Binary files /dev/null and b/installers/icons/nightly/48x48.ico differ diff --git a/installers/icons/nightly/48x48.png b/installers/icons/nightly/48x48.png new file mode 100644 index 0000000000..18bd4584d7 Binary files /dev/null and b/installers/icons/nightly/48x48.png differ diff --git a/installers/icons/nightly/512x512.png b/installers/icons/nightly/512x512.png new file mode 100644 index 0000000000..f7cbba2255 Binary files /dev/null and b/installers/icons/nightly/512x512.png differ diff --git a/installers/icons/nightly/64x64.ico b/installers/icons/nightly/64x64.ico new file mode 100644 index 0000000000..1a52bce60d Binary files /dev/null and b/installers/icons/nightly/64x64.ico differ diff --git a/installers/icons/nightly/64x64.png b/installers/icons/nightly/64x64.png new file mode 100644 index 0000000000..cf3e79947b Binary files /dev/null and b/installers/icons/nightly/64x64.png differ diff --git a/installers/icons/nightly/nightly.ico b/installers/icons/nightly/nightly.ico new file mode 100644 index 0000000000..dc0563936a Binary files /dev/null and b/installers/icons/nightly/nightly.ico differ diff --git a/installers/icons/qa.iconset/icon_1024x1024.png b/installers/icons/qa.iconset/icon_1024x1024.png new file mode 100644 index 0000000000..43821e4763 Binary files /dev/null and b/installers/icons/qa.iconset/icon_1024x1024.png differ diff --git a/installers/icons/qa.iconset/icon_1024x1024@2x.png b/installers/icons/qa.iconset/icon_1024x1024@2x.png new file mode 100644 index 0000000000..40918ada20 Binary files /dev/null and b/installers/icons/qa.iconset/icon_1024x1024@2x.png differ diff --git a/installers/icons/qa.iconset/icon_128x128.png b/installers/icons/qa.iconset/icon_128x128.png new file mode 100644 index 0000000000..9ddfe066cd Binary files /dev/null and b/installers/icons/qa.iconset/icon_128x128.png differ diff --git a/installers/icons/qa.iconset/icon_128x128@2x.png b/installers/icons/qa.iconset/icon_128x128@2x.png new file mode 100644 index 0000000000..710eba97c4 Binary files /dev/null and b/installers/icons/qa.iconset/icon_128x128@2x.png differ diff --git a/installers/icons/qa.iconset/icon_16x16.png b/installers/icons/qa.iconset/icon_16x16.png new file mode 100644 index 0000000000..ab2bbad5b8 Binary files /dev/null and b/installers/icons/qa.iconset/icon_16x16.png differ diff --git a/installers/icons/qa.iconset/icon_16x16@2x.png b/installers/icons/qa.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000..0c37aba948 Binary files /dev/null and b/installers/icons/qa.iconset/icon_16x16@2x.png differ diff --git a/installers/icons/qa.iconset/icon_18x18.png b/installers/icons/qa.iconset/icon_18x18.png new file mode 100644 index 0000000000..3b40a9bc8d Binary files /dev/null and b/installers/icons/qa.iconset/icon_18x18.png differ diff --git a/installers/icons/qa.iconset/icon_18x18@2x.png b/installers/icons/qa.iconset/icon_18x18@2x.png new file mode 100644 index 0000000000..27817a5625 Binary files /dev/null and b/installers/icons/qa.iconset/icon_18x18@2x.png differ diff --git a/installers/icons/qa.iconset/icon_19x19.png b/installers/icons/qa.iconset/icon_19x19.png new file mode 100644 index 0000000000..9df12b672f Binary files /dev/null and b/installers/icons/qa.iconset/icon_19x19.png differ diff --git a/installers/icons/qa.iconset/icon_19x19@2x.png b/installers/icons/qa.iconset/icon_19x19@2x.png new file mode 100644 index 0000000000..af23901ac8 Binary files /dev/null and b/installers/icons/qa.iconset/icon_19x19@2x.png differ diff --git a/installers/icons/qa.iconset/icon_22x22.png b/installers/icons/qa.iconset/icon_22x22.png new file mode 100644 index 0000000000..1612f54431 Binary files /dev/null and b/installers/icons/qa.iconset/icon_22x22.png differ diff --git a/installers/icons/qa.iconset/icon_22x22@2x.png b/installers/icons/qa.iconset/icon_22x22@2x.png new file mode 100644 index 0000000000..613b628db1 Binary files /dev/null and b/installers/icons/qa.iconset/icon_22x22@2x.png differ diff --git a/installers/icons/qa.iconset/icon_24x24.png b/installers/icons/qa.iconset/icon_24x24.png new file mode 100644 index 0000000000..936e1cfb3d Binary files /dev/null and b/installers/icons/qa.iconset/icon_24x24.png differ diff --git a/installers/icons/qa.iconset/icon_24x24@2x.png b/installers/icons/qa.iconset/icon_24x24@2x.png new file mode 100644 index 0000000000..3b888304c7 Binary files /dev/null and b/installers/icons/qa.iconset/icon_24x24@2x.png differ diff --git a/installers/icons/qa.iconset/icon_256x256.png b/installers/icons/qa.iconset/icon_256x256.png new file mode 100644 index 0000000000..c58589092b Binary files /dev/null and b/installers/icons/qa.iconset/icon_256x256.png differ diff --git a/installers/icons/qa.iconset/icon_256x256@2x.png b/installers/icons/qa.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000..2dfcf19c3a Binary files /dev/null and b/installers/icons/qa.iconset/icon_256x256@2x.png differ diff --git a/installers/icons/qa.iconset/icon_32x32.png b/installers/icons/qa.iconset/icon_32x32.png new file mode 100644 index 0000000000..eca87127ff Binary files /dev/null and b/installers/icons/qa.iconset/icon_32x32.png differ diff --git a/installers/icons/qa.iconset/icon_32x32@2x.png b/installers/icons/qa.iconset/icon_32x32@2x.png new file mode 100644 index 0000000000..02747700f0 Binary files /dev/null and b/installers/icons/qa.iconset/icon_32x32@2x.png differ diff --git a/installers/icons/qa.iconset/icon_40x40.png b/installers/icons/qa.iconset/icon_40x40.png new file mode 100644 index 0000000000..832a918ed5 Binary files /dev/null and b/installers/icons/qa.iconset/icon_40x40.png differ diff --git a/installers/icons/qa.iconset/icon_40x40@2x.png b/installers/icons/qa.iconset/icon_40x40@2x.png new file mode 100644 index 0000000000..4020f7a85f Binary files /dev/null and b/installers/icons/qa.iconset/icon_40x40@2x.png differ diff --git a/installers/icons/qa.iconset/icon_48x48.png b/installers/icons/qa.iconset/icon_48x48.png new file mode 100644 index 0000000000..59d7309e6d Binary files /dev/null and b/installers/icons/qa.iconset/icon_48x48.png differ diff --git a/installers/icons/qa.iconset/icon_48x48@2x.png b/installers/icons/qa.iconset/icon_48x48@2x.png new file mode 100644 index 0000000000..2ecd9c9e9c Binary files /dev/null and b/installers/icons/qa.iconset/icon_48x48@2x.png differ diff --git a/installers/icons/qa.iconset/icon_512x512.png b/installers/icons/qa.iconset/icon_512x512.png new file mode 100644 index 0000000000..5774c6bb5d Binary files /dev/null and b/installers/icons/qa.iconset/icon_512x512.png differ diff --git a/installers/icons/qa.iconset/icon_512x512@2x.png b/installers/icons/qa.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000..723c761b94 Binary files /dev/null and b/installers/icons/qa.iconset/icon_512x512@2x.png differ diff --git a/installers/icons/qa.iconset/icon_64x64.png b/installers/icons/qa.iconset/icon_64x64.png new file mode 100644 index 0000000000..20d7a1b10c Binary files /dev/null and b/installers/icons/qa.iconset/icon_64x64.png differ diff --git a/installers/icons/qa.iconset/icon_64x64@2x.png b/installers/icons/qa.iconset/icon_64x64@2x.png new file mode 100644 index 0000000000..aa49ecb0cd Binary files /dev/null and b/installers/icons/qa.iconset/icon_64x64@2x.png differ diff --git a/installers/icons/qa/1024x1024.png b/installers/icons/qa/1024x1024.png new file mode 100644 index 0000000000..43821e4763 Binary files /dev/null and b/installers/icons/qa/1024x1024.png differ diff --git a/installers/icons/qa/128x128.ico b/installers/icons/qa/128x128.ico new file mode 100644 index 0000000000..20d2e45dc6 Binary files /dev/null and b/installers/icons/qa/128x128.ico differ diff --git a/installers/icons/qa/128x128.png b/installers/icons/qa/128x128.png new file mode 100644 index 0000000000..9ddfe066cd Binary files /dev/null and b/installers/icons/qa/128x128.png differ diff --git a/installers/icons/qa/16x16.ico b/installers/icons/qa/16x16.ico new file mode 100644 index 0000000000..10bc3c93a6 Binary files /dev/null and b/installers/icons/qa/16x16.ico differ diff --git a/installers/icons/qa/16x16.png b/installers/icons/qa/16x16.png new file mode 100644 index 0000000000..ab2bbad5b8 Binary files /dev/null and b/installers/icons/qa/16x16.png differ diff --git a/installers/icons/qa/18x18.ico b/installers/icons/qa/18x18.ico new file mode 100644 index 0000000000..b747d287cd Binary files /dev/null and b/installers/icons/qa/18x18.ico differ diff --git a/installers/icons/qa/18x18.png b/installers/icons/qa/18x18.png new file mode 100644 index 0000000000..3b40a9bc8d Binary files /dev/null and b/installers/icons/qa/18x18.png differ diff --git a/installers/icons/qa/19x19.ico b/installers/icons/qa/19x19.ico new file mode 100644 index 0000000000..aab78223fa Binary files /dev/null and b/installers/icons/qa/19x19.ico differ diff --git a/installers/icons/qa/19x19.png b/installers/icons/qa/19x19.png new file mode 100644 index 0000000000..9df12b672f Binary files /dev/null and b/installers/icons/qa/19x19.png differ diff --git a/installers/icons/qa/22x22.ico b/installers/icons/qa/22x22.ico new file mode 100644 index 0000000000..fc76a53eef Binary files /dev/null and b/installers/icons/qa/22x22.ico differ diff --git a/installers/icons/qa/22x22.png b/installers/icons/qa/22x22.png new file mode 100644 index 0000000000..1612f54431 Binary files /dev/null and b/installers/icons/qa/22x22.png differ diff --git a/installers/icons/qa/24x24.ico b/installers/icons/qa/24x24.ico new file mode 100644 index 0000000000..e048aec592 Binary files /dev/null and b/installers/icons/qa/24x24.ico differ diff --git a/installers/icons/qa/24x24.png b/installers/icons/qa/24x24.png new file mode 100644 index 0000000000..936e1cfb3d Binary files /dev/null and b/installers/icons/qa/24x24.png differ diff --git a/installers/icons/qa/256x256.ico b/installers/icons/qa/256x256.ico new file mode 100644 index 0000000000..62d51343a6 Binary files /dev/null and b/installers/icons/qa/256x256.ico differ diff --git a/installers/icons/qa/256x256.png b/installers/icons/qa/256x256.png new file mode 100644 index 0000000000..c58589092b Binary files /dev/null and b/installers/icons/qa/256x256.png differ diff --git a/installers/icons/qa/32x32.ico b/installers/icons/qa/32x32.ico new file mode 100644 index 0000000000..8cb6dba844 Binary files /dev/null and b/installers/icons/qa/32x32.ico differ diff --git a/installers/icons/qa/32x32.png b/installers/icons/qa/32x32.png new file mode 100644 index 0000000000..eca87127ff Binary files /dev/null and b/installers/icons/qa/32x32.png differ diff --git a/installers/icons/qa/40x40.ico b/installers/icons/qa/40x40.ico new file mode 100644 index 0000000000..afafdf400d Binary files /dev/null and b/installers/icons/qa/40x40.ico differ diff --git a/installers/icons/qa/40x40.png b/installers/icons/qa/40x40.png new file mode 100644 index 0000000000..832a918ed5 Binary files /dev/null and b/installers/icons/qa/40x40.png differ diff --git a/installers/icons/qa/48x48.ico b/installers/icons/qa/48x48.ico new file mode 100644 index 0000000000..d90d6fb134 Binary files /dev/null and b/installers/icons/qa/48x48.ico differ diff --git a/installers/icons/qa/48x48.png b/installers/icons/qa/48x48.png new file mode 100644 index 0000000000..59d7309e6d Binary files /dev/null and b/installers/icons/qa/48x48.png differ diff --git a/installers/icons/qa/512x512.png b/installers/icons/qa/512x512.png new file mode 100644 index 0000000000..5774c6bb5d Binary files /dev/null and b/installers/icons/qa/512x512.png differ diff --git a/installers/icons/qa/64x64.ico b/installers/icons/qa/64x64.ico new file mode 100644 index 0000000000..6a89b0f70b Binary files /dev/null and b/installers/icons/qa/64x64.ico differ diff --git a/installers/icons/qa/64x64.png b/installers/icons/qa/64x64.png new file mode 100644 index 0000000000..20d7a1b10c Binary files /dev/null and b/installers/icons/qa/64x64.png differ diff --git a/installers/icons/qa/qa.ico b/installers/icons/qa/qa.ico new file mode 100644 index 0000000000..c30290dc84 Binary files /dev/null and b/installers/icons/qa/qa.ico differ diff --git a/installers/icons/selfnode.iconset/icon_1024x1024.png b/installers/icons/selfnode.iconset/icon_1024x1024.png new file mode 100644 index 0000000000..515ce101dd Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_1024x1024.png differ diff --git a/installers/icons/selfnode.iconset/icon_1024x1024@2x.png b/installers/icons/selfnode.iconset/icon_1024x1024@2x.png new file mode 100644 index 0000000000..de2b1940bc Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_1024x1024@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_128x128.png b/installers/icons/selfnode.iconset/icon_128x128.png new file mode 100644 index 0000000000..60d22b0502 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_128x128.png differ diff --git a/installers/icons/selfnode.iconset/icon_128x128@2x.png b/installers/icons/selfnode.iconset/icon_128x128@2x.png new file mode 100644 index 0000000000..2d3386b124 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_128x128@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_16x16.png b/installers/icons/selfnode.iconset/icon_16x16.png new file mode 100644 index 0000000000..591b5644d4 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_16x16.png differ diff --git a/installers/icons/selfnode.iconset/icon_16x16@2x.png b/installers/icons/selfnode.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000..748b7801c4 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_16x16@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_18x18.png b/installers/icons/selfnode.iconset/icon_18x18.png new file mode 100644 index 0000000000..94de052734 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_18x18.png differ diff --git a/installers/icons/selfnode.iconset/icon_18x18@2x.png b/installers/icons/selfnode.iconset/icon_18x18@2x.png new file mode 100644 index 0000000000..096ac24506 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_18x18@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_19x19.png b/installers/icons/selfnode.iconset/icon_19x19.png new file mode 100644 index 0000000000..91593441e5 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_19x19.png differ diff --git a/installers/icons/selfnode.iconset/icon_19x19@2x.png b/installers/icons/selfnode.iconset/icon_19x19@2x.png new file mode 100644 index 0000000000..7a39d6ce21 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_19x19@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_22x22.png b/installers/icons/selfnode.iconset/icon_22x22.png new file mode 100644 index 0000000000..cb15bdc508 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_22x22.png differ diff --git a/installers/icons/selfnode.iconset/icon_22x22@2x.png b/installers/icons/selfnode.iconset/icon_22x22@2x.png new file mode 100644 index 0000000000..9c934a4bc6 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_22x22@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_24x24.png b/installers/icons/selfnode.iconset/icon_24x24.png new file mode 100644 index 0000000000..0c001b441f Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_24x24.png differ diff --git a/installers/icons/selfnode.iconset/icon_24x24@2x.png b/installers/icons/selfnode.iconset/icon_24x24@2x.png new file mode 100644 index 0000000000..843803466b Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_24x24@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_256x256.png b/installers/icons/selfnode.iconset/icon_256x256.png new file mode 100644 index 0000000000..dda1881a75 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_256x256.png differ diff --git a/installers/icons/selfnode.iconset/icon_256x256@2x.png b/installers/icons/selfnode.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000..a3f7c176ee Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_256x256@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_32x32.png b/installers/icons/selfnode.iconset/icon_32x32.png new file mode 100644 index 0000000000..d0be5866da Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_32x32.png differ diff --git a/installers/icons/selfnode.iconset/icon_32x32@2x.png b/installers/icons/selfnode.iconset/icon_32x32@2x.png new file mode 100644 index 0000000000..72217210dc Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_32x32@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_40x40.png b/installers/icons/selfnode.iconset/icon_40x40.png new file mode 100644 index 0000000000..858ccd7c29 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_40x40.png differ diff --git a/installers/icons/selfnode.iconset/icon_40x40@2x.png b/installers/icons/selfnode.iconset/icon_40x40@2x.png new file mode 100644 index 0000000000..083944cf1c Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_40x40@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_48x48.png b/installers/icons/selfnode.iconset/icon_48x48.png new file mode 100644 index 0000000000..dfd62f0de8 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_48x48.png differ diff --git a/installers/icons/selfnode.iconset/icon_48x48@2x.png b/installers/icons/selfnode.iconset/icon_48x48@2x.png new file mode 100644 index 0000000000..fcabf143c8 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_48x48@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_512x512.png b/installers/icons/selfnode.iconset/icon_512x512.png new file mode 100644 index 0000000000..0d575da29e Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_512x512.png differ diff --git a/installers/icons/selfnode.iconset/icon_512x512@2x.png b/installers/icons/selfnode.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000..f387ea612e Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_512x512@2x.png differ diff --git a/installers/icons/selfnode.iconset/icon_64x64.png b/installers/icons/selfnode.iconset/icon_64x64.png new file mode 100644 index 0000000000..7fbf0705f0 Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_64x64.png differ diff --git a/installers/icons/selfnode.iconset/icon_64x64@2x.png b/installers/icons/selfnode.iconset/icon_64x64@2x.png new file mode 100644 index 0000000000..cb6a031caf Binary files /dev/null and b/installers/icons/selfnode.iconset/icon_64x64@2x.png differ diff --git a/installers/icons/selfnode/1024x1024.png b/installers/icons/selfnode/1024x1024.png new file mode 100644 index 0000000000..515ce101dd Binary files /dev/null and b/installers/icons/selfnode/1024x1024.png differ diff --git a/installers/icons/selfnode/128x128.ico b/installers/icons/selfnode/128x128.ico new file mode 100644 index 0000000000..a5d3a62751 Binary files /dev/null and b/installers/icons/selfnode/128x128.ico differ diff --git a/installers/icons/selfnode/128x128.png b/installers/icons/selfnode/128x128.png new file mode 100644 index 0000000000..60d22b0502 Binary files /dev/null and b/installers/icons/selfnode/128x128.png differ diff --git a/installers/icons/selfnode/16x16.ico b/installers/icons/selfnode/16x16.ico new file mode 100644 index 0000000000..eb241687bb Binary files /dev/null and b/installers/icons/selfnode/16x16.ico differ diff --git a/installers/icons/selfnode/16x16.png b/installers/icons/selfnode/16x16.png new file mode 100644 index 0000000000..591b5644d4 Binary files /dev/null and b/installers/icons/selfnode/16x16.png differ diff --git a/installers/icons/selfnode/18x18.ico b/installers/icons/selfnode/18x18.ico new file mode 100644 index 0000000000..aae71fb7fd Binary files /dev/null and b/installers/icons/selfnode/18x18.ico differ diff --git a/installers/icons/selfnode/18x18.png b/installers/icons/selfnode/18x18.png new file mode 100644 index 0000000000..94de052734 Binary files /dev/null and b/installers/icons/selfnode/18x18.png differ diff --git a/installers/icons/selfnode/19x19.ico b/installers/icons/selfnode/19x19.ico new file mode 100644 index 0000000000..c2d5aa393d Binary files /dev/null and b/installers/icons/selfnode/19x19.ico differ diff --git a/installers/icons/selfnode/19x19.png b/installers/icons/selfnode/19x19.png new file mode 100644 index 0000000000..91593441e5 Binary files /dev/null and b/installers/icons/selfnode/19x19.png differ diff --git a/installers/icons/selfnode/22x22.ico b/installers/icons/selfnode/22x22.ico new file mode 100644 index 0000000000..6bc205619a Binary files /dev/null and b/installers/icons/selfnode/22x22.ico differ diff --git a/installers/icons/selfnode/22x22.png b/installers/icons/selfnode/22x22.png new file mode 100644 index 0000000000..cb15bdc508 Binary files /dev/null and b/installers/icons/selfnode/22x22.png differ diff --git a/installers/icons/selfnode/24x24.ico b/installers/icons/selfnode/24x24.ico new file mode 100644 index 0000000000..7ceddc1238 Binary files /dev/null and b/installers/icons/selfnode/24x24.ico differ diff --git a/installers/icons/selfnode/24x24.png b/installers/icons/selfnode/24x24.png new file mode 100644 index 0000000000..0c001b441f Binary files /dev/null and b/installers/icons/selfnode/24x24.png differ diff --git a/installers/icons/selfnode/256x256.ico b/installers/icons/selfnode/256x256.ico new file mode 100644 index 0000000000..757886222f Binary files /dev/null and b/installers/icons/selfnode/256x256.ico differ diff --git a/installers/icons/selfnode/256x256.png b/installers/icons/selfnode/256x256.png new file mode 100644 index 0000000000..dda1881a75 Binary files /dev/null and b/installers/icons/selfnode/256x256.png differ diff --git a/installers/icons/selfnode/32x32.ico b/installers/icons/selfnode/32x32.ico new file mode 100644 index 0000000000..3df8821001 Binary files /dev/null and b/installers/icons/selfnode/32x32.ico differ diff --git a/installers/icons/selfnode/32x32.png b/installers/icons/selfnode/32x32.png new file mode 100644 index 0000000000..d0be5866da Binary files /dev/null and b/installers/icons/selfnode/32x32.png differ diff --git a/installers/icons/selfnode/40x40.ico b/installers/icons/selfnode/40x40.ico new file mode 100644 index 0000000000..1ae76f56e8 Binary files /dev/null and b/installers/icons/selfnode/40x40.ico differ diff --git a/installers/icons/selfnode/40x40.png b/installers/icons/selfnode/40x40.png new file mode 100644 index 0000000000..858ccd7c29 Binary files /dev/null and b/installers/icons/selfnode/40x40.png differ diff --git a/installers/icons/selfnode/48x48.ico b/installers/icons/selfnode/48x48.ico new file mode 100644 index 0000000000..8d709f8ca3 Binary files /dev/null and b/installers/icons/selfnode/48x48.ico differ diff --git a/installers/icons/selfnode/48x48.png b/installers/icons/selfnode/48x48.png new file mode 100644 index 0000000000..dfd62f0de8 Binary files /dev/null and b/installers/icons/selfnode/48x48.png differ diff --git a/installers/icons/selfnode/512x512.png b/installers/icons/selfnode/512x512.png new file mode 100644 index 0000000000..0d575da29e Binary files /dev/null and b/installers/icons/selfnode/512x512.png differ diff --git a/installers/icons/selfnode/64x64.ico b/installers/icons/selfnode/64x64.ico new file mode 100644 index 0000000000..ba0ede3bde Binary files /dev/null and b/installers/icons/selfnode/64x64.ico differ diff --git a/installers/icons/selfnode/64x64.png b/installers/icons/selfnode/64x64.png new file mode 100644 index 0000000000..7fbf0705f0 Binary files /dev/null and b/installers/icons/selfnode/64x64.png differ diff --git a/installers/icons/selfnode/selfnode.ico b/installers/icons/selfnode/selfnode.ico new file mode 100644 index 0000000000..7c339dc4a9 Binary files /dev/null and b/installers/icons/selfnode/selfnode.ico differ diff --git a/installers/icons/staging.iconset/icon_1024x1024.png b/installers/icons/staging.iconset/icon_1024x1024.png new file mode 100644 index 0000000000..2ffcf40e7e Binary files /dev/null and b/installers/icons/staging.iconset/icon_1024x1024.png differ diff --git a/installers/icons/staging.iconset/icon_1024x1024@2x.png b/installers/icons/staging.iconset/icon_1024x1024@2x.png new file mode 100644 index 0000000000..08494f6b0d Binary files /dev/null and b/installers/icons/staging.iconset/icon_1024x1024@2x.png differ diff --git a/installers/icons/staging.iconset/icon_128x128.png b/installers/icons/staging.iconset/icon_128x128.png index c0cf09dd95..f674635333 100644 Binary files a/installers/icons/staging.iconset/icon_128x128.png and b/installers/icons/staging.iconset/icon_128x128.png differ diff --git a/installers/icons/staging.iconset/icon_128x128@2x.png b/installers/icons/staging.iconset/icon_128x128@2x.png index 3a61cef7b1..7917d62863 100644 Binary files a/installers/icons/staging.iconset/icon_128x128@2x.png and b/installers/icons/staging.iconset/icon_128x128@2x.png differ diff --git a/installers/icons/staging.iconset/icon_16x16.png b/installers/icons/staging.iconset/icon_16x16.png index a6ee04d75f..bf8565a30a 100644 Binary files a/installers/icons/staging.iconset/icon_16x16.png and b/installers/icons/staging.iconset/icon_16x16.png differ diff --git a/installers/icons/staging.iconset/icon_16x16@2x.png b/installers/icons/staging.iconset/icon_16x16@2x.png index 40fce59dee..ccf586ab22 100644 Binary files a/installers/icons/staging.iconset/icon_16x16@2x.png and b/installers/icons/staging.iconset/icon_16x16@2x.png differ diff --git a/installers/icons/staging.iconset/icon_18x18.png b/installers/icons/staging.iconset/icon_18x18.png new file mode 100644 index 0000000000..54f566901f Binary files /dev/null and b/installers/icons/staging.iconset/icon_18x18.png differ diff --git a/installers/icons/staging.iconset/icon_18x18@2x.png b/installers/icons/staging.iconset/icon_18x18@2x.png new file mode 100644 index 0000000000..57d0a13e24 Binary files /dev/null and b/installers/icons/staging.iconset/icon_18x18@2x.png differ diff --git a/installers/icons/staging.iconset/icon_19x19.png b/installers/icons/staging.iconset/icon_19x19.png new file mode 100644 index 0000000000..c0bb69b259 Binary files /dev/null and b/installers/icons/staging.iconset/icon_19x19.png differ diff --git a/installers/icons/staging.iconset/icon_19x19@2x.png b/installers/icons/staging.iconset/icon_19x19@2x.png new file mode 100644 index 0000000000..58de23b1d6 Binary files /dev/null and b/installers/icons/staging.iconset/icon_19x19@2x.png differ diff --git a/installers/icons/staging.iconset/icon_22x22.png b/installers/icons/staging.iconset/icon_22x22.png new file mode 100644 index 0000000000..d54f01d74d Binary files /dev/null and b/installers/icons/staging.iconset/icon_22x22.png differ diff --git a/installers/icons/staging.iconset/icon_22x22@2x.png b/installers/icons/staging.iconset/icon_22x22@2x.png new file mode 100644 index 0000000000..4ef59f7213 Binary files /dev/null and b/installers/icons/staging.iconset/icon_22x22@2x.png differ diff --git a/installers/icons/staging.iconset/icon_24x24.png b/installers/icons/staging.iconset/icon_24x24.png index 6900bc653f..c2ae0906ff 100644 Binary files a/installers/icons/staging.iconset/icon_24x24.png and b/installers/icons/staging.iconset/icon_24x24.png differ diff --git a/installers/icons/staging.iconset/icon_24x24@2x.png b/installers/icons/staging.iconset/icon_24x24@2x.png index 6c977d1372..a2c0de7414 100644 Binary files a/installers/icons/staging.iconset/icon_24x24@2x.png and b/installers/icons/staging.iconset/icon_24x24@2x.png differ diff --git a/installers/icons/staging.iconset/icon_256x256.png b/installers/icons/staging.iconset/icon_256x256.png index a4c821802e..6412efe3f1 100644 Binary files a/installers/icons/staging.iconset/icon_256x256.png and b/installers/icons/staging.iconset/icon_256x256.png differ diff --git a/installers/icons/staging.iconset/icon_256x256@2x.png b/installers/icons/staging.iconset/icon_256x256@2x.png index 40c48c34f7..b2f1ea6d3a 100644 Binary files a/installers/icons/staging.iconset/icon_256x256@2x.png and b/installers/icons/staging.iconset/icon_256x256@2x.png differ diff --git a/installers/icons/staging.iconset/icon_32x32.png b/installers/icons/staging.iconset/icon_32x32.png index e4186b618b..71071e32d7 100644 Binary files a/installers/icons/staging.iconset/icon_32x32.png and b/installers/icons/staging.iconset/icon_32x32.png differ diff --git a/installers/icons/staging.iconset/icon_32x32@2x.png b/installers/icons/staging.iconset/icon_32x32@2x.png index a8eed9a531..3cac724537 100644 Binary files a/installers/icons/staging.iconset/icon_32x32@2x.png and b/installers/icons/staging.iconset/icon_32x32@2x.png differ diff --git a/installers/icons/staging.iconset/icon_40x40.png b/installers/icons/staging.iconset/icon_40x40.png index 1294afc06e..abf0802ed6 100644 Binary files a/installers/icons/staging.iconset/icon_40x40.png and b/installers/icons/staging.iconset/icon_40x40.png differ diff --git a/installers/icons/staging.iconset/icon_40x40@2x.png b/installers/icons/staging.iconset/icon_40x40@2x.png index 33caeaa0f1..ca089cfbf7 100644 Binary files a/installers/icons/staging.iconset/icon_40x40@2x.png and b/installers/icons/staging.iconset/icon_40x40@2x.png differ diff --git a/installers/icons/staging.iconset/icon_48x48.png b/installers/icons/staging.iconset/icon_48x48.png index 1f3aaf6745..b0d5ba2e5f 100644 Binary files a/installers/icons/staging.iconset/icon_48x48.png and b/installers/icons/staging.iconset/icon_48x48.png differ diff --git a/installers/icons/staging.iconset/icon_48x48@2x.png b/installers/icons/staging.iconset/icon_48x48@2x.png index 851d969638..a6040e0863 100644 Binary files a/installers/icons/staging.iconset/icon_48x48@2x.png and b/installers/icons/staging.iconset/icon_48x48@2x.png differ diff --git a/installers/icons/staging.iconset/icon_512x512.png b/installers/icons/staging.iconset/icon_512x512.png index 4811a4bfc3..0001879d60 100644 Binary files a/installers/icons/staging.iconset/icon_512x512.png and b/installers/icons/staging.iconset/icon_512x512.png differ diff --git a/installers/icons/staging.iconset/icon_512x512@2x.png b/installers/icons/staging.iconset/icon_512x512@2x.png index d02f432d32..1ec64b9221 100644 Binary files a/installers/icons/staging.iconset/icon_512x512@2x.png and b/installers/icons/staging.iconset/icon_512x512@2x.png differ diff --git a/installers/icons/staging.iconset/icon_64x64.png b/installers/icons/staging.iconset/icon_64x64.png index 0dda63ce01..c4c1cccee5 100644 Binary files a/installers/icons/staging.iconset/icon_64x64.png and b/installers/icons/staging.iconset/icon_64x64.png differ diff --git a/installers/icons/staging.iconset/icon_64x64@2x.png b/installers/icons/staging.iconset/icon_64x64@2x.png index aa6fbe8e9c..d83b3bf5b5 100644 Binary files a/installers/icons/staging.iconset/icon_64x64@2x.png and b/installers/icons/staging.iconset/icon_64x64@2x.png differ diff --git a/installers/icons/staging/1024x1024.png b/installers/icons/staging/1024x1024.png index d02f432d32..2ffcf40e7e 100644 Binary files a/installers/icons/staging/1024x1024.png and b/installers/icons/staging/1024x1024.png differ diff --git a/installers/icons/staging/128x128.ico b/installers/icons/staging/128x128.ico index 8255bf837b..679df7c5c7 100644 Binary files a/installers/icons/staging/128x128.ico and b/installers/icons/staging/128x128.ico differ diff --git a/installers/icons/staging/128x128.png b/installers/icons/staging/128x128.png index c0cf09dd95..f674635333 100644 Binary files a/installers/icons/staging/128x128.png and b/installers/icons/staging/128x128.png differ diff --git a/installers/icons/staging/16x16.ico b/installers/icons/staging/16x16.ico index b61402bc66..86b65b6201 100644 Binary files a/installers/icons/staging/16x16.ico and b/installers/icons/staging/16x16.ico differ diff --git a/installers/icons/staging/16x16.png b/installers/icons/staging/16x16.png index a6ee04d75f..bf8565a30a 100644 Binary files a/installers/icons/staging/16x16.png and b/installers/icons/staging/16x16.png differ diff --git a/installers/icons/staging/18x18.ico b/installers/icons/staging/18x18.ico index b80e0f03db..c97f9657fc 100644 Binary files a/installers/icons/staging/18x18.ico and b/installers/icons/staging/18x18.ico differ diff --git a/installers/icons/staging/18x18.png b/installers/icons/staging/18x18.png index f43138d485..54f566901f 100644 Binary files a/installers/icons/staging/18x18.png and b/installers/icons/staging/18x18.png differ diff --git a/installers/icons/staging/19x19.ico b/installers/icons/staging/19x19.ico index 0aa07084ee..1b79dd4dc3 100644 Binary files a/installers/icons/staging/19x19.ico and b/installers/icons/staging/19x19.ico differ diff --git a/installers/icons/staging/19x19.png b/installers/icons/staging/19x19.png index 4fbb976174..c0bb69b259 100644 Binary files a/installers/icons/staging/19x19.png and b/installers/icons/staging/19x19.png differ diff --git a/installers/icons/staging/22x22.ico b/installers/icons/staging/22x22.ico index 65bb0b8e58..e7ccda85b9 100644 Binary files a/installers/icons/staging/22x22.ico and b/installers/icons/staging/22x22.ico differ diff --git a/installers/icons/staging/22x22.png b/installers/icons/staging/22x22.png index f1ac6a5159..d54f01d74d 100644 Binary files a/installers/icons/staging/22x22.png and b/installers/icons/staging/22x22.png differ diff --git a/installers/icons/staging/24x24.ico b/installers/icons/staging/24x24.ico index b44a9dc18d..5cd3723dde 100644 Binary files a/installers/icons/staging/24x24.ico and b/installers/icons/staging/24x24.ico differ diff --git a/installers/icons/staging/24x24.png b/installers/icons/staging/24x24.png index 6900bc653f..c2ae0906ff 100644 Binary files a/installers/icons/staging/24x24.png and b/installers/icons/staging/24x24.png differ diff --git a/installers/icons/staging/256x256.ico b/installers/icons/staging/256x256.ico index eb5d56f43f..001d63b3d5 100644 Binary files a/installers/icons/staging/256x256.ico and b/installers/icons/staging/256x256.ico differ diff --git a/installers/icons/staging/256x256.png b/installers/icons/staging/256x256.png index a4c821802e..6412efe3f1 100644 Binary files a/installers/icons/staging/256x256.png and b/installers/icons/staging/256x256.png differ diff --git a/installers/icons/staging/32x32.ico b/installers/icons/staging/32x32.ico index 3e74490de6..a12a0020be 100644 Binary files a/installers/icons/staging/32x32.ico and b/installers/icons/staging/32x32.ico differ diff --git a/installers/icons/staging/32x32.png b/installers/icons/staging/32x32.png index e4186b618b..71071e32d7 100644 Binary files a/installers/icons/staging/32x32.png and b/installers/icons/staging/32x32.png differ diff --git a/installers/icons/staging/40x40.ico b/installers/icons/staging/40x40.ico index 7bc290ac6a..228aa95a4a 100644 Binary files a/installers/icons/staging/40x40.ico and b/installers/icons/staging/40x40.ico differ diff --git a/installers/icons/staging/40x40.png b/installers/icons/staging/40x40.png index 1294afc06e..abf0802ed6 100644 Binary files a/installers/icons/staging/40x40.png and b/installers/icons/staging/40x40.png differ diff --git a/installers/icons/staging/48x48.ico b/installers/icons/staging/48x48.ico index 7693331e88..ac6b293c54 100644 Binary files a/installers/icons/staging/48x48.ico and b/installers/icons/staging/48x48.ico differ diff --git a/installers/icons/staging/48x48.png b/installers/icons/staging/48x48.png index 1f3aaf6745..b0d5ba2e5f 100644 Binary files a/installers/icons/staging/48x48.png and b/installers/icons/staging/48x48.png differ diff --git a/installers/icons/staging/512x512.png b/installers/icons/staging/512x512.png index 4811a4bfc3..0001879d60 100644 Binary files a/installers/icons/staging/512x512.png and b/installers/icons/staging/512x512.png differ diff --git a/installers/icons/staging/64x64.ico b/installers/icons/staging/64x64.ico index 28966de4e1..a31bf5e932 100644 Binary files a/installers/icons/staging/64x64.ico and b/installers/icons/staging/64x64.ico differ diff --git a/installers/icons/staging/64x64.png b/installers/icons/staging/64x64.png index 0dda63ce01..c4c1cccee5 100644 Binary files a/installers/icons/staging/64x64.png and b/installers/icons/staging/64x64.png differ diff --git a/installers/icons/staging/staging.ico b/installers/icons/staging/staging.ico index 24aa249442..0797931da2 100644 Binary files a/installers/icons/staging/staging.ico and b/installers/icons/staging/staging.ico differ diff --git a/installers/icons/testnet.iconset/icon_1024x1024.png b/installers/icons/testnet.iconset/icon_1024x1024.png new file mode 100644 index 0000000000..289d9fb504 Binary files /dev/null and b/installers/icons/testnet.iconset/icon_1024x1024.png differ diff --git a/installers/icons/testnet.iconset/icon_1024x1024@2x.png b/installers/icons/testnet.iconset/icon_1024x1024@2x.png new file mode 100644 index 0000000000..52ea16f5c5 Binary files /dev/null and b/installers/icons/testnet.iconset/icon_1024x1024@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_128x128.png b/installers/icons/testnet.iconset/icon_128x128.png index d204b68694..a74db1e8b3 100644 Binary files a/installers/icons/testnet.iconset/icon_128x128.png and b/installers/icons/testnet.iconset/icon_128x128.png differ diff --git a/installers/icons/testnet.iconset/icon_128x128@2x.png b/installers/icons/testnet.iconset/icon_128x128@2x.png index 0fa80e0928..8521a2e4cf 100644 Binary files a/installers/icons/testnet.iconset/icon_128x128@2x.png and b/installers/icons/testnet.iconset/icon_128x128@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_16x16.png b/installers/icons/testnet.iconset/icon_16x16.png index bd40ef25ce..2dcd440146 100644 Binary files a/installers/icons/testnet.iconset/icon_16x16.png and b/installers/icons/testnet.iconset/icon_16x16.png differ diff --git a/installers/icons/testnet.iconset/icon_16x16@2x.png b/installers/icons/testnet.iconset/icon_16x16@2x.png index 15af0e5e24..1902aaf385 100644 Binary files a/installers/icons/testnet.iconset/icon_16x16@2x.png and b/installers/icons/testnet.iconset/icon_16x16@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_18x18.png b/installers/icons/testnet.iconset/icon_18x18.png new file mode 100644 index 0000000000..910d5a4986 Binary files /dev/null and b/installers/icons/testnet.iconset/icon_18x18.png differ diff --git a/installers/icons/testnet.iconset/icon_18x18@2x.png b/installers/icons/testnet.iconset/icon_18x18@2x.png new file mode 100644 index 0000000000..4b21177082 Binary files /dev/null and b/installers/icons/testnet.iconset/icon_18x18@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_19x19.png b/installers/icons/testnet.iconset/icon_19x19.png new file mode 100644 index 0000000000..0fc76cffdb Binary files /dev/null and b/installers/icons/testnet.iconset/icon_19x19.png differ diff --git a/installers/icons/testnet.iconset/icon_19x19@2x.png b/installers/icons/testnet.iconset/icon_19x19@2x.png new file mode 100644 index 0000000000..a3fa80aa4e Binary files /dev/null and b/installers/icons/testnet.iconset/icon_19x19@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_22x22.png b/installers/icons/testnet.iconset/icon_22x22.png new file mode 100644 index 0000000000..8be57fd323 Binary files /dev/null and b/installers/icons/testnet.iconset/icon_22x22.png differ diff --git a/installers/icons/testnet.iconset/icon_22x22@2x.png b/installers/icons/testnet.iconset/icon_22x22@2x.png new file mode 100644 index 0000000000..ef674c841e Binary files /dev/null and b/installers/icons/testnet.iconset/icon_22x22@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_24x24.png b/installers/icons/testnet.iconset/icon_24x24.png index 4b8d06131f..8eb22f1e28 100644 Binary files a/installers/icons/testnet.iconset/icon_24x24.png and b/installers/icons/testnet.iconset/icon_24x24.png differ diff --git a/installers/icons/testnet.iconset/icon_24x24@2x.png b/installers/icons/testnet.iconset/icon_24x24@2x.png index 8769b8f5cd..d066f79039 100644 Binary files a/installers/icons/testnet.iconset/icon_24x24@2x.png and b/installers/icons/testnet.iconset/icon_24x24@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_256x256.png b/installers/icons/testnet.iconset/icon_256x256.png index 9da70afc9f..cde3d5458d 100644 Binary files a/installers/icons/testnet.iconset/icon_256x256.png and b/installers/icons/testnet.iconset/icon_256x256.png differ diff --git a/installers/icons/testnet.iconset/icon_256x256@2x.png b/installers/icons/testnet.iconset/icon_256x256@2x.png index 4b7a7ec653..15b2bd80bf 100644 Binary files a/installers/icons/testnet.iconset/icon_256x256@2x.png and b/installers/icons/testnet.iconset/icon_256x256@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_32x32.png b/installers/icons/testnet.iconset/icon_32x32.png index 67713495ac..99437a5c89 100644 Binary files a/installers/icons/testnet.iconset/icon_32x32.png and b/installers/icons/testnet.iconset/icon_32x32.png differ diff --git a/installers/icons/testnet.iconset/icon_32x32@2x.png b/installers/icons/testnet.iconset/icon_32x32@2x.png index 79ac0d9500..51b612c901 100644 Binary files a/installers/icons/testnet.iconset/icon_32x32@2x.png and b/installers/icons/testnet.iconset/icon_32x32@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_40x40.png b/installers/icons/testnet.iconset/icon_40x40.png index 5c68eccb33..6b3a6237ad 100644 Binary files a/installers/icons/testnet.iconset/icon_40x40.png and b/installers/icons/testnet.iconset/icon_40x40.png differ diff --git a/installers/icons/testnet.iconset/icon_40x40@2x.png b/installers/icons/testnet.iconset/icon_40x40@2x.png index f53d68c39d..1693f87a4e 100644 Binary files a/installers/icons/testnet.iconset/icon_40x40@2x.png and b/installers/icons/testnet.iconset/icon_40x40@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_48x48.png b/installers/icons/testnet.iconset/icon_48x48.png index 75e1b26667..78eb9ad55b 100644 Binary files a/installers/icons/testnet.iconset/icon_48x48.png and b/installers/icons/testnet.iconset/icon_48x48.png differ diff --git a/installers/icons/testnet.iconset/icon_48x48@2x.png b/installers/icons/testnet.iconset/icon_48x48@2x.png index e44f28df0c..d049c35d13 100644 Binary files a/installers/icons/testnet.iconset/icon_48x48@2x.png and b/installers/icons/testnet.iconset/icon_48x48@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_512x512.png b/installers/icons/testnet.iconset/icon_512x512.png index eec6cdeed8..54997c800a 100644 Binary files a/installers/icons/testnet.iconset/icon_512x512.png and b/installers/icons/testnet.iconset/icon_512x512.png differ diff --git a/installers/icons/testnet.iconset/icon_512x512@2x.png b/installers/icons/testnet.iconset/icon_512x512@2x.png index f639b78257..6449a83b33 100644 Binary files a/installers/icons/testnet.iconset/icon_512x512@2x.png and b/installers/icons/testnet.iconset/icon_512x512@2x.png differ diff --git a/installers/icons/testnet.iconset/icon_64x64.png b/installers/icons/testnet.iconset/icon_64x64.png index a39a62bdb4..780d091231 100644 Binary files a/installers/icons/testnet.iconset/icon_64x64.png and b/installers/icons/testnet.iconset/icon_64x64.png differ diff --git a/installers/icons/testnet.iconset/icon_64x64@2x.png b/installers/icons/testnet.iconset/icon_64x64@2x.png index 07b6bfc25c..9df0ce6372 100644 Binary files a/installers/icons/testnet.iconset/icon_64x64@2x.png and b/installers/icons/testnet.iconset/icon_64x64@2x.png differ diff --git a/installers/icons/testnet/1024x1024.png b/installers/icons/testnet/1024x1024.png index f639b78257..289d9fb504 100644 Binary files a/installers/icons/testnet/1024x1024.png and b/installers/icons/testnet/1024x1024.png differ diff --git a/installers/icons/testnet/128x128.ico b/installers/icons/testnet/128x128.ico index 1dc483bbf2..977bc45329 100644 Binary files a/installers/icons/testnet/128x128.ico and b/installers/icons/testnet/128x128.ico differ diff --git a/installers/icons/testnet/128x128.png b/installers/icons/testnet/128x128.png index d204b68694..a74db1e8b3 100644 Binary files a/installers/icons/testnet/128x128.png and b/installers/icons/testnet/128x128.png differ diff --git a/installers/icons/testnet/16x16.ico b/installers/icons/testnet/16x16.ico index ce8804e27a..5b56282b90 100644 Binary files a/installers/icons/testnet/16x16.ico and b/installers/icons/testnet/16x16.ico differ diff --git a/installers/icons/testnet/16x16.png b/installers/icons/testnet/16x16.png index bd40ef25ce..2dcd440146 100644 Binary files a/installers/icons/testnet/16x16.png and b/installers/icons/testnet/16x16.png differ diff --git a/installers/icons/testnet/18x18.ico b/installers/icons/testnet/18x18.ico index ffd75f4f6e..35f657f2ed 100644 Binary files a/installers/icons/testnet/18x18.ico and b/installers/icons/testnet/18x18.ico differ diff --git a/installers/icons/testnet/18x18.png b/installers/icons/testnet/18x18.png index 368d287042..910d5a4986 100644 Binary files a/installers/icons/testnet/18x18.png and b/installers/icons/testnet/18x18.png differ diff --git a/installers/icons/testnet/19x19.ico b/installers/icons/testnet/19x19.ico index acff68a040..0ef4f06a9e 100644 Binary files a/installers/icons/testnet/19x19.ico and b/installers/icons/testnet/19x19.ico differ diff --git a/installers/icons/testnet/19x19.png b/installers/icons/testnet/19x19.png index 63c4cee79f..0fc76cffdb 100644 Binary files a/installers/icons/testnet/19x19.png and b/installers/icons/testnet/19x19.png differ diff --git a/installers/icons/testnet/22x22.ico b/installers/icons/testnet/22x22.ico index a9d74d8fa6..49d135e124 100644 Binary files a/installers/icons/testnet/22x22.ico and b/installers/icons/testnet/22x22.ico differ diff --git a/installers/icons/testnet/22x22.png b/installers/icons/testnet/22x22.png index 356a400793..8be57fd323 100644 Binary files a/installers/icons/testnet/22x22.png and b/installers/icons/testnet/22x22.png differ diff --git a/installers/icons/testnet/24x24.ico b/installers/icons/testnet/24x24.ico index ed4f72a028..23461f69c1 100644 Binary files a/installers/icons/testnet/24x24.ico and b/installers/icons/testnet/24x24.ico differ diff --git a/installers/icons/testnet/24x24.png b/installers/icons/testnet/24x24.png index 4b8d06131f..8eb22f1e28 100644 Binary files a/installers/icons/testnet/24x24.png and b/installers/icons/testnet/24x24.png differ diff --git a/installers/icons/testnet/256x256.ico b/installers/icons/testnet/256x256.ico index 9c03b26aa1..2d5a6716c3 100644 Binary files a/installers/icons/testnet/256x256.ico and b/installers/icons/testnet/256x256.ico differ diff --git a/installers/icons/testnet/256x256.png b/installers/icons/testnet/256x256.png index 9da70afc9f..cde3d5458d 100644 Binary files a/installers/icons/testnet/256x256.png and b/installers/icons/testnet/256x256.png differ diff --git a/installers/icons/testnet/32x32.ico b/installers/icons/testnet/32x32.ico index fb21096b24..b81dfe59ed 100644 Binary files a/installers/icons/testnet/32x32.ico and b/installers/icons/testnet/32x32.ico differ diff --git a/installers/icons/testnet/32x32.png b/installers/icons/testnet/32x32.png index 67713495ac..99437a5c89 100644 Binary files a/installers/icons/testnet/32x32.png and b/installers/icons/testnet/32x32.png differ diff --git a/installers/icons/testnet/40x40.ico b/installers/icons/testnet/40x40.ico index 8baaee8269..fca38feb1f 100644 Binary files a/installers/icons/testnet/40x40.ico and b/installers/icons/testnet/40x40.ico differ diff --git a/installers/icons/testnet/40x40.png b/installers/icons/testnet/40x40.png index 5c68eccb33..6b3a6237ad 100644 Binary files a/installers/icons/testnet/40x40.png and b/installers/icons/testnet/40x40.png differ diff --git a/installers/icons/testnet/48x48.ico b/installers/icons/testnet/48x48.ico index 8d0ded4aa8..ec72c04828 100644 Binary files a/installers/icons/testnet/48x48.ico and b/installers/icons/testnet/48x48.ico differ diff --git a/installers/icons/testnet/48x48.png b/installers/icons/testnet/48x48.png index 75e1b26667..78eb9ad55b 100644 Binary files a/installers/icons/testnet/48x48.png and b/installers/icons/testnet/48x48.png differ diff --git a/installers/icons/testnet/512x512.png b/installers/icons/testnet/512x512.png index eec6cdeed8..54997c800a 100644 Binary files a/installers/icons/testnet/512x512.png and b/installers/icons/testnet/512x512.png differ diff --git a/installers/icons/testnet/64x64.ico b/installers/icons/testnet/64x64.ico index 74223c7a14..dd2c5c5ea2 100644 Binary files a/installers/icons/testnet/64x64.ico and b/installers/icons/testnet/64x64.ico differ diff --git a/installers/icons/testnet/64x64.png b/installers/icons/testnet/64x64.png index a39a62bdb4..780d091231 100644 Binary files a/installers/icons/testnet/64x64.png and b/installers/icons/testnet/64x64.png differ diff --git a/installers/icons/testnet/testnet.ico b/installers/icons/testnet/testnet.ico index a9f312c32f..780a11e8b5 100644 Binary files a/installers/icons/testnet/testnet.ico and b/installers/icons/testnet/testnet.ico differ diff --git a/installers/icons/update_icons b/installers/icons/update_icons index 77c925ea5f..a8f04401aa 100755 --- a/installers/icons/update_icons +++ b/installers/icons/update_icons @@ -2,7 +2,7 @@ # # this script merges the png files into a single ico for windows -for x in mainnet staging testnet; do +for x in mainnet mainnet_flight staging testnet qa nightly itn_rewards_v1 itn_selfnode selfnode; do pushd $x convert 16x16.png 24x24.png 32x32.png 48x48.png 64x64.png 128x128.png 256x256.png $x.ico popd diff --git a/installers/nix/electron.nix b/installers/nix/electron.nix index 5a00a883f5..2995bc9716 100644 --- a/installers/nix/electron.nix +++ b/installers/nix/electron.nix @@ -1,4 +1,4 @@ -{ stdenv, libXScrnSaver, makeWrapper, fetchurl, unzip, atomEnv, libuuid, at-spi2-atk }: +{ stdenv, libXScrnSaver, makeWrapper, fetchurl, unzip, atomEnv, libuuid, at-spi2-atk, at_spi2_core }: let version = "4.0.0"; @@ -47,7 +47,7 @@ let patchelf \ --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" \ - --set-rpath "${atomEnv.libPath}:${stdenv.lib.makeLibraryPath [ libuuid at-spi2-atk ]}:$out/lib/electron" \ + --set-rpath "${atomEnv.libPath}:${stdenv.lib.makeLibraryPath [ libuuid at-spi2-atk at_spi2_core ]}:$out/lib/electron" \ $out/lib/electron/electron wrapProgram $out/lib/electron/electron \ diff --git a/installers/nix/linux.nix b/installers/nix/linux.nix index 55e0adc5a8..ae477c3a14 100644 --- a/installers/nix/linux.nix +++ b/installers/nix/linux.nix @@ -1,18 +1,19 @@ -{ stdenv, runCommand, writeText, writeScriptBin, electron3, -coreutils, utillinux, procps, cluster, -rawapp, daedalus-bridge, daedalus-installer, -sandboxed ? false +{ stdenv, runCommand, writeText, writeScriptBin, electron8 +, coreutils, utillinux, procps, cluster +, rawapp, daedalus-bridge, daedalus-installer +, sandboxed ? false +, nodeImplementation +, jormungandrLib +, launcherConfigs +, linuxClusterBinName }: let + cluster' = launcherConfigs.launcherConfig.networkName; daedalus-config = runCommand "daedalus-config" {} '' mkdir -pv $out - ## TODO: we don't need all of the genesis files (even if file names sound cool), - ## but the choice would have to be made in the Dhall-generated files, - ## splitting the dep chain further: - cp -v ${daedalus-bridge}/config/* $out cd $out - ${daedalus-installer}/bin/make-installer --out-dir "." --cluster ${cluster} config "${daedalus-installer.src}/dhall" "." + cp ${writeText "launcher-config.yaml" (builtins.toJSON launcherConfigs.launcherConfig)} $out/launcher-config.yaml ''; # closure size TODO list # electron depends on cups, which depends on avahi @@ -24,7 +25,7 @@ let cd "''${DAEDALUS_DIR}/${cluster}/" - exec ${electron3}/bin/electron ${rawapp}/share/daedalus "$@" + exec ${electron8}/bin/electron --disable-setuid-sandbox --no-sandbox ${rawapp}/share/daedalus "$@" ''; daedalus = writeScriptBin "daedalus" '' #!${stdenv.shell} @@ -37,15 +38,15 @@ let ''} test -z "$XDG_DATA_HOME" && { XDG_DATA_HOME="''${HOME}/.local/share"; } - export CLUSTER=${cluster} + export CLUSTER=${cluster'} export DAEDALUS_DIR="''${XDG_DATA_HOME}/Daedalus" - export DAEDALUS_CONFIG=${if sandboxed then "/nix/var/nix/profiles/profile-${cluster}/etc" else daedalus-config} + export DAEDALUS_CONFIG=${if sandboxed then "/nix/var/nix/profiles/profile-${linuxClusterBinName}/etc" else daedalus-config} mkdir -p "''${DAEDALUS_DIR}/${cluster}/"{Logs/pub,Secrets} cd "''${DAEDALUS_DIR}/${cluster}/" exec ${daedalus-bridge}/bin/cardano-launcher \ - --config ${if sandboxed then "/nix/var/nix/profiles/profile-${cluster}/etc/launcher-config.yaml" else "${daedalus-config}/launcher-config.yaml"} + --config ${if sandboxed then "/nix/var/nix/profiles/profile-${linuxClusterBinName}/etc/launcher-config.yaml" else "${daedalus-config}/launcher-config.yaml"} ''; wrappedConfig = runCommand "launcher-config" {} '' mkdir -pv $out/etc/ diff --git a/installers/nix/nix-installer.nix b/installers/nix/nix-installer.nix index 854055848f..a49284c1ad 100644 --- a/installers/nix/nix-installer.nix +++ b/installers/nix/nix-installer.nix @@ -1,6 +1,6 @@ { installationSlug ? "nix-install", installedPackages , postInstall ? null, nix-bundle, preInstall ? null -, cluster +, linuxClusterBinName , rawapp , pkgs }: let @@ -9,13 +9,6 @@ let run = "/bin/installer"; nixUserChrootFlags = "-c -m /home:/home -p HOME"; }; - nixSrc = pkgs.fetchFromGitHub { - owner = "nixos"; - repo = "nix"; - rev = "16551f54c94f2b551ebaf00a7bd0245dc3b0b9e4"; - sha256 = "0kd13v4xl4imwb3141pnn0lqx0xfcmgnwd026c10kmvjhm848pwx"; - }; - nixFix = pkgs.nixUnstable.overrideDerivation (drv: { src = nixSrc; }); utils = pkgs.writeText "utils.sh" '' function rmrf { chmod -R +w "$*" || true @@ -50,7 +43,7 @@ let nix copy --no-check-sigs --from local?root=$UNPACK2 $(readlink $UNPACK2/firstGeneration) export NIX_PROFILE=/nix/var/nix/profiles/profile nix-env --set $(readlink $UNPACK2/firstGeneration) - nix-env -p /nix/var/nix/profiles/profile-${cluster} --set $(readlink $UNPACK2/firstGeneration) + nix-env -p /nix/var/nix/profiles/profile-${linuxClusterBinName} --set $(readlink $UNPACK2/firstGeneration) rmrf $UNPACK2 post-install || true @@ -81,9 +74,9 @@ let set -e - export PATH=/nix/var/nix/profiles/profile-${cluster}/bin + export PATH=/nix/var/nix/profiles/profile-${linuxClusterBinName}/bin export PS1='\[\033]2;\h:\u:\w\007\]\n\[\033[1;32m\][\u@\h:\w] (namespaced) \$\[\033[0m\] ' - ln -svf /nix/var/nix/profiles/profile-${cluster}/bin/ /bin + ln -svf /nix/var/nix/profiles/profile-${linuxClusterBinName}/bin/ /bin export PATH=/bin ln -svf ${pkgs.iana-etc}/etc/protocols /etc/protocols ln -svf ${pkgs.iana-etc}/etc/services /etc/services @@ -121,7 +114,7 @@ let trap "exitHandler" EXIT - export PATH=${lib.makeBinPath [ coreutils pv xz gnutar nixFix gnused which gnugrep ]} + export PATH=${lib.makeBinPath [ coreutils pv xz gnutar nix gnused which gnugrep ]} export DIR=$HOME/${installationSlug} ${if preInstall == null then "" else '' @@ -149,7 +142,7 @@ let unset UNPACK export NIX_PROFILE=$DIR/nix/var/nix/profiles/profile nix-env --set ${builtins.unsafeDiscardStringContext firstGeneration} - nix-env -p $DIR/nix/var/nix/profiles/profile-${cluster} --set ${builtins.unsafeDiscardStringContext firstGeneration} + nix-env -p $DIR/nix/var/nix/profiles/profile-${linuxClusterBinName} --set ${builtins.unsafeDiscardStringContext firstGeneration} ${if postInstall == null then "" else '' exec ${postInstall}/bin/post-install @@ -159,7 +152,7 @@ let firstGeneration = with pkgs; buildEnv { name = "profile"; paths = [ - nixFix + nix bashInteractive enter coreutils diff --git a/installers/overlays/dhall-json.nix b/installers/overlays/dhall-json.nix new file mode 100644 index 0000000000..e3717748f5 --- /dev/null +++ b/installers/overlays/dhall-json.nix @@ -0,0 +1,29 @@ +{ mkDerivation, aeson, aeson-pretty, ansi-terminal, base +, bytestring, containers, dhall, exceptions, filepath, libyaml +, optparse-applicative, prettyprinter, prettyprinter-ansi-terminal +, scientific, stdenv, tasty, tasty-hunit, text +, unordered-containers, vector, yaml +}: +mkDerivation { + pname = "dhall-json"; + version = "1.4.1"; + sha256 = "f70ef4d4c34624e761e339977a01d7cac0b0bbef228231d25f46729ddfdd2df2"; + revision = "1"; + editedCabalFile = "0vwr27ikw0y39za9jc91g3xbd7vb745zkkni0x3k73944w0w47n3"; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson aeson-pretty base bytestring containers dhall exceptions + filepath libyaml optparse-applicative prettyprinter scientific text + unordered-containers vector yaml + ]; + executableHaskellDepends = [ + aeson aeson-pretty ansi-terminal base bytestring dhall exceptions + optparse-applicative prettyprinter prettyprinter-ansi-terminal text + ]; + testHaskellDepends = [ + aeson base bytestring dhall tasty tasty-hunit text + ]; + description = "Convert between Dhall and JSON or YAML"; + license = stdenv.lib.licenses.bsd3; +} diff --git a/installers/overlays/dhall.nix b/installers/overlays/dhall.nix new file mode 100644 index 0000000000..a95d44588a --- /dev/null +++ b/installers/overlays/dhall.nix @@ -0,0 +1,44 @@ +{ mkDerivation, aeson, aeson-pretty, ansi-terminal, base +, bytestring, case-insensitive, cborg, cborg-json, containers +, contravariant, cryptonite, data-fix, deepseq, Diff, directory +, doctest, dotgen, either, exceptions, filepath, foldl, gauge +, generic-random, haskeline, http-client, http-client-tls +, http-types, lens-family-core, megaparsec, memory, mockery, mtl +, network-uri, optparse-applicative, parsers, prettyprinter +, prettyprinter-ansi-terminal, profunctors, QuickCheck +, quickcheck-instances, repline, scientific, semigroups, serialise +, spoon, stdenv, tasty, tasty-expected-failure, tasty-hunit +, tasty-quickcheck, template-haskell, text, th-lift-instances +, transformers, transformers-compat, turtle, unordered-containers +, uri-encode, vector +}: +mkDerivation { + pname = "dhall"; + version = "1.26.1"; + sha256 = "f6269eb7f986e600ec5252a0b793b0a0a60eb24db28ff9c3e5f2adb006b51ebc"; + isLibrary = true; + isExecutable = true; + libraryHaskellDepends = [ + aeson aeson-pretty ansi-terminal base bytestring case-insensitive + cborg cborg-json containers contravariant cryptonite data-fix + deepseq Diff directory dotgen either exceptions filepath haskeline + http-client http-client-tls http-types lens-family-core megaparsec + memory mtl network-uri optparse-applicative parsers prettyprinter + prettyprinter-ansi-terminal profunctors repline scientific + serialise template-haskell text th-lift-instances transformers + transformers-compat unordered-containers uri-encode vector + ]; + executableHaskellDepends = [ base ]; + testHaskellDepends = [ + base bytestring cborg containers data-fix deepseq directory doctest + filepath foldl generic-random lens-family-core megaparsec mockery + prettyprinter QuickCheck quickcheck-instances scientific semigroups + serialise spoon tasty tasty-expected-failure tasty-hunit + tasty-quickcheck text transformers turtle vector + ]; + benchmarkHaskellDepends = [ + base bytestring containers directory gauge serialise text + ]; + description = "A configuration language guaranteed to terminate"; + license = stdenv.lib.licenses.bsd3; +} diff --git a/installers/overlays/nsis.nix b/installers/overlays/nsis.nix new file mode 100644 index 0000000000..9548ee58db --- /dev/null +++ b/installers/overlays/nsis.nix @@ -0,0 +1,20 @@ +{ mkDerivation, base, directory, fetchFromGitHub, process, stdenv +, transformers, uniplate +}: +mkDerivation { + pname = "nsis"; + version = "0.3.2"; + src = fetchFromGitHub { + owner = "input-output-hk"; + repo = "haskell-nsis"; + rev = "020e61eced93eaa6ab86ac603617e93aa6bf5af0"; + sha256 = "0l0naknnyyrmkrn41mn7ggnjdagld0isdji98khn2iasbcllyip5"; + }; + libraryHaskellDepends = [ base transformers uniplate ]; + testHaskellDepends = [ + base directory process transformers uniplate + ]; + homepage = "https://github.com/ndmitchell/nsis#readme"; + description = "DSL for producing Windows Installer using NSIS"; + license = stdenv.lib.licenses.bsd3; +} diff --git a/installers/overlays/required.nix b/installers/overlays/required.nix index 25a3c2bac5..64531e13b6 100644 --- a/installers/overlays/required.nix +++ b/installers/overlays/required.nix @@ -1,17 +1,11 @@ { pkgs }: -with import ../../lib.nix; - with pkgs.haskell.lib; -self: super: rec { - dhall-json = doJailbreak (self.callPackage ../../installers/dhall-json.nix {}); - dhall = doJailbreak (self.callPackage ../../installers/dhall-haskell.nix {}); - daedalus-installer = self.callPackage ../../installers/daedalus-installer.nix {}; - nsis = self.callCabal2nix "nsis" (pkgs.fetchFromGitHub { - owner = "input-output-hk"; - repo = "haskell-nsis"; - rev = "020e61eced93eaa6ab86ac603617e93aa6bf5af0"; - sha256 = "0l0naknnyyrmkrn41mn7ggnjdagld0isdji98khn2iasbcllyip5"; - }) {}; +self: super: { + daedalus-installer = self.callPackage ../daedalus-installer.nix {}; + dhall-json = self.callPackage ./dhall-json.nix {}; + dhall = dontCheck (doJailbreak (self.callPackage ./dhall.nix {})); + universum = dontCheck (self.callPackage ./universum.nix {}); + nsis = self.callPackage ./nsis.nix {}; } diff --git a/installers/overlays/universum.nix b/installers/overlays/universum.nix new file mode 100644 index 0000000000..39b70c7cb2 --- /dev/null +++ b/installers/overlays/universum.nix @@ -0,0 +1,26 @@ +{ mkDerivation, base, bytestring, containers, deepseq, doctest +, gauge, ghc-prim, Glob, hashable, hedgehog, microlens +, microlens-mtl, mtl, safe-exceptions, stdenv, stm, tasty +, tasty-hedgehog, text, transformers, unordered-containers +, utf8-string, vector +}: +mkDerivation { + pname = "universum"; + version = "1.5.0"; + sha256 = "53d29c4de630320c4364d37ea26a150c40e8df7faf81f69bb94372314f883f9f"; + libraryHaskellDepends = [ + base bytestring containers deepseq ghc-prim hashable microlens + microlens-mtl mtl safe-exceptions stm text transformers + unordered-containers utf8-string vector + ]; + testHaskellDepends = [ + base bytestring doctest Glob hedgehog tasty tasty-hedgehog text + utf8-string + ]; + benchmarkHaskellDepends = [ + base containers gauge unordered-containers + ]; + homepage = "https://github.com/serokell/universum"; + description = "Custom prelude used in Serokell"; + license = stdenv.lib.licenses.mit; +} diff --git a/lib.nix b/lib.nix index e0f05f267f..2069223449 100644 --- a/lib.nix +++ b/lib.nix @@ -1,38 +1,35 @@ +{ nodeImplementation ? "jormungandr" }: + let - # iohk-nix can be overridden for debugging purposes by setting - # NIX_PATH=iohk_nix=/path/to/iohk-nix - iohkNix = import ( - let try = builtins.tryEval ; - in if try.success - then builtins.trace "using host " try.value - else - let - spec = builtins.fromJSON (builtins.readFile ./iohk-nix.json); - in builtins.fetchTarball { - url = "${spec.url}/archive/${spec.rev}.tar.gz"; - inherit (spec) sha256; - }) {}; + sources = import ./nix/sources.nix; + iohkNix = import sources.iohk-nix { sourcesOverride = sources; }; + # TODO: can we use the filter in iohk-nix instead? + cleanSourceFilter = with pkgs.stdenv; + name: type: let baseName = baseNameOf (toString name); in ! ( + # Filter out .git repo + (type == "directory" && baseName == ".git") || + # Filter out editor backup / swap files. + lib.hasSuffix "~" baseName || + builtins.match "^\\.sw[a-z]$" baseName != null || + builtins.match "^\\..*\\.sw[a-z]$" baseName != null || - # NIX_PATH=cardano-sl=/path/to/cardano-sl - # WARNING: currently broken with infinite recursion - cardanoSL = { config ? {}, target }: - let try = builtins.tryEval ; - in if try.success - then builtins.trace "using host " (import try.value { inherit target; }) - else - let - spec = builtins.fromJSON (builtins.readFile ./cardano-sl-src.json); - in import (builtins.fetchTarball { - url = "${spec.url}/archive/${spec.rev}.tar.gz"; - inherit (spec) sha256; - }) { inherit target; gitrev = spec.rev; }; + # Filter out locally generated/downloaded things. + baseName == "dist" || + baseName == "node_modules" || - # nixpkgs can be overridden for debugging purposes by setting - # NIX_PATH=custom_nixpkgs=/path/to/nixpkgs + # Filter out the files which I'm editing often. + lib.hasSuffix ".nix" baseName || + lib.hasSuffix ".dhall" baseName || + lib.hasSuffix ".hs" baseName || + # Filter out nix-build result symlinks + (type == "symlink" && lib.hasPrefix "result" baseName) + ); + rustPkgs = iohkNix.rust-packages.pkgs; + isDaedalus = name: false; + cardanoSL = { target }: import sources.cardano-sl { gitrev = sources.cardano-sl.rev; }; pkgs = iohkNix.pkgs; lib = pkgs.lib; - isDaedalus = name: false; - -in lib // { - inherit iohkNix pkgs cardanoSL isDaedalus; +in +lib // { + inherit sources iohkNix pkgs cardanoSL isDaedalus cleanSourceFilter; } diff --git a/nix/cardano-bridge.nix b/nix/cardano-bridge.nix new file mode 100644 index 0000000000..6746eccda5 --- /dev/null +++ b/nix/cardano-bridge.nix @@ -0,0 +1,23 @@ +{ target, pkgs, runCommand, cardano-wallet, cardano-node, cardano-shell, export-wallets, cardano-cli }: + +let + commonLib = import ../lib.nix {}; + pkgsCross = import cardano-wallet.pkgs.path { crossSystem = cardano-wallet.pkgs.lib.systems.examples.mingwW64; config = {}; overlays = []; }; +in runCommand "daedalus-cardano-bridge" { + passthru = { + node-version = cardano-node.passthru.identifier.version; + wallet-version = cardano-wallet.version; + }; +} '' + mkdir -pv $out/bin + cd $out/bin + echo ${cardano-wallet.version} > $out/version + cp ${cardano-wallet.haskellPackages.cardano-wallet-byron.components.exes.cardano-wallet-byron}/bin/cardano-wallet-byron* . + cp ${cardano-shell.nix-tools.cexes.cardano-launcher.cardano-launcher}/bin/cardano-launcher* . + cp ${cardano-node}/bin/cardano-node* . + cp ${export-wallets}/bin/export-wallets* . + cp ${cardano-cli}/bin/cardano-cli* . + ${pkgs.lib.optionalString (target == "x86_64-windows") '' + cp ${pkgsCross.libffi}/bin/libffi-6.dll . + ''} +'' diff --git a/fastlist.nix b/nix/fastlist.nix similarity index 100% rename from fastlist.nix rename to nix/fastlist.nix diff --git a/nix/jormungandr-bridge.nix b/nix/jormungandr-bridge.nix new file mode 100644 index 0000000000..5b8a69b9c7 --- /dev/null +++ b/nix/jormungandr-bridge.nix @@ -0,0 +1,33 @@ +{ target, pkgs, cardano-wallet, cardano-shell, sources, jormungandrLib }: + +let + commonLib = import ../lib.nix {}; + pkgsCross = import cardano-wallet.pkgs.path { crossSystem = cardano-wallet.pkgs.lib.systems.examples.mingwW64; config = {}; overlays = []; }; +in pkgs.runCommandCC "daedalus-bridge" { + passthru = { + node-version = cardano-wallet.jormungandr.version; + wallet-version = cardano-wallet.version; + }; +} '' + mkdir -pv $out/bin + cd $out/bin + cp ${cardano-wallet.haskellPackages.cardano-wallet-jormungandr.components.exes.cardano-wallet-jormungandr}/bin/cardano-wallet-jormungandr* . + cp ${cardano-shell.nix-tools.cexes.cardano-launcher.cardano-launcher}/bin/cardano-launcher* . + cp ${cardano-wallet.jormungandr}/bin/jormungandr* . + + echo ${cardano-wallet.version} > $out/version + + chmod +w -R . + + ${pkgs.lib.optionalString (target == "x86_64-windows") '' + echo ${cardano-wallet.jormungandr} + cp ${pkgsCross.libffi}/bin/libffi-6.dll . + #cp {pkgsCross.openssl.out}/lib/libeay32.dll . + ''} + ${pkgs.lib.optionalString (target == "x86_64-linux") '' + for bin in cardano-launcher cardano-wallet-jormungandr; do + ${pkgs.binutils-unwrapped}/bin/strip $bin + ${pkgs.patchelf}/bin/patchelf --shrink-rpath $bin + done + ''} +'' diff --git a/nix/launcher-config.nix b/nix/launcher-config.nix new file mode 100644 index 0000000000..7dc10c2a74 --- /dev/null +++ b/nix/launcher-config.nix @@ -0,0 +1,315 @@ +{ backend ? "cardano" +, network ? "staging" +, os ? "linux" +, jormungandrLib ? (import ../. {}).jormungandrLib +, cardanoLib +, runCommand +, lib +, devShell ? false +, cardano-wallet-native +, runCommandNative +}: + +# Creates an attr set for a cluster containing: +# * launcherConfig (attr set) +# * installerConfig (attr set) +# * nodeConfigFiles +# * configFiles (launcher config + installer config) + + +let + dirSep = if os == "windows" then "\\" else "/"; + configDir = configFilesSource: { + linux = configFilesSource; + macos64 = if devShell then configFilesSource else "\${DAEDALUS_INSTALL_DIRECTORY}/../Resources"; + windows = "\${DAEDALUS_INSTALL_DIRECTORY}"; + }; + + isDevOrLinux = devShell || os == "linux"; + + mkSpacedName = network: "Daedalus ${installDirectorySuffix}"; + spacedName = mkSpacedName network; + + frontendBinPath = let + frontendBin.linux = "daedalus-frontend"; + frontendBin.windows = "${spacedName}"; + frontendBin.macos64 = "Frontend"; + in frontendBin.${os}; + + + # Helper function to make a path to a binary + mkBinPath = binary: let + binDir = { + macos64 = "\${DAEDALUS_INSTALL_DIRECTORY}"; + windows = "\${DAEDALUS_INSTALL_DIRECTORY}"; + }; + binary' = if binary == "frontend" then frontendBinPath else binary; + in if isDevOrLinux then binary' else "${binDir.${os}}${dirSep}${binary'}${lib.optionalString (os == "windows") ".exe"}"; + # Helper function to make a path to a config file + mkConfigPath = configSrc: configPath: "${(configDir configSrc).${os}}${dirSep}${configPath}"; + + envCfg = let + cardanoEnv = if network == "mainnet_flight" + then cardanoLib.environments.mainnet + else cardanoLib.environments.${network}; + jormungandrEnv = jormungandrLib.environments.${network}; + in if (backend == "cardano") then cardanoEnv else jormungandrEnv; + + installDirectorySuffix = let + supportedNetworks = { + mainnet = "Mainnet"; + mainnet_flight = "Flight"; + qa = "QA"; + selfnode = "Selfnode"; + itn_selfnode = "Selfnode - ITN"; + nightly = "Nightly"; + itn_rewards_v1 = "- Rewards v1"; + staging = "Staging"; + testnet = "Testnet"; + }; + unsupported = "Unsupported"; + networkSupported = __hasAttr network supportedNetworks; + in if networkSupported then supportedNetworks.${network} else unsupported; + + iconPath = let + networkIconExists = __pathExists (../. + "/installers/icons/${network}"); + network' = if networkIconExists then network else "mainnet"; + in { + small = ../installers/icons + "/${network'}/64x64.png"; + large = ../installers/icons + "/${network'}/1024x1024.png"; + base = ../installers/icons + "/${network'}"; + }; + + dataDir = let + path.linux = "\${XDG_DATA_HOME}/Daedalus/${network}"; + path.macos64 = "\${HOME}/Library/Application Support/${spacedName}"; + path.windows = "\${APPDATA}\\${spacedName}"; + in path.${os}; + + # Used for flight builds to find legacy paths for migration + legacyDataDir = let + path.linux = "\${XDG_DATA_HOME}/Daedalus/mainnet"; + path.macos64 = "\${HOME}/Library/Application Support/Daedalus"; + path.windows = "\${APPDATA}\\Daedalus"; + in path.${os}; + + logsPrefix = let + path.linux = "${dataDir}/Logs"; + path.windows = "Logs"; + path.macos64 = "${dataDir}/Logs"; + in path.${os}; + + tlsConfig = { + ca = { + organization = "Daedalus"; + commonName = "Daedalus Self-Signed Root CA"; + expiryDays = 3650; + }; + server = { + organization = "Daedalus"; + commonName = "Daedalus Wallet Backend"; + expiryDays = 365; + altDNS = [ + "localhost" + "localhost.localdomain" + "127.0.0.1" + "::1" + ]; + }; + clients = [ { + organization = "Daedalus"; + commonName = "Daedalus Frontend"; + expiryDays = 365; + } ]; + }; + + launcherLogsPrefix = "${logsPrefix}${dirSep}pub"; + + # Default configs for launcher from cardano-shell. Most of these do nothing. + # TODO: get rid of anything we don't need from cardano-shell + defaultLauncherConfig = { + inherit logsPrefix launcherLogsPrefix tlsConfig; + walletLogging = false; + daedalusBin = mkBinPath "frontend"; + # TODO: set when update system is complete + updaterArgs = []; + updaterPath = ""; + updateArchive = ""; + updateWindowsRunner = ""; + workingDir = dataDir; + stateDir = dataDir; + tlsPath = "${dataDir}${dirSep}tls"; + cluster = if network == "mainnet_flight" then "mainnet" else network; + networkName = if network == "mainnet_flight" then "mainnet" else network; + isFlight = network == "mainnet_flight"; + nodeImplementation = backend; + }; + + mkConfigFiles = nodeConfigFiles: launcherConfig: installerConfig: + runCommand "cfg-files" { + launcherConfig = builtins.toJSON launcherConfig; + installerConfig = builtins.toJSON installerConfig; + passAsFile = [ "launcherConfig" "installerConfig" ]; + } '' + mkdir $out + cp ${nodeConfigFiles}/* $out/ + cp $launcherConfigPath $out/launcher-config.yaml + cp $installerConfigPath $out/installer-config.json + ''; + + mkConfigByron = let + filterMonitoring = config: if devShell then config else builtins.removeAttrs config [ "hasPrometheus" "hasEKG" ]; + exportWalletsBin = mkBinPath "export-wallets"; + walletBin = mkBinPath "cardano-wallet-byron"; + nodeBin = mkBinPath "cardano-node"; + cliBin = mkBinPath "cardano-cli"; + nodeConfig = builtins.toJSON (filterMonitoring (envCfg.nodeConfig // (lib.optionalAttrs (!isDevOrLinux) { + GenesisFile = "genesis.json"; + }))); + genesisFile = if (network == "selfnode") then ../utils/cardano/selfnode/genesis.json else envCfg.genesisFile; + topologyFile = if network == "selfnode" then envCfg.topology else cardanoLib.mkEdgeTopology { + inherit (envCfg) edgePort; + edgeNodes = [ envCfg.relaysNew ]; + }; + nodeConfigFiles = let + genesisFile = if (network == "selfnode") then ../utils/cardano/selfnode/genesis.json else envCfg.genesisFile; + in runCommand "node-cfg-files" { + inherit nodeConfig; + topologyFile = if network == "selfnode" then envCfg.topology else cardanoLib.mkEdgeTopology { + inherit (envCfg) edgePort; + edgeNodes = [ envCfg.relaysNew ]; + }; + passAsFile = [ "nodeConfig" ]; + } '' + mkdir $out + cp ${genesisFile} $out/genesis.json + cp $nodeConfigPath $out/config.yaml + cp $topologyFile $out/topology.yaml + ${lib.optionalString (network == "selfnode") '' + cp ${envCfg.delegationCertificate} $out/delegation.cert + cp ${envCfg.signingKey} $out/signing.key + ''} + ''; + + legacyStateDir = if (network == "mainnet_flight") || (network == "mainnet") then legacyDataDir else dataDir; + + legacyWalletDB = let + path.linux = "Wallet"; + path.macos64 = "Wallet-1.0"; + path.windows = "Wallet-1.0"; + in path.${os}; + + legacySecretKey = let + path.linux = "Secrets${dirSep}secret.key"; + path.macos64 = "Secrets-1.0${dirSep}secret.key"; + path.windows = "Secrets-1.0${dirSep}secret.key"; + in path.${os}; + + launcherConfig = defaultLauncherConfig // { + inherit + nodeBin + cliBin + walletBin + exportWalletsBin + legacyStateDir + legacyWalletDB + legacySecretKey; + syncTolerance = "300s"; + nodeConfig = { + kind = "byron"; + configurationDir = ""; + network = { + configFile = mkConfigPath nodeConfigFiles "config.yaml"; + genesisFile = mkConfigPath nodeConfigFiles "genesis.json"; + genesisHash = if (network != "selfnode") then envCfg.genesisHash else ""; + topologyFile = mkConfigPath nodeConfigFiles "topology.yaml"; + }; + socketFile = if os != "windows" then "${dataDir}${dirSep}cardano-node.socket" else "\\\\.\\pipe\\cardano-node-${network}"; + } // (lib.optionalAttrs (network == "selfnode") { + delegationCertificate = mkConfigPath nodeConfigFiles "delegation.cert"; + signingKey = mkConfigPath nodeConfigFiles "signing.key"; + }); + }; + + installerConfig = { + installDirectory = if os == "linux" then "Daedalus/${network}" else spacedName; + inherit spacedName iconPath; + macPackageName = "Daedalus${network}"; + dataDir = dataDir; + hasBlock0 = false; + installerWinBinaries = [ "cardano-launcher.exe" "cardano-node.exe" "cardano-wallet-byron.exe" "export-wallets.exe" "cardano-cli.exe" ]; + }; + + in { + inherit nodeConfigFiles launcherConfig installerConfig; + configFiles = mkConfigFiles nodeConfigFiles launcherConfig installerConfig; + }; + + mkConfigJormungandr = let + jormungandrConfig = builtins.toJSON (jormungandrLib.mkConfig (envCfg // { + trustedPeers = envCfg.daedalusPeers or envCfg.trustedPeers; + })); + nodeBin = mkBinPath "jormungandr"; + walletBin = mkBinPath "cardano-wallet-jormungandr"; + cliBin = mkBinPath "jcli"; + hasBlock0 = (network == "itn_selfnode") || envCfg ? block0bin; + nodeConfigFiles = let + in runCommand "node-cfg-files" { + buildInputs = [ cardano-wallet-native.jormungandr-cli ]; + jormungandrConfig = if network == "itn_selfnode" then null else jormungandrConfig; + passAsFile = [ "jormungandrConfig" ]; + } '' + mkdir $out + ${if (network == "itn_selfnode") then '' + cp ${../utils/jormungandr/selfnode/config.yaml} $out/config.yaml + cp ${../utils/jormungandr/selfnode/secret.yaml} $out/secret.yaml + cp ${../utils/jormungandr/selfnode/genesis.yaml} $out/genesis.yaml + jcli genesis encode --input $out/genesis.yaml --output $out/block-0.bin + '' else '' + cp $jormungandrConfigPath $out/config.yaml + ${lib.optionalString hasBlock0 '' + cp ${envCfg.block0bin} $out/block-0.bin + jcli genesis hash --input $out/block-0.bin > $out/genesis-hash + ''} + ''} + ''; + + secretPath = mkConfigPath nodeConfigFiles "secret.yaml"; + configPath = mkConfigPath nodeConfigFiles "config.yaml"; + block0Path = if hasBlock0 then mkConfigPath nodeConfigFiles "block-0.bin" else ""; + genesisPath = mkConfigPath nodeConfigFiles "genesis.yaml"; + launcherConfig = defaultLauncherConfig // { + inherit + nodeBin + walletBin + cliBin + network + block0Path + secretPath + configPath; + block0Hash = let + selfnodeHash = builtins.replaceStrings ["\n"] [""] (builtins.readFile (runCommandNative "selfnode-block0.hash" { buildInputs = [ cardano-wallet-native.jormungandr-cli ]; } '' + jcli genesis hash --input ${nodeConfigFiles}/block-0.bin > $out + '')); + in if network == "itn_selfnode" then selfnodeHash else jormungandrLib.environments.${network}.genesisHash; + syncTolerance = if (network == "itn_selfnode") then "600s" else jormungandrLib.environments.${network}.syncTolerance; + }; + installerConfig = { + installDirectory = if os == "linux" then "Daedalus/${network}" else spacedName; + inherit spacedName iconPath dataDir hasBlock0; + installerWinBinaries = [ "cardano-launcher.exe" "jormungandr.exe" "cardano-wallet-jormungandr.exe" ]; + macPackageName = "Daedalus${network}"; + configPath = "${nodeConfigFiles}/config.yaml"; + } // (lib.optionalAttrs hasBlock0 { + block0 = "${nodeConfigFiles}/block-0.bin"; + }) // (lib.optionalAttrs (network == "itn_selfnode") { + genesisPath = "${nodeConfigFiles}/genesis.yaml"; + secretPath = "${nodeConfigFiles}/secret.yaml"; + }); + in { + inherit launcherConfig installerConfig nodeConfigFiles; + configFiles = mkConfigFiles nodeConfigFiles launcherConfig installerConfig; + }; + configs.jormungandr = mkConfigJormungandr; + configs.cardano = mkConfigByron; +in configs.${backend} diff --git a/nsis-inner.nix b/nix/nsis-inner.nix similarity index 100% rename from nsis-inner.nix rename to nix/nsis-inner.nix diff --git a/nsis.nix b/nix/nsis.nix similarity index 100% rename from nsis.nix rename to nix/nsis.nix diff --git a/nix/sources.json b/nix/sources.json new file mode 100644 index 0000000000..94c1e8d0c7 --- /dev/null +++ b/nix/sources.json @@ -0,0 +1,135 @@ +{ + "cardano-node": { + "branch": "tags/1.8.0", + "description": null, + "homepage": null, + "owner": "input-output-hk", + "repo": "cardano-node", + "rev": "64c2778c61245dcda13eaf16a2e0738dfacb7a16", + "sha256": "1aybhbip4j9275npvrk4w0bch5bl7fbq8faj1qj8h51f1g0ja74l", + "type": "tarball", + "url": "https://github.com/input-output-hk/cardano-node/archive/64c2778c61245dcda13eaf16a2e0738dfacb7a16.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "cardano-shell": { + "branch": "master", + "description": "Node shell, a thin layer for running the node and it's modules.", + "homepage": null, + "owner": "input-output-hk", + "repo": "cardano-shell", + "rev": "601bb4324c258e3c8cbd2d532e5696fd09e2582e", + "sha256": "0zzd711zgj62l729hyymnv0i6jn3rx8yrww2b4a8l3csf1d9xv7n", + "type": "tarball", + "url": "https://github.com/input-output-hk/cardano-shell/archive/601bb4324c258e3c8cbd2d532e5696fd09e2582e.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "cardano-sl": { + "branch": "KtorZ/export-wallets", + "description": "Cryptographic currency implementing Ouroboros PoS protocol", + "homepage": "", + "owner": "input-output-hk", + "repo": "cardano-sl", + "rev": "c1815b7dce5fb71d997bd3a94c4c5ccf2c9a9a94", + "sha256": "1dzfhv7qhv1498sd8jrac39qd8vgpdahv7dskg1hnfs5rw2gipar", + "type": "tarball", + "url": "https://github.com/input-output-hk/cardano-sl/archive/c1815b7dce5fb71d997bd3a94c4c5ccf2c9a9a94.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "cardano-wallet": { + "branch": "master", + "description": "Official Wallet Backend & API for Cardano decentralized", + "homepage": null, + "owner": "input-output-hk", + "repo": "cardano-wallet", + "rev": "a638544c454bf71eb4d97a18f2fc3ff49bbab2b0", + "sha256": "06qvra6x4886gff5dfnnwb8n83q0bpfz4flghlsxwb8ahh8mr2vc", + "type": "tarball", + "url": "https://github.com/input-output-hk/cardano-wallet/archive/a638544c454bf71eb4d97a18f2fc3ff49bbab2b0.tar.gz", + "url_template": "https://github.com///archive/.tar.gz", + "version": "v2020-04-07" + }, + "gitignore": { + "branch": "master", + "description": "Nix function for filtering local git sources", + "homepage": "", + "owner": "hercules-ci", + "repo": "gitignore", + "rev": "f9e996052b5af4032fe6150bba4a6fe4f7b9d698", + "sha256": "0jrh5ghisaqdd0vldbywags20m2cxpkbbk5jjjmwaw0gr8nhsafv", + "type": "tarball", + "url": "https://github.com/hercules-ci/gitignore/archive/f9e996052b5af4032fe6150bba4a6fe4f7b9d698.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "haskell.nix": { + "branch": "master", + "description": "Alternative Haskell Infrastructure for Nixpkgs", + "homepage": "https://input-output-hk.github.io/haskell.nix", + "owner": "input-output-hk", + "repo": "haskell.nix", + "rev": "6260dcb06b7d2855cbcd0a0f36483453c311248e", + "sha256": "1q9bqgf41ch3dpghsvh74xqnj98h3khj4gxk9n2kq9qai12720yv", + "type": "tarball", + "url": "https://github.com/input-output-hk/haskell.nix/archive/6260dcb06b7d2855cbcd0a0f36483453c311248e.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "iohk-nix": { + "branch": "master", + "description": "nix scripts shared across projects", + "homepage": null, + "owner": "input-output-hk", + "repo": "iohk-nix", + "rev": "2744051c403165303628c61c8fbb973fa67ab76f", + "sha256": "0gxaq42dcyb621av1jy099mjn0ajwng1wwwdkx5mllmbrvl4hfqw", + "type": "tarball", + "url": "https://github.com/input-output-hk/iohk-nix/archive/2744051c403165303628c61c8fbb973fa67ab76f.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "js-chain-libs": { + "branch": "master", + "description": "chain-libs javascript SDK", + "homepage": null, + "owner": "input-output-hk", + "repo": "js-chain-libs", + "rev": "1f1bbc5557ed5356a5fac89a3224d3449ef98ba3", + "sha256": "14v7zm2h3v4vb7f15i4mk6f5jsdd6akqv2pzqmj52hprh076p0hd", + "type": "tarball", + "url": "https://github.com/input-output-hk/js-chain-libs/archive/1f1bbc5557ed5356a5fac89a3224d3449ef98ba3.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "niv": { + "branch": "iohk", + "description": "Easy dependency management for Nix projects", + "homepage": "https://github.com/nmattia/niv", + "owner": "input-output-hk", + "repo": "niv", + "rev": "4229fbcf62997467c34283a2f353702359e78e5a", + "sha256": "1y1h6aj0rxrrhvp9jpr6xw3zsa1l8ac25ng4xzjskr5kg620pxqr", + "type": "tarball", + "url": "https://github.com/input-output-hk/niv/archive/4229fbcf62997467c34283a2f353702359e78e5a.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "nixpkgs": { + "branch": "nixos-19.09", + "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "NixOS", + "repo": "nixpkgs-channels", + "rev": "c5aabb0d603e2c1ea05f5a93b3be82437f5ebf31", + "sha256": "15fwszhn6078sbrb8qk83g8afvh4qnmvff0qbkbvq3cm1fxni2w1", + "type": "tarball", + "url": "https://github.com/NixOS/nixpkgs-channels/archive/c5aabb0d603e2c1ea05f5a93b3be82437f5ebf31.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, + "nixpkgs-nsis": { + "branch": "nsis", + "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", + "homepage": "https://github.com/NixOS/nixpkgs", + "owner": "input-output-hk", + "repo": "nixpkgs", + "rev": "be445a9074f139d63e704fa82610d25456562c3d", + "sha256": "15dc7gdspimavcwyw9nif4s59v79gk18rwsafylffs9m1ld2dxwa", + "type": "tarball", + "url": "https://github.com/input-output-hk/nixpkgs/archive/be445a9074f139d63e704fa82610d25456562c3d.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + } +} diff --git a/nix/sources.nix b/nix/sources.nix new file mode 100644 index 0000000000..b7be13a8a7 --- /dev/null +++ b/nix/sources.nix @@ -0,0 +1,130 @@ +# This file has been generated by Niv. + +# A record, from name to path, of the third-party packages +with rec +{ + pkgs = + if hasNixpkgsPath + then + if hasThisAsNixpkgsPath + then import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {} + else import {} + else + import (builtins_fetchTarball { inherit (sources_nixpkgs) url sha256; }) {}; + + sources_nixpkgs = + if builtins.hasAttr "nixpkgs" sources + then sources.nixpkgs + else abort + '' + Please specify either (through -I or NIX_PATH=nixpkgs=...) or + add a package called "nixpkgs" to your sources.json. + ''; + + sources_gitignore = + if builtins.hasAttr "gitignore" sources + then sources.gitignore + else abort + '' + Please add "gitignore" to your sources.json: + niv add hercules-ci/gitignore + ''; + + inherit (import (builtins_fetchTarball { inherit (sources_gitignore) url sha256; }) { + inherit (pkgs) lib; + }) gitignoreSource; + + # fetchTarball version that is compatible between all the versions of Nix + builtins_fetchTarball = + { url, sha256 ? null }@attrs: + let + inherit (builtins) lessThan nixVersion fetchTarball; + in + if sha256 == null || lessThan nixVersion "1.12" then + fetchTarball { inherit url; } + else + fetchTarball attrs; + + # fetchurl version that is compatible between all the versions of Nix + builtins_fetchurl = + { url, sha256 ? null }@attrs: + let + inherit (builtins) lessThan nixVersion fetchurl; + in + if sha256 == null || lessThan nixVersion "1.12" then + fetchurl { inherit url; } + else + fetchurl attrs; + + # A wrapper around pkgs.fetchzip that has inspectable arguments, + # annoyingly this means we have to specify them + fetchzip = { url, sha256 ? null }@attrs: if sha256 == null + then builtins.fetchTarball { inherit url; } + else pkgs.fetchzip attrs; + + # A wrapper around pkgs.fetchurl that has inspectable arguments, + # annoyingly this means we have to specify them + fetchurl = { url, sha256 }@attrs: pkgs.fetchurl attrs; + + hasNixpkgsPath = (builtins.tryEval ).success; + hasThisAsNixpkgsPath = + (builtins.tryEval ).success && == ./.; + + sources = builtins.fromJSON (builtins.readFile ./sources.json); + + mapAttrs = builtins.mapAttrs or + (f: set: with builtins; + listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))); + + # borrowed from nixpkgs + functionArgs = f: f.__functionArgs or (builtins.functionArgs f); + callFunctionWith = autoArgs: f: args: + let auto = builtins.intersectAttrs (functionArgs f) autoArgs; + in f (auto // args); + + getFetcher = spec: + let fetcherName = + if builtins.hasAttr "type" spec + then builtins.getAttr "type" spec + else "builtin-tarball"; + in builtins.getAttr fetcherName { + "tarball" = fetchzip; + "builtin-tarball" = builtins_fetchTarball; + "file" = fetchurl; + "builtin-url" = builtins_fetchurl; + }; +}; +# NOTE: spec must _not_ have an "outPath" attribute +mapAttrs (name: spec: + if builtins.hasAttr "outPath" spec + then abort + "The values in sources.json should not have an 'outPath' attribute" + else + let + host = if (name == "nixpkgs") then "custom_nixpkgs" else name; + tryFromPath = builtins.tryEval (builtins.findFile builtins.nixPath host); + defaultSpec = (if builtins.hasAttr "url" spec && builtins.hasAttr "sha256" spec + then spec // + { outPath = callFunctionWith spec (getFetcher spec) { }; } + else spec) // (if tryFromPath.success + then let path = tryFromPath.value; + in { + outPath = builtins.trace "using search host <${host}>" ( + if pkgs.lib.hasPrefix "/nix/store" (builtins.toString path) + then path else gitignoreSource path); + } + else {}); + in if builtins.hasAttr "rev" spec && builtins.hasAttr "url" spec then + defaultSpec // + { revOverride = rev: if (rev == null) then defaultSpec else + let + spec' = removeAttrs (spec // { + rev = rev; + url = builtins.replaceStrings [defaultSpec.rev] [rev] defaultSpec.url; + }) [ "sha256" ]; + in + spec' // + { outPath = callFunctionWith spec' (getFetcher spec') { }; }; + } + else defaultSpec + ) sources diff --git a/package.json b/package.json index d3d9370141..c31a04f962 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,37 @@ { "name": "daedalus", "productName": "Daedalus", - "version": "0.15.1", + "version": "1.0.0", "description": "Cryptocurrency Wallet", "main": "./dist/main/index.js", "scripts": { "build": "gulp build", + "check:all": "yarn prettier:check && yarn lint && yarn flow:test && yarn stylelint && yarn lockfile:check && yarn manage:translations", "start": "gulp start", "start:dev": "NODE_ENV=development gulp start", "dev": "IS_WATCH_MODE=true gulp dev", - "test": "NODE_ENV=test yarn build && yarn test:unit && yarn test:e2e", - "test:unit": "yarn cucumber --require 'features/tests/unit/**/*.js' --tags '@unit and not @skip and not @wip'", - "test:unit:watch": "nodemon --watch source --watch features --exec \"yarn test:unit --tags '@unit and @watch'\"", - "test:unit:unbound": "yarn cucumber --require 'features/tests/unit/**/*.js' --tags '@unbound and not @skip and not @wip'", - "test:e2e": "yarn cucumber --require 'features/tests/e2e/**/*.js' --tags '@e2e and not @skip and not @wip'", + "test": "NODE_ENV=test yarn build && yarn test:unit && yarn test:e2e:fail-fast", + "test:generate:report": "node tests/reporter.js", + "test:unit": "yarn cucumber:run --require 'tests/**/unit/**/*.js' --tags '@unit and not @skip and not @wip'", + "test:unit:rerun": "yarn cucumber:rerun --require 'tests/**/unit/**/*.js' --tags '@unit and not @skip and not @wip'", + "test:unit:watch": "nodemon --watch source --watch tests --exec \"yarn test:unit --tags '@unit and @watch'\"", + "test:unit:unbound": "yarn cucumber:run --require 'tests/**/unit/**/*.js' --tags '@unbound and not @skip and not @wip'", + "test:e2e": "yarn cucumber:run --require 'tests/setup-e2e.js' --require 'tests/**/e2e/**/*.js' --tags '@e2e and not @skip and not @wip'", + "test:e2e:fail-fast": "yarn cucumber:fail-fast --require 'tests/setup-e2e.js' --require 'tests/**/e2e/**/*.js' --tags '@e2e and not @skip and not @wip'", + "test:e2e:rerun": "yarn cucumber:rerun --require 'tests/setup-e2e.js' --require 'tests/**/e2e/**/*.js' --tags '@e2e and not @skip and not @wip'", + "test:e2e:rerun:fail-fast": "yarn cucumber:rerun --require 'tests/setup-e2e.js' --require 'tests/**/e2e/**/*.js' --tags '@e2e and not @skip and not @wip'", "test:e2e:watch": "gulp test:e2e:watch", "test:e2e:watch:once": "KEEP_APP_AFTER_TESTS=true yarn test:e2e --tags '@e2e and @watch'", - "cucumber": "cross-env NODE_ENV=test cucumber-js --require-module @babel/register -f node_modules/cucumber-pretty --format-options '{\"snippetInterface\": \"async-await\"}'", + "cucumber": "cross-env NODE_ENV=test cucumber-js --require 'tests/setup-common.js' --require-module @babel/register -f json:tests-report/report-data.json -f summary:tests-report/summary.log -f node_modules/cucumber-pretty:tests-report/results.log --format-options '{\"snippetInterface\": \"async-await\"}' -f node_modules/cucumber-pretty --format-options '{\"snippetInterface\": \"async-await\"}' -f rerun:tests/@rerun.txt", + "cucumber:run": "yarn cucumber tests", + "cucumber:fail-fast": "yarn cucumber tests --fail-fast", + "cucumber:rerun": "yarn cucumber tests-report/@rerun.txt", + "cucumber:rerun:fail-fast": "yarn cucumber tests-report/@rerun.txt --fail-fast", "debug": "gulp debug", "package": "gulp build && cross-env NODE_ENV=production node -r @babel/register -r @babel/polyfill scripts/package.js", "package:all": "yarn package --all", "cleanup": "mop -v", - "lint": "eslint --format=node_modules/eslint-formatter-pretty source features storybook *.js", + "lint": "eslint --format=node_modules/eslint-formatter-pretty source storybook utils *.js", "flow:test": "flow; test $? -eq 0 -o $? -eq 2", "prettier": "./node_modules/.bin/prettier \"**/*.*\"", "prettier:check": "yarn prettier --check", @@ -34,8 +44,23 @@ "themes:check:createTheme": "gulp build:themes && node -r esm ./dist/scripts/check.js", "themes:update": "gulp build:themes && node -r esm ./dist/scripts/update.js && yarn prettier --loglevel warn --write source/renderer/app/themes/daedalus/*.js", "clear:cache": "gulp clear:cache", - "nix:dev": "nix-shell --arg autoStartBackend true --arg allowFaultInjection true --arg systemStart", - "nix:staging": "nix-shell --arg autoStartBackend true --argstr cluster staging" + "nix:itn": "NETWORK=itn_rewards_v1 nix-shell --argstr nodeImplementation jormungandr --argstr cluster itn_rewards_v1", + "nix:itn_selfnode": "NETWORK=itn_selfnode nix-shell --argstr nodeImplementation jormungandr --argstr cluster itn_selfnode", + "nix:nightly": "NETWORK=nightly nix-shell --argstr nodeImplementation jormungandr --argstr cluster nightly", + "nix:qa": "NETWORK=qa nix-shell --argstr nodeImplementation jormungandr --argstr cluster qa", + "nix:mainnet": "NETWORK=mainnet nix-shell --argstr nodeImplementation cardano --argstr cluster mainnet", + "nix:flight": "NETWORK=mainnet nix-shell --argstr nodeImplementation cardano --argstr cluster mainnet_flight", + "nix:selfnode": "NETWORK=selfnode nix-shell --argstr nodeImplementation cardano --argstr cluster selfnode", + "nix:staging": "NETWORK=staging nix-shell --argstr nodeImplementation cardano --argstr cluster staging", + "nix:testnet": "NETWORK=testnet nix-shell --argstr nodeImplementation cardano --argstr cluster testnet", + "byron:wallet:importer": "node utils/api-importer/byron-wallet-importer.js", + "itn:byron:wallet:importer": "node utils/api-importer/itn-byron-wallet-importer.js", + "itn:shelley:wallet:importer": "node utils/api-importer/itn-shelley-wallet-importer.js", + "yoroi:wallet:importer": "node utils/api-importer/yoroi-wallet-importer.js", + "js-launcher": "node utils/js-launcher/index.js", + "create-news-verification-hashes": "node utils/create-news-verification-hashes/index.js", + "lockfile:check": "node utils/lockfile-checker/index.js --check", + "lockfile:fix": "node utils/lockfile-checker/index.js --fix" }, "bin": { "electron": "./node_modules/.bin/electron" @@ -43,174 +68,182 @@ "devDependencies": { "@babel/cli": "7.2.0", "@babel/core": "7.4.5", + "@babel/helper-create-regexp-features-plugin": "7.7.0", "@babel/plugin-proposal-class-properties": "7.2.1", "@babel/plugin-proposal-decorators": "7.2.0", "@babel/plugin-proposal-object-rest-spread": "7.2.0", "@babel/plugin-transform-runtime": "7.2.0", "@babel/polyfill": "7.0.0", "@babel/preset-env": "7.2.0", - "@babel/preset-flow": "7.0.0", + "@babel/preset-flow": "7.8.3", "@babel/preset-react": "7.0.0", "@babel/register": "7.0.0", - "@storybook/addon-actions": "5.1.9", - "@storybook/addon-knobs": "5.1.9", - "@storybook/addon-links": "5.1.9", - "@storybook/addon-notes": "5.1.9", - "@storybook/addons": "5.1.9", - "@storybook/react": "5.1.9", - "asar": "0.14.6", + "@storybook/addon-actions": "5.3.14", + "@storybook/addon-knobs": "5.3.14", + "@storybook/addon-links": "5.3.14", + "@storybook/addons": "5.3.14", + "@storybook/core": "5.3.14", + "@storybook/react": "5.3.14", + "asar": "2.1.0", "autodll-webpack-plugin": "0.4.2", - "babel-eslint": "10.0.1", + "axios": "0.19.2", + "babel-eslint": "10.1.0", "babel-loader": "8.0.4", "babel-plugin-react-intl": "3.0.1", - "bufferutil": "4.0.0", - "cache-loader": "2.0.1", + "bufferutil": "4.0.1", + "cache-loader": "4.1.0", "chai": "4.2.0", - "chalk": "2.4.2", - "concurrently": "4.1.0", - "cross-env": "5.2.0", + "chalk": "3.0.0", + "concurrently": "5.1.0", + "cross-env": "7.0.0", "css-loader": "2.0.1", - "cucumber": "5.0.3", - "cucumber-pretty": "1.5.0", - "del": "3.0.0", - "devtron": "1.4.0", - "electron-chromedriver": "4.0.0-beta.1", + "cucumber": "6.0.5", + "cucumber-pretty": "6.0.0", + "del": "5.1.0", + "electron-chromedriver": "8.0.0", "electron-connect": "0.6.3", "electron-devtools-installer": "2.2.4", - "electron-packager": "13.0.1", - "electron-rebuild": "1.8.2", - "eslint": "4.19.1", - "eslint-config-airbnb": "17.1.0", - "eslint-config-prettier": "4.1.0", - "eslint-formatter-pretty": "2.0.0", - "eslint-import-resolver-webpack": "0.10.1", - "eslint-loader": "2.1.1", - "eslint-plugin-flowtype": "3.2.0", - "eslint-plugin-flowtype-errors": "3.6.0", - "eslint-plugin-import": "2.18.0", - "eslint-plugin-jsx-a11y": "6.1.2", - "eslint-plugin-promise": "4.0.1", - "eslint-plugin-react": "7.11.1", + "electron-packager": "14.2.1", + "eslint": "6.8.0", + "eslint-config-airbnb": "18.0.1", + "eslint-config-prettier": "6.10.0", + "eslint-formatter-pretty": "3.0.1", + "eslint-plugin-flowtype": "4.6.0", + "eslint-plugin-import": "2.20.1", + "eslint-plugin-jsx-a11y": "6.2.3", + "eslint-plugin-promise": "4.2.1", + "eslint-plugin-react": "7.18.3", + "eslint-plugin-react-hooks": "1.7.0", "esm": "3.2.25", "faker": "4.1.0", "fast-sass-loader": "1.5.0", - "file-loader": "2.0.0", - "flow-bin": "0.98.1", + "file-loader": "4.2.0", + "flow-bin": "0.119.1", "gulp-flow-remove-types": "1.0.0", "gulp-shell": "0.6.5", "hash.js": "1.1.7", "html-loader": "0.5.5", - "husky": "1.3.1", - "json-loader": "0.5.7", - "markdown-loader": "4.0.0", - "mini-css-extract-plugin": "0.5.0", - "minimist": "1.2.0", + "husky": "4.2.3", + "markdown-loader": "5.1.0", + "mini-css-extract-plugin": "0.9.0", + "minimist": "1.2.3", "mobx-react-devtools": "6.0.3", - "node-libs-browser": "2.1.0", - "node-sass": "4.11.0", - "nodemon": "1.18.9", - "postcss": "7.0.7", - "postcss-modules": "1.4.1", - "postcss-modules-values": "2.0.0", - "prettier": "1.16.4", - "pretty-quick": "1.10.0", - "prettysize": "1.1.0", + "node-forge": "0.9.1", + "node-libs-browser": "2.2.1", + "node-sass": "4.13.1", + "nodemon": "2.0.2", + "npmlog": "4.1.2", + "postcss": "7.0.27", + "postcss-modules": "1.5.0", + "prettier": "1.19.1", + "pretty-quick": "2.0.1", + "prettysize": "2.0.0", "raw-loader": "1.0.0", "react-intl-translations-manager": "5.0.3", + "react-syntax-highlighter": "11.0.2", + "regenerator-runtime": "0.13.3", "resolve-url": "0.2.1", "sass-loader": "7.1.0", "spawn-sync": "2.0.0", - "spectron": "5.0.0", + "spectron": "10.0.1", "style-loader": "0.23.1", - "stylelint": "9.6.0", - "stylelint-order": "2.2.1", - "svg-inline-loader": "0.8.0", - "thread-loader": "2.1.2", + "stylelint": "13.2.0", + "stylelint-order": "4.0.0", + "svg-inline-loader": "0.8.2", + "thread-loader": "2.1.3", "transform-loader": "0.2.4", - "url-loader": "1.1.2", - "utf-8-validate": "5.0.1", - "webdriverio": "4.14.1", - "webpack": "4.27.1", + "url-loader": "2.0.1", + "utf-8-validate": "5.0.2", + "webdriverio": "5.18.7", + "webpack": "4.39.1", "webpack-cli": "3.1.2", "webpack-stream": "5.2.1", - "yamljs": "0.3.0" + "ws": "7.2.3", + "yamljs": "0.3.0", + "yarn-lockfile": "1.1.1" }, "dependencies": { - "aes-js": "3.1.0", + "aes-js": "3.1.2", "bignumber.js": "5.0.0", "bip39": "2.3.0", "blakejs": "1.1.0", "bs58": "4.0.1", - "check-disk-space": "1.5.0", - "chroma-js": "2.0.3", + "cardano-js": "0.3.0", + "cardano-launcher": "0.20200414.1", + "check-disk-space": "2.1.0", + "chroma-js": "2.1.0", "classnames": "2.2.6", - "electron": "3.0.14", - "electron-debug": "2.0.0", + "csv-stringify": "5.3.6", + "cucumber-html-reporter": "5.1.0", + "electron": "8.2.2", "electron-log-daedalus": "2.2.20", - "electron-store": "2.0.0", - "electron-unhandled": "2.1.0", + "electron-store": "5.1.1", "es6-error": "4.1.1", - "form-data": "2.3.1", - "graceful-fs": "4.1.15", + "form-data": "3.0.0", + "fs-extra": "8.1.0", + "graceful-fs": "4.2.3", "gulp": "4.0.0", - "humanize-duration": "3.16.0", - "lodash": "4.17.14", - "mime-types": "2.1.24", + "humanize-duration": "3.22.0", + "lodash": "4.17.15", + "lodash-es": "4.17.15", + "mime-types": "2.1.26", "mkdirp": "0.5.1", - "mobx": "5.6.0", + "mobx": "5.14.0", "mobx-react": "5.4.2", "mobx-react-form": "1.35.1", "mobx-react-router": "3.1.2", - "moment": "2.23.0", - "pbkdf2": "3.0.14", - "pdf.js-extract": "0.0.9", + "moment": "2.24.0", + "pbkdf2": "3.0.17", "pdfkit": "0.8.3", - "prop-types": "15.6.2", - "ps-list": "6.0.0", + "prop-types": "15.7.2", + "ps-list": "7.0.0", "qr-image": "3.2.0", - "qrcode.react": "0.8.0", - "react": "16.6.3", - "react-animate-height": "2.0.15", - "react-copy-to-clipboard": "5.0.1", + "qrcode.react": "1.0.0", + "react": "16.8.0", + "react-animate-height": "2.0.20", + "react-copy-to-clipboard": "5.0.2", "react-custom-scrollbars": "4.2.1", - "react-dom": "16.6.3", + "react-datetime": "2.16.3", + "react-dom": "16.8.0", "react-intl": "2.7.2", "react-lottie": "1.2.3", - "react-markdown": "3.1.0", - "react-number-format": "3.0.3", - "react-polymorph": "0.9.0-rc.15", + "react-markdown": "4.3.1", + "react-polymorph": "0.9.3-rc.1", "react-router": "3.2.1", - "react-svg-inline": "2.1.0", - "react-virtualized": "9.21.0", - "recharts": "1.5.0", - "retry": "0.12.0", + "react-svg-inline": "2.1.1", + "react-virtualized": "9.21.2", + "recharts": "1.8.5", + "rotating-file-stream": "1.4.6", "route-parser": "0.0.5", "rust-cardano-crypto": "0.2.0", - "safe-buffer": "5.1.1", - "semver": "6.3.0", - "source-map-support": "0.5.9", + "safe-buffer": "5.2.0", + "semver": "7.1.3", + "source-map-support": "0.5.16", "spectron-fake-dialog": "0.0.1", - "split-file": "2.1.0", - "unorm": "1.4.1", - "validator": "9.1.2", - "web3-utils": "1.0.0-beta.30" + "unorm": "1.6.0", + "validator": "12.2.0" }, "devEngines": { - "node": ">=8.9.4", - "yarn": "1.10.0" + "node": ">=12.13.0", + "yarn": "1.22.4" }, "husky": { "hooks": { "pre-commit": "pretty-quick --staged", - "pre-push": "yarn prettier:check && yarn lint && yarn flow:test && yarn stylelint && yarn manage:translations" + "pre-push": "yarn check:all" } }, "resolutions": { - "**/**/marked": "^0.6.1", + "**/**/marked": "^0.7.0", "**/**/minimatch": "^3.0.2", - "**/**/lodash.mergewith": "^4.6.2", "**/**/lodash.template": "^4.5.0", "**/**/lodash": "^4.17.14", - "**/**/unicode-properties": "1.1.0" + "**/**/unicode-properties": "1.1.0", + "**/**/handlebars": "4.5.3", + "**/**/minimist": "^1.2.2", + "**/**/acorn": "^6.4.1", + "@storybook/core/**/terser-webpack-plugin": "^2.1.2", + "browserslist": "4.8.3", + "webpack/**/terser-webpack-plugin": "1.4.1" } } diff --git a/release-build.nix b/release-build.nix index 317b8c81e3..3b489d6e93 100644 --- a/release-build.nix +++ b/release-build.nix @@ -8,5 +8,5 @@ let pkgs = (import ./. {}).pkgs; in pkgs.runCommand "signed-release" {} '' mkdir $out - cp -v ${mkLinux "staging"}/*bin $out/ + cp -v ${mkWindows "mainnet_flight"}/*exe $out/ '' diff --git a/release.nix b/release.nix index 82dce1423a..55335b7832 100644 --- a/release.nix +++ b/release.nix @@ -13,23 +13,28 @@ let }; suffix = if buildNum == null then "" else "-${toString buildNum}"; version = (builtins.fromJSON (builtins.readFile ./package.json)).version; - daedalusPkgsWithSystem = system: import ./. { target = system; }; - yaml2json = { - x86_64-linux = (daedalusPkgsWithSystem "x86_64-linux").yaml2json; - x86_64-darwin = (daedalusPkgsWithSystem "x86_64-darwin").yaml2json; - }; - daedalus-installer = { - x86_64-linux = (daedalusPkgsWithSystem "x86_64-linux").daedalus-installer; - x86_64-darwin = (daedalusPkgsWithSystem "x86_64-darwin").daedalus-installer; - }; + daedalusPkgsWithSystem = system: + let + table = { + x86_64-linux = import ./. { target = "x86_64-linux"; }; + x86_64-windows = import ./. { target = "x86_64-windows"; }; + x86_64-darwin = import ./. { target = "x86_64-darwin"; }; + }; + in + table.${system}; + mkPins = inputs: (daedalusPkgs {}).pkgs.runCommand "ifd-pins" {} '' + mkdir $out + cd $out + ${lib.concatMapStringsSep "\n" (input: "ln -sv ${input.value} ${input.key}") (lib.attrValues (lib.mapAttrs (key: value: { inherit key value; }) inputs))} + ''; makeJobs = cluster: with daedalusPkgs { inherit cluster; }; { daedalus.x86_64-linux = daedalus; - installer.x86_64-linux = wrappedBundle newBundle pkgs cluster daedalus-bridge.version; + installer.x86_64-linux = wrappedBundle newBundle pkgs cluster daedalus-bridge.wallet-version; installer.x86_64-windows = (import ./. { inherit cluster; target = "x86_64-windows"; }).windows-installer; }; wrappedBundle = newBundle: pkgs: cluster: cardanoVersion: let - backend = "cardano-sl-${cardanoVersion}"; + backend = "cardano-wallet-${cardanoVersion}"; fn = "daedalus-${version}-${backend}-${cluster}-${system}${suffix}.bin"; in pkgs.runCommand fn {} '' mkdir -pv $out/nix-support @@ -40,8 +45,26 @@ let ''; lib = (import ./. {}).pkgs.lib; clusters = lib.splitString " " (builtins.replaceStrings ["\n"] [""] (builtins.readFile ./installer-clusters.cfg)); + mapOverArches = supportedTree: lib.mapAttrsRecursive (path: value: lib.listToAttrs (map (arch: { name = arch; value = lib.attrByPath path null (daedalusPkgsWithSystem arch); }) value)) supportedTree; + sources = import ./nix/sources.nix; in { - inherit shellEnvs yaml2json daedalus-installer; + inherit shellEnvs; inherit ((daedalusPkgs {}).pkgs) mono; + wine = (daedalusPkgs {}).wine; + wine64 = (daedalusPkgs {}).wine64; tests = (daedalusPkgs {}).tests; -} // builtins.listToAttrs (map (x: { name = x; value = makeJobs x; }) clusters) + ifd-pins = mkPins { + inherit (sources) iohk-nix cardano-wallet cardano-shell; + }; + # below line blows up hydra with 300 GB derivations on every commit +} #// (builtins.listToAttrs (map (x: { name = x; value = makeJobs x; }) clusters)) +// (mapOverArches { + daedalus-installer = [ "x86_64-linux" "x86_64-darwin" ]; + yaml2json = [ "x86_64-linux" "x86_64-darwin" ]; + bridgeTable = { + jormungandr = [ "x86_64-linux" "x86_64-darwin" "x86_64-windows" ]; + cardano = [ "x86_64-linux" "x86_64-darwin" "x86_64-windows" ]; + }; + cardano-node = [ "x86_64-linux" "x86_64-darwin" "x86_64-windows" ]; + export-wallets = [ "x86_64-linux" "x86_64-darwin" "x86_64-windows" ]; +}) diff --git a/scripts/build-cross-windows.sh b/scripts/build-cross-windows.sh index ec0f92d869..1f9b0adcb5 100755 --- a/scripts/build-cross-windows.sh +++ b/scripts/build-cross-windows.sh @@ -16,9 +16,8 @@ CLUSTERS="$(xargs echo -n < "$(dirname "$0")/../installer-clusters.cfg")" for cluster in ${CLUSTERS}; do echo '~~~ Building '"${cluster}"' installer' - # to enable signing, re-add --arg signingKeys '{ spc = ./dummy-certs/authenticode.spc; pvk = ./dummy-certs/authenticode.pvk; }' - nix-build default.nix -A windows-installer --show-trace --allow-unsafe-native-code-during-evaluation --argstr cluster "$cluster" --argstr buildNum "$BUILDKITE_BUILD_NUMBER" --argstr target "x86_64-windows" + nix-build default.nix -A windows-installer --arg disabledsigningKeys '{ spc = ./dummy-certs/authenticode.spc; pvk = ./dummy-certs/authenticode.pvk; }' --show-trace --allow-unsafe-native-code-during-evaluation --argstr cluster "$cluster" --argstr buildNum "$BUILDKITE_BUILD_NUMBER" --argstr target "x86_64-windows" if [ -n "${BUILDKITE_JOB_ID:-}" ]; then - upload_artifacts_public result/daedalus-*-windows*.exe + upload_artifacts_public result/daedalus-*-*.exe fi done diff --git a/scripts/build-installer-nix.sh b/scripts/build-installer-nix.sh index ee1554a17c..d144c7923d 100755 --- a/scripts/build-installer-nix.sh +++ b/scripts/build-installer-nix.sh @@ -22,13 +22,11 @@ nix-build default.nix -A rawapp.deps -o node_modules.root -Q for cluster in ${CLUSTERS} do echo '~~~ Building '"${cluster}"' installer' - nix-build -Q release.nix -A "${cluster}.installer.x86_64-linux" --argstr buildNum "$BUILDKITE_BUILD_NUMBER" -o csl-daedalus + nix-build -Q default.nix -A "wrappedBundle" --argstr cluster "${cluster}" --argstr buildNum "$BUILDKITE_BUILD_NUMBER" -o csl-daedalus if [ -n "${BUILDKITE_JOB_ID:-}" ]; then upload_artifacts_public csl-daedalus/daedalus*.bin nix-build -A daedalus.cfg --argstr cluster "${cluster}" - for cf in launcher-config wallet-topology - do cp result/etc/$cf.yaml "$cf-${cluster}.linux.yaml" - upload_artifacts "$cf-${cluster}.linux.yaml" - done + cp result/etc/launcher-config.yaml "launcher-config-${cluster}.linux.yaml" + upload_artifacts "launcher-config-${cluster}.linux.yaml" fi done diff --git a/scripts/build-installer-unix.sh b/scripts/build-installer-unix.sh index cae873ebb7..9ea0b3078e 100755 --- a/scripts/build-installer-unix.sh +++ b/scripts/build-installer-unix.sh @@ -52,9 +52,10 @@ retry() { ### Argument processing ### fast_impure= -verbose=true +verbose= build_id=0 test_installer= +code_signing_config= signing_config= # Parallel build options for Buildkite agents only @@ -91,10 +92,15 @@ do case "$1" in shift; done set -e +echo "${verbose}" if test -n "${verbose}" then set -x fi +if [ -f /var/lib/buildkite-agent/code-signing-config.json ]; then + code_signing_config="--code-signing-config /var/lib/buildkite-agent/code-signing-config.json" +fi + if [ -f /var/lib/buildkite-agent/signing-config.json ]; then signing_config="--signing-config /var/lib/buildkite-agent/signing-config.json" fi @@ -118,14 +124,28 @@ upload_artifacts_public() { buildkite-agent artifact upload "$@" "${ARTIFACT_BUCKET:-}" --job "$BUILDKITE_JOB_ID" } +function checkItnCluster() { + for c in $2 + do + if [[ "${c}" == "${1}" ]] + then + echo 1 + fi + done +} + # Build/get cardano bridge which is used by make-installer -DAEDALUS_BRIDGE=$(nix-build --no-out-link -A daedalus-bridge) +echo '~~~ Prebuilding cardano bridge' +CARDANO_BRIDGE=$(nix-build --no-out-link -A daedalus-bridge --argstr nodeImplementation cardano) +echo '~~~ Prebuilding jormungandr bridge' +JORMUNGANDR_BRIDGE=$(nix-build --no-out-link -A daedalus-bridge --argstr nodeImplementation jormungandr) + +itnClusters="$(< "$(nix-build --no-out-link -A itnClustersFile)")" pushd installers echo '~~~ Prebuilding dependencies for cardano-installer, quietly..' $nix_shell ../default.nix -A daedalus-installer --run true || echo "Prebuild failed!" echo '~~~ Building the cardano installer generator..' - INSTALLER=$(nix-build -j 2 --no-out-link ../ -A daedalus-installer) for cluster in ${CLUSTERS} do @@ -134,25 +154,36 @@ pushd installers APP_NAME="csl-daedalus" rm -rf "${APP_NAME}" - INSTALLER_CMD=("$INSTALLER/bin/make-installer" + if [ "$(checkItnCluster "${cluster}" "${itnClusters}")" == "1" ]; then + echo "Cluster type: jormungandr" + BRIDGE_FLAG="--jormungandr ${JORMUNGANDR_BRIDGE}" + else + echo "Cluster type: cardano" + BRIDGE_FLAG="--cardano ${CARDANO_BRIDGE}" + fi + + INSTALLER_CMD=("make-installer" "${test_installer}" + "${code_signing_config}" "${signing_config}" - " --cardano ${DAEDALUS_BRIDGE}" + "${BRIDGE_FLAG}" " --build-job ${build_id}" " --cluster ${cluster}" " --out-dir ${APP_NAME}") + nix-build .. -A launcherConfigs.configFiles --argstr os macos64 --argstr cluster "${cluster}" -o cfg-files + cp -v cfg-files/* . + chmod -R +w . + echo 'Running make-installer in nix-shell' $nix_shell ../shell.nix -A buildShell --run "${INSTALLER_CMD[*]}" if [ -d ${APP_NAME} ]; then if [ -n "${BUILDKITE_JOB_ID:-}" ] then - echo "~~~ Uploading the installer package.." + echo "Uploading the installer package.." export PATH=${BUILDKITE_BIN_PATH:-}:$PATH upload_artifacts_public "${APP_NAME}/*" mv "launcher-config.yaml" "launcher-config-${cluster}.macos64.yaml" - mv "wallet-topology.yaml" "wallet-topology-${cluster}.macos64.yaml" upload_artifacts "launcher-config-${cluster}.macos64.yaml" - upload_artifacts "wallet-topology-${cluster}.macos64.yaml" rm -rf "${APP_NAME}" fi else diff --git a/scripts/package.js b/scripts/package.js index 4ce3fe1d2e..c9956287dd 100755 --- a/scripts/package.js +++ b/scripts/package.js @@ -27,7 +27,7 @@ const DEFAULT_OPTS = { ignore: [ /^\/.buildkite($|\/)/, /^\/.storybook($|\/)/, - /^\/features($|\/)/, + /^\/tests($|\/)/, /^\/flow($|\/)/, /^\/node_modules($|\/)/, /^\/scripts($|\/)/, diff --git a/shell.nix b/shell.nix index 01101541ec..f7e669f9da 100644 --- a/shell.nix +++ b/shell.nix @@ -1,10 +1,9 @@ -let - localLib = import ./lib.nix; -in { system ? builtins.currentSystem , config ? {} +, nodeImplementation ? "cardano" +, localLib ? import ./lib.nix { inherit nodeImplementation; } , pkgs ? localLib.iohkNix.getPkgs { inherit system config; } -, cluster ? "demo" +, cluster ? "selfnode" , systemStart ? null , autoStartBackend ? systemStart != null , walletExtraArgs ? [] @@ -13,28 +12,13 @@ in }: let - daedalusPkgs = import ./. { inherit cluster; target = system; }; + daedalusPkgs = import ./. { inherit nodeImplementation cluster; target = system; devShell = true; }; hostPkgs = import pkgs.path { config = {}; overlays = []; }; - yaml2json = pkgs.haskell.lib.disableCabalFlag pkgs.haskellPackages.yaml "no-exe"; - yarn = pkgs.yarn.override { inherit nodejs; }; - nodejs = pkgs.nodejs-8_x; - launcher-json = hostPkgs.runCommand "read-launcher-config.json" { buildInputs = [ yaml2json ]; } "yaml2json ${daedalusPkgs.daedalus.cfg}/etc/launcher-config.yaml > $out"; 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}'') - (pkgs.lib.optional (autoStartBackend == true) ''.frontendOnlyMode = true'') - ]; - 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; + launcherConfig' = "${daedalusPkgs.daedalus.cfg}/etc/launcher-config.yaml"; fixYarnLock = pkgs.stdenv.mkDerivation { name = "fix-yarn-lock"; - buildInputs = [ nodejs yarn pkgs.git ]; + buildInputs = [ daedalusPkgs.nodejs daedalusPkgs.yarn pkgs.git ]; shellHook = '' git diff > pre-yarn.diff yarn @@ -50,27 +34,14 @@ let exit ''; }; - demoTopology = { - wallet = { - fallbacks = 7; - valency = 1; - relays = [ - [ { addr = "127.0.0.1"; port = 3100; } ] - ]; - }; - }; - demoTopologyYaml = hostPkgs.runCommand "wallet-topology.yaml" { buildInputs = [ hostPkgs.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 - ''; # This has all the dependencies of daedalusShell, but no shellHook allowing hydra # to evaluate it. - daedalusShellBuildInputs = [ nodejs yarn ] ++ (with pkgs; [ + daedalusShellBuildInputs = [ + daedalusPkgs.nodejs + daedalusPkgs.yarn + daedalusPkgs.daedalus-bridge + daedalusPkgs.daedalus-installer + ] ++ (with pkgs; [ nix bash binutils coreutils curl gnutar git python27 curl jq nodePackages.node-gyp nodePackages.node-pre-gyp @@ -78,75 +49,56 @@ let chromedriver ] ++ (localLib.optionals autoStartBackend [ daedalusPkgs.daedalus-bridge - ]) ++ (localLib.optionals (pkgs.stdenv.hostPlatform.system != "x86_64-darwin") [ - daedalusPkgs.electron3 + ]) ++ (if (pkgs.stdenv.hostPlatform.system == "x86_64-darwin") then [ + darwin.apple_sdk.frameworks.CoreServices + ] else [ + daedalusPkgs.electron8 winePackages.minimal ]) + ) ++ (pkgs.lib.optionals (nodeImplementation == "cardano") [ + debug.node + ] ); buildShell = pkgs.stdenv.mkDerivation { name = "daedalus-build"; buildInputs = daedalusShellBuildInputs; }; + debug.node = pkgs.writeShellScriptBin "debug-node" (with daedalusPkgs.launcherConfigs.launcherConfig; '' + cardano-node run --topology ${nodeConfig.network.topologyFile} --config ${nodeConfig.network.configFile} --database-path ${stateDir}/chain --port 3001 --socket-path ${stateDir}/cardano-node.socket + ''); daedalusShell = pkgs.stdenv.mkDerivation (rec { buildInputs = daedalusShellBuildInputs; name = "daedalus"; buildCommand = "touch $out"; LAUNCHER_CONFIG = launcherConfig'; - DAEDALUS_CONFIG = if (cluster == "demo") then demoConfig else "${daedalusPkgs.daedalus.cfg}/etc/"; + DAEDALUS_CONFIG = "${daedalusPkgs.daedalus.cfg}/etc/"; DAEDALUS_INSTALL_DIRECTORY = "./"; DAEDALUS_DIR = DAEDALUS_INSTALL_DIRECTORY; CLUSTER = cluster; + NODE_EXE = if nodeImplementation == "jormungandr" then "cardano-wallet-jormungandr" else "cardano-wallet-http-bridge"; + CLI_EXE = if nodeImplementation == "jormungandr" then "jcli" else ""; + NODE_IMPLEMENTATION = nodeImplementation; 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 - STATE_PATH=$(eval echo $(jq ".statePath" < ${launcher-json})) - ${pkgs.lib.optionalString (cluster == "demo") '' - ln -svf ${demoTopologyYaml} wallet-topology.yaml - if [[ -f "''${STATE_PATH}/system-start" && "${systemStartString}" == $(cat "$''${STATE_PATH}/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 "''${STATE_PATH}" - mkdir -p "''${STATE_PATH}" - echo -n ${systemStartString} > "''${STATE_PATH}/system-start" - fi - ''} - mkdir -p "''${STATE_PATH}/${secretsDir}" + + cp -f ${daedalusPkgs.iconPath.small} $DAEDALUS_INSTALL_DIRECTORY/icon.png + + # These links will only occur to binaries that exist for the + # specific build config + ln -svf $(type -P jormungandr) + ln -svf $(type -P cardano-wallet-jormungandr) + ln -svf $(type -P jcli) + ${pkgs.lib.optionalString (nodeImplementation == "cardano") '' + source <(cardano-node --bash-completion-script `type -p cardano-node`) ''} - ${localLib.optionalString autoStartBackend '' - TLS_PATH=$(eval echo $(jq ".tlsPath" < ${launcher-json})) - mkdir -p "''${TLS_PATH}/server" "''${TLS_PATH}/client" - cardano-x509-certificates \ - --server-out-dir "''${TLS_PATH}/server" \ - --clients-out-dir "''${TLS_PATH}/client" \ - --configuration-file ${daedalusPkgs.daedalus.cfg}/etc/configuration.yaml \ - --configuration-key mainnet_dryrun_full - echo ''${TLS_PATH} - '' - } - export DAEDALUS_INSTALL_DIRECTORY - export NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -I${nodejs}/include/node" + + export NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -I${daedalusPkgs.nodejs}/include/node" ${localLib.optionalString purgeNpmCache '' warn "purging all NPM/Yarn caches" rm -rf node_modules @@ -156,28 +108,35 @@ let } yarn install ${pkgs.lib.optionalString (pkgs.stdenv.hostPlatform.system != "x86_64-darwin") '' - ln -svf ${daedalusPkgs.electron3}/bin/electron ./node_modules/electron/dist/electron + ln -svf ${daedalusPkgs.electron8}/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" - ''} + echo 'jq < $LAUNCHER_CONFIG' + echo debug the node by running debug-node ''; }); daedalus = daedalusShell.overrideAttrs (oldAttrs: { shellHook = '' - if [ ! -f "$CARDANO_TLS_PATH/ca.crt" ] || [ ! -f "tls/client/ca.crt" ] - then - echo "CARDANO_TLS_PATH must be set" - exit 1 - fi ${oldAttrs.shellHook} yarn dev exit 0 ''; }); -in daedalusShell // { inherit fixYarnLock buildShell; } + devops = pkgs.stdenv.mkDerivation { + name = "devops-shell"; + buildInputs = let + inherit (localLib.iohkNix) niv; + in [ niv ]; + shellHook = '' + echo "DevOps Tools" \ + | ${pkgs.figlet}/bin/figlet -f banner -c \ + | ${pkgs.lolcat}/bin/lolcat + + echo "NOTE: you may need to export GITHUB_TOKEN if you hit rate limits with niv" + echo "Commands: + * niv update - update package + + " + ''; + }; +in daedalusShell // { inherit fixYarnLock buildShell devops; } diff --git a/source/common/assets/pdf/NotoSans-Medium.ttf b/source/common/assets/pdf/NotoSans-Medium.ttf new file mode 100644 index 0000000000..5dbefd3727 Binary files /dev/null and b/source/common/assets/pdf/NotoSans-Medium.ttf differ diff --git a/source/common/assets/pdf/NotoSans-Regular.ttf b/source/common/assets/pdf/NotoSans-Regular.ttf new file mode 100644 index 0000000000..0a01a062f0 Binary files /dev/null and b/source/common/assets/pdf/NotoSans-Regular.ttf differ diff --git a/source/common/assets/pdf/NotoSansMono-Regular.ttf b/source/common/assets/pdf/NotoSansMono-Regular.ttf new file mode 100644 index 0000000000..d866336108 Binary files /dev/null and b/source/common/assets/pdf/NotoSansMono-Regular.ttf differ diff --git a/source/common/assets/pdf/arial-unicode.ttf b/source/common/assets/pdf/arial-unicode.ttf new file mode 100644 index 0000000000..de9b19d1bc Binary files /dev/null and b/source/common/assets/pdf/arial-unicode.ttf differ diff --git a/source/common/ipc/api.js b/source/common/ipc/api.js index f1ee4293dd..1e6ae3a9e4 100644 --- a/source/common/ipc/api.js +++ b/source/common/ipc/api.js @@ -5,6 +5,13 @@ import type { } from '../types/bug-report-request.types'; import type { GenerateFileMetaParams } from '../types/file-meta-request.types'; import type { GeneratePaperWalletParams } from '../types/paper-wallet-request.types'; +import type { + FileDialogRequestParams, + OpenFileDialogResponseParams, + SaveFileDialogResponseParams, +} from '../types/file-dialog.types'; +import type { GenerateAddressPDFParams } from '../types/address-pdf-request.types'; +import type { GenerateRewardsCsvParams } from '../types/rewards-csv-request.types'; import type { CardanoNodeState, CardanoStatus, @@ -14,7 +21,11 @@ import type { import type { CheckDiskSpaceResponse } from '../types/no-disk-space.types'; import type { LogFiles } from '../../renderer/app/types/LogTypes'; import type { GpuStatus } from '../../renderer/app/types/gpuStatus'; -import type { StateSnapshotLogParams } from '../types/logging.types'; +import type { ExportedByronWallet } from '../../renderer/app/types/walletExportTypes'; +import type { + StateSnapshotLogParams, + WalletMigrationReportData, +} from '../types/logging.types'; /** * ======================= IPC CHANNELS API ========================= @@ -74,6 +85,14 @@ export const GET_STATE_DIRECTORY_PATH_CHANNEL = 'GetStateDirectoryPathChannel'; export type GetStateDirectoryPathRendererRequest = string | any; export type GetStateDirectoryPathMainResponse = any; +/** + * Channel for checking the desktop directory path + */ +export const GET_DESKTOP_DIRECTORY_PATH_CHANNEL = + 'GetDesktopDirectoryPathChannel'; +export type GetDesktopDirectoryPathRendererRequest = string | any; +export type GetDesktopDirectoryPathMainResponse = any; + /** * Channel for setting log state snapshot */ @@ -120,14 +139,6 @@ export const REBUILD_APP_MENU_CHANNEL = 'REBUILD_APP_MENU_CHANNEL'; export type RebuildAppMenuRendererRequest = { isUpdateAvailable: boolean }; export type RebuildAppMenuMainResponse = void; -/** - * Channel to get the number of epochs consolidated - */ -export const GET_CONSOLIDATED_EPOCHS_COUNT_CHANNEL = - 'GET_CONSOLIDATED_EPOCHS_COUNT_CHANNEL'; -export type GetConsolidatedEpochsCountRendererRequest = void; -export type GetConsolidatedEpochsCountMainResponse = number; - /** * Channel to generate file blob */ @@ -142,6 +153,20 @@ export const GENERATE_PAPER_WALLET_CHANNEL = 'GENERATE_PAPER_WALLET_CHANNEL'; export type GeneratePaperWalletRendererRequest = GeneratePaperWalletParams; export type GeneratePaperWalletMainResponse = void; +/** + * Channel to generate and save a share address PDF + */ +export const GENERATE_ADDRESS_PDF_CHANNEL = 'GENERATE_ADDRESS_PDF_CHANNEL'; +export type GenerateAddressPDFRendererRequest = GenerateAddressPDFParams; +export type GenerateAddressPDFMainResponse = void; + +/** + * Channel to generate and save a rewards csv + */ +export const GENERATE_REWARDS_CSV_CHANNEL = 'GENERATE_REWARDS_CSV_CHANNEL'; +export type GenerateRewardsCsvRendererRequest = GenerateRewardsCsvParams; +export type GenerateRewardsCsvMainResponse = void; + /** * ====================== CARDANO IPC CHANNELS ====================== * This is the ipc-api contract between main & renderer process @@ -186,7 +211,7 @@ export type CardanoFaultInjectionRendererRequest = FaultInjectionIpcRequest; export type CardanoFaultInjectionMainResponse = void; /** - * Channel where renderer can ask for the last cached cardano-node status. + * Channel where renderer can ask for the last cached cardano-node status */ export const GET_CACHED_CARDANO_STATUS_CHANNEL = 'GET_CACHED_CARDANO_STATUS_CHANNEL'; @@ -194,7 +219,7 @@ export type GetCachedCardanoStatusRendererRequest = void; export type GetCachedCardanoStatusMainResponse = ?CardanoStatus; /** - * Channel where renderer and main process can exchange cardano-node status info. + * Channel where renderer and main process can exchange cardano-node status info */ export const SET_CACHED_CARDANO_STATUS_CHANNEL = 'SET_CACHED_CARDANO_STATUS_CHANNEL'; @@ -207,3 +232,48 @@ export type SetCachedCardanoStatusMainResponse = void; export const DETECT_SYSTEM_LOCALE_CHANNEL = 'DETECT_SYSTEM_LOCALE_CHANNEL'; export type DetectSystemLocaleRendererRequest = void; export type DetectSystemLocaleMainResponse = string; + +/** + * Channel where renderer can ask main process to export wallets + */ +export const EXPORT_WALLETS_CHANNEL = 'EXPORT_WALLETS_CHANNEL'; +export type ExportWalletsRendererRequest = { + exportSourcePath: string, + locale: string, +}; +export type ExportWalletsMainResponse = { + wallets: Array, + errors: string, +}; + +/** + * Channel for generating wallet migration report + */ +export const GENERATE_WALLET_MIGRATION_REPORT_CHANNEL = + 'GENERATE_WALLET_MIGRATION_REPORT_CHANNEL'; +export type GenerateWalletMigrationReportRendererRequest = WalletMigrationReportData; +export type GenerateWalletMigrationReportMainResponse = void; + +/** + * Channel for showing open dialog + */ +export const SHOW_OPEN_DIALOG_CHANNEL = 'SHOW_OPEN_DIALOG_CHANNEL'; +export type ShowOpenDialogRendererRequest = FileDialogRequestParams; +export type ShowOpenDialogMainResponse = OpenFileDialogResponseParams; + +/** + * Channel for showing save dialog + */ +export const SHOW_SAVE_DIALOG_CHANNEL = 'SHOW_SAVE_DIALOG_CHANNEL'; +export type ShowSaveDialogRendererRequest = FileDialogRequestParams; +export type ShowSaveDialogMainResponse = SaveFileDialogResponseParams; + +/** + * Channel for electron-store + */ +export const ELECTRON_STORE_CHANNEL = 'ELECTRON_STORE_CHANNEL'; +export type ElectronStoreMessage = { + type: 'get' | 'set' | 'delete', + key: string, + data?: any, +}; diff --git a/source/common/ipc/constants.js b/source/common/ipc/constants.js index 4fbae58484..f240d7178a 100644 --- a/source/common/ipc/constants.js +++ b/source/common/ipc/constants.js @@ -1,10 +1,14 @@ // @flow export const DIALOGS = { ABOUT: 'ABOUT_DIALOG', - BLOCK_CONSOLIDATION: 'BLOCK_CONSOLIDATION_DIALOG', DAEDALUS_DIAGNOSTICS: 'DAEDALUS_DIAGNOSTICS_DIALOG', }; export const NOTIFICATIONS = { DOWNLOAD_LOGS: 'DOWNLOAD_LOGS_NOTIFICATION', }; + +export const PAGES = { + SETTINGS: 'SETTINGS', + WALLET_SETTINGS: 'WALLET_SETTINGS', +}; diff --git a/source/common/ipc/lib/IpcConversation.js b/source/common/ipc/lib/IpcConversation.js new file mode 100644 index 0000000000..841010fdea --- /dev/null +++ b/source/common/ipc/lib/IpcConversation.js @@ -0,0 +1,113 @@ +// @flow +import { isString } from 'lodash'; +import uuid from 'uuid'; + +export type IpcSender = { + send: (channel: string, conversationId: string, ...args: Array) => void, +}; + +export type IpcEvent = { + sender: IpcSender, +}; + +export type IpcReceiver = { + on: ( + channel: string, + ( + event: IpcEvent, + conversationId: string, + ...args: Array + ) => Promise | void + ) => void, + removeListener: ( + channel: string, + listener: (...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 IpcConversation { + /** + * Each ipc channel should be a singleton (based on the channelName) + * Here we track the created instances. + */ + static _instances = {}; + /** + * The channel name + * @private + */ + _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 = IpcConversation._instances[channelName]; + if (existingChannel) { + throw new Error(`IPC channel "${channelName}" already exists.`); + } + IpcConversation._instances[channelName] = this; + this._channelName = channelName; + } + + /** + * 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. + */ + async request( + message: Outgoing, + sender: IpcSender, + receiver: IpcReceiver + ): Promise { + return new Promise((resolve, reject) => { + const conversationId = uuid(); + const handler = ( + event, + messageId: string, + isOk: boolean, + response: Incoming + ) => { + // Only handle messages with matching conversation id! + if (messageId !== conversationId) return; + // Simulate promise rejection over IPC (since it's not possible to throw over IPC) + if (isOk) { + resolve(response); + } else { + reject(response); + } + // Cleanup the lister once the request cycle is finished + receiver.removeListener(this._channelName, handler); + }; + receiver.on(this._channelName, handler); + sender.send(this._channelName, conversationId, message); + }); + } + + /** + * Sets up a permanent handler for receiving and responding to requests + * from the other side. + */ + onRequest( + handler: Incoming => Promise, + receiver: IpcReceiver + ): void { + receiver.on( + this._channelName, + async (event: IpcEvent, conversationId: string, message: Incoming) => { + try { + const response = await handler(message); + event.sender.send(this._channelName, conversationId, true, response); + } catch (error) { + event.sender.send(this._channelName, conversationId, false, error); + } + } + ); + } +} diff --git a/source/common/types/address-pdf-request.types.js b/source/common/types/address-pdf-request.types.js new file mode 100644 index 0000000000..1189fd7cbf --- /dev/null +++ b/source/common/types/address-pdf-request.types.js @@ -0,0 +1,14 @@ +// @flow +export type GenerateAddressPDFParams = { + title: string, + currentLocale: string, + creationDate: string, + address: string, + noteLabel: string, + note: string, + author: string, + filePath: string, + isMainnet: boolean, + networkLabel: string, + networkName: string, +}; diff --git a/source/common/types/bug-report-request.types.js b/source/common/types/bug-report-request.types.js index 637f0e5290..7c1142c2a5 100644 --- a/source/common/types/bug-report-request.types.js +++ b/source/common/types/bug-report-request.types.js @@ -1,11 +1,11 @@ // @flow export type BugReportRequestHttpOptions = { - hostname: ?string, + hostname?: string, method: string, path: string, - port: ?string, + port?: number, headers?: { - 'Content-Type': string, + 'Content-Type': mixed, }, }; diff --git a/source/common/types/cardano-node.types.js b/source/common/types/cardano-node.types.js index fc1535dad3..8e45336d5c 100644 --- a/source/common/types/cardano-node.types.js +++ b/source/common/types/cardano-node.types.js @@ -7,11 +7,14 @@ export type TlsConfig = { key: Uint8Array, }; +export type CardanoNodeImplementation = 'jormungandr' | 'cardano'; + export type NetworkNames = | 'mainnet' | 'staging' | 'testnet' | 'development' + | 'itn_rewards_v1' | string; export type PlatformNames = 'win32' | 'linux' | 'darwin' | string; @@ -21,32 +24,22 @@ export const NetworkNameOptions = { staging: 'staging', testnet: 'testnet', development: 'development', + itn_rewards_v1: 'itn_rewards_v1', }; export type CardanoNodeState = - | 'stopped' | 'starting' | 'running' + | 'exiting' | 'stopping' + | 'stopped' | '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, -} = { +export const CardanoNodeStates: EnumMap = { STARTING: 'starting', RUNNING: 'running', EXITING: 'exiting', @@ -64,26 +57,40 @@ export type CardanoPidOptions = | 'staging-PREVIOUS-CARDANO-PID' | 'testnet-PREVIOUS-CARDANO-PID' | 'development-PREVIOUS-CARDANO-PID' + | 'itn_rewards_v1-PREVIOUS-CARDANO-PID' | string; export type CardanoNodeStorageKeys = { PREVIOUS_CARDANO_PID: CardanoPidOptions, }; -export type CardanoNodeProcessNames = 'cardano-node' | 'cardano-node.exe'; +export type CardanoNodeProcessNames = + | 'cardano-node' + | 'cardano-node.exe' + | 'jormungandr' + | 'jormungandr.exe'; export type ProcessNames = { CARDANO_PROCESS_NAME: CardanoNodeProcessNames, }; export const CardanoProcessNameOptions: { - win32: CardanoNodeProcessNames, - linux: CardanoNodeProcessNames, - darwin: CardanoNodeProcessNames, + [CardanoNodeImplementation]: { + win32: CardanoNodeProcessNames, + linux: CardanoNodeProcessNames, + darwin: CardanoNodeProcessNames, + }, } = { - win32: 'cardano-node.exe', - linux: 'cardano-node', - darwin: 'cardano-node', + cardano: { + win32: 'cardano-node.exe', + linux: 'cardano-node', + darwin: 'cardano-node', + }, + jormungandr: { + win32: 'jormungandr.exe', + linux: 'jormungandr', + darwin: 'jormungandr', + }, }; /** @@ -113,9 +120,9 @@ export type FaultInjectionIpcRequest = [FaultInjection, boolean]; export type CardanoStatus = { isNodeResponding: boolean, - isNodeSubscribed: boolean, isNodeSyncing: boolean, isNodeInSync: boolean, hasBeenConnected: boolean, - cardanoNodeID: number, + cardanoNodePID: number, + cardanoWalletPID: number, }; diff --git a/source/common/types/environment.types.js b/source/common/types/environment.types.js index 85c0342726..7d23cc867d 100644 --- a/source/common/types/environment.types.js +++ b/source/common/types/environment.types.js @@ -1,7 +1,9 @@ // @flow export type Environment = { - network: string, + network: Network, + rawNetwork: string, apiVersion: string, + nodeVersion: string, mobxDevTools: boolean | string, current: string, isDev: boolean, @@ -10,11 +12,15 @@ export type Environment = { isMainnet: boolean, isStaging: boolean, isTestnet: boolean, + isSelfnode: boolean, + isIncentivizedTestnet: boolean, + isIncentivizedTestnetQA: boolean, + isIncentivizedTestnetNightly: boolean, + isIncentivizedTestnetSelfnode: boolean, isDevelopment: boolean, isWatchMode: boolean, build: string, buildNumber: string, - buildLabel: string, platform: string, platformVersion: string, mainProcessID: string, @@ -34,9 +40,20 @@ export type Environment = { export const PRODUCTION = 'production'; export const DEVELOPMENT = 'development'; export const TEST = 'test'; + +// cardano-node networks export const MAINNET = 'mainnet'; +export const MAINNET_FLIGHT = 'mainnet_flight'; +export const SELFNODE = 'selfnode'; export const STAGING = 'staging'; export const TESTNET = 'testnet'; + +// jormungandr networks +export const ITN_REWARDS_V1 = 'itn_rewards_v1'; +export const ITN_SELFNODE = 'itn_selfnode'; +export const QA = 'qa'; +export const NIGHTLY = 'nightly'; + export const MAC_OS = 'darwin'; export const WINDOWS = 'win32'; export const LINUX = 'linux'; @@ -45,3 +62,29 @@ export const OS_NAMES = { [WINDOWS]: 'Windows', [LINUX]: 'Linux', }; + +export type Network = + | 'mainnet' + | 'mainnet_flight' + | 'selfnode' + | 'staging' + | 'testnet' + | 'development' + | 'itn' + | 'itn_rewards_v1' + | 'itn_rewards_v1_selfnode' + | 'itn_rewards_v1_qa' + | 'itn_rewards_v1_nightly' + | 'itn_rewards' + | 'itn_rewards_selfnode' + | 'itn_rewards_qa' + | 'itn_rewards_nightly'; + +export const networkPrettyNames = { + mainnet: 'Mainnet', + selfnode: 'Selfnode', + staging: 'Staging', + testnet: 'Testnet', + development: 'Development', + itn_rewards_v1: 'Incentivized Testnet v1 - Rewards', +}; diff --git a/source/common/types/file-dialog.types.js b/source/common/types/file-dialog.types.js new file mode 100644 index 0000000000..6299d7fcf1 --- /dev/null +++ b/source/common/types/file-dialog.types.js @@ -0,0 +1,24 @@ +// @flow +export type FileDialogRequestParams = { + title?: string, + defaultPath?: string, + buttonLabel?: string, + filters?: Array, + properties?: Array, + message?: string, + nameFieldLabel?: string, + showsTagField?: boolean, + securityScopedBookmarks?: boolean, +}; + +export type OpenFileDialogResponseParams = { + canceled: boolean, + filePaths: string[], + bookmarks?: string[], +}; + +export type SaveFileDialogResponseParams = { + canceled: boolean, + filePath?: string, + bookmark?: string, +}; diff --git a/source/common/types/locales.types.js b/source/common/types/locales.types.js index 525a76cfe1..bfc97758e0 100644 --- a/source/common/types/locales.types.js +++ b/source/common/types/locales.types.js @@ -4,7 +4,12 @@ export type Locales = { japanese: string, }; -export const LOCALES: Locales = { +export const LOCALES = { english: 'en-US', japanese: 'ja-JP', }; + +export const humanizedDurationLanguages = { + 'en-US': 'en', + 'ja-JP': 'ja', +}; diff --git a/source/common/types/logging.types.js b/source/common/types/logging.types.js index 129e3d8803..52439724b4 100644 --- a/source/common/types/logging.types.js +++ b/source/common/types/logging.types.js @@ -2,6 +2,18 @@ import type { CardanoNodeState } from './cardano-node.types'; import type { SystemInfo } from '../../renderer/app/types/systemInfoTypes'; import type { CoreSystemInfo } from '../../renderer/app/types/coreSystemInfoTypes'; +import type { WalletImportStatus } from '../../renderer/app/types/walletExportTypes'; +import type { WalletMigrationStatus } from '../../renderer/app/stores/WalletMigrationStore'; +import LocalizableError from '../../renderer/app/i18n/LocalizableError'; + +export type LoggingLevel = 'debug' | 'info' | 'error' | 'warn'; + +export type Logger = { + debug: (string, ?Object) => void, + info: (string, ?Object) => void, + error: (string, ?Object) => void, + warn: (string, ?Object) => void, +}; export type FormatMessageContextParams = { appName: string, @@ -41,7 +53,8 @@ export type ElectronLoggerMessage = { }; export type LogSystemInfoParams = { - cardanoVersion: string, + cardanoNodeVersion: string, + cardanoWalletVersion: string, cpu: Array, daedalusVersion: string, isBlankScreenFixActive: boolean, @@ -59,22 +72,44 @@ export type StateSnapshotLogParams = { currentLocale: string, isConnected: boolean, isDev: boolean, - isForceCheckingNodeTime: boolean, isMainnet: boolean, isNodeInSync: boolean, isNodeResponding: boolean, - isNodeSubscribed: boolean, isNodeSyncing: boolean, - isNodeTimeCorrect: boolean, isStaging: boolean, isSynced: boolean, - isSystemTimeCorrect: boolean, - isSystemTimeIgnored: boolean, isTestnet: boolean, - latestLocalBlockTimestamp: number, - latestNetworkBlockTimestamp: number, - localBlockHeight: number, - localTimeDifference: ?number, - networkBlockHeight: number, currentTime: string, + syncPercentage: string, + localTip: ?Object, + networkTip: ?Object, +}; + +export type ExportedWalletData = { + id: string, + name: ?string, + hasPassword: boolean, + import?: { + status: WalletImportStatus, + error: ?LocalizableError, + }, +}; + +export type RestoredWalletData = { + id: string, + name: string, + hasPassword: boolean, +}; + +export type WalletMigrationReportData = { + exportedWalletsData: Array, + exportedWalletsCount: number, + exportErrors: string, + restoredWalletsData: Array, + restoredWalletsCount: number, + restorationErrors: Array<{ + error: LocalizableError, + wallet: ExportedWalletData, + }>, + finalMigrationStatus: WalletMigrationStatus, }; diff --git a/source/common/types/number.types.js b/source/common/types/number.types.js new file mode 100644 index 0000000000..07b7dba79b --- /dev/null +++ b/source/common/types/number.types.js @@ -0,0 +1,33 @@ +// @flow +export type NumberFormat = { + groupSeparator: '.' | ',' | ' ', + decimalSeparator: '.' | ',' | ' ', +}; + +type NumbersFormat = { + [key: string]: NumberFormat, +}; + +export const NUMBER_FORMATS: NumbersFormat = { + 'number-1': { + groupSeparator: ',', + decimalSeparator: '.', + }, + 'number-2': { + groupSeparator: '.', + decimalSeparator: ',', + }, + 'number-3': { + groupSeparator: ' ', + decimalSeparator: '.', + }, +}; + +export const DEFAULT_NUMBER_FORMAT: Object = { + decimalSeparator: '.', + groupSeparator: ',', + groupSize: 3, + secondaryGroupSize: 0, + fractionGroupSeparator: ' ', + fractionGroupSize: 0, +}; diff --git a/source/common/types/rewards-csv-request.types.js b/source/common/types/rewards-csv-request.types.js new file mode 100644 index 0000000000..2e824c0a15 --- /dev/null +++ b/source/common/types/rewards-csv-request.types.js @@ -0,0 +1,6 @@ +// @flow +export type CsvRecord = Array; +export type GenerateRewardsCsvParams = { + rewards: Array, + filePath: string, +}; diff --git a/source/common/utils/environmentCheckers.js b/source/common/utils/environmentCheckers.js new file mode 100644 index 0000000000..caeb041bd5 --- /dev/null +++ b/source/common/utils/environmentCheckers.js @@ -0,0 +1,71 @@ +// @flow +import { upperFirst } from 'lodash'; +import { + DEVELOPMENT, + LINUX, + MAC_OS, + MAINNET, + MAINNET_FLIGHT, + PRODUCTION, + SELFNODE, + STAGING, + TEST, + TESTNET, + WINDOWS, + ITN_REWARDS_V1, + QA, + NIGHTLY, + ITN_SELFNODE, + networkPrettyNames, +} from '../types/environment.types'; + +/* ================================================================== += Static checks and generators = +================================================================== */ + +export const evaluateNetwork = (network: ?string) => { + let currentNetwork = network || DEVELOPMENT; + if (network === QA || network === NIGHTLY || network === ITN_SELFNODE) { + currentNetwork = ITN_REWARDS_V1; + } + if (network === MAINNET_FLIGHT) { + currentNetwork = MAINNET; + } + return currentNetwork; +}; + +export const getBuildLabel = ( + build: string, + network: string, + currentNodeEnv: string, + isFlight: boolean, + version: string +) => { + const networkLabel = isFlight ? 'Flight' : networkPrettyNames[network]; + let buildLabel = `Daedalus ${networkLabel} (${version}#${build})`; + if (!checkIsProduction(currentNodeEnv)) + buildLabel += ` ${upperFirst(currentNodeEnv)}`; + return buildLabel; +}; + +export const checkIsDev = (currentNodeEnv: string) => + currentNodeEnv === DEVELOPMENT; +export const checkIsTest = (currentNodeEnv: string) => currentNodeEnv === TEST; +export const checkIsProduction = (currentNodeEnv: string) => + currentNodeEnv === PRODUCTION; +export const checkIsMainnet = (network: string) => network === MAINNET; +export const checkIsStaging = (network: string) => network === STAGING; +export const checkIsTestnet = (network: string) => network === TESTNET; +export const checkIsSelfnode = (network: string) => network === SELFNODE; +export const checkIsIncentivizedTestnet = (network: string) => + network === ITN_REWARDS_V1; +export const checkIsIncentivizedTestnetQA = (rawNetwork: string) => + rawNetwork === QA; +export const checkIsIncentivizedTestnetNightly = (rawNetwork: string) => + rawNetwork === NIGHTLY; +export const checkIsIncentivizedTestnetSelfnode = (rawNetwork: string) => + rawNetwork === ITN_SELFNODE; +export const checkIsDevelopment = (network: string) => network === DEVELOPMENT; +export const checkIsMacOS = (platform: string) => platform === MAC_OS; +export const checkIsWindows = (platform: string) => platform === WINDOWS; +export const checkIsLinux = (platform: string) => platform === LINUX; diff --git a/source/main/cardano/CardanoNode.js b/source/main/cardano/CardanoNode.js index 1ef0a280dd..9479a76b35 100644 --- a/source/main/cardano/CardanoNode.js +++ b/source/main/cardano/CardanoNode.js @@ -3,7 +3,10 @@ import Store from 'electron-store'; import { spawn, exec } from 'child_process'; import type { ChildProcess } from 'child_process'; import type { WriteStream } from 'fs'; -import { toInteger } from 'lodash'; +import type { Launcher } from 'cardano-launcher'; +import { get, toInteger } from 'lodash'; +import moment from 'moment'; +import rfs from 'rotating-file-stream'; import { environment } from '../environment'; import { deriveProcessNames, @@ -11,7 +14,9 @@ import { promisedCondition, } from './utils'; import { getProcess } from '../utils/processes'; +import { safeExitWithCode } from '../utils/safeExitWithCode'; import type { + CardanoNodeImplementation, CardanoNodeState, CardanoStatus, FaultInjection, @@ -20,15 +25,13 @@ import type { TlsConfig, } from '../../common/types/cardano-node.types'; import { CardanoNodeStates } from '../../common/types/cardano-node.types'; +import { CardanoWalletLauncher } from './CardanoWalletLauncher'; +import { launcherConfig } from '../config'; +import type { NodeConfig } from '../config'; +import type { Logger } from '../../common/types/logging.types'; /* eslint-disable consistent-return */ -type Logger = { - debug: (string, ?Object) => void, - info: (string, ?Object) => void, - error: (string, ?Object) => void, -}; - type Actions = { spawn: typeof spawn, exec: typeof exec, @@ -46,7 +49,7 @@ type StateTransitions = { onUpdating: () => void, onUpdated: () => void, onCrashed: (code: number, signal: string) => void, - onError: (error: Error) => void, + onError: (code: number, signal: string) => void, onUnrecoverable: () => void, }; @@ -56,28 +59,38 @@ type CardanoNodeIpcMessage = { FInjects?: FaultInjectionIpcResponse, }; -type NodeArgs = Array; - export type CardanoNodeConfig = { - nodePath: string, // Path to cardano-node executable + stateDir: string, // Path to the state directory + nodeImplementation: CardanoNodeImplementation, + nodeConfig: NodeConfig, 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 + cluster: string, + block0Path: string, + block0Hash: string, + secretPath: string, + configPath: string, + syncTolerance: string, + cliBin: string, // Path to cardano-cli executable }; 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); +const { nodeImplementation } = launcherConfig; // 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); +// derive Cardano process name based on current platform and node implementation +const { CARDANO_PROCESS_NAME } = deriveProcessNames( + platform, + nodeImplementation +); // create store for persisting CardanoNode and Daedalus PID's in fs const store = new Store(); @@ -87,11 +100,12 @@ export class CardanoNode { * @private */ _config: CardanoNodeConfig; + /** * The managed cardano-node child process * @private */ - _node: ?ChildProcess; + _node: ?Launcher; /** * The ipc channel used for broadcasting messages to the outside world @@ -106,16 +120,22 @@ export class CardanoNode { _transitionListeners: StateTransitions; /** - * Logger instance to print debug messages to + * logger instance to print debug messages to * @private */ _log: Logger; /** - * Log file stream for cardano-sl + * Log file stream for cardano-node / Jormungandr + * @private + */ + _cardanoNodeLogFile: WriteStream; + + /** + * Log file stream for cardano-wallet * @private */ - _cardanoLogFile: WriteStream; + _cardanoWalletLogFile: WriteStream; /** * The TLS config that is generated by the cardano-node @@ -147,6 +167,11 @@ export class CardanoNode { */ _startupTries: number = 0; + /** + * Flag which makes cardano node to exit Daedalus after stopping + */ + _exitOnStop: boolean = false; + /** * All faults that have been injected and confirmed by cardano-node. * These faults can be used during testing to trigger faulty behavior @@ -157,6 +182,14 @@ export class CardanoNode { */ _injectedFaults: Array = []; + /** + * Cardano Node config getter + * @returns {CardanoNodeImplementation} + */ + get config(): CardanoNodeConfig { + return this._config; + } + /** * Getter which copies and returns the internal tls config. * @returns {TlsConfig} @@ -170,7 +203,7 @@ export class CardanoNode { * @returns {TlsConfig} // I think this returns a number... */ get pid(): ?number { - return this._node ? this._node.pid : null; + return get(this, '_node.pid', null); } /** @@ -187,7 +220,8 @@ export class CardanoNode { */ get status(): ?CardanoStatus { return Object.assign({}, this._status, { - cardanoNodeID: this._node ? this._node.pid : 0, + cardanoNodePID: get(this, '_node.pid', 0), + cardanoWalletPID: get(this, '_node.wpid', 0), }); } @@ -226,67 +260,161 @@ export class CardanoNode { config: CardanoNodeConfig, isForced: boolean = false ): Promise => { + const { _log } = this; + // Guards const nodeCanBeStarted = await this._canBeStarted(); if (!nodeCanBeStarted) { + _log.error('CardanoNode#start: Cannot be started', { + startupTries: this._startupTries, + }); return Promise.reject(new Error('CardanoNode: Cannot be started')); } if (this._isUnrecoverable(config) && !isForced) { + _log.error('CardanoNode#start: Too many startup retries', { + startupTries: this._startupTries, + }); return Promise.reject(new Error('CardanoNode: Too many startup retries')); } + // Setup - const { _log } = this; - const { nodePath, nodeArgs, startupTimeout } = config; - const { createWriteStream } = this._actions; + const { + // startupTimeout, + nodeConfig, + stateDir, + cluster, + tlsPath, + block0Path, + block0Hash, + secretPath, + configPath, + syncTolerance, + cliBin, + } = config; + this._config = config; this._startupTries++; this._changeToState(CardanoNodeStates.STARTING); _log.info( - `CardanoNode#start: trying to start cardano-node for the ${ - this._startupTries - } time`, + `CardanoNode#start: trying to start cardano-node for the ${this._startupTries} time`, { startupTries: this._startupTries } ); - return new Promise((resolve, reject) => { - const logFile = createWriteStream(config.logFilePath, { flags: 'a' }); - logFile.on('open', async () => { - this._cardanoLogFile = logFile; - // Spawning cardano-node - _log.debug('CardanoNode path with args', { - path: nodePath, - args: nodeArgs, + return new Promise(async (resolve, reject) => { + const nodeLogFile = rfs( + time => { + // The module works by writing to the one file name before it is rotated out. + if (!time) return 'node.log'; + const timestamp = moment.utc().format('YYYYMMDDHHmmss'); + return `node.log-${timestamp}`; + }, + { + size: '5M', + path: config.logFilePath, + maxFiles: 4, + } + ); + this._cardanoNodeLogFile = nodeLogFile; + + const walletLogFile = rfs( + time => { + // The module works by writing to the one file name before it is rotated out. + if (!time) return 'cardano-wallet.log'; + const timestamp = moment.utc().format('YYYYMMDDHHmmss'); + return `cardano-wallet.log-${timestamp}`; + }, + { + size: '5M', + path: config.logFilePath, + maxFiles: 4, + } + ); + this._cardanoWalletLogFile = walletLogFile; + + try { + const node = await CardanoWalletLauncher({ + nodeImplementation, + nodeConfig, + cluster, + stateDir, + tlsPath, + block0Path, + block0Hash, + secretPath, + configPath, + syncTolerance, + nodeLogFile, + walletLogFile, + cliBin, }); - 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 - }`, - { pid: node.pid } - ); - resolve(); - } catch (_) { - reject( - new Error('CardanoNode#start: Error while spawning cardano-node') - ); - } - }); + + _log.info('Starting cardano-node now...'); + + // await promisedCondition(() => node.connected, startupTimeout); + + node + .start() + .then(api => { + const processes: { + wallet: ChildProcess, + node: ChildProcess, + } = { + wallet: node.walletService.getProcess(), + node: node.nodeService.getProcess(), + }; + + // Setup event handling + node.walletBackend.events.on('exit', exitStatus => { + _log.info('CardanoNode#exit', { exitStatus }); + const { code, signal } = exitStatus.wallet; + this._handleCardanoNodeExit(code, signal); + }); + + node.pid = processes.node.pid; + node.wpid = processes.wallet.pid; + node.connected = true; // TODO: use processes.wallet.connected here + _log.info( + `CardanoNode#start: cardano-node child process spawned with PID ${processes.node.pid}`, + { pid: processes.node.pid } + ); + _log.info( + `CardanoNode#start: cardano-wallet child process spawned with PID ${processes.wallet.pid}`, + { pid: processes.wallet.pid } + ); + this._handleCardanoNodeMessage({ + ReplyPort: api.requestParams.port, + }); + resolve(); + }) + .catch(exitStatus => { + _log.error('CardanoNode#start: Error while spawning cardano-node', { + exitStatus, + }); + const { code, signal } = exitStatus.wallet || {}; + this._handleCardanoNodeError(code, signal); + reject( + new Error('CardanoNode#start: Error while spawning cardano-node') + ); + }); + } catch (error) { + _log.error('CardanoNode#start: Unable to initialize cardano-launcher', { + error, + }); + const { code, signal } = error || {}; + this._handleCardanoNodeError(code, signal); + reject( + new Error('CardanoNode#start: Unable to initialize cardano-launcher') + ); + } }); }; /** - * Stops cardano-node, first by disconnecting and waiting up to `shutdownTimeout` + * Stops cardano-node, first by stopping and waiting up to `shutdownTimeout` * for the node to shutdown itself properly. If that doesn't work as expected the * node is killed. * @@ -298,10 +426,10 @@ export class CardanoNode { _log.info('CardanoNode#stop: process is not running anymore'); return Promise.resolve(); } - _log.info('CardanoNode#stop: disconnecting from cardano-node process'); + _log.info('CardanoNode#stop: stopping cardano-node process'); try { - if (_node) _node.disconnect(); this._changeToState(CardanoNodeStates.STOPPING); + if (_node) await _node.stop(_config.shutdownTimeout / 1000); await this._waitForNodeProcessToExit(_config.shutdownTimeout); await this._storeProcessStates(); this._reset(); @@ -359,7 +487,7 @@ export class CardanoNode { const { _log, _config } = this; try { // Stop cardano nicely if it is still awake - if (await this._isConnected()) { + if (this._isConnected()) { _log.info('CardanoNode#restart: stopping current node'); await this.stop(); } @@ -367,12 +495,21 @@ export class CardanoNode { isForced, }); await this._waitForCardanoToExitOrKillIt(); - await this.start(_config, isForced); + if (this._exitOnStop) { + _log.info('Daedalus:safeExit: exiting Daedalus with code 0', { + code: 0, + }); + safeExitWithCode(0); + } else { + await this.start(_config, isForced); + } } catch (error) { _log.error('CardanoNode#restart: Could not restart cardano-node', { error, }); - this._changeToState(CardanoNodeStates.ERRORED); + if (this._state !== CardanoNodeStates.UNRECOVERABLE) { + this._changeToState(CardanoNodeStates.ERRORED); + } return Promise.reject(error); } } @@ -438,22 +575,14 @@ export class CardanoNode { this._status = status; } - // ================================= 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 + * Signals the cardano-node to exit Daedalus on stop */ - _spawnNode(nodePath: string, args: NodeArgs, logFile: WriteStream) { - return this._actions.spawn(nodePath, args, { - stdio: ['inherit', logFile, logFile, 'ipc'], - }); - } + exitOnStop = () => { + this._exitOnStop = true; + }; + // ================================= PRIVATE =================================== /** * Handles node ipc messages sent by the cardano-node process. * Updates the tls config where possible and broadcasts it to @@ -486,13 +615,23 @@ export class CardanoNode { _handleCardanoReplyPortMessage = (port: number) => { const { _actions } = this; const { tlsPath } = this._config; - this._tlsConfig = { - ca: _actions.readFileSync(`${tlsPath}/client/ca.crt`), - key: _actions.readFileSync(`${tlsPath}/client/client.key`), - cert: _actions.readFileSync(`${tlsPath}/client/client.pem`), - hostname: 'localhost', - port, - }; + this._tlsConfig = + nodeImplementation === 'jormungandr' + ? { + ca: ('': any), + key: ('': any), + cert: ('': any), + hostname: 'localhost', + port, + } + : { + ca: _actions.readFileSync(`${tlsPath}/client/ca.crt`), + key: _actions.readFileSync(`${tlsPath}/client/client.key`), + cert: _actions.readFileSync(`${tlsPath}/client/client.pem`), + hostname: 'localhost', + port, + }; + if (this._state === CardanoNodeStates.STARTING) { this._changeToState(CardanoNodeStates.RUNNING); this.broadcastTlsConfig(); @@ -515,12 +654,16 @@ export class CardanoNode { this._injectedFaults = response; }; - _handleCardanoNodeError = async (error: Error) => { - const { _log } = this; - _log.error('CardanoNode: error', { error }); - this._changeToState(CardanoNodeStates.ERRORED); - this._transitionListeners.onError(error); - await this.restart(); + _handleCardanoNodeError = async (code: number, signal: string) => { + const { _log, _config } = this; + _log.error('CardanoNode: error', { code, signal }); + if (this._isUnrecoverable(_config)) { + this._changeToState(CardanoNodeStates.UNRECOVERABLE); + } else { + this._changeToState(CardanoNodeStates.ERRORED); + this._transitionListeners.onError(code, signal); + await this.restart(); + } }; _handleCardanoNodeExit = async (code: number, signal: string) => { @@ -535,9 +678,7 @@ export class CardanoNode { await this._waitForNodeProcessToExit(_config.shutdownTimeout); } catch (_) { _log.error( - `CardanoNode: sent exit code ${code} but was still running after ${ - _config.shutdownTimeout - }ms. Killing it now.`, + `CardanoNode: sent exit code ${code} but was still running after ${_config.shutdownTimeout}ms. Killing it now.`, { code, shutdownTimeout: _config.shutdownTimeout } ); try { @@ -564,12 +705,15 @@ export class CardanoNode { } else { this._changeToState(CardanoNodeStates.CRASHED, code, signal); } + await this._storeProcessStates(); this._reset(); }; _reset = () => { - if (this._cardanoLogFile) this._cardanoLogFile.end(); - if (this._node) this._node.removeAllListeners(); + if (this._cardanoNodeLogFile) this._cardanoNodeLogFile.end(); + if (this._cardanoWalletLogFile) this._cardanoWalletLogFile.end(); + // if (this._node) this._node.removeAllListeners(); + if (this._node) this._node = null; this._tlsConfig = null; }; diff --git a/source/main/cardano/CardanoWalletLauncher.js b/source/main/cardano/CardanoWalletLauncher.js new file mode 100644 index 0000000000..d275dc7691 --- /dev/null +++ b/source/main/cardano/CardanoWalletLauncher.js @@ -0,0 +1,219 @@ +// @flow +import { merge } from 'lodash'; +import path from 'path'; +import * as fs from 'fs-extra'; +import type { WriteStream } from 'fs'; +import * as cardanoLauncher from 'cardano-launcher'; +import type { Launcher } from 'cardano-launcher'; +import type { NodeConfig } from '../config'; +import { environment } from '../environment'; +import { STAKE_POOL_REGISTRY_URL } from '../config'; +import { + MAINNET, + SELFNODE, + TESTNET, + ITN_REWARDS_V1, + ITN_SELFNODE, + NIGHTLY, + QA, +} from '../../common/types/environment.types'; +import { createSelfnodeConfig } from './utils'; +import { logger } from '../utils/logging'; +import type { CardanoNodeImplementation } from '../../common/types/cardano-node.types'; + +export type WalletOpts = { + nodeImplementation: CardanoNodeImplementation, + nodeConfig: NodeConfig, + cluster: string, + stateDir: string, + tlsPath: string, + block0Path: string, + block0Hash: string, + secretPath: string, + configPath: string, + syncTolerance: string, + nodeLogFile: WriteStream, + walletLogFile: WriteStream, + cliBin: string, +}; + +export async function CardanoWalletLauncher(walletOpts: WalletOpts): Launcher { + const { + nodeImplementation, + nodeConfig, // For cardano-node / byron only! + cluster, + stateDir, + tlsPath, + block0Path, + block0Hash, + secretPath, + configPath, + syncTolerance, + nodeLogFile, + walletLogFile, + cliBin, + } = walletOpts; + // TODO: Update launcher config to pass number + const syncToleranceSeconds = parseInt(syncTolerance.replace('s', ''), 10); + + // Shared launcher config (node implementations agnostic) + const launcherConfig = { + networkName: cluster, + stateDir, + nodeConfig: { + kind: nodeImplementation, + configurationDir: '', + network: { + configFile: configPath, + }, + }, + syncToleranceSeconds, + childProcessLogWriteStreams: { + node: nodeLogFile, + wallet: walletLogFile, + }, + installSignalHandlers: false, + }; + + // TLS configuration used only for cardano-node + const tlsConfiguration = { + caCert: path.join(tlsPath, 'server/ca.crt'), + svCert: path.join(tlsPath, 'server/server.crt'), + svKey: path.join(tlsPath, 'server/server.key'), + }; + + // Prepare development TLS files + const { isProduction } = environment; + if (!isProduction && nodeImplementation === 'cardano') { + await fs.copy('tls', tlsPath); + } + + // This switch statement handles any node specifc + // configuration, prior to spawning the child process + logger.info('Node implementation', { nodeImplementation }); + switch (nodeImplementation) { + case 'cardano': + if (cluster === SELFNODE) { + const { configFile, genesisFile } = nodeConfig.network; + const { + configPath: selfnodeConfigPath, + genesisPath: selfnodeGenesisPath, + genesisHash: selfnodeGenesisHash, + } = await createSelfnodeConfig( + configFile, + genesisFile, + stateDir, + cliBin + ); + nodeConfig.network.configFile = selfnodeConfigPath; + nodeConfig.network.genesisFile = selfnodeGenesisPath; + nodeConfig.network.genesisHash = selfnodeGenesisHash; + merge(launcherConfig, { apiPort: 8088 }); + } else { + try { + const configFileDestPath = path.join(stateDir, 'config.yaml'); + const configFileSourcePath = nodeConfig.network.configFile; + if (configFileDestPath !== configFileSourcePath) { + logger.info(`Copying ${cluster} config file...`, { + configFileSourcePath, + configFileDestPath, + }); + await fs.copy(configFileSourcePath, configFileDestPath); + nodeConfig.network.configFile = configFileDestPath; + logger.info(`Copied ${cluster} config file`, { + configFileDestPath, + }); + } + } catch (error) { + logger.error(`Copying ${cluster} config file failed`, { error }); + } + try { + const genesisFileDestPath = path.join(stateDir, 'genesis.json'); + const genesisFileSourcePath = nodeConfig.network.genesisFile; + if (genesisFileDestPath !== genesisFileSourcePath) { + logger.info(`Copying ${cluster} genesis file...`, { + genesisFileSourcePath, + genesisFileDestPath, + }); + await fs.copy(genesisFileSourcePath, genesisFileDestPath); + nodeConfig.network.genesisFile = genesisFileDestPath; + logger.info(`Copied ${cluster} genesis file`, { + genesisFileDestPath, + }); + } + } catch (error) { + logger.error(`Copying ${cluster} genesis file failed`, { error }); + } + } + if (cluster !== MAINNET) { + // All clusters except for Mainnet are treated as "Testnets" + launcherConfig.networkName = TESTNET; + } + merge(launcherConfig, { nodeConfig, tlsConfiguration }); + break; + case 'jormungandr': + if (cluster === ITN_SELFNODE) { + merge(launcherConfig, { + apiPort: 8088, + networkName: SELFNODE, + nodeConfig: { + restPort: 8888, + network: { + genesisBlock: { + file: block0Path, + hash: block0Hash, + }, + secretFile: [secretPath], + }, + }, + stakePoolRegistryUrl: STAKE_POOL_REGISTRY_URL[ITN_SELFNODE], + }); + } + if (cluster === NIGHTLY) { + merge(launcherConfig, { + nodeConfig: { + network: { + genesisBlock: { + hash: block0Hash, + }, + }, + }, + stakePoolRegistryUrl: STAKE_POOL_REGISTRY_URL[NIGHTLY], + }); + } + if (cluster === QA) { + merge(launcherConfig, { + nodeConfig: { + network: { + genesisBlock: { + hash: block0Hash, + }, + }, + }, + stakePoolRegistryUrl: STAKE_POOL_REGISTRY_URL[QA], + }); + } + if (cluster === ITN_REWARDS_V1) { + merge(launcherConfig, { + nodeConfig: { + network: { + genesisBlock: { + file: block0Path, + hash: block0Hash, + }, + }, + }, + }); + } + break; + default: + break; + } + + logger.info('Setting up CardanoLauncher now...', { + walletOpts, + launcherConfig, + }); + + return new cardanoLauncher.Launcher(launcherConfig, logger); +} diff --git a/source/main/cardano/config.js b/source/main/cardano/config.js index 188a437c05..48caaa2736 100644 --- a/source/main/cardano/config.js +++ b/source/main/cardano/config.js @@ -1,37 +1,6 @@ // @flow -import type { LauncherConfig } from '../config'; - -const isDev = process.env.NODE_ENV === 'development'; - 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.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); - } - if (isDev) args.push('--wallet-doc-address', '127.0.0.1:8091'); - return args; -}; diff --git a/source/main/cardano/setup.js b/source/main/cardano/setup.js index d6f397c7c9..569c9ad271 100644 --- a/source/main/cardano/setup.js +++ b/source/main/cardano/setup.js @@ -1,10 +1,84 @@ // @flow import { BrowserWindow } from 'electron'; +import { createWriteStream, readFileSync } from 'fs'; +import { exec, spawn } from 'child_process'; import { CardanoNode } from './CardanoNode'; -import { frontendOnlyMode } from '../config'; +import { exportWallets } from './utils'; +import { + NODE_KILL_TIMEOUT, + NODE_SHUTDOWN_TIMEOUT, + NODE_STARTUP_MAX_RETRIES, + NODE_STARTUP_TIMEOUT, + NODE_UPDATE_TIMEOUT, +} from '../config'; +import { logger } from '../utils/logging'; import type { LauncherConfig } from '../config'; -import { setupFrontendOnlyMode } from './setupFrontendOnlyMode'; -import { setupCardanoNodeMode } from './setupCardanoNodeMode'; +import type { + CardanoNodeState, + CardanoStatus, + TlsConfig, +} from '../../common/types/cardano-node.types'; +import type { ExportWalletsRendererRequest } from '../../common/ipc/api'; +import { + cardanoAwaitUpdateChannel, + cardanoFaultInjectionChannel, + cardanoRestartChannel, + cardanoStateChangeChannel, + getCachedCardanoStatusChannel, + cardanoTlsConfigChannel, + setCachedCardanoStatusChannel, + exportWalletsChannel, +} from '../ipc/cardano.ipc'; +import { safeExitWithCode } from '../utils/safeExitWithCode'; + +const startCardanoNode = ( + node: CardanoNode, + launcherConfig: LauncherConfig +) => { + const { + logsPrefix, + nodeImplementation, + nodeConfig, + tlsPath, + stateDir, + cluster, + block0Path, + block0Hash, + secretPath, + configPath, + syncTolerance, + cliBin, + } = launcherConfig; + const logFilePath = `${logsPrefix}/pub/`; + const config = { + logFilePath, + nodeImplementation, + nodeConfig, + tlsPath, + stateDir, + cluster, + block0Path, + block0Hash, + secretPath, + configPath, + syncTolerance, + cliBin, + startupTimeout: NODE_STARTUP_TIMEOUT, + startupMaxRetries: NODE_STARTUP_MAX_RETRIES, + shutdownTimeout: NODE_SHUTDOWN_TIMEOUT, + killTimeout: NODE_KILL_TIMEOUT, + updateTimeout: NODE_UPDATE_TIMEOUT, + }; + return node.start(config); +}; + +const restartCardanoNode = async (node: CardanoNode) => { + try { + await node.restart(); + } catch (error) { + logger.error('Could not restart CardanoNode', { error }); + } +}; /** * Configures, starts and manages the CardanoNode responding to node @@ -13,12 +87,113 @@ import { setupCardanoNodeMode } from './setupCardanoNodeMode'; * @param launcherConfig {LauncherConfig} * @param mainWindow */ -export const setupCardano = ( +export const setupCardanoNode = ( launcherConfig: LauncherConfig, mainWindow: BrowserWindow -): ?CardanoNode => { - if (frontendOnlyMode) { - return setupFrontendOnlyMode(mainWindow); - } - return setupCardanoNodeMode(launcherConfig, mainWindow); +): 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 : 1000; + logger.info( + `CardanoNode crashed with code ${code}. Restarting in ${restartTimeout}ms...`, + { code, restartTimeout } + ); + setTimeout(() => restartCardanoNode(cardanoNode), restartTimeout); + }, + onError: () => {}, + onUnrecoverable: () => {}, + } + ); + + startCardanoNode(cardanoNode, launcherConfig); + + getCachedCardanoStatusChannel.onRequest(() => { + logger.info('ipcMain: Received request from renderer for cardano status', { + status: cardanoNode.status, + }); + return Promise.resolve(cardanoNode.status); + }); + + setCachedCardanoStatusChannel.onReceive((status: ?CardanoStatus) => { + logger.info( + 'ipcMain: Received request from renderer to cache cardano status', + { status } + ); + cardanoNode.saveStatus(status); + return Promise.resolve(); + }); + + cardanoStateChangeChannel.onRequest(() => { + logger.info('ipcMain: Received request from renderer for node state', { + state: cardanoNode.state, + }); + return Promise.resolve(cardanoNode.state); + }); + + cardanoTlsConfigChannel.onRequest(() => { + 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 + }); + + cardanoFaultInjectionChannel.onReceive(fault => { + logger.info( + 'ipcMain: Received request to inject a fault into cardano node', + { fault } + ); + return cardanoNode.setFault(fault); + }); + + exportWalletsChannel.onRequest( + ({ exportSourcePath, locale }: ExportWalletsRendererRequest) => { + logger.info('ipcMain: Received request from renderer to export wallets', { + exportSourcePath, + }); + return Promise.resolve( + exportWallets(exportSourcePath, launcherConfig, mainWindow, locale) + ); + } + ); + + return cardanoNode; }; diff --git a/source/main/cardano/setupCardanoNodeMode.js b/source/main/cardano/setupCardanoNodeMode.js deleted file mode 100644 index 0c4cc89dc2..0000000000 --- a/source/main/cardano/setupCardanoNodeMode.js +++ /dev/null @@ -1,156 +0,0 @@ -// @flow -import { createWriteStream, readFileSync } from 'fs'; -import { exec, spawn } from 'child_process'; -import { BrowserWindow } from 'electron'; -import { CardanoNode } from './CardanoNode'; -import { prepareArgs } from './config'; -import { - NODE_KILL_TIMEOUT, - NODE_SHUTDOWN_TIMEOUT, - NODE_STARTUP_MAX_RETRIES, - NODE_STARTUP_TIMEOUT, - NODE_UPDATE_TIMEOUT, -} from '../config'; -import { Logger } from '../utils/logging'; -import type { LauncherConfig } from '../config'; -import type { - CardanoNodeState, - CardanoStatus, - TlsConfig, -} from '../../common/types/cardano-node.types'; -import { - cardanoAwaitUpdateChannel, - cardanoFaultInjectionChannel, - cardanoRestartChannel, - cardanoStateChangeChannel, - getCachedCardanoStatusChannel, - cardanoTlsConfigChannel, - setCachedCardanoStatusChannel, -} from '../ipc/cardano.ipc'; -import { safeExitWithCode } from '../utils/safeExitWithCode'; - -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: NODE_STARTUP_TIMEOUT, - startupMaxRetries: NODE_STARTUP_MAX_RETRIES, - shutdownTimeout: NODE_SHUTDOWN_TIMEOUT, - killTimeout: NODE_KILL_TIMEOUT, - updateTimeout: NODE_UPDATE_TIMEOUT, - }; - return node.start(config); -}; - -const restartCardanoNode = async (node: CardanoNode) => { - try { - await node.restart(); - } catch (error) { - Logger.error('Could not restart CardanoNode', { error }); - } -}; - -export const setupCardanoNodeMode = ( - launcherConfig: LauncherConfig, - mainWindow: BrowserWindow -) => { - 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 …`, - { code, restartTimeout } - ); - setTimeout(() => restartCardanoNode(cardanoNode), restartTimeout); - }, - onError: () => {}, - onUnrecoverable: () => {}, - } - ); - - startCardanoNode(cardanoNode, launcherConfig); - - getCachedCardanoStatusChannel.onRequest(() => { - Logger.info('ipcMain: Received request from renderer for cardano status', { - status: cardanoNode.status, - }); - return Promise.resolve(cardanoNode.status); - }); - - setCachedCardanoStatusChannel.onReceive((status: ?CardanoStatus) => { - Logger.info( - 'ipcMain: Received request from renderer to cache cardano status', - { status } - ); - cardanoNode.saveStatus(status); - return Promise.resolve(); - }); - - cardanoStateChangeChannel.onRequest(() => { - Logger.info('ipcMain: Received request from renderer for node state', { - state: cardanoNode.state, - }); - return Promise.resolve(cardanoNode.state); - }); - - cardanoTlsConfigChannel.onRequest(() => { - 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 - }); - - cardanoFaultInjectionChannel.onReceive(fault => { - Logger.info( - 'ipcMain: Received request to inject a fault into cardano node', - { fault } - ); - return cardanoNode.setFault(fault); - }); - - return cardanoNode; -}; diff --git a/source/main/cardano/setupFrontendOnlyMode.js b/source/main/cardano/setupFrontendOnlyMode.js deleted file mode 100644 index 0cdb6d2eee..0000000000 --- a/source/main/cardano/setupFrontendOnlyMode.js +++ /dev/null @@ -1,87 +0,0 @@ -// @flow -import { readFileSync } from 'fs'; -import { BrowserWindow } from 'electron'; -import { - cardanoAwaitUpdateChannel, - cardanoFaultInjectionChannel, - cardanoRestartChannel, - cardanoStateChangeChannel, - getCachedCardanoStatusChannel, - cardanoTlsConfigChannel, - setCachedCardanoStatusChannel, -} from '../ipc/cardano.ipc'; -import { Logger } from '../utils/logging'; -import { CardanoNodeStates } from '../../common/types/cardano-node.types'; -import type { CardanoStatus } from '../../common/types/cardano-node.types'; -import { safeExitWithCode } from '../utils/safeExitWithCode'; - -export const setupFrontendOnlyMode = (mainWindow: BrowserWindow) => { - const { CARDANO_TLS_PATH, CARDANO_HOST, CARDANO_PORT } = process.env; - - if (!CARDANO_TLS_PATH) { - throw new Error('CARDANO_TLS_PATH must be set in frontendOnlyMode'); - } - - const cardanoTlsPath = CARDANO_TLS_PATH; - const cardanoHost = CARDANO_HOST || 'localhost'; - const cardanoPort = parseInt(CARDANO_PORT, 10) || 8090; - const tlsConfig = { - ca: readFileSync(`${cardanoTlsPath}/client/ca.crt`), - key: readFileSync(`${cardanoTlsPath}/client/client.key`), - cert: readFileSync(`${cardanoTlsPath}/client/client.pem`), - hostname: cardanoHost, - port: cardanoPort, - }; - - getCachedCardanoStatusChannel.onRequest(() => { - Logger.info('ipcMain: Received request from renderer for cardano status'); - return Promise.resolve(null); - }); - - setCachedCardanoStatusChannel.onReceive((status: ?CardanoStatus) => { - Logger.info( - 'ipcMain: Received request from renderer to cache cardano status', - { status } - ); - return Promise.resolve(); - }); - - cardanoStateChangeChannel.onRequest(() => { - Logger.info('ipcMain: Received request from renderer for node state', { - state: CardanoNodeStates.RUNNING, - }); - return Promise.resolve(CardanoNodeStates.RUNNING); - }); - - cardanoTlsConfigChannel.onRequest(() => { - Logger.info('ipcMain: Received request from renderer for tls config'); - return Promise.resolve(tlsConfig); - }); - - cardanoAwaitUpdateChannel.onReceive(() => { - Logger.info('ipcMain: Received request from renderer to await update'); - safeExitWithCode(20); - return Promise.resolve(); - }); - - cardanoRestartChannel.onReceive(() => { - Logger.info('ipcMain: Received request from renderer to restart node'); - cardanoStateChangeChannel.send(CardanoNodeStates.STARTING, mainWindow); - setTimeout(() => { - if (!mainWindow.isDestroyed()) { - cardanoStateChangeChannel.send(CardanoNodeStates.RUNNING, mainWindow); - } - }, 100); - return Promise.resolve(); - }); - - cardanoFaultInjectionChannel.onReceive(fault => { - Logger.info( - 'ipcMain: Received request to inject a fault into cardano node', - { fault } - ); - return Promise.reject(fault); - }); - - return null; -}; diff --git a/source/main/cardano/utils.js b/source/main/cardano/utils.js index f7cffc66bc..f4a4fa4163 100644 --- a/source/main/cardano/utils.js +++ b/source/main/cardano/utils.js @@ -1,6 +1,17 @@ // @flow +import * as fs from 'fs-extra'; +import path from 'path'; +import { BrowserWindow, dialog } from 'electron'; +import { spawnSync } from 'child_process'; +import { logger } from '../utils/logging'; +import { TESTNET_MAGIC } from '../config'; +import { getTranslation } from '../utils/getTranslation'; +import ensureDirectoryExists from '../utils/ensureDirectoryExists'; +import type { LauncherConfig } from '../config'; +import type { ExportWalletsMainResponse } from '../../common/ipc/api'; import type { CardanoNodeStorageKeys, + CardanoNodeImplementation, NetworkNames, PlatformNames, ProcessNames, @@ -66,6 +77,326 @@ export const deriveStorageKeys = ( PREVIOUS_CARDANO_PID: `${getNetworkName(network)}-PREVIOUS-CARDANO-PID`, }); -export const deriveProcessNames = (platform: PlatformNames): ProcessNames => ({ - CARDANO_PROCESS_NAME: CardanoProcessNameOptions[platform] || 'cardano-node', +export const deriveProcessNames = ( + platform: PlatformNames, + nodeImplementation: CardanoNodeImplementation +): ProcessNames => ({ + CARDANO_PROCESS_NAME: + CardanoProcessNameOptions[nodeImplementation][platform] || + (nodeImplementation === 'jormungandr' ? 'jormungandr' : 'cardano-node'), }); + +export const createSelfnodeConfig = async ( + configFilePath: string, + genesisFilePath: string, + stateDir: string, + cliBin: string +): Promise<{ + configPath: string, + genesisPath: string, + genesisHash: string, +}> => { + const genesisFileExists = await fs.pathExists(genesisFilePath); + if (!genesisFileExists) { + throw new Error('No genesis file found'); + } + + const genesisFileContent = await fs.readJson(genesisFilePath); + const startTime = Math.floor((Date.now() + 3000) / 1000); + const genesisFile = JSON.stringify({ + ...genesisFileContent, + startTime, + }); + const genesisPath = path.join(stateDir, 'genesis.json'); + + logger.info('Creating selfnode genesis file...', { + inputPath: genesisFilePath, + outputPath: genesisPath, + startTime, + }); + + await fs.remove(genesisPath); + await fs.writeFile(genesisPath, genesisFile); + + logger.info('Generating selfnode genesis hash...', { cliBin, genesisPath }); + const { stdout: genesisHashBuffer } = spawnSync(cliBin, [ + 'print-genesis-hash', + '--genesis-json', + genesisPath, + ]); + const genesisHash = genesisHashBuffer + .toString() + .replace('\r', '') + .replace('\n', ''); + logger.info('Generated selfnode genesis hash', { genesisHash }); + + const configFileExists = await fs.pathExists(configFilePath); + if (!configFileExists) { + throw new Error('No config file found'); + } + + const configFileContent = await fs.readFile(configFilePath); + const configFile = JSON.stringify({ + ...JSON.parse(configFileContent), + GenesisFile: genesisPath, + }); + const configPath = path.join(stateDir, 'config.yaml'); + + logger.info('Creating selfnode config file...', { + inputPath: configFilePath, + outputPath: configPath, + genesisPath, + genesisHash, + }); + + await fs.remove(configPath); + await fs.writeFile(configPath, configFile); + const chainDir = path.join(stateDir, 'chain'); + logger.info('Removing selfnode chain folder...', { + chainDir, + }); + await fs.remove(chainDir); + + const walletsDir = path.join(stateDir, 'wallets'); + logger.info('Removing selfnode wallets folder...', { + walletsDir, + }); + await fs.remove(walletsDir); + + return { configPath, genesisPath, genesisHash }; +}; + +export const exportWallets = async ( + exportSourcePath: string, + launcherConfig: LauncherConfig, + mainWindow: BrowserWindow, + locale: string +): Promise => { + const { + exportWalletsBin, + legacySecretKey, + legacyWalletDB, + stateDir, + cluster, + isFlight, + } = launcherConfig; + + logger.info('ipcMain: Starting wallets export...', { + exportSourcePath, + exportWalletsBin, + legacySecretKey, + legacyWalletDB, + stateDir, + cluster, + isFlight, + }); + + let legacySecretKeyPath; + let legacyWalletDBPath; + + if (exportSourcePath.endsWith('secret.key')) { + legacySecretKeyPath = exportSourcePath; + legacyWalletDBPath = path.join(exportSourcePath, '../..', legacyWalletDB); + } else { + legacySecretKeyPath = path.join(exportSourcePath, legacySecretKey); + legacyWalletDBPath = path.join(exportSourcePath, legacyWalletDB); + } + + // Prepare Daedalus migration data + try { + const response = await prepareMigrationData( + mainWindow, + stateDir, + legacySecretKeyPath, + legacyWalletDBPath, + locale + ); + legacySecretKeyPath = response.legacySecretKeyPath; + legacyWalletDBPath = response.legacyWalletDBPath; + } catch (error) { + const { code } = error || {}; + if (code === 'EBUSY') { + logger.info('ipcMain: Exporting wallets failed', { + errors: error, + }); + return Promise.resolve({ wallets: [], errors: error }); + } + } + + // Export tool flags + const exportWalletsBinFlags = []; + + // Cluster flags + if (cluster === 'testnet') { + exportWalletsBinFlags.push('--testnet', TESTNET_MAGIC); + } else { + exportWalletsBinFlags.push('--mainnet'); + } + + // Secret key flags + exportWalletsBinFlags.push('--keyfile', legacySecretKeyPath); + + // Wallet DB flags + const legacyWalletDBPathExists = await fs.pathExists( + `${legacyWalletDBPath}-acid` + ); + if (legacyWalletDBPathExists) { + exportWalletsBinFlags.push('--wallet-db-path', legacyWalletDBPath); + } + + logger.info('ipcMain: Exporting wallets...', { + exportWalletsBin, + exportWalletsBinFlags, + }); + + const { stdout, stderr } = spawnSync(exportWalletsBin, exportWalletsBinFlags); + const wallets = JSON.parse(stdout.toString() || '[]'); + const errors = stderr.toString(); + + logger.info(`ipcMain: Exported ${wallets.length} wallets`, { + walletsData: wallets.map(w => ({ + name: w.name, + id: w.id, + hasPassword: w.is_passphrase_empty, + })), + errors, + }); + + // Remove Daedalus migration data + await removeMigrationData(stateDir); + + return Promise.resolve({ wallets, errors }); +}; + +const prepareMigrationData = async ( + mainWindow: BrowserWindow, + stateDir: string, + legacySecretKey: string, + legacyWalletDB: string, + locale: string +): Promise<{ + legacySecretKeyPath: string, + legacyWalletDBPath: string, +}> => + new Promise(async (resolve, reject) => { + let legacySecretKeyPath = ''; + let legacyWalletDBPath = ''; + try { + // Remove migration data dir if it exists + const migrationDataDirPath = path.join(stateDir, 'migration-data'); + await fs.remove(migrationDataDirPath); + ensureDirectoryExists(migrationDataDirPath); + logger.info('ipcMain: Preparing Daedalus Flight migration data...', { + migrationDataDirPath, + }); + + const legacySecretKeyExists = await fs.pathExists(legacySecretKey); + if (legacySecretKeyExists) { + logger.info('ipcMain: Copying secret key file...', { + legacySecretKey, + }); + legacySecretKeyPath = path.join(stateDir, 'migration-data/secret.key'); + await fs.copy(legacySecretKey, legacySecretKeyPath); + logger.info('ipcMain: Copied secret key file', { + legacySecretKeyPath, + }); + } else { + logger.info('ipcMain: Secret key file not found', { + legacySecretKey, + }); + } + + const legacyWalletDBFullPath = `${legacyWalletDB}-acid`; + const legacyWalletDBPathExists = await fs.pathExists( + legacyWalletDBFullPath + ); + if (legacyWalletDBPathExists) { + logger.info('ipcMain: Copying wallet db directory...', { + legacyWalletDBFullPath, + }); + legacyWalletDBPath = path.join( + stateDir, + 'migration-data/wallet-db-acid' + ); + await fs.copy(legacyWalletDBFullPath, legacyWalletDBPath); + legacyWalletDBPath = legacyWalletDBPath.replace('-acid', ''); + logger.info('ipcMain: Copied wallet db directory', { + legacyWalletDBPath, + }); + } else { + logger.info('ipcMain: Wallet db directory not found', { + legacyWalletDBFullPath, + }); + } + resolve({ legacySecretKeyPath, legacyWalletDBPath }); + } catch (error) { + logger.info('ipcMain: Preparing Daedalus Flight migration data failed', { + error, + }); + const { code } = error || {}; + if (code === 'EBUSY') { + // "EBUSY" error happens on Windows when Daedalus mainnet is running during preparation + // of Daedalus Flight wallet migration data as this prevents the files from being copied. + logger.info('ipcMain: Showing "Automatic wallet migration" warning...'); + const { response } = await showExportWalletsWarning(mainWindow, locale); + if (response === 0) { + // User confirmed migration retry + logger.info('ipcMain: User confirmed wallet migration retry'); + resolve( + prepareMigrationData( + mainWindow, + stateDir, + legacySecretKey, + legacyWalletDB, + locale + ) + ); + } else { + // User canceled migration + logger.info('ipcMain: User canceled wallet migration'); + reject(error); + } + } else { + reject(error); + } + } + }); + +const removeMigrationData = async (stateDir: string) => { + try { + // Remove migration data dir if it exists + const migrationDataDirPath = path.join(stateDir, 'migration-data'); + logger.info('ipcMain: Removing Daedalus Flight migration data...', { + migrationDataDirPath, + }); + await fs.remove(migrationDataDirPath); + logger.info('ipcMain: Removed Daedalus Flight migration data', { + migrationDataDirPath, + }); + } catch (error) { + logger.info('ipcMain: Removing Daedalus Flight migration data failed', { + error, + }); + } +}; + +const showExportWalletsWarning = ( + mainWindow: BrowserWindow, + locale: string +): Promise<{ response: number }> => { + const translations = require(`../locales/${locale}`); + const translation = getTranslation(translations, 'dialog'); + const exportWalletsDialogOptions = { + buttons: [ + translation('exportWalletsWarning.confirm'), + translation('exportWalletsWarning.cancel'), + ], + type: 'warning', + title: translation('exportWalletsWarning.title'), + message: translation('exportWalletsWarning.message'), + defaultId: 0, + cancelId: 1, + noLink: true, + }; + return dialog.showMessageBox(mainWindow, exportWalletsDialogOptions); +}; diff --git a/source/main/config.js b/source/main/config.js index 37626736a8..86b19373b8 100644 --- a/source/main/config.js +++ b/source/main/config.js @@ -1,10 +1,20 @@ // @flow import path from 'path'; import { app, dialog } from 'electron'; -import { readLauncherConfig } from './utils/config'; import { environment } from './environment'; +import { readLauncherConfig } from './utils/config'; +import { getBuildLabel } from '../common/utils/environmentCheckers'; +import type { CardanoNodeImplementation } from '../common/types/cardano-node.types'; -const { isTest, isProduction, isBlankScreenFixActive } = environment; +const { + isTest, + isProduction, + isBlankScreenFixActive, + current, + build, + network, + version, +} = environment; // Make sure Daedalus is started with required configuration const { LAUNCHER_CONFIG } = process.env; @@ -30,26 +40,40 @@ if (!isStartedByLauncher) { } } +export type NodeConfig = { + configurationDir: string, + delegationCertificate?: string, + kind: 'byron', + network: { + configFile: string, + genesisFile: string, + genesisHash: string, + topologyFile: string, + }, + signingKey?: string, +}; + /** * The shape of the config params, usually provided to the cadano-node launcher */ export type LauncherConfig = { - frontendOnlyMode: boolean, - statePath: string, - nodePath: string, - nodeArgs: Array, + stateDir: string, + nodeImplementation: CardanoNodeImplementation, + nodeConfig: NodeConfig, tlsPath: string, - nodeDbPath: string, - workingDir: string, logsPrefix: string, - nodeLogConfig: string, - nodeTimeoutSec: number, - configuration: { - filePath: string, - key: string, - systemStart: string, - seed: string, - }, + cluster: string, + block0Path: string, + block0Hash: string, + secretPath: string, + configPath: string, + syncTolerance: string, + cliBin: string, + exportWalletsBin: string, + legacyStateDir: string, + legacySecretKey: string, + legacyWalletDB: string, + isFlight: boolean, }; type WindowOptionsType = { @@ -68,7 +92,7 @@ type WindowOptionsType = { export const WINDOW_WIDTH = 1150; export const WINDOW_HEIGHT = 870; export const MIN_WINDOW_CONTENT_WIDTH = 905; -export const MIN_WINDOW_CONTENT_HEIGHT = 600; +export const MIN_WINDOW_CONTENT_HEIGHT = 700; export const windowOptions: WindowOptionsType = { show: false, @@ -84,33 +108,44 @@ export const windowOptions: WindowOptionsType = { useContentSize: true, }; -export const APP_NAME = 'Daedalus'; export const launcherConfig: LauncherConfig = readLauncherConfig( LAUNCHER_CONFIG ); -export const appLogsFolderPath = launcherConfig.logsPrefix; +export const { + cluster, + nodeImplementation, + stateDir, + legacyStateDir, + logsPrefix, + isFlight, +} = launcherConfig; +export const appLogsFolderPath = logsPrefix; export const pubLogsFolderPath = path.join(appLogsFolderPath, 'pub'); -export const appFolderPath = launcherConfig.workingDir; -export const { nodeDbPath } = launcherConfig; -export const stateDirectoryPath = launcherConfig.statePath; +export const stateDirectoryPath = stateDir; export const stateDrive = isWindows ? stateDirectoryPath.slice(0, 2) : '/'; +export const buildLabel = getBuildLabel( + build, + network, + current, + isFlight, + version +); + +// Logging config export const ALLOWED_LOGS = [ 'Daedalus.json', 'System-info.json', 'Daedalus-versions.json', 'State-snapshot.json', + 'Wallet-migration-report.json', + 'cardano-wallet.log', + 'node.log', ]; -export const ALLOWED_NODE_LOGS = new RegExp(/(node.json-)(\d{14}$)/); +export const ALLOWED_NODE_LOGS = new RegExp(/(node.log-)(\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; -// We need to invert 'frontendOnlyMode' value received from the launcherConfig -// as this variable has an opposite meaning from the launcher's perspective. -// Launcher treats the 'frontendOnlyMode' set to 'true' as the case where Daedalus -// takes the responsiblity for launching and managing the cardano-node. -export const frontendOnlyMode = !launcherConfig.frontendOnlyMode; - // CardanoNode config export const NODE_STARTUP_TIMEOUT = 5000; export const NODE_STARTUP_MAX_RETRIES = 5; @@ -118,11 +153,22 @@ export const NODE_SHUTDOWN_TIMEOUT = isTest ? 5000 : 10000; export const NODE_KILL_TIMEOUT = isTest ? 5000 : 10000; export const NODE_UPDATE_TIMEOUT = isTest ? 10000 : 60000; -/* eslint-disable max-len */ export const DISK_SPACE_REQUIRED = 2 * 1073741274; // 2 GB | unit: bytes export const DISK_SPACE_REQUIRED_MARGIN_PERCENTAGE = 10; // 10% of the available disk space export const DISK_SPACE_CHECK_LONG_INTERVAL = 10 * 60 * 1000; // 10 minutes | unit: milliseconds export const DISK_SPACE_CHECK_MEDIUM_INTERVAL = 60 * 1000; // 1 minute | unit: milliseconds export const DISK_SPACE_CHECK_SHORT_INTERVAL = isTest ? 2000 : 10 * 1000; // 10 seconds | unit: milliseconds export const DISK_SPACE_RECOMMENDED_PERCENTAGE = 15; // 15% of the total disk space -/* eslint-disable max-len */ + +// CardanoWallet config +export const STAKE_POOL_REGISTRY_URL = { + itn_selfnode: + 'https://github.com/input-output-hk/daedalus/raw/selfnode/test-integration-registry.zip', + nightly: + 'https://github.com/piotr-iohk/incentivized-testnet-stakepool-registry/archive/master.zip', + qa: + 'https://explorer.qa.jormungandr-testnet.iohkdev.io/stakepool-registry/registry.zip', +}; + +// Cardano Byron Testnet network magic +export const TESTNET_MAGIC = '1097911063'; diff --git a/source/main/environment.js b/source/main/environment.js index 9bd959312b..c3e26ba108 100644 --- a/source/main/environment.js +++ b/source/main/environment.js @@ -1,33 +1,61 @@ // @flow import os from 'os'; -import { uniq, upperFirst, get, includes } from 'lodash'; +import { uniq, get, includes } from 'lodash'; import { version } from '../../package.json'; import type { Environment } from '../common/types/environment.types'; import { DEVELOPMENT, - LINUX, - MAC_OS, - MAINNET, OS_NAMES, - PRODUCTION, - STAGING, - TEST, - TESTNET, - WINDOWS, + MAINNET, + MAINNET_FLIGHT, } from '../common/types/environment.types'; +import { + evaluateNetwork, + checkIsDev, + checkIsTest, + checkIsProduction, + checkIsMainnet, + checkIsStaging, + checkIsTestnet, + checkIsSelfnode, + checkIsDevelopment, + checkIsIncentivizedTestnet, + checkIsIncentivizedTestnetQA, + checkIsIncentivizedTestnetNightly, + checkIsIncentivizedTestnetSelfnode, + checkIsMacOS, + checkIsWindows, + checkIsLinux, +} from '../common/utils/environmentCheckers'; + +/* ================================================================== += Evaluations = +================================================================== */ // environment variables const CURRENT_NODE_ENV = process.env.NODE_ENV || DEVELOPMENT; -const NETWORK = process.env.NETWORK || DEVELOPMENT; -const isDev = CURRENT_NODE_ENV === DEVELOPMENT; -const isTest = CURRENT_NODE_ENV === TEST; -const isProduction = CURRENT_NODE_ENV === PRODUCTION; -const isMainnet = NETWORK === MAINNET; -const isStaging = NETWORK === STAGING; -const isTestnet = NETWORK === TESTNET; -const isDevelopment = NETWORK === DEVELOPMENT; +const RAW_NETWORK = + process.env.NETWORK === MAINNET_FLIGHT ? MAINNET : process.env.NETWORK || ''; +const NETWORK = evaluateNetwork(process.env.NETWORK); +const isDev = checkIsDev(CURRENT_NODE_ENV); +const isTest = checkIsTest(CURRENT_NODE_ENV); +const isProduction = checkIsProduction(CURRENT_NODE_ENV); +const isMainnet = checkIsMainnet(NETWORK); +const isStaging = checkIsStaging(NETWORK); +const isTestnet = checkIsTestnet(NETWORK); +const isSelfnode = checkIsSelfnode(NETWORK); +const isIncentivizedTestnet = checkIsIncentivizedTestnet(NETWORK); +const isIncentivizedTestnetQA = checkIsIncentivizedTestnetQA(RAW_NETWORK); +const isIncentivizedTestnetNightly = checkIsIncentivizedTestnetNightly( + RAW_NETWORK +); +const isIncentivizedTestnetSelfnode = checkIsIncentivizedTestnetSelfnode( + RAW_NETWORK +); +const isDevelopment = checkIsDevelopment(NETWORK); const isWatchMode = process.env.IS_WATCH_MODE; const API_VERSION = process.env.API_VERSION || 'dev'; +const NODE_VERSION = '1.10.1'; // TODO: pick up this value from process.env const mainProcessID = get(process, 'ppid', '-'); const rendererProcessID = process.pid; const PLATFORM = os.platform(); @@ -38,24 +66,23 @@ const ram = os.totalmem(); const isBlankScreenFixActive = includes(process.argv.slice(1), '--safe-mode'); const BUILD = process.env.BUILD_NUMBER || 'dev'; const BUILD_NUMBER = uniq([API_VERSION, BUILD]).join('.'); -const BUILD_LABEL = (() => { - const networkLabel = !(isMainnet || isDev) ? ` ${upperFirst(NETWORK)}` : ''; - let buildLabel = `Daedalus${networkLabel} (${version}#${BUILD_NUMBER})`; - if (!isProduction) buildLabel += ` ${CURRENT_NODE_ENV}`; - return buildLabel; -})(); const INSTALLER_VERSION = uniq([API_VERSION, BUILD]).join('.'); const MOBX_DEV_TOOLS = process.env.MOBX_DEV_TOOLS || false; -const isMacOS = PLATFORM === MAC_OS; -const isWindows = PLATFORM === WINDOWS; -const isLinux = PLATFORM === LINUX; +const isMacOS = checkIsMacOS(PLATFORM); +const isWindows = checkIsWindows(PLATFORM); +const isLinux = checkIsLinux(PLATFORM); + +/* ================================================================== += Compose environment = +================================================================== */ -// compose environment export const environment: Environment = Object.assign( {}, { network: NETWORK, + rawNetwork: RAW_NETWORK, apiVersion: API_VERSION, + nodeVersion: NODE_VERSION, mobxDevTools: MOBX_DEV_TOOLS, current: CURRENT_NODE_ENV, isDev, @@ -64,11 +91,15 @@ export const environment: Environment = Object.assign( isMainnet, isStaging, isTestnet, + isSelfnode, + isIncentivizedTestnet, + isIncentivizedTestnetQA, + isIncentivizedTestnetNightly, + isIncentivizedTestnetSelfnode, isDevelopment, isWatchMode, build: BUILD, buildNumber: BUILD_NUMBER, - buildLabel: BUILD_LABEL, platform: PLATFORM, platformVersion: PLATFORM_VERSION, mainProcessID, diff --git a/source/main/index.js b/source/main/index.js index a6b9f5f817..e9db15a47b 100644 --- a/source/main/index.js +++ b/source/main/index.js @@ -3,13 +3,13 @@ import os from 'os'; import path from 'path'; import { app, BrowserWindow, shell } from 'electron'; import { client } from 'electron-connect'; -import { Logger } from './utils/logging'; +import { logger } from './utils/logging'; import { setupLogging, logSystemInfo, logStateSnapshot, + generateWalletMigrationReport, } from './utils/setupLogging'; -import { getNumberOfEpochsConsolidated } from './utils/getNumberOfEpochsConsolidated'; import { handleDiskSpace } from './utils/handleDiskSpace'; import { createMainWindow } from './windows/main'; import { installChromeExtensions } from './utils/installChromeExtensions'; @@ -17,12 +17,10 @@ import { environment } from './environment'; import mainErrorHandler from './utils/mainErrorHandler'; import { launcherConfig, - frontendOnlyMode, pubLogsFolderPath, - APP_NAME, stateDirectoryPath, } from './config'; -import { setupCardano } from './cardano/setup'; +import { setupCardanoNode } from './cardano/setup'; import { CardanoNode } from './cardano/CardanoNode'; import { safeExitWithCode } from './utils/safeExitWithCode'; import { buildAppMenus } from './utils/buildAppMenus'; @@ -32,10 +30,12 @@ import { ensureXDGDataIsSet } from './cardano/config'; import { rebuildApplicationMenu } from './ipc/rebuild-application-menu'; import { detectSystemLocaleChannel } from './ipc/detect-system-locale'; import { getStateDirectoryPathChannel } from './ipc/getStateDirectoryPathChannel'; +import { getDesktopDirectoryPathChannel } from './ipc/getDesktopDirectoryPathChannel'; import { CardanoNodeStates } from '../common/types/cardano-node.types'; import type { CheckDiskSpaceResponse } from '../common/types/no-disk-space.types'; import { logUsedVersion } from './utils/logUsedVersion'; import { setStateSnapshotLogChannel } from './ipc/set-log-state-snapshot'; +import { generateWalletMigrationReportChannel } from './ipc/generateWalletMigrationReportChannel'; /* eslint-disable consistent-return */ @@ -50,25 +50,35 @@ const { network, os: osName, version: daedalusVersion, - buildNumber: cardanoVersion, + nodeVersion: cardanoNodeVersion, + apiVersion: cardanoWalletVersion, } = environment; +if (isBlankScreenFixActive) { + // Run "location.assign('chrome://gpu')" in JavaScript console to see if the flag is active + app.disableHardwareAcceleration(); +} + const safeExit = async () => { if (!cardanoNode || cardanoNode.state === CardanoNodeStates.STOPPED) { - Logger.info('Daedalus:safeExit: exiting Daedalus with code 0', { code: 0 }); + logger.info('Daedalus:safeExit: exiting Daedalus with code 0', { code: 0 }); return safeExitWithCode(0); } - if (cardanoNode.state === CardanoNodeStates.STOPPING) return; + if (cardanoNode.state === CardanoNodeStates.STOPPING) { + logger.info('Daedalus:safeExit: waiting for cardano-node to stop...'); + cardanoNode.exitOnStop(); + return; + } try { const pid = cardanoNode.pid || 'null'; - Logger.info(`Daedalus:safeExit: stopping cardano-node with PID: ${pid}`, { + logger.info(`Daedalus:safeExit: stopping cardano-node with PID: ${pid}`, { pid, }); await cardanoNode.stop(); - Logger.info('Daedalus:safeExit: exiting Daedalus with code 0', { code: 0 }); + logger.info('Daedalus:safeExit: exiting Daedalus with code 0', { code: 0 }); safeExitWithCode(0); } catch (error) { - Logger.error('Daedalus:safeExit: cardano-node did not exit correctly', { + logger.error('Daedalus:safeExit: cardano-node did not exit correctly', { error, }); safeExitWithCode(0); @@ -79,7 +89,7 @@ const onAppReady = async () => { setupLogging(); logUsedVersion( environment.version, - path.join(pubLogsFolderPath, `${APP_NAME}-versions.json`) + path.join(pubLogsFolderPath, 'Daedalus-versions.json') ); const cpu = os.cpus(); @@ -90,7 +100,8 @@ const onAppReady = async () => { const systemLocale = detectSystemLocale(); const systemInfo = logSystemInfo({ - cardanoVersion, + cardanoNodeVersion, + cardanoWalletVersion, cpu, daedalusVersion, isBlankScreenFixActive, @@ -101,9 +112,16 @@ const onAppReady = async () => { startTime, }); - Logger.info(`Daedalus is starting at ${startTime}`, { startTime }); + logger.info(`Daedalus is starting at ${startTime}`, { startTime }); - Logger.info('Updating System-info.json file', { ...systemInfo.data }); + logger.info('Updating System-info.json file', { ...systemInfo.data }); + + // We need DAEDALUS_INSTALL_DIRECTORY in PATH + // in order for the cardano-launcher to find wallet and node bins + process.env.PATH = [ + process.env.PATH, + process.env.DAEDALUS_INSTALL_DIRECTORY, + ].join(path.delimiter); ensureXDGDataIsSet(); await installChromeExtensions(isDev); @@ -116,10 +134,6 @@ const onAppReady = async () => { const onCheckDiskSpace = ({ isNotEnoughDiskSpace, }: CheckDiskSpaceResponse) => { - // Daedalus is not managing cardano-node in `frontendOnlyMode` - // so we don't have a way to stop it in case there is not enough disk space - if (frontendOnlyMode) return; - if (cardanoNode) { if (isNotEnoughDiskSpace) { if ( @@ -148,7 +162,7 @@ const onAppReady = async () => { mainErrorHandler(onMainError); await handleCheckDiskSpace(); - cardanoNode = setupCardano(launcherConfig, mainWindow); + cardanoNode = setupCardanoNode(launcherConfig, mainWindow); if (isWatchMode) { // Connect to electron-connect server which restarts / reloads windows on file changes @@ -157,18 +171,24 @@ const onAppReady = async () => { detectSystemLocaleChannel.onRequest(() => Promise.resolve(systemLocale)); - getNumberOfEpochsConsolidated(); - setStateSnapshotLogChannel.onReceive(data => { return Promise.resolve(logStateSnapshot(data)); }); + generateWalletMigrationReportChannel.onReceive(data => { + return Promise.resolve(generateWalletMigrationReport(data)); + }); + getStateDirectoryPathChannel.onRequest(() => Promise.resolve(stateDirectoryPath) ); + getDesktopDirectoryPathChannel.onRequest(() => + Promise.resolve(app.getPath('desktop')) + ); + mainWindow.on('close', async event => { - Logger.info( + logger.info( 'mainWindow received event. Safe exiting Daedalus now.' ); event.preventDefault(); @@ -195,7 +215,7 @@ const onAppReady = async () => { contents.on('new-window', (event, url) => { // Prevent creation of new BrowserWindows via links / window.open event.preventDefault(); - Logger.info('Prevented creation of new browser window', { url }); + logger.info('Prevented creation of new browser window', { url }); // Open these links with the default browser shell.openExternal(url); }); @@ -203,7 +223,7 @@ const onAppReady = async () => { // 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.'); + logger.info('app received event. Safe exiting Daedalus now.'); event.preventDefault(); // prevent Daedalus from quitting immediately await safeExit(); }); diff --git a/source/main/ipc/bugReportRequestChannel.js b/source/main/ipc/bugReportRequestChannel.js index aef7a27e93..0ca312ba6c 100644 --- a/source/main/ipc/bugReportRequestChannel.js +++ b/source/main/ipc/bugReportRequestChannel.js @@ -9,7 +9,7 @@ import type { SubmitBugReportRequestMainResponse, SubmitBugReportRendererRequest, } from '../../common/ipc/api'; -import { Logger } from '../utils/logging'; +import { logger } from '../utils/logging'; /* eslint-disable consistent-return */ @@ -23,7 +23,7 @@ export const handleBugReportRequests = () => { bugReportRequestChannel.onReceive( (request: SubmitBugReportRendererRequest) => new Promise((resolve, reject) => { - Logger.info('bugReportRequestChannel::onReceive', { request }); + logger.info('bugReportRequestChannel::onReceive', { request }); const { httpOptions, requestPayload } = request; const options = Object.assign({}, httpOptions); const payload = Object.assign({}, requestPayload); @@ -42,7 +42,7 @@ export const handleBugReportRequests = () => { options.headers = formData.getHeaders(); - Logger.info('Sending bug report request with options', { options }); + logger.info('Sending bug report request with options', { options }); const httpRequest = http.request(options); httpRequest.on('response', response => { if (response.statusCode !== 200) { diff --git a/source/main/ipc/cardano.ipc.js b/source/main/ipc/cardano.ipc.js index 274535fb0b..4259dc5913 100644 --- a/source/main/ipc/cardano.ipc.js +++ b/source/main/ipc/cardano.ipc.js @@ -8,6 +8,7 @@ import { CARDANO_TLS_CONFIG_CHANNEL, CARDANO_AWAIT_UPDATE_CHANNEL, SET_CACHED_CARDANO_STATUS_CHANNEL, + EXPORT_WALLETS_CHANNEL, } from '../../common/ipc/api'; import type { CardanoAwaitUpdateMainResponse, @@ -24,6 +25,8 @@ import type { GetCachedCardanoStatusMainResponse, SetCachedCardanoStatusRendererRequest, SetCachedCardanoStatusMainResponse, + ExportWalletsRendererRequest, + ExportWalletsMainResponse, } from '../../common/ipc/api'; // IpcChannel @@ -62,3 +65,8 @@ export const setCachedCardanoStatusChannel: MainIpcChannel< SetCachedCardanoStatusRendererRequest, SetCachedCardanoStatusMainResponse > = new MainIpcChannel(SET_CACHED_CARDANO_STATUS_CHANNEL); + +export const exportWalletsChannel: MainIpcChannel< + ExportWalletsRendererRequest, + ExportWalletsMainResponse +> = new MainIpcChannel(EXPORT_WALLETS_CHANNEL); diff --git a/source/main/ipc/compress-logs.js b/source/main/ipc/compress-logs.js index 058592c48f..32f0b4f5e9 100644 --- a/source/main/ipc/compress-logs.js +++ b/source/main/ipc/compress-logs.js @@ -4,7 +4,7 @@ import archiver from 'archiver'; import path from 'path'; import { get } from 'lodash'; import { appLogsFolderPath, pubLogsFolderPath } from '../config'; -import { Logger } from '../utils/logging'; +import { logger } from '../utils/logging'; import { MainIpcChannel } from './lib/MainIpcChannel'; import { COMPRESS_LOGS_CHANNEL } from '../../common/ipc/api'; import type { @@ -28,16 +28,16 @@ export default () => { }); output.on('close', () => { - Logger.debug('COMPRESS_LOGS.SUCCESS', { outputPath }); + logger.debug('COMPRESS_LOGS.SUCCESS', { outputPath }); resolve(outputPath); }); archive.on('error', error => { - Logger.error('COMPRESS_LOGS.ERROR', { error }); + logger.error('COMPRESS_LOGS.ERROR', { error }); reject(error); }); - Logger.debug('COMPRESS_LOGS.START'); + logger.debug('COMPRESS_LOGS.START'); // compress files const logFiles = get(logs, ['files'], []); @@ -50,7 +50,7 @@ export default () => { archive.finalize(error => { if (error) { - Logger.error('COMPRESS_LOGS.ERROR', { error }); + logger.error('COMPRESS_LOGS.ERROR', { error }); reject(error); } }); diff --git a/source/main/ipc/electronStoreConversation.js b/source/main/ipc/electronStoreConversation.js new file mode 100644 index 0000000000..629cc1cb9a --- /dev/null +++ b/source/main/ipc/electronStoreConversation.js @@ -0,0 +1,29 @@ +// @flow +import ElectronStore from 'electron-store'; +import type { ElectronStoreMessage } from '../../common/ipc/api'; +import { ELECTRON_STORE_CHANNEL } from '../../common/ipc/api'; +import { MainIpcConversation } from './lib/MainIpcConversation'; + +const store = new ElectronStore(); + +// MainIpcChannel +export const electronStoreConversation: MainIpcConversation< + ElectronStoreMessage, + any +> = new MainIpcConversation(ELECTRON_STORE_CHANNEL); + +export const handleElectronStoreChannel = () => { + electronStoreConversation.onRequest(request => { + const { type, key, data } = request; + switch (type) { + case 'get': + return store.get(key); + case 'delete': + return store.delete(key); + case 'set': + return store.set(key, data); + default: + return Promise.reject(new Error(`Invalid type ${type} provided.`)); + } + }); +}; diff --git a/source/main/ipc/generateAddressPDFChannel.js b/source/main/ipc/generateAddressPDFChannel.js new file mode 100644 index 0000000000..ad04fef016 --- /dev/null +++ b/source/main/ipc/generateAddressPDFChannel.js @@ -0,0 +1,163 @@ +// @flow +import fs from 'fs'; +import path from 'path'; +import PDFDocument from 'pdfkit'; +import qr from 'qr-image'; +import { MainIpcChannel } from './lib/MainIpcChannel'; +import { GENERATE_ADDRESS_PDF_CHANNEL } from '../../common/ipc/api'; +import { getHeightOfString } from '../utils/pdf'; +import type { + GenerateAddressPDFRendererRequest, + GenerateAddressPDFMainResponse, +} from '../../common/ipc/api'; +import fontRegularEn from '../../common/assets/pdf/NotoSans-Regular.ttf'; +import fontMediumEn from '../../common/assets/pdf/NotoSans-Medium.ttf'; +import fontUnicode from '../../common/assets/pdf/arial-unicode.ttf'; +import fontMono from '../../common/assets/pdf/NotoSansMono-Regular.ttf'; + +export const generateAddressPDFChannel: // IpcChannel +MainIpcChannel< + GenerateAddressPDFRendererRequest, + GenerateAddressPDFMainResponse +> = new MainIpcChannel(GENERATE_ADDRESS_PDF_CHANNEL); + +export const handleAddressPDFRequests = () => { + generateAddressPDFChannel.onReceive( + (request: GenerateAddressPDFRendererRequest) => + new Promise((resolve, reject) => { + // Prepare params + const { + address, + filePath, + note, + currentLocale, + isMainnet, + networkLabel, + networkName, + creationDate, + noteLabel, + title, + author, + } = request; + + const readAssetSync = p => fs.readFileSync(path.join(__dirname, p)); + let fontRegular; + let fontMedium; + + if (currentLocale === 'ja-JP') { + fontRegular = fontUnicode; + fontMedium = fontUnicode; + } else { + fontRegular = fontRegularEn; + fontMedium = fontMediumEn; + } + + // Generate QR image for wallet address + const qrCodeImage = qr.imageSync(address, { + type: 'png', + size: 10, + ec_level: 'L', + margin: 0, + }); + + try { + const fontBufferMedium = readAssetSync(fontMedium); + const fontBufferRegular = readAssetSync(fontRegular); + const fontBufferMono = readAssetSync(fontMono); + const fontBufferUnicode = readAssetSync(fontUnicode); + + let noteHeight = 0; + if (note) { + noteHeight = getHeightOfString(note, fontBufferRegular, 14) + 30; + } + + const textColor = '#5e6066'; + const textColorRed = '#ea4c5b'; + const width = 640; + const height = 420 + noteHeight; + const doc = new PDFDocument({ + size: [width, height], + margins: { + bottom: 20, + left: 30, + right: 30, + top: 20, + }, + info: { + Title: title, + Author: author, + }, + }).fillColor(textColor); + + // Title + doc + .font(fontBufferMedium) + .fontSize(18) + .text(title.toUpperCase(), { + align: 'center', + characterSpacing: 2, + }); + + // Creation date + doc + .font(fontBufferRegular) + .fontSize(12) + .text(creationDate.toUpperCase(), { + align: 'center', + characterSpacing: 0.6, + }); + + doc.moveDown(); + + // QR Code + doc.image(qrCodeImage, { + fit: [width - 60, 192], + align: 'center', + }); + + doc.moveDown(); + + // Address + doc + .font(fontBufferMono) + .fontSize(19) + .text(address, { + align: 'center', + characterSpacing: 1.5, + }); + + if (note) { + doc.moveDown(); + // Note title + doc + .font(fontBufferRegular) + .fontSize(14) + .text(noteLabel); + + // Note + doc.font(fontBufferUnicode).text(note); + } + + doc.moveDown(); + + // Footer + doc + .fontSize(12) + .font(isMainnet ? fontBufferRegular : fontBufferMedium) + .fillColor(isMainnet ? textColor : textColorRed) + .text(`${networkLabel} ${networkName}`, { + align: 'right', + }); + + // Write file to disk + const writeStream = fs.createWriteStream(filePath); + doc.pipe(writeStream); + doc.end(); + writeStream.on('close', resolve); + writeStream.on('error', reject); + } catch (error) { + reject(error); + } + }) + ); +}; diff --git a/source/main/ipc/generateRewardsCsvChannel.js b/source/main/ipc/generateRewardsCsvChannel.js new file mode 100644 index 0000000000..6f0ab57ecf --- /dev/null +++ b/source/main/ipc/generateRewardsCsvChannel.js @@ -0,0 +1,38 @@ +// @flow +import fs from 'fs'; +import csvStringify from 'csv-stringify'; +import { MainIpcChannel } from './lib/MainIpcChannel'; +import { GENERATE_REWARDS_CSV_CHANNEL } from '../../common/ipc/api'; +import type { + GenerateRewardsCsvMainResponse, + GenerateRewardsCsvRendererRequest, +} from '../../common/ipc/api'; + +export const generateRewardsCsvChannel: // IpcChannel +MainIpcChannel< + GenerateRewardsCsvRendererRequest, + GenerateRewardsCsvMainResponse +> = new MainIpcChannel(GENERATE_REWARDS_CSV_CHANNEL); + +export const handleRewardsCsvRequests = () => { + generateRewardsCsvChannel.onReceive( + (request: GenerateRewardsCsvRendererRequest) => + new Promise((resolve, reject) => { + const { rewards, filePath } = request; + + csvStringify(rewards, (csvErr, output) => { + if (csvErr) { + return reject(csvErr); + } + + return fs.writeFile(filePath, output, fileErr => { + if (fileErr) { + return reject(fileErr); + } + + return resolve(); + }); + }); + }) + ); +}; diff --git a/source/main/ipc/generateWalletMigrationReportChannel.js b/source/main/ipc/generateWalletMigrationReportChannel.js new file mode 100644 index 0000000000..ade6d76bf7 --- /dev/null +++ b/source/main/ipc/generateWalletMigrationReportChannel.js @@ -0,0 +1,13 @@ +// @flow +import { MainIpcChannel } from './lib/MainIpcChannel'; +import { GENERATE_WALLET_MIGRATION_REPORT_CHANNEL } from '../../common/ipc/api'; +import type { + GenerateWalletMigrationReportRendererRequest, + GenerateWalletMigrationReportMainResponse, +} from '../../common/ipc/api'; + +export const generateWalletMigrationReportChannel: // IpcChannel +MainIpcChannel< + GenerateWalletMigrationReportRendererRequest, + GenerateWalletMigrationReportMainResponse +> = new MainIpcChannel(GENERATE_WALLET_MIGRATION_REPORT_CHANNEL); diff --git a/source/main/ipc/get-logs.js b/source/main/ipc/get-logs.js index 48101f56e0..6f5e5722a2 100644 --- a/source/main/ipc/get-logs.js +++ b/source/main/ipc/get-logs.js @@ -85,14 +85,11 @@ export default () => { const logs: LogFiles = { path: pubLogsFolderPath, - files: sortBy( - logFiles, - (log: string): string => { - // custom file sorting which enforces correct ordering (like in ALLOWED_LOGS) - const nameSegments = log.split('.'); - return nameSegments.shift() + nameSegments.join('').length; - } - ), + files: sortBy(logFiles, (log: string): string => { + // custom file sorting which enforces correct ordering (like in ALLOWED_LOGS) + const nameSegments = log.split('.'); + return nameSegments.shift() + nameSegments.join('').length; + }), }; return Promise.resolve(logs); diff --git a/source/main/ipc/getDesktopDirectoryPathChannel.js b/source/main/ipc/getDesktopDirectoryPathChannel.js new file mode 100644 index 0000000000..7d1245a752 --- /dev/null +++ b/source/main/ipc/getDesktopDirectoryPathChannel.js @@ -0,0 +1,14 @@ +// @flow +import { MainIpcChannel } from './lib/MainIpcChannel'; +import { GET_DESKTOP_DIRECTORY_PATH_CHANNEL } from '../../common/ipc/api'; +import type { + GetDesktopDirectoryPathRendererRequest, + GetDesktopDirectoryPathMainResponse, +} from '../../common/ipc/api'; + +// IpcChannel + +export const getDesktopDirectoryPathChannel: MainIpcChannel< + GetDesktopDirectoryPathRendererRequest, + GetDesktopDirectoryPathMainResponse +> = new MainIpcChannel(GET_DESKTOP_DIRECTORY_PATH_CHANNEL); diff --git a/source/main/ipc/getNumberOfEpochsConsolidated.ipc.js b/source/main/ipc/getNumberOfEpochsConsolidated.ipc.js deleted file mode 100644 index 9407c9dd3b..0000000000 --- a/source/main/ipc/getNumberOfEpochsConsolidated.ipc.js +++ /dev/null @@ -1,14 +0,0 @@ -// @flow -import { MainIpcChannel } from './lib/MainIpcChannel'; -import { GET_CONSOLIDATED_EPOCHS_COUNT_CHANNEL } from '../../common/ipc/api'; -import type { - GetConsolidatedEpochsCountMainResponse, - GetConsolidatedEpochsCountRendererRequest, -} from '../../common/ipc/api'; - -// IpcChannel - -export const getNumberOfEpochsConsolidatedChannel: MainIpcChannel< - GetConsolidatedEpochsCountRendererRequest, - GetConsolidatedEpochsCountMainResponse -> = new MainIpcChannel(GET_CONSOLIDATED_EPOCHS_COUNT_CHANNEL); diff --git a/source/main/ipc/index.js b/source/main/ipc/index.js index bd336de415..90b6a141f2 100644 --- a/source/main/ipc/index.js +++ b/source/main/ipc/index.js @@ -2,6 +2,7 @@ import type { BrowserWindow } from 'electron'; import compressLogsApi from './compress-logs'; import downloadLogsApi from './download-logs'; +import { handleElectronStoreChannel } from './electronStoreConversation'; import getLogsApi from './get-logs'; import resizeWindowApi from './resize-window'; import loadAsset from './load-asset'; @@ -9,6 +10,9 @@ import getGpuStatus from './get-gpu-status'; import { handleBugReportRequests } from './bugReportRequestChannel'; import { handleFileMetaRequests } from './generateFileMetaChannel'; import { handlePaperWalletRequests } from './generatePaperWalletChannel'; +import { handleAddressPDFRequests } from './generateAddressPDFChannel'; +import { handleRewardsCsvRequests } from './generateRewardsCsvChannel'; +import { handleFileDialogRequests } from './show-file-dialog-channels'; import { openExternalUrlChannel } from './open-external-url'; import { openLocalDirectoryChannel } from './open-local-directory'; @@ -22,8 +26,12 @@ export default (window: BrowserWindow) => { handleBugReportRequests(); handleFileMetaRequests(); handlePaperWalletRequests(); + handleAddressPDFRequests(); + handleRewardsCsvRequests(); + handleFileDialogRequests(window); // eslint-disable-next-line no-unused-expressions openExternalUrlChannel; // eslint-disable-next-line no-unused-expressions openLocalDirectoryChannel; + handleElectronStoreChannel(); }; diff --git a/source/main/ipc/lib/MainIpcConversation.js b/source/main/ipc/lib/MainIpcConversation.js new file mode 100644 index 0000000000..f9bcb8d294 --- /dev/null +++ b/source/main/ipc/lib/MainIpcConversation.js @@ -0,0 +1,45 @@ +// @flow +import { ipcMain } from 'electron'; +import type { + IpcReceiver, + IpcSender, +} from '../../../common/ipc/lib/IpcConversation'; +import { IpcConversation } from '../../../common/ipc/lib/IpcConversation'; + +/** + * Subclass of IpcChannel that uses ipcMain to receive messages. + */ +export class MainIpcConversation extends IpcConversation< + Incoming, + Outgoing +> { + async send( + message: Outgoing, + sender: IpcSender, + receiver: IpcReceiver = ipcMain + ): Promise { + return super.request(message, sender, receiver); + } + + async request( + message: Outgoing, + sender: IpcSender, + receiver: IpcReceiver = ipcMain + ): Promise { + return super.request(message, sender, receiver); + } + + onReceive( + handler: (message: Incoming) => Promise, + receiver: IpcReceiver = ipcMain + ): void { + super.onRequest(handler, receiver); + } + + onRequest( + handler: Incoming => Promise, + receiver: IpcReceiver = ipcMain + ): void { + super.onRequest(handler, receiver); + } +} diff --git a/source/main/ipc/show-file-dialog-channels.js b/source/main/ipc/show-file-dialog-channels.js new file mode 100644 index 0000000000..aa89132d87 --- /dev/null +++ b/source/main/ipc/show-file-dialog-channels.js @@ -0,0 +1,35 @@ +// @flow +import { dialog } from 'electron'; +import type { BrowserWindow } from 'electron'; +import { MainIpcChannel } from './lib/MainIpcChannel'; +import { + SHOW_OPEN_DIALOG_CHANNEL, + SHOW_SAVE_DIALOG_CHANNEL, +} from '../../common/ipc/api'; +import type { + ShowOpenDialogRendererRequest, + ShowOpenDialogMainResponse, + ShowSaveDialogRendererRequest, + ShowSaveDialogMainResponse, +} from '../../common/ipc/api'; + +export const showOpenDialogChannel: // IpcChannel +MainIpcChannel< + ShowOpenDialogRendererRequest, + ShowOpenDialogMainResponse +> = new MainIpcChannel(SHOW_OPEN_DIALOG_CHANNEL); + +export const showSaveDialogChannel: // IpcChannel +MainIpcChannel< + ShowSaveDialogRendererRequest, + ShowSaveDialogMainResponse +> = new MainIpcChannel(SHOW_SAVE_DIALOG_CHANNEL); + +export const handleFileDialogRequests = (window: BrowserWindow) => { + showOpenDialogChannel.onReceive((request: ShowOpenDialogRendererRequest) => + dialog.showOpenDialog(window, request) + ); + showSaveDialogChannel.onReceive((request: ShowSaveDialogRendererRequest) => + dialog.showSaveDialog(window, request) + ); +}; diff --git a/source/main/locales/en-US.json b/source/main/locales/en-US.json index 1fdb541579..433f83d690 100644 --- a/source/main/locales/en-US.json +++ b/source/main/locales/en-US.json @@ -1,6 +1,12 @@ { + "dialog.exportWalletsWarning.cancel": "Cancel", + "dialog.exportWalletsWarning.confirm": "Daedalus is closed, import wallets", + "dialog.exportWalletsWarning.message": "Daedalus Flight is about to import wallets from the production version of Daedalus.\n\nThe production version of Daedalus must not be running during the import process. If you have not already done so, please close the production Daedalus application now.", + "dialog.exportWalletsWarning.title": "Import wallets", "menu.daedalus": "Daedalus", "menu.daedalus.about": "About Daedalus", + "menu.daedalus.settings": "Settings", + "menu.daedalus.walletSettings": "Wallet Settings", "menu.daedalus.hideDaedalus": "Hide Daedalus", "menu.daedalus.hideOthers": "Hide Others", "menu.daedalus.showAll": "Show All", @@ -19,18 +25,17 @@ "menu.helpSupport.blankScreenFixDialogConfirm": "Yes", "menu.helpSupport.blankScreenFixDialogMessage": "Turn off 'Blank screen fix'? \n \nDisabling the blank screen fix setting will improve the performance of user interface rendering by enabling graphics acceleration, however, some users may find that Daedalus runs better with this setting enabled. If you see a blank screen instead of the Daedalus user interface after disabling this setting and restarting Daedalus, please turn this setting back on. \n \nDo you want to disable this setting and restart Daedalus?", "menu.helpSupport.blankScreenFixDialogTitle": "Turn off 'Blank screen fix'?", - "menu.helpSupport.blockConsolidationStatus": "Block Consolidation Status", "menu.helpSupport.daedalusDiagnostics": "Daedalus Diagnostics", "menu.helpSupport.downloadLogs": "Download Logs", - "menu.helpSupport.knownIssues": "Known Issues", + "menu.helpSupport.knownIssues": "Known Issues ↗", "menu.helpSupport.knownIssuesUrl": "https://daedaluswallet.io/known-issues/", "menu.helpSupport.nonBlankScreenFixDialogMessage": "Turn on 'Blank screen fix'? \n \nIf the Daedalus user interface is failing to load then enabling this setting may fix the problem. It will reduce the performance of user interface rendering by disabling graphics acceleration, but some users may find that Daedalus runs better with this setting enabled. \n \nEnable this setting only if the Daedalus user interface is failing to load. If Daedalus is working properly, please click ‘No’. Otherwise, click ‘Yes’ to restart Daedalus with the blank screen fix setting enabled. \n \nDo you want to enable this setting and restart Daedalus?", "menu.helpSupport.nonBlankScreenFixDialogTitle": "Turn on 'Blank screen fix'?", - "menu.helpSupport.featureRequest": "Feature Requests", + "menu.helpSupport.featureRequest": "Feature Requests ↗", "menu.helpSupport.featureRequestUrl": "https://iohk.zendesk.com/hc/en-us/sections/360002144373", - "menu.helpSupport.safetyTips": "Safety Tips", + "menu.helpSupport.safetyTips": "Safety Tips ↗", "menu.helpSupport.safetyTipsUrl": "https://iohk.zendesk.com/hc/en-us/articles/360015295134", - "menu.helpSupport.supportRequest": "Support Request", + "menu.helpSupport.supportRequest": "Support Request ↗", "menu.helpSupport.supportRequestUrl": "https://iohk.zendesk.com/hc/en-us/requests/new/", "menu.view": "View", "menu.view.reload": "Reload", diff --git a/source/main/locales/ja-JP.json b/source/main/locales/ja-JP.json index aca07a34bb..04aed7629a 100644 --- a/source/main/locales/ja-JP.json +++ b/source/main/locales/ja-JP.json @@ -1,6 +1,12 @@ { + "dialog.exportWalletsWarning.cancel": "キャンセル", + "dialog.exportWalletsWarning.confirm": "Daedalusは閉じている、ウォレットをインポートする", + "dialog.exportWalletsWarning.message": "Daedalus FlightはDaedalus製品バージョンからウォレットをインポートしようとしています。\n\nインポート中はDaedalus製品バージョンを実行しないでください。Daedalusアプリケーションが開いている場合は、直ちに終了してください。", + "dialog.exportWalletsWarning.title": "ウォレットをインポートする", "menu.daedalus": "Daedalus", "menu.daedalus.about": "このDaedalusについて", + "menu.daedalus.settings": "設定", + "menu.daedalus.walletSettings": "ウォレットの設定", "menu.daedalus.hideDaedalus": "Daedalusを非表示", "menu.daedalus.hideOthers": "他を非表示", "menu.daedalus.showAll": "すべてを表示", @@ -19,18 +25,17 @@ "menu.helpSupport.blankScreenFixDialogConfirm": "はい", "menu.helpSupport.blankScreenFixDialogMessage": "「ブランク画面修正」を無効にしますか? \n \nブランク画面修正設定を無効にすると、グラフィックアクセラレーションが有効化されてユーザーインターフェイスのレンダリングパフォーマンスが向上しますが、この設定を有効にした方がDaedalusがスムーズに作動する場合があります。この設定を無効にしてDaedalusを再起動した際にDaedalusユーザーインターフェイスの代わりにブランク画面が表示される場合は、この設定をもう一度有効にしてください。 \n \nこの設定を無効にしてDaedalusを再起動しますか。", "menu.helpSupport.blankScreenFixDialogTitle": "「ブランク画面修正」を無効にしますか?", - "menu.helpSupport.blockConsolidationStatus": "ブロック統合状況", "menu.helpSupport.daedalusDiagnostics": "Daedalus診断", "menu.helpSupport.downloadLogs": "ログのダウンロード", - "menu.helpSupport.knownIssues": "既知の問題", + "menu.helpSupport.knownIssues": "既知の問題 ↗", "menu.helpSupport.knownIssuesUrl": "https://daedaluswallet.io/ja/known-issues/", "menu.helpSupport.nonBlankScreenFixDialogMessage": "「ブランク画面修正」を有効にしますか? \n \nDaedalusユーザーインターフェイスがロードに失敗した際、この設定を有効にすることにより問題が解消される場合があります。この場合、グラフィックアクセラレーションが無効化されることでユーザーインターフェイスのレンダリングパフォーマンスが低下しますが、この設定を有効にした方がDaedalusがスムーズに作動する場合があります。 \n \nこの設定はDaedalusユーザーインターフェイスがロードに失敗した場合にのみ有効にしてください。Daedalusが正常に作動している場合は[いいえ]をクリックします。正常に作動していない場合には[はい]をクリックし、ブランク画面修正設定を有効にしてDaedalusを再起動してください。 \n \nこの設定を有効にしてDaedalusを再起動しますか。", "menu.helpSupport.nonBlankScreenFixDialogTitle": "「ブランク画面修正」を有効にしますか?", - "menu.helpSupport.featureRequest": "機能リクエスト", + "menu.helpSupport.featureRequest": "機能リクエスト ↗", "menu.helpSupport.featureRequestUrl": "https://iohk.zendesk.com/hc/ja/sections/360002144373", - "menu.helpSupport.safetyTips": "安全に利用するために", + "menu.helpSupport.safetyTips": "安全に利用するために ↗", "menu.helpSupport.safetyTipsUrl": "https://iohk.zendesk.com/hc/ja/articles/360015295134", - "menu.helpSupport.supportRequest": "サポートリクエスト", + "menu.helpSupport.supportRequest": "サポートリクエスト ↗", "menu.helpSupport.supportRequestUrl": "https://iohk.zendesk.com/hc/ja/requests/new/", "menu.view": "見る", "menu.view.reload": "リロード", diff --git a/source/main/menus/MenuActions.types.js b/source/main/menus/MenuActions.types.js index 55141f5414..94900feae6 100644 --- a/source/main/menus/MenuActions.types.js +++ b/source/main/menus/MenuActions.types.js @@ -2,6 +2,7 @@ export type MenuActions = { toggleBlankScreenFix: Function, openAboutDialog: Function, - openBlockConsolidationStatusDialog: Function, openDaedalusDiagnosticsDialog: Function, + openSettingsPage: Function, + openWalletSettingsPage: Function, }; diff --git a/source/main/menus/osx.js b/source/main/menus/osx.js index 1126af55fa..5ee8448e1f 100644 --- a/source/main/menus/osx.js +++ b/source/main/menus/osx.js @@ -10,7 +10,7 @@ import { NOTIFICATIONS } from '../../common/ipc/constants'; import { generateSupportRequestLink } from '../../common/utils/reporting'; const id = 'menu'; -const { isBlankScreenFixActive } = environment; +const { isBlankScreenFixActive, isIncentivizedTestnet } = environment; export const osxMenu = ( app: App, @@ -32,6 +32,23 @@ export const osxMenu = ( enabled: !isUpdateAvailable, }, { type: 'separator' }, + { + label: translation('daedalus.settings'), + accelerator: 'Command+,', + click() { + actions.openSettingsPage(); + }, + enabled: !isUpdateAvailable, + }, + { + label: translation('daedalus.walletSettings'), + accelerator: 'Command+;', + click() { + actions.openWalletSettingsPage(); + }, + enabled: !isUpdateAvailable, + }, + { type: 'separator' }, { label: translation('daedalus.hideDaedalus'), role: 'hide', @@ -120,14 +137,16 @@ export const osxMenu = ( shell.openExternal(faqLink); }, }, - { - label: translation('helpSupport.blankScreenFix'), - type: 'checkbox', - checked: isBlankScreenFixActive, - click(item) { - actions.toggleBlankScreenFix(item); - }, - }, + !isIncentivizedTestnet + ? { + label: translation('helpSupport.blankScreenFix'), + type: 'checkbox', + checked: isBlankScreenFixActive, + click(item) { + actions.toggleBlankScreenFix(item); + }, + } + : null, { type: 'separator' }, { label: translation('helpSupport.safetyTips'), @@ -167,14 +186,6 @@ export const osxMenu = ( enabled: !isUpdateAvailable, }, { type: 'separator' }, - { - label: translation('helpSupport.blockConsolidationStatus'), - accelerator: 'Command+B', - click() { - actions.openBlockConsolidationStatusDialog(); - }, - enabled: !isUpdateAvailable, - }, { label: translation('helpSupport.daedalusDiagnostics'), accelerator: 'Command+D', diff --git a/source/main/menus/win-linux.js b/source/main/menus/win-linux.js index d5f37f9872..192b8c3507 100644 --- a/source/main/menus/win-linux.js +++ b/source/main/menus/win-linux.js @@ -10,7 +10,11 @@ import { showUiPartChannel } from '../ipc/control-ui-parts'; import { generateSupportRequestLink } from '../../common/utils/reporting'; const id = 'menu'; -const { isWindows, isBlankScreenFixActive } = environment; +const { + isWindows, + isBlankScreenFixActive, + isIncentivizedTestnet, +} = environment; export const winLinuxMenu = ( app: App, @@ -88,6 +92,28 @@ export const winLinuxMenu = ( window.webContents.reload(); }, }, + { + type: 'separator', + }, + { + label: translation('daedalus.settings'), + accelerator: 'Alt+S', + click() { + actions.openSettingsPage(); + }, + enabled: !isUpdateAvailable, + }, + { + label: translation('daedalus.walletSettings'), + accelerator: 'Alt+Ctrl+S', + click() { + actions.openWalletSettingsPage(); + }, + enabled: !isUpdateAvailable, + }, + { + type: 'separator', + }, isWindows ? { label: translation('view.toggleFullScreen'), @@ -126,14 +152,16 @@ export const winLinuxMenu = ( shell.openExternal(faqLink); }, }, - { - label: translation('helpSupport.blankScreenFix'), - type: 'checkbox', - checked: isBlankScreenFixActive, - click(item) { - actions.toggleBlankScreenFix(item); - }, - }, + !isIncentivizedTestnet + ? { + label: translation('helpSupport.blankScreenFix'), + type: 'checkbox', + checked: isBlankScreenFixActive, + click(item) { + actions.toggleBlankScreenFix(item); + }, + } + : null, { type: 'separator' }, { label: translation('helpSupport.safetyTips'), @@ -173,14 +201,6 @@ export const winLinuxMenu = ( enabled: !isUpdateAvailable, }, { type: 'separator' }, - { - label: translation('helpSupport.blockConsolidationStatus'), - accelerator: 'Ctrl+B', - click() { - actions.openBlockConsolidationStatusDialog(); - }, - enabled: !isUpdateAvailable, - }, { label: translation('helpSupport.daedalusDiagnostics'), accelerator: 'Ctrl+D', diff --git a/source/main/preload.js b/source/main/preload.js index 4898560cf9..088be243d7 100644 --- a/source/main/preload.js +++ b/source/main/preload.js @@ -2,51 +2,48 @@ import os from 'os'; import _https from 'https'; import _http from 'http'; -import { ipcRenderer as _ipcRenderer, remote as _remote } from 'electron'; -import _electronLog from 'electron-log-daedalus'; -import ElectronStore from 'electron-store'; +import { ipcRenderer } from 'electron'; +import electronLog from 'electron-log-daedalus'; import { environment } from './environment'; +import { + buildLabel, + legacyStateDir, + nodeImplementation, + isFlight, +} from './config'; const _process = process; -const _electronStore = new ElectronStore(); +const _isIncentivizedTestnet = nodeImplementation === 'jormungandr'; process.once('loaded', () => { Object.assign(global, { - Buffer, - dialog: { - showOpenDialog: (...args) => - _remote.dialog.showOpenDialog(_remote.getCurrentWindow(), ...args), - showSaveDialog: (...args) => - _remote.dialog.showSaveDialog(_remote.getCurrentWindow(), ...args), - }, - electronLog: { - debug: (...args) => _electronLog.debug(...args), - info: (...args) => _electronLog.info(...args), - error: (...args) => _electronLog.error(...args), - warn: (...args) => _electronLog.warn(...args), - }, - electronStore: { - get: (...args) => _electronStore.get(...args), - set: (...args) => _electronStore.set(...args), - delete: (...args) => _electronStore.delete(...args), - }, environment, + buildLabel, https: { request: (...args) => _https.request(...args), }, http: { request: (...args) => _http.request(...args), }, - ipcRenderer: { - on: (...args) => _ipcRenderer.on(...args), - once: (...args) => _ipcRenderer.once(...args), - send: (...args) => _ipcRenderer.send(...args), - removeListener: (...args) => _ipcRenderer.removeListener(...args), - removeAllListeners: (...args) => _ipcRenderer.removeAllListeners(...args), - }, os: { platform: os.platform(), }, + ipcRenderer: { + on: (...args) => ipcRenderer.on(...args), + once: (...args) => ipcRenderer.once(...args), + send: (...args) => ipcRenderer.send(...args), + removeListener: (...args) => ipcRenderer.removeListener(...args), + removeAllListeners: (...args) => ipcRenderer.removeAllListeners(...args), + }, + electronLog: { + debug: (...args) => electronLog.debug(...args), + info: (...args) => electronLog.info(...args), + error: (...args) => electronLog.error(...args), + warn: (...args) => electronLog.warn(...args), + }, + isIncentivizedTestnet: _isIncentivizedTestnet, + isFlight, + legacyStateDir, }); // Expose require for Spectron! if (_process.env.NODE_ENV === 'test') { diff --git a/source/main/utils/buildAppMenus.js b/source/main/utils/buildAppMenus.js index ba275b6761..94ec84def1 100644 --- a/source/main/utils/buildAppMenus.js +++ b/source/main/utils/buildAppMenus.js @@ -3,10 +3,10 @@ import { app, globalShortcut, Menu, BrowserWindow, dialog } from 'electron'; import { environment } from '../environment'; import { winLinuxMenu } from '../menus/win-linux'; import { osxMenu } from '../menus/osx'; -import { Logger } from './logging'; +import { logger } from './logging'; import { safeExitWithCode } from './safeExitWithCode'; import { CardanoNode } from '../cardano/CardanoNode'; -import { DIALOGS } from '../../common/ipc/constants'; +import { DIALOGS, PAGES } from '../../common/ipc/constants'; import { showUiPartChannel } from '../ipc/control-ui-parts'; import { getTranslation } from './getTranslation'; @@ -18,7 +18,8 @@ export const buildAppMenus = async ( isUpdateAvailable: boolean, } ) => { - const { ABOUT, BLOCK_CONSOLIDATION, DAEDALUS_DIAGNOSTICS } = DIALOGS; + const { ABOUT, DAEDALUS_DIAGNOSTICS } = DIALOGS; + const { SETTINGS, WALLET_SETTINGS } = PAGES; const { isUpdateAvailable } = data; const { isMacOS, isBlankScreenFixActive } = environment; @@ -28,29 +29,33 @@ export const buildAppMenus = async ( if (mainWindow) showUiPartChannel.send(ABOUT, mainWindow); }; - const openBlockConsolidationStatusDialog = () => { - if (mainWindow) showUiPartChannel.send(BLOCK_CONSOLIDATION, mainWindow); - }; - const openDaedalusDiagnosticsDialog = () => { if (mainWindow) showUiPartChannel.send(DAEDALUS_DIAGNOSTICS, mainWindow); }; + const openSettingsPage = () => { + if (mainWindow) showUiPartChannel.send(SETTINGS, mainWindow); + }; + + const openWalletSettingsPage = () => { + if (mainWindow) showUiPartChannel.send(WALLET_SETTINGS, mainWindow); + }; + const restartWithBlankScreenFix = async () => { - Logger.info('Restarting in BlankScreenFix...'); + logger.info('Restarting in BlankScreenFix...'); if (cardanoNode) await cardanoNode.stop(); - Logger.info('Exiting Daedalus with code 21', { code: 21 }); + logger.info('Exiting Daedalus with code 21', { code: 21 }); safeExitWithCode(21); }; const restartWithoutBlankScreenFix = async () => { - Logger.info('Restarting without BlankScreenFix...'); + logger.info('Restarting without BlankScreenFix...'); if (cardanoNode) await cardanoNode.stop(); - Logger.info('Exiting Daedalus with code 22', { code: 22 }); + logger.info('Exiting Daedalus with code 22', { code: 22 }); safeExitWithCode(22); }; - const toggleBlankScreenFix = item => { + const toggleBlankScreenFix = async item => { const translation = getTranslation(translations, 'menu'); const blankScreenFixDialogOptions = { buttons: [ @@ -68,22 +73,26 @@ export const buildAppMenus = async ( cancelId: 1, noLink: true, }; - dialog.showMessageBox(mainWindow, blankScreenFixDialogOptions, buttonId => { - if (buttonId === 0) { - if (isBlankScreenFixActive) { - restartWithoutBlankScreenFix(); - } else { - restartWithBlankScreenFix(); - } + + const { response } = await dialog.showMessageBox( + mainWindow, + blankScreenFixDialogOptions + ); + if (response === 0) { + if (isBlankScreenFixActive) { + restartWithoutBlankScreenFix(); + } else { + restartWithBlankScreenFix(); } - item.checked = isBlankScreenFixActive; - }); + } + item.checked = isBlankScreenFixActive; }; const menuActions = { openAboutDialog, openDaedalusDiagnosticsDialog, - openBlockConsolidationStatusDialog, + openSettingsPage, + openWalletSettingsPage, toggleBlankScreenFix, }; diff --git a/source/main/utils/config.js b/source/main/utils/config.js index 356752e4ce..096eae5e6a 100644 --- a/source/main/utils/config.js +++ b/source/main/utils/config.js @@ -3,6 +3,43 @@ import { readFileSync } from 'fs'; import yamljs from 'yamljs'; import type { LauncherConfig } from '../config'; +function recurseReplace(obj) { + if (Array.isArray(obj)) { + const out = []; + for (let idx in obj) { + if (Object.prototype.hasOwnProperty.call(obj, idx)) { + idx = parseInt(idx, 10); + out[idx] = recurseReplace(obj[idx]); + } + } + return out; + } + if (obj === null) return null; + switch (typeof obj) { + case 'string': { + return obj.replace(/\${([^}]+)}/g, (a, b) => { + if (process.env[b]) { + return process.env[b]; + } + // eslint-disable-next-line no-console + console.log('readLauncherConfig: warning var undefined:', b); + return ''; + }); + } + case 'object': { + const out = {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + out[key] = recurseReplace(obj[key]); + } + } + return out; + } + default: + return obj; + } +} + /** * Reads and parses the launcher config yaml file on given path. * @param configPath {String} @@ -10,13 +47,11 @@ import type { LauncherConfig } from '../config'; */ 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]; - } - // eslint-disable-next-line no-console - console.log('readLauncherConfig: warning var undefined:', b); - return ''; - }); - return yamljs.parse(finalYaml); + const parsed = yamljs.parse(inputYaml); + const finalYaml = recurseReplace(parsed); + if (finalYaml === null || finalYaml === []) { + throw new Error('Daedalus requires a valid launcher config file to work'); + } + // $FlowFixMe + return finalYaml; }; diff --git a/source/main/utils/detectSystemLocale.js b/source/main/utils/detectSystemLocale.js index fc0afdb1dc..95802be4ae 100644 --- a/source/main/utils/detectSystemLocale.js +++ b/source/main/utils/detectSystemLocale.js @@ -1,11 +1,11 @@ // @flow import { app } from 'electron'; -import { Logger } from './logging'; +import { logger } from './logging'; import { LOCALES } from '../../common/types/locales.types.js'; export const detectSystemLocale = (): string => { const systemLocale = app.getLocale(); - Logger.info('Detected system locale', { systemLocale }); + logger.info('Detected system locale', { systemLocale }); if (systemLocale === 'ja') { return LOCALES.japanese; } diff --git a/source/main/utils/getNumberOfEpochsConsolidated.js b/source/main/utils/getNumberOfEpochsConsolidated.js deleted file mode 100644 index 7f2dc61a69..0000000000 --- a/source/main/utils/getNumberOfEpochsConsolidated.js +++ /dev/null @@ -1,24 +0,0 @@ -// @flow -import fs from 'fs'; -import path from 'path'; -import { nodeDbPath } from '../config'; -import { getNumberOfEpochsConsolidatedChannel } from '../ipc/getNumberOfEpochsConsolidated.ipc'; -import type { GetConsolidatedEpochsCountMainResponse } from '../../common/ipc/api'; - -export const getNumberOfEpochsConsolidated = () => { - getNumberOfEpochsConsolidatedChannel.onRequest( - (): Promise => { - const epochsPath = path.join(nodeDbPath, 'epochs'); - let latestConsolidatedEpoch = 0; - if (fs.existsSync(epochsPath)) { - const epochfiles = fs - .readdirSync(epochsPath) - .filter(file => file.indexOf('.epoch') > -1) - .map(file => parseInt(file.split('.').shift(), 10)); - if (epochfiles.length) - latestConsolidatedEpoch = Math.max(...epochfiles); - } - return Promise.resolve(latestConsolidatedEpoch); - } - ); -}; diff --git a/source/main/utils/handleDiskSpace.js b/source/main/utils/handleDiskSpace.js index 2999ca744b..2ea5c32b96 100644 --- a/source/main/utils/handleDiskSpace.js +++ b/source/main/utils/handleDiskSpace.js @@ -3,7 +3,7 @@ import { BrowserWindow } from 'electron'; import checkDiskSpace from 'check-disk-space'; import prettysize from 'prettysize'; import { getDiskSpaceStatusChannel } from '../ipc/get-disk-space-status'; -import { Logger } from './logging'; +import { logger } from './logging'; import { DISK_SPACE_REQUIRED, DISK_SPACE_REQUIRED_MARGIN_PERCENTAGE, @@ -73,7 +73,7 @@ export const handleDiskSpace = ( diskSpaceAvailable: prettysize(diskSpaceAvailable), }; if (isNotEnoughDiskSpace) - Logger.info('Not enough disk space', { response }); + logger.info('Not enough disk space', { response }); if (typeof onCheckDiskSpace === 'function') onCheckDiskSpace(response); getDiskSpaceStatusChannel.send(response, mainWindow.webContents); return response; diff --git a/source/main/utils/logging.js b/source/main/utils/logging.js index 1ab7958f2d..99a33b88f9 100644 --- a/source/main/utils/logging.js +++ b/source/main/utils/logging.js @@ -2,7 +2,10 @@ import log from 'electron-log-daedalus'; import { environment } from '../environment'; import { formatContext } from '../../common/utils/logging'; -import type { FormatMessageContextParams } from '../../common/types/logging.types'; +import type { + FormatMessageContextParams, + Logger, +} from '../../common/types/logging.types'; const appName = 'daedalus'; const electronProcess = 'ipcMain'; @@ -29,7 +32,7 @@ const logToLevel = (level: string) => (message: string, data: ?Object) => environmentData, }); -export const Logger = { +export const logger: Logger = { debug: logToLevel('debug'), info: logToLevel('info'), error: logToLevel('error'), diff --git a/source/main/utils/mainErrorHandler.js b/source/main/utils/mainErrorHandler.js index afc283e0f0..77f44a3229 100644 --- a/source/main/utils/mainErrorHandler.js +++ b/source/main/utils/mainErrorHandler.js @@ -1,25 +1,27 @@ // @flow import { app } from 'electron'; -import unhandled from 'electron-unhandled'; -import { Logger } from './logging'; +import { logger } from './logging'; import { stringifyError } from '../../common/utils/logging'; export default (onError?: Function) => { - Logger.info('Main Error Handler started'); + logger.info('Main Error Handler started'); - unhandled({ - logger: (error: any) => Logger.error('unhandledException::main', { error }), - showDialog: false, - }); - - process.on('uncaughtException', (error: any) => { + const handleError = (title: string, error: any) => { const err = `${stringifyError(error)}`; - Logger.error('uncaughtException', { error }); + logger.error(title, { error }); if (typeof onError === 'function') onError(err); + }; + + process.on('uncaughtException', (error: any) => { + handleError('uncaughtException', error); + }); + + process.on('unhandledRejection', (error: any) => { + handleError('unhandledRejection', error); }); app.on('gpu-process-crashed', (event: any, killed: boolean) => { - Logger.error( + logger.error( `uncaughtException::gpu-process-crashed: ${ killed ? 'killed' : 'not-killed' }`, diff --git a/source/main/utils/pdf.js b/source/main/utils/pdf.js new file mode 100644 index 0000000000..780fcda59c --- /dev/null +++ b/source/main/utils/pdf.js @@ -0,0 +1,7 @@ +import PDFDocument from 'pdfkit'; + +const doc = new PDFDocument(); +export const getHeightOfString = (text, font, fontSize) => { + doc.font(font).fontSize(fontSize); + return doc.heightOfString(text); +}; diff --git a/source/main/utils/processes.js b/source/main/utils/processes.js index 436b4102c5..bbd17db447 100644 --- a/source/main/utils/processes.js +++ b/source/main/utils/processes.js @@ -1,7 +1,7 @@ // @flow import psList from 'ps-list'; import { isObject } from 'lodash'; -import { Logger } from './logging'; +import { logger } from './logging'; /* eslint-disable consistent-return */ @@ -55,7 +55,7 @@ export const getProcess = async ( return previousProcess; } } catch (error) { - Logger.error('getProcess error', { error }); + logger.error('getProcess error', { error }); return null; } }; diff --git a/source/main/utils/rendererErrorHandler.js b/source/main/utils/rendererErrorHandler.js index 1d00de28df..3c1adf4a64 100644 --- a/source/main/utils/rendererErrorHandler.js +++ b/source/main/utils/rendererErrorHandler.js @@ -1,13 +1,6 @@ // @flow import { BrowserWindow } from 'electron'; -import unhandled from 'electron-unhandled'; -import { Logger } from './logging'; - -unhandled({ - logger: (error: any) => - Logger.error('unhandledException::renderer', { error }), - showDialog: false, -}); +import { logger } from './logging'; export default class RendererErrorHandler { count: number = 0; @@ -18,10 +11,11 @@ export default class RendererErrorHandler { setup(window: BrowserWindow, createMainWindow: Function) { this.window = window; this.createMainWindow = createMainWindow; + logger.info('Renderer Error Handler started'); } onError(errorType: string, error: any) { - Logger.error(`RendererError::${errorType}`, { error }); + logger.error(`RendererError::${errorType}`, { error }); if (this.count < this.maxReloads) { this.count++; diff --git a/source/main/utils/setupLogging.js b/source/main/utils/setupLogging.js index 9372eb04ba..79cd657ecd 100644 --- a/source/main/utils/setupLogging.js +++ b/source/main/utils/setupLogging.js @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; import log from 'electron-log-daedalus'; import ensureDirectoryExists from './ensureDirectoryExists'; -import { pubLogsFolderPath, appLogsFolderPath, APP_NAME } from '../config'; +import { pubLogsFolderPath, appLogsFolderPath } from '../config'; import { constructMessageBody, formatMessage, @@ -15,13 +15,14 @@ import type { MessageBody, LogSystemInfoParams, StateSnapshotLogParams, + WalletMigrationReportData, } from '../../common/types/logging.types'; const isTest = process.env.NODE_ENV === 'test'; const isDev = process.env.NODE_ENV === 'development'; export const setupLogging = () => { - const logFilePath = path.join(pubLogsFolderPath, `${APP_NAME}.json`); + const logFilePath = path.join(pubLogsFolderPath, 'Daedalus.json'); ensureDirectoryExists(pubLogsFolderPath); log.transports.console.level = isTest ? 'error' : 'info'; log.transports.rendererConsole.level = isDev ? 'info' : 'error'; @@ -115,10 +116,12 @@ export const logStateSnapshot = ( daedalusProcessID, daedalusMainProcessID, isBlankScreenFixActive, - cardanoVersion, + cardanoNodeVersion, cardanoNetwork, - cardanoProcessID, - cardanoAPIPort, + cardanoNodePID, + cardanoWalletVersion, + cardanoWalletPID, + cardanoWalletApiPort, daedalusStateDirectoryPath, } = coreInfo; const env = `${cardanoNetwork}:${platform}:${platformVersion}`; @@ -139,10 +142,12 @@ export const logStateSnapshot = ( daedalusProcessID, daedalusMainProcessID, isBlankScreenFixActive, - cardanoVersion, cardanoNetwork, - cardanoProcessID, - cardanoAPIPort, + cardanoNodeVersion, + cardanoNodePID, + cardanoWalletVersion, + cardanoWalletPID, + cardanoWalletApiPort, daedalusStateDirectoryPath, data, }; @@ -154,3 +159,17 @@ export const logStateSnapshot = ( fs.writeFileSync(stateSnapshotFilePath, JSON.stringify(messageBody)); return messageBody; }; + +export const generateWalletMigrationReport = ( + data: WalletMigrationReportData +) => { + const walletMigrationrReportFilePath = path.join( + pubLogsFolderPath, + 'Wallet-migration-report.json' + ); + const generatedAt = new Date().toISOString(); + fs.writeFileSync( + walletMigrationrReportFilePath, + JSON.stringify({ ...data, generatedAt }) + ); +}; diff --git a/source/main/windows/main.js b/source/main/windows/main.js index 75913e10e8..cf24fe0527 100644 --- a/source/main/windows/main.js +++ b/source/main/windows/main.js @@ -6,17 +6,11 @@ import ipcApi from '../ipc'; import RendererErrorHandler from '../utils/rendererErrorHandler'; import { getTranslation } from '../utils/getTranslation'; import { getContentMinimumSize } from '../utils/getContentMinimumSize'; -import { launcherConfig } from '../config'; +import { buildLabel, launcherConfig } from '../config'; const rendererErrorHandler = new RendererErrorHandler(); -const { - isDev, - isTest, - buildLabel, - isLinux, - isBlankScreenFixActive, -} = environment; +const { isDev, isTest, isLinux, isBlankScreenFixActive } = environment; const id = 'window'; @@ -57,7 +51,7 @@ export const createMainWindow = (locale: string) => { }; if (isLinux) { - windowOptions.icon = path.join(launcherConfig.statePath, 'icon.png'); + windowOptions.icon = path.join(launcherConfig.stateDir, 'icon.png'); } // Construct new BrowserWindow @@ -69,7 +63,7 @@ export const createMainWindow = (locale: string) => { window.setMinimumSize(minWindowsWidth, minWindowsHeight); // Initialize our ipc api methods that can be called by the render processes - ipcApi({ window }); + ipcApi(window); // Provide render process with an api to resize the main window ipcMain.on('resize-window', (event, { width, height, animate }) => { diff --git a/source/renderer/app/App.js b/source/renderer/app/App.js index a463279881..a1d8fbc155 100755 --- a/source/renderer/app/App.js +++ b/source/renderer/app/App.js @@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react'; import { Provider, observer } from 'mobx-react'; import { ThemeProvider } from 'react-polymorph/lib/components/ThemeProvider'; +import { SimpleSkins } from 'react-polymorph/lib/skins/simple'; import DevTools from 'mobx-react-devtools'; import { Router } from 'react-router'; import { IntlProvider } from 'react-intl'; @@ -12,8 +13,7 @@ import translations from './i18n/translations'; import ThemeManager from './ThemeManager'; import AboutDialog from './containers/static/AboutDialog'; import DaedalusDiagnosticsDialog from './containers/status/DaedalusDiagnosticsDialog'; -import BlockConsolidationStatusDialog from './containers/status/BlockConsolidationStatusDialog'; -import GenericNotificationContainer from './containers/notifications/GenericNotificationContainer'; +import NotificationsContainer from './containers/notifications/NotificationsContainer'; import AutomaticUpdateNotificationDialog from './containers/notifications/AutomaticUpdateNotificationDialog'; import NewsOverlayContainer from './containers/news/NewsOverlayContainer'; import { DIALOGS } from '../../common/ipc/constants'; @@ -34,31 +34,19 @@ export default class App extends Component<{ render() { const { stores, actions, history } = this.props; const { app, nodeUpdate, networkStatus } = stores; - const { - showNextUpdate, - isNewAppVersionAvailable, - isUpdatePostponed, - isUpdateAvailable, - } = nodeUpdate; + const { showManualUpdate, showNextUpdate } = nodeUpdate; const { isActiveDialog, isSetupPage } = app; const { isNodeStopping, isNodeStopped } = networkStatus; const locale = stores.profile.currentLocale; const mobxDevTools = global.environment.mobxDevTools ? : null; const { currentTheme } = stores.profile; const themeVars = require(`./themes/daedalus/${currentTheme}.js`).default; - const { ABOUT, BLOCK_CONSOLIDATION, DAEDALUS_DIAGNOSTICS } = DIALOGS; - - const isManualUpdateAvailable = - isNewAppVersionAvailable && - !isNodeStopping && - !isNodeStopped && - !isUpdatePostponed && - !isUpdateAvailable; + const { ABOUT, DAEDALUS_DIAGNOSTICS } = DIALOGS; const canShowNews = !isSetupPage && // Active page is not "Language Selection" or "Terms of Use" !showNextUpdate && // Autmatic update not available - !isManualUpdateAvailable && // Manual update not available + !showManualUpdate && // Manual update not available !isNodeStopping && // Daedalus is not shutting down !isNodeStopped; // Daedalus is not shutting down @@ -66,7 +54,11 @@ export default class App extends Component<{ - + @@ -77,14 +69,11 @@ export default class App extends Component<{ ) : ( [ - isActiveDialog(ABOUT) && , - isActiveDialog(BLOCK_CONSOLIDATION) && ( - - ), + isActiveDialog(ABOUT) && , isActiveDialog(DAEDALUS_DIAGNOSTICS) && ( - + ), - , + , ] )} {canShowNews && [ diff --git a/source/renderer/app/Routes.js b/source/renderer/app/Routes.js index 0880aed7d4..264da22442 100644 --- a/source/renderer/app/Routes.js +++ b/source/renderer/app/Routes.js @@ -5,7 +5,7 @@ import { ROUTES } from './routes-config'; // PAGES import Root from './containers/Root'; -import LanguageSelectionPage from './containers/profile/LanguageSelectionPage'; +import InitialSettingsPage from './containers/profile/InitialSettingsPage'; import Settings from './containers/settings/Settings'; import GeneralSettingsPage from './containers/settings/categories/GeneralSettingsPage'; import SupportSettingsPage from './containers/settings/categories/SupportSettingsPage'; @@ -34,8 +34,8 @@ export const Routes = ( {this.props.children}; } } diff --git a/source/renderer/app/WindowSizeManager.js b/source/renderer/app/WindowSizeManager.js index cdfa7563b0..745ad8630c 100644 --- a/source/renderer/app/WindowSizeManager.js +++ b/source/renderer/app/WindowSizeManager.js @@ -1,8 +1,8 @@ +/* eslint-disable react/prop-types */ import React, { Component, Fragment } from 'react'; export default class WindowSizeManager extends Component { componentDidMount() { - // eslint-disable-next-line react/prop-types this.updateMinScreenHeight(this.props.minScreenHeight); } @@ -20,7 +20,6 @@ export default class WindowSizeManager extends Component { } render() { - // eslint-disable-next-line react/prop-types return {this.props.children}; } } diff --git a/source/renderer/app/actions/addresses-actions.js b/source/renderer/app/actions/addresses-actions.js index f35aecf09e..55cd998691 100644 --- a/source/renderer/app/actions/addresses-actions.js +++ b/source/renderer/app/actions/addresses-actions.js @@ -4,9 +4,9 @@ import Action from './lib/Action'; // ======= ADDRESSES ACTIONS ======= export default class AddressesActions { - createAddress: Action<{ + createByronWalletAddress: Action<{ walletId: string, - spendingPassword: ?string, + passphrase: string, }> = new Action(); resetErrors: Action = new Action(); } diff --git a/source/renderer/app/actions/app-actions.js b/source/renderer/app/actions/app-actions.js index 91b6258084..6124ee760d 100644 --- a/source/renderer/app/actions/app-actions.js +++ b/source/renderer/app/actions/app-actions.js @@ -7,17 +7,14 @@ export default class AppActions { downloadLogs: Action = new Action(); getGpuStatus: Action = new Action(); initAppEnvironment: Action = new Action(); - setNotificationVisibility: Action = new Action(); + setIsDownloadingLogs: Action = new Action(); toggleNewsFeed: Action = new Action(); + closeNewsFeed: Action = new Action(); // About dialog actions closeAboutDialog: Action = new Action(); openAboutDialog: Action = new Action(); - // Block Consolidation dialog actions - closeBlockConsolidationStatusDialog: Action = new Action(); - openBlockConsolidationStatusDialog: Action = new Action(); - // Daedalus Diagnostics dialog actions closeDaedalusDiagnosticsDialog: Action = new Action(); openDaedalusDiagnosticsDialog: Action = new Action(); diff --git a/source/renderer/app/actions/block-consolidation-actions.js b/source/renderer/app/actions/block-consolidation-actions.js deleted file mode 100644 index fdf45c41cc..0000000000 --- a/source/renderer/app/actions/block-consolidation-actions.js +++ /dev/null @@ -1,9 +0,0 @@ -// @flow -import Action from './lib/Action'; - -// ======= NETWORK STATUS ACTIONS ======= - -export default class BlockConsolidationActions { - startBlockConsolidationDataPolling: Action = new Action(); - stopBlockConsolidationDataPolling: Action = new Action(); -} diff --git a/source/renderer/app/actions/index.js b/source/renderer/app/actions/index.js index 434fb8ef87..30b00f4785 100644 --- a/source/renderer/app/actions/index.js +++ b/source/renderer/app/actions/index.js @@ -1,7 +1,6 @@ // @flow import AddressesActions from './addresses-actions'; import AppActions from './app-actions'; -import BlockConsolidationActions from './block-consolidation-actions'; import DialogsActions from './dialogs-actions'; import NetworkStatusActions from './network-status-actions'; import NodeUpdateActions from './node-update-actions'; @@ -13,13 +12,13 @@ import StakingActions from './staking-actions'; import TransactionsActions from './transactions-actions'; import WalletsActions from './wallets-actions'; import WalletBackupActions from './wallet-backup-actions'; +import WalletMigrationActions from './wallet-migration-actions'; import WalletSettingsActions from './wallet-settings-actions'; import WindowActions from './window-actions'; export type ActionsMap = { addresses: AddressesActions, app: AppActions, - blockConsolidation: BlockConsolidationActions, dialogs: DialogsActions, networkStatus: NetworkStatusActions, nodeUpdate: NodeUpdateActions, @@ -31,6 +30,7 @@ export type ActionsMap = { transactions: TransactionsActions, wallets: WalletsActions, walletBackup: WalletBackupActions, + walletMigration: WalletMigrationActions, walletSettings: WalletSettingsActions, window: WindowActions, }; @@ -38,7 +38,6 @@ export type ActionsMap = { const actionsMap: ActionsMap = { addresses: new AddressesActions(), app: new AppActions(), - blockConsolidation: new BlockConsolidationActions(), dialogs: new DialogsActions(), networkStatus: new NetworkStatusActions(), nodeUpdate: new NodeUpdateActions(), @@ -50,6 +49,7 @@ const actionsMap: ActionsMap = { transactions: new TransactionsActions(), wallets: new WalletsActions(), walletBackup: new WalletBackupActions(), + walletMigration: new WalletMigrationActions(), walletSettings: new WalletSettingsActions(), window: new WindowActions(), }; diff --git a/source/renderer/app/actions/network-status-actions.js b/source/renderer/app/actions/network-status-actions.js index 2efd2ff3ee..81fd8cf1a3 100644 --- a/source/renderer/app/actions/network-status-actions.js +++ b/source/renderer/app/actions/network-status-actions.js @@ -7,4 +7,7 @@ export default class NetworkStatusActions { isSyncedAndReady: Action = new Action(); tlsConfigIsReady: Action = new Action(); restartNode: Action = new Action(); + toggleSplash: Action = new Action(); + copyStateDirectoryPath: Action = new Action(); + forceCheckNetworkClock: Action = new Action(); } diff --git a/source/renderer/app/actions/notifications-actions.js b/source/renderer/app/actions/notifications-actions.js index 991f28b254..58576e3153 100644 --- a/source/renderer/app/actions/notifications-actions.js +++ b/source/renderer/app/actions/notifications-actions.js @@ -1,11 +1,14 @@ // @flow import Action from './lib/Action'; +import type { + NotificationConfig, + NotificationId, +} from '../types/notificationTypes'; // ======= NOTIFICATIONS ACTIONS ======= export default class NotificationsActions { - open: Action<{ id: string, duration?: number }> = new Action(); - updateDataForActiveNotification: Action<{ data: Object }> = new Action(); - closeActiveNotification: Action<{ id: string }> = new Action(); - resetActiveNotification: Action = new Action(); + registerNotification: Action = new Action(); + closeActiveNotification: Action = new Action(); + closeNotification: Action<{ id: NotificationId }> = new Action(); } diff --git a/source/renderer/app/actions/profile-actions.js b/source/renderer/app/actions/profile-actions.js index 77607e7c59..73cae4c230 100644 --- a/source/renderer/app/actions/profile-actions.js +++ b/source/renderer/app/actions/profile-actions.js @@ -15,6 +15,10 @@ export default class ProfileActions { fresh?: boolean, }> = new Action(); downloadLogsSuccess: Action = new Action(); - updateLocale: Action<{ locale: string }> = new Action(); + updateUserLocalSetting: Action<{ + param: string, + value?: string, + }> = new Action(); updateTheme: Action<{ theme: string }> = new Action(); + finishInitialScreenSettings: Action = new Action(); } diff --git a/source/renderer/app/actions/staking-actions.js b/source/renderer/app/actions/staking-actions.js index 978b970bf4..a828c60a91 100644 --- a/source/renderer/app/actions/staking-actions.js +++ b/source/renderer/app/actions/staking-actions.js @@ -1,8 +1,15 @@ // @flow import Action from './lib/Action'; - +import type { + JoinStakePoolRequest, + QuitStakePoolRequest, +} from '../api/staking/types'; // ======= STAKING ACTIONS ======= export default class StakingActions { - goToStakingPage: Action = new Action(); + fakeStakePoolsLoading: Action = new Action(); + goToStakingInfoPage: Action = new Action(); + goToStakingDelegationCenterPage: Action = new Action(); + joinStakePool: Action = new Action(); + quitStakePool: Action = new Action(); } diff --git a/source/renderer/app/actions/transactions-actions.js b/source/renderer/app/actions/transactions-actions.js index 5a9bfc407a..a90be14a0d 100644 --- a/source/renderer/app/actions/transactions-actions.js +++ b/source/renderer/app/actions/transactions-actions.js @@ -1,9 +1,10 @@ // @flow import Action from './lib/Action'; +import type { TransactionFilterOptionsType } from '../stores/TransactionsStore'; // ======= TRANSACTIONS ACTIONS ======= export default class TransactionsActions { - filterTransactions: Action<{ searchTerm: string }> = new Action(); + filterTransactions: Action = new Action(); loadMoreTransactions: Action = new Action(); } diff --git a/source/renderer/app/actions/wallet-backup-actions.js b/source/renderer/app/actions/wallet-backup-actions.js index 49be9682f1..f2c584cbac 100644 --- a/source/renderer/app/actions/wallet-backup-actions.js +++ b/source/renderer/app/actions/wallet-backup-actions.js @@ -15,8 +15,9 @@ export default class WalletBackupActions { index: number, }> = new Action(); clearEnteredRecoveryPhrase: Action = new Action(); - acceptWalletBackupTermDevice: Action = new Action(); + acceptWalletBackupTermOffline: Action = new Action(); acceptWalletBackupTermRecovery: Action = new Action(); + acceptWalletBackupTermRewards: Action = new Action(); restartWalletBackup: Action = new Action(); cancelWalletBackup: Action = new Action(); finishWalletBackup: Action = new Action(); diff --git a/source/renderer/app/actions/wallet-migration-actions.js b/source/renderer/app/actions/wallet-migration-actions.js new file mode 100644 index 0000000000..43de8a5089 --- /dev/null +++ b/source/renderer/app/actions/wallet-migration-actions.js @@ -0,0 +1,16 @@ +// @flow +import Action from './lib/Action'; +import type { ImportFromOption } from '../types/walletExportTypes'; + +export default class WalletMigrationActions { + startMigration: Action = new Action(); + finishMigration: Action = new Action(); + resetMigration: Action = new Action(); + toggleWalletImportSelection: Action<{ index: number }> = new Action(); + updateWalletName: Action<{ index: number, name: string }> = new Action(); + nextStep: Action = new Action(); + selectExportSourcePath: Action<{ + importFrom: ImportFromOption, + }> = new Action(); + resetExportSourcePath: Action = new Action(); +} diff --git a/source/renderer/app/actions/wallet-settings-actions.js b/source/renderer/app/actions/wallet-settings-actions.js index 9b412d164a..b975beac5f 100644 --- a/source/renderer/app/actions/wallet-settings-actions.js +++ b/source/renderer/app/actions/wallet-settings-actions.js @@ -13,13 +13,17 @@ export default class WalletSettingsActions { startEditingWalletField: Action<{ field: string }> = new Action(); stopEditingWalletField: Action = new Action(); updateWalletField: Action<{ field: string, value: string }> = new Action(); - // eslint-disable-next-line max-len updateSpendingPassword: Action<{ walletId: string, - oldPassword: ?string, - newPassword: ?string, + oldPassword: string, + newPassword: string, + isLegacy: boolean, }> = new Action(); exportToFile: Action = new Action(); startWalletUtxoPolling: Action = new Action(); stopWalletUtxoPolling: Action = new Action(); + forceWalletResync: Action<{ + walletId: string, + isLegacy: boolean, + }> = new Action(); } diff --git a/source/renderer/app/actions/wallets-actions.js b/source/renderer/app/actions/wallets-actions.js index a549235c42..a4271d0ae5 100644 --- a/source/renderer/app/actions/wallets-actions.js +++ b/source/renderer/app/actions/wallets-actions.js @@ -1,47 +1,92 @@ // @flow import Action from './lib/Action'; -import type { walletExportTypeChoices } from '../types/walletExportTypes'; +import type { WalletExportTypeChoices } from '../types/walletExportTypes'; +import type { CsvRecord } from '../../../common/types/rewards-csv-request.types'; export type WalletImportFromFileParams = { filePath: string, walletName: ?string, - spendingPassword: ?string, + spendingPassword: string, }; // ======= WALLET ACTIONS ======= export default class WalletsActions { - // Create Wallet + /* ---------- Create Wallet ---------- */ createWallet: Action<{ name: string, - spendingPassword: ?string, + spendingPassword: string, }> = new Action(); createWalletBegin: Action = new Action(); createWalletChangeStep: Action = new Action(); createWalletClose: Action = new Action(); createWalletAbort: Action = new Action(); - // --- - restoreWallet: Action<{ - recoveryPhrase: string, - walletName: string, - spendingPassword: ?string, - type?: string, + + /* ---------- Restore Wallet ---------- */ + restoreWalletBegin: Action = new Action(); + restoreWalletEnd: Action = new Action(); + restoreWalletChangeStep: Action = new Action(); + restoreWalletClose: Action = new Action(); + restoreWalletCancelClose: Action = new Action(); + restoreWalletSetKind: Action<{ param?: string, kind: string }> = new Action(); + restoreWalletSetMnemonics: Action<{ + mnemonics: Array, + }> = new Action(); + restoreWalletSetConfig: Action<{ + param: string, + value: string, }> = new Action(); + + restoreWallet: Action = new Action(); importWalletFromFile: Action = new Action(); - deleteWallet: Action<{ walletId: string }> = new Action(); + deleteWallet: Action<{ walletId: string, isLegacy: boolean }> = new Action(); + undelegateWallet: Action<{ + walletId: string, + stakePoolId: string, + passphrase: string, + }> = new Action(); + setUndelegateWalletSubmissionSuccess: Action<{ + result: boolean, + }> = new Action(); sendMoney: Action<{ receiver: string, amount: string, - password: ?string, + passphrase: string, }> = new Action(); chooseWalletExportType: Action<{ - walletExportType: walletExportTypeChoices, + walletExportType: WalletExportTypeChoices, }> = new Action(); generateCertificate: Action<{ filePath: string }> = new Action(); + generateRewardsCsv: Action<{ + filePath: string, + rewards: Array, + }> = new Action(); + generateAddressPDF: Action<{ + address: string, + note: string, + filePath: string, + }> = new Action(); + copyAddress: Action<{ address: string }> = new Action(); updateCertificateStep: Action = new Action(); closeCertificateGeneration: Action = new Action(); + closeRewardsCsvGeneration: Action = new Action(); setCertificateTemplate: Action<{ selectedTemplate: string }> = new Action(); finishCertificate: Action = new Action(); + finishRewardsCsv: Action = new Action(); updateWalletLocalData: Action = new Action(); updateRecoveryPhraseVerificationDate: Action = new Action(); + + /* ---------- Transfer Funds ---------- */ + transferFundsNextStep: Action = new Action(); + transferFundsPrevStep: Action = new Action(); + transferFundsSetSourceWalletId: Action<{ + sourceWalletId: string, + }> = new Action(); + transferFundsSetTargetWalletId: Action<{ + targetWalletId: string, + }> = new Action(); + transferFundsRedeem: Action = new Action(); + transferFundsClose: Action = new Action(); + transferFundsCalculateFee: Action<{ sourceWalletId: string }> = new Action(); + transferFunds: Action<{ spendingPassword: string }> = new Action(); } diff --git a/source/renderer/app/api/accounts/requests/getAccounts.js b/source/renderer/app/api/accounts/requests/getAccounts.js deleted file mode 100644 index 074ee30dfc..0000000000 --- a/source/renderer/app/api/accounts/requests/getAccounts.js +++ /dev/null @@ -1,18 +0,0 @@ -// @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({ - 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 deleted file mode 100644 index ccaa13cb4a..0000000000 --- a/source/renderer/app/api/accounts/types.js +++ /dev/null @@ -1,12 +0,0 @@ -// @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/addresses/requests/createAddress.js b/source/renderer/app/api/addresses/requests/createAddress.js deleted file mode 100644 index 15f47ddf71..0000000000 --- a/source/renderer/app/api/addresses/requests/createAddress.js +++ /dev/null @@ -1,24 +0,0 @@ -// @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( - { - method: 'POST', - path: '/api/v1/addresses', - ...config, - }, - {}, - { spendingPassword, accountIndex, walletId } - ); diff --git a/source/renderer/app/api/addresses/requests/createByronWalletAddress.js b/source/renderer/app/api/addresses/requests/createByronWalletAddress.js new file mode 100644 index 0000000000..65ffcd4ef6 --- /dev/null +++ b/source/renderer/app/api/addresses/requests/createByronWalletAddress.js @@ -0,0 +1,28 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Address } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export type CreateAddressParams = { + walletId: string, + passphrase: string, + addressIndex?: number, +}; + +export const createByronWalletAddress = ( + config: RequestConfig, + { passphrase, addressIndex, walletId }: CreateAddressParams +): Promise
=> { + let data = { passphrase }; + data = addressIndex ? { ...data, address_index: addressIndex } : data; + return request( + { + method: 'POST', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}/addresses`, + ...config, + }, + {}, + data + ); +}; diff --git a/source/renderer/app/api/addresses/requests/getAddresses.js b/source/renderer/app/api/addresses/requests/getAddresses.js new file mode 100644 index 0000000000..1657d55e6b --- /dev/null +++ b/source/renderer/app/api/addresses/requests/getAddresses.js @@ -0,0 +1,18 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Addresses, GetAddressesRequestQueryParams } from '../types'; +import { request } from '../../utils/request'; + +export const getAddresses = ( + config: RequestConfig, + walletId: string, + queryParams?: GetAddressesRequestQueryParams +): Promise => + request( + { + method: 'GET', + path: `/v2/wallets/${walletId}/addresses`, + ...config, + }, + queryParams + ); diff --git a/source/renderer/app/api/addresses/requests/getByronWalletAddresses.js b/source/renderer/app/api/addresses/requests/getByronWalletAddresses.js new file mode 100644 index 0000000000..4011c2b460 --- /dev/null +++ b/source/renderer/app/api/addresses/requests/getByronWalletAddresses.js @@ -0,0 +1,19 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Addresses, GetAddressesRequestQueryParams } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const getByronWalletAddresses = ( + config: RequestConfig, + walletId: string, + queryParams?: GetAddressesRequestQueryParams +): Promise => + request( + { + method: 'GET', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}/addresses`, + ...config, + }, + queryParams + ); diff --git a/source/renderer/app/api/addresses/types.js b/source/renderer/app/api/addresses/types.js index fcfbacb0c7..9fa53ee197 100644 --- a/source/renderer/app/api/addresses/types.js +++ b/source/renderer/app/api/addresses/types.js @@ -1,24 +1,27 @@ // @flow +export type AddressState = 'used' | 'unused'; + +export type GetAddressesRequestQueryParams = { + state: AddressState, +}; + export type Address = { id: string, - used: boolean, - changeAddress: boolean, + state: AddressState, }; export type Addresses = Array
; -// req/res Address types -export type GetAddressesResponse = { - accountIndex: ?number, - addresses: Addresses, -}; - export type GetAddressesRequest = { walletId: string, + isLegacy: boolean, + queryParams?: GetAddressesRequestQueryParams, }; -export type CreateAddressRequest = { - spendingPassword: ?string, - accountIndex: number, +// Byron related types + +export type CreateByronWalletAddressRequest = { walletId: string, + passphrase: string, + addressIndex?: number, }; diff --git a/source/renderer/app/api/api.js b/source/renderer/app/api/api.js index e9b9ca2fb1..1873b29e83 100644 --- a/source/renderer/app/api/api.js +++ b/source/renderer/app/api/api.js @@ -1,64 +1,95 @@ // @flow -import { split, get, unionBy } from 'lodash'; +import { split, get, map, last } from 'lodash'; import { action } from 'mobx'; import BigNumber from 'bignumber.js'; import moment from 'moment'; // domains -import Wallet from '../domains/Wallet'; +import Wallet, { + WalletDelegationStatuses, + WalletUnits, +} from '../domains/Wallet'; import { WalletTransaction, - transactionTypes, + TransactionTypes, + TransactionStates, } 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'; +import { getAddresses } from './addresses/requests/getAddresses'; +import { getByronWalletAddresses } from './addresses/requests/getByronWalletAddresses'; +import { createByronWalletAddress } from './addresses/requests/createByronWalletAddress'; + +// Network requests +import { getNetworkInfo } from './network/requests/getNetworkInfo'; +import { getNetworkClock } from './network/requests/getNetworkClock'; +import { getNetworkParameters } from './network/requests/getNetworkParameters'; // Nodes requests import { applyNodeUpdate } from './nodes/requests/applyNodeUpdate'; -import { getNodeInfo } from './nodes/requests/getNodeInfo'; -import { getNodeSettings } from './nodes/requests/getNodeSettings'; -import { getCurrentEpoch } from './nodes/requests/getCurrentEpoch'; -import { getNextNodeUpdate } from './nodes/requests/getNextNodeUpdate'; +// import { getNextNodeUpdate } from './nodes/requests/getNextNodeUpdate'; import { postponeNodeUpdate } from './nodes/requests/postponeNodeUpdate'; import { getLatestAppVersion } from './nodes/requests/getLatestAppVersion'; // Transactions requests import { getTransactionFee } from './transactions/requests/getTransactionFee'; +import { getByronWalletTransactionFee } from './transactions/requests/getByronWalletTransactionFee'; import { getTransactionHistory } from './transactions/requests/getTransactionHistory'; +import { getLegacyWalletTransactionHistory } from './transactions/requests/getLegacyWalletTransactionHistory'; import { createTransaction } from './transactions/requests/createTransaction'; +import { createByronWalletTransaction } from './transactions/requests/createByronWalletTransaction'; +import { deleteLegacyTransaction } from './transactions/requests/deleteLegacyTransaction'; // Wallets requests -import { resetWalletState } from './wallets/requests/resetWalletState'; -import { changeSpendingPassword } from './wallets/requests/changeSpendingPassword'; +import { updateSpendingPassword } from './wallets/requests/updateSpendingPassword'; +import { updateByronSpendingPassword } from './wallets/requests/updateByronSpendingPassword'; import { deleteWallet } from './wallets/requests/deleteWallet'; +import { deleteLegacyWallet } from './wallets/requests/deleteLegacyWallet'; import { exportWalletAsJSON } from './wallets/requests/exportWalletAsJSON'; import { importWalletAsJSON } from './wallets/requests/importWalletAsJSON'; import { getWallets } from './wallets/requests/getWallets'; +import { getLegacyWallets } from './wallets/requests/getLegacyWallets'; import { importWalletAsKey } from './wallets/requests/importWalletAsKey'; import { createWallet } from './wallets/requests/createWallet'; import { restoreWallet } from './wallets/requests/restoreWallet'; +import { restoreLegacyWallet } from './wallets/requests/restoreLegacyWallet'; +import { restoreByronWallet } from './wallets/requests/restoreByronWallet'; +import { restoreExportedByronWallet } from './wallets/requests/restoreExportedByronWallet'; import { updateWallet } from './wallets/requests/updateWallet'; +import { updateByronWallet } from './wallets/requests/updateByronWallet'; +import { forceWalletResync } from './wallets/requests/forceWalletResync'; +import { forceLegacyWalletResync } from './wallets/requests/forceLegacyWalletResync'; import { getWalletUtxos } from './wallets/requests/getWalletUtxos'; +import { getByronWalletUtxos } from './wallets/requests/getByronWalletUtxos'; +import { getWallet } from './wallets/requests/getWallet'; +import { getLegacyWallet } from './wallets/requests/getLegacyWallet'; import { getWalletIdAndBalance } from './wallets/requests/getWalletIdAndBalance'; +import { transferFundsCalculateFee } from './wallets/requests/transferFundsCalculateFee'; +import { transferFunds } from './wallets/requests/transferFunds'; + +// Staking +import StakePool from '../domains/StakePool'; +import { EPOCH_LENGTH_ITN } from '../config/epochsConfig'; // News requests import { getNews } from './news/requests/getNews'; -// utility functions +// Stake Pools request +import { getStakePools } from './staking/requests/getStakePools'; +import { getDelegationFee } from './staking/requests/getDelegationFee'; +import { joinStakePool } from './staking/requests/joinStakePool'; +import { quitStakePool } from './staking/requests/quitStakePool'; + +// Utility functions +import { wait } from './utils/apiHelpers'; import { awaitUpdateChannel, cardanoFaultInjectionChannel, } from '../ipc/cardano.ipc'; import patchAdaApi from './utils/patchAdaApi'; -import { isValidMnemonic } from '../../../common/crypto/decrypt'; -import { utcStringToDate, encryptPassphrase } from './utils'; -import { Logger } from '../utils/logging'; +import { getLegacyWalletId, utcStringToDate } from './utils'; +import { logger } from '../utils/logging'; import { unscrambleMnemonics, scrambleMnemonics, @@ -67,52 +98,49 @@ import { } from './utils/mnemonics'; import { filterLogData } from '../../../common/utils/logging'; -// config constants -import { - LOVELACES_PER_ADA, - MAX_TRANSACTIONS_PER_PAGE, - MAX_TRANSACTION_CONFIRMATIONS, - TX_AGE_POLLING_THRESHOLD, -} from '../config/numbersConfig'; +// Config constants +import { LOVELACES_PER_ADA } from '../config/numbersConfig'; import { ADA_CERTIFICATE_MNEMONIC_LENGTH, WALLET_RECOVERY_PHRASE_WORD_COUNT, + LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT, } from '../config/cryptoConfig'; - -// Accounts types -import type { Accounts } from './accounts/types'; +import { FORCED_WALLET_RESYNC_WAIT } from '../config/timingConfig'; // Addresses Types import type { Address, GetAddressesRequest, - CreateAddressRequest, - GetAddressesResponse, + CreateByronWalletAddressRequest, } from './addresses/types'; // Common Types import type { RequestConfig } from './common/types'; +// Network Types +import type { + GetNetworkInfoResponse, + NetworkInfoResponse, + GetNetworkClockResponse, + NetworkClockResponse, + GetNetworkParametersResponse, + NetworkParametersResponse, +} from './network/types'; + // Nodes Types import type { - CardanoExplorerResponse, LatestAppVersionInfoResponse, - NodeInfoResponse, - NodeSettingsResponse, NodeSoftware, - GetNetworkStatusResponse, - GetNodeSettingsResponse, - GetCurrentEpochFallbackResponse, GetLatestAppVersionResponse, } from './nodes/types'; -import type { NodeInfoQueryParams } from './nodes/requests/getNodeInfo'; // Transactions Types import type { Transaction, - Transactions, TransactionFee, - TransactionRequest, + GetTransactionFeeRequest, + CreateTransactionRequest, + DeleteTransactionRequest, GetTransactionsRequest, GetTransactionsResponse, } from './transactions/types'; @@ -121,54 +149,57 @@ import type { import type { AdaWallet, AdaWallets, + LegacyAdaWallet, + LegacyAdaWallets, WalletUtxos, WalletIdAndBalance, CreateWalletRequest, DeleteWalletRequest, RestoreWalletRequest, + RestoreLegacyWalletRequest, + RestoreExportedByronWalletRequest, UpdateSpendingPasswordRequest, ExportWalletToFileRequest, GetWalletCertificateRecoveryPhraseRequest, GetWalletRecoveryPhraseFromCertificateRequest, ImportWalletFromKeyRequest, ImportWalletFromFileRequest, - UpdateWalletRequest, + ForceWalletResyncRequest, GetWalletUtxosRequest, + GetWalletRequest, GetWalletIdAndBalanceRequest, GetWalletIdAndBalanceResponse, + TransferFundsCalculateFeeRequest, + TransferFundsCalculateFeeResponse, + TransferFundsRequest, + TransferFundsResponse, + UpdateWalletRequest, } from './wallets/types'; +import type { WalletProps } from '../domains/Wallet'; // News Types import type { GetNewsResponse } from './news/types'; -// Common errors -import { - GenericApiError, - IncorrectSpendingPasswordError, - InvalidMnemonicError, - ForbiddenMnemonicError, -} from './common/errors'; - -// Wallets errors -import { - WalletAlreadyRestoredError, - WalletAlreadyImportedError, - WalletFileImportError, -} from './wallets/errors'; - -// Transactions errors -import { - CanNotCalculateTransactionFeesError, - NotAllowedToSendMoneyToRedeemAddressError, - NotEnoughFundsForTransactionFeesError, - NotEnoughFundsForTransactionError, - NotEnoughMoneyToSendError, - TooBigTransactionError, -} from './transactions/errors'; +// Staking Types +import type { + JoinStakePoolRequest, + GetDelegationFeeRequest, + DelegationFee, + AdaApiStakePools, + AdaApiStakePool, + QuitStakePoolRequest, +} from './staking/types'; +import type { StakePoolProps } from '../domains/StakePool'; import type { FaultInjectionIpcRequest } from '../../../common/types/cardano-node.types'; + import { TlsCertificateNotValidError } from './nodes/errors'; import { getSHA256HexForString } from './utils/hashing'; import { getNewsHash } from './news/requests/getNewsHash'; +import { deleteTransaction } from './transactions/requests/deleteTransaction'; +import { WALLET_BYRON_KINDS } from '../config/walletRestoreConfig'; +import ApiError from '../domains/ApiError'; + +const { isIncentivizedTestnet } = global; export default class AdaApi { config: RequestConfig; @@ -183,566 +214,987 @@ export default class AdaApi { } getWallets = async (): Promise> => { - Logger.debug('AdaApi::getWallets called'); + logger.debug('AdaApi::getWallets called'); try { - const response: AdaWallets = await getWallets(this.config); - Logger.debug('AdaApi::getWallets success', { wallets: response }); - return response.map(data => _createWalletFromServerData(data)); + const wallets: AdaWallets = isIncentivizedTestnet + ? await getWallets(this.config) + : []; + const legacyWallets: LegacyAdaWallets = await getLegacyWallets( + this.config + ); + logger.debug('AdaApi::getWallets success', { wallets, legacyWallets }); + + map(legacyWallets, legacyAdaWallet => { + const extraLegacyWalletProps = { + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + wallets.push({ + ...legacyAdaWallet, + ...extraLegacyWalletProps, + }); + }); + + return wallets.map(_createWalletFromServerData); } catch (error) { - Logger.error('AdaApi::getWallets error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getWallets error', { error }); + throw new ApiError(error); + } + }; + + getWallet = async (request: GetWalletRequest): Promise => { + logger.debug('AdaApi::getWallet called', { + parameters: filterLogData(request), + }); + try { + const { walletId, isLegacy } = request; + let wallet; + if (isLegacy) { + const legacyWallet: LegacyAdaWallet = await getLegacyWallet( + this.config, + { walletId } + ); + const extraLegacyWalletProps = { + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + wallet = { + ...legacyWallet, + ...extraLegacyWalletProps, + }; + } else { + wallet = await getWallet(this.config, { walletId }); + } + logger.debug('AdaApi::getWallet success', { wallet }); + return _createWalletFromServerData(wallet); + } catch (error) { + logger.error('AdaApi::getWallet error', { error }); + throw new ApiError(error); } }; getAddresses = async ( request: GetAddressesRequest - ): Promise => { - Logger.debug('AdaApi::getAddresses called', { + ): Promise> => { + logger.debug('AdaApi::getAddresses called', { parameters: filterLogData(request), }); - const { walletId } = request; + const { walletId, queryParams, isLegacy } = request; try { - const accounts: Accounts = await getAccounts(this.config, { walletId }); - - const response = accounts.map(account => - Object.assign({}, account, { addresses: account.addresses.length }) - ); - Logger.debug('AdaApi::getAddresses success', { response }); - - if (!accounts || !accounts.length) { - return new Promise(resolve => - resolve({ accountIndex: null, addresses: [] }) + let response = []; + if (isLegacy && !isIncentivizedTestnet) { + response = await getByronWalletAddresses( + this.config, + walletId, + queryParams ); + } else if (!isLegacy) { + response = await getAddresses(this.config, walletId, queryParams); } - - // 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 })); + logger.debug('AdaApi::getAddresses success', { addresses: response }); + return response.map(_createAddressFromServerData); } catch (error) { - Logger.error('AdaApi::getAddresses error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getAddresses error', { error }); + throw new ApiError(error); } }; getTransactions = async ( request: GetTransactionsRequest ): Promise => { - const requestTimestamp = moment(); - const requestStats = Object.assign({}, request, { - cachedTransactions: request.cachedTransactions.length, - }); - Logger.debug('AdaApi::searchHistory called', { parameters: requestStats }); - const { - walletId, - skip, - limit, - isFirstLoad, // during first load we fetch all wallet's transactions - isRestoreActive, // during restoration we fetch only missing transactions - isRestoreCompleted, // once restoration is done we fetch potentially missing transactions - cachedTransactions, - } = request; - const accounts: Accounts = await getAccounts(this.config, { walletId }); - - if (!accounts.length || !accounts[0].index) { - return new Promise(resolve => resolve({ transactions: [], total: 0 })); - } + logger.debug('AdaApi::getTransactions called', { parameters: request }); + const { walletId, order, fromDate, toDate, isLegacy } = request; - let perPage = limit; - const shouldLoadAll = limit === null; - if (shouldLoadAll || limit > MAX_TRANSACTIONS_PER_PAGE) { - perPage = MAX_TRANSACTIONS_PER_PAGE; - } - - const params = { - wallet_id: walletId, - account_index: accounts[0].index, - page: skip === 0 ? 1 : skip + 1, - per_page: perPage, - sort_by: 'DES[created_at]', - created_at: `LTE[${moment.utc().format('YYYY-MM-DDTHH:mm:ss')}]`, - // ^^ By setting created_at filter to current time we make sure - // all subsequent multi-pages requests load the same set of transactions - }; - - const shouldLoadOnlyFresh = - !isFirstLoad && !isRestoreActive && !isRestoreCompleted; - if (shouldLoadOnlyFresh) { - const tenMinutesAgo = moment - .utc(Date.now() - TX_AGE_POLLING_THRESHOLD) - .format('YYYY-MM-DDTHH:mm:ss'); - // Since we load all transactions in a first load, later on we only care about fresh ones - Object.assign(params, { created_at: `GTE[${tenMinutesAgo}]` }); - } - - const pagesToBeLoaded = Math.ceil(limit / params.per_page); + const params = Object.assign( + {}, + { + order: order || 'descending', + } + ); + if (fromDate) + params.start = `${moment.utc(fromDate).format('YYYY-MM-DDTHH:mm:ss')}Z`; + if (toDate) + params.end = `${moment.utc(toDate).format('YYYY-MM-DDTHH:mm:ss')}Z`; try { - // Load first page of transactions - const response: Transactions = await getTransactionHistory( - this.config, - params - ); - const { meta, data: txHistory } = response; - const { totalPages, totalEntries: totalTransactions } = meta.pagination; - - let transactions = txHistory.map(tx => + let response; + if (isLegacy) { + response = await getLegacyWalletTransactionHistory( + this.config, + walletId, + params + ); + } else { + response = await getTransactionHistory(this.config, walletId, params); + } + logger.debug('AdaApi::getTransactions success', { + transactions: response, + }); + const transactions = response.map(tx => _createTransactionFromServerData(tx) ); - - // Load additional pages of transactions - const hasMultiplePages = - totalPages > 1 && (shouldLoadAll || limit > perPage); - if (hasMultiplePages) { - let page = 2; - const hasNextPage = () => { - const hasMorePages = page < totalPages + 1; - if ((isRestoreActive || isRestoreCompleted) && hasMorePages) { - const loadedTransactions = unionBy( - transactions, - cachedTransactions, - 'id' - ); - const hasMoreTransactions = - totalTransactions - loadedTransactions.length > 0; - return hasMoreTransactions; - } - return hasMorePages; - }; - const shouldLoadNextPage = () => - shouldLoadAll || page <= pagesToBeLoaded; - - if (isRestoreActive || isRestoreCompleted) { - const latestLoadedTransactionDate = transactions[0].date; - const latestLoadedTransactionDateString = moment - .utc(latestLoadedTransactionDate) - .format('YYYY-MM-DDTHH:mm:ss'); - // During restoration we need to fetch only transactions older than the latest loaded one - // as this ensures that both totalPages and totalEntries remain unchanged throught out - // subsequent page loads (as in the meantime new transactions can be discovered) - Object.assign(params, { - created_at: `LTE[${latestLoadedTransactionDateString}]`, - }); - } - - for (page; hasNextPage() && shouldLoadNextPage(); page++) { - const { data: pageHistory } = await getTransactionHistory( - this.config, - Object.assign(params, { page }) - ); - transactions.push( - ...pageHistory.map(tx => _createTransactionFromServerData(tx)) - ); - } - } - - // Merge newly loaded and previously loaded transactions - // - unionBy also serves the purpose of removing transaction duplicates - // which may occur as a side-effect of transaction request pagination - // as multi-page requests are not executed at the exact same time! - transactions = unionBy(transactions, cachedTransactions, 'id'); - - // Enforce the limit in case we are not loading all transactions - if (!shouldLoadAll) transactions.splice(limit); - - const total = transactions.length; - - const responseStats = { - apiRequested: limit || 'all', - apiFiltered: shouldLoadOnlyFresh ? 'fresh' : '', - apiReturned: totalTransactions, - apiPagesTotal: totalPages, - apiPagesRequested: params.page, - daedalusCached: cachedTransactions.length, - daedalusLoaded: total - cachedTransactions.length, - daedalusTotal: total, - requestDurationInMs: moment - .duration(moment().diff(requestTimestamp)) - .as('milliseconds'), - }; - Logger.debug( - `AdaApi::searchHistory success: ${total} transactions loaded`, - { responseStats } + return new Promise(resolve => + resolve({ transactions, total: response.length }) ); - return new Promise(resolve => resolve({ transactions, total })); } catch (error) { - Logger.error('AdaApi::searchHistory error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getTransactions error', { error }); + throw new ApiError(error); } + + // @API TODO - Filter / Search fine tunning "pending" for V2 + + // const requestStats = Object.assign({}, request, { + // cachedTransactions: request.cachedTransactions.length, + // }); + // logger.debug('AdaApi::searchHistory called', { parameters: requestStats }); + // const requestTimestamp = moment(); + // const params = { + // wallet_id: walletId, + // page: skip === 0 ? 1 : skip + 1, + // per_page: perPage, + // sort_by: 'DES[created_at]', + // created_at: `LTE[${moment.utc().format('YYYY-MM-DDTHH:mm:ss')}]`, + // // ^^ By setting created_at filter to current time we make sure + // // all subsequent multi-pages requests load the same set of transactions + // }; + // + // + // const { + // walletId, + // skip, + // limit, + // isFirstLoad, // during first load we fetch all wallet's transactions + // isRestoreActive, // during restoration we fetch only missing transactions + // isRestoreCompleted, // once restoration is done we fetch potentially missing transactions + // cachedTransactions, + // } , unionBy= request; + // + // + // let perPage = limit; + // const shouldLoadAll = limit === null; + // if (shouldLoadAll || limit > MAX_TRANSACTIONS_PER_PAGE) { + // perPage = MAX_TRANSACTIONS_PER_PAGE; + // } + // + // const params = { + // wallet_id: walletId, + // page: skip === 0 ? 1 : skip + 1, + // per_page: perPage, + // sort_by: 'DES[created_at]', + // created_at: `LTE[${moment.utc().format('YYYY-MM-DDTHH:mm:ss')}]`, + // // ^^ By setting created_at filter to current time we make sure + // // all subsequent multi-pages requests load the same set of transactions + // }; + // + // const shouldLoadOnlyFresh = + // !isFirstLoad && !isRestoreActive && !isRestoreCompleted; + // if (shouldLoadOnlyFresh) { + // const tenMinutesAgo = moment + // .utc(Date.now() - TX_AGE_POLLING_THRESHOLD) + // .format('YYYY-MM-DDTHH:mm:ss'); + // // Since we load all transactions in a first load, later on we only care about fresh ones + // Object.assign(params, { created_at: `GTE[${tenMinutesAgo}]` }); + // } + // + // const pagesToBeLoaded = Math.ceil(limit / params.per_page); + // + // try { + // // Load first page of transactions + // const response: Transactions = await getTransactionHistory( + // this.config, + // params + // ); + // const { meta, data: txHistory } = response; + // const { totalPages, totalEntries: totalTransactions } = meta.pagination; + // + // let transactions = txHistory.map(tx => + // _createTransactionFromServerData(tx) + // ); + // + // // Load additional pages of transactions + // const hasMultiplePages = + // totalPages > 1 && (shouldLoadAll || limit > perPage); + // if (hasMultiplePages) { + // let page = 2; + // const hasNextPage = () => { + // const hasMorePages = page < totalPages + 1; + // if ((isRestoreActive || isRestoreCompleted) && hasMorePages) { + // const loadedTransactions = unionBy( + // transactions, + // cachedTransactions, + // 'id' + // ); + // const hasMoreTransactions = + // totalTransactions - loadedTransactions.length > 0; + // return hasMoreTransactions; + // } + // return hasMorePages; + // }; + // const shouldLoadNextPage = () => + // shouldLoadAll || page <= pagesToBeLoaded; + // + // if (isRestoreActive || isRestoreCompleted) { + // const latestLoadedTransactionDate = transactions[0].date; + // const latestLoadedTransactionDateString = moment + // .utc(latestLoadedTransactionDate) + // .format('YYYY-MM-DDTHH:mm:ss'); + // // During restoration we need to fetch only transactions older than the latest loaded one + // // as this ensures that both totalPages and totalEntries remain unchanged throught out + // // subsequent page loads (as in the meantime new transactions can be discovered) + // Object.assign(params, { + // created_at: `LTE[${latestLoadedTransactionDateString}]`, + // }); + // } + // + // for (page; hasNextPage() && shouldLoadNextPage(); page++) { + // const { data: pageHistory } = await getTransactionHistory( + // this.config, + // Object.assign(params, { page }) + // ); + // transactions.push( + // ...pageHistory.map(tx => _createTransactionFromServerData(tx)) + // ); + // } + // } + // + // // Merge newly loaded and previously loaded transactions + // // - unionBy also serves the purpose of removing transaction duplicates + // // which may occur as a side-effect of transaction request pagination + // // as multi-page requests are not executed at the exact same time! + // transactions = unionBy(transactions, cachedTransactions, 'id'); + // + // // Enforce the limit in case we are not loading all transactions + // if (!shouldLoadAll) transactions.splice(limit); + // + // const total = transactions.length; + // + // const responseStats = { + // apiRequested: limit || 'all', + // apiFiltered: shouldLoadOnlyFresh ? 'fresh' : '', + // apiReturned: totalTransactions, + // apiPagesTotal: totalPages, + // apiPagesRequested: params.page, + // daedalusCached: cachedTransactions.length, + // daedalusLoaded: total - cachedTransactions.length, + // daedalusTotal: total, + // requestDurationInMs: moment + // .duration(moment().diff(requestTimestamp)) + // .as('milliseconds'), + // }; + // logger.debug( + // `AdaApi::searchHistory success: ${total} transactions loaded`, + // { responseStats } + // ); + // return new Promise(resolve => resolve({ transactions, total })); + // } catch (error) { + // logger.error('AdaApi::searchHistory error', { error }); + // throw new GenericApiError(error); + // } }; createWallet = async (request: CreateWalletRequest): Promise => { - Logger.debug('AdaApi::createWallet called', { + logger.debug('AdaApi::createWallet called', { parameters: filterLogData(request), }); - const { name, mnemonic, spendingPassword: passwordString } = request; - const spendingPassword = passwordString - ? encryptPassphrase(passwordString) - : ''; - const assuranceLevel = 'normal'; + const { name, mnemonic, spendingPassword } = request; try { + let wallet: AdaWallet; const walletInitData = { - operation: 'create', - backupPhrase: split(mnemonic, ' '), - assuranceLevel, name, - spendingPassword, + mnemonic_sentence: split(mnemonic, ' '), + passphrase: spendingPassword, }; - const wallet: AdaWallet = await createWallet(this.config, { - walletInitData, - }); - Logger.debug('AdaApi::createWallet success', { wallet }); + + if (isIncentivizedTestnet) { + wallet = await createWallet(this.config, { + walletInitData, + }); + logger.debug('AdaApi::createWallet (Shelley) success', { wallet }); + } else { + const legacyWallet: LegacyAdaWallet = await restoreByronWallet( + this.config, + { walletInitData }, + 'random' + ); + + // Generate address for the newly created Byron wallet + const { id: walletId } = legacyWallet; + const address: Address = await createByronWalletAddress(this.config, { + passphrase: spendingPassword, + walletId, + }); + logger.debug('AdaApi::createAddress (Byron) success', { address }); + + const extraLegacyWalletProps = { + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + wallet = { + ...legacyWallet, + ...extraLegacyWalletProps, + }; + logger.debug('AdaApi::createWallet (Byron) success', { wallet }); + } return _createWalletFromServerData(wallet); } catch (error) { - Logger.error('AdaApi::createWallet error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::createWallet error', { error }); + throw new ApiError(error); } }; deleteWallet = async (request: DeleteWalletRequest): Promise => { - Logger.debug('AdaApi::deleteWallet called', { + logger.debug('AdaApi::deleteWallet called', { parameters: filterLogData(request), }); try { - const { walletId } = request; - const response = await deleteWallet(this.config, { walletId }); - Logger.debug('AdaApi::deleteWallet success', { response }); + const { walletId, isLegacy } = request; + let response; + if (isLegacy) { + response = await deleteLegacyWallet(this.config, { walletId }); + } else { + response = await deleteWallet(this.config, { walletId }); + } + logger.debug('AdaApi::deleteWallet success', { response }); return true; } catch (error) { - Logger.error('AdaApi::deleteWallet error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::deleteWallet error', { error }); + throw new ApiError(error); } }; createTransaction = async ( - request: TransactionRequest + request: CreateTransactionRequest ): Promise => { - Logger.debug('AdaApi::createTransaction called', { + logger.debug('AdaApi::createTransaction called', { parameters: filterLogData(request), }); - const { - accountIndex, - walletId, - address, - amount, - spendingPassword: passwordString, - } = request; - const spendingPassword = passwordString - ? encryptPassphrase(passwordString) - : ''; + const { walletId, address, amount, passphrase, isLegacy } = request; + try { const data = { - source: { - accountIndex, - walletId, - }, - destinations: [ + payments: [ { address, - amount, + amount: { + quantity: amount, + unit: WalletUnits.LOVELACE, + }, }, ], - groupingPolicy: 'OptimizeForSecurity', - spendingPassword, + passphrase, }; - const response: Transaction = await createTransaction(this.config, { - data, - }); - Logger.debug('AdaApi::createTransaction success', { + + let response: Transaction; + if (isLegacy) { + response = await createByronWalletTransaction(this.config, { + walletId, + data, + }); + } else { + response = await createTransaction(this.config, { + walletId, + data, + }); + } + + logger.debug('AdaApi::createTransaction success', { transaction: response, }); + return _createTransactionFromServerData(response); } catch (error) { - Logger.error('AdaApi::createTransaction error', { error }); - if (error.message === 'OutputIsRedeem') { - throw new NotAllowedToSendMoneyToRedeemAddressError(); - } - if ( - error.message === 'NotEnoughMoney' || - error.message === 'UtxoNotEnoughFragmented' - ) { - throw new NotEnoughMoneyToSendError(); - } - if (error.message === 'CannotCreateAddress') { - throw new IncorrectSpendingPasswordError(); - } - if (error.message === 'TooBigTransaction') { - throw new TooBigTransactionError(); - } - throw new GenericApiError(); + logger.error('AdaApi::createTransaction error', { error }); + throw new ApiError(error) + .set('wrongEncryptionPassphrase') + .where('code', 'bad_request') + .inc('message', 'passphrase is too short') + .set('transactionIsTooBig', true, { + linkLabel: 'tooBigTransactionErrorLinkLabel', + linkURL: 'tooBigTransactionErrorLinkURL', + }) + .where('code', 'transaction_is_too_big') + .result(); } }; calculateTransactionFee = async ( - request: TransactionRequest + request: GetTransactionFeeRequest ): Promise => { - Logger.debug('AdaApi::calculateTransactionFee called', { + logger.debug('AdaApi::calculateTransactionFee called', { parameters: filterLogData(request), }); - const { accountIndex, walletId, walletBalance, address, amount } = request; + const { + walletId, + address, + amount, + walletBalance, + availableBalance, + isLegacy, + } = request; + try { const data = { - source: { - accountIndex, - walletId, - }, - destinations: [ + payments: [ { address, - amount, + amount: { + quantity: amount, + unit: WalletUnits.LOVELACE, + }, }, ], - groupingPolicy: 'OptimizeForSecurity', }; - const response: TransactionFee = await getTransactionFee(this.config, { - data, - }); - Logger.debug('AdaApi::calculateTransactionFee success', { + + let response: TransactionFee; + if (isLegacy) { + response = await getByronWalletTransactionFee(this.config, { + walletId, + data, + }); + } else { + response = await getTransactionFee(this.config, { + walletId, + data, + }); + } + + const formattedTxAmount = new BigNumber(amount).dividedBy( + LOVELACES_PER_ADA + ); + const fee = _createTransactionFeeFromServerData(response); + const amountWithFee = formattedTxAmount.plus(fee); + if (amountWithFee.gt(walletBalance)) { + // Amount + fees exceeds walletBalance: + // = show "Not enough Ada for fees. Try sending a smaller amount." + throw new ApiError().result('cannotCoverFee'); + } + logger.debug('AdaApi::calculateTransactionFee success', { transactionFee: response, }); - return _createTransactionFeeFromServerData(response); + return fee; } catch (error) { - Logger.error('AdaApi::calculateTransactionFee error', { error }); - if ( - error.message === 'NotEnoughMoney' || - error.message === 'UtxoNotEnoughFragmented' - ) { - 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(); - } - } - if (error.message === 'TooBigTransaction') { - throw new TooBigTransactionError(); - } - throw new GenericApiError(); + // 1. 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." + // 2. 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." + const notEnoughMoneyError = walletBalance.gt(availableBalance) + ? 'canNotCalculateTransactionFees' + : 'notEnoughFundsForTransaction'; + + // ApiError with logging showcase + throw new ApiError(error, { + logError: true, + msg: 'AdaApi::calculateTransactionFee error', + }) + .set(notEnoughMoneyError, true) + .where('code', 'not_enough_money') + .set('invalidAddress') + .where('code', 'bad_request') + .inc('message', 'Unable to decode Address') + .result(); } }; - createAddress = async (request: CreateAddressRequest): Promise
=> { - Logger.debug('AdaApi::createAddress called', { + createAddress = async ( + request: CreateByronWalletAddressRequest + ): Promise => { + logger.debug('AdaApi::createAddress called', { parameters: filterLogData(request), }); - const { - accountIndex, - walletId, - spendingPassword: passwordString, - } = request; - const spendingPassword = passwordString - ? encryptPassphrase(passwordString) - : ''; + const { addressIndex, walletId, passphrase: passwordString } = request; + const passphrase = passwordString || ''; try { - const address: Address = await createAddress(this.config, { - spendingPassword, - accountIndex, + const address: Address = await createByronWalletAddress(this.config, { + passphrase, walletId, + addressIndex, }); - Logger.debug('AdaApi::createAddress success', { address }); + logger.debug('AdaApi::createAddress success', { address }); return _createAddressFromServerData(address); } catch (error) { - Logger.error('AdaApi::createAddress error', { error }); - if (error.message === 'CannotCreateAddress') { - throw new IncorrectSpendingPasswordError(); - } - throw new GenericApiError(); + logger.error('AdaApi::createAddress error', { error }); + + throw new ApiError(error) + .set('wrongEncryptionPassphrase') + .where('code', 'bad_request') + .inc('message', 'passphrase is too short') + .result(); } }; - async isValidAddress(address: string): Promise { - Logger.debug('AdaApi::isValidAdaAddress called', { - parameters: { address }, - }); + deleteTransaction = async ( + request: DeleteTransactionRequest + ): Promise => { + logger.debug('AdaApi::deleteTransaction called', { parameters: request }); + const { walletId, transactionId, isLegacy } = request; try { - const response: Address = await getAddress(this.config, { address }); - Logger.debug('AdaApi::isValidAdaAddress success', { response }); - return true; + let response; + if (isLegacy) { + response = await deleteLegacyTransaction(this.config, { + walletId, + transactionId, + }); + } else { + response = await deleteTransaction(this.config, { + walletId, + transactionId, + }); + } + logger.debug('AdaApi::deleteTransaction success', response); } catch (error) { - Logger.error('AdaApi::isValidAdaAddress error', { error }); - return false; + logger.error('AdaApi::deleteTransaction error', { error }); + // In this particular call we don't need to handle the error in the UI + // The only reason transaction canceling would fail is if the transaction + // is no longer pending - in which case there is nothign we can do. } - } - - isValidMnemonic = (mnemonic: string): boolean => - isValidMnemonic(mnemonic, WALLET_RECOVERY_PHRASE_WORD_COUNT); + }; isValidCertificateMnemonic = (mnemonic: string): boolean => mnemonic.split(' ').length === ADA_CERTIFICATE_MNEMONIC_LENGTH; getWalletRecoveryPhrase(): Promise> { - Logger.debug('AdaApi::getWalletRecoveryPhrase called'); + logger.debug('AdaApi::getWalletRecoveryPhrase called'); try { const response: Promise> = new Promise(resolve => - resolve(generateAccountMnemonics()) + resolve( + generateAccountMnemonics( + isIncentivizedTestnet + ? WALLET_RECOVERY_PHRASE_WORD_COUNT + : LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT + ) + ) ); - Logger.debug('AdaApi::getWalletRecoveryPhrase success'); + logger.debug('AdaApi::getWalletRecoveryPhrase success'); return response; } catch (error) { - Logger.error('AdaApi::getWalletRecoveryPhrase error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getWalletRecoveryPhrase error', { error }); + throw new ApiError(error); } } - // eslint-disable-next-line max-len getWalletCertificateAdditionalMnemonics(): Promise> { - Logger.debug('AdaApi::getWalletCertificateAdditionalMnemonics called'); + logger.debug('AdaApi::getWalletCertificateAdditionalMnemonics called'); try { const response: Promise> = new Promise(resolve => resolve(generateAdditionalMnemonics()) ); - Logger.debug('AdaApi::getWalletCertificateAdditionalMnemonics success'); + logger.debug('AdaApi::getWalletCertificateAdditionalMnemonics success'); return response; } catch (error) { - Logger.error('AdaApi::getWalletCertificateAdditionalMnemonics error', { + logger.error('AdaApi::getWalletCertificateAdditionalMnemonics error', { error, }); - throw new GenericApiError(); + throw new ApiError(error); } } getWalletCertificateRecoveryPhrase( request: GetWalletCertificateRecoveryPhraseRequest ): Promise> { - Logger.debug('AdaApi::getWalletCertificateRecoveryPhrase called'); + 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'); + logger.debug('AdaApi::getWalletCertificateRecoveryPhrase success'); return response; } catch (error) { - Logger.error('AdaApi::getWalletCertificateRecoveryPhrase error', { + logger.error('AdaApi::getWalletCertificateRecoveryPhrase error', { error, }); - throw new GenericApiError(); + throw new ApiError(error); } } getWalletRecoveryPhraseFromCertificate( request: GetWalletRecoveryPhraseFromCertificateRequest ): Promise> { - Logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate called'); + logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate called'); const { passphrase, scrambledInput } = request; try { const response = unscrambleMnemonics({ passphrase, scrambledInput }); - Logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate success'); + logger.debug('AdaApi::getWalletRecoveryPhraseFromCertificate success'); return Promise.resolve(response); } catch (error) { - Logger.error('AdaApi::getWalletRecoveryPhraseFromCertificate error', { + logger.error('AdaApi::getWalletRecoveryPhraseFromCertificate error', { error, }); - return Promise.reject(new InvalidMnemonicError()); + const errorRejection = new ApiError(error) + .set('invalidMnemonic', true) + .result(); + return Promise.reject(errorRejection); } } restoreWallet = async (request: RestoreWalletRequest): Promise => { - Logger.debug('AdaApi::restoreWallet called', { + logger.debug('AdaApi::restoreWallet called', { parameters: filterLogData(request), }); - const { - recoveryPhrase, - walletName, - spendingPassword: passwordString, - } = request; - const spendingPassword = passwordString - ? encryptPassphrase(passwordString) - : ''; - const assuranceLevel = 'normal'; + const { recoveryPhrase, walletName, spendingPassword } = request; const walletInitData = { - operation: 'restore', - backupPhrase: split(recoveryPhrase, ' '), - assuranceLevel, name: walletName, - spendingPassword, + mnemonic_sentence: recoveryPhrase, + passphrase: spendingPassword, }; try { const wallet: AdaWallet = await restoreWallet(this.config, { walletInitData, }); - Logger.debug('AdaApi::restoreWallet success', { wallet }); + logger.debug('AdaApi::restoreWallet success', { wallet }); return _createWalletFromServerData(wallet); } catch (error) { - Logger.error('AdaApi::restoreWallet error', { 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(); - } + logger.error('AdaApi::restoreWallet error', { error }); + + throw new ApiError(error) + .set('forbiddenMnemonic') + .where('message', 'JSONValidationFailed') + .inc( + 'diagnostic.validationError', + 'Forbidden Mnemonic: an example Mnemonic has been submitted' + ) + .set('forbiddenMnemonic') + .where('code', 'invalid_restoration_parameters') + .result(); + } + }; + + restoreLegacyWallet = async ( + request: RestoreLegacyWalletRequest + ): Promise => { + logger.debug('AdaApi::restoreLegacyWallet called', { + parameters: filterLogData(request), + }); + const { recoveryPhrase, walletName, spendingPassword } = request; + const walletInitData = { + style: 'random', + name: walletName, + mnemonic_sentence: recoveryPhrase, + passphrase: spendingPassword, + }; + try { + const legacyWallet: LegacyAdaWallet = await restoreLegacyWallet( + this.config, + { walletInitData } + ); + const extraLegacyWalletProps = { + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + const wallet = { + ...legacyWallet, + ...extraLegacyWalletProps, + }; + logger.debug('AdaApi::restoreLegacyWallet success', { wallet }); + return _createWalletFromServerData(wallet); + } catch (error) { + logger.error('AdaApi::restoreLegacyWallet error', { error }); + throw new ApiError(error) + .set('forbiddenMnemonic') + .where('message', 'JSONValidationFailed') + .inc( + 'diagnostic.validationError', + 'Forbidden Mnemonic: an example Mnemonic has been submitted' + ) + .set('forbiddenMnemonic') + .where('code', 'invalid_restoration_parameters') + .result(); + } + }; + + restoreByronRandomWallet = async ( + request: RestoreLegacyWalletRequest + ): Promise => { + logger.debug('AdaApi::restoreByronRandomWallet called', { + parameters: filterLogData(request), + }); + const { recoveryPhrase, walletName, spendingPassword } = request; + const walletInitData = { + name: walletName, + mnemonic_sentence: recoveryPhrase, + passphrase: spendingPassword, + }; + const type = WALLET_BYRON_KINDS.RANDOM; + try { + const legacyWallet: LegacyAdaWallet = await restoreByronWallet( + this.config, + { walletInitData }, + type + ); + + if (!isIncentivizedTestnet) { + // Generate address for the newly restored Byron wallet + const { id: walletId } = legacyWallet; + const address: Address = await createByronWalletAddress(this.config, { + passphrase: spendingPassword, + walletId, + }); + logger.debug('AdaApi::createAddress (Byron) success', { address }); } - throw new GenericApiError(); + + const extraLegacyWalletProps = { + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + const wallet = { + ...legacyWallet, + ...extraLegacyWalletProps, + }; + logger.debug('AdaApi::restoreByronRandomWallet success', { wallet }); + return _createWalletFromServerData(wallet); + } catch (error) { + logger.error('AdaApi::restoreByronRandomWallet error', { error }); + throw new ApiError(error) + .set('forbiddenMnemonic') + .where('message', 'JSONValidationFailed') + .inc( + 'diagnostic.validationError', + 'Forbidden Mnemonic: an example Mnemonic has been submitted' + ) + .set('forbiddenMnemonic') + .where('code', 'invalid_restoration_parameters') + .result(); + } + }; + + restoreByronIcarusWallet = async ( + request: RestoreLegacyWalletRequest + ): Promise => { + logger.debug('AdaApi::restoreByronIcarusWallet called', { + parameters: filterLogData(request), + }); + const { recoveryPhrase, walletName, spendingPassword } = request; + const walletInitData = { + name: walletName, + mnemonic_sentence: recoveryPhrase, + passphrase: spendingPassword, + }; + const type = WALLET_BYRON_KINDS.ICARUS; + try { + const legacyWallet: LegacyAdaWallet = await restoreByronWallet( + this.config, + { walletInitData }, + type + ); + const extraLegacyWalletProps = { + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + const wallet = { + ...legacyWallet, + ...extraLegacyWalletProps, + }; + logger.debug('AdaApi::restoreByronIcarusWallet success', { wallet }); + return _createWalletFromServerData(wallet); + } catch (error) { + logger.error('AdaApi::restoreByronIcarusWallet error', { error }); + throw new ApiError(error) + .set('forbiddenMnemonic') + .where('message', 'JSONValidationFailed') + .inc( + 'diagnostic.validationError', + 'Forbidden Mnemonic: an example Mnemonic has been submitted' + ) + .set('forbiddenMnemonic') + .where('code', 'invalid_restoration_parameters') + .result(); + } + }; + + restoreByronTrezorWallet = async ( + request: RestoreLegacyWalletRequest + ): Promise => { + logger.debug('AdaApi::restoreByronTrezorWallet called', { + parameters: filterLogData(request), + }); + const { recoveryPhrase, walletName, spendingPassword } = request; + const walletInitData = { + name: walletName, + mnemonic_sentence: recoveryPhrase, + passphrase: spendingPassword, + }; + const type = WALLET_BYRON_KINDS.TREZOR; + try { + const legacyWallet: LegacyAdaWallet = await restoreByronWallet( + this.config, + { walletInitData }, + type + ); + const extraLegacyWalletProps = { + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + const wallet = { + ...legacyWallet, + ...extraLegacyWalletProps, + }; + logger.debug('AdaApi::restoreByronTrezorWallet success', { wallet }); + return _createWalletFromServerData(wallet); + } catch (error) { + logger.error('AdaApi::restoreByronTrezorWallet error', { error }); + throw new ApiError(error) + .set('forbiddenMnemonic') + .where('message', 'JSONValidationFailed') + .inc( + 'diagnostic.validationError', + 'Forbidden Mnemonic: an example Mnemonic has been submitted' + ) + .set('forbiddenMnemonic') + .where('code', 'invalid_restoration_parameters') + .result(); + } + }; + + restoreByronLedgerWallet = async ( + request: RestoreLegacyWalletRequest + ): Promise => { + logger.debug('AdaApi::restoreByronLedgerWallet called', { + parameters: filterLogData(request), + }); + const { recoveryPhrase, walletName, spendingPassword } = request; + const walletInitData = { + name: walletName, + mnemonic_sentence: recoveryPhrase, + passphrase: spendingPassword, + }; + const type = WALLET_BYRON_KINDS.LEDGER; + try { + const legacyWallet: LegacyAdaWallet = await restoreByronWallet( + this.config, + { walletInitData }, + type + ); + const extraLegacyWalletProps = { + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + const wallet = { + ...legacyWallet, + ...extraLegacyWalletProps, + }; + logger.debug('AdaApi::restoreByronLedgerWallet success', { wallet }); + return _createWalletFromServerData(wallet); + } catch (error) { + logger.error('AdaApi::restoreByronLedgerWallet error', { error }); + throw new ApiError(error) + .set('forbiddenMnemonic') + .where('message', 'JSONValidationFailed') + .inc( + 'diagnostic.validationError', + 'Forbidden Mnemonic: an example Mnemonic has been submitted' + ) + .set('forbiddenMnemonic') + .where('code', 'invalid_restoration_parameters') + .result(); + } + }; + + restoreExportedByronWallet = async ( + request: RestoreExportedByronWalletRequest + ): Promise => { + logger.debug('AdaApi::restoreExportedByronWallet called', { + name: request.name, + }); + try { + const legacyWallet: LegacyAdaWallet = await restoreExportedByronWallet( + this.config, + { walletInitData: request } + ); + const extraLegacyWalletProps = { + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + const wallet = { + ...legacyWallet, + ...extraLegacyWalletProps, + }; + logger.debug('AdaApi::restoreExportedByronWallet success', { wallet }); + return _createWalletFromServerData(wallet); + } catch (error) { + logger.error('AdaApi::restoreExportedByronWallet error', { error }); + throw new ApiError(error); } }; importWalletFromKey = async ( request: ImportWalletFromKeyRequest ): Promise => { - Logger.debug('AdaApi::importWalletFromKey called', { + logger.debug('AdaApi::importWalletFromKey called', { parameters: filterLogData(request), }); - const { filePath, spendingPassword: passwordString } = request; - const spendingPassword = passwordString - ? encryptPassphrase(passwordString) - : ''; + const { filePath, spendingPassword } = request; try { const importedWallet: AdaWallet = await importWalletAsKey(this.config, { filePath, - spendingPassword, + spendingPassword: spendingPassword || '', }); - Logger.debug('AdaApi::importWalletFromKey success', { importedWallet }); + logger.debug('AdaApi::importWalletFromKey success', { importedWallet }); return _createWalletFromServerData(importedWallet); } catch (error) { - Logger.error('AdaApi::importWalletFromKey error', { error }); - if (error.message === 'WalletAlreadyExists') { - throw new WalletAlreadyImportedError(); - } - throw new WalletFileImportError(); + logger.error('AdaApi::importWalletFromKey error', { error }); + throw new ApiError(error) + .set('walletAlreadyImported', true) + .where('code', 'wallet_already_exists') + .result('walletFileImportError'); } }; importWalletFromFile = async ( request: ImportWalletFromFileRequest ): Promise => { - Logger.debug('AdaApi::importWalletFromFile called', { + logger.debug('AdaApi::importWalletFromFile called', { parameters: filterLogData(request), }); - const { filePath, spendingPassword: passwordString } = request; - const spendingPassword = passwordString - ? encryptPassphrase(passwordString) - : ''; + const { filePath, spendingPassword } = request; const isKeyFile = filePath .split('.') @@ -750,99 +1202,161 @@ export default class AdaApi { .toLowerCase() === 'key'; try { const importedWallet: AdaWallet = isKeyFile - ? await importWalletAsKey(this.config, { filePath, spendingPassword }) + ? await importWalletAsKey(this.config, { + filePath, + spendingPassword, + }) : await importWalletAsJSON(this.config, filePath); - Logger.debug('AdaApi::importWalletFromFile success', { importedWallet }); + logger.debug('AdaApi::importWalletFromFile success', { importedWallet }); return _createWalletFromServerData(importedWallet); } catch (error) { - Logger.error('AdaApi::importWalletFromFile error', { error }); - if (error.message === 'WalletAlreadyExists') { - throw new WalletAlreadyImportedError(); - } - throw new WalletFileImportError(); + logger.error('AdaApi::importWalletFromFile error', { error }); + throw new ApiError(error) + .set('walletAlreadyImported', true) + .where('code', 'wallet_already_exists') + .result('walletFileImportError'); } }; nextUpdate = async (): Promise => { - Logger.debug('AdaApi::nextUpdate called'); + logger.debug('AdaApi::nextUpdate called'); + + /* TODO: Re-enable when API is available try { const nodeUpdate = await getNextNodeUpdate(this.config); if (nodeUpdate && nodeUpdate.version) { - Logger.debug('AdaApi::nextUpdate success', { nodeUpdate }); + logger.debug('AdaApi::nextUpdate success', { nodeUpdate }); return nodeUpdate; } - Logger.debug('AdaApi::nextUpdate success: No Update Available'); + logger.debug('AdaApi::nextUpdate success: No Update Available'); } catch (error) { - Logger.error('AdaApi::nextUpdate error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::nextUpdate error', { error }); + throw new GenericApiError(error); } + */ + return null; }; postponeUpdate = async (): Promise => { - Logger.debug('AdaApi::postponeUpdate called'); + logger.debug('AdaApi::postponeUpdate called'); try { const response: Promise = await postponeNodeUpdate(this.config); - Logger.debug('AdaApi::postponeUpdate success', { response }); + logger.debug('AdaApi::postponeUpdate success', { response }); } catch (error) { - Logger.error('AdaApi::postponeUpdate error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::postponeUpdate error', { error }); + throw new ApiError(error); } }; applyUpdate = async (): Promise => { - Logger.debug('AdaApi::applyUpdate called'); + logger.debug('AdaApi::applyUpdate called'); try { await awaitUpdateChannel.send(); const response: Promise = await applyNodeUpdate(this.config); - Logger.debug('AdaApi::applyUpdate success', { response }); + logger.debug('AdaApi::applyUpdate success', { response }); } catch (error) { - Logger.error('AdaApi::applyUpdate error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::applyUpdate error', { error }); + throw new ApiError(error); } }; updateWallet = async (request: UpdateWalletRequest): Promise => { - Logger.debug('AdaApi::updateWallet called', { + logger.debug('AdaApi::updateWallet called', { parameters: filterLogData(request), }); - const { walletId, assuranceLevel, name } = request; + const { walletId, name, isLegacy } = request; try { - const wallet: AdaWallet = await updateWallet(this.config, { - walletId, - assuranceLevel, - name, - }); - Logger.debug('AdaApi::updateWallet success', { wallet }); + let wallet: AdaWallet; + if (isLegacy) { + const response = await updateByronWallet(this.config, { + walletId, + name, + }); + wallet = { + ...response, + address_pool_gap: 0, // Not needed for legacy wallets + delegation: { + active: { + status: WalletDelegationStatuses.NOT_DELEGATING, + }, + }, + isLegacy: true, + }; + } else { + wallet = await updateWallet(this.config, { walletId, name }); + } + logger.debug('AdaApi::updateWallet success', { wallet }); return _createWalletFromServerData(wallet); } catch (error) { - Logger.error('AdaApi::updateWallet error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::updateWallet error', { error }); + throw new ApiError(error); } }; updateSpendingPassword = async ( request: UpdateSpendingPasswordRequest ): Promise => { - Logger.debug('AdaApi::updateSpendingPassword called', { + logger.debug('AdaApi::updateSpendingPassword called', { parameters: filterLogData(request), }); - const { walletId, oldPassword, newPassword } = request; + const { walletId, oldPassword, newPassword, isLegacy } = request; try { - await changeSpendingPassword(this.config, { + if (isLegacy) { + await updateByronSpendingPassword(this.config, { + walletId, + oldPassword, + newPassword, + }); + + if (!isIncentivizedTestnet && !oldPassword) { + // Generate address for the Byron wallet for which password was set for the 1st time + const address: Address = await createByronWalletAddress(this.config, { + passphrase: newPassword, + walletId, + }); + logger.debug('AdaApi::createAddress (Byron) success', { address }); + } + } else { + await updateSpendingPassword(this.config, { + walletId, + oldPassword, + newPassword, + }); + } + logger.debug('AdaApi::updateSpendingPassword success'); + return true; + } catch (error) { + logger.error('AdaApi::updateSpendingPassword error', { error }); + throw new ApiError(error) + .set('wrongEncryptionPassphrase') + .where('code', 'bad_request') + .inc('message', 'passphrase is too short') + .result(); + } + }; + + quitStakePool = async ( + request: QuitStakePoolRequest + ): Promise => { + logger.debug('AdaApi::quitStakePool called', { + parameters: filterLogData(request), + }); + const { walletId, passphrase } = request; + try { + const result = await quitStakePool(this.config, { walletId, - oldPassword, - newPassword, + passphrase, }); - Logger.debug('AdaApi::updateSpendingPassword success'); - return true; + logger.debug('AdaApi::quitStakePool success', { result }); + return result; } catch (error) { - Logger.error('AdaApi::updateSpendingPassword error', { error }); - const errorMessage = get(error, 'diagnostic.msg', ''); - if (errorMessage.includes('UpdateWalletPasswordOldPasswordMismatch')) { - throw new IncorrectSpendingPasswordError(); - } - throw new GenericApiError(); + logger.error('AdaApi::quitStakePool error', { error }); + throw new ApiError(error) + .set('wrongEncryptionPassphrase') + .where('code', 'bad_request') + .inc('message', 'passphrase is too short') + .result(); } }; @@ -850,7 +1364,7 @@ export default class AdaApi { request: ExportWalletToFileRequest ): Promise<[]> => { const { walletId, filePath } = request; - Logger.debug('AdaApi::exportWalletToFile called', { + logger.debug('AdaApi::exportWalletToFile called', { parameters: filterLogData(request), }); try { @@ -858,30 +1372,33 @@ export default class AdaApi { walletId, filePath, }); - Logger.debug('AdaApi::exportWalletToFile success', { response }); + logger.debug('AdaApi::exportWalletToFile success', { response }); return response; } catch (error) { - Logger.error('AdaApi::exportWalletToFile error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::exportWalletToFile error', { error }); + throw new ApiError(error); } }; getWalletUtxos = async ( request: GetWalletUtxosRequest ): Promise => { - const { walletId } = request; - Logger.debug('AdaApi::getWalletUtxos called', { + const { walletId, isLegacy } = request; + logger.debug('AdaApi::getWalletUtxos called', { parameters: filterLogData(request), }); try { - const response: Promise = await getWalletUtxos(this.config, { - walletId, - }); - Logger.debug('AdaApi::getWalletUtxos success', { response }); + let response: WalletUtxos; + if (isLegacy) { + response = await getByronWalletUtxos(this.config, { walletId }); + } else { + response = await getWalletUtxos(this.config, { walletId }); + } + logger.debug('AdaApi::getWalletUtxos success', { response }); return response; } catch (error) { - Logger.error('AdaApi::getWalletUtxos error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getWalletUtxos error', { error }); + throw new ApiError(error); } }; @@ -889,7 +1406,7 @@ export default class AdaApi { request: GetWalletIdAndBalanceRequest ): Promise => { const { recoveryPhrase, getBalance } = request; - Logger.debug('AdaApi::getWalletIdAndBalance called', { + logger.debug('AdaApi::getWalletIdAndBalance called', { parameters: { getBalance }, }); try { @@ -900,7 +1417,7 @@ export default class AdaApi { getBalance, } ); - Logger.debug('AdaApi::getWalletIdAndBalance success', { response }); + logger.debug('AdaApi::getWalletIdAndBalance success', { response }); const { walletId, balance } = response; return { walletId, @@ -910,109 +1427,228 @@ export default class AdaApi { : null, }; } catch (error) { - Logger.error('AdaApi::getWalletIdAndBalance error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getWalletIdAndBalance error', { error }); + throw new ApiError(error); } }; - testReset = async (): Promise => { - Logger.debug('AdaApi::testReset called'); + forceWalletResync = async ( + request: ForceWalletResyncRequest + ): Promise => { + await wait(FORCED_WALLET_RESYNC_WAIT); // API request throttling + logger.debug('AdaApi::forceWalletResync called', { parameters: request }); try { - const response: Promise = await resetWalletState(this.config); - Logger.debug('AdaApi::testReset success'); - return response; + const { walletId, isLegacy } = request; + let response; + if (isLegacy) { + response = await forceLegacyWalletResync(this.config, { walletId }); + } else { + response = await forceWalletResync(this.config, { walletId }); + } + logger.debug('AdaApi::forceWalletResync success', { response }); } catch (error) { - Logger.error('AdaApi::testReset error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::forceWalletResync error', { error }); + throw new ApiError(error); } }; - getNetworkStatus = async ( - queryInfoParams?: NodeInfoQueryParams - ): Promise => { - const isForceNTPCheck = !!queryInfoParams; - const loggerText = `AdaApi::getNetworkStatus${ - isForceNTPCheck ? ' (FORCE-NTP-CHECK)' : '' - }`; - Logger.debug(`${loggerText} called`); + transferFundsCalculateFee = async ( + request: TransferFundsCalculateFeeRequest + ): Promise => { + const { sourceWalletId } = request; + logger.debug('AdaApi::transferFundsCalculateFee called', { + parameters: { sourceWalletId }, + }); try { - const nodeInfo: NodeInfoResponse = await getNodeInfo( + const response: TransferFundsCalculateFeeResponse = await transferFundsCalculateFee( this.config, - queryInfoParams + { + sourceWalletId, + } ); - Logger.debug(`${loggerText} success`, { nodeInfo }); + logger.debug('AdaApi::transferFundsCalculateFee success', { response }); + return _createMigrationFeeFromServerData(response); + } catch (error) { + logger.error('AdaApi::transferFundsCalculateFee error', { error }); + throw new ApiError(error); + } + }; - const { - blockchainHeight, - subscriptionStatus, - syncProgress, - localBlockchainHeight, - localTimeInformation, - } = nodeInfo; + transferFunds = async ( + request: TransferFundsRequest + ): Promise => { + const { sourceWalletId, targetWalletId, passphrase } = request; + logger.debug('AdaApi::transferFunds called', { + parameters: { sourceWalletId, targetWalletId }, + }); + try { + const response: TransferFundsResponse = await transferFunds(this.config, { + sourceWalletId, + targetWalletId, + passphrase, + }); + logger.debug('AdaApi::transferFunds success', { response }); + return response; + } catch (error) { + logger.error('AdaApi::transferFunds error', { error }); + throw new ApiError(error) + .set('wrongEncryptionPassphrase') + .where('code', 'bad_request') + .inc('message', 'passphrase is too short') + .result(); + } + }; + + getStakePools = async (): Promise> => { + logger.debug('AdaApi::getStakePools called'); + try { + const response: AdaApiStakePools = await getStakePools(this.config); + const stakePools = response + .filter(({ metadata }: AdaApiStakePool) => metadata !== undefined) + .map(_createStakePoolFromServerData); + logger.debug('AdaApi::getStakePools success', { + stakePoolsTotal: response.length, + stakePoolsWithMetadata: stakePools.length, + }); + return stakePools; + } catch (error) { + logger.error('AdaApi::getStakePools error', { error }); + throw new ApiError(error); + } + }; + + testReset = async (): Promise => { + logger.debug('AdaApi::testReset called'); + try { + const wallets = await this.getWallets(); + await Promise.all( + wallets.map(wallet => + this.deleteWallet({ + walletId: wallet.id, + isLegacy: wallet.isLegacy, + }) + ) + ); + logger.debug('AdaApi::testReset success'); + } catch (error) { + logger.error('AdaApi::testReset error', { error }); + throw new ApiError(error); + } + }; + + getNetworkInfo = async (): Promise => { + logger.debug('AdaApi::getNetworkInfo called'); + try { + const networkInfo: NetworkInfoResponse = await getNetworkInfo( + this.config + ); + logger.debug('AdaApi::getNetworkInfo success', { networkInfo }); + /* eslint-disable-next-line camelcase */ + const { sync_progress, node_tip, network_tip, next_epoch } = networkInfo; + const syncProgress = + get(sync_progress, 'status') === 'ready' + ? 100 + : get(sync_progress, 'progress.quantity', 0); // extract relevant data before sending to NetworkStatusStore return { - subscriptionStatus, - syncProgress: syncProgress.quantity, - blockchainHeight: get(blockchainHeight, 'quantity', 0), - localBlockchainHeight: localBlockchainHeight.quantity, - localTimeInformation: { - status: localTimeInformation.status, - difference: get( - localTimeInformation, - 'localTimeDifference.quantity', - null - ), + syncProgress, + localTip: { + epoch: get(node_tip, 'epoch_number', 0), + slot: get(node_tip, 'slot_number', 0), + }, + networkTip: { + epoch: get(network_tip, 'epoch_number', 0), + slot: get(network_tip, 'slot_number', 0), + }, + nextEpoch: { + // N+1 epoch + epochNumber: get(next_epoch, 'epoch_number', 0), + epochStart: get(next_epoch, 'epoch_start_time', ''), + }, + futureEpoch: { + // N+2 epoch + epochNumber: get(next_epoch, 'epoch_number', 0) + 1, + epochStart: moment(get(next_epoch, 'epoch_start_time', 0)) + .add(EPOCH_LENGTH_ITN, 'seconds') + .toISOString(), }, }; } catch (error) { - Logger.error(`${loggerText} error`, { error }); - if (error.code === TlsCertificateNotValidError.API_ERROR) { + logger.error('AdaApi::getNetworkInfo error', { error }); + // Special Error case + if ( + error.code === TlsCertificateNotValidError.API_ERROR || + error.code === 'EPROTO' + ) { throw new TlsCertificateNotValidError(); } - throw new GenericApiError(error); + throw new ApiError(error); } }; - getNodeSettings = async (): Promise => { - Logger.debug('AdaApi::getNodeSettings called'); + getNetworkClock = async ( + isForceCheck: boolean + ): Promise => { + logger.debug('AdaApi::getNetworkClock called', { isForceCheck }); try { - const nodeSettings: NodeSettingsResponse = await getNodeSettings( - this.config + const networkClock: NetworkClockResponse = await getNetworkClock( + this.config, + isForceCheck ); - Logger.debug('AdaApi::getNodeSettings success', { - nodeSettings, + logger.debug('AdaApi::getNetworkClock success', { + networkClock, + isForceCheck, }); - const { slotId } = nodeSettings; - return { slotId }; + return { + status: networkClock.status, + offset: get(networkClock, 'offset.quantity', null), + }; } catch (error) { - Logger.error('AdaApi::getNodeSettings error', { error }); - if (error.code === TlsCertificateNotValidError.API_ERROR) { - throw new TlsCertificateNotValidError(); - } - throw new GenericApiError(error); + logger.error('AdaApi::getNetworkClock error', { error, isForceCheck }); + throw new ApiError(error); } }; - getCurrentEpochFallback = async (): Promise => { - Logger.debug('AdaApi::getCurrentEpochFallback called'); + getNetworkParameters = async ( + epochId: number + ): Promise => { + logger.debug('AdaApi::getNetworkParameters called'); try { - const currentEpochInfo: CardanoExplorerResponse = await getCurrentEpoch(); - const currentEpochPath = 'Right[1][0].cbeEpoch'; - const currentEpoch = get(currentEpochInfo, currentEpochPath, null); - Logger.debug('AdaApi::getCurrentEpochFallback success', { - currentEpoch, - currentEpochInfo, + const networkParameters: NetworkParametersResponse = await getNetworkParameters( + epochId, + this.config + ); + logger.debug('AdaApi::getNetworkParameters success', { + networkParameters, }); - return { currentEpoch }; + + const { + genesis_block_hash: genesisBlockHash, + blockchain_start_time, // eslint-disable-line + slot_length: slotLength, + epoch_length: epochLength, + epoch_stability: epochStability, + active_slot_coefficient: activeSlotCoefficient, + } = networkParameters; + const blockchainStartTime = moment(blockchain_start_time).valueOf(); + + return { + genesisBlockHash, + blockchainStartTime, + slotLength, + epochLength, + epochStability, + activeSlotCoefficient, + }; } catch (error) { - Logger.error('AdaApi::getCurrentEpochFallback error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getNetworkParameters error', { error }); + throw new ApiError(error); } }; getLatestAppVersion = async (): Promise => { - Logger.debug('AdaApi::getLatestAppVersion called'); + logger.debug('AdaApi::getLatestAppVersion called'); try { const { isWindows, platform } = global.environment; const latestAppVersionInfo: LatestAppVersionInfoResponse = await getLatestAppVersion(); @@ -1036,20 +1672,20 @@ export default class AdaApi { applicationVersionPath, null ); - Logger.debug('AdaApi::getLatestAppVersion success', { + logger.debug('AdaApi::getLatestAppVersion success', { latestAppVersion, latestAppVersionInfo, applicationVersion, }); return { latestAppVersion, applicationVersion }; } catch (error) { - Logger.error('AdaApi::getLatestAppVersion error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getLatestAppVersion error', { error }); + throw new ApiError(error); } }; getNews = async (): Promise => { - Logger.debug('AdaApi::getNews called'); + logger.debug('AdaApi::getNews called'); // Fetch news json let rawNews: string; @@ -1058,7 +1694,7 @@ export default class AdaApi { rawNews = await getNews(); news = JSON.parse(rawNews); } catch (error) { - Logger.error('AdaApi::getNews error', { error }); + logger.error('AdaApi::getNews error', { error }); throw new Error('Unable to fetch news'); } @@ -1069,7 +1705,7 @@ export default class AdaApi { newsHash = await getSHA256HexForString(rawNews); expectedNewsHash = await getNewsHash(news.updatedAt); } catch (error) { - Logger.error('AdaApi::getNews (hash) error', { error }); + logger.error('AdaApi::getNews (hash) error', { error }); throw new Error('Unable to fetch news hash'); } @@ -1077,122 +1713,282 @@ export default class AdaApi { throw new Error('Newsfeed could not be verified'); } - Logger.debug('AdaApi::getNews success', { + logger.debug('AdaApi::getNews success', { updatedAt: news.updatedAt, items: news.items.length, }); return news; }; + calculateDelegationFee = async ( + request: GetDelegationFeeRequest + ): Promise => { + logger.debug('AdaApi::calculateDelegationFee called', { + parameters: filterLogData(request), + }); + try { + const response: DelegationFee = await getDelegationFee(this.config, { + walletId: request.walletId, + }); + logger.debug('AdaApi::calculateDelegationFee success', { response }); + const delegationFee = _createDelegationFeeFromServerData(response); + return delegationFee; + } catch (error) { + logger.error('AdaApi::calculateDelegationFee error', { error }); + throw new ApiError(error); + } + }; + + joinStakePool = async ( + request: JoinStakePoolRequest + ): Promise => { + logger.debug('AdaApi::joinStakePool called', { + parameters: filterLogData(request), + }); + const { walletId, stakePoolId, passphrase } = request; + try { + const response = await joinStakePool(this.config, { + walletId, + stakePoolId, + passphrase, + }); + logger.debug('AdaApi::joinStakePool success', { + stakePool: response, + }); + return response; + } catch (error) { + logger.error('AdaApi::joinStakePool error', { error }); + throw new ApiError(error) + .set('wrongEncryptionPassphrase') + .where('code', 'bad_request') + .inc('message', 'passphrase is too short') + .result(); + } + }; + setCardanoNodeFault = async (fault: FaultInjectionIpcRequest) => { await cardanoFaultInjectionChannel.send(fault); }; // No implementation here but can be overwritten - getLocalTimeDifference: Function; setLocalTimeDifference: Function; + setSyncProgress: Function; setNextUpdate: Function; - setSubscriptionStatus: Function; - setLocalBlockHeight: Function; - setNetworkBlockHeight: Function; setLatestAppVersion: Function; setApplicationVersion: Function; setFaultyNodeSettingsApi: boolean; resetTestOverrides: Function; // Newsfeed testing utility - setFakeNewsFeedJsonForTesting: (fakeNewsfeedJson: GetNewsResponse) => void; + setTestingNewsFeed: (testingNewsFeedData: GetNewsResponse) => void; + setTestingStakePools: (testingStakePoolsData: Array) => void; + setTestingWallets: (testingWalletsData: Array) => void; + setTestingWallet: (testingWalletData: Object, walletIndex?: number) => void; + + // Stake pools testing utility + setFakeStakePoolsJsonForTesting: ( + fakeStakePoolsJson: Array + ) => void; + setStakePoolsFetchingFailed: () => void; } // ========== TRANSFORM SERVER DATA INTO FRONTEND MODELS ========= const _createWalletFromServerData = action( 'AdaApi::_createWalletFromServerData', - (data: AdaWallet) => { + (wallet: AdaWallet) => { const { - id, + id: rawWalletId, + address_pool_gap: addressPoolGap, balance, name, - assuranceLevel, - hasSpendingPassword, - spendingPasswordLastUpdate, - syncState, - createdAt, - } = data; + passphrase, + delegation, + state: syncState, + isLegacy = false, + discovery, + } = wallet; + + const id = isLegacy ? getLegacyWalletId(rawWalletId) : rawWalletId; + const passphraseLastUpdatedAt = get(passphrase, 'last_updated_at', null); + const walletTotalAmount = + balance.total.unit === WalletUnits.LOVELACE + ? new BigNumber(balance.total.quantity).dividedBy(LOVELACES_PER_ADA) + : new BigNumber(balance.total.quantity); + const walletAvailableAmount = + balance.available.unit === WalletUnits.LOVELACE + ? new BigNumber(balance.available.quantity).dividedBy(LOVELACES_PER_ADA) + : new BigNumber(balance.available.quantity); + let walletRewardAmount = 0; + if (!isLegacy) { + walletRewardAmount = + balance.reward.unit === WalletUnits.LOVELACE + ? new BigNumber(balance.reward.quantity).dividedBy(LOVELACES_PER_ADA) + : new BigNumber(balance.reward.quantity); + } + + // Current (Active) + const active = get(delegation, 'active', null); + const target = get(active, 'target', null); + const status = get(active, 'status', null); + const delegatedStakePoolId = isLegacy ? null : target; + const delegationStakePoolStatus = isLegacy ? null : status; + + // Last + const next = get(delegation, 'next', null); + const lastPendingStakePool = next ? last(next) : null; + const lastTarget = get(lastPendingStakePool, 'target', null); + const lastDelegationStakePoolId = isLegacy ? null : lastTarget; return new Wallet({ id, - amount: new BigNumber(balance).dividedBy(LOVELACES_PER_ADA), + addressPoolGap, name, - assurance: assuranceLevel, - hasPassword: hasSpendingPassword, - passwordUpdateDate: new Date(`${spendingPasswordLastUpdate}Z`), + amount: walletTotalAmount, + availableAmount: walletAvailableAmount, + reward: walletRewardAmount, + passwordUpdateDate: + passphraseLastUpdatedAt && new Date(passphraseLastUpdatedAt), + hasPassword: passphraseLastUpdatedAt !== null, syncState, - isLegacy: false, - createdAt, + isLegacy, + delegatedStakePoolId, + delegationStakePoolStatus, + lastDelegationStakePoolId, + pendingDelegations: next, + discovery, }); } ); const _createAddressFromServerData = action( 'AdaApi::_createAddressFromServerData', - (address: Address) => new WalletAddress(address) + (address: Address) => { + const { id, state } = address; + return new WalletAddress({ + id, + used: state === 'used', + }); + } ); -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 _conditionToTxState = (condition: string) => + TransactionStates[condition === 'pending' ? 'PENDING' : 'OK']; const _createTransactionFromServerData = action( 'AdaApi::_createTransactionFromServerData', (data: Transaction) => { const { id, - direction, amount, - confirmations, - creationTime, + inserted_at, // eslint-disable-line camelcase + pending_since, // eslint-disable-line camelcase + depth, + direction, inputs, outputs, status, } = data; + const state = _conditionToTxState(status); + const stateInfo = + state === TransactionStates.PENDING ? pending_since : inserted_at; // eslint-disable-line + const date = get(stateInfo, 'time'); + const slotNumber = get(stateInfo, ['block', 'slot_number'], null); + const epochNumber = get(stateInfo, ['block', 'epoch_number'], null); return new WalletTransaction({ id, + depth, + slotNumber, + epochNumber, title: direction === 'outgoing' ? 'Ada sent' : 'Ada received', type: direction === 'outgoing' - ? transactionTypes.EXPEND - : transactionTypes.INCOME, + ? TransactionTypes.EXPEND + : TransactionTypes.INCOME, amount: new BigNumber( - direction === 'outgoing' ? amount * -1 : amount + direction === 'outgoing' ? amount.quantity * -1 : amount.quantity ).dividedBy(LOVELACES_PER_ADA), - date: utcStringToDate(creationTime), + date: utcStringToDate(date), description: '', - numberOfConfirmations: Math.min( - confirmations, - MAX_TRANSACTION_CONFIRMATIONS + 1 - ), addresses: { - from: inputs.map(({ address }) => address), + from: inputs.map(({ address }) => address || null), to: outputs.map(({ address }) => address), }, - state: _conditionToTxState(status.tag), + state, }); } ); const _createTransactionFeeFromServerData = action( 'AdaApi::_createTransactionFeeFromServerData', - (data: TransactionFee) => - new BigNumber(data.estimatedAmount).dividedBy(LOVELACES_PER_ADA) + (data: TransactionFee) => { + const amount = get(data, ['amount', 'quantity'], 0); + return new BigNumber(amount).dividedBy(LOVELACES_PER_ADA); + } +); + +const _createMigrationFeeFromServerData = action( + 'AdaApi::_createTransactionFeeFromServerData', + (data: TransactionFee) => { + const amount = get(data, ['migration_cost', 'quantity'], 0); + return new BigNumber(amount).dividedBy(LOVELACES_PER_ADA); + } +); + +const _createStakePoolFromServerData = action( + 'AdaApi::_createStakePoolFromServerData', + (stakePool: AdaApiStakePool, index: number) => { + const { + id, + metrics, + apparent_performance: performance, + cost, + margin: profitMargin, + metadata, + saturation, + } = stakePool; + const { + controlled_stake: controlledStake, + produced_blocks: producedBlocks, + } = metrics; // eslint-disable-line + const { + name, + description = '', + ticker, + homepage, + pledge_address: pledgeAddress, + } = metadata; + const controlledStakeQuantity = get(controlledStake, 'quantity', 0); + const producedBlocksCount = get(producedBlocks, 'quantity', 0); + const costQuantity = get(cost, 'quantity', 0); + const profitMarginPercentage = get(profitMargin, 'quantity', 0); + return new StakePool({ + id, + performance: performance * 100, + controlledStake: new BigNumber(controlledStakeQuantity).dividedBy( + LOVELACES_PER_ADA + ), + producedBlocks: producedBlocksCount, + ticker, + homepage, + pledgeAddress, + cost: new BigNumber(costQuantity).dividedBy(LOVELACES_PER_ADA), + description, + isCharity: false, + name, + // pledge: new BigNumber(pledge).dividedBy(LOVELACES_PER_ADA), + profitMargin: profitMarginPercentage, + ranking: index + 1, + retiring: null, + saturation: saturation * 100, + }); + } +); + +const _createDelegationFeeFromServerData = action( + 'AdaApi::_createDelegationFeeFromServerData', + (data: DelegationFee) => { + const amount = get(data, ['amount', 'quantity'], 0); + return new BigNumber(amount).dividedBy(LOVELACES_PER_ADA); + } ); diff --git a/source/renderer/app/api/common/errors.js b/source/renderer/app/api/common/errors.js index ef745164d4..f82e503f25 100644 --- a/source/renderer/app/api/common/errors.js +++ b/source/renderer/app/api/common/errors.js @@ -1,37 +1,18 @@ // @flow import { defineMessages } from 'react-intl'; import LocalizableError from '../../i18n/LocalizableError'; -import globalMessages from '../../i18n/global-messages'; -const messages = defineMessages({ +export 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 { @@ -44,42 +25,6 @@ export class GenericApiError extends LocalizableError { } } -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({ diff --git a/source/renderer/app/api/common/types.js b/source/renderer/app/api/common/types.js index 770e1454a6..83fe6000ae 100644 --- a/source/renderer/app/api/common/types.js +++ b/source/renderer/app/api/common/types.js @@ -1,24 +1,8 @@ // @flow -export type RequestConfig = { +export type RequestConfig = $Exact<{ hostname: string, 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, - }, -}; +}>; diff --git a/source/renderer/app/api/errors.js b/source/renderer/app/api/errors.js new file mode 100644 index 0000000000..56b4b71ff2 --- /dev/null +++ b/source/renderer/app/api/errors.js @@ -0,0 +1,90 @@ +import { defineMessages } from 'react-intl'; + +export const messages = defineMessages({ + // common + wrongEncryptionPassphrase: { + id: 'api.errors.IncorrectPasswordError', + defaultMessage: '!!!Incorrect wallet password.', + description: '"Incorrect wallet password." error message.', + }, + // wallets + walletAlreadyExists: { + id: 'api.errors.WalletAlreadyRestoredError', + defaultMessage: '!!!Wallet you are trying to restore already exists.', + description: + '"Wallet you are trying to restore already exists." error message.', + }, + forbiddenMnemonic: { + 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', + }, + walletAlreadyImported: { + 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.', + }, + invalidMnemonic: { + id: 'global.errors.invalidMnemonic', + defaultMessage: '!!!Invalid phrase entered, please check.', + description: 'Error message shown when invalid bip39 mnemonic was entered.', + }, + // transactions + notEnoughMoney: { + id: 'api.errors.NotEnoughMoneyToSendError', + defaultMessage: '!!!Not enough money to make this transaction.', + description: '"Not enough money to make this transaction." error message.', + }, + canNotCalculateTransactionFees: { + id: 'api.errors.CanNotCalculateTransactionFeesError', + defaultMessage: + '!!!Cannot calculate fees while there are pending transactions.', + description: + '"Cannot calculate fees while there are pending transactions." error message', + }, + cannotCoverFee: { + id: 'api.errors.NotEnoughFundsForTransactionFeesError', + defaultMessage: '!!!Not enough ada for fees. Try sending a smaller amount.', + description: + '"Not enough ada for fees. Try sending a smaller amount." error message', + }, + transactionIsTooBig: { + id: 'api.errors.TooBigTransactionError', + defaultMessage: '!!!Transaction too big due to too many inputs.', + description: '"Transaction too big due to too many inputs." error message.', + }, + notEnoughFundsForTransaction: { + id: 'api.errors.NotEnoughFundsForTransactionError', + defaultMessage: '!!!Not enough ada . Try sending a smaller amount.', + description: + '"Not enough ada . Try sending a smaller amount." error message', + }, + invalidAddress: { + id: 'api.errors.invalidAddress', + defaultMessage: '!!!Please enter a valid address.', + description: 'Error message shown when invalid address was entered.', + }, + tooBigTransactionErrorLinkLabel: { + id: 'api.errors.TooBigTransactionErrorLinkLabel', + defaultMessage: '!!!Learn more.', + description: + '"Transaction too big due to too many inputs." error link label.', + }, + tooBigTransactionErrorLinkURL: { + id: 'api.errors.TooBigTransactionErrorLinkURL', + defaultMessage: + '!!!https://iohk.zendesk.com/hc/en-us/articles/360017733353', + description: + '"Transaction too big due to too many inputs." error link URL.', + }, +}); diff --git a/source/renderer/app/api/index.js b/source/renderer/app/api/index.js index 7ac262bcb4..ab532ed216 100644 --- a/source/renderer/app/api/index.js +++ b/source/renderer/app/api/index.js @@ -5,6 +5,7 @@ import LocalStorageApi from './utils/localStorage'; export type Api = { ada: AdaApi, localStorage: LocalStorageApi, + setFaultyNodeSettingsApi?: boolean, }; export const setupApi = (isTest: boolean, network: string): Api => ({ diff --git a/source/renderer/app/api/network/requests/getNetworkClock.js b/source/renderer/app/api/network/requests/getNetworkClock.js new file mode 100644 index 0000000000..5c656c25b8 --- /dev/null +++ b/source/renderer/app/api/network/requests/getNetworkClock.js @@ -0,0 +1,17 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { NetworkClockResponse } from '../types'; +import { request } from '../../utils/request'; + +export const getNetworkClock = ( + config: RequestConfig, + isForceCheck: boolean +): Promise => + request( + { + method: 'GET', + path: '/v2/network/clock', + ...config, + }, + { forceNtpCheck: isForceCheck } + ); diff --git a/source/renderer/app/api/nodes/requests/getNodeSettings.js b/source/renderer/app/api/network/requests/getNetworkInfo.js similarity index 54% rename from source/renderer/app/api/nodes/requests/getNodeSettings.js rename to source/renderer/app/api/network/requests/getNetworkInfo.js index f57b99aeef..f76b88d481 100644 --- a/source/renderer/app/api/nodes/requests/getNodeSettings.js +++ b/source/renderer/app/api/network/requests/getNetworkInfo.js @@ -1,13 +1,13 @@ // @flow import type { RequestConfig } from '../../common/types'; -import type { NodeSettingsResponse } from '../types'; +import type { NetworkInfoResponse } from '../types'; import { request } from '../../utils/request'; -export const getNodeSettings = ( +export const getNetworkInfo = ( config: RequestConfig -): Promise => +): Promise => request({ method: 'GET', - path: '/api/v1/node-settings', + path: '/v2/network/information', ...config, }); diff --git a/source/renderer/app/api/network/requests/getNetworkParameters.js b/source/renderer/app/api/network/requests/getNetworkParameters.js new file mode 100644 index 0000000000..3e4f2b4efa --- /dev/null +++ b/source/renderer/app/api/network/requests/getNetworkParameters.js @@ -0,0 +1,14 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { NetworkParametersResponse } from '../types'; +import { request } from '../../utils/request'; + +export const getNetworkParameters = ( + epochId: number, + config: RequestConfig +): Promise => + request({ + method: 'GET', + path: `/v2/network/parameters/${epochId}`, + ...config, + }); diff --git a/source/renderer/app/api/network/types.js b/source/renderer/app/api/network/types.js new file mode 100644 index 0000000000..84a8bc6ca1 --- /dev/null +++ b/source/renderer/app/api/network/types.js @@ -0,0 +1,102 @@ +// @flow +export type TipInfo = { + epoch: number, + slot: number, +}; + +export type NextEpoch = { + epochNumber: number, + epochStart: string, +}; + +export type FutureEpoch = { + epochNumber: number, + epochStart: string, +}; + +export type ClockOffset = { + quantity: number, + unit: 'microsecond', +}; + +export type SlotLength = { + quantity: number, + unit: string, +}; + +export type EpochLength = { + quantity: number, + unit: string, +}; + +export type EpochStability = { + quantity: number, + unit: string, +}; + +export type ActiveSlotCoefficient = { + quantity: number, + unit: string, +}; + +export type GetNetworkInfoResponse = { + syncProgress: number, + localTip: TipInfo, + networkTip: TipInfo, + nextEpoch: NextEpoch, + futureEpoch: FutureEpoch, +}; + +export type NetworkInfoResponse = { + sync_progress: { + status: 'ready' | 'syncing', + progress?: { + quantity: number, + unit: 'percent', + }, + }, + node_tip: { + slot_number: number, + epoch_number: number, + height: { + quantity: number, + unit: 'block', + }, + }, + network_tip: { + slot_number: number, + epoch_number: number, + }, + next_epoch: { + epoch_number: number, + epoch_start_time: string, + }, +}; + +export type NetworkClockResponse = { + status: 'available' | 'unavailable' | 'pending', + offset?: ClockOffset, +}; + +export type GetNetworkClockResponse = { + status: 'available' | 'unavailable' | 'pending', + offset: ?number, +}; + +export type GetNetworkParametersResponse = { + genesisBlockHash: string, + blockchainStartTime: number, + slotLength: SlotLength, + epochLength: EpochLength, + epochStability: EpochStability, + activeSlotCoefficient: ActiveSlotCoefficient, +}; + +export type NetworkParametersResponse = { + genesis_block_hash: string, + blockchain_start_time: string, + slot_length: SlotLength, + epoch_length: EpochLength, + epoch_stability: EpochStability, + active_slot_coefficient: ActiveSlotCoefficient, +}; diff --git a/source/renderer/app/api/news/requests/getNews.js b/source/renderer/app/api/news/requests/getNews.js index 444a5259f8..a1291723ae 100644 --- a/source/renderer/app/api/news/requests/getNews.js +++ b/source/renderer/app/api/news/requests/getNews.js @@ -2,10 +2,13 @@ import { externalRequest } from '../../utils/externalRequest'; import { getNewsURL } from '../../../utils/network'; -const { network } = global.environment; +const { isFlight, environment } = global; +const { network } = environment; const hostname = getNewsURL(network); const path = '/newsfeed'; -const filename = `newsfeed_${network}.json`; +const filename = isFlight + ? 'newsfeed_mainnet_flight.json' + : `newsfeed_${network}.json`; export const getNews = (): Promise => externalRequest( diff --git a/source/renderer/app/api/news/requests/getNewsHash.js b/source/renderer/app/api/news/requests/getNewsHash.js index 522b3b4c4f..0915b6be1c 100644 --- a/source/renderer/app/api/news/requests/getNewsHash.js +++ b/source/renderer/app/api/news/requests/getNewsHash.js @@ -2,9 +2,12 @@ import { externalRequest } from '../../utils/externalRequest'; import { getNewsHashURL } from '../../../utils/network'; -const { network } = global.environment; +const { isFlight, environment } = global; +const { network } = environment; const hostname = getNewsHashURL(network); -const path = `/newsfeed-verification/${network}`; +const path = isFlight + ? '/newsfeed-verification/mainnet_flight' + : `/newsfeed-verification/${network}`; export const getNewsHash = (timestamp: number): Promise => externalRequest( diff --git a/source/renderer/app/api/nodes/requests/getCurrentEpoch.js b/source/renderer/app/api/nodes/requests/getCurrentEpoch.js deleted file mode 100644 index 63e38be4a1..0000000000 --- a/source/renderer/app/api/nodes/requests/getCurrentEpoch.js +++ /dev/null @@ -1,16 +0,0 @@ -// @flow -import type { CardanoExplorerResponse } from '../types'; -import { externalRequest } from '../../utils/externalRequest'; -import { getNetworkExplorerUri } from '../../../utils/network'; - -const { isStaging, isTestnet, network } = global.environment; -const hostname = getNetworkExplorerUri(network); -const protocol = isStaging || isTestnet ? 'http' : 'https'; - -export const getCurrentEpoch = (): Promise => - externalRequest({ - hostname, - path: '/api/blocks/pages', - method: 'GET', - protocol, - }); diff --git a/source/renderer/app/api/nodes/requests/getNodeInfo.js b/source/renderer/app/api/nodes/requests/getNodeInfo.js deleted file mode 100644 index 9c1682f58c..0000000000 --- a/source/renderer/app/api/nodes/requests/getNodeInfo.js +++ /dev/null @@ -1,21 +0,0 @@ -// @flow -import type { RequestConfig } from '../../common/types'; -import type { NodeInfoResponse } from '../types'; -import { request } from '../../utils/request'; - -export type NodeInfoQueryParams = { - force_ntp_check: boolean, -}; - -export const getNodeInfo = ( - config: RequestConfig, - queryInfoParams?: NodeInfoQueryParams -): Promise => - request( - { - method: 'GET', - path: '/api/v1/node-info', - ...config, - }, - queryInfoParams - ); diff --git a/source/renderer/app/api/nodes/types.js b/source/renderer/app/api/nodes/types.js index 10f4097412..201facbda0 100644 --- a/source/renderer/app/api/nodes/types.js +++ b/source/renderer/app/api/nodes/types.js @@ -1,50 +1,4 @@ // @flow -export type LocalTimeInformationStatus = - | 'unavailable' - | 'pending' - | 'available'; - -export type NodeInfoResponse = { - syncProgress: { - quantity: number, - unit: 'percent', - }, - blockchainHeight: ?{ - quantity: number, - unit: ?'blocks', - }, - localBlockchainHeight: { - quantity: number, - unit: ?'blocks', - }, - localTimeInformation: { - status: LocalTimeInformationStatus, - localTimeDifference?: { - quantity: number, - unit: ?'microseconds', - }, - }, - subscriptionStatus: Object, -}; - -export type NodeSettingsResponse = { - slotDuration: { - quantity: number, - unit: ?'milliseconds', - }, - softwareInfo: NodeSoftware, - projectVersion: string, - gitRevision: string, - slotId: { - slot: number, - epoch: number, - }, -}; - -export type CardanoExplorerResponse = { - Right: Array, -}; - export type NodeSoftware = { applicationName: string, version: number, @@ -52,27 +6,6 @@ export type NodeSoftware = { // req/res Node Types -export type GetNetworkStatusResponse = { - subscriptionStatus: Object, - syncProgress: number, - blockchainHeight: number, - localBlockchainHeight: number, - localTimeInformation: { - status: LocalTimeInformationStatus, - difference: ?number, - }, -}; - -export type GetNodeSettingsResponse = { - slotId?: { - epoch: number, - }, -}; - -export type GetCurrentEpochFallbackResponse = { - currentEpoch: number, -}; - export type GetLatestAppVersionResponse = { latestAppVersion: ?string, applicationVersion: ?number, diff --git a/source/renderer/app/api/staking/requests/getDelegationFee.js b/source/renderer/app/api/staking/requests/getDelegationFee.js new file mode 100644 index 0000000000..b3ca36af30 --- /dev/null +++ b/source/renderer/app/api/staking/requests/getDelegationFee.js @@ -0,0 +1,14 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { DelegationFee, GetDelegationFeeRequest } from '../types'; +import { request } from '../../utils/request'; + +export const getDelegationFee = ( + config: RequestConfig, + { walletId }: GetDelegationFeeRequest +): Promise => + request({ + method: 'GET', + path: `/v2/wallets/${walletId}/delegation-fees`, + ...config, + }); diff --git a/source/renderer/app/api/staking/requests/getStakePools.js b/source/renderer/app/api/staking/requests/getStakePools.js new file mode 100644 index 0000000000..0763420424 --- /dev/null +++ b/source/renderer/app/api/staking/requests/getStakePools.js @@ -0,0 +1,13 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaApiStakePools } from '../types'; +import { request } from '../../utils/request'; + +export const getStakePools = ( + config: RequestConfig +): Promise => + request({ + method: 'GET', + path: '/v2/stake-pools', + ...config, + }); diff --git a/source/renderer/app/api/staking/requests/joinStakePool.js b/source/renderer/app/api/staking/requests/joinStakePool.js new file mode 100644 index 0000000000..62f115bb28 --- /dev/null +++ b/source/renderer/app/api/staking/requests/joinStakePool.js @@ -0,0 +1,19 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { JoinStakePoolRequest } from '../types'; +import type { Transaction } from '../../transactions/types'; +import { request } from '../../utils/request'; + +export const joinStakePool = ( + config: RequestConfig, + { walletId, stakePoolId, passphrase }: JoinStakePoolRequest +): Promise => + request( + { + method: 'PUT', + path: `/v2/stake-pools/${stakePoolId}/wallets/${walletId}`, + ...config, + }, + {}, + { passphrase } + ); diff --git a/source/renderer/app/api/staking/requests/quitStakePool.js b/source/renderer/app/api/staking/requests/quitStakePool.js new file mode 100644 index 0000000000..7a8dea4b14 --- /dev/null +++ b/source/renderer/app/api/staking/requests/quitStakePool.js @@ -0,0 +1,19 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { QuitStakePoolRequest } from '../types'; +import type { Transaction } from '../../transactions/types'; +import { request } from '../../utils/request'; + +export const quitStakePool = ( + config: RequestConfig, + { walletId, passphrase }: QuitStakePoolRequest +): Promise => + request( + { + method: 'DELETE', + path: `/v2/stake-pools/*/wallets/${walletId}`, + ...config, + }, + {}, + { passphrase } + ); diff --git a/source/renderer/app/api/staking/types.js b/source/renderer/app/api/staking/types.js index 516cb81c4a..82680853fc 100644 --- a/source/renderer/app/api/staking/types.js +++ b/source/renderer/app/api/staking/types.js @@ -1,29 +1,61 @@ // @flow +import BigNumber from 'bignumber.js'; +import { WalletUnits } from '../../domains/Wallet'; +import StakePool from '../../domains/StakePool'; -export type StakePool = { +export type DelegationAction = + | 'changeDelegation' + | 'removeDelegation' + | 'delegate'; + +export type AdaApiStakePool = { id: string, - controlledStake: number, - description: string, - slug: string, - name: string, - performance: number, - profitMargin: number, - ranking: number, - retiring?: Date, - created_at: Date, - isCharity: boolean, - url: string, + metrics: { + controlled_stake: { + quantity: number, + unit: 'lovelace', + }, + produced_blocks: { + quantity: number, + unit: 'block', + }, + }, + apparent_performance: number, + cost: { + quantity: number, + unit: 'lovelace', + }, + margin: { + quantity: number, + unit: 'percent', + }, + metadata: { + owner: string, + ticker: string, // [3 .. 5] characters + name: string, // [1 .. 50] characters + description?: string, // <= 255 characters + homepage: string, + pledge_address: string, + }, + saturation: number, + desirability: number, }; - -export type StakePoolsListType = Array; +export type AdaApiStakePools = Array; export type Reward = { date: string, wallet: string, - amount: number, + reward: BigNumber, pool: StakePool, }; +export type RewardForIncentivizedTestnet = { + date?: string, + wallet: string, + reward: BigNumber, + pool?: StakePool, +}; + export type EpochData = { pool: StakePool, slotsElected: Array, @@ -38,3 +70,25 @@ export type Epoch = { endsAt?: string, data: Array, }; + +export type JoinStakePoolRequest = { + walletId: string, + stakePoolId: string, + passphrase: string, +}; + +export type GetDelegationFeeRequest = { + walletId: string, +}; + +export type DelegationFee = { + amount: { + quantity: number, + unit: WalletUnits.LOVELACE, + }, +}; + +export type QuitStakePoolRequest = { + walletId: string, + passphrase: string, +}; diff --git a/source/renderer/app/api/transactions/errors.js b/source/renderer/app/api/transactions/errors.js deleted file mode 100644 index 2f76e3274f..0000000000 --- a/source/renderer/app/api/transactions/errors.js +++ /dev/null @@ -1,149 +0,0 @@ -import { defineMessages } from 'react-intl'; -import LocalizableError from '../../i18n/LocalizableError'; - -const messages = defineMessages({ - 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.", - description: - '"It\'s not allowed to send money to the same address you are sending from." error message.', - }, - notAllowedToSendMoneyToRedeemAddressError: { - id: 'api.errors.NotAllowedToSendMoneyToRedeemAddressError', - 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.', - }, - allFundsAlreadyAtReceiverAddressError: { - id: 'api.errors.AllFundsAlreadyAtReceiverAddressError', - defaultMessage: - '!!!All your funds are already at the address you are trying send money to.', - description: - '"All your funds are already at the address you are trying send money to." error message.', - }, - notEnoughFundsForTransactionFeesError: { - id: 'api.errors.NotEnoughFundsForTransactionFeesError', - 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', - }, - tooBigTransactionError: { - id: 'api.errors.TooBigTransactionError', - defaultMessage: '!!!Transaction too big due to too many inputs.', - description: '"Transaction too big due to too many inputs." error message.', - }, - tooBigTransactionErrorLinkLabel: { - id: 'api.errors.TooBigTransactionErrorLinkLabel', - defaultMessage: '!!!Learn more.', - description: - '"Transaction too big due to too many inputs." error link label.', - }, - tooBigTransactionErrorLinkURL: { - id: 'api.errors.TooBigTransactionErrorLinkURL', - defaultMessage: - '!!!https://iohk.zendesk.com/hc/en-us/articles/360017733353', - description: - '"Transaction too big due to too many inputs." error link URL.', - }, -}); - -export class NotAllowedToSendMoneyToSameAddressError extends LocalizableError { - constructor() { - super({ - id: messages.notAllowedToSendMoneyToSameAddressError.id, - defaultMessage: - messages.notAllowedToSendMoneyToSameAddressError.defaultMessage, - }); - } -} - -export class NotAllowedToSendMoneyToRedeemAddressError extends LocalizableError { - constructor() { - super({ - id: messages.notAllowedToSendMoneyToRedeemAddressError.id, - defaultMessage: - messages.notAllowedToSendMoneyToRedeemAddressError.defaultMessage, - }); - } -} - -export class NotEnoughMoneyToSendError extends LocalizableError { - constructor() { - super({ - id: messages.notEnoughMoneyToSendError.id, - defaultMessage: messages.notEnoughMoneyToSendError.defaultMessage, - }); - } -} - -export class AllFundsAlreadyAtReceiverAddressError extends LocalizableError { - constructor() { - super({ - id: messages.allFundsAlreadyAtReceiverAddressError.id, - defaultMessage: - messages.allFundsAlreadyAtReceiverAddressError.defaultMessage, - }); - } -} - -export class NotEnoughFundsForTransactionFeesError extends LocalizableError { - constructor() { - super({ - id: messages.notEnoughFundsForTransactionFeesError.id, - defaultMessage: - messages.notEnoughFundsForTransactionFeesError.defaultMessage, - }); - } -} - -export class NotEnoughFundsForTransactionError extends LocalizableError { - constructor() { - super({ - id: messages.notEnoughFundsForTransactionError.id, - defaultMessage: messages.notEnoughFundsForTransactionError.defaultMessage, - }); - } -} - -export class CanNotCalculateTransactionFeesError extends LocalizableError { - constructor() { - super({ - id: messages.canNotCalculateTransactionFeesError.id, - defaultMessage: - messages.canNotCalculateTransactionFeesError.defaultMessage, - }); - } -} - -export class TooBigTransactionError extends LocalizableError { - constructor() { - super({ - id: messages.tooBigTransactionError.id, - defaultMessage: messages.tooBigTransactionError.defaultMessage, - values: { - linkLabel: messages.tooBigTransactionErrorLinkLabel, - linkURL: messages.tooBigTransactionErrorLinkURL, - }, - }); - } -} diff --git a/source/renderer/app/api/transactions/requests/createByronWalletTransaction.js b/source/renderer/app/api/transactions/requests/createByronWalletTransaction.js new file mode 100644 index 0000000000..0f61d24ffc --- /dev/null +++ b/source/renderer/app/api/transactions/requests/createByronWalletTransaction.js @@ -0,0 +1,27 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Transaction, TransactionPaymentData } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export type TransactionParams = { + walletId: string, + data: { + payments: Array, + passphrase: string, + }, +}; + +export const createByronWalletTransaction = ( + config: RequestConfig, + { walletId, data }: TransactionParams +): Promise => + request( + { + method: 'POST', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}/transactions/`, + ...config, + }, + {}, + data + ); diff --git a/source/renderer/app/api/transactions/requests/createTransaction.js b/source/renderer/app/api/transactions/requests/createTransaction.js index b693dfbf50..d431c4a60c 100644 --- a/source/renderer/app/api/transactions/requests/createTransaction.js +++ b/source/renderer/app/api/transactions/requests/createTransaction.js @@ -1,28 +1,24 @@ // @flow import type { RequestConfig } from '../../common/types'; -import type { Transaction, PaymentDistribution } from '../types'; +import type { Transaction, TransactionPaymentData } from '../types'; import { request } from '../../utils/request'; export type TransactionParams = { + walletId: string, data: { - source: { - accountIndex: number, - walletId: string, - }, - destinations: Array, - groupingPolicy: ?'OptimizeForSecurity' | 'OptimizeForSize', - spendingPassword?: string, + payments: Array, + passphrase: string, }, }; export const createTransaction = ( config: RequestConfig, - { data }: TransactionParams + { walletId, data }: TransactionParams ): Promise => request( { method: 'POST', - path: '/api/v1/transactions', + path: `/v2/wallets/${walletId}/transactions/`, ...config, }, {}, diff --git a/source/renderer/app/api/transactions/requests/deleteLegacyTransaction.js b/source/renderer/app/api/transactions/requests/deleteLegacyTransaction.js new file mode 100644 index 0000000000..79d06b6a78 --- /dev/null +++ b/source/renderer/app/api/transactions/requests/deleteLegacyTransaction.js @@ -0,0 +1,16 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const deleteLegacyTransaction = ( + config: RequestConfig, + { walletId, transactionId }: { walletId: string, transactionId: string } +): Promise<*> => + request({ + method: 'DELETE', + path: `/v2/byron-wallets/${getRawWalletId( + walletId + )}/transactions/${transactionId}`, + ...config, + }); diff --git a/source/renderer/app/api/transactions/requests/deleteTransaction.js b/source/renderer/app/api/transactions/requests/deleteTransaction.js new file mode 100644 index 0000000000..dd659094fe --- /dev/null +++ b/source/renderer/app/api/transactions/requests/deleteTransaction.js @@ -0,0 +1,13 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; + +export const deleteTransaction = ( + config: RequestConfig, + { walletId, transactionId }: { walletId: string, transactionId: string } +): Promise<*> => + request({ + method: 'DELETE', + path: `/v2/wallets/${walletId}/transactions/${transactionId}`, + ...config, + }); diff --git a/source/renderer/app/api/transactions/requests/getByronWalletTransactionFee.js b/source/renderer/app/api/transactions/requests/getByronWalletTransactionFee.js new file mode 100644 index 0000000000..c559170560 --- /dev/null +++ b/source/renderer/app/api/transactions/requests/getByronWalletTransactionFee.js @@ -0,0 +1,26 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { TransactionPaymentData, TransactionFee } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export type GetTransactionFeeParams = { + walletId: string, + data: { + payments: Array, + }, +}; + +export const getByronWalletTransactionFee = ( + config: RequestConfig, + { walletId, data }: GetTransactionFeeParams +): Promise => + request( + { + method: 'POST', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}/payment-fees`, + ...config, + }, + {}, + data + ); diff --git a/source/renderer/app/api/transactions/requests/getLegacyWalletTransactionHistory.js b/source/renderer/app/api/transactions/requests/getLegacyWalletTransactionHistory.js new file mode 100644 index 0000000000..465d6390c5 --- /dev/null +++ b/source/renderer/app/api/transactions/requests/getLegacyWalletTransactionHistory.js @@ -0,0 +1,26 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { Transactions } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export type GetTransactionsQueryParams = { + start?: string, + end?: string, + order: 'ascending' | 'descending', +}; + +export const getLegacyWalletTransactionHistory = ( + config: RequestConfig, + walletId: string, + { ...queryParams }: GetTransactionsQueryParams +): Promise => + request( + { + method: 'GET', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}/transactions`, + ...config, + }, + queryParams, + null + ); diff --git a/source/renderer/app/api/transactions/requests/getTransactionFee.js b/source/renderer/app/api/transactions/requests/getTransactionFee.js index e165f5383b..3cdf5d0900 100644 --- a/source/renderer/app/api/transactions/requests/getTransactionFee.js +++ b/source/renderer/app/api/transactions/requests/getTransactionFee.js @@ -1,17 +1,23 @@ // @flow import type { RequestConfig } from '../../common/types'; -import type { TransactionParams } from './createTransaction'; -import type { TransactionFee } from '../types'; +import type { TransactionPaymentData, TransactionFee } from '../types'; import { request } from '../../utils/request'; +export type GetTransactionFeeParams = { + walletId: string, + data: { + payments: Array, + }, +}; + export const getTransactionFee = ( config: RequestConfig, - { data }: TransactionParams + { walletId, data }: GetTransactionFeeParams ): Promise => request( { method: 'POST', - path: '/api/v1/transactions/fees', + path: `/v2/wallets/${walletId}/payment-fees`, ...config, }, {}, diff --git a/source/renderer/app/api/transactions/requests/getTransactionHistory.js b/source/renderer/app/api/transactions/requests/getTransactionHistory.js index 253e5d501c..d5d0350fae 100644 --- a/source/renderer/app/api/transactions/requests/getTransactionHistory.js +++ b/source/renderer/app/api/transactions/requests/getTransactionHistory.js @@ -3,30 +3,23 @@ import type { RequestConfig } from '../../common/types'; import type { Transactions } from '../types'; import { request } from '../../utils/request'; -export type GetTxnHistoryParams = { - wallet_id: string, - account_index: number, - page: number, - per_page: number, - sort_by: string, - created_at: string, -}; - -const requestOptions = { - returnMeta: true, +export type GetTransactionsQueryParams = { + start?: string, + end?: string, + order: 'ascending' | 'descending', }; export const getTransactionHistory = ( config: RequestConfig, - { ...requestParams }: GetTxnHistoryParams + walletId: string, + { ...queryParams }: GetTransactionsQueryParams ): Promise => request( { method: 'GET', - path: '/api/v1/transactions', + path: `/v2/wallets/${walletId}/transactions`, ...config, }, - requestParams, - null, - requestOptions + queryParams, + null ); diff --git a/source/renderer/app/api/transactions/types.js b/source/renderer/app/api/transactions/types.js index 66517d23b6..eb2876ee4c 100644 --- a/source/renderer/app/api/transactions/types.js +++ b/source/renderer/app/api/transactions/types.js @@ -1,65 +1,122 @@ // @flow import BigNumber from 'bignumber.js'; import { WalletTransaction } from '../../domains/WalletTransaction'; -import type { ResponseBase } from '../common/types'; +import { WalletUnits } from '../../domains/Wallet'; -export type Transactions = ResponseBase & { - data: Array, +export type TransactionAmount = { + quantity: number, + unit: WalletUnits.LOVELACE, +}; + +export type TransactionDepth = { + quantity: number, + unit: 'block', +}; + +export type TransactionInsertionBlock = { + slot_number: number, + epoch_number: number, }; 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: {}, + amount: TransactionAmount, + inserted_at?: { + time: Date, + block: TransactionInsertionBlock, }, + pending_since?: { + time: Date, + block: { + ...TransactionInsertionBlock, + height: { + quantity: number, + unit: string, + }, + }, + }, + depth: TransactionDepth, + direction: 'outgoing' | 'incoming', + inputs: Array, + outputs: Array, + status: TransactionState, }; -export type PaymentDistribution = { +export type Transactions = Array; + +export type TransactionInputs = { address: string, - amount: number, + amount?: TransactionAmount, + id: string, + index: number, }; -export type TxnAssuranceLevel = 'low' | 'medium' | 'high'; +export type TransactionOutputs = { + address: string, + amount: TransactionAmount, +}; -export type TransactionState = 'pending' | 'failed' | 'ok'; +export type TransactionState = 'pending' | 'in_ledger'; -export type TransactionFee = ResponseBase & { - estimatedAmount: number, -}; +export type TrasactionAddresses = { from: Array, to: Array }; -export type TrasactionAddresses = { from: Array, to: Array }; export type TransactionType = 'card' | 'expend' | 'income' | 'exchange'; -// req/res Transaction Types +// Req / Res Transaction Types export type GetTransactionsRequest = { walletId: string, - searchTerm: string, - skip: number, - limit: number, - isFirstLoad: boolean, - isRestoreActive: boolean, - isRestoreCompleted: boolean, - cachedTransactions: Array, + order?: 'ascending' | 'descending', + fromDate: ?string, + toDate: ?string, + isLegacy: boolean, + // @API TODO - Params "pending" for V2 + // searchTerm: string, + // skip: number, + // limit: number, + // isFirstLoad: boolean, + // isRestoreActive: boolean, + // isRestoreCompleted: boolean, + // cachedTransactions: Array, }; -export type TransactionRequest = { - accountIndex: number, +export type GetTransactionFeeRequest = { walletId: string, + address: string, + amount: number, walletBalance: BigNumber, + availableBalance: BigNumber, + isLegacy: boolean, +}; + +export type CreateTransactionRequest = { + walletId: string, address: string, amount: number, - spendingPassword?: ?string, + passphrase: string, + isLegacy: boolean, +}; + +export type DeleteTransactionRequest = { + walletId: string, + transactionId: string, + isLegacy: boolean, }; export type GetTransactionsResponse = { transactions: Array, total: number, }; + +export type TransactionFeeAmount = { + quantity: number, + unit: WalletUnits.LOVELACE, +}; + +export type TransactionPaymentData = { + address: string, + amount: TransactionFeeAmount, +}; + +export type TransactionFee = { + amount: TransactionFeeAmount, +}; diff --git a/source/renderer/app/api/utils/apiHelpers.js b/source/renderer/app/api/utils/apiHelpers.js index bca8a03dcf..cbdfc6d53c 100644 --- a/source/renderer/app/api/utils/apiHelpers.js +++ b/source/renderer/app/api/utils/apiHelpers.js @@ -21,3 +21,7 @@ export const testSync = (apiMethod: Function) => { console.log(`testSync result: ${result}`); return result; }; + +// helper code for deferring API call execution +export const wait = (ms: number): Promise => + new Promise(resolve => setTimeout(resolve, ms)); diff --git a/source/renderer/app/api/utils/index.js b/source/renderer/app/api/utils/index.js index 194da06851..0a8e597f92 100644 --- a/source/renderer/app/api/utils/index.js +++ b/source/renderer/app/api/utils/index.js @@ -12,10 +12,17 @@ export const utcStringToDate = (createDate: string) => const bytesToB16 = bytes => Buffer.from(bytes).toString('hex'); const blake2b = data => blakejs.blake2b(data, null, 32); -export const encryptPassphrase = (passphrase: ?string) => +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; + +// legacy wallet id utils +const LEGACY_WALLET_ID_PREFIX = 'legacy_'; +export const getLegacyWalletId = (rawWalletId: string) => + `${LEGACY_WALLET_ID_PREFIX}${rawWalletId}`; +export const getRawWalletId = (legacyWalletId: string) => + legacyWalletId.replace(LEGACY_WALLET_ID_PREFIX, ''); diff --git a/source/renderer/app/api/utils/localStorage.js b/source/renderer/app/api/utils/localStorage.js index 107122df1c..bbccee575b 100644 --- a/source/renderer/app/api/utils/localStorage.js +++ b/source/renderer/app/api/utils/localStorage.js @@ -3,9 +3,10 @@ /* eslint-disable consistent-return */ import { includes } from 'lodash'; +import { electronStoreConversation } from '../../ipc/electronStoreConversation'; import type { NewsTimestamp } from '../news/types'; - -const store = global.electronStore; +import type { WalletMigrationStatus } from '../../stores/WalletMigrationStore'; +import { WalletMigrationStatuses } from '../../stores/WalletMigrationStore'; export type WalletLocalData = { id: string, @@ -18,12 +19,7 @@ export type WalletsLocalData = { }; type StorageKeys = { - USER_LOCALE: string, - TERMS_OF_USE_ACCEPTANCE: string, - THEME: string, - DATA_LAYER_MIGRATION_ACCEPTANCE: string, - READ_NEWS: string, - WALLETS: string, + [key: string]: string, }; /** @@ -32,245 +28,200 @@ type StorageKeys = { */ export default class LocalStorageApi { + static get = async (key: string, fallbackValue: any): Promise => { + const value = await electronStoreConversation.request({ + type: 'get', + key, + }); + if (!value) return fallbackValue; + return value; + }; + + static set = async (key: string, value: any): Promise => { + await electronStoreConversation.request({ + type: 'set', + key, + data: value, + }); + }; + + static unset = async (key: string): Promise => { + await electronStoreConversation.request({ + type: 'delete', + key, + }); + }; + storageKeys: StorageKeys; constructor(NETWORK: string) { - this.storageKeys = { - USER_LOCALE: `${NETWORK}-USER-LOCALE`, - TERMS_OF_USE_ACCEPTANCE: `${NETWORK}-TERMS-OF-USE-ACCEPTANCE`, - THEME: `${NETWORK}-THEME`, - DATA_LAYER_MIGRATION_ACCEPTANCE: `${NETWORK}-DATA-LAYER-MIGRATION-ACCEPTANCE`, - READ_NEWS: `${NETWORK}-READ_NEWS`, - WALLETS: `${NETWORK}-WALLETS`, - }; + const storageKeysRaw = [ + 'USER_LOCALE', + 'USER_NUMBER_FORMAT', + 'USER_DATE_FORMAT_ENGLISH', + 'USER_DATE_FORMAT_JAPANESE', + 'USER_TIME_FORMAT', + 'TERMS_OF_USE_ACCEPTANCE', + 'THEME', + 'DATA_LAYER_MIGRATION_ACCEPTANCE', + 'READ_NEWS', + 'WALLETS', + 'WALLET_MIGRATION_STATUS', + ]; + this.storageKeys = {}; + storageKeysRaw.forEach(key => { + const keyStr = key.replace(new RegExp('_', 'g'), '-'); + this.storageKeys[key] = `${NETWORK}-${keyStr}`; + }); } getUserLocale = (): Promise => - new Promise((resolve, reject) => { - try { - const locale = store.get(this.storageKeys.USER_LOCALE); - if (!locale) return resolve(''); - resolve(locale); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.get(this.storageKeys.USER_LOCALE, ''); setUserLocale = (locale: string): Promise => - new Promise((resolve, reject) => { - try { - store.set(this.storageKeys.USER_LOCALE, locale); - resolve(); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.set(this.storageKeys.USER_LOCALE, locale); unsetUserLocale = (): Promise => - new Promise(resolve => { - try { - store.delete(this.storageKeys.USER_LOCALE); - resolve(); - } catch (error) {} // eslint-disable-line - }); + LocalStorageApi.unset(this.storageKeys.USER_LOCALE); + + getUserNumberFormat = (): Promise => + LocalStorageApi.get(this.storageKeys.USER_NUMBER_FORMAT, ''); + + setUserNumberFormat = (numberFormat: string): Promise => + LocalStorageApi.set(this.storageKeys.USER_NUMBER_FORMAT, numberFormat); + + unsetUserNumberFormat = (): Promise => + LocalStorageApi.unset(this.storageKeys.USER_NUMBER_FORMAT); + + getUserDateFormatEnglish = (): Promise => + LocalStorageApi.get(this.storageKeys.USER_DATE_FORMAT_ENGLISH, ''); + + setUserDateFormatEnglish = (dateFormat: string): Promise => + LocalStorageApi.set(this.storageKeys.USER_DATE_FORMAT_ENGLISH, dateFormat); + + unsetUserDateFormatEnglish = (): Promise => + LocalStorageApi.unset(this.storageKeys.USER_DATE_FORMAT_ENGLISH); + + getUserDateFormatJapanese = (): Promise => + LocalStorageApi.get(this.storageKeys.USER_DATE_FORMAT_JAPANESE, ''); + + setUserDateFormatJapanese = (dateFormat: string): Promise => + LocalStorageApi.set(this.storageKeys.USER_DATE_FORMAT_JAPANESE, dateFormat); + + unsetUserDateFormatJapanese = (): Promise => + LocalStorageApi.unset(this.storageKeys.USER_DATE_FORMAT_JAPANESE); + + getUserTimeFormat = (): Promise => + LocalStorageApi.get(this.storageKeys.USER_TIME_FORMAT, ''); + + setUserTimeFormat = (timeFormat: string): Promise => + LocalStorageApi.set(this.storageKeys.USER_TIME_FORMAT, timeFormat); + + unsetUserTimeFormat = (): Promise => + LocalStorageApi.unset(this.storageKeys.USER_TIME_FORMAT); getTermsOfUseAcceptance = (): Promise => - new Promise((resolve, reject) => { - try { - const accepted = store.get(this.storageKeys.TERMS_OF_USE_ACCEPTANCE); - if (!accepted) return resolve(false); - resolve(accepted); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.get(this.storageKeys.TERMS_OF_USE_ACCEPTANCE, false); setTermsOfUseAcceptance = (): Promise => - new Promise((resolve, reject) => { - try { - store.set(this.storageKeys.TERMS_OF_USE_ACCEPTANCE, true); - resolve(); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.set(this.storageKeys.TERMS_OF_USE_ACCEPTANCE, true); unsetTermsOfUseAcceptance = (): Promise => - new Promise(resolve => { - try { - store.delete(this.storageKeys.TERMS_OF_USE_ACCEPTANCE); - resolve(); - } catch (error) {} // eslint-disable-line - }); + LocalStorageApi.unset(this.storageKeys.TERMS_OF_USE_ACCEPTANCE); getUserTheme = (): Promise => - new Promise((resolve, reject) => { - try { - const theme = store.get(this.storageKeys.THEME); - if (!theme) return resolve(''); - resolve(theme); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.get(this.storageKeys.THEME, ''); setUserTheme = (theme: string): Promise => - new Promise((resolve, reject) => { - try { - store.set(this.storageKeys.THEME, theme); - resolve(); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.set(this.storageKeys.THEME, theme); unsetUserTheme = (): Promise => - new Promise(resolve => { - try { - store.delete(this.storageKeys.THEME); - resolve(); - } catch (error) {} // eslint-disable-line - }); + LocalStorageApi.unset(this.storageKeys.THEME); getDataLayerMigrationAcceptance = (): Promise => - new Promise((resolve, reject) => { - try { - const accepted = store.get( - this.storageKeys.DATA_LAYER_MIGRATION_ACCEPTANCE - ); - if (!accepted) return resolve(false); - resolve(true); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.get( + this.storageKeys.DATA_LAYER_MIGRATION_ACCEPTANCE, + false + ); setDataLayerMigrationAcceptance = (): Promise => - new Promise((resolve, reject) => { - try { - store.set(this.storageKeys.DATA_LAYER_MIGRATION_ACCEPTANCE, true); - resolve(); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.set(this.storageKeys.DATA_LAYER_MIGRATION_ACCEPTANCE, true); unsetDataLayerMigrationAcceptance = (): Promise => - new Promise(resolve => { - try { - store.delete(this.storageKeys.DATA_LAYER_MIGRATION_ACCEPTANCE); - resolve(); - } catch (error) {} // eslint-disable-line - }); + LocalStorageApi.unset(this.storageKeys.DATA_LAYER_MIGRATION_ACCEPTANCE); getWalletsLocalData = (): Promise => - new Promise((resolve, reject) => { - try { - const walletsLocalData = store.get(this.storageKeys.WALLETS); - if (!walletsLocalData) return resolve({}); - return resolve(walletsLocalData); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.get(this.storageKeys.WALLETS, {}); getWalletLocalData = (walletId: string): Promise => - new Promise((resolve, reject) => { - try { - const walletData = store.get(`${this.storageKeys.WALLETS}.${walletId}`); - if (!walletData) { - resolve({ - id: walletId, - }); - } - return resolve(walletData); - } catch (error) { - return reject(error); - } + LocalStorageApi.get(`${this.storageKeys.WALLETS}.${walletId}`, { + id: walletId, }); setWalletLocalData = (walletData: WalletLocalData): Promise => - new Promise((resolve, reject) => { - try { - const walletId = walletData.id; - store.set(`${this.storageKeys.WALLETS}.${walletId}`, walletData); - return resolve(); - } catch (error) { - return reject(error); - } - }); - - updateWalletLocalData = (updatedWalletData: Object): Promise => - new Promise(async (resolve, reject) => { - const walletId = updatedWalletData.id; - const currentWalletData = await this.getWalletLocalData(walletId); - const walletData = Object.assign( - {}, - currentWalletData, - updatedWalletData - ); - try { - store.set(`${this.storageKeys.WALLETS}.${walletId}`, walletData); - return resolve(walletData); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.set( + `${this.storageKeys.WALLETS}.${walletData.id}`, + walletData + ); + + updateWalletLocalData = async ( + updatedWalletData: Object + ): Promise => { + const walletId = updatedWalletData.id; + const currentWalletData = await this.getWalletLocalData(walletId); + const walletData = Object.assign({}, currentWalletData, updatedWalletData); + await LocalStorageApi.set( + `${this.storageKeys.WALLETS}.${walletId}`, + walletData + ); + return walletData; + }; unsetWalletLocalData = (walletId: string): Promise => - new Promise((resolve, reject) => { - try { - store.delete(`${this.storageKeys.WALLETS}.${walletId}`); - return resolve(); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.unset(`${this.storageKeys.WALLETS}.${walletId}`); getReadNews = (): Promise => - new Promise((resolve, reject) => { - try { - const readNews = store.get(this.storageKeys.READ_NEWS); - if (!readNews) return resolve([]); - resolve(readNews); - } catch (error) { - return reject(error); - } - }); + LocalStorageApi.get(this.storageKeys.READ_NEWS, []); - markNewsAsRead = ( + markNewsAsRead = async ( newsTimestamps: NewsTimestamp[] - ): Promise => - new Promise((resolve, reject) => { - try { - const readNews = store.get(this.storageKeys.READ_NEWS) || []; - - if (!includes(readNews, newsTimestamps[0])) { - store.set( - this.storageKeys.READ_NEWS, - readNews.concat(newsTimestamps) - ); - } - - resolve(readNews); - } catch (error) { - return reject(error); - } - }); + ): Promise => { + const readNews = + (await LocalStorageApi.get(this.storageKeys.READ_NEWS)) || []; + if (!includes(readNews, newsTimestamps[0])) { + await LocalStorageApi.set( + this.storageKeys.READ_NEWS, + readNews.concat(newsTimestamps) + ); + } + return readNews; + }; unsetReadNews = (): Promise => - new Promise(resolve => { - try { - store.delete(this.storageKeys.READ_NEWS); - resolve(); - } catch (error) {} // eslint-disable-line - }); + LocalStorageApi.unset(this.storageKeys.READ_NEWS); + + getWalletMigrationStatus = (): Promise => + LocalStorageApi.get( + this.storageKeys.WALLET_MIGRATION_STATUS, + WalletMigrationStatuses.UNSTARTED + ); + + setWalletMigrationStatus = (status: WalletMigrationStatus): Promise => + LocalStorageApi.set(this.storageKeys.WALLET_MIGRATION_STATUS, status); + + unsetWalletMigrationStatus = (): Promise => + LocalStorageApi.unset(this.storageKeys.WALLET_MIGRATION_STATUS); reset = async () => { await this.unsetUserLocale(); + await this.unsetUserNumberFormat(); + await this.unsetUserDateFormatEnglish(); + await this.unsetUserDateFormatJapanese(); + await this.unsetUserTimeFormat(); await this.unsetTermsOfUseAcceptance(); await this.unsetUserTheme(); await this.unsetDataLayerMigrationAcceptance(); await this.unsetReadNews(); + await this.unsetWalletMigrationStatus(); }; } diff --git a/source/renderer/app/api/utils/mnemonics.js b/source/renderer/app/api/utils/mnemonics.js index a5be558d67..809043f710 100644 --- a/source/renderer/app/api/utils/mnemonics.js +++ b/source/renderer/app/api/utils/mnemonics.js @@ -23,8 +23,9 @@ export const scrambleMnemonics = ({ }: MnemonicsParams): Array => scramblePaperWalletMnemonic(passphrase, scrambledInput); -export const generateAccountMnemonics = (): Array => - generateMnemonic().split(' '); +export const generateAccountMnemonics = ( + numberOfWords: number +): Array => generateMnemonic(numberOfWords).split(' '); // eslint-disable-next-line export const generateAdditionalMnemonics = (): Array => diff --git a/source/renderer/app/api/utils/patchAdaApi.js b/source/renderer/app/api/utils/patchAdaApi.js index c5f6e77e3d..64d9318125 100644 --- a/source/renderer/app/api/utils/patchAdaApi.js +++ b/source/renderer/app/api/utils/patchAdaApi.js @@ -1,116 +1,89 @@ // @flow -import { get } from 'lodash'; +import { get, map } from 'lodash'; +import moment from 'moment'; +import { action } from 'mobx'; +import BigNumber from 'bignumber.js/bignumber'; import AdaApi from '../api'; -import { getNodeInfo } from '../nodes/requests/getNodeInfo'; -import { getNodeSettings } from '../nodes/requests/getNodeSettings'; +import { getNetworkInfo } from '../network/requests/getNetworkInfo'; import { getLatestAppVersion } from '../nodes/requests/getLatestAppVersion'; -import { GenericApiError } from '../common/errors'; -import { Logger } from '../../utils/logging'; -import type { NodeInfoQueryParams } from '../nodes/requests/getNodeInfo'; +import { logger } from '../../utils/logging'; +import packageJson from '../../../../../package.json'; +import ApiError from '../../domains/ApiError'; + +// domains +import Wallet from '../../domains/Wallet'; +import StakePool from '../../domains/StakePool'; + +import type { + GetNetworkInfoResponse, + NetworkInfoResponse, +} from '../network/types'; import type { LatestAppVersionInfoResponse, - NodeInfoResponse, - GetNetworkStatusResponse, - NodeSettingsResponse, - GetNodeSettingsResponse, GetLatestAppVersionResponse, } from '../nodes/types'; import type { GetNewsResponse } from '../news/types'; +import { EPOCH_LENGTH_ITN } from '../../config/epochsConfig'; let LATEST_APP_VERSION = null; -let LOCAL_TIME_DIFFERENCE = 0; -let LOCAL_BLOCK_HEIGHT = null; -let NETWORK_BLOCK_HEIGHT = null; +let SYNC_PROGRESS = null; let NEXT_ADA_UPDATE = null; -let SUBSCRIPTION_STATUS = null; let APPLICATION_VERSION = null; -let FAKE_NEWSFEED_JSON: ?GetNewsResponse; +let TESTING_NEWSFEED_JSON: ?GetNewsResponse; +let TESTING_WALLETS_DATA: Object = {}; export default (api: AdaApi) => { - api.getLocalTimeDifference = async () => - Promise.resolve(LOCAL_TIME_DIFFERENCE); - - api.getNetworkStatus = async ( - queryInfoParams?: NodeInfoQueryParams - ): Promise => { - Logger.debug('AdaApi::getNetworkStatus (PATCHED) called'); + api.getNetworkInfo = async (): Promise => { + logger.debug('AdaApi::getNetworkInfo (PATCHED) called'); try { - const nodeInfo: NodeInfoResponse = await getNodeInfo( - api.config, - queryInfoParams - ); - Logger.debug('AdaApi::getNetworkStatus (PATCHED) success', { - nodeInfo, - }); + const networkInfo: NetworkInfoResponse = await getNetworkInfo(api.config); + logger.debug('AdaApi::getNetworkInfo (PATCHED) success', { networkInfo }); const { - blockchainHeight, - subscriptionStatus, - syncProgress, - localBlockchainHeight, - } = nodeInfo; + sync_progress, // eslint-disable-line camelcase + node_tip, // eslint-disable-line camelcase + network_tip, // eslint-disable-line camelcase + next_epoch, // eslint-disable-line camelcase + } = networkInfo; + const syncProgress = + get(sync_progress, 'status') === 'ready' + ? 100 + : get(sync_progress, 'quantity', 0); // extract relevant data before sending to NetworkStatusStore - const response = { - subscriptionStatus: SUBSCRIPTION_STATUS || subscriptionStatus, - syncProgress: syncProgress.quantity, - blockchainHeight: - NETWORK_BLOCK_HEIGHT || get(blockchainHeight, 'quantity', 0), - localBlockchainHeight: - LOCAL_BLOCK_HEIGHT || localBlockchainHeight.quantity, - localTimeInformation: { - status: 'available', - difference: LOCAL_TIME_DIFFERENCE, + return { + syncProgress: SYNC_PROGRESS || syncProgress, + localTip: { + epoch: get(node_tip, 'epoch_number', 0), + slot: get(node_tip, 'slot_number', 0), + }, + networkTip: { + epoch: get(network_tip, 'epoch_number', 0), + slot: get(network_tip, 'slot_number', 0), + }, + nextEpoch: { + epochNumber: get(next_epoch, 'epoch_number', 0), + epochStart: get(next_epoch, 'epoch_start_time', ''), + }, + futureEpoch: { + epochNumber: get(next_epoch, 'epoch_number', 0) + 1, + epochStart: moment(get(next_epoch, 'epoch_start', '')).add( + EPOCH_LENGTH_ITN, + 'seconds' + ), }, }; - - // Since in test environment we run multiple NTP force-checks - // we need to protect ourselves from getting punished by the NTP - // service which results in 30 second delay in NTP check response. - // In order to simulate NTP force-check we use 250ms timeout. - const isForcedTimeDifferenceCheck = !!queryInfoParams; - return isForcedTimeDifferenceCheck - ? new Promise(resolve => { - setTimeout(() => resolve(response), 250); - }) - : response; } catch (error) { - Logger.error('AdaApi::getNetworkStatus (PATCHED) error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getNetworkInfo (PATCHED) error', { error }); + throw new ApiError(); } }; - api.getNodeSettings = async (): Promise => { - Logger.debug('AdaApi::getNodeSettings (PATCHED) called'); - try { - const nodeSettings: NodeSettingsResponse = await getNodeSettings( - api.config - ); - if (api.setFaultyNodeSettingsApi) { - const error = new Error('getNodeSettings forced error'); - Logger.error('AdaApi::getNodeSettings (PATCHED) forced error', { - error, - }); - throw new GenericApiError(error); - } - Logger.debug('AdaApi::getNodeSettings (PATCHED) success', { - nodeSettings, - }); - const { slotId } = nodeSettings; - return { slotId }; - } catch (error) { - Logger.error('AdaApi::getNodeSettings (PATCHED) error', { error }); - throw new GenericApiError(error); - } + api.setSyncProgress = async syncProgress => { + SYNC_PROGRESS = syncProgress; }; - api.setFaultyNodeSettingsApi = false; - - api.setLocalTimeDifference = async timeDifference => { - LOCAL_TIME_DIFFERENCE = timeDifference; - }; - - api.nextUpdate = async () => { + api.nextUpdate = async (): Promise => { let nodeUpdate = null; if (NEXT_ADA_UPDATE) { @@ -119,7 +92,7 @@ export default (api: AdaApi) => { }; } - Logger.debug('AdaApi::nextUpdate success', { nodeUpdate }); + logger.debug('AdaApi::nextUpdate success', { nodeUpdate }); return Promise.resolve(nodeUpdate); }; @@ -128,7 +101,7 @@ export default (api: AdaApi) => { }; api.getLatestAppVersion = async (): Promise => { - Logger.debug('AdaApi::getLatestAppVersion (PATCHED) called'); + logger.debug('AdaApi::getLatestAppVersion (PATCHED) called'); try { const { isWindows, platform } = global.environment; const latestAppVersionInfo: LatestAppVersionInfoResponse = await getLatestAppVersion(); @@ -152,7 +125,7 @@ export default (api: AdaApi) => { null ); - Logger.debug('AdaApi::getLatestAppVersion success', { + logger.debug('AdaApi::getLatestAppVersion success', { latestAppVersion, latestAppVersionInfo, applicationVersion, @@ -163,8 +136,8 @@ export default (api: AdaApi) => { applicationVersion: APPLICATION_VERSION || applicationVersion, }; } catch (error) { - Logger.error('AdaApi::getLatestAppVersion (PATCHED) error', { error }); - throw new GenericApiError(); + logger.error('AdaApi::getLatestAppVersion (PATCHED) error', { error }); + throw new ApiError(); } }; @@ -176,39 +149,92 @@ export default (api: AdaApi) => { APPLICATION_VERSION = applicationVersion; }; - api.setSubscriptionStatus = async (subscriptionStatus: ?Object) => { - SUBSCRIPTION_STATUS = subscriptionStatus; - }; - - api.setLocalBlockHeight = async (height: number) => { - LOCAL_BLOCK_HEIGHT = height; - }; - - api.setNetworkBlockHeight = async (height: number) => { - NETWORK_BLOCK_HEIGHT = height; - }; + api.setTestingNewsFeed = (testingNewsFeedData: ?GetNewsResponse) => { + const { version: packageJsonVersion } = packageJson; + if (!testingNewsFeedData) { + TESTING_NEWSFEED_JSON = null; + return; + } + // Always mutate newsfeed target version to current app version + const newsFeedItems = map(testingNewsFeedData.items, item => { + return { + ...item, + target: { + ...item.target, + daedalusVersion: item.target.daedalusVersion + ? packageJsonVersion + : '', + }, + }; + }); - api.setFakeNewsFeedJsonForTesting = (fakeNewsfeedJson: ?GetNewsResponse) => { - FAKE_NEWSFEED_JSON = fakeNewsfeedJson; + TESTING_NEWSFEED_JSON = { + ...testingNewsFeedData, + items: newsFeedItems, + }; }; api.getNews = (): Promise => { return new Promise((resolve, reject) => { - if (!FAKE_NEWSFEED_JSON) { + if (!TESTING_NEWSFEED_JSON) { reject(new Error('Unable to fetch news')); } else { - resolve(FAKE_NEWSFEED_JSON); + resolve(TESTING_NEWSFEED_JSON); } }); }; + api.setTestingWallet = ( + testingWalletData: Object, + walletIndex?: number = 0 + ): void => { + TESTING_WALLETS_DATA[walletIndex] = testingWalletData; + }; + + api.setTestingWallets = (testingWalletsData: Array): void => { + TESTING_WALLETS_DATA = testingWalletsData; + }; + + const originalGetWallets: Function = api.getWallets; + + const getModifiedWallet = action((wallet: Object) => { + let { amount = 100000, availableAmount = 100000 } = wallet; + if (typeof amount !== 'object') amount = new BigNumber(amount); + if (typeof availableAmount !== 'object') + availableAmount = new BigNumber(availableAmount); + return new Wallet({ + ...wallet, + amount, + availableAmount, + }); + }); + + api.getWallets = async (): Promise> => { + const originalWallets = await originalGetWallets(); + const modifiedWallets = originalWallets.map( + (originalWallet: Wallet, index: number) => { + const testingWallet = TESTING_WALLETS_DATA[index] || {}; + const modifiedWallet = { + ...originalWallet, + ...testingWallet, + }; + return getModifiedWallet(modifiedWallet); + } + ); + return Promise.resolve(modifiedWallets); + }; + + api.setTestingStakePools = (testingStakePoolsData: Array): void => { + api.getStakePools = (): Array => + testingStakePoolsData.map( + (stakePool: Object) => new StakePool(stakePool) + ); + }; + api.resetTestOverrides = () => { + TESTING_WALLETS_DATA = {}; LATEST_APP_VERSION = null; - LOCAL_TIME_DIFFERENCE = 0; - LOCAL_BLOCK_HEIGHT = null; - NETWORK_BLOCK_HEIGHT = null; NEXT_ADA_UPDATE = null; - SUBSCRIPTION_STATUS = null; APPLICATION_VERSION = null; }; }; diff --git a/source/renderer/app/api/utils/request.js b/source/renderer/app/api/utils/request.js index 76b6a128d0..1f72196fe9 100644 --- a/source/renderer/app/api/utils/request.js +++ b/source/renderer/app/api/utils/request.js @@ -1,7 +1,7 @@ // @flow -import { size, has, get, omit } from 'lodash'; +import { includes, omit, size } from 'lodash'; import querystring from 'querystring'; -import { encryptPassphrase, getContentLength } from '.'; +import { getContentLength } from '.'; export type RequestOptions = { hostname: string, @@ -17,41 +17,26 @@ export type RequestOptions = { }, }; +const ALLOWED_ERROR_EXCEPTION_PATHS = [ + '/api/internal/next-update', // when nextAdaUpdate receives a 404, it isn't an error +]; + +const { isIncentivizedTestnet } = global.environment; + function typedRequest( httpOptions: RequestOptions, queryParams?: {}, - rawBodyParams?: any, - requestOptions?: { returnMeta: boolean } + rawBodyParams?: any + // requestOptions?: { returnMeta: boolean } ): Promise { return new Promise((resolve, reject) => { const options: RequestOptions = Object.assign({}, httpOptions); - const { returnMeta } = Object.assign({}, requestOptions); + // const { returnMeta } = Object.assign({}, requestOptions); let hasRequestBody = false; let requestBody = ''; - let queryString = ''; if (queryParams && size(queryParams) > 0) { - // Handle passphrase - if (has(queryParams, 'passphrase')) { - const passphrase = get(queryParams, 'passphrase'); - - // If passphrase is present it must be encrypted and included in options.path - if (passphrase) { - const encryptedPassphrase = encryptPassphrase(passphrase); - queryString = `?passphrase=${encryptedPassphrase}`; - } - - // Passphrase must be ommited from rest query params - queryParams = omit(queryParams, 'passphrase'); - - if (size(queryParams > 1) && passphrase) { - queryString += `&${querystring.stringify(queryParams)}`; - } - } else { - queryString = `?${querystring.stringify(queryParams)}`; - } - - if (queryString) options.path += queryString; + options.path += `?${querystring.stringify(queryParams)}`; } // Handle raw body params @@ -65,7 +50,11 @@ function typedRequest( }; } - const httpsRequest = global.https.request(options); + const httpOnlyOptions = omit(options, ['ca', 'cert', 'key']); + const httpsRequest = isIncentivizedTestnet + ? global.http.request(httpOnlyOptions) + : global.https.request(options); + if (hasRequestBody) { httpsRequest.write(requestBody); } @@ -80,42 +69,37 @@ 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; + const isSuccessResponse = + (statusCode >= 200 && statusCode <= 206) || + (statusCode === 404 && + includes(ALLOWED_ERROR_EXCEPTION_PATHS, options.path)); - 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(returnMeta ? parsedBody : parsedBody.data); - } else if (status === 'error' || status === 'fail') { + if (isSuccessResponse) { + const data = + statusCode === 404 + ? 'null' + : `"statusCode: ${statusCode} -- statusMessage: ${statusMessage}"`; + // When deleting a wallet, the API does not return any data in body + // even if it was successful + if (!body) { + body = `{ + "status": ${statusCode}, + "data": ${data} + }`; + } + resolve(JSON.parse(body)); + } else if (body) { + // Error response with a body + const parsedBody = JSON.parse(body); + if (parsedBody.code && 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.')); + reject(new Error('Unknown API response')); } } else { - // TODO: find a way to record this case and report to the backend team - reject(new Error('Unknown response from backend.')); + // Error response without a body + reject(new Error('Unknown API response')); } } catch (error) { // Handle internal server errors (e.g. HTTP 500 - 'Something went wrong') diff --git a/source/renderer/app/api/utils/requestV0.js b/source/renderer/app/api/utils/requestV0.js index aee8246ee1..e0bd3aa424 100644 --- a/source/renderer/app/api/utils/requestV0.js +++ b/source/renderer/app/api/utils/requestV0.js @@ -63,6 +63,7 @@ function typedRequest( }; } + // $FlowFixMe const httpsRequest = https.request(options); if (hasRequestBody) { httpsRequest.write(requestBody); diff --git a/source/renderer/app/api/wallets/errors.js b/source/renderer/app/api/wallets/errors.js deleted file mode 100644 index d0e780403e..0000000000 --- a/source/renderer/app/api/wallets/errors.js +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index d090fcca04..0000000000 --- a/source/renderer/app/api/wallets/requests/changeSpendingPassword.js +++ /dev/null @@ -1,32 +0,0 @@ -// @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( - { - 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 index fb9f0bbea9..8ef8cc14af 100644 --- a/source/renderer/app/api/wallets/requests/createWallet.js +++ b/source/renderer/app/api/wallets/requests/createWallet.js @@ -1,16 +1,8 @@ // @flow import type { RequestConfig } from '../../common/types'; -import type { AdaWallet, WalletAssuranceLevel } from '../types'; +import type { AdaWallet, WalletInitData } 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 } @@ -18,7 +10,7 @@ export const createWallet = ( request( { method: 'POST', - path: '/api/v1/wallets', + path: '/v2/wallets', ...config, }, {}, diff --git a/source/renderer/app/api/wallets/requests/deleteLegacyWallet.js b/source/renderer/app/api/wallets/requests/deleteLegacyWallet.js new file mode 100644 index 0000000000..fe05ceade3 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/deleteLegacyWallet.js @@ -0,0 +1,14 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const deleteLegacyWallet = ( + config: RequestConfig, + { walletId }: { walletId: string } +): Promise<*> => + request({ + method: 'DELETE', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}`, + ...config, + }); diff --git a/source/renderer/app/api/wallets/requests/deleteWallet.js b/source/renderer/app/api/wallets/requests/deleteWallet.js index f364ec008b..67cec0e481 100644 --- a/source/renderer/app/api/wallets/requests/deleteWallet.js +++ b/source/renderer/app/api/wallets/requests/deleteWallet.js @@ -1,14 +1,13 @@ // @flow import type { RequestConfig } from '../../common/types'; -import type { DeleteWalletRequest } from '../types'; import { request } from '../../utils/request'; export const deleteWallet = ( config: RequestConfig, - { walletId }: DeleteWalletRequest + { walletId }: { walletId: string } ): Promise<*> => request({ method: 'DELETE', - path: `/api/v1/wallets/${walletId}`, + path: `/v2/wallets/${walletId}`, ...config, }); diff --git a/source/renderer/app/api/wallets/requests/forceLegacyWalletResync.js b/source/renderer/app/api/wallets/requests/forceLegacyWalletResync.js new file mode 100644 index 0000000000..402db58322 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/forceLegacyWalletResync.js @@ -0,0 +1,21 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const forceLegacyWalletResync = ( + config: RequestConfig, + { walletId }: { walletId: string } +): Promise<*> => + request( + { + method: 'PUT', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}/tip`, + ...config, + }, + {}, + { + slot_number: 0, + epoch_number: 0, + } + ); diff --git a/source/renderer/app/api/wallets/requests/forceWalletResync.js b/source/renderer/app/api/wallets/requests/forceWalletResync.js new file mode 100644 index 0000000000..ed161d91a6 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/forceWalletResync.js @@ -0,0 +1,20 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import { request } from '../../utils/request'; + +export const forceWalletResync = ( + config: RequestConfig, + { walletId }: { walletId: string } +): Promise<*> => + request( + { + method: 'PUT', + path: `/v2/wallets/${walletId}/tip`, + ...config, + }, + {}, + { + slot_number: 0, + epoch_number: 0, + } + ); diff --git a/source/renderer/app/api/wallets/requests/getByronWalletUtxos.js b/source/renderer/app/api/wallets/requests/getByronWalletUtxos.js new file mode 100644 index 0000000000..fd3f73f86d --- /dev/null +++ b/source/renderer/app/api/wallets/requests/getByronWalletUtxos.js @@ -0,0 +1,15 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { WalletUtxos } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const getByronWalletUtxos = ( + config: RequestConfig, + { walletId }: { walletId: string } +): Promise => + request({ + method: 'GET', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}/statistics/utxos`, + ...config, + }); diff --git a/source/renderer/app/api/wallets/requests/getLegacyWallet.js b/source/renderer/app/api/wallets/requests/getLegacyWallet.js new file mode 100644 index 0000000000..26bbcd5f9d --- /dev/null +++ b/source/renderer/app/api/wallets/requests/getLegacyWallet.js @@ -0,0 +1,15 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { LegacyAdaWallet } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const getLegacyWallet = ( + config: RequestConfig, + { walletId }: { walletId: string } +): Promise => + request({ + method: 'GET', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}`, + ...config, + }); diff --git a/source/renderer/app/api/wallets/requests/getLegacyWallets.js b/source/renderer/app/api/wallets/requests/getLegacyWallets.js new file mode 100644 index 0000000000..0ac725588f --- /dev/null +++ b/source/renderer/app/api/wallets/requests/getLegacyWallets.js @@ -0,0 +1,13 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { LegacyAdaWallets } from '../types'; +import { request } from '../../utils/request'; + +export const getLegacyWallets = ( + config: RequestConfig +): Promise => + request({ + method: 'GET', + path: '/v2/byron-wallets', + ...config, + }); diff --git a/source/renderer/app/api/wallets/requests/getWallet.js b/source/renderer/app/api/wallets/requests/getWallet.js new file mode 100644 index 0000000000..7d5e601ddd --- /dev/null +++ b/source/renderer/app/api/wallets/requests/getWallet.js @@ -0,0 +1,14 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallet } from '../types'; +import { request } from '../../utils/request'; + +export const getWallet = ( + config: RequestConfig, + { walletId }: { walletId: string } +): Promise => + request({ + method: 'GET', + path: `/v2/wallets/${walletId}`, + ...config, + }); diff --git a/source/renderer/app/api/wallets/requests/getWalletUtxos.js b/source/renderer/app/api/wallets/requests/getWalletUtxos.js index d5c21c94ef..112402ddaa 100644 --- a/source/renderer/app/api/wallets/requests/getWalletUtxos.js +++ b/source/renderer/app/api/wallets/requests/getWalletUtxos.js @@ -1,14 +1,14 @@ // @flow import type { RequestConfig } from '../../common/types'; import { request } from '../../utils/request'; -import type { GetWalletUtxosRequest, WalletUtxos } from '../types'; +import type { WalletUtxos } from '../types'; export const getWalletUtxos = ( config: RequestConfig, - { walletId }: GetWalletUtxosRequest + { walletId }: { walletId: string } ): Promise => request({ method: 'GET', - path: `api/v1/wallets/${walletId}/statistics/utxos`, + path: `/v2/wallets/${walletId}/statistics/utxos`, ...config, }); diff --git a/source/renderer/app/api/wallets/requests/getWallets.js b/source/renderer/app/api/wallets/requests/getWallets.js index c70ca33004..55bd9bb38c 100644 --- a/source/renderer/app/api/wallets/requests/getWallets.js +++ b/source/renderer/app/api/wallets/requests/getWallets.js @@ -2,17 +2,10 @@ 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( - { - 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]', - } - ); + request({ + method: 'GET', + path: '/v2/wallets', + ...config, + }); diff --git a/source/renderer/app/api/wallets/requests/importWalletAsKey.js b/source/renderer/app/api/wallets/requests/importWalletAsKey.js index 5b80ec28dc..ad4d5cfef2 100644 --- a/source/renderer/app/api/wallets/requests/importWalletAsKey.js +++ b/source/renderer/app/api/wallets/requests/importWalletAsKey.js @@ -5,7 +5,7 @@ import { request } from '../../utils/request'; export type ImportWalletAsKey = { filePath: string, - spendingPassword?: string, + spendingPassword: string, }; export const importWalletAsKey = ( diff --git a/source/renderer/app/api/wallets/requests/resetWalletState.js b/source/renderer/app/api/wallets/requests/resetWalletState.js deleted file mode 100644 index 1fa4a55a8e..0000000000 --- a/source/renderer/app/api/wallets/requests/resetWalletState.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import type { RequestConfig } from '../../common/types'; -import { request } from '../../utils/request'; - -export const resetWalletState = (config: RequestConfig): Promise => - request({ - method: 'DELETE', - path: '/api/internal/reset-wallet-state', - ...config, - }); diff --git a/source/renderer/app/api/wallets/requests/restoreByronWallet.js b/source/renderer/app/api/wallets/requests/restoreByronWallet.js new file mode 100644 index 0000000000..4ba0f59fee --- /dev/null +++ b/source/renderer/app/api/wallets/requests/restoreByronWallet.js @@ -0,0 +1,20 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { LegacyAdaWallet, LegacyWalletInitData } from '../types'; +import type { WalletByronKind } from '../../../types/walletRestoreTypes'; +import { request } from '../../utils/request'; + +export const restoreByronWallet = ( + config: RequestConfig, + { walletInitData }: { walletInitData: LegacyWalletInitData }, + type: WalletByronKind +): Promise => + request( + { + method: 'POST', + path: '/v2/byron-wallets', + ...config, + }, + {}, + { ...walletInitData, style: type } + ); diff --git a/source/renderer/app/api/wallets/requests/restoreExportedByronWallet.js b/source/renderer/app/api/wallets/requests/restoreExportedByronWallet.js new file mode 100644 index 0000000000..1dc06aff85 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/restoreExportedByronWallet.js @@ -0,0 +1,19 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { LegacyAdaWallet } from '../types'; +import type { ExportedByronWallet } from '../../../types/walletExportTypes'; +import { request } from '../../utils/request'; + +export const restoreExportedByronWallet = ( + config: RequestConfig, + { walletInitData }: { walletInitData: ExportedByronWallet } +): Promise => + request( + { + method: 'POST', + path: '/v2/byron-wallets', + ...config, + }, + {}, + { ...walletInitData, style: 'random' } + ); diff --git a/source/renderer/app/api/wallets/requests/restoreLegacyWallet.js b/source/renderer/app/api/wallets/requests/restoreLegacyWallet.js new file mode 100644 index 0000000000..fb573f959e --- /dev/null +++ b/source/renderer/app/api/wallets/requests/restoreLegacyWallet.js @@ -0,0 +1,21 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { LegacyAdaWallet, LegacyWalletInitData } from '../types'; +import { request } from '../../utils/request'; + +export const restoreLegacyWallet = ( + config: RequestConfig, + { walletInitData }: { walletInitData: LegacyWalletInitData }, + type?: string = '' +): Promise => { + const queryParams = {}; + return request( + { + method: 'POST', + path: `/v2/byron-wallets${type}`, + ...config, + }, + queryParams, + walletInitData + ); +}; diff --git a/source/renderer/app/api/wallets/requests/restoreWallet.js b/source/renderer/app/api/wallets/requests/restoreWallet.js index 27e32c4832..02c9225d7f 100644 --- a/source/renderer/app/api/wallets/requests/restoreWallet.js +++ b/source/renderer/app/api/wallets/requests/restoreWallet.js @@ -1,7 +1,6 @@ // @flow import type { RequestConfig } from '../../common/types'; -import type { WalletInitData } from './createWallet'; -import type { AdaWallet } from '../types'; +import type { AdaWallet, WalletInitData } from '../types'; import { request } from '../../utils/request'; export const restoreWallet = ( @@ -11,7 +10,7 @@ export const restoreWallet = ( request( { method: 'POST', - path: '/api/v1/wallets', + path: '/v2/wallets', ...config, }, {}, diff --git a/source/renderer/app/api/wallets/requests/transferFunds.js b/source/renderer/app/api/wallets/requests/transferFunds.js new file mode 100644 index 0000000000..afb0b520bf --- /dev/null +++ b/source/renderer/app/api/wallets/requests/transferFunds.js @@ -0,0 +1,21 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { TransferFundsRequest, TransferFundsResponse } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const transferFunds = ( + config: RequestConfig, + { sourceWalletId, targetWalletId, passphrase }: TransferFundsRequest +): Promise => + request( + { + method: 'POST', + path: `/v2/byron-wallets/${getRawWalletId( + sourceWalletId + )}/migrations/${targetWalletId}`, + ...config, + }, + {}, + { passphrase } + ); diff --git a/source/renderer/app/api/wallets/requests/transferFundsCalculateFee.js b/source/renderer/app/api/wallets/requests/transferFundsCalculateFee.js new file mode 100644 index 0000000000..a909a7e089 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/transferFundsCalculateFee.js @@ -0,0 +1,18 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { + TransferFundsCalculateFeeRequest, + TransferFundsCalculateFeeResponse, +} from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const transferFundsCalculateFee = ( + config: RequestConfig, + { sourceWalletId }: TransferFundsCalculateFeeRequest +): Promise => + request({ + method: 'GET', + path: `/v2/byron-wallets/${getRawWalletId(sourceWalletId)}/migrations`, + ...config, + }); diff --git a/source/renderer/app/api/wallets/requests/updateByronSpendingPassword.js b/source/renderer/app/api/wallets/requests/updateByronSpendingPassword.js new file mode 100644 index 0000000000..306a940429 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/updateByronSpendingPassword.js @@ -0,0 +1,30 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallet } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const updateByronSpendingPassword = ( + config: RequestConfig, + { + walletId, + oldPassword, + newPassword, + }: { + walletId: string, + oldPassword?: string, + newPassword: string, + } +): Promise => + request( + { + method: 'PUT', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}/passphrase`, + ...config, + }, + {}, + { + old_passphrase: oldPassword, + new_passphrase: newPassword, + } + ); diff --git a/source/renderer/app/api/wallets/requests/updateByronWallet.js b/source/renderer/app/api/wallets/requests/updateByronWallet.js new file mode 100644 index 0000000000..c9bae41b8b --- /dev/null +++ b/source/renderer/app/api/wallets/requests/updateByronWallet.js @@ -0,0 +1,19 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallet } from '../types'; +import { request } from '../../utils/request'; +import { getRawWalletId } from '../../utils'; + +export const updateByronWallet = ( + config: RequestConfig, + { walletId, name }: { walletId: string, name: string } +): Promise => + request( + { + method: 'PUT', + path: `/v2/byron-wallets/${getRawWalletId(walletId)}`, + ...config, + }, + {}, + { name } + ); diff --git a/source/renderer/app/api/wallets/requests/updateSpendingPassword.js b/source/renderer/app/api/wallets/requests/updateSpendingPassword.js new file mode 100644 index 0000000000..d8d20f6908 --- /dev/null +++ b/source/renderer/app/api/wallets/requests/updateSpendingPassword.js @@ -0,0 +1,29 @@ +// @flow +import type { RequestConfig } from '../../common/types'; +import type { AdaWallet } from '../types'; +import { request } from '../../utils/request'; + +export const updateSpendingPassword = ( + config: RequestConfig, + { + walletId, + oldPassword, + newPassword, + }: { + walletId: string, + oldPassword: string, + newPassword: string, + } +): Promise => + request( + { + method: 'PUT', + path: `/v2/wallets/${walletId}/passphrase`, + ...config, + }, + {}, + { + old_passphrase: oldPassword, + new_passphrase: newPassword, + } + ); diff --git a/source/renderer/app/api/wallets/requests/updateWallet.js b/source/renderer/app/api/wallets/requests/updateWallet.js index d139d4324c..dc6526b688 100644 --- a/source/renderer/app/api/wallets/requests/updateWallet.js +++ b/source/renderer/app/api/wallets/requests/updateWallet.js @@ -1,24 +1,18 @@ // @flow import type { RequestConfig } from '../../common/types'; -import type { AdaWallet, WalletAssuranceLevel } from '../types'; +import type { AdaWallet } 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 + { walletId, name }: { walletId: string, name: string } ): Promise => request( { method: 'PUT', - path: `/api/v1/wallets/${walletId}`, + path: `/v2/wallets/${walletId}`, ...config, }, {}, - { assuranceLevel, name } + { name } ); diff --git a/source/renderer/app/api/wallets/types.js b/source/renderer/app/api/wallets/types.js index 5e3b25a078..96f60a7e35 100644 --- a/source/renderer/app/api/wallets/types.js +++ b/source/renderer/app/api/wallets/types.js @@ -1,55 +1,158 @@ // @flow import BigNumber from 'bignumber.js'; +import { WalletUnits } from '../../domains/Wallet'; +import type { ExportedByronWallet } from '../../types/walletExportTypes'; + +export type Block = { + slot_number: number, + epoch_number: number, + height: { + quantity: number, + unit: 'block', + }, +}; + +export type Input = { + address?: string, + amount?: { + quantity: number, + unit: WalletUnits.LOVELACE, + }, + id: string, + index: number, +}; + +export type Output = { + address: string, + amount: { + quantity: number, + unit: WalletUnits.LOVELACE, + }, +}; + export type AdaWallet = { - createdAt: Date, - syncState: WalletSyncState, - balance: number, - hasSpendingPassword: boolean, - assuranceLevel: WalletAssuranceLevel, + id: string, + address_pool_gap: number, + balance: { + available: WalletBalance, + total: WalletBalance, + reward: WalletBalance, + }, + delegation: { + active: WalletDelegation, + next?: WalletNextDelegation, + }, name: string, + passphrase?: { + last_updated_at: string, + }, + state: WalletSyncState, + discovery: Discovery, + isLegacy: boolean, +}; + +export type LegacyAdaWallet = { id: string, - spendingPasswordLastUpdate: string, + balance: { + available: WalletBalance, + total: WalletBalance, + reward: WalletBalance, // Unused prop - hack to keep flow happy + }, + name: string, + passphrase?: { + last_updated_at: string, + }, + state: WalletSyncState, + discovery: Discovery, + tip: Block, }; +export type LegacyAdaWallets = Array; + +export type WalletUnit = WalletUnits.LOVELACE | WalletUnits.ADA; + export type AdaWallets = Array; -export type WalletAssuranceLevel = 'normal' | 'strict'; +export type SyncStateStatus = + | 'ready' + | 'restoring' + | 'syncing' + | 'not_responding'; -export type WalletAssuranceMode = { low: number, medium: number }; +export type Discovery = 'random' | 'sequential'; -export type SyncStateTag = 'restoring' | 'synced'; +export type DelegationStatus = 'delegating' | 'not_delegating'; + +export type WalletSyncStateProgress = { + quantity: number, + unit: 'percentage', +}; export type WalletSyncState = { - data: ?{ - estimatedCompletionTime: { - quantity: number, - unit: 'milliseconds', - }, - percentage: { - quantity: number, - unit: 'percent', - }, - throughput: { - quantity: number, - unit: 'blocksPerSecond', - }, - }, - tag: SyncStateTag, + status: SyncStateStatus, + progress?: WalletSyncStateProgress, +}; + +export type WalletBalance = { + quantity: number, + unit: WalletUnits.LOVELACE | WalletUnits.ADA, +}; + +export type DelegationStakePool = { + active: WalletDelegation, + next?: WalletNextDelegation, +}; + +export type WalletNextDelegationEpoch = { + epoch_number: number, + epoch_start_time: string, +}; + +export type WalletDelegation = { + status: DelegationStatus, + target?: string, +}; + +export type WalletPendingDelegations = Array; + +export type WalletNextDelegation = { + status: DelegationStatus, + target?: string, + changes_at: WalletNextDelegationEpoch, }; export type Histogram = { [string]: number, }; +export type WalletUtxoTotal = { + quantity: number, + unit: WalletUnits.LOVELACE, +}; + export type WalletUtxos = { - allStakes: number, - boundType: string, - histogram: { + total: WalletUtxoTotal, + scale: 'log10', + distribution: { [string]: number, }, }; +export type WalletInitData = { + name: string, + mnemonic_sentence: Array, // [ 15 .. 24 ] words + mnemonic_second_factor?: Array, // [ 9 .. 12 ] words + passphrase: string, + address_pool_gap?: number, // 20 +}; + +export type LegacyWalletInitData = { + name: string, + mnemonic_sentence: Array, // [ 12 ] words + passphrase: string, +}; + export type WalletIdAndBalance = { walletId: string, balance: ?BigNumber, @@ -58,22 +161,27 @@ export type WalletIdAndBalance = { // req/res Wallet types export type CreateWalletRequest = { name: string, - mnemonic: string, - spendingPassword: ?string, + mnemonic: Array, + mnemonicPassphrase?: Array, + spendingPassword: string, + addressPoolGap?: number, }; export type UpdateSpendingPasswordRequest = { walletId: string, - oldPassword?: string, - newPassword: ?string, + oldPassword: string, + newPassword: string, + isLegacy: boolean, }; export type DeleteWalletRequest = { walletId: string, + isLegacy: boolean, }; export type GetWalletUtxosRequest = { walletId: string, + isLegacy: boolean, }; export type GetWalletIdAndBalanceRequest = { @@ -87,31 +195,45 @@ export type GetWalletIdAndBalanceResponse = { }; export type RestoreWalletRequest = { - recoveryPhrase: string, + recoveryPhrase: Array, walletName: string, - spendingPassword?: ?string, + spendingPassword: string, }; +export type RestoreLegacyWalletRequest = { + recoveryPhrase: Array, + walletName: string, + spendingPassword: string, +}; + +export type RestoreExportedByronWalletRequest = ExportedByronWallet; + export type UpdateWalletRequest = { walletId: string, - assuranceLevel: WalletAssuranceLevel, name: string, + isLegacy: boolean, }; + +export type ForceWalletResyncRequest = { + walletId: string, + isLegacy: boolean, +}; + export type ImportWalletFromKeyRequest = { filePath: string, - spendingPassword: ?string, + spendingPassword: string, }; export type ImportWalletFromFileRequest = { filePath: string, - spendingPassword: ?string, + spendingPassword: string, walletName: ?string, }; export type ExportWalletToFileRequest = { walletId: string, filePath: string, - password: ?string, + password: string, }; export type GetWalletCertificateRecoveryPhraseRequest = { @@ -123,3 +245,49 @@ export type GetWalletRecoveryPhraseFromCertificateRequest = { passphrase: string, scrambledInput: string, }; + +export type GetWalletRequest = { + walletId: string, + isLegacy: boolean, +}; + +export type TransferFundsCalculateFeeRequest = { + sourceWalletId: string, +}; + +export type TransferFundsCalculateFeeResponse = { + migration_cost: { + quantity: number, + unit: WalletUnits.LOVELACE, + }, +}; + +export type TransferFundsRequest = { + sourceWalletId: string, + targetWalletId: string, + passphrase: string, +}; + +export type TransferFundsResponse = { + id: string, + amount: { + quantity: number, + unit: WalletUnits.LOVELACE, + }, + inserted_at?: { + time: Date, + block: Block, + }, + pending_since?: { + time: Date, + block: Block, + }, + depth: { + quantity: number, + unit: 'block', + }, + direction: 'incoming' | 'outgoing', + inputs: Array, + outputs: Array, + status: 'pending' | 'in_ledger', +}; diff --git a/source/renderer/app/assets/images/arrow-right.inline.svg b/source/renderer/app/assets/images/arrow-right.inline.svg new file mode 100644 index 0000000000..8e39a14ad7 --- /dev/null +++ b/source/renderer/app/assets/images/arrow-right.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/block-consolidation/epochs.inline.svg b/source/renderer/app/assets/images/block-consolidation/epochs.inline.svg deleted file mode 100644 index 08b79b755b..0000000000 --- a/source/renderer/app/assets/images/block-consolidation/epochs.inline.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/source/renderer/app/assets/images/block-consolidation/epochs.png b/source/renderer/app/assets/images/block-consolidation/epochs.png deleted file mode 100755 index aa40f24a48..0000000000 Binary files a/source/renderer/app/assets/images/block-consolidation/epochs.png and /dev/null differ diff --git a/source/renderer/app/assets/images/circle-bg-faded.inline.svg b/source/renderer/app/assets/images/circle-bg-faded.inline.svg new file mode 100644 index 0000000000..fc16aa9d79 --- /dev/null +++ b/source/renderer/app/assets/images/circle-bg-faded.inline.svg @@ -0,0 +1,102 @@ + + + + hub-tripple + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/source/renderer/app/assets/images/delimeter-slash.inline.svg b/source/renderer/app/assets/images/delimeter-slash.inline.svg new file mode 100644 index 0000000000..36260b7233 --- /dev/null +++ b/source/renderer/app/assets/images/delimeter-slash.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/download-ic.inline.svg b/source/renderer/app/assets/images/download-ic.inline.svg new file mode 100644 index 0000000000..e60767529e --- /dev/null +++ b/source/renderer/app/assets/images/download-ic.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/experiment-icon.inline.svg b/source/renderer/app/assets/images/experiment-icon.inline.svg new file mode 100644 index 0000000000..acddf08b3c --- /dev/null +++ b/source/renderer/app/assets/images/experiment-icon.inline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/renderer/app/assets/images/experiment-icon.svg b/source/renderer/app/assets/images/experiment-icon.svg new file mode 100644 index 0000000000..95b549f68f --- /dev/null +++ b/source/renderer/app/assets/images/experiment-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/filter-dis-ic.inline.svg b/source/renderer/app/assets/images/filter-dis-ic.inline.svg new file mode 100644 index 0000000000..3260f46747 --- /dev/null +++ b/source/renderer/app/assets/images/filter-dis-ic.inline.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/source/renderer/app/assets/images/info-icon.inline.svg b/source/renderer/app/assets/images/info-icon.inline.svg new file mode 100644 index 0000000000..cecdea0a9f --- /dev/null +++ b/source/renderer/app/assets/images/info-icon.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/insecure-wallet.inline.svg b/source/renderer/app/assets/images/insecure-wallet.inline.svg new file mode 100644 index 0000000000..bc89d9595a --- /dev/null +++ b/source/renderer/app/assets/images/insecure-wallet.inline.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/not-responding.inline.svg b/source/renderer/app/assets/images/not-responding.inline.svg new file mode 100644 index 0000000000..ef2cb61a3a --- /dev/null +++ b/source/renderer/app/assets/images/not-responding.inline.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/source/renderer/app/assets/images/pen.inline.svg b/source/renderer/app/assets/images/pen.inline.svg new file mode 100644 index 0000000000..75545010fe --- /dev/null +++ b/source/renderer/app/assets/images/pen.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/qr-code.inline.svg b/source/renderer/app/assets/images/qr-code.inline.svg new file mode 100644 index 0000000000..33a941d8d5 --- /dev/null +++ b/source/renderer/app/assets/images/qr-code.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/questionmark.inline.svg b/source/renderer/app/assets/images/questionmark.inline.svg new file mode 100644 index 0000000000..5792a8fa51 --- /dev/null +++ b/source/renderer/app/assets/images/questionmark.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/sand-clock.inline.svg b/source/renderer/app/assets/images/sand-clock.inline.svg new file mode 100644 index 0000000000..22c1ea35c1 --- /dev/null +++ b/source/renderer/app/assets/images/sand-clock.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/sidebar/bug-report-ic-dark.inline.svg b/source/renderer/app/assets/images/sidebar/bug-report-ic-dark.inline.svg deleted file mode 100644 index cc4732ddea..0000000000 --- a/source/renderer/app/assets/images/sidebar/bug-report-ic-dark.inline.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/source/renderer/app/assets/images/sidebar/bug-report-ic.inline.svg b/source/renderer/app/assets/images/sidebar/bug-report-ic.inline.svg deleted file mode 100644 index 6ec0f3f28d..0000000000 --- a/source/renderer/app/assets/images/sidebar/bug-report-ic.inline.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/source/renderer/app/assets/images/sidebar/network-info-logo-cardano-ic.inline.svg b/source/renderer/app/assets/images/sidebar/network-info-logo-cardano-ic.inline.svg new file mode 100644 index 0000000000..d7216b5ff7 --- /dev/null +++ b/source/renderer/app/assets/images/sidebar/network-info-logo-cardano-ic.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/spinner-dark.svg b/source/renderer/app/assets/images/spinner-dark.svg index cb3402252d..159e3db401 100644 --- a/source/renderer/app/assets/images/spinner-dark.svg +++ b/source/renderer/app/assets/images/spinner-dark.svg @@ -1,7 +1,7 @@ - + diff --git a/source/renderer/app/assets/images/tada-ic.inline.svg b/source/renderer/app/assets/images/tada-ic.inline.svg new file mode 100644 index 0000000000..61aae1a0f4 --- /dev/null +++ b/source/renderer/app/assets/images/tada-ic.inline.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/source/renderer/app/assets/images/testnet-bulb.inline.svg b/source/renderer/app/assets/images/testnet-bulb.inline.svg new file mode 100644 index 0000000000..1b1d7c444d --- /dev/null +++ b/source/renderer/app/assets/images/testnet-bulb.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/assets/images/themes/flight-candidate.png b/source/renderer/app/assets/images/themes/flight-candidate.png new file mode 100755 index 0000000000..c327e34468 Binary files /dev/null and b/source/renderer/app/assets/images/themes/flight-candidate.png differ diff --git a/source/renderer/app/assets/images/themes/incentivized-testnet.png b/source/renderer/app/assets/images/themes/incentivized-testnet.png new file mode 100755 index 0000000000..f14eadd4c4 Binary files /dev/null and b/source/renderer/app/assets/images/themes/incentivized-testnet.png differ diff --git a/source/renderer/app/assets/images/untada.inline.svg b/source/renderer/app/assets/images/untada.inline.svg new file mode 100644 index 0000000000..602ca22989 --- /dev/null +++ b/source/renderer/app/assets/images/untada.inline.svg @@ -0,0 +1,16 @@ + + + + untada + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/source/renderer/app/assets/images/wallet-nav/pending.inline.svg b/source/renderer/app/assets/images/wallet-nav/pending.inline.svg new file mode 100644 index 0000000000..cb45f29ee5 --- /dev/null +++ b/source/renderer/app/assets/images/wallet-nav/pending.inline.svg @@ -0,0 +1,3 @@ + + + diff --git a/source/renderer/app/components/layout/TopBar.js b/source/renderer/app/components/layout/TopBar.js index d233753a26..9834c11bac 100644 --- a/source/renderer/app/components/layout/TopBar.js +++ b/source/renderer/app/components/layout/TopBar.js @@ -16,18 +16,45 @@ type Props = { leftIcon?: ?string, children?: ?Node, activeWallet?: ?Wallet, + onTransferFunds?: Function, + onWalletAdd?: Function, + hasRewardsWallets?: boolean, + onLearnMore?: Function, }; @observer export default class TopBar extends Component { render() { - const { onLeftIconClick, leftIcon, activeWallet, children } = this.props; + const { + onLeftIconClick, + leftIcon, + activeWallet, + children, + hasRewardsWallets, + onTransferFunds, + onWalletAdd, + onLearnMore, + } = this.props; + const { isIncentivizedTestnet } = global; const topBarStyles = classNames([ styles.topBar, activeWallet ? styles.withWallet : styles.withoutWallet, ]); + const hasLegacyNotification = + activeWallet && + activeWallet.isLegacy && + isIncentivizedTestnet && + activeWallet.amount.gt(0) && + !activeWallet.isRestoring && + ((hasRewardsWallets && onTransferFunds) || onWalletAdd); + + const onTransferFundsFn = + onTransferFunds && activeWallet + ? () => onTransferFunds(activeWallet.id) + : () => {}; + const topBarTitle = activeWallet ? ( @@ -38,7 +65,7 @@ export default class TopBar extends Component { {// show currency and use long format - formattedWalletAmount(activeWallet.amount, true)} + formattedWalletAmount(activeWallet.amount)} ) : null; @@ -62,8 +89,14 @@ export default class TopBar extends Component { )} {children} - {activeWallet && activeWallet.isLegacy && ( - null} onMove={() => null} /> + {hasLegacyNotification && activeWallet && ( + )} ); diff --git a/source/renderer/app/components/layout/TopBar.scss b/source/renderer/app/components/layout/TopBar.scss index 2d3c7dba15..4702fbc636 100644 --- a/source/renderer/app/components/layout/TopBar.scss +++ b/source/renderer/app/components/layout/TopBar.scss @@ -34,9 +34,17 @@ user-select: text; .walletName { + -webkit-box-orient: vertical; color: var(--theme-topbar-wallet-name-color); font-size: 21px; + -webkit-line-clamp: 1; + line-height: 25px; + margin-bottom: 4px; + max-width: 320px; + overflow: hidden; position: relative; + text-overflow: ellipsis; + word-break: keep-all; } .walletAmount { diff --git a/source/renderer/app/components/loading/manual-update/ManualUpdate.js b/source/renderer/app/components/loading/manual-update/ManualUpdate.js index ccab22f1ae..ce84266027 100644 --- a/source/renderer/app/components/loading/manual-update/ManualUpdate.js +++ b/source/renderer/app/components/loading/manual-update/ManualUpdate.js @@ -1,13 +1,11 @@ // @flow import React, { Component } from 'react'; -import SVGInline from 'react-svg-inline'; import { observer } from 'mobx-react'; import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; import ReactModal from 'react-modal'; -import { Button } from 'react-polymorph/lib/components/Button'; import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; -import linkNewWindow from '../../../assets/images/link-ic.inline.svg'; import styles from './ManualUpdate.scss'; +import ButtonLink from '../../widgets/ButtonLink'; const messages = defineMessages({ title: { @@ -85,28 +83,17 @@ export default class ManualUpdate extends Component {

-
-
+ + onExternalLinkClick(formatMessage(messages.manualUpdateButtonUrl)) + } + skin={ButtonSkin} + label={formatMessage(messages.actionButtonLabel)} + linkProps={{ + className: styles.btnLabel, + }} + /> ); diff --git a/source/renderer/app/components/loading/manual-update/ManualUpdate.scss b/source/renderer/app/components/loading/manual-update/ManualUpdate.scss index 592595a33e..fd692198a3 100644 --- a/source/renderer/app/components/loading/manual-update/ManualUpdate.scss +++ b/source/renderer/app/components/loading/manual-update/ManualUpdate.scss @@ -57,41 +57,28 @@ font-weight: 500; line-height: 1.36; margin: 0 auto; - - .linkNewWindow { - @include link(--theme-manual-update-overlay-button-icon-color); - border-bottom: none; - margin-right: 12px; - svg { - margin-left: 0; - vertical-align: sub; - width: 15px; - } - } - .btnLabel { color: var(--theme-manual-update-overlay-button-label-color); + &:before { + background-color: var( + --theme-manual-update-overlay-button-label-color + ); + } } &:not(.disabled):hover { background-color: var( --theme-manual-update-overlay-button-background-color-hover ); - border: none; color: var(--theme-manual-update-overlay-button-text-color-hover); - .linkNewWindow { - svg { - g { - stroke: var( - --theme-manual-update-overlay-button-icon-color-hover - ); - } - } - } - .btnLabel { color: var(--theme-manual-update-overlay-button-label-color-hover); + &:before { + background-color: var( + --theme-manual-update-overlay-button-label-color-hover + ); + } } } } diff --git a/source/renderer/app/components/loading/no-disk-space-error/NoDiskSpaceError.scss b/source/renderer/app/components/loading/no-disk-space-error/NoDiskSpaceError.scss index c6699eeb64..1aef87fbf6 100644 --- a/source/renderer/app/components/loading/no-disk-space-error/NoDiskSpaceError.scss +++ b/source/renderer/app/components/loading/no-disk-space-error/NoDiskSpaceError.scss @@ -15,7 +15,7 @@ right: 0; text-align: center; top: 0; - z-index: 1; + z-index: 20; .icon { margin-bottom: 36px; diff --git a/source/renderer/app/components/loading/syncing-connecting/ReportIssue.js b/source/renderer/app/components/loading/syncing-connecting/ReportIssue.js index 829b819e79..dcdaa43f0f 100644 --- a/source/renderer/app/components/loading/syncing-connecting/ReportIssue.js +++ b/source/renderer/app/components/loading/syncing-connecting/ReportIssue.js @@ -5,7 +5,8 @@ import { defineMessages, intlShape } from 'react-intl'; import classNames from 'classnames'; import { Button } from 'react-polymorph/lib/components/Button'; import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; - +import { Link } from 'react-polymorph/lib/components/Link'; +import { LinkSkin } from 'react-polymorph/lib/skins/simple/LinkSkin'; import styles from './ReportIssue.scss'; import externalLinkIcon from '../../../assets/images/link-ic.inline.svg'; @@ -15,11 +16,6 @@ const messages = defineMessages({ defaultMessage: '!!!Having trouble connecting to network?', description: 'Report connecting issue text on the loading screen.', }, - reportSyncingIssueText: { - id: 'loading.screen.reportIssue.syncing.text', - defaultMessage: '!!!Having trouble syncing?', - description: 'Report syncing issue text on the loading screen.', - }, reportIssueButtonLabel: { id: 'loading.screen.reportIssue.buttonLabel', defaultMessage: '!!!Open support ticket', @@ -40,12 +36,6 @@ const messages = defineMessages({ defaultMessage: '!!!https://iohk.zendesk.com/hc/en-us/requests/new/', description: 'Link to Open Support page', }, - syncIssueArticleUrl: { - id: 'loading.screen.readIssueArticle.syncIssueArticleUrl', - defaultMessage: - '!!!https://iohk.zendesk.com/hc/en-us/articles/360011536933', - description: 'Link to sync issue article page', - }, connectivityIssueArticleUrl: { id: 'loading.screen.readIssueArticle.connectivityIssueArticleUrl', defaultMessage: @@ -55,12 +45,10 @@ const messages = defineMessages({ }); type Props = { - isConnected: boolean, onIssueClick: Function, + onOpenExternalLink: Function, onDownloadLogs: Function, disableDownloadLogs: boolean, - isConnecting: boolean, - isSyncing: boolean, }; export default class ReportIssue extends Component { @@ -71,20 +59,12 @@ export default class ReportIssue extends Component { render() { const { intl } = this.context; const { - isConnected, onIssueClick, + onOpenExternalLink, onDownloadLogs, disableDownloadLogs, - isConnecting, - isSyncing, } = this.props; - const componentStyles = classNames([ - styles.component, - isConnecting ? styles['is-connecting'] : null, - isSyncing ? styles['is-syncing'] : null, - ]); - const reportIssueButtonClasses = classNames([ 'primary', 'reportIssueButton', @@ -97,19 +77,15 @@ export default class ReportIssue extends Component { ]); const downloadLogsButtonClasses = classNames([ styles.downloadLogsButton, - !isConnected ? styles.downloadLogsButtonConnecting : null, + disableDownloadLogs ? styles.disabled : null, ]); - const readArticleButtonUrl = isConnected - ? messages.syncIssueArticleUrl - : messages.connectivityIssueArticleUrl; + const readArticleButtonUrl = messages.connectivityIssueArticleUrl; return ( -
+

- {!isConnected - ? intl.formatMessage(messages.reportConnectingIssueText) - : intl.formatMessage(messages.reportSyncingIssueText)} + {intl.formatMessage(messages.reportConnectingIssueText)}

+ onClick={!disableDownloadLogs ? onDownloadLogs : null} + hasIconAfter={false} + label={intl.formatMessage(messages.reportIssueDownloadLogsLinkLabel)} + skin={LinkSkin} + />
); } diff --git a/source/renderer/app/components/loading/syncing-connecting/ReportIssue.scss b/source/renderer/app/components/loading/syncing-connecting/ReportIssue.scss index c96b6ab4ca..0e60a86d72 100644 --- a/source/renderer/app/components/loading/syncing-connecting/ReportIssue.scss +++ b/source/renderer/app/components/loading/syncing-connecting/ReportIssue.scss @@ -1,25 +1,15 @@ @import '../../../themes/mixins/link'; .component { + background-color: var(--theme-report-issue-connecting-background-color); position: fixed; text-align: center; + top: 0; width: 100%; - - &.is-connecting { - background-color: var(--theme-report-issue-connecting-background-color); - .reportIssueText { - color: var(--theme-report-issue-connecting-text-color); - } - } - &.is-syncing { - background-color: var(--theme-report-issue-syncing-background-color); - .reportIssueText { - color: var(--theme-report-issue-syncing-text-color); - } - } } .reportIssueText { + color: var(--theme-report-issue-connecting-text-color); font-family: var(--font-regular); font-size: 18px; line-height: 1.22; @@ -36,6 +26,7 @@ --theme-report-issue-button-background-color-hover ) !important; } + &:active { background-color: var( --theme-report-issue-button-background-color-active @@ -60,24 +51,15 @@ } .downloadLogsButton { - border-bottom: 1px solid - var(--theme-report-issue-syncing-download-logs-text-color); - color: var(--theme-report-issue-syncing-download-logs-text-color); - cursor: pointer; + border-color: var(--theme-report-issue-connecting-text-color) !important; + color: var(--theme-report-issue-connecting-text-color) !important; + display: inline-block; font-family: var(--font-regular); - font-size: 14px; - line-height: 1.36; margin-bottom: 20px; opacity: 0.8; - &:disabled { - border-bottom-color: transparent; + &.disabled { + border-color: transparent !important; cursor: default; text-decoration: none; } } - -.downloadLogsButtonConnecting { - border-bottom: 1px solid - var(--theme-report-issue-syncing-download-logs-text-color-connecting); - color: var(--theme-report-issue-syncing-download-logs-text-color-connecting); -} diff --git a/source/renderer/app/components/loading/syncing-connecting/StatusIcons.js b/source/renderer/app/components/loading/syncing-connecting/StatusIcons.js index e2f9379449..a70d2acd88 100644 --- a/source/renderer/app/components/loading/syncing-connecting/StatusIcons.js +++ b/source/renderer/app/components/loading/syncing-connecting/StatusIcons.js @@ -10,7 +10,7 @@ import tooltipStyles from './StatusIcons-tooltip.scss'; import { CardanoNodeStates } from '../../../../../common/types/cardano-node.types'; import nodeStateIcon from '../../../assets/images/node-state-icon.inline.svg'; import isNodeRespondingIcon from '../../../assets/images/is-node-responding-icon.inline.svg'; -import isNodeSubscribedIcon from '../../../assets/images/is-node-subscribed-icon.inline.svg'; +// import isNodeSubscribedIcon from '../../../assets/images/is-node-subscribed-icon.inline.svg'; import isNodeTimeCorrectIcon from '../../../assets/images/is-node-time-correct-icon.inline.svg'; import isNodeSyncingIcon from '../../../assets/images/is-node-syncing-icon.inline.svg'; import type { CardanoNodeState } from '../../../../../common/types/cardano-node.types'; @@ -18,53 +18,60 @@ import type { CardanoNodeState } from '../../../../../common/types/cardano-node. const messages = defineMessages({ nodeIsRunning: { id: 'status.icons.nodeIsRunning', - defaultMessage: '!!!Node is running!', - description: 'Message "Node is running" on the status icon tooltip', + defaultMessage: '!!!Cardano node is running!', + description: 'Message "Cardano node is running" on the status icon tooltip', }, nodeIsStarting: { id: 'status.icons.nodeIsStarting', - defaultMessage: '!!!Node is starting!', + defaultMessage: '!!!Cardano node is starting!', description: 'Message "Node is starting" on the status icon tooltip', }, nodeIsExiting: { id: 'status.icons.nodeIsExiting', - defaultMessage: '!!!Node is exiting!', - description: 'Message "Node is exiting" on the status icon tooltip', + defaultMessage: '!!!Cardano node is exiting!', + description: 'Message "Cardano node is exiting" on the status icon tooltip', }, nodeIsStopping: { id: 'status.icons.nodeIsStopping', - defaultMessage: '!!!Node is stopping!', - description: 'Message "Node is stopping" on the status icon tooltip', + defaultMessage: '!!!Cardano node is stopping!', + description: + 'Message "Cardano node is stopping" on the status icon tooltip', }, nodeHasStopped: { id: 'status.icons.nodeHasStopped', - defaultMessage: '!!!Node has stopped!', - description: 'Message "Node has stopped" on the status icon tooltip', + defaultMessage: '!!!Cardano node has stopped!', + description: + 'Message "Cardano node has stopped" on the status icon tooltip', }, nodeIsUpdating: { id: 'status.icons.nodeIsUpdating', - defaultMessage: '!!!Node is updating!', - description: 'Message "Node is updating" on the status icon tooltip', + defaultMessage: '!!!Cardano node is updating!', + description: + 'Message "Cardano node is updating" on the status icon tooltip', }, nodeHasBeenUpdated: { id: 'status.icons.nodeHasBeenUpdated', - defaultMessage: '!!!Node has been updated!', - description: 'Message "Node has been updated" on the status icon tooltip', + defaultMessage: '!!!Cardano node has been updated!', + description: + 'Message "Cardano node has been updated" on the status icon tooltip', }, nodeHasCrashed: { id: 'status.icons.nodeHasCrashed', - defaultMessage: '!!!Node has crashed!', - description: 'Message "Node has crashed" on the status icon tooltip', + defaultMessage: '!!!Cardano node has crashed!', + description: + 'Message "Cardano node has crashed" on the status icon tooltip', }, nodeHasErrored: { id: 'status.icons.nodeHasErrored', - defaultMessage: '!!!Node has errored!', - description: 'Message "Node has errored" on the status icon tooltip', + defaultMessage: '!!!Cardano node has errored!', + description: + 'Message "Cardano node has errored" on the status icon tooltip', }, nodeIsUnrecoverable: { id: 'status.icons.nodeIsUnrecoverable', - defaultMessage: '!!!Node is unrecoverable!', - description: 'Message "Node is unrecoverable" on the status icon tooltip', + defaultMessage: '!!!Cardano node is unrecoverable!', + description: + 'Message "Cardano node is unrecoverable" on the status icon tooltip', }, checkYourInternetConnection: { id: 'status.icons.checkYourInternetConnection', @@ -74,68 +81,74 @@ const messages = defineMessages({ }, isNodeRespondingOn: { id: 'status.icons.isNodeRespondingOn', - defaultMessage: '!!!Node is responding!', - description: 'Message "Node is responding" on the status icon tooltip', + defaultMessage: '!!!Cardano node is responding!', + description: + 'Message "Cardano node is responding" on the status icon tooltip', }, isNodeRespondingOff: { id: 'status.icons.isNodeRespondingOff', - defaultMessage: '!!!Node is not responding!', - description: 'Message "Node is not responding" on the status icon tooltip', + defaultMessage: '!!!Cardano node is not responding!', + description: + 'Message "Cardano node is not responding" on the status icon tooltip', }, isNodeRespondingLoading: { id: 'status.icons.isNodeRespondingLoading', - defaultMessage: '!!!Checking if Node is responding!', + defaultMessage: '!!!Checking if Cardano node is responding!', description: - 'Message "Checking if Node is responding" on the status icon tooltip', + 'Message "Checking if Cardano node is responding" on the status icon tooltip', }, isNodeSubscribedOn: { id: 'status.icons.isNodeSubscribedOn', - defaultMessage: '!!!Node is subscribed!', - description: 'Message "Node is subscribed" on the status icon tooltip', + defaultMessage: '!!!Cardano node is subscribed!', + description: + 'Message "Cardano node is subscribed" on the status icon tooltip', }, isNodeSubscribedOff: { id: 'status.icons.isNodeSubscribedOff', - defaultMessage: '!!!Node is not subscribed!', - description: 'Message "Node is not subscribed" on the status icon tooltip', + defaultMessage: '!!!Cardano node is not subscribed!', + description: + 'Message "Cardano node is not subscribed" on the status icon tooltip', }, isNodeSubscribedLoading: { id: 'status.icons.isNodeSubscribedLoading', - defaultMessage: '!!!Checking if Node is subscribed!', + defaultMessage: '!!!Checking if Cardano node is subscribed!', description: - 'Message "Checking if Node is subscribed" on the status icon tooltip', + 'Message "Checking if Cardano node is subscribed" on the status icon tooltip', }, isNodeTimeCorrectOn: { id: 'status.icons.isNodeTimeCorrectOn', - defaultMessage: '!!!Node time is correct!', - description: 'Message "Node time is correct" on the status icon tooltip', + defaultMessage: '!!!Cardano node time is correct!', + description: + 'Message "Cardano node time is correct" on the status icon tooltip', }, isNodeTimeCorrectOff: { id: 'status.icons.isNodeTimeCorrectOff', - defaultMessage: '!!!Node time is not correct!', + defaultMessage: '!!!Cardano node time is not correct!', description: - 'Message "Node time is not correct" on the status icon tooltip', + 'Message "Cardano node time is not correct" on the status icon tooltip', }, isNodeTimeCorrectLoading: { id: 'status.icons.isNodeTimeCorrectLoading', - defaultMessage: '!!!Checking if Node time is correct!', + defaultMessage: '!!!Checking if Cardano node time is correct!', description: - 'Message "Checking if Node time is correct" on the status icon tooltip', + 'Message "Checking if Cardano node time is correct" on the status icon tooltip', }, isNodeSyncingOn: { id: 'status.icons.isNodeSyncingOn', - defaultMessage: '!!!Node is syncing!', - description: 'Message "Node is syncing" on the status icon tooltip', + defaultMessage: '!!!Cardano node is syncing!', + description: 'Message "Cardano node is syncing" on the status icon tooltip', }, isNodeSyncingOff: { id: 'status.icons.isNodeSyncingOff', - defaultMessage: '!!!Node is not syncing!', - description: 'Message "Node is not syncing" on the status icon tooltip', + defaultMessage: '!!!Cardano node is not syncing!', + description: + 'Message "Cardano node is not syncing" on the status icon tooltip', }, isNodeSyncingLoading: { id: 'status.icons.isNodeSyncingLoading', - defaultMessage: '!!!Checking if Node is syncing!', + defaultMessage: '!!!Checking if Cardano node is syncing!', description: - 'Message "Checking if Node is syncing" on the status icon tooltip', + 'Message "Checking if Cardano node is syncing" on the status icon tooltip', }, }); @@ -233,10 +246,11 @@ export default class StatusIcons extends Component { getIconWithToolTip = (icon: string, paramName: string) => ( + label={intl.formatMessage(messages.onCheckTheTimeAgainLink)} + hasIconAfter={false} + skin={LinkSkin} + />
) : (
@@ -176,12 +169,15 @@ export default class SystemTimeError extends Component { />

- + label={intl.formatMessage( + messages.onContinueWithoutClockSyncCheckLink + )} + hasIconAfter={false} + skin={LinkSkin} + />
)} diff --git a/source/renderer/app/components/loading/system-time-error/SystemTimeError.scss b/source/renderer/app/components/loading/system-time-error/SystemTimeError.scss index a7fc2d3cb2..0c7de4dfde 100644 --- a/source/renderer/app/components/loading/system-time-error/SystemTimeError.scss +++ b/source/renderer/app/components/loading/system-time-error/SystemTimeError.scss @@ -16,7 +16,7 @@ right: 0; text-align: center; top: 0; - z-index: 21; + z-index: 20; .icon { margin-bottom: 36px; @@ -55,24 +55,33 @@ opacity: 0.8; } - a { - @include link(--theme-system-error-overlay-support-link-icon-color); - border-bottom: 1px solid var(--theme-system-error-overlay-text-color); + .supportPortalLink { + border-bottom: 1px solid var(--theme-system-error-overlay-text-color) !important; color: var(--theme-system-error-overlay-text-color); + font-size: 16px; + line-height: 1.38; + + &:after { + background-color: var( + --theme-system-error-overlay-support-link-icon-color + ) !important; + } } .checkLink { - border-bottom: 1px solid var(--theme-system-error-overlay-text-color); + border-bottom: 1px solid var(--theme-system-error-overlay-text-color) !important; color: var(--theme-system-error-overlay-text-color); cursor: pointer; + display: inline-block; + font-family: var(--font-regular); font-size: 14px; line-height: 1.36; margin-top: 30px; opacity: 0.8; - &:disabled { + &.disabled { + border-bottom-color: transparent !important; cursor: default; - text-decoration: none; @include animated-ellipsis($width: 16px); } } diff --git a/source/renderer/app/components/navigation/NavDropdown.js b/source/renderer/app/components/navigation/NavDropdown.js index 8650e7a8fb..ea76ba42bc 100644 --- a/source/renderer/app/components/navigation/NavDropdown.js +++ b/source/renderer/app/components/navigation/NavDropdown.js @@ -2,11 +2,7 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; - -import { Select } from 'react-polymorph/lib/components/Select'; -import { SelectSkin } from './NavSelectSkin'; -import selectStyles from './NavSelectStyles.scss'; - +import { Dropdown } from 'react-polymorph/lib/components/Dropdown'; import NavButton from './NavButton'; import styles from './NavDropdown.scss'; @@ -15,7 +11,10 @@ type Props = { activeItem: string, icon?: string, isActive: boolean, - options?: Array<{ value: number | string, label: string }>, + options: Array<{ + value: number | string, + label: string, + }>, onChange: Function, hasNotification?: boolean, }; @@ -38,7 +37,7 @@ export default class NavDropdown extends Component { ]); return (
- - - {error &&

{intl.formatMessage(error)}

} - -
- - ); - } -} diff --git a/source/renderer/app/components/profile/terms-of-use/TermsOfUseForm.js b/source/renderer/app/components/profile/terms-of-use/TermsOfUseForm.js index 63881d4fdd..ef5c2fd0bc 100644 --- a/source/renderer/app/components/profile/terms-of-use/TermsOfUseForm.js +++ b/source/renderer/app/components/profile/terms-of-use/TermsOfUseForm.js @@ -14,20 +14,20 @@ import styles from './TermsOfUseForm.scss'; const messages = defineMessages({ checkboxLabel: { id: 'profile.termsOfUse.checkboxLabel', - defaultMessage: '!!!I agree with terms of use', - description: 'Label for the "I agree with terms of use" checkbox.', + defaultMessage: '!!!I agree with terms of service', + description: 'Label for the "I agree with terms of service" checkbox.', }, checkboxLabelWithDisclaimer: { id: 'profile.termsOfUse.checkboxLabelWithDisclaimer', defaultMessage: '!!!I understand that the terms of use are only available in English and agree to the terms of use', description: - 'Label for the "I agree with terms of use" checkbox when terms of use are not translated.', + 'Label for the "I agree with terms of service" checkbox when terms of use are not translated.', }, submitLabel: { id: 'profile.termsOfUse.submitLabel', defaultMessage: '!!!Continue', - description: 'Label for the "Terms of use" form submit button.', + description: 'Label for the "Terms of service" form submit button.', }, }); @@ -36,6 +36,7 @@ type Props = { onSubmit: Function, isSubmitting: boolean, error?: ?LocalizableError, + onOpenExternalLink: Function, }; type State = { @@ -64,7 +65,12 @@ export default class TermsOfUseForm extends Component { render() { const { intl } = this.context; - const { isSubmitting, error, localizedTermsOfUse } = this.props; + const { + isSubmitting, + error, + localizedTermsOfUse, + onOpenExternalLink, + } = this.props; const { areTermsOfUseAccepted } = this.state; const buttonClasses = classnames([ 'primary', @@ -74,7 +80,10 @@ export default class TermsOfUseForm extends Component { return (
- +
{ + termsOfUseClickHandler = (event: SyntheticMouseEvent) => { + const linkUrl = get(event, ['target', 'href']); + if (linkUrl) { + event.preventDefault(); + this.props.onOpenExternalLink(linkUrl); + } + }; + render() { return ( -
+
{ render() { const { theme, selectTheme } = this.props; const { intl } = this.context; + const { isIncentivizedTestnet, isFlight, environment } = global; + const { isDev } = environment; + + const themeIncentivizedTestnetClasses = classnames([ + theme === THEMES.INCENTIVIZED_TESTNET ? styles.active : styles.inactive, + styles.themeImageWrapper, + ]); const themeLightBlueClasses = classnames([ theme === THEMES.LIGHT_BLUE ? styles.active : styles.inactive, @@ -90,6 +108,11 @@ export default class DisplaySettings extends Component { styles.themeImageWrapper, ]); + const themeFlightCandidateClasses = classnames([ + theme === THEMES.FLIGHT_CANDIDATE ? styles.active : styles.inactive, + styles.themeImageWrapper, + ]); + const themeYellowClasses = classnames([ theme === THEMES.YELLOW ? styles.active : styles.inactive, styles.themeImageWrapper, @@ -181,6 +204,42 @@ export default class DisplaySettings extends Component { {intl.formatMessage(messages.themeYellow)}
+ +
+ {(isDev || isIncentivizedTestnet) && ( + + )} + + {(isDev || isFlight) && ( + + )} +
); } diff --git a/source/renderer/app/components/settings/categories/DisplaySettings.scss b/source/renderer/app/components/settings/categories/DisplaySettings.scss index 97c781f26a..e1eeb660e8 100644 --- a/source/renderer/app/components/settings/categories/DisplaySettings.scss +++ b/source/renderer/app/components/settings/categories/DisplaySettings.scss @@ -13,6 +13,7 @@ margin-bottom: 20px; .themeImageWrapper { + width: calc(33.33% - 80px / 3); &.active { img, span { @@ -35,7 +36,7 @@ } & > img { - border: 1px solid var(--theme-input-border-color); + border: 1px solid var(--theme-settings-theme-select-border-color); display: block; opacity: 0.3; width: 100%; diff --git a/source/renderer/app/components/settings/categories/GeneralSettings.js b/source/renderer/app/components/settings/categories/GeneralSettings.js index a0934e5ea6..55d981f2ed 100644 --- a/source/renderer/app/components/settings/categories/GeneralSettings.js +++ b/source/renderer/app/components/settings/categories/GeneralSettings.js @@ -1,83 +1,29 @@ // @flow import React, { Component } from 'react'; import { observer } from 'mobx-react'; -import classNames from 'classnames'; -import { Select } from 'react-polymorph/lib/components/Select'; -import { SelectSkin } from 'react-polymorph/lib/skins/simple/SelectSkin'; -import { defineMessages, intlShape } from 'react-intl'; -import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; -import LocalizableError from '../../../i18n/LocalizableError'; -import styles from './GeneralSettings.scss'; -import type { ReactIntlMessage } from '../../../types/i18nTypes'; - -const messages = defineMessages({ - languageSelectLabel: { - id: 'settings.general.languageSelect.label', - defaultMessage: '!!!Language', - description: 'Label for the language select.', - }, -}); - -type Props = { - languages: Array<{ value: string, label: ReactIntlMessage }>, - currentLocale: string, - onSelectLanguage: Function, - isSubmitting: boolean, - error?: ?LocalizableError, -}; +import ProfileSettingsForm from '../../widgets/forms/ProfileSettingsForm'; +import type { ProfileSettingsFormProps } from '../../widgets/forms/ProfileSettingsForm'; @observer -export default class GeneralSettings extends Component { - static contextTypes = { - intl: intlShape.isRequired, - }; - - selectLanguage = (values: { locale: string }) => { - this.props.onSelectLanguage({ locale: values }); - }; - - form = new ReactToolboxMobxForm( - { - fields: { - languageId: { - label: this.context.intl.formatMessage(messages.languageSelectLabel), - value: this.props.currentLocale, - }, - }, - }, - { - options: { - validateOnChange: false, - }, - } - ); - +export default class GeneralSettings extends Component { render() { - const { languages, isSubmitting, error } = this.props; - const { intl } = this.context; - const { form } = this; - const languageId = form.$('languageId'); - const languageOptions = languages.map(language => ({ - value: language.value, - label: intl.formatMessage(language.label), - })); - const componentClassNames = classNames([styles.component, 'general']); - const languageSelectClassNames = classNames([ - styles.language, - isSubmitting ? styles.submitLanguageSpinner : null, - ]); + const { + onChangeItem, + currentLocale, + currentNumberFormat, + currentDateFormat, + currentTimeFormat, + error, + } = this.props; return ( -
-
); diff --git a/source/renderer/app/components/staking/delegation-center/DropdownMenu.scss b/source/renderer/app/components/staking/delegation-center/DropdownMenu.scss index 413753bd74..ba400b42e9 100644 --- a/source/renderer/app/components/staking/delegation-center/DropdownMenu.scss +++ b/source/renderer/app/components/staking/delegation-center/DropdownMenu.scss @@ -1,20 +1,7 @@ .component { :global { - .SimpleSelect_select { - .SimpleSelect_selectInput { - &::after { - display: none; - } - input { - border: 0; - height: 22px; - padding: 0; - width: 22px; - } - .SimpleInput_customValueBlock { - height: auto; - } - } + .SimpleDropdown_dropdown { + position: relative; } .SimpleOptions_options { @@ -42,7 +29,7 @@ ); } - &::after { + &:after { display: none; } @@ -78,8 +65,13 @@ border: none; border-radius: 3px; box-shadow: 0 5px 20px 0 rgba(0, 0, 0, 0.12); + margin-top: 5px !important; min-width: auto; right: 15px; + + .SimpleOptions_ul { + max-height: none !important; + } } [data-bubble-arrow]:before, [data-bubble-arrow]:after { diff --git a/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationDialog.js b/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationDialog.js new file mode 100644 index 0000000000..6eed206067 --- /dev/null +++ b/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationDialog.js @@ -0,0 +1,337 @@ +// @flow +/* eslint-disable jsx-a11y/label-has-associated-control, jsx-a11y/label-has-for */ +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { get } from 'lodash'; +import BigNumber from 'bignumber.js'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import classnames from 'classnames'; +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 ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; +import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../../config/timingConfig'; +import { formattedWalletAmount } from '../../../utils/formatters'; +import DialogCloseButton from '../../widgets/DialogCloseButton'; +import { FormattedHTMLMessageWithLink } from '../../widgets/FormattedHTMLMessageWithLink'; +import Dialog from '../../widgets/Dialog'; +import globalMessages from '../../../i18n/global-messages'; +import LocalizableError from '../../../i18n/LocalizableError'; +import { submitOnEnter } from '../../../utils/form'; +import styles from './UndelegateConfirmationDialog.scss'; + +const messages = defineMessages({ + dialogTitle: { + id: 'staking.delegationCenter.undelegate.dialog.title', + defaultMessage: '!!!Undelegate', + description: 'Title for the "Undelegate" dialog.', + }, + confirmButtonLabel: { + id: 'staking.delegationCenter.undelegate.dialog.confirmButtonLabel', + defaultMessage: '!!!Undelegate', + description: + 'Label for the "Undelegate" button in the "Undelegate" dialog.', + }, + descriptionWithTicker: { + id: 'staking.delegationCenter.undelegate.dialog.descriptionWithTicker', + defaultMessage: + '!!!

The stake from your wallet {walletName} is currently delegated to the [{stakePoolTicker}] {stakePoolName} stake pool.

Do you want to undelegate your stake and stop earning rewards?

', + description: 'Description for the "Undelegate" dialog.', + }, + descriptionWithUnknownTicker: { + id: + 'staking.delegationCenter.undelegate.dialog.descriptionWithUnknownTicker', + defaultMessage: + '!!!

The stake from your wallet {walletName} is currently delegated to the {stakePoolTicker} stake pool.

Do you want to undelegate your stake and stop earning rewards?

', + description: 'Description for the "Undelegate" dialog.', + }, + confirmUnsupportCheck: { + id: 'staking.delegationCenter.undelegate.dialog.confirmUnsupportCheck', + defaultMessage: + '!!!I understand that I am not supporting the Cardano network when my stake is undelegated.', + description: + 'Label for the unsupport confirmation check in the "Undelegate" dialog.', + }, + confirmUneligibleCheck: { + id: 'staking.delegationCenter.undelegate.dialog.confirmUneligibleCheck', + defaultMessage: + '!!!I understand that I will not be eligible to earn rewards when my stake is undelegated.', + description: + 'Label for the uneligible confirmation check in the "Undelegate" dialog.', + }, + feesLabel: { + id: 'staking.delegationCenter.undelegate.dialog.feesLabel', + defaultMessage: '!!!Fees', + description: 'Fees label in the "Undelegate" dialog.', + }, + adaLabel: { + id: 'staking.delegationCenter.undelegate.dialog.adaLabel', + defaultMessage: '!!!ADA', + description: 'ADA label in the "Undelegate" dialog.', + }, + spendingPasswordLabel: { + id: 'staking.delegationCenter.undelegate.dialog.spendingPasswordLabel', + defaultMessage: '!!!Spending password', + description: 'Spending password label in the "Undelegate" dialog.', + }, + spendingPasswordPlaceholder: { + id: + 'staking.delegationCenter.undelegate.dialog.spendingPasswordPlaceholder', + defaultMessage: '!!!Type your spending password here', + description: 'Spending password placeholder in the "Undelegate" dialog.', + }, + passwordErrorMessage: { + id: 'staking.delegationCenter.undelegate.dialog.passwordError', + defaultMessage: '!!!Incorrect spending password.', + description: 'Label for password error in the "Undelegate" dialog.', + }, + unknownStakePoolLabel: { + id: 'staking.delegationCenter.undelegate.dialog.unknownStakePoolLabel', + defaultMessage: '!!!unknown', + description: 'unknown stake pool label in the "Undelegate" dialog.', + }, +}); + +messages.fieldIsRequired = globalMessages.fieldIsRequired; + +type Props = { + walletName: string, + stakePoolName: ?string, + stakePoolTicker: ?string, + onConfirm: Function, + onCancel: Function, + onExternalLinkClick: Function, + isSubmitting: boolean, + error: ?LocalizableError, + fees: BigNumber, +}; + +@observer +export default class UndelegateConfirmationDialog extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + form = new ReactToolboxMobxForm( + { + fields: { + isConfirmUnsupportChecked: { + type: 'checkbox', + label: this.context.intl.formatMessage( + messages.confirmUnsupportCheck + ), + value: false, + validators: [ + ({ field }) => { + if (field.value === false) { + return [ + false, + this.context.intl.formatMessage(messages.fieldIsRequired), + ]; + } + return [true]; + }, + ], + }, + isConfirmUneligibleChecked: { + type: 'checkbox', + label: this.context.intl.formatMessage( + messages.confirmUneligibleCheck + ), + value: false, + validators: [ + ({ field }) => { + if (field.value === false) { + return [ + false, + this.context.intl.formatMessage(messages.fieldIsRequired), + ]; + } + return [true]; + }, + ], + }, + passphrase: { + type: 'password', + label: this.context.intl.formatMessage( + messages.spendingPasswordLabel + ), + placeholder: this.context.intl.formatMessage( + messages.spendingPasswordPlaceholder + ), + value: '', + validators: [ + ({ field }) => { + if (field.value === '') { + return [ + false, + this.context.intl.formatMessage(messages.fieldIsRequired), + ]; + } + return [true]; + }, + ], + }, + }, + }, + { + options: { + validateOnChange: true, + validationDebounceWait: FORM_VALIDATION_DEBOUNCE_WAIT, + }, + } + ); + + isConfirmDisabled = () => { + const { form } = this; + const { isSubmitting } = this.props; + const { isValid: unsupportCheckboxIsValid } = form.$( + 'isConfirmUnsupportChecked' + ); + const { isValid: uneligibleCheckboxIsValid } = form.$( + 'isConfirmUneligibleChecked' + ); + const { isValid: passphraseIsValid } = form.$('passphrase'); + + return ( + isSubmitting || + !unsupportCheckboxIsValid || + !uneligibleCheckboxIsValid || + !passphraseIsValid + ); + }; + + handleSubmit = () => { + if (this.isConfirmDisabled()) { + return false; + } + + return this.form.submit({ + onSuccess: form => { + const { onConfirm } = this.props; + const { passphrase } = form.values(); + onConfirm(passphrase); + }, + onError: () => null, + }); + }; + + handleSubmitOnEnter = (event: KeyboardEvent) => + submitOnEnter(this.handleSubmit, event); + + generateErrorElement = () => { + const { error, onExternalLinkClick } = this.props; + + if (!error) { + return null; + } + + const errorHasLink = !!get(error, 'values.linkLabel', false); + const result = errorHasLink ? ( + + ) : ( + this.context.intl.formatMessage(error) + ); + + return result; + }; + + render() { + const { form } = this; + const { intl } = this.context; + const unsupportCheckboxField = form.$('isConfirmUnsupportChecked'); + const uneligibleCheckboxField = form.$('isConfirmUneligibleChecked'); + const passphraseField = form.$('passphrase'); + const { + walletName, + stakePoolName, + stakePoolTicker, + onCancel, + isSubmitting, + fees, + } = this.props; + const isConfirmDisabled = this.isConfirmDisabled(); + const buttonClasses = classnames([ + 'attention', + isSubmitting ? styles.isSubmitting : null, + ]); + const actions = [ + { + label: intl.formatMessage(globalMessages.cancel), + onClick: !isSubmitting ? onCancel : () => null, + }, + { + className: buttonClasses, + label: intl.formatMessage(messages.confirmButtonLabel), + onClick: this.handleSubmit, + disabled: isConfirmDisabled, + primary: true, + }, + ]; + const errorElement = this.generateErrorElement(); + + return ( + null} + className={styles.dialog} + closeButton={} + > +
+ {stakePoolTicker ? ( + + ) : ( + + )} +
+ + +
+
+ +

+ {formattedWalletAmount(fees, false)} + +  {intl.formatMessage(messages.adaLabel)} + +

+
+ + {errorElement &&

{errorElement}

} +
+ ); + } +} diff --git a/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationDialog.scss b/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationDialog.scss new file mode 100644 index 0000000000..a77db0854e --- /dev/null +++ b/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationDialog.scss @@ -0,0 +1,62 @@ +@import '../../../themes/mixins/loading-spinner'; +@import '../../../themes/mixins/error-message'; + +.dialog { + font-family: var(--font-light); + + .description { + p { + font-weight: 300; + line-height: 1.38; + margin-bottom: 15px; + } + } + + :global { + .SimpleCheckbox_root { + margin-top: 20px; + + .SimpleCheckbox_check { + border-color: var(--theme-input-remove-color-light); + + &.SimpleCheckbox_checked { + background: var(--theme-input-remove-color-light); + } + } + } + } + + .divider { + border-top: 1px solid + var(--theme-staking-delegation-center-divider-border-color); + height: 1px; + margin: 20px 0; + width: 100%; + } + + .feesWrapper { + font-family: var(--font-medium); + font-weight: 500; + line-height: 1.38; + margin-bottom: 20px; + + .feesAmount { + color: var(--theme-staking-delegation-center-fees-amount-color); + user-select: text; + + .feesAmountLabel { + opacity: 0.5; + } + } + } + .error { + @include error-message; + margin-top: 27px; + text-align: center; + } + + .isSubmitting { + box-shadow: none !important; + @include loading-spinner('../../../assets/images/spinner-light.svg'); + } +} diff --git a/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationResultDialog.js b/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationResultDialog.js new file mode 100644 index 0000000000..dd3a4ca72e --- /dev/null +++ b/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationResultDialog.js @@ -0,0 +1,126 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import SVGInline from 'react-svg-inline'; +import DialogCloseButton from '../../widgets/DialogCloseButton'; +import Dialog from '../../widgets/Dialog'; +import styles from './UndelegateConfirmationResultDialog.scss'; +import globalMessages from '../../../i18n/global-messages'; +import sadLogo from '../../../assets/images/untada.inline.svg'; +import humanizeDurationByLocale from '../../../utils/humanizeDurationByLocale'; +import { EPOCH_COUNTDOWN_INTERVAL } from '../../../config/epochsConfig'; + +const messages = defineMessages({ + dialogTitle: { + id: 'staking.delegationCenter.undelegate.result.dialog.title', + defaultMessage: '!!!Wallet undelegated', + description: 'Title for the "Undelegate Result" dialog.', + }, + description1: { + id: 'staking.delegationCenter.undelegate.result.dialog.description1', + defaultMessage: + '!!!The stake from your wallet {walletName} is no longer delegated and you will soon stop earning rewards for this wallet.', + description: 'Description 1 for the "Undelegate Result" dialog.', + }, + description2: { + id: 'staking.delegationCenter.undelegate.result.dialog.description2', + defaultMessage: + '!!!Your new delegation preferences are now posted on the blockchain and will take effect after both the current and next Cardano epochs have completed in {timeUntilNextEpochStart}. During this time, your previous delegation preferences are still active.', + description: 'Description 2 for the "Undelegate Result" dialog.', + }, +}); + +type Props = { + walletName: string, + futureEpochStartTime: string, + currentLocale: string, + onClose: Function, +}; +type State = { timeUntilNextEpochStart: number }; + +@observer +export default class UndelegateConfirmationResultDialog extends Component< + Props, + State +> { + intervalHandler: ?IntervalID = null; + state = { timeUntilNextEpochStart: 0 }; + + static contextTypes = { + intl: intlShape.isRequired, + }; + + componentDidMount() { + this.updateTimeUntilNextEpochStart(); + this.intervalHandler = setInterval( + () => this.updateTimeUntilNextEpochStart(), + EPOCH_COUNTDOWN_INTERVAL + ); + } + + updateTimeUntilNextEpochStart = () => { + const { futureEpochStartTime } = this.props; + const timeUntilNextEpochStart = Math.max( + 0, + new Date(futureEpochStartTime).getTime() - new Date().getTime() + ); + this.setState({ timeUntilNextEpochStart }); + }; + + componentWillUnmount() { + if (this.intervalHandler) { + clearInterval(this.intervalHandler); + } + } + + render() { + const { intl } = this.context; + const { walletName, onClose, currentLocale } = this.props; + const actions = [ + { + label: intl.formatMessage(globalMessages.close), + onClick: onClose, + primary: true, + }, + ]; + + const timeUntilNextEpochStart = humanizeDurationByLocale( + this.state.timeUntilNextEpochStart, + currentLocale + ); + + return ( + } + > +
+ +
+
+

+ +

+

+ +

+
+
+ ); + } +} diff --git a/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationResultDialog.scss b/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationResultDialog.scss new file mode 100644 index 0000000000..bce0e3ba56 --- /dev/null +++ b/source/renderer/app/components/staking/delegation-center/UndelegateConfirmationResultDialog.scss @@ -0,0 +1,31 @@ +.dialog { + font-family: var(--font-light); + + .sadLogoContainer { + display: flex; + justify-content: center; + margin: 5px 0 25px; + + .sadLogoIcon { + height: 80px; + opacity: 0.7; + width: 80px; + + svg { + path { + fill: var(--theme-dialog-text-color); + } + } + } + } + .description { + p { + font-weight: 300; + line-height: 1.38; + margin-bottom: 15px; + } + p:last-child { + margin-bottom: 0; + } + } +} diff --git a/source/renderer/app/components/staking/delegation-center/WalletRow.js b/source/renderer/app/components/staking/delegation-center/WalletRow.js index 6ea52e03b7..9ca07cd11a 100644 --- a/source/renderer/app/components/staking/delegation-center/WalletRow.js +++ b/source/renderer/app/components/staking/delegation-center/WalletRow.js @@ -1,27 +1,21 @@ // @flow import React, { Component, Fragment } from 'react'; import { observer } from 'mobx-react'; -import { - defineMessages, - intlShape, - FormattedMessage, - FormattedHTMLMessage, -} from 'react-intl'; +import { isNil, get, map } from 'lodash'; +import { defineMessages, intlShape, FormattedMessage } from 'react-intl'; import SVGInline from 'react-svg-inline'; -import isNil from 'lodash/isNil'; -import Wallet from '../../../domains/Wallet'; +import classnames from 'classnames'; +import { TooltipSkin } from 'react-polymorph/lib/skins/simple/TooltipSkin'; +import { Tooltip } from 'react-polymorph/lib/components/Tooltip'; +import Wallet, { WalletDelegationStatuses } from '../../../domains/Wallet'; +import StakePool from '../../../domains/StakePool'; import { getColorFromRange } from '../../../utils/colors'; -import settingsIcon from '../../../assets/images/settings-ic.inline.svg'; -import { SIMPLE_DECIMAL_PLACES_IN_ADA } from '../../../config/numbersConfig'; -import DropdownMenu from './DropdownMenu'; -import DonutRing from './DonutRing'; +import adaIcon from '../../../assets/images/ada-symbol.inline.svg'; +import { DECIMAL_PLACES_IN_ADA } from '../../../config/numbersConfig'; import styles from './WalletRow.scss'; - -export const DELEGATION_ACTIONS = { - CHANGE_DELEGATION: 'changeDelegation', - REMOVE_DELEGATION: 'removeDelegation', - DELEGATE: 'delegate', -}; +import tooltipStyles from './WalletRowTooltip.scss'; +import LoadingSpinner from '../../widgets/LoadingSpinner'; +import arrow from '../../../assets/images/collapse-arrow.inline.svg'; const messages = defineMessages({ walletAmount: { @@ -30,57 +24,65 @@ const messages = defineMessages({ description: 'Amount of each wallet for the Delegation center body section.', }, - inactiveStakePercentageActivate: { - id: 'staking.delegationCenter.inactiveStakePercentageActivate', - defaultMessage: - '!!!activate {inactiveStakePercentage}% of inactive stake', - description: - 'Inactive stake percentage of each wallet for the Delegation center body section.', - }, - delegated: { - id: 'staking.delegationCenter.delegated', - defaultMessage: '!!!Delegated', - description: 'Delegated label for the Delegation center body section.', - }, notDelegated: { id: 'staking.delegationCenter.notDelegated', - defaultMessage: '!!!Not-delegated', - description: 'Not-delegated label for the Delegation center body section.', - }, - changeDelegation: { - id: 'staking.delegationCenter.changeDelegation', - defaultMessage: '!!!Change delegation', - description: - 'Change delegation label for the Delegation center body section.', + defaultMessage: '!!!Undelegated', + description: 'Undelegated label for the Delegation center body section.', }, removeDelegation: { id: 'staking.delegationCenter.removeDelegation', - defaultMessage: '!!!Remove delegation', + defaultMessage: '!!!Undelegate', description: 'Remove delegation label for the Delegation center body section.', }, - toStakePoolSlug: { - id: 'staking.delegationCenter.toStakePoolSlug', - defaultMessage: '!!!To [{delegatedStakePoolSlug}] stake pool', + or: { + id: 'staking.delegationCenter.or', + defaultMessage: '!!!or', + description: '"or" text for the Delegation center body section.', + }, + stakePoolTooltipTickerEpoch: { + id: 'staking.delegationCenter.stakePoolTooltipTickerEpoch', + defaultMessage: '!!!From epoch {fromEpoch}', + description: + 'Delegated stake pool tooltip ticker for the Delegation center body section.', + }, + stakePoolTooltipTickerEarningRewards: { + id: 'staking.delegationCenter.stakePoolTooltipTickerEarningRewards', + defaultMessage: '!!!Earning rewards', description: - 'Delegated stake pool slug for the Delegation center body section.', + 'Delegated stake pool tooltip ticker for the Delegation center body section.', }, delegate: { id: 'staking.delegationCenter.delegate', defaultMessage: '!!!Delegate', description: 'Delegate label for the Delegation center body section.', }, - yourStake: { - id: 'staking.delegationCenter.yourStake', - defaultMessage: '!!!your stake', - description: 'Your stake label for the Delegation center body section.', + redelegate: { + id: 'staking.delegationCenter.redelegate', + defaultMessage: '!!!Redelegate', + description: 'Redelegate label for the Delegation center body section.', + }, + unknownStakePoolLabel: { + id: 'staking.delegationCenter.unknownStakePoolLabel', + defaultMessage: '!!!unknown', + description: + 'unknown stake pool label for the Delegation center body section.', + }, + syncingTooltipLabel: { + id: 'staking.delegationCenter.syncingTooltipLabel', + defaultMessage: '!!!Syncing {syncingProgress}%', + description: + 'unknown stake pool label for the Delegation center body section.', }, }); type Props = { wallet: Wallet, - index?: number, + delegatedStakePool?: ?StakePool, + numberOfStakePools: number, onDelegate: Function, + onUndelegate: Function, + getStakePoolById: Function, }; @observer @@ -89,47 +91,63 @@ export default class WalletRow extends Component { intl: intlShape.isRequired, }; - onDelegate = () => { - const { wallet } = this.props; - this.props.onDelegate(wallet.id); - }; - render() { const { intl } = this.context; const { wallet: { name, amount, - inactiveStakePercentage, - isDelegated, - delegatedStakePool, + isRestoring, + syncState, + delegatedStakePoolId, + delegationStakePoolStatus, + pendingDelegations, + lastDelegationStakePoolId, }, - index, + delegatedStakePool, + numberOfStakePools, + getStakePoolById, + onDelegate, + onUndelegate, } = this.props; - const inactiveStakePercentageValue = inactiveStakePercentage || 0; - const color = - isDelegated && !isNil(index) ? getColorFromRange(index) : 'transparent'; + const syncingProgress = get(syncState, 'progress.quantity', ''); + const notDelegatedText = intl.formatMessage(messages.notDelegated); + const removeDelegationText = intl.formatMessage(messages.removeDelegation); + const delegateText = intl.formatMessage(messages.delegate); + const redelegateText = intl.formatMessage(messages.redelegate); + const orText = intl.formatMessage(messages.or); - const delegated = intl.formatMessage(messages.delegated); - const notDelegated = intl.formatMessage(messages.notDelegated); - const changeDelegation = intl.formatMessage(messages.changeDelegation); - const removeDelegation = intl.formatMessage(messages.removeDelegation); - const delegate = intl.formatMessage(messages.delegate); - const yourStake = intl.formatMessage(messages.yourStake); + const hasPendingDelegations = + pendingDelegations && pendingDelegations.length > 0; + const lastDelegationStatus = hasPendingDelegations + ? pendingDelegations[pendingDelegations.length - 1].status + : delegationStakePoolStatus; + const isLastDelegationDelegating = + lastDelegationStatus !== WalletDelegationStatuses.NOT_DELEGATING; - const delegatedWalletActionOptions = [ - { - label: changeDelegation, - value: DELEGATION_ACTIONS.CHANGE_DELEGATION, - className: styles.normalOption, - }, - { - label: removeDelegation, - value: DELEGATION_ACTIONS.REMOVE_DELEGATION, - className: styles.removeOption, - }, - ]; + const delegatedStakePoolColor = + delegatedStakePool && !isNil(delegatedStakePool.ranking) + ? getColorFromRange(delegatedStakePool.ranking, numberOfStakePools) + : null; + + let stakePoolRankingIndicatorColor = delegatedStakePoolColor; + if (hasPendingDelegations) { + const pendingDerlegationStakePool = getStakePoolById( + lastDelegationStakePoolId + ); + stakePoolRankingIndicatorColor = !pendingDerlegationStakePool + ? null + : getColorFromRange( + pendingDerlegationStakePool.ranking, + numberOfStakePools + ); + } + + const actionStyles = classnames([ + styles.action, + isLastDelegationDelegating ? styles.undelegateActions : null, + ]); return (
@@ -139,70 +157,180 @@ export default class WalletRow extends Component { - {inactiveStakePercentageValue > 0 && ( - - - - - - - )}
+ + {/* Delegation preferences */}
-
-
- {isDelegated ? delegated : notDelegated} - {isDelegated && ( - - } - menuItems={delegatedWalletActionOptions} - onMenuItemClick={() => null} - /> - )} -
-
- {isDelegated && delegatedStakePool ? ( - - ) : ( - + {!isRestoring ? ( + +
+ {/* Statuses */} +
+ {/* Active (current) delegation */} + {delegatedStakePoolId ? ( + + + {intl.formatMessage( + messages.stakePoolTooltipTickerEarningRewards + )} + +
+ } + > +
+ + [ + {delegatedStakePool + ? delegatedStakePool.ticker + : intl.formatMessage( + messages.unknownStakePoolLabel + )} + ] +
+ + ) : ( + notDelegatedText + )} + + + {/* Next (pending) delegations */} + {hasPendingDelegations && ( + + )} + {hasPendingDelegations && + map(pendingDelegations, (pendingDelegation, key) => { + const pendingDelegationStakePool = getStakePoolById( + pendingDelegation.target + ); + const isUnknown = + !!pendingDelegation.target && + !pendingDelegationStakePool; + const isLast = key + 1 === pendingDelegations.length; + const fromEpoch = get( + pendingDelegation, + ['changes_at', 'epoch_number'], + 0 + ); + + const pendingStakePoolColor = + pendingDelegationStakePool && + !isNil(pendingDelegationStakePool.ranking) + ? getColorFromRange( + pendingDelegationStakePool.ranking, + numberOfStakePools + ) + : null; + + const tickerClasses = classnames([ + styles.ticker, + isUnknown ? styles.unknown : null, + 'tickerText', + ]); + return [ + + +
+ } + > + + {pendingDelegation.target + ? `[${ + isUnknown + ? intl.formatMessage( + messages.unknownStakePoolLabel + ) + : pendingDelegationStakePool.ticker + }]` + : notDelegatedText} + + , + !isLast && ( + + ), + ]; + })} +
+ + {/* Actions */} +
+ {isLastDelegationDelegating && [ + + {removeDelegationText} + , + {orText} , + ]} + - {delegate} + {!isLastDelegationDelegating + ? delegateText + : redelegateText} - {` ${yourStake}`} - - )} -
-
-
-
-
+
+
+
+
+
+ + ) : ( + + + + )}
); diff --git a/source/renderer/app/components/staking/delegation-center/WalletRow.scss b/source/renderer/app/components/staking/delegation-center/WalletRow.scss index f2c3b9b12a..f51bc4514e 100644 --- a/source/renderer/app/components/staking/delegation-center/WalletRow.scss +++ b/source/renderer/app/components/staking/delegation-center/WalletRow.scss @@ -7,10 +7,24 @@ overflow: visible; padding: 20px 0; + :global { + .SimpleBubble_root { + .SimpleBubble_bubble { + margin-left: 0; + } + } + } + &:last-child { border-bottom: 0; } + &:hover { + .undelegateActions { + opacity: 1 !important; + } + } + .left { .title { @extend %accentText; @@ -34,83 +48,160 @@ .status { @extend %regularText; - align-items: center; display: flex; justify-content: flex-end; - margin-bottom: 3px; + margin-bottom: 7px; - .normalOption { - @extend %regularText; - font-size: 13px; - } - .removeOption { - @extend %regularText; - color: var(--theme-input-remove-color-light); - font-size: 13px; - } - li:hover { - .normalOption { - color: var(--theme-staking-dropdown-item-text-color-hover); + :global { + .SimpleTooltip_root { + margin-right: 0; } } - .gearIcon { - cursor: pointer; - - svg { - height: 12px; - width: 12px; + .ticker { + background-color: var( + --theme-staking-wallet-row-ticker-background-color + ); + border-radius: 2px; + color: var(--theme-staking-wallet-row-ticker-text-color); + font-family: var(--font-semibold); + font-size: 11px; + line-height: 14px; + padding: 3px 8px; + position: relative; + text-transform: uppercase; - path { - fill: var(--theme-staking-delegation-center-gear-icon-fill-color); - } - } - } + .activeAdaSymbol { + svg { + height: 10px; + margin-bottom: -1.5px; + margin-right: 8px; + width: 9px; - :global { - .SimpleSelect_isOpen { - .WalletRow_gearIcon { - svg { - path { - fill: var( - --theme-staking-delegation-center-gear-icon-fill-color-active + & > g { + & > g { + stroke: var( + --theme-staking-wallet-row-ticker-ada-icon-fill-color ); } } } } + } - .SimpleSelect_select { - &:hover { - .WalletRow_gearIcon { - svg { - path { - fill: var( - --theme-staking-delegation-center-gear-icon-fill-color-active - ); - } - } - } - } - } + .tooltipLabelWrapper { + font-family: var(--font-regular); + font-size: 13px; + text-transform: initial; } } + + .status, + .action { + .unknown { + font-family: var(--font-bold); + text-transform: initial; + } + } + .action { @extend %regularText; font-size: 14px; + text-align: right; + + > span { + display: initial !important; + margin-right: 0 !important; + + > div { + bottom: calc(73% + var(--rp-bubble-distance, 14px)); + + > div { + font-size: 13px; + left: -30px !important; + min-width: 200px !important; + padding: 4px 12px; + text-align: right !important; + white-space: normal !important; + + > span:after { + left: calc( + 100% - + calc( + var( + --rp-bubble-arrow-width, + calc(2 * var(--rp-bubble-arrow-size, 10px)) + ) - 2px + ) / 2 - 1px + ) !important; + } + } + } + } } + + .undelegateActions { + opacity: 0; + transition: opacity 0.15s ease-in; + } + .stakePoolRankingIndicator { border-radius: 2px; height: 100%; transform: translate(20px); width: 4px; } + + .sandClockIcon { + svg { + height: 12px; + margin-bottom: -1px; + margin-right: 4px; + width: 12px; + + path { + fill: var( + --theme-staking-delegation-center-gear-icon-fill-color-active + ); + } + } + } + } + + b { + color: var(--theme-link-main-color); + cursor: pointer; + } + + .actionDelegate { + color: var(--theme-staking-wallet-row-action-delegate-text-color); + cursor: pointer; } - b, - .actionLink { - color: var(--theme-staking-link-color); + .actionUndelegate { + color: var(--theme-staking-wallet-row-action-undelegate-text-color); cursor: pointer; } + + :global { + .LoadingSpinner_component.LoadingSpinner_medium { + margin-top: 10px; + } + } + + .arrow { + height: 7px; + margin: -2px 6px 0; + opacity: 0.3; + position: relative; + width: 3.5px; + > svg { + height: 3.5px; + transform: rotate(90deg) translate(-11px, 2px); + path { + stroke: var(--theme-staking-font-color-regular); + } + } + } } diff --git a/source/renderer/app/components/staking/delegation-center/WalletRowTooltip.scss b/source/renderer/app/components/staking/delegation-center/WalletRowTooltip.scss new file mode 100644 index 0000000000..700e970cbd --- /dev/null +++ b/source/renderer/app/components/staking/delegation-center/WalletRowTooltip.scss @@ -0,0 +1,9 @@ +.root { + .bubble { + top: -2px; + > div { + left: auto; + right: -60px; + } + } +} diff --git a/source/renderer/app/components/staking/delegation-center/helpers.js b/source/renderer/app/components/staking/delegation-center/helpers.js index 7be2d00c66..d22a192a6d 100644 --- a/source/renderer/app/components/staking/delegation-center/helpers.js +++ b/source/renderer/app/components/staking/delegation-center/helpers.js @@ -1,8 +1,76 @@ // @flow import { get } from 'lodash'; +import React from 'react'; +import SVGInline from 'react-svg-inline'; +import styles from './DelegationCenterHeader.scss'; +import delimeterIcon from '../../../assets/images/delimeter.inline.svg'; +import delimeterSlashIcon from '../../../assets/images/delimeter-slash.inline.svg'; + +const EPOCH_MAX_LENGTH = 5; export const with2Decimals = (value: number) => { const formattedValue = value.toString().match(/^-?\d+(?:\.\d{0,2})?/); const result = get(formattedValue, 0, 0); return result; }; + +export const generateFieldPanel = (labels: any, values: any, index: number) => { + const value = values[index]; + const includeSlashDelimeter = index === values.length - 2; + const includeDotsDelimeter = + !includeSlashDelimeter && index !== values.length - 1; + const labelStr = labels[index]; + const valueStr = value.toString(); + let zeroValues = ''; + if ( + (index === 1 && valueStr.length < values[index + 1].toString().length) || + (index === 0 && valueStr.length < EPOCH_MAX_LENGTH) + ) { + const zerosToAdd = + index === 1 + ? parseInt(values[index + 1].toString().length, 10) - + parseInt(valueStr.length, 10) + : parseInt(EPOCH_MAX_LENGTH, 10) - parseInt(valueStr.length, 10); + switch (zerosToAdd) { + case 1: + zeroValues = '0'; + break; + case 2: + zeroValues = '00'; + break; + case 3: + zeroValues = '000'; + break; + case 4: + zeroValues = '0000'; + break; + default: + break; + } + } + + return ( +
+
+
{labelStr}
+
+ {zeroValues && {zeroValues}} + {valueStr} +
+
+ {includeDotsDelimeter && ( +
+ +
+ )} + {includeSlashDelimeter && ( +
+ +
+ )} +
+ ); +}; diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationSetupWizardDialog.js b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationSetupWizardDialog.js index 9572174eaf..9959c53598 100644 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationSetupWizardDialog.js +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationSetupWizardDialog.js @@ -1,27 +1,21 @@ // @flow import React, { Component } from 'react'; import { observer } from 'mobx-react'; +import { BigNumber } from 'bignumber.js'; import { get } from 'lodash'; -import DelegationStepsActivationDialog from './DelegationStepsActivationDialog'; +import DelegationStepsSuccessDialog from './DelegationStepsSuccessDialog'; import DelegationStepsChooseWalletDialog from './DelegationStepsChooseWalletDialog'; import DelegationStepsConfirmationDialog from './DelegationStepsConfirmationDialog'; import DelegationStepsIntroDialog from './DelegationStepsIntroDialog'; import DelegationStepsNotAvailableDialog from './DelegationStepsNotAvailableDialog'; import DelegationStepsChooseStakePoolDialog from './DelegationStepsChooseStakePoolDialog'; -import type { StakePool } from '../../../api/staking/types'; - -type DelegationWalletData = { - id: string, - isAcceptableSetupWallet: boolean, - label: string, - value: string, - hasPassword: boolean, -}; +import LocalizableError from '../../../i18n/LocalizableError'; +import StakePool from '../../../domains/StakePool'; +import Wallet from '../../../domains/Wallet'; type Props = { activeStep: number, isDisabled: boolean, - onActivate: Function, onBack: Function, onClose: Function, onConfirm: Function, @@ -29,24 +23,43 @@ type Props = { onLearnMoreClick: Function, onSelectWallet: Function, onSelectPool: Function, + isWalletAcceptable: Function, stepsList: Array, - wallets: Array, + wallets: Array, minDelegationFunds: number, - stakePoolsDelegatingList: Array, + recentStakePools: Array, stakePoolsList: Array, onOpenExternalLink: Function, + getPledgeAddressUrl: Function, currentTheme: string, - selectedWallet: ?DelegationWalletData, + selectedWallet: ?Wallet, selectedPool: ?StakePool, + stakePoolJoinFee: ?BigNumber, + isSubmitting: boolean, + error: ?LocalizableError, + futureEpochStartTime: string, + currentLocale: string, + getStakePoolById: Function, }; @observer export default class DelegationSetupWizardDialog extends Component { + componentWillReceiveProps(nextProps: Props) { + // On confirm delegation step, wait for API stake pool "join" endpoint response + // and redirect to "Ta-Da" step + if ( + this.props.isSubmitting && + !nextProps.isSubmitting && + !nextProps.error + ) { + this.props.onContinue(); + } + } + render() { const { activeStep, isDisabled, - onActivate, onBack, onClose, onConfirm, @@ -57,15 +70,23 @@ export default class DelegationSetupWizardDialog extends Component { stepsList, wallets, minDelegationFunds, - stakePoolsDelegatingList, + recentStakePools, stakePoolsList, onOpenExternalLink, + getPledgeAddressUrl, currentTheme, selectedWallet, selectedPool, + isWalletAcceptable, + stakePoolJoinFee, + futureEpochStartTime, + currentLocale, + isSubmitting, + error, + getStakePoolById, } = this.props; - const selectedWalletHasPassword = get(selectedWallet, 'hasPassword', false); + const selectedWalletId = get(selectedWallet, 'id', null); if (isDisabled) { return ( @@ -81,13 +102,16 @@ export default class DelegationSetupWizardDialog extends Component { case 1: content = ( ); break; @@ -95,9 +119,11 @@ export default class DelegationSetupWizardDialog extends Component { content = ( { case 3: content = ( ); break; case 4: content = ( - ); break; diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationSteps.scss b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationSteps.scss index 548cbfacd3..b5076d0516 100644 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationSteps.scss +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationSteps.scss @@ -10,7 +10,7 @@ .delegationStepsIndicatorWrapper { margin: auto; - width: 410px; + width: 288px; .stepIndicatorLabel { color: var(--theme-delegation-steps-activation-steps-indicator-color); diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsActivationDialog.js b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsActivationDialog.js deleted file mode 100644 index 53ee10439d..0000000000 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsActivationDialog.js +++ /dev/null @@ -1,287 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { - defineMessages, - intlShape, - FormattedMessage, - FormattedHTMLMessage, -} from 'react-intl'; -import classNames from 'classnames'; -import { Stepper } from 'react-polymorph/lib/components/Stepper'; -import { StepperSkin } from 'react-polymorph/lib/skins/simple/StepperSkin'; -import { Input } from 'react-polymorph/lib/components/Input'; -import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; -import commonStyles from './DelegationSteps.scss'; -import styles from './DelegationStepsActivationDialog.scss'; -import Dialog from '../../widgets/Dialog'; -import DialogCloseButton from '../../widgets/DialogCloseButton'; -import DialogBackButton from '../../widgets/DialogBackButton'; -import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; -import { submitOnEnter } from '../../../utils/form'; -import globalMessages from '../../../i18n/global-messages'; -import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../../config/timingConfig'; - -const messages = defineMessages({ - title: { - id: 'staking.delegationSetup.activation.step.dialog.title', - defaultMessage: '!!!Confirm Delegation', - description: - 'Title "Confirm Delegation" on the delegation setup "activation" step dialog.', - }, - stepIndicatorLabel: { - id: 'staking.delegationSetup.activation.step.dialog.stepIndicatorLabel', - defaultMessage: '!!!STEP {currentStep} OF {totalSteps}', - description: - 'Step indicator labe on the delegation setup "activation" step dialog.', - }, - descriptionLine1: { - id: 'staking.delegationSetup.activation.step.dialog.description.line1', - defaultMessage: - '!!!All new wallet addresses will now match your delegation preferences and the stake associated with ada received on those addresses will be delegated. ', - description: - 'Description "line 1" on the delegation setup "activation" step dialog.', - }, - descriptionLine2: { - id: 'staking.delegationSetup.activation.step.dialog.description.line2', - defaultMessage: - '!!!To delegate ada which remains in old addresses in your wallet, move it to a new address where your delegation preferences have been applied.', - description: - 'Description "line 2" on the delegation setup "activation" step dialog.', - }, - addressLabel: { - id: 'staking.delegationSetup.activation.step.dialog.addressLabel', - defaultMessage: '!!!To', - description: - 'Address label on the delegation setup "activation" step dialog.', - }, - amountLabel: { - id: 'staking.delegationSetup.activation.step.dialog.amountLabel', - defaultMessage: '!!!Amount', - description: - 'Amount label on the delegation setup "activation" step dialog.', - }, - feesLabel: { - id: 'staking.delegationSetup.activation.step.dialog.feesLabel', - defaultMessage: '!!!Fees', - description: 'Fees label on the delegation setup "activation" step dialog.', - }, - totalLabel: { - id: 'staking.delegationSetup.activation.step.dialog.totalLabel', - defaultMessage: '!!!Total', - description: - 'Total label on the delegation setup "activation" step dialog.', - }, - spendingPasswordPlaceholder: { - id: - 'staking.delegationSetup.activation.step.dialog.spendingPasswordPlaceholder', - defaultMessage: '!!!Password', - description: 'Placeholder for "spending password"', - }, - spendingPasswordLabel: { - id: 'staking.delegationSetup.activation.step.dialog.spendingPasswordLabel', - defaultMessage: '!!!Spending password', - description: 'Label for "spending password"', - }, - confirmButtonLabel: { - id: 'staking.delegationSetup.activation.step.dialog.confirmButtonLabel', - defaultMessage: '!!!Confirm', - description: - 'Label for continue button on the delegation setup "activation" step dialog.', - }, - postponeButtonLabel: { - id: 'staking.delegationSetup.activation.step.dialog.postponeButtonLabel', - defaultMessage: '!!!I’ll do it later', - description: - 'Postpone button label on the delegation setup "activation" step dialog.', - }, -}); - -messages.fieldIsRequired = globalMessages.fieldIsRequired; - -type Props = { - isSpendingPasswordSet?: boolean, - onActivate: Function, - onBack: Function, - onClose: Function, - stepsList: Array, -}; - -@observer -export default class DelegationStepsActivationDialog extends Component { - static contextTypes = { - intl: intlShape.isRequired, - }; - - form = new ReactToolboxMobxForm( - { - fields: { - spendingPassword: { - type: 'password', - label: this.context.intl.formatMessage( - messages.spendingPasswordLabel - ), - placeholder: this.context.intl.formatMessage( - messages.spendingPasswordPlaceholder - ), - value: '', - validators: [ - ({ field }) => { - const { isSpendingPasswordSet } = this.props; - const password = field.value; - if (isSpendingPasswordSet && password === '') { - 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 { isSpendingPasswordSet } = this.props; - const { spendingPassword } = form.values(); - const password = isSpendingPasswordSet ? spendingPassword : null; - const data = { - fees: 12.042481, - amount: 3, - total: 15.042481, - password, - }; - this.props.onActivate(data); - form.clear(); - }, - onError: () => {}, - }); - }; - - handleSubmitOnEnter = submitOnEnter.bind(this, this.submit); - - render() { - const { form } = this; - const { intl } = this.context; - const { isSpendingPasswordSet, onBack, onClose, stepsList } = this.props; - - const spendingPasswordField = form.$('spendingPassword'); - - const actions = [ - { - className: 'postponeButton', - label: intl.formatMessage(messages.postponeButtonLabel), - onClick: onClose, - }, - { - className: 'confirmButton', - label: intl.formatMessage(messages.confirmButtonLabel), - onClick: this.submit, - primary: true, - }, - ]; - - const dialogClassName = classNames([ - commonStyles.delegationSteps, - styles.delegationStepsActivationDialogWrapper, - ]); - const contentClassName = classNames([commonStyles.content, styles.content]); - - const stepsIndicatorLabel = ( - - ); - - return ( - } - backButton={} - > -
- -
- -
-
- -

{intl.formatMessage(messages.descriptionLine2)}

-
- -
-

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

-

- YbDziZoPjGmJdssagaugyCqUUJVySKBdA1DUHbpYmQd6yTeFQqfrWWKx9gs19MxMbcEskurDMdVX1h32Fi94Nojxp1gvwM -

-
- -
-
-

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

-

- 3 ADA -

-
- -
-

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

-

- + 12.042481 ADA -

-
-
- -
-

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

-

- 15.042481 ADA -

-
- - {isSpendingPasswordSet && ( - - )} -
-
- ); - } -} diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsActivationDialog.scss b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsActivationDialog.scss deleted file mode 100644 index 6966df44e4..0000000000 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsActivationDialog.scss +++ /dev/null @@ -1,58 +0,0 @@ -.delegationStepsActivationDialogWrapper { - .content { - .description { - color: var(--theme-delegation-steps-activation-description-color); - margin-bottom: 20px; - span > span { - font-weight: bold; - } - } - - .resumeWrapper { - display: flex; - } - - .resumeWrapper, - .totalWrapper, - .addressWrapper { - font-family: var(--font-medium); - font-size: 16px; - font-weight: 500; - line-height: 1.38; - margin-bottom: 20px; - - .label { - color: var(--theme-delegation-steps-activation-fees-label-color); - margin-bottom: 6px; - } - - .amount { - color: var(--theme-delegation-steps-activation-fees-amount-color); - user-select: text; - span { - opacity: 0.5; - } - } - - .amountWrapper { - width: 50%; - } - - .feesWrapper { - width: 50%; - .amount { - opacity: 0.5; - } - } - - .addressValue { - color: var(--theme-delegation-steps-activation-address-value-color); - font-family: var(--font-light); - font-weight: 300; - opacity: 0.5; - user-select: text; - word-break: break-all; - } - } - } -} diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseStakePoolDialog.js b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseStakePoolDialog.js index 386330e082..06830871f3 100644 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseStakePoolDialog.js +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseStakePoolDialog.js @@ -1,6 +1,11 @@ // @flow import React, { Component } from 'react'; -import { defineMessages, intlShape, FormattedMessage } from 'react-intl'; +import { + defineMessages, + intlShape, + FormattedMessage, + FormattedHTMLMessage, +} from 'react-intl'; import classNames from 'classnames'; import SVGInline from 'react-svg-inline'; import { Stepper } from 'react-polymorph/lib/components/Stepper'; @@ -16,34 +21,67 @@ import BackToTopButton from '../../widgets/BackToTopButton'; import commonStyles from './DelegationSteps.scss'; import styles from './DelegationStepsChooseStakePoolDialog.scss'; import checkmarkImage from '../../../assets/images/check-w.inline.svg'; +import questionmarkImage from '../../../assets/images/questionmark.inline.svg'; import { getColorFromRange } from '../../../utils/colors'; +import Wallet from '../../../domains/Wallet'; -import type { StakePool, StakePoolsListType } from '../../../api/staking/types'; +import StakePool from '../../../domains/StakePool'; const messages = defineMessages({ title: { id: 'staking.delegationSetup.chooseStakePool.step.dialog.title', - defaultMessage: '!!!Delegation Setup', + defaultMessage: '!!!Delegate wallet', description: - 'Title "Delegation Setup" on the delegation setup "choose stake pool" dialog.', + 'Title "Delegate wallet" on the delegation setup "choose stake pool" dialog.', }, description: { id: 'staking.delegationSetup.chooseStakePool.step.dialog.description', - defaultMessage: - '!!!Choose a stake pool to which you would like to delegate.', + defaultMessage: '!!!Currently selected stake pool', description: 'Description on the delegation setup "choose stake pool" dialog.', }, - delegatedPoolsLabel: { + selectStakePoolLabel: { + id: + 'staking.delegationSetup.chooseStakePool.step.dialog.selectStakePoolLabel', + defaultMessage: + '!!!Select a stake pool to delegate to for {selectedWalletName} wallet.', + description: + 'Select / Selected pool section label on the delegation setup "choose stake pool" dialog.', + }, + selectedStakePoolLabel: { + id: + 'staking.delegationSetup.chooseStakePool.step.dialog.selectedStakePoolLabel', + defaultMessage: + '!!!You have selected [{selectedPoolTicker}] stake pool to delegate to for {selectedWalletName} wallet.', + description: + '"Selected Pools" Selected pool label on the delegation setup "choose stake pool" dialog.', + }, + delegatedStakePoolLabel: { id: - 'staking.delegationSetup.chooseStakePool.step.dialog.delegatedPoolsLabel', - defaultMessage: '!!!Stake pools you are already delegating to:', + 'staking.delegationSetup.chooseStakePool.step.dialog.delegatedStakePoolLabel', + defaultMessage: + '!!!You are already delegating {selectedWalletName} wallet to [{selectedPoolTicker}] stake pool. If you wish to re-delegate your stake, please select a different pool.', + description: + '"You are already delegating to stake pool" label on the delegation setup "choose stake pool" dialog.', + }, + delegatedStakePoolNextLabel: { + id: + 'staking.delegationSetup.chooseStakePool.step.dialog.delegatedStakePoolNextLabel', + defaultMessage: + '!!!You are already pending delegation {selectedWalletName} wallet to [{selectedPoolTicker}] stake pool. If you wish to re-delegate your stake, please select a different pool.', description: - '"Delegated Pools" section label on the delegation setup "choose stake pool" dialog.', + '"You are already delegating to stake pool" label on the delegation setup "choose stake pool" dialog.', + }, + recentPoolsLabel: { + id: 'staking.delegationSetup.chooseStakePool.step.dialog.recentPoolsLabel', + defaultMessage: '!!!Pick one of your recent stake pool choices:', + description: + 'Recent "Pool" choice section label on the delegation setup "choose stake pool" dialog.', }, searchInputLabel: { id: 'staking.delegationSetup.chooseStakePool.step.dialog.searchInput.label', - defaultMessage: '!!!Or search for a stake pool:', + defaultMessage: + '!!!Or select a stake pool from the list of all available stake pools:', description: 'Search "Pools" input label on the delegation setup "choose stake pool" dialog.', }, @@ -68,20 +106,15 @@ const messages = defineMessages({ description: 'Step indicator labe on the delegation setup "choose wallet" step dialog.', }, - selectPoolPlaceholder: { - id: - 'staking.delegationSetup.chooseStakePool.step.dialog.selectPoolPlaceholder', - defaultMessage: '!!!POOL', - description: - 'Selected pool box placeholder on the delegation setup "choose wallet" step dialog.', - }, }); type Props = { stepsList: Array, - stakePoolsDelegatingList: Array, + recentStakePools: Array, stakePoolsList: Array, + selectedWallet: ?Wallet, onOpenExternalLink: Function, + getPledgeAddressUrl: Function, currentTheme: string, selectedPool: ?StakePool, onClose: Function, @@ -120,10 +153,6 @@ export default class DelegationStepsChooseStakePoolDialog extends Component< this.setState({ selectedList }); }; - handleDeselectStakePool = () => { - this.setState({ selectedPoolId: null }); - }; - onAcceptPool = () => { const { selectedPoolId } = this.state; this.props.onSelectPool(selectedPoolId); @@ -133,14 +162,43 @@ export default class DelegationStepsChooseStakePoolDialog extends Component< const { intl } = this.context; const { stepsList, - stakePoolsDelegatingList, + recentStakePools, stakePoolsList, onOpenExternalLink, + getPledgeAddressUrl, currentTheme, + selectedWallet, onClose, onBack, } = this.props; const { searchValue, selectedList, selectedPoolId } = this.state; + const selectedWalletName = get(selectedWallet, 'name'); + const selectedPool = find( + stakePoolsList, + stakePool => stakePool.id === selectedPoolId + ); + const lastDelegatedStakePoolId = get( + selectedWallet, + 'lastDelegationStakePoolId', + null + ); + const delegatedStakePoolId = get( + selectedWallet, + 'delegatedStakePoolId', + null + ); + const pendingDelegations = get(selectedWallet, 'pendingDelegations', null); + + const hasPendingDelegations = + pendingDelegations && pendingDelegations.length > 0; + let activeStakePoolId = delegatedStakePoolId; + if (hasPendingDelegations) { + activeStakePoolId = lastDelegatedStakePoolId; + } + + const selectedPoolTicker = get(selectedPool, 'ticker'); + const canSubmit = + !activeStakePoolId || activeStakePoolId !== selectedPoolId; const actions = [ { @@ -148,7 +206,7 @@ export default class DelegationStepsChooseStakePoolDialog extends Component< label: intl.formatMessage(messages.continueButtonLabel), onClick: this.onAcceptPool, primary: true, - disabled: !selectedPoolId, + disabled: !selectedPoolId || !canSubmit, }, ]; @@ -158,43 +216,103 @@ export default class DelegationStepsChooseStakePoolDialog extends Component< ]); const contentClassName = classNames([commonStyles.content, styles.content]); - const selectedPoolBlock = stakePoolId => { - const selectedPool = find( - stakePoolsList, - stakePools => stakePools.id === stakePoolId - ); - const blockLabel = get( - selectedPool, - 'slug', - intl.formatMessage(messages.selectPoolPlaceholder) - ); - + const selectedPoolBlock = () => { const selectedPoolBlockClasses = classNames([ - styles.selectedPoolBlock, - selectedPool ? styles.selected : null, + selectedPool + ? styles.selectedPoolBlock + : styles.selectPoolBlockPlaceholder, + selectedPool && !canSubmit ? styles.alreadyDelegated : null, + ]); + + const selectedPoolImageClasses = classNames([ + selectedPool ? styles.checkmarkImage : styles.questionmarkImage, + ]); + + const wrapperClasses = classNames([ + selectedPool ? styles.checkmarkWrapper : styles.questionmarkWrapper, ]); const rankColor = selectedPool - ? getColorFromRange(selectedPool.ranking) + ? getColorFromRange(selectedPool.ranking, stakePoolsList.length) : 'transparent'; return (
-
{blockLabel}
-
- + {selectedPoolTicker && ( +
{selectedPoolTicker}
+ )} +
+
); }; + const selectionPoolLabel = () => { + let label = ''; + // Label when selected wallet already delegating to selected stake pool + if ( + selectedPoolId && + activeStakePoolId === delegatedStakePoolId && + delegatedStakePoolId === selectedPoolId + ) { + label = ( + + ); + } else if ( + selectedPoolId && + activeStakePoolId === lastDelegatedStakePoolId && + lastDelegatedStakePoolId === selectedPoolId + ) { + label = ( + + ); + } else if (selectedPoolId) { + // Stake pool selected and selected wallet are not delegated to it + label = ( + + ); + } else { + // Stake pool not selected. + label = ( + + ); + } + return label; + }; + const stepsIndicatorLabel = ( ); - const filteredStakePoolsList: StakePoolsListType = getFilteredStakePoolsList( + const filteredStakePoolsList: Array = getFilteredStakePoolsList( stakePoolsList, searchValue ); @@ -240,27 +358,37 @@ export default class DelegationStepsChooseStakePoolDialog extends Component<

{intl.formatMessage(messages.description)}

-
- {selectedPoolBlock(selectedPoolId)} -
-

- {intl.formatMessage(messages.delegatedPoolsLabel)} +

+ {selectedPoolBlock()} + +

+ {selectionPoolLabel()} +

+
+ +
+ {recentStakePools.length > 0 && ( +

+

- -
+ )} +
@@ -271,6 +399,7 @@ export default class DelegationStepsChooseStakePoolDialog extends Component< onSearch={this.handleSearch} onClearSearch={this.handleClearSearch} scrollableElementClassName="Dialog_content" + disabledStakePoolId={activeStakePoolId} />
@@ -279,12 +408,15 @@ export default class DelegationStepsChooseStakePoolDialog extends Component< listName="selectedIndexList" stakePoolsList={filteredStakePoolsList} onOpenExternalLink={onOpenExternalLink} + getPledgeAddressUrl={getPledgeAddressUrl} currentTheme={currentTheme} isListActive={selectedList === 'selectedIndexList'} setListActive={this.handleSetListActive} onSelect={this.handleSelect} selectedPoolId={selectedPoolId} containerClassName="Dialog_content" + numberOfStakePools={stakePoolsList.length} + disabledStakePoolId={activeStakePoolId} showSelected highlightOnHover /> diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseStakePoolDialog.scss b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseStakePoolDialog.scss index f3a56a1914..bdfdf562f2 100644 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseStakePoolDialog.scss +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseStakePoolDialog.scss @@ -1,6 +1,6 @@ .delegationStepsChooseStakePoolDialogWrapper { :global { - .StakePoolThumbnail_component { + .StakePoolThumbnail_content { background-color: 1px solid var(--theme-delegation-steps-choose-stake-pool-thumb-background-color); border: 1px solid @@ -17,43 +17,43 @@ } .stakePoolsListWrapper { - margin-top: 20px; + margin-top: 14px; } .content { color: var(--theme-delegation-steps-choose-stake-pool-title-color); .description { - margin: 0 auto 20px; + font-family: var(--font-medium); + line-height: 1.38; + margin: 0 auto 10px; } - .delegatedStakePoolsWrapper { + .selectStakePoolWrapper { + align-items: center; display: flex; - .selectedPoolBlock { - background-color: transparent; - border: 2px solid - var(--theme-delegation-steps-choose-stake-pool-thumb-border-color); + .selectedPoolBlock, + .selectPoolBlockPlaceholder { border-radius: 5.8px; - border-style: dashed; - height: 103px; - width: 116px; + height: 71px; + width: 80px; - .label { - color: var(--theme-delegation-steps-choose-stake-pool-slug-color); - font-family: var(--font-semibold); - font-size: 20.3px; - font-weight: 600; - margin-top: 12px; - text-align: center; + &.alreadyDelegated { + opacity: 0.3; } + } - .checkmarkWrapper { - margin-top: 14px; + .selectPoolBlockPlaceholder { + border: 2px solid + var(--theme-delegation-steps-choose-stake-pool-thumb-border-color); + border-style: dashed; + .questionmarkWrapper { + margin-top: 24.5px; text-align: center; - .checkmarkImage { + .questionmarkImage { svg { - width: 22.5px; + height: 16px; path { stroke: var( --theme-delegation-steps-choose-stake-pool-checkmark-icon-color @@ -62,21 +62,30 @@ } } } + } - &.selected { - background-color: var( - --theme-staking-stake-pool-selected-background-color + .selectedPoolBlock { + border: 1px solid + var(--theme-delegation-steps-choose-stake-pool-thumb-border-color); + .ticker { + color: var( + --theme-delegation-steps-choose-stake-pool-selected-ticker-color ); - border: 2px solid - var(--theme-delegation-steps-choose-stake-pool-thumb-border-color); + font-family: var(--font-semibold); + font-size: 14px; + letter-spacing: -0.5px; + line-height: 1.57; + margin-top: 8px; + text-align: center; + } - .label { - color: var( - --theme-delegation-steps-choose-stake-pool-selected-slug-color - ); - } + .checkmarkWrapper { + margin-top: 10px; + text-align: center; .checkmarkImage { svg { + vertical-align: top; + width: 15.5px; path { stroke: var( --theme-delegation-steps-choose-stake-pool-selected-checkmark-icon-color @@ -87,28 +96,29 @@ } } - .delegatedStakePoolsList { + .selectStakePoolLabel { + color: var( + --theme-delegation-steps-choose-stake-pool-delegated-pools-label-color + ); flex: 1; - margin-left: 20px; - - .stakePoolsDelegatingListLabel { - color: var( - --theme-delegation-steps-choose-stake-pool-delegated-pools-label-color - ); - font-family: var(--font-medium); - font-size: 16px; - font-weight: 500; - line-height: 1.38; - margin-bottom: 10px; - } - - :global { - .StakePoolsList_component { - grid-gap: 20px; - justify-content: left; + font-family: var(--font-light); + font-size: 16px; + line-height: 1.38; + margin-left: 19px; + span { + span { + font-family: var(--font-medium); } } } } + + .recentStakePoolsWrapper { + margin-top: 20px; + .recentStakePoolsLabel { + font-family: var(--font-medium); + margin-bottom: 10px; + } + } } } diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseWalletDialog.js b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseWalletDialog.js index 2b3f904a6d..26dcdd7a26 100644 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseWalletDialog.js +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsChooseWalletDialog.js @@ -6,10 +6,7 @@ import { FormattedHTMLMessage, FormattedMessage, } from 'react-intl'; -import { get } from 'lodash'; import classNames from 'classnames'; -import { Select } from 'react-polymorph/lib/components/Select'; -import { SelectSkin } from 'react-polymorph/lib/skins/simple/SelectSkin'; import { Stepper } from 'react-polymorph/lib/components/Stepper'; import { StepperSkin } from 'react-polymorph/lib/skins/simple/StepperSkin'; import commonStyles from './DelegationSteps.scss'; @@ -17,18 +14,20 @@ import styles from './DelegationStepsChooseWalletDialog.scss'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import DialogBackButton from '../../widgets/DialogBackButton'; import Dialog from '../../widgets/Dialog'; +import WalletsDropdown from '../../widgets/forms/WalletsDropdown'; +import Wallet from '../../../domains/Wallet'; const messages = defineMessages({ title: { id: 'staking.delegationSetup.chooseWallet.step.dialog.title', - defaultMessage: '!!!Delegation Setup', + defaultMessage: '!!!Delegate wallet', description: - 'Title "Delegation Setup" on the delegation setup "choose wallet" step dialog.', + 'Title "Delegate wallet" on the delegation setup "choose wallet" step dialog.', }, description: { id: 'staking.delegationSetup.chooseWallet.step.dialog.description', defaultMessage: - '!!!Choose a wallet with funds you would like to delegate to a stake pool. Selected wallet needs to have a minimum of {minDelegationFunds} ada.', + '!!!Choose a wallet with funds that you want to delegate. The selected wallet must contain a minimum amount of {minDelegationFunds} ADA for delegation to be available', description: 'Description on the delegation setup "choose wallet" step dialog.', }, @@ -52,12 +51,20 @@ const messages = defineMessages({ description: 'Step indicator labe on the delegation setup "choose wallet" step dialog.', }, - errorMessage: { - id: 'staking.delegationSetup.chooseWallet.step.dialog.errorMessage', + errorMinDelegationFunds: { + id: + 'staking.delegationSetup.chooseWallet.step.dialog.errorMinDelegationFunds', + defaultMessage: + '!!!This wallet does not contain the minimum amount of {minDelegationFunds} ADA which is required for delegation to be available. Please select a wallet with a minimum amount of {minDelegationFunds} ADA and click continue.', + description: + 'MinDelegationFunds Error Label on the delegation setup "choose wallet" step dialog.', + }, + errorRestoringWallet: { + id: 'staking.delegationSetup.chooseWallet.step.dialog.errorRestoringWallet', defaultMessage: - '!!!This wallet does not have enough ada for delegation setup. Please choose a wallet with a minimum of {minDelegationFunds} ada and click continue.', + '!!!This wallet can’t be used for delegation while it’s being synced.', description: - 'Error Label on the delegation setup "choose wallet" step dialog.', + 'RestoringWallet Error Label on the delegation setup "choose wallet" step dialog.', }, continueButtonLabel: { id: 'staking.delegationSetup.chooseWallet.step.dialog.continueButtonLabel', @@ -67,26 +74,21 @@ const messages = defineMessages({ }, }); -type DelegationWalletData = { - id: string, - label: string, - value: string, - isAcceptableSetupWallet: boolean, - hasPassword: boolean, -}; - type Props = { + numberOfStakePools: number, onClose: Function, onSelectWallet: Function, onBack: Function, - wallets: Array, + wallets: Array, stepsList: Array, minDelegationFunds: number, - selectedWallet: ?Object, + selectedWalletId: ?string, + isWalletAcceptable: Function, + getStakePoolById: Function, }; type State = { - selectedWallet: ?DelegationWalletData, + selectedWalletId: ?string, }; export default class DelegationStepsChooseWalletDialog extends Component< @@ -98,46 +100,38 @@ export default class DelegationStepsChooseWalletDialog extends Component< }; state = { - selectedWallet: this.props.selectedWallet, + selectedWalletId: this.props.selectedWalletId, }; - onWalletChange = (selectedWallet: DelegationWalletData) => { - this.setState({ selectedWallet }); + onWalletChange = (selectedWalletId: string) => { + this.setState({ selectedWalletId }); }; onSelectWallet = () => { - const { selectedWallet } = this.state; - const selectedWalletId = get(selectedWallet, 'id'); + const { selectedWalletId } = this.state; this.props.onSelectWallet(selectedWalletId); }; render() { const { intl } = this.context; - const { selectedWallet } = this.state; + const { selectedWalletId } = this.state; const { wallets, stepsList, minDelegationFunds, onClose, onBack, + isWalletAcceptable, + numberOfStakePools, + getStakePoolById, } = this.props; - const selectedWalletValue = get(selectedWallet, 'value', ''); - const isAcceptableSetupWallet = get( - selectedWallet, - 'isAcceptableSetupWallet', - false - ); - - const actions = [ - { - className: 'continueButton', - label: intl.formatMessage(messages.continueButtonLabel), - onClick: this.onSelectWallet.bind(this), - primary: true, - disabled: !selectedWalletValue || !isAcceptableSetupWallet, - }, - ]; + const selectedWallet: ?Wallet = + wallets.find( + (wallet: Wallet) => wallet && wallet.id === selectedWalletId + ) || null; + const amount = selectedWallet ? selectedWallet.amount : null; + const isAcceptableSetupWallet = amount && isWalletAcceptable(amount); const dialogClassName = classNames([ commonStyles.delegationSteps, @@ -145,11 +139,41 @@ export default class DelegationStepsChooseWalletDialog extends Component< ]); const contentClassName = classNames([commonStyles.content, styles.content]); + const errorMinDelegationFunds = selectedWalletId && + !isAcceptableSetupWallet && ( +

+ +

+ ); + + const errorRestoringWallet = selectedWallet && + selectedWallet.isRestoring && ( +

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

+ ); + const walletSelectClasses = classNames([ styles.walletSelect, - selectedWallet && !isAcceptableSetupWallet ? styles.error : null, + errorMinDelegationFunds || errorRestoringWallet ? styles.error : null, ]); + const actions = [ + { + className: 'continueButton', + label: intl.formatMessage(messages.continueButtonLabel), + onClick: this.onSelectWallet.bind(this), + primary: true, + disabled: + !selectedWalletId || + !!errorMinDelegationFunds || + !!errorRestoringWallet, + }, + ]; + const stepsIndicatorLabel = (

- - )} +
+ + {error ? ( +

{intl.formatMessage(error)}

+ ) : null} ); } diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.scss b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.scss index 05469a4961..4b23196beb 100644 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.scss +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsConfirmationDialog.scss @@ -1,8 +1,17 @@ +@import '../../../themes/mixins/loading-spinner'; +@import '../../../themes/mixins/error-message'; + .delegationStepsConfirmationDialogWrapper { .content { .description { color: var(--theme-delegation-steps-confirmation-description-color); + font-family: var(--font-light); margin-bottom: 20px; + span { + span { + font-family: var(--font-medium); + } + } } .feesWrapper { @@ -10,18 +19,30 @@ font-size: 16px; font-weight: 500; line-height: 1.38; - margin-bottom: 0; + margin-bottom: 20px; .feesLabel { color: var(--theme-delegation-steps-confirmation-fees-label-color); margin-bottom: 6px; } .feesAmount { color: var(--theme-delegation-steps-confirmation-fees-amount-color); + font-family: var(--font-medium); user-select: text; span { - opacity: 0.5; + font-family: var(--font-light); } } } } + + .error { + @include error-message; + margin-top: 27px; + text-align: center; + } +} + +.submitButtonSpinning { + box-shadow: none !important; + @include loading-spinner('../../../assets/images/spinner-light.svg'); } diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsIntroDialog.js b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsIntroDialog.js index d588c229ed..47f2edc49b 100644 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsIntroDialog.js +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsIntroDialog.js @@ -2,30 +2,30 @@ import React, { Component } from 'react'; import { defineMessages, intlShape } from 'react-intl'; import classNames from 'classnames'; -import SVGInline from 'react-svg-inline'; +// import SVGInline from 'react-svg-inline'; import commonStyles from './DelegationSteps.scss'; import styles from './DelegationStepsIntroDialog.scss'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import Dialog from '../../widgets/Dialog'; -import externalLinkIcon from '../../../assets/images/link-ic.inline.svg'; +// import externalLinkIcon from '../../../assets/images/link-ic.inline.svg'; type Props = { onClose: Function, onContinue: Function, - onLearnMoreClick: Function, + // onLearnMoreClick: Function, }; const messages = defineMessages({ title: { id: 'staking.delegationSetup.intro.step.dialog.title', - defaultMessage: '!!!Delegation Setup', + defaultMessage: '!!!Delegate wallet', description: 'Title "Delegation Setup" on the delegation setup "intro" dialog.', }, description: { id: 'staking.delegationSetup.intro.step.dialog.description', defaultMessage: - '!!!Follow next sequence of screens to configure delegation for your wallet. During this process, you will need to deposit and pay transaction fees.', + '!!!Follow these steps to configure delegation preferences for your wallet. Please be aware that the last step of delegation confirmation will incur transaction fees.', description: 'Description on the delegation setup "intro" dialog.', }, learnMoreButtonLabel: { @@ -34,43 +34,24 @@ const messages = defineMessages({ description: '"Learn more" button label on the delegation setup "intro" dialog.', }, - stepsExplanationLabel: { - id: 'staking.delegationSetup.intro.step.dialog.stepsExplanationLabel', - defaultMessage: '!!!You will need to complete the following steps:', - description: - 'Steps explanation label on the delegation setup "intro" dialog.', - }, stepsExplanationLabel1: { id: 'staking.delegationSetup.intro.step.dialog.stepsExplanation.step1', - defaultMessage: '!!!Choose a wallet', + defaultMessage: '!!!Wallet selection', description: 'Steps explanation list item 1 label on the delegation setup "intro" dialog.', }, stepsExplanationLabel2: { id: 'staking.delegationSetup.intro.step.dialog.stepsExplanation.step2', - defaultMessage: '!!!Choose a stake pool', + defaultMessage: '!!!Stake pool selection', description: 'Steps explanation list item 2 label on the delegation setup "intro" dialog.', }, stepsExplanationLabel3: { id: 'staking.delegationSetup.intro.step.dialog.stepsExplanation.step3', - defaultMessage: '!!!Confirm delegation', + defaultMessage: '!!!Delegation confirmation', description: 'Steps explanation list item 3 label on the delegation setup "intro" dialog.', }, - stepsExplanationLabel4: { - id: 'staking.delegationSetup.intro.step.dialog.stepsExplanation.step4', - defaultMessage: '!!!Move all of the ada to a new address', - description: - 'Steps explanation list item 4 label on the delegation setup "intro" dialog.', - }, - stepsExplanationOptionalLabel: { - id: - 'staking.delegationSetup.intro.step.dialog.stepsExplanation.optionalLabel', - defaultMessage: '!!!(optional)', - description: - 'Steps explanation list item 4 "Optional" label on the delegation setup "intro" dialog.', - }, cancelButtonLabel: { id: 'staking.delegationSetup.intro.step.dialog.cancelButtonLabel', defaultMessage: '!!!Cancel', @@ -92,7 +73,11 @@ export default class DelegationStepsIntroDialog extends Component { render() { const { intl } = this.context; - const { onClose, onContinue, onLearnMoreClick } = this.props; + const { + onClose, + onContinue, + // onLearnMoreClick, + } = this.props; const actions = [ { @@ -127,14 +112,13 @@ export default class DelegationStepsIntroDialog extends Component {

{intl.formatMessage(messages.description)}

- + {/* + + */}
-

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

  1. 1.{' '} @@ -148,15 +132,6 @@ export default class DelegationStepsIntroDialog extends Component { 3.{' '} {intl.formatMessage(messages.stepsExplanationLabel3)}
  2. -
  3. -

    - 4. - {intl.formatMessage(messages.stepsExplanationLabel4)}{' '} - - {intl.formatMessage(messages.stepsExplanationOptionalLabel)} - -

    -
diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsIntroDialog.scss b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsIntroDialog.scss index 893df5e1b3..f045d00237 100644 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsIntroDialog.scss +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsIntroDialog.scss @@ -14,12 +14,7 @@ border-top: 1px solid var(--theme-delegation-steps-intro-divider-border-color); margin-top: 20px; - padding-top: 20px; - .label { - color: var(--theme-delegation-steps-intro-list-label-color); - font-family: var(--font-medium); - font-weight: 500; - } + padding-top: 9px; ol { margin-left: 14px; } @@ -29,11 +24,6 @@ span { color: var(--theme-delegation-steps-intro-list-numbers-color); margin-right: 14px; - &.optionalLabel { - color: var( - --theme-delegation-steps-intro-list-optional-label-color - ); - } } } } diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsNotAvailableDialog.js b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsNotAvailableDialog.js index 71125467df..1a9b9e77be 100644 --- a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsNotAvailableDialog.js +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsNotAvailableDialog.js @@ -17,19 +17,14 @@ type Props = { const messages = defineMessages({ title: { id: 'staking.delegationSetup.notAvailable.dialog.title', - defaultMessage: '!!!Delegation Setup', + defaultMessage: '!!!Delegation not available', description: 'Title "Delegation Setup" on the delegation setup not available dialog.', }, - subtitle: { - id: 'staking.delegationSetup.notAvailable.dialog.subtitle', - defaultMessage: '!!!Delegation not available', - description: 'Subtitle on the delegation setup not available dialog.', - }, description: { id: 'staking.delegationSetup.notAvailable.dialog.description', defaultMessage: - '!!!A wallet with at least {minDelegationFunds} ada is required for delegation setup. Please restore a wallet with ada, or create a new one and fund it with ada in order to access delegation features.', + '!!!None of your wallets have the minimum amount of {minDelegationFunds} ada required for delegation', description: 'Description on the delegation setup not available dialog.', }, closeButtonLabel: { @@ -75,9 +70,6 @@ export default class DelegationStepsNotAvailableDialog extends Component >
-

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

span { diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsSuccessDialog.js b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsSuccessDialog.js new file mode 100644 index 0000000000..141126fdd9 --- /dev/null +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsSuccessDialog.js @@ -0,0 +1,154 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import classNames from 'classnames'; +import { get } from 'lodash'; +import SVGInline from 'react-svg-inline'; +import commonStyles from './DelegationSteps.scss'; +import styles from './DelegationStepsSuccessDialog.scss'; +import Dialog from '../../widgets/Dialog'; +import DialogCloseButton from '../../widgets/DialogCloseButton'; +import tadaImage from '../../../assets/images/tada-ic.inline.svg'; +import Wallet from '../../../domains/Wallet'; +import StakePool from '../../../domains/StakePool'; +import humanizeDurationByLocale from '../../../utils/humanizeDurationByLocale'; +import { EPOCH_COUNTDOWN_INTERVAL } from '../../../config/epochsConfig'; + +const messages = defineMessages({ + title: { + id: 'staking.delegationSetup.success.step.dialog.title', + defaultMessage: '!!!Wallet Delegated', + description: + 'Title "Wallet Delegated" on the delegation setup "success" step dialog.', + }, + descriptionLine1: { + id: 'staking.delegationSetup.success.step.dialog.description.line1', + defaultMessage: + '!!!The stake from your wallet {delegatedWalletName} is now delegated to the [{delegatedStakePoolTicker}] {delegatedStakePoolName} stake pool.', + description: + 'Description "line 1" on the delegation setup "success" step dialog.', + }, + descriptionLine2: { + id: 'staking.delegationSetup.success.step.dialog.description.line2', + defaultMessage: + '!!!Your new delegation preferences are now posted on the blockchain and will take effect after both the current and next Cardano epochs have completed in {timeUntilNextEpochStart}. During this time, your previous delegation preferences are still active.', + description: + 'Description "line 2" on the delegation setup "success" step dialog.', + }, + closeButtonLabel: { + id: 'staking.delegationSetup.success.step.dialog.closeButtonLabel', + defaultMessage: '!!!Close', + description: + 'Label for Close button on the delegation setup "success" step dialog.', + }, +}); + +type Props = { + delegatedWallet: ?Wallet, + delegatedStakePool: ?StakePool, + futureEpochStartTime: string, + onClose: Function, + currentLocale: string, +}; +type State = { timeUntilNextEpochStart: number }; + +@observer +export default class DelegationStepsSuccessDialog extends Component< + Props, + State +> { + intervalHandler: ?IntervalID = null; + state = { timeUntilNextEpochStart: 0 }; + + static contextTypes = { + intl: intlShape.isRequired, + }; + + componentDidMount() { + this.updateTimeUntilNextEpochStart(); + this.intervalHandler = setInterval( + () => this.updateTimeUntilNextEpochStart(), + EPOCH_COUNTDOWN_INTERVAL + ); + } + + updateTimeUntilNextEpochStart = () => { + const { futureEpochStartTime } = this.props; + const timeUntilNextEpochStart = Math.max( + 0, + new Date(futureEpochStartTime).getTime() - new Date().getTime() + ); + this.setState({ timeUntilNextEpochStart }); + }; + + componentWillUnmount() { + if (this.intervalHandler) { + clearInterval(this.intervalHandler); + } + } + render() { + const { intl } = this.context; + const { + delegatedWallet, + delegatedStakePool, + currentLocale, + onClose, + } = this.props; + + const actions = [ + { + className: 'closeButton', + label: intl.formatMessage(messages.closeButtonLabel), + onClick: onClose, + primary: true, + }, + ]; + + const dialogClassName = classNames([ + commonStyles.delegationSteps, + styles.delegationStepsSuccessDialogWrapper, + ]); + const contentClasses = classNames([commonStyles.content, styles.content]); + + const delegatedWalletName = get(delegatedWallet, 'name'); + const delegatedStakePoolName = get(delegatedStakePool, 'name'); + const delegatedStakePoolTicker = get(delegatedStakePool, 'ticker'); + + const timeUntilNextEpochStart = humanizeDurationByLocale( + this.state.timeUntilNextEpochStart, + currentLocale + ); + + return ( +

} + > +
+ +
+ +
+
+ +
+
+
+ ); + } +} diff --git a/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsSuccessDialog.scss b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsSuccessDialog.scss new file mode 100644 index 0000000000..ed52d97352 --- /dev/null +++ b/source/renderer/app/components/staking/delegation-setup-wizard/DelegationStepsSuccessDialog.scss @@ -0,0 +1,49 @@ +.delegationStepsSuccessDialogWrapper { + .content { + margin-top: 28px; + .tadaImage { + display: block; + margin-bottom: 23px; + text-align: center; + svg { + height: 75px; + stroke: var(--theme-delegation-steps-success-tada-color); + path { + stroke: var(--theme-delegation-steps-success-tada-color); + } + g { + ellipse { + &:nth-child(7), + &:nth-child(8), + &:nth-child(9) { + stroke: var(--theme-delegation-steps-success-tada-color); + } + + &:nth-child(11), + &:nth-child(12), + &:nth-child(13), + &:nth-child(14), + &:nth-child(15), + &:nth-child(16) { + fill: var(--theme-delegation-steps-success-tada-color); + } + } + } + } + } + .description1, + .description2 { + color: var(--theme-delegation-steps-success-description-color); + font-family: var(--font-light); + font-size: 16px; + line-height: 1.38; + span > span { + font-family: var(--font-medium); + } + } + + .description2 { + margin-top: 11px; + } + } +} diff --git a/source/renderer/app/components/staking/epochs/StakingEpochs.js b/source/renderer/app/components/staking/epochs/StakingEpochs.js index b186c9d901..fbc213f70d 100644 --- a/source/renderer/app/components/staking/epochs/StakingEpochs.js +++ b/source/renderer/app/components/staking/epochs/StakingEpochs.js @@ -115,6 +115,7 @@ export default class StakingEpochs extends Component { selectionRenderer={option => (
{option.label}
)} + optionHeight={50} />
{selectedEpoch === CURRENT_EPOCH && ( diff --git a/source/renderer/app/components/staking/epochs/StakingEpochs.scss b/source/renderer/app/components/staking/epochs/StakingEpochs.scss index 514cd40e24..07f0bc515f 100644 --- a/source/renderer/app/components/staking/epochs/StakingEpochs.scss +++ b/source/renderer/app/components/staking/epochs/StakingEpochs.scss @@ -27,7 +27,7 @@ } .SimpleSelect_selectInput { - &::after { + &:after { bottom: 24.5px; } } diff --git a/source/renderer/app/components/staking/epochs/StakingEpochsCurrentEpochData.js b/source/renderer/app/components/staking/epochs/StakingEpochsCurrentEpochData.js index b913fc9d64..7444b19265 100644 --- a/source/renderer/app/components/staking/epochs/StakingEpochsCurrentEpochData.js +++ b/source/renderer/app/components/staking/epochs/StakingEpochsCurrentEpochData.js @@ -97,7 +97,7 @@ export default class StakingEpochsCurrentEpochData extends Component< const tableBody = ( {map(sortedData, (row, key) => { - const poolSlug = get(row, ['pool', 'slug'], ''); + const poolTicker = get(row, ['pool', 'ticker'], ''); const poolName = get(row, ['pool', 'name'], ''); const slotsElected = get(row, 'slotsElected', [0]); @@ -106,15 +106,15 @@ export default class StakingEpochsCurrentEpochData extends Component<

- [{poolSlug}] + [{poolTicker}] {' '} {poolName}

- {`${ - slotsElected[0] - }%`} + {`${slotsElected[0]}%`} ); diff --git a/source/renderer/app/components/staking/epochs/StakingEpochsPreviousEpochData.js b/source/renderer/app/components/staking/epochs/StakingEpochsPreviousEpochData.js index 24c3023661..b3f32d489b 100644 --- a/source/renderer/app/components/staking/epochs/StakingEpochsPreviousEpochData.js +++ b/source/renderer/app/components/staking/epochs/StakingEpochsPreviousEpochData.js @@ -118,7 +118,7 @@ export default class StakingEpochsPreviousEpochData extends Component< const tableBody = ( {map(sortedData, (row, key) => { - const poolSlug = get(row, ['pool', 'slug'], ''); + const poolTicker = get(row, ['pool', 'ticker'], ''); const poolName = get(row, ['pool', 'name'], ''); const slotsElected = get(row, 'slotsElected', [0, 0]); const performance = get(row, 'performance', [0, 0, 0]); @@ -129,7 +129,7 @@ export default class StakingEpochsPreviousEpochData extends Component<

- [{poolSlug}] + [{poolTicker}] {' '} {poolName}

@@ -139,17 +139,17 @@ export default class StakingEpochsPreviousEpochData extends Component< {` ${intl.formatMessage( messages.tableBodySlots )} - `} - {`${ - slotsElected[1] - }%`} + {`${slotsElected[1]}%`} {`${performance[0]} ${intl.formatMessage( messages.tableBodyOf )} ${performance[1]} - `} - {`${ - performance[2] - }%`} + {`${performance[2]}%`} {sharedRewards[0]} diff --git a/source/renderer/app/components/staking/epochs/helpers.js b/source/renderer/app/components/staking/epochs/helpers.js index cc3442fece..b12a077ee7 100644 --- a/source/renderer/app/components/staking/epochs/helpers.js +++ b/source/renderer/app/components/staking/epochs/helpers.js @@ -67,15 +67,6 @@ export const humanizeDurationToShort = (currentLocale, dateTime) => { case 'ja-JP': humanizedDurationLanguage = 'ja'; break; - case 'zh-CN': - humanizedDurationLanguage = 'zh_CN'; - break; - case 'ko-KR': - humanizedDurationLanguage = 'ko'; - break; - case 'de-DE': - humanizedDurationLanguage = 'de'; - break; default: humanizedDurationLanguage = 'en'; } diff --git a/source/renderer/app/components/staking/info/StakingInfo.js b/source/renderer/app/components/staking/info/StakingInfo.js index 1292fc72c3..098ce20b31 100644 --- a/source/renderer/app/components/staking/info/StakingInfo.js +++ b/source/renderer/app/components/staking/info/StakingInfo.js @@ -2,10 +2,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import { defineMessages, intlShape, FormattedMessage } from 'react-intl'; -import { Button } from 'react-polymorph/lib/components/Button'; import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; -import SVGInline from 'react-svg-inline'; -import externalLinkIcon from '../../../assets/images/link-ic.inline.svg'; +import ButtonLink from '../../widgets/ButtonLink'; import styles from './StakingInfo.scss'; const messages = defineMessages({ @@ -113,19 +111,14 @@ export default class StakingInfo extends Component {
- - . +
*
+
+ + +
); diff --git a/source/renderer/app/components/staking/rewards/StakingRewards.scss b/source/renderer/app/components/staking/rewards/StakingRewards.scss index 4a4d44ca16..bece351d0e 100644 --- a/source/renderer/app/components/staking/rewards/StakingRewards.scss +++ b/source/renderer/app/components/staking/rewards/StakingRewards.scss @@ -35,20 +35,31 @@ .note { color: var(--theme-staking-font-color-lighter); + display: flex; font-family: var(--font-regular); font-size: 14px; line-height: 1.43; padding: 10px 20px; - & > button { - @include link(--theme-staking-link-color-light); - font-size: 14px; - &:hover { - border-bottom: 1px solid var(--theme-staking-link-color); - color: var(--theme-staking-link-color); - svg { - g { - stroke: var(--theme-staking-link-color); - } + + .asterisk { + margin-right: 12px; + } + + .noteContent { + align-items: flex-start; + display: flex; + flex-direction: column; + + p { + margin-bottom: 3px; + } + + .externalLink { + font-size: 14px; + margin-top: 3px; + opacity: 0.7; + &:hover { + opacity: 1; } } } diff --git a/source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js b/source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js new file mode 100644 index 0000000000..e0c9856542 --- /dev/null +++ b/source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.js @@ -0,0 +1,312 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import SVGInline from 'react-svg-inline'; +import { get, map, orderBy } from 'lodash'; +import classNames from 'classnames'; +import { Tooltip } from 'react-polymorph/lib/components/Tooltip'; +import { TooltipSkin } from 'react-polymorph/lib/skins/simple/TooltipSkin'; +import moment from 'moment'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; +import { DECIMAL_PLACES_IN_ADA } from '../../../config/numbersConfig'; +import { StakingPageScrollContext } from '../layouts/StakingWithNavigation'; +import BorderedBox from '../../widgets/BorderedBox'; +import LoadingSpinner from '../../widgets/LoadingSpinner'; +import sortIcon from '../../../assets/images/ascending.inline.svg'; +// import externalLinkIcon from '../../../assets/images/link-ic.inline.svg'; +import downloadIcon from '../../../assets/images/download-ic.inline.svg'; +import type { RewardForIncentivizedTestnet } from '../../../api/staking/types'; +import styles from './StakingRewardsForIncentivizedTestnet.scss'; +import tooltipStyles from './StakingRewardsForIncentivizedTestnetTooltip.scss'; + +const messages = defineMessages({ + title: { + id: 'staking.rewards.title', + defaultMessage: '!!!Earned delegation rewards', + description: + 'Title "Earned delegation rewards" label on the staking rewards page.', + }, + exportButtonLabel: { + id: 'staking.rewards.exportButtonLabel', + defaultMessage: '!!!Export CSV', + description: + 'Label for the "Export CSV" button on the staking rewards page.', + }, + noRewards: { + id: 'staking.rewards.no.rewards', + defaultMessage: '!!!No rewards', + description: '"No rewards" rewards label on staking rewards page.', + }, + tableHeaderWallet: { + id: 'staking.rewards.tableHeader.wallet', + defaultMessage: '!!!Wallet', + description: 'Table header "Wallet" label on staking rewards page', + }, + tableHeaderReward: { + id: 'staking.rewards.tableHeader.reward', + defaultMessage: '!!!Reward', + description: 'Table header "Reward" label on staking rewards page', + }, + tableHeaderDate: { + id: 'staking.rewards.tableHeader.date', + defaultMessage: '!!!Date', + description: 'Table header "Date" label in exported csv file', + }, + learnMoreButtonLabel: { + id: 'staking.rewards.learnMore.ButtonLabel', + defaultMessage: '!!!Learn more', + description: 'Label for "Learn more" button on staking rewards page', + }, + note: { + id: 'staking.rewards.note', + defaultMessage: + '!!!

Rewards earned by delegating your stake are automatically collected into your reward account.

Rewards earned on the Incentivized Testnet are not added to your Rewards wallet balance. They will be paid to you in real ada on the Cardano mainnet after the end of the Incentivized Testnet.

If you are using funds from this wallet to operate a stake pool, the rewards displayed here may include your pledged stake, which will not be counted when reward balances are paid out on the Cardano mainnet.

', + description: 'Rewards description text on staking rewards page', + }, + syncingTooltipLabel: { + id: 'staking.delegationCenter.syncingTooltipLabel', + defaultMessage: '!!!Syncing {syncingProgress}%', + description: 'unknown stake pool label on staking rewards page.', + }, +}); + +type Props = { + rewards: Array, + isLoading: boolean, + isExporting: boolean, + // onLearnMoreClick: Function, + onExportCsv: Function, +}; + +type State = { + rewardsOrder: string, + rewardsSortBy: string, +}; + +@observer +export default class StakingRewardsForIncentivizedTestnet extends Component< + Props, + State +> { + static contextTypes = { + intl: intlShape.isRequired, + }; + + static defaultProps = { + isLoading: false, + isExporting: false, + }; + + constructor() { + super(); + this.state = { + rewardsOrder: 'desc', + rewardsSortBy: 'wallet', + }; + } + + handleExportCsv = ( + availableTableHeaders: Array, + sortedRewards: Array + ) => { + const { onExportCsv } = this.props; + const { intl } = this.context; + const exportedHeader = [ + ...availableTableHeaders.map(header => header.title), + intl.formatMessage(messages.tableHeaderDate), + ]; + const date = moment().format('YYYY-MM-DDTHHmmss.0SSS'); + const exportedBody = sortedRewards.map(reward => { + const rewardWallet = get(reward, 'wallet'); + const rewardAmount = get(reward, 'reward').toFormat( + DECIMAL_PLACES_IN_ADA + ); + + return [rewardWallet, `${rewardAmount} ADA`, date]; + }); + const exportedContent = [exportedHeader, ...exportedBody]; + + onExportCsv(exportedContent); + }; + + render() { + const { + rewards, + isLoading, + isExporting, + // onLearnMoreClick, + } = this.props; + const { rewardsOrder, rewardsSortBy } = this.state; + const { intl } = this.context; + const noRewards = !isLoading && ((rewards && !rewards.length) || !rewards); + const showRewards = rewards && rewards.length > 0 && !isLoading; + const sortedRewards = showRewards + ? orderBy(rewards, rewardsSortBy, rewardsOrder) + : []; + const availableTableHeaders = [ + { + name: 'wallet', + title: intl.formatMessage(messages.tableHeaderWallet), + }, + { + name: 'reward', + title: intl.formatMessage(messages.tableHeaderReward), + }, + ]; + const exportCsvButtonLabel = isExporting ? ( +
+ +
+ ) : ( + <> +
+ {intl.formatMessage(messages.exportButtonLabel)} +
+ + + ); + const exportCsvButtonClasses = ctx => + classNames([ + 'primary', + styles.actionButton, + ctx.scrollTop > 10 ? styles.actionButtonFaded : null, + ]); + + return ( + + {context => ( +
+
+
+ {intl.formatMessage(messages.title)} +
+ {!noRewards && ( +
+ + {noRewards && ( +
+ {intl.formatMessage(messages.noRewards)} +
+ )} + + {sortedRewards.length > 0 && ( + + + + {map(availableTableHeaders, tableHeader => { + const isSorted = tableHeader.name === rewardsSortBy; + const sortIconClasses = classNames([ + styles.sortIcon, + isSorted ? styles.sorted : null, + isSorted && rewardsOrder === 'asc' + ? styles.ascending + : null, + ]); + + return ( + + ); + })} + + + + {map(sortedRewards, (reward, key) => { + const rewardWallet = get(reward, 'wallet'); + const isRestoring = get(reward, 'isRestoring'); + const syncingProgress = get(reward, 'syncingProgress'); + const rewardAmount = get(reward, 'reward').toFormat( + DECIMAL_PLACES_IN_ADA + ); + + return ( + + + + + ); + })} + +
+ this.handleRewardsSort(tableHeader.name) + } + > + {tableHeader.title} + +
{rewardWallet} + {rewardAmount} ADA + {isRestoring && ( +
+ + + +
+ )} +
+ )} + + {isLoading && ( +
+ +
+ )} +
+ +
+
+ + {/* + + */} +
+
+
+ )} +
+ ); + } + + handleRewardsSort = (newSortBy: string) => { + const { rewardsOrder, rewardsSortBy } = this.state; + let newRewardsOrder; + if (rewardsSortBy === newSortBy) { + // on same sort change order + newRewardsOrder = rewardsOrder === 'asc' ? 'desc' : 'asc'; + } else { + // on new sort instance, order by initial value 'descending' + newRewardsOrder = 'desc'; + } + + this.setState({ + rewardsSortBy: newSortBy, + rewardsOrder: newRewardsOrder, + }); + }; +} diff --git a/source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.scss b/source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.scss new file mode 100644 index 0000000000..8b3e4084cf --- /dev/null +++ b/source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnet.scss @@ -0,0 +1,205 @@ +@import '../../../themes/mixins/link'; +@import '../../../themes/mixins/loading-spinner'; + +.component { + padding: 20px; + + .headerWrapper { + align-items: flex-end; + display: flex; + line-height: 1.38; + margin: 0 20px 10px; + + & > .title { + color: var(--theme-staking-font-color-lighter); + flex-grow: 1; + font-family: var(--font-regular); + font-size: 16px; + } + + & > .actionButton { + box-shadow: 0 1.5px 5px 0 var(--theme-staking-export-button-shadow-color); + display: flex; + height: auto; + padding: 7px 12px; + position: fixed; + right: 20px; + top: 144px; + width: auto; + + &:focus { + box-shadow: 0 1.5px 5px 0 + var(--theme-staking-export-button-shadow-color) !important; + } + + &.actionButtonFaded { + opacity: 0.5; + + &:hover { + opacity: 1; + } + } + + .actionLabel { + color: var(--theme-staking-learn-more-button-color); + font-family: var(--font-medium); + font-size: 14px; + letter-spacing: normal; + line-height: 1.36; + margin-right: 12px; + } + + .downloadIcon { + height: 11px; + object-fit: contain; + width: 11px; + + path { + fill: var(--theme-staking-learn-more-button-color); + } + } + } + } + + .noRewardsLabel { + color: var(--theme-staking-font-color-lighter); + flex-grow: 1; + font-family: var(--font-regular); + font-size: 16px; + text-align: center; + } + + .note { + align-items: flex-start; + color: var(--theme-staking-font-color-lighter); + display: flex; + flex-direction: column; + font-family: var(--font-regular); + font-size: 14px; + line-height: 1.43; + padding: 10px 20px; + + p { + margin-bottom: 3px; + } + + button { + @include link(--theme-staking-link-color-light); + font-size: 14px; + margin-top: 3px; + + &:hover { + border-bottom: 1px solid var(--theme-staking-link-color); + color: var(--theme-staking-link-color); + svg { + g { + stroke: var(--theme-staking-link-color); + } + } + } + } + } + + .loadingSpinnerWrapper { + margin: auto; + } + + .exportingSpinnerWrapper { + :global { + .LoadingSpinner_component { + &.LoadingSpinner_small { + height: 19px; + margin: 0; + width: 19px; + } + } + } + } + + table { + border-style: hidden; + user-select: text; + width: 100%; + + thead { + background-color: var(--theme-staking-table-head-background-color); + } + + tr, + td, + th { + border: 1px solid var(--theme-staking-table-border-color); + } + + th { + color: var(--theme-staking-font-color-regular); + cursor: pointer; + font-family: var(--font-semibold); + font-size: 14px; + line-height: 1.14; + padding: 14px 20px; + text-align: left; + + &:hover { + & > .sortIcon { + opacity: 0.5; + visibility: visible; + } + } + + & > .sortIcon { + margin-left: 6px; + visibility: hidden; + & > svg { + height: 7.5px; + width: 7px; + & > path { + fill: var(--theme-staking-font-color-regular); + } + } + &.ascending { + & > svg { + transform: rotate(180deg); + } + } + &.sorted { + opacity: 1; + visibility: visible; + } + } + } + + td { + color: var(--theme-staking-font-color-regular); + font-family: var(--font-light); + font-size: 14px; + line-height: 1.3; + padding: 14px 20px; + position: relative; + text-align: left; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; + width: 25%; + + p { + -webkit-box-orient: vertical; + display: -webkit-box; + -webkit-line-clamp: 1; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + + .stakePoolReference { + font-family: var(--font-medium); + } + } + + .syncingProgress { + position: absolute; + right: 10px; + top: 10px; + } + } + } +} diff --git a/source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnetTooltip.scss b/source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnetTooltip.scss new file mode 100644 index 0000000000..aa32abf8e0 --- /dev/null +++ b/source/renderer/app/components/staking/rewards/StakingRewardsForIncentivizedTestnetTooltip.scss @@ -0,0 +1,5 @@ +.root { + .bubble { + top: -11px; + } +} diff --git a/source/renderer/app/components/staking/stake-pools/StakePoolThumbnail.js b/source/renderer/app/components/staking/stake-pools/StakePoolThumbnail.js index c868e28e4b..7e8ca83d7a 100644 --- a/source/renderer/app/components/staking/stake-pools/StakePoolThumbnail.js +++ b/source/renderer/app/components/staking/stake-pools/StakePoolThumbnail.js @@ -5,10 +5,10 @@ import SVGInline from 'react-svg-inline'; import classnames from 'classnames'; import clockIcon from '../../../assets/images/clock.inline.svg'; import styles from './StakePoolThumbnail.scss'; -import { getColorFromRange } from '../../../utils/colors'; +import { getColorFromRange, getSaturationColor } from '../../../utils/colors'; import StakePoolTooltip from './StakePoolTooltip'; import checkmarkImage from '../../../assets/images/check-w.inline.svg'; -import type { StakePool } from '../../../api/staking/types'; +import StakePool from '../../../domains/StakePool'; import { STAKE_POOL_TOOLTIP_HOVER_WAIT } from '../../../config/timingConfig'; import { getRelativePosition } from '../../../utils/domManipulation'; @@ -19,12 +19,15 @@ type Props = { onClose: Function, onHover?: Function, onOpenExternalLink: Function, + getPledgeAddressUrl: Function, onSelect: Function, showWithSelectButton?: boolean, showSelected?: boolean, stakePool: StakePool, isSelected?: ?Function, containerClassName: string, + numberOfStakePools: number, + disabledStakePoolId: ?string, }; type State = { @@ -93,37 +96,53 @@ export class StakePoolThumbnail extends Component { onClose, onHover, onOpenExternalLink, + getPledgeAddressUrl, showWithSelectButton, showSelected, stakePool, containerClassName, + numberOfStakePools, + disabledStakePoolId, } = this.props; const { top, left } = this.state; - const { ranking, slug, retiring } = stakePool; - const color = getColorFromRange(ranking); + const { ranking, ticker, retiring, id, saturation } = stakePool; + const color = getColorFromRange(ranking, numberOfStakePools); + const isDisabled = disabledStakePoolId === id; const componentClassnames = classnames([ styles.component, + isSelected && showSelected ? styles.isSelected : null, + ]); + + const contentClassnames = classnames([ + styles.content, isHighlighted ? styles.isHighlighted : null, onHover ? styles.isOnHover : null, - isSelected && showSelected ? styles.isSelected : null, + isDisabled ? styles.disabled : null, + ]); + + const saturationClassnames = classnames([ + styles.saturationBar, + styles[getSaturationColor(saturation)], ]); return (
-
{slug}
+
{ticker}
{retiring && (
{
)}
{description}
- + onOpenExternalLink(homepage)} + className={styles.homepage} + label={homepage} + skin={LinkSkin} + /> +
+
+ {intl.formatMessage(messages.saturation)} +
+
+ + + + + {`${parseFloat(saturation.toFixed(2))}%`} + +
{intl.formatMessage(messages.ranking)}
{ranking} + + +
{intl.formatMessage(messages.controlledStake)}
-
+
+ {formattedWalletAmount(controlledStake, true, false)} +
+
{intl.formatMessage(messages.profitMargin)}
+
- {controlledStake}% + {`${parseFloat(profitMargin.toFixed(2))}%`}
-
{intl.formatMessage(messages.profitMargin)}
-
+
{intl.formatMessage(messages.costPerEpoch)}
+
- {profitMargin}% + {`${formattedWalletAmount(cost, true, false)}`}
{intl.formatMessage(messages.performance)}
@@ -456,10 +546,61 @@ export default class StakePoolTooltip extends Component { }), }} > - {performance}% + {parseFloat(performance.toFixed(2))}% + + + + +
{intl.formatMessage(messages.producedBlocks)}
+
+ {shortNumber(producedBlocks)}
+ {/*
{intl.formatMessage(messages.cost)}
+
+ + {formattedWalletAmount(shortNumber(cost))} + +
*/}
+ {/*
{intl.formatMessage(messages.pledge)}
*/} + {/*
+ + {formattedWalletAmount(pledge)} + +
*/} + + onOpenExternalLink(getPledgeAddressUrl(pledgeAddress)) + } + label={intl.formatMessage(messages.pledgeAddressLabel)} + skin={LinkSkin} + />
{onSelect && showWithSelectButton && ( + )} + {getRow( + 'cardanoNodeState', + upperFirst( + cardanoNodeState != null + ? intl.formatMessage( + this.getLocalisationForCardanoNodeState() + ) + : 'unknown' + ) + )} + {getRow('cardanoNodeResponding', isNodeResponding)} + {/* getRow('cardanoNodeSubscribed', isNodeSubscribed) */} + {getRow('cardanoNodeTimeCorrect', isNodeTimeCorrect)} + {getRow('cardanoNodeSyncing', isNodeSyncing)} + {getRow('cardanoNodeInSync', isNodeInSync)} + + ); @@ -976,7 +784,7 @@ export default class DaedalusDiagnostics extends Component { }; checkTime = () => { - this.props.onForceCheckLocalTimeDifference(); + this.props.onForceCheckNetworkClock(); this.restoreDialogCloseOnEscKey(); }; @@ -985,25 +793,4 @@ export default class DaedalusDiagnostics extends Component { this.props.onRestartNode.trigger(); this.restoreDialogCloseOnEscKey(); }; - - getClassName = (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/DaedalusDiagnostics.scss b/source/renderer/app/components/status/DaedalusDiagnostics.scss index 8ae8ec79df..fd29612585 100644 --- a/source/renderer/app/components/status/DaedalusDiagnostics.scss +++ b/source/renderer/app/components/status/DaedalusDiagnostics.scss @@ -1,5 +1,29 @@ @import '../../themes/mixins/link'; +/*====================================== += GENERIC STYLES = +======================================*/ + +:global { + .DaedalusDiagnosticsDialog_overlay, + .DaedalusDiagnosticsDialog_dialog { + &:focus { + outline: none; + } + } + + .DaedalusDiagnostics_component { + .SimpleLink_withIconAfter { + border-color: var(--theme-network-window-white-color); + color: var(--theme-network-window-white-color); + line-height: 1.5; + &:after { + background-color: var(--theme-network-window-white-color); + } + } + } +} + .component { align-items: center; background: var(--theme-network-window-background-color); @@ -16,7 +40,6 @@ @media (max-height: 740px), (max-width: 1000px) { overflow-y: overlay; - padding-bottom: 400px; } } @@ -28,38 +51,48 @@ @media (max-width: 1000px) { flex-direction: column; - padding-bottom: 700px !important; - } - - @media (max-width: 900px) { - padding-bottom: 1200px; } } .table { width: calc(50% - 15px); + @media (max-width: 1000px) { + width: 100%; - tr { + &:first-child .layoutRow { + max-width: inherit; + } + } + + @media (max-height: 800px) { + &:nth-child(2) { + margin-bottom: 70px; + } + } + + .layoutRow { display: flex; justify-content: space-between; + } - &.platformVersion { - span { - div > div { - left: 100px; - text-align: center; - white-space: initial; - } - } + .layoutHeader, + .layoutData { + color: var(--theme-network-window-text-color); + font-family: var(--font-regular); + font-size: 14px; + font-stretch: normal; + font-style: normal; + font-weight: normal; + letter-spacing: normal; + line-height: 2; + } - @media (max-width: 1000px) { - display: flex; - justify-content: space-between; - } - } + .layoutHeader { + opacity: 0.7; + white-space: nowrap; } - tbody > tr:first-child > th { + .sectionTitle { color: var(--theme-network-window-text-color); display: inline-block; font-family: var(--font-medium); @@ -69,184 +102,33 @@ font-weight: 500; letter-spacing: 1px; line-height: normal; + opacity: 1; padding-top: 30px; text-align: left; white-space: nowrap; width: 100%; + span { opacity: 1; } - } - - th, - td { - color: var(--theme-network-window-text-color); - font-family: var(--font-regular); - font-size: 14px; - font-stretch: normal; - font-style: normal; - font-weight: normal; - letter-spacing: normal; - line-height: 2; - .blankScreenFix { - max-width: 100px; + *:not(button) { + opacity: 0.7; } } - th { - opacity: 0.7; - white-space: nowrap; - - &.sectionTitle { - opacity: 1; - - *:not(button) { - opacity: 0.7; - } - } - } - - td { + .layoutData { font-weight: 500; text-align: right; user-select: text; - - &.topPadding { - padding-top: 10px; + &.red { + color: var(--theme-network-window-red-color); } - - &.platform { - max-width: 435px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; - - @media (max-width: 1400px) { - max-width: 300px; - } - - @media (max-width: 1200px) { - max-width: 230px; - } - - @media (max-width: 1000px) { - max-width: 565px; - } - } - - &.stateDirectory { - user-select: none; - } - - .stateDirectoryPath { - cursor: pointer; - display: inline-block; - position: relative; - - .tooltipLabelWrapper { - text-align: center; - } - - p { - display: inline-block; - font-size: 14px; - margin: 0 6px 0 0; - max-width: 400px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; - } - - &.locale-en-US { - p { - @media (max-width: 1490px) { - max-width: 285px; - } - @media (max-width: 1300px) { - max-width: 189px; - } - @media (max-width: 1100px) { - max-width: 135px; - } - @media (max-width: 1000px) { - max-width: 100%; - } - } - } - - &.locale-ja-JP { - p { - @media (max-width: 1585px) { - max-width: 285px; - } - @media (max-width: 1370px) { - max-width: 189px; - } - @media (max-width: 1180px) { - max-width: 135px; - } - @media (max-width: 1070px) { - max-width: 115px; - } - @media (max-width: 1000px) { - max-width: 100%; - } - } - } - - svg { - height: 14px; - opacity: 0.7; - stroke: var(--theme-network-window-white-color); - transition: 0.15s opacity; - width: 11px; - } - - &:hover { - p { - &:after { - border-top: 1px solid var(--theme-network-window-white-color); - bottom: 6px; - content: ''; - left: 0; - position: absolute; - right: 1px; - } - } - svg { - opacity: 1; - } - } - } - - &.localTimeDifferenceItem { - align-items: center; - display: flex; - - button { - padding-top: 0; - } - } - } - - span { - td { - opacity: 1; - text-align: right; + &.green { + color: var(--theme-network-window-green-color); } } - .red { - color: var(--theme-network-window-red-color); - } - - .green { - color: var(--theme-network-window-green-color); - } - button { background: var(--theme-network-window-button-background-color); border-radius: 4px; @@ -258,7 +140,6 @@ line-height: 1.5; margin-right: 10px; padding: 0 8px; - padding-top: 2px; position: relative; text-transform: uppercase; user-select: none; @@ -267,7 +148,7 @@ cursor: default; opacity: 0.5; - &::after { + &:after { display: none; } } @@ -283,72 +164,21 @@ &:active { background: var(--theme-network-window-button-background-color-active); } - - &.statusBtn { - float: right; - margin-right: 0; - margin-top: -3px; - padding-top: 0; - } - - &.realTimeStatusBtn, - &.unknownDiskSpaceBtn { - @include link(--theme-network-window-white-color); - background: transparent !important; - border-bottom: none; - border-radius: initial; - font-size: 14px; - font-weight: initial; - height: initial; - line-height: 1.5; - margin-right: initial; - padding: initial; - position: relative; - text-transform: initial; - width: initial; - - &::after { - border-top: 1px solid var(--theme-network-window-white-color); - bottom: 1px; - content: ''; - left: 0; - position: absolute; - right: 1px; - } - } - - &.stateDirectoryOpenBtn { - margin-right: 16px; - } } hr { border: 0; border-top: 1px solid var(--theme-network-window-border-color); } +} - .error { - color: var(--theme-network-window-red-color); - font-size: 12px; - font-style: italic; - line-height: 1.36; - padding: 2px 0; - text-align: left; - } - - @media (max-width: 1000px) { - width: 100%; - - &:first-child tr { - max-width: inherit; - } - } - - @media (max-height: 800px) { - &:nth-child(2) { - margin-bottom: 70px; - } - } +.error { + color: var(--theme-network-window-red-color); + font-size: 12px; + font-style: italic; + line-height: 1.36; + padding: 2px 0; + text-align: left; } .closeButton { @@ -383,3 +213,150 @@ } } } + +/*======================================= += SPECIFIC STYLES = +=======================================*/ + +.layoutHeader.stateDirectoryPath { + width: 244px; +} + +.layoutData.platformVersion { + max-width: 435px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + + @media (max-width: 1400px) { + max-width: 300px; + } + + @media (max-width: 1200px) { + max-width: 230px; + } + + @media (max-width: 1000px) { + max-width: 565px; + } +} + +.layoutData.stateDirectoryPath { + align-items: center; + display: flex; + user-select: none; + + .stateDirectoryPath { + > span { + align-items: center; + cursor: pointer; + display: flex; + justify-content: space-between; + width: 100%; + } + + &:hover { + .daedalusStateDirectoryPath { + &:after { + border-top: 1px solid var(--theme-network-window-white-color); + bottom: 6px; + content: ''; + left: 0; + position: absolute; + right: 1px; + } + } + svg { + opacity: 1; + } + } + } + + .tooltipLabelWrapper { + text-align: center; + } + + .daedalusStateDirectoryPath { + font-size: 14px; + margin: 0 6px 0 0; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + user-select: none; + white-space: nowrap; + } + + svg { + height: 14px; + opacity: 0.7; + stroke: var(--theme-network-window-white-color); + transition: 0.15s opacity; + width: 11px; + } + + @media (min-width: 1001px) and (max-width: 1595px) { + max-width: calc(100% - 244px); + + .stateDirectoryPath { + width: calc(100% - 71px); + } + } +} + +.layoutData.localTimeDifference { + align-items: center; + display: flex; +} + +.layoutData.lastNetworkBlock, +.layoutData.lastSynchronizedBlock { + span { + opacity: 0.7; + + & + span { + border-left: 1px solid var(--theme-network-window-border-color); + margin-left: 6px; + padding-left: 6px; + } + } +} + +button.cardanoNodeStatusBtn { + float: right; + margin-right: 0; + margin-top: -3px; + padding-top: 0; +} + +button.realTimeStatusBtn { + @include link(--theme-network-window-white-color); + background: transparent !important; + border-bottom: none; + border-radius: initial; + font-size: 14px; + font-weight: initial; + height: initial; + line-height: 1.5; + margin-right: initial; + padding: initial; + position: relative; + text-transform: initial; + width: initial; + + &:after { + border-top: 1px solid var(--theme-network-window-white-color); + bottom: 1px; + content: ''; + left: 0; + position: absolute; + right: 1px; + } +} + +button.stateDirectoryOpenBtn { + margin-left: 18px; + margin-right: 16px; + padding: 0; + width: 36px; +} diff --git a/source/renderer/app/components/wallet/WalletAdd.js b/source/renderer/app/components/wallet/WalletAdd.js index b888d1723c..60a11b79f7 100644 --- a/source/renderer/app/components/wallet/WalletAdd.js +++ b/source/renderer/app/components/wallet/WalletAdd.js @@ -22,11 +22,17 @@ const messages = defineMessages({ defaultMessage: '!!!Create', description: 'Label for the "Create" button on the wallet add dialog.', }, + createDescriptionItn: { + id: 'wallet.add.dialog.create.description.itn', + defaultMessage: '!!!Create a new Rewards wallet', + description: + 'Description for the "Create a new Rewards wallet" button on the wallet add dialog.', + }, createDescription: { id: 'wallet.add.dialog.create.description', defaultMessage: '!!!Create a new wallet', description: - 'Description for the "Create" button on the wallet add dialog.', + 'Description for the "Create a new wallet" button on the wallet add dialog.', }, joinLabel: { id: 'wallet.add.dialog.join.label', @@ -46,7 +52,7 @@ const messages = defineMessages({ restoreWithCertificateDescription: { id: 'wallet.add.dialog.restore.withCertificate.description', defaultMessage: - '!!!Restore using backup-recovery phrase or paper wallet certificate.', + '!!!Restore a wallet or paper wallet using wallet recovery phrase', description: 'Description for the "Restore" button with paper wallet certificate on the wallet add dialog.', }, @@ -63,7 +69,8 @@ const messages = defineMessages({ }, importDescription: { id: 'wallet.add.dialog.import.description', - defaultMessage: '!!!Import wallet from a file', + defaultMessage: + '!!!Import wallets from an earlier version of Daedalus or the Daedalus state directory', description: 'Description for the "Import" button on the wallet add dialog.', }, @@ -83,14 +90,16 @@ const messages = defineMessages({ }, }); +const { isIncentivizedTestnet } = global; + type Props = { onCreate: Function, onRestore: Function, onImportFile: Function, - isRestoreActive: boolean, + isMaxNumberOfWalletsReached: boolean, isMainnet: boolean, isTestnet: boolean, - isMaxNumberOfWalletsReached: boolean, + isProduction: boolean, }; @observer @@ -110,10 +119,10 @@ export default class WalletAdd extends Component { onCreate, onRestore, onImportFile, - isRestoreActive, isMaxNumberOfWalletsReached, isMainnet, isTestnet, + isProduction, } = this.props; const componentClasses = classnames([styles.component, 'WalletAdd']); @@ -121,8 +130,6 @@ export default class WalletAdd extends Component { let activeNotification = null; if (isMaxNumberOfWalletsReached) { activeNotification = 'maxNumberOfWalletsNotificationMessage'; - } else if (isRestoreActive) { - activeNotification = 'restoreNotificationMessage'; } return ( @@ -134,7 +141,11 @@ export default class WalletAdd extends Component { onClick={onCreate} icon={createIcon} label={intl.formatMessage(messages.createLabel)} - description={intl.formatMessage(messages.createDescription)} + description={ + isIncentivizedTestnet + ? intl.formatMessage(messages.createDescriptionItn) + : intl.formatMessage(messages.createDescription) + } isDisabled={isMaxNumberOfWalletsReached} /> { description={intl.formatMessage( messages.restoreWithCertificateDescription )} - isDisabled={isMaxNumberOfWalletsReached || isRestoreActive} + isDisabled={isMaxNumberOfWalletsReached} /> { description={intl.formatMessage(messages.importDescription)} isDisabled={ isMaxNumberOfWalletsReached || - isRestoreActive || - isMainnet || - isTestnet + (isProduction && !isMainnet && !isTestnet) } /> diff --git a/source/renderer/app/components/wallet/WalletBackupDialog.js b/source/renderer/app/components/wallet/WalletBackupDialog.js index 813f80ab65..c45117da49 100644 --- a/source/renderer/app/components/wallet/WalletBackupDialog.js +++ b/source/renderer/app/components/wallet/WalletBackupDialog.js @@ -15,9 +15,10 @@ type Props = { canPhraseBeShown: boolean, isPrivacyNoticeAccepted: boolean, countdownRemaining: number, - isTermDeviceAccepted: boolean, + isTermOfflineAccepted: boolean, canFinishBackup: boolean, isTermRecoveryAccepted: boolean, + isTermRewardsAccepted: boolean, isValid: boolean, isSubmitting: boolean, recoveryPhrase: string, @@ -27,8 +28,9 @@ type Props = { onAcceptPrivacyNotice: Function, onContinue: Function, onStartWalletBackup: Function, - onAcceptTermDevice: Function, + onAcceptTermOffline: Function, onAcceptTermRecovery: Function, + onAcceptTermRewards: Function, onAddWord: Function, onClear: Function, onFinishBackup: Function, @@ -48,14 +50,16 @@ export default class WalletBackupDialog extends Component { onContinue, recoveryPhrase, onStartWalletBackup, - isTermDeviceAccepted, + isTermOfflineAccepted, enteredPhrase, canFinishBackup, isTermRecoveryAccepted, + isTermRewardsAccepted, isValid, isSubmitting, - onAcceptTermDevice, + onAcceptTermOffline, onAcceptTermRecovery, + onAcceptTermRewards, onAddWord, onClear, onFinishBackup, @@ -87,14 +91,16 @@ export default class WalletBackupDialog extends Component { if (currentStep === WALLET_BACKUP_STEPS.RECOVERY_PHRASE_ENTRY) { return ( { state = { isSubmitting: false, - createPassword: true, }; componentDidMount() { @@ -135,7 +141,6 @@ export default class WalletCreateDialog extends Component { value: '', validators: [ ({ field, form }) => { - if (!this.state.createPassword) return [true]; const repeatPasswordField = form.$('repeatPassword'); if (repeatPasswordField.value.length > 0) { repeatPasswordField.validate({ showErrors: true }); @@ -158,7 +163,6 @@ export default class WalletCreateDialog extends Component { value: '', validators: [ ({ field, form }) => { - if (!this.state.createPassword) return [true]; const spendingPassword = form.$('spendingPassword').value; if (spendingPassword.length === 0) return [true]; return [ @@ -184,11 +188,10 @@ export default class WalletCreateDialog extends Component { this.form.submit({ onSuccess: form => { this.setState({ isSubmitting: true }); - const { createPassword } = this.state; const { walletName, spendingPassword } = form.values(); const walletData = { name: walletName, - spendingPassword: createPassword ? spendingPassword : null, + spendingPassword, }; this.props.onSubmit(walletData); }, @@ -201,25 +204,21 @@ export default class WalletCreateDialog extends Component { handleSubmitOnEnter = submitOnEnter.bind(this, this.submit); - handlePasswordSwitchToggle = (value: boolean) => { - this.setState({ createPassword: value }); - }; - render() { const { form } = this; const { intl } = this.context; const { onCancel } = this.props; - const { createPassword, isSubmitting } = this.state; + const { isSubmitting } = this.state; const dialogClasses = classnames([styles.component, 'WalletCreateDialog']); - const spendingPasswordFieldsClasses = classnames([ - styles.spendingPasswordFields, - createPassword ? styles.show : null, - ]); const actions = [ { className: isSubmitting ? styles.isSubmitting : null, - label: this.context.intl.formatMessage(messages.createPersonalWallet), + label: this.context.intl.formatMessage( + isIncentivizedTestnet + ? messages.createPersonalWalletItn + : messages.createPersonalWallet + ), primary: true, onClick: this.submit, }, @@ -232,7 +231,9 @@ export default class WalletCreateDialog extends Component { return ( {}} @@ -249,21 +250,16 @@ export default class WalletCreateDialog extends Component { skin={InputSkin} /> -
-
-
- {intl.formatMessage(messages.passwordSwitchLabel)} -
- +
+
+ {intl.formatMessage(messages.passwordSectionLabel)} +
+ +
+ {intl.formatMessage(messages.passwordSectionDescription)}
-
+
.passwordLabel { - color: var(--rp-switch-label-text-color); - font-family: var(--font-semibold); - font-size: 16px; - line-height: 1.38; - margin-bottom: 10px; - } +.spendingPasswordWrapper { + border-top: 1px solid var(--theme-dialog-border-color); + margin-top: 30px; + padding-top: 20px; + + .passwordSectionLabel { + font-family: var(--font-medium); + font-size: 16px; + line-height: 1.38; + margin-bottom: 14px; + } - :global { - .SimpleSwitch_root { - margin-bottom: 0; - } - } + .passwordSectionDescription { + font-family: var(--font-light); + font-size: 16px; + line-height: 1.38; } .spendingPasswordFields { display: flex; flex-wrap: wrap; justify-content: space-between; - max-height: 0; - opacity: 0; - overflow: hidden; + max-height: 250px; + opacity: 1; + overflow: visible; transition: all 400ms ease; - &.show { - max-height: 250px; - opacity: 1; - overflow: visible; - } - & > div { - margin-top: 30px; + margin-top: 20px; width: 275px; } @@ -49,7 +40,7 @@ color: var(--theme-dialog-text-color); font-family: var(--font-light); line-height: 1.38; - margin-top: 16px; + margin-top: 10px; } } } diff --git a/source/renderer/app/components/wallet/WalletRestoreDialog.js b/source/renderer/app/components/wallet/WalletRestoreDialog.js index 4d1ff24ba8..6192746dc7 100644 --- a/source/renderer/app/components/wallet/WalletRestoreDialog.js +++ b/source/renderer/app/components/wallet/WalletRestoreDialog.js @@ -1,16 +1,14 @@ // @flow -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { join } from 'lodash'; import { observer } from 'mobx-react'; import classnames from 'classnames'; 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, FormattedHTMLMessage } from 'react-intl'; +import RadioSet from '../widgets/RadioSet'; import ReactToolboxMobxForm, { handleFormErrors, } from '../../utils/ReactToolboxMobxForm'; @@ -23,18 +21,19 @@ import { } from '../../utils/validations'; import globalMessages from '../../i18n/global-messages'; import LocalizableError from '../../i18n/LocalizableError'; +import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../config/timingConfig'; +import styles from './WalletRestoreDialog.scss'; +import { submitOnEnter } from '../../utils/form'; +import { + WALLET_RESTORE_TYPES, + RECOVERY_PHRASE_WORD_COUNT_OPTIONS, +} from '../../config/walletsConfig'; import { + LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT, PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT, WALLET_RECOVERY_PHRASE_WORD_COUNT, + YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT, } from '../../config/cryptoConfig'; -import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../config/timingConfig'; -import styles from './WalletRestoreDialog.scss'; -import { submitOnEnter } from '../../utils/form'; - -const RESTORE_TYPES = { - REGULAR: 'regular', - CERTIFICATE: 'certificate', -}; const messages = defineMessages({ title: { @@ -54,6 +53,30 @@ const messages = defineMessages({ description: 'Hint "Name the wallet you are restoring" for the wallet name input on the wallet restore dialog.', }, + recoveryPhraseTypeLabel: { + id: 'wallet.restore.dialog.recovery.phrase.type.options.label', + defaultMessage: '!!!Number of words in the recovery phrase', + description: + 'Label for the recovery phrase type options on the wallet restore dialog.', + }, + recoveryPhraseTypeOptionWord: { + id: 'wallet.restore.dialog.recovery.phrase.type.word', + defaultMessage: '!!! words', + description: + 'Word for the recovery phrase type on the wallet restore dialog.', + }, + recoveryPhraseType15WordOption: { + id: 'wallet.restore.dialog.recovery.phrase.type.15word.option', + defaultMessage: '!!!Rewards wallet', + description: + 'Label for the recovery phrase type 15-word option on the wallet restore dialog.', + }, + recoveryPhraseType12WordOption: { + id: 'wallet.restore.dialog.recovery.phrase.type.12word.option', + defaultMessage: '!!!Balance wallet', + description: + 'Label for the recovery phrase type 12-word option on the wallet restore dialog.', + }, recoveryPhraseInputLabel: { id: 'wallet.restore.dialog.recovery.phrase.input.label', defaultMessage: '!!!Recovery phrase', @@ -66,6 +89,11 @@ const messages = defineMessages({ description: 'Hint "Enter recovery phrase" for the recovery phrase input on the wallet restore dialog.', }, + newLabel: { + id: 'wallet.restore.dialog.recovery.phrase.newLabel', + defaultMessage: '!!!New', + description: 'Label "new" on the wallet restore dialog.', + }, recoveryPhraseNoResults: { id: 'wallet.restore.dialog.recovery.phrase.input.noResults', defaultMessage: '!!!No results', @@ -84,18 +112,16 @@ const messages = defineMessages({ description: 'Error message shown when invalid recovery phrase was entered.', }, - passwordSwitchPlaceholder: { - id: 'wallet.restore.dialog.passwordSwitchPlaceholder', - defaultMessage: - '!!!Keep your private keys safely encrypted by setting the spending password', - description: - 'Text for the "Spending password" switch in the wallet restore dialog.', - }, - passwordSwitchLabel: { - id: 'wallet.restore.dialog.passwordSwitchLabel', + passwordSectionLabel: { + id: 'wallet.restore.dialog.passwordSectionLabel', defaultMessage: '!!!Spending password', - description: - 'Label for the "Spending password" switch in the wallet restore dialog.', + description: 'Password creation label.', + }, + passwordSectionDescription: { + id: 'wallet.restore.dialog.passwordSectionDescription', + defaultMessage: + '!!!Keep your wallet secure by setting the spending password', + description: 'Password creation description.', }, spendingPasswordLabel: { id: 'wallet.restore.dialog.spendingPasswordLabel', @@ -117,28 +143,38 @@ const messages = defineMessages({ }, recoveryPhraseTabTitle: { id: 'wallet.restore.dialog.tab.title.recoveryPhrase', - defaultMessage: '!!!Backup recovery phrase', - description: - 'Tab title "Backup recovery phrase" in the wallet restore dialog.', + defaultMessage: '!!!Daedalus wallet', + description: 'Tab title "Daedalus wallet" in the wallet restore dialog.', }, certificateTabTitle: { id: 'wallet.restore.dialog.tab.title.certificate', - defaultMessage: '!!!Paper wallet certificate', + defaultMessage: '!!!Daedalus paper wallet', description: - 'Tab title "Paper wallet certificate" in the wallet restore dialog.', + 'Tab title "Daedalus paper wallet" in the wallet restore dialog.', + }, + yoroiTabTitle: { + id: 'wallet.restore.dialog.tab.title.yoroi', + defaultMessage: '!!!Yoroi wallet', + description: 'Tab title "Yoroi wallet" in the wallet restore dialog.', }, shieldedRecoveryPhraseInputLabel: { id: 'wallet.restore.dialog.shielded.recovery.phrase.input.label', - defaultMessage: '!!!Paper wallet recovery phrase', + defaultMessage: '!!!27-word paper wallet recovery phrase', description: 'Label for the shielded recovery phrase input on the wallet restore dialog.', }, shieldedRecoveryPhraseInputHint: { id: 'wallet.restore.dialog.shielded.recovery.phrase.input.hint', defaultMessage: - '!!!Enter the recovery phrase from your paper wallet certificate', + '!!!Enter your {numberOfWords}-word paper wallet recovery phrase', + description: + 'Hint "Enter your 27-word paper wallet recovery phrase." for the recovery phrase input on the wallet restore dialog.', + }, + restorePaperWalletButtonLabel: { + id: 'wallet.restore.dialog.paper.wallet.button.label', + defaultMessage: '!!!Restore paper wallet', description: - 'Hint "Enter shielded recovery phrase" for the recovery phrase input on the wallet restore dialog.', + 'Label for the "Restore paper wallet" button on the wallet restore dialog.', }, }); @@ -155,8 +191,7 @@ type Props = { }; type State = { - createPassword: boolean, - activeChoice: string, + walletType: string, }; @observer @@ -170,8 +205,7 @@ export default class WalletRestoreDialog extends Component { }; state = { - activeChoice: RESTORE_TYPES.REGULAR, // regular | certificate - createPassword: true, + walletType: WALLET_RESTORE_TYPES.LEGACY, // regular | certificate | legacy | yoroi }; recoveryPhraseAutocomplete: Autocomplete; @@ -202,11 +236,11 @@ export default class WalletRestoreDialog extends Component { value: [], validators: ({ field }) => { const { intl } = this.context; + const { walletType } = this.state; const enteredWords = field.value; const wordCount = enteredWords.length; - const expectedWordCount = this.isRegular() - ? WALLET_RECOVERY_PHRASE_WORD_COUNT - : PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT; + const expectedWordCount = + RECOVERY_PHRASE_WORD_COUNT_OPTIONS[walletType]; const value = join(enteredWords, ' '); // Regular mnemonics have 12 and paper wallet recovery needs 27 words const isPhraseComplete = wordCount === expectedWordCount; @@ -220,7 +254,9 @@ export default class WalletRestoreDialog extends Component { } return [ // TODO: we should also validate paper wallets mnemonics here! - this.isRegular() ? this.props.mnemonicValidator(value) : true, + !this.isCertificate() + ? this.props.mnemonicValidator(value, expectedWordCount) + : true, this.context.intl.formatMessage(messages.invalidRecoveryPhrase), ]; }, @@ -236,7 +272,6 @@ export default class WalletRestoreDialog extends Component { value: '', validators: [ ({ field, form }) => { - if (!this.state.createPassword) return [true]; const repeatPasswordField = form.$('repeatPassword'); if (repeatPasswordField.value.length > 0) { repeatPasswordField.validate({ showErrors: true }); @@ -259,7 +294,6 @@ export default class WalletRestoreDialog extends Component { value: '', validators: [ ({ field, form }) => { - if (!this.state.createPassword) return [true]; const spendingPassword = form.$('spendingPassword').value; if (spendingPassword.length === 0) return [true]; return [ @@ -281,24 +315,18 @@ export default class WalletRestoreDialog extends Component { } ); - handlePasswordSwitchToggle = (value: boolean) => { - this.setState({ createPassword: value }); - }; - submit = () => { this.form.submit({ onSuccess: form => { - const { createPassword } = this.state; const { onSubmit } = this.props; const { recoveryPhrase, walletName, spendingPassword } = form.values(); - const walletData: Object = { recoveryPhrase: join(recoveryPhrase, ' '), walletName, - spendingPassword: createPassword ? spendingPassword : null, + spendingPassword, }; - walletData.type = this.state.activeChoice; + walletData.type = this.state.walletType; onSubmit(walletData); }, @@ -317,6 +345,13 @@ export default class WalletRestoreDialog extends Component { }); form.reset(); form.showErrors(false); + }; + + resetMnemonics = () => { + const recoveryPhraseField = this.form.$('recoveryPhrase'); + recoveryPhraseField.debouncedValidation.cancel(); + recoveryPhraseField.reset(); + recoveryPhraseField.showErrors(false); // Autocomplete has to be reset manually this.recoveryPhraseAutocomplete.clear(); @@ -325,8 +360,8 @@ export default class WalletRestoreDialog extends Component { render() { const { intl } = this.context; const { form } = this; + const { walletType } = this.state; const { suggestedMnemonics, isSubmitting, error, onCancel } = this.props; - const { createPassword } = this.state; const dialogClasses = classnames([ styles.component, @@ -339,11 +374,6 @@ export default class WalletRestoreDialog extends Component { styles.walletName, ]); - const spendingPasswordFieldsClasses = classnames([ - styles.spendingPasswordFields, - createPassword ? styles.show : null, - ]); - const walletNameField = form.$('walletName'); const recoveryPhraseField = form.$('recoveryPhrase'); const spendingPasswordField = form.$('spendingPassword'); @@ -352,7 +382,11 @@ export default class WalletRestoreDialog extends Component { const actions = [ { className: isSubmitting ? styles.isSubmitting : null, - label: this.context.intl.formatMessage(messages.importButtonLabel), + label: this.isCertificate() + ? this.context.intl.formatMessage( + messages.restorePaperWalletButtonLabel + ) + : this.context.intl.formatMessage(messages.importButtonLabel), primary: true, disabled: isSubmitting, onClick: this.submit, @@ -361,7 +395,7 @@ export default class WalletRestoreDialog extends Component { const regularTabClasses = classnames([ 'regularTab', - this.isRegular() ? styles.activeButton : '', + this.isRegular() || this.isLegacy() ? styles.activeButton : '', ]); const certificateTabClasses = classnames([ @@ -369,6 +403,11 @@ export default class WalletRestoreDialog extends Component { this.isCertificate() ? styles.activeButton : '', ]); + const yoroiTabClasses = classnames([ + 'yoroiTab', + this.isYoroi() ? styles.activeButton : '', + ]); + return ( {
+
{ skin={InputSkin} /> + {(this.isRegular() || this.isLegacy()) && ( + + {LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT} + {intl.formatMessage( + messages.recoveryPhraseTypeOptionWord + )}{' '} + + ( + {intl.formatMessage( + messages.recoveryPhraseType12WordOption + )} + ) + + + ), + selected: this.isLegacy(), + onChange: () => + this.onSelectWalletType(WALLET_RESTORE_TYPES.LEGACY), + }, + { + key: WALLET_RESTORE_TYPES.REGULAR, + label: ( + + {WALLET_RECOVERY_PHRASE_WORD_COUNT} + {intl.formatMessage( + messages.recoveryPhraseTypeOptionWord + )}{' '} + + ( + {intl.formatMessage( + messages.recoveryPhraseType15WordOption + )} + ) + + + {intl.formatMessage(messages.newLabel)} + + + ), + selected: !this.isLegacy(), + onChange: () => + this.onSelectWalletType(WALLET_RESTORE_TYPES.REGULAR), + }, + ]} + /> + )} + + {this.isYoroi() && ( + + {YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT} + {intl.formatMessage( + messages.recoveryPhraseTypeOptionWord + )}{' '} + + ( + {intl.formatMessage( + messages.recoveryPhraseType12WordOption + )} + ) + + + ), + selected: this.isYoroiLegacy(), + onChange: () => + this.onSelectWalletType(WALLET_RESTORE_TYPES.YOROI_LEGACY), + }, + { + key: WALLET_RESTORE_TYPES.YOROI_REGULAR, + label: ( + + {YOROI_WALLET_RECOVERY_PHRASE_WORD_COUNT} + {intl.formatMessage( + messages.recoveryPhraseTypeOptionWord + )}{' '} + + ( + {intl.formatMessage( + messages.recoveryPhraseType15WordOption + )} + ) + + + {intl.formatMessage(messages.newLabel)} + + + ), + selected: this.isYoroiRegular(), + onChange: () => + this.onSelectWalletType(WALLET_RESTORE_TYPES.YOROI_REGULAR), + }, + ]} + /> + )} + { this.recoveryPhraseAutocomplete = autocomplete; }} label={ - this.isRegular() + !this.isCertificate() ? intl.formatMessage(messages.recoveryPhraseInputLabel) : intl.formatMessage(messages.shieldedRecoveryPhraseInputLabel) } placeholder={ - this.isRegular() + !this.isCertificate() ? intl.formatMessage(messages.recoveryPhraseInputHint) - : intl.formatMessage(messages.shieldedRecoveryPhraseInputHint) + : intl.formatMessage(messages.shieldedRecoveryPhraseInputHint, { + numberOfWords: PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT, + }) } options={suggestedMnemonics} - maxSelections={ - this.isCertificate() - ? PAPER_WALLET_RECOVERY_PHRASE_WORD_COUNT - : WALLET_RECOVERY_PHRASE_WORD_COUNT - } + maxSelections={RECOVERY_PHRASE_WORD_COUNT_OPTIONS[walletType]} error={recoveryPhraseField.error} maxVisibleOptions={5} noResultsMessage={intl.formatMessage( messages.recoveryPhraseNoResults )} skin={AutocompleteSkin} + optionHeight={50} /> -
-
-
- {intl.formatMessage(messages.passwordSwitchLabel)} -
- +
+
+ {intl.formatMessage(messages.passwordSectionLabel)}
-
+
+ {intl.formatMessage(messages.passwordSectionDescription)} +
+ +
{ } isRegular() { - return this.state.activeChoice === RESTORE_TYPES.REGULAR; + return this.state.walletType === WALLET_RESTORE_TYPES.REGULAR; } isCertificate() { - return this.state.activeChoice === RESTORE_TYPES.CERTIFICATE; + return this.state.walletType === WALLET_RESTORE_TYPES.CERTIFICATE; } - onSelectChoice = (choice: string) => { - const { isSubmitting, onChoiceChange } = this.props; - if (!isSubmitting) { - this.setState({ - activeChoice: choice, - createPassword: true, - }); - this.resetForm(); - if (onChoiceChange) onChoiceChange(); - } + isLegacy() { + return this.state.walletType === WALLET_RESTORE_TYPES.LEGACY; + } + + isYoroiLegacy() { + return this.state.walletType === WALLET_RESTORE_TYPES.YOROI_LEGACY; + } + + isYoroiRegular() { + return this.state.walletType === WALLET_RESTORE_TYPES.YOROI_REGULAR; + } + + isYoroi() { + return this.isYoroiLegacy() || this.isYoroiRegular(); + } + + onSelectWalletType = (walletType: string, shouldResetForm?: boolean) => { + const { onChoiceChange, isSubmitting } = this.props; + if (isSubmitting) return; + this.setState({ walletType }); + if (shouldResetForm) this.resetForm(); + this.resetMnemonics(); + if (onChoiceChange) onChoiceChange(); }; } diff --git a/source/renderer/app/components/wallet/WalletRestoreDialog.scss b/source/renderer/app/components/wallet/WalletRestoreDialog.scss index 9cf663a506..6aba0655a8 100644 --- a/source/renderer/app/components/wallet/WalletRestoreDialog.scss +++ b/source/renderer/app/components/wallet/WalletRestoreDialog.scss @@ -15,42 +15,50 @@ margin-bottom: 20px; } - .spendingPassword { - .spendingPasswordSwitch { - border-top: 1px solid var(--theme-dialog-border-color); - margin-top: 30px; - padding-top: 20px; + .newLabel { + background-color: var( + --theme-wallet-restore-dialog-new-label-background-color + ); + border-radius: 3px; + color: var(--theme-wallet-restore-dialog-new-label-color); + font-family: var(--font-bold); + font-size: 8px; + font-weight: bold; + margin-left: 6px; + opacity: 1 !important; + padding: 2px 8px 3px 8px; + position: relative; + text-transform: uppercase; + top: -2px; + } - & > .passwordLabel { - color: var(--rp-switch-label-text-color); - font-family: var(--font-semibold); - font-size: 16px; - line-height: 1.38; - margin-bottom: 10px; - } + .spendingPasswordWrapper { + border-top: 1px solid var(--theme-dialog-border-color); + margin-top: 30px; + padding-top: 20px; - :global { - .SimpleSwitch_root { - margin-bottom: 0; - } - } + .passwordSectionLabel { + font-family: var(--font-medium); + font-size: 16px; + line-height: 1.38; + margin-bottom: 14px; + } + + .passwordSectionDescription { + font-family: var(--font-light); + font-size: 16px; + line-height: 1.38; } .spendingPasswordFields { display: flex; flex-wrap: wrap; justify-content: space-between; - max-height: 0; - opacity: 0; - overflow: hidden; + max-height: 250px; + opacity: 1; + overflow: visible; transition: all 400ms ease; - &.show { - max-height: 250px; - opacity: 1; - overflow: visible; - } - & > div { margin-top: 20px; width: 275px; @@ -62,7 +70,7 @@ color: var(--theme-dialog-text-color); font-family: var(--font-light); line-height: 1.38; - margin-top: 16px; + margin-top: 10px; } } } @@ -77,9 +85,9 @@ cursor: pointer; flex: 1; font-family: var(--font-medium); - font-size: 16px; + font-size: 14px; opacity: 0.5; - padding: 20px; + padding: 20px 0; text-align: center; } diff --git a/source/renderer/app/components/wallet/WalletSendConfirmationDialog.js b/source/renderer/app/components/wallet/WalletSendConfirmationDialog.js index aa80200768..d1598da437 100644 --- a/source/renderer/app/components/wallet/WalletSendConfirmationDialog.js +++ b/source/renderer/app/components/wallet/WalletSendConfirmationDialog.js @@ -4,7 +4,9 @@ import { observer } from 'mobx-react'; import classnames from 'classnames'; import { Input } from 'react-polymorph/lib/components/Input'; import { InputSkin } from 'react-polymorph/lib/skins/simple/InputSkin'; -import { defineMessages, intlShape } from 'react-intl'; +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 ReactToolboxMobxForm from '../../utils/ReactToolboxMobxForm'; import Dialog from '../widgets/Dialog'; import DialogCloseButton from '../widgets/DialogCloseButton'; @@ -21,8 +23,8 @@ export const messages = defineMessages({ defaultMessage: '!!!Confirm transaction', description: 'Title for the "Confirm transaction" dialog.', }, - spendingPasswordLabel: { - id: 'wallet.send.confirmationDialog.spendingPasswordLabel', + passphraseLabel: { + id: 'wallet.send.confirmationDialog.passphraseLabel', defaultMessage: '!!!Spending password', description: 'Label for the "Spending password" input in the wallet send confirmation dialog.', @@ -49,12 +51,26 @@ export const messages = defineMessages({ description: 'Label for the "Total" in the wallet send confirmation dialog.', }, - spendingPasswordFieldPlaceholder: { - id: 'wallet.send.confirmationDialog.spendingPasswordFieldPlaceholder', + passphraseFieldPlaceholder: { + id: 'wallet.send.confirmationDialog.passphraseFieldPlaceholder', defaultMessage: '!!!Type your spending password', description: 'Placeholder for the "Spending password" inputs in the wallet send confirmation dialog.', }, + flightCandidateWarning: { + id: 'wallet.send.confirmationDialog.flightCandidateWarning', + defaultMessage: + '!!!{Warning}, flight candidate versions of Daedalus are connected to Cardano mainnet. If you confirm this transaction, your ada will be sent for real.', + description: + 'Text for the "Flight candidate" warning in the wallet send confirmation dialog.', + }, + flightCandidateCheckboxLabel: { + id: 'wallet.send.confirmationDialog.flightCandidateCheckboxLabel', + defaultMessage: + '!!!I understand that real ada will be moved as part of this transaction and that this action is irreversible.', + description: + 'Label for the "Flight candidate" warning checkbox in the wallet send confirmation dialog.', + }, sendButtonLabel: { id: 'wallet.send.confirmationDialog.submit', defaultMessage: '!!!Send', @@ -67,12 +83,17 @@ export const messages = defineMessages({ description: 'Label for the back button in the wallet send confirmation dialog.', }, + passwordErrorMessage: { + id: 'wallet.send.confirmationDialog.passwordError', + defaultMessage: '!!!Incorrect spending password.', + description: + 'Label for password error in the wallet send confirmation dialog.', + }, }); messages.fieldIsRequired = globalMessages.fieldIsRequired; type Props = { - isSpendingPasswordSet: boolean, amount: string, receiver: string, totalAmount: ?string, @@ -82,6 +103,7 @@ type Props = { onCancel: Function, onExternalLinkClick: Function, isSubmitting: boolean, + isFlight: boolean, error: ?LocalizableError, currencyUnit: string, }; @@ -95,18 +117,16 @@ export default class WalletSendConfirmationDialog extends Component { form = new ReactToolboxMobxForm( { fields: { - spendingPassword: { + passphrase: { type: 'password', - label: this.context.intl.formatMessage( - messages.spendingPasswordLabel - ), + label: this.context.intl.formatMessage(messages.passphraseLabel), placeholder: this.context.intl.formatMessage( - messages.spendingPasswordFieldPlaceholder + messages.passphraseFieldPlaceholder ), value: '', validators: [ ({ field }) => { - if (this.props.isSpendingPasswordSet && field.value === '') { + if (field.value === '') { return [ false, this.context.intl.formatMessage(messages.fieldIsRequired), @@ -116,6 +136,12 @@ export default class WalletSendConfirmationDialog extends Component { }, ], }, + flightCandidateCheckbox: { + type: 'checkbox', + label: this.context.intl.formatMessage( + messages.flightCandidateCheckboxLabel + ), + }, }, }, { @@ -129,17 +155,12 @@ export default class WalletSendConfirmationDialog extends Component { submit = () => { this.form.submit({ onSuccess: form => { - const { - isSpendingPasswordSet, - receiver, - amount, - amountToNaturalUnits, - } = this.props; - const { spendingPassword } = form.values(); + const { receiver, amount, amountToNaturalUnits } = this.props; + const { passphrase } = form.values(); const transactionData = { receiver, amount: amountToNaturalUnits(amount), - password: isSpendingPasswordSet ? spendingPassword : null, + passphrase, }; this.props.onSubmit(transactionData); }, @@ -148,21 +169,21 @@ export default class WalletSendConfirmationDialog extends Component { }; handleSubmitOnEnter = (event: {}) => - this.form.$('spendingPassword').isValid && - submitOnEnter(this.submit, event); + this.form.$('passphrase').isValid && submitOnEnter(this.submit, event); render() { const { form } = this; const { intl } = this.context; - const spendingPasswordField = form.$('spendingPassword'); + const passphraseField = form.$('passphrase'); + const flightCandidateCheckboxField = form.$('flightCandidateCheckbox'); const { onCancel, - isSpendingPasswordSet, amount, receiver, totalAmount, transactionFee, isSubmitting, + isFlight, error, currencyUnit, onExternalLinkClick, @@ -183,7 +204,9 @@ export default class WalletSendConfirmationDialog extends Component { onClick: this.submit, primary: true, className: confirmButtonClasses, - disabled: !spendingPasswordField.isValid, + disabled: + !passphraseField.isValid || + (!flightCandidateCheckboxField.value && isFlight), }, ]; @@ -196,7 +219,7 @@ export default class WalletSendConfirmationDialog extends Component { onExternalLinkClick={onExternalLinkClick} /> ) : ( - this.context.intl.formatMessage(error) + intl.formatMessage(error) ); } @@ -210,7 +233,7 @@ export default class WalletSendConfirmationDialog extends Component { className={styles.dialog} closeButton={} > -
+
{intl.formatMessage(messages.addressToLabel)} @@ -256,19 +279,31 @@ export default class WalletSendConfirmationDialog extends Component {
- {isSpendingPasswordSet ? ( - - ) : null} +
+ {isFlight && ( +
+ + +
+ )} + {errorElement ?

{errorElement}

: null}
); diff --git a/source/renderer/app/components/wallet/WalletSendConfirmationDialog.scss b/source/renderer/app/components/wallet/WalletSendConfirmationDialog.scss index 5af37f622f..c8389b852b 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; } - .spendingPassword { + .passphrase { margin-top: 20px; } @@ -86,6 +86,35 @@ margin-top: 27px; text-align: center; } + + .flightCandidateWarning { + margin-top: 20px; + p { + color: var(--theme-send-confirmation-dialog-send-values-color); + font-family: var(--font-regular); + line-height: 1.23; + margin-bottom: 20px; + b { + font-family: var(--font-bold); + } + } + em { + color: var(--theme-send-confirmation-dialog-send-values-color); + font-family: var(--font-light); + } + > div { + :global { + .SimpleCheckbox_check { + border-color: var(--theme-send-confirmation-dialog-send-values-color); + } + .SimpleCheckbox_checked { + background-color: var( + --theme-send-confirmation-dialog-send-values-color + ); + } + } + } + } } .submitButtonSpinning { diff --git a/source/renderer/app/components/wallet/WalletSendForm.js b/source/renderer/app/components/wallet/WalletSendForm.js index 37b070accd..b704cf1855 100755 --- a/source/renderer/app/components/wallet/WalletSendForm.js +++ b/source/renderer/app/components/wallet/WalletSendForm.js @@ -10,6 +10,7 @@ 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 { get } from 'lodash'; import ReactToolboxMobxForm from '../../utils/ReactToolboxMobxForm'; import { submitOnEnter } from '../../utils/form'; import AmountInputSkin from './skins/AmountInputSkin'; @@ -25,8 +26,9 @@ import { } from '../../utils/formatters'; import { FORM_VALIDATION_DEBOUNCE_WAIT } from '../../config/timingConfig'; import { FormattedHTMLMessageWithLink } from '../widgets/FormattedHTMLMessageWithLink'; - +import { NUMBER_FORMATS } from '../../../../common/types/number.types'; /* eslint-disable consistent-return */ +import { messages as apiErrorMessages } from '../../api/errors'; export const messages = defineMessages({ titleLabel: { @@ -72,11 +74,6 @@ export const messages = defineMessages({ defaultMessage: '!!!Next', description: 'Label for the next button on the wallet send form.', }, - invalidAddress: { - id: 'wallet.send.form.errors.invalidAddress', - defaultMessage: '!!!Please enter a valid address.', - description: 'Error message shown when invalid address was entered.', - }, invalidAmount: { id: 'wallet.send.form.errors.invalidAmount', defaultMessage: '!!!Please enter a valid amount.', @@ -108,6 +105,8 @@ type Props = { address: string, amount: number ) => Promise, + currentNumberFormat: string, + walletAmount: BigNumber, addressValidator: Function, openDialogAction: Function, isDialogOpen: Function, @@ -185,10 +184,10 @@ export default class WalletSendForm extends Component { this.context.intl.formatMessage(messages.fieldIsRequired), ]; } - const isValidAddress = await this.props.addressValidator(value); const amountField = form.$('amount'); - const amountValue = amountField.value; + const amountValue = amountField.value.toString(); const isAmountValid = amountField.isValid; + const isValidAddress = this.props.addressValidator(value); if (isValidAddress && isAmountValid) { await this._calculateTransactionFee(value, amountValue); } else { @@ -196,16 +195,18 @@ export default class WalletSendForm extends Component { } return [ isValidAddress, - this.context.intl.formatMessage(messages.invalidAddress), + this.context.intl.formatMessage( + apiErrorMessages.invalidAddress + ), ]; }, ], }, amount: { label: this.context.intl.formatMessage(messages.amountLabel), - placeholder: `0.${'0'.repeat( - this.props.currencyMaxFractionalDigits - )}`, + placeholder: `0${ + this._getCurrentNumberFormat().decimalSeparator + }${'0'.repeat(this.props.currencyMaxFractionalDigits)}`, value: null, validators: [ async ({ field, form }) => { @@ -309,6 +310,7 @@ export default class WalletSendForm extends Component { {...amountFieldProps} className="amount" label={intl.formatMessage(messages.amountLabel)} + numberFormat={this._getCurrentNumberFormat()} numberLocaleOptions={{ minimumFractionDigits: currencyMaxFractionalDigits, }} @@ -365,6 +367,7 @@ export default class WalletSendForm extends Component { async _calculateTransactionFee(address: string, amountValue: string) { const amount = formattedAmountToLovelace(amountValue); + try { const fee = await this.props.calculateTransactionFee(address, amount); if (this._isMounted) { @@ -376,7 +379,7 @@ export default class WalletSendForm extends Component { }); } } catch (error) { - const errorHasLink = !!error.values.linkLabel; + const errorHasLink = !!get(error, ['values', 'linkLabel']); const transactionFeeError = errorHasLink ? ( { } } } + + _getCurrentNumberFormat() { + return NUMBER_FORMATS[this.props.currentNumberFormat]; + } } diff --git a/source/renderer/app/components/wallet/backup-recovery/MnemonicWord.scss b/source/renderer/app/components/wallet/backup-recovery/MnemonicWord.scss index 89dc516e9c..cea7a67daa 100644 --- a/source/renderer/app/components/wallet/backup-recovery/MnemonicWord.scss +++ b/source/renderer/app/components/wallet/backup-recovery/MnemonicWord.scss @@ -1,22 +1,11 @@ .root { background-color: var(--theme-mnemonic-background-color); font-size: 16px !important; - margin: 10px 0; - width: 130px !important; + margin: 5px 0; + width: 187px !important; &.disabled { cursor: default !important; opacity: 0.3; } - - &:not(.disabled) { - &:active { - cursor: pointer; - &:hover { - background-color: var( - --theme-mnemonic-background-color-hover - ) !important; - } - } - } } diff --git a/source/renderer/app/components/wallet/backup-recovery/WalletBackupPrivacyWarningDialog.js b/source/renderer/app/components/wallet/backup-recovery/WalletBackupPrivacyWarningDialog.js index a52c003079..52869e2757 100644 --- a/source/renderer/app/components/wallet/backup-recovery/WalletBackupPrivacyWarningDialog.js +++ b/source/renderer/app/components/wallet/backup-recovery/WalletBackupPrivacyWarningDialog.js @@ -4,23 +4,46 @@ import { observer } from 'mobx-react'; import classnames from 'classnames'; import { Checkbox } from 'react-polymorph/lib/components/Checkbox'; import { CheckboxSkin } from 'react-polymorph/lib/skins/simple/CheckboxSkin'; -import { defineMessages, intlShape } from 'react-intl'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; import Dialog from '../../widgets/Dialog'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import WalletRecoveryInstructions from './WalletRecoveryInstructions'; import globalMessages from '../../../i18n/global-messages'; -import { WALLET_RECOVERY_PHRASE_WORD_COUNT } from '../../../config/cryptoConfig'; +import { + LEGACY_WALLET_RECOVERY_PHRASE_WORD_COUNT, + WALLET_RECOVERY_PHRASE_WORD_COUNT, +} from '../../../config/cryptoConfig'; import styles from './WalletBackupPrivacyWarningDialog.scss'; const messages = defineMessages({ - recoveryPhraseInstructions: { - id: 'wallet.backup.privacy.warning.dialog.recoveryPhraseInstructions', - defaultMessage: `!!!On the following screen, you will see a set of X random words. This is - your wallet backup phrase. It can be entered in any version of Daedalus application in order - to back up or restore your wallet’s funds and private key.`, + recoveryPhraseInstructions1: { + id: 'wallet.backup.privacy.warning.dialog.recoveryPhraseInstructions1', + defaultMessage: + '!!!On the following screen, you will be given a list of {walletRecoveryPhraseWordCount} words to write down on paper and keep in a safe place. This list of words is the wallet recovery phrase for the Rewards wallet you are creating.', + description: + 'Instructions for backing up wallet recovery phrase on dialog that displays wallet recovery phrase.', + }, + recoveryPhraseInstructions2: { + id: 'wallet.backup.privacy.warning.dialog.recoveryPhraseInstructions2', + defaultMessage: + '!!!The simplest way to keep your wallet recovery phrase secure is to never store it digitally or online. If you decide to use an online service, such as a password manager with an encrypted database, it is your responsibility to make sure that you use it correctly.', description: 'Instructions for backing up wallet recovery phrase on dialog that displays wallet recovery phrase.', }, + recoveryPhraseInstructions3Itn: { + id: 'wallet.backup.privacy.warning.dialog.recoveryPhraseInstructions3.itn', + defaultMessage: + '!!!Using your recovery phrase is the only way to recover your wallet if your computer is lost, broken, stolen, or stops working. You will also need this recovery phrase to receive your Incentivized Testnet ada rewards on the Cardano mainnet.', + description: + 'Instructions for backing up wallet recovery phrase on dialog that displays wallet recovery phrase.', + }, + recoveryPhraseInstructions3: { + id: 'wallet.backup.privacy.warning.dialog.recoveryPhraseInstructions3', + defaultMessage: + '!!!Using your recovery phrase is the only way to recover your wallet if your computer is lost, broken, stolen, or stops working.', + description: + 'Instructions for backing up wallet recovery phrase on dialog that displays wallet recovery phrase on ITN.', + }, buttonLabelContinue: { id: 'wallet.backup.privacy.warning.dialog..button.labelContinue', // TODO: fix translation key path 'dialog..button' defaultMessage: '!!!Continue', @@ -29,12 +52,14 @@ const messages = defineMessages({ termNobodyWatching: { id: 'wallet.backup.privacy.warning.dialog.checkbox.label.nobodyWatching', defaultMessage: - '!!!Make sure nobody looks into your screen unless you want them to have access to your funds.', + '!!!I confirm that nobody can see my screen, because anyone who knows my recovery phrase will be able to spend the ada in my new wallet.', description: 'Label for the checkbox on wallet backup dialog describing that nobody should be watching when recovery phrase is shown', }, }); +const { isIncentivizedTestnet } = global; + type Props = { countdownRemaining: number, canPhraseBeShown: boolean, @@ -88,12 +113,30 @@ export default class WalletBackupPrivacyWarningDialog extends Component { > + + + ) : ( + + ) + } + />
on a piece of paper in the exact order shown here.', description: 'Instructions for backing up wallet recovery phrase on dialog that displays wallet recovery phrase.', }, buttonLabelIHaveWrittenItDown: { id: 'wallet.backup.recovery.phrase.display.dialog.button.label.iHaveWrittenItDown', - defaultMessage: '!!!Yes, I’ve written it down', + defaultMessage: '!!!Yes, I have written down my wallet recovery phrase.', description: - 'Label for button "Yes, I’ve written it down" on wallet backup dialog', + 'Label for button "Yes, I have written down my wallet recovery phrase." on wallet backup dialog', }, }); @@ -41,6 +45,7 @@ export default class WalletRecoveryPhraseDisplayDialog extends Component render() { const { intl } = this.context; + const { isIncentivizedTestnet } = global; const { recoveryPhrase, onStartWalletBackup, onCancelBackup } = this.props; const dialogClasses = classnames([ styles.component, @@ -66,7 +71,14 @@ export default class WalletRecoveryPhraseDisplayDialog extends Component > + } /> diff --git a/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js b/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js index a9aef42b21..96975d11bf 100644 --- a/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js +++ b/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.js @@ -19,7 +19,7 @@ const messages = defineMessages({ verificationInstructions: { id: 'wallet.backup.recovery.phrase.entry.dialog.verification.instructions', defaultMessage: - '!!!Tap each word in the correct order to verify your recovery phrase', + '!!!Verify your wallet recovery phrase by clicking words in the exact order you wrote them down.', description: 'Instructions for verifying wallet recovery phrase on dialog for entering wallet recovery phrase.', }, @@ -33,35 +33,46 @@ const messages = defineMessages({ defaultMessage: '!!!Clear', description: 'Label for button "Clear" on wallet backup dialog', }, - termDevice: { - id: 'wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.device', + termOffline: { + id: + 'wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.offline', defaultMessage: - '!!!I understand that my money are held securely on this device only, not on the company servers', - description: - 'Term and condition on wallet backup dialog describing that wallet is on a users device, not on company servers', + '!!!I understand that the simplest way to keep my wallet recovery phrase secure is to never store it digitally or online. If I decide to use an online service, such as a password manager with an encrypted database, it is my responsibility to make sure that I use it correctly.', + description: 'Term on wallet creation to store recovery phrase offline', }, termRecovery: { id: 'wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.recovery', - defaultMessage: `!!!I understand that if this application is moved to another device or deleted, my money can - be only recovered with the backup phrase which were written down in a secure place`, + defaultMessage: + '!!!I understand that the only way to recover my wallet if my computer is lost, broken, stolen, or stops working is to use my wallet recovery phrase.', + description: + 'Term and condition on wallet backup dialog describing that wallet can only be recovered with a security phrase', + }, + termRewards: { + id: + 'wallet.backup.recovery.phrase.entry.dialog.terms.and.condition.rewards', + defaultMessage: `!!!I understand that I will need the wallet recovery phrase of this wallet to receive my Incentivized Testnet ada rewards on the Cardano mainnet.`, description: 'Term and condition on wallet backup dialog describing that wallet can only be recovered with a security phrase', }, }); +const { isIncentivizedTestnet } = global; + type Props = { recoveryPhraseShuffled: Array, enteredPhrase: Array<{ word: string }>, isValid: boolean, - isTermDeviceAccepted: boolean, + isTermOfflineAccepted: boolean, isTermRecoveryAccepted: boolean, + isTermRewardsAccepted: boolean, isSubmitting: boolean, onAddWord: Function, canFinishBackup: boolean, onClear: Function, - onAcceptTermDevice: Function, + onAcceptTermOffline: Function, onAcceptTermRecovery: Function, + onAcceptTermRewards: Function, onRestartBackup: Function, onCancelBackup: Function, onFinishBackup: Function, @@ -79,13 +90,15 @@ export default class WalletRecoveryPhraseEntryDialog extends Component { recoveryPhraseShuffled, enteredPhrase, isValid, - isTermDeviceAccepted, + isTermOfflineAccepted, isTermRecoveryAccepted, + isTermRewardsAccepted, isSubmitting, onAddWord, onClear, - onAcceptTermDevice, + onAcceptTermOffline, onAcceptTermRecovery, + onAcceptTermRewards, canFinishBackup, onRestartBackup, onCancelBackup, @@ -164,20 +177,31 @@ export default class WalletRecoveryPhraseEntryDialog extends Component {
} - onChange={onAcceptTermDevice} - checked={isTermDeviceAccepted} + label={} + onChange={onAcceptTermOffline} + checked={isTermOfflineAccepted} skin={CheckboxSkin} />
+ {isIncentivizedTestnet && ( +
+ } + onChange={onAcceptTermRewards} + checked={isTermRewardsAccepted} + 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 bd8e8b554f..efa87f267f 100644 --- a/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.scss +++ b/source/renderer/app/components/wallet/backup-recovery/WalletRecoveryPhraseEntryDialog.scss @@ -5,7 +5,7 @@ flex-direction: row; flex-wrap: wrap; justify-content: space-between; - margin-top: 30px; + margin-top: 20px; } .checkbox { @@ -24,6 +24,12 @@ } } +.isBold { + label { + font-family: var(--font-medium); + } +} + .isSubmitting { box-shadow: none !important; @include loading-spinner('../../../assets/images/spinner-light.svg'); diff --git a/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.js b/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.js index f93a58ccba..23184e4058 100644 --- a/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.js +++ b/source/renderer/app/components/wallet/file-import/WalletFileImportDialog.js @@ -4,18 +4,15 @@ import { observer } from 'mobx-react'; import classnames from 'classnames'; import { defineMessages, intlShape } from 'react-intl'; // 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, - isValidSpendingPassword, - isValidRepeatPassword, + // isValidSpendingPassword, + // isValidRepeatPassword, } from '../../../utils/validations'; import globalMessages from '../../../i18n/global-messages'; import LocalizableError from '../../../i18n/LocalizableError'; @@ -57,18 +54,6 @@ const messages = defineMessages({ description: 'Label "Import wallet" submit button on the dialog for importing a wallet from a file.', }, - passwordSwitchPlaceholder: { - id: 'wallet.file.import.dialog.passwordSwitchPlaceholder', - defaultMessage: '!!!Activate to create password', - description: - 'Text for the "Activate to create password" switch in the wallet file import dialog.', - }, - passwordSwitchLabel: { - id: 'wallet.file.import.dialog.passwordSwitchLabel', - defaultMessage: '!!!Password', - description: - 'Label for the "Activate to create password" switch in the wallet file import dialog.', - }, spendingPasswordLabel: { id: 'wallet.file.import.dialog.spendingPasswordLabel', defaultMessage: '!!!Wallet password', @@ -96,24 +81,12 @@ type Props = { error: ?LocalizableError, }; -type State = { - createPassword: boolean, -}; - @observer -export default class WalletFileImportDialog extends Component { - state = { - createPassword: false, - }; - +export default class WalletFileImportDialog extends Component { static contextTypes = { intl: intlShape.isRequired, }; - handlePasswordSwitchToggle = (value: boolean) => { - this.setState({ createPassword: value }); - }; - form = new ReactToolboxMobxForm( { fields: { @@ -150,18 +123,18 @@ export default class WalletFileImportDialog extends Component { ), value: '', validators: [ - ({ field, form }) => { - if (!this.state.createPassword) return [true]; - const repeatPasswordField = form.$('repeatPassword'); - if (repeatPasswordField.value.length > 0) { - repeatPasswordField.validate({ showErrors: true }); - } - return [ - isValidSpendingPassword(field.value), - this.context.intl.formatMessage( - globalMessages.invalidSpendingPassword - ), - ]; + () => { + // const repeatPasswordField = form.$('repeatPassword'); + // if (repeatPasswordField.value.length > 0) { + // repeatPasswordField.validate({ showErrors: true }); + // } + // return [ + // isValidSpendingPassword(field.value), + // this.context.intl.formatMessage( + // globalMessages.invalidSpendingPassword + // ), + // ]; + return [true]; // @API TODO - missing API v2 endpoint and password declaration }, ], }, @@ -173,16 +146,16 @@ export default class WalletFileImportDialog extends Component { ), value: '', validators: [ - ({ field, form }) => { - if (!this.state.createPassword) return [true]; - const spendingPassword = form.$('spendingPassword').value; - if (spendingPassword.length === 0) return [true]; - return [ - isValidRepeatPassword(spendingPassword, field.value), - this.context.intl.formatMessage( - globalMessages.invalidRepeatPassword - ), - ]; + () => { + // const spendingPassword = form.$('spendingPassword').value; + // if (spendingPassword.length === 0) return [true]; + // return [ + // isValidRepeatPassword(spendingPassword, field.value), + // this.context.intl.formatMessage( + // globalMessages.invalidRepeatPassword + // ), + // ]; + return [true]; // @API TODO - missing API v2 endpoint and password declaration }, ], }, @@ -199,11 +172,10 @@ export default class WalletFileImportDialog extends Component { submit = () => { this.form.submit({ onSuccess: form => { - const { createPassword } = this.state; const { walletFilePath, spendingPassword, walletName } = form.values(); const walletData = { filePath: walletFilePath, - spendingPassword: createPassword ? spendingPassword : null, + spendingPassword, walletName: walletName.length > 0 ? walletName : null, }; this.props.onSubmit(walletData); @@ -222,11 +194,6 @@ export default class WalletFileImportDialog extends Component { 'WalletFileImportDialog', ]); - // const spendingPasswordFieldsClasses = classnames([ - // styles.spendingPasswordFields, - // createPassword ? styles.show : null, - // ]); - const actions = [ { className: isSubmitting ? styles.isSubmitting : null, @@ -263,7 +230,7 @@ export default class WalletFileImportDialog extends Component { /> - {/* TODO: re-enable when wallet-name and wallet-password + {/* TODO: re-enable when wallet-name support is added to the API endpoint { />
-
-
- {intl.formatMessage(messages.passwordSwitchLabel)} -
- -
- -
+
.passwordLabel { - color: var(--rp-switch-label-text-color); - font-family: var(--font-semibold); - font-size: 16px; - line-height: 1.38; - margin-bottom: 10px; - } - - :global { - .SimpleSwitch_root { - margin-bottom: 0; - } - } - } - .spendingPasswordFields { display: flex; flex-wrap: wrap; justify-content: space-between; - max-height: 0; - opacity: 0; - overflow: hidden; + max-height: 250px; + opacity: 1; + overflow: visible; transition: all 400ms ease; - &.show { - max-height: 250px; - opacity: 1; - overflow: visible; - } - & > div { margin-top: 30px; width: 275px; diff --git a/source/renderer/app/components/wallet/layouts/WalletWithNavigation.js b/source/renderer/app/components/wallet/layouts/WalletWithNavigation.js index c7d3e01143..c32a4ee187 100644 --- a/source/renderer/app/components/wallet/layouts/WalletWithNavigation.js +++ b/source/renderer/app/components/wallet/layouts/WalletWithNavigation.js @@ -4,13 +4,22 @@ import type { Node } from 'react'; import { observer } from 'mobx-react'; import WalletNavigation from '../navigation/WalletNavigation'; import styles from './WalletWithNavigation.scss'; +import NotResponding from '../not-responding/NotResponding'; +import SetWalletPassword from '../settings/SetWalletPassword'; type Props = { children?: Node, activeItem: string, + hasNotification?: boolean, + hasPassword: boolean, isActiveScreen: Function, + isLegacy: boolean, + isNotResponding: boolean, + isSetWalletPasswordDialogOpen: boolean, + onOpenExternalLink: Function, + onRestartNode: Function, + onSetWalletPassword: Function, onWalletNavItemClick: Function, - hasNotification?: boolean, }; @observer @@ -18,22 +27,47 @@ export default class WalletWithNavigation extends Component { render() { const { children, - isActiveScreen, - onWalletNavItemClick, activeItem, hasNotification, + hasPassword, + isActiveScreen, + isLegacy, + isNotResponding, + isSetWalletPasswordDialogOpen, + onOpenExternalLink, + onRestartNode, + onSetWalletPassword, + onWalletNavItemClick, } = this.props; + return (
+
{children}
+ + {!hasPassword && ( + + )} + + {isNotResponding && ( + + )}
); } diff --git a/source/renderer/app/components/wallet/layouts/WalletWithNavigation.scss b/source/renderer/app/components/wallet/layouts/WalletWithNavigation.scss index c78c217222..cb136d14f4 100644 --- a/source/renderer/app/components/wallet/layouts/WalletWithNavigation.scss +++ b/source/renderer/app/components/wallet/layouts/WalletWithNavigation.scss @@ -1,14 +1,14 @@ .component { display: flex; + flex: 1; flex-direction: column; - height: 100%; + overflow-y: overlay; } .navigation { flex-shrink: 0; height: 50px; position: relative; - z-index: 1; } .page { diff --git a/source/renderer/app/components/wallet/navigation/WalletNavigation.js b/source/renderer/app/components/wallet/navigation/WalletNavigation.js index fab1254c4a..9428845d12 100755 --- a/source/renderer/app/components/wallet/navigation/WalletNavigation.js +++ b/source/renderer/app/components/wallet/navigation/WalletNavigation.js @@ -1,13 +1,22 @@ // @flow import React, { Component } from 'react'; import { observer } from 'mobx-react'; +import { includes } from 'lodash'; import { defineMessages, intlShape } from 'react-intl'; +import { + WALLET_NAV_IDS, + ITN_LEGACY_WALLET_EXCLUDED_NAV_ITEMS, +} from '../../../config/walletNavigationConfig'; import Navigation from '../../navigation/Navigation'; import summaryIcon from '../../../assets/images/wallet-nav/summary-ic.inline.svg'; import sendIcon from '../../../assets/images/wallet-nav/send-ic.inline.svg'; import receiveIcon from '../../../assets/images/wallet-nav/receive-ic.inline.svg'; import transactionsIcon from '../../../assets/images/wallet-nav/transactions-ic.inline.svg'; import settingsIcon from '../../../assets/images/wallet-nav/wallet-settings-2-ic.inline.svg'; +import type { + NavButtonProps, + NavDropdownProps, +} from '../../navigation/Navigation'; const messages = defineMessages({ summary: { @@ -53,6 +62,7 @@ const messages = defineMessages({ type Props = { activeItem: string, isActiveNavItem: Function, + isLegacy: boolean, onNavItemClick: Function, hasNotification?: boolean, }; @@ -66,56 +76,70 @@ export default class WalletNavigation extends Component { render() { const { isActiveNavItem, + isLegacy, onNavItemClick, activeItem, hasNotification, } = this.props; const { intl } = this.context; + const { isIncentivizedTestnet } = global; + const items: Array = [ + { + id: WALLET_NAV_IDS.SUMMARY, + label: intl.formatMessage(messages.summary), + icon: summaryIcon, + }, + { + id: WALLET_NAV_IDS.SEND, + label: intl.formatMessage(messages.send), + icon: sendIcon, + }, + { + id: WALLET_NAV_IDS.RECEIVE, + label: intl.formatMessage(messages.receive), + icon: receiveIcon, + }, + { + id: WALLET_NAV_IDS.TRANSACTIONS, + label: intl.formatMessage(messages.transactions), + icon: transactionsIcon, + }, + { + id: WALLET_NAV_IDS.SETTINGS, + type: 'dropdown', + label: + isLegacy && isIncentivizedTestnet + ? intl.formatMessage(messages.settings) + : intl.formatMessage(messages.more), + icon: settingsIcon, + hasNotification, + options: [ + { + label: intl.formatMessage(messages.settings), + value: 'settings', + hasNotification, + }, + { + label: intl.formatMessage(messages.utxo), + value: 'utxo', + }, + ], + }, + ].filter( + item => + !( + isIncentivizedTestnet && + isLegacy && + includes(ITN_LEGACY_WALLET_EXCLUDED_NAV_ITEMS, item.id) + ) + ); return ( ); } diff --git a/source/renderer/app/components/wallet/not-responding/NotResponding.js b/source/renderer/app/components/wallet/not-responding/NotResponding.js new file mode 100644 index 0000000000..1a6ef1c7b2 --- /dev/null +++ b/source/renderer/app/components/wallet/not-responding/NotResponding.js @@ -0,0 +1,90 @@ +// @flow +import React, { Component } from 'react'; +import SVGInline from 'react-svg-inline'; +import { defineMessages, intlShape, FormattedHTMLMessage } from 'react-intl'; +import { Button } from 'react-polymorph/lib/components/Button'; +import { ButtonSkin } from 'react-polymorph/lib/skins/simple/ButtonSkin'; +import { Link } from 'react-polymorph/lib/components/Link'; +import { LinkSkin } from 'react-polymorph/lib/skins/simple/LinkSkin'; +import icon from '../../../assets/images/not-responding.inline.svg'; +import styles from './NotResponding.scss'; + +type Props = { + walletName: string, + onRestartNode: Function, + onOpenExternalLink: Function, +}; + +const messages = defineMessages({ + title: { + id: 'wallet.notResponding.title', + defaultMessage: '!!!The wallet is not responding.', + description: 'Title on the NotResponding dialog.', + }, + description: { + id: 'wallet.notResponding.description', + defaultMessage: + '!!!The {walletName} wallet is not responding. This is caused by a known but rare issue, which is currently being fixed. Please restart the Cardano node by clicking the button below, which should resolve the issue. If the issue persists, or if it happens again, please submit a support request.', + description: 'Description on the NotResponding dialog.', + }, + restartNodeButtonLabel: { + id: 'wallet.notResponding.restartNodeButtonLabel', + defaultMessage: '!!!Restart Cardano Node', + description: 'Restart Node Button Label on the NotResponding dialog.', + }, + submitSupportRequestLabel: { + id: 'wallet.notResponding.submitSupportRequestLabel', + defaultMessage: '!!!Submit a support request', + description: 'Submit Support Request Label on the NotResponding dialog', + }, + submitSupportRequestUrl: { + id: 'wallet.notResponding.submitSupportRequestUrl', + defaultMessage: '!!!https://iohk.zendesk.com/hc/en-us/requests/new/', + description: 'Submit Support Request Url on the NotResponding dialog', + }, +}); + +export default class NotResponding extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + render() { + const { intl } = this.context; + const { walletName, onRestartNode, onOpenExternalLink } = this.props; + return ( +
+
+ +
+ {intl.formatMessage(messages.title)} +
+
+ +
+
+
+ ); + } +} diff --git a/source/renderer/app/components/wallet/not-responding/NotResponding.scss b/source/renderer/app/components/wallet/not-responding/NotResponding.scss new file mode 100644 index 0000000000..bb4254ca34 --- /dev/null +++ b/source/renderer/app/components/wallet/not-responding/NotResponding.scss @@ -0,0 +1,91 @@ +@import '../../../themes/mixins/link'; + +.component { + background: var(--theme-wallet-not-responding-background-color); + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + position: absolute; + width: 100%; + z-index: 9; + + .content { + display: flex; + flex-direction: column; + margin: 0 auto; + text-align: center; + width: 618px; + + .icon { + margin-bottom: 36px; + svg { + height: 73px; + width: 82px; + path, + circle { + stroke: var(--theme-wallet-not-responding-icon-color); + } + path:nth-child(4) { + fill: var(--theme-wallet-not-responding-icon-color); + } + } + } + .title { + color: var(--theme-wallet-not-responding-title-text-color); + font-family: var(--font-regular); + font-size: 20px; + line-height: 1.2; + margin-bottom: 16px; + text-align: center; + } + .description { + background-color: var( + --theme-wallet-not-responding-description-background-color + ); + border-radius: 4px; + margin-bottom: 30px; + padding: 12px 24px; + text-align: left; + p { + color: var(--theme-wallet-not-responding-title-text-color); + font-family: var(--font-regular); + font-size: 14px; + line-height: 1.43; + margin-bottom: 10px; + opacity: 0.7; + &:last-child { + margin-bottom: 0; + } + } + } + .restartNodeButton { + background-color: var( + --theme-wallet-not-responding-button-background-color + ); + border: solid 1px var(--theme-wallet-not-responding-button-border-color); + color: var(--theme-wallet-not-responding-button-text-color); + font-weight: 500; + line-height: 1.36; + margin: 0 auto 20px; + + &:hover { + background-color: var( + --theme-wallet-not-responding-button-background-color-hover + ); + border: none; + color: var(--theme-wallet-not-responding-button-text-color-hover); + } + } + .submitSupportLink { + border-bottom: 1px solid + var(--theme-wallet-not-responding-link-text-color); + color: var(--theme-wallet-not-responding-link-text-color); + margin: 0 auto; + opacity: 0.8; + &:after { + background-color: var(--theme-wallet-not-responding-link-text-color); + } + } + } +} 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 f33967dcf4..00fba7082f 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.js +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.js @@ -6,12 +6,13 @@ import classnames from 'classnames'; import { defineMessages, intlShape } from 'react-intl'; import CopyToClipboard from 'react-copy-to-clipboard'; import SVGInline from 'react-svg-inline'; +import { Link } from 'react-polymorph/lib/components/Link'; +import { LinkSkin } from 'react-polymorph/lib/skins/simple/LinkSkin'; import Dialog from '../../widgets/Dialog'; import { getNetworkExplorerUrl } from '../../../utils/network'; import styles from './CompletionDialog.scss'; import iconCopy from '../../../assets/images/clipboard-ic.inline.svg'; -import externalLinkIcon from '../../../assets/images/link-ic.inline.svg'; -import InlineNotification from '../../widgets/InlineNotification'; +import InlineNotification from '../../notifications/InlineNotification'; import { DEVELOPMENT } from '../../../../../common/types/environment.types'; const messages = defineMessages({ @@ -74,6 +75,7 @@ type Props = { onOpenExternalLink: Function, copyAddressNotificationDuration: number, network: string, + rawNetwork: string, }; type State = { @@ -88,6 +90,7 @@ export default class CompletionDialog extends Component { static defaultProps = { network: DEVELOPMENT, + rawNetwork: DEVELOPMENT, }; state = { @@ -115,6 +118,7 @@ export default class CompletionDialog extends Component { walletCertificateAddress, onOpenExternalLink, network, + rawNetwork, } = this.props; const { showCopyNotification } = this.state; const dialogClasses = classnames([styles.component, 'completionDialog']); @@ -128,7 +132,8 @@ export default class CompletionDialog extends Component { }, ]; const cardanoExplorerLink = `${getNetworkExplorerUrl( - network + network, + rawNetwork )}/address/${walletCertificateAddress}`; // Get QRCode color value from active theme's CSS variable @@ -162,15 +167,12 @@ export default class CompletionDialog extends Component {

- onOpenExternalLink(cardanoExplorerLink)} - role="link" - aria-hidden - > - {cardanoExplorerLink} - - + label={cardanoExplorerLink} + skin={LinkSkin} + />
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 dca07a0a1e..fe009c1967 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.scss +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/CompletionDialog.scss @@ -38,9 +38,19 @@ word-wrap: break-word; .link { - @include link( + border-color: var( --theme-paper-wallet-create-certificate-dialog-explorer-link-color ); + color: var( + --theme-paper-wallet-create-certificate-dialog-explorer-link-color + ); + font-family: var(--font-mono); + font-size: 16px; + &:after { + background-color: var( + --theme-paper-wallet-create-certificate-dialog-explorer-link-color + ); + } } } 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 4a8e46e340..67197fe572 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.js +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.js @@ -3,7 +3,8 @@ import React, { Component } from 'react'; import { observer } from 'mobx-react'; import classnames from 'classnames'; import { defineMessages, intlShape, FormattedMessage } from 'react-intl'; -import SVGInline from 'react-svg-inline'; +import { Link } from 'react-polymorph/lib/components/Link'; +import { LinkSkin } from 'react-polymorph/lib/skins/simple/LinkSkin'; import Dialog from '../../widgets/Dialog'; import DialogCloseButton from '../../widgets/DialogCloseButton'; import { getNetworkExplorerUrl } from '../../../utils/network'; @@ -16,7 +17,6 @@ import { WALLET_RECOVERY_PHRASE_WORD_COUNT, } from '../../../config/cryptoConfig'; import { DEVELOPMENT } from '../../../../../common/types/environment.types'; -import externalLinkIcon from '../../../assets/images/link-ic.inline.svg'; const messages = defineMessages({ headline: { @@ -110,6 +110,7 @@ const messages = defineMessages({ type Props = { inProgress: boolean, network: string, + rawNetwork: string, onClose: Function, onOpenExternalLink: Function, onPrint: Function, @@ -124,6 +125,7 @@ export default class InstructionsDialog extends Component { static defaultProps = { network: DEVELOPMENT, + rawNetwork: DEVELOPMENT, }; componentWillReceiveProps(newProps: Props) { @@ -140,6 +142,7 @@ export default class InstructionsDialog extends Component { inProgress, onOpenExternalLink, network, + rawNetwork, error, } = this.props; const dialogClasses = classnames([styles.component, 'instructionsDialog']); @@ -158,21 +161,16 @@ export default class InstructionsDialog extends Component { }, ]; - const openNetworkExplorer = onOpenExternalLink.bind( - null, - getNetworkExplorerUrl(network) - ); + const openNetworkExplorer = () => + onOpenExternalLink(getNetworkExplorerUrl(network, rawNetwork)); const cardanoExplorerLink = ( - - {intl.formatMessage(messages.cardanoExplorer)} - - + label={intl.formatMessage(messages.cardanoExplorer)} + skin={LinkSkin} + /> ); return ( 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 4e3ad6e955..e63a3c4c4e 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.scss +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/InstructionsDialog.scss @@ -27,10 +27,19 @@ } .link { - @include link( + border-color: var( --theme-paper-wallet-create-certificate-dialog-explorer-link-color ); - margin-right: 4px; + color: var( + --theme-paper-wallet-create-certificate-dialog-explorer-link-color + ); + font-size: 16px; + word-break: break-word; + &:after { + background-color: var( + --theme-paper-wallet-create-certificate-dialog-explorer-link-color + ); + } } ul { 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 175fa3fe29..d353804c84 100644 --- a/source/renderer/app/components/wallet/paper-wallet-certificate/VerificationDialog.js +++ b/source/renderer/app/components/wallet/paper-wallet-certificate/VerificationDialog.js @@ -297,6 +297,7 @@ export default class VerificationDialog extends Component { messages.recoveryPhraseNoResults )} skin={AutocompleteSkin} + optionHeight={50} /> { - render() { - const { address, onCopyAddress, copyAddressLabel, index } = this.props; - const addressClasses = classnames([ - `generatedAddress-${index + 1}`, - styles.component, - address.used ? styles.usedWalletAddress : null, - ]); - return ( -
-
- {address.id} -
-
- - - - - {copyAddressLabel} - - - -
-
- ); - } -} diff --git a/source/renderer/app/components/wallet/receive/Address.scss b/source/renderer/app/components/wallet/receive/Address.scss deleted file mode 100644 index 3d0771175b..0000000000 --- a/source/renderer/app/components/wallet/receive/Address.scss +++ /dev/null @@ -1,53 +0,0 @@ -.component { - box-sizing: border-box; - display: flex; - padding: 10.5px 0; - word-break: break-all; - - .addressId { - flex-grow: 1; - font-family: var(--font-mono); - letter-spacing: -0.4px; - margin-right: 32.5px; - user-select: text; - } - - .addressActions { - .copyAddress { - cursor: pointer; - display: flex; - white-space: nowrap; - - .copyAddressLabel { - color: var(--theme-label-button-color); - font-size: 14px; - margin-left: 6px; - opacity: 0.5; - } - } - } -} - -.usedWalletAddress { - .addressId, - .addressActions .copyAddress .copyIcon { - opacity: 0.4; - } - - .addressActions .copyAddress .copyAddressLabel { - opacity: 0.2; - } -} - -.copyIcon { - cursor: pointer; - margin-left: 4px; - object-fit: contain; - & > svg { - height: 12px; - width: 10px; - path { - fill: var(--theme-icon-copy-address-color); - } - } -} diff --git a/source/renderer/app/components/wallet/receive/AddressActions.js b/source/renderer/app/components/wallet/receive/AddressActions.js new file mode 100644 index 0000000000..c02c882130 --- /dev/null +++ b/source/renderer/app/components/wallet/receive/AddressActions.js @@ -0,0 +1,107 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import { defineMessages, intlShape } from 'react-intl'; +import SVGInline from 'react-svg-inline'; +import styles from './AddressActions.scss'; +import iconQR from '../../../assets/images/qr-code.inline.svg'; +import iconCopy from '../../../assets/images/clipboard-ic.inline.svg'; +import WalletAddress from '../../../domains/WalletAddress'; + +const messages = defineMessages({ + instructionsTitle: { + id: 'wallet.receive.page.instructions.instructionsTitle', + defaultMessage: '!!!Your wallet addresses', + description: 'Instructions Title on the wallet "Receive page"', + }, + instructionsDescription: { + id: 'wallet.receive.page.instructions.instructionsDescription', + defaultMessage: + '!!!Share this wallet address to receive payments. To protect your privacy, new addresses are generated automatically once you use them.', + description: 'Instructions Description on the wallet "Receive page"', + }, + addressesTitle: { + id: 'wallet.receive.page.addresses.addressesTitle', + defaultMessage: '!!!Addresses', + description: 'Addresses Title on the wallet "Receive page"', + }, + showUsedLabel: { + id: 'wallet.receive.page.showUsedLabel', + defaultMessage: '!!!show used', + description: + 'Label for "show used" wallet addresses link on the wallet "Receive page"', + }, + shareAddressLabel: { + id: 'wallet.receive.page.shareAddressLabel', + defaultMessage: '!!!Share', + description: 'Label for "Share" link on the wallet "Receive page"', + }, + copyAddressLabel: { + id: 'wallet.receive.page.copyAddressLabel', + defaultMessage: '!!!Copy address', + description: 'Label for "Copy address" link on the wallet "Receive page"', + }, +}); + +type Props = { + address: WalletAddress, + onShareAddress: Function, + onCopyAddress: Function, + type?: 'share' | 'copy', +}; + +@observer +export default class AddressActions extends Component { + static defaultProps = { + type: 'copy', + }; + static contextTypes = { + intl: intlShape.isRequired, + }; + + addressElement: ?HTMLElement; + addressContainerElement: ?HTMLElement; + + render() { + const { intl } = this.context; + const { + address, + onShareAddress, + onCopyAddress, + type = 'copy', + } = this.props; + const { id: addressId, used: isUsed } = address; + const componentClasses = classnames(styles[`${type}Actions`], { + [styles.isUsed]: isUsed, + }); + return ( +
+ {type === 'copy' ? ( + onCopyAddress(addressId)} + > + + + + {intl.formatMessage(messages.copyAddressLabel)} + + + + ) : ( + + )} +
+ ); + } +} diff --git a/source/renderer/app/components/wallet/receive/AddressActions.scss b/source/renderer/app/components/wallet/receive/AddressActions.scss new file mode 100644 index 0000000000..1236e40328 --- /dev/null +++ b/source/renderer/app/components/wallet/receive/AddressActions.scss @@ -0,0 +1,64 @@ +.shareActions { + display: flex; + flex-direction: row; + flex-shrink: 0; + height: 18px; + justify-content: flex-end; +} +.copyActions { +} +.isUsed { + .copyAddress .copyIcon { + opacity: 0.4; + } + .copyAddress .copyAddressLabel { + opacity: 0.2; + } +} + +/* COPY */ +.copyAddress { + cursor: pointer; + display: flex; + white-space: nowrap; +} +.copyAddressLabel { + color: var(--theme-label-button-color); + font-size: 14px; + margin-left: 6px; + opacity: 0.5; +} +.copyIcon { + cursor: pointer; + margin-left: 4px; + object-fit: contain; + & > svg { + height: 12px; + width: 10px; + path { + fill: var(--theme-icon-copy-address-color); + } + } +} + +/* SHARE */ +.shareAddressButton { + cursor: pointer; + line-height: 1.36; + white-space: nowrap; +} +.shareIcon { + & > svg { + height: 12px; + width: 12px; + path { + fill: var(--theme-icon-copy-address-color); + } + } +} +.shareAddressLabel { + color: var(--theme-label-button-color); + font-size: 14px; + margin-left: 6px; + opacity: 0.5; +} diff --git a/source/renderer/app/components/wallet/receive/AddressRandom.js b/source/renderer/app/components/wallet/receive/AddressRandom.js new file mode 100644 index 0000000000..64798e6a94 --- /dev/null +++ b/source/renderer/app/components/wallet/receive/AddressRandom.js @@ -0,0 +1,39 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import AddressActions from './AddressActions'; +import styles from './AddressRandom.scss'; +import WalletAddress from '../../../domains/WalletAddress'; + +type Props = { + address: WalletAddress, + index: number, + onCopyAddress: Function, + onShareAddress: Function, +}; + +@observer +export default class AddressRandom extends Component { + render() { + const { address, onCopyAddress, onShareAddress, index } = this.props; + const addressClasses = classnames([ + 'Address', + `generatedAddress-${index + 1}`, + styles.component, + address.used ? styles.usedWalletAddress : null, + ]); + return ( +
+
+ {address.id} +
+ +
+ ); + } +} diff --git a/source/renderer/app/components/wallet/receive/AddressRandom.scss b/source/renderer/app/components/wallet/receive/AddressRandom.scss new file mode 100644 index 0000000000..18b5e39c09 --- /dev/null +++ b/source/renderer/app/components/wallet/receive/AddressRandom.scss @@ -0,0 +1,20 @@ +.component { + box-sizing: border-box; + display: flex; + padding: 10.5px 0; + word-break: break-all; + + .addressId { + flex-grow: 1; + font-family: var(--font-mono); + letter-spacing: -0.4px; + margin-right: 32.5px; + user-select: text; + } +} + +.usedWalletAddress { + .addressId { + opacity: 0.4; + } +} diff --git a/source/renderer/app/components/wallet/receive/AddressSequential.js b/source/renderer/app/components/wallet/receive/AddressSequential.js new file mode 100644 index 0000000000..2407465ca7 --- /dev/null +++ b/source/renderer/app/components/wallet/receive/AddressSequential.js @@ -0,0 +1,88 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import classnames from 'classnames'; +import AddressActions from './AddressActions'; +import styles from './AddressSequential.scss'; +import WalletAddress from '../../../domains/WalletAddress'; + +type Props = { + address: WalletAddress, + onShareAddress: Function, + onCopyAddress: Function, + shouldRegisterAddressElement: boolean, + onRegisterHTMLElements: Function, + addressSlice: number, +}; + +@observer +export default class AddressSequential extends Component { + addressElement: ?HTMLElement; + addressContainerElement: ?HTMLElement; + + componentDidMount() { + if (this.props.shouldRegisterAddressElement) { + this.props.onRegisterHTMLElements( + this.addressElement, + this.addressContainerElement + ); + } + } + get rawAddress() { + return this.props.address.id; + } + + get renderAddress() { + const { rawAddress } = this; + const { addressSlice } = this.props; + if (!addressSlice) return rawAddress; + const addressBegin = rawAddress.slice(0, addressSlice); + const addressEnd = rawAddress.slice(-addressSlice); + return `${addressBegin}…${addressEnd}`; + } + + render() { + const { + address, + onShareAddress, + onCopyAddress, + shouldRegisterAddressElement, + } = this.props; + const { renderAddress, rawAddress } = this; + const addressClasses = classnames([ + 'Address', + `receiveAddress-${rawAddress}`, + styles.component, + address.used ? styles.usedWalletAddress : null, + ]); + const addressIdClasses = classnames([styles.addressId]); + return ( +
+
{ + this.addressContainerElement = ref; + }} + id={`address-${rawAddress}`} + > + {renderAddress} + {shouldRegisterAddressElement && ( + { + this.addressElement = ref; + }} + className={styles.addressElement} + > + {rawAddress} + + )} +
+ +
+ ); + } +} diff --git a/source/renderer/app/components/wallet/receive/AddressSequential.scss b/source/renderer/app/components/wallet/receive/AddressSequential.scss new file mode 100644 index 0000000000..e82d28c487 --- /dev/null +++ b/source/renderer/app/components/wallet/receive/AddressSequential.scss @@ -0,0 +1,87 @@ +$bg1: var(--theme-bordered-box-background-color); +$bg0: rgba(0, 0, 0, 0); + +.component { + box-sizing: border-box; + display: flex; + justify-content: space-between; + padding: 10.5px 0; + word-break: break-all; + &.usedWalletAddress { + .addressId, + .shareIcon { + opacity: 0.4; + } + .shareAddressLabel { + opacity: 0.2; + } + } +} +.addressId { + cursor: default; + display: flex; + flex-direction: row; + flex-grow: 1; + font-family: var(--font-mono); + letter-spacing: -0.4px; + margin-right: 4px; + overflow: hidden; + position: relative; + user-select: none; + white-space: nowrap; + + .addressElement { + position: absolute; + visibility: hidden; + } + .ellipsis { + left: 50%; + margin-left: -5px; + position: absolute; + text-align: center; + width: 10px; + } + .addressIdBegin, + .addressIdEnd { + flex-shrink: 0; + max-width: 50%; + overflow: hidden; + position: relative; + white-space: nowrap; + &:after { + background: rgb(255, 255, 255); + bottom: 0; + position: absolute; + top: 0; + width: 15px; + } + } + .addressIdBegin { + &:after { + background: linear-gradient(90deg, $bg0 0%, $bg1 50%); + right: -1px; + } + } + .addressIdEnd { + direction: rtl; + &:after { + background: linear-gradient(90deg, $bg1 50%, $bg0 100%); + left: -1px; + } + } + &.ellipsisIsVisible { + .addressIdBegin, + .addressIdEnd { + &:after { + content: ''; + } + } + } +} + +.invalidAddress { + & > svg { + height: 12px; + width: 12px; + } +} diff --git a/source/renderer/app/components/wallet/receive/VirtualAddressesList.js b/source/renderer/app/components/wallet/receive/VirtualAddressesList.js index 4ed40c0d2c..44db3e8082 100644 --- a/source/renderer/app/components/wallet/receive/VirtualAddressesList.js +++ b/source/renderer/app/components/wallet/receive/VirtualAddressesList.js @@ -3,20 +3,14 @@ import React, { Component } from 'react'; import { throttle } from 'lodash'; import { observer } from 'mobx-react'; import { AutoSizer, List } from 'react-virtualized'; -import type { Addresses } from '../../../api/addresses/types'; +import WalletAddress from '../../../domains/WalletAddress'; import styles from './VirtualAddressesList.scss'; -/* eslint-disable react/no-unused-prop-types */ - type Props = { - rows: Addresses, + rows: Array, renderRow: Function, }; -type State = { - height: number, -}; - /** * * The breakpoints define the number of lines @@ -30,10 +24,10 @@ const BREAKPOINT_2_LINES = 635; const ADDRESS_LINE_HEIGHT = 22; const ADDRESS_LINE_PADDING = 21; -const ADDRESS_SELECTOR = '.Address_component'; +const ADDRESS_SELECTOR = '.Address'; @observer -export class VirtualAddressesList extends Component { +export class VirtualAddressesList extends Component { list: List; listWidth: number = 0; addressHeight: number = 0; @@ -56,14 +50,13 @@ export class VirtualAddressesList extends Component { /** * Virtual row heights only once per tick (debounced) */ - updateRowHeights = (): void => { + updateRowHeights = () => { const { list, addressHeight } = this; if (!list) return; const firstAddress = document.querySelector(ADDRESS_SELECTOR); if (firstAddress instanceof HTMLElement) { this.addressHeight = firstAddress.offsetHeight; } else { - // DOM is not ready yet, so use an estimated height this.addressHeight = this.estimateAddressHeight( this.getLinesFromWidth(this.listWidth) ); diff --git a/source/renderer/app/components/wallet/receive/VirtualAddressesList.scss b/source/renderer/app/components/wallet/receive/VirtualAddressesList.scss index 7e12b82827..14e4dd791e 100644 --- a/source/renderer/app/components/wallet/receive/VirtualAddressesList.scss +++ b/source/renderer/app/components/wallet/receive/VirtualAddressesList.scss @@ -9,8 +9,8 @@ } .list { - overflow-y: scroll; - padding-right: 15px; + overflow-x: hidden !important; + padding-right: 2px; will-change: none !important; &:focus { outline: 0; diff --git a/source/renderer/app/components/wallet/receive/WalletReceiveDialog.js b/source/renderer/app/components/wallet/receive/WalletReceiveDialog.js new file mode 100644 index 0000000000..5dc5f7db43 --- /dev/null +++ b/source/renderer/app/components/wallet/receive/WalletReceiveDialog.js @@ -0,0 +1,161 @@ +// @flow +import React, { Component } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, intlShape } from 'react-intl'; +import CopyToClipboard from 'react-copy-to-clipboard'; +import SVGInline from 'react-svg-inline'; +import { TextArea } from 'react-polymorph/lib/components/TextArea'; +import { TextAreaSkin } from 'react-polymorph/lib/skins/simple/TextAreaSkin'; +import QRCode from 'qrcode.react'; +import Dialog from '../../widgets/Dialog'; +import DialogCloseButton from '../../widgets/DialogCloseButton'; +import WalletAddress from '../../../domains/WalletAddress'; +import globalMessages from '../../../i18n/global-messages'; +import styles from './WalletReceiveDialog.scss'; +import ReactToolboxMobxForm from '../../../utils/ReactToolboxMobxForm'; +import iconCopy from '../../../assets/images/clipboard-ic.inline.svg'; + +const messages = defineMessages({ + inputLabel: { + id: 'wallet.receive.dialog.inputLabel', + defaultMessage: '!!!PDF note', + description: 'placeholder on the wallet "Share Address" dialog', + }, + inputPlaceholder: { + id: 'wallet.receive.dialog.inputPlaceholder', + defaultMessage: '!!!Add a note to the sender', + description: 'inputPlaceholder on the wallet "Share Address" dialog', + }, + downloadPDFButton: { + id: 'wallet.receive.dialog.downloadPDFButton', + defaultMessage: '!!!Download as PDF', + description: 'downloadPDFButton on the wallet "Share Address" dialog', + }, + dialogTitle: { + id: 'wallet.receive.dialog.dialogTitle', + defaultMessage: '!!!Share wallet address', + description: 'dialogTitle on the wallet "Share Address" dialog', + }, + copyAddressLabel: { + id: 'wallet.receive.dialog.copyAddressLabel', + defaultMessage: '!!!Copy address', + description: 'Label for "Copy address" link on the wallet "Receive page"', + }, +}); + +messages.fieldIsRequired = globalMessages.fieldIsRequired; + +type Props = { + address: WalletAddress, + onCopyAddress: Function, + onDownloadPDF: Function, + onClose: Function, +}; + +@observer +export default class WalletReceiveDialog extends Component { + static contextTypes = { + intl: intlShape.isRequired, + }; + + form = new ReactToolboxMobxForm({ + fields: { + noteInput: { + value: '', + label: this.context.intl.formatMessage(messages.inputLabel), + placeholder: this.context.intl.formatMessage(messages.inputPlaceholder), + }, + }, + }); + + submit = () => { + this.form.submit({ + onSuccess: form => { + const { noteInput } = form.values(); + const { onDownloadPDF, onClose } = this.props; + onDownloadPDF(noteInput); + onClose(); + }, + onError: err => { + throw new Error(err); + }, + }); + }; + + handleChange = (field: { value: string }) => { + field.value = field.value.replace(/\n/g, ''); + }; + + render() { + const { address, onCopyAddress, onClose } = this.props; + const { intl } = this.context; + const noteInputField = this.form.$('noteInput'); + + const actions = [ + { + className: 'downloadPDFButton', + label: intl.formatMessage(messages.downloadPDFButton), + onClick: this.submit, + primary: true, + }, + ]; + + // Get QRCode color value from active theme's CSS variable + const qrCodeBackgroundColor = document.documentElement + ? 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 ( + } + > +
+
+ +
+ +
{address.id}
+ + onCopyAddress(address.id)} + > + + + + {intl.formatMessage(messages.copyAddressLabel)} + + + + +