From 6dab755aca7c6070038f154b4b0f710d3fce9821 Mon Sep 17 00:00:00 2001 From: Vynnyk Dmytro Date: Wed, 22 May 2024 00:01:11 +0300 Subject: [PATCH] Release/1.10.0 (#990) * added listener for window closing (#752) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fix displaying amount in CSPR only for Casper (#753) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * UI changed based on a new design (#754) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fixed skeleton for NFT image and made small changes (#755) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * feature: add remembering scroll position in the NFT, Deploys and Tokens list (#759) * added remembering scroll position in the NFT list and removed background fetch for nft * added remembering scroll position in the NFT list and removed background fetch for nft * added remembering scroll position in the Deploys list, removed background fetch for deploys and made some improvements * small improvements * added remembering scroll position in the Casper token activity list and small improvements * added remembering scroll position in the ERC20 token activity list and small improvements * removed duplication and small fix * fixed infinity scroll for NFT * fixed infinity scroll for Deploys tab * fixed infinity scroll for Casper token activity * fixed infinity scroll for ERC20 token activity * unused infinite scroll hook removed * updated error handler for account deploys and NFT tokens request --------- Co-authored-by: ost-ptk * illustrations are updated (#762) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fix: fix native transfer in Firefox (#763) * added a check for deploy hash before the extended deploys info request * updated manifest permission for Firefox * removed comment --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * added tooltip on hover hash (#764) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * Redesign list of NFTs (#766) * Redesign list of NFTs * Fix PR issues and add useAsyncEffect hook * Fix issue with audio loading state * Fix issue with deploy timestamp (#768) * Release 1.5.2 version (#777) * feature: add NFT token transfer (#769) * added UI for NFT token transfer flow and business logic draft * added nft transfer logic * added fix for deploy timestamp issue * removed unused packages * removed back button from success screen --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fix issues with the layout of a deploy list (#772) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * build(deps): bump sqlite3 from 5.0.8 to 5.1.6 (#694) Bumps [sqlite3](https://github.com/TryGhost/node-sqlite3) from 5.0.8 to 5.1.6. - [Release notes](https://github.com/TryGhost/node-sqlite3/releases) - [Commits](https://github.com/TryGhost/node-sqlite3/compare/v5.0.8...v5.1.6) --- updated-dependencies: - dependency-name: sqlite3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump json5 from 1.0.1 to 1.0.2 (#695) Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump dns-packet from 5.3.1 to 5.6.0 (#696) Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 5.3.1 to 5.6.0. - [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md) - [Commits](https://github.com/mafintosh/dns-packet/compare/v5.3.1...v5.6.0) --- updated-dependencies: - dependency-name: dns-packet dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump xml2js and web-ext (#697) Bumps [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) to 0.5.0 and updates ancestor dependency [web-ext](https://github.com/mozilla/web-ext). These dependencies need to be updated together. Updates `xml2js` from 0.4.23 to 0.5.0 - [Commits](https://github.com/Leonidas-from-XIV/node-xml2js/commits/0.5.0) Updates `web-ext` from 7.5.0 to 7.6.2 - [Release notes](https://github.com/mozilla/web-ext/releases) - [Commits](https://github.com/mozilla/web-ext/compare/7.5.0...7.6.2) --- updated-dependencies: - dependency-name: xml2js dependency-type: indirect - dependency-name: web-ext dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Dmytro Vynnyk * build(deps-dev): bump eslint-plugin-jsx-a11y from 6.6.1 to 6.7.1 (#703) Bumps [eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) from 6.6.1 to 6.7.1. - [Release notes](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/releases) - [Changelog](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/CHANGELOG.md) - [Commits](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/compare/v6.6.1...v6.7.1) --- updated-dependencies: - dependency-name: eslint-plugin-jsx-a11y dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump fs-extra from 11.1.0 to 11.1.1 (#705) Bumps [fs-extra](https://github.com/jprichardson/node-fs-extra) from 11.1.0 to 11.1.1. - [Changelog](https://github.com/jprichardson/node-fs-extra/blob/master/CHANGELOG.md) - [Commits](https://github.com/jprichardson/node-fs-extra/compare/11.1.0...11.1.1) --- updated-dependencies: - dependency-name: fs-extra dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump i18next-conv from 13.1.0 to 14.0.0 (#706) Bumps [i18next-conv](https://github.com/i18next/i18next-gettext-converter) from 13.1.0 to 14.0.0. - [Release notes](https://github.com/i18next/i18next-gettext-converter/releases) - [Changelog](https://github.com/i18next/i18next-gettext-converter/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next-gettext-converter/compare/v13.1.0...v14.0.0) --- updated-dependencies: - dependency-name: i18next-conv dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps): bump word-wrap from 1.2.3 to 1.2.4 (#731) Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump apollo-server-core from 3.11.1 to 3.12.1 (#765) Bumps [apollo-server-core](https://github.com/apollographql/apollo-server/tree/HEAD/packages/apollo-server-core) from 3.11.1 to 3.12.1. - [Release notes](https://github.com/apollographql/apollo-server/releases) - [Commits](https://github.com/apollographql/apollo-server/commits/apollo-server-core@3.12.1/packages/apollo-server-core) --- updated-dependencies: - dependency-name: apollo-server-core dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump i18next from 21.10.0 to 23.5.1 (#773) Bumps [i18next](https://github.com/i18next/i18next) from 21.10.0 to 23.5.1. - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v21.10.0...v23.5.1) --- updated-dependencies: - dependency-name: i18next dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * fix account status indicator (#775) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump eslint from 8.28.0 to 8.49.0 (#776) Bumps [eslint](https://github.com/eslint/eslint) from 8.28.0 to 8.49.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v8.28.0...v8.49.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps): bump got and @redux-devtools/cli (#780) Bumps [got](https://github.com/sindresorhus/got) to 11.8.6 and updates ancestor dependency [@redux-devtools/cli](https://github.com/reduxjs/redux-devtools). These dependencies need to be updated together. Updates `got` from 9.6.0 to 11.8.6 - [Release notes](https://github.com/sindresorhus/got/releases) - [Commits](https://github.com/sindresorhus/got/compare/v9.6.0...v11.8.6) Updates `@redux-devtools/cli` from 1.0.7 to 3.0.1 - [Release notes](https://github.com/reduxjs/redux-devtools/releases) - [Commits](https://github.com/reduxjs/redux-devtools/compare/@redux-devtools/cli@1.0.7...v3.0.1) --- updated-dependencies: - dependency-name: got dependency-type: indirect - dependency-name: "@redux-devtools/cli" dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps): bump react-player from 2.12.0 to 2.13.0 (#785) Bumps [react-player](https://github.com/CookPete/react-player) from 2.12.0 to 2.13.0. - [Changelog](https://github.com/cookpete/react-player/blob/master/CHANGELOG.md) - [Commits](https://github.com/CookPete/react-player/compare/v2.12.0...v2.13.0) --- updated-dependencies: - dependency-name: react-player dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump prettier from 2.8.0 to 3.0.3 (#786) Bumps [prettier](https://github.com/prettier/prettier) from 2.8.0 to 3.0.3. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/2.8.0...3.0.3) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump redux from 4.2.0 to 4.2.1 (#787) Bumps [redux](https://github.com/reduxjs/redux) from 4.2.0 to 4.2.1. - [Release notes](https://github.com/reduxjs/redux/releases) - [Changelog](https://github.com/reduxjs/redux/blob/master/CHANGELOG.md) - [Commits](https://github.com/reduxjs/redux/compare/v4.2.0...v4.2.1) --- updated-dependencies: - dependency-name: redux dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump webpack-dev-server from 4.11.1 to 4.15.1 (#788) Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.11.1 to 4.15.1. - [Release notes](https://github.com/webpack/webpack-dev-server/releases) - [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.11.1...v4.15.1) --- updated-dependencies: - dependency-name: webpack-dev-server dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @types/chrome from 0.0.203 to 0.0.246 (#791) Bumps [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) from 0.0.203 to 0.0.246. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome) --- updated-dependencies: - dependency-name: "@types/chrome" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * added message for not supporting reverse look-up modality (#783) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * added a placeholder for the contract logo (#784) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fix: fix issue with prettier config (#792) * fixed issue with prettier config * fixed code style errors --------- Co-authored-by: ost-ptk * added new UI for erc-20 action displaying (#790) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump geckodriver from 3.2.0 to 4.2.1 (#793) Bumps [geckodriver](https://github.com/webdriverio-community/node-geckodriver) from 3.2.0 to 4.2.1. - [Release notes](https://github.com/webdriverio-community/node-geckodriver/releases) - [Changelog](https://github.com/webdriverio-community/node-geckodriver/blob/main/CHANGELOG.md) - [Commits](https://github.com/webdriverio-community/node-geckodriver/compare/v3.2.0...v4.2.1) --- updated-dependencies: - dependency-name: geckodriver dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump react-query from 3.39.2 to 3.39.3 (#794) Bumps [react-query](https://github.com/tannerlinsley/react-query) from 3.39.2 to 3.39.3. - [Release notes](https://github.com/tannerlinsley/react-query/releases) - [Commits](https://github.com/tannerlinsley/react-query/commits) --- updated-dependencies: - dependency-name: react-query dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps): bump react-router-dom from 6.4.4 to 6.16.0 (#795) Bumps [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom) from 6.4.4 to 6.16.0. - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.16.0/packages/react-router-dom) --- updated-dependencies: - dependency-name: react-router-dom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps): bump @scure/bip39 from 1.1.0 to 1.2.1 (#796) Bumps [@scure/bip39](https://github.com/paulmillr/scure-bip39) from 1.1.0 to 1.2.1. - [Release notes](https://github.com/paulmillr/scure-bip39/releases) - [Commits](https://github.com/paulmillr/scure-bip39/compare/1.1.0...1.2.1) --- updated-dependencies: - dependency-name: "@scure/bip39" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump electron from 25.8.0 to 25.8.1 (#798) Bumps [electron](https://github.com/electron/electron) from 25.8.0 to 25.8.1. - [Release notes](https://github.com/electron/electron/releases) - [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md) - [Commits](https://github.com/electron/electron/compare/v25.8.0...v25.8.1) --- updated-dependencies: - dependency-name: electron dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump lint-staged from 13.0.4 to 14.0.1 (#800) Bumps [lint-staged](https://github.com/okonet/lint-staged) from 13.0.4 to 14.0.1. - [Release notes](https://github.com/okonet/lint-staged/releases) - [Changelog](https://github.com/okonet/lint-staged/blob/master/CHANGELOG.md) - [Commits](https://github.com/okonet/lint-staged/compare/v13.0.4...v14.0.1) --- updated-dependencies: - dependency-name: lint-staged dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump tsconfig-paths-webpack-plugin from 4.0.0 to 4.1.0 (#801) Bumps [tsconfig-paths-webpack-plugin](https://github.com/dividab/tsconfig-paths-webpack-plugin) from 4.0.0 to 4.1.0. - [Changelog](https://github.com/dividab/tsconfig-paths-webpack-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/dividab/tsconfig-paths-webpack-plugin/compare/v4.0.0...v4.1.0) --- updated-dependencies: - dependency-name: tsconfig-paths-webpack-plugin dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump @types/facepaint from 1.2.2 to 1.2.3 (#803) Bumps [@types/facepaint](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/facepaint) from 1.2.2 to 1.2.3. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/facepaint) --- updated-dependencies: - dependency-name: "@types/facepaint" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump webpack-cli from 5.0.0 to 5.1.4 (#804) Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 5.0.0 to 5.1.4. - [Release notes](https://github.com/webpack/webpack-cli/releases) - [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@5.0.0...webpack-cli@5.1.4) --- updated-dependencies: - dependency-name: webpack-cli dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * removed input type from the transaction fee fields (#807) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * changed method of creating Keys for signing (#808) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fix: fix scope of bugs (#809) * fixed issue with transfer NFT error after wallet unlocking and some improvements * fixed issue with empty NFT details page after wallet unlocking and some improvements * fixed issue with home screen scroll * possible fix for an issue with a gap above the sticky tabs container * fixed an issue with the scrollable Import account screen --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fixed issue with the navigation menu screen scroll (#818) Co-authored-by: ost-ptk * casper logo updated (#817) Co-authored-by: ost-ptk * removed the tooltip with the full public key when the user has only one account (#819) Co-authored-by: ost-ptk * build(deps-dev): bump electron from 25.8.1 to 25.9.0 (#822) Bumps [electron](https://github.com/electron/electron) from 25.8.1 to 25.9.0. - [Release notes](https://github.com/electron/electron/releases) - [Changelog](https://github.com/electron/electron/blob/main/docs/breaking-changes.md) - [Commits](https://github.com/electron/electron/compare/v25.8.1...v25.9.0) --- updated-dependencies: - dependency-name: electron dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * disabled send button on NFT details page after this NFT was sent (#824) Co-authored-by: ost-ptk * fixed issue with the scroll on home page (#825) Co-authored-by: ost-ptk * Release 1.6.0 version (#826) * added countdown for user password length (#806) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * feature: add change password page (#799) * fixed issue with prettier config * fixed code style errors * added change password page and small refactor * merge conflict fixed * merge conflict issues fixed --------- Co-authored-by: ost-ptk * added a checkbox for setting the max transfer amount for CSPR transfer and some refactoring (#805) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fixed issue with words highlighting in the confirm secret phrase view (#823) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * feature: add dark mode (#821) * added dark mode * merge conflict issues fixed * changed png images to svg * Fix DefaultTheme interface --------- Co-authored-by: ost-ptk Co-authored-by: Dmytro Vynnyk * build(deps): bump @scure/bip32 from 1.1.1 to 1.3.2 (#812) Bumps [@scure/bip32](https://github.com/paulmillr/scure-bip32) from 1.1.1 to 1.3.2. - [Release notes](https://github.com/paulmillr/scure-bip32/releases) - [Commits](https://github.com/paulmillr/scure-bip32/compare/1.1.1...1.3.2) --- updated-dependencies: - dependency-name: "@scure/bip32" dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump webpack from 5.76.0 to 5.88.2 (#813) Bumps [webpack](https://github.com/webpack/webpack) from 5.76.0 to 5.88.2. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.76.0...v5.88.2) --- updated-dependencies: - dependency-name: webpack dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump eslint-plugin-react from 7.31.11 to 7.33.2 (#814) Bumps [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) from 7.31.11 to 7.33.2. - [Release notes](https://github.com/jsx-eslint/eslint-plugin-react/releases) - [Changelog](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/CHANGELOG.md) - [Commits](https://github.com/jsx-eslint/eslint-plugin-react/compare/v7.31.11...v7.33.2) --- updated-dependencies: - dependency-name: eslint-plugin-react dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @babel/preset-env from 7.20.2 to 7.23.2 (#828) Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.20.2 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-preset-env) --- updated-dependencies: - dependency-name: "@babel/preset-env" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Improve skeleton for dark mode (#835) * Release 1.6.1 version (#837) * fixed issue with insufficient data for displaying erc-20 tokens (#840) Co-authored-by: ost-ptk * Update README.md to add reference to CSPR.click for integration (#842) * Improve erc20 tokens fetching (#843) * Release/1.6.2 (#844) * Improve erc20 tokens fetching * Release 1.6.2 version * feature: add QR code option to the settings menu (#774) * added QR code option to the settings menu * added QR generation page and removed unused props * merge conflict issue fixed * temp commit * Get symbol and decimal optional * Add qr code generation for sync wallet * UI and UX fixes, and fixed validation rule * moved qr generation under password protection and removed one time password * Improve qrCode generation * Add multi qr code presenting --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * Release 1.6.3 version (#852) * build(deps-dev): bump ts-jest from 29.0.3 to 29.1.1 (#831) Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 29.0.3 to 29.1.1. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.0.3...v29.1.1) --- updated-dependencies: - dependency-name: ts-jest dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump redux-saga from 1.2.1 to 1.2.3 (#832) Bumps [redux-saga](https://github.com/redux-saga/redux-saga) from 1.2.1 to 1.2.3. - [Release notes](https://github.com/redux-saga/redux-saga/releases) - [Changelog](https://github.com/redux-saga/redux-saga/blob/main/CHANGELOG.md) - [Commits](https://github.com/redux-saga/redux-saga/compare/redux-saga@1.2.1...redux-saga@1.2.3) --- updated-dependencies: - dependency-name: redux-saga dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump css-loader from 6.7.2 to 6.8.1 (#833) Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.7.2 to 6.8.1. - [Release notes](https://github.com/webpack-contrib/css-loader/releases) - [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.7.2...v6.8.1) --- updated-dependencies: - dependency-name: css-loader dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feature: add delegation and undelegation flow (#846) * added delegation and undelegation flow * fixed issue with sticky header in tabs * added a new modal window with buttons on the home page * added empty state UI for undelegation * fixed deploys tab and deploys details page for staking --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * added active address in top nav (#847) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fixed issue with an open modal window (#848) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump @babel/core from 7.20.5 to 7.23.3 (#850) Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.20.5 to 7.23.3. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.3/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @babel/traverse from 7.20.5 to 7.23.3 (#853) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.20.5 to 7.23.3. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.3/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump axios from 1.1.3 to 1.6.1 (#854) Bumps [axios](https://github.com/axios/axios) from 1.1.3 to 1.6.1. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.1.3...v1.6.1) --- updated-dependencies: - dependency-name: axios dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump i18next-http-backend from 1.4.5 to 2.4.1 (#856) Bumps [i18next-http-backend](https://github.com/i18next/i18next-http-backend) from 1.4.5 to 2.4.1. - [Changelog](https://github.com/i18next/i18next-http-backend/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next-http-backend/compare/v1.4.5...v2.4.1) --- updated-dependencies: - dependency-name: i18next-http-backend dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump chromedriver from 107.0.3 to 119.0.1 (#851) Bumps [chromedriver](https://github.com/giggio/node-chromedriver) from 107.0.3 to 119.0.1. - [Commits](https://github.com/giggio/node-chromedriver/compare/107.0.3...119.0.1) --- updated-dependencies: - dependency-name: chromedriver dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Release 1.7.0 version (#864) * feature: add e2e test with playwright (#849) * added new e2e framework and CI config * change test dir * removed test example * added onboarding tests and changed CI script * removed CI scripts for selenium tests * fixed CI script and removed unused function * removed unused import * CI config improvements * added onboarding tests for firefox * increase workers for CI tests * added docker container for CI tests * fixing flaky test and removed docker container form CI * added headless mode for tests * updated PR template and fixed whitespaces in the CI config file * clean up playwright config * added a few e2e tests for popup * added a few e2e tests for popup * added CI config for popup tests * updated CI config for popup tests * skipped all tests except import account * fixing import account test * fixing import account test * fixing import account test * fixing import account test * fixing import account test * fixing import account test * fixing import account test * fixing import account test * fixing tests on CI * use config context * testing * use config context * testing * testing * testing * added docker container to CI config * testing * testing * testing * testing * testing * testing * testing * testing * testing * testing * testing * testing * testing * testing * enabled connect account test * enabled all popup tests * removed Firefox test from CI * cleanup * added container to CI config for onboarding tests * cleanup * testing * added signature tests for popup * added a few new tests * fix import issue * fixed issues with truncated keys * fixed test issue * updated playwright config * removed old e2e config and unused packages * added a new test and minor fixes * minor fix --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * build(deps-dev): bump html-webpack-plugin from 5.5.0 to 5.5.3 (#858) Bumps [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) from 5.5.0 to 5.5.3. - [Release notes](https://github.com/jantimon/html-webpack-plugin/releases) - [Changelog](https://github.com/jantimon/html-webpack-plugin/blob/main/CHANGELOG.md) - [Commits](https://github.com/jantimon/html-webpack-plugin/compare/v5.5.0...v5.5.3) --- updated-dependencies: - dependency-name: html-webpack-plugin dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump prettier from 3.0.3 to 3.1.0 (#859) Bumps [prettier](https://github.com/prettier/prettier) from 3.0.3 to 3.1.0. - [Release notes](https://github.com/prettier/prettier/releases) - [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md) - [Commits](https://github.com/prettier/prettier/compare/3.0.3...3.1.0) --- updated-dependencies: - dependency-name: prettier dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vynnyk Dmytro * build(deps): bump react-loading-skeleton from 3.1.0 to 3.3.1 (#860) Bumps [react-loading-skeleton](https://github.com/dvtng/react-loading-skeleton) from 3.1.0 to 3.3.1. - [Release notes](https://github.com/dvtng/react-loading-skeleton/releases) - [Changelog](https://github.com/dvtng/react-loading-skeleton/blob/master/CHANGELOG.md) - [Commits](https://github.com/dvtng/react-loading-skeleton/compare/v3.1.0...v3.3.1) --- updated-dependencies: - dependency-name: react-loading-skeleton dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @adobe/css-tools from 4.0.1 to 4.3.1 (#865) Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.0.1 to 4.3.1. - [Changelog](https://github.com/adobe/css-tools/blob/main/History.md) - [Commits](https://github.com/adobe/css-tools/commits) --- updated-dependencies: - dependency-name: "@adobe/css-tools" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * added error handling for transfer and stakes (#867) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fix: fix wallet setup kick-off automatically in the Reset Wallet flow (#863) * fixed issue with auto launching onboarding after wallet reset * fixed prettier issues --------- Co-authored-by: ost-ptk * fix: fix truncated public key size (#868) * fixed issue with truncated public key size * fixed prettier issue --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fix: fix the moon icon (#869) * updated the moon icon * updated the moon icon * fixed issue with same keys --------- Co-authored-by: ost-ptk * spacing between buttons was fixed (#870) Co-authored-by: ost-ptk * build(deps): bump react-hook-form from 7.40.0 to 7.48.2 (#871) Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.40.0 to 7.48.2. - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.40.0...v7.48.2) --- updated-dependencies: - dependency-name: react-hook-form dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump web-ext from 7.6.2 to 7.8.0 (#874) Bumps [web-ext](https://github.com/mozilla/web-ext) from 7.6.2 to 7.8.0. - [Release notes](https://github.com/mozilla/web-ext/releases) - [Commits](https://github.com/mozilla/web-ext/compare/7.6.2...7.8.0) --- updated-dependencies: - dependency-name: web-ext dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @adobe/css-tools from 4.3.1 to 4.3.2 (#875) Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.3.1 to 4.3.2. - [Changelog](https://github.com/adobe/css-tools/blob/main/History.md) - [Commits](https://github.com/adobe/css-tools/commits) --- updated-dependencies: - dependency-name: "@adobe/css-tools" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Release/1.7.1 (#881) * removed the button from the footer and fixed indent * Release 1.7.1 version --------- Co-authored-by: ost-ptk * feature: add contact book to the wallet (#878) * added contact book to the wallet * added contact sort and fixed the issue with updating contact --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * showed user stake amount for validator on undelegate page (#879) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * added new UI for account switcher (#883) Co-authored-by: ost-ptk * updated header UI (#884) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * updated the theme switcher (#885) Co-authored-by: ost-ptk * feature: merge recent recipients with contacts list (#886) * added contact book to the wallet * added contact sort and fixed the issue with updating contact * merged recent recipient with contacts * list height fix --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fix: fix issue with empty contact screen (#888) * fixed issue with not trimmed names for contacts * minor fix * minor code improvements --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fix: fix contact list preserving after the wallet reset (#887) * added contact book to the wallet * added contact sort and fixed the issue with updating contact * merged recent recipient with contacts * list height fix * added reset actions for vault settings, contacts and recent recipient --------- Co-authored-by: ost-ptk * opening of the account import window has been updated (#889) Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * fixed an issue with a scrolled-down window in Firefox (#892) Co-authored-by: ost-ptk * fixed validation issue in recipient dropdown input (#893) Co-authored-by: ost-ptk * updated UI for recipient dropdown input (#894) Co-authored-by: ost-ptk * removed the System option for Safari from the theme switcher (#895) Co-authored-by: ost-ptk * changed 'Contacts list' to 'Contacts' (#896) Co-authored-by: ost-ptk * removed the 'max delegator' rule for the validator field from the Undelegate flow (#897) Co-authored-by: ost-ptk * removed hover tooltip from the public key in the account switcher (#898) Co-authored-by: ost-ptk * feature: add unlock wallet animation (#640) * Added animations lib and lottie dependency Added new unlock animation in the unlock vault view * Changed animation layout * Fixed review comments * Updated unlock animation to the linked animation file. * Optimized build all script * Optimized layout * Optimized animation * fix merge conflict issues * fixed version in package lock * updated unlock animation * updated unlock e2e test --------- Co-authored-by: ost-ptk Co-authored-by: Ostap Piatkovskyi <44294945+ost-ptk@users.noreply.github.com> Co-authored-by: Dmytro Vynnyk * Release 1.8.0 version (#900) * build(deps): bump postcss and web-ext (#899) Bumps [postcss](https://github.com/postcss/postcss) to 8.4.32 and updates ancestor dependency [web-ext](https://github.com/mozilla/web-ext). These dependencies need to be updated together. Updates `postcss` from 8.4.29 to 8.4.32 - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.29...8.4.32) Updates `web-ext` from 7.8.0 to 7.9.0 - [Release notes](https://github.com/mozilla/web-ext/releases) - [Commits](https://github.com/mozilla/web-ext/compare/7.8.0...7.9.0) --- updated-dependencies: - dependency-name: postcss dependency-type: indirect - dependency-name: web-ext dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feature: create tests for Contacts (#903) * added tests for Contacts * test description fixed --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * displaying validators icon fixed (#905) Co-authored-by: ost-ptk * build(deps-dev): bump follow-redirects from 1.15.0 to 1.15.4 (#907) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.0 to 1.15.4. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.0...v1.15.4) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feature: update eslint config (#906) * added sort imports and removed unused files * fixed eslint errors * content URLs fixed * code refactor to fix eslint error * Refactor code to fix ESLint errors and add new import rules --------- Co-authored-by: ost-ptk * feature: sign view improvements (#908) * Add contract hash and name to deploy details * Remove account switcher from PopupHeader in signature request page --------- Co-authored-by: ost-ptk * moved the timeout section to the security block (#909) Co-authored-by: ost-ptk * added a disabled attribute to the unlock button (#910) Co-authored-by: ost-ptk * fixed UI bugs (#911) Co-authored-by: ost-ptk * Add support for NFT transfers (#912) The update adds a new activity type called 'TransferNFT' to handle NFT transfers. Now, any transaction case labeled with 'Transfer' will be processed correctly. Also, the naming standard in constants was updated for different activity types from 'Mint' and 'Burn' to 'Mint NFT' and 'Burn NFT' respectively to better reflect the actions. Co-authored-by: ost-ptk * updated illustration for the contact deleting page (#913) Co-authored-by: ost-ptk * feature: show staked balance on home page (#917) * added staked balance to home page * Update account balance fetching and add new hosts * Replace CasperCloudApiUrl with CasperWalletApiUrl in settings and services --------- Co-authored-by: ost-ptk * Add redelegation feature and enhance form validation rules (#916) Co-authored-by: ost-ptk * feature: update secret phrase validation and recovery for 12 and 24 words (#918) * Update secret phrase validation and recovery for 12 and 24 words * Update e2e tests to include 12-words secret phrase recovery * Update CORS to fix issue for Firefox --------- Co-authored-by: ost-ptk * Update a success message for NFT transfer (#919) Co-authored-by: ost-ptk * Update warning color in theme configuration (#920) Co-authored-by: ost-ptk * fix account hash hint is overlapped by loading animation of stakes balance (#921) Co-authored-by: ost-ptk * fix screen twitch upon clicking on recipient's field (#922) Co-authored-by: ost-ptk * Fix checkbox behavior in transfer amount step (#923) Co-authored-by: ost-ptk * Release 1.8.1 version (#926) * fix: fix issue with redux devtools and clean up all other issues (#924) * Set Content Security Policy for Safari dynamically and update it for Chrome and Firefox * Update casper-js-sdk version and refactor Redux devtools * Wrap localStorage data retrieval in try-catch block --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * Add case-insensitive account matching utility function (#925) Co-authored-by: ost-ptk * feature: update 'Sign:Response' with additional error handling (#929) * Update 'Sign:Response' with additional error handling * Export isEqualCaseInsensitive util and optimize message property --------- Co-authored-by: ost-ptk * feature: update package versions and remove unused dependencies (#931) * Update package versions and remove unused dependencies * Update testing-library/jest-dom import paths --------- Co-authored-by: ost-ptk * feature: add rate app (#933) * create a rate app page and functionality * Add review request after successful transaction --------- Co-authored-by: ost-ptk Co-authored-by: Vynnyk Dmytro * feature: add support for importing from .cer file (#934) * add support for importing from .cer file * Add support to import .cer account in e2e tests * rename secret key files for tests --------- Co-authored-by: ost-ptk * Update casper-js-sdk version (#935) Co-authored-by: ost-ptk * refactor the validator loading logic on staking and add spinner (#937) * feature: improve error handling (#936) * Improve error handling for transfers * Improve error handling in stake operation * Improve error handling for transfer nft * Improve error handling in staking and NFT transfer actions * Fix native transfer --------- Co-authored-by: ost-ptk * feature: update URLs for CasperWallet API endpoints (#938) * Improve error handling in staking and NFT transfer actions * Update URLs for CasperWallet API endpoints The URLs for both the mainnet and testnet CasperWallet API endpoints have been updated in various files, including "constants.ts", "declarative_net_request_rules.json", "manifest.v2.json", "manifest.v2.safari.json", "manifest.v3.json", "utils.ts", and "webpack.config.js". No changes were made to error handling in staking or NFT transfer actions as the original commit message suggested. The update was a necessary adjustment to ensure that the application connects to the correct API endpoints. * Add support for JPG format in token icons (#949) * fix: add validation for the transaction fee field (#950) * Add validation for transaction fee field * Update UI to handle large balance and name in account plate and fix id for token activity list * fix: fix issue with big numbers (#951) * Fix issue with big numbers * update UI to handle large value amount in activity details page and activity plate * Update validation message (#939) * Refactor file upload input accept attribute (#940) * Update formatting for an amount input value (#941) * Corrected review request logic (#943) * fix stake action texts (#944) * build(deps-dev): bump ip from 1.1.8 to 1.1.9 (#945) Bumps [ip](https://github.com/indutny/node-ip) from 1.1.8 to 1.1.9. - [Commits](https://github.com/indutny/node-ip/compare/v1.1.8...v1.1.9) --- updated-dependencies: - dependency-name: ip dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: add handle for large fiat amount (#947) * Refactor the `motesToCurrency` function to handle large values in `formatters.ts`. Previously, the function would return large numbers directly but now, if the calculated amount equals or surpasses 10^9, it returns an empty string. This is a temporary solution, a clarification on future behavior for handling large amounts is still needed. * Update handling and formatting of large values * fix: add notification for accounts that try to undelegate with no liquid balance (#957) * Add error for staking when a user has not enough balances to cover min amount and transaction fee * update CSPR error UI in transfer flow and checkbox cursor behavior * Add support for hidden accounts and improve account listing (#953) Co-authored-by: Vynnyk Dmytro * Add translate property to Typography component (#960) * build(deps-dev): bump follow-redirects from 1.15.4 to 1.15.6 (#959) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.4 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.4...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feature: add account balance to account switcher and all accounts list (#958) * Add support for hidden accounts and improve account listing * Add account balance to account switcher and all accounts list * Update transfer and deploy functionality, refactor code (#961) The changes centralize all deploy-making methods into the deployer service to improve maintainability and anticipate future modifications. Asynchronous calls replace synchronous for improved performance, addressing the casper-node synchronization issue by fetching the timestamp directly from the node instead of the date library. * feature: add Torus Wallet account import feature (#964) * Add Torus Wallet account import feature * Improve validation of Torus Wallet secret key imports * Update Torus Wallet secret and public keys for E2E tests * Replace ERC20 icon with CEP18 contract icon in UI components (#962) * build(deps-dev): bump webpack-dev-middleware from 5.3.1 to 5.3.4 (#963) Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.1 to 5.3.4. - [Release notes](https://github.com/webpack/webpack-dev-middleware/releases) - [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.1...v5.3.4) --- updated-dependencies: - dependency-name: webpack-dev-middleware dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feature: add functionality for buying CSPR from within the extension (#966) * Add functionality for buying CSPR from within the extension * Reorder illustration and text in no-provider page * fix: fix UI glitch after create account (#967) * Separate account balances from account data in Redux * Revert minimum password length --------- Co-authored-by: Vynnyk Dmytro * fix: Update event handling for numeric input fields (#968) * Update event handling for numeric input fields * Remove unused import * fix: Adjust error handling and introduce node status URL (#969) * Adjust error handling and introduce node status URL * Update URL references and deployments to match updated Node API * Update condition for composeEnhancers in Redux * Remove unused import in transfer page * Add functionality to download account keys (#970) * fix: fix crash on onboarding flow (#971) * Update all i18next packages and code to new style * Fix issue with removeChild on node --------- Co-authored-by: Dmytro Vynnyk * build(deps-dev): bump markdownlint from 0.26.2 to 0.34.0 (#972) Bumps [markdownlint](https://github.com/DavidAnson/markdownlint) from 0.26.2 to 0.34.0. - [Changelog](https://github.com/DavidAnson/markdownlint/blob/main/CHANGELOG.md) - [Commits](https://github.com/DavidAnson/markdownlint/compare/v0.26.2...v0.34.0) --- updated-dependencies: - dependency-name: markdownlint dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: fix sending deploys for Safari (#974) * Fix sending deploys for Safari * Add error handling for failed deploy submissions * Replace Link components with window.open for external URLs (#975) * Add e2e tests for buying CSPR and make minor code adjustments (#976) * Release 1.9.0 version (#978) * fix: Disable Redux devtools in development mode (#977) * Disable Redux devtools in development mode * Update WebSocket URL in webpack.config.js * fix issue with infinity connection to WebSocket for devtools * Release 1.9.1 version (#980) * Improve handling of onboarding URL navigation (#983) * feature: ledger integration (#955) * Add connection ledger UI and update signing flow for using the ledger * Add animation for ledger, refactor code for dark mode * Remove unused imports in avatar and ledger accounts list * Update cursor behavior and colors for disabled checkboxes The code modifies the Cursor behavior and colors for disabled checkboxes and buttons. It also introduces a check to prevent duplicate Ledger accounts from being connected by deactivating the selection and changing the cursor to 'not-allowed' when an account is already connected. The checkbox colors have been adjusted accordingly. * Adjust spinner animation colors and tidy up layout styling * Update how imported account visibility is checked in tests Replaced the method of checking the visibility of imported accounts in e2e tests. Instead of searching for the exact text 'Imported', the test now looks for a specific dataTestId ('import-account-icon'). Also, added the 'import-account-icon' dataTestId to the SvgIcon component in the hash.tsx file. * Update SVG illustration for ledger * Add Ledger integration business logic * Update Ledger integration UI * Refactor account import functionality from ledger (#984) * Update maxHeight property in account-list component (#985) * Add useEffect to scrollToTop in Ledger error components (#986) * Remove lodash.debounce (#987) * Handle issue with browser's device permission window * Update global styles and layout parameters This commit includes a change in the global styles file to allow elements to display in a flex layout and centrally align their content. In addition, modifications have been made to the layout-window file to ensure elements take up 100% width, enhancing the app's layout flexibility. * Fix small issues --------- Co-authored-by: Dmytro Vynnyk * Release 1.10.0 version --------- Signed-off-by: dependabot[bot] Co-authored-by: Ostap Piatkovskyi <44294945+ost-ptk@users.noreply.github.com> Co-authored-by: ost-ptk Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Muhammet Kara Co-authored-by: Piotr Witek <739075+piotrwitek@users.noreply.github.com> --- .../import-account-with-file.spec.ts | 4 +- .../import-torus-account.spec.ts | 2 +- package-lock.json | 231 +++- package.json | 7 +- src/apps/popup/app-router.tsx | 10 + .../popup/pages/account-settings/content.tsx | 9 +- .../popup/pages/connected-sites/index.tsx | 3 +- .../pages/connected-sites/site-group-item.tsx | 8 +- .../components/account-list-item.tsx | 5 +- .../connected-ledger.tsx | 247 ++++ .../import-account-from-ledger/index.tsx | 48 + .../ledger-accounts-list.tsx | 403 ++++++ .../pages/import-account-from-ledger/types.ts | 6 + .../popup/pages/navigation-menu/index.tsx | 23 +- .../sign-with-ledger-in-new-window/index.tsx | 88 ++ .../success-view.tsx | 59 + src/apps/popup/pages/stakes/content.tsx | 173 ++- src/apps/popup/pages/stakes/index.tsx | 219 ++- src/apps/popup/pages/transfer-nft/index.tsx | 233 +++- src/apps/popup/pages/transfer/content.tsx | 77 +- src/apps/popup/pages/transfer/index.tsx | 242 +++- src/apps/popup/pages/transfer/utils.ts | 1 + src/apps/popup/router/paths.ts | 4 +- .../pages/sign-deploy/index.tsx | 174 ++- .../pages/sign-message/index.tsx | 171 ++- src/assets/icons/ledger-blue.svg | 3 + src/assets/icons/ledger-white.svg | 4 + src/assets/illustrations/ledger-connect.svg | 1211 +++++++++++++++++ src/assets/illustrations/ledger-error.svg | 711 ++++++++++ .../illustrations/ledger-not-connected.svg | 1170 ++++++++++++++++ src/assets/illustrations/ledger-rejected.svg | 442 ++++++ src/background/create-open-window.ts | 41 + src/background/index.ts | 12 + src/background/open-onboarding-flow.ts | 15 +- src/background/redux/get-main-store.ts | 3 +- src/background/redux/ledger/actions.ts | 12 + src/background/redux/ledger/reducer.ts | 42 + src/background/redux/ledger/selectors.ts | 11 + src/background/redux/ledger/types.ts | 5 + src/background/redux/redux-action.ts | 4 +- src/background/redux/root-reducer.ts | 4 +- src/background/redux/root-selector.ts | 1 + src/background/redux/sagas/vault-sagas.ts | 2 + src/background/redux/types.d.ts | 2 + src/background/redux/vault/actions.ts | 5 + src/background/redux/vault/reducer.ts | 24 +- src/background/redux/vault/selectors.ts | 29 +- src/constants.ts | 4 + src/fixtures/initial-state-for-popup-tests.ts | 5 + src/hooks/use-is-dark-mode.ts | 16 + src/hooks/use-ledger.ts | 233 ++++ src/libs/animations/dots_dark_mode.json | 284 ++++ src/libs/animations/dots_light_mode.json | 284 ++++ src/libs/animations/spinner_dark_mode.json | 491 +++++++ src/libs/animations/spinner_light_mode.json | 491 +++++++ .../header/header-submenu-bar-nav-link.tsx | 4 +- src/libs/layout/layout-window.tsx | 1 + src/libs/layout/unlock-vault/index.tsx | 2 +- src/libs/services/deployer-service/index.ts | 71 +- src/libs/services/ledger/errors.ts | 78 ++ src/libs/services/ledger/index.ts | 4 + src/libs/services/ledger/ledger.ts | 562 ++++++++ src/libs/services/ledger/transport.ts | 96 ++ src/libs/services/ledger/types.ts | 55 + src/libs/types/account.ts | 8 +- .../account-list/account-list-item.tsx | 5 +- .../components/account-list/account-list.tsx | 42 +- src/libs/ui/components/avatar/avatar.tsx | 15 +- src/libs/ui/components/checkbox/checkbox.tsx | 7 +- src/libs/ui/components/hash/hash.tsx | 45 +- src/libs/ui/components/index.ts | 6 + src/libs/ui/components/input/input.tsx | 17 +- .../ledger-connection-view.tsx | 61 + .../ledger-error-view/ledger-error-view.tsx | 92 ++ .../ledger-event-view/ledger-event-view.tsx | 57 + .../ledger-disconnected-footer.tsx | 111 ++ .../ledger-footer/ledger-footer.tsx | 50 + src/libs/ui/components/list/list.tsx | 22 +- .../no-connected-ledger.tsx | 156 +++ .../review-with-ledger/review-with-ledger.tsx | 67 + src/libs/ui/global-style.ts | 3 + src/libs/ui/utils/get-color-from-theme.ts | 6 +- src/utils.ts | 8 + .../Casper Wallet.xcodeproj/project.pbxproj | 8 +- 84 files changed, 9128 insertions(+), 539 deletions(-) create mode 100644 src/apps/popup/pages/import-account-from-ledger/connected-ledger.tsx create mode 100644 src/apps/popup/pages/import-account-from-ledger/index.tsx create mode 100644 src/apps/popup/pages/import-account-from-ledger/ledger-accounts-list.tsx create mode 100644 src/apps/popup/pages/import-account-from-ledger/types.ts create mode 100644 src/apps/popup/pages/sign-with-ledger-in-new-window/index.tsx create mode 100644 src/apps/popup/pages/sign-with-ledger-in-new-window/success-view.tsx create mode 100644 src/assets/icons/ledger-blue.svg create mode 100644 src/assets/icons/ledger-white.svg create mode 100644 src/assets/illustrations/ledger-connect.svg create mode 100644 src/assets/illustrations/ledger-error.svg create mode 100644 src/assets/illustrations/ledger-not-connected.svg create mode 100644 src/assets/illustrations/ledger-rejected.svg create mode 100644 src/background/redux/ledger/actions.ts create mode 100644 src/background/redux/ledger/reducer.ts create mode 100644 src/background/redux/ledger/selectors.ts create mode 100644 src/background/redux/ledger/types.ts create mode 100644 src/hooks/use-is-dark-mode.ts create mode 100644 src/hooks/use-ledger.ts create mode 100644 src/libs/animations/dots_dark_mode.json create mode 100644 src/libs/animations/dots_light_mode.json create mode 100644 src/libs/animations/spinner_dark_mode.json create mode 100644 src/libs/animations/spinner_light_mode.json create mode 100644 src/libs/services/ledger/errors.ts create mode 100644 src/libs/services/ledger/index.ts create mode 100644 src/libs/services/ledger/ledger.ts create mode 100644 src/libs/services/ledger/transport.ts create mode 100644 src/libs/services/ledger/types.ts create mode 100644 src/libs/ui/components/ledger-connection-view/ledger-connection-view.tsx create mode 100644 src/libs/ui/components/ledger-error-view/ledger-error-view.tsx create mode 100644 src/libs/ui/components/ledger-event-view/ledger-event-view.tsx create mode 100644 src/libs/ui/components/ledger-footer/ledger-disconnected-footer.tsx create mode 100644 src/libs/ui/components/ledger-footer/ledger-footer.tsx create mode 100644 src/libs/ui/components/no-connected-ledger/no-connected-ledger.tsx create mode 100644 src/libs/ui/components/review-with-ledger/review-with-ledger.tsx diff --git a/e2e-tests/popup/import-account-with-file/import-account-with-file.spec.ts b/e2e-tests/popup/import-account-with-file/import-account-with-file.spec.ts index c7847bd4a..520e115c5 100644 --- a/e2e-tests/popup/import-account-with-file/import-account-with-file.spec.ts +++ b/e2e-tests/popup/import-account-with-file/import-account-with-file.spec.ts @@ -58,7 +58,7 @@ popup.describe('Popup UI: import account with file', () => { popupPage.getByText(IMPORTED_PEM_ACCOUNT.truncatedPublicKey) ).toBeVisible(); await popupExpect( - popupPage.getByText('Imported', { exact: true }) + popupPage.getByTestId('import-account-icon') ).toBeVisible(); } ); @@ -112,7 +112,7 @@ popup.describe('Popup UI: import account with file', () => { popupPage.getByText(IMPORTED_CER_ACCOUNT.truncatedPublicKey) ).toBeVisible(); await popupExpect( - popupPage.getByText('Imported', { exact: true }) + popupPage.getByTestId('import-account-icon') ).toBeVisible(); } ); diff --git a/e2e-tests/popup/import-torus-account/import-torus-account.spec.ts b/e2e-tests/popup/import-torus-account/import-torus-account.spec.ts index 195b70f64..72705508f 100644 --- a/e2e-tests/popup/import-torus-account/import-torus-account.spec.ts +++ b/e2e-tests/popup/import-torus-account/import-torus-account.spec.ts @@ -46,7 +46,7 @@ popup.describe('Popup UI: import account with file', () => { popupPage.getByText(IMPORTED_TORUS_ACCOUNT.truncatedPublicKey) ).toBeVisible(); await popupExpect( - popupPage.getByText('Imported', { exact: true }) + popupPage.getByTestId('import-account-icon') ).toBeVisible(); }); }); diff --git a/package-lock.json b/package-lock.json index dd0b8d344..52f51f57e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,20 @@ { "name": "Casper Wallet", - "version": "1.8.1", + "version": "1.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "Casper Wallet", - "version": "1.8.1", + "version": "1.10.0", "dependencies": { "@formatjs/intl": "2.6.2", "@hookform/resolvers": "2.9.10", "@lapo/asn1js": "1.2.4", + "@ledgerhq/hw-transport": "^6.30.6", + "@ledgerhq/hw-transport-web-ble": "^6.28.6", + "@ledgerhq/hw-transport-webhid": "^6.28.6", + "@ledgerhq/hw-transport-webusb": "^6.28.6", "@lottiefiles/react-lottie-player": "3.5.3", "@make-software/ces-js-parser": "1.3.2", "@noble/ciphers": "^0.3.0", @@ -18,6 +22,7 @@ "@scure/bip39": "1.2.1", "@types/argon2-browser": "1.18.1", "@types/webextension-polyfill": "0.9.2", + "@zondax/ledger-casper": "^2.6.1", "base64-loader": "1.0.0", "big.js": "^6.2.1", "casper-cep18-js-client": "1.0.2", @@ -4563,6 +4568,86 @@ "resolved": "https://registry.npmjs.org/@lapo/asn1js/-/asn1js-1.2.4.tgz", "integrity": "sha512-mdInpQZaYUWu5QbKIB2+Vd+j6Y7cc6xQYNwYBPC9jri2rwy3tbxom0IhhT4G5WOKWO7Iht10SxYpKq+AfuH6dw==" }, + "node_modules/@ledgerhq/devices": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.3.0.tgz", + "integrity": "sha512-h5Scr+yIae8yjPOViCHLdMjpqn4oC2Whrsq8LinRxe48LEGMdPqSV1yY7+3Ch827wtzNpMv+/ilKnd8rY+rTlg==", + "dependencies": { + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/logs": "^6.12.0", + "rxjs": "^7.8.1", + "semver": "^7.3.5" + } + }, + "node_modules/@ledgerhq/devices/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@ledgerhq/errors": { + "version": "6.16.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.16.4.tgz", + "integrity": "sha512-M57yFaLYSN+fZCX0E0zUqOmrV6eipK+s5RhijHoUNlHUqrsvUz7iRQgpd5gRgHB5VkIjav7KdaZjKiWGcHovaQ==" + }, + "node_modules/@ledgerhq/hw-transport": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.30.6.tgz", + "integrity": "sha512-fT0Z4IywiuJuZrZE/+W0blkV5UCotDPFTYKLkKCLzYzuE6javva7D/ajRaIeR+hZ4kTmKF4EqnsmDCXwElez+w==", + "dependencies": { + "@ledgerhq/devices": "^8.3.0", + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/logs": "^6.12.0", + "events": "^3.3.0" + } + }, + "node_modules/@ledgerhq/hw-transport-web-ble": { + "version": "6.28.6", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-web-ble/-/hw-transport-web-ble-6.28.6.tgz", + "integrity": "sha512-SsseU5T4ePhdvFdwUOsF207gyMgiHyymvRAV66/hpHCd0+m/81kV8nZneeD3Z1pG0XPG+tPlF90r7nLwtUoiXw==", + "dependencies": { + "@ledgerhq/devices": "^8.3.0", + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/hw-transport": "^6.30.6", + "@ledgerhq/logs": "^6.12.0", + "rxjs": "^7.8.1" + } + }, + "node_modules/@ledgerhq/hw-transport-webhid": { + "version": "6.28.6", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.28.6.tgz", + "integrity": "sha512-npU1mgL97KovpTUgcdORoOZ7eVFgwCA7zt0MpgUGUMRNJWDgCFsJslx7KrVXlCGOg87gLfDojreIre502I5pYg==", + "dependencies": { + "@ledgerhq/devices": "^8.3.0", + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/hw-transport": "^6.30.6", + "@ledgerhq/logs": "^6.12.0" + } + }, + "node_modules/@ledgerhq/hw-transport-webusb": { + "version": "6.28.6", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.28.6.tgz", + "integrity": "sha512-rzICsvhcFcL4wSAvRPe+b9EEWB8cxj6yWy3FZdfs7ufi/0muNpFXWckWv1TC34em55sGXu2cMcwMKXg/O/Lc0Q==", + "dependencies": { + "@ledgerhq/devices": "^8.3.0", + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/hw-transport": "^6.30.6", + "@ledgerhq/logs": "^6.12.0" + } + }, + "node_modules/@ledgerhq/logs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.12.0.tgz", + "integrity": "sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA==" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -7057,6 +7142,14 @@ "@types/node": "*" } }, + "node_modules/@types/ledgerhq__hw-transport": { + "version": "4.21.8", + "resolved": "https://registry.npmjs.org/@types/ledgerhq__hw-transport/-/ledgerhq__hw-transport-4.21.8.tgz", + "integrity": "sha512-uO2AJYZUVCwgyqgyy2/KW+JsQaO0hcwDdubRaHgF2ehO0ngGAY41PbE8qnPnmUw1uerMXONvL68QFioA7Y6C5g==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.202", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", @@ -7816,6 +7909,16 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/@zondax/ledger-casper": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@zondax/ledger-casper/-/ledger-casper-2.6.1.tgz", + "integrity": "sha512-Zk+DOVK9G9Gyt7ua9x/G5iVVSlNfp1l+Tek+7+MoqP5aTr4YznBJIhdnPD8yYSxEEZZLs8Q0tV0TyfsXeRokQw==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "@ledgerhq/hw-transport": "^6.28.2", + "@types/ledgerhq__hw-transport": "^4.21.4" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -14323,7 +14426,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "engines": { "node": ">=0.8.x" } @@ -20882,7 +20984,7 @@ "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, "node_modules/lodash.get": { @@ -21115,7 +21217,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -25059,10 +25160,9 @@ "dev": true }, "node_modules/rxjs": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", - "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", - "dev": true, + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dependencies": { "tslib": "^2.1.0" } @@ -29322,8 +29422,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "1.10.2", @@ -32603,6 +32702,82 @@ "resolved": "https://registry.npmjs.org/@lapo/asn1js/-/asn1js-1.2.4.tgz", "integrity": "sha512-mdInpQZaYUWu5QbKIB2+Vd+j6Y7cc6xQYNwYBPC9jri2rwy3tbxom0IhhT4G5WOKWO7Iht10SxYpKq+AfuH6dw==" }, + "@ledgerhq/devices": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.3.0.tgz", + "integrity": "sha512-h5Scr+yIae8yjPOViCHLdMjpqn4oC2Whrsq8LinRxe48LEGMdPqSV1yY7+3Ch827wtzNpMv+/ilKnd8rY+rTlg==", + "requires": { + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/logs": "^6.12.0", + "rxjs": "^7.8.1", + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "@ledgerhq/errors": { + "version": "6.16.4", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.16.4.tgz", + "integrity": "sha512-M57yFaLYSN+fZCX0E0zUqOmrV6eipK+s5RhijHoUNlHUqrsvUz7iRQgpd5gRgHB5VkIjav7KdaZjKiWGcHovaQ==" + }, + "@ledgerhq/hw-transport": { + "version": "6.30.6", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.30.6.tgz", + "integrity": "sha512-fT0Z4IywiuJuZrZE/+W0blkV5UCotDPFTYKLkKCLzYzuE6javva7D/ajRaIeR+hZ4kTmKF4EqnsmDCXwElez+w==", + "requires": { + "@ledgerhq/devices": "^8.3.0", + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/logs": "^6.12.0", + "events": "^3.3.0" + } + }, + "@ledgerhq/hw-transport-web-ble": { + "version": "6.28.6", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-web-ble/-/hw-transport-web-ble-6.28.6.tgz", + "integrity": "sha512-SsseU5T4ePhdvFdwUOsF207gyMgiHyymvRAV66/hpHCd0+m/81kV8nZneeD3Z1pG0XPG+tPlF90r7nLwtUoiXw==", + "requires": { + "@ledgerhq/devices": "^8.3.0", + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/hw-transport": "^6.30.6", + "@ledgerhq/logs": "^6.12.0", + "rxjs": "^7.8.1" + } + }, + "@ledgerhq/hw-transport-webhid": { + "version": "6.28.6", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.28.6.tgz", + "integrity": "sha512-npU1mgL97KovpTUgcdORoOZ7eVFgwCA7zt0MpgUGUMRNJWDgCFsJslx7KrVXlCGOg87gLfDojreIre502I5pYg==", + "requires": { + "@ledgerhq/devices": "^8.3.0", + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/hw-transport": "^6.30.6", + "@ledgerhq/logs": "^6.12.0" + } + }, + "@ledgerhq/hw-transport-webusb": { + "version": "6.28.6", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-6.28.6.tgz", + "integrity": "sha512-rzICsvhcFcL4wSAvRPe+b9EEWB8cxj6yWy3FZdfs7ufi/0muNpFXWckWv1TC34em55sGXu2cMcwMKXg/O/Lc0Q==", + "requires": { + "@ledgerhq/devices": "^8.3.0", + "@ledgerhq/errors": "^6.16.4", + "@ledgerhq/hw-transport": "^6.30.6", + "@ledgerhq/logs": "^6.12.0" + } + }, + "@ledgerhq/logs": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.12.0.tgz", + "integrity": "sha512-ExDoj1QV5eC6TEbMdLUMMk9cfvNKhhv5gXol4SmULRVCx/3iyCPhJ74nsb3S0Vb+/f+XujBEj3vQn5+cwS0fNA==" + }, "@leichtgewicht/ip-codec": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", @@ -34630,6 +34805,14 @@ "@types/node": "*" } }, + "@types/ledgerhq__hw-transport": { + "version": "4.21.8", + "resolved": "https://registry.npmjs.org/@types/ledgerhq__hw-transport/-/ledgerhq__hw-transport-4.21.8.tgz", + "integrity": "sha512-uO2AJYZUVCwgyqgyy2/KW+JsQaO0hcwDdubRaHgF2ehO0ngGAY41PbE8qnPnmUw1uerMXONvL68QFioA7Y6C5g==", + "requires": { + "@types/node": "*" + } + }, "@types/lodash": { "version": "4.14.202", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", @@ -35253,6 +35436,16 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "@zondax/ledger-casper": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@zondax/ledger-casper/-/ledger-casper-2.6.1.tgz", + "integrity": "sha512-Zk+DOVK9G9Gyt7ua9x/G5iVVSlNfp1l+Tek+7+MoqP5aTr4YznBJIhdnPD8yYSxEEZZLs8Q0tV0TyfsXeRokQw==", + "requires": { + "@babel/runtime": "^7.21.0", + "@ledgerhq/hw-transport": "^6.28.2", + "@types/ledgerhq__hw-transport": "^4.21.4" + } + }, "abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -40139,8 +40332,7 @@ "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, "eventsource": { "version": "2.0.2", @@ -45080,7 +45272,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, "lodash.get": { @@ -45260,7 +45452,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -48218,10 +48409,9 @@ "dev": true }, "rxjs": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", - "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", - "dev": true, + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "requires": { "tslib": "^2.1.0" } @@ -51456,8 +51646,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", diff --git a/package.json b/package.json index 6624f27a6..475187cac 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Casper Wallet", "description": "Securely manage your CSPR tokens and interact with dapps with the self-custody wallet for the Casper blockchain.", - "version": "1.9.1", + "version": "1.10.0", "author": "MAKE LLC", "scripts": { "devtools:redux": "redux-devtools --hostname=localhost", @@ -55,6 +55,10 @@ "@formatjs/intl": "2.6.2", "@hookform/resolvers": "2.9.10", "@lapo/asn1js": "1.2.4", + "@ledgerhq/hw-transport": "^6.30.6", + "@ledgerhq/hw-transport-web-ble": "^6.28.6", + "@ledgerhq/hw-transport-webhid": "^6.28.6", + "@ledgerhq/hw-transport-webusb": "^6.28.6", "@lottiefiles/react-lottie-player": "3.5.3", "@make-software/ces-js-parser": "1.3.2", "@noble/ciphers": "^0.3.0", @@ -62,6 +66,7 @@ "@scure/bip39": "1.2.1", "@types/argon2-browser": "1.18.1", "@types/webextension-polyfill": "0.9.2", + "@zondax/ledger-casper": "^2.6.1", "base64-loader": "1.0.0", "big.js": "^6.2.1", "casper-cep18-js-client": "1.0.2", diff --git a/src/apps/popup/app-router.tsx b/src/apps/popup/app-router.tsx index 5b6515491..8cd0a67d0 100644 --- a/src/apps/popup/app-router.tsx +++ b/src/apps/popup/app-router.tsx @@ -18,6 +18,7 @@ import { ContactsBookPage } from '@popup/pages/contacts'; import { CreateAccountPage } from '@popup/pages/create-account'; import { DownloadAccountKeysPage } from '@popup/pages/download-account-keys'; import { HomePageContent } from '@popup/pages/home'; +import { ImportAccountFromLedgerPage } from '@popup/pages/import-account-from-ledger'; import { ImportAccountFromTorusPage } from '@popup/pages/import-account-from-torus'; import { NavigationMenuPageContent } from '@popup/pages/navigation-menu'; import { NftDetailsPage } from '@popup/pages/nft-details'; @@ -26,6 +27,7 @@ import { RateAppPage } from '@popup/pages/rate-app'; import { ReceivePage } from '@popup/pages/receive'; import { RemoveAccountPageContent } from '@popup/pages/remove-account'; import { RenameAccountPageContent } from '@popup/pages/rename-account'; +import { SignWithLedgerInNewWindowPage } from '@popup/pages/sign-with-ledger-in-new-window'; import { StakesPage } from '@popup/pages/stakes'; import { TimeoutPageContent } from '@popup/pages/timeout'; import { TokenDetailPage } from '@popup/pages/token-details'; @@ -283,6 +285,14 @@ function AppRoutes() { element={} /> } /> + } + /> + } + /> ); } diff --git a/src/apps/popup/pages/account-settings/content.tsx b/src/apps/popup/pages/account-settings/content.tsx index 0ba76b291..533b84a94 100644 --- a/src/apps/popup/pages/account-settings/content.tsx +++ b/src/apps/popup/pages/account-settings/content.tsx @@ -12,7 +12,8 @@ import { hideAccountFromListChange } from '@background/redux/vault/actions'; import { selectVaultAccount, selectVaultHiddenAccountsNames, - selectVaultImportedAccountNames + selectVaultImportedAccountNames, + selectVaultLedgerAccountNames } from '@background/redux/vault/selectors'; import { useFetchAccountInfo } from '@hooks/use-fetch-account-info'; @@ -143,6 +144,7 @@ export function AccountSettingsActionsGroup() { const { accountName } = useParams(); const importedAccountNames = useSelector(selectVaultImportedAccountNames); const hiddenAccountsNames = useSelector(selectVaultHiddenAccountsNames); + const ledgerAccountNames = useSelector(selectVaultLedgerAccountNames); if (!accountName) { return null; @@ -150,12 +152,15 @@ export function AccountSettingsActionsGroup() { const isImportedAccount = importedAccountNames.includes(accountName); const isAccountHidden = hiddenAccountsNames.includes(accountName); + const isLedgerAccount = ledgerAccountNames.includes(accountName); return ( - {isImportedAccount && } + {(isImportedAccount || isLedgerAccount) && ( + + )} ); } diff --git a/src/apps/popup/pages/connected-sites/index.tsx b/src/apps/popup/pages/connected-sites/index.tsx index ec39ce68b..66ad56b6e 100644 --- a/src/apps/popup/pages/connected-sites/index.tsx +++ b/src/apps/popup/pages/connected-sites/index.tsx @@ -86,7 +86,7 @@ export function ConnectedSitesPage() { /> )} renderRow={(account, index, array) => { - const { name, publicKey, imported } = account; + const { name, publicKey, imported, hardware } = account; return ( { if (array == null || array.length === 0) { return; diff --git a/src/apps/popup/pages/connected-sites/site-group-item.tsx b/src/apps/popup/pages/connected-sites/site-group-item.tsx index 0abf2fcf1..2e8a720ce 100644 --- a/src/apps/popup/pages/connected-sites/site-group-item.tsx +++ b/src/apps/popup/pages/connected-sites/site-group-item.tsx @@ -5,6 +5,7 @@ import { AlignedSpaceBetweenFlexRow, LeftAlignedFlexColumn } from '@libs/layout'; +import { HardwareWalletType } from '@libs/types/account'; import { Hash, HashVariant, SvgIcon, Typography } from '@libs/ui/components'; const SiteGroupItemContainer = styled(AlignedSpaceBetweenFlexRow)` @@ -21,13 +22,15 @@ interface SiteGroupItemProps { publicKey: string; handleOnClick: () => void; imported?: boolean; + hardware?: HardwareWalletType; } export function SiteGroupItem({ name, publicKey, handleOnClick, - imported + imported, + hardware }: SiteGroupItemProps) { return ( @@ -39,7 +42,8 @@ export function SiteGroupItem({ variant={HashVariant.CaptionHash} value={publicKey} truncated - withTag={imported} + isImported={imported} + isLedger={hardware === HardwareWalletType.Ledger} placement="bottomRight" /> diff --git a/src/apps/popup/pages/download-account-keys/components/account-list-item.tsx b/src/apps/popup/pages/download-account-keys/components/account-list-item.tsx index c02c0b9c6..39ea860d3 100644 --- a/src/apps/popup/pages/download-account-keys/components/account-list-item.tsx +++ b/src/apps/popup/pages/download-account-keys/components/account-list-item.tsx @@ -6,7 +6,7 @@ import { FlexColumn, SpacingSize } from '@libs/layout'; -import { AccountListRows } from '@libs/types/account'; +import { AccountListRows, HardwareWalletType } from '@libs/types/account'; import { Avatar, Checkbox, @@ -88,7 +88,8 @@ export const AccountListItem = ({ variant={HashVariant.CaptionHash} truncated withoutTooltip - withTag={account.imported} + isImported={account.imported} + isLedger={account.hardware === HardwareWalletType.Ledger} /> CSPR diff --git a/src/apps/popup/pages/import-account-from-ledger/connected-ledger.tsx b/src/apps/popup/pages/import-account-from-ledger/connected-ledger.tsx new file mode 100644 index 000000000..59e979464 --- /dev/null +++ b/src/apps/popup/pages/import-account-from-ledger/connected-ledger.tsx @@ -0,0 +1,247 @@ +import { Player } from '@lottiefiles/react-lottie-player'; +import React, { useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { isEqualCaseInsensitive } from '@src/utils'; + +import { RouterPath, useTypedNavigate } from '@popup/router'; + +import { selectApiConfigBasedOnActiveNetwork } from '@background/redux/settings/selectors'; +import { dispatchToMainStore } from '@background/redux/utils'; +import { accountsImported } from '@background/redux/vault/actions'; + +import { useIsDarkMode } from '@hooks/use-is-dark-mode'; + +import spinnerDarkModeAnimation from '@libs/animations/spinner_dark_mode.json'; +import spinnerLightModeAnimation from '@libs/animations/spinner_light_mode.json'; +import { getAccountHashFromPublicKey } from '@libs/entities/Account'; +import { + CenteredFlexColumn, + ContentContainer, + FooterButtonsContainer, + HeaderPopup, + HeaderSubmenuBarNavLink, + ParagraphContainer, + PopupLayout, + SpacingSize, + VerticalSpaceContainer +} from '@libs/layout'; +import { dispatchFetchAccountBalances } from '@libs/services/balance-service'; +import { + LedgerAccount, + LedgerEventStatus, + ledger +} from '@libs/services/ledger'; +import { Account, HardwareWalletType } from '@libs/types/account'; +import { Button, Tile, Typography } from '@libs/ui/components'; + +import { LedgerAccountsList } from './ledger-accounts-list'; +import { ILedgerAccountListItem } from './types'; + +const AnimationContainer = styled(CenteredFlexColumn)` + padding: 0 16px 52px; +`; + +interface IConnectedLedgerProps { + onClose: () => void; +} + +export const ConnectedLedger: React.FC = ({ + onClose +}) => { + const [isButtonDisabled, setIsButtonDisabled] = useState(true); + const [selectedAccounts, setSelectedAccounts] = useState< + ILedgerAccountListItem[] + >([]); + const [accountsFromLedger, setAccountsFromLedger] = useState( + [] + ); + const [ledgerAccountsWithBalance, setLedgerAccountsWithBalance] = useState< + ILedgerAccountListItem[] + >([]); + const [isLoading, setIsLoading] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [maxItemsToRender, setMaxItemsToRender] = useState(5); + + const { t } = useTranslation(); + const navigate = useTypedNavigate(); + const isDarkMode = useIsDarkMode(); + + const { casperWalletApiUrl } = useSelector( + selectApiConfigBasedOnActiveNetwork + ); + + useEffect(() => { + ledger.getAccountList({ size: 5, offset: 0 }); + }, []); + + useEffect(() => { + const sub = ledger.subscribeToLedgerEventStatuss(event => { + if (event.status === LedgerEventStatus.AccountListUpdated) { + setAccountsFromLedger(prev => { + return [...prev, ...(event.accounts ?? [])]; + }); + } + }); + + return () => sub.unsubscribe(); + }, []); + + useEffect(() => { + if (!accountsFromLedger.length) return; + + const hashes = accountsFromLedger.reduce( + (previousValue, currentValue, currentIndex) => { + const hash = getAccountHashFromPublicKey(currentValue.publicKey); + + return accountsFromLedger.length === currentIndex + 1 + ? previousValue + `${hash}` + : previousValue + `${hash},`; + }, + '' + ); + + dispatchFetchAccountBalances(hashes) + .then(({ payload }) => { + if ('data' in payload) { + const accountsWithBalance = + accountsFromLedger.map(account => { + const accountWithBalance = payload.data.find(ac => + isEqualCaseInsensitive(ac.public_key, account.publicKey) + ); + + return { + publicKey: account.publicKey, + derivationIndex: account.index, + name: '', + id: account.publicKey, + balance: { + liquidMotes: `${accountWithBalance?.balance ?? '0'}` + } + }; + }); + + setLedgerAccountsWithBalance(accountsWithBalance); + } + }) + .finally(() => { + setIsLoading(false); + setIsLoadingMore(false); + }); + }, [casperWalletApiUrl, accountsFromLedger]); + + const onSubmit = () => { + const accounts: Account[] = selectedAccounts.map(account => ({ + name: account.name, + publicKey: account.publicKey, + secretKey: '', + hardware: HardwareWalletType.Ledger, + hidden: false, + derivationIndex: account.derivationIndex + })); + + dispatchToMainStore(accountsImported(accounts)).then(() => { + onClose(); + navigate(RouterPath.Home); + }); + }; + + const onLoadMore = () => { + try { + setIsLoadingMore(true); + ledger.getAccountList({ + size: 5, + offset: ledgerAccountsWithBalance.length + }); + setMaxItemsToRender(prevState => prevState + 5); + } catch (e) { + setIsLoadingMore(false); + } + }; + + return ( + ( + ( + + )} + /> + )} + renderContent={() => ( + + + + Select accounts + + + + + to connect with Casper Wallet + + + + {isLoading ? ( + + + + + + + Just a moment + + + + Your accounts from Ledger will be here shortly. + + + + + + + ) : ( + + )} + + )} + renderFooter={() => ( + + + + )} + /> + ); +}; diff --git a/src/apps/popup/pages/import-account-from-ledger/index.tsx b/src/apps/popup/pages/import-account-from-ledger/index.tsx new file mode 100644 index 000000000..1b4c301c5 --- /dev/null +++ b/src/apps/popup/pages/import-account-from-ledger/index.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { RouterPath } from '@popup/router'; + +import { useLedger } from '@hooks/use-ledger'; + +import { LedgerEventStatus } from '@libs/services/ledger'; +import { LedgerConnectionView } from '@libs/ui/components'; + +import { ConnectedLedger } from './connected-ledger'; + +export const ImportAccountFromLedgerPage = () => { + const searchParams = new URLSearchParams(document.location.search); + const initialEventToRender = + (searchParams.get('initialEventToRender') as LedgerEventStatus) ?? + LedgerEventStatus.Disconnected; + + const { + ledgerEventStatusToRender, + makeSubmitLedgerAction, + closeNewLedgerWindowsAndClearState + } = useLedger({ + ledgerAction: async () => {}, + shouldLoadAccountList: true, + beforeLedgerActionCb: async () => {}, + initialEventToRender: { status: initialEventToRender }, + withWaitingEventOnDisconnect: false, + askPermissionUrlData: { + domain: 'popup.html', + params: {}, + hash: RouterPath.ImportAccountFromLedger + } + }); + + return ledgerEventStatusToRender.status === + LedgerEventStatus.AccountListUpdated || + ledgerEventStatusToRender.status === + LedgerEventStatus.LoadingAccountsList ? ( + + ) : ( + + ); +}; diff --git a/src/apps/popup/pages/import-account-from-ledger/ledger-accounts-list.tsx b/src/apps/popup/pages/import-account-from-ledger/ledger-accounts-list.tsx new file mode 100644 index 000000000..27e14e6b5 --- /dev/null +++ b/src/apps/popup/pages/import-account-from-ledger/ledger-accounts-list.tsx @@ -0,0 +1,403 @@ +import { Player } from '@lottiefiles/react-lottie-player'; +import React, { useEffect, useState } from 'react'; +import { + Controller, + FieldValues, + useFieldArray, + useForm +} from 'react-hook-form'; +import { Trans, useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; + +import { isEqualCaseInsensitive } from '@src/utils'; + +import { + selectVaultAccountsNames, + selectVaultLedgerAccounts +} from '@background/redux/vault/selectors'; + +import { useIsDarkMode } from '@hooks/use-is-dark-mode'; + +import dotsDarkModeAnimation from '@libs/animations/dots_dark_mode.json'; +import dotsLightModeAnimation from '@libs/animations/dots_light_mode.json'; +import { + CenteredFlexRow, + FlexColumn, + LeftAlignedCenteredFlexRow, + SpaceBetweenFlexRow, + SpacingSize +} from '@libs/layout'; +import { + Avatar, + Checkbox, + Hash, + HashVariant, + Input, + List, + Tooltip, + Typography +} from '@libs/ui/components'; +import { calculateSubmitButtonDisabled } from '@libs/ui/forms/get-submit-button-state-from-validation'; +import { formatNumber, motesToCSPR } from '@libs/ui/utils'; + +import { ILedgerAccountListItem } from './types'; + +const ListItemContainer = styled(FlexColumn)<{ disabled?: boolean }>` + padding: 20px 16px; + + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; +`; +const FooterContainer = styled(LeftAlignedCenteredFlexRow)` + padding: 18px 16px; +`; +const MoreItem = styled(Typography)` + cursor: pointer; +`; +const AmountContainer = styled(FlexColumn)` + max-width: 90px; +`; + +interface ListProps { + ledgerAccountsWithBalance: ILedgerAccountListItem[]; + setIsButtonDisabled: React.Dispatch>; + selectedAccounts: ILedgerAccountListItem[]; + setSelectedAccounts: React.Dispatch< + React.SetStateAction + >; + maxItemsToRender: number; + onLoadMore: () => void; + isLoadingMore: boolean; +} + +type FormFields = FieldValues & { + accountNames: { name: string }[]; + checkbox: boolean[]; +}; + +export const LedgerAccountsList = ({ + ledgerAccountsWithBalance, + setIsButtonDisabled, + selectedAccounts, + setSelectedAccounts, + maxItemsToRender, + onLoadMore, + isLoadingMore +}: ListProps) => { + const [accountNames, setAccountNames] = useState<{ name: string }[]>([]); + const [checkboxes, setCheckboxes] = useState([]); + + const { t } = useTranslation(); + const isDarkMode = useIsDarkMode(); + + const alreadyConnectedLedgerAccounts = useSelector(selectVaultLedgerAccounts); + const existingAccountNames = useSelector(selectVaultAccountsNames); + + const { + control, + formState: { isValid }, + getValues, + trigger + } = useForm({ + defaultValues: { + accountNames: [], + checkbox: [] + }, + mode: 'onChange', + reValidateMode: 'onChange' + }); + + const { fields: inputsFields, append } = useFieldArray({ + name: 'accountNames', + control + }); + + useEffect(() => { + for ( + let i = inputsFields.length; + i < ledgerAccountsWithBalance.length; + i++ + ) { + append({ name: `Ledger account ${i + 1}` }); + } + }, [append, inputsFields.length, ledgerAccountsWithBalance]); + + useEffect(() => { + setAccountNames(getValues('accountNames')); + setCheckboxes(getValues('checkbox')); + }, [getValues]); + + const handleInputChange = (id: string, newValue: string) => { + // Update the state with the new value + setSelectedAccounts(prevItems => + prevItems.map(item => + isEqualCaseInsensitive(item.id, id) + ? { ...item, name: newValue.trim() } + : item + ) + ); + }; + + useEffect(() => { + const isButtonDisabled = calculateSubmitButtonDisabled({ + isValid + }); + + setIsButtonDisabled(!!isButtonDisabled); + }, [isValid, setIsButtonDisabled]); + + return ( + + rows={ledgerAccountsWithBalance} + contentTop={SpacingSize.XL} + maxItemsToRender={maxItemsToRender} + renderRow={(account, index) => { + const inputFieldName = `accountNames.${index}.name`; + const checkBoxFieldName = `checkbox.${index}`; + const balance = formatNumber( + motesToCSPR(String(account.balance.liquidMotes)), + { + precision: { max: 0 } + } + ); + + const isAlreadyConnected = alreadyConnectedLedgerAccounts.some( + alreadyConnectedAccount => + isEqualCaseInsensitive( + alreadyConnectedAccount.publicKey, + account.publicKey + ) + ); + + const checkboxValue = getValues(checkBoxFieldName); + + return ( + + ( + { + if (isAlreadyConnected) return; + + const accountIndex = selectedAccounts.findIndex( + alreadySelectedAccount => + isEqualCaseInsensitive( + alreadySelectedAccount.id, + account.id + ) + ); + const accountName: string = getValues(inputFieldName); + + let updatedAccounts; + if (accountIndex !== -1) { + // Account exists, remove from list: + updatedAccounts = selectedAccounts.filter( + alreadySelectedAccount => + alreadySelectedAccount.id !== account.id + ); + } else { + // Account doesn't exist, add to list: + updatedAccounts = selectedAccounts.concat({ + ...account, + name: accountName + }); + } + + setSelectedAccounts(updatedAccounts); + checkboxControllerField.onChange( + !checkboxControllerField.value + ); + + trigger(); + }} + > + + + + + + + 9 ? balance : undefined} + placement="topLeft" + overflowWrap + fullWidth + > + + {balance} + + + + CSPR + + + + + + )} + name={checkBoxFieldName} + /> + {(checkboxValue || isAlreadyConnected) && + inputsFields.map((inputField, inputFieldIndex) => + inputFieldIndex === index ? ( + { + return ( + { + inputControllerField.onChange(event); + + // manually trigger validation in case when a few inputs have the same name + // and user change one of them. + // So we validate all of them to remove error from the fields. + // This is an edge case. + trigger().then(isValid => { + if (isValid) { + handleInputChange( + account.id, + event.target.value + ); + } + }); + }} + error={ + !!inputControllerFormState.errors.accountNames?.[ + inputFieldIndex + ]?.name + } + validationText={ + inputControllerFormState.errors.accountNames?.[ + inputFieldIndex + ]?.name?.message + } + /> + ); + }} + control={control} + name={`accountNames.${inputFieldIndex}.name`} + rules={{ + pattern: { + value: /^[\daA-zZ\s]+$/, + message: t( + 'Account name can’t contain special characters' + ) + }, + validate: + checkboxValue && !isAlreadyConnected + ? { + noEmptyInput: value => + (value != null && value.trim() !== '') || + t("Name can't be empty"), + maxLength: value => + value.length <= 20 || + t( + "Account name can't be longer than 20 characters" + ), + unique: value => { + // Filter the inputs of 'accountNames' to only leave those where the checkbox is checked + // and the field index doesn't match the current input field index. + // This leaves us with an array of inputs that are selected + // (checked) and not the one being validated. + const onlyCheckedInputs = accountNames + .map((input, index) => + checkboxes[index] ? input : null + ) + .filter( + (input, index) => + index !== inputFieldIndex && + input !== null + ); + + // Checks to see if the current value exists within the selected inputs. + // The `some` function will return true as soon as it finds a value that matches, + // hence it will return false if the name is unique. + const isUnique = !onlyCheckedInputs.some( + input => input?.name === value + ); + + // Checks if the current value exists in the 'existingAccountNames' array. + const isNotInExistingAccountNames = + !existingAccountNames.includes(value); + + // Returns the validation results. + // If the entered account name is both unique and not in the existing account names array, + // it returns true (passing validation), + // otherwise it returns the error message. + return ( + (isUnique && isNotInExistingAccountNames) || + t('Account name is already taken') + ); + } + } + : undefined + }} + /> + ) : null + )} + + ); + }} + marginLeftForItemSeparatorLine={56} + renderFooter={() => + isLoadingMore ? ( + + ) : ( + + + Show next 5 accounts + + + ) + } + /> + ); +}; diff --git a/src/apps/popup/pages/import-account-from-ledger/types.ts b/src/apps/popup/pages/import-account-from-ledger/types.ts new file mode 100644 index 000000000..bae38cb3e --- /dev/null +++ b/src/apps/popup/pages/import-account-from-ledger/types.ts @@ -0,0 +1,6 @@ +import { AccountWithBalance } from '@libs/types/account'; + +export type ILedgerAccountListItem = Omit< + AccountWithBalance, + 'hidden' | 'secretKey' | 'imported' | 'hardware' +> & { id: string }; diff --git a/src/apps/popup/pages/navigation-menu/index.tsx b/src/apps/popup/pages/navigation-menu/index.tsx index 42c9aea4c..fb410738e 100644 --- a/src/apps/popup/pages/navigation-menu/index.tsx +++ b/src/apps/popup/pages/navigation-menu/index.tsx @@ -1,13 +1,12 @@ import React, { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; -import { isSafariBuild } from '@src/utils'; +import { isLedgerAvailable, isSafariBuild } from '@src/utils'; import { TimeoutDurationSetting } from '@popup/constants'; -import { RouterPath, useNavigationMenu } from '@popup/router'; +import { RouterPath, useNavigationMenu, useTypedNavigate } from '@popup/router'; import { WindowApp } from '@background/create-open-window'; import { selectCountOfContacts } from '@background/redux/contacts/selectors'; @@ -82,7 +81,7 @@ interface MenuGroup { } export function NavigationMenuPageContent() { - const navigate = useNavigate(); + const navigate = useTypedNavigate(); const { t } = useTranslation(); const timeoutDurationSetting = useSelector(selectTimeoutDurationSetting); @@ -163,7 +162,21 @@ export function NavigationMenuPageContent() { closeNavigationMenu(); navigate(RouterPath.ImportAccountFromTorus); } - } + }, + ...(isLedgerAvailable + ? [ + { + id: 5, + title: t('Connect Ledger'), + iconPath: 'assets/icons/ledger-blue.svg', + disabled: false, + handleOnClick: () => { + closeNavigationMenu(); + navigate(RouterPath.ImportAccountFromLedger); + } + } + ] + : []) ] }, { diff --git a/src/apps/popup/pages/sign-with-ledger-in-new-window/index.tsx b/src/apps/popup/pages/sign-with-ledger-in-new-window/index.tsx new file mode 100644 index 000000000..8f3b04e16 --- /dev/null +++ b/src/apps/popup/pages/sign-with-ledger-in-new-window/index.tsx @@ -0,0 +1,88 @@ +import { DeployUtil } from 'casper-js-sdk'; +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; + +import { fetchAndDispatchExtendedDeployInfo } from '@src/utils'; + +import { + selectLedgerDeploy, + selectLedgerRecipientToSaveOnSuccess +} from '@background/redux/ledger/selectors'; +import { recipientPublicKeyAdded } from '@background/redux/recent-recipient-public-keys/actions'; +import { selectApiConfigBasedOnActiveNetwork } from '@background/redux/settings/selectors'; +import { dispatchToMainStore } from '@background/redux/utils'; +import { selectVaultActiveAccount } from '@background/redux/vault/selectors'; + +import { useLedger } from '@hooks/use-ledger'; + +import { createAsymmetricKey } from '@libs/crypto/create-asymmetric-key'; +import { sendSignDeploy, signDeploy } from '@libs/services/deployer-service'; +import { LedgerEventStatus } from '@libs/services/ledger'; +import { LedgerConnectionView } from '@libs/ui/components'; + +import { SuccessView } from './success-view'; + +export const SignWithLedgerInNewWindowPage = () => { + const deploy = useSelector(selectLedgerDeploy); + const recipient = useSelector(selectLedgerRecipientToSaveOnSuccess); + const activeAccount = useSelector(selectVaultActiveAccount); + const { nodeUrl } = useSelector(selectApiConfigBasedOnActiveNetwork); + const [isSuccess, setIsSuccess] = useState(false); + + const ledgerAction = async () => { + if (!(activeAccount && deploy)) { + return; + } + + const KEYS = createAsymmetricKey( + activeAccount.publicKey, + activeAccount.secretKey + ); + + const resp = DeployUtil.deployFromJson(JSON.parse(deploy)); + + if (!resp.ok) { + console.log('-------- json parse error', resp.val); + return; + } + + const signedDeploy = await signDeploy(resp.val, [KEYS], activeAccount); + + sendSignDeploy(signedDeploy, nodeUrl) + .then(resp => { + if (recipient) { + dispatchToMainStore(recipientPublicKeyAdded(recipient)); + } + + if ('result' in resp) { + fetchAndDispatchExtendedDeployInfo(resp.result.deploy_hash); + } + + setIsSuccess(true); + }) + .catch(error => { + console.error(error, 'transfer request error'); + }); + }; + + const { + ledgerEventStatusToRender, + makeSubmitLedgerAction, + closeNewLedgerWindowsAndClearState + } = useLedger({ + ledgerAction, + beforeLedgerActionCb: async () => {}, + initialEventToRender: { status: LedgerEventStatus.LedgerAskPermission }, + withWaitingEventOnDisconnect: false + }); + + return isSuccess ? ( + + ) : ( + + ); +}; diff --git a/src/apps/popup/pages/sign-with-ledger-in-new-window/success-view.tsx b/src/apps/popup/pages/sign-with-ledger-in-new-window/success-view.tsx new file mode 100644 index 000000000..ee84b47c3 --- /dev/null +++ b/src/apps/popup/pages/sign-with-ledger-in-new-window/success-view.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { + ContentContainer, + FooterButtonsContainer, + HeaderPopup, + ParagraphContainer, + PopupLayout, + SpacingSize, + VerticalSpaceContainer +} from '@libs/layout'; +import { Button, SvgIcon, Typography } from '@libs/ui/components'; + +interface ISuccessViewProps { + onClose: () => void; +} + +export const SuccessView: React.FC = ({ onClose }) => { + const { t } = useTranslation(); + + return ( + ( + + )} + renderContent={() => ( + + + + + + Deploy successfully sent + + + + + + You can close this window and continue to use extension + + + + + + )} + renderFooter={() => ( + + + + )} + /> + ); +}; diff --git a/src/apps/popup/pages/stakes/content.tsx b/src/apps/popup/pages/stakes/content.tsx index 2cc376506..5492eb9ef 100644 --- a/src/apps/popup/pages/stakes/content.tsx +++ b/src/apps/popup/pages/stakes/content.tsx @@ -17,8 +17,13 @@ import { SpacingSize, VerticalSpaceContainer } from '@libs/layout'; +import { ILedgerEvent } from '@libs/services/ledger'; import { ValidatorResultWithId } from '@libs/services/validators-service/types'; -import { TransferSuccessScreen, Typography } from '@libs/ui/components'; +import { + LedgerEventView, + TransferSuccessScreen, + Typography +} from '@libs/ui/components'; import { StakeAmountFormValues, StakeNewValidatorFormValues, @@ -45,6 +50,7 @@ interface DelegateStakePageContentProps { validatorList: ValidatorResultWithId[] | null; undelegateValidatorList: ValidatorResultWithId[] | null; loading: boolean; + LedgerEventStatus: ILedgerEvent; } export const StakesPageContent = ({ @@ -62,7 +68,8 @@ export const StakesPageContent = ({ setStakeAmount, validatorList, undelegateValidatorList, - loading + loading, + LedgerEventStatus }: DelegateStakePageContentProps) => { const { t } = useTranslation(); @@ -77,89 +84,81 @@ export const StakesPageContent = ({ amountStepMaxAmountValue } = useStakeActionTexts(stakesType, stakeAmountMotes); - switch (stakeStep) { - case StakeSteps.Validator: { - return ( - - - - ); - } - case StakeSteps.Amount: { - return ( - - - - ); - } - case StakeSteps.NewValidator: { - return ( - - - - - Amount: - - {`${inputAmountCSPR} CSPR`} - - - - - ); - } - case StakeSteps.Confirm: { - return ( - - - - ); - } - case StakeSteps.Success: { - return ( - - {stakesType === AuctionManagerEntryPoint.redelegate ? ( - - - - I usually takes around{' '} - 14 to 16 hours{' '} - for this operation to complete. - - - - ) : null} - - ); - } - default: { - throw Error('Out of bound: StakeSteps'); - } - } + const getContent = { + [StakeSteps.Validator]: ( + + + + ), + [StakeSteps.Amount]: ( + + + + ), + [StakeSteps.NewValidator]: ( + + + + + Amount: + + {`${inputAmountCSPR} CSPR`} + + + + + ), + [StakeSteps.Confirm]: ( + + + + ), + [StakeSteps.ConfirmWithLedger]: ( + + ), + [StakeSteps.Success]: ( + + {stakesType === AuctionManagerEntryPoint.redelegate ? ( + + + + I usually takes around{' '} + 14 to 16 hours for + this operation to complete. + + + + ) : null} + + ) + }; + + return getContent[stakeStep]; }; diff --git a/src/apps/popup/pages/stakes/index.tsx b/src/apps/popup/pages/stakes/index.tsx index ea1907278..698153f33 100644 --- a/src/apps/popup/pages/stakes/index.tsx +++ b/src/apps/popup/pages/stakes/index.tsx @@ -1,3 +1,4 @@ +import { DeployUtil } from 'casper-js-sdk'; import React, { useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; @@ -15,33 +16,49 @@ import { useConfirmationButtonText } from '@popup/pages/stakes/utils'; import { RouterPath, useTypedLocation, useTypedNavigate } from '@popup/router'; import { selectAccountBalance } from '@background/redux/account-info/selectors'; +import { ledgerDeployChanged } from '@background/redux/ledger/actions'; import { selectAskForReviewAfter, selectRatedInStore } from '@background/redux/rate-app/selectors'; import { selectApiConfigBasedOnActiveNetwork } from '@background/redux/settings/selectors'; -import { selectVaultActiveAccount } from '@background/redux/vault/selectors'; +import { dispatchToMainStore } from '@background/redux/utils'; +import { + selectIsActiveAccountFromLedger, + selectVaultActiveAccount +} from '@background/redux/vault/selectors'; + +import { useLedger } from '@hooks/use-ledger'; import { createAsymmetricKey } from '@libs/crypto/create-asymmetric-key'; import { + AlignedFlexRow, ErrorPath, FooterButtonsContainer, HeaderPopup, HeaderSubmenuBarNavLink, PopupLayout, SpaceBetweenFlexRow, + SpacingSize, createErrorLocationState } from '@libs/layout'; import { - makeAuctionManagerDeployAndSing, - sendSignDeploy + makeAuctionManagerDeploy, + sendSignDeploy, + signDeploy } from '@libs/services/deployer-service'; import { dispatchFetchAuctionValidatorsRequest, dispatchFetchValidatorsDetailsDataRequest } from '@libs/services/validators-service'; import { ValidatorResultWithId } from '@libs/services/validators-service/types'; -import { Button, HomePageTabsId, Typography } from '@libs/ui/components'; +import { + Button, + HomePageTabsId, + SvgIcon, + Typography, + renderLedgerFooter +} from '@libs/ui/components'; import { calculateSubmitButtonDisabled } from '@libs/ui/forms/get-submit-button-state-from-validation'; import { useStakesForm } from '@libs/ui/forms/stakes-form'; import { CSPRtoMotes, formatNumber, motesToCSPR } from '@libs/ui/utils'; @@ -70,6 +87,9 @@ export const StakesPage = () => { const [loading, setLoading] = useState(true); const activeAccount = useSelector(selectVaultActiveAccount); + const isActiveAccountFromLedger = useSelector( + selectIsActiveAccountFromLedger + ); const { networkName, nodeUrl, @@ -229,7 +249,7 @@ export const StakesPage = () => { activeAccount.secretKey ); - const signDeploy = await makeAuctionManagerDeployAndSing( + const deploy = await makeAuctionManagerDeploy( stakesType, activeAccount.publicKey, validatorPublicKey, @@ -237,11 +257,12 @@ export const StakesPage = () => { motesAmount, networkName, auctionManagerContractHash, - nodeUrl, - [KEYS] + nodeUrl ); - sendSignDeploy(signDeploy, nodeUrl) + const signedDeploy = await signDeploy(deploy, [KEYS], activeAccount); + + sendSignDeploy(signedDeploy, nodeUrl) .then(resp => { if ('result' in resp) { fetchAndDispatchExtendedDeployInfo(resp.result.deploy_hash); @@ -285,6 +306,34 @@ export const StakesPage = () => { } }; + const beforeLedgerActionCb = async () => { + setStakeStep(StakeSteps.ConfirmWithLedger); + + if (activeAccount) { + const motesAmount = CSPRtoMotes(inputAmountCSPR); + + const deploy = await makeAuctionManagerDeploy( + stakesType, + activeAccount.publicKey, + validatorPublicKey, + newValidatorPublicKey || null, + motesAmount, + networkName, + auctionManagerContractHash, + nodeUrl + ); + + dispatchToMainStore( + ledgerDeployChanged(JSON.stringify(DeployUtil.deployToJson(deploy))) + ); + } + }; + + const { ledgerEventStatusToRender, makeSubmitLedgerAction } = useLedger({ + ledgerAction: submitStake, + beforeLedgerActionCb + }); + const getButtonProps = () => { const isValidatorFormButtonDisabled = calculateSubmitButtonDisabled({ isValid: validatorFormState.isValid @@ -341,7 +390,9 @@ export const StakesPage = () => { isSubmitButtonDisable || isValidatorFormButtonDisabled || isAmountFormButtonDisabled, - onClick: submitStake + onClick: isActiveAccountFromLedger + ? makeSubmitLedgerAction() + : submitStake }; } case StakeSteps.Success: { @@ -373,34 +424,46 @@ export const StakesPage = () => { } }; - const handleBackButton = () => { - switch (stakeStep) { - case StakeSteps.Validator: { - navigate(-1); - break; - } - case StakeSteps.Amount: { - setStakeStep(StakeSteps.Validator); - break; - } - case StakeSteps.NewValidator: { - setStakeStep(StakeSteps.Amount); - break; - } - case StakeSteps.Confirm: { - if (stakesType === AuctionManagerEntryPoint.redelegate) { - setStakeStep(StakeSteps.NewValidator); - } else { - setStakeStep(StakeSteps.Amount); + const getBackButton = { + [StakeSteps.Validator]: () => ( + navigate(-1)} + /> + ), + [StakeSteps.Amount]: () => ( + setStakeStep(StakeSteps.Validator)} + /> + ), + [StakeSteps.NewValidator]: () => ( + setStakeStep(StakeSteps.Amount)} + /> + ), + [StakeSteps.Confirm]: () => ( + + stakesType === AuctionManagerEntryPoint.redelegate + ? setStakeStep(StakeSteps.NewValidator) + : setStakeStep(StakeSteps.Amount) } - break; - } - - default: { - navigate(-1); - break; - } - } + /> + ), + [StakeSteps.ConfirmWithLedger]: () => ( + setStakeStep(StakeSteps.Confirm)} + /> + ), + [StakeSteps.Success]: undefined }; const confirmButtonText = useConfirmationButtonText(stakesType); @@ -431,6 +494,50 @@ export const StakesPage = () => { ); } + const renderFooter = () => { + if (stakeStep === StakeSteps.ConfirmWithLedger) { + return renderLedgerFooter({ + onConnect: makeSubmitLedgerAction, + event: ledgerEventStatusToRender, + onErrorCtaPressed: () => setStakeStep(StakeSteps.Confirm) + }); + } + + return () => ( + + {stakeStep === StakeSteps.Amount ? ( + + + Transaction fee + + + {formatNumber(motesToCSPR(STAKE_COST_MOTES), { + precision: { max: 5 } + })}{' '} + CSPR + + + ) : null} + + + ); + }; + return ( ( @@ -438,17 +545,7 @@ export const StakesPage = () => { withNetworkSwitcher withMenu withConnectionStatus - renderSubmenuBarItems={ - stakeStep === StakeSteps.Success - ? undefined - : () => ( - - ) - } + renderSubmenuBarItems={getBackButton[stakeStep]} /> )} renderContent={() => ( @@ -468,34 +565,10 @@ export const StakesPage = () => { validatorList={validatorList} undelegateValidatorList={undelegateValidatorList} loading={loading} + LedgerEventStatus={ledgerEventStatusToRender} /> )} - renderFooter={() => ( - - {stakeStep === StakeSteps.Amount ? ( - - - Transaction fee - - - {formatNumber(motesToCSPR(STAKE_COST_MOTES), { - precision: { max: 5 } - })}{' '} - CSPR - - - ) : null} - - - )} + renderFooter={renderFooter()} /> ); }; diff --git a/src/apps/popup/pages/transfer-nft/index.tsx b/src/apps/popup/pages/transfer-nft/index.tsx index 63b51b7ab..d28cc16ad 100644 --- a/src/apps/popup/pages/transfer-nft/index.tsx +++ b/src/apps/popup/pages/transfer-nft/index.tsx @@ -1,3 +1,4 @@ +import { DeployUtil } from 'casper-js-sdk'; import React, { useEffect, useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; @@ -21,6 +22,10 @@ import { selectAccountNftTokens } from '@background/redux/account-info/selectors'; import { selectAllPublicKeys } from '@background/redux/contacts/selectors'; +import { + ledgerDeployChanged, + ledgerRecipientToSaveOnSuccessChanged +} from '@background/redux/ledger/actions'; import { selectAskForReviewAfter, selectRatedInStore @@ -28,26 +33,37 @@ import { import { recipientPublicKeyAdded } from '@background/redux/recent-recipient-public-keys/actions'; import { selectApiConfigBasedOnActiveNetwork } from '@background/redux/settings/selectors'; import { dispatchToMainStore } from '@background/redux/utils'; -import { selectVaultActiveAccount } from '@background/redux/vault/selectors'; +import { + selectIsActiveAccountFromLedger, + selectVaultActiveAccount +} from '@background/redux/vault/selectors'; + +import { useLedger } from '@hooks/use-ledger'; import { createAsymmetricKey } from '@libs/crypto/create-asymmetric-key'; import { getRawPublicKey } from '@libs/entities/Account'; import { + AlignedFlexRow, ErrorPath, FooterButtonsContainer, HeaderPopup, HeaderSubmenuBarNavLink, PopupLayout, + SpacingSize, createErrorLocationState } from '@libs/layout'; import { - makeNFTDeployAndSign, - sendSignDeploy + makeNFTDeploy, + sendSignDeploy, + signDeploy } from '@libs/services/deployer-service'; import { Button, HomePageTabsId, - TransferSuccessScreen + LedgerEventView, + SvgIcon, + TransferSuccessScreen, + renderLedgerFooter } from '@libs/ui/components'; import { calculateSubmitButtonDisabled } from '@libs/ui/forms/get-submit-button-state-from-validation'; import { useTransferNftForm } from '@libs/ui/forms/transfer-nft'; @@ -56,11 +72,16 @@ import { CSPRtoMotes } from '@libs/ui/utils'; export const TransferNftPage = () => { const [showSuccessScreen, setShowSuccessScreen] = useState(false); const [haveReverseOwnerLookUp, setHaveReverseOwnerLookUp] = useState(false); + const [showLedgerConfirm, setShowLedgerConfirm] = useState(false); + const { contractPackageHash, tokenId } = useParams(); const nftTokens = useSelector(selectAccountNftTokens); const csprBalance = useSelector(selectAccountBalance); const activeAccount = useSelector(selectVaultActiveAccount); + const isActiveAccountFromLedger = useSelector( + selectIsActiveAccountFromLedger + ); const { networkName, nodeUrl } = useSelector( selectApiConfigBasedOnActiveNetwork ); @@ -141,17 +162,18 @@ export const TransferNftPage = () => { target: getRawPublicKey(recipientPublicKey) }; - const signDeploy = await makeNFTDeployAndSign( + const deploy = await makeNFTDeploy( getRuntimeArgs(tokenStandard, args), CSPRtoMotes(paymentAmount), KEYS.publicKey, networkName, nftToken?.contract_package_hash!, - nodeUrl, - [KEYS] + nodeUrl ); - sendSignDeploy(signDeploy, nodeUrl) + const signedDeploy = await signDeploy(deploy, [KEYS], activeAccount); + + sendSignDeploy(signedDeploy, nodeUrl) .then(resp => { dispatchToMainStore(recipientPublicKeyAdded(recipientPublicKey)); @@ -206,6 +228,127 @@ export const TransferNftPage = () => { } }; + const beforeLedgerActionCb = async () => { + setShowLedgerConfirm(true); + + if (haveReverseOwnerLookUp || !nftToken || !activeAccount) return; + + const KEYS = createAsymmetricKey( + activeAccount.publicKey, + activeAccount.secretKey + ); + + const args = { + tokenId: nftToken.token_id, + source: KEYS.publicKey, + target: getRawPublicKey(recipientPublicKey) + }; + + const deploy = await makeNFTDeploy( + getRuntimeArgs(tokenStandard, args), + CSPRtoMotes(paymentAmount), + KEYS.publicKey, + networkName, + nftToken?.contract_package_hash!, + nodeUrl + ); + + dispatchToMainStore( + ledgerDeployChanged(JSON.stringify(DeployUtil.deployToJson(deploy))) + ); + dispatchToMainStore( + ledgerRecipientToSaveOnSuccessChanged(recipientPublicKey) + ); + }; + + const { ledgerEventStatusToRender, makeSubmitLedgerAction } = useLedger({ + ledgerAction: submitTransfer, + beforeLedgerActionCb + }); + + const renderFooter = () => { + if (showLedgerConfirm && !showSuccessScreen) { + return renderLedgerFooter({ + onConnect: makeSubmitLedgerAction, + event: ledgerEventStatusToRender, + onErrorCtaPressed: () => setShowLedgerConfirm(false) + }); + } + + return () => ( + + {showSuccessScreen ? ( + <> + + + {!isRecipientPublicKeyInContact && ( + + )} + + ) : ( + + )} + + ); + }; + return ( ( @@ -216,13 +359,22 @@ export const TransferNftPage = () => { renderSubmenuBarItems={ showSuccessScreen ? undefined - : () => + : showLedgerConfirm + ? () => ( + setShowLedgerConfirm(false)} + /> + ) + : () => } /> )} renderContent={() => showSuccessScreen ? ( + ) : showLedgerConfirm ? ( + ) : ( { /> ) } - renderFooter={() => ( - - {showSuccessScreen ? ( - <> - - - {!isRecipientPublicKeyInContact && ( - - )} - - ) : ( - - )} - - )} + renderFooter={renderFooter()} /> ); }; diff --git a/src/apps/popup/pages/transfer/content.tsx b/src/apps/popup/pages/transfer/content.tsx index 2e171dba1..5b3d1d7bf 100644 --- a/src/apps/popup/pages/transfer/content.tsx +++ b/src/apps/popup/pages/transfer/content.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { UseFormReturn } from 'react-hook-form'; -import { TransferSuccessScreen } from '@libs/ui/components'; +import { ILedgerEvent } from '@libs/services/ledger'; +import { LedgerEventView, TransferSuccessScreen } from '@libs/ui/components'; import { TransferAmountFormValues, TransferRecipientFormValues @@ -21,6 +22,7 @@ interface TransferPageContentProps { balance: string | null; symbol: string | null; paymentAmount: string; + LedgerEventStatus: ILedgerEvent; } export const TransferPageContent = ({ @@ -31,49 +33,44 @@ export const TransferPageContent = ({ amount, balance, symbol, - paymentAmount + paymentAmount, + LedgerEventStatus }: TransferPageContentProps) => { const [recipientName, setRecipientName] = useState(''); const isCSPR = symbol === 'CSPR'; - switch (transferStep) { - case TransactionSteps.Recipient: { - return ( - - ); - } - case TransactionSteps.Amount: { - return ( - - ); - } - case TransactionSteps.Confirm: { - return ( - - ); - } + const getContent = { + [TransactionSteps.Recipient]: ( + + ), + [TransactionSteps.Amount]: ( + + ), + [TransactionSteps.Confirm]: ( + + ), + [TransactionSteps.ConfirmWithLedger]: ( + + ), + [TransactionSteps.Success]: ( + + ) + }; - case TransactionSteps.Success: { - return ; - } - - default: { - throw Error('Out of bound: TransactionSteps'); - } - } + return getContent[transferStep]; }; diff --git a/src/apps/popup/pages/transfer/index.tsx b/src/apps/popup/pages/transfer/index.tsx index ba5c7bb9c..0f83091e0 100644 --- a/src/apps/popup/pages/transfer/index.tsx +++ b/src/apps/popup/pages/transfer/index.tsx @@ -16,6 +16,10 @@ import { RouterPath, useTypedLocation, useTypedNavigate } from '@popup/router'; import { selectAccountBalance } from '@background/redux/account-info/selectors'; import { selectAllPublicKeys } from '@background/redux/contacts/selectors'; +import { + ledgerDeployChanged, + ledgerRecipientToSaveOnSuccessChanged +} from '@background/redux/ledger/actions'; import { selectAskForReviewAfter, selectRatedInStore @@ -23,24 +27,39 @@ import { import { recipientPublicKeyAdded } from '@background/redux/recent-recipient-public-keys/actions'; import { selectApiConfigBasedOnActiveNetwork } from '@background/redux/settings/selectors'; import { dispatchToMainStore } from '@background/redux/utils'; -import { selectVaultActiveAccount } from '@background/redux/vault/selectors'; +import { + selectIsActiveAccountFromLedger, + selectVaultActiveAccount +} from '@background/redux/vault/selectors'; + +import { useLedger } from '@hooks/use-ledger'; import { createAsymmetricKey } from '@libs/crypto/create-asymmetric-key'; import { + AlignedFlexRow, ErrorPath, FooterButtonsContainer, HeaderPopup, HeaderSubmenuBarNavLink, PopupLayout, SpaceBetweenFlexRow, + SpacingSize, createErrorLocationState } from '@libs/layout'; import { - makeCep18TransferDeployAndSign, - makeNativeTransferDeployAndSign, - sendSignDeploy + makeCep18TransferDeploy, + makeNativeTransferDeploy, + sendSignDeploy, + signDeploy } from '@libs/services/deployer-service'; -import { Button, HomePageTabsId, Typography } from '@libs/ui/components'; +import { HardwareWalletType } from '@libs/types/account'; +import { + Button, + HomePageTabsId, + SvgIcon, + Typography, + renderLedgerFooter +} from '@libs/ui/components'; import { calculateSubmitButtonDisabled } from '@libs/ui/forms/get-submit-button-state-from-validation'; import { useTransferForm } from '@libs/ui/forms/transfer'; import { @@ -73,6 +92,9 @@ export const TransferPage = () => { const [isSubmitButtonDisable, setIsSubmitButtonDisable] = useState(true); const activeAccount = useSelector(selectVaultActiveAccount); + const isActiveAccountFromLedger = useSelector( + selectIsActiveAccountFromLedger + ); const { networkName, nodeUrl } = useSelector( selectApiConfigBasedOnActiveNetwork ); @@ -226,9 +248,10 @@ export const TransferPage = () => { activeAccount.publicKey, activeAccount.secretKey ); + if (isErc20Transfer) { // ERC20 transfer - const signDeploy = await makeCep18TransferDeployAndSign( + const deploy = await makeCep18TransferDeploy( nodeUrl, networkName, tokenContractHash, @@ -237,30 +260,82 @@ export const TransferPage = () => { amount, erc20Decimals, paymentAmount, - activeAccount, - [KEYS] + activeAccount ); - sendDeploy(signDeploy); + const signedDeploy = await signDeploy(deploy, [KEYS], activeAccount); + + sendDeploy(signedDeploy); } else { // CSPR transfer const motesAmount = CSPRtoMotes(amount); - const signDeploy = await makeNativeTransferDeployAndSign( - activeAccount.publicKey, + const deploy = await makeNativeTransferDeploy( + activeAccount, recipientPublicKey, motesAmount, networkName, nodeUrl, - [KEYS], transferIdMemo ); - sendDeploy(signDeploy); + const signedDeploy = await signDeploy(deploy, [KEYS], activeAccount); + + sendDeploy(signedDeploy); } } }; + const beforeLedgerActionCb = async () => { + setTransferStep(TransactionSteps.ConfirmWithLedger); + + if (activeAccount?.hardware === HardwareWalletType.Ledger) { + if (isErc20Transfer) { + const deploy = await makeCep18TransferDeploy( + nodeUrl, + networkName, + tokenContractHash, + tokenContractPackageHash, + recipientPublicKey, + amount, + erc20Decimals, + paymentAmount, + activeAccount + ); + + dispatchToMainStore( + ledgerDeployChanged(JSON.stringify(DeployUtil.deployToJson(deploy))) + ); + dispatchToMainStore( + ledgerRecipientToSaveOnSuccessChanged(recipientPublicKey) + ); + } else { + const motesAmount = CSPRtoMotes(amount); + + const deploy = await makeNativeTransferDeploy( + activeAccount, + recipientPublicKey, + motesAmount, + networkName, + nodeUrl, + transferIdMemo + ); + + dispatchToMainStore( + ledgerDeployChanged(JSON.stringify(DeployUtil.deployToJson(deploy))) + ); + dispatchToMainStore( + ledgerRecipientToSaveOnSuccessChanged(recipientPublicKey) + ); + } + } + }; + + const { ledgerEventStatusToRender, makeSubmitLedgerAction } = useLedger({ + ledgerAction: onSubmitSending, + beforeLedgerActionCb + }); + const getButtonProps = () => { const isRecipientFormButtonDisabled = calculateSubmitButtonDisabled({ isValid: recipientFormState.isValid @@ -304,7 +379,9 @@ export const TransferPage = () => { isSubmitButtonDisable || isRecipientFormButtonDisabled || isAmountFormButtonDisabled, - onClick: onSubmitSending + onClick: isActiveAccountFromLedger + ? makeSubmitLedgerAction() + : onSubmitSending }; } case TransactionSteps.Success: { @@ -335,64 +412,48 @@ export const TransferPage = () => { } }; - const handleBackButton = () => { - switch (transferStep) { - case TransactionSteps.Recipient: { - navigate(-1); - break; - } - case TransactionSteps.Amount: { - setTransferStep(TransactionSteps.Recipient); - break; - } - case TransactionSteps.Confirm: { - setTransferStep(TransactionSteps.Amount); - break; - } - - default: { - navigate(-1); - break; - } - } + const getBackButton = { + [TransactionSteps.Recipient]: () => ( + navigate(-1)} /> + ), + [TransactionSteps.Amount]: () => ( + setTransferStep(TransactionSteps.Recipient)} + /> + ), + [TransactionSteps.Confirm]: () => ( + setTransferStep(TransactionSteps.Amount)} + /> + ), + [TransactionSteps.ConfirmWithLedger]: () => ( + setTransferStep(TransactionSteps.Confirm)} + /> + ), + [TransactionSteps.Success]: undefined }; const transactionFee = isErc20Transfer ? `${paymentAmount}` : `${motesToCSPR(TRANSFER_COST_MOTES)}`; - return ( - ( - ( - - ) - } - /> - )} - renderContent={() => ( - - )} - renderFooter={() => ( + const renderFooter = () => { + if (transferStep === TransactionSteps.ConfirmWithLedger) { + return renderLedgerFooter({ + onConnect: makeSubmitLedgerAction, + event: ledgerEventStatusToRender, + onErrorCtaPressed: () => { + setTransferStep(TransactionSteps.Confirm); + } + }); + } + + return () => { + return ( {transferStep === TransactionSteps.Confirm || transferStep === TransactionSteps.Success ? null : ( @@ -409,13 +470,21 @@ export const TransferPage = () => { )} {transferStep === TransactionSteps.Success && !isRecipientPublicKeyInContact && ( @@ -433,7 +502,34 @@ export const TransferPage = () => { )} + ); + }; + }; + + return ( + ( + + )} + renderContent={() => ( + )} + renderFooter={renderFooter()} /> ); }; diff --git a/src/apps/popup/pages/transfer/utils.ts b/src/apps/popup/pages/transfer/utils.ts index e4c15a9ef..4a6653c12 100644 --- a/src/apps/popup/pages/transfer/utils.ts +++ b/src/apps/popup/pages/transfer/utils.ts @@ -2,6 +2,7 @@ export enum TransactionSteps { Recipient = 'recipient', Amount = 'amount', Confirm = 'confirm', + ConfirmWithLedger = 'confirm with ledger', Success = 'success' } diff --git a/src/apps/popup/router/paths.ts b/src/apps/popup/router/paths.ts index 05dc4197b..9558b75ad 100644 --- a/src/apps/popup/router/paths.ts +++ b/src/apps/popup/router/paths.ts @@ -31,5 +31,7 @@ export enum RouterPath { RateApp = '/rate-app', AllAccountsList = '/accounts-list', ImportAccountFromTorus = '/import-account-from-torus', - BuyCSPR = '/buy-cspr' + BuyCSPR = '/buy-cspr', + ImportAccountFromLedger = '/import-account-from-ledger', + SignWithLedgerInNewWindow = '/sign-with-ledger-in-new-window' } diff --git a/src/apps/signature-request/pages/sign-deploy/index.tsx b/src/apps/signature-request/pages/sign-deploy/index.tsx index 3aaa56cb3..74fd65ec9 100644 --- a/src/apps/signature-request/pages/sign-deploy/index.tsx +++ b/src/apps/signature-request/pages/sign-deploy/index.tsx @@ -5,6 +5,8 @@ import { useSelector } from 'react-redux'; import { getSigningAccount } from '@src/utils'; +import { RouterPath } from '@signature-request/router'; + import { closeCurrentWindow } from '@background/close-current-window'; import { selectConnectedAccountNamesWithActiveOrigin, @@ -13,16 +15,28 @@ import { } from '@background/redux/vault/selectors'; import { sendSdkResponseToSpecificTab } from '@background/send-sdk-response-to-specific-tab'; +import { useLedger } from '@hooks/use-ledger'; + import { sdkMethod } from '@content/sdk-method'; import { signDeploy } from '@libs/crypto'; import { convertBytesToHex } from '@libs/crypto/utils'; import { + AlignedFlexRow, FooterButtonsContainer, HeaderPopup, - LayoutWindow + HeaderSubmenuBarNavLink, + LayoutWindow, + SpacingSize } from '@libs/layout'; -import { Button } from '@libs/ui/components'; +import { LedgerEventStatus, ledger } from '@libs/services/ledger'; +import { HardwareWalletType } from '@libs/types/account'; +import { + Button, + LedgerEventView, + SvgIcon, + renderLedgerFooter +} from '@libs/ui/components'; import { CasperDeploy } from './deploy-types'; import { SignDeployContent } from './sign-deploy-content'; @@ -31,10 +45,18 @@ export function SignDeployPage() { const { t } = useTranslation(); const [deploy, setDeploy] = useState(undefined); + const [isSigningAccountFromLedger, setIsSigningAccountFromLedger] = + useState(false); const searchParams = new URLSearchParams(document.location.search); + const isLedgerNewWindow = Boolean(searchParams.get('initialEventToRender')); const requestId = searchParams.get('requestId'); const signingPublicKeyHex = searchParams.get('signingPublicKeyHex'); + const initialEventToRender = + (searchParams.get('initialEventToRender') as LedgerEventStatus) ?? + LedgerEventStatus.Disconnected; + const [showLedgerConfirm, setShowLedgerConfirm] = + useState(isLedgerNewWindow); if (!requestId || !signingPublicKeyHex) { throw Error('Missing search param'); @@ -55,7 +77,16 @@ export function SignDeployPage() { renderDeps ); + useEffect(() => { + const signingAccount = getSigningAccount(accounts, signingPublicKeyHex); + + setIsSigningAccountFromLedger( + signingAccount?.hardware === HardwareWalletType.Ledger + ); + }, [accounts, signingPublicKeyHex]); + const deployJsonById = useSelector(selectDeploysJsonById); + useEffect(() => { const deployJson = deployJsonById[requestId]; if (deployJson == null) { @@ -87,7 +118,8 @@ export function SignDeployPage() { // signing account should be connected to site if ( connectedAccountNames != null && - !connectedAccountNames.includes(signingAccount.name) + !connectedAccountNames.includes(signingAccount.name) && + !isLedgerNewWindow ) { const error = Error( 'Account with signingPublicKeyHex is not connected to site' @@ -96,16 +128,32 @@ export function SignDeployPage() { throw error; } - const handleSign = useCallback(() => { + const handleSign = useCallback(async () => { if (deploy?.hash == null) { return; } - const signature = signDeploy( - deploy.hash, - signingAccount.publicKey, - signingAccount.secretKey - ); + let signature: Uint8Array; + + if (signingAccount.hardware === HardwareWalletType.Ledger) { + const resp = await ledger.singDeploy(deploy, { + index: signingAccount.derivationIndex, + publicKey: signingAccount.publicKey + }); + + signature = resp.signature; + } else { + signature = signDeploy( + deploy.hash, + signingAccount.publicKey, + signingAccount.secretKey + ); + } + + if (!signature) { + return; + } + sendSdkResponseToSpecificTab( sdkMethod.signResponse( { signatureHex: convertBytesToHex(signature), cancelled: false }, @@ -114,9 +162,11 @@ export function SignDeployPage() { ); closeCurrentWindow(); }, [ - signingAccount?.publicKey, - signingAccount?.secretKey, - deploy?.hash, + deploy, + signingAccount.hardware, + signingAccount.derivationIndex, + signingAccount.publicKey, + signingAccount.secretKey, requestId ]); @@ -133,29 +183,91 @@ export function SignDeployPage() { return () => window.removeEventListener('beforeunload', handleCancel); }, [handleCancel]); + const { + ledgerEventStatusToRender, + makeSubmitLedgerAction, + closeNewLedgerWindowsAndClearState + } = useLedger({ + ledgerAction: handleSign, + beforeLedgerActionCb: async () => setShowLedgerConfirm(true), + initialEventToRender: { status: initialEventToRender }, + withWaitingEventOnDisconnect: false, + askPermissionUrlData: { + domain: 'signature-request.html', + params: { + requestId, + signingPublicKeyHex + }, + hash: RouterPath.SignDeploy + } + }); + + const onErrorCtaPressed = () => { + setShowLedgerConfirm(false); + closeNewLedgerWindowsAndClearState(); + }; + + const renderFooter = () => { + if (showLedgerConfirm) { + return renderLedgerFooter({ + onConnect: makeSubmitLedgerAction, + onErrorCtaPressed, + event: ledgerEventStatusToRender + }); + } + + return () => ( + + + + + ); + }; + return ( } - renderContent={() => ( - ( + ( + + ) + : undefined + } /> )} - renderFooter={() => ( - - - - - )} + renderContent={() => + showLedgerConfirm ? ( + + ) : ( + + ) + } + renderFooter={renderFooter()} /> ); } diff --git a/src/apps/signature-request/pages/sign-message/index.tsx b/src/apps/signature-request/pages/sign-message/index.tsx index 5b3c7d069..95c24be49 100644 --- a/src/apps/signature-request/pages/sign-message/index.tsx +++ b/src/apps/signature-request/pages/sign-message/index.tsx @@ -1,9 +1,11 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { getSigningAccount } from '@src/utils'; +import { RouterPath } from '@signature-request/router'; + import { closeCurrentWindow } from '@background/close-current-window'; import { selectConnectedAccountNamesWithActiveOrigin, @@ -11,26 +13,46 @@ import { } from '@background/redux/vault/selectors'; import { sendSdkResponseToSpecificTab } from '@background/send-sdk-response-to-specific-tab'; +import { useLedger } from '@hooks/use-ledger'; + import { sdkMethod } from '@content/sdk-method'; import { signMessage } from '@libs/crypto/sign-message'; import { convertBytesToHex } from '@libs/crypto/utils'; import { + AlignedFlexRow, FooterButtonsContainer, HeaderPopup, - LayoutWindow + HeaderSubmenuBarNavLink, + LayoutWindow, + SpacingSize } from '@libs/layout'; -import { Button } from '@libs/ui/components'; +import { LedgerEventStatus, ledger } from '@libs/services/ledger'; +import { HardwareWalletType } from '@libs/types/account'; +import { + Button, + LedgerEventView, + SvgIcon, + renderLedgerFooter +} from '@libs/ui/components'; import { SignMessageContent } from './sign-message-content'; export function SignMessagePage() { const { t } = useTranslation(); - const searchParams = new URLSearchParams(document.location.search); + const isLedgerNewWindow = Boolean(searchParams.get('initialEventToRender')); + const [isSigningAccountFromLedger, setIsSigningAccountFromLedger] = + useState(false); + const [showLedgerConfirm, setShowLedgerConfirm] = + useState(isLedgerNewWindow); + const requestId = searchParams.get('requestId'); const message = searchParams.get('message'); const signingPublicKeyHex = searchParams.get('signingPublicKeyHex'); + const initialEventToRender = + (searchParams.get('initialEventToRender') as LedgerEventStatus) ?? + LedgerEventStatus.Disconnected; if (!requestId || !message || !signingPublicKeyHex) { throw Error( @@ -53,6 +75,14 @@ export function SignMessagePage() { renderDeps ); + useEffect(() => { + const signingAccount = getSigningAccount(accounts, signingPublicKeyHex); + + setIsSigningAccountFromLedger( + signingAccount?.hardware === HardwareWalletType.Ledger + ); + }, [accounts, signingPublicKeyHex]); + const signingAccount = getSigningAccount(accounts, signingPublicKeyHex); // signing account should exist in wallet @@ -67,7 +97,8 @@ export function SignMessagePage() { // signing account should be connected to site if ( connectedAccountNames != null && - !connectedAccountNames.includes(signingAccount.name) + !connectedAccountNames.includes(signingAccount.name) && + !isLedgerNewWindow ) { const error = Error( 'Account with signingPublicKeyHex is not connected to site' @@ -78,16 +109,32 @@ export function SignMessagePage() { throw error; } - const handleSign = useCallback(() => { + const handleSign = useCallback(async () => { if (message == null) { return; } - const signature = signMessage( - message, - signingAccount.publicKey, - signingAccount.secretKey - ); + let signature: Uint8Array; + + if (signingAccount.hardware === HardwareWalletType.Ledger) { + const resp = await ledger.signMessage(message, { + index: signingAccount.derivationIndex, + publicKey: signingAccount.publicKey + }); + + signature = resp.signature; + } else { + signature = signMessage( + message, + signingAccount.publicKey, + signingAccount.secretKey + ); + } + + if (!signature) { + return; + } + sendSdkResponseToSpecificTab( sdkMethod.signMessageResponse( { signatureHex: convertBytesToHex(signature), cancelled: false }, @@ -96,12 +143,72 @@ export function SignMessagePage() { ); closeCurrentWindow(); }, [ - signingAccount?.publicKey, - signingAccount?.secretKey, message, + signingAccount.hardware, + signingAccount.derivationIndex, + signingAccount.publicKey, + signingAccount.secretKey, requestId ]); + const { + ledgerEventStatusToRender, + makeSubmitLedgerAction, + closeNewLedgerWindowsAndClearState + } = useLedger({ + ledgerAction: handleSign, + beforeLedgerActionCb: async () => setShowLedgerConfirm(true), + withWaitingEventOnDisconnect: false, + initialEventToRender: { status: initialEventToRender }, + askPermissionUrlData: { + domain: 'signature-request.html', + params: { + requestId, + signingPublicKeyHex, + message + }, + hash: RouterPath.SignMessage + } + }); + + const onErrorCtaPressed = () => { + setShowLedgerConfirm(false); + closeNewLedgerWindowsAndClearState(); + }; + + const renderFooter = () => { + if (showLedgerConfirm) { + return renderLedgerFooter({ + onConnect: makeSubmitLedgerAction, + onErrorCtaPressed, + event: ledgerEventStatusToRender + }); + } + + return () => ( + + + + + ); + }; + const handleCancel = useCallback(() => { sendSdkResponseToSpecificTab( sdkMethod.signResponse({ cancelled: true }, { requestId }) @@ -117,23 +224,31 @@ export function SignMessagePage() { return ( } - renderContent={() => ( - ( + ( + + ) + : undefined + } /> )} - renderFooter={() => ( - - - - - )} + renderContent={() => + showLedgerConfirm ? ( + + ) : ( + + ) + } + renderFooter={renderFooter()} /> ); } diff --git a/src/assets/icons/ledger-blue.svg b/src/assets/icons/ledger-blue.svg new file mode 100644 index 000000000..ab0f0a546 --- /dev/null +++ b/src/assets/icons/ledger-blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/ledger-white.svg b/src/assets/icons/ledger-white.svg new file mode 100644 index 000000000..2ecd38bf9 --- /dev/null +++ b/src/assets/icons/ledger-white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/illustrations/ledger-connect.svg b/src/assets/illustrations/ledger-connect.svg new file mode 100644 index 000000000..9353c81b6 --- /dev/null +++ b/src/assets/illustrations/ledger-connect.svg @@ -0,0 +1,1211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/ledger-error.svg b/src/assets/illustrations/ledger-error.svg new file mode 100644 index 000000000..205710823 --- /dev/null +++ b/src/assets/illustrations/ledger-error.svg @@ -0,0 +1,711 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/ledger-not-connected.svg b/src/assets/illustrations/ledger-not-connected.svg new file mode 100644 index 000000000..0e2524eff --- /dev/null +++ b/src/assets/illustrations/ledger-not-connected.svg @@ -0,0 +1,1170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/illustrations/ledger-rejected.svg b/src/assets/illustrations/ledger-rejected.svg new file mode 100644 index 000000000..e2bdc1a2c --- /dev/null +++ b/src/assets/illustrations/ledger-rejected.svg @@ -0,0 +1,442 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/background/create-open-window.ts b/src/background/create-open-window.ts index b837d18c1..1d328b18e 100644 --- a/src/background/create-open-window.ts +++ b/src/background/create-open-window.ts @@ -149,3 +149,44 @@ export function createOpenWindow({ } }; } + +export interface IOpenNewSeparateWindowParams { + url: string; +} + +export async function openNewSeparateWindow({ + url +}: IOpenNewSeparateWindowParams): Promise { + const currentWindow = await windows.getCurrent(); + + // If this flag is true, we create a new window without any size and positions. + const isTestEnv = Boolean(process.env.TEST_ENV); + + const windowWidth = currentWindow.width ?? 0; + const xOffset = currentWindow.left ?? 0; + const yOffset = currentWindow.top ?? 0; + const crossPlatformWidthOffset = 16; + const popupWidth = 360 + crossPlatformWidthOffset; + const popupHeight = 800; + const newWindow = + // We need this check for Firefox. If the Firefox browser is in fullscreen mode it ignores the width and height that we set and opens a popup in a small size. + // So we check it and if it is in a fullscreen mode we didn't set width and height, and the popup will also open in fullscreen mode. + // This is a default behavior for Safari and Chrome, but Firefox doesn't do this, so we need to do this manually for it. + currentWindow.state === 'fullscreen' || isTestEnv + ? await windows.create({ + url, + type: 'normal', + focused: true + }) + : await windows.create({ + url, + type: 'normal', + height: popupHeight, + width: popupWidth, + left: windowWidth + xOffset - popupWidth, + top: yOffset, + focused: true + }); + + return newWindow; +} diff --git a/src/background/index.ts b/src/background/index.ts index 4aa191a0b..3c2fd6513 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -63,6 +63,12 @@ import { CheckAccountNameIsTakenAction, CheckSecretKeyExistAction } from '@background/redux/import-account-actions-should-be-removed'; +import { + ledgerDeployChanged, + ledgerNewWindowIdChanged, + ledgerRecipientToSaveOnSuccessChanged, + ledgerStateCleared +} from '@background/redux/ledger/actions'; import { askForReviewAfterChanged, ratedInStoreChanged @@ -73,6 +79,7 @@ import { accountImported, accountRemoved, accountRenamed, + accountsImported, activeAccountChanged, anotherAccountConnected, deployPayloadReceived, @@ -575,6 +582,7 @@ runtime.onMessage.addListener( case getType(vaultReseted): case getType(secretPhraseCreated): case getType(accountImported): + case getType(accountsImported): case getType(accountAdded): case getType(accountRemoved): case getType(accountRenamed): @@ -634,6 +642,10 @@ runtime.onMessage.addListener( case getType(askForReviewAfterChanged): case getType(accountBalancesChanged): case getType(accountBalancesReseted): + case getType(ledgerNewWindowIdChanged): + case getType(ledgerStateCleared): + case getType(ledgerDeployChanged): + case getType(ledgerRecipientToSaveOnSuccessChanged): store.dispatch(action); return sendResponse(undefined); diff --git a/src/background/open-onboarding-flow.ts b/src/background/open-onboarding-flow.ts index dba6dd538..19e38ab60 100644 --- a/src/background/open-onboarding-flow.ts +++ b/src/background/open-onboarding-flow.ts @@ -24,6 +24,7 @@ export async function enableOnboardingFlow() { export async function openOnboardingUi() { const { tabId, windowId } = await loadState(); let tabExist = false; + if (tabId != null && windowId != null) { try { const tab = await tabs.get(tabId); @@ -38,7 +39,19 @@ export async function openOnboardingUi() { } } - if (!tabExist) { + // this needed for case when the user goes to another url from onboarding + // and then click on Casper Wallet from the extension menu + const tab = + tabId != null && + (await tabs.get(tabId).catch(() => { + // catch error if the tab does not exist + })); + // check if the tab URL is the onboarding URL + const isOnboardingUrl = tab && tab.url?.includes('onboarding.html'); + + // create a tab if it does not exist or if it's not an onboarding URL + if (!tabExist || !isOnboardingUrl) { + console.log(123); tabs .create({ url: 'onboarding.html', active: true }) .then(tab => { diff --git a/src/background/redux/get-main-store.ts b/src/background/redux/get-main-store.ts index 5cd492c45..0bed04d0f 100644 --- a/src/background/redux/get-main-store.ts +++ b/src/background/redux/get-main-store.ts @@ -55,7 +55,8 @@ export const selectPopupState = (state: RootState): PopupState => { accountInfo: state.accountInfo, contacts: state.contacts, rateApp: state.rateApp, - accountBalances: state.accountBalances + accountBalances: state.accountBalances, + ledger: state.ledger }; }; diff --git a/src/background/redux/ledger/actions.ts b/src/background/redux/ledger/actions.ts new file mode 100644 index 000000000..734c537e4 --- /dev/null +++ b/src/background/redux/ledger/actions.ts @@ -0,0 +1,12 @@ +import { createAction } from 'typesafe-actions'; + +export const ledgerNewWindowIdChanged = createAction( + 'LEDGER_NEW_WINDOW_ID_CHANGED' +)(); +export const ledgerDeployChanged = createAction( + 'LEDGER_DEPLOY_CHANGED' +)(); +export const ledgerRecipientToSaveOnSuccessChanged = createAction( + 'LEDGER_RECIPIENT_TO_SAVE_ON_SUCCESS_CHANGED' +)(); +export const ledgerStateCleared = createAction('LEDGER_STATE_CLEARED')(); diff --git a/src/background/redux/ledger/reducer.ts b/src/background/redux/ledger/reducer.ts new file mode 100644 index 000000000..a5ebb6c25 --- /dev/null +++ b/src/background/redux/ledger/reducer.ts @@ -0,0 +1,42 @@ +import { createReducer } from 'typesafe-actions'; + +import { + ledgerDeployChanged, + ledgerNewWindowIdChanged, + ledgerRecipientToSaveOnSuccessChanged, + ledgerStateCleared +} from './actions'; +import { LedgerState } from './types'; + +type State = LedgerState; + +const initialState: State = { + windowId: null, + deploy: null, + recipientToSaveOnSuccess: null +}; + +export const reducer = createReducer(initialState) + .handleAction( + ledgerNewWindowIdChanged, + (state, { payload }): State => ({ + ...state, + windowId: payload + }) + ) + .handleAction(ledgerStateCleared, (): State => initialState) + .handleAction(ledgerDeployChanged, (state, { payload }): State => { + return { + ...state, + deploy: payload + }; + }) + .handleAction( + ledgerRecipientToSaveOnSuccessChanged, + (state, { payload }): State => { + return { + ...state, + recipientToSaveOnSuccess: payload + }; + } + ); diff --git a/src/background/redux/ledger/selectors.ts b/src/background/redux/ledger/selectors.ts new file mode 100644 index 000000000..127871f14 --- /dev/null +++ b/src/background/redux/ledger/selectors.ts @@ -0,0 +1,11 @@ +import { RootState } from 'typesafe-actions'; + +export const selectLedgerNewWindowId = (state: RootState): number | null => + state.ledger.windowId; + +export const selectLedgerDeploy = (state: RootState): string | null => + state.ledger.deploy; + +export const selectLedgerRecipientToSaveOnSuccess = ( + state: RootState +): string | null => state.ledger.recipientToSaveOnSuccess; diff --git a/src/background/redux/ledger/types.ts b/src/background/redux/ledger/types.ts new file mode 100644 index 000000000..daa1e8755 --- /dev/null +++ b/src/background/redux/ledger/types.ts @@ -0,0 +1,5 @@ +export interface LedgerState { + windowId: number | null; + deploy: string | null; + recipientToSaveOnSuccess: string | null; +} diff --git a/src/background/redux/redux-action.ts b/src/background/redux/redux-action.ts index 719449aa5..c9a854f30 100644 --- a/src/background/redux/redux-action.ts +++ b/src/background/redux/redux-action.ts @@ -6,6 +6,7 @@ import * as activeOrigin from './active-origin/actions'; import * as contacts from './contacts/actions'; import * as keys from './keys/actions'; import * as lastActivityTime from './last-activity-time/actions'; +import * as ledger from './ledger/actions'; import * as loginRetryCount from './login-retry-count/actions'; import * as loginRetryLockoutTime from './login-retry-lockout-time/actions'; import * as rateApp from './rate-app/actions'; @@ -33,7 +34,8 @@ const reduxAction = { accountInfo, contacts, rateApp, - accountBalances + accountBalances, + ledger }; export type ReduxAction = ActionType; diff --git a/src/background/redux/root-reducer.ts b/src/background/redux/root-reducer.ts index 32b6cf81d..a0bfdce60 100644 --- a/src/background/redux/root-reducer.ts +++ b/src/background/redux/root-reducer.ts @@ -6,6 +6,7 @@ import { reducer as activeOrigin } from './active-origin/reducer'; import { reducer as contacts } from './contacts/reducer'; import { reducer as keys } from './keys/reducer'; import { reducer as lastActivityTime } from './last-activity-time/reducer'; +import { reducer as ledger } from './ledger/reducer'; import { reducer as loginRetryCount } from './login-retry-count/reducer'; import { reducer as loginRetryLockoutTime } from './login-retry-lockout-time/reducer'; import { reducer as rateApp } from './rate-app/reducer'; @@ -31,7 +32,8 @@ const rootReducer = combineReducers({ accountInfo, contacts, rateApp, - accountBalances + accountBalances, + ledger }); export default rootReducer; diff --git a/src/background/redux/root-selector.ts b/src/background/redux/root-selector.ts index 580f082f7..6ba5c8ada 100644 --- a/src/background/redux/root-selector.ts +++ b/src/background/redux/root-selector.ts @@ -10,3 +10,4 @@ export * from './windowManagement/selectors'; export * from './settings/selectors'; export * from './recent-recipient-public-keys/selectors'; export * from './rate-app/selectors'; +export * from './ledger/selectors'; diff --git a/src/background/redux/sagas/vault-sagas.ts b/src/background/redux/sagas/vault-sagas.ts index 55ec224f8..1df56ac45 100644 --- a/src/background/redux/sagas/vault-sagas.ts +++ b/src/background/redux/sagas/vault-sagas.ts @@ -52,6 +52,7 @@ import { accountImported, accountRemoved, accountRenamed, + accountsImported, activeAccountChanged, anotherAccountConnected, deployPayloadReceived, @@ -96,6 +97,7 @@ export function* vaultSagas() { [ getType(accountAdded), getType(accountImported), + getType(accountsImported), getType(accountRemoved), getType(accountRenamed), getType(siteConnected), diff --git a/src/background/redux/types.d.ts b/src/background/redux/types.d.ts index 37eed11df..70b4636d5 100644 --- a/src/background/redux/types.d.ts +++ b/src/background/redux/types.d.ts @@ -6,6 +6,7 @@ import { ActiveOriginState } from '@background/redux/active-origin/types'; import { ContactsState } from '@background/redux/contacts/types'; import { KeysState } from '@background/redux/keys/types'; import { LastActivityTimeState } from '@background/redux/last-activity-time/reducer'; +import { LedgerState } from '@background/redux/ledger/types'; import { LoginRetryCountState } from '@background/redux/login-retry-count/reducer'; import { LoginRetryLockoutTimeState } from '@background/redux/login-retry-lockout-time/types'; import { RateAppState } from '@background/redux/rate-app/types'; @@ -49,4 +50,5 @@ export type PopupState = { contacts: ContactsState; rateApp: RateAppState; accountBalances: AccountBalancesState; + ledger: LedgerState; }; diff --git a/src/background/redux/vault/actions.ts b/src/background/redux/vault/actions.ts index 52a5bbf7b..44c68bb16 100644 --- a/src/background/redux/vault/actions.ts +++ b/src/background/redux/vault/actions.ts @@ -14,10 +14,15 @@ export const secretPhraseCreated = createAction( )(); export const accountImported = createAction('ACCOUNT_IMPORTED')(); + export const accountAdded = createAction('ACCOUNT_ADDED')(); + +export const accountsImported = createAction('ACCOUNTS_IMPORTED')(); + export const accountRemoved = createAction('ACCOUNT_REMOVED')<{ accountName: string; }>(); + export const accountRenamed = createAction('ACCOUNT_RENAMED')<{ oldName: string; newName: string; diff --git a/src/background/redux/vault/reducer.ts b/src/background/redux/vault/reducer.ts index 2fb9f2770..5c1d41954 100644 --- a/src/background/redux/vault/reducer.ts +++ b/src/background/redux/vault/reducer.ts @@ -6,6 +6,7 @@ import { accountImported, accountRemoved, accountRenamed, + accountsImported, activeAccountChanged, anotherAccountConnected, deployPayloadReceived, @@ -80,14 +81,21 @@ export const reducer = createReducer(initialState) ( state, { payload: account }: ReturnType - ): State => { - return { - ...state, - accounts: [...state.accounts, account], - activeAccountName: - state.accounts.length === 0 ? account.name : state.activeAccountName - }; - } + ): State => ({ + ...state, + accounts: [...state.accounts, account], + activeAccountName: + state.accounts.length === 0 ? account.name : state.activeAccountName + }) + ) + .handleAction( + accountsImported, + (state, { payload: accounts }: ReturnType) => ({ + ...state, + accounts: [...state.accounts, ...accounts], + activeAccountName: + state.accounts.length === 0 ? accounts[0].name : state.activeAccountName + }) ) .handleAction( accountRemoved, diff --git a/src/background/redux/vault/selectors.ts b/src/background/redux/vault/selectors.ts index 514cf8841..1d1caea14 100644 --- a/src/background/redux/vault/selectors.ts +++ b/src/background/redux/vault/selectors.ts @@ -5,7 +5,11 @@ import { selectAccountBalances } from '@background/redux/account-balances/select import { VaultState } from '@background/redux/vault/types'; import { SecretPhrase } from '@libs/crypto'; -import { Account, AccountWithBalance } from '@libs/types/account'; +import { + Account, + AccountWithBalance, + HardwareWalletType +} from '@libs/types/account'; import { selectActiveOrigin } from '../active-origin/selectors'; @@ -85,9 +89,25 @@ export const selectVaultHiddenAccountsNames = createSelector( accounts => accounts.map(account => account.name) ); +export const selectVaultHasImportedAccount = createSelector( + selectVaultImportedAccounts, + importedAccounts => importedAccounts.length > 0 +); + export const selectVaultDerivedAccounts = createSelector( selectVaultAccountsWithBalances, - accounts => accounts.filter(account => !account.imported) + accounts => accounts.filter(account => !account.imported && !account.hardware) +); + +export const selectVaultLedgerAccounts = createSelector( + selectVaultAccountsWithBalances, + accounts => + accounts.filter(account => account.hardware === HardwareWalletType.Ledger) +); + +export const selectVaultLedgerAccountNames = createSelector( + selectVaultLedgerAccounts, + accounts => accounts.map(account => account.name) ); export const selectVaultAccountsSecretKeysBase64 = createSelector( @@ -113,6 +133,11 @@ export const selectVaultActiveAccount = createSelector( } ); +export const selectIsActiveAccountFromLedger = createSelector( + selectVaultActiveAccount, + account => Boolean(account && account.hardware === HardwareWalletType.Ledger) +); + export const selectAccountNamesByOriginDict = (state: RootState) => state.vault.accountNamesByOriginDict; diff --git a/src/constants.ts b/src/constants.ts index b4e36d2c2..8047aae37 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -43,6 +43,9 @@ export const getContractNftUrl = ( tokenId: string ) => `${casperLiveUrl}/contracts/${contractHash}/nfts/${tokenId}`; +export const ledgerSupportLink = + 'https://support.ledger.com/hc/en-us/articles/4416379141009-Casper-CSPR?docs=true'; + export enum CasperLiveUrl { MainnetUrl = 'https://cspr.live', TestnetUrl = 'https://testnet.cspr.live' @@ -163,6 +166,7 @@ export enum StakeSteps { NewValidator = 'new validator', Amount = 'amount', Confirm = 'confirm', + ConfirmWithLedger = 'confirm with ledger', Success = 'success' } diff --git a/src/fixtures/initial-state-for-popup-tests.ts b/src/fixtures/initial-state-for-popup-tests.ts index be820cd4d..8da58cac4 100644 --- a/src/fixtures/initial-state-for-popup-tests.ts +++ b/src/fixtures/initial-state-for-popup-tests.ts @@ -73,6 +73,11 @@ export const initialStateForPopupTests: RootState = { windowManagement: { windowId: null }, + ledger: { + windowId: null, + deploy: null, + recipientToSaveOnSuccess: null + }, vaultCipher: 'G89IRk1Zc+l46uPzkhTwSy09IUM5Q4R1JoIfOCeyMZEn47OnFK7Rk1fSPJ9gsSVsiq+d00AqKuW/lTV+s1OTGOucftVqKBF6XSyR9tG7P2sgRyJ6o5vS/h+tVSyqHt6wHFuTcee1IResAfxPJEjiKbMMm7gN1eFosvqM8utdBOgIkR17+HiojfvdI0Q07kWZXy0SuUceSxnXGHZU2LdMikZI2JmkaEgk+Qgm/nNzqlN2hAKxQRhr+68opUiIN/lpOYPLS64nZou6vuqSKu+Uogd8znNZOcFA+4+1zXlbJEp8HksSqy+fblAxDALpauljIogoPfwLIaSPU1GSwTfG63yuCiMVlAE+FwOAt31J+m0N++obOTomfp6ZjN0uOG700Kfm5NSWMMXqCp/f/M8C466/ONqsl0og/R1KXOw0nPYybzmgXCyS35yZyOXmxzKrKtXRdYVTBz79pjMbR8p1CCDnVLHJyKKIGbsGrX3ADjwkJHmBEjGPL2Qb4Ez7ATzcQ/XEdcK+VfzbNkJivssPMBV+6ETNWrwPbIR4BxfN12TbmdAej7nbP+oaM1plKhcoW1hp0oD60Ngwh8D1ztD9i+3R9yDGVNwjh56ytvk5E1Fo7e02NYBJgjvHFoBz+fX4iHlliHczRRVC3OVceZcPPMCeVuigkz7wirqscxBfnrc+EBXrziOrEc4NobSKJI33UEZAMLjxLZSD8CR9J9RrJzFCrda44P65uSypiSyw49EPdsG4etW9Eop2iHNO5Ny7oCr7mITsFvFkGtXDh+tQ4r6D4b7ZGe2AD2Jm/4t9jcBsPO3wHxPfS7eIHq8RUJZUK7DL90s8gt0wXzIFgIeMIc+mcK0HigU+zYaBHO9O+PUfetEHZANmSwsRu3nmiHogEZaPJAT+ATY3+3GjNMQ=', loginRetryLockoutTime: null, diff --git a/src/hooks/use-is-dark-mode.ts b/src/hooks/use-is-dark-mode.ts new file mode 100644 index 000000000..ed54d420e --- /dev/null +++ b/src/hooks/use-is-dark-mode.ts @@ -0,0 +1,16 @@ +import { useSelector } from 'react-redux'; + +import { selectThemeModeSetting } from '@background/redux/settings/selectors'; +import { ThemeMode } from '@background/redux/settings/types'; + +import { useSystemThemeDetector } from '@hooks/use-system-theme-detector'; + +export const useIsDarkMode = () => { + const themeMode = useSelector(selectThemeModeSetting); + + const isSystemDarkTheme = useSystemThemeDetector(); + + return themeMode === ThemeMode.SYSTEM + ? isSystemDarkTheme + : themeMode === ThemeMode.DARK; +}; diff --git a/src/hooks/use-ledger.ts b/src/hooks/use-ledger.ts new file mode 100644 index 000000000..7a86bed71 --- /dev/null +++ b/src/hooks/use-ledger.ts @@ -0,0 +1,233 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { windows } from 'webextension-polyfill'; + +import { RouterPath } from '@popup/router'; + +import { openNewSeparateWindow } from '@background/create-open-window'; +import { + ledgerNewWindowIdChanged, + ledgerStateCleared +} from '@background/redux/ledger/actions'; +import { selectLedgerNewWindowId } from '@background/redux/ledger/selectors'; +import { dispatchToMainStore } from '@background/redux/utils'; + +import { + ILedgerEvent, + IsBluetoothLedgerTransportAvailable, + LedgerEventStatus, + LedgerTransport, + SelectedTransport, + bluetoothTransportCreator, + getPreferredTransport, + isLedgerError, + isTransportAvailable, + ledger, + usbTransportCreator +} from '@libs/services/ledger'; + +interface IUseLedgerParams { + ledgerAction: () => Promise; + beforeLedgerActionCb: () => Promise; + initialEventToRender?: ILedgerEvent; + shouldLoadAccountList?: boolean; + withWaitingEventOnDisconnect?: boolean; + /** We have to open new browser window to handle device permission */ + askPermissionUrlData?: { + domain: string; + params?: Record; + hash: string; + }; +} + +export const useLedger = ({ + ledgerAction, + beforeLedgerActionCb, + initialEventToRender = { + status: LedgerEventStatus.WaitingResponseFromDevice + }, + withWaitingEventOnDisconnect = true, + shouldLoadAccountList = false, + askPermissionUrlData = { + domain: 'popup.html', + params: {}, + hash: RouterPath.SignWithLedgerInNewWindow + } +}: IUseLedgerParams) => { + const [isLedgerConnected, setIsLedgerConnected] = useState( + ledger.isConnected + ); + const [ledgerEventStatusToRender, setLedgerEventStatusToRender] = + useState(initialEventToRender); + const windowId = useSelector(selectLedgerNewWindowId); + const shouldTrySignAfterConnectRef = useRef(false); + const selectedTransportRef = useRef(undefined); + const isFirstEventRef = useRef(true); + const triggeredRef = useRef(false); + + const params = new URLSearchParams({ + ...(askPermissionUrlData.params ?? {}), + initialEventToRender: LedgerEventStatus.LedgerAskPermission, + ...(selectedTransportRef.current + ? { ledgerTransport: selectedTransportRef.current } + : {}) + }).toString(); + + const url = useMemo( + () => + `${askPermissionUrlData.domain}?${params}#${askPermissionUrlData.hash}`, + [askPermissionUrlData.domain, askPermissionUrlData.hash, params] + ); + + const makeSubmitLedgerAction = (transport?: LedgerTransport) => async () => { + if (!transport && !selectedTransportRef.current) { + selectedTransportRef.current = await getPreferredTransport(); + } + + if (transport) { + selectedTransportRef.current = transport; + } + + setLedgerEventStatusToRender({ + status: LedgerEventStatus.WaitingResponseFromDevice + }); + + await beforeLedgerActionCb(); + + if (isLedgerConnected) { + ledgerAction(); + + if (shouldLoadAccountList) { + setLedgerEventStatusToRender({ + status: LedgerEventStatus.LoadingAccountsList + }); + } + } else { + shouldTrySignAfterConnectRef.current = true; + + try { + if (selectedTransportRef.current === 'USB') { + await ledger.connect(usbTransportCreator, isTransportAvailable); + } else if (selectedTransportRef.current === 'Bluetooth') { + await ledger.connect( + bluetoothTransportCreator, + IsBluetoothLedgerTransportAvailable, + true + ); + } else { + setLedgerEventStatusToRender({ + status: LedgerEventStatus.Disconnected + }); + } + } catch (e) { + setIsLedgerConnected(false); + } + } + }; + + useEffect(() => { + const sub = ledger.subscribeToLedgerEventStatuss(event => { + if (event.status === LedgerEventStatus.Connected) { + setIsLedgerConnected(true); + } else if (event.status === LedgerEventStatus.Disconnected) { + setIsLedgerConnected(false); + + if (withWaitingEventOnDisconnect) { + setLedgerEventStatusToRender({ + status: LedgerEventStatus.WaitingResponseFromDevice + }); + } + } else if ( + event.status === LedgerEventStatus.SignatureRequestedToUser || + event.status === LedgerEventStatus.MsgSignatureRequestedToUser || + event.status === LedgerEventStatus.AccountListUpdated || + event.status === LedgerEventStatus.LoadingAccountsList || + event.status === LedgerEventStatus.WaitingResponseFromDevice || + isLedgerError(event) + ) { + setLedgerEventStatusToRender(event); + } + + if (isFirstEventRef.current && isLedgerError(event)) { + setLedgerEventStatusToRender({ + status: LedgerEventStatus.Disconnected + }); + setIsLedgerConnected(false); + } + + isFirstEventRef.current = false; + console.log('-------- event', JSON.stringify(event.status, null, ' ')); + }); + + return () => sub.unsubscribe(); + }, [withWaitingEventOnDisconnect]); + + useEffect(() => { + if (isLedgerConnected && shouldTrySignAfterConnectRef.current) { + makeSubmitLedgerAction(selectedTransportRef.current)(); + shouldTrySignAfterConnectRef.current = false; + } + }, [isLedgerConnected, makeSubmitLedgerAction]); + + /** We have to open new browser window to handle device permission */ + useEffect(() => { + (async () => { + if ( + ledgerEventStatusToRender.status === + LedgerEventStatus.LedgerPermissionRequired && + !windowId && + !triggeredRef.current + ) { + const w = await openNewSeparateWindow({ url }); + + if (w.id) { + dispatchToMainStore(ledgerNewWindowIdChanged(w.id)); + triggeredRef.current = true; + + const handleCloseWindow = () => { + dispatchToMainStore(ledgerStateCleared()); + windows.onRemoved.removeListener(handleCloseWindow); + }; + + windows.onRemoved.addListener(handleCloseWindow); + } + } + })(); + }, [ledgerEventStatusToRender.status, url, windowId]); + + const closeNewLedgerWindowsAndClearState = useCallback(async () => { + if (windowId) { + const all = await windows.getAll({ windowTypes: ['popup'] }); + all.forEach(w => w.id && windows.remove(w.id)); + dispatchToMainStore(ledgerStateCleared()); + await windows.remove(windowId); + } + }, [windowId]); + + useEffect(() => { + if (windowId && askPermissionUrlData?.domain !== 'popup.html') { + const sub = ledger.subscribeToLedgerEventStatuss(event => { + if ( + event.status === LedgerEventStatus.SignatureCompleted || + event.status === LedgerEventStatus.MsgSignatureCompleted + ) { + closeNewLedgerWindowsAndClearState(); + } + }); + + return () => sub.unsubscribe(); + } + }, [ + askPermissionUrlData?.domain, + closeNewLedgerWindowsAndClearState, + windowId + ]); + + return { + ledgerEventStatusToRender, + isLedgerConnected, + makeSubmitLedgerAction, + closeNewLedgerWindowsAndClearState, + windowId + }; +}; diff --git a/src/libs/animations/dots_dark_mode.json b/src/libs/animations/dots_dark_mode.json new file mode 100644 index 000000000..49cb26267 --- /dev/null +++ b/src/libs/animations/dots_dark_mode.json @@ -0,0 +1,284 @@ +{ + "nm": "Comp 1", + "ddd": 0, + "h": 1200, + "w": 1200, + "meta": { "g": "LottieFiles AE 0.1.20" }, + "layers": [ + { + "ty": 4, + "nm": "Shape Layer 2", + "sr": 1, + "st": 0, + "op": 600, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [-208, -6, 0], "ix": 1 }, + "s": { "a": 0, "k": [60.714, 60.714, 100], "ix": 6 }, + "sk": { "a": 0, "k": 0 }, + "p": { + "a": 1, + "k": [ + { + "o": { "x": 0.2, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [460, 600, 0], + "t": 0, + "ti": [0, 0, 0], + "to": [0, -16.667, 0] + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.4, "y": 1 }, + "s": [460, 500, 0], + "t": 15, + "ti": [0, -16.667, 0], + "to": [0, 0, 0] + }, + { "s": [460, 600, 0], "t": 40 } + ], + "ix": 2 + }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100, "ix": 11 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "s": { "a": 0, "k": [112, 112], "ix": 2 } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.5608, 0.651, 1], "ix": 4 }, + "r": 1, + "o": { "a": 0, "k": 100, "ix": 5 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [-208, -6], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 1 + }, + { + "ty": 4, + "nm": "Shape Layer 1", + "sr": 1, + "st": 0, + "op": 600, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [-208, -6, 0], "ix": 1 }, + "s": { "a": 0, "k": [60.714, 60.714, 100], "ix": 6 }, + "sk": { "a": 0, "k": 0 }, + "p": { + "a": 1, + "k": [ + { + "o": { "x": 0.2, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [600, 600, 0], + "t": 10, + "ti": [0, 0, 0], + "to": [0, -16.667, 0] + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.4, "y": 1 }, + "s": [600, 500, 0], + "t": 25, + "ti": [0, -16.667, 0], + "to": [0, 0, 0] + }, + { "s": [600, 600, 0], "t": 50 } + ], + "ix": 2 + }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100, "ix": 11 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "s": { "a": 0, "k": [112, 112], "ix": 2 } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.5608, 0.651, 1], "ix": 4 }, + "r": 1, + "o": { "a": 0, "k": 100, "ix": 5 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [-208, -6], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 2 + }, + { + "ty": 4, + "nm": "Shape Layer 3", + "sr": 1, + "st": 0, + "op": 600, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [-208, -6, 0], "ix": 1 }, + "s": { "a": 0, "k": [60.714, 60.714, 100], "ix": 6 }, + "sk": { "a": 0, "k": 0 }, + "p": { + "a": 1, + "k": [ + { + "o": { "x": 0.2, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [740, 600, 0], + "t": 20, + "ti": [0, 0, 0], + "to": [0, -16.667, 0] + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.4, "y": 1 }, + "s": [740, 500, 0], + "t": 35, + "ti": [0, -16.667, 0], + "to": [0, 0, 0] + }, + { "s": [740, 600, 0], "t": 60 } + ], + "ix": 2 + }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100, "ix": 11 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "s": { "a": 0, "k": [112, 112], "ix": 2 } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.5608, 0.651, 1], "ix": 4 }, + "r": 1, + "o": { "a": 0, "k": 100, "ix": 5 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [-208, -6], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 3 + } + ], + "v": "5.5.7", + "fr": 60, + "op": 60, + "ip": 0, + "assets": [] +} diff --git a/src/libs/animations/dots_light_mode.json b/src/libs/animations/dots_light_mode.json new file mode 100644 index 000000000..259375d71 --- /dev/null +++ b/src/libs/animations/dots_light_mode.json @@ -0,0 +1,284 @@ +{ + "nm": "Comp 1", + "ddd": 0, + "h": 1200, + "w": 1200, + "meta": { "g": "LottieFiles AE 0.1.20" }, + "layers": [ + { + "ty": 4, + "nm": "Shape Layer 2", + "sr": 1, + "st": 0, + "op": 600, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [-208, -6, 0], "ix": 1 }, + "s": { "a": 0, "k": [60.714, 60.714, 100], "ix": 6 }, + "sk": { "a": 0, "k": 0 }, + "p": { + "a": 1, + "k": [ + { + "o": { "x": 0.2, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [460, 600, 0], + "t": 0, + "ti": [0, 0, 0], + "to": [0, -16.667, 0] + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.4, "y": 1 }, + "s": [460, 500, 0], + "t": 15, + "ti": [0, -16.667, 0], + "to": [0, 0, 0] + }, + { "s": [460, 600, 0], "t": 40 } + ], + "ix": 2 + }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100, "ix": 11 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "s": { "a": 0, "k": [112, 112], "ix": 2 } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0, 0.1294, 0.6471], "ix": 4 }, + "r": 1, + "o": { "a": 0, "k": 100, "ix": 5 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [-208, -6], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 1 + }, + { + "ty": 4, + "nm": "Shape Layer 1", + "sr": 1, + "st": 0, + "op": 600, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [-208, -6, 0], "ix": 1 }, + "s": { "a": 0, "k": [60.714, 60.714, 100], "ix": 6 }, + "sk": { "a": 0, "k": 0 }, + "p": { + "a": 1, + "k": [ + { + "o": { "x": 0.2, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [600, 600, 0], + "t": 10, + "ti": [0, 0, 0], + "to": [0, -16.667, 0] + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.4, "y": 1 }, + "s": [600, 500, 0], + "t": 25, + "ti": [0, -16.667, 0], + "to": [0, 0, 0] + }, + { "s": [600, 600, 0], "t": 50 } + ], + "ix": 2 + }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100, "ix": 11 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "s": { "a": 0, "k": [112, 112], "ix": 2 } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0, 0.1294, 0.6471], "ix": 4 }, + "r": 1, + "o": { "a": 0, "k": 100, "ix": 5 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [-208, -6], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 2 + }, + { + "ty": 4, + "nm": "Shape Layer 3", + "sr": 1, + "st": 0, + "op": 600, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [-208, -6, 0], "ix": 1 }, + "s": { "a": 0, "k": [60.714, 60.714, 100], "ix": 6 }, + "sk": { "a": 0, "k": 0 }, + "p": { + "a": 1, + "k": [ + { + "o": { "x": 0.2, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [740, 600, 0], + "t": 20, + "ti": [0, 0, 0], + "to": [0, -16.667, 0] + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.4, "y": 1 }, + "s": [740, 500, 0], + "t": 35, + "ti": [0, -16.667, 0], + "to": [0, 0, 0] + }, + { "s": [740, 600, 0], "t": 60 } + ], + "ix": 2 + }, + "r": { "a": 0, "k": 0, "ix": 10 }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100, "ix": 11 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, 0], "ix": 3 }, + "s": { "a": 0, "k": [112, 112], "ix": 2 } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0, 0.1294, 0.6471], "ix": 4 }, + "r": 1, + "o": { "a": 0, "k": 100, "ix": 5 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [-208, -6], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 3 + } + ], + "v": "5.5.7", + "fr": 60, + "op": 60, + "ip": 0, + "assets": [] +} diff --git a/src/libs/animations/spinner_dark_mode.json b/src/libs/animations/spinner_dark_mode.json new file mode 100644 index 000000000..91f39b831 --- /dev/null +++ b/src/libs/animations/spinner_dark_mode.json @@ -0,0 +1,491 @@ +{ + "nm": "Comp 1", + "ddd": 0, + "h": 500, + "w": 500, + "meta": { "g": "@lottiefiles/toolkit-js 0.26.1" }, + "layers": [ + { + "ty": 4, + "nm": "Shape Layer 5", + "sr": 1, + "st": 20, + "op": 620, + "ip": 20, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [251, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 20 + }, + { "s": [360], "t": 110 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { "a": 0, "k": [10, 10] } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.302, 0.4392, 1, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 1 + }, + { + "ty": 4, + "nm": "Shape Layer 4", + "sr": 1, + "st": 15, + "op": 615, + "ip": 15, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [251, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 15 + }, + { "s": [360], "t": 105 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { "a": 0, "k": [20, 20] } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.302, 0.4392, 1, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 2 + }, + { + "ty": 4, + "nm": "Shape Layer 3", + "sr": 1, + "st": 10, + "op": 610, + "ip": 10, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [251, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 10 + }, + { "s": [360], "t": 100 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { "a": 0, "k": [30, 30] } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.302, 0.4392, 1, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 3 + }, + { + "ty": 4, + "nm": "Shape Layer 2", + "sr": 1, + "st": 5, + "op": 605, + "ip": 5, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [251, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 5 + }, + { "s": [360], "t": 95 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { "a": 0, "k": [40, 40] } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.302, 0.4392, 1, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 4 + }, + { + "ty": 4, + "nm": "Shape Layer 1", + "sr": 1, + "st": 0, + "op": 600, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [250, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 0 + }, + { "s": [360], "t": 90 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [50, 50], + "t": 0 + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [40, 40], + "t": 84 + }, + { "s": [50, 50], "t": 100 } + ] + } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.302, 0.4392, 1, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 5 + } + ], + "v": "4.6.8", + "fr": 60, + "op": 106, + "ip": 0, + "assets": [] +} diff --git a/src/libs/animations/spinner_light_mode.json b/src/libs/animations/spinner_light_mode.json new file mode 100644 index 000000000..fcb3127ac --- /dev/null +++ b/src/libs/animations/spinner_light_mode.json @@ -0,0 +1,491 @@ +{ + "nm": "Comp 1", + "ddd": 0, + "h": 500, + "w": 500, + "meta": { "g": "@lottiefiles/toolkit-js 0.26.1" }, + "layers": [ + { + "ty": 4, + "nm": "Shape Layer 5", + "sr": 1, + "st": 20, + "op": 620, + "ip": 20, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [251, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 20 + }, + { "s": [360], "t": 110 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { "a": 0, "k": [10, 10] } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.0392, 0.1804, 0.749, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 1 + }, + { + "ty": 4, + "nm": "Shape Layer 4", + "sr": 1, + "st": 15, + "op": 615, + "ip": 15, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [251, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 15 + }, + { "s": [360], "t": 105 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { "a": 0, "k": [20, 20] } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.0392, 0.1804, 0.749, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 2 + }, + { + "ty": 4, + "nm": "Shape Layer 3", + "sr": 1, + "st": 10, + "op": 610, + "ip": 10, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [251, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 10 + }, + { "s": [360], "t": 100 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { "a": 0, "k": [30, 30] } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.0392, 0.1804, 0.749, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 3 + }, + { + "ty": 4, + "nm": "Shape Layer 2", + "sr": 1, + "st": 5, + "op": 605, + "ip": 5, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [251, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 5 + }, + { "s": [360], "t": 95 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { "a": 0, "k": [40, 40] } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.0392, 0.1804, 0.749, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 4 + }, + { + "ty": 4, + "nm": "Shape Layer 1", + "sr": 1, + "st": 0, + "op": 600, + "ip": 0, + "hd": false, + "ddd": 0, + "bm": 0, + "hasMask": false, + "ao": 0, + "ks": { + "a": { "a": 0, "k": [0, 0, 0] }, + "s": { "a": 0, "k": [100, 100, 100] }, + "sk": { "a": 0, "k": 0 }, + "p": { "a": 0, "k": [250, 250, 0] }, + "r": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [0], + "t": 0 + }, + { "s": [360], "t": 90 } + ] + }, + "sa": { "a": 0, "k": 0 }, + "o": { "a": 0, "k": 100 } + }, + "ef": [], + "shapes": [ + { + "ty": "gr", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Group", + "nm": "Ellipse 1", + "ix": 1, + "cix": 2, + "np": 3, + "it": [ + { + "ty": "el", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Shape - Ellipse", + "nm": "Ellipse Path 1", + "d": 1, + "p": { "a": 0, "k": [0, -100] }, + "s": { + "a": 1, + "k": [ + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [50, 50], + "t": 0 + }, + { + "o": { "x": 0.333, "y": 0 }, + "i": { "x": 0.667, "y": 1 }, + "s": [40, 40], + "t": 84 + }, + { "s": [50, 50], "t": 100 } + ] + } + }, + { + "ty": "st", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Stroke", + "nm": "Stroke 1", + "lc": 1, + "lj": 1, + "ml": 4, + "o": { "a": 0, "k": 100 }, + "w": { "a": 0, "k": 0 }, + "c": { "a": 0, "k": [0, 0, 0, 1] } + }, + { + "ty": "fl", + "bm": 0, + "hd": false, + "mn": "ADBE Vector Graphic - Fill", + "nm": "Fill 1", + "c": { "a": 0, "k": [0.0392, 0.1804, 0.749, 1] }, + "r": 1, + "o": { "a": 0, "k": 100 } + }, + { + "ty": "tr", + "a": { "a": 0, "k": [0, 0], "ix": 1 }, + "s": { "a": 0, "k": [100, 100], "ix": 3 }, + "sk": { "a": 0, "k": 0, "ix": 4 }, + "p": { "a": 0, "k": [0, 0], "ix": 2 }, + "r": { "a": 0, "k": 0, "ix": 6 }, + "sa": { "a": 0, "k": 0, "ix": 5 }, + "o": { "a": 0, "k": 100, "ix": 7 } + } + ] + } + ], + "ind": 5 + } + ], + "v": "4.6.8", + "fr": 60, + "op": 106, + "ip": 0, + "assets": [] +} diff --git a/src/libs/layout/header/header-submenu-bar-nav-link.tsx b/src/libs/layout/header/header-submenu-bar-nav-link.tsx index b54c5d614..8108ffeea 100644 --- a/src/libs/layout/header/header-submenu-bar-nav-link.tsx +++ b/src/libs/layout/header/header-submenu-bar-nav-link.tsx @@ -61,7 +61,9 @@ export function HeaderSubmenuBarNavLink({ return ( navigate(RouterPath.Home)} + onClick={() => + onClick != null ? onClick() : navigate(RouterPath.Home) + } /> ); diff --git a/src/libs/layout/layout-window.tsx b/src/libs/layout/layout-window.tsx index 8421705fd..c375ab1c1 100644 --- a/src/libs/layout/layout-window.tsx +++ b/src/libs/layout/layout-window.tsx @@ -32,6 +32,7 @@ const PageFooter = styled.footer``; const Container = styled(FlexColumn)` height: 100%; + width: 100%; `; export function LayoutWindow({ diff --git a/src/libs/layout/unlock-vault/index.tsx b/src/libs/layout/unlock-vault/index.tsx index 4d9eabde1..d9fc924e4 100644 --- a/src/libs/layout/unlock-vault/index.tsx +++ b/src/libs/layout/unlock-vault/index.tsx @@ -233,7 +233,7 @@ export function UnlockVaultPageContent() { {isLoading ? ( { } }; -export const makeAuctionManagerDeployAndSing = async ( +export const makeAuctionManagerDeploy = async ( contractEntryPoint: AuctionManagerEntryPoint, delegatorPublicKeyHex: string, validatorPublicKeyHex: string, @@ -70,8 +75,7 @@ export const makeAuctionManagerDeployAndSing = async ( amountMotes: string, networkName: NetworkName, auctionManagerContractHash: string, - nodeUrl: CasperNodeUrl, - keys: Keys.AsymmetricKey[] + nodeUrl: CasperNodeUrl ) => { const hash = decodeBase16(auctionManagerContractHash); @@ -111,21 +115,18 @@ export const makeAuctionManagerDeployAndSing = async ( const payment = DeployUtil.standardPayment(deployCost); - const deploy = DeployUtil.makeDeploy(deployParams, session, payment); - - return deploy.sign(keys); + return DeployUtil.makeDeploy(deployParams, session, payment); }; -export const makeNativeTransferDeployAndSign = async ( - senderPublicKeyHex: string, +export const makeNativeTransferDeploy = async ( + activeAccount: AccountWithBalance, recipientPublicKeyHex: string, amountMotes: string, networkName: NetworkName, nodeUrl: CasperNodeUrl, - keys: Keys.AsymmetricKey[], transferIdMemo?: string ) => { - const senderPublicKey = CLPublicKey.fromHex(senderPublicKeyHex); + const senderPublicKey = CLPublicKey.fromHex(activeAccount.publicKey); const recipientPublicKey = CLPublicKey.fromHex(recipientPublicKeyHex); const date = await getDateForDeploy(nodeUrl); @@ -149,12 +150,10 @@ export const makeNativeTransferDeployAndSign = async ( const payment = DeployUtil.standardPayment(TRANSFER_COST_MOTES); - const deploy = DeployUtil.makeDeploy(deployParams, session, payment); - - return deploy.sign(keys); + return DeployUtil.makeDeploy(deployParams, session, payment); }; -export const makeCep18TransferDeployAndSign = async ( +export const makeCep18TransferDeploy = async ( nodeUrl: CasperNodeUrl, networkName: NetworkName, tokenContractHash: string | undefined, @@ -163,8 +162,7 @@ export const makeCep18TransferDeployAndSign = async ( amount: string, erc20Decimals: number | null, paymentAmount: string, - activeAccount: Account, - keys: Keys.AsymmetricKey[] + activeAccount: Account ) => { const cep18 = new CEP18Client(nodeUrl, networkName); @@ -195,23 +193,20 @@ export const makeCep18TransferDeployAndSign = async ( date // https://github.com/casper-network/casper-node/issues/4152 ); - const deploy = DeployUtil.makeDeploy( + return DeployUtil.makeDeploy( deployParams, tempDeploy.session, tempDeploy.payment ); - - return deploy.sign(keys); }; -export const makeNFTDeployAndSign = async ( +export const makeNFTDeploy = async ( runtimeArgs: RuntimeArgs, paymentAmount: string, deploySender: CLPublicKey, networkName: NetworkName, contractPackageHash: string, - nodeUrl: CasperNodeUrl, - keys: Keys.AsymmetricKey[] + nodeUrl: CasperNodeUrl ) => { const hash = Uint8Array.from(Buffer.from(contractPackageHash, 'hex')); @@ -233,7 +228,35 @@ export const makeNFTDeployAndSign = async ( runtimeArgs ); const payment = DeployUtil.standardPayment(paymentAmount); - const deploy = DeployUtil.makeDeploy(deployParams, session, payment); + + return DeployUtil.makeDeploy(deployParams, session, payment); +}; + +export const signLedgerDeploy = async ( + deploy: DeployUtil.Deploy, + activeAccount: Account +) => { + const resp = await ledger.singDeploy(deploy, { + index: activeAccount.derivationIndex, + publicKey: activeAccount.publicKey + }); + + const approval = new DeployUtil.Approval(); + approval.signer = activeAccount.publicKey; + approval.signature = resp.signatureHex; + deploy.approvals.push(approval); + + return deploy; +}; + +export const signDeploy = ( + deploy: DeployUtil.Deploy, + keys: Keys.AsymmetricKey[], + activeAccount: Account +) => { + if (activeAccount?.hardware === HardwareWalletType.Ledger) { + return signLedgerDeploy(deploy, activeAccount); + } return deploy.sign(keys); }; diff --git a/src/libs/services/ledger/errors.ts b/src/libs/services/ledger/errors.ts new file mode 100644 index 000000000..1d6dc8ede --- /dev/null +++ b/src/libs/services/ledger/errors.ts @@ -0,0 +1,78 @@ +import { ILedgerEvent, LedgerEventStatus } from './types'; + +interface ILedgerErrorData { + title: string | null; + description: string | null; +} + +export const ledgerErrorsData: Record = { + [LedgerEventStatus.Timeout]: { + title: 'Connection timeout', + description: 'The connection time is up, try again' + }, + [LedgerEventStatus.InvalidIndex]: { + title: 'Invalid account index', + description: + 'Try to remove the current account from the extension and add it again' + }, + [LedgerEventStatus.ErrorOpeningDevice]: { + title: 'Unable to connect to the Ledger device', + description: 'Check the Ledger device connection and try again' + }, + [LedgerEventStatus.LedgerPermissionRequired]: { + title: 'Please provide permission to connect your Ledger device', + description: + 'This permission is needed for each new device when connecting to the browser.' + }, + [LedgerEventStatus.MsgSignatureFailed]: { + title: 'Error when signing a message', + description: null + }, + [LedgerEventStatus.MsgSignatureCanceled]: { + title: 'You rejected to sign the message', + description: null + }, + [LedgerEventStatus.SignatureFailed]: { + title: 'Error when signing a deploy', + description: null + }, + [LedgerEventStatus.SignatureCanceled]: { + title: 'You rejected to sign the deploy', + description: null + }, + [LedgerEventStatus.AccountListFailed]: { + title: 'Synchronization of accounts from the Ledger device failed', + description: null + }, + [LedgerEventStatus.CasperAppNotLoaded]: { + title: 'Casper app isn’t open on Ledger', + description: + 'Please make sure to open Casper app on your Ledger and try connecting again.' + }, + [LedgerEventStatus.DeviceLocked]: { + title: 'The Ledger device is locked', + description: 'Unlock the Ledger device connection and try again' + }, + [LedgerEventStatus.NotAvailable]: { + title: "Your browser doesn't support connection to Ledger", + description: + 'Consider switching to latest versions of Chrome or Edge browsers' + }, + [LedgerEventStatus.WaitingToSignPrevDeploy]: { + title: 'Your have pending signing action on your Ledger device', + description: 'Handle the previous signing action and then make a new one' + }, + 'ledger-ask-permission': { title: null, description: null }, + 'ledger-account-list-updated': { title: null, description: null }, + 'ledger-connected': { title: null, description: null }, + 'ledger-disconnected': { title: null, description: null }, + 'ledger-loading-accounts-list': { title: null, description: null }, + 'ledger-msg-signature-completed': { title: null, description: null }, + 'ledger-msg-signature-requested-to-user': { title: null, description: null }, + 'ledger-signature-completed': { title: null, description: null }, + 'ledger-signature-requested-to-user': { title: null, description: null }, + 'ledger-waiting-response-from-device': { title: null, description: null } +}; + +export const isLedgerError = (event: ILedgerEvent) => + Boolean(ledgerErrorsData[event.status].title); diff --git a/src/libs/services/ledger/index.ts b/src/libs/services/ledger/index.ts new file mode 100644 index 000000000..49e97d32f --- /dev/null +++ b/src/libs/services/ledger/index.ts @@ -0,0 +1,4 @@ +export * from './ledger'; +export * from './transport'; +export * from './types'; +export * from './errors'; diff --git a/src/libs/services/ledger/ledger.ts b/src/libs/services/ledger/ledger.ts new file mode 100644 index 000000000..da3c9e5c5 --- /dev/null +++ b/src/libs/services/ledger/ledger.ts @@ -0,0 +1,562 @@ +import Transport from '@ledgerhq/hw-transport'; +import { blake2b } from '@noble/hashes/blake2b'; +import LedgerCasperApp from '@zondax/ledger-casper'; +import { ResponseSign } from '@zondax/ledger-casper/src/types'; +import { Buffer } from 'buffer'; +import { DeployUtil } from 'casper-js-sdk'; +import { + BehaviorSubject, + Observable, + Observer, + debounceTime, + distinct +} from 'rxjs'; + +import { getBip44Path } from '@libs/crypto'; + +import { + ILedgerEvent, + LedgerAccount, + LedgerAccountsOptions, + LedgerEventStatus, + SignResult +} from './types'; + +const CONNECTION_TIMEOUT_MS = 60000; +const CONNECTION_POLL_INTERVAL = 3000; + +export class LedgerError extends Error { + constructor(LedgerEventStatus: ILedgerEvent) { + super(JSON.stringify(LedgerEventStatus)); + } +} + +export class Ledger { + cachedAccounts: LedgerAccount[] = []; + + #transport: Transport | null = null; + #isBluetoothTransport: boolean = false; + #ledgerApp: LedgerCasperApp | null = null; + #ledgerConnected = false; + #allowReconnect: boolean = true; + #LedgerEventStatussSubject = new BehaviorSubject({ + status: LedgerEventStatus.Disconnected + }); + + subscribeToLedgerEventStatuss = (onData: (evt: ILedgerEvent) => void) => + this.#LedgerEventStatussSubject.pipe(debounceTime(300)).subscribe(onData); + + /** @throws {LedgerError} */ + async connect( + transportCreator: () => Promise, + checkTransportAvailability: () => Promise, + isBluetoothTransport = false + ): Promise { + this.#isBluetoothTransport = isBluetoothTransport; + + return new Promise(async (resolve, reject) => { + const available = await checkTransportAvailability(); + + if (!available) { + const evt = { status: LedgerEventStatus.NotAvailable }; + this.#LedgerEventStatussSubject.next(evt); + reject(new LedgerError(evt)); + } + + const connectionObserver: Observer = { + next: data => { + this.#LedgerEventStatussSubject.next(data); + + if ( + data.status === LedgerEventStatus.Timeout || + data.status === LedgerEventStatus.ErrorOpeningDevice + ) { + reject(new LedgerError(data)); + } + }, + error: () => { + const evt: ILedgerEvent = { + status: LedgerEventStatus.ErrorOpeningDevice + }; + this.#LedgerEventStatussSubject.next(evt); + reject(new LedgerError(evt)); + }, + complete: async () => { + resolve(); + } + }; + + try { + this.#transport = await transportCreator(); + this.#transport?.on('disconnect', this.#onDisconnect); + this.#ledgerApp = new LedgerCasperApp(this.#transport); + } catch (e) { + if (!this.#transport) { + const evt = { status: LedgerEventStatus.LedgerPermissionRequired }; + this.#LedgerEventStatussSubject.next(evt); + reject(new LedgerError(evt)); + return; + } + } + + this.#connectToLedger(transportCreator, connectionObserver); + }); + } + + async disconnect(): Promise { + if (this.#ledgerConnected) { + try { + await this.#transport?.close(); + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.Disconnected + }); + } catch (err: any) { + console?.error( + 'Error disconnecting from ledger: ' + err.name + ' - ' + err.message + ); + } + + this.#ledgerConnected = false; + } + + this.cachedAccounts = []; + + return true; + } + + get isConnected(): boolean { + return this.#ledgerConnected; + } + + async checkAppInfo(): Promise { + if (this.#ledgerConnected && this.#ledgerApp) { + const appInfo = await this.#ledgerApp?.getAppInfo(); + + await this.#processDelayAfterAction(); + + if (appInfo.returnCode === 65535) { + return LedgerEventStatus.WaitingToSignPrevDeploy; + } + + return appInfo.returnCode === 0x9000 && appInfo.appName === 'Casper' + ? null + : LedgerEventStatus.WaitingResponseFromDevice; + } + + return LedgerEventStatus.WaitingResponseFromDevice; + } + + /** @throws {LedgerError} message - ILedgerEvent JSON */ + getAccountList = async ({ + size, + offset + }: LedgerAccountsOptions): Promise => { + try { + if (!this.#ledgerApp || !this.#ledgerConnected) return; + + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.LoadingAccountsList + }); + + const response = await this.#ledgerApp.getAddressAndPubKey( + this.#getAccountPath(offset) + ); + await this.#processDelayAfterAction(); + + if (!response || response.returnCode !== 0x9000) { + if (response?.returnCode === 0xffff || response.returnCode === 21781) { + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.DeviceLocked + }); + } else if (response?.returnCode === 0x6e01) { + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.CasperAppNotLoaded + }); + } else { + this.#processError({ status: LedgerEventStatus.AccountListFailed }); + } + } + + const publicKeys: string[] = [this.#encodePublicKey(response.publicKey)]; + + for (let i = 1; i < size; i++) { + const key = await this.#ledgerApp.getAddressAndPubKey( + this.#getAccountPath(offset + i) + ); + await this.#processDelayAfterAction(); + + publicKeys.push(this.#encodePublicKey(key.publicKey)); + } + + const updatedAccountList = publicKeys.map((pk, i) => ({ + publicKey: pk, + index: offset + i + })); + + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.AccountListUpdated, + firstAcctIndex: offset, + accounts: updatedAccountList + }); + + if (offset === this.cachedAccounts.length) { + this.cachedAccounts.push(...updatedAccountList); + } + } catch (e) { + if (e instanceof LedgerError) { + throw e; + } else { + this.#processError({ status: LedgerEventStatus.AccountListFailed }); + } + } + }; + + /** @throws {LedgerError} message - ILedgerEvent JSON */ + async singDeploy( + deploy: DeployUtil.Deploy, + account: Partial + ): Promise { + try { + if (account.index === undefined) { + this.#processError({ status: LedgerEventStatus.InvalidIndex }); + } + + await this.#checkConnection(account.index); + + const deployHash = Buffer.from(deploy.hash).toString('hex'); + + const devicePk = + account.index !== undefined + ? await this.#ledgerApp?.getAddressAndPubKey( + this.#getAccountPath(account.index) + ) + : undefined; + await this.#processDelayAfterAction(); + + if (!devicePk) { + this.#processError({ + status: LedgerEventStatus.SignatureFailed, + error: 'Could not retrieve key by index from device', + publicKey: account.publicKey, + deployHash + }); + } + + const keyFromDevice: string = this.#encodePublicKey(devicePk.publicKey); + + if (account.publicKey !== keyFromDevice) { + this.#processError({ + status: LedgerEventStatus.SignatureFailed, + error: + 'Signing key not found on Ledger device. Signature process failed', + publicKey: account.publicKey, + deployHash + }); + } + + const deployBytes = DeployUtil.deployToBytes(deploy); + + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.SignatureRequestedToUser, + publicKey: account.publicKey, + deployHash: deployHash + }); + + let result: ResponseSign; + + if (deploy.session.isModuleBytes()) { + result = await this.#ledgerApp?.signWasmDeploy( + this.#getAccountPath(account.index), + Buffer.from(deployBytes) + ); + } else { + result = await this.#ledgerApp?.sign( + this.#getAccountPath(account.index), + Buffer.from(deployBytes) + ); + } + + await this.#processDelayAfterAction(); + + if (result.returnCode === 0x6986) { + //transaction rejected + this.#processError({ + status: LedgerEventStatus.SignatureCanceled, + publicKey: account.publicKey, + deployHash + }); + } + + if (result.returnCode !== 0x9000) { + this.#processError({ + status: LedgerEventStatus.SignatureFailed, + error: result.errorMessage, + publicKey: account.publicKey, + deployHash + }); + } + + // remove V byte if included + const patchedSignature = + result.signatureRSV.length > 64 + ? result.signatureRSV.subarray(0, 64) + : result.signatureRSV; + + const signatureHex = `02${patchedSignature.toString('hex')}`; + + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.SignatureCompleted, + publicKey: account.publicKey, + deployHash, + signatureHex + }); + + const prefix = new Uint8Array([0x02]); + + if (!signatureHex) { + this.#processError({ + status: LedgerEventStatus.SignatureFailed, + publicKey: account.publicKey, + deployHash, + error: `Empty signature` + }); + } + + return { + signatureHex, + signature: new Uint8Array([...prefix, ...patchedSignature]) + }; + } catch (e) { + if (e instanceof LedgerError) { + throw e; + } else { + this.#processError({ + status: LedgerEventStatus.SignatureFailed, + error: 'Unknown signature error' + }); + } + } + } + + /** @throws {LedgerError} message - ILedgerEvent JSON */ + async signMessage( + message: string, + account: Partial + ): Promise { + try { + if (account.index === undefined) { + this.#processError({ status: LedgerEventStatus.InvalidIndex }); + } + + await this.#checkConnection(account.index); + + const prefixedMessage = Buffer.from( + `Casper Message:\n${message}`, + 'utf-8' + ); + const hashedMessage = Buffer.from( + blake2b(prefixedMessage, { dkLen: 32 }) + ).toString('hex'); + + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.MsgSignatureRequestedToUser, + publicKey: account.publicKey, + message, + msgHash: hashedMessage + }); + + this.#transport?.setExchangeTimeout(10000); + + const result: ResponseSign = await this.#ledgerApp?.signMessage( + this.#getAccountPath(account.index), + prefixedMessage + ); + + await this.#processDelayAfterAction(); + + if (result.returnCode === 0x6986) { + //transaction rejected + this.#processError({ status: LedgerEventStatus.MsgSignatureCanceled }); + } + + if (result.returnCode !== 0x9000) { + this.#processError({ + status: LedgerEventStatus.MsgSignatureFailed, + error: `Error: ${result.errorMessage}` + }); + } + + // remove V byte if included + const patchedSignature = + result.signatureRSV.length > 64 + ? result.signatureRSV.subarray(0, 64) + : result.signatureRSV; + + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.MsgSignatureCompleted, + publicKey: account.publicKey, + message: message, + msgHash: hashedMessage, + signatureHex: patchedSignature.toString('hex') + }); + + return { + signatureHex: patchedSignature.toString('hex'), + signature: new Uint8Array(patchedSignature) + }; + } catch (e) { + if (e instanceof LedgerError) { + throw e; + } else { + this.#processError({ + status: LedgerEventStatus.MsgSignatureFailed, + error: 'Unknown msg signature error' + }); + } + } + } + + #checkConnection = async (accountIndex?: number) => { + let evt; + + if (Number.isNaN(Number(accountIndex))) { + evt = { status: LedgerEventStatus.InvalidIndex }; + } else if (!this.#ledgerConnected) { + evt = { status: LedgerEventStatus.CasperAppNotLoaded }; + } else { + const status = await this.checkAppInfo(); + + if (status) { + evt = { status }; + } + } + + if (evt) { + this.#processError(evt); + } + }; + + #onDisconnect = (evt: any) => { + console.log('device disconnected.', evt); + this.#ledgerConnected = false; + this.#allowReconnect = false; + this.cachedAccounts = []; + this.#LedgerEventStatussSubject.next({ + status: LedgerEventStatus.Disconnected + }); + this.#transport?.off('disconnect', this.#onDisconnect); + this.#transport = null; + + setTimeout(() => { + this.#allowReconnect = true; + }, CONNECTION_POLL_INTERVAL * 1.2); + }; + + #getAccountPath = (acctIdx: number): string => getBip44Path(acctIdx); + + #encodePublicKey = (bytes: Uint8Array) => + '02' + Buffer.from(bytes).toString('hex'); + + #connectToLedger( + transportCreator: () => Promise, + observer: Observer + ): void { + const observable = new Observable(subscriber => { + /** @return {boolean} is should stop retries */ + const retryConnection = async (): Promise => { + if (!this.#transport) { + try { + this.#transport = await transportCreator(); + this.#transport.on('disconnect', this.#onDisconnect); + this.#ledgerApp = new LedgerCasperApp(this.#transport); + } catch (error: any) { + console.log('Error connecting to a Ledger device', error); + subscriber.next({ status: LedgerEventStatus.ErrorOpeningDevice }); + + return true; + } + } + + if (!this.#transport || !this.#ledgerApp) { + console.debug('Cannot connect to device. Transport error'); + return false; + } + + subscriber.next({ + status: LedgerEventStatus.WaitingResponseFromDevice + }); + + try { + const appInfo = await this.#ledgerApp.getAppInfo(); + await this.#processDelayAfterAction(); + + if (appInfo.returnCode === 0xffff || appInfo.returnCode === 21781) { + subscriber.next({ status: LedgerEventStatus.DeviceLocked }); + + return false; + } + + if (appInfo.returnCode !== 0x9000) { + // subscriber.next({ status: LedgerEventStatus.DeviceLocked }); + return false; + } + + if (appInfo.appName !== 'Casper') { + subscriber.next({ status: LedgerEventStatus.CasperAppNotLoaded }); + + return false; + } + + this.#ledgerConnected = true; + subscriber.next({ status: LedgerEventStatus.Connected }); + + return true; + } catch (err) { + console.error('-------- err', err); + } + + return false; + }; + + retryConnection().then(async shouldStopRetries => { + if (shouldStopRetries) { + subscriber.complete(); + } else { + let timeoutLoops = CONNECTION_TIMEOUT_MS / CONNECTION_POLL_INTERVAL; + + const timer = setInterval(async () => { + if (--timeoutLoops <= 0) { + clearInterval(timer); + subscriber.next({ status: LedgerEventStatus.Timeout }); + } else if (!this.#allowReconnect) { + console.debug('waiting before for a reconnection attempt'); + return; + } else if (await retryConnection()) { + clearInterval(timer); + subscriber.complete(); + } + }, CONNECTION_POLL_INTERVAL); + } + }); + }); + + observable.pipe(distinct(({ status }) => status)).subscribe(observer); + } + + /** @throws {LedgerError} message - ILedgerEvent JSON */ + #processError(evt: ILedgerEvent): never { + this.#LedgerEventStatussSubject.next(evt); + throw new LedgerError(evt); + } + + /** Even though the Promise is resolved, bluetooth has not had time to process the messages + * We need to wait a bit due to errors + * */ + async #processDelayAfterAction() { + if (this.#isBluetoothTransport) { + await new Promise(resolve => setTimeout(resolve, 200)); + } + } +} + +export const ledger = new Ledger(); diff --git a/src/libs/services/ledger/transport.ts b/src/libs/services/ledger/transport.ts new file mode 100644 index 000000000..e46d81831 --- /dev/null +++ b/src/libs/services/ledger/transport.ts @@ -0,0 +1,96 @@ +import { ledgerUSBVendorId } from '@ledgerhq/devices'; +import Transport from '@ledgerhq/hw-transport'; +import BluetoothTransport from '@ledgerhq/hw-transport-web-ble'; +import TransportWebHID from '@ledgerhq/hw-transport-webhid'; +import TransportWebUsb from '@ledgerhq/hw-transport-webusb'; +import { getLedgerDevices } from '@ledgerhq/hw-transport-webusb/lib/webusb'; + +import { LedgerError } from '@libs/services/ledger/ledger'; + +import { LedgerEventStatus, SelectedTransport } from './types'; + +export const IsUsbLedgerTransportAvailable = async (): Promise => { + const hidAvailable = await TransportWebHID.isSupported(); + + if (hidAvailable) { + return true; + } + + return await TransportWebUsb.isSupported(); +}; + +export const IsBluetoothLedgerTransportAvailable = async (): Promise => + BluetoothTransport.isSupported(); + +export const subscribeToBluetoothAvailability = + BluetoothTransport.observeAvailability; + +export const isTransportAvailable = async () => { + try { + return ( + await Promise.all([ + IsUsbLedgerTransportAvailable(), + IsBluetoothLedgerTransportAvailable() + ]) + ).some(Boolean); + } catch { + return false; + } +}; + +export const usbTransportCreator = async (): Promise => { + if (await TransportWebHID.isSupported()) { + const connected = await TransportWebHID.openConnected(); + + return connected || (await TransportWebHID.request()); + } else if (await TransportWebUsb.isSupported()) { + const connected = await TransportWebUsb.openConnected(); + + if (!connected) { + throw new LedgerError({ + status: LedgerEventStatus.LedgerPermissionRequired + }); + } + + return connected || (await TransportWebUsb.request()); + } else { + throw new Error('Usb connection not supported'); + } +}; + +export const bluetoothTransportCreator = async () => + BluetoothTransport.create(); + +export const getPreferredTransport = async (): Promise => { + if (await TransportWebHID.isSupported()) { + // Copy from TransportWebHID.getLedgerDevices source code + const getHID = (): null | Record<'getDevices', () => Promise> => { + // @ts-ignore + const { hid } = navigator; + + if (!hid) return null; + + return hid; + }; + + async function getHiDLedgerDevices(): Promise { + const devices = (await getHID()?.getDevices()) ?? []; + + return devices.filter((d: any) => d.vendorId === ledgerUSBVendorId); + } + + const devices = await getHiDLedgerDevices(); + + if (devices.length) { + return 'USB'; + } + } else if (await TransportWebUsb.isSupported()) { + const devices = await getLedgerDevices(); + + if (devices.length) { + return 'USB'; + } + } + + return undefined; +}; diff --git a/src/libs/services/ledger/types.ts b/src/libs/services/ledger/types.ts new file mode 100644 index 000000000..83ea3a913 --- /dev/null +++ b/src/libs/services/ledger/types.ts @@ -0,0 +1,55 @@ +export enum LedgerEventStatus { + Disconnected = 'ledger-disconnected', + NotAvailable = 'ledger-not-available', + DeviceLocked = 'ledger-device-locked', + WaitingResponseFromDevice = 'ledger-waiting-response-from-device', + WaitingToSignPrevDeploy = 'waiting-to-sign-prev-deploy', + CasperAppNotLoaded = 'ledger-casper-app-not-loaded', + Connected = 'ledger-connected', + LoadingAccountsList = 'ledger-loading-accounts-list', + AccountListUpdated = 'ledger-account-list-updated', + AccountListFailed = 'ledger-account-list-failed', + SignatureRequestedToUser = 'ledger-signature-requested-to-user', + SignatureCompleted = 'ledger-signature-completed', + SignatureCanceled = 'ledger-signature-cancelled', + SignatureFailed = 'ledger-signature-failed', + MsgSignatureRequestedToUser = 'ledger-msg-signature-requested-to-user', + MsgSignatureCompleted = 'ledger-msg-signature-completed', + MsgSignatureCanceled = 'ledger-msg-signature-cancelled', + MsgSignatureFailed = 'ledger-msg-signature-failed', + LedgerPermissionRequired = 'ledger-permission-required', + LedgerAskPermission = 'ledger-ask-permission', + ErrorOpeningDevice = 'ledger-error-opening-device', + Timeout = 'ledger-timeout', + InvalidIndex = 'ledger-invalid-index' +} + +export interface LedgerAccount { + publicKey: string; + index: number; +} + +export interface ILedgerEvent { + status: LedgerEventStatus; + publicKey?: string; + firstAcctIndex?: number; + accounts?: LedgerAccount[]; + deployHash?: string; + error?: string; + message?: string; + msgHash?: string; + signatureHex?: string; +} + +export interface LedgerAccountsOptions { + size: number; + offset: number; +} + +export interface SignResult { + signatureHex: string; + signature: Uint8Array; +} + +export type LedgerTransport = 'USB' | 'Bluetooth'; +export type SelectedTransport = LedgerTransport | undefined; diff --git a/src/libs/types/account.ts b/src/libs/types/account.ts index 70c9c1f9a..93cc29517 100644 --- a/src/libs/types/account.ts +++ b/src/libs/types/account.ts @@ -1,11 +1,17 @@ export interface KeyPair { - secretKey: string; + secretKey: string; // can be empty string publicKey: string; } export interface Account extends KeyPair { name: string; imported?: boolean; + hardware?: HardwareWalletType; hidden: boolean; + derivationIndex?: number; +} + +export enum HardwareWalletType { + Ledger = 'Ledger' } export interface AccountWithBalance extends Account { diff --git a/src/libs/ui/components/account-list/account-list-item.tsx b/src/libs/ui/components/account-list/account-list-item.tsx index e3cf14b06..b08bab155 100644 --- a/src/libs/ui/components/account-list/account-list-item.tsx +++ b/src/libs/ui/components/account-list/account-list-item.tsx @@ -6,7 +6,7 @@ import { FlexColumn, SpacingSize } from '@libs/layout'; -import { AccountListRows } from '@libs/types/account'; +import { AccountListRows, HardwareWalletType } from '@libs/types/account'; import { AccountActionsMenuPopover, Avatar, @@ -105,7 +105,8 @@ export const AccountListItem = ({ variant={HashVariant.CaptionHash} truncated withoutTooltip - withTag={account.imported} + isImported={account.imported} + isLedger={account.hardware === HardwareWalletType.Ledger} /> CSPR diff --git a/src/libs/ui/components/account-list/account-list.tsx b/src/libs/ui/components/account-list/account-list.tsx index 7566c47aa..f38f213b9 100644 --- a/src/libs/ui/components/account-list/account-list.tsx +++ b/src/libs/ui/components/account-list/account-list.tsx @@ -3,8 +3,10 @@ import { Trans, useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import styled from 'styled-components'; +import { isLedgerAvailable } from '@src/utils'; + import { useAccountManager } from '@popup/hooks/use-account-actions-with-events'; -import { RouterPath, useTypedNavigate } from '@popup/router'; +import { RouterPath, useTypedLocation, useTypedNavigate } from '@popup/router'; import { WindowApp } from '@background/create-open-window'; import { @@ -16,14 +18,14 @@ import { import { useWindowManager } from '@hooks/use-window-manager'; import { getAccountHashFromPublicKey } from '@libs/entities/Account'; -import { CenteredFlexRow, SpacingSize } from '@libs/layout'; +import { FlexColumn, SpacingSize } from '@libs/layout'; import { AccountListRows } from '@libs/types/account'; import { Button, List } from '@libs/ui/components'; import { sortAccounts } from '@libs/ui/components/account-list/utils'; import { AccountListItem } from './account-list-item'; -const ButtonContainer = styled(CenteredFlexRow)` +const ButtonContainer = styled(FlexColumn)` padding: 16px; `; @@ -32,8 +34,8 @@ interface AccountListProps { } export const AccountList = ({ closeModal }: AccountListProps) => { + const { pathname } = useTypedLocation(); const [accountListRows, setAccountListRows] = useState([]); - const { changeActiveAccountWithEvent: changeActiveAccount } = useAccountManager(); const { t } = useTranslation(); @@ -66,7 +68,7 @@ export const AccountList = ({ closeModal }: AccountListProps) => { { const isConnected = connectedAccountNames.includes(account.name); const isActiveAccount = activeAccountName === account.name; @@ -89,25 +91,37 @@ export const AccountList = ({ closeModal }: AccountListProps) => { + {isLedgerAvailable && ( + + )} )} /> diff --git a/src/libs/ui/components/avatar/avatar.tsx b/src/libs/ui/components/avatar/avatar.tsx index da5ac598e..c4d851db6 100644 --- a/src/libs/ui/components/avatar/avatar.tsx +++ b/src/libs/ui/components/avatar/avatar.tsx @@ -1,13 +1,9 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import styled, { DefaultTheme, useTheme } from 'styled-components'; import { isValidAccountHash, isValidPublicKey } from '@src/utils'; -import { selectThemeModeSetting } from '@background/redux/settings/selectors'; -import { ThemeMode } from '@background/redux/settings/types'; - -import { useSystemThemeDetector } from '@hooks/use-system-theme-detector'; +import { useIsDarkMode } from '@hooks/use-is-dark-mode'; import { AlignedFlexRow, @@ -59,14 +55,7 @@ export const Avatar = ({ }: AvatarTypes) => { const theme = useTheme(); - const themeMode = useSelector(selectThemeModeSetting); - - const isSystemDarkTheme = useSystemThemeDetector(); - - const isDarkMode = - themeMode === ThemeMode.SYSTEM - ? isSystemDarkTheme - : themeMode === ThemeMode.DARK; + const isDarkMode = useIsDarkMode(); const connectIcon = isDarkMode ? displayContext === 'header' diff --git a/src/libs/ui/components/checkbox/checkbox.tsx b/src/libs/ui/components/checkbox/checkbox.tsx index e6d20a501..43d224074 100644 --- a/src/libs/ui/components/checkbox/checkbox.tsx +++ b/src/libs/ui/components/checkbox/checkbox.tsx @@ -68,7 +68,10 @@ export function Checkbox({ data-testid={dataTestId} disabled={disabled} > - + {label && ( {label} @@ -77,5 +80,3 @@ export function Checkbox({ ); } - -export default Checkbox; diff --git a/src/libs/ui/components/hash/hash.tsx b/src/libs/ui/components/hash/hash.tsx index 1b5899f19..351ae6bd2 100644 --- a/src/libs/ui/components/hash/hash.tsx +++ b/src/libs/ui/components/hash/hash.tsx @@ -2,11 +2,13 @@ import React, { useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import styled from 'styled-components'; -import { CenteredFlexRow } from '@libs/layout'; +import { useIsDarkMode } from '@hooks/use-is-dark-mode'; + +import { CenteredFlexRow, SpacingSize } from '@libs/layout'; import { CopyToClipboard, Placement, - Tag, + SvgIcon, Tooltip, Typography } from '@libs/ui/components'; @@ -36,7 +38,8 @@ interface HashProps { truncatedSize?: TruncateKeySize; color?: ContentColor; withCopyOnSelfClick?: boolean; - withTag?: boolean; + isImported?: boolean; + isLedger?: boolean; placement?: Placement; withoutTooltip?: boolean; } @@ -47,12 +50,14 @@ export function Hash({ withCopyOnSelfClick = true, truncated, color, - withTag, + isImported, truncatedSize, placement, - withoutTooltip = false + withoutTooltip = false, + isLedger }: HashProps) { const { t } = useTranslation(); + const isDarkMode = useIsDarkMode(); const HashComponent = useMemo( () => ( @@ -70,12 +75,27 @@ export function Hash({ {truncated ? truncateKey(value, { size: truncatedSize }) : value} - {withTag && ( - {`${t('Imported')}`} + {isImported && ( + + )} + {isLedger && ( + )} ), [ + isDarkMode, truncated, withoutTooltip, value, @@ -83,8 +103,8 @@ export function Hash({ variant, color, truncatedSize, - withTag, - t + isImported, + isLedger ] ); @@ -98,7 +118,9 @@ export function Hash({ Copied! ) : ( - {HashComponent} + + {HashComponent} + )} )} @@ -106,5 +128,6 @@ export function Hash({ /> ); } - return {HashComponent}; + + return {HashComponent}; } diff --git a/src/libs/ui/components/index.ts b/src/libs/ui/components/index.ts index 3b2070954..eaf008ce1 100644 --- a/src/libs/ui/components/index.ts +++ b/src/libs/ui/components/index.ts @@ -52,3 +52,9 @@ export * from './theme-switcher/theme-switcher'; export * from './identicon/identicon'; export * from './spinner/spinner'; export * from './tips/tips'; +export * from './review-with-ledger/review-with-ledger'; +export * from './no-connected-ledger/no-connected-ledger'; +export * from './ledger-footer/ledger-footer'; +export * from './ledger-error-view/ledger-error-view'; +export * from './ledger-event-view/ledger-event-view'; +export * from './ledger-connection-view/ledger-connection-view'; diff --git a/src/libs/ui/components/input/input.tsx b/src/libs/ui/components/input/input.tsx index 690a8baf2..9d4e32ead 100644 --- a/src/libs/ui/components/input/input.tsx +++ b/src/libs/ui/components/input/input.tsx @@ -15,7 +15,15 @@ const getThemeColorByError = (error?: boolean) => { }; const InputContainer = styled('div')( - ({ theme, oneColoredIcons, disabled, error, monotype, readOnly }) => ({ + ({ + theme, + oneColoredIcons, + disabled, + error, + monotype, + readOnly, + secondaryBackground + }) => ({ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', @@ -23,7 +31,9 @@ const InputContainer = styled('div')( padding: '0 16px', borderRadius: theme.borderRadius.base, color: theme.color.contentPrimary, - background: theme.color.backgroundPrimary, + background: secondaryBackground + ? theme.color.backgroundSecondary + : theme.color.backgroundPrimary, caretColor: theme.color.fillCritical, fontFamily: monotype ? theme.typography.fontFamily.mono @@ -139,6 +149,7 @@ export interface InputProps extends BaseProps { validationText?: string | null; dataTestId?: string; autoComplete?: string; + secondaryBackground?: boolean; } export const Input = forwardRef(function Input( @@ -162,6 +173,7 @@ export const Input = forwardRef(function Input( dataTestId, readOnly, autoComplete, + secondaryBackground, ...restProps }: InputProps, ref @@ -200,6 +212,7 @@ export const Input = forwardRef(function Input( error={error} height={height} oneColoredIcons={oneColoredIcons} + secondaryBackground={secondaryBackground} > {prefixIcon && {prefixIcon}} diff --git a/src/libs/ui/components/ledger-connection-view/ledger-connection-view.tsx b/src/libs/ui/components/ledger-connection-view/ledger-connection-view.tsx new file mode 100644 index 000000000..ff3dbf61f --- /dev/null +++ b/src/libs/ui/components/ledger-connection-view/ledger-connection-view.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +import { RouterPath, useTypedNavigate } from '@popup/router'; + +import { + HeaderPopup, + HeaderSubmenuBarNavLink, + PopupLayout +} from '@libs/layout'; +import { ILedgerEvent, LedgerTransport } from '@libs/services/ledger'; +import { LedgerEventView, renderLedgerFooter } from '@libs/ui/components'; + +interface INotConnectedLedgerProps { + event: ILedgerEvent; + onConnect: (tr?: LedgerTransport) => () => Promise; + closeNewLedgerWindowsAndClearState: () => void; + isAccountSelection?: boolean; +} + +export const LedgerConnectionView: React.FC = ({ + event, + onConnect, + closeNewLedgerWindowsAndClearState, + isAccountSelection = false +}) => { + const navigate = useTypedNavigate(); + + const onErrorCtaPressed = () => { + closeNewLedgerWindowsAndClearState(); + navigate(RouterPath.Home); + }; + + return ( + ( + ( + + )} + /> + )} + renderContent={() => ( + + )} + renderFooter={renderLedgerFooter({ + event, + onConnect, + onErrorCtaPressed + })} + /> + ); +}; diff --git a/src/libs/ui/components/ledger-error-view/ledger-error-view.tsx b/src/libs/ui/components/ledger-error-view/ledger-error-view.tsx new file mode 100644 index 000000000..56f668dfc --- /dev/null +++ b/src/libs/ui/components/ledger-error-view/ledger-error-view.tsx @@ -0,0 +1,92 @@ +import { Player } from '@lottiefiles/react-lottie-player'; +import React, { useEffect } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { useIsDarkMode } from '@hooks/use-is-dark-mode'; + +import dotsDarkModeAnimation from '@libs/animations/dots_dark_mode.json'; +import dotsLightModeAnimation from '@libs/animations/dots_light_mode.json'; +import { + CenteredFlexColumn, + ContentContainer, + IllustrationContainer, + ParagraphContainer, + SpacingSize +} from '@libs/layout'; +import { + ILedgerEvent, + LedgerEventStatus, + ledgerErrorsData +} from '@libs/services/ledger'; +import { SvgIcon, Typography } from '@libs/ui/components'; + +interface ILedgerErrorProps { + event: ILedgerEvent; +} + +export const LedgerErrorView: React.FC = ({ event }) => { + const { t } = useTranslation(); + const isDarkMode = useIsDarkMode(); + + const title = ledgerErrorsData[event.status]?.title; + const description = ledgerErrorsData[event.status]?.description; + + const isRejectedIcon = + event.status === LedgerEventStatus.CasperAppNotLoaded || + event.status === LedgerEventStatus.MsgSignatureCanceled || + event.status === LedgerEventStatus.SignatureCanceled; + + const withLoader = + event.status === LedgerEventStatus.CasperAppNotLoaded || + event.status === LedgerEventStatus.DeviceLocked; + + useEffect(() => { + const container = document.querySelector('#ms-container'); + + container?.scrollTo(0, 0); + }, []); + + if (!title) { + return null; + } + + return ( + + + + + + + {title} + + + {description && ( + + + {description} + + + )} + + {withLoader && ( + + + + )} + + ); +}; diff --git a/src/libs/ui/components/ledger-event-view/ledger-event-view.tsx b/src/libs/ui/components/ledger-event-view/ledger-event-view.tsx new file mode 100644 index 000000000..052f35897 --- /dev/null +++ b/src/libs/ui/components/ledger-event-view/ledger-event-view.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useState } from 'react'; + +import { + ILedgerEvent, + LedgerEventStatus, + isLedgerError, + isTransportAvailable +} from '@libs/services/ledger'; +import { + LedgerErrorView, + NoConnectedLedger, + ReviewWithLedger +} from '@libs/ui/components'; + +interface ILedgerEventViewProps { + event: ILedgerEvent; + isAccountSelection?: boolean; +} + +export const LedgerEventView: React.FC = ({ + event, + isAccountSelection = false +}) => { + const [available, setAvailable] = useState(true); + + useEffect(() => { + isTransportAvailable().then(setAvailable); + }, []); + + if (!available) { + return ( + + ); + } + + if ( + event.status === LedgerEventStatus.SignatureRequestedToUser && + event.deployHash + ) { + return ; + } + + if ( + event.status === LedgerEventStatus.MsgSignatureRequestedToUser && + event.msgHash + ) { + return ; + } + + if (isLedgerError(event)) { + return ; + } + + return ( + + ); +}; diff --git a/src/libs/ui/components/ledger-footer/ledger-disconnected-footer.tsx b/src/libs/ui/components/ledger-footer/ledger-disconnected-footer.tsx new file mode 100644 index 000000000..e5db4c210 --- /dev/null +++ b/src/libs/ui/components/ledger-footer/ledger-disconnected-footer.tsx @@ -0,0 +1,111 @@ +import { Player } from '@lottiefiles/react-lottie-player'; +import React, { useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { ledgerStateCleared } from '@background/redux/ledger/actions'; +import { dispatchToMainStore } from '@background/redux/utils'; + +import { useIsDarkMode } from '@hooks/use-is-dark-mode'; + +import dotsDarkModeAnimation from '@libs/animations/dots_dark_mode.json'; +import dotsLightModeAnimation from '@libs/animations/dots_light_mode.json'; +import { CenteredFlexColumn, FooterButtonsContainer } from '@libs/layout'; +import { + IsUsbLedgerTransportAvailable, + LedgerTransport, + subscribeToBluetoothAvailability +} from '@libs/services/ledger'; +import { Button } from '@libs/ui/components'; + +interface ILedgerDisconnectedFooterProps { + onConnect: (tr?: LedgerTransport) => () => Promise; +} + +export const LedgerDisconnectedFooter: React.FC< + ILedgerDisconnectedFooterProps +> = ({ onConnect }) => { + const { t } = useTranslation(); + const searchParams = new URLSearchParams(document.location.search); + const ledgerTransport = searchParams.get('ledgerTransport'); + + const isDarkMode = useIsDarkMode(); + const [isPlayingLoading, setIsPlayingLoading] = useState(false); + const [usbAvailable, setUsbAvailable] = useState(true); + const [bluetoothAvailable, setBluetoothAvailable] = useState(true); + + useEffect(() => { + IsUsbLedgerTransportAvailable().then(setUsbAvailable); + }, []); + + useEffect(() => { + const sub = subscribeToBluetoothAvailability(setBluetoothAvailable); + + return () => sub.unsubscribe(); + }, []); + + return ( + + {isPlayingLoading ? ( + + + + ) : ( + <> + {usbAvailable && + (ledgerTransport ? ledgerTransport === 'USB' : true) && ( + + )} + {bluetoothAvailable && + (ledgerTransport ? ledgerTransport === 'Bluetooth' : true) && ( + + )} + + )} + + ); +}; diff --git a/src/libs/ui/components/ledger-footer/ledger-footer.tsx b/src/libs/ui/components/ledger-footer/ledger-footer.tsx new file mode 100644 index 000000000..eac015ea4 --- /dev/null +++ b/src/libs/ui/components/ledger-footer/ledger-footer.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; + +import { FooterButtonsContainer } from '@libs/layout'; +import { + ILedgerEvent, + LedgerEventStatus, + LedgerTransport, + isLedgerError +} from '@libs/services/ledger'; +import { Button } from '@libs/ui/components'; + +import { LedgerDisconnectedFooter } from './ledger-disconnected-footer'; + +interface IRenderLedgerFooterParams { + event: ILedgerEvent; + onErrorCtaPressed: () => void; + onConnect: (tr?: LedgerTransport) => () => Promise; +} + +export const renderLedgerFooter = ({ + event, + onErrorCtaPressed, + onConnect +}: IRenderLedgerFooterParams) => { + if ( + event?.status === LedgerEventStatus.Disconnected || + event?.status === LedgerEventStatus.LedgerAskPermission + ) { + return () => ; + } else if (isLedgerError(event)) { + return () => ; + } + + return undefined; +}; + +export const LedgerErrorFooter: React.FC< + Pick +> = ({ onErrorCtaPressed }) => { + const { t } = useTranslation(); + + return ( + + + + ); +}; diff --git a/src/libs/ui/components/list/list.tsx b/src/libs/ui/components/list/list.tsx index f5ef9365a..4ff895b55 100644 --- a/src/libs/ui/components/list/list.tsx +++ b/src/libs/ui/components/list/list.tsx @@ -81,6 +81,7 @@ interface ListProps { maxHeight?: number; borderRadius?: 'base'; height?: number; + maxItemsToRender?: number; } export function List({ @@ -97,7 +98,8 @@ export function List({ stickyHeader, maxHeight, borderRadius, - height + height, + maxItemsToRender }: ListProps) { const separatorLine = marginLeftForHeaderSeparatorLine || marginLeftForHeaderSeparatorLine === 0 @@ -153,11 +155,19 @@ export function List({ - {rows.map((row, index, array) => ( - - {renderRow(row, index, array)} - - ))} + {maxItemsToRender + ? rows + .slice(0, maxItemsToRender) + .map((row, index, array) => ( + + {renderRow(row, index, array)} + + )) + : rows.map((row, index, array) => ( + + {renderRow(row, index, array)} + + ))} )} diff --git a/src/libs/ui/components/no-connected-ledger/no-connected-ledger.tsx b/src/libs/ui/components/no-connected-ledger/no-connected-ledger.tsx new file mode 100644 index 000000000..c7a857ef2 --- /dev/null +++ b/src/libs/ui/components/no-connected-ledger/no-connected-ledger.tsx @@ -0,0 +1,156 @@ +import { Player } from '@lottiefiles/react-lottie-player'; +import React, { useEffect, useMemo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +import { ledgerSupportLink } from '@src/constants'; + +import { useIsDarkMode } from '@hooks/use-is-dark-mode'; + +import dotsDarkModeAnimation from '@libs/animations/dots_dark_mode.json'; +import dotsLightModeAnimation from '@libs/animations/dots_light_mode.json'; +import { + AlignedFlexRow, + CenteredFlexColumn, + ContentContainer, + IllustrationContainer, + ParagraphContainer, + SpacingSize +} from '@libs/layout'; +import { ILedgerEvent, LedgerEventStatus } from '@libs/services/ledger'; +import { Link, List, SvgIcon, Typography } from '@libs/ui/components'; + +const ItemContainer = styled(AlignedFlexRow)` + padding: 16px; +`; + +interface INoConnectedLedgerProps { + event?: ILedgerEvent; + isAccountSelection: boolean; +} + +export const NoConnectedLedger: React.FC = ({ + event, + isAccountSelection +}) => { + const { t } = useTranslation(); + const isDarkMode = useIsDarkMode(); + + const steps = useMemo( + () => [ + { + id: 1, + text: 'Connect Ledger to your device' + }, + { + id: 2, + text: 'Open Casper app on your Ledger' + }, + { + id: 3, + text: isAccountSelection + ? 'Get back here to see list of accounts' + : 'Get back here to see Txn hash' + } + ], + [isAccountSelection] + ); + + useEffect(() => { + const container = document.querySelector('#ms-container'); + + container?.scrollTo(0, 0); + }, []); + + if ( + !( + event?.status === LedgerEventStatus.Disconnected || + event?.status === LedgerEventStatus.WaitingResponseFromDevice || + event?.status === LedgerEventStatus.LedgerAskPermission + ) + ) { + return null; + } + + return ( + + + {event.status === LedgerEventStatus.WaitingResponseFromDevice ? ( + + ) : ( + + )} + + + + {event.status === LedgerEventStatus.WaitingResponseFromDevice && ( + Ledger is connecting + )} + {event.status === LedgerEventStatus.Disconnected && ( + Open the Casper app on your Ledger device + )} + {event.status === LedgerEventStatus.LedgerAskPermission && ( + Next, approve access to your Ledger device + )} + + + + {event.status === LedgerEventStatus.WaitingResponseFromDevice && ( + + + Follow the steps to be able to [Sign/Confirm] transaction with + Ledger. + + + )} + {event.status === LedgerEventStatus.Disconnected && ( + + + Learn more about Ledger + + + )} + + + {event.status === LedgerEventStatus.WaitingResponseFromDevice && ( + ( + + + + {text} + + + )} + marginLeftForItemSeparatorLine={56} + /> + )} + + {event.status === LedgerEventStatus.WaitingResponseFromDevice && ( + + + + )} + + ); +}; diff --git a/src/libs/ui/components/review-with-ledger/review-with-ledger.tsx b/src/libs/ui/components/review-with-ledger/review-with-ledger.tsx new file mode 100644 index 000000000..1190c46ad --- /dev/null +++ b/src/libs/ui/components/review-with-ledger/review-with-ledger.tsx @@ -0,0 +1,67 @@ +import { Player } from '@lottiefiles/react-lottie-player'; +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +import { useIsDarkMode } from '@hooks/use-is-dark-mode'; + +import dotsDarkModeAnimation from '@libs/animations/dots_dark_mode.json'; +import dotsLightModeAnimation from '@libs/animations/dots_light_mode.json'; +import { + CenteredFlexColumn, + ContentContainer, + ParagraphContainer, + SpacingSize, + VerticalSpaceContainer +} from '@libs/layout'; +import { FormField, TextArea, Typography } from '@libs/ui/components'; + +interface ReviewWithLedgerProps { + hash: string; + hashLabel: string; +} + +const HeaderTextContainer = styled(ParagraphContainer)` + // We are using this instead of 'top' prop in , because there is a problem with height when we call it in layout window + padding-top: 24px; +`; + +export const ReviewWithLedger = ({ + hash, + hashLabel +}: ReviewWithLedgerProps) => { + const { t } = useTranslation(); + const isDarkMode = useIsDarkMode(); + + return ( + + + + Review and sign with Ledger + + + + + {`Compare the ${hashLabel.toLowerCase()} on your Ledger device with the value + below and approve or reject the signature.`} + + + + +