From 10e4edb601b48ed9992818e216cbe54f7bf179a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:55:06 +0000 Subject: [PATCH 01/37] chore(deps): bump i18next-browser-languagedetector from 7.2.0 to 7.2.1 (#2039) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 6fc9c3f88f..f46579f39b 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "framer-motion": "^11.0.18", "html5-qrcode": "^2.3.8", "i18next": "^23.10.0", - "i18next-browser-languagedetector": "^7.2.0", + "i18next-browser-languagedetector": "^7.2.1", "lodash.debounce": "^4.0.8", "lodash.throttle": "^4.1.1", "qrcode-generator": "1.4.4", diff --git a/yarn.lock b/yarn.lock index 0dc1c6e092..0e66e858fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4965,12 +4965,12 @@ __metadata: languageName: node linkType: hard -"i18next-browser-languagedetector@npm:^7.2.0": - version: 7.2.0 - resolution: "i18next-browser-languagedetector@npm:7.2.0" +"i18next-browser-languagedetector@npm:^7.2.1": + version: 7.2.1 + resolution: "i18next-browser-languagedetector@npm:7.2.1" dependencies: "@babel/runtime": "npm:^7.23.2" - checksum: 10c0/d7676e6c9895d46e659effaeba11f10c39cbe99429560667de7689cb56db6977239d350be850c4caf4279781c19c50a7193cb9cc38bb485f391b8e1893e407ae + checksum: 10c0/44fa71af4efb4cd6cc8bfbbd3f3b2735159e17d8f4396346e4016c6dd0ecbcdd68f1ec17609fd0de8dd6754c3d847d6e7e03227c19c1879d4c265cb1918948bb languageName: node linkType: hard @@ -6488,7 +6488,7 @@ __metadata: gh-pages: "npm:^6.1.1" html5-qrcode: "npm:^2.3.8" i18next: "npm:^23.10.0" - i18next-browser-languagedetector: "npm:^7.2.0" + i18next-browser-languagedetector: "npm:^7.2.1" lodash.debounce: "npm:^4.0.8" lodash.throttle: "npm:^4.1.1" prettier: "npm:^3.2.5" From bceb8f0399887363f47246ba8ebca49dc610a7ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:57:15 +0000 Subject: [PATCH 02/37] chore(deps-dev): bump vite from 5.2.2 to 5.2.7 (#2041) --- package.json | 2 +- yarn.lock | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index f46579f39b..50b825aaa0 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "prettier-plugin-organize-imports": "^3.2.4", "sass": "^1.72.0", "typescript": "^5.3.3", - "vite": "^5.2.2", + "vite": "^5.2.7", "vite-bundle-visualizer": "^1.1.0", "vite-plugin-checker": "^0.6.4", "vite-plugin-eslint": "^1.8.1", diff --git a/yarn.lock b/yarn.lock index 0e66e858fc..601c22a5f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6507,7 +6507,7 @@ __metadata: styled-components: "npm:^6.1.8" typescript: "npm:^5.3.3" usehooks-ts: "npm:^3.0.1" - vite: "npm:^5.2.2" + vite: "npm:^5.2.7" vite-bundle-visualizer: "npm:^1.1.0" vite-plugin-checker: "npm:^0.6.4" vite-plugin-eslint: "npm:^1.8.1" @@ -6553,6 +6553,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.38": + version: 8.4.38 + resolution: "postcss@npm:8.4.38" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.0.0" + source-map-js: "npm:^1.2.0" + checksum: 10c0/955407b8f70cf0c14acf35dab3615899a2a60a26718a63c848cf3c29f2467b0533991b985a2b994430d890bd7ec2b1963e36352b0774a19143b5f591540f7c06 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -8226,13 +8237,13 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.2.2": - version: 5.2.2 - resolution: "vite@npm:5.2.2" +"vite@npm:^5.2.7": + version: 5.2.7 + resolution: "vite@npm:5.2.7" dependencies: esbuild: "npm:^0.20.1" fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.36" + postcss: "npm:^8.4.38" rollup: "npm:^4.13.0" peerDependencies: "@types/node": ^18.0.0 || >=20.0.0 @@ -8262,7 +8273,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/472c6a1d41707ef51a5056ccc9e347333a3a975beb6069998d3d7a134555662b856e27628cc1354200c32d63373d7e4ef73385a4e90cc3032e48d06fb77928e5 + checksum: 10c0/ca927a8df388f75df194d5a5ba2be4ee46dc1d99d5be277f13c6d1ed4a4df833cc953741ef8e984061ea38b531df84e15e2a9f5deea1626317bcbec63c8ca01c languageName: node linkType: hard From 1b4645663a20302d6ce78d5b9bf78082ecbd9e0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:57:52 +0000 Subject: [PATCH 03/37] chore(deps-dev): bump @types/react-dom from 18.2.22 to 18.2.23 (#2040) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 50b825aaa0..e89f281b10 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@types/lodash.debounce": "^4.0.9", "@types/lodash.throttle": "^4.1.9", "@types/react": "^18.2.67", - "@types/react-dom": "^18.2.22", + "@types/react-dom": "^18.2.23", "@types/react-helmet": "^6.1.11", "@types/react-scroll": "^1.8.10", "@types/styled-components": "^5.1.34", diff --git a/yarn.lock b/yarn.lock index 601c22a5f0..c69af79f76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2218,12 +2218,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.2.22": - version: 18.2.22 - resolution: "@types/react-dom@npm:18.2.22" +"@types/react-dom@npm:^18.2.23": + version: 18.2.23 + resolution: "@types/react-dom@npm:18.2.23" dependencies: "@types/react": "npm:*" - checksum: 10c0/cd85b5f402126e44b8c7b573e74737389816abcc931b2b14d8f946ba81cce8637ea490419488fcae842efb1e2f69853bc30522e43fd8359e1007d4d14b8d8146 + checksum: 10c0/9348e93558aa67b4b237bd0eab62e72e85f3e17a1c45fde04d874476269730f7c671b3d62390c4fca588da2a026e90cc74148abc349dbfd4ee5535a82ccdf38e languageName: node linkType: hard @@ -6453,7 +6453,7 @@ __metadata: "@types/lodash.debounce": "npm:^4.0.9" "@types/lodash.throttle": "npm:^4.1.9" "@types/react": "npm:^18.2.67" - "@types/react-dom": "npm:^18.2.22" + "@types/react-dom": "npm:^18.2.23" "@types/react-helmet": "npm:^6.1.11" "@types/react-scroll": "npm:^1.8.10" "@types/styled-components": "npm:^5.1.34" From a99a8e659433c25fb09623835687d37aa0b5ddc8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:57:57 +0000 Subject: [PATCH 04/37] chore(deps-dev): bump @typescript-eslint/parser from 7.3.1 to 7.4.0 (#2044) --- package.json | 2 +- yarn.lock | 66 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index e89f281b10..947b65e6e3 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@types/react-scroll": "^1.8.10", "@types/styled-components": "^5.1.34", "@typescript-eslint/eslint-plugin": "^7.1.0", - "@typescript-eslint/parser": "^7.1.0", + "@typescript-eslint/parser": "^7.4.0", "@vitejs/plugin-react-swc": "^3.6.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", diff --git a/yarn.lock b/yarn.lock index c69af79f76..a502523e77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2320,21 +2320,21 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^7.1.0": - version: 7.3.1 - resolution: "@typescript-eslint/parser@npm:7.3.1" - dependencies: - "@typescript-eslint/scope-manager": "npm:7.3.1" - "@typescript-eslint/types": "npm:7.3.1" - "@typescript-eslint/typescript-estree": "npm:7.3.1" - "@typescript-eslint/visitor-keys": "npm:7.3.1" +"@typescript-eslint/parser@npm:^7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/parser@npm:7.4.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:7.4.0" + "@typescript-eslint/types": "npm:7.4.0" + "@typescript-eslint/typescript-estree": "npm:7.4.0" + "@typescript-eslint/visitor-keys": "npm:7.4.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.56.0 peerDependenciesMeta: typescript: optional: true - checksum: 10c0/c524e7021ea551cb83e19c7f1a697664171a6b227e16e33912243af659905a7effeaf9fc05e3c160cb99d8ba17552fa87e27be38261280daa733d4d4d4876eec + checksum: 10c0/70ae32d406685e83fc26b4f4d3eb90c59965e0ff4fec4fd89ecd3cb386376bedb75cd8c11691b9de4743243d61a7d17ae242fe6c689a7c443a8977bc9755700b languageName: node linkType: hard @@ -2348,6 +2348,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/scope-manager@npm:7.4.0" + dependencies: + "@typescript-eslint/types": "npm:7.4.0" + "@typescript-eslint/visitor-keys": "npm:7.4.0" + checksum: 10c0/d1dddf6819d753063fbbcae2cd01e861d0162a9755c6c786901654ccb9d316ca1dcc5887a61fb70e72372db4c2e67c6d1890f09d8b0270971c18b48808765ba9 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:7.3.1": version: 7.3.1 resolution: "@typescript-eslint/type-utils@npm:7.3.1" @@ -2372,6 +2382,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/types@npm:7.4.0" + checksum: 10c0/685df163cdd6d546de8a2d22896e461777a89756faf1f34342c959e7d3f4cc75b1f47a96da50483fe1da75d06515bb105f58339d277ad7e02c15ab61c90ad097 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:7.3.1": version: 7.3.1 resolution: "@typescript-eslint/typescript-estree@npm:7.3.1" @@ -2391,6 +2408,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.4.0" + dependencies: + "@typescript-eslint/types": "npm:7.4.0" + "@typescript-eslint/visitor-keys": "npm:7.4.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + minimatch: "npm:9.0.3" + semver: "npm:^7.5.4" + ts-api-utils: "npm:^1.0.1" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/31910f9283bcb2db7d3dd77b5a3b0c52e9769cd296e78a5ba742360f9e1971a6a3e1b5eb31109b4d584a62c2caa3075a346c5413b55e28cda0226a73865d62b7 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:7.3.1": version: 7.3.1 resolution: "@typescript-eslint/utils@npm:7.3.1" @@ -2418,6 +2454,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.4.0" + dependencies: + "@typescript-eslint/types": "npm:7.4.0" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10c0/bd2ca99f4a771494b89124a1e4cd7f3c817ca4916b8a0168c5c226a245f25cf646b10095100fb8cb6d97134f63fa5bb15098daa94f48657b65332e8671ffdb52 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -6458,7 +6504,7 @@ __metadata: "@types/react-scroll": "npm:^1.8.10" "@types/styled-components": "npm:^5.1.34" "@typescript-eslint/eslint-plugin": "npm:^7.1.0" - "@typescript-eslint/parser": "npm:^7.1.0" + "@typescript-eslint/parser": "npm:^7.4.0" "@vitejs/plugin-react-swc": "npm:^3.6.0" "@w3ux/extension-assets": "npm:^0.2.3" "@w3ux/hooks": "npm:^0.0.3" From 1d200babc7a02622052e5b9fb90532c9667cd255 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:58:03 +0000 Subject: [PATCH 05/37] chore(deps): bump @dotlottie/player-component from 2.7.11 to 2.7.12 (#2043) --- package.json | 2 +- yarn.lock | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 947b65e6e3..733444a2bb 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "visualizer": "vite-bundle-visualizer" }, "dependencies": { - "@dotlottie/player-component": "^2.7.10", + "@dotlottie/player-component": "^2.7.12", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-brands-svg-icons": "^6.5.1", "@fortawesome/free-regular-svg-icons": "^6.5.1", diff --git a/yarn.lock b/yarn.lock index a502523e77..cff7383210 100644 --- a/yarn.lock +++ b/yarn.lock @@ -268,16 +268,16 @@ __metadata: languageName: node linkType: hard -"@dotlottie/common@npm:0.7.10": - version: 0.7.10 - resolution: "@dotlottie/common@npm:0.7.10" +"@dotlottie/common@npm:0.7.11": + version: 0.7.11 + resolution: "@dotlottie/common@npm:0.7.11" dependencies: "@dotlottie/dotlottie-js": "npm:^0.7.0" "@preact/signals-core": "npm:^1.2.3" howler: "npm:^2.2.3" lottie-web: "npm:^5.12.2" xstate: "npm:^4.38.1" - checksum: 10c0/09d934b4a78670132ed337ff57cc5eed41e1aaf24abc08c8fda66844c3a20ea92d54b70570007d08e30fa1dfa34b7db496fa37205f94588a88a7e92ec96d8ed5 + checksum: 10c0/185446390c8c93eacce98a1d8dfa21415f26382c6523f3ce60e93cdbca37ce967ca6546809c2743fdcaedd657a872750b95bb1a711a35f64015fbe303ed0dc39 languageName: node linkType: hard @@ -294,13 +294,13 @@ __metadata: languageName: node linkType: hard -"@dotlottie/player-component@npm:^2.7.10": - version: 2.7.11 - resolution: "@dotlottie/player-component@npm:2.7.11" +"@dotlottie/player-component@npm:^2.7.12": + version: 2.7.12 + resolution: "@dotlottie/player-component@npm:2.7.12" dependencies: - "@dotlottie/common": "npm:0.7.10" + "@dotlottie/common": "npm:0.7.11" lit: "npm:^2.7.5" - checksum: 10c0/55d686a1ab0f5339e7c4e43c03b559fa6830d71aa90ef6a23e08a5b5d5bccfe29275c81f06efa07d0a44dab34e46c809e873a2da3ab6110470e63c0f38e07b7b + checksum: 10c0/0faed3adc8a734418f029f3858928e1d6436f4d31c05c4bbee7dadeadc7cffa9b01cd4fd9675b5b152e56aa44a53d3aa879a37465b2c36dcf6149adb74bb2c6f languageName: node linkType: hard @@ -6480,7 +6480,7 @@ __metadata: version: 0.0.0-use.local resolution: "polkadot-staking-dashboard@workspace:." dependencies: - "@dotlottie/player-component": "npm:^2.7.10" + "@dotlottie/player-component": "npm:^2.7.12" "@fortawesome/fontawesome-svg-core": "npm:^6.5.1" "@fortawesome/free-brands-svg-icons": "npm:^6.5.1" "@fortawesome/free-regular-svg-icons": "npm:^6.5.1" From 8ca23908ddbc641488de20cb804c384e5394e0f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:58:14 +0000 Subject: [PATCH 06/37] chore(deps): bump framer-motion from 11.0.18 to 11.0.24 (#2036) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 733444a2bb..e8e779179b 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "chart.js": "^4.4.2", "chroma-js": "^2.4.2", "date-fns": "^3.3.1", - "framer-motion": "^11.0.18", + "framer-motion": "^11.0.24", "html5-qrcode": "^2.3.8", "i18next": "^23.10.0", "i18next-browser-languagedetector": "^7.2.1", diff --git a/yarn.lock b/yarn.lock index cff7383210..ecb2114dd6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4522,9 +4522,9 @@ __metadata: languageName: node linkType: hard -"framer-motion@npm:^11.0.18": - version: 11.0.18 - resolution: "framer-motion@npm:11.0.18" +"framer-motion@npm:^11.0.24": + version: 11.0.24 + resolution: "framer-motion@npm:11.0.24" dependencies: tslib: "npm:^2.4.0" peerDependencies: @@ -4538,7 +4538,7 @@ __metadata: optional: true react-dom: optional: true - checksum: 10c0/a02e9ed86f03d6a20fb3dbcb9d17819c32f75fbeeba4825cba3cd1c48bea3e9ef35f44d7c199fa0c226e6cf4aa58446b6801fa1ee60ea10e609d1a46c5d7f45b + checksum: 10c0/8d884a828ef3e03683fdd5bbe5de64751f2a97f379f332262142caf2ae76d5af5fb1e2810a1a5845fb3322c8990f568997d22ffe8f0ff0f2eb7e637c5e0a5266 languageName: node linkType: hard @@ -6530,7 +6530,7 @@ __metadata: eslint-plugin-react: "npm:^7.34.1" eslint-plugin-react-hooks: "npm:^4.6.0" eslint-plugin-unused-imports: "npm:^3.1.0" - framer-motion: "npm:^11.0.18" + framer-motion: "npm:^11.0.24" gh-pages: "npm:^6.1.1" html5-qrcode: "npm:^2.3.8" i18next: "npm:^23.10.0" From 98fe3670bda62bf9e489cd30f5c4b689db6483e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:59:15 +0000 Subject: [PATCH 07/37] chore(deps): bump @zondax/ledger-substrate from 0.41.3 to 0.41.4 (#2045) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index e8e779179b..c3df684b11 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@w3ux/react-polkicon": "^0.0.2", "@w3ux/utils": "^0.0.2", "@w3ux/validator-assets": "^0.0.4", - "@zondax/ledger-substrate": "^0.41.3", + "@zondax/ledger-substrate": "^0.41.4", "bignumber.js": "^9.1.2", "bn.js": "^5.2.1", "buffer": "^6.0.3", diff --git a/yarn.lock b/yarn.lock index ecb2114dd6..fc1b98d1e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2613,9 +2613,9 @@ __metadata: languageName: node linkType: hard -"@zondax/ledger-substrate@npm:^0.41.3": - version: 0.41.3 - resolution: "@zondax/ledger-substrate@npm:0.41.3" +"@zondax/ledger-substrate@npm:^0.41.4": + version: 0.41.4 + resolution: "@zondax/ledger-substrate@npm:0.41.4" dependencies: "@ledgerhq/hw-transport": "npm:^6.27.1" bip32: "npm:^4.0.0" @@ -2626,7 +2626,7 @@ __metadata: hash.js: "npm:^1.1.7" bin: ledger-substrate: dist/cmd/cli.js - checksum: 10c0/fc1c3ce3e1b169be3dae05c5fbaf8e89bed709a4c25abceaff3c9f5f7938d4340834bb4ac21a43e67c3b603368f69486c3476f7378855dd99164d837d8c1a207 + checksum: 10c0/ac04674236f09f76d0cc5045fd6b23afdbbce493ec2a7fa3d37c5efb48be9aac355b2f2c146bb88b42f3423f38f9c249603076e9a9fbeaf22b5dc643fb1772f3 languageName: node linkType: hard @@ -6513,7 +6513,7 @@ __metadata: "@w3ux/react-polkicon": "npm:^0.0.2" "@w3ux/utils": "npm:^0.0.2" "@w3ux/validator-assets": "npm:^0.0.4" - "@zondax/ledger-substrate": "npm:^0.41.3" + "@zondax/ledger-substrate": "npm:^0.41.4" bignumber.js: "npm:^9.1.2" bn.js: "npm:^5.2.1" buffer: "npm:^6.0.3" From a5fd2837131cd04b2a8f95cb6d991526a3b9a94e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 22:02:56 +0000 Subject: [PATCH 08/37] chore(deps-dev): bump @typescript-eslint/eslint-plugin from 7.3.1 to 7.5.0 (#2049) --- package.json | 2 +- yarn.lock | 122 +++++++++++++++++++++++++-------------------------- 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index c3df684b11..f372e420ff 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@types/react-helmet": "^6.1.11", "@types/react-scroll": "^1.8.10", "@types/styled-components": "^5.1.34", - "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/parser": "^7.4.0", "@vitejs/plugin-react-swc": "^3.6.0", "eslint": "^8.57.0", diff --git a/yarn.lock b/yarn.lock index fc1b98d1e2..cd232d8757 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2295,15 +2295,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^7.1.0": - version: 7.3.1 - resolution: "@typescript-eslint/eslint-plugin@npm:7.3.1" +"@typescript-eslint/eslint-plugin@npm:^7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.5.0" dependencies: "@eslint-community/regexpp": "npm:^4.5.1" - "@typescript-eslint/scope-manager": "npm:7.3.1" - "@typescript-eslint/type-utils": "npm:7.3.1" - "@typescript-eslint/utils": "npm:7.3.1" - "@typescript-eslint/visitor-keys": "npm:7.3.1" + "@typescript-eslint/scope-manager": "npm:7.5.0" + "@typescript-eslint/type-utils": "npm:7.5.0" + "@typescript-eslint/utils": "npm:7.5.0" + "@typescript-eslint/visitor-keys": "npm:7.5.0" debug: "npm:^4.3.4" graphemer: "npm:^1.4.0" ignore: "npm:^5.2.4" @@ -2316,7 +2316,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/446c36801ee434854c935fd09f267bd68d537c1e422cfca87237230313b2ea40b512bb2357bcf489225df10a6d2f14dcd3ac8db80517b982abe0b609dd606c6c + checksum: 10c0/932a7b5a09c0138ef5a0bf00f8e6039fa209d4047092ffc187de048543c21f7ce24dc14f25f4c87b6f3bbb62335fc952e259e271fde88baf793217bde6460cfa languageName: node linkType: hard @@ -2338,16 +2338,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/scope-manager@npm:7.3.1" - dependencies: - "@typescript-eslint/types": "npm:7.3.1" - "@typescript-eslint/visitor-keys": "npm:7.3.1" - checksum: 10c0/08dd466b19445a8e2b093df7fcc59767289843d1cdc423b2f402a2a2c69a53e3cdf52dcc1497311346a45e875d77826a831b5b9a9fb7f709679f221344051c74 - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:7.4.0": version: 7.4.0 resolution: "@typescript-eslint/scope-manager@npm:7.4.0" @@ -2358,12 +2348,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/type-utils@npm:7.3.1" +"@typescript-eslint/scope-manager@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/scope-manager@npm:7.5.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:7.3.1" - "@typescript-eslint/utils": "npm:7.3.1" + "@typescript-eslint/types": "npm:7.5.0" + "@typescript-eslint/visitor-keys": "npm:7.5.0" + checksum: 10c0/a017b151a6b39ef591f8e2e65598e005e1b4b2d5494e4b91bddb5856b3a4d57dd8a58d2bc7a140e627eb574f93a2c8fe55f1307aa264c928ffd31d9e190bc5dd + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/type-utils@npm:7.5.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:7.5.0" + "@typescript-eslint/utils": "npm:7.5.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^1.0.1" peerDependencies: @@ -2371,14 +2371,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/0e9ad41fe9eac135e1f6b448a2e1660df83e93bd2c370f1aaabe8bbdd376cda0e00d02b884793a3ce3a51c962c1f5cac543bcc1f02e4d1de2af757031aa6cbed - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/types@npm:7.3.1" - checksum: 10c0/d3b579829db901b2ea52000a6e343b7e3814fa06f62ba42711df2533365a247e97699f64fc15482cc433302ff81e8a0eed1ed2b0478d0709171d57910d46bdd5 + checksum: 10c0/12915d4d1872638f5281e222a0d191676c478f250699c84864862e95a59e708222acefbf7ffdafc0872a007261219a3a2b1e667ff45eeafea7c4bcc5b955262c languageName: node linkType: hard @@ -2389,12 +2382,19 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/typescript-estree@npm:7.3.1" +"@typescript-eslint/types@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/types@npm:7.5.0" + checksum: 10c0/f3394f71f422dbd89f63b230f20e9769c12e47a287ff30ca03a80714e57ea21279b6f12a8ab14bafb00b59926f20a88894b2d1e72679f7ff298bae112679d4b3 + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:7.4.0": + version: 7.4.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.4.0" dependencies: - "@typescript-eslint/types": "npm:7.3.1" - "@typescript-eslint/visitor-keys": "npm:7.3.1" + "@typescript-eslint/types": "npm:7.4.0" + "@typescript-eslint/visitor-keys": "npm:7.4.0" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" @@ -2404,16 +2404,16 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/52dbfc590b01a43fae906dadd383c185b93fea5c8ac90aa2369f6c36d53a5d465fac02315a903a3b291974626045547ab53f346dc2271e93c8179deaad7a3961 + checksum: 10c0/31910f9283bcb2db7d3dd77b5a3b0c52e9769cd296e78a5ba742360f9e1971a6a3e1b5eb31109b4d584a62c2caa3075a346c5413b55e28cda0226a73865d62b7 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.4.0": - version: 7.4.0 - resolution: "@typescript-eslint/typescript-estree@npm:7.4.0" +"@typescript-eslint/typescript-estree@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.5.0" dependencies: - "@typescript-eslint/types": "npm:7.4.0" - "@typescript-eslint/visitor-keys": "npm:7.4.0" + "@typescript-eslint/types": "npm:7.5.0" + "@typescript-eslint/visitor-keys": "npm:7.5.0" debug: "npm:^4.3.4" globby: "npm:^11.1.0" is-glob: "npm:^4.0.3" @@ -2423,34 +2423,24 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 10c0/31910f9283bcb2db7d3dd77b5a3b0c52e9769cd296e78a5ba742360f9e1971a6a3e1b5eb31109b4d584a62c2caa3075a346c5413b55e28cda0226a73865d62b7 + checksum: 10c0/ea3a270c725d6be273188b86110e0393052cd64d1c54a56eb5ea405e6d3fbbe84fb3b1ce1b8496a4078ac1eefd37aedcf12be91876764f6de31d5aa5131c7bcd languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/utils@npm:7.3.1" +"@typescript-eslint/utils@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/utils@npm:7.5.0" dependencies: "@eslint-community/eslint-utils": "npm:^4.4.0" "@types/json-schema": "npm:^7.0.12" "@types/semver": "npm:^7.5.0" - "@typescript-eslint/scope-manager": "npm:7.3.1" - "@typescript-eslint/types": "npm:7.3.1" - "@typescript-eslint/typescript-estree": "npm:7.3.1" + "@typescript-eslint/scope-manager": "npm:7.5.0" + "@typescript-eslint/types": "npm:7.5.0" + "@typescript-eslint/typescript-estree": "npm:7.5.0" semver: "npm:^7.5.4" peerDependencies: eslint: ^8.56.0 - checksum: 10c0/1d7b049b2c4de1937832ae8ed681bbcd3b06b0d0b476cce67af96b2f65ff606413cc7dfdaad1e01057d24ba39bf5f6d4ba2923d23dab784d2bed5a217ab7b825 - languageName: node - linkType: hard - -"@typescript-eslint/visitor-keys@npm:7.3.1": - version: 7.3.1 - resolution: "@typescript-eslint/visitor-keys@npm:7.3.1" - dependencies: - "@typescript-eslint/types": "npm:7.3.1" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10c0/1765d9ee31adaa1cfaaa72a1acc987bba6cc382b5c6785ffcc2706a776c115e9310ea6761f70fe9b83bc7edf5ecb3cb6814c83704bd2bb807a6a35cf52f36958 + checksum: 10c0/c815ed6909769648953d6963c069038f7cac0c979051b25718feb30e0d3337b9647b75b8de00ac03fe960f0cc8dc4e8a81d4aac4719090a99785e0068712bd24 languageName: node linkType: hard @@ -2464,6 +2454,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:7.5.0": + version: 7.5.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.5.0" + dependencies: + "@typescript-eslint/types": "npm:7.5.0" + eslint-visitor-keys: "npm:^3.4.1" + checksum: 10c0/eecf02b8dd54e83738a143aca87b902af4b357028a90fd34ed7a2f40a3ae2f6a188b9ba53903f23c80e868f1fffbb039e9ddb63525438d659707cc7bfb269317 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -6503,7 +6503,7 @@ __metadata: "@types/react-helmet": "npm:^6.1.11" "@types/react-scroll": "npm:^1.8.10" "@types/styled-components": "npm:^5.1.34" - "@typescript-eslint/eslint-plugin": "npm:^7.1.0" + "@typescript-eslint/eslint-plugin": "npm:^7.5.0" "@typescript-eslint/parser": "npm:^7.4.0" "@vitejs/plugin-react-swc": "npm:^3.6.0" "@w3ux/extension-assets": "npm:^0.2.3" From 1612ca96867b6edcbee4376f421fabbae8963812 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 22:07:24 +0000 Subject: [PATCH 09/37] chore(deps-dev): bump @types/react from 18.2.67 to 18.2.73 (#2048) --- package.json | 2 +- yarn.lock | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f372e420ff..b941b87704 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@types/chroma-js": "^2.4.4", "@types/lodash.debounce": "^4.0.9", "@types/lodash.throttle": "^4.1.9", - "@types/react": "^18.2.67", + "@types/react": "^18.2.73", "@types/react-dom": "^18.2.23", "@types/react-helmet": "^6.1.11", "@types/react-scroll": "^1.8.10", diff --git a/yarn.lock b/yarn.lock index cd232d8757..3b27395c03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2245,7 +2245,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18.2.67": +"@types/react@npm:*": version: 18.2.67 resolution: "@types/react@npm:18.2.67" dependencies: @@ -2256,6 +2256,16 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.2.73": + version: 18.2.73 + resolution: "@types/react@npm:18.2.73" + dependencies: + "@types/prop-types": "npm:*" + csstype: "npm:^3.0.2" + checksum: 10c0/b6645ab3c20efa41cfccf58ce0be45419517a0ba4594e323dd400342fb1c1f9589d169cf9bfa85b5b0605e9097fe9de7734b6d0c533f5b9bc32aaadb624537a4 + languageName: node + linkType: hard + "@types/scheduler@npm:*": version: 0.16.8 resolution: "@types/scheduler@npm:0.16.8" @@ -6498,7 +6508,7 @@ __metadata: "@types/chroma-js": "npm:^2.4.4" "@types/lodash.debounce": "npm:^4.0.9" "@types/lodash.throttle": "npm:^4.1.9" - "@types/react": "npm:^18.2.67" + "@types/react": "npm:^18.2.73" "@types/react-dom": "npm:^18.2.23" "@types/react-helmet": "npm:^6.1.11" "@types/react-scroll": "npm:^1.8.10" From 5cef22b1ca49f4cccbd864aadd83e25c58f541d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 22:13:39 +0000 Subject: [PATCH 10/37] chore(deps-dev): bump typescript from 5.4.2 to 5.4.3 (#2046) --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index b941b87704..26da76dd61 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", "sass": "^1.72.0", - "typescript": "^5.3.3", + "typescript": "^5.4.3", "vite": "^5.2.7", "vite-bundle-visualizer": "^1.1.0", "vite-plugin-checker": "^0.6.4", diff --git a/yarn.lock b/yarn.lock index 3b27395c03..6ad5cea5bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6561,7 +6561,7 @@ __metadata: react-scroll: "npm:^1.9.0" sass: "npm:^1.72.0" styled-components: "npm:^6.1.8" - typescript: "npm:^5.3.3" + typescript: "npm:^5.4.3" usehooks-ts: "npm:^3.0.1" vite: "npm:^5.2.7" vite-bundle-visualizer: "npm:^1.1.0" @@ -8012,23 +8012,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.3.3": - version: 5.4.2 - resolution: "typescript@npm:5.4.2" +"typescript@npm:^5.4.3": + version: 5.4.3 + resolution: "typescript@npm:5.4.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/583ff68cafb0c076695f72d61df6feee71689568179fb0d3a4834dac343df6b6ed7cf7b6f6c801fa52d43cd1d324e2f2d8ae4497b09f9e6cfe3d80a6d6c9ca52 + checksum: 10c0/22443a8760c3668e256c0b34b6b45c359ef6cecc10c42558806177a7d500ab1a7d7aac1f976d712e26989ddf6731d2fbdd3212b7c73290a45127c1c43ba2005a languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin": - version: 5.4.2 - resolution: "typescript@patch:typescript@npm%3A5.4.2#optional!builtin::version=5.4.2&hash=5adc0c" +"typescript@patch:typescript@npm%3A^5.4.3#optional!builtin": + version: 5.4.3 + resolution: "typescript@patch:typescript@npm%3A5.4.3#optional!builtin::version=5.4.3&hash=5adc0c" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/fcf6658073d07283910d9a0e04b1d5d0ebc822c04dbb7abdd74c3151c7aa92fcddbac7d799404e358197222006ccdc4c0db219d223d2ee4ccd9e2b01333b49be + checksum: 10c0/6e51f8b7e6ec55b897b9e56b67e864fe8f44e30f4a14357aad5dc0f7432db2f01efc0522df0b6c36d361c51f2dc3dcac5c832efd96a404cfabf884e915d38828 languageName: node linkType: hard From 89142181db88a279070e4fb3fb7f855f2b0820f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Apr 2024 22:18:16 +0000 Subject: [PATCH 11/37] chore(deps): bump usehooks-ts from 3.0.1 to 3.0.2 (#2042) --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 26da76dd61..c2c65a1568 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "react-router-dom": "^6.22.3", "react-scroll": "^1.9.0", "styled-components": "^6.1.8", - "usehooks-ts": "^3.0.1" + "usehooks-ts": "^3.0.2" }, "devDependencies": { "@ledgerhq/logs": "^6.12.0", diff --git a/yarn.lock b/yarn.lock index 6ad5cea5bd..98dcad88c2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6562,7 +6562,7 @@ __metadata: sass: "npm:^1.72.0" styled-components: "npm:^6.1.8" typescript: "npm:^5.4.3" - usehooks-ts: "npm:^3.0.1" + usehooks-ts: "npm:^3.0.2" vite: "npm:^5.2.7" vite-bundle-visualizer: "npm:^1.1.0" vite-plugin-checker: "npm:^0.6.4" @@ -8106,14 +8106,14 @@ __metadata: languageName: node linkType: hard -"usehooks-ts@npm:^3.0.1": - version: 3.0.1 - resolution: "usehooks-ts@npm:3.0.1" +"usehooks-ts@npm:^3.0.2": + version: 3.0.2 + resolution: "usehooks-ts@npm:3.0.2" dependencies: lodash.debounce: "npm:^4.0.8" peerDependencies: react: ^16.8.0 || ^17 || ^18 - checksum: 10c0/c1673758100251c35a62d8d50aafe375fdce253eaa4374e7f4686bfc68505bd1b24dfe1c605db6d2ddfbe3a4ac1eca826c52ce454c9e58d8da96a45c351e0e05 + checksum: 10c0/8df3f65fa343838b0dfe359d7c12162180d62eac04efc611fbda43bc9703afaa1d70007c19eadcc9021671aaa2105e659c07f2acb707013e017c966e2a2aec82 languageName: node linkType: hard From 6d0442947b4322ec949bbb88e82b24720dce4143 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Tue, 2 Apr 2024 16:48:42 +0700 Subject: [PATCH 12/37] feat: Simple pool join & call to action UI (#2050) Co-authored-by: Ting A Lin --- src/canvas/JoinPool/Header.tsx | 119 ++++++ src/canvas/JoinPool/Nominations/index.tsx | 53 +++ .../JoinPool/Overview/AddressSection.tsx | 45 +++ src/canvas/JoinPool/Overview/Addresses.tsx | 27 ++ .../JoinPool/Overview/JoinForm.tsx} | 183 +++++---- .../JoinPool/Overview/PerformanceGraph.tsx | 180 +++++++++ src/canvas/JoinPool/Overview/Roles.tsx | 55 +++ src/canvas/JoinPool/Overview/Stats.tsx | 67 +++ src/canvas/JoinPool/Overview/index.tsx | 29 ++ src/canvas/JoinPool/Wrappers.ts | 382 ++++++++++++++++++ src/canvas/JoinPool/index.tsx | 107 +++++ src/canvas/JoinPool/types.ts | 31 ++ src/canvas/{Wrappers.tsx => Wrappers.ts} | 0 src/contexts/Api/index.tsx | 5 + src/contexts/Pools/ActivePool/index.tsx | 21 +- src/contexts/Pools/BondedPools/index.tsx | 5 +- .../ActivePoolsController/defaults.ts | 4 + src/controllers/SyncController/defaults.ts | 12 + src/controllers/SyncController/index.ts | 38 +- src/controllers/SyncController/types.ts | 1 + src/hooks/useActivePools/index.tsx | 15 +- src/hooks/useSyncing/index.tsx | 18 +- src/kits/Buttons/index.scss | 9 + src/kits/Structure/PageTitle/types.ts | 4 + src/kits/Structure/PageTitleTabs/Wrapper.ts | 4 + src/kits/Structure/PageTitleTabs/index.tsx | 12 +- src/kits/Structure/Tx/Signer.tsx | 33 ++ src/kits/Structure/Tx/Wrapper.ts | 74 ++-- src/kits/Structure/Tx/index.tsx | 51 +-- src/kits/Structure/Tx/types.ts | 18 + src/library/CallToAction/index.tsx | 187 +++++++++ src/library/Card/Wrappers.ts | 18 + src/library/Form/Bond/BondFeedback.tsx | 7 +- .../Form/ClaimPermissionInput/index.tsx | 74 +--- .../Form/ClaimPermissionInput/types.ts | 10 + src/library/Form/Unbond/UnbondFeedback.tsx | 7 +- src/library/Form/types.ts | 2 + src/library/Graphs/GeoDonut.tsx | 4 +- src/library/Graphs/PayoutBar.tsx | 1 + src/library/Headers/Sync.tsx | 2 +- src/library/ListItem/Labels/EraStatus.tsx | 2 +- src/library/ListItem/Labels/JoinPool.tsx | 10 +- src/library/ListItem/Wrappers.ts | 6 +- src/library/Loader/Announcement.tsx | 2 +- src/library/Loader/CallToAction.tsx | 15 + .../Loader/{Wrapper.ts => Wrappers.ts} | 0 src/library/Nominations/index.tsx | 2 +- src/library/PoolList/Default.tsx | 2 +- src/library/SideMenu/Main.tsx | 2 +- src/library/Stat/index.tsx | 6 +- src/library/Stat/types.ts | 1 + src/library/StatusLabel/index.tsx | 2 +- src/library/SubmitTx/ButtonSubmitLarge.tsx | 38 ++ src/library/SubmitTx/Default.tsx | 41 +- .../SubmitTx/ManualSign/Ledger/Submit.tsx | 11 +- .../SubmitTx/ManualSign/Ledger/index.tsx | 5 +- .../SubmitTx/ManualSign/Vault/index.tsx | 53 ++- src/library/SubmitTx/types.ts | 10 + .../ValidatorItem/Nomination.tsx | 16 +- src/library/ValidatorList/index.tsx | 2 +- src/library/WithdrawPrompt/index.tsx | 7 + src/locale/cn/library.json | 13 + src/locale/cn/pages.json | 4 +- src/locale/en/library.json | 18 +- src/locale/en/pages.json | 4 +- .../Forms/SetClaimPermission/index.tsx | 6 +- src/modals/PoolNominations/index.tsx | 2 +- src/model/Api/index.ts | 3 +- src/overlay/index.tsx | 4 +- src/pages/Nominate/Active/ManageBond.tsx | 2 +- .../Nominate/Active/Status/NewNominator.tsx | 61 +++ .../Active/Status/NominationStatus.tsx | 30 +- .../Active/Status/PayoutDestinationStatus.tsx | 2 +- .../Active/Status/UnclaimedPayoutsStatus.tsx | 3 +- src/pages/Nominate/Active/Status/index.tsx | 46 ++- src/pages/Nominate/Active/UnstakePrompts.tsx | 2 +- src/pages/Nominate/Active/index.tsx | 10 +- src/pages/Nominate/Active/types.ts | 4 + src/pages/Nominate/Setup/index.tsx | 2 + src/pages/Overview/Payouts.tsx | 2 +- src/pages/Payouts/index.tsx | 2 +- src/pages/Pools/Create/index.tsx | 2 + src/pages/Pools/Home/ClosurePrompts.tsx | 1 + .../Pools/Home/Status/MembershipStatus.tsx | 38 +- src/pages/Pools/Home/Status/NewMember.tsx | 96 +++++ src/pages/Pools/Home/Status/RewardsStatus.tsx | 65 +-- src/pages/Pools/Home/Status/index.tsx | 39 +- src/pages/Pools/Home/Status/types.ts | 15 + .../Pools/Home/Status/useStatusButtons.tsx | 37 +- src/pages/Pools/Home/index.tsx | 31 +- src/theme/theme.scss | 6 +- src/types.ts | 2 +- 92 files changed, 2236 insertions(+), 463 deletions(-) create mode 100644 src/canvas/JoinPool/Header.tsx create mode 100644 src/canvas/JoinPool/Nominations/index.tsx create mode 100644 src/canvas/JoinPool/Overview/AddressSection.tsx create mode 100644 src/canvas/JoinPool/Overview/Addresses.tsx rename src/{modals/JoinPool/index.tsx => canvas/JoinPool/Overview/JoinForm.tsx} (52%) create mode 100644 src/canvas/JoinPool/Overview/PerformanceGraph.tsx create mode 100644 src/canvas/JoinPool/Overview/Roles.tsx create mode 100644 src/canvas/JoinPool/Overview/Stats.tsx create mode 100644 src/canvas/JoinPool/Overview/index.tsx create mode 100644 src/canvas/JoinPool/Wrappers.ts create mode 100644 src/canvas/JoinPool/index.tsx create mode 100644 src/canvas/JoinPool/types.ts rename src/canvas/{Wrappers.tsx => Wrappers.ts} (100%) create mode 100644 src/controllers/ActivePoolsController/defaults.ts create mode 100644 src/controllers/SyncController/defaults.ts create mode 100644 src/kits/Structure/Tx/Signer.tsx create mode 100644 src/kits/Structure/Tx/types.ts create mode 100644 src/library/CallToAction/index.tsx create mode 100644 src/library/Form/ClaimPermissionInput/types.ts create mode 100644 src/library/Loader/CallToAction.tsx rename src/library/Loader/{Wrapper.ts => Wrappers.ts} (100%) create mode 100644 src/library/SubmitTx/ButtonSubmitLarge.tsx create mode 100644 src/pages/Nominate/Active/Status/NewNominator.tsx create mode 100644 src/pages/Pools/Home/Status/NewMember.tsx create mode 100644 src/pages/Pools/Home/Status/types.ts diff --git a/src/canvas/JoinPool/Header.tsx b/src/canvas/JoinPool/Header.tsx new file mode 100644 index 0000000000..0215f53d48 --- /dev/null +++ b/src/canvas/JoinPool/Header.tsx @@ -0,0 +1,119 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faArrowsRotate, + faHashtag, + faTimes, +} from '@fortawesome/free-solid-svg-icons'; +import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; +import { ButtonPrimaryInvert } from 'kits/Buttons/ButtonPrimaryInvert'; +import { TitleWrapper } from './Wrappers'; +import { Polkicon } from '@w3ux/react-polkicon'; +import { determinePoolDisplay, remToUnit } from '@w3ux/utils'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { PageTitleTabs } from 'kits/Structure/PageTitleTabs'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from 'kits/Overlay/Provider'; +import type { JoinPoolHeaderProps } from './types'; + +export const Header = ({ + activeTab, + bondedPool, + filteredBondedPools, + metadata, + autoSelected, + setActiveTab, + setSelectedPoolId, + setSelectedPoolCount, +}: JoinPoolHeaderProps) => { + const { t } = useTranslation(); + const { closeCanvas } = useOverlay().canvas; + + // Randomly select a new pool to display. + const handleChooseNewPool = () => { + // Trigger refresh of memoied selected bonded pool. + setSelectedPoolCount((prev: number) => prev + 1); + + // Randomly select a filtered bonded pool and set it as the selected pool. + const index = Math.ceil(Math.random() * filteredBondedPools.length - 1); + setSelectedPoolId(filteredBondedPools[index].id); + }; + + return ( + <> +
+ handleChooseNewPool()} + lg + /> + closeCanvas()} + iconLeft={faTimes} + style={{ marginLeft: '1.1rem' }} + /> +
+ +
+
+ +
+
+
+

+ {determinePoolDisplay( + bondedPool?.addresses.stash || '', + metadata + )} +

+
+
+

+ {t('pool', { ns: 'library' })}{' '} + + {bondedPool.id} + {['Blocked', 'Destroying'].includes(bondedPool.state) && ( + + {t(bondedPool.state.toLowerCase(), { ns: 'library' })} + + )} +

+ + {autoSelected && ( +

+ {t('autoSelected', { ns: 'library' })} +

+ )} +
+
+
+ + setActiveTab(0), + }, + { + title: t('nominate.nominations', { ns: 'pages' }), + active: activeTab === 1, + onClick: () => setActiveTab(1), + }, + ]} + tabClassName="canvas" + inline={true} + /> +
+ + ); +}; diff --git a/src/canvas/JoinPool/Nominations/index.tsx b/src/canvas/JoinPool/Nominations/index.tsx new file mode 100644 index 0000000000..276d67e96a --- /dev/null +++ b/src/canvas/JoinPool/Nominations/index.tsx @@ -0,0 +1,53 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ValidatorList } from 'library/ValidatorList'; +import { ListWrapper } from 'modals/PoolNominations/Wrappers'; +import { useTranslation } from 'react-i18next'; +import { HeadingWrapper, NominationsWrapper } from '../Wrappers'; +import type { NominationsProps } from '../types'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; + +export const Nominations = ({ stash, poolId }: NominationsProps) => { + const { t } = useTranslation(); + const { validators } = useValidators(); + const { poolsNominations } = useBondedPools(); + + // Extract validator entries from pool targets. + const targets = poolsNominations[poolId]?.targets || []; + const filteredTargets = validators.filter(({ address }) => + targets.includes(address) + ); + + return ( + + +

+ {targets.length}{' '} + {!targets.length + ? t('nominate.noNominationsSet', { ns: 'pages' }) + : ``}{' '} + {t('nominations', { ns: 'library', count: targets.length })} +

+
+ + {targets.length > 0 ? ( + + ) : ( +

{t('poolIsNotNominating', { ns: 'modals' })}

+ )} +
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/AddressSection.tsx b/src/canvas/JoinPool/Overview/AddressSection.tsx new file mode 100644 index 0000000000..ac569d23b1 --- /dev/null +++ b/src/canvas/JoinPool/Overview/AddressSection.tsx @@ -0,0 +1,45 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useHelp } from 'contexts/Help'; +import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { HeadingWrapper } from '../Wrappers'; +import { Polkicon } from '@w3ux/react-polkicon'; +import { CopyAddress } from 'library/ListItem/Labels/CopyAddress'; +import { ellipsisFn, remToUnit } from '@w3ux/utils'; +import type { AddressSectionProps } from '../types'; + +export const AddressSection = ({ + address, + label, + helpKey, +}: AddressSectionProps) => { + const { openHelp } = useHelp(); + + return ( +
+ +

+ {label} + {!!helpKey && ( + openHelp(helpKey)} /> + )} +

+
+ +
+ + + +

+ {ellipsisFn(address, 6)} + +

+
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/Addresses.tsx b/src/canvas/JoinPool/Overview/Addresses.tsx new file mode 100644 index 0000000000..8a8cc93a27 --- /dev/null +++ b/src/canvas/JoinPool/Overview/Addresses.tsx @@ -0,0 +1,27 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CardWrapper } from 'library/Card/Wrappers'; +import { AddressesWrapper, HeadingWrapper } from '../Wrappers'; +import { AddressSection } from './AddressSection'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +export const Addresses = ({ + bondedPool: { addresses }, +}: OverviewSectionProps) => { + const { t } = useTranslation('library'); + + return ( + + +

{t('addresses')}

+
+ + + + + +
+ ); +}; diff --git a/src/modals/JoinPool/index.tsx b/src/canvas/JoinPool/Overview/JoinForm.tsx similarity index 52% rename from src/modals/JoinPool/index.tsx rename to src/canvas/JoinPool/Overview/JoinForm.tsx index c53268a453..82fdb79f85 100644 --- a/src/modals/JoinPool/index.tsx +++ b/src/canvas/JoinPool/Overview/JoinForm.tsx @@ -2,99 +2,88 @@ // SPDX-License-Identifier: GPL-3.0-only import { planckToUnit, unitToPlanck } from '@w3ux/utils'; -import BigNumber from 'bignumber.js'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useApi } from 'contexts/Api'; -import { usePoolMembers } from 'contexts/Pools/PoolMembers'; -import { useSetup } from 'contexts/Setup'; -import { defaultPoolProgress } from 'contexts/Setup/defaults'; +import type BigNumber from 'bignumber.js'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useNetwork } from 'contexts/Network'; +import type { ClaimPermission } from 'contexts/Pools/types'; import { useTransferOptions } from 'contexts/TransferOptions'; -import { useTxMeta } from 'contexts/TxMeta'; -import { BondFeedback } from 'library/Form/Bond/BondFeedback'; +import { useState } from 'react'; +import { JoinFormWrapper } from '../Wrappers'; import { ClaimPermissionInput } from 'library/Form/ClaimPermissionInput'; -import { useBatchCall } from 'hooks/useBatchCall'; +import { BondFeedback } from 'library/Form/Bond/BondFeedback'; import { useBondGreatestFee } from 'hooks/useBondGreatestFee'; -import { useSignerWarnings } from 'hooks/useSignerWarnings'; +import { useApi } from 'contexts/Api'; +import { useBatchCall } from 'hooks/useBatchCall'; import { useSubmitExtrinsic } from 'hooks/useSubmitExtrinsic'; -import { Close } from 'library/Modal/Close'; -import { SubmitTx } from 'library/SubmitTx'; import { useOverlay } from 'kits/Overlay/Provider'; -import { useNetwork } from 'contexts/Network'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import type { ClaimPermission } from 'contexts/Pools/types'; -import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; +import { useSetup } from 'contexts/Setup'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import { defaultPoolProgress } from 'contexts/Setup/defaults'; +import { useSignerWarnings } from 'hooks/useSignerWarnings'; +import { SubmitTx } from 'library/SubmitTx'; +import type { OverviewSectionProps } from '../types'; +import { defaultClaimPermission } from 'controllers/ActivePoolsController/defaults'; +import { useTranslation } from 'react-i18next'; -export const JoinPool = () => { - const { t } = useTranslation('modals'); +export const JoinForm = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation(); const { api } = useApi(); const { - networkData: { units }, + networkData: { units, unit }, } = useNetwork(); - const { activeAccount } = useActiveAccounts(); + const { + closeCanvas, + config: { options }, + } = useOverlay().canvas; const { newBatchCall } = useBatchCall(); const { setActiveAccountSetup } = useSetup(); - const { txFees, notEnoughFunds } = useTxMeta(); + const { activeAccount } = useActiveAccounts(); const { getSignerWarnings } = useSignerWarnings(); const { getTransferOptions } = useTransferOptions(); + const largestTxFee = useBondGreatestFee({ bondFor: 'pool' }); const { queryPoolMember, addToPoolMembers } = usePoolMembers(); - const { - setModalStatus, - config: { options }, - setModalResize, - } = useOverlay().modal; - - const { id: poolId, setActiveTab } = options; const { pool: { totalPossibleBond }, - transferrableBalance, } = getTransferOptions(activeAccount); - const largestTxFee = useBondGreatestFee({ bondFor: 'pool' }); - - // if we are bonding, subtract tx fees from bond amount - const freeBondAmount = BigNumber.max(transferrableBalance.minus(txFees), 0); + // Pool claim permission value. + const [claimPermission, setClaimPermission] = useState( + defaultClaimPermission + ); - // local bond value + // Bond amount to join pool with. const [bond, setBond] = useState<{ bond: string }>({ bond: planckToUnit(totalPossibleBond, units).toString(), }); - // handler to set bond as a string - const handleSetBond = (newBond: { bond: BigNumber }) => { - setBond({ bond: newBond.bond.toString() }); - }; - - // Updated claim permission value - const [claimPermission, setClaimPermission] = useState< - ClaimPermission | undefined - >('Permissioned'); - - // bond valid + // Whether the bond amount is valid. const [bondValid, setBondValid] = useState(false); // feedback errors to trigger modal resize const [feedbackErrors, setFeedbackErrors] = useState([]); - // modal resize on form update - useEffect( - () => setModalResize(), - [bond, notEnoughFunds, feedbackErrors.length] - ); + // Handler to set bond on input change. + const handleSetBond = (value: { bond: BigNumber }) => { + setBond({ bond: value.bond.toString() }); + }; - // tx to submit + // Whether the form is ready to submit. + const formValid = bondValid && feedbackErrors.length === 0; + + // Get transaction for submission. const getTx = () => { const tx = null; - if (!api) { + if (!api || !claimPermission || !formValid) { return tx; } const bondToSubmit = unitToPlanck(!bondValid ? '0' : bond.bond, units); const bondAsString = bondToSubmit.isNaN() ? '0' : bondToSubmit.toString(); - const txs = [api.tx.nominationPools.join(bondAsString, poolId)]; + const txs = [api.tx.nominationPools.join(bondAsString, bondedPool.id)]; - if (![undefined, 'Permissioned'].includes(claimPermission)) { + // If claim permission is not the default, add it to tx. + if (claimPermission !== defaultClaimPermission) { txs.push(api.tx.nominationPools.setClaimPermission(claimPermission)); } @@ -110,17 +99,23 @@ export const JoinPool = () => { from: activeAccount, shouldSubmit: bondValid, callbackSubmit: () => { - setModalStatus('closing'); - setActiveTab(0); + closeCanvas(); + + // Optional callback function on join success. + const onJoinCallback = options?.onJoinCallback; + + if (typeof onJoinCallback === 'function') { + onJoinCallback(); + } }, callbackInBlock: async () => { - // query and add account to poolMembers list + // Query and add account to poolMembers list const member = await queryPoolMember(activeAccount); if (member) { addToPoolMembers(member); } - // reset localStorage setup progress + // Reset local storage setup progress setActiveAccountSetup('pool', defaultPoolProgress); }, }); @@ -132,33 +127,49 @@ export const JoinPool = () => { ); return ( - <> - - -

{t('joinPool')}

- { - setBondValid(valid); - setFeedbackErrors(errors); - }} - defaultBond={null} - setters={[handleSetBond]} - parentErrors={warnings} - txFees={largestTxFee} - /> - { - setClaimPermission(val); - }} - disabled={freeBondAmount.isZero()} + +

{t('pools.joinPool', { ns: 'pages' })}

+

+ {t('bond', { ns: 'library' })} {unit} +

+ +
+
+ { + setBondValid(valid); + setFeedbackErrors(errors); + }} + defaultBond={null} + setters={[handleSetBond]} + parentErrors={warnings} + txFees={largestTxFee} + /> +
+
+ +

{t('claimSetting', { ns: 'library' })}

+ + { + setClaimPermission(val); + }} + /> + +
+ - - - +
+
); }; diff --git a/src/canvas/JoinPool/Overview/PerformanceGraph.tsx b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx new file mode 100644 index 0000000000..8e551ed3d0 --- /dev/null +++ b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx @@ -0,0 +1,180 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + BarElement, + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, +} from 'chart.js'; +import { useNetwork } from 'contexts/Network'; +import { GraphWrapper, HeadingWrapper } from '../Wrappers'; +import { Bar } from 'react-chartjs-2'; +import BigNumber from 'bignumber.js'; +import type { AnyJson } from 'types'; +import { graphColors } from 'theme/graphs'; +import { useTheme } from 'contexts/Themes'; +import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { useHelp } from 'contexts/Help'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import { useRef } from 'react'; +import { formatSize } from 'library/Graphs/Utils'; +import { useSize } from 'hooks/useSize'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Title, + Tooltip, + Legend +); + +export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation(); + const { mode } = useTheme(); + const { openHelp } = useHelp(); + const { colors } = useNetwork().networkData; + const { poolRewardPoints } = usePoolPerformance(); + const rawEraRewardPoints = poolRewardPoints[bondedPool.addresses.stash] || {}; + + // Ref to the graph container. + const graphInnerRef = useRef(null); + + // Get the size of the graph container. + const size = useSize(graphInnerRef?.current || undefined); + const { width, height } = formatSize(size, 150); + + // Format reward points as an array of strings. + const dataset = Object.values( + Object.fromEntries( + Object.entries(rawEraRewardPoints).map(([k, v]: AnyJson) => [ + k, + new BigNumber(v).toString(), + ]) + ) + ); + + // Format labels, only displaying the first and last era. + const labels = Object.keys(rawEraRewardPoints).map(() => ''); + + const firstEra = Object.keys(rawEraRewardPoints)[0]; + labels[0] = firstEra + ? `${t('era', { ns: 'library' })} ${Object.keys(rawEraRewardPoints)[0]}` + : ''; + + const lastEra = Object.keys(rawEraRewardPoints)[labels.length - 1]; + labels[labels.length - 1] = lastEra + ? `${t('era', { ns: 'library' })} ${Object.keys(rawEraRewardPoints)[labels.length - 1]}` + : ''; + + // Use primary color for bars. + const color = colors.primary[mode]; + + const options = { + responsive: true, + maintainAspectRatio: false, + barPercentage: 0.3, + maxBarThickness: 13, + scales: { + x: { + stacked: true, + grid: { + display: false, + }, + ticks: { + font: { + size: 10, + }, + autoSkip: true, + }, + }, + y: { + stacked: true, + ticks: { + font: { + size: 10, + }, + }, + border: { + display: false, + }, + grid: { + color: graphColors.grid[mode], + }, + }, + }, + plugins: { + legend: { + display: false, + }, + title: { + display: false, + }, + tooltip: { + displayColors: false, + backgroundColor: graphColors.tooltip[mode], + titleColor: graphColors.label[mode], + bodyColor: graphColors.label[mode], + bodyFont: { + weight: 600, + }, + callbacks: { + title: () => [], + label: (context: AnyJson) => + `${new BigNumber(context.parsed.y).decimalPlaces(0).toFormat()} ${t('eraPoints', { ns: 'library' })}`, + }, + }, + }, + }; + + const data = { + labels, + datasets: [ + { + label: t('era', { ns: 'library' }), + data: dataset, + borderColor: color, + backgroundColor: color, + pointRadius: 0, + borderRadius: 3, + }, + ], + }; + + return ( +
+ +

+ {t('recentPerformance', { ns: 'library' })} + openHelp('Era Points')} + /> +

+
+ + +
+ +
+
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/Roles.tsx b/src/canvas/JoinPool/Overview/Roles.tsx new file mode 100644 index 0000000000..cc3f2a89a6 --- /dev/null +++ b/src/canvas/JoinPool/Overview/Roles.tsx @@ -0,0 +1,55 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CardWrapper } from 'library/Card/Wrappers'; +import { AddressesWrapper, HeadingWrapper } from '../Wrappers'; +import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { useHelp } from 'contexts/Help'; +import { AddressSection } from './AddressSection'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +export const Roles = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation('pages'); + const { openHelp } = useHelp(); + + return ( +
+ + +

+ {t('pools.roles')} + openHelp('Pool Roles')} /> +

+
+ + + {bondedPool.roles.root && ( + + )} + {bondedPool.roles.nominator && ( + + )} + {bondedPool.roles.bouncer && ( + + )} + {bondedPool.roles.depositor && ( + + )} + +
+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/Stats.tsx b/src/canvas/JoinPool/Overview/Stats.tsx new file mode 100644 index 0000000000..5281c2debd --- /dev/null +++ b/src/canvas/JoinPool/Overview/Stats.tsx @@ -0,0 +1,67 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useNetwork } from 'contexts/Network'; +import { HeadingWrapper } from '../Wrappers'; +import { planckToUnit, rmCommas } from '@w3ux/utils'; +import { useApi } from 'contexts/Api'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import type { OverviewSectionProps } from '../types'; +import { useTranslation } from 'react-i18next'; + +export const Stats = ({ bondedPool }: OverviewSectionProps) => { + const { t } = useTranslation('library'); + const { + networkData: { + units, + unit, + brand: { token: Token }, + }, + } = useNetwork(); + const { isReady, api } = useApi(); + + // Store the pool balance. + const [poolBalance, setPoolBalance] = useState(null); + + // Fetches the balance of the bonded pool. + const getPoolBalance = async () => { + if (!api) { + return; + } + + const balance = ( + await api.call.nominationPoolsApi.pointsToBalance( + bondedPool.id, + rmCommas(bondedPool.points) + ) + ).toString(); + + if (balance) { + setPoolBalance(new BigNumber(balance)); + } + }; + + // Fetch the balance when pool or points change. + useEffect(() => { + if (isReady) { + getPoolBalance(); + } + }, [bondedPool.id, bondedPool.points, isReady]); + + return ( + +

+ {t('activelyNominating')} + + + + {!poolBalance + ? `...` + : planckToUnit(poolBalance, units).decimalPlaces(3).toFormat()}{' '} + {unit} {t('bonded')} + +

+
+ ); +}; diff --git a/src/canvas/JoinPool/Overview/index.tsx b/src/canvas/JoinPool/Overview/index.tsx new file mode 100644 index 0000000000..107a5b7ed9 --- /dev/null +++ b/src/canvas/JoinPool/Overview/index.tsx @@ -0,0 +1,29 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { JoinForm } from './JoinForm'; + +import { PerformanceGraph } from './PerformanceGraph'; +import { Stats } from './Stats'; +import { Addresses } from './Addresses'; +import { Roles } from './Roles'; +import { GraphLayoutWrapper } from '../Wrappers'; +import type { OverviewSectionProps } from '../types'; + +export const Overview = (props: OverviewSectionProps) => ( + <> +
+ + + + + + +
+
+
+ +
+
+ +); diff --git a/src/canvas/JoinPool/Wrappers.ts b/src/canvas/JoinPool/Wrappers.ts new file mode 100644 index 0000000000..d1d76432d3 --- /dev/null +++ b/src/canvas/JoinPool/Wrappers.ts @@ -0,0 +1,382 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const JoinPoolInterfaceWrapper = styled.div` + display: flex; + flex-direction: column; + width: 100%; + + > .header { + display: flex; + margin-bottom: 2rem; + } + + > .content { + display: flex; + flex-grow: 1; + + @media (max-width: 1000px) { + flex-flow: row wrap; + } + + > div { + display: flex; + + &.main { + flex-grow: 1; + display: flex; + flex-direction: column; + padding-right: 4rem; + + @media (max-width: 1000px) { + flex-basis: 100%; + padding-right: 0; + } + } + + &.side { + min-width: 450px; + + @media (max-width: 1000px) { + flex-grow: 1; + flex-basis: 100%; + margin-top: 0.5rem; + } + + > div { + width: 100%; + } + } + } + } +`; + +export const TitleWrapper = styled.div` + border-bottom: 1px solid var(--border-secondary-color); + flex: 1; + display: flex; + flex-direction: column; + margin: 2rem 0 1.55rem 0; + padding-bottom: 0.1rem; + + > .inner { + display: flex; + align-items: center; + margin-bottom: 0.5rem; + flex: 1; + + > div { + display: flex; + flex: 1; + + &:nth-child(1) { + max-width: 4rem; + } + + &:nth-child(2) { + padding-left: 1rem; + flex-direction: column; + + > .title { + position: relative; + padding-top: 2rem; + flex: 1; + + h1 { + position: absolute; + top: 0; + left: 0; + margin: 0; + line-height: 2.2rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + } + } + + > .labels { + display: flex; + margin-top: 1.1rem; + + > h3 { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + margin: 0; + + > svg { + margin: 0 0 0 0.2rem; + } + + > span { + border: 1px solid var(--border-secondary-color); + border-radius: 0.5rem; + padding: 0.4rem 0.6rem; + margin-left: 1rem; + font-size: 1.1rem; + + &.blocked { + color: var(--status-warning-color); + border-color: var(--status-warning-color); + } + + &.destroying { + color: var(--status-danger-color); + border-color: var(--status-danger-color); + } + } + } + } + } + } + } +`; + +export const JoinFormWrapper = styled.div` + background: var(--background-canvas-card); + border: 0.75px solid var(--border-primary-color); + box-shadow: var(--card-shadow); + border-radius: 1.5rem; + padding: 1.5rem; + width: 100%; + + @media (max-width: 1000px) { + margin-top: 1rem; + } + + h4 { + display: flex; + align-items: center; + &.note { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + } + } + + > h2 { + color: var(--text-color-secondary); + margin: 0.25rem 0; + } + + > h4 { + margin: 1.5rem 0 0.5rem 0; + color: var(--text-color-tertiary); + + &.underline { + border-bottom: 1px solid var(--border-primary-color); + padding-bottom: 0.5rem; + margin: 2rem 0 1rem 0; + } + } + + > .input { + border-bottom: 1px solid var(--border-primary-color); + padding: 0 0.25rem; + display: flex; + align-items: flex-end; + padding-bottom: 1.25rem; + + > div { + flex-grow: 1; + display: flex; + flex-direction: column; + + > div { + margin: 0; + } + } + } + + > .available { + margin-top: 0.5rem; + margin-bottom: 1.5rem; + display: flex; + } + + > .submit { + margin-top: 2.5rem; + } +`; + +export const HeadingWrapper = styled.div` + margin: 0.5rem 0.5rem 0.5rem 0rem; + + @media (max-width: 600px) { + margin-right: 0; + } + + h3, + p { + padding: 0 0.5rem; + } + + h4 { + font-size: 1.15rem; + } + + p { + color: var(--text-color-tertiary); + margin: 0.35rem 0 0 0; + } + + > h3, + h4 { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + margin: 0; + display: flex; + align-items: center; + + @media (max-width: 600px) { + flex-wrap: wrap; + } + + > span { + background-color: var(--background-canvas-card-secondary); + color: var(--text-color-secondary); + font-family: InterBold, sans-serif; + border-radius: 1.5rem; + padding: 0rem 1.25rem; + margin-right: 1rem; + height: 2.6rem; + display: flex; + align-items: center; + + @media (max-width: 600px) { + flex-grow: 1; + justify-content: center; + margin-bottom: 1rem; + margin-right: 0; + height: 2.9rem; + width: 100%; + + &:last-child { + margin-bottom: 0; + } + } + + &.balance { + padding-left: 0.5rem; + } + + > .icon { + width: 2.1rem; + height: 2.1rem; + margin-right: 0.3rem; + } + &.inactive { + color: var(--text-color-tertiary); + border: 1px solid var(--border-secondary-color); + } + } + } +`; + +export const AddressesWrapper = styled.div` + flex: 1; + display: flex; + padding: 0rem 0.25rem; + flex-wrap: wrap; + + > section { + display: flex; + flex-direction: column; + flex-basis: 50%; + margin: 0.9rem 0 0.7rem 0; + + @media (max-width: 600px) { + flex-basis: 100%; + } + + > div { + display: flex; + flex-direction: row; + align-items: center; + + > span { + margin-right: 0.75rem; + } + + > h4 { + color: var(--text-color-secondary); + font-family: InterSemiBold, sans-serif; + display: flex; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + margin: 0; + flex: 1; + + &.heading { + font-family: InterBold, sans-serif; + } + + > .label { + margin-left: 0.75rem; + + > button { + color: var(--text-color-tertiary); + } + } + } + } + } +`; + +// Wrapper that houses the chart, allowing it to be responsive. +export const GraphWrapper = styled.div` + flex: 1; + position: relative; + padding: 0 4rem 0 1rem; + margin-top: 2rem; + + @media (max-width: 1000px) { + padding: 0 0 0 1rem; + } + + > .inner { + position: absolute; + width: 100%; + height: 100%; + padding-left: 1rem; + padding-right: 4rem; + + @media (max-width: 1000px) { + padding-right: 1.5rem; + } + } +`; + +// Element used to wrap graph and pool stats, allowing flex ordering on smaller screens. +export const GraphLayoutWrapper = styled.div` + flex: 1; + display: flex; + flex-direction: column; + + @media (min-width: 1001px) { + > div:last-child { + margin-top: 1.25rem; + } + } + + @media (max-width: 1000px) { + > div { + &:first-child { + order: 2; + margin-top: 1.5rem; + margin-bottom: 0; + } + &:last-child { + order: 1; + } + } + } +`; + +export const NominationsWrapper = styled.div` + flex: 1; + display: flex; + flex-direction: column; +`; diff --git a/src/canvas/JoinPool/index.tsx b/src/canvas/JoinPool/index.tsx new file mode 100644 index 0000000000..7f31fbb023 --- /dev/null +++ b/src/canvas/JoinPool/index.tsx @@ -0,0 +1,107 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { CanvasFullScreenWrapper } from 'canvas/Wrappers'; +import { useOverlay } from 'kits/Overlay/Provider'; +import { JoinPoolInterfaceWrapper } from './Wrappers'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { useMemo, useState } from 'react'; +import { Header } from './Header'; +import { Overview } from './Overview'; +import { Nominations } from './Nominations'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import { MaxEraRewardPointsEras } from 'consts'; +import { useStaking } from 'contexts/Staking'; + +export const JoinPool = () => { + const { + closeCanvas, + config: { options }, + } = useOverlay().canvas; + const { eraStakers } = useStaking(); + const { poolRewardPoints } = usePoolPerformance(); + const { poolsMetaData, bondedPools } = useBondedPools(); + + // The active canvas tab. + const [activeTab, setActiveTab] = useState(0); + + // Trigger re-render when chosen selected pool is incremented. + const [selectedPoolCount, setSelectedPoolCount] = useState(0); + + // Filter bonded pools to only those that are open and that have active daily rewards for the last + // `MaxEraRewardPointsEras` eras. The second filter checks if the pool is in `eraStakers` for the + // active era. + const filteredBondedPools = useMemo( + () => + bondedPools + .filter((pool) => { + // Fetch reward point data for the pool. + const rawEraRewardPoints = + poolRewardPoints[pool.addresses.stash] || {}; + const rewardPoints = Object.values(rawEraRewardPoints); + + // Ensure pool has been active for every era in performance data. + const activeDaily = + rewardPoints.every((points) => Number(points) > 0) && + rewardPoints.length === MaxEraRewardPointsEras; + + return pool.state === 'Open' && activeDaily; + }) + // Ensure the pool is currently in the active set of backers. + .filter((pool) => + eraStakers.stakers.find((staker) => + staker.others.find(({ who }) => who !== pool.addresses.stash) + ) + ), + [bondedPools, poolRewardPoints] + ); + + // The bonded pool to display. Use the provided `poolId`, or assign a random eligible filtered + // pool otherwise. Re-fetches when the selected pool count is incremented. + const bondedPool = useMemo( + () => + options?.poolId + ? bondedPools.find(({ id }) => id === options.poolId) + : filteredBondedPools[ + (filteredBondedPools.length * Math.random()) << 0 + ], + [selectedPoolCount] + ); + + // The selected bonded pool id. + const [selectedPoolId, setSelectedPoolId] = useState( + bondedPool?.id || 0 + ); + + if (!bondedPool) { + closeCanvas(); + return null; + } + + return ( + +
+ + +
+ {activeTab === 0 && } + {activeTab === 1 && ( + + )} +
+
+ + ); +}; diff --git a/src/canvas/JoinPool/types.ts b/src/canvas/JoinPool/types.ts new file mode 100644 index 0000000000..b7d2dcedac --- /dev/null +++ b/src/canvas/JoinPool/types.ts @@ -0,0 +1,31 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { BondedPool } from 'contexts/Pools/BondedPools/types'; +import type { Dispatch, SetStateAction } from 'react'; + +export interface JoinPoolHeaderProps { + activeTab: number; + bondedPool: BondedPool; + filteredBondedPools: BondedPool[]; + metadata: string; + autoSelected: boolean; + setActiveTab: (tab: number) => void; + setSelectedPoolId: Dispatch>; + setSelectedPoolCount: Dispatch>; +} + +export interface NominationsProps { + stash: string; + poolId: number; +} + +export interface AddressSectionProps { + address: string; + label: string; + helpKey?: string; +} + +export interface OverviewSectionProps { + bondedPool: BondedPool; +} diff --git a/src/canvas/Wrappers.tsx b/src/canvas/Wrappers.ts similarity index 100% rename from src/canvas/Wrappers.tsx rename to src/canvas/Wrappers.ts diff --git a/src/contexts/Api/index.tsx b/src/contexts/Api/index.tsx index 46b1bea262..031bd3149e 100644 --- a/src/contexts/Api/index.tsx +++ b/src/contexts/Api/index.tsx @@ -331,6 +331,11 @@ export const APIProvider = ({ children, network }: APIProviderProps) => { const reInitialiseApi = async (type: ConnectionType) => { setApiStatus('disconnected'); + + // Dispatch all default syncIds as syncing. + SyncController.dispatchAllDefault(); + + // Instanaite new API instance. await ApiController.instantiate(network, type, rpcEndpoint); }; diff --git a/src/contexts/Pools/ActivePool/index.tsx b/src/contexts/Pools/ActivePool/index.tsx index b329d86608..b8904850af 100644 --- a/src/contexts/Pools/ActivePool/index.tsx +++ b/src/contexts/Pools/ActivePool/index.tsx @@ -76,8 +76,9 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { // Only listen to the currently selected active pool, otherwise return an empty array. const poolIds = activePoolIdRef.current ? [activePoolIdRef.current] : []; - // Listen for active pools. - const { activePools, poolNominations } = useActivePools({ + // Listen for active pools. NOTE: `activePoolsRef` is needed to check if the pool has changed + // after the async call of fetching pending rewards. + const { activePools, activePoolsRef, poolNominations } = useActivePools({ poolIds, onCallback: async () => { // Sync: active pools synced once all account pools have been reported. @@ -116,6 +117,9 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { addresses: { ...createPoolAccounts(Number(pool)) }, })); ActivePoolsController.syncPools(api, newActivePools); + } else { + // No active pools to sync. Mark as complete. + SyncController.dispatch('active-pools', 'complete'); } }; @@ -197,9 +201,20 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { if ( activePool && membership?.poolId && + membership?.address && String(activePool.id) === String(membership.poolId) ) { - setPendingPoolRewards(await fetchPendingRewards(membership?.address)); + const pendingRewards = await fetchPendingRewards(membership.address); + + // Check if active pool has changed in the time the pending rewards were being fetched. If it + // has, do not update. + if ( + activePoolId && + activePoolsRef.current[activePoolId]?.id === + Number(membership.poolId || -1) + ) { + setPendingPoolRewards(pendingRewards); + } } else { setPendingPoolRewards(new BigNumber(0)); } diff --git a/src/contexts/Pools/BondedPools/index.tsx b/src/contexts/Pools/BondedPools/index.tsx index d3d22677f8..c070f79a8f 100644 --- a/src/contexts/Pools/BondedPools/index.tsx +++ b/src/contexts/Pools/BondedPools/index.tsx @@ -20,6 +20,7 @@ import { useNetwork } from 'contexts/Network'; import { useApi } from '../../Api'; import { defaultBondedPoolsContext } from './defaults'; import { useCreatePoolAccounts } from 'hooks/useCreatePoolAccounts'; +import { SyncController } from 'controllers/SyncController'; export const BondedPoolsContext = createContext( defaultBondedPoolsContext @@ -85,6 +86,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { ); bondedPoolsSynced.current = 'synced'; + SyncController.dispatch('bonded-pools', 'complete'); }; // Fetches pool nominations and updates state. @@ -197,7 +199,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { }); const getBondedPool = (poolId: MaybePool) => - bondedPools.find((p) => p.id === poolId) ?? null; + bondedPools.find((p) => String(p.id) === String(poolId)) ?? null; /* * poolSearchFilter Iterates through the supplied list and refers to the meta batch of the list to @@ -386,6 +388,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { // Clear existing state for network refresh. useEffectIgnoreInitial(() => { bondedPoolsSynced.current = 'unsynced'; + SyncController.dispatch('bonded-pools', 'syncing'); setStateWithRef([], setBondedPools, bondedPoolsRef); setPoolsMetadata({}); setPoolsNominations({}); diff --git a/src/controllers/ActivePoolsController/defaults.ts b/src/controllers/ActivePoolsController/defaults.ts new file mode 100644 index 0000000000..5280165f72 --- /dev/null +++ b/src/controllers/ActivePoolsController/defaults.ts @@ -0,0 +1,4 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export const defaultClaimPermission = 'PermissionlessWithdraw'; diff --git a/src/controllers/SyncController/defaults.ts b/src/controllers/SyncController/defaults.ts new file mode 100644 index 0000000000..e04d5254b5 --- /dev/null +++ b/src/controllers/SyncController/defaults.ts @@ -0,0 +1,12 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { SyncID } from './types'; + +export const defaultSyncIds: SyncID[] = [ + 'initialization', + 'balances', + 'era-stakers', + 'bonded-pools', + 'active-pools', +]; diff --git a/src/controllers/SyncController/index.ts b/src/controllers/SyncController/index.ts index 67baa82887..7ad062c693 100644 --- a/src/controllers/SyncController/index.ts +++ b/src/controllers/SyncController/index.ts @@ -1,18 +1,27 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only +import { defaultSyncIds } from './defaults'; import type { SyncEvent, SyncID, SyncIDConfig, SyncStatus } from './types'; export class SyncController { // ------------------------------------------------------ // Class members // ------------------------------------------------------ - static syncIds: SyncID[] = []; + + // List of all syncIds currently syncing. NOTE: `initialization` is added by default as the + // network always initializes from initial state. + static syncIds: SyncID[] = defaultSyncIds; // ------------------------------------------------------ // Dispatch sync events // ------------------------------------------------------ + // Dispatch all default syncId events as syncing. + static dispatchAllDefault = () => { + this.syncIds.forEach((id) => this.dispatch(id, 'syncing')); + }; + // Dispatches a new sync event to the document. static dispatch = (id: SyncID, status: SyncStatus) => { const detail: SyncEvent = { @@ -20,20 +29,31 @@ export class SyncController { status, }; + // Whether to dispatch the event. + let dispatch = true; + // Keep class syncIds up to date. - if (status === 'syncing' && !this.syncIds.includes(id)) { - this.syncIds.push(id); + if (status === 'syncing') { + if (this.syncIds.includes(id)) { + // Cancel event if already syncing. + dispatch = false; + } else { + this.syncIds.push(id); + } } - if (status === 'complete' && this.syncIds.includes(id)) { + + if (status === 'complete') { this.syncIds = this.syncIds.filter((syncId) => syncId !== id); } // Dispatch event to UI. - document.dispatchEvent( - new CustomEvent('new-sync-status', { - detail, - }) - ); + if (dispatch) { + document.dispatchEvent( + new CustomEvent('new-sync-status', { + detail, + }) + ); + } }; // Checks if event detailis a valid `new-sync-status` event. diff --git a/src/controllers/SyncController/types.ts b/src/controllers/SyncController/types.ts index ced7533cba..75c3f97b81 100644 --- a/src/controllers/SyncController/types.ts +++ b/src/controllers/SyncController/types.ts @@ -5,6 +5,7 @@ export type SyncID = | 'initialization' | 'balances' | 'era-stakers' + | 'bonded-pools' | 'active-pools'; export interface SyncEvent { diff --git a/src/hooks/useActivePools/index.tsx b/src/hooks/useActivePools/index.tsx index f658c9da59..02a80374ce 100644 --- a/src/hooks/useActivePools/index.tsx +++ b/src/hooks/useActivePools/index.tsx @@ -33,11 +33,6 @@ export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { const { pool, nominations } = e.detail; const { id } = pool; - // Call custom `onCallback` function if provided. - if (typeof onCallback === 'function') { - await onCallback(e.detail); - } - // Persist to active pools state if this pool is specified in `poolIds`. if ( poolIds === '*' || @@ -55,11 +50,14 @@ export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { poolNominationsRef ); } + + // Call custom `onCallback` function if provided. + if (typeof onCallback === 'function') { + await onCallback(e.detail); + } } }; - const documentRef = useRef(document); - // Bootstrap state on initial render. useEffect(() => { const initialActivePools = @@ -94,7 +92,8 @@ export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { }, [network, activeAccount]); // Listen for new active pool events. + const documentRef = useRef(document); useEventListener('new-active-pool', newActivePoolCallback, documentRef); - return { activePools, poolNominations }; + return { activePools, activePoolsRef, poolNominations }; }; diff --git a/src/hooks/useSyncing/index.tsx b/src/hooks/useSyncing/index.tsx index b1a6c843a2..f1a19b1fae 100644 --- a/src/hooks/useSyncing/index.tsx +++ b/src/hooks/useSyncing/index.tsx @@ -8,12 +8,12 @@ import type { SyncID, SyncIDConfig } from 'controllers/SyncController/types'; import { isCustomEvent } from 'controllers/utils'; import { useEventListener } from 'usehooks-ts'; -export const useSyncing = (config: SyncIDConfig) => { +export const useSyncing = (config: SyncIDConfig = '*') => { // Retrieve the ids from the config provided. const ids = SyncController.getIdsFromSyncConfig(config); // Keep a record of active sync statuses. - const [syncIds, setSyncIds] = useState([]); + const [syncIds, setSyncIds] = useState(SyncController.syncIds); const syncIdsRef = useRef(syncIds); // Handle new syncing status events. @@ -40,7 +40,16 @@ export const useSyncing = (config: SyncIDConfig) => { } }; - const documentRef = useRef(document); + // Helper to determine if pool membership is syncing. + const poolMembersipSyncing = (): boolean => { + const POOL_SYNC_IDS: SyncID[] = [ + 'initialization', + 'balances', + 'bonded-pools', + 'active-pools', + ]; + return syncIds.some(() => POOL_SYNC_IDS.find((id) => syncIds.includes(id))); + }; // Bootstrap existing sync statuses of interest when hook is mounted. useEffect(() => { @@ -54,7 +63,8 @@ export const useSyncing = (config: SyncIDConfig) => { }, []); // Listen for new sync events. + const documentRef = useRef(document); useEventListener('new-sync-status', newSyncStatusCallback, documentRef); - return { syncing: syncIds.length > 0 }; + return { syncing: syncIds.length > 0, poolMembersipSyncing }; }; diff --git a/src/kits/Buttons/index.scss b/src/kits/Buttons/index.scss index 2ecc25d1e1..f80d20a92e 100644 --- a/src/kits/Buttons/index.scss +++ b/src/kits/Buttons/index.scss @@ -440,6 +440,15 @@ border: 1px solid var(--border-secondary-color); } } + + &.canvas { + color: var(--text-color-tertiary); + + &.active { + color: var(--text-color-primary); + background: var(--button-tab-canvas-background); + } + } } .btn-tertiary { diff --git a/src/kits/Structure/PageTitle/types.ts b/src/kits/Structure/PageTitle/types.ts index bedc24c1d9..c71624a590 100644 --- a/src/kits/Structure/PageTitle/types.ts +++ b/src/kits/Structure/PageTitle/types.ts @@ -4,6 +4,10 @@ import type { PageTitleTabsProps } from '../PageTitleTabs/types'; export type PageTitleProps = PageTitleTabsProps & { + // tab button class. + tabClassName?: string; + // whether tabs are inline. + inline?: boolean; // title of the page. title?: string; // a button right next to the page title. diff --git a/src/kits/Structure/PageTitleTabs/Wrapper.ts b/src/kits/Structure/PageTitleTabs/Wrapper.ts index d9dcdf0bc1..7137b57580 100644 --- a/src/kits/Structure/PageTitleTabs/Wrapper.ts +++ b/src/kits/Structure/PageTitleTabs/Wrapper.ts @@ -11,6 +11,10 @@ export const Wrapper = styled.section` margin-top: 0.9rem; max-width: 100%; + &.inline { + border-bottom: none; + } + @media (max-width: ${PageWidthMediumThreshold}px) { margin-top: 0.5rem; } diff --git a/src/kits/Structure/PageTitleTabs/index.tsx b/src/kits/Structure/PageTitleTabs/index.tsx index 180de7132a..5855b96cb5 100644 --- a/src/kits/Structure/PageTitleTabs/index.tsx +++ b/src/kits/Structure/PageTitleTabs/index.tsx @@ -11,13 +11,21 @@ import { Wrapper } from './Wrapper'; * @name PageTitleTabs * @summary The element in a page title. Inculding the ButtonTab. */ -export const PageTitleTabs = ({ sticky, tabs = [] }: PageTitleProps) => ( - +export const PageTitleTabs = ({ + sticky, + tabs = [], + inline = false, + tabClassName, +}: PageTitleProps) => ( +
{tabs.map( ({ active, onClick, title, badge }: PageTitleTabProps, i: number) => ( onClick()} diff --git a/src/kits/Structure/Tx/Signer.tsx b/src/kits/Structure/Tx/Signer.tsx new file mode 100644 index 0000000000..b763f2d700 --- /dev/null +++ b/src/kits/Structure/Tx/Signer.tsx @@ -0,0 +1,33 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faPenToSquare, faWarning } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import type { SignerProps } from './types'; +import { SignerWrapper } from './Wrapper'; + +export const Signer = ({ + dangerMessage, + notEnoughFunds, + name, + label, +}: SignerProps) => ( + + + + {label} + + {name} + {notEnoughFunds && ( + + /   + + {dangerMessage} + + )} + +); diff --git a/src/kits/Structure/Tx/Wrapper.ts b/src/kits/Structure/Tx/Wrapper.ts index 88573abb21..89ff719b76 100644 --- a/src/kits/Structure/Tx/Wrapper.ts +++ b/src/kits/Structure/Tx/Wrapper.ts @@ -23,6 +23,10 @@ export const Wrapper = styled.div` background: var(--background-canvas-card); } + &.card { + border-radius: 0.5rem; + } + > section { width: 100%; @@ -31,6 +35,26 @@ export const Wrapper = styled.div` flex-direction: row; align-items: center; + &.col { + flex-direction: column; + margin-top: 0.5rem; + + > div { + width: 100%; + margin-bottom: 0.4rem; + + > div, + > p { + width: 100%; + margin-bottom: 0.4rem; + } + + > div:last-child { + margin-bottom: 0; + } + } + } + > div { display: flex; @@ -81,35 +105,35 @@ export const Wrapper = styled.div` } } } +`; - .sign { - display: flex; - align-items: center; - font-size: 0.9rem; - padding-bottom: 0.5rem; - margin: 0; - - .badge { - border: 1px solid var(--border-secondary-color); - border-radius: 0.45rem; - padding: 0.2rem 0.5rem; - margin-right: 0.75rem; - - > svg { - margin-right: 0.5rem; - } +export const SignerWrapper = styled.p` + display: flex; + align-items: center; + font-size: 0.9rem; + padding-bottom: 0.5rem; + margin: 0; + + .badge { + border: 1px solid var(--border-secondary-color); + border-radius: 0.45rem; + padding: 0.2rem 0.5rem; + margin-right: 0.75rem; + + > svg { + margin-right: 0.5rem; } + } - .not-enough { - margin-left: 0.5rem; - } + .not-enough { + margin-left: 0.5rem; + } - .danger { - color: var(--status-danger-color); - } + .danger { + color: var(--status-danger-color); + } - > .icon { - margin-right: 0.3rem; - } + > .icon { + margin-right: 0.3rem; } `; diff --git a/src/kits/Structure/Tx/index.tsx b/src/kits/Structure/Tx/index.tsx index 8c93b9f4b2..5e312d441a 100644 --- a/src/kits/Structure/Tx/index.tsx +++ b/src/kits/Structure/Tx/index.tsx @@ -1,29 +1,10 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faPenToSquare, faWarning } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import type { ReactElement } from 'react'; -import type { DisplayFor } from 'types'; import { Wrapper } from './Wrapper'; import { appendOrEmpty } from '@w3ux/utils'; - -export interface TxProps { - // whether there is margin on top. - margin?: boolean; - // account type for the transaction signing. - label: string; - // account id - name: string; - // whether there is enough funds for the transaction. - notEnoughFunds: boolean; - // warning messgae. - dangerMessage: string; - // signing component. - SignerComponent: ReactElement; - // display for. - displayFor?: DisplayFor; -} +import type { TxProps } from './types'; +import { Signer } from './Signer'; /** * @name Tx @@ -39,25 +20,15 @@ export const Tx = ({ displayFor = 'default', }: TxProps) => ( -
-

- - - {label} - - {name} - {notEnoughFunds && ( - - /   - {' '} - {dangerMessage} - - )} -

+
+
{SignerComponent}
diff --git a/src/kits/Structure/Tx/types.ts b/src/kits/Structure/Tx/types.ts new file mode 100644 index 0000000000..01e5981dac --- /dev/null +++ b/src/kits/Structure/Tx/types.ts @@ -0,0 +1,18 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ReactElement } from 'react'; +import type { DisplayFor } from 'types'; + +export interface SignerProps { + label: string; + name: string; + notEnoughFunds: boolean; + dangerMessage: string; +} + +export interface TxProps extends SignerProps { + margin?: boolean; + SignerComponent: ReactElement; + displayFor?: DisplayFor; +} diff --git a/src/library/CallToAction/index.tsx b/src/library/CallToAction/index.tsx new file mode 100644 index 0000000000..259285960a --- /dev/null +++ b/src/library/CallToAction/index.tsx @@ -0,0 +1,187 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const CallToActionWrapper = styled.div` + --button-border-radius: 2rem; + --button-vertical-space: 1.1rem; + + height: inherit; + width: 100%; + + > .inner { + flex: 1; + display: flex; + flex-direction: row; + margin-top: 0.38rem; + + @media (max-width: 650px) { + flex-wrap: wrap; + } + + > section { + display: flex; + flex-direction: row; + height: inherit; + + @media (max-width: 650px) { + margin-top: var(--button-vertical-space); + flex-grow: 1; + flex-basis: 100%; + + &:nth-child(1) { + margin-top: 0; + } + } + + &:nth-child(1) { + flex-grow: 1; + + @media (min-width: 651px) { + border-right: 1px solid var(--border-primary-color); + padding-right: 1rem; + } + } + + &:nth-child(2) { + flex: 1; + + @media (min-width: 651px) { + padding-left: 1rem; + } + } + + &.standalone { + border: none; + padding: 0; + } + + h3 { + line-height: 1.4rem; + } + + .buttons { + border: 0.75px solid var(--border-primary-color); + border-radius: var(--button-border-radius); + display: flex; + flex-wrap: nowrap; + width: 100%; + + @media (max-width: 650px) { + flex-wrap: wrap; + } + + > .button { + height: 3.75rem; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; + white-space: nowrap; + overflow: hidden; + transition: filter 0.15s; + + &.primary { + background-color: var(--accent-color-primary); + border-top-left-radius: var(--button-border-radius); + border-bottom-left-radius: var(--button-border-radius); + color: white; + flex-grow: 1; + + &:hover { + filter: brightness(90%); + } + + &.disabled { + background-color: var(--accent-color-pending); + + &:hover { + filter: none; + } + } + + &.pulse { + box-shadow: 0 0 30px 0 var(--accent-color-pending); + transform: scale(1); + animation: pulse 4s infinite; + + @keyframes pulse { + 0% { + transform: scale(0.98); + box-shadow: 0 0 0 0 var(--accent-color-pending); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgb(0 0 0 / 0%); + } + + 100% { + transform: scale(0.98); + box-shadow: 0 0 0 0 rgb(0 0 0 / 0%); + } + } + } + } + + &.secondary { + background-color: var(--button-primary-background); + border-top-right-radius: var(--button-border-radius); + border-bottom-right-radius: var(--button-border-radius); + color: var(--text-color-primary); + + &:hover { + filter: brightness(95%); + } + + &.disabled { + opacity: 0.5; + + &:hover { + filter: none; + } + } + } + + &.standalone { + border-radius: var(--button-border-radius); + flex-grow: 1; + } + + @media (max-width: 650px) { + border-radius: var(--button-border-radius); + margin-top: var(--button-vertical-space); + flex-grow: 1; + flex-basis: 100%; + + &:nth-child(1) { + margin-top: 0; + } + } + + > button { + color: inherit; + height: inherit; + transition: transform 0.25s; + padding: 0 2rem; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + font-size: 1.3rem; + width: 100%; + + &:disabled { + cursor: default; + } + + > svg { + margin: 0 0.75rem; + } + } + } + } + } + } +`; diff --git a/src/library/Card/Wrappers.ts b/src/library/Card/Wrappers.ts index 87e04050d1..a63ebf9f65 100644 --- a/src/library/Card/Wrappers.ts +++ b/src/library/Card/Wrappers.ts @@ -72,6 +72,7 @@ export const CardHeaderWrapper = styled.div` * Used to separate the main modules throughout the app. */ export const CardWrapper = styled.div` + border: 1px solid transparent; box-shadow: var(--card-shadow); background: var(--background-primary); border-radius: 1.1rem; @@ -82,10 +83,23 @@ export const CardWrapper = styled.div` overflow: hidden; margin-top: 1.4rem; padding: 1.5rem; + transition: border 0.2s; &.canvas { background: var(--background-canvas-card); padding: 1.25rem; + + &.secondary { + padding: 1rem; + + @media (max-width: 1000px) { + background: var(--background-canvas-card); + } + + @media (min-width: 1001px) { + background: var(--background-canvas-card-secondary); + } + } } &.transparent { @@ -101,6 +115,10 @@ export const CardWrapper = styled.div` border: 1px solid var(--status-warning-color); } + &.prompt { + border: 1px solid var(--accent-color-pending); + } + @media (max-width: ${PageWidthMediumThreshold}px) { padding: 1rem 0.75rem; } diff --git a/src/library/Form/Bond/BondFeedback.tsx b/src/library/Form/Bond/BondFeedback.tsx index 06ccbfb027..944095dd51 100644 --- a/src/library/Form/Bond/BondFeedback.tsx +++ b/src/library/Form/Bond/BondFeedback.tsx @@ -27,6 +27,7 @@ export const BondFeedback = ({ txFees, maxWidth, syncing = false, + displayFirstWarningOnly = true, }: BondFeedbackProps) => { const { t } = useTranslation('library'); const { @@ -145,6 +146,10 @@ export const BondFeedback = ({ setErrors(newErrors); }; + // If `displayFirstWarningOnly` is set, filter errors to only the first one. + const filteredErrors = + displayFirstWarningOnly && errors.length > 1 ? [errors[0]] : errors; + // update bond on account change useEffect(() => { setBond({ @@ -168,7 +173,7 @@ export const BondFeedback = ({ return ( <> - {errors.map((err, i) => ( + {filteredErrors.map((err, i) => ( ))} diff --git a/src/library/Form/ClaimPermissionInput/index.tsx b/src/library/Form/ClaimPermissionInput/index.tsx index 9915509d6a..ac87b2c324 100644 --- a/src/library/Form/ClaimPermissionInput/index.tsx +++ b/src/library/Form/ClaimPermissionInput/index.tsx @@ -6,48 +6,35 @@ import { useTranslation } from 'react-i18next'; import { TabWrapper, TabsWrapper } from 'library/Filter/Wrappers'; import type { ClaimPermission } from 'contexts/Pools/types'; import type { ClaimPermissionConfig } from '../types'; -import { ActionItem } from 'library/ActionItem'; - -export interface ClaimPermissionInputProps { - current: ClaimPermission | undefined; - permissioned: boolean; - onChange: (value: ClaimPermission | undefined) => void; - disabled?: boolean; -} +import type { ClaimPermissionInputProps } from './types'; export const ClaimPermissionInput = ({ current, - permissioned, onChange, disabled = false, }: ClaimPermissionInputProps) => { const { t } = useTranslation('library'); const claimPermissionConfig: ClaimPermissionConfig[] = [ - { - label: t('allowCompound'), - value: 'PermissionlessCompound', - description: t('allowAnyoneCompound'), - }, { label: t('allowWithdraw'), value: 'PermissionlessWithdraw', description: t('allowAnyoneWithdraw'), }, { - label: t('allowAll'), - value: 'PermissionlessAll', - description: t('allowAnyoneCompoundWithdraw'), + label: t('allowCompound'), + value: 'PermissionlessCompound', + description: t('allowAnyoneCompound'), + }, + { + label: t('permissioned'), + value: 'Permissioned', + description: t('permissionedSubtitle'), }, ]; - // Updated claim permission value - const [selected, setSelected] = useState( - current - ); - - // Permissionless claim enabled. - const [enabled, setEnabled] = useState(permissioned); + // Updated claim permission value. + const [selected, setSelected] = useState(current); const activeTab = claimPermissionConfig.find( ({ value }) => value === selected @@ -60,43 +47,22 @@ export const ClaimPermissionInput = ({ return ( <> - { - // toggle enable claim permission. - setEnabled(val); - - const newClaimPermission = val - ? claimPermissionConfig[0].value - : current === undefined - ? undefined - : 'Permissioned'; - - setSelected(newClaimPermission); - onChange(newClaimPermission); - }} - disabled={disabled} - inactive={disabled} - /> {claimPermissionConfig.map(({ label, value }, i) => ( { setSelected(value); onChange(value); }} + style={{ flexGrow: 1 }} > {label} @@ -104,13 +70,17 @@ export const ClaimPermissionInput = ({
{activeTab ? ( -

{activeTab.description}

+

+ {activeTab.description} +

) : ( -

{t('permissionlessClaimingTurnedOff')}

+

+ {t('permissionlessClaimingTurnedOff')} +

)}
diff --git a/src/library/Form/ClaimPermissionInput/types.ts b/src/library/Form/ClaimPermissionInput/types.ts new file mode 100644 index 0000000000..b4d2de3367 --- /dev/null +++ b/src/library/Form/ClaimPermissionInput/types.ts @@ -0,0 +1,10 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ClaimPermission } from 'contexts/Pools/types'; + +export interface ClaimPermissionInputProps { + current: ClaimPermission; + onChange: (value: ClaimPermission) => void; + disabled?: boolean; +} diff --git a/src/library/Form/Unbond/UnbondFeedback.tsx b/src/library/Form/Unbond/UnbondFeedback.tsx index 6f82845d57..9521af7982 100644 --- a/src/library/Form/Unbond/UnbondFeedback.tsx +++ b/src/library/Form/Unbond/UnbondFeedback.tsx @@ -24,6 +24,7 @@ export const UnbondFeedback = ({ setLocalResize, parentErrors = [], txFees, + displayFirstWarningOnly = true, }: UnbondFeedbackProps) => { const { t } = useTranslation('library'); const { @@ -129,6 +130,10 @@ export const UnbondFeedback = ({ setErrors(newErrors); }; + // If `displayFirstWarningOnly` is set, filter errors to only the first one. + const filteredErrors = + displayFirstWarningOnly && errors.length > 1 ? [errors[0]] : errors; + // update bond on account change useEffect(() => { setBond({ bond: defaultValue }); @@ -148,7 +153,7 @@ export const UnbondFeedback = ({ return ( <> - {errors.map((err, i) => ( + {filteredErrors.map((err, i) => ( ))} diff --git a/src/library/Form/types.ts b/src/library/Form/types.ts index 3222f067f7..186f78672b 100644 --- a/src/library/Form/types.ts +++ b/src/library/Form/types.ts @@ -49,6 +49,7 @@ export interface BondFeedbackProps { setLocalResize?: () => void; txFees: BigNumber; maxWidth?: boolean; + displayFirstWarningOnly?: boolean; } export interface BondInputProps { @@ -70,6 +71,7 @@ export interface UnbondFeedbackProps { parentErrors?: string[]; setLocalResize?: () => void; txFees: BigNumber; + displayFirstWarningOnly?: boolean; } export interface UnbondInputProps { diff --git a/src/library/Graphs/GeoDonut.tsx b/src/library/Graphs/GeoDonut.tsx index 3dd8904e45..4ce00d3d13 100644 --- a/src/library/Graphs/GeoDonut.tsx +++ b/src/library/Graphs/GeoDonut.tsx @@ -76,8 +76,8 @@ export const GeoDonut = ({ { label: title, data, - // We make a gradient of N+2 colors from active to inactive, and we discard both ends - // N is the number of datapoints to plot + // We make a gradient of N+2 colors from active to inactive, and we discard both ends N is + // the number of datapoints to plot. backgroundColor: chroma .scale([backgroundColor, graphColors.inactive[mode]]) .colors(data.length + 1), diff --git a/src/library/Graphs/PayoutBar.tsx b/src/library/Graphs/PayoutBar.tsx index e6d17e6230..9d4fdbcd78 100644 --- a/src/library/Graphs/PayoutBar.tsx +++ b/src/library/Graphs/PayoutBar.tsx @@ -94,6 +94,7 @@ export const PayoutBar = ({ }); return `${dateObj}`; }), + datasets: [ { order: 1, diff --git a/src/library/Headers/Sync.tsx b/src/library/Headers/Sync.tsx index a6f4f63c1c..6c4188a047 100644 --- a/src/library/Headers/Sync.tsx +++ b/src/library/Headers/Sync.tsx @@ -13,8 +13,8 @@ import { useTxMeta } from 'contexts/TxMeta'; import { useSyncing } from 'hooks/useSyncing'; export const Sync = () => { + const { syncing } = useSyncing(); const { pathname } = useLocation(); - const { syncing } = useSyncing('*'); const { pendingNonces } = useTxMeta(); const { payoutsSynced } = usePayouts(); const { pluginEnabled } = usePlugins(); diff --git a/src/library/ListItem/Labels/EraStatus.tsx b/src/library/ListItem/Labels/EraStatus.tsx index b3256cf61d..d407860c9c 100644 --- a/src/library/ListItem/Labels/EraStatus.tsx +++ b/src/library/ListItem/Labels/EraStatus.tsx @@ -10,7 +10,7 @@ import { useSyncing } from 'hooks/useSyncing'; export const EraStatus = ({ noMargin, status, totalStake }: EraStatusProps) => { const { t } = useTranslation('library'); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { unit, units } = useNetwork().networkData; // Fallback to `waiting` status if still syncing. diff --git a/src/library/ListItem/Labels/JoinPool.tsx b/src/library/ListItem/Labels/JoinPool.tsx index 0bb9e9e6ff..b6e98cb25b 100644 --- a/src/library/ListItem/Labels/JoinPool.tsx +++ b/src/library/ListItem/Labels/JoinPool.tsx @@ -14,20 +14,20 @@ export const JoinPool = ({ setActiveTab: (t: number) => void; }) => { const { t } = useTranslation('library'); - const { openModal } = useOverlay().modal; + const { openCanvas } = useOverlay().canvas; return (
+
+
+ +
+ +); diff --git a/src/library/SubmitTx/Default.tsx b/src/library/SubmitTx/Default.tsx index e475696a4b..5305e89ec0 100644 --- a/src/library/SubmitTx/Default.tsx +++ b/src/library/SubmitTx/Default.tsx @@ -8,6 +8,8 @@ import { EstimatedTxFee } from 'library/EstimatedTxFee'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import type { SubmitProps } from './types'; import { ButtonSubmit } from 'kits/Buttons/ButtonSubmit'; +import { ButtonSubmitLarge } from './ButtonSubmitLarge'; +import { appendOrEmpty } from '@w3ux/utils'; export const Default = ({ onSubmit, @@ -25,22 +27,35 @@ export const Default = ({ submitting || !valid || !accountHasSigner(submitAddress) || !txFeesValid; return ( -
-
- + <> +
+
+ +
+
+ {buttons} + {displayFor !== 'card' && ( + onSubmit()} + disabled={disabled} + pulse={!disabled} + /> + )} +
-
- {buttons} - onSubmit()} + {displayFor === 'card' && ( + -
-
+ )} + ); }; diff --git a/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx b/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx index 04a9fb1500..4f95c80416 100644 --- a/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx +++ b/src/library/SubmitTx/ManualSign/Ledger/Submit.tsx @@ -11,6 +11,7 @@ import { getLedgerApp } from 'contexts/Hardware/Utils'; import { useNetwork } from 'contexts/Network'; import { useTxMeta } from 'contexts/TxMeta'; import { ButtonSubmit } from 'kits/Buttons/ButtonSubmit'; +import { ButtonSubmitLarge } from 'library/SubmitTx/ButtonSubmitLarge'; import type { LedgerSubmitProps } from 'library/SubmitTx/types'; import { useTranslation } from 'react-i18next'; @@ -73,7 +74,7 @@ export const Submit = ({ // Button icon. const icon = !integrityChecked ? faUsb : faSquarePen; - return ( + return displayFor !== 'card' ? ( + ) : ( + ); }; diff --git a/src/library/SubmitTx/ManualSign/Ledger/index.tsx b/src/library/SubmitTx/ManualSign/Ledger/index.tsx index 3011f06e19..1b2701cb0f 100644 --- a/src/library/SubmitTx/ManualSign/Ledger/index.tsx +++ b/src/library/SubmitTx/ManualSign/Ledger/index.tsx @@ -19,6 +19,7 @@ import { getLedgerApp } from 'contexts/Hardware/Utils'; import type { SubmitProps } from '../../types'; import { Submit } from './Submit'; import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; +import { appendOrEmpty } from '@w3ux/utils'; export const Ledger = ({ uid, @@ -133,7 +134,9 @@ export const Ledger = ({
)} -
+
{valid ? (

diff --git a/src/library/SubmitTx/ManualSign/Vault/index.tsx b/src/library/SubmitTx/ManualSign/Vault/index.tsx index 5160640ba2..94a5a1d4be 100644 --- a/src/library/SubmitTx/ManualSign/Vault/index.tsx +++ b/src/library/SubmitTx/ManualSign/Vault/index.tsx @@ -11,6 +11,8 @@ import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import type { SubmitProps } from '../../types'; import { SignPrompt } from './SignPrompt'; import { ButtonSubmit } from 'kits/Buttons/ButtonSubmit'; +import { ButtonSubmitLarge } from 'library/SubmitTx/ButtonSubmitLarge'; +import { appendOrEmpty } from '@w3ux/utils'; export const Vault = ({ onSubmit, @@ -30,38 +32,51 @@ export const Vault = ({ const disabled = submitting || !valid || !accountHasSigner(submitAddress) || !txFeesValid; + // Format submit button based on whether signature currently exists or submission is ongoing. + let buttonText: string; + let buttonOnClick: () => void; + let buttonDisabled: boolean; + let buttonPulse: boolean; + + if (getTxSignature() !== null || submitting) { + buttonText = submitText || ''; + buttonOnClick = onSubmit; + buttonDisabled = disabled; + buttonPulse = !(!valid || promptStatus !== 0); + } else { + buttonText = promptStatus === 0 ? t('sign') : t('signing'); + buttonOnClick = async () => { + openPromptWith(, 'small'); + }; + buttonDisabled = disabled || promptStatus !== 0; + buttonPulse = !disabled || promptStatus === 0; + } + return ( -

+
{valid ?

{t('submitTransaction')}

:

...

}
{buttons} - {getTxSignature() !== null || submitting ? ( + {displayFor !== 'card' ? ( onSubmit()} - disabled={disabled} - pulse={!(!valid || promptStatus !== 0)} + onClick={() => buttonOnClick()} + pulse={buttonPulse} /> ) : ( - { - openPromptWith( - , - 'small' - ); - }} - disabled={disabled || promptStatus !== 0} - pulse={!disabled || promptStatus === 0} + )}
diff --git a/src/library/SubmitTx/types.ts b/src/library/SubmitTx/types.ts index 3e6072d3bf..4f1416550d 100644 --- a/src/library/SubmitTx/types.ts +++ b/src/library/SubmitTx/types.ts @@ -1,6 +1,7 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; import type { ReactNode } from 'react'; import type { DisplayFor, MaybeAddress } from 'types'; @@ -33,3 +34,12 @@ export interface LedgerSubmitProps { disabled: boolean; submitText?: string; } + +export interface ButtonSubmitLargeProps { + disabled: boolean; + onSubmit: () => void; + submitText: string; + icon?: IconProp; + iconTransform?: string; + pulse: boolean; +} diff --git a/src/library/ValidatorList/ValidatorItem/Nomination.tsx b/src/library/ValidatorList/ValidatorItem/Nomination.tsx index 7ca4be2327..0d0236637e 100644 --- a/src/library/ValidatorList/ValidatorItem/Nomination.tsx +++ b/src/library/ValidatorList/ValidatorItem/Nomination.tsx @@ -43,13 +43,15 @@ export const Nomination = ({ {toggleFavorites && } - + {displayFor !== 'canvas' && ( + + )}
diff --git a/src/library/ValidatorList/index.tsx b/src/library/ValidatorList/index.tsx index 9c967dca10..691989594c 100644 --- a/src/library/ValidatorList/index.tsx +++ b/src/library/ValidatorList/index.tsx @@ -78,7 +78,7 @@ export const ValidatorListInner = ({ } = useFilters(); const { mode } = useTheme(); const listProvider = useList(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { isReady, activeEra } = useApi(); const { activeAccount } = useActiveAccounts(); const { setModalResize } = useOverlay().modal; diff --git a/src/library/WithdrawPrompt/index.tsx b/src/library/WithdrawPrompt/index.tsx index 3696f05e1c..ad1021a051 100644 --- a/src/library/WithdrawPrompt/index.tsx +++ b/src/library/WithdrawPrompt/index.tsx @@ -18,17 +18,21 @@ import { useErasToTimeLeft } from 'hooks/useErasToTimeLeft'; import { useApi } from 'contexts/Api'; import { useTranslation } from 'react-i18next'; import type { BondFor } from 'types'; +import { useActivePool } from 'contexts/Pools/ActivePool'; export const WithdrawPrompt = ({ bondFor }: { bondFor: BondFor }) => { const { t } = useTranslation('modals'); const { mode } = useTheme(); const { consts } = useApi(); + const { activePool } = useActivePool(); const { openModal } = useOverlay().modal; const { colors } = useNetwork().networkData; + const { syncing } = useSyncing(['balances']); const { activeAccount } = useActiveAccounts(); const { erasToSeconds } = useErasToTimeLeft(); const { getTransferOptions } = useTransferOptions(); + const { state } = activePool?.bondedPool || {}; const { bondDuration } = consts; const allTransferOptions = getTransferOptions(activeAccount); @@ -49,6 +53,9 @@ export const WithdrawPrompt = ({ bondFor }: { bondFor: BondFor }) => { const displayPrompt = totalUnlockChunks > 0; return ( + /* NOTE: ClosurePrompts is a component that displays a prompt to the user when a pool is being + destroyed. */ + state !== 'Destroying' && displayPrompt && ( diff --git a/src/locale/cn/library.json b/src/locale/cn/library.json index 894afb3979..3802a7e018 100644 --- a/src/locale/cn/library.json +++ b/src/locale/cn/library.json @@ -9,10 +9,12 @@ "activePools": "活跃提名池", "activeValidator": "活跃验证人", "activeValidators": "活跃验证人", + "activelyNominating": "活跃提名中", "add": "添加", "addFromFavorites": "从收藏夹添加", "address": "地址", "addressCopiedToClipboard": "复制到剪贴板的地址", + "addresses": "地址", "all": "全部", "allowAll": "允许所有", "allowAnyoneCompound": "允许任何人代表您复利收益", @@ -24,19 +26,24 @@ "asAPoolMember": "作为提名池成员", "asThePoolDepositor": "作为提名池存款人", "atLeast": "质押金最低为", + "autoSelected": "己自动选定", "available": "可用", "backToMethods": "返回方案选择", "backToScan": "回到扫描", + "blocked": "己关闭", "blockedNominations": "己冻结提名", "blockingNominations": "冻结提名中", "bond": "质押", "bondAmountDecimals": "质押金额最多只能有 {{units}}个小数位", "bondDecimalsError": "质押金额能最多有 {{units}} 位点数", "bonded": "己质押", + "browseValidators": "浏览验证人", "cancel": "取消", "cancelled": "已取消", + "chooseAnotherPool": "选择另一个池", "chooseValidators": "最多能选择 {{maxNominations}} 个验证人。", "chooseValidators2": "自动生成提名或手动加入提名", + "claimSetting": "申领设置", "clear": "清除", "clearSelection": "清除选择", "clickToReload": "重新加载", @@ -64,6 +71,7 @@ "displayingValidators": "正在显示 {{count}} 个验证人", "done": "完成", "enablePermissionlessClaiming": "启用己许可申领", + "era": "Era", "eraPoints": "Era 点数", "errorUnknown": "抱歉,页面出现点小问题哦", "errorWithTransaction": "交易出错", @@ -121,6 +129,7 @@ "nominate": "提名", "nominateActive": "激活", "nominateInactive": "未激活", + "nominations": "提名", "nominationsReverted": "已恢复原来提名", "nominator": "提名人", "notEnough": "不足", @@ -142,6 +151,8 @@ "payoutAccount": "收益到账账户", "payoutAddress": "收益到账地址", "pending": "待定中", + "permissioned": "已获许可", + "permissionedSubtitle": "仅本人可申领奖励", "permissionlessClaimingTurnedOff": "己许可申领己关闭", "points": "点数", "pool": "提名池", @@ -154,6 +165,7 @@ "proxy": "代理账户", "randomValidator": "随机验证人", "reGenerate": "重新生成", + "recentPerformance": "最近表现", "remove": "删除", "removeSelected": "移除选定项", "reset": "重设", @@ -170,6 +182,7 @@ "signing": "签署中", "submitTransaction": "准备提交交易", "syncing": "正在同步", + "syncingPoolData": "同步提名池数据中", "syncingPoolList": "同步提名池列表", "tooSmall": "质押金额太少", "top": "首", diff --git a/src/locale/cn/pages.json b/src/locale/cn/pages.json index cca2e74eba..b0934d3feb 100644 --- a/src/locale/cn/pages.json +++ b/src/locale/cn/pages.json @@ -137,10 +137,10 @@ "bondedFunds": "己质押金额", "bouncer": "守护人", "browseMembers": "浏览成员", + "browsePools": "浏览提名池", "cancel": "取消", "closePool": "可提取己解锁金额并关闭池", "compound": "复利", - "create": "创建", "createAPool": "创建提名池", "createPool": "创建提名池", "depositor": "存款人", @@ -154,7 +154,7 @@ "generateNominations": "生成提名", "inPool": "提名池中", "inactivePoolNotNominating": "非活跃:提名池未提名任何验证人", - "join": "加入", + "joinPool": "加入提名池", "leave": "离开", "leavingPool": "离开提名池中", "leftThePool": "所有成员已离开", diff --git a/src/locale/en/library.json b/src/locale/en/library.json index c77b0a37c7..084462da4d 100644 --- a/src/locale/en/library.json +++ b/src/locale/en/library.json @@ -9,34 +9,41 @@ "activePools": "Active Pools", "activeValidator": "Active Validator", "activeValidators": "Active Validators", + "activelyNominating": "Actively Nominating", "add": "Add", "addFromFavorites": "Add From Favorites", "address": "Address", "addressCopiedToClipboard": "Address Copied to Clipboard", + "addresses": "Addresses", "all": "All", "allowAll": "Allow All", - "allowAnyoneCompound": "Allow anyone to compound rewards on your behalf.", + "allowAnyoneCompound": "Allow anyone to compound your rewards.", "allowAnyoneCompoundWithdraw": "Allow anyone to compound or withdraw rewards on your behalf.", - "allowAnyoneWithdraw": "Allow anyone to withdraw rewards on your behalf.", + "allowAnyoneWithdraw": "Allow anyone to withdraw your rewards to your account.", "allowCompound": "Allow Compound", "allowWithdraw": "Allow Withdraw", "alreadyImported": "Address Already Imported", "asAPoolMember": "as a pool member.", "asThePoolDepositor": "as the pool depositor.", "atLeast": "Bond amount must be at least", + "autoSelected": "Auto Selected", "available": "Available", "backToMethods": "Back to Methods", "backToScan": "Back to Scan", + "blocked": "Blocked", "blockedNominations": "Blocked Nominations", "blockingNominations": "Blocking Nominations", "bond": "Bond", "bondAmountDecimals": "Bond amount can only have at most {{units}} decimals.", "bondDecimalsError": "Bond amount can have at most {{units}} decimals.", "bonded": "Bonded", + "browseValidators": "Browse Validators", "cancel": "Cancel", "cancelled": "Cancelled", + "chooseAnotherPool": "Choose Another Pool", "chooseValidators": "Choose up to {{maxNominations}} validators to nominate.", "chooseValidators2": "Generate your nominations automatically or manually insert them.", + "claimSetting": "Claim Setting", "clear": "Clear", "clearSelection": "clear selection", "clickToReload": "Click to reload", @@ -65,6 +72,7 @@ "displayingValidators_other": "Displaying {{count}} Validators", "done": "Done", "enablePermissionlessClaiming": "Enable Permissionless Claiming", + "era": "Era", "eraPoints": "Era Points", "errorUnknown": "Oops, Something Went Wrong", "errorWithTransaction": "Error with transaction", @@ -124,6 +132,8 @@ "nominateActive": "Active", "nominateInactive": "Inactive", "nominationsReverted": "Nominations Reverted", + "nominations_one": "Nomination", + "nominations_other": "Nominations", "nominator": "Nominator", "notEnough": "Not Enough", "notEnoughAfter": "Not enough {{unit}} to bond after transaction fees.", @@ -144,6 +154,8 @@ "payoutAccount": "Payout Account", "payoutAddress": "Payout Adddress", "pending": "Pending", + "permissioned": "Permissioned", + "permissionedSubtitle": "Only you can claim rewards.", "permissionlessClaimingTurnedOff": "Permissionless claiming is turned off.", "points": "Points", "pool": "Pool", @@ -156,6 +168,7 @@ "proxy": "Proxy", "randomValidator": "Random Validator", "reGenerate": "Re-Generate", + "recentPerformance": "Recent Performance", "remove": "Remove", "removeSelected": "Remove Selected", "reset": "Reset", @@ -172,6 +185,7 @@ "signing": "Signing", "submitTransaction": "Ready to submit transaction.", "syncing": "Syncing", + "syncingPoolData": "Syncing Pool Data", "syncingPoolList": "Syncing Pool list", "tooSmall": "Bond amount is too small.", "top": "Top", diff --git a/src/locale/en/pages.json b/src/locale/en/pages.json index 9c06757d17..b833194a2b 100644 --- a/src/locale/en/pages.json +++ b/src/locale/en/pages.json @@ -139,10 +139,10 @@ "bondedFunds": "Bonded Funds", "bouncer": "Bouncer", "browseMembers": "Browse Members", + "browsePools": "Browse Pools", "cancel": "Cancel", "closePool": "You can now withdraw and close the pool.", "compound": "Compound", - "create": "Create", "createAPool": "Create a Pool", "createPool": "Create Pool", "depositor": "Depositor", @@ -156,7 +156,7 @@ "generateNominations": "Generate Nominations", "inPool": "In Pool", "inactivePoolNotNominating": "Inactive: Pool Not Nominating", - "join": "Join", + "joinPool": "Join Pool", "leave": "Leave", "leavingPool": "Leaving Pool", "leftThePool": "All members have now left the pool", diff --git a/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx b/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx index 919d3b9198..7ce53ec93b 100644 --- a/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx +++ b/src/modals/ManagePool/Forms/SetClaimPermission/index.tsx @@ -19,6 +19,7 @@ import { useBalances } from 'contexts/Balances'; import { ButtonSubmitInvert } from 'kits/Buttons/ButtonSubmitInvert'; import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; import { ModalWarnings } from 'kits/Overlay/structure/ModalWarnings'; +import { defaultClaimPermission } from 'controllers/ActivePoolsController/defaults'; export const SetClaimPermission = ({ setSection, @@ -92,10 +93,7 @@ export const SetClaimPermission = ({ ) : null} { setClaimPermission(val); }} diff --git a/src/modals/PoolNominations/index.tsx b/src/modals/PoolNominations/index.tsx index b29b23fdfd..6e6db3e987 100644 --- a/src/modals/PoolNominations/index.tsx +++ b/src/modals/PoolNominations/index.tsx @@ -9,11 +9,11 @@ import { ListWrapper } from './Wrappers'; import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; export const PoolNominations = () => { + const { t } = useTranslation('modals'); const { config: { options }, } = useOverlay().modal; const { nominator, targets } = options; - const { t } = useTranslation('modals'); return ( <> diff --git a/src/model/Api/index.ts b/src/model/Api/index.ts index bfb40e30e8..ec1fa28b5f 100644 --- a/src/model/Api/index.ts +++ b/src/model/Api/index.ts @@ -92,7 +92,8 @@ export class Api { // Class initialization. Sets the `provider` and `api` class members. async initialize(type: ConnectionType, rpcEndpoint: string) { - // Add initial syncing items. + // Add initial syncing items. Even though `initialization` is added by default, it is called + // again here in case a new API is initialized. SyncController.dispatch('initialization', 'syncing'); // Persist the network to local storage. diff --git a/src/overlay/index.tsx b/src/overlay/index.tsx index 7b76f3821f..287b125755 100644 --- a/src/overlay/index.tsx +++ b/src/overlay/index.tsx @@ -16,7 +16,6 @@ import { Connect } from '../modals/Connect'; import { GoToFeedback } from '../modals/GoToFeedback'; import { ImportLedger } from '../modals/ImportLedger'; import { ImportVault } from '../modals/ImportVault'; -import { JoinPool } from '../modals/JoinPool'; import { ManageFastUnstake } from '../modals/ManageFastUnstake'; import { ManagePool } from '../modals/ManagePool'; import { Networks } from '../modals/Networks'; @@ -31,6 +30,7 @@ import { ValidatorMetrics } from '../modals/ValidatorMetrics'; import { ValidatorGeo } from '../modals/ValidatorGeo'; import { ManageNominations } from '../canvas/ManageNominations'; import { PoolMembers } from 'canvas/PoolMembers'; +import { JoinPool } from 'canvas/JoinPool'; import { Overlay } from 'kits/Overlay'; export const Overlays = () => { @@ -51,7 +51,6 @@ export const Overlays = () => { Connect, Accounts, GoToFeedback, - JoinPool, ImportLedger, ImportVault, ManagePool, @@ -70,6 +69,7 @@ export const Overlays = () => { canvas={{ ManageNominations, PoolMembers, + JoinPool, }} /> ); diff --git a/src/pages/Nominate/Active/ManageBond.tsx b/src/pages/Nominate/Active/ManageBond.tsx index 76999e7e83..d0967f16cc 100644 --- a/src/pages/Nominate/Active/ManageBond.tsx +++ b/src/pages/Nominate/Active/ManageBond.tsx @@ -31,8 +31,8 @@ export const ManageBond = () => { }, } = useNetwork(); const { openHelp } = useHelp(); + const { syncing } = useSyncing(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { getLedger } = useBalances(); const { openModal } = useOverlay().modal; const { isFastUnstaking } = useUnstaking(); diff --git a/src/pages/Nominate/Active/Status/NewNominator.tsx b/src/pages/Nominate/Active/Status/NewNominator.tsx new file mode 100644 index 0000000000..90384c193f --- /dev/null +++ b/src/pages/Nominate/Active/Status/NewNominator.tsx @@ -0,0 +1,61 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CallToActionWrapper } from 'library/CallToAction'; +import { + faChevronCircleRight, + faChevronRight, +} from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import { useSetup } from 'contexts/Setup'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useNavigate } from 'react-router-dom'; +import { useApi } from 'contexts/Api'; +import type { NewNominatorProps } from '../types'; +import { CallToActionLoader } from 'library/Loader/CallToAction'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const NewNominator = ({ syncing }: NewNominatorProps) => { + const { t } = useTranslation(); + const { isReady } = useApi(); + const navigate = useNavigate(); + const { setOnNominatorSetup } = useSetup(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + const nominateButtonDisabled = + !isReady || !activeAccount || isReadOnlyAccount(activeAccount); + + return ( + +
+ {syncing ? ( + + ) : ( +
+
+
+ +
+
+ +
+
+
+ )} +
+
+ ); +}; diff --git a/src/pages/Nominate/Active/Status/NominationStatus.tsx b/src/pages/Nominate/Active/Status/NominationStatus.tsx index 495a5d212e..5ee363c027 100644 --- a/src/pages/Nominate/Active/Status/NominationStatus.tsx +++ b/src/pages/Nominate/Active/Status/NominationStatus.tsx @@ -1,16 +1,11 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { - faBolt, - faChevronCircleRight, - faSignOutAlt, -} from '@fortawesome/free-solid-svg-icons'; +import { faBolt, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; import { useTranslation } from 'react-i18next'; import { useApi } from 'contexts/Api'; import { useBonded } from 'contexts/Bonded'; import { useFastUnstake } from 'contexts/FastUnstake'; -import { useSetup } from 'contexts/Setup'; import { useStaking } from 'contexts/Staking'; import { useNominationStatus } from 'hooks/useNominationStatus'; import { useUnstaking } from 'hooks/useUnstaking'; @@ -41,7 +36,6 @@ export const NominationStatus = ({ const { isReadOnlyAccount } = useImportedAccounts(); const { getNominationStatus } = useNominationStatus(); const { getFastUnstakeText, isUnstaking } = useUnstaking(); - const { setOnNominatorSetup, getNominatorSetupPercent } = useSetup(); const fastUnstakeText = getFastUnstakeText(); const controller = getBondedAccount(activeAccount); @@ -67,15 +61,6 @@ export const NominationStatus = ({ onClick: () => openModal({ key: 'Unstake', size: 'sm' }), }; - // Display progress alongside start title if exists and in setup. - let startTitle = t('nominate.startNominating'); - if (inSetup()) { - const progress = getNominatorSetupPercent(activeAccount); - if (progress > 0) { - startTitle += `: ${progress}%`; - } - } - return ( setOnNominatorSetup(true), - }, - ] + : [] } buttonType={buttonType} /> diff --git a/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx b/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx index 39cc95b035..8334686cfc 100644 --- a/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx +++ b/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx @@ -16,8 +16,8 @@ import { useSyncing } from 'hooks/useSyncing'; export const PayoutDestinationStatus = () => { const { t } = useTranslation('pages'); const { getPayee } = useBalances(); + const { syncing } = useSyncing(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { openModal } = useOverlay().modal; const { isFastUnstaking } = useUnstaking(); const { getPayeeItems } = usePayeeConfig(); diff --git a/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx index cca2d2cc3c..46805ace60 100644 --- a/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx +++ b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx @@ -13,7 +13,7 @@ import { useNetwork } from 'contexts/Network'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -export const UnclaimedPayoutsStatus = () => { +export const UnclaimedPayoutsStatus = ({ dimmed }: { dimmed: boolean }) => { const { t } = useTranslation(); const { networkData: { units }, @@ -43,6 +43,7 @@ export const UnclaimedPayoutsStatus = () => { 2 ), }} + dimmed={dimmed} buttons={ Object.keys(unclaimedPayouts || {}).length > 0 && !totalUnclaimed.isZero() diff --git a/src/pages/Nominate/Active/Status/index.tsx b/src/pages/Nominate/Active/Status/index.tsx index 2ed3ce0c39..f37a61ec12 100644 --- a/src/pages/Nominate/Active/Status/index.tsx +++ b/src/pages/Nominate/Active/Status/index.tsx @@ -6,13 +6,41 @@ import { UnclaimedPayoutsStatus } from './UnclaimedPayoutsStatus'; import { NominationStatus } from './NominationStatus'; import { PayoutDestinationStatus } from './PayoutDestinationStatus'; import { Separator } from 'kits/Structure/Separator'; +import { useSyncing } from 'hooks/useSyncing'; +import { useStaking } from 'contexts/Staking'; +import { NewNominator } from './NewNominator'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -export const Status = ({ height }: { height: number }) => ( - - - - - - - -); +export const Status = ({ height }: { height: number }) => { + const { syncing } = useSyncing(); + const { inSetup } = useStaking(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + return ( + + + + + + {!syncing ? ( + !inSetup() ? ( + <> + + + + ) : ( + !isReadOnlyAccount(activeAccount) && ( + + ) + ) + ) : ( + + )} + + ); +}; diff --git a/src/pages/Nominate/Active/UnstakePrompts.tsx b/src/pages/Nominate/Active/UnstakePrompts.tsx index 122eb5ec0a..f9a3ea106a 100644 --- a/src/pages/Nominate/Active/UnstakePrompts.tsx +++ b/src/pages/Nominate/Active/UnstakePrompts.tsx @@ -19,7 +19,7 @@ import { ButtonRow } from 'kits/Structure/ButtonRow'; export const UnstakePrompts = () => { const { t } = useTranslation('pages'); const { mode } = useTheme(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { openModal } = useOverlay().modal; const { activeAccount } = useActiveAccounts(); const { unit, colors } = useNetwork().networkData; diff --git a/src/pages/Nominate/Active/index.tsx b/src/pages/Nominate/Active/index.tsx index 58e7ef1d59..cf05491721 100644 --- a/src/pages/Nominate/Active/index.tsx +++ b/src/pages/Nominate/Active/index.tsx @@ -31,8 +31,8 @@ import { WithdrawPrompt } from 'library/WithdrawPrompt'; export const Active = () => { const { t } = useTranslation(); const { openHelp } = useHelp(); + const { syncing } = useSyncing(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); const { getNominations } = useBalances(); const { openCanvas } = useOverlay().canvas; const { isFastUnstaking } = useUnstaking(); @@ -55,14 +55,14 @@ export const Active = () => { - - - - + + + + diff --git a/src/pages/Nominate/Active/types.ts b/src/pages/Nominate/Active/types.ts index d3c14eeec1..db68ada844 100644 --- a/src/pages/Nominate/Active/types.ts +++ b/src/pages/Nominate/Active/types.ts @@ -10,3 +10,7 @@ export interface BondedChartProps { unlocked: BigNumber; inactive: boolean; } + +export interface NewNominatorProps { + syncing: boolean; +} diff --git a/src/pages/Nominate/Setup/index.tsx b/src/pages/Nominate/Setup/index.tsx index a67fc654a9..df7384c110 100644 --- a/src/pages/Nominate/Setup/index.tsx +++ b/src/pages/Nominate/Setup/index.tsx @@ -42,6 +42,7 @@ export const Setup = () => { setOnNominatorSetup(false); } }} + lg /> @@ -53,6 +54,7 @@ export const Setup = () => { setOnNominatorSetup(false); removeSetupProgress('nominator', activeAccount); }} + lg />
diff --git a/src/pages/Overview/Payouts.tsx b/src/pages/Overview/Payouts.tsx index b9b9d8b2a3..5c4750d1fd 100644 --- a/src/pages/Overview/Payouts.tsx +++ b/src/pages/Overview/Payouts.tsx @@ -30,7 +30,7 @@ export const Payouts = () => { }, } = useNetwork(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { plugins } = usePlugins(); const { getData, injectBlockTimestamp } = useSubscanData([ 'payouts', diff --git a/src/pages/Payouts/index.tsx b/src/pages/Payouts/index.tsx index eaeb36ea8d..ea1a94a200 100644 --- a/src/pages/Payouts/index.tsx +++ b/src/pages/Payouts/index.tsx @@ -32,7 +32,7 @@ export const Payouts = ({ page: { key } }: PageProps) => { const { openHelp } = useHelp(); const { plugins } = usePlugins(); const { inSetup } = useStaking(); - const { syncing } = useSyncing('*'); + const { syncing } = useSyncing(); const { getData, injectBlockTimestamp } = useSubscanData([ 'payouts', 'unclaimedPayouts', diff --git a/src/pages/Pools/Create/index.tsx b/src/pages/Pools/Create/index.tsx index af4d3c5f71..75c6f4c576 100644 --- a/src/pages/Pools/Create/index.tsx +++ b/src/pages/Pools/Create/index.tsx @@ -33,6 +33,7 @@ export const Create = () => { iconLeft={faChevronLeft} iconTransform="shrink-3" onClick={() => setOnPoolSetup(false)} + lg /> @@ -42,6 +43,7 @@ export const Create = () => { setOnPoolSetup(false); removeSetupProgress('pool', activeAccount); }} + lg />
diff --git a/src/pages/Pools/Home/ClosurePrompts.tsx b/src/pages/Pools/Home/ClosurePrompts.tsx index 473506c0bd..0adf25705c 100644 --- a/src/pages/Pools/Home/ClosurePrompts.tsx +++ b/src/pages/Pools/Home/ClosurePrompts.tsx @@ -46,6 +46,7 @@ export const ClosurePrompts = () => { active.toNumber() === 0 && totalUnlockChunks === 0 && !targets.length; return ( + state === 'Destroying' && depositorCanClose && ( diff --git a/src/pages/Pools/Home/Status/MembershipStatus.tsx b/src/pages/Pools/Home/Status/MembershipStatus.tsx index b76848427a..59b40f75f6 100644 --- a/src/pages/Pools/Home/Status/MembershipStatus.tsx +++ b/src/pages/Pools/Home/Status/MembershipStatus.tsx @@ -13,22 +13,18 @@ import { useOverlay } from 'kits/Overlay/Provider'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { useStatusButtons } from './useStatusButtons'; -import { useSyncing } from 'hooks/useSyncing'; +import type { MembershipStatusProps } from './types'; export const MembershipStatus = ({ showButtons = true, buttonType = 'primary', -}: { - showButtons?: boolean; - buttonType?: string; -}) => { +}: MembershipStatusProps) => { const { t } = useTranslation('pages'); const { isReady } = useApi(); - const { syncing } = useSyncing('*'); const { openModal } = useOverlay().modal; const { poolsMetaData } = useBondedPools(); const { activeAccount } = useActiveAccounts(); - const { label, buttons } = useStatusButtons(); + const { label } = useStatusButtons(); const { isReadOnlyAccount } = useImportedAccounts(); const { getTransferOptions } = useTransferOptions(); const { activePool, isOwner, isBouncer, isMember } = useActivePool(); @@ -52,18 +48,21 @@ export const MembershipStatus = ({ (poolState !== 'Destroying' && (isOwner() || isBouncer())) || (isMember() && active?.isGreaterThan(0)) ) { - membershipButtons.push({ - title: t('pools.manage'), - icon: faCog, - disabled: !isReady || isReadOnlyAccount(activeAccount), - small: true, - onClick: () => - openModal({ - key: 'ManagePool', - options: { disableWindowResize: true, disableScroll: true }, - size: 'sm', - }), - }); + // Display manage button if active account is not a read-only account. + if (!isReadOnlyAccount(activeAccount)) { + membershipButtons.push({ + title: t('pools.manage'), + icon: faCog, + disabled: !isReady, + small: true, + onClick: () => + openModal({ + key: 'ManagePool', + options: { disableWindowResize: true, disableScroll: true }, + size: 'sm', + }), + }); + } } } @@ -83,7 +82,6 @@ export const MembershipStatus = ({ label={t('pools.poolMembership')} helpKey="Pool Membership" stat={t('pools.notInPool')} - buttons={!showButtons || syncing ? [] : buttons} buttonType={buttonType} /> ); diff --git a/src/pages/Pools/Home/Status/NewMember.tsx b/src/pages/Pools/Home/Status/NewMember.tsx new file mode 100644 index 0000000000..042cb55368 --- /dev/null +++ b/src/pages/Pools/Home/Status/NewMember.tsx @@ -0,0 +1,96 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { CallToActionWrapper } from '../../../../library/CallToAction'; +import { faChevronRight, faUserGroup } from '@fortawesome/free-solid-svg-icons'; +import { useSetup } from 'contexts/Setup'; +import { usePoolsTabs } from '../context'; +import { useStatusButtons } from './useStatusButtons'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from 'kits/Overlay/Provider'; +import type { NewMemberProps } from './types'; +import { CallToActionLoader } from 'library/Loader/CallToAction'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; + +export const NewMember = ({ syncing }: NewMemberProps) => { + const { t } = useTranslation(); + const { setOnPoolSetup } = useSetup(); + const { setActiveTab } = usePoolsTabs(); + const { openCanvas } = useOverlay().canvas; + const { poolRewardPointsFetched } = usePoolPerformance(); + const { disableJoin, disableCreate } = useStatusButtons(); + + const joinButtonDisabled = + disableJoin() || poolRewardPointsFetched !== 'synced'; + + const createButtonDisabled = disableCreate(); + + return ( + +
+ {syncing ? ( + + ) : ( + <> +
+
+
+ +
+ +
+ +
+
+
+
+
+
+ +
+
+
+ + )} +
+
+ ); +}; diff --git a/src/pages/Pools/Home/Status/RewardsStatus.tsx b/src/pages/Pools/Home/Status/RewardsStatus.tsx index d9f5df9885..c2d2e25862 100644 --- a/src/pages/Pools/Home/Status/RewardsStatus.tsx +++ b/src/pages/Pools/Home/Status/RewardsStatus.tsx @@ -14,7 +14,7 @@ import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { useSyncing } from 'hooks/useSyncing'; -export const RewardsStatus = () => { +export const RewardsStatus = ({ dimmed }: { dimmed: boolean }) => { const { t } = useTranslation('pages'); const { networkData: { units }, @@ -34,37 +34,37 @@ export const RewardsStatus = () => { : '0'; // Display Reward buttons if unclaimed rewards is a non-zero value. - const buttonsRewards = pendingPoolRewards.isGreaterThan(minUnclaimedDisplay) - ? [ - { - title: t('pools.withdraw'), - icon: faCircleDown, - disabled: !isReady || isReadOnlyAccount(activeAccount), - small: true, - onClick: () => - openModal({ - key: 'ClaimReward', - options: { claimType: 'withdraw' }, - size: 'sm', - }), - }, - { - title: t('pools.compound'), - icon: faPlus, - disabled: - !isReady || - isReadOnlyAccount(activeAccount) || - activePool?.bondedPool?.state === 'Destroying', - small: true, - onClick: () => - openModal({ - key: 'ClaimReward', - options: { claimType: 'bond' }, - size: 'sm', - }), - }, - ] - : undefined; + const buttonsRewards = isReadOnlyAccount(activeAccount) + ? [] + : pendingPoolRewards.isGreaterThan(minUnclaimedDisplay) + ? [ + { + title: t('pools.withdraw'), + icon: faCircleDown, + disabled: !isReady, + small: true, + onClick: () => + openModal({ + key: 'ClaimReward', + options: { claimType: 'withdraw' }, + size: 'sm', + }), + }, + { + title: t('pools.compound'), + icon: faPlus, + disabled: + !isReady || activePool?.bondedPool?.state === 'Destroying', + small: true, + onClick: () => + openModal({ + key: 'ClaimReward', + options: { claimType: 'bond' }, + size: 'sm', + }), + }, + ] + : undefined; return ( { helpKey="Pool Rewards" type="odometer" stat={{ value: labelRewards }} + dimmed={dimmed} buttons={syncing ? [] : buttonsRewards} /> ); diff --git a/src/pages/Pools/Home/Status/index.tsx b/src/pages/Pools/Home/Status/index.tsx index 119e7eb46f..145d1303fa 100644 --- a/src/pages/Pools/Home/Status/index.tsx +++ b/src/pages/Pools/Home/Status/index.tsx @@ -7,20 +7,43 @@ import { MembershipStatus } from './MembershipStatus'; import { PoolStatus } from './PoolStatus'; import { RewardsStatus } from './RewardsStatus'; import { Separator } from 'kits/Structure/Separator'; +import type { StatusProps } from './types'; +import { NewMember } from './NewMember'; +import { useSyncing } from 'hooks/useSyncing'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useBalances } from 'contexts/Balances'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -export const Status = ({ height }: { height: number }) => { +export const Status = ({ height }: StatusProps) => { const { activePool } = useActivePool(); + const { getPoolMembership } = useBalances(); + const { poolMembersipSyncing } = useSyncing(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + const membership = getPoolMembership(activeAccount); + const syncing = poolMembersipSyncing(); return ( - + - - {activePool && ( - <> - - - + + {!syncing ? ( + activePool && !!membership ? ( + <> + + + + ) : ( + membership === null && + !isReadOnlyAccount(activeAccount) && + ) + ) : ( + )} ); diff --git a/src/pages/Pools/Home/Status/types.ts b/src/pages/Pools/Home/Status/types.ts new file mode 100644 index 0000000000..dc15b67805 --- /dev/null +++ b/src/pages/Pools/Home/Status/types.ts @@ -0,0 +1,15 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface StatusProps { + height: number; +} + +export interface MembershipStatusProps { + showButtons?: boolean; + buttonType?: string; +} + +export interface NewMemberProps { + syncing: boolean; +} diff --git a/src/pages/Pools/Home/Status/useStatusButtons.tsx b/src/pages/Pools/Home/Status/useStatusButtons.tsx index 9ddc1c7adb..53b6ee85d1 100644 --- a/src/pages/Pools/Home/Status/useStatusButtons.tsx +++ b/src/pages/Pools/Home/Status/useStatusButtons.tsx @@ -1,16 +1,13 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faPlusCircle, faUserPlus } from '@fortawesome/free-solid-svg-icons'; import { useTranslation } from 'react-i18next'; import { useApi } from 'contexts/Api'; import { useActivePool } from 'contexts/Pools/ActivePool'; import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useSetup } from 'contexts/Setup'; import { useTransferOptions } from 'contexts/TransferOptions'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -import { usePoolsTabs } from '../context'; import { useBalances } from 'contexts/Balances'; export const useStatusButtons = () => { @@ -20,17 +17,14 @@ export const useStatusButtons = () => { poolsConfig: { maxPools }, } = useApi(); const { isOwner } = useActivePool(); - const { setActiveTab } = usePoolsTabs(); const { bondedPools } = useBondedPools(); const { getPoolMembership } = useBalances(); const { activeAccount } = useActiveAccounts(); const { getTransferOptions } = useTransferOptions(); const { isReadOnlyAccount } = useImportedAccounts(); - const { setOnPoolSetup, getPoolSetupPercent } = useSetup(); const membership = getPoolMembership(activeAccount); const { active } = getTransferOptions(activeAccount).pool; - const poolSetupPercent = getPoolSetupPercent(activeAccount); const disableCreate = () => { if (!isReady || isReadOnlyAccount(activeAccount) || !activeAccount) { @@ -46,34 +40,15 @@ export const useStatusButtons = () => { }; let label; - let buttons; - const createBtn = { - title: `${t('pools.create')}${ - poolSetupPercent > 0 ? `: ${poolSetupPercent}%` : `` - }`, - icon: faPlusCircle, - large: false, - transform: 'grow-1', - disabled: disableCreate(), - onClick: () => setOnPoolSetup(true), - }; - const joinPoolBtn = { - title: `${t('pools.join')}`, - icon: faUserPlus, - large: false, - transform: 'grow-1', - disabled: - !isReady || - isReadOnlyAccount(activeAccount) || - !activeAccount || - !bondedPools.length, - onClick: () => setActiveTab(1), - }; + const disableJoin = () => + !isReady || + isReadOnlyAccount(activeAccount) || + !activeAccount || + !bondedPools.length; if (!membership) { label = t('pools.poolMembership'); - buttons = [createBtn, joinPoolBtn]; } else if (isOwner()) { label = `${t('pools.ownerOfPool')} ${membership.poolId}`; } else if (active?.isGreaterThan(0)) { @@ -81,5 +56,5 @@ export const useStatusButtons = () => { } else { label = `${t('pools.leavingPool')} ${membership.poolId}`; } - return { label, buttons }; + return { label, disableJoin, disableCreate }; }; diff --git a/src/pages/Pools/Home/index.tsx b/src/pages/Pools/Home/index.tsx index 7a347df83f..590a49af35 100644 --- a/src/pages/Pools/Home/index.tsx +++ b/src/pages/Pools/Home/index.tsx @@ -31,9 +31,11 @@ import type { PageTitleTabProps } from 'kits/Structure/PageTitleTabs/types'; import { PageRow } from 'kits/Structure/PageRow'; import { RowSection } from 'kits/Structure/RowSection'; import { WithdrawPrompt } from 'library/WithdrawPrompt'; +import { useSyncing } from 'hooks/useSyncing'; export const HomeInner = () => { const { t } = useTranslation('pages'); + const { poolMembersipSyncing } = useSyncing(); const { favorites } = useFavoritePools(); const { openModal } = useOverlay().modal; const { bondedPools } = useBondedPools(); @@ -42,16 +44,16 @@ export const HomeInner = () => { const { activeTab, setActiveTab } = usePoolsTabs(); const { getPoolRoles, activePool } = useActivePool(); const { counterForBondedPools } = useApi().poolsConfig; - const membership = getPoolMembership(activeAccount); - const { state } = activePool?.bondedPool || {}; const { activePools } = useActivePools({ poolIds: '*', }); - const activePoolsNoMembership = { ...activePools }; - delete activePoolsNoMembership[membership?.poolId || -1]; + // Calculate the number of _other_ pools the user has a role in. + const poolRoleCount = Object.keys(activePools).filter( + (poolId) => poolId !== String(membership?.poolId) + ).length; let tabs: PageTitleTabProps[] = [ { @@ -76,6 +78,8 @@ export const HomeInner = () => { } ); + const ROW_HEIGHT = 220; + // Back to tab 0 if not in a pool & on members tab. useEffect(() => { if (!activePool) { @@ -83,15 +87,13 @@ export const HomeInner = () => { } }, [activePool]); - const ROW_HEIGHT = 220; - return ( <> 0 + !poolMembersipSyncing() && poolRoleCount > 0 ? { title: t('pools.allRoles'), onClick: () => @@ -111,21 +113,18 @@ export const HomeInner = () => { - {state === 'Destroying' ? ( - - ) : ( - - )} + + - - - - + + + + {activePool !== null && ( <> diff --git a/src/theme/theme.scss b/src/theme/theme.scss index aab7daf572..4e46e8af83 100644 --- a/src/theme/theme.scss +++ b/src/theme/theme.scss @@ -9,6 +9,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --background-list-item: rgb(238 238 238 / 100%); --background-modal-card: rgb(237 237 237 / 75%); --background-canvas-card: rgb(245 245 245 / 90%); + --background-canvas-card-secondary: rgb(255 255 255 / 35%); --background-floating-card: rgb(255 255 255 / 90%); --background-app-footer: rgb(244 225 225 / 75%); --background-warning: #fdf9eb; @@ -23,6 +24,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --button-secondary-background: #e7e5e5; --button-tertiary-background: #ececec; --button-tab-background: #e4e2e2; + --button-tab-canvas-background: #d9d9d9; --button-hover-background: #e8e6e6; --card-shadow-color: rgb(158 158 158 / 20%); --card-deep-shadow-color: rgb(158 158 158 / 50%); @@ -44,7 +46,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --status-danger-color: #ae2324; --status-danger-color-transparent: rgb(255 0 0 / 25%); --text-color-primary: #3f3f3f; - --text-color-secondary: #555; + --text-color-secondary: #585858; --text-color-tertiary: #888; --text-color-invert: #fafafa; --gradient-background: linear-gradient( @@ -86,6 +88,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --background-list-item: rgb(38 33 38 / 100%); --background-modal-card: rgb(32 26 32 / 50%); --background-canvas-card: rgb(44 40 44 / 90%); + --background-canvas-card-secondary: rgb(44 40 44 / 35%); --background-floating-card: rgb(43 38 43 / 95%); --background-app-footer: #262327; --background-warning: #33332a; @@ -100,6 +103,7 @@ SPDX-License-Identifier: GPL-3.0-only */ --button-secondary-background: rgb(55 50 55); --button-tertiary-background: rgb(54 49 54); --button-tab-background: rgb(56 51 56); + --button-tab-canvas-background: rgb(60 59 60); --button-hover-background: rgb(66 61 68); --card-shadow-color: rgb(28 24 28 / 25%); --card-deep-shadow-color: rgb(28 24 28 / 50%); diff --git a/src/types.ts b/src/types.ts index 9289752501..77fb00fdfd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -140,7 +140,7 @@ export type Sync = 'unsynced' | 'syncing' | 'synced'; export type BondFor = 'pool' | 'nominator'; // which medium components are being displayed on. -export type DisplayFor = 'default' | 'modal' | 'canvas'; +export type DisplayFor = 'default' | 'modal' | 'canvas' | 'card'; // generic function with no args or return type. export type Fn = () => void; From b536faf8fc410c8291dea84fa2b96189ab2c8e76 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Tue, 2 Apr 2024 19:08:34 +0700 Subject: [PATCH 13/37] feat: use `bondedPool.memberCounter`, deprecate `nomination_pool/pool` Subscan call (#2054) --- src/canvas/PoolMembers/Lists/FetchPage.tsx | 2 +- src/canvas/PoolMembers/Lists/types.ts | 2 +- src/canvas/PoolMembers/Members.tsx | 7 ++-- src/contexts/Pools/ActivePool/defaults.ts | 1 - src/contexts/Pools/ActivePool/index.tsx | 49 +--------------------- src/contexts/Pools/ActivePool/types.ts | 1 - src/controllers/SubscanController/index.ts | 31 +------------- src/controllers/SubscanController/types.ts | 7 +--- src/pages/Pools/Home/PoolStats/index.tsx | 8 ++-- 9 files changed, 13 insertions(+), 95 deletions(-) diff --git a/src/canvas/PoolMembers/Lists/FetchPage.tsx b/src/canvas/PoolMembers/Lists/FetchPage.tsx index 22018ef3a0..0f56e0f7ea 100644 --- a/src/canvas/PoolMembers/Lists/FetchPage.tsx +++ b/src/canvas/PoolMembers/Lists/FetchPage.tsx @@ -51,7 +51,7 @@ export const MembersListInner = ({ }; // pagination - const totalPages = Math.ceil(memberCount / listItemsPerPage); + const totalPages = Math.ceil(Number(memberCount) / listItemsPerPage); const pageEnd = listItemsPerPage - 1; const pageStart = pageEnd - (listItemsPerPage - 1); diff --git a/src/canvas/PoolMembers/Lists/types.ts b/src/canvas/PoolMembers/Lists/types.ts index 4afd4dcd56..c6f74832ef 100644 --- a/src/canvas/PoolMembers/Lists/types.ts +++ b/src/canvas/PoolMembers/Lists/types.ts @@ -15,5 +15,5 @@ export type DefaultMembersListProps = MembersListProps & { }; export type FetchpageMembersListProps = MembersListProps & { - memberCount: number; + memberCount: string; }; diff --git a/src/canvas/PoolMembers/Members.tsx b/src/canvas/PoolMembers/Members.tsx index 4e6bf6e8ee..de96654816 100644 --- a/src/canvas/PoolMembers/Members.tsx +++ b/src/canvas/PoolMembers/Members.tsx @@ -18,8 +18,7 @@ export const Members = () => { const { mode } = useTheme(); const { pluginEnabled } = usePlugins(); const { getMembersOfPoolFromNode } = usePoolMembers(); - const { activePool, isOwner, isBouncer, activePoolMemberCount } = - useActivePool(); + const { activePool, isOwner, isBouncer } = useActivePool(); const { colors } = useNetwork().networkData; const annuncementBorderColor = colors.secondary[mode]; @@ -27,6 +26,8 @@ export const Members = () => { const showBlockedPrompt = activePool?.bondedPool?.state === 'Blocked' && (isOwner() || isBouncer()); + const memberCount = activePool?.bondedPool?.memberCounter ?? '0'; + const membersListProps = { batchKey: 'active_pool_members', pagination: true, @@ -80,7 +81,7 @@ export const Members = () => { {pluginEnabled('subscan') ? ( ) : ( {}, activePool: null, activePoolNominations: null, - activePoolMemberCount: 0, pendingPoolRewards: new BigNumber(0), }; diff --git a/src/contexts/Pools/ActivePool/index.tsx b/src/contexts/Pools/ActivePool/index.tsx index b8904850af..6d360ce834 100644 --- a/src/contexts/Pools/ActivePool/index.tsx +++ b/src/contexts/Pools/ActivePool/index.tsx @@ -11,16 +11,12 @@ import { useRef, useState, } from 'react'; -import type { Sync } from 'types'; import { useEffectIgnoreInitial } from '@w3ux/hooks'; -import { usePlugins } from 'contexts/Plugins'; import { useNetwork } from 'contexts/Network'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useApi } from '../../Api'; import { useBondedPools } from '../BondedPools'; -import { usePoolMembers } from '../PoolMembers'; import type { ActivePoolContextState } from './types'; -import { SubscanController } from 'controllers/SubscanController'; import { useCreatePoolAccounts } from 'hooks/useCreatePoolAccounts'; import { useBalances } from 'contexts/Balances'; import { ActivePoolsController } from 'controllers/ActivePoolsController'; @@ -38,12 +34,11 @@ export const useActivePool = () => useContext(ActivePoolContext); export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { const { network } = useNetwork(); const { isReady, api } = useApi(); - const { pluginEnabled } = usePlugins(); const { getPoolMembership } = useBalances(); const { activeAccount } = useActiveAccounts(); const createPoolAccounts = useCreatePoolAccounts(); - const { getMembersOfPoolFromNode } = usePoolMembers(); const { getAccountPoolRoles, bondedPools } = useBondedPools(); + const membership = getPoolMembership(activeAccount); // Determine active pools to subscribe to. Dependencies of `activeAccount`, and `membership` mean @@ -103,12 +98,6 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { ? poolNominations[activePoolId] : null; - // Store the member count of the selected pool. - const [activePoolMemberCount, setactivePoolMemberCount] = useState(0); - - // Keep track of whether the pool member count is being fetched. - const fetchingMemberCount = useRef('unsynced'); - // Sync active pool subscriptions. const syncActivePoolSubscriptions = async () => { if (api && accountPoolIds.length) { @@ -230,41 +219,6 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { return new BigNumber(0); }; - // Gets the member count of the currently selected pool. If Subscan is enabled, it is used instead of the connected node. - const getMemberCount = async () => { - if (!activePool?.id) { - setactivePoolMemberCount(0); - return; - } - // If `Subscan` plugin is enabled, fetch member count directly from the API. - if ( - pluginEnabled('subscan') && - fetchingMemberCount.current === 'unsynced' - ) { - fetchingMemberCount.current = 'syncing'; - const poolDetails = await SubscanController.handleFetchPoolDetails( - activePool.id - ); - fetchingMemberCount.current = 'synced'; - setactivePoolMemberCount(poolDetails?.member_count || 0); - return; - } - // If no plugin available, fetch all pool members from RPC and filter them to determine current - // pool member count. NOTE: Expensive operation. - setactivePoolMemberCount( - getMembersOfPoolFromNode(activePool?.id || 0)?.length || 0 - ); - }; - - // Fetch pool member count. We use `membership` as a dependency as the member count could change - // in the UI when active account's membership changes. NOTE: Do not have `poolMembersNode` as a - // dependency - could trigger many re-renders if value is constantly changing - more suited as a - // custom event. - useEffect(() => { - fetchingMemberCount.current = 'unsynced'; - getMemberCount(); - }, [activeAccount, activePool, membership?.poolId]); - // Re-calculate pending rewards when membership changes. useEffectIgnoreInitial(() => { if (isReady) { @@ -308,7 +262,6 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { getPoolRoles, setActivePoolId, activePool, - activePoolMemberCount, activePoolNominations, pendingPoolRewards, }} diff --git a/src/contexts/Pools/ActivePool/types.ts b/src/contexts/Pools/ActivePool/types.ts index bf30385589..1ef0b32509 100644 --- a/src/contexts/Pools/ActivePool/types.ts +++ b/src/contexts/Pools/ActivePool/types.ts @@ -19,7 +19,6 @@ export interface ActivePoolContextState { setActivePoolId: (p: string) => void; activePool: ActivePool | null; activePoolNominations: Nominations | null; - activePoolMemberCount: number; pendingPoolRewards: BigNumber; } diff --git a/src/controllers/SubscanController/index.ts b/src/controllers/SubscanController/index.ts index a83d14c99a..4d30175e9c 100644 --- a/src/controllers/SubscanController/index.ts +++ b/src/controllers/SubscanController/index.ts @@ -5,7 +5,6 @@ import type { SubscanPoolClaim, SubscanData, SubscanPayout, - SubscanPoolDetails, SubscanPoolMember, SubscanRequestBody, SubscanEraPoints, @@ -26,7 +25,6 @@ export class SubscanController { // List of endpoints to be used for Subscan API calls. static ENDPOINTS = { eraStat: '/api/scan/staking/era_stat', - poolDetails: '/api/scan/nomination_pool/pool', poolMembers: '/api/scan/nomination_pool/pool/members', poolRewards: '/api/scan/nomination_pool/rewards', rewardSlash: '/api/v2/scan/account/reward_slash', @@ -45,7 +43,7 @@ export class SubscanController { static payoutData: Record = {}; // Subscan pool data, keyed by `---...`. - static poolData: Record = {}; + static poolData: Record = {}; // Subscan era points data, keyed by `-
-`. static eraPointsData: Record = {}; @@ -178,19 +176,6 @@ export class SubscanController { .splice(0, result.list.length - 1); }; - // Fetch a pool's details from Subscan. - static fetchPoolDetails = async ( - poolId: number - ): Promise => { - const result = await this.makeRequest(this.ENDPOINTS.poolDetails, { - pool_id: poolId, - }); - if (!result) { - return { member_count: 0 }; - } - return { member_count: result.member_count }; - }; - // Fetch a pool's era points from Subscan. static fetchEraPoints = async ( address: string, @@ -235,20 +220,6 @@ export class SubscanController { } }; - // Handle fetching pool details. - static handleFetchPoolDetails = async (poolId: number) => { - const dataKey = `${this.network}-${poolId}-details}`; - const currentValue = this.poolData[dataKey]; - - if (currentValue) { - return currentValue as SubscanPoolDetails; - } else { - const result = await this.fetchPoolDetails(poolId); - this.poolData[dataKey] = result; - return result; - } - }; - // Handle fetching era point history. static handleFetchEraPoints = async (address: string, era: number) => { const dataKey = `${this.network}-${address}-${era}}`; diff --git a/src/controllers/SubscanController/types.ts b/src/controllers/SubscanController/types.ts index a436a3a65a..02d11191b0 100644 --- a/src/controllers/SubscanController/types.ts +++ b/src/controllers/SubscanController/types.ts @@ -39,8 +39,7 @@ export interface SubscanRequestPagination { export type SubscanResult = | SubscanPayout[] | SubscanPoolClaim[] - | SubscanPoolMember[] - | SubscanPoolDetails; + | SubscanPoolMember[]; export interface SubscanPoolClaim { account_display: { @@ -89,10 +88,6 @@ export interface SubscanPoolMember { claimable: string; } -export interface SubscanPoolDetails { - member_count: number; -} - export interface SubscanEraPoints { era: number; reward_point: number; diff --git a/src/pages/Pools/Home/PoolStats/index.tsx b/src/pages/Pools/Home/PoolStats/index.tsx index d9861e2dca..9f66c2afec 100644 --- a/src/pages/Pools/Home/PoolStats/index.tsx +++ b/src/pages/Pools/Home/PoolStats/index.tsx @@ -20,12 +20,12 @@ export const PoolStats = () => { const { networkData: { units, unit }, } = useNetwork(); + const { activePool } = useActivePool(); const { getCurrentCommission } = usePoolCommission(); - const { activePool, activePoolMemberCount } = useActivePool(); const poolId = activePool?.id || 0; - const { state, points } = activePool?.bondedPool || {}; + const { state, points, memberCounter } = activePool?.bondedPool || {}; const currentCommission = getCurrentCommission(poolId); const bonded = planckToUnit( @@ -65,13 +65,13 @@ export const PoolStats = () => { items.push( { label: t('pools.poolMembers'), - value: `${activePoolMemberCount}`, + value: `${memberCounter}`, button: { text: t('pools.browseMembers'), onClick: () => { openCanvas({ key: 'PoolMembers', size: 'xl' }); }, - disabled: activePoolMemberCount === 0, + disabled: memberCounter === '0', }, }, { From bf16d80a661ca1d1cd0cf038bcff4525fbff19c8 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Tue, 2 Apr 2024 22:35:16 +0700 Subject: [PATCH 14/37] feat(ux): use secondary accent color for status UI (#2055) --- src/canvas/JoinPool/Wrappers.ts | 4 ++-- src/library/Card/Wrappers.ts | 2 +- src/library/Form/Warning/Wrapper.ts | 13 +++++++------ src/library/Form/Warning/index.tsx | 7 +------ src/library/Pool/Rewards.tsx | 4 ++-- src/library/SideMenu/Primary/Wrappers.ts | 8 ++++---- src/theme/theme.scss | 2 -- 7 files changed, 17 insertions(+), 23 deletions(-) diff --git a/src/canvas/JoinPool/Wrappers.ts b/src/canvas/JoinPool/Wrappers.ts index d1d76432d3..adc3f9bf39 100644 --- a/src/canvas/JoinPool/Wrappers.ts +++ b/src/canvas/JoinPool/Wrappers.ts @@ -118,8 +118,8 @@ export const TitleWrapper = styled.div` font-size: 1.1rem; &.blocked { - color: var(--status-warning-color); - border-color: var(--status-warning-color); + color: var(--accent-color-secondary); + border-color: var(--accent-color-secondary); } &.destroying { diff --git a/src/library/Card/Wrappers.ts b/src/library/Card/Wrappers.ts index a63ebf9f65..c06f80a5f3 100644 --- a/src/library/Card/Wrappers.ts +++ b/src/library/Card/Wrappers.ts @@ -112,7 +112,7 @@ export const CardWrapper = styled.div` } &.warning { - border: 1px solid var(--status-warning-color); + border: 1px solid var(--accent-color-secondary); } &.prompt { diff --git a/src/library/Form/Warning/Wrapper.ts b/src/library/Form/Warning/Wrapper.ts index 43da4488f8..973a61ab0f 100644 --- a/src/library/Form/Warning/Wrapper.ts +++ b/src/library/Form/Warning/Wrapper.ts @@ -4,10 +4,10 @@ import styled from 'styled-components'; export const Wrapper = styled.div` - background: var(--background-warning); - border: 1px solid var(--status-warning-color-transparent); + background: var(--button-primary-background); + border: 1px solid var(--accent-color-secondary); margin: 0.5rem 0; - padding: 0.75rem 0.75rem; + padding: 0.6rem 0.9rem; border-radius: 0.75rem; display: flex; flex-flow: row wrap; @@ -15,11 +15,12 @@ export const Wrapper = styled.div` width: 100%; > h4 { - color: var(--status-warning-color); + color: var(--accent-color-secondary); + font-family: Inter, sans-serif; .icon { - color: var(--status-warning-color); - margin-right: 0.6rem; + color: var(--accent-color-secondary); + margin-right: 0.5rem; } } `; diff --git a/src/library/Form/Warning/index.tsx b/src/library/Form/Warning/index.tsx index 3575164094..c9afb6fd4f 100644 --- a/src/library/Form/Warning/index.tsx +++ b/src/library/Form/Warning/index.tsx @@ -1,16 +1,11 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import type { WarningProps } from '../types'; import { Wrapper } from './Wrapper'; export const Warning = ({ text }: WarningProps) => ( -

- - {text} -

+

{text}

); diff --git a/src/library/Pool/Rewards.tsx b/src/library/Pool/Rewards.tsx index f461ac0b71..18bd3f9bae 100644 --- a/src/library/Pool/Rewards.tsx +++ b/src/library/Pool/Rewards.tsx @@ -129,7 +129,7 @@ export const RewardsGraph = ({ points = [], syncing }: RewardsGraphProps) => { key={`line_coord_${index}`} strokeWidth={5} opacity={1} - stroke="var(--accent-color-tertiary)" + stroke="var(--accent-color-transparent)" x1={x1} y1={y1} x2={x2} @@ -146,7 +146,7 @@ export const RewardsGraph = ({ points = [], syncing }: RewardsGraphProps) => { stroke={ zero ? 'var(--text-color-tertiary)' - : 'var(--accent-color-secondary)' + : 'var(--accent-color-primary)' } x1={x1} y1={y1} diff --git a/src/library/SideMenu/Primary/Wrappers.ts b/src/library/SideMenu/Primary/Wrappers.ts index 083e558b53..7fc18bdb2d 100644 --- a/src/library/SideMenu/Primary/Wrappers.ts +++ b/src/library/SideMenu/Primary/Wrappers.ts @@ -27,7 +27,7 @@ export const Wrapper = styled(motion.div)` border: 1px solid var(--accent-color-primary); } &.warning { - border: 1px solid var(--status-warning-color); + border: 1px solid var(--accent-color-secondary); } } @@ -68,8 +68,8 @@ export const Wrapper = styled(motion.div)` border: 1px solid var(--accent-color-primary); } &.warning { - color: var(--status-warning-color); - border: 1px solid var(--status-warning-color-transparent); + color: var(--accent-color-secondary); + border: 1px solid var(--accent-color-secondary); } border-radius: 0.5rem; padding: 0.15rem 0.5rem; @@ -82,7 +82,7 @@ export const Wrapper = styled(motion.div)` } &.warning { svg { - color: var(--status-warning-color); + color: var(--accent-color-secondary); } } &.minimised { diff --git a/src/theme/theme.scss b/src/theme/theme.scss index 4e46e8af83..c931951bba 100644 --- a/src/theme/theme.scss +++ b/src/theme/theme.scss @@ -12,7 +12,6 @@ SPDX-License-Identifier: GPL-3.0-only */ --background-canvas-card-secondary: rgb(255 255 255 / 35%); --background-floating-card: rgb(255 255 255 / 90%); --background-app-footer: rgb(244 225 225 / 75%); - --background-warning: #fdf9eb; --background-modal: #f9f7f7; --background-modal-footer: #efefef; --background-status-overlay: rgb(255 255 255 / 85%); @@ -91,7 +90,6 @@ SPDX-License-Identifier: GPL-3.0-only */ --background-canvas-card-secondary: rgb(44 40 44 / 35%); --background-floating-card: rgb(43 38 43 / 95%); --background-app-footer: #262327; - --background-warning: #33332a; --background-modal: rgb(43 38 43); --background-modal-footer: rgb(37 32 37); --background-status-overlay: rgb(43 38 43 / 75%); From c4749c6e7ca338a9f3fd3299ebb53bbf45c3de07 Mon Sep 17 00:00:00 2001 From: frankli-dev <51565705+frankli-dev@users.noreply.github.com> Date: Wed, 3 Apr 2024 00:36:55 -0600 Subject: [PATCH 15/37] fix: Search bar initial value on Pools/Validators page (#2032) --- src/library/List/SearchInput.tsx | 2 ++ src/library/List/types.ts | 1 + src/library/PoolList/Default.tsx | 1 + src/library/ValidatorList/index.tsx | 1 + 4 files changed, 5 insertions(+) diff --git a/src/library/List/SearchInput.tsx b/src/library/List/SearchInput.tsx index d6a2c414f1..b66f0f8a02 100644 --- a/src/library/List/SearchInput.tsx +++ b/src/library/List/SearchInput.tsx @@ -6,12 +6,14 @@ import { SearchInputWrapper } from '.'; import type { SearchInputProps } from './types'; export const SearchInput = ({ + value, handleChange, placeholder, }: SearchInputProps) => ( ) => handleChange(e)} diff --git a/src/library/List/types.ts b/src/library/List/types.ts index a4823568e7..5daa5200c2 100644 --- a/src/library/List/types.ts +++ b/src/library/List/types.ts @@ -22,6 +22,7 @@ export interface PaginationProps { } export interface SearchInputProps { + value: string; handleChange: (e: FormEvent) => void; placeholder: string; } diff --git a/src/library/PoolList/Default.tsx b/src/library/PoolList/Default.tsx index 634f7db58f..a281d147fb 100644 --- a/src/library/PoolList/Default.tsx +++ b/src/library/PoolList/Default.tsx @@ -183,6 +183,7 @@ export const PoolList = ({ {allowSearch && poolsDefault.length > 0 && ( diff --git a/src/library/ValidatorList/index.tsx b/src/library/ValidatorList/index.tsx index 691989594c..807ea03a2d 100644 --- a/src/library/ValidatorList/index.tsx +++ b/src/library/ValidatorList/index.tsx @@ -333,6 +333,7 @@ export const ValidatorListInner = ({ {allowSearch && ( From 965d3e182c77e0b6d46c2d1c603e74a30cd7be92 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Thu, 4 Apr 2024 15:14:30 +0700 Subject: [PATCH 16/37] feat: Pool performance data to batches. Per-page fetching, pool join subset. (#2057) Co-authored-by: Ting A Lin --- src/Providers.tsx | 2 + src/canvas/JoinPool/Header.tsx | 11 +- .../JoinPool/Overview/PerformanceGraph.tsx | 13 +- src/canvas/JoinPool/index.tsx | 50 ++-- src/canvas/JoinPool/types.ts | 1 - src/canvas/PoolMembers/Lists/Default.tsx | 38 +-- src/canvas/PoolMembers/Lists/FetchPage.tsx | 36 +-- src/canvas/PoolMembers/Lists/types.ts | 1 - src/consts.ts | 2 +- src/contexts/Filters/defaults.ts | 16 +- src/contexts/Filters/index.tsx | 10 +- src/contexts/Pools/BondedPools/defaults.ts | 4 +- src/contexts/Pools/BondedPools/index.tsx | 6 + src/contexts/Pools/BondedPools/types.ts | 7 +- src/contexts/Pools/JoinPools/defaults.ts | 12 + src/contexts/Pools/JoinPools/index.tsx | 69 +++++ src/contexts/Pools/JoinPools/types.ts | 9 + .../Pools/PoolPerformance/defaults.ts | 22 +- src/contexts/Pools/PoolPerformance/index.tsx | 255 +++++++++++++----- src/contexts/Pools/PoolPerformance/types.ts | 59 +++- src/controllers/SubscanController/index.ts | 4 +- src/kits/Structure/Tx/Signer.tsx | 2 +- src/kits/Structure/Tx/Wrapper.ts | 12 +- src/library/CallToAction/index.tsx | 78 +++++- src/library/Filter/Tabs.tsx | 56 ++-- src/library/Filter/types.ts | 1 - src/library/List/defaults.ts | 14 +- src/library/Nominations/index.tsx | 1 - src/library/Pool/Rewards.tsx | 7 +- .../PoolList/{Default.tsx => index.tsx} | 138 ++++------ src/library/PoolList/types.ts | 5 - .../ValidatorList/ValidatorItem/Utils.tsx | 2 +- src/library/ValidatorList/index.tsx | 46 +--- src/library/ValidatorList/types.ts | 1 - src/locale/cn/library.json | 3 +- src/locale/cn/pages.json | 1 - src/locale/en/library.json | 3 +- src/locale/en/pages.json | 1 - src/pages/Payouts/PayoutList/index.tsx | 42 +-- src/pages/Payouts/types.ts | 1 - src/pages/Pools/Home/Favorites/index.tsx | 2 +- .../Pools/Home/Status/FindingPoolPercent.tsx | 30 +++ src/pages/Pools/Home/Status/NewMember.tsx | 84 +++--- .../Pools/Home/Status/useStatusButtons.tsx | 6 +- src/pages/Pools/Home/index.tsx | 21 +- src/theme/accents/polkadot-relay.css | 2 +- src/workers/poolPerformance.ts | 58 ++-- 47 files changed, 785 insertions(+), 459 deletions(-) create mode 100644 src/contexts/Pools/JoinPools/defaults.ts create mode 100644 src/contexts/Pools/JoinPools/index.tsx create mode 100644 src/contexts/Pools/JoinPools/types.ts rename src/library/PoolList/{Default.tsx => index.tsx} (70%) create mode 100644 src/pages/Pools/Home/Status/FindingPoolPercent.tsx diff --git a/src/Providers.tsx b/src/Providers.tsx index 53f0b9b206..b76e4e3d55 100644 --- a/src/Providers.tsx +++ b/src/Providers.tsx @@ -44,6 +44,7 @@ import type { Provider } from 'hooks/withProviders'; import { withProviders } from 'hooks/withProviders'; import { CommunityProvider } from 'contexts/Community'; import { OverlayProvider } from 'kits/Overlay/Provider'; +import { JoinPoolsProvider } from 'contexts/Pools/JoinPools'; export const Providers = () => { const { @@ -83,6 +84,7 @@ export const Providers = () => { FastUnstakeProvider, PayoutsProvider, PoolPerformanceProvider, + JoinPoolsProvider, SetupProvider, MenuProvider, TooltipProvider, diff --git a/src/canvas/JoinPool/Header.tsx b/src/canvas/JoinPool/Header.tsx index 0215f53d48..4b17920a72 100644 --- a/src/canvas/JoinPool/Header.tsx +++ b/src/canvas/JoinPool/Header.tsx @@ -25,19 +25,20 @@ export const Header = ({ autoSelected, setActiveTab, setSelectedPoolId, - setSelectedPoolCount, }: JoinPoolHeaderProps) => { const { t } = useTranslation(); const { closeCanvas } = useOverlay().canvas; // Randomly select a new pool to display. const handleChooseNewPool = () => { - // Trigger refresh of memoied selected bonded pool. - setSelectedPoolCount((prev: number) => prev + 1); + // Remove current pool from filtered so it is not selected again. + const filteredPools = filteredBondedPools.filter( + (pool) => String(pool.id) !== String(bondedPool.id) + ); // Randomly select a filtered bonded pool and set it as the selected pool. - const index = Math.ceil(Math.random() * filteredBondedPools.length - 1); - setSelectedPoolId(filteredBondedPools[index].id); + const index = Math.ceil(Math.random() * filteredPools.length - 1); + setSelectedPoolId(filteredPools[index].id); }; return ( diff --git a/src/canvas/JoinPool/Overview/PerformanceGraph.tsx b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx index 8e551ed3d0..f2562b61ee 100644 --- a/src/canvas/JoinPool/Overview/PerformanceGraph.tsx +++ b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx @@ -14,7 +14,7 @@ import { } from 'chart.js'; import { useNetwork } from 'contexts/Network'; import { GraphWrapper, HeadingWrapper } from '../Wrappers'; -import { Bar } from 'react-chartjs-2'; +import { Line } from 'react-chartjs-2'; import BigNumber from 'bignumber.js'; import type { AnyJson } from 'types'; import { graphColors } from 'theme/graphs'; @@ -44,7 +44,9 @@ export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { const { mode } = useTheme(); const { openHelp } = useHelp(); const { colors } = useNetwork().networkData; - const { poolRewardPoints } = usePoolPerformance(); + const { getPoolRewardPoints } = usePoolPerformance(); + + const poolRewardPoints = getPoolRewardPoints('pool_join'); const rawEraRewardPoints = poolRewardPoints[bondedPool.addresses.stash] || {}; // Ref to the graph container. @@ -100,6 +102,7 @@ export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { }, y: { stacked: true, + beginAtZero: true, ticks: { font: { size: 10, @@ -133,6 +136,10 @@ export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { label: (context: AnyJson) => `${new BigNumber(context.parsed.y).decimalPlaces(0).toFormat()} ${t('eraPoints', { ns: 'library' })}`, }, + intersect: false, + interaction: { + mode: 'nearest', + }, }, }, }; @@ -172,7 +179,7 @@ export const PerformanceGraph = ({ bondedPool }: OverviewSectionProps) => { height, }} > - +
diff --git a/src/canvas/JoinPool/index.tsx b/src/canvas/JoinPool/index.tsx index 7f31fbb023..94d908ab6b 100644 --- a/src/canvas/JoinPool/index.tsx +++ b/src/canvas/JoinPool/index.tsx @@ -5,13 +5,14 @@ import { CanvasFullScreenWrapper } from 'canvas/Wrappers'; import { useOverlay } from 'kits/Overlay/Provider'; import { JoinPoolInterfaceWrapper } from './Wrappers'; import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Header } from './Header'; import { Overview } from './Overview'; import { Nominations } from './Nominations'; import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; import { MaxEraRewardPointsEras } from 'consts'; import { useStaking } from 'contexts/Staking'; +import { useJoinPools } from 'contexts/Pools/JoinPools'; export const JoinPool = () => { const { @@ -19,21 +20,20 @@ export const JoinPool = () => { config: { options }, } = useOverlay().canvas; const { eraStakers } = useStaking(); - const { poolRewardPoints } = usePoolPerformance(); + const { poolsForJoin } = useJoinPools(); + const { getPoolRewardPoints } = usePoolPerformance(); const { poolsMetaData, bondedPools } = useBondedPools(); + const poolRewardPoints = getPoolRewardPoints('pool_join'); // The active canvas tab. const [activeTab, setActiveTab] = useState(0); - // Trigger re-render when chosen selected pool is incremented. - const [selectedPoolCount, setSelectedPoolCount] = useState(0); - // Filter bonded pools to only those that are open and that have active daily rewards for the last // `MaxEraRewardPointsEras` eras. The second filter checks if the pool is in `eraStakers` for the // active era. const filteredBondedPools = useMemo( () => - bondedPools + poolsForJoin .filter((pool) => { // Fetch reward point data for the pool. const rawEraRewardPoints = @@ -53,28 +53,39 @@ export const JoinPool = () => { staker.others.find(({ who }) => who !== pool.addresses.stash) ) ), - [bondedPools, poolRewardPoints] + [poolsForJoin, poolRewardPoints] ); - // The bonded pool to display. Use the provided `poolId`, or assign a random eligible filtered - // pool otherwise. Re-fetches when the selected pool count is incremented. - const bondedPool = useMemo( + const initialSelectedPoolId = useMemo( () => - options?.poolId - ? bondedPools.find(({ id }) => id === options.poolId) - : filteredBondedPools[ - (filteredBondedPools.length * Math.random()) << 0 - ], - [selectedPoolCount] + options?.poolId || + filteredBondedPools[(filteredBondedPools.length * Math.random()) << 0] + .id || + 0, + [] ); - // The selected bonded pool id. + // The selected bonded pool id. Assigns a random id if one is not provided. const [selectedPoolId, setSelectedPoolId] = useState( - bondedPool?.id || 0 + initialSelectedPoolId + ); + + // The bonded pool to display. Use the provided `poolId`, or assign a random eligible filtered + // pool otherwise. Re-fetches when the selected pool count is incremented. + const bondedPool = useMemo( + () => bondedPools.find(({ id }) => id === selectedPoolId), + [selectedPoolId] ); + // Close canvas if no pool id is selected. + useEffect(() => { + if (selectedPoolId === 0) { + closeCanvas(); + } + }, [selectedPoolId]); + + // Ensure bonded pool exists before rendering. Canvas should close if this is the case. if (!bondedPool) { - closeCanvas(); return null; } @@ -86,7 +97,6 @@ export const JoinPool = () => { setSelectedPoolId={setSelectedPoolId} bondedPool={bondedPool} metadata={poolsMetaData[selectedPoolId]} - setSelectedPoolCount={setSelectedPoolCount} autoSelected={options?.poolId === undefined} filteredBondedPools={filteredBondedPools} /> diff --git a/src/canvas/JoinPool/types.ts b/src/canvas/JoinPool/types.ts index b7d2dcedac..0317be1315 100644 --- a/src/canvas/JoinPool/types.ts +++ b/src/canvas/JoinPool/types.ts @@ -12,7 +12,6 @@ export interface JoinPoolHeaderProps { autoSelected: boolean; setActiveTab: (tab: number) => void; setSelectedPoolId: Dispatch>; - setSelectedPoolCount: Dispatch>; } export interface NominationsProps { diff --git a/src/canvas/PoolMembers/Lists/Default.tsx b/src/canvas/PoolMembers/Lists/Default.tsx index aab68aa687..b8d7d676bd 100644 --- a/src/canvas/PoolMembers/Lists/Default.tsx +++ b/src/canvas/PoolMembers/Lists/Default.tsx @@ -2,9 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only import { isNotZero } from '@w3ux/utils'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { listItemsPerBatch, listItemsPerPage } from 'library/List/defaults'; +import { poolMembersPerPage } from 'library/List/defaults'; import { useApi } from 'contexts/Api'; import { usePoolMembers } from 'contexts/Pools/PoolMembers'; import { List, ListStatusHeader, Wrapper as ListWrapper } from 'library/List'; @@ -20,7 +20,6 @@ export const MembersListInner = ({ pagination, batchKey, members: initialMembers, - disableThrottle = false, }: DefaultMembersListProps) => { const { t } = useTranslation('pages'); const { isReady, activeEra } = useApi(); @@ -29,9 +28,6 @@ export const MembersListInner = ({ // current page const [page, setPage] = useState(1); - // current render iteration - const [renderIteration, setRenderIterationState] = useState(1); - // default list of validators const [membersDefault, setMembersDefault] = useState(initialMembers); @@ -42,26 +38,13 @@ export const MembersListInner = ({ // is this the initial fetch const [fetched, setFetched] = useState('unsynced'); - // render throttle iteration - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - setRenderIterationState(iter); - }; - // pagination - const totalPages = Math.ceil(members.length / listItemsPerPage); - const pageEnd = page * listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); - - // render batch - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + const totalPages = Math.ceil(members.length / poolMembersPerPage); + const pageEnd = page * poolMembersPerPage - 1; + const pageStart = pageEnd - (poolMembersPerPage - 1); // get throttled subset or entire list - const listMembers = members.slice(pageStart).slice(0, listItemsPerPage); + const listMembers = members.slice(pageStart).slice(0, poolMembersPerPage); // handle validator list bootstrapping const setupMembersList = () => { @@ -85,15 +68,6 @@ export const MembersListInner = ({ } }, [isReady, fetched, activeEra.index]); - // Render throttle. - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); - } - }, [renderIterationRef.current]); - return !members.length ? null : ( diff --git a/src/canvas/PoolMembers/Lists/FetchPage.tsx b/src/canvas/PoolMembers/Lists/FetchPage.tsx index 0f56e0f7ea..7962735cad 100644 --- a/src/canvas/PoolMembers/Lists/FetchPage.tsx +++ b/src/canvas/PoolMembers/Lists/FetchPage.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { listItemsPerBatch, listItemsPerPage } from 'library/List/defaults'; +import { poolMembersPerPage } from 'library/List/defaults'; import { usePlugins } from 'contexts/Plugins'; import { useActivePool } from 'contexts/Pools/ActivePool'; import { usePoolMembers } from 'contexts/Pools/PoolMembers'; @@ -21,7 +21,6 @@ import { SubscanController } from 'controllers/SubscanController'; export const MembersListInner = ({ pagination, batchKey, - disableThrottle = false, memberCount, }: FetchpageMembersListProps) => { const { t } = useTranslation('pages'); @@ -40,26 +39,10 @@ export const MembersListInner = ({ // current page. const [page, setPage] = useState(1); - // current render iteration. - const [renderIteration, setRenderIterationState] = useState(1); - - // render throttle iteration. - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - setRenderIterationState(iter); - }; - // pagination - const totalPages = Math.ceil(Number(memberCount) / listItemsPerPage); - const pageEnd = listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); - - // render batch - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + const totalPages = Math.ceil(Number(memberCount) / poolMembersPerPage); + const pageEnd = poolMembersPerPage - 1; + const pageStart = pageEnd - (poolMembersPerPage - 1); // handle validator list bootstrapping const fetchingMemberList = useRef(false); @@ -85,7 +68,7 @@ export const MembersListInner = ({ // get throttled subset or entire list const listMembers = poolMembersApi .slice(pageStart) - .slice(0, listItemsPerPage); + .slice(0, poolMembersPerPage); // Refetch list when page changes. useEffect(() => { @@ -109,15 +92,6 @@ export const MembersListInner = ({ } }, [fetchedPoolMembersApi, activePool]); - // Render throttle. - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); - } - }, [renderIterationRef.current]); - return ( diff --git a/src/canvas/PoolMembers/Lists/types.ts b/src/canvas/PoolMembers/Lists/types.ts index c6f74832ef..ea4d49b511 100644 --- a/src/canvas/PoolMembers/Lists/types.ts +++ b/src/canvas/PoolMembers/Lists/types.ts @@ -6,7 +6,6 @@ import type { AnyJson } from 'types'; export interface MembersListProps { pagination: boolean; batchKey: string; - disableThrottle?: boolean; selectToggleable?: boolean; } diff --git a/src/consts.ts b/src/consts.ts index 752b99aca4..a5eb722df6 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -37,4 +37,4 @@ export const TipsThresholdMedium = 1200; * Misc Values */ export const MaxPayoutDays = 60; -export const MaxEraRewardPointsEras = 14; +export const MaxEraRewardPointsEras = 10; diff --git a/src/contexts/Filters/defaults.ts b/src/contexts/Filters/defaults.ts index 32c3ed62b8..5ebda7af35 100644 --- a/src/contexts/Filters/defaults.ts +++ b/src/contexts/Filters/defaults.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */ -import type { FiltersContextInterface } from './types'; +import type { FilterItem, FiltersContextInterface } from './types'; export const defaultFiltersInterface: FiltersContextInterface = { getFilters: (type, group) => [], @@ -18,3 +18,17 @@ export const defaultFiltersInterface: FiltersContextInterface = { applyFilters: (type, g, l, f) => {}, applyOrder: (g, l, f) => {}, }; + +export const defaultIncludes: FilterItem[] = [ + { + key: 'pools', + filters: ['active'], + }, +]; + +export const defaultExcludes: FilterItem[] = [ + { + key: 'pools', + filters: ['locked', 'destroying'], + }, +]; diff --git a/src/contexts/Filters/index.tsx b/src/contexts/Filters/index.tsx index b587f7e069..1d2a62a0f0 100644 --- a/src/contexts/Filters/index.tsx +++ b/src/contexts/Filters/index.tsx @@ -4,7 +4,11 @@ import type { ReactNode } from 'react'; import { createContext, useContext, useState } from 'react'; import type { AnyFunction, AnyJson } from 'types'; -import { defaultFiltersInterface } from './defaults'; +import { + defaultExcludes, + defaultFiltersInterface, + defaultIncludes, +} from './defaults'; import type { FilterItem, FilterItems, @@ -24,10 +28,10 @@ export const useFilters = () => useContext(FiltersContext); export const FiltersProvider = ({ children }: { children: ReactNode }) => { // groups along with their includes - const [includes, setIncludes] = useState([]); + const [includes, setIncludes] = useState(defaultIncludes); // groups along with their excludes. - const [excludes, setExcludes] = useState([]); + const [excludes, setExcludes] = useState(defaultExcludes); // groups along with their order. const [orders, setOrders] = useState([]); diff --git a/src/contexts/Pools/BondedPools/defaults.ts b/src/contexts/Pools/BondedPools/defaults.ts index 1859dc2279..b87911defc 100644 --- a/src/contexts/Pools/BondedPools/defaults.ts +++ b/src/contexts/Pools/BondedPools/defaults.ts @@ -14,9 +14,11 @@ export const defaultBondedPoolsContext: BondedPoolsContextState = { getPoolNominationStatusCode: (statuses) => '', getAccountPoolRoles: (address) => null, replacePoolRoles: (poolId, roleEdits) => {}, - poolSearchFilter: (filteredPools, searchTerm) => {}, + poolSearchFilter: (filteredPools, searchTerm) => [], bondedPools: [], poolsMetaData: {}, poolsNominations: {}, updatePoolNominations: (id, nominations) => {}, + poolListActiveTab: 'Active', + setPoolListActiveTab: (tab) => {}, }; diff --git a/src/contexts/Pools/BondedPools/index.tsx b/src/contexts/Pools/BondedPools/index.tsx index c070f79a8f..f064555b52 100644 --- a/src/contexts/Pools/BondedPools/index.tsx +++ b/src/contexts/Pools/BondedPools/index.tsx @@ -12,6 +12,7 @@ import type { MaybePool, NominationStatuses, PoolNominations, + PoolTab, } from './types'; import { useStaking } from 'contexts/Staking'; import type { AnyApi, AnyJson, MaybeAddress, Sync } from 'types'; @@ -56,6 +57,9 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { Record >({}); + // Store pool list active tab. Defaults to `Active` tab. + const [poolListActiveTab, setPoolListActiveTab] = useState('Active'); + // Fetch all bonded pool entries and their metadata. const fetchBondedPools = async () => { if (!api || bondedPoolsSynced.current !== 'unsynced') { @@ -425,6 +429,8 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { poolsMetaData, poolsNominations, updatePoolNominations, + poolListActiveTab, + setPoolListActiveTab, }} > {children} diff --git a/src/contexts/Pools/BondedPools/types.ts b/src/contexts/Pools/BondedPools/types.ts index 030c39d615..547cdbb5ab 100644 --- a/src/contexts/Pools/BondedPools/types.ts +++ b/src/contexts/Pools/BondedPools/types.ts @@ -4,6 +4,7 @@ import type { AnyApi, AnyJson, MaybeAddress } from 'types'; import type { ActiveBondedPool } from '../ActivePool/types'; import type { AnyFilter } from 'library/Filter/types'; +import type { Dispatch, SetStateAction } from 'react'; export interface BondedPoolsContextState { queryBondedPool: (poolId: number) => AnyApi; @@ -18,11 +19,13 @@ export interface BondedPoolsContextState { getPoolNominationStatusCode: (statuses: NominationStatuses | null) => string; getAccountPoolRoles: (address: MaybeAddress) => AnyApi; replacePoolRoles: (poolId: number, roleEdits: AnyJson) => void; - poolSearchFilter: (filteredPools: AnyFilter, searchTerm: string) => void; + poolSearchFilter: (filteredPools: AnyFilter, searchTerm: string) => AnyJson[]; bondedPools: BondedPool[]; poolsMetaData: Record; poolsNominations: Record; updatePoolNominations: (id: number, nominations: string[]) => void; + poolListActiveTab: PoolTab; + setPoolListActiveTab: Dispatch>; } export type BondedPool = ActiveBondedPool & { @@ -60,3 +63,5 @@ export type AccountPoolRoles = { nominator: number[]; bouncer: number[]; } | null; + +export type PoolTab = 'All' | 'Active' | 'Locked' | 'Destroying'; diff --git a/src/contexts/Pools/JoinPools/defaults.ts b/src/contexts/Pools/JoinPools/defaults.ts new file mode 100644 index 0000000000..342f1ca12a --- /dev/null +++ b/src/contexts/Pools/JoinPools/defaults.ts @@ -0,0 +1,12 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */ + +import type { JoinPoolsContextInterface } from './types'; + +export const defaultJoinPoolsContext: JoinPoolsContextInterface = { + poolsForJoin: [], + startJoinPoolFetch: () => {}, +}; + +export const MaxPoolsForJoin = 8; diff --git a/src/contexts/Pools/JoinPools/index.tsx b/src/contexts/Pools/JoinPools/index.tsx new file mode 100644 index 0000000000..32f6342896 --- /dev/null +++ b/src/contexts/Pools/JoinPools/index.tsx @@ -0,0 +1,69 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ReactNode } from 'react'; +import { createContext, useContext, useState } from 'react'; +import type { JoinPoolsContextInterface } from './types'; +import { MaxPoolsForJoin, defaultJoinPoolsContext } from './defaults'; +import { useEffectIgnoreInitial } from '@w3ux/hooks'; +import { useBondedPools } from '../BondedPools'; +import { useApi } from 'contexts/Api'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { usePoolPerformance } from '../PoolPerformance'; +import type { BondedPool } from '../BondedPools/types'; +import { shuffle } from '@w3ux/utils'; + +export const JoinPoolsContext = createContext( + defaultJoinPoolsContext +); + +export const useJoinPools = () => useContext(JoinPoolsContext); + +export const JoinPoolsProvider = ({ children }: { children: ReactNode }) => { + const { api, activeEra } = useApi(); + const { bondedPools } = useBondedPools(); + const { erasRewardPointsFetched } = useValidators(); + const { getPoolPerformanceTask, startPoolRewardPointsFetch } = + usePoolPerformance(); + + // Save the bonded pools subset for pool joining. + const [poolsForJoin, setPoolsToJoin] = useState([]); + + // Start finding pools to join. + const startJoinPoolFetch = () => { + startPoolRewardPointsFetch( + 'pool_join', + poolsForJoin.map(({ addresses }) => addresses.stash) + ); + }; + + // Trigger worker to calculate join pool performance data. + useEffectIgnoreInitial(() => { + if ( + api && + bondedPools.length && + activeEra.index.isGreaterThan(0) && + erasRewardPointsFetched === 'synced' && + getPoolPerformanceTask('pool_join')?.status === 'unsynced' + ) { + // Generate a subset of pools to fetch performance data for. TODO: Send pools to JoinPool + // canvas and only select those. Move this logic to a separate context. + const poolJoinSelection = shuffle( + bondedPools.filter(({ state }) => state === 'Open') + ).slice(0, MaxPoolsForJoin); + + setPoolsToJoin(poolJoinSelection); + } + }, [ + bondedPools, + activeEra, + erasRewardPointsFetched, + getPoolPerformanceTask('pool_join'), + ]); + + return ( + + {children} + + ); +}; diff --git a/src/contexts/Pools/JoinPools/types.ts b/src/contexts/Pools/JoinPools/types.ts new file mode 100644 index 0000000000..fe49a8aa8f --- /dev/null +++ b/src/contexts/Pools/JoinPools/types.ts @@ -0,0 +1,9 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { BondedPool } from '../BondedPools/types'; + +export interface JoinPoolsContextInterface { + poolsForJoin: BondedPool[]; + startJoinPoolFetch: () => void; +} diff --git a/src/contexts/Pools/PoolPerformance/defaults.ts b/src/contexts/Pools/PoolPerformance/defaults.ts index 26e9287fc6..3d839fb70f 100644 --- a/src/contexts/Pools/PoolPerformance/defaults.ts +++ b/src/contexts/Pools/PoolPerformance/defaults.ts @@ -1,10 +1,24 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function */ -import type { PoolPerformanceContextInterface } from './types'; +import BigNumber from 'bignumber.js'; +import type { + PoolPerformanceContextInterface, + PoolPerformanceTaskStatus, +} from './types'; +export const defaultPoolPerformanceTask: PoolPerformanceTaskStatus = { + status: 'unsynced', + addresses: [], + startEra: BigNumber(0), + currentEra: BigNumber(0), + endEra: BigNumber(0), +}; export const defaultPoolPerformanceContext: PoolPerformanceContextInterface = { - poolRewardPointsFetched: 'unsynced', - poolRewardPoints: {}, + getPoolRewardPoints: () => ({}), + getPoolPerformanceTask: (key) => defaultPoolPerformanceTask, + setNewPoolPerformanceTask: (key, status, addresses) => {}, + updatePoolPerformanceTask: (key, status) => {}, + startPoolRewardPointsFetch: (key, addresses) => {}, }; diff --git a/src/contexts/Pools/PoolPerformance/index.tsx b/src/contexts/Pools/PoolPerformance/index.tsx index 304c30bc51..3efa8aff97 100644 --- a/src/contexts/Pools/PoolPerformance/index.tsx +++ b/src/contexts/Pools/PoolPerformance/index.tsx @@ -2,20 +2,28 @@ // SPDX-License-Identifier: GPL-3.0-only import type { ReactNode } from 'react'; -import { createContext, useContext, useState } from 'react'; +import { createContext, useContext, useRef, useState } from 'react'; import { MaxEraRewardPointsEras } from 'consts'; import { useEffectIgnoreInitial } from '@w3ux/hooks'; import Worker from 'workers/poolPerformance?worker'; import { useNetwork } from 'contexts/Network'; import { useValidators } from 'contexts/Validators/ValidatorEntries'; -import { useBondedPools } from 'contexts/Pools/BondedPools'; import { useApi } from 'contexts/Api'; import BigNumber from 'bignumber.js'; -import { mergeDeep } from '@w3ux/utils'; +import { mergeDeep, setStateWithRef } from '@w3ux/utils'; import { useStaking } from 'contexts/Staking'; import { formatRawExposures } from 'contexts/Staking/Utils'; -import type { PoolPerformanceContextInterface } from './types'; -import { defaultPoolPerformanceContext } from './defaults'; +import type { + PoolPerformanceContextInterface, + PoolPerformanceTasks, + PoolRewardPoints, + PoolRewardPointsMap, + PoolRewardPointsKey, +} from './types'; +import { + defaultPoolPerformanceTask, + defaultPoolPerformanceContext, +} from './defaults'; import type { Sync } from 'types'; const worker = new Worker(); @@ -31,64 +39,153 @@ export const PoolPerformanceProvider = ({ children: ReactNode; }) => { const { network } = useNetwork(); - const { bondedPools } = useBondedPools(); const { getPagedErasStakers } = useStaking(); + const { erasRewardPoints } = useValidators(); const { api, activeEra, isPagedRewardsActive } = useApi(); - const { erasRewardPointsFetched, erasRewardPoints } = useValidators(); - // Store whether pool performance data is being fetched. - const [poolRewardPointsFetched, setPoolRewardPointsFetched] = - useState('unsynced'); + // Store pool performance task data under a given key as it is being fetched . NOTE: Requires a + // ref to be accessed in `processEra` before re-render. + const [tasks, setTasks] = useState({}); + const tasksRef = useRef(tasks); - // Store pool performance data. - const [poolRewardPoints, setPoolRewardPoints] = useState< - Record> - >({}); + // Store pool performance data. NOTE: Requires a ref to update state with current data. + const [poolRewardPoints, setPoolRewardPointsState] = + useState({}); + const poolRewardPointsRef = useRef(poolRewardPoints); - // Store the currently active era being processed for pool performance. - const [currentEra, setCurrentEra] = useState(new BigNumber(0)); + // Gets a batch of pool reward points, or returns an empty object otherwise. + const getPoolRewardPoints = (key: PoolRewardPointsKey) => + poolRewardPoints?.[key] || {}; - // Store the earliest era that should be processed. - const [finishEra, setFinishEra] = useState(new BigNumber(0)); + // Sets a batch of pool reward points. + const setPoolRewardPoints = ( + key: PoolRewardPointsKey, + batch: PoolRewardPoints + ) => { + const newRewardPoints = { + ...poolRewardPointsRef.current, + [key]: batch, + }; - // Handle worker message on completed exposure check. - worker.onmessage = (message: MessageEvent) => { - if (message) { - const { data } = message; - const { task } = data; - if (task !== 'processNominationPoolsRewardData') { - return; - } + setStateWithRef( + newRewardPoints, + setPoolRewardPointsState, + poolRewardPointsRef + ); + }; - // Update state with new data. - const { poolRewardData } = data; - setPoolRewardPoints(mergeDeep(poolRewardPoints, poolRewardData)); + // Gets whether pool performance data is being fetched under a given key. + const getPoolPerformanceTask = (key: PoolRewardPointsKey) => + tasks[key] || defaultPoolPerformanceTask; - if (currentEra.isEqualTo(finishEra)) { - setPoolRewardPointsFetched('synced'); - } else { - const nextEra = BigNumber.max(currentEra.minus(1), 1); - processEra(nextEra); - } + // Sets a pool performance task under a given key. + const setNewPoolPerformanceTask = ( + key: PoolRewardPointsKey, + status: Sync, + addresses: string[], + currentEra: BigNumber, + endEra: BigNumber + ) => { + const startEra = activeEra.index; + + setStateWithRef( + { + ...tasksRef.current, + [key]: { status, addresses, startEra, endEra, currentEra }, + }, + setTasks, + tasksRef + ); + + // Reset pool reward points for the given key. + if (status === 'syncing') { + setStateWithRef( + { + ...poolRewardPointsRef.current, + [key]: {}, + }, + setPoolRewardPointsState, + poolRewardPointsRef + ); + } + }; + + // Set current era for performance fetched key. + const updateTaskCurrentEra = (key: PoolRewardPointsKey, era: BigNumber) => { + if (!getPoolPerformanceTask(key)) { + return; + } + setStateWithRef( + { + ...tasksRef.current, + [key]: { ...tasksRef.current[key], currentEra: era }, + }, + setTasks, + tasksRef + ); + }; + + // Updates an existing performance fetched key with a new status. + const updatePoolPerformanceTask = ( + key: PoolRewardPointsKey, + status: Sync + ) => { + if (!getPoolPerformanceTask(key)) { + return; } + setStateWithRef( + { + ...tasksRef.current, + [key]: { ...tasksRef.current[key], status }, + }, + setTasks, + tasksRef + ); }; - // Start fetching pool performance calls from the current era. - const startGetPoolPerformance = async () => { - setPoolRewardPointsFetched('syncing'); - setFinishEra( - BigNumber.max(activeEra.index.minus(MaxEraRewardPointsEras), 1) + // Start fetching pool performance data, starting from the current era. + const startPoolRewardPointsFetch = async ( + key: PoolRewardPointsKey, + addresses: string[] + ) => { + // Set as synced and exit early if there are no addresses to process. + if (!addresses.length) { + setNewPoolPerformanceTask( + key, + 'synced', + addresses, + activeEra.index, + activeEra.index + ); + return; + } + + // If the addresses have not changed for this key, exit early. + const current = getPoolPerformanceTask(key); + if (current.addresses.toString() === addresses.toString()) { + return; + } + + const currentEra = BigNumber.max(activeEra.index.minus(1)); + const endEra = BigNumber.max( + activeEra.index.minus(MaxEraRewardPointsEras), + 1 ); - const startEra = BigNumber.max(activeEra.index.minus(1), 1); - processEra(startEra); + // Set as syncing and start processing. + setNewPoolPerformanceTask(key, 'syncing', addresses, currentEra, endEra); + + // Start processing from the previous active era. + processEra(key, currentEra); }; // Get era data and send to worker. - const processEra = async (era: BigNumber) => { + const processEra = async (key: PoolRewardPointsKey, era: BigNumber) => { if (!api) { return; } - setCurrentEra(era); + + // NOTE: This will not make any difference on the first run. + updateTaskCurrentEra(key, era); let exposures; if (isPagedRewardsActive(era)) { @@ -103,52 +200,64 @@ export const PoolPerformanceProvider = ({ exposures = formatRawExposures(result); } + const addresses = tasksRef.current[key]?.addresses || []; + worker.postMessage({ task: 'processNominationPoolsRewardData', + key, era: era.toString(), exposures, - bondedPools: bondedPools.map((b) => b.addresses.stash), + addresses, erasRewardPoints, }); }; - // Trigger worker to calculate pool reward data for garaphs once: - // - // - active era is synced. - // - era reward points are fetched. - // - bonded pools have been fetched. - // - // Re-calculates when any of the above change. - useEffectIgnoreInitial(() => { - if ( - api && - bondedPools.length && - activeEra.index.isGreaterThan(0) && - erasRewardPointsFetched === 'synced' && - poolRewardPointsFetched === 'unsynced' - ) { - startGetPoolPerformance(); + // Handle worker message on completed exposure check. + worker.onmessage = (message: MessageEvent) => { + if (message) { + const { data } = message; + const { task, key, addresses } = data; + + if (task !== 'processNominationPoolsRewardData') { + return; + } + + // If addresses for the given key have changed or been removed, ignore the result. + const current = getPoolPerformanceTask(key); + + if (current.addresses.toString() !== addresses.toString()) { + return; + } + + // Update state with new data. + setPoolRewardPoints( + key, + mergeDeep(getPoolRewardPoints(key), data.poolRewardData) + ); + + if (current.currentEra.isEqualTo(current.endEra)) { + updatePoolPerformanceTask(key, 'synced'); + } else { + const nextEra = BigNumber.max(current.currentEra.minus(1), 1); + processEra(key, nextEra); + } } - }, [ - bondedPools, - activeEra, - erasRewardPointsFetched, - poolRewardPointsFetched, - ]); + }; // Reset state data on network change. useEffectIgnoreInitial(() => { - setPoolRewardPoints({}); - setCurrentEra(new BigNumber(0)); - setFinishEra(new BigNumber(0)); - setPoolRewardPointsFetched('unsynced'); + setStateWithRef({}, setPoolRewardPointsState, poolRewardPointsRef); + setStateWithRef({}, setTasks, tasksRef); }, [network]); return ( {children} diff --git a/src/contexts/Pools/PoolPerformance/types.ts b/src/contexts/Pools/PoolPerformance/types.ts index 16ce7f6272..62828a25f8 100644 --- a/src/contexts/Pools/PoolPerformance/types.ts +++ b/src/contexts/Pools/PoolPerformance/types.ts @@ -1,9 +1,62 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import type { AnyJson, Sync } from 'types'; +import type BigNumber from 'bignumber.js'; +import type { Sync } from 'types'; export interface PoolPerformanceContextInterface { - poolRewardPointsFetched: Sync; - poolRewardPoints: AnyJson; + getPoolRewardPoints: (key: PoolRewardPointsKey) => PoolRewardPoints; + getPoolPerformanceTask: ( + key: PoolRewardPointsKey + ) => PoolPerformanceTaskStatus; + setNewPoolPerformanceTask: ( + key: PoolRewardPointsKey, + status: Sync, + addresses: string[], + currentEra: BigNumber, + endEra: BigNumber + ) => void; + updatePoolPerformanceTask: (key: PoolRewardPointsKey, status: Sync) => void; + startPoolRewardPointsFetch: ( + key: PoolRewardPointsKey, + addresses: string[] + ) => void; } + +// Fetching status for keys. +export type PoolPerformanceTasks = Partial< + Record +>; + +// Performance fetching status. +export interface PoolPerformanceTaskStatus { + status: Sync; + addresses: string[]; + startEra: BigNumber; + currentEra: BigNumber; + endEra: BigNumber; +} + +/* + * Batch Key -> Pool Address -> Era -> Points. + */ + +// Supported reward points batch keys. +export type PoolRewardPointsKey = 'pool_join' | 'pool_page'; + +// Pool reward batches, keyed by batch key. +export type PoolRewardPointsMap = Partial>; + +// Pool reward points are keyed by era, then by pool address. + +export type PoolRewardPoints = Record; + +export type PointsByEra = Record; + +// Type aliases to better understand pool reward records. + +export type PoolAddress = string; + +export type EraKey = number; + +export type EraPoints = string; diff --git a/src/controllers/SubscanController/index.ts b/src/controllers/SubscanController/index.ts index 4d30175e9c..b19a08431d 100644 --- a/src/controllers/SubscanController/index.ts +++ b/src/controllers/SubscanController/index.ts @@ -12,7 +12,7 @@ import type { import type { Locale } from 'date-fns'; import { format, fromUnixTime, getUnixTime, subDays } from 'date-fns'; import type { PoolMember } from 'contexts/Pools/PoolMembers/types'; -import { listItemsPerPage } from 'library/List/defaults'; +import { poolMembersPerPage } from 'library/List/defaults'; export class SubscanController { // ------------------------------------------------------ @@ -160,7 +160,7 @@ export class SubscanController { ): Promise => { const result = await this.makeRequest(this.ENDPOINTS.poolMembers, { pool_id: poolId, - row: listItemsPerPage, + row: poolMembersPerPage, page: page - 1, }); if (!result?.list) { diff --git a/src/kits/Structure/Tx/Signer.tsx b/src/kits/Structure/Tx/Signer.tsx index b763f2d700..4a35574c79 100644 --- a/src/kits/Structure/Tx/Signer.tsx +++ b/src/kits/Structure/Tx/Signer.tsx @@ -23,7 +23,7 @@ export const Signer = ({ /   {dangerMessage} diff --git a/src/kits/Structure/Tx/Wrapper.ts b/src/kits/Structure/Tx/Wrapper.ts index 89ff719b76..927c5010ce 100644 --- a/src/kits/Structure/Tx/Wrapper.ts +++ b/src/kits/Structure/Tx/Wrapper.ts @@ -127,13 +127,13 @@ export const SignerWrapper = styled.p` .not-enough { margin-left: 0.5rem; - } - .danger { - color: var(--status-danger-color); - } + > .danger { + color: var(--status-danger-color); + } - > .icon { - margin-right: 0.3rem; + > .icon { + margin-right: 0.3rem; + } } `; diff --git a/src/library/CallToAction/index.tsx b/src/library/CallToAction/index.tsx index 259285960a..6c6ac900d8 100644 --- a/src/library/CallToAction/index.tsx +++ b/src/library/CallToAction/index.tsx @@ -37,10 +37,20 @@ export const CallToActionWrapper = styled.div` &:nth-child(1) { flex-grow: 1; - @media (min-width: 651px) { border-right: 1px solid var(--border-primary-color); padding-right: 1rem; + + &.fixedWidth { + flex-grow: 0; + flex-basis: 70%; + } + } + + @media (max-width: 650px) { + &.fixedWidth { + flex-basis: 100%; + } } } @@ -170,8 +180,68 @@ export const CallToActionWrapper = styled.div` justify-content: center; flex-wrap: nowrap; font-size: 1.3rem; + line-height: 1.3rem; width: 100%; + .counter { + font-family: InterBold, sans-serif; + font-size: 1.1rem; + margin-left: 0.75rem; + } + + .loader { + height: 0.8rem; + margin-left: 1.6rem; + aspect-ratio: 5; + --_g: no-repeat radial-gradient(farthest-side, white 94%, #0000); + background: var(--_g), var(--_g), var(--_g), var(--_g); + background-size: 20% 100%; + animation: + l40-1 0.75s infinite alternate, + l40-2 1.5s infinite alternate; + } + @keyframes l40-1 { + 0%, + 10% { + background-position: + 0 0, + 0 0, + 0 0, + 0 0; + } + 33% { + background-position: + 0 0, + calc(100% / 3) 0, + calc(100% / 3) 0, + calc(100% / 3) 0; + } + 66% { + background-position: + 0 0, + calc(100% / 3) 0, + calc(2 * 100% / 3) 0, + calc(2 * 100% / 3) 0; + } + 90%, + 100% { + background-position: + 0 0, + calc(100% / 3) 0, + calc(2 * 100% / 3) 0, + 100% 0; + } + } + @keyframes l40-2 { + 0%, + 49.99% { + transform: scale(1); + } + 50%, + 100% { + transform: scale(-1); + } + } &:disabled { cursor: default; } @@ -180,6 +250,12 @@ export const CallToActionWrapper = styled.div` margin: 0 0.75rem; } } + + &.inactive { + > button { + cursor: default; + } + } } } } diff --git a/src/library/Filter/Tabs.tsx b/src/library/Filter/Tabs.tsx index 51f0bfaf6e..48d4e14748 100644 --- a/src/library/Filter/Tabs.tsx +++ b/src/library/Filter/Tabs.tsx @@ -1,42 +1,46 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { useState } from 'react'; import { useFilters } from 'contexts/Filters'; import { TabsWrapper, TabWrapper } from './Wrappers'; import type { FilterTabsProps } from './types'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import type { PoolTab } from 'contexts/Pools/BondedPools/types'; -export const Tabs = ({ config, activeIndex }: FilterTabsProps) => { +export const Tabs = ({ config }: FilterTabsProps) => { const { resetFilters, setMultiFilters } = useFilters(); - - const [active, setActive] = useState(activeIndex); + const { poolListActiveTab, setPoolListActiveTab } = useBondedPools(); return ( - {config.map((c, i) => ( - { - if (c.includes?.length) { - setMultiFilters('include', 'pools', c.includes, true); - } else { - resetFilters('include', 'pools'); - } + {config.map((c, i) => { + const label = c.label as PoolTab; + + return ( + { + if (c.includes?.length) { + setMultiFilters('include', 'pools', c.includes, true); + } else { + resetFilters('include', 'pools'); + } - if (c.excludes?.length) { - setMultiFilters('exclude', 'pools', c.excludes, true); - } else { - resetFilters('exclude', 'pools'); - } + if (c.excludes?.length) { + setMultiFilters('exclude', 'pools', c.excludes, true); + } else { + resetFilters('exclude', 'pools'); + } - setActive(i); - }} - > - {c.label} - - ))} + setPoolListActiveTab(label); + }} + > + {label} + + ); + })} ); }; diff --git a/src/library/Filter/types.ts b/src/library/Filter/types.ts index dbc2e83277..aa6a4ca3aa 100644 --- a/src/library/Filter/types.ts +++ b/src/library/Filter/types.ts @@ -19,7 +19,6 @@ export interface LargerFilterItemProps { } export interface FilterTabsProps { config: FilterConfig[]; - activeIndex: number; } export interface FilterConfig { diff --git a/src/library/List/defaults.ts b/src/library/List/defaults.ts index 0aa87f0e92..1cfbba438e 100644 --- a/src/library/List/defaults.ts +++ b/src/library/List/defaults.ts @@ -16,8 +16,14 @@ export const defaultContext: ListContextInterface = { selectToggleable: true, }; -// Total list items to show per page. -export const listItemsPerPage = 25; +// The amount of pools per page. +export const poolsPerPage = 21; -// If throttling a list of items, how many items to show per batch. -export const listItemsPerBatch = 25; +// The amount of validators per page. +export const validatorsPerPage = 30; + +// The amount of payouts per page. +export const payoutsPerPage = 50; + +// The amount of pool members per page. +export const poolMembersPerPage = 50; diff --git a/src/library/Nominations/index.tsx b/src/library/Nominations/index.tsx index 442a523faa..5fd5f5e2c2 100644 --- a/src/library/Nominations/index.tsx +++ b/src/library/Nominations/index.tsx @@ -143,7 +143,6 @@ export const Nominations = ({ format="nomination" refetchOnListUpdate allowMoreCols - disableThrottle allowListFormat={false} /> ) : poolDestroying ? ( diff --git a/src/library/Pool/Rewards.tsx b/src/library/Pool/Rewards.tsx index 18bd3f9bae..ec18e8c2a4 100644 --- a/src/library/Pool/Rewards.tsx +++ b/src/library/Pool/Rewards.tsx @@ -24,7 +24,9 @@ export const Rewards = ({ address, displayFor = 'default' }: RewardProps) => { const { isReady } = useApi(); const { setTooltipTextAndOpen } = useTooltip(); const { eraPointsBoundaries } = useValidators(); - const { poolRewardPoints, poolRewardPointsFetched } = usePoolPerformance(); + const { getPoolRewardPoints, getPoolPerformanceTask } = usePoolPerformance(); + + const poolRewardPoints = getPoolRewardPoints('pool_page'); const eraRewardPoints = Object.fromEntries( Object.entries(poolRewardPoints[address] || {}).map(([k, v]: AnyJson) => [ @@ -38,7 +40,8 @@ export const Rewards = ({ address, displayFor = 'default' }: RewardProps) => { const prefilledPoints = prefillEraPoints(Object.values(normalisedPoints)); const empty = Object.values(poolRewardPoints).length === 0; - const syncing = !isReady || poolRewardPointsFetched !== 'synced'; + const syncing = + !isReady || getPoolPerformanceTask('pool_page').status !== 'synced'; const tooltipText = `${MaxEraRewardPointsEras} ${t('dayPoolPerformance')}`; return ( diff --git a/src/library/PoolList/Default.tsx b/src/library/PoolList/index.tsx similarity index 70% rename from src/library/PoolList/Default.tsx rename to src/library/PoolList/index.tsx index a281d147fb..a348e6f6b7 100644 --- a/src/library/PoolList/Default.tsx +++ b/src/library/PoolList/index.tsx @@ -3,13 +3,11 @@ import { faBars, faGripVertical } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { isNotZero } from '@w3ux/utils'; import { motion } from 'framer-motion'; import type { FormEvent } from 'react'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { listItemsPerBatch, listItemsPerPage } from 'library/List/defaults'; -import { useApi } from 'contexts/Api'; +import { poolsPerPage } from 'library/List/defaults'; import { useFilters } from 'contexts/Filters'; import { useBondedPools } from 'contexts/Pools/BondedPools'; import { useTheme } from 'contexts/Themes'; @@ -30,130 +28,114 @@ import { usePoolList } from './context'; import type { PoolListProps } from './types'; import type { BondedPool } from 'contexts/Pools/BondedPools/types'; import { useSyncing } from 'hooks/useSyncing'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { useApi } from 'contexts/Api'; +import { useEffectIgnoreInitial } from '@w3ux/hooks'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; export const PoolList = ({ allowMoreCols, pagination, - disableThrottle, allowSearch, pools, - defaultFilters, allowListFormat = true, }: PoolListProps) => { const { t } = useTranslation('library'); - const { mode } = useTheme(); - const { isReady, activeEra } = useApi(); const { + network, networkData: { colors }, } = useNetwork(); + const { mode } = useTheme(); + const { activeEra } = useApi(); const { syncing } = useSyncing(); const { applyFilter } = usePoolFilters(); + const { erasRewardPointsFetched } = useValidators(); const { listFormat, setListFormat } = usePoolList(); - const { getFilters, setMultiFilters, getSearchTerm, setSearchTerm } = - useFilters(); - const { poolSearchFilter, poolsNominations } = useBondedPools(); + const { startPoolRewardPointsFetch } = usePoolPerformance(); + const { getFilters, getSearchTerm, setSearchTerm } = useFilters(); + const { poolSearchFilter, poolsNominations, bondedPools } = useBondedPools(); const includes = getFilters('include', 'pools'); const excludes = getFilters('exclude', 'pools'); const searchTerm = getSearchTerm('pools'); - // current page - const [page, setPage] = useState(1); + // Carry out filter of pool list. + const filterPoolList = () => { + let filteredPools = Object.assign(poolsDefault); + filteredPools = applyFilter(includes, excludes, filteredPools); + if (searchTerm) { + filteredPools = poolSearchFilter(filteredPools, searchTerm); + } + return filteredPools; + }; - // current render iteration - const [renderIteration, setRenderIterationState] = useState(1); + // The current page of pool list. + const [page, setPage] = useState(1); - // default list of pools + // Default pool list items before filtering. const [poolsDefault, setPoolsDefault] = useState(pools || []); - // manipulated list (ordering, filtering) of pools - const [listPools, setListPools] = useState(pools || []); + // Manipulated pool list items after filtering. + const [listPools, setListPools] = useState(filterPoolList()); - // is this the initial fetch - const [fetched, setFetched] = useState(false); - - // render throttle iteration - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - setRenderIterationState(iter); - }; + // Whether this the initial render. + const [synced, setSynced] = useState(false); // pagination - const totalPages = Math.ceil(listPools.length / listItemsPerPage); - const pageEnd = page * listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); + const totalPages = Math.ceil(listPools.length / poolsPerPage); + const pageEnd = page * poolsPerPage - 1; + const pageStart = pageEnd - (poolsPerPage - 1); - // render batch - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + // get paged subset of list items. + const poolsToDisplay = listPools.slice(pageStart).slice(0, poolsPerPage); - // get throttled subset or entire list - const poolsToDisplay = disableThrottle - ? listPools - : listPools.slice(pageStart).slice(0, listItemsPerPage); - - // handle pool list bootstrapping - const setupPoolList = () => { + // Handle resetting of pool list when provided pools change. + const resetPoolList = () => { setPoolsDefault(pools || []); setListPools(pools || []); - setFetched(true); + setSynced(true); }; // handle filter / order update - const handlePoolsFilterUpdate = ( - filteredPools = Object.assign(poolsDefault) - ) => { - filteredPools = applyFilter(includes, excludes, filteredPools); - if (searchTerm) { - filteredPools = poolSearchFilter(filteredPools, searchTerm); - } + const handlePoolsFilterUpdate = () => { + const filteredPools = filterPoolList(); setListPools(filteredPools); setPage(1); - setRenderIteration(1); }; const handleSearchChange = (e: FormEvent) => { const newValue = e.currentTarget.value; - let filteredPools = Object.assign(poolsDefault); + let filteredPools: BondedPool[] = Object.assign(poolsDefault); filteredPools = applyFilter(includes, excludes, filteredPools); filteredPools = poolSearchFilter(filteredPools, newValue); // ensure no duplicates filteredPools = filteredPools.filter( - (value: BondedPool, index: number, self: BondedPool[]) => + (value, index: number, self) => index === self.findIndex((i) => i.id === value.id) ); setPage(1); - setRenderIteration(1); setListPools(filteredPools); setSearchTerm('pools', newValue); }; - // Refetch list when pool list changes. - useEffect(() => { - if (pools !== poolsDefault) { - setFetched(false); - } - }, [pools]); - - // Configure pool list when network is ready to fetch. + // Fetch pool performance data when list items or page changes. Requires `erasRewardPoints` and + // `bondedPools` to be fetched. useEffect(() => { - if (isReady && isNotZero(activeEra.index) && !fetched) { - setupPoolList(); + if (erasRewardPointsFetched && bondedPools.length) { + startPoolRewardPointsFetch( + 'pool_page', + poolsToDisplay.map(({ addresses }) => addresses.stash) + ); } - }, [isReady, fetched, activeEra.index]); + }, [JSON.stringify(listPools), page, erasRewardPointsFetched, bondedPools]); - // Render throttling. Only render a batch of pools at a time. + // Refetch list when pool list changes. useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); + if (JSON.stringify(pools) !== JSON.stringify(poolsDefault) && synced) { + resetPoolList(); } - }, [renderIterationRef.current]); + }, [JSON.stringify(pools)]); // List ui changes / validator changes trigger re-render of list. useEffect(() => { @@ -168,15 +150,10 @@ export const PoolList = ({ window.scrollTo(0, 0); }, [includes, excludes]); - // Set default filters. - useEffect(() => { - if (defaultFilters?.includes?.length) { - setMultiFilters('include', 'pools', defaultFilters?.includes, false); - } - if (defaultFilters?.excludes?.length) { - setMultiFilters('exclude', 'pools', defaultFilters?.excludes, false); - } - }, []); + // Reset list on network change or active era change. + useEffectIgnoreInitial(() => { + resetPoolList(); + }, [network, activeEra.index.toString()]); return ( @@ -213,7 +190,6 @@ export const PoolList = ({ excludes: [], }, ]} - activeIndex={1} />
diff --git a/src/library/PoolList/types.ts b/src/library/PoolList/types.ts index e3e819078d..1621c5e1f3 100644 --- a/src/library/PoolList/types.ts +++ b/src/library/PoolList/types.ts @@ -13,12 +13,7 @@ export interface PoolListProps { allowMoreCols?: boolean; allowSearch?: boolean; pagination?: boolean; - disableThrottle?: boolean; refetchOnListUpdate?: string; allowListFormat?: boolean; pools?: BondedPool[]; - defaultFilters?: { - includes: string[] | null; - excludes: string[] | null; - }; } diff --git a/src/library/ValidatorList/ValidatorItem/Utils.tsx b/src/library/ValidatorList/ValidatorItem/Utils.tsx index 1ba2a553a5..47239d7663 100644 --- a/src/library/ValidatorList/ValidatorItem/Utils.tsx +++ b/src/library/ValidatorList/ValidatorItem/Utils.tsx @@ -72,7 +72,7 @@ export const normaliseEraPoints = ( return Object.fromEntries( Object.entries(eraPoints).map(([era, points]) => [ era, - points.dividedBy(percentile).multipliedBy(0.01).toNumber(), + Math.min(points.dividedBy(percentile).multipliedBy(0.01).toNumber(), 1), ]) ); }; diff --git a/src/library/ValidatorList/index.tsx b/src/library/ValidatorList/index.tsx index 807ea03a2d..298c06b656 100644 --- a/src/library/ValidatorList/index.tsx +++ b/src/library/ValidatorList/index.tsx @@ -35,7 +35,7 @@ import { FilterHeaders } from './Filters/FilterHeaders'; import { FilterBadges } from './Filters/FilterBadges'; import type { NominationStatus } from './ValidatorItem/types'; import { useSyncing } from 'hooks/useSyncing'; -import { listItemsPerBatch, listItemsPerPage } from 'library/List/defaults'; +import { validatorsPerPage } from 'library/List/defaults'; export const ValidatorListInner = ({ // Default list values. @@ -57,9 +57,8 @@ export const ValidatorListInner = ({ allowListFormat = true, defaultOrder = undefined, defaultFilters = undefined, - // Throttling and re-fetching. + // Re-fetching. alwaysRefetchValidators = false, - disableThrottle = false, }: ValidatorListProps) => { const { t } = useTranslation('library'); const { @@ -163,26 +162,10 @@ export const ValidatorListInner = ({ // Store whether the search bar is being used. const [isSearching, setIsSearching] = useState(false); - // Current render iteration. - const [renderIteration, setRenderIterationState] = useState(1); - - // Render throttle iteration. - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - setRenderIterationState(iter); - }; - // Pagination. - const totalPages = Math.ceil(validators.length / listItemsPerPage); - const pageEnd = page * listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); - - // Render batch. - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + const totalPages = Math.ceil(validators.length / validatorsPerPage); + const pageEnd = page * validatorsPerPage - 1; + const pageStart = pageEnd - (validatorsPerPage - 1); // handle filter / order update const handleValidatorsFilterUpdate = ( @@ -198,14 +181,13 @@ export const ValidatorListInner = ({ } setValidators(filteredValidators); setPage(1); - setRenderIteration(1); } }; // get throttled subset or entire list - const listValidators = disableThrottle - ? validators - : validators.slice(pageStart).slice(0, listItemsPerPage); + const listValidators = validators + .slice(pageStart) + .slice(0, validatorsPerPage); // if in modal, handle resize const maybeHandleModalResize = () => { @@ -233,7 +215,6 @@ export const ValidatorListInner = ({ setValidators(filteredValidators); setPage(1); setIsSearching(e.currentTarget.value !== ''); - setRenderIteration(1); setSearchTerm('validators', newValue); }; @@ -300,15 +281,6 @@ export const ValidatorListInner = ({ } }, [isReady, activeEra.index, syncing, fetched]); - // Control render throttle. - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 50); - } - }, [renderIterationRef.current]); - // Trigger `onSelected` when selection changes. useEffect(() => { if (onSelected) { @@ -326,7 +298,7 @@ export const ValidatorListInner = ({ // Handle modal resize on list format change. useEffect(() => { maybeHandleModalResize(); - }, [listFormat, renderIteration, validators, page]); + }, [listFormat, validators, page]); return ( diff --git a/src/library/ValidatorList/types.ts b/src/library/ValidatorList/types.ts index f18d552e7f..ddd4ad01c3 100644 --- a/src/library/ValidatorList/types.ts +++ b/src/library/ValidatorList/types.ts @@ -31,7 +31,6 @@ export interface ValidatorListProps { alwaysRefetchValidators?: boolean; defaultFilters?: AnyJson; defaultOrder?: string; - disableThrottle?: boolean; selectActive?: boolean; selectToggleable?: boolean; refetchOnListUpdate?: boolean; diff --git a/src/locale/cn/library.json b/src/locale/cn/library.json index 3802a7e018..a1aab456a0 100644 --- a/src/locale/cn/library.json +++ b/src/locale/cn/library.json @@ -165,6 +165,7 @@ "proxy": "代理账户", "randomValidator": "随机验证人", "reGenerate": "重新生成", + "readyToJoinPool": "可加入提名池", "recentPerformance": "最近表现", "remove": "删除", "removeSelected": "移除选定项", @@ -182,7 +183,7 @@ "signing": "签署中", "submitTransaction": "准备提交交易", "syncing": "正在同步", - "syncingPoolData": "同步提名池数据中", + "syncingPoolData": "查找提名池中", "syncingPoolList": "同步提名池列表", "tooSmall": "质押金额太少", "top": "首", diff --git a/src/locale/cn/pages.json b/src/locale/cn/pages.json index b0934d3feb..8745641b97 100644 --- a/src/locale/cn/pages.json +++ b/src/locale/cn/pages.json @@ -137,7 +137,6 @@ "bondedFunds": "己质押金额", "bouncer": "守护人", "browseMembers": "浏览成员", - "browsePools": "浏览提名池", "cancel": "取消", "closePool": "可提取己解锁金额并关闭池", "compound": "复利", diff --git a/src/locale/en/library.json b/src/locale/en/library.json index 084462da4d..29d1faaa69 100644 --- a/src/locale/en/library.json +++ b/src/locale/en/library.json @@ -168,6 +168,7 @@ "proxy": "Proxy", "randomValidator": "Random Validator", "reGenerate": "Re-Generate", + "readyToJoinPool": "Ready to Join Pool", "recentPerformance": "Recent Performance", "remove": "Remove", "removeSelected": "Remove Selected", @@ -185,7 +186,7 @@ "signing": "Signing", "submitTransaction": "Ready to submit transaction.", "syncing": "Syncing", - "syncingPoolData": "Syncing Pool Data", + "syncingPoolData": "Finding Pools to Join", "syncingPoolList": "Syncing Pool list", "tooSmall": "Bond amount is too small.", "top": "Top", diff --git a/src/locale/en/pages.json b/src/locale/en/pages.json index b833194a2b..eae84374ad 100644 --- a/src/locale/en/pages.json +++ b/src/locale/en/pages.json @@ -139,7 +139,6 @@ "bondedFunds": "Bonded Funds", "bouncer": "Bouncer", "browseMembers": "Browse Members", - "browsePools": "Browse Pools", "cancel": "Cancel", "closePool": "You can now withdraw and close the pool.", "compound": "Compound", diff --git a/src/pages/Payouts/PayoutList/index.tsx b/src/pages/Payouts/PayoutList/index.tsx index 6e44c0c0f1..331792b21c 100644 --- a/src/pages/Payouts/PayoutList/index.tsx +++ b/src/pages/Payouts/PayoutList/index.tsx @@ -7,7 +7,7 @@ import { ellipsisFn, isNotZero, planckToUnit } from '@w3ux/utils'; import BigNumber from 'bignumber.js'; import { formatDistance, fromUnixTime } from 'date-fns'; import { motion } from 'framer-motion'; -import { Component, useEffect, useRef, useState } from 'react'; +import { Component, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useApi } from 'contexts/Api'; import { useBondedPools } from 'contexts/Pools/BondedPools'; @@ -25,14 +25,13 @@ import { useNetwork } from 'contexts/Network'; import { ItemWrapper } from '../Wrappers'; import type { PayoutListProps } from '../types'; import { PayoutListProvider, usePayoutList } from './context'; -import { listItemsPerPage, listItemsPerBatch } from 'library/List/defaults'; +import { payoutsPerPage } from 'library/List/defaults'; export const PayoutListInner = ({ allowMoreCols, pagination, title, payouts: initialPayouts, - disableThrottle = false, }: PayoutListProps) => { const { i18n, t } = useTranslation('pages'); const { mode } = useTheme(); @@ -47,32 +46,16 @@ export const PayoutListInner = ({ // current page const [page, setPage] = useState(1); - // current render iteration - const [renderIteration, _setRenderIteration] = useState(1); - // manipulated list (ordering, filtering) of payouts const [payouts, setPayouts] = useState(initialPayouts); // is this the initial fetch const [fetched, setFetched] = useState(false); - // render throttle iteration - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - _setRenderIteration(iter); - }; - // pagination - const totalPages = Math.ceil(payouts.length / listItemsPerPage); - const pageEnd = page * listItemsPerPage - 1; - const pageStart = pageEnd - (listItemsPerPage - 1); - - // render batch - const batchEnd = Math.min( - renderIteration * listItemsPerBatch - 1, - listItemsPerPage - ); + const totalPages = Math.ceil(payouts.length / payoutsPerPage); + const pageEnd = page * payoutsPerPage - 1; + const pageStart = pageEnd - (payoutsPerPage - 1); // refetch list when list changes useEffect(() => { @@ -87,24 +70,11 @@ export const PayoutListInner = ({ } }, [isReady, fetched, activeEra.index]); - // render throttle - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); - } - }, [renderIterationRef.current]); - // get list items to render let listPayouts = []; // get throttled subset or entire list - if (!disableThrottle) { - listPayouts = payouts.slice(pageStart).slice(0, listItemsPerPage); - } else { - listPayouts = payouts; - } + listPayouts = payouts.slice(pageStart).slice(0, payoutsPerPage); if (!payouts.length) { return null; diff --git a/src/pages/Payouts/types.ts b/src/pages/Payouts/types.ts index b9ff5d9650..745245ce51 100644 --- a/src/pages/Payouts/types.ts +++ b/src/pages/Payouts/types.ts @@ -6,7 +6,6 @@ import type { AnySubscan } from 'types'; export interface PayoutListProps { allowMoreCols?: boolean; pagination?: boolean; - disableThrottle?: boolean; title?: string | null; payoutsList?: AnySubscan; payouts?: AnySubscan; diff --git a/src/pages/Pools/Home/Favorites/index.tsx b/src/pages/Pools/Home/Favorites/index.tsx index 6f98398d59..e2603f5868 100644 --- a/src/pages/Pools/Home/Favorites/index.tsx +++ b/src/pages/Pools/Home/Favorites/index.tsx @@ -8,7 +8,7 @@ import { useApi } from 'contexts/Api'; import { useBondedPools } from 'contexts/Pools/BondedPools'; import { useFavoritePools } from 'contexts/Pools/FavoritePools'; import { CardWrapper } from 'library/Card/Wrappers'; -import { PoolList } from 'library/PoolList/Default'; +import { PoolList } from 'library/PoolList'; import { ListStatusHeader } from 'library/List'; import { PoolListProvider } from 'library/PoolList/context'; import type { BondedPool } from 'contexts/Pools/BondedPools/types'; diff --git a/src/pages/Pools/Home/Status/FindingPoolPercent.tsx b/src/pages/Pools/Home/Status/FindingPoolPercent.tsx new file mode 100644 index 0000000000..668c087e28 --- /dev/null +++ b/src/pages/Pools/Home/Status/FindingPoolPercent.tsx @@ -0,0 +1,30 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; + +export const FindingPoolsPercent = () => { + const { getPoolPerformanceTask } = usePoolPerformance(); + + // Get the pool performance task to determine if performance data is ready. + const poolJoinPerformanceTask = getPoolPerformanceTask('pool_join'); + + if (poolJoinPerformanceTask.status !== 'syncing') { + return null; + } + + // Calculate syncing status. + const { startEra, currentEra, endEra } = poolJoinPerformanceTask; + const totalEras = startEra.minus(endEra); + const erasPassed = startEra.minus(currentEra); + const percentPassed = erasPassed.isEqualTo(0) + ? new BigNumber(0) + : erasPassed.dividedBy(totalEras).multipliedBy(100); + + return ( + + {percentPassed.decimalPlaces(0).toFormat()}% + + ); +}; diff --git a/src/pages/Pools/Home/Status/NewMember.tsx b/src/pages/Pools/Home/Status/NewMember.tsx index 042cb55368..480efffe1c 100644 --- a/src/pages/Pools/Home/Status/NewMember.tsx +++ b/src/pages/Pools/Home/Status/NewMember.tsx @@ -3,28 +3,38 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CallToActionWrapper } from '../../../../library/CallToAction'; -import { faChevronRight, faUserGroup } from '@fortawesome/free-solid-svg-icons'; +import { + faChevronRight, + faUserGroup, + faUserPlus, +} from '@fortawesome/free-solid-svg-icons'; import { useSetup } from 'contexts/Setup'; -import { usePoolsTabs } from '../context'; import { useStatusButtons } from './useStatusButtons'; import { useTranslation } from 'react-i18next'; import { useOverlay } from 'kits/Overlay/Provider'; import type { NewMemberProps } from './types'; import { CallToActionLoader } from 'library/Loader/CallToAction'; import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import { FindingPoolsPercent } from './FindingPoolPercent'; +import { useJoinPools } from 'contexts/Pools/JoinPools'; export const NewMember = ({ syncing }: NewMemberProps) => { const { t } = useTranslation(); const { setOnPoolSetup } = useSetup(); - const { setActiveTab } = usePoolsTabs(); + const { poolsForJoin } = useJoinPools(); const { openCanvas } = useOverlay().canvas; - const { poolRewardPointsFetched } = usePoolPerformance(); - const { disableJoin, disableCreate } = useStatusButtons(); + const { startJoinPoolFetch } = useJoinPools(); + const { getPoolPerformanceTask } = usePoolPerformance(); + const { getJoinDisabled, getCreateDisabled } = useStatusButtons(); - const joinButtonDisabled = - disableJoin() || poolRewardPointsFetched !== 'synced'; + // Get the pool performance task to determine if performance data is ready. + const poolJoinPerformanceTask = getPoolPerformanceTask('pool_join'); - const createButtonDisabled = disableCreate(); + // Alias for create button disabled state. + const createDisabled = getCreateDisabled(); + + // Disable opening the canvas if data is not ready. + const joinButtonDisabled = getJoinDisabled() || !poolsForJoin.length; return ( @@ -33,39 +43,51 @@ export const NewMember = ({ syncing }: NewMemberProps) => { ) : ( <> -
+
-
-
-
@@ -73,11 +95,11 @@ export const NewMember = ({ syncing }: NewMemberProps) => {
); diff --git a/src/kits/Buttons/index.scss b/src/kits/Buttons/index.scss index f80d20a92e..721f2ed2a2 100644 --- a/src/kits/Buttons/index.scss +++ b/src/kits/Buttons/index.scss @@ -397,7 +397,6 @@ .btn-tab { @include btn-core; @include btn-layout; - @include btn-disabled; color: var(--text-color-primary); transition: @@ -418,35 +417,59 @@ opacity: 0.9; } - > .badge { - border: 1px solid var(--border-primary-color); + &.canvas { color: var(--text-color-tertiary); - font-size: var(--button-font-size-small); - margin-left: var(--button-spacing-large); + + &.active { + color: var(--text-color-primary); + background: var(--button-tab-canvas-background); + } + } + + &:disabled { + cursor: default; + } + + > span { display: flex; - padding: 0.3rem 0.6rem; align-items: center; - justify-content: center; - border-radius: 0.4rem; - overflow: hidden; - width: fit-content; - max-width: 3rem; + + &.preload { + opacity: 0; + } + + > .badge { + border: 1px solid var(--border-primary-color); + color: var(--text-color-tertiary); + font-size: var(--button-font-size-small); + margin-left: var(--button-spacing-large); + display: flex; + padding: 0.3rem 0.6rem; + align-items: center; + justify-content: center; + border-radius: 0.4rem; + overflow: hidden; + width: fit-content; + max-width: 3rem; + } } &.active { background: var(--button-tab-background); - > .badge { + > span > .badge { border: 1px solid var(--border-secondary-color); } } - &.canvas { - color: var(--text-color-tertiary); + &.preload { + background: var(--shimmer-foreground); + } - &.active { - color: var(--text-color-primary); - background: var(--button-tab-canvas-background); + &.canvas { + &.preload { + background: var(--shimmer-foreground); + opacity: 1; } } } diff --git a/src/kits/Structure/PageTitleTabs/index.tsx b/src/kits/Structure/PageTitleTabs/index.tsx index 5855b96cb5..6d0a9b51bf 100644 --- a/src/kits/Structure/PageTitleTabs/index.tsx +++ b/src/kits/Structure/PageTitleTabs/index.tsx @@ -23,7 +23,17 @@ export const PageTitleTabs = ({
{tabs.map( - ({ active, onClick, title, badge }: PageTitleTabProps, i: number) => ( + ( + { + active, + onClick, + title, + badge, + disabled, + asPreloader, + }: PageTitleTabProps, + i: number + ) => ( onClick()} title={title} badge={badge} + disabled={disabled === undefined ? false : disabled} + asPreloader={asPreloader == undefined ? false : asPreloader} /> ) )} diff --git a/src/kits/Structure/PageTitleTabs/types.ts b/src/kits/Structure/PageTitleTabs/types.ts index 431f403527..83022a7732 100644 --- a/src/kits/Structure/PageTitleTabs/types.ts +++ b/src/kits/Structure/PageTitleTabs/types.ts @@ -19,4 +19,8 @@ export interface PageTitleTabProps { onClick: () => void; // a badge that can have a glance at before visting the tab page. badge?: string | number; + // whether the tab button is disabled. + disabled?: boolean; + // whether the tab is acting as a preloader. + asPreloader?: boolean; } diff --git a/src/library/CallToAction/index.tsx b/src/library/CallToAction/index.tsx index 6c6ac900d8..5a74ed0c7a 100644 --- a/src/library/CallToAction/index.tsx +++ b/src/library/CallToAction/index.tsx @@ -189,59 +189,6 @@ export const CallToActionWrapper = styled.div` margin-left: 0.75rem; } - .loader { - height: 0.8rem; - margin-left: 1.6rem; - aspect-ratio: 5; - --_g: no-repeat radial-gradient(farthest-side, white 94%, #0000); - background: var(--_g), var(--_g), var(--_g), var(--_g); - background-size: 20% 100%; - animation: - l40-1 0.75s infinite alternate, - l40-2 1.5s infinite alternate; - } - @keyframes l40-1 { - 0%, - 10% { - background-position: - 0 0, - 0 0, - 0 0, - 0 0; - } - 33% { - background-position: - 0 0, - calc(100% / 3) 0, - calc(100% / 3) 0, - calc(100% / 3) 0; - } - 66% { - background-position: - 0 0, - calc(100% / 3) 0, - calc(2 * 100% / 3) 0, - calc(2 * 100% / 3) 0; - } - 90%, - 100% { - background-position: - 0 0, - calc(100% / 3) 0, - calc(2 * 100% / 3) 0, - 100% 0; - } - } - @keyframes l40-2 { - 0%, - 49.99% { - transform: scale(1); - } - 50%, - 100% { - transform: scale(-1); - } - } &:disabled { cursor: default; } diff --git a/src/library/Loader/Wrappers.ts b/src/library/Loader/Wrappers.ts index d350113cb1..954606fe47 100644 --- a/src/library/Loader/Wrappers.ts +++ b/src/library/Loader/Wrappers.ts @@ -13,7 +13,7 @@ export const LoaderWrapper = styled.div` var(--shimmer-foreground) 100% ); background-repeat: no-repeat; - background-size: 600px 104px; + background-size: 600px 100%; animation-duration: 1.5s; animation-fill-mode: forwards; animation-iteration-count: infinite; diff --git a/src/library/PoolSync/Loader.ts b/src/library/PoolSync/Loader.ts new file mode 100644 index 0000000000..f40091beb1 --- /dev/null +++ b/src/library/PoolSync/Loader.ts @@ -0,0 +1,59 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const StyledLoader = styled.div` + height: 0.8rem; + margin-left: 1.6rem; + aspect-ratio: 5; + --_g: no-repeat radial-gradient(farthest-side, white 94%, #0000); + background: var(--_g), var(--_g), var(--_g), var(--_g); + background-size: 20% 100%; + animation: + l40-1 0.75s infinite alternate, + l40-2 1.5s infinite alternate; + + @keyframes l40-1 { + 0%, + 10% { + background-position: + 0 0, + 0 0, + 0 0, + 0 0; + } + 33% { + background-position: + 0 0, + calc(100% / 3) 0, + calc(100% / 3) 0, + calc(100% / 3) 0; + } + 66% { + background-position: + 0 0, + calc(100% / 3) 0, + calc(2 * 100% / 3) 0, + calc(2 * 100% / 3) 0; + } + 90%, + 100% { + background-position: + 0 0, + calc(100% / 3) 0, + calc(2 * 100% / 3) 0, + 100% 0; + } + } + @keyframes l40-2 { + 0%, + 49.99% { + transform: scale(1); + } + 50%, + 100% { + transform: scale(-1); + } + } +`; diff --git a/src/pages/Pools/Home/Status/FindingPoolPercent.tsx b/src/library/PoolSync/index.tsx similarity index 87% rename from src/pages/Pools/Home/Status/FindingPoolPercent.tsx rename to src/library/PoolSync/index.tsx index 668c087e28..216cee80e5 100644 --- a/src/pages/Pools/Home/Status/FindingPoolPercent.tsx +++ b/src/library/PoolSync/index.tsx @@ -4,7 +4,7 @@ import BigNumber from 'bignumber.js'; import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; -export const FindingPoolsPercent = () => { +export const PoolSync = ({ label }: { label?: string }) => { const { getPoolPerformanceTask } = usePoolPerformance(); // Get the pool performance task to determine if performance data is ready. @@ -24,7 +24,7 @@ export const FindingPoolsPercent = () => { return ( - {percentPassed.decimalPlaces(0).toFormat()}% + {percentPassed.decimalPlaces(0).toFormat()}%{label && ` ${label}`} ); }; diff --git a/src/locale/cn/library.json b/src/locale/cn/library.json index a1aab456a0..65e3b9ec4b 100644 --- a/src/locale/cn/library.json +++ b/src/locale/cn/library.json @@ -23,6 +23,7 @@ "allowCompound": "允许复利", "allowWithdraw": "允许取出收益", "alreadyImported": "地址已导入", + "analyzingPoolPerformance": "分析提名池性能并找到最佳提名池", "asAPoolMember": "作为提名池成员", "asThePoolDepositor": "作为提名池存款人", "atLeast": "质押金最低为", diff --git a/src/locale/en/library.json b/src/locale/en/library.json index 29d1faaa69..43023d92c6 100644 --- a/src/locale/en/library.json +++ b/src/locale/en/library.json @@ -23,6 +23,7 @@ "allowCompound": "Allow Compound", "allowWithdraw": "Allow Withdraw", "alreadyImported": "Address Already Imported", + "analyzingPoolPerformance": "Analyzing pool performance and finding optimal pools.", "asAPoolMember": "as a pool member.", "asThePoolDepositor": "as the pool depositor.", "atLeast": "Bond amount must be at least", diff --git a/src/pages/Pools/Home/Status/NewMember.tsx b/src/pages/Pools/Home/Status/NewMember.tsx index 480efffe1c..82d5f3aff2 100644 --- a/src/pages/Pools/Home/Status/NewMember.tsx +++ b/src/pages/Pools/Home/Status/NewMember.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { CallToActionWrapper } from '../../../../library/CallToAction'; +import { CallToActionWrapper } from 'library/CallToAction'; import { faChevronRight, faUserGroup, @@ -15,8 +15,9 @@ import { useOverlay } from 'kits/Overlay/Provider'; import type { NewMemberProps } from './types'; import { CallToActionLoader } from 'library/Loader/CallToAction'; import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; -import { FindingPoolsPercent } from './FindingPoolPercent'; +import { PoolSync } from 'library/PoolSync'; import { useJoinPools } from 'contexts/Pools/JoinPools'; +import { StyledLoader } from 'library/PoolSync/Loader'; export const NewMember = ({ syncing }: NewMemberProps) => { const { t } = useTranslation(); @@ -46,23 +47,20 @@ export const NewMember = ({ syncing }: NewMemberProps) => {
From 078551973ddc822859eab139e01a3a3f7e348057 Mon Sep 17 00:00:00 2001 From: divdeploy <166095818+divdeploy@users.noreply.github.com> Date: Fri, 5 Apr 2024 17:10:07 +0800 Subject: [PATCH 18/37] remove repetitive word in comment (#2060) Signed-off-by: divdeploy --- src/contexts/Pools/BondedPools/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contexts/Pools/BondedPools/index.tsx b/src/contexts/Pools/BondedPools/index.tsx index f064555b52..e110524485 100644 --- a/src/contexts/Pools/BondedPools/index.tsx +++ b/src/contexts/Pools/BondedPools/index.tsx @@ -292,7 +292,7 @@ export const BondedPoolsProvider = ({ children }: { children: ReactNode }) => { }; // Gets all pools that the account has a role in. Returns an object with each pool role as keys, - // and and array of pool ids as their values. + // and array of pool ids as their values. const accumulateAccountPoolRoles = (who: MaybeAddress): AccountPoolRoles => { if (!who) { return { From 65176b4f1cf735c9b51a07154b88b63803f0b239 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Fri, 5 Apr 2024 16:23:25 +0700 Subject: [PATCH 19/37] chore: loader animation fix --- src/library/Loader/Wrappers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/library/Loader/Wrappers.ts b/src/library/Loader/Wrappers.ts index 954606fe47..c601ccb3b8 100644 --- a/src/library/Loader/Wrappers.ts +++ b/src/library/Loader/Wrappers.ts @@ -13,7 +13,7 @@ export const LoaderWrapper = styled.div` var(--shimmer-foreground) 100% ); background-repeat: no-repeat; - background-size: 600px 100%; + background-size: 60% 100%; animation-duration: 1.5s; animation-fill-mode: forwards; animation-iteration-count: infinite; @@ -30,7 +30,7 @@ export const LoaderWrapper = styled.div` background-position: 0px 0; } 100% { - background-position: 150% 0; + background-position: 200% 0; } } `; From de3ad50ed2eda49a0378a26c22fb8a48fdc9e305 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Fri, 5 Apr 2024 16:50:33 +0700 Subject: [PATCH 20/37] feat: Create pool canvas (#2061) --- .../CreatePool}/Bond/index.tsx | 0 .../CreatePool}/PoolName/Input.tsx | 0 .../CreatePool}/PoolName/index.tsx | 0 .../CreatePool}/PoolRoles/index.tsx | 2 +- .../CreatePool}/Summary/Wrapper.ts | 0 .../CreatePool}/Summary/index.tsx | 7 +- src/canvas/CreatePool/index.tsx | 70 ++++++++++++++ src/canvas/JoinPool/Header.tsx | 8 +- src/canvas/JoinPool/Preloader.tsx | 8 +- src/canvas/JoinPool/Wrappers.ts | 89 ------------------ src/canvas/Wrappers.ts | 93 +++++++++++++++++++ src/contexts/Setup/defaults.ts | 2 - src/contexts/Setup/index.tsx | 8 -- src/contexts/Setup/types.ts | 2 - src/library/Prompt/Wrappers.ts | 2 +- src/library/SideMenu/Main.tsx | 10 -- src/overlay/index.tsx | 2 + src/pages/Pools/Create/index.tsx | 85 ----------------- src/pages/Pools/Home/Status/NewMember.tsx | 21 ++--- src/pages/Pools/index.tsx | 7 +- 20 files changed, 190 insertions(+), 226 deletions(-) rename src/{pages/Pools/Create => canvas/CreatePool}/Bond/index.tsx (100%) rename src/{pages/Pools/Create => canvas/CreatePool}/PoolName/Input.tsx (100%) rename src/{pages/Pools/Create => canvas/CreatePool}/PoolName/index.tsx (100%) rename src/{pages/Pools/Create => canvas/CreatePool}/PoolRoles/index.tsx (98%) rename src/{pages/Pools/Create => canvas/CreatePool}/Summary/Wrapper.ts (100%) rename src/{pages/Pools/Create => canvas/CreatePool}/Summary/index.tsx (97%) create mode 100644 src/canvas/CreatePool/index.tsx delete mode 100644 src/pages/Pools/Create/index.tsx diff --git a/src/pages/Pools/Create/Bond/index.tsx b/src/canvas/CreatePool/Bond/index.tsx similarity index 100% rename from src/pages/Pools/Create/Bond/index.tsx rename to src/canvas/CreatePool/Bond/index.tsx diff --git a/src/pages/Pools/Create/PoolName/Input.tsx b/src/canvas/CreatePool/PoolName/Input.tsx similarity index 100% rename from src/pages/Pools/Create/PoolName/Input.tsx rename to src/canvas/CreatePool/PoolName/Input.tsx diff --git a/src/pages/Pools/Create/PoolName/index.tsx b/src/canvas/CreatePool/PoolName/index.tsx similarity index 100% rename from src/pages/Pools/Create/PoolName/index.tsx rename to src/canvas/CreatePool/PoolName/index.tsx diff --git a/src/pages/Pools/Create/PoolRoles/index.tsx b/src/canvas/CreatePool/PoolRoles/index.tsx similarity index 98% rename from src/pages/Pools/Create/PoolRoles/index.tsx rename to src/canvas/CreatePool/PoolRoles/index.tsx index 60450fc52b..65fbf2a644 100644 --- a/src/pages/Pools/Create/PoolRoles/index.tsx +++ b/src/canvas/CreatePool/PoolRoles/index.tsx @@ -9,7 +9,7 @@ import { Header } from 'library/SetupSteps/Header'; import { MotionContainer } from 'library/SetupSteps/MotionContainer'; import type { SetupStepProps } from 'library/SetupSteps/types'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { Roles } from '../../Roles'; +import { Roles } from 'pages/Pools/Roles'; import type { PoolProgress } from 'contexts/Setup/types'; import type { PoolRoles as PoolRolesInterface } from 'contexts/Pools/ActivePool/types'; diff --git a/src/pages/Pools/Create/Summary/Wrapper.ts b/src/canvas/CreatePool/Summary/Wrapper.ts similarity index 100% rename from src/pages/Pools/Create/Summary/Wrapper.ts rename to src/canvas/CreatePool/Summary/Wrapper.ts diff --git a/src/pages/Pools/Create/Summary/index.tsx b/src/canvas/CreatePool/Summary/index.tsx similarity index 97% rename from src/pages/Pools/Create/Summary/index.tsx rename to src/canvas/CreatePool/Summary/index.tsx index 80315cfe9e..7e992e4df5 100644 --- a/src/pages/Pools/Create/Summary/index.tsx +++ b/src/canvas/CreatePool/Summary/index.tsx @@ -21,6 +21,7 @@ import { useApi } from 'contexts/Api'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { SummaryWrapper } from './Wrapper'; +import { useOverlay } from 'kits/Overlay/Provider'; export const Summary = ({ section }: SetupStepProps) => { const { t } = useTranslation('pages'); @@ -32,11 +33,12 @@ export const Summary = ({ section }: SetupStepProps) => { networkData: { units, unit }, } = useNetwork(); const { newBatchCall } = useBatchCall(); + const { closeCanvas } = useOverlay().canvas; const { accountHasSigner } = useImportedAccounts(); const { getPoolSetup, removeSetupProgress } = useSetup(); + const { activeAccount, activeProxy } = useActiveAccounts(); const { queryPoolMember, addToPoolMembers } = usePoolMembers(); const { queryBondedPool, addToBondedPools } = useBondedPools(); - const { activeAccount, activeProxy } = useActiveAccounts(); const poolId = lastPoolId.plus(1); const setup = getPoolSetup(activeAccount); @@ -74,6 +76,9 @@ export const Summary = ({ section }: SetupStepProps) => { from: activeAccount, shouldSubmit: true, callbackInBlock: async () => { + // Close canvas. + closeCanvas(); + // query and add created pool to bondedPools list const pool = await queryBondedPool(poolId.toNumber()); addToBondedPools(pool); diff --git a/src/canvas/CreatePool/index.tsx b/src/canvas/CreatePool/index.tsx new file mode 100644 index 0000000000..048dd6db2f --- /dev/null +++ b/src/canvas/CreatePool/index.tsx @@ -0,0 +1,70 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import { Element } from 'react-scroll'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { Nominate } from 'library/SetupSteps/Nominate'; +import { Summary } from 'canvas/CreatePool/Summary'; +import { Bond } from 'canvas/CreatePool/Bond'; +import { PoolName } from 'canvas/CreatePool/PoolName'; +import { PoolRoles } from 'canvas/CreatePool/PoolRoles'; +import { CanvasFullScreenWrapper, CanvasTitleWrapper } from 'canvas/Wrappers'; +import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; +import { useOverlay } from 'kits/Overlay/Provider'; + +export const CreatePool = () => { + const { t } = useTranslation(); + const { closeCanvas } = useOverlay().canvas; + + return ( + +
+ closeCanvas()} + iconLeft={faTimes} + style={{ marginLeft: '1.1rem' }} + /> +
+ + +
+
+
+
+

{t('pools.createAPool', { ns: 'pages' })}

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/canvas/JoinPool/Header.tsx b/src/canvas/JoinPool/Header.tsx index 4b17920a72..93109708a9 100644 --- a/src/canvas/JoinPool/Header.tsx +++ b/src/canvas/JoinPool/Header.tsx @@ -8,7 +8,6 @@ import { } from '@fortawesome/free-solid-svg-icons'; import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; import { ButtonPrimaryInvert } from 'kits/Buttons/ButtonPrimaryInvert'; -import { TitleWrapper } from './Wrappers'; import { Polkicon } from '@w3ux/react-polkicon'; import { determinePoolDisplay, remToUnit } from '@w3ux/utils'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -16,6 +15,7 @@ import { PageTitleTabs } from 'kits/Structure/PageTitleTabs'; import { useTranslation } from 'react-i18next'; import { useOverlay } from 'kits/Overlay/Provider'; import type { JoinPoolHeaderProps } from './types'; +import { CanvasTitleWrapper } from 'canvas/Wrappers'; export const Header = ({ activeTab, @@ -51,14 +51,14 @@ export const Header = ({ lg /> closeCanvas()} iconLeft={faTimes} style={{ marginLeft: '1.1rem' }} />
- +
- + ); }; diff --git a/src/canvas/JoinPool/Preloader.tsx b/src/canvas/JoinPool/Preloader.tsx index 742ae6cb3f..adb76055f1 100644 --- a/src/canvas/JoinPool/Preloader.tsx +++ b/src/canvas/JoinPool/Preloader.tsx @@ -10,12 +10,12 @@ import { JoinFormWrapper, JoinPoolInterfaceWrapper, PreloaderWrapper, - TitleWrapper, } from './Wrappers'; import { PoolSync } from 'library/PoolSync'; import { CallToActionLoader } from 'library/Loader/CallToAction'; import { LoaderWrapper } from 'library/Loader/Wrappers'; import { PageTitleTabs } from 'kits/Structure/PageTitleTabs'; +import { CanvasTitleWrapper } from 'canvas/Wrappers'; export const Preloader = () => { const { t } = useTranslation(); @@ -31,14 +31,14 @@ export const Preloader = () => { lg /> closeCanvas()} iconLeft={faTimes} style={{ marginLeft: '1.1rem' }} />
- +
@@ -78,7 +78,7 @@ export const Preloader = () => { tabClassName="canvas" inline={true} /> - +
diff --git a/src/canvas/JoinPool/Wrappers.ts b/src/canvas/JoinPool/Wrappers.ts index a1f298d251..99a46091a3 100644 --- a/src/canvas/JoinPool/Wrappers.ts +++ b/src/canvas/JoinPool/Wrappers.ts @@ -61,95 +61,6 @@ export const PreloaderWrapper = styled.div` opacity: 0.4; `; -export const TitleWrapper = styled.div` - border-bottom: 1px solid var(--border-secondary-color); - flex: 1; - display: flex; - flex-direction: column; - margin: 2rem 0 1.55rem 0; - padding-bottom: 0.1rem; - - > .inner { - display: flex; - align-items: center; - margin-bottom: 0.5rem; - flex: 1; - - > div { - display: flex; - flex: 1; - - &:nth-child(1) { - max-width: 4rem; - - &.empty { - max-width: 0px; - } - } - - &:nth-child(2) { - padding-left: 1rem; - flex-direction: column; - - &.standalone { - padding-left: 0; - } - - > .title { - position: relative; - padding-top: 2rem; - flex: 1; - - h1 { - position: absolute; - top: 0; - left: 0; - margin: 0; - line-height: 2.2rem; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - width: 100%; - } - } - - > .labels { - display: flex; - margin-top: 1.1rem; - - > h3 { - color: var(--text-color-secondary); - font-family: Inter, sans-serif; - margin: 0; - - > svg { - margin: 0 0 0 0.2rem; - } - - > span { - border: 1px solid var(--border-secondary-color); - border-radius: 0.5rem; - padding: 0.4rem 0.6rem; - margin-left: 1rem; - font-size: 1.1rem; - - &.blocked { - color: var(--accent-color-secondary); - border-color: var(--accent-color-secondary); - } - - &.destroying { - color: var(--status-danger-color); - border-color: var(--status-danger-color); - } - } - } - } - } - } - } -`; - export const JoinFormWrapper = styled.div` background: var(--background-canvas-card); border: 0.75px solid var(--border-primary-color); diff --git a/src/canvas/Wrappers.ts b/src/canvas/Wrappers.ts index 9b1d29a4ec..5a03b1a6ea 100644 --- a/src/canvas/Wrappers.ts +++ b/src/canvas/Wrappers.ts @@ -21,6 +21,99 @@ export const CanvasFullScreenWrapper = styled.div` } `; +export const CanvasTitleWrapper = styled.div` + border-bottom: 1px solid var(--border-secondary-color); + flex: 1; + display: flex; + flex-direction: column; + margin: 2rem 0 1.55rem 0; + padding-bottom: 0.1rem; + + > .inner { + display: flex; + align-items: center; + margin-bottom: 0.5rem; + flex: 1; + + &.standalone { + padding-bottom: 0.5rem; + } + + > div { + display: flex; + flex: 1; + + &:nth-child(1) { + max-width: 4rem; + + &.empty { + max-width: 0px; + } + } + + &:nth-child(2) { + padding-left: 1rem; + flex-direction: column; + + &.standalone { + padding-left: 0; + } + + > .title { + position: relative; + padding-top: 2rem; + flex: 1; + + h1 { + position: absolute; + top: 0; + left: 0; + margin: 0; + line-height: 2.2rem; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + } + } + + > .labels { + display: flex; + margin-top: 1.1rem; + + > h3 { + color: var(--text-color-secondary); + font-family: Inter, sans-serif; + margin: 0; + + > svg { + margin: 0 0 0 0.2rem; + } + + > span { + border: 1px solid var(--border-secondary-color); + border-radius: 0.5rem; + padding: 0.4rem 0.6rem; + margin-left: 1rem; + font-size: 1.1rem; + + &.blocked { + color: var(--accent-color-secondary); + border-color: var(--accent-color-secondary); + } + + &.destroying { + color: var(--status-danger-color); + border-color: var(--status-danger-color); + } + } + } + } + } + } + } +`; + export const CanvasSubmitTxFooter = styled.div` border-radius: 1rem; overflow: hidden; diff --git a/src/contexts/Setup/defaults.ts b/src/contexts/Setup/defaults.ts index 1309ba94ef..b9beaf9e34 100644 --- a/src/contexts/Setup/defaults.ts +++ b/src/contexts/Setup/defaults.ts @@ -31,9 +31,7 @@ export const defaultSetupContext: SetupContextInterface = { setActiveAccountSetup: (t, p) => {}, setActiveAccountSetupSection: (t, s) => {}, setOnNominatorSetup: (v) => {}, - setOnPoolSetup: (v) => {}, onNominatorSetup: false, - onPoolSetup: false, getNominatorSetup: (address) => ({ section: 1, progress: defaultNominatorProgress, diff --git a/src/contexts/Setup/index.tsx b/src/contexts/Setup/index.tsx index 2da627cb08..462f680f86 100644 --- a/src/contexts/Setup/index.tsx +++ b/src/contexts/Setup/index.tsx @@ -50,9 +50,6 @@ export const SetupProvider = ({ children }: { children: ReactNode }) => { // is the user actively on the setup page const [onNominatorSetup, setOnNominatorSetup] = useState(false); - // is the user actively on the pool creation page - const [onPoolSetup, setOnPoolSetup] = useState(false); - // Store all imported accounts nominator setups. const [nominatorSetups, setNominatorSetups] = useState({}); @@ -276,9 +273,6 @@ export const SetupProvider = ({ children }: { children: ReactNode }) => { if (!inSetup()) { setOnNominatorSetup(false); } - if (poolMembership) { - setOnPoolSetup(false); - } }, [inSetup(), network, poolMembership]); // Update setup state when activeAccount changes @@ -297,9 +291,7 @@ export const SetupProvider = ({ children }: { children: ReactNode }) => { setActiveAccountSetup, setActiveAccountSetupSection, setOnNominatorSetup, - setOnPoolSetup, onNominatorSetup, - onPoolSetup, getNominatorSetup, getPoolSetup, }} diff --git a/src/contexts/Setup/types.ts b/src/contexts/Setup/types.ts index aba1b0ca80..594ae87231 100644 --- a/src/contexts/Setup/types.ts +++ b/src/contexts/Setup/types.ts @@ -54,9 +54,7 @@ export interface SetupContextInterface { ) => void; setActiveAccountSetupSection: (t: BondFor, s: number) => void; setOnNominatorSetup: (v: boolean) => void; - setOnPoolSetup: (v: boolean) => void; onNominatorSetup: boolean; - onPoolSetup: boolean; getNominatorSetup: (address: MaybeAddress) => NominatorSetup; getPoolSetup: (address: MaybeAddress) => PoolSetup; } diff --git a/src/library/Prompt/Wrappers.ts b/src/library/Prompt/Wrappers.ts index 3a16c2c50a..ab23592d5a 100644 --- a/src/library/Prompt/Wrappers.ts +++ b/src/library/Prompt/Wrappers.ts @@ -89,7 +89,7 @@ export const TitleWrapper = styled.div` align-items: center; padding: 0 0.5rem; - button { + > button { padding: 0; } diff --git a/src/library/SideMenu/Main.tsx b/src/library/SideMenu/Main.tsx index 97f1c0d7a5..fc42c010bd 100644 --- a/src/library/SideMenu/Main.tsx +++ b/src/library/SideMenu/Main.tsx @@ -34,7 +34,6 @@ export const Main = () => { const { inSetup: inNominatorSetup, addressDifferentToStash } = useStaking(); const { onNominatorSetup, - onPoolSetup, getPoolSetupPercent, getNominatorSetupPercent, }: SetupContextInterface = useSetup(); @@ -102,7 +101,6 @@ export const Main = () => { if (uri === `${import.meta.env.BASE_URL}pools`) { // configure Pools action const inPool = membership; - const setupPercent = getPoolSetupPercent(activeAccount); if (inPool) { pages[i].action = { @@ -111,13 +109,6 @@ export const Main = () => { text: t('active'), }; } - if (!inPool && (setupPercent > 0 || onPoolSetup)) { - pages[i].action = { - type: 'text', - status: 'warning', - text: `${setupPercent}%`, - }; - } } i++; } @@ -138,7 +129,6 @@ export const Main = () => { getPoolSetupPercent(activeAccount), i18n.resolvedLanguage, onNominatorSetup, - onPoolSetup, ]); // remove pages that network does not support diff --git a/src/overlay/index.tsx b/src/overlay/index.tsx index 287b125755..6d7d92919f 100644 --- a/src/overlay/index.tsx +++ b/src/overlay/index.tsx @@ -31,6 +31,7 @@ import { ValidatorGeo } from '../modals/ValidatorGeo'; import { ManageNominations } from '../canvas/ManageNominations'; import { PoolMembers } from 'canvas/PoolMembers'; import { JoinPool } from 'canvas/JoinPool'; +import { CreatePool } from 'canvas/CreatePool'; import { Overlay } from 'kits/Overlay'; export const Overlays = () => { @@ -70,6 +71,7 @@ export const Overlays = () => { ManageNominations, PoolMembers, JoinPool, + CreatePool, }} /> ); diff --git a/src/pages/Pools/Create/index.tsx b/src/pages/Pools/Create/index.tsx deleted file mode 100644 index 75c6f4c576..0000000000 --- a/src/pages/Pools/Create/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; -import { PageRow } from 'kits/Structure/PageRow'; -import { useTranslation } from 'react-i18next'; -import { Element } from 'react-scroll'; -import { useSetup } from 'contexts/Setup'; -import { CardWrapper } from 'library/Card/Wrappers'; -import { Nominate } from 'library/SetupSteps/Nominate'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { Bond } from './Bond'; -import { PoolName } from './PoolName'; -import { PoolRoles } from './PoolRoles'; -import { Summary } from './Summary'; -import { ButtonSecondary } from 'kits/Buttons/ButtonSecondary'; -import { PageTitle } from 'kits/Structure/PageTitle'; -import { PageHeadingWrapper } from 'kits/Structure/PageHeading/Wrapper'; - -export const Create = () => { - const { t } = useTranslation('pages'); - const { activeAccount } = useActiveAccounts(); - const { setOnPoolSetup, removeSetupProgress } = useSetup(); - - return ( - <> - - - - - setOnPoolSetup(false)} - lg - /> - - - { - setOnPoolSetup(false); - removeSetupProgress('pool', activeAccount); - }} - lg - /> - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/src/pages/Pools/Home/Status/NewMember.tsx b/src/pages/Pools/Home/Status/NewMember.tsx index 82d5f3aff2..c4a47b4dba 100644 --- a/src/pages/Pools/Home/Status/NewMember.tsx +++ b/src/pages/Pools/Home/Status/NewMember.tsx @@ -3,12 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CallToActionWrapper } from 'library/CallToAction'; -import { - faChevronRight, - faUserGroup, - faUserPlus, -} from '@fortawesome/free-solid-svg-icons'; -import { useSetup } from 'contexts/Setup'; +import { faUserGroup, faUserPlus } from '@fortawesome/free-solid-svg-icons'; import { useStatusButtons } from './useStatusButtons'; import { useTranslation } from 'react-i18next'; import { useOverlay } from 'kits/Overlay/Provider'; @@ -21,7 +16,6 @@ import { StyledLoader } from 'library/PoolSync/Loader'; export const NewMember = ({ syncing }: NewMemberProps) => { const { t } = useTranslation(); - const { setOnPoolSetup } = useSetup(); const { poolsForJoin } = useJoinPools(); const { openCanvas } = useOverlay().canvas; const { startJoinPoolFetch } = useJoinPools(); @@ -84,7 +78,6 @@ export const NewMember = ({ syncing }: NewMemberProps) => { )} -
@@ -96,14 +89,16 @@ export const NewMember = ({ syncing }: NewMemberProps) => { className={`button secondary standalone${createDisabled ? ` disabled` : ``}`} >
diff --git a/src/pages/Pools/index.tsx b/src/pages/Pools/index.tsx index 6918c650ea..5d5f50cdff 100644 --- a/src/pages/Pools/index.tsx +++ b/src/pages/Pools/index.tsx @@ -1,11 +1,6 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { useSetup } from 'contexts/Setup'; -import { Create } from './Create'; import { Home } from './Home'; -export const Pools = () => { - const { onPoolSetup } = useSetup(); - return onPoolSetup ? : ; -}; +export const Pools = () => ; From 0208d5fc5658bc375eeef3aa853954c05290796f Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Fri, 5 Apr 2024 17:09:49 +0700 Subject: [PATCH 21/37] feat: Start nominating canvas (#2062) --- src/canvas/CreatePool/Summary/index.tsx | 6 +- .../NominatorSetup}/Bond/index.tsx | 0 .../NominatorSetup}/Payee/index.tsx | 0 .../NominatorSetup}/Summary/Wrapper.ts | 0 .../NominatorSetup}/Summary/index.tsx | 6 ++ src/canvas/NominatorSetup/index.tsx | 64 +++++++++++++ src/contexts/Setup/defaults.ts | 2 - src/contexts/Setup/index.tsx | 18 ---- src/contexts/Setup/types.ts | 2 - src/library/SideMenu/Main.tsx | 10 --- src/overlay/index.tsx | 2 + .../Nominate/Active/Status/NewNominator.tsx | 59 +++++++----- src/pages/Nominate/Setup/index.tsx | 89 ------------------- src/pages/Nominate/index.tsx | 11 ++- src/pages/Pools/Home/Status/NewMember.tsx | 4 +- 15 files changed, 117 insertions(+), 156 deletions(-) rename src/{pages/Nominate/Setup => canvas/NominatorSetup}/Bond/index.tsx (100%) rename src/{pages/Nominate/Setup => canvas/NominatorSetup}/Payee/index.tsx (100%) rename src/{pages/Nominate/Setup => canvas/NominatorSetup}/Summary/Wrapper.ts (100%) rename src/{pages/Nominate/Setup => canvas/NominatorSetup}/Summary/index.tsx (95%) create mode 100644 src/canvas/NominatorSetup/index.tsx delete mode 100644 src/pages/Nominate/Setup/index.tsx diff --git a/src/canvas/CreatePool/Summary/index.tsx b/src/canvas/CreatePool/Summary/index.tsx index 7e992e4df5..e69b39d7fd 100644 --- a/src/canvas/CreatePool/Summary/index.tsx +++ b/src/canvas/CreatePool/Summary/index.tsx @@ -79,17 +79,17 @@ export const Summary = ({ section }: SetupStepProps) => { // Close canvas. closeCanvas(); - // query and add created pool to bondedPools list + // Query and add created pool to bondedPools list. const pool = await queryBondedPool(poolId.toNumber()); addToBondedPools(pool); - // query and add account to poolMembers list + // Query and add account to poolMembers list. const member = await queryPoolMember(activeAccount); if (member) { addToPoolMembers(member); } - // reset localStorage setup progress + // Reset setup progress. removeSetupProgress('pool', activeAccount); }, }); diff --git a/src/pages/Nominate/Setup/Bond/index.tsx b/src/canvas/NominatorSetup/Bond/index.tsx similarity index 100% rename from src/pages/Nominate/Setup/Bond/index.tsx rename to src/canvas/NominatorSetup/Bond/index.tsx diff --git a/src/pages/Nominate/Setup/Payee/index.tsx b/src/canvas/NominatorSetup/Payee/index.tsx similarity index 100% rename from src/pages/Nominate/Setup/Payee/index.tsx rename to src/canvas/NominatorSetup/Payee/index.tsx diff --git a/src/pages/Nominate/Setup/Summary/Wrapper.ts b/src/canvas/NominatorSetup/Summary/Wrapper.ts similarity index 100% rename from src/pages/Nominate/Setup/Summary/Wrapper.ts rename to src/canvas/NominatorSetup/Summary/Wrapper.ts diff --git a/src/pages/Nominate/Setup/Summary/index.tsx b/src/canvas/NominatorSetup/Summary/index.tsx similarity index 95% rename from src/pages/Nominate/Setup/Summary/index.tsx rename to src/canvas/NominatorSetup/Summary/index.tsx index e782f3c8c7..e948318fb8 100644 --- a/src/pages/Nominate/Setup/Summary/index.tsx +++ b/src/canvas/NominatorSetup/Summary/index.tsx @@ -20,6 +20,7 @@ import { useApi } from 'contexts/Api'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { SummaryWrapper } from './Wrapper'; +import { useOverlay } from 'kits/Overlay/Provider'; export const Summary = ({ section }: SetupStepProps) => { const { t } = useTranslation('pages'); @@ -29,6 +30,7 @@ export const Summary = ({ section }: SetupStepProps) => { } = useNetwork(); const { newBatchCall } = useBatchCall(); const { getPayeeItems } = usePayeeConfig(); + const { closeCanvas } = useOverlay().canvas; const { accountHasSigner } = useImportedAccounts(); const { activeAccount, activeProxy } = useActiveAccounts(); const { getNominatorSetup, removeSetupProgress } = useSetup(); @@ -70,6 +72,10 @@ export const Summary = ({ section }: SetupStepProps) => { from: activeAccount, shouldSubmit: true, callbackInBlock: () => { + // Close the canvas after the extrinsic is included in a block. + closeCanvas(); + + // Reset setup progress. removeSetupProgress('nominator', activeAccount); }, }); diff --git a/src/canvas/NominatorSetup/index.tsx b/src/canvas/NominatorSetup/index.tsx new file mode 100644 index 0000000000..cecec5bac0 --- /dev/null +++ b/src/canvas/NominatorSetup/index.tsx @@ -0,0 +1,64 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import { Element } from 'react-scroll'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { Nominate } from 'library/SetupSteps/Nominate'; +import { Payee } from 'canvas/NominatorSetup/Payee'; +import { Bond } from 'canvas/NominatorSetup/Bond'; +import { Summary } from 'canvas/NominatorSetup/Summary'; +import { CanvasFullScreenWrapper, CanvasTitleWrapper } from 'canvas/Wrappers'; +import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; +import { useOverlay } from 'kits/Overlay/Provider'; + +export const NominatorSetup = () => { + const { t } = useTranslation('pages'); + const { closeCanvas } = useOverlay().canvas; + + return ( + +
+ closeCanvas()} + iconLeft={faTimes} + style={{ marginLeft: '1.1rem' }} + /> +
+ + +
+
+
+
+

{t('nominate.startNominating')}

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/contexts/Setup/defaults.ts b/src/contexts/Setup/defaults.ts index b9beaf9e34..58eb99dd21 100644 --- a/src/contexts/Setup/defaults.ts +++ b/src/contexts/Setup/defaults.ts @@ -30,8 +30,6 @@ export const defaultSetupContext: SetupContextInterface = { getPoolSetupPercent: (a) => 0, setActiveAccountSetup: (t, p) => {}, setActiveAccountSetupSection: (t, s) => {}, - setOnNominatorSetup: (v) => {}, - onNominatorSetup: false, getNominatorSetup: (address) => ({ section: 1, progress: defaultNominatorProgress, diff --git a/src/contexts/Setup/index.tsx b/src/contexts/Setup/index.tsx index 462f680f86..8aff6e9986 100644 --- a/src/contexts/Setup/index.tsx +++ b/src/contexts/Setup/index.tsx @@ -13,7 +13,6 @@ import { useEffectIgnoreInitial } from '@w3ux/hooks'; import { useNetwork } from 'contexts/Network'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; -import { useStaking } from '../Staking'; import { defaultNominatorProgress, defaultPoolProgress, @@ -28,7 +27,6 @@ import type { PoolSetups, SetupContextInterface, } from './types'; -import { useBalances } from 'contexts/Balances'; export const SetupContext = createContext(defaultSetupContext); @@ -36,20 +34,13 @@ export const SetupContext = export const useSetup = () => useContext(SetupContext); export const SetupProvider = ({ children }: { children: ReactNode }) => { - const { inSetup } = useStaking(); const { network, networkData: { units }, } = useNetwork(); const { accounts } = useImportedAccounts(); - const { getPoolMembership } = useBalances(); const { activeAccount } = useActiveAccounts(); - const poolMembership = getPoolMembership(activeAccount); - - // is the user actively on the setup page - const [onNominatorSetup, setOnNominatorSetup] = useState(false); - // Store all imported accounts nominator setups. const [nominatorSetups, setNominatorSetups] = useState({}); @@ -268,13 +259,6 @@ export const SetupProvider = ({ children }: { children: ReactNode }) => { } }; - // Move away from setup pages on completion / network change. - useEffectIgnoreInitial(() => { - if (!inSetup()) { - setOnNominatorSetup(false); - } - }, [inSetup(), network, poolMembership]); - // Update setup state when activeAccount changes useEffectIgnoreInitial(() => { if (accounts.length) { @@ -290,8 +274,6 @@ export const SetupProvider = ({ children }: { children: ReactNode }) => { getPoolSetupPercent, setActiveAccountSetup, setActiveAccountSetupSection, - setOnNominatorSetup, - onNominatorSetup, getNominatorSetup, getPoolSetup, }} diff --git a/src/contexts/Setup/types.ts b/src/contexts/Setup/types.ts index 594ae87231..1ab6dcaebd 100644 --- a/src/contexts/Setup/types.ts +++ b/src/contexts/Setup/types.ts @@ -53,8 +53,6 @@ export interface SetupContextInterface { p: NominatorProgress | PoolProgress ) => void; setActiveAccountSetupSection: (t: BondFor, s: number) => void; - setOnNominatorSetup: (v: boolean) => void; - onNominatorSetup: boolean; getNominatorSetup: (address: MaybeAddress) => NominatorSetup; getPoolSetup: (address: MaybeAddress) => PoolSetup; } diff --git a/src/library/SideMenu/Main.tsx b/src/library/SideMenu/Main.tsx index fc42c010bd..896ec6e485 100644 --- a/src/library/SideMenu/Main.tsx +++ b/src/library/SideMenu/Main.tsx @@ -33,7 +33,6 @@ export const Main = () => { const { activeAccount } = useActiveAccounts(); const { inSetup: inNominatorSetup, addressDifferentToStash } = useStaking(); const { - onNominatorSetup, getPoolSetupPercent, getNominatorSetupPercent, }: SetupContextInterface = useSetup(); @@ -74,7 +73,6 @@ export const Main = () => { // configure Stake action const staking = !inNominatorSetup(); const warning = !syncing && controllerDifferentToStash; - const setupPercent = getNominatorSetupPercent(activeAccount); if (staking) { pages[i].action = { @@ -89,13 +87,6 @@ export const Main = () => { status: 'warning', }; } - if (!staking && (onNominatorSetup || setupPercent > 0)) { - pages[i].action = { - type: 'text', - status: 'warning', - text: `${setupPercent}%`, - }; - } } if (uri === `${import.meta.env.BASE_URL}pools`) { @@ -128,7 +119,6 @@ export const Main = () => { getNominatorSetupPercent(activeAccount), getPoolSetupPercent(activeAccount), i18n.resolvedLanguage, - onNominatorSetup, ]); // remove pages that network does not support diff --git a/src/overlay/index.tsx b/src/overlay/index.tsx index 6d7d92919f..5c7101bd9d 100644 --- a/src/overlay/index.tsx +++ b/src/overlay/index.tsx @@ -32,6 +32,7 @@ import { ManageNominations } from '../canvas/ManageNominations'; import { PoolMembers } from 'canvas/PoolMembers'; import { JoinPool } from 'canvas/JoinPool'; import { CreatePool } from 'canvas/CreatePool'; +import { NominatorSetup } from 'canvas/NominatorSetup'; import { Overlay } from 'kits/Overlay'; export const Overlays = () => { @@ -72,6 +73,7 @@ export const Overlays = () => { PoolMembers, JoinPool, CreatePool, + NominatorSetup, }} /> ); diff --git a/src/pages/Nominate/Active/Status/NewNominator.tsx b/src/pages/Nominate/Active/Status/NewNominator.tsx index 90384c193f..1a26975256 100644 --- a/src/pages/Nominate/Active/Status/NewNominator.tsx +++ b/src/pages/Nominate/Active/Status/NewNominator.tsx @@ -3,24 +3,21 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CallToActionWrapper } from 'library/CallToAction'; -import { - faChevronCircleRight, - faChevronRight, -} from '@fortawesome/free-solid-svg-icons'; +import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; import { useTranslation } from 'react-i18next'; -import { useSetup } from 'contexts/Setup'; import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useNavigate } from 'react-router-dom'; import { useApi } from 'contexts/Api'; import type { NewNominatorProps } from '../types'; import { CallToActionLoader } from 'library/Loader/CallToAction'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useOverlay } from 'kits/Overlay/Provider'; export const NewNominator = ({ syncing }: NewNominatorProps) => { const { t } = useTranslation(); const { isReady } = useApi(); const navigate = useNavigate(); - const { setOnNominatorSetup } = useSetup(); + const { openCanvas } = useOverlay().canvas; const { activeAccount } = useActiveAccounts(); const { isReadOnlyAccount } = useImportedAccounts(); @@ -33,27 +30,41 @@ export const NewNominator = ({ syncing }: NewNominatorProps) => { {syncing ? ( ) : ( -
-
-
- + +
-
- +
+
+
+
+ +
-
-
+
+ )}
diff --git a/src/pages/Nominate/Setup/index.tsx b/src/pages/Nominate/Setup/index.tsx deleted file mode 100644 index df7384c110..0000000000 --- a/src/pages/Nominate/Setup/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import { faChevronLeft, faTimes } from '@fortawesome/free-solid-svg-icons'; -import { PageRow } from 'kits/Structure/PageRow'; -import { extractUrlValue, removeVarFromUrlHash } from '@w3ux/utils'; -import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { Element } from 'react-scroll'; -import { useSetup } from 'contexts/Setup'; -import { CardWrapper } from 'library/Card/Wrappers'; -import { Nominate } from 'library/SetupSteps/Nominate'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { Bond } from './Bond'; -import { Payee } from './Payee'; -import { Summary } from './Summary'; -import { ButtonSecondary } from 'kits/Buttons/ButtonSecondary'; -import { PageTitle } from 'kits/Structure/PageTitle'; -import { PageHeadingWrapper } from 'kits/Structure/PageHeading/Wrapper'; - -export const Setup = () => { - const { t } = useTranslation('pages'); - const navigate = useNavigate(); - const { activeAccount } = useActiveAccounts(); - const { setOnNominatorSetup, removeSetupProgress } = useSetup(); - - return ( - <> - - - - - { - if (extractUrlValue('f') === 'overview') { - navigate('/overview'); - } else { - removeVarFromUrlHash('f'); - setOnNominatorSetup(false); - } - }} - lg - /> - - - { - removeVarFromUrlHash('f'); - setOnNominatorSetup(false); - removeSetupProgress('nominator', activeAccount); - }} - lg - /> - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/src/pages/Nominate/index.tsx b/src/pages/Nominate/index.tsx index 0238d1d752..fbbd63515e 100644 --- a/src/pages/Nominate/index.tsx +++ b/src/pages/Nominate/index.tsx @@ -1,12 +1,11 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { useSetup } from 'contexts/Setup'; import { Active } from './Active'; -import { Setup } from './Setup'; import { Wrapper } from './Wrappers'; -export const Nominate = () => { - const { onNominatorSetup } = useSetup(); - return {onNominatorSetup ? : }; -}; +export const Nominate = () => ( + + + +); diff --git a/src/pages/Pools/Home/Status/NewMember.tsx b/src/pages/Pools/Home/Status/NewMember.tsx index c4a47b4dba..acc41d3fd1 100644 --- a/src/pages/Pools/Home/Status/NewMember.tsx +++ b/src/pages/Pools/Home/Status/NewMember.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CallToActionWrapper } from 'library/CallToAction'; -import { faUserGroup, faUserPlus } from '@fortawesome/free-solid-svg-icons'; +import { faUserPlus } from '@fortawesome/free-solid-svg-icons'; import { useStatusButtons } from './useStatusButtons'; import { useTranslation } from 'react-i18next'; import { useOverlay } from 'kits/Overlay/Provider'; @@ -61,7 +61,7 @@ export const NewMember = ({ syncing }: NewMemberProps) => { {poolJoinPerformanceTask.status === 'unsynced' && ( <> {t('pools.joinPool', { ns: 'pages' })} - + )} From 69d716e2e99a6f32e45407362d951352fd6a884f Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Fri, 5 Apr 2024 19:01:19 +0700 Subject: [PATCH 22/37] feat(ux): Improve Join Pool preloader (#2063) Co-authored-by: Ting A Lin --- src/canvas/JoinPool/Preloader.tsx | 73 +++++++++++++++++-------------- src/canvas/JoinPool/Wrappers.ts | 11 ++++- src/library/Loader/Wrappers.ts | 4 +- src/locale/cn/pages.json | 1 + src/locale/en/pages.json | 1 + 5 files changed, 54 insertions(+), 36 deletions(-) diff --git a/src/canvas/JoinPool/Preloader.tsx b/src/canvas/JoinPool/Preloader.tsx index adb76055f1..27824af378 100644 --- a/src/canvas/JoinPool/Preloader.tsx +++ b/src/canvas/JoinPool/Preloader.tsx @@ -1,35 +1,44 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faArrowsRotate, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; -import { ButtonPrimaryInvert } from 'kits/Buttons/ButtonPrimaryInvert'; import { useOverlay } from 'kits/Overlay/Provider'; import { useTranslation } from 'react-i18next'; -import { - JoinFormWrapper, - JoinPoolInterfaceWrapper, - PreloaderWrapper, -} from './Wrappers'; -import { PoolSync } from 'library/PoolSync'; -import { CallToActionLoader } from 'library/Loader/CallToAction'; -import { LoaderWrapper } from 'library/Loader/Wrappers'; +import { JoinPoolInterfaceWrapper } from './Wrappers'; import { PageTitleTabs } from 'kits/Structure/PageTitleTabs'; import { CanvasTitleWrapper } from 'canvas/Wrappers'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import BigNumber from 'bignumber.js'; +import type { BondedPool } from 'contexts/Pools/BondedPools/types'; +import { capitalizeFirstLetter, planckToUnit, rmCommas } from '@w3ux/utils'; +import { useNetwork } from 'contexts/Network'; +import { useApi } from 'contexts/Api'; +import { PoolSync } from 'library/PoolSync'; export const Preloader = () => { - const { t } = useTranslation(); + const { t } = useTranslation('pages'); + const { + network, + networkData: { units, unit }, + } = useNetwork(); + const { bondedPools } = useBondedPools(); + const { + poolsConfig: { counterForPoolMembers }, + } = useApi(); const { closeCanvas } = useOverlay().canvas; + let totalPoolPoints = new BigNumber(0); + bondedPools.forEach((b: BondedPool) => { + totalPoolPoints = totalPoolPoints.plus(rmCommas(b.points)); + }); + const totalPoolPointsUnit = planckToUnit(totalPoolPoints, units) + .decimalPlaces(0) + .toFormat(); + return ( <>
- {
-

{t('syncingPoolData', { ns: 'library' })}...

+

{t('pools.joinPool')}

- {t('analyzingPoolPerformance', { ns: 'library' })} - + {t('pools.joinPoolHeading', { + totalMembers: new BigNumber(counterForPoolMembers).toFormat(), + totalPoolPoints: totalPoolPointsUnit, + unit, + network: capitalizeFirstLetter(network), + })}

@@ -81,19 +94,13 @@ export const Preloader = () => { -
-
- - - -
-
-
- - - -
-
+
+

+ {t('analyzingPoolPerformance', { ns: 'library' })}... +

+

+ +

diff --git a/src/canvas/JoinPool/Wrappers.ts b/src/canvas/JoinPool/Wrappers.ts index 99a46091a3..b9cef2e3db 100644 --- a/src/canvas/JoinPool/Wrappers.ts +++ b/src/canvas/JoinPool/Wrappers.ts @@ -50,13 +50,22 @@ export const JoinPoolInterfaceWrapper = styled.div` } } } + + > .tip { + color: var(--accent-color-primary); + margin-top: 1rem; + font-family: Inter, sans-serif; + display: flex; + align-items: center; + justify-content: center; + } } `; export const PreloaderWrapper = styled.div` background-color: var(--background-floating-card); width: 100%; - height: 3.75rem; + height: 2rem; border-radius: 2rem; opacity: 0.4; `; diff --git a/src/library/Loader/Wrappers.ts b/src/library/Loader/Wrappers.ts index c601ccb3b8..983dc0d62f 100644 --- a/src/library/Loader/Wrappers.ts +++ b/src/library/Loader/Wrappers.ts @@ -17,7 +17,7 @@ export const LoaderWrapper = styled.div` animation-duration: 1.5s; animation-fill-mode: forwards; animation-iteration-count: infinite; - animation-name: shimmer; + animation-name: shimmer-loader; animation-timing-function: linear; opacity: 0.1; @@ -25,7 +25,7 @@ export const LoaderWrapper = styled.div` display: inline-block; position: relative; - @keyframes shimmer { + @keyframes shimmer-loader { 0% { background-position: 0px 0; } diff --git a/src/locale/cn/pages.json b/src/locale/cn/pages.json index 8745641b97..09dae46e41 100644 --- a/src/locale/cn/pages.json +++ b/src/locale/cn/pages.json @@ -154,6 +154,7 @@ "inPool": "提名池中", "inactivePoolNotNominating": "非活跃:提名池未提名任何验证人", "joinPool": "加入提名池", + "joinPoolHeading": " {{totalMembers}} 个提名池成员在{{network}}上抵押共{{totalPoolPoints}} 个{{unit}} ", "leave": "离开", "leavingPool": "离开提名池中", "leftThePool": "所有成员已离开", diff --git a/src/locale/en/pages.json b/src/locale/en/pages.json index eae84374ad..211f72d6ca 100644 --- a/src/locale/en/pages.json +++ b/src/locale/en/pages.json @@ -156,6 +156,7 @@ "inPool": "In Pool", "inactivePoolNotNominating": "Inactive: Pool Not Nominating", "joinPool": "Join Pool", + "joinPoolHeading": "Join {{totalMembers}} pool members staking a total of {{totalPoolPoints}} {{unit}} on {{network}}.", "leave": "Leave", "leavingPool": "Leaving Pool", "leftThePool": "All members have now left the pool", From e5027fffc3151dbdf0c4b7cce09f37aaeb184971 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Fri, 5 Apr 2024 20:55:12 +0700 Subject: [PATCH 23/37] feat: Join pool progress bar on performance fetch. (#2064) --- src/canvas/JoinPool/Preloader.tsx | 33 ++++--------------------------- src/canvas/JoinPool/Wrappers.ts | 33 +++++++++++++++++++++++++++++-- src/canvas/Wrappers.ts | 4 ++++ src/library/PoolSync/Bar.tsx | 31 +++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 31 deletions(-) create mode 100644 src/library/PoolSync/Bar.tsx diff --git a/src/canvas/JoinPool/Preloader.tsx b/src/canvas/JoinPool/Preloader.tsx index 27824af378..c643d0a6a2 100644 --- a/src/canvas/JoinPool/Preloader.tsx +++ b/src/canvas/JoinPool/Preloader.tsx @@ -6,7 +6,6 @@ import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; import { useOverlay } from 'kits/Overlay/Provider'; import { useTranslation } from 'react-i18next'; import { JoinPoolInterfaceWrapper } from './Wrappers'; -import { PageTitleTabs } from 'kits/Structure/PageTitleTabs'; import { CanvasTitleWrapper } from 'canvas/Wrappers'; import { useBondedPools } from 'contexts/Pools/BondedPools'; import BigNumber from 'bignumber.js'; @@ -14,7 +13,7 @@ import type { BondedPool } from 'contexts/Pools/BondedPools/types'; import { capitalizeFirstLetter, planckToUnit, rmCommas } from '@w3ux/utils'; import { useNetwork } from 'contexts/Network'; import { useApi } from 'contexts/Api'; -import { PoolSync } from 'library/PoolSync'; +import { PoolSyncBar } from 'library/PoolSync/Bar'; export const Preloader = () => { const { t } = useTranslation('pages'); @@ -47,7 +46,7 @@ export const Preloader = () => { style={{ marginLeft: '1.1rem' }} />
- +
@@ -66,31 +65,6 @@ export const Preloader = () => {
- { - /* Do nothing */ - }, - disabled: true, - asPreloader: true, - }, - { - title: t('nominate.nominations', { ns: 'pages' }), - active: true, - onClick: () => { - /* Do nothing */ - }, - disabled: true, - asPreloader: true, - }, - ]} - tabClassName="canvas" - inline={true} - /> @@ -98,8 +72,9 @@ export const Preloader = () => {

{t('analyzingPoolPerformance', { ns: 'library' })}...

+

- +

diff --git a/src/canvas/JoinPool/Wrappers.ts b/src/canvas/JoinPool/Wrappers.ts index b9cef2e3db..13ba45b924 100644 --- a/src/canvas/JoinPool/Wrappers.ts +++ b/src/canvas/JoinPool/Wrappers.ts @@ -53,11 +53,40 @@ export const JoinPoolInterfaceWrapper = styled.div` > .tip { color: var(--accent-color-primary); - margin-top: 1rem; + margin-bottom: 1rem; font-family: Inter, sans-serif; display: flex; align-items: center; - justify-content: center; + justify-content: flex-start; + + > .loader { + border: 1.5px solid var(--accent-color-pending); + color: var(--accent-color-primary); + width: 100%; + height: 1.5rem; + border-radius: 1rem; + position: relative; + + > div { + position: absolute; + top: 0.25rem; + left: 0.3rem; + width: calc(100% - 0.6rem); + height: calc(100% - 0.5rem); + border-radius: 1rem; + + > .progress { + background-color: var(--accent-color-primary); + position: absolute; + top: 0; + left: 0; + width: 0; + height: 100%; + border-radius: 2rem; + transition: width 1s cubic-bezier(0.1, 1, 0.1, 1); + } + } + } } } `; diff --git a/src/canvas/Wrappers.ts b/src/canvas/Wrappers.ts index 5a03b1a6ea..fcde790c22 100644 --- a/src/canvas/Wrappers.ts +++ b/src/canvas/Wrappers.ts @@ -29,6 +29,10 @@ export const CanvasTitleWrapper = styled.div` margin: 2rem 0 1.55rem 0; padding-bottom: 0.1rem; + &.padding { + padding-bottom: 0.75rem; + } + > .inner { display: flex; align-items: center; diff --git a/src/library/PoolSync/Bar.tsx b/src/library/PoolSync/Bar.tsx new file mode 100644 index 0000000000..fd11802626 --- /dev/null +++ b/src/library/PoolSync/Bar.tsx @@ -0,0 +1,31 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; + +export const PoolSyncBar = () => { + const { getPoolPerformanceTask } = usePoolPerformance(); + + // Get the pool performance task to determine if performance data is ready. + const poolJoinPerformanceTask = getPoolPerformanceTask('pool_join'); + + // Calculate syncing status. + const { startEra, currentEra, endEra } = poolJoinPerformanceTask; + const totalEras = startEra.minus(endEra); + const erasPassed = startEra.minus(currentEra); + const percentPassed = erasPassed.isEqualTo(0) + ? new BigNumber(0) + : erasPassed.dividedBy(totalEras).multipliedBy(100); + + return ( +
+
+ +
+
+ ); +}; From 5ad2ac3b8ef2b198319f8af53dfd514bfeb38e16 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Sat, 6 Apr 2024 00:41:25 +0700 Subject: [PATCH 24/37] chore: bump @w3ux/extension-assets --- package.json | 2 +- yarn.lock | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c2c65a1568..8ea71658ac 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@polkadot/util-crypto": "^12.6.2", "@polkawatch/ddp-client": "^2.0.11", "@substrate/connect": "0.7.35", - "@w3ux/extension-assets": "^0.2.3", + "@w3ux/extension-assets": "^0.2.4", "@w3ux/hooks": "^0.0.3", "@w3ux/react-connect-kit": "^0.1.2", "@w3ux/react-odometer": "^0.0.3", diff --git a/yarn.lock b/yarn.lock index 98dcad88c2..a36af7b7a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2555,6 +2555,15 @@ __metadata: languageName: node linkType: hard +"@w3ux/extension-assets@npm:^0.2.4": + version: 0.2.4 + resolution: "@w3ux/extension-assets@npm:0.2.4" + peerDependencies: + react: ^18 + checksum: 10c0/a5609003a211b8106218177596d9d67861761e50d540c0af47c884dd9e168cb1371db1b0b0c14e46e986b16ec5883246b86e11ed607eedde866db49b37ba0a88 + languageName: node + linkType: hard + "@w3ux/hooks@npm:^0.0.3": version: 0.0.3 resolution: "@w3ux/hooks@npm:0.0.3" @@ -6516,7 +6525,7 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:^7.5.0" "@typescript-eslint/parser": "npm:^7.4.0" "@vitejs/plugin-react-swc": "npm:^3.6.0" - "@w3ux/extension-assets": "npm:^0.2.3" + "@w3ux/extension-assets": "npm:^0.2.4" "@w3ux/hooks": "npm:^0.0.3" "@w3ux/react-connect-kit": "npm:^0.1.2" "@w3ux/react-odometer": "npm:^0.0.3" From bd26840542705c6e1ff3a7c02ba0f4a27bf632dd Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Sat, 6 Apr 2024 00:56:00 +0700 Subject: [PATCH 25/37] chore: simplify bar --- src/canvas/JoinPool/Wrappers.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/canvas/JoinPool/Wrappers.ts b/src/canvas/JoinPool/Wrappers.ts index 13ba45b924..1447d35731 100644 --- a/src/canvas/JoinPool/Wrappers.ts +++ b/src/canvas/JoinPool/Wrappers.ts @@ -53,26 +53,26 @@ export const JoinPoolInterfaceWrapper = styled.div` > .tip { color: var(--accent-color-primary); - margin-bottom: 1rem; + margin-bottom: 1.25rem; font-family: Inter, sans-serif; display: flex; align-items: center; justify-content: flex-start; > .loader { - border: 1.5px solid var(--accent-color-pending); + background-color: var(--background-canvas-card-secondary); color: var(--accent-color-primary); width: 100%; - height: 1.5rem; + height: 0.5rem; border-radius: 1rem; position: relative; > div { position: absolute; - top: 0.25rem; - left: 0.3rem; - width: calc(100% - 0.6rem); - height: calc(100% - 0.5rem); + top: 0; + left: 0; + width: 100%; + height: 100%; border-radius: 1rem; > .progress { From d22e7435aa06c9ce788afb3a7bd48d0a2ce3c5ee Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Sat, 6 Apr 2024 16:03:19 +0700 Subject: [PATCH 26/37] chore: disable polkagate support --- package.json | 4 ++-- src/Providers.tsx | 2 +- src/modals/Connect/index.tsx | 16 ++++++++++------ src/modals/Connect/types.ts | 1 + yarn.lock | 28 ++++++++++++++-------------- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/package.json b/package.json index 8ea71658ac..01301a442a 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "@polkadot/util-crypto": "^12.6.2", "@polkawatch/ddp-client": "^2.0.11", "@substrate/connect": "0.7.35", - "@w3ux/extension-assets": "^0.2.4", + "@w3ux/extension-assets": "0.2.6", "@w3ux/hooks": "^0.0.3", - "@w3ux/react-connect-kit": "^0.1.2", + "@w3ux/react-connect-kit": "0.1.6", "@w3ux/react-odometer": "^0.0.3", "@w3ux/react-polkicon": "^0.0.2", "@w3ux/utils": "^0.0.2", diff --git a/src/Providers.tsx b/src/Providers.tsx index b76e4e3d55..c0c775eb8e 100644 --- a/src/Providers.tsx +++ b/src/Providers.tsx @@ -59,7 +59,7 @@ export const Providers = () => { [APIProvider, { network }], VaultAccountsProvider, LedgerHardwareProvider, - ExtensionsProvider, + [ExtensionsProvider, { options: { chainSafeSnapEnabled: true } }], [ ExtensionAccountsProvider, { dappName: DappName, network, ss58, activeAccount, setActiveAccount }, diff --git a/src/modals/Connect/index.tsx b/src/modals/Connect/index.tsx index 861d558a23..57684ea48b 100644 --- a/src/modals/Connect/index.tsx +++ b/src/modals/Connect/index.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: GPL-3.0-only import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; -import extensions from '@w3ux/extension-assets'; import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Close } from 'library/Modal/Close'; @@ -26,6 +25,8 @@ import { ModalMotionThreeSection } from 'kits/Overlay/structure/ModalMotionThree import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; import { useExtensions } from '@w3ux/react-connect-kit'; import { useEffectIgnoreInitial } from '@w3ux/hooks'; +import extensions from '@w3ux/extension-assets'; +import type { ExtensionArrayListItem } from '@w3ux/extension-assets/util'; export const Connect = () => { const { t } = useTranslation('modals'); @@ -44,12 +45,15 @@ export const Connect = () => { // Whether the app is running on of mobile wallets. const inMobileWallet = inNova || inSubWallet; - // If in SubWallet Mobile, keep `subwallet-js` only. - const extensionsAsArray = Object.entries(extensions).map(([key, value]) => ({ - id: key, - ...value, - })); + // Get supported extensions. NOTE: Snaps are currently disabled. + const extensionsAsArray = Object.entries(extensions) + .filter(([key]) => !['polkagate-snap'].includes(key)) + .map(([key, value]) => ({ + id: key, + ...value, + })) as ExtensionArrayListItem[]; + // If in SubWallet Mobile, keep `subwallet-js` only. const web = inSubWallet ? extensionsAsArray.filter((a) => a.id === 'subwallet-js') : // If in Nova Wallet, fetch nova wallet metadata and replace its id with `polkadot-js`. diff --git a/src/modals/Connect/types.ts b/src/modals/Connect/types.ts index 935e3dc83f..930f35b256 100644 --- a/src/modals/Connect/types.ts +++ b/src/modals/Connect/types.ts @@ -20,6 +20,7 @@ export interface ExtensionMetaProps { url: string; text: string; }; + otherEcosystems?: string[]; } export interface ListWithInputProps { diff --git a/yarn.lock b/yarn.lock index a36af7b7a4..4c5e205638 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2546,21 +2546,21 @@ __metadata: languageName: node linkType: hard -"@w3ux/extension-assets@npm:^0.2.3": - version: 0.2.3 - resolution: "@w3ux/extension-assets@npm:0.2.3" +"@w3ux/extension-assets@npm:0.2.6": + version: 0.2.6 + resolution: "@w3ux/extension-assets@npm:0.2.6" peerDependencies: react: ^18 - checksum: 10c0/7cda51bf0653e22df71f46c1bd449df8605433e1b6d1ffc85f1b9d468d2da57377aada90783d21e4b737570098f22a10a46c94cc3e4b278d36962591db80f73e + checksum: 10c0/e789249b579c8786669cbeb13e0835bd790a092b61964f7714ec758b6d8b211287e09b934b02890991ffa660ba8bc18626364f5d7bf111e42ae640bb48258691 languageName: node linkType: hard -"@w3ux/extension-assets@npm:^0.2.4": - version: 0.2.4 - resolution: "@w3ux/extension-assets@npm:0.2.4" +"@w3ux/extension-assets@npm:^0.2.3": + version: 0.2.3 + resolution: "@w3ux/extension-assets@npm:0.2.3" peerDependencies: react: ^18 - checksum: 10c0/a5609003a211b8106218177596d9d67861761e50d540c0af47c884dd9e168cb1371db1b0b0c14e46e986b16ec5883246b86e11ed607eedde866db49b37ba0a88 + checksum: 10c0/7cda51bf0653e22df71f46c1bd449df8605433e1b6d1ffc85f1b9d468d2da57377aada90783d21e4b737570098f22a10a46c94cc3e4b278d36962591db80f73e languageName: node linkType: hard @@ -2573,9 +2573,9 @@ __metadata: languageName: node linkType: hard -"@w3ux/react-connect-kit@npm:^0.1.2": - version: 0.1.2 - resolution: "@w3ux/react-connect-kit@npm:0.1.2" +"@w3ux/react-connect-kit@npm:0.1.6": + version: 0.1.6 + resolution: "@w3ux/react-connect-kit@npm:0.1.6" dependencies: "@chainsafe/metamask-polkadot-adapter": "npm:^0.6.0" "@polkadot/util": "npm:^12.6.2" @@ -2583,7 +2583,7 @@ __metadata: "@w3ux/extension-assets": "npm:^0.2.3" "@w3ux/hooks": "npm:^0.0.3" "@w3ux/utils": "npm:^0.0.2" - checksum: 10c0/708cf3f44e7f52f3cb5862fde553252fac8906c1c6c574145fa5b3b90cb8abe98cb2abcfc5533b7bebe48db521b63f08a134fd8502a459740b83f9d586858a8f + checksum: 10c0/77046448b22062770006cae03dd25c33ff8ab69130a8fc46c3643b70d1519e41df52a683193f7a68ade1e015343eaebd5f63083ada3cd7a11611e5da8d7fef4c languageName: node linkType: hard @@ -6525,9 +6525,9 @@ __metadata: "@typescript-eslint/eslint-plugin": "npm:^7.5.0" "@typescript-eslint/parser": "npm:^7.4.0" "@vitejs/plugin-react-swc": "npm:^3.6.0" - "@w3ux/extension-assets": "npm:^0.2.4" + "@w3ux/extension-assets": "npm:0.2.6" "@w3ux/hooks": "npm:^0.0.3" - "@w3ux/react-connect-kit": "npm:^0.1.2" + "@w3ux/react-connect-kit": "npm:0.1.6" "@w3ux/react-odometer": "npm:^0.0.3" "@w3ux/react-polkicon": "npm:^0.0.2" "@w3ux/utils": "npm:^0.0.2" From f3aeef0005400fbf27ff73993a5efc2f8eb3dbb0 Mon Sep 17 00:00:00 2001 From: Ross Bulat Date: Sat, 6 Apr 2024 16:29:52 +0700 Subject: [PATCH 27/37] add saEvents --- .../Nominate/Active/Status/NewNominator.tsx | 13 ++++++++++--- src/pages/Pools/Home/Status/NewMember.tsx | 16 +++++++++++++--- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/pages/Nominate/Active/Status/NewNominator.tsx b/src/pages/Nominate/Active/Status/NewNominator.tsx index 1a26975256..6f3cd3e59c 100644 --- a/src/pages/Nominate/Active/Status/NewNominator.tsx +++ b/src/pages/Nominate/Active/Status/NewNominator.tsx @@ -12,11 +12,14 @@ import type { NewNominatorProps } from '../types'; import { CallToActionLoader } from 'library/Loader/CallToAction'; import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { useOverlay } from 'kits/Overlay/Provider'; +import { registerSaEvent } from 'Utils'; +import { useNetwork } from 'contexts/Network'; export const NewNominator = ({ syncing }: NewNominatorProps) => { const { t } = useTranslation(); const { isReady } = useApi(); const navigate = useNavigate(); + const { network } = useNetwork(); const { openCanvas } = useOverlay().canvas; const { activeAccount } = useActiveAccounts(); const { isReadOnlyAccount } = useImportedAccounts(); @@ -37,13 +40,17 @@ export const NewNominator = ({ syncing }: NewNominatorProps) => { className={`button primary standalone${nominateButtonDisabled ? ` disabled` : ``}`} >
diff --git a/src/library/ListItem/Wrappers.ts b/src/library/ListItem/Wrappers.ts index bf449628cf..f56c3f20a3 100644 --- a/src/library/ListItem/Wrappers.ts +++ b/src/library/ListItem/Wrappers.ts @@ -12,7 +12,7 @@ export const Wrapper = styled.div` &.member { --height-bottom-row: 2.75rem; } - &.pool-join { + &.pool-more { --height-bottom-row: 7.5rem; } @@ -155,6 +155,12 @@ export const Labels = styled.div` &:hover { opacity: 1; } + + &:disabled { + &:hover { + opacity: var(--opacity-disabled); + } + } > svg { margin-left: 0.3rem; } diff --git a/src/library/Pool/index.tsx b/src/library/Pool/index.tsx index facef6ae8e..27ecee3663 100644 --- a/src/library/Pool/index.tsx +++ b/src/library/Pool/index.tsx @@ -1,15 +1,6 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faCopy } from '@fortawesome/free-regular-svg-icons'; -import { faBars, faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import type { MouseEvent as ReactMouseEvent } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useMenu } from 'contexts/Menu'; -import type { NotificationText } from 'controllers/NotificationsController/types'; -import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useValidators } from 'contexts/Validators/ValidatorEntries'; import { usePoolCommission } from 'hooks/usePoolCommission'; import { FavoritePool } from 'library/ListItem/Labels/FavoritePool'; import { PoolBonded } from 'library/ListItem/Labels/PoolBonded'; @@ -17,130 +8,29 @@ import { PoolCommission } from 'library/ListItem/Labels/PoolCommission'; import { PoolIdentity } from 'library/ListItem/Labels/PoolIdentity'; import { Labels, Separator, Wrapper } from 'library/ListItem/Wrappers'; import { usePoolsTabs } from 'pages/Pools/Home/context'; -import { useOverlay } from 'kits/Overlay/Provider'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; import { JoinPool } from '../ListItem/Labels/JoinPool'; import { Members } from '../ListItem/Labels/Members'; import { PoolId } from '../ListItem/Labels/PoolId'; import type { PoolProps } from './types'; import { Rewards } from './Rewards'; -import { NotificationsController } from 'controllers/NotificationsController'; -import type { MenuItem } from 'contexts/Menu/types'; -import { useBalances } from 'contexts/Balances'; import { useSyncing } from 'hooks/useSyncing'; -import { MenuList } from 'library/Menu/List'; export const Pool = ({ pool }: PoolProps) => { - const { t } = useTranslation('library'); - const { memberCounter, addresses, id, state } = pool; - const { openMenu, open } = useMenu(); - const { validators } = useValidators(); + const { memberCounter, addresses, id } = pool; const { setActiveTab } = usePoolsTabs(); - const { openModal } = useOverlay().modal; - const { getPoolMembership } = useBalances(); - const { poolsNominations } = useBondedPools(); - const { activeAccount } = useActiveAccounts(); const { syncing } = useSyncing(['active-pools']); - const { isReadOnlyAccount } = useImportedAccounts(); const { getCurrentCommission } = usePoolCommission(); - const membership = getPoolMembership(activeAccount); const currentCommission = getCurrentCommission(id); - // get metadata from pools metabatch - const nominations = poolsNominations[pool.id]; - - // get pool targets from nominations metadata - const targets = nominations?.targets || []; - - // extract validator entries from pool targets - const targetValidators = validators.filter(({ address }) => - targets.includes(address) - ); - - // copy address notification - const notificationCopyAddress = ( - key: 'stash' | 'reward' - ): NotificationText | null => - addresses[key] == null - ? null - : { - title: t('addressCopiedToClipboard'), - subtitle: addresses[key], - }; - - // Consruct pool menu items. - const menuItems: MenuItem[] = []; - - // Add view pool nominations button to menu - menuItems.push({ - icon: , - title: `${t('viewPoolNominations')}`, - cb: () => { - openModal({ - key: 'PoolNominations', - options: { - nominator: addresses.stash, - targets: targetValidators, - }, - }); - }, - }); - - // add copy pool stash address button to menu - menuItems.push({ - icon: , - title: t('copyPoolAddress', { type: 'Stash' }), - cb: () => { - const notification = notificationCopyAddress('stash'); - if (notification) { - navigator.clipboard.writeText(addresses.stash); - NotificationsController.emit(notification); - } - }, - }); - - // add copy pool reward address button to menu - menuItems.push({ - icon: , - title: t('copyPoolAddress', { type: 'Reward' }), - cb: () => { - const notification = notificationCopyAddress('reward'); - if (notification) { - navigator.clipboard.writeText(addresses.reward); - NotificationsController.emit(notification); - } - }, - }); - - // Handler for opening menu. - const toggleMenu = (ev: ReactMouseEvent) => { - if (!open) { - openMenu(ev, ); - } - }; - - const displayJoin = - !syncing && - state === 'Open' && - !membership && - !isReadOnlyAccount(activeAccount) && - activeAccount; - return ( - +
-
- -
@@ -158,11 +48,14 @@ export const Pool = ({ pool }: PoolProps) => { - {displayJoin && ( - - - - )} + + + +
diff --git a/src/library/PoolSync/Bar.tsx b/src/library/PoolSync/Bar.tsx index fd11802626..bd9788f948 100644 --- a/src/library/PoolSync/Bar.tsx +++ b/src/library/PoolSync/Bar.tsx @@ -3,12 +3,17 @@ import BigNumber from 'bignumber.js'; import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import type { PoolRewardPointsKey } from 'contexts/Pools/PoolPerformance/types'; -export const PoolSyncBar = () => { +export const PoolSyncBar = ({ + performanceKey, +}: { + performanceKey: PoolRewardPointsKey; +}) => { const { getPoolPerformanceTask } = usePoolPerformance(); // Get the pool performance task to determine if performance data is ready. - const poolJoinPerformanceTask = getPoolPerformanceTask('pool_join'); + const poolJoinPerformanceTask = getPoolPerformanceTask(performanceKey); // Calculate syncing status. const { startEra, currentEra, endEra } = poolJoinPerformanceTask; diff --git a/src/library/PoolSync/index.tsx b/src/library/PoolSync/index.tsx index 216cee80e5..e7fd7c0b23 100644 --- a/src/library/PoolSync/index.tsx +++ b/src/library/PoolSync/index.tsx @@ -3,12 +3,19 @@ import BigNumber from 'bignumber.js'; import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import type { PoolRewardPointsKey } from 'contexts/Pools/PoolPerformance/types'; -export const PoolSync = ({ label }: { label?: string }) => { +export const PoolSync = ({ + label, + performanceKey, +}: { + label?: string; + performanceKey: PoolRewardPointsKey; +}) => { const { getPoolPerformanceTask } = usePoolPerformance(); // Get the pool performance task to determine if performance data is ready. - const poolJoinPerformanceTask = getPoolPerformanceTask('pool_join'); + const poolJoinPerformanceTask = getPoolPerformanceTask(performanceKey); if (poolJoinPerformanceTask.status !== 'syncing') { return null; diff --git a/src/locale/cn/library.json b/src/locale/cn/library.json index 65e3b9ec4b..43bdfd1293 100644 --- a/src/locale/cn/library.json +++ b/src/locale/cn/library.json @@ -23,7 +23,7 @@ "allowCompound": "允许复利", "allowWithdraw": "允许取出收益", "alreadyImported": "地址已导入", - "analyzingPoolPerformance": "分析提名池性能并找到最佳提名池", + "analyzingPoolPerformance": "分析提名池性能", "asAPoolMember": "作为提名池成员", "asThePoolDepositor": "作为提名池存款人", "atLeast": "质押金最低为", diff --git a/src/locale/en/library.json b/src/locale/en/library.json index 43023d92c6..8c844b5239 100644 --- a/src/locale/en/library.json +++ b/src/locale/en/library.json @@ -23,7 +23,7 @@ "allowCompound": "Allow Compound", "allowWithdraw": "Allow Withdraw", "alreadyImported": "Address Already Imported", - "analyzingPoolPerformance": "Analyzing pool performance and finding optimal pools.", + "analyzingPoolPerformance": "Analyzing pool performance", "asAPoolMember": "as a pool member.", "asThePoolDepositor": "as the pool depositor.", "atLeast": "Bond amount must be at least", diff --git a/src/modals/PoolNominations/Wrappers.ts b/src/modals/PoolNominations/Wrappers.ts deleted file mode 100644 index 9348154c0a..0000000000 --- a/src/modals/PoolNominations/Wrappers.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import styled from 'styled-components'; - -export const ListWrapper = styled.div` - display: flex; - flex-flow: column wrap; - align-items: center; - position: relative; - width: 100%; - - > div, - h3 { - width: 100%; - } -`; diff --git a/src/modals/PoolNominations/index.tsx b/src/modals/PoolNominations/index.tsx deleted file mode 100644 index 6e6db3e987..0000000000 --- a/src/modals/PoolNominations/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: GPL-3.0-only - -import { useTranslation } from 'react-i18next'; -import { Title } from 'library/Modal/Title'; -import { ValidatorList } from 'library/ValidatorList'; -import { useOverlay } from 'kits/Overlay/Provider'; -import { ListWrapper } from './Wrappers'; -import { ModalPadding } from 'kits/Overlay/structure/ModalPadding'; - -export const PoolNominations = () => { - const { t } = useTranslation('modals'); - const { - config: { options }, - } = useOverlay().modal; - const { nominator, targets } = options; - - return ( - <> - - <ModalPadding> - <ListWrapper> - {targets.length > 0 ? ( - <ValidatorList - format="nomination" - bondFor="pool" - validators={targets} - nominator={nominator} - showMenu={false} - displayFor="modal" - allowListFormat={false} - refetchOnListUpdate - /> - ) : ( - <h3>{t('poolIsNotNominating')}</h3> - )} - </ListWrapper> - </ModalPadding> - </> - ); -}; diff --git a/src/overlay/index.tsx b/src/overlay/index.tsx index 5c7101bd9d..56960499cd 100644 --- a/src/overlay/index.tsx +++ b/src/overlay/index.tsx @@ -19,7 +19,6 @@ import { ImportVault } from '../modals/ImportVault'; import { ManageFastUnstake } from '../modals/ManageFastUnstake'; import { ManagePool } from '../modals/ManagePool'; import { Networks } from '../modals/Networks'; -import { PoolNominations } from '../modals/PoolNominations'; import { Settings } from '../modals/Settings'; import { Unbond } from '../modals/Unbond'; import { UnlockChunks } from '../modals/UnlockChunks'; @@ -58,7 +57,6 @@ export const Overlays = () => { ManagePool, ManageFastUnstake, Networks, - PoolNominations, Settings, ValidatorMetrics, ValidatorGeo, diff --git a/src/pages/Pools/Home/Status/NewMember.tsx b/src/pages/Pools/Home/Status/NewMember.tsx index acc41d3fd1..dbc15dfc71 100644 --- a/src/pages/Pools/Home/Status/NewMember.tsx +++ b/src/pages/Pools/Home/Status/NewMember.tsx @@ -78,7 +78,7 @@ export const NewMember = ({ syncing }: NewMemberProps) => { <FontAwesomeIcon icon={faUserPlus} /> </> )} - <PoolSync /> + <PoolSync performanceKey="pool_join" /> </button> </div> </div> From c9c4f5a53c90330b875ff57c648a6eab3a2e0660 Mon Sep 17 00:00:00 2001 From: Ross Bulat <ross@parity.io> Date: Sun, 7 Apr 2024 00:29:06 +0700 Subject: [PATCH 29/37] chore: re-enable polkagate --- src/Providers.tsx | 5 ++++- src/modals/Connect/index.tsx | 10 ++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Providers.tsx b/src/Providers.tsx index c0c775eb8e..a0c4ba4150 100644 --- a/src/Providers.tsx +++ b/src/Providers.tsx @@ -59,7 +59,10 @@ export const Providers = () => { [APIProvider, { network }], VaultAccountsProvider, LedgerHardwareProvider, - [ExtensionsProvider, { options: { chainSafeSnapEnabled: true } }], + [ + ExtensionsProvider, + { options: { chainSafeSnapEnabled: true, polkagateSnapEnabled: true } }, + ], [ ExtensionAccountsProvider, { dappName: DappName, network, ss58, activeAccount, setActiveAccount }, diff --git a/src/modals/Connect/index.tsx b/src/modals/Connect/index.tsx index 57684ea48b..efac50e902 100644 --- a/src/modals/Connect/index.tsx +++ b/src/modals/Connect/index.tsx @@ -46,12 +46,10 @@ export const Connect = () => { const inMobileWallet = inNova || inSubWallet; // Get supported extensions. NOTE: Snaps are currently disabled. - const extensionsAsArray = Object.entries(extensions) - .filter(([key]) => !['polkagate-snap'].includes(key)) - .map(([key, value]) => ({ - id: key, - ...value, - })) as ExtensionArrayListItem[]; + const extensionsAsArray = Object.entries(extensions).map(([key, value]) => ({ + id: key, + ...value, + })) as ExtensionArrayListItem[]; // If in SubWallet Mobile, keep `subwallet-js` only. const web = inSubWallet From 04cc46186b4e0f71202cbd7d540dff4eaf75c76e Mon Sep 17 00:00:00 2001 From: Ross Bulat <ross@parity.io> Date: Sun, 7 Apr 2024 00:29:59 +0700 Subject: [PATCH 30/37] chore: remove note --- src/modals/Connect/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modals/Connect/index.tsx b/src/modals/Connect/index.tsx index efac50e902..e60a67e6ab 100644 --- a/src/modals/Connect/index.tsx +++ b/src/modals/Connect/index.tsx @@ -45,7 +45,7 @@ export const Connect = () => { // Whether the app is running on of mobile wallets. const inMobileWallet = inNova || inSubWallet; - // Get supported extensions. NOTE: Snaps are currently disabled. + // Get supported extensions. const extensionsAsArray = Object.entries(extensions).map(([key, value]) => ({ id: key, ...value, From a452723e704d292c03da6d95cb3a2a4a04ff7e49 Mon Sep 17 00:00:00 2001 From: Ross Bulat <ross@parity.io> Date: Sun, 7 Apr 2024 15:10:10 +0700 Subject: [PATCH 31/37] chore: bump react-connect-kit --- package.json | 2 +- yarn.lock | 126 ++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 115 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 01301a442a..7f7653f009 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@substrate/connect": "0.7.35", "@w3ux/extension-assets": "0.2.6", "@w3ux/hooks": "^0.0.3", - "@w3ux/react-connect-kit": "0.1.6", + "@w3ux/react-connect-kit": "0.1.8", "@w3ux/react-odometer": "^0.0.3", "@w3ux/react-polkicon": "^0.0.2", "@w3ux/utils": "^0.0.2", diff --git a/yarn.lock b/yarn.lock index 4c5e205638..32acedb245 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1146,7 +1146,25 @@ __metadata: languageName: node linkType: hard -"@polkadot/extension-inject@npm:^0.46.5, @polkadot/extension-inject@npm:latest": +"@polkadot/extension-inject@npm:0.46.9": + version: 0.46.9 + resolution: "@polkadot/extension-inject@npm:0.46.9" + dependencies: + "@polkadot/api": "npm:^10.12.4" + "@polkadot/rpc-provider": "npm:^10.12.4" + "@polkadot/types": "npm:^10.12.4" + "@polkadot/util": "npm:^12.6.2" + "@polkadot/util-crypto": "npm:^12.6.2" + "@polkadot/x-global": "npm:^12.6.2" + tslib: "npm:^2.6.2" + peerDependencies: + "@polkadot/api": "*" + "@polkadot/util": "*" + checksum: 10c0/6afd3f8f5b1b803004eb50ab4588035c679933533042010cf55f685d21d8f34e2d3c8644f61831098b0cbd1abe8a669b48c22a1d19d0cc06175e9ff798e9a87c + languageName: node + linkType: hard + +"@polkadot/extension-inject@npm:^0.46.5": version: 0.46.8 resolution: "@polkadot/extension-inject@npm:0.46.8" dependencies: @@ -1264,6 +1282,30 @@ __metadata: languageName: node linkType: hard +"@polkadot/rpc-provider@npm:^10.12.4": + version: 10.12.6 + resolution: "@polkadot/rpc-provider@npm:10.12.6" + dependencies: + "@polkadot/keyring": "npm:^12.6.2" + "@polkadot/types": "npm:10.12.6" + "@polkadot/types-support": "npm:10.12.6" + "@polkadot/util": "npm:^12.6.2" + "@polkadot/util-crypto": "npm:^12.6.2" + "@polkadot/x-fetch": "npm:^12.6.2" + "@polkadot/x-global": "npm:^12.6.2" + "@polkadot/x-ws": "npm:^12.6.2" + "@substrate/connect": "npm:0.8.8" + eventemitter3: "npm:^5.0.1" + mock-socket: "npm:^9.3.1" + nock: "npm:^13.5.0" + tslib: "npm:^2.6.2" + dependenciesMeta: + "@substrate/connect": + optional: true + checksum: 10c0/c265e95967ff4224d93edee72dbe06ce23782e09f036f3a1a65f017cfd95ed992828bb6ccea83d75295d36540c9f1451bbecf1d1e958cc78d0865624f3fb3f03 + languageName: node + linkType: hard + "@polkadot/types-augment@npm:10.11.2": version: 10.11.2 resolution: "@polkadot/types-augment@npm:10.11.2" @@ -1288,6 +1330,18 @@ __metadata: languageName: node linkType: hard +"@polkadot/types-augment@npm:10.12.6": + version: 10.12.6 + resolution: "@polkadot/types-augment@npm:10.12.6" + dependencies: + "@polkadot/types": "npm:10.12.6" + "@polkadot/types-codec": "npm:10.12.6" + "@polkadot/util": "npm:^12.6.2" + tslib: "npm:^2.6.2" + checksum: 10c0/be32309d68686a41ba1ddccfcbc4dab1e973c44a565fbfbfb177b217d787ac12d7aa68df595d08d7a600a30b275d17c334384aeef67dc856babba9bc74105854 + languageName: node + linkType: hard + "@polkadot/types-codec@npm:10.11.2": version: 10.11.2 resolution: "@polkadot/types-codec@npm:10.11.2" @@ -1310,6 +1364,17 @@ __metadata: languageName: node linkType: hard +"@polkadot/types-codec@npm:10.12.6": + version: 10.12.6 + resolution: "@polkadot/types-codec@npm:10.12.6" + dependencies: + "@polkadot/util": "npm:^12.6.2" + "@polkadot/x-bigint": "npm:^12.6.2" + tslib: "npm:^2.6.2" + checksum: 10c0/804d2fcc299ef461ed370de84b9848470450ba9ed4afc9cd7665f3f1cce44402f06fd432e8e7bfba893759f939bd8452d6a6ea63309c67e0311c9e9581732046 + languageName: node + linkType: hard + "@polkadot/types-create@npm:10.11.2": version: 10.11.2 resolution: "@polkadot/types-create@npm:10.11.2" @@ -1332,6 +1397,17 @@ __metadata: languageName: node linkType: hard +"@polkadot/types-create@npm:10.12.6": + version: 10.12.6 + resolution: "@polkadot/types-create@npm:10.12.6" + dependencies: + "@polkadot/types-codec": "npm:10.12.6" + "@polkadot/util": "npm:^12.6.2" + tslib: "npm:^2.6.2" + checksum: 10c0/62aaf5bf8bee78f1ed26d490f2d4a6c57338403ee1994abee7df39536d770c0603d144d6b3897a801dee6470d19d64de20c04d424dfb66d8c297a97e37258de6 + languageName: node + linkType: hard + "@polkadot/types-known@npm:10.12.4": version: 10.12.4 resolution: "@polkadot/types-known@npm:10.12.4" @@ -1366,6 +1442,16 @@ __metadata: languageName: node linkType: hard +"@polkadot/types-support@npm:10.12.6": + version: 10.12.6 + resolution: "@polkadot/types-support@npm:10.12.6" + dependencies: + "@polkadot/util": "npm:^12.6.2" + tslib: "npm:^2.6.2" + checksum: 10c0/cf050fd816572e67430ae58ecc7f5035ec9dedc9e85c0f017580a10ba31d70f5edd0ccc8c43351bd3cb2423cb30053d26a857110e6b08633713bae5d5b4ac419 + languageName: node + linkType: hard + "@polkadot/types@npm:10.11.2": version: 10.11.2 resolution: "@polkadot/types@npm:10.11.2" @@ -1398,6 +1484,22 @@ __metadata: languageName: node linkType: hard +"@polkadot/types@npm:10.12.6, @polkadot/types@npm:^10.12.4": + version: 10.12.6 + resolution: "@polkadot/types@npm:10.12.6" + dependencies: + "@polkadot/keyring": "npm:^12.6.2" + "@polkadot/types-augment": "npm:10.12.6" + "@polkadot/types-codec": "npm:10.12.6" + "@polkadot/types-create": "npm:10.12.6" + "@polkadot/util": "npm:^12.6.2" + "@polkadot/util-crypto": "npm:^12.6.2" + rxjs: "npm:^7.8.1" + tslib: "npm:^2.6.2" + checksum: 10c0/4a54b3a2f999d24e54946b4411aadfdc91d1cb37e3430b795e208f6b33325571750d9a7d45e307eb0dd92bbe1850c2b5111b5c54213e554465dae82da7143249 + languageName: node + linkType: hard + "@polkadot/util-crypto@npm:12.6.2, @polkadot/util-crypto@npm:^12.6.2": version: 12.6.2 resolution: "@polkadot/util-crypto@npm:12.6.2" @@ -1587,11 +1689,11 @@ __metadata: languageName: node linkType: hard -"@polkagate/extension-dapp@npm:^0.46.7-22": - version: 0.46.7-22 - resolution: "@polkagate/extension-dapp@npm:0.46.7-22" +"@polkagate/extension-dapp@npm:^0.46.12": + version: 0.46.12 + resolution: "@polkagate/extension-dapp@npm:0.46.12" dependencies: - "@polkadot/extension-inject": "npm:latest" + "@polkadot/extension-inject": "npm:0.46.9" "@polkadot/types": "npm:^10.11.2" "@polkadot/util": "npm:^12.6.2" "@polkadot/util-crypto": "npm:^12.6.2" @@ -1600,7 +1702,7 @@ __metadata: "@polkadot/api": "*" "@polkadot/util": "*" "@polkadot/util-crypto": "*" - checksum: 10c0/7d91fb0a3c7ab71dbf69a3b80dd68ed28fe4f3611c55038e5492e4f7ebf7a7d929b3a4e6e7e996a73ef728c21dd09ff8ef7d5696ff356b3a6c63e93a9d718a43 + checksum: 10c0/a4e41d566eb6cf4d2b5092627b03ec1b1a4a029445264a340cb3ede6b2e9e60e6cc658a58291395194700542842626b859d82b60aca1deb1166a1fce9c76c3d8 languageName: node linkType: hard @@ -2573,17 +2675,17 @@ __metadata: languageName: node linkType: hard -"@w3ux/react-connect-kit@npm:0.1.6": - version: 0.1.6 - resolution: "@w3ux/react-connect-kit@npm:0.1.6" +"@w3ux/react-connect-kit@npm:0.1.8": + version: 0.1.8 + resolution: "@w3ux/react-connect-kit@npm:0.1.8" dependencies: "@chainsafe/metamask-polkadot-adapter": "npm:^0.6.0" "@polkadot/util": "npm:^12.6.2" - "@polkagate/extension-dapp": "npm:^0.46.7-22" + "@polkagate/extension-dapp": "npm:^0.46.12" "@w3ux/extension-assets": "npm:^0.2.3" "@w3ux/hooks": "npm:^0.0.3" "@w3ux/utils": "npm:^0.0.2" - checksum: 10c0/77046448b22062770006cae03dd25c33ff8ab69130a8fc46c3643b70d1519e41df52a683193f7a68ade1e015343eaebd5f63083ada3cd7a11611e5da8d7fef4c + checksum: 10c0/aabfedb3736f4a17a2afcf4f49f22cbcb2fd01157e384ceae545fdec2d668318ea5d1e966db738a01ea35beefa54c5e094c5bfdcd807de59cdfd0085191efe93 languageName: node linkType: hard @@ -6527,7 +6629,7 @@ __metadata: "@vitejs/plugin-react-swc": "npm:^3.6.0" "@w3ux/extension-assets": "npm:0.2.6" "@w3ux/hooks": "npm:^0.0.3" - "@w3ux/react-connect-kit": "npm:0.1.6" + "@w3ux/react-connect-kit": "npm:0.1.8" "@w3ux/react-odometer": "npm:^0.0.3" "@w3ux/react-polkicon": "npm:^0.0.2" "@w3ux/utils": "npm:^0.0.2" From 1a1847deb0d4763b893335293c85dbe8d3f330b1 Mon Sep 17 00:00:00 2001 From: Ross Bulat <ross@parity.io> Date: Sun, 7 Apr 2024 19:43:14 +0700 Subject: [PATCH 32/37] feat(refactor): Persist all imported accounts active pools. (#2066) --- src/canvas/JoinPool/index.tsx | 2 +- src/contexts/Balances/index.tsx | 24 ++- src/contexts/Pools/ActivePool/index.tsx | 47 +++-- src/contexts/Pools/JoinPools/index.tsx | 4 +- src/contexts/Staking/index.tsx | 2 +- .../ActivePoolsController/index.ts | 197 ++++++++++++++---- .../ActivePoolsController/types.ts | 8 + src/hooks/useActivePools/index.tsx | 74 ++++--- src/hooks/useActivePools/types.ts | 3 +- src/library/Nominations/index.tsx | 6 +- src/modals/AccountPoolRoles/index.tsx | 2 +- src/pages/Pools/Home/ManagePool/index.tsx | 8 +- src/pages/Pools/Home/index.tsx | 2 +- 13 files changed, 263 insertions(+), 116 deletions(-) diff --git a/src/canvas/JoinPool/index.tsx b/src/canvas/JoinPool/index.tsx index 6f08abadd1..8500a588d0 100644 --- a/src/canvas/JoinPool/index.tsx +++ b/src/canvas/JoinPool/index.tsx @@ -60,7 +60,7 @@ export const JoinPool = () => { rewardPoints.every((points) => Number(points) > 0) && rewardPoints.length === MaxEraRewardPointsEras; - return pool.state === 'Open' && activeDaily; + return activeDaily; }) // Ensure the pool is currently in the active set of backers. .filter((pool) => diff --git a/src/contexts/Balances/index.tsx b/src/contexts/Balances/index.tsx index ca7c16dc7f..e5bbc3f7dc 100644 --- a/src/contexts/Balances/index.tsx +++ b/src/contexts/Balances/index.tsx @@ -14,6 +14,9 @@ import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useActiveBalances } from 'hooks/useActiveBalances'; import { useBonded } from 'contexts/Bonded'; import { SyncController } from 'controllers/SyncController'; +import { useApi } from 'contexts/Api'; +import { ActivePoolsController } from 'controllers/ActivePoolsController'; +import { useCreatePoolAccounts } from 'hooks/useCreatePoolAccounts'; export const BalancesContext = createContext<BalancesContextInterface>( defaults.defaultBalancesContext @@ -22,8 +25,10 @@ export const BalancesContext = createContext<BalancesContextInterface>( export const useBalances = () => useContext(BalancesContext); export const BalancesProvider = ({ children }: { children: ReactNode }) => { + const { api } = useApi(); const { getBondedAccount } = useBonded(); const { accounts } = useImportedAccounts(); + const createPoolAccounts = useCreatePoolAccounts(); const { activeAccount, activeProxy } = useActiveAccounts(); const controller = getBondedAccount(activeAccount); @@ -46,9 +51,24 @@ export const BalancesProvider = ({ children }: { children: ReactNode }) => { isCustomEvent(e) && BalancesController.isValidNewAccountBalanceEvent(e) ) { - // Update whether all account balances have been synced. Uses greater than to account for - // possible errors on the API side. + // Update whether all account balances have been synced. checkBalancesSynced(); + + const { address, ...newBalances } = e.detail; + const { poolMembership } = newBalances; + + // If a pool membership exists, let `ActivePools` know of pool membership to re-sync pool + // details and nominations. + if (api && poolMembership) { + const { poolId } = poolMembership; + const newPools = ActivePoolsController.getformattedPoolItems( + address + ).concat({ + id: String(poolId), + addresses: { ...createPoolAccounts(Number(poolId)) }, + }); + ActivePoolsController.syncPools(api, address, newPools); + } } }; diff --git a/src/contexts/Pools/ActivePool/index.tsx b/src/contexts/Pools/ActivePool/index.tsx index 6d360ce834..ed30bd33b0 100644 --- a/src/contexts/Pools/ActivePool/index.tsx +++ b/src/contexts/Pools/ActivePool/index.tsx @@ -68,35 +68,34 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { setStateWithRef(id, setActivePoolIdState, activePoolIdRef); }; - // Only listen to the currently selected active pool, otherwise return an empty array. - const poolIds = activePoolIdRef.current ? [activePoolIdRef.current] : []; - - // Listen for active pools. NOTE: `activePoolsRef` is needed to check if the pool has changed - // after the async call of fetching pending rewards. - const { activePools, activePoolsRef, poolNominations } = useActivePools({ - poolIds, - onCallback: async () => { - // Sync: active pools synced once all account pools have been reported. - if (accountPoolIds.length <= ActivePoolsController.pools.length) { - SyncController.dispatch('active-pools', 'complete'); - } - }, - }); + // Only listen to the active account's active pools, otherwise return an empty array. NOTE: + // `activePoolsRef` is needed to check if the pool has changed after the async call of fetching + // pending rewards. + const { getActivePools, activePoolsRef, getPoolNominations } = useActivePools( + { + who: activeAccount, + onCallback: async () => { + // Sync: active pools synced once all account pools have been reported. + if ( + accountPoolIds.length <= + ActivePoolsController.getPools(activeAccount).length + ) { + SyncController.dispatch('active-pools', 'complete'); + } + }, + } + ); // Store the currently active pool's pending rewards for the active account. const [pendingPoolRewards, setPendingPoolRewards] = useState<BigNumber>( new BigNumber(0) ); - const activePool = - activePoolId && activePools[activePoolId] - ? activePools[activePoolId] - : null; + const activePool = activePoolId ? getActivePools(activePoolId) : null; - const activePoolNominations = - activePoolId && poolNominations[activePoolId] - ? poolNominations[activePoolId] - : null; + const activePoolNominations = activePoolId + ? getPoolNominations(activePoolId) + : null; // Sync active pool subscriptions. const syncActivePoolSubscriptions = async () => { @@ -105,7 +104,9 @@ export const ActivePoolProvider = ({ children }: { children: ReactNode }) => { id: pool, addresses: { ...createPoolAccounts(Number(pool)) }, })); - ActivePoolsController.syncPools(api, newActivePools); + + SyncController.dispatch('active-pools', 'syncing'); + ActivePoolsController.syncPools(api, activeAccount, newActivePools); } else { // No active pools to sync. Mark as complete. SyncController.dispatch('active-pools', 'complete'); diff --git a/src/contexts/Pools/JoinPools/index.tsx b/src/contexts/Pools/JoinPools/index.tsx index 148e41fa35..73371e7580 100644 --- a/src/contexts/Pools/JoinPools/index.tsx +++ b/src/contexts/Pools/JoinPools/index.tsx @@ -56,8 +56,8 @@ export const JoinPoolsProvider = ({ children }: { children: ReactNode }) => { ({ state }) => state === 'Open' ); - // Filter pools that do not have at least double the minimum active stake in points. NOTE: - // assumes that points are a 1:1 ratio between balance and points. + // Filter pools that do not have at least double the minimum stake to earn rewards, in points. + // NOTE: assumes that points are a 1:1 ratio between balance and points. const rewardBondedPools = activeBondedPools.filter(({ points }) => { const pointsBn = new BigNumber(rmCommas(points)); const threshold = minimumActiveStake.multipliedBy(2); diff --git a/src/contexts/Staking/index.tsx b/src/contexts/Staking/index.tsx index 25885e6bf8..cb061c5adc 100644 --- a/src/contexts/Staking/index.tsx +++ b/src/contexts/Staking/index.tsx @@ -313,7 +313,7 @@ export const StakingProvider = ({ children }: { children: ReactNode }) => { } }, [apiStatus]); - // handle syncing with eraStakers + // handle syncing with eraStakers. useEffectIgnoreInitial(() => { if (isReady) { fetchActiveEraStakers(); diff --git a/src/controllers/ActivePoolsController/index.ts b/src/controllers/ActivePoolsController/index.ts index cb1a5d023c..edba26acdc 100644 --- a/src/controllers/ActivePoolsController/index.ts +++ b/src/controllers/ActivePoolsController/index.ts @@ -5,8 +5,14 @@ import type { VoidFn } from '@polkadot/api/types'; import { defaultPoolNominations } from 'contexts/Pools/ActivePool/defaults'; import type { ActivePool, PoolRoles } from 'contexts/Pools/ActivePool/types'; import { IdentitiesController } from 'controllers/IdentitiesController'; -import type { AnyApi } from 'types'; -import type { ActivePoolItem, DetailActivePool } from './types'; +import type { AnyApi, MaybeAddress } from 'types'; +import type { + AccountActivePools, + AccountPoolNominations, + AccountUnsubs, + ActivePoolItem, + DetailActivePool, +} from './types'; import { SyncController } from 'controllers/SyncController'; import type { Nominations } from 'contexts/Balances/types'; import type { ApiPromise } from '@polkadot/api'; @@ -16,17 +22,18 @@ export class ActivePoolsController { // Class members. // ------------------------------------------------------ - // Pool ids that are being subscribed to. - static pools: ActivePoolItem[] = []; + // Pool ids that are being subscribed to. Keyed by address. + static pools: Record<string, ActivePoolItem[]> = {}; - // Active pools that are being returned from subscriptions, keyed by pool id. - static activePools: Record<string, ActivePool | null> = {}; + // Active pools that are being returned from subscriptions, keyed by account address, then pool + // id. + static activePools: Record<string, AccountActivePools> = {}; - // Active pool nominations, keyed by pool id. - static poolNominations: Record<string, Nominations> = {}; + // Active pool nominations, keyed by account address, then pool id. + static poolNominations: Record<string, AccountPoolNominations> = {}; - // Unsubscribe objects. - static #unsubs: Record<string, VoidFn> = {}; + // Unsubscribe objects, keyed by account address, then pool id. + static #unsubs: Record<string, AccountUnsubs> = {}; // ------------------------------------------------------ // Pool membership syncing. @@ -35,23 +42,27 @@ export class ActivePoolsController { // Subscribes to pools and unsubscribes from removed pools. static syncPools = async ( api: ApiPromise, + address: MaybeAddress, newPools: ActivePoolItem[] ): Promise<void> => { - // Sync: Checking active pools. - SyncController.dispatch('active-pools', 'syncing'); + if (!address) { + return; + } // Handle pools that have been removed. - this.handleRemovedPools(newPools); + this.handleRemovedPools(address, newPools); + + const currentPools = this.getPools(address); // Determine new pools that need to be subscribed to. const poolsAdded = newPools.filter( - (newPool) => !this.pools.find((pool) => pool.id === newPool.id) + (newPool) => !currentPools.find(({ id }) => id === newPool.id) ); if (poolsAdded.length) { // Subscribe to and add new pool data. poolsAdded.forEach(async (pool) => { - this.pools.push(pool); + this.pools[address] = currentPools.concat(pool); const unsub = await api.queryMulti<AnyApi>( [ @@ -69,26 +80,31 @@ export class ActivePoolsController { // NOTE: async: fetches identity data for roles. await this.handleActivePoolCallback( api, + address, pool, bondedPool, rewardPool, accountData ); - this.handleNominatorsCallback(pool, nominators); + this.handleNominatorsCallback(address, pool, nominators); - if (this.activePools[pool.id] && this.poolNominations[pool.id]) { + if ( + this.activePools?.[address]?.[pool.id] && + this.poolNominations?.[address]?.[pool.id] + ) { document.dispatchEvent( new CustomEvent('new-active-pool', { detail: { - pool: this.activePools[pool.id], - nominations: this.poolNominations[pool.id], + address, + pool: this.activePools[address][pool.id], + nominations: this.poolNominations[address][pool.id], }, }) ); } } ); - this.#unsubs[pool.id] = unsub; + this.setUnsub(address, pool.id, unsub); }); } else { // Status: Pools Synced Completed. @@ -99,6 +115,7 @@ export class ActivePoolsController { // Handle active pool callback. static handleActivePoolCallback = async ( api: ApiPromise, + address: string, pool: ActivePoolItem, bondedPoolResult: AnyApi, rewardPoolResult: AnyApi, @@ -126,15 +143,16 @@ export class ActivePoolsController { rewardAccountBalance, }; - this.activePools[pool.id] = newPool; + this.setActivePool(address, pool.id, newPool); } else { // Invalid pools were returned. To signal pool was synced, set active pool to `null`. - this.activePools[pool.id] = null; + this.setActivePool(address, pool.id, null); } }; // Handle nominators callback. static handleNominatorsCallback = ( + address: string, pool: ActivePoolItem, nominatorsResult: AnyApi ): void => { @@ -148,28 +166,50 @@ export class ActivePoolsController { submittedIn: maybeNewNominations.submittedIn.toHuman(), }; - this.poolNominations[pool.id] = newNominations; + this.setPoolNominations(address, pool.id, newNominations); }; // Remove pools that no longer exist. - static handleRemovedPools = (newPools: ActivePoolItem[]): void => { + static handleRemovedPools = ( + address: string, + newPools: ActivePoolItem[] + ): void => { + const currentPools = this.getPools(address); + // Determine removed pools - current ones that no longer exist in `newPools`. - const poolsRemoved = this.pools.filter( + const poolsRemoved = currentPools.filter( (pool) => !newPools.find((newPool) => newPool.id === pool.id) ); // Unsubscribe from removed pool subscriptions. poolsRemoved.forEach((pool) => { - if (this.#unsubs[pool.id]) { - this.#unsubs[pool.id](); + if (this.#unsubs?.[address]?.[pool.id]) { + this.#unsubs[address][pool.id](); } - delete this.#unsubs[pool.id]; - delete this.activePools[pool.id]; - delete this.poolNominations[pool.id]; + delete this.activePools[address][pool.id]; + delete this.poolNominations[address][pool.id]; }); // Remove removed pools from class. - this.pools = this.pools.filter((pool) => !poolsRemoved.includes(pool)); + this.pools[address] = currentPools.filter( + (pool) => !poolsRemoved.includes(pool) + ); + + // Tidy up empty class state. + if (!this.pools[address].length) { + delete this.pools[address]; + } + + if (!this.activePools[address]) { + delete this.activePools[address]; + } + + if (!this.poolNominations[address]) { + delete this.poolNominations[address]; + } + if (!this.#unsubs[address]) { + delete this.#unsubs[address]; + } }; // ------------------------------------------------------ @@ -178,22 +218,51 @@ export class ActivePoolsController { // Unsubscribe from all subscriptions and reset class members. static unsubscribe = (): void => { - Object.values(this.#unsubs).forEach((unsub) => { - unsub(); + Object.values(this.#unsubs).forEach((accountUnsubs) => { + Object.values(accountUnsubs).forEach((unsub) => { + unsub(); + }); }); + this.#unsubs = {}; }; static resetState = (): void => { - this.pools = []; + this.pools = {}; this.activePools = {}; this.poolNominations = {}; }; // ------------------------------------------------------ - // Class helpers. + // Getters. // ------------------------------------------------------ + // Gets pools for a provided address. + static getPools = (address: MaybeAddress): ActivePoolItem[] => { + if (!address) { + return []; + } + return this.pools?.[address] || []; + }; + + // Gets active pools for a provided address. + static getActivePools = (address: MaybeAddress): AccountActivePools => { + if (!address) { + return {}; + } + return this.activePools?.[address] || {}; + }; + + // Gets active pool nominations for a provided address. + static getPoolNominations = ( + address: MaybeAddress + ): AccountPoolNominations => { + if (!address) { + return {}; + } + return this.poolNominations?.[address] || {}; + }; + // Gets unique role addresses from a bonded pool's `roles` record. static getUniqueRoleAddresses = (roles: PoolRoles): string[] => { const roleAddresses: string[] = [ @@ -202,9 +271,65 @@ export class ActivePoolsController { return roleAddresses; }; + // ------------------------------------------------------ + // Setters. + // ------------------------------------------------------ + + // Set an active pool for an address. + static setActivePool = ( + address: string, + poolId: string, + activePool: ActivePool | null + ): void => { + if (!this.activePools[address]) { + this.activePools[address] = {}; + } + this.activePools[address][poolId] = activePool; + }; + + // Set pool nominations for an address. + static setPoolNominations = ( + address: string, + poolId: string, + nominations: Nominations + ): void => { + if (!this.poolNominations[address]) { + this.poolNominations[address] = {}; + } + this.poolNominations[address][poolId] = nominations; + }; + + // Set unsub for an address and pool id. + static setUnsub = (address: string, poolId: string, unsub: VoidFn): void => { + if (!this.#unsubs[address]) { + this.#unsubs[address] = {}; + } + this.#unsubs[address][poolId] = unsub; + }; + + // ------------------------------------------------------ + // Class helpers. + // ------------------------------------------------------ + + // Format pools into active pool items (id and addresses only). + static getformattedPoolItems = (address: MaybeAddress): ActivePoolItem[] => { + if (!address) { + return []; + } + return ( + this.pools?.[address]?.map(({ id, addresses }) => ({ + id: id.toString(), + addresses, + })) || [] + ); + }; + // Checks if event detailis a valid `new-active-pool` event. static isValidNewActivePool = ( event: CustomEvent ): event is CustomEvent<DetailActivePool> => - event.detail && event.detail.pool && event.detail.nominations; + event.detail && + event.detail.address && + event.detail.pool && + event.detail.nominations; } diff --git a/src/controllers/ActivePoolsController/types.ts b/src/controllers/ActivePoolsController/types.ts index 4ffe7b691f..2108f15062 100644 --- a/src/controllers/ActivePoolsController/types.ts +++ b/src/controllers/ActivePoolsController/types.ts @@ -1,10 +1,12 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only +import type { VoidFn } from '@polkadot/api/types'; import type { Nominations } from 'contexts/Balances/types'; import type { ActivePool } from 'contexts/Pools/ActivePool/types'; export interface DetailActivePool { + address: string; pool: ActivePool; nominations: Nominations; } @@ -16,3 +18,9 @@ export interface ActivePoolItem { reward: string; }; } + +export type AccountActivePools = Record<string, ActivePool | null>; + +export type AccountPoolNominations = Record<string, Nominations>; + +export type AccountUnsubs = Record<string, VoidFn>; diff --git a/src/hooks/useActivePools/index.tsx b/src/hooks/useActivePools/index.tsx index 02a80374ce..4ba1508847 100644 --- a/src/hooks/useActivePools/index.tsx +++ b/src/hooks/useActivePools/index.tsx @@ -11,33 +11,32 @@ import type { ActivePoolsProps, ActivePoolsState, } from './types'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; import { useNetwork } from 'contexts/Network'; -export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { +export const useActivePools = ({ onCallback, who }: ActivePoolsProps) => { const { network } = useNetwork(); - const { activeAccount } = useActiveAccounts(); // Stores active pools. - const [activePools, setActivePools] = useState<ActivePoolsState>({}); + const [activePools, setActivePools] = useState<ActivePoolsState>( + ActivePoolsController.getActivePools(who) + ); const activePoolsRef = useRef(activePools); // Store nominations of active pools. const [poolNominations, setPoolNominations] = - useState<ActiveNominationsState>({}); + useState<ActiveNominationsState>( + ActivePoolsController.getPoolNominations(who) + ); const poolNominationsRef = useRef(poolNominations); // Handle report of new active pool data. const newActivePoolCallback = async (e: Event) => { if (isCustomEvent(e) && ActivePoolsController.isValidNewActivePool(e)) { - const { pool, nominations } = e.detail; + const { address, pool, nominations } = e.detail; const { id } = pool; - // Persist to active pools state if this pool is specified in `poolIds`. - if ( - poolIds === '*' || - (Array.isArray(poolIds) && poolIds.includes(String(id))) - ) { + // Persist to active pools state for the specified account. + if (address === who) { const newActivePools = { ...activePoolsRef.current }; newActivePools[id] = pool; setStateWithRef(newActivePools, setActivePools, activePoolsRef); @@ -58,42 +57,41 @@ export const useActivePools = ({ onCallback, poolIds }: ActivePoolsProps) => { } }; - // Bootstrap state on initial render. + // Get an active pool. + const getActivePools = (poolId: string) => activePools?.[poolId] || null; + + // Get an active pool's nominations. + const getPoolNominations = (poolId: string) => + poolNominations?.[poolId] || null; + + // Reset state on network change. useEffect(() => { - const initialActivePools = - poolIds === '*' - ? ActivePoolsController.activePools - : Object.fromEntries( - Object.entries(ActivePoolsController.activePools).filter(([key]) => - poolIds.includes(key) - ) - ); - setStateWithRef(initialActivePools || {}, setActivePools, activePoolsRef); + setStateWithRef({}, setActivePools, activePoolsRef); + setStateWithRef({}, setPoolNominations, poolNominationsRef); + }, [network]); - const initialPoolNominations = - poolIds === '*' - ? ActivePoolsController.poolNominations - : Object.fromEntries( - Object.entries(ActivePoolsController.poolNominations).filter( - ([key]) => poolIds.includes(key) - ) - ); + // Update state on account change. + useEffect(() => { setStateWithRef( - initialPoolNominations, + ActivePoolsController.getActivePools(who), + setActivePools, + activePoolsRef + ); + setStateWithRef( + ActivePoolsController.getPoolNominations(who), setPoolNominations, poolNominationsRef ); - }, [JSON.stringify(poolIds)]); - - // Reset state on active account or network change. - useEffect(() => { - setStateWithRef({}, setActivePools, activePoolsRef); - setStateWithRef({}, setPoolNominations, poolNominationsRef); - }, [network, activeAccount]); + }, [who]); // Listen for new active pool events. const documentRef = useRef<Document>(document); useEventListener('new-active-pool', newActivePoolCallback, documentRef); - return { activePools, activePoolsRef, poolNominations }; + return { + activePools, + activePoolsRef, + getActivePools, + getPoolNominations, + }; }; diff --git a/src/hooks/useActivePools/types.ts b/src/hooks/useActivePools/types.ts index 84d5d82e8d..c0eaa31af2 100644 --- a/src/hooks/useActivePools/types.ts +++ b/src/hooks/useActivePools/types.ts @@ -4,9 +4,10 @@ import type { Nominations } from 'contexts/Balances/types'; import type { ActivePool } from 'contexts/Pools/ActivePool/types'; import type { DetailActivePool } from 'controllers/ActivePoolsController/types'; +import type { MaybeAddress } from 'types'; export interface ActivePoolsProps { - poolIds: string[] | '*'; + who: MaybeAddress; onCallback?: (detail: DetailActivePool) => Promise<void>; } diff --git a/src/library/Nominations/index.tsx b/src/library/Nominations/index.tsx index 5fd5f5e2c2..07ed808a14 100644 --- a/src/library/Nominations/index.tsx +++ b/src/library/Nominations/index.tsx @@ -41,7 +41,7 @@ export const Nominations = ({ modal: { openModal }, canvas: { openCanvas }, } = useOverlay(); - const { syncing } = useSyncing(); + const { syncing } = useSyncing(['balances', 'era-stakers']); const { getNominations } = useBalances(); const { isFastUnstaking } = useUnstaking(); const { formatWithPrefs } = useValidators(); @@ -77,7 +77,7 @@ export const Nominations = ({ // Determine whether buttons are disabled. const btnsDisabled = (!isPool && inSetup()) || - syncing || + (!isPool && syncing) || isReadOnlyAccount(activeAccount) || poolDestroying || isFastUnstaking; @@ -131,7 +131,7 @@ export const Nominations = ({ )} </div> </CardHeaderWrapper> - {syncing ? ( + {!isPool && syncing ? ( <ListStatusHeader>{`${t('nominate.syncing')}...`}</ListStatusHeader> ) : !nominator ? ( <ListStatusHeader>{t('nominate.notNominating')}.</ListStatusHeader> diff --git a/src/modals/AccountPoolRoles/index.tsx b/src/modals/AccountPoolRoles/index.tsx index a9a65f77f1..a1f36b1cb8 100644 --- a/src/modals/AccountPoolRoles/index.tsx +++ b/src/modals/AccountPoolRoles/index.tsx @@ -43,7 +43,7 @@ export const AccountPoolRoles = () => { )} <h4> {t('activeRoles', { - count: activePools?.length || 0, + count: Object.keys(activePools)?.length || 0, })} </h4> <div className="items"> diff --git a/src/pages/Pools/Home/ManagePool/index.tsx b/src/pages/Pools/Home/ManagePool/index.tsx index a588e32963..03adf2dbb9 100644 --- a/src/pages/Pools/Home/ManagePool/index.tsx +++ b/src/pages/Pools/Home/ManagePool/index.tsx @@ -9,18 +9,14 @@ import { useActivePool } from 'contexts/Pools/ActivePool'; import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; import { Nominations } from 'library/Nominations'; import { useOverlay } from 'kits/Overlay/Provider'; -import { useActiveAccounts } from 'contexts/ActiveAccounts'; -import { useSyncing } from 'hooks/useSyncing'; import { useValidators } from 'contexts/Validators/ValidatorEntries'; import { ButtonHelp } from 'kits/Buttons/ButtonHelp'; import { ButtonPrimary } from 'kits/Buttons/ButtonPrimary'; export const ManagePool = () => { const { t } = useTranslation(); - const { syncing } = useSyncing(['active-pools']); const { openCanvas } = useOverlay().canvas; const { formatWithPrefs } = useValidators(); - const { activeAccount } = useActiveAccounts(); const { isOwner, isNominator, activePoolNominations, activePool } = useActivePool(); @@ -38,9 +34,7 @@ export const ManagePool = () => { return ( <PageRow> <CardWrapper> - {syncing ? ( - <Nominations bondFor="pool" nominator={activeAccount} /> - ) : canNominate && !isNominating && state !== 'Destroying' ? ( + {canNominate && !isNominating && state !== 'Destroying' ? ( <> <CardHeaderWrapper $withAction $withMargin> <h3> diff --git a/src/pages/Pools/Home/index.tsx b/src/pages/Pools/Home/index.tsx index 9ea8201108..8d3bcf104e 100644 --- a/src/pages/Pools/Home/index.tsx +++ b/src/pages/Pools/Home/index.tsx @@ -47,7 +47,7 @@ export const HomeInner = () => { const membership = getPoolMembership(activeAccount); const { activePools } = useActivePools({ - poolIds: '*', + who: activeAccount, }); // Calculate the number of _other_ pools the user has a role in. From e146bfcb15df96cd0a10fe1d268e3eab343ef1d1 Mon Sep 17 00:00:00 2001 From: Ross Bulat <ross@parity.io> Date: Sun, 7 Apr 2024 21:07:46 +0700 Subject: [PATCH 33/37] feat(ux):`JoinPool`: Inline sync for provided pool (#2067) --- .../JoinPool/Overview/PerformanceGraph.tsx | 21 +++++---- src/canvas/JoinPool/Overview/Stats.tsx | 45 ++++++++++++++----- src/canvas/JoinPool/index.tsx | 7 ++- src/canvas/JoinPool/types.ts | 1 + src/library/PoolSync/Loader.ts | 3 +- 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/src/canvas/JoinPool/Overview/PerformanceGraph.tsx b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx index 910c3f7b12..7d8c1126b2 100644 --- a/src/canvas/JoinPool/Overview/PerformanceGraph.tsx +++ b/src/canvas/JoinPool/Overview/PerformanceGraph.tsx @@ -42,6 +42,7 @@ ChartJS.register( export const PerformanceGraph = ({ bondedPool, performanceKey, + graphSyncing, }: OverviewSectionProps) => { const { t } = useTranslation(); const { mode } = useTheme(); @@ -59,15 +60,17 @@ export const PerformanceGraph = ({ const size = useSize(graphInnerRef?.current || undefined); const { width, height } = formatSize(size, 150); - // Format reward points as an array of strings. - const dataset = Object.values( - Object.fromEntries( - Object.entries(rawEraRewardPoints).map(([k, v]: AnyJson) => [ - k, - new BigNumber(v).toString(), - ]) - ) - ); + // Format reward points as an array of strings, or an empty array if syncing. + const dataset = graphSyncing + ? [] + : Object.values( + Object.fromEntries( + Object.entries(rawEraRewardPoints).map(([k, v]: AnyJson) => [ + k, + new BigNumber(v).toString(), + ]) + ) + ); // Format labels, only displaying the first and last era. const labels = Object.keys(rawEraRewardPoints).map(() => ''); diff --git a/src/canvas/JoinPool/Overview/Stats.tsx b/src/canvas/JoinPool/Overview/Stats.tsx index 198991e83a..61e9befdd9 100644 --- a/src/canvas/JoinPool/Overview/Stats.tsx +++ b/src/canvas/JoinPool/Overview/Stats.tsx @@ -11,8 +11,15 @@ import type { OverviewSectionProps } from '../types'; import { useTranslation } from 'react-i18next'; import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; import { MaxEraRewardPointsEras } from 'consts'; +import { StyledLoader } from 'library/PoolSync/Loader'; +import type { CSSProperties } from 'styled-components'; +import { PoolSync } from 'library/PoolSync'; -export const Stats = ({ bondedPool, performanceKey }: OverviewSectionProps) => { +export const Stats = ({ + bondedPool, + performanceKey, + graphSyncing, +}: OverviewSectionProps) => { const { t } = useTranslation('library'); const { networkData: { @@ -56,20 +63,36 @@ export const Stats = ({ bondedPool, performanceKey }: OverviewSectionProps) => { } }, [bondedPool.id, bondedPool.points, isReady]); + const vars = { + '--loader-color': 'var(--text-color-secondary)', + } as CSSProperties; + return ( <HeadingWrapper> <h4> - {rawEraRewardPoints.length === MaxEraRewardPointsEras && ( - <span className="active">{t('activelyNominating')}</span> - )} + {graphSyncing ? ( + <span> + {t('syncing')} + <StyledLoader style={{ ...vars, marginRight: '1.25rem' }} /> + <PoolSync performanceKey={performanceKey} /> + </span> + ) : ( + <> + {rawEraRewardPoints.length === MaxEraRewardPointsEras && ( + <span className="active">{t('activelyNominating')}</span> + )} - <span className="balance"> - <Token className="icon" /> - {!poolBalance - ? `...` - : planckToUnit(poolBalance, units).decimalPlaces(3).toFormat()}{' '} - {unit} {t('bonded')} - </span> + <span className="balance"> + <Token className="icon" /> + {!poolBalance + ? `...` + : planckToUnit(poolBalance, units) + .decimalPlaces(3) + .toFormat()}{' '} + {unit} {t('bonded')} + </span> + </> + )} </h4> </HeadingWrapper> ); diff --git a/src/canvas/JoinPool/index.tsx b/src/canvas/JoinPool/index.tsx index 8500a588d0..9d63aba25f 100644 --- a/src/canvas/JoinPool/index.tsx +++ b/src/canvas/JoinPool/index.tsx @@ -104,7 +104,8 @@ export const JoinPool = () => { return ( <CanvasFullScreenWrapper> - {poolJoinPerformanceTask.status !== 'synced' || !bondedPool ? ( + {(!providedPoolId && poolJoinPerformanceTask.status !== 'synced') || + !bondedPool ? ( <Preloader performanceKey={performanceKey} /> ) : ( <> @@ -125,6 +126,10 @@ export const JoinPool = () => { <Overview bondedPool={bondedPool} performanceKey={performanceKey} + graphSyncing={ + providedPoolId && + poolJoinPerformanceTask.status !== 'synced' + } /> )} {activeTab === 1 && ( diff --git a/src/canvas/JoinPool/types.ts b/src/canvas/JoinPool/types.ts index b44f08558b..7f890837d9 100644 --- a/src/canvas/JoinPool/types.ts +++ b/src/canvas/JoinPool/types.ts @@ -30,4 +30,5 @@ export interface AddressSectionProps { export interface OverviewSectionProps { bondedPool: BondedPool; performanceKey: PoolRewardPointsKey; + graphSyncing: boolean; } diff --git a/src/library/PoolSync/Loader.ts b/src/library/PoolSync/Loader.ts index f40091beb1..80e7bb6e31 100644 --- a/src/library/PoolSync/Loader.ts +++ b/src/library/PoolSync/Loader.ts @@ -7,7 +7,8 @@ export const StyledLoader = styled.div` height: 0.8rem; margin-left: 1.6rem; aspect-ratio: 5; - --_g: no-repeat radial-gradient(farthest-side, white 94%, #0000); + --_g: no-repeat + radial-gradient(farthest-side, var(--loader-color, white) 94%, #0000); background: var(--_g), var(--_g), var(--_g), var(--_g); background-size: 20% 100%; animation: From c5c2ecb54d31b59cc4db3bdb20b55e48cc01160a Mon Sep 17 00:00:00 2001 From: Ross Bulat <ross@parity.io> Date: Mon, 8 Apr 2024 14:37:38 +0700 Subject: [PATCH 34/37] feat(ux): Disconnect from extension (#2069) Co-authored-by: Ting A Lin <linshaoting6@gmail.com> --- src/locale/cn/modals.json | 1 + src/locale/en/modals.json | 1 + src/modals/Connect/Extension.tsx | 58 +++++++++++++++++++++++--------- src/modals/Connect/Wrappers.ts | 41 ++++++++++++++++++---- 4 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/locale/cn/modals.json b/src/locale/cn/modals.json index 7827a08bdb..e817ffd244 100644 --- a/src/locale/cn/modals.json +++ b/src/locale/cn/modals.json @@ -71,6 +71,7 @@ "developerTools": "开发者工具", "differentNetworkAddress": "不同的网络地址", "disconnect": "断开", + "disconnectFromExtension": "请重新确认断开与此扩展的连接,这将重新加载该应用", "done": "完成", "ensureLedgerIsConnected": "提示:请确保您的Ledger设备已连接", "exitYourStakingPosition": "退出抵押", diff --git a/src/locale/en/modals.json b/src/locale/en/modals.json index 791d225ce8..2c2b989839 100644 --- a/src/locale/en/modals.json +++ b/src/locale/en/modals.json @@ -74,6 +74,7 @@ "developerTools": "Developer Tools", "differentNetworkAddress": "Different Network Address", "disconnect": "Disconnect", + "disconnectFromExtension": "Are you sure you want to disconnect from this extension? This will reload the dashboard.", "done": "Done", "ensureLedgerIsConnected": "Tip: Ensure your Ledger device is connected before continuing.", "exitYourStakingPosition": "Exit your staking position.", diff --git a/src/modals/Connect/Extension.tsx b/src/modals/Connect/Extension.tsx index 5dace6bada..c9cab29022 100644 --- a/src/modals/Connect/Extension.tsx +++ b/src/modals/Connect/Extension.tsx @@ -1,7 +1,11 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { + faExternalLinkAlt, + faMinus, + faPlus, +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,6 +15,7 @@ import type { ExtensionProps } from './types'; import { NotificationsController } from 'controllers/NotificationsController'; import { ModalConnectItem } from 'kits/Overlay/structure/ModalConnectItem'; import { useExtensionAccounts, useExtensions } from '@w3ux/react-connect-kit'; +import { localStorageOrDefault } from '@w3ux/utils'; export const Extension = ({ meta, size, flag }: ExtensionProps) => { const { t } = useTranslation('modals'); @@ -24,18 +29,34 @@ export const Extension = ({ meta, size, flag }: ExtensionProps) => { // Force re-render on click. const [increment, setIncrement] = useState<number>(0); - // click to connect to extension + const connected = extensionsStatus[id] === 'connected'; + + // Click to connect to extension. const handleClick = async () => { - if (canConnect) { - const connected = await connectExtensionAccounts(id); - // force re-render to display error messages - setIncrement(increment + 1); + if (!connected) { + if (canConnect) { + const success = await connectExtensionAccounts(id); + // force re-render to display error messages + setIncrement(increment + 1); + + if (success) { + NotificationsController.emit({ + title: t('extensionConnected'), + subtitle: `${t('titleExtensionConnected', { title })}`, + }); + } + } + } else { + if (confirm(t('disconnectFromExtension'))) { + const updatedAtiveExtensions = ( + localStorageOrDefault('active_extensions', [], true) as string[] + ).filter((ext: string) => ext !== id); - if (connected) { - NotificationsController.emit({ - title: t('extensionConnected'), - subtitle: `${t('titleExtensionConnected', { title })}`, - }); + localStorage.setItem( + 'active_extensions', + JSON.stringify(updatedAtiveExtensions) + ); + location.reload(); } } }; @@ -47,15 +68,22 @@ export const Extension = ({ meta, size, flag }: ExtensionProps) => { : id; const Icon = ExtensionIcons[iconId]; - // determine message to be displayed based on extension status. + // Determine message to be displayed based on extension status. let statusJsx; switch (extensionsStatus[id]) { case 'connected': - statusJsx = <p className="success">{t('connected')}</p>; + statusJsx = ( + <p className="active"> + <FontAwesomeIcon icon={faMinus} className="plus" /> + {t('disconnect')} + </p> + ); break; + case 'not_authenticated': statusJsx = <p>{t('notAuthenticated')}</p>; break; + default: statusJsx = ( <p className="active"> @@ -67,8 +95,7 @@ export const Extension = ({ meta, size, flag }: ExtensionProps) => { const websiteText = typeof website === 'string' ? website : website.text; const websiteUrl = typeof website === 'string' ? website : website.url; - - const disabled = extensionsStatus[id] === 'connected' || !isInstalled; + const disabled = !isInstalled; return ( <ModalConnectItem canConnect={canConnect}> @@ -94,6 +121,7 @@ export const Extension = ({ meta, size, flag }: ExtensionProps) => { </div> <div className="row"> <h3>{title}</h3> + {connected && <p className="active inline">{t('connected')}</p>} </div> </div> <div className="foot"> diff --git a/src/modals/Connect/Wrappers.ts b/src/modals/Connect/Wrappers.ts index 85df9608d4..25a1e8c3d7 100644 --- a/src/modals/Connect/Wrappers.ts +++ b/src/modals/Connect/Wrappers.ts @@ -26,19 +26,43 @@ export const ExtensionInner = styled.div` h3 { font-family: InterSemiBold, sans-serif; - margin: 1rem 0 0 0; + margin: 0; + > svg { margin-right: 0.5rem; } } + p { color: var(--text-color-secondary); padding: 0; margin: 0; - .plus { + + &.success { + color: var(--status-success-color); + } + + &.danger { + color: var(--status-danger-color); + } + + &.active { + color: var(--accent-color-primary); + } + + &.inline { + color: var(--status-success-color); + border: 0.75px solid var(--status-success-color); + border-radius: 0.4rem; + margin-left: 0.75rem; + padding: 0.1rem 0.4rem; + } + + > .plus { margin-right: 0.4rem; } } + .body { width: 100%; padding: 1.35rem 0.85rem 0.75rem 0.85rem; @@ -60,24 +84,27 @@ export const ExtensionInner = styled.div` .row { width: 100%; display: flex; + align-items: center; } .foot { padding: 0.25rem 1rem 1rem 1rem; } + .status { position: absolute; top: 0.9rem; right: 0.9rem; - .success { - color: var(--status-success-color); - } - .active { - color: var(--accent-color-primary); + display: flex; + + > p { + margin-left: 1rem; } } + .icon { color: var(--text-color-primary); width: 100%; + margin-bottom: 0.75rem; svg { max-width: 2.6rem; From 5063d239a432d2b16be67759f22bf2785dd2d967 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 07:38:10 +0000 Subject: [PATCH 35/37] chore(main): release 1.3.0 --- CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..370a32d520 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +# Changelog + +## [1.3.0](https://github.com/paritytech/polkadot-staking-dashboard/compare/v1.2.1...v1.3.0) (2024-04-08) + + +### Features + +* Create pool canvas ([#2061](https://github.com/paritytech/polkadot-staking-dashboard/issues/2061)) ([de3ad50](https://github.com/paritytech/polkadot-staking-dashboard/commit/de3ad50ed2eda49a0378a26c22fb8a48fdc9e305)) +* Join pool progress bar on performance fetch. ([#2064](https://github.com/paritytech/polkadot-staking-dashboard/issues/2064)) ([e5027ff](https://github.com/paritytech/polkadot-staking-dashboard/commit/e5027fffc3151dbdf0c4b7cce09f37aaeb184971)) +* Open `JoinPool` canvas immediately, preloader, prioritise low member pools. ([#2059](https://github.com/paritytech/polkadot-staking-dashboard/issues/2059)) ([5360eaa](https://github.com/paritytech/polkadot-staking-dashboard/commit/5360eaa17ef08b6b602d21967d9174f2eed9cf83)) +* Pool performance data to batches. Per-page fetching, pool join subset. ([#2057](https://github.com/paritytech/polkadot-staking-dashboard/issues/2057)) ([965d3e1](https://github.com/paritytech/polkadot-staking-dashboard/commit/965d3e182c77e0b6d46c2d1c603e74a30cd7be92)) +* **refactor:** Persist all imported accounts active pools. ([#2066](https://github.com/paritytech/polkadot-staking-dashboard/issues/2066)) ([1a1847d](https://github.com/paritytech/polkadot-staking-dashboard/commit/1a1847deb0d4763b893335293c85dbe8d3f330b1)) +* Simple pool join & call to action UI ([#2050](https://github.com/paritytech/polkadot-staking-dashboard/issues/2050)) ([6d04429](https://github.com/paritytech/polkadot-staking-dashboard/commit/6d0442947b4322ec949bbb88e82b24720dce4143)) +* Start nominating canvas ([#2062](https://github.com/paritytech/polkadot-staking-dashboard/issues/2062)) ([0208d5f](https://github.com/paritytech/polkadot-staking-dashboard/commit/0208d5fc5658bc375eeef3aa853954c05290796f)) +* use `bondedPool.memberCounter`, deprecate `nomination_pool/pool` Subscan call ([#2054](https://github.com/paritytech/polkadot-staking-dashboard/issues/2054)) ([b536faf](https://github.com/paritytech/polkadot-staking-dashboard/commit/b536faf8fc410c8291dea84fa2b96189ab2c8e76)) +* **ux:** `JoinPool`: Inline sync for provided pool ([#2067](https://github.com/paritytech/polkadot-staking-dashboard/issues/2067)) ([e146bfc](https://github.com/paritytech/polkadot-staking-dashboard/commit/e146bfcb15df96cd0a10fe1d268e3eab343ef1d1)) +* **ux:** Disconnect from extension ([#2069](https://github.com/paritytech/polkadot-staking-dashboard/issues/2069)) ([c5c2ecb](https://github.com/paritytech/polkadot-staking-dashboard/commit/c5c2ecb54d31b59cc4db3bdb20b55e48cc01160a)) +* **ux:** Improve Join Pool preloader ([#2063](https://github.com/paritytech/polkadot-staking-dashboard/issues/2063)) ([69d716e](https://github.com/paritytech/polkadot-staking-dashboard/commit/69d716e2e99a6f32e45407362d951352fd6a884f)) +* **ux:** Pool display polishes, pre-release fixes ([#2065](https://github.com/paritytech/polkadot-staking-dashboard/issues/2065)) ([89e5f98](https://github.com/paritytech/polkadot-staking-dashboard/commit/89e5f98dd146d4838b9580a857eddfa73090762f)) +* **ux:** use secondary accent color for status UI ([#2055](https://github.com/paritytech/polkadot-staking-dashboard/issues/2055)) ([bf16d80](https://github.com/paritytech/polkadot-staking-dashboard/commit/bf16d80a661ca1d1cd0cf038bcff4525fbff19c8)) + + +### Bug Fixes + +* Search bar initial value on Pools/Validators page ([#2032](https://github.com/paritytech/polkadot-staking-dashboard/issues/2032)) ([c4749c6](https://github.com/paritytech/polkadot-staking-dashboard/commit/c4749c6e7ca338a9f3fd3299ebb53bbf45c3de07)) From 2ddf247e2dd76e37a57a840d1e7e73ad9a8b3764 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:01:31 +0700 Subject: [PATCH 36/37] chore(main): release 1.3.0 (#2051) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ross Bulat <ross@parity.io> --- docs/CHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b3873171c6..43f5ea7358 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## [1.3.0](https://github.com/paritytech/polkadot-staking-dashboard/compare/v1.2.1...v1.3.0) (2024-04-08) + + +### Features + +* Create pool canvas ([#2061](https://github.com/paritytech/polkadot-staking-dashboard/issues/2061)) ([de3ad50](https://github.com/paritytech/polkadot-staking-dashboard/commit/de3ad50ed2eda49a0378a26c22fb8a48fdc9e305)) +* Join pool progress bar on performance fetch. ([#2064](https://github.com/paritytech/polkadot-staking-dashboard/issues/2064)) ([e5027ff](https://github.com/paritytech/polkadot-staking-dashboard/commit/e5027fffc3151dbdf0c4b7cce09f37aaeb184971)) +* Open `JoinPool` canvas immediately, preloader, prioritise low member pools. ([#2059](https://github.com/paritytech/polkadot-staking-dashboard/issues/2059)) ([5360eaa](https://github.com/paritytech/polkadot-staking-dashboard/commit/5360eaa17ef08b6b602d21967d9174f2eed9cf83)) +* Pool performance data to batches. Per-page fetching, pool join subset. ([#2057](https://github.com/paritytech/polkadot-staking-dashboard/issues/2057)) ([965d3e1](https://github.com/paritytech/polkadot-staking-dashboard/commit/965d3e182c77e0b6d46c2d1c603e74a30cd7be92)) +* **refactor:** Persist all imported accounts active pools. ([#2066](https://github.com/paritytech/polkadot-staking-dashboard/issues/2066)) ([1a1847d](https://github.com/paritytech/polkadot-staking-dashboard/commit/1a1847deb0d4763b893335293c85dbe8d3f330b1)) +* Simple pool join & call to action UI ([#2050](https://github.com/paritytech/polkadot-staking-dashboard/issues/2050)) ([6d04429](https://github.com/paritytech/polkadot-staking-dashboard/commit/6d0442947b4322ec949bbb88e82b24720dce4143)) +* Start nominating canvas ([#2062](https://github.com/paritytech/polkadot-staking-dashboard/issues/2062)) ([0208d5f](https://github.com/paritytech/polkadot-staking-dashboard/commit/0208d5fc5658bc375eeef3aa853954c05290796f)) +* use `bondedPool.memberCounter`, deprecate `nomination_pool/pool` Subscan call ([#2054](https://github.com/paritytech/polkadot-staking-dashboard/issues/2054)) ([b536faf](https://github.com/paritytech/polkadot-staking-dashboard/commit/b536faf8fc410c8291dea84fa2b96189ab2c8e76)) +* **ux:** `JoinPool`: Inline sync for provided pool ([#2067](https://github.com/paritytech/polkadot-staking-dashboard/issues/2067)) ([e146bfc](https://github.com/paritytech/polkadot-staking-dashboard/commit/e146bfcb15df96cd0a10fe1d268e3eab343ef1d1)) +* **ux:** Disconnect from extension ([#2069](https://github.com/paritytech/polkadot-staking-dashboard/issues/2069)) ([c5c2ecb](https://github.com/paritytech/polkadot-staking-dashboard/commit/c5c2ecb54d31b59cc4db3bdb20b55e48cc01160a)) +* **ux:** Improve Join Pool preloader ([#2063](https://github.com/paritytech/polkadot-staking-dashboard/issues/2063)) ([69d716e](https://github.com/paritytech/polkadot-staking-dashboard/commit/69d716e2e99a6f32e45407362d951352fd6a884f)) +* **ux:** Pool display polishes, pre-release fixes ([#2065](https://github.com/paritytech/polkadot-staking-dashboard/issues/2065)) ([89e5f98](https://github.com/paritytech/polkadot-staking-dashboard/commit/89e5f98dd146d4838b9580a857eddfa73090762f)) +* **ux:** use secondary accent color for status UI ([#2055](https://github.com/paritytech/polkadot-staking-dashboard/issues/2055)) ([bf16d80](https://github.com/paritytech/polkadot-staking-dashboard/commit/bf16d80a661ca1d1cd0cf038bcff4525fbff19c8)) + + +### Bug Fixes + +* Search bar initial value on Pools/Validators page ([#2032](https://github.com/paritytech/polkadot-staking-dashboard/issues/2032)) ([c4749c6](https://github.com/paritytech/polkadot-staking-dashboard/commit/c4749c6e7ca338a9f3fd3299ebb53bbf45c3de07)) +* Fixes an issue where Polkagate Snap enablement would also enable other extensions . + ## [1.2.1](https://github.com/paritytech/polkadot-staking-dashboard/compare/v1.2.0...v1.2.1) (2024-03-14) From fb5008d0ebae166943ee6b3749f4c350758c9315 Mon Sep 17 00:00:00 2001 From: Ross Bulat <ross@parity.io> Date: Tue, 9 Apr 2024 10:36:08 +0700 Subject: [PATCH 37/37] feat(ux): Simplify pool list, fetch performance on More (#2070) --- src/contexts/Pools/PoolPerformance/types.ts | 9 +- src/library/List/defaults.ts | 2 +- .../Labels/{JoinPool.tsx => More.tsx} | 17 +++- src/library/ListItem/Labels/PoolBonded.tsx | 89 ++++++------------- .../ListItem/Labels/PoolNominateStatus.tsx | 70 +++++++++++++++ src/library/ListItem/Wrappers.ts | 59 ++++++++++-- src/library/Pool/index.tsx | 21 ++--- src/library/PoolList/index.tsx | 23 +---- 8 files changed, 183 insertions(+), 107 deletions(-) rename src/library/ListItem/Labels/{JoinPool.tsx => More.tsx} (67%) create mode 100644 src/library/ListItem/Labels/PoolNominateStatus.tsx diff --git a/src/contexts/Pools/PoolPerformance/types.ts b/src/contexts/Pools/PoolPerformance/types.ts index 62828a25f8..6f0a735f1c 100644 --- a/src/contexts/Pools/PoolPerformance/types.ts +++ b/src/contexts/Pools/PoolPerformance/types.ts @@ -24,8 +24,9 @@ export interface PoolPerformanceContextInterface { } // Fetching status for keys. -export type PoolPerformanceTasks = Partial< - Record<PoolRewardPointsKey, PoolPerformanceTaskStatus> +export type PoolPerformanceTasks = Record< + PoolRewardPointsKey, + PoolPerformanceTaskStatus >; // Performance fetching status. @@ -42,10 +43,10 @@ export interface PoolPerformanceTaskStatus { */ // Supported reward points batch keys. -export type PoolRewardPointsKey = 'pool_join' | 'pool_page'; +export type PoolRewardPointsKey = string; // Pool reward batches, keyed by batch key. -export type PoolRewardPointsMap = Partial<Record<string, PoolRewardPoints>>; +export type PoolRewardPointsMap = Record<PoolRewardPointsKey, PoolRewardPoints>; // Pool reward points are keyed by era, then by pool address. diff --git a/src/library/List/defaults.ts b/src/library/List/defaults.ts index 1cfbba438e..9941325af3 100644 --- a/src/library/List/defaults.ts +++ b/src/library/List/defaults.ts @@ -17,7 +17,7 @@ export const defaultContext: ListContextInterface = { }; // The amount of pools per page. -export const poolsPerPage = 21; +export const poolsPerPage = 30; // The amount of validators per page. export const validatorsPerPage = 30; diff --git a/src/library/ListItem/Labels/JoinPool.tsx b/src/library/ListItem/Labels/More.tsx similarity index 67% rename from src/library/ListItem/Labels/JoinPool.tsx rename to src/library/ListItem/Labels/More.tsx index b15d8d503c..b0e7682be5 100644 --- a/src/library/ListItem/Labels/JoinPool.tsx +++ b/src/library/ListItem/Labels/More.tsx @@ -5,30 +5,39 @@ import { faCaretRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useTranslation } from 'react-i18next'; import { useOverlay } from 'kits/Overlay/Provider'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import type { BondedPool } from 'contexts/Pools/BondedPools/types'; -export const JoinPool = ({ - id, +export const More = ({ + pool, setActiveTab, disabled, }: { - id: number; + pool: BondedPool; setActiveTab: (t: number) => void; disabled: boolean; }) => { const { t } = useTranslation('tips'); const { openCanvas } = useOverlay().canvas; + const { startPoolRewardPointsFetch } = usePoolPerformance(); + + const { id, addresses } = pool; + + // Define a unique pool performance data key + const performanceKey = `pool_page_standalone_${id}`; return ( <div className="label button-with-text"> <button type="button" onClick={() => { + startPoolRewardPointsFetch(performanceKey, [addresses.stash]); openCanvas({ key: 'JoinPool', options: { providedPool: { id, - performanceBatchKey: 'pool_page', + performanceBatchKey: performanceKey, }, onJoinCallback: () => setActiveTab(0), }, diff --git a/src/library/ListItem/Labels/PoolBonded.tsx b/src/library/ListItem/Labels/PoolBonded.tsx index 71a05e59de..65a20797e8 100644 --- a/src/library/ListItem/Labels/PoolBonded.tsx +++ b/src/library/ListItem/Labels/PoolBonded.tsx @@ -1,78 +1,45 @@ // Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: GPL-3.0-only -import { capitalizeFirstLetter, planckToUnit, rmCommas } from '@w3ux/utils'; +import { planckToUnit, rmCommas } from '@w3ux/utils'; import BigNumber from 'bignumber.js'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useStaking } from 'contexts/Staking'; -import { ValidatorStatusWrapper } from 'library/ListItem/Wrappers'; -import type { Pool } from 'library/Pool/types'; import { useNetwork } from 'contexts/Network'; +import type { Pool } from 'library/Pool/types'; +import { TooltipTrigger } from '../Wrappers'; +import { useTranslation } from 'react-i18next'; +import { useTooltip } from 'contexts/Tooltip'; export const PoolBonded = ({ pool }: { pool: Pool }) => { const { t } = useTranslation('library'); const { - networkData: { units, unit }, + networkData: { + units, + brand: { token }, + }, } = useNetwork(); - const { getPoolNominationStatusCode, poolsNominations } = useBondedPools(); - const { eraStakers, getNominationsStatusFromTargets } = useStaking(); - const { addresses, points } = pool; - - // get pool targets from nominations meta batch - const nominations = poolsNominations[pool.id]; - const targets = nominations?.targets || []; + const { setTooltipTextAndOpen } = useTooltip(); - // store nomination status in state - const [nominationsStatus, setNominationsStatus] = - useState<Record<string, string>>(); + const tooltipText = t('bonded'); - // update pool nomination status as nominations metadata becomes available. - // we cannot add effect dependencies here as this needs to trigger - // as soon as the component displays. (upon tab change). - const handleNominationsStatus = () => { - setNominationsStatus( - getNominationsStatusFromTargets(addresses.stash, targets) - ); - }; + const { points } = pool; + const TokenIcon = token; - // recalculate nominations status as app syncs - useEffect(() => { - if ( - targets.length && - nominationsStatus === null && - eraStakers.stakers.length - ) { - handleNominationsStatus(); - } - }); - - // metadata has changed, which means pool items may have been added. - // recalculate nominations status - useEffect(() => { - handleNominationsStatus(); - }, [pool, eraStakers.stakers.length, Object.keys(poolsNominations).length]); - - // calculate total bonded pool amount - const poolBonded = planckToUnit(new BigNumber(rmCommas(points)), units); - - // determine nominations status and display - const nominationStatus = getPoolNominationStatusCode( - nominationsStatus || null - ); + // Format total bonded pool amount. + const bonded = planckToUnit(new BigNumber(rmCommas(points)), units); return ( - <ValidatorStatusWrapper $status={nominationStatus} $noMargin> - <h5> - {nominationStatus === null || !eraStakers.stakers.length - ? `${t('syncing')}...` - : targets.length - ? capitalizeFirstLetter(t(`${nominationStatus}`) ?? '') - : t('notNominating')} - {' / '} - {t('bonded')}: {poolBonded.decimalPlaces(3).toFormat()} {unit} - </h5> - </ValidatorStatusWrapper> + <div className="label pool"> + <TooltipTrigger + className="tooltip-trigger-element" + data-tooltip-text={tooltipText} + onMouseMove={() => setTooltipTextAndOpen(tooltipText)} + /> + + <TokenIcon + style={{ maxWidth: '1.25rem', height: '1.25rem' }} + className="token" + /> + {bonded.decimalPlaces(0).toFormat()} + </div> ); }; diff --git a/src/library/ListItem/Labels/PoolNominateStatus.tsx b/src/library/ListItem/Labels/PoolNominateStatus.tsx new file mode 100644 index 0000000000..919f98e5b1 --- /dev/null +++ b/src/library/ListItem/Labels/PoolNominateStatus.tsx @@ -0,0 +1,70 @@ +// Copyright 2024 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { capitalizeFirstLetter } from '@w3ux/utils'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { useStaking } from 'contexts/Staking'; +import { PoolStatusWrapper } from 'library/ListItem/Wrappers'; +import type { Pool } from 'library/Pool/types'; + +export const PoolNominateStatus = ({ pool }: { pool: Pool }) => { + const { t } = useTranslation('library'); + const { getPoolNominationStatusCode, poolsNominations } = useBondedPools(); + const { eraStakers, getNominationsStatusFromTargets } = useStaking(); + const { addresses } = pool; + + // get pool targets from nominations meta batch + const nominations = poolsNominations[pool.id]; + const targets = nominations?.targets || []; + + // store nomination status in state + const [nominationsStatus, setNominationsStatus] = + useState<Record<string, string>>(); + + // update pool nomination status as nominations metadata becomes available. + // we cannot add effect dependencies here as this needs to trigger + // as soon as the component displays. (upon tab change). + const handleNominationsStatus = () => { + setNominationsStatus( + getNominationsStatusFromTargets(addresses.stash, targets) + ); + }; + + // recalculate nominations status as app syncs + useEffect(() => { + if ( + targets.length && + nominationsStatus === null && + eraStakers.stakers.length + ) { + handleNominationsStatus(); + } + }); + + // metadata has changed, which means pool items may have been added. + // recalculate nominations status + useEffect(() => { + handleNominationsStatus(); + }, [pool, eraStakers.stakers.length, Object.keys(poolsNominations).length]); + + // determine nominations status and display + const nominationStatus = getPoolNominationStatusCode( + nominationsStatus || null + ); + + return ( + <PoolStatusWrapper $status={nominationStatus}> + <h4> + <span> + {nominationStatus === null || !eraStakers.stakers.length + ? `${t('syncing')}...` + : targets.length + ? capitalizeFirstLetter(t(`${nominationStatus}`) ?? '') + : t('notNominating')} + </span> + </h4> + </PoolStatusWrapper> + ); +}; diff --git a/src/library/ListItem/Wrappers.ts b/src/library/ListItem/Wrappers.ts index f56c3f20a3..72b8032ed9 100644 --- a/src/library/ListItem/Wrappers.ts +++ b/src/library/ListItem/Wrappers.ts @@ -13,7 +13,7 @@ export const Wrapper = styled.div` --height-bottom-row: 2.75rem; } &.pool-more { - --height-bottom-row: 7.5rem; + --height-bottom-row: 5.75rem; } --height-total: calc(var(--height-top-row) + var(--height-bottom-row)); @@ -63,9 +63,14 @@ export const Wrapper = styled.div` &.bottom { height: var(--height-bottom-row); + &.pools { + align-items: flex-start; + } + &.lg { display: flex; align-items: center; + > div { &:first-child { flex-grow: 1; @@ -94,6 +99,10 @@ export const Labels = styled.div` padding: 0 0 0 0.25rem; height: inherit; + &.yMargin { + margin-bottom: 0.9rem; + } + button { background: var(--shimmer-foreground); padding: 0 0.1rem; @@ -129,16 +138,13 @@ export const Labels = styled.div` align-items: center; justify-content: center; font-size: inherit; + margin: 0 0.4em; - @media (min-width: ${SmallFontSizeMaxWidth}px) { - margin: 0 0.35rem; - &.pool { - margin: 0 0.45rem; - } + > .token { + margin-right: 0.25rem; } - &.button-with-text { - margin-right: 0; + margin: 0.25rem 0 0 0; button { color: var(--accent-color-secondary); @@ -253,6 +259,43 @@ export const ValidatorStatusWrapper = styled.div<{ } `; +export const PoolStatusWrapper = styled.div<{ + $status: string; +}>` + h4, + h5 { + display: flex; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + h4 { + color: var(--text-color-tertiary); + font-size: 1rem; + + padding-top: ${(props) => + props.$status === 'active' ? '0.15rem' : '0.25rem'}; + + > span { + color: ${(props) => + props.$status === 'active' + ? 'var(--status-success-color)' + : 'var(--text-color-tertiary)'}; + + border: 0.75px solid + ${(props) => + props.$status === 'active' + ? 'var(--status-success-color)' + : 'transparent'}; + + padding: ${(props) => (props.$status === 'active' ? '0 0.5rem' : '0')}; + border-radius: 0.3rem; + opacity: ${(props) => (props.$status === 'active' ? 1 : 0.6)}; + } + } +`; + export const SelectWrapper = styled.button` background: var(--background-input); margin: 0 0.75rem 0 0.25rem; diff --git a/src/library/Pool/index.tsx b/src/library/Pool/index.tsx index 27ecee3663..5efc8c7f38 100644 --- a/src/library/Pool/index.tsx +++ b/src/library/Pool/index.tsx @@ -3,17 +3,17 @@ import { usePoolCommission } from 'hooks/usePoolCommission'; import { FavoritePool } from 'library/ListItem/Labels/FavoritePool'; -import { PoolBonded } from 'library/ListItem/Labels/PoolBonded'; +import { PoolNominateStatus } from 'library/ListItem/Labels/PoolNominateStatus'; import { PoolCommission } from 'library/ListItem/Labels/PoolCommission'; import { PoolIdentity } from 'library/ListItem/Labels/PoolIdentity'; import { Labels, Separator, Wrapper } from 'library/ListItem/Wrappers'; import { usePoolsTabs } from 'pages/Pools/Home/context'; -import { JoinPool } from '../ListItem/Labels/JoinPool'; +import { More } from '../ListItem/Labels/More'; import { Members } from '../ListItem/Labels/Members'; import { PoolId } from '../ListItem/Labels/PoolId'; import type { PoolProps } from './types'; -import { Rewards } from './Rewards'; import { useSyncing } from 'hooks/useSyncing'; +import { PoolBonded } from 'library/ListItem/Labels/PoolBonded'; export const Pool = ({ pool }: PoolProps) => { const { memberCounter, addresses, id } = pool; @@ -35,23 +35,24 @@ export const Pool = ({ pool }: PoolProps) => { </div> </div> <Separator /> - <div className="row bottom lg"> + <div className="row bottom lg pools"> <div> - <Rewards address={addresses.stash} displayFor="default" /> + <Labels className="yMargin"> </Labels> + <PoolNominateStatus pool={pool} /> </div> <div> - <Labels style={{ marginBottom: '0.9rem' }}> + <Labels className="yMargin"> {currentCommission > 0 && ( <PoolCommission commission={`${currentCommission}%`} /> )} <PoolId id={id} /> <Members members={memberCounter} /> + <PoolBonded pool={pool} /> </Labels> - <PoolBonded pool={pool} /> - <Labels style={{ marginTop: '1rem' }}> - <JoinPool - id={id} + <Labels> + <More + pool={pool} setActiveTab={setActiveTab} disabled={syncing} /> diff --git a/src/library/PoolList/index.tsx b/src/library/PoolList/index.tsx index a348e6f6b7..ebff45c5a4 100644 --- a/src/library/PoolList/index.tsx +++ b/src/library/PoolList/index.tsx @@ -28,10 +28,8 @@ import { usePoolList } from './context'; import type { PoolListProps } from './types'; import type { BondedPool } from 'contexts/Pools/BondedPools/types'; import { useSyncing } from 'hooks/useSyncing'; -import { useValidators } from 'contexts/Validators/ValidatorEntries'; import { useApi } from 'contexts/Api'; import { useEffectIgnoreInitial } from '@w3ux/hooks'; -import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; export const PoolList = ({ allowMoreCols, @@ -49,11 +47,9 @@ export const PoolList = ({ const { activeEra } = useApi(); const { syncing } = useSyncing(); const { applyFilter } = usePoolFilters(); - const { erasRewardPointsFetched } = useValidators(); const { listFormat, setListFormat } = usePoolList(); - const { startPoolRewardPointsFetch } = usePoolPerformance(); + const { poolSearchFilter, poolsNominations } = useBondedPools(); const { getFilters, getSearchTerm, setSearchTerm } = useFilters(); - const { poolSearchFilter, poolsNominations, bondedPools } = useBondedPools(); const includes = getFilters('include', 'pools'); const excludes = getFilters('exclude', 'pools'); @@ -81,12 +77,12 @@ export const PoolList = ({ // Whether this the initial render. const [synced, setSynced] = useState<boolean>(false); - // pagination + // Handle Pagination. const totalPages = Math.ceil(listPools.length / poolsPerPage); const pageEnd = page * poolsPerPage - 1; const pageStart = pageEnd - (poolsPerPage - 1); - // get paged subset of list items. + // Get paged subset of list items. const poolsToDisplay = listPools.slice(pageStart).slice(0, poolsPerPage); // Handle resetting of pool list when provided pools change. @@ -96,7 +92,7 @@ export const PoolList = ({ setSynced(true); }; - // handle filter / order update + // Handle filter / order update const handlePoolsFilterUpdate = () => { const filteredPools = filterPoolList(); setListPools(filteredPools); @@ -119,17 +115,6 @@ export const PoolList = ({ setSearchTerm('pools', newValue); }; - // Fetch pool performance data when list items or page changes. Requires `erasRewardPoints` and - // `bondedPools` to be fetched. - useEffect(() => { - if (erasRewardPointsFetched && bondedPools.length) { - startPoolRewardPointsFetch( - 'pool_page', - poolsToDisplay.map(({ addresses }) => addresses.stash) - ); - } - }, [JSON.stringify(listPools), page, erasRewardPointsFetched, bondedPools]); - // Refetch list when pool list changes. useEffect(() => { if (JSON.stringify(pools) !== JSON.stringify(poolsDefault) && synced) {