From c071345ee25d0863d493ec640991529f428659fa Mon Sep 17 00:00:00 2001 From: Raid Ateir Date: Tue, 13 Feb 2024 17:16:20 +0100 Subject: [PATCH 1/6] prepare merge --- .env | 17 + .eslintignore | 18 + .eslintrc.json | 101 + .github/dependabot.yml | 12 + .github/workflows/auto-merge.yml | 17 + .github/workflows/ci.yml | 46 + .github/workflows/gh-publish.yml | 23 + .github/workflows/release-please.yml | 21 + .github/workflows/stale.yml | 17 + .gitignore | 25 + .licenserc.json | 7 + .prettierignore | 10 + .prettierrc.json | 7 + .scripts/localeOrderKeys.cjs | 42 + .scripts/localeValidate.cjs | 78 + .scripts/utils.cjs | 96 + .shell/build-container.sh | 6 + CONTRIBUTING.md | 90 + Dockerfile | 13 + LICENSE | 674 +++ README.md | 54 + index.html | 84 + package.json | 100 + .../kusama/android-chrome-192x192.png | Bin 0 -> 5063 bytes .../kusama/android-chrome-512x512.png | Bin 0 -> 14430 bytes public/favicons/kusama/apple-touch-icon.png | Bin 0 -> 2129 bytes public/favicons/kusama/browserconfig.xml | 9 + public/favicons/kusama/favicon-16x16.png | Bin 0 -> 524 bytes public/favicons/kusama/favicon-32x32.png | Bin 0 -> 659 bytes public/favicons/kusama/favicon.ico | Bin 0 -> 15086 bytes public/favicons/kusama/mstile-150x150.png | Bin 0 -> 1952 bytes public/favicons/kusama/safari-pinned-tab.svg | 32 + public/favicons/kusama/site.webmanifest | 19 + .../polkadot/android-chrome-192x192.png | Bin 0 -> 7736 bytes .../polkadot/android-chrome-512x512.png | Bin 0 -> 24341 bytes public/favicons/polkadot/apple-touch-icon.png | Bin 0 -> 7238 bytes public/favicons/polkadot/browserconfig.xml | 9 + public/favicons/polkadot/favicon-16x16.png | Bin 0 -> 680 bytes public/favicons/polkadot/favicon-32x32.png | Bin 0 -> 1166 bytes public/favicons/polkadot/favicon.ico | Bin 0 -> 15086 bytes public/favicons/polkadot/mstile-150x150.png | Bin 0 -> 3840 bytes .../favicons/polkadot/safari-pinned-tab.svg | 67 + public/favicons/polkadot/site.webmanifest | 19 + .../westend/android-chrome-192x192.png | Bin 0 -> 20669 bytes .../westend/android-chrome-512x512.png | Bin 0 -> 58904 bytes public/favicons/westend/apple-touch-icon.png | Bin 0 -> 13627 bytes public/favicons/westend/browserconfig.xml | 9 + public/favicons/westend/favicon-16x16.png | Bin 0 -> 1487 bytes public/favicons/westend/favicon-32x32.png | Bin 0 -> 2494 bytes public/favicons/westend/favicon.ico | Bin 0 -> 15086 bytes public/favicons/westend/mstile-150x150.png | Bin 0 -> 2358 bytes public/favicons/westend/safari-pinned-tab.svg | 235 + public/favicons/westend/site.webmanifest | 19 + public/fonts/Inter.ttf | Bin 0 -> 805360 bytes public/fonts/Unbounded.ttf | Bin 0 -> 601956 bytes public/img/og-image.png | Bin 0 -> 293272 bytes public/lottie/analytics-dark.lottie | Bin 0 -> 2200 bytes public/lottie/analytics-light.lottie | Bin 0 -> 2185 bytes public/lottie/globe-dark.lottie | Bin 0 -> 4051 bytes public/lottie/globe-light.lottie | Bin 0 -> 4039 bytes public/lottie/groups-dark.lottie | Bin 0 -> 5161 bytes public/lottie/groups-light.lottie | Bin 0 -> 5147 bytes public/lottie/label-dark.lottie | Bin 0 -> 2651 bytes public/lottie/label-light.lottie | Bin 0 -> 2656 bytes public/lottie/player.js | 2 + public/lottie/refresh-dark.lottie | Bin 0 -> 2107 bytes public/lottie/refresh-light.lottie | Bin 0 -> 2098 bytes public/lottie/trending-dark.lottie | Bin 0 -> 2460 bytes public/lottie/trending-light.lottie | Bin 0 -> 2432 bytes public/lottie/view-dark.lottie | Bin 0 -> 2948 bytes public/lottie/view-light.lottie | Bin 0 -> 2937 bytes public/robots.txt | 3 + src/App.tsx | 32 + src/Providers.tsx | 108 + src/Router.tsx | 161 + src/Themes.tsx | 23 + .../Prompts/FavoritesPrompt.tsx | 104 + .../Prompts/RevertPrompt.tsx | 25 + src/canvas/ManageNominations/Wrappers.tsx | 27 + src/canvas/ManageNominations/index.tsx | 208 + src/canvas/ManageNominations/types.ts | 13 + src/config/help.ts | 163 + src/config/ledger.ts | 19 + src/config/networks.ts | 224 + src/config/pages.ts | 77 + src/config/proxies.ts | 60 + src/config/tips.ts | 53 + src/config/tokens/svg/DOT.svg | 1 + src/config/tokens/svg/KSM.svg | 4 + src/config/tokens/svg/WND.svg | 1 + src/config/validators/Amforc.tsx | 52 + src/config/validators/ApertureMining.tsx | 97 + src/config/validators/Blockseeker.tsx | 30 + src/config/validators/Blockshard.tsx | 17 + src/config/validators/CoinbaseCloud.tsx | 18 + src/config/validators/Crifferent.tsx | 206 + src/config/validators/Decentradot.tsx | 11 + src/config/validators/Dionysus.tsx | 43 + src/config/validators/Dozenodes.tsx | 8 + src/config/validators/DragonStake.tsx | 14 + src/config/validators/Gatotech.tsx | 83 + src/config/validators/Gdot.tsx | 20 + src/config/validators/GenericChain.tsx | 46 + src/config/validators/GoOpen.tsx | 17 + src/config/validators/Helikon.tsx | 15 + src/config/validators/Highstake.tsx | 143 + src/config/validators/Metaspan.tsx | 28 + src/config/validators/PDP.tsx | 16 + src/config/validators/Paranodes.tsx | 18 + src/config/validators/PioneerStake.tsx | 67 + src/config/validators/Polkachu.tsx | 18 + src/config/validators/Polkadotters.tsx | 59 + .../PythagorasCapitalInvestment.tsx | 34 + src/config/validators/SekoyaLabs.tsx | 30 + src/config/validators/StakeWorld.tsx | 238 + src/config/validators/Stakely.tsx | 20 + src/config/validators/Stakenode.tsx | 29 + src/config/validators/Stakepile.tsx | 112 + src/config/validators/Stakeplus.tsx | 41 + src/config/validators/StakerSpace.tsx | 15 + src/config/validators/Staking4All.tsx | 28 + src/config/validators/StakingFacilities.tsx | 46 + src/config/validators/TurboFlakes.tsx | 29 + src/config/validators/VFValidierung.tsx | 40 + src/config/validators/Wojdot.tsx | 59 + src/config/validators/bLdNodes.tsx | 21 + src/consts.ts | 92 + src/contexts/ActiveAccounts/defaults.ts | 14 + src/contexts/ActiveAccounts/index.tsx | 79 + src/contexts/ActiveAccounts/types.ts | 21 + src/contexts/Api/defaults.ts | 33 + src/contexts/Api/index.tsx | 308 + src/contexts/Api/types.ts | 53 + src/contexts/Balances/Utils.ts | 20 + src/contexts/Balances/defaults.ts | 29 + src/contexts/Balances/index.tsx | 232 + src/contexts/Balances/types.ts | 50 + src/contexts/Bonded/defaults.ts | 21 + src/contexts/Bonded/index.tsx | 171 + src/contexts/Bonded/types.ts | 25 + .../Connect/ImportedAccounts/defaults.ts | 14 + .../Connect/ImportedAccounts/index.tsx | 71 + .../Connect/ImportedAccounts/types.ts | 16 + .../Connect/OtherAccounts/defaults.ts | 16 + src/contexts/Connect/OtherAccounts/index.tsx | 277 + src/contexts/Connect/OtherAccounts/types.ts | 16 + src/contexts/Connect/Utils.ts | 102 + src/contexts/Extrinsics/defaults.ts | 11 + src/contexts/Extrinsics/index.tsx | 49 + src/contexts/Extrinsics/types.ts | 8 + src/contexts/FastUnstake/defaults.ts | 19 + src/contexts/FastUnstake/index.tsx | 335 ++ src/contexts/FastUnstake/types.ts | 23 + src/contexts/Filters/defaults.ts | 20 + src/contexts/Filters/index.tsx | 221 + src/contexts/Filters/types.ts | 35 + src/contexts/Hardware/Ledger.tsx | 556 ++ src/contexts/Hardware/Utils.tsx | 59 + src/contexts/Hardware/Vault.tsx | 154 + src/contexts/Hardware/defaults.ts | 51 + src/contexts/Hardware/types.ts | 87 + src/contexts/Help/defaults.ts | 14 + src/contexts/Help/index.tsx | 81 + src/contexts/Help/types.ts | 49 + src/contexts/Identities/defaults.ts | 10 + src/contexts/Identities/index.tsx | 198 + src/contexts/Identities/types.ts | 9 + src/contexts/Menu/defaults.ts | 17 + src/contexts/Menu/index.tsx | 92 + src/contexts/Menu/types.ts | 17 + src/contexts/Migrate/index.tsx | 81 + src/contexts/Network/defaults.ts | 10 + src/contexts/Network/index.tsx | 79 + src/contexts/Network/types.ts | 10 + src/contexts/NetworkMetrics/defaults.ts | 26 + src/contexts/NetworkMetrics/index.tsx | 156 + src/contexts/NetworkMetrics/types.ts | 22 + src/contexts/Notifications/defaults.ts | 11 + src/contexts/Notifications/index.tsx | 81 + src/contexts/Notifications/types.ts | 22 + src/contexts/Payouts/Utils.ts | 98 + src/contexts/Payouts/defaults.ts | 13 + src/contexts/Payouts/index.tsx | 348 ++ src/contexts/Payouts/types.ts | 21 + src/contexts/Plugins/Polkawatch/defaults.tsx | 8 + src/contexts/Plugins/Polkawatch/index.tsx | 71 + src/contexts/Plugins/Polkawatch/types.tsx | 13 + src/contexts/Plugins/Subscan/defaults.ts | 17 + src/contexts/Plugins/Subscan/index.tsx | 371 ++ src/contexts/Plugins/Subscan/types.ts | 16 + src/contexts/Plugins/defaults.ts | 11 + src/contexts/Plugins/index.tsx | 72 + src/contexts/Plugins/types.ts | 10 + src/contexts/Pools/ActivePools/defaults.ts | 69 + src/contexts/Pools/ActivePools/index.tsx | 562 ++ src/contexts/Pools/BondedPools/defaults.ts | 22 + src/contexts/Pools/BondedPools/index.tsx | 481 ++ src/contexts/Pools/PoolMembers/defaults.ts | 20 + src/contexts/Pools/PoolMembers/index.tsx | 338 ++ .../Pools/PoolMemberships/defaults.ts | 12 + src/contexts/Pools/PoolMemberships/index.tsx | 194 + .../Pools/PoolPerformance/defaults.ts | 10 + src/contexts/Pools/PoolPerformance/index.tsx | 145 + src/contexts/Pools/PoolPerformance/types.ts | 10 + src/contexts/Pools/PoolsConfig/defaults.ts | 35 + src/contexts/Pools/PoolsConfig/index.tsx | 218 + src/contexts/Pools/types.ts | 175 + src/contexts/Prompt/defaults.tsx | 15 + src/contexts/Prompt/index.tsx | 67 + src/contexts/Prompt/types.ts | 15 + src/contexts/Proxies/defaults.ts | 14 + src/contexts/Proxies/index.tsx | 301 + src/contexts/Proxies/type.ts | 51 + src/contexts/Setup/defaults.ts | 41 + src/contexts/Setup/index.tsx | 252 + src/contexts/Setup/types.ts | 61 + src/contexts/Staking/Utils.ts | 84 + src/contexts/Staking/defaults.ts | 62 + src/contexts/Staking/index.tsx | 410 ++ src/contexts/Staking/types.ts | 101 + src/contexts/Themes/defaults.ts | 10 + src/contexts/Themes/index.tsx | 66 + src/contexts/Themes/types.ts | 9 + src/contexts/Tooltip/defaults.ts | 17 + src/contexts/Tooltip/index.tsx | 69 + src/contexts/Tooltip/types.ts | 14 + src/contexts/TransferOptions/defaults.ts | 33 + src/contexts/TransferOptions/index.tsx | 217 + src/contexts/TransferOptions/types.ts | 32 + src/contexts/TxMeta/defaults.ts | 24 + src/contexts/TxMeta/index.tsx | 147 + src/contexts/TxMeta/types.ts | 26 + src/contexts/UI/defaults.ts | 19 + src/contexts/UI/index.tsx | 170 + src/contexts/UI/types.ts | 16 + .../Validators/FavoriteValidators/defaults.ts | 20 + .../Validators/FavoriteValidators/index.tsx | 93 + src/contexts/Validators/Utils.ts | 139 + .../Validators/ValidatorEntries/defaults.ts | 35 + .../Validators/ValidatorEntries/index.tsx | 603 ++ src/contexts/Validators/types.ts | 91 + src/img/appIcons/kusama.svg | 1 + src/img/appIcons/polkadot.svg | 1 + src/img/brave-logo.svg | 1 + src/img/cog-outline.svg | 1 + src/img/cross.svg | 1 + src/img/forum.svg | 1 + src/img/info-outline.svg | 1 + src/img/info.svg | 1 + src/img/kusama_icon.svg | 1 + src/img/kusama_inline.svg | 1 + src/img/kusama_logo.svg | 1 + src/img/language.svg | 1 + src/img/ledgerLogo.svg | 3 + src/img/logo-github.svg | 1 + src/img/moon-outline.svg | 1 + src/img/polkadot_icon.svg | 1 + src/img/polkadot_inline.svg | 10 + src/img/polkadot_logo.svg | 1 + src/img/sunny-outline.svg | 1 + src/img/wallet.svg | 1 + src/img/walletConnect.svg | 1 + src/img/westend_icon.svg | 1 + src/img/westend_inline.svg | 1 + src/img/westend_logo.svg | 1 + src/library/Account/Default.tsx | 79 + src/library/Account/Pool.tsx | 87 + src/library/Account/Wrapper.ts | 64 + src/library/Account/types.ts | 19 + src/library/AccountInput/Wrapper.ts | 93 + src/library/AccountInput/index.tsx | 216 + src/library/AccountInput/types.ts | 17 + src/library/BarChart/BarSegment.tsx | 26 + src/library/BarChart/BondedChart.tsx | 103 + src/library/BarChart/LegendItem.tsx | 25 + src/library/BarChart/Wrappers.ts | 109 + src/library/BarChart/defaults.ts | 5 + src/library/BarChart/types.ts | 21 + src/library/Card/Wrappers.ts | 130 + src/library/Countdown/index.tsx | 58 + src/library/Countdown/types.ts | 9 + src/library/ErrorBoundary/Wrapper.ts | 48 + src/library/ErrorBoundary/index.tsx | 74 + src/library/EstimatedTxFee/Wrapper.ts | 17 + src/library/EstimatedTxFee/index.tsx | 58 + src/library/EstimatedTxFee/types.ts | 6 + src/library/Filter/Container.tsx | 13 + src/library/Filter/Item.tsx | 36 + src/library/Filter/LargeItem.tsx | 47 + src/library/Filter/Tabs.tsx | 41 + src/library/Filter/Wrappers.ts | 138 + src/library/Filter/defaults.ts | 17 + src/library/Filter/types.ts | 31 + src/library/Form/Bond/BondFeedback.tsx | 193 + src/library/Form/Bond/BondInput.tsx | 102 + .../Form/ClaimPermissionInput/index.tsx | 101 + .../Form/CreatePoolStatusBar/Wrapper.ts | 78 + .../Form/CreatePoolStatusBar/index.tsx | 51 + src/library/Form/MinDelayInput/Wrapper.ts | 65 + src/library/Form/MinDelayInput/index.tsx | 62 + src/library/Form/MinDelayInput/types.ts | 9 + src/library/Form/NominateStatusBar/Wrapper.ts | 81 + src/library/Form/NominateStatusBar/index.tsx | 78 + src/library/Form/Unbond/UnbondFeedback.tsx | 165 + src/library/Form/Unbond/UnbondInput.tsx | 100 + src/library/Form/Warning/Wrapper.ts | 25 + src/library/Form/Warning/index.tsx | 16 + src/library/Form/Wrappers.ts | 115 + src/library/Form/types.ts | 97 + src/library/GenerateNominations/index.tsx | 366 ++ src/library/GenerateNominations/types.ts | 24 + .../GenerateNominations/useFetchMethods.tsx | 215 + src/library/Graphs/EraPoints.tsx | 125 + src/library/Graphs/GeoDonut.tsx | 93 + src/library/Graphs/PayoutBar.tsx | 196 + src/library/Graphs/PayoutLine.tsx | 184 + src/library/Graphs/Utils.ts | 528 ++ src/library/Graphs/Wrapper.ts | 11 + src/library/Graphs/types.ts | 51 + src/library/Headers/Connect.tsx | 54 + src/library/Headers/Connected.tsx | 79 + src/library/Headers/SideMenuToggle.tsx | 26 + src/library/Headers/Spinner.tsx | 74 + src/library/Headers/Wrappers.ts | 139 + src/library/Headers/index.tsx | 83 + src/library/Help/Items/ActiveDefinition.tsx | 22 + src/library/Help/Items/Definition.tsx | 47 + src/library/Help/Items/External.tsx | 44 + src/library/Help/Wrappers.ts | 134 + src/library/Help/index.tsx | 215 + src/library/Hooks/index.tsx | 50 + src/library/Hooks/useBatchCall/index.tsx | 38 + src/library/Hooks/useBlockNumber/index.tsx | 45 + .../Hooks/useBondGreatestFee/index.tsx | 63 + src/library/Hooks/useBuildPayload/index.tsx | 64 + .../Hooks/useDotLottieButton/index.tsx | 100 + src/library/Hooks/useEraTimeLeft/index.tsx | 42 + src/library/Hooks/useErasToTimeLeft/index.tsx | 32 + src/library/Hooks/useFillVariables/index.tsx | 82 + src/library/Hooks/useFillVariables/types.ts | 7 + src/library/Hooks/useInflation/index.tsx | 62 + src/library/Hooks/useLedgerLoop/index.tsx | 45 + src/library/Hooks/useLedgerLoop/types.ts | 15 + .../Hooks/useNominationStatus/index.tsx | 120 + src/library/Hooks/usePayeeConfig/index.tsx | 65 + src/library/Hooks/usePoolCommission/index.tsx | 19 + src/library/Hooks/usePoolFilters/index.tsx | 162 + src/library/Hooks/usePrices/index.tsx | 83 + src/library/Hooks/useProxySupported/index.tsx | 64 + src/library/Hooks/useSignerWarnings/index.tsx | 47 + src/library/Hooks/useSize/index.tsx | 38 + .../Hooks/useSubmitExtrinsic/index.tsx | 307 + src/library/Hooks/useSubmitExtrinsic/types.ts | 20 + src/library/Hooks/useTimeLeft/defaults.ts | 14 + src/library/Hooks/useTimeLeft/index.tsx | 134 + src/library/Hooks/useTimeLeft/types.ts | 32 + src/library/Hooks/useTimeLeft/utils.ts | 83 + src/library/Hooks/useUnitPrice/index.tsx | 41 + src/library/Hooks/useUnstaking/index.tsx | 63 + .../Hooks/useValidatorFilters/index.tsx | 269 + src/library/Import/Confirm.tsx | 37 + src/library/Import/Heading.tsx | 56 + src/library/Import/NoAccounts.tsx | 35 + src/library/Import/Remove.tsx | 37 + src/library/Import/Wrappers.ts | 182 + src/library/Import/types.ts | 37 + src/library/List/MotionContainer.tsx | 29 + src/library/List/Pagination.tsx | 46 + src/library/List/SearchInput.tsx | 20 + src/library/List/Selectable.tsx | 68 + src/library/List/context.tsx | 69 + src/library/List/defaults.ts | 15 + src/library/List/index.ts | 188 + src/library/List/types.ts | 33 + src/library/ListItem/Labels/Blocked.tsx | 38 + src/library/ListItem/Labels/Commission.tsx | 24 + src/library/ListItem/Labels/CopyAddress.tsx | 39 + src/library/ListItem/Labels/EraStatus.tsx | 34 + src/library/ListItem/Labels/FavoritePool.tsx | 59 + .../ListItem/Labels/FavoriteValidator.tsx | 59 + src/library/ListItem/Labels/Identity.tsx | 43 + src/library/ListItem/Labels/JoinPool.tsx | 39 + src/library/ListItem/Labels/Members.tsx | 27 + src/library/ListItem/Labels/Metrics.tsx | 30 + .../ListItem/Labels/NominationStatus.tsx | 58 + .../ListItem/Labels/Oversubscribed.tsx | 67 + src/library/ListItem/Labels/ParaValidator.tsx | 33 + src/library/ListItem/Labels/PoolBonded.tsx | 93 + .../ListItem/Labels/PoolCommission.tsx | 28 + src/library/ListItem/Labels/PoolId.tsx | 27 + src/library/ListItem/Labels/PoolIdentity.tsx | 39 + .../ListItem/Labels/PoolMemberBonded.tsx | 63 + src/library/ListItem/Labels/Quartile.tsx | 35 + src/library/ListItem/Labels/Select.tsx | 28 + src/library/ListItem/Wrappers.ts | 367 ++ src/library/ListItem/types.ts | 65 + src/library/Loader/Announcement.tsx | 8 + src/library/Loader/Wrapper.ts | 36 + src/library/Menu/Wrappers.ts | 40 + src/library/Menu/index.tsx | 74 + src/library/Modal/Close.tsx | 18 + src/library/Modal/Title.tsx | 58 + src/library/Modal/Wrappers.ts | 124 + src/library/NetworkBar/Status.tsx | 31 + src/library/NetworkBar/Wrappers.ts | 115 + src/library/NetworkBar/index.tsx | 102 + src/library/Nominations/Wrapper.ts | 23 + src/library/Nominations/index.tsx | 155 + src/library/Nominations/types.ts | 18 + src/library/Notifications/Wrapper.ts | 42 + src/library/Notifications/index.tsx | 45 + src/library/PayeeInput/Wrapper.ts | 92 + src/library/PayeeInput/index.tsx | 155 + src/library/PayeeInput/types.ts | 13 + src/library/PluginLabel/Wrapper.ts | 23 + src/library/PluginLabel/index.tsx | 20 + src/library/PluginLabel/types.ts | 8 + src/library/Pool/Rewards.tsx | 165 + src/library/Pool/index.tsx | 173 + src/library/Pool/types.ts | 30 + src/library/PoolList/Default.tsx | 282 + src/library/PoolList/context.tsx | 33 + src/library/PoolList/defaults.ts | 12 + src/library/PoolList/types.ts | 23 + src/library/Prompt/Title.tsx | 59 + src/library/Prompt/Wrappers.ts | 201 + src/library/Prompt/index.tsx | 26 + src/library/QRCode/Display.tsx | 121 + src/library/QRCode/DisplayPayload.tsx | 39 + src/library/QRCode/Scan.tsx | 49 + src/library/QRCode/ScanSignature.tsx | 32 + src/library/QRCode/Wrappers.ts | 33 + src/library/QRCode/constants.ts | 8 + src/library/QRCode/qrcode.ts | 13 + src/library/QRCode/types.ts | 58 + src/library/QRCode/util.ts | 77 + src/library/SelectItems/Item.tsx | 50 + src/library/SelectItems/Wrapper.ts | 161 + src/library/SelectItems/index.tsx | 92 + src/library/SelectItems/types.ts | 28 + src/library/SetupSteps/Footer/Wrapper.ts | 14 + src/library/SetupSteps/Footer/index.tsx | 36 + src/library/SetupSteps/Header/Wrapper.ts | 44 + src/library/SetupSteps/Header/index.tsx | 54 + src/library/SetupSteps/MotionContainer.tsx | 39 + src/library/SetupSteps/Nominate.tsx | 62 + src/library/SetupSteps/types.ts | 26 + src/library/SideMenu/Heading/Heading.tsx | 11 + src/library/SideMenu/Heading/Wrapper.ts | 19 + src/library/SideMenu/Main.tsx | 192 + src/library/SideMenu/Primary/Wrappers.ts | 104 + src/library/SideMenu/Primary/index.tsx | 79 + src/library/SideMenu/Secondary/Wrappers.ts | 104 + src/library/SideMenu/Secondary/index.tsx | 47 + src/library/SideMenu/Wrapper.ts | 110 + src/library/SideMenu/index.tsx | 161 + src/library/SideMenu/types.ts | 40 + src/library/Stat/Wrapper.ts | 88 + src/library/Stat/index.tsx | 144 + src/library/Stat/types.ts | 28 + src/library/StatBoxList/Item.tsx | 37 + src/library/StatBoxList/Number.tsx | 42 + src/library/StatBoxList/Pie.tsx | 76 + src/library/StatBoxList/Text.tsx | 37 + src/library/StatBoxList/Timeleft.tsx | 59 + src/library/StatBoxList/Wrapper.ts | 199 + src/library/StatBoxList/index.tsx | 12 + src/library/StatBoxList/types.ts | 49 + src/library/StatsHead/Wrapper.ts | 72 + src/library/StatsHead/index.tsx | 29 + src/library/StatsHead/types.ts | 10 + src/library/StatusButton/Wrapper.ts | 33 + src/library/StatusButton/index.tsx | 30 + src/library/StatusButton/types.ts | 8 + src/library/StatusLabel/Wrapper.ts | 40 + src/library/StatusLabel/index.tsx | 61 + src/library/StatusLabel/types.ts | 17 + src/library/SubmitTx/Default.tsx | 46 + src/library/SubmitTx/ManualSign/Ledger.tsx | 174 + .../SubmitTx/ManualSign/Vault/SignPrompt.tsx | 98 + .../SubmitTx/ManualSign/Vault/index.tsx | 70 + src/library/SubmitTx/ManualSign/index.tsx | 34 + src/library/SubmitTx/index.tsx | 111 + src/library/SubmitTx/types.ts | 27 + src/library/Tips/Tip.tsx | 88 + src/library/Tips/Wrappers.ts | 51 + src/library/Tooltip/Wrapper.ts | 23 + src/library/Tooltip/index.tsx | 71 + src/library/UpdateHeader/Wrapper.ts | 29 + src/library/UpdateHeader/index.tsx | 36 + .../ValidatorList/FilterValidators.tsx | 62 + .../ValidatorList/Filters/FilterBadges.tsx | 64 + .../ValidatorList/Filters/FilterHeaders.tsx | 57 + src/library/ValidatorList/OrderValidators.tsx | 40 + .../ValidatorList/ValidatorItem/Default.tsx | 148 + .../ValidatorItem/Nomination.tsx | 81 + .../ValidatorList/ValidatorItem/Pulse.tsx | 179 + .../ValidatorList/ValidatorItem/Utils.tsx | 83 + .../ValidatorList/ValidatorItem/index.tsx | 27 + .../ValidatorList/ValidatorItem/types.ts | 29 + src/library/ValidatorList/index.tsx | 417 ++ src/library/ValidatorList/types.ts | 33 + src/locale/cn/base.json | 56 + src/locale/cn/help.json | 417 ++ src/locale/cn/library.json | 191 + src/locale/cn/modals.json | 279 + src/locale/cn/pages.json | 230 + src/locale/cn/tips.json | 103 + src/locale/en/base.json | 62 + src/locale/en/help.json | 411 ++ src/locale/en/library.json | 193 + src/locale/en/modals.json | 294 + src/locale/en/pages.json | 233 + src/locale/en/tips.json | 103 + src/locale/index.tsx | 91 + src/locale/utils.ts | 137 + src/main.tsx | 27 + src/modals/AccountPoolRoles/Wrappers.ts | 35 + src/modals/AccountPoolRoles/index.tsx | 87 + src/modals/Accounts/Account.tsx | 136 + src/modals/Accounts/Delegates/Wrapper.ts | 22 + src/modals/Accounts/Delegates/index.tsx | 40 + src/modals/Accounts/Wrappers.ts | 163 + src/modals/Accounts/index.tsx | 245 + src/modals/Accounts/types.ts | 37 + src/modals/BalanceTest/index.tsx | 74 + src/modals/Bio/Wrapper.ts | 19 + src/modals/Bio/index.tsx | 20 + src/modals/Bond/index.tsx | 179 + src/modals/ChangeNominations/index.tsx | 155 + src/modals/ChangePoolRoles/RoleChange.tsx | 37 + src/modals/ChangePoolRoles/Wrapper.ts | 60 + src/modals/ChangePoolRoles/index.tsx | 90 + src/modals/ChooseLanguage/Wrapper.ts | 55 + src/modals/ChooseLanguage/index.tsx | 50 + src/modals/ClaimPayouts/Forms.tsx | 163 + src/modals/ClaimPayouts/Item.tsx | 67 + src/modals/ClaimPayouts/Overview.tsx | 39 + src/modals/ClaimPayouts/Wrappers.ts | 61 + src/modals/ClaimPayouts/index.tsx | 102 + src/modals/ClaimPayouts/types.ts | 28 + src/modals/ClaimReward/index.tsx | 119 + src/modals/Connect/Extension.tsx | 110 + src/modals/Connect/Ledger.tsx | 86 + src/modals/Connect/Proxies.tsx | 116 + src/modals/Connect/ReadOnly.tsx | 115 + src/modals/Connect/Vault.tsx | 72 + src/modals/Connect/Wrappers.ts | 218 + src/modals/Connect/index.tsx | 194 + src/modals/Connect/types.ts | 29 + src/modals/DismissTips/index.tsx | 43 + src/modals/GoToFeedback/index.tsx | 45 + src/modals/ImportLedger/Addresses.tsx | 110 + src/modals/ImportLedger/Manage.tsx | 87 + src/modals/ImportLedger/Reset.tsx | 55 + src/modals/ImportLedger/Splash.tsx | 112 + src/modals/ImportLedger/Wrappers.ts | 99 + src/modals/ImportLedger/index.tsx | 169 + src/modals/ImportVault/Reader.tsx | 93 + src/modals/ImportVault/index.tsx | 143 + src/modals/JoinPool/index.tsx | 160 + src/modals/ManageFastUnstake/index.tsx | 223 + .../ManagePool/Forms/ClaimCommission.tsx | 106 + src/modals/ManagePool/Forms/Commission.tsx | 656 +++ src/modals/ManagePool/Forms/LeavePool.tsx | 152 + .../ManagePool/Forms/SetClaimPermission.tsx | 109 + src/modals/ManagePool/Forms/SetMetadata.tsx | 118 + src/modals/ManagePool/Forms/SetState.tsx | 158 + src/modals/ManagePool/Forms/index.tsx | 41 + src/modals/ManagePool/Forms/types.ts | 10 + src/modals/ManagePool/Tasks.tsx | 150 + src/modals/ManagePool/Wrappers.ts | 136 + src/modals/ManagePool/index.tsx | 92 + src/modals/Networks/ProvidersPrompt.tsx | 52 + src/modals/Networks/Wrapper.ts | 204 + src/modals/Networks/index.tsx | 151 + src/modals/Networks/types.ts | 6 + src/modals/PoolNominations/Wrappers.ts | 17 + src/modals/PoolNominations/index.tsx | 41 + src/modals/Settings/index.tsx | 53 + src/modals/Unbond/index.tsx | 245 + src/modals/UnbondPoolMember/index.tsx | 128 + src/modals/UnlockChunks/Chunk.tsx | 76 + src/modals/UnlockChunks/Forms.tsx | 179 + src/modals/UnlockChunks/Overview.tsx | 147 + src/modals/UnlockChunks/Wrappers.ts | 59 + src/modals/UnlockChunks/index.tsx | 141 + src/modals/Unstake/index.tsx | 152 + .../UpdateController/Switch/Wrappers.ts | 68 + src/modals/UpdateController/Switch/index.tsx | 50 + src/modals/UpdateController/Wrapper.ts | 17 + src/modals/UpdateController/index.tsx | 86 + src/modals/UpdatePayee/index.tsx | 167 + src/modals/UpdateReserve/index.tsx | 153 + src/modals/Utils/StaticNote.tsx | 33 + src/modals/ValidatorGeo/index.tsx | 147 + src/modals/ValidatorMetrics/index.tsx | 143 + src/modals/WithdrawPoolMember/index.tsx | 111 + src/overlay/index.tsx | 80 + src/pages/Community/Entity.tsx | 101 + src/pages/Community/Item.tsx | 169 + src/pages/Community/List.tsx | 51 + src/pages/Community/Wrappers.ts | 188 + src/pages/Community/context.tsx | 56 + src/pages/Community/defaults.ts | 14 + src/pages/Community/index.tsx | 31 + src/pages/Community/types.ts | 16 + .../Nominate/Active/ControllerNotStash.tsx | 70 + src/pages/Nominate/Active/ManageBond.tsx | 131 + .../Active/Stats/ActiveNominators.tsx | 41 + .../Active/Stats/MinimumActiveStake.tsx | 27 + .../Active/Stats/MinimumNominatorBond.tsx | 25 + .../Active/Status/NominationStatus.tsx | 109 + .../Active/Status/PayoutDestinationStatus.tsx | 72 + .../Active/Status/UnclaimedPayoutsStatus.tsx | 68 + src/pages/Nominate/Active/Status/index.tsx | 18 + src/pages/Nominate/Active/UnstakePrompts.tsx | 99 + src/pages/Nominate/Active/index.tsx | 108 + src/pages/Nominate/Active/types.ts | 12 + src/pages/Nominate/Setup/Bond/index.tsx | 93 + src/pages/Nominate/Setup/Payee/index.tsx | 110 + src/pages/Nominate/Setup/Summary/Wrapper.ts | 43 + src/pages/Nominate/Setup/Summary/index.tsx | 139 + src/pages/Nominate/Setup/index.tsx | 89 + src/pages/Nominate/Wrappers.ts | 31 + src/pages/Nominate/index.tsx | 12 + src/pages/Overview/ActiveAccounts/Item.tsx | 96 + src/pages/Overview/ActiveAccounts/Wrappers.ts | 120 + src/pages/Overview/ActiveAccounts/index.tsx | 17 + src/pages/Overview/ActiveAccounts/types.ts | 9 + src/pages/Overview/BalanceChart.tsx | 297 + src/pages/Overview/BalanceLinks.tsx | 59 + .../Overview/NetworkSats/Announcements.tsx | 137 + src/pages/Overview/NetworkSats/Wrappers.ts | 52 + src/pages/Overview/NetworkSats/index.tsx | 62 + src/pages/Overview/Payouts.tsx | 61 + src/pages/Overview/StakeStatus/Tips/Items.tsx | 135 + .../Overview/StakeStatus/Tips/PageToggle.tsx | 69 + .../Overview/StakeStatus/Tips/Syncing.tsx | 45 + .../Overview/StakeStatus/Tips/Wrappers.ts | 155 + src/pages/Overview/StakeStatus/Tips/index.tsx | 202 + src/pages/Overview/StakeStatus/Tips/types.ts | 11 + src/pages/Overview/StakeStatus/Wrappers.ts | 45 + src/pages/Overview/StakeStatus/index.tsx | 34 + .../Overview/Stats/ActiveEraTimeLeft.tsx | 44 + .../Overview/Stats/HistoricalRewardsRate.tsx | 38 + src/pages/Overview/Stats/SupplyStaked.tsx | 47 + src/pages/Overview/Wrappers.ts | 105 + src/pages/Overview/index.tsx | 132 + src/pages/Payouts/PayoutList/context.tsx | 36 + src/pages/Payouts/PayoutList/index.tsx | 279 + src/pages/Payouts/Stats/LastEraPayout.tsx | 26 + src/pages/Payouts/Wrappers.ts | 95 + src/pages/Payouts/index.tsx | 124 + src/pages/Payouts/types.ts | 13 + src/pages/Pools/Create/Bond/index.tsx | 93 + src/pages/Pools/Create/PoolName/Input.tsx | 55 + src/pages/Pools/Create/PoolName/index.tsx | 81 + src/pages/Pools/Create/PoolRoles/index.tsx | 100 + src/pages/Pools/Create/Summary/Wrapper.ts | 41 + src/pages/Pools/Create/Summary/index.tsx | 152 + src/pages/Pools/Create/index.tsx | 85 + src/pages/Pools/Home/ClosurePrompts.tsx | 110 + src/pages/Pools/Home/Favorites/index.tsx | 67 + src/pages/Pools/Home/ManageBond.tsx | 128 + src/pages/Pools/Home/ManagePool/Wrappers.ts | 53 + src/pages/Pools/Home/ManagePool/index.tsx | 76 + src/pages/Pools/Home/Members.tsx | 94 + src/pages/Pools/Home/MembersList/Default.tsx | 195 + .../Pools/Home/MembersList/FetchPage.tsx | 201 + src/pages/Pools/Home/MembersList/Member.tsx | 140 + src/pages/Pools/Home/MembersList/types.ts | 18 + .../Pools/Home/PoolStats/Announcements.tsx | 104 + src/pages/Pools/Home/PoolStats/Wrappers.ts | 53 + src/pages/Pools/Home/PoolStats/index.tsx | 82 + src/pages/Pools/Home/Stats/ActivePools.tsx | 19 + src/pages/Pools/Home/Stats/MinCreateBond.tsx | 25 + src/pages/Pools/Home/Stats/MinJoinBond.tsx | 25 + .../Pools/Home/Status/MembershipStatus.tsx | 103 + src/pages/Pools/Home/Status/PoolStatus.tsx | 67 + src/pages/Pools/Home/Status/RewardsStatus.tsx | 81 + src/pages/Pools/Home/Status/index.tsx | 27 + .../Pools/Home/Status/useStatusButtons.tsx | 82 + src/pages/Pools/Home/context.tsx | 43 + src/pages/Pools/Home/index.tsx | 177 + src/pages/Pools/PoolAccount/Wrapper.ts | 53 + src/pages/Pools/PoolAccount/index.tsx | 95 + .../Pools/Roles/RoleEditInput/Wrapper.ts | 47 + src/pages/Pools/Roles/RoleEditInput/index.tsx | 70 + src/pages/Pools/Roles/index.tsx | 264 + src/pages/Pools/Roles/types.ts | 19 + src/pages/Pools/index.tsx | 11 + src/pages/Pools/types.ts | 18 + src/pages/Validators/AllValidators.tsx | 70 + src/pages/Validators/Favorites.tsx | 45 + .../Validators/Stats/ActiveValidators.tsx | 41 + .../Validators/Stats/AverageCommission.tsx | 18 + .../Validators/Stats/TotalValidators.tsx | 40 + src/pages/Validators/context.tsx | 47 + src/pages/Validators/index.tsx | 53 + src/styles/graphs.ts | 23 + src/styles/index.scss | 9 + src/types/index.ts | 129 + src/vite-env.d.ts | 8 + src/workers/poolPerformance.ts | 63 + src/workers/stakers.ts | 204 + src/workers/types.ts | 30 + tests/graphs.test.ts | 157 + tsconfig.json | 30 + tsconfig.node.json | 9 + vite.config.ts | 46 + yarn.lock | 5200 +++++++++++++++++ 712 files changed, 58755 insertions(+) create mode 100644 .env create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/auto-merge.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/gh-publish.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .github/workflows/stale.yml create mode 100644 .gitignore create mode 100644 .licenserc.json create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 .scripts/localeOrderKeys.cjs create mode 100644 .scripts/localeValidate.cjs create mode 100644 .scripts/utils.cjs create mode 100755 .shell/build-container.sh create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 index.html create mode 100644 package.json create mode 100644 public/favicons/kusama/android-chrome-192x192.png create mode 100644 public/favicons/kusama/android-chrome-512x512.png create mode 100644 public/favicons/kusama/apple-touch-icon.png create mode 100644 public/favicons/kusama/browserconfig.xml create mode 100644 public/favicons/kusama/favicon-16x16.png create mode 100644 public/favicons/kusama/favicon-32x32.png create mode 100644 public/favicons/kusama/favicon.ico create mode 100644 public/favicons/kusama/mstile-150x150.png create mode 100644 public/favicons/kusama/safari-pinned-tab.svg create mode 100644 public/favicons/kusama/site.webmanifest create mode 100644 public/favicons/polkadot/android-chrome-192x192.png create mode 100644 public/favicons/polkadot/android-chrome-512x512.png create mode 100644 public/favicons/polkadot/apple-touch-icon.png create mode 100644 public/favicons/polkadot/browserconfig.xml create mode 100644 public/favicons/polkadot/favicon-16x16.png create mode 100644 public/favicons/polkadot/favicon-32x32.png create mode 100644 public/favicons/polkadot/favicon.ico create mode 100644 public/favicons/polkadot/mstile-150x150.png create mode 100644 public/favicons/polkadot/safari-pinned-tab.svg create mode 100644 public/favicons/polkadot/site.webmanifest create mode 100644 public/favicons/westend/android-chrome-192x192.png create mode 100644 public/favicons/westend/android-chrome-512x512.png create mode 100644 public/favicons/westend/apple-touch-icon.png create mode 100644 public/favicons/westend/browserconfig.xml create mode 100644 public/favicons/westend/favicon-16x16.png create mode 100644 public/favicons/westend/favicon-32x32.png create mode 100644 public/favicons/westend/favicon.ico create mode 100644 public/favicons/westend/mstile-150x150.png create mode 100644 public/favicons/westend/safari-pinned-tab.svg create mode 100644 public/favicons/westend/site.webmanifest create mode 100644 public/fonts/Inter.ttf create mode 100644 public/fonts/Unbounded.ttf create mode 100644 public/img/og-image.png create mode 100644 public/lottie/analytics-dark.lottie create mode 100644 public/lottie/analytics-light.lottie create mode 100644 public/lottie/globe-dark.lottie create mode 100644 public/lottie/globe-light.lottie create mode 100644 public/lottie/groups-dark.lottie create mode 100644 public/lottie/groups-light.lottie create mode 100644 public/lottie/label-dark.lottie create mode 100644 public/lottie/label-light.lottie create mode 100644 public/lottie/player.js create mode 100644 public/lottie/refresh-dark.lottie create mode 100644 public/lottie/refresh-light.lottie create mode 100644 public/lottie/trending-dark.lottie create mode 100644 public/lottie/trending-light.lottie create mode 100644 public/lottie/view-dark.lottie create mode 100644 public/lottie/view-light.lottie create mode 100644 public/robots.txt create mode 100644 src/App.tsx create mode 100644 src/Providers.tsx create mode 100644 src/Router.tsx create mode 100644 src/Themes.tsx create mode 100644 src/canvas/ManageNominations/Prompts/FavoritesPrompt.tsx create mode 100644 src/canvas/ManageNominations/Prompts/RevertPrompt.tsx create mode 100644 src/canvas/ManageNominations/Wrappers.tsx create mode 100644 src/canvas/ManageNominations/index.tsx create mode 100644 src/canvas/ManageNominations/types.ts create mode 100644 src/config/help.ts create mode 100644 src/config/ledger.ts create mode 100644 src/config/networks.ts create mode 100644 src/config/pages.ts create mode 100644 src/config/proxies.ts create mode 100644 src/config/tips.ts create mode 100644 src/config/tokens/svg/DOT.svg create mode 100644 src/config/tokens/svg/KSM.svg create mode 100644 src/config/tokens/svg/WND.svg create mode 100644 src/config/validators/Amforc.tsx create mode 100644 src/config/validators/ApertureMining.tsx create mode 100644 src/config/validators/Blockseeker.tsx create mode 100644 src/config/validators/Blockshard.tsx create mode 100644 src/config/validators/CoinbaseCloud.tsx create mode 100644 src/config/validators/Crifferent.tsx create mode 100644 src/config/validators/Decentradot.tsx create mode 100644 src/config/validators/Dionysus.tsx create mode 100644 src/config/validators/Dozenodes.tsx create mode 100644 src/config/validators/DragonStake.tsx create mode 100644 src/config/validators/Gatotech.tsx create mode 100644 src/config/validators/Gdot.tsx create mode 100644 src/config/validators/GenericChain.tsx create mode 100644 src/config/validators/GoOpen.tsx create mode 100644 src/config/validators/Helikon.tsx create mode 100644 src/config/validators/Highstake.tsx create mode 100644 src/config/validators/Metaspan.tsx create mode 100644 src/config/validators/PDP.tsx create mode 100644 src/config/validators/Paranodes.tsx create mode 100644 src/config/validators/PioneerStake.tsx create mode 100644 src/config/validators/Polkachu.tsx create mode 100644 src/config/validators/Polkadotters.tsx create mode 100644 src/config/validators/PythagorasCapitalInvestment.tsx create mode 100644 src/config/validators/SekoyaLabs.tsx create mode 100644 src/config/validators/StakeWorld.tsx create mode 100644 src/config/validators/Stakely.tsx create mode 100644 src/config/validators/Stakenode.tsx create mode 100644 src/config/validators/Stakepile.tsx create mode 100644 src/config/validators/Stakeplus.tsx create mode 100644 src/config/validators/StakerSpace.tsx create mode 100644 src/config/validators/Staking4All.tsx create mode 100644 src/config/validators/StakingFacilities.tsx create mode 100644 src/config/validators/TurboFlakes.tsx create mode 100644 src/config/validators/VFValidierung.tsx create mode 100644 src/config/validators/Wojdot.tsx create mode 100644 src/config/validators/bLdNodes.tsx create mode 100644 src/consts.ts create mode 100644 src/contexts/ActiveAccounts/defaults.ts create mode 100644 src/contexts/ActiveAccounts/index.tsx create mode 100644 src/contexts/ActiveAccounts/types.ts create mode 100644 src/contexts/Api/defaults.ts create mode 100644 src/contexts/Api/index.tsx create mode 100644 src/contexts/Api/types.ts create mode 100644 src/contexts/Balances/Utils.ts create mode 100644 src/contexts/Balances/defaults.ts create mode 100644 src/contexts/Balances/index.tsx create mode 100644 src/contexts/Balances/types.ts create mode 100644 src/contexts/Bonded/defaults.ts create mode 100644 src/contexts/Bonded/index.tsx create mode 100644 src/contexts/Bonded/types.ts create mode 100644 src/contexts/Connect/ImportedAccounts/defaults.ts create mode 100644 src/contexts/Connect/ImportedAccounts/index.tsx create mode 100644 src/contexts/Connect/ImportedAccounts/types.ts create mode 100644 src/contexts/Connect/OtherAccounts/defaults.ts create mode 100644 src/contexts/Connect/OtherAccounts/index.tsx create mode 100644 src/contexts/Connect/OtherAccounts/types.ts create mode 100644 src/contexts/Connect/Utils.ts create mode 100644 src/contexts/Extrinsics/defaults.ts create mode 100644 src/contexts/Extrinsics/index.tsx create mode 100644 src/contexts/Extrinsics/types.ts create mode 100644 src/contexts/FastUnstake/defaults.ts create mode 100644 src/contexts/FastUnstake/index.tsx create mode 100644 src/contexts/FastUnstake/types.ts create mode 100644 src/contexts/Filters/defaults.ts create mode 100644 src/contexts/Filters/index.tsx create mode 100644 src/contexts/Filters/types.ts create mode 100644 src/contexts/Hardware/Ledger.tsx create mode 100644 src/contexts/Hardware/Utils.tsx create mode 100644 src/contexts/Hardware/Vault.tsx create mode 100644 src/contexts/Hardware/defaults.ts create mode 100644 src/contexts/Hardware/types.ts create mode 100644 src/contexts/Help/defaults.ts create mode 100644 src/contexts/Help/index.tsx create mode 100644 src/contexts/Help/types.ts create mode 100644 src/contexts/Identities/defaults.ts create mode 100644 src/contexts/Identities/index.tsx create mode 100644 src/contexts/Identities/types.ts create mode 100644 src/contexts/Menu/defaults.ts create mode 100644 src/contexts/Menu/index.tsx create mode 100644 src/contexts/Menu/types.ts create mode 100644 src/contexts/Migrate/index.tsx create mode 100644 src/contexts/Network/defaults.ts create mode 100644 src/contexts/Network/index.tsx create mode 100644 src/contexts/Network/types.ts create mode 100644 src/contexts/NetworkMetrics/defaults.ts create mode 100644 src/contexts/NetworkMetrics/index.tsx create mode 100644 src/contexts/NetworkMetrics/types.ts create mode 100644 src/contexts/Notifications/defaults.ts create mode 100644 src/contexts/Notifications/index.tsx create mode 100644 src/contexts/Notifications/types.ts create mode 100644 src/contexts/Payouts/Utils.ts create mode 100644 src/contexts/Payouts/defaults.ts create mode 100644 src/contexts/Payouts/index.tsx create mode 100644 src/contexts/Payouts/types.ts create mode 100644 src/contexts/Plugins/Polkawatch/defaults.tsx create mode 100644 src/contexts/Plugins/Polkawatch/index.tsx create mode 100644 src/contexts/Plugins/Polkawatch/types.tsx create mode 100644 src/contexts/Plugins/Subscan/defaults.ts create mode 100644 src/contexts/Plugins/Subscan/index.tsx create mode 100644 src/contexts/Plugins/Subscan/types.ts create mode 100644 src/contexts/Plugins/defaults.ts create mode 100644 src/contexts/Plugins/index.tsx create mode 100644 src/contexts/Plugins/types.ts create mode 100644 src/contexts/Pools/ActivePools/defaults.ts create mode 100644 src/contexts/Pools/ActivePools/index.tsx create mode 100644 src/contexts/Pools/BondedPools/defaults.ts create mode 100644 src/contexts/Pools/BondedPools/index.tsx create mode 100644 src/contexts/Pools/PoolMembers/defaults.ts create mode 100644 src/contexts/Pools/PoolMembers/index.tsx create mode 100644 src/contexts/Pools/PoolMemberships/defaults.ts create mode 100644 src/contexts/Pools/PoolMemberships/index.tsx create mode 100644 src/contexts/Pools/PoolPerformance/defaults.ts create mode 100644 src/contexts/Pools/PoolPerformance/index.tsx create mode 100644 src/contexts/Pools/PoolPerformance/types.ts create mode 100644 src/contexts/Pools/PoolsConfig/defaults.ts create mode 100644 src/contexts/Pools/PoolsConfig/index.tsx create mode 100644 src/contexts/Pools/types.ts create mode 100644 src/contexts/Prompt/defaults.tsx create mode 100644 src/contexts/Prompt/index.tsx create mode 100644 src/contexts/Prompt/types.ts create mode 100644 src/contexts/Proxies/defaults.ts create mode 100644 src/contexts/Proxies/index.tsx create mode 100644 src/contexts/Proxies/type.ts create mode 100644 src/contexts/Setup/defaults.ts create mode 100644 src/contexts/Setup/index.tsx create mode 100644 src/contexts/Setup/types.ts create mode 100644 src/contexts/Staking/Utils.ts create mode 100644 src/contexts/Staking/defaults.ts create mode 100644 src/contexts/Staking/index.tsx create mode 100644 src/contexts/Staking/types.ts create mode 100644 src/contexts/Themes/defaults.ts create mode 100644 src/contexts/Themes/index.tsx create mode 100644 src/contexts/Themes/types.ts create mode 100644 src/contexts/Tooltip/defaults.ts create mode 100644 src/contexts/Tooltip/index.tsx create mode 100644 src/contexts/Tooltip/types.ts create mode 100644 src/contexts/TransferOptions/defaults.ts create mode 100644 src/contexts/TransferOptions/index.tsx create mode 100644 src/contexts/TransferOptions/types.ts create mode 100644 src/contexts/TxMeta/defaults.ts create mode 100644 src/contexts/TxMeta/index.tsx create mode 100644 src/contexts/TxMeta/types.ts create mode 100644 src/contexts/UI/defaults.ts create mode 100644 src/contexts/UI/index.tsx create mode 100644 src/contexts/UI/types.ts create mode 100644 src/contexts/Validators/FavoriteValidators/defaults.ts create mode 100644 src/contexts/Validators/FavoriteValidators/index.tsx create mode 100644 src/contexts/Validators/Utils.ts create mode 100644 src/contexts/Validators/ValidatorEntries/defaults.ts create mode 100644 src/contexts/Validators/ValidatorEntries/index.tsx create mode 100644 src/contexts/Validators/types.ts create mode 100644 src/img/appIcons/kusama.svg create mode 100644 src/img/appIcons/polkadot.svg create mode 100644 src/img/brave-logo.svg create mode 100644 src/img/cog-outline.svg create mode 100644 src/img/cross.svg create mode 100644 src/img/forum.svg create mode 100644 src/img/info-outline.svg create mode 100644 src/img/info.svg create mode 100644 src/img/kusama_icon.svg create mode 100644 src/img/kusama_inline.svg create mode 100644 src/img/kusama_logo.svg create mode 100644 src/img/language.svg create mode 100644 src/img/ledgerLogo.svg create mode 100644 src/img/logo-github.svg create mode 100644 src/img/moon-outline.svg create mode 100644 src/img/polkadot_icon.svg create mode 100644 src/img/polkadot_inline.svg create mode 100755 src/img/polkadot_logo.svg create mode 100644 src/img/sunny-outline.svg create mode 100644 src/img/wallet.svg create mode 100644 src/img/walletConnect.svg create mode 100644 src/img/westend_icon.svg create mode 100644 src/img/westend_inline.svg create mode 100644 src/img/westend_logo.svg create mode 100644 src/library/Account/Default.tsx create mode 100644 src/library/Account/Pool.tsx create mode 100644 src/library/Account/Wrapper.ts create mode 100644 src/library/Account/types.ts create mode 100644 src/library/AccountInput/Wrapper.ts create mode 100644 src/library/AccountInput/index.tsx create mode 100644 src/library/AccountInput/types.ts create mode 100644 src/library/BarChart/BarSegment.tsx create mode 100644 src/library/BarChart/BondedChart.tsx create mode 100644 src/library/BarChart/LegendItem.tsx create mode 100644 src/library/BarChart/Wrappers.ts create mode 100644 src/library/BarChart/defaults.ts create mode 100644 src/library/BarChart/types.ts create mode 100644 src/library/Card/Wrappers.ts create mode 100644 src/library/Countdown/index.tsx create mode 100644 src/library/Countdown/types.ts create mode 100644 src/library/ErrorBoundary/Wrapper.ts create mode 100644 src/library/ErrorBoundary/index.tsx create mode 100644 src/library/EstimatedTxFee/Wrapper.ts create mode 100644 src/library/EstimatedTxFee/index.tsx create mode 100644 src/library/EstimatedTxFee/types.ts create mode 100644 src/library/Filter/Container.tsx create mode 100644 src/library/Filter/Item.tsx create mode 100644 src/library/Filter/LargeItem.tsx create mode 100644 src/library/Filter/Tabs.tsx create mode 100644 src/library/Filter/Wrappers.ts create mode 100644 src/library/Filter/defaults.ts create mode 100644 src/library/Filter/types.ts create mode 100644 src/library/Form/Bond/BondFeedback.tsx create mode 100644 src/library/Form/Bond/BondInput.tsx create mode 100644 src/library/Form/ClaimPermissionInput/index.tsx create mode 100644 src/library/Form/CreatePoolStatusBar/Wrapper.ts create mode 100644 src/library/Form/CreatePoolStatusBar/index.tsx create mode 100644 src/library/Form/MinDelayInput/Wrapper.ts create mode 100644 src/library/Form/MinDelayInput/index.tsx create mode 100644 src/library/Form/MinDelayInput/types.ts create mode 100644 src/library/Form/NominateStatusBar/Wrapper.ts create mode 100644 src/library/Form/NominateStatusBar/index.tsx create mode 100644 src/library/Form/Unbond/UnbondFeedback.tsx create mode 100644 src/library/Form/Unbond/UnbondInput.tsx create mode 100644 src/library/Form/Warning/Wrapper.ts create mode 100644 src/library/Form/Warning/index.tsx create mode 100644 src/library/Form/Wrappers.ts create mode 100644 src/library/Form/types.ts create mode 100644 src/library/GenerateNominations/index.tsx create mode 100644 src/library/GenerateNominations/types.ts create mode 100644 src/library/GenerateNominations/useFetchMethods.tsx create mode 100644 src/library/Graphs/EraPoints.tsx create mode 100644 src/library/Graphs/GeoDonut.tsx create mode 100644 src/library/Graphs/PayoutBar.tsx create mode 100644 src/library/Graphs/PayoutLine.tsx create mode 100644 src/library/Graphs/Utils.ts create mode 100644 src/library/Graphs/Wrapper.ts create mode 100644 src/library/Graphs/types.ts create mode 100644 src/library/Headers/Connect.tsx create mode 100644 src/library/Headers/Connected.tsx create mode 100644 src/library/Headers/SideMenuToggle.tsx create mode 100644 src/library/Headers/Spinner.tsx create mode 100644 src/library/Headers/Wrappers.ts create mode 100644 src/library/Headers/index.tsx create mode 100644 src/library/Help/Items/ActiveDefinition.tsx create mode 100644 src/library/Help/Items/Definition.tsx create mode 100644 src/library/Help/Items/External.tsx create mode 100644 src/library/Help/Wrappers.ts create mode 100644 src/library/Help/index.tsx create mode 100644 src/library/Hooks/index.tsx create mode 100644 src/library/Hooks/useBatchCall/index.tsx create mode 100644 src/library/Hooks/useBlockNumber/index.tsx create mode 100644 src/library/Hooks/useBondGreatestFee/index.tsx create mode 100644 src/library/Hooks/useBuildPayload/index.tsx create mode 100644 src/library/Hooks/useDotLottieButton/index.tsx create mode 100644 src/library/Hooks/useEraTimeLeft/index.tsx create mode 100644 src/library/Hooks/useErasToTimeLeft/index.tsx create mode 100644 src/library/Hooks/useFillVariables/index.tsx create mode 100644 src/library/Hooks/useFillVariables/types.ts create mode 100644 src/library/Hooks/useInflation/index.tsx create mode 100644 src/library/Hooks/useLedgerLoop/index.tsx create mode 100644 src/library/Hooks/useLedgerLoop/types.ts create mode 100644 src/library/Hooks/useNominationStatus/index.tsx create mode 100644 src/library/Hooks/usePayeeConfig/index.tsx create mode 100644 src/library/Hooks/usePoolCommission/index.tsx create mode 100644 src/library/Hooks/usePoolFilters/index.tsx create mode 100644 src/library/Hooks/usePrices/index.tsx create mode 100644 src/library/Hooks/useProxySupported/index.tsx create mode 100644 src/library/Hooks/useSignerWarnings/index.tsx create mode 100644 src/library/Hooks/useSize/index.tsx create mode 100644 src/library/Hooks/useSubmitExtrinsic/index.tsx create mode 100644 src/library/Hooks/useSubmitExtrinsic/types.ts create mode 100644 src/library/Hooks/useTimeLeft/defaults.ts create mode 100644 src/library/Hooks/useTimeLeft/index.tsx create mode 100644 src/library/Hooks/useTimeLeft/types.ts create mode 100644 src/library/Hooks/useTimeLeft/utils.ts create mode 100644 src/library/Hooks/useUnitPrice/index.tsx create mode 100644 src/library/Hooks/useUnstaking/index.tsx create mode 100644 src/library/Hooks/useValidatorFilters/index.tsx create mode 100644 src/library/Import/Confirm.tsx create mode 100644 src/library/Import/Heading.tsx create mode 100644 src/library/Import/NoAccounts.tsx create mode 100644 src/library/Import/Remove.tsx create mode 100644 src/library/Import/Wrappers.ts create mode 100644 src/library/Import/types.ts create mode 100644 src/library/List/MotionContainer.tsx create mode 100644 src/library/List/Pagination.tsx create mode 100644 src/library/List/SearchInput.tsx create mode 100644 src/library/List/Selectable.tsx create mode 100644 src/library/List/context.tsx create mode 100644 src/library/List/defaults.ts create mode 100644 src/library/List/index.ts create mode 100644 src/library/List/types.ts create mode 100644 src/library/ListItem/Labels/Blocked.tsx create mode 100644 src/library/ListItem/Labels/Commission.tsx create mode 100644 src/library/ListItem/Labels/CopyAddress.tsx create mode 100644 src/library/ListItem/Labels/EraStatus.tsx create mode 100644 src/library/ListItem/Labels/FavoritePool.tsx create mode 100644 src/library/ListItem/Labels/FavoriteValidator.tsx create mode 100644 src/library/ListItem/Labels/Identity.tsx create mode 100644 src/library/ListItem/Labels/JoinPool.tsx create mode 100644 src/library/ListItem/Labels/Members.tsx create mode 100644 src/library/ListItem/Labels/Metrics.tsx create mode 100644 src/library/ListItem/Labels/NominationStatus.tsx create mode 100644 src/library/ListItem/Labels/Oversubscribed.tsx create mode 100644 src/library/ListItem/Labels/ParaValidator.tsx create mode 100644 src/library/ListItem/Labels/PoolBonded.tsx create mode 100644 src/library/ListItem/Labels/PoolCommission.tsx create mode 100644 src/library/ListItem/Labels/PoolId.tsx create mode 100644 src/library/ListItem/Labels/PoolIdentity.tsx create mode 100644 src/library/ListItem/Labels/PoolMemberBonded.tsx create mode 100644 src/library/ListItem/Labels/Quartile.tsx create mode 100644 src/library/ListItem/Labels/Select.tsx create mode 100644 src/library/ListItem/Wrappers.ts create mode 100644 src/library/ListItem/types.ts create mode 100644 src/library/Loader/Announcement.tsx create mode 100644 src/library/Loader/Wrapper.ts create mode 100644 src/library/Menu/Wrappers.ts create mode 100644 src/library/Menu/index.tsx create mode 100644 src/library/Modal/Close.tsx create mode 100644 src/library/Modal/Title.tsx create mode 100644 src/library/Modal/Wrappers.ts create mode 100644 src/library/NetworkBar/Status.tsx create mode 100644 src/library/NetworkBar/Wrappers.ts create mode 100644 src/library/NetworkBar/index.tsx create mode 100644 src/library/Nominations/Wrapper.ts create mode 100644 src/library/Nominations/index.tsx create mode 100644 src/library/Nominations/types.ts create mode 100644 src/library/Notifications/Wrapper.ts create mode 100644 src/library/Notifications/index.tsx create mode 100644 src/library/PayeeInput/Wrapper.ts create mode 100644 src/library/PayeeInput/index.tsx create mode 100644 src/library/PayeeInput/types.ts create mode 100644 src/library/PluginLabel/Wrapper.ts create mode 100644 src/library/PluginLabel/index.tsx create mode 100644 src/library/PluginLabel/types.ts create mode 100644 src/library/Pool/Rewards.tsx create mode 100644 src/library/Pool/index.tsx create mode 100644 src/library/Pool/types.ts create mode 100644 src/library/PoolList/Default.tsx create mode 100644 src/library/PoolList/context.tsx create mode 100644 src/library/PoolList/defaults.ts create mode 100644 src/library/PoolList/types.ts create mode 100644 src/library/Prompt/Title.tsx create mode 100644 src/library/Prompt/Wrappers.ts create mode 100644 src/library/Prompt/index.tsx create mode 100644 src/library/QRCode/Display.tsx create mode 100644 src/library/QRCode/DisplayPayload.tsx create mode 100644 src/library/QRCode/Scan.tsx create mode 100644 src/library/QRCode/ScanSignature.tsx create mode 100644 src/library/QRCode/Wrappers.ts create mode 100644 src/library/QRCode/constants.ts create mode 100644 src/library/QRCode/qrcode.ts create mode 100644 src/library/QRCode/types.ts create mode 100644 src/library/QRCode/util.ts create mode 100644 src/library/SelectItems/Item.tsx create mode 100644 src/library/SelectItems/Wrapper.ts create mode 100644 src/library/SelectItems/index.tsx create mode 100644 src/library/SelectItems/types.ts create mode 100644 src/library/SetupSteps/Footer/Wrapper.ts create mode 100644 src/library/SetupSteps/Footer/index.tsx create mode 100644 src/library/SetupSteps/Header/Wrapper.ts create mode 100644 src/library/SetupSteps/Header/index.tsx create mode 100644 src/library/SetupSteps/MotionContainer.tsx create mode 100644 src/library/SetupSteps/Nominate.tsx create mode 100644 src/library/SetupSteps/types.ts create mode 100644 src/library/SideMenu/Heading/Heading.tsx create mode 100644 src/library/SideMenu/Heading/Wrapper.ts create mode 100644 src/library/SideMenu/Main.tsx create mode 100644 src/library/SideMenu/Primary/Wrappers.ts create mode 100644 src/library/SideMenu/Primary/index.tsx create mode 100644 src/library/SideMenu/Secondary/Wrappers.ts create mode 100644 src/library/SideMenu/Secondary/index.tsx create mode 100644 src/library/SideMenu/Wrapper.ts create mode 100644 src/library/SideMenu/index.tsx create mode 100644 src/library/SideMenu/types.ts create mode 100644 src/library/Stat/Wrapper.ts create mode 100644 src/library/Stat/index.tsx create mode 100644 src/library/Stat/types.ts create mode 100644 src/library/StatBoxList/Item.tsx create mode 100644 src/library/StatBoxList/Number.tsx create mode 100644 src/library/StatBoxList/Pie.tsx create mode 100644 src/library/StatBoxList/Text.tsx create mode 100644 src/library/StatBoxList/Timeleft.tsx create mode 100644 src/library/StatBoxList/Wrapper.ts create mode 100644 src/library/StatBoxList/index.tsx create mode 100644 src/library/StatBoxList/types.ts create mode 100644 src/library/StatsHead/Wrapper.ts create mode 100644 src/library/StatsHead/index.tsx create mode 100644 src/library/StatsHead/types.ts create mode 100644 src/library/StatusButton/Wrapper.ts create mode 100644 src/library/StatusButton/index.tsx create mode 100644 src/library/StatusButton/types.ts create mode 100644 src/library/StatusLabel/Wrapper.ts create mode 100644 src/library/StatusLabel/index.tsx create mode 100644 src/library/StatusLabel/types.ts create mode 100644 src/library/SubmitTx/Default.tsx create mode 100644 src/library/SubmitTx/ManualSign/Ledger.tsx create mode 100644 src/library/SubmitTx/ManualSign/Vault/SignPrompt.tsx create mode 100644 src/library/SubmitTx/ManualSign/Vault/index.tsx create mode 100644 src/library/SubmitTx/ManualSign/index.tsx create mode 100644 src/library/SubmitTx/index.tsx create mode 100644 src/library/SubmitTx/types.ts create mode 100644 src/library/Tips/Tip.tsx create mode 100644 src/library/Tips/Wrappers.ts create mode 100644 src/library/Tooltip/Wrapper.ts create mode 100644 src/library/Tooltip/index.tsx create mode 100644 src/library/UpdateHeader/Wrapper.ts create mode 100644 src/library/UpdateHeader/index.tsx create mode 100644 src/library/ValidatorList/FilterValidators.tsx create mode 100644 src/library/ValidatorList/Filters/FilterBadges.tsx create mode 100644 src/library/ValidatorList/Filters/FilterHeaders.tsx create mode 100644 src/library/ValidatorList/OrderValidators.tsx create mode 100644 src/library/ValidatorList/ValidatorItem/Default.tsx create mode 100644 src/library/ValidatorList/ValidatorItem/Nomination.tsx create mode 100644 src/library/ValidatorList/ValidatorItem/Pulse.tsx create mode 100644 src/library/ValidatorList/ValidatorItem/Utils.tsx create mode 100644 src/library/ValidatorList/ValidatorItem/index.tsx create mode 100644 src/library/ValidatorList/ValidatorItem/types.ts create mode 100644 src/library/ValidatorList/index.tsx create mode 100644 src/library/ValidatorList/types.ts create mode 100644 src/locale/cn/base.json create mode 100644 src/locale/cn/help.json create mode 100644 src/locale/cn/library.json create mode 100644 src/locale/cn/modals.json create mode 100644 src/locale/cn/pages.json create mode 100644 src/locale/cn/tips.json create mode 100644 src/locale/en/base.json create mode 100644 src/locale/en/help.json create mode 100644 src/locale/en/library.json create mode 100644 src/locale/en/modals.json create mode 100644 src/locale/en/pages.json create mode 100644 src/locale/en/tips.json create mode 100644 src/locale/index.tsx create mode 100644 src/locale/utils.ts create mode 100644 src/main.tsx create mode 100644 src/modals/AccountPoolRoles/Wrappers.ts create mode 100644 src/modals/AccountPoolRoles/index.tsx create mode 100644 src/modals/Accounts/Account.tsx create mode 100644 src/modals/Accounts/Delegates/Wrapper.ts create mode 100644 src/modals/Accounts/Delegates/index.tsx create mode 100644 src/modals/Accounts/Wrappers.ts create mode 100644 src/modals/Accounts/index.tsx create mode 100644 src/modals/Accounts/types.ts create mode 100644 src/modals/BalanceTest/index.tsx create mode 100644 src/modals/Bio/Wrapper.ts create mode 100644 src/modals/Bio/index.tsx create mode 100644 src/modals/Bond/index.tsx create mode 100644 src/modals/ChangeNominations/index.tsx create mode 100644 src/modals/ChangePoolRoles/RoleChange.tsx create mode 100644 src/modals/ChangePoolRoles/Wrapper.ts create mode 100644 src/modals/ChangePoolRoles/index.tsx create mode 100644 src/modals/ChooseLanguage/Wrapper.ts create mode 100644 src/modals/ChooseLanguage/index.tsx create mode 100644 src/modals/ClaimPayouts/Forms.tsx create mode 100644 src/modals/ClaimPayouts/Item.tsx create mode 100644 src/modals/ClaimPayouts/Overview.tsx create mode 100644 src/modals/ClaimPayouts/Wrappers.ts create mode 100644 src/modals/ClaimPayouts/index.tsx create mode 100644 src/modals/ClaimPayouts/types.ts create mode 100644 src/modals/ClaimReward/index.tsx create mode 100644 src/modals/Connect/Extension.tsx create mode 100644 src/modals/Connect/Ledger.tsx create mode 100644 src/modals/Connect/Proxies.tsx create mode 100644 src/modals/Connect/ReadOnly.tsx create mode 100644 src/modals/Connect/Vault.tsx create mode 100644 src/modals/Connect/Wrappers.ts create mode 100644 src/modals/Connect/index.tsx create mode 100644 src/modals/Connect/types.ts create mode 100644 src/modals/DismissTips/index.tsx create mode 100644 src/modals/GoToFeedback/index.tsx create mode 100644 src/modals/ImportLedger/Addresses.tsx create mode 100644 src/modals/ImportLedger/Manage.tsx create mode 100644 src/modals/ImportLedger/Reset.tsx create mode 100644 src/modals/ImportLedger/Splash.tsx create mode 100644 src/modals/ImportLedger/Wrappers.ts create mode 100644 src/modals/ImportLedger/index.tsx create mode 100644 src/modals/ImportVault/Reader.tsx create mode 100644 src/modals/ImportVault/index.tsx create mode 100644 src/modals/JoinPool/index.tsx create mode 100644 src/modals/ManageFastUnstake/index.tsx create mode 100644 src/modals/ManagePool/Forms/ClaimCommission.tsx create mode 100644 src/modals/ManagePool/Forms/Commission.tsx create mode 100644 src/modals/ManagePool/Forms/LeavePool.tsx create mode 100644 src/modals/ManagePool/Forms/SetClaimPermission.tsx create mode 100644 src/modals/ManagePool/Forms/SetMetadata.tsx create mode 100644 src/modals/ManagePool/Forms/SetState.tsx create mode 100644 src/modals/ManagePool/Forms/index.tsx create mode 100644 src/modals/ManagePool/Forms/types.ts create mode 100644 src/modals/ManagePool/Tasks.tsx create mode 100644 src/modals/ManagePool/Wrappers.ts create mode 100644 src/modals/ManagePool/index.tsx create mode 100644 src/modals/Networks/ProvidersPrompt.tsx create mode 100644 src/modals/Networks/Wrapper.ts create mode 100644 src/modals/Networks/index.tsx create mode 100644 src/modals/Networks/types.ts create mode 100644 src/modals/PoolNominations/Wrappers.ts create mode 100644 src/modals/PoolNominations/index.tsx create mode 100644 src/modals/Settings/index.tsx create mode 100644 src/modals/Unbond/index.tsx create mode 100644 src/modals/UnbondPoolMember/index.tsx create mode 100644 src/modals/UnlockChunks/Chunk.tsx create mode 100644 src/modals/UnlockChunks/Forms.tsx create mode 100644 src/modals/UnlockChunks/Overview.tsx create mode 100644 src/modals/UnlockChunks/Wrappers.ts create mode 100644 src/modals/UnlockChunks/index.tsx create mode 100644 src/modals/Unstake/index.tsx create mode 100644 src/modals/UpdateController/Switch/Wrappers.ts create mode 100644 src/modals/UpdateController/Switch/index.tsx create mode 100644 src/modals/UpdateController/Wrapper.ts create mode 100644 src/modals/UpdateController/index.tsx create mode 100644 src/modals/UpdatePayee/index.tsx create mode 100644 src/modals/UpdateReserve/index.tsx create mode 100644 src/modals/Utils/StaticNote.tsx create mode 100644 src/modals/ValidatorGeo/index.tsx create mode 100644 src/modals/ValidatorMetrics/index.tsx create mode 100644 src/modals/WithdrawPoolMember/index.tsx create mode 100644 src/overlay/index.tsx create mode 100644 src/pages/Community/Entity.tsx create mode 100644 src/pages/Community/Item.tsx create mode 100644 src/pages/Community/List.tsx create mode 100644 src/pages/Community/Wrappers.ts create mode 100644 src/pages/Community/context.tsx create mode 100644 src/pages/Community/defaults.ts create mode 100644 src/pages/Community/index.tsx create mode 100644 src/pages/Community/types.ts create mode 100644 src/pages/Nominate/Active/ControllerNotStash.tsx create mode 100644 src/pages/Nominate/Active/ManageBond.tsx create mode 100644 src/pages/Nominate/Active/Stats/ActiveNominators.tsx create mode 100644 src/pages/Nominate/Active/Stats/MinimumActiveStake.tsx create mode 100644 src/pages/Nominate/Active/Stats/MinimumNominatorBond.tsx create mode 100644 src/pages/Nominate/Active/Status/NominationStatus.tsx create mode 100644 src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx create mode 100644 src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx create mode 100644 src/pages/Nominate/Active/Status/index.tsx create mode 100644 src/pages/Nominate/Active/UnstakePrompts.tsx create mode 100644 src/pages/Nominate/Active/index.tsx create mode 100644 src/pages/Nominate/Active/types.ts create mode 100644 src/pages/Nominate/Setup/Bond/index.tsx create mode 100644 src/pages/Nominate/Setup/Payee/index.tsx create mode 100644 src/pages/Nominate/Setup/Summary/Wrapper.ts create mode 100644 src/pages/Nominate/Setup/Summary/index.tsx create mode 100644 src/pages/Nominate/Setup/index.tsx create mode 100644 src/pages/Nominate/Wrappers.ts create mode 100644 src/pages/Nominate/index.tsx create mode 100644 src/pages/Overview/ActiveAccounts/Item.tsx create mode 100644 src/pages/Overview/ActiveAccounts/Wrappers.ts create mode 100644 src/pages/Overview/ActiveAccounts/index.tsx create mode 100644 src/pages/Overview/ActiveAccounts/types.ts create mode 100644 src/pages/Overview/BalanceChart.tsx create mode 100644 src/pages/Overview/BalanceLinks.tsx create mode 100644 src/pages/Overview/NetworkSats/Announcements.tsx create mode 100644 src/pages/Overview/NetworkSats/Wrappers.ts create mode 100644 src/pages/Overview/NetworkSats/index.tsx create mode 100644 src/pages/Overview/Payouts.tsx create mode 100644 src/pages/Overview/StakeStatus/Tips/Items.tsx create mode 100644 src/pages/Overview/StakeStatus/Tips/PageToggle.tsx create mode 100644 src/pages/Overview/StakeStatus/Tips/Syncing.tsx create mode 100644 src/pages/Overview/StakeStatus/Tips/Wrappers.ts create mode 100644 src/pages/Overview/StakeStatus/Tips/index.tsx create mode 100644 src/pages/Overview/StakeStatus/Tips/types.ts create mode 100644 src/pages/Overview/StakeStatus/Wrappers.ts create mode 100644 src/pages/Overview/StakeStatus/index.tsx create mode 100644 src/pages/Overview/Stats/ActiveEraTimeLeft.tsx create mode 100644 src/pages/Overview/Stats/HistoricalRewardsRate.tsx create mode 100644 src/pages/Overview/Stats/SupplyStaked.tsx create mode 100644 src/pages/Overview/Wrappers.ts create mode 100644 src/pages/Overview/index.tsx create mode 100644 src/pages/Payouts/PayoutList/context.tsx create mode 100644 src/pages/Payouts/PayoutList/index.tsx create mode 100644 src/pages/Payouts/Stats/LastEraPayout.tsx create mode 100644 src/pages/Payouts/Wrappers.ts create mode 100644 src/pages/Payouts/index.tsx create mode 100644 src/pages/Payouts/types.ts create mode 100644 src/pages/Pools/Create/Bond/index.tsx create mode 100644 src/pages/Pools/Create/PoolName/Input.tsx create mode 100644 src/pages/Pools/Create/PoolName/index.tsx create mode 100644 src/pages/Pools/Create/PoolRoles/index.tsx create mode 100644 src/pages/Pools/Create/Summary/Wrapper.ts create mode 100644 src/pages/Pools/Create/Summary/index.tsx create mode 100644 src/pages/Pools/Create/index.tsx create mode 100644 src/pages/Pools/Home/ClosurePrompts.tsx create mode 100644 src/pages/Pools/Home/Favorites/index.tsx create mode 100644 src/pages/Pools/Home/ManageBond.tsx create mode 100644 src/pages/Pools/Home/ManagePool/Wrappers.ts create mode 100644 src/pages/Pools/Home/ManagePool/index.tsx create mode 100644 src/pages/Pools/Home/Members.tsx create mode 100644 src/pages/Pools/Home/MembersList/Default.tsx create mode 100644 src/pages/Pools/Home/MembersList/FetchPage.tsx create mode 100644 src/pages/Pools/Home/MembersList/Member.tsx create mode 100644 src/pages/Pools/Home/MembersList/types.ts create mode 100644 src/pages/Pools/Home/PoolStats/Announcements.tsx create mode 100644 src/pages/Pools/Home/PoolStats/Wrappers.ts create mode 100644 src/pages/Pools/Home/PoolStats/index.tsx create mode 100644 src/pages/Pools/Home/Stats/ActivePools.tsx create mode 100644 src/pages/Pools/Home/Stats/MinCreateBond.tsx create mode 100644 src/pages/Pools/Home/Stats/MinJoinBond.tsx create mode 100644 src/pages/Pools/Home/Status/MembershipStatus.tsx create mode 100644 src/pages/Pools/Home/Status/PoolStatus.tsx create mode 100644 src/pages/Pools/Home/Status/RewardsStatus.tsx create mode 100644 src/pages/Pools/Home/Status/index.tsx create mode 100644 src/pages/Pools/Home/Status/useStatusButtons.tsx create mode 100644 src/pages/Pools/Home/context.tsx create mode 100644 src/pages/Pools/Home/index.tsx create mode 100644 src/pages/Pools/PoolAccount/Wrapper.ts create mode 100644 src/pages/Pools/PoolAccount/index.tsx create mode 100644 src/pages/Pools/Roles/RoleEditInput/Wrapper.ts create mode 100644 src/pages/Pools/Roles/RoleEditInput/index.tsx create mode 100644 src/pages/Pools/Roles/index.tsx create mode 100644 src/pages/Pools/Roles/types.ts create mode 100644 src/pages/Pools/index.tsx create mode 100644 src/pages/Pools/types.ts create mode 100644 src/pages/Validators/AllValidators.tsx create mode 100644 src/pages/Validators/Favorites.tsx create mode 100644 src/pages/Validators/Stats/ActiveValidators.tsx create mode 100644 src/pages/Validators/Stats/AverageCommission.tsx create mode 100644 src/pages/Validators/Stats/TotalValidators.tsx create mode 100644 src/pages/Validators/context.tsx create mode 100644 src/pages/Validators/index.tsx create mode 100644 src/styles/graphs.ts create mode 100644 src/styles/index.scss create mode 100644 src/types/index.ts create mode 100644 src/vite-env.d.ts create mode 100644 src/workers/poolPerformance.ts create mode 100644 src/workers/stakers.ts create mode 100644 src/workers/types.ts create mode 100644 tests/graphs.test.ts create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts create mode 100644 yarn.lock diff --git a/.env b/.env new file mode 100644 index 0000000000..ff9f481697 --- /dev/null +++ b/.env @@ -0,0 +1,17 @@ +# Disable all mentioning of fiat values and token prices. +# VITE_DISABLE_FIAT=1 + +# Display an organisation label in the network bar. +# VITE_ORGANISATION="© Parity Technologies" + +# Provide a privacy policy url in the network bar. +# VITE_PRIVACY_URL=https://www.parity.io/privacy/ + +# Toggle i18n to be in debug mode. Not debug by default. +# VITE_DEBUG_I18N=1 + +# Provide a disclaimer url in the network bar. +# VITE_DISCLAIMER_URL=https://parity.io/disclaimer/ + +# Provide a legal disclosure url in the network bar. +# VITE_LEGAL_DISCLOSURES_URL=https://polkadot.network/legal-disclosures/ \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000..8ce1b0402c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,18 @@ +*.css +*.svg +*.png +*.json +*.log +*.lock +*.md +*.ico +*.ttf +*.xml +*.txt +*.html +*.webmanifest +LICENSE +dist +build +vite.config.ts +public/lottie/player.js \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000000..0a2c0477a4 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,101 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:import/recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "airbnb", + // "airbnb-typescript", + "plugin:prettier/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true, + "arrowFunctions": true + }, + "ecmaVersion": "latest", + "sourceType": "module", + "project": "./tsconfig.json" + }, + "plugins": [ + "react", + "@typescript-eslint", + "prefer-arrow-functions", + "unused-imports" + ], + "rules": { + // NOTE: These rules are being reviewed and comments justifying their deactivation will be + // added. + "react/require-default-props": "off", + "react/no-access-state-in-setstate": "off", + "react/destructuring-assignment": "off", + "react/function-component-definition": "off", + "react/jsx-no-constructed-context-values": "off", + "react/no-array-index-key": "off", + "react/react-in-jsx-scope": "off", + "react/jsx-props-no-spreading": "off", + "react/jsx-no-useless-fragment": "off", + "react/static-property-placement": "off", + "no-unused-vars": "off", + "no-param-reassign": "off", + "no-restricted-syntax": "off", + "@typescript-eslint/no-empty-function": "off", + "no-use-before-define": "off", + "@typescript-eslint/no-explicit-any": "off", + "no-promise-executor-return": "off", + "prefer-destructuring": "off", + "no-nested-ternary": "off", + // `continue` statements cut down on conditional nesting and improve readability where it is + // used in this project. Conditionals would further bloat the code. + "no-continue": "off", + // Unary operators are not impacting code as semi-colons are currently enforced. + "no-plusplus": "off", + // Default imports cause naming inconsistencies to imports when component names are changed. + "import/prefer-default-export": "off", + + "unused-imports/no-unused-imports": "error", + "@typescript-eslint/consistent-type-imports": [ + "error", + { + "prefer": "type-imports", + "fixStyle": "separate-type-imports" + } + ], + "semi": [2, "always"], + "react/jsx-filename-extension": [ + "warn", + { + "extensions": [".tsx"] + } + ], + "import/extensions": [ + "error", + "ignorePackages", + { + "ts": "never", + "tsx": "never" + } + ], + "@typescript-eslint/no-shadow": ["error"], + "prefer-arrow-functions/prefer-arrow-functions": [ + "warn", + { + "classPropertiesAllowed": false, + "disallowPrototype": false, + "returnStyle": "unchanged", + "singleReturnOnly": false + } + ], + "react/prop-types": "off" + }, + "settings": { + "import/resolver": { + "typescript": {} + } + } +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..c7d3172c1e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 + +updates: + - package-ecosystem: npm + directory: '/' + schedule: + interval: daily + open-pull-requests-limit: 10 + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: daily diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000000..0fd2326124 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,17 @@ +name: Dependabot Auto Merge + +on: pull_request + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: ahmadnassri/action-dependabot-auto-merge@v2 + with: + target: minor + github-token: ${{ secrets.DEPENDABOT_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..49aa13e4a2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check-license-lines: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Check License Lines + uses: kt3k/license_checker@v1.0.6 + validate-locales: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 16.x + uses: actions/setup-node@v4 + with: + node-version: 16.x + - run: yarn run locale:validate + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 19.x, 20.x] + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: yarn install + - run: yarn build + - run: yarn lint + - run: yarn test + + all: + # This job ensures that all jobs above (now we have just build) are successful. + needs: [check-license-lines, build, validate-locales] + runs-on: ubuntu-latest + steps: + - run: echo Success diff --git a/.github/workflows/gh-publish.yml b/.github/workflows/gh-publish.yml new file mode 100644 index 0000000000..97d2397d4f --- /dev/null +++ b/.github/workflows/gh-publish.yml @@ -0,0 +1,23 @@ +name: GitHub Pages Publish + +on: + push: + branches: [main] + +jobs: + gh-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + registry-url: https://registry.npmjs.org + - run: yarn install + - name: Build + working-directory: '.' + run: yarn build:pages + - name: Deploy + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: build diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000000..9e93ec92d7 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,21 @@ +name: Release Automation + +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: google-github-actions/release-please-action@v3 + with: + # Commit to start releases from. This can be removed after the first release is merged. + last-release-sha: 99cfade027ce6ca81d0a14657d6bdd1b05406ad8 + release-type: node + package-name: staking-dashboard diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..a472af1ab9 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,17 @@ +name: 'Close Stale PRs' + +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v8 + with: + stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' + close-pr-message: 'This PR was closed because it has been stale for 5 days with no activity.' + days-before-pr-stale: 30 + days-before-pr-close: 5 + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..bd98e77ade --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +build +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.licenserc.json b/.licenserc.json new file mode 100644 index 0000000000..c502a50c86 --- /dev/null +++ b/.licenserc.json @@ -0,0 +1,7 @@ +{ + "**/*.{js, ts, tsx}": [ + "// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors", + "// SPDX-License-Identifier: GPL-3.0-only" + ], + "ignore": ["testdata", "npm", "public/", "Dockerfile"] +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..bb33007fd9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +**/build +**/node_modules +**/package.json +**/yarn.lock +**/package-lock.json +**/.eslintrc.js +**/tsconfig.json +dist +src/img/**/* +public/lottie/player.js \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000000..bfa6c96ee7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "arrowParens": "always", + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/.scripts/localeOrderKeys.cjs b/.scripts/localeOrderKeys.cjs new file mode 100644 index 0000000000..62b1009a2a --- /dev/null +++ b/.scripts/localeOrderKeys.cjs @@ -0,0 +1,42 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +const fs = require('fs'); +const path = require('path'); +const prettier = require('prettier'); +const { getDirectories, localeDir, orderJsonByKeys } = require('./utils.cjs'); + +// Get all language paths to re-order. +const languages = getDirectories(localeDir, []); + +// Gor each language path. +for (const lng of languages) { + const pathToLanguage = path.join(localeDir, `/${lng}`); + + fs.readdir(pathToLanguage, (error, files) => { + if (error) return; + + files.forEach(async (file) => { + const pathToFile = path.join(pathToLanguage, file); + const json = JSON.parse(fs.readFileSync(pathToFile).toString()); + + // order json object alphabetically. + const orderedJson = orderJsonByKeys(json); + + // format json object. + const formatted = await prettier.format(JSON.stringify(orderedJson), { + parser: 'json', + }); + + fs.writeFile(pathToFile, formatted, (err) => { + if (err) { + console.err(err); + } else { + console.log( + `----------Keys In ${pathToLanguage}/${file} Are Ordered Alphabetically-------------` + ); + } + }); + }); + }); +} diff --git a/.scripts/localeValidate.cjs b/.scripts/localeValidate.cjs new file mode 100644 index 0000000000..0a71147e74 --- /dev/null +++ b/.scripts/localeValidate.cjs @@ -0,0 +1,78 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +const fs = require('fs'); +const { join } = require('path'); +const { + getDeepKeys, + getDirectories, + localeDir, + orderJsonByKeys, +} = require('./utils.cjs'); + +// Missing key validation function. +const validateMissingKeys = () => { + const defaultPath = join(localeDir, 'en'); + const languages = getDirectories(localeDir, ['en']); + + fs.readdir(defaultPath, (error, files) => { + if (error) console.log(error); + + files.forEach((file) => { + const defaultJson = JSON.parse( + fs.readFileSync(join(defaultPath, file)).toString() + ); + + for (const lng of languages) { + const otherPath = join(localeDir, lng); + const otherJson = JSON.parse( + fs.readFileSync(join(otherPath, file)).toString() + ); + + const a = getDeepKeys(defaultJson); + const b = getDeepKeys(otherJson); + + if (a.sort().length !== b.sort().length) { + const missing = a.filter((item) => b.indexOf(item) < 0); + if (missing.join('').trim().length > 0) { + throw new Error( + `Missing the following keys from locale "${lng}", file: "${file}":\n"${missing}".` + ); + } + } + } + }); + }); +}; + +// Key order validation function. +const validateKeyOrder = () => { + // get all language paths to re-order. + const languages = getDirectories(localeDir, []); + + for (const lng of languages) { + const pathToLanguage = join(localeDir, `/${lng}`); + + fs.readdir(pathToLanguage, (error, files) => { + if (error) return; + + files.forEach((file) => { + const pathToFile = join(pathToLanguage, file); + const json = JSON.parse(fs.readFileSync(pathToFile).toString()); + + // order json object alphabetically. + const orderedJson = orderJsonByKeys(json); + if (JSON.stringify(json) !== JSON.stringify(orderedJson)) { + throw new Error( + `Keys are in the incorrect order from locale "${lng}", file: "${file}".` + ); + } + }); + }); + } +}; + +// validate missing keys +validateMissingKeys(); +// validate key order +validateKeyOrder(); diff --git a/.scripts/utils.cjs b/.scripts/utils.cjs new file mode 100644 index 0000000000..83eb0209e5 --- /dev/null +++ b/.scripts/utils.cjs @@ -0,0 +1,96 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +const fs = require('fs'); +const { join } = require('path'); + +// Project locale directory. +const localeDir = join(__dirname, '..', 'src', 'locale'); + +// The suffixes of keys related to i18n functionality that should be ignored. +const ignoreSuffixes = ['_one', '_two', '_few', '_many', '_other']; + +// Check if value is an object. Do not count arrays as objects. +const isObject = (o) => (Array.isArray(o) ? false : typeof o === 'object'); + +// Checks whether a key contains an ingore suffix. +const endsWithIgnoreSuffix = (key) => + ignoreSuffixes.some((i) => key.endsWith(i)); + +// Locale directories, ommitting `en` - the langauge to check missing keys against. +const getDirectories = (source, omit) => + fs + .readdirSync(source, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .filter((v) => !omit.includes(v.name)) + .map((dirent) => dirent.name); + +// Order keys of a json object. +const orderKeysAlphabetically = (o) => + Object.keys(o) + .sort() + .reduce((obj, key) => { + obj[key] = o[key]; + return obj; + }, {}); + +// Order json object by its keys. +const orderJsonByKeys = (json) => { + // order top level keys + json = orderKeysAlphabetically(json); + // order child objects if they are values. + const jsonOrdered = {}; + Object.entries(json).forEach(([k, v]) => { + if (isObject(v)) { + jsonOrdered[k] = orderJsonByKeys(v); + } else { + jsonOrdered[k] = v; + } + }); + return jsonOrdered; +}; + +// Recursive function to get all keys of a locale object. +const getDeepKeys = (obj) => { + let keys = []; + for (const key in obj) { + let isSubstring = false; + + // not number + if (isNaN(key)) { + // check if key includes any special substrings + if (endsWithIgnoreSuffix(key)) { + isSubstring = true; + // get the substring up to the last underscore + const rawKey = key.substring(0, key.lastIndexOf('_')); + // add the key to `keys` if it does not already exist + if (!keys.includes(rawKey)) { + keys.push(rawKey); + } + } + } + // full string, if not already added, go ahead and add + if (!isSubstring) { + if (!keys.includes(key)) { + keys.push(key); + } + } + // if object, recursively get keys + if (typeof obj[key] === 'object') { + const subkeys = getDeepKeys(obj[key]); + keys = keys.concat(subkeys.map((subkey) => `${key}.${subkey}`)); + } + } + return keys; +}; + +module.exports = { + endsWithIgnoreSuffix, + getDeepKeys, + getDirectories, + ignoreSuffixes, + isObject, + localeDir, + orderJsonByKeys, + orderKeysAlphabetically, +}; diff --git a/.shell/build-container.sh b/.shell/build-container.sh new file mode 100755 index 0000000000..5a3cde22b1 --- /dev/null +++ b/.shell/build-container.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +podman build \ + -t polkadot-staking-dashboard \ + -t psd \ + . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..bed439733f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +# Contribution Guide + +This section aims to familiarise developers to the Polkadot Staking Dashboard [[GitHub](https://github.com/paritytech/polkadot-staking-dashboard), [Demo](https://paritytech.github.io/polkadot-staking-dashboard/#/overview)] for the purpose of contributing to the project. + +Reach out to ross@parity.io for clarification of any content within this document. + +## Submitting Pull Requests + +This project follows the Conventional Commits specification. Pull requests are merged and squashed, with the pull request title being used as the commit message. Commit messages should adhere to the following structure: + +``` +(): +``` + +Some example PR titles: + +- `feat: implement help overlay` +- `feat(auth): implement login API` +- `fix(button): resolve issue with button alignment` +- `docs(readme): add installation section` +- `chore(tests): refactor user tests` + +The `(scope)` could be anything specifying the place of the commit change. For example, api, app, cli, etc. + +If you would like to know more about the Conventional Commits specification, please visit the [Conventional Commits website](https://www.conventionalcommits.org/). + +## Releases + +[Release Please](https://github.com/googleapis/release-please) is used for automating staking dashboard's changelog and release generation. + +Release Please is a GitHub action maintained by Google that automates CHANGELOG generation, the creation of GitHub releases, and version bumps. [[Gtihub docs](https://github.com/googleapis/release-please), [Action](https://github.com/marketplace/actions/release-please-action)] + +## Major Packages Used + +- React 18 +- Polkadot JS API [[docs](https://polkadot.js.org/docs/api)] +- React Chart JS 2 for graphing. [[docs](https://www.chartjs.org/docs/latest/), [React docs](https://react-chartjs-2.js.org/)] +- Framer Motion. [[docs](https://www.framer.com/docs/animation/)] +- [Font Awesome](https://fontawesome.com/v5/search) for the majority of icons. [Ionicons](https://ionic.io/ionicons) for side menu footer icons +- SCSS for theme configuration and Styled Components [[docs](https://styled-components.com/docs)] for component styling. + +## Environment Variables + +Optionally apply envrionment variables in an environment file such as `.env` or with `yarn build` to customise the build of staking dashboard. + +Refer to the [`.env`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/.env) file in the root of the repository for all supported variables. + +## Config Files + +There are some ad-hoc files defining app configuration where needed. These just provide a means of bootstrapping app data, and further abstraction could be explored in the future. + +- [`config/pages.ts`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/config/pages.ts): provides the pages and page categories of the app. +- [`config/help.ts`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/config/help.ts): provides the help content. +- [`Utils.ts`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/Utils.ts): Various general helper functions used throughout the app, such as formatting utilities. + +## Folders + +Folders are structured in the [`src/`](https://github.com/paritytech/polkadot-staking-dashboard/tree/main/src) directory to separate functional, presentational and context components: + +- [`contexts`](https://github.com/paritytech/polkadot-staking-dashboard/tree/main/src/contexts): context providers for the app. All Polkadot JS API interaction happens in these files. +- [`img`](https://github.com/paritytech/polkadot-staking-dashboard/tree/main/src/img): app SVGs. +- [`library`](https://github.com/paritytech/polkadot-staking-dashboard/tree/main/src/library): reusable components that could eventually be abstracted into a separate UI library. +- [`modals`](https://github.com/paritytech/polkadot-staking-dashboard/tree/main/src/modals): the various modal pop-ups used in the app. +- [`pages`](https://github.com/paritytech/polkadot-staking-dashboard/tree/main/src/pages): similar to modals, page components and components that comprise pages. +- [`workers`](https://github.com/paritytech/polkadot-staking-dashboard/tree/main/src/workers): web workers that crunch process-heavy scripts. Only one exists right now, that iterates `erasStakers` and calculates active nominators and minimum nomination bond. + +## App Entry + +Going from the top-most component, the component hierarchy is set up as follows: + +- [`index.tsx`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/index.tsx): DOM render, of little interest. +- [`App.tsx`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/App.tsx): wraps `` in the theme provider context and determines the active network from local storage. +- [`Providers.tsx`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/Providers.tsx): imports and wraps `` with all the contexts using a withProviders hook. We also wrap styled component's theme provider context here to make the theme configuration work. +- [`Router.tsx`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/Router.tsx): contains react router ``'s, in addition to the major app presentational components. + +## Development Patterns + +Documenting some of the development patterns used: + +- We use the **"Wrapper" terminology for styled components** that wrap a functional component. +- **Styles are defined on a per-component basis**, being defined in the same folder as the component markup itself. Where unavoidable (such as global styles, interface layout), styled components should reside in [`src/Wrappers.ts`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/Wrappers.tsx). +- **Theme values** are configured in [`styles/theme.scss`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/styles/theme.scss), and can be included in any styled component as custom properties. Graph colors are configurable from [`styles/graphs.ts`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/styles/graphs.ts). +- **Constants or default values** (such as those waiting for Polkadot API) are defined in [`src/constants.ts`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/constants.ts). +- Packages with **missing TypeScript definitions** can be declared in [`src/react-app-env.d.ts`](https://github.com/paritytech/polkadot-staking-dashboard/blob/main/src/react-app-env.d.ts). + +## TypeScript Support + +The majority of components have types. Type additions are welcome for data that makes sense to type (e.g. data that is unlikely to change as we continue development). + +Strict mode is used in development, so types are always required for objects. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..928422436e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:buster as BUILDER + +WORKDIR /app +COPY . . +RUN apt update && apt install -y git +RUN yarn install && yarn build + +# -------------------------------------------------- +FROM nginx + +COPY --from=BUILDER /app/build /usr/share/nginx/html + +EXPOSE 80 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..bdfbac5031 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Substrate Connect Copyright (C) 2020-2022 Parity Technologies Ltd + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..5b8cd91e4c --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +[![Polkadot - App](https://img.shields.io/badge/Polkadot-App-E6007A?logo=polkadot&logoColor=E6007A)](https://staking.polkadot.network) ![ci](https://github.com/paritytech/polkadot-staking-dashboard/actions/workflows/ci.yml/badge.svg) [![License](https://img.shields.io/badge/License-GPL3.0-blue.svg)](https://opensource.org/licenses/GPL-3.0) + +# Polkadot Staking Dashboard + +Screenshot 2023-08-29 at 18 54 33 + +## Contributing Community Assets + +The Polkadot Staking Dashboard is a community-driven project, and we welcome contributions to bolster the dashboard's functionality and features. + +- **Web Extensions**: Submit PR to the [Polkadot Cloud repository](https://github.com/paritytech/polkadot-cloud/tree/main/packages/assets#adding-web-extension-wallets) to add a web extension. The extension will then be available in the `@polkadot-cloud/assets` NPM package. Full instructions can be found [here](https://github.com/paritytech/polkadot-cloud/tree/main/packages/assets#adding-web-extension-wallets). + +- **Validator Operators**: Submit PR to the [Polkadot Cloud repository](https://github.com/paritytech/polkadot-cloud/tree/main/packages/assets#adding-validator-operators) to add a validator operator. The operator will then be available in the `@polkadot-cloud/assets` NPM package. Full instructions can be found [here](https://github.com/paritytech/polkadot-cloud/tree/main/packages/assets#adding-validator-operators). + +## URL Variable Support + +Polkadot Staking Dashboard supports URL variables that can be used to direct users to specific configurations of the app, such as landing on a specific language or on a specific network. Variables are added at the end of the hash portion of URL. + +The currently supported URL variables are as follows: + +- **n**: Controls the default network to connect to upon visiting the dashboard. Supported values are `polkadot`, `kusama` and `westend`. +- **l**: Controls the default to use upon visiting the dashboard. Supported values are `en` and `cn`. +- **a**: Controls the account to connect to upon visiting the dashboard. Ignored if the account is not present in the initial imported accounts. + +URL variables take precedence over saved values in local storage, and will overwrite current configurations. URL variables will update (if present) as a user switches configurations in-app, such as changing the network or language. + +### Example URL: + +The following URL will load Kusama and use the Chinese localisation resource: + +``` +staking.polkadot.network/#/overview?n=kusama&l=cn +``` + +## Using Containers + +You may build a container using: + +``` +./shell/build-container.sh +``` + +Then run your container with: + +``` +podman run --d -p 8080:80 localhost/polkadot-staking-dashboard +``` + +And access the **Staking Dashboard** at http://localhost:8080/. + +## Presentations + +- 29/06/2023: [[Video] Polkadot Decoded 2023: The Next Step of the Polkadot UX Journey](https://www.youtube.com/watch?v=s78SZZ_ZA64) +- 30/06/2022: [[Video] Polkadot Decoded 2022: Polkadot Staking Dashboard Demo](https://youtu.be/H1WGu6mf1Ls) diff --git a/index.html b/index.html new file mode 100644 index 0000000000..09726b6ee2 --- /dev/null +++ b/index.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Polkadot Staking Dashboard | Polkadot Staking (DOT) + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000000..f46a3abd3e --- /dev/null +++ b/package.json @@ -0,0 +1,100 @@ +{ + "name": "polkadot-staking-dashboard", + "version": "0.1.0", + "type": "module", + "license": "GPL-3.0-only", + "private": false, + "scripts": { + "build": "tsc && vite build --base '/'", + "build:pages": "tsc && vite build --base '/polkadot-staking-dashboard/'", + "clear": "rm -rf node_modules build tsconfig.tsbuildinfo", + "deploy:pages": "yarn build:pages && gh-pages -d build", + "dev": "vite", + "lint": "eslint . --fix && npx prettier --write . && npx prettier --write ./.scripts && node ./.scripts/localeOrderKeys.cjs", + "locale:order": "node ./.scripts/localeOrderKeys.cjs", + "locale:validate": "node ./.scripts/localeValidate.cjs", + "preview": "vite preview", + "test": "vitest", + "visualizer": "vite-bundle-visualizer" + }, + "dependencies": { + "@dotlottie/player-component": "^2.7.0", + "@fortawesome/fontawesome-svg-core": "^6.4.2", + "@fortawesome/free-brands-svg-icons": "^6.4.2", + "@fortawesome/free-regular-svg-icons": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.4.2", + "@fortawesome/react-fontawesome": "^0.2.0", + "@ledgerhq/hw-transport-webhid": "^6.27.19", + "@polkadot-cloud/assets": "^0.1.32", + "@polkadot-cloud/core": "^1.0.30", + "@polkadot-cloud/react": "^0.1.101", + "@polkadot-cloud/utils": "^0.0.23", + "@polkadot/api": "^10.10.1", + "@polkadot/keyring": "^12.1.1", + "@polkadot/rpc-provider": "^10.9.1", + "@polkadot/util": "^12.4.2", + "@polkadot/util-crypto": "12.5.1", + "@polkawatch/ddp-client": "^2.0.8", + "@substrate/connect": "^0.7.34", + "@zondax/ledger-substrate": "^0.41.3", + "bignumber.js": "^9.1.2", + "bn.js": "^5.2.1", + "buffer": "^6.0.3", + "chart.js": "^4.4.0", + "chroma-js": "^2.4.2", + "date-fns": "^2.29.3", + "framer-motion": "^10.16.3", + "i18next": "^23.6.0", + "i18next-browser-languagedetector": "^7.1.0", + "lodash.throttle": "^4.1.1", + "qrcode-generator": "1.4.4", + "rc-slider": "^10.3.1", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.11", + "react-helmet": "^6.1.0", + "react-i18next": "^13.3.1", + "react-qr-reader": "^2.2.1", + "react-router-dom": "^6.17.0", + "react-scroll": "^1.9.0", + "styled-components": "^6.1.0" + }, + "devDependencies": { + "@ledgerhq/logs": "^6.10.1", + "@types/chroma-js": "^2.4.0", + "@types/lodash.throttle": "^4.1.7", + "@types/react": "^18.2.33", + "@types/react-dom": "^18.2.14", + "@types/react-helmet": "^6.1.8", + "@types/react-qr-reader": "^2.1.6", + "@types/react-scroll": "^1.8.9", + "@typescript-eslint/eslint-plugin": "^6.9.0", + "@typescript-eslint/parser": "^6.9.0", + "@vitejs/plugin-react-swc": "^3.4.0", + "eslint": "^8.52.0", + "eslint-config-airbnb": "^19.0.4", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-prefer-arrow-functions": "^3.2.4", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-unused-imports": "^3.0.0", + "gh-pages": "^6.0.0", + "prettier": "^3.0.3", + "prettier-plugin-organize-imports": "^3.2.3", + "sass": "^1.69.5", + "typescript": "^5.2.2", + "vite": "^4.4.11", + "vite-bundle-visualizer": "^0.10.0", + "vite-plugin-checker": "^0.6.2", + "vite-plugin-eslint": "^1.8.1", + "vite-plugin-svgr": "^4.1.0", + "vite-tsconfig-paths": "^4.2.1", + "vitest": "^0.34.5" + } +} diff --git a/public/favicons/kusama/android-chrome-192x192.png b/public/favicons/kusama/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..18c3e3aa6b0854518fa15d21dffae62ede6d4a8c GIT binary patch literal 5063 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 literal 0 HcmV?d00001 diff --git a/public/favicons/kusama/android-chrome-512x512.png b/public/favicons/kusama/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..7169b6206004e7105238411188333be0a4f16839 GIT binary patch literal 14430 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 BH7@`F literal 0 HcmV?d00001 diff --git a/public/favicons/kusama/apple-touch-icon.png b/public/favicons/kusama/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ae7adbfed0164c4651c3de9cb094ec7faa76f8a9 GIT binary patch literal 2129 zcma)7X*3jiAO26ecFCS4rokY~9b>s7F*2FaB{6718iS^?W;A4<5eZGU4zea&BzxJC zW!&JJ0!j`kg1i&elpqP)-m4AcDnM+H<-7 zAMkSrI97sR;?m)3C>s<2HFTjp!f#v+C1UJt0JsAOAUYO+9j+D41|S##0Ou+I#+d-f z1mrYdJk4Dk@;Gm02@c}v@7uXbAOM380zg{zA3(77Dl1%*kBqfJ^UXp81hjR?%tTQD zc)wvSQMiz?#hfrZoTI|Ks4&zeNJ_}P+OMfv+x$|JA6&I<>fn_fwc)XJU90_~-V%jB zO9S5rS_Wwi)f6P?J2*(xd{{G}68w1}XkA!peA(jKSg_kj8)cXHfk5111u@#fwv2L@ z-A%%xX7|o)QKDv>W~=%jqWQ<%dF)83=t2$syKr;KYzqg{A%ISDI;WK5Rc4%kkKH?K* z$JE`{sNQR{4Iy)H2;FX9h!S^xTq=3-b6e)QR1x5Tr5P&bao?%{{}+>=w8Go7*#Vh_KS zpNqMlPFp&??r~HQDXcF2%(|RK)BJ)TdEnhVg+)ntb&kKXrQHcjFrYSQh+psj1-&fodp5rLHJkamaWDfwpXBHw>JpfU0kD-@9Q`BtDW1E%%{BA&5Q2! z2`3RZVAK@nLT#%?r-bl<8zn1HQI^j=*HMM1km^$2^e00LZJ3t}tc$-lKl~Lasp*fZ z`*JN`#?@xCq?do=1bOnWaOHEl6S(@Yb>7;|?ipm|mO_5wx-5BfxXZheS}1!ju9hNq zyovsewJ0o+E!3nuu%EZ@CgY87A64&{+g9FZlpNcWSG-7PqlYn{6aPraChb|(eaJ3s zO{sF1(wTmw)E39VoFUDB)1}J0wO*z#SR1l$p0{&J;VTw+-r*H*7;%4s5v;l9LiVpU z50Y`W$ktoIv?V)Ak5#KW!H^sHhzqf2=a@f&2358mbF{i^?BMLT6y;*r*%9eP^?dO{ z6QRMUo80FT;Pd_7YE7%cpZK|(Ua&XV-`$*XD1yS^@9jaTx1j5Z;iRhYJa+KS-^;J< z!=6;62~Vu0I3baGw_lW9nS71Qj5=8R^k~qDFS?81|8Ad0*0FP5Xcb=jiuwFpTmyvI zl77Im7FkwNpOr{{Wh#2x)M43Ubz!8Ke5j9onkTrz(4*cZ$jem^r2 z22`vqghpQmrI)-oEAput5lu{O9O)*OCCl1(akTr> z!`}YMObV12MC;*(Ti^#zLz*(h3R@!kOh-F-#xIx{8=N}9LA|DAp_oaoMf)6&!)oEN6_5{8+C29`mVv2jwopHVx`ZBN?=zX1+me98 zurHKkCo06-+3ckYu9Fd@gcfg)g3&(4gYm#94XEFEx|%q3yI)U(uUWIXR8rr0bGy*{ zrSlF4ZkGEDMG{Nvasi;A4l7Ht+RWogUebvber&9*D*l|1{85P#S@uwLZ2fEJ->ovs zkBoe6QZ9BIT^z|Z?|nHX=W?2UFIawjV3p?ZNit!)v|1}bT4ZF_i!{7gYiY_^tm9oJ z1_syG{ZtvXSbUxTYxj**zX<`El_C+XS%bG*PN_A=ixp+n zy-yU_SKeO6-gdJ2&)b0rkyPgITs} zi|!&5fNsStxs3@o8UUFwiJ!W!DYj|%>rGpK1mL^iblHI0+15OVn}i)v+bqP;$Rnup z=a}paHOSRBZ_y+D2EXEZE)UGujts;&Qlc)mR%w+DlNxB9v_&{^Fj;-!aRJB zTN8pd(f?8?TC<}d@OZDHqykSCxBpbh=u2cz0@=%$c*~1R0Es~AX(M#C5vOp7Q^xuR z#`=bu2!t^LAwYdU_CEn%f6wdQq5nS+@`dcp4LIMyT_W2PDA0gg{@&MpyrAUJ059nE z05TDPketaiVg5iTb@jdr{qM2U_JBuONQWpSp)7GeO_E0fY8kEH<;kO5hlBQy4E1!s i?^fvM>yO41SnB|ej2L;%!XSft0$|a$mQ@y4ss9C8(bT#C literal 0 HcmV?d00001 diff --git a/public/favicons/kusama/browserconfig.xml b/public/favicons/kusama/browserconfig.xml new file mode 100644 index 0000000000..890e5d861b --- /dev/null +++ b/public/favicons/kusama/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #111 + + + diff --git a/public/favicons/kusama/favicon-16x16.png b/public/favicons/kusama/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..9f22c2cf83015df0447e55fb600993bbd8258cd5 GIT binary patch literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{XiaP zfk$L91A~|<2s3&HseAwmvX^-Jy0SlK;TJ#4;`*Z&5z(7(Y};kPk~l-e1x8R_`v^(D!^vfoJ^Z_rjt_%4<)GCVDEGJ!_lQ z4s@q#iEBhjN@7W>RdP`(kYX@0Ff!IPFw`}$3^A~@GBLF>G1oRQure@M|MhqtiiX_$ zl+3hB+#2q!sJ{f%5MC7$Q4*9`u24{vpO%@Es!&o{kgAYbP?F5RP%-E6CmxQ%Fb$1U z{-@7)J`G}ER_4}A<`z~K_MR-lEUe(tU~)KxS$T7a!s#1VP8>ONMCJ(l=?0GlUV03# X#05(}IhjrcTEXDy>gTe~DWM4fL?yS4 literal 0 HcmV?d00001 diff --git a/public/favicons/kusama/favicon-32x32.png b/public/favicons/kusama/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..aadbb775c323c1e0cd6e6bcdfe701b9a466b59a4 GIT binary patch literal 659 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKptm- zM`SSrgP1A^GkON8d;kivmw5WRvOj0!@`{gMHoKZ zf9}cSlP_@ekao9-rr2ypvF0hRVQ$oDc{$% zUUI(wJ?GWjpa0J7{C=lcf9eXBl}6XiXBub~E&VaQ=&zp$i@(^F>D}2qC%Bpxyoian z+;MI~?0NY;39*}3EwQ+ieET#zUz2g~$?WB4cm8W?PO#HlXZGSkH0S=SQ+u{Yd<*~z z1(!}*)@vF5r;YpD7PXtdo@_5>ca7Y9$+Kd<>`$S(WY66ie9KF2PY`BVzizJ8RPh+e zj^m+LYj=vL7=AXu zLQzot6Bt-2ML`c-9~BZ}1(iklh{D^$+|s;P+wSS}ot?QYgYM2wv%906FMRg5vvcO0 z-}%jX&6zbOV>(Q4ufgWbvWdn_Gsfg{{&A-e(`gms zK3xP{1Y87M1Y87M1Y86TvIt~C{)Uf3r&D#~?pZvZB*1&Rj z2h>-CG=KB`ZfNJ2c?{)i>{|_LOM!1u?GCQ#2lb(L>NpC&3D$%9cU1qCPWRzcxD8H% zc1YvlT=)P6p%MRsw5qqp=y`B7v|1-p-~;d@1U|*FYKQ9MXF{u`zBmsfF(eE1H)VJED9tzexT|MVDQipw9dqcaN_sboNy%sjWFf`)7(yj!J z5AD_|IPf@V-fg4~N`D@tL0Y?A4DW*08&T`=2x-%#V{zto_zI%5sV_*EgS4p|hZe$@ zp!uuT{dk`9SxB2SXQ*%70ky_kwKuH+uK~@KX_THJKZaWOVHf9BSJgX>;th;>3$(5a z*F(M+pdITYijAxjNck{q1NCp!MbCM2px*NxNm3y_Q%?rXzt=+!(%NQl&B;Z;MZiVC zMZiVCMZiU%^%1b1ShP0Z?zOCp%l&rHvst@c)@JNR&ldAfd$weu<)ibHk-e4T*y13T{&-wGo*DIj16^l4hJ`ciApm&~Y;eMC~wf0%=;r!1?hID^5KGbU$8dI7jwezX440gl*?!g9rKN*@S zwa?37Is6XQeyA-jg$WRMZ06EP(inIZs1H{ApgyC%qp_qFx`0AH0@Yo9=>IYcq1HUY z=|nZ2-1KhkNl<@Dx*w9IlZ$|hz`+^;?LySkF1ynX*)6u5eTl7#E^CWyp=|BY7Kye; z`t2~-0u=N7NP;qZEyGy53Tmo{z-e0|2P69w|KCwcbC;e$4kWGlHI8e28-7mH{5uoQ zf&)qa-_bgD1-uM%;0(~(K>y#o6QW4}5zzYfd(gAN*RTiPgYzNE9hColFaXu_wDNlb z)GX>rYYENgTIcSAARo=S3m~c#*ZHssg0=OBd_Nuo$o_=0-fcifqe{kVz-0;rU}yi0|DAD0{{R3 literal 0 HcmV?d00001 diff --git a/public/favicons/kusama/mstile-150x150.png b/public/favicons/kusama/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..e93f838d5cbfb434a2d9c92dea4733dc548940ba GIT binary patch literal 1952 zcmbtVdpOezAOA5?E<>&%&RmNrynFwA|9GG0eV+3?-|zEzKA-RNc|QMrzWHZ;+~uS-qyYes z^Yn1_2LLhffA*k+NEvU3T8Kd6veOwS0ANyNzK0(Wab2v3{}}*CJq`ewKLfyyNXlFW z01^ZM_?G|xNd*A4_^M7nJJCTr!rR>y*w3Sk^op1i(en&SYDw(SA#+IayS7*WkVJX9 zIt3)p{5=~NTXy*g#4~vEe&~(kQ2#h8#4C$|!JK_iIR15$ z+fcr=y4!Unm6N`=KY`(pFk7VA2E|eX&wn%)_Q8Dd82KAfH+K+<_Q4takRdE0Velzh z)%xW;_3$jyx^0lmbkHXSdKI(j@2_qq_~<-Gk1o>8&iN~EOJpEUs$Z^JFj0NT@MZr# zkfXIQe=Wft{8zaiV*%;|>)NGJ437&PriC=SzEH!o$W10WuOoM7 zvaG45JsTdYNw^L}s5f(mwTxQfMdBMNndROGC5g;mDrjawD8Gf6Q)@eq>YJB{8+g9| zG()Yz!|P4rVb9=apKfwi}Xm8x@W}$yW11 z3hmzMR8)0J1xf_1&6YB7U7z(P=~n0L481THrE*h9VlYM@^N>?%8z z77JRc3Z_A#U?54>o3zb7ZHaS{dl6g)0HbI5voxwL($~NRIikQ0JsCkD^ z$msNIWKzL-6V=s?Dx>Bc(%OS>J&lKpxNJee0T7r#8GGc6OQXIcbY+hvS7@$DGWq=h zf~D8elC|V&*hx~k=@HbO#kO(e?bCZhjUnBX=K9g*3gsqV5B2D-6K+iF*1COKm#PEV zs8yf#jedIu%o?Do48F>y9;qh7q2|3V?xA*yy_~Og0zZ7X^s|k_MhTi;J8xNTrigFDLKOPW*s7(7PDLc##SI(PuVT8vgWKRp&OEPA(e@8=vrTOCq9@Hr7NxB|FcZV_JhzD;4`uj;iGO`fc0@g|nQ$6Q#b% z`JDfC(mqDkI37+Jht3DP=tw2sf1C(v&E2bvK0Y==WZ<;PLgTwq_FMVhUmD$1G7?G# zYqtHls)@Pj=&Mf$i=*=2BwK#!-tKRiQ{M~?iI{q;Jq*G`s-%9`fCkQ>%)9Su^p5Fu4lRm+NUettXbblwWB!W83hg&N- zS=eWZar`;BIIOb0xLIw)b!OYJiCVEj&(^zQ16kGhQ|QPSz)uv=7s|36ra$^p^zDW( zj5Lj8s~?$O(^~;nPqg}PJto)uB1aV8)xz64gxkt9Z>K%myRhA8xjom%7evlg@7Swj z0*NVk(M(vR6v5zPQ3i`frFPp}JPy_Q#JfADu9#8$7`N}SdwO|anq&uq6n1mEh4-ZBSPXZL`+On15r}?_ArXZp;=+k| zBsKvr0ssbqS(-zj<`A0zhz-&Tj + + + + + + diff --git a/public/favicons/kusama/site.webmanifest b/public/favicons/kusama/site.webmanifest new file mode 100644 index 0000000000..45a14840bb --- /dev/null +++ b/public/favicons/kusama/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Kusama", + "short_name": "Kusama", + "icons": [ + { + "src": "/favicons/kusama/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicons/kusama/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/public/favicons/polkadot/android-chrome-192x192.png b/public/favicons/polkadot/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..c6b3cb673acb5bae2f2a9a481a2374edef95beb1 GIT binary patch literal 7736 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 LYJuwHZQlPsf~oWU literal 0 HcmV?d00001 diff --git a/public/favicons/polkadot/android-chrome-512x512.png b/public/favicons/polkadot/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..6aa07a0158592d4e88d0936ca57d97f505524b3a GIT binary patch literal 24341 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 z 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 literal 0 HcmV?d00001 diff --git a/public/favicons/polkadot/apple-touch-icon.png b/public/favicons/polkadot/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..af04946776a82d65d19d96d94f1ccec667e2071b GIT binary patch literal 7238 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 literal 0 HcmV?d00001 diff --git a/public/favicons/polkadot/browserconfig.xml b/public/favicons/polkadot/browserconfig.xml new file mode 100644 index 0000000000..e20f0764df --- /dev/null +++ b/public/favicons/polkadot/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #d33079 + + + diff --git a/public/favicons/polkadot/favicon-16x16.png b/public/favicons/polkadot/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..6eaf58b637dd1d300a51a241d655fa9161125c0b GIT binary patch literal 680 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>zS|?@4%%U6N zs`ym(2T%`VlDE5y=Be$iDnJfedff|6tghKf0lKk;xBhG}S=@;`mX^Jx$Rvog0{ zGPkg@u=ivUW?==F29v`n%*vZX6i(l`a^lFDBQi(WPd9ih@X}*=B`#R<$;oso&!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081EXeuPlzi}!83-c#|&jp z8LFN#)I4OUc+ZgkjG_7&L(OxBDxlOT0*1i<|NoP7^rr&@E3qWVFBnKe0e|Aomw#SK zE&txZx{-z3ue(Af^GAm7qQ&eSMfd&|ep@K|Or1$3dCIB{?f>2zn>|*3VcNynDR}&t z+Jhrvs`p*CgfoU&?!IDfA$;|@>xZ_K)$5m(b1h5Q5$Vu<6=(xvlDE4{lJ-tnV01CC zmw5WRvOi;$7S)qAyvw;CC_Ty3#WBRCF^C1VGz0aqqb_6lrbe2qAt)QSaTWi6B zChlMV|F`U2?X=PL%<|oLcB|-DDn2>RF!x@9$;@v&Rk98tY%_F!D{Zdrj$~$xI65DV~{ zD50ClXu>Ge8)j^C8m|5+dA_x&&1(8~ghFx3**h?11Vl2ohYqEsNo zU}Ruqq-$WRYh)f`WN2k#W@TilZD3$!V35oxW{IL9H$NpatrE8ep_p~8Kn>wlArU1( ziRB6fMfqu&IjIUIl?AB^nFS@u3=9=>9)IHDC=AokIOTu(jOWuJ24-b$y<~1-Wnu5h zBFw@HE)6D!Q<#-EhbWxBaplC3Ge=~Ou%B-5Sm33{@Jd{;RG<|Mp00i_>zopr E0Pw5bF#rGn literal 0 HcmV?d00001 diff --git a/public/favicons/polkadot/favicon.ico b/public/favicons/polkadot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..83ccfcbd1952f40d0fc743866f78153ea2541371 GIT binary patch literal 15086 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 literal 0 HcmV?d00001 diff --git a/public/favicons/polkadot/mstile-150x150.png b/public/favicons/polkadot/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..44839aea96774b32fe1b44ad2aa9ee81bdeaca45 GIT binary patch literal 3840 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 literal 0 HcmV?d00001 diff --git a/public/favicons/polkadot/safari-pinned-tab.svg b/public/favicons/polkadot/safari-pinned-tab.svg new file mode 100644 index 0000000000..8ef3afcf55 --- /dev/null +++ b/public/favicons/polkadot/safari-pinned-tab.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + diff --git a/public/favicons/polkadot/site.webmanifest b/public/favicons/polkadot/site.webmanifest new file mode 100644 index 0000000000..ef09a5b848 --- /dev/null +++ b/public/favicons/polkadot/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/favicons/polkadot/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicons/polkadot/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/public/favicons/westend/android-chrome-192x192.png b/public/favicons/westend/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..ff25849158d26ad5639194dc4bbf6359d9ac60f1 GIT binary patch literal 20669 zcmV)|KzzT6P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 UH}x-MGh8q}87vK}^j+ir1J3A0#{d8T literal 0 HcmV?d00001 diff --git a/public/favicons/westend/android-chrome-512x512.png b/public/favicons/westend/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..060c3490609b125fdc5521ddfce0bb82f1ad0b4c GIT binary patch literal 58904 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�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 literal 0 HcmV?d00001 diff --git a/public/favicons/westend/apple-touch-icon.png b/public/favicons/westend/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4fdcc4902137cd95c1a7a3da2d2738720f19283b GIT binary patch literal 13627 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 + + + + + #e86c5b + + + diff --git a/public/favicons/westend/favicon-16x16.png b/public/favicons/westend/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..d9421bac17e8df92cef40207b3a94a939bb0b406 GIT binary patch literal 1487 zcmZ`&dtB316u*Rq4p9tKQ_~r0q=Ewk6&NrWu<_W&Hs;tEV=vxggZ;L#CrFk`l&O(s zXlkJe`AW0WEDg#BSmq<21^fNB{eF8wC|QQkZ=e3^56|aw?z!jO^F8ODbI;wvV&sGo z<`Dn@VRRamjmz)=PYK4?sOT9STuiE<6i@&-u{Cr|G8xy2G8(%80OK+Mnl}RQ1UEHf z0BA`7xXS=Y+6BNIb;p^)6ab#P$jawt;SS!TsV)s4;Xp7?=c33Q;KY4an~iBi7iu3* zfc^=cW46(qBt=%5{Kjk>&FnK~BV}qYsIgJaUR}XxD`^O9ax65u)kPRbhOnW57LD7; z#p;EqSmq?xIf`rSf;xoPfUvdhYOYIBj0ww7TBTi}LW&w-mKH8(bkjwyYza&=zzIfA zuE0$bA*)Qta`W(pWWRw5$C?N8n}?goLlz3W#$uy14O_{6J=Ktv2eiPMapxd%dwhW6`pXbpskrV@* zYVzn=E>Q_47NcB+y;y}5YLHwVlGcbZWmruKDwjHQYVE>0n5PX?B&=~2i?C8Lnpf#e zu5)mdfm*m4n4xn~B`#hm#*w0#)y@Q!gRHjG>g|~jlBP#i8+_Vax0L4+h%k{DWtThC zln$oS&Q~Fv2Dm^6lMQgX$!DN>8dxrY7!!)oB88KpatM?NA42GQm>RgMPm||SGltvS zMmi2+CH0REo&V#=DGb^;dZFK4yVFL~!$~HeBHvraar1dYo7=`b{$Xq9Q*7iP1Rd@; zGUgeFrwKfV%{SHgA+*LziUV<6=<+v-kE;VPQbI+3ph__{j zfBS(aHoIr@UYG#|-Z#jyI7G~+-JcEs5a`8ndU4hCa zPIi@pr9^mj_?);Z_-IxIDv z2nzF^U)73Z=F_PZ?%W$e;Xy;=ff&!dyl@e5@hg!_f+yiBnEd?GWv@n+SG=}-#mbN= zcvwPoOl4JdO|7y{wTcuv4TQzUsp}gwkXEN}G{mo-4iXZLCbPxb)V!hP^^I?=Sqs)B zy}4=gme#l0lyASY_1)wYKu+DZeaCz4@9*68!R`+`($WEC&)$#r?LYAGC!GfmeR{Yn z17vo8cI4=><0n2pdFqSPUw%aeS=nEoIs47IZ@)Wt{=)Y?y*YqJ|KZ1rmo8uF>%aQb z&)0JEKz@Pom+Lof-um_SoxAt$Gnilot8mkU-+mt$w83}pu{q&j<_p{+#O`pSn9I#u zA2ADn7iaT}1wxOvL{uuCGdED~OY_PkQrTj8q+<%}c%gy$gCn`kHC`a$|sd&P-?|BcUBn0q9u_>d}m{mVW_=q@@A? literal 0 HcmV?d00001 diff --git a/public/favicons/westend/favicon-32x32.png b/public/favicons/westend/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..85e595b0dc556be1bc10bae98d9eef7dcb454681 GIT binary patch literal 2494 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

}zcpH9h}ubh(SpP!IA>`xu>Lw%V`Y;7L-?srbe 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 f-#d&|eS_#>NIBcch<SL&$> literal 0 HcmV?d00001 diff --git a/public/favicons/westend/mstile-150x150.png b/public/favicons/westend/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..d37b459b5750993b1defbc9f3ca76af7bbacc39f GIT binary patch literal 2358 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 literal 0 HcmV?d00001 diff --git a/public/favicons/westend/safari-pinned-tab.svg b/public/favicons/westend/safari-pinned-tab.svg new file mode 100644 index 0000000000..62f23b2eff --- /dev/null +++ b/public/favicons/westend/safari-pinned-tab.svg @@ -0,0 +1,235 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/public/favicons/westend/site.webmanifest b/public/favicons/westend/site.webmanifest new file mode 100644 index 0000000000..7ad920e785 --- /dev/null +++ b/public/favicons/westend/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Kusama", + "short_name": "Kusama", + "icons": [ + { + "src": "/favicons/westend/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/favicons/westend/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/public/fonts/Inter.ttf b/public/fonts/Inter.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1cb674b74f4e304ea59b433959143b3896412556 GIT binary patch literal 805360 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

&oGMGREMN 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� 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Œ`K=Tfr@=3#v^^f88U+^c2? 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 PBu9nJqZqRa|HXd-w3BNw literal 0 HcmV?d00001 diff --git a/public/fonts/Unbounded.ttf b/public/fonts/Unbounded.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f9b6c49f291f5515a8b6d38e2ffb65b88dc3a978 GIT binary patch literal 601956 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 z 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 ^ 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 AB>(^b literal 0 HcmV?d00001 diff --git a/public/img/og-image.png b/public/img/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..10bf7cf76678ba9db64257d732f7fddbee4356e0 GIT binary patch literal 293272 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

M;v>3+9n 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�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 z zGQe#n(guz%KISK;Lwa7bnA{E;^@@Nw4JOE8|A&E{-dpL776+yHaNLEh-&?^s?hy-? zbK8FJ=sA4J4)G*$IBaix+Dx666uRE0Va#UZ7$+*{2Usco$ikcaVv#|TKPSv+`Iu$p zw)L>5iu`tElVvGd$cpgV8xt8^#o}2T1Tc}5N4RNIE30$lCQwA!VcHDO;ccin#F)p` zYJ`4?8&T-!rZw{Gl&p>>08BoLmOx9w4+3IKThRHicmlgPrgPa^YHVW(afpBEWTB+Q*c{dV~$d^8o7YO zDL+UK0u7I`OE*~U+}>UC*s~NDro;Fszo~;6kLC#1k7XIuH0?R+>QkS_f@y>{7c8#o z3_tlsuhwsHWET z&=0ldWCdEa_mCVrVa^yeO&A_ePL=B?1+5_rP{O5%d!QlfFT68$k<#+6V$et`P%z4u zSbHpoHf)j(2^sinV)#x&*5VSK^jO9r1X@>m0J6MV|5@iax9RH93yjvPm91_)w3&$z z!gJH>wZCM5*Tr$-$~tuW*4u$puGyAy;SnP79;Srd5Pk3G_5A~8Z&7w+oktsdAOJb0 zxSo$ov5uXIkeQMuWsBWi@AHCe+NMm@C+gQYY^4LIa_PNtOkKRzahsQ$&%o$h${X9o z;^{}ds2OF34kKUD*yZ1-5)XcTs4jm(oq5pwG_U;9M_v!WIh<@Gujp^wRDjXEBTCRu zGb=L@EwclVl;agz?xX+m`Evs`8-;lOe#-a?J^Xc69_veJVV)EysDM+0$d>?b%khuM zM2ot>{_c>-thec>zWn1`RtGaYG<;`I3i+duIfFUEj2>NOuu;1xZRhd`JQt@^RrV^!=s)NwK~I&FS>!ns3Os<+jB6j+V0R$l*q$**d5;WKUOupNxVink5W0NHvzPA#|9Escz;PJoFM^>+-&8lp@C#4Ev* z6G;6ABRr~iurxG}=j1@fN1-*d{Df)YF*F3hM@ZzRCtW7M^FS-YuSD1V2M}+qLfYmc zM+R@mAPd|+>8|ZFJTh zjKD7hZ1=-)o{WcVET)#>#l67`_ean7{Z8MlBxVY}LcUd_CC~)-H@;eN^li1>_Yv~u zMocLVE|A0F%l2il%t4U}{^S7=VG@4V1a81N#PuQd-Hr*n$AuEMve02sC@ZXqrjc5c zz3-T4AHF1~(SkqyQU+yG#;P;g(%4k-AID7uwg{9Mr&r}K@)_2U?J(uYX4^kkt!`HR z*i4z3fvgqnqzPVaj5@0tsa^Kjf@faY_A#ktR;l9c<$5OlTned{e&^=qFNIDKO4wrs zC8%j04nCT*!5Sglq9F7rhN-y*2niMcRjVV|q(BXMpnxv<5#85I0QQUEVe=T5Q3M*Z zoLE+k!cb*U^|o|Vx|csWLYRp-yHopz-|CwM>ST{q6N0~tIitO1Md)(F+-vHx{bPvX z;oqyP*xoPQbsx=6S>BViz?P%9 zlRsDKajAuok#K2h!x0pE+~-qqFq~g~U&?+Q*qrr%h zV08&&?qIVN`uQ5dv$(6_(kh@)snq@g(@bsBQ6U$rCPvkeEJR}!7(3T1vsma4Ac`}2D2hgj5Osp9Tb9e^|Q*wF%Y~O@;zHB3n+62)$jcRdG z$j7UdP`ru3Xv%IOM^Qrf31QsTmKL%Z*RzdBeHcexwzL}wKK+FZ@8Ig@KqW<>7ao$` z!o6@V8-zG5M{J$mIqnKFJAjFO&UGc003vY{z@hFdx1@Z&tWZIn-f?(aq2}0PnS)#9boNTW(GriQP1S4i4 zF!6pMa(v?}w1`NE{ll?Ydyj|?eU?d(jr!hJ;GK`le6aZdg3_4`q<(JbSHScZ=L7+KelwIXzL}>@7Xu zais1lEZyk7)NZzjEbHlt4~D7dgO*;)S@q1ORu90ZAoO&L{Li}p%*W|VANML+-6lca z)C2!L!MjA{-pw|bm;M9KGB};Sq`#5zkrNBJj|6g)VZUszy`c(qEoKp0@& zC_^{dBG2n1pWKdF8-34yY+DfD@j0}wVpSfGyk?D!S=5JsYW7s&la-lUDUhS<)BlAR znW!ZaC_J~s7g7vTr zp&^A_lQ19UdlCybkcS&_H+LL(=!%c)myN^m?N|)i62%IA_O^+$K}D1l1d^>f)Z`0xn~Na02U-4Okqs5_Nz|QiC2pO5@p#^{<-Sr5t{HdX zlo>(f59fcM#kU8P(v%9MNdBcL@f>V}6@de#-f?>Tun5X<&HtaG^_M7Du{=?}lxouY z8fw6OY#Z&xejaF)+%3(j390>Bc+QuS66|{UZ{i=dL@Tw zY`eTxu~WWfj2T!(KgIxgnv#{>5F&?wT`D0XeUj3jeL92&woGXwymz#Wz@JgiGj8wg zPq|@hO*t}O!aRmhYGP|wJ!f*?kWMkPC$cD~k`suj%$GTr!a)g9L=iBCCA(I?!#FA+_Z<@Wkj81p;v$g=2Nak z{T&>(5=S}!g8Qv)%nLN>Vy<#V(Vh0O?^BMiC1I?E_V*S8I@5CCs@=*eYB4(h)ooqz z*v3q4XF=yDhG+!A6x;pCicWc@FJ~_DFw$_SsioMT4<3h3MqSN|5kD`X>(T|iJI9D( zrQxA7nMMJbi+r7@Zdd*-36C7b5HLePgeEM@zzqTRc#gY&%B&AN4k|m1`^Tn`>W9n= z&?~Kw$q%b2^r@b~YR1|q7ZMWkk6QmSu!UUGQMCP!(t^zYc;EtD8-DW+h~->cTc%#b z6^s>mx&obM-WK=ns|^-R@;U2Ym&U;NB;;`(JkUNZA2@!+E7KxXc{G&;8So%QbZYsW zn#^Ih-l#HnFHLg3f3B9-jzie`&Ek30_V{nMT(upmyi8gMrfpZO!z!D8KIr*erjBvv z!u5P!ZaH3e#rKxBw3Ko=ESCU@55cL&j1kY>&vyr}7r8T)X!8hcJ&ZSBbtkHkrI5j~ z%GX+ojEG`@$i!hnN!P#gT~+u$S#stq$1aJ#`@L0H>uJ5#j5&vk9^ouQ%ItGjB|GC9j~Ts(bsROl764FEf30GwJRG*a??9X*i9R0f7HU?pZEFAYI6A5*(ET^++MUAq!Yl$7 zsbVxun0NGdb5X)=Y0xStPU$v;YdEjt{G!wv?u`BvQ@->Id0COhZWiX06VF+8BU?CQ zBNn7AuQfd_1<+k$5WJ#dm4tc}29>3yIz?jA=tfHVEEZ6r`!(~5qnd2wG5P_hVj6`~ zc|s84=f+>7PGcbbUcV2VCc!mXMK9oK&HO{G1KmukjaKlso2{_=ewR~*8flMCO`f-;8Gj~O;IXD6fMz73 zUp2b9lUiy%9W~>T^qp>&#QeNuNtmYu>o5?@9NeN|&SPNnL$7?rtK`%wuuh8_A}=+(ccKjxPgF zK9Wts+Pe$OJ96vza!IV9oT{P295ttq#}^#_iBMD>TkYmC%v1 zso!YAu%EY5V7B@9T?hX;CkzsRVw?iPC-GZ!Zs6W2$@64nfK*ENnS#e>8xIOhXVU`R z6_Xzkkn*B(#Bc~AX7z@IJ16HM1>)&Jdz1)fcFw;vU~ zCNmLl!R!5Tf76)3bDodCKxdu(gGU4q1jGY6l_px&=&&Q=dAY=;Bg-~#TiRxs8o>oVmJ_x;Lh9kpKgJyCNEjdurRh5VmeA`9r_3?0usL_kV+uf~ zNf|}9a$Ixbk%Aps_Af1Vm4qj(URP!#b5bVjW<@b6>$(}9@d&D#}+o54acOE1Ci~VEM{!#r(3XUh#0ogq)ppQ&=Viz#wyE*d_bBQ z<_(sK_4P5{Qzq&c20RhwUENz2+xkMMk#uPJ#wQaAug{R;q6U`wwwmy-s^1vNcr(D2 z@FxV;hf(L|4g1I;Kea*-n#yfKs#m zF5P_?_h5tsAsH7G27xS$A=wXM#UsZh7J|}#ki}ATfN`~39Szu1+tDyVud{Xr!&C@P zOavc$hXEE$57>i0%$LlEW8r;hhj=L2JsNs4)4BdK2U4BYc3h``4r$3;171p(U&zew z97r395)xpMysj>anw;pEF_gtg#N}iUI5vKfsa?ylVU*bNSJc#^ruGtOHegn=_P!V^ zB_@Qu>i{#GGuxN{ms9H7RV%1_5sY~(7|1BfmbFx$q*>02w>LL4R*C27pQf(UiQOl z^ic`kg8nNPo4-Zv0{(y;AP5m~H>3o-7E_6~SWadLYSuY2;{Sa(DN-tJVPMn(VDpl6 zxLi7XE#y7^@2K5`DD|o-Rm+SRmUYxoPv!cFzG-!TAe^XR!LDh@4u6Iig}$vg>5W9K zR9==zD9-H!s%zY((_2lp`J)Bwg}m23T$~^g;Km5GUGw}$G?AV&%hyJLuZTiUCrb>g zo1X#+y!szVQxID$<;1-GlrbI$dLnSd%#w1VLeNhJlMZE1ed!$$9L_drEss*{d#_Kr zjiF9O@jcRM6rS9yY22YW)}kquf(u9Nx@BTtNx9sB=yo_Lbj(ks<(_Yp+(v}vskh`> zSmx3IZ?Ij(Fe6VY(BDbjJ6w#H$O=)u*iXb5lP)e>^ZCC39|Wm9dgGJqmjk94z^-cM z`XcCiwAL?PFs*maSb8!CECVX7p-{ehkX1&>$JYEg0~fQ#q5{Pl2mA5EH=4-yN2`6m zsFvSzcd1r5wZrJ8EaTLMCOlTbH1PaxvTWv)fk0?F?$@V*KOqnvYGc?J zd=-QqNB`oqW_CP4^m#k7JISULp*YH$7jRmlp;~rRJ=?o$*ltse4&2PAG4K)8be|LVToB95)f#AI>$+Tr@ zI-3VPPTZRWH0$f@x(Dg|V`fJ#XaSghoX-2aVEA{_y08n~c{_@q&ry9c4kIKe2_+Pt zzd);^7=(z{z}bo74;M&qU9uWD)L4pja!dQE+iu13lm6J_cZ_j<@!sx$LdW4>9jwye z7TVmqr71Z%(YwETM-_y6KTM*&>3JeS=}OcVW)_v>Qzk~8acB0kA8=_}k_GijGfI2@ z_BqJDtSeJ|x|@*H8aRP#bn%o*sM-p@36`=zy$#2OfSW@gQGkM<*2Eu}j=Dio zHmKYU9O9x2+@!r5vIbAxM|bv}TV+qnQ(?RrEB9;*rKw7;6V_};-5K7p3^g1`3~Q*1 z-{!W!zUW~4Ws&^VKzIwO0yh}JE`DUoOou!xOAL5JyQvTYwJ#I?ZuOc4N@T+bVg}#~ zIZENtOSsYKXyzJ_60PuQbg+j!wH{eLk0BhT0r&_UtlzHSGUbqf3;U|Xhy8a*B8@%I z(BPG&SV4bZ1IT`|H|(kS2eE0T{ma%TKRUdb;wep>PWl{Ay&qDP_ko5Nssahm8=5&d zB+(uaH5)8F;1gHm+TY_)z}+k2DT75$5+rX;mpcb=eG38P&pLU2+*<4b`KG~-Og%_B zvp^KuZg#2#CCI31>s33Qs?=c4X0~MuMfd&mFVC9-s{Eh0_}HP^v zFPKJ+YT)|1l?n8lb5TCpu^)C}53q#Q|GpETP&n~Q@Ol`PD1awuBKht=CK=6X?~B*{ zaac=X_+N6J`&6{!{S;R(Bvw_h;wgGwD-NOe$(Xu*e4MWLNkQ4}$Ulkk;nR7Z?h z_>v$ta?R8JhMjc9$@&ny6lf)SqU`yZj54n?!|bqx!K{kVZC)b;DQk)Oum-WeJ~gbL zPANPakJ{;k_v8d<;B=f;j8W0@fr_Fas^a%0f;&CWZzk z^@KB_K3PUZA*&nc?7~R5)l&l({$+8!AD6iHfJ8{S_(r$=jObLSv%hk{k?2+D{U|{b z_;|s#e0sxodfRZx=T5mhVl#KXd+GIzemot_mX-_VXM*e4KZ$dy@EfyNyYr>^;Y7a7 z)K9Zo=NS!{i@cwuUE8u|>Uy9Bs+m~JZLlcM8Ude>+=tB_dqY?&18N)(^^a^O!|L3? z0;Prhl;i9M{;f53`wxhh+V98fjJyhPXr$iDmhS=+5W0z$#O3e2&ov9fwLfKjzzSiw z+Ri)Pf1PcqJ7?}!Z43A@AQB07}`EJd|f0MJGZseVkBGC zL4}@XMBccz#D29*=9Zc4i}tSwMoS$Uv7eIlCAO8mE4v!~-F$$`fWP@DH>*U)niBYC98V*# zh-$we-~vB`*-8sQnS}rzu`O?mL~dee+rI>IdDG;eF~H}Zp9;@a6)71*F;lYa2sa)h zV_Wl8k8WsaNsSY&CG!?(+m4>7U$cA1JTw)f+hM$kW9Y@kytPzN9&q}h`{YqwZ8_GU zff2Vk;Q!x?sDZXBWde(p=2H3@%5M*jQm5DDDOMgvkg95(=7FwfK?Go8Pq2#j8vg`S zt4C8VEj!!4ruz+m(Yv1a@nXBt3=ZTIi*W?H4bz-9xQX*S=s)}7t%=+(4=dbcL9kmt zzT}s5`5^bY*ciz6!JhASq64NUfybLvv;AQaqt`hwQ(zkytiXfo29ivF>?(m8<|Kn^ zTIBxWY1jc!JVclxF1}y+A#D4)m}taZto7Wj78L@MH@w`}A1wjCH z5!q_1Qc*EzC!962g8nNC+Iid$#$6)|ExN?+`2pB15MZ_JgO^=?eIxMr00ov>e$LY9 zxz2exej;l0#{0kEoy;Z5VXA-aAqPOBC-K}K>Dc2x;z^$;&6f^pi+FnD20sB$I}YK&A9Ry z?f`{sFBPYC@bV$9{jOsS+KFsbi;o_5xeA&_nyPJBW3mk7I)AhJ8FL;s2D1hmYcP^R zOX|S70WPwx;th7P6W5fsU%s0Bkn(4`OjfaT*&I5he5e3;#ICRl1f?<2>VUd*j2Krt z*lwWj!V57`esvJV_-XB?Wf+Xp?C^U`VLqs52ws;yV*ycSAt2Zzw=B`jgVR}E_hcZFTM0b9rtgjVT&hVnO&Czg&7aQLbl9D{~GNYR%3P_U=xZl4>D3B=Ff^WH;F$mkiokP zXMRhvh4v)q->??+hT%L}2CoTUjgx-D%e!J)yv^8&L z0bfm6qGhHz@iRGmRG+$Dhb2}S98ffYW;xw04L*O@8l@;&`j`T|pg#@`zF>hBhW7M6 z#k2lEJJ5d^z1lrrO~v-%3ns)6?X=H|D%GU!bCRm#i9U8M{(h6ic0Vm$ z_rC%+itt$O?(by;8U`Q#S?vBES?R5;wGSmEXlvYSYT4EKe%pU|1t;o60*1!+>IkTO z_hVy$?zB%qP>le@+MX}>_Lq~CaHxo0{7b&NlT2n`g})U;#&Y7$#1iyY6s^Vd2Vu5$ zr|Yz#Mzjl|NtFwW$qPJ9FvEkMAwy(=<_LpD+Dk=0tf9-x=SxI)H2oluibSv_=at99 z3Ua%Q!$A`KJ%TAtLPfzzQMNq4tU0CWxa2+AB(+uM5Wcai)Jrjc zNtfQ0xl_b)@Dph#2AA;slLVE7pLL1`#r!zOkWlZNUOPBkbQH5`S=vU>p{hC36NAIi zGaRsyE*r*VcslsY>E6iG68d82-V{vyLMDh*{zd-aNtaW=$|Y&~R7~cx2qH3uk!mB1 z18O)v58^!{VnIjL&XX+t@?FF;W>#YEFGgU9nIvYBGC0Z0CkhsPDi1YUhD%d2f^ zXo&JJcl$CHAF!}|MJKh@r_SAXV+@acQ*EssN2ar{0-Zbsot)(RXd11NZGE=PtT4SHkHM#pw?tVBK zrGhs8)VcF|Tw2+28@Y0qD(ynN2PR06d_T{D;hlIL;q9xmCO&Mwf-RpXir&lm({e(c z-4YwUHp7Llq(2PTmg9GjoySZwJ@h#=sWm0DE6+=&`F!flFtu6TrHm@OBB*1gS(~y! zD*1+|1|c(zqiThGF5dcy0;*mNi+poqjm(%QTuS>=Im~v!AJ%_M1=v_9bC2}O0RGAW z`1y4s+IVjDzGQCtiHUk->m1vIztzzA0`T!|oE}7!KO|PeayDn^pLT%1@KiSTc zDV#zVv$>Ox251j zP<;v)Pu5svh`FvvyFwfZOY9L7w5eHB)dp?SxTD6)ZvQuMhT`R&!v7#pyYW%3Ppx#K z$vu~51VC&<$bS@n6J2;D_!W;hiVuV|!RyD$^j6l-o@7Co49kZk?se69Je$noZ^NJu z$I3CWGE3++(55r>#z!S_V@wSj@ROu5vY$vd-*3M_XkHNsfrg`f^*uO;+49HbzaQ+Q zdgjctSlxBwJwfx<^giEamz}&54<{v4c>+G}ID<5IH13!Vj`k}ONIcgvl2~2;`0KtS z=iq&02VRJcG%^ukK6>eTHTnT}fsW_ZJwd>`*K-C$eRzZv8j6L$$32%nh|P*S)X~Lu z7c$VRO4BTNCS8)4NZASaLVN*+f>v&0LyJU~1N}cAx%U28EQ^*c?g&9)=6zgUe-*m! z0Er~uFXLSQB-0Y#C(FHt8A0f+{4D!+P~bfP4fxSi0y^)fJp$mqFRyz*pSSOoy;H7% zsoe83gYQSFze>${+iyCn^(GpqTqbXg7@=387b>pr(7{ES%6_KTxG8AS3!}_lOEeYo z7fEjBa#r}RC<_Hk{Ow9VIp#`7S!rW9VCY0D6Wno&4xO9EZM1i-N}$v0Ovn+#d4^qJ zNx;-qQ@MWAB+|W(3$nt}Ak(fbokA11nXT))LE_xf&l^c-8|Q(u3^ZmU>?r#ta~FwA zjOugF@2nuJQ7VNTWw1a-nq9lO6E8=8Yk*(tl^afS;3q#$C7>SdHgQ(e8E!==1 zh^c!w;Cr#k1Tt5KmyM?R* zJUtgKvs1t62_7F|l?kyA`krI|b+2jlj@cf5HhL9#3w2QGtD~{v67f4 zd%iT}SX3v=bUau#m|syMYM{7J2QaL2Kd9a39QN9MvOiDUN5NrR_vy>DTEb@&u)@m6+cH1e7jS{*9Ny2ysZ!O@PI1BO(k+u)(qw)U_tUSR zG##Bn#I!xa)S`{`rUSUUT{c!`HngV@Mlb)##f076t;{Xqxa1nbxnr8&F~pXFEWfE{ zkhpB-2C9#FG-7sb(Kqz1fp?0b{*|t+3P7@mu|ev6@BA%ghbpm%-yI1yjj+)G!w5@ogsk`lDYSIZ}~Mk+V84qpHq- zf8nR*jtCKAgnQcjjlO+UisVFCvD9r*_}dIRK2-~fDlR^LIFO<*FG9aTQ`>FMA^(RJ zC(a!36{+uQB=7<3C&ISx`6N8Syj*XBzIyreyUC5#Ao!K1?>6=p1{^;AnQz<;ar?+z zYoMJ#V*Cpb&WqIZa(Inyc&UAoa=}tQ;5M-V59|4G*z?ZI|0@2e_tRtNQQ5oryrS}# zSXraG^Ku_k+O6+?XMzEn*OTrZ*X=jR5lk7?w$mz%VZ?2=5PGlB|{KoQ|KbBe-)z;ksZrw|bOhPe=E& zSGdl1zy{fEt9E7UHbKcTk+XS*dF#uqLe0yR#NR#EvGG6UDF9lUm2XX|Wp3;F#R!!B zGgXIhuk(!6Di(nFdmlt#TcN22WrG0gt%vrM2v5Ce+&;_YVfFCXmxFK~Pd*4XoEjGD z!DYEUl&Hk{UFOujvq`tB=#pxdt$8-Pk*c-r)kX_ec#@EEJUcT-hFZKd_E_EtdL%^7m5au5=|WQF+Bb`zq%)`e<7;L%#g=cP?^W zAmuPVpyc_p;#btclo`z6vl|N>)?clE8h8fqto zk9IE(3-Y~{pJT_${cb3fan}*Be|PBQ>E^J89-{$mpuQ_G<1$)z?yO!i4OM|ky6m8d zd+W2FHb$f5 zyG4Zo)sWJ_kv&pq{WbS`snaTO%Z{RIH9&{7`Q*uk`KY@k5K*x=VeqBxJzz*@>SyYA zX|oe{I588*<6!m59S9Bm<8ojD%Y>6S{_oVLn__rtx0-~J+AnOVJE^tK& zGD7ZnuPB(_rX10?ZU$IKCUFO%#L#v#!44(v`C5C)4% zeP^kM78G(plHY4u-^-m09+s$)%~SOoEEKvWg1|JU7p^QSzhY!iuWRQq^w%Z%%egND zQyTBZtilC5#VeV6#Y)KJJ0UUwoUo1@gv0R!>HDOnZh?34+KFCy(`>d$MQ)P2Xfb0k z2O`IX_>TbV^;@p#vz)*&Xr!mUg1!)D{`t^w@cQ1S*qIfSpP!0swL}vH67SS%(KAp% z{qb*1A*k{CLPhoG!2m<`0}^$mCVQ;x-NObB*7{UYm*IW1UI37EcAOW0<9@N3ihZ1g zj&e@>4}x4|&Xp#R`83U^vx^+yiQ@mXumSuN4mKjMt0M@ZT+E<@-+jc5g~6My)b#QW zBCdR3A@IR~HXQe<@h^&rq3gNlbz`6WIQBS<=g^NF@B;rbrsNN<({dp_(9+pJ=5`=x zB>Fy54y>m!oW9Vyv7fKe>)12+di8O9ceoC7A8j51zHfcz3G+e}t;UjuZc2o{5k95@ zKA32VD98iCf6iPcb432Yz-S(n%FG@Thp+M`1%m?reoKs!Y5dPhsH zL3N27*1)*Sj4V^^N$ADx1&|Nar=nyEI<1hWEwkA!WAG$l>Y`<;0_!wTnz}*P)QR#R zFA6%0=NNC%G=*}M2-2-;(})IJ^{+m&kEVl>@ztmpT0%<_WIGH)4k0>Oi7ts~dO{zX_BK?_X?tvdT__ml{XxPGKX{?DWp`4$y*Vo!Bur;|T zsP2AYGxbaz}&ML z#?H=qpP#`(NTiX?aS4?^MY2YK^Hkzm6VS~e?up+n_$|O-)Kpe8CGmjW-}`%DmjrfA z`xzSdWg`VF2rFisI-moE(C>hz_47%nIBuEC))QVbxk~JLhjKS;i{^GrX)Ajh*ONTi zto)p8MTZ9jX4|DNCXn6IV(pI#!cd@{<&e{mTHkJa3>F+)hg zZ?r7$*>chw1b%84?2e^H^{{pQfl$+C?gl69CcC2N@k7_|k$~YE>U{2G-e;$SmjmLe z>loGZerhUp6J;`?ljYNdZ#&u3SY*zMJJ9A~^R)^o0= zHBC(SS zJ+C(MQVRlAe?Fa3bpdGHS+odNJv`}hs%oX-D>KvlWcZ1Vu&rV1RjUR`aP-~^toN8Ff^*BWauLJ#_+I9G+}a@|sYScltK2|{0pKf5wH zLkP!BuAAVRX-}5|Ao2Vk>{S7;Za|CD@owRBHawQTr@iq@w(sD@D1f@T%jo*FY^78R zvU(@IEZ3WGn*s1u=l?pvrv^Rt4=?99CvjX7*A%br%vaxRw*?C1BC+}&6mj(yN)&nc zUk}C zpf;p3&FR*OxcYt-`WE8tMx5pi{m%M+un>s2lEP*%=*D2|e(?ncNNrn>k&*j|uDn|B zJa;VTe`x3gk-QEtw9w=oM5yK}_}1&?g6`_Tcr~ z=Rd4shE_PatkpTOBaMNed>ds z+~$^r~0EjY# zTd5eVTCeCM#J*D;LGyg+Q+UDUCiOa6Xc`c*mTr1&oRN+D41 zx8WF>_gV`0&NpWj98n={eK)(pYBX@-@^DMb^0o>FQBT0UP1IPUfq ztXown0l&=#c~{ClJ`3ipz^$?2;rJo5$JOsyhVj550ob}1{LQZoA5}w^qx12rgH=EY zTknu!2C(reB3Usv9mJ)!*K+>)`UTwgvU2AVSNb73PeJDkM@2Z@#?}w!w#3-518$(VYu@ZPB|JtOR6m~ocv^aNxPgR9#iQ)34`4y*Cob2S?lyxNfM5Uh7-iBnmtCgy{Z_$l(0hEMRapc2y z%D5ID0SNIirX9^yd-CqV*qkhKM9Sn!p~hI~ko9nZqQ!mxHMh}siX33P&UPlG$BOZ6 znh6#@nFJsF__LiYjq6c~PkXD)8UpID%4W7EYwEn`E!egjGjKomteTeyi4*=uF286r zI-J!p1dP}Sw>>(xIYeOGT2DlCNBL^&NzKQ4M9C@qwZj-Py{Kjs#>G|!lruOiiWFX= zqH~)i;ppY`L-KImql^sWupSS7xKIF>hm$B;>{Qd1a(^Li&irj`#7z6x6go4p9b{(Y zF56u^_F3609&+Za0%}2?yf2eBZQUf7t-ss zI#;hj6u)XHoe$dlRXP*=y=uGMFKo*h4ZrvVb+NcO+kl-r&Bd9shfjPbmq3{di#fmTz7n}*2^zO z7gbl8-{kb5+mi*Y?25a7M?I4QK-O^;XY66~5Jm9b6Sfw@gOzAbngHLSeozjA9FYC_>VQBfc#U^7$)L<*Fin zpyD85B`L+b{hSJi1Oeg!G@|I^$%Ld)4GynV7$vSXZdxLhV7QQ6$i%U!Xk;CVFd#pL zq7*L+ECl~7Fwn@4%Azn>v)(R40P6RQN+0eYxKK5p3l5O4-MKYw;Ayq&ArS@Wc?I+8 zvCo|J2nut@(G$Xl6G2_va>#_3WBR_9pWiS_M<6rrxP8P~C46X%qYn9w8FzDc31`G& z)s`lm!a}z6RRGW1)2l~G9bU@h)0+X%1m0Ag)#iB}JzkXUUo5yueWUhAgw4wL1i}%i zTQh^qd6VyAfV#-~^2cS9^_-BQB=Yo6V5p5A&V&-wa*ZqaB?E6JPrzAe%+>XK+b%An z?yF(zaNCx@t)m5zb=br^wD0}*1)W*4&GUF_!T#n59wLGx$(s0|)|SeYNV0_WsI%u0j8fy{jXTj)Wiv6MU zk|uBq*p1SI+9L`=f*-|Tq0`*tSw-$F6JqWJ1;JCfHJ*Q=CVZMZ22Ld8&- z&I;3Bm?p-HcArV*uU^gaDbPS4h->D7Qa)zWsP_piMbQ6x>!KfA!5bQF?in_`BaZzZr0$ zxf$>|m0YX3&`0S79Q+)0s^5XJa`DX{%3SM4wE^!q_`@nSeSa(Svi*XvASzbta2h*Y ztopJrK0I_ICGBP2w3{$zaqs7LCEo^mE@@TgyH#jlC{e{6#T!Rf}SFp zEYR-C=eSsy&}jXwG;bv`@FY=|it@Y#;>Jp@C>!zi6lyf+^U^jFMp(!?2i32`Iyr67J!&XJ}^>=txq(9%Eqe#UVUS$GPG%<%ln+XL#J^Z&^5q_j* z23x>V1qmRFe*-EEocD=33xf;<;AAvgOd{{uwW~UvuO6t?A%=#pV_9ecS_+jo?wp3x ze?Yq#B7=}Px2}uZP8AeU__hu`^gcsBz>8r9h65oqh*eya0%sK!3`@8*5?>~T_ zvhVV0%lndHJ`R;IO!Jn)(DS+L0My@pu6)jMsW-)hNF$qo6ASA+ZkD3?mp%O%l%%)=RKZpcByh57DB6ro!7rm9z*In5884t`l4n;-w_5&X$2 zm4}91IR%^@7AhM;8mqshmGlzo=6>JLlP0I6rgE*=3FLbpGjB<#<;BOp*7qan z-VY(|WT&lD@KCYKRnbpNeXsr=g!xVbGC1X zF1+oKg#p<+TuIQnPKVzOtbQ{WO!LuHG}HloK9gK8$*IV9%byaOqj-lg{HzthVr^ed z=#p|IqjGZo8Y2;nKRugT7}tAwt&h#gHx@QWWqb$SYkie3XaC`nRFy_+#tO^DF(ese zKAEPw5HYK-0=7brBR#&Tbd^_a>=C#=p&Ia0o%_ZEgG5+;u$9+1$SX;%WuzfXXgsKu ztB*Rz+U#XApl)EuGOA)gfHR=5NV8Wk#q{v?L*jmr3%YubA502r;u&ll3&|5MT#3@< zWt|ADzEv{TGVmu?1Wg{40N1o5`%*V(ZI82`(P`hZKQfJPQsJn4h(wC)x+Ulb#9|me zY~{Lt`GhH8R6CCn{@Q~|fI5`jfK2y{*U2;|G%WpEV$LqpIxWSo7&;1Y{h| zNHljg&8$lPIa3D5nxgS^;h1kle(lQlM^L6_pJi4mr@o2cK3*LYa;EV=cWHuFQQeE- zG_T7m2s@@(VoVgUE}70ZqA1vheZyznxH;JHT>LRf&cm?}6_=WRawVP}(=z^k$w{xA z?dONG=Ox~EJB)J0a@N1sSp1R-jt;VeBc8A7Fp8AOVrV=C1q4>`@&2$5Fx*?})3HON zH`T-+F|JX49y74`&GFD_+(Fjr(W-S_1i2*C5H&h){y-#gyWqO05Ka4^D0NyNV^HOh zUluf39|iG+l9R+)0y3@k095pHiqXGW-@GL`wWkvq@G%=<9lea)-!RKhrABzfgEJWa z$-$1y_|;2x(Q}z7DJ)Q@RPTas*3q~#q$G)CFeQn{l3xy@WM&7f`_6eWc2HX7sxV!- z&t4mx&`9R2^zmRuyMH!=64L~)MVQXmeT1<5w>11)giMwLT6z3c<>`U;(bD`~2tN?INDb5^6xLc|?!3qk5F zti$nKA2q=ok_S~+{v~*rlAH-^msL4T-8ekE`rh#gX_l3 zzQ7C-p~|gt7`6TWxReNMqJ5@=&VaxkM*m!*R!*)4_lk_KO=~slzHHdr+(@xFm^UEwCW_vSQ!C?4y4M5~z3$W|0KTQIHU4 zv<3RQu?TuVtNUQ;W2*xVL-?46N$n|jO=;!0o-BujR(GRk8p;-#q53V!#g=&T*IJD5dJK`=-+ZzrrD#q6i?U#RV4 z#fu>`P&4IquKx~6#KDrp0;o$apl)4^4#%JEzrB`SRIuGBo<=T!L5b_7JDtOs{Z6kL3=6jHD=8Eh_B z+CK3&?bnU3#=nz@7b&M!D+ZJkND&ofGK0*75qB{15h_S$~|939EjMBvf zp8Br-qCur$ObG>lQk>&~J8H%;7I0{iS(T@sM%)V}ePWAR_`y!G8x)HsiZwOOJSGlX zx_{QQa0E8*?IjL({^4`F;N|+RlY;W%@@95%5&{1Fy(oJ~h)x zhE<#t(xu&McB<6ziv@`OHJJ`-yXz|y+z#1(b@csF7~IpwJ1;gruUf<&D~FeOA|{Cw z`M4E`Ie@3KIGLTw__0FxxF~SC0akL8it{XIVno67*6K}F#_D^ekF&PbfsUq`L-xG) zgZY60-jw>|PsdC0N7Onwag)RQ6BSM4K&wfq)t-pOhc`xWpF3qkj3K26@FiHgYbaG* zM|3NyK^FxX^(uzDG8k*PZ>&aULK)A>Dw5pusx9M|3f`f$zTGMyOBZP%w##y@%FJBg zUnOA{a-$b9)8R+Y!Xn|G&dL)gprlP>|mj%^# z*j}dOmA(QaBFWnDD9_a-{7d86WnNLSt4#c;N18Y-TLU*EI-QdGyg~e} zaH!Xaji!eY({tP+m1qBXlWqZL6kkZ!weV-)>+`)bF}zQlRg(T~M2ZRoFKK}KeHW61 zhFZ%_8-a<4K%cIqYI1a@f2Z}>1!lS~3Cdp*A!JCVNvy26qoSm^Ht(N@o9!NAkiF1F zR_ZNLvwNmv5_R*v^cu5t5qYiC43@uEcqcNYFdw7^**))U+V=N(?b;bg=oHawd|}l#SQ)HY zJIu~!%_#GUW1f<&VM<1&$_gzgqqMpmV@A+y8`+l0C2(NS-R%`IVLk)G6QEA<2V>|q zEd&X5A?f}o^G^?|FOg1_Jssn7UFE2HiQTcs*0l{sI-sAV$wHMK}|u9mnT8S5A<_juEFl45@b zNJId?d^*q6$0tLoJYVVFq*F2Z9lSyRWm;Ka;<4+KY$%pp* zHBV~wKR@o!>jzi^Cce*MabD1j!8kkTzj0U>wN9U?OOjUKWaRtX6|l)cU%!$ zoslZeh9Ng54|JfTh`WDEais2A(3U22#Sn7!wcyhsyKSjymoWWNhRfvJo$7ZhHqhO0 zsH~iQSgdYXd8G_0`aIVd7eBN=lRIE)%$poUpjP#sJPIkBmM9OeE4wIf2neDY```n@ zTCOpyfEot3K{cgV8gWX~S{iBn-Pcn0}{sq-XaK0vq z8(!P=AX;UTWTEZf%I0+~3kAJU=fy(@eY5{WDd^6e$vG}LrTx&UTOy@8?^t&U5Bd87 zJAMJH9V)%!u-qti{T^5hVPw_)d8R}9&#t}^?Z&L|vi|jFrCCa0E$%I7WWVw3H$On) zpDwt%PCfOTvOsEFr`xuLJqX%%M#{E3^*Qtn8{SN9d$ML$Cr0Aw*dA5@Ev{A_=4D6f`Xj)F z%6D$Ws?3H?muSDSu#WfP?=Db$_WR)_e5QaS4^{A9_5EEqxu^R!WJIOzSoc!~0`q*4 z>qlk}_+CGQGbfWNYefG^D^h^&e=Zp`_gu<^*RLOKzyb*-q7~DJ`k~TgCGyDxCoBV- z2)-9UpNp3t^YF zLP2?y$D@ska#}%yTqisI@LO8;$$+tEZ6Q>IDl6!n8|YeTZJA1%#PobHpiRD~hjj`#|E;}vweVW@T3#;A!1R0ZHp3W`LfLv`& zJuY|~BJi;rMY@^}fs;lJZ*)x$Ct+l7yPYI>__i`Ne?It*_4MuRpz1Rj4Ci7~4UYA7 z*{?g4RX1v<|HeKslM(&FWmzGn?umpw?()`u)@`r4xyX3p< zZt1*0>{aj|5n8Z?w;2|G#K`G3iV@FW+gj;R1U)Ud9@<55U?pP>rTQ9J2nk@HF(K-E@XJ8^DX#g` zb^px923lXheb{Wdyk7g<*~v*5)M2W>QW=HG`)C3Tqk9oGUp+PO{udS^;*HvY3^-4o z`p6ZpFM@|xPfRnM;$D;~&hARzdU_#UP}F13ayP1!Jz4!i;x=MB-CXZrcovYULFMWM zI3@jFmRbE=j!wbA2Ba@TyD#B$o`EAlbwkpU{t|t#J8dES*~8xVxb8i7Os`cUy?G=T zI3-N>TSD$1+>lsFc^X+GO)`~kEN}Tl3_y3vmR4{7*AO!EU~0I|PWpfds#(hjmkOLi zLFk@+Ay3Y-I~zw1W!PqFgsh)dI9P_^M&G&U`xCDD`l}SEoAWHX);Vf%`?MoUiIieO~6UhprBMU$Vj** z`^l#ic}ar8RCTA^W^2#`adP;?6JH#2&@ttP?`SsFN+c_nKQS?VNa~9a-v^YuaTP$W zfV225&%v34o-~Q?7}L%KlIh%&j6UKHzP+dCh~_i+EqZWVi8Z3KLT5Rr3m-0R!?E!< z+ekKjxzltQQbhvkT4;x=>ea8a!{vxuL3h0-q61g9;?<#WY1fTR&C#`q*rp1e*vOFRhG`1%g86K8OFe(4nCYyVv{o zBCj3^w4NN*m(#0(hssUMN?Ro2 z$|w(nn;|a;B7!L10Y?W4$7)(SuoWJPVszph8*sA7 zf`UT64@T$!0ySX{rMPTeioZ4^>$5Ow41|5i7FKd-O|e0CQCa7vb(s=}x#`7C#PDPM zeT&4-IJ>8`>fr*7F4sgqSYi!lBs~s6dJ}g{EvUz-`FPE4wengzvxN+%EPew#4Booo ziUe*yktuXx@a~?6;&$zKbm%MrY&BqE#4rRhWK0anS6S1xk8)3K>KI4xRUYF?fYCJC zR&v`$r-^A~krr&6w-_2>)*4ZQdbd_}u$Wy;iwDkhtiK}uH@wb+aw=?p{lAd&grTn% z{hoE}iOj#2>>U~3aqrIR!nl#_Jr8ZIxc2~7(NZf#Jkcb^BTwFU;}$;hR>j$z$$b;O z;${_eE=UUXnnxiHir6$4#Kf34L;EgSZ&Bg2tdsev5@IM9qRD=}LKPAu07poeIFz{H zER9`^A&9!WbTOP<9Hv!0HFBryoLzy?e>#8s{~qX#45rzrOMG&;D$|%doRMwg^QV(} z98AW=Hz$Q??jG2alt^1?crEnb)l%u*Iri|cq6=OguYiAF?!(I>b>pkmila!A64Ipb za+J_X4C&hzfZi2KW+wdQr8xgqgz2*;qpi2SHYG&L8x-6-Rum)_lX1dIb-#aY4|h=> z57!$k(!q*3WERhtWzdKcb)^9K)`2Cm5t4-vDlL}47_7xeK`p;Ad%N_WU6L>%x)$9p@^EJNxWGOnN+VR$ ze?#eRfLa4mLo0Qz>BEuP0FLhf&9Q1@I!Xx9m?TD2%Vk+kkF%i8}i0|#{l z`!VEc1y=}trbdHx#67v%=($$wOyn2rgdP(`u#pYc_&4YqWLr6%x58TUwm!P& z#AUt5my=0}aLP0i8b?0_kLpnnEvQB!Jx=}IV>;dAmHiSEaDJW z&$IaZ%U7jL?<4{fBbv4d^5f?Xg&JU46X7vU{>n+YfobO#MQZ^oi&<~877q`A-YBRj z^D~0~Mg)=G$R8Q_nea8@Nf>(-Fy4X%fID02I1cF&{<&}q;L)EM>HmnHayZ^Je_c{v z(aDD&dknb@X{`Q+&rI z@&2zB`_5xj+Q+43gc_rEdXRu6TePP;DQsBkSkz6g9Yf&e>S*!TR|C&0dr`NA}_JOau~U|7d!(`F(Ao+tUd5%8x6mdT<-@|A7$DYcute0=r*|DtX*Py`D(TWw>+t86=- z`nmgIxWw~Io~#@F*e5FL-*N30!c8+$;%126Co=(GrJ`_J_j-u1!cAg7D1!zH#jT(7 z>E-**Uz;O)yIOh77YVc_`uEyoyzz97PBy<-G)x<1@=u99rCZuqGlTT`&^@QXRMbdW zdsQPKh(ZG!5qJ2(w;Yel&X=#v&QkGg|Xv%O+(1~6 z4uzvzf@lw&+uQ?%>%M@(dOAS#pc{q;Uje^(lc=UiXMD@r3uRcGZo-{?NGHt_xB2>Y5^D9vjnK0@reAw5ZRi%|) zYf-1I_VQhamH%Ck+Ss`){&z7?_6}XTb3RqCY(VerQUedGXLfNr2*dr^53=g(dpnkB z$b$9a8(p0M5_beY5BimQzQnhfK{p9b`TBE>9> zKYm$RKnL4YkX_Vm7K>lr(4aBUVqIJY)pWO1`fMiih@ z?qZaLAbr%@n<}|5b6N-Gx97ewq&fmdWSyKX0@$i6u@UCBUJyrA|Bz7ECe{blXA_9~ zo=$}`Kwl8%Yr9v>&HnJ%_CS;;lojL*HPe#oa-4a&H{2Q7-Jkit(TZW#+u@)H{6TgE zOu-XKW+*ZT5?4dK+5dJ+mi4jeHUU9(jfW-v3mUaVQC{9+$DF573yJ2wYN4!;tgM4$ z58CmxX;@?BnL|Nx@ymH=sVbbD9|2`v(KSZrzC@qvne z+KUx8X!hof2^95rhaE$=FQW2`ry-6a)d;|ijML1%Pe=!c>Na` zz&Pqk5kugbX?Ak_^GZ7!Zxy_&T%@z+%E!YGjibuEGB1bsS;OdF!Eg^Qd> z(#EPnAEu>NtQImtgb`{&m4e5yEJr_j=x+~P&IuHsDF7Oq)UnMFm5dMZsPy(F3+1G$ zM^p*!ZVkyg!bF(YC!hlTlX^liYoa(0cSR7G*NU;vdYX)t%k6yU&im{@O`)_G3!x=Q z_yfmAy6{1Mt|)!ivP25K;u!g08I0McRU|hj0b34S=T-oa%{V}`UcZH4E8y;f{#S<& zmak9$MVKAb8Dz8N@iXN6e5~z9M=V4-640KW&)4<+4jdTh$j_{z9-F~TJxfu&`WT0d^ zulG;@NAvT(*g4ZLY7Eutul{P;#cgJ0u_zjyq$|Z39#duXeP+888CS_SQDCsq`>@U> zp{mVwCx>cop2xM7iKvt#C?UU=Qo+3wA$&J4zk&Oelw6(I*x$QueK<;X&fvYDYc?W1 zYtp-q6@Zn%qwXwOWICo9^LcJ3;%&U|mUqZ)VPwolc3_}4{Z#+JM^B<^NW6Ln9(!hC zyXxcZ>-|y>!C4*J#*sTCsv`jZduI+aDGff{-If`aA-5kOZ9h84=i%4od1sA!0sBo%NKV5W1R>}SiMR_7`106qK6r8-N0PPHm^mL_uNcM_Mk zBYd~OTPtzj{|94cD18q(X{gzZ1W2VkP`O5VSU4W->^nvla}ug)Z0XCbf;|qwYlpjp z9#iY?Gv&TpUSMKm250>*?Hdz3fbEQB)?s!Cp@TG+OI#Cm(xNN;G4fU{}L` zMc7J4h^#v9mC(KPOe7W*BmkC$KBIXYeWgj}Z`%9?sfj^q=4!MqyXdDKuFzz-7WIYw z$qVP!<=kLRhahrc9#y@AOME>sWbE_vpE+j(D=o^+7{Qw7+j+3&&lpHvVS>EP1(kYlV+T8gP*A z(Hi+=3NF_zCm5?l?64)Pr_C#qn8&SGqW_zOQ9no6&RQ|+gPO;Had?}|D)3lms9P5+ zV!-tWG3}M63XKm8^P=kY^yF*5zDO?RG$+ zVk(SQPT}%=OMgkS{r3%;1xxrrKbuUA)6rNgAU?kQP26Dlr{+ISl66ZyJ4Fx0+1alL zr^h6kwR=+-IwQItC`>OJ^2$7`hEt=!etf*~ybnS>6Z+P>EQorxSgf1{P@?egfwjHI z?CslIEtz~)n#C|pFDTa{OLqb|PBEe+^sK|cX>xRhh74b}V6PPNBjsgA7=3!&p_x=A zO4SZe(haV~kSl(bTY5$}S9IJe&yzL~=c%vW4DWXVwP26pWFM)tx@^XW8~wxBUZkKKqardo0zSNh&akn$*bK+F-TU-nj;ySL zJFY}ttK7iC>Vq1U(%ul=tTE2tgdfoX=bn;6BZ%lE$jRalfCLI&oR|A6ILeUbGVFuC z|6K(<&1jCS*6)1dK-1X7N|euo+0<5bsm@otn_GxqsbZ#iZm>8Tt& zcj|K4GWL@HzR!e8N-!xoJHtX=euFBMGky9<>{&JRvMn>kB2Hecb>?rR7}QE@Su0Tp zYCb6WTo%S)QVG5kdzZvX{9~)5O6UEsvCzBNk>8-e#$x<=T<2fH(Z?}7-@3r6%mJ3` z<_wX1VZjYN7fdVUD~AUBA@TWtwpNpOp z+^#Fik0EEt#W5f5b4GoehX1{nh3&#kpD4_HtFcBR;bW4k6)p^soT|O7$|kM z)mOq~EEI~JR4i1;%kNydf_#QHHcuk^G-ugxzgVY`EY%{Zh0k=Q;LALNo4Y-(`~0Uk zwcVdCn+@bK)rxa$&*%H21>~vitqo4f$vdSusQ8R%tIX&jgSx|QQgg^92u`Z9VSZ)? z3wWh#IywwYG z#vD_ZLek}X3?PES&E%ukUXIKZ-ll+>GyH|x0Mh+MAFEhOR;v5n2?q|%Dyd$BzO zeS$jJRKYdXa_O1a~t=?96Zm5!A)ge{TdsYu%_U-;Pd@29K&KWW{@@CfY z*o2~PHYOaw68Bwio|nD<{&V{Kf$FlhnpcptRhi0O(X1~T{bfTM31I0sAum7QZ10!* z_2yN`M?~M?m(y)Q$@>43oh7$;*r*Uw4+Vf%)rU}94y%U3H##=a8rFy=mPqB&FCHj> zOh{e(EA=S?pK@xp&O~8M!2NG#L2qUy+v&oXpE0+M*bXLL1SMyIkkP!3B(*x!#Vn9x z5|{JXeYuj%Krf_v;v4-^h!8b>pTpiY)uBe+M zW}9bi!)iW!k$hmCp9^=vh*(K7+T9!$)5z+DS_E%VuXxxm3lA=jg@i}AnN zLU5X}HOeVN*y9v)Y;)EvbTYBu2;^YPP!+%T#6Z>o$>RSDU zheLX##*{~EP)|Q-E!V#R{*b5BHKlNjvC@gW5PgFx=+$M}-Bg5N+wtXf!I%YOpfFyU z|BN13{_#$Ka>Dd;I`qNmXJPYyL(=<7Y{cJ}QdgRr1{SrlRlw3+$et+-O#p1PsN;7@kt9d1TriGir~~5z*`~A#}I$xa6e=#cZ0SH9@QYWrhDzxWeaCjCjM(JPn*7r|h}p;MzF5X)7BTw0X>z7)amP&bQR+JkiU1PG+_6~G zvQyu^eknD!)%V_)pk;wxeW1apHx04n`@>hxu_uBb@ApuBBMVZ#|8x{*5>;sBFjDh1 z|Jd1&79LNmwl!P6ZVi!TM@)Q@X^P45EmT(#+=D{+-&e$HtB=w+J*N5`UD`+K2$h;XXotQw`YTX*5WhcpCId&pwYQ0`{Ti1kN0SG z!Z<~2|H2gUBMFv>Gd2ZVzFqhqN1R#B@JfEQq&MN2R+#qQfaW%0?RuT5(j(8WxsLhf z?W%>Sk=$uBJrkK_Q+6WPYDAJ}U^)NqDJC>Wj5e3#$nOFEeS)pgO)M_&>fH1=ePYWXuld6@?ip?I zDOQ@yROMt+u@n|fnadKQ^$?Jl9(0pHxT+nr@)$9$L3b!Z9)E=1b~7JKSijWNphTln zqRm&SMqaXSn*F*qpZO6ATKOFXBs5=c?Audz6D1Q1fb9U%|G86|oSfux-?Jm)cZdWH z`;c&ZFrkqMHCvdxX@rrMQNrGSfr ze>1CLefKQW(e3aIU;n!ke-MTJ*@8Ql5lCLh#%3Z1b+gAezDm^ljE_v%!zd{`k+gGQ zG~ISn2u_gb{&Mrff-Nc<*$U-QA|LKsJ=jy0|jt?om%epvH*t4 z>Qj;lxoLw~mv`jojsJ-U8w`@I>V7a)3nm< zpUcG_H#9c*=BdJ0G#8WFg%1Y;O%R$33i2F}F!|K*F_4GWl!(qC5P4cC^ku_S)ys3l z<@?#;O|e&*`cnCr6xcqaoXd94GI#W?t7Xj`hX>Y-LLq&Dsm?^(I@$|HUug>s+%+u* zYl0A}^UTKoaUf=}QM4M0%w5x2u#n$bE%@fLwsj9raI-gCtxtK%SMV6ZF%86%bzN9* z>yrT7)?qC0lUZ*-7*BCvNHx8P2F<6xcbx6lnV+sOh zkuycJ66fulTEp5s3+I#H6`z0IN+Krz@xXZi9a~-9pq&|ktJtu{6Kt|L)S#!gK&wU@ z2YB9c-QX&Qg7iR%t#sjYf+igmFp;y%Wz1v=WmjQQO}ga>IG6Q`gP_%eA2&?%+WPw7 zz&pItO7lk+ht~)4$VAfH(YXHeD}$2O+qaelzG8-s(eg34jVKG-#RsUYX^R z0{vLw_$iN;X=(HF#|dzLG7BAPZ#J(J5=U9@yuhQ{Ruz_WBQtg-5&A-@g6;V$gBkbG z2DiNzc@e6URmt>0AvZ17Op(>j7-$M0|E5H{m~#GNYP*kx+a5d{`0 zOtNz@UUoEi3j0Lbr}>+MhV^k@G)Gg<6;WcCsU6(aVcVP-C$4W=AE z`}!v{J{1GmN;rMMFub4l?-dkb(~HeOWC7PLnAgFLKIipIz{G8AYAX`jZ{TLPcLYtO z*-2O1J$$oW%n+iMtwvO<15DE!AATK#T0%c1>jx{7@$QXVxLx9g`1l&EXXRQZ9B z0n0SG2+j_bqs)&R2(%s=*>bo}o1Pl&m(gq^dLB^%Z{UOOe!F|Oor#5Q7#O-P%b`~*#Y4B1W9xqV11QbJvk#{$G5r zwzvWxR6Ikr|M~)DFG}F4pe#AYjJAgBS@}BFvFn)+zQB}AR`tkh(A{3OJzK zV2jMMk0+Rp3H;Hl-$Bk2xFXZk;KAXq!2v z26o|M`^OKm^?SO%1!ly^2h=H1RL3v(>j!NuE*Atsq3<+a{H4~bzgIgUu-uF#PC|M# zc7rsi9}AH#+!K!saUBqyN8pHS8M$>xwvo2Qfg#l75VL`i50gWvwUwJfu)_KD`t zoN`mYE_W3cM0!p+KDC_dVQ(LL_tCy&VkL;PpLO_Ng?tNW*t~u;v_JS_-t5-z>^58m zUG32rCY(Qje4~MfC1>hfGZDM^oIs!K2#BhtA)3x&KumPkqrCns?oYP2LS>p@F(OK5f zY42;)JmOMe7V{8BANyQbeV)uVocX72s!tL=D{bME(gUyw=M;TczeA9B3^llEXpi4PXuDl|#S61W04f6OSwC=%R^Ik^fO%LX zSK@faAG%eTOQ|4y$;Dd?cH zj<661j()0^oFX1;oQdzw-SA=2XvFaM72?4!kU4r^$gOqVPNJZAWWY>GKip2dX)onv zmu;%H{uwnd3js7u;hDBELJEC3@RW`TYEBvr`oy7Hzcm?vFjUq4y0t0MEHu@ekh`xYrT8%RGh+h&`*u_H%9ea&w<#P{5D zH-!L=>+5tSk1vi_%8#XuRPZiPadLFu?O-J6M2-r~SYqn5vL03l8RQv0R5^Q#xIALcw%e?5*_tEasS88fjSZq|Gjpve*zV{c&`r>8B4CRUjiR5EyPJ-8-fG+bLWetoSub(U+DbK zYGMXs;sZV4^zNzJZv&tUFpKres#48IgQ~oQmsPypWEocVr&R)ftr)>w#eO&H3!p6! z3K1!)4wX2OD51ow?C41 zaJ{%O4clLX)(L7!U!w}{Vf&jkgv@M6Y@)2gZpMlDrphisz*}7)45bU8p`tf!fMslF zXdJb0#mhX8o);m-?4OjnFZp>~%mus)9G6M-jzDG@_Itoq;%{xDZun36Lt%<8uXA&F zf2tu|hzsoL&zM$gN#Rx|JbJ{x<`MljKX5(o^9(xz6 zpkRu7b*6Z?0x05KolRIv5r_=7cCNMpvmr0u`8$Qa5}%SK3=n#JDI-Z^qmzlyl0iY7 zL07M)lU~^Mnd&uzK9T@nLvF}@9ifZ9W4%0O4`Gqt9yRyYvGk9Szo>9}+|qhmj75Ih z6Rh**15e!7FXvbNN#udMd95C&XmgOsG$RVSQYN zw3oBNct*317rDQOUfCt4Dde6zhu2ou7(!d@_WnWr+Hji$@jrY@!RB!ca{`CYfzo^G=#KUMl}a==sv;EIzz-JrqvCbAG2c z1_qyPuGP7{u~q<&IR|cVt9M`2>Ak2L1RY9q_V$d&KJ?B_D$$~xsTT%x0M&KodTfe? zrES5oc^ze0Xr@bA(p2^tUBMh`TvH)OZ#MzwFGs5IS<)>z%j{d9%Nf@eHB# zYHRKmemk6?MtiJ)V2x%vxs|ey1W_sLh?J zhcrmX23sKl|K85o&U5Z_@ArQ4Gjm~s2fZf> z{l^9dnWt?sno2VA_2Q_bmTX0VDLJ_e>6pl5+GXizL(c5lN+8!AY;1ilKpkheJeRl zZIj|{%No34M?k6RYx>C*!9k5VPyU=NlAuR&1{3!~ObEolPgkdXeuc#LX#Vn|u||~b z?*)=N;PtUh?lCx7-fED`WPpR8Lga3hxz=+Ib9lBo2fj7}cyh4qendO8-S+-~^6OW~ z*6+)N?R7dZ#chZV%6@g%%ZX2UNu%cR|V+yeSt6G_W6Wtzc2sDUmO4Ma7>@f7GyWwY1NU1s6dhk;gX#en^idp z#y7LnSc_D(RkHQPGd)^39_cm6$_HvHm~l$2z?-=wPLBmYKU=swiM@*{x<)hUO}fC8 zm(L#KkBs6>{lUnvB0rINEW%Fka2$jS_N-6xP3csy9g;EzD! zyzsB@#A!U(mSVCAh^s&4<7UbII@jRbPnaCU;Y`Hc&Ym6hK>TQJRIQV}vARQiK%EII z$_`p`Nh3@Y7dkN<`n@@}pU%h-KCJo{EGXJVn*PhGh~iINZ}!pt(2m;Qc=VhcCBk|} zVLw`Stym!*YdF6|LHW<&)&8?=Wy83 zd=NTmZALN2GyxC?%xAgiuvtdkJE?E?7NltHn!qlzz(c1Me641C=96__F~qYfdaYmr zeV7A_zT;)_svN(`??q>;H|#9%-0#lT6>i}Qhv}B6dC&i$LNuk$CHr*&?Sm2j`ZzAK zaO5-y4$f$wPMtj4=g4@5!0?B7<2N%2^9y-HFRdZ9zKx2SPG|icQ!0-e{|AUDl->Vf zi>+~_7yJb4e8?GZVXx_Y6w&)eBIw5F^CYMg;pQO;dS9gKT9ZT_tjULks*J@;aR0!b z9dNgy3rt}*4DnKi!0|TXOok|lJfsrE?!dP^MR>5Sht*pi;ZA$OG`JTfCiWh?Kb~l< z=97XK#bI9y2Z)CD%_+TMis*F7`Mj5PV?L!3yUjoUF(o3%J~X!XyG@NvIEF%yYF3W{ z-Uz1BR0`xyoW{BH-gshKb)MR83xFggYrrI-an%uq4`ZfIXD4!fev z+^N!a*Cvg5ih$qL!MQv-0Xc&1;@Oa&#D_vxJWjp_MJ#E@yK=&03wS%)+98$Tj)`va zg7ORzb;Vrr7s5#Sy(IO3fV-5zG`{8vjWR8Crt46#^;@H~5^G-0Fw#ib)Y$h$^Yo|y zk5;08Viap(id?^a=qy2&#v}<@I#jK9tka)7Jr2=n5W#34F!Eb{e6j)d4z9lpOU!$o z1>iUF<)gf~2aLojaenk8Ra@~FHnp8aeXQLGMXyxRG>h`=Pw?a={!+p)+9y0Ega*&Q zElE%1O6M_v-(nnZi~Wl=l(d;Yl0msr(+R98hj$+#gpuAnHZJ^mV$)^yS_NeGhtB}7 z4{SPdQ4-atAch8~hAs7bg6f|x29fN!(U+*}v@e*7=qx33{MMm~o=mJ9LQwB$RMm*i zN_AcBD9lWivgIuALpTvdqlX3;w=QJ3Avm#We(*P)ML?~+nM7n|^TsO-VB@EcVek2b z<(n6$i-@CR{3{iV|97HOO2>PTuA!stu1gTsy!5=Xgi+f|*=nVKar(_4K}Sbtv-_nv zrypjuH9n{f2&qElD7_g?cmS}dzjogjJ^fc{GspJbS%X~yfV9>!6hjK^zJG9D8Z5$I zL##paD6;Mgz61?C5<1+BH6GhUhbBWr@9V#z4&FOjw=NO01$kN>>PHr8EKB7P)o9h_ z)zU5Az}wqU!8>!r-R6qJcYel_4!t!jiNyf|N9)rH`0IH z#9p__4&UW`on?oOz&C7jvIIt5&dL*s`W?fLLt>nsIY~V5Ym2*LTt5lsP~0b@NpXruC>$JqJMx(uG-ci6Q(x$ho$$6<)f6cMm^Mj zfm-mFMtA{Rp2I@U52V=P0j60}bMLfSN_YR66i%Ynn#`Zp?k;?}1y z${fYxv$v*^MF&SWki#EWYn;{m5ErV1{ysR?aewt>f@mN>?u?UF(7Ak*{vU1Av#GSA zuo>cC4p%pa>oF(L%F|SXd!H8I-#-qx#a*VtbB%GSUFfyKCUgbY--*`E4ZT=|npt9wE zH7Z?1*Te3A*`ZDpiKAn*b>^CcY^t!Ou53@+t9gGJip#`v&BY2wGIRxs=kQ=?>7AJT))9JC}fGgeYAQyGe0Dc z)>M0W#CLZdTGLr!YeYC^?5rLpQ+c<`1QYGZD%a`ON6mz?On4BRIk>v0ikH~2iZb8`M zyw$NJ6-mI;9Bd*UvLq)4Xv+1sEVqm7}b>0nt362?=R! zX##VVo(uv5j6(V1)O5LW!&w;U>Rvl+$VLVILF4iZ8)2^3sdd%vw`oPt;OLyeFW^ZF z)FZ5B-YTJDkx@fj$oQ!G2hRXl0%EJ;ej;Tw*M@`h=h|rlQS(d{@fz3qG(elbbY2` zv*aD6^o^1ccq3NdhiN4~P@=PS`SpLiU#Q9cPdF#ToK77qM%1I(YgcUh1Rd$3u2C>Q8Ml!9dKue~R><~7CgXDkx6n^py9XneVbWtcFl(UVt zkgd0ew|iS4{Gv1IjSC5%J5ar^Jv0_|Fi#5eSsCv^+JvC2mN>fW-V{l zhU@A-rd-o$N&r`X>G&Hm9{h=Vj83R1aHJhT@e0R)%_ zh3PrLV41e8A709*Dy!`80N;~NS~w2z#LQpIkLh&SfMx31nE>rsRYbj>C{pa`5&Ee{ zDGgbDP2jTpE(t=9)uy{ibcm*ix$3vSKvyh|Kq=K-LK%9d z%K<=lhN3-}R8Ib4h}$?sfW^&3`R$=IW(w_Uemg14i#DAybauHVD9a0vEmp83;!oN*}uxEH$<~ z?#bd@^AS4Li(OFVES4{_xI9VRttl(huBMYeO5=PUzRqetfcgctRe@Rm5HB@VN zq8NDYeYu~BeQk&%MP~CEjv}LDO)>N)EvP+~6-J@pfS2sL>#XVT z@*S)SziPqG7aS%$(`V6-v3KhOL+eu9>^9)u?qj<{swtxeU1?@`yDs579>d&?=ZdqY zHG=Dwu{}C}^f1%Mz5{EmM@l5CnEeDE~Oh6yPuU&W!jF zE}!ko=4EN9N63Vu`NT|Fs-}Uh71d|?*d9&v0A|D`$%x`xW{zzqq^J&^z`Q*I;@jeA z*PbF(;%d}d;D%OeH4C@isxCyw1`H=l#DO-_DrC__t%qhli2^E^BQ+Y=5u)>I%2vVM z{GQ$JJ1#Rg3NGk#oFyCU3xn&OB|3yJ+O7Y&tY4EkvY1zWc$LJQ*_9JsXv(~$jnqP5 zK=<_iAP0nAdzy;75;=jUerMrgMvT~${g?8@oA90z;sNghL{J#n;rv~>s9L(KOK8nO z4Y1=Y)CM^AeG1|sX#p?#dkq}=(L&S7qOpN-e67E-+J>*<LHdiQ{KB6vCCs!$o%?V)}Pdm(K)O?N@o=8uxpzHY#l z^lr7N6nP8e@wc)p4M5N~zUH>#lII|*!ngj}5}S#zG;YCE-g))*rKh()&3f8=2X=9( zWfLPGBKfavy`iyD7GVF@MKj~_8>wE3BKH_;$giwqg<8ktGzFknfVjVCswfzyZ#F<3dcWXibv25#m+4ZVMTTGWr`~du z|9raFwATM^OV1M#^9X6)c8xr$09c9#CP11q8rTkZp0<|w4Grk zIl^=5a||DmlBra5`4pDmA2fo)(NhqDr-oY`3cKV!*noH+y3DVJD^g&k0>O$0*Gf_h zCjWAgRUlk^%<$IxC8zB}_`#q2sM6^QJN7*ngkB{D-Lp;Zqmoy&dFDP{I(t@_W}69< zRHa-<8R8mUR-YF?rL?^?k=&^(xbdsZFAr0!&VPTDuVa; z$^8KPU_sjrUlGmxUnR7Lt=6rDWnM{Lcx71^5a)qyD zdm~DE)Y=ECD2XME2~C97mqMQgH*rw;I3HLX;1uAb&1HHzaDHbaPG1BIBRU&O5TtjD z8{uSg;_oXywAr0q@;)o*cx4q8(hoPVMT9+nTBB%u7Wznd?A6lP5Eql2)9Sj_#Zs)A zExW6Wl1&&MtGSXI*KT)%>v&tIuQlDbw;rVgR=(f^Sn^t~k2kQ271NO2*L(#PQaPF& zHoXdAPEWdZ-Jpi99iUm#8gsP7WgK5J6%uZI{96X` zzFIBI)HJiVzw>MS>3Im}8y$e_+7j9}Y&)9k^dhUBcFvKU#5L@C7XfeD!dwLBQx>^2 zV0)_bK%&qxk15Y~7dHIBM>P1@hq8zm_6NZV_d?wr_>()*FyYzpfth|f^80lmkU642 ztCZF8u9#9&)#s$u=?!M&+vM3Lx0c5cLa25PjeSYV{ODjRXANa!RBA;AZGF|tBqHMN zRfQ))gEE+#oA)qh47Cx)bEZ<1RG5i#7qd<&plk-ZraQ$rCQazHYaxTEd*VqMGMFhp zgH<>2Ae284ROLv9YB$yYBpYABnzp9w`b2?gV>-X#gE{dx3w@@3#T^RCY5G<|KMYm9 zq_!alEGN&2;q)*Q_l&FSHpbQ=&jZNlmdz&2j~jro*C}pfu49pfRL~5H@l|%69HM?c z)!5B7+aA`1L245%)6~wRhS$NbP)Qn`Gx} zp1G7T3Ttc(xiBJ34-@!>)E$}bAs$eRDO2b^-2+qw_ai)>tQ3D|zX)olRTdEOD7DMt z%RZ9`4>&pRaQ35`M((Y^XV&c9c)pq1EQr`BtG>6Q<2o($CxVAVnnN zLxfk9fiQ50F2NidM$wA&y0Y<%V^1q_@prSgA9S)x?XIxuzZvIoJB0$MKq*$prUkOV z$A0)iLO(3lsh5RK`7{P054l@^;Y-0^Z0>#uPNg4Ota`4uoa%#R+8Qm4VO-U~*Dl&e z1j-!z4AV43Hu{yxgYb7EjSjlzxP7eb&LGFv?DujU+uP=vVRTde)t9k1~96nW`R>Fr^TvMY;K)$r=_ou%L6_Bm@(Rzn6R(RfD z%aYH|dH`+)|95@2dL1o|2~`BAyvyV!s}mWbq!tmajoHD;ieXq30XN6Z-z^_yKt^Q3 zRKO+U{;)m(uq%d~3skI>gACJk)kI1;k_-54bH5jH(IcNTuPd?ml#Op$6=Qx=kghq1 z@cp4JSYAkhoG#mSL%J`b!P#77+39wAmVtC4&_efgP(m`fo zX0=9I=vt~`nv4xGr~qb9J5#emBC*hB#~+1IEtJeut>P#gM`8GJ+{%;9Msd5~$Jo!a zT=3x_MUaiNDXSR}LlH^}(gb5P{Py`SNS6(>O5pozs?rqMT`<56Xhky6Rq?ZHE04Ps z#)pX5!uxP0o7ltg%>kRg#)pe-+cIro>oNT(wQYIe3d!QY#g z+PpTZDbCN8yN zg+QIMq9Qsja#WpTD^4ko}$7lYab*nc)iKr>3=9$DLcNWf>j77_9=`;(~Y^&enNZswZ!d{|Ix}tovFL&faDW(-c*UJa4LL zby~okZ1_(Dv=R;cPvIU2ft916o)+7~Q-}Tdm7;B3EauVQgHm0OqZAQ-Vkh>kq4P+k zkmvuoJK<+AXTupG8|DgxBwgk3J8SB2lZm*%-<}8~6HYc0^K(a2i0Vvij0%0%RBDPo z$tRhcNYeDZ0mfaJ{`ybn1UD0FyJVkC;A_ADP8BH8 z%mtkvxxgC+(33^i{I7gV-}LfF#DPZCO8mu+&sQCq1pVKfGj?J zTa<@t?r%0gt0upu5RGLyty#?d*a6tegTl zB}&`2%|FfG&e>L?!JnYpE&#}aR80%Ulvjo|P%k|jP5J>sZl%Jg?8kXuz>FJ2V zDCBMOkyi$a7cpdO8o4Z-?dCP@vTatkKV2LK(a4I0HqGgxvt z))%9QZ_*$arU-41-4}hwS3w9mce6YKbRUkLBwt1B9`HooQFy?b?Co3L0*XM>BpZzF zP1~>zavv}ne~QV7f}{N)-zB>J4P)=me_P02?-_$M_@>y1osq6?dbty8bC~R`*uXp7Cu!d z#5vc!ET_erToJhim9j_Doh9@`nHfDgYZcZ39~_F_IHkpKSEC(M z7$x!?6rEO65^)#kY`8S*SzgLdu-`exb& z5h@Ve3X$K>`(`U@ zWpATtnj#&-9W(0Ek=o>9qZ)uC^52E2&<%*Z@N=P42^krt+vWw-PV3u`&0ji6SLe^K zuLA|$P6GU&!wh?EYkZYw$^%tfCk7$Ua~CJ|HF6F+b;IW=x*1C=Mez6surt7n9!Cil zh@1~I1v;F=0-Y1$D*_%4@;LD7^zj7F%1^5oa$W?ECZ+*5G&;M$A7OTABH&+Za{N5`` zV~WxQgiyR*tZ;6+s$iqBMj&FI(3`RiLL2H#!}^~eoO8YJwl$^4ldGWJZ|#PCCCt1s zXmivJzE1}C4^uh#&Sru>$16&`aa4nDYyJEVizR#a-wZGrtc2ZUT6ynk@>L|>dXY9 z`xH74LEL>9rLWjLZBBv3c=~~)V=`;KBv4_pS>@M;TC*~SH>t=SVo*=H^a*oAyp-5& z&g&s>t!mUE^FSac2Hv#1ltN^6sB}y=^mM!PVE`9PmRWjLtS~!R%xs-U@`g)RhxMOY z-5=(mUo&mF>vZZY&PT6!#vV0d=$N;XoTeT0A(Zx-f4t_fE9i|O_k3C6>0+6ItDxnH#7^TKg!dY>KhV` zhTaeQ;gBnAiA`iOXJB&x2u+8+#_Fg8TSVauz@lR9>yEW%_n-q8fpDSMuOKv=L#N6L zD)=6JIGLx^&qSQ#h^N)JKf z6!H2G*N;7jBS?Nt3qXEEbh3&)Iw8 ztxc3iP0`X{m_Dy1RX{ek_adiDAQUpx)~(LuxM+89>|B)|8tQbo4w6pZ~?6a_mPgi+-w2%S6>8RdT^n4(_QN|n|lB{B>nDa|f zJ?!K8pL!&rTcR_Q-x$g4hA1hL-gYZp?bxXyi%f4j-~2s4o!b-Az}LLK!6c8cmR^GZ z8x--%ASBH3I`1X@$UO1}qu!HakP(c~yz_r!zF+(24>cWT($z1Tc|e1~2j2|hO;2D% zt)DS3Cs`P#`_`wB)sn%;{ZS%UsQ^xw1j+FVFDN8^n&kM<-w*gXnj8qLaR}+F0}-;P zBT@J^d=T)@8z&xOXRjYAO4xt zIcbm&8=#_I@Bg-_t|q* z%412nRB}MKjndq;(s^S}z~?2`;;NxG-IGIU3z++;0lC#H3jCR_!&@K9)W==v^oO5$ z+)3~IU#d@kQoU?gY2ZDwyNtG=p*JpE?^IHSpXXJ++T3sQ0l647y>tJSCb%~%l& zx#-0QpCxe{4TM}!-^#PB9R=w#S?M^Xni0{KZB-S{IJ32@#_Xadodb~LR0lbGwaJ%= zqo!Y_tPF6$SeS{aO41N z?tq4AJ)jGsFdycKAU!TLvvX7vwOfoNWk2Di$A_%;-T|S>Z!miP6tKMC*a5TpONldqbG#k%4t=PYWb za+*`)(V<6m`8*FXGA4c}^)oPMKQ`j{N5y~M5SLoQXv%d*Z+;oo)G`1;Q#{#@@C+chubaG4(7 zWG{$V#UfB}hd!zFiEZkCDP4W*-w?$Ref#4)yI0CU6@Ig_(Z$l!vSKtEFSu*(DrNQh zth*J>n=B2i3~8RiHPQx~Q;27z82YI}=SYtcVgD2R_+j55K-WHv$=!~1TaNGRBi=y< z2-)PM%xfTXxzU!l<9_=X)--k7C&sU1+WGd1N=(~f33JC=o>hem2NwU^XCKK@^Npw) zbrcWEP@a$ESX?;I_Q&vhjkH97`VQbFeV)8ZtaJ_Xh9p7zr*RdjxTbb$A}tu_=^LqE%z1Rd&ppCzk6L&wQ=38*z~H4 zs@6VL8CB!X-qhK}fvuFkvMQ^#ACf(jAT4L*>XoQzvwRwIN{R(R;Tp>Json$BsO?Po_;V zw3HoavxnUFFJw`nx*UI&hoFv1dmlFWk-j>bQ=;Im$h87ZbPN_*(HAAq#L@}V*5~hV zwCDkK=)a}Jt2l|_t{KYk_BVmrAC=o1LubuD1-61?F_dnn(fSAlqZN3lbd^(Q?)ZwKD;M~tO2u3?Md^vKTuJNb1|I3?GDW6FQ9|}vM z%1ry4E&~I@{7RAXn5o;WsPae&=wONop_J52oStR(fz}wg?POM2F&}k5_p@a*>DbI* z(j*v%ye6@k^ua~&`Q_X&sQaguOgE_PO{|uq=(j zJ$cx4yNq{`z5OwAr&+hzq<7`+RFKkLP{_C3?;ji;`c=&30(K?~h%=f@kOUEnn8B3u zq5T~u?mx3Ca5guU61=cx)pF`UN;$6Y^Cbyb={96hq|@Agfu=`v$if-=tX$yuw)mYR zc~S7(2f5q#?;xNfVj&{J9KVpy7>1rajv2n|YbD*8VKr=GA_*HWNu{oQoAoB;wM!rf z@qKJUb?{QucYBt+6JMSA4auD|yQ-;Ayw>(S>ErKY7f5K(W0e5ge3$#7Nh4?GNX9bB zL;CGPu$qcRzg7Y^3|&RdH`R0)4X>)zYG*tBYsg8NI+8lHrqo1DA~f5y&f*X$Zd@LS z($&xOj#~BN-x2(ZZsy1Zjg$AvKEi&BZqW%1F~798K4R2InhM{bG7oUYS>EJAFwlP& zx4^R}kP81*$&K6&lAomEO@tk(O+@uqAa$hIP7sHxG5#b9myVBYnWb-TacOcabDmgT zTR=1E%b3IvOuYuv-cs)zg}qeWb7*oR96k~F%o1qQZDWRm>(tX#ag}=_7#+qjP9`(; zu@X!g=~5tsjGuQsw@5~&hgaW^lkXB}acXMCM+c&{y5e$gMgV`xTwlk_ePXGKf0__f zifIM)wfsEj^Rh`HsH$-x4lYYsch_%# z@&JeN!p(6V=-J(waJ`)wW+V;uZMzXq$(D9?{5{eR3%6t2K`?CTR5=kF(8njC9ZWz? z#Bnbz`Z%YELR*89go~b=)AVT>_%i7yL z{(L0tK0$i%a_%obRL3OIVyh3Cg`wtwhOgVL>7Zmte(WIMTAgo7rP@2F`)bR~%rnuO z&2-Oe$QlxO^P>}`tHp4;)r5B#McHc zE!Kwg&(d(v7b{0J_q8wB5%wgwMd;T4 z>eG!0%QIAwYu`j}>=^J1#V>#UUCOKfPR;;p9saHvBlH1+*khiTi{J*o0PUz7Do^t1 z?9nr2L?~Gs-_(aQKl?~n*odd58)Z`J-f-ng9lr=+k;gvx+H{_1fD|QHRfUo|6RlEY z!XdgwD}u6(7^^B<)ALHK`a((Qm+IgaofcsAB5j=vf>EiZV1 zkdt!w8n*E)B;00k;s!DtKm}RDUJ}-)7V}3Xs-zj1%C>JgZhrun#eu<7b#5*K2l;aodjZNnFd5=w$n)3J?3 z6<(*ne-Awg@kzHqZ!F$tK5kw4J<3jT?>QZmng6joaMJCs{&7^k1YdEe#jI0J3kkYZ z^5rJ^E1E5tANkFi5Is~e=Lzyozf$(#AF_Ao;7kGh2HL$&%kMAN;(Qn^E^?-8 zhd&(#Mq>VVnTShA@X)xn1n$D*OOlz1eQqY))&0% zSMs|eJtSABeZ^HYvQklI#KhMTHBLhw8x$)%mi=u>RI8oPbRG}M3iGG7By1#C z307CQF1ERn9-|h+aBk&5cbIi9n0mef;u}P3IRk^KHupvS4G?XrdH!k;y~%x}NR~(VLz|J=z_uADv8O(HCVT zNado1=X1PUhA}H+sSq>Iv_$EcEFQXn#HNc%C0lkNItIf@c9`Rf+xfC(P?K6OCxa%I zp`&DE^?w3a7^j5wDGZYl4-1Vjjb171hoGKn?g(MyV%m2N->C(MBOxF3yd*hTisxB} z5d7mRwc3mGr%>eGsAd`vibyBVGjsQZ~5S^gXmR{2En9348zpQ)mQq zazk7b#8+ZRHxT$(Kc8q5-yNj`YYK0YJa`LMj7_wQ-N@iADyvzhE%i~NJ>>W;CYmfY z-&$V=qw16^u})=2mOuse-l+sXh?Gr)2j4$%Te2_W)jQU?2w-@CK?aVUL$n>rT7@T<-iGGt6hNai6A)USmiYh z__DWKBT`cBN753vG`86`$tcg#=AE}y5Rr@(-8M8|XJfA)3SQ2)4`p!}X@vWPWLEMU zGfg1yu8}QyhQ7jVB_tpsV~{1)OpIT@7||Byp~z?l7x;)V1E!+XnQ-C8A>(pD9`AP@ zfjxDIG@Q#sTAMs%NS5HDKk*MR$Ptw37{9nUICjvECot81NCpzt2;m2p8?XH=^Z%TlKoWe&reMZ2ldSHCtr*|&lp3f8 z==c5ZPNQmF30S!;;=jU)n-2|>45&s6%%7FXz2078jR)y%>7c9H!CzJJu*HomE;e5S zAqn+d)PWy~49k$Weha!>XswwVJ_Og*I?zk`Ohy8}wA4v8+kcsCd`A_s~-hkR|=Y4bE**{uCFGPWZ)U}dRlg@yM5fIKcIxF7fB(eLfm zH`kv`1lf4n>lq<%w~&%wduh9~0JQt;GN{w?Z}MTbvsn{nCN1|6HFR;4?s!!%Yd-qK zXPfuaUpvt~y0uQP{6n&%@bnRXt8^mW=Q&3R)y#g@gM$V&K|A)GL?g8(Mh(%1Ohy{* z>Q3*P_oQnqRv_Mc^_-IMc&{LS+1awT7?U5{mmB1#=0pSS^0wJrCWZM63)WU@f|zdM zG^FRK{^rRo$ZmjtdWf#@R22@+0LX);f7Wr_8tLCQV3MANS9rT?V-KDO=kztf^$c66 z0HD*ge~v&M5uTZj62Igj;58)JP&P=LWq8V!)QoOPt^$5Ji38XYp933uh=8>t_Mr-I zuFLUJvZ$W}mdn1t$qg-+XSOU{(h7+NB9dIpCPM_$lN6?&`Q*(P@_*dRqYjwt{oGat zS+o$Fq2Mfufb<;k-O(ynDCu)k2Nhf%%-+-DLkLXDA8F@PWa*Df#QAD_e(b`s1L^G( zzeDzZX%AeDYL&^V90KSliFxof%+p46EUKG2$Fk_d1GTVSnYmr#P-G0)7n9K34mBO0 z;?#x;DJd$q+q#4ry0#P9N6%IHHmKrOsP3t{dNQoiPtxZ8BFVfJpD&fc2z!_LPXGlP zX>|OtIR^#_NVQ&oTFDgrlFDd>w1bOFbl>T!U;>+-Rgh(lFM@ryuiq~=39DbONAqQR z*5K5earRESW}fcjYAJ{McWt`c=K)Afz5k{X-ktpJB5aGmmh`w=i_KdP7Q|U+ z3AX>-{c;-V8tsi=@Kx6x1$x4SS7h9eB=7HrYIJkDQm*jVwuH-aa)iH>85wv~0a-zV zdBwF-(pLpcjqdE%BDy7DeSH}F?DHE*iLfZMDjerUvhAILxb(Ftu>Gw0$*CKort`|g4=S*!{XDZ`f9>hN938-seqSJ(sjL211rRNm_s^A-N z&iy-z=d7^DwQPW?soG*|u$FpyXI9@lC;S#YZ9(I2AL?qt7w{YVOdfxO^i!J(1AjWh z`8dwiUZ=a7GBX3j131msoXwfp>Ybqh+!K>Q^LrB%U(v76!j1I4_r?Hysai@Z%ZX^L ztzeCkNX&4P9j6!txG?K4EmE~|#HLg_iHtA0+##mD{)OO#~c zfrZ+%jUuxG3D!m09VRH?O=HN=2vwzM2MoJ_*$VPy1mVRp6v=F0SO8#l+)7eAO> zU5_y`IHiBv2TkDPPFP2S=jiE|B>d9`CvpZ@8jP%P6oIeg^&JW1FghTZ( zeP+0`8Bv3jk4#Y__t_HMvGB+%yDBX?;O-~V^oj7tN`F8b+nLCM*NUTQoM0M=)Vt39 zK;Rts>kME0E`+V|>V1DQS+zpbIc)&}1d^aeBKW=n0AQN`JZ+fdnQ?fqoqZg&OLHu1Ut@Ies6 zP2h9mFKY*C%Cz6Rt7&Te8d`{Z2kD2eRjq5&nk_eotoUS3(Z6PNirC^*S(|u+kI?7< zKW&R621^)ypw*p3(H=vMQm$)sS+OCMNJLojFmG3MtuhTik@Le~qnk>==r}v6My7p_ zFbFxg=k`=c4vwOygPdx)f!~jY!FEjkiTBPJi5cBuVhmU0+)hDz1-R8FC9;9sXsmK8;aUJUCcz}sjhRsD50JJY;Q(1yJ zg2F?6uz3XW{`~EwPuUWxFixU+86&B?KlU&^+wyA`w`_o za*KGUjB+l&em&n5bWP<|Cl0ZFt2Zm^&wabE!0(X&Ec+GyoUw92Ym*-sbGnT32B_1G zxC>|hOvc%s&%=95XFw&{W6fy~n&DUYF)U5=Y>N4AtHETGNyLrbEuRnGgzJ35CQT1) zDy|R6FOF5D5-LdRh*U9PG=-`N<{zkiyEJwYAAds(%ua zhVTlc&#dHQUOt$kD&!w#I-|N_E34{iTjzq1Ig09K^|Qf6b6l!ePxW;rmFCH$E&51y z?o-4xFv>^+JAird%h3Ig9(-Jz22icskr1%0HmcZcO^l`AB?FA`=&-Pyxss|%lG@ij zD>c2Uw2G8Q0Vfar7p1$@M|e>%RMrv9_LPqD~|}%@)U}(RHq2-?WQ{} zz+=bW6JdMZ)1;n;;-5nfSseTZ30g6^SCUo-)4eJiN5#!J02EFs&Q`PNs1n0yj*Ih>n@~ zk4aCWBZWpD{m$Lt2Us*j?YapSyX#tmn-0$Zg|GSIL67~g?qS4wi#3oGN)=9%eJ9wh zm|3D!%Z(Yk8`;q8*Ovuh#L|Jzi+0}4*&_K1n0o~Yrc48MdbzGo24~40sNSEylJ&ej zi@t5_O{! z;7-!b0Im4Pd8UEW@Pc|Amki;&#fO74IV3~60mnY$SZm>7@leJY)*bj9Y9D|z77_o1 zF;GxC{eBh`OGupLiq3vkpg3klJ@j>*XdFy1^-L;s4610~#5cV~65wl(a1OB#EGo9g zX<$lfW2|6yt**9psE~!z%5`UNI7>m~*vy5#C#%&bv!#?rKl=$^D}XbNio?MS{D~9E zEQx}%$y6sJG0v<&U$p{8es`=amR!oHWTKNqb`hx!m4ze6Sv5gr2-@vX*90j4xi;B$ z(o(gLX(Oi_S8}V4frOmuHWm9nX0OmHH8-o@wCoduHL;cPu$u6_M|iln2((!ns%DK6L71lr0eQa;`K1=t~7# zPQ_-$079){h`E|ymkl(H%1s?*1O;A5BHs_IUBGT%_1HsQ=_00_J`dl+Y-5b`QCM+c z@>0xvu zS6=!@$?n0Ptt~++g@Lsik@>`$z0uyo7It8S>P2pROUtozB^#?R4mml>?^u#>(TJm_ z+m&-1jZm4>$*cu-n4xP8utc0tYh)4}i`(Rq^4mcSos4|g^4AP^{U#GB*13i+dER-V~yOMimwHB+84b+~+ zR}RwFgOf8?>H4`kYm^v~5MrJv=|ya|0>vGBltb`YdNE$9&1^`Ri2bdOqs!du$t|-L z&Fz`46$qZzR7F0zgl~?YvP0n;aATN9RvxgCn-a>w++m8KpJROzxBYGf!{Q)Rf>#r~To4YCNht8~(qOR%hodlq|?iJ4Atux^c zF?-_~;ML>W4{LcJ22C^H$r6SA(w!u++~@*xdfwPCw0pGSQvF&BYAZL^l5$H+q>^1k zcyC`a>p2vCy~CJO8S=wia6;lTIW56xJW~sDagNo91NnwLDZQnx#EHfNx|olZLJ`Gl zsJPs}Fk2&umb#&lHZV~^Xg<9%Lvk)pve%G(21a_5rEzn%!=E=H#2%6#b1q))Q3^Ky zF5BUPZVj0v;OzJN!9E-Jmpu9?4M~=)S~4ExL5E*XQ^OjiivAvYZmk(8NG=cMH+(em zz*U#KB#1in_Rz#T!&;T(`&1x%>1LeV0Ri@{C*GmAY zkp^hwven#-2Rf&S-_rgQ*F(gxt99Wmrx{I7D^DELSnAf6NTWL0!~C(!=~A8KxFuWS z=KUjtkzgZ#&^eZfQ!dG*s;Mh+*m;#~BLY#`oS;+!R>TahC2}BjW~&Q(%5CV8xIpE1 z5#K<4{fR6r0oGFZeT4*Fya~3XLQjNZx)*hqU~ZEPCGAEykn@7;MiKj1c;wgF{nz)$BY$4lv$9RAu}iXS<|uZ9tKd}K*$ z3I;bf6n&8yrmZo0@}!Kxb2rO=M*%MW;_($^B?krBeeR-eIGFNCE2q-Tjq0mI4M>*5 z#)5e{1%EYbRTnh}ALL)Xci9Z#_sO!SWsDO}ElqgT`di2zr$>MRr)p4tOtp(KCdkMQ zahsxlZ9VH2I8$4+kr1ogfKVh2AG+${|9{jYGFp1@`NV^2DEtR0{jiV3o2#CIscgaB z(o%Km=}08@*%!d>RHz77YyYVZ*nxMsbw0T;N!<6FPHrrLpbDj?9S;NC?sr*ALHX}c zzRKr7A_;znc7H$wo|C;nQ}e0;+Wg!K>Ac{xR%>wkMO#y@c-AKil$+mn!r0*VDxw$#uzeEI(fic3qoedjo5`diu_q>2&<5=Wlc$x9% z_!M>n+XM&9KnT8?YGG3ppT|VF>mWG+Pyuh+!~#PpdnO~Bv8QCKw8eb1y}EtwR#DIh zRuH97a>B(S3sRuS>K|J01e2p**r)@SO4Gex>?0c0Jjah5@fe#Bx>fDVzv5!=>iEg- zmk*R7R&*bEC*{?$pqhI;vUDgAcc__6?=vSIeUsG~lk%0$gtZlYA6Tel zkxb2HvY3@Jigze*#YO{U=}E`*2D>EYMWq9Njy9_=>0wvZnm)6_*VyBxB*S+}P*t>) z2qotxH!QKpaJ(sY6Nb?!_1E~WUlLCp}h#EN_jW9 zVA&p|u61fySvMjh_N}^`e5_(im%gqJUP;$aR*bpjYbbToobAYYOofS75>9H@hJ1G! z`i-vmwh29fy8LaN&zjnL5-zr!w(*}b)$6aQ*jrFe)GIK-H=nQP(=lw}*l%G%aL38W zg-(^&LRUA-Vs|T>85eYiWj2U55G1rMaVa4fYwD=9_9tGB9IO}ONtSVzsS8`uxMP84 zm>eKc2ibE6?Gq^Xk;mg6gF7fCfj5G-M5JG~V{_Q;ywz$iB~ zCb=eJ?Nk3vmwy5GkXH<(?aklQWhC2u<=^O-GM*!j(?}$ahTRUkR zbiKFA;-+>g4>}&FN9))9jGp_E%5M4Rs{KoVbzQ{98UV{E5;Ti*O?~ggf=>GOe`yYd zW*a`eGkNH|;|7u&{IwMZ~vm<3#}6Vs>{PPP96Dsthsp|w?x7a z`39~~ZB|%&`k!4Y=>t(dGavEVQ;FSp)N#J`AxV33P6HjYAjny(ihkcM16DP@!{5jO zetVX3_zoio--XZlo)b4gwhdJ~MS=uQ?p&;tt)_LjS%bcK1@Vs=b~h`E@hz-4S$}e+ zswviB#f}|ZZl!=qm;ekhXqkecj(_5I9fwIiCXJ3*Noi?s3aW!tG7Sv5DB{OS-Fw&3fk~b{kRus{_JFWbGXWZl&F(5U1~Up zqh}|nC+^4_JzOkoe+dFq<*#M8oaqO|<&&_&OQ^9ghB;*)6uc(`ktNWEX1bm%#9O4-O#D1*}%MuB}R)2h*6+30c=$)@VyQiLlYy?F7p2AFBKn8ECd)a!t=RhY zH#)7!M54q>@2ffKzVrA$vg(0%*BLIGGC&1iR#=IDlc-7Z3Zd^Mudh@JRvf*t~@EVLuWyFV-Wjp zrU`+OfQqBBzV5A!dxUgJARP6^x zu4Tn&=`on*Pvlk2u?mx`56bOh9#(AD4)nZ|M~eV&hDsu`0%Z&BEa=k2=ui*M@Nx@5 z0)-UimZss?Qi};z`}N;?)Wd4&+zMn^Fh?3t=xTgpcoRE)%G{M<)NVGEG@lQAp;qZ8>SeYtt>i5S0G`KztwCndDH}r&aWQL2Pe1l zWip4f-H{RbuT&ITZxuQ1 z&u^DCrgsX+AO2R+K+2pqtCL==Tl^qs#>p7`JBe_?xGuVs3hu7D$SJpsTI1+Z1s@As zAtHyI`uXGR#n5+hYp21E0IXWBe zogd0A%Z)wQf>3npkg>ZG6T`v<8&*+3$JT&7CZW;EZI>dZ-f~EiC}2;U#bBJK=Gx5~ zhw0B&ef}e;tYJywn4+7|jXp71gDlumam(29T7<7B3OAdq*_CebGEWyxZa6xuK5bOj zNP?`?IuAY%FN}T_lR3<6Dn)*=SRN%l@ymnR1zR0XX4LZjpY<%xTKFldSRGvM=Wb2! zLmfQpOMhss7|Z_4N(v%DOaIg@dWU6YgK54bHi}K$791_Js(CtxI&#yUe}IDFIqBzA z$c%*^$Qw--j!LzH%Y^=LLbC2JIP5)hv6(fnD2SU3@@oevPSV3EJOWe_1i9p~P-33q z8T@969;9O+xOj0?ao!b2yJ+w*VbBa}b34b4ynL@5G+BP&k8~CS#ACZjYy319k^pW) zHqZ@m>v_G>5XGZX{8t(Lzm?^t^iA2_pQXbWq~sCb@}$Wk)A_TGT89rJ*1XPgJw$>N zqIaHFBwxlT31tNW>|#VbMddT!VMZvw;mjAGot`O6*fe;}pd7xu1Nfi_(o=|1$l@6Q zF;YX@X0CVD{omtn`(4MCs=or_%CyFL?xRFQ*c@H-`j-&GdlMmQryV)fZAjkm2AYlVT|YG$$N;CUMc zxtnNxq)CV5@2xB;=>F}!@(%NRmf2?g#DMI_JI8PwRphJq@r;5s?K{;GR&xaHXaY1k z6MRZ`_bM|uwF8!-9e)l^eq31`aerYF9Gh0WbJQ_exjSph#Tv*SPn~l%qBma z>pwXQr0@8U=Sps>01S^>-jst*r;?OGS_-S$eOOgBLHbm`SkcMQIkif^`3!&D(U3Az zjIc8(>A%a~z8A!@{|Kd5#*c?3YhE=*o~y0m*#{$@BO6n9&E|DO+tmm3BHAK5e{cP| zHxgoR7{Cz?u?-<74;Q2v80i_457Y{1Xk+9( z427SVY|KoI)6OOlHKw##>Uv};0^@ziqP!?qvy^XZ$j0hQv|D!58VJN3DR@4LLpi(J z&<;5FTXq;IiLDDhw_pnEGwCqBNG^SO>BF2LGIO{U;u z3=F6XI+P;aQch}sxFZv?EpBz4iaT0TA z@7`|(yZ{eFQUB{4Jy?vWz7B3!FafH`0A`9o)9V$7pR&dUDheF?S09hT{+HL6&R0_6 zzGiiaiU`0_i1qTX^VP4rp(Z}(aw znmj=);)H}nQDtc^2wy|tZqNKbZ%s*i8^7^at8lgy9GmblGk6-dSqPaI?*O^VDf`#XM(t6@%Yy zi8`Z*#mlr+Rn{NVI*D+fMRL z*btZT_etVA@AuYR2uARxHz8N}kycEU=%z`Ewbj~PNxPEz@15c|4dg_m@OQ&kDq|VT zxJDCr<9RBG&ZD$6k-E#RW5@o@2lcFXH>0dnV<_-iAZ%1LK9XFYJt!FykZ3%F--t6< zM-2!id#WqCx3-}LfHO``hT7}pc2YIf#U*+7W0AoMz8`w`UmH^aR&03<^2T*p6^rpzf3r#FIaU zk%*z_rTs1^tSa#&&SNl6sLH!0p9w8ViQ0o0nGZqN`)XNp#0F#mtKeC)z1oeSj;;P@ z5C5x~lXt_T-4fbluxE?U(f2zXK$qVmUquMIp-+1B!H>YC5EB)Oh|%_dWfAnu5Tk1v zM`8i!Al0VwrLK3*ztVh>__MqJu>TTE{3MHu$TQfi_Vo9ZaAXn>SYGLIn+$xhw7k0W zyK7KJoJT3*G??6rkAj;ew9OJG2U@%o?)RHsg8jcSOjK`=b0`=~^t@@h?|G$wnD1uG zcJ#x=y~$mV(?qBKJ3}G*ll7)GT}08B%R4};aJwM;OMDi$HRxsi<=-o`Yu9a~QDZ$2 zpLI@cmXPQeP#1e#)Vy4Ua=M-zNQgxglMA|zkdM@_JOcTAVPH5wa&vAl_J}N={6CbC z8*`PFqm#}5Y&509PlQ2<80i|`{T6`u_GG?qB5);_)CQa-K;>T(hV9Vw>)%sMM2ll& z>UOcrWF9iL^2=J-%e$GJ`Wd$LkLxDb5(vECCy4lQ7TPUC@4Zxnln&>bJ&^Y|yw3Gl(nRl8 zDjWv^NIkb}DqWS4SPW&#as6;a)5RT=^+y&Fo!OBe zJRhyJiPvO>iE{rWd7I4zlg60U^AG9pEkptWD9kb0TM&h#3>anxF zc#O3ZBz_Q_9L;b=s~_#NIlW3Lh2cbka4NI3@ZT~dv=Fu^MmW4eoWMm?ls{6^Ci*Bc z-2C^|B_Fus*BzLgp`~H+QBor`ZMYad+AR_8-g%jeD0}b(8%9dieW62svps)7e|Hed}NREFs+h5PxRm) z0_>k)&|5;bdI5OO{z;mO0-Y`O(w&6rU?#T*ZJcRYu%j9A>yb9jkX|c6gtewd3#k## z52o&t)YZJ&8Qbk2v>ei|&^{r>16pqN9S@o952#_(hLIkwSz^cVoS%ILX3v6fekuzR zL%>y2VwLcA^{|BZ^V>1k7h0U9r2O?mR7WJ(1?pN>JM_aVC<=qqqt^gOxxW0uqugx#csyCHk2)urtVsx^Hk_UAvZgnpNL z&F+WwD+t^iu^Qo%o+b+y-NE=-z}wS@z-laN&Pe}N^WW*1oe#f9r-JWp%6MwMCIATs zJ!UqQVW42ns@u7-`%N8Ep_j(zQC7N!XOH75tj(#SlV%sYUk_mKhtZfYbEl7+L~_6; z@pfh#2iptipLwKum}RLTZb{wkvTUn%hX^n==ams~gVLcSf0|uaK5y z!?<^fNyu!qp5^WazgmHS!U5h^oReP}ZUSp$nqr&Jli~v_sv&jW@&j$m3WwO@=cyx) z)g?B^yY+?47_>R@K*TUEA&FLW8X_N{zA3qm2}D=D_c17DC=sy^IYh?ECeB97!~P=D zzFPg74YsRs#p*V8w8pGEmI^OP89r=Lfn9x}d#s&hx3fY^B_(UHRzLBmXf@4tjgzW* z+C|hV&$`z7Y5#dr2s_5?szeF)d&GikoHn7fJe<@Bm(Dg&z?Hy-Q2vX-naqjoE;{Rl zlBWA`=SJ8eoT7MuAvV$d1Wc7hJRL!e3R7Vaio798aYRBZmN;xOq6!KG*T;A2!Wlkl zYU_uaMETRdyiJniNQ?)aBV#*U;tnOSTN7VOTlZRsEW%ASKp9*qk9Yr555!Jvp}y$Itd zHh_)uun-9qoQ24b^L*VG2YyyZxfe^0Lk@(dh4#H1qMISOi4%K|>IJA);aCRt!nk2r{InMJ;4-Iuc*yk_x zjW=fM(h`&3Ng{eS4eb!K`nz-~`t)t%9k*@#6g&sw{1aT8T_TPh%F5cY@@%Tj9hdf)u9I#ki33uFHY)*gCp^Scx6KKFYi0VZ;c-0X|M z7BW{|;;wsnPo7Y$r#HB28i>fDV6^@U5@UOLYqJ1_?SB9`HflIxQlqiggzO9xe}*=3 z@P6I>lEV2SMc?w}_edG!bhV9A|6gyL@rt>Q_~y9xgwVAY(R;^K_>9PakH~#%j-$?p z>fvHN#R|)S3`o!Zk-Jff6kS86cEi1Es)$MY%BjO*eG<;eU<_(b zmMF)QZ&+%LeaSqt>R@CH98cl0q3~PBQb~*8h+T+QS2}%cQ}PL_ah%iXS5<|tiEaSm zt=4n)qh_j?qfVTTp`AoW4yOlp`^ctU4VIu~M=<~<9+RENj}+*hjz?+R)};<8fx%fJQgTL$ z>4lY;hKRD=1_`ZtD&FZpcn;Cl&R7O?_tngYzMCbFQ5p@}h)d0Fc<_x~|3SE~slwa8}zIwhgyfjA0L+ z;!}oWGpuDM;nDls3Ewf(oa}@Yy%#^NiW$~F`ZsG+ONk4AWfh{}IVgeA*lb6%ea0Y8 zQorlpf`aWrS_s6cIFezSECw{B1^CntY6JpCkjNSZ6pKO`IJIGV{1g2E&*62`Y<5yj z!MJ{^sB+t*3rN2toRi8sMY8BSHzB!+2EP~F8vuUl`v6k0B_(Nhg}gnOVZ`jpt5NsKVl#GA?Y zY>E2cerwj~?Rxh(T=byT$SU(qRKxt-cgcZ}KZ5=1zq_GJO{^&~f)`$LVNdyhjsx~c z@bsV4dX1Vb0wv2J!?RJ2P7HuaR+IYoIry#n_V>I0GX%kb1FHXXx1~SW71#N_ia&@$ z#H}m+G(hSid1yxKzdHI;_T!_}_MluVx7vEl<PZgtvD!mswP z6I>p%$ClbcTb7_PZILwhtr3H zOxTK-|3vjzz>usF{zPYsMYUa>lJF7_ZWc_P3B}HgvvW(R=*m!qN4AB6+qLL->@wM;#Szdemt7uADOWImD2jW`i~`cz;!iMj)@ zDgykT>|RS+`{=~!eg2-LUEST<34sCCd)!%eGcEz*kYd4^gh$8qxCg}kn6YzZezXw> z9Iqsz4~Nmpg;EvY8gQj;VEO+p>G>*UBGTME3Y1ywN>cmQ`=0aR7*ztc2@Lo;Ky|Jq z1HY~g|J`W}k*iR8U#v-pcl|)F;=j~jHPV}OsSInR*rOD&sRfyN4*%kpLpys$Auy9- zy0=gB_c2XUs)8Kg#HY{Otu^SiHW%EU!@##OO<_UR{!%M&6IpVueSP<_0Q70>5tX9u zZ601A0aLXK@%@y+caI>RYuT31!zqo?a4UFkJ`;H>vsUFff#1KEq{6wsEW-IaJC`-< z%*xghURrR&UZv_-r2KWi+MlS|(|TMe(f!%c6A?7H6mcasm5a{E0_CI>C|*gMmAO`@ zncaapW+w%avEGBA3zE?a#)uoN|7H*-TShZGhbT`2P30XOLJU=Y49(DtBcd3VcY^Y@ z5u|5Xz~u}N*71hw{3sTD;a>C)$HgzGvE_6(s3BeW0TTC3p`aR1nHr4ZE>?D5+PA}JxLV>So&us>UT&dM;Yh0Bl`3Jize|32|o2W*3W2ga4a{e-h6d7fjnyDu-vQZ6mA++yW%9)jf@OfjWm## z>XE;HBil&Ys0qoWkH51cNU_z)=tzWj!Uo-dX?^v5N zWjrm{S?_#Fd>X=5O)JMaAL}xMMDjCuw8lbJLZ)YxNQMx97}c z2ge0Fz^!SGM364dNehPS-7J{Cf6zL?qN^GhGN6fx;0V%qQ+s4MoNJ@5@dvX#2V{?k zYFHj~j{)0h4h<^94(Yx{DgUK#l>+}(!=X-=WPusw*D#joy>u7qD~m?Y*SN-vdlwDr zI>cM)dRIh@H&*kpd0VvllrTL=*OG^>_dCh?o=6CX1QjdFANuc=JL@XOMg68iHD}7U zgtxISyC|HqpCPLOX1gC#4V4;R`}uYinfU4Y77PO9WaUpJe+y?)r%_;(L1Nuhnck=P*({pZv=dCB0yAxzR zZxc6R^F{&<>Sw;n+yDMpVkO$J4& zT*`AI-R7YDv|aa{#Duky7>XOYMOT?F!Y;y zEwX1X&ZQ2h!!Q|EHBd)z@H^d1uO^K0?AS|-={yCYVpL+GV(XkQ52Z=2sjcrf=Id2F z>Y2U@RmyXu|5RE7P{TSPdBICWFgPP>~uw+1N|D^tTMRp3u zQ`%y$C_JZM&SUrynf}TEEo%4Gl&rk3qq9v+=StSVS+6K#uH$7*Y8@84t(-A^Nxm^C z78AlJjVfEk@L$X#-Q`+1&Yem%C^Hqqv{h5p!-2KFtLY36*+JJQ<*O6n7m*rKK)pFOcB^|kjOU-Kk16Hu7xnlu zq;-tuNPRIA#g~M=h>kNPDQJ2fJEA8w}f=_ z_38C#c>*IWSa!+pU(M@G?XM$=$9;oW{is!bJEmN?z=7Ah*B2-7ls-)Rutd~{;APLw z7XmaD*(-J3`0{pV~8%@ z^Ty7l4%;Ls5ZNudS*8k1 zFhR2&Le+^-k%uQ4@u>?|smOdW=}@vie47|;D*nrPT=8RACby{1+X*qga#yB{2*L z>cO?RQ8*Lsn;=*;DudMIv{+ZVJ-c-sgrs2V2iyyEi4WER{q%7!sN39OZQrL=>$%_0 zal;FB^P)Czbrv&3kV+FupnsoTO4mKMk?`6G_W>nG!)bCO81@jsMBw^e9ICt~;Jc2h z!B7NaJ{c+?Rjyf7Oo7uU8{I|^K2k7z{Oa_bxhuGi0WK3}3x{IEj9t-(ZqXCf7>8yI zMo^QbM>>&x^am+d?%X20`&Te+k0tFGV>Sr6dz`sxI&*cz1eQ-QegdcoW-J1`JHpvQ zoC|N48nb0*@mBOSn-cB`xGb`5Zl}RY>XrT}2A-2;Yj)7atikK8D1Zm7h9SC;107lL%O#~B22iy5e?~v7DR{diTtl$b;gtiQ z{nKWR4bF)$kI?rHQMY3WQ}loHpz<~8Zv@tl$SUm*dtMg_ex4s2r0QBsUtbC?oWKFM zgigc4chETSd=25IZsXVAi0+Sv`(`$bCul3+8YZKQ^E2ZU6;*%3yKX=N;;&fnl2=}X zI78!Unpxr&bqIjt*{W3PD04iIxGY^#v?xv!DmJ(FLku*QSK>e=XhAk`>F5r=S?H(v(zPSL)wXmhn=y_`6R6VJS_LE z{~q1!DXZ2{5S(vPdKbHA^k$Leja=uL|G1HU+tZ-dSeJHf9aV=(+MlVriXc96P4B-N;Aq?3Ray^WfRNlR9sN^_`vN`&yhZKmp_$w&bF4Gw%0X_ndKdI-K9XY04sj8#XX*R6VQhdeb|1HPC?mV_dA2kO6K& zpwI3lFRGZkK83^?-XB@2C!A<0DKE;nbV}Y7PVnY zGkQ5BahZyvqLXa(t&1P$ZqH)%mA&$xTNw@q`Vz;s8MT`~)~}>+!ZC{Pj%ZB8HKEs) zP82)wr&25UtbWfI%-O$yzG7j}SpYB}h+A&zyqycZ0Q9Bm9n*$Y6uRR$!l@Duo zzdkdi+E)dQ@Ult{OEnJHWZ{ zo%ClfP!8rOV%_byV^0z@)4gaI#k^+)Rls?6d)x3*rub6#8+xL zftD8&?U~OA&^fUq9NwJ?!*@R1%GFm<_&tjgy|qDh=G#OnSIGDo?&~cMtE5B*UF9cU z1OPmVj`K^O#U=wgVkEg3EQQ&Hxi7LQbDyB;0>XM`u}7X`T(@MvllQyamI++rMFk5V ze?-&^s+}{-C-ek?P>XCb=x+BY6-KCijFi+DMg=m0iS46a{dLa>Up%^qR9PvNc-*}# z8kn*xxKbByoq<|*JoU<41Y^gd`sQumz5H;X_wcH;wzi$qi5=HaCbgr2S#Q&U^yRtx zB`K|Mm4uH}SGhuYFZqa7MBsNGc=#`%rs%%%0W#b>j?*i|krAdkS4;JI+zZ*wH&Hu@6u1~HrXy`&lcAO0z>Bvst8h7b;S?6pLxO&@kNCsnl zo5OV;tH=8^Qw2&RM8(tMEwnCBP(wLerFMZp*eV;l)s z;0SIWsgWXtx#Ru1tzz6<QCos&Ih4zdW0!%iUim05I8-9$@ieq_XIM-~c(DP^oH(V(-9roy|)Q5oF-+qXu$R zYwCtp?K)#N@B7Wci}7Vj4g6YRtj(2+c?~}n*elw_Mxa5g;D8H$Sf~?luK3aI9T6I> zw*H2|w^*HJb=vC(&;YvU0Bw|QhTDTbz5k?9)*kZ(PFXV?587TFnK0CmtDWvz+dqvg z*Evx>KkrY-h+RY)cuRXX6VIAu;BD9j0-b#AU*{JdR&Cz>L*K7-+ZPa}%KTn^f##C( zLaR%S5Z13!PsuN{O#Mq)e|5Aw3wN82eBK)NK-{JL5YE!g*~u;}*_7FIp85qF7a}T| zp$Q)_ujEVb7e3dfAZW0}#ryC3#QVg#fAmu)(W;}VB&%Cf^>oi?tY-?uBYw6XBTn;0 zdrZ_MWhRtRH)1i{w0!SME8Rbp20xtUmX1K{)efgYU;EgQ6O%}*hoDuTL3Kv+P8P<# zore^)$>zw|?Hc!z50R3sl!O|4BV;5huvNp})JBlOxpcA+b3(&=X9_uVVA9Wh70QUt zo%%r}3v6(pM`TXxMrF~zlyA;M8O*--cM-)}){Maq9wEvkBK=9QXf56~KfCV2V+!_@ zHA~jCwv1$5B>=fv%X>`4X&U)QrcAoQ#zqROQpgV9f|{2Ulun6C18Yn~?QPxY(cLiM z?Ll}0vjf3n{a?on(3XfsRYM6iw#7SSp1TgjA{(~D!44U`b$zYx+0&Qke-y5AAHsDf z4D2AbTPcAmqOG^w)SlCK48jRr!;)Dq>CnT&V+~}Fss|iiu<2!b;Zu{$KlcX;fnI*> z-Zf<9Svd0O z=g>IXQGlFJu$|u%a|^4%>r0loPtY91d^|IOt0dd&MUB{Sk+ES6;4FL*2Ss*w`TxuN zG(m8fAE)#dGZZTe_Ij1$N0YcYd(rjpuItoo>juvP7k}6tI!DsO+ljcp;@sbid-+J7?pqwyn*$HTuKC3^FU&u$*#h=ZJ=eok}FogXyK^n z^IB1A&8TAxvEG>@!SJW^NMI-)xs58n5btKE{$2~&p z=Ij?9t0dTQ^&O1MRHXT7icJg;j|rN8GoT8&fA8^@!VTNP@W)YnSft%c5|6i#Lo1Q* zG*cb9tOH%zwUzrR^VPa{cLMh zB$A0<#QDD8M~3wQ#gLDYKvVC{YdOyMH#IjDCSXKlkDp|CtxNBz2}ut1xieTvK&)l; zZ$%7$mM!&%%4b)8z9ITzAHU+G+@oOeUQIR|Pv2G+l+Z`Z;qa8O5mTQ#)f^EQU$M_p z=gZxdB-x5HIWk1cGxb1oh!tRkOhC`4jeKwXm`!?-b{v}S6$ytlTW<+**5GdHcUNx^@yU^Tp7LzI-?S|L;42UT0cRylJlBErK!!5@Z4zEv_cEF-j&R;Lmn9e#6{LwT4~K;P*A2K7zW4gg z+tDqYK98nmyFCCYl$6qDu^w*C``SlKUq@#ip2Y99L&@-W`K|p^zK8w()`R_E{sX7& ziUmrLbFdnYb_TFA($%)lms$W$4(O5*VT0b$^P`D;W#>~{iP6Lw+{I1;)@ZZgJI0ym z2&8>V=PLac$BxS7D>K&3`^)J+4g2F+tSjeCPe9oMUV2>)Qk%6J*at_YsqyDY%sKYp zt|QF$_@cpDpOH8l_*C=khd^&6(c7d?Mw>m$eQn0hdTh40UN);s3XmXOOiJHo987YN zV#$JdZZm&1XqX_*IG4}U;zEpX$3JV2$8@tog;7dZj8JE$0P~%KUFVmTsWQ{nl~XS) z^3r%MeBzdCq~}ycS^ANQ`G+fgt*J`uS+$(qk%O{&nQ^P-MdlI8U}%w~R|?Obr*jz{ z)I6`+lTazabF4>*I=kq&Fv&E(_R)j9MKhuS9le>(R|AhX__C@P4cQv+j0#!7UO~4l zcg&Z_p#?Qu3w|nvi>-sl#4=WrS_QsO76u(nX9iV#Qi8E-4(#MpOUxb!%06p@<|zB_ zl09wzXr{O(-Isizu)vOtCPGhGugrXH_U>kg_E~xx!Dt)UiovPy9_0_6R=L*-856eD zfC5Wd%tw?ivtoE=4dQrB<$StoUCcB@!TG&AdoaKNZ*l2%?i0CD-A2$`sij)Pw?3H{XpJbO%q-%3Pzuk@#W2NM z)6O=~`_j|BR8e$IxU^BlpEA6@)$c?f#`o<=GL_Q-9i4<%T3eJLd~^r5RhyQ`{$xRB zflVxwO~P8SzykQb&_ zFV$%}37Xw{@24}CfP`tPqn`bGBBAFhO(p=WpK2f9Rj;J8RFuxD%I9Gywqq54_DQQW zw1-(0>STzpW?KC&^KL|g5FA~SkNo3!# zeVMP;N`BX3A{1SO${|$uNG@yB4{HPwV$||*k11s%!f%iqcyXIdUh~}2y3I?1ZwNYj5`#^+R22&l5i!!)R z-Rddy)oCH7y{=9Z85dfE%J(Q?MRgR)Pw!;WC>p=8Cbht;r3$G#47jYPHp`QYGcEo^ z^olvmJ=+t`glUAMk@=wUoa@zyF(k#2LRn?bpGUivT{PPW7| z+0rEQtp(bI5&#Nmu7HEL5k16K7)jzT`BxuK1)#S2raNl@r%8T_)79#^E2L{Jh;sUk zF}~}J2&=+b4HkPax1>107x!s)rWr)@PI0v1fQ%`$SR-CODsL|Oj^Ur zEMYYEPm~0Nwk=hu-yviB$)f-pLV<)u9s&&EN1VR2&ebeC>gmE+(>Dw}?rgi%e!cyg z%4>-}mIGME*5BfOM5lz%e#A_e`xJ2o;(nRm5FP7KshH`%>h)KSHBPwgt_Og! z{buW}H>>Dxt4l4yZQaPM`9S3jkAzLvwfQVIUob|4$n4BQJw!5ypz)wf0+SEjUPk1;!yb+)q z@HAHzHWl>yb#-Qgghd$&Ovgqs4E5;qilQhv7a8HjE}JYa$-u_HW9#++5+|#hWN!+# zwr}C-Sty9%Y$|+WdaVPgXcu`vNT<SJK*WiH=Jh)rX0F4ve-L-L--~_i|jk|lW1PyMDySpFG zzIW{Zy!7)oddaMst7cVfCi$uRPedrcBQ&9*PF#RK`LVwscRYe;fp$014u)%q29R^L z7p+@?_x}Gab-$UtSKxTl&b^85uX7%k+ZS~ry1Kgb326jcSYjA(Unc#~0?Xe;;*sn2 zpho9lPYbdM`u@{ZMAP@q#Zl8Q2IqNFXsOBJ+FWJ(!1K(8X(8zYlzR03H|bgb?$<98 zYZSU^KRV;Cvf`9e+v0!~C6fkR&%aM~Cg zRkRa#OpSoVyYwgn@s<*~eTBCBYaJu5?H{?>?e7^l_uM8bE++Ui7_GWf*gq@{^r_m| z`bI6!5vL)u!C_r}Y3!s|oFFp+hFX+8ET7+eEnS1NDr#k;t?F<<0_HfYk{G>Vb@p{X zj+4&O6@QOOOaLL(^Z+$<$kG6*et$a+KbRUuHiH3N@g63|9Qigr=7(76O!=GoDp=Cz z=5sjOig&3F--z9F1za-@4x>%>z)Ka~JT+R+rZyUHpJ>`+)oLII``0tfN#^Y*Nh30% zPa$6V<0I?lQ@14TQ)j9%vBj&1KSmL{m>6`$IUR)#lknVB+d7gWHMIp(TDc}$+;>op7k$v0 z?3YmHtC}J^(jXdn%-Kg6?gTtptZ{ukw~DhQSvprOy_&mE(b{OS?PaUa+AJ{*J_iGE zKSvp24B^nNRo>MwQqr*`C|86lC~U@IG77K_G?7#AK%+|CW>sd1a7=RisM``>H6rpBR;@ujdZ^f&*FR%9n{{9n|bUX!oW#^XB+qk|DU4+|(IH~PI4I8f|zKXOh zOxwQWH-GzjfffSPvnQ{>m5J#gMd7~ry%g?5;=gC>5!)Vf^#J@WGl?*W5d5To$!|#_ zUVpUfvw6(lG2~|Yhn;Pd3jr!YRr{lnqIqzYzB$Z7u*1Pu@Ix^T1`A{{+k2*E)%A`@ z$)alJig61qz<1}pBjrySdd3E%zZxyIR6^%s(7hR$#?&{mc81QD3bYgGLpI9s2YiUM zpZ#rDZ1Bf=uhWNkN8`*=cMOpa2i1tfiBnXj+?_t8ssOh)R}H#6DG&@hU~qrx3(C;RPz?g9nOndXt% zrF&8KNYqaP!KmQL!rEtD*!9zFYH(nVaRcH)Yt+@0`jM zq%gF1X=_!{-3uhmaX9meIjzx6bB$bmXbr@5kl(}s`eXI9Nc|N8b2mo}dSNoU;dY;P z=9d*~JQKO%u4iPzTh4xcevAXAcVp#govd?x$&NDwf|``UpztJel5erP=Ly zCvIfF*{Qfpb^41`4i(R##tP0?weXg}DqA_x3!4u$d}sSLL!1UlOKcN*>RP zQ1qrcGNUl0O5Yo#ID(!a8_m)U;k04!L_76oBJ`C;x@-)s=K?)e=EjaOGNN199<%Ya zHgf$NF)&WF83ay=T=iP19#3$Rm)RMd>SUi7POLpR|Jl(lD>P@9^YXei-RzYswt35X z_wc}`xhQXvL}~hR{`z^pkq+<&>iD<^Q(T&`()#jQ>Esw}G`O5KY=|%9wc+Xn91|M4 zUCietV1Xa`O5C)ANkSxt#;zthF>+-DoSwNlUgmD%FXmH1s#4Cn*F&8SK#~`i7*^NR z()CWV@CEygARNT|OUBVeRFbhFQtn|t8 zhg<2eTyd$dFuh4~NiwS33jIa192oU09e(*e3eBe|uW)E^Z>?&O`-LHKit~Z&Jdh2- zn#YYH1Sb;sJ+eEpUC%}N*$9)D3mu>tXQ8t)4EjjHQSOrKK9|9wx5sJFlYAfeoPU&xoy(G@_g7{}BV#9S5F7&i|mr7iL zjiQJ&ye3gy{lS5!b1G-nNZaJYo&S(g=^1GSK}*5A@j?hZ&RU#2pC~l%c5$E{7O9*k zzv-I@+nc!*qHRdB$F|Ma$ol+F1=VQ02-8Eu1K6ZK?9R>oAs+Dl+*_^v=$i3+Svmbj zInao^RP6og4JNrB62&7j-e7BlQ9~Yf^Q*d?xxuh7(5Y)tRG+W!>a;?Q{-%*Rj5Vx; zf|{DiTJY*6@Lf?lhvtj=+W?-e;V+03N&L3kHbxw7KJiA6^JU}7h}sx{V6UILip7Fm zbgS;0z};2d@F&ylK+!(=$`uVf@+ssCvPHqczQ0+`_6Jt2L&7~HLFS5n@+@ikW?FQk zC2>Bm7jo2^WNa)Ff1s^M6j2R->~kDGOQ;V5OK;{GCn_0#lap$p0DPb%O7)~jfAb;af` zzv{R)Fj@cmiDN7`GPE34fX;UIE0lrUxw*5b=tE!`ml*Gl>UQvo&H#DLR47z~{lbQc zK2?j`C)1r89oP9INw6qI&uQ>&o0p7J4W*|iFJ~Se*lZ4dB%0WR#qRyGx)f_7}PVR_>^jXL&(proEthn4=^xu9ue#$uaivDoI z1IxK0+M0aPspwfS`B_}RFPIam+H>ZVDzW9kq#Eljl@?DsD%Yd6tN|bH)W{;qQ!YQ} zk4r6EI$vxTd=aYj1lXi^G>FHdbTSriXI_SBa3zWin3BR<760n{@r)NQD9}h3iMs7a zCGccQ;qn3?2><_Pc(okXk{@xCGyT!?;`5j;g-7F;UFHyP`XW2a-)6LfwTozo*4UFg z2I|bAnO9swTm4XX?Ru#yW^5yjanL~?N-4fN@g4GTz)hyGr_FYeOtWgnr*G|?7%p{x z0rl4TqNy;ueK%u%BPAI$`cj(LAL9kwf%q4zxO3}G>C7B7+mRs3h zd|}QqxE3fmIrx}Z}L^=^P7m>Tpw2VeKEsZmwRai80 z06Kv&5>|rbsHJq$wHReXI>Ah?Y|;qbj4^^*(I3lXz)y5ha5_-bkpp1fRx*EhSbnJE z>~*$w6!VvMMM!X*|ISQP1JlrR*Y7S1Bq%tC``S+74;6mV1^Q_C;pqwGFbeamD0~+9 zXf^gH7m2EqqU+5ZQ033tBjzAkQGrAW|DEvQk9TETj1%Kp291$hC0cN?hYj zl2>mi23J!6zmUilKE3XH?#%R75UcPi`Y4=AGXY%X3o9@tH!$KM5)cj&;k`bgI(obI zd3aoZmIc$B=HO!I6&ziniS#<>2_#H^4lM1;KmRjywOQ{$J)ZUa-3;+;QGi89#jFMa zmGVx_;$m)@i>y{=;oIcS5@c0osty)FnEjluvI(WzL$$YAriQk z`%D+p&XIV4G3_L0?bQOp!cpVS9-0n8yIkCZ=KV}jZ|br^HZ^vwx`Ti!JHP9Lzu&{v ze0VpD4!a#Y#UCr3)}8OorIc9 z8-2bwR50}Nnag6ZuXL3BZJ=!}dp_xZT9O+>G&))v@dVyp0ZZe z`Px>sXoH2C$e3WFL1n;TpU1N#h+#K+2&oXY(qjv|PwVb-dm_8bANHKmR|Mbye7Xld z30eT6ml#Ymfn9~h#_q!+7Oe2;BM8ciDA6xUYK$9%4o^X`u6+dp&tRN$|4W&WL5nO@m;eR!~>%rOeX~j?q8!EQYIXzeSszI z${W7VMB^U0JqNeMlNTVr&`+L&6Z70^8q|xzO@CeE5cuT99i(7x6kX)({Uz5IpKEf2 zrvG;@AWCwfFg^*y*I`V3OKq|f4;^1H{CLD>p|7F#vr7CQ1AQTBr12L`%^wZS1XvIq z(b#XNKkhk|V0b8IbYvK+X=*3jy)Z7H_}(M@jsP&qSe7|LJWapsJGvM3OB#h&Mm?-3 z5ZhYQ`_0S*jNl$rpYHWOmdNa%m%s!Sc^+>yj}b&|{4CoZkLvFDq-kSYJ@4gj&oLF^ z9VZ}F>1;y9$Zymfor{I?qsJ^3Hb0B=MZ*rSX}a0m*&Iu3PLBOBzJD(bIDG#CV1FU^ z#HB#Q+G^E;vHtZ^JK&ou`fGFH#j12M9=4eh;q?qyOdDJRr>NhLPM%nQ55KItNyFNQ zKxI>5;KxEnSHnVm12HUEY!wgUY?3|8Hv%XXzHHu~7u`)a zDhWKRW`*fC@na1?%IkfHM!qsO1+{9?+XsA1@acX*T88ZdtTr#N_reuyKk0Y<+ni$i zlP-sO9T%UgE`!@0uJWz4uWQ(2PoGKAROG6Z#C9KdwrgL~vrU5?eLKnztB1Oa>htVl zwsej$5DX|9_mAoE0yg?S1-9ANhQvu4-RNHSq+C1ym_95^b$PS;raj}Y(P!V(^kmtI zx6W~Y2Mfl}k9hmkkrMY|HBesxy8}PeEI#kuPxSdt`~Hd67Z^YwnwqGKHufI*z*R)zN}fS=Kg2d;U+m`EfUnmlSIyqx(`LZvM44YN2S%z%*X zA|Z0#_CPcRGb6V$%)VPqF;-29kwfObDEJ1LcejJ^(^`g7klyzrOlByJO8xIq0*!~> z+$%Bo3KFPV9Q>Rx{r<$;&D|)m?}PoEE{)Jn2HM*#RrsRK&kKZ=r5v%RvU_lX8_t5Y0|L_arvdB%W~C(;{z`?P^** zEH9H{v0Ko`*NcxJE8F<(grs%g_WaL9E|)}s!VkjN)%y2Bww|Er`j7N-iE?m>1w(PV z4L@Ia1=|IyCcY@WTrvM|VV#vAy-rtpOnoR; zfx7vZiVN^KWg^u}+avz4Ua6_6@T;%@|Jf%ZEi*?Q1#}n?v7!yK%yx;LP0)V0mYPPT zuWiXJSV7Y`+PYnrLNAuOMDO~;l6)KqH%-u2;z=@Up=H-q@fI;gEi8!Gt11=Clo6zw z0-3WFtA|3vbetCm^FRmw`sQ^{iiz|3W#i1^;BNq@1}#4=?Y^7-yfdj=L z%i=I0LU75o#FC0>x7I-m^~4j3&c%bG6}QH3i5ss_su#l%beXowq?lxU&7HzIr8Q`> z4GsJR1oH2OfAR3AABa16#HH?M-wsjV;+uv)?*{@yjlZKO-+&NPD}1)vzB{HMkSpu= zlEl=!{P?_7ZRdGN3Wq%hEX-kHGR4j-q>>&yqJNYEg^_3t%?kYh=6DN+h_@S; z5o7YlOLpdmVbo3@<&!w+T_P}0J$<{oAcbj_e8_692~=mmbArU4Aexrw5>6l&+;ix&C>Pz|(V&XM zQsUSD_#$opi2bp?u#J4NEdlpdTE(3*w>qio*7@IGiWa?1@{UfW_T-_lqQ;#q%>uD! zsh$LMk~~JuI~boV4E%B2tsM5LrJ*mcOn7tXHnb^StAUvEu|ranNh#h!ygP!Ve+m4Q zgJC?`HZnl(jsylY@f}yb`DO6Z>^#1n?1$$UE$hg2e*bhgu2fr|e9l?!MZ)+W{sJWY zVE!#(e_kJcR7FD`eN~2HyaICi<1f-zy4T_GZ{0vfe1>?~3nejDY?d;0b+nh)(tM*4 zN^lsPKWb(U5)3TwHe>(+*Pp>>a9H~+)j+{kZ@2ol=z<03?mKjFd`@T^oXY7pNX_^( zuO_f@pXiX>k5|>5D(=El52m^b$Fq@j@l0L7o`}5Dgx`~BhAtP>N2!_d1teOQ6-Twa zaN#6s>rz62zkO}?P4l>H!6IeLf3~&VdyJe1JmPbRpGwS1OFTKPCSrJG5Fukv{5j&G z({G3CySIr6+%~79+c=bb)THIx>mJ%bE*FOKLqCBwcTQy!Tm(0rP}0-8Gcu5`+8E9; z=gzF|CmxdZh_jc~(w(Qx3HSe2@O+h3M|7SQ9iM6B#YM&#-lNRND$&llXMWxDk+tQ4 z(Z$x)+)a>Xt%k;i+Bejm%CZCwDE*iT1z+OG9I27+mrbEBmhkQlB8@d&7j*ZMZWU{o zcwAY{JVgYS1=$Zx!^_Mv6&3SAFc;n4k9PODLQm2v*%RlN^qpkI-Yn$e7_L*85W3;8 zB@#@Vkf6wUxYNv81o$n+6Zgpv`K`5&shM-5^M^zy(ywbrB7tvrg}MFhdos*6m5a=P zp!Q&!MG1Sh!mKU0Oy)w;Uu%;K#nYqoc5Y#+@=w+cd{?uZ$kFS4wi>s@QbUJ}7XmZ7 z8y|zz0x(6w-h{f)9S~Bk)hz+Kf**U$x4!_YBfLZ^e%8}v_b~k~iR@1gIIJjGDFKYOT{R@>gQ;3`SHhwWOy-0N z2q$I29udz)D0i@4o-|sFv1MjS+C8+8vc&zJMt-QR+ z+n<*h?`)-=5~x(pNQq~$1>q=FHiE1azVEq82{#D3ZrH(U*$QvdaHJ+HpmCQc*Kb<*E6>Ep@&8A^nJ~;MBx#&8q|DY4QQ51H5y?vjG zdD}=@$Dn36>YVKyfff_nwFxn?m-P}#jYF$9(GLSzAYqG=?W{h59Q%7)RdQkDDtgeQ zJn$QISL>Wogr6BuaSL?}!Za_H>uyxM5j=TrwhMw+U}aJfaJ=iRVG;>zQFDITVqGG= zXaV+MtKkSI)CmC_*GzXYwLy|kJd%${3;glHt(1T zX3@bBWUGsiEH3SxH0b8ZDIG& z2ZDf;>VPq7N;X9*O`u`wWc!qB1;}W}#BGqKLfC~^ZK2iEyjk);e^`I9)- z7DwjqY?4y=J#%^_7TV5#6%>?^S+y?_nz1-xhY`DG|0v*t8`ZILyRP%=oL-TQ{y)Ai zhZ)HFla5!J>eiHRX=%TvN$w(Y#;MlpZM(C5bRyHU{uIGmgej06Cv>*X5^$dI81#c< zTMBb&tZA0SH!#@7?+5LyFfO<91bZ3->VhB{6X!Lc&@jMy+1`@7(QT@@pkFEutZ?0~ znhA4%g2K$o@}-+kFkGbT0ZYk~tIpn`!7y%)&KKTRf6fP+(msDm2Wn$yv*M-1lSdS- z5gO6_FqxsMiyA^2r*sa}EH?GJN^LKk94g*l4wyaYuhN}A)iB(w z`5K1yZz*WgB&krb%982jm5XtS9SetOc2{T)W^);49m(SjCZ~74Z#4kPA@qs9cPs$! z^>yal3sZT-WASLze{A}8#f-nA!wk8?rpXgMx5=MBg|`nLi_Q1+l!C#CUd(C&XfFb| zA1MKxg`PF9{V|%m>)Tu2c&tzcbUDL1##Xz|bvr+z=HtHVT!-V6bhT*9%b-l4%LML_ znNK|k1Zp&7)eUOMwy=hy6(u*L|7wAe-l_5IG*zga?(#|)!fzqlt;$X!EvI0IC@m46 zTR}AqvbIC)Xy=-VnJ5P-nNq$EE(%f7D`H%| z=U6+heaz`!a<*QtNAoD&mO^e9l9%DKq&U?r;d1@Cz_vd5AIX>TqSe6vDk4>#{!4WG z_$xNiwwd}3o1-b)3hwyom8D+s2bDRw(j;bKdLxDKuU=(L2UM3!m8p#swBc!l7{RVL z{)iJ*{F6)*$8yaz!Ft*LnR@vUr32~khq0MCVG_mXuPUXd;#F_QBk>!#Lf(|DzBjE2 z&1V!CkCi%Tpygc+Xb>L2txE~%_#dw}Nd{MD?8g*}+%k1}In88pD^{?6xw9Qhh{>AP z)Y9Iex@V@nfRg#-DIJ-`qPCWwRMA<=dYvoavObMB?%1$AwMnTos=<>$g z;UK4!x8@p#XNoV`_q48-t1yO*FOPMPu)YX}gEsdp?-vBJAZFg&)j9ibLwbtG;Rh*l z4TWvY6qAr)usnQq9vKsB>O0cctM3yaHw-D)>m@g6+=KG#vJP&!Qz8b6JkSAj&Jdx@ zVDWWtUa*8i_}M&c`py14>F{IbI^f8*Ull9k%Kab(d>)}HF{YN|^2x0Z!{VXLXEQDNH8?`8D_zOs*WbTP^wj?W5859O^vCOg zocW_eo7aH%=7732CjUDB)%_c)neIC~Fs48J`IH?(jd7!$Nk%T3|HFb8#Cn@tuRgcT9?euPtNmiJjl-k{cyG^CV>y=`RQJwPOk>l z%S7W{b)#9;4)E8xP`hyzg8BC?2UA{%vS@0b+uJl976v+?mW<35eeo^*BJY zJuEm*->%FsVE#Er6txtVFUV3i|Li$zU!_sI@#EcYRpSykQG})Vk71C>Mf8QB4n`lp zqGxi7(>UxJUM_g#f9B8`F^Qb%^6!$*ok(l)HfPUN z8}p9SL_0UIz%g5?7wqvD>Twbw%C) z8D6nRy`I}+=lstfX(e5OjU|w*OV6gi@H^7r51#XjDl~*Vn7VP~bh)nVM0ibvx^ZAj zpgPpq7vn_WrCyZ<~N$H1hK7$G9y875-h?0ATKy_4EVJJeW zy^iVpGTsb$wMw{i)e?m)@bB&Z>m;Wa+*S6b{SKFjaX=a^!V0RR*M);v@)yzmd_WJK zMzI0<9~ov}U84I9Xhtd+z~qygY5`PMlaqfKD0Vj*(%XU+feH(R^TKVA`ky!4DO+Xc z>VsoG6y7SlxC2J1fa-hD_m^LOq^OKGOrxCJKdCekdD5C$r0-CRw%H16URT%EnpheA z){01{{~a_n12>zLO2pyX{6RX|5>B(xwXEmSa}jG9+D0rV938$A49< zRHkxA&iTyY$>PV&D*_ur&ibho9$_bJE5tD^zh7~zbZz9Ja<}vXUcWtpTWfp!lSan1 z@*00~50(*@PHapRKhb{e&uVWIt2A~y-BHd6?4NIRKC5K+*Xfn4foh7do009+%ITct zS-wTM38K5age(YZQkVOx=9Kz&5s2nV)U8_K`I+elT6l!%pfw9UiWSV&R_#gI)ZgHn z$t+IkAT1u!KX&VEdbQC%d=j8M>$`Iq7S5u8P3EQH1mnlAd`d{^fh62KrUya-Zt$Rc zl<-(AV9D+)o*v#@z{)WRgCUxf>T9+G;Q4?ZxEAn348r(DHv^uv8Vtg2L8Y2|_()lc zhMcN$pst8jS&|BWNs2Ib(d&xfGyNovc zO-4)Pz2>Y`MI6ZcfqC1D zY{UjeA+zhH=s^O@;|QsfsiftPz7f z5O@J~(g)^Km;xc}f1X;Xkft_MN^S2>Z8IUNR+-8CKYtrkJae)zQ8p+}N?h3k@P5)O1(V$! z*N-`Gw4h?Qr5W6lX>Q6FU31A8=z_42{)TD|=S3PgU zKm>@OTxLh9-|a86Xs5wbv9tko3)opZFj{8yh0VRjQ#HQDVf|`S%+tIGl|pYAh0dw< zR1^pCl!94cgym&JPC`hB>5K12j`cCnL3gS#?2PTzd%Tx&b=fM#xIJ6{0_(RmP^8)U|`;1I&v^mD2G3 zBt?(~N>>V_7#8ko8m0i#oR`wUat%UcWw`;Fi;g!>WtAkR=*cj z<7g0A7+)y=`tnz0aQ%s=$A^*!z~Y~XKS#~m zn<%UWv}wcm>x`DGUN_LUjmo0O9*zbhJsA~``GM+viMd%jQ4y8^UW3)9&&YEbnK{6K zg?}{gxvVoR8$xPiJeeC_?G*QKxJ$eNDr3WWa%q$O0QR$Q! z;4657QH=v>bs55uNoN*AQPw(5ClyKgG9|*ZMZncfn5QSvA ze0)`g<-4ZweZ6f4_;1}9Vs?W9HY%wU0JEswslgQ>dZ|T%Wc|k%BnE-$2OEt=I{W*jr;{?VxJoYrH}FKgkS+Y zem2M#uJUYq{|kx`#X7`ns7FlKPfdmw7m)r*XyXMo>rXjKd~7OWeJFml3yiHi(aba}bZUidvU zM1!d8%a~_^zT)q=E9-Hg0ympk(66p8CeukY!P(AXH?K<3HC7GO?lBm|4S!IM)5ShC z+Sm`Cz>@LELXh)51Q_`fw^gMjojHPj+i9M%-8yL>e5neUZv|`r>tSsg7BJDLJyKMqf4XWC;paYS>(E==9b?)srP{V|LFEP9l^q!MbR_b~N{}x9 zSo+&g5eyxg3%Wc`$3HB5*JZsD4*#Y`K_`1l3_F=G)0Ql00|KQI9tzf0#63ZZQ;ci> zlF(GEs56;s4t;^)du^8{TS6Bov&E=NI>r}z`scE;)2^YU{2zruQ@vs6ErRsJd(!t( zwC*{g)nERG_d43Qi&^^pm*O+zfOt}@_6$}(ZUvTm?W8~_@PehTR{=@lFMzjC4-h+( zuOgbw)$#DnzfCYh@r$RAG8bX_i~POj&8#r754I`3^s)lkT6*mk1CV4u{XK>J#BETR zQYV+eixU&!uasb{yZyV$-5`wZRUjHpr-ZiB+d1=Ue(ua7ge{ta53A;mF2ETr7@5zGd=l{vbdR0QnKr0^?){Dlt7 zWC?*q1WKH+%tBpjqQn-1wLY%}D8J~&SpGOrn+`!$U+Dp#_pWL^4WmvRQSQ`6ot%iP{>)jhhEwE&hc8*P&G#Qy+uMtz@{Wiu(U6m*F6>Yw^L9X3AusaWnm$`ocxH=3&aSv#Ub6|=jHn8aeN3B{1Ya0hCKlUxSG!?-Ec>dZ92^)RcUwhLFy&a8K4yK; zNR*hh%f4)E;^m94P|{iYY>-Kos+C?HvF8fI!)U?1EPtMprfu{x4JSyhoLGM6 z%3xMn!R4zHy&0v5$3r-0ZtX>JQfP^X&cX~O=avVs+n<20ia}=F+NLOS*aMH(e&7H+ z7X+*$510tImqAv|k5UJ3)9nC}CywhIi$#A=kvqYJZ?+~5TY@Xcau-V0Ikvx3$=LP{~dP?;NF z>Xe>wioKCj{(^5;HYeIWJD0(kqtS@>vVwRV+`n|n&8WuaA^R`HN~RlG{|69!Wkc(RuX|vd_7q2|VfR*UDHwe~A)glKlLg0kLd*IRw3@_ea#x9}?Haf6*c9aOUhZ zM8HYb8^v|$r=LS3ePK3W{S1|U?${-{%B1q>75v2&6Ce_aQNl$98kuRy$-}*oKSyfw z0sR~3fjbDzUh*jaGk&h}9$HJ5^3b=v`o~#n?>FXKt06b+t&^?uhwC)Jo7=E6t|+AY z{4wol;pGV^`LtCH<9k`LIVq&`U8>|1*kg=!l=X1|<()5L-xE=rEAS*JEzc|?MW0nH zq<(siIqAAq-@|0a7N8{7dK}lmjXWr{zyW7kVy%UdyH9+gk0we&2cNy?Cv|~K1z-2k z)S%mNlq4$}l~b0i?C2Gxxx?jirnPxum_ABXD1M4txwM)#!jK2HZFV)Vw zt5()VT5j&3)PB;v`2M8$O^v!=o^Ix+SlogL2(zmGOYTi%p6 zd2bao7gby!L#ymTOS&j+iJi}zQb?_^`m`AgRT9|mu@)b%w?!A`5z_wftp0AYw(F44 z?8}UN8hHyW{4zSPVHUuHpM^R~V*6F;XRem675mB>2 ziS`db^VSc4`4`A^un9E}Kj*=+yb)lR@|OtRI=EEUbS6#Mbqi?ZW+CyoIMNMf0yBo^ zv;KB3KpD7&K}l{(wlV4QyeV<+sEd*Dr={&SqB2Z#FZ__^O)(>Xqa{Z-Fh}r=w0}qU z`W&FW*;e%P>KgCjzv_sB9NJ(%HW1c-ig}U~o?JKI#2!M56vjcAn91t!rL0c2SCBei ztIAe@)64e7l)U-YgJ4@BFiRU_i<`R;B6M z5{u84EgmNHpH%WzT4cZTQ0QV={FL^rN(ELNM?}KNgIP}mAhKD?N(Oh zAROF}%aiFgMeV^KSF&jM1!H%`L4oiy!lU|HhmF&or%SzP+vrhbZktEi^hfdJ><*!e zQx#?Vf9|9cLX#+0Mc6_&Hh6P1p`9r*~GDEvj=JHxIFTjofE?$;Dn6qqEg5DMxl-&~yPm5=~LE05u6k0)WhKrg!wX)I` ze>5t4&Z6DQzYc(FBXa?B|5qLK_2)HJsn+r~4>K&z!@Q;P1XU7jr{@K3nZRb)Lh|Q) zMn2u5{3@2a1j~Lv^3IRHhGRRj8SCo`?nOK*mM~uz3oixzS&yVISLsq*j{+bSRaWY? zLwAY$uT0NcKaG)iv;#+oF*$}=X0WbJN+*uuwPj+&UO#OxH*1ONX8J8-^}{U}mIGy8 zqA5zB2cP1UAOS0k{8ipwqKq+XquZZXNYs}#4sfY@4-e8Cdm}RIw)m`REofkhXSB#0 z#x-dRGQQmq487nn^kNiE|o-pSg-3aGPbtVh`7}w^2imo?|o{2Tg7k~Y%R?5!_DUg~( zb_kNDelhr7y6?DFWL{ptMP{_Crp`=LuD`2B6bxFSf&Y`(L+~#JCI5{+OvdwBIG7~R z{gVo&L9OMnLAM(s&pGqFHV_m6v+&tVA!8tqDVr2D%8n;%=LB)6`J+8i%$8cW$5-*V zXTfD9I$EVlPzd`rY|#8h%=N=9f3lg-?(-+@4)w(Ujxg1%T@v8uuoVBFwaGB1Ukg(5 zJ+!NaHLz!*6!{0!@*iMmqtwk;g^_opk2kC-;1?n~d{sZeO~rX_A#HgRL%%j+UR$qH zEYv0`Ko!XPVV~yuokk$VR?-}DLqs_Kj z;pkH=bxHg+ExkB}zXM<8#aRC)3ui)KDb31E1oDO+7EE zTRN>y1@6yj`4)JvNzI3N0dB#ZCSlqB|uaJLEvJ2djhs1{p? zx}_;C&|@7vGWAd*VjbyL1D|h?4CB=AK-+g+Fz4%C8I?6O;CD?0@__t2sIq4KUHGHI z#+vuFu?q&l355u$Z#0U$BZ(h{C6L9xW{!g?-#_Yds{c?1^=Gr>gVt5vDd+l?#Plyh zCsrF^WsN|M2$w>`G=<6*e0zffzZQTcumBtBtI+||Ra_qQf$^7zuAyFtod2u{XKhDX zeTKK-WR=l^d91z9N`01mzIUtLG!CoD|I?%`h)P|sLsJ(T*Icuwt!LJ#8PEVAkr-|g z1yadXz}H*24E~8CyMAI-+N5G^R5Grobrm9_SclbBF6rpvWBWo(Y^rl8mg;02fXa(C zb|OP&YP&om9M7zEn+CiToNM|sHRqW;bUPNI)yyOIbTW-(K8|JQC!G@XYVeeV0flhRwvHsLc5{AV+TR(B1Q=Kf?af}hh0Cmn%` zoU7@UG9=n<`Zm5bDp6YyjsoE+Z$=$MdKPM;-k#}4YmPA$y$bE+md!PLcyML2_*{m} z^gMQ?-lT6B;d#`H(GG7XzM&*`2(?!-){JT&n)MDZfiZAwe9y&cp8{Oz;(lzPm&Tw% z>z9pU8kX@=8#B0Xa!7O$yNpKAc~1nzgo(Jheb;JieF)YyE|F-A^wy2`4fyG(_f+<(0&o*%YJ6dEzWd|#(A=fw^BZ>gzhl)u2||BL<` zRpNV3@WjL2#Ax}PEKHX^ER?bnt7&g>MSE9aJs}`_#w1|OemG5XPkW_}6(PB6uY7WD zz%nV&PpEp5Bl^Pl{$wovactPerOct6HXnM%CUYJcOUiXo6Z2lg$*sU8pp@;L5Lukn;2r)EHrO()KSJT&gz&HGQJjTLKEdgK~cX#=T2jf+@g@=EK zZ5J=#K<0yrU)9KhL8KL$8&7cYk`p-l;nmJbxhsNEThVuK4ZTL=s{()6N3)8cnTZzF z@2iL}0#V}Cg4zGcekSIbShLSLW6bW&=>UJ)#nL5jb0hOL1cw`*)A;}kYo_erj~ zOt{O3%IQBf)~~1<(Pm^d(rjU`XmtW@eS#X>2yXaEpT1w9#cMWz%pYXmH!kS|dr_sQ zPQxHYpW)@U+2i0x1xBeg*eblSzeRUxQQ`B_<;<2LH9M&01q`aN475M0-TZZsM6PjGR3FVo0QPQhlQc$r{|3in+f~Uj~I`{ z^zdD(W(`~w1o8VJ=;CvX!3wvG1tH6&8yX$PRGJniBM%}w-$o<9aL78A``6T|`7!r= zbE@gwZpQhz@*&yJDtqF>bAwtGW8({XWlY>!^=B~bp#C?f3b#>)0~{(>K~o!u<$%i- z8r1L&2HNfb^E{WtzE>Gid)#b*==mtugjT_)I9S3F%!^agcl^Xc$z&>8!SDQkn0m*! z$p7zuc(QG`xn{$*Z8o>s++<_3?a8)nPqwvrvu)d3TkX~Vy7B$|?$5pRnD^_PCl$6q z4_Lv%8!!HFtpVp0mGW_%x!S8qh^zceYO4Kx&ta=swbWd(U{A;->B8X4gbQNt#!egn z$iE)s>X{`tzEFsXf2r~(zX=D?o^H%za#-nvRJhXkPnPYDvp4*3M`^fUi1Zx^+m|kJ z0Ml`X2M5j(;NwU&NEH*wjnBflvH8qbzR$uLA%uAaK*Ns(Hnj7ykzcUbwarT=r{hw zYh^@hxRl2j$T;0~aD){H3j`#p_HibYDy3v;;Mazbv#l`7kN>FM>A8BR&QIB?b6lR) z(z9O_v)<~~I#Y1Xfp_?vON~?El|GFsN?V{e*sex^uj1-Y`mJFM z;khbs^awAI>Gh}R+r zPLZ=gMi!Y{URinWciQt8u&uo{8M7c1YHEN z?5Ir?OL^EzJ-utW9Yaxf0os?>(5kQMpuJX0-zb}y%EG|sLVpIHoYgW8y_{YlN%Mw$ zBql|Y=K8s<{on6#pKGeCb9)yi&VF-B1j0kAqTU2J zmhgM4`2Q!#^^fy@B{?W&z_7_JXcTctpH;n3eyy(N1e-tk5dSuxY@XM~U>2snt2uGI z4R{i_=?Nz5ICV5KW#v@Ok?n%BqE&r+sK1sf<*hH@cLZF?a(u&n1Ve+_V-fe+_Q6>OalVESZF{U%#wtCA~A0|>aCduUBwF=gbtfz{} zkhK~YfCi@c{3+qDDvDG<-{t-HcK+DB7Uu3F9wIiB)!r@6973(B-7;LX=&q#UTE=W? zMz1cd?()s034)am2&kLvM+Rz| zEVN+Nf5X@3qW^b3-}O*QMqRcff;Bm{c8IFi<53%Ex4c_@JC6F&HoGX#M=nwfGfk=H zuuC3`3}1VCIJ$OysNRF2u=(QI>MPKw3;R^9yasqQ&K~0~m}9*Ei)EWD6sREdYVqfl zGDxBlLICyJ&Md{6db=p8M9`AW|NMmALSk(T-wJpCQ^CPs4hq3y--+Qp`+s+r;>asi z^zM?Q8*9{Txs#a5X>&DPIDhkx_rlm=jS1(*Vv$%p#O=~0onG4tZVDrcLp-`Z##2mO zRRbc&I6&h5d!-DubD6x(Xpc@|c>_vFzVejGBj{0!Z9&5FY`F;s-5u(<+hr^cPoKo; z*c<2Gql;gUKUVkL@jK)Cj2~;idK?v2l>~>WbywiUliJFDNu!TF*6657(8g|L#IPmu zldEotKsd|y*fXT3FuEli+S@|B=Xff`z!{PTBRiTlhv}6mh7lMSDFckhFwU5{0v5=n#;21pdW*iV-QPtG|p#d z<5_-aY1jHa;pO7}YTZ(u86 z>-nN(=M>&5fj9U(%d}3`h06Uo^xrP1M^@RskYl%TZyXK zTTh_1j|<>Po*bp zd>=EENSWO8oq%SF6aUA&@NfLkPyw~JCxA{RJ1kTXl+741$!N9 zb4On{A?f}~;d<=tBEPG8TfLn+zjY`vW+||AosHqk>dWfYO!M4gwikcv@jBTc+jX_gX#0H1r}9_qnS-+^#=1kx5ricI{dt{O3sQF!+xK=TT>P%( zL9SvF6pDGqL2jbd26n{6<@w|d(J`gojGSyoJ~0k~(=z_s?@e$NjIgeP^3dzZh`8j- zW2ZW2>4$Uap8}m6;VJr>-S64{er=bZS~2N$VX{U6<6U8tzWN{o7H@dH{gAKep@d&1 zq@a3vdkJLhf^U0>0gzmMGesMRFBN}E!5Wk?3x?zf)-I*Pvf3-(egJp(*?X;m3#h<> zz1uNy3wRO30nyNWOenATe}#lQhC9rzH(Bj=q^<;HEQB3k=gj8L1wvfF954rj-BG8lV10+=dF5`NNnx47@|w?COI( zKR3Wo)NPZ+*I@oU4Du0io%{J*iIx(sU7iw$^pN8U#)_Msp@}nUuzcSRG=@ok#9pb_ zOvh`FZ`oC&XP$0hW{;P#Z-WEynk?I1gqL#`~CNVWNW{-cO0w}hnlIcQtM0^ zVug^}!O(3PR$ypn`|3hD0Nv6g)lAxDUf#lf@q?AKV{Y!Qm>gL_z~}SShyC}Pt4D-| zS|uoS|23O|oQsd4u*Nc20i8j36W#RARnX&SWYhA4rn^a`DtoDtbC-K%&vxUfi z_!qS1v?La~GmmP7=E@=$!Y(P|VzjNtP2)XO!B};U0t8nEpd9gaB-=>ErlLE*28Q9P zZzq(mS>yOPw#gMJo)^0wKNV;vu%*UCE_s?EeRSg97;hmCOM>hs+nf>t4^i=EG)7BX zC1LT{vplC=e^o8KTsN=xDprMKw6?MHW#2OI{-!3*83SZ~cc7URP8DpOB*?6QgPJ4C+((9`A?*^n#f&quVmbZnjT|4Lpym zpJdU^J*uAf3;8S8zX_B-AKO=@y~?I&SZL-sJ?eo@AUnDIsF0R7%Ff9jDu6~VRE{7R z_yP*^!ygjm|Luj1M+vS%3)gPJdaevdcTp?Hvqu@E8+q?Ix!v^{L-RdsaBc0;;JTK^jNU@WlR-6iK|kk>xwojJlW zOb5sYou3FpQ2JY*eVTHWI39N<`ob)=%W{=1_Ja428*fYriBiC@knHfbD!AVg}vcPtbS+{iP}>d znAUu7ELs7ge{*)6aVS*f4!`8lSE`U~nPDt$ehYVC_>woYMU(o^>hmoC$Hs;P{Tr@u zB|&YLFWh(}K!>bGtV0)Ou^HP9%Ft+Iv78^3WlOJ-%!Cbkdl;*WV5>(6b6XSx!@XWH^!;h z5GV!E1Pp#O2?M(4EXtd#mRRb$tchZx)=_iz+}Jul%C+Yug8O_6M7BJNq>6|wrdx)Z zanGniOaP{&RI8cx?fQipFQE%wZUx#Dxz&MKp!L(I==f{{`@Dziz_kMK| z9x|Q*ZF1lM*v=Ixpw6q2rG2!25$vM2j<|o*$JF&=4>1>Lkc7{3kaSXT&<(r|Z#44x zIVNZ1{2b3Ui)Etrxw+>~P;2Su3MRplVQD_gzo2|$-mY`5o> z<<4_zAM!x-vHt0z27ZZFBxt|C-EtYQ=$NSnZkkx(q^(# zWJ6mm94=D)W*LQXsb%n5nVzQm!{%gwpSKG`vHZ2bTRLe_bxw8tOrc*_GU#|*hb{fJ z-wcyuozKbb(f*t>_G08hdA})n@a+?b$BMe>^&pwzQ)pOQ54x*QB6DnG)0Oas3mPV&y3<-6U+}ZpUx^38p%N zQ%pU0hTn!{IsXr6p*Bd=WT;us22&_`w)*kZ;&zDToAF^CDM3WS^ z)iSe8wI{XDdwUp+_L4%s7}G&N{5Cj*y8FRLq94nhR`V&Q!0)|l0S$}NTHbFC#X;Cz zu4RId-eOyUep!=1=sJ$yBGKqUw>dmt-DYgsySdHk5+=7)+Dg*9)N1UwRmBYBMzvFA zHxyYKU2>o}a@AdfEoC}WKwu4N$TgrD+7hj82cjcY=hGYTM|jow2W~B#Q!DdTA=exBCh2l=w4VuD^XRoi zh8Hvrfj0{GhrYf3!l*Pjx^!A<+k}(_+rEOiPhL5Z`7LB|cH%6oX>#U`{eQE}k6X5r zZmPSu`wcH@^U|a>-MJwA4@F2>pT!!O(dJwkhKmn6Oqe|0|E2Yv(GHXkqaLfYv+<}I z@X`G?97`i6wuwIx$i8?=`8=Hw(4s7TsSI6}mw>E+mrrfS$3&zmfdTJ*@$TBYB>sfV z5p^(Rn4d12OF%}>9p*u_y1m$h)^~t?%-@J@6+3mx*`0T8sEh_@gM;4R?6jtOJhF{F z>RQWMQA?W^#V><7{QJVe+F@ZL-t!Sn&9uy_%qZMSbG|EO<~CRZrT7~!F~G=yv)m&(C?#IQf$(;#dSoB+Orx%rV+jPL#oOsk_%RWqTPV;z^R@hV?bVIfDKS0K%IWiK&2SKnAk0A zRgi@fWu$FplAlw03SZf?AVk&GFiI1@)SRz;Z0%`@^qtOicEmHrq-kM5BrD0HxI7eV zz+2MyJkV`c@QkX$f^`pFhR#tel;hNZiV%k8FOo|ma$FlAXu*C^Ps>31nR~n+J&uVQ z$na#=V@?HQv`M&9`#8y-8YW&ae>jJYaQ zp^cf0IusoCh?(d2 zM0q)ORGj|5kx?Rc0JYYHaCMafR5qHG$ge70PgfC8&R|UDii;-K+D8!0?+F=EwETH! zGbkDB6C_5=oaN!r2}>8Pw5u-(H-(?c7=vANH3nulqE&~iby58mhPra{0?T^Dh1xeE zT?;z2r6uF;#>h4ly@Hx19Udo_3U6go2kWtX=wf8$QpWDOv1=ke*^S6sL+!EAZNNU- z!RSP6aDd_|ZS_}mo5n)6b(zXwNy6wLmx5b@rA=2hBSwg!lyN(myG>*?9|6!E&;=g2 zPmigG#Vs%tY%0m;I?hE>)}%<8GofzqF5Dd$eK=JTu;0-l3iQ%VNGuY7$uOd1aE;JMkJ3SLf8gC0uF_jxNN{@S14Sz8$=*Z%227J zl%3pNkSvVC-|s{Vqe4=or8l2IMv1s;^u{R2$??`Bburw_7;Dz9d>@;F(VlQu$oOwM zVO*f-Ti*(s={0R1^CYrU@*l^T`16#S!pN3P8tG*Q z8A-L@!NLI`;Fh}1j~RBPo%5Y?qJdkn+HuSG_3$$#-RNHAMH z*Pxq*zl;)Tgybo1uIROvVI%$hUV_WS0K}Nc0?sYd1wB7 z$cIrs)YcyK`hpg;d_(jkstr**V5BIdd}6n4In#1($qA??`Jjb|il)_chp2$MU#16V ze$)ADOw@*I#_{qz% zn;M>V)3f0Kv~kAKuvWTT535g(M>xoaGb^xsTn@Ry^t6x9{$eo~dM3^7TATNYlL{brG?5b;(2@E7CdOILKr0(WO&=&3{ctIhzy|@QBMm2ae}_ZvDrnXgj4i6RFKSC+9vheRmoX=zL2{YExD1=2BfKvlWeP0dI;uucqT< z0{7B3$0B*Lg<))?z1c4-R?=d7C^OfXy@3R|JLoyu6K6#B@*tasr*$vs0^BqU;9-+v7}6R11eia zx)7{xjUS@<@7V#5*yg*Vq&16Kzf^Cssf4*XKY~0JlQ>mHsL)QCNh}tcoJc2^bcyo7 zBw$~lMI)`Qw2Ho(h{TpTU4kfBHd5&0;%ob;|Dg{I;JrpL!03zmF^jDn$4^45jLqM?&;&9_td5&T*{N zgG~*YslrdY9c&#eF#2-afqA2fp> zommju7s(S%kDPwR++0-M^76ehJ4IJ^4`OyQ_bSf#3G)XzySL{@u{4cSqVib8%gQg) zZBL;np36^f=c}oi{?GxJ2Xx;udf(w-)R4RjwM^=07ca0_hS~HI(DP5#)$_<&Qn`Jr zfsvm9P6MgOf&pF48-c;cqMcY>L;hKnE)_XA?NL;sReLlAa6dLQ4dS6{24LbImV?WP5WF8>24MbX(nBCrXT%6u|qpHl&+LxT;{wcNOf zG0O0nYMR(YA!FvKKFK1stvY(pjPZ7OcQI_?ILW=pSOIFt#bGib#J2O9@@2k3V0%_!H zqwHIX6m|waIJ`~n%)t#}XPV0M6~ObYnPqoHkS{>Zd^Ors3UjB$jd|LIN?0a)zwfcdm)F%uH?2@UkA`D|v{ku1D2^lQ!c+l!EW_9m}{ zMTEBoPzrT39J)eV>9Ie}!>vqm2%kE)3Bt`!(HNCL!0Wex4hh&AsWE@s?)H#M zU*)PD)34r7RXYEHiIn9w&`9^CitwK@>iaWspa~i3^IY8+d!98lv|OR0IM?Qj8&F+q`VPTM^ z7Y&~C^h1nfYge-_-PsM_-Jyb!@)NH-z>v)#m{+?TUxN3-(yfq={frE?wJO$MY#05} z#h-x0e;YSGRW$)2WtH5B``v&3*dtJzsXxm)TWbS^={XJc@;B2}?;|!W>7DO95At_2 zJy@N=|Mr`5d;jg-OyR`w{M?DjF;H-NiqLXO!PL`gq=n zySD;Ti5&>$cDr3Lqoz1{+||QgtbCoR^L>A&jKDvJ3C}I=*QHt!8;jd4=+W{>dd44M zP@B8Ue8BZ0HVRa2(usUuLq2>-#};*)lDtLh2WVLX2p!`oQ>-&7GtzmSV&o4y{|yH7 zVU0jhW=m@ozE-&DHChRm*HB&JuRM*<=i9 zlxABK{PRDjIF!HMaCs})9FYh$9a*nk94b<*PxJ_E6l)VD75Zp4o6Rg-kZCtmB`gsj zPYG}26kdF@U0Q4(7MIXI8!c}i{oN1R$F)uQ;gIuqx-RNL( zDwM;%!+3gUZ8hoY->&`gwh5NVc;sVzj*YoNc8n#h3>(8b=KX38?n~IWu;Wl2#$XsZ ztI7vsZiuD6bU3HyD$8^xiscX7*l1>(3p)C6&B{4#q@L{qR1vqH9-0q{OKQG88p^UF zTonI12kNoZx6v=mQw*y`xhesM5QdXw#=Tqtpdt4mC#B^Aj*%?>`%5n-N7D zjZW1k{-;nAzE%BX7t6f2KXsE`pxTEC$qSE&<3H6#xNCwHzE@%^Pd~1wy>YDw5Tp|# z+4}0Fz*8xav}UVMd#ZXjK0(wCEipq{lO&Aoza_-cTJv|JnO^OqF{?%k^Bz}pd`+^y z)i=kxuvU(2Dk)NpuJn~O^m^wO+SmTgp|v|upY^ur=_l>;>T}mv2jwX3An5tmHt0tP zu^iIf$B(1FrBDCZX2Z(qhw^P(hdVw6bdF8&^FU0a%KEs&=YqsuU~#^ zhDr>ohS=gRQc#Nf1t4tZvDizxQn~`Ls^kX4F%9nUxzQ^U!vsj=!(HBW|1I09a(4`krhlm_L@CsyN~0 zTpE|`kSK{m+f-FpMh&3?q^qE(I?XDiYm&ZWJn=D=nD}e7u8!}-yEi<;$v>8m)vV$X6ebct8C&VSnf~^2EXuO zTsDjCJnoYOR}oWjElaO=F{QOP7WMlWl&~TUA*t&9c%Qi?$&oew#YtT!M0Bkzd-jvZ zcs1D?O=UHnKQw-XB}a4=R8_d_Ueq#zjD zy>BfgSYVLvoMCgZW2`M(tw9nQ3NIsjMAL?p^vb9qC085`41%~Gw$d>}Rx>7+dE&lP37=$xbk+bewgYT`O^$cBFFT z*ga>A9^08}1%HJac*$yqQOtYJoH`HPNLj`OD~Ta{bj{^=^fxB-oZO_F7cI0?V(B{h z4s;84MXHh$inUiDC%xgM3EYVPHaCj(lBUo=tv zLLl%g(6%&-b?%)_eSaJIe8FPB=Qw&`D-21p*B@5+pO;A3gv-ftJjV3jbat^=Q=RZl zB9DX45l$uEArF^&SnRx?cpde)cRFZP1$qQuWSWRYV;bF{lK}Kr+QSaX%YNhqkO*<+ zDLSB6jQ9cPLPJS&0HvQ(V0_{%Nz*})tXsw=ohixa)P6a^n$FV8v}P;tS;2k`^L4$e zk&(Va0jt;j%z0xJisaeJn`JZxt1>3Wms@`bdBn&fz18>M40 zrpx*mtPJr=Jh5`qDTCXJLi>CE#YGXN1<2O?Td%b&RNY1OBKVttlG4PML|+hB#1SNCDP2waM_2M^Q%sqw_qe_I)OHFBIoOlIOQm&qfhLP%^?c^4D(5 z%pB~JO?^)+A-vDFiq&qD4XOGA84k4MsIsj>k!FH;W{>l^=NHJEbCf2lE`gTDZaTfX z10&}M#5i$PK0F8fC?Abq|9{jhIbz+R2&kd-MbMT`dGw5c)6}1f5+8PU4J$t4(FPHC ztkhPpEh(43o!!CT{~bH;%RxSVr0Om)B!No96gR4t9Se!JXaUZ&SorGlq8rDQBta#G z03H1gtncPns!6&i`G@=}Bd`{PBw#4ht%Vx^jbZV7wKPGRv&HS#!cQoyQ2DfI3Z=+W z79n1;K%nx7A!Hh)HnXAT@1HT!yol}VYil^r2 zU3WkEP{%&G>&_PbzQS`xT4?gHTG;Tdw!y0dcG}{#ELP$RG5?p*KSCn8DJ`owNPgfn z?_~ODngRNn7*;;w#Y;O-*X;J_wwnrJ8)wdGbxxt#>}bEB-mT17R};4OJ7LC>d{TSux+^N5 z06Mm!ws=Wo&8{o4+QDMF+laEH&SWqp+hv;37NwjpT$!&F#uboi4#3@;>~wM`32dQ_ zNk2iGfQv_m4Rr;SlEIP%bfw1X0qv(6;;F66F4Kp0&7pmj^tVvD%7GKU!pujYnB$Pl{>mGsKC!tR~(kh_igbm+6(ZSsoxT&F6ZPE%oQn0PC!enB`lMQ|n;R ztycWw?6H4af<5eu%h>|&fV$*=OC~asFvEst^D*fnj~FysQBq@aDxk(P0c&?4lQkcA zftf~{{4FM>MMZm4IWJtpaU{ULEVInSk=vMhd=+Ap5pPDYola9_)4$Dnm?D zT74N%Ck#^Xs=k^C$0Th=7faypQ|P*pakbK1@ccMdC%#?Ty{}Vqvm(36Z3Swk?fGnB z{yX~e!4Ribot(JDV$L(`r$kt`L{(G*duxH%UY${+9w7Xl^z2X%ugC6MfO!{BWb>Ab zL1Gw@Zn|<3sKs83t*f7=v^eij&Y;FIvETvi{NUwuuMio7qz8L)QZ$YhjL8E zk(~vn&P41JOk^KrQY$Ot%r!15NMkhR&Y+_@Aa5kM-}CIt)b}4dMrl>mREH1yfNNrF zuII`ka}^i*=n7QMZy^HQP>**y*BX9YdfI1US=_H0P7j8GRJ8f9b>dWf zTy@cnf+AL5d_T!S*MCr;mhw<1IuRn)1<%$BIgoJV--jg5kU|Wh) zSIJU{OLx}K{|U2TXg7Z!brHC(Ar+r8P?`ePlbkuGmo?H8@s*1>-O3(~Czg=8BUC%6 z%J+%lCe=zxSbgc|yHyyypL4QWI_tJri@!59(3`2qO?&$*QKWM2DPsgQcQr^VlbHwS zJqBd!GYvL{N2ZjHvJQ;k{4Osn74q!l!*W6MxjW!^XL$P8WfWxZ8WVUz%+SKaXBG2 z^5&*C#Mz&_s-^+Op<=5NV6Z{D*zs_ervgxmDSd_ib#F}_S6u{K+XV0U%dCt?t2OmB zFsZqZUCatwmoC(2Ilndv$N0Nz1rg(C1^Mi%U-fW{JR9_dooU;s7OVm8ER0a6C|}z- zIjz#p6Jy{XilhH%O=>;X16JQ+x(3F_GyjLJf=9d}^;d=R0AM4+0!2l4R;Ddvb8tw6 z=X{kcs3_=V-v0~xg_7(O@jL*d-5w+ZO3t@brL$Y^^SN&h$8|CIOQ-<)7_s%# zLbr}7o{a<>xiGtxOx&(06Q)xitU`Cz?>nhR8%)wP+8hK-JdG6Jrs==9YLs*Lh z&pd$#KX+cz3C*z%bDv+KvDvfoUW{bex7)LLH)#s#I{3gmLq3Jlpq$zXkKGpOhYNu)1nwATX zg{RW2lnrOMFz-vgGCO-Ty*k#0*{jLcqfdjfBAc0}D4%8i);MV@u#uc7Sy-hsfOA(8 zqFiGL*Ccq6;8cSmmS`kepCsnjY24KPa_Ia z(c#~E9lhF9eo$j43bc=Q?AFU(?u!l-j(xw-2mxuNBLMQghUh%k+RW?fQG2NUDPc=r zD%$WS+Cr{+8fOZ)kN7@;eEdhYQFIf-tR&S#HekJ#f9=MU7-vZ~0kk7+F+2{WjeN8& zUK@-;%B~#${?x7d==M`J31q}vqFUgPcwqErrO#fn@CVcM=-%>H0Xt89YSiMjIaEVR z-gdly-s^_DQImCl#l3bU1AL05CqSv>#shBR*;OK0hImQ`+4N;T?>tv_tRc*?cd*CA zLH6+(jga_Xw3I$S_<;kUJ{7wHEvu=|c+DiG2wgOpC-T`&9OsC={G2$1yY`Kh@rUxqBuZEjvB>sw#PI=*k72dHSMtodlBCzDg-9y!^5Vp zvgRe$&ZECM)WL{dr-_*H!W(}UX$qfYUvD12Z~9DJ+H`wnsgf&2C0Ukkfx-;g&RJ@t zezqC+^s;E|oK}L(YP976dYHaou`X-cPKkvyhU_=5C75aBe*-oe>?3(eUwDg7*DJNK z>4;M0n(Dx;wNzm1YS3LIipwE!YytNtE@IkLeok~5+aIx5TQv+qoi|Nx1s%__ZGqU6 z?N6N<96`yM8jMq;Q(BaAh}jE^+--zkx0!Ddr!^gkP=L<2W$1PuGsv0^PVGb5P3`2UF`>i`DehouUY}Pm+e#r9& zhffNxEuYm;{o$c255YE*9);5Y3y&aGi1;8oirl~`-1t0Pc;i21@PgYJM1s};XGL;j4pfxzn+g*Ix=oR{pR**fjNr?i|waDK9}P1yoqsM zxW!Y(^wD|dR1pxjV-f%?(t~O=RKB!l63#9_#Ar^Rd*L`*v~t+a8k|JvQfo4cwky!o zZ|J!q$Sxj`zJ0i?v=7H2-(7eVBF#c%vDUPaV4^E?Ueh)noME<=Ub1vnCCWm!(;n6C z8#D#AdP8%ASax%?D_hI@HC8`hb61-F?iPvx7UBGL7XJq}|Hwb3h7_wt>DUsXWf?jc zIW%wc6CQ$gBQ74cah@`+`p?$Nw89*=^7n$)K6shifa@0=aLtzY4ZcP$9N^3yfhd=^ z<@#qh-oGsD<$m94PpRE`L6h6SbbLD26I=nIZ;_pj`6$2+!Y3Y1Xs`L{nk`f{Jb;xU z<(cAe5;?(OMFa;};z;Eq*=a9qQi_{ZH0Ntiu|2r@Q`xt{#I9nZ1KrYgx}@kn`;+4L zQ8BW_ar6KFNz~!0fGFScmcMDuCvk%1jbpNsnt8WQ5W!tZ-E>a=(&Up>^wfxgK5Zdg z)_A>?wkBE8hAlC?;sZz-wrYBYX*z6N2C57PTjqB{>pC=d(Asx+VqfIW`h#6?X;)s! z50~2!UX5Ji0{Qhw*H_3x|7I7YMLl)r_}7ZJo>r`Vp*Lwsdy<;_IXl$yoTqOUk6HAf z-imM2yvc<#?RkRESiNyE73=mHp)PSZP;{FT0o`fdORvVgipcG+c~NYu-AIrx)A_gi z7ak9OEYg+xE7EJjC0K-lu)sjw`+aL$cmWR#AY2pV5b4(7PfiYf_rGTrny5RPQ zr8UlFktIYpaZ2uyrMEQT7#A@^(hJ5QA8-Lw_q*+q?IkXN0F6}tFX3P@lJ`+SrHRld z1kbV(ZADH*UAL|J7^D`cx*w76UMYLN*S^m4pAt}v2BGm3&k5kMkFjX|2TP|m;7$Dl zKQe6op_q!ceeUp!*&iqTLxEhcd~>+k><>WxtWh|&e?b%|z|boDJ`JGZSo_*FXSI|~ zl=fT9H#3X`12X%XtwjP0W7AcB;+;{9DStj_Dxh>5iq-{a|EaHFZZas?u2Oi=#HIH% z5@_kwExU?VPx90)zHrT|YceIS$)DlyIQzO#6?0Z`0AA}D>l5MTZrWH2ajD+U(<#k0 z2>j?=v{5)+eGH4Tx*Pss^yzsq3no6$8eoC~`Fd~D$NPoHTgL2kb%bus+7CDC)oKv> z;ftk}1t{6;I2MC5lJJlk{1uA*`VJLN2u^%knc41-;NK=o!8pay+0Tt^{+iRhOpMKd zy5%PjZ0$AicJ%Nd@fk`HqE>e+a2R0_OBBAIJ&YDo9r5t>@2|0++6HW>0ulGVVl*k> zR1c=u8zyM2Y`Byq{0Hjp=oUv(=kRS1Y*qE$(f0tldKV6C7|)N8Oymo%R6+N4 zxyyA%V|Aid-fWp{ta%by_yp_=O{5WTual%ui`?H+7^zT0L)jh2Mzs z6Aqlgs~M6&Fk!Go`kStNpd_^Ui(qqC*I;j_>&>q23~Am}#TbhnHh)nk8v~-YkL2GQ z+coRcaG-^N))}@=+YzSfli+Yu^-}qz$ez*FpzOQ*)z0@S7HzoQ(~E;fspod^#AKiU zQ>3%Gs<}|W;_OJ?&HAA4+e#8rHmRneAlq@kbaD8W*@$2SI^@UKQ!16+NlDfdMaWVH z2FE**95k~=Lu&x>1R$BG9n?pkZj0q@l2IJI(7Dcn0={($n(TVj3&NZ1D++Ywoz>94 zsp|v#(R{=WR_6e`KN>{N)W7^=%|p!gc3pzh>{4>a!`IiVK#Wo(R%gewbmL)Lu^ss5 zIXZjd@{;7oF_fnhEw1X7a@F?%oWLTA>EZ@-q%tikcb5l-^^mCrx$YrUqdrGAN+4d3 zI#DlgZtM;pKx3GXn>-ilm%?{9-VkS5akSLeJP5|P7r*I94LT1G$uY+W!|hcp4n~&l zJoo=eAKbTNqluvEW1J?5I>kWA-jdbC0TjPgP`gZXe(y?eS5+o<7nRIc8oo`Vcu_;4 zk@)JcDH%;e2^KJ!0T!esvq0U(#g!l89^nq>NvAu zJk?@iOFWfKuNnjNA`IcGp!qm!tF7m|AsWcyw>Q1fW_O;63|vCbX6cb_L2<1JlU5O5 z_|irrk`dXcvb1vw>r?NJ1qE^MJwd+L>RF{=7?=EF@`s2@3(X}ztEI{>YxtVf`#urIS(DKf z%ansd8b)PkmdUrm>>=>GHpC$hN^h24S*#r0h3rsDpgiL}!Ridel+YM%v|8 zzB9mR<6$xAEd){^bwQ(o7ZwVa#Gbm5LeFZ7&cpx>w+^DN>r)s#j?S_5TeVJrI{6PF zwf7Ct_Jx#v_g})x?Y)mg%2+(AoV*vZ4JbXsa$=W@lcgg4WNr#uGneozlOdMs#sSX% za+(puP$dy@#i8*ux>x!ml40Q7?Qd`8c2gCaF7n%Bac#?q|I7pO@uk#eL|H@_B&7S8 zizDQE;H|}ZmbGn0dB2;^RUtSNMy-*v`)?fHRGuT5mct^G*1zT(@7nTJI)at^<`{*c zV`yZAscXvIF<#@)t;NcWEBA$!{J_;dy4hNzs|%8}Yn$y)9xcr~+^v@$O?58eq-!+9 z8U)J)9cPA-=zsWVZI99Suxyn*Hvi#a=9^Z{G!A;vV0pmk4&@%8Bq=GLdWq7YNn88g z3$}hA2V$jsn1F9@M38$IxVB(hpFfTwVTvs%??--$Pd{>9Do!yAR(zj73R2@(-u4|N6xtgWN#jKmqbEa;-8r1$yKb_3 zlW>2^+zOG3)kpCmjKyW~%<_-RjR@9)t&MTB_xg(~==qQSdQ;&YH>Om(SH=8KB0vqv z(0HZV`;HgMD4Ps(c6;Tls%{X<2Sp=wfG%<3M~7v?yZrw}#?i;hnO-cFWSDkWLsV>a zWPA$*^`Vk@Rj8E-pwUK8iVspa1w=t~75sUh>qDf_iCz=_N(JRd4EKyBqMN7BoTEh! zK^{$tL`0s|Q*54>tDH-l4SVdYCnDc+K%?@^QJ&8j>NKCG?rMJxY=3fQ`$~EErjvv|1l!`PN=U1;cBax=A8o0TPhB0q~5i(%uPWC zmNWQTaKU53%L&53GBpjjU%Q?Sg!Sg|jp!H0293S#zG&NCP`&N#OGO?$o>!XiwI4=f zqsU5*&%qBML3B50LBTTSnzS(uIwwf2ve!FT=oQu2pq!=UG+3@@5wb_yc(>`?n=q_? zzP9&_tmk#6eeL(M^^W(a!|x7Qesz9-S5laC=Iu%F?)UGS`A^U5R&HM?#Vb3{Tg{$* z@(-9c6XGG*aVu4yK0_L?kTOx(UWZf;v*q*up_|4$C03%A1%g*8o6#a^5>7s28uf?f z$sG8@afx8Dg3J18%WaOAmGI!kfu*C!-IuB^*3NhNxL^IfD6fO`lAp$mar}@_kLwka ztt!+w|1LZCRgk?rFnYQrLVdHlGqzjE;Y;bP`Q}L-FpBY$BzAZ8(`SAuhvcKNP0}vM zz7?R~%d*(}`;^RdOS+ZM{E6mHWCb@KlIKv3BPlih{ zGJ03s6#9|Z8Ks&(7_0klrFkU~;!@OP8}elIWud|dkD1ieBI~-LAs%Z=z3^(%IR770 zf8iD7|8-EZba!`m^UVEw*804^ zYt4T!>w2Ai_SsPc!a?#`uNeLQXFdU`7jdCSp3F)jbCD7)q^oFm??y zdcO+&_-WUwHTBY7bu_$)y5>v%73fE7DBk2D`jPgbHUywtfmQ*MF$U6lVs?C8KB6=>e#tv zDN7B|e53{>5}5F4{QmGdjs!oaMuZJbjqxnVjjOOR;=+APmeY=?IpBc})2l!3X)4ij z4a)(Kk>r4=X{h}w){*UstPK19ia6KE$X8SY75dfN%>x@X;wSTgw^Y!zDpM$(;1&r*?v%L|?Rvm$kB9S)WC8|kIuP^Lv;D@58RK<|hri%q* zJHhPMrH}uuGz8d@S#l~&vECDgsCw?zKh~)L5gLh0fwJQAhYB4?MuxW@UMa=R1A4Dr zy*PTlXd7azMo@%&^jGGJ3Ph?kRDXCkWj*5nG6jf{(dOG+mSdBEbFB0Ed$*j83gR3p6)M30O`GS-;vto%ge)fHBt1( zQ_r(s-`on4vy~Dm^)?p4R0;am>mSs-L}PxDLOJkY@xK)e$Kz5|>`o*W65IkWx=>Xo z-$OHkdb2hz0OCIAVlQWH-*5b$xdlFWatpdfMK0bc#yj0amw3s0jO>^IFftIXVP&KY z1EeJ%teqx{w~rk4aDm_=6-I{AXa8e;qQh~+46_}APpL6pZ!Z30`67ewlK?UMEj1qX zs~{mNg5UEqr25b@UjpX(nJp3nehz&Wqo*MTYDvGv?) zd+U4om#DrN*_`B^TgSJ z-V%ZYB)9CgF15O$CncsqwG>r(GZI?R$~af{=W>$f^_;-58;wJl+y5nQC7JpVl^yz$ z0!o{7WZ~(hnM9!{>&=KBg;MT^&UHU4*(683e|cOL38F~-v6|Z0E72b?&st0gnyk{Npv(LIJm%`i5iQ7@Jj{v9xDvVFp_DPCwc>uorMcgAlxEQ-<%g*y3`=a@_ zF}PV_T08AK&Nn<43jJ&NnG|EC2grgh*WWj98~d>`0HN0+V&6n88MuGqw^L3k{C5M9 zz-NW!LCAXS?|aGQwaw&)SjWYc!>tD+0?IL|?Xb*UH$j|qY5f4O2xiW~S^$8jtLgB;G4kZjUR6A9%YwOBg#>Z!_x}aX=Sc%RcALY?rylw#>r+ zB(NP9DQuf8aNc(y>XBmXl}71cL`EUV@h+0)RN6*w1NfI!gJ!w`NV)P>iN-x@PwGa9 zq}cm2&{a(5?>8myijTpFQ?;gMMaU_#upz$;TFI6MuKrd|Xb)zIH#N?E@~V^N@O&8} zRlgOJ+8YdmocIb(Lt;czvTDWeC|#J^b;EtGq@jmv=VJnH6{T)#pcbBZCrbAhHatvYF!v+WLS z#C-oe6##}B1qJ7y3T7Yzwpy_pG;UFr)g?N<)qbUeDD6^r3&_?d*rM(EtW`F?()=x8@L`^AU6D>n|<%T|f_W4hxa4A(% zG(;JJzG~HB`N(DN?1;L$MS_%KecRflqxeukfy;0McN&NMb`eK88rLf4aEgb1Eyct? zE%Tatx#7jE=BMF`P!IdAl^N^bC~2AYA5W(lnZEbEMb?QZuM=09W)Gp%3L4*MWdyGh zrHb-WYhp>0o?5@}0|}q*j|M`pw1{YIidIyjaMrWD6H$%xaE0yOOhZ->ws3>H*ity{ z)8!YKvglMF+T1^lt02(fT#Gg;OE~LUjM0=+A(94Yv}ooo5-xsT;&@G}yzZ~~RGo>W zox1@kgQKpww5AQcn6xs;Yg)cSUEOOyp<#3L1Q{~$quz!QE!-_4(gtvQHUjsAez28r zoB1#X(VqGXrX=3q3)I`hOi?G-#Ge4<$hOY4sMAvY0CH%b~}GCP7MIbkE3LLHByI`(h0fCJzfGDn8l4#e&)0 zWchi!rA%>bPr2ygX(Ej!R<}QX8ZnJx>Xj%sDm{Z`Sab;m=Tcd$GiRvjUon*?>|@yn ztBy1_A<9*xUcw0yQYVT@Gz>{eW!u2&r5>ak?qk|Itv20<_p9Bsi}f0OWmiJINV{Z2 zVHE^ob?e`NML*?>e5FWPnFPwKS;KLtnOfuk(|@EUo&Rj(FJczE)EMlW^*WONTbDn# zODr&4PhcPlvtgdrCYoKZM@n>x)hYd0*%&JW*s2A>aptHKuRLlAsda`rpG)PTgng)Y zLR6HWMJsByG+Y*`z~vDW*Xa67D67^UD5tk2fR|%_K)ScwG^A45@BV%JgvH#wJWy-u zMZflpeqgl=qjNLskVd72-OxWID~)BDr`ZC*4dEF3^UIYLEpF ztR8asLr4~kFvQg<6n40zRi8g-ZWi|eq=eJM#mX!_h}x9z4)dMERwAL28z1Yif0bI@ zr$R@~kIb-e791W6+fXPL^8XvHL9?`ISE%1n&_M*3vR>drKH)GtVNf#sn``yl``r*E z^KKtxbQCQvKw`TVnQDvz(fZ~YMYGNybyttyxC1q*<{x`63xmne~Z`4fgCm|#11E~~N7H)?ADAfOc zU>>w3cl0ArZ9hxYkEA&^a#lj1UHWyOrsmbpkXJRQ48kfFr`FoHx0xt#Vn^H4Q9sE= zq`6o|N(o*0#;)vTf#lzH1(!2LzjsmEkRaix@Nyia=CDKv|NZW&!2@HvTe*Q}@k**F zX8)#FhUozt>(}Jr9$aGTw||+ZD`DUrq;hnbjl=F1fwxS|j+svaQpGe8RqWO_Y%by& z^eFq|iI>|Rwnf^w2Tr#hQ)X)6^e!}-KFqz0q1EfZ`=l4dV00+zZ0ymQD|fn4M3RD% zT2ZFcbs#+%8WL@>`qi<3hHo*Gd3v0Dd)c+K3IhLodx0@E!TZ;E&7s`et5?kAY$Q@C zZ&R=Vm&(VDzvXc0)!8n~N-~uzA$vC0eB~>nyT|*hQs|gH zC-oMUljO^meLKYONsr^>co>ohK|cqr*7_U7QNp z)K9B-M*}q{5!2nwrZkqHinp{6IfXh9GQ)73Syn!X-ytPr6Gf9K$VXBJU&UyzM?_5X zFW;X>;z-}C0^p(%08MeGsIix+IYCL1w7R4XyK}IIdQ^|xilT*`MEtQtr*;sEEb_jQ z&3Yx0AUODRj3SLG*nSb{ng243lx8yxzJc4ph+C=R47Zg_o2 z7;V{Y44oO~>NrmR?25VqeS9&N?7WZ&q%OQ?e}u`BEIyFxeTbpezD zO4$)_7SLbxDsppC&IMPLc|xJYw0NeblN8dhPw`L;g92~`8=`Z*NIpwDfGaq*^BLcY z|Co+<&au9_g0k2IHrAR|r^x;rZTV*y#eqXmHhe%rm6n;YpOzHRo0VO~12v)WA9eaz zkTR&)t)cIX^=%)WI_=AcjsxDfM;`NLV)X+SHc9NH6@`ulCoSa4bH~+?{J zj&X|mcc2h?MpXw{-_lyK zrRimfI8px`*gk(RABHOyebgxV zQIp48snZ?bxrm-)?>`2TQ1TnaC`^LXfrX4+RfC3b6-cv6woCaFq8{}9qsy*8WlB1K zpdQeYko(VB_b=+7pX)@qJ`Z--sdf2hBDd3v)p+r!zFwS-8sHi-=O1E9P##B@T;lxY z^-s%|qc-o&dkey=FO2vUtn**4q`quyISyDCF$hlizVWB&m8H%*@1ju(W&A2*?^&bW z*m^Tp9!rUP;BoVlkT@52!uF+?PV&-3N=~SNBjv^%+TA?c%8 zWhIoAosV@N?i3>#ynV?sv?6LIuqSVBh2XMl z1hD*|u{&by<3b1^e&|HLz78Zo<~aW{e!;C6pv{#5$U6L0{xwV^ z5%Uc>CQ_8_2jCN7%hZ>q4GX$yu1)V=W58Y+gF2jT)ouG)IKlI`Myi@%Y|#vgwNAj` zD2s)+cJc1i5<9J|;S3#tSD3_s9X#!8%CCk0c@EJ&Deg%`PiuG>1o=Xha0xQ9$(CDc zix)D9j?lddCFjLm#L)*u-eCO6TA4sU;M|qpD?&DG;1~_(a4Cz-_!gm>u;#BktK(J? z(0jJX-+k1Z>Uvnl@R;M*Sjygu4lo3Eqt;Kp``KV{xJuAg6H57iG)s0CAa6hR zO)#Um_poGZlY4hke{npYU>~hI#BMA=Xly^XxgGno3H;vFkb2hy$u&x-gXK~_zB`FQ zUD?A+{W}=QsgL2nYg_imQbtR!hhLQ$d;;wt-if<-Qf+9|7!#o(Dyt}O-}q@VSNs^h zIw#JG08(3L9gQ5*0_F+8qMq9d>*un;vrfd>shb>yv|SBnd1wKH&Y|{{e2W;he2^UN zV`CSP+(CoA92WJ8gS2MdB=D7v{(JW;0dGIhs=d#fRkw6g@1vn?ligLZJHTqSGEiyk zW=apWRB8N015hxZdPd?dctfnY-)~q=7JDr-{YF#gGrc3#et8oo&!c5aE4FWpKZ=j} z)8)nvEekl*2R-Hfw)Xp!(62`Ad2G?L zMf*!Bg+4E_LWR!%{gXdFr4td#cvz+E$B4&_L-{e5!>I2?^W9~tidHglN`pU3R#sgV zUeReyVNO|nvLsjLA6ga2m78q6(_%P8R<3zwie&2c?oVCo?c2&s)x4?(H9}R@jFQOM zT<^ch^VZze#PsUVWq5Z4sig_pK7}cU|_%a#zId`-=IewPRan z_J0IW6t!&NMx)jqoKyWv;)I0I?-~p* zN1TW42Za?@xBkL3&%zUM`k)H59uZMRKvnCJw>R`e@NCp$cie_wzoCp)Ak@x%g>%g} zn@;LU=Xd>EL;uE=HUV}Vp6 z$Dcku`~MhUnB%X31f|S3au>8Mf`{j71#7OW8gdrkti#$RCQmPCitgz$t$GPIvv0C) zkE4kdIGxO`M4v`mJJm%7pb9`3vU`fi*M)^&56}55v31YHr)R}#KbtbK199?SS5=E~ zPdQ&n(|%&UweERiTTKT05=-e}9n};NB>PMpLi!U7KUBEuac)Nm)(0>s1j$OzMIfXe)!Yy>H(^@scQrLmXXS$iYDKh<0<_%=0JQ4wu)i0WAXnbr(xeSmZtdm)|l z#W5?Vk()+M%Pp%POaY3&`RR$Vt%sA7N%L8c*r(zBm{Tb`y4qu+@_!;@mqc~yKL>fG z1^qv}E&=8%KtKOVnVQHrDy$c-@x?Xkh?5{WN4{+BY8GwkU4^~$Uyn;vOq3|bICk9x zdQ&t8VN!iHzG6%w3$P}i>&Id`o6_F4X%MYuo~!HNb6>xr zjd&%rae;wWrDQwj(QwpV9f?MVC7v-?P`n^_W0JSNWuG)e+4l!xw4} z`0!nnHJJ z8CN$#y-&mZ%St|82f1iG>h&n2l+%W|AfBBQ-#4o#sHgk)7tjihOk8BcE2cJdn#~Ju z=jaZ$YabVRRLL;}2rEAxf8WU+sFo|H-jRg#_3Q!;RhkNP$c!RE;!!Dq@m+c3N|Z*T zi;Ekf3dd9{>^N#qq3I5F$so%)F;p`ZJ4ImPa}`qKE9kHozRG<2DK_CooSFKXL+mqC z(WEl_@4#)6O*_(ygqknGBA7!BMA^S=I%nz5^a(%O$@q7KI00RK}xW# zHlU&^|Hr6*H9eo(%pquN|!|;w3cA+#se$74>>BCvhMZe^Brw- zw+sgL#JIDSc}2~ikv>!+LyZ^X+B#zz`0_{O#@RpjYQPd#TZfM8x^@&Tq7t@ikA!nT zp!Pb6QROr=(dJCEvjj=s9~F*s1f?Q1#bXn+X&Y3U-&jD+*_t8{Rc^&ZJlDvl^{w|? zI{H#!dKd5fFi!D>?RPc;v$#W7#I=@1(Fa}!umWEA!OF<#| zNHuFB!G5P`P9P`BrJi{3??cq#2kDNozuzOV?^g0`Yx`*WIwNjmH_w`*Ew>Ui^N}uZ`{6%q8svUbRw;`AM##BtT~dsgzX)^u7;ld)QMz6tqR8 zV^G=U>N{cRJUfgi^_|zJsB1gpYd=BYCwR;A)qi7qGq2{ z^-}n7!QsO`g&0PSP##_2RhQDzhM|B$!7*slxNuqvY9_=6K%0ya$EV|DQu8-{jz$R( zKmAUWeV%cN>feSpncg{oZBnAlhAikfX`yQCOy)GP&l_jh9!VkE1sL+OM;+I;0+)S> z9T5E=kC7E4vd$FMIpAy=xwiU?tKW;jU~LQQ<0%u1NEr{|Ze zTn8t1yrPUZYOybyoj}u>hk^Lo;q`WymP|$C@GLSZk^99sx&$SSc#wndtM-KNbh=*} zCmq97rfEZbnq`Q(X|ZLCsuVpPj!y#AoPSufmm$4lL?$cE=N>0_s5Pk%fAZN6taZ=l zBf`Y05ipb;n(xh<7@-Qn9qsC6RR1&3Z zTGj}+mghehf|x|DQhUq-Xw!^u-sVxfAg|qotxzPh7vda|7~Th?%Y%r2fyS~OgVvLukI`1jxXd(4hjziDyCNCR>@N=|{%9uZMhMNdM)o%)d( zhh9|-o$jy}5%yZfE1X2V+2CxGhZbLW(oae@^>#TkJ@@swcaXMbKn+$cy+Oc-oy^uH z{pA~*xW8d#&AwbUZ}n~`MOoSDES<<{GdeNw-UV{n#cb$P_4J>P6+@@#10$mveIb>B zPSecQoqBVo4(%r6svRaHrTIVW@w+h_6sx+d--BRCD1WA>om8}-dHvg?X-@`cjve04 z6#}~NR@D(8t)t`sz$1lrI5U;as-vejMHhrXv(J^{4-LjiI)^LP_wR-DuSi=aJTJ5SiX$jv~ZKhJnUkDb%?yjUI6GDyR8Cz2@n;`Lw%DjQx0 ze}9ifUy0xY`0fA$0-@45tRWtY3^+xm_NKXl8or@LmT zyy3MRsr&kqp4^HfCZXk1o8I z!-P3R60+hIzEiOschV-h6n7qS?x42(V4EH@sF1J!cSsU@H~-2_O9M&i-9G!(0krRz zszRBIPnW_Va}+)+4nk7T=`;STDV~<^Y|7X_GvLekK5Dlr&OvDg|BXPEeq57&lMLD@ za219mJ(sX)_kGDE0vUX&4FK!oh8@1#O`LSRtnbyYfzo3&mT2x6=iXb^pTQ#!Eo*0P zw&cuCHxP-2+dWtG&;MF4Er|q{IqdG7uIE=(P=s z>3X+P&4*z)QkgyURe8f)J8_T(PVdqa5+-(S+jIwV2=#5C4fpLThMH6;Qy%54t9BMZ zt$QpwY@e=abKS9awTg|kUrfDdofEZw=P$nlts>UqidO#$Efpz;eLVk?RDb{x+99Yx zh@!l@HlgaTkUBMlS-fO;B;nGnX9j}UqrUzh(~NdEIGiWhxN+i!){xCAL$bgDN4D`mCB-)k`- zRG~Ar_~Bp6@=nDA80%bTBMsZ}W0b5FiFs(p2Me2Zmw2Fnf!={S5`6hQ<@4EswbS0j z4>dBnj(#wVqb|X^Zn~;{K&f#o(n4nLhSwMqu^&)HJh9sTs5gmD>u0WnD2~_~>DOVj z+oc7!RWZ+dAH$@Hw&%$xRLWt;hu;3DPY&@MYyt8oi*B@GsX@OZqys-{!PmK%OT$_b zc`}f@cA7DfqN8Tk^1{6 zI+EA^MLU6|hwuYpF7(^v86pZoe?+kvIm-Nfv6gvWJ!r5Vn^>VVkAgB{RRd6jqwpMQ zPyGR1g-zQG%s&^v=8c1tD|zxSfEmWd?ufQ_vfl{Mn%UYly!=0)ZI1xiZe2S|B_l4= z%c;-1bTF|8kKK|t2x&Uz3dF*7HC?>ucDiHX^bcDmk+7}(G=%7g7F!b^UHlc|@BzpZ z2SAqIH%L1zp(W&QCKBsbD1BN+{8Ud8m1CCRr`#I7IK(p2W&7_fRaW8w^VOWi<_2yk zWGdw7vidxKLO0hG9Bv49zt=iIUuUj3l)o1Gfcc+3E3;dN*zD(qmiAOxVC`U~X%yTa zkbCPZ#T^ezRDVZ0T1>8XxYC>ygJo?dukg0uGsH)Om}QX>+dI38PAaDL7(3LT3b^aS zl}IzJA)ayGHt=ASUbo^6+El6mBnyWfe)z`jetoC-q2m;z zM5&$SpLVjeM~|S!>jUPPO}!0(fC?K~WzgYNpv-+$)N zV@+Is)b~dzFSciojM+=R6!E|Xy=y`0 zl{Ni}`=&An@B|Gx8!YhiHsPeuA&vzICiYo#ypek1{KLw;!m{<}SRyxKeTb>4L19e7 z^VP(FPgRi%R)?ALhz|nT|Id)DB6}CaYNQqlrcu4wevjxtjEb4s2<27%XS32>a8BDe z7_(JYBB7T2tAI6#-cKr*%%Uj3n5zt|tPQ!tZ24@FT3h}3^&%9#SY?inN;!eJgTn07 zn6LROInA#HC#HTZ;X)~`vfz$S|0;P?QSRiI%ZYdGT}xhGoz5>rf%4=$8WqQfaxs+w9$B;5InYxqzG^O09doL!r#UV#pJQJ7#ikP?trH<4 zRa>KzUadlCKBbXQuU?wj)HSuf0sMz*uq!^{6!j z!cSGsErv>hfZ%J7o{W+b0B6q$6GpR8H(e0g=f3CJyGEpZiWLavqH%lJ3a8ei9>ePX zPk`2wAfTL9s_*FKbO(TtGQ-pn`sr__@NqZKgH^o?uY}T4lHUld9wbiOPHUDn6$v1= zIHrk0Nl%bUM5-lmF192NwTS;*Aa?|UT~=e!zbZEH&{-?n*j!0cOrUBT?dAP}VWUde zLn{pW{5Wito$o_p7S79F@{w;2H*K4QM zfBLu!nSZee+|M-lyp~-5o2}d7QZH)~?TkbV{aKI1 zkjwDvPrVswi+Dnv+I{k7x;t3gifI9&d#n-iAt+U>`I#hLRHzd~;N9_k<5YEP+C5^N zGnq!k)*ty}o8<19Fg=EVb7-HS*%UFTk)5qbk>viYO7#D-`gNQWfPde@8L{lJUqjyv z*uO7XOeh(B6E6SHPU$Mq<=PcYDAVO9o3k14?xTa#Q!E^8`R=`VlHy@kAIXY*|6RpU zhxk0D>FP0l=E4H6Ol_kzS8c^Qnu0g%$q+o(rs2(PgbGKpba{f_`cjBO=43P~(~Dv8 zD5aa^s6pSZ^P2TeRYYfI=K7}MW&o{JuoAZwlv|}+?;BLqtNW{L>JOmX9lykmlFbKJ z7^JU4ooZ+SO?l@w>+m5A3|Bg1HUg#ztw65-1%h&gGRReW3}Y_?^CGc`Y01+iC<|rq z$#uJ6Z;H?erVh-XK0qm4?mu_+)JeU!VMD>LpY;;KXv*SK{U^kdra zIbIVJ4_D@3xHa{LU8DDu;*VHBAR*2wbKt?f-$Ld&Te}2IaTXKh?tmV{rxcz?Cv1eA zS*kJJFNw_~o^)U&9?gof#~IxaeC*%2axE^LraCYxS>n06KlY?IRrqgqPYQV;DHWSc z^L1OBBqC;Mf>&4mxq)o#IsRb}N!-}&PN2EuiV`MiNtt_!@A zGqtPp2uO>P4@I~1;u5YWaL#MNDs>buJ|+INwDuuBMH`J zi)dWS%IpcOb$ajPclNdf0B4uwwbB|P2PGd})*}&@$FY;CxBd`*jONBhPwaC0L@a1! zMlrHFXLwo3C_wWH9Ehd6wAmaLxO##zx=#+bINcXsd&c(_W$W0H$UuSZQ$J#~A2EU2 z8~?3phzPlF<^-NK(6qUnYDuu}wNnlk5SN!$WQ*uu#3dAJ8OgmsjxajJV?}vpwM|2v zGpWqf!=tl~esyyrLO2laVsy55RtW6x^Z~g7R}dO^Gsv?xHX?S9NmcI&fE4*9M%Q!8bzgka|6^Z* z;>`%e(LQ#@@c_mEm_>iJS_fYUBZ!c+UCn^2NV-*2nB82OYCrAo zOu)5@YBwsu}b6FdBX5^L_R%8)miI;vc)-j=nS~XmNI7^nUYSeZbuYzVrG%kMT zl@_6s(MZjwcnKTN?f^|?c+tSUytL9+^p zroZKNlD-6+?}jnTlO@YabXUnsFKE(Zbx?8kcd87U?PPyG4gZXfW}uI0+UDy4BGrli zm{Q4{r#|9}9;2 zoHN$VM4B6zH>|y#j=po$vFo_L4d7M;n2~nMCpqG(P;|GzBa&Xjw3=vt7fg zRw2SuR$nTR!#={hKpmS=%!JsguI*82_Dkl%_T?$wHY3VbvMA{yWH9Z+T&Br^yPn+M ztppS88TMhm@aH+SyIqX9PW&NS-kq;_|`t7FckNWffM zmUWuXr~MLM-m>lglDQ2F?Rp85kpsbK#`PU)Mx9{;OYa=1R2%QN@Ou&+94M5Eg-vg# zF1L4cVu`%^E)~_)xN<-cyr+M^3vm;@uOKGNpOK1qHYuM1>nI_a(I%DBE!tSjn^8m% zwj7h5ZGXAa+3Pd@u91GSbZ^1J| z4kyuQg|B;27Xl2b%&9`IJZdy&>@)54>v-Jc5P|oX6>ZI+?+o;|L<;kR;G&C9#!6rM zI`wzV0DWfkWRr%Ri{3a7I<$pzMBSdMb79S$)TBRF5}omB8YKZfhsC$}tm5qfor6JT z@UXqf|0`pv$6@?&bSBK1cj~co0C;J70jwS|>E4^yzMx4}rXwEnlNULdg6OOEsf)u0 z*1MKQn?BTwon&C?X1hqBrUh$j#c=0K!X-j9f>#yjc7`emQXvx4q5*OMtNOiYhc&Zr zB^RrM68jA%k{cIDH+3H8D~?%;jrZg1J8bbu`Zk?347e)PX>6=$Y_}NpQ|B3K=3>E1 z%r!3oeN|{7;+ipFeZ#XLsed!Sm;Bo2X;kc1WeAQ^@V8hbNMQz=LrzGfSPQ&BtN)GQ zK-#-F(%qk4uMXd06wzy+()gKpZhb>@@&^R(;}CjMtE-6){l4&2gD|Ml5MMz=)Bs6N zpY0{yeft#sxgl`^AV{mXB#sLc*l#cbM#+DW#MPR6mK> zEwj16oWuL8CH~zNOEtg*+&cKmV~Cw{ZawU{1yf6KP3_-oEt9X=C96VQGuDXWPp=G$ zrAK5DrCiWjGF{}K2`$Y4%E`)QGD`5@NKib@Dsw7%<#=KsbNWuhpbnP%{!ZLx?sT36 z`F<~TKTl~y^A#Lbg^U4^N3!jLw%y!yh;vdD^q`Mw)8 zpgz$-35u;5prqyJDI-8TITBOtw)q05eWR;2=2GmFr qmlrXh`@QOJ)DEI0*KKQe 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 Jgy;akKLJdH_rCxD literal 0 HcmV?d00001 diff --git a/public/lottie/analytics-light.lottie b/public/lottie/analytics-light.lottie new file mode 100644 index 0000000000000000000000000000000000000000..a348e5ae9bd2670e920d144250ebb303a7bc5199 GIT binary patch literal 2185 zcmZ{lc{mh`8pa1>5VB1($P$q;*_UZj_N6o!L$c2#S;oX*#+Kcbv6X}A#&8J{nJgXA zNQ9E`U?|6k?1M0pt-|TJ_m6Y$Irq8W_q@;h{_%a!^Zxg9MDYQ^002PnPyj&hxwdr5 zp;sSD^iaV*A$Wg35?L#VL<}*$+i|2*N8Bp0VwDNB?63?`zWG(mN+th|RwcWh4R`vn zUI{T4ZH?j6PevO=t2*#c#H92TbyR1KrR*T5i%$w8lOVA7`+KgQn0IOS#=j4zC_ejH z&C_QO)BbdXoA20~uqRa`si1xhA4#p$|0ny(Lh?EkRb~2*4yg(Tfq>9S1UUYst*D2R zfuH~Ya0&|m96xM(ST@*)j3WmO3n8Rb2@KT}OA3gq3*cnMXRmKeQ>VEIX@1-Da#ME} zcURbch78Ci+ojPX?nME}Cp04BSk|mgKB(NFd)<_?8~Id0pqx4l;l(%3t$gwd3250e_V^_i|blD!+!DwTO=JY+8Zg`4K1J1BJ))cJT+ zcVQBg$x}=?|Mf)<6cWP}j@gj=DuPtf_N&ZUl+E4`nXj|4;ca#)-)*^NPL&PPi9GeEgid>A=$eChxkP+4)=mDkhm}5iW`=!ZGK2whr6ThAR zMha(Q86eZSQ&)wYiy>Mf$Q=wHpzvJQOR{_^a_qTH~6s(0MC~|cQ<8y~A>F&e-d$u}4ETwUoo1G%;K;)$@k%{84#q1NYJM{F3R9!Ow2O%$+vND4 zXdZxcTS?qC?f_9_KVu4J#&ZR!N7F*`-7aKOX^o{_1q;j5H-8_`*T*^p+!s93qt-+9 zQTR#fy&0y&KD@t!y5=ADTF5e zmit-iL&5j~Pzw_NP*7ilLXpn6BdMEoWb-0iex+Wu$Q=Tao1%YwCiLc~-HQ+wi)F3V)u;4Er%a+!m=(j$AvcJVT6^r$RoS-CC{h$WOiL=-=1`gp-lv zb9IdJxuFTt5+3}3udo~c69tzDhiTq@iM#I9cD?d?Q#ian;YLPpB) zUsT3lUbkZfqDT>qr-g5OCa5A~EOezJByw6|T?S5aW)ffP3sAoKD975PG24;B79zRQ zQ^8&tR$Li9`#k|A$pOhlUXrTMmQaKlB`8_Dt(3saxe*QdcN^WKKu=Y0_acTI3^zT||Z zXw2@x@ZuHBeMmqo=C{li0VOxaVB(MII%4nqhU#P#&vB8|bY+OagB!Fyxe5A3So=GZ zT&)UH_eXro^Um1vD3czw8*V%2}|nN4PiGQu&^3v`Eq54cd~R|3Y8aXt-*DRXIt z7h429%Uo}pZ4TQ3-VEa14ytpFMVpr?Vrb_8>BWjSXebFqgWFioQ6pkjQMlNa8|Oot z!@|O@>=wVwd^&Gm^JH`wj?>FMYQD^9lrrFcx>aO+yO?g#3~$?V60G}n^Ubb~N}-7* z)a+Tn?1GTN)Gm))O&bQ@pLoq}>q#c#`*8Uzm)NG0TNlZ#m_-duo4SVa(QR@{J2H*? z-ACnRcD4;-gXr5%b!tO%QcK^}NpGRCQ$nFd)tq5ldQoD*Q`_oaURO-2+UND-7ZZA4 zYOKQtLNyV!t6)gT;ROKx uKkyGJ`d{-G{Qv#+@AUo8U+rQ16Ttt=Vn-A|=wHXYhkNVL*AEd60Q><7NZQ%} literal 0 HcmV?d00001 diff --git a/public/lottie/globe-dark.lottie b/public/lottie/globe-dark.lottie new file mode 100644 index 0000000000000000000000000000000000000000..b92bc8a85c73c9114b8d1fa9a2194109e32fef88 GIT binary patch literal 4051 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 literal 0 HcmV?d00001 diff --git a/public/lottie/globe-light.lottie b/public/lottie/globe-light.lottie new file mode 100644 index 0000000000000000000000000000000000000000..60f9b14d0907ecd801948a9eebb8a46ca923c539 GIT binary patch literal 4039 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 a|EaJ4x5+~FNl1U239oO|^%ST5z5NT_EP`MF literal 0 HcmV?d00001 diff --git a/public/lottie/groups-dark.lottie b/public/lottie/groups-dark.lottie new file mode 100644 index 0000000000000000000000000000000000000000..fad73c0e272789ed9e0b20f6e0582c6829939f0a GIT binary patch literal 5161 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 C3~+=1 literal 0 HcmV?d00001 diff --git a/public/lottie/groups-light.lottie b/public/lottie/groups-light.lottie new file mode 100644 index 0000000000000000000000000000000000000000..31d1777420616ef1b9194a5ae546c75c7c7a69c7 GIT binary patch literal 5147 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 r{jd5LAouUqf9GNU>-@z@b0|9SflV$E!Q literal 0 HcmV?d00001 diff --git a/public/lottie/label-dark.lottie b/public/lottie/label-dark.lottie new file mode 100644 index 0000000000000000000000000000000000000000..e985d619f4ebac95c89b1a7026fc8a8d9edb4866 GIT binary patch literal 2651 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 literal 0 HcmV?d00001 diff --git a/public/lottie/player.js b/public/lottie/player.js new file mode 100644 index 0000000000..1a2d57c7d6 --- /dev/null +++ b/public/lottie/player.js @@ -0,0 +1,2 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self)["dotlottie-player"]={})}(this,function(exports){"use strict";function _taggedTemplateLiteral(t,e){return e=e||t.slice(0),Object.freeze(Object.defineProperties(t,{raw:{value:Object.freeze(e)}}))}function __decorate(t,e,r,i){var s,n=arguments.length,a=n<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,r):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(t,e,r,i);else for(var o=t.length-1;0<=o;o--)(s=t[o])&&(a=(n<3?s(a):3{for(;e!==r;){const r=e.nextSibling;t.removeChild(e),e=r}},marker=`{{lit-${String(Math.random()).slice(2)}}}`,nodeMarker=``,markerRegex=new RegExp(marker+"|"+nodeMarker),boundAttributeSuffix="$lit$";class Template{constructor(i,r){this.parts=[],this.element=r;const s=[],n=[],a=document.createTreeWalker(r.content,133,null,!1);let t=0,o=-1,h=0;for(var{strings:l,values:{length:e}}=i;h{var r=t.length-e.length;return 0<=r&&t.slice(r)===e},isTemplatePartActive=t=>-1!==t.index,createMarker=()=>document.createComment(""),lastAttributeNameRegex=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/,walkerNodeFilter=133;function removeNodesFromTemplate(t,e){var{element:{content:t},parts:r}=t,i=document.createTreeWalker(t,walkerNodeFilter,null,!1);let s=nextActiveIndexInTemplateParts(r),n=r[s],a=-1,o=0;var h=[];let l=null;for(;i.nextNode();){a++;const t=i.currentNode;for(t.previousSibling===l&&(l=null),null!==(l=e.has(t)&&(h.push(t),null===l)?t:l)&&o++;void 0!==n&&n.index===a;)n.index=null!==l?-1:n.index-o,s=nextActiveIndexInTemplateParts(r,s),n=r[s]}h.forEach(t=>t.parentNode.removeChild(t))}const countNodes=t=>{let e=11===t.nodeType?0:1;for(var r=document.createTreeWalker(t,walkerNodeFilter,null,!1);r.nextNode();)e++;return e},nextActiveIndexInTemplateParts=(e,r=-1)=>{for(let t=r+1;t"function"==typeof t&&directives.has(t),noChange={},nothing={};class TemplateInstance{constructor(t,e,r){this.__parts=[],this.template=t,this.processor=e,this.options=r}update(t){let e=0;for(const r of this.__parts)void 0!==r&&r.setValue(t[e]),e++;for(const t of this.__parts)void 0!==t&&t.commit()}_clone(){const t=isCEPolyfill?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),e=[],r=this.template.parts,i=document.createTreeWalker(t,133,null,!1);let s,n=0,a=0,o=i.nextNode();for(;nnull===t||!("object"==typeof t||"function"==typeof t),isIterable=t=>Array.isArray(t)||!(!t||!t[Symbol.iterator]);class AttributeCommitter{constructor(t,e,r){this.dirty=!0,this.element=t,this.name=e,this.strings=r,this.parts=[];for(let t=0;t{try{var t={get capture(){return!(eventOptionsSupported=!0)}};window.addEventListener("test",t,t),window.removeEventListener("test",t,t)}catch(t){}})();class EventPart{constructor(t,e,r){this.value=void 0,this.__pendingValue=void 0,this.element=t,this.eventName=e,this.eventContext=r,this.__boundHandleEvent=t=>this.handleEvent(t)}setValue(t){this.__pendingValue=t}commit(){for(;isDirective(this.__pendingValue);){const t=this.__pendingValue;this.__pendingValue=noChange,t(this)}if(this.__pendingValue!==noChange){const t=this.__pendingValue,e=this.value,r=null==t||null!=e&&(t.capture!==e.capture||t.once!==e.once||t.passive!==e.passive),i=null!=t&&(null==e||r);r&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),i&&(this.__options=getOptions(t),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=t,this.__pendingValue=noChange}}handleEvent(t){"function"==typeof this.value?this.value.call(this.eventContext||this.element,t):this.value.handleEvent(t)}}const getOptions=t=>t&&(eventOptionsSupported?{capture:t.capture,passive:t.passive,once:t.once}:t.capture);function templateFactory(t){let e=templateCaches.get(t.type),r=(void 0===e&&(e={stringsArray:new WeakMap,keyString:new Map},templateCaches.set(t.type,e)),e.stringsArray.get(t.strings));var i;return void 0===r&&(i=t.strings.join(marker),void 0===(r=e.keyString.get(i))&&(r=new Template(t,t.getTemplateElement()),e.keyString.set(i,r)),e.stringsArray.set(t.strings,r)),r}const templateCaches=new Map,parts=new WeakMap,render=(t,e,r)=>{let i=parts.get(e);void 0===i&&(removeNodes(e,e.firstChild),parts.set(e,i=new NodePart(Object.assign({templateFactory:templateFactory},r))),i.appendInto(e)),i.setValue(t),i.commit()};class DefaultTemplateProcessor{handleAttributeExpressions(t,e,r,i){var s=e[0];return"."===s?new PropertyCommitter(t,e.slice(1),r).parts:"@"===s?[new EventPart(t,e.slice(1),i.eventContext)]:"?"===s?[new BooleanAttributePart(t,e.slice(1),r)]:new AttributeCommitter(t,e,r).parts}handleTextExpression(t){return new NodePart(t)}}const defaultTemplateProcessor=new DefaultTemplateProcessor,html=("undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.2.1"),(t,...e)=>new TemplateResult(t,e,"html",defaultTemplateProcessor)),getTemplateCacheKey=(t,e)=>t+"--"+e;let compatibleShadyCSSVersion=!0;void 0===window.ShadyCSS?compatibleShadyCSSVersion=!1:void 0===window.ShadyCSS.prepareTemplateDom&&(console.warn("Incompatible ShadyCSS version detected. Please update to at least @webcomponents/webcomponentsjs@2.0.2 and @webcomponents/shadycss@1.3.1."),compatibleShadyCSSVersion=!1);const shadyTemplateFactory=n=>t=>{const e=getTemplateCacheKey(t.type,n);let r=templateCaches.get(e),i=(void 0===r&&(r={stringsArray:new WeakMap,keyString:new Map},templateCaches.set(e,r)),r.stringsArray.get(t.strings));if(void 0===i){var s=t.strings.join(marker);if(void 0===(i=r.keyString.get(s))){const e=t.getTemplateElement();compatibleShadyCSSVersion&&window.ShadyCSS.prepareTemplateDom(e,n),i=new Template(t,e),r.keyString.set(s,i)}r.stringsArray.set(t.strings,i)}return i},TEMPLATE_TYPES=["html","svg"],removeStylesFromLitTemplates=e=>{TEMPLATE_TYPES.forEach(t=>{t=templateCaches.get(getTemplateCacheKey(t,e));void 0!==t&&t.keyString.forEach(t=>{const{content:e}=t["element"],r=new Set;Array.from(e.querySelectorAll("style")).forEach(t=>{r.add(t)}),removeNodesFromTemplate(t,r)})})},shadyRenderSet=new Set,prepareTemplateStyles=(t,e,r)=>{shadyRenderSet.add(t);var i=r?r.element:document.createElement("template"),s=e.querySelectorAll("style"),n=s["length"];if(0===n)window.ShadyCSS.prepareTemplateStyles(i,t);else{var a=document.createElement("style");for(let t=0;t{if(!r||"object"!=typeof r||!r.scopeName)throw new Error("The `scopeName` option is required.");var i=r.scopeName,s=parts.has(e),n=compatibleShadyCSSVersion&&11===e.nodeType&&!!e.host,a=n&&!shadyRenderSet.has(i),o=a?document.createDocumentFragment():e;if(render(t,o,Object.assign({templateFactory:shadyTemplateFactory(i)},r)),a){const t=parts.get(o),r=(parts.delete(o),t.value instanceof TemplateInstance?t.value.template:void 0);prepareTemplateStyles(i,o,r),removeNodes(e,e.firstChild),e.appendChild(o),parts.set(e,t)}!s&&n&&window.ShadyCSS.styleElement(e.host)};var _a;window.JSCompiler_renameProperty=(t,e)=>t;const defaultConverter={toAttribute(t,e){switch(e){case Boolean:return t?"":null;case Object:case Array:return null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){switch(e){case Boolean:return null!==t;case Number:return null===t?null:Number(t);case Object:case Array:return JSON.parse(t)}return t}},notEqual=(t,e)=>e!==t&&(e==e||t==t),defaultPropertyDeclaration={attribute:!0,type:String,converter:defaultConverter,reflect:!1,hasChanged:notEqual},STATE_HAS_UPDATED=1,STATE_UPDATE_REQUESTED=4,STATE_IS_REFLECTING_TO_ATTRIBUTE=8,STATE_IS_REFLECTING_TO_PROPERTY=16,finalized="finalized";class UpdatingElement extends HTMLElement{constructor(){super(),this._updateState=0,this._instanceProperties=void 0,this._updatePromise=new Promise(t=>this._enableUpdatingResolver=t),this._changedProperties=new Map,this._reflectingProperties=void 0,this.initialize()}static get observedAttributes(){this.finalize();const r=[];return this._classProperties.forEach((t,e)=>{t=this._attributeNameForProperty(e,t);void 0!==t&&(this._attributeToPropertyMap.set(t,e),r.push(t))}),r}static _ensureClassProperties(){var t;this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))||(this._classProperties=new Map,void 0!==(t=Object.getPrototypeOf(this)._classProperties)&&t.forEach((t,e)=>this._classProperties.set(e,t)))}static createProperty(t,e=defaultPropertyDeclaration){var r;this._ensureClassProperties(),this._classProperties.set(t,e),e.noAccessor||this.prototype.hasOwnProperty(t)||(r="symbol"==typeof t?Symbol():"__"+t,void 0!==(r=this.getPropertyDescriptor(t,r,e))&&Object.defineProperty(this.prototype,t,r))}static getPropertyDescriptor(r,i,t){return{get(){return this[i]},set(t){var e=this[r];this[i]=t,this._requestUpdate(r,e)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this._classProperties&&this._classProperties.get(t)||defaultPropertyDeclaration}static finalize(){const t=Object.getPrototypeOf(this);if(t.hasOwnProperty(finalized)||t.finalize(),this[finalized]=!0,this._ensureClassProperties(),this._attributeToPropertyMap=new Map,this.hasOwnProperty(JSCompiler_renameProperty("properties",this))){const t=this.properties,e=[...Object.getOwnPropertyNames(t),..."function"==typeof Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(t):[]];for(const r of e)this.createProperty(r,t[r])}}static _attributeNameForProperty(t,e){e=e.attribute;return!1===e?void 0:"string"==typeof e?e:"string"==typeof t?t.toLowerCase():void 0}static _valueHasChanged(t,e,r=notEqual){return r(t,e)}static _propertyValueFromAttribute(t,e){var r=e.type,e=e.converter||defaultConverter,e="function"==typeof e?e:e.fromAttribute;return e?e(t,r):t}static _propertyValueToAttribute(t,e){var r;if(void 0!==e.reflect)return r=e.type,((e=e.converter)&&e.toAttribute||defaultConverter.toAttribute)(t,r)}initialize(){this._saveInstanceProperties(),this._requestUpdate()}_saveInstanceProperties(){this.constructor._classProperties.forEach((t,e)=>{if(this.hasOwnProperty(e)){const t=this[e];delete this[e],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(e,t)}})}_applyInstanceProperties(){this._instanceProperties.forEach((t,e)=>this[e]=t),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){void 0!==this._enableUpdatingResolver&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(t,e,r){e!==r&&this._attributeToProperty(t,r)}_propertyToAttribute(t,e,r=defaultPropertyDeclaration){var i=this.constructor,s=i._attributeNameForProperty(t,r);if(void 0!==s){const t=i._propertyValueToAttribute(e,r);void 0!==t&&(this._updateState=this._updateState|STATE_IS_REFLECTING_TO_ATTRIBUTE,null==t?this.removeAttribute(s):this.setAttribute(s,t),this._updateState=this._updateState&~STATE_IS_REFLECTING_TO_ATTRIBUTE)}}_attributeToProperty(t,e){if(!(this._updateState&STATE_IS_REFLECTING_TO_ATTRIBUTE)){var r=this.constructor,i=r._attributeToPropertyMap.get(t);if(void 0!==i){const t=r.getPropertyOptions(i);this._updateState=this._updateState|STATE_IS_REFLECTING_TO_PROPERTY,this[i]=r._propertyValueFromAttribute(e,t),this._updateState=this._updateState&~STATE_IS_REFLECTING_TO_PROPERTY}}}_requestUpdate(t,e){let r=!0;var i,s;void 0!==t&&(s=(i=this.constructor).getPropertyOptions(t),i._valueHasChanged(this[t],e,s.hasChanged)?(this._changedProperties.has(t)||this._changedProperties.set(t,e),!0!==s.reflect||this._updateState&STATE_IS_REFLECTING_TO_PROPERTY||(void 0===this._reflectingProperties&&(this._reflectingProperties=new Map),this._reflectingProperties.set(t,s))):r=!1),!this._hasRequestedUpdate&&r&&(this._updatePromise=this._enqueueUpdate())}requestUpdate(t,e){return this._requestUpdate(t,e),this.updateComplete}async _enqueueUpdate(){this._updateState=this._updateState|STATE_UPDATE_REQUESTED;try{await this._updatePromise}catch(t){}var t=this.performUpdate();return null!=t&&await t,!this._hasRequestedUpdate}get _hasRequestedUpdate(){return this._updateState&STATE_UPDATE_REQUESTED}get hasUpdated(){return this._updateState&STATE_HAS_UPDATED}performUpdate(){this._instanceProperties&&this._applyInstanceProperties();let t=!1;var e=this._changedProperties;try{(t=this.shouldUpdate(e))?this.update(e):this._markUpdated()}catch(e){throw t=!1,this._markUpdated(),e}t&&(this._updateState&STATE_HAS_UPDATED||(this._updateState=this._updateState|STATE_HAS_UPDATED,this.firstUpdated(e)),this.updated(e))}_markUpdated(){this._changedProperties=new Map,this._updateState=this._updateState&~STATE_UPDATE_REQUESTED}get updateComplete(){return this._getUpdateComplete()}_getUpdateComplete(){return this._updatePromise}shouldUpdate(t){return!0}update(t){void 0!==this._reflectingProperties&&0this._propertyToAttribute(e,this[e],t)),this._reflectingProperties=void 0),this._markUpdated()}updated(t){}firstUpdated(t){}}_a=finalized,UpdatingElement[_a]=!0;const legacyCustomElement=(t,e)=>(window.customElements.define(t,e),e),standardCustomElement=(e,t)=>{var{kind:t,elements:r}=t;return{kind:t,elements:r,finisher(t){window.customElements.define(e,t)}}},customElement=e=>t=>("function"==typeof t?legacyCustomElement:standardCustomElement)(e,t),standardProperty=(e,r)=>"method"!==r.kind||!r.descriptor||"value"in r.descriptor?{kind:"field",key:Symbol(),placement:"own",descriptor:{},initializer(){"function"==typeof r.initializer&&(this[r.key]=r.initializer.call(this))},finisher(t){t.createProperty(r.key,e)}}:Object.assign(Object.assign({},r),{finisher(t){t.createProperty(r.key,e)}}),legacyProperty=(t,e,r)=>{e.constructor.createProperty(r,t)};function property(r){return(t,e)=>void 0!==e?legacyProperty(r,t,e):standardProperty(r,t)}function query(i){return(t,e)=>{var r={get(){return this.renderRoot.querySelector(i)},enumerable:!0,configurable:!0};return void 0!==e?legacyQuery(r,t,e):standardQuery(r,t)}}const legacyQuery=(t,e,r)=>{Object.defineProperty(e,r,t)},standardQuery=(t,e)=>({kind:"method",placement:"prototype",key:e.key,descriptor:t}),supportsAdoptingStyleSheets="adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,constructionToken=Symbol();class CSSResult{constructor(t,e){if(e!==constructionToken)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return void 0===this._styleSheet&&(supportsAdoptingStyleSheets?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}const textFromCSSResult=t=>{if(t instanceof CSSResult)return t.cssText;if("number"==typeof t)return t;throw new Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but + take care to ensure page security.`)},css=(i,...t)=>{t=t.reduce((t,e,r)=>t+textFromCSSResult(e)+i[r+1],i[0]);return new CSSResult(t,constructionToken)},renderNotImplemented=((window.litElementVersions||(window.litElementVersions=[])).push("2.3.1"),{});class LitElement extends UpdatingElement{static getStyles(){return this.styles}static _getUniqueStyles(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_styles",this))){var t=this.getStyles();if(void 0===t)this._styles=[];else if(Array.isArray(t)){const r=(t,e)=>t.reduceRight((t,e)=>Array.isArray(e)?r(e,t):(t.add(e),t),e),e=r(t,new Set),i=[];e.forEach(t=>i.unshift(t)),this._styles=i}else this._styles=[t]}}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){var t=this.constructor._styles;0!==t.length&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow?supportsAdoptingStyleSheets?this.renderRoot.adoptedStyleSheets=t.map(t=>t.styleSheet):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(t.map(t=>t.cssText),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&void 0!==window.ShadyCSS&&window.ShadyCSS.styleElement(this)}update(t){var e=this.render();super.update(t),e!==renderNotImplemented&&this.constructor.render(e,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach(t=>{var e=document.createElement("style");e.textContent=t.cssText,this.renderRoot.appendChild(e)}))}render(){return renderNotImplemented}}LitElement.finalized=!0,LitElement.render=render$1;var commonjsGlobal="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function createCommonjsModule(t,e,r){return t(r={path:e,exports:{},require:function(t,e){return commonjsRequire(t,null==e?r.path:e)}},r.exports),r.exports}function commonjsRequire(){throw new Error("Dynamic requires are not currently supported by @rollup/plugin-commonjs")}var lottie_svg=createCommonjsModule(function(module){"undefined"!=typeof navigator&&function(t,e){module.exports?module.exports=e(t):(t.lottie=e(t),t.bodymovin=t.lottie)}(window||{},function(window){var svgNS="http://www.w3.org/2000/svg",locationHref="",initialDefaultFrame=-999999,subframeEnabled=!0,expressionsPlugin,isSafari=/^((?!chrome|android).)*safari/i.test(navigator.userAgent),bm_pow=Math.pow,bm_sqrt=Math.sqrt,bm_floor=Math.floor,bm_min=Math.min,BMMath={};function ProjectInterface(){return{}}!function(){for(var t=["abs","acos","acosh","asin","asinh","atan","atanh","atan2","ceil","cbrt","expm1","clz32","cos","cosh","exp","floor","fround","hypot","imul","log","log1p","log2","log10","max","min","pow","random","round","sign","sin","sinh","sqrt","tan","tanh","trunc","E","LN10","LN2","LOG10E","LOG2E","PI","SQRT1_2","SQRT2"],e=t.length,r=0;r>>=1;return(t+r)/e}var s=[],t=u(function t(e,r){var i,s=[],n=typeof e;if(r&&"object"==n)for(i in e)try{s.push(t(e[i],r-1))}catch(t){}return s.length?s:"string"==n?e:e+"\0"}((e=!0===e?{entropy:!0}:e||{}).entropy?[t,m(a)]:null===t?function(){try{var t=new Uint8Array(256);return(h.crypto||h.msCrypto).getRandomValues(t),m(t)}catch(t){var e=h.navigator,e=e&&e.plugins;return[+new Date,h,e,h.screen,m(a)]}}():t,3),s),n=new f(s);return i.int32=function(){return 0|n.g(4)},i.quick=function(){return n.g(4)/4294967296},i.double=i,u(m(n.S),a),(e.pass||r||function(t,e,r,i){return i&&(i.S&&d(i,n),t.state=function(){return d(n,{})}),r?(o.random=t,e):t})(i,t,"global"in e?e.global:this==o,e.state)},u(o.random(),a)}([],BMMath),function(){var t={getBezierEasing:function(t,e,r,i,s){s=s||("bez_"+t+"_"+e+"_"+r+"_"+i).replace(/\./g,"p");return n[s]||(t=new a([t,e,r,i]),n[s]=t)}},n={},e="function"==typeof Float32Array;function i(t,e){return 1-3*e+3*t}function P(t,e,r){return((i(e,r)*t+(3*r-6*e))*t+3*e)*t}function x(t,e,r){return 3*i(e,r)*t*t+2*(3*r-6*e)*t+3*e}function a(t){this._p=t,this._mSampleValues=new(e?Float32Array:Array)(11),this._precomputed=!1,this.get=this.get.bind(this)}return a.prototype={get:function(t){var e=this._p[0],r=this._p[1],i=this._p[2],s=this._p[3];return this._precomputed||this._precompute(),e===r&&i===s?t:0===t?0:1===t?1:P(this._getTForX(t),r,s)},_precompute:function(){var t=this._p[0],e=this._p[1],r=this._p[2],i=this._p[3];this._precomputed=!0,t===e&&r===i||this._calcSampleValues()},_calcSampleValues:function(){for(var t=this._p[0],e=this._p[2],r=0;r<11;++r)this._mSampleValues[r]=P(.1*r,t,e)},_getTForX:function(t){for(var e=this._p[0],r=this._p[2],i=this._mSampleValues,s=0,n=1;10!==n&&i[n]<=t;++n)s+=.1;var a=s+(t-i[--n])/(i[n+1]-i[n])*.1,o=x(a,e,r);if(.001<=o){for(var h=t,l=a,p=e,c=r,f=0;f<4;++f){var d=x(l,p,c);if(0===d)return l;l-=(P(l,p,c)-h)/d}return l}if(0===o)return a;for(var u,m,y=t,g=s,v=s+.1,_=e,b=r,S=0;0<(u=P(m=g+(v-g)/2,_,b)-y)?v=m:g=m,1e-7a?-1:1,l=!0;l;)if(i[n]<=a&&i[n+1]>a?(o=(a-i[n])/(i[n+1]-i[n]),l=!1):n+=h,n<0||s-1<=n){if(n===s-1)return r[n];l=!1}return r[n]+(r[n+1]-r[n])*o}var A=createTypedArray("float32",8);return{getSegmentsLength:function(t){for(var e=segments_length_pool.newElement(),r=t.c,i=t.v,s=t.o,n=t.i,a=t._length,o=e.lengths,h=0,l=0;le[0]||!(e[0]>t[0])&&(t[1]>e[1]||!(e[1]>t[1])&&(t[2]>e[2]||(e[2],t[2],0)))}function r(t){if(t.chars&&!o(h,t.v))for(var e,r,i,s,n=t.chars.length,a=0;a=n.t-i){s.h&&(s=n),o=0;break}if(n.t-i>t){o=h;break}h=r&&r<=t||this._caching.lastFrame=t&&(this._caching._lastKeyframeIndex=-1,this._caching.lastIndex=0),r=this.interpolateValue(t,this._caching),this.pv=r),this._caching.lastFrame=t,this.pv}function u(t){var e;if("unidimensional"===this.propType)e=t*this.mult,1e-5=this.p.keyframes[this.p.keyframes.length-1].t?(e=this.p.getValueAtTime(this.p.keyframes[this.p.keyframes.length-1].t/t,0),this.p.getValueAtTime((this.p.keyframes[this.p.keyframes.length-1].t-.05)/t,0)):(e=this.p.pv,this.p.getValueAtTime((this.p._caching.lastFrame+this.p.offsetTime-.01)/t,this.p.offsetTime)):this.px&&this.px.keyframes&&this.py.keyframes&&this.px.getValueAtTime&&this.py.getValueAtTime?(e=[],r=[],i=this.px,s=this.py,i._caching.lastFrame+i.offsetTime<=i.keyframes[0].t?(e[0]=i.getValueAtTime((i.keyframes[0].t+.01)/t,0),e[1]=s.getValueAtTime((s.keyframes[0].t+.01)/t,0),r[0]=i.getValueAtTime(i.keyframes[0].t/t,0),r[1]=s.getValueAtTime(s.keyframes[0].t/t,0)):i._caching.lastFrame+i.offsetTime>=i.keyframes[i.keyframes.length-1].t?(e[0]=i.getValueAtTime(i.keyframes[i.keyframes.length-1].t/t,0),e[1]=s.getValueAtTime(s.keyframes[s.keyframes.length-1].t/t,0),r[0]=i.getValueAtTime((i.keyframes[i.keyframes.length-1].t-.01)/t,0),r[1]=s.getValueAtTime((s.keyframes[s.keyframes.length-1].t-.01)/t,0)):(e=[i.pv,s.pv],r[0]=i.getValueAtTime((i._caching.lastFrame+i.offsetTime-.01)/t,i.offsetTime),r[1]=s.getValueAtTime((s._caching.lastFrame+s.offsetTime-.01)/t,s.offsetTime))):e=r=n,this.v.rotate(-Math.atan2(e[1]-r[1],e[0]-r[0]))),this.data.p&&this.data.p.s?this.data.p.z?this.v.translate(this.px.v,this.py.v,-this.pz.v):this.v.translate(this.px.v,this.py.v,0):this.v.translate(this.p.v[0],this.p.v[1],-this.p.v[2])),this.frameId=this.elem.globalData.frameId)},precalculateMatrix:function(){if(!this.a.k&&(this.pre.translate(-this.a.v[0],-this.a.v[1],this.a.v[2]),this.appliedTransformations=1,!this.s.effectsSequence.length)){if(this.pre.scale(this.s.v[0],this.s.v[1],this.s.v[2]),this.appliedTransformations=2,this.sk){if(this.sk.effectsSequence.length||this.sa.effectsSequence.length)return;this.pre.skewFromAxis(-this.sk.v,this.sa.v),this.appliedTransformations=3}this.r?this.r.effectsSequence.length||(this.pre.rotate(-this.r.v),this.appliedTransformations=4):this.rz.effectsSequence.length||this.ry.effectsSequence.length||this.rx.effectsSequence.length||this.or.effectsSequence.length||(this.pre.rotateZ(-this.rz.v).rotateY(this.ry.v).rotateX(this.rx.v).rotateZ(-this.or.v[2]).rotateY(this.or.v[1]).rotateX(this.or.v[0]),this.appliedTransformations=4)}},autoOrient:function(){}},extendPrototype([DynamicPropertyContainer],i),i.prototype.addDynamicProperty=function(t){this._addDynamicProperty(t),this.elem.addDynamicProperty(t),this._isDirty=!0},i.prototype._addDynamicProperty=DynamicPropertyContainer.prototype.addDynamicProperty,{getTransformProperty:function(t,e,r){return new i(t,e,r)}}}();function ShapePath(){this.c=!1,this._length=0,this._maxLength=8,this.v=createSizedArray(this._maxLength),this.o=createSizedArray(this._maxLength),this.i=createSizedArray(this._maxLength)}ShapePath.prototype.setPathData=function(t,e){this.c=t,this.setLength(e);for(var r=0;r=this._maxLength&&this.doubleArrayLength(),r){case"v":n=this.v;break;case"i":n=this.i;break;case"o":n=this.o}n[i]&&(!n[i]||s)||(n[i]=point_pool.newElement()),n[i][0]=t,n[i][1]=e},ShapePath.prototype.setTripleAt=function(t,e,r,i,s,n,a,o){this.setXYAt(t,e,"v",a,o),this.setXYAt(r,i,"o",a,o),this.setXYAt(s,n,"i",a,o)},ShapePath.prototype.reverse=function(){var t=new ShapePath,e=(t.setPathData(this.c,this._length),this.v),r=this.o,i=this.i,s=0;this.c&&(t.setTripleAt(e[0][0],e[0][1],i[0][0],i[0][1],r[0][0],r[0][1],0,!1),s=1);for(var n=this._length-1,a=this._length,o=s;o=c[c.length-1].t-this.offsetTime)i=(c[c.length-1].s?c[c.length-1].s:c[c.length-2].e)[0],s=!0;else{for(var f,d,u=p,m=c.length-1,y=!0;y&&(f=c[u],!((d=c[u+1]).t-this.offsetTime>t));)u=d.t-this.offsetTime?1:ti+r||(a=o.s*s<=i?0:(o.s*s-i)/r,o=o.e*s>=i+r?1:(o.e*s-i)/r,h.push([a,o]));return h.length||h.push([0,0]),h},TrimModifier.prototype.releasePathsData=function(t){for(var e=t.length,r=0;r(s=(1e.e){r.c=!1;break}e.s<=u&&e.e>=u+p.addedLength?(this.addSegment(f[i].v[s-1],f[i].o[s-1],f[i].i[s],f[i].v[s],r,a,y),y=!1):(h=bez.getNewSegment(f[i].v[s-1],f[i].v[s],f[i].o[s-1],f[i].i[s],(e.s-u)/p.addedLength,(e.e-u)/p.addedLength,o[s-1]),this.addSegmentFromArray(h,r,a,y),r.c=y=!1),u+=p.addedLength,a+=1}if(f[i].c&&o.length&&(p=o[s-1],u<=e.e?(l=o[s-1].addedLength,e.s<=u&&e.e>=u+l?(this.addSegment(f[i].v[s-1],f[i].o[s-1],f[i].i[0],f[i].v[0],r,a,y),y=!1):(h=bez.getNewSegment(f[i].v[s-1],f[i].v[0],f[i].o[s-1],f[i].i[0],(e.s-u)/l,(e.e-u)/l,o[s-1]),this.addSegmentFromArray(h,r,a,y),r.c=y=!1)):r.c=!1,u+=p.addedLength,a+=1),r._length&&(r.setXYAt(r.v[g][0],r.v[g][1],"i",g),r.setXYAt(r.v[r._length-1][0],r.v[r._length-1][1],"o",r._length-1)),u>e.e)break;i=o.length&&(s=0,o=h[n+=1]?h[n].points:p.v.c?h[n=s=0].points:(i-=r.partialLength,null)),o)&&(a=r,N=(r=o[s]).partialLength);x=m[b].an/2-m[b].add,u.translate(-x,0,0)}else x=m[b].an/2-m[b].add,u.translate(-x,0,0),u.translate(-c[0]*m[b].an/200,-c[1]*K/100,0);for(m[b].l,z=0;ze));)r+=1;return this.keysIndex!==r&&(this.keysIndex=r),this.data.d.k[this.keysIndex].s},TextProperty.prototype.buildFinalText=function(t){for(var e,r=FontManager.getCombinedCharacterCodes(),i=[],s=0,n=t.length;sthis.minimumFontSize&&O=p(n)&&(s=h(0,l(t-n<0?l(a,1)-(n-t):a-t,1))),i(s)))*this.a.v},getValue:function(t){this.iterateDynamicProperties(),this._mdf=t||this._mdf,this._currentTextLength=this.elem.textProperty.currentData.l.length||0,t&&2===this.data.r&&(this.e.v=this._currentTextLength);var t=2===this.data.r?1:100/this.data.totalChars,e=this.o.v/t,r=this.s.v/t+e,t=this.e.v/t+e;tt-this.layers[e].st&&this.buildItem(e),this.completeLayers=!!this.elements[e]&&this.completeLayers;this.checkPendingElements()},BaseRenderer.prototype.createItem=function(t){switch(t.ty){case 2:return this.createImage(t);case 0:return this.createComp(t);case 1:return this.createSolid(t);case 3:return this.createNull(t);case 4:return this.createShape(t);case 5:return this.createText(t);case 13:return this.createCamera(t)}return this.createNull(t)},BaseRenderer.prototype.createCamera=function(){throw new Error("You're using a 3d camera. Try the html renderer.")},BaseRenderer.prototype.buildAllItems=function(){for(var t=this.layers.length,e=0;et?!0!==this.isInRange&&(this.globalData._mdf=!0,this._mdf=!0,this.isInRange=!0,this.show()):!1!==this.isInRange&&(this.globalData._mdf=!0,this.isInRange=!1,this.hide())},renderRenderable:function(){for(var t=this.renderableComponents.length,e=0;ethis.animationData.op&&(this.animationData.op=t.op,this.totalFrames=Math.floor(t.op-this.animationData.ip));for(var e,r=this.animationData.layers,i=r.length,s=t.layers,n=s.length,a=0;athis.timeCompleted&&(this.currentFrame=this.timeCompleted),this.trigger("enterFrame"),this.renderFrame()},AnimationItem.prototype.renderFrame=function(){if(!1!==this.isLoaded)try{this.renderer.renderFrame(this.currentFrame+this.firstFrame)}catch(t){this.triggerRenderFrameError(t)}},AnimationItem.prototype.play=function(t){t&&this.name!=t||!0===this.isPaused&&(this.isPaused=!1,this._idle)&&(this._idle=!1,this.trigger("_active"))},AnimationItem.prototype.pause=function(t){t&&this.name!=t||!1===this.isPaused&&(this.isPaused=!0,this._idle=!0,this.trigger("_idle"))},AnimationItem.prototype.togglePause=function(t){t&&this.name!=t||(!0===this.isPaused?this.play():this.pause())},AnimationItem.prototype.stop=function(t){t&&this.name!=t||(this.pause(),this.playCount=0,this._completedLoop=!1,this.setCurrentRawFrameValue(0))},AnimationItem.prototype.goToAndStop=function(t,e,r){r&&this.name!=r||(e?this.setCurrentRawFrameValue(t):this.setCurrentRawFrameValue(t*this.frameModifier),this.pause())},AnimationItem.prototype.goToAndPlay=function(t,e,r){this.goToAndStop(t,e,r),this.play()},AnimationItem.prototype.advanceTime=function(t){var e;!0!==this.isPaused&&!1!==this.isLoaded&&(e=!1,(t=this.currentRawFrame+t*this.frameModifier)>=this.totalFrames-1&&0=this.totalFrames?(this.playCount+=1,this.checkSegments(t%this.totalFrames)||(this.setCurrentRawFrameValue(t%this.totalFrames),this._completedLoop=!0,this.trigger("loopComplete"))):this.setCurrentRawFrameValue(t):this.checkSegments(t>this.totalFrames?t%this.totalFrames:0)||(e=!0,t=this.totalFrames-1):t<0?this.checkSegments(t%this.totalFrames)||(!this.loop||this.playCount--<=0&&!0!==this.loop?(e=!0,t=0):(this.setCurrentRawFrameValue(this.totalFrames+t%this.totalFrames),this._completedLoop?this.trigger("loopComplete"):this._completedLoop=!0)):this.setCurrentRawFrameValue(t),e)&&(this.setCurrentRawFrameValue(t),this.pause(),this.trigger("complete"))},AnimationItem.prototype.adjustSegment=function(t,e){this.playCount=0,t[1]t[0]&&(this.frameModifier<0&&(this.playSpeed<0?this.setSpeed(-this.playSpeed):this.setDirection(1)),this.timeCompleted=this.totalFrames=t[1]-t[0],this.firstFrame=t[0],this.setCurrentRawFrameValue(.001+e)),this.trigger("segmentStart")},AnimationItem.prototype.setSegment=function(t,e){var r=-1;this.isPaused&&(this.currentRawFrame+this.firstFramee&&(r=e-t)),this.firstFrame=t,this.timeCompleted=this.totalFrames=e-t,-1!==r&&this.goToAndStop(r,!0)},AnimationItem.prototype.playSegments=function(t,e){if(e&&(this.segments.length=0),"object"==typeof t[0])for(var r=t.length,i=0;il.length-1)&&(e=l.length-1),i=p-(s=l[l.length-1-e].t)),"pingpong"===t){if(Math.floor((h-s)/i)%2!=0)return this.getValueAtTime((i-(h-s)%i+s)/this.comp.globalData.frameRate,0)}else{if("offset"===t){var c=this.getValueAtTime(s/this.comp.globalData.frameRate,0),f=this.getValueAtTime(p/this.comp.globalData.frameRate,0),d=this.getValueAtTime(((h-s)%i+s)/this.comp.globalData.frameRate,0),u=Math.floor((h-s)/i);if(this.pv.length){for(a=(o=new Array(c.length)).length,n=0;nl.length-1?l.length-1:e].t)-p,"pingpong"===t){if(Math.floor((p-h)/i)%2==0)return this.getValueAtTime(((p-h)%i+p)/this.comp.globalData.frameRate,0)}else{if("offset"===t){var c=this.getValueAtTime(p/this.comp.globalData.frameRate,0),f=this.getValueAtTime(s/this.comp.globalData.frameRate,0),d=this.getValueAtTime((i-(p-h)%i+p)/this.comp.globalData.frameRate,0),u=Math.floor((p-h)/i)+1;if(this.pv.length){for(a=(o=new Array(c.length)).length,n=0;ns){var h=n,l=r.c&&n===a-1?0:n+1,p=(s-o)/i[n].addedLength,c=bez.getPointInSegment(r.v[h],r.v[l],r.o[h],r.i[l],p,i[n]);break}o+=i[n].addedLength,n+=1}return c=c||(r.c?[r.v[0][0],r.v[0][1]]:[r.v[r._length-1][0],r.v[r._length-1][1]])},vectorOnPath:function(t,e,r){t=1==t?this.v.c?0:.999:t;var i=this.pointOnPath(t,e),t=this.pointOnPath(t+.001,e),e=t[0]-i[0],t=t[1]-i[1],i=Math.sqrt(Math.pow(e,2)+Math.pow(t,2));return 0===i?[0,0]:"tangent"===r?[e/i,t/i]:[-t/i,e/i]},tangentOnPath:function(t,e){return this.vectorOnPath(t,e,"tangent")},normalOnPath:function(t,e){return this.vectorOnPath(t,e,"normal")},setGroupProperty:expressionHelpers.setGroupProperty,getValueAtTime:expressionHelpers.getStaticValueAtTime},extendPrototype([r],t),extendPrototype([r],e),e.prototype.getValueAtTime=function(t){return this._cachingAtTime||(this._cachingAtTime={shapeValue:shape_pool.clone(this.pv),lastIndex:0,lastTime:initialDefaultFrame}),(t=(t*=this.elem.globalData.frameRate)-this.offsetTime)!==this._cachingAtTime.lastTime&&(this._cachingAtTime.lastIndex=this._cachingAtTime.lastTime>4,n=1>6:64,a=2>2)+f.charAt(s)+f.charAt(n)+f.charAt(a));return o.join("")},r.decode=function(t){var e,r,i,s,n,a=0,o=0;if("data:"===t.substr(0,"data:".length))throw new Error("Invalid base64 input, it looks like a data url.");var h,l=3*(t=t.replace(/[^A-Za-z0-9\+\/\=]/g,"")).length/4;if(t.charAt(t.length-1)===f.charAt(64)&&l--,t.charAt(t.length-2)===f.charAt(64)&&l--,l%1!=0)throw new Error("Invalid base64 input, bad content length.");for(h=new(p.uint8array?Uint8Array:Array)(0|l);a>4,r=(15&s)<<4|(s=f.indexOf(t.charAt(a++)))>>2,i=(3&s)<<6|(n=f.indexOf(t.charAt(a++))),h[o++]=e,64!==s&&(h[o++]=r),64!==n&&(h[o++]=i);return h}},{"./support":30,"./utils":32}],2:[function(t,e,r){var i=t("./external"),s=t("./stream/DataWorker"),n=t("./stream/DataLengthProbe"),a=t("./stream/Crc32Probe");function o(t,e,r,i,s){this.compressedSize=t,this.uncompressedSize=e,this.crc32=r,this.compression=i,this.compressedContent=s}n=t("./stream/DataLengthProbe"),o.prototype={getContentWorker:function(){var t=new s(i.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new n("data_length")),e=this;return t.on("end",function(){if(this.streamInfo.data_length!==e.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),t},getCompressedWorker:function(){return new s(i.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(t,e,r){return t.pipe(new a).pipe(new n("uncompressedSize")).pipe(e.compressWorker(r)).pipe(new n("compressedSize")).withStreamInfo("compression",e)},e.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,e,r){var i=t("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(t){return new i("STORE compression")},uncompressWorker:function(){return new i("STORE decompression")}},r.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,e,r){var i=t("./utils"),a=function(){for(var t=[],e=0;e<256;e++){for(var r=e,i=0;i<8;i++)r=1&r?3988292384^r>>>1:r>>>1;t[e]=r}return t}();e.exports=function(t,e){return void 0!==t&&t.length?("string"!==i.getTypeOf(t)?function(t,e,r){var i=a,s=0+r;t^=-1;for(var n=0;n>>8^i[255&(t^e[n])];return-1^t}:function(t,e,r){var i=a,s=0+r;t^=-1;for(var n=0;n>>8^i[255&(t^e.charCodeAt(n))];return-1^t})(0|e,t,t.length):0}},{"./utils":32}],5:[function(t,e,r){r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(t,e,r){t="undefined"!=typeof Promise?Promise:t("lie");e.exports={Promise:t}},{lie:37}],7:[function(t,e,r){var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,s=t("pako"),n=t("./utils"),a=t("./stream/GenericWorker"),o=i?"uint8array":"array";function h(t,e){a.call(this,"FlateWorker/"+t),this._pako=null,this._pakoAction=t,this._pakoOptions=e,this.meta={}}r.magic="\b\0",n.inherits(h,a),h.prototype.processChunk=function(t){this.meta=t.meta,null===this._pako&&this._createPako(),this._pako.push(n.transformTo(o,t.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new s[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var e=this;this._pako.onData=function(t){e.push({data:t,meta:e.meta})}},r.compressWorker=function(t){return new h("Deflate",t)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,e,r){function v(t,e){for(var r="",i=0;i>>=8;return r}function i(t,e,r,i,s,n){var a=t.file,o=t.compression,h=n!==b.utf8encode,l=_.transformTo("string",n(a.name)),p=_.transformTo("string",b.utf8encode(a.name)),c=a.comment,n=_.transformTo("string",n(c)),f=_.transformTo("string",b.utf8encode(c)),d=p.length!==a.name.length,c=f.length!==c.length,u="",m=a.dir,y=a.date,g={crc32:0,compressedSize:0,uncompressedSize:0},r=(e&&!r||(g.crc32=t.crc32,g.compressedSize=t.compressedSize,g.uncompressedSize=t.uncompressedSize),0);return e&&(r|=8),h||!d&&!c||(r|=2048),e=t=0,m&&(t|=16),"UNIX"===s?(e=798,t|=(65535&((h=a.unixPermissions)?h:m?16893:33204))<<16):(e=20,t|=63&(a.dosPermissions||0)),s=y.getUTCHours(),s=(s=((s<<=6)|y.getUTCMinutes())<<5)|y.getUTCSeconds()/2,h=y.getUTCFullYear()-1980,h=(h=((h<<=4)|y.getUTCMonth()+1)<<5)|y.getUTCDate(),d&&(m=v(1,1)+v(S(l),4)+p,u+="up"+v(m.length,2)+m),c&&(a=v(1,1)+v(S(n),4)+f,u+="uc"+v(a.length,2)+a),y="",y=(y=(y=(y=(y=(y=(y=(y=(y=(y+="\n\0")+v(r,2))+o.magic)+v(s,2))+v(h,2))+v(g.crc32,4))+v(g.compressedSize,4))+v(g.uncompressedSize,4))+v(l.length,2))+v(u.length,2),{fileRecord:P.LOCAL_FILE_HEADER+y+l+u,dirRecord:P.CENTRAL_FILE_HEADER+v(e,2)+y+v(n.length,2)+"\0\0\0\0"+v(t,4)+v(i,4)+l+u+n}}var _=t("../utils"),s=t("../stream/GenericWorker"),b=t("../utf8"),S=t("../crc32"),P=t("../signature");function n(t,e,r,i){s.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=e,this.zipPlatform=r,this.encodeFileName=i,this.streamFiles=t,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}_.inherits(n,s),n.prototype.push=function(t){var e=t.meta.percent||0,r=this.entriesCount,i=this._sources.length;this.accumulate?this.contentBuffer.push(t):(this.bytesWritten+=t.data.length,s.prototype.push.call(this,{data:t.data,meta:{currentFile:this.currentFile,percent:r?(e+100*(r-i-1))/r:100}}))},n.prototype.openedSource=function(t){this.currentSourceOffset=this.bytesWritten,this.currentFile=t.file.name;var e=this.streamFiles&&!t.file.dir;e?(t=i(t,e,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName),this.push({data:t.fileRecord,meta:{percent:0}})):this.accumulate=!0},n.prototype.closedSource=function(t){this.accumulate=!1;var e=this.streamFiles&&!t.file.dir,r=i(t,e,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),e)this.push({data:(e=t,P.DATA_DESCRIPTOR+v(e.crc32,4)+v(e.compressedSize,4)+v(e.uncompressedSize,4)),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},n.prototype.flush=function(){for(var t=this.bytesWritten,e=0;e=this.index;e--)r=(r<<8)+this.byteAt(e);return this.index+=t,r},readString:function(t){return i.transformTo("string",this.readData(t))},readData:function(t){},lastIndexOfSignature:function(t){},readAndCheckSignature:function(t){},readDate:function(){var t=this.readInt(4);return new Date(Date.UTC(1980+(t>>25&127),(t>>21&15)-1,t>>16&31,t>>11&31,t>>5&63,(31&t)<<1))}},e.exports=s},{"../utils":32}],19:[function(t,e,r){var i=t("./Uint8ArrayReader");function s(t){i.call(this,t)}t("../utils").inherits(s,i),s.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=s},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(t,e,r){var i=t("./DataReader");function s(t){i.call(this,t)}t("../utils").inherits(s,i),s.prototype.byteAt=function(t){return this.data.charCodeAt(this.zero+t)},s.prototype.lastIndexOfSignature=function(t){return this.data.lastIndexOf(t)-this.zero},s.prototype.readAndCheckSignature=function(t){return t===this.readData(4)},s.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=s},{"../utils":32,"./DataReader":18}],21:[function(t,e,r){var i=t("./ArrayReader");function s(t){i.call(this,t)}t("../utils").inherits(s,i),s.prototype.readData=function(t){var e;return this.checkOffset(t),0===t?new Uint8Array(0):(e=this.data.subarray(this.zero+this.index,this.zero+this.index+t),this.index+=t,e)},e.exports=s},{"../utils":32,"./ArrayReader":17}],22:[function(t,e,r){var i=t("../utils"),s=t("../support"),n=t("./ArrayReader"),a=t("./StringReader"),o=t("./NodeBufferReader"),h=t("./Uint8ArrayReader");e.exports=function(t){var e=i.getTypeOf(t);return i.checkSupport(e),"string"!==e||s.uint8array?"nodebuffer"===e?new o(t):s.uint8array?new h(i.transformTo("uint8array",t)):new n(i.transformTo("array",t)):new a(t)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,e,r){r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(t,e,r){var i=t("./GenericWorker"),s=t("../utils");function n(t){i.call(this,"ConvertWorker to "+t),this.destType=t}s.inherits(n,i),n.prototype.processChunk=function(t){this.push({data:s.transformTo(this.destType,t.data),meta:t.meta})},e.exports=n},{"../utils":32,"./GenericWorker":28}],25:[function(t,e,r){var i=t("./GenericWorker"),s=t("../crc32");function n(){i.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}t("../utils").inherits(n,i),n.prototype.processChunk=function(t){this.streamInfo.crc32=s(t.data,this.streamInfo.crc32||0),this.push(t)},e.exports=n},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,e,r){var i=t("../utils"),s=t("./GenericWorker");function n(t){s.call(this,"DataLengthProbe for "+t),this.propName=t,this.withStreamInfo(t,0)}i.inherits(n,s),n.prototype.processChunk=function(t){var e;t&&(e=this.streamInfo[this.propName]||0,this.streamInfo[this.propName]=e+t.data.length),s.prototype.processChunk.call(this,t)},e.exports=n},{"../utils":32,"./GenericWorker":28}],27:[function(t,e,r){var i=t("../utils"),s=t("./GenericWorker");function n(t){s.call(this,"DataWorker");var e=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,t.then(function(t){e.dataIsReady=!0,e.data=t,e.max=t&&t.length||0,e.type=i.getTypeOf(t),e.isPaused||e._tickAndRepeat()},function(t){e.error(t)})}i.inherits(n,s),n.prototype.cleanUp=function(){s.prototype.cleanUp.call(this),this.data=null},n.prototype.resume=function(){return!!s.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,i.delay(this._tickAndRepeat,[],this)),!0)},n.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished)||(i.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0)},n.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var t=null,e=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":t=this.data.substring(this.index,e);break;case"uint8array":t=this.data.subarray(this.index,e);break;case"array":case"nodebuffer":t=this.data.slice(this.index,e)}return this.index=e,this.push({data:t,meta:{percent:this.max?this.index/this.max*100:0}})},e.exports=n},{"../utils":32,"./GenericWorker":28}],28:[function(t,e,r){function i(t){this.name=t||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}i.prototype={push:function(t){this.emit("data",t)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(t){this.emit("error",t)}return!0},error:function(t){return!this.isFinished&&(this.isPaused?this.generatedError=t:(this.isFinished=!0,this.emit("error",t),this.previous&&this.previous.error(t),this.cleanUp()),!0)},on:function(t,e){return this._listeners[t].push(e),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(t,e){if(this._listeners[t])for(var r=0;r "+t:t}},e.exports=i},{}],29:[function(t,e,r){var l=t("../utils"),s=t("./ConvertWorker"),n=t("./GenericWorker"),p=t("../base64"),i=t("../support"),a=t("../external"),o=null;if(i.nodestream)try{o=t("../nodejs/NodejsStreamOutputAdapter")}catch(t){}function h(t,e,r){var i=e;switch(e){case"blob":case"arraybuffer":i="uint8array";break;case"base64":i="string"}try{this._internalType=i,this._outputType=e,this._mimeType=r,l.checkSupport(i),this._worker=t.pipe(new s(i)),t.lock()}catch(t){this._worker=new n("error"),this._worker.error(t)}}h.prototype={accumulate:function(t){return o=this,h=t,new a.Promise(function(e,r){var i=[],s=o._internalType,n=o._outputType,a=o._mimeType;o.on("data",function(t,e){i.push(t),h&&h(e)}).on("error",function(t){i=[],r(t)}).on("end",function(){try{var t=function(t,e,r){switch(t){case"blob":return l.newBlob(l.transformTo("arraybuffer",e),r);case"base64":return p.encode(e);default:return l.transformTo(t,e)}}(n,function(t,e){for(var r=0,i=null,s=0,n=0;n>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e},s.utf8decode=function(t){if(l.nodebuffer)return h.transformTo("nodebuffer",t).toString("utf-8");for(var e,r,i=t=h.transformTo(l.uint8array?"uint8array":"array",t),s=i.length,n=new Array(2*s),a=0,o=0;o>10&1023,n[a++]=56320|1023&e)}return n.length!==a&&(n.subarray?n=n.subarray(0,a):n.length=a),h.applyFromCharCode(n)},h.inherits(n,r),n.prototype.processChunk=function(t){var e=h.transformTo(l.uint8array?"uint8array":"array",t.data),r=(this.leftOver&&this.leftOver.length&&(l.uint8array?(r=e,(e=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),e.set(r,this.leftOver.length)):e=this.leftOver.concat(e),this.leftOver=null),function(t,e){for(var r=(e=(e=e||t.length)>t.length?t.length:e)-1;0<=r&&128==(192&t[r]);)r--;return!(r<0||0===r)&&r+c[t[r]]>e?r:e}(e)),i=e;r!==e.length&&(l.uint8array?(i=e.subarray(0,r),this.leftOver=e.subarray(r,e.length)):(i=e.slice(0,r),this.leftOver=e.slice(r,e.length))),this.push({data:s.utf8decode(i),meta:t.meta})},n.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=n,h.inherits(a,r),a.prototype.processChunk=function(t){this.push({data:s.utf8encode(t.data),meta:t.meta})},s.Utf8EncodeWorker=a},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(t,e,a){var o=t("./support"),h=t("./base64"),r=t("./nodejsUtils"),i=t("set-immediate-shim"),l=t("./external");function s(t){return t}function p(t,e){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==t&&(this.dosPermissions=63&this.externalFileAttributes),3==t&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(t){var e;this.extraFields[1]&&(e=i(this.extraFields[1].value),this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS)&&(this.diskNumberStart=e.readInt(4))},readExtraFields:function(t){var e,r,i,s=t.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});t.index+4>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e},r.buf2binstring=function(t){return p(t,t.length)},r.binstring2buf=function(t){for(var e=new h.Buf8(t.length),r=0,i=e.length;r>10&1023,n[a++]=56320|1023&r)}return p(n,a)},r.utf8border=function(t,e){for(var r=(e=(e=e||t.length)>t.length?t.length:e)-1;0<=r&&128==(192&t[r]);)r--;return!(r<0||0===r)&&r+l[t[r]]>e?r:e}},{"./common":41}],43:[function(t,e,r){e.exports=function(t,e,r,i){for(var s=65535&t|0,n=t>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:r>>>1;t[e]=r}return t}();e.exports=function(t,e,r,i){var s=o,n=i+r;t^=-1;for(var a=i;a>>8^s[255&(t^e[a])];return-1^t}},{}],46:[function(t,e,r){var o,c=t("../utils/common"),h=t("./trees"),f=t("./adler32"),d=t("./crc32"),i=t("./messages");function l(t,e){return t.msg=i[e],e}function p(t){return(t<<1)-(4t.avail_out?t.avail_out:r)&&(c.arraySet(t.output,e.pending_buf,e.pending_out,r,t.next_out),t.next_out+=r,e.pending_out+=r,t.total_out+=r,t.avail_out-=r,e.pending-=r,0===e.pending)&&(e.pending_out=0)}function y(t,e){h._tr_flush_block(t,0<=t.block_start?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,m(t.strm)}function g(t,e){t.pending_buf[t.pending++]=e}function v(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function n(t,e){var r,i,s=t.max_chain_length,n=t.strstart,a=t.prev_length,o=t.nice_match,h=t.strstart>t.w_size-262?t.strstart-(t.w_size-262):0,l=t.window,p=t.w_mask,c=t.prev,f=t.strstart+258,d=l[n+a-1],u=l[n+a];t.prev_length>=t.good_match&&(s>>=2),o>t.lookahead&&(o=t.lookahead);do{if(l[(r=e)+a]===u&&l[r+a-1]===d&&l[r]===l[n]&&l[++r]===l[n+1]){for(n+=2,r++;l[++n]===l[++r]&&l[++n]===l[++r]&&l[++n]===l[++r]&&l[++n]===l[++r]&&l[++n]===l[++r]&&l[++n]===l[++r]&&l[++n]===l[++r]&&l[++n]===l[++r]&&nh&&0!=--s);return a<=t.lookahead?a:t.lookahead}function _(t){var e,r,i,s,n,a,o,h,l,p=t.w_size;do{if(h=t.window_size-t.lookahead-t.strstart,t.strstart>=p+(p-262)){for(c.arraySet(t.window,t.window,p,p,0),t.match_start-=p,t.strstart-=p,t.block_start-=p,e=r=t.hash_size;i=t.head[--e],t.head[e]=p<=i?i-p:0,--r;);for(e=r=p;i=t.prev[--e],t.prev[e]=p<=i?i-p:0,--r;);h+=p}if(0===t.strm.avail_in)break;if(n=t.strm,a=t.window,o=t.strstart+t.lookahead,h=h,l=void 0,r=0===(l=(l=n.avail_in)>h?h:l)?0:(n.avail_in-=l,c.arraySet(a,n.input,n.next_in,l,o),1===n.state.wrap?n.adler=f(n.adler,a,l,o):2===n.state.wrap&&(n.adler=d(n.adler,a,l,o)),n.next_in+=l,n.total_in+=l,l),t.lookahead+=r,3<=t.lookahead+t.insert)for(s=t.strstart-t.insert,t.ins_h=t.window[s],t.ins_h=(t.ins_h<t.pending_buf_size-5&&(r=t.pending_buf_size-5);;){if(t.lookahead<=1){if(_(t),0===t.lookahead&&0===e)return 1;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+r;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,y(t,!1),0===t.strm.avail_out))return 1;if(t.strstart-t.block_start>=t.w_size-262&&(y(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,4===e?(y(t,!0),0===t.strm.avail_out?3:4):(t.strstart>t.block_start&&(y(t,!1),t.strm.avail_out),1)}),new b(4,4,8,4,s),new b(4,5,16,8,s),new b(4,6,32,32,s),new b(4,4,16,16,a),new b(8,16,32,32,a),new b(8,16,128,128,a),new b(8,32,128,256,a),new b(32,128,258,1024,a),new b(32,258,258,4096,a)],r.deflateInit=function(t,e){return k(t,e,8,15,8,0)},r.deflateInit2=k,r.deflateReset=x,r.deflateResetKeep=P,r.deflateSetHeader=function(t,e){return!t||!t.state||2!==t.state.wrap?-2:(t.state.gzhead=e,0)},r.deflate=function(t,e){var r,i,s,n;if(!t||!t.state||5>8&255),g(i,i.gzhead.time>>16&255),g(i,i.gzhead.time>>24&255),g(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),g(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(g(i,255&i.gzhead.extra.length),g(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=d(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(g(i,0),g(i,0),g(i,0),g(i,0),g(i,0),g(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),g(i,3),i.status=113)):(a=8+(i.w_bits-8<<4)<<8,a|=(2<=i.strategy||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(a|=32),a+=31-a%31,i.status=113,v(i,a),0!==i.strstart&&(v(i,t.adler>>>16),v(i,65535&t.adler)),t.adler=1)),69===i.status)if(i.gzhead.extra){for(s=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>s&&(t.adler=d(t.adler,i.pending_buf,i.pending-s,s)),m(t),s=i.pending,i.pending!==i.pending_buf_size));)g(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>s&&(t.adler=d(t.adler,i.pending_buf,i.pending-s,s)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73)}else i.status=73;if(73===i.status)if(i.gzhead.name){s=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>s&&(t.adler=d(t.adler,i.pending_buf,i.pending-s,s)),m(t),s=i.pending,i.pending===i.pending_buf_size)){n=1;break}}while(n=i.gzindexs&&(t.adler=d(t.adler,i.pending_buf,i.pending-s,s)),0===n&&(i.gzindex=0,i.status=91)}else i.status=91;if(91===i.status)if(i.gzhead.comment){s=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>s&&(t.adler=d(t.adler,i.pending_buf,i.pending-s,s)),m(t),s=i.pending,i.pending===i.pending_buf_size)){n=1;break}}while(n=i.gzindexs&&(t.adler=d(t.adler,i.pending_buf,i.pending-s,s)),0===n&&(i.status=103)}else i.status=103;if(103===i.status&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&m(t),i.pending+2<=i.pending_buf_size&&(g(i,255&t.adler),g(i,t.adler>>8&255),t.adler=0,i.status=113)):i.status=113),0!==i.pending){if(m(t),0===t.avail_out)return i.last_flush=-1,0}else if(0===t.avail_in&&p(e)<=p(r)&&4!==e)return l(t,-5);if(666===i.status&&0!==t.avail_in)return l(t,-5);if(0!==t.avail_in||0!==i.lookahead||0!==e&&666!==i.status){var a=2===i.strategy?function(t,e){for(var r;;){if(0===t.lookahead&&(_(t),0===t.lookahead)){if(0===e)return 1;break}if(t.match_length=0,r=h._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,r&&(y(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,4===e?(y(t,!0),0===t.strm.avail_out?3:4):t.last_lit&&(y(t,!1),0===t.strm.avail_out)?1:2}(i,e):3===i.strategy?function(t,e){for(var r,i,s,n,a=t.window;;){if(t.lookahead<=258){if(_(t),t.lookahead<=258&&0===e)return 1;if(0===t.lookahead)break}if(t.match_length=0,3<=t.lookahead&&0t.lookahead&&(t.match_length=t.lookahead)}if(3<=t.match_length?(r=h._tr_tally(t,1,t.match_length-3),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(r=h._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),r&&(y(t,!1),0===t.strm.avail_out))return 1}return t.insert=0,4===e?(y(t,!0),0===t.strm.avail_out?3:4):t.last_lit&&(y(t,!1),0===t.strm.avail_out)?1:2}(i,e):o[i.level].func(i,e);if(3!==a&&4!==a||(i.status=666),1===a||3===a)return 0===t.avail_out&&(i.last_flush=-1),0;if(2===a&&(1===e?h._tr_align(i):5!==e&&(h._tr_stored_block(i,0,0,!1),3===e)&&(u(i.head),0===i.lookahead)&&(i.strstart=0,i.block_start=0,i.insert=0),m(t),0===t.avail_out))return i.last_flush=-1,0}return 4!==e||!(i.wrap<=0)&&(2===i.wrap?(g(i,255&t.adler),g(i,t.adler>>8&255),g(i,t.adler>>16&255),g(i,t.adler>>24&255),g(i,255&t.total_in),g(i,t.total_in>>8&255),g(i,t.total_in>>16&255),g(i,t.total_in>>24&255)):(v(i,t.adler>>>16),v(i,65535&t.adler)),m(t),0=r.w_size&&(0===n&&(u(r.head),r.strstart=0,r.block_start=0,r.insert=0),h=new c.Buf8(r.w_size),c.arraySet(h,e,l-r.w_size,r.w_size,0),e=h,l=r.w_size),h=t.avail_in,a=t.next_in,o=t.input,t.avail_in=l,t.next_in=0,t.input=e,_(r);3<=r.lookahead;){for(i=r.strstart,s=r.lookahead-2;r.ins_h=(r.ins_h<>>=i=r>>>24,P-=i,0==(i=r>>>16&255))d[f++]=65535&r;else{if(!(16&i)){if(0==(64&i)){r=x[(65535&r)+(S&(1<>>=i,P-=i),P<15&&(S+=p[l++]<>>=i=r>>>24,P-=i,!(16&(i=r>>>16&255))){if(0==(64&i)){r=k[(65535&r)+(S&(1<y){t.msg="invalid distance too far back",h.mode=30;break t}if(S>>>=i,P-=i,n>(i=f-u)){if((i=n-i)>v&&h.sane){t.msg="invalid distance too far back",h.mode=30;break t}if(o=b,(a=0)===_){if(a+=g-i,i>3,S&=(1<<(P-=s<<3))-1,t.next_in=l,t.next_out=f,t.avail_in=l>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function i(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new F.Buf16(320),this.work=new F.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function s(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=1,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new F.Buf32(852),e.distcode=e.distdyn=new F.Buf32(592),e.sane=1,e.back=-1,0):-2}function n(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,s(t)):-2}function a(t,e){var r,i;return!t||!t.state||(i=t.state,e<0?(r=0,e=-e):(r=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15=t.wsize?(F.arraySet(t.window,e,r-t.wsize,t.wsize,0),t.wnext=0,t.whave=t.wsize):((s=t.wsize-t.wnext)>i&&(s=i),F.arraySet(t.window,e,r-i,s,t.wnext),(i-=s)?(F.arraySet(t.window,e,r-i,i,0),t.wnext=i,t.whave=t.wsize):(t.wnext+=s,t.wnext===t.wsize&&(t.wnext=0),t.whave>>8&255,r.check=D(r.check,A,2,0),p=l=0,r.mode=2;else if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&l)<<8)+(l>>8))%31)t.msg="incorrect header check",r.mode=30;else if(8!=(15&l))t.msg="unknown compression method",r.mode=30;else{if(p-=4,P=8+(15&(l>>>=4)),0===r.wbits)r.wbits=P;else if(P>r.wbits){t.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(A[0]=255&l,A[1]=l>>>8&255,r.check=D(r.check,A,2,0)),p=l=0,r.mode=3;case 3:for(;p<32;){if(0===o)break t;o--,l+=i[n++]<>>8&255,A[2]=l>>>16&255,A[3]=l>>>24&255,r.check=D(r.check,A,4,0)),p=l=0,r.mode=4;case 4:for(;p<16;){if(0===o)break t;o--,l+=i[n++]<>8),512&r.flags&&(A[0]=255&l,A[1]=l>>>8&255,r.check=D(r.check,A,2,0)),p=l=0,r.mode=5;case 5:if(1024&r.flags){for(;p<16;){if(0===o)break t;o--,l+=i[n++]<>>8&255,r.check=D(r.check,A,2,0)),p=l=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&((d=(d=r.length)>o?o:d)&&(r.head&&(P=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),F.arraySet(r.head.extra,i,n,d,P)),512&r.flags&&(r.check=D(r.check,i,d,n)),o-=d,n+=d,r.length-=d),r.length))break t;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break t;for(d=0;P=i[n+d++],r.head&&P&&r.length<65536&&(r.head.name+=String.fromCharCode(P)),P&&d>9&1,r.head.done=!0),t.adler=r.check=0,r.mode=12;break;case 10:for(;p<32;){if(0===o)break t;o--,l+=i[n++]<>>=7&p,p-=7&p,r.mode=27;else{for(;p<3;){if(0===o)break t;o--,l+=i[n++]<>>=1)){case 0:r.mode=14;break;case 1:C=I=void 0;var C,I=r;if(B){for(O=new F.Buf32(512),N=new F.Buf32(32),C=0;C<144;)I.lens[C++]=8;for(;C<256;)I.lens[C++]=9;for(;C<280;)I.lens[C++]=7;for(;C<288;)I.lens[C++]=8;for(z(1,I.lens,0,288,O,0,I.work,{bits:9}),C=0;C<32;)I.lens[C++]=5;z(2,I.lens,0,32,N,0,I.work,{bits:5}),B=!1}if(I.lencode=O,I.lenbits=9,I.distcode=N,I.distbits=5,r.mode=20,6!==e)break;l>>>=2,p-=2;break t;case 2:r.mode=17;break;case 3:t.msg="invalid block type",r.mode=30}l>>>=2,p-=2}break;case 14:for(l>>>=7&p,p-=7&p;p<32;){if(0===o)break t;o--,l+=i[n++]<>>16^65535)){t.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&l,p=l=0,r.mode=15,6===e)break t;case 15:r.mode=16;case 16:if(d=r.length){if(0===(d=h<(d=o>>=5,p-=5,r.ndist=1+(31&l),l>>>=5,p-=5,r.ncode=4+(15&l),l>>>=4,p-=4,286>>=3,p-=3}for(;r.have<19;)r.lens[T[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,k={bits:r.lenbits},x=z(0,r.lens,0,19,r.lencode,0,r.work,k),r.lenbits=k.bits,x){t.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,v=65535&w,!((y=w>>>24)<=p);){if(0===o)break t;o--,l+=i[n++]<>>=y,p-=y,r.lens[r.have++]=v;else{if(16===v){for(E=y+2;p>>=y,p-=y,0===r.have){t.msg="invalid bit length repeat",r.mode=30;break}P=r.lens[r.have-1],d=3+(3&l),l>>>=2,p-=2}else if(17===v){for(E=y+3;p>>=y)),l>>>=3,p=p-y-3}else{for(E=y+7;p>>=y)),l>>>=7,p=p-y-7}if(r.have+d>r.nlen+r.ndist){t.msg="invalid bit length repeat",r.mode=30;break}for(;d--;)r.lens[r.have++]=P}}if(30===r.mode)break;if(0===r.lens[256]){t.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,k={bits:r.lenbits},x=z(1,r.lens,0,r.nlen,r.lencode,0,r.work,k),r.lenbits=k.bits,x){t.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,k={bits:r.distbits},x=z(2,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,k),r.distbits=k.bits,x){t.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===e)break t;case 20:r.mode=21;case 21:if(6<=o&&258<=h){t.next_out=a,t.avail_out=h,t.next_in=n,t.avail_in=o,r.hold=l,r.bits=p,R(t,f),a=t.next_out,s=t.output,h=t.avail_out,n=t.next_in,i=t.input,o=t.avail_in,l=r.hold,p=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(w=r.lencode[l&(1<>>16&255,v=65535&w,!((y=w>>>24)<=p);){if(0===o)break t;o--,l+=i[n++]<>_)])>>>16&255,v=65535&w,!(_+(y=w>>>24)<=p);){if(0===o)break t;o--,l+=i[n++]<>>=_,p-=_,r.back+=_}if(l>>>=y,p-=y,r.back+=y,r.length=v,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){t.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(E=r.extra;p>>=r.extra,p-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(w=r.distcode[l&(1<>>16&255,v=65535&w,!((y=w>>>24)<=p);){if(0===o)break t;o--,l+=i[n++]<>_)])>>>16&255,v=65535&w,!(_+(y=w>>>24)<=p);){if(0===o)break t;o--,l+=i[n++]<>>=_,p-=_,r.back+=_}if(l>>>=y,p-=y,r.back+=y,64&g){t.msg="invalid distance code",r.mode=30;break}r.offset=v,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(E=r.extra;p>>=r.extra,p-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){t.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break t;if(r.offset>(d=f-h)){if((d=r.offset-d)>r.whave&&r.sane){t.msg="invalid distance too far back",r.mode=30;break}u=d>r.wnext?(d-=r.wnext,r.wsize-d):r.wnext-d,d>r.length&&(d=r.length),m=r.window}else m=s,u=a-r.offset,d=r.length;for(h-=d=hd?(m=M[D+a[_]],T[C+a[_]]):(m=96,0),h=1<<(u=v-k),b=l=1<>k)+(l-=h)]=u<<24|m<<16|y|0,0!==l;);for(h=1<>=1;if(0!==h?A=(A&h-1)+h:A=0,_++,0==--I[v]){if(v===S)break;v=e[r+a[_]]}if(P>>7)]}function n(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function x(t,e,r){t.bi_valid>16-r?(t.bi_buf|=e<>16-t.bi_valid,t.bi_valid+=r-16):(t.bi_buf|=e<>>=1,r<<=1,0<--e;);return r>>>1}function w(t,e,r){for(var i,s=new Array(16),n=0,a=1;a<=15;a++)s[a]=n=n+r[a-1]<<1;for(i=0;i<=e;i++){var o=t[2*i+1];0!==o&&(t[2*i]=E(s[o]++,o))}}function A(t){for(var e=0;e<286;e++)t.dyn_ltree[2*e]=0;for(e=0;e<30;e++)t.dyn_dtree[2*e]=0;for(e=0;e<19;e++)t.bl_tree[2*e]=0;t.dyn_ltree[512]=1,t.opt_len=t.static_len=0,t.last_lit=t.matches=0}function T(t){8>1;1<=r;r--)C(t,n,r);for(s=h;r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],C(t,n,1),i=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=i,n[2*s]=n[2*r]+n[2*i],t.depth[s]=(t.depth[r]>=t.depth[i]?t.depth[r]:t.depth[i])+1,n[2*r+1]=n[2*i+1]=s,t.heap[1]=s++,C(t,n,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1];for(var p,c,f,d,u,m=t,y=e.dyn_tree,g=e.max_code,v=e.stat_desc.static_tree,_=e.stat_desc.has_stree,b=e.stat_desc.extra_bits,S=e.stat_desc.extra_base,P=e.stat_desc.max_length,x=0,k=0;k<=15;k++)m.bl_count[k]=0;for(y[2*m.heap[m.heap_max]+1]=0,p=m.heap_max+1;p<573;p++)(k=y[2*y[2*(c=m.heap[p])+1]+1]+1)>P&&(k=P,x++),y[2*c+1]=k,gg||(y[2*f+1]!==k&&(m.opt_len+=(k-y[2*f+1])*y[2*f],y[2*f+1]=k),c--)}w(n,l,t.bl_count)}function M(t,e,r){var i,s,n=-1,a=e[1],o=0,h=7,l=4;for(0===a&&(h=138,l=3),e[2*(r+1)+1]=65535,i=0;i<=r;i++)s=a,a=e[2*(i+1)+1],++o>=7;a<30;a++)for(_[a]=i<<7,e=0;e<1<>>=1)if(1&e&&0!==t.dyn_ltree[2*r])return 0;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return 1;for(r=32;r<256;r++)if(0!==t.dyn_ltree[2*r])return 1;return 0}(t)),F(t,t.l_desc),F(t,t.d_desc),o=function(t){var e;for(M(t,t.dyn_ltree,t.l_desc.max_code),M(t,t.dyn_dtree,t.d_desc.max_code),F(t,t.bl_desc),e=18;3<=e&&0===t.bl_tree[2*p[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e}(t),s=t.opt_len+3+7>>>3,(n=t.static_len+3+7>>>3)<=s&&(s=n)):s=n=r+5,r+4<=s&&-1!==e)z(t,e,r,i);else if(4===t.strategy||n===s)x(t,2+(i?1:0),3),I(t,c,f);else{x(t,4+(i?1:0),3);var h=t,l=(e=t.l_desc.max_code+1,r=t.d_desc.max_code+1,o+1);for(x(h,e-257,5),x(h,r-1,5),x(h,l-4,4),a=0;a>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&r,t.last_lit++,0===e?t.dyn_ltree[2*r]++:(t.matches++,e--,t.dyn_ltree[2*(u[r]+256+1)]++,t.dyn_dtree[2*P(e)]++),t.last_lit===t.lit_bufsize-1},e._tr_align=function(t){x(t,2,3),k(t,256,c),16===(t=t).bi_valid?(n(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):8<=t.bi_valid&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}},{"../utils/common":41}],53:[function(t,e,r){e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,e,r){e.exports="function"==typeof setImmediate?setImmediate:function(){var t=[].slice.apply(arguments);t.splice(1,0,0),setTimeout.apply(null,t)}},{}]},{},[10])(10)});function _templateObject(){const t=_taggedTemplateLiteral(["\n* {\n box-sizing: border-box;\n padding: 0;\n margin: 0;\n}\n\n:host {\n --lottie-player-toolbar-height: 35px;\n --lottie-player-toolbar-background-color: transparent;\n --lottie-player-toolbar-icon-color: #999;\n --lottie-player-toolbar-icon-hover-color: #222;\n --lottie-player-toolbar-icon-active-color: #555;\n --lottie-player-seeker-track-color: #CCC;\n --lottie-player-seeker-thumb-color: rgba(0, 107, 120, 0.8);\n\n display: block;\n}\n\n.main {\n display: grid;\n grid-auto-columns: auto;\n grid-template-rows: auto;\n height: inherit;\n width: inherit;\n}\n\n.main.controls {\n grid-template-rows: 1fr var(--lottie-player-toolbar-height);\n}\n\n.animation {\n display: flex;\n justify-content: center;\n align-items: center;\n width: inherit;\n height: inherit;\n}\n\n.toolbar {\n display: grid;\n grid-template-columns: 32px 32px 1fr 32px;\n align-items: center;\n justify-items: center;\n background-color: var(--lottie-player-toolbar-background-color);\n}\n\n.toolbar button {\n cursor: pointer;\n fill: var(--lottie-player-toolbar-icon-color);\n display: flex;\n background: none;\n border: 0;\n padding: 0;\n outline: none;\n height: 100%;\n}\n\n.toolbar button:hover {\n fill: var(--lottie-player-toolbar-icon-hover-color);\n}\n\n.toolbar button.active {\n fill: var(--lottie-player-toolbar-icon-active-color);\n}\n\n.toolbar button svg {\n}\n\n.toolbar button.disabled svg {\n display: none;\n}\n\n.toolbar a {\n filter: grayscale(100%);\n display: flex;\n transition: filter .5s, opacity 0.5s;\n opacity: 0.4;\n height: 100%;\n align-items: center;\n}\n\n.toolbar a:hover {\n filter: none;\n display: flex;\n opacity: 1;\n}\n\n.toolbar a svg {\n}\n\n.seeker {\n -webkit-appearance: none;\n width: 95%;\n outline: none;\n}\n\n.seeker::-webkit-slider-runnable-track {\n width: 100%;\n height: 5px;\n cursor: pointer;\n background: var(--lottie-player-seeker-track-color);\n border-radius: 3px;\n}\n.seeker::-webkit-slider-thumb {\n height: 15px;\n width: 15px;\n border-radius: 50%;\n background: var(--lottie-player-seeker-thumb-color);\n cursor: pointer;\n -webkit-appearance: none;\n margin-top: -5px;\n}\n.seeker:focus::-webkit-slider-runnable-track {\n background: #999;\n}\n.seeker::-moz-range-track {\n width: 100%;\n height: 5px;\n cursor: pointer;\n background: var(--lottie-player-seeker-track-color);\n border-radius: 3px;\n}\n.seeker::-moz-range-thumb {\n height: 15px;\n width: 15px;\n border-radius: 50%;\n background: var(--lottie-player-seeker-thumb-color);\n cursor: pointer;\n}\n.seeker::-ms-track {\n width: 100%;\n height: 5px;\n cursor: pointer;\n background: transparent;\n border-color: transparent;\n color: transparent;\n}\n.seeker::-ms-fill-lower {\n background: var(--lottie-player-seeker-track-color);\n border-radius: 3px;\n}\n.seeker::-ms-fill-upper {\n background: var(--lottie-player-seeker-track-color);\n border-radius: 3px;\n}\n.seeker::-ms-thumb {\n border: 0;\n height: 15px;\n width: 15px;\n border-radius: 50%;\n background: var(--lottie-player-seeker-thumb-color);\n cursor: pointer;\n}\n.seeker:focus::-ms-fill-lower {\n background: var(--lottie-player-seeker-track-color);\n}\n.seeker:focus::-ms-fill-upper {\n background: var(--lottie-player-seeker-track-color);\n}\n\n.error {\n display: flex;\n justify-content: center;\n height: 100%;\n align-items: center;\n}\n"]);return _templateObject=function(){return t},t}var styles=css(_templateObject()),PlayerState,PlayMode,PlayerEvents;function _templateObject5(){const t=_taggedTemplateLiteral(['\n
\n ']);return _templateObject5=function(){return t},t}function _templateObject4(){const t=_taggedTemplateLiteral(["\n
\n ']);return _templateObject3=function(){return t},t}function _templateObject2(){const t=_taggedTemplateLiteral(['\n \n \n \n ']);return _templateObject2=function(){return t},t}function _templateObject$1(){const t=_taggedTemplateLiteral(['\n
\n \n \n \n \n \n \n
\n ']);return _templateObject$1=function(){return t},t}function fetchPath(i){return new Promise((r,e)=>{const t=new XMLHttpRequest;t.open("GET",i,!0),t.responseType="arraybuffer",t.send(),t.onreadystatechange=function(){4==t.readyState&&200==t.status&&jszip.loadAsync(t.response).then(i=>{i.file("manifest.json").async("string").then(t=>{t=JSON.parse(t);if(!("animations"in t))throw new Error("Manifest not found");if(0===t.animations.length)throw new Error("No animations listed in the manifest");t=t.animations[0];i.file("animations/".concat(t.id,".json")).async("string").then(t=>{const e=JSON.parse(t);"assets"in e&&Promise.all(e.assets.map(r=>{if(r.p&&null!=i.file("images/".concat(r.p)))return new Promise(e=>{i.file("images/".concat(r.p)).async("base64").then(t=>{r.p="data:;base64,"+t,r.e=1,e()})})})).then(()=>{r(e)})})})}).catch(t=>{e(t)})}})}PlayerState=exports.PlayerState||(exports.PlayerState={}),PlayerState.Loading="loading",PlayerState.Playing="playing",PlayerState.Paused="paused",PlayerState.Stopped="stopped",PlayerState.Frozen="frozen",PlayerState.Error="error",PlayMode=exports.PlayMode||(exports.PlayMode={}),PlayMode.Normal="normal",PlayMode.Bounce="bounce",PlayerEvents=exports.PlayerEvents||(exports.PlayerEvents={}),PlayerEvents.Load="load",PlayerEvents.Error="error",PlayerEvents.Ready="ready",PlayerEvents.Play="play",PlayerEvents.Pause="pause",PlayerEvents.Stop="stop",PlayerEvents.Freeze="freeze",PlayerEvents.Loop="loop",PlayerEvents.Complete="complete",PlayerEvents.Frame="frame",exports.DotLottiePlayer=class extends LitElement{constructor(){super(...arguments),this.mode=exports.PlayMode.Normal,this.autoplay=!1,this.background="transparent",this.controls=!1,this.direction=1,this.hover=!1,this.loop=!1,this.renderer="svg",this.speed=1,this.currentState=exports.PlayerState.Loading,this.intermission=1,this._counter=0}_onVisibilityChange(){document.hidden&&this.currentState===exports.PlayerState.Playing?this.freeze():this.currentState===exports.PlayerState.Frozen&&this.play()}_handleSeekChange(t){this._lottie&&!isNaN(t.target.value)&&(t=t.target.value/100*this._lottie.totalFrames,this.seek(t))}async load(t){if(this.shadowRoot){var e={container:this.container,loop:!1,autoplay:!1,renderer:this.renderer,rendererSettings:{scaleMode:"noScale",clearCanvas:!1,progressiveLoad:!0,hideOnTransparent:!0}};try{var r=await fetchPath(t);this._lottie&&this._lottie.destroy(),this._lottie=lottie_svg.loadAnimation(Object.assign(Object.assign({},e),{animationData:r}))}catch(t){return this.currentState=exports.PlayerState.Error,void this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Error))}this._lottie&&(this._lottie.addEventListener("enterFrame",()=>{this.seeker=this._lottie.currentFrame/this._lottie.totalFrames*100,this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Frame,{detail:{frame:this._lottie.currentFrame,seeker:this.seeker}}))}),this._lottie.addEventListener("complete",()=>{this.currentState!==exports.PlayerState.Playing||!this.loop||this.count&&this._counter>=this.count?this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Complete)):this.mode===exports.PlayMode.Bounce?(this.count&&(this._counter+=.5),setTimeout(()=>{this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Loop)),this.currentState===exports.PlayerState.Playing&&(this._lottie.setDirection(-1*this._lottie.playDirection),this._lottie.play())},this.intermission)):(this.count&&(this._counter+=1),window.setTimeout(()=>{this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Loop)),this.currentState===exports.PlayerState.Playing&&(this._lottie.stop(),this._lottie.play())},this.intermission))}),this._lottie.addEventListener("DOMLoaded",()=>{this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Ready))}),this._lottie.addEventListener("data_ready",()=>{this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Load))}),this._lottie.addEventListener("data_failed",()=>{this.currentState=exports.PlayerState.Error,this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Error))}),this.container.addEventListener("mouseenter",()=>{this.hover&&this.currentState!==exports.PlayerState.Playing&&this.play()}),this.container.addEventListener("mouseleave",()=>{this.hover&&this.currentState===exports.PlayerState.Playing&&this.stop()}),this.setSpeed(this.speed),this.setDirection(this.direction),this.autoplay)&&this.play()}}getLottie(){return this._lottie}play(){this._lottie&&(this._lottie.play(),this.currentState=exports.PlayerState.Playing,this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Play)))}pause(){this._lottie&&(this._lottie.pause(),this.currentState=exports.PlayerState.Paused,this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Pause)))}stop(){this._lottie&&(this._counter=0,this._lottie.stop(),this.currentState=exports.PlayerState.Stopped,this.dispatchEvent(new CustomEvent(exports.PlayerEvents.Stop)))}seek(t){this._lottie&&(t=t.toString().match(/^([0-9]+)(%?)$/))&&(t="%"===t[2]?this._lottie.totalFrames*Number(t[1])/100:t[1],this.seeker=t,this.currentState===exports.PlayerState.Playing?this._lottie.goToAndPlay(t,!0):(this._lottie.goToAndStop(t,!0),this._lottie.pause()))}snapshot(){let t=!(0{t[0].isIntersecting?this.currentState===exports.PlayerState.Frozen&&this.play():this.currentState===exports.PlayerState.Playing&&this.freeze()}),this._io.observe(this.container)),void 0!==document.hidden&&document.addEventListener("visibilitychange",()=>this._onVisibilityChange()),this.src&&await this.load(this.src)}disconnectedCallback(){this._io&&(this._io.disconnect(),this._io=void 0),document.removeEventListener("visibilitychange",()=>this._onVisibilityChange())}renderControls(){var t=this.currentState===exports.PlayerState.Playing,e=this.currentState===exports.PlayerState.Paused,r=this.currentState===exports.PlayerState.Stopped;return html(_templateObject$1(),this.togglePlay,t||e?"active":"",html((t?_templateObject2:_templateObject3)()),this.stop,r?"active":"",this.seeker,this._handleSeekChange,()=>{this._prevState=this.currentState,this.freeze()},()=>{this._prevState===exports.PlayerState.Playing&&this.play()},this.toggleLooping,this.loop?"active":"")}render(){var t=this.controls?"controls":"";return html(_templateObject4(),"main "+t,"background:"+this.background,this.currentState===exports.PlayerState.Error?html(_templateObject5()):void 0,this.controls?this.renderControls():void 0)}},__decorate([query(".animation")],exports.DotLottiePlayer.prototype,"container",void 0),__decorate([property()],exports.DotLottiePlayer.prototype,"mode",void 0),__decorate([property({type:Boolean})],exports.DotLottiePlayer.prototype,"autoplay",void 0),__decorate([property({type:String,reflect:!0})],exports.DotLottiePlayer.prototype,"background",void 0),__decorate([property({type:Boolean})],exports.DotLottiePlayer.prototype,"controls",void 0),__decorate([property({type:Number})],exports.DotLottiePlayer.prototype,"count",void 0),__decorate([property({type:Number})],exports.DotLottiePlayer.prototype,"direction",void 0),__decorate([property({type:Boolean})],exports.DotLottiePlayer.prototype,"hover",void 0),__decorate([property({type:Boolean,reflect:!0})],exports.DotLottiePlayer.prototype,"loop",void 0),__decorate([property({type:String})],exports.DotLottiePlayer.prototype,"renderer",void 0),__decorate([property({type:Number})],exports.DotLottiePlayer.prototype,"speed",void 0),__decorate([property({type:String})],exports.DotLottiePlayer.prototype,"src",void 0),__decorate([property({type:String})],exports.DotLottiePlayer.prototype,"currentState",void 0),__decorate([property()],exports.DotLottiePlayer.prototype,"seeker",void 0),__decorate([property()],exports.DotLottiePlayer.prototype,"intermission",void 0),exports.DotLottiePlayer=__decorate([customElement("dotlottie-player")],exports.DotLottiePlayer),exports.fetchPath=fetchPath,Object.defineProperty(exports,"__esModule",{value:!0})}); \ No newline at end of file diff --git a/public/lottie/refresh-dark.lottie b/public/lottie/refresh-dark.lottie new file mode 100644 index 0000000000000000000000000000000000000000..a029a1f1141804d4191ed68237dacbda92e8c4d5 GIT binary patch literal 2107 zcmZ{lc{tRI8pnUO$&z(6V;fUr2wBE1D*ITbaSSRa61ie5GnHj**+#N&jh#@;Gop>e zi8-363@u`0i9)VHhB6g0GadK-aqd0mKKFf}@ALh>f4tB0{qN&}5fN1c06=_K05E9% zdp3F3n|CF>tH?kC{(Nv$v_V)Dk#MwtA^8L)=a|y8`kSZ&!-0TE*pPKZRy;MJPTrf; z_586;g&dXci5D?R`^7X)(_L&jK7F8u(VRDt{@rr+x`vcxDny&b+d1WnV`i03ZI7hG zJAO6`4Pmtn-%GAlc=ROifLj#QbQ3fT z*YvmdT=u|@@lKn;AFYHveG0E%Hx3y6Tqs#@!<;V?%xgsR;4Fsa_$%{bB?`Yi-_*>0 zto}qEVj{+CT%sz#wy&_&{YRIwD%uH^=wYu_vA7{w`c22XK7<`(Ayxh3D>P2S(qDf( z<@tqqo-iWQ?EBa46)*ZEJ7U8)-WT$A#(S23xx2TCYY|<@r~BM~yEFZgpp|k^*=^v1 z-#je2yARC15OalE=TVbm?=eyLM1eD}gpR%FXNH|oS(>YCP)AFhm#U`JGCl~BHVJKD zilVB3LK97_OBqCIhh4%`#Y*pIoQjO$dxddXXzUt}^-6vnEW;cyy1dF1lXtixn4V%2@5f%m}A zHS3T{h+BDJqe8>b*T*fl58r(o1LrX&LH-xtFVQ=DY-g(BDRI`@EyqAKY%=JO&rO}z zQc_@$gC`Ku&Y)=)Ip|QI>x5>xP1`XNjUyED_woHZ*bYY(k>q6~4f@^!GQN8|%U`~? z{a4)*zhc!8&&FVRv?mQg9^hK!I+azfSmwF7Jko`nspYS$M2yvs9@kuJX?$Unf+!yO zj%aGh+2`-1c4!TW2G!pwK<%rzJE`ZELD_Z*4q;&zr_B|!30}H-X86p=9EaQp7tD%t z8M+NN8}7;pY{1=?%au>63^RrdO9kOn!VO+FAp1Op( zOqI_h0;yyPE1`w~BA6hCKGwdOpfJ|o>_9bv&y($XK6yTN6HuKKMs@Z^Fv z0INKn^~Mlz_l$X0lQId@wH8pzVCf>gO5Q7qEXll zC&VjUrR^yr8DOx6g8ODi#voFtQQ}EHKz@wUxAmGI2<#?@BFQ6-$}fY248mWjiN09# z&7Iv+1RcnIW_keKrt@qfY*hB!n=vKE)N--l6XK|OfUHBprE;;j_ug;_8@#w&uWhvl z`Sl22ff9YZrwfASBu==C5S-zLZ&43 z8=DfpH81lr;%S@k7@>z89P~Zf`OMVY2w2QT1H@#8DXhAiY>+cld-b#$*Sc6`AF6wZ zsIIA=u8@tUF#DTk`@F!zMW^hraV_Gpn0_I_VZiLZN~nnyYqpsgg}WBHm^M>7r{yaK zOZ>uGz}q?pCs_-3>f-t*jwgm~bcMoEV29(e8zln@*9yuV6SJ~4R4g4){f&nE8J|lE z%A#AJ8kG_gDvucT*{jjjGrzU=i`tczS7IJtJ*YKVRDp{2>;1I2mB=K`Z00lwvumgz zqIm(b_FioJB4TBZF5N7j(C26qOq`k7HfxVrofquy+SuxN#Y3>*{D%zo z-n<91BtkNKkwh?*UFwV_IG_3{;(-wo27v#M=w0sp*Zc+8e}DZuFaPs*XE*+d%>SjQ U2S#icvj8BxyBWLQu;*XbA4#Ib3jhEB literal 0 HcmV?d00001 diff --git a/public/lottie/refresh-light.lottie b/public/lottie/refresh-light.lottie new file mode 100644 index 0000000000000000000000000000000000000000..cd14ae89175ce7ce4054be2e53a4d3dfd84620c7 GIT binary patch literal 2098 zcmZ{lXH*ji7KKBV4hbm8ARP%!z#!Eq5NcvV5;90)8A21xB4g>&nST)?c zec;6hk~|QZNb&de2@E=MH86kzP2z|(8p+s2K3k|0gmGXL^_x6tJI&1I6NN+NL%P_l zrNSdn$=9I*5Soigh!#p{I5fH~hf|y|7`<-(HBUp_Jn9IzZ)f}b1x$Nv`sdA_XqD>y zV!lo!*l<*AF%$hdVq3NZq^#rV9~@H%$sKZ=q%FxK7-us$vd>CMBvYeE0QxU&BO*A3 zOkn_E;Vb~~(?Q#VvSebAe*h)W@Xz>*@xQxs312ARO=Wfu!#;r2;_A1^@sxOee~ibM z1_hgfkHi7(l%uOrI+vi#S2gWUR`y5u{; z!6`S*8qD-wKL44UsQu}3U^BbCX4bw)QgJTbEF;8Z*ps>1B9!NP+S#Bg|3PK_oc`va zTWBPHpD-{@zESseJaC4!K4=n-;ahcdp6S&-ZP&VQb1b2fdrtPA&7=GN53m}zHNstR z^d;(zV+EqZeUa@m^oR2WM4Vt{n3i`mq8kv3U!aMb`8qj6okzz4S6z$rzpXYO)EIsc5ezay*wgy zzw}+dWLAXzb^YILh>`pV>`$#m6^Y*~UX(b%q(4Zk8Mzyg2I*ZAd+XC2j-Lit$8^U6 zBf8$O^MHu%n;^J|>%xFjcb717@nEcD-6 zBh%yw(ekXz>hA1MH-9lVJKH>xmgWIw4_Rat2(0WeZtaT(3Kxx@^1@{5<-bw3yyC2j zBmAJn*%6w;E|b^rb*1%RH&Ex79 z`OUK>9SmqnaAT;O4H7r5EY^@reColN*{HCXpD$3IMc!AQt)@;SX7_8|wBJD;XU3ki zr{p*R(WrXMCY(pZ@ibKs)>k0C3`2cXNdkjLT5#@k`q3I4DT4~N6ATr+Uwcqg ztyXo+T|%|3zVNb7PAW6%vO;{1(PU48qL%sxSrwRwctm>}J)sE~@Aux*w6tG6{E!gY z%imsWynLo9wz-D+;;scAl%^z}UNG-w8tm{Hby9DGgLLId>%`rH+`-A=;kNMIRA-Q+ zPlk#zNQrl{iZPMfwa9Yom>gOa;YfI%Te_y-8<6B3*yt45)p38V!;v0O8NSsnE;JZMH75>t&}#a&SFBTu}kbKA{ji69J_KR7FhgN zx_vWBt!46d(Zua9;QC`kE8M5Y6yr;^ULhs`m{a#l(hgH6hG!ewE-+{`9?+%-&8fkA zI6@XPmWLZHZZj}Wg?70*ifsl-B!y4lMHfKdBh)!o&xLsn@m*`=I9RZaF02Nh<;0~N z5zS{2Y`sze-GU>8C>xIq;Y=N(cln4dB-Og07t!Ixr(Bd{ERxJ+3D^BoSHgB(!r!*a z8yjEKPd_&IwCrOvvsZ0rUiGx;#rv?3-yZS>kJ>HCk7-OzKGyUX2G4|SLKX)Sggy6; z{)mHZwW_jb-C(;i!vo7V(A|teERY6RfypAagv8AC0le%~$xK>XxT$&>|<(vYvVKY^M`xCrQ&0(fKF_ww{5Rvus*706E0CZ4R2ed7DX}L zS6nRXw=q&bZo)9in#?v61U=k29MXmbYBv*>9dWVmjRt*a@QKEYn#^yOY%QSE&#{75 zbU|4=e65}bpPJm0Y?D?XFny5VT27>ytZavh%vKh=UHhnFeSKbS3#x9`UzTqq-M_A3 z@V45br|~5x#qH|Vfut()Ln)=OtfW>h?MU-v%R9ZK%|wwzvCNUFQ!!G~HZkD4q9-eK zsQWm2y;%9I80TxDx>V ziqhiAWcxVU=N*}Z9)EJL%|RKYpx(nv_7Dd$oA_e{vtomUH2su&dXh7qx%=|e9=1Xt zI!zz8iC9Cpoy<9W%HSPVqTBcQ&Qa(|Uskezk)( z)SfwP8<06Oy->y$f8!IZ`1#dXXBoRq`{fac;vw$9Fy%dWV3ef3Z^SyaacI6aMQRxJp4&H! z;&K5ypHbZ9sYo<}j~@W||71SE-G9wrIQ{q6zhm-0e=iQ=pQQX>Xrd892XqAh_z(8Z KfoC52*YyWOamg3} literal 0 HcmV?d00001 diff --git a/public/lottie/trending-dark.lottie b/public/lottie/trending-dark.lottie new file mode 100644 index 0000000000000000000000000000000000000000..8cf537a92f6fb41673199936dd9805f5d8df1adf GIT binary patch literal 2460 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 S_yzvF0^Zx)J@5Ssx&94jN>ES$ literal 0 HcmV?d00001 diff --git a/public/lottie/view-dark.lottie b/public/lottie/view-dark.lottie new file mode 100644 index 0000000000000000000000000000000000000000..5a99974e51090a3e7518619ecd63c2f9d908212b GIT binary patch literal 2948 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 { + let network = localStorage.getItem('network'); + + if (network === null) { + network = DefaultNetwork; + localStorage.setItem('network', network); + } + + return ( + + + + + + + + + + ); +}; diff --git a/src/Providers.tsx b/src/Providers.tsx new file mode 100644 index 0000000000..e29983d323 --- /dev/null +++ b/src/Providers.tsx @@ -0,0 +1,108 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { BalancesProvider } from 'contexts/Balances'; +import { BondedProvider } from 'contexts/Bonded'; +import { + ExtensionsProvider, + ExtensionAccountsProvider, + OverlayProvider, +} from '@polkadot-cloud/react/providers'; +import { ExtrinsicsProvider } from 'contexts/Extrinsics'; +import { FastUnstakeProvider } from 'contexts/FastUnstake'; +import { FiltersProvider } from 'contexts/Filters'; +import { LedgerHardwareProvider } from 'contexts/Hardware/Ledger'; +import { VaultHardwareProvider } from 'contexts/Hardware/Vault'; +import { HelpProvider } from 'contexts/Help'; +import { IdentitiesProvider } from 'contexts/Identities'; +import { MenuProvider } from 'contexts/Menu'; +import { MigrateProvider } from 'contexts/Migrate'; +import { NetworkMetricsProvider } from 'contexts/NetworkMetrics'; +import { NotificationsProvider } from 'contexts/Notifications'; +import { PromptProvider } from 'contexts/Prompt'; +import { PluginsProvider } from 'contexts/Plugins'; +import { ActivePoolsProvider } from 'contexts/Pools/ActivePools'; +import { BondedPoolsProvider } from 'contexts/Pools/BondedPools'; +import { PoolMembersProvider } from 'contexts/Pools/PoolMembers'; +import { PoolMembershipsProvider } from 'contexts/Pools/PoolMemberships'; +import { PoolsConfigProvider } from 'contexts/Pools/PoolsConfig'; +import { ProxiesProvider } from 'contexts/Proxies'; +import { SetupProvider } from 'contexts/Setup'; +import { StakingProvider } from 'contexts/Staking'; +import { SubscanProvider } from 'contexts/Plugins/Subscan'; +import { TooltipProvider } from 'contexts/Tooltip'; +import { TransferOptionsProvider } from 'contexts/TransferOptions'; +import { TxMetaProvider } from 'contexts/TxMeta'; +import { UIProvider } from 'contexts/UI'; +import { ValidatorsProvider } from 'contexts/Validators/ValidatorEntries'; +import { FavoriteValidatorsProvider } from 'contexts/Validators/FavoriteValidators'; +import { PayoutsProvider } from 'contexts/Payouts'; +import { PolkawatchProvider } from 'contexts/Plugins/Polkawatch'; +import { useNetwork } from 'contexts/Network'; +import { APIProvider } from 'contexts/Api'; +import { ThemedRouter } from 'Themes'; +import type { AnyJson } from 'types'; +import type { FC } from 'react'; +import { withProviders } from 'library/Hooks'; +import { OtherAccountsProvider } from 'contexts/Connect/OtherAccounts'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { DappName } from 'consts'; +import { ImportedAccountsProvider } from 'contexts/Connect/ImportedAccounts'; +import { PoolPerformanceProvider } from 'contexts/Pools/PoolPerformance'; + +// Embed providers from hook. +export const Providers = () => { + const { + network, + networkData: { ss58 }, + } = useNetwork(); + const { activeAccount, setActiveAccount } = useActiveAccounts(); + + // !! Provider order matters + const providers: Array | [FC, AnyJson]> = [ + [APIProvider, { network }], + FiltersProvider, + NotificationsProvider, + PluginsProvider, + VaultHardwareProvider, + LedgerHardwareProvider, + ExtensionsProvider, + [ + ExtensionAccountsProvider, + { dappName: DappName, network, ss58, activeAccount, setActiveAccount }, + ], + OtherAccountsProvider, + ImportedAccountsProvider, + HelpProvider, + NetworkMetricsProvider, + SubscanProvider, + PolkawatchProvider, + IdentitiesProvider, + ProxiesProvider, + BalancesProvider, + BondedProvider, + StakingProvider, + PoolsConfigProvider, + BondedPoolsProvider, + PoolMembershipsProvider, + PoolMembersProvider, + ActivePoolsProvider, + TransferOptionsProvider, + ValidatorsProvider, + FavoriteValidatorsProvider, + FastUnstakeProvider, + PayoutsProvider, + PoolPerformanceProvider, + UIProvider, + SetupProvider, + MenuProvider, + TooltipProvider, + TxMetaProvider, + ExtrinsicsProvider, + OverlayProvider, + PromptProvider, + MigrateProvider, + ]; + + return <>{withProviders(providers, ThemedRouter)}; +}; diff --git a/src/Router.tsx b/src/Router.tsx new file mode 100644 index 0000000000..ab504685f1 --- /dev/null +++ b/src/Router.tsx @@ -0,0 +1,161 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { Body, Main, Page, Side } from '@polkadot-cloud/react'; +import { extractUrlValue } from '@polkadot-cloud/utils'; +import { AnimatePresence } from 'framer-motion'; +import { useEffect, useRef } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { Helmet } from 'react-helmet'; +import { useTranslation } from 'react-i18next'; +import { + HashRouter, + Navigate, + Route, + Routes, + useLocation, +} from 'react-router-dom'; +import { Prompt } from 'library/Prompt'; +import { PagesConfig } from 'config/pages'; +import { useNotifications } from 'contexts/Notifications'; +import { useUi } from 'contexts/UI'; +import { ErrorFallbackApp, ErrorFallbackRoutes } from 'library/ErrorBoundary'; +import { Headers } from 'library/Headers'; +import { Help } from 'library/Help'; +import { Menu } from 'library/Menu'; +import { NetworkBar } from 'library/NetworkBar'; +import { Notifications } from 'library/Notifications'; +import { SideMenu } from 'library/SideMenu'; +import { Tooltip } from 'library/Tooltip'; +import { Overlays } from 'overlay'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { SideMenuMaximisedWidth } from 'consts'; + +export const RouterInner = () => { + const { t } = useTranslation(); + const { network } = useNetwork(); + const { pathname } = useLocation(); + const { accounts } = useImportedAccounts(); + const { addNotification } = useNotifications(); + const { accountsInitialised } = useOtherAccounts(); + const { activeAccount, setActiveAccount } = useActiveAccounts(); + const { sideMenuOpen, sideMenuMinimised, setContainerRefs } = useUi(); + + // Scroll to top of the window on every page change or network change. + useEffect(() => { + window.scrollTo(0, 0); + }, [pathname, network]); + + // Set references to UI context and make available throughout app. + useEffect(() => { + setContainerRefs({ + mainInterface: mainInterfaceRef, + }); + }, []); + + // Open default account modal if url var present and accounts initialised. + useEffect(() => { + if (accountsInitialised) { + const aUrl = extractUrlValue('a'); + if (aUrl) { + const account = accounts.find((a) => a.address === aUrl); + if (account && aUrl !== activeAccount) { + setActiveAccount(account?.address || null); + addNotification({ + title: t('accountConnected', { ns: 'library' }), + subtitle: `${t('connectedTo', { ns: 'library' })} ${ + account?.name || aUrl + }.`, + }); + } + } + } + }, [accountsInitialised]); + + // References to outer containers + const mainInterfaceRef = useRef(null); + + return ( + + + {/* Help: closed by default */} + + + {/* Overlays: modal and canvas. Closed by default */} + + + {/* Menu: closed by default */} +
+ + {/* Tooltip: invisible by default */} + + + {/* Prompt: closed by default */} + + + {/* Left side menu */} + + + + + {/* Main content window */} +
+ {/* Fixed headers */} + + + + + + {PagesConfig.map((page, i) => { + const { Entry, hash, key } = page; + + return ( + + + {`${t(key, { ns: 'base' })} : ${t('title', { + context: `${network}`, + ns: 'base', + })}`} + + + + } + /> + ); + })} + } + /> + + + +
+ + + {/* Network status and network details */} + + + {/* Notification popups */} + + + ); +}; + +export const Router = () => ( + + + +); diff --git a/src/Themes.tsx b/src/Themes.tsx new file mode 100644 index 0000000000..1aa9738b58 --- /dev/null +++ b/src/Themes.tsx @@ -0,0 +1,23 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ThemeProvider } from 'styled-components'; +import { Entry } from '@polkadot-cloud/react'; +import { Router } from 'Router'; +import { useTheme } from 'contexts/Themes'; +import { useNetwork } from 'contexts/Network'; + +// light / dark `mode` added to styled-components provider +// `@polkadot-cloud/react` themes are added to `Entry`. +export const ThemedRouter = () => { + const { mode } = useTheme(); + const { network } = useNetwork(); + + return ( + + + + + + ); +}; diff --git a/src/canvas/ManageNominations/Prompts/FavoritesPrompt.tsx b/src/canvas/ManageNominations/Prompts/FavoritesPrompt.tsx new file mode 100644 index 0000000000..032af6190c --- /dev/null +++ b/src/canvas/ManageNominations/Prompts/FavoritesPrompt.tsx @@ -0,0 +1,104 @@ +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ButtonPrimary } from '@polkadot-cloud/react'; +import { useApi } from 'contexts/Api'; +import { useNotifications } from 'contexts/Notifications'; +import { useFavoriteValidators } from 'contexts/Validators/FavoriteValidators'; +import type { Validator } from 'contexts/Validators/types'; +import { Identity } from 'library/ListItem/Labels/Identity'; +import { SelectWrapper } from 'library/ListItem/Wrappers'; +import { Title } from 'library/Prompt/Title'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FooterWrapper, PromptListItem } from 'library/Prompt/Wrappers'; +import type { FavoritesPromptProps } from '../types'; + +export const FavoritesPrompt = ({ + callback, + nominations, +}: FavoritesPromptProps) => { + const { t } = useTranslation('modals'); + const { consts } = useApi(); + const { addNotification } = useNotifications(); + const { favoritesList } = useFavoriteValidators(); + const { maxNominations } = consts; + + // Store the total number of selected favorites. + const [selected, setSelected] = useState([]); + + const addToSelected = (item: Validator) => + setSelected([...selected].concat(item)); + + const removeFromSelected = (items: Validator[]) => + setSelected([...selected].filter((item) => !items.includes(item))); + + const remaining = maxNominations + .minus(nominations.length) + .minus(selected.length); + + const canAdd = remaining.isGreaterThan(0); + + return ( + <> + + <div className="padded"> + {remaining.isZero() ? ( + <h4 className="subheading"> + {t('moreFavoritesSurpassLimit', { + max: maxNominations.toString(), + })} + </h4> + ) : ( + <h4 className="subheading"> + {t('addUpToFavorites', { count: remaining.toNumber() })}. + </h4> + )} + + {favoritesList?.map((favorite: Validator, i) => { + const inInitial = !!nominations.find( + ({ address }: Validator) => address === favorite.address + ); + const isDisabled = + selected.includes(favorite) || !canAdd || inInitial; + + return ( + <PromptListItem + key={`favorite_${i}`} + className={isDisabled && inInitial ? 'inactive' : undefined} + > + <SelectWrapper + disabled={inInitial} + onClick={() => { + if (selected.includes(favorite)) + removeFromSelected([favorite]); + else addToSelected(favorite); + }} + > + {(inInitial || selected.includes(favorite)) && ( + <FontAwesomeIcon icon={faCheck} transform="shrink-2" /> + )} + </SelectWrapper> + <Identity key={`favorite_${i}`} address={favorite.address} /> + </PromptListItem> + ); + })} + + <FooterWrapper> + <ButtonPrimary + text={t('addToNominations')} + onClick={() => { + callback(nominations.concat(selected)); + addNotification({ + title: t('favoritesAddedTitle', { count: selected.length }), + subtitle: t('favoritesAddedSubtitle', { + count: selected.length, + }), + }); + }} + disabled={selected.length === 0} + /> + </FooterWrapper> + </div> + </> + ); +}; diff --git a/src/canvas/ManageNominations/Prompts/RevertPrompt.tsx b/src/canvas/ManageNominations/Prompts/RevertPrompt.tsx new file mode 100644 index 0000000000..06a44dddc8 --- /dev/null +++ b/src/canvas/ManageNominations/Prompts/RevertPrompt.tsx @@ -0,0 +1,25 @@ +import { ButtonPrimary } from '@polkadot-cloud/react'; +import { Title } from 'library/Prompt/Title'; +import { useTranslation } from 'react-i18next'; +import { FooterWrapper } from 'library/Prompt/Wrappers'; +import type { RevertPromptProps } from '../types'; + +export const RevertPrompt = ({ onRevert }: RevertPromptProps) => { + const { t } = useTranslation('modals'); + + return ( + <> + <Title title={t('revertNominations')} closeText={t('cancel')} /> + <div className="body"> + <h4 className="subheading">{t('revertNominationChanges')}</h4> + <FooterWrapper> + <ButtonPrimary + marginRight + text={t('revertChanges')} + onClick={() => onRevert()} + /> + </FooterWrapper> + </div> + </> + ); +}; diff --git a/src/canvas/ManageNominations/Wrappers.tsx b/src/canvas/ManageNominations/Wrappers.tsx new file mode 100644 index 0000000000..483a89fb90 --- /dev/null +++ b/src/canvas/ManageNominations/Wrappers.tsx @@ -0,0 +1,27 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const ManageNominationsWrapper = styled.div` + padding-top: 3rem; + min-height: calc(100vh - 12rem); + padding-bottom: 2rem; + + > .head { + display: flex; + align-items: center; + justify-content: flex-end; + } + + > h1 { + margin-top: 1.5rem; + margin-bottom: 1.25rem; + } +`; + +export const CanvasSubmitTxFooter = styled.div` + border-radius: 1rem; + overflow: hidden; + margin-bottom: 2rem; +`; diff --git a/src/canvas/ManageNominations/index.tsx b/src/canvas/ManageNominations/index.tsx new file mode 100644 index 0000000000..9a04d337e3 --- /dev/null +++ b/src/canvas/ManageNominations/index.tsx @@ -0,0 +1,208 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + ButtonHelp, + ButtonPrimary, + ButtonPrimaryInvert, +} from '@polkadot-cloud/react'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { GenerateNominations } from 'library/GenerateNominations'; +import { useEffect, useState } from 'react'; +import { Subheading } from 'pages/Nominate/Wrappers'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { usePrompt } from 'contexts/Prompt'; +import { useHelp } from 'contexts/Help'; +import { useNotifications } from 'contexts/Notifications'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useBonded } from 'contexts/Bonded'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { SubmitTx } from 'library/SubmitTx'; +import type { + NominationSelection, + NominationSelectionWithResetCounter, +} from 'library/GenerateNominations/types'; +import { RevertPrompt } from './Prompts/RevertPrompt'; +import { CanvasSubmitTxFooter, ManageNominationsWrapper } from './Wrappers'; + +export const ManageNominations = () => { + const { t } = useTranslation('library'); + const { + closeCanvas, + setCanvasStatus, + config: { options }, + } = useOverlay().canvas; + const { openHelp } = useHelp(); + const { consts, api } = useApi(); + const { getBondedAccount } = useBonded(); + const { activeAccount } = useActiveAccounts(); + const { addNotification } = useNotifications(); + const { selectedActivePool } = useActivePools(); + const { openPromptWith, closePrompt } = usePrompt(); + const controller = getBondedAccount(activeAccount); + const { maxNominations } = consts; + const bondFor = options?.bondFor || 'nominator'; + const isPool = bondFor === 'pool'; + const signingAccount = isPool ? activeAccount : controller; + + // Valid to submit transaction. + const [valid, setValid] = useState<boolean>(false); + + // Default nominators, from canvas options. + const [defaultNominations, setDefaultNominations] = + useState<NominationSelectionWithResetCounter>({ + nominations: options?.nominated || [], + reset: 0, + }); + + // Current nominator selection, defaults to defaultNominations. + const [newNominations, setNewNominations] = useState<NominationSelection>({ + nominations: options?.nominated || [], + }); + + // Handler for updating setup. + const handleSetupUpdate = (value: NominationSelection) => { + setNewNominations(value); + }; + + // Handler for reverting nomination updates. + const handleRevertChanges = () => { + setNewNominations({ nominations: defaultNominations.nominations }); + setDefaultNominations({ + nominations: defaultNominations.nominations, + reset: defaultNominations.reset + 1, + }); + addNotification({ + title: t('nominationsReverted'), + subtitle: t('revertedToActiveSelection'), + }); + closePrompt(); + }; + + // Check if default nominations match new ones. + const nominationsMatch = () => { + return ( + newNominations.nominations.every((n) => + defaultNominations.nominations.find((d) => d.address === n.address) + ) && + newNominations.nominations.length > 0 && + newNominations.nominations.length === + defaultNominations.nominations.length + ); + }; + + // Tx to submit. + const getTx = () => { + let tx = null; + if (!valid || !api) { + return tx; + } + + // Note: `targets` structure differs between staking and pools. + const targetsToSubmit = newNominations.nominations.map((nominee) => + isPool + ? nominee.address + : { + Id: nominee.address, + } + ); + + if (isPool) { + tx = api.tx.nominationPools.nominate( + selectedActivePool?.id || 0, + targetsToSubmit + ); + } else { + tx = api.tx.staking.nominate(targetsToSubmit); + } + return tx; + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: signingAccount, + shouldSubmit: valid, + callbackSubmit: () => { + setCanvasStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + // Valid if there are between 1 and `maxNominations` nominations. + useEffect(() => { + setValid( + maxNominations.isGreaterThanOrEqualTo( + newNominations.nominations.length + ) && + newNominations.nominations.length > 0 && + !nominationsMatch() + ); + }, [newNominations]); + + return ( + <> + <ManageNominationsWrapper> + <div className="head"> + <ButtonPrimaryInvert + text={t('revertChanges', { ns: 'modals' })} + lg + onClick={() => { + openPromptWith(<RevertPrompt onRevert={handleRevertChanges} />); + }} + disabled={ + newNominations.nominations === defaultNominations.nominations + } + /> + <ButtonPrimary + text={t('cancel', { ns: 'library' })} + lg + onClick={() => closeCanvas()} + iconLeft={faTimes} + style={{ marginLeft: '1.1rem' }} + /> + </div> + <h1>{t('manageNominations', { ns: 'modals' })}</h1> + + <Subheading> + <h3 style={{ marginBottom: '1.5rem' }}> + {t('chooseValidators', { + ns: 'library', + maxNominations: maxNominations.toString(), + })} + <ButtonHelp + onClick={() => openHelp('Nominations')} + background="none" + outline + /> + </h3> + </Subheading> + + <GenerateNominations + displayFor="canvas" + setters={[ + { + current: { + callable: true, + fn: () => newNominations, + }, + set: handleSetupUpdate, + }, + ]} + nominations={defaultNominations} + /> + </ManageNominationsWrapper> + <CanvasSubmitTxFooter> + <SubmitTx + noMargin + fromController={!isPool} + valid={valid} + displayFor="canvas" + {...submitExtrinsic} + /> + </CanvasSubmitTxFooter> + </> + ); +}; diff --git a/src/canvas/ManageNominations/types.ts b/src/canvas/ManageNominations/types.ts new file mode 100644 index 0000000000..05581f16f7 --- /dev/null +++ b/src/canvas/ManageNominations/types.ts @@ -0,0 +1,13 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { Validator } from 'contexts/Validators/types'; + +export interface FavoritesPromptProps { + callback: (newNominations: Validator[]) => void; + nominations: Validator[]; +} + +export interface RevertPromptProps { + onRevert: () => void; +} diff --git a/src/config/help.ts b/src/config/help.ts new file mode 100644 index 0000000000..caadcb7d93 --- /dev/null +++ b/src/config/help.ts @@ -0,0 +1,163 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { HelpItems } from 'contexts/Help/types'; + +export const HelpConfig: HelpItems = [ + { + key: 'vault', + definitions: ['Polkadot Vault'], + }, + { + key: 'overview', + definitions: [ + 'Total Nominators', + 'Active Nominators', + 'Your Balance', + 'Reserve Balance', + 'Locked Balance', + 'Historical Rewards Rate', + 'Adjusted Rewards Rate', + 'Inflation', + 'Ideal Staked', + 'Supply Staked', + 'Read Only Accounts', + 'Proxy Accounts', + 'Reserve Balance For Existential Deposit', + ], + external: [ + [ + 'connectAccounts', + 'https://support.polkadot.network/support/solutions/articles/65000182121-how-to-use-the-staking-dashboard-connecting-your-account', + 'polkadot.network', + ], + [ + 'howToUse', + 'https://support.polkadot.network/support/solutions/articles/65000182104-how-to-use-the-staking-dashboard-overview', + 'polkadot.network', + ], + [ + 'stakeDot', + 'https://support.polkadot.network/support/solutions/articles/65000182104-how-to-use-the-staking-dashboard-overview', + 'polkadot.network', + ], + ], + }, + { + key: 'nominate', + definitions: [ + 'Nomination Status', + 'Bonding', + 'Active Stake Threshold', + 'Payout Destination', + 'Nominating', + 'Nominations', + 'Inactive Nominations', + ], + external: [ + [ + 'changeDestination', + 'https://support.polkadot.network/support/solutions/articles/65000182220-how-to-use-the-staking-dashboard-changing-reward-destination', + 'polkadot.network', + ], + [ + 'bondMore', + 'https://support.polkadot.network/support/solutions/articles/65000182207-how-to-use-the-staking-dashboard-bond-more-tokens-to-your-existing-stake', + 'polkadot.network', + ], + [ + 'unbondingTokens', + 'https://support.polkadot.network/support/solutions/articles/65000182201-how-to-use-the-staking-dashboard-unbonding-your-tokens', + 'polkadot.network', + ], + [ + 'rebonding', + 'https://support.polkadot.network/support/solutions/articles/65000182221-how-to-use-the-staking-dashboard-rebonding', + 'polkadot.network', + ], + [ + 'changeAccount', + 'https://support.polkadot.network/support/solutions/articles/65000182218-how-to-use-the-staking-dashboard-changing-your-controller-account', + 'polkadot.network', + ], + [ + 'changeNominations', + 'https://support.polkadot.network/support/solutions/articles/65000182518-how-to-use-the-staking-dashboard-changing-your-nominations', + 'polkadot.network', + ], + ], + }, + { + key: 'pools', + definitions: [ + 'Nomination Pools', + 'Active Pools', + 'Minimum To Join Pool', + 'Minimum To Create Pool', + 'Pool Membership', + 'Bonded in Pool', + 'Pool Rewards', + 'Pool Roles', + 'Pool Commission Rate', + 'Pool Max Commission', + 'Pool Commission Change Rate', + ], + external: [ + [ + 'createPools', + 'https://support.polkadot.network/support/solutions/articles/65000182388-how-to-use-the-staking-dashboard-creating-nomination-pools', + 'polkadot.network', + ], + [ + 'claimRewards', + 'https://support.polkadot.network/support/solutions/articles/65000182399-how-to-use-staking-dashboard-claiming-nomination-pool-rewards', + 'polkadot.network', + ], + ], + }, + { + key: 'validators', + definitions: [ + 'Validator', + 'Active Validator', + 'Average Commission', + 'Era', + 'Epoch', + 'Era Points', + 'Self Stake', + 'Nominator Stake', + 'Commission', + 'Over Subscribed', + 'Blocked Nominations', + 'Rewards By Country And Network', + ], + external: [ + [ + 'chooseValidators', + 'https://support.polkadot.network/support/solutions/articles/65000150130-how-do-i-know-which-validators-to-choose-', + 'polkadot.network', + ], + ], + }, + { + key: 'payouts', + definitions: ['Payout', 'Last Era Payout', 'Payout History'], + external: [], + }, + { + key: 'community', + definitions: [], + external: [], + }, + { + key: 'ledger', + definitions: [ + 'Ledger Hardware Wallets', + 'Ledger Rejected Transaction', + 'Ledger Request Timeout', + 'Open App On Ledger', + 'Wrong Transaction', + ], + external: [], + }, +]; diff --git a/src/config/ledger.ts b/src/config/ledger.ts new file mode 100644 index 0000000000..597b0183ef --- /dev/null +++ b/src/config/ledger.ts @@ -0,0 +1,19 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { LedgerApp } from 'contexts/Hardware/types'; +import KusamaSVG from 'img/appIcons/kusama.svg?react'; +import PolkadotSVG from 'img/appIcons/polkadot.svg?react'; + +export const LedgerApps: LedgerApp[] = [ + { + network: 'polkadot', + appName: 'Polkadot', + Icon: PolkadotSVG, + }, + { + network: 'kusama', + appName: 'Kusama', + Icon: KusamaSVG, + }, +]; diff --git a/src/config/networks.ts b/src/config/networks.ts new file mode 100644 index 0000000000..d0c5313ec7 --- /dev/null +++ b/src/config/networks.ts @@ -0,0 +1,224 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { WellKnownChain } from '@substrate/connect'; +import { DefaultParams } from 'consts'; +import KusamaIconSVG from 'img/kusama_icon.svg?react'; +import KusamaInlineSVG from 'img/kusama_inline.svg?react'; +import KusamaLogoSVG from 'img/kusama_logo.svg?react'; +import PolkadotIconSVG from 'img/polkadot_icon.svg?react'; +import PolkadotInlineSVG from 'img/polkadot_inline.svg?react'; +import PolkadotLogoSVG from 'img/polkadot_logo.svg?react'; +import WestendIconSVG from 'img/westend_icon.svg?react'; +import WestendInlineSVG from 'img/westend_inline.svg?react'; +import WestendLogoSVG from 'img/westend_logo.svg?react'; +import PolkadotTokenSVG from 'config/tokens/svg/DOT.svg?react'; +import KusamaTokenSVG from 'config/tokens/svg/KSM.svg?react'; +import WestendTokenSVG from 'config/tokens/svg/WND.svg?react'; + +import type { Networks } from 'types'; + +export const NetworkList: Networks = { + polkadot: { + name: 'polkadot', + endpoints: { + lightClient: WellKnownChain.polkadot, + defaultRpcEndpoint: 'Parity', + rpcEndpoints: { + 'Automata 1RPC': 'wss://1rpc.io/dot', + Dwellir: 'wss://polkadot-rpc.dwellir.com', + 'Dwellir Tunisia': 'wss://polkadot-rpc-tn.dwellir.com', + 'IBP-GeoDNS1': 'wss://rpc.ibp.network/polkadot', + 'IBP-GeoDNS2': 'wss://rpc.dotters.network/polkadot', + LuckyFriday: 'wss://rpc-polkadot.luckyfriday.io', + OnFinality: 'wss://polkadot.api.onfinality.io/public-ws', + RadiumBlock: 'wss://polkadot.public.curie.radiumblock.co/ws', + Stakeworld: 'wss://dot-rpc.stakeworld.io', + Parity: 'wss://apps-rpc.polkadot.io', + }, + }, + namespace: '91b171bb158e2d3848fa23a9f1c25182', + colors: { + primary: { + light: 'rgb(211, 48, 121)', + dark: 'rgb(211, 48, 121)', + }, + secondary: { + light: '#552bbf', + dark: '#6d39ee', + }, + stroke: { + light: 'rgb(211, 48, 121)', + dark: 'rgb(211, 48, 121)', + }, + transparent: { + light: 'rgb(211, 48, 121, 0.05)', + dark: 'rgb(211, 48, 121, 0.05)', + }, + pending: { + light: 'rgb(211, 48, 121, 0.33)', + dark: 'rgb(211, 48, 121, 0.33)', + }, + }, + subscanEndpoint: 'https://polkadot.api.subscan.io', + unit: 'DOT', + units: 10, + ss58: 0, + brand: { + icon: PolkadotIconSVG, + token: PolkadotTokenSVG, + logo: { + svg: PolkadotLogoSVG, + width: '7.2em', + }, + inline: { + svg: PolkadotInlineSVG, + size: '1.05em', + }, + }, + api: { + unit: 'DOT', + priceTicker: 'DOTUSDT', + }, + params: { + ...DefaultParams, + stakeTarget: 0.75, + }, + defaultFeeReserve: 0.1, + }, + kusama: { + name: 'kusama', + endpoints: { + lightClient: WellKnownChain.ksmcc3, + defaultRpcEndpoint: 'Parity', + rpcEndpoints: { + 'Automata 1RPC': 'wss://1rpc.io/ksm', + Dwellir: 'wss://kusama-rpc.dwellir.com', + 'Dwellir Tunisia': 'wss://kusama-rpc-tn.dwellir.com', + 'IBP-GeoDNS1': 'wss://rpc.ibp.network/kusama', + 'IBP-GeoDNS2': 'wss://rpc.dotters.network/kusama', + LuckyFriday: 'wss://rpc-kusama.luckyfriday.io', + OnFinality: 'wss://kusama.api.onfinality.io/public-ws', + RadiumBlock: 'wss://kusama.public.curie.radiumblock.co/ws', + Stakeworld: 'wss://ksm-rpc.stakeworld.io', + Parity: 'wss://kusama-rpc.polkadot.io', + }, + }, + namespace: 'b0a8d493285c2df73290dfb7e61f870f', + colors: { + primary: { + light: 'rgb(31, 41, 55)', + dark: 'rgb(126, 131, 141)', + }, + secondary: { + light: 'rgb(31, 41, 55)', + dark: 'rgb(141, 144, 150)', + }, + stroke: { + light: '#4c4b63', + dark: '#d1d1db', + }, + transparent: { + light: 'rgb(51,51,51,0.05)', + dark: 'rgb(102,102,102, 0.05)', + }, + pending: { + light: 'rgb(51,51,51,0.33)', + dark: 'rgb(102,102,102, 0.33)', + }, + }, + subscanEndpoint: 'https://kusama.api.subscan.io', + unit: 'KSM', + units: 12, + ss58: 2, + brand: { + icon: KusamaIconSVG, + token: KusamaTokenSVG, + logo: { + svg: KusamaLogoSVG, + width: '7.2em', + }, + inline: { + svg: KusamaInlineSVG, + size: '1.35em', + }, + }, + api: { + unit: 'KSM', + priceTicker: 'KSMUSDT', + }, + params: { + ...DefaultParams, + auctionAdjust: 0.3 / 60, + auctionMax: 60, + stakeTarget: 0.75, + }, + defaultFeeReserve: 0.05, + }, + westend: { + name: 'westend', + endpoints: { + lightClient: WellKnownChain.westend2, + defaultRpcEndpoint: 'Parity', + rpcEndpoints: { + Dwellir: 'wss://westend-rpc.dwellir.com', + 'Dwellir Tunisia': 'wss://westend-rpc-tn.dwellir.com', + 'IBP-GeoDNS1': 'wss://rpc.ibp.network/westend', + 'IBP-GeoDNS2': 'wss://rpc.dotters.network/westend', + LuckyFriday: 'wss://rpc-westend.luckyfriday.io', + OnFinality: 'wss://westend.api.onfinality.io/public-ws', + RadiumBlock: 'wss://westend.public.curie.radiumblock.co/ws', + Stakeworld: 'wss://wnd-rpc.stakeworld.io', + Parity: 'wss://westend-rpc.polkadot.io', + }, + }, + namespace: 'e143f23803ac50e8f6f8e62695d1ce9e', + colors: { + primary: { + light: '#da4e71', + dark: '#da4e71', + }, + secondary: { + light: '#de6a50', + dark: '#d7674e', + }, + stroke: { + light: '#da4e71', + dark: '#da4e71', + }, + transparent: { + light: 'rgb(218, 78, 113, 0.05)', + dark: 'rgb(218, 78, 113, 0.05)', + }, + pending: { + light: 'rgb(218, 78, 113, 0.33)', + dark: 'rgb(218, 78, 113, 0.33)', + }, + }, + subscanEndpoint: 'https://westend.api.subscan.io', + unit: 'WND', + units: 12, + ss58: 42, + brand: { + icon: WestendIconSVG, + token: WestendTokenSVG, + logo: { + svg: WestendLogoSVG, + width: '7.1em', + }, + inline: { + svg: WestendInlineSVG, + size: '0.96em', + }, + }, + api: { + unit: 'DOT', + priceTicker: 'DOTUSDT', + }, + params: { + ...DefaultParams, + stakeTarget: 0.75, + }, + defaultFeeReserve: 0.1, + }, +}; diff --git a/src/config/pages.ts b/src/config/pages.ts new file mode 100644 index 0000000000..25e1c12d97 --- /dev/null +++ b/src/config/pages.ts @@ -0,0 +1,77 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { Community } from 'pages/Community'; +import { Nominate } from 'pages/Nominate'; +import { Overview } from 'pages/Overview'; +import { Payouts } from 'pages/Payouts'; +import { Pools } from 'pages/Pools'; +import { Validators } from 'pages/Validators'; +import type { PageCategoryItems, PagesConfigItems } from 'types'; + +const BASE_URL = import.meta.env.BASE_URL; +export const PageCategories: PageCategoryItems = [ + { + id: 1, + key: 'default', + }, + { + id: 2, + key: 'stake', + }, + { + id: 3, + key: 'validators', + }, +]; + +export const PagesConfig: PagesConfigItems = [ + { + category: 1, + key: 'overview', + uri: `${BASE_URL}`, + hash: '/overview', + Entry: Overview, + lottie: 'globe', + }, + { + category: 2, + key: 'pools', + uri: `${BASE_URL}pools`, + hash: '/pools', + Entry: Pools, + lottie: 'groups', + }, + { + category: 2, + key: 'nominate', + uri: `${BASE_URL}nominate`, + hash: '/nominate', + Entry: Nominate, + lottie: 'trending', + }, + { + category: 2, + key: 'payouts', + uri: `${BASE_URL}payouts`, + hash: '/payouts', + Entry: Payouts, + lottie: 'analytics', + }, + { + category: 3, + key: 'validators', + uri: `${BASE_URL}validators`, + hash: '/validators', + Entry: Validators, + lottie: 'view', + }, + { + category: 3, + key: 'community', + uri: `${BASE_URL}community`, + hash: '/community', + Entry: Community, + lottie: 'label', + }, +]; diff --git a/src/config/proxies.ts b/src/config/proxies.ts new file mode 100644 index 0000000000..618e46571f --- /dev/null +++ b/src/config/proxies.ts @@ -0,0 +1,60 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export const SupportedProxies: Record<string, string[]> = { + Any: ['*'], + Staking: [ + 'staking.bond', + 'staking.bondExtra', + 'staking.chill', + 'staking.nominate', + 'staking.rebond', + 'staking.setController', + 'staking.setPayee', + 'staking.unbond', + 'staking.withdrawUnbonded', + 'nominationPools.create', + 'nominationPools.nominate', + 'nominationPools.bondExtra', + 'nominationPools.chill', + 'nominationPools.claimPayout', + 'nominationPools.join', + 'nominationPools.setClaimPermission', + 'nominationPools.claimCommission', + 'nominationPools.setCommission', + 'nominationPools.setCommissionMax', + 'nominationPools.setCommissionChangeRate', + 'nominationPools.unbond', + 'nominationPools.setMetadata', + 'nominationPools.setState', + 'nominationPools.withdrawUnbonded', + 'fastUnstake.registerFastUnstake', + 'fastUnstake.deregister', + ], +}; + +export const UnsupportedIfUniqueController: string[] = [ + 'staking.chill', + 'staking.nominate', + 'staking.rebond', + 'staking.unbond', + 'staking.setPayee', + 'staking.withdrawUnbonded', +]; + +export const isSupportedProxy = (proxy: string) => + Object.keys(SupportedProxies).includes(proxy) || proxy === 'Any'; + +export const isSupportedProxyCall = ( + proxy: string, + pallet: string, + method: string +) => { + if ([method, pallet].includes('undefined')) { + return false; + } + + const call = `${pallet}.${method}`; + const calls = SupportedProxies[proxy]; + return (calls || []).find((c) => ['*', call].includes(c)) !== undefined; +}; diff --git a/src/config/tips.ts b/src/config/tips.ts new file mode 100644 index 0000000000..282ede798d --- /dev/null +++ b/src/config/tips.ts @@ -0,0 +1,53 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export const TipsConfig = [ + { + id: 'connectExtensions', + s: 1, + }, + { + id: 'recommendedNominator', + s: 2, + page: 'nominate', + }, + { + id: 'recommendedJoinPool', + s: 3, + page: 'pools', + }, + { + id: 'howToStake', + s: 4, + }, + { + id: 'managingNominations', + s: 5, + page: 'nominate', + }, + { + id: 'monitoringPool', + s: 6, + page: 'pools', + }, + { + id: 'joinAnotherPool', + s: 6, + page: 'pools', + }, + { + id: 'keepPoolNominating', + s: 7, + page: 'pools', + }, + { + id: 'reviewingPayouts', + s: 8, + page: 'payouts', + }, + { + id: 'understandingValidatorPerformance', + s: 8, + page: 'validators', + }, +]; diff --git a/src/config/tokens/svg/DOT.svg b/src/config/tokens/svg/DOT.svg new file mode 100644 index 0000000000..d9badbeba9 --- /dev/null +++ b/src/config/tokens/svg/DOT.svg @@ -0,0 +1 @@ +<svg version="1.1" width="100%" height="100%" viewBox="0 0 310 310" fill="none" xmlns="http://www.w3.org/2000/svg"><g><circle cx="155" cy="151" r="146" fill="#eee" stroke="#ccc" stroke-width="10"/><path d="M154.962 89.56c21.165 0 38.322-9.975 38.322-22.28 0-12.305-17.157-22.28-38.322-22.28-21.165 0-38.322 9.975-38.322 22.28 0 12.305 17.157 22.28 38.322 22.28zm0 168.44c21.165 0 38.322-9.975 38.322-22.28 0-12.305-17.157-22.281-38.322-22.281-21.165 0-38.322 9.976-38.322 22.281 0 12.305 17.157 22.28 38.322 22.28zm-53.497-137.409c10.597-18.369 10.528-38.263-.154-44.435-10.683-6.172-27.934 3.715-38.53 22.084-10.598 18.369-10.53 38.263.153 44.435 10.683 6.172 27.934-3.715 38.531-22.084zm145.627 84.14c10.598-18.369 10.534-38.26-.141-44.428-10.675-6.168-27.92 3.723-38.517 22.091-10.598 18.369-10.535 38.26.141 44.428 10.675 6.168 27.92-3.723 38.517-22.091zm-145.776 22.086c10.683-6.172 10.752-26.067.154-44.435-10.597-18.369-27.848-28.257-38.53-22.084-10.683 6.172-10.752 26.066-.154 44.435 10.597 18.369 27.848 28.256 38.53 22.084zM246.96 142.68c10.676-6.168 10.739-26.059.141-44.428-10.597-18.369-27.842-28.26-38.517-22.091-10.675 6.168-10.739 26.059-.141 44.427 10.597 18.369 27.842 28.26 38.517 22.092z" fill="#E6007A"/></g></svg> \ No newline at end of file diff --git a/src/config/tokens/svg/KSM.svg b/src/config/tokens/svg/KSM.svg new file mode 100644 index 0000000000..98ed7699fd --- /dev/null +++ b/src/config/tokens/svg/KSM.svg @@ -0,0 +1,4 @@ +<svg version="1.1" width="100%" height="100%" viewBox="0 0 302 302" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle cx="151" cy="151" r="151" fill="black"/> +<path d="M227.655 94.7713C224.991 92.6735 221.816 89.8082 216.029 89.0919C210.6 88.3756 205.069 92.0083 201.33 94.4131C197.591 96.8179 190.523 103.879 187.604 106.028C184.685 108.177 177.207 110.172 165.172 117.386C153.136 124.601 105.916 154.891 105.916 154.891L118.207 155.044L63.4071 183.236H68.8872L61 189.223C61 189.223 67.9653 191.065 73.8038 187.381V189.069C73.8038 189.069 139.052 163.384 151.651 170.036L143.969 172.287C144.635 172.287 157.029 173.106 157.029 173.106C157.029 173.106 157.438 180.832 164.916 185.795C172.393 190.707 172.547 193.418 172.547 193.418C172.547 193.418 168.655 195.005 168.655 197C168.655 197 174.391 195.26 179.717 195.414C185.043 195.567 189.704 197 189.704 197C189.704 197 189.294 194.851 184.122 193.418C178.898 191.935 173.827 186.358 171.318 183.288C168.808 180.218 167.067 174.743 169.218 169.268C171.01 164.612 177.259 162.054 190.165 155.402C205.376 147.523 208.859 141.69 211.01 137.136C213.161 132.583 216.336 123.526 218.128 119.28C220.382 113.805 223.148 110.888 225.452 109.149C227.706 107.409 238 103.572 238 103.572C238 103.572 230.164 96.7667 227.655 94.7713Z" fill="#EEEEEE"/> +</svg> diff --git a/src/config/tokens/svg/WND.svg b/src/config/tokens/svg/WND.svg new file mode 100644 index 0000000000..83d722fb81 --- /dev/null +++ b/src/config/tokens/svg/WND.svg @@ -0,0 +1 @@ +<svg version="1.1" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1671.16 1671.66"><defs><linearGradient id="b" x1="40.98" y1="835.47" x2="1774.13" y2="836.99" gradientTransform="matrix(1 0 0 -1 -.84 1672)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f79420" stop-opacity=".92"/><stop offset="1" stop-color="#c4c4c4" stop-opacity="0"/></linearGradient><mask id="a" x="0" y="0" width="1671.16" height="1671.66" maskUnits="userSpaceOnUse"><circle cx="836.27" cy="836.22" r="835.44" fill="#c4c4c4" transform="translate(-.84)"/></mask></defs><circle cx="835.44" cy="836.22" r="835.44" fill="#e6007a"/><g mask="url(#a)"><path fill="url(#b)" d="M.66 0h1670.5v1671.65H.66z"/></g><path d="M533.46 1196.48H735.4L799.78 963c8.87-32.19 54.14-32.17 63 0l64.15 233.43h201.55l154.53-597h-189.23l-48.32 237c-7 34.4-55.41 35.35-63.75 1.25l-58.25-238.16h-184.6l-56.7 239.64c-8.11 34.22-56.6 33.55-63.78-.87l-17-81.7c-19.22-91.53-99.22-157.07-192-157.07h-30.06z" fill="#fff"/></svg> \ No newline at end of file diff --git a/src/config/validators/Amforc.tsx b/src/config/validators/Amforc.tsx new file mode 100644 index 0000000000..3f2afb9f3b --- /dev/null +++ b/src/config/validators/Amforc.tsx @@ -0,0 +1,52 @@ +const Amforc = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 94.12 94.24"> + <defs> + <linearGradient + id="a" + x1="46.57" + y1="97.04" + x2="47.53" + y2="2.32" + gradientTransform="matrix(1 0 0 -1 0 96.38)" + gradientUnits="userSpaceOnUse" + > + <stop offset="0" stopColor="#507e99" /> + <stop offset=".78" stopColor="#8fd698" /> + <stop offset="1" stopColor="#aad4a0" /> + </linearGradient> + </defs> + <path + d="M77.22 94.11H16.89A16.89 16.89 0 010 77.22V16.89A16.89 16.89 0 0116.89 0h60.33a16.89 16.89 0 0116.89 16.89v60.33a16.89 16.89 0 01-16.89 16.89z" + fill="#1d5c81" + /> + <path + d="M91.09 86.85L60 10.16l-34 84h4.91l29-72.41 27.25 69.11a17 17 0 003.93-4.01z" + fill="#8fd698" + /> + <path fill="#3a8286" d="M20.53 39.49h41.9v4.27h-41.9z" /> + <path fill="#8fd698" d="M38.94 63.06h41.93v4.33H38.94z" /> + <path + fill="#3a8286" + d="M31.6 51.41h41.89v4.36H31.6zM63.12 57.09h30.99v4.45H63.12z" + /> + <path fill="#8fd698" d="M5.72 45.46h35.6v4.36H5.72z" /> + <path + d="M77.22 94.11H16.89A16.89 16.89 0 010 77.22V16.89A16.89 16.89 0 0116.89 0h60.33a16.89 16.89 0 0116.89 16.89v60.33a16.89 16.89 0 01-16.89 16.89z" + fill="url(#a)" + /> + <path + d="M60.24 10.74h-.39l-33.8 83.5H31l29-72.41L87.18 91a16.81 16.81 0 003.93-4z" + fill="#fff" + /> + <path + fill="#3a8286" + d="M12.85 39.49h41.9v4.27h-41.9zM23.93 51.41h41.89v4.36H23.93zM55.45 57.09h38.67v4.45H55.45z" + /> + <path + fill="#114a62" + d="M0 45.46h33.64v4.36H0zM31.26 63.06h41.93v4.33H31.26z" + /> + </svg> +); + +export default Amforc; diff --git a/src/config/validators/ApertureMining.tsx b/src/config/validators/ApertureMining.tsx new file mode 100644 index 0000000000..c9199eeaec --- /dev/null +++ b/src/config/validators/ApertureMining.tsx @@ -0,0 +1,97 @@ +const ApertureMining = () => ( + <svg + version="1.0" + xmlns="http://www.w3.org/2000/svg" + width="376.000000pt" + height="376.000000pt" + viewBox="0 0 376.000000 376.000000" + preserveAspectRatio="xMidYMid meet" + > + <g + transform="translate(0.000000,376.000000) scale(0.100000,-0.100000)" + fill="#000000" + stroke="none" + > + <path + d="M1683 3713 c-27 -4 -58 -32 -185 -169 -84 -90 -154 -169 -156 -174 + -2 -7 287 -10 807 -10 446 0 811 3 811 8 0 14 -166 114 -285 172 -259 126 + -513 182 -808 178 -84 0 -167 -3 -184 -5z" + /> + <path + d="M1422 3665 c-146 -38 -206 -61 -367 -141 -193 -95 -309 -178 -472 + -339 l-133 -131 0 -249 0 -250 311 310 c378 377 798 825 772 825 -10 -1 -60 + -12 -111 -25z" + /> + <path + d="M3125 2730 c314 -314 571 -566 573 -561 5 16 -49 230 -80 321 -95 + 270 -268 536 -474 729 l-87 81 -251 0 -251 0 570 -570z" + /> + <path + d="M1810 3230 c-19 -4 -26 -14 -30 -40 -4 -27 -10 -36 -27 -38 -16 -2 + -23 2 -23 12 0 23 -14 20 -45 -11 -35 -36 -53 -71 -61 -122 -4 -24 -12 -41 + -20 -41 -16 0 -29 -39 -17 -51 5 -5 129 -8 289 -7 l279 3 3 23 c3 17 -2 25 + -17 29 -15 4 -21 14 -21 32 0 33 -31 97 -64 136 -25 29 -27 29 -40 12 -22 -29 + -50 -17 -56 22 -4 27 -11 36 -32 42 -26 7 -27 5 -30 -35 -3 -36 -6 -41 -28 + -41 -22 0 -26 5 -30 41 -5 37 -7 40 -30 34z" + /> + <path + d="M326 2874 c-117 -181 -213 -418 -255 -630 -28 -140 -43 -339 -35 + -449 l7 -93 173 -173 174 -174 0 803 c0 441 -2 802 -4 802 -2 0 -29 -39 -60 + -86z" + /> + <path + d="M1632 2841 c10 -146 161 -248 303 -206 59 17 126 68 150 116 20 37 + 36 114 29 133 -5 14 -38 16 -246 16 l-240 0 4 -59z" + /> + <path + d="M2482 2758 c112 -111 248 -307 248 -358 0 -22 -12 -32 -97 -75 -54 + -28 -120 -62 -146 -78 -44 -25 -50 -26 -75 -13 -43 22 -136 126 -197 220 -36 + 56 -65 90 -82 97 -16 6 -122 9 -267 7 l-241 -3 -60 -29 c-35 -17 -86 -56 -123 + -92 -103 -103 -152 -237 -152 -419 0 -161 49 -393 90 -430 32 -29 88 -25 116 + 9 28 32 30 54 8 125 -63 215 -56 433 19 546 57 86 57 85 57 -214 0 -149 2 + -271 4 -271 15 0 563 295 574 309 7 9 12 38 12 66 -1 28 -1 54 -1 59 1 5 22 + -12 47 -38 25 -26 44 -49 42 -49 -51 -25 -63 -38 -66 -70 l-3 -35 64 34 63 33 + 43 -21 c93 -46 203 -20 276 64 34 39 36 47 32 91 l-4 48 48 24 c26 14 55 22 + 64 19 46 -18 164 -353 165 -469 l1 -40 17 30 c13 23 17 53 16 125 -1 104 -19 + 191 -66 308 -17 40 -27 80 -24 89 11 28 -13 81 -42 94 -15 7 -44 37 -64 68 + -95 147 -237 273 -332 295 -25 6 -21 0 36 -56z" + /> + <path + d="M3360 1594 c0 -512 3 -803 10 -799 19 12 125 192 175 300 125 263 + 180 524 173 817 l-3 148 -170 170 c-93 93 -173 170 -177 170 -5 0 -8 -363 -8 + -806z" + /> + <path + d="M1872 1851 c-156 -82 -287 -153 -292 -157 -4 -5 -15 -88 -24 -184 -9 + -96 -30 -317 -46 -490 -40 -419 -40 -413 9 -461 34 -34 44 -39 86 -39 54 0 95 + 21 119 61 15 26 33 186 99 924 l12 130 35 3 36 3 38 -413 c64 -698 53 -627 96 + -670 33 -34 43 -38 86 -38 54 0 99 23 120 62 20 36 17 94 -31 606 -29 317 -45 + 533 -46 645 0 92 -4 167 -7 167 -4 -1 -135 -68 -290 -149z" + /> + <path + d="M1530 1733 c0 -10 5 -25 10 -33 7 -11 10 -7 10 18 0 17 -4 32 -10 32 + -5 0 -10 -8 -10 -17z" + /> + <path + d="M1194 1569 c-96 -51 -111 -63 -107 -80 3 -11 11 -23 18 -25 12 -5 + 220 100 230 117 7 10 -12 49 -23 48 -4 0 -57 -27 -118 -60z" + /> + <path + d="M60 1580 c0 -14 9 -63 20 -110 64 -267 164 -483 325 -698 87 -116 + 274 -309 308 -317 12 -3 126 -4 253 -3 l231 3 -569 575 c-452 457 -568 569 + -568 550z" + /> + <path + d="M2725 628 c-357 -359 -554 -564 -544 -566 26 -5 223 45 326 84 275 + 102 497 247 701 457 l92 96 0 246 c0 135 -3 245 -7 245 -5 0 -260 -253 -568 + -562z" + /> + <path + d="M805 380 c11 -18 190 -123 284 -167 263 -124 490 -173 795 -173 l171 + 0 175 175 175 175 -803 0 c-511 0 -801 -3 -797 -10z" + /> + </g> + </svg> +); + +export default ApertureMining; diff --git a/src/config/validators/Blockseeker.tsx b/src/config/validators/Blockseeker.tsx new file mode 100644 index 0000000000..3f1e98bbba --- /dev/null +++ b/src/config/validators/Blockseeker.tsx @@ -0,0 +1,30 @@ +const Blockseeker = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 389.83 450.04"> + <defs> + <linearGradient + id="a" + x1="194.91" + y1="448.54" + x2="194.91" + y2="1.5" + gradientUnits="userSpaceOnUse" + > + <stop offset="0" stopColor="#010101" /> + <stop offset="1" stopColor="#010101" /> + </linearGradient> + </defs> + <path + d="M68.7 335.07a2 2 0 00-3.16-1.53l-22.1 17.58a24.44 24.44 0 01-27.5 2A29.05 29.05 0 011.5 328V131.5a38.7 38.7 0 0118.62-33.08L171 6.91a37.29 37.29 0 0137.89-.47 13 13 0 01.29 22.38l-77.18 47a13.84 13.84 0 00-5.88 7.33 1.84 1.84 0 001.14 2.34 1.82 1.82 0 001.54-.16l86.84-51.87a38.68 38.68 0 0139.08-.35l114.17 65.47a38.69 38.69 0 0119.44 33.56v196a27.14 27.14 0 01-14.72 24.13 10.13 10.13 0 01-14.77-9l-.18-53.84a1.45 1.45 0 00-2.73-.68 23 23 0 00-2.71 10.83v42.25a38.68 38.68 0 01-20.11 33.93L209 443.78a38.67 38.67 0 01-38.11-.54l-116.46-68.1A14.12 14.12 0 0152.72 352l12.83-10.31a8.44 8.44 0 003.15-6.62z" + stroke="#fff" + strokeMiterlimit="10" + strokeWidth="1" + fill="#010101" + /> + <path + d="M91 216v-56.31h20.28A44.26 44.26 0 01122 160.9a15.39 15.39 0 017.71 4.25q2.87 3 2.87 8.58a15.31 15.31 0 01-1.88 7.27 10.06 10.06 0 01-5.2 4.9v.35a13.91 13.91 0 016.88 4.33q2.84 3.19 2.83 8.84a14.46 14.46 0 01-3 9.49 17.6 17.6 0 01-8.06 5.42A33.73 33.73 0 01113 216zm14.91-34.32h5c2.42 0 4.22-.52 5.37-1.56a5.29 5.29 0 001.74-4.16 4.1 4.1 0 00-1.74-3.73 10.2 10.2 0 00-5.38-1.12h-5v10.59zm0 22.88H112q8.65 0 8.66-6.24a4.73 4.73 0 00-2.13-4.42c-1.41-.87-3.58-1.3-6.53-1.3h-6.07zm50.26 12.47a13.62 13.62 0 01-7.93-2 11.2 11.2 0 01-4.16-5.72 26.56 26.56 0 01-1.25-8.53V155.7h14.91v45.58a4.22 4.22 0 00.82 3 2.35 2.35 0 001.6.82 4.6 4.6 0 00.74 0l.82-.12 1.73 10.92a13.85 13.85 0 01-2.95.87 23.27 23.27 0 01-4.33.28zm31.72 0a21.36 21.36 0 01-18.41-10.44 25.94 25.94 0 010-24.52 21.45 21.45 0 0136.83 0 26 26 0 010 24.53 21.38 21.38 0 01-18.42 10.44zm0-12a5 5 0 004.81-2.9 17.66 17.66 0 001.43-7.84 17.87 17.87 0 00-1.43-7.85 5.43 5.43 0 00-9.61 0 22.09 22.09 0 000 15.69 4.94 4.94 0 004.8 2.97zm48.88 12a22.94 22.94 0 01-11.09-2.67 19.9 19.9 0 01-7.84-7.76 24.39 24.39 0 01-2.9-12.26 22.23 22.23 0 013.24-12.27 21.69 21.69 0 018.54-7.76 25 25 0 0111.44-2.68 18.93 18.93 0 0112.65 4.51l-6.93 9.52a9 9 0 00-2.52-1.64 6.48 6.48 0 00-2.34-.44 7.93 7.93 0 00-6.5 2.9 12.25 12.25 0 00-2.34 7.86 11.79 11.79 0 002.43 7.83 7.42 7.42 0 005.9 2.9 8.56 8.56 0 003.59-.81 19.63 19.63 0 003.33-1.95l5.73 9.71a17.73 17.73 0 01-7.11 3.93 27 27 0 01-7.28 1.1zm21.32-1V155.7h14.56v32.06h.35l12-15.07h16.12l-15.43 18L301.94 216h-16.11l-8.68-15.42-4.5 5V216zM83.47 294.09a33.6 33.6 0 01-11-2A30.65 30.65 0 0162.14 286l8.5-10.24a28.64 28.64 0 006.54 3.95 16.78 16.78 0 006.63 1.52 8.85 8.85 0 004.9-1 3.36 3.36 0 001.52-2.94 3.27 3.27 0 00-2-3q-2.75-1.35-5.59-2.43l-7-2.95a20.8 20.8 0 01-7.93-5.72 14.49 14.49 0 01-3.43-10 15.29 15.29 0 012.69-8.8 19.11 19.11 0 017.41-6.32 23.68 23.68 0 0110.88-2.39 27.77 27.77 0 019.74 1.87 25 25 0 018.89 5.67l-7.45 9.36a27.2 27.2 0 00-5.52-3 15.66 15.66 0 00-5.75-1.05 8.32 8.32 0 00-4.29 1 3.11 3.11 0 00-1.61 2.86 3.38 3.38 0 002.25 3.08c1.51.72 3.53 1.58 6.08 2.55l6.84 2.69a18.61 18.61 0 018.15 5.8 15.43 15.43 0 012.86 9.62 16.47 16.47 0 01-2.56 8.89 18.34 18.34 0 01-7.5 6.63 26.15 26.15 0 01-11.92 2.44zm49.91 0A23.8 23.8 0 01122 291.4a19.94 19.94 0 01-8-7.75 24.1 24.1 0 01-3-12.27 23.28 23.28 0 013-12.08 21.28 21.28 0 017.75-7.84 19.69 19.69 0 0110.06-2.78 18.65 18.65 0 0110.61 2.82 16.94 16.94 0 016.24 7.58 26.81 26.81 0 012 10.57 31.27 31.27 0 01-.21 3.68c-.15 1.19-.28 2-.39 2.56h-24.7a8.28 8.28 0 003.72 5.42 12.7 12.7 0 006.33 1.51 18.41 18.41 0 008.85-2.6l4.84 8.84a26.93 26.93 0 01-7.79 3.74 27.8 27.8 0 01-7.93 1.29zm-8.14-27.9h12.82a7.59 7.59 0 00-1.26-4.38c-.83-1.24-2.38-1.87-4.64-1.87a6.9 6.9 0 00-4.32 1.48 7.76 7.76 0 00-2.6 4.77zm53.76 27.9a23.86 23.86 0 01-11.4-2.69 19.88 19.88 0 01-8-7.75 24.1 24.1 0 01-2.94-12.27 23.19 23.19 0 013-12.08 21.26 21.26 0 017.74-7.84 19.69 19.69 0 0110.06-2.78A18.7 18.7 0 01188 251.5a17 17 0 016.24 7.58 26.81 26.81 0 012 10.57 29.23 29.23 0 01-.22 3.68c-.14 1.19-.27 2-.38 2.56H171a8.21 8.21 0 003.72 5.42 12.67 12.67 0 006.33 1.51 18.28 18.28 0 008.83-2.6l4.86 8.84a26.86 26.86 0 01-7.8 3.74 27.84 27.84 0 01-7.94 1.29zm-8.15-27.9h12.83a7.74 7.74 0 00-1.26-4.38c-.84-1.24-2.38-1.87-4.64-1.87a6.94 6.94 0 00-4.33 1.48 7.91 7.91 0 00-2.61 4.77zm33.63 26.87v-60.34H219v32.07h.35l12-15.09h16.12l-15.43 18 16.3 25.31h-16.17l-8.66-15.44-4.51 5v10.41zm66.55 1a23.82 23.82 0 01-11.39-2.69 19.9 19.9 0 01-8-7.75 24.2 24.2 0 01-2.94-12.27 23.28 23.28 0 013-12.08 21.28 21.28 0 017.75-7.84 19.65 19.65 0 0110.05-2.78 18.66 18.66 0 0110.62 2.82 16.88 16.88 0 016.24 7.58 26.81 26.81 0 012 10.57 31.27 31.27 0 01-.21 3.68c-.15 1.19-.27 2-.39 2.56H263a8.25 8.25 0 003.73 5.42 12.63 12.63 0 006.32 1.51 18.28 18.28 0 008.84-2.6l4.85 8.84a26.93 26.93 0 01-7.74 3.77 27.85 27.85 0 01-8 1.29zm-8.14-27.9h12.82a7.65 7.65 0 00-1.25-4.38c-.85-1.24-2.38-1.87-4.65-1.87a6.92 6.92 0 00-4.32 1.48 7.81 7.81 0 00-2.61 4.77zm33.62 26.87v-43.31h12.13l1.05 7.45h.31a16.69 16.69 0 015.77-6.45 12.46 12.46 0 016.53-2 19.14 19.14 0 013.25.21 7.77 7.77 0 012.13.65l-2.43 12.82c-.74-.17-1.5-.33-2.25-.47a13.36 13.36 0 00-2.6-.22 9.27 9.27 0 00-4.89 1.6 11 11 0 00-4.12 5.68v24.1z" + fill="#fff" + /> + </svg> +); + +export default Blockseeker; diff --git a/src/config/validators/Blockshard.tsx b/src/config/validators/Blockshard.tsx new file mode 100644 index 0000000000..20c560c6d3 --- /dev/null +++ b/src/config/validators/Blockshard.tsx @@ -0,0 +1,17 @@ +const Blockshard = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800"> + <path fill="#151c2c" d="M-6.87-11.4h813.73v822.81H-6.87z" /> + <path + fill="#12e1ff" + d="M325.29 401.94l74.86 43.22 74.86-43.22-74.86-43.22z" + /> + <path fill="#e7168b" d="M409.88 465.93v86.45l74.86-43.23v-86.44z" /> + <path fill="#6300f2" d="M390.32 465.93v86.45l-74.86-43.23v-86.44z" /> + <path + fill="#fff" + d="M586.77 350.12l-.13.02.02-.01-93.27-53.86L400 242.36l-39.84 24.7-44.75 27.47v73.28l85.15-51.03 60.74 35.07 61.19 35.32.21-.11-.1.11v141.64l-61.3 35.4-61.36 35.42-.06.14v-.23h.01l-61.19-35.33-61.3-35.4V126.27l-64.18 37.05v98.1l.01 76.8h-.01l.01 78.22-.01 149.42 93.39 53.92 93.28 53.85v-.11l.05.21 93.45-53.95 93.39-53.92V458.03z" + /> + </svg> +); + +export default Blockshard; diff --git a/src/config/validators/CoinbaseCloud.tsx b/src/config/validators/CoinbaseCloud.tsx new file mode 100644 index 0000000000..d53ed4fb9d --- /dev/null +++ b/src/config/validators/CoinbaseCloud.tsx @@ -0,0 +1,18 @@ +const CoinbaseCloud = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> + <path + style={{ + fill: '#45e1e5', + }} + d="M0 0h64v64H0z" + /> + <path + d="M32 43a11 11 0 1 1 10.83-12.83h11.09a22 22 0 1 0 0 3.66H42.83A11 11 0 0 1 32 43Z" + style={{ + fill: '#0a0b0d', + }} + /> + </svg> +); + +export default CoinbaseCloud; diff --git a/src/config/validators/Crifferent.tsx b/src/config/validators/Crifferent.tsx new file mode 100644 index 0000000000..50138ffdaa --- /dev/null +++ b/src/config/validators/Crifferent.tsx @@ -0,0 +1,206 @@ +const Crifferent = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"> + <path + style={{ + fill: '#2d2d2d', + }} + d="M0 0h500v500H0z" + /> + <path + d="M66.5 162 122 94m-55.5 68v79.5l75 16m-75-95.5 136.5-5.5M122 94l66.5-32.5M122 94l19.5 163.5m47-196H303m-114.5 0L322 162M303 61.5l87.5 40m-87.5-40-100 95m187.5-55 55.5 68m-55.5-68L322 162m124 7.5V264m0-94.5-148.5 80M446 264 322 162m124 102-80.5 67m80.5-67-148.5-14.5M322 162l-180.5 95.5m0 0L169 325l94 27m-121.5-94.5 224 73.5M263 352v-38m0 38v87.5m0-125.5-60-157.5M263 314l34.5-64.5m-94.5-93 94.5 93m68 81.5v108.5" + style={{ + fill: 'none', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <path + style={{ + fill: '#f8f8f8', + }} + d="M49 55h12v12H49zM61 67h12v12H61zM61 43h12v12H61zM73 55h12v12H73zM427 83h12v12h-12zM439 95h12v12h-12zM439 71h12v12h-12zM451 83h12v12h-12zM387 365h12v12h-12zM399 377h12v12h-12zM399 353h12v12h-12zM411 365h12v12h-12zM116 379h12v12h-12zM128 391h12v12h-12zM128 367h12v12h-12zM140 379h12v12h-12zM78 305h12v12H78zM90 317h12v12H90zM90 293h12v12H90zM102 305h12v12h-12z" + /> + <circle + cx={390} + cy={103} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={303} + cy={63.5} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={189} + cy={63.5} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={122} + cy={96} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={203} + cy={158} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={298} + cy={251.5} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={263} + cy={315} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={365} + cy={333} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={263} + cy={354} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={263} + cy={441.5} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={365} + cy={441.5} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={169} + cy={327} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={142} + cy={259} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={66.5} + cy={243} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={66.5} + cy={164} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={446} + cy={172} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={446} + cy={266} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + <circle + cx={322} + cy={164} + r={14} + style={{ + fill: '#2d2d2d', + stroke: '#f8f8f8', + strokeWidth: 8, + }} + /> + </svg> +); + +export default Crifferent; diff --git a/src/config/validators/Decentradot.tsx b/src/config/validators/Decentradot.tsx new file mode 100644 index 0000000000..3a6209e098 --- /dev/null +++ b/src/config/validators/Decentradot.tsx @@ -0,0 +1,11 @@ +const Decentradot = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> + <path fill="#fff" d="M0 0h512v512H0z" /> + <path + d="M141.22 94.56a37.39 37.39 0 0 1 32.36-18.68h164.84a37.39 37.39 0 0 1 32.36 18.68l82.42 142.76a37.38 37.38 0 0 1 0 37.36l-82.42 142.76a37.39 37.39 0 0 1-32.36 18.68H173.58a37.39 37.39 0 0 1-32.36-18.68L58.81 274.68a37.33 37.33 0 0 1 0-37.36z" + fill="#ed1382" + /> + </svg> +); + +export default Decentradot; diff --git a/src/config/validators/Dionysus.tsx b/src/config/validators/Dionysus.tsx new file mode 100644 index 0000000000..3f4c068ca1 --- /dev/null +++ b/src/config/validators/Dionysus.tsx @@ -0,0 +1,43 @@ +const Dionysus = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + x={0} + y={0} + viewBox="0 0 646.47 652.6" + xmlSpace="preserve" + > + <style>{'.dion0{fill:#e3187d}'}</style> + <circle fill="#e3187d" cx={158.39} cy={300.66} r={27.75} /> + <circle fill="#e3187d" cx={159.01} cy={232.74} r={23.93} /> + <circle fill="#e3187d" cx={169.83} cy={124.95} r={14.77} /> + <circle fill="#e3187d" cx={100.74} cy={206.94} r={14.77} /> + <circle fill="#e3187d" cx={112.82} cy={134.87} r={9.92} /> + <circle fill="#e3187d" cx={137.68} cy={74.69} r={9.92} /> + <path + fill="#e3187d" + d="m505.72 243.01-29.35 40.11-48.3 63.09-83.47 109.18-101.84-109.18-1.16-1.24 18.13-16.93 16.97 18.17 65.96 70.82 54.24-70.82 96.21-128.78c-28.03-46.51-77.19-78.78-134.22-83.47-4.74-.43-9.59-.62-14.44-.62-11.53 0-22.79 1.13-33.62 3.26.27.39.43.85.66 1.24 0 .04 0 .08.04.08 0 .04.04.08.04.08 2.29 4.39 3.57 9.28 3.57 14.6 0 17.55-14.25 31.8-31.84 31.8-11.14 0-20.85-5.71-26.52-14.33-.04 0-.04 0-.04-.04l-.04-.04a31.448 31.448 0 0 1-4.39-10.17c-23.3 14.6-42.86 34.63-56.92 58.24 2.21.97 4.27 2.14 6.21 3.49 0 0 0 .04.04.04 8.04 5.78 13.24 15.18 13.24 25.82 0 17.63-14.25 31.88-31.8 31.88-2.29 0-4.5-.23-6.64-.74-.66-.12-1.28-.27-1.94-.47h-.04c-.35-.12-.7-.19-1.05-.31-1.59 9.43-2.41 19.1-2.41 28.96 0 6.1.35 12.07.93 17.98v.16c9.05 87.28 82.81 155.3 172.5 155.3 95.78 0 173.39-77.65 173.39-173.43.01-22.49-4.26-44-12.1-63.73zM307.91 343.42c-9.47-5.09-13.01-16.89-7.92-26.36 5.09-9.47 16.89-13.01 26.36-7.92 9.47 5.09 13.01 16.89 7.92 26.36-5.09 9.47-16.89 13-26.36 7.92zm14.48-67.79c5.09-9.47 16.89-13.01 26.36-7.92 9.47 5.09 13.01 16.89 7.92 26.36-5.09 9.47-16.89 13.01-26.36 7.92s-13.01-16.89-7.92-26.36zm34.28 102.54c-5.09 9.47-16.89 13.01-26.36 7.92s-13.01-16.89-7.92-26.36 16.89-13.01 26.36-7.92 13.01 16.88 7.92 26.36zm25.2-42.67c-5.12 9.47-16.93 13.01-26.36 7.92-9.47-5.09-13.05-16.89-7.92-26.36 5.09-9.47 16.89-13.01 26.32-7.92 9.47 5.08 13.04 16.88 7.96 26.36zm-12.54-59.87c5.09-9.47 16.89-13.01 26.36-7.92 9.47 5.09 13.01 16.89 7.92 26.36-5.09 9.47-16.89 13.01-26.36 7.92-9.48-5.09-13.01-16.89-7.92-26.36zm-53.89-60.76c9.09.16 17.28 5.44 21.24 13.63l3.3 6.99.5-.7c3.22-4.74 8.39-7.77 14.09-8.35l12.19-1.13-2.6 11.69c-1.48 6.79-6.52 12.23-13.12 14.29l-13.55 4.19-.08.04v-.04l-19.61-6.06a24.05 24.05 0 0 1-16.35-17.74l-3.8-17.16 17.79.35zm-41.55 60.76c5.09-9.47 16.89-13.01 26.36-7.92 9.47 5.09 13.01 16.89 7.92 26.36-5.09 9.47-16.89 13.01-26.36 7.92s-13-16.89-7.92-26.36z" + /> + <circle fill="#e3187d" cx={215.27} cy={180.92} r={42.87} /> + <circle fill="#e3187d" cx={334.02} cy={121.69} r={27.75} /> + <circle fill="#e3187d" cx={273.46} cy={134.1} r={23.93} /> + <circle fill="#e3187d" cx={323.82} cy={61.69} r={14.77} /> + <circle fill="#e3187d" cx={251.82} cy={64.77} r={9.92} /> + <circle fill="#e3187d" cx={200.42} cy={74.69} r={9.92} /> + <circle fill="#e3187d" cx={275.13} cy={22.15} r={9.92} /> + <circle fill="#e3187d" cx={210.34} cy={24.23} r={9.92} /> + <path + fill="#e3187d" + d="m534.45 203.8-14.44 19.76c11.84 24.93 18.44 52.76 18.44 82.15 0 106.23-86.43 192.65-192.65 192.65-98.73 0-180.27-74.54-191.37-170.29v-.04c-.04-.19-.04-.35-.08-.54-.78-6.87-1.2-13.86-1.24-20.93h-21.82c.43 117.91 96.44 213.66 214.51 213.66 118.26 0 214.51-96.25 214.51-214.51 0-36.84-9.36-71.59-25.86-101.91z" + /> + <path + d="M70.17 601.31v27.17c0 10-8.11 18.11-18.11 18.11H6.77v-63.4h45.29c10 .01 18.11 8.12 18.11 18.12zm-9.06 0c0-5-4.06-9.06-9.06-9.06H15.83v45.29h36.23c5 0 9.06-4.05 9.06-9.06v-27.17zm63.22-9.06v45.29h27.17v9.06H88.1v-9.06h27.17v-45.29H88.1v-9.06h63.4v9.06h-27.17zm90.38 54.35h-27.17c-10 0-18.11-8.11-18.11-18.11V583.2h45.29c10 0 18.11 8.11 18.11 18.12v27.17c0 10-8.11 18.11-18.12 18.11zm0-54.35h-36.23v36.23c0 5 4.05 9.06 9.06 9.06h27.17c5 0 9.06-4.05 9.06-9.06v-27.17c0-5-4.05-9.06-9.06-9.06zm99.45 9.06v45.29h-9.06v-45.29c0-5-4.05-9.06-9.06-9.06h-36.23v54.34h-9.06v-63.4h45.29c10.01.01 18.12 8.12 18.12 18.12zm81.33-18.11v45.29c0 10-8.11 18.11-18.11 18.11H350.2c-10 0-18.11-8.11-18.11-18.11h9.06c0 5 4.05 9.06 9.06 9.06h27.17c5 0 9.06-4.05 9.06-9.06v-9.06H350.2c-10 0-18.11-8.11-18.11-18.11V583.2h9.06v18.12c0 5 4.05 9.06 9.06 9.06h36.23V583.2h9.05zm26.98 18.11c0 5 4.05 9.06 9.06 9.06h27.17c10 0 18.11 8.11 18.11 18.11s-8.11 18.12-18.11 18.12h-45.29v-9.06h45.29c5 0 9.06-4.05 9.06-9.06 0-5-4.05-9.06-9.06-9.06h-27.17c-10 0-18.11-8.11-18.11-18.11V583.2h63.4v9.06h-54.34v9.05zm135.67 27.17c0 10-8.11 18.11-18.11 18.11h-45.29v-63.4h9.06v54.34h36.23c5 0 9.06-4.05 9.06-9.06V583.2h9.06v45.28zm26.99-27.17c0 5 4.05 9.06 9.06 9.06h27.17c10 0 18.11 8.11 18.11 18.11s-8.11 18.12-18.11 18.12h-45.29v-9.06h45.29c5 0 9.06-4.05 9.06-9.06 0-5-4.05-9.06-9.06-9.06h-27.17c-10 0-18.11-8.11-18.11-18.11V583.2h63.4v9.06h-54.34v9.05z" + style={{ + stroke: '#e3187d', + strokeMiterlimit: 10, + fill: '#e3187d', + }} + /> + </svg> +); + +export default Dionysus; diff --git a/src/config/validators/Dozenodes.tsx b/src/config/validators/Dozenodes.tsx new file mode 100644 index 0000000000..0e9c481aae --- /dev/null +++ b/src/config/validators/Dozenodes.tsx @@ -0,0 +1,8 @@ +const Dozenodes = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192"> + <path d="M113.8 8c-5.1.9-11.1 5.1-13.9 9.7C98 20.8 97.5 23 97.4 29l-.1 7.5-10.8 7.9-10.7 7.8-4.4-5C65 39.8 57.6 36.5 48 36.5c-12.9 0-22.2 6.2-27.8 18.3-2.1 4.7-2.4 6.4-2 13.6.5 9.4 2.5 14.1 8.8 20.3 2.2 2.3 6.5 5.2 9.5 6.4 6.2 2.6 6.1 2.1 4.4 14.4l-1.1 8-5.8 2.7c-20.7 9.8-14.3 40.3 8.5 40.3 9.6 0 15.3-4.2 21-15.2 0-.1 7.5.9 16.5 2.2 9.1 1.4 16.8 2.5 17.1 2.5.4 0 1.2 2 1.9 4.5 2.9 10.3 10.6 17.9 21.4 21 14.1 4.1 29.5-3.3 35.9-17.4 1.6-3.4 2.2-6.5 2.1-12.1 0-8.8-2.2-14.6-7.8-20.7l-3.6-4 5.8-7.6c5.7-7.6 5.7-7.6 9.7-7 12.3 1.6 23.4-8.5 23.5-21.2 0-13.5-13.2-24.1-25.7-20.6-2.7.7-5.5 1.5-6.3 1.7-.9.2-5-3.8-10.5-10.3-4.9-5.8-9.1-11-9.3-11.4-.2-.5.9-3.4 2.3-6.4 8-16.3-5-33.7-22.7-30.5zm9.6 9.2c.9 1.2 1.6 3.2 1.6 4.3 0 1.1.9 2.8 2 3.8 1.5 1.2 2 2.9 2 6.5 0 7.6-1 8.3-11.5 8l-9-.3-.3-6.5c-.2-5.2 0-6.7 1.4-7.7 1-.7 1.9-2.4 2-3.9.7-6.1 8.5-8.9 11.8-4.2zm-20.3 26.4c5.3 5.8 15.2 8 22.6 4.9 1.9-.8 3.7-1.5 3.9-1.5.2 0 4.8 5.2 10.3 11.6l9.9 11.7-2.5 3.6c-1.3 1.9-2.6 3.7-2.8 3.9-.2.2-15-2.2-32.9-5.4l-32.5-5.8-.8-4.5c-.4-2.5-.8-4.9-.8-5.3 0-.9 20.7-15.7 22.1-15.8.6 0 2.1 1.2 3.5 2.6zm-50.4 5.9c2.5 1.8 4.8 8.4 3.3 9.9s-4-.2-4-2.7c0-3.2-2.2-4.9-5.7-4.5-2.2.2-3 1-3.9 3.8-1.6 5-4.4 5.3-4.4.5 0-7.1 8.7-11.2 14.7-7zm7.7 13.1c2.3 2.3 2.3 16.6-.1 18.5-1.2 1-4.9 1.4-13.5 1.4C32.5 82.5 32 82.2 32 72c0-10.5.7-11 15-11 9.4 0 12.2.3 13.4 1.6zM170 73c2.4 2.4 2.7 7 .5 7-.8 0-1.5-.8-1.5-1.9 0-1-.7-2.4-1.6-3.2-2-1.6-4.8-.1-5.2 3-.2 1.1-1 2.1-1.8 2.1-2.1 0-1.7-4.7.6-7 1.1-1.1 3.1-2 4.5-2s3.4.9 4.5 2zm-56 8.4 29.5 5.2.3 3 .2 2.9-41.2 16.8C80.1 118.5 61.2 126 60.9 126c-.4 0-3-1.5-5.8-3.4-2.7-1.9-5.5-3.6-6.1-3.8-1.2-.4 1.3-19.6 2.7-20.8.4-.4 2.8-1.2 5.3-1.9 8.5-2.5 15.9-9.3 19.8-18.4 1.1-2.5 1.6-2.7 4.4-2.2 1.8.3 16.6 3 32.8 5.9zm60.8.8c.7.7 1.2 3.7 1.2 6.8 0 3.1-.5 6.1-1.2 6.8-1.6 1.6-17 1.6-18.6 0-1.3-1.3-1.7-13-.5-14.1 1.2-1.2 17.8-.8 19.1.5zM149 99.5l2.3 2.5-5.7 7.5-5.8 7.5-4.2-.8c-6.8-1.2-14-.5-19.5 1.8-6.7 2.9-14.3 11.2-16.6 18-2 5.9-.8 5.8-21.4 2.7-14.3-2.2-14.5-2.2-15.2-5-.7-2.5-.4-3 2.4-4.3 8.2-3.8 78.4-32.3 79.8-32.3.8-.1 2.6 1 3.9 2.4zM46.4 127.1c1.6 1.3 2.6 3 2.6 4.7 0 1.6.8 3.2 2 4 1.6 1 2 2.3 2 7 0 3.2-.5 6.3-1.2 7-1.6 1.6-17 1.6-18.6 0-.7-.7-1.2-4-1.2-7.6 0-5.5.3-6.5 2-6.9 1.4-.4 2-1.4 2-3.2 0-3.1.8-4.4 3.5-5.9 2.8-1.6 3.9-1.5 6.9.9zm88.6 2.4c2.5 2.7 3.3 9 1.1 9-.7 0-1.8-1.6-2.5-3.5-.9-2.8-1.7-3.6-3.9-3.8-3.5-.4-5.4 1.1-6.2 4.9-1 4.3-4.1 3.5-3.8-1 .3-7.9 9.9-11.4 15.3-5.6zm6.8 11.7c.8.8 1.2 4.5 1.2 10 0 8.6-.1 8.9-2.6 9.8-3.7 1.4-20.4 1.2-23.4-.3-2.3-1.1-2.5-1.9-2.8-9-.4-11.7-.5-11.7 14.1-11.7 8.2 0 12.7.4 13.5 1.2z" /> + <path d="M115 20.6c-.6 1.5-.9 2.9-.6 3.1.2.2 2.2.3 4.5.1 4-.3 4-.4 2.8-2.8-1.8-3.6-5.4-3.9-6.7-.4zM116.8 30.6c-.8.8.3 4.4 1.3 4.4.5 0 .9-.4.9-.9s.3-1.6.6-2.5c.6-1.5-1.6-2.3-2.8-1zM45.5 69c-.7 1.2.4 8 1.4 8 .9 0 2.8-6.4 2.3-7.8-.5-1.5-2.9-1.6-3.7-.2zM164 88.4c0 1.4.5 2.8 1 3.1.6.3 1 .3 1-.2 0-.4.3-1.8.6-3.1.5-1.6.2-2.2-1-2.2-1 0-1.6.9-1.6 2.4zM39.5 130.4c-.3.9-.7 2.3-1 3.1-.3 1.2.5 1.5 3.6 1.5 3.8 0 4-.2 3.7-2.8-.2-2.1-.9-2.8-3.1-3-1.8-.2-2.9.2-3.2 1.2zM41.7 140.6c-.4.4-.7 1.8-.7 3.1 0 2.8 2.3 2.7 2.8-.1.4-2-1.1-4-2.1-3zM126.4 148.5c-.4.9-.2 2.1.5 2.8.6.6 1.1 2 1.1 3.1 0 3.3 1.9.4 2.2-3.4.2-2.6-.1-3.5-1.5-3.8-.9-.2-2 .4-2.3 1.3z" /> + </svg> +); + +export default Dozenodes; diff --git a/src/config/validators/DragonStake.tsx b/src/config/validators/DragonStake.tsx new file mode 100644 index 0000000000..039209ed92 --- /dev/null +++ b/src/config/validators/DragonStake.tsx @@ -0,0 +1,14 @@ +const DragonStake = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 258.16 232.53"> + <path + d="M230.54 96.72c-13.81-11.26-15.72-19.46-15.72-19.46C223.25 56.7 181.5 1.68 136.89.19c7.39 10.37 9.23 16.81 9.23 16.81l-1-.14c-.28-.24-.53-.51-.82-.74a69.77 69.77 0 00-14.95-9.69 66.75 66.75 0 00-8-3.27 64.68 64.68 0 00-7.83-2.08 57.06 57.06 0 00-7.22-.94c-1.14-.05-2.21-.12-3.24-.14s-2 .05-2.94.11a33.48 33.48 0 00-4.75.54 40.41 40.41 0 00-4.11 1s1.27 1 3.43 2.39l3.83 2.5c1.47.9 3.09 1.95 4.83 3.07s3.6 2.31 5.56 3.49l4.79 3-2.35.44c-.23-.06-.45-.14-.68-.21a74.38 74.38 0 00-8.75-1.74 67.42 67.42 0 00-8.63-.62 65.61 65.61 0 00-8.09.44 57.77 57.77 0 00-7.16 1.35c-1.09.3-2.13.57-3.11.87s-1.91.67-2.76 1a32.16 32.16 0 00-4.36 2 39.69 39.69 0 00-3.61 2.19s1.51.54 4 1.21l4.42 1.19c1.68.39 3.54.89 5.54 1.42 1.56.34 3.23.8 4.93 1.23a103.06 103.06 0 00-53.7 69.05H15.86v22.05h11a102.68 102.68 0 006.11 35.57H17.21v45.36h41.78v21.43h30.84l1.22.63a81.46 81.46 0 009.4 4c1.58.6 3.14 1.23 4.72 1.8l4.83 1.38a71.54 71.54 0 009.63 2.28l4.81.82a37.29 37.29 0 004.78.64c3.2.27 6.36.46 9.48.65 6.26-.17 12.36-.14 18.2-1.2a67.87 67.87 0 008.7-1.55c2.88-.69 5.7-1.37 8.44-2.1l7.12-2.59 3.45-1.26h.11s.75-.24.47-.16l.22-.09.44-.2.88-.39 1.74-.79a117.64 117.64 0 0012.72-6.89 114.71 114.71 0 0010.49-7.57 101.5 101.5 0 0014.67-14.72 97.82 97.82 0 007.56-10.78c.79-1.27 1.3-2.31 1.69-3s.57-1 .57-1l-3.4 3.1c-2.2 2-5.56 4.76-9.95 8a124.86 124.86 0 01-16.56 10.2c-1.93 1-4 1.94-6.11 2.87-1.43-6.16-4.39-15.88-9.84-22.62-25.66-31.72-47.79-18.65-66.86-40-11-12.33-8.25-24.15-6-31.22a39 39 0 0114.35-19.4c.26 38.18 42.77 30.8 59.68 40.21 27 15 24.16 26.15 24.16 26.15l31.71 4.51s9.56-19.34 10.73-32.78c1.68-19.65-16.16-21.26-27.54-30.54zM57.73 135.66h4.83v4.88h-4.83zm-19.09-4.83h9.71v9.71h-9.71zm12.09 25.12h-4.84v-4.83h4.84zm24.91 14.18H53.59v-22.05h22.05zm15.22 33.59h-9.67v-9.67h9.67zm51.66-9.67h9.67v9.67h-9.67zm-12.17 16.72h4.84v4.83h-4.84zM113.04 17.06l.61-.1zm1.74-.29l-.6-.38-.75.44.75-.44.6.38zm50.13 37.93s25.77 11.84 25.44 26.84c0 0-23-3.34-25.44-26.84z" + fill="#006938" + /> + <path + fill="#006938" + d="M4.83 124.64h22.05v22.05H4.83zM0 106.97h9.67v9.67H0zM0 165.29h9.67v9.67H0z" + /> + </svg> +); + +export default DragonStake; diff --git a/src/config/validators/Gatotech.tsx b/src/config/validators/Gatotech.tsx new file mode 100644 index 0000000000..b9363514e2 --- /dev/null +++ b/src/config/validators/Gatotech.tsx @@ -0,0 +1,83 @@ +const Gatotech = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 521.94 521.94"> + <g> + <path + d="m170.29 270.91-15.09-31.62-12.23-76.18 31.63-58.22 14.91 50.81 41.34 2.64 26.4-50.57 25.87 59.65 18.37 82.55-8.31 29.2 59.93 158.8 44.3-108.88 53.9 55.34-43.89-7.95-9.21 98.1-87.77-8.22-120.74 9z" + strokeWidth={35} + stroke="#010101" + fill="none" + /> + <rect + strokeWidth={10} + stroke="#010101" + fill="#010101" + x={201.02} + y={201.79} + width={12.88} + height={9.04} + rx={3.46} + /> + <rect + strokeWidth={10} + stroke="#010101" + fill="#010101" + x={237.97} + y={202.75} + width={12.88} + height={9.04} + rx={3.46} + /> + <path + strokeWidth={15} + stroke="#010101" + d="m191.69 231.52 61.38 9.82" + transform="translate(4.97 4.97)" + /> + <rect + x={4.97} + y={4.97} + width={512} + height={512} + rx={3.44} + fill="#c9d8ef" + strokeOpacity={0} + strokeWidth={9.94} + stroke="#010101" + /> + </g> + <g id="g843"> + <path + d="m170.29 270.91-15.09-31.62-12.23-76.18 31.63-58.22 14.91 50.81 41.34 2.64 26.4-50.57 25.87 59.65 18.37 82.55-8.31 29.2 59.93 158.8 44.3-108.88 53.9 55.34-43.89-7.95-9.21 98.1-87.77-8.22-120.74 9z" + fill="#fff" + strokeWidth={35} + stroke="#010101" + /> + <rect + strokeWidth={10} + fill="#010101" + x={201.02} + y={201.79} + width={12.88} + height={9.04} + rx={3.46} + /> + <rect + strokeWidth={10} + fill="#010101" + x={237.97} + y={202.75} + width={12.88} + height={9.04} + rx={3.46} + /> + <path + strokeWidth={15} + stroke="#010101" + d="m191.69 231.52 61.38 9.82" + transform="translate(4.97 4.97)" + /> + </g> + </svg> +); + +export default Gatotech; diff --git a/src/config/validators/Gdot.tsx b/src/config/validators/Gdot.tsx new file mode 100644 index 0000000000..613d6823f0 --- /dev/null +++ b/src/config/validators/Gdot.tsx @@ -0,0 +1,20 @@ +const Gdot = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> + <g> + <g> + <path + style={{ + fill: '#161719', + }} + d="M0 0h512v512H0z" + /> + <path + fill="#eee" + d="M90.39 308.09c-18.78.59-35.46-12.1-34.94-31.5 1.23-12.16-3.46-51 4.37-61 16-25.71 67.2-17.15 65.51 16.61h-21.48c1.24-12.75-15.59-16.41-23.41-9.6q-3.51 3.3-3.51 9.6c1.33 7.27-3.39 49.14 3.51 53.91 4.21 4.49 15.69 4.49 19.9 0 4.57-3.56 3.39-13.12 3.51-18.54H88.39v-18.19h36.94c1.1 28.25 2.49 58.46-34.94 58.71ZM149.38 269.43v-20.05h45.82v20.05ZM245.89 308.09c-16.65 0-25.73-14.19-25.49-30.79.2-12.5-2-33.52 7.09-42.45 10.52-13.34 34.6-10.44 39.59 6.65h.43c-.6-9.66-.86-29.62-.72-39.37h21.48v104.53h-20.76v-13.61h-.43c-3.41 9.53-10.97 15.1-21.19 15.04Zm8.31-18.62c8.5 0 12.62-5.17 12.59-13.6-.57-10.41 3.93-31.77-12.6-30.79-8 0-12.36 4.78-12.31 12.89.74 10.2-4.32 32.51 12.32 31.5ZM337.24 308.09c-18.82.63-34.92-12.21-34.36-31.5-2.06-26.38 2.5-50 34.36-50.12 18.93-.66 34.87 12.2 34.37 31.5 2.2 26.03-2.8 50.3-34.37 50.12Zm0-18.62c17.18 1.26 12.23-21 12.89-31.5 0-8.38-4.51-12.91-12.89-12.89s-12.93 4.58-12.88 12.89c.64 10.52-4.22 32.7 12.88 31.5ZM428.31 306.66c-32.95 1.47-25-38.3-25.77-59.43h-20.77V227.9h20.77v-22.19H424v22.19h29.36v19.33H424c2.5 38.71-11.45 41.64 27.93 40.1v19.33Z" + /> + </g> + </g> + </svg> +); + +export default Gdot; diff --git a/src/config/validators/GenericChain.tsx b/src/config/validators/GenericChain.tsx new file mode 100644 index 0000000000..425ef3f474 --- /dev/null +++ b/src/config/validators/GenericChain.tsx @@ -0,0 +1,46 @@ +const GenericChain = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> + <defs> + <linearGradient + id="gnrcc-linear-gradient" + x1={259.06} + y1={-669.92} + x2={256.91} + y2={-434.66} + gradientTransform="matrix(1 0 0 -1 1.99 -210.71)" + gradientUnits="userSpaceOnUse" + > + <stop offset={0} stopColor="#fff" /> + <stop offset={0.08} stopColor="#e0e1e0" /> + <stop offset={0.28} stopColor="#9c9c9d" /> + <stop offset={0.46} stopColor="#666" /> + <stop offset={0.63} stopColor="#3a3a39" /> + <stop offset={0.78} stopColor="#1a1a1a" /> + <stop offset={0.91} stopColor="#090909" /> + <stop offset={1} stopColor="#010101" /> + </linearGradient> + <style>{'.gnrcc-2{fill:#010101}'}</style> + </defs> + <path fill="#fff" d="M0 0h512v512H0z" /> + <g id="Layer_1" data-name="Layer 1"> + <path + fill="#010101" + d="M274.69 189.05c-.86 39.64-32.74 71.52-72.38 70.66s-71.53-32.71-70.67-72.38 32.75-70.67 71.53-70.67c39.64 0 71.52 31.89 71.52 72.39zm72.39 0c0-76.7-64.63-138.74-143.91-138.74s-144.78 62-144.78 138.74S123 327.79 203.17 327.79s143.91-62.04 143.91-138.74z" + /> + <path + fill="#010101" + d="M164.39 422.58h294.72c1.72-11.2 3.45-23.26 3.45-35.33 0-106.85-90.49-193-202.51-193s-202.52 86.15-202.52 193A187.84 187.84 0 0 0 61 425.17z" + /> + <path + fill="#fff" + d="M164.39 351.92c19.82-52.57 77.56-79.28 130.13-59.46 27.57 10.34 50 31.88 59.46 59.46zm-117.2 0h415.37v104.27H47.19z" + /> + <path + d="M166.11 345.89 61 347.61c18.1 62.05 102.55 109.44 202.51 109.44 91.35 0 173.21-34.47 195.62-106H354.84c-19 31-58.6 42.22-91.35 42.22-44.81-.86-81.86-19.82-97.38-47.39" + fill="url(#gnrcc-linear-gradient)" + /> + </g> + </svg> +); + +export default GenericChain; diff --git a/src/config/validators/GoOpen.tsx b/src/config/validators/GoOpen.tsx new file mode 100644 index 0000000000..74ff9dba74 --- /dev/null +++ b/src/config/validators/GoOpen.tsx @@ -0,0 +1,17 @@ +const GoOpen = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 266.33 196.67"> + <path d="m224.78 60.19 41.55 55.23-13.26 49.68-37.49 24.03-45.57-7.73-19.74-30.61 3.22-8.92 21.05 32.63 39.42 6.7 32.92-21.08 10.82-43.13-35.96-48.8-59.99 7.55-10.36.12 1.9-6.64 71.49-9.03z" /> + <path + d="m221.75 68.19 36 48.8-10.82 43.13-32.96 21.08-39.42-6.7-21.05-32.64 23.91-66.28-15.65.16zm-5 68.55 4.88-20-21.16-12.89-21.58 8-2.8 20 14.65 9.38 13.62 5.12z" + fill="#ffc300" + /> + <path d="m221.59 116.78-4.88 20-12.4 9.61-13.62-5.12-14.65-9.38 2.8-20 21.58-8zm-11.58 15.6 2.92-12-13.31-8.13-13.94 5.14-1.51 10.71 10.22 6.57 8.58 3.14z" /> + <path + fill="#fd6412" + d="m157.32 82.61-7.95.27-46.27 1.62-13.46 36.18 32.53-3.24-10.09 30.24-42.98-5.59-21.18-35.12 9.63-39.61 37.42-22.63 21.49 17.93 45.93-12.54-26.45-40.33-69.19-.98-54.97 38.24-3.52 88.75 30.85 52.02 90.61-9.97 14.21-36.89 3.36-8.7 19.26-49.98-9.23.33z" + /> + <path d="m161.76 75.74-10.37.11-54.22.57-20.07 53.92 33-3.35-3.81 11.51-32.12-4.16-17.32-28.76 8-32.91 29.32-17.7 20.3 16.86 61.22-16.7-34.75-52.92L64.87 0 3.17 42.78l-3.2 95.09 34 58.8 103.76-11.09 12.54-34.8 3.22-8.92 23.92-66.28zm-14.47 56.51-3.35 8.71-14.22 36.88-90.61 10-30.85-52 3.52-88.75 55-38.24 69.18 1 26.45 40.33-45.95 12.47-21.49-17.92-37.42 22.63-9.63 39.61 21.19 35.12 43 5.59 10.06-30.24-32.54 3.24 13.46-36.18 46.27-1.61 8-.27 9.24-.33z" /> + </svg> +); + +export default GoOpen; diff --git a/src/config/validators/Helikon.tsx b/src/config/validators/Helikon.tsx new file mode 100644 index 0000000000..3e8027b4d6 --- /dev/null +++ b/src/config/validators/Helikon.tsx @@ -0,0 +1,15 @@ +const Helikon = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 300 300" + style={{ + backgroundColor: '#000', + padding: '5.5px', + }} + fill="#fff" + > + <path d="M260.4 110.4C260.4 49.5 210.9 0 150 0S39.6 49.6 39.6 110.5 89.1 220.9 150 220.9s110.4-49.6 110.4-110.5zM150 5c58.1 0 105.4 47.3 105.4 105.4 0 14.2-2.8 27.7-7.9 40l-83.8-83.8C160 63 155.2 61 150 61s-10 2-13.7 5.6l-83.8 83.8c-5.1-12.3-7.9-25.9-7.9-40C44.6 52.3 91.9 5 150 5zm-47.4 102.5 8.3 8.4c2.1 2.1 4.9 3.2 7.7 3.2s5.6-1 7.9-3.1l8.4-8.4c1.1-1.1 2.6-1.8 4.1-1.8 1.5-.1 2.9.5 3.9 1.5 4.1 4.1 10.9 3.9 15.1-.3 1.3-1.3 2.8-2.2 4.1-2.6.9-.2 1.5-.1 1.6 0 2.9 2.9 8.4 1.8 12.8-2.6l7.6-7.6 51.7 51.7-11.7 11.7c-2.2 2.2-6.1 2.2-8.3 0l-14.2-14.1c-4.2-4.3-11.1-4.3-15.4 0l-16.9 16.9c-2.3 2.3-6 2.3-8.3 0l-22.3-22.3c-2-2.1-4.8-3.2-7.7-3.2s-5.6 1.1-7.7 3.2l-3.4 3.4c-2.3 2.3-6 2.3-8.3 0L90.1 120l12.5-12.5zm3.6-3.5L140 70.2c2.7-2.7 6.3-4.2 10.1-4.2s7.4 1.5 10.1 4.2l20.4 20.4-7.6 7.6c-2.7 2.7-5.3 3-5.7 2.6-1.6-1.6-4-2-6.6-1.3-2.2.6-4.4 1.9-6.3 3.8-2.3 2.3-5.9 2.4-8 .3-4.1-4.1-10.8-4-15.1.3l-8.4 8.4c-2.3 2.3-6 2.3-8.3 0l-8.4-8.3zM150 215.8c-42 0-78.4-24.7-95.3-60.4l31.8-31.8 21.5 21.5c4.2 4.3 11.1 4.3 15.4 0l3.4-3.4c2.2-2.2 6.1-2.2 8.3 0l22.3 22.3c2.1 2.1 4.9 3.2 7.7 3.2s5.6-1.1 7.7-3.2l17-16.9c2.3-2.3 6-2.3 8.3 0l14.2 14.1c2 2.1 4.8 3.2 7.7 3.2 2.9 0 5.6-1.1 7.7-3.2l11.7-11.7 5.9 5.9c-16.9 35.7-53.3 60.4-95.3 60.4zM41.5 259.5c0-1.2 0-1.6-.4-2.2-.4-.6-1.1-1-1.9-1s-1.5.4-1.9 1c-.3.6-.4 1-.4 2.2v16.4H16.6v-16.4c0-1.2 0-1.6-.4-2.2-.4-.6-1.1-1-1.9-1s-1.5.4-1.9 1c-.3.6-.4 1-.4 2.2v37.2c0 1.2 0 1.6.4 2.2.4.6 1.1 1 1.9 1s1.5-.4 1.9-1c.3-.6.4-1 .4-2.2V280H37v16.7c0 1.2 0 1.6.4 2.2.4.6 1.1 1 1.9 1s1.5-.4 1.9-1c.3-.6.4-1 .3-2.2v-37.2zm86.8 36.4c-.5-.3-.9-.4-2-.4H107v-36c0-1.2 0-1.6-.4-2.2-.4-.6-1.1-1-1.9-1s-1.5.4-1.9 1c-.3.6-.4 1-.4 2.2v37.3c0 .7 0 1.6.6 2.2.7.6 1.5.6 2.2.7h21.1c1.2 0 1.5-.1 2-.4.6-.4.9-1 .9-1.7s-.4-1.3-.9-1.7zm17.1-39.6c-.8 0-1.5.4-1.9 1-.3.6-.4 1-.4 2.2v37.2c0 1.2 0 1.6.4 2.2.4.6 1.1 1 1.9 1s1.5-.4 1.9-1c.3-.6.4-1 .4-2.2v-37.2c0-1.2 0-1.6-.4-2.2-.4-.6-1.1-1-1.9-1zM183 273l10.3-12.5c.7-.9 1-1.4 1-2 0-.7-.3-1.2-.8-1.7-.5-.3-1-.5-1.5-.5-.8 0-1.5.6-2.1 1.4l-19.1 23.2h-.1v-21.4c0-1.2 0-1.6-.4-2.2-.4-.6-1.1-1-1.9-1s-1.5.4-1.9 1c-.3.6-.4 1-.4 2.2v37.2c0 1.2 0 1.6.4 2.2.4.6 1.1 1 1.9 1s1.5-.4 1.9-1c.3-.6.4-1 .4-2.2v-9.4l9-10.8 12.1 21.5c.6 1 1.2 1.9 2.4 1.9.4 0 .8-.1 1.2-.3.8-.4 1.2-1.1 1.2-1.8 0-.6-.2-1.1-.7-2.1L183 273zm41.2-16.7c-4.5 0-8.1 1.6-10.8 4.2-4.1 4.1-4.4 7.8-4.4 17.7s.3 13.6 4.4 17.7c2.7 2.6 6.3 4.2 10.8 4.2s8.1-1.6 10.8-4.2c4.1-4.1 4.4-7.8 4.4-17.7 0-9.9-.3-13.6-4.4-17.7-2.7-2.7-6.3-4.2-10.8-4.2zm7.4 36.5c-1.9 2-4.5 3.1-7.4 3.1-2.9 0-5.4-1.1-7.4-3.1-2.7-2.8-3-5.8-3-14.7 0-8.9.3-11.9 3-14.7 1.9-2 4.5-3.1 7.4-3.1 2.9 0 5.4 1.1 7.4 3.1 2.7 2.8 3 5.8 3 14.7s-.3 11.9-3 14.7zm56-35.5c-.4-.6-1.1-1-1.9-1s-1.5.4-1.9 1c-.3.6-.4 1-.4 2.2v31.3l-22.1-32.6c-.8-1.1-1.5-1.8-2.6-1.8-.7 0-1.4.2-1.8.7-.5.5-.7 1.2-.7 2.3v37.3c0 1.2 0 1.6.4 2.2.4.6 1.1 1 1.9 1s1.5-.4 1.9-1c.3-.6.4-1 .4-2.2v-31.3l22.1 32.6c.8 1.1 1.5 1.8 2.6 1.8.7 0 1.3-.2 1.8-.7s.7-1.2.7-2.3v-37.3c0-1.2 0-1.6-.4-2.2zm-200.7 1.3c0-1.2-.9-2.1-2.1-2.1H62.3c-.8 0-1.5.4-1.9 1.1-.3.7-.3 1.6.2 2.2L74 278l-13.4 18.6c-.5.7-.6 1.5-.2 2.2.4.7 1.1 1.1 1.9 1.1h22.4c1.2 0 2-.9 2-2.1s-.9-2.1-2.1-2.1H66.3l11.9-16.4c.5-.8.5-1.8 0-2.5l-11.8-16.1h18.4c1.2 0 2.1-.9 2.1-2.1z" /> + </svg> +); + +export default Helikon; diff --git a/src/config/validators/Highstake.tsx b/src/config/validators/Highstake.tsx new file mode 100644 index 0000000000..1264ed0f3c --- /dev/null +++ b/src/config/validators/Highstake.tsx @@ -0,0 +1,143 @@ +const HighStake = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576.72 454.33"> + <defs> + <clipPath id="a" transform="translate(-15.35 -23.13)"> + <path + style={{ + fill: 'none', + }} + d="M143.54 105.83h369.07V474.9H143.54z" + /> + </clipPath> + <clipPath id="b" transform="translate(-15.35 -23.13)"> + <path + style={{ + fill: 'none', + }} + d="M143.54 105.83h369.07V474.9H143.54z" + /> + </clipPath> + </defs> + <g + style={{ + clipPath: 'url(#a)', + }} + > + <path + d="M374.21 246.8c-115.33 0-217.85 44.35-217.85 115.33 0 53.23 51.26 97.59 128.15 97.59 44.26 0 91.12-26.72 124.65-26.72 1.17 0 2.34 0 3.49.1 50.1 3 51.26 44.36 115.34 44.36 30 0 64.07-26.61 64.07-88.71 0-79.85-89.7-141.95-217.85-141.95" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#ffd983', + }} + /> + </g> + <g + style={{ + clipPath: 'url(#a)', + }} + > + <path + d="M566.43 336.5h25.63v51.26h-25.63Z" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#ffd983', + }} + /> + </g> + <g + style={{ + clipPath: 'url(#a)', + }} + > + <path + d="M156.36 310.87H182v51.26h-25.64Z" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#ffd983', + }} + /> + </g> + <g + style={{ + clipPath: 'url(#a)', + }} + > + <path + d="M528 417.33c-29.09 0-42.06-9.67-57.09-20.86-13.7-10.21-29.23-21.77-57.13-23.46-1.53-.09-3-.15-4.61-.15-18.37 0-38.17 6.21-59.13 12.77-21.94 6.85-44.61 13.95-65.52 13.95-66.83 0-115.34-37.31-115.34-88.71 0-69.15 105.65-106.46 205-106.46 118.81 0 205 56 205 133.09 0 58.57-30.67 79.83-51.26 79.83" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#a01d22', + }} + /> + </g> + <g + style={{ + clipPath: 'url(#a)', + }} + > + <path + d="M374.21 195.54c-115.33 0-217.85 44.35-217.85 115.33 0 53.23 51.26 97.59 128.15 97.59 44.26 0 91.12-26.72 124.65-26.72 1.17 0 2.34 0 3.49.1 50.1 3 51.26 44.37 115.34 44.37 30 0 64.07-26.62 64.07-88.72 0-79.85-89.7-142-217.85-142m0 17.75c111.37 0 192.22 52.23 192.22 124.2 0 53.1-26.41 71-38.44 71-23.5 0-33.13-7.17-47.71-18s-32.59-24.28-65.41-26.26c-1.88-.12-3.78-.18-5.71-.18-21.12 0-42.15 6.59-64.41 13.54-20.7 6.49-42.1 13.18-60.24 13.18-51 0-102.52-27.43-102.52-79.84 0-63.38 99-97.58 192.22-97.58" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#ffe8b6', + }} + /> + </g> + <g + style={{ + clipPath: 'url(#a)', + }} + > + <path + d="M350.64 214s36.38-.75 62 17c12.81 8.88 4.29 28.83 4.29 28.83l21.34 6.67s-.78-10.72 38.44-17.75c28.85-5.18 58.21 17.75 58.21 17.75s-32.58-44.37-96.65-53.24-87.64.75-87.64.75" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#ffe8b6', + }} + /> + </g> + <g + style={{ + clipPath: 'url(#b)', + }} + > + <path + d="M463.91 284.25c0 19.6-23 35.5-51.26 35.5s-51.26-15.9-51.26-35.5 23-35.48 51.26-35.48 51.26 15.89 51.26 35.48" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#e6e7e7', + }} + /> + </g> + <g + style={{ + clipPath: 'url(#a)', + }} + > + <path + d="M438.28 284.25c0 9.81-11.47 17.75-25.63 17.75S387 294.06 387 284.25s11.49-17.73 25.63-17.73 25.63 7.93 25.63 17.73" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#a8aaad', + }} + /> + </g> + <path + d="M187.91 381.05c-.74-.43-.82-2.44-.87-20.8 0-11.17-.15-20.6-.26-21s-.35-8.6-.52-18.33-.4-18.43-.5-19.35-.33-7.4-.52-14.43-.54-12.91-.78-13.05-.46.13-.48.62c-.17 4.43-1 6.57-2.25 5.56-.81-.67-.78-.75-1.95 5.26-1 5.15-1.51 6.39-2.5 6-.38-.15-.7-.06-.7.2s-.55.61-1.22.78c-.93.23-1.81 1.44-3.7 5.12-2.3 4.48-2.62 4.88-4.52 5.51-2.19.72-5.55 4.43-7.27 8-.5 1.05-1.14 1.89-1.43 1.9-.57 0-4.23 6.56-6.1 10.92-1.47 3.43-1.43 3.39-2.08 1.68s-.65-13.69.08-17.12c.49-2.28.46-2.67-.29-3.22-1.06-.77-.78-1.77 1.79-6.32 2.15-3.8 2.22-4.07 1.17-4.47a1.18 1.18 0 0 1-.74-.95c0-.77 4.26-6.68 5.63-7.82.84-.7.85-.83.12-1.43-1.25-1-1-1.75 2-5.22 1.52-1.79 2.58-3.26 2.36-3.26a2.71 2.71 0 0 1-1.08-.44c-.55-.34-.56-.63 0-1.6.61-1.15.59-1.14-1.16.19a6.53 6.53 0 0 1-2.93 1.37c-1.1 0-2.25.52-7.35 3.23a10.79 10.79 0 0 1-3.47 1.22 8.32 8.32 0 0 0-3.23 1.25 37.06 37.06 0 0 1-3.72 2c-1 .42-2.13 1-2.6 1.25a1.61 1.61 0 0 1-1.8-.09c-.77-.48-1.65-.29-4.91 1.06-4.23 1.74-6.56 1.79-6.57.13 0-2.12-.7-2.17-4.65-.35-2.48 1.15-4.4 1.73-5.42 1.63-1.38-.14-1.6-.36-1.74-1.77-.18-1.83-.1-1.78-1.79-1.13a37.58 37.58 0 0 1-5.43 1c-4.82.6-6.76.15-6.76-1.53 0-1-.17-1-2.74-.72-3.75.47-5.7 0-5.7-1.38 0-1-.24-1.08-4.57-1.17-3.53-.08-4.7-.27-5.15-.88s-2.14-.79-12.17-.79c-6.38 0-11.88-.18-12.23-.39s.46-.91 2.73-1.94c5.38-2.44 16.06-6.13 18.61-6.43 2.16-.26 2.33-.37 2.06-1.43-.35-1.43 1-1.85 5.93-1.9h3.4l-.26-1.6c-.25-1.49-.16-1.61 1.47-1.91a17.78 17.78 0 0 1 7 .56c1 .3 1.08.19.89-1.32-.26-2 .28-2.41 3.47-2.41a23.09 23.09 0 0 1 6.68 1c.55.14.69-.22.57-1.5a2.39 2.39 0 0 1 .52-2.1c1-.66 5.42-.46 7.61.35 2.94 1.09 3 1.08 2.79-.66-.14-1.57-.1-1.61 1.84-1.61a21.23 21.23 0 0 1 5 .94l3 .94v-3l3.5.33a23.49 23.49 0 0 1 5.27 1.06c2.3 1 2.3 1 1.92-.56l-.33-1.31 4.12.35a22 22 0 0 1 5.56 1.06c1.84.9 1.91.89 1.55-.25-.29-.9-.09-.94 3.56-.71a33.64 33.64 0 0 1 5.31.71 9.22 9.22 0 0 0 2.16.46c.63 0 .63-.12.06-.82s-.31-.79 4-.46c2.59.2 5.27.45 6 .56 3.22.54-3.86-1.42-8.74-2.42-3.27-.66-5.85-1.46-6.36-2a3.34 3.34 0 0 0-2.08-.85c-.68 0-3.6-.37-6.48-.82-4.2-.66-5.29-1-5.47-1.7-.31-1.16-2.63-1.94-5.79-2-5 0-9.6-1.93-8.92-3.7.18-.46-.63-.64-3.11-.7a21.38 21.38 0 0 1-8.38-1.64c-.94-.43-1.12-.8-.9-1.88l.27-1.34-3.94-.07c-3.79-.08-8.64-.75-9.59-1.35-.28-.17-.27-.65 0-1.21a5.47 5.47 0 0 0 .49-1.14 20.18 20.18 0 0 0-4.12-.47c-3.79-.23-7.31-1.33-7.27-2.25a4.07 4.07 0 0 1 .74-1.32 3.94 3.94 0 0 0 .73-1.29c0-.19-1.77-.47-3.94-.64-4.07-.31-7.47-1.37-7.47-2.31a5.08 5.08 0 0 1 .81-1.78c.45-.68.73-1.3.62-1.37s-1.86-.53-3.91-1c-4.1-1-7.44-2.5-7.44-3.33A5.1 5.1 0 0 1 72 214c1-1.25 1-1.33.12-1.49-2.2-.43-9.33-3.12-9.66-3.65-.21-.34.28-1.32 1.16-2.32l1.52-1.74-1.43-.53a44.41 44.41 0 0 1-4.06-1.91c-2.16-1.13-2.58-1.56-2.32-2.36s0-1.12-1.38-1.68c-2.44-1-3.2-2.08-2.41-3.34.34-.55.55-1 .46-1.1s-1.73-.88-3.59-1.88a27.63 27.63 0 0 1-4.41-2.59c-.94-.88-.95-1-.14-1.87.47-.52.77-1 .66-1.11s-1.94-1.23-4.05-2.53c-3.21-2-3.8-2.54-3.6-3.43s-.32-1.46-2.59-3.05c-6.07-4.25-11.19-7.56-16-10.33-2.75-1.59-5-3.06-5-3.28 0-.52 8.13 1.46 13.64 3.33 2.45.83 5.24 1.67 6.2 1.86a13 13 0 0 1 3.35 1.26c.89.51 1.61.77 1.62.57 0-1.5.93-1.51 4.48-.07a21.9 21.9 0 0 1 4.72 2.47c1.37 1.29 1.67 1.22 2.27-.5.45-1.29.64-1.41 1.73-1 3.29 1.25 6.83 3.29 7.88 4.54a7.08 7.08 0 0 0 1.38 1.39 7.36 7.36 0 0 0 1.08-1.95c.65-1.43 1.09-1.86 1.69-1.63a23.09 23.09 0 0 1 6.44 4.82l1.52 1.73 1.18-1.73 1.17-1.72 2.44 1.51a22.33 22.33 0 0 1 4.27 3.55l1.82 2 1.08-1.81 1.07-1.82 2.23 1.53a16.68 16.68 0 0 1 3.55 3.45 6.69 6.69 0 0 0 1.7 1.93c.21 0 .8-.66 1.3-1.5l.93-1.52 3 2.53a32.67 32.67 0 0 1 4.12 4c1.36 1.84 1.63 1.83 2.4 0 .33-.82.9-1.49 1.26-1.49.86 0 5.55 4.77 6.51 6.63l.77 1.49.93-1.21a5.08 5.08 0 0 0 .93-1.57c0-1.49 4.07 1.94 6.53 5.57a10.4 10.4 0 0 0 2 2.47c.19 0 .48-.45.67-1 .23-.75 0-1.43-1-2.44l-1.34-1.4 1-.66c1.18-.74 1.4-1.9.35-1.9s-9.52-5.71-9.87-6.62c-.21-.54.17-1.07 1.16-1.66.81-.48 1.47-1 1.47-1.21a6.87 6.87 0 0 0-2.11-1.6 45.21 45.21 0 0 1-5.34-4.22l-3.22-2.94 1.42-1.13 1.42-1.12-1.42-.72c-1.55-.78-6.91-5.8-7.67-7.17-.38-.7-.09-1 1.65-1.93 2-1 2-1.12 1.13-1.63a47.76 47.76 0 0 1-5-4.41c-4.31-4.18-4.27-4-1.24-5.85.66-.4.65-.53 0-1.08a35.28 35.28 0 0 1-5.41-5.23c-2.59-2.93-2.94-4.27-1.24-4.72 2.58-.69 2.61-1.09.29-3.36s-4.87-5.75-4.87-6.65c0-.29.81-.92 1.81-1.4 1.76-.84 1.78-.88.77-1.59-1.43-1-5.72-8-5.41-8.82a2.78 2.78 0 0 1 1.83-1l1.57-.31-1.72-2.15c-1.88-2.32-3.82-5.71-3.82-6.7 0-.34.55-.63 1.22-.63 1.59 0 2-.84 1.06-1.93s-3.76-6.72-3.76-7.5c0-.27.56-.49 1.24-.49 1.41 0 1.52-.35.58-1.75-1.38-2-2.4-5.33-1.78-5.72.44-.27.29-1.09-.57-3.18s-1-2.89-.52-3.21.53-.64.08-1.47c-.53-1-3-12.09-3.71-16.67a27 27 0 0 0-.58-2.85c-.15-.48 0-.87.26-.87.69 0 5.61 9.82 5.23 10.44-.16.26-.06.48.23.48s1.41 1.78 2.51 4a51.67 51.67 0 0 0 4 6.57 14.42 14.42 0 0 1 2.27 4c.31 1.38.34 1.39 1.31.51s1-.86 1.74.09c1 1.29 3.12 5.78 3.12 6.57s.65.77 1.84-.34c1-1 1-1 2 .13 1.32 1.4 3.56 6.21 3.56 7.65s.27 1.4 2.1-.14c1.17-1 1.64-1.13 2.18-.68 1.13.94 4.66 8.4 4.66 9.85 0 2 .45 2.13 2.11.73.82-.68 1.73-1.16 2-1.06.9.3 3.43 5.91 3.69 8.17s.61 2.52 2.57 1.12c1.08-.76 1.5-.85 1.89-.37.89 1.09 3 6.66 3.38 8.7l.31 2 1.69-1c1.24-.74 1.86-.86 2.33-.46 1 .78 3.47 8.34 3.17 9.5-.41 1.57.34 1.56 2.51 0l2-1.48 1.23 2.62a20.56 20.56 0 0 1 2.33 8.19c0 .89.12 1.62.27 1.62a15 15 0 0 0 2.21-1c1.3-.67 2-.82 2.26-.45.83 1.33 1.7 5.94 1.71 9a10.65 10.65 0 0 0 .27 3.35 11.54 11.54 0 0 0 2.1-1.24c1.33-.9 1.9-1.07 2.15-.62.53 1 1.43 7.48 1.43 10.28v2.53l1.89-1.29a10.76 10.76 0 0 1 2.18-1.26c.86 0 1.5 3.36 1.58 8.33l.1 5.51 1.69-1c1.19-.7 1.79-.83 2-.43.35.55 1.17 10.08 1.06 12.26-.07 1.36.45 1.41 2 .19 1-.81 1.29-.85 2.17-.27a3 3 0 0 0 2.13.38l1.1-.29-1.39-1.8c-3-3.89-6.05-9.33-6.05-10.69 0-.45.68-.87 1.75-1.07l1.75-.33L167 180a58.76 58.76 0 0 1-5.31-10.33c-.37-1.25-.28-1.34 1.55-1.66a4.6 4.6 0 0 0 1.94-.56 16.59 16.59 0 0 0-1.2-2.31c-1.47-2.56-4.76-10.5-4.76-11.51 0-.53.57-.74 2-.74 2.23 0 2.28-.07 1.24-1.66s-3.23-8.18-3.23-9.79c0-1.3.09-1.34 2-1.08s2 .24 1.68-.91a18.41 18.41 0 0 0-1-2.45 35.94 35.94 0 0 1-2.21-8.18c0-.72.33-.82 2-.6 2.47.33 2.71.06 1.5-1.63-1-1.42-3.11-9.27-2.68-10a4.58 4.58 0 0 1 2.54-.09c2.57.35 2.52.59.87-3.81a18.43 18.43 0 0 1-.9-5c-.07-3-.15-2.91 3.22-2.25l1.8.35-.91-2.65a21.41 21.41 0 0 1-.94-6.52v-3.84l2.31.34c2.14.32 2.28.28 1.93-.59-1.25-3.08-2-10-1.18-10.85.16-.17 1.15 0 2.17.46a4.83 4.83 0 0 0 2.07.58 6.37 6.37 0 0 0-.38-2.36 19.59 19.59 0 0 1 .3-8.51c.29-.74.46-.75 1.73-.09 2.35 1.22 2.94 1 2.39-.88s1-10.59 1.85-10.47a13.71 13.71 0 0 1 1.54.39c1 .31 1.07.2.84-1.3a18.12 18.12 0 0 1 .43-4.86l.68-3.23 1.64.16c1.19.12 1.57 0 1.4-.49s.88-3.79 2.24-7.61 2.89-8.28 3.41-9.92a49.51 49.51 0 0 1 1.78-4.71 16.54 16.54 0 0 0 1.13-3.72l.31-2 .66 2.1c.57 1.79.55 3.09-.17 8.93-1 7.8-1 15-.12 15 .64 0 .31 7.23-.38 8.35-.29.46 0 .59 1 .48l1.41-.15-.06 3.75a15.91 15.91 0 0 1-.66 4.88c-.82 1.53-.45 1.73 1.61.87l1.74-.73.27 1.57a28.56 28.56 0 0 1-.93 9c-.36.84-.25.9 1.05.56 3.05-.8 2.94-1 2.75 4.7a28.74 28.74 0 0 1-.87 6.89 4.58 4.58 0 0 0-.51 1.88 6.92 6.92 0 0 0 2.33-.29 6 6 0 0 1 2.61-.2c.73.45-.1 5.12-1.43 8l-1.21 2.67 2.48-.33 2.48-.33v1.8a28.85 28.85 0 0 1-2.52 9.72c-1.12 2.2-1.15 2.43-.33 2.18.34-.1 1.57-.32 2.73-.49 2-.27 2.11-.22 2.11.94 0 1.52-1.71 5.93-3 7.75a7.07 7.07 0 0 0-1 1.64c0 .16 1 .15 2.23 0 2.24-.3 2.24-.3 2.24 1.23 0 1.76-1.79 6.9-2.8 8.07s-.79 1.59 1.51 1.32l2.19-.24-.65 2c-1.28 4-2.23 6.35-3.22 7.73-1.56 2.2-1.33 2.66 1.23 2.38 1.38-.16 2.23-.06 2.23.26 0 1.1-3.19 8.62-4.3 10.12a8.88 8.88 0 0 0-1.16 1.86c0 .15.9.28 2 .28 1.44 0 2 .21 2 .75 0 1.13-3.86 10.23-5 11.82s-1.1 1.81 1 2c1 .07 1.56.38 1.56.81 0 .83-4.28 9.37-5.27 10.5s-.84 1.45 1.06 1.45c1.41 0 1.73.18 1.73 1a6.71 6.71 0 0 1-.73 2.43c-.69 1.34-.67 1.45.48 2.2a8.74 8.74 0 0 0 1.35.8 36.68 36.68 0 0 0 .2-4.59c.06-4.43.84-8.81 1.57-8.81a9.1 9.1 0 0 1 2 1l1.61 1v-3.3c0-4.18.89-9.38 1.73-10.07.47-.39 1-.21 2.13.72a7.56 7.56 0 0 0 1.8 1.26c.17 0 .27-.95.23-2.11-.1-3 1.09-9.76 1.85-10.52.36-.36.49-.83.3-1s.32.06 1.14.61 1.72 1.13 2 1.28.53-.77.57-2.53c.07-2.84.79-6.19 1.61-7.46.36-.57.71-.49 2 .4 1.75 1.25 2.49 1.07 2.18-.57-.23-1.25 1.94-8.57 3-10 .63-.87.73-.86 2.49.18l1.84 1.09v-1.74a22.42 22.42 0 0 1 1-5c1.07-3.48 1.77-4 3.67-2.52 1.49 1.13 2.33.92 2-.48-.48-1.91 1.11-6.67 3.59-10.83.12-.2 1 .19 1.86.87a8.37 8.37 0 0 0 1.95 1.24c.17 0 .32-.89.32-2 0-2 2-6.94 3.22-7.94.47-.4 1-.21 2.13.72 1.86 1.56 2.09 1.56 2.09 0a14.83 14.83 0 0 1 1.55-4.34c2.16-4.31 2.31-4.46 3.58-3.57s1.82.93 1.83-.14 1.81-5.07 2.82-6.19c.76-.84.83-.83 1.49.12.95 1.36 1.55 1.24 1.93-.38s3.09-6.07 3.7-6.07a1 1 0 0 1 .72.49c.48.78 1.14.59 1.46-.41.54-1.69 4.3-6.28 5-6.14.39.08.71-.06.71-.3 0-.4 2.61-3.47 5.95-7 .68-.72 2.33-2.25 3.66-3.4 2.24-2 3.29-2.28 3.29-1.06 0 .27-.26.33-.57.14s-.44-.14-.22.22-.29 1.52-1.09 2.68a50.25 50.25 0 0 0-6.33 12 30.56 30.56 0 0 1-1.79 4.44c-.67.75-.48 1.76.32 1.76.41 0 .75.23.75.51 0 .77-2.88 6.32-3.47 6.68-.75.46-.11 1.24 1 1.24a.88.88 0 0 1 1 .77c0 1-1.77 4.5-3.31 6.56-1.41 1.88-1.17 2.6.86 2.6.8 0 1.46.19 1.46.43 0 .75-4.84 7.87-5.5 8.09-1 .34 0 1.4 1.28 1.4a2.09 2.09 0 0 1 1.42.39c.44.71-1.47 4.29-3.83 7.18a27 27 0 0 0-2.3 3.06 3.91 3.91 0 0 0 1.73.56c1 .18 1.74.57 1.74.86 0 1-3.88 7-4.92 7.67-1.54.93-1.26 1.49.95 1.86 1.42.24 2 .58 2 1.19 0 .95-3.6 5.42-6.3 7.81-1.69 1.5-1.86 2.66-.4 2.66 1 0 3.22 1.07 3.22 1.54s-2.84 3.39-6.2 6.33c-2.26 2-2.4 2.22-1.57 2.73 2.77 1.7 2.8 1.52-.91 5a26 26 0 0 1-4.34 3.53c-1.23.4-1.08.86.62 1.87.82.49 1.49 1.07 1.49 1.31 0 .74-5 4.8-8 6.53a14.63 14.63 0 0 0-2.92 1.95c0 .16.54.59 1.23 1 1.39.75 1.45.92.6 2-.65.84-6 4.49-8.45 5.75l-1.5.77 1.32.8a2.63 2.63 0 0 1 1.33 1.65c0 1.47.85 1.45 1.47 0s6.47-8 7.26-8c.28 0 .91.84 1.4 1.86l.88 1.86.9-1.73a29.5 29.5 0 0 1 4-4.9c3.27-3.35 3.15-3.33 4.69-.77.47.77.65.67 1.57-.9s5.69-5.84 6.56-5.84c.21 0 .64.9 1 2 .54 1.79 1.55 2.75 1.55 1.47 0-.72 4.9-5.38 6.56-6.24 1.35-.7 1.4-.67 2.22 1a4.41 4.41 0 0 0 1.14 1.75 7.33 7.33 0 0 0 1.77-1.53c2.79-3 8.16-5.88 8.16-4.45a8 8 0 0 0 .49 1.77l.5 1.31 1.19-1.4a20 20 0 0 1 4-3.11c2.7-1.62 2.87-1.66 3.5-.8a7.59 7.59 0 0 1 .83 1.39c.08.26.87-.19 1.74-1a18.39 18.39 0 0 1 4.94-2.74c3.08-1.15 3.42-1.19 4-.45s.73.74 2 0a12.63 12.63 0 0 1 3.84-1.13c1.37-.19 5.16-.79 8.42-1.34 6.79-1.14 12.78-.94 11.46.38a2.49 2.49 0 0 1-1.44.7c-1.18 0-10.75 3.45-13.9 5a35.81 35.81 0 0 0-5.36 3.4 20.1 20.1 0 0 1-3 2.14c-.45.15-.43.42.09 1s.56 1-.22 1.83a19.6 19.6 0 0 1-3.66 2.56c-1.52.85-2.85 1.62-3 1.71s.17.57.62 1.07a3.58 3.58 0 0 1 .83 1.25c0 .45-6 4.8-6.68 4.81a3.11 3.11 0 0 0-1.25.46c-.62.4-.55.66.42 1.7l1.16 1.22-2.41 1.75a41.62 41.62 0 0 1-5.23 3.14 9.54 9.54 0 0 0-2.86 1.79 4.16 4.16 0 0 0 1.33 1.55c1.19 1 1.26 1.26.65 2-.82 1-5.36 3.32-8.2 4.22s-2.88.9-1.25 2.88c1.18 1.43 1.36 1.93.87 2.4-.78.73-7.55 3.16-8.83 3.16s-1.22.41.08 1.79l1 1.11-1.92 1a45.82 45.82 0 0 1-6.13 2.11c-2.32.65-4.31 1.24-4.44 1.34s.15.79.61 1.56c.79 1.35.78 1.42-.27 2a37.37 37.37 0 0 1-9.66 2.55c-1.81 0-1.95.29-.87 1.84.7 1 .65 1.13-.63 2-1.5 1.07-6 2.07-9.32 2.07-1.91 0-2.05.09-1.84 1.17.29 1.54-1.35 2.13-7.19 2.6-3.63.29-4.5.53-5.33 1.47-1.13 1.27-3.5 2-8.19 2.43-5 .49-6 .77-5.76 1.54.38 1 .06 1.07-5.59 1.92a29.33 29.33 0 0 0-5.91 1.3c-.74.48-.73.51.12.23a27.18 27.18 0 0 1 3.1-.6c1.77-.24 2.14-.15 2.14.52s.19.71.87.32c1.66-1 7-2.35 7.59-2a1 1 0 0 1 .31 1.17c-.37 1-.37 1 1.73.09a22.12 22.12 0 0 1 4.84-1.06l3.15-.36-.23 1.56a6.27 6.27 0 0 0-.13 1.56c.06 0 1.05-.34 2.21-.74a12.44 12.44 0 0 1 6.87-.42c1 .27 1.14.53.89 2s-.19 1.69.82 1.4c3.65-1 6.44-1.31 8.09-.76s1.71.72 1.45 1.91l-.3 1.34 3.23-.21a25.9 25.9 0 0 1 5 .14c1.66.31 1.78.45 1.59 1.79l-.21 1.46h3.5c4.29 0 5 .36 4.54 2.32-.31 1.41-.26 1.46 1.2 1.25a17.31 17.31 0 0 1 5 .67c3.12.81 3.42 1 3.18 2s0 1.15 1.09 1.43a35 35 0 0 1 4.38 1.73c1.65.77 5.8 2.34 9.21 3.5s6.87 2.45 7.68 2.86l1.48.76-1.23.46c-.68.25-5.59.47-10.91.48-15.94 0-19.53.21-19.75.87-.46 1.38-4.92 1.52-9.6.29-1.13-.3-1.35-.18-1.49.81-.24 1.69-3.2 1.78-8 .24-4.13-1.31-3.82-1.39-4.58 1.11-.3 1-3.08.44-6.9-1.36-3.25-1.52-3.71-1.51-3.71 0s-3 1.15-6.68-.61c-2.49-1.18-3.59-1.44-4.93-1.19-1.73.33-2.73 0-8.86-3.27a14.06 14.06 0 0 0-2.72-1.14c-1.59-.31-3.32-1.21-5.83-3-3.74-2.69-4.29-2.14-1.1 1.09a15.26 15.26 0 0 1 2.83 3.41c0 .3-.48.68-1.07.83-1 .27-1 .39.87 2.42 2.58 2.82 5.32 7.17 4.93 7.81a1.32 1.32 0 0 1-1 .52c-.8 0-1 .78-.24 1.23s4 6.74 4 7.51a1.14 1.14 0 0 1-.79.88c-.71.28-.72.44-.07 1.42 1 1.49 2.33 6.24 1.84 6.43-.21.08.26 2.6 1 5.6 1.45 5.49 1.82 8.68 1 8.68-.24 0-2.66-3-5.38-6.57-6-8-7.67-9.8-9-9.8-1.17 0-2.83-2.37-5.15-7.31l-1.45-3.11-1.3 1c-1.23 1-1.33 1-1.94.15a3.25 3.25 0 0 1-.63-1.31c0-1-2.68-7.12-3.42-7.82a2.1 2.1 0 0 0-1.62-.59c-.81.16-1.34-1-2.13-4.5-.27-1.22-.39-1.3-1-.66-.9.9-1 13.76-.24 34.29.27 7.37.71 20.09 1 28.28.49 14.67.66 19 1.28 31.75.3 6.12.26 6.52-.76 7.81s-2.17 1.67-3.57.85Zm-6.85-130.43a26.92 26.92 0 0 0-3.36-4.54l-4.81-5.26a67.91 67.91 0 0 0-7.44-6.42c-5.06-3.79-5.9-4-4.75-.93.5 1.33.56 1.35 1.22.46.38-.52.77-.83.88-.7s1.23 1.7 2.48 3.47c2.55 3.6 8.4 9.48 13.26 13.34 1.76 1.4 3.27 2.54 3.36 2.54a8 8 0 0 0-.84-2Zm4.44-1.62c0-7.67-3-22-5.28-25.58-.49-.76-.57-1.35-.25-1.74.48-.59-1.61-4.48-4.44-8.27a10.73 10.73 0 0 1-1.63-3.1c-.28-1.22-.18-1.37 1-1.37.72 0 1.22-.19 1.13-.41-.57-1.44-5.64-8.41-6-8.21s-.4 2.27-.4 4.74c0 3.39.15 4.42.62 4.23 2.33-1 2.18-1.25 2.58 5.21.2 3.36.33 7 .3 8-.06 1.74 0 1.85.91 1.37 1.41-.76 1.55-.68 1.55.84a103.48 103.48 0 0 0 3 13.91c1.62 4.31 6.09 14 6.46 14s.42-1.62.42-3.6Zm22.92 3.26a1 1 0 0 0-.95 0c-.16.17.13.28.65.26s.7-.14.3-.3ZM190.93 247a80 80 0 0 0 3.73-9.8c.75-2.59 1.53-5 1.75-5.48a43.4 43.4 0 0 0 .75-6.86l.37-6.09 1.18.29 1.18.3v-5c0-5.7-.43-6.17-2.91-3.15-1.54 1.85-1.6 2.64-.23 2.64.54 0 .66.27.41.87a19.3 19.3 0 0 0-.61 1.86 5.87 5.87 0 0 1-.85 1.74c-1.15 1.48-3.9 9.62-3.31 9.82.31.1.47.35.34.55-.28.46-3.6 15.51-3.95 17.91-.14 1-.38 2.57-.54 3.59s-.16 1.86 0 1.86a31.26 31.26 0 0 0 2.68-5.08Zm7.47-.62c4.65-4.07 7.9-7.9 7.27-8.53s-8.75 6.66-8.75 7.84a.47.47 0 0 1-.48.44 10.21 10.21 0 0 0-2.48 2.71c-2 2.71-2.59 3.88-1.33 2.66.37-.36 3-2.67 5.77-5.12Zm-45.31-22.2c-.14-.34-.25-.07-.25.62s.11 1 .25.62a1.88 1.88 0 0 0 0-1.24Zm-9.75-8.31a.5.5 0 1 0-1 0 .5.5 0 0 0 1 0Zm-9.43-9.37c0-.43-2-1.67-2.29-1.4s0 .81.37 1.52c.46 1 .74 1.18 1.26.75a1.75 1.75 0 0 0 .66-.87Zm104.18.23a5.64 5.64 0 0 1 .33-1c.26-.7.12-.78-.87-.47-1.51.46-1.61.56-1.21 1.2s1.75.77 1.75.32Zm-35.46-3.12c-.32-1.65-.42-1.75-.77-.81a3.33 3.33 0 0 1-1 1.43c-.41.25-.23.57.59 1 1.55.82 1.61.66 1.18-1.62Zm.24-5.38c0-1.21-.14-2.21-.31-2.21-.51 0-3.15 3.19-3.15 3.81s2.3 1.14 3.09.77a4.68 4.68 0 0 0 .37-2.37Z" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#33502e', + }} + /> + <path + d="M154 317.38c0-.43.22-.78.5-.78a.48.48 0 0 1 .49.47 1.05 1.05 0 0 1-.49.77c-.24.16-.5-.04-.5-.46Zm65.27-4.1c-.27-.71-.22-.77.28-.28.33.33.49.73.33.89s-.38-.13-.57-.61ZM157 311.42a2.1 2.1 0 0 1 .49-1.52c.28-.16.5 0 .5.35s.54 0 1.19-.84c1.28-1.59 1.31-2.29.06-1.25-.52.43-.75.45-.75.07a.53.53 0 0 1 .49-.56c.27 0 .5-.33.5-.74s.22-.75.49-.75.5-.55.5-1.24-.28-1.24-.77-1.24-.64-.22-.47-.5a.54.54 0 0 1 .77-.2c.23.14.55-.3.72-1s.67-1.24 1.11-1.24c.62 0 .73-.33.49-1.54-.29-1.45-.23-1.52.93-1.24.71.19 1.24.1 1.24-.19s.53-.51 1.17-.51a2.67 2.67 0 0 0 1.81-.74c.42-.51.44-.74.05-.74s-.55-.56-.55-1.25.28-1.24.74-1.24a.75.75 0 0 0 .75-.74c0-.41-.19-.74-.41-.74s-.28-.94-.12-2.09c.25-1.88.44-2.12 1.92-2.42 2.09-.43 2.54-.9 1.45-1.51-.7-.39-.76-.68-.32-1.5s.38-1.18 0-1.54a2.68 2.68 0 0 1-.22-2c.23-1.13.13-1.43-.4-1.25a2 2 0 0 0-.85 1.56c-.09.72-.33 1.32-.55 1.32s-.8 1-1.31 2.23a6.1 6.1 0 0 1-1.24 2.24 7.57 7.57 0 0 0-1.51 2.35 32.75 32.75 0 0 1-2.48 4.09 15.93 15.93 0 0 0-1.57 2.48c-.19.5-.3.38-.32-.37 0-1.06-1-1.65-1-.62a.5.5 0 0 1-.5.5c-.92 0-.51-.82.74-1.48a3.51 3.51 0 0 0 1.52-1.58 1.25 1.25 0 0 1 1-.91c.39 0 .71-.19.71-.43s-.51-.4-1.12-.37-1.1.28-1.09.55c0 .6-3.15 3-4.61 3.44-.57.17-1.18.09-1.36-.19s.05-.52.51-.52a4.31 4.31 0 0 0 2-1c1.22-.95 1.22-1 .22-1.59s-.94-.67.74-1.49a4.44 4.44 0 0 1 2.59-.55c.7.25.67.11-.15-.84l-1-1.15 1.07.56a2.57 2.57 0 0 0 2.23.11c.64-.26.94-.49.66-.51s.13-.57.9-1.19a4.59 4.59 0 0 0 1.53-2 2 2 0 0 1 .82-1.29c.38-.24.53-.59.34-.79s-.77 0-1.27.47a3.13 3.13 0 0 1-1.36.83 6 6 0 0 0-1.65.62c-1 .53-1.64.54-3.22.05-1.09-.34-1.59-.63-1.12-.66s.75-.32.54-.88-.08-.76 1-.49 1.24.18 1-.66a2.3 2.3 0 0 1 1.81-2.49.53.53 0 0 0 .53-.5c0-.27.44-.49 1-.49s1-.23 1-.5c0-1.53-4.49 1.24-5 3.12l-.33 1.1-.05-1.1a4.41 4.41 0 0 1 1.57-2.61c1.49-1.43 1.54-1.59.74-2.2-1.19-.89-1.1-1.42.5-2.82 3.57-3.15 4.09-3.41 6.29-3.11 1.69.24 2.17.13 2.43-.54.18-.46.55-.69.82-.52s.5 0 .5-.44a1.73 1.73 0 0 1 .87-1.26c1.59-.92.11-.55-1.92.49a5.38 5.38 0 0 1-3.33.73 5.35 5.35 0 0 0-3.29.72 10.41 10.41 0 0 1-2.5 1 8.29 8.29 0 0 0-2.48 2 6.06 6.06 0 0 1-3.05 2c-1.46 0-3.21.78-7 3.11a12.12 12.12 0 0 1-4 1.83c-1.84-.2-2.52.09-6.61 2.74s-4.22 2.67-4.65 1.7c-.56-1.2-1.69-1-5.5.81a21.54 21.54 0 0 1-4.09 1.52c-1.5.3-1.64.21-1.92-1.23s-1.42-2.05-2.45-1.13a38 38 0 0 1-7.89 3c-.43 0-.85-.71-1-1.74-.36-1.87-.61-2-3.13-1.25-2.7.77-8.7 1.49-9.88 1.18a1.34 1.34 0 0 1-1.1-1.39c0-1.57-.27-1.69-2.79-1.15-3.23.69-5.47.17-5.77-1.35-.12-.67-.09-1.29.07-1.4s0-.05-.44.11a25.83 25.83 0 0 1-5 .16c-3.09-.1-4.31-.33-4.61-.87s-1.27-.73-3.36-.73c-1.63 0-2.95-.17-2.95-.38 0-.42-9.91-.34-11.87.1-1.09.25-1.15.21-.39-.25a16.49 16.49 0 0 1 4.59-.82c2-.16 3.7-.47 3.7-.7s.67-.28 1.49-.11 1.48.1 1.48-.28.79-.48 2.61-.27 2.41.17 1.86-.23-.34-.46.92-.21c.93.17 1.8.1 2-.18.41-.64 4-1.32 4-.76 0 .23-.94.84-2.09 1.35s-1.83 1-1.51 1.12a10.18 10.18 0 0 0 3.6-1.32c3.48-1.77 3.48-1.77 3.48-.79a.74.74 0 0 0 .74.75.76.76 0 0 0 .75-.75c0-.62.48-.69 2.72-.43 2 .24 2.73.16 2.73-.27s.37-.48.95-.3 1.1.07 1.27-.22a1.35 1.35 0 0 1 1-.51c.52 0 .45.26-.28 1a3.4 3.4 0 0 1-1.49 1 .51.51 0 0 0-.5.5c0 .27-.44.49-1 .49s-1 .23-1 .5-.17.47-.37.44c-.8-.11-2.61.7-2.61 1.16 0 .27.36.36.79.19a20.16 20.16 0 0 1 2-.6c.66-.17 1.08-.47 1-.69s.34-.54 1-.71a9.51 9.51 0 0 0 2.6-1.25c1.58-1.1 3.88-1.69 4.25-1.07.29.45 2.64-.23 4.67-1.37.64-.35 1.26-.54 1.38-.43.34.35-3.93 4.33-4.64 4.33a.56.56 0 0 0-.62.5c0 .27.33.49.74.49s.74-.2.74-.44.33-.56.72-.72.63 0 .44.45c-.42 1.11 1.13.35 3.23-1.59A6.41 6.41 0 0 1 111 273c.48 0 .87-.23.87-.5 0-1 1-.44 1 .62 0 .61.15.84.29.49a1.38 1.38 0 0 1 1.2-.62c.52 0 1-.24 1-.55s.23-.37.75.06.74.44.74.06.61-.57 1.37-.59c1.2 0 1.25-.09.37-.46s-.45-.44 1.26-.47a5.68 5.68 0 0 0 2.8-.57 2.32 2.32 0 0 1 1.7-.37c1.11.16 1.13.2.26.91-.5.41-1.4 1.19-2 1.74-1 .92-2.19 1.73-6.86 4.75a5.81 5.81 0 0 0-1.86 1.57c0 .21-.33.37-.72.37a1.37 1.37 0 0 0-1.07.58c-.22.35-.13.44.22.22s.57-.14.57.13a.74.74 0 0 1-.48.65c-.27.09-.35.67-.19 1.3l.3 1.13.67-1.29a4 4 0 0 0 .56-1.7c-.11-.39 4.75-4 5.37-4a2.46 2.46 0 0 0 1-.62 24.89 24.89 0 0 1 3.41-2.62l1.42-.93-.37 1.22c-.34 1.12-.3 1.15.47.42a4.38 4.38 0 0 0 1.22-2.93 10 10 0 0 1 3-.26c2.12 0 3.14-.25 3.59-.82.64-.82 2.86-1.72 2.86-1.16s-3.08 3.28-9.08 8.19c-2.17 1.78-3.87 3.59-3.62 3.84s.65 0 1.11-.41a2.57 2.57 0 0 1 1.82-.53c.71.12.9 0 .69-.59s-.07-.68.37-.51 1-.25 1.47-.88.8-.86.8-.53a.72.72 0 0 0 .79.61c.57 0 .73-.31.54-1s-.08-.95.69-.71 1 0 1.27-1.23.48-1.48 1.36-1.22.94.2.43-.33c-1-1-.75-1.62.87-2.54s2-.59.89.5c-.43.42-.44.59 0 .59s.69-.38.85-.87a4.56 4.56 0 0 1 4.17-3.39l2.07-.17-.76 1.45a12.64 12.64 0 0 1-2 2.69 4.21 4.21 0 0 0-1.23 1.88c0 .36.52 0 1.16-.8.77-1 1.37-1.31 1.81-1s.53.22.29-.19.2-1.34 1-2.35c1.08-1.4 1.58-1.69 2.53-1.45s1.18.08 1.18-.41.35-.62 1-.45 1 0 1-.58.28-.75.62-.63a2.65 2.65 0 0 0 1.81-.71c.66-.51 1.29-.84 1.4-.72.29.3-.94 2.37-2 3.35-.6.56-.71.94-.34 1.17s3-2 3-2.85c0-.2.51-.31 1.12-.25.82.09 1.08-.11 1-.75s.23-.87 1.86-.87c2.08 0 2.36.39 1.49 2-.35.66-.35 1 0 1s.52-.17.52-.38c0-.61 2.86-3.59 3.44-3.59.29 0 .53-.38.53-.87 0-.74.08-.75.49-.12.27.41.48.52.49.23 0-.81 4.36-2.27 4.64-1.56.14.37.52.18 1-.5s1.81-1.3 3.66-1.7a48.2 48.2 0 0 0 5.25-1.31c1.44-.43 2.14-.42 2.85 0a1.48 1.48 0 0 0 1.88 0c.8-.5 1-.44 1.2.37.26 1 1.34 1.37 1.34.49 0-.27.31-.36.69-.22a.85.85 0 0 0 1-.21c.42-.68 7.46-.3 10.81.59 1.56.41 3.86.9 5.13 1.09s2.41.65 2.56 1a1.28 1.28 0 0 0 1.14.68c1 0 2.15 1.16 1.66 1.65-.18.19.17.33.78.33s1 .22.77.53 0 .46.42.38c.89-.19 3.42 2.57 3.11 3.39s1.53 2.33 2.46 2c.43-.17.64-.55.46-.84a.9.9 0 0 0-1.06-.25 1.63 1.63 0 0 1-1.29-.05c-.34-.21-.21-.45.36-.65.77-.27.51-.74-1.77-3.2-3-3.22-3.53-4.13-2-3.31a6.38 6.38 0 0 0 2.41.58c.77 0 1.12.16.78.3-1.21.48-.6 1.1 1.24 1.25 1.25.1 1.86.39 1.86.89 0 .91 1.56 2.16 2.2 1.76a.64.64 0 0 1 .82.3c.22.36.14.44-.22.23s-.57 0-.57.72c0 .9.2 1 1.29.75s1.26-.24.95 1.15c-.25 1.18-.11 1.59.7 2 .56.3 1 .71 1 .9-.16 1 .12 1.55.69 1.33.37-.14.87.38 1.2 1.25.42 1.13.42 1.52 0 1.55s0 .2.62.39a2.22 2.22 0 0 1 1.41.94c.33 1-.51.7-3.37-1.17-3.31-2.17-6.64-3.68-8.12-3.68-1.17 0-4.27-2.43-4.27-3.35 0-.29-.44-.41-1-.27s-1 .06-1-.19-.89-.48-2-.52-2 .11-2 .35a17.42 17.42 0 0 0 3 3.27 16.29 16.29 0 0 1 3 3.35c0 .28-.14.35-.32.18s-.68.07-1.11.54a3.12 3.12 0 0 1-3.9.51c-.61-.31-1.28-.38-1.49-.17s-.09.51.25.62a1.29 1.29 0 0 1 .62 1.18.87.87 0 0 0 .75.95c.4 0 .74.2.74.45s.57.39 1.27.31c1.11-.12 1.24 0 1 1s-.13 1.09.83.85a2.94 2.94 0 0 0 1.48-.82c.2-.3.37.19.37 1.07s.23 1.61.51 1.61c.58 0 .44 1.1-.19 1.56-.25.17.54.49 1.73.69 1.76.3 2 .45 1.18.76-1.83.73-3.4.48-5.31-.85-1.05-.72-2-1.17-2.21-1-.46.46.83 1.82 1.76 1.83.71 0 .7.1-.06.68-.95.72-1.18 1.79-.38 1.79.28 0 .5.38.5.84 0 .73.09.71.71-.13s.73-.82 1.05 1.12.41 2 1.55 1.61c1.55-.59 2.11-.58 1.16 0-.61.38-.54.65.45 1.71.65.69 1.37 1.13 1.6 1s.43 0 .43.26a.5.5 0 0 0 .48.52c.26 0 .35-.38.19-.86-.23-.73-.19-.75.26-.13a2.81 2.81 0 0 1 .54 1.12c0 .75-2.37.34-3.6-.62-1.49-1.17-3.3-1.41-2.39-.3s2.91 5.26 2.5 5.26c-.22 0-.71-.61-1.11-1.36-2.77-5.33-3.6-6.71-4.17-6.93a1 1 0 0 1-.64-.86 8.71 8.71 0 0 0-1.49-2.68 10 10 0 0 1-1.49-2.52 1.06 1.06 0 0 0-.5-.78c-1-.6-.48 1.62.75 3.43a6.86 6.86 0 0 1 1.22 2.67c0 .51-.48.09-1.19-1.07-1.11-1.81-1.21-1.86-1.72-.9-.67 1.25-.7 1.73-.07 1.34s1.29 1 1.71 3.33c.26 1.42.18 1.86-.35 1.86-.37 0-.95-.83-1.26-1.86-.94-3-1.27-3.36-2.62-2.42s-2.14 1.06-1.63.24c.21-.34.14-.44-.17-.25a.55.55 0 0 1-.81-.29c-.15-.34-.16.05 0 .86.24 1.47.23 1.47-.32.25-.31-.68-.9-2.13-1.32-3.22s-1.06-2.69-1.43-3.55c-.94-2.15-.91-3 .08-2.15.41.35.89.49 1.07.31s.06-.32-.26-.32-.58-.47-.58-1.05c0-.89-.11-.95-.75-.42s-.7.46-.22-.8a4.49 4.49 0 0 1 1.11-1.78c.32-.2.41-.53.19-.74s-.56-.1-.78.26a1.12 1.12 0 0 1-1.09.5c-1.14-.22-3 .59-2.6 1.17.2.33.1.39-.28.16s-.48-.9-.28-1.81.12-1.44-.22-1.44a.51.51 0 0 1-.53-.48c0-.26.3-.36.66-.23a1.22 1.22 0 0 0 1.25-.45c.47-.56.34-.86-.63-1.49A4.32 4.32 0 0 1 191 273c-.37-1.17-1.24-1.36-1.24-.26 0 .41-.22.74-.5.74s-.49-.33-.49-.74a.73.73 0 0 0-.71-.75c-.38 0-.85-.66-1-1.48-.4-1.81-1.14-2.37-1.55-1.15-.16.49-.72 1.84-1.23 3a12.43 12.43 0 0 0-.94 4.25 6.44 6.44 0 0 1-.66 3c-.62.85-.72.83-1.73-.46-.74-.94-1.14-1.15-1.28-.68s-.46 1.43-.75 2.36a10.85 10.85 0 0 0-.53 2.73c0 .95.06 1 .53.17a1.6 1.6 0 0 0 .22-1.33.56.56 0 0 1 .22-.77c.68-.42.65.11-.32 5-.67 3.43-1 4.18-1.73 4.18a2.11 2.11 0 0 1-1.53-1c-.88-1.41-1.37-1.23-1.37.5 0 .82-.19 1.49-.42 1.49s-.71.89-1.07 2c-.57 1.74-2 3.69-2 2.75 0-.18-.25-.18-.55 0s-.37.46-.07.65c.72.47-1.17 4.53-2.11 4.53a1.22 1.22 0 0 1-1.06-.75c-.42-1.09-.92-.91-1.7.62a21.58 21.58 0 0 1-2.22 3.2 41.09 41.09 0 0 0-3.23 4.83c-1.8 3.13-2.95 3.84-2.95 1.8Zm5.21-7.72a1.15 1.15 0 0 0-.9-.48c-.36 0-.29.19.15.48.97.62 1.14.62.77 0Zm1.93-.78c.63-1.64.28-2.43-.45-1-.86 1.66-.89 1.8-.3 1.8.26-.02.61-.37.77-.8Zm8.38-10.16c.22-.83.14-.91-.43-.44-.38.32-.57.78-.42 1 .4.66.56.55.85-.58Zm1.31-4.34c0-.64-.18-1-.46-.84a3.33 3.33 0 0 0-.46 2.14c0 1.48.11 1.63.46.84a7.23 7.23 0 0 0 .48-2.14ZM200 289c-.16-.16-.28.13-.26.66s.14.69.3.3a1 1 0 0 0 0-1Zm-1.62-2.26c-.16-1-.49-1.84-.73-1.84-.57 0-.54.52.12 2.43s.97 1.67.59-.64Zm-21.87-1.1c.06-.27-.43-.57-1.1-.66-1.07-.15-1.13-.08-.54.64s1.41.81 1.62-.03Zm23-.61c-.16-.16-.28.13-.25.66s.13.69.29.3a1 1 0 0 0 0-1Zm-21.83-2c-.16-.16-.28.13-.25.66s.14.69.29.29a1 1 0 0 0 0-.95Zm-1.29.36a.48.48 0 0 0-.46-.49 1.07 1.07 0 0 0-.78.49c-.16.28 0 .5.47.5s.74-.25.74-.53Zm31.35.16a1 1 0 0 0-1 0c-.16.16.13.28.65.26s.7-.14.3-.3Zm-82.34-1.58a1.11 1.11 0 0 1-.3-1.35c.31-.89.29-.89-.47 0-1 1.32-1 1.52.28 1.62.7.06.87-.05.46-.3Zm77.47-.82c.17-.43-.32-1.61-1.09-2.68-1.18-1.62-1.47-1.8-1.89-1.13s-.25 1 .41 1.68a3 3 0 0 0 1.45.9c.31 0 .44.44.3 1-.3 1.16.39 1.36.82.24Zm-71.62-.52c0-.11-.22-.21-.5-.21a.5.5 0 0 0-.49.52c0 .29.22.39.49.22s.47-.43.47-.56Zm-31.75-.21a.5.5 0 1 0-.5.49.5.5 0 0 0 .47-.52Zm100.71-.5c0-.27-.39-.49-.87-.49-.74 0-.76.08-.12.49.9.58.96.58.96-.03Zm-71.06-.9c.12-.36-.11-.47-.59-.29s-.78.53-.78.79c-.03.62 1.09.21 1.34-.52Zm46.25.4a.5.5 0 1 0-.49.5.49.49 0 0 0 .46-.53Zm-72.4-1.64c-.94-.94-1.18-1-2.31-.38-1.79 1-1.57 1.24 1.06 1.34l2.3.09Zm68.87.34c0-.42.16-.67.37-.54s.4-.42.43-1.23l.05-1.45-.92 1.52c-1 1.69-1.17 2.48-.43 2.48.28 0 .5-.35.5-.78Zm-33.64-2.82c.36-.13.47-.43.25-.65-.38-.42-3.32 2.33-3.32 3.1 0 .22.54-.2 1.21-.92a7.2 7.2 0 0 1 1.89-1.53Zm-53.09.28a1 1 0 0 0-1 0c-.16.17.13.29.65.26s.7-.14.3-.3Zm114.86-.95c-.18-.46-.44-.72-.57-.58s-.11.62.07 1.08.43.72.57.59.08-.63-.07-1.09Zm4.66.58c0-.95-.71-1.35-1.7-1-.62.24-.67.47-.24 1 .73.92 1.92.9 1.92 0Zm20.84-.06c0-.42-.22-.64-.5-.47a1.07 1.07 0 0 0-.49.77.48.48 0 0 0 .49.47c.26 0 .48-.35.48-.77Zm-125.3-.48c.73-.55.78-.73.22-.73a3.49 3.49 0 0 0-1.7.73c-.73.55-.79.73-.22.73a3.62 3.62 0 0 0 1.68-.73Zm77.18 0c0-.41-.22-.74-.48-.74s-.36.33-.2.74.37.74.48.74.2-.33.2-.74Zm2.77-.74a.73.73 0 0 0 .7-.75c0-.4-.22-.74-.48-.74-.5 0-1.53 1.63-1.46 2.33 0 .22.14.12.28-.22a1.11 1.11 0 0 1 .94-.61Zm-4.71-.15c-.95-.94-.63-1.87.57-1.67.61.1.84.09.5 0s-.62-.69-.62-1.28c0-1-.05-1-1.24.26-1.65 1.78-1.63 3.09.05 3.19.92.03 1.12-.1.72-.49Zm31.21-.07a1.29 1.29 0 0 0-.46-1c-.59-.37-1.19.56-.78 1.22s1.22.61 1.22-.21Zm-59-.56c0-.42-.22-.63-.5-.46a1.05 1.05 0 0 0-.49.77.48.48 0 0 0 .49.47c.22.01.44-.34.44-.77Zm-3.22-.22a1.09 1.09 0 0 0-.81-.49c-.27 0-.35.22-.19.49a1.09 1.09 0 0 0 .81.5c.21.01.3-.21.13-.49Zm-15.46-.76c-.16-.42-.4-.66-.53-.53s-.09.48.09.77c.49.79.78.63.44-.24Zm37.33-.59c-.29-.3-1.78.47-1.78.92 0 .24.43.17 1-.16s.9-.68.81-.76Zm30.22.36a1.42 1.42 0 0 0-1.15-.46c-.68 0-.65.11.15.46 1.27.58 1.29.58.94.06Zm24.77-.23c-1.26-1-2-1-1.21 0a2.11 2.11 0 0 0 1.4.75c.52-.01.52-.16-.25-.74Zm-90.38-.1a2 2 0 0 0-1.24 0c-.34.14-.07.25.62.25s.89-.1.56-.24Zm18.08-.44c.45-.45.6-1 .34-1.22s-.68.07-1 .78c-.66 1.4-.44 1.54.67.44Zm19.92-.27c-.89-.57-1.91-.56-1.91 0s1.52 1 2.16.63c.18-.1.06-.39-.31-.62Zm20.3-2.13c-.06-.68-.34-1.08-.68-1-.65.25-.34 2.15.35 2.15.18.08.33-.48.27-1.14Zm-13-.66c-.17-.17-.29.13-.26.65s.14.7.3.3a1 1 0 0 0 0-.95Zm-21.39-.63a1 1 0 0 0-.77-.5.48.48 0 0 0-.47.5c0 .27.35.49.78.49s.64-.21.42-.48Zm14-2.74c-.14-.15-.42.12-.61.61-.27.7-.21.76.28.28.33-.33.51-.73.33-.88Zm11.48-1c.31.19.4 0 .22-.45-.37-1-.35-1-1.48.34-.72.84-.74.95-.1.45a1.45 1.45 0 0 1 1.36-.34Zm35.75.23c0-.28-.22-.38-.49-.21s-.5.4-.5.52.22.22.5.22a.51.51 0 0 0 .49-.5Zm-28.44-1.58c-.14-.34-.26-.06-.26.62s.12 1 .26.62a1.88 1.88 0 0 0 0-1.23Zm16.74.8c.48-1.23 0-1.4-.68-.22-.3.57-.35 1-.09 1s.6-.36.77-.81Zm-35.2-.71c-.43-.68-.72-.55-.72.31 0 .43.22.64.5.47a.57.57 0 0 0 .22-.77Zm2 .53c.16-.27 0-.49-.47-.49s-.77.22-.77.49a.48.48 0 0 0 .46.5 1.06 1.06 0 0 0 .79-.51Zm22.07 0a.51.51 0 0 0-.52-.49c-.29 0-.39.22-.22.49s.4.5.52.5.23-.21.23-.51Zm-19.65-.54a1.28 1.28 0 0 0 1.39.23c.45-.27.27-.49-.59-.71l-1.24-.33 1.2-.48c.72-.29 1-.62.66-.82s-2.2.22-3.49.95c-.07 0 0 .46.2.93.24.65.42.7.72.23s.53-.51 1.15 0Zm24.48-.58c.34-.26.5-.67.35-.92-.4-.63-1.21-.54-1.21.13a2.15 2.15 0 0 1-.87 1.24c-.81.63-.8.65.13.35a6.63 6.63 0 0 0 1.61-.79Zm-16.24-1.45c0-.26-.34-.14-.74.27a2.63 2.63 0 0 0-.75 1.09c0 .19.34.07.75-.28a2 2 0 0 0 .75-1.07Zm21.33 1.11a1.07 1.07 0 0 0-.49-.77c-.27-.17-.5 0-.5.47s.23.77.5.77a.48.48 0 0 0 .5-.46Zm-2-1.76c-.34-.41-.7-.75-.81-.75s-.18.34-.18.75a.77.77 0 0 0 .8.74c.69.01.73-.12.22-.73Zm-24.31-.25a.49.49 0 0 0-.47-.5 1.09 1.09 0 0 0-.77.5c-.17.27 0 .5.47.5s.8-.22.8-.49Zm3.64-.29a1.55 1.55 0 0 1 .39-1c.27-.32.3-.69.06-.83-.52-.33-3.67 1.62-3.38 2.1s3 .29 2.93-.24Zm15.46.29a1.71 1.71 0 0 0-1.3-.5c-.54 0-.85.23-.68.5a1.71 1.71 0 0 0 1.3.5c.57.01.88-.22.75-.49Zm-12.76-.9c.11-.32-.16-.59-.59-.59a.79.79 0 0 0-.79.79c.03.88 1.1.73 1.41-.19Zm7.55-.37c0-.12-.22-.22-.49-.22a.51.51 0 0 0-.5.53c0 .29.22.38.5.22s.52-.4.52-.52ZM156 309.41c0-.41.22-.75.49-.75s.35.34.19.75-.37.74-.48.74-.2-.33-.2-.74Zm59.34-1.65c-.14-.23.06-.42.44-.42s.59.19.45.42-.34.41-.45.41-.28-.17-.42-.41Zm-1-1.58c-.17-.27-.07-.49.21-.49a.51.51 0 0 1 .53.49c0 .28-.1.5-.22.5s-.37-.22-.54-.5Zm-60.48-3.1a13.94 13.94 0 0 1 1.12-1.52c.58-.67.63-1 .18-1.26s.17-.39 1.14-.42c1.17 0 1.73.18 1.73.65s-.19.69-.42.69a6.06 6.06 0 0 0-1.86 1.24c-1.55 1.33-2.47 1.63-1.89.62Zm57.07-2.17c-1.66-1.79-1.23-2.22.49-.5.73.73 1.2 1.45 1.06 1.6s-.87-.35-1.57-1.1Zm-3.48 0a7.3 7.3 0 0 1-.31-.9 2.51 2.51 0 0 1 1-.18c1.15-.09 1.28.49.3 1.31-.55.47-.78.42-1.01-.14Zm-51.9-2c0-.4.2-.72.46-.72a5 5 0 0 0 1.3-.33c.58-.22.78-.12.63.33-.33 1-2.39 1.63-2.39.72Zm2.48.27a.5.5 0 1 1 .5.49.5.5 0 0 1-.53-.43Zm-4-.22c0-.12.22-.36.5-.53s.49-.07.49.22a.51.51 0 0 1-.49.53c-.27.06-.53-.04-.53-.18Zm5.05-1.3a1.55 1.55 0 0 1 .87-.87c.37-.12.56.07.44.44a1.61 1.61 0 0 1-.87.87c-.36.18-.52-.01-.43-.38Zm48.53-.71c0-.41.37-.56 1-.4a2.5 2.5 0 0 1 1 .4 2.57 2.57 0 0 1-1 .39c-.61.22-.99.07-.99-.34Zm-50.11-.19a1.1 1.1 0 0 1 .5-.81c.27-.17.5-.08.5.19a1.12 1.12 0 0 1-.5.81c-.26.22-.49.14-.49-.13Zm48.62-.06a.5.5 0 1 1 .5.49.5.5 0 0 1-.49-.43Zm-42.46-.63c-.38-.42-.34-.94.13-1.84a6.17 6.17 0 0 0 .65-2.37c0-.62.31-1.12.7-1.12.84 0 1.37-1.21 1-2.23-.16-.41 0-.74.26-.74.78 0 .68 3-.18 5.4a5.86 5.86 0 0 0-.44 2.79c.33.96-1.31 1.04-2.11.17Zm39.79-2.07c-.18-.46-.21-1-.07-1.08s.39.13.57.58.21.95.07 1.09-.39-.15-.57-.59Zm-16.1-4.17c0-.68.12-1 .26-.62a1.88 1.88 0 0 1 0 1.24c-.14.32-.26.05-.26-.64Zm26 .5c-.16-.27-.07-.5.22-.5a.52.52 0 0 1 .53.5c0 .27-.1.49-.22.49s-.39-.24-.56-.51Zm-49-6.28a1.88 1.88 0 0 1 1.24 0c.34.14.06.25-.62.25s-.97-.13-.64-.3Zm82.68-3.4a27.54 27.54 0 0 1-4-1.65c-2.55-1.31-3.12-1.34-3.12-.18s-1 2.08-1.93 1.63l-4-2a36 36 0 0 0-4.68-2 2.67 2.67 0 0 1-1.68-1.05c-.12-.37 0-.52.31-.33s.77-.07 1.06-.55c.5-.84.55-.84 1.05 0 .29.48.74.75 1 .59.45-.28-6.85-7.65-7.6-7.66-.21 0-.37-.25-.37-.55a7.14 7.14 0 0 0-1.79-2.27c-1-.95-1.66-1.85-1.51-2a5.35 5.35 0 0 1 2.48.48 5 5 0 0 0 2.53.45c.17-.17.44.19.6.79s.54 1.09.85 1.09a12.75 12.75 0 0 1 2.92 1.26 8 8 0 0 0 2.35 1 15.87 15.87 0 0 0-2.11-2.24l-2.1-2 1.57.44a3.47 3.47 0 0 0 1.81.21c.14-.13.4.26.59.86s.49.88.67.62a3.67 3.67 0 0 1 2.36-.27c1.73.17 2 .36 1.81 1.15-.14.54 0 .95.33.95s.45-.23.28-.5 0-.49.47-.49a.76.76 0 0 1 .77.74.68.68 0 0 1-.61.74c-.33 0 .16.7 1.1 1.56s1.78 1.31 1.89 1c.27-.82 1.57-.69 1.91.18a1.55 1.55 0 0 0 1.34.79c.59 0 .79.15.45.29a1.48 1.48 0 0 0-.62 1.32c0 .88.1.94.49.33.29-.44.49-.51.49-.15s.58.49 1.75.3c1.67-.27 2.48.46 1.11 1-.37.15-.27.27.25.29.89 0 1.13-.8.49-1.71a17.24 17.24 0 0 0-2.85-2.09 19 19 0 0 1-3.27-2.51 13 13 0 0 0-2-1.75c-.63-.44-1-1-.81-1.15s1 .13 1.8.72a3.67 3.67 0 0 0 3.1.81l1.64-.26-1.47.67c-.81.37-1.26.8-1 1a7.2 7.2 0 0 0 3.2.4 13.62 13.62 0 0 1 1.87-.25c1.7-.14 2 .17 1 1.14-.46.46-.29.59.78.59.75 0 1.24-.2 1.08-.46a.56.56 0 0 1 .22-.77c.27-.17.5 0 .5.4 0 1.13 1.2 1.92 1.89 1.23.31-.32.75-.41 1-.2s-.06.73-.61 1.14c-.95.74-.93.76.38.45.75-.17 1.36-.12 1.36.11 0 .42 1 1 4.44 2.43 1.54.66 1.65.66 1.08 0a51.49 51.49 0 0 0-4.62-3.78c-4.07-3.08-4.32-4.09-.38-1.6 3.77 2.37 3.65 2.32 3.31 1.43a1 1 0 0 1 .24-1.15c.33-.2.41-.11.2.22s.19.69 1 .9c1.23.31 1.27.38.4.72-.64.25-.72.39-.23.41 1.5.06 2.9-.56 2.66-1.18-.17-.44.14-.56 1-.39.7.13 1.27.44 1.27.68s.33.44.74.44.75-.23.75-.51c0-.65 2.47-.07 3.51.82a2.54 2.54 0 0 0 1.24.67c.25 0 .12-.22-.29-.49-.6-.38-.53-.48.37-.48.7 0 1.12.29 1.12.78s.27.69.62.55c.91-.38 2.17-.72 1.74-.48-1 .54-.22 1.07 1.11.8.82-.17 1.49-.1 1.49.15s1.14.5 2.54.56a13.26 13.26 0 0 1 4.19.9 5.24 5.24 0 0 0 3.29.46c1.22-.24 1.45-.19.9.2s-.52.52.49.53 1.07.13.52.47a9.49 9.49 0 0 1-3.38.45 7.33 7.33 0 0 0-3.42.64c-.8.66-5.53.74-8.22.15-1.18-.26-1.49-.13-1.71.7-.35 1.35-1.19 1.45-5.8.67-4.33-.73-5.27-.58-5.27.82 0 1-1.13 1.75-1.71 1.18a3.91 3.91 0 0 0-1.66-.48 36.18 36.18 0 0 1-4.71-1.2 11.28 11.28 0 0 0-3.64-.78 6.1 6.1 0 0 0-.65 2.39c-.06.44-1.18.34-3.31-.3Zm3.24-4.37c0-.21-.19-.26-.46-.09a1.75 1.75 0 0 0-.46 1.39c0 1.05 0 1.05.46.1a7 7 0 0 0 .44-1.42Zm-17.06-.13c-1.46-1.07-2.18-.82-.78.27.68.52 1.35.83 1.49.69s-.18-.57-.71-1Zm39.72-.08c-.17-.16-.29.13-.26.66s.14.69.3.3a1 1 0 0 0 0-1Zm-33.13-.46c-.24-1.23-1.36-1.41-1.27-.19 0 .56.38 1 .75 1s.61-.37.52-.83Zm-2.31 0a1 1 0 0 0-.94 0c-.17.17.13.29.65.26s.69-.14.29-.3Zm34.82-1.26c-.43-.69-1.15.1-.83.92.23.6.38.63.69.13a1.15 1.15 0 0 0 .12-1.08Zm-36.24-.4c-.05-.67 0-.94.16-.6.3.71 1.67.84 1.67.17a.7.7 0 0 0-.46-.62 26.51 26.51 0 0 1-3-2.56c-1.38-1.32-2.51-2.14-2.51-1.84 0 .66 3 3.48 3.67 3.48.27 0 .05.33-.48.73-.7.53-.77.74-.24.75s.59.14.12.61c-.86.86-.73 1.49.28 1.28.57-.12.83-.59.77-1.4Zm33.79.71c1.07-.42 1-1.15-.29-2.25-1.06-.92-2.18-1.27-1.72-.53.13.22-.53.3-1.46.19-1.87-.22-2.16.2-.83 1.21 1 .78 2.6.9 2.6.19 0-.27.34-.5.75-.5.9 0 .95.58.15 1.39s-.4.76.8.3Zm-27.74-2.18c-.17-.27-.41-.5-.53-.5s-.22.23-.22.5a.51.51 0 0 0 .53.49c.27-.03.37-.25.2-.52Zm-9.06-1c-.48-.46-.94-.64-1-.41a.4.4 0 0 1-.62.15c-.55-.33-1.55.21-1.25.68a3.83 3.83 0 0 0 2 .38c1.67 0 1.72 0 .91-.8Zm-20.36 8.35c-.33-.33-.6-.43-.6-.23 0 .43-2.79-1-3.18-1.59-.15-.24.31-.44 1-.44 1 0 1.12.13.61.48s-.38.42.41.24 1-.05.78.54-.09.64.23.45.8 0 1.14.41c.73.88.44 1-.4.14Zm82.38-3.23a11.33 11.33 0 0 1 2.73 0c.74.12.13.2-1.37.2s-2.14-.12-1.38-.24Zm-3.47-.44a1 1 0 0 1 1 0c.4.16.28.28-.3.3s-.81-.1-.65-.26Zm-228.34-.85c.57-.57 5.9-1.68 6.45-1.33s-.29.6-4.71 1.28c-1.09.17-1.87.19-1.74 0Zm67.47-.34a.5.5 0 0 1 1 0 .5.5 0 0 1-1 0Zm160.49-.73c-.27-.08-3.79-.77-7.81-1.54-7.22-1.37-9.09-2.14-5.46-2.25 2.9-.09 3.76.09 4.37.92.31.43.79.64 1.06.46a4 4 0 0 1 2.19.22c.92.28 3.25.93 5.16 1.43s2.91.91 2.23.91a7.86 7.86 0 0 1-1.74-.15Zm-218-1.3a11.63 11.63 0 0 1 4.64-.75c2.81.32 3.66-.13 2.13-1.14-1.23-.81-1.23-.81 1.33-1 1.41-.1 4.4-.19 6.66-.21a23.41 23.41 0 0 0 4.09-.2 4.65 4.65 0 0 0-1.54-.71c-1.17-.41-1.37-.65-.87-1s.4-.41-.35-.25-1-.05-1.11-1c-.06-.69.11-1.06.41-.88s.4.46.29.65.68.4 1.76.47 1.85-.06 1.7-.32.07-.59.49-.75c1.18-.46 1.54-.35.71.19-.56.36-.26.49 1.15.49a4.56 4.56 0 0 1 2.64.75 2.47 2.47 0 0 0 1.57.71c.68 0 .65-.12-.15-.5s-.86-.45.24-.28a12 12 0 0 1 2.24.6c.87.36.9.32.24-.33a13.05 13.05 0 0 0-3.22-1.7c-2.25-.88-2.35-1-1.11-1.14.76-.08 1.24-.37 1.08-.63s.8-.34 2.55-.13c2.09.24 2.75.18 2.48-.24-.38-.63 1.17-.77 3-.28.75.2 1 .57.81 1.24-.28 1.09.75 1.31 1.17.23.22-.58.51-.61 1.54-.14l1.28.59-1.19.36c-.77.24-.48.28.79.13a6 6 0 0 1 3.4.51c1.61.85 3.52 1 3 .26a1.75 1.75 0 0 0-1.35-.5 35 35 0 0 1-7.81-3.31c-.13-.11.09-.43.49-.68.64-.42.64-.53 0-.94-1.23-.8-.4-.93 3-.46 1.93.26 3.45.76 3.8 1.22a5 5 0 0 0 3 1 23.87 23.87 0 0 1 4.25.78c3 .91 2.58.26-1-1.61-1.81-1-3.38-2.07-3.49-2.47-.15-.58-.1-.59.27-.06s.53.43.74-.37c.34-1.32 1.92-1.35 3.09-.05.92 1 3.23 1.38 3.23.5 0-.7.83-.62 2.74.29a5.6 5.6 0 0 0 2.55.62c.51-.1-.33-.59-1.95-1.14-1.87-.63-2.84-1.23-2.81-1.73s.12-.56.29-.15a1 1 0 0 0 .73.62c.27 0 .35-.22.18-.49s-.07-.5.22-.5a.5.5 0 0 1 .53.45c0 .52.93.45 1.87-.13a3.61 3.61 0 0 1 2.23.1 28.73 28.73 0 0 0 4 .77 8.54 8.54 0 0 1 3.08.84c.69.57 1.87.16 1.45-.51a3.68 3.68 0 0 0-1.74-.7c-.82-.17-1.5-.55-1.5-.86s.46-.45 1.17-.27a2.08 2.08 0 0 0 1.49 0c.84-.85 3-.54 4.06.56.74.81 1.29 1 1.77.72.8-.51 2.77-.2 5.85.93 1.78.65 2 .86 1.24 1.17s-.29.37 1.17.4a5.28 5.28 0 0 0 2.1-.18 10.58 10.58 0 0 0-2.35-1.37 10.19 10.19 0 0 1-2.53-1.52c-.09-.2-.64-.37-1.24-.38a3.63 3.63 0 0 1-1.81-.53c-.58-.4-.48-.44.43-.19s1.07.16.84-.2 0-.44.49-.23.76.11.49-.75c-.34-1.09 0-1.3 1.39-.77a.73.73 0 0 1 .54.86 1.77 1.77 0 0 0 .26 1.31c.38.6.83.63 2.47.19 1.11-.29 2-.39 2-.21s.83.65 1.86 1c2.75 1.06 2.92 1.1 2.11.47-.61-.47-.52-.52.49-.28.68.17 1.91.44 2.73.59l1.49.28-1.7-1.08a10.57 10.57 0 0 0-3.34-1.35c-1.55-.25-3.45-1.63-2.24-1.63a1.13 1.13 0 0 1 .83.49 2.58 2.58 0 0 0 1.8.5c1.37 0 1.43-.06.69-.8-.93-.93-.61-1.6.37-.78a2.19 2.19 0 0 0 1.7.28 1.31 1.31 0 0 1 1.36.27c.19.29.1.53-.19.53a.48.48 0 0 0-.52.43c0 .24 1.17.57 2.6.74l4.84.6c3.17.4 1.47-.58-2.58-1.5l-3.13-.7 3.73.26c2 .14 4.5.37 5.45.5a33.61 33.61 0 0 0 3.72.23c2.86 0 1.18.57-6 2.15a65.39 65.39 0 0 0-6.89 1.78 28 28 0 0 1-5.95 1.8 5.45 5.45 0 0 0-2 .67 19.06 19.06 0 0 1-4 1.13c-1.91.4-6.94 1.52-11.16 2.48-10.22 2.31-16.71 3.47-19 3.38a37.43 37.43 0 0 0-6 .67 161.55 161.55 0 0 1-20.2 2c-7.5.27-19.69 1.22-22.52 1.76-4.23.79-5.35.5-2.28-.59Zm76.15-13.46c.27-.2-.15-.37-.94-.38-1.22 0-1.33.1-.79.75.34.42.76.59.93.38a4.49 4.49 0 0 1 .8-.75Zm-21.09 10.44c0-.22.67-.41 1.49-.41s1.49.09 1.49.2a3.63 3.63 0 0 1-1.49.41c-.87.08-1.54-.01-1.54-.24Zm141.14.09c-1.36-.22-4.6-.75-7.19-1.19L251.52 267c-3.27-.55-7.07-1.25-8.43-1.55s-3.6-.72-5-.92c-3.7-.56-8.5-1.43-12.4-2.24-1.91-.4-6.49-1.2-10.17-1.78a41.65 41.65 0 0 1-7.3-1.53 1 1 0 0 0-1-.22 23.35 23.35 0 0 1-5.11-.7l-7.11-1.49a15.93 15.93 0 0 1-2.61-.67 13.3 13.3 0 0 1 3.19-.78l4.1-.74a35.64 35.64 0 0 0 5.09-1.38c1-.38 1.55-.36 2.07.08a1.34 1.34 0 0 0 1.71.06c1.21-.65 1.55-.67 1.55-.07 0 .25-.51.6-1.12.77-1.87.52-4.82 2-4.83 2.36 0 .21-.38.37-.83.37s-.71.12-.56.27c.4.41 4.37-.34 4.11-.77-.13-.21.1-.51.53-.68s.64-.09.48.17a.56.56 0 0 0 .2.75c.26.16.72-.16 1-.71a1.86 1.86 0 0 1 1.29-1c.45 0 .61-.23.4-.57s0-.49.69-.3a1.88 1.88 0 0 0 1.85-.63c.75-.83.75-.91-.1-1.11-.57-.12-.19-.35 1-.6s2-.21 2.25.16 1.06.46 1.93.35 1.59 0 1.59.26-.34.45-.75.45c-.88 0-2.51.89-3.91 2.11a3.61 3.61 0 0 1-1.39.87.46.46 0 0 0-.4.5c0 .75 1.29.59 2-.25a2.35 2.35 0 0 1 1.61-.75 2.53 2.53 0 0 0 1.71-.86c.49-.6.78-.69 1-.28s.54.47 1.45.05 1.08-.7.65-1.21-.32-.67.55-.67a1.32 1.32 0 0 0 1.35-1c.31-1.2 1.64-.78 1.64.52a.88.88 0 0 0 .87 1c.82 0 .82 0 0 .67-.48.37-.87.78-.87.93 0 .44 2.43.75 2.58.32a3.66 3.66 0 0 1 1.88-.7 9.13 9.13 0 0 0 2.87-1c.62-.41 1.3-.64 1.5-.51.81.5.22 2.79-.93 3.65a58.91 58.91 0 0 1-8.4 4.23c-.41.13.82.12 2.73 0 3.38-.24 5.21-1.14 5.21-2.57 0-.29.53-.52 1.19-.52a1.63 1.63 0 0 0 1.47-.74 1.14 1.14 0 0 1 .92-.74c.48 0 .47.11-.05.44-.89.56-.28 1.54 1 1.54s1.16-.2.49-1.45c-.41-.76-.3-.86.74-.66.67.13 1.09.43.94.68s-.05.43.22.43a4.88 4.88 0 0 1 1.35.34c.48.18.73.13.58-.13-.38-.61.58-1.88 2-2.62 1-.51 1.07-.48.79.29a3.77 3.77 0 0 1-1.07 1.5c-.56.45-.59.62-.12.62 1.27 0-.43 1.38-2.09 1.69a23.43 23.43 0 0 0-4.51 1.79 14.49 14.49 0 0 1-3.85 1.48c-.51 0-.92.21-.92.45s.78.25 1.86 0 2.75-.6 3.84-.77a4.52 4.52 0 0 0 2.35-.85c.25-.37.37-.35.37.08s.46.62 1 .62 1-.22 1-.48.63-.36 1.41-.21a6.41 6.41 0 0 0 3.59-.71 7.21 7.21 0 0 1 3.49-.74c.72.13 1.45 0 1.62-.26s.86-.35 1.8-.08 1.48.32 1.48.18.28-.06.62.15 0 .51-1.12.72a83.35 83.35 0 0 0-11.85 3.3l-2.36 1 2.48-.34c1.36-.2 3-.36 3.59-.37s1.12-.22 1.12-.47.78-.3 1.74-.15 1.73.07 1.73-.18a.5.5 0 0 1 .53-.47c.29 0 .39.21.24.46-.47.76.7 1.16 1.48.52a2.27 2.27 0 0 1 1.79-.32 3.5 3.5 0 0 0 2.73-1c1.29-1 1.82-1.15 2.35-.71a2.36 2.36 0 0 0 3-.66c.25-.32.3-.19.14.33s.07.94 1.07 1.16c2.23.49 2.41.63.91.67a22.84 22.84 0 0 0-4.87 1.28 24.6 24.6 0 0 1-4.52 1.27c-1 0-1 .06-.09.42a5.14 5.14 0 0 0 2.72-.11 38.93 38.93 0 0 1 7-.55c3.88 0 5.17-.2 5.08-.66a.47.47 0 0 1 .43-.62c.3 0 .41.35.24.78a4.57 4.57 0 0 0-.29 1.21c0 .23-.73.57-1.62.76a7 7 0 0 0-2.1.73 5.82 5.82 0 0 1-1.49.67c-.55.15 0 .19 1.26.08a19.44 19.44 0 0 0 3.22-.55c.94-.33.94-.32 0 .45s-1 .77.25.49a6.08 6.08 0 0 0 1.82-.75c.35-.27.7-.25.89.07.47.75 1.88.66 1.5-.1-.18-.33 0-.2.44.31.74.91 1.36.91 4.4 0 .62-.18.78 0 .58.49-.56 1.46-7.07 2.49-11.37 1.8ZM235.9 258c0-.29-.23-.39-.5-.22s-.5.41-.5.53.23.22.5.22a.52.52 0 0 0 .5-.53Zm-35.77-2.26a.28.28 0 0 0 0-.42c-.37-.37-2.41.32-2.41.82s1.85.14 2.41-.37Zm25.55-.43c-1.18-.89-1.52-.91-1.82-.13-.16.4.29.66 1.26.73 1.41.1 1.44.07.56-.6Zm13 13.19c-.15-.24 0-.56.42-.71.72-.28 1.28 0 1.28.74s-1.38.56-1.72 0Zm-146-4.32a1 1 0 0 1 1 0c.39.16.28.28-.3.3s-.82-.09-.65-.26Zm67.09-.67c-1-.41-1-.41 0-.79.54-.2 1.21-.5 1.48-.64s.22.07-.12.49-.47.91-.29 1.09c.4.4.2.37-1.07-.15Zm-53.7-1.31a1 1 0 0 1 1 0c.4.16.28.28-.3.3s-.81-.1-.65-.26Zm-2-.5a1 1 0 0 1 1 0c.39.16.28.28-.3.3s-.82-.09-.65-.26Zm151.49-.09c-.14-.22.17-.39.68-.39s.81.17.67.39a.78.78 0 0 1-1.35 0Zm-131.78-2.73a2 2 0 0 1 .67-1.29c.54-.44.67-.34.62.52-.07 1.23-1.29 1.96-1.29.8Zm105.8.34a1 1 0 0 1 .95 0c.4.16.28.28-.3.3s-.81-.09-.65-.26Zm-100.84-2.68a.5.5 0 1 1 .5.5.49.49 0 0 1-.5-.47Zm106.66-2.45c0-.83.88-1 1.17-.27.18.45 0 .74-.44.74s-.73-.21-.73-.47Zm-48.56-.77c-.06-.73-.18-.82-.48-.34s-.56-1.85-.76-5.36c-.19-3.3-.67-7.89-1.07-10.19a35.94 35.94 0 0 1-.62-4.57 2.79 2.79 0 0 0-.44-1.37 26.9 26.9 0 0 1-1.36-4.19 13.64 13.64 0 0 0-1.55-4c-.81-.9-1-1.69-.26-1.25a.56.56 0 0 0 .76-.19.92.92 0 0 1 1.09-.18c.55.21.72.08.56-.39a1.11 1.11 0 0 0-1.21-.57c-.69.1-1.21-.36-1.81-1.61-1-2.19-.66-2.27.65-.14 1.19 2 1.29 1.42.17-.94-.48-1-1.07-1.61-1.46-1.46-.67.26-2.85-2.78-2.85-4 0-.36-.23-.61-.5-.55s-.79-.46-1.15-1.15c-.62-1.19-.58-1.24 1-1.4s3.2-1.08 1.91-1.08a1.94 1.94 0 0 1-1.21-.63 15.91 15.91 0 0 1-2.82-4.27.75.75 0 0 0-.37-.64c-.6-.26-3.32-4.51-3.68-5.74s0-1.45 1-.53c.46.47.6.47.6 0a.53.53 0 0 1 .62-.53 18 18 0 0 0 2.11 0c1.26-.06 1.35-.15.61-.58a1.66 1.66 0 0 0-1.34-.22c-.47.29-4.48-4.74-4.48-5.63a.75.75 0 0 0-.37-.64 26.11 26.11 0 0 1-3.11-5.56c0-.17.73-.33 1.62-.36 1.55 0 1.56-.06.37-.49-1-.36-.92-.4.62-.23 1 .12 1.86 0 1.86-.25a.47.47 0 0 0-.47-.47c-.61 0-3.48-4.13-4.62-6.63-1.06-2.34-1.06-2.34 0-1.94.76.29.78.24.13-.46-.4-.44-.91-.76-1.15-.72s-.62-.44-.86-1.07c-.4-1-.32-1.13.72-1a4.94 4.94 0 0 0 2.36-.42c.65-.31 1.63-.73 2.17-.93.86-.33.88-.4.15-.57a.75.75 0 0 1-.57-1c.22-.7.18-.72-.25-.1s-.7.56-1.73-.41c-1.34-1.26-3.79-6.34-4-8.28a3.15 3.15 0 0 0-.54-1.6c-.23-.2-.23 0 0 .38s.24.58-.11.25a4.69 4.69 0 0 1-.85-1.74c-.26-1-.22-1.06.18-.5.29.41.52.54.53.28s.28-.21.6.12c.72.72 2.07.8 1.64.1s.92-.62 1.63.09c.32.33.74.43.92.25s-.18-.78-.81-1.33c-1.15-1-.9-1.22 1.11-1 .58.06.77-.16.58-.65s-.83-.7-1.59-.65c-1 .07-1.49-.27-2.17-1.65a31.85 31.85 0 0 1-2.61-6.72c-.31-1.49-.28-1.52 1.39-1.32 1.16.13 1.79.5 2 1.14.36 1.35 1 1.17 2-.58 1.1-1.94 1.08-2.25-.16-3.58-.77-.82-.94-.88-.69-.23.54 1.42-.55 1-1.27-.49a5.05 5.05 0 0 1-.58-1.74c.05-.2-.13-.37-.4-.37s-.5-.45-.5-1 .23-1 .5-1 .49-.44.49-1 .23-1 .5-1a.48.48 0 0 1 .5.45c0 .58 3.16.11 3.38-.5a1 1 0 0 1 .87-.45c.39 0 .71-.22.71-.49s-.67-.5-1.49-.5-1.49-.22-1.49-.5.45-.49 1-.49c1.1 0 1.22-.37.49-1.49-.38-.58-.58-.61-.89-.12-.88 1.35-4.33-3.87-4.57-6.92-.11-1.45.49-3.38 1.06-3.38a.43.43 0 0 1 .45.37 10.49 10.49 0 0 0 .33 1.62c.3 1.11.24 1.17-.5.56-.54-.45-.83-.49-.83-.13s.19.56.42.56.33.61.21 1.36c-.18 1.11-.11 1.23.36.62s.52-.47.27.64c-.37 1.73.66 3 1.6 2 .47-.51.43-.81-.17-1.41-1.27-1.27-1.2-5.74.1-5.74.66 0 .69.13.18.74s-.44.75-.06.75.56.5.57 1.11c0 .87.1 1 .46.41s.58-.6 1.22-.07.85.44 1.3-.74c.62-1.64.26-2.91-.73-2.53a1.26 1.26 0 0 1-1.29-.45c-.49-.58-.4-.71.45-.71.58 0 1.06-.23 1.06-.53s-.2-.39-.45-.24c-.68.42-2-.23-2-1a1.07 1.07 0 0 0-.62-.9c-.34-.13 0-.26.74-.29s1.37-.22 1.37-.43-.64-.36-1.41-.31-1.42-.16-1.45-.47a13.73 13.73 0 0 0-.38-1.8c-.22-.88-.18-1 .16-.56.79 1.12 1.62.2 1.08-1.2-.47-1.25-.2-1.46 1.18-.93a.76.76 0 0 1 .5 1c-.17.46 0 .6.65.45 1-.24 3.14 2.55 3.14 4 0 .78 1.31 1.58 1.8 1.09.15-.14 0-.73-.29-1.3s-.38-1 0-1 .52-.36.52-.8c0-.63-.15-.68-.7-.22-.95.78-1.59-.25-.78-1.23a1.19 1.19 0 0 0-1.29-1.88c-.39.15-.71 0-.71-.28s.45-.55 1-.55 1-.22 1-.49-.34-.5-.75-.5-.74-.22-.74-.5a.47.47 0 0 1 .46-.49c.25 0 0-1.24-.51-2.75a31.64 31.64 0 0 1-1.57-5.01c-.24-1.78-.4-2.09-.69-1.34s-.39.63-.42-.65c0-.88.07-1.61.19-1.61a6.67 6.67 0 0 1 1.47 1.24 4.16 4.16 0 0 0 2 1.24c.45 0 .64.29.48.72a2.41 2.41 0 0 0 .51 1.85c.69 1 .79 1 .79.2a2.26 2.26 0 0 1 .86-1.57c.7-.51.75-.76.25-1.27-.72-.73-.84-3.4-.15-3.4.26 0 .7.39 1 .86.65 1.12.68.08 0-1.57-.26-.7-.7-1.28-1-1.28s-.37.3-.24.67a1.32 1.32 0 0 1-.53 1.32c-.67.56-.77.55-.65 0 .09-.39-.13-.65-.49-.57-.65.13-2.08-2.69-2.08-4.13 0-.72.07-.72.77 0 .43.43.64 1 .47 1.28s0 .5.46.5c.89 0 .81-1.66-.12-2.59s-.73-3.2.2-3.56c1.09-.42 1.3-.37 1 .2-.17.27-.06.49.25.49s.41.23.24.5c-.39.63.06.63 1.24 0 .62-.33 1-.33 1.24 0s0 .49-.46.49c-1.09 0-1 .86.22 1.49s1.3 1.49.17 1.49c-.61 0-.46.33.58 1.3 1.23 1.13 1.3 1.35.59 1.75a1 1 0 0 0-.53 1.18c.15.4.5.59.76.43.78-.48 1.87 1.41 1.36 2.37-.62 1.15-.57 1.41.26 1.41s1.44-1.68.86-2.82a21.64 21.64 0 0 1-2.36-7.09 6.57 6.57 0 0 0-.78-2.79.54.54 0 0 1-.15-.73c.16-.26-.06-.61-.5-.77-.61-.24-.66-.46-.22-1s.38-.69.07-.69c-.7 0-1.05-4.62-.39-5.08.35-.24.31-.36-.1-.37a.59.59 0 0 1-.62-.54c0-.29.61-.46 1.37-.37a3.09 3.09 0 0 1 2.58 3.23 1.5 1.5 0 0 0 1 1.41c1.4.44 1.66 0 .87-1.34a6.1 6.1 0 0 1-.66-2.65c0-1.24.18-1.46 1.12-1.36 1.15.12 1.51-.56.59-1.13-.65-.41.35-1.72 1.33-1.72a.75.75 0 0 0 .68-.81c0-.56-.18-.66-.62-.33-1.24.93-3.29.68-3.91-.48-.54-1-.52-1 .25-.42s.79.59.5-.17a1 1 0 0 1 .26-1.23c.43-.27.45-.73.05-1.77s-.38-1.5 0-1.76a1.26 1.26 0 0 0 .23-1.4c-.23-.71-.12-1 .32-1 1.14 0 2 .86 1.75 1.64a.78.78 0 0 0 .43 1c.5.19.63.67.41 1.53-.35 1.39.33 1.66 1.29.51.33-.41.39-.75.12-.75-.8 0-1.26-4.06-.52-4.54.49-.3.44-.41-.17-.41a1.34 1.34 0 0 1-1.12-.79c-.46-1.21-.33-1.55.24-.58.28.47.76.87 1.07.87s.42-.16 0-.59-.45-.75 0-1.28.42-.8.06-1a3.53 3.53 0 0 1-.39-2.31c.17-2.71.13-2.59 1.1-3 .69-.26.74-.2.25.3a2 2 0 0 0-.62 1.14c0 .29.22.19.48-.22.4-.63.57-.6 1.22.25s.75.81.76.24a2.2 2.2 0 0 1 1.08-1.41c1-.6 1-.58.71.25-.25.66-.2.77.2.38.8-.8.09-2.37-.76-1.67s-.71-.54.21-3.85c.45-1.61.77-2 1.4-1.78s.74.1.45-.88c-.57-1.91-.58-2.19-.05-2.32.74-.18 2 3.82 1.36 4.43s-.24 2.39.42 2.39c.23 0 .3-.32.14-.72s.16-.83 1-1 1.29-.67 1.29-1.09.14-.64.31-.46 0 1.08-.5 2a10.08 10.08 0 0 0-.8 2.11 7.15 7.15 0 0 1-1.51 2c-.93 1-1.39 1.85-1.21 2.33.33.85 2.22 1.56 2.22.83a.47.47 0 0 0-.5-.45.53.53 0 0 1-.49-.55c0-.3.31-.43.69-.28a1.76 1.76 0 0 0 2.15-1.93c-.23-.87-.25-.86-.31.1-.06 1.22-1.54 2.06-1.54.88 0-.42.22-.62.5-.45s.49-.21.49-.85a2.43 2.43 0 0 1 1-1.8c1-.59 1-.55.69.46a15.66 15.66 0 0 0-.34 3.51c0 1.87-.22 2.54-.94 2.92a1.72 1.72 0 0 0-.93 1.21 10.25 10.25 0 0 1-1.11 2.84 15.18 15.18 0 0 1-2.77 4.12c-.56.62-.51.73.33.73 1.12 0 2.74-2 3.22-4.06.42-1.7 1.74-3 2.66-2.66a4.32 4.32 0 0 0 1.21.27c.27 0 .36.21.2.45s-.83.4-1.52.32-1.23.12-1.23.47.47.52 1.36.35c1.12-.21 1.27-.14.81.44-.77 1-1.83 5.74-1.54 6.88.17.66 0 .86-.44.68s-.69.13-.69 1c0 .71-.22 1.29-.49 1.29s-.5.5-.5 1.12a7 7 0 0 1-1 2.78c-.56.92-.9 1.78-.77 1.92s-.4.84-1.21 1.54c-1.58 1.39-1.85 2.06-.81 2.06s3.73-2.92 3.46-4c-.16-.62.21-1.38 1-2.14a12 12 0 0 0 2.25-3.46c.84-1.94 1.15-2.24 2.13-2s1 .14.41-.28c-.41-.29-.51-.54-.23-.54a1.5 1.5 0 0 1 1.37 1.79c-.14.37-.46.55-.7.4s-.44.88-.44 2.29c0 1.85-.18 2.5-.62 2.33-2.08-.78-2.86-.89-2.86-.39 0 .3.67.68 1.49.84s1.5.61 1.51 1.11.47-.17 1-1.35 1.11-2 1.25-1.92c.41.42-1.23 3.83-2 4.12a1.29 1.29 0 0 0-.74 1.11 11.64 11.64 0 0 1-.34 2.2c-.28 1.15-.2 1.32.51 1s.71-.41.19-.74-.47-.42-.07-.43a1.19 1.19 0 0 1 .9.49 1.8 1.8 0 0 0 1.39.47c1 0 1-.06-.15-.55s-1.2-.54-.25-.93c.54-.23.77-.43.49-.45s-.17-.25.22-.49c1-.63 2.75.32 2.09 1.12a2.24 2.24 0 0 0-.51 1.08 24.07 24.07 0 0 1-1.35 6.11c-.54 1.32-.88 1.59-1.68 1.33-.58-.18-1-.09-1 .22s.5.52 1.12.53c.9 0 1.12.29 1.16 1.49 0 .92-.19 1.48-.6 1.48s-.54.31-.37.75a2.73 2.73 0 0 1-.51 2 6.18 6.18 0 0 0-.8 1.48c0 .55 1.41.24 1.65-.36.17-.39.27-.32.3.19a2.33 2.33 0 0 0 .81 1.45c.68.57.73.49.46-.63-.23-.88-.09-1.41.41-1.7a2.48 2.48 0 0 0 .91-1.66c.12-.91.48-1.24 1.31-1.25a2.1 2.1 0 0 0 1.61-.75c.35-.56.46-.44.47.53a3.15 3.15 0 0 1-.43 1.74c-.7.75-1.24 2.88-.64 2.51 1.48-.91.92 1.91-.69 3.52a4.34 4.34 0 0 0-.85 1.25c-.13.37-.41.45-.67.2s-.17-.54.19-.67a1.21 1.21 0 0 0 .62-1.11 1.36 1.36 0 0 1 .77-1.2c.43-.16.63-.53.45-.82s-.79 0-1.52.77-1.08 1.51-.92 1.76.07.46-.2.46-.37.4-.25.88a2.59 2.59 0 0 1-.79 2c-1 1.13-1.33 2.12-.6 2.12.23 0 .71-.61 1.06-1.36.47-1 .71-1.16.9-.62.56 1.63 2.59 2 2.59.51 0-.66-.24-.88-.75-.69s-.74 0-.74-.26.51-.55 1.14-.55a2.76 2.76 0 0 0 1.68-.54c.38-.38.83-.39 1.52 0 .54.29 1.1.41 1.23.28.38-.38-.42 3.33-1 4.74l-.52 1.24v-1.26a2.9 2.9 0 0 1 .56-1.86c.44-.44.45-.6 0-.6s-.45-.33-.3-.74 0-.75-.7-.75c-.91 0-1.91 1.84-1.33 2.43.12.12.23 0 .23-.36a.57.57 0 0 1 .55-.58c.38 0 .49.83.33 2.47-.13 1.35-.05 2.34.19 2.2s.42-.13.42 0a19.2 19.2 0 0 1-2.2 4.22c-.34 0 0-1.2.69-2.48s.63-1.2-.22-.49-.88.68-.09-.85a7 7 0 0 0 .81-2.23c0-.35-.5.36-1.11 1.59a12 12 0 0 1-1.85 2.84c-.5.4-.55.61-.14.62s.48.35.32.75 0 .76.88.79c1 0 1 .12-.44.75-1.89.82-2.06 1.49-.5 2a3.23 3.23 0 0 1 1.44.85c.17.27.57 0 .88-.57.48-.89.43-1.06-.32-1.06s-.76-.09-.15-.47.85-.27 1.06.08a.94.94 0 0 0 1.1.24.79.79 0 0 1 1 .31c.18.44.35.42.7-.07.67-1 1.1.32.57 1.65-.39 1-.39 1-.43 0s-.12-.85-.67.25a7.19 7.19 0 0 1-2.21 2.23c-.86.55-1.58 1.19-1.59 1.43s.29.17.68-.16c.58-.48.66-.4.44.44-.15.56-.46 1-.71 1a.46.46 0 0 0-.43.48c0 .79 1.43 0 1.73-1 .43-1.38 2.73-3.7 2.73-2.76 0 .42-.17.77-.37.78a6.12 6.12 0 0 0-1.57 1.95 5.48 5.48 0 0 1-2.11 2.17 1.21 1.21 0 0 0-.91 1c0 .67.18.71 1 .26.57-.31 1.17-.43 1.33-.27s0 .29-.42.3-.14.34.54.73c1.58.9 2 .91 2 0a1.11 1.11 0 0 1 .68-1c.56-.22.54.13-.11 1.81a9.62 9.62 0 0 1-1.62 2.81 8.22 8.22 0 0 0-1.47 2.48 9 9 0 0 1-1.31 2.39c-.9.92-.8 1.74.36 3.07.89 1 1.12 1.09 2.22.5a3.38 3.38 0 0 0 1.48-1.45 3.09 3.09 0 0 1 1.18-1.37c.83-.52.94-.44.93.74 0 .73-.11 1.05-.23.71-.28-.8-1.12-.29-1.12.68 0 .41.21.61.48.45.67-.42-.87 3.84-1.61 4.47a6 6 0 0 0-1.16 1.48 1.88 1.88 0 0 1-1.37 1c-.45 0-1.14.62-1.53 1.37s-.5 1.36-.24 1.36c.73 0 .54 2.57-.26 3.46-.67.73-.56.78 1.11.58 1-.12 1.57-.32 1.23-.43-1.2-.42-.61-1.57 1.12-2.19 1-.34 1.73-1 1.74-1.4 0-.67.06-.68.49 0s.55.66 1 0 .48-.55.19.25c-1.31 3.58-1.68 4.36-1.7 3.59 0-.47-.24-.86-.51-.86-.72 0-.61 1.86.12 2.1s-1.78 4.14-3.14 4.87c-.72.37-.82.27-.62-.65s.2-1-.54.13a28 28 0 0 0-1.39 2.6c-.33.75-.78 1.36-1 1.36s-.39.24-.39.53a2.46 2.46 0 0 1-.75 1.28c-.85.85-.56 1.67.58 1.67.49 0 .67-.28.47-.77-.36-1 1.42-3 1.92-2.22a1.22 1.22 0 0 1-.26 1.28c-1 1.16.82.91 2-.28s1.23-1.41-.08-2.54c-.89-.78-.93-.93-.23-.93a1.27 1.27 0 0 1 1.11.72c.2.51.71.64 1.72.44 1.54-.31 1.89.37 1 1.82-.41.63-.48.64-.49 0s-.13-.59-.62-.1-.48.7 0 1 .28 1.12-.58 3.21c-1.55 3.82-2.54 5.25-4.33 6.23-2.67 1.46-10.3 10.21-9.53 10.93.17.17.79-.35 1.37-1.16 1.45-2 5.91-6.5 6.32-6.37.18.06.3-.27.27-.74-.05-.66.31-.83 1.67-.78 4.48.15 4.39 0 2.55 3.56-2.4 4.61-3.15 5.83-4 6.59s-.82 1.38.79 2.27c.85.47 1.2.43 1.86-.23s.81-.74.81.18a4.29 4.29 0 0 1-.71 2 21.46 21.46 0 0 0-1.7 3.6c-1.3 3.43-3.93 6.84-5.77 7.5-1.2.43-1.35.63-.75 1a.86.86 0 0 0 1.31-.28c.55-.73 3.87-1.17 4.46-.58.17.17-.5 1.41-1.49 2.76a11.53 11.53 0 0 0-1.8 3c0 1.12-2.68 5.24-4.44 6.82-1.6 1.45-1.66 1.6-.67 1.62a1.85 1.85 0 0 0 1.49-.66c.26-.45.73-.55 1.39-.28s.7.41.34.43-.5.27-.32.56 0 .9-.41 1.37a18.56 18.56 0 0 0-1.88 4.4c-.62 2-1.3 3.47-1.49 3.35s-.26.17-.14.65a2.53 2.53 0 0 1-.77 1.94c-.86.92-.89 1.11-.22 1.36s.69.66.43 1.66c-.19.75-.44 1.81-.54 2.35-2.48 12.54-3.21 16.38-3.59 18.85-.25 1.64-.49 2.53-.53 2Zm.44-9.18c.12-3.88 0-11.57-.19-11.39s-.34 4.14-.48 8.91c-.17 6.07-.11 7.86.2 5.95.25-1.46.46-3.03.47-3.44Zm2.48-13.14c0-.4-.22-.6-.5-.43a1.35 1.35 0 0 0-.49 1.05c0 .41.22.6.49.44a1.33 1.33 0 0 0 .5-1.06Zm-2.49-4.8c0-.49-.21-.76-.49-.59a2.3 2.3 0 0 0-.49 1.64c0 1 .12 1.17.49.59a3.64 3.64 0 0 0 .49-1.68Zm-2.68-2.2a2.47 2.47 0 0 1-.78-1.32c0-.3-.13-.41-.29-.25-.4.4.7 2.35 1.33 2.35.28 0 .16-.34-.26-.78Zm-2.27-1.2c0-.27-.35-.5-.78-.5s-.63.23-.46.5a1.05 1.05 0 0 0 .77.49.49.49 0 0 0 .47-.45Zm9.21-5.18c.78-.77.91-1.27.33-1.27-.21 0-.65.45-1 1-.71 1.19-.36 1.34.67.31Zm-12.93-2.72c-.17-.27-.41-.5-.53-.5s-.22.23-.22.5a.52.52 0 0 0 .53.5c.29 0 .39-.23.22-.5Zm1.91-4.22c-.31-.8-1.17-1-1.17-.28 0 .37.35.63 1.35 1 .06 0 0-.29-.18-.71Zm11.73-2.93a4.1 4.1 0 0 1 1.08-1.62c.59-.63.92-1.3.74-1.48s-.4-.24-.48-.12-.86 1.05-1.73 2.08c-1.81 2.12-2.08 3.15-.6 2.22.54-.34 1-.83 1-1.08Zm-5.46.21a.5.5 0 1 0-.49.49.49.49 0 0 0 .49-.45Zm-1.73-1.49c-.17-.28-.41-.5-.53-.5s-.22.22-.22.5a.51.51 0 0 0 .53.49c.29.04.39-.22.22-.49Zm12.65 0a.5.5 0 1 0-.5.49.5.5 0 0 0 .5-.49Zm-13.91-2c-1.17-.73-1.76-.54-.7.23.53.39 1.07.6 1.2.47s-.1-.45-.5-.71Zm11.43-1a.5.5 0 1 0-.5.5.51.51 0 0 0 .5-.48Zm-13.15-.5a1.07 1.07 0 0 0-.77-.49.48.48 0 0 0-.47.49c0 .28.35.5.77.5s.64-.2.47-.47Zm8-4.18c.78-.78.91-1.27.33-1.27-.2 0-.65.44-1 1-.74 1.17-.4 1.32.6.29Zm-3.75-.28c0-.27-.45-.5-1-.5s-1 .23-1 .5.45.5 1 .5.97-.21.97-.48Zm-9.75-1.24c-.33-.86-.3-.85-1.5-.39-.8.3-.76.38.33.69l1.35.39c.06 0 0-.28-.18-.69Zm16.09-.41c-.08-.36.53-1.24 1.35-1.94s1.31-1.46 1.09-1.67-1.33.57-2.48 1.76-1.7 2-1.22 1.85.87-.09.87.18.12.48.27.48.17-.28.09-.64Zm-9.1-1c-1-.76-1.51-.67-1 .18a1.44 1.44 0 0 0 1.13.53c.68 0 .65-.12-.15-.71Zm-2.7-2.71c0-.24-.56-.8-1.24-1.25-1.09-.71-1.24-.72-1.24-.06 0 .43.43.76 1 .76s1 .22 1 .49.11.5.25.5.2-.22.2-.46Zm15.56-.8c-.16-.4-.51-.6-.78-.43s-.24.47.17.73c.88.56.9.54.58-.32Zm-8.07-1.39c-.63-.64-1.28-.14-.81.62a.62.62 0 0 0 .84.23c.34-.22.33-.49 0-.85Zm-12.53-3.66c-1.86-2.49-2.18-2.73-2.31-1.77-.09.61-.37 1.11-.62 1.11s-.46.34-.46.75a.68.68 0 0 1-.58.74c-.32 0-.44.15-.26.33s.8-.1 1.37-.61c1-.89 1.08-.89 1.67-.08.35.47.83.86 1.08.86s.74.47 1.1 1 .76.92.9.78-.71-1.55-1.89-3.15Zm14.47 1.82a1.79 1.79 0 0 1 1.25-1c.4 0 .73-.16.72-.37 0-.57-.91-1.86-1.1-1.6l-1.54 2.1c-1 1.39-1.18 1.86-.62 1.86a1.84 1.84 0 0 0 1.26-1.01Zm-9.43-.79c0-.11-.34-.2-.74-.2s-.75.22-.75.48.33.35.75.2.74-.38.74-.48Zm20.59.3a1.16 1.16 0 0 0-.9-.49c-.36 0-.29.2.15.49.93.59 1.09.59.72-.02Zm-16.94-1.74c-.3-.78-1.17-1-1.17-.33a1.18 1.18 0 0 0 1.06 1.08c.22 0 .27-.34.11-.75Zm-2.16-2.23c-1-.63-1.63-.63-1.24 0a1.47 1.47 0 0 0 1.14.49c.69-.03.75-.09.07-.51Zm18.85-.28c0-.12-.22-.22-.5-.22a.51.51 0 0 0-.49.53c0 .29.22.39.49.22s.47-.43.47-.55Zm-32.25-.22a3 3 0 0 0-1.24-.48c-.27 0-.16.22.25.48a2.92 2.92 0 0 0 1.24.48c.24-.02.13-.23-.28-.5Zm3.57-.6c-.36-.35-.59-.38-.59-.07 0 .66.59 1.24.92.92.15-.17-.04-.55-.36-.87Zm-1.09-1.19c0-1-.65-.85-1.34.25-.51.81-.46.91.38.69.53-.16.93-.58.93-.96Zm8.92.43a4.08 4.08 0 0 0-.75-1.34c-.71-.94-.72-.94-.72 0a2.38 2.38 0 0 0 .32 1.34c.42.46 1.13.44 1.12-.02Zm16.19-1c-.17-.17-.29.12-.26.65s.14.7.3.3a1 1 0 0 0 0-.95Zm3.4-1.6c1.15-2.21.91-2.76-.48-1.11s-1.59 2.57-.76 2.57c.27 0 .83-.65 1.24-1.46Zm-11.88.07c-.15-.15-.55 0-.88.34-.49.49-.43.55.27.28.46-.19.74-.47.55-.62Zm-9.68-.1a.5.5 0 1 0-.5.5.49.49 0 0 0 .47-.5Zm15.73-5.95c.94-1.22 1.86-2.12 2-2s.4-.12.49-.56c.31-1.61-1.34-1.45-2.2.2a15.37 15.37 0 0 1-1.86 2.75l-2 2.24c-2.25 2.47-2.35 2.92-.3 1.3a26.85 26.85 0 0 0 3.88-3.95Zm-25.16 4.56a3.2 3.2 0 0 0-.64-1.27c-.59-.82-.63-.81-.63.11 0 .55.17.89.39.75s.39 0 .39.29.11.52.25.52.24-.18.24-.4Zm7.77-.51a3 3 0 0 1-.82-1.48.55.55 0 0 0-.5-.59.57.57 0 0 1-.49-.63c0-.6-5.1-5.81-5.69-5.81-.76 0-.12 1.4.89 1.94a6.78 6.78 0 0 1 2.08 2.19c1.06 1.8 4.23 5.21 4.9 5.26.25 0 .08-.38-.37-.88Zm17.05-.34c.86-1 .22-1.53-.74-.58a2.9 2.9 0 0 0-.76 1.05c0 .55.88.28 1.5-.47Zm-22.25-.84c-.35-.35-.58-.38-.58-.07 0 .66.58 1.24.92.92.14-.15-.04-.52-.37-.85Zm24.72.57c0-.28-.22-.38-.5-.21s-.49.4-.49.52.22.22.49.22a.51.51 0 0 0 .47-.53Zm6.7-1a1.19 1.19 0 0 0-.9-.49c-.36 0-.29.2.15.49.92.66 1.09.66.72.04Zm-18-.36a.9.9 0 0 1-.62-.71 2.91 2.91 0 0 0-1.89-1.91c-.26 0-.58.43-.72.94-.19.73 0 .87.77.66s.94-.08.73.5 0 .76 1 .75c.73 0 1-.11.7-.23Zm-3.12-3.48a10.86 10.86 0 0 0-2-2.35 11.69 11.69 0 0 1-2.26-2.87c-.2-.65-.59-.82-1.36-.6s-.88.17-.41-.17c1-.66.31-2.31-1.17-3-1.08-.49-1.23-.44-1.23.39a1.66 1.66 0 0 1-.5 1.26c-.91.56-.5 2 .71 2.54.85.4 1.36.41 1.74 0s.53-.34.53.1.21.49.47.33.73.2 1 .8c1.06 2 4.47 4.7 4.41 3.53Zm-8.66-6.08c-.17-.27-.07-.5.22-.5a.51.51 0 0 1 .52.5c0 .27-.09.5-.21.5s-.38-.19-.55-.46Zm26.05 3.47a.5.5 0 0 0-1 0 .5.5 0 0 0 1 0ZM188.68 169a15.74 15.74 0 0 0 1.49-1.48c.45-.53 1.74-2 2.85-3.23 2.35-2.62 3-4 1.93-4-.4 0-.73.17-.73.37a5.88 5.88 0 0 1-1.11 1.74c-.62.75-1.68 2.07-2.36 2.94s-1.85 2.23-2.6 3.05c-1.9 2-1.65 2.33.53.61Zm9.61-1.45c-.21-.8-.06-.92.85-.68s1 .17.5-.43-.72-.6-1.52.12a2.68 2.68 0 0 0-.92 1.5c0 .37-.22.66-.5.66a.48.48 0 0 0-.49.45c0 .25.52.2 1.16-.1.82-.38 1.1-.84.92-1.52Zm8.32-1.56c0-.27-.22-.15-.48.25a2.92 2.92 0 0 0-.48 1.24c0 .28.21.17.48-.24a3 3 0 0 0 .48-1.24Zm-21.81-.72c0-.65-3.91-4.71-4.23-4.39-.48.48.34 1.85 1.3 2.15 1.18.38 1.22 1.08.08 1.38-.49.13-.15.26.74.29s1.61.28 1.61.55.11.5.25.5.25-.22.25-.48Zm11.41-1c0-.62-.53-.62-1.49 0-.6.39-.53.48.37.49.61.03 1.12-.19 1.12-.46Zm-23.81-.71a4.78 4.78 0 0 1 .3-1c.22-.58.09-.71-.5-.48-.87.33-1.11 1.68-.3 1.68.27 0 .5-.09.5-.21Zm15.1-1.32a.55.55 0 0 0-.76-.17.54.54 0 0 0-.18.75.55.55 0 0 0 .94-.58Zm10.2 0c0-.27-.34-.49-.75-.49s-.74.22-.74.49.33.5.74.5.75-.15.75-.43Zm-33.55-.76c.17-.45.09-.64-.19-.47a1 1 0 0 0-.49.77c0 .78.35.62.68-.23Zm7.75 0a10.39 10.39 0 0 0-1.47-2.15c-1.23-1.56-1.7-1.82-3-1.63-1.67.25-1.58 1.09.13 1.12 1.11 0 2.82 1 2.82 1.69 0 .2-.67.21-1.49 0-1.36-.3-2.22.42-1 .83 1 .32 4 .4 4 .11Zm6.45-.2a.51.51 0 0 0-.53-.49c-.28 0-.38.22-.21.49s.4.5.52.5.22-.19.22-.46Zm20.6-.89c-.15-.15-.55 0-.89.34-.49.49-.43.54.28.27.49-.14.76-.42.66-.57Zm-23 .06a1 1 0 0 0-1 0c-.16.16.13.28.65.26s.69-.14.3-.3Zm2.86-1.76a6.21 6.21 0 0 1-1.46-2.12c0-.29-.45-.84-1-1.24a2.29 2.29 0 0 1-1-1.35.7.7 0 0 0-.74-.64c-.41 0-.75.23-.75.5a.53.53 0 0 0 .55.5c.3 0 .42.33.27.74s-.06.74.22.74a9.44 9.44 0 0 1 2.54 2.24 11.14 11.14 0 0 0 2.42 2.23c.22 0-.26-.72-1.06-1.6Zm19.37-.65c0-.13-.44-.23-1-.23s-1 .33-1 .76c0 .61.2.66 1 .23.57-.25 1.01-.59 1.01-.72Zm-23.31 0c-.69-.37-1.35-.58-1.48-.45-.33.33 1.08 1.16 2 1.15.41.07.21-.22-.51-.62Zm14.88-.55a7.93 7.93 0 0 1 1.91-2.69c1.06-1.1 1.72-2.14 1.48-2.29-.68-.42-5.37 4.76-5.37 5.92 0 .13.44.12 1 0s1-.56 1-.92Zm11.11-.77c.74-1.48.73-1.53-.21-1.28-.63.16-1 0-1-.44s.47-.7 1-.7c.74 0 1-.23.76-.76a2 2 0 0 1 .69-1.73 3.28 3.28 0 0 0 1-1.54c0-.31-.89.41-2 1.61a10.91 10.91 0 0 0-2 2.62 3.32 3.32 0 0 1-.86 1.41c-.82.91-.81.93.46.61s1.28-.2 1.05.71c-.41 1.53.15 1.26 1-.51Zm6.45-2.38c-.33-.34-2.18 1.83-2.18 2.54 0 .35.54 0 1.19-.85s1.12-1.5 1-1.61Zm-38.71 1.69c-.31-.78-1.18-1-1.18-.32a1.17 1.17 0 0 0 1.06 1.07c.22 0 .27-.34.12-.75Zm14.29.41a1 1 0 0 0-.94 0c-.17.16.12.28.65.26s.69-.14.29-.3Zm-9-.6c0-.24-.44-.55-1-.7-1-.25-1.31.16-.66.81.48.56 1.65.49 1.65-.03Zm9.38-2c-.66-.79-2.2-2.68-3.42-4.19a16.12 16.12 0 0 0-2.59-2.78c-.59 0 .23 1.63 1.45 2.95a4.21 4.21 0 0 1 1.14 2.11c0 .48.28.87.63.87a4.75 4.75 0 0 1 2 1.56c1.7 1.94 2.41 1.47.79-.52Zm13 .45a.51.51 0 0 0-.53-.5c-.28 0-.38.22-.22.5s.41.49.53.49.16-.13.16-.4Zm-24.06-1.49a1.09 1.09 0 0 0-.78-.5.49.49 0 0 0-.46.5c0 .27.34.49.77.49s.58-.13.41-.4Zm.79-1.8c-.76-1-2.06-2.6-2.88-3.51a12.55 12.55 0 0 1-1.82-2.49c-.23-.55-.35-.28-.37.79 0 .89.12 1.56.33 1.49.55-.19 2.74 2.14 2.37 2.51-.17.17-.53 0-.78-.42-.42-.64-.53-.63-1 0a2.84 2.84 0 0 0-.44 1.48c0 .53.12.49.28-.12.28-1.06 1.15-1.15 1.15-.12 0 .41.22.74.49.74s.5-.33.5-.74c0-1.22.7-.85 1.41.74.36.82 1 1.49 1.38 1.49s.34-.53-.66-1.86Zm21.74 1.41a.75.75 0 0 1-.34-1c.48-.78 1.27-.68 1.27.17a1.34 1.34 0 0 0 .49 1c.34.2.34-.13 0-1-.58-1.54-.62-2.24-.12-2.16 1.19.18 2.61-.14 2.61-.59s-.43-.4-1-.2c-1 .32-1 .3-.14-1 .48-.74 1-1.1 1.07-.79a1.06 1.06 0 0 0 1 .56c.63 0 .67-.13.21-.59-.9-.9-.75-2.88.22-2.88a2.06 2.06 0 0 0 1.42-.84c.5-.68.48-1-.09-1.55-.83-.83-1.36-.29-3.24 3.34a24.07 24.07 0 0 1-3 4.21c-1.95 2.14-2.27 3-1.25 3.44s1.65.37.93-.09Zm-30-2.26c-.33-.86-1.81-1.05-1.81-.24 0 .29.22.39.5.22s.64.05.8.49.42.68.56.55a1.26 1.26 0 0 0-.05-1Zm25.72-.58c-1-.73-1-.75 0-1.45.72-.54.78-.72.23-.72-1.73 0-2.12 1.66-.79 3.37.37.47.65.5 1 .09s.27-.75-.45-1.29Zm-7.44-.68c.17-.27 0-.5-.46-.5s-.78.23-.78.5a.49.49 0 0 0 .47.5 1.07 1.07 0 0 0 .81-.47Zm-19.1-.53c0-.28-.2-.4-.44-.24s-.8-.62-1.24-1.71-1-2-1.24-2c-.55 0-.12 1.89.5 2.28a1.64 1.64 0 0 1 .5 1.31.87.87 0 0 0 1 1c.52-.08 1-.31 1-.61Zm25.24-1.57c2-2 2-2.08 2.74-1.12s.75.93 1.06.12c.22-.57.87-.87 1.89-.87h1.55l-1.6-1.12c-1.84-1.28-2-1.86-.62-1.86.55 0 1-.22 1-.49a.51.51 0 0 0-.5-.5.5.5 0 0 1-.5-.49c0-.28.32-.5.7-.5a1.11 1.11 0 0 0 1-.62c.19-.48.26-.47.29 0s.26.51.53.34c.71-.43.62-2.23-.12-2.24-.42 0-.37-.16.12-.49s.56-.48.11-.48-.78.38-1 .86a5.48 5.48 0 0 1-1.48 1.77 2.92 2.92 0 0 0-1.14 1.5 19.45 19.45 0 0 1-3.21 4.06c-2.88 3.1-3.67 4.2-3 4.2a16.9 16.9 0 0 0 2.21-2.1Zm2.6-2.3c.21-.63.94-.77.94-.19 0 .21-.26.46-.56.56s-.44-.03-.34-.34Zm-27.34 1.42a.51.51 0 0 0-.53-.49c-.29 0-.38.22-.22.49s.41.5.53.5.26-.19.26-.47Zm20.59 0c-.17-.27-.41-.49-.53-.49s-.22.22-.22.49a.51.51 0 0 0 .53.5c.33.03.43-.19.26-.47ZM166 145.2c0-.41-.23-.75-.5-.75s-.5.34-.5.75.23.74.5.74.5-.33.5-.74Zm10.41 0c0-.42-.22-.63-.5-.46a.56.56 0 0 0-.22.77c.38.66.67.49.67-.34Zm5.72-.19a.85.85 0 0 0-1-.2c-1 .39-.9.67.3.67.49-.03.81-.25.65-.48Zm24.55-.74a2.17 2.17 0 0 1 1-1.39c1.09-.76 1.94-3.36 1.1-3.36-.3 0-.41.34-.25.75s.06.67-.4.49-.94.36-1.42 1.61c-.4 1-.8 2-.89 2.24s.07.37.37.37a.65.65 0 0 0 .54-.71Zm-31.46-1.14c-.5-.5-.91.26-.5.93.31.5.43.5.59 0a1 1 0 0 0-.09-.93Zm9.13.86a.52.52 0 0 0-.52-.5c-.29 0-.39.23-.22.5s.4.49.52.49.17-.25.17-.48Zm-.87-1.32a1 1 0 0 1-.62-.76 5.5 5.5 0 0 0-1.19-1.93 9.36 9.36 0 0 1-1.4-2c-.14-.42-.57-.27-1.38.48s-1 1.09-.39 1.09c1.07 0 3.39 1.94 2.38 2-.46 0-.27.31.5.74a5 5 0 0 0 2 .69q.75 0 .12-.27Zm7.52-5.33c1.44-1.62 1.66-2.39.55-2-.42.15-.75.48-.75.73a3.33 3.33 0 0 1-.87 1.38c-2.7 2.93-3.1 3.52-3.1 4.56s.25.77 1.44-1.06a35.68 35.68 0 0 1 2.73-3.64ZM172.4 142a.5.5 0 1 0-.5.5.5.5 0 0 0 .5-.5Zm.93-1.61c-1.29-2.56-1.69-3-1.23-1.36a24.11 24.11 0 0 1 .75 3.1c0 .21.38.37.8.37.62-.03.57-.39-.32-2.14Zm16.83.36c-.15-.15-.43.12-.62.61-.27.71-.21.77.28.28s.49-.77.34-.92Zm15.47.52a3.85 3.85 0 0 0-.81-1c-.66-.66-.9-.69-1.23-.16-.62 1-.53 1.3.43 1.52s1.61-.01 1.61-.39Zm-34-.75a1 1 0 0 0-.77-.5.48.48 0 0 0-.47.5c0 .27.35.49.78.49s.65-.25.48-.52Zm31.5-1.52a.51.51 0 0 0 .52-.5c0-.73-1-.6-1.82.26a1.55 1.55 0 0 0-.41 1.62c.3.75.39.72.78-.27.23-.61.66-1.11.93-1.11Zm-11.39.22c0-.43-.22-.64-.49-.47a1.06 1.06 0 0 0-.5.77.48.48 0 0 0 .5.47c.27.01.49-.35.49-.77ZM177.36 138a.5.5 0 1 0-.5.49.5.5 0 0 0 .5-.49Zm-6-.5a.5.5 0 1 0-.49.5.5.5 0 0 0 .53-.49Zm-7.26-.38c.33-.54-1.1-2.34-1.52-1.91-.21.2-.07.55.3.78a.72.72 0 0 1 .33 1c-.19.3-.12.55.16.55a1 1 0 0 0 .77-.41Zm15.2-.12c.52-.33.55-.48.11-.48s-.85-.59-1.12-1.3c-.34-.89-.74-1.2-1.27-1a4.31 4.31 0 0 1-1.23.3.49.49 0 0 0-.46.51c0 .29.53.38 1.24.19 1.19-.3 1.56.26 1.3 1.91-.08.51.53.45 1.43-.13Zm14.26-.78c-.47-.47-1.42.29-1.07.86.22.34.49.33.85 0s.43-.68.26-.85Zm-17 .29c.17-.28.09-.5-.19-.5a1.07 1.07 0 0 0-.8.5c-.17.27-.08.49.19.49a1.09 1.09 0 0 0 .85-.48Zm23.57-3.87c0-.59.14-.6.75-.09 1.13.94 1.29.39.26-1l-.89-1.19-.35 1.08c-.18.59-.55 1.07-.8 1.07s-.46.34-.46.74-.22.75-.5.75-.49.37-.49.83c0 .68.21.63 1.24-.34a3.87 3.87 0 0 0 1.29-1.84Zm-17.37 1.08c0-.55.31-.72 1-.55a1.66 1.66 0 0 0 1.58-.47c.52-.64.35-.69-1.35-.36a16.09 16.09 0 0 1-2.79.32c-.78 0-.78 0 0 .87 1.09 1.16 1.61 1.23 1.61.2Zm-13.8-.79c-.35-.36-.58-.39-.58-.07 0 .66.58 1.24.91.91.19-.13.05-.51-.29-.83Zm6.86.1a.51.51 0 0 0-.53-.49c-.28 0-.38.22-.22.49s.41.5.53.5.27-.21.27-.53Zm1.31-.24a11.52 11.52 0 0 1-1.62-2.73c-.89-2-1.37-2.39-1.92-1.5-.17.28.26 1.06 1 1.76a12.32 12.32 0 0 1 1.78 2.23c.29.54.75 1 1 1s.22-.34-.19-.75Zm13.82-.75a1.07 1.07 0 0 0-.77-.49.48.48 0 0 0-.47.49c0 .27.35.5.77.5s.69-.22.52-.49Zm-8-.49a1.34 1.34 0 0 0 1.41-1c.19-.77 0-1-.83-1s-1.43-.57-2-1.59-1-1.32-1.15-1a1.65 1.65 0 0 0 .38 1.32 1.18 1.18 0 0 1 .27 1.23c-.18.28.19.73.82 1 1.13.45 1.12.46-.21.49-.76 0-1.37.28-1.37.58s.36.41.79.25a6.39 6.39 0 0 1 1.92-.3Zm11.68-.56c0-.27-.22-.36-.5-.19a1.09 1.09 0 0 0-.49.81c0 .27.22.35.49.18a1.06 1.06 0 0 0 .59-.79Zm1.47-2.15a1.59 1.59 0 0 0 .37-1.37q-.24-.63-.63.36a3.47 3.47 0 0 1-.93 1.35c-.37.26-.36.37 0 .38a2 2 0 0 0 1.28-.71Zm9.52-.57c-.62-.43-1.28-.65-1.46-.46-.4.39.86 1.24 1.87 1.25.56.02.41-.27-.32-.78Zm-32.7-2.27c-.4-1-.46-.91-.48.82 0 1.52.1 1.79.47 1.16a2.31 2.31 0 0 0 .01-1.98Zm30.23 2.06a.5.5 0 0 0-.52-.49c-.29 0-.39.22-.22.49s.4.5.53.5.21-.2.21-.48Zm-37-1a1.05 1.05 0 0 0-.77-.5.49.49 0 0 0-.47.5c0 .27.35.5.78.5s.67-.2.5-.47Zm21.59.29a5 5 0 0 1 .29-1c.44-1.14-.66-1-1.29.2-.42.8-.37 1 .23 1 .46.04.81-.06.81-.17Zm-10.19-.76c.16-.26-.14-.7-.67-1-.82-.44-.92-.37-.7.47.32 1.1.93 1.33 1.41.56Zm7.67.1a7.62 7.62 0 0 0-.46-1.37c-.38-.87-.43-.83-.46.38 0 .75.19 1.36.46 1.36s.52-.13.5-.34Zm-4.93-1a2 2 0 0 0-.74-1.06c-.61-.5-.74-.48-.74.1a1.76 1.76 0 0 0 .32 1.06c.51.45 1.2.39 1.2-.12Zm17.86-.62c0-.28-.22-.36-.49-.19a1.07 1.07 0 0 0-.5.8c0 .28.22.36.5.19a1.09 1.09 0 0 0 .53-.82Zm1.86 0a1.89 1.89 0 0 1-.86-1.48c0-.53-.23-1-.5-1a.5.5 0 0 0-.5.5c0 .94 1.23 2.47 2 2.46.61.03.59-.08-.1-.48Zm-28.15-.13c0-.62-1.65-3.31-2-3.31-.7 0-.55.8.28 1.49a2.08 2.08 0 0 1 .75 1.31c0 .37.22.68.49.68s.52-.11.52-.17Zm19.47-2.61a9 9 0 0 0 1.33-3.08c0-.63-.14-.57-.46.24a10.76 10.76 0 0 1-1.58 2.39c-1.4 1.6-2.11 3.29-1.25 2.93a8.85 8.85 0 0 0 2-2.48Zm-13.46 0c-.43-.74-.53-.76-.54-.12 0 .84.51 1.59.86 1.24.15-.16.01-.66-.28-1.16Zm-.92-1.82c-.14-.24-.45-.13-.68.25-.66 1-.52 1.52.25.81.42-.39.61-.87.48-1.1Zm6.32 1.16a.51.51 0 0 0-.53-.49c-.28 0-.38.22-.21.49s.4.5.52.5.26-.26.26-.54Zm17.81-1.52c.63 0 .65-.1.09-.32-.71-.28-2.05 1.11-2 2.06 0 .29.27 0 .57-.59a2 2 0 0 1 1.38-1.19Zm1.13 1.19a1 1 0 0 0-.95 0c-.16.16.13.28.65.25s.74-.13.34-.29Zm-17.57-2.07a4.64 4.64 0 0 0-1.48-3.09 16.72 16.72 0 0 1-2.37-3.54c-.46-1-1-1.73-1.15-1.55-.51.5.21 2.71 1.22 3.71a3 3 0 0 1 .92 1.55c0 .34.5.84 1.12 1.12a2 2 0 0 1 1.16 1.48c.12 2.2.52 2.43.58.32Zm-2.61-1.07c-.17-.27-.41-.49-.53-.49s-.21.22-.21.49a.51.51 0 0 0 .52.5c.33-.04.43-.27.26-.54Zm12.15-.49a.49.49 0 0 0-.49-.5.5.5 0 1 0 0 1 .49.49 0 0 0 .53-.54Zm3.16-.18c.71-.37 1.18-.78 1-.93-.34-.34-2.69.73-2.7 1.23s.26.44 1.74-.34Zm9.74.18a.5.5 0 1 0-.49.49.49.49 0 0 0 .53-.53Zm-31.75-.47a1.07 1.07 0 0 0-.49-.78c-.28-.16-.5 0-.5.47s.22.78.5.78a.48.48 0 0 0 .53-.51Zm-2.48-.53a.5.5 0 1 0-.49.5.49.49 0 0 0 .53-.54Zm-5.45-1.27c0-.12-.35-.22-.78-.22s-.63.23-.46.51c.32.42 1.28.2 1.28-.33Zm28.6-.69c0-1.27-.08-1.36-.6-.66a1.46 1.46 0 0 0 .56 2.11 11 11 0 0 0 0-1.45Zm-21.66.44c0-.79-1.48-2.12-1.88-1.69-.2.21-.07.47.27.59a1.05 1.05 0 0 1 .62.93c0 .39.22.7.49.7a.51.51 0 0 0 .54-.57Zm24.81-1.73c0-.12-.35-.22-.78-.22s-.63.23-.46.51c.32.41 1.28.19 1.28-.33Zm-9.7-1.08a4.82 4.82 0 0 0 1.26-2 2.48 2.48 0 0 1 .75-1.41 2.5 2.5 0 0 0 .74-1.28c0-.29-.56.11-1.24.91a5.25 5.25 0 0 0-1.24 2.08 4.1 4.1 0 0 1-1.32 1.86c-.71.67-1.06 1.22-.76 1.22a4.46 4.46 0 0 0 1.85-1.42Zm-23.54.12c0-.41-.23-.75-.5-.75s-.5.34-.5.75.23.74.5.74.59-.37.59-.78Zm12.65.24a1.07 1.07 0 0 0-.78-.49.48.48 0 0 0-.46.49c0 .28.34.5.77.5s.68-.24.51-.54Zm14.69-1.54c1.17-1.17 1.19-1.93.06-1.93s-2.11 1.61-1.74 3c.19.71.4.9.53.49a5.57 5.57 0 0 1 1.19-1.6Zm7.72-1c-.87-.5-.87-.54 0-1.19s.86-.71 0-1.55-.87-.77-.87-.1c0 .42-.51 1-1.12 1.31-.81.39-.89.55-.3.57a1.34 1.34 0 0 1 1.1.78 1.28 1.28 0 0 0 1.17.72c.91-.05.91-.08.05-.58Zm-24.61-1.2c0-.4-.08-.74-.19-.74s-.33.34-.48.74-.07.75.19.75.48-.33.48-.75Zm2.49-.24a.5.5 0 1 0-.5.49.5.5 0 0 0 .5-.51Zm-2-4.09c0-.21-.22-.38-.5-.38a.49.49 0 0 0-.49.5.49.49 0 0 1-.5.49c-.27 0-.49-.37-.49-.84 0-.62-.15-.7-.56-.29-.9.9.54 2.14 1.66 1.44.49-.32.89-.73.89-.94Zm7.94.36c-.35-.42-.75-.65-.89-.52s0 .6.39 1 .75.66.89.52-.03-.59-.38-1.03Zm2.73-.24c-.17-.28-.41-.5-.53-.5s-.22.22-.22.5a.51.51 0 0 0 .53.49c.3-.02.4-.28.23-.51Zm7.19-1.24c0-.41-.22-.75-.5-.75s-.49.34-.49.75.22.74.49.74.51-.35.51-.76Zm2.48.27c0-.58-1-1.21-1.32-.85s.27 1.32.85 1.32a.47.47 0 0 0 .48-.49Zm-13.89-.52a.5.5 0 1 0-1 0 .5.5 0 0 0 1 0Zm-7.44-1.77c0-.12-.22-.22-.5-.22a.51.51 0 0 0-.49.53c0 .29.22.39.49.22s.51-.42.51-.55Zm19.84 0c0-.4-.22-.74-.49-.74s-.5.34-.5.74.22.75.5.75.5-.28.5-.74Zm-25.29-2.22c-.62-1.48-1-1.79-1.65-1.18-.32.32 1.74 3.15 2.08 2.86a3.66 3.66 0 0 0-.42-1.67Zm4.46.73a.75.75 0 0 0-.75-.74c-1.29 0-.87 1 .62 1.45.08.03.14-.28.14-.7Zm6-.8c0-.27-.22-.36-.5-.19a1.09 1.09 0 0 0-.49.81c0 .27.22.35.49.18a1.07 1.07 0 0 0 .46-.79Zm16.62-.19c0-1.26-1.06-1.49-1.38-.3a1.86 1.86 0 0 0 0 1.28c.53.54 1.34-.03 1.34-.97Zm-18.11-.25a.51.51 0 0 0-.52-.49c-.29 0-.39.22-.22.49s.4.5.52.5.18-.21.18-.49Zm10.85-3.22a8.25 8.25 0 0 0 1.73-2.85c.19-.76.68-1.37 1.11-1.37s.69-.26.58-.58c-.35-1.06-1.28-1-1.63.11-.63 2-3.19 5.59-4.52 6.33-.5.28-.67.71-.42 1.1s.55.41 1.07-.31c.38-.52 1.31-1.61 2.08-2.43Zm-4.4-2.12A4.8 4.8 0 0 0 185 92c-.6-.86-.95-1-1.44-.59s-.76.39-1 0c-.48-.77-1.25-.63-1.25.22a.86.86 0 0 0 .95.74c1.4 0 2.55.54 2.24 1-.15.24.07.44.5.44s.77-.17.77-.38Zm9.38-.43c.12-.1-.49-.18-1.36-.18s-1.57.23-1.57.51c-.04.45 2.3.19 2.89-.36Zm-11.74-3.36c.07-.25-.15-.55-.49-.66s-.62.16-.62.62c-.04.85.85.88 1.07.05Zm11.29-1.28c0-.55-.1-1-.23-1s-.47.45-.76 1c-.43.79-.38 1 .23 1 .39 0 .72-.39.72-.99Zm-6 0c.4-.26.56-.63.36-.83s-.74 0-1.21.46c-.91.98-.37 1.16.84.4Zm-7.1-.8c.41-1.05-.74-4.79-1.34-4.42s-.58 3.21.15 3.22c.44 0 .43.12-.07.44a.75.75 0 0 0-.34 1c.48.8 1.25.69 1.59-.21Zm4.14-1.21a.52.52 0 0 0-.53-.5c-.28 0-.38.23-.21.5s.4.5.52.5.21-.2.21-.47Zm10.91-.26c-.92-1.09.76-2.88 2.34-2.48.62.15 1.14.08 1.14-.17 0-.64-2.63-1.06-3.27-.52a.76.76 0 0 1-.95.08c-.22-.2-.27 0-.09.5s.06.87-.42.87-.73.44-.73 1 .24.89.74.69.75 0 .75.27a.55.55 0 0 0 .55.55c.39 0 .37-.24-.07-.76Zm-11.23-1.48c.16-.4.1-.74-.14-.74s-.56.34-.71.74-.09.75.14.75.54-.3.7-.72Zm6.07-2.1c-.39-.39-2.28 1.12-2.28 1.82 0 .49.38.35 1.24-.45.67-.61 1.14-1.23 1.03-1.34Zm-11.61-1a1 1 0 0 0-1 0c-.16.16.13.28.65.26s.73-.03.34-.19Zm6.45 0a1 1 0 0 0-.95 0c-.17.16.13.28.65.26s.68-.03.29-.19Zm-.09-2.89c0-.68-.19-1.24-.42-1.24s-.4 1.53-.12 2.36c.18.66.53-.05.53-1.05Zm-10.75-.45a.74.74 0 0 0-.39-1c-.45-.17-.58 0-.41.71.25 1.25.43 1.29.79.36Zm8.85-1.39c-.36-.36-.58-.39-.58-.07 0 .66.58 1.24.91.91.13-.07-.02-.45-.34-.77Zm11 .27a1.88 1.88 0 0 0-1.24 0c-.34.14-.06.25.62.25s.9-.04.56-.18Zm-8.83-2.27a23.27 23.27 0 0 0-1-2.66 5 5 0 0 1-.58-2c.22-.75-1.09-2.73-1.59-2.42-.88.54-.54 1.72.39 1.37.74-.28.78-.21.23.47s-.49.89-.1 1.13.38.53.05.92c-.88 1.05.13 3.88 1.23 3.45.38-.14.7 0 .7.29a.54.54 0 0 0 .53.54c.32 0 .39-.44.19-1.11Zm-2.2-1.49c0-.9.1-1 .49-.37.62 1 .62 1.48 0 1.48-.33.07-.56-.43-.55-1.04Zm4.87-.3c1.27-1.1 1.35-1.85.31-2.89s-.7-1.8.8-2.15c.81-.19 1-.4.58-.67a2.24 2.24 0 0 0-1.57-.08c-.78.25-1 .87-1.11 3.51-.09 1.76-.15 3.2-.12 3.2s.47-.35 1.05-.85Zm8-.57c0-.54-.09-1-.19-1s-.31.45-.45 1-.06 1 .19 1 .41-.38.41-.93Zm-9.94-3.72c0-.27-.21-.16-.48.25a3.06 3.06 0 0 0-.48 1.24c0 .27.22.16.48-.25a2.92 2.92 0 0 0 .44-1.17Zm.43-5.06c-.47-.56-.46-.8 0-1.11.78-.48.53-3.6-.32-3.88s-.83 1.46-.08 2.2c.39.39.26.74-.46 1.24s-.86.94-.62 1.33a3.55 3.55 0 0 1 .19 2l-.21 1.34 1-1.18c.83-1 .92-1.33.44-1.9Zm5.68 1.49c-.23-.61-.38-.66-.54-.19s-.31.5-.58 0-.45-.37-.67.2 0 .78.91.78 1.13-.19.88-.84Zm-2.14-6.91a1.79 1.79 0 0 0-.7-1.15c-.58-.49-.8-.27-1.22 1.26a4.68 4.68 0 0 0-.21 2.33c.29.6 2.08-1.45 2.09-2.37Zm-4.46-2.08c0-.63-.19-1-.42-.87-.53.32-1 2.63-.65 3s1.06-.87 1.07-2.14Zm-2.17-.45c-.51-.51-.92.25-.51.92.32.51.44.51.6 0a1 1 0 0 0-.09-.92Zm9.46-.76c-.14-.34-.26-.06-.26.62s.12 1 .26.62a2 2 0 0 0-.04-1.2Zm-5.54-5.33c-.17-.27-.41-.5-.53-.5s-.21.23-.21.5a.51.51 0 0 0 .52.5c.25.07.35-.13.18-.43Zm1.24-2a.51.51 0 0 0-.53-.5c-.28 0-.38.22-.21.5s.4.49.52.49.18-.12.18-.4Zm4.2 203.28c1.91-1.8 4.77-4.43 6.37-5.84a36.58 36.58 0 0 0 8-10.2c1.4-2.58 2.25-3.28 2.25-1.86 0 1.23.94.84 1.29-.52.41-1.65.13-1.86-1.6-1.15a11.49 11.49 0 0 1-3.41.64c-1.5 0-2 .26-2.15 1s0 .8.37.58a.6.6 0 0 0 .26-.8c-.15-.26.29-.46 1-.46s1.27.24 1.27.55-.33.42-.74.26-.68-.11-.62.08c.14.45-.82 1.63-3.06 3.72-3.6 3.38-4.75 4.32-5.32 4.32a1 1 0 0 0-.81.62c-.16.48-.38.47-1-.06s-.86-.51-1.14.86c-.16.86-.5 1.55-.73 1.55-.62 0-1.93 2.75-1.94 4.07 0 .75-.14.93-.4.53s-.62 0-1.08 1.24a7.68 7.68 0 0 0-.6 2.22c.06.21-.12.38-.39.38-.88 0-.54-1.4 1.29-5.33 1.91-4.11 4.66-12.12 4.66-13.58a1.51 1.51 0 0 1 .46-1.16 10 10 0 0 0 .79-3.63c.18-1.83.4-3.79.5-4.33a29.38 29.38 0 0 0 .2-3.14c0-1.5.23-2.09.66-1.94a.94.94 0 0 1 .47 1 .63.63 0 0 0 .62.81c.42 0 .77.22.77.5s.34.49.77.49c.58 0 .69-.28.43-1.11a3.17 3.17 0 0 0-.74-1.37 13.48 13.48 0 0 1-.42-4.22c0-2.18-.16-5.36-.29-7.06-.14-1.88-.05-3.1.25-3.1s.51.39.53.86.14.62.29.25a1 1 0 0 1 .76-.62c.28 0 .38.34.22.75s0 .74.72.74.87.22.7.5 0 .49.42.49c.85 0 .58-1.57-.56-3.35-1-1.6-.38-11.74.73-11.32a4.61 4.61 0 0 0 1.42.29.45.45 0 0 1 .43.66 1.19 1.19 0 0 0 .41 1.22c.55.45 1.12.48 2.79.14.1 0-.07-.35-.39-.74s-.45-.78 0-1.07.15-.59-.72-1.05c-1.2-.62-1.31-.94-1.31-3.64 0-1.63.2-3 .44-3s.91-.71 1.46-1.59c.8-1.27.89-1.75.42-2.32s-.06-.42 1 .21c.88.52 1.61 1.14 1.61 1.39s-.34.18-.75-.17-.74-.4-.74-.11.33.63.74.79c1 .38 2.31-.15 1.93-.76-.16-.26.1-.9.58-1.43.77-.86.78-1 .07-1a1.65 1.65 0 0 0-1.24.81c-.31.55-.57.63-.8.25s-.09-.56.22-.56.48-.67.29-2-.12-2 .33-1.82.6.81.59 1.53.16 1.12.42.87.36-2.44.27-4.89c-.14-3.56-.06-4.28.43-3.64a4 4 0 0 1 .62 2 4.71 4.71 0 0 0 .49 2c.81 1.28 1.23.06.68-2-.67-2.54-.07-2.53 1.08 0a5.29 5.29 0 0 0 1.22 2c.53 0 2-3.5 1.73-4-.16-.26.07-.83.51-1.26 1.09-1.09.5-1.55-.65-.52-1 .87-1.57.65-1.57-.56 0-.54.16-.58.65-.18s.65 0 .67-1.93c0-2.9.35-3.39 1.69-2.55 1.16.73 2.06.05 3.51-2.66.66-1.22.73-1.71.28-1.93s-.11-.2.49-.09a2.82 2.82 0 0 0 2-.51c.82-.66.77-.7-.59-.45-1.67.31-2.4 0-2.89-1.38-.25-.67-.07-1 .68-1.18s.92-.54.75-1c-.48-1.24.22-1.65 1.44-.85s1.19.7 1.34-.1.3-.59.78.46c.54 1.21.51 1.34-.37 1.34-.55 0-1 .2-1 .45s.42.34.94.2a1.23 1.23 0 0 1 1.27.3 1.14 1.14 0 0 0 .8.54c.66 0 .57-1.67-.13-2.38-.47-.47-.47-.73 0-1.2.92-.91.73-2-.3-1.76-.63.16-.91-.07-1-.8s0-.86.34-.36.66.57 1.94 0c1-.46 1.36-.86 1.05-1.23s.14-1 1.36-2q2.81-2.24 2.52-.95a.59.59 0 0 1-.74.35c-.82-.19-.83.73 0 1.06.37.15.27.27-.24.29s-.87.28-.87.57.5.46 1.11.37 1.12-.49 1.13-.9.42-.71 1.24-.63 1 0 .61-.17a1.12 1.12 0 0 1-.62-.94 1.1 1.1 0 0 1 .62-.92c.42-.17.22-.6-.62-1.34l-1.24-1.08 1.12-.75a2.57 2.57 0 0 0 1.11-1.92c0-.65.23-1.18.5-1.18s.5-.34.5-.75-.23-.74-.5-.74a.48.48 0 0 1-.5-.45c0-.25.48-.32 1.08-.17.85.23 1 .11.76-.54s-.07-.83.41-.83a.74.74 0 0 1 .73.75.74.74 0 0 0 .74.74.81.81 0 0 0 .75-.87c0-.74.08-.75.5-.12s.48.57.41-.25v-2.9c.07-1.77.18-1.9 1.32-1.69.83.16 1.24 0 1.24-.44s-.44-.67-1-.67a1.67 1.67 0 0 1-1.27-.5c-.18-.29.41-.49 1.46-.49 2.1 0 2.3-.71.4-1.41-1.27-.47-1.28-.5-.23-.54a4.61 4.61 0 0 0 3.77-4c.06-1.53.55-2 .91-1 .29.88 2.29.85 2.62 0 .17-.43.07-.85-.22-1a.79.79 0 0 1-.53-.69c0-.31.29-.28.74.1.89.73 1.36-.15 1.44-2.69a4.16 4.16 0 0 1 .92-2.46c.83-.83.87-.82.87.18s.12.93.68.41a1.43 1.43 0 0 0 .42-1.39c-.14-.41-.08-.58.14-.37.51.46 1.9-1.45 1.51-2.08-.18-.29.29-.3 1.21 0s1.5.24 1.5 0-.28-.46-.62-.47-.28-.22.21-.5a3.35 3.35 0 0 0 1.14-1.92c.39-1.75 2.07-1.82 1.87-.08-.06.59.09 1 .35.8a3.7 3.7 0 0 0 .43-2.22c0-2.1.41-3.18 1.09-2.76s3.74-1.08 5.14-2.75c1.11-1.31 1.47-1.5 1.83-.94.25.39.45.48.45.21a2.94 2.94 0 0 0-.52-1.24c-.4-.56-.59-.59-.78-.12-.41 1-1 .71-1.28-.6-.21-1.09-.13-1.16.76-.69s1.16.33 1.9-.64a4.26 4.26 0 0 0 .91-2 1.64 1.64 0 0 1 2.07-1.48c.45.07 1.29-.47 1.87-1.21 1.16-1.47 2.1-1.21 1.14.32a4.25 4.25 0 0 0-.61 1.84c0 1.06-2.17 3.6-3.06 3.61-.51 0-.51.09 0 .4s.39.83-.37 2.31a4.76 4.76 0 0 0-.74 2.16c.29.29 2.23-1.88 2.23-2.5 0-.25.22-.32.5-.15s.49-.36.49-1.3c0-1.48.06-1.54.67-.7s.71.83 1.33-.57a4.28 4.28 0 0 1 2-2c1.08-.4 1.32-.77 1.15-1.69-.12-.64-.09-.9.07-.57.3.63 1.71.3 2.35-.56.23-.31.32-.12.22.46s-.47.92-.83.84-.54.15-.34.68c.45 1.16-1.51 4.12-3 4.48-.66.17-1.21.56-1.22.87 0 .66-3.6 3.39-6.1 4.66a6 6 0 0 0-2.5 2.58c-1 2.15-.56 2.64.85 1a2.73 2.73 0 0 1 1.94-1.08 1.06 1.06 0 0 0 1.17-.61c.17-.43.54-.78.83-.78.94 0 4.33-1.67 4.33-2.14 0-.24-.56-.31-1.24-.14s-1.24.11-1.24-.13c0-.64 2.66-1.08 3-.5.17.27.58.13.95-.3.54-.66.52-.74-.12-.49a1 1 0 0 1-1.14-.32c-.28-.44 0-.52 1.12-.3a2.56 2.56 0 0 0 2.1-.3c.47-.46.47-.3 0 .76-.34.74-.84 1.21-1.12 1s-.38 0-.18.54.14.73-.25.5-.47-.08-.23.52.14.81-.46.58-2.4.84-2.09 1.41-3.54 4.77-6.49 7c-1.75 1.33-3.57 2.79-4.05 3.24a6.3 6.3 0 0 1-2 1.18 4.41 4.41 0 0 0-3.13 2.82c-.71 1.41-.69 1.48.2 1.24a3.2 3.2 0 0 1 2.1.48l1.15.73-1.11-1c-1.48-1.3-1.43-1.49.62-2.34 1-.41 1.73-.95 1.73-1.22 0-.9 2-.53 2 .38 0 .69.09.72.32.12a2.48 2.48 0 0 1 .83-1.1c.33-.23.19-.37-.37-.38-1.84 0-.43-1.25 1.69-1.5 1.37-.15 2-.39 1.58-.64-1.24-.8-.58-1.24 1.49-1 1.15.14 1.95.09 1.75-.12-.44-.47 2.44-3.23 3.38-3.23 1.23 0 .74 1.85-1.13 4.31-2.19 2.88-3.06 3.6-4.39 3.62a2.52 2.52 0 0 0-1.72 1c-.4.55-.89 1-1.1 1-.9 0-2.78 2.15-2.54 2.91.19.58.13.63-.19.19s-.65-.47-1.41.11-.87.73-.11.74a2.68 2.68 0 0 1 1.66 1.24c.67 1 1 1.12 1.71.64s.89-.6-.05-1.26-.85-1.9.11-1.3a4.74 4.74 0 0 0 2.19 0c1.51-.28 1.63-.42 1.11-1.25s-.81-.82-1.45-.48-1 .28-1.14 0 .22-.57.81-.72c1.41-.37 4.36 1 4.37 2.09 0 .7.07.71.49.06.59-.9.64-1.63.1-1.3a1.12 1.12 0 0 1-1-.15c-.47-.28-.36-.47.39-.66.59-.15 1 0 1 .27s.34.11.77-.42c.61-.77.59-.61-.08.77-1.6 3.31-5 7.45-6.18 7.45-.31 0-.45.32-.29.72.22.6.06.63-.91.18s-1.54-.28-3.82 1.38c-1.45 1-3.2 2.42-3.88 3a7.54 7.54 0 0 1-2.11 1.4c-1 .33-1.13 1-.24 1.39.33.15.05.16-.65 0a7.05 7.05 0 0 0-3.79 1 12.89 12.89 0 0 1-3.29 1.27c-1.37 0-2.32.59-2.32 1.45a2.37 2.37 0 0 1-.59 1.43c-.32.32-.46.71-.29.87.42.43 2.37-.9 2.37-1.63 0-.35.16-.59.37-.55a15 15 0 0 0 3.26-1.29 8.86 8.86 0 0 1 4.34-1.15c1.09.15 1.45 0 1.45-.58 0-1.11 1.94-2.26 2.64-1.56a1.56 1.56 0 0 0 2.52-.25c.43-1.11.39-1.2-.44-1.2-.41 0-.75-.22-.75-.5a.47.47 0 0 1 .44-.49c.24 0 .56-.45.7-1a1.32 1.32 0 0 1 1.15-1c.69 0 .75.14.29.6-.89.89-.72 2.24.33 2.51 1.3.34 2.57-.49 2.2-1.44-.23-.61-.07-.72.71-.47s1.07.07 1.25-.6a1.29 1.29 0 0 1 1.17-.93c2.23 0 1.68 2-1.58 5.7-1.44 1.64-2.12 2.07-3 1.87-1.16-.3-3 1.22-2.34 1.93.21.21-.24.28-1 .15s-1.37-.11-1.37.07-5.51 4-6.94 4.69c-.42.18-2.09 1.12-3.72 2.09-3.86 2.26-8.06 4.2-8.78 4.05-.31-.07-1.07.77-1.69 1.86a20.68 20.68 0 0 1-1.65 2.58 2.1 2.1 0 0 0-.54 1.12.51.51 0 0 1-.49.52.5.5 0 0 0-.5.49c0 .27.45.5 1 .5s1-.33 1-.75.46-.74 1.22-.74c1.11 0 1.2-.15.92-1.54-.33-1.65-.26-1.66 2.82-.53.63.23.64.15.06-.56a2.25 2.25 0 0 0-1.49-.84c-.47 0-.13-.42.84-1s1.87-.85 2.16-.67.32.43-.07.68-.36.46.24.68.91 0 1.1-.7.51-.92 1.07-.69a13.46 13.46 0 0 0 1.79.56c.73.17.64 0-.37-.66-1.61-1.06-1.8-1.89-.35-1.51.65.17 1 .05.83-.31-.21-.63 2.7-1.81 4.51-1.82.69 0 1-.22.82-.54a.86.86 0 0 0-1-.27c-.42.16-.57 0-.4-.45s.42-.68.6-.66c.85.11 1.46-.15 2.48-1.1l1.1-1-.31 1.28c-.22.83-.12 1.27.27 1.27s.71-.45.86-1c.27-1 1.17-1.1 1.51-.07.11.32-.15.59-.59.59s-.78.22-.78.49c0 1 1.87.48 2.15-.59.16-.61.44-.95.62-.76s1-.11 1.72-.65a5.26 5.26 0 0 1 2.21-1 3.11 3.11 0 0 0 1.82-1.24c1.14-1.44 2.66-1.7 2.2-.37-.26.73-.18.71.44-.09.86-1.11 1-.67.23.75a1.76 1.76 0 0 1-1.27.95c-.42 0-.65.18-.51.39.3.5-2.47 3.55-4.58 5a8.06 8.06 0 0 1-3.61 1.28 5.45 5.45 0 0 0-2.84.92 3.31 3.31 0 0 1-1.21.81 18.48 18.48 0 0 0-2.94 1.67c-5.48 3.5-15.26 7.74-17.87 7.75-.48 0-.87.21-.87.47 0 .67 3 2.06 3.62 1.67.32-.19.14-.46-.43-.68-.82-.3-.75-.39.53-.68a11.36 11.36 0 0 0 2.35-.81c1.17-.63 1.37-.59 1.37.28 0 1.05 1.67.93 3-.24.8-.67 1-1.1.6-1.36s-.39-.54.07-1 .85-.42 1.56.15c.87.7.85.72-.32.41s-1.19-.28-.39.55 1 .77 1.58.26a1.28 1.28 0 0 1 1.24-.32.64.64 0 0 0 .84-.28c.2-.32.16-.47-.09-.34s-.84-.16-1.32-.67-.81-.93-.74-1 .77-.14 1.56-.23c2.21-.25 2.91-.65 2.6-1.46s.73-1 1.57-.14c.33.32.6.41.6.19s.35-.12.77.24.91.47 1.12.15c.5-.75 4.72-2.67 5.85-2.67a2.55 2.55 0 0 0 1.56-.57c.43-.43.77-.41 1.38.09s.88.56 1.31 0 .44-.53.2.19a7 7 0 0 0-.29 1.12c0 .11-.23.21-.51.21a6 6 0 0 0-2 1.71c-2.14 2.44-4.79 4-6.7 3.83a10.46 10.46 0 0 0-4.49 1.34c-2.87 1.43-8.71 4.18-12.07 5.67-2.52 1.12-10.09 3.55-10.67 3.44-.27-.06-1.09 1-1.81 2.37s-1.11 2.47-.87 2.47a.47.47 0 0 0 .45-.49c0-.28.33-.5.74-.5a.74.74 0 0 0 .74-.75c0-.41.45-.74 1-.74a2.21 2.21 0 0 0 1.61-1c.65-1 2.1-1.33 2.61-.5.41.67 2.24.64 2.24 0 0-.3-.43-.39-1-.22-1.5.49-1.2-.68.33-1.27 1-.36 1.43-.34 1.69.09a1 1 0 0 0 1.28.21c.88-.36.88-.39-.07-.82-.7-.31-.39-.36 1-.15a4.62 4.62 0 0 1 2.35.87c.24.38.36.28.37-.29 0-1.55 1.72-2.7 4.41-3 3-.29 3.55-.65 2.29-1.57-.88-.64-.88-.69.17-1.25s1.08-.46 1.08.65c0 1.76.88 2.06 1.58.54.46-1 .46-1.39 0-1.69s-.21-.55.65-.94c1.31-.59 1.67-.21.69.77a1.12 1.12 0 0 0 1.18 1.87 11.55 11.55 0 0 0 3.87-1.85c.15-.2.92-.19 1.71 0 1.43.38 1.43.39.47 1.34a25.64 25.64 0 0 1-8.31 5.75c-1.41.49-2.29.9-1.95.91a.72.72 0 0 1 .62.76c0 1-.3.94-1.48-.24s-2.84-1-7.45 1.13a104.22 104.22 0 0 1-11.41 4.4 9.24 9.24 0 0 0-2.26.95 5.42 5.42 0 0 1-2.24.56c-2.88.24-3.79 1.88-1 1.88a5 5 0 0 0 2.46-.78c1-.7 1.18-.7 1.91 0a1.6 1.6 0 0 0 1.61.48 14.94 14.94 0 0 1 1.67-.58c.47-.14.72-.48.55-.75s.26-.37 1.2-.16c1.4.31 2.61-.36 1.91-1.06-.25-.26-3.06.56-5.39 1.56-.54.23-.87.16-.87-.19 0-.6.94-1 4.37-2.06 1.7-.51 2.23-.51 2.53 0a.93.93 0 0 0 .83.51 2.84 2.84 0 0 1 1.35.29 1.38 1.38 0 0 0 1.4-.1c.33-.33 0-.6-1-.8-2.17-.43-2.08-1.89.1-1.52 1 .18 1.38.12 1-.16s-.12-.68 1.14-1.28c2-.94 3.43-1.11 3-.35-.31.5 1.52.7 4.43.5 1.86-.12 3.88-1.43 3.55-2.31-.16-.41.1-.67.69-.67s.89.29.76 1 .07 1 .49 1c1.66 0 1.08 1.11-1.6 3.08-4.53 3.33-7.4 4.71-8.44 4.06s-3.86.1-14 3.26c-4.06 1.26-10.55 3-11.2 3-.11 0-.21.31-.21.69a2.09 2.09 0 0 1-.77 1.32 10.23 10.23 0 0 0-1.75 2.79c-.53 1.18-1.31 2.15-1.71 2.15a.75.75 0 0 0-.73.76c0 .67.17.67 1.5 0 .83-.44 1.38-1 1.23-1.24s.45-.67 1.34-.93c1.47-.42 1.51-.47.4-.55-.69-.05-1-.2-.62-.33a1.28 1.28 0 0 0 .62-1.11 2.09 2.09 0 0 1 2.08-2c.49 0 .89-.23.89-.53s.23-.38.5-.21.5.12.5-.11 1.28-.7 2.85-1.06a34.53 34.53 0 0 0 7.54-2.41c.93-.46 1.31-.47 1.58 0 .53.85-.11 1.6-.67.81-.38-.53-.43-.5-.23.13a1.48 1.48 0 0 1-.3 1.37c-.83.82 0 .67 1.67-.3 1.47-.86 1.95-2.11.83-2.15-.35 0 .27-.37 1.36-.79s2.43-.95 3-1.18c2.15-.91 3.1-1 3.37-.33.33.87-.14 1.56-1.39 2-.8.3-.87.24-.37-.26a2.07 2.07 0 0 0 .61-1.15c0-.29-.17-.26-.39.08a3.6 3.6 0 0 1-2.22.72c-1.5.11-1.84.31-1.81 1.14 0 .56.15.74.29.4.3-.74 2.65-.85 2.65-.13 0 .27-.37.5-.83.5s-.71.11-.58.25a12.24 12.24 0 0 0 3.52-.56c2.62-.65 3.21-1 3-1.61s-.05-.58.73.18.94 1.07.24 1.87c-1.43 1.62-8.76 5.82-10.28 5.88-.83 0-1.1.16-.62.28a1.49 1.49 0 0 1 .53 2.32c-.51.51-.63 1.81-.16 1.81.21 0 .55-.39.73-.87.31-.82.37-.81.85.06s.61.8 1 .24.43-.54.43 0 .24.52.51.35.5-.07.5.22.67.52 1.51.52 1.39-.21 1.21-.49 0-.37.42-.21c1 .37 1.62-.78.76-1.34-.57-.36-.56-.52.06-.93 1.14-.74 1.42-.6 1.12.57-.19.73-.1 1 .29.71a1.26 1.26 0 0 0 .58-.95c0-.33.46-.89 1-1.24.88-.55 1.18-.52 2 .23s.94.79.64 0c-.17-.46-.5-.84-.72-.84s-.26-.48-.11-1.07c.27-1 .3-1 1-.12s.71.87.44-.3-.25-1.2.45-.26a2.08 2.08 0 0 1 .48 1.61c-.13.35 0 .63.24.63.45 0 .67-.88.61-2.38 0-.37.09-.54.23-.4s.82-.16 1.5-.7a2.8 2.8 0 0 1 2.42-.67c.74.18 1.18.07 1.18-.29s-.22-.44-.49-.27-.37 0-.22-.42 1.26-.87 2.47-1 2.21-.51 2.21-.73.46-.53 1-.68.91-.48.75-.74.16-1 .7-1.6 1-.88 1-.58a2.09 2.09 0 0 1-.67 1.18c-.56.52-.46.73.55 1.26 1.54.79 1.44.83 1.4-.49a1.06 1.06 0 0 1 1.08-1.27c.81-.11 1.11-.49 1.11-1.36 0-1.44.77-1.61 1.13-.25.18.7.54.89 1.3.69.57-.15 1.05-.53 1.05-.84 0-.65 1.74-1.87 2.12-1.49.13.13-.51 1-1.43 1.88a6.25 6.25 0 0 0-1.69 2.2c0 1 1.36.62 1.64-.42.15-.61.53-.89 1-.73a1.33 1.33 0 0 0 1.35-.49 2.36 2.36 0 0 1 1.57-.77c.51 0 .93-.28.94-.62s.17-.37.49.13c.53.83.63.48.5-1.86 0-.89.13-1.62.4-1.62s.44.39.41.86.31.88 1.05.93 1.11-.22 1.11-.58a1.14 1.14 0 0 1 .41-.9c.22-.14.31.4.19 1.21-.21 1.44.56 2.06 1.05.84.21-.5.26-.5.3 0a.62.62 0 0 0 .56.62c.29 0 .4-.42.23-.93-.24-.77.11-1.05 1.94-1.59s2.29-.93 2.52-2 .6-1.36 2.25-1.52c1.89-.18 2.51.18 1.46.84s.17 1.17 1.49.49a6.69 6.69 0 0 1 2.34-.75 6.14 6.14 0 0 0 2.69-1.92c1.89-2 3.73-3.43 2.56-2a2.27 2.27 0 0 0-.32 1.8c.23.91.43 1 1.45.52.65-.29 1-.76.86-1s0-.39.48-.2a2.57 2.57 0 0 0 2-.64 4.08 4.08 0 0 1 2.14-.91 15.09 15.09 0 0 0 5.16-1.64 11.19 11.19 0 0 1 1.49-.71 11.46 11.46 0 0 0 1.49-.74 12.55 12.55 0 0 1 4.45-1.64c2.46-.47 1.39.22-3.71 2.41a32.08 32.08 0 0 0-5.7 3c-1.55 1.34-7.32 5.21-7.77 5.21a8.37 8.37 0 0 0-1.95 1.24 8.86 8.86 0 0 1-1.9 1.24 9 9 0 0 0-1.81.94c-2.16 1.31-1.13 1.67 1.27.44a11.32 11.32 0 0 1 3.23-1.12l2.73-.21c1.43-.11 1.45-.13.37-.65-1.31-.63-1.54-1.67-.25-1.11 1.66.7 3.76.89 3.39.29-.2-.31-.13-.56.13-.56a.89.89 0 0 1 .7.37c.11.21.16.15.11-.12s-.69-.44-1.41-.36a4.31 4.31 0 0 1-2.19-.35c-.62-.36.13-.48 2.76-.44 2 0 3.72.24 3.86.46.38.62 1.73-.09 1.38-.73-.18-.31 0-.27.37.09.59.56.47.84-.79 1.89-2.69 2.22-5.88 3.63-6.92 3-.68-.37-1.43-.34-2.71.14a21.54 21.54 0 0 1-5.28.72 17.76 17.76 0 0 0-4 .36c-.26.15-1.7.95-3.2 1.76-4.72 2.56-5.94 3.33-6 3.71 0 .82 1.35.29 2.9-1.12.9-.83 1.86-1.36 2.12-1.2.54.34 3.69-1 4.17-1.79a2.16 2.16 0 0 1 1.63-.44c.73 0 2.67-.08 4.31-.23 2.8-.26 2.76-.17-.25.51l-1.24.29 1.24.49c1.55.62 4.38.58 5.17-.08a2.66 2.66 0 0 1 2-.1l1.44.38-2.21 1.51c-3.25 2.23-5.16 3.11-7.27 3.39-1 .13-2 .46-2.05.74a.42.42 0 0 1-.65.21c-.62-.38 1-1.65 2.18-1.68a4.29 4.29 0 0 0 1.74-.48 9.73 9.73 0 0 1 2.1-.72 6.77 6.77 0 0 0 2.23-1c.86-.63.72-.65-1.3-.13a10.92 10.92 0 0 0-3 1.12c-1.76 1.38-14.52 2.27-15.7 1.1-.44-.44-1.25-.14-3.37 1.27-3.24 2.16-3.28 2.8-.08 1.34a7.61 7.61 0 0 1 4.81-.7c1.61.19 2.71.11 2.91-.22.4-.64 4.35 0 5.17.81.41.41.33.72-.32 1.2-.81.59-.78.64.35.62a3 3 0 0 0 1.62-.4 1.9 1.9 0 0 1 1.15-.36c.69 0 .7.06.05.49-.85.54-.47.64.71.19.61-.23.67-.47.25-1a1.19 1.19 0 0 1-.18-1.27c.25-.41.5-.25.79.53a1.18 1.18 0 0 0 1.6.91c1.69-.32 1.35.84-.7 2.35a8 8 0 0 1-2 1.23 9.68 9.68 0 0 0-2.21 1.24 7.11 7.11 0 0 1-3.52 1.24c-.93 0-1.57.21-1.42.46s-.14.33-.67.2a3.29 3.29 0 0 0-2 .34c-.8.42-1.25.44-1.64.05s-.3-.55.33-.55A21.67 21.67 0 0 0 297 201c1.93-.82 3.7-1.37 3.95-1.22s.44-.07.44-.5c0-1-.63-1-2.95.21-4.15 2.12-6.13 2.5-14.54 2.83-6.14.23-8.3.16-8.3-.26s.45-.57 1-.57a3.66 3.66 0 0 0 2.11-1.12c1.3-1.33 4.81-3 4.81-2.35a.49.49 0 0 1-.49.49.5.5 0 0 0-.5.5c0 .27.33.49.74.49a.71.71 0 0 0 .75-.66c0-1 2.43-2 3.26-1.29a1.47 1.47 0 0 0 1.31.31c.4-.17.32-.28-.23-.3s-.87-.23-.87-.47-.81-.56-1.79-.72c-2.12-.34-5.61 1.33-10.36 5a21.47 21.47 0 0 1-2.24 1.26 51.36 51.36 0 0 0-5.77 3.39c-6 3.94-7.92 5.17-8.36 5.35a48.77 48.77 0 0 0-6.7 4.38c0 .5 3.17-.38 3.57-1s.48-.47.78 0c.49.76 1.86 0 1.39-.79-.22-.34.11-.55.9-.55a2.1 2.1 0 0 0 1.54-.49c.17-.27 0-.5-.34-.5s-.51-.1 0-.44a2.49 2.49 0 0 1 1.69-.16c.78.2.95.08.72-.51s0-.86.67-1 .86-.09.74.1 1.28.3 3.1.25 2.65 0 1.83.13c-2.53.42-2.46 1.07.08.79a5.23 5.23 0 0 0 2.56-.69.39.39 0 0 1 .6-.16c.24.14.29.64.12 1.08-.24.63-.11.74.59.47a1.92 1.92 0 0 1 1.91.53c1 .85 1 .85.63-.11-.66-1.79-.56-2 1.12-2.14 1.09-.11 1.55 0 1.34.37s.09.53.69.53 1-.22 1-.5.46-.49 1-.49.89.21.73.46.33.55 1.09.67-.42.26-2.6.32c-4 .12-4.14.19-1.65.74a5.07 5.07 0 0 1 2.12 1.08c.53.53 1.87.83 4.17.91a15.58 15.58 0 0 1 4 .48c.32.2.58.11.58-.18s.35-.35 1.12.07a3.07 3.07 0 0 0 1.52.5c.22-.06.28.09.13.34a18 18 0 0 1-4.16 1.74 15.3 15.3 0 0 1-7.94 1c-2.91-.22-4.06-.15-4.06.26s-1.19.58-3.35.6c-2.69 0-3.11.13-2.15.51a8.1 8.1 0 0 0 3.43.15c1.82-.27 2.17-.2 2 .38s.31.74 2.56.8a17.64 17.64 0 0 1 4 .52c1.17.43 1.16.47-.78 1.45-4.2 2.12-11.15 3.41-11.15 2.06 0-.3-.31-.43-.69-.29a1.23 1.23 0 0 1-1.21-.26c-.37-.38-.45-.19-.27.65s.11 1-.17.58-.78-.49-2 0c-2.12.8-2 1.36.28 1.42 4.41.12 4.84.22 5.36 1.19s.34 1.11-.59 1.62c-1.36.75-6.77 2-9.73 2.32-1.94.18-2.15.09-2.16-.87a1.92 1.92 0 0 1 .39-1.36c.22-.15-3.34-.57-7.91-.93-9.62-.75-10.36-.81-14-1.31-2.25-.29-2.51-.42-1.49-.72a6.28 6.28 0 0 1 2.48-.11c1.22.24 1.22.23.25-.59s-.94-.83.74-.52a34.27 34.27 0 0 0 3.56.47c1.5.11 1.72 0 1.18-.51a2 2 0 0 0-1.82-.34c-1.13.3-1.15.27-.31-.58a5.79 5.79 0 0 1 3-1.07c1.52-.14 1.76-.08.86.19s-1 .52-.66.74a2.79 2.79 0 0 0 1.94-.16c1.18-.45 1.6-.32 3.18.95 1.21 1 2.05 1.32 2.5 1a1.74 1.74 0 0 1 1.54 0 8.78 8.78 0 0 0 3.12.46c1.68 0 2.27-.18 2.27-.74s-.58-.74-2.18-.74a9.49 9.49 0 0 1-3.59-.74c-1.26-.66-1.3-.75-.35-.75a6.6 6.6 0 0 0 1.87-.3c.57-.23.7-.09.48.49s.06.8 1.69.8c1.1 0 2.07-.2 2.16-.45.22-.59 3.33-.82 4.75-.35a2.38 2.38 0 0 0 2.12-.34c.89-.62-.17-.73-9.05-1a115.07 115.07 0 0 1-11.79-.75l-1.74-.44 1.49-.36c2.92-.71 3-.77 2.22-1.29s-.37-.65 1.07-.51a1.2 1.2 0 0 0 1.13-.78c.29-.76.4-.73 1.08.23.42.6 1 1.08 1.22 1.08.7 0 .57-1.37-.15-1.63-.34-.12-.46-.39-.26-.6s.65-.16 1 .14.86.35 1.42-.06c.93-.68 7.55-.48 7.12.21-.16.25.19.45.77.45s.91-.22.74-.5c-.28-.46 1.32-.58 10.92-.79 5.42-.12 6.2-.19 6.2-.57 0-.2-4.07-.31-9.05-.23s-11.4-.06-14.27-.31c-4.73-.42-5.33-.37-6.55.48a5.5 5.5 0 0 1-1.86.93.49.49 0 0 0-.52.44c0 .24-.39.57-.86.74a30.71 30.71 0 0 0-6.32 3.73 18.49 18.49 0 0 1-2.72 1.84 18.79 18.79 0 0 0-2.7 1.8 6.4 6.4 0 0 1-2 1.18 19.6 19.6 0 0 0-3.38 2 12.07 12.07 0 0 1-2.88 1.67.52.52 0 0 0-.5.54c0 .3-.28.45-.62.33a1.56 1.56 0 0 0-1.36.63A34.59 34.59 0 0 1 220 236a43.77 43.77 0 0 0-4 2.72 10.76 10.76 0 0 1-2 1.29 16.8 16.8 0 0 0-2.4 1.58 2.24 2.24 0 0 1-1.17.62 7.79 7.79 0 0 0-2.33 1.42 25.67 25.67 0 0 1-4 2.47 16.13 16.13 0 0 0-4.48 2.65 5.3 5.3 0 0 1-1.49.88 32.76 32.76 0 0 0-3.46 2c-4.13 2.71-4.23 2.61-.74-.69Zm12.65-10.52c0-.4-.23-.6-.5-.43a1.34 1.34 0 0 0-.5 1c0 .41.23.6.5.43a1.36 1.36 0 0 0 .48-1Zm-7.94-2.23c0-.24-.34-.44-.74-.44s-.75.45-.75 1.06c0 .88.12.95.75.43a2 2 0 0 0 .72-1.05Zm2.54-1.78c-.47-.19-.19-.68 1-1.67.9-.78 1.47-1.59 1.25-1.8s-.68 0-1 .48a1.27 1.27 0 0 1-1.5.61c-.58-.23-.8 0-.8.74 0 .57-.29 1.05-.62 1.07-.7 0 1.09.9 1.86.89.27 0 .19-.13-.19-.28Zm11.35-1.14a.51.51 0 0 0-.53-.49c-.28 0-.38.22-.21.49s.4.5.52.5.2-.22.2-.5Zm-14.53-7.56c-.1-.89-.19-.16-.19 1.61s.09 2.5.19 1.61a15.75 15.75 0 0 0-.02-3.22Zm10.19-1.79c.15-.6.88-1 2-1.26a4.78 4.78 0 0 0 2-.72c.27-.45-4.79.47-5.17.93a3.28 3.28 0 0 1-1.11.62c-1.37.51-1 1.35.56 1.35 1 0 1.51-.29 1.67-.92Zm7.56-1.56c.17-.27 0-.36-.47-.19-.85.33-1 .68-.3.68a1 1 0 0 0 .75-.49Zm55.41-.84a1 1 0 0 0-.95 0c-.16.17.13.29.65.26s.7-.14.3-.3Zm2.64-.15c.65-.28.74-.44.25-.46a3 3 0 0 0-1.49.46c-.93.58-.15.58 1.22 0Zm-55.32-1.77c0-.12-.22-.22-.49-.22a.51.51 0 0 0-.5.53c0 .29.22.38.5.22s.47-.41.47-.53Zm-8.43-1.3c0-.78 1.16-1.71 2.61-2.07.74-.19 1.36-.56 1.36-.83s.61-.5 1.36-.52a7.61 7.61 0 0 0 2.36-.47c.67-.3.27-.35-1.24-.16a16.84 16.84 0 0 0-4.25 1.22 11.75 11.75 0 0 1-2.53.93c-.41 0-1.65 2-1.65 2.71 0 .1.44.06 1-.08s1-.47 1-.73Zm25-.65c-.16-.41 0-.75.27-.75a.52.52 0 0 1 .55.48c0 .27.33.35.74.2 1.26-.49.82-1.18-.74-1.18-1.16 0-1.49.23-1.49 1 0 .54.21 1 .48 1s.35-.33.19-.74Zm24.95.13c-.75-.48-4.17-.49-4.46 0-.13.21.95.37 2.4.37s2.34-.12 1.99-.37Zm6.75.26a7 7 0 0 0-2.23 0c-.62.12-.12.22 1.11.22s1.69-.1 1.08-.23Zm-45.62-2.33c-.49-.66-1-1-1.07-.71s.24.79.75 1.17c1.25.98 1.3.86.28-.46Zm47.57.85a1 1 0 0 0-1 0c-.16.17.13.29.65.26s.7-.14.3-.3Zm5.74 0a5.46 5.46 0 0 0-2 0c-.62.12-.23.22.86.23s1.59-.09 1.12-.22Zm-42.68-.5a2.82 2.82 0 0 0-1.48 0c-.34.14.06.24.87.23s1.06-.1.57-.26Zm4.11-.65a.5.5 0 1 0-.5.5.5.5 0 0 0 .46-.5Zm-24.81-.77c0-.12-.22-.22-.49-.22a.51.51 0 0 0-.5.53c0 .28.22.38.5.21s.45-.4.45-.52Zm28.28.28c0-.28-.34-.5-.74-.5s-.75.22-.75.5.33.49.75.49.7-.22.7-.49Zm45.76.17c-.34-.14-.5-.46-.34-.71s-.17-.46-.71-.46c-1.43 0-1.77.89-.44 1.15 1.59.3 2.22.31 1.49 0Zm-53.7-.67a.49.49 0 0 0-.46-.5 1.09 1.09 0 0 0-.78.5c-.17.27 0 .5.47.5s.73-.23.73-.5Zm-3.47-1.27c0-.42-.23-.63-.51-.46a.58.58 0 0 0-.22.77c.39.69.73.57.73-.31Zm10.17.28c.17-.28-.24-.5-1-.5s-1.27.23-1.27.5.43.49 1 .49a1.67 1.67 0 0 0 1.23-.49ZM225 213.88c0-.25-.33-.47-.74-.47-.77 0-1 .59-.43 1.15.38.44 1.17-.05 1.17-.68Zm2 .06a1.21 1.21 0 0 0-.59-.83c-.41-.25-.29-.48.37-.74.89-.35.88-.37-.13-.41-1.28 0-1.76.56-1.34 1.66.29.84 1.69 1.1 1.69.32Zm-12.9-.93c0-1.47 1.81-3 4.55-3.79a19.64 19.64 0 0 1 3.38-.77 2.28 2.28 0 0 0 1.27-.4 11.31 11.31 0 0 1 2.58-1.08c1.16-.36 2.11-.85 2.11-1.08 0-.53-.3-.52-1.37 0a46.88 46.88 0 0 1-4.58 1.54l-5.46 1.59c-2.22.65-3 1.27-3 2.43a.85.85 0 0 1-.81.93c-.65 0-.69.14-.18.75.82 1.04 1.48.97 1.48-.13Zm32-1.08c-.17-.28-.41-.5-.53-.5s-.22.22-.22.5a.51.51 0 0 0 .53.49c.26 0 .36-.22.19-.49Zm-2-1a1.14 1.14 0 0 0-.9-.48c-.35 0-.29.2.16.48.93.62 1.1.62.72 0Zm-19.59-1.54c0-.28-.23-.36-.5-.19a1.1 1.1 0 0 0-.5.8c0 .27.23.36.5.19a1.1 1.1 0 0 0 .48-.8Zm1.73.55a.57.57 0 0 0-.78-.22c-.69.42-.56.72.32.72.4 0 .61-.23.44-.5Zm21.09-.22a1.33 1.33 0 0 0-.5-1c-.27-.17-.5.15-.5.72s.23 1 .5 1 .48-.33.48-.72Zm-18.09-.3c.18-.3.24-.62.13-.73-.34-.34-2.38.36-2.38.81 0 .62 1.86.55 2.25-.08Zm5.33-1.65c1-.42 1.76-.57 1.61-.33-.69 1.12 1.78-.25 2.52-1.39a6.59 6.59 0 0 1 1.52-1.72.69.69 0 0 0 .37-.9c-.19-.3-.53-.22-.88.22-.8 1-3.69 1.9-4.07 1.28-.17-.27-.74-.35-1.28-.18a1.89 1.89 0 0 1-1.57-.15 2.38 2.38 0 0 0-1.83 0c-1.21.41-1.21.41.13.24 1-.12 1.54.11 1.87.72.66 1.26.6 1.61-.27 1.28-.45-.18-.74 0-.74.44s-.45.73-1 .73c-1.11 0-1.35-.77-.38-1.16.45-.18.46-.26 0-.29a1.06 1.06 0 0 0-.89.45 2.36 2.36 0 0 1-1.65.55c-.9 0-.54.23 1.13.62 1.36.31 2.42.74 2.36.94-.26.9-.06 1 .51.26a7.49 7.49 0 0 1 2.5-1.56Zm6.31 2c0-.11-.34-.32-.75-.48s-.74-.07-.74.19.33.48.74.48.73-.1.73-.21Zm.49-2.29c0-.54-.22-1-.49-1s-.5.45-.5 1 .22 1 .5 1 .47-.48.47-1.02ZM256 208a.56.56 0 0 0-.77-.22c-.69.43-.56.72.31.72.4-.05.61-.27.46-.5Zm-7.68-.77c0-.12-.22-.22-.5-.22a.51.51 0 0 0-.49.53c0 .29.22.39.49.22s.48-.45.48-.58Zm-31-.22a1.05 1.05 0 0 0-.77-.49.48.48 0 0 0-.47.49c0 .28.35.5.78.5s.6-.27.43-.51Zm-6.69-1.48a.5.5 0 1 0-.5.49.5.5 0 0 0 .47-.54Zm7.93 0c0-.28-.33-.5-.74-.5s-.74.22-.74.5.33.49.74.49.71-.27.71-.54Zm39-1.17c.93.36.93-.36 0-1.59-.71-.93-.73-.93-.74.18 0 .64-.24 1-.51.84s-.5-.2-.5-.07a6.1 6.1 0 0 1-.35 1.15c-.32.85-.29.86.51.06a1.79 1.79 0 0 1 1.52-.62Zm-6.17-2c.24.15.45-.07.45-.49 0-1.3-1.42-.86-2.22.68-1.06 2.05-.94 2.39.28.84.56-.73 1.23-1.19 1.49-1Zm-31.59 1.28c.41-.66 5.13-2.76 8.48-3.76 3-.9 4.56-1.77 3.24-1.77a108.79 108.79 0 0 0-12 4.05 1.59 1.59 0 0 0-.87 1.18c0 .8.78 1 1.2.3Zm-15.29-1.42c-.16-.17-.28.13-.25.65s.14.7.29.3a1 1 0 0 0 0-1Zm43.54.12c-.15-.41-.48-.62-.73-.46-.52.32-.11 1.2.56 1.2.25 0 .33-.34.17-.74ZM213.57 201a.5.5 0 0 0-1 0 .5.5 0 1 0 1 0Zm45.64 0a.5.5 0 0 0-1 0 .5.5 0 1 0 1 0Zm-5-1.76c0-.4-.22-.72-.49-.72s-.5.46-.5 1 .22.89.5.72a1.32 1.32 0 0 0 .53-.99Zm-44.88-.75a1.56 1.56 0 0 0-.35-1.26c-.23-.14-.41.47-.41 1.35 0 1.69.63 1.62.76-.09Zm2.22 1a.51.51 0 0 0-.53-.49c-.29 0-.38.22-.22.49s.41.5.53.5.26-.19.26-.47Zm28.45-.25c-.19-.5-.06-.66.39-.49s1-.19 1.3-.74c.52-.82.5-1-.1-1a1.31 1.31 0 0 0-1 .48.86.86 0 0 1-1 .19c-.4-.15-.73 0-.73.28s-.33.54-.74.54c-1.31 0-.8.92.62 1.14a14.25 14.25 0 0 1 1.47.27c.06.05 0-.26-.18-.67Zm-28.45-1.76c0-.29-.23-.39-.5-.22s-.49.41-.49.53.22.21.49.21a.51.51 0 0 0 .54-.49Zm23.31 0c0-.26-.33-.35-.74-.19s-.74.37-.74.48.33.19.74.19.78-.14.78-.41Zm-28.49-.48a.56.56 0 0 0-.77-.22c-.69.42-.56.72.31.72.42.04.63-.18.46-.5Zm21.58-.87c0-.49-.16-.65-.38-.37a5.24 5.24 0 0 1-.82.87c-.24.21-.08.37.38.37a.84.84 0 0 0 .87-.83Zm9.68.38c.66-.28.74-.44.25-.45a3 3 0 0 0-1.49.45c-.9.62-.12.62 1.24.04Zm61.52-.61c.27-.2-.27-.37-1.21-.38-1.6 0-1.65 0-.75.7.53.39 1.08.55 1.22.38a4.22 4.22 0 0 1 .74-.66Zm7.28.77a1 1 0 0 0-.95 0c-.16.17.13.29.65.26s.7-.14.3-.29ZM226 195.8c0-.4-.08-.74-.19-.74s-.33.34-.48.74-.07.75.19.75.48-.34.48-.75Zm-22-1.1c-.17-.17-.28.13-.26.65s.14.7.3.3a1 1 0 0 0 0-1Zm58.13 0c.15-.46-.14-.61-.91-.5a1.76 1.76 0 0 0-1.36.83c-.14.45.15.6.92.49a1.76 1.76 0 0 0 1.3-.87Zm5.57.13c0-.42-.23-.63-.5-.47a1.09 1.09 0 0 0-.5.78.48.48 0 0 0 .5.46c.22-.05.45-.39.45-.82Zm31.13-.06a3.93 3.93 0 0 0-1.74 0c-.48.12-.09.22.87.22s1.29-.15.82-.27Zm-62.39-3.23c.51-.32.22-.38-.93-.18-.92.15-1.87.28-2.11.28a.46.46 0 0 0-.43.5c-.05.5 2.19.12 3.42-.65Zm-28.68-1c-.36-.36-.59-.39-.59-.08 0 .66.59 1.25.92.92.09-.19-.09-.57-.38-.89Zm62.42.6a.5.5 0 0 0-.47-.5 1.07 1.07 0 0 0-.77.5c-.17.27 0 .5.46.5s.73-.28.73-.55Zm-56.67-.9c.11-.32-.16-.59-.59-.59a.8.8 0 0 0-.79.79c-.05.82 1.02.67 1.33-.25Zm14.18.16a2.67 2.67 0 0 1 1.58-.75c.67 0 .72-.14.26-.7s-.44-.78 0-1.06a.78.78 0 0 1 1 .33c.37.58.53.57.93-.06.81-1.22.59-1.49-1.23-1.49-1.41 0-1.73.19-1.73 1 0 .57-.34 1-.78 1s-.63.23-.42.57-.06.87-.56 1.24-.67.67-.36.67a2.47 2.47 0 0 0 1.3-.74Zm73.74 0a3.16 3.16 0 0 0-1.19-.36c-.81-.16-1.12 0-1 .38s2.14.42 2.14-.07Zm-14.8-2.91c-.19-.75 0-.87.8-.6.56.18 1.16.09 1.34-.2s0-.51-.42-.51a1 1 0 0 1-.91-.87c-.11-.61-.42-.32-1.05 1-.49 1-1.07 1.86-1.31 1.86s-.31.43-.18.95c.24.88.31.89 1.1.12a2.1 2.1 0 0 0 .63-1.74Zm19.76 1.62c0-.25-.58-.45-1.3-.45-1.07 0-1.2.12-.71.71s1.96.49 1.96-.31Zm-26.37.11c.54-.34.52-.47-.08-.7-.77-.3-1.33 0-1.33.72 0 .53.56.52 1.41 0Zm22.49-.9a1 1 0 0 0-.94 0c-.17.16.12.28.65.26s.69-.14.29-.3Zm-89.09-2c-.5-.5-.91.26-.5.93.31.51.43.51.59 0a1 1 0 0 0-.09-.93Zm10.21-.24c-.35-.35-.58-.38-.58-.07 0 .66.58 1.25.92.92.08-.21-.06-.58-.39-.93Zm-12.12-.23a3.89 3.89 0 0 0-1.73 0c-.48.12-.09.22.87.22s1.29-.16.81-.28ZM249.8 181c0-1 .06-1 .68-.18.8 1 1.29 1.11 1.29.14 0-.43-.7-.85-1.76-1.05-1.87-.35-2.87.07-1.77.75.44.27.54.8.29 1.58-.33 1.05-.29 1.1.44.5a2.65 2.65 0 0 0 .83-1.74Zm5.94 1.07c0-.27-.35-.36-.78-.19a4.64 4.64 0 0 1-1 .3c-.11 0-.2.2-.2.45s.44.33 1 .19.98-.5.98-.77Zm-35.72.08c0-.29-.22-.39-.49-.22s-.5.41-.5.53.22.22.5.22a.51.51 0 0 0 .47-.55Zm78.39-.53c0-.27-.23-.35-.5-.18a1.09 1.09 0 0 0-.5.8c0 .27.23.36.5.19a1.1 1.1 0 0 0 .5-.83Zm-76.09-.18a2.62 2.62 0 0 0-1.08-.75c-.2 0-.07.34.27.75a2 2 0 0 0 1.09.74c.25-.02.13-.35-.28-.76Zm-4.78-.9c1.07-.22 1-.24-.21-.18-.82 0-1.54.41-1.65.83s-.06.55.21.17a3.22 3.22 0 0 1 1.65-.84Zm8.43.18c0-.78-.94-1.11-2.11-.72l-1.11.37 1.24.36c1.78.5 2.01.49 2.01-.03Zm71-1a.5.5 0 0 0-.47-.5 1.07 1.07 0 0 0-.77.5c-.17.27 0 .5.46.5s.73-.22.73-.54Zm-77.61-.76c-1.11-.83-1.77-.93-1.77-.27 0 .26.5.56 1.12.68l1.36.26c.09-.01-.24-.31-.76-.71Zm8.63-1.35a5.45 5.45 0 0 0-1.27-2c-.91-1-1.41-1.28-1.8-.89s-.36.76.09 1.31.48.76-.34.76-1-.27-.8-1.36c.18-1.31.16-1.32-.39-.27a2.79 2.79 0 0 1-1.36 1.31c-.54.13-.43.23.31.27s1.11.29 1.11.58.18.42.4.28a1.28 1.28 0 0 1 1.11.21c1.06.67 3 .57 2.94-.15Zm-9-.71a2.75 2.75 0 0 0-1.49 0c-.34.13.06.23.88.22s1-.12.52-.26Zm25.93-.44c0-.11-.35-.21-.78-.21s-.63.22-.46.5c.19.42 1.15.25 1.15-.33Zm11.74-.81c1.5-.54 2.25-1.39 1.23-1.39a10.62 10.62 0 0 0-3.55 1.78c-.09.3.48.21 2.23-.43Zm-35.06-1.39a.51.51 0 0 0-.53-.49c-.28 0-.38.22-.21.49s.4.5.52.5.13-.27.13-.54Zm6.45 0a.51.51 0 0 0-.53-.49c-.28 0-.38.22-.21.49s.4.5.52.5.17-.27.17-.54ZM239 172c1.46-2.3 2.17-2.66 3.21-1.64.32.33.59.39.58.13 0-.84-.71-1.57-1.83-1.92-.92-.29-1.21-.07-1.78 1.3a7.44 7.44 0 0 1-1.51 2.32c-.84.69-1.13 2-.45 2A7.66 7.66 0 0 0 239 172Zm12.73 1.74c0-.28-.34-.5-.74-.5s-.75.22-.75.5.34.49.75.49.78-.23.78-.5Zm3 0c0-.28-.34-.5-.75-.5s-.74.22-.74.5.33.49.74.49.77-.23.77-.5Zm5.31-.76c.68 0 2.4-1.2 1.78-1.21-1.2 0-3.12 1.16-3.12 1.92s.07.7.48 0c.28-.37.67-.69.88-.69Zm-37.47-.08a1 1 0 0 0-1 0c-.16.17.13.28.65.26s.7-.14.3-.3Zm2.53-2.22c-.61-.71-.61-.88 0-1.24s.52-.58-.48-1.33l-1.23-.91.25 2.89c.13 1.6.26 3 .28 3s.44-.27.94-.75c.8-.75.83-1 .22-1.69Zm6.38 1.62c-.47-.3-.5-.52-.12-.76.84-.52.65-1.27-.32-1.3-.73 0-.69-.15.25-.81 1.13-.79 1.43-2.63.8-5-.31-1.16-2.31-1.59-2.91-.62-.17.28.1.5.6.5.77 0 .88.32.72 2s-.42 2.1-1.87 2.7c-1.89.79-2.23 1.75-.64 1.75.71 0 1 .23.77.71-.28.72.5 1.11 2.4 1.21.74 0 .82-.06.32-.38Zm-8.75-1.54c-.17-.27-.41-.5-.53-.5s-.21.23-.21.5a.51.51 0 0 0 .52.5c.31-.01.41-.26.24-.51Zm41.42 0c0-.29-.22-.39-.49-.22s-.5.41-.5.53.23.22.5.22a.51.51 0 0 0 .51-.57ZM246.81 168c0-.42-.23-.63-.51-.46a.57.57 0 0 0-.21.77c.42.69.72.55.72-.31Zm-19.35-1.71c0-.81-.22-1.48-.49-1.48-.56 0-.69 2.12-.17 2.65s.66.37.66-1.18Zm-2.35.21a1.14 1.14 0 0 0 0-1c-.24-.6-.37-.61-.71-.07-.71 1.1-.18 1.95.71 1.06Zm18-1.69c.17-.28.08-.5-.19-.5a1.07 1.07 0 0 0-.8.5c-.17.27-.09.49.19.49a1.09 1.09 0 0 0 .78-.5Zm20.09-.5a.5.5 0 1 0-.49.5.5.5 0 0 0 .47-.51Zm-35.2-.74c0-.46-.29-.61-.79-.43s-.65.39-.12.73c.84.55.91.54.91-.3Zm37.6-.53c1.06-1 1.13-1.19.35-.9-1 .38-2.23 1.39-2.23 1.88s.51.27 1.84-1.02Zm-31.65-.2a1.05 1.05 0 0 0-.49-.77c-.28-.17-.5 0-.5.46s.22.78.5.78a.48.48 0 0 0 .45-.47Zm6-.52c0-.22-.32-.52-.71-.66a2 2 0 0 1-.93-1.42c-.15-.78-.23-.58-.28.63a5 5 0 0 0 .22 2.08c.39.37 1.7-.11 1.7-.63Zm-7.67-1.62a3.91 3.91 0 0 1 .9-2c.41-.25.4-.51 0-.94s-.66-.29-.91.73-.65 1.31-1.52 1.31c-1.37 0-1.59.81-.31 1.14.63.17.56.24-.28.29-1.49.07-.9 1 .64 1 .92 0 1.24-.35 1.52-1.62Zm17.09.93c0-.11-.34-.33-.74-.49s-.75-.06-.75.2.34.48.75.48.66-.08.66-.19Zm20.09-.8c.17-.28 0-.5-.46-.5s-.78.22-.78.5a.48.48 0 0 0 .47.49 1.05 1.05 0 0 0 .69-.49Zm-32.81-4.45c-.33-.34-1.16 1.08-1.14 2 0 .43.29.22.69-.5s.5-1.37.37-1.5Zm-10.35 1.47a1.71 1.71 0 0 0-1.3-.5c-.54 0-.85.23-.68.5a1.71 1.71 0 0 0 1.3.5c.46 0 .77-.23.6-.5Zm8.37-.25c-.16-.4-.37-.74-.48-.74s-.2.34-.2.74.22.75.48.75.28-.35.12-.75Zm-2.12-4.86c-.39-.39-.54-.25-.54.51a8.2 8.2 0 0 1-.28 2c-.16.5.08.27.54-.51.51-1.07.59-1.61.2-2Zm26.47 2.67c.33-.54-.72-1-1.59-.65-.34.12-.62.42-.62.66 0 .59 1.85.58 2.21 0Zm-20.81-1.28c0-.27-.51-.57-1.12-.65-.77-.11-1.11.09-1.11.65s.34.76 1.11.65c.53-.08 1.04-.38 1.04-.65Zm22.57.28a1.07 1.07 0 0 0-.49-.78c-.28-.16-.5.05-.5.47s.22.77.5.77a.47.47 0 0 0 .41-.46Zm-15.87-.53a.5.5 0 0 0-1 0 .5.5 0 0 0 1 0Zm7.16-2.31c.47.18.63-.2.55-1.32s-.32-1.52-.81-1.41c-.79.18-1.27.9-2.56 3.8l-.88 2 1.52-1.66c.84-.9 1.82-1.54 2.18-1.4Zm16 1.91c.12-.36-.11-.47-.59-.29s-.78.52-.78.79c-.04.65 1.08.24 1.3-.5Zm-32.88-1.09c.17-.27 0-.49-.46-.49s-.78.22-.78.49a.48.48 0 0 0 .47.5 1 1 0 0 0 .73-.5Zm35 0c.86-.46 1.38-2 .68-2s-2.2 1.12-2.2 1.76c-.03.85.21.91 1.49.23Zm-24.34-1.23c0-.4-.22-.74-.5-.74s-.49.34-.49.74.22.75.49.75.47-.34.47-.76Zm-2-1c0-.42-.22-.63-.5-.46a.56.56 0 0 0-.22.77c.41.66.7.53.7-.34Zm29.28-.24c.51-.93.47-1-.47-.37s-1.36 1.39-.55 1.39c.25-.03.71-.49 1-1.05Zm-18.77-.58c-.36-.35-.58-.38-.58-.07 0 .66.58 1.24.91.92.12-.19-.03-.55-.35-.88Zm9.34-5.37c0-.15-.9-.26-2-.25-2 0-2 0-1 .88.86.69 1.23.73 2 .25.55-.36.98-.76.98-.91Zm-19.85 0c0-.47-1.53-1.11-1.87-.77-.12.12-.1.57.06 1 .26.73 1.79.54 1.79-.2Zm.62-2.71c-.13-.69-.46-1.26-.73-1.26s-.35.35-.19.78a5.05 5.05 0 0 1 .3 1.52c0 .41.2.62.43.47s.32-.83.19-1.51Zm2.36.7c0-.29-.22-.39-.5-.22s-.49.4-.49.52.22.22.49.22a.51.51 0 0 0 .48-.48Zm22.57 0a1.07 1.07 0 0 0-.8-.5c-.27 0-.36.22-.19.5a1.09 1.09 0 0 0 .81.49c.25.02.33-.2.16-.45Zm3.6.2c-.39-.39 1.63-2.69 2.37-2.69s1.82-2.35 1.19-2.74c-.27-.17-.42-.12-.35.12s-.92 1.48-2.22 2.77c-2.61 2.61-2.73 2.83-1.53 2.83.44.02.68-.11.54-.27Zm-20.91-1.56c-.16-.16-.28.13-.26.66s.14.69.3.29a1 1 0 0 0 0-.95Zm14.59.86a.5.5 0 1 0-.5.5.51.51 0 0 0 .48-.48Zm-8.84-1.83a4.39 4.39 0 0 0 1.39-2.22c0-.75.2-.81 1.31-.39 1.39.53 3.16.22 3.16-.56 0-.27-.85-.33-2-.15-1.29.2-2 .13-2-.22 0-1 2.4-3 4.33-3.62a13.72 13.72 0 0 0 2.47-1.08 1.11 1.11 0 0 1 1.08-.14.56.56 0 0 0 .77-.16.57.57 0 0 0-.21-.79c-.8-.49-.59-1.24.34-1.24.67 0 .71-.13.23-.62s-.74-.44-1-.1c-.7 1-3.87 3.09-5.65 3.66-1.57.5-2.09 1.09-3.67 4.16a18.58 18.58 0 0 1-2.26 3.73c-.51.2-.58 1.07-.09 1.07a6.25 6.25 0 0 0 1.74-1.33Zm13.8.84a.49.49 0 0 0-.47-.5 1 1 0 0 0-.77.5c-.17.27 0 .49.46.49s.76-.2.76-.47Zm-19.85-2a.5.5 0 1 0-.49.5.49.49 0 0 0 .47-.47Zm0-3a.5.5 0 1 0-.49.49.49.49 0 0 0 .47-.43Zm11.89-1.47c0-.27-.35-.5-.78-.5s-.63.23-.46.5a1.05 1.05 0 0 0 .77.49.48.48 0 0 0 .47-.49Zm10.42-1.48c0-.27.86-1.07 1.93-1.77a5.92 5.92 0 0 0 2.18-2.24c.36-1.33-.32-1.2-1.43.28-.52.68-1.16 1.25-1.44 1.27s-1 .68-1.66 1.49c-.92 1.17-1 1.46-.37 1.46.43.04.79-.18.79-.45Zm-9.67-6.37c-.39-.39-.64 0-1 1.65l-.34 1.48.8-1.43c.44-.79.68-1.56.54-1.7Zm-5.71-1.58a.49.49 0 0 0-.47-.49 1.05 1.05 0 0 0-.77.49c-.17.27 0 .5.46.5s.78-.19.78-.46Zm-1-2.26c0-.12-.23-.22-.5-.22a.52.52 0 0 0-.5.53c0 .29.23.38.5.22s.51-.37.51-.49Zm19.84-1.24c0-.29-.22-.39-.5-.22s-.49.41-.49.53.22.22.49.22a.51.51 0 0 0 .51-.49Zm-7.13-4.44a17.22 17.22 0 0 1 .38-2.23c.23-1-.13-.55-1 1.24a21.83 21.83 0 0 1-2 3.39c-1.23 1.3-.58 1.15 1-.24a5.09 5.09 0 0 0 1.66-2.16Zm8.89 3.1c.86-.61 2.7-3.45 2.7-4.16 0-.37-.55.22-1.23 1.31s-1.22 1.8-1.23 1.58c0-.45-1.5.9-1.5 1.36s.64.39 1.27-.05Zm-18.42-3.43c.38-1-.76-1.86-1.3-1s-.46 1.83.29 1.83a1.24 1.24 0 0 0 1.02-.79Zm2.77-.91c0-1.61-.73-1.61-.82 0 0 .69.13 1.24.37 1.24s.46-.51.46-1.2Zm8.93-2c0-.12-.22-.22-.5-.22a.51.51 0 0 0-.49.53c0 .29.22.38.49.22s.51-.38.51-.5Zm2.38-.32a2 2 0 0 0 .59-1.24c0-.41-.35-.29-1 .35a4.76 4.76 0 0 0-1 1.24c.03.49.81.29 1.43-.32Zm-7.33-3.07c.5-.61.47-.75-.19-.75a.78.78 0 0 0-.8.75c0 .41.08.74.18.74a2.73 2.73 0 0 0 .81-.74Zm10.42 0c0-.12-.35-.22-.78-.22s-.63.23-.46.51c.28.4 1.24.18 1.24-.29Zm-7.45-.5c0-.12-.34-.22-.77-.22s-.64.23-.46.51c.28.41 1.23.21 1.23-.34Zm-.49-1.71a.5.5 0 0 0-.5-.49.5.5 0 0 0 0 1 .5.5 0 0 0 .5-.56Zm14.07-8.19c.94-1.44 1-1.78.31-1.78-.27 0-.49.34-.49.75s-.22.74-.5.74a.54.54 0 0 0-.49.57 2.31 2.31 0 0 1-.87 1.27c-.8.64-.78.66.18.19a5.72 5.72 0 0 0 1.86-1.74ZM172 252.8a1 1 0 0 1 .95 0c.4.16.28.28-.3.3s-.81-.09-.65-.26Zm7.26-.62a5 5 0 0 0-1.44-.91 5.47 5.47 0 0 1-1.49-.72c-3.19-2-6.39-3.8-8.43-4.86a23.83 23.83 0 0 1-3-1.78 6.71 6.71 0 0 0-1.91-1 13.36 13.36 0 0 1-2.79-1.42c-.9-.59-2.37-1.52-3.28-2.06a35.73 35.73 0 0 1-4-2.56c-.59-.47-5.3-3-12.32-6.69a19.73 19.73 0 0 1-2.48-1.43c-.14-.15-2.26-1.41-4.72-2.81l-4.4-2.53-4.71 1a56.74 56.74 0 0 1-5.82 1c-.62 0-1.12.25-1.12.56s.42.42 1.12.21a43.36 43.36 0 0 1 5.86-.62c4.61-.26 4.84-.23 7.69 1.28a17.19 17.19 0 0 0 3.56 1.55.57.57 0 0 1 .62.48c0 .26.39.34.87.17s.7-.14.37.2-4.13.78-8.56 1.13-8.06.82-8.06 1.06.28.43.62.44.37.17-.12.49c-1 .67-.54.61 2-.23 2-.65 5.9-1.07 12.61-1.34 1.69-.07 2.44.09 2.36.48a.44.44 0 0 0 .41.59 2.52 2.52 0 0 1 1.27.74 4.49 4.49 0 0 0 2.69.75c3.14 0 2.25.74-1.64 1.39-8.36 1.38-10.74 1.85-11.1 2.21-.53.55 4.92.09 6.06-.5a21.19 21.19 0 0 1 4.85-.85c3.07-.29 3.82-.23 3.52.26s.05.55 1.43.33c1-.17 1.81-.09 1.81.17s-.39.47-.87.48c-.82 0-.82 0 0 .52a2.66 2.66 0 0 0 1.86.2c.56-.17 1-.08 1 .22s-.73.56-1.62.6c-1.21.05-1.33.12-.47.29 2 .38.86 1.37-1.92 1.62-2.17.21-2.45.33-1.45.62a14.7 14.7 0 0 0 3.31.37c1.75 0 2.11.18 2.29 1.12l.22 1.12.37-1.12c.21-.61.66-1.11 1-1.11a1 1 0 0 0 .89-.62c.2-.5.26-.5.29 0s.74.62 1.75.62c1.64 0 4.21.84 4.74 1.55a8.09 8.09 0 0 0 2.48.82l2.23.5-2.6.05c-1.43 0-2.6.27-2.6.52s-.84.53-1.86.62c-1.3.11-1.91.42-2 1s.07.87.36.87a.63.63 0 0 0 .54-.69c0-.46.89-.79 2.68-1a15 15 0 0 1 8.45 1.26 5.08 5.08 0 0 0 2.36.64c.22-.14.4 0 .4.24s.39.59.87.71-.25.28-1.61.34l-2.49.11 1.74.49a24.52 24.52 0 0 1 2.73 1c.78.37.65.42-.6.22-.9-.14-1.75 0-1.94.32s-.12.42.18.23a2.75 2.75 0 0 1 1.93.44c1.41.75 1.4.75-1.06.5-4.78-.51-6.69-.89-6.45-1.27.13-.21-.48-.51-1.37-.66a41.1 41.1 0 0 1-7.31-1.81 7.14 7.14 0 0 0-2.48-.31c-3.46-.06-7-.7-6.68-1.22s-1.83-1.46-3.52-1.46c-.67 0-1.21-.22-1.21-.49s-1.18-.51-2.61-.53c-3.21 0-9.09-1.59-8.93-2.34.06-.33-.22-.44-.68-.26-.67.26-.71.14-.22-.77s.48-1.06-2.3-1.09c-6.8-.08-10.67-1.2-10.2-3 .13-.53.46-1 .71-1s.33-.24.14-.53-1-.41-2.15-.22a52.91 52.91 0 0 1-11.54-1c-1.17-.35-.56-2.17.91-2.73.67-.25 1.07-.68.9-1s-.07-.49.22-.49a.52.52 0 0 0 .53-.5c0-.91-1.42-.51-1.74.5s-.72 1-3.88.93c-5.39-.09-6.54-.44-6.52-2 0-.72-.16-1.2-.38-1.07a1.24 1.24 0 0 0-.38.95c0 .4-.23.72-.5.72-.68 0-.63-.25.41-2 .5-.82 1-1.38 1.17-1.24s.55-.09.9-.51c.91-1.09.22-1.52-1.22-.76-2.65 1.42-13.44-.6-11.5-2.15a3.52 3.52 0 0 0 1-1.31c.2-.53.29-.46.32.25s.23.63.85-.62c.51-1 1.16-1.61 1.82-1.61s.86-.18.56-.47-1.25-.28-2.59.06c-2.1.53-5.52-.06-9.66-1.66-3.07-1.19-.71-3.84 3.55-4l2.77-.08-2.35-.19c-1.3-.1-2.36-.34-2.35-.53 0-1 1-1.66 2.28-1.55 2.28.18 3.66 0 3.66-.49 0-.25-1.55-.45-3.45-.45a15.72 15.72 0 0 1-4-.32c-.28-.18-.51-.09-.51.2s.37.53.83.53c.73 0 .72.09-.12.73-1.33 1-2.06.94-6.61-.52-4.38-1.42-5.92-2.55-4.11-3 .58-.15.94-.48.8-.71s.07-.31.5-.14.77 0 .77-.45c0-.69 1.29-1.39 3-1.61a8 8 0 0 0 1.63-.47c.49-.21 1-.24 1.2-.07s0 .3-.49.3c-.81 0-1.69 1.82-1.14 2.37s3-1.67 2.89-2.32c-.08-.34 1.17-.58 3.47-.64 2.13-.06.76-.2-3.35-.37-3.82-.15-7-.11-7.11.1-.42.95-4-.33-4-1.43 0-.37-.32-.53-.75-.36a2.85 2.85 0 0 1-2-.55l-1.27-.82 1.29-1c1.15-.9 1.54-.95 3.48-.46 2.66.66 3.23.68 3.23.08 0-.25-.51-.53-1.12-.62s2.4-.18 6.7-.22c4.56 0 7.81.14 7.81.41s-.43.33-.95.16a1.2 1.2 0 0 0-1.31.29c-.28.44.15.51 1.69.26 1.7-.27 2.06-.18 2.06.48 0 .49.27.7.71.53a1.28 1.28 0 0 1 1.3.44 2.6 2.6 0 0 0 1.8.72c2.1 0 .76.88-2 1.28l-2.6.39 2.48.39a11.09 11.09 0 0 0 3.36.12 6.25 6.25 0 0 1 2.48.08c1.4.32 1.48.41.61.75s-.7.4.62.43c1.76 0 2.1.69.74 1.48-.78.46-.79.5 0 .52a1.13 1.13 0 0 0 1.05-.61c.18-.49.43-.49 1.27 0a4.86 4.86 0 0 0 2.12.62c1.78 0 .26-1.25-3.95-3.24a63.72 63.72 0 0 1-9-4.94c-3-1.82-4-2.19-5-1.88a67.56 67.56 0 0 1-9.14.39c-5.87 0-7.94.16-8.1.62-.28.83-1.63.78-3.47-.13s-3.1-2.26-1.7-1.89a1.25 1.25 0 0 0 1.29-.27.89.89 0 0 1 1-.24c.41.15.73.06.73-.23a.74.74 0 0 0-.46-.67c-.25-.09-.17-.56.18-1 .57-.77.72-.79 1.27-.12.43.51.45.75.07.75a.53.53 0 0 0-.56.49c0 .27.67.5 1.49.5s1.49-.23 1.49-.5a.5.5 0 0 0-.5-.49.51.51 0 0 1-.5-.5c0-.27.45-.5 1-.5s1 .17 1 .38 2 .37 4.46.37c4.19 0 5.31.28 3.85 1-.34.15-.12.15.5 0s1.11-.07 1.11.19.8.62 1.77.81a20.22 20.22 0 0 1 2.56.63c1.21.47.26-.82-1-1.38-.58-.26-2.63-1.35-4.54-2.43l-5.71-3.19c-1.22-.68-3.57-2.15-5.2-3.25a16.82 16.82 0 0 0-3.51-2 3.51 3.51 0 0 1-1.51-.87 22.29 22.29 0 0 0-3.2-2.05l-3.69-2c-1.48-.79-6.2-1.14-6.2-.45s2 1.43 3 1.12a2.2 2.2 0 0 1 1.56.06c.37.23-.15.61-1.36 1l-1.95.62 2.38-.23c2-.19 2.52-.06 3.27.87a3.61 3.61 0 0 0 2.83 1.2c2.28.13 6 1.45 5.29 1.88s-4 .31-6.19-.23a3.55 3.55 0 0 0-2.74.1c-.71.45-1.28.33-2.78-.55S42.5 183.39 43 183s.42-.39-.24-.18c-1 .34-3.82-.94-3.82-1.76 0-.33.32-.28.84.14s.72.48.47.06 0-.63 1-.66c.8 0 1.08-.16.65-.32a2.58 2.58 0 0 1-1.14-.95 1.72 1.72 0 0 0-1.37-.67c-.53 0-1-.23-1-.5s-.32-.5-.72-.5a5 5 0 0 1-2.11-1.22l-1.39-1.23 1.74.18 2.38.24c.36 0 .5.29.32.59s0 .49.59.42a6.87 6.87 0 0 1 2.66.66 4 4 0 0 0 2 .55c.47-.47-2.88-2.67-7.84-5.15-2.81-1.41-5-2.69-4.85-2.84s.56-.06.91.19a13.66 13.66 0 0 0 2.62 1.13c1.09.36 3.41 1.32 5.15 2.12a17.38 17.38 0 0 0 5 1.55 4.75 4.75 0 0 1 3.37 1.67c1 1 1.66 1.36 1.79 1s.92.17 1.93 1.26c2.44 2.65 2.71 2.31.68-.87-1-1.5-1.58-2.73-1.37-2.73s.69.43 1.07.95.93.86 1.24.74 1.21.45 2 1.28a4.77 4.77 0 0 0 2.34 1.5c.49 0 .9.22.9.5 0 1 3 .52 4-.62.59-.7.94-.87.95-.44s.38.58 1 .42c1-.27 1.27.09.61.91-.21.26-.29.41-.17.35s.55.16 1 .5c.6.49.79.49 1 0 .23-.67 1.62-.87 1.62-.22a2 2 0 0 1-.59 1c-.32.32-.42.75-.2 1s.64 0 1-.44c.55-.75.71-.71 1.75.37 1.5 1.56 2.44 1.51 2.85-.14l.34-1.34.44 1.21c.33.89.55 1 .85.53s.51-.5.75-.1.51.46.69.34c.48-.29 2.38 1.16 2.07 1.59-.14.19.36.41 1.09.5a1.9 1.9 0 0 1 1.65 1.11c.17.54.62 1 1 1 1 0 .42-1.14-1-2-.88-.53-1.08-.93-.75-1.47s.75-.44 2.15.92c1.21 1.17 1.78 1.46 2 1s.27-.5.3-.05.46.62 1 .62a4.39 4.39 0 0 1 2.1.92 2.62 2.62 0 0 0 1.86.69c.49-.17.58.06.34.82s-.09 1.15.57 1.38a3.12 3.12 0 0 1 1.24.86.46.46 0 0 0 .75.09c.22-.23.12-.52-.22-.65-.74-.26-.85-1.62-.13-1.62a.49.49 0 0 1 .5.49c0 .73 1.48.61 1.5-.12 0-.38.28-.28.68.25a2.15 2.15 0 0 0 1.82.74c1.61-.17 1.94 1.45.37 1.76-.74.14-.5.24.72.28s1.86-.14 1.86-.68a.69.69 0 0 0-.59-.74c-.31 0-.46-.11-.31-.26.45-.45 1.89.34 1.89 1 0 .43.31.56.87.37s.72-.15.54 0c-.55.62 2.66 3.36 3.24 2.78.83-.83 1.39-.58 1 .48-.22.69-.09 1 .41 1a1.25 1.25 0 0 1 1 .82c.17.44.46.66.64.48.36-.36 3.12 2.1 3.12 2.79 0 .24-.44.3-1 .12s-1-.12-1 .12c0 .66 1.11 1.62 1.88 1.62a3 3 0 0 1 1.61 1.12c.52.61 1 .86 1 .56a1.94 1.94 0 0 0-.72-1.18 3.33 3.33 0 0 1-.69-2.42c0-1 .16-1.4.29-.92s.4.76.63.62.66.07 1 .46a1.28 1.28 0 0 0 1.31.43 1.83 1.83 0 0 1 1.58.47 6.42 6.42 0 0 0 2 1.09c1 .29 1.17.12 1.42-1.19.17-.84.5-1.52.74-1.52.64 0 1.93 3.39 1.61 4.23-.58 1.52.67.66 1.46-1s1.83-2.34 1.83-1.24a.5.5 0 0 0 .5.5c.73 0 .61-1.77-.16-2.41-.43-.36-.83-.37-1.15-.05s-.87.08-1.61-.7l-1.11-1.19 1.27-1c1.36-1.07 1.6-1.61.74-1.61s-6.93-3.7-8.89-5.37c-1.48-1.26-1.64-1.59-1-2.15.4-.38.56-.41.36-.07s0 .56.68.52 1-.43 1-1.22c0-.62.24-1.13.52-1.13s.4.43.22 1-.09 1 .22 1 .53-.47.53-1 .23-1 .7-.78a.79.79 0 0 0 1-.49c.2-.52.49-.62.91-.31a17 17 0 0 0 3 1.34c1.29.48 2.35 1.09 2.35 1.36s.51.34 1.12.17.86-.13.55.1-.35.6.22 1.13.74.63.45-.13-.12-.81 1-.53a3.52 3.52 0 0 1 1.66.79c.17.26.84.37 1.49.26 1.11-.19 1.12-.23.19-.59l-6.55-2.59c-4.45-1.76-6-2.15-7.63-2a6.3 6.3 0 0 1-3.62-.55c-2.36-1.21-5-3.72-4.87-4.59.1-.58 0-.62-.29-.14s-.73.4-1.78-.42l-1.34-1.06 1.08-.94a3.83 3.83 0 0 1 2-.94c.5 0 .92-.23.92-.5 0-.64-.78-.63-2.15 0-1 .48-1 .46-.21-.18 1.17-.95 1.1-1.16-.5-1.49-.78-.15-3-1.8-5.09-3.85s-3.33-3.3-2.84-2.95.86.45.86.07.56-.56 1.24-.56 1.24-.28 1.24-.74.41-.69 1.12-.62 1.07-.12 1-.5.21-.62.62-.62c1 0 .94-.49-.12-1.29-1.28-1-1.72-.83-1.37.42.17.62.13.88-.09.58a3.52 3.52 0 0 0-1.7-.87c-1.6-.39-5.64-4-5.22-4.68.17-.27-.08-.47-.56-.43s-.81-.2-.74-.53-.2-.48-.62-.32c-.89.34-1-.61-.12-1 .34-.15.11-.16-.5 0s-1.11.09-1.11-.14c0-.5 3-1.73 3.4-1.37.13.13 0 .23-.35.23a.55.55 0 0 0-.57.5c0 .93 2.43.52 3-.5.38-.71.37-1 0-1s-.46-.28-.34-.62.74-.54 1.38-.44c1 .14 1.08.06.53-.6s-.84-.65-1.51-.23c-1.61 1-3.66-.06-7.35-3.83a23.6 23.6 0 0 1-3.6-4.18c0-.28.66-.52 1.46-.52a2.59 2.59 0 0 0 1.77-.49c.48-.79 2.23.38 2.23 1.5 0 1.6 2.26 1.83 3.72.37l1.22-1.22-2.1-1.48c-2.93-2.07-3.57-2.42-3.14-1.73.21.35 0 .57-.47.57s-.72-.27-.6-.62a2.61 2.61 0 0 0-.2-1.61c-.4-.91-.42-.9-.23.12.3 1.55-.64 1.41-2.59-.37-.9-.82-1.53-1.49-1.4-1.49s-.42-.68-1.21-1.53c-2.44-2.62-1.6-3.88 1.7-2.56 1.15.46 1.79 1 1.64 1.39-.31.79 1 1.38 1.47.65.22-.36.48-.31.77.14s.34.14.12-.81c-.42-1.91-.64-2.13-1.2-1.23-.4.63-.46.61-.47-.08 0-1.09 1.44-1.62 1.8-.67a1.12 1.12 0 0 0 .92.74c.34 0 .79.5 1 1.11.32 1 .37 1 .6.18a1.23 1.23 0 0 0-.6-1.42 10.17 10.17 0 0 1-1.79-1.42 3.26 3.26 0 0 0-1.37-.93 2 2 0 0 1-1.06-.76 1.5 1.5 0 0 0-1.62-.44c-.71.22-.87.14-.59-.31s.18-.51-.2-.28c-.85.54-2.61-1.58-6.18-7.48-.25-.42.27-.57 1.85-.52 1.21 0 2.1.25 2 .46-.44.72 5 6 7.37 7.08 1.29.61 2.18 1.29 2 1.51s0 .22.32 0a.77.77 0 0 1 1.09.28c.3.48.4.36.34-.43 0-.61-.37-1.11-.72-1.11a5.55 5.55 0 0 1-2.52-1.56 8 8 0 0 0-1.85-1.61c-.17 0-.24-.48-.17-1.07s-.55-1.73-2-3.1a11.79 11.79 0 0 0-2.51-2 .46.46 0 0 1-.4-.5 12.66 12.66 0 0 0-2.48-3c-1.36-1.35-2.48-2.28-2.48-2.05a10.79 10.79 0 0 0 2.05 2.49c2.13 2.18 2.43 3.45.33 1.45-1.7-1.62-3.4-4.17-3-4.55.18-.17 0-.31-.29-.31s-.47-.23-.3-.5 0-.52-.34-.54.1-.26 1-.52c1.32-.38 1.84-.32 2.55.32s.93.66 1.12.15.4-.49.61-.29.14.66-.17 1-.38.92.14 1.63c.38.52.87.78 1.07.57s.12-.64-.2-1c-1.17-1.17-.56-1.52.66-.38.84.79 1.08 1.34.74 1.67-.81.82-.61 1.1 1.5 2.05 1.45.66 2.15.76 2.54.36.86-.85.15-2-1.91-3a14.08 14.08 0 0 1-3.57-3c-.93-1.11-1.91-1.88-2.19-1.71s-.34 0-.13-.29c.36-.58-.69-2.58-2.09-4-.54-.55-.78-.57-1.08-.09s-.46.42-.7-.2c-.48-1.27-.36-2.09.18-1.24.29.45.48.51.49.15s-.48-.93-1.08-1.35c-1.25-.88-1-1.39.9-1.59.71-.07 1.16 0 1 .16s.15.9.69 1.65a5.39 5.39 0 0 1 1 2c0 .33.28.49.62.37a1 1 0 0 0 .55-1c-.08-.92-.06-.93 1-.52s1 1-.21 1.33c-1 .26-1.44 2.34-.49 2.34a.53.53 0 0 0 .49-.55c0-.37.72-.45 2.19-.23 1.31.2 2.31.13 2.5-.17.34-.54-1.14-2.19-2.16-2.41s-6-4.13-6-4.64c0-.24-.35-.44-.77-.44s-.71-.27-.64-.62c.19-1-.61-2.88-1.11-2.57-.25.15-.68-.31-1-1-.68-1.78-.67-1.79.74-1.15.68.31 1.24.87 1.24 1.23s.21.66.46.66.61.45.78 1a2.36 2.36 0 0 0 1.57 1.31c.93.23 1.08.43.59.74s-.4.42.36.43a2.25 2.25 0 0 1 1.74 1.37c.67 1.3 2 2.68 2.33 2.38.15-.14-1.3-2.93-3.16-6.1-.52-.88-1.11-1.61-1.32-1.61s-.37-.38-.37-.84a18.55 18.55 0 0 0-1.55-5.23c-.85-1.67-.42-2.1.74-.76A4.13 4.13 0 0 0 85 89.39c.3 0 .61.84.69 1.86.15 1.84.17 1.86 2.26 1.8 1.18 0 2 .13 1.82.39a1 1 0 0 0 .19 1.06 6.72 6.72 0 0 1 .83 2.21c.19.88.55 1.61.8 1.61s.46.56.46 1.24c0 1.54 1 1.66 1 .12 0-1 .08-1 .54.25.51 1.41 2.12 1.89 2.54.75.18-.49.36-.5.87 0 .87.82-.42 1.31-2.76 1-1.08-.13-1.68 0-1.57.31s1.22.6 2.5.68 2.21.35 2 .64c-.45.72.74 1.93 1.88 1.93.83 0 .89-.12.38-.74-.67-.81-.51-.91.7-.44 1.61.61.85 1.68-1.2 1.68s-2.85 1-.86 1c.61 0 .83.16.49.29-.92.37-.24 1.16 1 1.16h1.06l-1.09 1c-1.35 1.26-1.37 1.81-.09 2.22s1.24.77.37 1.7c-.5.53-.47.61.13.43.4-.13.81-.93.89-1.8.15-1.51.25-1.57 2.37-1.57 2.68 0 3.37 1 1.68 2.5-.63.54-1 1.14-.74 1.34s.49.09.62-.25a1.06 1.06 0 0 1 .95-.62c.4 0 .59.24.41.53s0 1 .51 1.46.71 1.14.55 1.4.14.58.65.71a.86.86 0 0 0 1.2-.73c.22-.84.37-.89 1-.32.39.37.57.79.38.92a8.82 8.82 0 0 0-1.16 2c-.85 1.78-.42 2.56.82 1.53.49-.4.8-.36 1.24.16a1.64 1.64 0 0 0 1.55.44c.57-.15 1 0 1 .35a4.66 4.66 0 0 0 1 1.85c.54.69.86 1.44.71 1.69-.4.66.5.52 1.21-.19.5-.49.55-.42.26.38s-.29.92.5.28.94-.62 1.35 0 .67.46 1.59-.66c1.39-1.69 2.13-1.79 1.05-.14-.93 1.42-.52 2.63.58 1.72.42-.35.7-.38.7-.08s-.4.66-.87.85c-.79.3-.77.42.25 1.25a3 3 0 0 1 1.11 1.47.51.51 0 0 0 .62.48c1.3-.17 1.86.06 1.86.75a1.3 1.3 0 0 1-.49 1c-.3.18-.36-.05-.15-.59.3-.79.24-.82-.53-.2-1.1.9-.84 2.28.43 2.28.74 0 1 .28.83 1.12-.08.61.09 1.11.37 1.11s.53-.44.53-1c0-.75.3-1 1.23-.89 1.31.11 2.22 1.38 1 1.38-.81 0-1 .95-.23 1.05l.95.13c1 .14 1.12.8.12.8-1.44 0-1.36.71.17 1.4 1.81.82 1.52 1.57-.62 1.62-1.84 0-1.84.05-.43.47a7 7 0 0 0 2.85.15c.92-.19 1.42-.08 1.42.3s-.2.46-.46.3a.56.56 0 0 0-.77.22 2 2 0 0 1-1.4.55c-.81 0-.67.16.53.51s1.61.73 1.61 1.72c0 .72.21 1.12.5.94a.6.6 0 0 0 .19-.82c-.21-.33 0-.4.68-.2a1.77 1.77 0 0 0 1.76-.44c1-1 1.33-1 1.33 0a.74.74 0 0 1-.74.74c-.58 0-.51.24.27 1a2 2 0 0 1 .71 2c-.17.55-.07 1 .22 1s.55-.61.58-1.36l.06-1.37.54 1.38c.31.79.82 1.28 1.18 1.14a.83.83 0 0 1 1 .31c.2.32.12.41-.2.22a.61.61 0 0 0-.85.17c-.17.28-.85.37-1.52.21-1.63-.42-1.53.39.16 1.18 1.17.54 1.44.51 2-.23.34-.46.61-.64.6-.38 0 .55-1.74 2.7-2.17 2.7-.16 0-.29-.33-.29-.74 0-.85-.43-.93-1.46-.28-.58.36-.44.58.65 1.08a5.33 5.33 0 0 1 2.07 1.89c.48.9.69 1 .71.44 0-1 2.1-1.82 2.39-1a1 1 0 0 0 .89.56.74.74 0 0 1 .7.78c0 .42.2.65.45.5s.59.32.77 1 .1 1.45-.24 1.67c-.89.54-.34 2.2.77 2.3.77.08.79.05.11-.15-.48-.14-.86-.5-.86-.8s.14-.39.32-.21.63.07 1-.24c.95-.79 1.18-.36.71 1.34-.27 1-.67 1.42-1.24 1.31s-.77.11-.5 1.2.18 1.24-.28.95-.49-.14 0 .78l.62 1.16.39-1.06a1.19 1.19 0 0 1 2.35.27c.11.76.51 1.07 1.36 1.07 1.23 0 1.49.75 1.33 3.81a1.41 1.41 0 0 0 .71 1.36c.54.21.68.07.48-.45s-.05-.75.25-.75.32-.27-.09-.76-.49-.92-.09-1.31.62-.23.82.64c.42 1.83.11 4.92-.44 4.38a2 2 0 0 0-1.24-.46c-.65 0-.63.13.1.68 1.06.81 1.15 1.87.12 1.47-.41-.15-.74 0-.74.32s.78.54 2.48.42c1.36-.09 2.48 0 2.48.2s-.45.37-1 .37c-1.38 0-1.25.83.17 1.1 1.08.21 1.16.09 1-1.65s.43-2.93 1.06-1.9a9.78 9.78 0 0 1 .31 3.05c0 2.52.63 2.77 1.06.39.12-.67.25.22.28 2 .08 4.16-.2 5.09-1.82 6.09l-1.33.83h2.14c1.49 0 2-.16 1.78-.56s-.13-.43.22-.21c.81.49.76 2.94 0 2.47-.35-.19-.46-.17-.26.05a.77.77 0 0 1 0 .95 2 2 0 0 0 .24 1.78l.65 1.24v-1.49c-.06-2.84.87-5.17 2.07-5.2.43 0 1.63 10.25 1.29 11.12a1.71 1.71 0 0 1-1.43.84c-.91 0-1 .1-.22.29s.84.54.61 1.55c-.28 1.26-.26 1.28.45.36.42-.53.76-.72.76-.43s.37.68.81.86c.69.26.72.19.18-.46-.87-1-.46-1.3 1.43-.93 1.11.23 1.52.54 1.33 1s-.08.58.16.43.55.8.69 2.09.07 2.42-.24 2.53-.3.31.06.53a1.06 1.06 0 0 1 .22 1.27c-.34.88-.37.88-.73 0s-.37-.86-.4-.07a1.26 1.26 0 0 0 .67 1.16c.77.3.52 1.49-.56 2.58-.76.77-.87 2.57-.15 2.57.27 0 .5-.36.5-.81 0-.66.13-.69.74-.18s.75.48.75-.07a2.11 2.11 0 0 1 1.06-1.35c1-.65 1.05-.61.74 1.34-.18 1.1-.13 2.12.11 2.27.76.46 1.22 9.17.53 10-.93 1.12-.2 1.38 1.23.44 1.23-.81 1.29-.8 1.29.14A104.22 104.22 0 0 0 177.4 236c.3 1.09 1.1 3.39 1.79 5.12s1.1 3.27.94 3.43c-.47.47-1.94-1.37-3-3.73-.64-1.45-1.1-2-1.45-1.62s-.19.94.49 1.88c2 2.75 1.62 3.15-.8.83-1.37-1.31-2.49-2.82-2.49-3.36s-.13-.85-.29-.69-1-.24-2.86-1.86c-3.71-3.26-8.84-6.2-10.8-6.2-.71 0-.49 1.09.41 2.09a5 5 0 0 1 .86 3.14c0 1.21.18 2.21.39 2.21s.39-.35.39-.78.23-.62.57-.41.44.13.24-.18a1.83 1.83 0 0 1 0-1.33c.24-.62.47-.67 1-.23a1.87 1.87 0 0 1 .68 1.13c0 .31-.25.22-.53-.19-.48-.66-.51-.66-.33 0 .11.41.42.75.7.75a13.73 13.73 0 0 1 3 3c1.38 1.64 3.75 4.19 5.27 5.67s2.65 2.87 2.51 3.09.1.28.52.11.77-.05.77.25.61.87 1.37 1.28c1.93 1.06 4.35 3.15 4 3.48-.15.15-.81-.14-1.47-.66ZM149.53 245a14.69 14.69 0 0 0-2.68-.52c-1.76-.23-2.27-.18-1.74.18s4.92.79 4.42.34Zm-1.94-2.3c0-.27-.32-.49-.71-.49a1.33 1.33 0 0 0-1 .49c-.16.28.16.5.72.5s.99-.2.99-.52Zm19-2c-.25-.95-1.14-1.41-1.14-.6a3.27 3.27 0 0 0 1.29 1.59 2.39 2.39 0 0 0-.15-.99Zm-1.59-2.23c-.34-.4-.82-.61-1.06-.46-.64.39 0 1.2.9 1.2.63 0 .66-.14.16-.74Zm-28-.28c.54-.87-.07-1.05-.75-.23-.39.47-.43.76-.1.76a1.14 1.14 0 0 0 .79-.53Zm37.94.05A9.66 9.66 0 0 0 174 236c-.67-1.4-1.12-1.76-2.23-1.76-1.51 0-1.55 0-1.1 1.19.23.62.51.67 1.31.23s1-.41.73.22a.77.77 0 0 0 .42 1 1.2 1.2 0 0 1 .71 1c0 .41.23.75.5.75a.49.49 0 0 0 .5-.47Zm-34.47-.91c-.15-.16-.55 0-.89.33-.49.49-.43.55.28.28.43-.19.7-.47.55-.61Zm-17.62-3.11c0-.29-.23-.39-.5-.22s-.5.41-.5.53.23.22.5.22a.52.52 0 0 0 .44-.53Zm35-.83c-.16-.16-.28.13-.26.66s.14.69.3.29a1 1 0 0 0 0-1Zm-21.88-.13c.83-.35.72-.43-.65-.46-1 0-1.52.17-1.33.46.32.58.59.58 1.94 0Zm32.5-.25a2 2 0 0 0-1.12-.74c-.28 0-.22.33.13.74a1.91 1.91 0 0 0 1.11.74c.23 0 .18-.32-.16-.75Zm-41.68-.55a3.25 3.25 0 0 0-1.51-.19c-.84 0-1.4.19-1.26.43.25.39 2.77.18 2.77-.24Zm-7.07-.54a2.86 2.86 0 0 0-1.49 0c-.34.14.06.24.88.23s1.05-.1.57-.23Zm14.52-.43c0-.12-.23-.22-.5-.22a.52.52 0 0 0-.5.53c0 .29.23.39.5.22s.46-.4.46-.53Zm35.52-2.07c-.17-.16-.29.13-.26.66s.14.69.3.3a1 1 0 0 0 0-1Zm-56.3-.48c-.57-.22-.57-.33 0-.69s.24-.43-.77-.44c-1.39 0-1.42 0-.5.72a3.21 3.21 0 0 0 1.46.7c.27 0 .19-.14-.19-.29Zm45-.95a2 2 0 0 0-1.26-.69c-.56 0-.57.19-.1.95.32.52.89.83 1.26.69s.53-.42.1-.94Zm-11.19.06c-.15-.4-.5-.6-.78-.43s-.23.47.18.73c.9.58.93.56.6-.3Zm27.41-.61c-.17-.17-.28.13-.26.65s.14.7.3.3a1 1 0 0 0 0-.95Zm-74.09.53a1.88 1.88 0 0 0-1.24 0c-.35.14-.07.25.62.25s.95-.11.62-.22Zm13.27-.39c1.77 0 2.67-.06 2-.17l-1.23-.2 1.23-.68 1.22-.68h-1.24a64.19 64.19 0 0 0-6.94 1.21c-5.43 1.14-5.53 1.18-2 .84 2.1-.18 5.18-.33 6.96-.32Zm6 .38a1 1 0 0 0-1 0c-.16.16.13.28.65.26s.7-.14.3-.3Zm33-.37c.7.23.76.19.25-.18s-.53-.71.25-1.77c1.13-1.55 1.48-1.6 2.17-.32s1.49 1.32 1.49.28c0-.39-.45-.85-1-1s-1-.52-1-.78-.43-.46-1-.46a1.67 1.67 0 0 1-1.27-.5 1.71 1.71 0 0 0-1.3-.5c-.92 0-.94.08-.22 1.11a4.7 4.7 0 0 1 .76 2.36c0 1.2 0 1.21-.74.28s-.73-.86-.73.68c0 1.34.12 1.55.68 1.09a1.85 1.85 0 0 1 1.61-.27Zm15.46-.23c.12-.34-.09-.57-.46-.51a4.59 4.59 0 0 1-1.91-.38 20.34 20.34 0 0 0-2.73-.84c-1.06-.24-.7.09 1.24 1.12 2.87 1.53 3.54 1.63 3.86.61Zm6.06-1.11c0-.27-.22-.35-.5-.19a1.11 1.11 0 0 0-.49.81c0 .27.22.36.49.19a1.07 1.07 0 0 0 .5-.81Zm-24.51-.92c0-.82-.27-1.42-.54-1.33s-.44.31-.37.5a6 6 0 0 1 .12 1.32c0 .54.19 1 .42 1s.4-.68.37-1.49Zm8.88.49c-.17-.28-.4-.5-.52-.5s-.22.22-.22.5a.5.5 0 0 0 .52.49c.29 0 .39-.22.22-.49Zm-57.3-1c.58-.4.5-.45-.37-.2s-1 .15-.81-.47 0-.77-.44-.77a1.24 1.24 0 0 0-1 1c-.2.77 0 1 .81 1a3.77 3.77 0 0 0 1.81-.54Zm8.28-2c-.31-1.21 3.41-2.71 5.63-2.27.84.17 1.42.14 1.29-.06s.43-.41 1.22-.47 1.44.13 1.44.57c0 .81 1.36 1.42 1.59.71s2.41.17 3.54 1.3a2.84 2.84 0 0 0 2.19.65l1.37-.16-1.3-.74c-2-1.17-28.38-14.24-29.28-14.54a6.17 6.17 0 0 0-2.73.38 55.13 55.13 0 0 1-7.49 1.27c-3.07.34-5.48.77-5.37 1s2.61.13 5.56-.1c3.85-.3 5.41-.26 5.55.13s.37.31.75-.15c.78-1 4.12-1.43 4.91-.66a8.63 8.63 0 0 0 2.73 1.23c1.16.34 2.11.81 2.11 1.05s.83.64 1.86.9l1.86.47-1.42.58c-1.76.71-15.74 2.54-19.44 2.54-2.11 0-2.71.17-2.71.76s.22.65.89.29a4.71 4.71 0 0 1 2.6-.12c1.84.38 8.5-.3 8.92-.91a21.69 21.69 0 0 1 5-1c2.59-.32 5.55-.72 6.58-.88 1.67-.26 1.84-.19 1.56.64s-.2.79.22.2.67-.58 1.55.37a3.43 3.43 0 0 0 1.89 1.11 6.47 6.47 0 0 1 2.27.75 6.67 6.67 0 0 0 2.38.73c.53 0 .85.2.7.44a10.8 10.8 0 0 1-3.69.77c-5.06.49-20.4 3.09-20.8 3.51s4.81 0 8.08-.73c2.61-.58 7.2-.24 7.2.53 0 .24-.62.48-1.37.51h-1.36l1.48.35c1.86.44 2.39.18 2.08-1Zm-2.19-1.26a1 1 0 0 1 .95 0c.4.16.28.28-.3.3s-.81-.1-.65-.26Zm-5.5 1.94a1 1 0 0 0-1 0c-.16.17.13.29.65.26s.7-.14.3-.3Zm11.32-.65a.5.5 0 1 0-.5.5.5.5 0 0 0 .5-.51Zm51.15-1.13c-.4-.41-.55-.35-.55.21 0 1 .67 1.65.92.91a1.25 1.25 0 0 0-.37-1.13Zm-58.5.79a1 1 0 0 0-1 0c-.16.17.13.28.65.26s.69-.14.3-.3Zm11.57-.16c-.17-.27-.41-.49-.53-.49s-.22.22-.22.49a.51.51 0 0 0 .53.5c.29-.01.39-.23.22-.51Zm45.11-3.5a3.17 3.17 0 0 0-2.4-.91h-1.54l1.62.52c.88.28 1.61.77 1.61 1.09a2.17 2.17 0 0 0 .62 1.2c1 1 1.1-.83.09-1.95Zm-21.48.36c-.31-.37-.44-.79-.29-.93s-.19-.38-.73-.52c-.93-.25-.95-.2-.21.93.43.65 1 1.18 1.28 1.18s.29-.25 0-.66Zm-45.42-.33c2 0 1.32-.77-.79-.86-1.49-.06-2 .11-2 .69s.27.67.78.47a7.07 7.07 0 0 1 2-.3Zm18.69-.82a1.88 1.88 0 0 0-1.24 0c-.35.14-.07.25.62.25s.95-.12.62-.26Zm-3.23-.51a3.89 3.89 0 0 0-1.73 0c-.49.12-.09.22.86.22s1.35-.11.87-.23Zm32.26-1.34c.48-1.06.41-1.22-.67-1.43s-1.2 0-1.2.86c0 2 1.06 2.34 1.87.57Zm13.75.69a1.31 1.31 0 0 0-1-.5c-.41 0-.6.22-.43.5a1.35 1.35 0 0 0 1.05.49c.36-.01.55-.23.38-.5Zm-76.15-.5c0-.27-.73-.47-1.61-.44l-1.61.06 1.49.38c.82.21 1.54.41 1.61.44s.12-.18.12-.45Zm21.83-.3c0-.11-.44-.2-1-.2s-1 .21-1 .46.44.33 1 .18 1-.35 1-.45Zm58.63.46a1 1 0 0 0-.95 0c-.16.16.13.28.65.26s.7-.14.3-.3Zm-54.07-1a1 1 0 0 0-1 0c-.17.16.13.28.65.25s.69-.13.3-.29Zm44.8-.16a6.07 6.07 0 0 0-2-.43c-1 0-1 0 0 .43a5.91 5.91 0 0 0 2 .42c1 0 1 0 0-.42Zm-19.41-.75c-.16-.4-.38-.74-.48-.74s-.2.34-.2.74.22.75.48.75.36-.33.2-.75Zm34.93.16c-.31-.92-1.38-1.07-1.38-.2a.79.79 0 0 0 .79.79c.43 0 .7-.26.59-.59Zm-22.71-1.43c0-.56-.22-.88-.5-.71a1.32 1.32 0 0 0-.49 1c0 .39.22.72.49.72s.5-.39.5-1.01Zm-56.55-.49a.5.5 0 0 0-.5-.47c-.27 0-.5.35-.5.78s.23.63.5.46a1.07 1.07 0 0 0 .5-.77Zm70.44.52a.5.5 0 1 0-.49.5.49.49 0 0 0 .49-.5Zm-77.67-.95c-.58-.56-4.51-.87-5-.39-.2.19.8.35 2.21.35 1.62 0 2.56.22 2.56.58s.15.43.34.24a.56.56 0 0 0-.12-.78Zm5-.15c.29-.21.13-.37-.37-.37a.82.82 0 0 0-.87.81c0 .46.17.63.37.38a6.79 6.79 0 0 1 .86-.82Zm-2 .11c-.17-.27-.41-.49-.53-.49s-.22.22-.22.49a.52.52 0 0 0 .53.5c.3 0 .4-.23.23-.5Zm47-.49c-.14-.55-.34-1-.45-1s-.19.45-.19 1 .2 1 .45 1 .36-.46.22-1Zm26.65-.22a1.28 1.28 0 0 0-.5-1 1.67 1.67 0 0 1-.49-1.27c0-.78-.36-1-1.87-1a14.9 14.9 0 0 1-3.6-.52 102.28 102.28 0 0 0-10.82-2.43c-1.19.07 3.65 1.56 7.89 2.44 5.78 1.2 6.45 1.49 7.59 3.22.91 1.39 1.8 1.66 1.8.55Zm-23.81-.78a.49.49 0 0 0-.47-.49 1 1 0 0 0-.77.49c-.17.28 0 .5.46.5s.81-.22.81-.5Zm-36.13-.33a1 1 0 0 0-.95 0c-.16.16.13.28.65.25s.7-.14.3-.29Zm-26.63-1.15A15.05 15.05 0 0 0 69 209c-.82-.05-.86 0-.25.43a5.52 5.52 0 0 0 2.48.43h1.74Zm-6.45-1a.58.58 0 0 0-.78-.22c-.69.42-.56.72.31.72.51.05.66-.22.51-.5Zm65.69.36-1.2-.87c-.91-.67-.94-.66-.31.14a2.15 2.15 0 0 0 1.2.87c.31.05.45-.06.34-.14Zm6.29-1.59c-.18-.71-.53-1.16-.77-1a.56.56 0 0 1-.76-.23 1.84 1.84 0 0 0-1.4-.5 6.68 6.68 0 0 1-2.71-1c-2-1.18-2.09-1.2-2.09-.24 0 .41.24.69.53.62s1 .65 1.49 1.6c.76 1.39 1.27 1.74 2.56 1.8a4.07 4.07 0 0 0 2.24-.42c.45-.36.62-.26.62.39 0 .49.14.75.3.58a2.35 2.35 0 0 0 0-1.59Zm-3.29 0c.6-.72 2-.53 2 .28s-1 .52-1-.18c0-.5-.09-.5-.29 0a1 1 0 0 1-.76.62c-.33.06-.3-.25.08-.7Zm-2.46.22a.52.52 0 0 0-.53-.5c-.28 0-.38.23-.21.5s.4.5.52.5.25-.21.25-.48Zm-7.94-.53c0-.28-.22-.38-.49-.21s-.5.4-.5.52.22.22.5.22a.51.51 0 0 0 .52-.51Zm32.25.09c0-.24-.32-.57-.72-.72a4.55 4.55 0 0 1-1.55-1.89c-1.15-2.21-1.61-2.58-4.18-3.35-1.23-.37-3.63-1.1-5.33-1.64a14 14 0 0 0-3.91-.8c-.45.09 1.91 1 5.25 2.1s6.18 2.25 6.35 2.67c.36.94 3.22 4.07 3.71 4.07a.42.42 0 0 0 .38-.44Zm-33.35-2.81c-.17-.17-.57.21-.9.85-.57 1.11-.56 1.13.3.31.52-.45.79-.97.63-1.14Zm6.8 1.26a.56.56 0 0 0-.77-.22c-.69.42-.56.72.31.72.45.05.66-.2.51-.48Zm-28.53-1.24c-.35-.42-.76-.63-.92-.48s0 .5.45.75c1.05.69 1.22.61.5-.25Zm7.7.25a.57.57 0 0 0-.78-.22c-.69.42-.56.72.31.72.46.04.66-.2.5-.48Zm30.6.16a1 1 0 0 0-.95 0c-.16.17.13.29.65.26s.7-.14.3-.3ZM122.56 202c-.43-1-.74-1.32-.75-.78 0 1 .94 3 1.27 2.69a4 4 0 0 0-.52-1.91Zm3.5.68c-.16-.16-.28.13-.26.66s.14.69.3.3a1 1 0 0 0-.04-1.01Zm33.44-1.12a.5.5 0 0 0-.5-.5.5.5 0 0 0 0 1 .5.5 0 0 0 .5-.55Zm-53.91-1.26c-.16-.42-.4-.66-.53-.53s-.09.47.09.77c.49.78.78.63.44-.24Zm-36.38-.51c0-.42-.23-.63-.51-.46a.56.56 0 0 0-.22.77c.43.64.73.51.73-.36Zm3.59.45a1.88 1.88 0 0 0-1.24 0c-.34.14-.06.25.62.25s.96-.16.62-.3Zm3 0a1.88 1.88 0 0 0-1.24 0c-.34.14-.06.25.62.25s.94-.16.6-.3ZM73.67 199c.51-.33.11-.38-1.24-.18-2.45.38-2.87.67-1 .66a5.31 5.31 0 0 0 2.24-.48Zm74.39-2.71c-1.65-2.72-3.21-3.75-5.65-3.75a2.68 2.68 0 0 1-1.54-.45c-.61-.58-5.78-2.1-6.09-1.79-.13.12.19.37.71.54s1.39.48 1.93.7c4.89 2 6.38 2.44 6.8 2.17s.36.14.14.88c-.29 1-.24 1.06.29.55a2.27 2.27 0 0 1 1.06-.62c.64 0 .45 1.39-.23 1.66-.34.14 0 .27.68.29 1.36 0 3.19 1.52 1.89 1.52-.39 0-.27.29.31.71 1.54 1.13 1.46.5-.3-2.41Zm-59.19.3a12.18 12.18 0 0 1-1.52-2.58c-.41-1-.75-1.43-.76-1.09 0 1.66 2.49 6.06 2.88 5 .1-.22-.17-.82-.6-1.29Zm49 1.38c-.57-.57-5.71-2-6-1.73-.11.11.66.44 1.72.73a12.66 12.66 0 0 1 2.65 1c1 .68 2.34.66 1.66 0Zm-41.1-.47a1.09 1.09 0 0 0-.77-.5.49.49 0 0 0-.47.5c0 .27.35.49.77.49s.61-.18.44-.45ZM56.8 197a3.34 3.34 0 0 0-1.49-.48c-.66 0-.66.06 0 .48a3.22 3.22 0 0 0 1.49.48c.66.04.66-.01 0-.48Zm45.9-1c.32-.51-.57-2-1.19-1.95-.82 0-1.63.93-1.3 1.47s.47.45.8-.07.43-.48.43.2c.01.99.76 1.19 1.26.35Zm25.3-.48c.95-.41.95-.42-.1-.46a1.78 1.78 0 0 0-1.39.46c-.37.62.12.62 1.49.03Zm12.65-.49a.5.5 0 0 0-1 0 .5.5 0 0 0 1 0Zm-3-.2c0-.1-.35-.32-.77-.49s-.64-.08-.47.2c.29.47 1.24.7 1.24.29Zm1.49-.82c0-.29-.23-.39-.5-.22s-.49.4-.49.52.22.22.49.22a.51.51 0 0 0 .52-.53Zm-44.93-.91c.15-.24 0-.57-.45-.72s-.62 0-.43.44c.33.86.52.92.9.31Zm64.86-.39a1 1 0 0 0-1 0c-.16.16.13.28.65.25s.7-.13.3-.29Zm4.27-.36c-.35-.38-.72-.62-.81-.53-.3.28.45 1.22 1 1.22.25.03.18-.28-.17-.66Zm-28.17-.58c0-.12-.22-.21-.5-.21a.5.5 0 0 0-.49.52c0 .29.22.39.49.22s.52-.37.52-.5Zm8.82-.91c-.35-.38-.72-.62-.81-.52-.3.27.45 1.22 1 1.22.25.03.19-.29-.18-.67Zm18.76-1.65c-.16-.16-.28.13-.26.65s.14.7.3.3a1 1 0 0 0 0-.95Zm-35.52.42c0-.5-1.2-1-2.27-1-.39 0-.71.24-.71.52s.23.39.5.22.5-.07.5.22.44.53 1 .53 1-.22 1-.46Zm-6.94-1.58c0-.28-.23-.38-.5-.22s-.5.41-.5.53.23.22.5.22a.51.51 0 0 0 .52-.5Zm38.2-.22c.42-.51.44-.74.05-.74a.52.52 0 0 0-.55.46c0 .25-.39.56-.87.69-.73.19-.74.23-.06.28a2 2 0 0 0 1.43-.69Zm-33.65-.08a1 1 0 0 0-1 0c-.16.16.13.28.65.25s.7-.14.3-.29Zm17.77 0c0-.33-.2-.57-2.57-3.17a7.79 7.79 0 0 0-4.13-1.9 86.22 86.22 0 0 1-18.22-7.05c-1.14-.6-2.37-.58-2.37 0 0 .27.46.5 1 .5.84 0 .93.16.5 1-.7 1.29-.65 1.54.23 1.2.41-.15.75 0 .75.27s.33.54.74.54c.9 0 1-.8.12-1.17-.34-.15 0-.14.87 0a5.3 5.3 0 0 1 2 .72 2.33 2.33 0 0 0 1.37.43c.49 0 .89.2.89.43s.89.58 2 .76 2 .5 2 .74.72.57 1.61.74c2.5.49 7 1.91 7.54 2.37a2.39 2.39 0 0 0 1.36.42c.5 0 .9.21.9.48 0 .61 2.38 3 3 3 .27 0 .48-.15.48-.32Zm-19.35-.67c0-.27-.61-.49-1.36-.49-1.07 0-1.2.12-.62.49.99.67 2 .67 2 .04Zm39.69 0a.5.5 0 1 0-.5.5.5.5 0 0 0 .55-.46Zm-11.19-1.52c-.42-.69-.72-.56-.72.31 0 .43.23.64.5.46a.56.56 0 0 0 .24-.73Zm-17.58.05c0-.27-.34-.36-.75-.2s-.74.38-.74.48.33.2.74.2.77-.17.77-.44Zm15.87-.05c0-.29-.22-.38-.49-.22s-.5.41-.5.53.22.22.5.22a.51.51 0 0 0 .51-.49Zm-76.4-.46c0-.28-.39-.49-.87-.49-.74 0-.76.07-.12.49.95.64 1.01.64 1.01.04Zm73.42 0a.51.51 0 0 0-.52-.5c-.29 0-.39.22-.22.5s.4.49.53.49.23-.18.23-.45Zm-101.89-1.24c-.45-.46-1-.68-1.14-.51s.12.63.65 1c1.23.91 1.56.58.49-.49Zm85-.38c-.69-.36-1.35-.57-1.48-.47-.35.29 1.12 1.12 2 1.12.43 0 .21-.29-.47-.65ZM64 183c-.16-.34-.43-1-.6-1.49-.37-1.06-.87-1.1-1.68-.12-.5.61-.47.74.21.74a1.27 1.27 0 0 1 1.1.74c.16.42.51.75.77.75s.4-.25.2-.62Zm52.54-.88a.57.57 0 0 0-.77-.21c-.69.42-.56.72.31.72.46.03.67-.2.5-.48Zm7.45-.49c-.92-1-2.24-1.34-2.24-.57 0 .42 1.83 1.47 2.68 1.54.29.04.09-.39-.43-.94Zm2-.46a1.17 1.17 0 0 0-.85-.53c-.34 0-.3.29.1.76.76.85 1.32.67.76-.2Zm33 .46c0-.27-.34-.49-.74-.49s-.75.22-.75.49.34.5.75.5.75-.19.75-.47Zm-21.65-1.48a5.5 5.5 0 0 0-.93-1.49c-.11 0 0 .67.33 1.49s.7 1.48.93 1.48.08-.63-.32-1.45Zm-15.56-.5a.49.49 0 0 0-.46-.5 1.09 1.09 0 0 0-.78.5c-.17.27 0 .5.47.5s.78-.15.78-.47Zm27.79 0c0-.26-1-.54-2.11-.62s-2.14-.41-2.18-.78c-.14-1.45-.2-1.61-.66-1.61-.26 0-.71.43-1 1-.81 1.51.58 2.52 3.49 2.52 1.36.02 2.47-.16 2.47-.45Zm-1.86-2.22a.64.64 0 0 0-.5-.66c-.34-.12-.62.17-.62.62.01.9.9.93 1.13.1Zm-35.85-.57c0-.12-.22-.22-.49-.22a.51.51 0 0 0-.5.53c0 .29.22.39.5.22s.5-.39.5-.47Zm2 .58c0-.11-.35-.33-.77-.49s-.65-.08-.47.19c.29.47 1.24.7 1.24.3Zm20.83-1.22c0-.81-2.44-3.05-3.33-3.05-1.18 0-11.75-3.62-12-4.09a.66.66 0 0 0-.56-.38 51.23 51.23 0 0 1-5.18-2.27c-2.62-1.25-5-2.16-5.38-2s-.28.27.24.29.74.31.59.72.55.87 2.62 1.37 2.83.92 2.73 1.37.19.58.94.39a3.34 3.34 0 0 1 2.63.86c.85.64 1.8 1.06 2.11.94a5.25 5.25 0 0 1 2.48.81 9.12 9.12 0 0 0 2.52 1c.35 0 .63.2.63.43s.73.58 1.61.75c2.59.51 5.83 2 5.83 2.66a.7.7 0 0 0 .75.62c.41 0 .74-.19.74-.42Zm10.64-1.6c-.42-.68-.72-.55-.72.32 0 .42.23.63.51.46a.58.58 0 0 0 .21-.72Zm-31.33-.18c-1.13-.86-1.64-.88-1.64-.09 0 .34.57.65 1.26.69 1.18.07 1.21.03.38-.6Zm6-1.47c-2.48-.89-2.74-.89-1.52 0a4.21 4.21 0 0 0 2.23.71c1.11.01.99-.12-.65-.68Zm20.09.26a.5.5 0 1 0-.5.5.5.5 0 0 0 .56-.5Zm-22.87-2.73c0-.27-.18-.16-.45.25a3 3 0 0 0-.45 1.49c.09.76.91-.8.96-1.74Zm14.63 1.61a13.21 13.21 0 0 0-1.14-1.53 27.3 27.3 0 0 1-2-3c-3.11-5.2-3.05-5.15-5.53-5.47a14.26 14.26 0 0 1-3.74-1 7 7 0 0 0-2.64-.73c-1.19 0-1.18 0 .38 1a6.94 6.94 0 0 0 2.78 1 10.45 10.45 0 0 1 2.77.56c1.17.41 1.43.68.94 1s-.37.43.43.44 1.83 1 3.59 3.35c2.42 3.27 4 5.08 4.36 5.08.1 0 0-.27-.17-.62Zm15.77.29a1 1 0 0 0-1 0c-.16.16.13.28.65.25s.7-.13.3-.29Zm-26.38-.66c-1-.63-2.13-.63-1.74 0a1.84 1.84 0 0 0 1.4.49c.94 0 1-.1.4-.49Zm-14-1c-2.38-1.28-3-1.28-1.11 0a5.8 5.8 0 0 0 2.22 1c.45 0-.06-.44-1.07-.99Zm38.42.65a1 1 0 0 0-.95 0c-.17.17.13.29.65.26s.69-.14.3-.3Zm-31.62-.62a.53.53 0 0 0-.17-.74c-.59-.36-2 .18-2 .76s1.85.58 2.21-.01Zm33.76-1.52a1.1 1.1 0 0 0-.8-.5c-.28 0-.36.23-.19.5a1.1 1.1 0 0 0 .8.5c.31.01.39-.22.23-.49Zm-35.6-1.36c.14-.41-.32-.62-1.32-.62-1.53 0-2.07.69-.91 1.16 1.03.41 2.03.15 2.27-.53Zm31.38.37c-1-.62-1.49-.62-1.49 0 0 .27.51.49 1.12.49.94 0 1.01-.09.41-.48Zm-2-1a1.09 1.09 0 0 0-.5-.78c-.27-.17-.49.05-.49.47s.22.77.49.77a.49.49 0 0 0 .56-.41Zm6.74-1.39c-.16-.16-.28.13-.25.66s.14.69.29.29a1 1 0 0 0 0-1Zm-22.62.39a1.07 1.07 0 0 0-.49-.77c-.28-.17-.5 0-.5.47s.22.77.5.77a.48.48 0 0 0 .55-.42Zm-7.44-3.5a.48.48 0 0 0-.47-.49 1.07 1.07 0 0 0-.77.49c-.17.27 0 .5.47.5s.83-.18.83-.45Zm-14.88-2.09c0-.55-1.37-.48-1.72.08-.16.26.16.43.72.39s1.06-.2 1.06-.42Zm6.71-.29c-.15-.15-.56 0-.89.34-.49.49-.43.54.28.28.52-.14.82-.42.67-.57Zm14.3.15c-.16-.41-.43-.74-.61-.74s-.59-.62-.92-1.37a11.52 11.52 0 0 0-1.77-2.6c-1.14-1.22-1.34-1.59-1.34-2.39 0-.22-1.29-.81-2.85-1.3a65.92 65.92 0 0 1-7.19-3c-2.5-1.24-4.46-1.94-4.63-1.65-.49.79.8 1.93 2.2 1.94a4.51 4.51 0 0 1 2.27.75 3.74 3.74 0 0 0 1.85.73c1.11 0 4.37 1.68 5.75 3a5.15 5.15 0 0 0 1.31 1c.47 0 3 2.73 2.89 3.11s2.38 3.34 2.93 3.34c.28-.03.33-.35.17-.77Zm-9.48-.83c.21-.56-12.56-6.82-13.32-6.52-1.32.5-.69 1.09 1.54 1.43 1.23.18 2.23.49 2.23.68s-1 .29-2.23.22-2.23.08-2.23.34.61.48 1.36.5c1.21 0 1.25.08.37.46s-.73.43.62.46c.89 0 1.62-.18 1.62-.45s.37-.33.82-.16.71.13.47-.26c-.45-.72 2 .23 3.21 1.25 1.07.88 4.58 2.62 5 2.49a.85.85 0 0 0 .63-.39Zm17.92-2.15c.17-.44 0-.74-.43-.74s-.6.3-.43.74.35.75.43.75a1.77 1.77 0 0 0 .49-.7Zm11.36-1.24c-1-1.16-1.9-1.27-2.28-.28-.19.5.14.78 1.06.92l1.7.28c.26.09.05-.32-.42-.87Zm-35-3.81a2.76 2.76 0 0 0-1.49 0c-.34.13.06.24.88.23s1.16-.06.68-.18Zm-1.84-1.41c1-1.15.37-1.27-1-.21-.91.7-1 .95-.42 1a2.19 2.19 0 0 0 1.49-.74ZM118 151a2.78 2.78 0 0 1-.83-1.21 1.27 1.27 0 0 0-1-.86c-.45 0-.41.15.11.49s.54.48.29.49 0 .45.45 1 1 .87 1.18.71.11-.43-.2-.62Zm-18.55-.09c0-.28-.35-.5-.77-.5s-.64.22-.47.5a1.07 1.07 0 0 0 .77.49.48.48 0 0 0 .49-.49Zm6.22-.53c.31-.5-2.09-2.94-2.89-2.95s-1 1.55-.42 2.12c.42.42.56.41.56 0a.71.71 0 0 1 .78-.6c.42 0 .63.22.46.5a1 1 0 0 0 .75 1.49 1.11 1.11 0 0 0 .76-.53Zm-8.64-1.09c-.49-.49-.55-.43-.28.28.19.48.47.76.62.61s.02-.56-.32-.89Zm27.72.13a.51.51 0 0 0-.53-.5c-.28 0-.38.22-.21.5s.4.49.52.49.24-.22.24-.49Zm8.93-.78c0-.12-.22-.22-.5-.22a.51.51 0 0 0-.49.53c0 .29.22.39.49.22s.52-.4.52-.53Zm-38.2-.46a2 2 0 0 0-1.12-.75c-.27 0-.22.34.13.75a2 2 0 0 0 1.11.74c.3 0 .24-.33-.1-.74Zm20.34-.52c0-1.63-4-6.18-5.36-6.18a16.34 16.34 0 0 1-3.56-1.28l-2.85-1.27-1.68 1-1.68 1 1.55-.3a2.28 2.28 0 0 1 2.21.42 1.74 1.74 0 0 0 1.51.5 3.61 3.61 0 0 1 2.22.69c.77.5 1.4.83 1.41.73.07-.57 3.25.63 3.25 1.22 0 .38.23.7.5.7s.5.4.5.9a2.36 2.36 0 0 0 .59 1.48c.64.73 1.41.86 1.41.39Zm-11.16-7.17c-.18-.28 0-.36.47-.19.86.33 1 .68.3.68a1 1 0 0 1-.75-.49Zm31.39 6.85c-.31-.92-1.38-1.07-1.38-.2a.79.79 0 0 0 .79.79c.45 0 .72-.26.61-.59Zm-34.93-.65c-.36-.92.19-.92 1.8 0 1.43.81 2 .91 2 .35s-4.36-2.59-5.09-2.31-2 2.21-1.41 2.21a.91.91 0 0 0 .69-.62c.17-.46.47-.39 1.14.24 1.12 1.07 1.25 1.09.89.13Zm-8.62-.25a.51.51 0 0 0-.52-.5c-.29 0-.39.23-.22.5s.4.5.53.5.23-.23.23-.5Zm19.76-1.08c-.2-1.36-1.4-1.82-1.4-.53 0 1 .4 1.61 1.11 1.61.26 0 .39-.44.31-1.08Zm-3.09-.77c-.16-.16-.28.13-.26.65s.14.7.3.3a1 1 0 0 0-.02-.95Zm-1.83-1.31c-.58-.53-2.43-.81-2.43-.38 0 .27.5.58 1.11.67 1.14.18 1.73.05 1.34-.29Zm21.38-.81c0-.27-.35-.49-.77-.49s-.64.22-.47.49a1.07 1.07 0 0 0 .77.5.49.49 0 0 0 .49-.5ZM101 139.06a2 2 0 0 0-.76-1.06c-.42-.35-.65-.74-.52-.87s-.11-.58-.53-1c-.62-.61-1-.66-1.7-.2a2.18 2.18 0 0 1-1.68.28c-.44-.17-.77 0-.77.32s.57.52 2 .3c1.63-.26 1.92-.19 1.69.43a1.51 1.51 0 0 0 1.27 2.24c.55 0 1-.2 1-.44Zm8.65-1.34c-.42-.8-1.2-2.12-1.72-2.94s-1.3-2.11-1.73-2.87a5 5 0 0 0-4-2.34c-.41 0-.59.25-.39.55a.82.82 0 0 1-.35 1c-.46.31-.16.37.92.15a4.24 4.24 0 0 1 2.11 0c.77.49-.59 1.63-2.27 1.91-.8.13-.25.21 1.2.16l2.64-.08-.28 1.43c-.24 1.21-.18 1.33.43.83.82-.68 1.76-.3 1.76.71 0 .64 2.08 3.31 2.35 3a4.14 4.14 0 0 0-.71-1.51Zm-6.18.91c0-.21-.56-.63-1.24-.95-1.31-.61-1.62-.26-.64.72.62.66 1.85.81 1.84.23Zm22.83-1.72c0-.27-.32-.37-.72-.22a.69.69 0 0 1-.92-.32c-.12-.33-.38-.44-.58-.25-.52.48.85 1.81 1.59 1.52a.92.92 0 0 0 .59-.73Zm-35 .1a.57.57 0 0 0-.78-.22c-.68.42-.56.72.32.72.4 0 .61-.22.44-.51Zm-2.73-3.1c0-.86-.11-.95-.47-.39a1.3 1.3 0 0 0-.21 1.12c.41.69.67.42.66-.73Zm34.54-2.42c-.35-1.78-2.78-4.6-2.78-3.22 0 .44.22.81.49.81a.59.59 0 0 1 .5.64c0 1 1.18 3.32 1.67 3.32.22.01.27-.69.1-1.55Zm-12.21-.71c0-.42-.23-.63-.51-.46a.58.58 0 0 0-.21.78c.4.69.7.56.7-.32Zm-1.24-1.7c.17-.28.08-.5-.19-.5a1.07 1.07 0 0 0-.8.5c-.17.27-.09.49.19.49a1.09 1.09 0 0 0 .78-.49Zm-9.43-.37a2.52 2.52 0 0 1-1.34-.79.46.46 0 0 0-.73-.16c-.51.51.82 1.32 2.07 1.25.98-.01.98-.01-.02-.3Zm19.1-.61c.57-.36.6-.6.14-1.06s-.76-.46-1.35 0c-1.31 1.1-.27 1.96 1.19 1.06Zm-31.13-1.24c-1-1.4-1.63-1.59-1.63-.5 0 .6 1.17 1.44 2.38 1.69a4.51 4.51 0 0 0-.75-1.19Zm14.14-.64a10.1 10.1 0 0 0-1-1.83 2 2 0 0 1-.4-1.09 4.79 4.79 0 0 0-1.24-2 4.69 4.69 0 0 1-1.24-1.93c0-.86-1.48-2-3.39-2.65-2.21-.75-1.91.29.49 1.65 1 .55 1.65 1.28 1.54 1.61s.13.62.54.62c1.18 0 1.5 1 .54 1.7-.61.44-.7.74-.29 1a1.12 1.12 0 0 0 1.08 0c.75-.46 2 .71 2 1.88a2.34 2.34 0 0 0 .87 1.66c1.17.89 1.17.88.51-.68Zm10.77 0c-.1-.74-.2-1.58-.23-1.86s-.27-.11-.54.36a1.63 1.63 0 0 0 .13 1.86c.82 1.3.86 1.28.64-.36Zm-4.09 0a1.88 1.88 0 0 0-1.24 0c-.34.14-.07.25.62.25s.96-.06.6-.2Zm-13-.56c0-.21-.67-.78-1.49-1.27-1.73-1-2-.33-.37.83 1.23.92 1.84 1.07 1.84.49Zm-10.92-2a4.45 4.45 0 0 0-1-1.61l-1-1.17.89 1.61c.84 1.69 1.09 1.94 1.09 1.18Zm24.53 0c.4-.64-.23-2.07-.77-1.74s-.57 2.2 0 2.2a1 1 0 0 0 .75-.46Zm2.06-1.39c-.16-.17-.28.12-.25.65s.13.69.29.3a1 1 0 0 0-.06-.95Zm-16.61-8.18a2.19 2.19 0 0 0-1.1-1.27c-.44-.12-.5 0-.16.22a3.45 3.45 0 0 1 .85 1.28c.18.48.43.87.57.87s.05-.5-.18-1.1Zm8.26-1.47c-.3-.93-1.22-1-1.54-.18-.21.53 0 .76.72.76s.91-.26.8-.58Zm-10-2.71c-.43-.51-1.11-1.37-1.52-1.92-.72-1-.73-1-.75.14a1.72 1.72 0 0 1-1.28 1.61c-.7.27-1.14.71-1 1 .4.65 2.24.65 2.24 0 0-1 .87-.48 1.16.62s.35 1 1.1.31.71-.91.03-1.76Zm-4.27-.66c.5-.32.57-.63.23-1s-.7-.19-1.12.47c-.67 1.08-.32 1.28.87.53Zm7-5c0-.27-.45-.49-1-.49s-1 .22-1 .49.45.5 1 .5.93-.21.93-.48ZM89.18 95c-.13-.88-.43-1.61-.65-1.61-.8 0-1 2.08-.43 3.57.55 1.33.67 1.4 1 .59a6.46 6.46 0 0 0 .08-2.55Zm94.35 157.46a19.79 19.79 0 0 1-1.65-2.91c-.62-1.29-1.36-2.3-1.65-2.23s-.63-.2-.77-.58 0-.55.27-.36.62-.08.73-.56.56-.06 1.27 1.61c.58 1.36 1.41 3.12 1.83 3.91.83 1.54.82 2.05 0 1.12Zm14.8.37a1 1 0 0 1 .94 0c.4.16.29.28-.29.3s-.82-.09-.65-.26Zm2.47-.52c-1.29-.52-.55-1.53 1.49-2.06l2.1-.53-2.31.09c-1.27.06-2.2-.07-2.08-.27a1 1 0 0 1 .73-.38 14.72 14.72 0 0 0 2.8-1c1.9-.87 3-1 6.15-.83 4.48.26 4.78-.35.42-.86s-3.95-1.4.74-1.31c1.44 0 1.45 0 .25-.38s-1.18-.42-.25-.76c.55-.19 1.51-.56 2.13-.81 1.82-.72 5.24-.53 6.53.38.65.45 1 .56.82.25s.16-.6 1.28-.64c2.16-.06-4-1.35-6.79-1.43h-2l2.18-.77a11.66 11.66 0 0 1 4.09-.58c1.85.17 2.56-.25 1.29-.76-.34-.14.22-.26 1.24-.29 1.21 0 1.82.17 1.74.56-.15.75 6.24 1.61 6.49.87.09-.26.82-.48 1.61-.48s1.45-.22 1.45-.49.45-.5 1-.5 1-.18 1-.41c0-.51-7.77-1.56-12-1.61-3 0-3.1-.07-1.71-.68 2-.87 9.48-.92 10.33-.07.53.52.75.52 1.27 0 .34-.37.45-.69.23-.7s.17-.28.85-.58l1.24-.55-1.73-.34-1.74-.35 1.68-.07a14.54 14.54 0 0 1 3.47.39c4.86 1.23 6.9 1.54 7.77 1.2 1.32-.51-.41-.93-10.81-2.68-2.12-.35-3.85-.85-3.85-1.1s.56-.32 1.24-.15a3.19 3.19 0 0 0 2.46-.48c1.16-.76 6.72-1.83 7.2-1.38s-1.31 1.14-3.05 1.15a3.84 3.84 0 0 0-2.07.39c-.13.21.66.3 1.76.19 1.31-.13 2.15 0 2.42.47s.58.5.79.28.06-.59-.34-.85c-.58-.36-.49-.46.41-.46.64 0 1-.23.84-.51-.37-.59 5.34-.59 7.2 0 1.17.38 1.15.4-.38.45-.88 0-1.61.3-1.61.6s.34.41.76.26A38.79 38.79 0 0 1 253 231c4-.28 4.7-.22 4.7.44 0 .94-1.06 1.39-2.44 1s-1.35.63 0 1.35c.76.41 1.21.4 1.74 0a.91.91 0 0 1 1.39 0 1.74 1.74 0 0 0 1.55.24c.71-.28.8-.14.53.73s-.18 1 .56.76.75-.2-.09.41c-2 1.45-4.51 2.09-8.25 2.11h-3.83l.27 1.33c.24 1.21.08 1.38-1.65 1.82-3.57.9-8.72 1.22-9.13.55-.26-.43-.46-.33-.67.34-.48 1.51-2.83 2.58-6.42 3-1.82.18-3.31.54-3.31.79s-.2.33-.45.18a16.57 16.57 0 0 0-4.42.57c-3.37.7-4.08 1-4.56 2.08s-.67 1.06-1 .59-.5-.49-.75-.09-.5.46-.69.34a33.55 33.55 0 0 0-5.8 1 57.94 57.94 0 0 1-7.08 1.2c-1 0-1.52.22-1.34.51.33.53-.08.59-1.11.17Zm5-4a1.88 1.88 0 0 0-1.24 0c-.34.14-.07.25.62.25s.92-.11.58-.25Zm19.2-3.05a2.49 2.49 0 0 0-1.54-1.58c-.19 0-.23.44-.09 1 .24.94 1.63 1.44 1.63.59Zm-10.73-.84c.17-.45 0-.74-.44-.74a.73.73 0 0 0-.73.74c0 .41.19.74.44.74s.55-.33.71-.74Zm14.95-1.24a3.91 3.91 0 0 0-2.39-.43l-2.08.07 1.74.36c2.6.55 3.07.55 2.73 0Zm-6.2-1.74c0-.4-.22-.74-.47-.74-.62 0-1.07.77-.68 1.17.54.56 1.13.34 1.13-.43Zm16.87-5.7c0-.27-.35-.5-.78-.5s-.63.23-.46.5a1 1 0 0 0 .77.49.49.49 0 0 0 .45-.49Zm11.11-3.49c.69-.32.38-.37-1-.15a10.11 10.11 0 0 0-2.2.48c-.42.38 2.24.1 3.19-.33Zm-6.82-.79c.61-.17 1.11-.5 1.11-.74 0-.68-1.63-.48-2.7.32s-.4.96 1.62.42Zm-33.48 20.85a1 1 0 0 1 1 0c.39.16.28.28-.3.3s-.81-.1-.65-.26Zm-36.85-1.19a.48.48 0 0 1 .47-.5 1 1 0 0 1 .77.5c.17.27 0 .49-.46.49s-.75-.22-.75-.49Zm47.27.2a1 1 0 0 1 .95 0c.39.15.28.27-.3.29s-.82-.09-.65-.25Zm-27.1-5.41c.15-.41.51-.75.78-.75s.24.32-.11.75a2.63 2.63 0 0 1-.79.74c-.06 0-.01-.33.15-.74Zm31.68-6.2a1.07 1.07 0 0 1 .8-.5c.27 0 .36.22.19.5a1.09 1.09 0 0 1-.8.49c-.25 0-.33-.2-.16-.49Zm-76-.76c.44-.32.64-.73.47-.9s-.08-.33.22-.33a1.14 1.14 0 0 1 .86.5 2.51 2.51 0 0 0 1.74.49 2.49 2.49 0 0 1 1.73.5c.19.3-.85.45-2.75.4-2.41-.07-2.85-.2-2.21-.61Zm79.69.26-1.74-.36 2.11-.07c1.16 0 2.11.16 2.11.43s-.17.47-.37.43-1.16-.23-2.11-.43Zm-8.06-.85a13.48 13.48 0 0 1 3 0c.75.12 0 .21-1.62.2s-2.21-.09-1.32-.2Zm7.44-3.46a7 7 0 0 1 2.23 0c.61.11.11.21-1.12.21s-1.67-.11-1.05-.21Zm5.08-5.61a.51.51 0 0 1 .5-.5.5.5 0 0 1 0 1 .51.51 0 0 1-.44-.5Zm46.5-9c.34-.34.74-.49.89-.34s-.12.43-.61.62c-.63.28-.7.22-.22-.27Zm.88-6.89a1.35 1.35 0 0 1 1-.49c.41 0 .61.22.44.49a1.34 1.34 0 0 1-1 .5c-.36.01-.55-.21-.38-.49Zm5.21-2.48-3.23-.24 2.86-.41c1.56-.22 2.85-.63 2.85-.9s-.41-.37-.9-.21c-1.52.48-21.1.43-22.32 0s-1.08-.48.62-1.15c1-.38 1.76-.87 1.76-1.07s.78-.53 1.74-.71 1.74-.52 1.74-.75c0-.52 3.42-1.47 5.29-1.47.77 0 4.27-.22 7.77-.48 5.31-.4 6.48-.37 7.07.22.94.94.89 1.45-.11 1.07-1.21-.47-4 .2-4.36 1-.16.42-.06.59.24.41a1.13 1.13 0 0 1 1.17.22c1 .81 2.47.14 1.73-.76-.44-.54-.4-.69.19-.69a1 1 0 0 1 .95.57c.15.42.46.42 1.25 0 1.34-.72 2.28-.73 1.84 0-.21.36.32.49 1.6.4a5.86 5.86 0 0 1 2.6.27.79.79 0 0 0 1.13-.28c.4-.56.45-.55.3.06-.23.9-6.37 4.24-8.82 4.81a21.12 21.12 0 0 1-5 .15Zm5.7-2.48c.63-.27.72-.42.25-.42a5.48 5.48 0 0 0-1.73.42c-.63.27-.72.43-.25.43a5.32 5.32 0 0 0 1.79-.42Zm2.82-.83a1 1 0 0 0-.95 0c-.16.16.13.28.65.26s.7-.14.3-.3Zm2.89-.69c0-.28-.22-.38-.5-.21s-.49.4-.49.52.22.22.49.22a.51.51 0 0 0 .56-.52Zm-12.68-1c-.42-.69-.72-.56-.72.31 0 .43.23.64.51.46a.57.57 0 0 0 .27-.75Zm-7.16 0a.51.51 0 0 0-.53-.49c-.29 0-.39.22-.22.49s.4.5.53.5.28-.17.28-.42Zm17.49-1.78a1 1 0 0 1 .95 0c.4.16.28.27-.3.29s-.81-.09-.65-.25ZM118 200.47c-.23-.39 0-.44.84-.19 1.14.35 1.15.33.31-.6-1.26-1.42-1.07-1.8.25-.51.7.68.93 1.24.62 1.46a1.37 1.37 0 0 1-2-.16Zm-6.88-3c-.62-.73-.71-1.17-.32-1.55s.54-.31.54.3a.85.85 0 0 0 .93.84c1.2 0 1.73 1.27.58 1.35a2.52 2.52 0 0 1-1.69-.96Zm56 .58a1.05 1.05 0 0 1 .77-.49.49.49 0 0 1 .47.49c0 .28-.35.5-.78.5s-.58-.24-.39-.55Zm39.44-2.48a.5.5 0 1 1 .49.5.49.49 0 0 1-.42-.52Zm-155-3.33c-2.72-1.11-6.63-3.59-5.68-3.6a2.29 2.29 0 0 0 1.37-.62 5.37 5.37 0 0 1 2.71-.7 43 43 0 0 0 4.71-.56c1.44-.25 2.72-.3 2.85-.1a8 8 0 0 0 2 1.22c1.36.64 1.64 1 1.13 1.29s-.43.44.38.45a13.94 13.94 0 0 1 4 1.52l2.9 1.51-6.7-.23c-3.89-.14-6.28-.06-5.71.18.85.36.78.42-.49.43a11.15 11.15 0 0 1-3.47-.79Zm.49-1.39a12 12 0 0 0-2.48-.7c-.9 0 2.46 1.38 3.47 1.4.59-.01.14-.33-.95-.72Zm-3.47-1.23a1.1 1.1 0 0 0-.8-.5c-.28 0-.36.23-.19.5a1.1 1.1 0 0 0 .8.5c.31-.02.4-.25.23-.52Zm7.07-.62c-.24-.74-2.36-1.15-2.36-.45 0 .53.82 1 1.9 1a.42.42 0 0 0 .5-.55Zm.74-1.71a7 7 0 0 0-2.23 0c-.61.12-.11.21 1.12.21s1.77-.11 1.15-.23Zm204.27 5c0-.56 2-2.35 2.28-2.05.11.11-.36.72-1 1.36s-1.24.94-1.24.68Zm-170.03-4.46a1.65 1.65 0 0 1-.48-1.29c.1-.3.57 0 1 .76.96 1.4.57 1.77-.52.53Zm159.65 0c0-.42.23-.63.5-.46a1.07 1.07 0 0 1 .5.77.49.49 0 0 1-.5.47c-.27 0-.5-.35-.5-.78Zm-158.44-1c-.48-1.24-.34-1.56.25-.56.29.5.42 1 .29 1.14s-.38-.11-.54-.56Zm-43-.31c0-.23.67-.41 1.48-.41a3.17 3.17 0 0 1 1.49.19 3.31 3.31 0 0 1-1.49.41c-.78.14-1.45.05-1.45-.17Zm40.18-.88a.51.51 0 0 1 .5-.52c.27 0 .49.1.49.21s-.22.36-.49.53-.47.14-.47-.2Zm192.74-1.52a1 1 0 0 1 .77-.49.49.49 0 0 1 .47.49c0 .28-.35.5-.78.5s-.6-.2-.43-.48Zm26.79-1.48a1 1 0 0 1 .77-.5.49.49 0 0 1 .47.5c0 .27-.35.49-.78.49s-.6-.2-.43-.47Zm-259.71-.5c-.45-.29-.51-.48-.16-.49a1.17 1.17 0 0 1 .9.49c.42.63.24.63-.71.02Zm158.25 0a1.05 1.05 0 0 1 .5-.77c.27-.17.5 0 .5.46s-.23.78-.5.78a.49.49 0 0 1-.47-.42Zm79.17 0c-.21-.35-.25-.76-.09-.93.44-.45.93.21.69.93-.14.6-.26.6-.57.09Zm-215.91-1.39c-.85-.82-1.44-1.61-1.29-1.75s.58.05 1 .45a8.31 8.31 0 0 0 1.49 1.14 1 1 0 0 1 .58 1c-.13.41-.8.09-1.78-.84Zm219 .55c-.18-.29.23-.88.89-1.32 1.23-.8 2.74-1.11 2-.42a1.34 1.34 0 0 1-.8.37c-.24 0-.74.42-1.13.94s-.81.74-1 .43Zm24.47-2.13c0-.23.51-.57 1.12-.76a4 4 0 0 0 1.37-.66 10.79 10.79 0 0 1 4.16-2c.44 0 .8-.25.8-.55s.38-.46.86-.33a4.75 4.75 0 0 0 2.36-.42l1.49-.64-1.49-.05c-1.43-.05-1.45-.07-.33-.52a2.43 2.43 0 0 1 1.58-.21.54.54 0 0 0 .72-.23.81.81 0 0 1 .94-.23 1.3 1.3 0 0 0 1.29-.52 2.36 2.36 0 0 1 1.69-.72c.73 0 .83.12.31.32a2.87 2.87 0 0 0-1 .6 9.88 9.88 0 0 1-2.1 1.19 21.83 21.83 0 0 0-4.13 2.83c-2.1 1.82-2.8 2.19-3.85 2a19.73 19.73 0 0 0-3.1.64c-1.51.36-2.73.49-2.73.26Zm-61-1c0-.27.34-.5.75-.5s.74.23.74.5-.33.5-.74.5-.81-.21-.81-.48Zm46.64-.2a1.19 1.19 0 0 1 .87-1 11.17 11.17 0 0 0 2.1-1.11c.69-.45.86-.49.38-.09a2.39 2.39 0 0 0-.87 1.22c0 .27-.56.74-1.24 1-1.05.56-1.3.54-1.3.01Zm-241.39-1.53c-1.23-.94-.77-.93 1 0 .78.41 1.08.74.67.73a3.75 3.75 0 0 1-1.67-.73Zm96-1.23c0-.27.5-.49 1.12-.48.9 0 1 .1.37.48-.99.63-1.56.63-1.56 0Zm-122.84-1.74c-.91-.71-.88-.73.52-.41.82.18 2 .5 2.73.71 1.11.33 1.06.37-.53.41a5.06 5.06 0 0 1-2.72-.71Zm14.16.29a.52.52 0 0 1 .5-.53c.27 0 .5.1.5.22s-.23.36-.5.52-.5.03-.5-.21Zm200.92 0c0-.28.34-.5.75-.5s.74.22.74.5-.33.49-.74.49-.75-.25-.75-.52Zm-205.88-1c0-.27.1-.49.22-.49s.36.22.53.49.07.5-.22.5a.51.51 0 0 1-.53-.53Zm-11.9-1c-.64-.41-.62-.48.12-.48.48 0 .87.21.87.48 0 .59-.06.59-.99-.02Zm303.11-1c0-.27.61-.48 1.37-.46 1.2 0 1.25.08.37.46-1.38.57-1.74.57-1.74-.01ZM29 169.26c-.41-.26-.52-.48-.25-.48a3 3 0 0 1 1.24.48c.41.27.53.48.25.48a2.92 2.92 0 0 1-1.24-.48Zm123.81-2.83c-.27-.71-.21-.76.28-.27.33.33.49.73.33.88s-.42-.12-.59-.61Zm-2.6-1.43a1 1 0 0 1 .94 0c.4.16.29.28-.3.3s-.81-.09-.64-.26Zm69.44-1.54c-1-.35-.73-1.13.37-1.13.55 0 1 .21 1 .46-.02.63-.62.93-1.37.66Zm3.6-2.62c-.17-.28-.07-.5.21-.5a.51.51 0 0 1 .53.5c0 .27-.1.49-.22.49s-.36-.23-.52-.5Zm36.83-.35c-1.5-.39-1-1.28.57-1 1.28.26 1.54.09 2.26-1.42.59-1.25 1.14-1.72 2-1.72a2.05 2.05 0 0 1 1.53.55c.22.37 0 .44-.72.22s-1.05-.13-1.05.44-.22.78-.5.78-.49.33-.49.74a.76.76 0 0 1-.75.75c-.41 0-.74.22-.74.49 0 .51-.59.56-2.11.15Zm-113.48-2.64a.5.5 0 1 1 .49.5.5.5 0 0 1-.49-.5Zm-1.17-3.22c-.16-.41-.07-.75.19-.75s.48.34.48.75-.09.74-.2.74-.32-.33-.47-.74Zm85.51-5.91c0-.71.22-1.29.49-1.29s.5-.61.5-1.36c0-1 .13-1.18.47-.64s.64.61 1.68.06a2.83 2.83 0 0 1 1.89-.38c.48.19.45.27-.14.3a8.09 8.09 0 0 0-2.85 2.32c-1.95 2.18-2 2.22-2 1Zm-1.49-.54c0-.68.08-1.24.19-1.24s.29.56.42 1.24.05 1.24-.19 1.24-.42-.56-.42-1.24Zm35.22.62a12.24 12.24 0 0 1 5.1-2.3c1 .05 1 .08.11.3a4.66 4.66 0 0 0-1.74.9c-.7.61-4 1.66-3.47 1.1Zm6.45-3.35c-.41-.26-.52-.48-.25-.48a3.06 3.06 0 0 1 1.24.48c.41.27.52.48.25.48a2.92 2.92 0 0 1-1.24-.48Zm-175.72-2c-1.54-1.25-.93-1.69 1-.73 1.78.89 1.85 1 .85 1.38a2.41 2.41 0 0 1-1.85-.62Zm38.31.12c-.65-.73-.63-.78.17-.48a5.91 5.91 0 0 0 1.1.34c.11 0 .21.23.21.5 0 .76-.65.61-1.48-.33Zm98.5.42c-.34-.55 1.54-1.08 2.49-.71.38.14.57.46.42.7-.35.6-2.56.61-2.91.02Zm-94.5-.76c0-.53.09-.82.26-.65a1 1 0 0 1 0 .95c-.12.47-.24.31-.26-.27Zm96.45-1.23c1.35-.58 1.74-.58 1.74 0 0 .27-.62.48-1.37.46-1.21-.05-1.25-.11-.37-.46Zm32.49-3.45a1.07 1.07 0 0 1 .5-.77c.27-.17.5 0 .5.46s-.23.78-.5.78a.49.49 0 0 1-.5-.49ZM239.1 137c-.18-.29-.22-.63-.09-.76s.37.1.53.53c.34.85.05 1-.44.23Zm1.76-1.7c0-.68.22-1.24.49-1.24s.5.56.5 1.24-.22 1.24-.5 1.24-.49-.54-.49-1.26Zm30.26 0c0-.52.95-.75 1.23-.28.17.27 0 .5-.46.5s-.77-.09-.77-.21Zm-31.71-1c0-.53.09-.82.26-.65a1 1 0 0 1 0 .95c-.16.4-.28.28-.3-.3Zm-33.28-.15a5.48 5.48 0 0 1 1.24-1.73 6 6 0 0 0 1.26-1.74 12.86 12.86 0 0 1 1-2.38c1.3-2.58.88-.36-.55 2.88-.87 2.04-2.95 4.12-2.95 3Zm36.71-1.63c0-.27.1-.5.22-.5s.36.23.53.5.07.5-.22.5a.51.51 0 0 1-.53-.47Zm-81.36-1.49a.5.5 0 0 1 1 0 .5.5 0 1 1-1 0Zm85.1-3.93c-.26-.42 1.9-3 2.52-3 .42 0-.39 2.13-1.08 2.83s-1.02.82-1.44.2Zm-82.89-2c-.18-.29-.22-.64-.09-.77s.37.11.53.53c.34.85.05 1.01-.44.22Zm83.66-1.17c0-.52.09-.82.25-.65a1 1 0 0 1 0 .95c-.15.4-.27.28-.29-.3Zm-157.63-1c-.47-1.2.18-1.42.79-.27.39.71.4 1 0 1s-.63-.38-.79-.8Zm-2.41-2.22c-.39-.62.27-.62 1.24 0 .64.42.63.48-.09.49a1.43 1.43 0 0 1-1.15-.56Zm79.13-.51c0-.64 1.42-1.21 1.87-.76.18.19 0 .54-.39.8-.92.54-1.48.52-1.48-.11Zm84.38-.19c0-.52.09-.81.26-.65a1 1 0 0 1 0 .95c-.16.4-.28.28-.3-.3Zm-160.65-3.8c-1.23-1-1.42-1.52-.58-1.52s3 1.8 2.59 2.17-.79.37-2.01-.65Zm22.71.48c-.4-.25-.58-.61-.4-.79.46-.46 1.87.12 1.87.76s-.51.64-1.47.03ZM259.21 112c0-.52 1-.74 1.24-.28.17.28 0 .5-.46.5s-.78-.11-.78-.22Zm21.35-.61a2.35 2.35 0 0 1 .7-1.31c.4-.4.59-.86.42-1s-.05-.3.27-.3a.53.53 0 0 1 .58.44c0 .64-1.95 2.78-1.97 2.16Zm-111.64-1.65c0-.27.34-.5.75-.5s.74.23.74.5-.34.5-.74.5-.75-.24-.75-.51Zm-68-1.49c0-.27.33-.5.74-.5s.75.23.75.5-.34.5-.75.5-.66-.24-.66-.51Zm-18.37-.75c-.93-1.12-.39-1.25.72-.18.52.51.72.93.43.93a2 2 0 0 1-1.11-.76Zm20.6-.24a1.1 1.1 0 0 1 .8-.5c.28 0 .36.23.19.5a1.09 1.09 0 0 1-.8.49c-.23-.01-.34-.23-.15-.5ZM99.6 107a1 1 0 0 1 1 0c.4.16.28.28-.3.3s-.86-.19-.7-.3Zm162.59-.69c0-.28.1-.5.22-.5s.36.22.52.5.07.49-.21.49a.51.51 0 0 1-.53-.54Zm-157.83-1.12c.37-.37.73-.4 1.06-.08s.12.52-.57.57-.93-.11-.49-.54Zm63.86-.3c-.21-.35-.25-.77-.09-.93.44-.45.93.21.68.93-.16.45-.28.45-.59-.05Zm100.78-1.4c-.3-1.54.75-2.14 1.85-1s1 1.38 0 1c-.47-.18-.74 0-.74.51 0 1.28-.86.92-1.12-.48Zm-102.07-1c0-.43.22-.64.49-.47a1.06 1.06 0 0 1 .5.77.48.48 0 0 1-.5.47c-.26.02-.48-.33-.48-.75Zm-.4-2.45c0-.82.09-1.22.22-.88a2.75 2.75 0 0 1 0 1.49c-.09.5-.2.22-.21-.59Zm102.26.47c0-.4.19-.72.41-.72a.45.45 0 0 1 .42.46 1 1 0 0 1-.42.73c-.2.16-.4-.05-.4-.45Zm12-4.32c2.6-3.07 3-3.41 3.37-2.57s-1.41 2.65-2.55 2.68a.59.59 0 0 0-.62.51.55.55 0 0 1-.57.5c-.28.02-.11-.47.42-1.1Zm-75.63-.37c0-.68.12-1 .26-.62a2 2 0 0 1 0 1.24c-.09.36-.21.08-.21-.6Zm-5.45-1c0-.68.11-1 .25-.62a1.88 1.88 0 0 1 0 1.24c-.09.37-.2.09-.2-.59Zm75.16.66a1.37 1.37 0 0 1-.33-.84c0-.32.23-.29.59.07s.47.7.33.85a.45.45 0 0 1-.54-.05ZM81.15 93.05c-1.3-1.66-1.32-2.36-.05-1.68a4.54 4.54 0 0 1 2 2.7c0 .7-1.01.17-1.95-1.02Zm198.79.18c-.7-.55-.61-.79.91-2.17 1.8-1.64 1.83-1.54.63 1.68-.48 1.3-.5 1.31-1.54.49Zm4.57-.86c0-.82.22-1.49.48-1.49s.43.67.37 1.49c-.14 1.93-.85 1.94-.85 0ZM200 90.17a2.46 2.46 0 0 1 .63-1.87 6.85 6.85 0 0 0 1.15-2.14c.19-.74.27-.65.31.4.05 1.21-1.12 4.32-1.64 4.32-.09 0-.3-.32-.45-.71Zm-120.55-.53a1.16 1.16 0 0 1 1-.75.73.73 0 0 1 .69.75.88.88 0 0 1-1 .74c-.67 0-.89-.24-.69-.74Zm203.85-1.39c.19-.49.47-.76.62-.61s0 .55-.34.89-.58.47-.28-.28Zm-205.39-.31a.57.57 0 0 1 .22-.78c.27-.17.5 0 .5.46 0 .88-.29 1-.72.32Zm91-2.52a.5.5 0 1 1 .5.5.51.51 0 0 1-.49-.5Zm-90.78-1c0-.27.1-.5.21-.5s.36.23.53.5.07.49-.22.49a.5.5 0 0 1-.51-.48Zm123.69-.28a1.37 1.37 0 0 1 .26-1.34c.42-.42.57-.22.57.77.01 1.51-.36 1.76-.82.58Zm-121.2-.21a.5.5 0 0 1 1 0 .5.5 0 1 1-1 0Zm93.11-2.35c-.38-.69-.92-3.35-.62-3s1.27 3.42 1 3.42-.28-.17-.4-.37ZM79.13 80c0-.27.1-.49.22-.49s.35.22.52.49.07.5-.22.5a.51.51 0 0 1-.52-.5Zm99.59-7.38c-1.2-1.22-1.09-1.62.29-1 .63.29 1.15.85 1.15 1.23 0 .91-.37.85-1.44-.23Zm16-5.07a1.86 1.86 0 0 1 .37-1.15 1.55 1.55 0 0 0 .42-.7 9.33 9.33 0 0 1 .62-1.57c.53-1.15.55-1.11.26.5-.39 1.9-1.67 4.21-1.67 2.88Zm-2.23-23c0-1.5.22-2.81.49-2.91s.48.67.45 1.85c-.09 3.24-.17 3.78-.57 3.78-.21-.05-.37-1.27-.37-2.77ZM190.55 42c-.45-1.17-.34-1.54.2-.7.39.6.48.54.49-.34a8.15 8.15 0 0 0-.46-2.32c-.42-1.1-.36-1.22.54-1s1 0 .79-.72a2.29 2.29 0 0 1 .18-1.59c.29-.46.43-.18.44.9a2.8 2.8 0 0 1-.49 1.89 4.39 4.39 0 0 0-.5 2.51c0 2.11-.61 2.84-1.19 1.37Z" + transform="translate(-15.35 -23.13)" + style={{ + fill: '#303030', + fillOpacity: 0.38999998569488525, + }} + /> + </svg> +); + +export default HighStake; diff --git a/src/config/validators/Metaspan.tsx b/src/config/validators/Metaspan.tsx new file mode 100644 index 0000000000..f56a3b4e7b --- /dev/null +++ b/src/config/validators/Metaspan.tsx @@ -0,0 +1,28 @@ +const Metaspan = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> + <path + style={{ + fill: '#fff', + }} + d="M0 0h512v512H0z" + /> + <path + d="M251.47 64.72c-107.7.55-194.55 88.3-194 196s88.3 194.55 196 194 194.55-88.3 194-196c-.54-106.91-87.08-193.45-194-194Z" + style={{ + strokeWidth: 26, + fill: 'none', + stroke: '#010101', + }} + /> + <path + d="M252.47 64.72v390m195-195h-390m44-120a260 260 0 0 0 302 0m0 240a260 260 0 0 0-302 0m136-310C132 158.8 118.68 316.53 207.77 422a250.18 250.18 0 0 0 29.7 29.7m30 0C373 362.63 386.25 204.9 297.16 99.41a250 250 0 0 0-29.69-29.69" + style={{ + strokeWidth: 18, + fill: 'none', + stroke: '#010101', + }} + /> + </svg> +); + +export default Metaspan; diff --git a/src/config/validators/PDP.tsx b/src/config/validators/PDP.tsx new file mode 100644 index 0000000000..551d823215 --- /dev/null +++ b/src/config/validators/PDP.tsx @@ -0,0 +1,16 @@ +const PDP = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> + <path + d="M0 0h512m0 512H0M0 0h512v512H0z" + style={{ + fill: '#fff', + }} + /> + <path + d="M0 256v256h512V0H0zm293.4-134.15c8.9 7.6 12 15.88 11.13 31.53l-.67 13.19-62.77 1.12c-61.67 1.11-63 1.11-67.9 6.26-5.12 4.91-5.12 6.26-6.23 68.19l-1.12 63.05L150 305c-11.35 0-17.81-1.11-22-4-11.8-7.83-12.47-12.52-11.8-94.13l.67-74.45 5.56-6c3.12-3.35 7.57-6.93 10-7.82 2.45-1.12 38.29-2 79.47-2l74.8-.23zm90.38 89.43c11.8 7.83 12.46 12.3 12.46 90.55 0 78.93-.44 82.51-13.13 90.11-5.79 3.35-14.69 3.8-81.48 3.8-82.36 0-84.59-.23-91.26-13.64-2.45-4.47-3.34-11.4-2.9-21.69l.67-15 63.44-1.12c34.95-.67 63.67-1.56 64.11-1.78.23-.23 2.45-3.81 4.9-7.83 4.23-6.71 4.45-11.18 4.45-66.63 0-32.64.67-60.14 1.78-61 2.68-2.92 30.95.43 36.96 4.23zM305 246.83c0 36.67-.22 39.13-5.12 46.28-7.56 11.41-16.47 13.42-57 12.75l-34.73-.67-.67-38c-.67-40.69.67-47.18 10.24-54.78 4.9-4 9.13-4.47 46.3-4.47h41z" + fill="#010101" + /> + </svg> +); + +export default PDP; diff --git a/src/config/validators/Paranodes.tsx b/src/config/validators/Paranodes.tsx new file mode 100644 index 0000000000..f62ee2d88e --- /dev/null +++ b/src/config/validators/Paranodes.tsx @@ -0,0 +1,18 @@ +const Paranodes = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> + <path + d="M0 0h512m0 512H0M0 0h512v512H0z" + style={{ + fill: '#fff', + }} + /> + <path + d="M34.71 289.6v-43.53h35.65a7.68 7.68 0 0 1 4 1.09 8.16 8.16 0 0 1 2.87 2.84 7.47 7.47 0 0 1 1.06 3.9v12.23a7.61 7.61 0 0 1-1.06 4 7.93 7.93 0 0 1-6.87 3.9l-29.11.06v15.5zm8-22.16h27.53a1.4 1.4 0 0 0 1-.42 1.41 1.41 0 0 0 .42-1v-11.89a1.5 1.5 0 0 0-.42-1.06 1.33 1.33 0 0 0-1-.46H42.76a1.56 1.56 0 0 0-1.51 1.52V266a1.36 1.36 0 0 0 .45 1 1.51 1.51 0 0 0 1.06.44zm40.36-13.56a7.67 7.67 0 0 1 1.06-4 8.1 8.1 0 0 1 2.87-2.81 7.67 7.67 0 0 1 4-1.06h27.79a7.83 7.83 0 0 1 4 1.06 8 8 0 0 1 2.88 2.85 7.67 7.67 0 0 1 1.06 4v35.68h-6.6v-15.26H89.61v15.26h-6.54zm37 13.93v-13.75a1.49 1.49 0 0 0-.42-1.05 1.35 1.35 0 0 0-1-.46H91.12a1.57 1.57 0 0 0-1.51 1.51v13.75zm13.49 21.79v-43.53h35.65a7.7 7.7 0 0 1 4 1.09 8.24 8.24 0 0 1 2.87 2.88 7.47 7.47 0 0 1 1.06 3.9v12.23a7.61 7.61 0 0 1-1.06 4 7.89 7.89 0 0 1-2.87 2.84 7.77 7.77 0 0 1-4 1.06l-29.12.06v15.5zm8.05-22.16h27.48a1.43 1.43 0 0 0 1.45-1.45v-11.86a1.5 1.5 0 0 0-.42-1.06 1.34 1.34 0 0 0-1-.46h-27.51a1.42 1.42 0 0 0-1.06.46 1.45 1.45 0 0 0-.46 1.06V266a1.37 1.37 0 0 0 .46 1 1.5 1.5 0 0 0 1.06.44zm27.66 22.16-13.62-16.29h8.54l12.71 15.08v1.21zm14.35-35.72a7.58 7.58 0 0 1 1.06-4 8 8 0 0 1 2.84-2.85 7.68 7.68 0 0 1 4-1.06h27.78a7.9 7.9 0 0 1 4 1.06 8 8 0 0 1 2.87 2.85 7.58 7.58 0 0 1 1.06 4v35.72h-6.6v-15.26h-30.48v15.26h-6.53zm37 13.93v-13.75a1.49 1.49 0 0 0-.42-1.05 1.34 1.34 0 0 0-1-.46h-27.53a1.42 1.42 0 0 0-1.06.46 1.45 1.45 0 0 0-.46 1.05v13.75zm13.48 21.79V246h7.81l29.18 34.75V246h6.59v43.6h-7.8l-29.24-34.81v34.81zm58.11 0a7.58 7.58 0 0 1-4-1.06 8 8 0 0 1-2.85-2.85 7.58 7.58 0 0 1-1.06-4v-27.81a7.58 7.58 0 0 1 1.06-4 8 8 0 0 1 2.85-2.85 7.67 7.67 0 0 1 4-1.06h27.84a7.68 7.68 0 0 1 4 1.06 8 8 0 0 1 2.84 2.85 7.58 7.58 0 0 1 1.06 4v27.85a7.58 7.58 0 0 1-1.06 4 8 8 0 0 1-2.84 2.85 7.59 7.59 0 0 1-4 1.06zm.18-6.54h27.48a1.35 1.35 0 0 0 1-.45 1.54 1.54 0 0 0 .43-1.06v-27.49a1.5 1.5 0 0 0-.43-1.05 1.33 1.33 0 0 0-1-.46h-27.48a1.56 1.56 0 0 0-1.51 1.51v27.49a1.48 1.48 0 0 0 .45 1.06 1.46 1.46 0 0 0 1.06.45zm42.31 6.54V246h35.66a7.83 7.83 0 0 1 4 1.06 8 8 0 0 1 2.88 2.85 7.67 7.67 0 0 1 1.06 4v27.85a7.67 7.67 0 0 1-1.06 4 8 8 0 0 1-2.88 2.85 7.74 7.74 0 0 1-4 1.06zm8-6.54h27.54a1.35 1.35 0 0 0 1-.45 1.54 1.54 0 0 0 .43-1.06v-27.49a1.5 1.5 0 0 0-.43-1.05 1.33 1.33 0 0 0-1-.46h-27.55a1.35 1.35 0 0 0-1 .46 1.49 1.49 0 0 0-.42 1.05v27.49a1.53 1.53 0 0 0 .42 1.06 1.38 1.38 0 0 0 1 .45zm42.49 6.54V246h39.95v6.54h-33.36v12h26.82v6.53h-26.82v12h33.36v6.54zm53.87 0a7.61 7.61 0 0 1-4-1.06 8 8 0 0 1-2.84-2.85 7.58 7.58 0 0 1-1.06-4v-2.54h6.54v2.36a1.48 1.48 0 0 0 .45 1.06 1.46 1.46 0 0 0 1.06.45h27.48a1.36 1.36 0 0 0 1-.45 1.53 1.53 0 0 0 .42-1.06v-9a1.47 1.47 0 0 0-.42-1.09 1.42 1.42 0 0 0-1-.43h-27.63a7.7 7.7 0 0 1-4-1.05 8 8 0 0 1-2.84-2.85 7.58 7.58 0 0 1-1.06-4v-9.33a7.58 7.58 0 0 1 1.06-4 8 8 0 0 1 2.84-2.85 7.71 7.71 0 0 1 4-1.06h27.84a7.68 7.68 0 0 1 4 1.06 8 8 0 0 1 2.84 2.85 7.58 7.58 0 0 1 1.06 4v2.55h-6.6v-2.37a1.49 1.49 0 0 0-.42-1.05 1.34 1.34 0 0 0-1-.46h-27.54a1.56 1.56 0 0 0-1.51 1.51v9a1.42 1.42 0 0 0 .45 1.09 1.48 1.48 0 0 0 1.06.43h27.66a7.59 7.59 0 0 1 4 1.06 7.89 7.89 0 0 1 2.84 2.84 7.59 7.59 0 0 1 1.06 4v9.32a7.58 7.58 0 0 1-1.06 4 8 8 0 0 1-2.84 2.85 7.59 7.59 0 0 1-4 1.06z" + style={{ + fill: '#231f20', + }} + /> + </svg> +); + +export default Paranodes; diff --git a/src/config/validators/PioneerStake.tsx b/src/config/validators/PioneerStake.tsx new file mode 100644 index 0000000000..cb8ab2035e --- /dev/null +++ b/src/config/validators/PioneerStake.tsx @@ -0,0 +1,67 @@ +const PionerStake = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + xmlnsXlink="http://www.w3.org/1999/xlink" + viewBox="0 0 375 375" + > + <defs> + <symbol id="glyph0-1" data-name="glyph0-1" viewBox="0 0 52.92 63.22"> + <path + fill="#222" + d="M26 63.22a34.93 34.93 0 0 0 14.33-2.72 20.74 20.74 0 0 0 9.34-7.77 21.63 21.63 0 0 0 3.25-12 21.82 21.82 0 0 0-3.25-12A20.68 20.68 0 0 0 40.33 21 35.1 35.1 0 0 0 26 18.25H11.75V0H0v63.22zm-.55-35q7.68 0 11.65 3.25t4 9.31c0 4-1.32 7.13-4 9.3s-6.53 3.25-11.65 3.25h-13.7V28.17z" + /> + </symbol> + <symbol id="glyph0-2" data-name="glyph0-2" viewBox="0 0 54.02 63.22"> + <path + fill="#222" + d="M41.38 0 28.45 18.52a20.83 20.83 0 0 0-2.43-.1H11.75V0H0v63.22h26a34.93 34.93 0 0 0 14.33-2.72 20.74 20.74 0 0 0 9.34-7.77 21.63 21.63 0 0 0 3.25-12 21.44 21.44 0 0 0-3.48-12.29 20.66 20.66 0 0 0-10-7.67L54 0zm-.29 40.73c0 4-1.32 7.13-4 9.3s-6.53 3.25-11.65 3.25H11.75V28.09h13.72q7.68 0 11.65 3.3t3.97 9.34z" + /> + </symbol> + <symbol id="glyph0-3" data-name="glyph0-3" viewBox="0 0 59.61 65.03"> + <path + fill="#222" + d="M47.69 33.23H58.8V8.05a35.07 35.07 0 0 0-11.38-6A43.79 43.79 0 0 0 34.05 0a36.06 36.06 0 0 0-17.43 4.2 31.41 31.41 0 0 0-12.2 11.61A32 32 0 0 0 0 32.52a32 32 0 0 0 4.42 16.7 31.27 31.27 0 0 0 12.25 11.61A36.49 36.49 0 0 0 34.23 65a38.3 38.3 0 0 0 14.46-2.62 28.32 28.32 0 0 0 10.92-7.68l-7.41-7.2a23.72 23.72 0 0 1-17.42 7.23 24.76 24.76 0 0 1-11.89-2.79 20.4 20.4 0 0 1-8.12-7.86 22.51 22.51 0 0 1-2.94-11.56 22.21 22.21 0 0 1 2.94-11.4 21 21 0 0 1 8.12-7.93 23.85 23.85 0 0 1 11.8-2.89 25.4 25.4 0 0 1 13 3.25z" + /> + </symbol> + <clipPath id="clip-path"> + <path fill="none" d="M27.05 114.83h76.77v76.94H27.05z" /> + </clipPath> + <clipPath id="clip-path-2"> + <path fill="none" d="M271.17 183.22h76.52v76.95h-76.52z" /> + </clipPath> + </defs> + <g id="surface1"> + <path fill="#fff" d="M0 0h375v375H0z" /> + <g clipPath="url(#clip-path)"> + <path fill="#ff66c4" d="M30.21 190.89h-3.16v-76.35h76.42v3.15H30.21z" /> + </g> + <g clipPath="url(#clip-path-2)"> + <path fill="#ff66c4" d="M347.7 260.16h-76.42V257h73.27v-73.2h3.15z" /> + </g> + <use + width={52.92} + height={63.22} + transform="matrix(1.22 0 0 -1.22 61.91 233.73)" + xlinkHref="#glyph0-1" + /> + <use + width={54.02} + height={63.22} + transform="matrix(1.22 0 0 -1.22 156.32 233.73)" + xlinkHref="#glyph0-2" + /> + <use + width={59.61} + height={65.03} + transform="matrix(1.22 0 0 -1.22 245.76 234.83)" + xlinkHref="#glyph0-3" + /> + <use + transform="matrix(1.22 0 0 -1.22 340.39 233.73)" + xlinkHref="#glyph0-4" + /> + </g> + </svg> +); + +export default PionerStake; diff --git a/src/config/validators/Polkachu.tsx b/src/config/validators/Polkachu.tsx new file mode 100644 index 0000000000..8385fa8719 --- /dev/null +++ b/src/config/validators/Polkachu.tsx @@ -0,0 +1,18 @@ +const Polkachu = () => ( + <svg + viewBox="0 0 44.426 44.424" + xmlSpace="preserve" + xmlns="http://www.w3.org/2000/svg" + > + <path + fill="#7c3aed" + d="M28.272 25.817c1.006-.551 1.754-1.306 2.24-2.268.486-.96.73-2.065.73-3.314 0-1.247-.244-2.346-.73-3.296-.486-.949-1.236-1.691-2.25-2.223-1.013-.533-2.314-.799-3.905-.799h-5.839V26.64h5.873c1.58 0 2.873-.274 3.881-.823z" + /> + <path + fill="#7c3aed" + d="M22.213 0C9.945 0 0 9.945 0 22.213c0 7.366 3.588 13.892 9.109 17.933.264.103.544.171.844.171a2.364 2.364 0 0 0 2.363-2.363l.004-2.257V8.594h13.014c2.702 0 4.972.504 6.809 1.511 1.838 1.008 3.229 2.39 4.173 4.146.944 1.757 1.417 3.752 1.417 5.983 0 2.256-.476 4.259-1.426 6.011-.95 1.75-2.354 3.127-4.207 4.128-1.853 1.001-4.138 1.502-6.851 1.502h-6.606l-.014 9.086a3.59 3.59 0 0 0 2.559 3.437c.34.016.682.026 1.025.026 12.269 0 22.213-9.944 22.213-22.213C44.426 9.945 34.482 0 22.213 0Z" + /> + </svg> +); + +export default Polkachu; diff --git a/src/config/validators/Polkadotters.tsx b/src/config/validators/Polkadotters.tsx new file mode 100644 index 0000000000..492ed7d8d3 --- /dev/null +++ b/src/config/validators/Polkadotters.tsx @@ -0,0 +1,59 @@ +const Polkadotters = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + x={0} + y={0} + viewBox="0 0 512 512" + xmlSpace="preserve" + > + <g transform="translate(-1200)"> + <circle fill="#fdebf3" fillOpacity={0.33} cx={1286.7} cy={105.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.33} cx={1261.7} cy={155.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.33} cx={1248.7} cy={205.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.33} cx={1261.7} cy={255.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.33} cx={1274.7} cy={305.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.33} cx={1261.7} cy={355.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.33} cx={1236.7} cy={405.8} r={20} /> + </g> + <g transform="translate(-1200)"> + <circle fill="#fdebf3" fillOpacity={0.66} cx={1336.7} cy={105.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.66} cx={1311.7} cy={155.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.66} cx={1298.7} cy={205.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.66} cx={1311.7} cy={255.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.66} cx={1324.7} cy={305.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.66} cx={1311.7} cy={355.8} r={20} /> + <circle fill="#fdebf3" fillOpacity={0.66} cx={1286.7} cy={405.8} r={20} /> + </g> + <g transform="translate(-1200)"> + <circle fill="#ee4699" cx={1386.7} cy={105.8} r={20} /> + <circle fill="#ee4699" cx={1361.7} cy={155.8} r={20} /> + <circle fill="#ee4699" cx={1348.7} cy={205.8} r={20} /> + <circle fill="#ee4699" cx={1361.7} cy={255.8} r={20} /> + <circle fill="#ee4699" cx={1374.7} cy={305.8} r={20} /> + <circle fill="#ee4699" cx={1361.7} cy={355.8} r={20} /> + <circle fill="#ee4699" cx={1336.7} cy={405.8} r={20} /> + </g> + <g transform="translate(-1200)"> + <circle fill="#bbb" cx={1491.7} cy={120.8} r={20} /> + <circle fill="#bbb" cx={1536.7} cy={105.8} r={20} /> + <circle fill="#bbb" cx={1586.7} cy={105.8} r={20} /> + <circle fill="#bbb" cx={1631.7} cy={121.8} r={20} /> + <circle fill="#bbb" cx={1461.7} cy={155.8} r={20} /> + <circle fill="#bbb" cx={1661.7} cy={155.8} r={20} /> + <circle fill="#bbb" cx={1446.7} cy={205.8} r={20} /> + <circle fill="#bbb" cx={1536.7} cy={205.8} r={20} /> + <circle fill="#bbb" cx={1675.7} cy={205.8} r={20} /> + <circle fill="#bbb" cx={1511.7} cy={255.8} r={20} /> + <circle fill="#bbb" cx={1661.7} cy={255.8} r={20} /> + <circle fill="#bbb" cx={1486.7} cy={305.8} r={20} /> + <circle fill="#bbb" cx={1536.7} cy={305.8} r={20} /> + <circle fill="#bbb" cx={1586.7} cy={305.8} r={20} /> + <circle fill="#bbb" cx={1631.7} cy={290.8} r={20} /> + <circle fill="#bbb" cx={1461.7} cy={355.8} r={20} /> + <circle fill="#bbb" cx={1436.7} cy={405.8} r={20} /> + </g> + <circle cx={336.7} cy={405.8} r={20} fill="#ed1e7f" /> + </svg> +); + +export default Polkadotters; diff --git a/src/config/validators/PythagorasCapitalInvestment.tsx b/src/config/validators/PythagorasCapitalInvestment.tsx new file mode 100644 index 0000000000..5d5f7ea44c --- /dev/null +++ b/src/config/validators/PythagorasCapitalInvestment.tsx @@ -0,0 +1,34 @@ +const PythagorasCapitalInvestment = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 481 481"> + <path + style={{ + fill: '#e5d3b0', + stroke: '#000', + }} + d="M.5.5h480v480H.5z" + /> + <path fill="none" stroke="#ff7f00" strokeWidth={12} d="M50.5 240.5h120" /> + <path fill="none" stroke="#ff7f00" strokeWidth={12} d="M310.5 240.5h120" /> + <g + style={{ + isolation: 'isolate', + }} + > + <path + fill="#ff7f00" + d="M119.2 299.41a3.56 3.56 0 0 0 2.28-.73 4.4 4.4 0 0 0 .46-2.52V282.3a3.35 3.35 0 0 0-.51-2.21 3.73 3.73 0 0 0-2.23-.67v-.59h8.44a10.37 10.37 0 0 1 6.22 1.58 4.9 4.9 0 0 1 2.16 4.15 5.14 5.14 0 0 1-2.4 4.74 11.21 11.21 0 0 1-6 1.47h-2.52v5.81c0 1.2.19 2 .56 2.27a4.94 4.94 0 0 0 2.48.61v.54h-9Zm10.8-18.8a8.57 8.57 0 0 0-3.44-.59 2.27 2.27 0 0 0-1.21.19 1.18 1.18 0 0 0-.23.87v8.34l1.13.1h.52a8 8 0 0 0 3.46-.62c1.54-.76 2.3-2.19 2.3-4.27a4 4 0 0 0-2.53-4.02ZM136.92 285.64h6.6v.44a4.3 4.3 0 0 0-1.16.14c-.48.14-.72.41-.72.82a1.42 1.42 0 0 0 .08.45 6.61 6.61 0 0 0 .28.7l3.62 7.82 3-7.89q0-.12.15-.57a3.14 3.14 0 0 0 .1-.61.68.68 0 0 0-.28-.61 1.46 1.46 0 0 0-.72-.22h-.48v-.44h4.3v.44a1.41 1.41 0 0 0-1 .45 3.2 3.2 0 0 0-.56 1l-4.92 13a20.19 20.19 0 0 1-2.45 4.89 3.63 3.63 0 0 1-3 1.52 3.25 3.25 0 0 1-1.64-.44 1.48 1.48 0 0 1-.77-1.39 1.26 1.26 0 0 1 .46-1 1.65 1.65 0 0 1 1.15-.4 3.81 3.81 0 0 1 1.19.3 4 4 0 0 0 1.14.29c.57 0 1.18-.7 1.82-2.12a9.07 9.07 0 0 0 .95-2.8 1.55 1.55 0 0 0 0-.34c0-.13-.07-.25-.1-.35L139 288.08a5.12 5.12 0 0 0-.93-1.53 2.14 2.14 0 0 0-1.18-.47ZM160.62 285.59v1.13h-3.18v9a5.94 5.94 0 0 0 .2 1.8 1.45 1.45 0 0 0 1.47 1.07 1.72 1.72 0 0 0 1-.26 4.89 4.89 0 0 0 .95-.85l.41.35-.35.47a4.88 4.88 0 0 1-1.72 1.54 3.78 3.78 0 0 1-1.75.46 2.47 2.47 0 0 1-2.5-1.64 6.82 6.82 0 0 1-.36-2.47v-9.47H153a.4.4 0 0 1-.12-.1.21.21 0 0 1 0-.12.36.36 0 0 1 .07-.24 3.49 3.49 0 0 1 .44-.4 13.29 13.29 0 0 0 1.56-1.45c.31-.37 1.06-1.34 2.24-2.93a.51.51 0 0 1 .24 0s0 .1 0 .23v3.84ZM161.66 299.56a2.72 2.72 0 0 0 1.68-.63 3.85 3.85 0 0 0 .39-2.18v-15a2.85 2.85 0 0 0-.22-1.29c-.15-.27-.52-.41-1.1-.41a1.88 1.88 0 0 0-.34 0l-.37.05v-.56l1.7-.49 1.4-.41 1.5-.47v9.7a10.29 10.29 0 0 1 1.75-1.66 5.17 5.17 0 0 1 3-.93 3.43 3.43 0 0 1 3.45 2.12 7.44 7.44 0 0 1 .5 3v6.37a4.08 4.08 0 0 0 .38 2.16 2.33 2.33 0 0 0 1.57.65v.42h-6.76v-.44a2.92 2.92 0 0 0 1.75-.64 3.79 3.79 0 0 0 .39-2.17v-6.33a5.1 5.1 0 0 0-.51-2.45 2 2 0 0 0-1.91-.94 3.79 3.79 0 0 0-2.36.88q-1.14.87-1.14 1.14v7.7a3.7 3.7 0 0 0 .4 2.19 3 3 0 0 0 1.74.62v.44h-6.89ZM180.81 293.11a34.27 34.27 0 0 1 5.74-2.55v-1.33a5.35 5.35 0 0 0-.32-2.21 2.21 2.21 0 0 0-2.2-1 2.91 2.91 0 0 0-1.51.41 1.28 1.28 0 0 0-.72 1.15 3.29 3.29 0 0 0 .08.63 4.41 4.41 0 0 1 .07.57 1.3 1.3 0 0 1-.57 1.22 1.43 1.43 0 0 1-.79.21 1.31 1.31 0 0 1-1.07-.46 1.6 1.6 0 0 1-.38-1 3.26 3.26 0 0 1 1.35-2.29 5.84 5.84 0 0 1 4-1.19c2 0 3.39.65 4.11 2a6.72 6.72 0 0 1 .58 3.14v6.26a3.92 3.92 0 0 0 .13 1.25.8.8 0 0 0 .84.61 1.27 1.27 0 0 0 .59-.11 5.84 5.84 0 0 0 .82-.53v.81a5.28 5.28 0 0 1-1.08 1 3.14 3.14 0 0 1-1.78.6 1.73 1.73 0 0 1-1.54-.69 3 3 0 0 1-.53-1.64 14.28 14.28 0 0 1-2 1.53 5.35 5.35 0 0 1-2.7.84 3.25 3.25 0 0 1-2.33-.94 3.2 3.2 0 0 1-1-2.4 4.44 4.44 0 0 1 2.21-3.89Zm5.74-1.77a14.4 14.4 0 0 0-3 1.32c-1.5.92-2.25 2-2.25 3.15a2.4 2.4 0 0 0 .94 2.1 2.24 2.24 0 0 0 1.35.43 3.78 3.78 0 0 0 2-.57 1.71 1.71 0 0 0 1-1.47ZM194.54 293.08a4.78 4.78 0 0 1-.79-2.7 5.29 5.29 0 0 1 5.56-5.11 7.17 7.17 0 0 1 2.52.53 7.16 7.16 0 0 0 2.64.54h2.19v1.26h-2.7c.18.44.33.82.43 1.14a6.07 6.07 0 0 1 .27 1.72 4.58 4.58 0 0 1-1.48 3.28 5.35 5.35 0 0 1-4 1.49 13.22 13.22 0 0 1-1.41-.14c-.3 0-.7.25-1.2.76a2.11 2.11 0 0 0-.75 1.25c0 .34.36.58 1.09.74a7.84 7.84 0 0 0 1.6.15 21 21 0 0 1 5.46.46 3 3 0 0 1 2.36 3.15q0 2.47-2.76 3.94A11.69 11.69 0 0 1 198 307a7.06 7.06 0 0 1-4.06-1 2.82 2.82 0 0 1-1.5-2.18 1.82 1.82 0 0 1 .4-1.1 11.85 11.85 0 0 1 1.56-1.6l1-.93.19-.18a4.51 4.51 0 0 1-1.07-.55 1.37 1.37 0 0 1-.62-1.11 2.15 2.15 0 0 1 .54-1.27 18.77 18.77 0 0 1 2.3-2.26 4.06 4.06 0 0 1-2.2-1.74Zm1.66 11.62a10.32 10.32 0 0 0 3.38.5 8.69 8.69 0 0 0 4.22-.9 2.57 2.57 0 0 0 1.62-2.21c0-.69-.44-1.16-1.31-1.39a16.7 16.7 0 0 0-3.09-.23h-1.22l-1.17-.05c-.23 0-.6-.06-1.11-.13s-.9-.14-1.15-.19-.43.33-.91 1a3.19 3.19 0 0 0-.74 1.84 1.84 1.84 0 0 0 1.48 1.76Zm5-10.73a3.18 3.18 0 0 0 .93-2.61 9 9 0 0 0-.75-3.34 2.61 2.61 0 0 0-2.53-1.86 2.11 2.11 0 0 0-2.11 1.45 5.22 5.22 0 0 0-.29 1.89 6.4 6.4 0 0 0 .92 3.42 2.73 2.73 0 0 0 2.39 1.52 2.24 2.24 0 0 0 1.4-.44ZM210.37 287.49a6.59 6.59 0 0 1 5.08-2.15 6.94 6.94 0 0 1 5.13 2 7.3 7.3 0 0 1 2 5.4 8.07 8.07 0 0 1-2 5.41 6.39 6.39 0 0 1-5.09 2.29 6.7 6.7 0 0 1-5.08-2.2 7.72 7.72 0 0 1-2.08-5.53 7.48 7.48 0 0 1 2.04-5.22Zm2.49-.4q-1.56 1.43-1.56 4.91a11.06 11.06 0 0 0 1.26 5.19 3.85 3.85 0 0 0 3.5 2.4 3 3 0 0 0 2.7-1.61 8.23 8.23 0 0 0 .94-4.21 11.22 11.22 0 0 0-1.2-5.1 3.79 3.79 0 0 0-3.49-2.39 3.13 3.13 0 0 0-2.15.81ZM223.72 299.48a3.77 3.77 0 0 0 1.9-.5 2.08 2.08 0 0 0 .47-1.64v-7a5.86 5.86 0 0 0-.29-2.21 1 1 0 0 0-1.05-.67h-.41l-.54.1v-.51l1.82-.64 1.29-.47c.52-.2 1.06-.42 1.61-.67.07 0 .11 0 .13.07a2.16 2.16 0 0 1 0 .33v2.56a12.27 12.27 0 0 1 1.94-2.17 3 3 0 0 1 1.93-.78 1.8 1.8 0 0 1 1.3.48A1.69 1.69 0 0 1 234 288a1.21 1.21 0 0 1-1 .44 1.8 1.8 0 0 1-1.2-.55 1.68 1.68 0 0 0-.94-.54c-.37 0-.81.29-1.34.88a2.67 2.67 0 0 0-.8 1.82v7a2.36 2.36 0 0 0 .62 1.87 3.09 3.09 0 0 0 2.08.49v.59h-7.7ZM237.67 293.11a34.27 34.27 0 0 1 5.74-2.55v-1.33a5.35 5.35 0 0 0-.32-2.21 2.22 2.22 0 0 0-2.2-1 2.91 2.91 0 0 0-1.51.41 1.28 1.28 0 0 0-.72 1.15 4 4 0 0 0 .07.63 3.5 3.5 0 0 1 .08.57 1.29 1.29 0 0 1-.58 1.22 1.4 1.4 0 0 1-.78.21 1.3 1.3 0 0 1-1.07-.46 1.55 1.55 0 0 1-.38-1 3.26 3.26 0 0 1 1.35-2.29 5.84 5.84 0 0 1 4-1.19c2 0 3.39.65 4.11 2a6.72 6.72 0 0 1 .58 3.14v6.26a4.34 4.34 0 0 0 .12 1.25.82.82 0 0 0 .85.61 1.27 1.27 0 0 0 .59-.11 5.84 5.84 0 0 0 .82-.53v.81a5.28 5.28 0 0 1-1.08 1 3.14 3.14 0 0 1-1.78.6 1.73 1.73 0 0 1-1.54-.69 3 3 0 0 1-.53-1.64 14.28 14.28 0 0 1-2 1.53 5.35 5.35 0 0 1-2.7.84 3.25 3.25 0 0 1-2.33-.94 3.17 3.17 0 0 1-1-2.4 4.44 4.44 0 0 1 2.21-3.89Zm5.74-1.77a14.4 14.4 0 0 0-3 1.32c-1.5.92-2.25 2-2.25 3.15a2.42 2.42 0 0 0 .93 2.1 2.29 2.29 0 0 0 1.36.43 3.78 3.78 0 0 0 2-.57 1.71 1.71 0 0 0 .95-1.47ZM250.11 295.08h.51a8.21 8.21 0 0 0 1 2.73 3.53 3.53 0 0 0 3.2 1.75 2.51 2.51 0 0 0 1.84-.65 2.19 2.19 0 0 0 .68-1.68 2.33 2.33 0 0 0-.39-1.26 4.05 4.05 0 0 0-1.37-1.19l-1.75-1a10 10 0 0 1-2.83-2.08 3.66 3.66 0 0 1-.91-2.47 3.71 3.71 0 0 1 1.25-2.87 4.53 4.53 0 0 1 3.14-1.13 6.08 6.08 0 0 1 1.82.32 8 8 0 0 0 1.12.31.84.84 0 0 0 .41-.08.69.69 0 0 0 .22-.25h.37l.11 4.36H258a7.11 7.11 0 0 0-.85-2.36 3.09 3.09 0 0 0-2.79-1.56 2.22 2.22 0 0 0-1.72.67 2.25 2.25 0 0 0-.63 1.58c0 1 .72 1.81 2.16 2.56l2.06 1.11q3.33 1.82 3.33 4.22a3.76 3.76 0 0 1-1.38 3 5.36 5.36 0 0 1-3.61 1.18 8.47 8.47 0 0 1-2.13-.32 10.59 10.59 0 0 0-1.4-.31.53.53 0 0 0-.33.13 1 1 0 0 0-.21.32h-.41ZM284.3 279a18.19 18.19 0 0 0 2 .51 1.57 1.57 0 0 0 .86-.25 1.3 1.3 0 0 0 .57-.78h.67l.29 7.19H288a10.28 10.28 0 0 0-1.67-3.45 6.41 6.41 0 0 0-5.3-2.47 6.7 6.7 0 0 0-5.49 2.66 11.36 11.36 0 0 0-2.13 7.31 10 10 0 0 0 2.24 6.83 7.21 7.21 0 0 0 5.68 2.56 9.18 9.18 0 0 0 4.58-1.19 13.77 13.77 0 0 0 2.64-2l.59.59a11.28 11.28 0 0 1-16.66.78 10.83 10.83 0 0 1-2.79-7.61 11.07 11.07 0 0 1 2.95-7.84 10.56 10.56 0 0 1 8.09-3.33 12.9 12.9 0 0 1 3.57.49ZM293.67 293.11a34.27 34.27 0 0 1 5.74-2.55v-1.33a5.35 5.35 0 0 0-.32-2.21 2.22 2.22 0 0 0-2.2-1 2.91 2.91 0 0 0-1.51.41 1.28 1.28 0 0 0-.72 1.15 4 4 0 0 0 .07.63 3.5 3.5 0 0 1 .08.57 1.29 1.29 0 0 1-.58 1.22 1.4 1.4 0 0 1-.78.21 1.3 1.3 0 0 1-1.07-.46 1.55 1.55 0 0 1-.38-1 3.26 3.26 0 0 1 1.35-2.29 5.84 5.84 0 0 1 4-1.19c2 0 3.39.65 4.11 2a6.72 6.72 0 0 1 .58 3.14v6.26a4.34 4.34 0 0 0 .12 1.25.82.82 0 0 0 .85.61 1.27 1.27 0 0 0 .59-.11 5.84 5.84 0 0 0 .82-.53v.81a5.28 5.28 0 0 1-1.08 1 3.14 3.14 0 0 1-1.78.6 1.73 1.73 0 0 1-1.54-.69 3 3 0 0 1-.53-1.64 14.28 14.28 0 0 1-2 1.53 5.35 5.35 0 0 1-2.7.84 3.25 3.25 0 0 1-2.33-.94 3.17 3.17 0 0 1-1-2.4 4.44 4.44 0 0 1 2.21-3.89Zm5.74-1.77a14.4 14.4 0 0 0-3 1.32c-1.5.92-2.25 2-2.25 3.15a2.42 2.42 0 0 0 .93 2.1 2.29 2.29 0 0 0 1.36.43 3.78 3.78 0 0 0 2-.57 1.71 1.71 0 0 0 1-1.47ZM304.59 306.45a2.54 2.54 0 0 0 1.86-.62 2.86 2.86 0 0 0 .39-1.67v-15a2.59 2.59 0 0 0-.29-1.53 1.39 1.39 0 0 0-1.08-.33h-.32l-.43.07v-.52l1.48-.48 3.14-1.08a.12.12 0 0 1 .12.07.27.27 0 0 1 0 .16v2.11a11.22 11.22 0 0 1 1.81-1.53 5.44 5.44 0 0 1 2.89-.88 4.72 4.72 0 0 1 3.71 1.84 7.49 7.49 0 0 1 1.56 5 9.29 9.29 0 0 1-1.87 5.66 5.61 5.61 0 0 1-4.69 2.55 4.88 4.88 0 0 1-1.52-.22 4.51 4.51 0 0 1-1.84-1.2V304c0 1.05.17 1.7.51 1.94a5.2 5.2 0 0 0 2.25.48v.58h-7.72Zm6.17-7.75a3.44 3.44 0 0 0 4.78-1.16 7.91 7.91 0 0 0 1.19-4.67 6.33 6.33 0 0 0-1.24-4.29 3.73 3.73 0 0 0-2.88-1.42 3.4 3.4 0 0 0-2.11.71 1.86 1.86 0 0 0-.95 1.39v7.85a3.15 3.15 0 0 0 1.21 1.59ZM321.06 299.56a3.89 3.89 0 0 0 2.11-.57c.29-.27.44-1 .44-2.24v-7.45a4.89 4.89 0 0 0-.14-1.41 1 1 0 0 0-1-.64h-.33l-.95.25v-.48l.68-.22c1.82-.6 3.09-1 3.81-1.35a2.11 2.11 0 0 1 .56-.18.85.85 0 0 1 0 .23v11.22a3.61 3.61 0 0 0 .43 2.23 3.16 3.16 0 0 0 2 .58v.47h-7.6Zm2.49-20.89a1.53 1.53 0 0 1 1.15-.48 1.59 1.59 0 0 1 1.15.47 1.57 1.57 0 0 1 .48 1.16 1.53 1.53 0 0 1-.48 1.14 1.56 1.56 0 0 1-1.15.48 1.53 1.53 0 0 1-1.15-.48 1.52 1.52 0 0 1-.47-1.14 1.57 1.57 0 0 1 .47-1.15ZM337.47 285.59v1.13h-3.19v9a6.28 6.28 0 0 0 .2 1.8 1.45 1.45 0 0 0 1.47 1.07 1.75 1.75 0 0 0 1-.26 5.19 5.19 0 0 0 .94-.85l.41.35-.34.47a5.06 5.06 0 0 1-1.72 1.54 3.84 3.84 0 0 1-1.75.46 2.47 2.47 0 0 1-2.5-1.64 6.82 6.82 0 0 1-.36-2.47v-9.47h-1.7a.25.25 0 0 1-.12-.1.16.16 0 0 1 0-.12.36.36 0 0 1 .07-.24 4.52 4.52 0 0 1 .44-.4 13.29 13.29 0 0 0 1.56-1.45c.32-.37 1.06-1.34 2.24-2.93a.45.45 0 0 1 .24 0s0 .1 0 .23v3.84ZM341.66 293.11a34 34 0 0 1 5.73-2.55v-1.33a5.39 5.39 0 0 0-.31-2.21 2.23 2.23 0 0 0-2.2-1 2.94 2.94 0 0 0-1.52.41 1.28 1.28 0 0 0-.72 1.15 4.4 4.4 0 0 0 .08.63 5.34 5.34 0 0 1 .08.57 1.31 1.31 0 0 1-.58 1.22 1.42 1.42 0 0 1-.78.21 1.33 1.33 0 0 1-1.08-.46 1.6 1.6 0 0 1-.38-1 3.24 3.24 0 0 1 1.36-2.29 5.84 5.84 0 0 1 4-1.19c2 0 3.39.65 4.11 2a6.87 6.87 0 0 1 .57 3.14v6.26a4.24 4.24 0 0 0 .13 1.25.81.81 0 0 0 .84.61 1.34 1.34 0 0 0 .6-.11 6.63 6.63 0 0 0 .81-.53v.81a4.81 4.81 0 0 1-1.08 1 3.12 3.12 0 0 1-1.78.6 1.74 1.74 0 0 1-1.54-.69 3 3 0 0 1-.52-1.64 14.28 14.28 0 0 1-2 1.53 5.35 5.35 0 0 1-2.71.84 3.23 3.23 0 0 1-2.32-.94 3.17 3.17 0 0 1-1-2.4 4.45 4.45 0 0 1 2.21-3.89Zm5.73-1.77a14.4 14.4 0 0 0-3 1.32c-1.5.92-2.25 2-2.25 3.15a2.4 2.4 0 0 0 .94 2.1 2.29 2.29 0 0 0 1.36.43 3.8 3.8 0 0 0 2-.57 1.7 1.7 0 0 0 .94-1.47ZM353.08 299.56a3.76 3.76 0 0 0 2-.58 2.3 2.3 0 0 0 .51-1.75V282a4.08 4.08 0 0 0-.17-1.42 1.26 1.26 0 0 0-1.27-.66 2.79 2.79 0 0 0-.47.05l-.65.14v-.52q2.12-.56 5.08-1.5a.11.11 0 0 1 .13.1 1.8 1.8 0 0 1 0 .4v18.71a2.33 2.33 0 0 0 .47 1.75 3.61 3.61 0 0 0 1.94.51v.44h-7.56Z" + transform="translate(.5 .5)" + /> + </g> + <path + style={{ + fillOpacity: 0, + stroke: '#e87f6a', + strokeWidth: 10, + }} + d="m239.67 170.92 20.58 35.65 20.57 35.64h-82.31l20.58-35.64 20.58-35.65 20.58 35.65-20.58-35.65z" + /> + </svg> +); + +export default PythagorasCapitalInvestment; diff --git a/src/config/validators/SekoyaLabs.tsx b/src/config/validators/SekoyaLabs.tsx new file mode 100644 index 0000000000..4d03cfade2 --- /dev/null +++ b/src/config/validators/SekoyaLabs.tsx @@ -0,0 +1,30 @@ +const SekoyaLabs = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 473 479" + xmlSpace="preserve" + > + <path + fill="#040401" + d="M474 164v315.959H1.104V1.166H474V164m-70-84.5c-5.67-5.334-11.258-10.757-17.025-15.985-12.167-11.03-25.88-19.796-40.284-27.618-18.138-9.85-37.18-17.544-57.348-21.522-16.17-3.19-32.698-5.039-49.157-6.12-10.606-.696-21.489.313-32.04 1.919-14.282 2.173-28.6 4.819-42.479 8.757-23.21 6.586-44.259 17.866-64.087 31.695C82.452 63.968 65.904 79.92 51.587 98.113c-13.241 16.826-23.646 35.438-31.729 55.33-10.5 25.843-15.955 52.733-16.77 80.544-.24 8.248.049 16.59 1.03 24.778 1.628 13.602 3.007 27.365 6.313 40.606 4.892 19.586 12.429 38.301 22.555 55.944 13.71 23.888 30.692 44.936 51.624 62.893 16.088 13.801 33.763 24.885 52.853 34.075 27.802 13.382 57.269 20.064 87.755 21.408 16.38.722 33.058-.63 49.321-2.984 23.069-3.34 45.02-10.742 66-21.377 28.109-14.25 52.208-33.211 72.736-57.02 14.174-16.438 25.516-34.568 34.752-54.166 12.55-26.629 19.03-54.827 20.705-83.962.85-14.774-.081-29.837-1.994-44.537-2.685-20.642-7.876-40.762-16.336-60.028C439.037 123.736 424.325 100.224 404 79.5z" + /> + <path + fill="#CFAA48" + d="M404.25 79.75c20.075 20.474 34.787 43.986 46.152 69.867 8.46 19.266 13.65 39.386 16.336 60.028 1.913 14.7 2.844 29.763 1.994 44.537-1.676 29.135-8.155 57.333-20.705 83.962-9.236 19.598-20.578 37.728-34.752 54.167-20.528 23.808-44.627 42.768-72.737 57.019-20.978 10.635-42.93 18.037-65.999 21.377-16.263 2.355-32.94 3.706-49.321 2.984-30.486-1.344-59.953-8.026-87.755-21.408-19.09-9.19-36.765-20.274-52.853-34.075-20.932-17.957-37.914-39.005-51.624-62.893-10.126-17.643-17.663-36.358-22.555-55.944-3.306-13.24-4.685-27.004-6.314-40.606-.98-8.188-1.27-16.53-1.028-24.778.814-27.81 6.27-54.7 16.77-80.544 8.082-19.892 18.487-38.504 31.728-55.33C65.904 79.92 82.452 63.968 101.58 50.626c19.828-13.83 40.876-25.109 64.087-31.695 13.878-3.938 28.197-6.584 42.478-8.757 10.552-1.606 21.435-2.615 32.041-1.919 16.459 1.081 32.987 2.93 49.157 6.12 20.167 3.978 39.21 11.672 57.348 21.522 14.403 7.822 28.117 16.588 40.284 27.618 5.767 5.228 11.356 10.65 17.275 16.235m18.25 31.228c-11.568-17.25-25.656-32.312-41.645-45.385-16.17-13.221-33.85-24.097-53.027-32.837-28.002-12.76-57.411-18.113-87.758-19.665-5.899-.301-11.897.25-17.78.975-13.127 1.62-26.406 2.727-39.266 5.627-20.36 4.593-39.673 12.346-57.905 22.686-16.887 9.577-32.58 20.76-46.338 34.462-7.01 6.98-13.61 14.408-20.007 21.962-16.294 19.236-28.2 41.088-36.69 64.708-10.273 28.584-15.48 58.085-12.993 88.546 1.144 14.013 2.034 28.237 5.23 41.851 6.138 26.135 16.922 50.537 31.984 72.913 11.744 17.446 25.598 32.966 41.592 46.667 13.712 11.748 28.405 22.026 44.476 30.153 16.86 8.527 34.494 15.208 53 19.311 17.348 3.846 34.944 5.722 52.696 5.989 4.258.064 8.537-.52 12.79-.959 9.455-.973 19.053-1.302 28.32-3.216 23.679-4.89 46.269-13.033 67.391-24.966 23.37-13.203 43.965-29.75 61.449-50.22 15.698-18.382 28.32-38.476 37.812-60.792 11.823-27.795 17.158-56.822 17.954-86.704.313-11.754-1.126-23.675-2.96-35.332-1.915-12.168-4.361-24.406-8.185-36.087-6.91-21.105-16.608-41.014-30.14-59.687z" + /> + <path + fill="#040401" + d="M422.754 111.244c13.278 18.407 22.977 38.316 29.886 59.421 3.824 11.68 6.27 23.919 8.185 36.087 1.834 11.657 3.273 23.578 2.96 35.332-.796 29.882-6.13 58.909-17.954 86.704-9.492 22.316-22.114 42.41-37.812 60.791-17.484 20.471-38.08 37.018-61.449 50.22-21.122 11.934-43.712 20.077-67.392 24.967-9.266 1.914-18.864 2.243-28.318 3.216-4.254.438-8.533 1.023-12.791.959-17.752-.267-35.348-2.143-52.695-5.989-18.507-4.103-36.14-10.784-53.001-19.311-16.071-8.127-30.764-18.405-44.476-30.153-15.994-13.701-29.848-29.22-41.592-46.667-15.062-22.376-25.846-46.778-31.984-72.913-3.196-13.614-4.086-27.838-5.23-41.85-2.487-30.462 2.72-59.963 12.993-88.547 8.49-23.62 20.396-45.472 36.69-64.708C65.172 91.249 71.77 83.822 78.78 76.84c13.757-13.701 29.451-24.885 46.338-34.462 18.232-10.34 37.545-18.093 57.905-22.686 12.86-2.9 26.14-4.008 39.266-5.627 5.883-.725 11.881-1.276 17.78-.975 30.347 1.552 59.756 6.904 87.758 19.665 19.178 8.74 36.857 19.616 53.027 32.837 15.99 13.073 30.077 28.135 41.899 45.651m-193.82 245.597c-1.316-3.495-2.497-7.048-3.97-10.475-8.491-19.745-22.172-35.032-40.457-46.14-15.273-9.28-32.385-12.621-49.868-14.156-14.929-1.31-29.988-1.417-44.836-3.28-11.376-1.428-22.644-4.3-33.706-7.43-6.454-1.826-12.408-5.421-19.153-8.476.404 2.09.733 3.763 1.051 5.44.334 1.758.687 3.513.984 5.277 4.129 24.526 13.25 47.187 26.062 68.364 13.961 23.078 32.119 42.462 53.87 58.315 19.514 14.221 41.2 24.23 64.474 30.614 7.823 2.147 15.855 3.611 23.864 4.958 3.428.577 6.639.703 9.238-3.51 15.159-24.565 19.254-50.708 12.447-79.501m59.992-24.42c-3.987 8.151-8.793 16.013-11.786 24.514-5.815 16.518-6.76 33.658-3.03 50.84 1.929 8.888 4.14 17.826 10.281 24.984.935 1.09 3.25 1.978 4.56 1.62 22.462-6.145 43.306-15.71 62.43-29.155 15.938-11.202 29.974-24.453 42.1-39.494 16.823-20.87 28.83-44.523 35.707-70.48 3.033-11.453 4.962-23.243 6.655-34.986.88-6.108.155-12.446.155-18.624-.328.297-.915.66-1.276 1.182-6.736 9.699-15.56 17.222-24.922 24.236-15.75 11.801-34.201 16.877-52.744 22.098-11.489 3.235-22.904 7.234-33.71 12.269-14.216 6.623-24.914 17.843-34.42 30.996m-75.924-152.917c-9.725-8.052-19.362-16.216-29.265-24.043-1.575-1.246-4.518-1.747-6.536-1.292-17.29 3.891-34.696 3.633-52.142 1.811-18.119-1.891-35.408-6.814-51.753-14.925-9.408-4.668-9.216-4.578-14.39 5.173-11.01 20.751-18.266 42.756-21.651 65.93-.705 4.824-2.819 10.038.733 15.229 5.784 8.454 13.776 14.158 22.65 18.307 17.205 8.042 36.003 8.81 54.555 10.275 10.678.843 21.473.717 32.053 2.19 18.633 2.597 36.727 7.389 52.727 17.88 10.541 6.912 21.046 13.894 28.826 24.187 5.633 7.454 11.613 14.646 17.264 21.737 15.517-41.404 8.17-99.83-33.07-142.46m57.508 32.492c-.052 1.602-.638 3.425-.078 4.773 10.415 25.06 12.96 51.234 11.121 79.52 21.882-21.264 48.113-31.513 76.06-37.29.211-.93.387-1.262.34-1.56-2.222-14.232-3.314-28.552-.86-42.805 2.146-12.468 4.543-24.954 7.896-37.138 5.568-20.236 16.802-37.622 29.218-54.292.822-1.104 1.667-3.311 1.141-4.107-2.335-3.536-4.812-7.12-7.88-10.004-11.065-10.395-22.014-20.975-33.752-30.573-7.57-6.189-16.442-10.785-25.202-16.398-.58 6.253-.912 11.316-1.542 16.341-1.402 11.182-2.218 22.505-4.6 33.483-4.601 21.207-12.24 41.414-23.074 60.292-8.065 14.05-16.538 27.842-28.788 39.758M157 79.311c3.103 13.534 10.558 24.395 21.038 33.167 12.31 10.304 25.287 19.841 37.237 30.536 11.003 9.848 21.157 20.665 31.409 31.321 3.316 3.447 5.758 7.735 8.768 11.87 1.154-1.623 1.944-2.853 2.85-3.99 13.865-17.403 23.577-36.99 30.817-57.933.927-2.68.266-4.012-1.801-5.146-2.548-1.397-5.207-2.592-7.768-3.968-16.73-8.986-28.976-22.383-37.462-39.156-5.172-10.222-8.342-21.158-9.205-32.699-.08-1.06-1.763-2.9-2.627-2.858-5.716.279-11.401 1.133-17.116 1.503-15.427.998-30.088 5.282-44.677 10.008-5.303 1.718-10.405 3.898-11.233 9.956-.74 5.415-.205 11.003-.23 17.389m260.929 76.421-4.907-9.359c-21.692 30.635-30.791 63.754-27.152 101.745 5.482-4.108 9.976-7.06 13.991-10.561 13.289-11.585 22.14-26.219 27.585-42.794 1.327-4.039 1.302-9.171.086-13.26-2.536-8.524-6.283-16.687-9.603-25.77M102.522 90.984c-6.693 7.3-14.734 13.39-20.695 21.92 22.083 11.283 44.665 16.018 67.891 14.991-5.311-8.777-11.397-17.002-15.492-26.121-4.036-8.984-6.054-18.874-8.994-28.489-7.002 5.418-14.607 11.301-22.71 17.699m196.477-34.112c-.179-4.345-1.437-7.705-6.434-8.669-9.251-1.784-18.44-3.896-27.696-5.656-.608-.116-2.36 1.73-2.236 2.424 2.626 14.79 9.231 27.542 20.515 37.55 3.911 3.468 8.555 6.11 12.864 9.13l1.445-.834a8936.44 8936.44 0 0 0 1.542-33.945z" + /> + <path + fill="#CFAA48" + d="M228.963 357.259c6.778 28.375 2.683 54.518-12.476 79.084-2.6 4.212-5.81 4.086-9.238 3.51-8.009-1.348-16.041-2.812-23.864-4.959-23.273-6.385-44.96-16.393-64.474-30.614-21.751-15.853-39.909-35.237-53.87-58.315-12.812-21.177-21.933-43.838-26.062-68.364-.297-1.764-.65-3.52-.984-5.278-.318-1.676-.647-3.35-1.05-5.44 6.744 3.056 12.698 6.65 19.152 8.477 11.062 3.13 22.33 6.002 33.706 7.43 14.848 1.863 29.907 1.97 44.836 3.28 17.483 1.535 34.595 4.876 49.868 14.156 18.285 11.108 31.966 26.395 40.456 46.14 1.474 3.427 2.655 6.98 4 10.893zM288.992 332.06c9.44-12.792 20.138-24.012 34.354-30.635 10.806-5.035 22.221-9.034 33.71-12.269 18.543-5.221 36.994-10.297 52.744-22.098 9.362-7.014 18.186-14.537 24.922-24.236.361-.521.948-.885 1.276-1.182 0 6.178.725 12.516-.155 18.624-1.693 11.743-3.622 23.533-6.655 34.985-6.876 25.958-18.884 49.611-35.708 70.481-12.125 15.041-26.161 28.292-42.098 39.494-19.125 13.444-39.97 23.01-62.431 29.155-1.31.358-3.625-.53-4.56-1.62-6.14-7.158-8.352-16.096-10.282-24.985-3.73-17.181-2.784-34.321 3.031-50.84 2.993-8.5 7.8-16.362 11.852-24.874zM213.256 179.755c40.987 42.378 48.334 100.804 32.817 142.208-5.651-7.09-11.63-14.283-17.264-21.737-7.78-10.293-18.285-17.275-28.826-24.187-16-10.491-34.094-15.283-52.727-17.88-10.58-1.473-21.375-1.347-32.053-2.19-18.552-1.464-37.35-2.233-54.554-10.275-8.875-4.15-16.867-9.853-22.651-18.307-3.552-5.191-1.438-10.405-.733-15.23 3.385-23.173 10.64-45.178 21.651-65.929 5.174-9.751 4.982-9.841 14.39-5.173 16.345 8.11 33.634 13.034 51.753 14.925 17.446 1.822 34.852 2.08 52.142-1.811 2.018-.455 4.96.046 6.536 1.292 9.903 7.827 19.54 15.99 29.519 24.294m-111.332.654c-14.504 24.855-.358 51.798 25.62 58.283 21.217-29.918 3.978-58.033-21.905-64.678-1.162 1.835-2.368 3.738-3.715 6.395m67.488 66.663 4.815 2.962c22.969-21.081 17.956-52.038-9.683-65.748-14.28 12.125-20.628 34.882-7.001 51.656 3.271 4.027 7.409 7.35 11.869 11.13M86 212.591c1.228-14.104-13.355-28.992-26.877-31.313-.451.764-.912 1.61-1.433 2.416-13.65 21.12-5.588 42.798 16.073 52.606 2.946 1.334 5.109 1.227 6.784-2.24 3.155-6.532 6.29-12.974 5.453-21.47m151.654 39.24c2.631-17.008-6.622-30.874-23.436-37.908-1.472 2.207-3.004 4.487-4.517 6.778-12.28 18.591-4.232 40.209 14.708 49.245 1.242.593 4.29.024 4.81-.927 2.917-5.343 5.317-10.969 8.435-17.189zM270.76 211.741c12.001-11.662 20.474-25.453 28.539-39.504 10.835-18.878 18.473-39.085 23.074-60.292 2.382-10.978 3.198-22.3 4.6-33.483.63-5.025.963-10.088 1.542-16.34 8.76 5.612 17.633 10.208 25.202 16.397 11.738 9.598 22.687 20.178 33.752 30.573 3.068 2.883 5.545 6.468 7.88 10.004.526.796-.319 3.003-1.141 4.107-12.416 16.67-23.65 34.056-29.218 54.292-3.353 12.184-5.75 24.67-7.896 37.138-2.454 14.253-1.362 28.573.86 42.805.047.298-.129.63-.34 1.56-27.947 5.777-54.178 16.026-76.06 37.29 1.84-28.286-.706-54.46-11.12-79.52-.56-1.348.025-3.171.326-5.027m70.805 17.324c-11.646-20.064-33-23.744-51.697-14.308-1.342.678-2.915 3.726-2.43 4.742 3.26 6.84 5.999 14.348 10.875 19.914 11.381 12.991 32.31 14.342 48.712 4.13-1.659-4.514-3.37-9.167-5.46-14.478m-1.51-123.345c-1.351 3.97-3.46 7.857-3.917 11.928-1.671 14.935 5.466 25.772 17.263 33.967.96.668 3.279.234 4.469-.497 1.454-.894 2.432-2.605 3.532-4.028 7.138-9.231 9.999-19.385 6.683-30.897-2.937-10.2-10.13-16.652-19.287-22.184-2.851 3.77-5.606 7.412-8.744 11.71m-20.596 56.325c-.486 4.538-1.66 9.128-1.328 13.605.974 13.11 12.575 22.662 24.775 23.243 1.205.057 3.498-1.3 3.562-2.134.468-6.1 1.623-12.465.428-18.31-2.47-12.08-13.083-17.493-23.686-19.314-.945-.162-2.19 1.412-3.75 2.91zM157 78.874c.026-5.949-.509-11.537.231-16.952.828-6.058 5.93-8.238 11.233-9.956 14.59-4.726 29.25-9.01 44.677-10.008 5.715-.37 11.4-1.224 17.116-1.503.864-.042 2.548 1.797 2.627 2.858.863 11.54 4.033 22.477 9.205 32.699 8.486 16.773 20.732 30.17 37.462 39.156 2.56 1.376 5.22 2.57 7.768 3.968 2.067 1.134 2.728 2.467 1.801 5.146-7.24 20.944-16.952 40.53-30.816 57.932-.907 1.138-1.697 2.368-2.85 3.99-3.011-4.134-5.453-8.422-8.77-11.869-10.251-10.656-20.405-21.473-31.408-31.32-11.95-10.696-24.928-20.233-37.237-30.537-10.48-8.772-17.935-19.633-21.04-33.604m53.108 20.592c7.89-17.829.32-35.552-16.111-44.758-3.123 4.032-6.496 8.036-9.484 12.308-11.688 16.709-3.018 36.622 11.038 45.87.954.629 3.393.535 4.067-.204 3.633-3.98 6.926-8.27 10.49-13.216m14.9 15.517c-.002 2.164.1 4.334-.024 6.49-.786 13.698 11.174 27.523 24.448 31.184 1.052.29 3.254-.311 3.54-1.04 2.521-6.424 6.268-12.862 6.737-19.502 1.352-19.125-9.71-27.832-23.434-34-2.207-.992-4.253-.857-5.368 2.104-1.768 4.695-3.89 9.258-5.9 14.764zM417.965 156.119c3.285 8.697 7.032 16.86 9.568 25.383 1.216 4.09 1.241 9.222-.086 13.26-5.446 16.576-14.296 31.21-27.585 42.795-4.015 3.5-8.51 6.453-13.99 10.561-3.64-37.991 5.46-71.11 27.151-101.745 1.812 3.456 3.36 6.408 4.942 9.746m-5.013 50.41c6.509-6.7 9.226-16.813 1.404-27.39-11.278 3.711-16.41 10.838-15.19 22.312.336 3.166 2.11 6.657 4.34 8.884.782.782 4.902-1.774 7.496-2.812.45-.18.886-.39 1.95-.995zM102.771 90.728c7.854-6.14 15.459-12.024 22.46-17.442 2.941 9.615 4.96 19.505 8.995 28.489 4.095 9.119 10.181 17.344 15.492 26.121-23.226 1.027-45.808-3.708-67.89-14.992 5.96-8.529 14.001-14.618 20.943-22.176m7.065 8.926-4.575.704c-1.607 6.892 4.077 14.253 12.351 14.558 1.986.074 5.331-.385 5.73-1.46.732-1.974.32-4.87-.65-6.88-2.383-4.929-7.137-6.6-12.856-6.922zM299 57.336c-.515 11.47-1.029 22.476-1.543 33.482l-1.445.834c-4.309-3.02-8.953-5.662-12.864-9.13-11.284-10.008-17.889-22.76-20.515-37.55-.124-.695 1.628-2.54 2.236-2.424 9.256 1.76 18.445 3.872 27.696 5.656 4.997.964 6.255 4.324 6.434 9.132M289.953 63c-1.647-4.501-4.36-7.875-10.633-9.104-.255 3.845-1.612 7.943-.43 11.085 1.179 3.135 4.902 5.314 7.51 7.912 1.198-3.025 2.395-6.05 3.553-9.893z" + /> + <path + fill="#040401" + d="M101.995 180.033c1.276-2.28 2.482-4.184 3.644-6.02 25.883 6.646 43.122 34.76 21.905 64.68-25.978-6.486-40.124-33.429-25.55-58.66zM169.051 247.006c-4.1-3.713-8.237-7.037-11.508-11.064-13.627-16.774-7.28-39.53 7.001-51.656 27.639 13.71 32.652 44.667 9.683 65.748-1.554-.956-3.185-1.96-5.176-3.028zM86 213.062c.838 8.024-2.298 14.466-5.453 20.999-1.675 3.466-3.838 3.573-6.784 2.239-21.661-9.808-29.722-31.487-16.073-52.606.52-.806.982-1.652 1.433-2.416 13.522 2.32 28.105 17.209 26.877 31.784zM237.383 252.175c-2.847 5.874-5.247 11.5-8.164 16.843-.52.95-3.568 1.52-4.81.927-18.94-9.036-26.987-30.654-14.708-49.245 1.513-2.291 3.045-4.57 4.517-6.778 16.814 7.034 26.067 20.9 23.165 38.253zM341.755 229.394c1.9 4.982 3.611 9.635 5.27 14.15-16.402 10.211-37.331 8.86-48.712-4.13-4.876-5.567-7.615-13.075-10.875-19.915-.485-1.016 1.088-4.064 2.43-4.742 18.697-9.436 40.05-5.756 51.887 14.637zM340.245 105.392c2.947-3.97 5.702-7.614 8.553-11.383 9.156 5.532 16.35 11.985 19.287 22.184 3.316 11.512.455 21.666-6.683 30.897-1.1 1.423-2.078 3.134-3.532 4.028-1.19.731-3.508 1.165-4.47.497-11.796-8.195-18.933-19.032-17.262-33.967.456-4.071 2.566-7.957 4.107-12.256zM319.687 161.679c1.333-1.133 2.577-2.707 3.522-2.545 10.603 1.821 21.217 7.234 23.686 19.314 1.195 5.845.04 12.21-.428 18.31-.064.834-2.357 2.191-3.562 2.134-12.2-.581-23.8-10.133-24.775-23.243-.332-4.477.842-9.067 1.557-13.97zM210.03 99.847c-3.487 4.565-6.78 8.855-10.413 12.835-.674.739-3.113.833-4.067.205-14.056-9.25-22.726-29.162-11.038-45.87 2.988-4.273 6.361-8.277 9.484-12.309 16.43 9.206 24 26.93 16.034 45.14zM225.025 114.54c1.991-5.063 4.113-9.626 5.88-14.32 1.116-2.962 3.162-3.097 5.37-2.105 13.724 6.168 24.785 14.875 23.433 34-.469 6.64-4.216 13.078-6.736 19.501-.287.73-2.49 1.331-3.541 1.041-13.274-3.661-25.234-17.486-24.448-31.183.124-2.157.022-4.327.042-6.935zM412.641 206.732c-.753.4-1.19.61-1.639.79-2.594 1.039-6.714 3.595-7.496 2.813-2.23-2.227-4.004-5.718-4.34-8.884-1.22-11.474 3.912-18.6 15.19-22.312 7.822 10.577 5.105 20.69-1.715 27.593zM110.156 99.392c5.4.584 10.153 2.255 12.535 7.185.971 2.01 1.383 4.905.651 6.879-.399 1.075-3.744 1.534-5.73 1.46-8.274-.305-13.958-7.666-12.351-14.558 1.44-.222 3.008-.463 4.895-.966zM289.973 63.409c-1.178 3.434-2.375 6.459-3.573 9.484-2.608-2.598-6.331-4.777-7.51-7.912-1.182-3.142.175-7.24.43-11.085 6.272 1.23 8.986 4.603 10.653 9.513z" + /> + </svg> +); + +export default SekoyaLabs; diff --git a/src/config/validators/StakeWorld.tsx b/src/config/validators/StakeWorld.tsx new file mode 100644 index 0000000000..1f62d47037 --- /dev/null +++ b/src/config/validators/StakeWorld.tsx @@ -0,0 +1,238 @@ +const StakeWorld = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> + <path + style={{ + fill: '#fff', + }} + d="M0 0h512v512H0z" + /> + <path + fill="#184891" + d="M273.84 462.49c-15.14 0-30.15 0-45.7-.41a7.68 7.68 0 0 0-1.93-1.65c-27.81-4-54-12.12-77.78-28.09-17.76-11.7-34.83-23.95-48.87-40-4-4.41-7.16-9.92-10.18-15a50.74 50.74 0 0 1 7.57-1c4.54-.42 9.36-.83 11.15-6.06 2.2-5.51 5.78-7.57 11.84-6.61 3.71 2.07 7 4.82 10.73 5.92a33 33 0 0 0 13.22 1c1.38-.13 1.93-4.81 3.58-6.74a31.6 31.6 0 0 1 7.57-7.85c2.89-1.79 6.47-2.61 9.77-3.3a25.25 25.25 0 0 1 9.09 0 77.12 77.12 0 0 1 12.53 4c.41 1.1.55 2.48 1.1 2.75 8.53 3.31 10.6 11.29 13.63 18.73 1 2.2 2.06 5.36 3.85 6.33 4.41 2.34 6.06 5.92 7 10.19 1.24 4.4 2.89 8.67 4 12.94a10.84 10.84 0 0 1-.41 6.33c-4 8.4-8.4 16.66-12.8 25.33 3.44 1.51 7 2.89 10.46 4.68 3.3 1.51 7.43 2.61 9.91 5.23a14.09 14.09 0 0 0 9.78 3.72c5.64.27 11.56.27 17.2 1.51 8 1.65 15-2.06 16.25-10.05.55-4.4.55-8.95 1.1-13.35.14-1.24.27-2.89 1.24-3.58a116.25 116.25 0 0 1 12.8-10.6c3.17-1.93 6.2-3.44 7-7.57.14-1.38 2.34-2.62 3.86-3.17 5.92-1.65 11.7-2.89 17.75-4.4 1.24-.42 2.9-.83 3.58-1.79 5.79-8.13 6.47-17.49 4.41-26.85-.83-2.89-3.3-5.78-5.64-8.12-3.17-3.3-7.58-5.78-10.6-9.36-9.5-11.7-9.37-11.84-24-9.5a1.32 1.32 0 0 0-.41.14c-4.41.41-7-1.38-7.85-5.78-1.24-6.89-1.93-14-3.3-20.93-.41-2.06-2.07-3.71-3-6-2.89-15 2.75-24.1 17.48-27.4a3.15 3.15 0 0 0 1.38-.69c2.89-1.79 6.33-3.3 8.53-5.78 3.86-4 7-8.67 10.47-13.22 6.47-8.53 14.59-14 25.6-15.14 1.38-.14 3.31-2.48 3.58-4 1.1-5.64 1.52-11.29 2.2-17.35a54.71 54.71 0 0 1 4.55-6.88c4.95 3.17 9.91 5.92 14.31 9.09 2.76 1.79 4.82 1.51 6.89-.83 2.75-3.17 5.37-6.33 8.26-9.22 6.05-6.33 6.19-6.2 13.62-1.65a54.28 54.28 0 0 1 5.1 3c.69 6.47-3.17 12.81.55 18.86 1.93 3.17 1.93 6.34 1 9.78-.82 2.48-1.79 5.92-.82 7.43a44.06 44.06 0 0 1 9.5 24.92c3.3-.55 5.91-.69 8.67-1.52 5.5-1.65 11-3.71 16.65-5.64 4.27-1.51 7.16.28 8.13 4.13a156.73 156.73 0 0 0 5.37 16.79c.68 1.52 2.89 2.62 4.68 4a1.78 1.78 0 0 0 1.37.27 5.24 5.24 0 0 1 .59-.48 2 2 0 0 1 .69.41c3.72 3.86 7.16 7.44 10.88 11.29a87.59 87.59 0 0 1-2.34 10.74 189.45 189.45 0 0 1-27.54 52.45 213.59 213.59 0 0 1-42.95 44.05c-19.41 14.73-40.74 26-64.28 32.76-12.53 3.44-25.06 7.57-38.27 6.75-1.11-.14-2.21 1.24-3.31 2.06Z" + /> + <path + d="M49.18 229.71c4.4-2.07.82-5.65 2.48-8.4 7.29 4.27 17.89 5 17.75 16.24h25.88a37.6 37.6 0 0 1 2.9 2.62c-1.38 10.05-3.31 19-14.46 21.61-5.37 1.38-6.47 5-4.27 9.64 2.48 4.82.55 8.26-3.44 11-3.85 2.48-7.43 5.09-11.56 7.71-4.68-1.51-8.81-2.89-13.08-4.27a8.83 8.83 0 0 0-1.79-3.16c-.41-17.49-.41-35.11-.41-53Z" + style={{ + fill: '#1163ae', + }} + /> + <path + fill="#1773ba" + d="M261.73 49.51c1.24 4.4 1.93 8.81 3 13.08.82 3.16 0 5.36-3 7.57-5.37.68-10.33 1.1-15.15 1.37-6.05-3.71-7.84-8.81-5.64-15.41.55-1.79.28-3.86.28-6.2 6.74-.41 13.35-.41 20.51-.41Z" + /> + <path + d="M460.79 232.32c0 14.73 0 29.46-.42 44.6a15 15 0 0 1-5.5-1.79c-1.38-2.2-2.89-4-3.72-6.19a14.48 14.48 0 0 1-.83-5.92c.42-3.44 1.66-6.74 1.93-10.19s-.69-7.15-1.1-11.15c.41-8.26 1.38-9.36 9.64-9.36Z" + style={{ + fill: '#174994', + }} + /> + <path + fill="#273779" + d="M455 275.41c1.66.55 3.31 1.38 5.37 2.07a17.14 17.14 0 0 1 .42 4.81c-1.1 3.44-2.34 6.34-3 9.36-1.79 7-3.17 14.05-5 21.48a105 105 0 0 1-11.15-11c4.41-9.36 9-18.17 13.35-26.71Z" + /> + <path + fill="#1e4485" + d="M460.79 231.91c-8.26.41-9.23 1.51-9.91 9.64-4.55-3.17-8.68-6.47-12.95-9.92a12.24 12.24 0 0 1-2.34-15.69c.42 1.24.42 2.75 1.11 3 7.43 2.06 13.07 9.5 21.75 8.12a5 5 0 0 0 1.92 1.1 9.75 9.75 0 0 1 .42 3.72Z" + /> + <path + d="M263.38 313.4a129.47 129.47 0 0 1-15.83 3.72 9.9 9.9 0 0 1-3.44-.41c-6.61-1.79-13.22-3.72-20.24-6.06 1-9.63 2.34-18.72 3.72-27.67 8.53 1.38 13.35-2.48 14.87-10.74a8.65 8.65 0 0 1 1.37-2.89c3.17-2.2 6.75-5.78 8.54-5.09 8 3 15.55-.28 23.4.55 3.3.41 5.78-1.1 6.88-4.82 2.07-6.88 4.68-13.63 7.3-20.37 1.93-.28 3.44-.69 5.37-.83 3 0 6.19.28 8.81-.41 9.63-2.48 19-5.51 28.49-8.12-.68 5.64-1.1 11.29-2.2 16.93-.27 1.51-2.2 3.85-3.58 4-11 1.1-19.13 6.61-25.6 15.14-3.44 4.55-6.61 9.23-10.47 13.22-2.2 2.48-5.64 4-8.53 5.78a3.26 3.26 0 0 1-1.38.69c-14.73 3.3-20.37 12.39-17.48 27.39Z" + style={{ + fill: '#124b96', + }} + /> + <path + fill="#0f4e99" + d="M89.38 377.28a138.87 138.87 0 0 1-10.6-13.77c-.83-1.24-.28-3.58 0-5.78 8.94-5.09 17.75-9.36 22.16-19.27 7.16-3.58 13.9-7 20.92-10.74a6.88 6.88 0 0 0 2.07-2.61c1.51-5.65 5.37-6.34 10.19-5.1a7.63 7.63 0 0 0 2.47.42c8.26-.28 16.66-.55 24.92-.42.83 11.29-6.06 15-15 15.28a77.73 77.73 0 0 0-9.77.14 4.91 4.91 0 0 0-3 1.79c1.65 4.41-2.2 6.33-3.85 9.09-3.31 5.92-6.89 11.56-10.33 17.34-5.64-1-9.22 1.1-11.42 6.61-1.79 5.23-6.61 5.64-11.15 6.06a50.74 50.74 0 0 0-7.57 1Z" + /> + <path + fill="#124b97" + d="M119.94 363.65c3-5.78 6.6-11.42 9.91-17.34 1.65-2.76 5.5-4.68 3.85-9.09a4.91 4.91 0 0 1 3-1.79 77.73 77.73 0 0 1 9.77-.14c8.95-.27 15.84-4 15.29-15.55a52.57 52.57 0 0 1 5.36-7.57c7.58 3.58 14.73 7.15 21.89 10.6-5.37 10.46-4.82 21.61-2.61 33.17-4.27-1-8.4-2.48-12.53-3.3a25.25 25.25 0 0 0-9.09 0c-3.3.69-6.88 1.51-9.77 3.3a31.6 31.6 0 0 0-7.57 7.85c-1.65 1.93-2.2 6.61-3.58 6.74a33 33 0 0 1-13.22-1c-3.71-1.1-7-3.85-10.73-5.92Z" + /> + <path + fill="#124b97" + d="M384.11 192.4c2.75 1.93 6.88 3.44 8 6.2 1.93 5.23 2.34 11.15 2.89 16.93.14 1-2.06 2.34-3.44 3.3a58 58 0 0 1-6.05 3.86 26.58 26.58 0 0 1-5.24-2.34c-7.43-4.55-7.57-4.68-13.62 1.65-2.89 2.89-5.51 6.05-8.26 9.22-2.07 2.34-4.13 2.62-6.89.83-4.4-3.17-9.36-5.92-14.31-9.36a2.24 2.24 0 0 1-.28-1.38c.41-2.48.83-4.41 1.1-6.61.69-4 2.48-5.78 7-5.64a64.08 64.08 0 0 0 14.46-1.1c4.54-.83 9.91.13 11.42-7.3 1.93-10.32 2.21-8 11.15-8.26Z" + /> + <path + fill="#0f4e99" + d="M336.89 221.86v.69c-1.38 2.48-2.75 4.68-4.27 7.29-9.5 3-18.86 6.06-28.49 8.54-2.62.69-5.78.41-8.81.14-.55-.42-.55-1-.14-1.1a33.6 33.6 0 0 0 1.93-5c1.1-2.75 1.65-7.16 3.44-7.71 6.74-2.34 13.35-6.06 21.06-4.4 4.96 1 10.19 1 15.28 1.55Z" + /> + <path + fill="#273779" + d="M439 302.12c.09-.19.46-.1 1.1.27-.1.19-.51.09-1.1-.27Z" + /> + <path + fill="#105ca8" + d="M383 133.62a10.09 10.09 0 0 1-1.38 2.75 69.21 69.21 0 0 1-12.11-4.4c-3.72-1.66-5.65-1.24-7.85 2.75-1.79 3.17-5 7.16-8.12 7.71-8.67 1.51-12.25 5.92-12.53 14.18a6.58 6.58 0 0 1-.14 1.65c-.55 2.75-1.79 4.13-5.09 3.17-4.27-1.24-8.54-1.79-12.67-2.89-7.15-1.66-8-1.66-10.18 5.5-1.38 4.68-4.54 6.2-8.81 6.2-3.72.13-7.44 0-11 0-3.85 0-6.05 1.37-5.64 5.64a15.66 15.66 0 0 1-.14 6.2c-.41 1.79-1.37 4.26-2.75 4.68-8 3.58-13.35 8.53-13.63 17.75a15.4 15.4 0 0 1-.27 1.79c-3.58 4.55-5.92 10.6-12.53 10a32.89 32.89 0 0 0-15.57 2.7 9.76 9.76 0 0 1-4 .69c-5.64-.28-11-.83-16.66-1.1-1.92-7.44-3.71-15-5.5-22.44-.14-1 1-2.21 1.79-2.89a6.76 6.76 0 0 1 2.47-1c13.08-1.92 19.69-10 22.72-22.85a33.58 33.58 0 0 1 8.67 3.3c6.06 3.72 13.49.56 14.32-6.74.55-5 .82-9.91 1.1-14.87 5.23-1.24 10.19-2.61 15.28-3.85 1.79-.55 3.58-1.79 6.19-2.89 5.37-.28 10-.28 14.73-.28 5.24 0 11.57-5.78 12.12-11s.69-10.6 1.1-16c.14-4.54 1.51-5.51 5.37-3.44 5.09 2.89 9.64 6.19 14.87 8.67 2.06 1.1 5.92 1.51 7.43.41 4.54-3.16 9.09-4.13 14.73-4 1.24.55 2.75.55 3 1.24 1.93 5.09 6.47 6.33 10.74 8.12a77 77 0 0 1 9.91 5.51Z" + /> + <path + d="M57.44 195.57c.69-3 1-6.47 2.34-9.23 4.68-10.6 9.77-20.92 14.73-31.25 2.61-5.23 4.54-10.87 7.57-15.83 2.48-4 6.19-7.16 9.36-10.73 6.19-7.16 11.84-14.73 18.45-21.48 5.78-5.64 12.39-10.46 18.86-15.42A192.39 192.39 0 0 1 175 64c13.49-5.64 27.26-10.46 42-12 .14 0 .27.13.69.68a47.81 47.81 0 0 1 .55 7.44c0 6-1.24 7.16-7.44 7.16-5.5 0-11.15-.55-16.79 3.16-1.93 0-3.17.41-3.86-.14-8.12-5.23-12.94 0-17.75 5.37a98.45 98.45 0 0 1-7.71 7.44c-.69 1.65-1.93 3.3-2.07 5.09-.69 7.16-4 13.08-9.22 18.45-3.72-1.66-7.71-2.76-11.15-5.1-7.71-5.23-13.36-.55-19.69 3-5.92 3.31-5.64 9.36-7.29 14.46a11.88 11.88 0 0 1-3.17 4.54 35.68 35.68 0 0 1-7.57 4.54c-4.68 2.48-7.57 5.23-7.57 11.43.13 3.58-3 7.57-5.65 10.6-3.3 4-3.3 7.29 0 11.15 1.52 1.79 2.62 3.72 4.13 5.78-3.58 5.92-6.88 12.25-11 18-1.65 2.07-4.82 3-7.3 4.41-1.92 1.24-4 2.06-6.47 3.58-4.81 1.1-9.08 1.79-13.21 2.48Z" + style={{ + fill: '#1972b9', + }} + /> + <path + d="M221.81 218.69c5.78.14 11.15.69 16.79 1a9.76 9.76 0 0 0 4-.69 32.89 32.89 0 0 1 15.56-2.62c6.61.55 9-5.5 12.8-10a47.31 47.31 0 0 0 3.86-8.12c1.92-7 11.56-10.87 17.48-7.16 3.44 2.21 7.29 3.72 10.87 5.92-.27 1.93-.27 3.72-1.1 5-4.68 7.71-6.88 15.83-5.5 25 .55 3.31-1.11 7-1.79 10.6v.69a15.13 15.13 0 0 1-5.23 1.38c-5.51-1.1-10.19-2.34-15-3.17-1.79-.41-4.68-.41-5.64.69-3.72 4.41-8.95 5-13.91 6.47-2.75.69-4.95 3-7.57 4.68-1.51 1.1-3 2.62-4.54 2.89-6.47.55-12.8.83-19.27 1.1-9.23-10.46-8.4-21.61-1.79-33.59Z" + style={{ + fill: '#0f54a0', + }} + /> + <path + d="M358.92 118.75a22.52 22.52 0 0 0-14.32 4c-1.51 1.1-5.37.69-7.43-.41-5.23-2.48-9.78-5.78-14.87-8.67-3.86-2.07-5.23-1.1-5.37 3.44-.41 5.37-.69 10.74-1.1 16s-6.88 11-12.12 11h-14.59a75.48 75.48 0 0 1-4.54-11.56c1.38-5.92 2.75-11 4-16.38 8.4-4.13 17.07-7 26.57-7.16a4.63 4.63 0 0 0 1.93-.28c10.05-6.33 18.86-13.76 23.13-25.47.41-1 .55-2.2 1.1-4 4.81-1.79 9.22-3.16 13.49-4.68a77.51 77.51 0 0 1 6.88 4.68 7.47 7.47 0 0 1 1.38 2.34c3.58 7.85 7 15.7 10.05 23.54-7.17 2.32-12.82 5.76-14.19 13.61Z" + style={{ + fill: '#1262ae', + }} + /> + <path + d="M166.74 312.17c-1.65 2.34-3.17 4.68-5 7.15-8.54.56-16.94.83-25.2 1.11a7.63 7.63 0 0 1-2.47-.42c-4.82-1.24-8.68-.55-10.19 5.1a4 4 0 0 1-2.07 2.2c.42-5.92 1.1-11.56 1.93-17.62 4.82-5.23 9.09-10.33 9.5-17.76a4.12 4.12 0 0 1 1.38-2.48c5.64-6.74 11.56-13.63 17.34-20.37a1.7 1.7 0 0 1 1.1.14c6.34 3.85 12.25 7.7 18.72 11.83 1.24-3.16 2.34-5.64 3.31-8.12 2.06-5.5 4.13-11 6.61-16.65 2.89-1.38 5.5-2.76 8.12-4 4.54 7 9.08 14.18 13.35 21.89-1.24 1.1-2.06 2.07-3.16 2.48-7 2.75-14.32 5.23-21.2 8.26-2.07.82-4.27 2.75-4.82 4.68-2.71 7.41-4.91 15-7.25 22.58Z" + style={{ + fill: '#0e54a1', + }} + /> + <path + d="M167.15 312.17a221.94 221.94 0 0 1 6.85-22.58c.55-1.93 2.75-3.86 4.82-4.68 6.88-3 14.17-5.51 21.2-8.26 1.1-.41 1.92-1.38 3.16-2.2 2.2.27 4.41 1.51 5.78.82 5.65-2.34 9.5.42 13.36 3.86 1.51 1.24 3 2.34 4.95 3.71-1.1 9.09-2.48 18.18-3.85 27.81-11 4.82-21.48 10.88-34 12.12-7.58-3.45-14.73-7-22.31-10.6Z" + style={{ + fill: '#0f519c', + }} + /> + <path + d="M64.87 290.14c3.72-2.62 7.3-5.23 11.15-7.71 4-2.75 5.92-6.19 3.44-11-2.2-4.68-1.1-8.26 4.27-9.64 11.15-2.61 13.08-11.56 14.59-21.47a81.48 81.48 0 0 1 11-1.52 9.58 9.58 0 0 1 4.81 1.24c1.79 1.1 3.17 2.89 5 3.86 5.78 2.89 7.71 7.57 6.61 14.45-4.41 7-6.47 14.73-16 16.38-5.51.83-10.33 5.23-15.28 8.26a7.35 7.35 0 0 0-3.31 4.13c-.55 6.2-.82 12.53-1.23 18.86-.55.28-.83.69-1.38 1-6.2-.68-11.7-1.37-17.35-2.47-2.06-5.1-4.26-9.78-6.33-14.32Z" + style={{ + fill: '#0f5faa', + }} + /> + <path + d="M90.06 305.83c.28-6.19.55-12.52 1.1-18.72a7.35 7.35 0 0 1 3.31-4.11c5-3 9.77-7.43 15.28-8.26 9.5-1.65 11.56-9.36 16-15.83 8.67 2.89 17.34 6.33 26.15 9.91-5.64 7-11.56 13.91-17.2 20.65a4.12 4.12 0 0 0-1.38 2.48c-.41 7.43-4.68 12.53-9.77 17.76-7-2.2-13.22-4.68-20.38-4-4.3.54-8.7.12-13.11.12Z" + style={{ + fill: '#105ba6', + }} + /> + <path + fill="#1773ba" + d="M196.06 95.76c7.57.41 15.15.69 23.13 1.1-1.38 9.09-2.75 17.9-4.27 26.85a5.35 5.35 0 0 1-2.75 3 28.51 28.51 0 0 1-7.3 2.34c-3.85.68-6.05 3-7.84 6.19-.69 1.38-2.21 3.3-3.58 3.44a161.4 161.4 0 0 1-16.38.83 6.31 6.31 0 0 1-4.27-2.07c-3.17-4.26-6.2-8.81-9.91-14.18 8-4.4 15.69-8.53 23.26-12.94 1.38-.82 2.2-2.89 2.89-4.4.83-2.34 1.38-5 2.34-7.71a19.5 19.5 0 0 1 4.68-2.45Z" + /> + <path + d="M223.46 252.7c6.61-.69 12.94-1 19.41-1.52 1.51-.27 3-1.79 4.54-2.89 2.62-1.65 4.82-4 7.57-4.68 5-1.51 10.19-2.06 13.91-6.47 1-1.1 3.85-1.1 5.64-.69 4.82.83 9.5 2.07 14.59 3.17-1.79 6.74-4.4 13.49-6.47 20.37-1.1 3.72-3.58 5.23-6.88 4.82-7.85-.83-15.42 2.48-23.4-.55-1.79-.69-5.37 2.89-8.54 4.68-5.64-2.75-11-5.78-16.66-8-4-1.79-5.36-3.86-3.71-8.26Z" + style={{ + fill: '#0e4f9a', + }} + /> + <path + d="M57.44 196c4.13-1.1 8.4-1.79 12.94-2.62a12.82 12.82 0 0 1 2.62 4.25c2.76 8.4 7.16 14.87 16.94 16.25a4.34 4.34 0 0 1 3 3.3c1.11 6.47 1.66 12.94 2.34 20-8.53.41-16.93.41-25.88.41.14-11.28-10.46-12-17.62-16.79 1.66-8.12 3.58-16.25 5.65-24.78Z" + style={{ + fill: '#146db6', + }} + /> + <path + d="M189.87 251.87a55.36 55.36 0 0 1-8.54 4.41 58.45 58.45 0 0 1-10.87-1.66c-4.82-1.37-9.36-1.79-13.77.83-3.58-3.86-7-7.85-10.6-11.7-4.13-4.54-4.27-7.3-.41-12.12 3.17-3.85 14-5.5 17.21-1.65 5.37 6.75 13.35 8.26 20.65 10.88s7.7 3.14 6.33 11.01Z" + style={{ + fill: '#0e5da8', + }} + /> + <path + d="M246.58 72c4.82-.69 9.78-1.11 15-1.52 4 9.91 7.43 19.83 10.46 30.15a12.09 12.09 0 0 1-3.3 1.38c-9.09-3.72-18.17-7.57-27.81-11.57 1.93-6.19 3.86-12.11 5.65-18.44Z" + style={{ + fill: '#207bc0', + }} + /> + <path + d="M268.75 102.37a10.51 10.51 0 0 1 3.3-1.38c5.51 5 10.74 9.78 16.11 15a135.5 135.5 0 0 1-3.72 16.1c-9.63-2.47-19.13-5.5-29.46-8.81 4.82-7.28 9.23-13.89 13.77-20.91Z" + style={{ + fill: '#146ab2', + }} + /> + <path + d="M196.06 95.21a23 23 0 0 1-4.82 2.62C184 94 177.07 90 170.32 86.13c-1.65-1-3.44-1.79-5.37-2.9 2.34-2.61 5.1-4.81 7.44-7.57 4.81-5.37 9.63-10.6 17.75-5.37.69.55 2.07.14 3.45.14 1.1 8.26 1.65 16.38 2.47 24.78Z" + style={{ + fill: '#1e76bd', + }} + /> + <path + d="M267.1 151a109.88 109.88 0 0 1-.69 15c-.83 7.3-8.26 10.46-14.32 6.74a45.31 45.31 0 0 0-8.26-3.44c0-5.09-1.24-10.05 1.38-15.14 2.89-5.64 7.16-8.26 13.21-6.06 2.76 1 5.51 1.79 8.68 2.89Z" + style={{ + fill: '#0d62ab', + }} + /> + <path + d="M156.55 255.86c4.55-3 9.09-2.61 13.91-1.24a72.78 72.78 0 0 0 10.6 1.79c-1.79 5.51-3.86 11-5.92 16.52-1 2.48-2.07 5-3.31 8.12-6.47-4.13-12.38-8-18.58-11.83a128.72 128.72 0 0 1 3.3-13.36Z" + style={{ + fill: '#1256a7', + }} + /> + <path + d="M71.2 304.87c5.65.69 11.15 1.38 17.35 2.48 1.51 8 2.61 15.42 3.58 22.85-2.48.83-4.41 1.24-6.61 1.93a15.46 15.46 0 0 1-3.85-.28 9.08 9.08 0 0 0-3.58-.82c-5 .82-9.5 1.37-14 2.2-1.93-5.37-3.72-10.6-5.24-16.52 4.23-4.27 8.36-8.12 12.35-11.84Z" + style={{ + fill: '#0c5aa5', + }} + /> + <path + d="M81.67 332.13a23.55 23.55 0 0 1 4 .27c4 1.65 7.16 2.89 10.46 4.13l4.41 1.93c-4 9.91-12.81 14.18-21.75 18.86-8.14-5.92-10.79-15.14-14.74-23.82a89.24 89.24 0 0 1 14.31-2.06 24.65 24.65 0 0 0 3.31.69Z" + style={{ + fill: '#1154a0', + }} + /> + <path + d="M71.2 304.46c-4 4.13-8.12 8-12.39 11.84-2.75-9.78-5.23-19.69-7.43-30 4.27 1 8.4 2.34 13.08 3.85a135.08 135.08 0 0 1 6.74 14.31Z" + style={{ + fill: '#1655a6', + }} + /> + <path + d="M414.81 179.74q-1.8-9.65-3.44-19.28c-.55-4 0-7.43 3.44-10.05s3.44-6.33 1.92-10.32c-2.61-6.61-5-13.63-7.29-20.79 8 10 17.07 20 23.95 31 5.65 8.94 8.81 19.13 12.8 28.9.69 1.79.56 4 .42 6.34-10.88-1.65-21.34-3.72-31.8-5.78Z" + style={{ + fill: '#194790', + }} + /> + <path + d="M414.67 180.15c10.6 1.65 21.06 3.72 31.94 5.78a39.78 39.78 0 0 1 4.13 9.22c-3.44-.13-5.37 1-6.89 3.86-2.34 4.4-5.64 8.4-8.39 12.53-5.65-4.69-11.57-9.09-16.66-14.18-4.54-4.36-4.54-10.6-4.13-17.21Z" + style={{ + fill: '#184787', + }} + /> + <path + d="M373.65 105.12q-5.37-11.7-10.6-23.54a20.75 20.75 0 0 0-1-2.2c.83-.41 2.07-.83 2.62-.41 13.77 11.84 27.53 23.81 41.3 36.2a96.3 96.3 0 0 1-14.32 4.54c-2.75-2.34-5-4.54-7.43-6.47-3.58-2.89-7.16-5.36-10.6-8.12Z" + style={{ + fill: '#0c5e9c', + }} + /> + <path + fill="#184891" + d="M435.46 212c2.75-4.54 6.05-8.54 8.39-12.94 1.52-2.89 3.45-4 6.89-3.44 2.48 7.57 4.68 15.28 6.88 23a40.39 40.39 0 0 1 .83 5.09c-.28.55-.42.69-.28.83s.14.27.41.41v1.79c-8.81 1.79-14.45-5.65-21.88-7.71-.69-.28-.69-1.79-1.11-3 0-1.6-.13-2.71-.13-4.03Z" + /> + <path + d="M354.51 74.29a99.41 99.41 0 0 1-13.21 4.54c-5.37-2.89-10.19-6.2-15.28-9-4.13-2.2-7.3-4.95-7.85-10.19 5.09 1 10.46 1.66 15.14 3.58 7.3 3.17 14.18 7.16 21.2 11Z" + style={{ + fill: '#0d5f9d', + }} + /> + <path + fill="#1e4485" + d="M458.58 224.61c-.27.14-.41 0-.41-.13s0-.28.28-.42.13.14.13.55Z" + /> + <path + d="M295.18 237.42c.27-3.45 1.93-7.16 1.38-10.47-1.38-9.22.82-17.34 5.5-25 .83-1.24.83-3 1.65-5 6.89-3 13.08-6.74 20.65-5.78 1.38.14 3.45-1.38 4.41-2.61q6.33-8.4 12.11-16.94c1.52-2.47 3-3.58 5.92-2.47 3.72 1.23 7.71 1.79 11.57 2.89 4.13 1.51 7.57.82 10.6-2.62 1.51-1.79 4-2.34 6-3.44 6.2 3.85 12.39 7.85 19.14 12.11-3.58 4.82-6.88 9.09-10.05 14a5.65 5.65 0 0 1-2.07.28c-8.94.28-9.22-2.06-11.15 8.26-1.51 7.43-6.88 6.47-11.42 7.3a64.08 64.08 0 0 1-14.46 1.1c-4.54-.14-6.33 1.65-7 5.64-.27 2.2-.69 4.13-1.1 6.61a81 81 0 0 1-15.28-1c-7.71-1.66-14.32 2.06-21.06 4.4-1.79.55-2.34 5-3.44 7.71a33.6 33.6 0 0 1-1.93 5Z" + style={{ + fill: '#11519d', + }} + /> + <path + d="M374.89 165.56c-1.93 1.51-4.41 2.06-5.92 3.85-3 3.44-6.47 4.13-10.6 2.62-3.86-1.1-7.85-1.66-11.57-2.89-2.89-1.11-4.4 0-5.92 2.47q-5.78 8.53-12.11 16.94c-1 1.23-3 2.75-4.41 2.61-7.57-1-13.76 2.75-20.65 5.37-4.13-1.79-8-3.3-11.42-5.51-5.92-3.71-15.56.14-17.48 7.16a39.7 39.7 0 0 1-3.72 7.71c-.28-.41-.14-1-.14-1.38.28-9.22 5.65-14.17 13.63-17.75 1.38-.42 2.34-2.89 2.75-4.68a15.66 15.66 0 0 0 .14-6.2c-.41-4.27 1.79-5.64 5.64-5.64 3.58 0 7.3.13 11 0 4.27 0 7.43-1.52 8.81-6.2 2.2-7.16 3-7.16 10.18-5.5 4.13 1.1 8.4 1.65 12.67 2.89 3.3 1 4.54-.42 5.09-3.17a6.58 6.58 0 0 0 .14-1.65c.28-8.26 3.86-12.67 12.53-14.18 3.16-.55 6.33-4.54 8.12-7.71 2.2-4 4.13-4.41 7.85-2.75a109.51 109.51 0 0 0 12 4.68c2.34 7.43 5 15-2.34 21.33-2.06 1.79-2.89 4.82-4.26 7.58Z" + style={{ + fill: '#0c58a3', + }} + /> + <path + fill="#105ca8" + d="M373.1 105.12c4 2.76 7.57 5.23 11.15 8.12 2.47 1.93 4.68 4.13 7.16 6.47a156.55 156.55 0 0 1-8.13 13.77c-3.44-1.79-6.6-3.85-10.18-5.37-4.27-1.79-8.81-3-10.74-8.12-.28-.69-1.79-.69-3-1.24 1-7.85 6.61-11.29 13.77-13.63Z" + /> + <path + fill="#0f56a2" + d="M100.94 338.46c-1.79-.69-3.44-1.24-4.82-1.93-3.3-1.24-6.47-2.48-10.05-4.13 1.65-1 3.58-1.37 6.06-2.2-1-7.43-2.07-14.87-3.17-22.85a1.76 1.76 0 0 1 1-1.38c4.54-.14 8.94.28 13.21-.27 7.16-.69 13.35 1.79 20.1 4.4-.28 5.65-1 11.29-1.65 17.35-6.78 3.99-13.52 7.43-20.68 11.01Z" + /> + <path + fill="#0f56a2" + d="M81.67 331.85a5.48 5.48 0 0 1-2.89-.41 3.75 3.75 0 0 1 2.89.41Z" + /> + </svg> +); + +export default StakeWorld; diff --git a/src/config/validators/Stakely.tsx b/src/config/validators/Stakely.tsx new file mode 100644 index 0000000000..4f47984501 --- /dev/null +++ b/src/config/validators/Stakely.tsx @@ -0,0 +1,20 @@ +const Stakely = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.02 87.02"> + <path + d="m63.29 15.51-4.18 11.43c-8.27-4.5-19-5-21.46-1.33-2 2.95-1.64 7.67 6.27 10.19a1.49 1.49 0 0 1 .64 2.43l-5.49 6a5.93 5.93 0 0 1-7.17 1.23c-4.06-2.16-9.49-6.67-9.9-15.24-.77-18.12 21.44-23.28 41.29-14.71Z" + style={{ + fillRule: 'evenodd', + fill: '#f79367', + }} + /> + <path + d="M41.32 46.79a1.56 1.56 0 0 0 .6 2.53c4.27 1.57 10.59 4 9.27 9.68-1.56 6.75-17.3 4.88-26.26.73L21 71.08a49.74 49.74 0 0 0 31.74 3.19C70.46 70.11 65.86 52 64.29 48.69c-1.39-2.9-4.88-7.12-11.6-9.85a4.4 4.4 0 0 0-4.83 1Z" + style={{ + fill: '#415876', + fillRule: 'evenodd', + }} + /> + </svg> +); + +export default Stakely; diff --git a/src/config/validators/Stakenode.tsx b/src/config/validators/Stakenode.tsx new file mode 100644 index 0000000000..7e10712965 --- /dev/null +++ b/src/config/validators/Stakenode.tsx @@ -0,0 +1,29 @@ +const Stakenode = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> + <defs> + <linearGradient + id="a" + x1={-285.62} + y1={1459.68} + x2={-284.99} + y2={1459.68} + gradientTransform="scale(-647.12 647.12) rotate(2.588 32160.098 7037.231)" + gradientUnits="userSpaceOnUse" + > + <stop offset={0} stopColor="#33c4f2" /> + <stop offset={1} stopColor="#2e3690" /> + </linearGradient> + </defs> + <path fill="#fff" d="M0 0h512v512H0z" /> + <path + d="m206 104.43-82.28 33.76a8.55 8.55 0 0 0-4.78 5 8.37 8.37 0 0 0 .42 6.89l6.4 12.45a4 4 0 0 1-.49 4.43l-24.83 29.6a.63.63 0 0 1-.77.14.63.63 0 0 1-.35-.7l8.16-33.33a8.33 8.33 0 0 0 0-4.15L104 145.15a8.48 8.48 0 0 0-5.7-6.05 8.39 8.39 0 0 0-8.15 1.62l-28.41 23.77a8.56 8.56 0 0 0-3.1 6.61v12.65a8.67 8.67 0 0 0 2.67 6.19l9.36 9a4 4 0 0 1 1.19 3.59l-5.2 30.24a8.74 8.74 0 0 0 2.18 7.31l29 31.29a8.55 8.55 0 0 0 6.12 2.75 8.52 8.52 0 0 0 6.26-2.46l33-32.56a3.88 3.88 0 0 1 4.36-.85 4 4 0 0 1 2.53 3.66l.14 6.54a8.63 8.63 0 0 0 8.58 8.44h57.1a4.17 4.17 0 0 1 2.81 1.12l30.66 29.68a4 4 0 0 1 1.27 2.74L251 302a4.42 4.42 0 0 1-.63 2.32l-3.59 5.63a8.56 8.56 0 0 0-.28 8.79 8.51 8.51 0 0 0 7.52 4.5h9.71a8.63 8.63 0 0 0 7.17-3.8l7.39-10.9a8.35 8.35 0 0 0 1.47-4.78V248.1a3.77 3.77 0 0 1 .78-2.32l1.68-2.32a3.84 3.84 0 0 1 4.5-1.48 4 4 0 0 1 2.82 3.87V257a8.57 8.57 0 0 0 8.58 8.58h75.66a4.06 4.06 0 0 1 2.53.84l34.25 27.08a4 4 0 0 1 1.55 3.16v11.11a3.89 3.89 0 0 1-.64 2.18 8.45 8.45 0 0 0-.28 8.79 8.52 8.52 0 0 0 7.53 4.5h5.62a8.55 8.55 0 0 0 8-5.48l2.32-6a9.34 9.34 0 0 0 .56-3.1v-34a8.79 8.79 0 0 0-2.88-6.47l-5-4.5a6.93 6.93 0 0 0-5-6.33v-51.74l23.77 14.49v15.68a6.86 6.86 0 0 0-4.64 6.54 6.93 6.93 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-4.64-6.54v-18.21l-32-19.41a9.85 9.85 0 0 0-2.82-1.48l-54.07-15.4a3.67 3.67 0 0 1-.78-.28l-10.68-5.48v-.36a6.94 6.94 0 0 0-7-7 7.09 7.09 0 0 0-4.29 1.48L308.6 153.9v-.28a7 7 0 0 0-7-7 6.89 6.89 0 0 0-4.22 1.48l-84.31-43.46a8.86 8.86 0 0 0-3.94-1 10.71 10.71 0 0 0-3.23.7m1.75 4.22a4.07 4.07 0 0 1 3.38.14l43.39 22.36a7 7 0 0 0-4.15 4.22h-23.21l-7.38 7.95h-30.24l-11.25-11.25h32.49a6.91 6.91 0 0 0 6.54 4.64 6.93 6.93 0 0 0 7-7 7 7 0 0 0-7-7 7 7 0 0 0-6.54 4.64h-48.81zm7.35 21.82v-1.27a2.4 2.4 0 0 1 2.25-1.75 2.37 2.37 0 0 1 2.39 2.39 2.38 2.38 0 0 1-2.39 2.39 2.31 2.31 0 0 1-2.25-1.76M123.41 148a4.3 4.3 0 0 1-.21-3.23 4 4 0 0 1 2.25-2.32l25.31-10.41h21l15.75 15.82h34.18L229 140h21.38a6.9 6.9 0 0 0 6.54 4.65 6.94 6.94 0 0 0 7-7 8.19 8.19 0 0 0-.28-1.82L295.06 152v.07h-54.43l-20.81 20.81h-93.53l2.46-2.95a8.56 8.56 0 0 0 1.06-9.49l-2-3.8h20.67l8.72 9.07h46.48a6.93 6.93 0 0 0 6.54 4.64 7 7 0 0 0 7-7 7 7 0 0 0-7-7 7 7 0 0 0-6.54 4.64h-24a6.94 6.94 0 0 0 5.41-6.75 6.93 6.93 0 0 0-7-7 7 7 0 0 0-6.54 4.64h-46.2zm131.15-10.34a2.37 2.37 0 0 1 2.44-2.36 2.38 2.38 0 0 1 2.39 2.39 2.37 2.37 0 0 1-2.39 2.39 2.41 2.41 0 0 1-2.39-2.39M63.21 171.1a4.11 4.11 0 0 1 1.48-3.1l28.41-23.77a4 4 0 0 1 3.87-.77 4 4 0 0 1 2.67 2.88l3.45 13.36a4.67 4.67 0 0 1 0 2l-8.23 33.41a5.1 5.1 0 0 0 2.67 5.83 4.3 4.3 0 0 0 1.62.49l-.07.15h39.17a6.91 6.91 0 0 0 6.54 4.64 6.94 6.94 0 0 0 7-7 7 7 0 0 0-7-7 7 7 0 0 0-6.54 4.64h-8.93l3.58-6h16.32a6.91 6.91 0 0 0 6.54 4.64 6.94 6.94 0 0 0 7-7 6.94 6.94 0 0 0-7-7 7 7 0 0 0-6.54 4.65H130.3l-6.4 10.47h-17.79l16.31-19.48h29.4L182 195.85h141.54a6.93 6.93 0 0 0 6.54 4.64 6.93 6.93 0 0 0 7-7 6.93 6.93 0 0 0-7-7 7 7 0 0 0-6.54 4.64h-33.4l11.32-14.07h35.65a6.92 6.92 0 0 0 6.54 4.65 6.78 6.78 0 0 0 5.21-2.4l10.34 5.35a6.42 6.42 0 0 0 1.61.63l47.33 13.5a6.88 6.88 0 0 0-4.78 4.43H348.3l-19.2 20.89h-26.3l-15.19-11.25h21.09a6.93 6.93 0 0 0 6.54 4.64 7 7 0 0 0 7-7 6.94 6.94 0 0 0-7-7 7 7 0 0 0-6.54 4.64H75.45l.84-5.07a8.61 8.61 0 0 0-2.53-7.66l-9.35-9a3.06 3.06 0 0 1-.42-.64h16.66a6.92 6.92 0 0 0 6.54 4.65 6.94 6.94 0 0 0 7-7 7 7 0 0 0-7-7 7 7 0 0 0-6.54 4.64H63.21zm112.59-16.67a2.36 2.36 0 0 1 2.39-2.39 2.37 2.37 0 0 1 2.39 2.39 2.37 2.37 0 0 1-2.39 2.39 2.46 2.46 0 0 1-2.39-2.39m66.66 2.25h53a7 7 0 0 0 6.26 3.94 6.87 6.87 0 0 0 5.21-2.46l28.55 14.69h-36.27l-15 18.57h-101l-22.78-14.07h102.85a6.9 6.9 0 0 0 6.54 4.65 7 7 0 0 0 7-7 6.94 6.94 0 0 0-7-7 7 7 0 0 0-6.54 4.64h-37.13zm-87.62 0h16.81a7 7 0 0 0 5 4.5h-17.52zm112.66 18.57a2.37 2.37 0 0 1 2.39-2.4 2.38 2.38 0 0 1 2.39 2.4 2.38 2.38 0 0 1-2.39 2.39 2.42 2.42 0 0 1-2.39-2.39m75.59 2.25h1.13a1.65 1.65 0 0 1-.57.14 1.32 1.32 0 0 1-.56-.14m-15.4 16.31a2.37 2.37 0 0 1 2.39-2.39 2.36 2.36 0 0 1 2.39 2.39 2.36 2.36 0 0 1-2.39 2.39 2.42 2.42 0 0 1-2.39-2.39m-184.87 6.61v-2.32a2.5 2.5 0 0 1 2-1.27 2.38 2.38 0 0 1 2.39 2.39 2.38 2.38 0 0 1-2.39 2.4 2.43 2.43 0 0 1-2-1.2m274 5.49a6.65 6.65 0 0 0-1.9-4.72 2 2 0 0 1 .7.36v.07l.71.42a3.94 3.94 0 0 1 1.47 3v52.6a6.83 6.83 0 0 0-4.15 5h-6.25l-33.83-33.82h6a6.86 6.86 0 0 0 6.68 5.2 7 7 0 0 0 7-7 7 7 0 0 0-7-7 6.88 6.88 0 0 0-6.33 4.15h-44.56l14.9-16h53.1a6.84 6.84 0 0 0 6.54 4.64 6.92 6.92 0 0 0 6.89-7m-9.35 0a2.37 2.37 0 0 1 2.39-2.4 2.38 2.38 0 0 1 2.39 2.4 2.38 2.38 0 0 1-2.39 2.39 2.42 2.42 0 0 1-2.39-2.39m-94.59 4.78a2.37 2.37 0 0 1 2.39-2.39 2.37 2.37 0 0 1 2.4 2.39 2.38 2.38 0 0 1-2.4 2.39 2.42 2.42 0 0 1-2.39-2.39m-111.24 2.25H280l21.31 15.82h13.43l19.41 23.84h30.38a6.92 6.92 0 0 0 6.54 4.64 7 7 0 0 0 7-7 7 7 0 0 0-7-7 7 7 0 0 0-6.54 4.64h-28.2l-15.75-19.27H367l38.4 38.39h8.79a6.84 6.84 0 0 0 6.18 3.8 6.71 6.71 0 0 0 5.35-2.6l3.59 3.16a4.18 4.18 0 0 1 1.33 3v34a3.67 3.67 0 0 1-.28 1.48L428 316a4.12 4.12 0 0 1-3.8 2.6h-5.62a4.1 4.1 0 0 1-3.59-2.11 4.18 4.18 0 0 1 .14-4.15 8.51 8.51 0 0 0 1.34-4.64v-11.15a8.58 8.58 0 0 0-3.24-6.75L379 262.72a8.82 8.82 0 0 0-5.35-1.82h-75.56a4.08 4.08 0 0 1-4.08-4.08v-11.11a8.52 8.52 0 0 0-6-8.23 9.21 9.21 0 0 0-2.67-.42 8.38 8.38 0 0 0-7 3.65l-1.63 2.29a8.46 8.46 0 0 0-1.62 5v4.22h-20.74l-10.06-10.06h17.93a6.86 6.86 0 0 0 6.54 4.64 6.93 6.93 0 0 0 7-7 7 7 0 0 0-7-7 7 7 0 0 0-6.54 4.64h-29.81zM72.14 237.06a4.08 4.08 0 0 1-1-3.45l3.52-20.67h119.63l36.64 29.32h7l14.63 14.63h22.64v46.83a3.71 3.71 0 0 1-.7 2.25l-7.39 10.9a4 4 0 0 1-3.37 1.76h-9.71a4.09 4.09 0 0 1-3.58-2.11 4 4 0 0 1 .14-4.15l3.59-5.62a8.72 8.72 0 0 0 1.33-4.93l-.42-11.53a8.49 8.49 0 0 0-2.6-5.91L224 256.89h9.28l27.92 25.24a6.77 6.77 0 0 0-.92 3.38 6.94 6.94 0 0 0 7 7 7 7 0 0 0 7-7 7 7 0 0 0-7-7 7.86 7.86 0 0 0-2.61.49l-29.53-26.79h-76.3a4.1 4.1 0 0 1-4.08-3.94l-.14-6.54a8.49 8.49 0 0 0-5.34-7.81 3.77 3.77 0 0 0-.85-.21l5.07-5.06h10l12 15.47h36.14v-4.57h-33.87l-8.44-10.9h23.13a6.87 6.87 0 0 0 6.68 5.2 6.94 6.94 0 0 0 7-7 7 7 0 0 0-7-7 6.87 6.87 0 0 0-6.32 4.15h-41.14l-31.79 31.64-12.8 12.66a4.6 4.6 0 0 1-3 1.2 4.06 4.06 0 0 1-2.88-1.27l-10.78-11.62 17.93-20.26h15.19a7 7 0 0 0 6.61 4.86 7 7 0 0 0 7-7 7 7 0 0 0-7-7 7 7 0 0 0-6.47 4.5h-17.58l-19 21.45zM383.74 227a2.37 2.37 0 0 1 2.39-2.39 2.38 2.38 0 0 1 2.39 2.39 2.37 2.37 0 0 1-2.39 2.39 2.36 2.36 0 0 1-2.39-2.39m-187.13 0a2.38 2.38 0 0 1 2.39-2.39 2.37 2.37 0 0 1 2.39 2.39 2.36 2.36 0 0 1-2.39 2.39 2.37 2.37 0 0 1-2.39-2.39m-68.77 8.37v-2.32a2.4 2.4 0 0 1 2-1.27 2.38 2.38 0 0 1 2.39 2.39 2.38 2.38 0 0 1-2.39 2.4 2.16 2.16 0 0 1-2-1.2M446 242.26a2.35 2.35 0 0 1 2-2.32h.84a2.34 2.34 0 0 1 2 2.32 2.36 2.36 0 0 1-2.39 2.39 2.42 2.42 0 0 1-2.39-2.39M418 264a2.42 2.42 0 0 1 1.13-2 11.52 11.52 0 0 0 1.48 1.83l1.68 1.48a2.3 2.3 0 0 1-1.9 1.05A2.42 2.42 0 0 1 418 264m-153.11 21.58a2.37 2.37 0 0 1 2.4-2.39 2.37 2.37 0 0 1 2.39 2.39 2.37 2.37 0 0 1-2.39 2.39 2.37 2.37 0 0 1-2.4-2.39m-75.59-22.22a8.51 8.51 0 0 0-8.09 5.62 8.5 8.5 0 0 0 2.46 9.5l9.22 7.87a7.87 7.87 0 0 0 1.82 1.2l8.44 4.15a4 4 0 0 1 2.18 2.81 4.13 4.13 0 0 1-.84 3.45l-9.28 11a8.51 8.51 0 0 0-1.2 9.14 8.5 8.5 0 0 0 7.81 5h7.66a8.62 8.62 0 0 0 6.89-3.45l18.5-24.47a8.61 8.61 0 0 0-1.2-11.67l-20.88-18.11a8.71 8.71 0 0 0-5.63-2.11H189.3zm8.79 52.95a3.85 3.85 0 0 1 .56-4.29l9.28-11a8.57 8.57 0 0 0 1.83-7.31 8.68 8.68 0 0 0-4.64-6l-8.44-4.15a5.84 5.84 0 0 1-.84-.56l-9.21-7.88a4 4 0 0 1-1.13-4.5 4 4 0 0 1 3.8-2.67H207a4.19 4.19 0 0 1 2.67 1l20.89 18.15a4 4 0 0 1 .56 5.48L212.58 317a4.17 4.17 0 0 1-3.24 1.62h-7.59a3.92 3.92 0 0 1-3.66-2.32m159.77-43.74a8.4 8.4 0 0 0-7.95 5.34 8.47 8.47 0 0 0 2 9.43l6.12 6a8.45 8.45 0 0 0 2.32 1.62l6.54 3.1a4.1 4.1 0 0 1 2.18 2.53 3.93 3.93 0 0 1-.49 3.3l-5.28 8.37a8.48 8.48 0 0 0-.21 8.72 8.45 8.45 0 0 0 7.53 4.43h1.68a8.61 8.61 0 0 0 6.9-3.52l15.05-20.39a8.55 8.55 0 0 0-1.16-11.49l-16.74-15.19a8.51 8.51 0 0 0-5.83-2.25h-12.66zm9.14 46.2a4 4 0 0 1 .14-4.15l5.28-8.44a8.58 8.58 0 0 0 1-7 8.63 8.63 0 0 0-4.57-5.34l-6.54-3.1a4.2 4.2 0 0 1-1.13-.77l-6.11-6a4 4 0 0 1-.92-4.43 4 4 0 0 1 3.73-2.53h12.73a4.07 4.07 0 0 1 2.74 1.05l16.74 15.26a4.12 4.12 0 0 1 .56 5.42l-15.05 20.39a4 4 0 0 1-3.23 1.62h-1.83a3.56 3.56 0 0 1-3.52-2" + fill="url(#a)" + /> + <path + d="M31.34 392c0-2.22.83-3.36 2.47-3.36s2.4 1 2.4 3.1a7.37 7.37 0 0 0 1.08 4.49c.69.89 2.09 1.33 4.05 1.33h23.21c3.66 0 5.5-2.28 5.5-6.89a9.76 9.76 0 0 0-1.52-5.67 4.6 4.6 0 0 0-4-2.15H43.36c-4.3 0-7.34-1.14-9.23-3.36a13.14 13.14 0 0 1-2.85-8.85c0-3.73.95-6.51 2.91-8.47s5.31-2.91 10.12-2.91h21.06a12.57 12.57 0 0 1 5 .88 5.66 5.66 0 0 1 2.85 2.53 12 12 0 0 1 1.2 3.23 21.72 21.72 0 0 1 .32 3.79c0 2.22-.83 3.29-2.41 3.29s-2.46-1-2.46-3.1c0-2.21-.38-3.73-1.14-4.55s-2.09-1.2-4-1.2h-23q-5.51 0-5.51 6.45a8.59 8.59 0 0 0 1.51 5.42 4.86 4.86 0 0 0 4 1.89H63a14.43 14.43 0 0 1 4.87.76 9.44 9.44 0 0 1 3.42 2 10 10 0 0 1 2.15 2.91A14.08 14.08 0 0 1 74.6 387a22.58 22.58 0 0 1 .32 3.67c0 3.86-1 6.76-3 8.79s-5.24 3-10 3H40.7a12.24 12.24 0 0 1-4.23-.63 7.34 7.34 0 0 1-2.72-1.58 7 7 0 0 1-1.52-2.4 13.3 13.3 0 0 1-.76-2.79c-.06-.94-.13-1.96-.13-3.06zM85 361.42c0-1.39 1-2.15 2.91-2.28v-9.67c0-2.21.76-3.35 2.22-3.35 1.77 0 2.65 1.14 2.65 3.35v9.74h12.9c2.22 0 3.36.88 3.36 2.65 0 1.52-1.14 2.28-3.36 2.28H92.81v21.69a23.73 23.73 0 0 0 .38 4.55 9.45 9.45 0 0 0 1.27 3.23 7.76 7.76 0 0 0 2 2.09 9 9 0 0 0 3 1.2 26.2 26.2 0 0 0 3.73.5c1.27.07 2.85.13 4.74.13 2.22 0 3.29.76 3.29 2.21 0 1.78-1.07 2.66-3.29 2.66a52.64 52.64 0 0 1-6.76-.38 20.59 20.59 0 0 1-5.5-1.45 10.86 10.86 0 0 1-4.24-2.85 12 12 0 0 1-2.6-4.72 23.22 23.22 0 0 1-1-7.09V364c-1.83-.11-2.83-1-2.83-2.58zm33.4 29.41a23.2 23.2 0 0 1 .63-5.88 11.54 11.54 0 0 1 2-4.3 13.23 13.23 0 0 1 3-2.91 14.26 14.26 0 0 1 4.18-1.84 34.21 34.21 0 0 1 5.06-.95 58.17 58.17 0 0 1 6.07-.25h13.78v-5.31a6.32 6.32 0 0 0-1.07-4.11c-.7-.82-2.09-1.2-4-1.2H134.8c-7.65 0-11.44 1.89-11.44 5.63a5.07 5.07 0 0 1-.51 2.46 2.19 2.19 0 0 1-2 .82c-1.58 0-2.34-1.07-2.34-3.28a8.64 8.64 0 0 1 4.3-8c2.84-1.71 6.64-2.53 11.32-2.53h14.54q5.5 0 7.4 2.65c1.27 1.77 2 4.81 2 9.17v28.84c0 2.15-.82 3.16-2.46 3.16s-2.41-1.07-2.41-3.29v-3.28a16.56 16.56 0 0 1-6.83 4.42 28 28 0 0 1-9.1 1.46h-8.73a9.37 9.37 0 0 1-7.21-3.16c-1.96-2-2.9-4.84-2.9-8.32zm4.86.5a6.37 6.37 0 0 0 1.65 4.3 4.65 4.65 0 0 0 3.54 1.9h8.73c5.25 0 9.23-1.39 11.89-4.24s4-6.89 4-12.26v-1.4h-13.71c-1.84 0-3.29 0-4.49.07a34 34 0 0 0-3.61.38 12.43 12.43 0 0 0-3 .76 11 11 0 0 0-2.15 1.32 5.71 5.71 0 0 0-1.58 2.09 15.47 15.47 0 0 0-.89 3 20 20 0 0 0-.35 4.08zm48.45 8.79v-54.89c0-2.09.82-3.1 2.4-3.1s2.47 1 2.47 3.1v31.94l18.15-17.33a3.53 3.53 0 0 1 2.27-1.14 2.36 2.36 0 0 1 1.58.7 2.19 2.19 0 0 1 .76 1.77 3.16 3.16 0 0 1-1.14 2.15l-15.56 14.23 22.36 20.8a3.32 3.32 0 0 1 1 2.22 2.31 2.31 0 0 1-.63 1.58 2 2 0 0 1-1.58.76 4 4 0 0 1-2.34-1.14l-22.2-21.44-2.72 2.72V400c0 2-.82 3-2.53 3a2.24 2.24 0 0 1-1.64-.63 3.19 3.19 0 0 1-.62-2.25zm39.71-14.23v-10.18a23.47 23.47 0 0 1 1-7.08 13 13 0 0 1 2.59-4.74 11 11 0 0 1 4.37-2.85 24.78 24.78 0 0 1 5.75-1.39 59.68 59.68 0 0 1 7.21-.38H235a58 58 0 0 1 7.15.38 23.58 23.58 0 0 1 5.69 1.39 11 11 0 0 1 4.36 2.85 12.9 12.9 0 0 1 2.59 4.74 23.16 23.16 0 0 1 .95 7.08v3.73h-39.48v6.39a25.67 25.67 0 0 0 .31 4.05 11.71 11.71 0 0 0 .89 3 5.71 5.71 0 0 0 1.58 2.09 8.77 8.77 0 0 0 2.15 1.33 8.91 8.91 0 0 0 3 .76c1.2.18 2.47.31 3.61.37s2.65.13 4.49.13h12.58c2 0 3.35-.44 4-1.33a7.45 7.45 0 0 0 1.07-4.49c0-2.08.83-3.1 2.47-3.1s2.4 1.14 2.4 3.36a21.08 21.08 0 0 1-.19 3 17.59 17.59 0 0 1-.69 2.78 7.24 7.24 0 0 1-1.52 2.41 7.87 7.87 0 0 1-2.72 1.58 12.26 12.26 0 0 1-4.24.63h-13.13a59.68 59.68 0 0 1-7.21-.38 24.29 24.29 0 0 1-5.75-1.39 10.9 10.9 0 0 1-4.37-2.85 12.85 12.85 0 0 1-2.59-4.8 23.86 23.86 0 0 1-.95-7.09zm4.87-11.32H251a14.05 14.05 0 0 0-.7-4.67 7.81 7.81 0 0 0-1.77-3 6.92 6.92 0 0 0-3.16-1.77 24.46 24.46 0 0 0-4.37-.76c-1.51-.13-3.47-.19-5.94-.19h-2.72c-2.47 0-4.43.06-5.94.19a24.46 24.46 0 0 0-4.37.76 6.63 6.63 0 0 0-3.22 1.77 7.81 7.81 0 0 0-1.77 3 13.56 13.56 0 0 0-.72 4.67zM268 399.81v-38c0-2.21.76-3.29 2.21-3.29 1.77 0 2.66 1.14 2.66 3.35V365c3.66-3.93 9-5.89 16-5.89h2.72a57.76 57.76 0 0 1 7.14.38 23.59 23.59 0 0 1 5.69 1.4 10.89 10.89 0 0 1 4.37 2.84 13 13 0 0 1 2.59 4.74 23.54 23.54 0 0 1 .95 7.09v24.15c0 2.15-.76 3.17-2.34 3.17-1.71 0-2.53-1.08-2.53-3.17v-24.06a24.15 24.15 0 0 0-.38-4.62 9.2 9.2 0 0 0-1.27-3.22 7.93 7.93 0 0 0-2-2.09 9.66 9.66 0 0 0-3.16-1.2 30.24 30.24 0 0 0-4-.51c-1.32-.06-3-.12-5.12-.12h-2.72c-5.31 0-9.29 1.39-12 4.23s-4 7-4 12.4v19.1c0 2.15-.82 3.16-2.4 3.16-1.6.22-2.41-.78-2.41-2.97zm56.66-13.92v-10.18a23.47 23.47 0 0 1 1-7.08 13 13 0 0 1 2.59-4.74 11 11 0 0 1 4.36-2.85 25 25 0 0 1 5.76-1.39 59.68 59.68 0 0 1 7.21-.38h2.72a57.76 57.76 0 0 1 7.14.38 23.66 23.66 0 0 1 5.7 1.39 11 11 0 0 1 4.36 2.85 13 13 0 0 1 2.59 4.74 23.47 23.47 0 0 1 1 7.08v10.18a23.54 23.54 0 0 1-1 7.09 13.34 13.34 0 0 1-2.59 4.8 10.93 10.93 0 0 1-4.36 2.85 22 22 0 0 1-5.7 1.39 58.45 58.45 0 0 1-7.14.38h-2.72a59.68 59.68 0 0 1-7.21-.38 24.49 24.49 0 0 1-5.76-1.39 10.93 10.93 0 0 1-4.36-2.85 12.85 12.85 0 0 1-2.59-4.8 23.86 23.86 0 0 1-1.01-7.09zm4.87-10.11v10.05a25.59 25.59 0 0 0 .32 4.05 11.33 11.33 0 0 0 .88 3 5.82 5.82 0 0 0 1.58 2.09 8.77 8.77 0 0 0 2.15 1.33 8.91 8.91 0 0 0 3 .76c1.21.18 2.47.31 3.61.37s2.65.13 4.49.13h2.72c2.08 0 3.79-.06 5.12-.13a28.3 28.3 0 0 0 4-.5 7.67 7.67 0 0 0 3.17-1.2 7.09 7.09 0 0 0 2-2.09 8.22 8.22 0 0 0 1.26-3.23 23.19 23.19 0 0 0 .38-4.61v-10.09a23.19 23.19 0 0 0-.38-4.61 9.24 9.24 0 0 0-1.26-3.23 8.08 8.08 0 0 0-2-2.09 9.64 9.64 0 0 0-3.17-1.2 28.3 28.3 0 0 0-4-.5c-1.33-.07-3-.13-5.12-.13h-2.72c-1.84 0-3.29 0-4.49.06s-2.4.19-3.61.38a12.93 12.93 0 0 0-3 .76 15 15 0 0 0-2.15 1.33 5.82 5.82 0 0 0-1.58 2.09 14.82 14.82 0 0 0-.88 3 29.59 29.59 0 0 0-.32 4.24zm51.16 10.11v-10.18a23.47 23.47 0 0 1 .95-7.08 13 13 0 0 1 2.59-4.74 11 11 0 0 1 4.36-2.85 25.25 25.25 0 0 1 5.7-1.39 58.45 58.45 0 0 1 7.14-.38h18.72v-13.66c0-2.28.89-3.35 2.66-3.35 1.52 0 2.21 1.14 2.21 3.35v54.32c0 2.15-.82 3.17-2.4 3.17s-2.47-1.08-2.47-3.29v-3.29c-3.67 3.92-9 5.88-16 5.88h-2.72a57.76 57.76 0 0 1-7.14-.38 23.2 23.2 0 0 1-5.7-1.39 11.37 11.37 0 0 1-4.36-2.85 12.85 12.85 0 0 1-2.59-4.8 23.86 23.86 0 0 1-.96-7.09zm4.87-10.11v10.05a24.07 24.07 0 0 0 .38 4.62 9.19 9.19 0 0 0 1.26 3.22 8.11 8.11 0 0 0 2 2.09 9.23 9.23 0 0 0 3.16 1.2 30.31 30.31 0 0 0 4 .51c1.33.06 3 .12 5.13.12h2.71c5.32 0 9.3-1.39 12-4.23s4-6.9 4-12.27v-16.95h-18.65c-2.09 0-3.8.06-5.12.13a28.63 28.63 0 0 0-4 .5 7.91 7.91 0 0 0-3.16 1.2 8.06 8.06 0 0 0-2 2.09 7.29 7.29 0 0 0-1.27 3.23 17.81 17.81 0 0 0-.44 4.49zm52.3 10.11v-10.18a23.16 23.16 0 0 1 .95-7.08 12.9 12.9 0 0 1 2.59-4.74 11 11 0 0 1 4.36-2.85 24.87 24.87 0 0 1 5.76-1.39 59.57 59.57 0 0 1 7.21-.38h2.72a57.76 57.76 0 0 1 7.14.38 23.46 23.46 0 0 1 5.69 1.39 11 11 0 0 1 4.37 2.85 13 13 0 0 1 2.59 4.74 23.47 23.47 0 0 1 .95 7.08v3.73h-39.54v6.39a25.59 25.59 0 0 0 .32 4.05 11.71 11.71 0 0 0 .89 3 5.71 5.71 0 0 0 1.58 2.09 8.58 8.58 0 0 0 2.15 1.33 8.91 8.91 0 0 0 3 .76c1.26.18 2.47.31 3.6.37s2.66.13 4.49.13h12.59c2 0 3.35-.44 4.05-1.33a7.45 7.45 0 0 0 1.07-4.49c0-2.08.82-3.1 2.4-3.1s2.47 1.14 2.47 3.36a21.08 21.08 0 0 1-.19 3 18.75 18.75 0 0 1-.69 2.78 7.24 7.24 0 0 1-1.52 2.41 7.87 7.87 0 0 1-2.72 1.58 12.26 12.26 0 0 1-4.24.63h-13.25a59.51 59.51 0 0 1-7.2-.38 24.25 24.25 0 0 1-5.76-1.39 10.86 10.86 0 0 1-4.36-2.85 12.86 12.86 0 0 1-2.6-4.8 26.16 26.16 0 0 1-.88-7.09zm4.87-11.32h34.65a15.83 15.83 0 0 0-.69-4.67 7.93 7.93 0 0 0-1.77-3 6.91 6.91 0 0 0-3.17-1.77 24.35 24.35 0 0 0-4.36-.76c-1.52-.13-3.48-.19-5.94-.19h-2.72c-2.47 0-4.43.06-6 .19a24.51 24.51 0 0 0-4.36.76 6.67 6.67 0 0 0-3.23 1.77 8.06 8.06 0 0 0-1.77 3 15.83 15.83 0 0 0-.65 4.67z" + fill="#2f3b96" + /> + </svg> +); + +export default Stakenode; diff --git a/src/config/validators/Stakepile.tsx b/src/config/validators/Stakepile.tsx new file mode 100644 index 0000000000..9816035c8c --- /dev/null +++ b/src/config/validators/Stakepile.tsx @@ -0,0 +1,112 @@ +const Stakepile = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 560 560"> + <path + style={{ + fill: '#fff', + }} + d="M0 0h560v560H0z" + /> + <path + style={{ + fill: 'none', + stroke: '#f5980c', + strokeWidth: 9, + }} + d="M27.5 388.3v33.03M467.93 388.3v33.03" + /> + <ellipse + cx={247.8} + cy={421.33} + rx={224.8} + ry={64.23} + style={{ + fill: '#f5980c', + }} + /> + <path + d="M465.11 386.47c0 5.06-3.66 11.55-14.37 18.78-10.45 7.06-26.07 13.72-46 19.42-39.84 11.38-95.31 18.53-156.9 18.53s-117.06-7.15-156.9-18.53c-20-5.7-35.58-12.36-46-19.42C34.15 398 30.5 391.53 30.5 386.47s3.65-11.55 14.36-18.79c10.46-7.06 26.07-13.71 46-19.42 39.84-11.38 95.32-18.52 156.9-18.52s117.06 7.14 156.9 18.52c20 5.71 35.59 12.36 46 19.42 10.79 7.24 14.45 13.72 14.45 18.79Z" + style={{ + fill: '#f2c94c', + stroke: '#f5980c', + strokeWidth: 15, + }} + /> + <path + style={{ + fill: 'none', + stroke: '#f5980c', + strokeWidth: 9, + }} + d="M89.89 307.56v33.03M530.32 307.56v33.03" + /> + <ellipse + cx={310.2} + cy={340.59} + rx={224.8} + ry={64.23} + style={{ + fill: '#f5980c', + }} + /> + <path + d="M527.5 305.72c0 5.06-3.65 11.55-14.36 18.79-10.46 7.06-26.07 13.71-46 19.42-39.84 11.38-95.32 18.52-156.9 18.52s-117.06-7.14-156.9-18.52c-20-5.71-35.59-12.36-46-19.42-10.71-7.24-14.37-13.73-14.37-18.79s3.66-11.55 14.37-18.79c10.45-7.06 26.07-13.71 46-19.41C193.14 256.13 248.61 249 310.2 249s117.06 7.14 156.9 18.53c20 5.7 35.58 12.35 46 19.41 10.75 7.23 14.4 13.72 14.4 18.78Z" + style={{ + fill: '#f2c94c', + stroke: '#f5980c', + strokeWidth: 15, + }} + /> + <path + style={{ + fill: 'none', + stroke: '#f5980c', + strokeWidth: 9, + }} + d="M27.5 226.81v33.03M467.93 226.81v33.03" + /> + <ellipse + cx={247.8} + cy={259.84} + rx={224.8} + ry={64.23} + style={{ + fill: '#f5980c', + }} + /> + <path + d="M465.11 225c0 5.06-3.66 11.54-14.37 18.78-10.45 7.06-26.07 13.71-46 19.42-39.84 11.38-95.31 18.52-156.9 18.52s-117.06-7.14-156.9-18.52c-20-5.71-35.58-12.36-46-19.42C34.15 236.52 30.5 230 30.5 225s3.65-11.55 14.36-18.79c10.46-7.06 26.07-13.71 46-19.42 39.84-11.38 95.32-18.53 156.9-18.53s117.06 7.15 156.9 18.53c20 5.71 35.59 12.36 46 19.42 10.79 7.22 14.45 13.7 14.45 18.79Z" + style={{ + fill: '#f2c94c', + stroke: '#f5980c', + strokeWidth: 15, + }} + /> + <path + style={{ + fill: 'none', + stroke: '#f5980c', + strokeWidth: 9, + }} + d="M89.89 146.06v33.04M530.32 146.06v33.04" + /> + <ellipse + cx={310.2} + cy={179.1} + rx={224.8} + ry={64.23} + style={{ + fill: '#f5980c', + }} + /> + <path + d="M527.5 144.23c0 5.06-3.65 11.55-14.36 18.78-10.46 7.07-26.07 13.72-46 19.42C427.26 193.82 371.78 201 310.2 201s-117.06-7.14-156.9-18.53c-20-5.7-35.59-12.35-46-19.42-10.71-7.23-14.37-13.72-14.37-18.78s3.66-11.55 14.37-18.79c10.45-7.06 26.07-13.71 46-19.42 39.84-11.42 95.31-18.56 156.9-18.56s117.06 7.14 156.9 18.5c20 5.71 35.58 12.36 46 19.42 10.75 7.26 14.4 13.75 14.4 18.81Z" + style={{ + fill: '#f2c94c', + stroke: '#f5980c', + strokeWidth: 15, + }} + /> + </svg> +); + +export default Stakepile; diff --git a/src/config/validators/Stakeplus.tsx b/src/config/validators/Stakeplus.tsx new file mode 100644 index 0000000000..c7777d3e8a --- /dev/null +++ b/src/config/validators/Stakeplus.tsx @@ -0,0 +1,41 @@ +const Stakeplus = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400"> + <path + d="M169.48 176.66a7.35 7.35 0 0 0-.28 2.59v2.31l1.5.24c.82.14 1.91.33 2.4.43l.91.19-.11 18.28-.1 18.3-1.41.23c-2.2.35-2.39.61-2.39 3.2v2.38h16.8v-2.38c0-2.59-.19-2.85-2.39-3.2-1.5-.24-2-1.85-1.43-4.27.2-.83 4.47-4.5 4.75-4.11l2.79 3.63c2.91 3.77 3.28 5 1.68 5.37-.93.23-1 .42-1 2.6v2.36h16.4v-5.19l-1.67-.27c-2.07-.33-5.67-4.08-8.93-9.3a5.79 5.79 0 0 0-.61-.84c-2.55-2.42-3.1-4.51-1.44-5.48.29-.18 1.84-1.49 3.43-2.92 3.12-2.81 4.36-3.6 6.22-4 1.18-.25 1.2-.29 1.2-2.84v-2.57h-16.4l-.12 2.36c-.13 2.52.17 3 1.82 3 .5 0 .9.26.9.56 0 .64-7.88 8.05-8.56 8.05-.31 0-.5-4.26-.64-14.4l-.2-14.4-6.42-.11a43.42 43.42 0 0 0-6.7.17m-85.08 3.15c-13.75 3.93-14.34 18.08-1 23.61a47.27 47.27 0 0 0 5.4 2c6.81 2.13 9.29 4.2 9.29 7.73 0 5.76-8.8 8-15.88 4.09-1.22-.67-1.21-.64-2.15-5.6l-.27-1.4-3.3-.12-3.29-.09v10.66l3 1.51c6.7 3.36 17.62 4.12 23 1.61 12.14-5.66 9.63-19.27-4.41-23.94-10.34-3.44-12.21-4.73-12.18-8.4 0-4.57 7.58-7.5 12.91-5l1.67.79c.68.33 1.92 3.67 2 5.32v1.1h6.4v-4.73c0-5.48-.07-5.6-4.17-7.45-4.34-2-13-2.8-17-1.64m30.31 7.31-.13 3.86-2.2.2-2.2.2v5.6l1.93.22c2.66.31 2.65.28 2.69 10.38 0 12.45.24 13.34 3.66 16.11 1.61 1.3 6 1.86 9.52 1.2 2-.37 2.31-1 1.81-4.39l-.26-1.72h-1.76c-4.31 0-4.59-.74-4.59-12 0-9.9-.24-9.27 3.6-9.56l2.6-.2.12-2.44c.14-3-.14-3.28-3.53-3.44l-2.59-.16-.2-3.8-.2-3.8-4.08-.11-4.09-.12-.12 3.92m107.69 4a10 10 0 0 0-3.06.7 8.17 8.17 0 0 1-1.5.58c-.88 0-5.06 4.27-5.89 6-4.59 9.62-1.61 21.33 6.44 25.29 2.86 1.41 11.79 1.92 14.06.8.52-.25 1.67-.79 2.55-1.18 2.7-1.2 2.81-1.45 1.79-4-1.08-2.67-1.27-2.8-2.84-2-5.75 2.87-11.54 1.91-14-2.31-1-1.69-1.54-4-1.09-4.34.06-.05 4.51-.18 9.9-.3l9.8-.22.12-3c.45-11.2-5.26-16.84-16.32-16.12m-81.05.88c-1.12.39-2.7 1-3.51 1.41l-1.47.69.12 4.05.11 4h6l.24-1.77c.36-2.69 1.75-3.63 5.45-3.63 4.4 0 6.71 2.88 5.83 7.28l-.22 1.12h-4.05c-10.76 0-15.88 4.3-14.92 12.47.86 7.24 14.36 10.58 18 4.46.57-1 1.84-.63 2.11.56.13.6.31 1.32.39 1.6s1.59.5 5.57.5h5.41v-4.73l-1.7-.57-1.7-.57-.2-10c-.24-12.17-.91-14-5.8-16.34l-2.29-1.09c-1.33-.65-11.12-.31-13.36.46m86.21 5.58c2.19 1.12 3.82 5 2.85 6.8-.6 1.13-11.06 1.28-11.49.16-1.52-3.95 4.71-9 8.64-7m-73.41 13.67c.75 3.73.13 5.45-2.46 6.86-4.85 2.64-10.07-.12-8.29-4.38 1.07-2.55 3.34-3.65 7.65-3.7h2.85l.25 1.25" + style={{ + fill: '#f6f6f6', + fillRule: 'evenodd', + }} + /> + <path + d="M0 200v200h400V0H0v200m183.1-9.9c.06 7.86.25 14.3.43 14.3a6.68 6.68 0 0 0 1.68-1.41 44.89 44.89 0 0 1 3.48-3.1c2.31-1.84 2.66-2.69 1.11-2.69-1 0-1 0-1-3.21v-3.2l9.1.1 9.1.11v5.87l-2.35.81a13.54 13.54 0 0 0-3.38 1.67c-1.42 1.18-4.4 3.84-5.69 5.08l-1.07 1 1.15 1.56c.64.87 1.39 1.85 1.67 2.18s1.31 1.69 2.29 3 2.18 2.84 2.68 3.39a3.54 3.54 0 0 1 .9 1.44c0 .51 2.53 1.68 4 1.84 1 .11 1 .25 1.12 3.22l.11 3.1H190.8v-2.78c0-2.29.14-2.87.77-3.31.86-.6.41-1.5-2.74-5.47-1.32-1.67-1.32-1.67-2.38-1-3.92 2.56-4.4 5.35-1 6.05l1.55.35.12 3.09.11 3.09-9.11-.1L169 225l-.12-2.54c-.14-2.91.28-3.66 2-3.66 2.76 0 2.68.57 2.68-18.28 0-15.79 0-17-.7-17.17-.39-.12-1.06-.38-1.5-.57a8 8 0 0 0-1.6-.46c-.7-.11-.81-.48-.92-2.88-.18-4.14-.7-3.87 7.21-3.75l6.91.11.1 14.3M59.9 176.38c.39.22 1.78.86 3.1 1.44s3.35 1.49 4.51 2a11.66 11.66 0 0 0 2.71 1c1.32 0 1 1.44-1.68 6.7-3.66 7.35-3.36 7.13-7.54 5.3l-3-1.32c-5.18-2.28-7.85-2-9.18 1-.54 1.21-.07 4.57.79 5.7 1.27 1.66 2 9.45 1.14 11.68-1.76 4.44-2.81 5.63-6.37 7.25a27.77 27.77 0 0 1-7.13 2.07c-.62 0-1.14.64-2.06 2.5-1.9 3.84-1.93 3.85-4.88 2.52l-8-3.57a8 8 0 0 0-1.9-.65c-1 0 3.76-11.16 5.53-12.93.37-.37 2.08.2 7 2.33.66.28 1.65.69 2.2.9s1.47.62 2 .89c1.3.61 3.33 0 5.25-1.62s2-4.89.49-9.37c-3-9 .32-17.21 7.1-17.37 3.23-.08 4.48-.82 5.69-3.39 1.73-3.64 2.3-4.06 4.12-3.06M297.9 197c.12 22.42.05 21.84 2.33 21.84 1.74 0 2.25.94 2.09 3.91l-.12 2.25-9 .11c-10 .12-9.61.21-9.62-2.58s.52-3.73 2.37-3.73c2.15 0 2.06.92 1.93-18.8l-.11-17-1.88-.46c-2.65-.66-2.72-.74-2.72-3.13 0-3.64-.46-3.44 7.55-3.32l7.05.11.1 20.76M98 179.27c2.82.85 6.52 2.62 7.64 3.66.71.66.77 1.19.67 6l-.11 5.27-3.69.12-3.68.11-.59-2.11a29.23 29.23 0 0 1-.72-3.11c-.24-2-9.09-4.08-10.12-2.41a1.28 1.28 0 0 1-1 .4 4.1 4.1 0 0 0-2 1.41c-2.4 2.74-1.4 5.53 2.64 7.4 1.06.49 2.23 1 2.61 1.24a3.9 3.9 0 0 0 1.43.35 2.93 2.93 0 0 1 1.5.59 3.46 3.46 0 0 0 1.69.6 4.26 4.26 0 0 1 1.59.36l3.1 1.5c10.84 5.21 11.64 18.55 1.4 23.4a24.8 24.8 0 0 1-15.73 1.75 19.15 19.15 0 0 0-3.1-.6 3.91 3.91 0 0 1-1.47-.35L76.2 223l-3.2-1.6V209l3.3-.12 3.29-.11.8 1.31a6.41 6.41 0 0 1 .81 3.29c0 2.08.78 3.43 2 3.43a1.89 1.89 0 0 1 1.15.49c.85.86 9.63.67 11-.23a5.88 5.88 0 0 0 2.31-3.93c0-1.36-2.44-4.73-3.43-4.73a1.61 1.61 0 0 1-1-.6 2.06 2.06 0 0 0-1.42-.6 1.58 1.58 0 0 1-1.18-.4 1.91 1.91 0 0 0-1.35-.4 3.54 3.54 0 0 1-1.87-.59 3.19 3.19 0 0 0-1.49-.6 3.77 3.77 0 0 1-1.39-.36l-3.1-1.5c-6.4-3.08-9.81-8.85-8.21-13.92.21-.68.48-1.71.6-2.3.48-2.44 4.74-6 9.61-8 1.9-.78 11.88-.7 14.6.12m171.24.34a19.51 19.51 0 0 1 7.34 3.27 2.94 2.94 0 0 0 1.06.72c.5 0 3.56 5.14 3.56 6a13.91 13.91 0 0 0 .47 2.58c.71 2.65-.43 7.47-2.43 10.24-3.32 4.61-6.76 6-15.64 6.36l-6.2.24-.11 4.57c-.13 5.05-.06 5.21 2.33 5.22 2 0 2.38.58 2.38 3.69v2.72H242.8v-3c0-3.22.16-3.44 2.49-3.44s2.28 0 2.3-16.89v-15.5l-1.1-.24c-3.77-.83-3.7-.76-3.7-3.73a9.76 9.76 0 0 1 .27-3c.43-.43 23.81-.31 26.17.14M123.77 183c.48.48.63 1.48.63 4.18v3.55l2.9.12 2.9.12.12 3.14c.15 4-.08 4.26-3.38 4.26h-2.55l.1 9.5.11 9.5 2.39.12c3.33.17 3.55.51 3.92 6 .25 3.73-10.27 3.72-14.09 0-2.62-2.56-3-4.29-3.14-15.42l-.14-9.7H112c-2.39 0-2.85-.74-2.72-4.35l.12-3.05 2.1-.12 2.1-.12V187c0-3.16.11-3.82.7-4.17 1.2-.7 8.73-.54 9.47.2m133.63 4.23a65 65 0 0 0-.11 7.15l.11 6.59h4.6c5.3 0 6.22-.21 7.62-1.46a7.47 7.47 0 0 0-.83-11.52c-1.55-1.1-11-1.71-11.39-.73M154 190.6a13.82 13.82 0 0 1 8.76 6.57c.43.78.63 3.48.81 11.23l.23 10.2 1.6.2 1.6.2.12 3.1.11 3.1H161c-5.82 0-6.22 0-6.22-.75 0-1.62-1.07-1.82-2.68-.49a5.61 5.61 0 0 1-2.1 1.24 1.7 1.7 0 0 0-1.05.42c-2.48 2.48-14.14-1.7-14.14-5.07a5.34 5.34 0 0 0-.6-1.6c-1.74-3.35 1.6-12.13 4.62-12.15a1.79 1.79 0 0 0 1.09-.47c1-1 3.66-1.53 8.59-1.79l5.1-.26v-1.48a5.41 5.41 0 0 0-7.88-4.94 3.73 3.73 0 0 0-1.52 1.1c-.07.24-.37 1.3-.66 2.35l-.52 1.92-3.61-.12-3.62-.11-.11-4.3c-.14-5 0-5.46 2.58-6.44l2.73-1c2.61-1.06 8.93-1.35 13-.61m74.69-.14c4.79 1.15 9.71 5.55 9.71 8.68a1.39 1.39 0 0 0 .4 1.06c.24.15.4 2.47.4 5.62v5.38h-19.69l.28 1.1c1.34 5.39 7.62 7.41 13.61 4.38 2.28-1.16 2.36-1.12 4 1.84s1.07 4.41-2.19 5.71l-1.4.58a12 12 0 0 1-1.8.53c-.55.11-1.81.41-2.8.67-2.11.54-7.44.31-8.1-.35a1.91 1.91 0 0 0-1.17-.46c-2.1 0-7.41-4.33-8.71-7.1-.28-.61-.71-1.46-.94-1.9-1.59-3-1.61-13.15 0-15.69a3.83 3.83 0 0 0 .57-1.4c.05-1.15 3.12-4.93 5.18-6.38a16 16 0 0 1 12.69-2.27m133.91 0a23.9 23.9 0 0 1 7.91 2.06l1.51.74-.11 4.68-.11 4.69h-3.16c-3.38 0-3.56-.1-4-2.33-.92-4.33-9.41-4.13-9.41.22 0 1.3 1.5 2.55 3.75 3.13.91.23 2.1.57 2.65.75s1.81.57 2.8.86c6.22 1.79 8.81 4.75 8.79 10.06 0 8.05-8.89 12.72-19.48 10.27-8-1.85-8.1-1.94-8.1-7.16 0-5.7-.06-5.6 3.31-5.6s4.28.6 4.28 2.68c0 2.49 1.36 3.32 5.4 3.32 3.11 0 3.53-.09 4.42-1 2.21-2.22.39-4.61-4.42-5.78l-3-.76c-11.17-2.89-13.58-13.95-4.21-19.32a9.07 9.07 0 0 1 3.41-1.18 7.4 7.4 0 0 0 2.2-.38c1.1-.47 1.92-.47 5.6 0M317.2 202.6c.25 14.78.47 15.4 5.31 15.4a5.26 5.26 0 0 0 3.83-1.2l1.66-1.2v-8.55c0-9.31.05-9.11-2.4-9.74l-1.4-.36V191l6.37-.11c4.15-.07 6.52 0 6.8.32s.5 5.08.63 13.9l.2 13.48 1.54.25c2 .32 2.29.84 2.16 3.76l-.1 2.4H329l-.12-1.5c-.15-1.71-.44-1.85-1.52-.66-4.34 4.79-14.26 3.89-17.51-1.59-1.83-3.1-2-3.89-2.14-13.72l-.18-9.46-1.07-.4c-.58-.22-1.38-.5-1.76-.62-.72-.23-1.08-5.34-.42-6a39.61 39.61 0 0 1 6.5-.17l6.22.12.2 11.6m-94.7-4c-.71.36-1.3.89-1.3 1.18a.74.74 0 0 1-.35.65 3.6 3.6 0 0 0-.64 1.83l-.28 1.7h9.67v-1.7c0-3.47-3.72-5.39-7.1-3.66m-77 13.66c-3.24 3.52-.91 6.9 3.95 5.73 3.12-.75 4.12-1.86 4.12-4.58v-2.21h-3.53c-3.33 0-3.58.06-4.54 1.1" + style={{ + fill: '#040404', + fillRule: 'evenodd', + }} + /> + <path + d="M284.24 177a11 11 0 0 0-.24 2.58v1.94l1.9.28c3.11.44 2.9-.91 2.9 19v17.95l-1.1.23c-3.1.68-3.3.89-3.3 3.43v2.35h17.24l-.12-2.5-.12-2.5-1.6-.4a10.75 10.75 0 0 1-2.1-.72c-.39-.26-.5-4.83-.5-20.67 0-11.19-.11-20.63-.24-21-.21-.54-1.15-.63-6.36-.63s-6.15.09-6.36.63m-226.51.27a20.1 20.1 0 0 0-1.15 2.1c-1.69 3.48-2 3.8-4.22 3.8-8.11 0-12.26 7.89-8.73 16.6 2.67 6.56 1.24 10.44-4.54 12.35-.61.2-5.13-1.33-7.39-2.5a23.91 23.91 0 0 0-4.7-1.74c-.65-.09-1.25.78-3.28 4.76-2.7 5.27-3.3 6.93-2.52 6.93.47 0 1.3.34 5.6 2.3 6.55 3 5.94 3.13 8.6-2 .8-1.55 1-1.65 4.9-2.13a5.45 5.45 0 0 0 2.32-.62 1.12 1.12 0 0 1 .87-.36 4.92 4.92 0 0 0 1.95-1 6 6 0 0 1 1.64-1c.74 0 2.51-3.44 3.09-6 .74-3.25.1-7.46-1.8-11.81-2.17-5 2.4-9.57 7.23-7.24a16.19 16.19 0 0 0 2.46 1 49.17 49.17 0 0 1 5.68 2.46c.94.5.69.86 3.68-5.06 3.44-6.79 3.58-6.23-2.12-8.62l-1.8-.76-2.78-1.2c-2.19-1-2.5-1-3-.32m31.37 1.78a5.44 5.44 0 0 0 1.8 0c.49-.09.09-.17-.9-.17s-1.39.08-.9.17m6.3.52a4.36 4.36 0 0 0 1.4.34c.38 0 .3-.12-.2-.34a4.36 4.36 0 0 0-1.4-.34c-.38 0-.3.12.2.34m147.8 2.78c0 2.32 0 2.42 1 2.42a7.81 7.81 0 0 1 2.41.58l1.4.74-.11 16.52-.09 16.48-1.2.12c-2.91.28-3.4.76-3.4 3.34v2.36l9.1-.11 9.1-.11.12-2c.14-2.41-.38-3-2.85-3.34L257 219v-10.8l6.6-.25a40.66 40.66 0 0 0 8-.8c4-1.55 7.8-4.91 8.28-7.26a2.59 2.59 0 0 1 .57-1.32 14.34 14.34 0 0 0 .35-4.21c0-6.61-2.22-10.2-8-12.93l-2.61-1.23-13.5-.12-13.49-.08v2.42m-125.89.72a25.38 25.38 0 0 0 3.6 0c.93-.08.07-.15-1.91-.15s-2.74.07-1.69.16m-2.91 4c-.11 2.25-.06 3.94.12 3.77a15.13 15.13 0 0 0 .19-4.09l-.11-3.83-.2 4.08m-8.72 1.8-.08 4.69-3.3.13-3.3.14 3.31.08c4 .1 3.76.4 3.59-5.35l-.13-4.37-.09 4.68m-16.37-3a7 7 0 0 0 2 0c.49-.09 0-.17-1.11-.17s-1.5.08-.89.18m177.75.61q5.34 1.42 5.34 7.57c0 5.83-3.29 8.09-11.4 7.85l-4-.12-.11-7.39c-.06-4.06 0-7.62.09-7.9.26-.68 7.53-.69 10.08 0M73.69 191.6c0 .77.09 1.08.19.7a3.3 3.3 0 0 0 0-1.4c-.1-.39-.19-.07-.19.7m8.8 0c0 .77.08 1.08.19.7a3.3 3.3 0 0 0 0-1.4c-.11-.39-.19-.07-.19.7m64-.5a37.9 37.9 0 0 0 4.4 0c1.15-.08.11-.14-2.31-.14s-3.36.07-2.09.15m75.59 0a3.3 3.3 0 0 0 1.4 0c.38-.1.07-.19-.7-.19s-1.09.09-.7.19m3.61 0a7 7 0 0 0 2 0c.49-.09 0-.17-1.11-.17s-1.5.08-.89.18M205.8 194c0 1.5.13 2.64.3 2.54a10.9 10.9 0 0 0 0-5.08c-.17-.1-.3 1-.3 2.54m99.09-2.33c-1.24 1.24-.31 5.12 1.22 5.14 2.05 0 2.07.1 2.34 10.28.29 11.41.75 13.38 3.7 15.82 4.14 3.42 12.8 2.64 15.64-1.42.73-1 1.09-.71 1.61 1.44l.4 1.67H341l.12-1.94c.15-2.3-.65-3.46-2.36-3.46s-1.75-.17-2-14.4l-.2-13.4-5.55-.11c-6.26-.13-6.65 0-6.65 2.39 0 2.17.59 3.12 1.93 3.12 2 0 2.07.28 2.07 10.08 0 10.86-.55 11.93-6.09 11.91-5 0-5.27-.84-5.51-15.32l-.2-12.07-5.61-.11c-4-.08-5.75 0-6.1.38m47.27.7c-7.2 3.33-7.7 13.47-.82 16.61a20.52 20.52 0 0 0 5 1.82c4.17 0 8.89 3.29 8.26 5.76-1.06 4.25-11.77 4.12-11.77-.14 0-2.1-1-2.82-3.82-2.82h-2.57v9l1.1.46c3.78 1.55 5.61 1.85 11.34 1.9s6.56-.1 8.76-1.41c.33-.2.86-.48 1.17-.62 2.11-1 4-4.84 4-8.15 0-3.68-4.89-8.78-8.45-8.78a1.22 1.22 0 0 1-.94-.4 1.38 1.38 0 0 0-1-.4c-3.65 0-7.55-2.58-7.55-5 0-4.86 10.12-5 10.48-.1l.12 1.69H371v-8.37l-1.8-.67c-3.73-1.39-4.78-1.56-9.63-1.54-4.59 0-5.16.1-7.41 1.15m-118.56.95a9.18 9.18 0 0 0 1.3 1.3l1.3 1.18-1.2-1.29c-1.09-1.21-1.42-1.48-1.42-1.18m-97.51 2.28c0 .77.09 1.08.19.7a3.3 3.3 0 0 0 0-1.4c-.1-.39-.19-.07-.19.7m-25.72 1.73a3.57 3.57 0 0 0 1.88.19l1.55-.14-1.88-.19c-1-.11-1.74 0-1.55.14m16.43-.13-2.6.26 2.48.07a5.63 5.63 0 0 0 2.72-.33c.14-.22.19-.37.12-.33s-1.29.19-2.72.33m97.7-.12a3.3 3.3 0 0 0 1.4 0c.39-.1.07-.19-.7-.19s-1.08.09-.7.19M114.57 207c0 4.84.06 6.76.13 4.28s.07-6.45 0-8.8-.13-.32-.13 4.52m81.43-4.12c-1.21 1.13-1.66 1.67-1 1.22 1.14-.78 3.75-3.32 3.38-3.28a25.22 25.22 0 0 0-2.38 2.06m-33.12 7.2a67.92 67.92 0 0 0 .22 8.86c.16.1.25-2.45.2-5.68-.19-10.82-.34-11.92-.42-3.18m-26.22-7.58a14.56 14.56 0 0 0 5.88 0c.1-.16-1.22-.3-2.94-.3s-3 .14-2.94.3M103 204.4a11.21 11.21 0 0 0 1.76 1.6 6.87 6.87 0 0 0-1.36-1.6 11.21 11.21 0 0 0-1.76-1.6 6.87 6.87 0 0 0 1.36 1.6m82-.39c-.71.74-1.4 1.12-1.78 1s-.51-.14-.1.27.79.23 2-1c.82-.82 1.39-1.49 1.27-1.49A6.52 6.52 0 0 0 185 204m34 0v1l5.37.11a28.55 28.55 0 0 0 5.68-.2c.17-.17-2.19-.26-5.24-.19-5.52.12-5.55.12-5.72-.8s-.16-.83-.13.08m-70.05 1.1a25.38 25.38 0 0 0 3.6 0c.93-.08.07-.15-1.91-.15s-2.74.07-1.69.16m60.81 3.5c0 1.32.08 1.81.17 1.09a10.69 10.69 0 0 0 0-2.4c-.1-.6-.17 0-.17 1.31M74.4 209.85c2.35.3 5.51.33 5.34.05a10.19 10.19 0 0 0-3.26-.24c-1.7 0-2.63.12-2.08.19m75.11.44a19 19 0 0 0 3.2 0c.82-.08 0-.15-1.71-.15s-2.43.07-1.49.15m79.49.11-9.6.22 9.48.09c6 .06 9.56-.06 9.72-.31s.19-.36.12-.31-4.44.19-9.72.31m-10.11 1.6c0 .77.09 1.09.19.7a3.3 3.3 0 0 0 0-1.4c-.1-.39-.19-.07-.19.7m-64.77 1.6c0 1.21.08 1.7.17 1.1a9 9 0 0 0 0-2.2c-.09-.6-.17-.11-.17 1.1m-19.6 2.2c0 1.32.08 1.81.17 1.09a10.69 10.69 0 0 0 0-2.4c-.09-.6-.17 0-.17 1.31m-8 2.89a9 9 0 0 0 2.2 0c.6-.09.11-.17-1.1-.17s-1.71.08-1.1.17m-37.8.4a12.55 12.55 0 0 0 2.6 0c.71-.09.13-.16-1.3-.16s-2 .07-1.3.16m58.2 0a9 9 0 0 0 2.2 0c.61-.1.11-.17-1.1-.17s-1.71.07-1.1.17m79.81 0a4 4 0 0 0 1.59 0c.39-.09 0-.17-.9-.17s-1.19.09-.69.18m-61.41.58c1 .38 1.1.58 1.18 2.87l.07 2.47.15-2.39c.15-2.49-.38-3.45-1.88-3.38-.34 0-.12.21.48.43m4.73.11a5.52 5.52 0 0 0-.33 2.9l.15 2.33.07-2.48c.06-1.89.25-2.58.78-2.89s.5-.41.25-.42a1.52 1.52 0 0 0-.92.56m66.8-.37a4.51 4.51 0 0 0 .57 1.4c.31.55.57.82.57.6a4.51 4.51 0 0 0-.57-1.4c-.31-.55-.57-.82-.57-.6m-29.1 3c0 1.43.07 2 .16 1.3a12.55 12.55 0 0 0 0-2.6c-.09-.72-.16-.13-.16 1.3m5.47-2.28a10.29 10.29 0 0 0 1.3 1.31l1.3 1.17-1.18-1.3c-1.09-1.21-1.42-1.48-1.42-1.18M116 221.73a10.08 10.08 0 0 0 1.3 1.29l1.3 1.18-1.17-1.3c-1.1-1.21-1.43-1.48-1.43-1.17m37 .26c-.14.24 0 .3.44.14a1.12 1.12 0 0 1 1.27.5c.55.71.58.71.34 0a5.46 5.46 0 0 1-.25-.9c0-.35-1.56-.15-1.8.22m-68.9 3.09a3.3 3.3 0 0 0 1.4 0c.39-.1.07-.19-.7-.19s-1.09.09-.7.19m4 .41a26.77 26.77 0 0 0 3.8 0c1-.08.19-.15-1.9-.15s-2.94.07-1.9.15m35.2 0a9 9 0 0 0 2.2 0c.61-.09.11-.17-1.1-.17s-1.71.08-1.1.17m20.8 0a9 9 0 0 0 2.2 0c.6-.09.11-.17-1.1-.17s-1.71.08-1.1.17m80.19 0a14.54 14.54 0 0 0 2.8 0c.83-.09.25-.16-1.29-.16s-2.22.07-1.51.16" + style={{ + fill: '#53b96a', + fillRule: 'evenodd', + }} + /> + <path + d="M168.8 179c0 3 0 3 1.1 3.25.61.13 1.69.42 2.4.64l1.3.39v17.29c0 18.73.06 18.25-2.23 18.25-1.81 0-2.23.73-2.09 3.66l.12 2.54H187l.12-2.54c.14-2.86-.29-3.68-1.87-3.61-1 0-1 .05.15.3s1.2.31 1.32 2.95l.12 2.7H169.6v-2.32c0-2.52.44-3.28 1.91-3.28 2.63 0 2.49 1 2.48-18.31s.15-18.32-2.6-18.77l-2-.34-.12-2.69-.12-2.69h13.63l.11 14.3c.07 10.25.23 14.3.55 14.3s3.16-2.35 7.66-6.48c1.15-1.06.87-1.72-.73-1.72h-1.17v-5.62l8.5.11 8.5.11.12 2.51c.11 2.29 0 2.54-.8 2.89l-.92.39 1-.09c1-.1 1-.19 1-3.1v-3H189v6l1.35.2 1.35.2-4 3.7a54.52 54.52 0 0 1-4.25 3.7c-.12 0-.28-6.43-.34-14.3l-.1-14.3-7.1-.11-7.1-.11v3m-85.4.47-1.2.58 1.2-.24c.66-.14 2.1-.39 3.2-.57l2-.31h-2a8.78 8.78 0 0 0-3.2.56m10.6-.24a21 21 0 0 1 8.13 2.38c3.75 1.82 3.92 2.15 3.78 7.66l-.11 4.55-3.16.12c-2.25.08-3.28 0-3.54-.4s-.32-.38-.2.08c.21.77 6.08 1.08 6.94.37.66-.55.81-8.47.19-10.09-.76-2-7.39-4.7-12-4.9l-2.6-.1 2.6.33m-14 1.72c-2.78 1.69-6 5-6 6.2 0 .32.47-.12 1-1a13.39 13.39 0 0 1 5.23-4.95 6.27 6.27 0 0 0 1.73-1c0-.36-.58-.15-2 .73m34.3 2.14a16.67 16.67 0 0 0-.27 4v3.71l-2.1.12-2.1.12v6.8l2 .2 2 .2.2 10.07c.11 5.53.3 10.16.42 10.28s0-19.67-.22-20.43c-.09-.26-1-.53-2.08-.6l-1.92-.12-.12-3.08-.11-3.08 2.11-.12 2.12-.12.11-3.87c.1-3.27.23-3.9.8-4.06s.58-.19.06-.22a1.3 1.3 0 0 0-.9.22m8.43 0c.34.16.5 1.31.5 3.71 0 4.26.16 4.45 3.7 4.45h2.73l-.11 3.1-.12 3.1-2.58.12c-3.77.18-3.62-.17-3.6 8.63 0 11.39.32 12.25 4.22 12.25 1.3 0 2.17.19 2.3.5s.21.22.23-.2c0-.59-.34-.7-2.34-.7-3.76 0-3.66.29-3.54-10.54l.11-9.26 2.8-.2 2.8-.2V191l-2.89-.12-2.88-.11-.12-3.88c-.1-3.54-.18-3.89-.91-4-.44 0-.57 0-.3.13M85.46 187a6.13 6.13 0 0 0-2 1.67c-.31.58-.13.52.74-.27a9.11 9.11 0 0 1 4.56-2.09c.72-.12.61-.17-.4-.19a6.81 6.81 0 0 0-2.94.88m6.74-.54c1.1.18 2.54.43 3.2.57 1.19.25 1.19.25-.15-.37a8.38 8.38 0 0 0-3.2-.56l-1.85.05 2 .31m-18.81 3.74a12.43 12.43 0 0 0 1.47 7.59c.92 1.37 4 4.32 4.25 4.08A7.71 7.71 0 0 0 77.4 200c-2.83-2.6-3.53-4.27-3.53-8.45s-.11-4.34-.48-1.34m24.66 0a3.89 3.89 0 0 0 .35 1.8c.49.76.49-.47 0-2-.31-1-.32-1-.35.2m-15.18 1.22q-.12 3.24 3.53 5c.95.45 1 .44.2-.14-3.26-2.48-3.35-2.62-3.51-4.75L83 189.4l-.08 2m62.93-1c-.52.22.54.34 3 .33s3.41-.12 2.6-.33a12.5 12.5 0 0 0-5.6 0m75.8.16a13.83 13.83 0 0 0-9.5 6.58c-1.17 2-2.29 4.8-2 5.09.09.1.41-.59.71-1.53 1.94-6.06 6.55-9.45 13.39-9.82 2.62-.14 3.65-.32 3-.51a11.51 11.51 0 0 0-5.6.19m-80.4.74-3 1.34c-1.9.84-2.56 1.54-2.54 2.69 0 .26.23 0 .51-.53.56-1.12 2.22-2 5.48-3l2.6-.79c.22-.07-.14-.13-.8-.13a6.34 6.34 0 0 0-2.2.44m12.6-.12c5 1.24 8.22 4.5 9 9l.28 1.6.05-1.52c.13-3.92-5.51-9.58-9.41-9.44h-1.14l1.2.3m75.67 0c6.13 1.46 9.56 7.33 9.26 15.82l-.13 3.6-9.8.21-9.88.23h20l.12-4.4c.23-8.37-4.49-16-9.81-15.76-1.1 0-1.09 0 .16.34M88.55 197.4a5.24 5.24 0 0 0 1.6.59c.65 0-1.43-1.16-2.15-1.17-.33 0-.08.25.55.58m47.61 2.4v3.2h3.4c3.33 0 3.4 0 3.5-1l.09-1-.39.92c-.36.85-.59.91-3.29.8l-2.91-.12-.24-3-.23-3v3.2m12-2.64c3.94.47 5 1.5 5.41 5.44l.27 2.4.06-2.34c.1-3.81-2-5.92-5.82-5.79l-1.92.06 2 .23m-4.13.86a2.65 2.65 0 0 0-.83 1.9c0 1.05 0 1 .41-.21a3.4 3.4 0 0 1 1.46-1.89c.65-.35.84-.59.46-.6a2.87 2.87 0 0 0-1.5.8m58.24-.02c-2.1 1.32-8 6.94-8 7.61a27.85 27.85 0 0 0 3.2 4.8c5.08 6.65 6.34 8.16 7 8.36.33.11.15-.24-.4-.76s-2.35-2.76-4-5-3.1-4.09-3.22-4.2-.74-.89-1.38-1.74l-1.15-1.54 2.57-2.47c2.88-2.77 5.28-4.66 6.78-5.34.55-.25.73-.47.4-.48a4.44 4.44 0 0 0-1.8.72m20.4 0a7 7 0 0 0-3.4 5.93v.95l5.5-.11 5.5-.11.07-1.4c.07-1.36.06-1.37-.2-.2l-.28 1.2-5.14.11-5.14.12.28-1.5c.66-3.53 2.35-5.1 5.45-5.05 1.07 0 1.85-.13 1.73-.33-.38-.62-3.13-.37-4.37.39m-131.2.46c1.1.47 1.49.47 1.2 0a1.42 1.42 0 0 0-1.12-.37c-.85 0-.85 0-.08.37m137.09.7a5.56 5.56 0 0 1 1.22 1.9c.19.76.2.76.24-.05s-1.35-2.95-2-2.95c-.25 0 0 .49.59 1.1m-134.14.32a7.22 7.22 0 0 0 1.8.57c.79 0-1.51-1.16-2.35-1.16-.55 0-.38.18.55.59m3.85 1.48c.88.5 2.12 1.17 2.75 1.5a16.43 16.43 0 0 1 3.15 2.8c1.1 1.21 1.8 1.83 1.57 1.38-1.06-2.08-7-6.59-8.59-6.57-.26 0 .24.4 1.12.89m-18.6 1.25c0 .33 5.09 2.58 7.73 3.43s2 .2-.84-.76c-1.48-.5-3.58-1.35-4.67-1.87-2-1-2.22-1-2.22-.8m129.79 4.09a27.93 27.93 0 0 0 .06 5.5c.29 1.7.36 1.09.39-3.44 0-6.38-.06-6.82-.45-2.06m-66.59-1.06c-.33.18-1.56.75-2.75 1.26-2.24 1-5.28 3.95-5.22 5.12 0 .54.07.54.25 0 1-2.92 6.16-5.92 11.12-6.53.67-.08.31-.17-.8-.19a7.66 7.66 0 0 0-2.6.3m-52.3 1.44a4.9 4.9 0 0 0 1.8.58c.36 0 0-.27-.7-.6-1.76-.76-2.58-.75-1.1 0m2.65.75a5.87 5.87 0 0 0 1.4 1c6.31 3.72 3.08 9.87-5.16 9.81-2.77 0-3.67.09-3 .34 6.87 2.59 14.39-3.68 10.32-8.62-1.05-1.27-3.56-3.06-3.56-2.53m12.82.73a1.15 1.15 0 0 0 .38.92 3.43 3.43 0 0 1 .47 1.92l.06 1.68.16-1.48a5 5 0 0 0-.45-2.6c-.41-.75-.61-.9-.62-.44m-32.94 1.37a33.29 33.29 0 0 0-.28 5.76c0 6.27-.18 5.92 4 7.88 3.88 1.82 4.73 2.11 5.92 2.07s.71-.2-2.65-1a29.4 29.4 0 0 1-3.8-1.72l-2.87-1.48V209.8h6.4l.6 3c.76 3.8 1.95 5.21 4.36 5.2.35 0-.28-.44-1.41-1-2-.95-2-1-2.22-3.1-.34-4-.83-4.52-4.55-4.65a12.39 12.39 0 0 0-3.5.16m73.58 1.36c-3.47 1-4.58 6.19-1.63 7.53 1.52.69 2.14.52.77-.21-1.57-.85-1.79-1.22-1.79-3.15 0-2.78 1.44-3.77 5.94-4.06l3.65-.24v2.59c0 3-.74 4-3.44 4.82-1.07.31-1.33.5-.82.62a5.48 5.48 0 0 0 2.59-1l1.87-1.15.12-3.09.11-3.08h-3.11a22.15 22.15 0 0 0-4.26.38m38.22 2.31c-1.87 1.74-2.07 2.08-2 3.5l.07 1.58.16-1.4c.17-1.42 1.38-3 3.4-4.46l1.09-.78 2.56 3.5 2.57 3.5-1 .76c-.87.69-1 1-.86 3.22L191 225h17.2v-4.6l-.16 2.2-.15 2.2H191.2v-2.44c0-2.07.12-2.46.77-2.64 1.44-.37 1.19-1.25-1.43-4.89-3-4.23-3-4.2-5.66-1.71m34.35-.6c-.07 1.73 2.83 5.49 4.23 5.48.19 0-.17-.34-.78-.74a7.1 7.1 0 0 1-2.87-4c-.3-.82-.57-1.18-.58-.78m-85 .15c-.46.46-.28 5.06.25 6.6.88 2.52 4.5 6 6.24 5.91.35 0-.11-.36-1-.79a9.57 9.57 0 0 1-5.22-9.8c.23-2.24.21-2.38-.25-1.92M106.74 215c-.14.88-.37 2.05-.52 2.6-.25 1-.23.94.38-.12a5 5 0 0 0 .52-2.6l-.12-1.48-.25 1.6m103.25-.32a1.42 1.42 0 0 0 .37 1.12c.47.29.47-.1 0-1.2-.33-.77-.35-.77-.37.08m23.37 2.24c-1.2.62-2.6.87-5.6 1-2.54.11-3.71.3-3.2.5 2.19.86 7.46-.07 10.69-1.9.76-.43 2.78 4.73 2.13 5.45a16.92 16.92 0 0 1-6 3c-.5.11-.35.18.4.19 1.33 0 5.74-2.17 6.32-3.14.43-.71-2-6-2.68-5.94a10.91 10.91 0 0 0-2 .88m-22.6-.13c0 .93 4.48 6.41 5.25 6.41a9.45 9.45 0 0 0-1.89-2.22 10.86 10.86 0 0 1-2.17-2.7c0-.69-1.19-2.17-1.19-1.49m-47.37 2.07a1.43 1.43 0 0 0 1 .34c1.85 0 2.35.69 2.35 3.21v2.39H155.3l-.24-1.21c-.35-1.74-1.23-1.87-2.86-.44a11.59 11.59 0 0 1-8.37 2.3c-1.64-.16-2.23-.12-1.83.15 2.16 1.4 8.44-.09 11-2.61.79-.8 1.72-.25 1.92 1.14.12.85.26.87 6.12.87h6l.1-2.4a8 8 0 0 0-.25-2.95c-.38-.58-3.77-1.37-3.42-.79M104 220.72c-3.43 3.58-9 5.2-16.32 4.78-2.92-.17-3.54-.13-2.5.17 1.4.4 8.64.23 11.22-.27a16.59 16.59 0 0 0 8.78-5.36c1.18-1.67.95-1.53-1.18.68m102-1.64a4.27 4.27 0 0 1 1.57.66c.44.36.52.33.36-.14s-.74-.62-1.56-.66-1.07 0-.37.14m-91.15.6c-.13 1.71 3.35 5.53 5 5.51.47 0 .26-.24-.59-.66-1.55-.76-4-3.45-4.2-4.68-.12-.57-.19-.63-.23-.17m15.33 2.25c.14 2.28.07 2.56-.72 2.87l-.86.35h.91c1.23 0 1.42-.66.93-3.43l-.42-2.32.15 2.53m86.75 1.54c.26.42 2.47 1.49 4.3 2.07s7.94.6 8.77.06c.4-.26-.39-.31-2.4-.15a18.38 18.38 0 0 1-9.34-1.53c-.85-.43-1.45-.63-1.33-.45m-95.59 2.22c.61.39 5.93.38 6.54 0 .27-.17-1.2-.3-3.26-.29s-3.54.14-3.28.3" + style={{ + fill: '#5f5f5f', + fillRule: 'evenodd', + }} + /> + <path + d="M171.4 175.87c5.86.29 11.71.31 11.54 0a43.54 43.54 0 0 0-7.06-.22c-3.79 0-5.8.13-4.48.19m-114.08.63a29.66 29.66 0 0 0-1.54 2.94c-1.21 2.57-2.46 3.31-5.69 3.39-6.78.2-10.09 8.41-7.09 17.4 1.51 4.48 1.34 7.85-.49 9.37s-4 2.23-5.25 1.62c-.58-.27-1.5-.67-2-.89s-1.54-.62-2.2-.9c-4.95-2.13-6.66-2.7-7-2.33-1.86 1.77-6.63 12.93-5.62 12.93a8 8 0 0 1 1.9.65l8 3.57c2.95 1.33 3 1.32 4.88-2.52.92-1.86 1.44-2.5 2.06-2.5a27.77 27.77 0 0 0 7.13-2.07c3.56-1.62 4.61-2.81 6.37-7.25.89-2.23.13-10-1.14-11.68-.86-1.13-1.33-4.49-.79-5.7 1.33-3 4-3.3 9.18-1 7.71 3.39 6.67 3.78 10.54-4 2.64-5.26 3-6.7 1.68-6.7a11.66 11.66 0 0 1-2.71-1c-1.16-.53-3.19-1.44-4.51-2s-2.71-1.22-3.1-1.44a2.12 2.12 0 0 0-2.58.12m226.13.12c-.41 1.06-.28 4.91.17 5.19a13.31 13.31 0 0 0 2.3.73l1.88.46.11 17c.13 19.72.22 18.8-1.93 18.8-1.85 0-2.38.84-2.37 3.73s-.33 2.7 9.62 2.58l9-.11.12-2.29c.16-3-.35-3.91-2.09-3.91-2.28 0-2.21.58-2.33-21.84l-.1-20.76-7.05-.11c-6.17-.09-7.09 0-7.3.53m13.49.4c.13.35.24 9.79.24 21 0 15.84.11 20.41.5 20.67a10.75 10.75 0 0 0 2.1.72l1.6.4.12 2.5.12 2.5H284.4v-2.35c0-2.54.2-2.75 3.3-3.43l1.1-.23v-17.94c0-19.92.21-18.57-2.9-19l-1.9-.28v-1.94c0-3.35-.29-3.21 6.6-3.21 5.21 0 6.15.09 6.36.63m-236.24.59 2.78 1.2 1.8.76c5.7 2.39 5.56 1.83 2.12 8.62-3 5.92-2.74 5.56-3.68 5.06a49.17 49.17 0 0 0-5.68-2.46 16.19 16.19 0 0 1-2.46-1c-4.83-2.33-9.4 2.25-7.23 7.24 1.9 4.35 2.54 8.56 1.8 11.81-.58 2.55-2.35 6-3.09 6a6 6 0 0 0-1.64 1 4.92 4.92 0 0 1-1.95 1 1.12 1.12 0 0 0-.87.36 5.45 5.45 0 0 1-2.32.62c-3.88.48-4.1.58-4.9 2.13-2.66 5.12-2 5-8.6 2-4.3-2-5.13-2.3-5.6-2.3-.78 0-.18-1.66 2.52-6.93 2-4 2.63-4.85 3.28-4.76a23.91 23.91 0 0 1 4.7 1.74c2.26 1.17 6.78 2.7 7.39 2.5 5.78-1.91 7.21-5.79 4.54-12.35-3.53-8.71.62-16.6 8.73-16.6 2.17 0 2.53-.32 4.22-3.8 1.46-3 1.41-3 4.14-1.78m28.38 1.07a12.55 12.55 0 0 0 2.6 0c.72-.09.13-.16-1.3-.16s-2 .07-1.3.16m154 .78a9.76 9.76 0 0 0-.27 3c0 3-.07 2.9 3.7 3.73l1.1.24v15.5c0 16.93 0 16.89-2.3 16.89s-2.49.22-2.49 3.44v3H262v-2.72c0-3.11-.36-3.68-2.38-3.69-2.39 0-2.46-.17-2.33-5.22l.11-4.57 6.2-.24c8.88-.36 12.32-1.75 15.64-6.36 2-2.77 3.14-7.59 2.43-10.24a13.91 13.91 0 0 1-.47-2.58c0-.84-3.06-6-3.56-6a2.94 2.94 0 0 1-1.06-.72 19.51 19.51 0 0 0-7.34-3.27c-2.36-.45-25.74-.57-26.17-.14m29.74 2c5.77 2.73 8 6.32 8 12.93a14.34 14.34 0 0 1-.35 4.21 2.59 2.59 0 0 0-.57 1.32c-.48 2.35-4.3 5.71-8.28 7.26a40.66 40.66 0 0 1-8 .8l-6.6.25V219l1.67.22c2.47.33 3 .93 2.85 3.34l-.12 2-9.1.11-9.1.11v-2.36c0-2.58.49-3.06 3.4-3.34l1.2-.12.1-16.52L248 186l-1.39-.58a7.81 7.81 0 0 0-2.41-.58c-1 0-1-.1-1-2.42V180l13.5.12 13.5.12 2.61 1.23m-155.89 1.23a40.28 40.28 0 0 0 4.4 0c1.14-.08.1-.14-2.32-.14s-3.36.07-2.08.15m-3.15.63a24 24 0 0 0-.08 4.1l.12 3.57.19-4.1c.2-4.07.16-4.59-.23-3.57m10.37 3.87c0 2.09.07 3 .15 1.9a26.77 26.77 0 0 0 0-3.8c-.08-1-.15-.19-.15 1.9m132.86-.7c-.11.28-.15 3.84-.09 7.9l.11 7.39 4 .12c8.11.24 11.4-2 11.4-7.85s-3-8.07-11.22-8.07c-2.89 0-4.06.14-4.2.51m11.79 1.49a7.47 7.47 0 0 1 .83 11.52c-1.4 1.25-2.32 1.43-7.62 1.46h-4.6l-.11-6.59a65 65 0 0 1 .11-7.15c.38-1 9.84-.37 11.39.73m-184.59.43c-.63.65-1.06 1.26-1 1.33s.68-.46 1.31-1.19c1.41-1.66 1.21-1.75-.34-.14M73 189.83a7.89 7.89 0 0 0 .07 4c.16.26.29-.69.29-2.1 0-2.81 0-2.84-.36-1.93m284 .57a7.4 7.4 0 0 1-2.2.38 9.07 9.07 0 0 0-3.41 1.18c-9.37 5.37-7 16.43 4.21 19.32l3 .76c4.81 1.17 6.63 3.56 4.42 5.78-2.64 2.64-9.82.93-9.82-2.34 0-2.08-1-2.68-4.28-2.68s-3.31-.1-3.31 5.6c0 5.22.11 5.31 8.1 7.16 10.59 2.45 19.45-2.22 19.48-10.27 0-5.31-2.57-8.27-8.79-10.06-1-.29-2.25-.67-2.8-.86s-1.74-.52-2.65-.75c-2.25-.58-3.75-1.83-3.75-3.13 0-4.35 8.49-4.55 9.41-.22.47 2.23.65 2.33 4 2.33h3.16l.11-4.69.11-4.68-1.51-.74c-3.59-1.77-11.44-3-13.51-2.09m-247.63.94a15.33 15.33 0 0 0-.08 3.3l.12 2.76.2-3.3c.19-3.3.15-3.78-.24-2.76m20.43 3v3.46l-2.8.2-2.8.2v18.4l.12-9.1.12-9.1h2.51a6.41 6.41 0 0 0 3-.48c.53-.53.68-6.55.18-6.86-.17-.1-.3 1.37-.3 3.28m76.8-.34c0 1.72.14 3 .3 2.94a14.56 14.56 0 0 0 0-5.88c-.16-.1-.3 1.22-.3 2.94m97.68-2.94c-.66.65-.3 5.76.42 6 .38.12 1.18.4 1.76.62l1.07.4.18 9.46c.18 9.83.31 10.62 2.14 13.72 3.25 5.48 13.17 6.38 17.51 1.59 1.08-1.19 1.37-1.05 1.52.66L329 225h12.8l.1-2.4c.13-2.92-.17-3.44-2.16-3.76l-1.54-.25-.2-13.48c-.13-8.82-.35-13.62-.63-13.9s-2.65-.39-6.8-.32l-6.37.11v6l1.4.36c2.45.63 2.4.43 2.4 9.74v8.55l-1.66 1.2a5.26 5.26 0 0 1-3.83 1.2c-4.84 0-5.06-.62-5.31-15.4L317 191l-6.22-.11a39.61 39.61 0 0 0-6.5.17m12.52 12.41c.24 14.48.54 15.31 5.51 15.32 5.54 0 6.09-1 6.09-11.91 0-9.8-.06-10.08-2.07-10.08-1.34 0-1.93-1-1.93-3.12 0-2.38.39-2.52 6.65-2.39l5.55.11.2 13.4c.21 14.23.24 14.4 2 14.4s2.51 1.16 2.36 3.46l-.16 1.94h-11.2l-.4-1.67c-.52-2.15-.88-2.48-1.61-1.44-2.84 4.06-11.5 4.84-15.64 1.42-2.95-2.44-3.41-4.41-3.7-15.82-.27-10.18-.29-10.27-2.34-10.28-1.11 0-1.71-1.1-1.71-3.1 0-2.44.29-2.54 6.59-2.42l5.61.11.2 12.07m49.2-11.83c.79.25 2.24.75 3.23 1.12l1.8.67v8.37h-5.6l-.12-1.69c-.36-4.86-10.48-4.76-10.48.1 0 2.41 3.9 5 7.55 5a1.38 1.38 0 0 1 1 .4 1.22 1.22 0 0 0 .94.4c3.56 0 8.47 5.1 8.45 8.78 0 3.31-1.91 7.16-4 8.15-.31.14-.84.42-1.17.62-2.2 1.31-3.08 1.45-8.76 1.41s-7.56-.35-11.34-1.9l-1.1-.46v-9h2.6c2.85 0 3.82.72 3.82 2.82 0 4.26 10.71 4.39 11.77.14.63-2.47-4.09-5.76-8.26-5.76a20.52 20.52 0 0 1-5-1.82c-6.88-3.14-6.38-13.28.82-16.61 2.71-1.26 10.74-1.68 13.81-.73m-132 1.29a11.43 11.43 0 0 0 1.3 1.3l1.3 1.17-1.17-1.3c-1.1-1.21-1.43-1.48-1.43-1.17m-98.34 6.15a14 14 0 0 0 .24 3.86c.17.1.25-1 .19-2.48-.19-4.64-.36-5.18-.43-1.38m-23.67-.8 1.59.17.12 9 .12 9V198.2h-3.41l1.6.16m31.66 1.39a7.71 7.71 0 0 1-.28 1.4c-.22.69-.21.69.23 0a2 2 0 0 0 .27-1.4c-.16-.62-.18-.62-.22 0m66.37 1.07a2.59 2.59 0 0 0-.31 1.43c.06.33.32 0 .56-.82.5-1.61.4-1.83-.25-.61m-46.71 8.66a73.2 73.2 0 0 0 .22 9.06c.16.1.25-2.54.2-5.88-.19-11-.34-12.17-.42-3.18m22.05-6.67c-1.14 1.09-1.94 2-1.79 2a12.47 12.47 0 0 0 2.26-2c2.54-2.58 2.22-2.58-.47 0m44.32-.11-.05 1.3h-4.77c-4.21 0-4.79-.08-5-.7s-.2-.48-.23.2c-.05.89 0 .9 5 .9 5.37 0 5.65-.11 5.28-2.07-.14-.71-.19-.62-.24.37m-124.8 2.68a14.7 14.7 0 0 1 1.08 1.62c.25.44.45.58.46.31a4 4 0 0 0-1.09-1.62c-.61-.62-.81-.76-.45-.31m43.66-.68a25.38 25.38 0 0 0 3.6 0c.93-.08.07-.15-1.91-.15s-2.74.07-1.69.16m60.54 1c-.34 1.7-.31 5.64.06 6.21.16.25.29-1.32.29-3.5 0-4.42 0-4.46-.35-2.71m-121.67.12a2.83 2.83 0 0 0 1.6.6c.66 0 .61-.1-.25-.36a12.41 12.41 0 0 1-1.6-.6c-.29-.15-.18 0 .25.36m-14.32 3.27c-.1.16 1.25.24 3 .17 2.21-.09 3.39 0 3.86.4s.62.46.13-.17-6.57-1.07-7-.4m76.43 2a14.54 14.54 0 0 0 2.8 0c.83-.08.25-.16-1.29-.16s-2.22.07-1.51.16m-42.17 1.9c0 1.1.08 1.5.18.9a7 7 0 0 0 0-2c-.09-.5-.17 0-.17 1.1m37.72-.63c-.62.64-1 1.4-.91 1.7s.22.4.25 0a4.15 4.15 0 0 1 1.11-1.7c.59-.61 1-1.12.87-1.12a6.54 6.54 0 0 0-1.32 1.17m-11.31 3.43c0 1.32.08 1.81.17 1.09a10.69 10.69 0 0 0 0-2.4c-.1-.6-.17 0-.17 1.31m-7.41 2.09a10.69 10.69 0 0 0 2.4 0c.6-.1 0-.17-1.31-.17s-1.81.08-1.09.17m-37.41.4a9 9 0 0 0 2.2 0c.61-.09.11-.17-1.1-.17s-1.71.08-1.1.17m58.9.11c-1.11.28-1.12.31-.2.35a3.32 3.32 0 0 0 1.6-.35c.68-.44.33-.44-1.4 0m21.48.88c-.52.52-.68 5.35-.18 5.66.16.1.24-1 .16-2.39a6.17 6.17 0 0 1 .35-3.17c.59-.7.34-.77-.33-.1M214 221.8a8.83 8.83 0 0 0 1.56 1.4c.11 0-.41-.63-1.16-1.4a8.83 8.83 0 0 0-1.56-1.4c-.11 0 .41.63 1.16 1.4m-83.41 1.58c0 1 .15 1.66.31 1.56.39-.24.38-2.2 0-2.82-.16-.26-.29.31-.29 1.26m-42.51 2.91a26.77 26.77 0 0 0 3.8 0c1-.08.19-.15-1.9-.15s-2.94.07-1.9.15m35 0a10.69 10.69 0 0 0 2.4 0c.6-.09 0-.17-1.31-.17s-1.81.08-1.09.17m20.8 0a10.69 10.69 0 0 0 2.4 0c.6-.09 0-.17-1.31-.17s-1.81.08-1.09.17m80.19 0a16.7 16.7 0 0 0 3 0c.83-.08.15-.16-1.5-.16s-2.33.08-1.5.16" + style={{ + fill: '#31683c', + fillRule: 'evenodd', + }} + /> + </svg> +); + +export default Stakeplus; diff --git a/src/config/validators/StakerSpace.tsx b/src/config/validators/StakerSpace.tsx new file mode 100644 index 0000000000..6808a408a3 --- /dev/null +++ b/src/config/validators/StakerSpace.tsx @@ -0,0 +1,15 @@ +const StakerSpace = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> + <rect width={512} height={512} rx={26} fill="#fff" /> + <path + fill="#010101" + d="M226.59 61a134.31 134.31 0 0 0-99.4 75.13c-8.43 17.63-11.62 30.91-12.39 51-.77 18.78.64 31.43 5 45.49 1.92 6.38 2.94 7.92 4.6 7.41 1.15-.39 3.84-1 5.75-1.54 2.69-.64 3.32-1.4 2.81-3.06-1-3.2 3.33-4.6 5.37-1.92 1.41 2 3.83 1.4 27.47-5.75 29.13-8.94 69-22.61 112.81-38.71 35.14-12.91 89.31-33.73 94.16-36.16l3.45-1.79L372 141c-23.78-56.33-85.23-90.06-145.41-80zm14.31 13.57c0 1.92-.89 2.43-4.47 2.43s-4.43-.51-4.43-2.43 1-2.68 3.58-2.93c4.42-.64 5.32-.13 5.32 2.93zm15.33-.76c0 2.68-.63 3.19-3.7 3.19-2.17 0-4.22-.64-4.6-1.28-1.66-2.68.51-5.11 4.47-5.11 3.2 0 3.83.51 3.83 3.2zm13.29-1.15a3.62 3.62 0 0 1 2.05 3.34c0 3.83-6.9 2.68-7.41-1.28-.39-2.96 1.4-3.6 5.36-2.06zm-44.72 2.68c1 2.56-.25 3.83-4.34 4.47-2.43.26-3.32-.25-3.58-2.43s.39-2.93 2.56-3.32a34.2 34.2 0 0 0 3.7-.76c.51 0 1.28.89 1.66 2.04zm61.84 4.09c.38 2.68.13 2.94-2.55 2.3a19 19 0 0 0-3.84-.9c-.64 0-1-1.15-1-2.68-.02-4.09 6.88-2.94 7.39 1.28zm-76.4 2.17c0 2.3-6.52 3.58-7.79 1.53-1.79-2.68-1.41-3.32 2.42-4.6 3.45-1.15 5.37-.12 5.37 3.07zm89.56.51c2.68 1.53 2.94 2.3 1.28 4.86-.77 1.27-1.66 1.4-4.35.12-3.32-1.53-4.34-3.7-2.55-5.36 1.15-1.28 2.68-1.15 5.62.38zm-106.17 7.67c-4 3.06-4.6 3.06-5.62.38-.64-1.4.13-2.94 2-4.47 2.56-1.92 3.2-1.92 4.86-.26s1.57 2.18-1.24 4.35zm120.1 0c2.81 2.17 2.94 2.68 1.27 4.34s-2.3 1.66-4.85-.25c-1.92-1.54-2.68-3.07-2-4.48.98-2.68 1.62-2.68 5.58.39zM182 94.12c1.28 1.53.9 2.3-1.66 4.09-2.81 1.79-3.7 1.79-5.36.38-1.92-1.53-1.79-1.91.76-4 3.45-2.77 4.35-2.77 6.26-.47zm81 8 4.08.89-4.08 1c-3.2.89-4.48 2-5.24 5.24-1.28 4.08-1.41 4-3.45-2.3-.26-1-2.43-2.3-4.73-2.94l-4.22-1 4.22-.89c2.3-.51 4.47-1.66 4.73-2.81 2-6.26 2.17-6.39 3.45-2.17.9 3.37 1.92 4.39 5.24 5.03zm66.05-.77c0 .51-.9 1.54-2.05 2.43-1.66 1.41-2.42 1.28-4.6-1s-2.42-3.07-1.15-4.6c1.54-1.79 2.05-1.79 4.73.26 1.67 1.18 3.08 2.56 3.08 2.96zm-160.6 6.52c-2.43 2.3-3.06 2.43-4.85 1s-1.66-1.91.89-4.73 2.94-2.81 4.73-1 1.67 2.22-.76 4.78zm170.18 6c-1.66 1.79-2.17 1.67-4.73-.76-2.3-2.43-2.42-3.07-1-4.86s1.92-1.66 4.73.9 2.8 2.99 1.01 4.77zM158 119c-1.54 2.82-4.6 3.58-5.88 1.41-.51-.77.13-2.81 1.28-4.73 1.91-2.81 2.42-3.06 4.08-1.27 1.52 1.43 1.65 2.45.52 4.59zm190.23 3.45c1.54 2.82 1.54 3.45 0 4.35-1.28.77-2.68.38-4.09-.9-2.68-2.42-2.93-5-.63-5.87 2.67-1.12 2.92-1.03 4.71 2.45zm-199.43 4.86c1.91 2.3-.26 7.15-3.07 7.15s-3.32-2-1.79-5.62c1.78-3.84 2.8-4.06 4.85-1.5zm207.74 8.43c1.79 3.58 1.79 4-.13 4.73-1.41.51-3.07-.26-4.6-2.17-2.43-2.94-2.43-3.2-.26-4.73 2.93-2.14 2.68-2.27 4.98 2.2zm-215.41 5c1.53 1 1.66 1.79.38 4.48-1.53 3.57-2.42 4-5.11 2.17-1.27-.77-1.4-1.66-.12-4.35 1.65-3.69 2.16-3.95 4.84-2.29zm221.41 13.93c-2.43 1.79-3.58.89-4.73-3.83s3.2-5.75 5.37-1.15c1.27 2.82 1.14 3.59-.65 4.99zm-226 1c.38.39.13 2.3-.51 4.35-1 2.68-1.92 3.45-3.83 2.94-2.69-.77-2.69-1-1.66-5.24.74-2.53 4.19-3.81 5.98-2.02zm-4 17.76c-.64 4.6-1.4 5.75-3.45 5-1.15-.51-1.92-2.3-1.92-4.6 0-3.19.52-3.7 2.94-3.32 2.07.27 2.71 1.16 2.45 2.95zm52.13 1.15a5.72 5.72 0 0 0 4.09 3.58c2.93.51 2.93.51-.39 1.92a8.67 8.67 0 0 0-4.34 4.47l-1 2.94-.51-2.94a7 7 0 0 0-4-4.35l-3.41-1.2 2.93-1.15a6.59 6.59 0 0 0 3.71-3.71c.89-3.36 1.53-3.23 2.94.47zM131 190.07c0 3.58-.51 4.47-2.56 4.47s-2.55-.89-2.55-4.47.51-4.47 2.55-4.47 2.56.89 2.56 4.47zm.51 13.67c1.28 5 .89 6.13-2.43 6.13-2.68 0-3.19-.64-3.19-3.57.03-5.88 4.24-7.8 5.65-2.56zm2.55 15.33c1.28 5.11.9 6.39-2 5.88-4.86-.64-4.34-8.56.38-8.69.5 0 1.14 1.28 1.65 2.81z" + /> + <path + fill="#010101" + d="M450.82 110.35c-5.12.64-12 1.53-15.34 2.17l-6.13 1 10.22.9c11.37 1.15 18.53 2.93 18.53 5 0 5.62-35.65 24.91-77.3 41.78C249.46 214.73 106.75 261 62.16 264.43c-10.09.76-10.22.76-11.75-2.56-1.28-2.81-.89-4.47 2.43-11.62 3.7-7.8 3.7-8.31 1.27-7C45.17 248 22.43 275.16 22.43 281c-.13 11.88 47 4.47 128.4-19.93C285.75 220.61 454.52 150.72 484 123.25c7-6.64 7.92-11.63 2.18-13.16-4.32-1.15-24.12-1.02-35.36.26zM376.71 191c-5 2.18-5.49 2.82-5.49 6.9s-.38 4.48-2.81 4.09-2.81-1-2.56-3.45c.39-2.17 0-2.81-1.27-2.3-70 28.49-135.81 52-216.69 77.43l-6.64 2 7.66 8.56c10.61 11.76 26.32 23.25 41.53 30.54 65.92 31.43 145 4.72 177.59-60.18 9.58-19.16 15.45-43.18 14.43-59.54l-.38-6.38zm-7.41 19c1.66 2.43-.51 7.54-3.19 7.54s-3.07-1.28-1.79-6.13c.89-2.94 3.58-3.84 4.98-1.41zm-4.09 14.69c2.82 1 .77 7.54-2.42 7.54-1.28 0-2.43-.76-2.3-1.66s.25-2.68.38-4.09c.13-2.68 1.13-3.06 4.34-1.79zM327.52 244a10.63 10.63 0 0 0 5.24 1.78c2.69 0 2.3.39-1.79 1.67-4.34 1.27-5.23 2.17-6.51 6.51s-1.66 4.6-1.66 1.53a9.34 9.34 0 0 0-8.69-8.68c-3.32-.13-3.19-.26.89-1.15 5-1.15 7.67-4.22 7.8-8.82.13-2.81.25-2.68 1.4 1.15.64 2.42 2.18 5.23 3.32 6.01zm33.86-4c.39.51-.13 2.55-1.15 4.47-1.53 3.19-2.3 3.57-4.34 2.42s-2.3-1.78-1-4.47c1.25-2.91 4.7-4.18 6.49-2.42zm-6.77 15.33a16.47 16.47 0 0 1-2 3.7c-1.66 2.56-2.43 2.81-4.35 1.66-2.3-1.4-2.3-1.79-.38-5 1.66-2.81 2.55-3.32 4.47-2.3 1.24.69 2.26 1.61 2.26 1.97zM345 267.49c1 1.54-2.94 6.26-5.24 6.26-2.81 0-3.45-2.68-1-5.23s4.99-3.2 6.24-1.03zm-181.39 11.25c-1.66 1.79-2.18 1.66-4.73-.77-2.3-2.43-2.43-3.07-1-4.85s1.91-1.67 4.72.89 2.8 2.99 1.01 4.73zm170.43.51c1.79 2.3-2.68 6.64-5.62 5.62-2.56-1-2.43-4.34.38-5.88 2.94-1.79 3.58-1.66 5.2.26zM175 285.13c1.79 2 1.91 2.55.25 3.83-3.19 2.68-9.2-2.56-6.26-5.37 1.39-1.53 3.69-1.02 6.01 1.54zm145.26 7.92c-2.81 2.3-3.32 2.3-5 .64s-1.66-2.18 1.15-4.48 3.32-2.3 5-.64 1.52 2.18-1.17 4.48zm-106.57 2.55c1.79 0 1.66.26-.51 1.15a4.8 4.8 0 0 0-3.07 3.45c0 1.79-.25 1.66-1.15-.64a4.88 4.88 0 0 0-3.45-2.94c-1.79 0-1.66-.25.64-1.15a4.86 4.86 0 0 0 2.94-3.45c0-1.78.26-1.66 1.15.52a4.78 4.78 0 0 0 3.45 3.06zm-26.83-2c2.81 2 2.68 5.75-.13 5.75-3.06 0-6-3.2-5.11-5.5 1.02-2.72 1.79-2.72 5.24-.29zM308 301.22c-3.07 2.3-6.26 1.92-6.77-.76-.52-2.94 5-5.24 7.41-2.94 1.51 1.53 1.51 2.04-.64 3.7zm-107.58-.51c2.17 1 2.94 2.17 2.56 3.84-.77 2.81-2.81 3.06-6.14.63-1.4-1-2-2.42-1.53-3.83 1-2.43 1.13-2.43 5.09-.64zM295 307.1a12.73 12.73 0 0 1-4.48 1.79c-2.93.77-3.57.51-3.57-1.53 0-3.58 5.23-5.88 7.41-3.2.71 1.15 1.1 2.43.64 2.94zm-78.45-.26a2.7 2.7 0 0 1 .89 3.33c-.63 1.66-1.66 1.91-4.34 1.27-4.34-1-4.34-1.15-3.32-3.7.84-2.17 3.91-2.68 6.72-.9zm63.11 2.82c.64 2.55-.12 3.19-4.85 4-4 .64-4.34-5-.38-5.75a33.39 33.39 0 0 0 3.7-.77c.46-.04 1.1.98 1.48 2.52zM233 312.59c-.25 2.18-1.15 2.56-4.21 2.3-4.73-.63-4.86-.76-4-3.7 1.21-3.71 8.7-2.43 8.21 1.4zm31.3-.63c.77 3.19 0 4-4.47 4s-5.24-4.6-.64-5.5c1.54-.25 3.2-.64 3.58-.76s1.13.98 1.51 2.3zm-15.71 1.27c0 3.32-1.66 4.22-5.5 3.32-4.6-1.15-3.19-5.74 1.79-5.74 2.79 0 3.69.63 3.69 2.42zm-96.48 35.27c-5.24 1.91-9.46 6.26-9.58 9.83 0 3.32 3.19 5.75 10.22 7.41 7.41 1.79 9.07 4 5.75 7.67-2.81 3.06-12.91 3.45-16.74.77-2.17-1.67-2.68-1.54-4.6.12-3.07 2.69-2.94 3.58.89 5.24 4.86 2.17 16.74 1.92 22-.64s9.33-9.58 7.28-12.9c-.76-1.15-4.47-3.2-8.3-4.47-9.46-3.2-10.73-4.47-7.92-7.67 1.91-2 3.45-2.43 10.22-1.79 5.62.39 8.43.13 9.32-1S171 349 168 348c-5.42-1.8-9.63-1.68-15.89.5zm23.12 1.27c-.51 1.54.51 1.92 5 1.92 6.89 0 6.77-.64 2.68 16.48l-3.07 12.9h3.58c4 0 4-.12 7.79-20.82l1.54-8.56H199c4.6 0 6.13-.51 6.13-1.92s-2.43-1.91-14.57-1.91c-11.75 0-14.69.38-15.33 1.91zm34.12 14.7-10.48 16.6h3.58c2.93 0 4.34-1 6.38-4.47l2.56-4.47h19.42l1.92 4.47c1.53 3.71 2.55 4.47 5.49 4.47h3.7l-7.41-16.35c-6.77-14.82-7.79-16.22-11.11-16.61s-4.34.89-14.05 16.36zm16.22-5.63c3.71 8.44 3.45 8.82-4.09 8.69-3.7-.13-6.64-.76-6.39-1.28.64-1.66 7.29-13.28 7.54-13.28s1.37 2.68 2.94 5.87zm21.72 5.63v16.6h3.83c3.58 0 3.84-.25 3.84-4.85 0-3.45.89-5.75 2.93-7.67l2.94-2.81 7.54 7.67c5.88 6 8.43 7.66 11.37 7.66 3.83 0 3.71-.12-5.62-9.58l-9.45-9.58 6.9-7 6.89-7h-3.83c-2.94 0-5.49 1.66-11.37 7.66-4.22 4.22-8 7.67-8.56 7.67s-1-3.45-1-7.67c0-7.28-.13-7.66-3.2-7.66h-3.19zm39.71-1.03c1.28 8.69 2.43 16.1 2.43 16.61s6.65 1 14.82 1c13.42 0 14.69-.25 14.31-2.3-.38-1.78-2.17-2.17-11.24-2.55-11.5-.38-12.78-1-12.78-7 0-3.45 0-3.45 9.07-3.45 7.8 0 8.95-.25 8.31-2a18.88 18.88 0 0 1-.77-2.56c0-.25-4-.51-8.81-.51-9.2 0-10.35-.76-10.35-6.51 0-2.18 1-2.43 9.58-2.43 7.66 0 9.58-.38 9.58-1.92s-2.3-1.91-13.16-1.91h-13.27zm33.75-9.58c.9 3.45 2.94 10.86 4.48 16.61 2.68 10.09 2.93 10.6 6.51 10.6h3.58l-1.79-5.07c-1.92-5.49-1.79-5.75 4.09-6.26 3.32-.25 5.24.77 10.35 5.5 4.47 4.21 7.28 5.87 10 5.87 3.71-.12 3.71-.12-2.68-5.74-6.13-5.37-6.26-5.63-4.09-8.05 2.94-3.2 2.81-8.18-.51-12.27-4.34-5.62-8.94-7.15-20.83-7.15H319zm20.19-.77a11.3 11.3 0 0 1 4.73 4.73c1.27 2.94 1.15 3.71-1.28 5.62-1.79 1.41-5 2.3-8.43 2.3-5.37 0-5.62-.12-6.9-5.24-.77-2.81-1.79-6-2.17-7-.51-1.41.63-1.79 5.11-1.79a26.85 26.85 0 0 1 8.94 1.38zm-204.16 50.09c-9.71 5.49-13.93 17-8.31 22.61 1.28 1.28 5.75 3.45 9.84 4.6 10.35 3.07 12 4.22 11.63 8.69C149 447.51 135 450.83 125 445l-4.72-2.82-3.07 3.45c-3.58 4.22-3.2 5.63 2.43 9 3.32 1.91 6.64 2.55 13.92 2.55 8.18 0 10.48-.51 15.72-3.57 6.77-4 11.11-11 11.11-17.76s-2.94-9.59-12.9-12.65c-8.31-2.56-12.65-5.11-12.65-7.41 0-.64 1.15-2.56 2.56-4.35 3.44-4.34 10.6-5.49 18-2.93 5.37 1.91 5.75 1.91 7.92-.64 3.07-3.71 2.81-4.48-1.79-6.14-6.99-2.51-19-1.73-24.75 1.45zm41.01-.77c-1 3.58-9.71 50.34-9.71 52.38 0 1.28 1.28 1.66 4.85 1.41l4.73-.38 1.28-8.31 1.28-8.3 10.86-.64c18-1.28 25.68-7.67 25.8-21.59 0-12.4-6.77-16.74-26.44-16.74-10.35 0-12.01.25-12.65 2.17zm28.11 8.69c3.06 4.21 2.17 10.6-1.92 14.82-3.45 3.45-4.47 3.7-12.78 3.7-5 0-9.07-.38-9.07-.89 0-1.53 2.68-17.63 3.32-19.42.9-2.81 18.15-1.41 20.45 1.79zm30.02-.38c-2.68 5.87-8.43 18.14-12.78 27.46s-7.79 17.12-7.79 17.51 2.17.76 4.86.76c4.72 0 5-.25 7.79-7l2.94-7.16 14.05.39 14.18.38 2.56 6.64c2.43 6.65 2.55 6.77 7.92 6.77h5.49l-12.9-28.1-12.91-28.11h-4.21c-4.22-.02-4.47.36-9.2 10.46zm12 5.11c1.15 3.06 3.32 8.17 4.6 11.49l2.43 6.14h-20.18l4.6-11.5c2.55-6.26 5.11-11.5 5.49-11.5s1.92 2.43 3.07 5.37zm41.53-14.19c-8.56 3.45-13.29 11.5-13.29 22.87 0 17 12 30.92 28.37 32.84 8.05.89 15.58-1.28 20.06-6l3.7-3.83-3.19-3.32-3.32-3.2-4.86 3.2c-13.67 9.33-31.3-2.17-31.3-20.19 0-6.77.38-7.79 4.34-11.75 3.71-3.71 5.24-4.35 10.61-4.35a26.71 26.71 0 0 1 10.86 2.43c4.6 2.56 4.6 2.56 6.9 0s2.3-2.68-.13-4.6c-6.9-5.25-20.83-7.16-28.75-4.1zm39.61.26c0 2 11.5 53.66 12 54.17.25.26 9.58.26 20.82.13l20.32-.38-1-4.22-1.2-4.09h-31.52l-1.92-7.92c-1.15-4.34-1.79-8.43-1.4-8.94s6-1 12.64-1h12l-1.27-3.83-1.4-3.82h-24.91l-.77-3.71c-.38-2-1.15-5.11-1.66-7l-.79-3.39h13.55c7.41 0 13.54-.25 13.54-.51a17.59 17.59 0 0 0-1.28-3.83l-1.28-3.32h-17.22c-13.42 0-17.25.38-17.25 1.66z" + /> + </svg> +); + +export default StakerSpace; diff --git a/src/config/validators/Staking4All.tsx b/src/config/validators/Staking4All.tsx new file mode 100644 index 0000000000..231781c58d --- /dev/null +++ b/src/config/validators/Staking4All.tsx @@ -0,0 +1,28 @@ +const Staking4All = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400"> + <g fillRule="evenodd"> + <path + d="M6.41 224.1c0 4 2 21.33 2.79 24.3.2.77.58 2.39.85 3.6 9.84 45 43.72 85.79 91 109.52 3.92 2 10.11 4.88 10.38 4.88a21.16 21.16 0 0 1 2.31 1c6 2.75 24.2 8.8 29.91 10 .55.11 2.05.49 3.33.84s3.81.92 5.61 1.25 4.07.78 5.06 1 3 .56 4.4.76l7 .95a220.12 220.12 0 0 0 50 1c7.07-.66 17.57-2.07 20.6-2.75 1-.23 3.26-.68 5-1 10.06-1.85 30.24-8.1 38.87-12.05a22.72 22.72 0 0 1 2.29-1c.61 0 11.08-5.11 16.8-8.2a197.5 197.5 0 0 0 19.22-12c.89-.66 2.13-1.57 2.76-2 38.48-27 64.73-73.68 65.91-117.2l.11-4-11.9-.11-11.9-.1v.88c0 31.79-16.56 67.65-42.64 92.34a173.94 173.94 0 0 1-70.56 40c-1 .28-2.25.67-2.8.85s-2.62.72-4.6 1.17-4.23 1-5 1.21c-17.32 4.8-50.67 5.9-72.4 2.38-6.4-1-10.63-1.83-12.8-2.39-.77-.2-3-.75-5-1.22-3.84-.92-3.89-.93-10-2.82-5.94-1.84-10.9-3.56-13.6-4.73l-4.4-1.86c-47.1-19.9-82.4-60.69-90.17-104.2a120.3 120.3 0 0 1-2.1-15.5l-.49-6.1H6.4v1.3" + fill="#060606" + /> + <path + d="M189.17 11.37c-.1.09-3.2.36-6.9.6-6 .39-13.53 1.26-20.87 2.39-7.54 1.15-26.74 5.79-30 7.23a21.93 21.93 0 0 1-2.2.78c-3.52 1.07-5.2 1.63-7.8 2.63-1.54.59-3.11 1.24-3.5 1.44a3.17 3.17 0 0 1-1.09.37 17.53 17.53 0 0 0-2.88 1.2 18.87 18.87 0 0 1-2.81 1.2c-.45 0-14.29 6.79-17.52 8.6a202.5 202.5 0 0 0-24.17 15.91c-1.43 1.11-5.56 4.38-5.83 4.66-.11.12-1.55 1.38-3.2 2.81C42.07 77 26.28 98.75 17.43 120.36c-2.8 6.85-6.42 18.08-7.17 22.24-.11.66-.41 2-.66 3a140.64 140.64 0 0 0-2.39 16.8c-.2 2.31-.49 5.5-.64 7.1l-.26 2.9H30.4v-2.1c0-26.83 13.92-60 35-83.08C73.64 78.18 89 65 94.8 62a5.68 5.68 0 0 0 1.2-.78 10.22 10.22 0 0 1 1.6-1.08l4.17-2.53a156.18 156.18 0 0 1 18-9.41c6.35-2.83 7.13-3.16 9.4-4 1-.37 2.16-.83 2.6-1s3-1 5.6-1.91c14.55-4.74 23.67-6.67 42-8.89 24.62-3 64.43 1.78 84.1 10 1.63.68 2.68 1.09 4.1 1.59.77.27 1.94.72 2.6 1l2.8 1.17c24.1 9.89 47.77 27.1 63 45.82 1 1.21 1.89 2.29 2 2.4s1.31 1.65 2.63 3.42c12.43 16.63 21 36.16 24.35 55.38a150.06 150.06 0 0 1 1.8 16.9v2.3h23.6v-3.9c0-13.3-4.54-34.45-10-46.8a11.71 11.71 0 0 1-.76-2c0-2.19-12-23.7-16.28-29.3l-2.4-3.22c-2.41-3.26-2.22-3-6.16-7.8a224.73 224.73 0 0 0-20.35-20.31c-2.09-1.73-4.25-3.53-4.8-4-2.75-2.36-15.7-11.33-19.49-13.52-8.38-4.84-19.18-10.45-22.91-11.92-.77-.3-2.53-1.06-3.91-1.68a19 19 0 0 0-2.9-1.13 3.27 3.27 0 0 1-1.09-.35c-7.05-3.59-30.9-10.22-43.5-12.09-7.16-1.07-10.91-1.53-15.6-1.94l-9.62-.83c-4.33-.38-21.08-.55-21.41-.22m-3.37 97.09c-36.26 4-57.4 23.27-57.4 52.39 0 25.58 12.45 42.12 37 49.16a85.94 85.94 0 0 0 9.6 2.39l4.2.76c4.87.88 11.58 2 20.2 3.25 17.47 2.61 23.54 6.27 23.48 14.18-.07 9.06-8.28 13.41-25.31 13.41-13.45 0-21.76-3.89-27.27-12.75-3.86-6.21-6.06-7.23-12.1-5.57a79.64 79.64 0 0 0-9.2 3.13c-.72.31-4.12 1.51-11.95 4.24-11.38 4-12.66 7.53-6.87 19.24 4.8 9.7 12.67 17.32 23.7 22.91a31.25 31.25 0 0 0 4.42 2 3.48 3.48 0 0 1 1.21.35 51.68 51.68 0 0 0 9.49 3l3 .65c11.21 2.49 32.84 3.06 43.2 1.14 12-2.22 17.76-3.95 25.6-7.68 7.67-3.65 15.24-9.48 19.6-15.09a12.6 12.6 0 0 1 1.7-1.93.63.63 0 0 0 .3-.53 8.15 8.15 0 0 1 1.16-2.09c9.62-14 9.12-40.22-1.07-55.75-8.69-13.25-23.49-20.21-52.09-24.5-1.65-.25-4-.61-5.2-.81s-3.73-.56-5.6-.8c-17.44-2.26-24.24-6.65-23.46-15.14.48-5.19 4.07-8.68 10.26-10a43.51 43.51 0 0 1 11.72-1.27c9.35 0 15.35 1.84 20.19 6.19 1.95 1.76 2.36 2.3 5.93 7.89s5.7 5.9 15.16 1.75c3.12-1.37 4-1.74 8.3-3.58l3.15-1.38c1-.45 3.62-1.56 5.8-2.46 10.21-4.24 11.79-7.13 8.09-14.78-8.19-16.93-23.34-27.62-43.74-30.85-9.58-1.52-26.61-2.06-35.2-1.12" + fill="#fc443c" + /> + <path + d="M0 200v200h400V0H0v200M218 11.54c6.11.55 11.7 1.19 14.4 1.65l4.6.81c11 1.8 29.15 6.55 36.8 9.64l5.2 2c2.42.94 5.48 2.19 6.8 2.78s3.2 1.41 4.19 1.83c23.19 9.89 50.32 30.47 66.9 50.76 13 15.86 24.14 36.83 28.8 54l1.14 4.21c2.3 8.55 4.33 22.92 4.36 30.9v2.7h-24.65l-.49-5.7a128.25 128.25 0 0 0-4.44-25.9c-1.56-5.55-1.75-6.2-2-6.6a15.21 15.21 0 0 1-.8-2.2 39.5 39.5 0 0 0-1.43-3.8L355 123.2a129.17 129.17 0 0 0-7-13.4c-.57-.88-1.27-2-1.55-2.6-4.19-8.34-21.8-27.85-32.87-36.39-1-.76-1.89-1.49-2-1.61-.6-.66-11.26-8.06-13-9-.44-.25-2.3-1.38-4.13-2.51s-3.36-2.09-3.45-2.09-1.42-.73-3-1.6-5.47-2.8-8.55-4.23A124.64 124.64 0 0 0 267 44.41c-4.17-1.57-6.18-2.26-9.4-3.24-.77-.23-1.76-.57-2.2-.75-2.46-1-15.75-4.19-21.8-5.21L226.4 34c-13.2-2.32-42.41-2.32-55.6 0l-6 1c-5.26.89-9.61 1.76-10.8 2.16-.55.18-2 .55-3.2.82a103 103 0 0 0-10 2.83c-11.9 3.63-31.24 12.19-37.22 16.4a5.45 5.45 0 0 1-1.33.79c-.46 0-9.72 6.19-14 9.35a144.65 144.65 0 0 0-21.93 19.77c-2.26 2.53-4.31 4.79-4.55 5-5.5 5.72-16 22.64-20.35 32.85-.38.88-.85 1.91-1.05 2.29a3.24 3.24 0 0 0-.37 1.14 10 10 0 0 1-.78 2.1 125.92 125.92 0 0 0-4 12.1 130.73 130.73 0 0 0-4 24.92L31 172.6l-12.31.11c-11 .09-12.34 0-12.56-.54-.31-.8.56-12.68 1.28-17.57.29-2 .65-4.41.79-5.4a22.47 22.47 0 0 1 .59-2.8c.18-.55.55-2.08.83-3.4s.73-3.3 1-4.4.64-2.45.77-3a16.59 16.59 0 0 1 .58-1.8c.17-.44.77-2.24 1.32-4 7.61-24.2 25.94-51.31 46.69-69 1.65-1.41 3.09-2.65 3.2-2.76C70.73 50.45 95 35.53 111.6 28.28A231.39 231.39 0 0 1 142 18c3.41-.86 6.83-1.73 7.6-1.95 2-.55 11.26-2.22 17.6-3.18a127.13 127.13 0 0 1 12.8-1.3 30 30 0 0 0 4-.36c2.39-.45 28.15-.19 34 .33M214.2 108c4.55.55 8.57 1.17 10.2 1.57l3.4.82a59.1 59.1 0 0 1 14.4 5.47c12.8 6.46 26.37 24.14 24.8 32.28-.44 2.36-8.74 8.26-11.61 8.26a15.72 15.72 0 0 0-2.13.85c-1.92.85-3.7 1.62-5.9 2.55-.66.28-2.1.91-3.19 1.4-3.05 1.36-10.22 4.24-11.94 4.79-3.53 1.13-5.56-.25-9.68-6.59-6.4-9.85-14-13-28.79-11.77-12.07 1-17 4.28-17 11.45 0 7.68 5.55 10.93 23.17 13.57 8.61 1.29 10.8 1.61 14.43 2.11a83 83 0 0 1 8.6 1.65c.88.19 2.5.54 3.6.76s2.63.58 3.4.8l2.8.76a30.22 30.22 0 0 1 3.8 1.27c.44.18 1.88.73 3.2 1.23 17.06 6.45 27.53 19.18 30.45 37 6.13 37.42-18.91 62.48-65.68 65.76-38.81 2.71-68.49-11.55-77.39-37.21-2.84-8.19-.27-11.1 14-15.85 1-.33 2.16-.76 2.6-1s1.61-.63 2.6-.95 3.33-1.17 5.2-1.86c11.41-4.25 14.66-3.68 18.83 3.29 5.84 9.77 16.52 14 31.92 12.72 14.33-1.21 20-4.83 20.05-12.71 0-7.68-5.36-10.67-24.6-13.67s-27.39-4.55-31.8-6c-.55-.18-1.81-.56-2.8-.84a9.52 9.52 0 0 1-2.18-.85 1.87 1.87 0 0 0-1-.34c-2.34 0-13.62-6.7-18-10.66-13.27-12-18.08-36.31-11-55.65 4.19-11.48 14.44-21.79 27.3-27.47 2.36-1.05 4.6-2.06 5-2.25a3.72 3.72 0 0 1 1.42-.36 2 2 0 0 0 1.11-.33c.58-.5 4.83-1.6 12.38-3.21 7.05-1.5 26.71-2 36-.83M31.46 229c3.63 50.62 37.54 95.07 90.14 118.3.77.34 2.44 1.08 3.71 1.66a20.82 20.82 0 0 0 2.58 1c.15 0 1.9.62 3.89 1.37a204.71 204.71 0 0 0 27 7.82c15.67 3 24.2 3.78 39.8 3.78 14.18 0 25-.91 35.6-3l4.2-.81c1-.18 2.56-.52 3.5-.76l3.2-.79c1.37-.34 6.17-1.61 9.1-2.42 1.29-.35 10-3.36 14-4.81 1.32-.48 3.35-1.31 4.51-1.84a18.8 18.8 0 0 1 2.35-1c.24 0 8-3.67 9.54-4.5l3.4-1.81c5.08-2.68 17.85-10.74 20.7-13.06.38-.32 1.6-1.25 2.7-2.06 17.82-13.23 35.39-35.13 43.78-54.57.91-2.09 1.8-4.16 2-4.6s.57-1.43.83-2.2.67-1.85.89-2.4a38.72 38.72 0 0 0 1.88-5.6c.46-1.65 1-3.54 1.19-4.2s.54-2 .77-3l.83-3.6c.66-2.82 2-12.25 2.23-16 .12-1.87.34-4.35.47-5.5l.23-2.1h12.33c6.78 0 12.42.14 12.53.32.24.39-.83 14.6-1.33 17.68-.19 1.21-.54 3.37-.78 4.8-.46 2.89-1.78 9-2.39 11.2-.22.77-.74 2.66-1.16 4.21-2.3 8.46-6.92 19.42-12.82 30.39a177 177 0 0 1-12.77 19.4l-3 3.8a184.38 184.38 0 0 1-25.12 25l-3.34 2.67c-.43.36-2.05 1.56-3.6 2.68s-2.91 2.15-3 2.28c-.3.37-10.77 7.16-11 7.16a5.48 5.48 0 0 0-1.3.75c-3.8 2.7-18.61 10.23-26.06 13.25-1.21.49-2.74 1.13-3.41 1.42s-2.56 1-4.2 1.57a39.18 39.18 0 0 0-3.69 1.43 3.24 3.24 0 0 1-1.3.38 3.24 3.24 0 0 0-1.3.38 31 31 0 0 1-3.7 1.24c-1.65.47-3.36 1-3.8 1.19a16.59 16.59 0 0 1-1.8.58l-4 1.07c-3 .8-4.49 1.18-7.7 1.94-25.28 6-59 7.13-85.3 2.8-8.3-1.37-14.45-2.6-18-3.6-.88-.25-2.32-.62-3.2-.82s-2.23-.54-3-.75l-2.4-.64a16.59 16.59 0 0 1-1.8-.58c-.44-.17-2.15-.72-3.8-1.2a37.17 37.17 0 0 1-3.7-1.25 3.36 3.36 0 0 0-1.3-.36 3.48 3.48 0 0 1-1.31-.37c-.38-.2-1.77-.76-3.09-1.24s-2.85-1.05-3.4-1.28l-1.8-.73a233 233 0 0 1-22.8-10.8c-1.54-.87-3.07-1.7-3.4-1.85a3.19 3.19 0 0 1-.8-.5 6.74 6.74 0 0 0-1.16-.77L86.25 354a213.71 213.71 0 0 1-19.46-13.6c-.38-.33-1.39-1.14-2.23-1.8-12.69-9.93-30.07-29.82-37.29-42.67-.44-.78-1-1.85-1.33-2.38a8.1 8.1 0 0 0-.75-1.15c-.49-.47-4.7-8.76-6.71-13.2-1.58-3.48-4.22-10.35-5.3-13.8-.49-1.54-1-3.16-1.2-3.6s-.43-1.25-.57-1.8-.43-1.63-.64-2.4a138 138 0 0 1-4.62-29.47c-.37-6.41-1.81-5.76 12.54-5.64l12.31.11.46 6.4" + fill="#fbfbfb" + /> + <path + d="M6.27 222.66c-.92.92.9 19.85 2.56 26.64.89 3.62 1.12 4.63 1.57 6.7a100.69 100.69 0 0 0 3 10A152.45 152.45 0 0 0 24 290.1l1.7 3c.46.82 1.18 2 1.6 2.56A5.26 5.26 0 0 1 28 297c0 1 10.5 15.33 14.91 20.4a187.57 187.57 0 0 0 31.92 29 198.52 198.52 0 0 0 43 23.17c2.42.94 4.76 1.86 5.2 2.05 1.1.47 7.37 2.52 11 3.59 1.65.49 3.81 1.16 4.8 1.47s2.61.75 3.6 1 2.43.55 3.2.74l3.8.94c10.17 2.52 27.16 4.65 38.8 4.87l8 .14-6.4-.33c-7.62-.41-14.78-1.05-20.8-1.88l-7-.95c-1.43-.2-3.41-.54-4.4-.76s-3.27-.68-5.06-1-4.32-.89-5.61-1.25-2.78-.73-3.33-.84c-5.71-1.16-23.91-7.21-29.91-10a21.16 21.16 0 0 0-2.31-1c-.27 0-6.46-2.91-10.38-4.88C53.77 337.79 19.89 297 10.05 252c-.27-1.21-.65-2.83-.85-3.6-.79-3-2.78-20.3-2.79-24.3v-1.3h23.84l.49 6.1a120.3 120.3 0 0 0 2.1 15.5c7.77 43.51 43.07 84.3 90.16 104.17l4.4 1.86c2.7 1.17 7.66 2.89 13.6 4.73 6.11 1.89 6.16 1.9 10 2.82 2 .47 4.23 1 5 1.22 2.17.56 6.4 1.35 12.8 2.39a205.75 205.75 0 0 0 51.8 1 190.12 190.12 0 0 0 20.6-3.37c.77-.21 3-.76 5-1.21s4.05-1 4.6-1.17 1.81-.57 2.8-.85c58.57-16.53 102.78-62.25 111.56-115.38.67-4.09 1.64-14 1.64-16.93v-.88l11.9.1 11.9.11-.11 4c-1.18 43.52-27.43 90.19-65.91 117.2-.63.44-1.87 1.35-2.76 2a197.5 197.5 0 0 1-19.22 12c-5.72 3.09-16.19 8.2-16.8 8.2a22.72 22.72 0 0 0-2.29 1c-8.63 3.95-28.81 10.2-38.87 12.05-1.78.33-4 .78-5 1-5.67 1.28-21.11 3-32.2 3.59l-6.4.33 8-.14c13.08-.25 31.3-2.7 42.2-5.68 1.21-.32 2.92-.75 3.8-.94s2.86-.73 4.4-1.2 3.34-1 4-1.16c1.55-.42 9.75-3.14 10.8-3.6.44-.19 1.88-.74 3.2-1.22s2.76-1 3.2-1.22l4.69-2a216.69 216.69 0 0 0 22.85-11.55c1.51-.92 2.81-1.67 2.89-1.67.37 0 12-7.87 13.77-9.32.33-.27 1.23-.93 2-1.47a21.72 21.72 0 0 0 1.93-1.5c.3-.29 1.2-1 2-1.62 5.73-4.29 16-14 21.88-20.73 3.3-3.76 3.48-4 5.8-7 .76-1 1.49-1.89 1.6-2 1.54-1.45 9.64-13.65 12.61-19 1.77-3.17 5.55-10.67 6.16-12.2.31-.77 1-2.3 1.45-3.4a147.22 147.22 0 0 0 9-31c1.22-7.3 2.05-16.09 1.85-19.8l-.12-2.4-12.3-.11c-11.52-.09-12.3-.06-12.31.6a196.31 196.31 0 0 1-3.19 24.81c-.66 2.8-1.12 4.61-1.57 6.2-.24.83-.59 2.08-.78 2.8a26.49 26.49 0 0 1-1.27 3.7c-.19.44-.63 1.7-1 2.8s-.89 2.54-1.18 3.2-.87 2-1.27 3c-8.31 20.23-24.05 40.19-44.55 56.5a177.1 177.1 0 0 1-34 20.4l-3.38 1.5a197.12 197.12 0 0 1-25.82 8.86 113.57 113.57 0 0 1-15.4 3.31c-2.2.37-4.9.82-6 1-15.39 2.76-40.48 2.6-59.2-.37-15.14-2.4-32.5-7.31-43.31-12.25a18 18 0 0 0-2.35-1c-.47 0-10.36-4.82-13.54-6.59A189.12 189.12 0 0 1 85 325.6c-8.66-6.72-20.15-18.21-26.24-26.23-1-1.3-1.9-2.46-2-2.57-2.2-1.86-12.83-20.34-15.66-27.2-.55-1.34-1.13-2.81-1.55-3.9-.19-.5-.52-1.31-.74-1.8-3.68-8.62-7.19-26.37-7.72-39.1l-.09-2.2-12.23-.11c-6.72-.05-12.35 0-12.5.17m191.43 162a9 9 0 0 0 2.2 0c.61-.09.11-.17-1.1-.17s-1.7.08-1.1.17" + fill="#959595" + /> + <path + d="M184 11.21a30 30 0 0 1-4 .36 127.13 127.13 0 0 0-12.8 1.27c-6.34 1-15.58 2.63-17.6 3.18-.77.22-4.19 1.09-7.6 1.95C117.41 24.16 94 35.23 72 51c-2.15 1.56-8.08 6.24-8.8 7-.11.11-1.55 1.35-3.2 2.76-20.75 17.73-39.08 44.84-46.69 69-.55 1.76-1.15 3.56-1.32 4a16.59 16.59 0 0 0-.58 1.8c-.13.55-.48 1.9-.77 3s-.75 3.08-1 4.4-.65 2.85-.83 3.4a22.47 22.47 0 0 0-.59 2.8c-.14 1-.5 3.42-.79 5.4-.72 4.89-1.59 16.77-1.28 17.57.22.57 1.54.63 12.56.54L31 172.6l.26-5.08a130.73 130.73 0 0 1 4-24.92 125.92 125.92 0 0 1 4-12.1 10 10 0 0 0 .78-2.1 3.24 3.24 0 0 1 .37-1.11c.2-.38.67-1.41 1.05-2.29 4.37-10.21 14.85-27.13 20.35-32.85.24-.24 2.29-2.5 4.55-5a144.65 144.65 0 0 1 21.9-19.8c4.27-3.16 13.53-9.35 14-9.35a5.45 5.45 0 0 0 1.33-.79c6-4.25 25.32-12.77 37.22-16.39a103 103 0 0 1 10-2.83c1.21-.27 2.65-.64 3.2-.82 1.19-.4 5.54-1.27 10.8-2.16l6-1c13.19-2.32 42.4-2.32 55.6 0l7.2 1.22c6.05 1 19.34 4.19 21.8 5.21.44.18 1.43.52 2.2.75a147.52 147.52 0 0 1 21.8 8.57c3.08 1.43 6.93 3.34 8.55 4.23s3 1.63 3.08 1.63 1.61.93 3.44 2.06 3.69 2.26 4.13 2.51c1.74 1 12.4 8.37 13 9 .11.12 1 .85 2 1.61 11.07 8.54 28.68 28.05 32.87 36.39.28.55 1 1.72 1.55 2.6a129.17 129.17 0 0 1 7 13.4l2.35 5.4a39.5 39.5 0 0 1 1.43 3.8 15.21 15.21 0 0 0 .8 2.2c.26.4.45 1.05 2 6.6a92.08 92.08 0 0 1 3.16 14.8c.43 3 1 8 1.28 11.1l.49 5.7h24.66v-2.7c0-8-2.06-22.35-4.36-30.9l-1.15-4.19c-4.66-17.16-15.84-38.13-28.8-54-16.58-20.29-43.71-40.87-66.89-50.76-1-.42-2.87-1.24-4.19-1.83s-4.38-1.84-6.8-2.78l-5.2-2C266.15 20.51 248 15.76 237 14l-4.6-.77c-2.7-.46-8.29-1.1-14.4-1.65-5.85-.52-31.61-.78-34-.33m26.58.38 9.62.83c4.69.41 8.44.87 15.6 1.94 12.6 1.87 36.45 8.5 43.5 12.09a3.27 3.27 0 0 0 1.09.35 19 19 0 0 1 2.9 1.13c1.38.62 3.14 1.38 3.91 1.68 3.73 1.47 14.53 7.08 22.91 11.92 3.79 2.19 16.74 11.16 19.49 13.52.55.47 2.71 2.27 4.8 4a224.73 224.73 0 0 1 20.36 20.31c3.94 4.78 3.75 4.54 6.16 7.8l2.4 3.22c4.33 5.6 16.28 27.11 16.28 29.3a11.71 11.71 0 0 0 .76 2c5.5 12.35 10 33.5 10 46.8v3.9H366.8v-2.3c0-22.66-10.27-51-26.15-72.28-1.32-1.77-2.5-3.31-2.63-3.42s-1-1.19-2-2.4c-13-16-31.3-30.3-51.81-40.59-6.07-3-7.43-3.68-11.2-5.23L270.2 45c-.66-.28-1.83-.73-2.6-1-1.42-.5-2.47-.91-4.1-1.59-19.67-8.26-59.48-13-84.1-10-18.33 2.22-27.45 4.15-42 8.89-2.64.86-5.16 1.72-5.6 1.91s-1.61.65-2.6 1c-2.27.84-3 1.17-9.4 4a156.18 156.18 0 0 0-18 9.41l-4.2 2.52a10.22 10.22 0 0 0-1.6 1.08 5.68 5.68 0 0 1-1.2.78c-1.42.74-5.3 3.58-10.94 8-21.69 17-37.92 39.3-46.5 63.75-3.73 10.63-7 27.68-7 36.53v2.1H6.31l.26-2.9c.15-1.6.44-4.79.64-7.1A140.64 140.64 0 0 1 9.6 145.6c.25-1 .55-2.34.66-3 .75-4.16 4.37-15.39 7.17-22.24C26.28 98.75 42.07 77 60.4 61.19c1.65-1.43 3.09-2.69 3.2-2.81.27-.28 4.35-3.55 5.83-4.66A202.5 202.5 0 0 1 93.6 37.8c3.23-1.81 17.07-8.6 17.52-8.6a18.87 18.87 0 0 0 2.81-1.2 17.53 17.53 0 0 1 2.88-1.2 3.17 3.17 0 0 0 1.09-.37c.39-.2 2-.85 3.5-1.44 2.6-1 4.28-1.55 7.8-2.62a21.93 21.93 0 0 0 2.2-.78c3.26-1.44 22.46-6.08 30-7.23 7.34-1.13 14.92-2 20.87-2.39 3.7-.24 6.8-.51 6.9-.6.33-.33 17.08-.16 21.41.22m-19 95.86a132.26 132.26 0 0 0-13.4 1.41c-7.55 1.61-11.8 2.71-12.38 3.21a2 2 0 0 1-1.11.33 3.72 3.72 0 0 0-1.42.36c-.38.19-2.62 1.2-5 2.25a52.33 52.33 0 0 0-24.52 21.53c-9.16 16.09-7.68 41.27 3.3 56.07 4.69 6.33 18.6 16.14 22.93 16.18a1.87 1.87 0 0 1 1 .34 9.52 9.52 0 0 0 2.18.85c1 .28 2.25.66 2.8.84 4.41 1.45 12.65 3 31.8 6s24.61 6 24.6 13.67c0 7.88-5.72 11.5-20.05 12.71-15.4 1.3-26.08-2.95-31.92-12.72-4.17-7-7.42-7.54-18.83-3.29-1.87.69-4.21 1.53-5.2 1.86s-2.16.75-2.6.95-1.61.63-2.6 1c-14.29 4.75-16.86 7.66-14 15.85 8.9 25.66 38.58 39.92 77.39 37.21 46.77-3.28 71.81-28.34 65.68-65.76-2.92-17.85-13.39-30.58-30.45-37-1.32-.5-2.76-1-3.2-1.23a30.22 30.22 0 0 0-3.8-1.27L230 178c-.77-.22-2.3-.57-3.4-.8s-2.72-.57-3.6-.76a83 83 0 0 0-8.6-1.65c-3.63-.5-5.82-.82-14.43-2.11-17.62-2.64-23.17-5.89-23.17-13.57 0-7.17 4.93-10.49 17-11.45 14.8-1.18 22.39 1.92 28.79 11.77 4.12 6.34 6.15 7.72 9.68 6.59 1.72-.55 8.89-3.43 11.94-4.79 1.09-.49 2.53-1.12 3.19-1.4 2.2-.93 4-1.7 5.9-2.55a15.72 15.72 0 0 1 2.13-.85c.12 0 2.06-.87 4.3-1.93 8-3.82 9.22-7 5.43-14.56q-9.24-18.4-27.36-26.11a47.53 47.53 0 0 0-10-3.38l-3.4-.82c-1.63-.4-5.65-1-10.2-1.57-3.46-.42-18.38-.8-22.6-.58m21.6 1.15c26.59 3 42.25 12.63 51.54 31.83 3.7 7.65 2.12 10.54-8.09 14.78-2.18.9-4.79 2-5.8 2.46l-3.15 1.38a564.85 564.85 0 0 0-8.3 3.58c-9.46 4.15-11.56 3.91-15.16-1.75s-4-6.13-5.93-7.89c-4.84-4.35-10.84-6.19-20.19-6.19a43.51 43.51 0 0 0-11.72 1.27c-6.19 1.3-9.78 4.79-10.26 10-.78 8.49 6 12.88 23.46 15.14 1.87.24 4.39.6 5.6.8s3.55.56 5.2.81c28.6 4.29 43.4 11.25 52.09 24.5 10.19 15.53 10.69 41.77 1.07 55.75a8.15 8.15 0 0 0-1.16 2.09.63.63 0 0 1-.3.53 12.6 12.6 0 0 0-1.7 1.93c-4.36 5.61-11.93 11.44-19.6 15.09-7.84 3.73-13.6 5.46-25.6 7.68-10.36 1.92-32 1.35-43.2-1.14l-3-.65a51.68 51.68 0 0 1-9.49-3 3.48 3.48 0 0 0-1.21-.35 31.25 31.25 0 0 1-4.42-2c-11-5.59-18.9-13.21-23.7-22.91-5.79-11.71-4.51-15.28 6.87-19.24 7.83-2.73 11.23-3.93 11.95-4.24a79.64 79.64 0 0 1 9.2-3.13c6-1.66 8.24-.64 12.1 5.57 5.51 8.86 13.82 12.75 27.27 12.75 17 0 25.24-4.35 25.31-13.41.06-7.91-6-11.57-23.48-14.18-8.62-1.29-15.33-2.37-20.2-3.25l-4.2-.76a104.4 104.4 0 0 1-12-3.12c-27-8.71-39.93-32.88-32.9-61.68 6.85-28.06 40.29-43.76 83.1-39m178 115.09c.19 1.11.24 1.16.37.36a1.81 1.81 0 0 0-.22-1.31c-.22-.22-.28.15-.15.95m-198.46 161a14.54 14.54 0 0 0 2.8 0c.83-.08.25-.16-1.29-.16s-2.22.07-1.51.16m9.21 0a12.55 12.55 0 0 0 2.6 0c.71-.09.13-.16-1.3-.16s-2 .07-1.3.16" + fill="#fc9f9b" + /> + </g> + </svg> +); + +export default Staking4All; diff --git a/src/config/validators/StakingFacilities.tsx b/src/config/validators/StakingFacilities.tsx new file mode 100644 index 0000000000..16cdbd3cc6 --- /dev/null +++ b/src/config/validators/StakingFacilities.tsx @@ -0,0 +1,46 @@ +const StakingFacilities = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600"> + <defs> + <linearGradient + id="linear-gradient" + y1={300} + x2={600} + y2={300} + gradientUnits="userSpaceOnUse" + > + <stop offset={0} stopColor="#141b30" /> + <stop offset={1} stopColor="#1c3d6f" /> + </linearGradient> + </defs> + <path + style={{ + stroke: '#010101', + strokeMiterlimit: 10, + fill: 'url(#linear-gradient)', + }} + d="M0 0h600v600H0z" + /> + <path + fill="#fff" + d="M498.16 210.2v-21.48l-142.45-68.44-253.87 52.52v23.87l251.48-39.79 144.84 53.32z" + /> + <path + fill="#fff" + d="M498.16 219.75v22.29L334.22 210.2l-232.38 26.27V210.2l238.75-36.6 157.57 46.15z" + /> + <path + fill="#fff" + d="M498.16 252.38v23.08L319.1 263.92l-217.26 11.54v-27.85l219.65-19.9 176.67 24.67zM498.16 287.4v23.87l-198.47 6.37-197.85-6.37V287.4l202.14-5.57 194.18 5.57zM498.16 324v24.15l-217.45 25.42-178.87-27.96V324l180.78 13.34L498.16 324z" + /> + <path + fill="#fff" + d="M498.16 362.77v24.79l-235.89 38.77-160.43-46.4v-22.88l162.34 34.64 233.98-28.92z" + /> + <path + fill="#fff" + d="M101.84 388.19v21.61l143.28 69.92 253.04-53.39v-24.79l-251.14 41.95-145.18-55.3z" + /> + </svg> +); + +export default StakingFacilities; diff --git a/src/config/validators/TurboFlakes.tsx b/src/config/validators/TurboFlakes.tsx new file mode 100644 index 0000000000..1d905e56ff --- /dev/null +++ b/src/config/validators/TurboFlakes.tsx @@ -0,0 +1,29 @@ +const TurboFlakes = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 156.84 156.84"> + <circle + cx={78.42} + cy={78.42} + r={78.42} + style={{ + fill: '#ed1c24', + }} + /> + <circle + cx={53.96} + cy={108.04} + r={13.17} + style={{ + fill: '#fff', + }} + /> + <path + d="M98.23 78.64a13.17 13.17 0 0 1-13.17 13.17c-7.27 0-37.64-13.17-37.64-13.17s30.37-13.17 37.64-13.17a13.17 13.17 0 0 1 13.17 13.17ZM116.59 49a13.17 13.17 0 0 1-13.17 13.17C96.15 62.18 53.54 49 53.54 49s42.6-13.17 49.88-13.17A13.17 13.17 0 0 1 116.59 49Z" + transform="translate(-.52 -.22)" + style={{ + fill: '#fff', + }} + /> + </svg> +); + +export default TurboFlakes; diff --git a/src/config/validators/VFValidierung.tsx b/src/config/validators/VFValidierung.tsx new file mode 100644 index 0000000000..8a1b8f4796 --- /dev/null +++ b/src/config/validators/VFValidierung.tsx @@ -0,0 +1,40 @@ +const VFValidierung = () => ( + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 513"> + <path + strokeMiterlimit={10} + fill="none" + stroke="#fff" + d="M.5.5h512v512H.5z" + /> + <g> + <path + d="M98.01 432.1c-8.17-7.77-14.62-15.05-17-17.82-20.73-23.72-37.61-54.18-48.2-87a261.4 261.4 0 0 1-10.12-45.52c-1.29-9.35-1.8-35.83-.88-46.56 3.29-38.71 17.73-79 40-111.76a232.51 232.51 0 0 1 46.34-49.94c11.36-9.07 29.79-20.75 44.07-27.9 19.66-9.86 34.16-14.75 56.39-19 19.69-3.78 26.86-4.48 46-4.48 49.81 0 91.83 12.8 133.71 40.73a157.89 157.89 0 0 1 22.07 17.6c8.06 6.15 17.88 16.67 17.88 16.67 16.51 15.39 31 39.48 41.05 61.69a236.86 236.86 0 0 1 14.18 155.54c-13.62 54.39-46.64 103-91.45 134.47-32 22.48-68.33 36.32-108.21 41.19-7.58.92-36.17 1.65-43.18 1.09-40.26-3.2-76-14.38-107-33.46-7.6-4.68-22.56-15.27-22.56-15.27" + fill="#ffcd05" + /> + <path + d="M27.4 307.85c-1.9-8.2-3.46-16.78-4.75-26.1s-1.8-35.83-.88-46.56c3.29-38.71 17.73-79 40-111.76a232.51 232.51 0 0 1 46.38-49.93c11.36-9.07 29.79-20.75 44.07-27.9 19.66-9.86 34.16-14.75 56.39-19 19.69-3.78 26.86-4.48 46-4.48 49.81 0 91.83 12.8 133.71 40.73a157.89 157.89 0 0 1 22.07 17.6c8.06 6.15 17.88 16.67 17.88 16.67 16.51 15.39 31 39.48 41.05 61.69a235.05 235.05 0 0 1 15.49 149.07" + fill="#ed2224" + /> + <path + d="M24.34 215.66a241.6 241.6 0 0 1 37.47-92.23 232.51 232.51 0 0 1 46.34-49.93c11.36-9.07 29.79-20.75 44.07-27.9 19.66-9.86 34.16-14.75 56.39-19 19.69-3.78 26.86-4.48 46-4.48 49.81 0 91.83 12.8 133.71 40.73a157.89 157.89 0 0 1 22.07 17.6c8.06 6.15 17.88 16.67 17.88 16.67 16.51 15.39 31 39.48 41.05 61.69a231.88 231.88 0 0 1 17.71 56.88" + fill="#010101" + /> + <path + d="M98.01 432.1C252.38 245.68 327.32 164.5 418.83 87.64c5.22 4.88 9.49 9.08 9.49 9.08 16.51 15.4 30.76 38.28 41.49 61.69 22.29 48.68 26.69 103.9 13.69 155.94-13.62 54.39-46.64 103-91.45 134.47-32 22.48-68.33 36.32-108.21 41.19-7.58.92-36.17 1.65-43.18 1.09-40.26-3.2-76-14.38-107-33.46-7.6-4.68-22.56-15.27-22.56-15.27" + fill="#c0c0cc" + /> + <path + d="M118.85 439.75c3.65-1.22 10.07-3.45 14.27-5a69.46 69.46 0 0 1 15.57-3.87c7.64-1.09 13.56-2.73 16.27-4.5a6 6 0 0 0 1.93-2.78c.56-1.7.4-2.45-1.38-6.35-.79-2.79-3.76-4.17-.21-4.69 4.54-.67 8 1 18.89 9.21 15.16 11.4 26.34 16.2 37.79 16.2a35.39 35.39 0 0 0 16.69-3.74c4.49-2.24 5.1-3.55 5.15-11 .06-8.21-3-16.09-8.89-23.23a43.17 43.17 0 0 0-12.68-10.13c-3.29-1.52-3.56-2-2.18-4 1.24-1.77 4.54-2.71 9.37-2.65 5.16.07 9.88 1.51 20.56 6.29a83.91 83.91 0 0 0 27.05 7.4c15.13 1.57 27.81-1.36 35-8.11l2.86-2.68v-6.53c0-9.55-1.7-15.62-5.63-20.29a54.09 54.09 0 0 0-6.34-5.5c-5.7-4.33-8.85-7.41-11.39-11.1-3.26-4.73-4-4.57 19.16-4.18 43.13.73 64-3.06 74.46-13.57 5.06-5.07 6.37-8.65 5.9-16.21-.41-6.7-1-8.87-3.27-12.48-2.59-4.09-6.64-6.42-17.07-9.85-8.82-2.9-13.12-4.86-14.63-6.67-1.1-1.34 1.15-5.19 4.64-8 5.64-4.46 13.77-7.51 32.9-12.37 19.41-4.93 22.43-6.23 29.09-12.45 12.32-11.53 14.93-25.31 6.37-33.64-3.27-3.19-6.76-4.76-16.53-7.45-13.27-3.64-14.25-4.21-15.12-8.87-.48-2.56 1-6.1 5.18-12.58 2.82-4.34 6-7.39 14.5-13.74 7.11-5.33 11.3-9.79 13.6-14.48 1.84-3.72 2-4.37 1.92-9.59-.08-7.89-.31-8.26-6.95-11.43-7.82-3.73-7.9-3.89-5.41-11.84 3.34-10.69 7.42-15.82 4.16-23.05l-5.71-12.67s-7.95-6-15.07-6.13c-10.36-.26-16.19.61-20.1 3-7.08 4.33-8.59 5.1-10.65 5.45-2.66.45-4.18-.65-6.58-4.75-1.6-2.72-6.69-6.52-9.48-7.07-2.51-.5-8.42 1.1-14.13 3.82-4.14 2-5.72 3.22-11.75 9.28-7.25 7.29-10.73 9.57-15.83 10.41-2.23.37-2.54.25-4.07-1.55a52.51 52.51 0 0 1-4.53-8.11c-6.56-14-8.71-18.09-11-20.68q-9.22-10.45-24.26.27a26.73 26.73 0 0 0-5.3 4.93c-2.79 4-6.95 13.74-8.4 19.59-.77 3.14-3.62 11.89-6.33 19.45s-5.62 16.78-6.49 20.5c-2.31 9.91-4.91 13.68-8.77 12.71-2.6-.65-5.18-3.42-11.71-12.53-9.16-12.8-13.07-16.46-19.33-18.13-9.37-2.49-14.39-.18-21.47 9.93-5.52 7.88-7.82 17-7.82 30.87 0 9.39.5 13.92 3.25 29.46 1.48 8.37 2.6 12.89 4.13 16.67 3.2 7.94 5.1 15.77 5.16 21.19.05 4.74-.81 8.49-2.19 9.64-1.57 1.3-6.44-2.07-13.89-9.61-4.19-4.23-8.41-7.94-10.13-8.87-5.43-3-15-2.54-22.55 1-6 2.8-8.94 7.15-10.2 14.92-1.52 9.69 1.25 22.26 10.25 46.74 3.52 9.57 3.57 9.77 3.57 15.86 0 4.61-.29 7-1.14 9.4-1.12 3.17-1.19 3.23-3.43 3.23-2.51 0-2.33.22-5-5.75a37.66 37.66 0 0 0-18.1-18.2c-3.49-1.65-4.61-1.89-8.8-1.89-4.43 0-4.94.12-6.47 1.59a27.6 27.6 0 0 0-6.34 11.28c-1.17 4.49-1.18 14.9 0 24.09 1.24 10 1.61 10.65 13.87 24.64 4.73 5.4 7.37 10.2 8.48 15.46 1.67 7.87.82 8.4-5.41 3.4-4-3.21-6.3-4.21-8.25-3.58-1.6.51-4.74 5.54-6.48 10.37-1.46 4.08-1.56 5-1.59 14.64v10.28l-4 11.75c-2.21 6.46 7.68 25.52 7.68 25.52s17.2 8.87 20.84 7.65z" + fill="#fff" + /> + <path + d="M80.97 414.28c-20.73-23.72-37.61-54.18-48.2-87a261.4 261.4 0 0 1-10.12-45.52c-1.29-9.35-1.8-35.83-.88-46.56 3.29-38.71 17.73-79 40-111.76a232.51 232.51 0 0 1 46.38-49.94c11.36-9.07 29.79-20.75 44.07-27.9 19.66-9.86 34.16-14.75 56.39-19 19.69-3.78 26.86-4.48 46-4.48 49.81 0 91.83 12.8 133.71 40.73a157.89 157.89 0 0 1 22.07 17.6c8.06 6.15 17.88 16.67 17.88 16.67 16.51 15.39 31 39.48 41.05 61.69a236.86 236.86 0 0 1 14.18 155.54c-13.62 54.39-46.64 103-91.45 134.47-32 22.48-68.33 36.32-108.21 41.19-7.58.92-36.17 1.65-43.18 1.09-40.26-3.2-76-14.38-107-33.46-7.6-4.68-29.06-20.41-29.06-20.41-14.68-12.9-21.13-20.18-23.55-23z" + strokeWidth={18.9} + fill="none" + stroke="#fff" + /> + </g> + </svg> +); + +export default VFValidierung; diff --git a/src/config/validators/Wojdot.tsx b/src/config/validators/Wojdot.tsx new file mode 100644 index 0000000000..c0bdd6df32 --- /dev/null +++ b/src/config/validators/Wojdot.tsx @@ -0,0 +1,59 @@ +const Wojdot = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 512 512" + xmlSpace="preserve" + > + <path d="M338 513H1.041V1.104h511.793V513H338m-79.418-211.033c.667.044 1.334.088 2.167.961-.393 2.951.823 4.222 3.844 3.904 1.444-.153 2.924.039 4.667.53.606-.352 1.212-.703 2.338-1.473 1.182-2.391 3.022-4.685 3.393-7.195.574-3.89.234-7.929.123-11.9-.15-5.343-2.609-7.76-8.03-7.947-1.976-.068-3.962.146-6.803.162-.86.057-1.721.114-3.395-.032-2.865-.679-3.856.535-3.784 3.365.13 5.11.02 10.225.038 15.338.009 2.38.117 4.629-3.14 5.224-.797.145-1.317 1.806-1.964 2.77 1.081.495 2.592 1.706 3.169 1.36 2.39-1.433 4.496-3.338 7.377-5.067m21.209-62.478c10.039 6.962 26.772 7.908 36.043 1.862l-1.924-9.653c-10.763 2.432-21.305 6.135-30.155-4.126 3.194-4.236 6.13-8.245 9.186-12.16 5.26-6.74 8.575-14.2 8.95-22.851.444-10.207-3.172-18.605-12.328-23.33-10.13-5.227-20.81-5.13-30.396 1.42-8.691 5.94-11.186 14.905-9.594 25.117 1.742 11.173 8.05 19.98 15.107 28.243 2.818 3.3 1.926 5.048-.995 7.14-5.501 3.939-11.599 3.593-17.77 2.575-2.889-.477-5.728-1.25-8.658-1.904-.406 1.524-.771 2.632-.993 3.77-1.368 7.032-1.363 7.062 5.499 8.51 12.508 2.643 24.216 1.747 33.642-8.25a372.457 372.457 0 0 0 4.386 3.637m-164.928-24.91v-9.557c-3.161 0-5.473.042-7.783-.007-8.709-.187-14.148-3.8-17.017-12.038-4.124-11.836-4.282-23.897-.187-35.744 3.137-9.074 10.027-13.14 19.827-12.62 8.676.46 14.18 5.237 16.716 14.468.212.772.615 1.492.834 2.01h12.249c0-.995.074-1.646-.011-2.275-1.634-12.028-9.83-21.22-21.53-24.177-14.49-3.661-28.87.823-36.396 11.265-6.672 9.257-8.128 19.77-7.95 30.826.343 21.4 7.435 31.843 27.661 39.457v27.235h13.587V214.58m270.29-73.875c-5.832 4.763-9.916 10.442-10.277 18.754 3.76 0 6.928.206 10.042-.111 1.092-.112 2.548-1.301 2.993-2.355 3.552-8.413 9.37-12.35 18.553-12.258 9.072.092 15.44 4.19 18.27 12.753 3.637 11.01 3.673 22.325.403 33.411-2.843 9.636-9.226 13.96-19.331 14.118-2.443.038-4.886.006-7.498.006v38.51h13.842c0-8.116.12-15.917-.06-23.71-.07-2.989.735-4.367 3.869-5.068 11.232-2.513 18.134-9.99 22.005-20.478 3.732-10.113 3.8-20.579 2.054-31.03-4.515-27.04-31.625-38.45-54.865-22.542m-193.561 55.818c3.346 7.875 9.945 11.041 18.627 8.939 7.243-1.754 11.005-7.723 10.365-16.446-.653-8.915-6.564-14.199-15.51-13.865-8.825.328-13.982 5.655-14.08 14.635-.022 1.982.21 3.967.598 6.737m164.51 5.05c4.38-5.304 4.39-11.368 2.544-17.464-1.81-5.976-7.545-9.253-14.58-8.955-7.096.3-12.057 4.033-13.647 10.266-1.88 7.37.201 14.573 5.214 18.05 5.58 3.869 13.696 3.313 20.47-1.897M208.336 298.64c.282 1.642.283 3.407.934 4.887.552 1.257 1.854 2.184 2.824 3.257.933-1.11 2.195-2.084 2.72-3.362.722-1.752.887-3.735 1.29-5.618 1.529 2.362 1.754 4.677 2.51 6.802.33.927 1.689 1.897 2.694 2.037.715.1 2.207-1.048 2.348-1.822 1.541-8.452 2.884-16.941 4.268-25.34-7.083-2.486-5.308 3.528-6.512 6.268l-5.015-6.929-4.807 6.577c-2.978-6.652-2.978-6.652-6.623-5.727a4260.21 4260.21 0 0 0 3.369 18.97m36.91-9.84c0-.664.022-1.328-.006-1.99-.22-5.252-2.852-8.284-7.196-8.305-4.357-.021-7.173 3.1-7.262 8.238-.077 4.473-.146 8.954.04 13.421.18 4.295 2.7 6.686 6.939 6.98 4.064.281 7.2-2.665 7.43-6.984.185-3.47.049-6.958.055-11.36m42.299-9.87c-6.595-.817-9.586 1.547-9.721 7.842-.093 4.319-.124 8.645.016 12.962.145 4.502 2.971 7.36 7.049 7.442 4.282.086 7.231-2.863 7.367-7.626.123-4.317.506-8.715-.146-12.935-.403-2.611-2.526-4.957-4.565-7.685m16.738 25.748v-19.499l4.723-4.208c-1.712-.719-3.368-1.74-5.153-2.074-1.925-.361-4.017-.335-5.946.017-1.119.205-2.06 1.384-3.08 2.124.982.753 1.943 1.535 2.957 2.243.382.267.885.36 1.743.69 0 6.367.012 12.82-.004 19.272-.01 4.064 1.486 4.907 4.76 1.435z" /> + <path + fill="#F7F7F7" + d="M279.492 239.258a606.687 606.687 0 0 1-4.087-3.407c-9.426 9.998-21.134 10.894-33.642 8.252-6.862-1.45-6.867-1.48-5.499-8.512.222-1.137.587-2.245.993-3.77 2.93.655 5.77 1.428 8.658 1.905 6.171 1.018 12.269 1.364 17.77-2.575 2.92-2.092 3.813-3.84.995-7.14-7.057-8.263-13.365-17.07-15.107-28.243-1.592-10.212.903-19.177 9.594-25.116 9.586-6.551 20.266-6.648 30.396-1.42 9.156 4.724 12.772 13.122 12.329 23.329-.376 8.652-3.692 16.112-8.95 22.85-3.057 3.916-5.993 7.925-9.187 12.16 8.85 10.262 19.392 6.56 30.155 4.127l1.924 9.653c-9.271 6.046-26.004 5.1-36.342-2.093z" + /> + <path + fill="#F3F3F3" + d="M114.863 215.068v28.354h-13.587v-27.235c-20.226-7.614-27.318-18.057-27.662-39.457-.177-11.055 1.279-21.57 7.95-30.826 7.527-10.442 21.907-14.926 36.397-11.265 11.7 2.957 19.896 12.15 21.53 24.177.085.63.01 1.28.01 2.275h-12.248c-.22-.518-.622-1.238-.834-2.01-2.536-9.23-8.04-14.007-16.716-14.468-9.8-.52-16.69 3.546-19.827 12.62-4.095 11.847-3.937 23.908.187 35.744 2.87 8.238 8.308 11.85 17.017 12.038 2.31.05 4.622.007 7.783.007v10.046zM385.432 140.485c22.961-15.689 50.07-4.279 54.586 22.76 1.745 10.452 1.678 20.918-2.054 31.03-3.871 10.49-10.773 17.966-22.005 20.479-3.134.701-3.939 2.08-3.87 5.067.18 7.794.06 15.595.06 23.712h-13.841v-38.511c2.612 0 5.055.032 7.498-.006 10.105-.158 16.488-4.482 19.33-14.118 3.271-11.086 3.235-22.401-.403-33.41-2.829-8.563-9.197-12.662-18.27-12.754-9.182-.093-15 3.845-18.552 12.258-.445 1.054-1.901 2.243-2.993 2.355-3.114.317-6.282.111-10.042.111.361-8.312 4.445-13.991 10.556-18.973z" + /> + <path + fill="#F6F6F6" + d="M191.455 196.128c-.251-2.376-.483-4.36-.461-6.343.098-8.98 5.255-14.307 14.08-14.635 8.946-.334 14.857 4.95 15.51 13.865.64 8.723-3.122 14.692-10.365 16.446-8.682 2.102-15.281-1.064-18.764-9.333zM355.854 201.837c-6.525 4.945-14.642 5.5-20.22 1.632-5.014-3.477-7.095-10.68-5.215-18.05 1.59-6.233 6.551-9.965 13.647-10.266 7.035-.298 12.77 2.979 14.58 8.955 1.846 6.096 1.835 12.16-2.792 17.729z" + /> + <path + fill="#E4E4E4" + d="M208.248 298.227a7977.639 7977.639 0 0 1-3.281-18.558c3.645-.925 3.645-.925 6.623 5.727l4.807-6.577 5.015 6.929c1.204-2.74-.571-8.754 6.512-6.267-1.384 8.398-2.727 16.887-4.268 25.34-.141.773-1.633 1.92-2.348 1.821-1.005-.14-2.364-1.11-2.694-2.037-.756-2.125-.981-4.44-2.51-6.802-.403 1.883-.568 3.866-1.29 5.618-.525 1.278-1.787 2.253-2.72 3.362-.97-1.073-2.272-2-2.824-3.257-.65-1.48-.652-3.245-1.022-5.3z" + /> + <path + fill="#E0E0E0" + d="M245.246 289.26c-.006 3.941.13 7.429-.054 10.9-.23 4.318-3.367 7.264-7.431 6.983-4.239-.294-6.76-2.685-6.939-6.98-.186-4.467-.117-8.948-.04-13.42.089-5.139 2.905-8.26 7.262-8.24 4.344.022 6.976 3.054 7.196 8.307.028.661.005 1.325.006 2.45m-9.902-.382c.008 3.47-.18 6.96.139 10.401.102 1.098 1.546 2.072 2.375 3.102.825-1.033 2.275-2.017 2.361-3.108.299-3.778.349-7.613.024-11.385-.142-1.642-1.426-3.185-2.19-4.772-.903 1.622-1.805 3.244-2.71 5.762z" + /> + <path + fill="#E1E1E1" + d="M287.889 279.06c1.695 2.597 3.818 4.943 4.221 7.554.652 4.22.27 8.618.146 12.935-.136 4.763-3.085 7.712-7.367 7.626-4.078-.081-6.904-2.94-7.05-7.442-.139-4.317-.108-8.643-.015-12.962.135-6.295 3.126-8.66 10.065-7.71m-1.052 5.017c-3.042-2.192-4.262-.557-4.393 2.338-.186 4.148-.248 8.326.06 12.46.093 1.239 1.597 2.373 2.455 3.555.825-1.213 2.302-2.395 2.36-3.643.213-4.636-.04-9.293-.482-14.71z" + /> + <path + fill="#E6E6E6" + d="M261.141 279.078c1.981-.085 3.967-.299 5.942-.231 5.422.186 7.881 2.604 8.03 7.947.112 3.971.452 8.01-.122 11.9-.371 2.51-2.211 4.804-3.937 7.355a26.844 26.844 0 0 0-2.073.858c-1.464-.036-2.944-.228-4.388-.075-3.02.318-4.237-.953-3.762-4.768.157-8.238.234-15.612.31-22.986m4.173 9.746v13.219c3.625.904 5.354-.026 5.354-3.39 0-3.796.005-7.592-.002-11.387-.006-3.22-1.6-4.396-5.08-3.722-.094 1.512-.183 2.946-.272 5.28z" + /> + <path + fill="#E5E5E5" + d="M304.246 305.086c-3.237 3.063-4.734 2.22-4.723-1.844.016-6.452.004-12.905.004-19.272-.858-.33-1.36-.423-1.743-.69-1.014-.708-1.975-1.49-2.958-2.243 1.021-.74 1.962-1.92 3.081-2.124 1.929-.352 4.021-.378 5.946-.017 1.785.335 3.44 1.355 5.153 2.074l-4.723 4.208c0 5.613 0 12.556-.037 19.908z" + /> + <path + fill="#D5D5D5" + d="M257.912 301.965c-2.21 1.731-4.318 3.636-6.707 5.07-.577.345-2.088-.866-3.17-1.361.648-.964 1.168-2.625 1.964-2.77 3.258-.595 3.15-2.844 3.141-5.224-.019-5.113.092-10.228-.038-15.338-.072-2.83.919-4.044 4.199-2.807.48 7.849.545 15.14.61 22.43z" + /> + <path + fill="#303030" + d="M258.247 301.966c-.4-7.292-.466-14.582-.54-22.33.853-.513 1.713-.57 3.004-.592.354 7.408.277 14.782.036 22.605-.831.406-1.498.362-2.5.317zM269.12 307.134c.37-.46.88-.693 1.674-.876-.322.4-.928.752-1.674.876z" + /> + <path + fill="#202020" + d="M235.345 288.43c.903-2.07 1.805-3.692 2.708-5.314.764 1.587 2.048 3.13 2.19 4.772.325 3.772.275 7.607-.024 11.385-.086 1.091-1.536 2.075-2.36 3.108-.83-1.03-2.274-2.004-2.376-3.102-.318-3.442-.13-6.93-.138-10.849zM287.02 284.462c.26 5.033.512 9.69.298 14.326-.057 1.248-1.534 2.43-2.359 3.643-.858-1.182-2.362-2.316-2.454-3.556-.309-4.133-.247-8.31-.06-12.46.13-2.894 1.35-4.529 4.575-1.953z" + /> + <path + fill="#2E2E2E" + d="M265.314 288.374c.09-1.884.178-3.318.271-4.83 3.481-.674 5.075.502 5.08 3.722.008 3.795.003 7.59.003 11.387 0 3.364-1.73 4.294-5.354 3.39v-13.67z" + /> + </svg> +); + +export default Wojdot; diff --git a/src/config/validators/bLdNodes.tsx b/src/config/validators/bLdNodes.tsx new file mode 100644 index 0000000000..669b51e886 --- /dev/null +++ b/src/config/validators/bLdNodes.tsx @@ -0,0 +1,21 @@ +const IconBldnodes = () => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="1em" + height="1em" + viewBox="0 0 285.000000 222.000000" + preserveAspectRatio="xMidYMid meet" + > + <g + transform="translate(0.000000,222.000000) scale(0.100000,-0.100000)" + fill="#000000" + stroke="none" + > + <path d="M2320 1993 c-36 -11 -62 -29 -73 -48 -8 -13 -14 -105 -17 -266 l-5 -246 -239 269 c-132 149 -251 274 -265 280 -38 14 -99 3 -127 -23 -29 -27 -41 -81 -25 -115 21 -46 712 -814 739 -821 44 -11 94 9 120 47 l24 35 -4 407 c-5 452 -4 445 -73 474 -19 8 -36 14 -37 13 -2 0 -10 -3 -18 -6z" /> + <path d="M755 1951 c-11 -5 -31 -20 -44 -35 -30 -32 -32 -94 -5 -128 11 -13 306 -346 657 -740 674 -756 666 -749 736 -733 61 13 103 83 82 138 -9 23 -1271 1450 -1314 1485 -20 17 -86 25 -112 13z" /> + <path d="M480 1252 c-58 -29 -60 -41 -53 -493 l6 -402 28 -31 c23 -26 35 -31 73 -31 36 0 51 6 73 28 l28 27 5 253 5 252 238 -268 c131 -147 247 -272 257 -277 25 -13 83 -13 113 1 47 21 71 94 46 141 -18 33 -677 771 -711 796 -37 27 -62 28 -108 4z" /> + </g> + </svg> +); + +export default IconBldnodes; diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 0000000000..ff7511dfd8 --- /dev/null +++ b/src/consts.ts @@ -0,0 +1,92 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { stringToU8a } from '@polkadot/util'; +import BigNumber from 'bignumber.js'; +import type { Plugin } from 'types'; + +/* + * Global Constants + */ +export const AppVersion = '1.0.8'; +export const DappName = 'Polkadot Staking Dashboard'; +export const PolkadotUrl = 'https://polkadot.network/features/staking/'; +export const DefaultNetwork = 'polkadot'; +export const ManualSigners = ['ledger', 'vault']; +/* + * Data Structure Helpers + */ +export const EmptyH256 = new Uint8Array(32); +export const ModPrefix = stringToU8a('modl'); +export const U32Opts = { bitLength: 32, isLe: true }; + +export const SideMenuMaximisedWidth = 185; +export const SideMenuMinimisedWidth = 75; +export const SideMenuStickyThreshold = 1150; +export const SectionFullWidthThreshold = 1000; +export const ShowAccountsButtonWidthThreshold = 825; +export const FloatingMenuWidth = 250; +export const SmallFontSizeMaxWidth = 600; +export const TipsThresholdSmall = 750; +export const TipsThresholdMedium = 1200; + +/* + * Available plugins + */ +export const PluginsList: Plugin[] = [ + 'subscan', + 'binance_spot', + 'tips', + 'polkawatch', +]; + +/* + * Fallback config values + */ +export const FallbackMaxNominations = new BigNumber(16); +export const FallbackBondingDuration = new BigNumber(28); +export const FallbackSessionsPerEra = new BigNumber(6); +export const FallbackNominatorRewardedPerValidator = new BigNumber(512); +export const FallbackMaxElectingVoters = new BigNumber(22500); +export const FallbackExpectedBlockTime = new BigNumber(6000); +export const FallbackEpochDuration = new BigNumber(2400); + +/* + * Misc values + */ +export const ListItemsPerPage = 25; +export const ListItemsPerBatch = 25; +export const MinBondPrecision = 3; +export const MaxPayoutDays = 60; +export const MaxEraRewardPointsEras = 14; + +/* + * Third party API keys and endpoints + */ +export const ApiSubscanKey = 'd37149339f64775155a82a53f4253b27'; +export const EndpointPrice = 'https://api.binance.com/api/v3'; +export const ApiEndpoints = { + priceChange: `${EndpointPrice}/ticker/24hr?symbol=`, + subscanRewardSlash: '/api/v2/scan/account/reward_slash', + subscanPoolRewards: '/api/scan/nomination_pool/rewards', + subscanEraStat: '/api/scan/staking/era_stat', + subscanPoolMembers: '/api/scan/nomination_pool/pool/members', + subscanPoolDetails: '/api/scan/nomination_pool/pool', +}; + +/* + * default network parameters + */ +export const DefaultParams = { + auctionAdjust: 0, + auctionMax: 0, + falloff: 0.05, + maxInflation: 0.1, + minInflation: 0.025, + stakeTarget: 0.5, +}; + +/* + * locale + */ +export const DefaultLocale = 'en'; diff --git a/src/contexts/ActiveAccounts/defaults.ts b/src/contexts/ActiveAccounts/defaults.ts new file mode 100644 index 0000000000..3335436090 --- /dev/null +++ b/src/contexts/ActiveAccounts/defaults.ts @@ -0,0 +1,14 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { ActiveAccountsContextInterface } from './types'; + +export const defaultActiveAccountsContext: ActiveAccountsContextInterface = { + activeAccount: null, + activeProxy: null, + activeProxyType: null, + getActiveAccount: () => null, + setActiveAccount: (address, updateLocal) => {}, + setActiveProxy: (address, updateLocal) => {}, +}; diff --git a/src/contexts/ActiveAccounts/index.tsx b/src/contexts/ActiveAccounts/index.tsx new file mode 100644 index 0000000000..c3f5ff907c --- /dev/null +++ b/src/contexts/ActiveAccounts/index.tsx @@ -0,0 +1,79 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ReactNode } from 'react'; +import { createContext, useContext, useEffect, useRef, useState } from 'react'; +import type { MaybeAddress } from 'types'; +import { setStateWithRef } from '@polkadot-cloud/utils'; +import { useNetwork } from 'contexts/Network'; +import type { ActiveAccountsContextInterface, ActiveProxy } from './types'; +import { defaultActiveAccountsContext } from './defaults'; + +export const ActiveAccountsContext = + createContext<ActiveAccountsContextInterface>(defaultActiveAccountsContext); + +export const ActiveAccountsProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const { network } = useNetwork(); + + // Store the currently active account. + const [activeAccount, setActiveAccountState] = useState<MaybeAddress>(null); + const activeAccountRef = useRef<string | null>(activeAccount); + + // Store the active proxy account. + const [activeProxy, setActiveProxyState] = useState<ActiveProxy>(null); + const activeProxyRef = useRef(activeProxy); + + // Setter for the active proxy account. + const setActiveProxy = (newActiveProxy: ActiveProxy, updateLocal = true) => { + if (updateLocal) + if (newActiveProxy) { + localStorage.setItem( + `${network}_active_proxy`, + JSON.stringify(newActiveProxy) + ); + } else { + localStorage.removeItem(`${network}_active_proxy`); + } + setStateWithRef(newActiveProxy, setActiveProxyState, activeProxyRef); + }; + + // Setter for the active account. + const setActiveAccount = ( + newActiveAccount: MaybeAddress, + updateLocalStorage: boolean = true + ) => { + if (updateLocalStorage) + if (newActiveAccount === null) + localStorage.removeItem(`${network}_active_account`); + else localStorage.setItem(`${network}_active_account`, newActiveAccount); + + setStateWithRef(newActiveAccount, setActiveAccountState, activeAccountRef); + }; + + // Getter for the active account. + const getActiveAccount = () => activeAccountRef.current; + + // Disconnect from the active account on network change, but don't remove local record. + useEffect(() => setActiveAccount(null, false), [network]); + + return ( + <ActiveAccountsContext.Provider + value={{ + activeAccount: activeAccountRef.current, + activeProxy: activeProxyRef.current?.address ?? null, + activeProxyType: activeProxyRef.current?.proxyType ?? null, + setActiveAccount, + getActiveAccount, + setActiveProxy, + }} + > + {children} + </ActiveAccountsContext.Provider> + ); +}; + +export const useActiveAccounts = () => useContext(ActiveAccountsContext); diff --git a/src/contexts/ActiveAccounts/types.ts b/src/contexts/ActiveAccounts/types.ts new file mode 100644 index 0000000000..dc5f98cc2a --- /dev/null +++ b/src/contexts/ActiveAccounts/types.ts @@ -0,0 +1,21 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { MaybeAddress } from 'types'; + +export interface ActiveAccountsContextInterface { + activeAccount: MaybeAddress; + activeProxy: MaybeAddress; + activeProxyType: string | null; + getActiveAccount: () => string | null; + setActiveAccount: ( + address: MaybeAddress, + updateLocalStorage?: boolean + ) => void; + setActiveProxy: (address: ActiveProxy, updateLocalStorage?: boolean) => void; +} + +export type ActiveProxy = { + address: MaybeAddress; + proxyType: string; +} | null; diff --git a/src/contexts/Api/defaults.ts b/src/contexts/Api/defaults.ts new file mode 100644 index 0000000000..2e7afaa837 --- /dev/null +++ b/src/contexts/Api/defaults.ts @@ -0,0 +1,33 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { stringToU8a } from '@polkadot/util'; +import BigNumber from 'bignumber.js'; +import type { APIConstants, APIContextInterface } from 'contexts/Api/types'; + +export const consts: APIConstants = { + bondDuration: new BigNumber(0), + maxNominations: new BigNumber(0), + sessionsPerEra: new BigNumber(0), + maxNominatorRewardedPerValidator: new BigNumber(0), + historyDepth: new BigNumber(0), + maxElectingVoters: new BigNumber(0), + expectedBlockTime: new BigNumber(0), + epochDuration: new BigNumber(0), + existentialDeposit: new BigNumber(0), + fastUnstakeDeposit: new BigNumber(0), + poolsPalletId: stringToU8a('0'), +}; + +export const defaultApiContext: APIContextInterface = { + api: null, + consts, + chainState: undefined, + isReady: false, + apiStatus: 'disconnected', + isLightClient: false, + setIsLightClient: () => {}, + rpcEndpoint: '', + setRpcEndpoint: (key) => {}, +}; diff --git a/src/contexts/Api/index.tsx b/src/contexts/Api/index.tsx new file mode 100644 index 0000000000..2bb80f79a9 --- /dev/null +++ b/src/contexts/Api/index.tsx @@ -0,0 +1,308 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { ScProvider } from '@polkadot/rpc-provider/substrate-connect'; +import { makeCancelable, rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { createContext, useContext, useEffect, useState } from 'react'; +import { NetworkList } from 'config/networks'; +import { + FallbackBondingDuration, + FallbackEpochDuration, + FallbackExpectedBlockTime, + FallbackMaxElectingVoters, + FallbackMaxNominations, + FallbackNominatorRewardedPerValidator, + FallbackSessionsPerEra, +} from 'consts'; +import type { + APIChainState, + APIConstants, + APIContextInterface, + APIProviderProps, + ApiStatus, +} from 'contexts/Api/types'; +import type { AnyApi } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import * as defaults from './defaults'; + +export const APIProvider = ({ children, network }: APIProviderProps) => { + // Store povider instance. + const [provider, setProvider] = useState<WsProvider | ScProvider | null>( + null + ); + + // Store chain state. + const [chainState, setchainState] = useState<APIChainState>(undefined); + + // Store the active RPC provider. + const initialRpcEndpoint = () => { + const local = localStorage.getItem(`${network}_rpc_endpoint`); + if (local) + if (NetworkList[network].endpoints.rpcEndpoints[local]) { + return local; + } else { + localStorage.removeItem(`${network}_rpc_endpoint`); + } + + return NetworkList[network].endpoints.defaultRpcEndpoint; + }; + const [rpcEndpoint, setRpcEndpointState] = useState<string>( + initialRpcEndpoint() + ); + + // Store whether in light client mode. + const [isLightClient, setIsLightClient] = useState<boolean>( + !!localStorage.getItem('light_client') + ); + + // API instance state. + const [api, setApi] = useState<ApiPromise | null>(null); + + // Store network constants. + const [consts, setConsts] = useState<APIConstants>(defaults.consts); + + // Store API connection status. + const [apiStatus, setApiStatus] = useState<ApiStatus>('disconnected'); + + // Set RPC provider with local storage and validity checks. + const setRpcEndpoint = (key: string) => { + if (!NetworkList[network].endpoints.rpcEndpoints[key]) return; + localStorage.setItem(`${network}_rpc_endpoint`, key); + + setRpcEndpointState(key); + }; + + // Handle light client connection. + const handleLightClientConnection = async (Sc: AnyApi) => { + const newProvider = new ScProvider( + Sc, + NetworkList[network].endpoints.lightClient + ); + connectProvider(newProvider); + }; + + // Handle a switch in API. + let cancelFn: () => void | undefined; + + const handleApiSwitch = () => { + setApi(null); + setConsts(defaults.consts); + setchainState(undefined); + }; + + // Handle connect to API. + // Dynamically load `Sc` when user opts to use light client. + const handleConnectApi = async () => { + if (api) { + await api.disconnect(); + setApi(null); + } + // handle local light client flag. + if (isLightClient) { + localStorage.setItem('light_client', isLightClient ? 'true' : ''); + } else { + localStorage.removeItem('light_client'); + } + + if (isLightClient) { + handleApiSwitch(); + setApiStatus('connecting'); + + const ScPromise = makeCancelable(import('@substrate/connect')); + cancelFn = ScPromise.cancel; + ScPromise.promise.then((Sc) => { + handleLightClientConnection(Sc); + }); + } else { + // if not light client, directly connect. + setApiStatus('connecting'); + connectProvider(); + } + }; + + // Fetch chain state. Called once `provider` has been initialised. + const getChainState = async () => { + if (!provider) return; + + // initiate new api and set connected. + const newApi = await ApiPromise.create({ provider }); + + // set connected here in case event listeners have not yet initialised. + setApiStatus('connected'); + + const newChainState = await Promise.all([ + newApi.rpc.system.chain(), + newApi.rpc.system.version(), + newApi.consts.system.ss58Prefix, + ]); + + // check that chain values have been fetched before committing to state. + // could be expanded to check supported chains. + if ( + newChainState.every((c) => { + return !!c?.toHuman(); + }) + ) { + const chain = newChainState[0]?.toString(); + const version = newChainState[1]?.toString(); + const ss58Prefix = Number(newChainState[2]?.toString()); + + // set fetched chain state in storage. + setchainState({ chain, version, ss58Prefix }); + } + + // store active network in localStorage. + // NOTE: this should ideally refer to above `chain` value. + localStorage.setItem('network', String(network)); + + // Assume chain state is correct and bootstrap network consts. + connectedCallback(newApi); + }; + + // Connection callback. Called once `provider` and `api` have been initialised. + const connectedCallback = async (newApi: ApiPromise) => { + // fetch constants. + const result = await Promise.all([ + newApi.consts.staking.bondingDuration, + newApi.consts.staking.maxNominations, + newApi.consts.staking.sessionsPerEra, + newApi.consts.staking.maxNominatorRewardedPerValidator, + newApi.consts.electionProviderMultiPhase.maxElectingVoters, + newApi.consts.babe.expectedBlockTime, + newApi.consts.babe.epochDuration, + newApi.consts.balances.existentialDeposit, + newApi.consts.staking.historyDepth, + newApi.consts.fastUnstake.deposit, + newApi.consts.nominationPools.palletId, + ]); + + // format constants. + const bondDuration = result[0] + ? new BigNumber(rmCommas(result[0].toString())) + : FallbackBondingDuration; + + const maxNominations = result[1] + ? new BigNumber(rmCommas(result[1].toString())) + : FallbackMaxNominations; + + const sessionsPerEra = result[2] + ? new BigNumber(rmCommas(result[2].toString())) + : FallbackSessionsPerEra; + + const maxNominatorRewardedPerValidator = result[3] + ? new BigNumber(rmCommas(result[3].toString())) + : FallbackNominatorRewardedPerValidator; + + const maxElectingVoters = result[4] + ? new BigNumber(rmCommas(result[4].toString())) + : FallbackMaxElectingVoters; + + const expectedBlockTime = result[5] + ? new BigNumber(rmCommas(result[5].toString())) + : FallbackExpectedBlockTime; + + const epochDuration = result[6] + ? new BigNumber(rmCommas(result[6].toString())) + : FallbackEpochDuration; + + const existentialDeposit = result[7] + ? new BigNumber(rmCommas(result[7].toString())) + : new BigNumber(0); + + const historyDepth = result[8] + ? new BigNumber(rmCommas(result[8].toString())) + : new BigNumber(0); + + const fastUnstakeDeposit = result[9] + ? new BigNumber(rmCommas(result[9].toString())) + : new BigNumber(0); + + const poolsPalletId = result[10] ? result[10].toU8a() : new Uint8Array(0); + + setConsts({ + bondDuration, + maxNominations, + sessionsPerEra, + maxNominatorRewardedPerValidator, + historyDepth, + maxElectingVoters, + epochDuration, + expectedBlockTime, + poolsPalletId, + existentialDeposit, + fastUnstakeDeposit, + }); + setApi(newApi); + }; + + // Connect function sets provider and updates active network. + const connectProvider = async (lc?: ScProvider) => { + const newProvider = + lc || + new WsProvider(NetworkList[network].endpoints.rpcEndpoints[rpcEndpoint]); + if (lc) { + await newProvider.connect(); + } + setProvider(newProvider); + }; + + // Handle an initial RPC connection. + useEffect(() => { + if (!provider && !isLightClient) { + connectProvider(); + } + }); + + // If RPC endpoint changes, and not on light client, re-connect. + useEffectIgnoreInitial(() => { + if (!isLightClient) handleConnectApi(); + }, [rpcEndpoint]); + + // Trigger API connection handler on network or light client change. + useEffect(() => { + setRpcEndpoint(initialRpcEndpoint()); + handleConnectApi(); + return () => { + cancelFn?.(); + }; + }, [isLightClient, network]); + + // Initialise provider event handlers when provider is set. + useEffectIgnoreInitial(() => { + if (provider) { + provider.on('connected', () => { + setApiStatus('connected'); + }); + provider.on('error', () => { + setApiStatus('disconnected'); + }); + getChainState(); + } + }, [provider]); + + return ( + <APIContext.Provider + value={{ + api, + consts, + chainState, + apiStatus, + isLightClient, + setIsLightClient, + rpcEndpoint, + setRpcEndpoint, + isReady: apiStatus === 'connected' && api !== null, + }} + > + {children} + </APIContext.Provider> + ); +}; + +export const APIContext = createContext<APIContextInterface>( + defaults.defaultApiContext +); + +export const useApi = () => useContext(APIContext); diff --git a/src/contexts/Api/types.ts b/src/contexts/Api/types.ts new file mode 100644 index 0000000000..e5e23c4792 --- /dev/null +++ b/src/contexts/Api/types.ts @@ -0,0 +1,53 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ApiPromise } from '@polkadot/api'; +import type { U8aLike } from '@polkadot/util/types'; +import type BigNumber from 'bignumber.js'; +import type { ReactNode } from 'react'; +import type { Network, NetworkName } from '../../types'; + +export type ApiStatus = 'connecting' | 'connected' | 'disconnected'; + +export interface APIProviderProps { + children: ReactNode; + network: NetworkName; +} + +export interface NetworkState { + name: NetworkName; + meta: Network; +} +export interface APIConstants { + bondDuration: BigNumber; + maxNominations: BigNumber; + sessionsPerEra: BigNumber; + maxNominatorRewardedPerValidator: BigNumber; + historyDepth: BigNumber; + maxElectingVoters: BigNumber; + expectedBlockTime: BigNumber; + epochDuration: BigNumber; + existentialDeposit: BigNumber; + fastUnstakeDeposit: BigNumber; + poolsPalletId: U8aLike; +} + +export type APIChainState = + | { + chain: string; + version: string; + ss58Prefix: number; + } + | undefined; + +export interface APIContextInterface { + api: ApiPromise | null; + consts: APIConstants; + chainState: APIChainState; + isReady: boolean; + apiStatus: ApiStatus; + isLightClient: boolean; + setIsLightClient: (isLightClient: boolean) => void; + rpcEndpoint: string; + setRpcEndpoint: (key: string) => void; +} diff --git a/src/contexts/Balances/Utils.ts b/src/contexts/Balances/Utils.ts new file mode 100644 index 0000000000..50a8008ddb --- /dev/null +++ b/src/contexts/Balances/Utils.ts @@ -0,0 +1,20 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { MaybeAddress } from 'types'; +import { defaultLedger } from './defaults'; +import type { Ledger } from './types'; + +/** + * @name getLedger + * @summary Get an account's ledger record according to a key. + * @param {Ledger} ledgers + * @param {string} key + * @param { MaybeAddress } address + * @returns Ledger + */ +export const getLedger = ( + ledgers: Ledger[], + key: 'stash' | 'address', + address: MaybeAddress +): Ledger => ledgers.find((l) => l[key] === address) || defaultLedger; diff --git a/src/contexts/Balances/defaults.ts b/src/contexts/Balances/defaults.ts new file mode 100644 index 0000000000..c8c19b5a47 --- /dev/null +++ b/src/contexts/Balances/defaults.ts @@ -0,0 +1,29 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import BigNumber from 'bignumber.js'; +import type { Balance, BalancesContextInterface, Ledger } from './types'; + +export const defaultBalancesContext: BalancesContextInterface = { + ledgers: [], + balances: [], + getStashLedger: (address) => defaultLedger, + getBalance: (address) => defaultBalance, + getLocks: (address) => [], + getNonce: (address) => 0, +}; + +export const defaultLedger: Ledger = { + address: null, + stash: null, + active: new BigNumber(0), + total: new BigNumber(0), + unlocking: [], +}; + +export const defaultBalance: Balance = { + free: new BigNumber(0), + reserved: new BigNumber(0), + frozen: new BigNumber(0), +}; diff --git a/src/contexts/Balances/index.tsx b/src/contexts/Balances/index.tsx new file mode 100644 index 0000000000..21a7181feb --- /dev/null +++ b/src/contexts/Balances/index.tsx @@ -0,0 +1,232 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { VoidFn } from '@polkadot/api/types'; +import { + addedTo, + matchedProperties, + removedFrom, + rmCommas, + setStateWithRef, +} from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useRef, useState } from 'react'; +import { useApi } from 'contexts/Api'; +import type { AnyApi, MaybeAddress } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; +import { getLedger } from './Utils'; +import * as defaults from './defaults'; +import type { + Balances, + BalancesContextInterface, + Ledger, + UnlockChunkRaw, +} from './types'; + +/** + * @name useBalances + * @summary A provider that subscribes to an account's balances and wrap app children. + */ +export const BalancesProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { api, isReady } = useApi(); + const { network } = useNetwork(); + const { accounts } = useImportedAccounts(); + const { getAccount } = useImportedAccounts(); + const { addExternalAccount } = useOtherAccounts(); + + const [balances, setBalances] = useState<Balances[]>([]); + const balancesRef = useRef(balances); + + const [ledgers, setLedgers] = useState<Ledger[]>([]); + const ledgersRef = useRef(ledgers); + + const unsubs = useRef<Record<string, VoidFn>>({}); + + // Handle the syncing of accounts on accounts change. + const handleSyncAccounts = () => { + // Sync removed accounts. + const handleRemovedAccounts = () => { + const removed = removedFrom(accounts, ledgersRef.current, [ + 'address', + ]).map(({ address }) => address); + + removed?.forEach((address) => { + const unsub = unsubs.current[address]; + if (unsub) unsub(); + }); + unsubs.current = Object.fromEntries( + Object.entries(unsubs.current).filter(([key]) => !removed.includes(key)) + ); + }; + // Sync added accounts. + const handleAddedAccounts = () => { + addedTo(accounts, ledgersRef.current, ['address'])?.map(({ address }) => + handleSubscriptions(address) + ); + }; + // Sync existing accounts. + const handleExistingAccounts = () => { + setStateWithRef( + matchedProperties(accounts, ledgersRef.current, ['address']), + setLedgers, + ledgersRef + ); + }; + handleRemovedAccounts(); + handleAddedAccounts(); + handleExistingAccounts(); + }; + + const handleSubscriptions = async (address: string) => { + if (!api) return undefined; + + const unsub = await api.queryMulti<AnyApi>( + [ + [api.query.staking.ledger, address], + [api.query.system.account, address], + [api.query.balances.locks, address], + ], + async ([ledger, { data: accountData, nonce }, locks]) => { + const handleLedger = () => { + const newLedger = ledger.unwrapOr(null); + + if (newLedger !== null) { + const { stash, total, active, unlocking } = newLedger; + + // add stash as external account if not present + if (!getAccount(stash.toString())) { + addExternalAccount(stash.toString(), 'system'); + } + + setStateWithRef( + Object.values([...ledgersRef.current]) + // remove stale account if it's already in list + .filter((l) => l.stash !== stash.toString()) + // add new ledger record to list. + .concat({ + address, + stash: stash.toString(), + active: new BigNumber(rmCommas(active.toString())), + total: new BigNumber(rmCommas(total.toString())), + unlocking: unlocking + .toHuman() + .map(({ era, value }: UnlockChunkRaw) => ({ + era: Number(rmCommas(era)), + value: new BigNumber(rmCommas(value)), + })), + }), + setLedgers, + ledgersRef + ); + } else { + // no ledger: remove account if it's already in list. + setStateWithRef( + Object.values([...ledgersRef.current]).filter( + (l) => l.address !== address + ), + setLedgers, + ledgersRef + ); + } + }; + + const handleAccount = () => { + const free = new BigNumber(accountData.free.toString()); + const newBalances: Balances = { + address, + nonce: nonce.toNumber(), + balance: { + free, + reserved: new BigNumber(accountData.reserved.toString()), + frozen: new BigNumber(accountData.frozen.toString()), + }, + locks: locks.toHuman().map((l: AnyApi) => ({ + ...l, + id: l.id.trim(), + amount: new BigNumber(rmCommas(l.amount)), + })), + }; + + setStateWithRef( + Object.values(balancesRef.current) + .filter((a) => a.address !== address) + .concat(newBalances), + setBalances, + balancesRef + ); + }; + + handleLedger(); + handleAccount(); + } + ); + unsubs.current[address] = unsub; + return unsub; + }; + + const unsubAll = () => { + for (const unsub of Object.values(unsubs.current)) { + unsub(); + } + unsubs.current = {}; + }; + + // fetch account balances & ledgers. Remove or add subscriptions + useEffectIgnoreInitial(() => { + if (isReady) { + handleSyncAccounts(); + } + }, [accounts, network, isReady]); + + // Unsubscribe from subscriptions on network change & unmount. + useEffectIgnoreInitial(() => { + unsubAll(); + return () => unsubAll(); + }, [network]); + + // Gets a ledger for a stash address. + const getStashLedger = (address: MaybeAddress) => { + return getLedger(ledgersRef.current, 'stash', address); + }; + + // Gets an account's balance metadata. + const getBalance = (address: MaybeAddress) => + balancesRef.current.find((a) => a.address === address)?.balance || + defaults.defaultBalance; + + // Gets an account's locks. + const getLocks = (address: MaybeAddress) => + balancesRef.current.find((a) => a.address === address)?.locks ?? []; + + // Gets an account's nonce. + const getNonce = (address: MaybeAddress) => + balancesRef.current.find((a) => a.address === address)?.nonce ?? 0; + + return ( + <BalancesContext.Provider + value={{ + ledgers: ledgersRef.current, + balances: balancesRef.current, + getStashLedger, + getBalance, + getLocks, + getNonce, + }} + > + {children} + </BalancesContext.Provider> + ); +}; + +export const BalancesContext = React.createContext<BalancesContextInterface>( + defaults.defaultBalancesContext +); + +export const useBalances = () => React.useContext(BalancesContext); diff --git a/src/contexts/Balances/types.ts b/src/contexts/Balances/types.ts new file mode 100644 index 0000000000..99ad4a820b --- /dev/null +++ b/src/contexts/Balances/types.ts @@ -0,0 +1,50 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { MaybeAddress } from 'types'; + +export interface BalancesContextInterface { + ledgers: Ledger[]; + balances: Balances[]; + getStashLedger: (a: MaybeAddress) => Ledger; + getBalance: (address: MaybeAddress) => Balance; + getLocks: (address: MaybeAddress) => BalanceLock[]; + getNonce: (address: MaybeAddress) => number; +} + +export interface Balances { + address?: string; + nonce?: number; + balance?: Balance; + locks?: BalanceLock[]; +} + +export interface Balance { + free: BigNumber; + reserved: BigNumber; + frozen: BigNumber; +} + +export interface UnlockChunkRaw { + era: string; + value: string; +} +export interface UnlockChunk { + era: number; + value: BigNumber; +} + +export interface BalanceLock { + id: string; + amount: BigNumber; + reasons: string; +} + +export interface Ledger { + address: MaybeAddress; + stash: string | null; + active: BigNumber; + total: BigNumber; + unlocking: UnlockChunk[]; +} diff --git a/src/contexts/Bonded/defaults.ts b/src/contexts/Bonded/defaults.ts new file mode 100644 index 0000000000..1b8d622d71 --- /dev/null +++ b/src/contexts/Bonded/defaults.ts @@ -0,0 +1,21 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { + BondedContextInterface, + Nominations, +} from 'contexts/Bonded/types'; + +export const nominations: Nominations = { + targets: [], + submittedIn: 0, +}; + +export const defaultBondedContext: BondedContextInterface = { + getAccount: (address) => null, + getBondedAccount: (address) => null, + getAccountNominations: (address) => [], + isController: (address) => false, + bondedAccounts: [], +}; diff --git a/src/contexts/Bonded/index.tsx b/src/contexts/Bonded/index.tsx new file mode 100644 index 0000000000..212d0ecc65 --- /dev/null +++ b/src/contexts/Bonded/index.tsx @@ -0,0 +1,171 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { VoidFn } from '@polkadot/api/types'; +import { + addedTo, + matchedProperties, + removedFrom, + setStateWithRef, +} from '@polkadot-cloud/utils'; +import React, { useEffect, useRef, useState } from 'react'; +import { useApi } from 'contexts/Api'; +import type { AnyApi, MaybeAddress } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; +import * as defaults from './defaults'; +import type { BondedAccount, BondedContextInterface } from './types'; + +export const BondedProvider = ({ children }: { children: React.ReactNode }) => { + const { network } = useNetwork(); + const { api, isReady } = useApi(); + const { accounts } = useImportedAccounts(); + const { addExternalAccount } = useOtherAccounts(); + + // Balance accounts state. + const [bondedAccounts, setBondedAccounts] = useState<BondedAccount[]>([]); + const bondedAccountsRef = useRef(bondedAccounts); + + const unsubs = useRef<Record<string, VoidFn>>({}); + + // Handle the syncing of accounts on accounts change. + const handleSyncAccounts = () => { + // Sync removed accounts. + const handleRemovedAccounts = () => { + const removed = removedFrom(accounts, bondedAccountsRef.current, [ + 'address', + ]).map(({ address }) => address); + + removed?.forEach((address) => { + const unsub = unsubs.current[address]; + if (unsub) unsub(); + }); + + unsubs.current = Object.fromEntries( + Object.entries(unsubs.current).filter(([key]) => !removed.includes(key)) + ); + }; + // Sync added accounts. + const handleAddedAccounts = () => { + addedTo(accounts, bondedAccountsRef.current, ['address'])?.map( + ({ address }) => subscribeToBondedAccount(address) + ); + }; + // Sync existing accounts. + const handleExistingAccounts = () => { + setStateWithRef( + matchedProperties(accounts, bondedAccountsRef.current, ['address']), + setBondedAccounts, + bondedAccountsRef + ); + }; + handleRemovedAccounts(); + handleAddedAccounts(); + handleExistingAccounts(); + }; + + // Handle accounts sync on connected accounts change. + useEffectIgnoreInitial(() => { + if (isReady) { + handleSyncAccounts(); + } + }, [accounts, network, isReady]); + + // Unsubscribe from subscriptions on unmount. + useEffect( + () => () => + Object.values(unsubs.current).forEach((unsub) => { + unsub(); + }), + [] + ); + + // Subscribe to account, get controller and nominations. + const subscribeToBondedAccount = async (address: string) => { + if (!api) return undefined; + + const unsub = await api.queryMulti<AnyApi>( + [ + [api.query.staking.bonded, address], + [api.query.staking.nominators, address], + ], + async ([controller, nominations]) => { + const newAccount: BondedAccount = { + address, + }; + + // set account bonded (controller) or null + let newController = controller.unwrapOr(null); + newController = + newController === null + ? null + : (newController.toHuman() as string | null); + newAccount.bonded = newController; + + // add bonded (controller) account as external account if not presently imported + if (newController) { + if (accounts.find((s) => s.address === newController) === undefined) { + addExternalAccount(newController, 'system'); + } + } + + // set account nominations. + const newNominations = nominations.unwrapOr(null); + newAccount.nominations = + newNominations === null + ? defaults.nominations + : { + targets: newNominations.targets.toHuman(), + submittedIn: newNominations.submittedIn.toHuman(), + }; + + // remove stale account if it's already in list. + const newBonded = Object.values(bondedAccountsRef.current) + .filter((a) => a.address !== address) + .concat(newAccount); + + setStateWithRef(newBonded, setBondedAccounts, bondedAccountsRef); + } + ); + + unsubs.current[address] = unsub; + return unsub; + }; + + const getBondedAccount = (address: MaybeAddress) => + bondedAccountsRef.current.find((a) => a.address === address)?.bonded || + null; + + const getAccountNominations = (address: MaybeAddress) => + bondedAccountsRef.current.find((a) => a.address === address)?.nominations + ?.targets || []; + + const getAccount = (address: MaybeAddress) => + bondedAccountsRef.current.find((a) => a.address === address) || null; + + const isController = (address: MaybeAddress) => + bondedAccountsRef.current.filter((a) => (a?.bonded || '') === address) + ?.length > 0 || false; + + return ( + <BondedContext.Provider + value={{ + getAccount, + getBondedAccount, + getAccountNominations, + isController, + bondedAccounts: bondedAccountsRef.current, + }} + > + {children} + </BondedContext.Provider> + ); +}; + +export const BondedContext = React.createContext<BondedContextInterface>( + defaults.defaultBondedContext +); + +export const useBonded = () => React.useContext(BondedContext); diff --git a/src/contexts/Bonded/types.ts b/src/contexts/Bonded/types.ts new file mode 100644 index 0000000000..7ff494e281 --- /dev/null +++ b/src/contexts/Bonded/types.ts @@ -0,0 +1,25 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { MaybeAddress } from 'types'; + +export interface BondedAccount { + address?: string; + bonded?: string; + nominations?: Nominations; +} + +export interface Nominations { + targets: Targets; + submittedIn: string | number; +} + +export type Targets = string[]; + +export interface BondedContextInterface { + getAccount: (address: MaybeAddress) => BondedAccount | null; + getBondedAccount: (address: MaybeAddress) => string | null; + getAccountNominations: (address: MaybeAddress) => Targets; + isController: (address: MaybeAddress) => boolean; + bondedAccounts: BondedAccount[]; +} diff --git a/src/contexts/Connect/ImportedAccounts/defaults.ts b/src/contexts/Connect/ImportedAccounts/defaults.ts new file mode 100644 index 0000000000..3d8bebec5d --- /dev/null +++ b/src/contexts/Connect/ImportedAccounts/defaults.ts @@ -0,0 +1,14 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { ImportedAccountsContextInterface } from './types'; + +export const defaultImportedAccountsContext: ImportedAccountsContextInterface = + { + accounts: [], + getAccount: (address) => null, + isReadOnlyAccount: (address) => false, + accountHasSigner: (address) => false, + requiresManualSign: (address) => false, + }; diff --git a/src/contexts/Connect/ImportedAccounts/index.tsx b/src/contexts/Connect/ImportedAccounts/index.tsx new file mode 100644 index 0000000000..139c8594db --- /dev/null +++ b/src/contexts/Connect/ImportedAccounts/index.tsx @@ -0,0 +1,71 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ReactNode } from 'react'; +import { createContext, useContext } from 'react'; +import type { MaybeAddress } from 'types'; +import type { ExternalAccount } from '@polkadot-cloud/react/types'; +import { ManualSigners } from 'consts'; +import { useExtensionAccounts } from '@polkadot-cloud/react/hooks'; +import { defaultImportedAccountsContext } from './defaults'; +import type { ImportedAccountsContextInterface } from './types'; +import { useOtherAccounts } from '../OtherAccounts'; + +export const ImportedAccountsContext = + createContext<ImportedAccountsContextInterface>( + defaultImportedAccountsContext + ); + +export const ImportedAccountsProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const { otherAccounts } = useOtherAccounts(); + const { extensionAccounts } = useExtensionAccounts(); + + const allAccounts = extensionAccounts.concat(otherAccounts); + + const getAccount = (who: MaybeAddress) => + allAccounts.find(({ address }) => address === who) || null; + + const isReadOnlyAccount = (address: MaybeAddress) => { + const account = getAccount(address) ?? {}; + + if (Object.prototype.hasOwnProperty.call(account, 'addedBy')) { + const { addedBy } = account as ExternalAccount; + return addedBy === 'user'; + } + return false; + }; + + // Checks whether an account can sign transactions + const accountHasSigner = (address: MaybeAddress) => + allAccounts.find( + (a) => a.address === address && a.source !== 'external' + ) !== undefined; + + // Checks whether an account needs manual signing. This is the case for Ledger accounts, + // transactions of which cannot be automatically signed by a provided `signer` as is the case with + // extensions. + const requiresManualSign = (address: MaybeAddress) => + allAccounts.find( + (a) => a.address === address && ManualSigners.includes(a.source) + ) !== undefined; + + return ( + <ImportedAccountsContext.Provider + value={{ + accounts: allAccounts, + getAccount, + isReadOnlyAccount, + accountHasSigner, + requiresManualSign, + }} + > + {children} + </ImportedAccountsContext.Provider> + ); +}; + +export const useImportedAccounts = () => useContext(ImportedAccountsContext); diff --git a/src/contexts/Connect/ImportedAccounts/types.ts b/src/contexts/Connect/ImportedAccounts/types.ts new file mode 100644 index 0000000000..1743dd74c5 --- /dev/null +++ b/src/contexts/Connect/ImportedAccounts/types.ts @@ -0,0 +1,16 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { + ExtensionAccount, + ImportedAccount, +} from '@polkadot-cloud/react/types'; +import type { MaybeAddress } from 'types'; + +export interface ImportedAccountsContextInterface { + accounts: ImportedAccount[]; + getAccount: (address: MaybeAddress) => ExtensionAccount | null; + isReadOnlyAccount: (address: MaybeAddress) => boolean; + accountHasSigner: (address: MaybeAddress) => boolean; + requiresManualSign: (address: MaybeAddress) => boolean; +} diff --git a/src/contexts/Connect/OtherAccounts/defaults.ts b/src/contexts/Connect/OtherAccounts/defaults.ts new file mode 100644 index 0000000000..7629215a2b --- /dev/null +++ b/src/contexts/Connect/OtherAccounts/defaults.ts @@ -0,0 +1,16 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { OtherAccountsContextInterface } from './types'; + +export const defaultOtherAccountsContext: OtherAccountsContextInterface = { + addExternalAccount: (a, b) => {}, + addOtherAccounts: (a) => {}, + renameOtherAccount: (a, n) => {}, + importLocalOtherAccounts: (n) => {}, + forgetOtherAccounts: (a) => {}, + forgetExternalAccounts: (a) => {}, + otherAccounts: [], + accountsInitialised: false, +}; diff --git a/src/contexts/Connect/OtherAccounts/index.tsx b/src/contexts/Connect/OtherAccounts/index.tsx new file mode 100644 index 0000000000..b4ce6519e5 --- /dev/null +++ b/src/contexts/Connect/OtherAccounts/index.tsx @@ -0,0 +1,277 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ReactNode } from 'react'; +import { createContext, useContext, useEffect, useRef, useState } from 'react'; +import { + useEffectIgnoreInitial, + useExtensions, + useExtensionAccounts, +} from '@polkadot-cloud/react/hooks'; +import { + getLocalLedgerAccounts, + getLocalVaultAccounts, +} from 'contexts/Hardware/Utils'; +import type { AnyFunction, MaybeAddress, NetworkName } from 'types'; +import { ellipsisFn, setStateWithRef } from '@polkadot-cloud/utils'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import Keyring from '@polkadot/keyring'; +import type { + ExternalAccount, + ImportedAccount, +} from '@polkadot-cloud/react/types'; +import { + getActiveAccountLocal, + getLocalExternalAccounts, + removeLocalExternalAccounts, +} from '../Utils'; +import type { OtherAccountsContextInterface } from './types'; +import { defaultOtherAccountsContext } from './defaults'; + +export const OtherAccountsContext = + createContext<OtherAccountsContextInterface>(defaultOtherAccountsContext); + +export const OtherAccountsProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const { + network, + networkData: { ss58 }, + } = useNetwork(); + const { checkingInjectedWeb3 } = useExtensions(); + const { extensionAccountsSynced } = useExtensionAccounts(); + const { activeAccount, setActiveAccount } = useActiveAccounts(); + + // Store whether other (non-extension) accounts have been initialised. + const [otherAccountsSynced, setOtherAccountsSynced] = + useState<boolean>(false); + + // Store other (non-extension) accounts list. + const [otherAccounts, setOtherAccounts] = useState<ImportedAccount[]>([]); + const otherAccountsRef = useRef(otherAccounts); + + // Store unsubscribe handlers for connected extensions. + const unsubs = useRef<Record<string, AnyFunction>>({}); + + // Store whether all accounts have been synced. + const [accountsInitialised, setAccountsInitialised] = + useState<boolean>(false); + + // Handle forgetting of an imported other account. + const forgetOtherAccounts = (forget: ImportedAccount[]) => { + // Unsubscribe and remove unsub from context ref. + if (forget.length) { + for (const { address } of forget) { + if (otherAccountsRef.current.find((a) => a.address === address)) { + const unsub = unsubs.current[address]; + if (unsub) { + unsub(); + delete unsubs.current[address]; + } + } + } + // Remove forgotten accounts from context state. + setStateWithRef( + [...otherAccountsRef.current].filter( + (a) => + forget.find(({ address }) => address === a.address) === undefined + ), + setOtherAccounts, + otherAccountsRef + ); + // If the currently active account is being forgotten, disconnect. + if (forget.find(({ address }) => address === activeAccount) !== undefined) + setActiveAccount(null); + } + }; + + // Checks `localStorage` for previously added accounts from the provided source, and adds them to + // `accounts` state. if local active account is present, it will also be assigned as active. + // Accounts are ignored if they are already imported through an extension. + const importLocalOtherAccounts = ( + getter: (n: NetworkName) => ImportedAccount[] + ) => { + // Get accounts from provided `getter` function. The resulting array of accounts must contain an + // `address` field. + let localAccounts = getter(network); + + if (localAccounts.length) { + const activeAccountInSet = + localAccounts.find( + ({ address }) => address === getActiveAccountLocal(network, ss58) + ) ?? null; + + // remove already-imported accounts. + localAccounts = localAccounts.filter( + (l) => + otherAccountsRef.current.find( + ({ address }) => address === l.address + ) === undefined + ); + + // set active account for networkData. + if (activeAccountInSet) { + setActiveAccount(activeAccountInSet?.address || null); + } + // add accounts to imported. + addOtherAccounts(localAccounts); + } + }; + + // Renames an other account. + const renameOtherAccount = (address: MaybeAddress, newName: string) => { + setStateWithRef( + [...otherAccountsRef.current].map((a) => + a.address !== address + ? a + : { + ...a, + name: newName, + } + ), + setOtherAccounts, + otherAccountsRef + ); + }; + + // Adds an external account (non-wallet) to accounts. + const addExternalAccount = (address: string, addedBy: string) => { + // ensure account is formatted correctly + const keyring = new Keyring(); + keyring.setSS58Format(ss58); + const formatted = keyring.addFromAddress(address).address; + + const newAccount = { + address: formatted, + network, + name: ellipsisFn(address), + source: 'external', + addedBy, + }; + + // get all external accounts from localStorage. + const localExternalAccounts = getLocalExternalAccounts(); + const existsLocal = localExternalAccounts.find( + (l) => l.address === address && l.network === network + ); + + // check that address is not sitting in imported accounts (currently cannot check which + // network). + const existsImported = otherAccountsRef.current.find( + (a) => a.address === address + ); + + // add external account if not there already. + if (!existsLocal && !existsImported) { + localStorage.setItem( + 'external_accounts', + JSON.stringify(localExternalAccounts.concat(newAccount)) + ); + + // add external account to imported accounts + addOtherAccounts([newAccount]); + } else if (existsLocal && existsLocal.addedBy !== 'system') { + // the external account needs to change to `system` so it cannot be removed. This will replace + // the whole entry. + localStorage.setItem( + 'external_accounts', + JSON.stringify( + localExternalAccounts.map((item) => + item.address !== address ? item : newAccount + ) + ) + ); + // re-sync account state. + setStateWithRef( + [...otherAccountsRef.current].map((item) => + item.address !== newAccount.address ? item : newAccount + ), + setOtherAccounts, + otherAccountsRef + ); + } + }; + + // Get any external accounts and remove from localStorage. + const forgetExternalAccounts = (forget: ImportedAccount[]) => { + if (!forget.length) return; + removeLocalExternalAccounts( + network, + forget.filter((i) => 'network' in i) as ExternalAccount[] + ); + + // If the currently active account is being forgotten, disconnect. + if (forget.find((a) => a.address === activeAccount) !== undefined) + setActiveAccount(null); + }; + + // Re-sync other accounts on network switch. Waits for `injectedWeb3` to be injected. + useEffect(() => { + if (!checkingInjectedWeb3) { + // unsubscribe from all accounts and reset state. + unsubscribe(); + setStateWithRef([], setOtherAccounts, otherAccountsRef); + } + return () => unsubscribe(); + }, [network, checkingInjectedWeb3]); + + // Unsubscrbe all account subscriptions. + const unsubscribe = () => { + Object.values(unsubs.current).forEach((unsub) => { + unsub(); + }); + }; + + // Add other accounts to context state. + const addOtherAccounts = (a: ImportedAccount[]) => { + setStateWithRef( + [...otherAccountsRef.current].concat(a), + setOtherAccounts, + otherAccountsRef + ); + }; + + // Once extensions are fully initialised, fetch accounts from other sources. + useEffectIgnoreInitial(() => { + if (extensionAccountsSynced) { + // Fetch accounts from supported hardware wallets. + importLocalOtherAccounts(getLocalVaultAccounts); + importLocalOtherAccounts(getLocalLedgerAccounts); + + // Mark hardware wallets as initialised. + setOtherAccountsSynced(true); + + // Finally, fetch any read-only accounts that have been added by `system` or `user`. + importLocalOtherAccounts(getLocalExternalAccounts); + } + }, [extensionAccountsSynced]); + + // Account fetching complete, mark accounts as initialised. Does not include read only accounts. + useEffectIgnoreInitial(() => { + if (extensionAccountsSynced && otherAccountsSynced === true) { + setAccountsInitialised(true); + } + }, [extensionAccountsSynced, otherAccountsSynced]); + + return ( + <OtherAccountsContext.Provider + value={{ + addExternalAccount, + addOtherAccounts, + renameOtherAccount, + importLocalOtherAccounts, + forgetOtherAccounts, + forgetExternalAccounts, + accountsInitialised, + otherAccounts: otherAccountsRef.current, + }} + > + {children} + </OtherAccountsContext.Provider> + ); +}; + +export const useOtherAccounts = () => useContext(OtherAccountsContext); diff --git a/src/contexts/Connect/OtherAccounts/types.ts b/src/contexts/Connect/OtherAccounts/types.ts new file mode 100644 index 0000000000..58a1a3aaca --- /dev/null +++ b/src/contexts/Connect/OtherAccounts/types.ts @@ -0,0 +1,16 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ImportedAccount } from '@polkadot-cloud/react/types'; +import type { MaybeAddress, NetworkName } from 'types'; + +export interface OtherAccountsContextInterface { + addExternalAccount: (a: string, addedBy: string) => void; + addOtherAccounts: (a: ImportedAccount[]) => void; + renameOtherAccount: (a: MaybeAddress, n: string) => void; + importLocalOtherAccounts: (g: (n: NetworkName) => ImportedAccount[]) => void; + forgetOtherAccounts: (a: ImportedAccount[]) => void; + forgetExternalAccounts: (a: ImportedAccount[]) => void; + accountsInitialised: boolean; + otherAccounts: ImportedAccount[]; +} diff --git a/src/contexts/Connect/Utils.ts b/src/contexts/Connect/Utils.ts new file mode 100644 index 0000000000..70b293fc8b --- /dev/null +++ b/src/contexts/Connect/Utils.ts @@ -0,0 +1,102 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import Keyring from '@polkadot/keyring'; +import { localStorageOrDefault } from '@polkadot-cloud/utils'; +import type { + ExtensionAccount, + ExternalAccount, +} from '@polkadot-cloud/react/types'; +import type { NetworkName } from 'types'; + +// adds an extension to local `active_extensions` +export const addToLocalExtensions = (id: string) => { + const localExtensions = localStorageOrDefault<string[]>( + `active_extensions`, + [], + true + ); + if (Array.isArray(localExtensions)) { + if (!localExtensions.includes(id)) { + localExtensions.push(id); + localStorage.setItem( + 'active_extensions', + JSON.stringify(localExtensions) + ); + } + } +}; + +// account utils + +// gets local `activeAccount` for a network +export const getActiveAccountLocal = (network: NetworkName, ss58: number) => { + const keyring = new Keyring(); + keyring.setSS58Format(ss58); + let account = localStorageOrDefault(`${network}_active_account`, null); + if (account !== null) { + account = keyring.addFromAddress(account).address; + } + return account; +}; + +// gets local external accounts, formatting their addresses +// using active network ss58 format. +export const getLocalExternalAccounts = (network?: NetworkName) => { + let localAccounts = localStorageOrDefault<ExternalAccount[]>( + 'external_accounts', + [], + true + ) as ExternalAccount[]; + if (network) { + localAccounts = localAccounts.filter((l) => l.network === network); + } + return localAccounts; +}; + +// gets accounts that exist in local `external_accounts` +export const getInExternalAccounts = ( + accounts: ExtensionAccount[], + network: NetworkName +) => { + const localExternalAccounts = getLocalExternalAccounts(network); + + return ( + localExternalAccounts.filter( + (a) => (accounts || []).find((b) => b.address === a.address) !== undefined + ) || [] + ); +}; + +// removes supplied accounts from local `external_accounts`. +export const removeLocalExternalAccounts = ( + network: NetworkName, + accounts: ExternalAccount[] +) => { + if (!accounts.length) return; + + let localExternalAccounts = getLocalExternalAccounts(network); + localExternalAccounts = localExternalAccounts.filter( + (a) => + accounts.find((b) => b.address === a.address && a.network === network) === + undefined + ); + localStorage.setItem( + 'external_accounts', + JSON.stringify(localExternalAccounts) + ); +}; + +export const formatAccountSs58 = (address: string, ss58: number) => { + try { + const keyring = new Keyring(); + keyring.setSS58Format(ss58); + const formatted = keyring.addFromAddress(address).address; + if (formatted !== address) { + return formatted; + } + return null; + } catch (e) { + return null; + } +}; diff --git a/src/contexts/Extrinsics/defaults.ts b/src/contexts/Extrinsics/defaults.ts new file mode 100644 index 0000000000..a1202dd7b0 --- /dev/null +++ b/src/contexts/Extrinsics/defaults.ts @@ -0,0 +1,11 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { ExtrinsicsContextInterface } from './types'; + +export const defaultExtrinsicsContext: ExtrinsicsContextInterface = { + addPending: (t) => {}, + removePending: (t) => {}, + pending: [], +}; diff --git a/src/contexts/Extrinsics/index.tsx b/src/contexts/Extrinsics/index.tsx new file mode 100644 index 0000000000..86c5ed285f --- /dev/null +++ b/src/contexts/Extrinsics/index.tsx @@ -0,0 +1,49 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import React, { useRef, useState } from 'react'; +import { defaultExtrinsicsContext } from './defaults'; +import type { ExtrinsicsContextInterface } from './types'; + +export const ExtrinsicsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [pending, setPending] = useState<string[]>([]); + const pendingRef = useRef(pending); + + const addPending = (nonce: string) => { + setStateWithRef( + [...pendingRef.current].concat(nonce), + setPending, + pendingRef + ); + }; + + const removePending = (nonce: string) => { + setStateWithRef( + pendingRef.current.filter((n: string) => n !== nonce), + setPending, + pendingRef + ); + }; + + return ( + <ExtrinsicsContext.Provider + value={{ + addPending, + removePending, + pending: pendingRef.current, + }} + > + {children} + </ExtrinsicsContext.Provider> + ); +}; + +export const ExtrinsicsContext = + React.createContext<ExtrinsicsContextInterface>(defaultExtrinsicsContext); + +export const useExtrinsics = () => React.useContext(ExtrinsicsContext); diff --git a/src/contexts/Extrinsics/types.ts b/src/contexts/Extrinsics/types.ts new file mode 100644 index 0000000000..5113f5bca9 --- /dev/null +++ b/src/contexts/Extrinsics/types.ts @@ -0,0 +1,8 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface ExtrinsicsContextInterface { + addPending: (n: string) => void; + removePending: (n: string) => void; + pending: string[]; +} diff --git a/src/contexts/FastUnstake/defaults.ts b/src/contexts/FastUnstake/defaults.ts new file mode 100644 index 0000000000..0f1cc87e0f --- /dev/null +++ b/src/contexts/FastUnstake/defaults.ts @@ -0,0 +1,19 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { FastUnstakeContextInterface, MetaInterface } from './types'; + +export const defaultMeta: MetaInterface = { + checked: [], +}; + +export const defaultFastUnstakeContext: FastUnstakeContextInterface = { + getLocalkey: (a) => '', + checking: false, + meta: defaultMeta, + isExposed: null, + queueDeposit: null, + head: null, + counterForQueue: null, +}; diff --git a/src/contexts/FastUnstake/index.tsx b/src/contexts/FastUnstake/index.tsx new file mode 100644 index 0000000000..6c7712af60 --- /dev/null +++ b/src/contexts/FastUnstake/index.tsx @@ -0,0 +1,335 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + greaterThanZero, + isNotZero, + rmCommas, + setStateWithRef, +} from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useRef, useState } from 'react'; +import { useApi } from 'contexts/Api'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useStaking } from 'contexts/Staking'; +import type { AnyApi, AnyJson, MaybeAddress } from 'types'; +import Worker from 'workers/stakers?worker'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNominationStatus } from 'library/Hooks/useNominationStatus'; +import { validateLocalExposure } from 'contexts/Validators/Utils'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { defaultFastUnstakeContext, defaultMeta } from './defaults'; +import type { + FastUnstakeContextInterface, + LocalMeta, + MetaInterface, +} from './types'; + +const worker = new Worker(); + +export const FastUnstakeProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + const { api, isReady, consts } = useApi(); + const { activeAccount } = useActiveAccounts(); + const { inSetup, fetchEraStakers } = useStaking(); + const { metrics, activeEra } = useNetworkMetrics(); + const { getNominationStatus } = useNominationStatus(); + const { fastUnstakeErasToCheckPerBlock } = metrics; + const { bondDuration } = consts; + const { nominees } = getNominationStatus(activeAccount, 'nominator'); + + // store whether a fast unstake check is in progress. + const [checking, setChecking] = useState<boolean>(false); + const checkingRef = useRef(checking); + + // store whether the account is exposed for fast unstake + const [isExposed, setIsExposed] = useState<boolean | null>(null); + const isExposedRef = useRef(isExposed); + + // store state of elibigility checking. + const [meta, setMeta] = useState<MetaInterface>(defaultMeta); + const metaRef = useRef(meta); + + // store fastUnstake queue deposit for user. + const [queueDeposit, setqueueDeposit] = useState<AnyApi>(null); + const queueDepositRef = useRef(queueDeposit); + + // store fastUnstake head. + const [head, setHead] = useState<AnyApi>(null); + const headRef = useRef(head); + + // store fastUnstake counter for queue. + const [counterForQueue, setCounterForQueue] = useState<number | null>(null); + const counterForQueueRef = useRef(counterForQueue); + + // store fastUnstake subscription unsub. + const unsubs = useRef<AnyApi[]>([]); + + // localStorage key to fetch local metadata. + const getLocalkey = (a: MaybeAddress) => `${network}_fast_unstake_${a}`; + + // check until bond duration eras surpasssed. + const checkToEra = activeEra.index.minus(bondDuration); + + // initiate fast unstake check for accounts that are nominating but not active. + useEffectIgnoreInitial(() => { + if ( + isReady && + activeAccount && + isNotZero(activeEra.index) && + fastUnstakeErasToCheckPerBlock > 0 + ) { + // cancel fast unstake check on network change or account change. + for (const unsub of unsubs.current) unsub(); + + setStateWithRef(false, setChecking, checkingRef); + setStateWithRef(null, setqueueDeposit, queueDepositRef); + setStateWithRef(null, setCounterForQueue, counterForQueueRef); + unsubs.current = []; + + // get any existing localStorage records for account. + const localMeta: LocalMeta | null = getLocalMeta(); + + const initialMeta = localMeta + ? { checked: localMeta.checked } + : defaultMeta; + + // even if localMeta.isExposed is false, we don't assume a final value until current era + + // bondDuration is checked. + let initialIsExposed = null; + if (localMeta) { + if (bondDuration.plus(1).isEqualTo(localMeta.checked.length)) { + initialIsExposed = localMeta.isExposed; + } else if (localMeta.isExposed === true) { + initialIsExposed = true; + } else { + initialIsExposed = null; + } + } + + // checkpoint: initial local meta: localMeta + + setStateWithRef(initialMeta, setMeta, metaRef); + setStateWithRef(initialIsExposed, setIsExposed, isExposedRef); + + // start process if account is inactively nominating & local fast unstake data is not + // complete. + if ( + activeAccount && + !inSetup() && + !nominees.active.length && + initialIsExposed === null + ) { + // if localMeta existed, start checking from the next era. + const nextEra = localMeta?.checked.at(-1) || 0; + const maybeNextEra = localMeta + ? new BigNumber(nextEra - 1) + : activeEra.index; + + // checkpoint: check from the possible next era `maybeNextEra`. + + processEligibility(activeAccount, maybeNextEra); + } + + // subscribe to fast unstake queue immediately if synced in localStorage and still up to date. + if (initialIsExposed === false) { + subscribeToFastUnstakeQueue(); + } + } + + return () => { + for (const unsub of unsubs.current) unsub(); + }; + }, [ + inSetup(), + isReady, + network, + activeAccount, + activeEra.index, + fastUnstakeErasToCheckPerBlock, + ]); + + // handle worker message on completed exposure check. + worker.onmessage = (message: MessageEvent) => { + if (message) { + // ensure correct task received. + const { data } = message; + const { task } = data; + if (task !== 'processEraForExposure') return; + + // ensure still same conditions. + const { networkName, who } = data; + if (networkName !== network || who !== activeAccount) return; + + const { era, exposed } = data; + + // ensure checked eras are in order highest first. + const checked = metaRef.current.checked + .concat(Number(era)) + .sort((a: number, b: number) => b - a); + + if (!metaRef.current.checked.includes(Number(era))) { + // update localStorage with updated changes. + localStorage.setItem( + getLocalkey(who), + JSON.stringify({ + isExposed: exposed, + checked, + }) + ); + + // update check metadata. + setStateWithRef( + { + checked, + }, + setMeta, + metaRef + ); + } + + if (exposed) { + // checkpoint: 'exposed! Stop checking. + + // cancel checking and update exposed state. + setStateWithRef(false, setChecking, checkingRef); + setStateWithRef(true, setIsExposed, isExposedRef); + } else if (bondDuration.plus(1).isEqualTo(checked.length)) { + // successfully checked current era - bondDuration eras. + setStateWithRef(false, setChecking, checkingRef); + setStateWithRef(false, setIsExposed, isExposedRef); + + // checkpoint: check finished, not exposed. + + // subscribe to fast unstake queue for user and queue counter. + subscribeToFastUnstakeQueue(); + } else { + // continue checking the next era. + checkEra(new BigNumber(era).minus(1)); + } + } + }; + + // initiate fast unstake eligibility check. + const processEligibility = async (a: MaybeAddress, era: BigNumber) => { + // ensure current era has synced + if ( + era.isLessThan(0) || + !greaterThanZero(bondDuration) || + !api || + !a || + checkingRef.current || + !activeAccount + ) + return; + + setStateWithRef(true, setChecking, checkingRef); + checkEra(era); + }; + + // calls service worker to check exppsures for given era. + const checkEra = async (era: BigNumber) => { + if (!api) return; + + const exposures = await fetchEraStakers(era.toString()); + + worker.postMessage({ + task: 'processEraForExposure', + era: era.toString(), + who: activeAccount, + networkName: network, + exitOnExposed: true, + exposures, + }); + }; + + // subscribe to fastUnstake queue + const subscribeToFastUnstakeQueue = async () => { + if (!api || !activeAccount) return; + const subscribeQueue = async (a: MaybeAddress) => { + const u = await api.query.fastUnstake.queue(a, (q: AnyApi) => + setStateWithRef( + new BigNumber(rmCommas(q.unwrapOrDefault(0).toString())), + setqueueDeposit, + queueDepositRef + ) + ); + return u; + }; + const subscribeHead = async () => { + const u = await api.query.fastUnstake.head((result: AnyApi) => { + const h = result.unwrapOrDefault(null).toHuman(); + setStateWithRef(h, setHead, headRef); + }); + return u; + }; + const subscribeCounterForQueue = async () => { + const u = await api.query.fastUnstake.counterForQueue( + (result: AnyApi) => { + const c = result.toHuman(); + setStateWithRef(c, setCounterForQueue, counterForQueueRef); + } + ); + return u; + }; + + // checkpoint: subscribing to queue + head. + + // initiate subscription, add to unsubs. + await Promise.all([ + subscribeQueue(activeAccount), + subscribeHead(), + subscribeCounterForQueue(), + ]).then((u: any) => { + unsubs.current = u; + }); + }; + + // gets any existing fast unstake metadata for an account. + const getLocalMeta = (): LocalMeta | null => { + const localMeta: AnyJson = localStorage.getItem(getLocalkey(activeAccount)); + if (!localMeta) return null; + + const localMetaValidated = validateLocalExposure( + JSON.parse(localMeta), + checkToEra + ); + if (!localMetaValidated) { + // remove if not valid. + localStorage.removeItem(getLocalkey(activeAccount)); + return null; + } + // set validated localStorage. + localStorage.setItem( + getLocalkey(activeAccount), + JSON.stringify(localMetaValidated) + ); + return localMetaValidated; + }; + + return ( + <FastUnstakeContext.Provider + value={{ + getLocalkey, + checking: checkingRef.current, + meta: metaRef.current, + isExposed: isExposedRef.current, + queueDeposit: queueDepositRef.current, + head: headRef.current, + counterForQueue: counterForQueueRef.current, + }} + > + {children} + </FastUnstakeContext.Provider> + ); +}; + +export const FastUnstakeContext = + React.createContext<FastUnstakeContextInterface>(defaultFastUnstakeContext); + +export const useFastUnstake = () => React.useContext(FastUnstakeContext); diff --git a/src/contexts/FastUnstake/types.ts b/src/contexts/FastUnstake/types.ts new file mode 100644 index 0000000000..1c6dfc2c1b --- /dev/null +++ b/src/contexts/FastUnstake/types.ts @@ -0,0 +1,23 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { AnyApi, MaybeAddress } from 'types'; + +export interface LocalMeta { + isExposed: boolean; + checked: number[]; +} +export interface MetaInterface { + checked: number[]; +} + +export interface FastUnstakeContextInterface { + getLocalkey: (a: MaybeAddress) => string; + checking: boolean; + meta: MetaInterface; + isExposed: boolean | null; + queueDeposit: BigNumber | null; + head: AnyApi; + counterForQueue: number | null; +} diff --git a/src/contexts/Filters/defaults.ts b/src/contexts/Filters/defaults.ts new file mode 100644 index 0000000000..009b13a09a --- /dev/null +++ b/src/contexts/Filters/defaults.ts @@ -0,0 +1,20 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { FiltersContextInterface } from './types'; + +export const defaultFiltersInterface: FiltersContextInterface = { + getFilters: (t, g) => [], + toggleFilter: (t, g, f) => {}, + setMultiFilters: (t, g, fs, r) => {}, + getOrder: (g) => 'default', + setOrder: (g, o) => {}, + getSearchTerm: (g) => null, + setSearchTerm: (g, t) => {}, + resetFilters: (t, g) => {}, + resetOrder: (g) => {}, + clearSearchTerm: (g) => {}, + applyFilters: (t, g, l, f) => {}, + applyOrder: (g, l, f) => {}, +}; diff --git a/src/contexts/Filters/index.tsx b/src/contexts/Filters/index.tsx new file mode 100644 index 0000000000..c87e6f45d9 --- /dev/null +++ b/src/contexts/Filters/index.tsx @@ -0,0 +1,221 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useState } from 'react'; +import type { AnyFunction, AnyJson } from 'types'; +import { defaultFiltersInterface } from './defaults'; +import type { + FilterItem, + FilterItems, + FilterOrder, + FilterOrders, + FilterSearch, + FilterSearches, + FilterType, + FiltersContextInterface, +} from './types'; + +export const FiltersProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + // groups along with their includes + const [includes, setIncludes] = useState<FilterItems>([]); + + // groups along with their excludes. + const [excludes, setExcludes] = useState<FilterItems>([]); + + // groups along with their order. + const [orders, setOrders] = useState<FilterOrders>([]); + + // groups along with their search terms. + const [searchTerms, setSearchTerms] = useState<FilterSearches>([]); + + // Get stored includes or excludes for a group. + const getFilters = (t: FilterType, g: string): string[] | null => { + const current = t === 'exclude' ? excludes : includes; + return current.find((e) => e.key === g)?.filters || null; + }; + + const setFilters = (t: FilterType, n: FilterItems) => { + if (t === 'exclude') { + setExcludes(n); + } else { + setIncludes(n); + } + }; + + // Toggle a filter for a group. + // Adds the group to `excludes` or `includes` if it does not already exist. + const toggleFilter = (t: FilterType, g: string, f: string) => { + const current = t === 'exclude' ? excludes : includes; + const exists = getFilters(t, g); + + if (!exists) { + const newFilters = [...current, { key: g, filters: [f] }]; + setFilters(t, newFilters); + return; + } + const newFilters = [...current] + .map((e) => { + if (e.key !== g) return e; + let { filters } = e; + + if (filters.includes(f)) { + filters.splice(filters.indexOf(f), 1); + } else { + filters = filters.concat(f); + } + return { + key: e.key, + filters, + }; + }) + .filter((e) => e.filters.length !== 0); + setFilters(t, newFilters); + }; + + // Sets an array of filters to a group. + const setMultiFilters = ( + t: FilterType, + g: string, + fs: string[], + reset: boolean + ) => { + // get the current filters from the group. + const current = reset ? [] : t === 'exclude' ? excludes : includes; + // check if filters currently exist in the group. + const exists = getFilters(t, g); + + if (!exists) { + const newFilters = [...current, { key: g, filters: [...fs] }]; + setFilters(t, newFilters); + return; + } + + let newFilters: FilterItems; + if (current.length) { + newFilters = [...current].map((e) => { + // return groups we are not manipulating. + if (e.key !== g) return e; + + let { filters } = e; + filters = filters.filter((f: string) => !fs.includes(f)).concat(fs); + return { + key: e.key, + filters, + }; + }); + } else { + newFilters = [{ key: g, filters: fs }]; + } + setFilters(t, newFilters); + }; + + // Get the current order of a list or null. + const getOrder = (g: string) => + orders.find((o) => o.key === g)?.order || 'default'; + + // Sets an order key for a group. + const setOrder = (g: string, o: string) => { + let newOrders = []; + if (o === 'default') { + newOrders = [...orders].filter((order) => order.key !== g); + } else if (orders.length) { + newOrders = [...orders].map((order) => + order.key !== g ? order : { ...order, order: o } + ); + } else { + newOrders = [{ key: g, order: o }]; + } + setOrders(newOrders); + }; + + // Get the current search term of a list or null. + const getSearchTerm = (g: string) => + searchTerms.find((o) => o.key === g)?.searchTerm || null; + + // Sets an order key for a group. + const setSearchTerm = (g: string, t: string) => { + let newSearchTerms = []; + if (orders.length) { + newSearchTerms = [...searchTerms].map((term) => + term.key !== g ? term : { ...term, searchTerm: t } + ); + } else { + newSearchTerms = [{ key: g, searchTerm: t }]; + } + setSearchTerms(newSearchTerms); + }; + + // resets excludes for a given group + const resetFilters = (t: FilterType, g: string) => { + const current = t === 'exclude' ? excludes : includes; + setFilters( + t, + [...current].filter((e: FilterItem) => e.key !== g) + ); + }; + + // resets order for a given group + const resetOrder = (g: string) => { + setOrders([...orders].filter((e: FilterOrder) => e.key !== g)); + }; + + // clear searchTerm from given group + const clearSearchTerm = (g: string) => { + setSearchTerms([...searchTerms].filter((e: FilterSearch) => e.key !== g)); + }; + + // apply filters to list + const applyFilters = ( + t: FilterType, + g: string, + list: AnyJson, + fn: AnyFunction + ) => { + const filtersToApply = getFilters(t, g); + + if (!filtersToApply) { + return list; + } + return fn(list, filtersToApply); + }; + + // apply order to a list + const applyOrder = (g: string, list: AnyJson, fn: AnyFunction) => { + const orderToApply = getOrder(g); + if (!orderToApply) { + return list; + } + return fn(list, orderToApply); + }; + + return ( + <FiltersContext.Provider + value={{ + getFilters, + toggleFilter, + setMultiFilters, + getOrder, + setOrder, + getSearchTerm, + setSearchTerm, + resetFilters, + resetOrder, + clearSearchTerm, + applyFilters, + applyOrder, + }} + > + {children} + </FiltersContext.Provider> + ); +}; + +export const FiltersContext = React.createContext<FiltersContextInterface>( + defaultFiltersInterface +); + +export const useFilters = () => React.useContext(FiltersContext); diff --git a/src/contexts/Filters/types.ts b/src/contexts/Filters/types.ts new file mode 100644 index 0000000000..fa9044c718 --- /dev/null +++ b/src/contexts/Filters/types.ts @@ -0,0 +1,35 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyFunction, AnyJson } from 'types'; + +export type FilterType = 'exclude' | 'include'; + +export interface FiltersContextInterface { + getFilters: (t: FilterType, g: string) => string[] | null; + toggleFilter: (t: FilterType, g: string, f: string) => void; + setMultiFilters: (t: FilterType, g: string, fs: string[], r: boolean) => void; + getOrder: (g: string) => string; + setOrder: (g: string, o: string) => void; + getSearchTerm: (g: string) => string | null; + setSearchTerm: (g: string, t: string) => void; + resetFilters: (t: FilterType, g: string) => void; + resetOrder: (g: string) => void; + clearSearchTerm: (g: string) => void; + applyFilters: ( + t: FilterType, + g: string, + list: AnyJson, + fn: AnyFunction + ) => void; + applyOrder: (g: string, list: AnyJson, fn: AnyFunction) => void; +} + +export type FilterItems = FilterItem[]; +export type FilterItem = { key: string; filters: string[] }; + +export type FilterOrders = FilterOrder[]; +export type FilterOrder = { key: string; order: string }; + +export type FilterSearches = FilterSearch[]; +export type FilterSearch = { key: string; searchTerm: string }; diff --git a/src/contexts/Hardware/Ledger.tsx b/src/contexts/Hardware/Ledger.tsx new file mode 100644 index 0000000000..32628b4b77 --- /dev/null +++ b/src/contexts/Hardware/Ledger.tsx @@ -0,0 +1,556 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import TransportWebHID from '@ledgerhq/hw-transport-webhid'; +import { u8aToBuffer } from '@polkadot/util'; +import { localStorageOrDefault, setStateWithRef } from '@polkadot-cloud/utils'; +import { newSubstrateApp } from '@zondax/ledger-substrate'; +import React, { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { LedgerAccount } from '@polkadot-cloud/react/types'; +import type { AnyFunction, AnyJson, MaybeString } from 'types'; +import { useNetwork } from 'contexts/Network'; +import { + getLocalLedgerAccounts, + getLocalLedgerAddresses, + isLocalNetworkAddress, +} from './Utils'; +import { + LEDGER_DEFAULT_ACCOUNT, + LEDGER_DEFAULT_CHANGE, + LEDGER_DEFAULT_INDEX, + TOTAL_ALLOWED_STATUS_CODES, + defaultFeedback, + defaultLedgerHardwareContext, +} from './defaults'; +import type { + FeedbackMessage, + LedgerAddress, + LedgerHardwareContextInterface, + LedgerResponse, + LedgerStatusCode, + LedgerTask, + PairingStatus, +} from './types'; + +export const LedgerHardwareProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { t } = useTranslation('modals'); + const { network } = useNetwork(); + + const [ledgerAccounts, setLedgerAccountsState] = useState<LedgerAccount[]>( + getLocalLedgerAccounts(network) + ); + const ledgerAccountsRef = useRef(ledgerAccounts); + + // Store whether the device has been paired. + const [isPaired, setIsPairedState] = useState<PairingStatus>('unknown'); + const isPairedRef = useRef(isPaired); + + // Store whether an import is in process. + const [isExecuting, setIsExecutingState] = useState(false); + const isExecutingRef = useRef(isExecuting); + + // Store status codes received from Ledger device. + const [statusCodes, setStatusCodes] = useState<LedgerResponse[]>([]); + const statusCodesRef = useRef(statusCodes); + + // Get the default message to display, set when a failed loop has happened. + const [feedback, setFeedbackState] = + useState<FeedbackMessage>(defaultFeedback); + + const feedbackRef = useRef(feedback); + + // Store the latest successful response from an attempted `executeLedgerLoop`. + const [transportResponse, setTransportResponse] = useState<AnyJson>(null); + + // Whether pairing is in progress: protects against re-renders & duplicate attempts. + const pairInProgress = useRef(false); + + // Whether a ledger-loop is in progress: protects against re-renders & duplicate attempts. + const ledgerLoopInProgress = useRef(false); + + // The ledger transport interface. + const ledgerTransport = useRef<any>(null); + + // Refresh imported ledger accounts on network change. + useEffect(() => { + setStateWithRef( + getLocalLedgerAccounts(network), + setLedgerAccountsState, + ledgerAccountsRef + ); + }, [network]); + + // Handles errors that occur during `executeLedgerLoop` and `pairDevice` calls. + const handleErrors = (appName: string, err: AnyJson) => { + // reset any in-progress calls. + ledgerLoopInProgress.current = false; + pairInProgress.current = false; + + // execution failed - no longer executing. + setIsExecuting(false); + + // close any open device connections. + if (ledgerTransport.current?.device?.opened) { + ledgerTransport.current?.device?.close(); + } + + // format error message. + err = String(err); + if (err === 'Error: Timeout') { + // only set default message here - maintain previous status code. + setFeedback(t('ledgerRequestTimeout'), 'Ledger Request Timeout'); + handleNewStatusCode('failure', 'DeviceTimeout'); + } else if (err.startsWith('Error: Call nesting not supported')) { + setFeedback(t('missingNesting')); + handleNewStatusCode('failure', 'NestingNotSupported'); + } else if ( + err.startsWith('Error: TransportError: Invalid channel') || + err.startsWith('Error: InvalidStateError') + ) { + // occurs when tx was approved outside of active channel. + setFeedback(t('queuedTransactionRejected'), 'Wrong Transaction'); + handleNewStatusCode('failure', 'WrongTransaction'); + } else if ( + err.startsWith('TransportOpenUserCancelled') || + err.startsWith('Error: Ledger Device is busy') + ) { + // occurs when the device is not connected. + setFeedback(t('connectLedgerToContinue')); + handleNewStatusCode('failure', 'DeviceNotConnected'); + } else if (err.startsWith('Error: LockedDeviceError')) { + // occurs when the device is connected but not unlocked. + setFeedback(t('unlockLedgerToContinue')); + handleNewStatusCode('failure', 'DeviceLocked'); + } else if (err.startsWith('Error: Transaction rejected')) { + // occurs when user rejects a transaction. + setFeedback( + t('transactionRejectedPending'), + 'Ledger Rejected Transaction' + ); + handleNewStatusCode('failure', 'TransactionRejected'); + } else if (err.startsWith('Error: Unknown Status Code: 28161')) { + // occurs when the required app is not open. + handleNewStatusCode('failure', 'AppNotOpenContinue'); + setFeedback(t('openAppOnLedger', { appName }), 'Open App On Ledger'); + } else { + // miscellanous errors - assume app is not open or ready. + setFeedback(t('openAppOnLedger', { appName }), 'Open App On Ledger'); + handleNewStatusCode('failure', 'AppNotOpen'); + } + }; + + // Timeout function for hanging tasks. Used for tasks that require no input from the device, such + // as getting an address that does not require confirmation. + const withTimeout = (millis: AnyFunction, promise: AnyFunction) => { + const timeout = new Promise((_, reject) => + setTimeout(async () => { + ledgerTransport.current?.device?.close(); + reject(Error('Timeout')); + }, millis) + ); + return Promise.race([promise, timeout]); + }; + + // Attempt to pair a device. + // + // Trigger a one-time connection to the device to determine if it is available. If the device + // needs to be paired, a browser prompt will open. If cancelled, an error will be thrown. + const pairDevice = async () => { + try { + // return `paired` if pairing is already in progress. + if (pairInProgress.current) { + return isPairedRef.current === 'paired'; + } + // set pairing in progress. + pairInProgress.current = true; + + // remove any previously stored status codes. + resetStatusCodes(); + + // close any open connections. + if (ledgerTransport.current?.device?.opened) { + await ledgerTransport.current?.device?.close(); + } + // establish a new connection with device. + ledgerTransport.current = await TransportWebHID.create(); + setIsPaired('paired'); + pairInProgress.current = false; + return true; + } catch (err) { + pairInProgress.current = false; + handleErrors('', err); + return false; + } + }; + + // Connects to a Ledger device to perform a task. This is the main execute function that handles + // all Ledger tasks, along with errors that occur during the process. + const executeLedgerLoop = async ( + appName: string, + tasks: LedgerTask[], + options?: AnyJson + ) => { + try { + // do not execute again if already in progress. + if (ledgerLoopInProgress.current) { + return; + } + + // set ledger loop in progress. + ledgerLoopInProgress.current = true; + + // test for tasks and execute them. This is designed such that `result` will only store the + // result of one task. This will have to be refactored if we ever need to execute multiple + // tasks at once. + let result = null; + if (tasks.includes('get_address')) { + result = await handleGetAddress(appName, options?.accountIndex || 0); + } else if (tasks.includes('sign_tx')) { + const uid = options?.uid || 0; + const index = options?.accountIndex || 0; + const payload = options?.payload || ''; + + result = await handleSignTx(appName, uid, index, payload); + } + + // a populated result indicates a successful execution. Set the transport response state for + // other components to respond to via useEffect. + if (result) { + setTransportResponse({ + ack: 'success', + options, + ...result, + }); + } + ledgerLoopInProgress.current = false; + } catch (err) { + handleErrors(appName, err); + } + }; + + // Gets an app address on device. + const handleGetAddress = async (appName: string, index: number) => { + const substrateApp = newSubstrateApp(ledgerTransport.current, appName); + const { deviceModel } = ledgerTransport.current; + const { id, productName } = deviceModel; + + setTransportResponse({ + ack: 'success', + statusCode: 'GettingAddress', + body: null, + }); + setFeedback(t('gettingAddress')); + + if (!ledgerTransport.current?.device?.opened) { + await ledgerTransport.current?.device?.open(); + } + const result: AnyJson = await withTimeout( + 3000, + substrateApp.getAddress( + LEDGER_DEFAULT_ACCOUNT + index, + LEDGER_DEFAULT_CHANGE, + LEDGER_DEFAULT_INDEX + 0, + false + ) + ); + + await ledgerTransport.current?.device?.close(); + + const error = result?.error_message; + if (error) { + if (!error.startsWith('No errors')) { + throw new Error(error); + } + } + + if (!(result instanceof Error)) { + setFeedback(t('successfullyFetchedAddress')); + + return { + statusCode: 'ReceivedAddress', + device: { id, productName }, + body: [result], + }; + } + return undefined; + }; + + // Signs a payload on device. + const handleSignTx = async ( + appName: string, + uid: number, + index: number, + payload: AnyJson + ) => { + const substrateApp = newSubstrateApp(ledgerTransport.current, appName); + const { deviceModel } = ledgerTransport.current; + const { id, productName } = deviceModel; + + setTransportResponse({ + ack: 'success', + statusCode: 'SigningPayload', + body: null, + }); + + setFeedback(t('approveTransactionLedger')); + + if (!ledgerTransport.current?.device?.opened) { + await ledgerTransport.current?.device?.open(); + } + const result = await substrateApp.sign( + LEDGER_DEFAULT_ACCOUNT + index, + LEDGER_DEFAULT_CHANGE, + LEDGER_DEFAULT_INDEX + 0, + u8aToBuffer(payload.toU8a(true)) + ); + + setFeedback(t('signedTransactionSuccessfully')); + await ledgerTransport.current?.device?.close(); + + const error = result?.error_message; + if (error) { + if (!error.startsWith('No errors')) { + throw new Error(error); + } + } + + if (!(result instanceof Error)) { + return { + statusCode: 'SignedPayload', + device: { id, productName }, + body: { + uid, + sig: result.signature, + }, + }; + } + return undefined; + }; + + // Handle an incoming new status code and persist to state. + const handleNewStatusCode = (ack: string, statusCode: LedgerStatusCode) => { + const newStatusCodes = [{ ack, statusCode }, ...statusCodes]; + + // Remove last status code if there are more than allowed number of status codes. + if (newStatusCodes.length > TOTAL_ALLOWED_STATUS_CODES) { + newStatusCodes.pop(); + } + setStateWithRef(newStatusCodes, setStatusCodes, statusCodesRef); + }; + + // Check if a Ledger address exists in imported addresses. + const ledgerAccountExists = (address: string) => + !!getLocalLedgerAccounts().find((a) => + isLocalNetworkAddress(network, a, address) + ); + + const addLedgerAccount = (address: string, index: number) => { + let newLedgerAccounts = getLocalLedgerAccounts(); + + const ledgerAddress = getLocalLedgerAddresses().find((a) => + isLocalNetworkAddress(network, a, address) + ); + + if ( + ledgerAddress && + !newLedgerAccounts.find((a) => isLocalNetworkAddress(network, a, address)) + ) { + const account = { + address, + network, + name: ledgerAddress.name, + source: 'ledger', + index, + }; + + // update the full list of local ledger accounts with new entry. + newLedgerAccounts = [...newLedgerAccounts].concat(account); + localStorage.setItem( + 'ledger_accounts', + JSON.stringify(newLedgerAccounts) + ); + + // store only those accounts on the current network in state. + setStateWithRef( + newLedgerAccounts.filter((a) => a.network === network), + setLedgerAccountsState, + ledgerAccountsRef + ); + + return account; + } + return null; + }; + + const removeLedgerAccount = (address: string) => { + let newLedgerAccounts = getLocalLedgerAccounts(); + + newLedgerAccounts = newLedgerAccounts.filter((a) => { + if (a.address !== address) { + return true; + } + if (a.network !== network) { + return true; + } + return false; + }); + if (!newLedgerAccounts.length) { + localStorage.removeItem('ledger_accounts'); + } else { + localStorage.setItem( + 'ledger_accounts', + JSON.stringify(newLedgerAccounts) + ); + } + setStateWithRef( + newLedgerAccounts.filter((a) => a.network === network), + setLedgerAccountsState, + ledgerAccountsRef + ); + }; + + // Gets an imported address along with its Ledger metadata. + const getLedgerAccount = (address: string) => { + const localLedgerAccounts = getLocalLedgerAccounts(); + + if (!localLedgerAccounts) { + return null; + } + return ( + localLedgerAccounts.find((a) => + isLocalNetworkAddress(network, a, address) + ) ?? null + ); + }; + + // Renames an imported ledger account. + const renameLedgerAccount = (address: string, newName: string) => { + let newLedgerAccounts = getLocalLedgerAccounts(); + + newLedgerAccounts = newLedgerAccounts.map((a) => + isLocalNetworkAddress(network, a, address) + ? { + ...a, + name: newName, + } + : a + ); + renameLocalLedgerAddress(address, newName); + localStorage.setItem('ledger_accounts', JSON.stringify(newLedgerAccounts)); + setStateWithRef( + newLedgerAccounts.filter((a) => a.network === network), + setLedgerAccountsState, + ledgerAccountsRef + ); + }; + + // Renames a record from local ledger addresses. + const renameLocalLedgerAddress = (address: string, name: string) => { + const localLedger = ( + localStorageOrDefault('ledger_addresses', [], true) as LedgerAddress[] + )?.map((i) => + !(i.address === address && i.network === network) + ? i + : { + ...i, + name, + } + ); + + if (localLedger) { + localStorage.setItem('ledger_addresses', JSON.stringify(localLedger)); + } + }; + + const getTransport = () => { + return ledgerTransport.current; + }; + + const getIsExecuting = () => { + return isExecutingRef.current; + }; + + const getStatusCodes = () => { + return statusCodesRef.current; + }; + + const getFeedback = () => { + return feedbackRef.current; + }; + + const setFeedback = (message: MaybeString, helpKey: MaybeString = null) => { + setStateWithRef({ message, helpKey }, setFeedbackState, feedbackRef); + }; + + const resetFeedback = () => { + setStateWithRef(defaultFeedback, setFeedbackState, feedbackRef); + }; + + const setIsPaired = (p: PairingStatus) => { + setStateWithRef(p, setIsPairedState, isPairedRef); + }; + + const setIsExecuting = (val: boolean) => { + setStateWithRef(val, setIsExecutingState, isExecutingRef); + }; + + const resetStatusCodes = () => { + setStateWithRef([], setStatusCodes, statusCodesRef); + }; + + const handleUnmount = () => { + // reset refs + ledgerLoopInProgress.current = false; + pairInProgress.current = false; + // reset state + resetStatusCodes(); + setIsExecuting(false); + resetFeedback(); + // close transport + if (getTransport()?.device?.opened) { + getTransport().device.close(); + } + }; + + return ( + <LedgerHardwareContext.Provider + value={{ + pairDevice, + transportResponse, + executeLedgerLoop, + setIsPaired, + setIsExecuting, + handleNewStatusCode, + resetStatusCodes, + getIsExecuting, + getStatusCodes, + getTransport, + ledgerAccountExists, + addLedgerAccount, + removeLedgerAccount, + renameLedgerAccount, + getLedgerAccount, + getFeedback, + setFeedback, + resetFeedback, + handleUnmount, + isPaired: isPairedRef.current, + ledgerAccounts: ledgerAccountsRef.current, + }} + > + {children} + </LedgerHardwareContext.Provider> + ); +}; + +export const LedgerHardwareContext = + React.createContext<LedgerHardwareContextInterface>( + defaultLedgerHardwareContext + ); + +export const useLedgerHardware = () => React.useContext(LedgerHardwareContext); diff --git a/src/contexts/Hardware/Utils.tsx b/src/contexts/Hardware/Utils.tsx new file mode 100644 index 0000000000..c78de28dfc --- /dev/null +++ b/src/contexts/Hardware/Utils.tsx @@ -0,0 +1,59 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { localStorageOrDefault } from '@polkadot-cloud/utils'; +import { LedgerApps } from 'config/ledger'; +import type { MaybeString } from 'types'; +import type { LedgerAccount, VaultAccount } from '@polkadot-cloud/react/types'; +import type { LedgerAddress } from './types'; + +// Gets ledger app from local storage, fallback to first entry. +export const getLedgerApp = (network: string) => { + return LedgerApps.find((a) => a.network === network) || LedgerApps[0]; +}; + +// Gets saved ledger addresses from local storage. +export const getLocalLedgerAddresses = (network?: string) => { + const localAddresses = localStorageOrDefault( + 'ledger_addresses', + [], + true + ) as LedgerAddress[]; + + return network + ? localAddresses.filter((a) => a.network === network) + : localAddresses; +}; + +// Gets imported Ledger accounts from local storage. +export const getLocalLedgerAccounts = (network?: string) => { + const localAddresses = localStorageOrDefault( + 'ledger_accounts', + [], + true + ) as LedgerAccount[]; + + return network + ? localAddresses.filter((a) => a.network === network) + : localAddresses; +}; + +// Gets imported Vault accounts from local storage. +export const getLocalVaultAccounts = (network?: string) => { + const localAddresses = localStorageOrDefault( + 'polkadot_vault_accounts', + [], + true + ) as VaultAccount[]; + + return network + ? localAddresses.filter((a) => a.network === network) + : localAddresses; +}; + +// Gets whether an address is a local network address. +export const isLocalNetworkAddress = ( + chain: string, + a: { address: MaybeString; network: string }, + address: string +) => a.address === address && a.network === chain; diff --git a/src/contexts/Hardware/Vault.tsx b/src/contexts/Hardware/Vault.tsx new file mode 100644 index 0000000000..35acc77792 --- /dev/null +++ b/src/contexts/Hardware/Vault.tsx @@ -0,0 +1,154 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ellipsisFn, setStateWithRef } from '@polkadot-cloud/utils'; +import React, { useEffect, useRef, useState } from 'react'; +import type { VaultAccount } from '@polkadot-cloud/react/types'; +import { useNetwork } from 'contexts/Network'; +import { getLocalVaultAccounts, isLocalNetworkAddress } from './Utils'; +import { defaultVaultHardwareContext } from './defaults'; +import type { VaultHardwareContextInterface } from './types'; + +export const VaultHardwareProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + + const [vaultAccounts, seVaultAccountsState] = useState<VaultAccount[]>( + getLocalVaultAccounts(network) + ); + const vaultAccountsRef = useRef(vaultAccounts); + + // Check if a Vault address exists in imported addresses. + const vaultAccountExists = (address: string) => + !!getLocalVaultAccounts().find((a) => + isLocalNetworkAddress(network, a, address) + ); + + // Adds a vault account to state and local storage. + const addVaultAccount = (address: string, index: number) => { + let newVaultAccounts = getLocalVaultAccounts(); + + if ( + !newVaultAccounts.find((a) => isLocalNetworkAddress(network, a, address)) + ) { + const account = { + address, + network, + name: ellipsisFn(address), + source: 'vault', + index, + }; + + newVaultAccounts = [...newVaultAccounts].concat(account); + localStorage.setItem( + 'polkadot_vault_accounts', + JSON.stringify(newVaultAccounts) + ); + + // store only those accounts on the current network in state. + setStateWithRef( + newVaultAccounts.filter((a) => a.network === network), + seVaultAccountsState, + vaultAccountsRef + ); + return account; + } + return null; + }; + + const removeVaultAccount = (address: string) => { + let newVaultAccounts = getLocalVaultAccounts(); + + newVaultAccounts = newVaultAccounts.filter((a) => { + if (a.address !== address) { + return true; + } + if (a.network !== network) { + return true; + } + return false; + }); + + if (!newVaultAccounts.length) { + localStorage.removeItem('polkadot_vault_accounts'); + } else { + localStorage.setItem( + 'polkadot_vault_accounts', + JSON.stringify(newVaultAccounts) + ); + } + setStateWithRef( + newVaultAccounts.filter((a) => a.network === network), + seVaultAccountsState, + vaultAccountsRef + ); + }; + + const getVaultAccount = (address: string) => { + const localVaultAccounts = getLocalVaultAccounts(); + if (!localVaultAccounts) { + return null; + } + return ( + localVaultAccounts.find((a) => + isLocalNetworkAddress(network, a, address) + ) ?? null + ); + }; + + const renameVaultAccount = (address: string, newName: string) => { + let newVaultAccounts = getLocalVaultAccounts(); + + newVaultAccounts = newVaultAccounts.map((a) => + isLocalNetworkAddress(network, a, address) + ? { + ...a, + name: newName, + } + : a + ); + localStorage.setItem( + 'polkadot_vault_accounts', + JSON.stringify(newVaultAccounts) + ); + setStateWithRef( + newVaultAccounts.filter((a) => a.network === network), + seVaultAccountsState, + vaultAccountsRef + ); + }; + + // Refresh imported vault accounts on network change. + useEffect(() => { + setStateWithRef( + getLocalVaultAccounts(network), + seVaultAccountsState, + vaultAccountsRef + ); + }, [network]); + + return ( + <VaultHardwareContext.Provider + value={{ + vaultAccountExists, + addVaultAccount, + removeVaultAccount, + renameVaultAccount, + getVaultAccount, + vaultAccounts: vaultAccountsRef.current, + }} + > + {children} + </VaultHardwareContext.Provider> + ); +}; + +export const VaultHardwareContext = + React.createContext<VaultHardwareContextInterface>( + defaultVaultHardwareContext + ); + +export const useVaultHardware = () => React.useContext(VaultHardwareContext); diff --git a/src/contexts/Hardware/defaults.ts b/src/contexts/Hardware/defaults.ts new file mode 100644 index 0000000000..af4bab8bef --- /dev/null +++ b/src/contexts/Hardware/defaults.ts @@ -0,0 +1,51 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { + LedgerHardwareContextInterface, + VaultHardwareContextInterface, +} from './types'; + +export const TOTAL_ALLOWED_STATUS_CODES = 50; +export const LEDGER_DEFAULT_ACCOUNT = 0x80000000; +export const LEDGER_DEFAULT_CHANGE = 0x80000000; +export const LEDGER_DEFAULT_INDEX = 0x80000000; + +export const defaultFeedback = { + message: null, + helpKey: null, +}; + +export const defaultLedgerHardwareContext: LedgerHardwareContextInterface = { + transportResponse: null, + pairDevice: async () => new Promise((resolve) => resolve(false)), + executeLedgerLoop: async (a, s, o) => new Promise((resolve) => resolve()), + setIsPaired: (v) => {}, + handleNewStatusCode: (a, s) => {}, + setIsExecuting: (b) => {}, + resetStatusCodes: () => {}, + getIsExecuting: () => false, + getStatusCodes: () => [], + getTransport: () => null, + ledgerAccountExists: (a) => false, + addLedgerAccount: (a, i) => null, + removeLedgerAccount: (a) => {}, + renameLedgerAccount: (a, n) => {}, + getLedgerAccount: (a) => null, + isPaired: 'unknown', + ledgerAccounts: [], + getFeedback: () => defaultFeedback, + setFeedback: (s, h) => {}, + resetFeedback: () => {}, + handleUnmount: () => {}, +}; + +export const defaultVaultHardwareContext: VaultHardwareContextInterface = { + vaultAccountExists: (a) => false, + addVaultAccount: (a, i) => null, + removeVaultAccount: (a) => {}, + renameVaultAccount: (a, n) => {}, + getVaultAccount: (a) => null, + vaultAccounts: [], +}; diff --git a/src/contexts/Hardware/types.ts b/src/contexts/Hardware/types.ts new file mode 100644 index 0000000000..0785085953 --- /dev/null +++ b/src/contexts/Hardware/types.ts @@ -0,0 +1,87 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { LedgerAccount, VaultAccount } from '@polkadot-cloud/react/types'; +import type { FunctionComponent, SVGProps } from 'react'; +import type { AnyJson, MaybeString, NetworkName } from 'types'; + +export type LedgerHardwareContextInterface = { + pairDevice: () => Promise<boolean>; + setIsPaired: (v: PairingStatus) => void; + transportResponse: AnyJson; + executeLedgerLoop: ( + appName: string, + tasks: LedgerTask[], + options?: AnyJson + ) => Promise<void>; + handleNewStatusCode: (ack: string, statusCode: LedgerStatusCode) => void; + setIsExecuting: (v: boolean) => void; + resetStatusCodes: () => void; + getIsExecuting: () => boolean; + getStatusCodes: () => LedgerResponse[]; + getTransport: () => AnyJson; + ledgerAccountExists: (a: string) => boolean; + addLedgerAccount: (a: string, i: number) => LedgerAccount | null; + removeLedgerAccount: (a: string) => void; + renameLedgerAccount: (a: string, name: string) => void; + getLedgerAccount: (a: string) => LedgerAccount | null; + isPaired: PairingStatus; + ledgerAccounts: LedgerAccount[]; + getFeedback: () => FeedbackMessage; + setFeedback: (s: MaybeString, helpKey?: MaybeString) => void; + resetFeedback: () => void; + handleUnmount: () => void; +}; + +export interface FeedbackMessage { + message: MaybeString; + helpKey?: MaybeString; +} + +export type LedgerStatusCode = + | 'GettingAddress' + | 'ReceivedAddress' + | 'SigningPayload' + | 'SignedPayload' + | 'DeviceTimeout' + | 'NestingNotSupported' + | 'WrongTransaction' + | 'DeviceNotConnected' + | 'DeviceLocked' + | 'TransactionRejected' + | 'AppNotOpenContinue' + | 'AppNotOpen'; + +export interface LedgerResponse { + ack: string; + statusCode: LedgerStatusCode; + body?: AnyJson; + options?: AnyJson; +} + +export type LedgerTask = 'get_address' | 'sign_tx'; + +export type PairingStatus = 'paired' | 'unpaired' | 'unknown'; + +export interface LedgerAddress { + address: string; + index: number; + name: string; + network: NetworkName; + pubKey: string; +} + +export type LedgerApp = { + network: NetworkName; + appName: string; + Icon: FunctionComponent<SVGProps<SVGSVGElement>>; +}; + +export type VaultHardwareContextInterface = { + vaultAccountExists: (a: string) => boolean; + addVaultAccount: (a: string, i: number) => LedgerAccount | null; + removeVaultAccount: (a: string) => void; + renameVaultAccount: (a: string, name: string) => void; + getVaultAccount: (a: string) => LedgerAccount | null; + vaultAccounts: VaultAccount[]; +}; diff --git a/src/contexts/Help/defaults.ts b/src/contexts/Help/defaults.ts new file mode 100644 index 0000000000..4c2411813d --- /dev/null +++ b/src/contexts/Help/defaults.ts @@ -0,0 +1,14 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { HelpContextInterface } from './types'; + +export const defaultHelpContext: HelpContextInterface = { + openHelp: (key) => {}, + closeHelp: () => {}, + setStatus: (status) => {}, + setDefinition: (definition) => {}, + status: 'closed', + definition: null, +}; diff --git a/src/contexts/Help/index.tsx b/src/contexts/Help/index.tsx new file mode 100644 index 0000000000..270a1efb92 --- /dev/null +++ b/src/contexts/Help/index.tsx @@ -0,0 +1,81 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useState } from 'react'; +import type { MaybeString } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import * as defaults from './defaults'; +import type { + HelpContextInterface, + HelpContextProps, + HelpContextState, + HelpStatus, +} from './types'; + +export const HelpProvider = ({ children }: HelpContextProps) => { + // help module state + const [state, setState] = useState<HelpContextState>({ + status: 'closed', + definition: null, + }); + + // when fade out completes, reset active definiton + useEffectIgnoreInitial(() => { + if (state.status === 'closed') { + setState({ + ...state, + definition: null, + }); + } + }, [state.status]); + + const setDefinition = (definition: MaybeString) => { + setState({ + ...state, + definition, + }); + }; + + const setStatus = (newStatus: HelpStatus) => { + setState({ + ...state, + status: newStatus, + }); + }; + + const openHelp = (definition: MaybeString) => { + setState({ + ...state, + definition, + status: 'open', + }); + }; + + const closeHelp = () => { + setState({ + ...state, + status: 'closing', + }); + }; + + return ( + <HelpContext.Provider + value={{ + openHelp, + closeHelp, + setStatus, + setDefinition, + status: state.status, + definition: state.definition, + }} + > + {children} + </HelpContext.Provider> + ); +}; + +export const HelpContext = React.createContext<HelpContextInterface>( + defaults.defaultHelpContext +); + +export const useHelp = () => React.useContext(HelpContext); diff --git a/src/contexts/Help/types.ts b/src/contexts/Help/types.ts new file mode 100644 index 0000000000..d51c07ee21 --- /dev/null +++ b/src/contexts/Help/types.ts @@ -0,0 +1,49 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { ReactNode } from 'react'; +import type { MaybeString } from 'types'; + +export type HelpItems = HelpItem[]; + +export interface HelpItem { + key?: string; + definitions?: string[]; + external?: ExternalItems; +} + +export type ExternalItems = ExternalItem[]; +export type ExternalItem = [string, string, string]; + +export type DefinitionWithKeys = { + title: string; + description: string[]; +}; + +export interface ExternalWithKeys { + title: string; + url: string; + website?: string; +} + +export type HelpStatus = 'closed' | 'open' | 'closing'; + +export interface HelpContextInterface { + openHelp: (d: MaybeString) => void; + closeHelp: () => void; + setStatus: (s: HelpStatus) => void; + setDefinition: (d: MaybeString) => void; + status: HelpStatus; + definition: MaybeString; +} + +export interface HelpContextState { + status: HelpStatus; + definition: MaybeString; +} + +export interface HelpContextProps { + children: ReactNode; +} + +export type HelpConfig = Record<string, string | any>; diff --git a/src/contexts/Identities/defaults.ts b/src/contexts/Identities/defaults.ts new file mode 100644 index 0000000000..6a1ed169a3 --- /dev/null +++ b/src/contexts/Identities/defaults.ts @@ -0,0 +1,10 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { IdentitiesContextInterface } from './types'; + +export const defaultIdentitiesContext: IdentitiesContextInterface = { + fetchIdentitiesMetaBatch: (k, v, r) => {}, + meta: {}, +}; diff --git a/src/contexts/Identities/index.tsx b/src/contexts/Identities/index.tsx new file mode 100644 index 0000000000..856c12707a --- /dev/null +++ b/src/contexts/Identities/index.tsx @@ -0,0 +1,198 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import React, { useEffect, useRef, useState } from 'react'; +import type { AnyApi, AnyMetaBatch } from 'types'; +import { useApi } from '../Api'; +import { defaultIdentitiesContext } from './defaults'; +import type { IdentitiesContextInterface } from './types'; + +export const IdentitiesProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { isReady, api } = useApi(); + + // stores the meta data batches for validator lists + const [identitiesMetaBatches, setIdentitiesMetaBatch] = + useState<AnyMetaBatch>({}); + const identitiesMetaBatchesRef = useRef(identitiesMetaBatches); + + // stores the meta batch subscriptions for validator lists + const identitiesSubsRef = useRef<AnyApi>({}); + + // unsubscribe from any validator meta batches + useEffect( + () => () => { + Object.values(identitiesSubsRef.current).map((batch: AnyMetaBatch) => + Object.entries(batch).map(([, v]: AnyApi) => v()) + ); + }, + [] + ); + + /* + Fetches a new batch of subscribed accounts metadata. Stores the returning + metadata alongside the unsubscribe function in state. + structure: + { + key: { + [ + { + addresses [], + identities: [], + } + ] + }, + }; + */ + const fetchIdentitiesMetaBatch = async ( + key: string, + addresses: string[], + refetch = false + ) => { + if (!isReady || !api) { + return; + } + + if (!addresses.length) { + return; + } + + if (!refetch) { + // if already exists, do not re-fetch + if (identitiesMetaBatchesRef.current[key] !== undefined) { + return; + } + } else { + // tidy up if existing batch exists + const updatedMetaBatches: AnyMetaBatch = { + ...identitiesMetaBatchesRef.current, + }; + delete updatedMetaBatches[key]; + setStateWithRef( + updatedMetaBatches, + setIdentitiesMetaBatch, + identitiesMetaBatchesRef + ); + if (identitiesSubsRef.current[key] !== undefined) { + for (const unsub of identitiesSubsRef.current[key]) { + unsub(); + } + } + } + + // store batch addresses + const batchesUpdated = Object.assign(identitiesMetaBatchesRef.current); + batchesUpdated[key] = {}; + batchesUpdated[key].addresses = addresses; + setStateWithRef( + { ...batchesUpdated }, + setIdentitiesMetaBatch, + identitiesMetaBatchesRef + ); + + const subscribeToIdentities = async (addr: string[]) => { + const unsub = await api.query.identity.identityOf.multi<AnyApi>( + addr, + (_identities) => { + const identities = []; + for (let i = 0; i < _identities.length; i++) { + identities.push(_identities[i].toHuman()); + } + const updated = Object.assign(identitiesMetaBatchesRef.current); + updated[key].identities = identities; + setStateWithRef( + { ...updated }, + setIdentitiesMetaBatch, + identitiesMetaBatchesRef + ); + } + ); + return unsub; + }; + + const subscribeToSuperIdentities = async (addr: string[]) => { + const unsub = await api.query.identity.superOf.multi<AnyApi>( + addr, + async (result) => { + // determine where supers exist + const supers: AnyApi = []; + const supersWithIdentity: AnyApi = []; + + for (let i = 0; i < result.length; i++) { + const item = result[i].toHuman(); + supers.push(item); + if (item !== null) { + supersWithIdentity.push(i); + } + } + + // get supers one-off multi query + const query = supers + .filter((s: AnyApi) => s !== null) + .map((s: AnyApi) => s[0]); + + ( + await api.query.identity.identityOf.multi<AnyApi>( + query, + (_identities) => { + for (let j = 0; j < _identities.length; j++) { + const identity = _identities[j].toHuman(); + // inject identity into super array + supers[supersWithIdentity[j]].identity = identity; + } + } + ) + )(); + + const updated = Object.assign(identitiesMetaBatchesRef.current); + updated[key].supers = supers; + setStateWithRef( + { ...updated }, + setIdentitiesMetaBatch, + identitiesMetaBatchesRef + ); + } + ); + return unsub; + }; + + await Promise.all([ + subscribeToIdentities(addresses), + subscribeToSuperIdentities(addresses), + ]).then((unsubs: AnyApi) => { + addMetaBatchUnsubs(key, unsubs); + }); + }; + + /* + * Helper function to add mataBatch unsubs by key. + */ + const addMetaBatchUnsubs = (key: string, unsubs: AnyApi) => { + const identityUnsubs = identitiesSubsRef.current; + const keyUnsubs = identityUnsubs[key] ?? []; + + keyUnsubs.push(...unsubs); + identityUnsubs[key] = keyUnsubs; + identitiesSubsRef.current = identityUnsubs; + }; + + return ( + <IdentitiesContext.Provider + value={{ + fetchIdentitiesMetaBatch, + meta: identitiesMetaBatchesRef.current, + }} + > + {children} + </IdentitiesContext.Provider> + ); +}; + +export const IdentitiesContext = + React.createContext<IdentitiesContextInterface>(defaultIdentitiesContext); + +export const useIdentities = () => React.useContext(IdentitiesContext); diff --git a/src/contexts/Identities/types.ts b/src/contexts/Identities/types.ts new file mode 100644 index 0000000000..4af71a55d9 --- /dev/null +++ b/src/contexts/Identities/types.ts @@ -0,0 +1,9 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyMetaBatch } from 'types'; + +export interface IdentitiesContextInterface { + fetchIdentitiesMetaBatch: (k: string, v: string[], r?: boolean) => void; + meta: AnyMetaBatch; +} diff --git a/src/contexts/Menu/defaults.ts b/src/contexts/Menu/defaults.ts new file mode 100644 index 0000000000..a0b4c25659 --- /dev/null +++ b/src/contexts/Menu/defaults.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { MenuContextInterface } from './types'; + +export const defaultMenuContext: MenuContextInterface = { + openMenu: () => {}, + closeMenu: () => {}, + setMenuPosition: (r) => {}, + checkMenuPosition: (r) => {}, + setMenuItems: (items) => {}, + open: 0, + show: 0, + position: [0, 0], + items: [], +}; diff --git a/src/contexts/Menu/index.tsx b/src/contexts/Menu/index.tsx new file mode 100644 index 0000000000..6c93cd6417 --- /dev/null +++ b/src/contexts/Menu/index.tsx @@ -0,0 +1,92 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { RefObject } from 'react'; +import React, { useState } from 'react'; +import { defaultMenuContext } from './defaults'; +import type { MenuContextInterface } from './types'; + +export const MenuProvider = ({ children }: { children: React.ReactNode }) => { + const [open, setOpen] = useState(0); + const [show, setShow] = useState(0); + const [items, setItems] = useState<React.ReactNode[]>([]); + + const [position, setPosition] = useState<[number, number]>([0, 0]); + + const openMenu = () => { + if (open) return; + setOpen(1); + }; + + const closeMenu = () => { + setShow(0); + setTimeout(() => { + setOpen(0); + }, 100); + }; + + const setMenuPosition = (ref: RefObject<HTMLDivElement>) => { + if (open || !ref?.current) return; + const bodyRect = document.body.getBoundingClientRect(); + const elemRect = ref.current.getBoundingClientRect(); + + const x = elemRect.left - bodyRect.left; + const y = elemRect.top - bodyRect.top; + + setPosition([x, y]); + openMenu(); + }; + + const checkMenuPosition = (ref: RefObject<HTMLDivElement>) => { + if (!ref?.current) return; + + const bodyRect = document.body.getBoundingClientRect(); + const menuRect = ref.current.getBoundingClientRect(); + + let x = menuRect.left - bodyRect.left; + let y = menuRect.top - bodyRect.top; + const right = menuRect.right; + const bottom = menuRect.bottom; + + // small offset from menu start + y -= 10; + + const documentPadding = 20; + + if (right > bodyRect.right) { + x = bodyRect.right - ref.current.offsetWidth - documentPadding; + } + if (bottom > bodyRect.bottom) { + y = bodyRect.bottom - ref.current.offsetHeight - documentPadding; + } + setPosition([x, y]); + setShow(1); + }; + + const setMenuItems = (_items: React.ReactNode[]) => { + setItems(_items); + }; + + return ( + <MenuContext.Provider + value={{ + openMenu, + closeMenu, + setMenuPosition, + checkMenuPosition, + setMenuItems, + open, + show, + position, + items, + }} + > + {children} + </MenuContext.Provider> + ); +}; + +export const MenuContext = + React.createContext<MenuContextInterface>(defaultMenuContext); + +export const useMenu = () => React.useContext(MenuContext); diff --git a/src/contexts/Menu/types.ts b/src/contexts/Menu/types.ts new file mode 100644 index 0000000000..229e917b20 --- /dev/null +++ b/src/contexts/Menu/types.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type React from 'react'; +import type { RefObject } from 'react'; + +export interface MenuContextInterface { + openMenu: () => void; + closeMenu: () => void; + setMenuPosition: (ref: RefObject<HTMLDivElement>) => void; + checkMenuPosition: (ref: RefObject<HTMLDivElement>) => void; + setMenuItems: (items: React.ReactNode[]) => void; + open: number; + show: number; + position: [number, number]; + items: React.ReactNode[]; +} diff --git a/src/contexts/Migrate/index.tsx b/src/contexts/Migrate/index.tsx new file mode 100644 index 0000000000..4f5713ff4e --- /dev/null +++ b/src/contexts/Migrate/index.tsx @@ -0,0 +1,81 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useState } from 'react'; +import { NetworkList } from 'config/networks'; +import { AppVersion } from 'consts'; +import { useApi } from 'contexts/Api'; +import { useUi } from 'contexts/UI'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const MigrateProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { isReady } = useApi(); + const { isNetworkSyncing } = useUi(); + const { accounts } = useImportedAccounts(); + + // The local app version of the current user. + const localAppVersion = localStorage.getItem('app_version'); + + // Store whether the migration check has taken place. + const [done, setDone] = useState<boolean>(localAppVersion === AppVersion); + + // Removes the previous nominator setup objects from local storage. + const removeDeprecatedNominatorSetups = () => + Object.values(NetworkList).forEach((n: any) => { + for (const a of accounts) + localStorage.removeItem(`${n.name}_stake_setup_${a.address}`); + }); + + // Removes the previous pool setup objects from local storage. + const removeDeprecatedPoolSetups = () => + Object.values(NetworkList).forEach((n: any) => { + for (const a of accounts) + localStorage.removeItem(`${n.name}_pool_setup_${a.address}`); + }); + + // Removes the previous active proxies from local storage. + const removeDeprecatedActiveProxies = () => + Object.values(NetworkList).forEach((n: any) => { + localStorage.removeItem(`${n.name}_active_proxy`); + }); + + useEffectIgnoreInitial(() => { + if (isReady && !isNetworkSyncing && !done) { + // Carry out migrations if local version is different to current version. + if (localAppVersion !== AppVersion) { + // Added in 1.0.2. + // + // Remove local language resources. No expiry. + localStorage.removeItem('lng_resources'); + + // Added in 1.0.4. + // + // Remove legacy local nominator setup and pool setup items. + removeDeprecatedNominatorSetups(); + removeDeprecatedPoolSetups(); + + // Added in 1.0.8. + // + // Remove legacy local active proxy records. + removeDeprecatedActiveProxies(); + + // Finally, + // + // Update local version to current app version. + localStorage.setItem('app_version', AppVersion); + setDone(true); + } + } + }, [isNetworkSyncing]); + + return ( + <MigrateContext.Provider value={{}}>{children}</MigrateContext.Provider> + ); +}; + +export const MigrateContext = React.createContext<any>(null); diff --git a/src/contexts/Network/defaults.ts b/src/contexts/Network/defaults.ts new file mode 100644 index 0000000000..3066d85c43 --- /dev/null +++ b/src/contexts/Network/defaults.ts @@ -0,0 +1,10 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { NetworkList } from 'config/networks'; + +export const defaultNetworkContext = { + network: NetworkList.polkadot.name, + networkData: NetworkList.polkadot, + switchNetwork: () => {}, +}; diff --git a/src/contexts/Network/index.tsx b/src/contexts/Network/index.tsx new file mode 100644 index 0000000000..c41711a351 --- /dev/null +++ b/src/contexts/Network/index.tsx @@ -0,0 +1,79 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { extractUrlValue, varToUrlHash } from '@polkadot-cloud/utils'; +import React, { createContext, useContext, useState } from 'react'; +import { NetworkList } from 'config/networks'; +import { DefaultNetwork } from 'consts'; +import type { NetworkName } from 'types'; +import type { NetworkState } from 'contexts/Api/types'; +import type { NetworkContextInterface } from './types'; +import { defaultNetworkContext } from './defaults'; + +export const NetworkProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + // Get the initial network and prepare meta tags if necessary. + const getInitialNetwork = () => { + const urlNetworkRaw = extractUrlValue('n'); + + const urlNetworkValid = !!Object.values(NetworkList).find( + (n) => n.name === urlNetworkRaw + ); + + // use network from url if valid. + if (urlNetworkValid) { + const urlNetwork = urlNetworkRaw as NetworkName; + + if (urlNetworkValid) { + return urlNetwork; + } + } + // fallback to localStorage network if there. + const localNetwork: NetworkName = localStorage.getItem( + 'network' + ) as NetworkName; + const localNetworkValid = !!Object.values(NetworkList).find( + (n) => n.name === localNetwork + ); + return localNetworkValid ? localNetwork : DefaultNetwork; + }; + + // handle network switching + const switchNetwork = (name: NetworkName) => { + setNetwork({ + name, + meta: NetworkList[name], + }); + + // update url `n` if needed. + varToUrlHash('n', name, false); + }; + + // Store the initial active network. + const initialNetwork = getInitialNetwork(); + const [network, setNetwork] = useState<NetworkState>({ + name: initialNetwork, + meta: NetworkList[initialNetwork], + }); + + return ( + <NetworkContext.Provider + value={{ + network: network.name, + networkData: network.meta, + switchNetwork, + }} + > + {children} + </NetworkContext.Provider> + ); +}; + +export const NetworkContext = createContext<NetworkContextInterface>( + defaultNetworkContext +); + +export const useNetwork = () => useContext(NetworkContext); diff --git a/src/contexts/Network/types.ts b/src/contexts/Network/types.ts new file mode 100644 index 0000000000..1589c7cab1 --- /dev/null +++ b/src/contexts/Network/types.ts @@ -0,0 +1,10 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { Network, NetworkName } from 'types'; + +export interface NetworkContextInterface { + network: NetworkName; + networkData: Network; + switchNetwork: (network: NetworkName) => void; +} diff --git a/src/contexts/NetworkMetrics/defaults.ts b/src/contexts/NetworkMetrics/defaults.ts new file mode 100644 index 0000000000..02af14f760 --- /dev/null +++ b/src/contexts/NetworkMetrics/defaults.ts @@ -0,0 +1,26 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import type { + ActiveEra, + NetworkMetrics, + NetworkMetricsContextInterface, +} from './types'; + +export const activeEra: ActiveEra = { + index: new BigNumber(0), + start: new BigNumber(0), +}; +export const metrics: NetworkMetrics = { + totalIssuance: new BigNumber(0), + auctionCounter: new BigNumber(0), + earliestStoredSession: new BigNumber(0), + fastUnstakeErasToCheckPerBlock: 0, + minimumActiveStake: new BigNumber(0), +}; + +export const defaultNetworkContext: NetworkMetricsContextInterface = { + activeEra, + metrics, +}; diff --git a/src/contexts/NetworkMetrics/index.tsx b/src/contexts/NetworkMetrics/index.tsx new file mode 100644 index 0000000000..725c3f24a0 --- /dev/null +++ b/src/contexts/NetworkMetrics/index.tsx @@ -0,0 +1,156 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useRef, useState } from 'react'; +import type { AnyApi, AnyJson } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useApi } from '../Api'; +import * as defaults from './defaults'; +import type { + ActiveEra, + NetworkMetrics, + NetworkMetricsContextInterface, +} from './types'; + +export const NetworkMetricsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + const { isReady, api } = useApi(); + + // Store active era in state. + const [activeEra, setActiveEra] = useState<ActiveEra>(defaults.activeEra); + const activeEraRef = useRef(activeEra); + + // Store network metrics in state. + const [metrics, setMetrics] = useState<NetworkMetrics>(defaults.metrics); + const metricsRef = useRef(metrics); + + // Store unsubscribe objects. + const unsubsRef = useRef<AnyApi[]>([]); + + // active subscription + const initialiseSubscriptions = async () => { + if (!api) return; + + if (isReady) { + const subscribeToMetrics = async () => { + const unsub = await api.queryMulti( + [ + api.query.balances.totalIssuance, + api.query.auctions.auctionCounter, + api.query.paraSessionInfo.earliestStoredSession, + api.query.fastUnstake.erasToCheckPerBlock, + api.query.staking.minimumActiveStake, + ], + ([ + totalIssuance, + auctionCounter, + earliestStoredSession, + erasToCheckPerBlock, + minimumActiveStake, + ]: AnyApi) => { + setStateWithRef( + { + totalIssuance: new BigNumber(totalIssuance.toString()), + auctionCounter: new BigNumber(auctionCounter.toString()), + earliestStoredSession: new BigNumber( + earliestStoredSession.toString() + ), + fastUnstakeErasToCheckPerBlock: erasToCheckPerBlock.toNumber(), + minimumActiveStake: new BigNumber( + minimumActiveStake.toString() + ), + }, + setMetrics, + metricsRef + ); + } + ); + return unsub; + }; + + const subscribeToActiveEra = async () => { + const unsub = await api.query.staking.activeEra((result: AnyApi) => { + // determine activeEra: toString used as alternative to `toHuman`, that puts commas in + // numbers + let newActiveEra = result + .unwrapOrDefault({ + index: 0, + start: 0, + }) + .toString(); + + newActiveEra = JSON.parse(newActiveEra); + setStateWithRef( + { + index: new BigNumber(newActiveEra.index), + start: new BigNumber(newActiveEra.start), + }, + setActiveEra, + activeEraRef + ); + }); + return unsub; + }; + + // initiate subscription, add to unsubs. + await Promise.all([subscribeToMetrics(), subscribeToActiveEra()]).then( + (u: any) => { + unsubsRef.current = unsubsRef.current.concat(u); + } + ); + } + }; + + // Unsubscribe from unsubs + const unsubscribe = () => { + Object.values(unsubsRef.current).forEach((unsub: AnyJson) => { + unsub(); + }); + }; + + // Set defaults for all metrics. + const handleResetMetrics = () => { + unsubscribe(); + unsubsRef.current = []; + setStateWithRef(defaults.activeEra, setActiveEra, activeEraRef); + setStateWithRef(defaults.metrics, setMetrics, metricsRef); + }; + + // manage unsubscribe + useEffectIgnoreInitial(() => { + initialiseSubscriptions(); + return () => { + unsubscribe(); + }; + }, [isReady]); + + // Reset active era and metrics on network change. + useEffectIgnoreInitial(() => { + handleResetMetrics(); + }, [network]); + + return ( + <NetworkMetricsContext.Provider + value={{ + activeEra: activeEraRef.current, + metrics: metricsRef.current, + }} + > + {children} + </NetworkMetricsContext.Provider> + ); +}; + +export const NetworkMetricsContext = + React.createContext<NetworkMetricsContextInterface>( + defaults.defaultNetworkContext + ); + +export const useNetworkMetrics = () => React.useContext(NetworkMetricsContext); diff --git a/src/contexts/NetworkMetrics/types.ts b/src/contexts/NetworkMetrics/types.ts new file mode 100644 index 0000000000..b3c5dd7e4b --- /dev/null +++ b/src/contexts/NetworkMetrics/types.ts @@ -0,0 +1,22 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; + +export interface NetworkMetricsContextInterface { + activeEra: ActiveEra; + metrics: NetworkMetrics; +} + +export interface NetworkMetrics { + totalIssuance: BigNumber; + auctionCounter: BigNumber; + earliestStoredSession: BigNumber; + fastUnstakeErasToCheckPerBlock: number; + minimumActiveStake: BigNumber; +} + +export interface ActiveEra { + index: BigNumber; + start: BigNumber; +} diff --git a/src/contexts/Notifications/defaults.ts b/src/contexts/Notifications/defaults.ts new file mode 100644 index 0000000000..4493a040f6 --- /dev/null +++ b/src/contexts/Notifications/defaults.ts @@ -0,0 +1,11 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { NotificationsContextInterface } from './types'; + +export const defaultNotificationsContext: NotificationsContextInterface = { + addNotification: (n) => {}, + removeNotification: (n) => {}, + notifications: [], +}; diff --git a/src/contexts/Notifications/index.tsx b/src/contexts/Notifications/index.tsx new file mode 100644 index 0000000000..297c6c9d5f --- /dev/null +++ b/src/contexts/Notifications/index.tsx @@ -0,0 +1,81 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import type { ReactNode } from 'react'; +import React, { useRef, useState } from 'react'; +import { defaultNotificationsContext } from './defaults'; +import type { + NotificationInterface, + NotificationItem, + NotificationsContextInterface, +} from './types'; + +export const NotificationsProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const [index, setIndexState] = useState<number>(0); + const [notifications, setNotifications] = useState<NotificationInterface[]>( + [] + ); + + const indexRef = useRef(index); + const notificationsRef = useRef(notifications); + + const setIndex = (i: number) => { + indexRef.current = i; + setIndexState(i); + }; + + const addNotification = (newNotification: NotificationItem) => { + const newNotifications: NotificationInterface[] = [ + ...notificationsRef.current, + ]; + + const newIndex: number = indexRef.current + 1; + + newNotifications.push({ + index: newIndex, + item: { + ...newNotification, + index: newIndex, + }, + }); + + setIndex(newIndex); + setStateWithRef(newNotifications, setNotifications, notificationsRef); + setTimeout(() => { + removeNotification(newIndex); + }, 3000); + + return newIndex; + }; + + const removeNotification = (i: number) => { + const newNotifications = notificationsRef.current.filter( + (item: NotificationInterface) => item.index !== i + ); + setStateWithRef(newNotifications, setNotifications, notificationsRef); + }; + + return ( + <NotificationsContext.Provider + value={{ + addNotification, + removeNotification, + notifications: notificationsRef.current, + }} + > + {children} + </NotificationsContext.Provider> + ); +}; + +export const NotificationsContext = + React.createContext<NotificationsContextInterface>( + defaultNotificationsContext + ); + +export const useNotifications = () => React.useContext(NotificationsContext); diff --git a/src/contexts/Notifications/types.ts b/src/contexts/Notifications/types.ts new file mode 100644 index 0000000000..8936af26cc --- /dev/null +++ b/src/contexts/Notifications/types.ts @@ -0,0 +1,22 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface NotificationsContextInterface { + addNotification: (n: NotificationItem) => void; + removeNotification: (i: number) => void; + notifications: NotificationInterface[]; +} + +export interface NotificationInterface { + index: number; + item: NotificationItem; +} + +export interface NotificationItem extends NotificationText { + index?: number; +} + +export interface NotificationText { + title: string; + subtitle: string; +} diff --git a/src/contexts/Payouts/Utils.ts b/src/contexts/Payouts/Utils.ts new file mode 100644 index 0000000000..28bb1c53d4 --- /dev/null +++ b/src/contexts/Payouts/Utils.ts @@ -0,0 +1,98 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import type { AnyJson, NetworkName } from 'types'; +import type { LocalValidatorExposure } from './types'; + +// Check if local exposure entry exists for an era. +export const hasLocalEraExposure = ( + network: NetworkName, + era: string, + who: string +) => { + const current = JSON.parse( + localStorage.getItem(`${network}_era_exposures`) || '{}' + ); + return !!current?.[who]?.[era]; +}; + +// Get local exposure entry for an era. +export const getLocalEraExposure = ( + network: NetworkName, + era: string, + who: string +) => { + const current = JSON.parse( + localStorage.getItem(`${network}_era_exposures`) || '{}' + ); + return current?.[who]?.[era] || []; +}; + +// Set local exposure entry for an era. +export const setLocalEraExposure = ( + network: NetworkName, + era: string, + who: string, + exposedValidators: Record<string, LocalValidatorExposure> | null, + endEra: string +) => { + const current = JSON.parse( + localStorage.getItem(`${network}_era_exposures`) || '{}' + ); + + const whoRemoveStaleEras = Object.fromEntries( + Object.entries(current[who] || {}).filter(([k]: AnyJson) => + new BigNumber(k).isGreaterThanOrEqualTo(endEra) + ) + ); + + localStorage.setItem( + `${network}_era_exposures`, + JSON.stringify({ + ...current, + [who]: { + ...whoRemoveStaleEras, + [era]: exposedValidators, + }, + }) + ); +}; + +// Get unclaimed payouts for an account. +export const getLocalUnclaimedPayouts = (network: NetworkName, who: string) => { + const current = JSON.parse( + localStorage.getItem(`${network}_unclaimed_payouts`) || '{}' + ); + return current?.[who] || {}; +}; + +// Set local unclaimed payouts for an account. +export const setLocalUnclaimedPayouts = ( + network: NetworkName, + era: string, + who: string, + unclaimdPayouts: Record<string, string>, + endEra: string +) => { + const current = JSON.parse( + localStorage.getItem(`${network}_unclaimed_payouts`) || '{}' + ); + + const whoRemoveStaleEras = Object.fromEntries( + Object.entries(current[who] || {}).filter(([k]: AnyJson) => + new BigNumber(k).isGreaterThanOrEqualTo(endEra) + ) + ); + + localStorage.setItem( + `${network}_unclaimed_payouts`, + JSON.stringify({ + ...current, + [who]: { + ...whoRemoveStaleEras, + [era]: unclaimdPayouts, + }, + }) + ); +}; diff --git a/src/contexts/Payouts/defaults.ts b/src/contexts/Payouts/defaults.ts new file mode 100644 index 0000000000..f9d101e66a --- /dev/null +++ b/src/contexts/Payouts/defaults.ts @@ -0,0 +1,13 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { PayoutsContextInterface } from './types'; + +export const MaxSupportedPayoutEras = 7; + +export const defaultPayoutsContext: PayoutsContextInterface = { + payoutsSynced: 'unsynced', + unclaimedPayouts: null, + removeEraPayout: (era, validator) => {}, +}; diff --git a/src/contexts/Payouts/index.tsx b/src/contexts/Payouts/index.tsx new file mode 100644 index 0000000000..150f8d72c5 --- /dev/null +++ b/src/contexts/Payouts/index.tsx @@ -0,0 +1,348 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useState, useEffect, useRef } from 'react'; +import { useStaking } from 'contexts/Staking'; +import { useApi } from 'contexts/Api'; +import type { AnyApi, AnyJson, Sync } from 'types'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import Worker from 'workers/stakers?worker'; +import { rmCommas, setStateWithRef } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { MaxSupportedPayoutEras, defaultPayoutsContext } from './defaults'; +import type { + LocalValidatorExposure, + PayoutsContextInterface, + UnclaimedPayouts, +} from './types'; +import { + getLocalEraExposure, + hasLocalEraExposure, + setLocalEraExposure, + setLocalUnclaimedPayouts, +} from './Utils'; + +const worker = new Worker(); + +export const PayoutsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { api } = useApi(); + const { network } = useNetwork(); + const { activeEra } = useNetworkMetrics(); + const { activeAccount } = useActiveAccounts(); + const { isNominating, fetchEraStakers } = useStaking(); + + // Store active accont's payout state. + const [unclaimedPayouts, setUnclaimedPayouts] = + useState<UnclaimedPayouts>(null); + + // Track whether payouts have been fetched. + const [payoutsSynced, setPayoutsSynced] = useState<Sync>('unsynced'); + const payoutsSyncedRef = useRef(payoutsSynced); + + // Calculate eras to check for pending payouts. + const getErasInterval = () => { + const startEra = activeEra?.index.minus(1) || new BigNumber(1); + const endEra = BigNumber.max( + startEra.minus(MaxSupportedPayoutEras).plus(1), + 1 + ); + return { + startEra, + endEra, + }; + }; + + // Determine whether to keep processing a next era, or move onto checking for pending payouts. + const shouldContinueProcessing = async ( + era: BigNumber, + endEra: BigNumber + ) => { + // If there are more exposures to process, check next era. + if (new BigNumber(era).isGreaterThan(endEra)) + checkEra(new BigNumber(era).minus(1)); + // If all exposures have been processed, check for pending payouts. + else if (new BigNumber(era).isEqualTo(endEra)) { + await getUnclaimedPayouts(); + setStateWithRef('synced', setPayoutsSynced, payoutsSyncedRef); + } + }; + + // Fetch exposure data for an era, and pass the data to the worker to determine the validator the + // active account was backing in that era. + const checkEra = async (era: BigNumber) => { + if (!activeAccount) return; + + // Bypass worker if local exposure data is available. + if (hasLocalEraExposure(network, era.toString(), activeAccount)) { + // Continue processing eras, or move onto reward processing. + shouldContinueProcessing(era, getErasInterval().endEra); + } else { + const exposures = await fetchEraStakers(era.toString()); + worker.postMessage({ + task: 'processEraForExposure', + era: String(era), + who: activeAccount, + networkName: network, + exposures, + }); + } + }; + + // Handle worker message on completed exposure check. + worker.onmessage = (message: MessageEvent) => { + if (message) { + // ensure correct task received. + const { data } = message; + const { task } = data; + if (task !== 'processEraForExposure') return; + + // Exit early if network or account conditions have changed. + const { networkName, who } = data; + if (networkName !== network || who !== activeAccount) return; + const { era, exposedValidators } = data; + const { endEra } = getErasInterval(); + + // Store received era exposure data results in local storage. + setLocalEraExposure( + networkName, + era, + who, + exposedValidators, + endEra.toString() + ); + + // Continue processing eras, or move onto reward processing. + shouldContinueProcessing(era, endEra); + } + }; + + // Start pending payout process once exposure data is fetched. + const getUnclaimedPayouts = async () => { + if (!api || !activeAccount) return; + + // Accumulate eras to check, and determine all validator ledgers to fetch from exposures. + const erasValidators = []; + const { startEra, endEra } = getErasInterval(); + let erasToCheck: string[] = []; + let currentEra = startEra; + while (currentEra.isGreaterThanOrEqualTo(endEra)) { + const validators = Object.keys( + getLocalEraExposure(network, currentEra.toString(), activeAccount) + ); + erasValidators.push(...validators); + erasToCheck.push(currentEra.toString()); + currentEra = currentEra.minus(1); + } + + // Ensure no validator duplicates. + const uniqueValidators = [...new Set(erasValidators)]; + + // Ensure `erasToCheck` is in order, highest first. + erasToCheck = erasToCheck.sort((a: string, b: string) => + new BigNumber(b).minus(a).toNumber() + ); + + // Helper function to check which eras a validator was exposed in. + const validatorExposedEras = (validator: string) => { + const exposedEras: string[] = []; + for (const era of erasToCheck) + if ( + Object.values( + Object.keys(getLocalEraExposure(network, era, activeAccount)) + )?.[0] === validator + ) + exposedEras.push(era); + return exposedEras; + }; + + // Fetch controllers in order to query ledgers. + const bondedResults = + await api.query.staking.bonded.multi<AnyApi>(uniqueValidators); + const validatorControllers: Record<string, string> = {}; + for (let i = 0; i < bondedResults.length; i++) { + const ctlr = bondedResults[i].unwrapOr(null); + if (ctlr) validatorControllers[uniqueValidators[i]] = ctlr; + } + + // Fetch ledgers to determine which eras have not yet been claimed per validator. Only includes + // eras that are in `erasToCheck`. + const ledgerResults = await api.query.staking.ledger.multi<AnyApi>( + Object.values(validatorControllers) + ); + const unclaimedRewards: Record<string, string[]> = {}; + for (const ledgerResult of ledgerResults) { + const ledger = ledgerResult.unwrapOr(null)?.toHuman(); + if (ledger) { + // get claimed eras within `erasToCheck`. + const erasClaimed = ledger.claimedRewards + .map((e: string) => rmCommas(e)) + .filter( + (e: string) => + new BigNumber(e).isLessThanOrEqualTo(startEra) && + new BigNumber(e).isGreaterThanOrEqualTo(endEra) + ); + + // filter eras yet to be claimed + unclaimedRewards[ledger.stash] = erasToCheck + .map((e) => e.toString()) + .filter((r: string) => validatorExposedEras(ledger.stash).includes(r)) + .filter((r: string) => !erasClaimed.includes(r)); + } + } + + // Reformat unclaimed rewards to be { era: validators[] }. + const unclaimedByEra: Record<string, string[]> = {}; + erasToCheck.forEach((era) => { + const eraValidators: string[] = []; + Object.entries(unclaimedRewards).forEach(([validator, eras]) => { + if (eras.includes(era)) eraValidators.push(validator); + }); + if (eraValidators.length > 0) unclaimedByEra[era] = eraValidators; + }); + + // Accumulate calls needed to fetch data to calculate rewards. + const calls: AnyApi[] = []; + Object.entries(unclaimedByEra).forEach(([era, validators]) => { + if (validators.length > 0) + calls.push( + Promise.all([ + api.query.staking.erasValidatorReward<AnyApi>(era), + api.query.staking.erasRewardPoints<AnyApi>(era), + ...validators.map((validator: AnyJson) => + api.query.staking.erasValidatorPrefs<AnyApi>(era, validator) + ), + ]) + ); + }); + + // Iterate calls and determine unclaimed payouts. + const unclaimed: UnclaimedPayouts = {}; + let i = 0; + for (const [reward, points, ...prefs] of await Promise.all(calls)) { + const era = Object.keys(unclaimedByEra)[i]; + const eraTotalPayout = new BigNumber(rmCommas(reward.toHuman())); + const eraRewardPoints = points.toHuman(); + const unclaimedValidators = unclaimedByEra[era]; + + let j = 0; + for (const pref of prefs) { + const eraValidatorPrefs = pref.toHuman(); + const commission = new BigNumber( + eraValidatorPrefs.commission.replace(/%/g, '') + ).multipliedBy(0.01); + + // Get validator from era exposure data. Falls back no null if it cannot be found. + const validator = unclaimedValidators?.[j] || ''; + + const localExposed: LocalValidatorExposure | null = getLocalEraExposure( + network, + era, + activeAccount + )?.[validator]; + + const staked = new BigNumber(localExposed?.staked || '0'); + const total = new BigNumber(localExposed?.total || '0'); + const isValidator = localExposed?.isValidator || false; + + // Calculate the validator's share of total era payout. + const totalRewardPoints = new BigNumber( + rmCommas(eraRewardPoints.total) + ); + const validatorRewardPoints = new BigNumber( + rmCommas(eraRewardPoints.individual?.[validator] || '0') + ); + const avail = eraTotalPayout + .multipliedBy(validatorRewardPoints) + .dividedBy(totalRewardPoints); + + const valCut = commission.multipliedBy(avail); + + const unclaimedPayout = total.isZero() + ? new BigNumber(0) + : avail + .minus(valCut) + .multipliedBy(staked) + .dividedBy(total) + .plus(isValidator ? valCut : 0); + + if (!unclaimedPayout.isZero()) { + unclaimed[era] = { + ...unclaimed[era], + [validator]: unclaimedPayout.toString(), + }; + j++; + } + } + + // This is not currently useful for preventing re-syncing. Need to know the eras that have + // been claimed already and remove them from `erasToCheck`. + setLocalUnclaimedPayouts( + network, + era, + activeAccount, + unclaimed[era], + endEra.toString() + ); + i++; + } + + setUnclaimedPayouts({ + ...unclaimedPayouts, + ...unclaimed, + }); + }; + + // Removes a payout from `unclaimedPayouts` based on an era and validator record. + const removeEraPayout = (era: string, validator: string) => { + if (!unclaimedPayouts) return; + const newUnclaimedPayouts = { ...unclaimedPayouts }; + delete newUnclaimedPayouts[era][validator]; + setUnclaimedPayouts(newUnclaimedPayouts); + }; + + // Fetch payouts if active account is nominating. + useEffect(() => { + if (!activeEra.index.isZero()) { + if (!isNominating()) { + setStateWithRef('synced', setPayoutsSynced, payoutsSyncedRef); + } else if ( + unclaimedPayouts === null && + payoutsSyncedRef.current !== 'syncing' + ) { + setStateWithRef('syncing', setPayoutsSynced, payoutsSyncedRef); + // Start checking eras for exposures, starting with the previous one. + checkEra(activeEra.index.minus(1)); + } + } + }, [unclaimedPayouts, isNominating(), activeEra, payoutsSynced]); + + // Clear payout state on network / active account change. + useEffect(() => { + setUnclaimedPayouts(null); + setStateWithRef('unsynced', setPayoutsSynced, payoutsSyncedRef); + }, [network, activeAccount]); + + return ( + <PayoutsContext.Provider + value={{ + unclaimedPayouts, + payoutsSynced: payoutsSyncedRef.current, + removeEraPayout, + }} + > + {children} + </PayoutsContext.Provider> + ); +}; + +export const PayoutsContext = React.createContext<PayoutsContextInterface>( + defaultPayoutsContext +); + +export const usePayouts = () => React.useContext(PayoutsContext); diff --git a/src/contexts/Payouts/types.ts b/src/contexts/Payouts/types.ts new file mode 100644 index 0000000000..3adfe39aeb --- /dev/null +++ b/src/contexts/Payouts/types.ts @@ -0,0 +1,21 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { Sync } from 'types'; + +export type PayoutsContextInterface = { + payoutsSynced: Sync; + unclaimedPayouts: UnclaimedPayouts; + removeEraPayout: (era: string, validator: string) => void; +}; + +export type UnclaimedPayouts = Record<string, EraUnclaimedPayouts> | null; + +export type EraUnclaimedPayouts = Record<string, string>; + +export interface LocalValidatorExposure { + staked: string; + total: string; + share: string; + isValidator: boolean; +} diff --git a/src/contexts/Plugins/Polkawatch/defaults.tsx b/src/contexts/Plugins/Polkawatch/defaults.tsx new file mode 100644 index 0000000000..59bd28a2e3 --- /dev/null +++ b/src/contexts/Plugins/Polkawatch/defaults.tsx @@ -0,0 +1,8 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +// Polkawatch API version. +export const PolkaWatchApiVersion = 'v2'; + +// Polkawatch supported networks. +export const PolkaWatchNetworks = ['polkadot', 'kusama']; diff --git a/src/contexts/Plugins/Polkawatch/index.tsx b/src/contexts/Plugins/Polkawatch/index.tsx new file mode 100644 index 0000000000..b92aa8b049 --- /dev/null +++ b/src/contexts/Plugins/Polkawatch/index.tsx @@ -0,0 +1,71 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { Configuration, PolkawatchApi } from '@polkawatch/ddp-client'; +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { localStorageOrDefault } from '@polkadot-cloud/utils'; +import { useNetwork } from 'contexts/Network'; +import type { NetworkName } from '../../../types'; +import type { PolkawatchState } from './types'; +import { DefaultNetwork } from '../../../consts'; +import { PolkaWatchApiVersion, PolkaWatchNetworks } from './defaults'; + +/** + * This is the Polkawatch API provider, which builds polkawatch API depending on the Chain that is currently + * in context. Also returns information about whether there exist decentralization analytics for the Network. + */ + +/** + * Builds the API Configuration based on Chain and API Version + * @param name the chain to query: polkadot, kusama, etc. + * @param version the API version + * @constructor + */ +const apiConfiguration = ( + name: NetworkName = localStorageOrDefault( + 'network', + DefaultNetwork + ) as NetworkName, + version = PolkaWatchApiVersion +): Configuration => + new Configuration({ + basePath: `https://${name}-${version}-api.polkawatch.app`, + }); + +const PolkawatchInitialState = { + pwApi: new PolkawatchApi(apiConfiguration()), + networkSupported: true, +}; + +export const PolkawatchProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + + const [state, setState] = useState<PolkawatchState>(PolkawatchInitialState); + + /** + * We update the API object when another network is selected. The api Object is stateless, there + * is no network or computational cost involved in creating a new API. + */ + useEffect(() => { + setState({ + pwApi: new PolkawatchApi(apiConfiguration(network)), + networkSupported: PolkaWatchNetworks.includes(network), + }); + }, [network]); + + return ( + <PolkawatchContext.Provider value={state}> + {children} + </PolkawatchContext.Provider> + ); +}; + +const PolkawatchContext = createContext<PolkawatchState>( + PolkawatchInitialState +); + +export const usePolkawatchApi = () => useContext(PolkawatchContext); diff --git a/src/contexts/Plugins/Polkawatch/types.tsx b/src/contexts/Plugins/Polkawatch/types.tsx new file mode 100644 index 0000000000..9e0a15b4e7 --- /dev/null +++ b/src/contexts/Plugins/Polkawatch/types.tsx @@ -0,0 +1,13 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { PolkawatchApi } from '@polkawatch/ddp-client'; + +/** + * The provider will return an API and also information about whether the selected network has decentralization + * analytics support. + */ +export interface PolkawatchState { + pwApi: PolkawatchApi; + networkSupported: boolean; +} diff --git a/src/contexts/Plugins/Subscan/defaults.ts b/src/contexts/Plugins/Subscan/defaults.ts new file mode 100644 index 0000000000..92e9cef8e9 --- /dev/null +++ b/src/contexts/Plugins/Subscan/defaults.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { SubscanContextInterface } from './types'; + +export const defaultSubscanContext: SubscanContextInterface = { + fetchEraPoints: (v, e) => {}, + payouts: [], + poolClaims: [], + unclaimedPayouts: [], + payoutsFromDate: undefined, + payoutsToDate: undefined, + fetchPoolDetails: (poolId) => new Promise((resolve) => resolve({})), + fetchPoolMembers: (poolId, page) => new Promise((resolve) => resolve([])), + setUnclaimedPayouts: (payouts) => {}, +}; diff --git a/src/contexts/Plugins/Subscan/index.tsx b/src/contexts/Plugins/Subscan/index.tsx new file mode 100644 index 0000000000..d373fe1ebe --- /dev/null +++ b/src/contexts/Plugins/Subscan/index.tsx @@ -0,0 +1,371 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { isNotZero } from '@polkadot-cloud/utils'; +import { format, fromUnixTime } from 'date-fns'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ApiEndpoints, + ApiSubscanKey, + DefaultLocale, + ListItemsPerPage, +} from 'consts'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { sortNonZeroPayouts } from 'library/Graphs/Utils'; +import { useErasToTimeLeft } from 'library/Hooks/useErasToTimeLeft'; +import { locales } from 'locale'; +import type { AnyApi, AnySubscan } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useApi } from '../../Api'; +import { usePlugins } from '..'; +import { defaultSubscanContext } from './defaults'; +import type { SubscanContextInterface } from './types'; + +export const SubscanProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { i18n } = useTranslation(); + const { isReady } = useApi(); + const { + network, + networkData: { subscanEndpoint }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { activeEra } = useNetworkMetrics(); + const { erasToSeconds } = useErasToTimeLeft(); + const { plugins, pluginEnabled } = usePlugins(); + + // store fetched payouts from Subscan + const [payouts, setPayouts] = useState<AnySubscan>([]); + + // store fetched pool claims from Subscan + const [poolClaims, setPoolClaims] = useState<AnySubscan>([]); + + // store fetched unclaimed payouts from Subscan + const [unclaimedPayouts, setUnclaimedPayouts] = useState<AnyApi>([]); + + // store the start date of fetched payouts and pool claims combined. + const [payoutsFromDate, setPayoutsFromDate] = useState<string | undefined>(); + + // store the end date of fetched payouts and pool claims combined. + const [payoutsToDate, setPayoutsToDate] = useState<string | undefined>(); + + // handle fetching the various types of payout and set state in one render. + const handleFetchPayouts = async () => { + const results = await Promise.all([fetchPayouts(), fetchPoolClaims()]); + + const { newClaimedPayouts, newUnclaimedPayouts } = results[0]; + const newPoolClaims = results[1]; + + setPayouts(newClaimedPayouts); + setUnclaimedPayouts(newUnclaimedPayouts); + setPoolClaims(newPoolClaims); + }; + + // reset all payout state + const resetPayouts = () => { + setPayouts([]); + setUnclaimedPayouts([]); + setPoolClaims([]); + }; + + // Reset payouts on network switch. + useEffectIgnoreInitial(() => { + resetPayouts(); + }, [network]); + + // Reset payouts on no active account. + useEffectIgnoreInitial(() => { + if (!activeAccount) resetPayouts(); + }, [activeAccount]); + + // Reset payouts on subscan plugin not enabled. + useEffectIgnoreInitial(() => { + if (!plugins.includes('subscan')) resetPayouts(); + else if (isReady && isNotZero(activeEra.index)) handleFetchPayouts(); + }, [plugins.includes('subscan'), isReady, activeEra]); + + // Fetch payouts as soon as network is ready. + useEffectIgnoreInitial(() => { + if (isReady && isNotZero(activeEra.index)) { + handleFetchPayouts(); + } + }, [isReady, network, activeAccount, activeEra]); + + // Store start and end date of fetched payouts. + useEffectIgnoreInitial(() => { + const filteredPayouts = sortNonZeroPayouts(payouts, poolClaims, true); + if (filteredPayouts.length) { + setPayoutsFromDate( + format( + fromUnixTime( + filteredPayouts[filteredPayouts.length - 1].block_timestamp + ), + 'do MMM', + { + locale: locales[i18n.resolvedLanguage ?? DefaultLocale], + } + ) + ); + + // latest payout date + setPayoutsToDate( + format(fromUnixTime(filteredPayouts[0].block_timestamp), 'do MMM', { + locale: locales[i18n.resolvedLanguage ?? DefaultLocale], + }) + ); + } + }, [payouts, poolClaims, unclaimedPayouts]); + + /* fetchPayouts + * fetches payout history from Subscan. + * Fetches a total of 300 records from 3 asynchronous requests. + * Also checks if subscan service is active *after* the fetch has resolved + * as the user could have turned off the service while payouts were fetching. + * Stores resulting payouts in context state. + */ + const fetchPayouts = async () => { + let newClaimedPayouts: AnySubscan[] = []; + let newUnclaimedPayouts: AnySubscan[] = []; + + // fetch results if subscan is enabled + if (activeAccount && pluginEnabled('subscan')) { + // fetch 1 page of results + const results = await Promise.all([ + handleFetch(0, ApiEndpoints.subscanRewardSlash, 100, { + address: activeAccount, + is_stash: true, + }), + ]); + + // user may have turned off service while results were fetching. + // test again whether subscan service is still active. + if (pluginEnabled('subscan')) { + for (const result of results) { + if (!result?.data?.list) { + break; + } + // ensure no payouts have block_timestamp of 0 + const list = result.data.list.filter( + (l: AnyApi) => l.block_timestamp !== 0 + ); + newClaimedPayouts = newClaimedPayouts.concat(list); + + const unclaimedList = result.data.list.filter( + (l: AnyApi) => l.block_timestamp === 0 + ); + + // Inject block_timestamp for unclaimed payouts. We take the timestamp of the start of the + // following payout era - this is the time payouts become available to claim by + // validators. + unclaimedList.forEach((p: AnyApi) => { + p.block_timestamp = activeEra.start + .multipliedBy(0.001) + .minus(erasToSeconds(activeEra.index.minus(p.era).minus(1))) + .toNumber(); + }); + newUnclaimedPayouts = newUnclaimedPayouts.concat(unclaimedList); + } + } + } + return { + newClaimedPayouts, + newUnclaimedPayouts, + }; + }; + + /* fetchPoolClaims + * fetches claim history from Subscan. + * Fetches a total of 300 records from 3 asynchronous requests. + * Also checks if subscan service is active *after* the fetch has resolved + * as the user could have turned off the service while payouts were fetching. + * Stores resulting claims in context state. + */ + const fetchPoolClaims = async () => { + let newPoolClaims: AnySubscan[] = []; + + // fetch results if subscan is enabled + if (activeAccount && pluginEnabled('subscan')) { + // fetch 1 page of results + const results = await Promise.all([ + handleFetch(0, ApiEndpoints.subscanPoolRewards, 100, { + address: activeAccount, + }), + ]); + + // user may have turned off service while results were fetching. + // test again whether subscan service is still active. + if (pluginEnabled('subscan')) { + for (const result of results) { + // check incorrectly formatted result object + if (!result?.data?.list) { + break; + } + // check list has records + if (!result.data.list.length) { + break; + } + // ensure no payouts have block_timestamp of 0 + const list = result.data.list.filter( + (l: AnyApi) => l.block_timestamp !== 0 + ); + newPoolClaims = newPoolClaims.concat(list); + } + } + } + return newPoolClaims; + }; + + /* fetchEraPoints + * fetches recent era point history for a particular address. + * Also checks if subscan service is active *after* the fetch has resolved + * as the user could have turned off the service while payouts were fetching. + * returns eraPoints + */ + const fetchEraPoints = async (address: string, era: number) => { + if (address === '' || !plugins.includes('subscan')) { + return []; + } + const res = await handleFetch(0, ApiEndpoints.subscanEraStat, 100, { + address, + }); + + if (res.message === 'Success') { + if (pluginEnabled('subscan')) { + if (res.data?.list !== null) { + const list = []; + for (let i = era; i > era - 100; i--) { + list.push({ + era: i, + reward_point: + res.data.list.find((item: AnySubscan) => item.era === i) + ?.reward_point ?? 0, + }); + } + // removes last zero item and returns + return list.reverse().splice(0, list.length - 1); + } + return []; + } + } + return []; + }; + + /* fetchPoolDetails + * Also checks if subscan service is active *after* the fetch has resolved + * as the user could have turned off the service while payouts were fetching. + */ + const fetchPoolDetails = async (poolId: number) => { + if (!plugins.includes('subscan')) { + return []; + } + const res: Response = await fetch( + subscanEndpoint + ApiEndpoints.subscanPoolDetails, + { + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': ApiSubscanKey, + }, + body: JSON.stringify({ + pool_id: poolId, + }), + method: 'POST', + } + ); + const json: AnySubscan = await res.json(); + return json?.data || undefined; + }; + + /* fetchPoolMembers + * Also checks if subscan service is active *after* the fetch has resolved + * as the user could have turned off the service while payouts were fetching. + */ + const fetchPoolMembers = async (poolId: number, page: number) => { + if (!plugins.includes('subscan')) { + return []; + } + const res = await handleFetch( + page - 1, + ApiEndpoints.subscanPoolMembers, + ListItemsPerPage, + { + pool_id: poolId, + } + ); + + if (res.message === 'Success') { + if (pluginEnabled('subscan')) { + if (res.data?.list !== null) { + const result = res.data?.list || []; + const list: AnySubscan = []; + for (const item of result) { + list.push({ + who: item.account_display.address, + poolId: item.pool_id, + }); + } + // removes last zero item and returns + return list.reverse().splice(0, list.length - 1); + } + } + return []; + } + return []; + }; + + /* handleFetch + * utility to handle a fetch request to Subscan + * returns resulting JSON. + */ + const handleFetch = async ( + page: number, + endpoint: string, + row: number, + body: AnyApi = {} + ): Promise<AnySubscan> => { + const bodyJson = { + row, + page, + ...body, + }; + const res: Response = await fetch(subscanEndpoint + endpoint, { + headers: { + 'Content-Type': 'application/json', + 'X-API-Key': ApiSubscanKey, + }, + body: JSON.stringify(bodyJson), + method: 'POST', + }); + const resJson: AnySubscan = await res.json(); + return resJson; + }; + + return ( + <SubscanContext.Provider + value={{ + fetchEraPoints, + payouts, + poolClaims, + unclaimedPayouts, + payoutsFromDate, + payoutsToDate, + fetchPoolDetails, + fetchPoolMembers, + setUnclaimedPayouts, + }} + > + {children} + </SubscanContext.Provider> + ); +}; + +export const SubscanContext = React.createContext<SubscanContextInterface>( + defaultSubscanContext +); + +export const useSubscan = () => React.useContext(SubscanContext); diff --git a/src/contexts/Plugins/Subscan/types.ts b/src/contexts/Plugins/Subscan/types.ts new file mode 100644 index 0000000000..65dd9deae6 --- /dev/null +++ b/src/contexts/Plugins/Subscan/types.ts @@ -0,0 +1,16 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnySubscan } from 'types'; + +export interface SubscanContextInterface { + fetchEraPoints: (v: string, e: number) => void; + payouts: AnySubscan; + poolClaims: AnySubscan; + unclaimedPayouts: AnySubscan; + payoutsFromDate: string | undefined; + payoutsToDate: string | undefined; + fetchPoolDetails: (poolId: number) => Promise<any>; + fetchPoolMembers: (poolId: number, page: number) => Promise<any[]>; + setUnclaimedPayouts: (payouts: AnySubscan) => void; +} diff --git a/src/contexts/Plugins/defaults.ts b/src/contexts/Plugins/defaults.ts new file mode 100644 index 0000000000..35d64a1168 --- /dev/null +++ b/src/contexts/Plugins/defaults.ts @@ -0,0 +1,11 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { PluginsContextInterface } from './types'; + +export const defaultPluginsContext: PluginsContextInterface = { + togglePlugin: (k) => {}, + pluginEnabled: (k) => false, + plugins: [], +}; diff --git a/src/contexts/Plugins/index.tsx b/src/contexts/Plugins/index.tsx new file mode 100644 index 0000000000..53158c2346 --- /dev/null +++ b/src/contexts/Plugins/index.tsx @@ -0,0 +1,72 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { localStorageOrDefault, setStateWithRef } from '@polkadot-cloud/utils'; +import React, { useRef, useState } from 'react'; +import { PluginsList } from 'consts'; +import type { Plugin } from 'types'; +import * as defaults from './defaults'; +import type { PluginsContextInterface } from './types'; + +export const PluginsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + // Get initial plugins from local storage. + const getAvailablePlugins = () => { + const localPlugins = localStorageOrDefault( + 'plugins', + PluginsList, + true + ) as Plugin[]; + + // if fiat is disabled, remove binance_spot service + const DISABLE_FIAT = Number(import.meta.env.VITE_DISABLE_FIAT ?? 0); + if (DISABLE_FIAT && localPlugins.includes('binance_spot')) { + const index = localPlugins.indexOf('binance_spot'); + if (index !== -1) localPlugins.splice(index, 1); + } + return localPlugins; + }; + + // Store the currently active plugins. + const [plugins, setPlugins] = useState<Plugin[]>(getAvailablePlugins()); + const pluginsRef = useRef(plugins); + + // Toggle a plugin. + const togglePlugin = (key: Plugin) => { + let localPlugins = [...plugins]; + const found = localPlugins.find((p) => p === key); + + if (found) { + localPlugins = localPlugins.filter((p) => p !== key); + } else { + localPlugins.push(key); + } + + localStorage.setItem('plugins', JSON.stringify(localPlugins)); + setStateWithRef(localPlugins, setPlugins, pluginsRef); + }; + + // Check if a plugin is currently enabled. + const pluginEnabled = (key: Plugin) => pluginsRef.current.includes(key); + + return ( + <PluginsContext.Provider + value={{ + togglePlugin, + pluginEnabled, + plugins: pluginsRef.current, + }} + > + {children} + </PluginsContext.Provider> + ); +}; + +export const PluginsContext = React.createContext<PluginsContextInterface>( + defaults.defaultPluginsContext +); + +export const usePlugins = () => React.useContext(PluginsContext); diff --git a/src/contexts/Plugins/types.ts b/src/contexts/Plugins/types.ts new file mode 100644 index 0000000000..19ecb6550a --- /dev/null +++ b/src/contexts/Plugins/types.ts @@ -0,0 +1,10 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { Plugin } from 'types'; + +export interface PluginsContextInterface { + togglePlugin: (k: Plugin) => void; + pluginEnabled: (key: Plugin) => boolean; + plugins: Plugin[]; +} diff --git a/src/contexts/Pools/ActivePools/defaults.ts b/src/contexts/Pools/ActivePools/defaults.ts new file mode 100644 index 0000000000..cb75b2ca4c --- /dev/null +++ b/src/contexts/Pools/ActivePools/defaults.ts @@ -0,0 +1,69 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import BigNumber from 'bignumber.js'; +import type { ActivePool, ActivePoolsContextState } from '../types'; + +export const nominationStatus = {}; + +export const poolRoles = { + depositor: '', + nominator: '', + root: '', + bouncer: '', +}; + +export const bondedPool = { + points: '0', + state: 'Blocked', + memberCounter: '0', + roles: null, +}; + +export const rewardPool = { + lastRecordedRewardCounter: '0', + lastRecordedTotalPayouts: '0', + totalRewardsClaimed: '0', +}; + +export const selectedActivePool: ActivePool = { + id: 0, + addresses: { + stash: '', + reward: '', + }, + bondedPool, + rewardPool, + rewardAccountBalance: {}, + pendingRewards: new BigNumber(0), +}; + +export const targets = { + nominations: [], +}; + +export const poolNominations = { + targets: [], + submittedIn: 0, +}; + +export const defaultActivePoolContext: ActivePoolsContextState = { + isBonding: () => false, + isNominator: () => false, + isOwner: () => false, + isMember: () => false, + isDepositor: () => false, + isBouncer: () => false, + getPoolBondedAccount: () => null, + getPoolUnlocking: () => [], + getPoolRoles: () => poolRoles, + setTargets: (t) => {}, + getNominationsStatus: () => nominationStatus, + setSelectedPoolId: (p) => {}, + selectedActivePool, + targets, + poolNominations, + synced: 'unsynced', + selectedPoolMemberCount: 0, +}; diff --git a/src/contexts/Pools/ActivePools/index.tsx b/src/contexts/Pools/ActivePools/index.tsx new file mode 100644 index 0000000000..a0fa72a39d --- /dev/null +++ b/src/contexts/Pools/ActivePools/index.tsx @@ -0,0 +1,562 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { localStorageOrDefault, setStateWithRef } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import type { + ActivePool, + ActivePoolsContextState, + PoolAddresses, +} from 'contexts/Pools/types'; +import { useStaking } from 'contexts/Staking'; +import type { AnyApi, AnyJson, Sync } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useSubscan } from 'contexts/Plugins/Subscan'; +import { usePlugins } from 'contexts/Plugins'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useApi } from '../../Api'; +import { useBondedPools } from '../BondedPools'; +import { usePoolMemberships } from '../PoolMemberships'; +import { usePoolsConfig } from '../PoolsConfig'; +import * as defaults from './defaults'; +import { usePoolMembers } from '../PoolMembers'; + +export const ActivePoolsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + const { api, isReady } = useApi(); + const { eraStakers } = useStaking(); + const { pluginEnabled } = usePlugins(); + const { fetchPoolDetails } = useSubscan(); + const { membership } = usePoolMemberships(); + const { createAccounts } = usePoolsConfig(); + const { activeAccount } = useActiveAccounts(); + const { getAccountPools, bondedPools } = useBondedPools(); + const { getMembersOfPoolFromNode, poolMembersNode } = usePoolMembers(); + + // Determine active pools to subscribe to. + const accountPools = useMemo(() => { + const newAccountPools = Object.keys(getAccountPools(activeAccount) || {}); + const p = membership?.poolId ? String(membership.poolId) : '-1'; + + if (membership?.poolId && !newAccountPools.includes(p || '-1')) { + newAccountPools.push(String(membership.poolId)); + } + return newAccountPools; + }, [activeAccount, bondedPools, membership]); + + // Stores member's active pools. + const [activePools, setActivePools] = useState<ActivePool[]>([]); + const activePoolsRef = useRef(activePools); + + // Store active pools unsubs. + const unsubActivePools = useRef<AnyApi[]>([]); + + // Store active pools nominations. + const [poolNominations, setPoolNominations] = useState< + Record<number, AnyJson> + >({}); + const poolNominationsRef = useRef(poolNominations); + + // Store pool nominations unsubs. + const unsubNominations = useRef<AnyApi[]>([]); + + // Store account target validators. + const [targets, setTargetsState] = useState<Record<number, AnyJson>>({}); + const targetsRef = useRef(targets); + + // Store the member count of the selected pool. + const [selectedPoolMemberCount, setSelectedPoolMemberCount] = + useState<number>(0); + + const fetchingMemberCount = useRef<boolean>(false); + + // Store whether active pool data has been synced. this will be true if no active pool exists for + // the active account. We just need confirmation this is the case. + const [synced, setSynced] = useState<Sync>('unsynced'); + const syncedRef = useRef(synced); + + // Store the currently selected active pool for the UI. Should default to the membership pool (if + // present). + const [selectedPoolId, setSelectedPoolId] = useState<string | null>(null); + + const getActivePoolMembership = () => + // get the activePool that the active account + activePoolsRef.current.find((a) => { + const p = membership?.poolId ? String(membership.poolId) : '0'; + return String(a.id) === p; + }) || null; + const getSelectedActivePool = () => + activePoolsRef.current.find((a) => a.id === Number(selectedPoolId)) || null; + + const getSelectedPoolNominations = () => + poolNominationsRef.current[Number(selectedPoolId) ?? -1] || + defaults.poolNominations; + + const getSelectedPoolTargets = () => + targetsRef.current[Number(selectedPoolId) ?? -1] || defaults.targets; + + // handle active pool subscriptions + const handlePoolSubscriptions = async () => { + if (accountPools.length) { + Promise.all(accountPools.map((p) => subscribeToActivePool(Number(p)))); + } else { + setStateWithRef('synced', setSynced, syncedRef); + } + + // assign default pool immediately if active pool not currently selected + const defaultSelected = membership?.poolId || accountPools[0] || null; + const activePoolSelected = + activePoolsRef.current.find( + (a) => String(a.id) === String(selectedPoolId) + ) || null; + + if (defaultSelected && !activePoolSelected) { + setSelectedPoolId(String(defaultSelected)); + } + }; + + // Unsubscribe and reset poolNominations. + const unsubscribePoolNominations = () => { + if (unsubNominations.current.length) { + for (const unsub of unsubNominations.current) { + unsub(); + } + } + setStateWithRef({}, setPoolNominations, poolNominationsRef); + unsubNominations.current = []; + }; + + // Unsubscribe and reset activePool and poolNominations. + const unsubscribeActivePools = () => { + if (unsubActivePools.current.length) { + for (const unsub of unsubActivePools.current) { + unsub(); + } + setStateWithRef([], setActivePools, activePoolsRef); + unsubActivePools.current = []; + } + }; + + const subscribeToActivePool = async (poolId: number) => { + if (!api) { + return; + } + + const addresses: PoolAddresses = createAccounts(poolId); + + // new active pool subscription + const subscribeActivePool = async (id: number) => { + const unsub = await api.queryMulti<AnyApi>( + [ + [api.query.nominationPools.bondedPools, id], + [api.query.nominationPools.rewardPools, id], + [api.query.system.account, addresses.reward], + ], + async ([bondedPool, rewardPool, accountData]): Promise<void> => { + const balance = accountData.data; + bondedPool = bondedPool?.unwrapOr(undefined)?.toHuman(); + rewardPool = rewardPool?.unwrapOr(undefined)?.toHuman(); + if (rewardPool && bondedPool) { + const rewardAccountBalance = balance?.free; + + const pendingRewards = await fetchPendingRewards(); + + const pool = { + id, + addresses, + bondedPool, + rewardPool, + rewardAccountBalance, + pendingRewards, + }; + + // set active pool state, removing the pool if it already exists first. + setStateWithRef( + [...activePoolsRef.current.filter((a) => a.id !== pool.id), pool], + setActivePools, + activePoolsRef + ); + + // get pool target nominations and set in state + const newTargets = localStorageOrDefault( + `${addresses.stash}_pool_targets`, + defaults.targets, + true + ); + + // add or replace current pool targets in targetsRef + const newPoolTargets = { ...targetsRef.current }; + newPoolTargets[poolId] = newTargets; + + // set pool staking targets + setStateWithRef(newPoolTargets, setTargetsState, targetsRef); + + // subscribe to pool nominations + subscribeToPoolNominations(poolId, addresses.stash); + } else { + // set default targets for pool + const newPoolTargets = { ...targetsRef.current }; + newPoolTargets[poolId] = defaults.targets; + setStateWithRef(newPoolTargets, setTargetsState, targetsRef); + } + } + ); + return unsub; + }; + + // initiate subscription, add to unsubs. + await Promise.all([subscribeActivePool(poolId)]).then((unsubs: any) => { + unsubActivePools.current = unsubActivePools.current.concat(unsubs); + }); + }; + + const subscribeToPoolNominations = async ( + poolId: number, + poolBondAddress: string + ) => { + if (!api) return; + const subscribePoolNominations = async (bondedAddress: string) => { + const unsub = await api.query.staking.nominators( + bondedAddress, + (nominations: AnyApi) => { + // set pool nominations + let newNominations = nominations.unwrapOr(null); + if (newNominations === null) { + newNominations = defaults.poolNominations; + } else { + newNominations = { + targets: newNominations.targets.toHuman(), + submittedIn: newNominations.submittedIn.toHuman(), + }; + } + + // add or replace current pool nominations in poolNominations + const newPoolNominations = { ...poolNominationsRef.current }; + newPoolNominations[poolId] = newNominations; + + // set pool nominations state + setStateWithRef( + newPoolNominations, + setPoolNominations, + poolNominationsRef + ); + } + ); + return unsub; + }; + + // initiate subscription, add to unsubs. + await Promise.all([subscribePoolNominations(poolBondAddress)]).then( + (unsubs) => { + unsubNominations.current = unsubNominations.current.concat(unsubs); + } + ); + }; + + // Utility functions + /* + * updateActivePoolPendingRewards + * A helper function to set the unclaimed rewards of an active pool. + */ + const updateActivePoolPendingRewards = ( + pendingRewards: BigNumber, + poolId: number + ) => { + if (!poolId) return; + + // update the active pool the account is a member of. + setStateWithRef( + [...activePoolsRef.current].map((a) => + a.id === poolId + ? { + ...a, + pendingRewards, + } + : a + ), + setActivePools, + activePoolsRef + ); + }; + + /* + * setTargets + * Sets currently selected pool's target validators in storage. + */ + const setTargets = (newTargets: any) => { + if (!selectedPoolId) { + return; + } + + const stashAddress = getPoolBondedAccount(); + if (stashAddress) { + localStorage.setItem( + `${stashAddress}_pool_targets`, + JSON.stringify(newTargets) + ); + // inject targets into targets object + const newPoolTargets = { ...targetsRef.current }; + newPoolTargets[Number(selectedPoolId)] = newTargets; + + setStateWithRef(newPoolTargets, setTargetsState, targetsRef); + } + }; + + /* + * isBonding + * Returns whether active pool exists + */ + const isBonding = () => !!getSelectedActivePool(); + + /* + * isNominator + * Returns whether the active account is + * the nominator in the active pool. + */ + const isNominator = () => { + const roles = getSelectedActivePool()?.bondedPool?.roles; + if (!activeAccount || !roles) { + return false; + } + return activeAccount === roles?.nominator; + }; + + /* + * isOwner + * Returns whether the active account is + * the owner of the active pool. + */ + const isOwner = () => { + const roles = getSelectedActivePool()?.bondedPool?.roles; + if (!activeAccount || !roles) { + return false; + } + return activeAccount === roles?.root; + }; + + /* + * isMember + * Returns whether the active account is + * a member of the active pool. + */ + const isMember = () => { + const selectedPool = getSelectedActivePool(); + const p = selectedPool ? String(selectedPool.id) : '-1'; + return String(membership?.poolId || '') === p; + }; + + /* + * isDepositor + * Returns whether the active account is + * the depositor of the active pool. + */ + const isDepositor = () => { + const roles = getSelectedActivePool()?.bondedPool?.roles; + if (!activeAccount || !roles) { + return false; + } + return activeAccount === roles?.depositor; + }; + + /* + * isBouncer + * Returns whether the active account is + * the depositor of the active pool. + */ + const isBouncer = () => { + const roles = getSelectedActivePool()?.bondedPool?.roles; + if (!activeAccount || !roles) { + return false; + } + return activeAccount === roles?.bouncer; + }; + + /* + * getPoolBondedAccount + * get the stash address of the bonded pool + * that the member is participating in. + */ + const getPoolBondedAccount = () => + getSelectedActivePool()?.addresses?.stash || null; + + /* + * Get the status of nominations. + * Possible statuses: waiting, inactive, active. + */ + const getNominationsStatus = () => { + const nominations = getSelectedPoolNominations().nominations?.targets || []; + const statuses: Record<string, string> = {}; + + for (const nomination of nominations) { + const s = eraStakers.stakers.find( + ({ address }) => address === nomination + ); + + if (s === undefined) { + statuses[nomination] = 'waiting'; + continue; + } + const exists = (s.others ?? []).find(({ who }) => who === activeAccount); + if (exists === undefined) { + statuses[nomination] = 'inactive'; + continue; + } + statuses[nomination] = 'active'; + } + return statuses; + }; + + /* + * getPoolRoles + * Returns the active pool's roles or a default roles object. + */ + const getPoolRoles = () => { + return getSelectedActivePool()?.bondedPool?.roles || defaults.poolRoles; + }; + + const getPoolUnlocking = () => { + const membershipPoolId = membership?.poolId + ? String(membership.poolId) + : '-1'; + + // exit early if the currently selected pool is not membership pool + if (selectedPoolId !== membershipPoolId) { + return []; + } + return membership?.unlocking || []; + }; + + // Fetch and update unclaimed rewards from runtime call. + const fetchPendingRewards = async () => { + if (getActivePoolMembership() && membership && api && isReady) { + const pendingRewards = await api.call.nominationPoolsApi.pendingRewards( + membership?.address || '' + ); + return new BigNumber(pendingRewards?.toString() || 0); + } + return new BigNumber(0); + }; + + // Fetch and update pending rewards when membership changes. + const updatePendingRewards = async () => { + const pendingRewards = await fetchPendingRewards(); + + updateActivePoolPendingRewards( + pendingRewards, + getActivePoolMembership()?.id || 0 + ); + }; + + // re-sync when number of accountRoles change. + // this can happen when bondedPools sync, when roles + // are edited within the dashboard, or when pool + // membership changes. + useEffectIgnoreInitial(() => { + unsubscribeActivePools(); + unsubscribePoolNominations(); + setStateWithRef('unsynced', setSynced, syncedRef); + }, [activeAccount, accountPools.length]); + + // subscribe to pool that the active account is a member of. + useEffectIgnoreInitial(() => { + if (isReady && syncedRef.current === 'unsynced') { + setStateWithRef('syncing', setSynced, syncedRef); + handlePoolSubscriptions(); + } + }, [network, isReady, syncedRef.current]); + + // unsubscribe all on component unmount + useEffect( + () => () => { + unsubscribeActivePools(); + unsubscribePoolNominations(); + }, + [network] + ); + + // re-calculate pending rewards when membership changes + useEffectIgnoreInitial(() => { + updatePendingRewards(); + }, [ + network, + isReady, + getActivePoolMembership()?.bondedPool, + getActivePoolMembership()?.rewardPool, + membership, + ]); + + // Gets the member count of the currently selected pool. If Subscan is enabled, it is used instead of the connected node. + const getMemberCount = async () => { + const selectedActivePool = getSelectedActivePool(); + + if (!selectedActivePool?.id) { + setSelectedPoolMemberCount(0); + return; + } + // If `Subscan` plugin is enabled, fetch member count directly from the API. + if (pluginEnabled('subscan') && !fetchingMemberCount.current) { + fetchingMemberCount.current = true; + const poolDetails = await fetchPoolDetails(selectedActivePool.id); + fetchingMemberCount.current = false; + setSelectedPoolMemberCount(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. + setSelectedPoolMemberCount( + getMembersOfPoolFromNode(selectedActivePool?.id ?? 0).length + ); + }; + + // when we are subscribed to all active pools, syncing is considered + // completed. + useEffectIgnoreInitial(() => { + if (unsubNominations.current.length === accountPools.length) { + setStateWithRef('synced', setSynced, syncedRef); + } + }, [accountPools, unsubNominations.current]); + + // 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. + useEffect(() => { + getMemberCount(); + }, [activeAccount, getSelectedActivePool(), membership, poolMembersNode]); + + return ( + <ActivePoolsContext.Provider + value={{ + isNominator, + isOwner, + isMember, + isDepositor, + isBouncer, + isBonding, + getPoolBondedAccount, + getPoolUnlocking, + getPoolRoles, + setTargets, + getNominationsStatus, + setSelectedPoolId, + synced: syncedRef.current, + selectedActivePool: getSelectedActivePool(), + targets: getSelectedPoolTargets(), + poolNominations: getSelectedPoolNominations(), + selectedPoolMemberCount, + }} + > + {children} + </ActivePoolsContext.Provider> + ); +}; + +export const ActivePoolsContext = React.createContext<ActivePoolsContextState>( + defaults.defaultActivePoolContext +); + +export const useActivePools = () => React.useContext(ActivePoolsContext); diff --git a/src/contexts/Pools/BondedPools/defaults.ts b/src/contexts/Pools/BondedPools/defaults.ts new file mode 100644 index 0000000000..0074e1771c --- /dev/null +++ b/src/contexts/Pools/BondedPools/defaults.ts @@ -0,0 +1,22 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { BondedPoolsContextState } from '../types'; + +export const defaultBondedPoolsContext: BondedPoolsContextState = { + fetchPoolsMetaBatch: (k, v: [], r) => {}, + queryBondedPool: (p) => {}, + getBondedPool: (p) => null, + updateBondedPools: (p) => {}, + addToBondedPools: (p) => {}, + removeFromBondedPools: (p) => {}, + getPoolNominationStatus: (n, o) => {}, + getPoolNominationStatusCode: (t) => '', + getAccountRoles: (w) => null, + getAccountPools: (w) => null, + replacePoolRoles: (p, e) => {}, + poolSearchFilter: (l, k, v) => {}, + bondedPools: [], + meta: {}, +}; diff --git a/src/contexts/Pools/BondedPools/index.tsx b/src/contexts/Pools/BondedPools/index.tsx new file mode 100644 index 0000000000..df6330040e --- /dev/null +++ b/src/contexts/Pools/BondedPools/index.tsx @@ -0,0 +1,481 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { u8aToString, u8aUnwrapBytes } from '@polkadot/util'; +import { setStateWithRef, shuffle } from '@polkadot-cloud/utils'; +import React, { useRef, useState } from 'react'; +import type { + BondedPool, + BondedPoolsContextState, + MaybePool, + NominationStatuses, +} from 'contexts/Pools/types'; +import { useStaking } from 'contexts/Staking'; +import type { AnyApi, AnyMetaBatch, Fn, MaybeAddress } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useApi } from '../../Api'; +import { usePoolsConfig } from '../PoolsConfig'; +import { defaultBondedPoolsContext } from './defaults'; + +export const BondedPoolsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + const { api, isReady } = useApi(); + const { createAccounts, stats } = usePoolsConfig(); + const { getNominationsStatusFromTargets } = useStaking(); + const { lastPoolId } = stats; + + // Stores the meta data batches for pool lists. + const [poolMetaBatches, setPoolMetaBatch]: AnyMetaBatch = useState({}); + const poolMetaBatchesRef = useRef(poolMetaBatches); + + // Stores the meta batch subscriptions for pool lists. + const poolSubs = useRef<Record<string, Fn[]>>({}); + + // Store bonded pools. + const [bondedPools, setBondedPools] = useState<BondedPool[]>([]); + + const unsubscribe = () => { + Object.values(poolSubs.current).map((batch: Fn[]) => + Object.entries(batch).map(([, v]) => v()) + ); + setBondedPools([]); + }; + + // fetch all bonded pool entries + const fetchBondedPools = async () => { + if (!api) return; + + const result = await api.query.nominationPools.bondedPools.entries(); + let exposures = result.map(([_keys, _val]: AnyApi) => { + const id = _keys.toHuman()[0]; + const pool = _val.toHuman(); + return getPoolWithAddresses(id, pool); + }); + + exposures = shuffle(exposures); + setBondedPools(exposures); + }; + + // queries a bonded pool and injects ID and addresses to a result. + const queryBondedPool = async (id: number) => { + if (!api) return null; + + const bondedPool: AnyApi = ( + await api.query.nominationPools.bondedPools(id) + ).toHuman(); + + if (!bondedPool) { + return null; + } + return { + id, + addresses: createAccounts(id), + ...bondedPool, + }; + }; + + /* + Fetches a new batch of pool metadata. + Fetches the metadata of a pool that we assume to be a string. + structure: + { + key: { + [ + { + metadata: [], + } + ] + }, + }; + */ + const fetchPoolsMetaBatch = async ( + key: string, + p: AnyMetaBatch, + refetch = false + ) => { + if (!isReady || !api || !p.length) return; + + if (!refetch) { + // if already exists, do not re-fetch + if (poolMetaBatchesRef.current[key] !== undefined) { + return; + } + } else { + // tidy up if existing batch exists + const updatedPoolMetaBatches: AnyMetaBatch = { + ...poolMetaBatchesRef.current, + }; + delete updatedPoolMetaBatches[key]; + setStateWithRef( + updatedPoolMetaBatches, + setPoolMetaBatch, + poolMetaBatchesRef + ); + + if (poolSubs.current[key] !== undefined) { + for (const unsub of poolSubs.current[key]) { + unsub(); + } + } + } + + // aggregate pool ids and addresses + const ids = []; + const addresses = []; + for (const pool of p) { + ids.push(Number(pool.id)); + + if (pool?.addresses?.stash) { + addresses.push(pool.addresses.stash); + } + } + + // store batch ids + const batchesUpdated = Object.assign(poolMetaBatchesRef.current); + batchesUpdated[key] = {}; + batchesUpdated[key].ids = ids; + setStateWithRef( + { ...batchesUpdated }, + setPoolMetaBatch, + poolMetaBatchesRef + ); + + const subscribeToMetadata = async (_ids: AnyApi) => { + const unsub = await api.query.nominationPools.metadata.multi( + _ids, + (_metadata: AnyApi) => { + const metadata = []; + for (let i = 0; i < _metadata.length; i++) { + metadata.push(_metadata[i].toHuman()); + } + const updated = Object.assign(poolMetaBatchesRef.current); + updated[key].metadata = metadata; + + setStateWithRef({ ...updated }, setPoolMetaBatch, poolMetaBatchesRef); + } + ); + return unsub; + }; + + const subscribeToNominations = async (_addresses: AnyApi) => { + const unsub = await api.query.staking.nominators.multi( + _addresses, + (_nominations: AnyApi) => { + const nominations = []; + for (let i = 0; i < _nominations.length; i++) { + nominations.push(_nominations[i].toHuman()); + } + const updated = Object.assign(poolMetaBatchesRef.current); + updated[key].nominations = nominations; + setStateWithRef({ ...updated }, setPoolMetaBatch, poolMetaBatchesRef); + } + ); + return unsub; + }; + + // initiate subscriptions + await Promise.all([ + subscribeToMetadata(ids), + subscribeToNominations(addresses), + ]).then((unsubs: Fn[]) => { + addMetaBatchUnsubs(key, unsubs); + }); + }; + + /* + * Get bonded pool nomination statuses + */ + const getPoolNominationStatus = ( + nominator: MaybeAddress, + nomination: MaybeAddress + ) => { + const pool = bondedPools.find((p: any) => p.addresses.stash === nominator); + + if (!pool) { + return 'waiting'; + } + // get pool targets from nominations metadata + const batchIndex = bondedPools.indexOf(pool); + const nominations = poolMetaBatches.bonded_pools?.nominations ?? []; + const targets = nominations[batchIndex]?.targets ?? []; + + const target = targets.find((t: string) => t === nomination); + + const nominationStatus = getNominationsStatusFromTargets(nominator, [ + target, + ]); + + return getPoolNominationStatusCode(nominationStatus); + }; + + /* + * Determine bonded pool's current nomination statuse + */ + const getPoolNominationStatusCode = (statuses: NominationStatuses | null) => { + let status = 'waiting'; + + if (statuses) { + for (const childStatus of Object.values(statuses)) { + if (childStatus === 'active') { + status = 'active'; + break; + } + if (childStatus === 'inactive') { + status = 'inactive'; + } + } + } + return status; + }; + + /* + * Helper: to add mataBatch unsubs by key. + */ + const addMetaBatchUnsubs = (key: string, unsubs: Fn[]) => { + const newUnsubs = poolSubs.current; + const newUnsubItem = newUnsubs[key] ?? []; + + newUnsubItem.push(...unsubs); + newUnsubs[key] = newUnsubItem; + poolSubs.current = newUnsubs; + }; + + /* + * Helper: to add addresses to pool record. + */ + const getPoolWithAddresses = (id: number, pool: BondedPool) => ({ + ...pool, + id, + addresses: createAccounts(id), + }); + + const getBondedPool = (poolId: MaybePool) => + bondedPools.find((p) => p.id === String(poolId)) ?? null; + + /* + * poolSearchFilter + * Iterates through the supplied list and refers to the meta + * batch of the list to filter those list items that match + * the search term. + * Returns the updated filtered list. + */ + const poolSearchFilter = ( + list: any, + batchKey: string, + searchTerm: string + ) => { + const meta = poolMetaBatchesRef.current; + + if (meta[batchKey] === undefined) { + return list; + } + const filteredList: any = []; + + for (const pool of list) { + const batchIndex = meta[batchKey].ids?.indexOf(Number(pool.id)) ?? -1; + + // if we cannot derive data, fallback to include pool in filtered list + if (batchIndex === -1) { + filteredList.push(pool); + continue; + } + + const ids = meta[batchKey].ids ?? false; + const metadatas = meta[batchKey]?.metadata ?? false; + + if (!metadatas || !ids) { + filteredList.push(pool); + continue; + } + + const id = ids[batchIndex] ?? 0; + const address = pool?.addresses?.stash ?? ''; + const metadata = metadatas[batchIndex] ?? ''; + + const metadataAsBytes = u8aToString(u8aUnwrapBytes(metadata)); + const metadataSearch = ( + metadataAsBytes === '' ? metadata : metadataAsBytes + ).toLowerCase(); + + if (String(id).includes(searchTerm.toLowerCase())) { + filteredList.push(pool); + } + if (address.toLowerCase().includes(searchTerm.toLowerCase())) { + filteredList.push(pool); + } + if (metadataSearch.includes(searchTerm.toLowerCase())) { + filteredList.push(pool); + } + } + return filteredList; + }; + + const updateBondedPools = (updatedPools: BondedPool[]) => { + if (!updatedPools) return; + + setBondedPools( + bondedPools.map( + (original) => + updatedPools.find((updated) => updated.id === original.id) || original + ) + ); + }; + + const removeFromBondedPools = (id: number) => { + setBondedPools(bondedPools.filter((b: BondedPool) => b.id !== id)); + }; + + // adds a record to bondedPools. + // currently only used when a new pool is created. + const addToBondedPools = (pool: BondedPool) => { + if (!pool) return; + + const exists = bondedPools.find((b) => b.id === pool.id); + if (!exists) setBondedPools(bondedPools.concat(pool)); + }; + + // get all the roles belonging to one pool account + const getAccountRoles = (who: MaybeAddress) => { + if (!who) { + return { + depositor: [], + root: [], + nominator: [], + bouncer: [], + }; + } + + const depositor = bondedPools + .filter((b) => b.roles.depositor === who) + .map((b) => b.id); + + const root = bondedPools + .filter((b: BondedPool) => b.roles.root === who) + .map((b) => b.id); + + const nominator = bondedPools + .filter((b) => b.roles.nominator === who) + .map((b) => b.id); + + const bouncer = bondedPools + .filter((b) => b.roles.bouncer === who) + .map((b) => b.id); + + return { + depositor, + root, + nominator, + bouncer, + }; + }; + + // accumulate account pool list + const getAccountPools = (who: MaybeAddress) => { + // first get the roles of the account + const roles = getAccountRoles(who); + // format new list has pool => roles + const pools: any = {}; + Object.entries(roles).forEach(([key, poolIds]: any) => { + // now looping through a role + poolIds.forEach((poolId: string) => { + const exists = Object.keys(pools).find((k) => k === poolId); + if (!exists) { + pools[poolId] = [key]; + } else { + pools[poolId].push(key); + } + }); + }); + return pools; + }; + + // determine roles to replace from roleEdits + const toReplace = (roleEdits: any) => { + const root = roleEdits?.root?.newAddress ?? ''; + const nominator = roleEdits?.nominator?.newAddress ?? ''; + const bouncer = roleEdits?.bouncer?.newAddress ?? ''; + + return { + root, + nominator, + bouncer, + }; + }; + + // replaces the pool roles from roleEdits + const replacePoolRoles = (poolId: number, roleEdits: any) => { + let pool = bondedPools.find((b) => String(b.id) === String(poolId)) || null; + + if (!pool) return; + + pool = { + ...pool, + roles: { + ...pool.roles, + ...toReplace(roleEdits), + }, + }; + + const newBondedPools = [ + ...bondedPools.map((b) => + String(b.id) === String(poolId) && pool !== null ? pool : b + ), + ]; + + setBondedPools(newBondedPools); + }; + + // Clear existing state for network refresh. + useEffectIgnoreInitial(() => { + setBondedPools([]); + setStateWithRef({}, setPoolMetaBatch, poolMetaBatchesRef); + }, [network]); + + // Initial setup for fetching bonded pools. + useEffectIgnoreInitial(() => { + if (isReady) fetchBondedPools(); + return () => { + unsubscribe(); + }; + }, [network, isReady, lastPoolId]); + + // After bonded pools have synced, fetch metabatch. + useEffectIgnoreInitial(() => { + if (bondedPools.length) + fetchPoolsMetaBatch('bonded_pools', bondedPools, true); + }, [bondedPools]); + + return ( + <BondedPoolsContext.Provider + value={{ + fetchPoolsMetaBatch, + queryBondedPool, + getBondedPool, + updateBondedPools, + addToBondedPools, + removeFromBondedPools, + getPoolNominationStatus, + getPoolNominationStatusCode, + getAccountRoles, + getAccountPools, + replacePoolRoles, + poolSearchFilter, + bondedPools, + meta: poolMetaBatchesRef.current, + }} + > + {children} + </BondedPoolsContext.Provider> + ); +}; + +export const BondedPoolsContext = React.createContext<BondedPoolsContextState>( + defaultBondedPoolsContext +); + +export const useBondedPools = () => React.useContext(BondedPoolsContext); diff --git a/src/contexts/Pools/PoolMembers/defaults.ts b/src/contexts/Pools/PoolMembers/defaults.ts new file mode 100644 index 0000000000..8cb8fbce28 --- /dev/null +++ b/src/contexts/Pools/PoolMembers/defaults.ts @@ -0,0 +1,20 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { PoolMemberContext } from '../types'; + +export const defaultPoolMembers: PoolMemberContext = { + fetchPoolMembersMetaBatch: (k, v, r) => {}, + queryPoolMember: (w) => {}, + getMembersOfPoolFromNode: (p) => {}, + addToPoolMembers: (m) => {}, + removePoolMember: (w) => {}, + getPoolMemberCount: (p) => 0, + poolMembersApi: [], + setPoolMembersApi: (p) => {}, + poolMembersNode: [], + meta: {}, + fetchedPoolMembersApi: 'unsynced', + setFetchedPoolMembersApi: (s) => {}, +}; diff --git a/src/contexts/Pools/PoolMembers/index.tsx b/src/contexts/Pools/PoolMembers/index.tsx new file mode 100644 index 0000000000..bc3e9af98c --- /dev/null +++ b/src/contexts/Pools/PoolMembers/index.tsx @@ -0,0 +1,338 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import React, { useRef, useState } from 'react'; +import { usePlugins } from 'contexts/Plugins'; +import type { PoolMember, PoolMemberContext } from 'contexts/Pools/types'; +import type { AnyApi, AnyMetaBatch, Fn, MaybeAddress, Sync } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useApi } from '../../Api'; +import { defaultPoolMembers } from './defaults'; + +export const PoolMembersProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + const { api, isReady } = useApi(); + const { pluginEnabled } = usePlugins(); + const { activeAccount } = useActiveAccounts(); + + // Store pool members from node. + const [poolMembersNode, setPoolMembersNode] = useState<PoolMember[]>([]); + + // Store pool members from api. + const [poolMembersApi, setPoolMembersApi] = useState<PoolMember[]>([]); + + // Store whether pool members from api have been fetched. + const fetchedPoolMembersApi = useRef<Sync>('unsynced'); + + // Stores the meta data batches for pool member lists. + const [poolMembersMetaBatches, setPoolMembersMetaBatch]: AnyMetaBatch = + useState({}); + const poolMembersMetaBatchesRef = useRef(poolMembersMetaBatches); + + // Stores the meta batch subscriptions for pool lists. + const poolMembersSubs = useRef<Record<string, Fn[]>>({}); + + // Update poolMembersApi fetched status. + const setFetchedPoolMembersApi = (status: Sync) => { + fetchedPoolMembersApi.current = status; + }; + + // Clear existing state for network refresh + useEffectIgnoreInitial(() => { + setPoolMembersNode([]); + unsubscribeAndResetMeta(); + }, [network]); + + // Clear meta state when activeAccount changes + useEffectIgnoreInitial(() => { + unsubscribeAndResetMeta(); + }, [activeAccount]); + + // Initial setup for fetching members if Subscan is not enabled. Ensure poolMembers are reset if + // subscan is disabled. + useEffectIgnoreInitial(() => { + if (!pluginEnabled('subscan')) { + if (isReady) fetchPoolMembers(); + } else { + setPoolMembersNode([]); + } + return () => { + unsubscribe(); + }; + }, [network, isReady, pluginEnabled('subscan')]); + + const unsubscribe = () => { + unsubscribeAndResetMeta(); + setPoolMembersNode([]); + }; + + const unsubscribeAndResetMeta = () => { + Object.values(poolMembersSubs.current).map((batch: Fn[]) => + Object.entries(batch).map(([, v]) => v()) + ); + setStateWithRef({}, setPoolMembersMetaBatch, poolMembersMetaBatchesRef); + }; + + // Fetch all pool members entries from node. + const fetchPoolMembers = async () => { + if (!api) return; + + const result = await api.query.nominationPools.poolMembers.entries(); + const newMembers = result.map(([keys, val]: AnyApi) => { + const who = keys.toHuman()[0]; + const { poolId } = val.toHuman(); + return { + who, + poolId, + }; + }); + setPoolMembersNode(newMembers); + }; + + const getMembersOfPoolFromNode = (poolId: number) => + poolMembersNode.filter((p: any) => p.poolId === String(poolId)) ?? null; + + // queries a pool member and formats to `PoolMember`. + const queryPoolMember = async (who: MaybeAddress) => { + if (!api) return null; + + const poolMember: AnyApi = ( + await api.query.nominationPools.poolMembers(who) + ).toHuman(); + + if (!poolMember) { + return null; + } + return { + who, + poolId: poolMember.poolId, + }; + }; + + // Gets the count of members in a pool from node data. + const getPoolMemberCount = (poolId: number) => + getMembersOfPoolFromNode(poolId ?? 0).length; + + /* + Fetches a new batch of pool member metadata. + structure: + { + key: { + [ + { + identities: [], + super_identities: [], + } + ] + }, + }; + */ + const fetchPoolMembersMetaBatch = async ( + key: string, + p: AnyMetaBatch, + refetch = false + ) => { + if (!isReady || !api) { + return; + } + if (!p.length) { + return; + } + if (!refetch) { + // if already exists, do not re-fetch + if (poolMembersMetaBatchesRef.current[key] !== undefined) { + return; + } + } else { + // tidy up if existing batch exists + const updatedPoolMembersMetaBatches: AnyMetaBatch = { + ...poolMembersMetaBatchesRef.current, + }; + delete updatedPoolMembersMetaBatches[key]; + setStateWithRef( + updatedPoolMembersMetaBatches, + setPoolMembersMetaBatch, + poolMembersMetaBatchesRef + ); + + if (poolMembersSubs.current[key] !== undefined) { + for (const unsub of poolMembersSubs.current[key]) { + unsub(); + } + } + } + + // aggregate member addresses + const addresses = []; + for (const { who } of p) { + addresses.push(who); + } + + // store batch addresses + const batchesUpdated = Object.assign(poolMembersMetaBatchesRef.current); + batchesUpdated[key] = {}; + batchesUpdated[key].addresses = addresses; + setStateWithRef( + { ...batchesUpdated }, + setPoolMembersMetaBatch, + poolMembersMetaBatchesRef + ); + + const subscribeToPoolMembers = async (addr: string[]) => { + const unsub = await api.query.nominationPools.poolMembers.multi<AnyApi>( + addr, + (_pools) => { + const pools = []; + for (let i = 0; i < _pools.length; i++) { + pools.push(_pools[i].toHuman()); + } + const updated = Object.assign(poolMembersMetaBatchesRef.current); + updated[key].poolMembers = pools; + setStateWithRef( + { ...updated }, + setPoolMembersMetaBatch, + poolMembersMetaBatchesRef + ); + } + ); + return unsub; + }; + + const subscribeToIdentities = async (addr: string[]) => { + const unsub = await api.query.identity.identityOf.multi<AnyApi>( + addr, + (_identities) => { + const identities = []; + for (let i = 0; i < _identities.length; i++) { + identities.push(_identities[i].toHuman()); + } + const updated = Object.assign(poolMembersMetaBatchesRef.current); + updated[key].identities = identities; + setStateWithRef( + { ...updated }, + setPoolMembersMetaBatch, + poolMembersMetaBatchesRef + ); + } + ); + return unsub; + }; + + const subscribeToSuperIdentities = async (addr: string[]) => { + const unsub = await api.query.identity.superOf.multi<AnyApi>( + addr, + async (result) => { + // determine where supers exist + const supers: AnyApi = []; + const supersWithIdentity: AnyApi = []; + + for (let i = 0; i < result.length; i++) { + const item = result[i].toHuman(); + supers.push(item); + if (item !== null) { + supersWithIdentity.push(i); + } + } + + // get supers one-off multi query + const query = supers + .filter((s: AnyApi) => s !== null) + .map((s: AnyApi) => s[0]); + + const temp = await api.query.identity.identityOf.multi<AnyApi>( + query, + (_identities) => { + for (let j = 0; j < _identities.length; j++) { + const identity = _identities[j].toHuman(); + // inject identity into super array + supers[supersWithIdentity[j]].identity = identity; + } + } + ); + temp(); + + const updated = Object.assign(poolMembersMetaBatchesRef.current); + updated[key].supers = supers; + setStateWithRef( + { ...updated }, + setPoolMembersMetaBatch, + poolMembersMetaBatchesRef + ); + } + ); + return unsub; + }; + + // initiate subscriptions + await Promise.all([ + subscribeToIdentities(addresses), + subscribeToSuperIdentities(addresses), + subscribeToPoolMembers(addresses), + ]).then((unsubs: Fn[]) => { + addMetaBatchUnsubs(key, unsubs); + }); + }; + + // Removes a member from the member list and updates state. + const removePoolMember = (who: MaybeAddress) => { + if (!pluginEnabled('subscan')) return; + + const newMembers = poolMembersNode.filter((p: any) => p.who !== who); + setPoolMembersNode(newMembers ?? []); + }; + + // Adds a record to poolMembers. + // Currently only used when an account joins or creates a pool. + const addToPoolMembers = (member: any) => { + if (!member || pluginEnabled('subscan')) return; + + const exists = poolMembersNode.find((m: any) => m.who === member.who); + if (!exists) { + setPoolMembersNode(poolMembersNode.concat(member)); + } + }; + + /* + * Helper: to add mataBatch unsubs by key. + */ + const addMetaBatchUnsubs = (key: string, unsubs: Fn[]) => { + const subs = poolMembersSubs.current; + const sub = subs[key] ?? []; + sub.push(...unsubs); + subs[key] = sub; + poolMembersSubs.current = subs; + }; + + return ( + <PoolMembersContext.Provider + value={{ + fetchPoolMembersMetaBatch, + queryPoolMember, + getMembersOfPoolFromNode, + addToPoolMembers, + removePoolMember, + getPoolMemberCount, + poolMembersNode, + poolMembersApi, + setPoolMembersApi, + fetchedPoolMembersApi: fetchedPoolMembersApi.current, + meta: poolMembersMetaBatchesRef.current, + setFetchedPoolMembersApi, + }} + > + {children} + </PoolMembersContext.Provider> + ); +}; + +export const PoolMembersContext = + React.createContext<PoolMemberContext>(defaultPoolMembers); + +export const usePoolMembers = () => React.useContext(PoolMembersContext); diff --git a/src/contexts/Pools/PoolMemberships/defaults.ts b/src/contexts/Pools/PoolMemberships/defaults.ts new file mode 100644 index 0000000000..b708431b9a --- /dev/null +++ b/src/contexts/Pools/PoolMemberships/defaults.ts @@ -0,0 +1,12 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { PoolMembership, PoolMembershipsContextState } from '../types'; + +export const poolMembership: PoolMembership | null = null; + +export const defaultPoolMembershipsContext: PoolMembershipsContextState = { + memberships: [], + membership: null, + claimPermissionConfig: [], +}; diff --git a/src/contexts/Pools/PoolMemberships/index.tsx b/src/contexts/Pools/PoolMemberships/index.tsx new file mode 100644 index 0000000000..4ef225fbed --- /dev/null +++ b/src/contexts/Pools/PoolMemberships/index.tsx @@ -0,0 +1,194 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { rmCommas, setStateWithRef } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { + ClaimPermissionConfig, + PoolMembership, + PoolMembershipsContextState, +} from 'contexts/Pools/types'; +import type { AnyApi, Fn } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useApi } from '../../Api'; +import * as defaults from './defaults'; + +export const PoolMembershipsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { t } = useTranslation('base'); + const { network } = useNetwork(); + const { api, isReady } = useApi(); + const { activeAccount } = useActiveAccounts(); + const { accounts: connectAccounts } = useImportedAccounts(); + + // Stores pool memberships for the imported accounts. + const [poolMemberships, setPoolMemberships] = useState<PoolMembership[]>([]); + const poolMembershipsRef = useRef(poolMemberships); + + // Stores pool membership unsubs. + const unsubs = useRef<AnyApi[]>([]); + + useEffectIgnoreInitial(() => { + if (isReady) { + (() => { + setStateWithRef([], setPoolMemberships, poolMembershipsRef); + unsubAll(); + getPoolMemberships(); + })(); + } + }, [network, isReady, connectAccounts]); + + // subscribe to account pool memberships + const getPoolMemberships = async () => { + Promise.all( + connectAccounts.map(({ address }) => subscribeToPoolMembership(address)) + ); + }; + + // unsubscribe from pool memberships on unmount + useEffect( + () => () => { + unsubAll(); + }, + [] + ); + + // unsubscribe from all pool memberships + const unsubAll = () => { + Object.values(unsubs.current).forEach((v: Fn) => v()); + }; + + // subscribe to an account's pool membership + const subscribeToPoolMembership = async (address: string) => { + if (!api) return undefined; + + const unsub = await api.queryMulti<AnyApi>( + [ + [api.query.nominationPools.poolMembers, address], + [api.query.nominationPools.claimPermissions, address], + ], + ([poolMember, claimPermission]) => { + handleMembership(poolMember, claimPermission); + } + ); + + const handleMembership = async ( + poolMember: AnyApi, + claimPermission?: AnyApi + ) => { + let membership = poolMember?.unwrapOr(undefined)?.toHuman(); + + if (membership) { + // format pool's unlocking chunks + const unbondingEras: AnyApi = membership.unbondingEras; + const unlocking = []; + for (const [e, v] of Object.entries(unbondingEras || {})) { + unlocking.push({ + era: Number(rmCommas(e as string)), + value: new BigNumber(rmCommas(v as string)), + }); + } + membership.points = membership.points + ? rmCommas(membership.points) + : '0'; + + const balance = + ( + await api.call.nominationPoolsApi.pointsToBalance( + membership.poolId, + membership.points + ) + )?.toString() || '0'; + + membership = { + ...membership, + balance: new BigNumber(balance), + address, + unlocking, + claimPermission: claimPermission?.toString() || 'Permissioned', + }; + + // remove stale membership if it's already in list, and add to memberships. + setStateWithRef( + Object.values(poolMembershipsRef.current) + .filter((m) => m.address !== address) + .concat(membership), + setPoolMemberships, + poolMembershipsRef + ); + } else { + // no membership: remove account membership if present. + setStateWithRef( + Object.values(poolMembershipsRef.current).filter( + (m) => m.address !== address + ), + setPoolMemberships, + poolMembershipsRef + ); + } + }; + + unsubs.current = unsubs.current.concat(unsub); + return unsub; + }; + + // gets the membership of the active account + const getActiveAccountPoolMembership = () => { + if (!activeAccount) { + return defaults.poolMembership; + } + const poolMembership = poolMembershipsRef.current.find( + (m) => m.address === activeAccount + ); + if (poolMembership === undefined) { + return defaults.poolMembership; + } + return poolMembership; + }; + + 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'), + }, + ]; + + return ( + <PoolMembershipsContext.Provider + value={{ + membership: getActiveAccountPoolMembership(), + memberships: poolMembershipsRef.current, + claimPermissionConfig, + }} + > + {children} + </PoolMembershipsContext.Provider> + ); +}; + +export const PoolMembershipsContext = + React.createContext<PoolMembershipsContextState>( + defaults.defaultPoolMembershipsContext + ); + +export const usePoolMemberships = () => + React.useContext(PoolMembershipsContext); diff --git a/src/contexts/Pools/PoolPerformance/defaults.ts b/src/contexts/Pools/PoolPerformance/defaults.ts new file mode 100644 index 0000000000..a1e9bd2420 --- /dev/null +++ b/src/contexts/Pools/PoolPerformance/defaults.ts @@ -0,0 +1,10 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { PoolPerformanceContextInterface } from './types'; + +export const defaultPoolPerformanceContext: PoolPerformanceContextInterface = { + poolRewardPointsFetched: 'unsynced', + poolRewardPoints: {}, +}; diff --git a/src/contexts/Pools/PoolPerformance/index.tsx b/src/contexts/Pools/PoolPerformance/index.tsx new file mode 100644 index 0000000000..49e16e51d4 --- /dev/null +++ b/src/contexts/Pools/PoolPerformance/index.tsx @@ -0,0 +1,145 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useState } from 'react'; +import { MaxEraRewardPointsEras } from 'consts'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/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 { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useApi } from 'contexts/Api'; +import type { Sync } from '@polkadot-cloud/react/types'; +import BigNumber from 'bignumber.js'; +import { formatRawExposures } from 'contexts/Staking/Utils'; +import { mergeDeep } from '@polkadot-cloud/utils'; +import type { PoolPerformanceContextInterface } from './types'; +import { defaultPoolPerformanceContext } from './defaults'; + +const worker = new Worker(); + +export const PoolPerformanceProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { api } = useApi(); + const { network } = useNetwork(); + const { bondedPools } = useBondedPools(); + const { activeEra } = useNetworkMetrics(); + const { erasRewardPointsFetched, erasRewardPoints } = useValidators(); + + // Store whether pool performance data is being fetched. + const [poolRewardPointsFetched, setPoolRewardPointsFetched] = + useState<Sync>('unsynced'); + + // Store pool performance data. + const [poolRewardPoints, setPoolRewardPoints] = useState< + Record<string, Record<string, string>> + >({}); + + // Store the currently active era being processed for pool performance. + const [currentEra, setCurrentEra] = useState<BigNumber>(new BigNumber(0)); + + // Store the earliest era that should be processed. + const [finishEra, setFinishEra] = useState<BigNumber>(new BigNumber(0)); + + // Handle worker message on completed exposure check. + worker.onmessage = (message: MessageEvent) => { + if (message) { + const { data } = message; + const { task } = data; + if (task !== 'processNominationPoolsRewardData') return; + + // Update state with new data. + const { poolRewardData } = data; + setPoolRewardPoints(mergeDeep(poolRewardPoints, poolRewardData)); + + if (currentEra.isEqualTo(finishEra)) { + setPoolRewardPointsFetched('synced'); + } else { + const nextEra = BigNumber.max(currentEra.minus(1), 1); + processEra(nextEra); + } + } + }; + + // Start fetching pool performance calls from the current era. + const startGetPoolPerformance = async () => { + setPoolRewardPointsFetched('syncing'); + setFinishEra( + BigNumber.max(activeEra.index.minus(MaxEraRewardPointsEras), 1) + ); + const startEra = BigNumber.max(activeEra.index.minus(1), 1); + processEra(startEra); + }; + + // Get era data and send to worker. + const processEra = async (era: BigNumber) => { + if (!api) return; + setCurrentEra(era); + const result = await api.query.staking.erasStakersClipped.entries( + era.toString() + ); + const exposures = formatRawExposures(result); + worker.postMessage({ + task: 'processNominationPoolsRewardData', + era: era.toString(), + exposures, + bondedPools: bondedPools.map((b) => b.addresses.stash), + 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(); + } + }, [ + bondedPools, + activeEra, + erasRewardPointsFetched, + poolRewardPointsFetched, + ]); + + // Reset state data on network change. + useEffectIgnoreInitial(() => { + setPoolRewardPoints({}); + setCurrentEra(new BigNumber(0)); + setFinishEra(new BigNumber(0)); + setPoolRewardPointsFetched('unsynced'); + }, [network]); + + return ( + <PoolPerformanceContext.Provider + value={{ + poolRewardPointsFetched, + poolRewardPoints, + }} + > + {children} + </PoolPerformanceContext.Provider> + ); +}; + +export const PoolPerformanceContext = + React.createContext<PoolPerformanceContextInterface>( + defaultPoolPerformanceContext + ); + +export const usePoolPerformance = () => + React.useContext(PoolPerformanceContext); diff --git a/src/contexts/Pools/PoolPerformance/types.ts b/src/contexts/Pools/PoolPerformance/types.ts new file mode 100644 index 0000000000..acef8792bb --- /dev/null +++ b/src/contexts/Pools/PoolPerformance/types.ts @@ -0,0 +1,10 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyJson } from '@polkadot-cloud/react/types'; +import type { Sync } from 'types'; + +export interface PoolPerformanceContextInterface { + poolRewardPointsFetched: Sync; + poolRewardPoints: AnyJson; +} diff --git a/src/contexts/Pools/PoolsConfig/defaults.ts b/src/contexts/Pools/PoolsConfig/defaults.ts new file mode 100644 index 0000000000..e726d5e388 --- /dev/null +++ b/src/contexts/Pools/PoolsConfig/defaults.ts @@ -0,0 +1,35 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import type { + PoolAddresses, + PoolsConfigContextState, + PoolStats, +} from 'contexts/Pools/types'; + +export const stats: PoolStats = { + counterForPoolMembers: new BigNumber(0), + counterForBondedPools: new BigNumber(0), + counterForRewardPools: new BigNumber(0), + lastPoolId: new BigNumber(0), + maxPoolMembers: null, + maxPoolMembersPerPool: null, + maxPools: null, + minCreateBond: new BigNumber(0), + minJoinBond: new BigNumber(0), + globalMaxCommission: 0, +}; + +export const defaultPoolsConfigContext: PoolsConfigContextState = { + addFavorite: () => {}, + removeFavorite: () => {}, + createAccounts: () => poolAddresses, + favorites: [], + stats, +}; + +export const poolAddresses: PoolAddresses = { + stash: '', + reward: '', +}; diff --git a/src/contexts/Pools/PoolsConfig/index.tsx b/src/contexts/Pools/PoolsConfig/index.tsx new file mode 100644 index 0000000000..9b2f38a56a --- /dev/null +++ b/src/contexts/Pools/PoolsConfig/index.tsx @@ -0,0 +1,218 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { bnToU8a, u8aConcat } from '@polkadot/util'; +import { rmCommas, setStateWithRef } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import BN from 'bn.js'; +import React, { useRef, useState } from 'react'; +import { EmptyH256, ModPrefix, U32Opts } from 'consts'; +import type { + PoolConfigState, + PoolsConfigContextState, +} from 'contexts/Pools/types'; +import type { AnyApi } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useApi } from '../../Api'; +import * as defaults from './defaults'; + +export const PoolsConfigProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + const { api, isReady, consts } = useApi(); + const { poolsPalletId } = consts; + + // store pool metadata + const [poolsConfig, setPoolsConfig] = useState<PoolConfigState>({ + stats: defaults.stats, + unsub: null, + }); + const poolsConfigRef = useRef(poolsConfig); + + // get favorite pools from local storage. + const getLocalFavorites = () => { + const localFavorites = localStorage.getItem(`${network}_favorite_pools`); + return localFavorites !== null ? JSON.parse(localFavorites) : []; + }; + + // stores the user's favorite pools + const [favorites, setFavorites] = useState<string[]>(getLocalFavorites()); + + useEffectIgnoreInitial(() => { + if (isReady) { + subscribeToPoolConfig(); + } + return () => { + unsubscribe(); + }; + }, [network, isReady]); + + const unsubscribe = () => { + if (poolsConfigRef.current.unsub !== null) { + poolsConfigRef.current.unsub(); + } + }; + + // subscribe to pool chain state + const subscribeToPoolConfig = async () => { + if (!api) return; + + const unsub = await api.queryMulti<AnyApi>( + [ + api.query.nominationPools.counterForPoolMembers, + api.query.nominationPools.counterForBondedPools, + api.query.nominationPools.counterForRewardPools, + api.query.nominationPools.lastPoolId, + api.query.nominationPools.maxPoolMembers, + api.query.nominationPools.maxPoolMembersPerPool, + api.query.nominationPools.maxPools, + api.query.nominationPools.minCreateBond, + api.query.nominationPools.minJoinBond, + api.query.nominationPools.globalMaxCommission, + ], + ([ + counterForPoolMembers, + counterForBondedPools, + counterForRewardPools, + lastPoolId, + maxPoolMembers, + maxPoolMembersPerPool, + maxPools, + minCreateBond, + minJoinBond, + globalMaxCommission, + ]) => { + // format optional configs to BigNumber or null + maxPoolMembers = maxPoolMembers.toHuman(); + if (maxPoolMembers !== null) { + maxPoolMembers = new BigNumber(rmCommas(maxPoolMembers)); + } + maxPoolMembersPerPool = maxPoolMembersPerPool.toHuman(); + if (maxPoolMembersPerPool !== null) { + maxPoolMembersPerPool = new BigNumber( + rmCommas(maxPoolMembersPerPool) + ); + } + maxPools = maxPools.toHuman(); + if (maxPools !== null) { + maxPools = new BigNumber(rmCommas(maxPools)); + } + + setStateWithRef( + { + ...poolsConfigRef.current, + stats: { + counterForPoolMembers: new BigNumber( + counterForPoolMembers.toString() + ), + counterForBondedPools: new BigNumber( + counterForBondedPools.toString() + ), + counterForRewardPools: new BigNumber( + counterForRewardPools.toString() + ), + lastPoolId: new BigNumber(lastPoolId.toString()), + maxPoolMembers, + maxPoolMembersPerPool, + maxPools, + minCreateBond: new BigNumber(minCreateBond.toString()), + minJoinBond: new BigNumber(minJoinBond.toString()), + globalMaxCommission: Number( + globalMaxCommission.toHuman().slice(0, -1) + ), + }, + }, + setPoolsConfig, + poolsConfigRef + ); + } + ); + + setStateWithRef( + { + ...poolsConfigRef.current, + unsub, + }, + setPoolsConfig, + poolsConfigRef + ); + }; + + /* + * Adds a favorite validator. + */ + const addFavorite = (address: string) => { + const newFavorites = Object.assign(favorites); + if (!newFavorites.includes(address)) newFavorites.push(address); + + localStorage.setItem( + `${network}_favorite_pools`, + JSON.stringify(newFavorites) + ); + setFavorites([...newFavorites]); + }; + + /* + * Removes a favorite validator if they exist. + */ + const removeFavorite = (address: string) => { + let newFavorites = Object.assign(favorites); + newFavorites = newFavorites.filter( + (validator: string) => validator !== address + ); + localStorage.setItem( + `${network}_favorite_pools`, + JSON.stringify(newFavorites) + ); + setFavorites([...newFavorites]); + }; + + // Helper: generates pool stash and reward accounts. assumes poolsPalletId is synced. + const createAccounts = (poolId: number) => { + const poolIdBigNumber = new BigNumber(poolId); + return { + stash: createAccount(poolIdBigNumber, 0), + reward: createAccount(poolIdBigNumber, 1), + }; + }; + + const createAccount = (poolId: BigNumber, index: number): string => { + if (!api) return ''; + return api.registry + .createType( + 'AccountId32', + u8aConcat( + ModPrefix, + poolsPalletId, + new Uint8Array([index]), + bnToU8a(new BN(poolId.toString()), U32Opts), + EmptyH256 + ) + ) + .toString(); + }; + + return ( + <PoolsConfigContext.Provider + value={{ + addFavorite, + removeFavorite, + createAccounts, + favorites, + stats: poolsConfigRef.current.stats, + }} + > + {children} + </PoolsConfigContext.Provider> + ); +}; + +export const PoolsConfigContext = React.createContext<PoolsConfigContextState>( + defaults.defaultPoolsConfigContext +); + +export const usePoolsConfig = () => React.useContext(PoolsConfigContext); diff --git a/src/contexts/Pools/types.ts b/src/contexts/Pools/types.ts new file mode 100644 index 0000000000..f8c068cff4 --- /dev/null +++ b/src/contexts/Pools/types.ts @@ -0,0 +1,175 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { AnyApi, AnyJson, AnyMetaBatch, MaybeAddress, Sync } from 'types'; + +// PoolsConfig types +export interface PoolsConfigContextState { + addFavorite: (a: string) => void; + removeFavorite: (a: string) => void; + createAccounts: (p: number) => PoolAddresses; + favorites: string[]; + stats: PoolStats; +} + +export interface PoolConfigState { + stats: PoolStats; + unsub: AnyApi; +} + +export type ClaimPermission = + | 'Permissioned' + | 'PermissionlessCompound' + | 'PermissionlessWithdraw' + | 'PermissionlessAll'; + +export interface PoolStats { + counterForPoolMembers: BigNumber; + counterForBondedPools: BigNumber; + counterForRewardPools: BigNumber; + lastPoolId: BigNumber; + maxPoolMembers: BigNumber | null; + maxPoolMembersPerPool: BigNumber | null; + maxPools: BigNumber | null; + minCreateBond: BigNumber; + minJoinBond: BigNumber; + globalMaxCommission: number; +} + +// PoolMemberships types +export interface PoolMembershipsContextState { + memberships: PoolMembership[]; + membership: PoolMembership | null; + claimPermissionConfig: ClaimPermissionConfig[]; +} + +export interface PoolMembership { + address: string; + poolId: number; + points: string; + balance: BigNumber; + lastRecordedRewardCounter: string; + unbondingEras: Record<number, string>; + claimPermission: ClaimPermission; + unlocking: { + era: number; + value: BigNumber; + }[]; +} + +// BondedPool types +export interface BondedPoolsContextState { + fetchPoolsMetaBatch: (k: string, v: [], r?: boolean) => void; + queryBondedPool: (p: number) => any; + getBondedPool: (p: number) => BondedPool | null; + updateBondedPools: (p: BondedPool[]) => void; + addToBondedPools: (p: BondedPool) => void; + removeFromBondedPools: (p: number) => void; + getPoolNominationStatus: (n: MaybeAddress, o: MaybeAddress) => any; + getPoolNominationStatusCode: (t: NominationStatuses | null) => string; + getAccountRoles: (w: MaybeAddress) => any; + getAccountPools: (w: MaybeAddress) => any; + replacePoolRoles: (poolId: number, roleEdits: AnyJson) => void; + poolSearchFilter: (l: any, k: string, v: string) => void; + bondedPools: BondedPool[]; + meta: AnyMetaBatch; +} + +export interface ActivePool { + id: number; + addresses: PoolAddresses; + bondedPool: any; + rewardPool: any; + rewardAccountBalance: any; + pendingRewards: any; +} + +export interface BondedPool { + addresses: PoolAddresses; + id: number | string; + memberCounter: string; + points: string; + roles: { + depositor: string; + nominator: string; + root: string; + bouncer: string; + }; + state: PoolState; + commission?: { + current?: AnyJson | null; + max?: AnyJson | null; + changeRate: { + maxIncrease: AnyJson; + minDelay: AnyJson; + } | null; + throttleFrom?: AnyJson | null; + }; +} + +export type NominationStatuses = Record<string, string>; + +export interface ActivePoolsContextState { + isBonding: () => boolean; + isNominator: () => boolean; + isOwner: () => boolean; + isMember: () => boolean; + isDepositor: () => boolean; + isBouncer: () => boolean; + getPoolBondedAccount: () => MaybeAddress; + getPoolUnlocking: () => any; + getPoolRoles: () => PoolRoles; + setTargets: (t: any) => void; + getNominationsStatus: () => NominationStatuses; + setSelectedPoolId: (p: string) => void; + selectedActivePool: ActivePool | null; + targets: any; + poolNominations: any; + synced: Sync; + selectedPoolMemberCount: number; +} + +// PoolMembers types +export interface PoolMemberContext { + fetchPoolMembersMetaBatch: (k: string, v: AnyMetaBatch[], r: boolean) => void; + queryPoolMember: (w: MaybeAddress) => any; + getMembersOfPoolFromNode: (p: number) => any; + addToPoolMembers: (m: any) => void; + removePoolMember: (w: MaybeAddress) => void; + getPoolMemberCount: (p: number) => number; + poolMembersNode: any; + meta: AnyMetaBatch; + poolMembersApi: PoolMember[]; + setPoolMembersApi: (p: PoolMember[]) => void; + fetchedPoolMembersApi: Sync; + setFetchedPoolMembersApi: (s: Sync) => void; +} + +// Misc types +export interface PoolRoles { + depositor: string; + nominator: string; + root: string; + bouncer: string; +} + +export interface PoolAddresses { + stash: string; + reward: string; +} + +export type MaybePool = number | null; + +export type PoolState = 'Open' | 'Blocked' | 'Destroying'; + +export interface ClaimPermissionConfig { + label: string; + value: ClaimPermission; + description: string; +} + +export interface PoolMember { + poolId: number; + who: string; +} diff --git a/src/contexts/Prompt/defaults.tsx b/src/contexts/Prompt/defaults.tsx new file mode 100644 index 0000000000..4612df759e --- /dev/null +++ b/src/contexts/Prompt/defaults.tsx @@ -0,0 +1,15 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { PromptContextInterface } from './types'; + +export const defaultPromptContext: PromptContextInterface = { + openPromptWith: (o, s) => {}, + closePrompt: () => {}, + setStatus: (s) => {}, + setPrompt: (d) => {}, + size: 'small', + status: 0, + Prompt: null, +}; diff --git a/src/contexts/Prompt/index.tsx b/src/contexts/Prompt/index.tsx new file mode 100644 index 0000000000..173947fb1f --- /dev/null +++ b/src/contexts/Prompt/index.tsx @@ -0,0 +1,67 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useState } from 'react'; +import { defaultPromptContext } from './defaults'; +import type { PromptContextInterface } from './types'; + +export const PromptProvider = ({ children }: { children: React.ReactNode }) => { + const [state, setState] = useState<any>({ + size: 'large', + status: 0, + Prompt: null, + }); + + const setPrompt = (Prompt: any) => { + setState({ + ...state, + Prompt, + }); + }; + + const setStatus = (status: number) => { + setState({ + ...state, + status, + dismissOpen: status !== 0, + }); + }; + + const openPromptWith = (Prompt: any, size = 'small') => { + setState({ + ...state, + size, + Prompt, + status: 1, + }); + }; + + const closePrompt = () => { + setState({ + ...state, + status: 0, + Prompt: null, + }); + }; + + return ( + <PromptContext.Provider + value={{ + openPromptWith, + closePrompt, + setStatus, + setPrompt, + size: state.size, + status: state.status, + Prompt: state.Prompt, + }} + > + {children} + </PromptContext.Provider> + ); +}; + +export const PromptContext = + React.createContext<PromptContextInterface>(defaultPromptContext); + +export const usePrompt = () => React.useContext(PromptContext); diff --git a/src/contexts/Prompt/types.ts b/src/contexts/Prompt/types.ts new file mode 100644 index 0000000000..8d79208511 --- /dev/null +++ b/src/contexts/Prompt/types.ts @@ -0,0 +1,15 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type React from 'react'; +import type { MaybeString } from 'types'; + +export interface PromptContextInterface { + openPromptWith: (o: React.ReactNode | null, s?: string) => void; + closePrompt: () => void; + setStatus: (s: number) => void; + setPrompt: (d: MaybeString) => void; + size: string; + status: number; + Prompt: React.ReactNode | null; +} diff --git a/src/contexts/Proxies/defaults.ts b/src/contexts/Proxies/defaults.ts new file mode 100644 index 0000000000..f7aaab8608 --- /dev/null +++ b/src/contexts/Proxies/defaults.ts @@ -0,0 +1,14 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { ProxiesContextInterface } from './type'; + +export const defaultProxiesContext: ProxiesContextInterface = { + getDelegates: (a) => undefined, + getProxyDelegate: (x, y) => null, + getProxiedAccounts: (a) => [], + handleDeclareDelegate: (a) => new Promise((resolve) => resolve([])), + proxies: [], + delegates: {}, +}; diff --git a/src/contexts/Proxies/index.tsx b/src/contexts/Proxies/index.tsx new file mode 100644 index 0000000000..3289ada39e --- /dev/null +++ b/src/contexts/Proxies/index.tsx @@ -0,0 +1,301 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { VoidFn } from '@polkadot/api/types'; +import { + addedTo, + ellipsisFn, + localStorageOrDefault, + matchedProperties, + removedFrom, + rmCommas, + setStateWithRef, +} from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useRef, useState } from 'react'; +import { isSupportedProxy } from 'config/proxies'; +import { useApi } from 'contexts/Api'; +import type { AnyApi, MaybeAddress } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; +import * as defaults from './defaults'; +import type { + Delegates, + ProxiedAccounts, + Proxies, + ProxiesContextInterface, + Proxy, + ProxyDelegate, +} from './type'; + +export const ProxiesProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + const { api, isReady } = useApi(); + const { accounts } = useImportedAccounts(); + const { addExternalAccount } = useOtherAccounts(); + const { activeProxy, setActiveProxy, activeAccount } = useActiveAccounts(); + + // store the proxy accounts of each imported account. + const [proxies, setProxies] = useState<Proxies>([]); + const proxiesRef = useRef(proxies); + const unsubs = useRef<Record<string, VoidFn>>({}); + + // Handle the syncing of accounts on accounts change. + const handleSyncAccounts = () => { + // Sync removed accounts. + const handleRemovedAccounts = () => { + const removed = removedFrom(accounts, proxiesRef.current, [ + 'address', + ]).map(({ address }) => address); + + removed?.forEach((address) => { + // if delegates still exist for removed account, re-add the account as a read only system + // account. + if (delegatesRef.current[address]) { + addExternalAccount(address, 'system'); + } else { + const unsub = unsubs.current[address]; + if (unsub) unsub(); + } + }); + + unsubs.current = Object.fromEntries( + Object.entries(unsubs.current).filter(([key]) => !removed.includes(key)) + ); + }; + // Sync added accounts. + const handleAddedAccounts = () => { + addedTo(accounts, proxiesRef.current, ['address'])?.map(({ address }) => + subscribeToProxies(address) + ); + }; + // Sync existing accounts. + const handleExistingAccounts = () => { + setStateWithRef( + matchedProperties(accounts, proxiesRef.current, ['address']), + setProxies, + proxiesRef + ); + }; + handleRemovedAccounts(); + handleAddedAccounts(); + handleExistingAccounts(); + }; + + // store the delegates and the corresponding delegators + const [delegates, setDelegates] = useState<Delegates>({}); + const delegatesRef = useRef(delegates); + + const subscribeToProxies = async (address: string) => { + if (!api) return undefined; + + const unsub = await api.queryMulti<AnyApi>( + [[api.query.proxy.proxies, address]], + async ([result]) => { + const data = result.toHuman(); + const newProxies = data[0]; + const reserved = new BigNumber(rmCommas(data[1])); + + if (newProxies.length) { + setStateWithRef( + [...proxiesRef.current] + .filter(({ delegator }) => delegator !== address) + .concat({ + address, + delegator: address, + delegates: newProxies.map((d: AnyApi) => ({ + delegate: d.delegate.toString(), + proxyType: d.proxyType.toString(), + })), + reserved, + }), + setProxies, + proxiesRef + ); + } else { + // no proxies: remove stale proxies if already in list. + setStateWithRef( + [...proxiesRef.current].filter( + ({ delegator }) => delegator !== address + ), + setProxies, + proxiesRef + ); + } + } + ); + + unsubs.current[address] = unsub; + return unsub; + }; + + // Gets the delegates of the given account + const getDelegates = (address: MaybeAddress): Proxy | undefined => + proxiesRef.current.find(({ delegator }) => delegator === address) || + undefined; + + // Gets delegators and proxy types for the given delegate address + const getProxiedAccounts = (address: MaybeAddress) => { + const delegate = delegatesRef.current[address || '']; + if (!delegate) { + return []; + } + const proxiedAccounts: ProxiedAccounts = delegate + .filter(({ proxyType }) => isSupportedProxy(proxyType)) + .map(({ delegator, proxyType }) => ({ + address: delegator, + name: ellipsisFn(delegator), + proxyType, + })); + return proxiedAccounts; + }; + + // Gets the delegates and proxy type of an account, if any. + const getProxyDelegate = ( + delegator: MaybeAddress, + delegate: MaybeAddress + ): ProxyDelegate | null => + proxiesRef.current + .find((p) => p.delegator === delegator) + ?.delegates.find((d) => d.delegate === delegate) ?? null; + + // Subscribe new accounts to proxies, and remove accounts that are no longer imported. + useEffectIgnoreInitial(() => { + if (isReady) { + handleSyncAccounts(); + } + }, [accounts, isReady, network]); + + // If active proxy has not yet been set, check local storage `activeProxy` & set it as active + // proxy if it is the delegate of `activeAccount`. + useEffectIgnoreInitial(() => { + const localActiveProxy = localStorageOrDefault( + `${network}_active_proxy`, + null + ); + + if (!localActiveProxy) { + setActiveProxy(null); + } else if ( + proxiesRef.current.length && + localActiveProxy && + !activeProxy && + activeAccount + ) { + try { + const { address, proxyType } = JSON.parse(localActiveProxy); + // Add proxy address as external account if not imported. + if (!accounts.find((a) => a.address === address)) { + addExternalAccount(address, 'system'); + } + + const isActive = ( + proxiesRef.current.find( + ({ delegator }) => delegator === activeAccount + )?.delegates || [] + ).find((d) => d.delegate === address && d.proxyType === proxyType); + if (isActive) { + setActiveProxy({ address, proxyType }); + } + } catch (e) { + // Corrupt local active proxy record. Remove it. + localStorage.removeItem(`${network}_active_proxy`); + } + } + }, [accounts, activeAccount, proxiesRef.current, network]); + + // Reset active proxy state, unsubscribe from subscriptions on network change & unmount. + useEffectIgnoreInitial(() => { + setActiveProxy(null, false); + unsubAll(); + return () => unsubAll(); + }, [network]); + + const unsubAll = () => { + for (const unsub of Object.values(unsubs.current)) { + unsub(); + } + unsubs.current = {}; + }; + + // Listens to `proxies` state updates and reformats the data into a list of delegates. + useEffectIgnoreInitial(() => { + // Reformat proxiesRef.current into a list of delegates. + const newDelegates: Delegates = {}; + for (const proxy of proxiesRef.current) { + const { delegator } = proxy; + + // checking if delegator is not null to keep types happy. + if (delegator) { + // get each delegate of this proxy record. + for (const { delegate, proxyType } of proxy.delegates) { + const item = { + delegator, + proxyType, + }; + // check if this delegate exists in `newDelegates`. + if (Object.keys(newDelegates).includes(delegate)) { + // append delegator to the existing delegate record if it exists. + newDelegates[delegate].push(item); + } else { + // create a new delegate record if it does not yet exist in `newDelegates`. + newDelegates[delegate] = [item]; + } + } + } + } + + setStateWithRef(newDelegates, setDelegates, delegatesRef); + }, [proxiesRef.current]); + + // Queries the chain to check if the given delegator & delegate pair is valid proxy. + const handleDeclareDelegate = async (delegator: string) => { + if (!api) return []; + + const result: AnyApi = (await api.query.proxy.proxies(delegator)).toHuman(); + + let addDelegatorAsExternal = false; + for (const { delegate: newDelegate } of result[0] || []) { + if ( + accounts.find(({ address }) => address === newDelegate) && + !delegatesRef.current[newDelegate] + ) { + subscribeToProxies(delegator); + addDelegatorAsExternal = true; + } + } + if (addDelegatorAsExternal) { + addExternalAccount(delegator, 'system'); + } + + return []; + }; + + return ( + <ProxiesContext.Provider + value={{ + proxies: proxiesRef.current, + delegates: delegatesRef.current, + handleDeclareDelegate, + getDelegates, + getProxyDelegate, + getProxiedAccounts, + }} + > + {children} + </ProxiesContext.Provider> + ); +}; + +export const ProxiesContext = React.createContext<ProxiesContextInterface>( + defaults.defaultProxiesContext +); + +export const useProxies = () => React.useContext(ProxiesContext); diff --git a/src/contexts/Proxies/type.ts b/src/contexts/Proxies/type.ts new file mode 100644 index 0000000000..9bff84464d --- /dev/null +++ b/src/contexts/Proxies/type.ts @@ -0,0 +1,51 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { AnyJson, MaybeAddress } from 'types'; + +export type ProxyType = + | 'Any' + | 'NonTransfer' + | 'Governance' + | 'Staking' + | 'IdentityJudgement' + | 'CancelProxy' + | 'Auction'; + +export type Proxies = Proxy[]; + +export interface Proxy { + address: MaybeAddress; + delegator: MaybeAddress; + delegates: ProxyDelegate[]; + reserved: BigNumber; +} + +export interface ProxyDelegate { + delegate: string; + proxyType: ProxyType; +} +export type Delegates = Record<string, DelegateItem[]>; + +export interface DelegateItem { + delegator: string; + proxyType: ProxyType; +} + +export type ProxiedAccounts = ProxiedAccount[]; + +export interface ProxiedAccount { + address: string; + name: string; + proxyType: ProxyType; +} + +export interface ProxiesContextInterface { + getDelegates: (a: MaybeAddress) => Proxy | undefined; + getProxyDelegate: (x: MaybeAddress, y: MaybeAddress) => ProxyDelegate | null; + getProxiedAccounts: (a: MaybeAddress) => ProxiedAccounts; + handleDeclareDelegate: (delegator: string) => Promise<AnyJson[]>; + proxies: Proxies; + delegates: Delegates; +} diff --git a/src/contexts/Setup/defaults.ts b/src/contexts/Setup/defaults.ts new file mode 100644 index 0000000000..f17bd8c25d --- /dev/null +++ b/src/contexts/Setup/defaults.ts @@ -0,0 +1,41 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { + NominatorProgress, + PoolProgress, + SetupContextInterface, +} from './types'; + +export const defaultNominatorProgress: NominatorProgress = { + payee: { + destination: null, + account: null, + }, + nominations: [], + bond: '', +}; + +export const defaultPoolProgress: PoolProgress = { + metadata: '', + bond: '', + nominations: [], + roles: null, +}; + +export const defaultSetupContext: SetupContextInterface = { + getSetupProgress: (a, b) => ({ + section: 1, + progress: defaultNominatorProgress, + }), + removeSetupProgress: (a, b) => {}, + getNominatorSetupPercent: (a) => 0, + getPoolSetupPercent: (a) => 0, + setActiveAccountSetup: (t, p) => {}, + setActiveAccountSetupSection: (t, s) => {}, + setOnNominatorSetup: (v) => {}, + setOnPoolSetup: (v) => {}, + onNominatorSetup: false, + onPoolSetup: false, +}; diff --git a/src/contexts/Setup/index.tsx b/src/contexts/Setup/index.tsx new file mode 100644 index 0000000000..1d89267de4 --- /dev/null +++ b/src/contexts/Setup/index.tsx @@ -0,0 +1,252 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + greaterThanZero, + localStorageOrDefault, + unitToPlanck, +} from '@polkadot-cloud/utils'; +import React, { useState } from 'react'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import type { BondFor, MaybeAddress } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useStaking } from '../Staking'; +import { + defaultNominatorProgress, + defaultPoolProgress, + defaultSetupContext, +} from './defaults'; +import type { + NominatorProgress, + NominatorSetup, + NominatorSetups, + PoolProgress, + PoolSetup, + PoolSetups, + SetupContextInterface, +} from './types'; + +export const SetupProvider = ({ children }: { children: React.ReactNode }) => { + const { inSetup } = useStaking(); + const { + network, + networkData: { units }, + } = useNetwork(); + const { accounts } = useImportedAccounts(); + const { activeAccount } = useActiveAccounts(); + const { membership: poolMembership } = usePoolMemberships(); + + // is the user actively on the setup page + const [onNominatorSetup, setOnNominatorSetup] = useState<boolean>(false); + + // is the user actively on the pool creation page + const [onPoolSetup, setOnPoolSetup] = useState<boolean>(false); + + // Store all imported accounts nominator setups. + const [nominatorSetups, setNominatorSetups] = useState<NominatorSetups>({}); + + // Store all imported accounts pool creation setups. + const [poolSetups, setPoolSetups] = useState<PoolSetups>({}); + + // Generates the default setup objects or the currently connected accounts. Refers to local + // storage to hydrate state, falls back to defaults otherwise. + const refreshSetups = () => { + setNominatorSetups(localNominatorSetups()); + setPoolSetups(localPoolSetups()); + }; + + // Gets the setup progress for a connected account. Falls back to default setup if progress does + // not yet exist. + const getSetupProgress = ( + type: BondFor, + address: MaybeAddress + ): NominatorSetup | PoolSetup => { + const setup = Object.fromEntries( + Object.entries( + type === 'nominator' ? nominatorSetups : poolSetups + ).filter(([k]) => k === address) + ); + return ( + setup[address || ''] || { + progress: defaultProgress(type), + section: 1, + } + ); + }; + + // Remove setup progress for an account. + const removeSetupProgress = (type: BondFor, address: MaybeAddress) => { + const updatedSetups = Object.fromEntries( + Object.entries( + type === 'nominator' ? nominatorSetups : poolSetups + ).filter(([k]) => k !== address) + ); + setSetups(type, updatedSetups); + }; + + // Sets setup progress for an address. Updates localStorage followed by app state. + const setActiveAccountSetup = ( + type: BondFor, + progress: NominatorProgress | PoolProgress + ) => { + if (!activeAccount) return; + + const updatedSetups = updateSetups( + assignSetups(type), + progress, + activeAccount + ); + setSetups(type, updatedSetups); + }; + + // Sets active setup section for an address. + const setActiveAccountSetupSection = (type: BondFor, section: number) => { + if (!activeAccount) return; + + const setups = assignSetups(type); + const updatedSetups = updateSetups( + setups, + setups[activeAccount]?.progress ?? defaultProgress(type), + activeAccount, + section + ); + setSetups(type, updatedSetups); + }; + + // Utility to update the progress item of either a nominator setup or pool setup, + const updateSetups = < + T extends NominatorSetups | PoolSetups, + U extends NominatorProgress | PoolProgress, + >( + all: T, + newSetup: U, + account: string, + maybeSection?: number + ) => { + const current = Object.assign(all[account] || {}); + const section = maybeSection ?? current.section ?? 1; + + all[account] = { + ...current, + progress: newSetup, + section, + }; + + return all; + }; + + // Gets the stake setup progress as a percentage for an address. + const getNominatorSetupPercent = (address: MaybeAddress) => { + if (!address) return 0; + const setup = getSetupProgress('nominator', address) as NominatorSetup; + const { progress } = setup; + const bond = unitToPlanck(progress?.bond || '0', units); + + const p = 33; + let percentage = 0; + if (greaterThanZero(bond)) percentage += p; + if (progress.nominations.length) percentage += p; + if (progress.payee.destination !== null) percentage += p; + return percentage; + }; + + // Gets the stake setup progress as a percentage for an address. + const getPoolSetupPercent = (address: MaybeAddress) => { + if (!address) return 0; + const setup = getSetupProgress('pool', address) as PoolSetup; + const { progress } = setup; + const bond = unitToPlanck(progress?.bond || '0', units); + + const p = 25; + let percentage = 0; + if (progress.metadata !== '') percentage += p; + if (greaterThanZero(bond)) percentage += p; + if (progress.nominations.length) percentage += p; + if (progress.roles !== null) percentage += p - 1; + return percentage; + }; + + // Utility to copy the current setup state based on setup type. + const assignSetups = (type: BondFor) => + type === 'nominator' ? { ...nominatorSetups } : { ...poolSetups }; + + // Utility to get the default progress based on type. + const defaultProgress = (type: BondFor) => + type === 'nominator' ? defaultNominatorProgress : defaultPoolProgress; + + // Utility to get nominator setups, type casted as NominatorSetups. + const localNominatorSetups = () => + localStorageOrDefault('nominator_setups', {}, true) as NominatorSetups; + + // Utility to get pool setups, type casted as PoolSetups. + const localPoolSetups = () => + localStorageOrDefault('pool_setups', {}, true) as PoolSetups; + + // Utility to update setups state depending on type. + const setSetups = (type: BondFor, setups: NominatorSetups | PoolSetups) => { + setLocalSetups(type, setups); + + if (type === 'nominator') { + setNominatorSetups(setups as NominatorSetups); + } else { + setPoolSetups(setups as PoolSetups); + } + }; + + // Utility to either update local setups or remove if empty. + const setLocalSetups = ( + type: BondFor, + setups: NominatorSetups | PoolSetups + ) => { + const key = type === 'nominator' ? 'nominator_setups' : 'pool_setups'; + const setupsStr = JSON.stringify(setups); + + if (setupsStr === '{}') { + localStorage.removeItem(key); + } else { + localStorage.setItem(key, setupsStr); + } + }; + + // Move away from setup pages on completion / network change. + useEffectIgnoreInitial(() => { + if (!inSetup()) { + setOnNominatorSetup(false); + } + if (poolMembership) { + setOnPoolSetup(false); + } + }, [inSetup(), network, poolMembership]); + + // Update setup state when activeAccount changes + useEffectIgnoreInitial(() => { + if (accounts.length) refreshSetups(); + }, [activeAccount, network, accounts]); + + return ( + <SetupContext.Provider + value={{ + getSetupProgress, + removeSetupProgress, + getNominatorSetupPercent, + getPoolSetupPercent, + setActiveAccountSetup, + setActiveAccountSetupSection, + setOnNominatorSetup, + setOnPoolSetup, + onNominatorSetup, + onPoolSetup, + }} + > + {children} + </SetupContext.Provider> + ); +}; + +export const SetupContext = + React.createContext<SetupContextInterface>(defaultSetupContext); + +export const useSetup = () => React.useContext(SetupContext); diff --git a/src/contexts/Setup/types.ts b/src/contexts/Setup/types.ts new file mode 100644 index 0000000000..08e9178f92 --- /dev/null +++ b/src/contexts/Setup/types.ts @@ -0,0 +1,61 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { PoolRoles } from 'contexts/Pools/types'; +import type { ValidatorPrefs } from 'contexts/Validators/types'; +import type { AnyJson, BondFor, MaybeAddress, MaybeString } from 'types'; + +export type PayeeOptions = + | 'Staked' + | 'Stash' + | 'Controller' + | 'Account' + | 'None'; + +export type NominatorSetups = Record<string, NominatorSetup>; + +export interface NominatorSetup { + progress: NominatorProgress; + section: number; +} + +export interface NominatorProgress { + payee: PayeeConfig; + nominations: AnyJson[]; + bond: MaybeString; +} + +export interface PayeeConfig { + destination: PayeeOptions | null; + account: MaybeAddress; +} + +export type PoolSetups = Record<string, PoolSetup>; + +export interface PoolSetup { + progress: PoolProgress; + section: number; +} + +export interface PoolProgress { + metadata: string; + bond: string; + nominations: { address: string; prefs: ValidatorPrefs }[]; + roles: PoolRoles | null; +} + +export interface SetupContextInterface { + getSetupProgress: (t: BondFor, a: MaybeAddress) => any; + removeSetupProgress: (t: BondFor, a: MaybeAddress) => void; + getNominatorSetupPercent: (a: MaybeAddress) => number; + getPoolSetupPercent: (a: MaybeAddress) => number; + setActiveAccountSetup: ( + t: BondFor, + p: NominatorProgress | PoolProgress + ) => void; + setActiveAccountSetupSection: (t: BondFor, s: number) => void; + setOnNominatorSetup: (v: boolean) => void; + setOnPoolSetup: (v: boolean) => void; + onNominatorSetup: boolean; + onPoolSetup: boolean; +} diff --git a/src/contexts/Staking/Utils.ts b/src/contexts/Staking/Utils.ts new file mode 100644 index 0000000000..b41151958c --- /dev/null +++ b/src/contexts/Staking/Utils.ts @@ -0,0 +1,84 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyApi, NetworkName } from 'types'; +import { rmCommas } from '@polkadot-cloud/utils'; +import type { Exposure, LocalExposure, LocalExposuresData } from './types'; + +// Get local `erasStakers` entries for an era. +export const getLocalEraExposures = ( + network: NetworkName, + era: string, + activeEra: string +) => { + const data = localStorage.getItem(`${network}_exposures`); + const current = data ? (JSON.parse(data) as LocalExposuresData) : null; + const currentEra = current?.era; + + if (currentEra && currentEra !== activeEra) + localStorage.removeItem(`${network}_exposures`); + + if (currentEra === era && current?.exposures) + return maxifyExposures(current.exposures) as Exposure[]; + + return null; +}; + +// Set local stakers entries data for an era. +export const setLocalEraExposures = ( + network: NetworkName, + era: string, + exposures: Exposure[] +) => { + localStorage.setItem( + `${network}_exposures`, + JSON.stringify({ + era, + exposures: minifyExposures(exposures), + }) + ); +}; + +// Humanise and remove commas from fetched exposures. +export const formatRawExposures = (exposures: AnyApi) => + exposures.map(([k, v]: AnyApi) => { + const keys = k.toHuman(); + const { own, total, others } = v.toHuman(); + + return { + keys: [rmCommas(keys[0]), keys[1]], + val: { + others: others.map(({ who, value }: AnyApi) => ({ + who, + value: rmCommas(value), + })), + own: rmCommas(own), + total: rmCommas(total), + }, + }; + }); + +// Minify exposures data structure for local storage. +const minifyExposures = (exposures: Exposure[]) => + exposures.map(({ keys, val: { others, own, total } }: AnyApi) => ({ + k: [keys[0], keys[1]], + v: { + o: others.map(({ who, value }: AnyApi) => [who, value]), + w: own, + t: total, + }, + })); + +// Expand local exposure data into JSON format. +const maxifyExposures = (exposures: LocalExposure[]) => + exposures.map(({ k, v }: AnyApi) => ({ + keys: [k[0], k[1]], + val: { + others: v.o.map(([who, value]: AnyApi) => ({ + who, + value, + })), + own: v.w, + total: v.t, + }, + })); diff --git a/src/contexts/Staking/defaults.ts b/src/contexts/Staking/defaults.ts new file mode 100644 index 0000000000..20f5bdfb2d --- /dev/null +++ b/src/contexts/Staking/defaults.ts @@ -0,0 +1,62 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import BigNumber from 'bignumber.js'; +import type { + EraStakers, + NominationStatuses, + StakingContextInterface, + StakingMetrics, + StakingTargets, +} from 'contexts/Staking/types'; + +export const defaultStakingMetrics: StakingMetrics = { + totalNominators: new BigNumber(0), + totalValidators: new BigNumber(0), + lastReward: new BigNumber(0), + lastTotalStake: new BigNumber(0), + validatorCount: new BigNumber(0), + maxValidatorsCount: new BigNumber(0), + minNominatorBond: new BigNumber(0), + payee: { + destination: null, + account: null, + }, + totalStaked: new BigNumber(0), +}; + +export const defaultEraStakers: EraStakers = { + activeAccountOwnStake: [], + activeValidators: 0, + stakers: [], + totalActiveNominators: 0, +}; + +export const defaultTargets: StakingTargets = { + nominations: [], +}; + +const defaultLowestReward = { + lowest: new BigNumber(0), + oversubscribed: false, +}; + +export const defaultNominationStatus: NominationStatuses = {}; + +export const defaultStakingContext: StakingContextInterface = { + fetchEraStakers: async (e) => new Promise((resolve) => resolve([])), + getNominationsStatusFromTargets: (w, t) => defaultNominationStatus, + setTargets: (t) => {}, + hasController: () => false, + getControllerNotImported: (a) => null, + addressDifferentToStash: (a) => false, + isBonding: () => false, + isNominating: () => false, + inSetup: () => true, + getLowestRewardFromStaker: (address) => defaultLowestReward, + staking: defaultStakingMetrics, + eraStakers: defaultEraStakers, + targets: defaultTargets, + erasStakersSyncing: true, +}; diff --git a/src/contexts/Staking/index.tsx b/src/contexts/Staking/index.tsx new file mode 100644 index 0000000000..e9c07835dc --- /dev/null +++ b/src/contexts/Staking/index.tsx @@ -0,0 +1,410 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { VoidFn } from '@polkadot/api/types'; +import { + greaterThanZero, + isNotZero, + localStorageOrDefault, + setStateWithRef, +} from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useRef, useState } from 'react'; +import { useBalances } from 'contexts/Balances'; +import type { ExternalAccount } from '@polkadot-cloud/react/types'; +import type { PayeeConfig, PayeeOptions } from 'contexts/Setup/types'; +import type { + EraStakers, + Exposure, + StakingContextInterface, + StakingMetrics, + StakingTargets, +} from 'contexts/Staking/types'; +import type { AnyApi, AnyJson, MaybeAddress } from 'types'; +import Worker from 'workers/stakers?worker'; +import type { ResponseInitialiseExposures } from 'workers/types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useApi } from '../Api'; +import { useBonded } from '../Bonded'; +import { useNetworkMetrics } from '../NetworkMetrics'; +import { + defaultEraStakers, + defaultStakingContext, + defaultStakingMetrics, + defaultTargets, +} from './defaults'; +import { + setLocalEraExposures, + getLocalEraExposures, + formatRawExposures, +} from './Utils'; + +const worker = new Worker(); + +export const StakingProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { accounts: connectAccounts } = useImportedAccounts(); + const { activeAccount, getActiveAccount } = useActiveAccounts(); + const { getStashLedger } = useBalances(); + const { activeEra } = useNetworkMetrics(); + const { networkData, network } = useNetwork(); + const { isReady, api, apiStatus, consts } = useApi(); + const { bondedAccounts, getBondedAccount, getAccountNominations } = + useBonded(); + const { maxNominatorRewardedPerValidator } = consts; + + // Store staking metrics in state. + const [stakingMetrics, setStakingMetrics] = useState<StakingMetrics>( + defaultStakingMetrics + ); + + // Store unsub object fro staking metrics. + const unsub = useRef<VoidFn | null>(null); + + // Store eras stakers in state. + const [eraStakers, setEraStakers] = useState<EraStakers>(defaultEraStakers); + const eraStakersRef = useRef(eraStakers); + + // Flags whether `eraStakers` is resyncing. + const [erasStakersSyncing, setErasStakersSyncing] = useState(false); + const erasStakersSyncingRef = useRef(erasStakersSyncing); + + // Store target validators for the active account. + const [targets, setTargetsState] = useState<StakingTargets>( + localStorageOrDefault<StakingTargets>( + `${activeAccount ?? ''}_targets`, + defaultTargets, + true + ) as StakingTargets + ); + + // Handle metrics unsubscribe. + const unsubscribeMetrics = () => { + if (unsub.current !== null) { + unsub.current(); + unsub.current = null; + } + }; + + worker.onmessage = (message: MessageEvent) => { + if (message) { + const { data }: { data: ResponseInitialiseExposures } = message; + const { task, networkName, era } = data; + + // ensure task matches, & era is still the same. + if ( + task !== 'processExposures' || + networkName !== network || + era !== activeEra.index.toString() + ) + return; + + const { + stakers, + totalActiveNominators, + activeValidators, + activeAccountOwnStake, + who, + } = data; + + // finish sync + setStateWithRef(false, setErasStakersSyncing, erasStakersSyncingRef); + + // check if account hasn't changed since worker started + if (getActiveAccount() === who) { + setStateWithRef( + { + ...eraStakersRef.current, + stakers, + totalActiveNominators, + activeValidators, + activeAccountOwnStake, + }, + setEraStakers, + eraStakersRef + ); + } + } + }; + + // Multi subscription to staking metrics. + const subscribeToStakingkMetrics = async () => { + if (api !== null && isReady && isNotZero(activeEra.index)) { + const previousEra = activeEra.index.minus(1); + + const u = await api.queryMulti<AnyApi>( + [ + api.query.staking.counterForNominators, + api.query.staking.counterForValidators, + api.query.staking.maxValidatorsCount, + api.query.staking.validatorCount, + [api.query.staking.erasValidatorReward, previousEra.toString()], + [api.query.staking.erasTotalStake, previousEra.toString()], + api.query.staking.minNominatorBond, + [api.query.staking.payee, activeAccount], + [api.query.staking.erasTotalStake, activeEra.index.toString()], + ], + (q) => { + setStakingMetrics({ + totalNominators: new BigNumber(q[0].toString()), + totalValidators: new BigNumber(q[1].toString()), + maxValidatorsCount: new BigNumber(q[2].toString()), + validatorCount: new BigNumber(q[3].toString()), + lastReward: new BigNumber(q[4].toString()), + lastTotalStake: new BigNumber(q[5].toString()), + minNominatorBond: new BigNumber(q[6].toString()), + payee: processPayee(q[7]), + totalStaked: new BigNumber(q[8].toString()), + }); + } + ); + + unsub.current = u; + } + }; + + // Process raw payee object from API. payee with `Account` type is returned as an key value pair, + // with all others strings. This function handles both cases and formats into a unified structure. + const processPayee = (rawPayee: AnyApi) => { + const payeeHuman = rawPayee.toHuman(); + + let payeeFinal: PayeeConfig; + if (typeof payeeHuman === 'string') { + const destination = payeeHuman as PayeeOptions; + payeeFinal = { + destination, + account: null, + }; + } else { + const payeeEntry = Object.entries(payeeHuman); + const destination = `${payeeEntry[0][0]}` as PayeeOptions; + const account = `${payeeEntry[0][1]}` as MaybeAddress; + payeeFinal = { + destination, + account, + }; + } + return payeeFinal; + }; + + // Fetches erasStakers exposures for an era, and saves to `localStorage`. + const fetchEraStakers = async (era: string) => { + if (!isReady || activeEra.index.isZero() || !api) return []; + + let exposures: Exposure[] = []; + const localExposures = getLocalEraExposures( + network, + era, + activeEra.index.toString() + ); + + if (localExposures) { + exposures = localExposures; + } else { + exposures = formatRawExposures( + await api.query.staking.erasStakers.entries(era) + ); + } + + // For resource limitation concerns, only store the current era in local storage. + if (era === activeEra.index.toString()) + setLocalEraExposures(network, era, exposures); + + return exposures; + }; + + // Fetches the active nominator set and metadata around it. + const fetchActiveEraStakers = async () => { + if (!isReady || activeEra.index.isZero() || !api) return; + + // flag eraStakers is recyncing + setStateWithRef(true, setErasStakersSyncing, erasStakersSyncingRef); + + const exposures = await fetchEraStakers(activeEra.index.toString()); + + // worker to calculate stats + worker.postMessage({ + era: activeEra.index.toString(), + networkName: network, + task: 'processExposures', + activeAccount, + units: networkData.units, + exposures, + maxNominatorRewardedPerValidator: + maxNominatorRewardedPerValidator.toNumber(), + }); + }; + + // Sets an account's stored target validators. + const setTargets = (value: StakingTargets) => { + localStorage.setItem(`${activeAccount}_targets`, JSON.stringify(value)); + setTargetsState(value); + }; + + // Gets the nomination statuses of passed in nominations. + const getNominationsStatusFromTargets = ( + who: MaybeAddress, + fromTargets: AnyJson[] + ) => { + const statuses: Record<string, string> = {}; + + if (!fromTargets.length) { + return statuses; + } + + for (const target of fromTargets) { + const staker = eraStakersRef.current.stakers.find( + ({ address }) => address === target + ); + + if (staker === undefined) { + statuses[target] = 'waiting'; + continue; + } + + if (!(staker.others ?? []).find((o: any) => o.who === who)) { + statuses[target] = 'inactive'; + continue; + } + statuses[target] = 'active'; + } + return statuses; + }; + + // Helper function to determine whether the controller account is the same as the stash account. + const addressDifferentToStash = (address: MaybeAddress) => { + // check if controller is imported. + if (!connectAccounts.find((acc) => acc.address === address)) { + return false; + } + return address !== activeAccount && activeAccount !== null; + }; + + // Helper function to determine whether the controller account has been imported. + const getControllerNotImported = (address: MaybeAddress) => { + if (address === null || !activeAccount) { + return false; + } + // check if controller is imported + const exists = connectAccounts.find((a) => a.address === address); + if (exists === undefined) { + return true; + } + // controller account exists. If it is a read-only account, then controller is imported. + if (Object.prototype.hasOwnProperty.call(exists, 'addedBy')) { + if ((exists as ExternalAccount).addedBy === 'user') { + return false; + } + } + // if the controller is a Ledger account, then it can act as a signer. + if (exists.source === 'ledger') { + return false; + } + // if a `signer` does not exist on the account, then controller is not imported. + return !Object.prototype.hasOwnProperty.call(exists, 'signer'); + }; + + // Helper function to determine whether the active account. + const hasController = () => getBondedAccount(activeAccount) !== null; + + // Helper function to determine whether the active account is bonding, or is yet to start. + const isBonding = () => + hasController() && greaterThanZero(getStashLedger(activeAccount).active); + + // Helper function to determine whether the active account. + const isUnlocking = () => + hasController() && getStashLedger(activeAccount).unlocking.length; + + // Helper function to determine whether the active account is nominating, or is yet to start. + const isNominating = () => getAccountNominations(activeAccount).length > 0; + + // Helper function to determine whether the active account is nominating, or is yet to start. + const inSetup = () => + !activeAccount || + (!hasController() && !isBonding() && !isNominating() && !isUnlocking()); + + // Helper function to get the lowest reward from an active validator. + const getLowestRewardFromStaker = (address: MaybeAddress) => { + const staker = eraStakersRef.current.stakers.find( + (s) => s.address === address + ); + const lowest = new BigNumber(staker?.lowestReward || 0); + const oversubscribed = staker?.oversubscribed || false; + + return { + lowest, + oversubscribed, + }; + }; + + useEffectIgnoreInitial(() => { + if (apiStatus === 'connecting') { + setStateWithRef(defaultEraStakers, setEraStakers, eraStakersRef); + setStakingMetrics(stakingMetrics); + } + }, [apiStatus]); + + // Handle staking metrics subscription + useEffectIgnoreInitial(() => { + if (isReady) { + unsubscribeMetrics(); + subscribeToStakingkMetrics(); + } + return () => { + unsubscribeMetrics(); + }; + }, [isReady, activeEra, activeAccount]); + + // handle syncing with eraStakers + useEffectIgnoreInitial(() => { + if (isReady) fetchActiveEraStakers(); + }, [isReady, activeEra.index, activeAccount]); + + useEffectIgnoreInitial(() => { + if (activeAccount) { + // set account's targets + setTargetsState( + localStorageOrDefault( + `${activeAccount}_targets`, + defaultTargets, + true + ) as StakingTargets + ); + } + }, [isReady, bondedAccounts, activeAccount, eraStakersRef.current?.stakers]); + + return ( + <StakingContext.Provider + value={{ + fetchEraStakers, + getNominationsStatusFromTargets, + setTargets, + hasController, + getControllerNotImported, + addressDifferentToStash, + isBonding, + isNominating, + inSetup, + getLowestRewardFromStaker, + staking: stakingMetrics, + eraStakers: eraStakersRef.current, + erasStakersSyncing: erasStakersSyncingRef.current, + targets, + }} + > + {children} + </StakingContext.Provider> + ); +}; + +export const StakingContext = React.createContext<StakingContextInterface>( + defaultStakingContext +); + +export const useStaking = () => React.useContext(StakingContext); diff --git a/src/contexts/Staking/types.ts b/src/contexts/Staking/types.ts new file mode 100644 index 0000000000..3eebda7323 --- /dev/null +++ b/src/contexts/Staking/types.ts @@ -0,0 +1,101 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { PayeeConfig } from 'contexts/Setup/types'; +import type { MaybeAddress } from 'types'; + +export interface StakingMetrics { + totalNominators: BigNumber; + totalValidators: BigNumber; + lastReward: BigNumber; + lastTotalStake: BigNumber; + validatorCount: BigNumber; + maxValidatorsCount: BigNumber; + minNominatorBond: BigNumber; + payee: PayeeConfig; + totalStaked: BigNumber; +} + +export interface ActiveAccountOwnStake { + address: string; + value: string; +} +export interface EraStakers { + activeAccountOwnStake: ActiveAccountOwnStake[]; + activeValidators: number; + stakers: Staker[]; + totalActiveNominators: number; +} + +export type NominationStatuses = Record<string, string>; + +export interface StakingTargets { + nominations: string[]; +} + +export interface Exposure { + keys: string[]; + val: ExposureValue; +} + +export interface ExposureValue { + others: { + value: string; + who: string; + }[]; + own: string; + total: string; +} + +export type Staker = ExposureValue & { + address: string; + lowestReward: string; + oversubscribed: boolean; +}; + +export interface ActiveAccountStaker { + address: string; + value: string; +} + +export interface ExposureOther { + who: string; + value: string; +} + +interface LowestReward { + lowest: BigNumber; + oversubscribed: boolean; +} + +export interface StakingContextInterface { + fetchEraStakers: (era: string) => Promise<Exposure[]>; + getNominationsStatusFromTargets: (w: MaybeAddress, t: any[]) => any; + setTargets: (t: any) => any; + hasController: () => boolean; + getControllerNotImported: (a: MaybeAddress) => any; + addressDifferentToStash: (a: MaybeAddress) => boolean; + isBonding: () => boolean; + isNominating: () => boolean; + inSetup: () => any; + getLowestRewardFromStaker: (a: MaybeAddress) => LowestReward; + staking: StakingMetrics; + eraStakers: EraStakers; + targets: any; + erasStakersSyncing: any; +} + +export interface LocalExposuresData { + era: string; + exposures: LocalExposure[]; +} + +export interface LocalExposure { + k: [string, string]; + v: { + o: [string, string]; + w: string; + t: string; + }; +} diff --git a/src/contexts/Themes/defaults.ts b/src/contexts/Themes/defaults.ts new file mode 100644 index 0000000000..b226cc91e9 --- /dev/null +++ b/src/contexts/Themes/defaults.ts @@ -0,0 +1,10 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { ThemeContextInterface } from './types'; + +export const defaultThemeContext: ThemeContextInterface = { + toggleTheme: (str) => {}, + mode: 'light', +}; diff --git a/src/contexts/Themes/index.tsx b/src/contexts/Themes/index.tsx new file mode 100644 index 0000000000..d0d68ba69f --- /dev/null +++ b/src/contexts/Themes/index.tsx @@ -0,0 +1,66 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import React, { useRef } from 'react'; +import { defaultThemeContext } from './defaults'; +import type { Theme, ThemeContextInterface } from './types'; + +export const ThemesProvider = ({ children }: { children: React.ReactNode }) => { + let initialTheme: Theme = 'light'; + + // get the current theme + const localThemeRaw = localStorage.getItem('theme') || ''; + + // Provide system theme if raw theme is not valid. + if (!['light', 'dark'].includes(localThemeRaw)) { + const systemTheme = + window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; + + initialTheme = systemTheme; + localStorage.setItem('theme', systemTheme); + } else { + // `localThemeRaw` is a valid theme. + initialTheme = localThemeRaw as Theme; + } + + // the theme mode + const [theme, setTheme] = React.useState<Theme>(initialTheme); + const themeRef = useRef(theme); + + // Automatically change theme on system change. + window + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', (event) => { + const newTheme = event.matches ? 'dark' : 'light'; + localStorage.setItem('theme', newTheme); + setStateWithRef(newTheme, setTheme, themeRef); + }); + + const toggleTheme = (maybeTheme: Theme | null = null): void => { + const newTheme = + maybeTheme || (themeRef.current === 'dark' ? 'light' : 'dark'); + + localStorage.setItem('theme', newTheme); + setStateWithRef(newTheme, setTheme, themeRef); + }; + + return ( + <ThemeContext.Provider + value={{ + toggleTheme, + mode: themeRef.current, + }} + > + {children} + </ThemeContext.Provider> + ); +}; + +export const ThemeContext = + React.createContext<ThemeContextInterface>(defaultThemeContext); + +export const useTheme = () => React.useContext(ThemeContext); diff --git a/src/contexts/Themes/types.ts b/src/contexts/Themes/types.ts new file mode 100644 index 0000000000..c4e51cecc7 --- /dev/null +++ b/src/contexts/Themes/types.ts @@ -0,0 +1,9 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export type Theme = 'light' | 'dark'; + +export interface ThemeContextInterface { + toggleTheme: (str?: Theme) => void; + mode: Theme; +} diff --git a/src/contexts/Tooltip/defaults.ts b/src/contexts/Tooltip/defaults.ts new file mode 100644 index 0000000000..e2f9de4d9b --- /dev/null +++ b/src/contexts/Tooltip/defaults.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { TooltipContextInterface } from './types'; + +export const defaultTooltipContext: TooltipContextInterface = { + openTooltip: () => {}, + closeTooltip: () => {}, + setTooltipPosition: (x, y) => {}, + showTooltip: () => {}, + setTooltipTextAndOpen: (t) => {}, + open: 0, + show: 0, + position: [0, 0], + text: '', +}; diff --git a/src/contexts/Tooltip/index.tsx b/src/contexts/Tooltip/index.tsx new file mode 100644 index 0000000000..c8872b5a1b --- /dev/null +++ b/src/contexts/Tooltip/index.tsx @@ -0,0 +1,69 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import React, { useState } from 'react'; +import { defaultTooltipContext } from './defaults'; +import type { TooltipContextInterface } from './types'; + +export const TooltipProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [open, setOpen] = useState(0); + const [show, setShow] = useState(0); + const showRef = React.useRef(show); + + const [text, setText] = useState<string>(''); + const [position, setPosition] = useState<[number, number]>([0, 0]); + + const openTooltip = () => { + if (open) return; + setOpen(1); + }; + + const closeTooltip = () => { + setStateWithRef(0, setShow, showRef); + setOpen(0); + }; + + const setTooltipPosition = (x: number, y: number) => { + setPosition([x, y]); + openTooltip(); + }; + + const showTooltip = () => { + setStateWithRef(1, setShow, showRef); + }; + + const setTooltipTextAndOpen = (t: string) => { + if (open) return; + setText(t); + openTooltip(); + }; + + return ( + <TooltipContext.Provider + value={{ + openTooltip, + closeTooltip, + setTooltipPosition, + showTooltip, + setTooltipTextAndOpen, + open, + show: showRef.current, + position, + text, + }} + > + {children} + </TooltipContext.Provider> + ); +}; + +export const TooltipContext = React.createContext<TooltipContextInterface>( + defaultTooltipContext +); + +export const useTooltip = () => React.useContext(TooltipContext); diff --git a/src/contexts/Tooltip/types.ts b/src/contexts/Tooltip/types.ts new file mode 100644 index 0000000000..f903b8e29c --- /dev/null +++ b/src/contexts/Tooltip/types.ts @@ -0,0 +1,14 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface TooltipContextInterface { + openTooltip: () => void; + closeTooltip: () => void; + setTooltipPosition: (x: number, y: number) => void; + showTooltip: () => void; + setTooltipTextAndOpen: (t: string) => void; + open: number; + show: number; + position: [number, number]; + text: string; +} diff --git a/src/contexts/TransferOptions/defaults.ts b/src/contexts/TransferOptions/defaults.ts new file mode 100644 index 0000000000..d30021aeda --- /dev/null +++ b/src/contexts/TransferOptions/defaults.ts @@ -0,0 +1,33 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import BigNumber from 'bignumber.js'; +import type { TransferOptions, TransferOptionsContextInterface } from './types'; + +export const defaultBondedContext: TransferOptionsContextInterface = { + getTransferOptions: (a) => transferOptions, + setFeeReserveBalance: (r) => {}, + feeReserve: new BigNumber(0), +}; + +export const transferOptions: TransferOptions = { + freeBalance: new BigNumber(0), + edReserved: new BigNumber(0), + nominate: { + active: new BigNumber(0), + totalUnlocking: new BigNumber(0), + totalUnlocked: new BigNumber(0), + totalPossibleBond: new BigNumber(0), + totalAdditionalBond: new BigNumber(0), + totalUnlockChuncks: 0, + }, + pool: { + active: new BigNumber(0), + totalUnlocking: new BigNumber(0), + totalUnlocked: new BigNumber(0), + totalPossibleBond: new BigNumber(0), + totalAdditionalBond: new BigNumber(0), + totalUnlockChuncks: 0, + }, +}; diff --git a/src/contexts/TransferOptions/index.tsx b/src/contexts/TransferOptions/index.tsx new file mode 100644 index 0000000000..19c68955ee --- /dev/null +++ b/src/contexts/TransferOptions/index.tsx @@ -0,0 +1,217 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { unitToPlanck } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useState } from 'react'; +import { useApi } from 'contexts/Api'; +import { useBalances } from 'contexts/Balances'; +import { useBonded } from 'contexts/Bonded'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import type { MaybeAddress } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import * as defaults from './defaults'; +import type { TransferOptions, TransferOptionsContextInterface } from './types'; + +export const TransferOptionsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { consts } = useApi(); + const { + networkData: { name, units, defaultFeeReserve }, + } = useNetwork(); + const { activeEra } = useNetworkMetrics(); + const { getStashLedger, getBalance, getLocks } = useBalances(); + const { getAccount } = useBonded(); + const { membership } = usePoolMemberships(); + const { existentialDeposit } = consts; + const { activeAccount } = useActiveAccounts(); + + // Get the local storage rcord for an account reserve balance. + const getFeeReserveLocalStorage = (address: MaybeAddress) => { + const reserves = JSON.parse( + localStorage.getItem('reserve_balances') ?? '{}' + ); + return new BigNumber( + reserves?.[name]?.[address || ''] ?? + unitToPlanck(String(defaultFeeReserve), units) + ); + }; + + // A user-configurable reserve amount to be used to pay for transaction fees. + const [feeReserve, setFeeReserve] = useState<BigNumber>( + getFeeReserveLocalStorage(activeAccount) + ); + + // Update an account's reserve amount on account or network change. + useEffectIgnoreInitial(() => { + setFeeReserve(getFeeReserveLocalStorage(activeAccount)); + }, [activeAccount, name]); + + // Get the bond and unbond amounts available to the user + const getTransferOptions = (address: MaybeAddress): TransferOptions => { + const account = getAccount(address); + if (account === null) { + return defaults.transferOptions; + } + const balance = getBalance(address); + const ledger = getStashLedger(address); + const locks = getLocks(address); + + const { free } = balance; + const { active, total, unlocking } = ledger; + + const totalLocked = + locks?.reduce( + (prev, { amount }) => prev.plus(amount), + new BigNumber(0) + ) || new BigNumber(0); + + // Calculate a forced amount of free balance that needs to be reserved to keep the account + // alive. Deducts `locks` from free balance reserve needed. + const edReserved = BigNumber.max(existentialDeposit.minus(totalLocked), 0); + + // Total free balance after `edReserved` is subtracted. + const freeMinusReserve = BigNumber.max(free.minus(edReserved), 0); + + // calculate total balance locked + const maxLockBalance = + locks.reduce( + (prev, current) => { + return prev.amount.isGreaterThan(current.amount) ? prev : current; + }, + { amount: new BigNumber(0) } + )?.amount || new BigNumber(0); + + const poolBalance = membership?.balance; + const activePool = poolBalance || new BigNumber(0); + + // total amount actively unlocking + let totalUnlocking = new BigNumber(0); + let totalUnlocked = new BigNumber(0); + for (const u of unlocking) { + const { value, era } = u; + if (activeEra.index.isGreaterThan(era)) { + totalUnlocked = totalUnlocked.plus(value); + } else { + totalUnlocking = totalUnlocking.plus(value); + } + } + + // free balance after `total` ledger amount. + const freeBalance = BigNumber.max(freeMinusReserve.minus(total), 0); + + const nominateOptions = () => { + // total possible balance that can be bonded + const totalPossibleBond = BigNumber.max( + freeMinusReserve + .minus(totalUnlocking) + .minus(totalUnlocked) + .minus(feeReserve), + 0 + ); + + // total additional balance that can be bonded. + const totalAdditionalBond = totalPossibleBond.minus(active); + + return { + active, + totalUnlocking, + totalUnlocked, + totalPossibleBond, + totalAdditionalBond, + totalUnlockChuncks: unlocking.length, + }; + }; + + const poolOptions = () => { + const unlockingPool = membership?.unlocking || []; + + // total possible balance that can be bonded + const totalPossibleBondPool = BigNumber.max( + freeMinusReserve.minus(maxLockBalance).minus(feeReserve), + new BigNumber(0) + ); + + // total additional balance that can be bonded. + const totalAdditionalBondPool = totalPossibleBondPool; + + let totalUnlockingPool = new BigNumber(0); + let totalUnlockedPool = new BigNumber(0); + for (const u of unlockingPool) { + const { value, era } = u; + if (activeEra.index.isGreaterThan(era)) { + totalUnlockedPool = totalUnlockedPool.plus(value); + } else { + totalUnlockingPool = totalUnlockingPool.plus(value); + } + } + return { + active: activePool, + totalUnlocking: totalUnlockingPool, + totalUnlocked: totalUnlockedPool, + totalPossibleBond: totalPossibleBondPool, + totalAdditionalBond: totalAdditionalBondPool, + totalUnlockChuncks: unlockingPool.length, + }; + }; + + return { + freeBalance, + edReserved, + nominate: nominateOptions(), + pool: poolOptions(), + }; + }; + + // Updates account's reserve amount in state and in local storage. + const setFeeReserveBalance = (amount: BigNumber) => { + if (!activeAccount) return; + setFeeReserveLocalStorage(amount); + setFeeReserve(amount); + }; + + // Update the local storage record for account reserve balances. + const setFeeReserveLocalStorage = (amount: BigNumber) => { + if (!activeAccount) return; + + try { + const newReserves = JSON.parse( + localStorage.getItem('reserve_balances') ?? '{}' + ); + const newReservesNetwork = newReserves?.[name] ?? {}; + newReservesNetwork[activeAccount] = amount.toString(); + + newReserves[name] = newReservesNetwork; + localStorage.setItem('reserve_balances', JSON.stringify(newReserves)); + } catch (e) { + // corrupted local storage record - remove it. + localStorage.removeItem('reserve_balances'); + } + }; + + return ( + <TransferOptionsContext.Provider + value={{ + getTransferOptions, + setFeeReserveBalance, + feeReserve, + }} + > + {children} + </TransferOptionsContext.Provider> + ); +}; + +export const TransferOptionsContext = + React.createContext<TransferOptionsContextInterface>( + defaults.defaultBondedContext + ); + +export const useTransferOptions = () => + React.useContext(TransferOptionsContext); diff --git a/src/contexts/TransferOptions/types.ts b/src/contexts/TransferOptions/types.ts new file mode 100644 index 0000000000..fe8a1ea136 --- /dev/null +++ b/src/contexts/TransferOptions/types.ts @@ -0,0 +1,32 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { MaybeAddress } from 'types'; + +export interface TransferOptionsContextInterface { + getTransferOptions: (a: MaybeAddress) => TransferOptions; + setFeeReserveBalance: (r: BigNumber) => void; + feeReserve: BigNumber; +} + +export interface TransferOptions { + freeBalance: BigNumber; + edReserved: BigNumber; + nominate: { + active: BigNumber; + totalUnlocking: BigNumber; + totalUnlocked: BigNumber; + totalPossibleBond: BigNumber; + totalAdditionalBond: BigNumber; + totalUnlockChuncks: number; + }; + pool: { + active: BigNumber; + totalUnlocking: BigNumber; + totalUnlocked: BigNumber; + totalPossibleBond: BigNumber; + totalAdditionalBond: BigNumber; + totalUnlockChuncks: number; + }; +} diff --git a/src/contexts/TxMeta/defaults.ts b/src/contexts/TxMeta/defaults.ts new file mode 100644 index 0000000000..a98d2cb82f --- /dev/null +++ b/src/contexts/TxMeta/defaults.ts @@ -0,0 +1,24 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import BigNumber from 'bignumber.js'; +import type { TxMetaContextInterface } from './types'; + +export const defaultTxMeta: TxMetaContextInterface = { + controllerSignerAvailable: (a, b) => 'ok', + txFees: new BigNumber(0), + notEnoughFunds: false, + setTxFees: (f) => {}, + resetTxFees: () => {}, + sender: null, + setSender: (s) => {}, + txFeesValid: false, + incrementPayloadUid: () => 0, + getPayloadUid: () => 0, + getTxPayload: () => {}, + setTxPayload: (p, u) => {}, + getTxSignature: () => null, + resetTxPayloads: () => {}, + setTxSignature: (s) => {}, +}; diff --git a/src/contexts/TxMeta/index.tsx b/src/contexts/TxMeta/index.tsx new file mode 100644 index 0000000000..efac8cbdb3 --- /dev/null +++ b/src/contexts/TxMeta/index.tsx @@ -0,0 +1,147 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useState } from 'react'; +import { useBonded } from 'contexts/Bonded'; +import { useStaking } from 'contexts/Staking'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import type { AnyJson, MaybeAddress } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import * as defaults from './defaults'; +import type { TxMetaContextInterface } from './types'; + +export const TxMetaProvider = ({ children }: { children: React.ReactNode }) => { + const { getBondedAccount } = useBonded(); + const { activeProxy } = useActiveAccounts(); + const { getControllerNotImported } = useStaking(); + const { accountHasSigner } = useImportedAccounts(); + const { getTransferOptions } = useTransferOptions(); + + // Store the transaction fees for the transaction. + const [txFees, setTxFees] = useState(new BigNumber(0)); + + // Store the sender of the transaction. + const [sender, setSender] = useState<MaybeAddress>(null); + + // Store whether the sender does not have enough funds. + const [notEnoughFunds, setNotEnoughFunds] = useState(false); + + // Store the payloads of transactions if extrinsics require manual signing (e.g. Ledger). payloads + // are calculated asynchronously and extrinsic associated with them may be cancelled. For this + // reason we give every payload a uid, and check whether this uid matches the active extrinsic + // before submitting it. + const [txPayload, setTxPayloadState] = useState<{ + payload: AnyJson; + uid: number; + } | null>(null); + const txPayloadRef = React.useRef(txPayload); + + // Store an optional signed transaction if extrinsics require manual signing (e.g. Ledger). + const [txSignature, setTxSignatureState] = useState<AnyJson>(null); + const txSignatureRef = React.useRef(txSignature); + + useEffectIgnoreInitial(() => { + const { freeBalance } = getTransferOptions(sender); + setNotEnoughFunds(freeBalance.minus(txFees).isLessThan(0)); + }, [txFees, sender]); + + const resetTxFees = () => { + setTxFees(new BigNumber(0)); + }; + + const getPayloadUid = () => { + return txPayloadRef.current?.uid || 1; + }; + + const incrementPayloadUid = () => { + return (txPayloadRef.current?.uid || 0) + 1; + }; + + const getTxPayload = () => { + return txPayloadRef.current?.payload || null; + }; + + const setTxPayload = (p: AnyJson, uid: number) => { + setStateWithRef( + { + payload: p, + uid, + }, + setTxPayloadState, + txPayloadRef + ); + }; + + const resetTxPayloads = () => { + setStateWithRef(null, setTxPayloadState, txPayloadRef); + }; + + const getTxSignature = () => { + return txSignatureRef.current; + }; + + const setTxSignature = (s: AnyJson) => { + setStateWithRef(s, setTxSignatureState, txSignatureRef); + }; + + const txFeesValid = (() => { + if (txFees.isZero() || notEnoughFunds) { + return false; + } + return true; + })(); + + const controllerSignerAvailable = ( + stash: MaybeAddress, + proxySupported: boolean + ) => { + const controller = getBondedAccount(stash); + + if (controller !== stash) { + if (getControllerNotImported(controller)) + return 'controller_not_imported'; + + if (!accountHasSigner(controller)) return 'read_only'; + } else if ( + (!proxySupported || !accountHasSigner(activeProxy)) && + !accountHasSigner(stash) + ) { + return 'read_only'; + } + return 'ok'; + }; + + return ( + <TxMetaContext.Provider + value={{ + controllerSignerAvailable, + txFees, + notEnoughFunds, + setTxFees, + resetTxFees, + txFeesValid, + sender, + setSender, + incrementPayloadUid, + getPayloadUid, + getTxPayload, + setTxPayload, + resetTxPayloads, + getTxSignature, + setTxSignature, + }} + > + {children} + </TxMetaContext.Provider> + ); +}; + +export const TxMetaContext = React.createContext<TxMetaContextInterface>( + defaults.defaultTxMeta +); + +export const useTxMeta = () => React.useContext(TxMetaContext); diff --git a/src/contexts/TxMeta/types.ts b/src/contexts/TxMeta/types.ts new file mode 100644 index 0000000000..70510cdc76 --- /dev/null +++ b/src/contexts/TxMeta/types.ts @@ -0,0 +1,26 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { AnyJson, MaybeAddress } from 'types'; + +export interface TxMetaContextInterface { + controllerSignerAvailable: ( + a: MaybeAddress, + b: boolean + ) => 'controller_not_imported' | 'read_only' | 'ok'; + txFees: BigNumber; + notEnoughFunds: boolean; + setTxFees: (f: BigNumber) => void; + resetTxFees: () => void; + sender: MaybeAddress; + setSender: (s: MaybeAddress) => void; + txFeesValid: boolean; + incrementPayloadUid: () => number; + getPayloadUid: () => number; + getTxPayload: () => AnyJson; + setTxPayload: (s: AnyJson, u: number) => void; + resetTxPayloads: () => void; + getTxSignature: () => AnyJson; + setTxSignature: (s: AnyJson) => void; +} diff --git a/src/contexts/UI/defaults.ts b/src/contexts/UI/defaults.ts new file mode 100644 index 0000000000..02447cd199 --- /dev/null +++ b/src/contexts/UI/defaults.ts @@ -0,0 +1,19 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { UIContextInterface } from './types'; + +export const defaultUIContext: UIContextInterface = { + setSideMenu: (v) => {}, + setUserSideMenuMinimised: (v) => {}, + setContainerRefs: (v) => {}, + sideMenuOpen: false, + userSideMenuMinimised: false, + sideMenuMinimised: false, + containerRefs: {}, + isSyncing: false, + isNetworkSyncing: false, + isPoolSyncing: false, + isBraveBrowser: false, +}; diff --git a/src/contexts/UI/index.tsx b/src/contexts/UI/index.tsx new file mode 100644 index 0000000000..be3e164e16 --- /dev/null +++ b/src/contexts/UI/index.tsx @@ -0,0 +1,170 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { localStorageOrDefault, setStateWithRef } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useEffect, useRef, useState } from 'react'; +import { SideMenuStickyThreshold } from 'consts'; +import { useBalances } from 'contexts/Balances'; +import type { ImportedAccount } from '@polkadot-cloud/react/types'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import type { AnyJson } from 'types'; +import { useApi } from '../Api'; +import { useNetworkMetrics } from '../NetworkMetrics'; +import { useStaking } from '../Staking'; +import * as defaults from './defaults'; +import type { UIContextInterface } from './types'; + +export const UIProvider = ({ children }: { children: React.ReactNode }) => { + const { isReady } = useApi(); + const { balances } = useBalances(); + const { staking, eraStakers } = useStaking(); + const { activeEra, metrics } = useNetworkMetrics(); + const { synced: activePoolsSynced } = useActivePools(); + const { accounts: connectAccounts } = useImportedAccounts(); + + // Set whether the network has been synced. + const [isNetworkSyncing, setIsNetworkSyncing] = useState<boolean>(false); + + // Set whether pools are being synced. + const [isPoolSyncing, setIsPoolSyncing] = useState<boolean>(false); + + // Set whether app is syncing. Includes workers (active nominations). + const [isSyncing, setIsSyncing] = useState<boolean>(false); + + // Side whether the side menu is open. + const [sideMenuOpen, setSideMenu] = useState<boolean>(false); + + // Store whether in Brave browser. Used for light client warning. + const [isBraveBrowser, setIsBraveBrowser] = useState<boolean>(false); + + // Store referneces for main app conainers. + const [containerRefs, setContainerRefsState] = useState({}); + const setContainerRefs = (v: any) => { + setContainerRefsState(v); + }; + + // Get side menu minimised state from local storage, default to false. + const [userSideMenuMinimised, setUserSideMenuMinimisedState] = useState( + localStorageOrDefault('side_menu_minimised', false, true) as boolean + ); + const userSideMenuMinimisedRef = useRef(userSideMenuMinimised); + const setUserSideMenuMinimised = (v: boolean) => { + localStorage.setItem('side_menu_minimised', String(v)); + setStateWithRef(v, setUserSideMenuMinimisedState, userSideMenuMinimisedRef); + }; + + // Automatic side menu minimised. + const [sideMenuMinimised, setSideMenuMinimised] = useState( + window.innerWidth <= SideMenuStickyThreshold + ? true + : userSideMenuMinimisedRef.current + ); + + // Resize side menu callback. + const resizeCallback = () => { + if (window.innerWidth <= SideMenuStickyThreshold) { + setSideMenuMinimised(false); + } else { + setSideMenuMinimised(userSideMenuMinimisedRef.current); + } + }; + + // Resize event listener. + useEffect(() => { + (window.navigator as AnyJson)?.brave + ?.isBrave() + .then(async (isBrave: boolean) => { + setIsBraveBrowser(isBrave); + }); + + window.addEventListener('resize', resizeCallback); + return () => { + window.removeEventListener('resize', resizeCallback); + }; + }, []); + + // Re-configure minimised on user change. + useEffectIgnoreInitial(() => { + resizeCallback(); + }, [userSideMenuMinimised]); + + // App syncing updates. + useEffect(() => { + let syncing = false; + let networkSyncing = false; + let poolSyncing = false; + + if (!isReady) { + syncing = true; + networkSyncing = true; + poolSyncing = true; + } + // staking metrics have synced + if (staking.lastReward === new BigNumber(0)) { + syncing = true; + networkSyncing = true; + poolSyncing = true; + } + + // era has synced from Network + if (activeEra.index.isZero()) { + syncing = true; + networkSyncing = true; + poolSyncing = true; + } + + // all extension accounts have been synced + const extensionAccounts = connectAccounts.filter( + (a: ImportedAccount) => a.source !== 'external' + ); + if (balances.length < extensionAccounts.length) { + syncing = true; + networkSyncing = true; + poolSyncing = true; + } + + setIsNetworkSyncing(networkSyncing); + + // active pools have been synced + if (activePoolsSynced !== 'synced') { + syncing = true; + poolSyncing = true; + } + + setIsPoolSyncing(poolSyncing); + + // eraStakers total active nominators has synced + if (!eraStakers.totalActiveNominators) syncing = true; + + setIsSyncing(syncing); + }, [isReady, staking, metrics, balances, eraStakers, activePoolsSynced]); + + return ( + <UIContext.Provider + value={{ + setSideMenu, + setUserSideMenuMinimised, + setContainerRefs, + sideMenuOpen, + sideMenuMinimised, + isSyncing, + isNetworkSyncing, + isPoolSyncing, + containerRefs, + isBraveBrowser, + userSideMenuMinimised: userSideMenuMinimisedRef.current, + }} + > + {children} + </UIContext.Provider> + ); +}; + +export const UIContext = React.createContext<UIContextInterface>( + defaults.defaultUIContext +); + +export const useUi = () => React.useContext(UIContext); diff --git a/src/contexts/UI/types.ts b/src/contexts/UI/types.ts new file mode 100644 index 0000000000..300daa25b9 --- /dev/null +++ b/src/contexts/UI/types.ts @@ -0,0 +1,16 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface UIContextInterface { + setSideMenu: (v: boolean) => void; + setUserSideMenuMinimised: (v: boolean) => void; + setContainerRefs: (v: any) => void; + sideMenuOpen: boolean; + userSideMenuMinimised: boolean; + sideMenuMinimised: boolean; + containerRefs: any; + isSyncing: boolean; + isNetworkSyncing: boolean; + isPoolSyncing: boolean; + isBraveBrowser: boolean; +} diff --git a/src/contexts/Validators/FavoriteValidators/defaults.ts b/src/contexts/Validators/FavoriteValidators/defaults.ts new file mode 100644 index 0000000000..5559364cec --- /dev/null +++ b/src/contexts/Validators/FavoriteValidators/defaults.ts @@ -0,0 +1,20 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import BigNumber from 'bignumber.js'; +import type { FavoriteValidatorsContextInterface } from '../types'; + +export const defaultValidatorsData = { + entries: [], + notFullCommissionCount: 0, + totalNonAllCommission: new BigNumber(0), +}; + +export const defaultFavoriteValidatorsContext: FavoriteValidatorsContextInterface = + { + addFavorite: (a) => {}, + removeFavorite: (a) => {}, + favorites: [], + favoritesList: null, + }; diff --git a/src/contexts/Validators/FavoriteValidators/index.tsx b/src/contexts/Validators/FavoriteValidators/index.tsx new file mode 100644 index 0000000000..1884fdd06f --- /dev/null +++ b/src/contexts/Validators/FavoriteValidators/index.tsx @@ -0,0 +1,93 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useState } from 'react'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useApi } from 'contexts/Api'; +import type { Validator, FavoriteValidatorsContextInterface } from '../types'; +import { getLocalFavorites } from '../Utils'; +import { defaultFavoriteValidatorsContext } from './defaults'; +import { useValidators } from '../ValidatorEntries'; + +export const FavoriteValidatorsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { isReady } = useApi(); + const { + networkData: { name }, + network, + } = useNetwork(); + const { fetchValidatorPrefs } = useValidators(); + + // Stores the user's favorite validators. + const [favorites, setFavorites] = useState<string[]>(getLocalFavorites(name)); + + // Stores the user's favorites validators as list. + const [favoritesList, setFavoritesList] = useState<Validator[] | null>(null); + + const fetchFavoriteList = async () => { + // fetch preferences + const favoritesWithPrefs = await fetchValidatorPrefs( + [...favorites].map((address) => ({ + address, + })) + ); + setFavoritesList(favoritesWithPrefs || []); + }; + + // Adds a favorite validator. + const addFavorite = (address: string) => { + const newFavorites: any = Object.assign(favorites); + if (!newFavorites.includes(address)) { + newFavorites.push(address); + } + + localStorage.setItem(`${network}_favorites`, JSON.stringify(newFavorites)); + setFavorites([...newFavorites]); + }; + + // Removes a favorite validator if they exist. + const removeFavorite = (address: string) => { + const newFavorites = Object.assign(favorites).filter( + (validator: string) => validator !== address + ); + localStorage.setItem(`${network}_favorites`, JSON.stringify(newFavorites)); + setFavorites([...newFavorites]); + }; + + // Re-fetch favorites on network change + useEffectIgnoreInitial(() => { + setFavorites(getLocalFavorites(name)); + }, [network]); + + // Fetch favorites in validator list format + useEffectIgnoreInitial(() => { + if (isReady) { + fetchFavoriteList(); + } + }, [isReady, favorites]); + + return ( + <FavoriteValidatorsContext.Provider + value={{ + addFavorite, + removeFavorite, + favorites, + favoritesList, + }} + > + {children} + </FavoriteValidatorsContext.Provider> + ); +}; + +export const FavoriteValidatorsContext = + React.createContext<FavoriteValidatorsContextInterface>( + defaultFavoriteValidatorsContext + ); + +export const useFavoriteValidators = () => + React.useContext(FavoriteValidatorsContext); diff --git a/src/contexts/Validators/Utils.ts b/src/contexts/Validators/Utils.ts new file mode 100644 index 0000000000..dd496f08e3 --- /dev/null +++ b/src/contexts/Validators/Utils.ts @@ -0,0 +1,139 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import type { LocalMeta } from 'contexts/FastUnstake/types'; +import type { + EraRewardPoints, + LocalValidatorEntriesData, + Validator, +} from 'contexts/Validators/types'; +import type { AnyJson, NetworkName } from 'types'; + +// Get favorite validators from local storage. +export const getLocalFavorites = (network: NetworkName) => { + const localFavourites = localStorage.getItem(`${network}_favorites`); + return localFavourites !== null + ? (JSON.parse(localFavourites) as string[]) + : []; +}; + +// Get local validator entries data for an era. +export const getLocalEraValidators = (network: NetworkName, era: string) => { + const data = localStorage.getItem(`${network}_validators`); + const current = data ? (JSON.parse(data) as LocalValidatorEntriesData) : null; + const currentEra = current?.era; + + if (currentEra && currentEra !== era) + localStorage.removeItem(`${network}_validators`); + + return currentEra === era ? current : null; +}; + +// Set local validator entries data for an era. +export const setLocalEraValidators = ( + network: NetworkName, + era: string, + entries: Validator[], + avgCommission: number +) => { + localStorage.setItem( + `${network}_validators`, + JSON.stringify({ + era, + entries, + avgCommission, + }) + ); +}; + +// Validate local exposure metadata, currently used for fast unstake only. +export const validateLocalExposure = ( + localMeta: AnyJson, + endEra: BigNumber +): LocalMeta | null => { + const localIsExposed = localMeta?.isExposed ?? null; + let localChecked = localMeta?.checked ?? null; + + // check types saved. + if (typeof localIsExposed !== 'boolean' || !Array.isArray(localChecked)) + return null; + + // check checked only contains numbers. + const checkedNumeric = localChecked.every((e) => typeof e === 'number'); + if (!checkedNumeric) return null; + + // remove any expired eras and sort highest first. + localChecked = localChecked + .filter((e: number) => endEra.isLessThan(e)) + .sort((a: number, b: number) => b - a); + + // if no remaining eras, invalid. + if (!localChecked.length) { + return null; + } + + // check if highest -> lowest are decremented, no missing eras. + let i = 0; + let prev = 0; + const noMissingEras = localChecked.every((e: number) => { + i++; + if (i === 1) { + prev = e; + return true; + } + const p = prev; + prev = e; + if (e === p - 1) return true; + return false; + }); + + if (!noMissingEras) return null; + + return { + isExposed: localIsExposed, + checked: localChecked, + }; +}; + +// Check if era reward points entry exists for an era. +export const hasLocalEraRewardPoints = (network: NetworkName, era: string) => { + const current = JSON.parse( + localStorage.getItem(`${network}_era_reward_points`) || '{}' + ); + return !!current?.[era]; +}; + +// Get local era reward points entry for an era. +export const getLocalEraRewardPoints = (network: NetworkName, era: string) => { + const current = JSON.parse( + localStorage.getItem(`${network}_era_reward_points`) || '{}' + ); + return current?.[era] || {}; +}; + +// Set local era reward points entry for an era. +export const setLocalEraRewardPoints = ( + network: NetworkName, + era: string, + eraRewardPoints: EraRewardPoints | null, + endEra: string +) => { + const current = JSON.parse( + localStorage.getItem(`${network}_era_reward_points`) || '{}' + ); + + const removeStaleEras = Object.fromEntries( + Object.entries(current || {}).filter(([k]: [string, unknown]) => + new BigNumber(k).isGreaterThanOrEqualTo(endEra) + ) + ); + + localStorage.setItem( + `${network}_era_reward_points`, + JSON.stringify({ + ...removeStaleEras, + [era]: eraRewardPoints, + }) + ); +}; diff --git a/src/contexts/Validators/ValidatorEntries/defaults.ts b/src/contexts/Validators/ValidatorEntries/defaults.ts new file mode 100644 index 0000000000..6624b6d499 --- /dev/null +++ b/src/contexts/Validators/ValidatorEntries/defaults.ts @@ -0,0 +1,35 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import BigNumber from 'bignumber.js'; +import type { EraPointsBoundaries, ValidatorsContextInterface } from '../types'; + +export const defaultValidatorsContext: ValidatorsContextInterface = { + fetchValidatorPrefs: async (a) => new Promise((resolve) => resolve(null)), + getValidatorPointsFromEras: (startEra, address) => ({}), + getNominated: (bondFor) => [], + injectValidatorListData: (entries) => [], + validators: [], + validatorIdentities: {}, + validatorSupers: {}, + avgCommission: 0, + sessionValidators: [], + sessionParaValidators: [], + nominated: null, + poolNominated: null, + validatorCommunity: [], + erasRewardPoints: {}, + validatorsFetched: 'unsynced', + eraPointsBoundaries: null, + validatorEraPointsHistory: {}, + erasRewardPointsFetched: 'unsynced', +}; + +export const defaultValidatorsData = { + entries: [], + notFullCommissionCount: 0, + totalNonAllCommission: new BigNumber(0), +}; + +export const defaultEraPointsBoundaries: EraPointsBoundaries = null; diff --git a/src/contexts/Validators/ValidatorEntries/index.tsx b/src/contexts/Validators/ValidatorEntries/index.tsx new file mode 100644 index 0000000000..38a8918770 --- /dev/null +++ b/src/contexts/Validators/ValidatorEntries/index.tsx @@ -0,0 +1,603 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { greaterThanZero, rmCommas, shuffle } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useEffect, useRef, useState } from 'react'; +import { ValidatorCommunity } from '@polkadot-cloud/assets/validators'; +import type { AnyApi, AnyJson, BondFor, Fn, Sync } from 'types'; +import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; +import { useBonded } from 'contexts/Bonded'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useNetwork } from 'contexts/Network'; +import { useApi } from 'contexts/Api'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { MaxEraRewardPointsEras } from 'consts'; +import { useStaking } from 'contexts/Staking'; +import type { + EraPointsBoundaries, + ErasRewardPoints, + Identity, + Validator, + ValidatorAddresses, + ValidatorSuper, + ValidatorListEntry, + ValidatorsContextInterface, + ValidatorEraPointHistory, +} from '../types'; +import { + defaultValidatorsData, + defaultValidatorsContext, + defaultEraPointsBoundaries, +} from './defaults'; +import { getLocalEraValidators, setLocalEraValidators } from '../Utils'; + +export const ValidatorsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + const { isReady, api } = useApi(); + const { stakers } = useStaking().eraStakers; + const { poolNominations } = useActivePools(); + const { activeAccount } = useActiveAccounts(); + const { activeEra, metrics } = useNetworkMetrics(); + const { bondedAccounts, getAccountNominations } = useBonded(); + const { earliestStoredSession } = metrics; + + // Stores all validator entries. + const [validators, setValidators] = useState<Validator[]>([]); + + // Track whether the validator list has been fetched. + const [validatorsFetched, setValidatorsFetched] = useState<Sync>('unsynced'); + + // Store validator identity data. + const [validatorIdentities, setValidatorIdentities] = useState< + Record<string, Identity> + >({}); + + // Store validator super identity data. + const [validatorSupers, setValidatorSupers] = useState< + Record<string, ValidatorSuper> + >({}); + + // Stores the currently active validator set. + const [sessionValidators, setSessionValidators] = useState<string[]>([]); + + // Stores the currently active parachain validator set. + const [sessionParaValidators, setSessionParaValidators] = useState<string[]>( + [] + ); + + // Stores unsub object for para session. + const sessionParaUnsub = useRef<Fn>(); + + // Stores the average network commission rate. + const [avgCommission, setAvgCommission] = useState(0); + + // Stores the user's nominated validators as list + const [nominated, setNominated] = useState<Validator[] | null>(null); + + // Stores the nominated validators by the members pool's as list + const [poolNominated, setPoolNominated] = useState<Validator[] | null>(null); + + // Stores a randomised validator community dataset. + const [validatorCommunity] = useState([...shuffle(ValidatorCommunity)]); + + // Track whether the validator list has been fetched. + const [erasRewardPointsFetched, setErasRewawrdPointsFetched] = + useState<Sync>('unsynced'); + + // Store era reward points, keyed by era. + const [erasRewardPoints, setErasRewardPoints] = useState<ErasRewardPoints>( + {} + ); + + // Store validator era points history and metrics. + const [validatorEraPointsHistory, setValidatorEraPointsHistory] = useState< + Record<string, ValidatorEraPointHistory> + >({}); + + // Store era point high and low for `MaxEraPointsEras` eras. + const [eraPointsBoundaries, setEraPointsBoundaries] = + useState<EraPointsBoundaries>(defaultEraPointsBoundaries); + + // Processes reward points for a given era. + const processEraRewardPoints = (result: AnyJson, era: BigNumber) => { + if (erasRewardPoints[era.toString()]) + return erasRewardPoints[era.toString()]; + + return { + total: rmCommas(result.total), + individual: Object.fromEntries( + Object.entries(result.individual).map(([key, value]) => [ + key, + rmCommas(value as string), + ]) + ), + }; + }; + + // Get quartile data for validator performance data. + const getQuartile = (qIndex: number, total: number) => { + const q1 = Math.ceil(total * 0.25); + const q2 = Math.ceil(total * 0.5); + const q3 = Math.ceil(total * 0.75); + + if (qIndex <= q1) return 25; + if (qIndex <= q2) return 50; + if (qIndex <= q3) return 75; + return 100; + }; + + // Fetches era reward points for eligible eras. + const fetchErasRewardPoints = async () => { + if ( + activeEra.index.isZero() || + !api || + erasRewardPointsFetched !== 'unsynced' + ) + return; + + setErasRewawrdPointsFetched('syncing'); + + // start fetching from the current era. + let currentEra = BigNumber.max(activeEra.index.minus(1), 1); + const endEra = BigNumber.max( + currentEra.minus(MaxEraRewardPointsEras - 1), + 1 + ); + + // Introduce additional safeguard againt looping forever. + const totalEras = new BigNumber(MaxEraRewardPointsEras); + let erasProcessed = new BigNumber(0); + + // Iterate eras and process reward points. + const calls = []; + const eras = []; + do { + calls.push(api.query.staking.erasRewardPoints(currentEra.toString())); + eras.push(currentEra); + + currentEra = currentEra.minus(1); + erasProcessed = erasProcessed.plus(1); + } while ( + currentEra.isGreaterThanOrEqualTo(endEra) && + erasProcessed.isLessThan(totalEras) + ); + + // Make calls and format reward point results. + const newErasRewardPoints: ErasRewardPoints = {}; + let i = 0; + for (const result of await Promise.all(calls)) { + const formatted = processEraRewardPoints(result.toHuman(), eras[i]); + if (formatted) newErasRewardPoints[eras[i].toString()] = formatted; + i++; + } + + let newEraPointsHistory: Record<string, ValidatorEraPointHistory> = {}; + + // Calculate points per era and total points per era of each validator. + Object.entries(newErasRewardPoints).forEach(([era, { individual }]) => { + Object.entries(individual).forEach(([address, points]) => { + if (!newEraPointsHistory[address]) + newEraPointsHistory[address] = { + eras: {}, + totalPoints: new BigNumber(0), + }; + else { + newEraPointsHistory[address].eras[era] = new BigNumber(points); + newEraPointsHistory[address].totalPoints = + newEraPointsHistory[address].totalPoints.plus(points); + } + }); + }); + + // Iterate `newEraPointsHistory` and re-order the object based on its totalPoints, highest + // first. + newEraPointsHistory = Object.fromEntries( + Object.entries(newEraPointsHistory) + .sort( + ( + a: [string, ValidatorEraPointHistory], + b: [string, ValidatorEraPointHistory] + ) => a[1].totalPoints.minus(b[1].totalPoints).toNumber() + ) + .reverse() + ); + + const totalEntries = Object.entries(newEraPointsHistory).length; + let j = 0; + newEraPointsHistory = Object.fromEntries( + Object.entries(newEraPointsHistory).map(([k, v]) => { + j++; + return [k, { ...v, rank: j, quartile: getQuartile(j, totalEntries) }]; + }) + ); + + // Commit results to state. + setErasRewardPoints({ + ...newErasRewardPoints, + }); + setValidatorEraPointsHistory(newEraPointsHistory); + }; + + // Fetches the active account's nominees. + const fetchNominatedList = async () => { + if (!activeAccount) return; + + // format to list format + const targetsFormatted = getAccountNominations(activeAccount).map( + (item) => ({ address: item }) + ); + // fetch preferences + const nominationsWithPrefs = await fetchValidatorPrefs(targetsFormatted); + setNominated(nominationsWithPrefs || []); + }; + + // Fetches the active pool's nominees. + const fetchPoolNominatedList = async () => { + // get raw nominations list + let n = poolNominations.targets; + // format to list format + n = n.map((item: string) => ({ address: item })); + // fetch preferences + const nominationsWithPrefs = await fetchValidatorPrefs(n); + setPoolNominated(nominationsWithPrefs || []); + }; + + // Fetch validator entries and format the returning data. + const getValidatorEntries = async () => { + if (!isReady || !api) return defaultValidatorsData; + + const result = await api.query.staking.validators.entries(); + + const entries: Validator[] = []; + let notFullCommissionCount = 0; + let totalNonAllCommission = new BigNumber(0); + result.forEach(([a, p]: AnyApi) => { + const address = a.toHuman().pop(); + const prefs = p.toHuman(); + const commission = new BigNumber(prefs.commission.replace(/%/g, '')); + + if (!commission.isEqualTo(100)) + totalNonAllCommission = totalNonAllCommission.plus(commission); + else notFullCommissionCount++; + + entries.push({ + address, + prefs: { + commission: Number(commission.toFixed(2)), + blocked: prefs.blocked, + }, + }); + }); + + return { entries, notFullCommissionCount, totalNonAllCommission }; + }; + + // Fetches and formats the active validator set, and derives metrics from the result. + const fetchValidators = async () => { + if (!isReady || !api || validatorsFetched !== 'unsynced') return; + setValidatorsFetched('syncing'); + + // If local validator entries exist for the current era, store these values in state. Otherwise, + // fetch entries from API. + const localEraValidators = getLocalEraValidators( + network, + activeEra.index.toString() + ); + + // The validator entries for the current active era. + let validatorEntries: Validator[] = []; + // Average network commission for all non-100% commissioned validators. + let avg = 0; + + if (localEraValidators) { + validatorEntries = localEraValidators.entries; + avg = localEraValidators.avgCommission; + } else { + const { entries, notFullCommissionCount, totalNonAllCommission } = + await getValidatorEntries(); + + validatorEntries = entries; + avg = notFullCommissionCount + ? totalNonAllCommission + .dividedBy(notFullCommissionCount) + .decimalPlaces(2) + .toNumber() + : 0; + } + + // Set entries data for the era to local storage. + setLocalEraValidators( + network, + activeEra.index.toString(), + validatorEntries, + avg + ); + setAvgCommission(avg); + // Validators are shuffled before committed to state. + setValidators(shuffle(validatorEntries)); + + const addresses = validatorEntries.map(({ address }) => address); + const [identities, supers] = await Promise.all([ + fetchValidatorIdentities(addresses), + fetchValidatorSupers(addresses), + ]); + setValidatorIdentities(identities); + setValidatorSupers(supers); + setValidatorsFetched('synced'); + }; + + // Subscribe to active session validators. + const fetchSessionValidators = async () => { + if (!api || !isReady) return; + const sessionValidatorsRaw: AnyApi = await api.query.session.validators(); + setSessionValidators(sessionValidatorsRaw.toHuman()); + }; + + // Subscribe to active parachain validators. + const subscribeParachainValidators = async () => { + if (!api || !isReady) return; + const unsub: AnyApi = await api.query.paraSessionInfo.accountKeys( + earliestStoredSession.toString(), + (v: AnyApi) => { + setSessionParaValidators(v.toHuman()); + sessionParaUnsub.current = unsub; + } + ); + }; + + // Fetches prefs for a list of validators. + const fetchValidatorPrefs = async (addresses: ValidatorAddresses) => { + if (!addresses.length || !api) return null; + + const v: string[] = []; + for (const { address } of addresses) v.push(address); + const results = await api.query.staking.validators.multi(v); + + const formatted: Validator[] = []; + for (let i = 0; i < results.length; i++) { + const prefs: AnyApi = results[i].toHuman(); + formatted.push({ + address: v[i], + prefs: { + commission: prefs?.commission.replace(/%/g, '') ?? '0', + blocked: prefs.blocked, + }, + }); + } + return formatted; + }; + + // Fetches validator identities. + const fetchValidatorIdentities = async (addresses: string[]) => { + if (!api) return {}; + + const identities: AnyApi[] = ( + await api.query.identity.identityOf.multi(addresses) + ).map((identity) => identity.toHuman()); + + return Object.fromEntries( + Object.entries( + Object.fromEntries(identities.map((k, i) => [addresses[i], k])) + ).filter(([, v]) => v !== null) + ); + }; + + // Fetch validator super accounts and their identities. + const fetchValidatorSupers = async (addresses: string[]) => { + if (!api) return {}; + + const supersRaw: AnyApi[] = ( + await api.query.identity.superOf.multi(addresses) + ).map((superOf) => superOf.toHuman()); + + const supers = Object.fromEntries( + Object.entries( + Object.fromEntries( + supersRaw.map((k, i) => [ + addresses[i], + { + superOf: k, + }, + ]) + ) + ).filter(([, { superOf }]) => superOf !== null) + ); + + const superIdentities = ( + await api.query.identity.identityOf.multi( + Object.values(supers).map(({ superOf }) => superOf[0]) + ) + ).map((superIdentity) => superIdentity.toHuman()); + + const supersWithIdentity = Object.fromEntries( + Object.entries(supers).map(([k, v]: AnyApi, i) => [ + k, + { + ...v, + identity: superIdentities[i], + }, + ]) + ); + return supersWithIdentity; + }; + + // Gets era points for a validator + const getValidatorPointsFromEras = (startEra: BigNumber, address: string) => { + startEra = BigNumber.max(startEra, 1); + + // minus 1 from `MaxRewardPointsEras` to account for the current era. + const endEra = BigNumber.max(startEra.minus(MaxEraRewardPointsEras - 1), 1); + + const points: Record<string, BigNumber> = {}; + let currentEra = startEra; + do { + const eraPoints = erasRewardPoints[currentEra.toString()]; + if (eraPoints) { + const validatorPoints = eraPoints.individual[address]; + points[currentEra.toString()] = new BigNumber(validatorPoints || 0); + } else { + points[currentEra.toString()] = new BigNumber(0); + } + currentEra = currentEra.minus(1); + } while (currentEra.isGreaterThanOrEqualTo(endEra)); + + return points; + }; + + // Gets the highest and lowest (non-zero) era points earned `MaxEraRewardPointsEras` timeframe. + const calculateEraPointsBoundaries = () => { + let high: BigNumber | null = null; + let low: BigNumber | null = null; + + Object.entries(erasRewardPoints).forEach(([, { individual }]) => { + for (const [, points] of Object.entries(individual)) { + const p = new BigNumber(points); + + if (p.isGreaterThan(high || 0)) high = p; + if (low === null) low = p; + else if (p.isLessThan(low) && !p.isZero()) low = p; + } + }); + + setEraPointsBoundaries({ + high: high || new BigNumber(0), + low: low || new BigNumber(0), + }); + setErasRewawrdPointsFetched('synced'); + }; + + // Gets either `nominated` or `poolNominated` depending on bondFor, and injects the validator + // status into the entries. + const getNominated = (bondFor: BondFor) => + bondFor === 'nominator' ? nominated : poolNominated; + + // Inject status into validator entries. + const injectValidatorListData = ( + entries: Validator[] + ): ValidatorListEntry[] => { + const injected: ValidatorListEntry[] = + entries.map((entry) => { + const inEra = + stakers.find(({ address }) => address === entry.address) || false; + let totalStake = new BigNumber(0); + if (inEra) { + const { others, own } = inEra; + if (own) totalStake = totalStake.plus(own); + others.forEach(({ value }) => { + totalStake = totalStake.plus(value); + }); + } + return { + ...entry, + totalStake, + validatorStatus: inEra ? 'active' : 'waiting', + }; + }) || []; + return injected; + }; + + // Reset validator state data on network change. + useEffectIgnoreInitial(() => { + setValidatorsFetched('unsynced'); + setErasRewawrdPointsFetched('unsynced'); + setSessionValidators([]); + setSessionParaValidators([]); + setAvgCommission(0); + setValidators([]); + setValidatorIdentities({}); + setValidatorSupers({}); + setErasRewardPoints({}); + setEraPointsBoundaries(null); + setValidatorEraPointsHistory({}); + }, [network]); + + // Fetch validators and era reward points when fetched status changes. + useEffect(() => { + if (isReady && activeEra.index.isGreaterThan(0)) { + fetchValidators(); + fetchErasRewardPoints(); + } + }, [validatorsFetched, erasRewardPointsFetched, isReady, activeEra]); + + // Mark unsynced and fetch session validators when activeEra changes. + useEffectIgnoreInitial(() => { + if (isReady && activeEra.index.isGreaterThan(0)) { + if (erasRewardPointsFetched === 'synced') + setErasRewawrdPointsFetched('unsynced'); + + if (validatorsFetched === 'synced') setValidatorsFetched('unsynced'); + fetchSessionValidators(); + } + }, [isReady, activeEra]); + + // Fetch era points boundaries when `erasRewardPoints` ready. + useEffectIgnoreInitial(() => { + if (isReady && Object.values(erasRewardPoints).length) + calculateEraPointsBoundaries(); + }, [isReady, erasRewardPoints]); + + // Fetch parachain session validators when `earliestStoredSession` ready. + useEffectIgnoreInitial(() => { + if (isReady && greaterThanZero(earliestStoredSession)) + subscribeParachainValidators(); + }, [isReady, earliestStoredSession]); + + // Fetch active account's nominations in validator list format. + useEffectIgnoreInitial(() => { + if (isReady && activeAccount) { + fetchNominatedList(); + } + }, [isReady, activeAccount, bondedAccounts]); + + // Fetch active account's pool nominations in validator list format. + useEffectIgnoreInitial(() => { + if (isReady && poolNominations) fetchPoolNominatedList(); + }, [isReady, poolNominations]); + + // Unsubscribe on network change and component unmount. + useEffect(() => { + if (sessionParaValidators.length) sessionParaUnsub.current?.(); + + return () => { + sessionParaUnsub.current?.(); + }; + }, [network]); + + return ( + <ValidatorsContext.Provider + value={{ + fetchValidatorPrefs, + getValidatorPointsFromEras, + getNominated, + injectValidatorListData, + validators, + validatorIdentities, + validatorSupers, + avgCommission, + sessionValidators, + sessionParaValidators, + nominated, + poolNominated, + validatorCommunity, + erasRewardPoints, + validatorsFetched, + eraPointsBoundaries, + validatorEraPointsHistory, + erasRewardPointsFetched, + }} + > + {children} + </ValidatorsContext.Provider> + ); +}; + +export const ValidatorsContext = + React.createContext<ValidatorsContextInterface>(defaultValidatorsContext); + +export const useValidators = () => React.useContext(ValidatorsContext); diff --git a/src/contexts/Validators/types.ts b/src/contexts/Validators/types.ts new file mode 100644 index 0000000000..af772b6b58 --- /dev/null +++ b/src/contexts/Validators/types.ts @@ -0,0 +1,91 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { AnyJson, BondFor, Sync } from 'types'; + +export interface ValidatorsContextInterface { + fetchValidatorPrefs: (a: ValidatorAddresses) => Promise<Validator[] | null>; + getValidatorPointsFromEras: ( + startEra: BigNumber, + address: string + ) => Record<string, BigNumber>; + getNominated: (bondFor: BondFor) => Validator[] | null; + injectValidatorListData: (entries: Validator[]) => ValidatorListEntry[]; + validators: Validator[]; + validatorIdentities: Record<string, Identity>; + validatorSupers: Record<string, AnyJson>; + avgCommission: number; + sessionValidators: string[]; + sessionParaValidators: string[]; + nominated: Validator[] | null; + poolNominated: Validator[] | null; + validatorCommunity: any[]; + erasRewardPoints: ErasRewardPoints; + validatorsFetched: Sync; + eraPointsBoundaries: EraPointsBoundaries; + validatorEraPointsHistory: Record<string, ValidatorEraPointHistory>; + erasRewardPointsFetched: Sync; +} + +export interface FavoriteValidatorsContextInterface { + addFavorite: (a: string) => void; + removeFavorite: (a: string) => void; + favorites: string[]; + favoritesList: Validator[] | null; +} + +export interface Identity { + deposit: string; + info: AnyJson; + judgements: AnyJson[]; +} + +export interface ValidatorSuper { + identity: Identity; + superOf: [string, { Raw: string }]; +} + +export type ValidatorAddresses = { + address: string; +}[]; + +export interface Validator { + address: string; + prefs: ValidatorPrefs; +} + +export interface ValidatorPrefs { + commission: number; + blocked: boolean; +} + +export interface LocalValidatorEntriesData { + avgCommission: number; + era: string; + entries: Validator[]; +} + +export type ErasRewardPoints = Record<string, EraRewardPoints>; + +export interface EraRewardPoints { + total: string; + individual: Record<string, string>; +} + +export type EraPointsBoundaries = { + high: BigNumber; + low: BigNumber; +} | null; + +export type ValidatorListEntry = Validator & { + validatorStatus: 'waiting' | 'active'; + totalStake: BigNumber; +}; + +export interface ValidatorEraPointHistory { + eras: Record<string, BigNumber>; + totalPoints: BigNumber; + rank?: number; + quartile?: number; +} diff --git a/src/img/appIcons/kusama.svg b/src/img/appIcons/kusama.svg new file mode 100644 index 0000000000..05eaa63921 --- /dev/null +++ b/src/img/appIcons/kusama.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300.02 240.34"><path d="M0,149.38H60.29V120.6h60.29V90.52H150V59.9H209.1V29.75h31.11V0h29.71V30.71H300V60.86H269.87V120.6H240.29v29.78H209.58v30.19H180v30.14H150V180.13H0Z"/><path d="M180,210.71h29.62v29.63H180Z"/></svg> \ No newline at end of file diff --git a/src/img/appIcons/polkadot.svg b/src/img/appIcons/polkadot.svg new file mode 100644 index 0000000000..564dfcaae1 --- /dev/null +++ b/src/img/appIcons/polkadot.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.86 109.25"><path d="M87.86,44.13H65.05v-22H87.86ZM55.66,0H32.85V22H55.66ZM22.81,21.73H0v22H22.81ZM55.66,87.22H32.85v22H55.66Zm-32.85-22H0v22H22.81Zm65.05,0H65.05v22H87.86Z"/></svg> \ No newline at end of file diff --git a/src/img/brave-logo.svg b/src/img/brave-logo.svg new file mode 100644 index 0000000000..188639654c --- /dev/null +++ b/src/img/brave-logo.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><defs><linearGradient id="A" x1="-.031" y1="44.365" x2="26.596" y2="44.365" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f1562b"/><stop offset=".3" stop-color="#f1542b"/><stop offset=".41" stop-color="#f04d2a"/><stop offset=".49" stop-color="#ef4229"/><stop offset=".5" stop-color="#ef4029"/><stop offset=".56" stop-color="#e83e28"/><stop offset=".67" stop-color="#e13c26"/><stop offset="1" stop-color="#df3c26"/></linearGradient></defs><path d="M26.605 38.85l-.964-2.617.67-1.502c.086-.194.044-.42-.105-.572l-1.822-1.842a2.94 2.94 0 0 0-3.066-.712l-.5.177-2.783-3.016-4.752-.026-4.752.037-2.78 3.04-.495-.175a2.95 2.95 0 0 0-3.086.718L.304 34.237a.41.41 0 0 0-.083.456l.7 1.56-.96 2.615 3.447 13.107c.326 1.238 1.075 2.323 2.118 3.066l6.817 4.62a1.51 1.51 0 0 0 1.886 0l6.813-4.627c1.042-.743 1.8-1.828 2.115-3.066l2.812-10.752z" fill="url(#A)" transform="matrix(2.048177 0 0 2.048177 4.795481 -58.865395)"/><path d="M33.595 39.673a8.26 8.26 0 0 0-1.139-.413h-.686a8.26 8.26 0 0 0-1.139.413l-1.727.718-1.95.897-3.176 1.655c-.235.076-.4.288-.417.535s.118.48.34.586L26.458 46a21.86 21.86 0 0 1 1.695 1.346l.776.668 1.624 1.422.736.65a1.27 1.27 0 0 0 1.62 0l3.174-2.773 1.7-1.346 2.758-1.974a.6.6 0 0 0-.085-1.117l-3.17-1.6-1.96-.897zm19.555-17.77l.1-.287a7.73 7.73 0 0 0-.072-1.148c-.267-.68-.6-1.326-1.023-1.93l-1.794-2.633-1.278-1.736-2.404-3c-.22-.293-.458-.572-.713-.834h-.05l-1.068.197-5.284 1.018c-.535.025-1.07-.053-1.574-.23l-2.902-.937-2.077-.574a8.68 8.68 0 0 0-1.834 0l-2.077.58-2.902.942a4.21 4.21 0 0 1-1.574.23l-5.278-1-1.068-.197h-.05c-.256.262-.494.54-.713.834l-2.4 3a29.33 29.33 0 0 0-1.278 1.736l-1.794 2.633-.848 1.413c-.154.543-.235 1.104-.242 1.67l.1.287c.043.184.1.366.166.543l1.417 1.628 6.28 6.674a1.79 1.79 0 0 1 .318 1.794L18.178 35a3.16 3.16 0 0 0-.049 2.005l.206.565a5.45 5.45 0 0 0 1.673 2.346l.987.803c.52.376 1.2.457 1.794.215l3.508-1.673a8.79 8.79 0 0 0 1.794-1.19l2.808-2.534a1.12 1.12 0 0 0 .37-.795 1.13 1.13 0 0 0-.312-.82l-6.338-4.27a1.23 1.23 0 0 1-.386-1.556l2.458-4.62a2.4 2.4 0 0 0 .121-1.834 2.8 2.8 0 0 0-1.395-1.265l-7.706-2.9c-.556-.2-.525-.45.063-.484l4.526-.45a7.02 7.02 0 0 1 2.113.188l3.938 1.1c.578.174.94.75.843 1.346l-1.547 8.45a4.37 4.37 0 0 0-.076 1.426c.063.202.592.45 1.17.592l2.4.5a5.83 5.83 0 0 0 2.108 0l2.157-.5c.58-.13 1.103-.404 1.17-.606a4.38 4.38 0 0 0-.08-1.426l-1.556-8.45a1.21 1.21 0 0 1 .843-1.346l3.938-1.103a6.98 6.98 0 0 1 2.113-.188l4.526.422c.592.054.62.274.067.484l-7.7 2.92a2.76 2.76 0 0 0-1.395 1.265 2.41 2.41 0 0 0 .12 1.834l2.462 4.62a1.23 1.23 0 0 1-.386 1.556l-6.333 4.28a1.13 1.13 0 0 0 .058 1.615l2.812 2.534a8.89 8.89 0 0 0 1.794 1.184l3.508 1.67c.596.24 1.274.158 1.794-.22l.987-.807a5.44 5.44 0 0 0 1.673-2.35l.206-.565a3.16 3.16 0 0 0-.049-2.005l-1.032-2.436a1.8 1.8 0 0 1 .318-1.794l6.28-6.683 1.413-1.628a4.36 4.36 0 0 0 .193-.53z" fill="#fff"/></svg> \ No newline at end of file diff --git a/src/img/cog-outline.svg b/src/img/cog-outline.svg new file mode 100644 index 0000000000..5d8430b5f4 --- /dev/null +++ b/src/img/cog-outline.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M456.7 242.27l-26.08-4.2a8 8 0 01-6.6-6.82c-.5-3.2-1-6.41-1.7-9.51a8.08 8.08 0 013.9-8.62l23.09-12.82a8.05 8.05 0 003.9-9.92l-4-11a7.94 7.94 0 00-9.4-5l-25.89 5a8 8 0 01-8.59-4.11q-2.25-4.2-4.8-8.41a8.16 8.16 0 01.7-9.52l17.29-19.94a8 8 0 00.3-10.62l-7.49-9a7.88 7.88 0 00-10.5-1.51l-22.69 13.63a8 8 0 01-9.39-.9c-2.4-2.11-4.9-4.21-7.4-6.22a8 8 0 01-2.5-9.11l9.4-24.75A8 8 0 00365 78.77l-10.2-5.91a8 8 0 00-10.39 2.21l-16.64 20.84a7.15 7.15 0 01-8.5 2.5s-5.6-2.3-9.8-3.71A8 8 0 01304 87l.4-26.45a8.07 8.07 0 00-6.6-8.42l-11.59-2a8.07 8.07 0 00-9.1 5.61l-8.6 25.05a8 8 0 01-7.79 5.41h-9.8a8.07 8.07 0 01-7.79-5.41l-8.6-25.05a8.07 8.07 0 00-9.1-5.61l-11.59 2a8.07 8.07 0 00-6.6 8.42l.4 26.45a8 8 0 01-5.49 7.71c-2.3.9-7.3 2.81-9.7 3.71-2.8 1-6.1.2-8.8-2.91l-16.51-20.34A8 8 0 00156.75 73l-10.2 5.91a7.94 7.94 0 00-3.3 10.09l9.4 24.75a8.06 8.06 0 01-2.5 9.11c-2.5 2-5 4.11-7.4 6.22a8 8 0 01-9.39.9L111 116.14a8 8 0 00-10.5 1.51l-7.49 9a8 8 0 00.3 10.62l17.29 19.94a8 8 0 01.7 9.52q-2.55 4-4.8 8.41a8.11 8.11 0 01-8.59 4.11l-25.89-5a8 8 0 00-9.4 5l-4 11a8.05 8.05 0 003.9 9.92L85.58 213a7.94 7.94 0 013.9 8.62c-.6 3.2-1.2 6.31-1.7 9.51a8.08 8.08 0 01-6.6 6.82l-26.08 4.2a8.09 8.09 0 00-7.1 7.92v11.72a7.86 7.86 0 007.1 7.92l26.08 4.2a8 8 0 016.6 6.82c.5 3.2 1 6.41 1.7 9.51a8.08 8.08 0 01-3.9 8.62L62.49 311.7a8.05 8.05 0 00-3.9 9.92l4 11a7.94 7.94 0 009.4 5l25.89-5a8 8 0 018.59 4.11q2.25 4.2 4.8 8.41a8.16 8.16 0 01-.7 9.52l-17.29 19.96a8 8 0 00-.3 10.62l7.49 9a7.88 7.88 0 0010.5 1.51l22.69-13.63a8 8 0 019.39.9c2.4 2.11 4.9 4.21 7.4 6.22a8 8 0 012.5 9.11l-9.4 24.75a8 8 0 003.3 10.12l10.2 5.91a8 8 0 0010.39-2.21l16.79-20.64c2.1-2.6 5.5-3.7 8.2-2.6 3.4 1.4 5.7 2.2 9.9 3.61a8 8 0 015.49 7.71l-.4 26.45a8.07 8.07 0 006.6 8.42l11.59 2a8.07 8.07 0 009.1-5.61l8.6-25a8 8 0 017.79-5.41h9.8a8.07 8.07 0 017.79 5.41l8.6 25a8.07 8.07 0 009.1 5.61l11.59-2a8.07 8.07 0 006.6-8.42l-.4-26.45a8 8 0 015.49-7.71c4.2-1.41 7-2.51 9.6-3.51s5.8-1 8.3 2.1l17 20.94A8 8 0 00355 439l10.2-5.91a7.93 7.93 0 003.3-10.12l-9.4-24.75a8.08 8.08 0 012.5-9.12c2.5-2 5-4.1 7.4-6.21a8 8 0 019.39-.9L401 395.66a8 8 0 0010.5-1.51l7.49-9a8 8 0 00-.3-10.62l-17.29-19.94a8 8 0 01-.7-9.52q2.55-4.05 4.8-8.41a8.11 8.11 0 018.59-4.11l25.89 5a8 8 0 009.4-5l4-11a8.05 8.05 0 00-3.9-9.92l-23.09-12.82a7.94 7.94 0 01-3.9-8.62c.6-3.2 1.2-6.31 1.7-9.51a8.08 8.08 0 016.6-6.82l26.08-4.2a8.09 8.09 0 007.1-7.92V250a8.25 8.25 0 00-7.27-7.73zM256 112a143.82 143.82 0 01139.38 108.12A16 16 0 01379.85 240H274.61a16 16 0 01-13.91-8.09l-52.1-91.71a16 16 0 019.85-23.39A146.94 146.94 0 01256 112zM112 256a144 144 0 0143.65-103.41 16 16 0 0125.17 3.47L233.06 248a16 16 0 010 15.87l-52.67 91.7a16 16 0 01-25.18 3.36A143.94 143.94 0 01112 256zm144 144a146.9 146.9 0 01-38.19-4.95 16 16 0 01-9.76-23.44l52.58-91.55a16 16 0 0113.88-8H379.9a16 16 0 0115.52 19.88A143.84 143.84 0 01256 400z"/></svg> \ No newline at end of file diff --git a/src/img/cross.svg b/src/img/cross.svg new file mode 100644 index 0000000000..ffad129857 --- /dev/null +++ b/src/img/cross.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path class="primary" d="M19.6 17.5c.6.6.6 1.6 0 2.1-.3.3-.7.4-1.1.4s-.8-.1-1.1-.4L12 14.1l-5.5 5.5c-.3.3-.7.4-1.1.4s-.8-.1-1.1-.4c-.6-.6-.6-1.6 0-2.1L9.9 12 4.4 6.5c-.6-.6-.6-1.6 0-2.1.6-.6 1.6-.6 2.1 0L12 9.9l5.5-5.5c.6-.6 1.6-.6 2.1 0 .6.6.6 1.6 0 2.1L14.1 12l5.5 5.5z" fill="#121331"/></svg> \ No newline at end of file diff --git a/src/img/forum.svg b/src/img/forum.svg new file mode 100644 index 0000000000..52b794a3db --- /dev/null +++ b/src/img/forum.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path class="primary" d="M18.99 11.35c.01-.08.01-.16.01-.24V9.89C19 6.64 16.36 4 13.11 4H7.89C4.64 4 2 6.64 2 9.89V16c0 .55.45 1 1 1h5.07c.37 2.27 2.34 4 4.71 4H21c.55 0 1-.45 1-1v-4.22c0-2.01-1.25-3.74-3.01-4.43zm-11.26-.61c-.02.08-.04.16-.08.24-.03.07-.07.15-.11.21-.05.07-.1.14-.16.19-.05.06-.12.11-.19.16-.07.04-.14.08-.21.11-.08.04-.16.06-.24.08-.08.01-.16.02-.24.02s-.16-.01-.24-.02c-.08-.02-.16-.04-.24-.08a1.39 1.39 0 01-.21-.11c-.07-.05-.14-.1-.19-.16-.06-.05-.11-.12-.16-.19a1.39 1.39 0 01-.11-.21c-.04-.08-.06-.16-.08-.24-.01-.08-.02-.16-.02-.24 0-.33.13-.65.37-.88.45-.47 1.3-.47 1.76 0 .06.05.11.12.16.19.04.06.08.14.11.21.04.08.06.16.08.24.01.08.02.16.02.24 0 .08-.01.16-.02.24zm5.89-1.12c.46-.47 1.31-.47 1.76 0 .24.23.37.55.37.88 0 .08-.01.16-.02.24-.02.08-.04.16-.08.24 0 .01-.01.01-.01.02-.03.06-.06.14-.1.19-.05.07-.1.14-.16.19-.23.24-.55.37-.88.37s-.65-.13-.88-.37c-.06-.05-.11-.12-.16-.19-.04-.05-.07-.13-.1-.19 0-.01-.01-.01-.01-.02-.04-.08-.06-.16-.08-.24-.01-.08-.02-.16-.02-.24 0-.33.13-.65.37-.88zm-4 0c.46-.47 1.3-.47 1.76 0 .24.23.37.55.37.88 0 .08-.01.16-.02.24-.02.08-.04.16-.08.24-.03.06-.06.13-.1.19 0 .01-.01.01-.01.02-.05.07-.1.14-.16.19-.23.24-.55.37-.88.37-.08 0-.16-.01-.24-.02-.08-.02-.16-.04-.24-.08a1.39 1.39 0 01-.21-.11c-.07-.05-.14-.1-.19-.16-.06-.05-.11-.12-.16-.19a1.39 1.39 0 01-.11-.21c-.04-.08-.06-.16-.08-.24-.01-.08-.02-.16-.02-.24 0-.33.13-.65.37-.88zM20 19h-7.22c-1.26 0-2.33-.84-2.67-2h3c2.46 0 4.56-1.51 5.45-3.65.86.47 1.44 1.39 1.44 2.43V19z"/></svg> \ No newline at end of file diff --git a/src/img/info-outline.svg b/src/img/info-outline.svg new file mode 100644 index 0000000000..de305200f4 --- /dev/null +++ b/src/img/info-outline.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="100%" height="100%"><path d="M32 15c-2.5 0-4.5 2-4.5 4.5s2 4.5 4.5 4.5 4.5-2 4.5-4.5-2-4.5-4.5-4.5zm0 14.5c-2.5 0-4.5 2-4.5 4.5v12c0 2.5 2 4.5 4.5 4.5s4.5-2 4.5-4.5V34c0-2.5-2-4.5-4.5-4.5z"/></svg> \ No newline at end of file diff --git a/src/img/info.svg b/src/img/info.svg new file mode 100644 index 0000000000..385da41405 --- /dev/null +++ b/src/img/info.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path class="primary" d="M12 17c-.41 0-.75-.34-.75-.75v-5c0-.41.34-.75.75-.75s.75.34.75.75v5c0 .41-.34.75-.75.75zM12 9c-.26 0-.52-.11-.71-.29-.09-.1-.16-.2-.21-.33A.995.995 0 0111 8c0-.26.11-.52.29-.71.38-.37 1.04-.37 1.41 0 .19.19.3.45.3.71 0 .13-.03.26-.08.38a.988.988 0 01-.54.54c-.12.05-.25.08-.38.08z"/><path class="primary" d="M11.99 22c-2.58 0-5.02-.97-6.89-2.76-1.93-1.84-3.03-4.33-3.1-7s.92-5.2 2.76-7.14a9.974 9.974 0 017-3.1c2.65-.06 5.21.91 7.14 2.76a9.974 9.974 0 013.1 7c.06 2.67-.91 5.21-2.76 7.14a9.974 9.974 0 01-7 3.1h-.25zm.02-18.5h-.21c-2.28.06-4.39.99-5.95 2.64a8.4 8.4 0 00-2.34 6.07c.05 2.27.99 4.38 2.63 5.95a8.416 8.416 0 006.07 2.34c2.27-.05 4.38-.99 5.95-2.63s2.4-3.8 2.34-6.07a8.446 8.446 0 00-2.63-5.95 8.455 8.455 0 00-5.86-2.35z"/></svg> \ No newline at end of file diff --git a/src/img/kusama_icon.svg b/src/img/kusama_icon.svg new file mode 100644 index 0000000000..c321d5fdb4 --- /dev/null +++ b/src/img/kusama_icon.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 441 441"><g data-name="Layer 2"><g data-name="Layer 1"><path stroke="#000" stroke-miterlimit="10" d="M.5.5h440v440H.5z"/><path d="M373.6 127.4c-5.2-4.1-11.4-9.7-22.7-11.1-10.6-1.4-21.4 5.7-28.7 10.4s-21.1 18.5-26.8 22.7-20.3 8.1-43.8 22.2-115.7 73.3-115.7 73.3l24 .3-107 55.1h10.7L48.2 312s13.6 3.6 25-3.6v3.3s127.4-50.2 152-37.2l-15 4.4c1.3 0 25.5 1.6 25.5 1.6a34.34 34.34 0 0015.4 24.8c14.6 9.6 14.9 14.9 14.9 14.9s-7.6 3.1-7.6 7c0 0 11.2-3.4 21.6-3.1a82.64 82.64 0 0119.5 3.1s-.8-4.2-10.9-7-20.1-13.8-25-19.8a28 28 0 01-4.1-27.4c3.5-9.1 15.7-14.1 40.9-27.1 29.7-15.4 36.5-26.8 40.7-35.7s10.4-26.6 13.9-34.9c4.4-10.7 9.8-16.4 14.3-19.8s24.5-10.9 24.5-10.9-15.3-13.3-20.2-17.2z" fill="#fff"/></g></g></svg> \ No newline at end of file diff --git a/src/img/kusama_inline.svg b/src/img/kusama_inline.svg new file mode 100644 index 0000000000..cd740221d9 --- /dev/null +++ b/src/img/kusama_inline.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 128 128"><path className="primary" d="M37.57,71.5l6.85.1L12,88.49l2.68.4-4.49,3.49s1.77.08,3.48,0a14,14,0,0,0,4.77-1.28l-.47,1.15s16.15-5.78,21.15-7.42c18.65-6.11,26.56-4.62,26.56-4.62l-4.17,1.45a31.75,31.75,0,0,0,5.77.53c1.24,0,1.63.84,1.74,1.73a8,8,0,0,0,2.94,4.67,70,70,0,0,1,5.75,4.85c.4.5.57,1-.23,1.6a11.26,11.26,0,0,0-1.66,1.69A34.34,34.34,0,0,1,82.35,96a47.28,47.28,0,0,1,6.22.6,8.74,8.74,0,0,0-2.83-1.9c-1.86-.86-10.2-4.79-10-12.53.15-6.09,9.81-8.27,17.58-13,8.58-5.22,9.51-12.47,11.37-17.52,2.22-6,5.09-8.88,8.22-10.14,1.66-.68,5-1.87,5-1.87s-6-5.81-9.71-7.51a10.93,10.93,0,0,0-9.57.53c-3.86,2.06-8.28,6.19-11,8.88-1.89,1.89-5.8,2.44-10.47,5C67.51,51.84,37.57,71.5,37.57,71.5Z"/></svg> \ No newline at end of file diff --git a/src/img/kusama_logo.svg b/src/img/kusama_logo.svg new file mode 100644 index 0000000000..c914a9bef0 --- /dev/null +++ b/src/img/kusama_logo.svg @@ -0,0 +1 @@ +<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 990 260"><path class="primary" d="M156.82 83.17a23.34 23.34 0 00-32.88 3l-47.09 56.48a23.34 23.34 0 000 29.9L123.94 229a23.34 23.34 0 1035.86-29.89l-34.63-41.51 34.63-41.55a23.34 23.34 0 00-2.98-32.88zM43 77.76a23.34 23.34 0 00-23.34 23.35v113a23.35 23.35 0 0046.69 0v-113A23.34 23.34 0 0043 77.76zm409.6 46.69a23.35 23.35 0 000-46.69h-98.87a23.35 23.35 0 00-23.35 23.35v42.37a23.36 23.36 0 0016.94 22.45l81.93 23.41v1.41h-75.52a23.35 23.35 0 100 46.69h98.87a23.34 23.34 0 0023.34-23.34v-42.37A23.34 23.34 0 00459 149.28l-81.93-23.41v-1.42zM297.23 77.76a23.34 23.34 0 00-23.34 23.35v89.64h-52.18v-89.64a23.35 23.35 0 10-46.69 0v113a23.34 23.34 0 0023.35 23.34h98.86a23.34 23.34 0 0023.35-23.34v-113a23.34 23.34 0 00-23.35-23.35zm272.17 8.4a23.33 23.33 0 00-17.93-8.4h-42.38a23.34 23.34 0 00-23.34 23.35v113a23.34 23.34 0 0023.34 23.34h58.45a23.35 23.35 0 000-46.69h-35.1v-66.3h8.09l58.21 69.85v19.8a23.35 23.35 0 0046.69 0v-28.26A23.35 23.35 0 00640 170.9zm395.53 84.74l-70.62-84.74a23.33 23.33 0 00-17.93-8.4H834a23.34 23.34 0 00-23.35 23.35v113A23.34 23.34 0 00834 237.44h58.44a23.35 23.35 0 000-46.69h-35.1v-66.3h8.09l58.21 69.85v19.8a23.35 23.35 0 0046.69 0v-28.25a23.35 23.35 0 00-5.4-14.95zm-177.67-91a23.34 23.34 0 00-24.94 3.48l-34.25 29.36-34.31-29.37a23.34 23.34 0 00-38.52 17.74v113a23.35 23.35 0 0046.69 0v-62.29l11 9.39a23.35 23.35 0 0030.38 0l10.89-9.34v62.23a23.35 23.35 0 0046.7 0v-113a23.34 23.34 0 00-13.64-21.2zM623.64 67.12V68s32.11-12.7 38.36-9.42l-3.77 1.11c.33 0 6.44.4 6.44.4a8.58 8.58 0 003.87 6.24c3.68 2.43 3.75 3.75 3.75 3.75s-1.91.78-1.91 1.77a20.48 20.48 0 015.46-.79 20.22 20.22 0 014.92.79s-.19-1-2.76-1.77-5.05-3.49-6.3-5a7.06 7.06 0 01-1-6.9c.89-2.28 4-3.55 10.31-6.83 7.49-3.87 9.2-6.77 10.25-9s2.63-6.7 3.5-8.8a11.51 11.51 0 013.6-5c1.11-.85 6.17-2.76 6.17-2.76s-3.86-3.29-5.12-4.28a10.68 10.68 0 00-5.71-2.8c-2.68-.35-5.4 1.43-7.24 2.62s-5.4 4.67-6.84 5.67-5.13 2-11 5.59-29.17 18.46-29.17 18.46l6 .06-26.93 13.97h2.69l-3.88 3a8.69 8.69 0 006.31-.96z"/></svg> \ No newline at end of file diff --git a/src/img/language.svg b/src/img/language.svg new file mode 100644 index 0000000000..5b8b648342 --- /dev/null +++ b/src/img/language.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g id="design"><path class="primary" d="M22 19c0 .55-.45 1-1 1-1.47 0-2.87-.46-4.01-1.26-1.06.79-2.39 1.25-3.86 1.25-.56 0-1-.45-1-1 0-.56.44-1 1-1 .9 0 1.71-.24 2.39-.66-.34-.43-.62-.9-.84-1.41-.22-.5.02-1.09.52-1.31.51-.23 1.1.01 1.32.51.12.27.26.52.42.76a4.8 4.8 0 00.62-1.88H13c-.55 0-1-.45-1-1s.45-1 1-1h2.87v-1c0-.55.44-1 1-1 .55 0 1 .45 1 1v1h3.11c.56 0 1 .45 1 1s-.44 1-1 1h-1.4a6.682 6.682 0 01-1.18 3.28c.76.46 1.66.72 2.6.72.55 0 1 .45 1 1zM6.23 4h-.46C3.69 4 2 5.72 2 7.84V14c0 .55.45 1 1 1s1-.45 1-1v-3h4v3c0 .55.45 1 1 1s1-.45 1-1V7.84C10 5.72 8.31 4 6.23 4zM4 9V7.84C4 6.83 4.79 6 5.77 6h.46C7.21 6 8 6.83 8 7.84V9H4z"/></g></svg> \ No newline at end of file diff --git a/src/img/ledgerLogo.svg b/src/img/ledgerLogo.svg new file mode 100644 index 0000000000..32b2408ed8 --- /dev/null +++ b/src/img/ledgerLogo.svg @@ -0,0 +1,3 @@ +<svg width="383" height="128" viewBox="0 0 383 128" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M327.629 119.94V127.998H382.998V91.6548H374.931V119.94H327.629ZM327.629 0V8.05844H374.931V36.3452H382.998V0H327.629ZM299.075 62.3411V43.6158H311.731C317.901 43.6158 320.116 45.6696 320.116 51.2803V54.5982C320.116 60.3657 317.98 62.3411 311.731 62.3411H299.075ZM319.165 65.6589C324.939 64.1578 328.972 58.7842 328.972 52.3856C328.972 48.3564 327.391 44.7211 324.385 41.7972C320.589 38.1619 315.525 36.3452 308.961 36.3452H291.164V91.6529H299.075V69.6097H310.94C317.03 69.6097 319.483 72.1378 319.483 78.4599V91.6548H327.55V79.7239C327.55 71.0325 325.494 67.7147 319.165 66.7662V65.6589ZM252.565 67.4756H276.928V60.207H252.565V43.6139H279.3V36.3452H244.496V91.6529H280.487V84.3842H252.565V67.4756ZM226.065 70.3995V74.1916C226.065 82.1717 223.138 84.78 215.783 84.78H214.043C206.685 84.78 203.126 82.4088 203.126 71.4264V56.5717C203.126 45.5109 206.844 43.2181 214.2 43.2181H215.781C222.979 43.2181 225.273 45.9048 225.351 53.3322H234.052C233.262 42.4283 225.985 35.5555 215.069 35.5555C209.77 35.5555 205.34 37.2153 202.018 40.3745C197.035 45.0367 194.266 52.9383 194.266 63.9991C194.266 74.6659 196.64 82.5675 201.543 87.4649C204.865 90.7044 209.454 92.4426 213.962 92.4426C218.708 92.4426 223.06 90.5456 225.273 86.438H226.379V91.6529H233.656V63.1309H212.22V70.3995H226.065ZM156.301 43.6139H164.924C173.072 43.6139 177.502 45.6677 177.502 56.7304V71.2677C177.502 82.3285 173.072 84.3842 164.924 84.3842H156.301V43.6139ZM165.634 91.6548C180.743 91.6548 186.358 80.1982 186.358 64.001C186.358 47.5666 180.346 36.3471 165.475 36.3471H148.389V91.6548H165.634ZM110.186 67.4756H134.549V60.207H110.186V43.6139H136.921V36.3452H102.116V91.6529H138.108V84.3842H110.186V67.4756ZM63.5175 36.3452H55.45V91.6529H91.8359V84.3842H63.5175V36.3452ZM0 91.6548V128H55.3696V119.94H8.06747V91.6548H0ZM0 0V36.3452H8.06747V8.05844H55.3696V0H0Z" fill="black"/> +</svg> diff --git a/src/img/logo-github.svg b/src/img/logo-github.svg new file mode 100644 index 0000000000..06087bf954 --- /dev/null +++ b/src/img/logo-github.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M256 32C132.3 32 32 134.9 32 261.7c0 101.5 64.2 187.5 153.2 217.9a17.56 17.56 0 003.8.4c8.3 0 11.5-6.1 11.5-11.4 0-5.5-.2-19.9-.3-39.1a102.4 102.4 0 01-22.6 2.7c-43.1 0-52.9-33.5-52.9-33.5-10.2-26.5-24.9-33.6-24.9-33.6-19.5-13.7-.1-14.1 1.4-14.1h.1c22.5 2 34.3 23.8 34.3 23.8 11.2 19.6 26.2 25.1 39.6 25.1a63 63 0 0025.6-6c2-14.8 7.8-24.9 14.2-30.7-49.7-5.8-102-25.5-102-113.5 0-25.1 8.7-45.6 23-61.6-2.3-5.8-10-29.2 2.2-60.8a18.64 18.64 0 015-.5c8.1 0 26.4 3.1 56.6 24.1a208.21 208.21 0 01112.2 0c30.2-21 48.5-24.1 56.6-24.1a18.64 18.64 0 015 .5c12.2 31.6 4.5 55 2.2 60.8 14.3 16.1 23 36.6 23 61.6 0 88.2-52.4 107.6-102.3 113.3 8 7.1 15.2 21.1 15.2 42.5 0 30.7-.3 55.5-.3 63 0 5.4 3.1 11.5 11.4 11.5a19.35 19.35 0 004-.4C415.9 449.2 480 363.1 480 261.7 480 134.9 379.7 32 256 32z"/></svg> \ No newline at end of file diff --git a/src/img/moon-outline.svg b/src/img/moon-outline.svg new file mode 100644 index 0000000000..c85150b1f6 --- /dev/null +++ b/src/img/moon-outline.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><title>Moon \ No newline at end of file diff --git a/src/img/polkadot_icon.svg b/src/img/polkadot_icon.svg new file mode 100644 index 0000000000..4186d21b4a --- /dev/null +++ b/src/img/polkadot_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/img/polkadot_inline.svg b/src/img/polkadot_inline.svg new file mode 100644 index 0000000000..b1c7a71678 --- /dev/null +++ b/src/img/polkadot_inline.svg @@ -0,0 +1,10 @@ + + diff --git a/src/img/polkadot_logo.svg b/src/img/polkadot_logo.svg new file mode 100755 index 0000000000..3362d3ee97 --- /dev/null +++ b/src/img/polkadot_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/img/sunny-outline.svg b/src/img/sunny-outline.svg new file mode 100644 index 0000000000..5f3f21a161 --- /dev/null +++ b/src/img/sunny-outline.svg @@ -0,0 +1 @@ +Sunny \ No newline at end of file diff --git a/src/img/wallet.svg b/src/img/wallet.svg new file mode 100644 index 0000000000..cf82222532 --- /dev/null +++ b/src/img/wallet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/img/walletConnect.svg b/src/img/walletConnect.svg new file mode 100644 index 0000000000..afd307e46b --- /dev/null +++ b/src/img/walletConnect.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/img/westend_icon.svg b/src/img/westend_icon.svg new file mode 100644 index 0000000000..f2e18607a5 --- /dev/null +++ b/src/img/westend_icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/img/westend_inline.svg b/src/img/westend_inline.svg new file mode 100644 index 0000000000..61659f44d1 --- /dev/null +++ b/src/img/westend_inline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/img/westend_logo.svg b/src/img/westend_logo.svg new file mode 100644 index 0000000000..d4acb7e391 --- /dev/null +++ b/src/img/westend_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/library/Account/Default.tsx b/src/library/Account/Default.tsx new file mode 100644 index 0000000000..c3b2d57f96 --- /dev/null +++ b/src/library/Account/Default.tsx @@ -0,0 +1,79 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faGlasses } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ellipsisFn, remToUnit } from '@polkadot-cloud/utils'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Polkicon } from '@polkadot-cloud/react'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { Wrapper } from './Wrapper'; +import type { AccountProps } from './types'; + +export const Account = ({ + fontSize = '1.05rem', + format, + value, + label, + readOnly, + canClick, + title, + onClick, +}: AccountProps) => { + const { t } = useTranslation('library'); + const { getAccount } = useImportedAccounts(); + + const [displayValue, setDisplayValue] = useState(); + + const unassigned = value === null || value === undefined || !value.length; + + useEffect(() => { + // format value based on `format` prop + switch (format) { + case 'name': + setDisplayValue( + value !== '' + ? getAccount(value)?.name || ellipsisFn(value) + : ellipsisFn(value) + ); + break; + case 'text': + setDisplayValue(value); + break; + default: + if (value) setDisplayValue(ellipsisFn(value)); + } + + // if title prop is provided, override `displayValue` + if (title !== undefined) setDisplayValue(title); + }, [value, title]); + + return ( + + {label !== undefined && ( +
+ {label}{' '} + {readOnly && ( + <> +   + + + )} +
+ )} + {unassigned ? ( + {t('notStaking')} + ) : ( + <> + {format !== 'text' && ( + + + + )} + {displayValue} + + )} +
+ ); +}; diff --git a/src/library/Account/Pool.tsx b/src/library/Account/Pool.tsx new file mode 100644 index 0000000000..0c416f7832 --- /dev/null +++ b/src/library/Account/Pool.tsx @@ -0,0 +1,87 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { u8aToString, u8aUnwrapBytes } from '@polkadot/util'; +import { ellipsisFn, remToUnit } from '@polkadot-cloud/utils'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { Polkicon } from '@polkadot-cloud/react'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Wrapper } from './Wrapper'; +import type { AccountProps } from './types'; + +export const Account = ({ + label, + pool, + onClick, + canClick, + fontSize = '1.05rem', +}: AccountProps) => { + const { t } = useTranslation('library'); + const { isReady } = useApi(); + const { activeAccount } = useActiveAccounts(); + const { fetchPoolsMetaBatch, meta } = useBondedPools(); + + // is this the initial fetch + const [fetched, setFetched] = useState(false); + + const batchKey = 'pool_header'; + + // refetch when pool or active account changes + useEffect(() => { + setFetched(false); + }, [activeAccount, pool]); + + // configure pool list when network is ready to fetch + useEffect(() => { + if (isReady) { + setFetched(true); + + if (!fetched) { + getPoolMeta(); + } + } + }, [isReady, fetched]); + + // handle pool list bootstrapping + const getPoolMeta = () => { + const pools: any = [{ id: pool.id }]; + fetchPoolsMetaBatch(batchKey, pools, true); + }; + + const metaBatch = meta[batchKey]; + const metaData = metaBatch?.metadata?.[0]; + const syncing = metaData === undefined; + + // display value + const defaultDisplay = ellipsisFn(pool.addresses.stash); + let display = syncing ? t('syncing') : metaData ?? defaultDisplay; + + // check if super identity has been byte encoded + const displayAsBytes = u8aToString(u8aUnwrapBytes(display)); + if (displayAsBytes !== '') { + display = displayAsBytes; + } + // if still empty string, default to clipped address + if (display === '') { + display = defaultDisplay; + } + + return ( + + {label !== undefined &&
{label}
} + + + + + + {display} + +
+ ); +}; diff --git a/src/library/Account/Wrapper.ts b/src/library/Account/Wrapper.ts new file mode 100644 index 0000000000..6a912c8ce2 --- /dev/null +++ b/src/library/Account/Wrapper.ts @@ -0,0 +1,64 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import type { WrapperProps } from './types'; + +export const Wrapper = styled.button` + border: 1px solid var(--border-primary-color); + transition: transform var(--transition-duration) ease-out; + cursor: ${(props) => (props.$canClick ? 'pointer' : 'default')}; + font-size: ${(props) => props.$fontSize}; + border-radius: 1.25rem; + box-shadow: none; + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 0 1rem; + max-width: 235px; + flex: 1; + &:hover { + transform: scale(1.03); + } + .identicon { + margin: 0.15rem 0.25rem 0 0; + } + .account-label { + border-right: 1px solid var(--border-secondary-color); + color: var(--text-color-secondary); + font-size: 0.8em; + display: flex; + align-items: center; + justify-content: flex-end; + margin-right: 0.5rem; + padding-right: 0.5rem; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + flex-shrink: 1; + + > svg { + color: var(--text-color-primary); + } + } + + .title { + color: var(--text-color-secondary); + font-family: InterSemiBold, sans-serif; + margin-left: 0.25rem; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + line-height: 2.25rem; + flex: 1; + + &.syncing { + opacity: 0.4; + } + + &.unassigned { + color: var(--text-color-secondary); + opacity: 0.45; + } + } +`; diff --git a/src/library/Account/types.ts b/src/library/Account/types.ts new file mode 100644 index 0000000000..a869a08ace --- /dev/null +++ b/src/library/Account/types.ts @@ -0,0 +1,19 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface AccountProps { + onClick?: () => void; + value: string; + format: string; + label?: string; + canClick: boolean; + fontSize?: string; + title?: string; + readOnly?: boolean; + pool?: any; +} + +export interface WrapperProps { + $canClick: boolean; + $fontSize: string; +} diff --git a/src/library/AccountInput/Wrapper.ts b/src/library/AccountInput/Wrapper.ts new file mode 100644 index 0000000000..b0292c59a8 --- /dev/null +++ b/src/library/AccountInput/Wrapper.ts @@ -0,0 +1,93 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const AccountInputWrapper = styled.div` + position: relative; + width: 100%; + margin-top: 0.5rem; + + &.inactive { + opacity: 0.5; + } + + .inactive-block { + position: absolute; + z-index: 2; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + &.border { + > .input { + border: 1px solid var(--border-primary-color); + } + } + + .input { + border: 1px solid transparent; + border-radius: 1rem; + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 0.5rem 0.5rem 0.5rem 1rem; + + &.disabled { + background: var(--background-default); + } + > section { + display: flex; + flex-flow: row wrap; + align-items: center; + + > div { + &:first-child { + padding-right: 0.5rem; + .ph { + background: var(--background-default); + width: 22px; + height: 22px; + border-radius: 50%; + } + } + &:last-child { + display: flex; + flex-flow: column wrap; + flex-grow: 1; + + > input { + font-family: InterSemiBold, sans-serif; + width: 100%; + border: none; + margin: 0; + padding-right: 1rem; + + &:disabled { + opacity: 0.75; + } + } + } + } + + &:first-child { + flex: 1; + } + } + } + h5 { + margin: 0.75rem 0.25rem; + &.neutral { + color: var(--text-color-primary); + opacity: 0.8; + } + &.danger { + color: var(--status-danger-color); + } + &.success { + color: var(--status-success-color); + } + } +`; diff --git a/src/library/AccountInput/index.tsx b/src/library/AccountInput/index.tsx new file mode 100644 index 0000000000..6da53ba134 --- /dev/null +++ b/src/library/AccountInput/index.tsx @@ -0,0 +1,216 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ButtonSecondary, Polkicon } from '@polkadot-cloud/react'; +import { isValidAddress } from '@polkadot-cloud/utils'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useNetwork } from 'contexts/Network'; +import { formatAccountSs58 } from 'contexts/Connect/Utils'; +import { AccountInputWrapper } from './Wrapper'; +import type { AccountInputProps } from './types'; + +export const AccountInput = ({ + successCallback, + resetCallback, + defaultLabel, + resetOnSuccess = false, + successLabel, + locked = false, + inactive = false, + disallowAlreadyImported = true, + initialValue = null, + border = true, +}: AccountInputProps) => { + const { t } = useTranslation('library'); + + const { + networkData: { ss58 }, + } = useNetwork(); + const { accounts } = useImportedAccounts(); + const { setModalResize } = useOverlay().modal; + + // store current input value + const [value, setValue] = useState(initialValue || ''); + + // store whether current input value is valid + const [valid, setValid] = useState(null); + + // store whether address was formatted (displays confirm prompt) + const [reformatted, setReformatted] = useState(false); + + // store whether the form is being submitted. + const [submitting, setSubmitting] = useState(false); + + // store whether account input is in success lock state. + const [successLock, setSuccessLocked] = useState(locked); + + const handleChange = (e: React.FormEvent) => { + const newValue = e.currentTarget.value; + // set value on key change + setValue(newValue); + + // reset reformatted if true - value has changed + if (reformatted) { + setReformatted(false); + } + + // reset valid if empty value + if (newValue === '') { + setValid(null); + return; + } + // check address already imported + const alreadyImported = accounts.find( + (a) => a.address.toUpperCase() === newValue.toUpperCase() + ); + if (alreadyImported !== undefined && disallowAlreadyImported) { + setValid('already_imported'); + return; + } + // check if valid address + setValid(isValidAddress(newValue) ? 'valid' : 'not_valid'); + }; + + const handleImport = async () => { + // reformat address if in wrong format + const addressFormatted = formatAccountSs58(value, ss58); + if (addressFormatted) { + setValid('confirm_reformat'); + setValue(addressFormatted); + setReformatted(true); + } else { + // handle successful import. + setSubmitting(true); + const result = await successCallback(value); + setSubmitting(false); + + // reset state on successful import. + if (result && resetOnSuccess) { + resetInput(); + } else { + // flag reset & lock state. + setSuccessLocked(true); + } + } + }; + + // If initial value changes, update current input value. + useEffect(() => { + setValue(initialValue || ''); + }, [initialValue]); + + let label; + let labelClass; + const showSuccess = successLock && successLabel; + + switch (valid) { + case 'confirm_reformat': + label = t('confirmReformat'); + labelClass = 'neutral'; + + break; + case 'already_imported': + label = t('alreadyImported'); + labelClass = 'danger'; + break; + case 'not_valid': + label = t('invalid'); + labelClass = 'danger'; + break; + case 'valid': + label = showSuccess ? successLabel : t('valid'); + labelClass = showSuccess ? 'neutral' : 'success'; + break; + default: + label = showSuccess ? successLabel : defaultLabel; + labelClass = 'neutral'; + break; + } + + const handleConfirm = () => { + setValid('valid'); + setReformatted(false); + handleImport(); + }; + + const resetInput = () => { + setReformatted(false); + setValue(''); + setValid(null); + setModalResize(); + setSuccessLocked(false); + if (resetCallback) { + resetCallback(); + } + }; + + const className = []; + if (inactive) className.push('inactive'); + if (border) className.push('border'); + + return ( + + {inactive &&
} +
+ {successLock && ( + <> + +   + + )}{' '} + {label} +
+
+
+
+ {isValidAddress(value) ? ( + + ) : ( +
+ )} +
+
+ ) => + handleChange(e) + } + value={value} + disabled={successLock} + /> +
+
+
+ {successLock ? ( + <> + resetInput()} text={t('reset')} /> + + ) : ( + <> + {!reformatted ? ( + handleImport()} + text={submitting ? t('importing') : t('import')} + disabled={valid !== 'valid' || submitting} + /> + ) : ( + handleConfirm()} + text={t('confirm')} + /> + )} + + )} +
+
+ + ); +}; diff --git a/src/library/AccountInput/types.ts b/src/library/AccountInput/types.ts new file mode 100644 index 0000000000..583af2aaa5 --- /dev/null +++ b/src/library/AccountInput/types.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyApi, MaybeAddress } from 'types'; + +export interface AccountInputProps { + successCallback: (a: string) => Promise; + resetCallback?: () => void; + defaultLabel: string; + resetOnSuccess?: boolean; + successLabel?: string; + locked?: boolean; + inactive?: boolean; + disallowAlreadyImported?: boolean; + initialValue?: MaybeAddress; + border?: boolean; +} diff --git a/src/library/BarChart/BarSegment.tsx b/src/library/BarChart/BarSegment.tsx new file mode 100644 index 0000000000..d2172519db --- /dev/null +++ b/src/library/BarChart/BarSegment.tsx @@ -0,0 +1,26 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { BarSegmentShowLabelThreshold } from './defaults'; +import type { BarSegmentProps } from './types'; + +export const BarSegment = ({ + dataClass, + widthPercent, + flexGrow, + label, + forceShow, +}: BarSegmentProps) => ( +
+ {widthPercent >= BarSegmentShowLabelThreshold || + (widthPercent < BarSegmentShowLabelThreshold && forceShow) ? ( + {label} + ) : null} +
+); diff --git a/src/library/BarChart/BondedChart.tsx b/src/library/BarChart/BondedChart.tsx new file mode 100644 index 0000000000..983c8168cd --- /dev/null +++ b/src/library/BarChart/BondedChart.tsx @@ -0,0 +1,103 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { greaterThanZero } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { BarSegment } from 'library/BarChart/BarSegment'; +import { LegendItem } from 'library/BarChart/LegendItem'; +import { Bar, BarChartWrapper, Legend } from 'library/BarChart/Wrappers'; +import { useNetwork } from 'contexts/Network'; +import type { BondedChartProps } from '../../pages/Nominate/Active/types'; + +export const BondedChart = ({ + active, + free, + unlocking, + unlocked, + inactive, +}: BondedChartProps) => { + const { t } = useTranslation('library'); + const { + networkData: { unit }, + } = useNetwork(); + const totalUnlocking = unlocking.plus(unlocked); + + const MinimumLowerBound = 0.5; + const MinimumNoNZeroPercent = 13; + + // graph percentages + const graphTotal = active.plus(totalUnlocking).plus(free); + + const graphActive = greaterThanZero(active) + ? BigNumber.max( + active.dividedBy(graphTotal.multipliedBy(0.01)), + active.isGreaterThan(MinimumLowerBound) ? MinimumNoNZeroPercent : 0 + ) + : new BigNumber(0); + + const graphUnlocking = greaterThanZero(totalUnlocking) + ? BigNumber.max( + totalUnlocking.dividedBy(graphTotal.multipliedBy(0.01)), + totalUnlocking.isGreaterThan(MinimumLowerBound) + ? MinimumNoNZeroPercent + : 0 + ) + : new BigNumber(0); + + const freeBalance = free.decimalPlaces(3); + const remaining = new BigNumber(100).minus(graphActive).minus(graphUnlocking); + + const graphFree = greaterThanZero(remaining) + ? BigNumber.max( + remaining, + freeBalance.isGreaterThan(MinimumLowerBound) ? MinimumNoNZeroPercent : 0 + ) + : new BigNumber(0); + + return ( + <> + + + {totalUnlocking.plus(active).isZero() ? ( + + ) : greaterThanZero(active) ? ( + + ) : null} + + {greaterThanZero(totalUnlocking) ? ( + + ) : null} + + {greaterThanZero(totalUnlocking.plus(active)) ? ( + + ) : null} + + + + + + + + + ); +}; diff --git a/src/library/BarChart/LegendItem.tsx b/src/library/BarChart/LegendItem.tsx new file mode 100644 index 0000000000..8ec768a126 --- /dev/null +++ b/src/library/BarChart/LegendItem.tsx @@ -0,0 +1,25 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp } from '@polkadot-cloud/react'; +import { useHelp } from 'contexts/Help'; +import type { LegendItemProps } from './types'; + +export const LegendItem = ({ + dataClass, + label, + helpKey, + button, +}: LegendItemProps) => { + const { openHelp } = useHelp(); + + return ( +

+ {dataClass ? : null} {label} + {helpKey ? ( + openHelp(helpKey)} /> + ) : null} + {button && button} +

+ ); +}; diff --git a/src/library/BarChart/Wrappers.ts b/src/library/BarChart/Wrappers.ts new file mode 100644 index 0000000000..b4d92e3434 --- /dev/null +++ b/src/library/BarChart/Wrappers.ts @@ -0,0 +1,109 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const BarChartWrapper = styled.div<{ $lessPadding?: boolean }>` + padding: ${(props) => (props.$lessPadding ? '0' : '0 0.5rem')}; + margin-top: 1rem; + width: 100%; + + .available { + display: flex; + margin-top: 2.7rem; + width: 100%; + + > div { + display: flex; + flex-flow: row wrap; + padding: 0 0.35rem; + &:first-child { + padding-left: 0; + } + &:last-child { + padding-right: 0; + } + } + } + .d1 { + background: var(--accent-color-primary); + color: rgba(255, 255, 255, 0.95); + } + .d2 { + background: var(--accent-color-secondary); + color: rgba(255, 255, 255, 0.95); + } + .d3 { + background: var(--text-color-secondary); + color: rgba(255, 255, 255, 0.95); + } + .d4 { + background: var(--button-tertiary-background); + color: var(--text-color-secondary); + } +`; + +export const Legend = styled.div` + width: 100%; + margin-bottom: 0.4rem; + display: flex; + align-items: flex-end; + height: 2.5rem; + + &.end { + > h4 { + flex-direction: row; + flex-grow: 1; + justify-content: flex-end; + padding-right: 0; + } + } + + > h4 { + font-family: InterSemiBold, sans-serif; + display: flex; + align-items: center; + font-size: 1.1rem; + margin: 0; + padding: 0.5rem 1rem 0.25rem 1rem; + + &:first-child { + padding-left: 0; + } + > span { + width: 1rem; + height: 1rem; + margin-right: 0.5rem; + border-radius: 0.25rem; + } + } +`; + +export const Bar = styled.div` + background: var(--button-secondary-background); + border-radius: 0.65rem; + display: flex; + overflow: hidden; + height: 3.75rem; + width: 100%; + + > div { + position: relative; + height: 100%; + display: flex; + align-items: center; + transition: width 1.5s cubic-bezier(0, 1, 0, 1); + + > span { + font-family: InterBold, sans-serif; + position: absolute; + left: 0; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding: 0 0.8rem; + width: 100%; + font-size: 1.05rem; + } + } +`; diff --git a/src/library/BarChart/defaults.ts b/src/library/BarChart/defaults.ts new file mode 100644 index 0000000000..7d7eb7ce51 --- /dev/null +++ b/src/library/BarChart/defaults.ts @@ -0,0 +1,5 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +// Bar segment width threshold (as a percentage) to display graph labels. +export const BarSegmentShowLabelThreshold = 9; diff --git a/src/library/BarChart/types.ts b/src/library/BarChart/types.ts new file mode 100644 index 0000000000..84180f05a6 --- /dev/null +++ b/src/library/BarChart/types.ts @@ -0,0 +1,21 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type React from 'react'; + +export type DataClass = 'd1' | 'd2' | 'd3' | 'd4'; + +export interface LegendItemProps { + dataClass?: DataClass; + label: string; + helpKey?: string; + button?: React.ReactNode; +} + +export interface BarSegmentProps { + dataClass: DataClass; + label?: string; + widthPercent: number; + flexGrow: number; + forceShow?: boolean; +} diff --git a/src/library/Card/Wrappers.ts b/src/library/Card/Wrappers.ts new file mode 100644 index 0000000000..f8f0441ba4 --- /dev/null +++ b/src/library/Card/Wrappers.ts @@ -0,0 +1,130 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { SideMenuStickyThreshold } from 'consts'; +import type { CardHeaderWrapperProps, CardWrapperProps } from '../Graphs/types'; + +/* CardHeaderWrapper + * + * Used as headers for individual cards. Usually a h4 accompanied + * with a h2. withAction allows a full-width header with a right-side + * button. + */ +export const CardHeaderWrapper = styled.div` + display: flex; + flex-flow: ${(props) => (props.$withAction ? 'row' : 'column')} wrap; + align-items: ${(props) => (props.$withAction ? 'center' : 'none')}; + justify-content: ${(props) => (props.$withAction ? 'none' : 'center')}; + margin-bottom: ${(props) => (props.$withMargin ? '1rem' : 0)}; + padding: 0rem 0.25rem; + width: 100%; + + h2 { + font-family: InterBold, sans-serif; + margin-bottom: 1rem; + } + h2, + h3 { + color: var(--text-color-primary); + display: flex; + flex-flow: row wrap; + align-items: center; + flex-grow: ${(props) => (props.$withAction ? 1 : 0)}; + + @media (max-width: ${SideMenuStickyThreshold}px) { + margin-top: 0.5rem; + } + } + h3, + h4 { + font-family: InterSemiBold, sans-serif; + } + h4 { + margin-top: 0; + margin-bottom: 0.4rem; + display: flex; + flex-flow: row wrap; + align-items: center; + flex-grow: ${(props) => (props.$withAction ? 1 : 0)}; + } + .note { + color: var(--text-color-secondary); + font-family: InterSemiBold, sans-serif; + font-size: 1.1rem; + margin-top: 0.2rem; + margin-left: 0.4rem; + } + .networkIcon { + width: 1.9rem; + height: 1.9rem; + margin-right: 0.55rem; + } + + > div { + display: flex; + align-items: center; + } +`; + +/* CardWrapper + * + * Used to separate the main modules throughout the app. + */ +export const CardWrapper = styled.div` + box-shadow: var(--card-shadow); + background: var(--background-primary); + border-radius: 1.1rem; + display: flex; + flex-direction: column; + flex: 1; + position: relative; + overflow: hidden; + margin-top: 1.4rem; + padding: 1.5rem; + + &.transparent { + background: none; + border: none; + border-radius: 0; + box-shadow: none; + margin-top: 0; + padding: 0; + } + + &.warning { + border: 1px solid var(--status-warning-color); + } + + @media (max-width: ${SideMenuStickyThreshold}px) { + padding: 1rem 0.75rem; + } + + @media (min-width: ${SideMenuStickyThreshold + 1}px) { + height: ${(props) => (props.height ? `${props.height}px` : 'inherit')}; + } + + .inner { + padding: 1rem; + display: flex; + flex-flow: column nowrap; + width: 100%; + position: relative; + } + + .content { + padding: 0 0.5rem; + + h3, + h4 { + margin-top: 0; + } + h3 { + margin-bottom: 0.75rem; + } + + h4 { + margin-bottom: 0; + } + } +`; diff --git a/src/library/Countdown/index.tsx b/src/library/Countdown/index.tsx new file mode 100644 index 0000000000..dc8da539e2 --- /dev/null +++ b/src/library/Countdown/index.tsx @@ -0,0 +1,58 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import type { CountdownProps } from './types'; + +export const Countdown = ({ timeleft, markup = true }: CountdownProps) => { + const { t } = useTranslation('base'); + const { days, hours, minutes, seconds } = timeleft; + + const secondsNumber = seconds ? seconds[0] : 0; + const secondsLabel = seconds + ? seconds[1] + : t('second', { count: secondsNumber }); + + if (markup) { + return ( + <> + {days[0] > 0 ? ( + <> + {days[0]} {days[1]} + + ) : null} + {hours[0] > 0 ? ( + <> + {hours[0]} {hours[1]} + + ) : null} + {minutes[0] > 0 ? ( + <> + {minutes[0]} {minutes[1]} + + ) : null} + {days[0] === 0 && hours[0] === 0 && minutes[0] > 0 ? ( + <>:  + ) : null} + + {days[0] === 0 && hours[0] === 0 && ( + <> + {secondsNumber} + {minutes[0] === 0 ? {secondsLabel} : null} + + )} + + ); + } + + return ( + <> + {days[0] > 0 ? `${days[0]} ${days[1]} ` : null} + {hours[0] > 0 ? `${hours[0]} ${hours[1]} ` : null} + {minutes[0] > 0 ? `${minutes[0]} ${minutes[1]} ` : null} + {days[0] === 0 && hours[0] === 0 + ? `${secondsNumber} ${minutes[0] === 0 ? secondsLabel : ''}` + : null} + + ); +}; diff --git a/src/library/Countdown/types.ts b/src/library/Countdown/types.ts new file mode 100644 index 0000000000..cf96ae2caa --- /dev/null +++ b/src/library/Countdown/types.ts @@ -0,0 +1,9 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { TimeLeftFormatted } from 'library/Hooks/useTimeLeft/types'; + +export interface CountdownProps { + timeleft: TimeLeftFormatted; + markup?: boolean; +} diff --git a/src/library/ErrorBoundary/Wrapper.ts b/src/library/ErrorBoundary/Wrapper.ts new file mode 100644 index 0000000000..412ff6f325 --- /dev/null +++ b/src/library/ErrorBoundary/Wrapper.ts @@ -0,0 +1,48 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 100%; + display: flex; + flex-flow: column wrap; + justify-content: center; + align-items: center; + + &.app { + height: 100vh; + } + &.modal { + padding: 0.75rem 0 0 0; + } + + h1, + h2 { + color: var(--text-color-secondary); + } + + h2 { + margin: 1rem 0; + } + + h3 { + margin: 1rem 0; + &.with-margin { + margin-top: 10rem; + } + margin-bottom: 3rem; + svg { + color: var(--text-color-secondary); + } + } + + button { + color: var(--text-color-secondary); + font-size: 1.25rem; + + &:hover { + opacity: 0.75; + } + } +`; diff --git a/src/library/ErrorBoundary/index.tsx b/src/library/ErrorBoundary/index.tsx new file mode 100644 index 0000000000..e92fa6318d --- /dev/null +++ b/src/library/ErrorBoundary/index.tsx @@ -0,0 +1,74 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBug } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import React from 'react'; +import { Wrapper } from './Wrapper'; + +export const ErrorFallbackApp = ({ + resetErrorBoundary, +}: { + resetErrorBoundary: () => void; +}) => { + const { t } = useTranslation('library'); + + return ( + +

+ +

+

{t('errorUnknown')}

+

+ +

+
+ ); +}; + +export const ErrorFallbackRoutes = ({ + resetErrorBoundary, +}: { + resetErrorBoundary: () => void; +}) => { + const { t } = useTranslation('library'); + + return ( + +

+ +

+

{t('errorUnknown')}

+

+ +

+
+ ); +}; + +interface ErrorFallbackProps { + resetErrorBoundary?: () => void; +} +export const ErrorFallbackModal: React.FC = (props: ErrorFallbackProps) => { + const { resetErrorBoundary } = props; + const { t } = useTranslation('library'); + + return ( + +

{t('errorUnknown')}

+

+ +

+
+ ); +}; diff --git a/src/library/EstimatedTxFee/Wrapper.ts b/src/library/EstimatedTxFee/Wrapper.ts new file mode 100644 index 0000000000..ad43da3884 --- /dev/null +++ b/src/library/EstimatedTxFee/Wrapper.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + p { + color: var(--text-color-secondary); + padding: 0; + font-size: 1rem; + margin: 0.5rem 0; + + > span { + margin: 0 0.5rem 0 0; + } + } +`; diff --git a/src/library/EstimatedTxFee/index.tsx b/src/library/EstimatedTxFee/index.tsx new file mode 100644 index 0000000000..6308a02ee8 --- /dev/null +++ b/src/library/EstimatedTxFee/index.tsx @@ -0,0 +1,58 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit } from '@polkadot-cloud/utils'; +import type { Context } from 'react'; +import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { TxMetaContext, useTxMeta } from 'contexts/TxMeta'; +import type { TxMetaContextInterface } from 'contexts/TxMeta/types'; +import { useNetwork } from 'contexts/Network'; +import { Wrapper } from './Wrapper'; +import type { EstimatedTxFeeProps } from './types'; + +export const EstimatedTxFeeInner = ({ format }: EstimatedTxFeeProps) => { + const { t } = useTranslation('library'); + const { unit, units } = useNetwork().networkData; + const { txFees, resetTxFees } = useTxMeta(); + + useEffect(() => () => resetTxFees(), []); + + const txFeesUnit = planckToUnit(txFees, units).toFormat(); + + return ( + <> + {format === 'table' ? ( + <> +
{t('estimatedFee')}:
+
{txFees.isZero() ? `...` : `${txFeesUnit} ${unit}`}
+ + ) : ( + +

+ {t('estimatedFee')}: + {txFees.isZero() ? `...` : `${txFeesUnit} ${unit}`} +

+
+ )} + + ); +}; + +export class EstimatedTxFee extends React.Component { + static contextType: Context = TxMetaContext; + + componentDidMount(): void { + const { resetTxFees } = this.context as TxMetaContextInterface; + resetTxFees(); + } + + componentWillUnmount(): void { + const { resetTxFees } = this.context as TxMetaContextInterface; + resetTxFees(); + } + + render() { + return ; + } +} diff --git a/src/library/EstimatedTxFee/types.ts b/src/library/EstimatedTxFee/types.ts new file mode 100644 index 0000000000..c139c49779 --- /dev/null +++ b/src/library/EstimatedTxFee/types.ts @@ -0,0 +1,6 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface EstimatedTxFeeProps { + format?: string; +} diff --git a/src/library/Filter/Container.tsx b/src/library/Filter/Container.tsx new file mode 100644 index 0000000000..8ebbff912f --- /dev/null +++ b/src/library/Filter/Container.tsx @@ -0,0 +1,13 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React from 'react'; +import { Wrapper } from './Wrappers'; + +export const Container = ({ children }: { children: React.ReactNode }) => ( + +
+
{children}
+
+
+); diff --git a/src/library/Filter/Item.tsx b/src/library/Filter/Item.tsx new file mode 100644 index 0000000000..999bcf1701 --- /dev/null +++ b/src/library/Filter/Item.tsx @@ -0,0 +1,36 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { motion } from 'framer-motion'; +import { ItemWrapper } from './Wrappers'; +import type { ItemProps } from './types'; + +export const Item = ({ + disabled = false, + icon, + label, + transform, + onClick, +}: ItemProps) => ( + { + if (onClick !== undefined) onClick(); + }} + disabled={disabled} + > + + {icon ? ( +
+ +
+ ) : null} +

{label}

+
+
+); diff --git a/src/library/Filter/LargeItem.tsx b/src/library/Filter/LargeItem.tsx new file mode 100644 index 0000000000..c6f3b57d54 --- /dev/null +++ b/src/library/Filter/LargeItem.tsx @@ -0,0 +1,47 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { motion } from 'framer-motion'; +import { LargeItemWrapper } from './Wrappers'; + +export const LargeItem = ({ + disabled = false, + active, + icon, + title, + subtitle, + transform, + onClick, +}: any) => ( + onClick()} + className="item" + style={{ + opacity: disabled ? 0.5 : 1, + }} + > + +
+
+ + +

{title}

+
+
+

{subtitle}

+
+
+
+
+); diff --git a/src/library/Filter/Tabs.tsx b/src/library/Filter/Tabs.tsx new file mode 100644 index 0000000000..876f879fce --- /dev/null +++ b/src/library/Filter/Tabs.tsx @@ -0,0 +1,41 @@ +// Copyright 2023 @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'; + +export const Tabs = ({ config, activeIndex }: any) => { + const { resetFilters, setMultiFilters } = useFilters(); + + const [active, setActive] = useState(activeIndex); + + return ( + + {config.map((c: any, i: number) => ( + { + 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'); + } + + setActive(i); + }} + > + {c.label} + + ))} + + ); +}; diff --git a/src/library/Filter/Wrappers.ts b/src/library/Filter/Wrappers.ts new file mode 100644 index 0000000000..7c9e74b2c3 --- /dev/null +++ b/src/library/Filter/Wrappers.ts @@ -0,0 +1,138 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + padding: 0 0.5rem; + position: relative; + overflow: hidden; + display: flex; + flex-wrap: nowrap; + height: 3rem; + margin-bottom: 0.5rem; + + > .hide-scrollbar { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + + > div { + display: flex; + flex-wrap: nowrap; + overflow: auto; + width: 100%; + padding-bottom: 3rem; + + > .items { + display: flex; + > button { + padding: 0 0.25rem; + } + } + } + } +`; + +export const ItemWrapper = styled.div` + border: 1px solid var(--border-primary-color); + font-family: InterSemiBold, sans-serif; + border-radius: 1.5rem; + display: flex; + position: relative; + padding: 0.6rem 1rem; + margin-right: 1rem; + align-items: center; + width: max-content; + + &:last-child { + margin-right: 0; + } + .icon { + color: var(--text-color-secondary); + display: flex; + flex-flow: column wrap; + justify-content: center; + align-items: center; + margin-right: 0.55rem; + } + p { + color: var(--text-color-secondary); + font-size: 0.9rem; + margin: 0; + text-align: left; + line-height: 0.95rem; + } +`; + +export const LargeItemWrapper = styled.div` + display: flex; + flex-flow: column nowrap; + justify-content: center; + padding: 0.5rem; + > .inner { + border: 1.5px solid var(--border-primary-color); + background: var(--background-list-item); + border-radius: 1.25rem; + display: flex; + flex-flow: column nowrap; + justify-content: center; + position: relative; + padding: 1rem 1.25rem; + + &:last-child { + margin-right: 0; + } + + > section { + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: center; + } + svg { + color: var(--accent-color-primary); + margin-right: 0.75rem; + } + p { + color: var(--text-color-secondary); + margin: 0; + text-align: left; + padding: 0.5rem 0 0 0; + } + } +`; + +export const TabsWrapper = styled.div` + display: flex; + margin-bottom: 0.75rem; + + > button { + &:first-child { + border-top-left-radius: 1.5rem; + border-bottom-left-radius: 1.5rem; + } + &:last-child { + border-top-right-radius: 1.5rem; + border-bottom-right-radius: 1.5rem; + } + } +`; + +export const TabWrapper = styled.button<{ $active?: boolean }>` + font-family: InterSemiBold, sans-serif; + border: 1px solid + ${(props) => + props.$active + ? 'var(--accent-color-primary)' + : 'var(--border-primary-color)'}; + color: ${(props) => + props.$active + ? 'var(--accent-color-primary)' + : 'var(--text-color-secondary)'}; + font-size: 0.9rem; + padding: 0.5rem 1.25rem; +`; diff --git a/src/library/Filter/defaults.ts b/src/library/Filter/defaults.ts new file mode 100644 index 0000000000..dbb446c644 --- /dev/null +++ b/src/library/Filter/defaults.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { ValidatorFilterContextInterface } from './types'; + +export const defaultContext: ValidatorFilterContextInterface = { + orderValidators: (v) => {}, + applyValidatorOrder: (l, o) => {}, + applyValidatorFilters: (l, k, f) => {}, + toggleFilterValidators: (v) => {}, + toggleAllValidatorFilters: (t) => {}, + resetValidatorFilters: () => {}, + validatorSearchFilter: (l, k, v) => {}, + validatorFilters: [], + validatorOrder: 'default', +}; diff --git a/src/library/Filter/types.ts b/src/library/Filter/types.ts new file mode 100644 index 0000000000..07c6007394 --- /dev/null +++ b/src/library/Filter/types.ts @@ -0,0 +1,31 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; +import type React from 'react'; + +export interface ItemProps { + icon?: IconProp; + label?: string; + transform?: string; + onClick?: () => void; + disabled?: boolean; +} + +export interface CategoryProps { + title: string; + buttons?: any[]; + children: React.ReactNode; +} + +export interface ValidatorFilterContextInterface { + orderValidators: (v: string) => void; + applyValidatorOrder: (l: any, o: string) => any; + applyValidatorFilters: (l: any, k: string, f?: string[]) => any; + toggleFilterValidators: (v: string) => void; + toggleAllValidatorFilters: (t: number) => void; + resetValidatorFilters: () => void; + validatorSearchFilter: (l: any, k: string, v: string) => void; + validatorFilters: string[]; + validatorOrder: string; +} diff --git a/src/library/Form/Bond/BondFeedback.tsx b/src/library/Form/Bond/BondFeedback.tsx new file mode 100644 index 0000000000..a4a2e21867 --- /dev/null +++ b/src/library/Form/Bond/BondFeedback.tsx @@ -0,0 +1,193 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit, unitToPlanck } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { useStaking } from 'contexts/Staking'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Warning } from '../Warning'; +import { Spacer } from '../Wrappers'; +import type { BondFeedbackProps } from '../types'; +import { BondInput } from './BondInput'; + +export const BondFeedback = ({ + bondFor, + inSetup = false, + joiningPool = false, + parentErrors = [], + setters = [], + listenIsValid = () => {}, + disableTxFeeUpdate = false, + defaultBond, + txFees, + maxWidth, + syncing = false, +}: BondFeedbackProps) => { + const { t } = useTranslation('library'); + const { + networkData: { units, unit }, + } = useNetwork(); + const { staking } = useStaking(); + const { activeAccount } = useActiveAccounts(); + const { stats } = usePoolsConfig(); + const { isDepositor } = useActivePools(); + const { getTransferOptions } = useTransferOptions(); + const { minJoinBond, minCreateBond } = stats; + const { minNominatorBond } = staking; + const allTransferOptions = getTransferOptions(activeAccount); + + const defaultBondStr = defaultBond ? String(defaultBond) : ''; + + // get bond options for either staking or pooling. + const freeBalanceBn = + bondFor === 'nominator' + ? allTransferOptions.nominate.totalAdditionalBond + : allTransferOptions.pool.totalAdditionalBond; + + // if we are bonding, subtract tx fees from bond amount + const freeBondAmount = !disableTxFeeUpdate + ? BigNumber.max(freeBalanceBn.minus(txFees), 0) + : freeBalanceBn; + + // the default bond balance + const freeBalance = planckToUnit(freeBondAmount, units); + + // store errors + const [errors, setErrors] = useState([]); + + // local bond state + const [bond, setBond] = useState<{ bond: string }>({ + bond: defaultBondStr, + }); + + // current bond value BigNumber + const bondBn = unitToPlanck(bond.bond, units); + + // whether bond is disabled + const [bondDisabled, setBondDisabled] = useState(false); + + // bond minus tx fees if too much + const enoughToCoverTxFees = freeBondAmount + .minus(bondBn) + .isGreaterThan(txFees); + + const bondAfterTxFees = enoughToCoverTxFees + ? bondBn + : BigNumber.max(bondBn.minus(txFees), 0); + + // update bond on account change + useEffect(() => { + setBond({ + bond: defaultBondStr, + }); + }, [activeAccount]); + + // handle errors on input change + useEffect(() => { + handleErrors(); + }, [bond, txFees]); + + // update max bond after txFee sync + useEffect(() => { + if (!disableTxFeeUpdate) { + if (bondBn.isGreaterThan(freeBondAmount)) { + setBond({ bond: String(freeBalance) }); + } + } + }, [txFees]); + + // add this component's setBond to setters + setters.push({ + set: setBond, + current: bond, + }); + + // bond amount to minimum threshold. + const minBondBn = + bondFor === 'pool' + ? inSetup || isDepositor() + ? minCreateBond + : minJoinBond + : minNominatorBond; + const minBondUnit = planckToUnit(minBondBn, units); + + // handle error updates + const handleErrors = () => { + let disabled = false; + const newErrors = parentErrors; + const decimals = bond.bond.toString().split('.')[1]?.length ?? 0; + + // bond errors + if (freeBondAmount.isZero()) { + disabled = true; + newErrors.push(`${t('noFree', { unit })}`); + } + + // bond amount must not surpass freeBalalance + if (bondBn.isGreaterThan(freeBondAmount)) { + newErrors.push(t('moreThanBalance')); + } + + // bond amount must not be smaller than 1 planck + if (bond.bond !== '' && bondBn.isLessThan(1)) { + newErrors.push(t('tooSmall')); + } + + // check bond after transaction fees is still valid + if (bond.bond !== '' && bondAfterTxFees.isLessThan(0)) { + newErrors.push(`${t('notEnoughAfter', { unit })}`); + } + + // cbond amount must not surpass network supported units + if (decimals > units) { + newErrors.push(`${t('bondDecimalsError', { units })}`); + } + + if (inSetup || joiningPool) { + if (freeBondAmount.isLessThan(minBondBn)) { + disabled = true; + newErrors.push(`${t('notMeet')} ${minBondUnit} ${unit}.`); + } + // bond amount must be more than minimum required bond + if (bond.bond !== '' && bondBn.isLessThan(minBondBn)) { + newErrors.push(`${t('atLeast')} ${minBondUnit} ${unit}.`); + } + } + + const bondValid = !newErrors.length && bond.bond !== ''; + setBondDisabled(disabled); + listenIsValid(bondValid, newErrors); + setErrors(newErrors); + }; + + return ( + <> + {errors.map((err, i) => ( + + ))} + +
+ +
+ + ); +}; diff --git a/src/library/Form/Bond/BondInput.tsx b/src/library/Form/Bond/BondInput.tsx new file mode 100644 index 0000000000..cc8f5835f0 --- /dev/null +++ b/src/library/Form/Bond/BondInput.tsx @@ -0,0 +1,102 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonSubmitInvert } from '@polkadot-cloud/react'; +import BigNumber from 'bignumber.js'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { InputWrapper } from '../Wrappers'; +import type { BondInputProps } from '../types'; + +export const BondInput = ({ + setters = [], + disabled, + defaultValue, + freeBalance, + disableTxFeeUpdate = false, + value = '0', + syncing = false, +}: BondInputProps) => { + const { t } = useTranslation('library'); + const { + networkData: { unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + + // the current local bond value + const [localBond, setLocalBond] = useState(value); + + // reset value to default when changing account. + useEffect(() => { + setLocalBond(defaultValue ?? '0'); + }, [activeAccount]); + + useEffect(() => { + if (!disableTxFeeUpdate) { + setLocalBond(value.toString()); + } + }, [value]); + + // handle change for bonding. + const handleChangeBond = (e: React.ChangeEvent) => { + const val = e.target.value; + if (new BigNumber(val).isNaN() && val !== '') { + return; + } + setLocalBond(val); + updateParentState(val); + }; + + // apply bond to parent setters. + const updateParentState = (val: string) => { + for (const s of setters) { + s.set({ + ...s.current, + bond: val, + }); + } + }; + + // available funds as jsx. + const availableFundsJsx = ( +

+ {syncing ? '...' : `${freeBalance.toFormat()} ${unit} ${t('available')}`} +

+ ); + + return ( + +
+
+
+
+ { + handleChangeBond(e); + }} + disabled={disabled} + /> +
+
{availableFundsJsx}
+
+
+
+ { + setLocalBond(freeBalance.toString()); + updateParentState(freeBalance.toString()); + }} + /> +
+
+
{availableFundsJsx}
+
+ ); +}; diff --git a/src/library/Form/ClaimPermissionInput/index.tsx b/src/library/Form/ClaimPermissionInput/index.tsx new file mode 100644 index 0000000000..04720dc468 --- /dev/null +++ b/src/library/Form/ClaimPermissionInput/index.tsx @@ -0,0 +1,101 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ActionItem } from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import type { ClaimPermission } from 'contexts/Pools/types'; +import { TabWrapper, TabsWrapper } from 'library/Filter/Wrappers'; + +export interface ClaimPermissionInputProps { + current: ClaimPermission | undefined; + permissioned: boolean; + onChange: (value: ClaimPermission | undefined) => void; + disabled?: boolean; +} + +export const ClaimPermissionInput = ({ + current, + permissioned, + onChange, + disabled = false, +}: ClaimPermissionInputProps) => { + const { t } = useTranslation('library'); + const { claimPermissionConfig } = usePoolMemberships(); + + // Updated claim permission value + const [selected, setSelected] = useState( + current + ); + + // Permissionless claim enabled. + const [enabled, setEnabled] = useState(permissioned); + + const activeTab = claimPermissionConfig.find( + ({ value }) => value === selected + ); + + // Update selected value when current changes. + useEffect(() => { + setSelected(current); + }, [current]); + + 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 }: any, i) => ( + { + setSelected(value); + onChange(value); + }} + > + {label} + + ))} + +
+ {activeTab ? ( +

{activeTab.description}

+ ) : ( +

{t('permissionlessClaimingTurnedOff')}

+ )} +
+ + ); +}; diff --git a/src/library/Form/CreatePoolStatusBar/Wrapper.ts b/src/library/Form/CreatePoolStatusBar/Wrapper.ts new file mode 100644 index 0000000000..10d736a083 --- /dev/null +++ b/src/library/Form/CreatePoolStatusBar/Wrapper.ts @@ -0,0 +1,78 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: flex-end; + margin-top: 1rem; + + .bars { + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: flex-end; + margin-top: 1rem; + + > section { + padding: 0 0.15rem; + + &:nth-child(1) { + flex-basis: 20%; + } + &:nth-child(2) { + flex-basis: 80%; + } + h4, + h5 { + color: var(--text-color-secondary); + } + + h4 { + display: flex; + flex-flow: row wrap; + align-items: center; + margin: 1.25rem 0 0.4rem 0; + } + h5 { + position: relative; + opacity: 0.75; + } + .bar { + background: var(--background-list-item); + width: 100%; + padding: 0.65rem 0.75rem; + overflow: hidden; + position: relative; + transition: background var(--transition-duration); + } + &:first-child .bar { + border-top-left-radius: 1.5rem; + border-bottom-left-radius: 1.5rem; + h5 { + margin-left: 0.25rem; + } + } + &:last-child .bar { + border-top-right-radius: 1.5rem; + border-bottom-right-radius: 1.5rem; + } + + &.invert { + h4 { + color: var(--accent-color-primary); + } + h5 { + opacity: 1; + color: var(--text-color-invert); + } + .bar { + background: var(--accent-color-primary); + } + } + } + } +`; diff --git a/src/library/Form/CreatePoolStatusBar/index.tsx b/src/library/Form/CreatePoolStatusBar/index.tsx new file mode 100644 index 0000000000..ab4ece2fe1 --- /dev/null +++ b/src/library/Form/CreatePoolStatusBar/index.tsx @@ -0,0 +1,51 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faFlag } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { useUi } from 'contexts/UI'; +import { useNetwork } from 'contexts/Network'; +import type { NominateStatusBarProps } from '../types'; +import { Wrapper } from './Wrapper'; + +export const CreatePoolStatusBar = ({ value }: NominateStatusBarProps) => { + const { t } = useTranslation('library'); + const { isSyncing } = useUi(); + const { unit, units } = useNetwork().networkData; + const { minCreateBond } = usePoolsConfig().stats; + + const minCreateBondUnit = planckToUnit(minCreateBond, units); + const sectionClassName = + value.isGreaterThanOrEqualTo(minCreateBondUnit) && !isSyncing + ? 'invert' + : ''; + + return ( + +
+
+

 

+
+
0 {unit}
+
+
+
+

+ +  {t('createPool')} +

+
+
+ {isSyncing + ? '...' + : `${minCreateBondUnit.decimalPlaces(3).toFormat()} ${unit}`} +
+
+
+
+
+ ); +}; diff --git a/src/library/Form/MinDelayInput/Wrapper.ts b/src/library/Form/MinDelayInput/Wrapper.ts new file mode 100644 index 0000000000..c5af7b3c1f --- /dev/null +++ b/src/library/Form/MinDelayInput/Wrapper.ts @@ -0,0 +1,65 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const MinDelayInputWrapper = styled.div` + color: var(--text-color-secondary); + border: 1px solid var(--border-primary-color); + flex: 0 1 auto; + display: flex; + height: 3rem; + align-items: center; + margin: 0.5rem 1rem 0 0; + border-radius: 0.75rem; + overflow: hidden; + + &:first-child { + margin-left: 0; + } + &:last-child { + margin-right: 0; + } + + > .input { + flex-grow: 1; + padding-right: 0.75rem; + + input { + font-family: InterSemiBold, sans-serif; + width: 32px; + margin: 0 0.6rem 0 0; + padding: 0.5rem 0.2rem 0.5rem 0.75rem; + } + } + + > .toggle { + background: var(--button-primary-background); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100%; + width: 1.5rem; + + > button { + color: var(--text-color-secondary); + height: 1.5rem; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + + &:first-child { + padding-top: 0.3rem; + } + &:last-child { + padding-bottom: 0.3rem; + } + + &:hover { + background: var(--button-secondary-background); + } + } + } +`; diff --git a/src/library/Form/MinDelayInput/index.tsx b/src/library/Form/MinDelayInput/index.tsx new file mode 100644 index 0000000000..f017393ec4 --- /dev/null +++ b/src/library/Form/MinDelayInput/index.tsx @@ -0,0 +1,62 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { MinDelayInputWrapper } from './Wrapper'; +import type { MinDelayProps } from './types'; + +export const MinDelayInput = ({ + initial, + field, + label, + handleChange, +}: MinDelayProps) => { + const [current, setCurrent] = useState(initial); + + useEffect(() => { + setCurrent(initial); + }, [initial]); + + const onChange = (value: string) => { + if (!new BigNumber(value).isNaN() || value === '') { + const newValue = new BigNumber(value || 0).toNumber(); + setCurrent(newValue); + handleChange(field, newValue); + } + }; + + const onIncrement = () => { + const newValue = current + 1; + onChange(String(newValue)); + }; + + const onDecrement = () => { + const newValue = Math.max(current - 1, 0); + onChange(String(newValue)); + }; + + return ( + +
+ onChange(value)} + /> + {label} +
+
+ + +
+
+ ); +}; diff --git a/src/library/Form/MinDelayInput/types.ts b/src/library/Form/MinDelayInput/types.ts new file mode 100644 index 0000000000..f34b6d0395 --- /dev/null +++ b/src/library/Form/MinDelayInput/types.ts @@ -0,0 +1,9 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface MinDelayProps { + initial: number; + field: string; + label: string; + handleChange: (field: string, value: number) => void; +} diff --git a/src/library/Form/NominateStatusBar/Wrapper.ts b/src/library/Form/NominateStatusBar/Wrapper.ts new file mode 100644 index 0000000000..dd1b5d9089 --- /dev/null +++ b/src/library/Form/NominateStatusBar/Wrapper.ts @@ -0,0 +1,81 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: flex-end; + margin-top: 1rem; + + .bars { + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: flex-end; + margin-top: 1rem; + + > section { + padding: 0 0.15rem; + + &:nth-child(1) { + flex-basis: 15%; + } + &:nth-child(2) { + flex-basis: 40%; + } + &:nth-child(3) { + flex-basis: 45%; + } + h4, + h5 { + color: var(--text-color-secondary); + } + + h4 { + display: flex; + flex-flow: row wrap; + align-items: center; + margin: 1.25rem 0 0.4rem 0; + } + h5 { + position: relative; + opacity: 0.75; + } + .bar { + background: var(--background-list-item); + width: 100%; + padding: 0.65rem 0.75rem; + overflow: hidden; + position: relative; + transition: background var(--transition-duration); + } + &:first-child .bar { + border-top-left-radius: 1.5rem; + border-bottom-left-radius: 1.5rem; + h5 { + margin-left: 0.25rem; + } + } + &:last-child .bar { + border-top-right-radius: 1.5rem; + border-bottom-right-radius: 1.5rem; + } + + &.invert { + h4 { + color: var(--accent-color-primary); + } + h5 { + opacity: 1; + color: var(--text-color-invert); + } + .bar { + background: var(--accent-color-primary); + } + } + } + } +`; diff --git a/src/library/Form/NominateStatusBar/index.tsx b/src/library/Form/NominateStatusBar/index.tsx new file mode 100644 index 0000000000..6fa05806d0 --- /dev/null +++ b/src/library/Form/NominateStatusBar/index.tsx @@ -0,0 +1,78 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faFlag } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ButtonHelp } from '@polkadot-cloud/react'; +import { planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { useNetwork } from 'contexts/Network'; +import type { NominateStatusBarProps } from '../types'; +import { Wrapper } from './Wrapper'; + +export const NominateStatusBar = ({ value }: NominateStatusBarProps) => { + const { t } = useTranslation('library'); + const { staking } = useStaking(); + const { isSyncing } = useUi(); + const { unit, units } = useNetwork().networkData; + const { minNominatorBond } = staking; + const { metrics } = useNetworkMetrics(); + const { minimumActiveStake } = metrics; + const { openHelp } = useHelp(); + + const minNominatorBondUnit = planckToUnit(minNominatorBond, units); + const minimumActiveStakeUnit = planckToUnit(minimumActiveStake, units); + const gtMinNominatorBond = value.isGreaterThanOrEqualTo(minNominatorBondUnit); + const gtMinActiveStake = value.isGreaterThanOrEqualTo(minimumActiveStakeUnit); + + return ( + +
+
+

 

+
+
{t('nominateInactive')}
+
+
+
+

+ +   {t('nominate')} + openHelp('Nominating')} /> +

+
+
+ {minNominatorBondUnit.decimalPlaces(3).toFormat()} {unit} +
+
+
+
+

+ +  {t('nominateActive')} + openHelp('Active Stake Threshold')} + /> +

+
+
+ {isSyncing + ? '...' + : `${(minimumActiveStakeUnit.isLessThan(minNominatorBondUnit) + ? minNominatorBondUnit + : minimumActiveStakeUnit + ) + .decimalPlaces(3) + .toFormat()} ${unit}`} +
+
+
+
+
+ ); +}; diff --git a/src/library/Form/Unbond/UnbondFeedback.tsx b/src/library/Form/Unbond/UnbondFeedback.tsx new file mode 100644 index 0000000000..462d7ea500 --- /dev/null +++ b/src/library/Form/Unbond/UnbondFeedback.tsx @@ -0,0 +1,165 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { isNotZero, planckToUnit, unitToPlanck } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { useStaking } from 'contexts/Staking'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Warning } from '../Warning'; +import { Spacer } from '../Wrappers'; +import type { UnbondFeedbackProps } from '../types'; +import { UnbondInput } from './UnbondInput'; + +export const UnbondFeedback = ({ + bondFor, + inSetup = false, + setters = [], + listenIsValid = () => {}, + defaultBond, + setLocalResize, + parentErrors = [], + txFees, +}: UnbondFeedbackProps) => { + const { t } = useTranslation('library'); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { staking } = useStaking(); + const { getTransferOptions } = useTransferOptions(); + const { isDepositor } = useActivePools(); + const { stats } = usePoolsConfig(); + const { minJoinBond, minCreateBond } = stats; + const { minNominatorBond } = staking; + const allTransferOptions = getTransferOptions(activeAccount); + + const defaultValue = defaultBond ? String(defaultBond) : ''; + + // get bond options for either nominating or pooling. + const transferOptions = + bondFor === 'pool' ? allTransferOptions.pool : allTransferOptions.nominate; + const { active } = transferOptions; + + // store errors + const [errors, setErrors] = useState([]); + + // local bond state + const [bond, setBond] = useState<{ bond: string }>({ + bond: defaultValue, + }); + + // current bond value BigNumber + const bondBn = unitToPlanck(String(bond.bond), units); + + // update bond on account change + useEffect(() => { + setBond({ + bond: defaultValue, + }); + }, [activeAccount]); + + // handle errors on input change + useEffect(() => { + handleErrors(); + }, [bond, txFees]); + + // if resize is present, handle on error change + useEffect(() => { + if (setLocalResize) setLocalResize(); + }, [errors]); + + // add this component's setBond to setters + setters.push({ + set: setBond, + current: bond, + }); + + // bond amount to minimum threshold + const minBondBn = + bondFor === 'pool' + ? inSetup || isDepositor() + ? minCreateBond + : minJoinBond + : minNominatorBond; + const minBondUnit = planckToUnit(minBondBn, units); + + // unbond amount to minimum threshold + const unbondToMin = + bondFor === 'pool' + ? inSetup || isDepositor() + ? BigNumber.max(active.minus(minCreateBond), 0) + : BigNumber.max(active.minus(minJoinBond), 0) + : BigNumber.max(active.minus(minNominatorBond), 0); + + // check if bonded is below the minimum required + const nominatorActiveBelowMin = + bondFor === 'nominator' && + isNotZero(active) && + active.isLessThan(minNominatorBond); + const poolToMinBn = isDepositor() ? minCreateBond : minJoinBond; + const poolActiveBelowMin = + bondFor === 'pool' && active.isLessThan(poolToMinBn); + + // handle error updates + const handleErrors = () => { + const newErrors = parentErrors; + const decimals = bond.bond.toString().split('.')[1]?.length ?? 0; + + if (bondBn.isGreaterThan(active)) { + newErrors.push(t('unbondAmount')); + } + + if (bond.bond !== '' && bondBn.isLessThan(1)) { + newErrors.push(t('valueTooSmall')); + } + + if (decimals > units) { + newErrors.push(`${t('bondAmountDecimals', { unit })}`); + } + + if (bondBn.isGreaterThan(unbondToMin)) { + // start the error message stating a min bond is required. + let err = `${t('minimumBond', { + minBondUnit: minBondUnit.toString(), + unit, + })} `; + // append the subject to the error message. + if (bondFor === 'nominator') { + err += t('whenActivelyNominating'); + } else if (isDepositor()) { + err += t('asThePoolDepositor'); + } else { + err += t('asAPoolMember'); + } + newErrors.push(err); + } + + listenIsValid(!newErrors.length && bond.bond !== '', newErrors); + setErrors(newErrors); + }; + + return ( + <> + {errors.map((err, i) => ( + + ))} + + + + ); +}; diff --git a/src/library/Form/Unbond/UnbondInput.tsx b/src/library/Form/Unbond/UnbondInput.tsx new file mode 100644 index 0000000000..91db1bbfb0 --- /dev/null +++ b/src/library/Form/Unbond/UnbondInput.tsx @@ -0,0 +1,100 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonSubmitInvert } from '@polkadot-cloud/react'; +import { planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { InputWrapper } from '../Wrappers'; +import type { UnbondInputProps } from '../types'; + +export const UnbondInput = ({ + defaultValue, + disabled, + unbondToMin, + setters = [], + value = 0, + active, +}: UnbondInputProps) => { + const { t } = useTranslation('library'); + const { networkData } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + + // get the actively bonded amount. + const activeUnit = planckToUnit(active, networkData.units); + + // the current local bond value. + const [localBond, setLocalBond] = useState(value); + + // reset value to default when changing account. + useEffect(() => { + setLocalBond(defaultValue ?? 0); + }, [activeAccount]); + + // handle change for unbonding. + const handleChangeUnbond = (e: React.ChangeEvent) => { + const val = e.target.value; + if (new BigNumber(val).isNaN() && val !== '') { + return; + } + setLocalBond(val); + updateParentState(val); + }; + + // apply bond to parent setters. + const updateParentState = (val: any) => { + for (const s of setters) { + s.set({ + ...s.current, + bond: val, + }); + } + }; + + // unbond to min as unit. + const unbondToMinUnit = planckToUnit(unbondToMin, networkData.units); + + // available funds as jsx. + const maxBondedJsx = ( +

+ {activeUnit.toFormat()} {networkData.unit} {t('bonded')} +

+ ); + + return ( + +
+
+
+
+ { + handleChangeUnbond(e); + }} + disabled={disabled} + /> +
+
{maxBondedJsx}
+
+
+
+ { + setLocalBond(unbondToMinUnit); + updateParentState(unbondToMinUnit); + }} + /> +
+
+
{maxBondedJsx}
+
+ ); +}; diff --git a/src/library/Form/Warning/Wrapper.ts b/src/library/Form/Warning/Wrapper.ts new file mode 100644 index 0000000000..cddd5e03b3 --- /dev/null +++ b/src/library/Form/Warning/Wrapper.ts @@ -0,0 +1,25 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + background: var(--background-warning); + border: 1px solid var(--status-warning-color-transparent); + margin: 0.5rem 0; + padding: 0.75rem 0.75rem; + border-radius: 0.75rem; + display: flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + + > h4 { + color: var(--status-warning-color); + + .icon { + color: var(--status-warning-color); + margin-right: 0.6rem; + } + } +`; diff --git a/src/library/Form/Warning/index.tsx b/src/library/Form/Warning/index.tsx new file mode 100644 index 0000000000..0af8518375 --- /dev/null +++ b/src/library/Form/Warning/index.tsx @@ -0,0 +1,16 @@ +// Copyright 2023 @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} +

+
+); diff --git a/src/library/Form/Wrappers.ts b/src/library/Form/Wrappers.ts new file mode 100644 index 0000000000..1f80e5f3a7 --- /dev/null +++ b/src/library/Form/Wrappers.ts @@ -0,0 +1,115 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { SmallFontSizeMaxWidth } from 'consts'; + +export const Spacer = styled.div` + width: 100%; + height: 1px; + margin: 0.75rem 0; +`; + +export const RowWrapper = styled.div` + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: flex-end; + + > div:last-child { + height: 100%; + flex-grow: 1; + display: flex; + flex-flow: row wrap; + align-items: flex-end; + padding: 0.5rem 1rem; + } +`; + +export const InputWrapper = styled.div` + width: 100%; + display: flex; + flex-flow: column wrap; + + h3 { + color: var(--text-color-secondary); + margin: 0; + padding: 0 0.25rem; + } + + > .inner { + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: center; + margin-top: 0.75rem; + padding: 0; + + > section { + &:first-child { + flex: 1; + } + &:last-child { + padding: 0 0.25rem 0 1.25rem; + } + .input { + border: 1px solid var(--border-primary-color); + padding: 1rem; + border-radius: 0.75rem; + display: flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + max-width: 100%; + + > div { + &:first-child { + flex-grow: 1; + } + &:last-child { + color: var(--text-color-secondary); + padding-left: 0.5rem; + justify-content: flex-end; + opacity: 0.5; + position: relative; + + @media (max-width: ${SmallFontSizeMaxWidth}px) { + display: none; + } + + p { + font-family: InterSemiBold, sans-serif; + position: absolute; + top: 0; + left: 0; + width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + position: relative; + margin: 0; + font-size: 1rem; + } + } + > input { + font-family: InterBold, sans-serif; + border: none; + padding: 0; + width: 100%; + max-width: 100%; + flex-grow: 1; + } + } + } + } + } + + .availableOuter { + @media (min-width: ${SmallFontSizeMaxWidth + 1}px) { + display: none; + } + color: var(--text-color-secondary); + opacity: 0.5; + padding: 0 0.5rem; + } +`; diff --git a/src/library/Form/types.ts b/src/library/Form/types.ts new file mode 100644 index 0000000000..45617ae9b4 --- /dev/null +++ b/src/library/Form/types.ts @@ -0,0 +1,97 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { Balance } from 'contexts/Balances/types'; +import type { + ExtensionAccount, + ExternalAccount, +} from '@polkadot-cloud/react/types'; +import type { BondFor, MaybeAddress } from 'types'; + +export interface ExtensionAccountItem extends ExtensionAccount { + active?: boolean; + alert?: string; + balance?: Balance; +} +export interface ExternalAccountItem extends ExternalAccount { + active?: boolean; + alert?: string; + balance?: Balance; +} +export type ImportedAccountItem = ExtensionAccountItem | ExternalAccountItem; + +export type InputItem = ImportedAccountItem | null; + +export interface DropdownInput { + key: string; + name: string; +} + +export interface AccountDropdownProps { + current: InputItem; + to: MaybeAddress; +} + +export interface BondFeedbackProps { + syncing?: boolean; + setters: any; + bondFor: BondFor; + defaultBond: number | null; + inSetup?: boolean; + joiningPool?: boolean; + listenIsValid: { (valid: boolean, errors: string[]): void } | { (): void }; + parentErrors?: string[]; + disableTxFeeUpdate?: boolean; + setLocalResize?: () => void; + txFees: BigNumber; + maxWidth?: boolean; +} + +export interface BondInputProps { + freeBalance: BigNumber; + value: string; + defaultValue: string; + syncing?: boolean; + setters: any; + disabled: boolean; + disableTxFeeUpdate?: boolean; +} + +export interface UnbondFeedbackProps { + setters: any; + bondFor: BondFor; + defaultBond?: number; + inSetup?: boolean; + listenIsValid: { (valid: boolean, errors: string[]): void } | { (): void }; + parentErrors?: string[]; + setLocalResize?: () => void; + txFees: BigNumber; +} + +export interface UnbondInputProps { + active: BigNumber; + unbondToMin: BigNumber; + defaultValue: number | string; + disabled: boolean; + setters: any; + value: any; +} + +export interface NominateStatusBarProps { + value: BigNumber; +} + +export interface DropdownProps { + items: DropdownInput[]; + onChange: (o: any) => void; + label?: string; + placeholder: string; + value: DropdownInput; + current: DropdownInput; + height: string; +} + +export interface WarningProps { + text: string; +} diff --git a/src/library/GenerateNominations/index.tsx b/src/library/GenerateNominations/index.tsx new file mode 100644 index 0000000000..c8c4163670 --- /dev/null +++ b/src/library/GenerateNominations/index.tsx @@ -0,0 +1,366 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faChartPie, + faChevronLeft, + faCoins, + faHeart, + faPlus, + faUserEdit, +} from '@fortawesome/free-solid-svg-icons'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { SelectableWrapper } from 'library/List'; +import { SelectItems } from 'library/SelectItems'; +import { SelectItem } from 'library/SelectItems/Item'; +import { ValidatorList } from 'library/ValidatorList'; +import { Wrapper } from 'pages/Overview/NetworkSats/Wrappers'; +import { useStaking } from 'contexts/Staking'; +import { useFavoriteValidators } from 'contexts/Validators/FavoriteValidators'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import type { Validator } from 'contexts/Validators/types'; +import { ButtonMonoInvert, ButtonPrimaryInvert } from '@polkadot-cloud/react'; +import { Subheading } from 'pages/Nominate/Wrappers'; +import { FavoritesPrompt } from 'canvas/ManageNominations/Prompts/FavoritesPrompt'; +import { usePrompt } from 'contexts/Prompt'; +import { useFetchMehods } from './useFetchMethods'; +import type { AddNominationsType, GenerateNominationsProps } from './types'; + +export const GenerateNominations = ({ + setters = [], + nominations: defaultNominations, + displayFor = 'default', +}: GenerateNominationsProps) => { + const { t } = useTranslation('library'); + const { isReady, consts } = useApi(); + const { isFastUnstaking } = useUnstaking(); + const { stakers } = useStaking().eraStakers; + const { activeAccount } = useActiveAccounts(); + const { favoritesList } = useFavoriteValidators(); + const { openPromptWith, closePrompt } = usePrompt(); + const { isReadOnlyAccount } = useImportedAccounts(); + const { validators, validatorsFetched } = useValidators(); + const { + fetch: fetchFromMethod, + add: addNomination, + available: availableToNominate, + } = useFetchMehods(); + const { maxNominations } = consts; + const defaultNominationsCount = defaultNominations.nominations?.length || 0; + + // store the method of fetching validators + const [method, setMethod] = useState( + defaultNominationsCount ? 'Manual' : null + ); + + // store whether validators are being fetched + const [fetching, setFetching] = useState(false); + + // store the currently selected set of nominations + const [nominations, setNominations] = useState( + defaultNominations.nominations + ); + + // store the height of the container + const [height, setHeight] = useState(null); + + // ref for the height of the container + const heightRef = useRef(null); + + // Update nominations on account switch, or if `defaultNominations` change. + useEffect(() => { + if ( + nominations !== defaultNominations.nominations && + defaultNominationsCount > 0 + ) { + setNominations([...(defaultNominations?.nominations || [])]); + if (defaultNominationsCount) setMethod('manual'); + } + }, [activeAccount, defaultNominations]); + + // refetch if fetching is triggered + useEffect(() => { + if ( + !isReady || + !validators.length || + !stakers.length || + validatorsFetched !== 'synced' + ) + return; + + if (fetching) fetchNominationsForMethod(); + }); + + // reset fixed height on window size change + useEffect(() => { + window.addEventListener('resize', resizeCallback); + return () => { + window.removeEventListener('resize', resizeCallback); + }; + }, []); + + const resizeCallback = () => { + setHeight(null); + }; + + // fetch nominations based on method + const fetchNominationsForMethod = () => { + if (method) { + const newNominations = fetchFromMethod(method); + + // update component state + setNominations([...newNominations]); + setFetching(false); + updateSetters(newNominations); + } + }; + + // add nominations based on method + const addNominationByType = (type: AddNominationsType) => { + if (method) { + const newNominations = addNomination(nominations, type); + setNominations([...newNominations]); + updateSetters([...newNominations]); + } + }; + + const updateSetters = (newNominations: Validator[]) => { + for (const { current, set } of setters) { + const currentValue = current?.callable ? current.fn() : current; + set({ + ...currentValue, + nominations: newNominations, + }); + } + }; + + // callback function for adding nominations. + const cbAddNominations = ({ setSelectActive }: any) => { + setSelectActive(false); + + const updateList = (newNominations: Validator[]) => { + setNominations([...newNominations]); + updateSetters(newNominations); + closePrompt(); + }; + + openPromptWith( + + ); + }; + + // function for clearing nomination list + const clearNominations = () => { + setMethod(null); + setNominations([]); + updateSetters([]); + }; + + // callback function for removing selected validators + const cbRemoveSelected = ({ + selected, + resetSelected, + setSelectActive, + }: any) => { + const newNominations = [...nominations].filter( + (n: any) => !selected.map((_s: any) => _s.address).includes(n.address) + ); + setNominations([...newNominations]); + updateSetters([...newNominations]); + setSelectActive(false); + resetSelected(); + }; + + const disabledMaxNominations = () => + maxNominations.isLessThanOrEqualTo(nominations.length); + const disabledAddFavorites = () => + !favoritesList?.length || + maxNominations.isLessThanOrEqualTo(nominations.length); + + // accumulate generation methods + const methods = [ + { + title: t('optimalSelection'), + subtitle: t('optimalSelectionSubtitle'), + icon: faChartPie, + onClick: () => { + setMethod('Optimal Selection'); + setNominations([]); + setFetching(true); + }, + }, + { + title: t('activeLowCommission'), + subtitle: t('activeLowCommissionSubtitle'), + icon: faCoins, + onClick: () => { + setMethod('Active Low Commission'); + setNominations([]); + setFetching(true); + }, + }, + { + title: t('fromFavorites'), + subtitle: t('fromFavoritesSubtitle'), + icon: faHeart, + onClick: () => { + setMethod('From Favorites'); + setNominations([]); + setFetching(true); + }, + }, + { + title: t('manual_selection'), + subtitle: t('manualSelectionSubtitle'), + icon: faUserEdit, + onClick: () => { + setMethod('Manual'); + setNominations([]); + }, + }, + ]; + + // accumulate actions + const actions = [ + { + title: t('addFromFavorites'), + onClick: cbAddNominations, + onSelected: false, + isDisabled: disabledAddFavorites, + }, + { + title: `${t('removeSelected')}`, + onClick: cbRemoveSelected, + onSelected: true, + isDisabled: () => false, + }, + { + title: t('highPerformanceValidator'), + onClick: () => addNominationByType('High Performance Validator'), + onSelected: false, + icon: faPlus, + isDisabled: () => + disabledMaxNominations() || + !availableToNominate(nominations).highPerformance.length, + }, + { + title: t('activeValidator'), + onClick: () => addNominationByType('Active Validator'), + onSelected: false, + icon: faPlus, + isDisabled: () => + disabledMaxNominations() || + !availableToNominate(nominations).activeValidators.length, + }, + { + title: t('randomValidator'), + onClick: () => addNominationByType('Random Validator'), + onSelected: false, + icon: faPlus, + isDisabled: () => + disabledMaxNominations() || + !availableToNominate(nominations).randomValidators.length, + }, + ]; + + // Determine button style depending on in canvas. + const ButtonType = + displayFor === 'canvas' ? ButtonPrimaryInvert : ButtonMonoInvert; + + return ( + <> + {method && ( + + clearNominations()} + marginRight + /> + + {['Active Low Commission', 'Optimal Selection'].includes( + method || '' + ) && ( + { + // set a temporary height to prevent height snapping on re-renders. + setHeight(heightRef?.current?.clientHeight || null); + setTimeout(() => setHeight(null), 200); + setFetching(true); + }} + /> + )} + + )} + +
+ {!isReadOnlyAccount(activeAccount) && !method && ( + <> + +

+ {t('chooseValidators2', { + maxNominations: maxNominations.toString(), + })} +

+
+ + {methods.map((m: any, n: number) => ( + + ))} + + + )} +
+ + {fetching ? ( + <> + ) : ( + <> + {isReady && method !== null && ( +
+ +
+ )} + + )} +
+ + ); +}; diff --git a/src/library/GenerateNominations/types.ts b/src/library/GenerateNominations/types.ts new file mode 100644 index 0000000000..65262d10ed --- /dev/null +++ b/src/library/GenerateNominations/types.ts @@ -0,0 +1,24 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { Validator } from 'contexts/Validators/types'; +import type { AnyFunction, DisplayFor } from 'types'; + +export interface GenerateNominationsProps { + setters: AnyFunction[]; + nominations: NominationSelection; + displayFor?: DisplayFor; +} + +export type NominationSelectionWithResetCounter = NominationSelection & { + reset: number; +}; + +export interface NominationSelection { + nominations: Validator[]; +} + +export type AddNominationsType = + | 'High Performance Validator' + | 'Active Validator' + | 'Random Validator'; diff --git a/src/library/GenerateNominations/useFetchMethods.tsx b/src/library/GenerateNominations/useFetchMethods.tsx new file mode 100644 index 0000000000..d6580a82df --- /dev/null +++ b/src/library/GenerateNominations/useFetchMethods.tsx @@ -0,0 +1,215 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { shuffle } from '@polkadot-cloud/utils'; +import { useFavoriteValidators } from 'contexts/Validators/FavoriteValidators'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import type { Validator } from 'contexts/Validators/types'; +import { useValidatorFilters } from 'library/Hooks/useValidatorFilters'; +import type { AddNominationsType } from './types'; + +export const useFetchMehods = () => { + const { favoritesList } = useFavoriteValidators(); + const { applyFilter, applyOrder } = useValidatorFilters(); + const { validators, validatorEraPointsHistory } = useValidators(); + + const fetch = (method: string) => { + let nominations; + switch (method) { + case 'Optimal Selection': + nominations = fetchOptimal(); + break; + case 'Active Low Commission': + nominations = fetchLowCommission(); + break; + case 'From Favorites': + nominations = fetchFavorites(); + break; + default: + return []; + } + return nominations; + }; + + const add = (nominations: Validator[], type: AddNominationsType) => { + switch (type) { + case 'High Performance Validator': + nominations = addHighPerformanceValidator(nominations); + break; + case 'Active Validator': + nominations = addActiveValidator(nominations); + break; + case 'Random Validator': + nominations = addRandomValidator(nominations); + break; + default: + return nominations; + } + return nominations; + }; + + const fetchFavorites = () => { + let favs: Validator[] = []; + + if (!favoritesList) { + return favs; + } + + if (favoritesList?.length) { + // take subset of up to 16 favorites + favs = favoritesList.slice(0, 16); + } + return favs; + }; + + const fetchLowCommission = () => { + let filtered = Object.assign(validators); + + // filter validators to find active candidates + filtered = applyFilter( + ['active'], + ['all_commission', 'blocked_nominations', 'missing_identity'], + filtered + ); + + // order validators to find profitable candidates + filtered = applyOrder('low_commission', filtered); + + // take the lowest commission half of the set + filtered = filtered.slice(0, filtered.length * 0.5); + + // keep validators that are in upper 75% performance quartile. + filtered = filtered.filter((a: Validator) => { + const quartile = validatorEraPointsHistory[a.address]?.quartile || 100; + return quartile <= 75; + }); + + // choose shuffled subset of validators + if (filtered.length) { + filtered = shuffle(filtered).slice(0, 16); + } + return filtered; + }; + + const fetchOptimal = () => { + let active = Object.assign(validators); + let waiting = Object.assign(validators); + + // filter validators to find waiting candidates + waiting = applyFilter( + null, + [ + 'all_commission', + 'blocked_nominations', + 'missing_identity', + 'in_session', + ], + waiting + ); + + // filter validators to find active candidates + active = applyFilter( + ['active'], + ['all_commission', 'blocked_nominations', 'missing_identity'], + active + ); + + // keep validators that are in upper 50% performance quartile. + active = active.filter((a: Validator) => { + const quartile = validatorEraPointsHistory[a.address]?.quartile || 100; + return quartile <= 50; + }); + + // choose shuffled subset of waiting + if (waiting.length) { + waiting = shuffle(waiting).slice(0, 2); + } + // choose shuffled subset of active + if (active.length) { + active = shuffle(active).slice(0, 14); + } + + return shuffle(waiting.concat(active)); + }; + + const available = (nominations: Validator[]) => { + const all = Object.assign(validators); + + const parachainActive = + applyFilter( + ['active'], + [ + 'all_commission', + 'blocked_nominations', + 'missing_identity', + 'not_parachain_validator', + ], + all + ).filter( + (n: Validator) => !nominations.find((o) => o.address === n.address) + ) || []; + + const active = + applyFilter( + ['active'], + ['all_commission', 'blocked_nominations', 'missing_identity'], + all + ).filter( + (n: Validator) => !nominations.find((o) => o.address === n.address) + ) || []; + + const highPerformance = active.filter((a: Validator) => { + const quartile = validatorEraPointsHistory[a.address]?.quartile || 100; + return quartile <= 50; + }); + + const random = + applyFilter( + null, + ['all_commission', 'blocked_nominations', 'missing_identity'], + all + ).filter( + (n: Validator) => !nominations.find((o) => o.address === n.address) + ) || []; + + return { + parachainValidators: parachainActive, + highPerformance, + activeValidators: active, + randomValidators: random, + }; + }; + + const addActiveValidator = (nominations: Validator[]) => { + const all: Validator[] = available(nominations).activeValidators; + + // take one validator + const validator = shuffle(all).slice(0, 1)[0] || null; + if (validator) nominations.push(validator); + return nominations; + }; + + const addHighPerformanceValidator = (nominations: Validator[]) => { + const all: Validator[] = available(nominations).highPerformance; + + // take one validator + const validator = shuffle(all).slice(0, 1)[0] || null; + if (validator) nominations.push(validator); + return nominations; + }; + + const addRandomValidator = (nominations: Validator[]) => { + const all: Validator[] = available(nominations).randomValidators; + + // take one validator + const validator = shuffle(all).slice(0, 1)[0] || null; + if (validator) nominations.push(validator); + return nominations; + }; + + return { + fetch, + add, + available, + }; +}; diff --git a/src/library/Graphs/EraPoints.tsx b/src/library/Graphs/EraPoints.tsx new file mode 100644 index 0000000000..459b644439 --- /dev/null +++ b/src/library/Graphs/EraPoints.tsx @@ -0,0 +1,125 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; +import { useTranslation } from 'react-i18next'; +import { useTheme } from 'contexts/Themes'; +import { graphColors } from 'styles/graphs'; +import { useNetwork } from 'contexts/Network'; +import type { EraPointsProps } from './types'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +export const EraPoints = ({ items = [], height }: EraPointsProps) => { + const { t } = useTranslation('library'); + const { mode } = useTheme(); + const { colors } = useNetwork().networkData; + + const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + border: { + display: false, + }, + grid: { + color: 'rgba(0,0,0,0)', + }, + ticks: { + display: true, + maxTicksLimit: 30, + autoSkip: true, + }, + title: { + display: true, + text: 'Era', + font: { + size: 10, + }, + }, + }, + y: { + border: { + display: false, + }, + grid: { + color: graphColors.grid[mode], + }, + ticks: { + display: true, + beginAtZero: false, + }, + }, + }, + plugins: { + legend: { + display: false, + }, + title: { + display: false, + text: t('eraPoints'), + }, + tooltip: { + displayColors: false, + backgroundColor: graphColors.tooltip[mode], + titleColor: graphColors.label[mode], + bodyColor: graphColors.label[mode], + bodyFont: { + weight: '600', + }, + callbacks: { + title: () => [], + label: (context: any) => `${context.parsed.y}`, + }, + intersect: false, + interaction: { + mode: 'nearest', + }, + }, + }, + }; + + const data = { + labels: items.map((item: any) => item.era), + datasets: [ + { + label: t('points'), + data: items.map((item: any) => item.reward_point), + borderColor: colors.primary[mode], + backgroundColor: colors.primary[mode], + pointStyle: undefined, + pointRadius: 0, + borderWidth: 2, + }, + ], + }; + + return ( +
+ +
+ ); +}; diff --git a/src/library/Graphs/GeoDonut.tsx b/src/library/Graphs/GeoDonut.tsx new file mode 100644 index 0000000000..4c94142949 --- /dev/null +++ b/src/library/Graphs/GeoDonut.tsx @@ -0,0 +1,93 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ArcElement, Chart as ChartJS, Legend, Tooltip } from 'chart.js'; +import { Doughnut } from 'react-chartjs-2'; +import { useTheme } from 'contexts/Themes'; +import { graphColors } from 'styles/graphs'; +import chroma from 'chroma-js'; +import { ellipsisFn } from '@polkadot-cloud/utils'; +import { useNetwork } from 'contexts/Network'; +import type { GeoDonutProps } from './types'; + +ChartJS.register(ArcElement, Tooltip, Legend); + +export const GeoDonut = ({ + title, + series = { labels: [], data: [] }, + height = 'auto', + width = 'auto', +}: GeoDonutProps) => { + const { mode } = useTheme(); + const { colors } = useNetwork().networkData; + + const { labels } = series; + let { data } = series; + const isZero = data.length === 0; + const backgroundColor = isZero + ? graphColors.inactive[mode] + : colors.primary[mode]; + + const total = data.reduce((acc: number, value: number) => acc + value, 0); + + data = data.map((value: number) => (value / total) * 100); + + const options = { + borderColor: graphColors.inactive[mode], + hoverBorderColor: graphColors.inactive[mode], + backgroundColor, + hoverBackgroundColor: [backgroundColor, graphColors.inactive[mode]], + responsive: true, + maintainAspectRatio: false, + spacing: 0, + cutout: '70%', + plugins: { + legend: { + display: true, + position: 'bottom' as const, + maxHeight: 25, + labels: { + boxWidth: 10, + generateLabels: (chart: any) => { + const ls = + ChartJS.overrides.doughnut.plugins.legend.labels.generateLabels( + chart + ); + return ls.map((l) => { + l.text = ellipsisFn(l.text, undefined, 'end'); + return l; + }); + }, + }, + }, + tooltip: { + enabled: true, + callbacks: { + label: (context: any) => ` ${title}: ${context.raw.toFixed(1)} %`, + }, + }, + }, + }; + + const chartData = { + labels, + datasets: [ + { + 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 + backgroundColor: chroma + .scale([backgroundColor, graphColors.inactive[mode]]) + .colors(data.length + 1), + borderWidth: 0.5, + }, + ], + }; + + return ( +
+ +
+ ); +}; diff --git a/src/library/Graphs/PayoutBar.tsx b/src/library/Graphs/PayoutBar.tsx new file mode 100644 index 0000000000..2ae33052c2 --- /dev/null +++ b/src/library/Graphs/PayoutBar.tsx @@ -0,0 +1,196 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { + BarElement, + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, +} from 'chart.js'; +import { format, fromUnixTime } from 'date-fns'; +import { Bar } from 'react-chartjs-2'; +import { useTranslation } from 'react-i18next'; +import { DefaultLocale } from 'consts'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { useStaking } from 'contexts/Staking'; +import { useSubscan } from 'contexts/Plugins/Subscan'; +import { useTheme } from 'contexts/Themes'; +import { useUi } from 'contexts/UI'; +import { locales } from 'locale'; +import { graphColors } from 'styles/graphs'; +import type { AnySubscan } from 'types'; +import { useNetwork } from 'contexts/Network'; +import type { PayoutBarProps } from './types'; +import { formatRewardsForGraphs } from './Utils'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Title, + Tooltip, + Legend +); + +export const PayoutBar = ({ days, height }: PayoutBarProps) => { + const { i18n, t } = useTranslation('library'); + const { mode } = useTheme(); + const { isSyncing } = useUi(); + const { inSetup } = useStaking(); + const { membership } = usePoolMemberships(); + const { unit, units, colors } = useNetwork().networkData; + const { payouts, poolClaims, unclaimedPayouts } = useSubscan(); + const notStaking = !isSyncing && inSetup() && !membership; + + // remove slashes from payouts (graph does not support negative values). + const payoutsNoSlash = payouts.filter( + (p: AnySubscan) => p.event_id !== 'Slashed' + ); + + // remove slashes from unclaimed payouts. + const unclaimedPayoutsNoSlash = unclaimedPayouts.filter( + (p: AnySubscan) => p.event_id !== 'Slashed' + ); + + // get formatted rewards data for graph. + const { allPayouts, allPoolClaims, allUnclaimedPayouts } = + formatRewardsForGraphs( + new Date(), + days, + units, + payoutsNoSlash, + poolClaims, + unclaimedPayoutsNoSlash + ); + + const { p: graphPayouts } = allPayouts; + const { p: graphUnclaimedPayouts } = allUnclaimedPayouts; + const { p: graphPoolClaims } = allPoolClaims; + + // determine color for payouts + const colorPayouts = notStaking + ? colors.transparent[mode] + : colors.primary[mode]; + + // determine color for poolClaims + const colorPoolClaims = notStaking + ? colors.transparent[mode] + : colors.secondary[mode]; + + const data = { + labels: graphPayouts.map((item: AnySubscan) => { + const dateObj = format(fromUnixTime(item.block_timestamp), 'do MMM', { + locale: locales[i18n.resolvedLanguage ?? DefaultLocale], + }); + return `${dateObj}`; + }), + datasets: [ + { + order: 1, + label: t('payout'), + data: graphPayouts.map((item: AnySubscan) => item.amount), + borderColor: colorPayouts, + backgroundColor: colorPayouts, + pointRadius: 0, + borderRadius: 3, + }, + { + order: 2, + label: t('poolClaim'), + data: graphPoolClaims.map((item: AnySubscan) => item.amount), + borderColor: colorPoolClaims, + backgroundColor: colorPoolClaims, + pointRadius: 0, + borderRadius: 3, + }, + { + order: 3, + data: graphUnclaimedPayouts.map((item: AnySubscan) => item.amount), + label: t('unclaimedPayouts'), + borderColor: colorPayouts, + backgroundColor: colors.pending[mode], + pointRadius: 0, + borderRadius: 3, + }, + ], + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + barPercentage: 0.4, + 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: any) => + `${ + context.dataset.order === 3 ? `${t('pending')}: ` : '' + }${new BigNumber(context.parsed.y) + .decimalPlaces(units) + .toFormat()} ${unit}`, + }, + }, + }, + }; + + return ( +
+ +
+ ); +}; diff --git a/src/library/Graphs/PayoutLine.tsx b/src/library/Graphs/PayoutLine.tsx new file mode 100644 index 0000000000..6b72b35a89 --- /dev/null +++ b/src/library/Graphs/PayoutLine.tsx @@ -0,0 +1,184 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, +} from 'chart.js'; +import { Line } from 'react-chartjs-2'; +import { useTranslation } from 'react-i18next'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { useStaking } from 'contexts/Staking'; +import { useSubscan } from 'contexts/Plugins/Subscan'; +import { useTheme } from 'contexts/Themes'; +import { useUi } from 'contexts/UI'; +import { graphColors } from 'styles/graphs'; +import type { AnySubscan } from 'types'; +import { useNetwork } from 'contexts/Network'; +import type { PayoutLineProps } from './types'; +import { + calculatePayoutAverages, + combineRewards, + formatRewardsForGraphs, +} from './Utils'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend +); + +export const PayoutLine = ({ + days, + average, + height, + background, +}: PayoutLineProps) => { + const { t } = useTranslation('library'); + const { mode } = useTheme(); + const { isSyncing } = useUi(); + const { inSetup } = useStaking(); + const { payouts, poolClaims } = useSubscan(); + const { unit, units, colors } = useNetwork().networkData; + const { membership: poolMembership } = usePoolMemberships(); + + const notStaking = !isSyncing && inSetup() && !poolMembership; + const poolingOnly = !isSyncing && inSetup() && poolMembership !== null; + + // remove slashes from payouts (graph does not support negative values). + const payoutsNoSlash = payouts.filter( + (p: AnySubscan) => p.event_id !== 'Slashed' + ); + + // define the most recent date that we will show on the graph. + const fromDate = new Date(); + + const { allPayouts, allPoolClaims } = formatRewardsForGraphs( + fromDate, + days, + units, + payoutsNoSlash, + poolClaims, + [] // Note: we are not using `unclaimedPayouts` here. + ); + + const { p: graphPayouts, a: graphPrePayouts } = allPayouts; + const { p: graphPoolClaims, a: graphPrePoolClaims } = allPoolClaims; + + // combine payouts and pool claims into one dataset and calculate averages. + const combined = combineRewards(graphPayouts, graphPoolClaims); + const preCombined = combineRewards(graphPrePayouts, graphPrePoolClaims); + + const combinedPayouts = calculatePayoutAverages( + preCombined.concat(combined), + fromDate, + days, + 10 + ); + + // determine color for payouts + const color = notStaking + ? colors.primary[mode] + : !poolingOnly + ? colors.primary[mode] + : colors.secondary[mode]; + + // configure graph options + const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + grid: { + display: false, + }, + ticks: { + display: false, + maxTicksLimit: 30, + autoSkip: true, + }, + }, + y: { + ticks: { + display: false, + beginAtZero: false, + }, + border: { + display: false, + }, + grid: { + color: graphColors.grid[mode], + }, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + displayColors: false, + backgroundColor: graphColors.tooltip[mode], + titleColor: graphColors.label[mode], + bodyColor: graphColors.label[mode], + bodyFont: { + weight: '600', + }, + callbacks: { + title: () => [], + label: (context: any) => + ` ${new BigNumber(context.parsed.y) + .decimalPlaces(units) + .toFormat()} ${unit}`, + }, + intersect: false, + interaction: { + mode: 'nearest', + }, + }, + }, + }; + + const data = { + labels: combinedPayouts.map(() => ''), + datasets: [ + { + label: t('payout'), + data: combinedPayouts.map((item: AnySubscan) => item?.amount ?? 0), + borderColor: color, + backgroundColor: color, + pointStyle: undefined, + pointRadius: 0, + borderWidth: 2.3, + }, + ], + }; + + return ( + <> +
+ {average > 1 ? `${average} ${t('dayAverage')}` : null} +
+
+ +
+ + ); +}; diff --git a/src/library/Graphs/Utils.ts b/src/library/Graphs/Utils.ts new file mode 100644 index 0000000000..2c75cb8582 --- /dev/null +++ b/src/library/Graphs/Utils.ts @@ -0,0 +1,528 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { greaterThanZero, planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { + addDays, + differenceInDays, + fromUnixTime, + getUnixTime, + isSameDay, + startOfDay, + subDays, +} from 'date-fns'; +import { MaxPayoutDays } from 'consts'; +import type { AnyApi, AnyJson, AnySubscan } from 'types'; +import type { PayoutDayCursor } from './types'; + +// Take non-zero rewards in most-recent order. +export const sortNonZeroPayouts = ( + payouts: AnySubscan, + poolClaims: AnySubscan, + capMaxDays: boolean +) => { + const list = [ + ...payouts.concat(poolClaims).filter((p: AnySubscan) => p.amount > 0), + ].sort( + (a: AnySubscan, b: AnySubscan) => b.block_timestamp - a.block_timestamp + ); + + // this function always calculates from the current date (not fromDate). + const fromTimestamp = getUnixTime(subDays(new Date(), MaxPayoutDays)); + + if (capMaxDays) { + return list.filter((l: AnySubscan) => l.block_timestamp >= fromTimestamp); + } + + return list; +}; + +// Given payouts, calculate daily income and fill missing days with zero amounts. +export const calculateDailyPayouts = ( + payouts: AnySubscan, + fromDate: Date, + maxDays: number, + units: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + subject: string +) => { + let dailyPayouts: AnySubscan = []; + + // remove days that are beyond end day limit + payouts = payouts.filter( + (p: AnySubscan) => + daysPassed(fromUnixTime(p.block_timestamp), fromDate) <= maxDays + ); + + // return now if no payouts. + if (!payouts.length) return payouts; + + // post-fill any missing days. [current day -> last payout] + dailyPayouts = postFillMissingDays(payouts, fromDate, maxDays); + + // start iterating payouts, most recent first. + // + // payouts passed. + let p = 0; + // current day cursor. + let curDay: Date = fromDate; + // current payout cursor. + let curPayout: PayoutDayCursor = { + amount: new BigNumber(0), + event_id: '', + }; + for (const payout of payouts) { + p++; + + // extract day from current payout. + const thisDay = startOfDay(fromUnixTime(payout.block_timestamp)); + + // initialise current day if first payout. + if (p === 1) { + curDay = thisDay; + } + + // handle surpassed maximum days. + if (daysPassed(thisDay, fromDate) >= maxDays) { + dailyPayouts.push({ + amount: planckToUnit(curPayout.amount, units), + event_id: getEventId(curPayout), + block_timestamp: getUnixTime(curDay), + }); + break; + } + + // get day difference between cursor and currentpayout. + const daysDiff = daysPassed(thisDay, curDay); + + // handle new day. + if (daysDiff > 0) { + // add current payout cursor to dailyPayouts. + dailyPayouts.push({ + amount: planckToUnit(curPayout.amount, units), + event_id: getEventId(curPayout), + block_timestamp: getUnixTime(curDay), + }); + + // update day cursor to the new day. + curDay = thisDay; + // reset current payout cursor for the new day. + curPayout = { + amount: new BigNumber(payout.amount), + event_id: new BigNumber(payout.amount).isLessThan(0) + ? 'Slash' + : 'Reward', + }; + } else { + // in same day. Aadd payout amount to current payout cursor. + curPayout.amount = curPayout.amount.plus(payout.amount); + } + + // if only 1 payout exists, exit early here. + if (payouts.length === 1) { + dailyPayouts.push({ + amount: planckToUnit(curPayout.amount, units), + event_id: getEventId(curPayout), + block_timestamp: getUnixTime(curDay), + }); + break; + } + } + + // return payout amounts as plain numbers. + return dailyPayouts.map((q: AnyApi) => ({ + ...q, + amount: Number(q.amount.toString()), + })); +}; + +// Calculate average payouts per day. +export const calculatePayoutAverages = ( + payouts: AnySubscan, + fromDate: Date, + days: number, + avgDays: number +) => { + // if we don't need to take an average, just return `payouts`. + if (avgDays <= 1) return payouts; + + // create moving average value over `avgDays` past days, if any. + let payoutsAverages = []; + for (let i = 0; i < payouts.length; i++) { + // average period end. + const end = Math.max(0, i - avgDays); + + // the total amount earned in period. + let total = 0; + // period length to be determined. + let num = 0; + + for (let j = i; j >= end; j--) { + if (payouts[j]) { + total += payouts[j].amount; + } + // increase by one to treat non-existent as zero value + num += 1; + } + + if (total === 0) { + total = payouts[i].amount; + } + + payoutsAverages.push({ + amount: total / num, + block_timestamp: payouts[i].block_timestamp, + }); + } + + // return an array with the expected number of items + payoutsAverages = payoutsAverages.filter( + (p: AnySubscan) => + daysPassed(fromUnixTime(p.block_timestamp), fromDate) <= days + ); + + return payoutsAverages; +}; + +// Fetch rewards and graph meta data. +// +// Format provided payouts and returns the last payment. +export const formatRewardsForGraphs = ( + fromDate: Date, + days: number, + units: number, + payouts: AnySubscan, + poolClaims: AnySubscan, + unclaimedPayouts: AnySubscan +) => { + // process nominator payouts. + const allPayouts = processPayouts(payouts, fromDate, days, units, 'nominate'); + + // process unclaimed nominator payouts. + const allUnclaimedPayouts = processPayouts( + unclaimedPayouts, + fromDate, + days, + units, + 'nominate' + ); + + // process pool claims. + const allPoolClaims = processPayouts( + poolClaims, + fromDate, + days, + units, + 'pools' + ); + + return { + // reverse rewards: most recent last + allPayouts, + allUnclaimedPayouts, + allPoolClaims, + lastReward: getLatestReward(payouts, poolClaims), + }; +}; + +// Process payouts. +// +// calls the relevant functions on raw payouts to format them correctly. +const processPayouts = ( + payouts: AnySubscan, + fromDate: Date, + days: number, + units: number, + subject: string +) => { + // normalise payout timestamps. + const normalised = normalisePayouts(payouts); + // calculate payouts per day from the current day. + let p = calculateDailyPayouts(normalised, fromDate, days, units, subject); + // pre-fill payouts if max days have not been reached. + p = p.concat(prefillMissingDays(p, fromDate, days)); + // fill in gap days between payouts with zero values. + p = fillGapDays(p, fromDate); + // reverse payouts: most recent last. + p = p.reverse(); + + // use normalised payouts for calculating the 10-day average prior to the start of the payout graph. + const avgDays = 10; + const preNormalised = getPreMaxDaysPayouts( + normalised, + fromDate, + days, + avgDays + ); + // start of average calculation should be the earliest date. + const averageFromDate = subDays(fromDate, MaxPayoutDays); + + let a = calculateDailyPayouts( + preNormalised, + averageFromDate, + avgDays, + units, + subject + ); + // prefill payouts if we are missing the earlier dates. + a = a.concat(prefillMissingDays(a, averageFromDate, avgDays)); + // fill in gap days between payouts with zero values. + a = fillGapDays(a, averageFromDate); + // reverse payouts: most recent last. + a = a.reverse(); + + return { p, a }; +}; + +// Get payout average in `avgDays` day period after to `days` threshold +// +// These payouts are used for calculating the `avgDays`-day average prior to the start of the payout +// graph. +const getPreMaxDaysPayouts = ( + payouts: AnySubscan, + fromDate: Date, + days: number, + avgDays: number +) => { + // remove payouts that are not within `avgDays` `days` pre-graph window. + return payouts.filter( + (p: AnySubscan) => + daysPassed(fromUnixTime(p.block_timestamp), fromDate) > days && + daysPassed(fromUnixTime(p.block_timestamp), fromDate) <= days + avgDays + ); +}; + +// Combine payouts and pool claims. +// +// combines payouts and pool claims into daily records. Removes the `event_id` field from records. +export const combineRewards = (payouts: AnySubscan, poolClaims: AnySubscan) => { + // we first check if actual payouts exist, e.g. there are non-zero payout + // amounts present in either payouts or pool claims. + const poolClaimExists = + poolClaims.find((p: AnySubscan) => p.amount > 0) || null; + const payoutExists = payouts.find((p: AnySubscan) => p.amount > 0) || null; + + // if no pool claims exist but payouts do, return payouts w.o. event_id + // also do this if there are no payouts period. + if ( + (!poolClaimExists && payoutExists) || + (!payoutExists && !poolClaimExists) + ) { + return payouts.map((p: AnySubscan) => ({ + amount: p.amount, + block_timestamp: p.block_timestamp, + })); + } + + // if no payouts exist but pool claims do, return pool claims w.o. event_id + if (!payoutExists && poolClaimExists) { + return poolClaims.map((p: AnySubscan) => ({ + amount: p.amount, + block_timestamp: p.block_timestamp, + })); + } + + // We now know pool claims *and* payouts exist. + // + // Now determine which dates to display. + let payoutDays: AnyJson[] = []; + // prefill `dates` with all pool claim and payout days + poolClaims.forEach((p: AnySubscan) => { + const dayStart = getUnixTime(startOfDay(fromUnixTime(p.block_timestamp))); + if (!payoutDays.includes(dayStart)) { + payoutDays.push(dayStart); + } + }); + payouts.forEach((p: AnySubscan) => { + const dayStart = getUnixTime(startOfDay(fromUnixTime(p.block_timestamp))); + if (!payoutDays.includes(dayStart)) { + payoutDays.push(dayStart); + } + }); + + // sort payoutDays by `block_timestamp`; + payoutDays = payoutDays.sort((a: AnySubscan, b: AnySubscan) => a - b); + + // Iterate payout days. + // + // Combine payouts into one unified `rewards` array. + const rewards: AnySubscan = []; + + // loop pool claims and consume / combine payouts + payoutDays.forEach((d: AnySubscan) => { + let amount = 0; + + // check payouts exist on this day + const payoutsThisDay = payouts.filter((p: AnySubscan) => + isSameDay(fromUnixTime(p.block_timestamp), fromUnixTime(d)) + ); + // check pool claims exist on this day + const poolClaimsThisDay = poolClaims.filter((p: AnySubscan) => + isSameDay(fromUnixTime(p.block_timestamp), fromUnixTime(d)) + ); + // add amounts + if (payoutsThisDay.concat(poolClaimsThisDay).length) { + for (const payout of payoutsThisDay) { + amount += payout.amount; + } + } + rewards.push({ + amount, + block_timestamp: d, + }); + }); + return rewards; +}; + +// Get latest reward. +// +// Gets the latest reward from pool claims and nominator payouts. +export const getLatestReward = ( + payouts: AnySubscan, + poolClaims: AnySubscan +) => { + // get most recent payout + const payoutExists = + payouts.find((p: AnySubscan) => greaterThanZero(new BigNumber(p.amount))) ?? + null; + const poolClaimExists = + poolClaims.find((p: AnySubscan) => + greaterThanZero(new BigNumber(p.amount)) + ) ?? null; + + // calculate which payout was most recent + let lastReward = null; + if (!payoutExists || !poolClaimExists) { + if (payoutExists) { + lastReward = payoutExists; + } + if (poolClaimExists) { + lastReward = poolClaimExists; + } + } else { + // both `payoutExists` and `poolClaimExists` are present + lastReward = + payoutExists.block_timestamp > poolClaimExists.block_timestamp + ? payoutExists + : poolClaimExists; + } + return lastReward; +}; + +// Fill in the days from the earliest payout day to `maxDays`. +// +// Takes the last (earliest) payout and fills the missing days from that payout day to `maxDays`. +export const prefillMissingDays = ( + payouts: AnySubscan, + fromDate: Date, + maxDays: number +) => { + const newPayouts = []; + const payoutStartDay = subDays(startOfDay(fromDate), maxDays); + const payoutEndDay = !payouts.length + ? startOfDay(fromDate) + : startOfDay(fromUnixTime(payouts[payouts.length - 1].block_timestamp)); + + const daysToPreFill = daysPassed(payoutStartDay, payoutEndDay); + + if (daysToPreFill > 0) { + for (let i = 1; i < daysToPreFill; i++) { + newPayouts.push({ + amount: 0, + event_id: 'Reward', + block_timestamp: getUnixTime(subDays(payoutEndDay, i)), + }); + } + } + return newPayouts; +}; + +// Fill in the days from the current day to the last payout. +// +// Takes the first payout (most recent) and fills the missing days from current day. +export const postFillMissingDays = ( + payouts: AnySubscan, + fromDate: Date, + maxDays: number +) => { + const newPayouts = []; + const payoutsEndDay = startOfDay(fromUnixTime(payouts[0].block_timestamp)); + const daysSinceLast = Math.min( + daysPassed(payoutsEndDay, startOfDay(fromDate)), + maxDays + ); + + for (let i = daysSinceLast; i > 0; i--) { + newPayouts.push({ + amount: 0, + event_id: 'Reward', + block_timestamp: getUnixTime(addDays(payoutsEndDay, i)), + }); + } + return newPayouts; +}; + +// Fill gap days within payouts with zero amounts. +export const fillGapDays = (payouts: AnySubscan, fromDate: Date) => { + const finalPayouts: AnySubscan = []; + + // current day cursor. + let curDay = fromDate; + + for (const p of payouts) { + const thisDay = fromUnixTime(p.block_timestamp); + const gapDays = Math.max(0, daysPassed(thisDay, curDay) - 1); + + if (gapDays > 0) { + // add any gap days. + if (gapDays > 0) { + for (let j = 1; j <= gapDays; j++) { + finalPayouts.push({ + amount: 0, + event_id: 'Reward', + block_timestamp: getUnixTime(subDays(curDay, j)), + }); + } + } + } + + // add the current day. + finalPayouts.push(p); + + // day cursor is now the new day. + curDay = thisDay; + } + return finalPayouts; +}; + +// Utiltiy: normalise payout timestamps to start of day. +export const normalisePayouts = (payouts: AnySubscan) => + payouts.map((p: AnySubscan) => ({ + ...p, + block_timestamp: getUnixTime(startOfDay(fromUnixTime(p.block_timestamp))), + })); + +// Utility: days passed since 2 dates. +export const daysPassed = (from: Date, to: Date) => + differenceInDays(startOfDay(to), startOfDay(from)); + +// Utility: extract whether an event id should be a slash or reward, based on the net day amount. +const getEventId = (c: PayoutDayCursor) => + c.amount.isLessThan(0) ? 'Slash' : 'Reward'; + +// Utility: Formats a width and height pair. +export const formatSize = ( + { + width, + height, + }: { + width: string | number; + height: number; + }, + minHeight: number +) => ({ + width: width || '100%', + height: height || minHeight, + minHeight, +}); diff --git a/src/library/Graphs/Wrapper.ts b/src/library/Graphs/Wrapper.ts new file mode 100644 index 0000000000..29ae05fea6 --- /dev/null +++ b/src/library/Graphs/Wrapper.ts @@ -0,0 +1,11 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const GraphWrapper = styled.div` + position: relative; + flex: 0; + padding: 1rem 2rem 1rem 0; + width: 100%; +`; diff --git a/src/library/Graphs/types.ts b/src/library/Graphs/types.ts new file mode 100644 index 0000000000..5b05943115 --- /dev/null +++ b/src/library/Graphs/types.ts @@ -0,0 +1,51 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; +import type { AnyPolkawatch, AnySubscan } from 'types'; + +export interface BondedProps { + active: BigNumber; + free: BigNumber; + unlocking: BigNumber; + unlocked: BigNumber; + inactive: boolean; +} + +export interface EraPointsProps { + items: AnySubscan; + height: number; +} + +export interface PayoutBarProps { + days: number; + height: string; +} + +export interface PayoutLineProps { + days: number; + average: number; + height: string; + background?: string; +} + +export interface CardHeaderWrapperProps { + $withAction?: boolean; + $withMargin?: boolean; +} + +export interface CardWrapperProps { + height?: string | number; +} + +export interface PayoutDayCursor { + amount: BigNumber; + event_id: string; +} + +export interface GeoDonutProps { + title: string; + series: AnyPolkawatch; + width?: string | number; + height?: string | number; +} diff --git a/src/library/Headers/Connect.tsx b/src/library/Headers/Connect.tsx new file mode 100644 index 0000000000..5a2100ef49 --- /dev/null +++ b/src/library/Headers/Connect.tsx @@ -0,0 +1,54 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faPlug, faWallet } from '@fortawesome/free-solid-svg-icons'; +import { ButtonText } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { ConnectedAccount, HeadingWrapper } from './Wrappers'; + +export const Connect = () => { + const { t } = useTranslation('library'); + const { openModal } = useOverlay().modal; + const { accounts } = useImportedAccounts(); + + return ( + + + {accounts.length ? ( + <> + { + openModal({ key: 'Accounts' }); + }} + style={{ color: 'white', fontSize: '1.05rem' }} + /> + + { + openModal({ key: 'Connect' }); + }} + style={{ color: 'white', fontSize: '1.05rem' }} + /> + + ) : ( + { + openModal({ key: accounts.length ? 'Accounts' : 'Connect' }); + }} + style={{ color: 'white', fontSize: '1.05rem' }} + /> + )} + + + ); +}; diff --git a/src/library/Headers/Connected.tsx b/src/library/Headers/Connected.tsx new file mode 100644 index 0000000000..241e77a7d0 --- /dev/null +++ b/src/library/Headers/Connected.tsx @@ -0,0 +1,79 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { Account } from '../Account/Default'; +import { Account as PoolAccount } from '../Account/Pool'; +import { HeadingWrapper } from './Wrappers'; + +export const Connected = () => { + const { t } = useTranslation('library'); + const { isNetworkSyncing } = useUi(); + const { isNominating } = useStaking(); + const { selectedActivePool } = useActivePools(); + const { accountHasSigner } = useImportedAccounts(); + const { activeAccount, activeProxy } = useActiveAccounts(); + + let poolAddress = ''; + if (selectedActivePool) { + const { addresses } = selectedActivePool; + poolAddress = addresses.stash; + } + + return ( + <> + {activeAccount && ( + <> + {/* default account display / stash label if actively nominating */} + + + + + {/* pool account display / hide if not in pool */} + {selectedActivePool !== null && !isNetworkSyncing && ( + + {}} + /> + + )} + + {/* proxy account display / hide if no proxy */} + {activeProxy && ( + + + + )} + + )} + + ); +}; diff --git a/src/library/Headers/SideMenuToggle.tsx b/src/library/Headers/SideMenuToggle.tsx new file mode 100644 index 0000000000..8a0d111466 --- /dev/null +++ b/src/library/Headers/SideMenuToggle.tsx @@ -0,0 +1,26 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBars } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useUi } from 'contexts/UI'; +import { Item } from './Wrappers'; + +export const SideMenuToggle = () => { + const { setSideMenu, sideMenuOpen } = useUi(); + + return ( +
+ { + setSideMenu(!sideMenuOpen); + }} + > + + + + +
+ ); +}; diff --git a/src/library/Headers/Spinner.tsx b/src/library/Headers/Spinner.tsx new file mode 100644 index 0000000000..6fa2969dfc --- /dev/null +++ b/src/library/Headers/Spinner.tsx @@ -0,0 +1,74 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +const StyledSpinner = styled.div` + font-size: 10px; + margin: 0; + text-indent: -9999em; + width: 2rem; + height: 2rem; + border-radius: 50%; + background: #ccc; + background: -moz-linear-gradient(left, #ccc 10%, rgba(255, 255, 255, 0) 42%); + background: -webkit-linear-gradient( + left, + #ccc 10%, + rgba(255, 255, 255, 0) 42% + ); + background: -o-linear-gradient(left, #ccc 10%, rgba(255, 255, 255, 0) 42%); + background: -ms-linear-gradient(left, #ccc 10%, rgba(255, 255, 255, 0) 42%); + background: linear-gradient(to right, #ccc 10%, rgba(255, 255, 255, 0) 42%); + position: relative; + -webkit-animation: load3 0.7s infinite linear; + animation: load3 0.7s infinite linear; + -webkit-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); + &:before { + width: 50%; + height: 50%; + background: #ccc; + border-radius: 100% 0 0 0; + position: absolute; + top: 0; + left: 0; + content: ''; + } + &:after { + background: var(--background-primary); + width: 75%; + height: 75%; + border-radius: 50%; + content: ''; + margin: auto; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + } + @-webkit-keyframes load3 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } + @keyframes load3 { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } + } +`; + +export const Spinner = () => ; diff --git a/src/library/Headers/Wrappers.ts b/src/library/Headers/Wrappers.ts new file mode 100644 index 0000000000..516e437efa --- /dev/null +++ b/src/library/Headers/Wrappers.ts @@ -0,0 +1,139 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; +import { + ShowAccountsButtonWidthThreshold, + SideMenuStickyThreshold, +} from 'consts'; + +export const Wrapper = styled.div` + position: fixed; + top: 0px; + right: 0px; + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + align-items: center; + align-content: center; + padding: 0 1.25rem; + transition: all var(--transition-duration); + margin: 0.5rem 0; + height: 4rem; + z-index: 6; + + @media (max-width: ${SideMenuStickyThreshold}px) { + width: 100%; + } + + .menu { + display: none; + @media (max-width: ${SideMenuStickyThreshold}px) { + color: var(--text-color-secondary); + display: flex; + flex-flow: row wrap; + align-items: center; + flex-grow: 1; + } + } +`; + +export const ConnectedAccount = styled(motion.div)` + background: var(--accent-color-primary); + border-radius: 1.5rem; + display: flex; + transition: transform var(--transition-duration); + padding: 0.1rem 0.75rem; + + &:hover { + transform: scale(1.015); + } + + > span { + border-right: 1px solid var(--text-color-invert); + opacity: 0.2; + margin: 0 0.4rem; + } +`; + +export const HeadingWrapper = styled.div` + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + margin-left: 0.9rem; +`; + +export const Item = styled.button` + background: var(--button-tab-background); + border: 1px solid var(--border-primary-color); + flex-grow: 1; + padding: 0.05rem 1rem; + border-radius: 1.5rem; + box-shadow: none; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + font-size: 1.05rem; + transition: transform var(--transition-duration) ease-out; + + &:hover { + transform: scale(1.03); + } + + .label { + color: var(--accent-color-primary); + border: 0.125rem solid var(--accent-color-primary); + border-radius: 0.8rem; + font-size: 0.85rem; + margin-right: 0.6rem; + padding: 0.1rem 0.5rem; + } + + > span { + color: white; + line-height: 2.2rem; + .icon { + color: var(--text-color-secondary); + cursor: pointer; + } + } + + &.connect { + background: var(--accent-color-primary); + > span { + color: white; + } + .icon { + margin-right: 0.6rem; + path { + fill: white; + } + } + } +`; + +export const ItemInactive = styled(motion.div)` + background: var(--button-secondary-background); + flex-grow: 1; + padding: 0 1rem; + border-radius: 1rem; + display: flex; + flex-flow: column nowrap; + justify-content: center; + align-items: center; + font-size: 1rem; + + > span { + color: var(--text-color-primary); + line-height: 2.2rem; + } +`; + +export const LargeScreensOnly = styled.div` + display: flex; + @media (max-width: ${ShowAccountsButtonWidthThreshold}px) { + display: none; + } +`; diff --git a/src/library/Headers/index.tsx b/src/library/Headers/index.tsx new file mode 100644 index 0000000000..c4db3ff5ec --- /dev/null +++ b/src/library/Headers/index.tsx @@ -0,0 +1,83 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { pageFromUri } from '@polkadot-cloud/utils'; +import { useLocation } from 'react-router-dom'; +import { useExtrinsics } from 'contexts/Extrinsics'; +import { usePlugins } from 'contexts/Plugins'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import { useUi } from 'contexts/UI'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { usePayouts } from 'contexts/Payouts'; +import { Connect } from './Connect'; +import { Connected } from './Connected'; +import { SideMenuToggle } from './SideMenuToggle'; +import { Spinner } from './Spinner'; +import { LargeScreensOnly, Wrapper } from './Wrappers'; + +export const Headers = () => { + const { isSyncing } = useUi(); + const { pathname } = useLocation(); + const { pending } = useExtrinsics(); + const { payoutsSynced } = usePayouts(); + const { pluginEnabled } = usePlugins(); + const { validators } = useValidators(); + const { bondedPools } = useBondedPools(); + const { poolMembersNode } = usePoolMembers(); + + // Keep syncing if on nominate page and still fetching payouts. + const onNominateSyncing = () => { + if (pageFromUri(pathname, 'overview') === 'nominate') + if (payoutsSynced !== 'synced') return true; + + return false; + }; + + // Keep syncing if on pools page and still fetching bonded pools or pool members. Ignore pool + // member sync if Subscan is enabled. + const onPoolsSyncing = () => { + if (pageFromUri(pathname, 'overview') === 'pools') + if ( + !bondedPools.length || + (!poolMembersNode.length && !pluginEnabled('subscan')) + ) + return true; + + return false; + }; + + // Keep syncing if on validators page and still fetching. + const onValidatorsSyncing = () => { + if (pageFromUri(pathname, 'overview') === 'validators') + if (!validators.length) return true; + + return false; + }; + + const syncing = + isSyncing || + onNominateSyncing() || + onValidatorsSyncing() || + onPoolsSyncing(); + + return ( + <> + + {/* side menu toggle: shows on small screens */} + + + {/* spinner to show app syncing */} + {syncing || pending.length > 0 ? : null} + + {/* connected accounts */} + + + + + {/* connect button */} + + + + ); +}; diff --git a/src/library/Help/Items/ActiveDefinition.tsx b/src/library/Help/Items/ActiveDefinition.tsx new file mode 100644 index 0000000000..b84857741b --- /dev/null +++ b/src/library/Help/Items/ActiveDefinition.tsx @@ -0,0 +1,22 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { DefinitionWrapper } from '../Wrappers'; + +export const ActiveDefinition = ({ + description, +}: { + description: string[]; +}) => { + return ( + +
+ {description.map((item: any, index: number) => ( +

+ {item} +

+ ))} +
+
+ ); +}; diff --git a/src/library/Help/Items/Definition.tsx b/src/library/Help/Items/Definition.tsx new file mode 100644 index 0000000000..5235b786d6 --- /dev/null +++ b/src/library/Help/Items/Definition.tsx @@ -0,0 +1,47 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { RefObject } from 'react'; +import { useEffect, useRef, useState } from 'react'; +import { DefinitionWrapper } from '../Wrappers'; + +export const Definition = ({ title, description, open: o }: any) => { + // Store whether the definition is open or not. + const [open, setOpen] = useState(o || false); + + // Store the current height of the definition content. + const [height, setHeight] = useState(0); + + const contentRef: RefObject = useRef(null); + + useEffect(() => { + const h = contentRef?.current?.clientHeight || 0; + setHeight(h); + }, [open]); + + return ( + + {!o ? ( + + ) : null} +
+
+ {open ? ( + <> + {description.map((item: any, index: number) => ( +

+ {item} +

+ ))} + + ) : null} +
+
+
+ ); +}; diff --git a/src/library/Help/Items/External.tsx b/src/library/Help/Items/External.tsx new file mode 100644 index 0000000000..30416e97c6 --- /dev/null +++ b/src/library/Help/Items/External.tsx @@ -0,0 +1,44 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faExternalLinkAlt as faExt } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { motion } from 'framer-motion'; +import { useCallback } from 'react'; +import { ItemWrapper } from '../Wrappers'; + +export const External = ({ + width, + height, + subtitle, + title, + url, + website, +}: any) => { + const handleClick = useCallback(() => { + window.open(url, '_blank'); + }, [url]); + + return ( + + +

{title}

+ {subtitle} +

+ + {website !== undefined && website} +

+
+
+ ); +}; diff --git a/src/library/Help/Wrappers.ts b/src/library/Help/Wrappers.ts new file mode 100644 index 0000000000..face240cd7 --- /dev/null +++ b/src/library/Help/Wrappers.ts @@ -0,0 +1,134 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +export const ListWrapper = styled(motion.div)` + display: flex; + flex-flow: row wrap; + flex-grow: 1; + overflow: auto; + padding: 0.75rem 0.5rem; + + > button { + color: var(--text-color-primary); + padding: 0.25rem; + display: flex; + flex-flow: row wrap; + align-items: center; + } + h2 { + color: var(--text-color-primary); + padding: 0 0.75rem; + margin: 0.5rem 0; + width: 100%; + } + p { + color: var(--text-color-primary); + } + .definition { + color: var(--text-color-primary); + padding: 0.75rem; + line-height: 1.4rem; + margin: 0; + } +`; + +export const DefinitionWrapper = styled(motion.div)` + background: var(--background-floating-card); + border-radius: 1.5rem; + display: flex; + flex-flow: row wrap; + flex: 1; + overflow: hidden; + margin-bottom: 1.25rem; + padding: 1.5rem 1.5rem 0 1.5rem; + width: 100%; + + button { + padding: 0; + h2 { + margin: 0 0 1.5rem 0; + display: flex; + flex-flow: row wrap; + align-items: center; + + > span { + color: var(--text-color-secondary); + margin-left: 0.75rem; + opacity: 0.75; + font-size: 1.1rem; + } + } + } + + > div { + position: relative; + transition: height 0.4s cubic-bezier(0.1, 1, 0.2, 1); + width: 100%; + + > .content { + position: absolute; + } + + h4 { + font-family: InterSemiBold, sans-serif; + margin-bottom: 1.15rem; + } + + p { + color: var(--text-color-primary); + margin: 0.5rem 0 0 0; + text-align: left; + } + + p.icon { + opacity: 0.5; + } + } +`; + +export const ItemWrapper = styled(motion.div)` + display: flex; + width: ${(props) => props.width}; + height: ${(props) => (props.height === undefined ? '160px' : props.height)}; + overflow: hidden; + flex-flow: row wrap; + + > * { + background: var(--background-floating-card); + border-radius: 1.5rem; + flex: 1; + padding: 1.5rem; + display: flex; + flex-flow: column nowrap; + margin-bottom: 1.5rem; + position: relative; + + > h2 { + color: var(--text-color-primary); + text-align: left; + } + > h4 { + color: var(--text-color-primary); + margin: 0.65rem 0; + text-transform: uppercase; + font-size: 0.7rem; + } + + > p { + color: var(--text-color-primary); + text-align: left; + + &.icon { + color: var(--accent-color-primary); + margin-bottom: 0; + } + } + + .ext { + margin-right: 0.75rem; + } + } +`; diff --git a/src/library/Help/index.tsx b/src/library/Help/index.tsx new file mode 100644 index 0000000000..42fdf197ad --- /dev/null +++ b/src/library/Help/index.tsx @@ -0,0 +1,215 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import { + ButtonPrimaryInvert, + CanvasContainer, + ModalContent, + CanvasScroll, +} from '@polkadot-cloud/react'; +import { camelize } from '@polkadot-cloud/utils'; +import { useAnimation } from 'framer-motion'; +import { useCallback, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { HelpConfig } from 'config/help'; +import { DefaultLocale } from 'consts'; +import { useHelp } from 'contexts/Help'; +import type { + DefinitionWithKeys, + ExternalItems, + HelpItem, +} from 'contexts/Help/types'; +import { useFillVariables } from 'library/Hooks/useFillVariables'; +import { Definition } from './Items/Definition'; +import { External } from './Items/External'; +import { ActiveDefinition } from './Items/ActiveDefinition'; + +export const Help = () => { + const { t, i18n } = useTranslation('help'); + const controls = useAnimation(); + const { fillVariables } = useFillVariables(); + const { setStatus, status, definition, closeHelp } = useHelp(); + + const onFadeIn = useCallback(async () => { + await controls.start('visible'); + }, []); + + const onFadeOut = useCallback(async () => { + await controls.start('hidden'); + setStatus('closed'); + }, []); + + // control canvas fade. + useEffect(() => { + if (status === 'open') onFadeIn(); + if (status === 'closing') onFadeOut(); + }, [status]); + + // render early if help not open + if (status === 'closed') return <>; + + let meta: HelpItem | undefined; + + if (definition) { + // get items for active category + meta = Object.values(HelpConfig).find( + (c) => c?.definitions?.find((d) => d === definition) + ); + } else { + // get all items + let definitions: string[] = []; + let external: ExternalItems = []; + + Object.values(HelpConfig).forEach((c) => { + definitions = definitions.concat([...(c.definitions || [])]); + external = external.concat([...(c.external || [])]); + }); + meta = { definitions, external }; + } + + let definitions = meta?.definitions ?? []; + + const activeDefinitions = definitions + .filter((d) => d !== definition) + .map((d) => { + const localeKey = camelize(d); + + return fillVariables( + { + title: t(`definitions.${localeKey}.0`), + description: i18n.getResource( + i18n.resolvedLanguage ?? DefaultLocale, + 'help', + `definitions.${localeKey}.1` + ), + }, + ['title', 'description'] + ); + }); + + // get active definiton + const activeRecord = definition + ? definitions.find((d) => d === definition) + : null; + + let activeDefinition: DefinitionWithKeys | null = null; + if (activeRecord) { + const localeKey = camelize(activeRecord); + + const title = t(`definitions.${localeKey}.0`); + const description = i18n.getResource( + i18n.resolvedLanguage ?? DefaultLocale, + 'help', + `definitions.${localeKey}.1` + ); + + activeDefinition = fillVariables( + { + title, + description, + }, + ['title', 'description'] + ); + + // filter active definition + definitions = definitions.filter((d: string) => d !== definition); + } + + // accumulate external resources + const externals = meta?.external ?? []; + const activeExternals = externals.map((e) => { + const localeKey = e[0]; + const url = e[1]; + const website = e[2]; + + return { + title: t(`externals.${localeKey}`), + url, + website, + }; + }); + + return ( + + + +
+ closeHelp()} + /> +
+

+ {activeDefinition + ? `${activeDefinition.title}` + : `${t('modal.helpResources')}`} +

+ + {activeDefinition !== null && ( + + )} + + {definitions.length > 0 && ( + <> +

+ {activeDefinition ? `${t('modal.related')} ` : ''} + {t('modal.definitions')} +

+ {activeDefinitions.map((item, index: number) => ( + {}} + title={item.title} + description={item.description} + /> + ))} + + )} + + {activeExternals.length > 0 && ( + <> +

{t('modal.articles')}

+ {activeExternals.map((item, index: number) => ( + + ))} + + )} +
+
+ +
+ ); +}; diff --git a/src/library/Hooks/index.tsx b/src/library/Hooks/index.tsx new file mode 100644 index 0000000000..9f40972048 --- /dev/null +++ b/src/library/Hooks/index.tsx @@ -0,0 +1,50 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { FC } from 'react'; +import { useEffect } from 'react'; +import type { AnyJson } from 'types'; + +/* + * A hook that alerts clicks outside of the passed ref. + */ +export const useOutsideAlerter = ( + ref: any, + callback: any, + ignore: any = [] +) => { + useEffect(() => { + const handleClickOutside = (event: any) => { + if (ref.current && !ref.current.contains(event.target)) { + const invalid = ignore.find((i: any) => + event.target.classList.contains(i) + ); + if (invalid === undefined) { + callback(); + } + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref]); +}; + +// A pure function that applies an arbitrary amount of context providers to a wrapped +// component. +export const withProviders = ( + providers: (FC | [FC, AnyJson])[], + Wrapped: FC +) => + providers.reduceRight( + (acc, prov) => { + if (Array.isArray(prov)) { + const Provider = prov[0]; + return {acc}; + } + const Provider = prov; + return {acc}; + }, + + ); diff --git a/src/library/Hooks/useBatchCall/index.tsx b/src/library/Hooks/useBatchCall/index.tsx new file mode 100644 index 0000000000..b3ee2dfca7 --- /dev/null +++ b/src/library/Hooks/useBatchCall/index.tsx @@ -0,0 +1,38 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useApi } from 'contexts/Api'; +import type { AnyApi, MaybeAddress } from 'types'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useProxySupported } from '../useProxySupported'; + +export const useBatchCall = () => { + const { api } = useApi(); + const { activeProxy } = useActiveAccounts(); + const { isProxySupported } = useProxySupported(); + + const newBatchCall = (txs: AnyApi[], from: MaybeAddress) => { + if (!api) return undefined; + + from = from || ''; + + if (activeProxy && isProxySupported(api.tx.utility.batch(txs), from)) { + return api?.tx.utility.batch( + txs.map((tx) => + api.tx.proxy.proxy( + { + id: from, + }, + null, + tx + ) + ) + ); + } + return api?.tx.utility.batch(txs); + }; + + return { + newBatchCall, + }; +}; diff --git a/src/library/Hooks/useBlockNumber/index.tsx b/src/library/Hooks/useBlockNumber/index.tsx new file mode 100644 index 0000000000..02bbd178ae --- /dev/null +++ b/src/library/Hooks/useBlockNumber/index.tsx @@ -0,0 +1,45 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useEffect, useRef, useState } from 'react'; +import { useApi } from 'contexts/Api'; +import type { AnyApi } from 'types'; +import { useNetwork } from 'contexts/Network'; + +export const useBlockNumber = () => { + const { network } = useNetwork(); + const { isReady, api } = useApi(); + + // store the current block number. + const [block, setBlock] = useState(new BigNumber(0)); + + // store block unsub. + const unsub = useRef(); + + useEffect(() => { + if (isReady) { + subscribeBlockNumber(); + } + return () => { + if (unsub.current) unsub.current(); + }; + }, [network, isReady]); + + const subscribeBlockNumber = async () => { + if (!api) return; + + const subscribeBlock = async () => { + const u = await api.query.system.number((number: AnyApi) => { + setBlock(new BigNumber(rmCommas(number.toString()))); + }); + return u; + }; + Promise.all([subscribeBlock]).then(([u]) => { + unsub.current = u; + }); + }; + + return block; +}; diff --git a/src/library/Hooks/useBondGreatestFee/index.tsx b/src/library/Hooks/useBondGreatestFee/index.tsx new file mode 100644 index 0000000000..49dffb0bba --- /dev/null +++ b/src/library/Hooks/useBondGreatestFee/index.tsx @@ -0,0 +1,63 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useEffect, useMemo, useState } from 'react'; +import { useApi } from 'contexts/Api'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import type { BondFor } from 'types'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +interface Props { + bondFor: BondFor; +} + +export const useBondGreatestFee = ({ bondFor }: Props) => { + const { api } = useApi(); + const { activeAccount } = useActiveAccounts(); + const { feeReserve, getTransferOptions } = useTransferOptions(); + const transferOptions = useMemo( + () => getTransferOptions(activeAccount), + [activeAccount] + ); + const { freeBalance } = transferOptions; + + // store the largest possible tx fees for bonding. + const [largestTxFee, setLargestTxFee] = useState(new BigNumber(0)); + + // update max tx fee on free balance change + useEffect(() => { + handleFetch(); + }, [transferOptions]); + + // handle fee fetching + const handleFetch = async () => { + const largestFee = await txLargestFee(); + setLargestTxFee(largestFee); + }; + + // estimate the largest possible tx fee based on users free balance. + const txLargestFee = async () => { + const bond = BigNumber.max(freeBalance.minus(feeReserve), 0).toString(); + + let tx = null; + if (!api) { + return new BigNumber(0); + } + if (bondFor === 'pool') { + tx = api.tx.nominationPools.bondExtra({ + FreeBalance: bond, + }); + } else if (bondFor === 'nominator') { + tx = api.tx.staking.bondExtra(bond); + } + + if (tx) { + const { partialFee } = await tx.paymentInfo(activeAccount || ''); + return new BigNumber(partialFee.toString()); + } + return new BigNumber(0); + }; + + return largestTxFee; +}; diff --git a/src/library/Hooks/useBuildPayload/index.tsx b/src/library/Hooks/useBuildPayload/index.tsx new file mode 100644 index 0000000000..230e24e25c --- /dev/null +++ b/src/library/Hooks/useBuildPayload/index.tsx @@ -0,0 +1,64 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useApi } from 'contexts/Api'; +import { useBalances } from 'contexts/Balances'; +import { useTxMeta } from 'contexts/TxMeta'; +import type { AnyApi } from 'types'; + +export const useBuildPayload = () => { + const { api } = useApi(); + const { getNonce } = useBalances(); + const { setTxPayload } = useTxMeta(); + + // Build and set payload of the transaction and store it in TxMetaContext. + const buildPayload = async (tx: AnyApi, from: string, uid: number) => { + if (api && tx) { + const lastHeader = await api.rpc.chain.getHeader(); + const blockNumber = api.registry.createType( + 'BlockNumber', + lastHeader.number.toNumber() + ); + const method = api.createType('Call', tx); + const era = api.registry.createType('ExtrinsicEra', { + current: lastHeader.number.toNumber(), + period: 64, + }); + + const accountNonce = getNonce(from); + const nonce = api.registry.createType('Compact', accountNonce); + + const payload = { + specVersion: api.runtimeVersion.specVersion.toHex(), + transactionVersion: api.runtimeVersion.transactionVersion.toHex(), + address: from, + blockHash: lastHeader.hash.toHex(), + blockNumber: blockNumber.toHex(), + era: era.toHex(), + genesisHash: api.genesisHash.toHex(), + method: method.toHex(), + nonce: nonce.toHex(), + signedExtensions: [ + 'CheckNonZeroSender', + 'CheckSpecVersion', + 'CheckTxVersion', + 'CheckGenesis', + 'CheckMortality', + 'CheckNonce', + 'CheckWeight', + 'ChargeTransactionPayment', + ], + tip: api.registry.createType('Compact', 0).toHex(), + version: tx.version, + }; + const raw = api.registry.createType('ExtrinsicPayload', payload, { + version: payload.version, + }); + setTxPayload(raw, uid); + } + }; + + return { + buildPayload, + }; +}; diff --git a/src/library/Hooks/useDotLottieButton/index.tsx b/src/library/Hooks/useDotLottieButton/index.tsx new file mode 100644 index 0000000000..3caf2c6d4d --- /dev/null +++ b/src/library/Hooks/useDotLottieButton/index.tsx @@ -0,0 +1,100 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useEffect, useRef, useState } from 'react'; +import { useTheme } from 'contexts/Themes'; +import type { Theme } from 'contexts/Themes/types'; +import type { AnyJson } from 'types'; + +export const useDotLottieButton = (filename: string, options: AnyJson = {}) => { + const { mode } = useTheme(); + + const refLight = useRef(null); + const refDark = useRef(null); + const refsInitialised = useRef(false); + + const getRef = (m: Theme) => { + return m === 'light' ? refLight.current : refDark.current; + }; + + const handlePlayAnimation = async () => { + if (!getRef(mode)) return; + getRef(mode).play(); + }; + + const handleComplete = (r: AnyJson) => { + if (options?.autoLoop !== true) { + r?.stop(); + } + }; + useEffect(() => { + if (!getRef('light') || !getRef('dark') || refsInitialised.current) return; + refsInitialised.current = true; + + getRef('light').addEventListener('loop', () => + handleComplete(getRef('light')) + ); + getRef('dark').addEventListener('loop', () => + handleComplete(getRef('dark')) + ); + }, [getRef('light'), getRef('dark'), refsInitialised.current]); + + useEffect(() => { + refsInitialised.current = false; + }, [options?.deps]); + + const autoPlay = options?.autoLoop ?? undefined; + + const [iconLight] = useState( + + ); + + const [iconDark] = useState( + + ); + + const icon = ( + <> + + + + ); + + return { icon, play: handlePlayAnimation }; +}; diff --git a/src/library/Hooks/useEraTimeLeft/index.tsx b/src/library/Hooks/useEraTimeLeft/index.tsx new file mode 100644 index 0000000000..b52a2221d2 --- /dev/null +++ b/src/library/Hooks/useEraTimeLeft/index.tsx @@ -0,0 +1,42 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { getUnixTime } from 'date-fns'; +import { useApi } from 'contexts/Api'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; + +export const useEraTimeLeft = () => { + const { consts } = useApi(); + const { epochDuration, expectedBlockTime, sessionsPerEra } = consts; + const { activeEra } = useNetworkMetrics(); + + // important to fetch the actual timeleft from when other components ask for it. + const get = () => { + // get timestamp of era start and convert to seconds. + const start = activeEra.start.multipliedBy(0.001); + + // store the duration of an era in block numbers. + const eraDurationBlocks = epochDuration.multipliedBy(sessionsPerEra); + + // estimate the duration of the era in seconds + const eraDuration = eraDurationBlocks + .multipliedBy(expectedBlockTime) + .multipliedBy(0.001); + + // estimate the end time of the era + const end = start.plus(eraDuration); + + // estimate remaining time of era. + const timeleft = BigNumber.max(0, end.minus(getUnixTime(new Date()))); + + // percentage of eraDuration + const percentage = eraDuration.multipliedBy(0.01); + const percentRemaining = timeleft.dividedBy(percentage); + const percentSurpassed = new BigNumber(100).minus(percentRemaining); + + return { timeleft, percentSurpassed, percentRemaining }; + }; + + return { get }; +}; diff --git a/src/library/Hooks/useErasToTimeLeft/index.tsx b/src/library/Hooks/useErasToTimeLeft/index.tsx new file mode 100644 index 0000000000..bd27343776 --- /dev/null +++ b/src/library/Hooks/useErasToTimeLeft/index.tsx @@ -0,0 +1,32 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { greaterThanZero } from '@polkadot-cloud/utils'; +import type BigNumber from 'bignumber.js'; +import { useApi } from 'contexts/Api'; + +export const useErasToTimeLeft = () => { + const { consts } = useApi(); + const { epochDuration, expectedBlockTime, sessionsPerEra } = consts; + + // converts a number of eras to timeleft in seconds. + const erasToSeconds = (eras: BigNumber) => { + if (!greaterThanZero(eras)) { + return 0; + } + // store the duration of an era in number of blocks. + const eraDurationBlocks = epochDuration.multipliedBy(sessionsPerEra); + // estimate the duration of the era in seconds. + const eraDuration = eraDurationBlocks + .multipliedBy(expectedBlockTime) + .multipliedBy(0.001) + .integerValue(); + + // multiply by number of eras. + return eras.multipliedBy(eraDuration).toNumber(); + }; + + return { + erasToSeconds, + }; +}; diff --git a/src/library/Hooks/useFillVariables/index.tsx b/src/library/Hooks/useFillVariables/index.tsx new file mode 100644 index 0000000000..7cc2126443 --- /dev/null +++ b/src/library/Hooks/useFillVariables/index.tsx @@ -0,0 +1,82 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { capitalizeFirstLetter, planckToUnit } from '@polkadot-cloud/utils'; +import { useApi } from 'contexts/Api'; +import { useNetwork } from 'contexts/Network'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import type { AnyJson } from 'types'; + +export const useFillVariables = () => { + const { consts } = useApi(); + const { stats } = usePoolsConfig(); + const { networkData } = useNetwork(); + const { + maxNominations, + maxNominatorRewardedPerValidator, + existentialDeposit, + } = consts; + const { minJoinBond, minCreateBond } = stats; + const { metrics } = useNetworkMetrics(); + const { minimumActiveStake } = metrics; + + const fillVariables = (d: AnyJson, keys: string[]) => { + const fields: AnyJson = Object.entries(d).filter(([k]: any) => + keys.includes(k) + ); + const transformed = Object.entries(fields).map( + ([, [key, val]]: AnyJson) => { + const varsToValues = [ + ['{NETWORK_UNIT}', networkData.unit], + ['{NETWORK_NAME}', capitalizeFirstLetter(networkData.name)], + [ + '{MAX_NOMINATOR_REWARDED_PER_VALIDATOR}', + maxNominatorRewardedPerValidator.toString(), + ], + ['{MAX_NOMINATIONS}', maxNominations.toString()], + [ + '{MIN_ACTIVE_STAKE}', + planckToUnit(minimumActiveStake, networkData.units) + .decimalPlaces(3) + .toFormat(), + ], + [ + '{MIN_POOL_JOIN_BOND}', + planckToUnit(minJoinBond, networkData.units) + .decimalPlaces(3) + .toFormat(), + ], + [ + '{MIN_POOL_CREATE_BOND}', + planckToUnit(minCreateBond, networkData.units) + .decimalPlaces(3) + .toFormat(), + ], + [ + '{EXISTENTIAL_DEPOSIT}', + planckToUnit(existentialDeposit, networkData.units).toFormat(), + ], + ]; + + for (const varToVal of varsToValues) { + if (val.constructor === Array) { + val = val.map((_d) => _d.replaceAll(varToVal[0], varToVal[1])); + } else { + val = val.replaceAll(varToVal[0], varToVal[1]); + } + } + return [key, val]; + } + ); + + return { + ...d, + ...Object.fromEntries(transformed), + }; + }; + + return { + fillVariables, + }; +}; diff --git a/src/library/Hooks/useFillVariables/types.ts b/src/library/Hooks/useFillVariables/types.ts new file mode 100644 index 0000000000..0bd4de61e7 --- /dev/null +++ b/src/library/Hooks/useFillVariables/types.ts @@ -0,0 +1,7 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface FillVariableItem { + title: string; + description: string[]; +} diff --git a/src/library/Hooks/useInflation/index.tsx b/src/library/Hooks/useInflation/index.tsx new file mode 100644 index 0000000000..74f770b63f --- /dev/null +++ b/src/library/Hooks/useInflation/index.tsx @@ -0,0 +1,62 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useNetwork } from 'contexts/Network'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useStaking } from 'contexts/Staking'; + +export const useInflation = () => { + const { + networkData: { params }, + } = useNetwork(); + const { metrics } = useNetworkMetrics(); + const { staking } = useStaking(); + const { lastTotalStake } = staking; + const { totalIssuance, auctionCounter } = metrics; + + const { + auctionAdjust, + auctionMax, + falloff, + maxInflation, + minInflation, + stakeTarget, + } = params; + + const BIGNUMBER_MILLION = new BigNumber(1_000_000); + + const calculateInflation = ( + totalStaked: BigNumber, + numAuctions: BigNumber + ) => { + const stakedFraction = + totalStaked.isZero() || totalIssuance.isZero() + ? 0 + : totalStaked + .multipliedBy(BIGNUMBER_MILLION) + .dividedBy(totalIssuance) + .toNumber() / BIGNUMBER_MILLION.toNumber(); + const idealStake = + stakeTarget - + Math.min(auctionMax, numAuctions.toNumber()) * auctionAdjust; + const idealInterest = maxInflation / idealStake; + const inflation = + 100 * + (minInflation + + (stakedFraction <= idealStake + ? stakedFraction * (idealInterest - minInflation / idealStake) + : (idealInterest * idealStake - minInflation) * + 2 ** ((idealStake - stakedFraction) / falloff))); + + return { + idealInterest, + idealStake, + inflation, + stakedFraction, + stakedReturn: stakedFraction ? inflation / stakedFraction : 0, + }; + }; + + return calculateInflation(lastTotalStake, auctionCounter); +}; diff --git a/src/library/Hooks/useLedgerLoop/index.tsx b/src/library/Hooks/useLedgerLoop/index.tsx new file mode 100644 index 0000000000..a8fece5af8 --- /dev/null +++ b/src/library/Hooks/useLedgerLoop/index.tsx @@ -0,0 +1,45 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useLedgerHardware } from 'contexts/Hardware/Ledger'; +import { getLedgerApp } from 'contexts/Hardware/Utils'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useNetwork } from 'contexts/Network'; +import type { LederLoopProps } from './types'; + +export const useLedgerLoop = ({ tasks, options, mounted }: LederLoopProps) => { + const { setIsPaired, getIsExecuting, getStatusCodes, executeLedgerLoop } = + useLedgerHardware(); + const { network } = useNetwork(); + const { getTxPayload, getPayloadUid } = useTxMeta(); + const { appName } = getLedgerApp(network); + + // Connect to Ledger device and perform necessary tasks. + // + // The tasks sent to the device depend on the current state of the import process. + const handleLedgerLoop = async () => { + // If the import modal is no longer open, cancel execution. + if (!mounted()) { + return; + } + // If the app is not open on-device, or device is not connected, cancel execution. + // If we are to explore auto looping via an interval, this may wish to use `determineStatusFromCode` instead. + if (['DeviceNotConnected'].includes(getStatusCodes()[0]?.statusCode)) { + setIsPaired('unpaired'); + } else { + // Get task options and execute the loop. + const uid = getPayloadUid(); + const accountIndex = options?.accountIndex ? options.accountIndex() : 0; + const payload = await getTxPayload(); + if (getIsExecuting()) { + await executeLedgerLoop(appName, tasks, { + uid, + accountIndex, + payload, + }); + } + } + }; + + return { handleLedgerLoop }; +}; diff --git a/src/library/Hooks/useLedgerLoop/types.ts b/src/library/Hooks/useLedgerLoop/types.ts new file mode 100644 index 0000000000..2e4b5aa817 --- /dev/null +++ b/src/library/Hooks/useLedgerLoop/types.ts @@ -0,0 +1,15 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { LedgerTask } from 'contexts/Hardware/types'; +import type { AnyJson } from 'types'; + +export interface LederLoopProps { + tasks: LedgerTask[]; + options: { + uid?: number; + accountIndex?: () => number; + payload?: () => Promise; + }; + mounted: () => boolean; +} diff --git a/src/library/Hooks/useNominationStatus/index.tsx b/src/library/Hooks/useNominationStatus/index.tsx new file mode 100644 index 0000000000..df6a71a792 --- /dev/null +++ b/src/library/Hooks/useNominationStatus/index.tsx @@ -0,0 +1,120 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useBonded } from 'contexts/Bonded'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import type { AnyJson, BondFor, MaybeAddress } from 'types'; +import { useNetwork } from 'contexts/Network'; + +export const useNominationStatus = () => { + const { t } = useTranslation(); + const { isSyncing } = useUi(); + const { + networkData: { units }, + } = useNetwork(); + const { + inSetup, + eraStakers, + erasStakersSyncing, + getNominationsStatusFromTargets, + getLowestRewardFromStaker, + } = useStaking(); + const { validators } = useValidators(); + const { poolNominations } = useActivePools(); + const { getAccountNominations } = useBonded(); + + // Utility to get an account's nominees alongside their status. + const getNomineesStatus = (who: MaybeAddress, type: BondFor) => { + const nominations = + type === 'nominator' + ? getAccountNominations(who) + : poolNominations?.targets ?? []; + + return getNominationsStatusFromTargets(who, nominations); + }; + + // Utility to get the nominees of a provided nomination status. + const getNomineesByStatus = (nominees: AnyJson[], status: string) => + nominees + .map(([k, v]) => (v === status ? k : false)) + .filter((v) => v !== false); + + // Utility to get the status of the provided account's nominations, and whether they are earning + // reards. + const getNominationStatus = (who: MaybeAddress, type: BondFor) => { + // Get the sets nominees from the provided account's targets. + const nominees = Object.entries(getNomineesStatus(who, type)); + const activeNominees = getNomineesByStatus(nominees, 'active'); + + // Determine whether active nominees are earning rewards. This function exists once the + // eras stakers has synced. + let earningRewards = false; + if (!erasStakersSyncing) { + getNomineesByStatus(nominees, 'active').every((nominee) => { + const validator = validators.find(({ address }) => address === nominee); + + if (validator) { + const others = + eraStakers.stakers.find(({ address }) => address === nominee) + ?.others || []; + + if (others.length) { + // If the provided account is a part of the validator's backers, check if they are above + // the lowest reward threshold. If so, they are earning rewards and this iteration can + // exit. + const stakedValue = + others?.find((o) => o.who === who)?.value ?? false; + if (stakedValue) { + const { lowest } = getLowestRewardFromStaker(nominee); + if ( + planckToUnit( + new BigNumber(stakedValue), + units + ).isGreaterThanOrEqualTo(lowest) + ) { + earningRewards = true; + return false; + } + } + } + } + return true; + }); + } + + // Determine the localised message to display based on the nomination status. + let str; + if (inSetup() || isSyncing) { + str = t('nominate.notNominating', { ns: 'pages' }); + } else if (!nominees.length) { + str = t('nominate.noNominationsSet', { ns: 'pages' }); + } else if (activeNominees.length) { + str = t('nominate.nominatingAnd', { ns: 'pages' }); + if (earningRewards) { + str += ` ${t('nominate.earningRewards', { ns: 'pages' })}`; + } else { + str += ` ${t('nominate.notEarningRewards', { ns: 'pages' })}`; + } + } else { + str = t('nominate.waitingForActiveNominations', { ns: 'pages' }); + } + + return { + nominees: { + active: activeNominees, + inactive: getNomineesByStatus(nominees, 'inactive'), + waiting: getNomineesByStatus(nominees, 'waiting'), + }, + earningRewards, + message: str, + }; + }; + + return { getNominationStatus, getNomineesStatus }; +}; diff --git a/src/library/Hooks/usePayeeConfig/index.tsx b/src/library/Hooks/usePayeeConfig/index.tsx new file mode 100644 index 0000000000..b0749ce9fd --- /dev/null +++ b/src/library/Hooks/usePayeeConfig/index.tsx @@ -0,0 +1,65 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { + faArrowDown, + faArrowRightFromBracket, + faRedoAlt, + faStop, +} from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import type { PayeeOptions } from 'contexts/Setup/types'; + +export interface PayeeItem { + icon: IconProp; + value: PayeeOptions; + title: string; + activeTitle: string; + subtitle: string; +} + +export const usePayeeConfig = () => { + const { t } = useTranslation('base'); + const getPayeeItems = (extended?: boolean): PayeeItem[] => { + let items: PayeeItem[] = [ + { + value: 'Staked', + title: t('payee.staked.title', { context: 'default' }), + activeTitle: t('payee.staked.title', { context: 'active' }), + subtitle: t('payee.staked.subtitle'), + icon: faRedoAlt, + }, + { + value: 'Stash', + title: t('payee.stash.title'), + activeTitle: t('payee.stash.title'), + subtitle: t('payee.stash.subtitle'), + icon: faArrowDown, + }, + { + value: 'Account', + title: t('payee.account.title', { context: 'default' }), + activeTitle: t('payee.account.title'), + subtitle: t('payee.account.subtitle'), + icon: faArrowRightFromBracket, + }, + ]; + + if (extended) { + items = items.concat([ + { + value: 'None', + title: t('payee.none.title', { context: 'default' }), + activeTitle: t('payee.none.title', { context: 'active' }), + subtitle: t('payee.none.subtitle'), + icon: faStop, + }, + ]); + } + + return items; + }; + + return { getPayeeItems }; +}; diff --git a/src/library/Hooks/usePoolCommission/index.tsx b/src/library/Hooks/usePoolCommission/index.tsx new file mode 100644 index 0000000000..7358421f84 --- /dev/null +++ b/src/library/Hooks/usePoolCommission/index.tsx @@ -0,0 +1,19 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; + +export const usePoolCommission = () => { + const { getBondedPool } = useBondedPools(); + const { stats } = usePoolsConfig(); + const { globalMaxCommission } = stats; + + const getCurrentCommission = (id: number): number => + Math.min( + Number(getBondedPool(id)?.commission?.current?.[0]?.slice(0, -1) || 0), + globalMaxCommission + ); + + return { getCurrentCommission }; +}; diff --git a/src/library/Hooks/usePoolFilters/index.tsx b/src/library/Hooks/usePoolFilters/index.tsx new file mode 100644 index 0000000000..07e87b87bd --- /dev/null +++ b/src/library/Hooks/usePoolFilters/index.tsx @@ -0,0 +1,162 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors + +import { useTranslation } from 'react-i18next'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import type { BondedPool } from 'contexts/Pools/types'; +import { useStaking } from 'contexts/Staking'; +import type { AnyFunction, AnyJson } from 'types'; + +export const usePoolFilters = () => { + const { t } = useTranslation('library'); + const { meta } = useBondedPools(); + const { getNominationsStatusFromTargets } = useStaking(); + const { getPoolNominationStatusCode } = useBondedPools(); + + /* + * include active pools. + * Iterates through the supplied list and refers to the meta + * batch of the list to filter those list items that are + * actively nominating. + * Returns the updated filtered list. + */ + const includeActive = (list: any, batchKey: string) => { + // get pool targets from nominations meta batch + const nominations = meta[batchKey]?.nominations ?? []; + if (!nominations) { + return list; + } + let i = -1; + const filteredList = list.filter((p: BondedPool) => { + i++; + const targets = nominations[i]?.targets ?? []; + const status = getPoolNominationStatusCode( + getNominationsStatusFromTargets(p.addresses.stash, targets) + ); + return status === 'active'; + }); + return filteredList; + }; + + /* + * dont include active pools. + * Iterates through the supplied list and refers to the meta + * batch of the list to filter those list items that are + * actively nominating. + * Returns the updated filtered list. + */ + const excludeActive = (list: any, batchKey: string) => { + // get pool targets from nominations meta batch + const nominations = meta[batchKey]?.nominations ?? []; + if (!nominations) { + return list; + } + let i = -1; + const filteredList = list.filter((p: BondedPool) => { + i++; + const targets = nominations[i]?.targets ?? []; + const status = getPoolNominationStatusCode( + getNominationsStatusFromTargets(p.addresses.stash, targets) + ); + return status !== 'active'; + }); + return filteredList; + }; + + /* + * include locked pools. + * Iterates through the supplied list and checks whether state is locked. + * Returns the updated filtered list. + */ + const includeLocked = (list: any) => + list.filter((p: BondedPool) => p.state.toLowerCase() === 'Blocked'); + + /* + * include destroying pools. + * Iterates through the supplied list and checks whether state is destroying. + * Returns the updated filtered list. + */ + const includeDestroying = (list: any) => + list.filter((p: BondedPool) => p.state === 'Destroying'); + + /* + * exclude locked pools. + * Iterates through the supplied list and checks whether state is locked. + * Returns the updated filtered list. + */ + const excludeLocked = (list: any) => + list.filter((p: BondedPool) => p.state !== 'Blocked'); + + /* + * exclude destroying pools. + * Iterates through the supplied list and checks whether state is destroying. + * Returns the updated filtered list. + */ + const excludeDestroying = (list: any) => + list.filter((p: BondedPool) => p.state !== 'Destroying'); + + // includes to be listed in filter overlay. + const includesToLabels: Record = { + active: t('activePools'), + }; + + // excludes to be listed in filter overlay. + const excludesToLabels: Record = { + locked: t('lockedPools'), + destroying: t('destroyingPools'), + }; + + // match include keys to their associated filter functions. + const includeToFunction: Record = { + active: includeActive, + locked: includeLocked, + destroying: includeDestroying, + }; + + // match exclude keys to their associated filter functions. + const excludeToFunction: Record = { + active: excludeActive, + locked: excludeLocked, + destroying: excludeDestroying, + }; + + // get filter functions from keys and type of filter. + const getFiltersFromKey = (key: string[], type: string) => { + const filters = type === 'include' ? includeToFunction : excludeToFunction; + const fns = []; + for (const k of key) { + if (filters[k]) { + fns.push(filters[k]); + } + } + return fns; + }; + + // applies filters based on the provided include and exclude keys. + const applyFilter = ( + includes: string[] | null, + excludes: string[] | null, + list: AnyJson, + batchKey: string + ) => { + if (!excludes && !includes) { + return list; + } + if (includes) { + for (const fn of getFiltersFromKey(includes, 'include')) { + list = fn(list, batchKey); + } + } + if (excludes) { + for (const fn of getFiltersFromKey(excludes, 'exclude')) { + list = fn(list, batchKey); + } + } + return list; + }; + + return { + includesToLabels, + excludesToLabels, + applyFilter, + }; +}; diff --git a/src/library/Hooks/usePrices/index.tsx b/src/library/Hooks/usePrices/index.tsx new file mode 100644 index 0000000000..6280659d1f --- /dev/null +++ b/src/library/Hooks/usePrices/index.tsx @@ -0,0 +1,83 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useEffect, useRef, useState } from 'react'; +import { usePlugins } from 'contexts/Plugins'; +import { useUnitPrice } from 'library/Hooks/useUnitPrice'; +import { useNetwork } from 'contexts/Network'; + +export const usePrices = () => { + const { network } = useNetwork(); + const { plugins } = usePlugins(); + const fetchUnitPrice = useUnitPrice(); + + const pricesLocalStorage = () => { + const pricesLocal = localStorage.getItem(`${network}_prices`); + return pricesLocal === null + ? { + lastPrice: 0, + change: 0, + } + : JSON.parse(pricesLocal); + }; + + const [prices, _setPrices] = useState(pricesLocalStorage()); + const pricesRef = useRef(prices); + + const setPrices = (p: any) => { + localStorage.setItem(`${network}_prices`, JSON.stringify(p)); + pricesRef.current = { + ...pricesRef.current, + ...p, + }; + _setPrices({ + ...pricesRef.current, + ...p, + }); + }; + + const initiatePriceInterval = async () => { + setPrices(await fetchUnitPrice()); + if (priceHandle === null) { + setPriceInterval(); + } + }; + + let priceHandle: any = null; + const setPriceInterval = async () => { + priceHandle = setInterval(async () => { + setPrices(await fetchUnitPrice()); + }, 1000 * 30); + }; + + // initial price subscribe + useEffect(() => { + initiatePriceInterval(); + return () => { + if (priceHandle !== null) { + clearInterval(priceHandle); + } + }; + }, []); + + // resubscribe on network toggle + useEffect(() => { + if (priceHandle !== null) { + clearInterval(priceHandle); + } + initiatePriceInterval(); + }, [network]); + + // servie toggle + useEffect(() => { + if (plugins.includes('binance_spot')) { + if (priceHandle === null) { + initiatePriceInterval(); + } + } else if (priceHandle !== null) { + clearInterval(priceHandle); + } + }, [plugins]); + + return pricesRef.current; +}; diff --git a/src/library/Hooks/useProxySupported/index.tsx b/src/library/Hooks/useProxySupported/index.tsx new file mode 100644 index 0000000000..64dc7ff438 --- /dev/null +++ b/src/library/Hooks/useProxySupported/index.tsx @@ -0,0 +1,64 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + UnsupportedIfUniqueController, + isSupportedProxyCall, +} from 'config/proxies'; +import { useBonded } from 'contexts/Bonded'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useProxies } from 'contexts/Proxies'; +import type { AnyApi, AnyJson, MaybeAddress } from 'types'; + +export const useProxySupported = () => { + const { getBondedAccount } = useBonded(); + const { getProxyDelegate } = useProxies(); + const { activeProxy } = useActiveAccounts(); + + // If call is from controller, & controller is different from stash, then proxy is not + // supported. + const controllerNotSupported = (c: string, f: MaybeAddress) => + UnsupportedIfUniqueController.includes(c) && getBondedAccount(f) !== f; + + // Determine whether the provided tx is proxy supported. + const isProxySupported = (tx: AnyApi, delegator: MaybeAddress) => { + // if already wrapped, return. + if ( + tx?.method.toHuman().section === 'proxy' && + tx?.method.toHuman().method === 'proxy' + ) { + return true; + } + + const proxyDelegate = getProxyDelegate(delegator, activeProxy); + const proxyType = proxyDelegate?.proxyType || ''; + const pallet = tx?.method.toHuman().section; + const method = tx?.method.toHuman().method; + const call = `${pallet}.${method}`; + + // If a batch call, test if every inner call is a supported proxy call. + if (call === 'utility.batch') { + return (tx?.method?.toHuman()?.args?.calls || []) + .map((c: AnyJson) => ({ + pallet: c.section, + method: c.method, + })) + .every( + (c: AnyJson) => + (isSupportedProxyCall(proxyType, c.pallet, c.method) || + (c.pallet === 'proxy' && c.method === 'proxy')) && + !controllerNotSupported(`${pallet}.${method}`, delegator) + ); + } + + // Check if the current call is a supported proxy call. + return ( + isSupportedProxyCall(proxyType, pallet, method) && + !controllerNotSupported(call, delegator) + ); + }; + + return { + isProxySupported, + }; +}; diff --git a/src/library/Hooks/useSignerWarnings/index.tsx b/src/library/Hooks/useSignerWarnings/index.tsx new file mode 100644 index 0000000000..0b51c27a85 --- /dev/null +++ b/src/library/Hooks/useSignerWarnings/index.tsx @@ -0,0 +1,47 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { useTxMeta } from 'contexts/TxMeta'; +import type { MaybeAddress } from 'types'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const useSignerWarnings = () => { + const { t } = useTranslation('modals'); + const { activeProxy } = useActiveAccounts(); + const { accountHasSigner } = useImportedAccounts(); + const { controllerSignerAvailable } = useTxMeta(); + + const getSignerWarnings = ( + account: MaybeAddress, + controller = false, + proxySupported = false + ) => { + const warnings = []; + + if (controller) { + switch (controllerSignerAvailable(account, proxySupported)) { + case 'controller_not_imported': + warnings.push(`${t('controllerImported')}`); + break; + case 'read_only': + warnings.push(`${t('readOnlyCannotSign')}`); + break; + default: + break; + } + } else if ( + !( + accountHasSigner(account) || + (accountHasSigner(activeProxy) && proxySupported) + ) + ) { + warnings.push(`${t('readOnlyCannotSign')}`); + } + + return warnings; + }; + + return { getSignerWarnings }; +}; diff --git a/src/library/Hooks/useSize/index.tsx b/src/library/Hooks/useSize/index.tsx new file mode 100644 index 0000000000..e6c07df999 --- /dev/null +++ b/src/library/Hooks/useSize/index.tsx @@ -0,0 +1,38 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import throttle from 'lodash.throttle'; +import React from 'react'; +import { useUi } from 'contexts/UI'; + +export const getSize = (element: any) => { + const width = element?.offsetWidth; + const height = element?.offsetHeight; + return { height, width }; +}; + +export const useSize = (element: any) => { + const { containerRefs } = useUi(); + + const [size, setSize] = React.useState(getSize(element)); + + const throttleCallback = () => { + setSize(getSize(element)); + }; + + React.useEffect(() => { + const resizeThrottle = throttle(throttleCallback, 100, { + trailing: true, + leading: false, + }); + + // listen to main interface resize if ref is available, otherwise + // fall back to window resize. + const listenFor = containerRefs?.mainInterface?.current ?? window; + listenFor.addEventListener('resize', resizeThrottle); + return () => { + listenFor.removeEventListener('resize', resizeThrottle); + }; + }); + return size; +}; diff --git a/src/library/Hooks/useSubmitExtrinsic/index.tsx b/src/library/Hooks/useSubmitExtrinsic/index.tsx new file mode 100644 index 0000000000..70fabf3baa --- /dev/null +++ b/src/library/Hooks/useSubmitExtrinsic/index.tsx @@ -0,0 +1,307 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DappName, ManualSigners } from 'consts'; +import { useApi } from 'contexts/Api'; +import { useExtensions } from '@polkadot-cloud/react/hooks'; +import { useExtrinsics } from 'contexts/Extrinsics'; +import { useLedgerHardware } from 'contexts/Hardware/Ledger'; +import { useNotifications } from 'contexts/Notifications'; +import { useTxMeta } from 'contexts/TxMeta'; +import type { AnyApi, AnyJson } from 'types'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useBuildPayload } from '../useBuildPayload'; +import { useProxySupported } from '../useProxySupported'; +import type { UseSubmitExtrinsic, UseSubmitExtrinsicProps } from './types'; + +export const useSubmitExtrinsic = ({ + tx, + from, + shouldSubmit, + callbackSubmit, + callbackInBlock, +}: UseSubmitExtrinsicProps): UseSubmitExtrinsic => { + const { t } = useTranslation('library'); + const { api } = useApi(); + const { buildPayload } = useBuildPayload(); + const { activeProxy } = useActiveAccounts(); + const { extensionsStatus } = useExtensions(); + const { addNotification } = useNotifications(); + const { isProxySupported } = useProxySupported(); + const { addPending, removePending } = useExtrinsics(); + const { setIsExecuting, resetStatusCodes, resetFeedback } = + useLedgerHardware(); + const { getAccount, requiresManualSign } = useImportedAccounts(); + const { + txFees, + setTxFees, + setSender, + getTxPayload, + getTxSignature, + setTxSignature, + resetTxPayloads, + incrementPayloadUid, + } = useTxMeta(); + + // Store given tx as a ref. + const txRef = useRef(tx); + + // Store given submit address as a ref. + const fromRef = useRef(from || ''); + + // Store whether the transaction is in progress. + const [submitting, setSubmitting] = useState(false); + + // Store the uid of the extrinsic. + const [uid] = useState(incrementPayloadUid()); + + // Track for one-shot transaction reset after submission. + const didTxReset = useRef(false); + + // If proxy account is active, wrap tx in a proxy call and set the sender to the proxy account. + const wrapTxIfActiveProxy = () => { + // if already wrapped, update fromRef and return. + if ( + txRef.current?.method.toHuman().section === 'proxy' && + txRef.current?.method.toHuman().method === 'proxy' + ) { + if (activeProxy) { + fromRef.current = activeProxy; + } + return; + } + + if ( + api && + activeProxy && + txRef.current && + isProxySupported(txRef.current, fromRef.current) + ) { + // update submit address to active proxy account. + fromRef.current = activeProxy; + + // Do not wrap batch transactions. Proxy calls should already be wrapping each tx within the + // batch via `useBatchCall`. + if ( + txRef.current?.method.toHuman().section === 'utility' && + txRef.current?.method.toHuman().method === 'batch' + ) { + return; + } + + // Not a batch transaction: wrap tx in proxy call. + txRef.current = api.tx.proxy.proxy( + { + id: from, + }, + null, + txRef.current + ); + } + }; + + // Calculate the estimated tx fee of the transaction. + const calculateEstimatedFee = async () => { + if (txRef.current === null) { + return; + } + // get payment info + const { partialFee } = await txRef.current.paymentInfo(fromRef.current); + const partialFeeBn = new BigNumber(partialFee.toString()); + + // give tx fees to global useTxMeta context + if (partialFeeBn.toString() !== txFees.toString()) { + setTxFees(partialFeeBn); + } + }; + + // Extrinsic submission handler. + const onSubmit = async () => { + const account = getAccount(fromRef.current); + if ( + account === null || + submitting || + !shouldSubmit || + !api || + (requiresManualSign(fromRef.current) && !getTxSignature()) + ) { + return; + } + + const nonce = ( + await api.rpc.system.accountNextIndex(fromRef.current) + ).toHuman(); + + const { source } = account; + + // if `activeAccount` is imported from an extension, ensure it is enabled. + if (!ManualSigners.includes(source)) { + const isInstalled = Object.entries(extensionsStatus).find( + ([id, status]) => id === source && status === 'connected' + ); + + if (!isInstalled) throw new Error(`${t('walletNotFound')}`); + + if (!window?.injectedWeb3?.[source]) + throw new Error(`${t('walletNotFound')}`); + + // summons extension popup if not already connected. + window.injectedWeb3[source].enable(DappName); + } + + const onReady = () => { + addPending(nonce); + addNotification({ + title: t('pending'), + subtitle: t('transactionInitiated'), + }); + callbackSubmit(); + }; + + const onInBlock = () => { + setSubmitting(false); + removePending(nonce); + addNotification({ + title: t('inBlock'), + subtitle: t('transactionInBlock'), + }); + callbackInBlock(); + }; + + const onFinalizedEvent = (method: string) => { + if (method === 'ExtrinsicSuccess') { + addNotification({ + title: t('finalized'), + subtitle: t('transactionSuccessful'), + }); + } else if (method === 'ExtrinsicFailed') { + addNotification({ + title: t('failed'), + subtitle: t('errorWithTransaction'), + }); + setSubmitting(false); + removePending(nonce); + } + }; + + const resetTx = () => { + resetTxPayloads(); + setTxSignature(null); + setSubmitting(false); + }; + + const resetLedgerTx = () => { + setIsExecuting(false); + resetStatusCodes(); + resetFeedback(); + }; + const resetManualTx = () => { + resetTx(); + resetLedgerTx(); + }; + + const onError = (type?: string) => { + resetTx(); + if (type === 'ledger') { + resetLedgerTx(); + } + removePending(nonce); + addNotification({ + title: t('cancelled'), + subtitle: t('transactionCancelled'), + }); + }; + + const handleStatus = (status: AnyApi) => { + if (status.isReady) onReady(); + if (status.isInBlock) onInBlock(); + }; + + const unsubEvents = ['ExtrinsicSuccess', 'ExtrinsicFailed']; + + // pre-submission state update + setSubmitting(true); + + const txPayload: AnyJson = getTxPayload(); + const txSignature: AnyJson = getTxSignature(); + + // handle signed transaction. + if (getTxSignature()) { + try { + txRef.current.addSignature(fromRef.current, txSignature, txPayload); + + const unsub = await txRef.current.send( + ({ status, events = [] }: AnyApi) => { + if (!didTxReset.current) { + didTxReset.current = true; + resetManualTx(); + } + + handleStatus(status); + if (status.isFinalized) { + events.forEach(({ event: { method } }: AnyApi) => { + onFinalizedEvent(method); + if (unsubEvents?.includes(method)) unsub(); + }); + } + } + ); + } catch (e) { + onError(ManualSigners.includes(source) ? source : 'default'); + } + } else { + // handle unsigned transaction. + const { signer } = account; + try { + const unsub = await txRef.current.signAndSend( + fromRef.current, + { signer }, + ({ status, events = [] }: AnyApi) => { + if (!didTxReset.current) { + didTxReset.current = true; + resetTx(); + } + + handleStatus(status); + if (status.isFinalized) { + events.forEach(({ event: { method } }: AnyApi) => { + onFinalizedEvent(method); + if (unsubEvents?.includes(method)) unsub(); + }); + } + } + ); + } catch (e) { + onError('default'); + } + } + }; + + // Refresh state upon `tx` updates. + useEffect(() => { + // update txRef to latest tx. + txRef.current = tx; + // update submit address to latest from. + fromRef.current = from || ''; + // wrap tx in proxy call if active proxy & proxy supported. + wrapTxIfActiveProxy(); + // ensure sender is up to date. + setSender(fromRef.current); + // re-calculate estimated tx fee. + calculateEstimatedFee(); + // rebuild tx payload. + buildPayload(txRef.current, fromRef.current, uid); + }, [tx?.toString(), tx?.method?.args?.calls?.toString(), from]); + + return { + uid, + onSubmit, + submitting, + submitAddress: fromRef.current, + proxySupported: isProxySupported(txRef.current, fromRef.current), + }; +}; diff --git a/src/library/Hooks/useSubmitExtrinsic/types.ts b/src/library/Hooks/useSubmitExtrinsic/types.ts new file mode 100644 index 0000000000..9b281cf9d8 --- /dev/null +++ b/src/library/Hooks/useSubmitExtrinsic/types.ts @@ -0,0 +1,20 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyApi, MaybeAddress } from 'types'; + +export interface UseSubmitExtrinsicProps { + tx: AnyApi; + shouldSubmit: boolean; + callbackSubmit: { (): void }; + callbackInBlock: { (): void }; + from: MaybeAddress; +} + +export interface UseSubmitExtrinsic { + uid: number; + onSubmit: { (): void }; + submitting: boolean; + proxySupported: boolean; + submitAddress: MaybeAddress; +} diff --git a/src/library/Hooks/useTimeLeft/defaults.ts b/src/library/Hooks/useTimeLeft/defaults.ts new file mode 100644 index 0000000000..3175e6e05d --- /dev/null +++ b/src/library/Hooks/useTimeLeft/defaults.ts @@ -0,0 +1,14 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { TimeleftDuration } from './types'; + +export const defaultDuration: TimeleftDuration = { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + lastMinute: false, +}; + +export const defaultRefreshInterval = 60; diff --git a/src/library/Hooks/useTimeLeft/index.tsx b/src/library/Hooks/useTimeLeft/index.tsx new file mode 100644 index 0000000000..31731db674 --- /dev/null +++ b/src/library/Hooks/useTimeLeft/index.tsx @@ -0,0 +1,134 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNetwork } from 'contexts/Network'; +import type { + TimeLeftAll, + TimeLeftFormatted, + TimeLeftRaw, + TimeleftDuration, +} from './types'; +import { getDuration } from './utils'; + +export const useTimeLeft = () => { + const { network } = useNetwork(); + const { t, i18n } = useTranslation(); + + // check whether timeleft is within a minute of finishing. + const inLastHour = () => { + const { days, hours } = getDuration(toRef.current); + return !days && !hours; + }; + + // get the amount of seconds left if timeleft is in the last minute. + const lastMinuteCountdown = () => { + const { seconds } = getDuration(toRef.current); + if (!inLastHour()) { + return 60; + } + return seconds; + }; + + // calculate resulting timeleft object from latest duration. + const getTimeleft = (c?: TimeleftDuration): TimeLeftAll => { + const { days, hours, minutes, seconds } = c || getDuration(toRef.current); + + const raw: TimeLeftRaw = { + days, + hours, + minutes, + }; + const formatted: TimeLeftFormatted = { + days: [days, t('time.day', { count: days, ns: 'base' })], + hours: [hours, t('time.hr', { count: hours, ns: 'base' })], + minutes: [minutes, t('time.min', { count: minutes, ns: 'base' })], + }; + if (!days && !hours) { + formatted.seconds = [ + seconds, + t('time.second', { count: seconds, ns: 'base' }), + ]; + raw.seconds = seconds; + } + + return { + raw, + formatted, + }; + }; + + // the end time as a date. + const [to, setTo] = useState(null); + const toRef = useRef(to); + + // resulting timeleft object to be returned. + const [timeleft, setTimeleft] = useState(getTimeleft()); + + // timeleft refresh intervals. + const [minInterval, setMinInterval] = useState< + ReturnType | undefined + >(undefined); + const minIntervalRef = useRef(minInterval); + + const [secInterval, setSecInterval] = useState< + ReturnType | undefined + >(undefined); + const secIntervalRef = useRef(secInterval); + + // refresh effects. + useEffect(() => { + setTimeleft(getTimeleft()); + if (inLastHour()) { + // refresh timeleft every second. + if (!secIntervalRef.current) { + const interval = setInterval(() => { + if (!inLastHour()) { + clearInterval(secIntervalRef.current); + setStateWithRef(undefined, setSecInterval, secIntervalRef); + } + setTimeleft(getTimeleft()); + }, 1000); + + setStateWithRef(interval, setSecInterval, secIntervalRef); + } + } + // refresh timeleft every minute. + else if (!minIntervalRef.current) { + const interval = setInterval(() => { + if (inLastHour()) { + clearInterval(minIntervalRef.current); + setStateWithRef(undefined, setMinInterval, minIntervalRef); + } + setTimeleft(getTimeleft()); + }, 60000); + setStateWithRef(interval, setMinInterval, minIntervalRef); + } + }, [to, inLastHour(), lastMinuteCountdown(), network]); + + // re-render the timeleft upon langauge switch. + useEffect(() => { + setTimeleft(getTimeleft()); + }, [i18n.resolvedLanguage]); + + // clear intervals on unmount + useEffect( + () => () => { + clearInterval(minInterval); + clearInterval(secInterval); + }, + [] + ); + + const setFromNow = (dateFrom: Date, dateTo: Date) => { + setTimeleft(getTimeleft(getDuration(dateFrom))); + setStateWithRef(dateTo, setTo, toRef); + }; + + return { + setFromNow, + timeleft, + }; +}; diff --git a/src/library/Hooks/useTimeLeft/types.ts b/src/library/Hooks/useTimeLeft/types.ts new file mode 100644 index 0000000000..30bdd1c3fc --- /dev/null +++ b/src/library/Hooks/useTimeLeft/types.ts @@ -0,0 +1,32 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors + +export interface TimeleftDuration { + days: number; + hours: number; + minutes: number; + seconds: number; + lastMinute: boolean; +} + +export interface TimeLeftRaw { + days: number; + hours: number; + minutes: number; + seconds?: number; +} + +export interface TimeLeftFormatted { + days: [number, string]; + hours: [number, string]; + minutes: [number, string]; + seconds?: [number, string]; +} + +export interface TimeLeftAll { + raw: TimeLeftRaw; + formatted: TimeLeftFormatted; +} + +export interface TimeleftHookProps { + refreshInterval: number; +} diff --git a/src/library/Hooks/useTimeLeft/utils.ts b/src/library/Hooks/useTimeLeft/utils.ts new file mode 100644 index 0000000000..daf1e843ed --- /dev/null +++ b/src/library/Hooks/useTimeLeft/utils.ts @@ -0,0 +1,83 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + differenceInDays, + fromUnixTime, + getUnixTime, + intervalToDuration, +} from 'date-fns'; +import type { AnyFunction } from 'types'; +import { defaultDuration } from './defaults'; +import type { TimeleftDuration } from './types'; + +// adds `seconds` to the current time and returns the resulting date. +export const fromNow = (seconds: number): Date => { + const end = new Date(); + end.setSeconds(end.getSeconds() + seconds); + return end; +}; + +// calculates the current timeleft duration. +export const getDuration = (toDate: Date | null): TimeleftDuration => { + if (!toDate) { + return defaultDuration; + } + if (getUnixTime(toDate) <= getUnixTime(new Date())) { + return defaultDuration; + } + + toDate.setSeconds(toDate.getSeconds()); + const d = intervalToDuration({ + start: Date.now(), + end: toDate, + }); + + const days = differenceInDays(toDate, Date.now()); + const hours = d?.hours || 0; + const minutes = d?.minutes || 0; + const seconds = d?.seconds || 0; + const lastHour = days === 0 && hours === 0; + const lastMinute = lastHour && minutes === 0; + + return { + days, + hours, + minutes, + seconds, + lastMinute, + }; +}; + +// format the duration (from seconds) as a string. +export const timeleftAsString = ( + t: AnyFunction, + start: number, + duration: number, + full?: boolean +) => { + const { days, hours, minutes, seconds } = getDuration( + fromUnixTime(start + duration) || null + ); + + const tHour = `time.${full ? `hour` : `hr`}`; + const tMinute = `time.${full ? `minute` : `min`}`; + + let str = ''; + if (days > 0) { + str += `${days} ${t('time.day', { count: days, ns: 'base' })}`; + } + if (hours > 0) { + if (str) str += ', '; + str += ` ${hours} ${t(tHour, { count: hours, ns: 'base' })}`; + } + if (minutes > 0) { + if (str) str += ', '; + str += ` ${minutes} ${t(tMinute, { count: minutes, ns: 'base' })}`; + } + if (!days && !hours) { + if (str) str += ', '; + str += ` ${seconds}`; + } + return str; +}; diff --git a/src/library/Hooks/useUnitPrice/index.tsx b/src/library/Hooks/useUnitPrice/index.tsx new file mode 100644 index 0000000000..e7e4c1a98f --- /dev/null +++ b/src/library/Hooks/useUnitPrice/index.tsx @@ -0,0 +1,41 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { NetworkList } from 'config/networks'; +import { ApiEndpoints } from 'consts'; +import { useNetwork } from 'contexts/Network'; + +export const useUnitPrice = () => { + const { network } = useNetwork(); + + const fetchUnitPrice = async () => { + const urls = [ + `${ApiEndpoints.priceChange}${NetworkList[network].api.priceTicker}`, + ]; + + const responses = await Promise.all( + urls.map((u) => fetch(u, { method: 'GET' })) + ); + const texts = await Promise.all(responses.map((res) => res.json())); + const newPrice = texts[0]; + + if ( + newPrice.lastPrice !== undefined && + newPrice.priceChangePercent !== undefined + ) { + const price: string = (Math.ceil(newPrice.lastPrice * 100) / 100).toFixed( + 2 + ); + + return { + lastPrice: price, + change: (Math.round(newPrice.priceChangePercent * 100) / 100).toFixed( + 2 + ), + }; + } + return null; + }; + + return fetchUnitPrice; +}; diff --git a/src/library/Hooks/useUnstaking/index.tsx b/src/library/Hooks/useUnstaking/index.tsx new file mode 100644 index 0000000000..8e0acffe41 --- /dev/null +++ b/src/library/Hooks/useUnstaking/index.tsx @@ -0,0 +1,63 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useFastUnstake } from 'contexts/FastUnstake'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useStaking } from 'contexts/Staking'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import type { AnyJson } from 'types'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useNominationStatus } from '../useNominationStatus'; + +export const useUnstaking = () => { + const { t } = useTranslation('library'); + const { consts } = useApi(); + const { inSetup } = useStaking(); + const { activeAccount } = useActiveAccounts(); + const { activeEra } = useNetworkMetrics(); + const { getTransferOptions } = useTransferOptions(); + const { getNominationStatus } = useNominationStatus(); + const { checking, head, isExposed, queueDeposit, meta } = useFastUnstake(); + const { bondDuration } = consts; + const transferOptions = getTransferOptions(activeAccount).nominate; + const { nominees } = getNominationStatus(activeAccount, 'nominator'); + + // determine if user is regular unstaking + const { active } = transferOptions; + + // determine if user is fast unstaking. + const inHead = + head?.stashes.find((s: AnyJson) => s[0] === activeAccount) ?? undefined; + const inQueue = queueDeposit?.isGreaterThan(0) ?? false; + + const registered = inHead || inQueue; + + // determine unstake button + const getFastUnstakeText = () => { + const { checked } = meta; + if (checking) { + return `${t('fastUnstakeCheckingEras', { + checked: checked.length, + total: bondDuration.toString(), + })}...`; + } + if (isExposed) { + const lastExposed = activeEra.index.minus(checked[0] || 0); + return t('fastUnstakeExposed', { + count: lastExposed.toNumber(), + }); + } + if (registered) { + return t('inQueue'); + } + return t('fastUnstake'); + }; + + return { + getFastUnstakeText, + isUnstaking: !inSetup() && !nominees.active.length && active.isZero(), + isFastUnstaking: !!registered, + }; +}; diff --git a/src/library/Hooks/useValidatorFilters/index.tsx b/src/library/Hooks/useValidatorFilters/index.tsx new file mode 100644 index 0000000000..6cb47d1e27 --- /dev/null +++ b/src/library/Hooks/useValidatorFilters/index.tsx @@ -0,0 +1,269 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { u8aToString, u8aUnwrapBytes } from '@polkadot/util'; +import { useTranslation } from 'react-i18next'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import type { AnyFunction, AnyJson } from 'types'; +import { useStaking } from 'contexts/Staking'; +import { MaxEraRewardPointsEras } from 'consts'; + +export const useValidatorFilters = () => { + const { t } = useTranslation('library'); + const { + sessionValidators, + sessionParaValidators, + validatorIdentities, + validatorSupers, + validatorEraPointsHistory, + } = useValidators(); + const { erasStakersSyncing, getLowestRewardFromStaker } = useStaking(); + + /* + * filterMissingIdentity: Iterates through the supplied list and filters those with missing + * identities. Returns the updated filtered list. + */ + const filterMissingIdentity = (list: any) => { + // Return lsit early if identity sync has not completed. + if ( + !Object.values(validatorIdentities).length || + !Object.values(validatorSupers).length + ) { + return list; + } + const filteredList: any = []; + for (const validator of list) { + const identityExists = validatorIdentities[validator.address] ?? false; + const superExists = validatorSupers[validator.address] ?? false; + + // Validator included if identity or super identity has been set. + if (!!identityExists || !!superExists) { + filteredList.push(validator); + continue; + } + } + return filteredList; + }; + + /* + * filterOverSubscribed: Iterates through the supplied list and filters those who are over + * subscribed. Returns the updated filtered list. + */ + const filterOverSubscribed = (list: any) => { + // Return list early if eraStakers is still syncing. + if (erasStakersSyncing) { + return list; + } + + const filteredList: any = []; + for (const validator of list) { + const { oversubscribed } = getLowestRewardFromStaker(validator.address); + + if (!oversubscribed) { + filteredList.push(validator); + continue; + } + } + return filteredList; + }; + + /* + * filterAllCommission: Filters the supplied list and removes items with 100% commission. Returns + * the updated filtered list. + */ + const filterAllCommission = (list: any) => + list.filter((validator: any) => validator?.prefs?.commission !== 100); + + /* + * filterBlockedNominations: Filters the supplied list and removes items that have blocked + * nominations. Returns the updated filtered list. + */ + const filterBlockedNominations = (list: any) => + list.filter((validator: any) => validator?.prefs?.blocked !== true); + + /* + * filterActive: Filters the supplied list and removes items that are inactive. Returns the + * updated filtered list. + */ + const filterActive = (list: any) => { + // if list has not yet been populated, return original list + if (sessionValidators.length === 0) return list; + return list.filter((validator: any) => + sessionValidators.includes(validator.address) + ); + }; + + /* + * filterNonParachainValidator: Filters the supplied list and removes items that are inactive. + * Returns the updated filtered list. + */ + const filterNonParachainValidator = (list: any) => { + // if list has not yet been populated, return original list + if ((sessionParaValidators?.length ?? 0) === 0) return list; + return list.filter((validator: any) => + sessionParaValidators.includes(validator.address) + ); + }; + + /* + * filterInSession: Filters the supplied list and removes items that are in the current session. + * Returns the updated filtered list. + */ + const filterInSession = (list: any) => { + // if list has not yet been populated, return original list + if (sessionValidators.length === 0) return list; + return list.filter( + (validator: any) => !sessionValidators.includes(validator.address) + ); + }; + + const includesToLabels: Record = { + active: t('activeValidators'), + }; + + const excludesToLabels: Record = { + over_subscribed: t('overSubscribed'), + all_commission: t('100Commission'), + blocked_nominations: t('blockedNominations'), + missing_identity: t('missingIdentity'), + }; + + const filterToFunction: Record = { + active: filterActive, + missing_identity: filterMissingIdentity, + over_subscribed: filterOverSubscribed, + all_commission: filterAllCommission, + blocked_nominations: filterBlockedNominations, + not_parachain_validator: filterNonParachainValidator, + in_session: filterInSession, + }; + + const getFiltersToApply = (excludes: string[]) => { + const fns = []; + for (const exclude of excludes) { + if (filterToFunction[exclude]) { + fns.push(filterToFunction[exclude]); + } + } + return fns; + }; + + const applyFilter = ( + includes: string[] | null, + excludes: string[] | null, + list: AnyJson + ) => { + if (!excludes && !includes) { + return list; + } + if (includes) { + for (const fn of getFiltersToApply(includes)) { + list = fn(list); + } + } + if (excludes) { + for (const fn of getFiltersToApply(excludes)) { + list = fn(list); + } + } + return list; + }; + + /* + * orderLowestCommission: Orders a list by commission, lowest first. Returns the updated ordered + * list. + */ + const orderLowestCommission = (list: any) => + [...list].sort((a, b) => a.prefs.commission - b.prefs.commission); + + /* + * orderHighestCommission: Orders a list by commission, highest first. Returns the updated ordered + * list. + */ + const orderHighestCommission = (list: any) => + [...list].sort((a, b) => b.prefs.commission - a.prefs.commission); + + /* + * orderByRank: Orders a list by validator rank. + */ + const orderByRank = (list: any) => + [...list].sort((a, b) => { + const aRank = validatorEraPointsHistory[a.address]?.rank || 9999; + const bRank = validatorEraPointsHistory[b.address]?.rank || 9999; + return aRank - bRank; + }); + + const ordersToLabels: Record = { + rank: `${MaxEraRewardPointsEras} ${t('dayPerformance')}`, + low_commission: t('lowCommission'), + high_commission: t('highCommission'), + default: t('unordered'), + }; + + const orderToFunction: Record = { + rank: orderByRank, + low_commission: orderLowestCommission, + high_commission: orderHighestCommission, + }; + + const applyOrder = (o: string, list: AnyJson) => { + const fn = orderToFunction[o]; + if (fn) { + return fn(list); + } + return list; + }; + + /* + * applySearch Iterates through the supplied list and filters those that match the search term. + * Returns the updated filtered list. + */ + const applySearch = (list: any, searchTerm: string) => { + // If we cannot derive data, fallback to include validator in filtered list. + if ( + !searchTerm || + !Object.values(validatorIdentities).length || + !Object.values(validatorSupers).length + ) { + return list; + } + + const filteredList: any = []; + for (const validator of list) { + const identity = validatorIdentities[validator.address] ?? ''; + const identityRaw = identity?.info?.display?.Raw ?? ''; + const identityAsBytes = u8aToString(u8aUnwrapBytes(identityRaw)); + const identitySearch = ( + identityAsBytes === '' ? identityRaw : identityAsBytes + ).toLowerCase(); + + const superIdentity = validatorSupers[validator.address] ?? null; + const superIdentityRaw = + superIdentity?.identity?.info?.display?.Raw ?? ''; + const superIdentityAsBytes = u8aToString( + u8aUnwrapBytes(superIdentityRaw) + ); + const superIdentitySearch = ( + superIdentityAsBytes === '' ? superIdentityRaw : superIdentityAsBytes + ).toLowerCase(); + + if (validator.address.toLowerCase().includes(searchTerm.toLowerCase())) + filteredList.push(validator); + if ( + identitySearch.includes(searchTerm.toLowerCase()) || + superIdentitySearch.includes(searchTerm.toLowerCase()) + ) + filteredList.push(validator); + } + return filteredList; + }; + + return { + includesToLabels, + excludesToLabels, + ordersToLabels, + applyFilter, + applyOrder, + applySearch, + }; +}; diff --git a/src/library/Import/Confirm.tsx b/src/library/Import/Confirm.tsx new file mode 100644 index 0000000000..de1ee528b5 --- /dev/null +++ b/src/library/Import/Confirm.tsx @@ -0,0 +1,37 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonMono, ButtonMonoInvert, Polkicon } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { usePrompt } from 'contexts/Prompt'; + +import { ConfirmWrapper } from 'library/Import/Wrappers'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; +import type { ConfirmProps } from './types'; + +export const Confirm = ({ address, index, addHandler }: ConfirmProps) => { + const { t } = useTranslation('modals'); + const { setStatus } = usePrompt(); + const { addOtherAccounts } = useOtherAccounts(); + + return ( + + +

{t('importAccount')}

+
{address}
+
+ setStatus(0)} /> + { + const account = addHandler(address, index); + if (account) { + addOtherAccounts([account]); + } + setStatus(0); + }} + /> +
+
+ ); +}; diff --git a/src/library/Import/Heading.tsx b/src/library/Import/Heading.tsx new file mode 100644 index 0000000000..4ef196a0e0 --- /dev/null +++ b/src/library/Import/Heading.tsx @@ -0,0 +1,56 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faChevronRight, + faCircleMinus, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ButtonText } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { HeadingWrapper } from './Wrappers'; +import type { HeadingProps } from './types'; + +export const Heading = ({ + connectTo, + title, + Icon, + disabled, + handleReset, +}: HeadingProps) => { + const { t } = useTranslation('library'); + + return ( + +
+

+ {Icon && } + + {connectTo && ( + <> + {connectTo}{' '} + + + )} + {title} + +

+
+
+ {handleReset && ( + { + if (typeof handleReset === 'function') { + handleReset(); + } + }} + disabled={disabled || false} + marginLeft + /> + )} +
+
+ ); +}; diff --git a/src/library/Import/NoAccounts.tsx b/src/library/Import/NoAccounts.tsx new file mode 100644 index 0000000000..dce67c8bc5 --- /dev/null +++ b/src/library/Import/NoAccounts.tsx @@ -0,0 +1,35 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { ButtonSecondary } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { NoAccountsWrapper } from './Wrappers'; + +export const NoAccounts = ({ children, text, Icon }: any) => { + const { t } = useTranslation('modals'); + const { replaceModal } = useOverlay().modal; + + return ( + <> +
+

+ + replaceModal({ key: 'Connect', options: { disableScroll: true } }) + } + /> +

+
+ + +

{text}

+ {children} +
+ + ); +}; diff --git a/src/library/Import/Remove.tsx b/src/library/Import/Remove.tsx new file mode 100644 index 0000000000..9728378249 --- /dev/null +++ b/src/library/Import/Remove.tsx @@ -0,0 +1,37 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonMono, ButtonMonoInvert, Polkicon } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { usePrompt } from 'contexts/Prompt'; +import { ConfirmWrapper } from 'library/Import/Wrappers'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; +import type { RemoveProps } from './types'; + +export const Remove = ({ address, getHandler, removeHandler }: RemoveProps) => { + const { t } = useTranslation('modals'); + const { setStatus } = usePrompt(); + const { forgetOtherAccounts } = useOtherAccounts(); + + return ( + + +

{t('removeAccount')}

+
{address}
+
+ setStatus(0)} /> + { + const account = getHandler(address); + if (account) { + removeHandler(address); + forgetOtherAccounts([account]); + setStatus(0); + } + }} + /> +
+
+ ); +}; diff --git a/src/library/Import/Wrappers.ts b/src/library/Import/Wrappers.ts new file mode 100644 index 0000000000..6345bce356 --- /dev/null +++ b/src/library/Import/Wrappers.ts @@ -0,0 +1,182 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const HeadingWrapper = styled.div` + position: sticky; + width: 100%; + top: 0px; + padding: 1.25rem 1.25rem 0.5rem 1.25rem; + display: flex; + z-index: 3; + + > section { + flex: 1; + + &:first-child { + display: flex; + flex-grow: 1; + > h4 { + font-family: InterSemiBold, sans-serif; + padding: 0; + display: flex; + align-items: center; + > span { + color: var(--text-color-primary); + margin-right: 0.5rem; + > svg { + margin: 0 0.7rem 0 0.2rem; + } + } + > svg { + width: 1.1rem; + height: 1.1rem; + margin-right: 0.6rem; + path { + fill: var(--text-color-primary); + } + } + } + } + &:last-child { + display: flex; + justify-content: flex-end; + } + } +`; + +export const AddressesWrapper = styled.div` + --address-item-height: 7rem; + + box-sizing: content-box; + overflow: auto; + height: auto; + display: flex; + flex-direction: column; + padding: 0rem 0rem 7rem 0rem; + + .items { + display: flex; + flex-direction: column; + padding: 0 1rem; + } + + .more { + margin-top: 1rem; + padding: 0 1.5rem; + h4 { + opacity: var(--opacity-disabled); + padding: 0; + } + } +`; + +export const ConfirmWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + padding: 1.5rem 2.5rem; + + h3, + h5, + p { + text-align: center; + } + h3 { + margin: 1.25rem 0 0.5rem 0; + } + h5 { + margin: 0.25rem 0; + } + .footer { + display: flex; + margin-top: 1rem; + + > button { + margin-right: 1rem; + &:last-child { + margin-right: 0; + } + } + } +`; + +export const QRViewerWrapper = styled.div` + width: 100%; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + padding: 2rem 1rem; + + .title { + color: var(--accent-color-primary); + font-family: 'Unbounded'; + margin-bottom: 1rem; + } + + .progress { + margin-bottom: 1rem; + border-radius: 1rem; + background: var(--background-menu); + padding: 0.45rem 1.5rem 0.75rem 1.5rem; + + span { + opacity: 0.4; + &.active { + opacity: 1; + } + } + .arrow { + margin: 0 0.85rem; + } + } + + .viewer { + border-radius: 1.25rem; + display: flex; + justify-content: center; + align-items: center; + position: relative; + overflow: hidden; + + &.withBorder { + padding: 0.95rem; + border: 3.75px solid var(--accent-color-pending); + } + } + .foot { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 1.75rem; + padding: 0 1rem; + width: 100%; + + > div { + display: flex; + justify-content: center; + margin-top: 1rem; + width: 100%; + } + } +`; + +export const NoAccountsWrapper = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem 0 3rem 0; + + .icon { + width: 6rem; + height: 6rem; + margin-bottom: 1rem; + } + + h3 { + margin-bottom: 1rem; + } +`; diff --git a/src/library/Import/types.ts b/src/library/Import/types.ts new file mode 100644 index 0000000000..ef4f3b8558 --- /dev/null +++ b/src/library/Import/types.ts @@ -0,0 +1,37 @@ +import type { FunctionComponent, SVGProps } from 'react'; +import type { AnyFunction } from 'types'; + +export interface HeadingProps { + connectTo?: string; + disabled?: boolean; + handleReset?: () => void; + Icon?: FunctionComponent>; + title: string; +} + +export interface AddressProps { + address: string; + index: number; + initial: string; + disableEditIfImported?: boolean; + renameHandler: AnyFunction; + existsHandler: AnyFunction; + openRemoveHandler: AnyFunction; + openConfirmHandler: AnyFunction; + t: { + tImport: string; + tRemove: string; + }; +} + +export interface ConfirmProps { + address: string; + index: number; + addHandler: AnyFunction; +} + +export interface RemoveProps { + address: string; + getHandler: AnyFunction; + removeHandler: AnyFunction; +} diff --git a/src/library/List/MotionContainer.tsx b/src/library/List/MotionContainer.tsx new file mode 100644 index 0000000000..a6facfc9be --- /dev/null +++ b/src/library/List/MotionContainer.tsx @@ -0,0 +1,29 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import React from 'react'; + +export const MotionContainer = ({ + children, + staggerChildren = 0.015, +}: { + staggerChildren?: number; + children: React.ReactNode; +}) => ( + + {children} + +); diff --git a/src/library/List/Pagination.tsx b/src/library/List/Pagination.tsx new file mode 100644 index 0000000000..64075eb952 --- /dev/null +++ b/src/library/List/Pagination.tsx @@ -0,0 +1,46 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PaginationWrapper } from '.'; +import type { PaginationProps } from './types'; + +export const Pagination = ({ page, total, setter }: PaginationProps) => { + const { t } = useTranslation('library'); + const [next, setNext] = useState(page + 1 > total ? total : page + 1); + const [prev, setPrev] = useState(page - 1 < 1 ? 1 : page - 1); + + useEffect(() => { + setNext(page + 1 > total ? total : page + 1); + setPrev(page - 1 < 1 ? 1 : page - 1); + }, [page, total]); + + return ( + +
+

{t('page', { page, total })}

+
+
+ + +
+
+ ); +}; diff --git a/src/library/List/SearchInput.tsx b/src/library/List/SearchInput.tsx new file mode 100644 index 0000000000..77c3969b30 --- /dev/null +++ b/src/library/List/SearchInput.tsx @@ -0,0 +1,20 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React from 'react'; +import { SearchInputWrapper } from '.'; +import type { SearchInputProps } from './types'; + +export const SearchInput = ({ + handleChange, + placeholder, +}: SearchInputProps) => ( + + ) => handleChange(e)} + /> + +); diff --git a/src/library/List/Selectable.tsx b/src/library/List/Selectable.tsx new file mode 100644 index 0000000000..a96ad58358 --- /dev/null +++ b/src/library/List/Selectable.tsx @@ -0,0 +1,68 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { ButtonMonoInvert } from '@polkadot-cloud/react'; +import { SelectableWrapper } from '.'; +import { useList } from './context'; +import type { SelectableProps } from './types'; + +export const Selectable = ({ + actionsAll, + actionsSelected, + canSelect, + displayFor, +}: SelectableProps) => { + const { t } = useTranslation('library'); + const provider = useList(); + const { isFastUnstaking } = useUnstaking(); + + // Get list provider props. + const { selectActive, setSelectActive, selected, selectToggleable } = + provider; + + // Determine button style depending on in canvas. Same for now, may change as design evolves. + const ButtonType = + displayFor === 'canvas' ? ButtonMonoInvert : ButtonMonoInvert; + + return ( + + {selectToggleable === true ? ( + { + setSelectActive(!selectActive); + }} + marginRight + /> + ) : null} + {selected.length > 0 ? ( + <> + {actionsSelected.map((a: any, i: number) => ( + a.onClick(provider)} + marginRight + /> + ))} + + ) : null} + {actionsAll.map((a: any, i: number) => ( + a.onClick(provider)} + iconLeft={a.icon ? a.icon : undefined} + marginRight + /> + ))} + + ); +}; diff --git a/src/library/List/context.tsx b/src/library/List/context.tsx new file mode 100644 index 0000000000..d99922ec46 --- /dev/null +++ b/src/library/List/context.tsx @@ -0,0 +1,69 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useState } from 'react'; +import * as defaults from './defaults'; + +export const ListContext: React.Context = React.createContext( + defaults.defaultContext +); + +export const useList = () => React.useContext(ListContext); + +export const ListProvider = ({ + selectToggleable = true, + selectActive: initialSelctActive = false, + children, +}: any) => { + // store the currently selected validators from the list + const [selected, setSelected] = useState([]); + + // store whether validator selection is active + const [selectActive, setSelectActiveState] = useState( + initialSelctActive ?? false + ); + + // store the list format of the list + const [listFormat, _setListFormat] = useState('col'); + + const addToSelected = (_item: any) => { + setSelected([...selected].concat(_item)); + }; + + const removeFromSelected = (items: any[]) => { + setSelected([...selected].filter((item) => !items.includes(item))); + }; + + const resetSelected = () => { + setSelected([]); + }; + + const setSelectActive = (_selectActive: boolean) => { + setSelectActiveState(_selectActive); + if (_selectActive === false) { + resetSelected(); + } + }; + + const setListFormat = (v: string) => { + _setListFormat(v); + }; + + return ( + + {children} + + ); +}; diff --git a/src/library/List/defaults.ts b/src/library/List/defaults.ts new file mode 100644 index 0000000000..72bf30b512 --- /dev/null +++ b/src/library/List/defaults.ts @@ -0,0 +1,15 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +export const defaultContext = { + setSelectable: (_selectable: boolean) => {}, + addToSelected: (item: any) => {}, + removeFromSelected: (items: any[]) => {}, + resetSelected: () => {}, + setListFormat: (v: string) => {}, + selected: [], + selectable: false, + listFormat: 'col', + selectToggleable: true, +}; diff --git a/src/library/List/index.ts b/src/library/List/index.ts new file mode 100644 index 0000000000..cd888f458c --- /dev/null +++ b/src/library/List/index.ts @@ -0,0 +1,188 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import type { DisplayFor } from 'types'; +import type { ListProps, PaginationWrapperProps } from './types'; + +export const Wrapper = styled.div` + display: flex; + width: 100%; + flex-flow: column nowrap; +`; + +// NOTE: used for member lists and payout list only. +export const Header = styled.div<{ $displayFor?: DisplayFor }>` + border-bottom: ${(props) => + props.$displayFor === 'canvas' + ? '1px solid var(--border-secondary-color)' + : '1px solid var(--border-primary-color)'}; + + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + padding: 0 0.25rem 0.75rem 0.25rem; + flex: 1; + + h4 { + color: var(--text-color-secondary); + font-family: InterSemiBold, sans-serif; + } + + > div { + display: flex; + align-items: center; + } + + > div:last-child { + justify-content: flex-end; + flex: 1; + + button { + color: var(--text-color-secondary); + font-size: 1.1rem; + margin: 0 0.5rem 0 0.75rem; + opacity: 0.6; + transition: all var(--transition-duration); + + &:hover { + opacity: 0.9; + } + } + } +`; + +export const PaginationWrapper = styled.div` + width: 100%; + display: flex; + align-items: center; + padding: 0.75rem 0.5rem; + + > div:first-child { + display: flex; + flex: 1; + } + + > div:last-child { + display: flex; + justify-content: flex-end; + + button { + font-size: 0.98rem; + padding: 0 0.25rem; + margin-left: 0.5rem; + &.next { + color: ${(props) => + props.$next + ? 'var(--accent-color-primary)' + : 'var(--text-color-secondary)'}; + cursor: ${(props) => (props.$next ? 'pointer' : 'default')}; + opacity: ${(props) => (props.$next ? 1 : 0.4)}; + } + &.prev { + color: ${(props) => + props.$prev + ? 'var(--accent-color-primary)' + : 'var(--text-color-secondary)'}; + cursor: ${(props) => (props.$prev ? 'pointer' : 'default')}; + opacity: ${(props) => (props.$prev ? 1 : 0.4)}; + } + } + } +`; + +export const SelectableWrapper = styled.div` + width: 100%; + display: flex; + align-items: center; + padding: 0 0.15rem; + margin-top: 0.5rem; + + > button { + margin-bottom: 0.75rem; + } +`; + +export const ListStatusHeader = styled.h4` + padding: 0.25rem 0.5rem; +`; + +export const List = styled.div` + width: 100%; + + > div { + display: flex; + flex-wrap: wrap; + + > .item { + display: flex; + align-items: center; + overflow: hidden; + + &.row { + flex-basis: 100%; + } + + &.col { + flex-grow: 1; + flex-basis: 100%; + @media (min-width: 875px) { + flex-basis: 50%; + max-width: 50%; + } + @media (min-width: 1500px) { + flex-basis: ${(props) => props.$flexBasisLarge}; + max-width: ${(props) => props.$flexBasisLarge}; + } + } + } + } +`; + +export const SearchInputWrapper = styled.div` + display: flex; + flex-flow: row wrap; + margin: 0.5rem 0 2rem 0; + width: 100%; + + > input { + border: 1px solid var(--border-primary-color); + color: var(--text-color-secondary); + font-family: InterBold, sans-serif; + border-radius: 1.75rem; + padding: 0.9rem 1.25rem; + font-size: 1.15rem; + width: 100%; + } +`; + +export const FilterHeaderWrapper = styled.div` + display: flex; + align-items: center; + width: 100%; + padding: 0 0.25rem; + + > div { + display: flex; + + &:first-child { + flex-grow: 1; + flex-direction: column; + } + &:last-child { + flex-shrink: 1; + + button { + color: var(--text-color-secondary); + font-size: 1.1rem; + margin: 0 0.5rem 0 0.75rem; + opacity: 0.6; + transition: all var(--transition-duration); + + &:hover { + opacity: 0.9; + } + } + } + } +`; diff --git a/src/library/List/types.ts b/src/library/List/types.ts new file mode 100644 index 0000000000..7f36487d93 --- /dev/null +++ b/src/library/List/types.ts @@ -0,0 +1,33 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyJson } from '@polkadot-cloud/react/types'; +import type React from 'react'; +import type { DisplayFor } from 'types'; + +export interface PaginationWrapperProps { + $next: boolean; + $prev: boolean; +} + +export interface ListProps { + $flexBasisLarge: string; +} + +export interface PaginationProps { + page: number; + total: number; + setter: (p: number) => void; +} + +export interface SearchInputProps { + handleChange: (e: React.FormEvent) => void; + placeholder: string; +} + +export interface SelectableProps { + actionsAll: AnyJson[]; + actionsSelected: AnyJson[]; + canSelect: boolean; + displayFor: DisplayFor; +} diff --git a/src/library/ListItem/Labels/Blocked.tsx b/src/library/ListItem/Labels/Blocked.tsx new file mode 100644 index 0000000000..97d96ea768 --- /dev/null +++ b/src/library/ListItem/Labels/Blocked.tsx @@ -0,0 +1,38 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faUserSlash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useTooltip } from 'contexts/Tooltip'; +import { TooltipTrigger } from 'library/ListItem/Wrappers'; +import type { BlockedProps } from '../types'; + +export const Blocked = ({ prefs }: BlockedProps) => { + const { t } = useTranslation('library'); + const blocked = prefs?.blocked ?? null; + const { setTooltipTextAndOpen } = useTooltip(); + + const tooltipText = t('blockingNominations'); + + return ( + <> + {blocked && ( + <> +
+ setTooltipTextAndOpen(tooltipText)} + /> + +
+ + )} + + ); +}; diff --git a/src/library/ListItem/Labels/Commission.tsx b/src/library/ListItem/Labels/Commission.tsx new file mode 100644 index 0000000000..984d288613 --- /dev/null +++ b/src/library/ListItem/Labels/Commission.tsx @@ -0,0 +1,24 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { useTooltip } from 'contexts/Tooltip'; +import { TooltipTrigger } from 'library/ListItem/Wrappers'; + +export const Commission = ({ commission }: { commission: number }) => { + const { t } = useTranslation('library'); + const { setTooltipTextAndOpen } = useTooltip(); + + const tooltipText = t('validatorCommission'); + + return ( +
+ setTooltipTextAndOpen(tooltipText)} + /> + {commission}% +
+ ); +}; diff --git a/src/library/ListItem/Labels/CopyAddress.tsx b/src/library/ListItem/Labels/CopyAddress.tsx new file mode 100644 index 0000000000..06e148dda1 --- /dev/null +++ b/src/library/ListItem/Labels/CopyAddress.tsx @@ -0,0 +1,39 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCopy } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useNotifications } from 'contexts/Notifications'; +import type { NotificationText } from 'contexts/Notifications/types'; +import type { CopyAddressProps } from '../types'; + +export const CopyAddress = ({ address }: CopyAddressProps) => { + const { t } = useTranslation('library'); + const { addNotification } = useNotifications(); + + // copy address notification + const notificationCopyAddress: NotificationText | null = + address == null + ? null + : { + title: t('addressCopiedToClipboard'), + subtitle: address, + }; + + return ( +
+ +
+ ); +}; diff --git a/src/library/ListItem/Labels/EraStatus.tsx b/src/library/ListItem/Labels/EraStatus.tsx new file mode 100644 index 0000000000..13c84374f6 --- /dev/null +++ b/src/library/ListItem/Labels/EraStatus.tsx @@ -0,0 +1,34 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { capitalizeFirstLetter, planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { ValidatorStatusWrapper } from 'library/ListItem/Wrappers'; +import { useNetwork } from 'contexts/Network'; +import type { EraStatusProps } from '../types'; + +export const EraStatus = ({ noMargin, status, totalStake }: EraStatusProps) => { + const { t } = useTranslation('library'); + const { isSyncing } = useUi(); + const { erasStakersSyncing } = useStaking(); + const { unit, units } = useNetwork().networkData; + + // Fallback to `waiting` status if still syncing. + const validatorStatus = isSyncing ? 'waiting' : status; + + return ( + +
+ {isSyncing || erasStakersSyncing + ? t('syncing') + : validatorStatus !== 'waiting' + ? `${t('listItemActive')} / ${planckToUnit(totalStake, units) + .integerValue() + .toFormat()} ${unit}` + : capitalizeFirstLetter(t(`${validatorStatus}`) ?? '')} +
+
+ ); +}; diff --git a/src/library/ListItem/Labels/FavoritePool.tsx b/src/library/ListItem/Labels/FavoritePool.tsx new file mode 100644 index 0000000000..3ce62dbd63 --- /dev/null +++ b/src/library/ListItem/Labels/FavoritePool.tsx @@ -0,0 +1,59 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faHeart as faHeartRegular } from '@fortawesome/free-regular-svg-icons'; +import { faHeart } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useNotifications } from 'contexts/Notifications'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { useTooltip } from 'contexts/Tooltip'; +import { TooltipTrigger } from 'library/ListItem/Wrappers'; +import type { FavoriteProps } from '../types'; + +export const FavoritePool = ({ address }: FavoriteProps) => { + const { t } = useTranslation('library'); + const { addNotification } = useNotifications(); + const { setTooltipTextAndOpen } = useTooltip(); + const { favorites, addFavorite, removeFavorite } = usePoolsConfig(); + + const isFavorite = favorites.includes(address); + + const notificationFavorite = !isFavorite + ? { + title: t('favoritePoolAdded'), + subtitle: address, + } + : { + title: t('favoritePoolRemoved'), + subtitle: address, + }; + + const tooltipText = `${isFavorite ? `${t('remove')}` : `${t('add')}`} ${t( + 'favorite' + )}`; + + return ( +
+ setTooltipTextAndOpen(tooltipText)} + onClick={() => { + if (isFavorite) { + removeFavorite(address); + } else { + addFavorite(address); + } + addNotification(notificationFavorite); + }} + /> + +
+ ); +}; diff --git a/src/library/ListItem/Labels/FavoriteValidator.tsx b/src/library/ListItem/Labels/FavoriteValidator.tsx new file mode 100644 index 0000000000..043c2bf91c --- /dev/null +++ b/src/library/ListItem/Labels/FavoriteValidator.tsx @@ -0,0 +1,59 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faHeart as faHeartRegular } from '@fortawesome/free-regular-svg-icons'; +import { faHeart } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useNotifications } from 'contexts/Notifications'; +import { useTooltip } from 'contexts/Tooltip'; +import { TooltipTrigger } from 'library/ListItem/Wrappers'; +import { useFavoriteValidators } from 'contexts/Validators/FavoriteValidators'; +import type { FavoriteProps } from '../types'; + +export const FavoriteValidator = ({ address }: FavoriteProps) => { + const { t } = useTranslation('library'); + const { setTooltipTextAndOpen } = useTooltip(); + const { addNotification } = useNotifications(); + const { favorites, addFavorite, removeFavorite } = useFavoriteValidators(); + + const isFavorite = favorites.includes(address); + + const notificationFavorite = !isFavorite + ? { + title: t('favoriteValidatorAdded'), + subtitle: address, + } + : { + title: t('favoriteValidatorRemoved'), + subtitle: address, + }; + + const tooltipText = `${isFavorite ? `${t('remove')}` : `${t('add')}`} ${t( + 'favorite' + )}`; + + return ( +
+ setTooltipTextAndOpen(tooltipText)} + onClick={() => { + if (isFavorite) { + removeFavorite(address); + } else { + addFavorite(address); + } + addNotification(notificationFavorite); + }} + /> + +
+ ); +}; diff --git a/src/library/ListItem/Labels/Identity.tsx b/src/library/ListItem/Labels/Identity.tsx new file mode 100644 index 0000000000..289fe5694c --- /dev/null +++ b/src/library/ListItem/Labels/Identity.tsx @@ -0,0 +1,43 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ellipsisFn } from '@polkadot-cloud/utils'; +import { useEffect, useState } from 'react'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { Polkicon } from '@polkadot-cloud/react'; +import { IdentityWrapper } from 'library/ListItem/Wrappers'; +import { getIdentityDisplay } from '../../ValidatorList/ValidatorItem/Utils'; +import type { IdentityProps } from '../types'; + +export const Identity = ({ address }: IdentityProps) => { + const { validatorIdentities, validatorSupers, validatorsFetched } = + useValidators(); + + const [display, setDisplay] = useState( + getIdentityDisplay(validatorIdentities[address], validatorSupers[address]) + ); + + useEffect(() => { + setDisplay( + getIdentityDisplay(validatorIdentities[address], validatorSupers[address]) + ); + }, [validatorSupers, validatorIdentities, address]); + + return ( + + +
+ {validatorsFetched && display !== null ? ( +

{display}

+ ) : ( +

{ellipsisFn(address, 6)}

+ )} +
+
+ ); +}; diff --git a/src/library/ListItem/Labels/JoinPool.tsx b/src/library/ListItem/Labels/JoinPool.tsx new file mode 100644 index 0000000000..11c96c0cad --- /dev/null +++ b/src/library/ListItem/Labels/JoinPool.tsx @@ -0,0 +1,39 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCaretRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; + +export const JoinPool = ({ + id, + setActiveTab, +}: { + id: number; + setActiveTab: any; +}) => { + const { t } = useTranslation('library'); + const { openModal } = useOverlay().modal; + + return ( +
+ +
+ ); +}; diff --git a/src/library/ListItem/Labels/Members.tsx b/src/library/ListItem/Labels/Members.tsx new file mode 100644 index 0000000000..bf7d08db70 --- /dev/null +++ b/src/library/ListItem/Labels/Members.tsx @@ -0,0 +1,27 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faUsers } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useTooltip } from 'contexts/Tooltip'; +import { TooltipTrigger } from 'library/ListItem/Wrappers'; + +export const Members = ({ members }: { members: string }) => { + const { t } = useTranslation('library'); + const { setTooltipTextAndOpen } = useTooltip(); + + const tooltipText = t('poolMembers'); + + return ( +
+ setTooltipTextAndOpen(tooltipText)} + /> + +  {members} +
+ ); +}; diff --git a/src/library/ListItem/Labels/Metrics.tsx b/src/library/ListItem/Labels/Metrics.tsx new file mode 100644 index 0000000000..dabfe8a7ad --- /dev/null +++ b/src/library/ListItem/Labels/Metrics.tsx @@ -0,0 +1,30 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChartLine } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import type { MetricsProps } from '../types'; + +export const Metrics = ({ display, address }: MetricsProps) => { + const { openModal } = useOverlay().modal; + + return ( +
+ +
+ ); +}; diff --git a/src/library/ListItem/Labels/NominationStatus.tsx b/src/library/ListItem/Labels/NominationStatus.tsx new file mode 100644 index 0000000000..09e3a50adf --- /dev/null +++ b/src/library/ListItem/Labels/NominationStatus.tsx @@ -0,0 +1,58 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { greaterThanZero, planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useStaking } from 'contexts/Staking'; +import { ValidatorStatusWrapper } from 'library/ListItem/Wrappers'; +import { useNetwork } from 'contexts/Network'; +import type { NominationStatusProps } from '../types'; + +export const NominationStatus = ({ + address, + nominator, + bondFor, + noMargin = false, + status, +}: NominationStatusProps) => { + const { t } = useTranslation('library'); + const { + networkData: { unit, units }, + } = useNetwork(); + const { + eraStakers: { activeAccountOwnStake, stakers }, + erasStakersSyncing, + } = useStaking(); + + // determine staked amount + let stakedAmount = new BigNumber(0); + if (bondFor === 'nominator') { + // bonded amount within the validator. + stakedAmount = + status === 'active' + ? new BigNumber( + activeAccountOwnStake?.find((own) => own.address)?.value ?? 0 + ) + : new BigNumber(0); + } else { + const staker = stakers?.find((s) => s.address === address); + const exists = (staker?.others || []).find(({ who }) => who === nominator); + if (exists) { + stakedAmount = planckToUnit(new BigNumber(exists.value), units); + } + } + + return ( + +
+ {t(`${status || 'waiting'}`)} + {greaterThanZero(stakedAmount) + ? ` / ${ + erasStakersSyncing ? '...' : `${stakedAmount.toFormat()} ${unit}` + }` + : null} +
+
+ ); +}; diff --git a/src/library/ListItem/Labels/Oversubscribed.tsx b/src/library/ListItem/Labels/Oversubscribed.tsx new file mode 100644 index 0000000000..ebb935fd65 --- /dev/null +++ b/src/library/ListItem/Labels/Oversubscribed.tsx @@ -0,0 +1,67 @@ +// Copyright 2023 @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 { motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { MinBondPrecision } from 'consts'; +import { useTooltip } from 'contexts/Tooltip'; +import { + OverSubscribedWrapper, + TooltipTrigger, +} from 'library/ListItem/Wrappers'; +import { useStaking } from 'contexts/Staking'; +import { useNetwork } from 'contexts/Network'; +import type { OversubscribedProps } from '../types'; + +export const Oversubscribed = ({ address }: OversubscribedProps) => { + const { t } = useTranslation('library'); + const { + networkData: { unit }, + } = useNetwork(); + const { setTooltipTextAndOpen } = useTooltip(); + const { erasStakersSyncing, getLowestRewardFromStaker } = useStaking(); + + const { lowest, oversubscribed } = getLowestRewardFromStaker(address); + + const displayOversubscribed = !erasStakersSyncing && oversubscribed; + + const lowestRewardFormatted = lowest + .decimalPlaces(MinBondPrecision) + .toFormat(); + + const tooltipText = `${t( + 'overSubscribedMinReward' + )} ${lowestRewardFormatted} ${unit}`; + + return ( + <> + {displayOversubscribed && ( + +
+ setTooltipTextAndOpen(tooltipText)} + /> + + + + + {lowestRewardFormatted} {unit} + +
+
+ )} + + ); +}; diff --git a/src/library/ListItem/Labels/ParaValidator.tsx b/src/library/ListItem/Labels/ParaValidator.tsx new file mode 100644 index 0000000000..0b83068372 --- /dev/null +++ b/src/library/ListItem/Labels/ParaValidator.tsx @@ -0,0 +1,33 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCubes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useTooltip } from 'contexts/Tooltip'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { TooltipTrigger } from 'library/ListItem/Wrappers'; +import type { ParaValidatorProps } from '../types'; + +export const ParaValidator = ({ address }: ParaValidatorProps) => { + const { t } = useTranslation('library'); + const { sessionParaValidators } = useValidators(); + const { setTooltipTextAndOpen } = useTooltip(); + + const tooltipText = t('validatingParachainBlocks'); + + if (!sessionParaValidators?.includes(address || '')) { + return <>; + } + + return ( +
+ setTooltipTextAndOpen(tooltipText)} + /> + +
+ ); +}; diff --git a/src/library/ListItem/Labels/PoolBonded.tsx b/src/library/ListItem/Labels/PoolBonded.tsx new file mode 100644 index 0000000000..e2a12a473c --- /dev/null +++ b/src/library/ListItem/Labels/PoolBonded.tsx @@ -0,0 +1,93 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + capitalizeFirstLetter, + planckToUnit, + rmCommas, +} from '@polkadot-cloud/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'; + +export const PoolBonded = ({ + pool, + batchKey, + batchIndex, +}: { + pool: Pool; + batchKey: string; + batchIndex: number; +}) => { + const { t } = useTranslation('library'); + const { + networkData: { units, unit }, + } = useNetwork(); + const { meta, getPoolNominationStatusCode } = useBondedPools(); + const { eraStakers, getNominationsStatusFromTargets } = useStaking(); + const { addresses, points } = pool; + + // get pool targets from nominations meta batch + const nominations = meta[batchKey]?.nominations ?? []; + const targets = nominations[batchIndex]?.targets ?? []; + + // store nomination status in state + const [nominationsStatus, setNominationsStatus] = + useState>(); + + // 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 && + nominations.length + ) { + handleNominationsStatus(); + } + }); + + // metadata has changed, which means pool items may have been added. + // recalculate nominations status + useEffect(() => { + handleNominationsStatus(); + }, [meta, pool, eraStakers.stakers.length]); + + // calculate total bonded pool amount + const poolBonded = planckToUnit(new BigNumber(rmCommas(points)), units); + + // determine nominations status and display + const nominationStatus = getPoolNominationStatusCode( + nominationsStatus || null + ); + + return ( + <> + +
+ {nominationStatus === null || !eraStakers.stakers.length + ? `${t('syncing')}...` + : targets.length + ? capitalizeFirstLetter(t(`${nominationStatus}`) ?? '') + : t('notNominating')} + {' / '} + {t('bonded')}: {poolBonded.decimalPlaces(3).toFormat()} {unit} +
+
+ + ); +}; diff --git a/src/library/ListItem/Labels/PoolCommission.tsx b/src/library/ListItem/Labels/PoolCommission.tsx new file mode 100644 index 0000000000..51b56b83c4 --- /dev/null +++ b/src/library/ListItem/Labels/PoolCommission.tsx @@ -0,0 +1,28 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { useTooltip } from 'contexts/Tooltip'; +import { TooltipTrigger } from 'library/ListItem/Wrappers'; + +export const PoolCommission = ({ commission }: { commission: string }) => { + const { t } = useTranslation('library'); + const { setTooltipTextAndOpen } = useTooltip(); + + const tooltipText = t('poolCommission'); + + if (!commission) { + return null; + } + + return ( +
+ setTooltipTextAndOpen(tooltipText)} + /> + {commission} +
+ ); +}; diff --git a/src/library/ListItem/Labels/PoolId.tsx b/src/library/ListItem/Labels/PoolId.tsx new file mode 100644 index 0000000000..97da6b9600 --- /dev/null +++ b/src/library/ListItem/Labels/PoolId.tsx @@ -0,0 +1,27 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faHashtag } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useTooltip } from 'contexts/Tooltip'; +import { TooltipTrigger } from 'library/ListItem/Wrappers'; + +export const PoolId = ({ id }: { id: number }) => { + const { t } = useTranslation('library'); + const { setTooltipTextAndOpen } = useTooltip(); + + const tooltipText = t('poolId'); + + return ( +
+ setTooltipTextAndOpen(tooltipText)} + /> + +  {id} +
+ ); +}; diff --git a/src/library/ListItem/Labels/PoolIdentity.tsx b/src/library/ListItem/Labels/PoolIdentity.tsx new file mode 100644 index 0000000000..b9111f1d6d --- /dev/null +++ b/src/library/ListItem/Labels/PoolIdentity.tsx @@ -0,0 +1,39 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ellipsisFn, determinePoolDisplay } from '@polkadot-cloud/utils'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { Polkicon } from '@polkadot-cloud/react'; +import { IdentityWrapper } from 'library/ListItem/Wrappers'; +import type { PoolIdentityProps } from '../types'; + +export const PoolIdentity = ({ + pool, + batchKey, + batchIndex, +}: PoolIdentityProps) => { + const { meta } = useBondedPools(); + const { addresses } = pool; + + // get metadata from pools metabatch + const metadata = meta[batchKey]?.metadata ?? []; + + // aggregate synced status + const metadataSynced = metadata.length > 0 ?? false; + + // pool display name + const display = determinePoolDisplay(addresses.stash, metadata[batchIndex]); + + return ( + + +
+ {!metadataSynced ? ( +

{ellipsisFn(addresses.stash)}

+ ) : ( +

{display}

+ )} +
+
+ ); +}; diff --git a/src/library/ListItem/Labels/PoolMemberBonded.tsx b/src/library/ListItem/Labels/PoolMemberBonded.tsx new file mode 100644 index 0000000000..76e90bca17 --- /dev/null +++ b/src/library/ListItem/Labels/PoolMemberBonded.tsx @@ -0,0 +1,63 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { greaterThanZero, planckToUnit, rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { ValidatorStatusWrapper } from 'library/ListItem/Wrappers'; +import { useNetwork } from 'contexts/Network'; + +export const PoolMemberBonded = ({ meta, batchKey, batchIndex }: any) => { + const { t } = useTranslation('library'); + const { units, unit } = useNetwork().networkData; + + const poolMembers = meta[batchKey]?.poolMembers ?? []; + const poolMember = poolMembers[batchIndex] ?? null; + + let bonded = new BigNumber(0); + let totalUnbonding = new BigNumber(0); + + let status = ''; + if (poolMember) { + const { points, unbondingEras } = poolMember; + + bonded = planckToUnit(new BigNumber(rmCommas(points)), units); + status = greaterThanZero(bonded) ? 'active' : 'inactive'; + + // converting unbonding eras from points to units + let totalUnbondingUnit = new BigNumber(0); + Object.values(unbondingEras).forEach((amount: any) => { + const amountBn = new BigNumber(rmCommas(amount)); + totalUnbondingUnit = totalUnbondingUnit.plus(amountBn); + }); + totalUnbonding = planckToUnit(new BigNumber(totalUnbondingUnit), units); + } + + return ( + <> + {!poolMember ? ( + +
{t('syncing')}...
+
+ ) : ( + <> + {greaterThanZero(bonded) && ( + +
+ {t('bonded')}: {bonded.decimalPlaces(3).toFormat()} {unit} +
+
+ )} + + )} + + {poolMember && greaterThanZero(totalUnbonding) && ( + +
+ {t('unbonding')} {totalUnbonding.decimalPlaces(3).toFormat()} {unit} +
+
+ )} + + ); +}; diff --git a/src/library/ListItem/Labels/Quartile.tsx b/src/library/ListItem/Labels/Quartile.tsx new file mode 100644 index 0000000000..c6442f2dbd --- /dev/null +++ b/src/library/ListItem/Labels/Quartile.tsx @@ -0,0 +1,35 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { MaxEraRewardPointsEras } from 'consts'; +import { useTooltip } from 'contexts/Tooltip'; +import { useTranslation } from 'react-i18next'; + +export const Quartile = ({ address }: { address: string }) => { + const { t } = useTranslation(); + const { setTooltipTextAndOpen } = useTooltip(); + const { validatorEraPointsHistory, erasRewardPointsFetched } = + useValidators(); + + const quartile = validatorEraPointsHistory[address]?.quartile; + const tooltipText = `${t('dayPerformanceStanding', { + count: MaxEraRewardPointsEras, + ns: 'library', + })}`; + + if (erasRewardPointsFetched !== 'synced') return null; + + return ( +
setTooltipTextAndOpen(tooltipText)} + style={{ cursor: 'default' }} + > + {![100, undefined].includes(quartile) + ? `${t('top', { ns: 'library' })} ${quartile}%` + : ``} +
+ ); +}; diff --git a/src/library/ListItem/Labels/Select.tsx b/src/library/ListItem/Labels/Select.tsx new file mode 100644 index 0000000000..45e56f5722 --- /dev/null +++ b/src/library/ListItem/Labels/Select.tsx @@ -0,0 +1,28 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { SelectWrapper } from 'library/ListItem/Wrappers'; +import { useList } from '../../List/context'; +import type { SelectProps } from '../types'; + +export const Select = ({ item }: SelectProps) => { + const { addToSelected, removeFromSelected, selected } = useList(); + + const isSelected = selected.includes(item); + + return ( + { + if (isSelected) { + removeFromSelected([item]); + } else { + addToSelected(item); + } + }} + > + {isSelected && } + + ); +}; diff --git a/src/library/ListItem/Wrappers.ts b/src/library/ListItem/Wrappers.ts new file mode 100644 index 0000000000..58035beb2f --- /dev/null +++ b/src/library/ListItem/Wrappers.ts @@ -0,0 +1,367 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; +import { SmallFontSizeMaxWidth } from 'consts'; + +export const Wrapper = styled.div` + --height-top-row: 3.25rem; + --height-bottom-row: 5rem; + + &.member { + --height-bottom-row: 2.75rem; + } + &.pool-join { + --height-bottom-row: 7.5rem; + } + + --height-total: calc(var(--height-top-row) + var(--height-bottom-row)); + + height: var(--height-total); + display: flex; + flex-flow: row wrap; + position: relative; + margin: 0.5rem; + width: 100%; + + > .inner { + background: var(--background-list-item); + &.modal { + background: var(--background-modal-card); + } + &.canvas { + background: var(--background-canvas-card); + } + &.modal, + &.canvas { + box-shadow: none; + border: none; + } + + border-radius: 1rem; + display: flex; + flex-flow: row wrap; + align-items: center; + overflow: hidden; + position: absolute; + padding: 0; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + + .row { + flex: 1 0 100%; + display: flex; + align-items: center; + padding: 0 0.5rem; + + &.top { + height: var(--height-top-row); + } + &.bottom { + height: var(--height-bottom-row); + + &.lg { + display: flex; + align-items: center; + > div { + &:first-child { + flex-grow: 1; + padding: 0 0.25rem; + } + &:last-child { + flex-shrink: 1; + display: flex; + flex-direction: column; + align-items: flex-end; + } + } + } + } + } + } +`; + +export const Labels = styled.div` + display: flex; + justify-content: flex-end; + font-size: 0.85rem; + align-items: center; + overflow: hidden; + flex-grow: 1; + padding: 0 0 0 0.25rem; + height: inherit; + + button { + padding: 0 0.1rem; + background: var(--shimmer-foreground); + font-size: 1rem; + border-radius: 50%; + width: 1.9rem; + height: 1.9rem; + + @media (min-width: ${SmallFontSizeMaxWidth}px) { + padding: 0 0.2rem; + } + color: var(--text-color-secondary); + &:hover { + opacity: 0.75; + } + &.active { + color: var(--accent-color-primary); + } + &:disabled { + opacity: var(--opacity-disabled); + } + } + + &.canvas button { + background: none; + border: 1px solid var(--border-secondary-color); + } + + .label { + color: var(--text-color-secondary); + position: relative; + display: flex; + align-items: center; + margin: 0 0.2rem; + font-size: inherit; + + @media (min-width: ${SmallFontSizeMaxWidth}px) { + margin: 0 0.35rem; + &.pool { + margin: 0 0.45rem; + } + } + + &.button-with-text { + margin-right: 0; + + button { + color: var(--accent-color-secondary); + font-family: InterSemiBold, sans-serif; + font-size: 0.95rem; + display: flex; + flex-flow: row wrap; + align-items: center; + width: auto; + height: auto; + border-radius: 0.75rem; + padding: 0.25rem 0.75rem; + + &:hover { + opacity: 1; + } + > svg { + margin-left: 0.3rem; + } + } + } + + &.warning { + color: #d2545d; + display: flex; + flex-flow: row wrap; + align-items: center; + padding-right: 0.35rem; + } + } +`; + +export const OverSubscribedWrapper = styled.div` + display: flex; + align-items: center; + width: 100%; + height: 100%; + + .warning { + margin-right: 0.25rem; + @media (max-width: 500px) { + display: none; + } + } +`; +export const IdentityWrapper = styled(motion.div)` + display: flex; + margin-right: 0.5rem; + align-items: center; + align-content: center; + overflow: hidden; + flex: 1 1 25%; + position: relative; + + .inner { + display: flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + height: 3.25rem; + padding: 0 0 0 0.2rem; + } + h4 { + color: var(--text-color-secondary); + font-family: InterSemiBold, sans-serif; + position: absolute; + top: 0; + height: 3.25rem; + line-height: 3.25rem; + padding: 0 0 0 0.3rem; + margin: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 1rem; + width: 100%; + + > span { + color: var(--text-color-secondary); + opacity: 0.75; + font-size: 0.88rem; + margin-left: 0.35rem; + position: relative; + top: -0.1rem; + } + } +`; + +export const ValidatorStatusWrapper = styled.div<{ + $status: string; + $noMargin?: boolean; +}>` + margin-right: ${(props) => (props.$noMargin ? '0' : '0.35rem')}; + padding: 0 0.5rem; + + h5 { + color: ${(props) => + props.$status === 'active' + ? 'var(--status-success-color)' + : 'var(--text-color-secondary)'}; + opacity: ${(props) => (props.$status === 'active' ? 0.8 : 0.5)}; + display: flex; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +`; + +export const SelectWrapper = styled.button` + background: var(--background-input); + margin: 0 0.75rem 0 0.25rem; + overflow: hidden; + display: flex; + flex-flow: row wrap; + align-items: center; + border-radius: 0.25rem; + width: 1.1rem; + height: 1.1rem; + padding: 0; + * { + cursor: pointer; + width: 100%; + padding: 0; + } + + span { + display: flex; + align-items: center; + justify-content: center; + } + svg { + color: var(--text-color-primary); + width: 1rem; + height: 1rem; + } + .select-checkbox { + display: flex; + align-items: center; + justify-content: center; + font-size: 1rem; + } +`; + +export const Separator = styled.div` + border-bottom: 1px solid var(--border-primary-color); + width: 100%; + height: 1px; + opacity: 0.7; +`; + +export const MenuPosition = styled.div` + position: absolute; + top: -10px; + right: 10px; + width: 0; + height: 0; + opacity: 0; +`; + +export const TooltipTrigger = styled.div` + z-index: 1; + width: 130%; + height: 130%; + position: absolute; + top: -10%; + left: -10%; + + &.as-button { + cursor: pointer; + } +`; + +export const ValidatorPulseWrapper = styled.div` + border: 1px solid var(--grid-color-primary); + border-radius: 0.25rem; + height: 3.2rem; + display: flex; + align-items: center; + width: 100%; + max-width: 13.5rem; + position: relative; + padding: 0.15rem 0; + + &.canvas { + border: 1px solid var(--grid-color-secondary); + } + + > svg { + max-width: 100%; + max-height: 100%; + } + + > .preload { + position: absolute; + top: 0; + left: 0; + background: var(--shimmer-foreground); + background-image: linear-gradient( + to right, + var(--shimmer-foreground) 0%, + var(--shimmer-background) 20%, + var(--shimmer-foreground) 40%, + var(--shimmer-foreground) 100% + ); + background-repeat: no-repeat; + background-size: 500px 104px; + animation-duration: 1.5s; + opacity: 0.2; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; + z-index: 0; + width: 100%; + height: 100%; + + @keyframes shimmer { + 0% { + background-position: -50% 0; + } + 100% { + background-position: 200% 0; + } + } + } +`; diff --git a/src/library/ListItem/types.ts b/src/library/ListItem/types.ts new file mode 100644 index 0000000000..0a1967deab --- /dev/null +++ b/src/library/ListItem/types.ts @@ -0,0 +1,65 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type React from 'react'; +import type { BondedPool } from 'contexts/Pools/types'; +import type { BondFor, MaybeAddress } from 'types'; +import type { ValidatorPrefs } from 'contexts/Validators/types'; +import type BigNumber from 'bignumber.js'; +import type { NominationStatus } from 'library/ValidatorList/ValidatorItem/types'; + +export interface BlockedProps { + prefs: ValidatorPrefs; +} + +export interface CopyAddressProps { + address: string; +} + +export interface FavoriteProps { + address: string; +} + +export interface IdentityProps { + address: string; +} + +export interface PoolIdentityProps { + batchIndex: number; + batchKey: string; + pool: BondedPool; +} + +export interface MetricsProps { + display: React.ReactNode | null; + address: string; +} + +export interface NominationStatusProps { + address: string; + bondFor: BondFor; + nominator: MaybeAddress; + status?: NominationStatus; + noMargin?: boolean; +} + +export interface OversubscribedProps { + address: MaybeAddress; +} + +export interface SelectProps { + item: { + address: string; + }; +} + +export interface ParaValidatorProps { + address: MaybeAddress; +} + +export interface EraStatusProps { + address: MaybeAddress; + noMargin: boolean; + totalStake: BigNumber; + status: 'waiting' | 'active'; +} diff --git a/src/library/Loader/Announcement.tsx b/src/library/Loader/Announcement.tsx new file mode 100644 index 0000000000..3d78af0dac --- /dev/null +++ b/src/library/Loader/Announcement.tsx @@ -0,0 +1,8 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { LoaderWrapper } from './Wrapper'; + +export const Announcement = () => ( + +); diff --git a/src/library/Loader/Wrapper.ts b/src/library/Loader/Wrapper.ts new file mode 100644 index 0000000000..2b64ddba6a --- /dev/null +++ b/src/library/Loader/Wrapper.ts @@ -0,0 +1,36 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const LoaderWrapper = styled.div` + background: var(--shimmer-foreground); + background-image: linear-gradient( + to right, + var(--shimmer-foreground) 0%, + var(--shimmer-background) 20%, + var(--shimmer-foreground) 40%, + var(--shimmer-foreground) 100% + ); + background-repeat: no-repeat; + background-size: 600px 104px; + animation-duration: 1.5s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; + + opacity: 0.1; + border-radius: 0.75rem; + display: inline-block; + position: relative; + + @keyframes shimmer { + 0% { + background-position: 0px 0; + } + 100% { + background-position: 150% 0; + } + } +`; diff --git a/src/library/Menu/Wrappers.ts b/src/library/Menu/Wrappers.ts new file mode 100644 index 0000000000..02906fc2e5 --- /dev/null +++ b/src/library/Menu/Wrappers.ts @@ -0,0 +1,40 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { FloatingMenuWidth } from 'consts'; + +export const Wrapper = styled.div` + background: var(--background-default); + width: ${FloatingMenuWidth}px; + padding: 0.25rem 0.75rem; + display: flex; + flex-flow: column wrap; + transition: opacity var(--transition-duration); + border-radius: 1rem; + + > button:last-child { + border: none; + } +`; + +export const ItemWrapper = styled.button` + border-bottom: 1px solid var(--border-primary-color); + color: var(--text-color-secondary); + display: flex; + width: 100%; + padding: 0.75rem 0.5rem; + display: flex; + flex-flow: row wrap; + align-items: center; + + &:hover { + opacity: 0.75; + } + + .title { + color: var(--text-color-secondary); + padding: 0 0 0 0.75rem; + font-size: 1rem; + } +`; diff --git a/src/library/Menu/index.tsx b/src/library/Menu/index.tsx new file mode 100644 index 0000000000..d6149f4743 --- /dev/null +++ b/src/library/Menu/index.tsx @@ -0,0 +1,74 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useEffect, useRef } from 'react'; +import { useMenu } from 'contexts/Menu'; +import { useOutsideAlerter } from 'library/Hooks'; +import { ItemWrapper, Wrapper } from './Wrappers'; + +export const Menu = () => { + const menu = useMenu(); + const { position } = menu; + + const ref = useRef(null); + + useEffect(() => { + if (menu.open === 1) { + menu.checkMenuPosition(ref); + // check position + } + }, [menu.open]); + + useEffect(() => { + window.addEventListener('resize', resizeCallback); + return () => { + window.removeEventListener('resize', resizeCallback); + }; + }, []); + + const resizeCallback = () => { + menu.closeMenu(); + }; + + useOutsideAlerter( + ref, + () => { + menu.closeMenu(); + }, + ['ignore-open-menu-button'] + ); + + return ( + <> + {menu.open === 1 && ( + + {menu.items.map((item: any, i: number) => { + const { icon, title, cb } = item; + + return ( + { + cb(); + menu.closeMenu(); + }} + > + {icon} +
{title}
+
+ ); + })} +
+ )} + + ); +}; diff --git a/src/library/Modal/Close.tsx b/src/library/Modal/Close.tsx new file mode 100644 index 0000000000..5b69517bd2 --- /dev/null +++ b/src/library/Modal/Close.tsx @@ -0,0 +1,18 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import CrossSVG from 'img/cross.svg?react'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { CloseWrapper } from './Wrappers'; + +export const Close = () => { + const { setModalStatus } = useOverlay().modal; + + return ( + + + + ); +}; diff --git a/src/library/Modal/Title.tsx b/src/library/Modal/Title.tsx new file mode 100644 index 0000000000..1a51353bcd --- /dev/null +++ b/src/library/Modal/Title.tsx @@ -0,0 +1,58 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ButtonHelp } from '@polkadot-cloud/react'; +import type { FunctionComponent } from 'react'; +import React from 'react'; +import { useHelp } from 'contexts/Help'; +import CrossSVG from 'img/cross.svg?react'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { TitleWrapper } from './Wrappers'; + +interface TitleProps { + title: string; + icon?: IconProp; + Svg?: FunctionComponent; + fixed?: boolean; + helpKey?: string; + style?: React.CSSProperties; +} + +export const Title = ({ + helpKey, + title, + icon, + fixed, + Svg, + style, +}: TitleProps) => { + const { setModalStatus } = useOverlay().modal; + const { openHelp } = useHelp(); + + const graphic = Svg ? ( + + ) : icon ? ( + + ) : null; + + return ( + +
+ {graphic} +

+ {title} + {helpKey ? ( + openHelp(helpKey)} /> + ) : null} +

+
+
+ +
+
+ ); +}; diff --git a/src/library/Modal/Wrappers.ts b/src/library/Modal/Wrappers.ts new file mode 100644 index 0000000000..198d793454 --- /dev/null +++ b/src/library/Modal/Wrappers.ts @@ -0,0 +1,124 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const TitleWrapper = styled.div<{ $fixed: boolean }>` + padding: ${(props) => + props.$fixed ? '0.6rem 1rem 0rem 1rem' : '2rem 1rem 0 1rem'}; + display: flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + min-height: 3rem; + + > div { + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 0 0.5rem; + + button { + padding: 0; + } + + path { + fill: var(--text-color-primary); + } + + &:first-child { + flex-grow: 1; + + > h2 { + display: flex; + align-items: center; + font-family: 'Unbounded', 'sans-serif', sans-serif; + font-size: 1.3rem; + margin: 0; + + > button { + margin-left: 0.85rem; + } + } + > svg { + margin-right: 0.9rem; + } + } + + &:last-child { + button { + position: absolute; + top: 1.5rem; + right: 1.5rem; + opacity: 0.25; + &:hover { + opacity: 1; + } + } + } + } +`; + +export const StatsWrapper = styled.div` + width: 100%; + display: flex; + flex-flow: row wrap; + margin-top: 1rem; +`; +export const StatWrapper = styled.div` + display: flex; + flex-flow: column wrap; + margin-bottom: 1rem; + padding: 0 0.75rem; + flex-grow: 1; + flex-basis: 100%; + + @media (min-width: 600px) { + margin-bottom: 0.5rem; + } + + @media (min-width: 601px) { + flex-basis: 33%; + } + + > .inner { + border-bottom: 1px solid var(--border-primary-color); + padding-bottom: 0.5rem; + + > h2, + h3, + h4 { + margin: 0.25rem 0; + } + h4 { + margin: 0rem 0 0.75rem 0; + display: flex; + align-items: center; + + .icon { + margin-right: 0.425rem; + } + } + h2, + h3, + h4 { + color: var(--text-color-secondary); + } + } +`; + +export const CloseWrapper = styled.div` + position: absolute; + right: 1.5rem; + top: 1.5rem; + z-index: 2; + + > button { + opacity: 0.4; + transition: opacity var(--transition-duration) ease-in-out; + + &:hover { + opacity: 1; + } + } +`; diff --git a/src/library/NetworkBar/Status.tsx b/src/library/NetworkBar/Status.tsx new file mode 100644 index 0000000000..1a429847e7 --- /dev/null +++ b/src/library/NetworkBar/Status.tsx @@ -0,0 +1,31 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; + +export const Status = () => { + const { t } = useTranslation('library'); + const { apiStatus } = useApi(); + + return ( + <> + {apiStatus === 'disconnected' && ( + + {t('disconnected')} + + )} + {apiStatus === 'connecting' && ( + + {t('connecting')}... + + )} + {apiStatus === 'connected' && ( + + {t('connectedToNetwork')} + + )} + + ); +}; diff --git a/src/library/NetworkBar/Wrappers.ts b/src/library/NetworkBar/Wrappers.ts new file mode 100644 index 0000000000..ce9f2c195d --- /dev/null +++ b/src/library/NetworkBar/Wrappers.ts @@ -0,0 +1,115 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { SideMenuStickyThreshold } from 'consts'; + +export const Wrapper = styled.div` + background: var(--background-app-footer); + color: var(--text-color-secondary); + display: flex; + flex-flow: row nowrap; + align-items: center; + bottom: 0px; + left: 0px; + overflow: hidden; + z-index: 6; + backdrop-filter: blur(4px); + position: relative; + padding-top: 0.15rem; + font-size: 0.85rem; + width: 100%; + + @media (min-width: ${SideMenuStickyThreshold + 1}px) { + position: fixed; + } + + .network_icon { + margin: 0 0 0 1rem; + width: 1.5rem; + height: 1.5rem; + } +`; + +export const Summary = styled.div` + width: 100%; + display: flex; + align-items: center; + align-content: center; + + /* hide connection status text on small screens */ + .hide-small { + display: flex; + align-items: center; + align-content: center; + + @media (max-width: 600px) { + display: none; + } + } + + a, + button { + color: var(--text-color-secondary); + font-size: 0.85rem; + opacity: 0.75; + } + p { + border-left: 1px solid var(--accent-color-transparent); + margin: 0.25rem 0.5rem 0.25rem 0.15rem; + font-size: 0.85rem; + padding-left: 0.5rem; + line-height: 1.2rem; + } + .stat { + margin: 0 0.25rem; + display: flex; + align-items: center; + } + + /* left and right sections for each row*/ + > section { + color: var(--text-color-secondary); + padding: 0.5rem 0; + + /* left section */ + &:nth-child(1) { + display: flex; + flex-flow: row wrap; + align-items: center; + flex-grow: 1; + } + + /* right section */ + &:last-child { + flex-grow: 1; + display: flex; + align-items: center; + flex-flow: row-reverse wrap; + padding-right: 0.5rem; + + button { + color: var(--text-color-secondary); + border-radius: 0.4rem; + padding: 0.25rem 0.5rem; + font-size: 0.85rem; + } + span { + &.pos { + color: #3eb955; + } + &.neg { + color: #d2545d; + } + } + } + } +`; + +export const Separator = styled.div` + border-left: 1px solid var(--text-color-secondary); + opacity: 0.2; + margin: 0 0.3rem; + width: 1px; + height: 1rem; +`; diff --git a/src/library/NetworkBar/index.tsx b/src/library/NetworkBar/index.tsx new file mode 100644 index 0000000000..99ce621782 --- /dev/null +++ b/src/library/NetworkBar/index.tsx @@ -0,0 +1,102 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { capitalizeFirstLetter } from '@polkadot-cloud/utils'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { usePlugins } from 'contexts/Plugins'; +import { usePrices } from 'library/Hooks/usePrices'; +import { useNetwork } from 'contexts/Network'; +import { Status } from './Status'; +import { Summary, Wrapper } from './Wrappers'; + +export const NetworkBar = () => { + const { t } = useTranslation('library'); + const { plugins } = usePlugins(); + const { isLightClient } = useApi(); + const { networkData, network } = useNetwork(); + const prices = usePrices(); + + const PRIVACY_URL = import.meta.env.VITE_PRIVACY_URL; + const DISCLAIMER_URL = import.meta.env.VITE_DISCLAIMER_URL; + const ORGANISATION = import.meta.env.VITE_ORGANISATION; + const LEGAL_DISCLOSURES_URL = import.meta.env.VITE_LEGAL_DISCLOSURES_URL; + + const [networkName, setNetworkName] = useState( + capitalizeFirstLetter(network) + ); + + useEffect(() => { + setNetworkName( + `${capitalizeFirstLetter(network)}${isLightClient ? ` Light` : ``}` + ); + }, [network, isLightClient]); + + return ( + + +
+
+

{ORGANISATION === undefined ? networkName : ORGANISATION}

+ {PRIVACY_URL !== undefined ? ( +

+ + {t('privacy')} + +

+ ) : ( + + )} + {DISCLAIMER_URL !== undefined && ( + <> +

+ + {t('disclaimer')} + +

+ + )} + {LEGAL_DISCLOSURES_URL !== undefined && ( + <> +

+ + {t('legalDisclosures')} + +

+ + )} +
+
+
+ {plugins.includes('binance_spot') && ( + <> +
+ 0 + ? ' pos' + : '' + }`} + > + {prices.change < 0 ? '' : prices.change > 0 ? '+' : ''} + {prices.change}% + +
+
+ 1 {networkData.api.unit} / {prices.lastPrice} USD +
+ + )} +
+
+
+ + ); +}; diff --git a/src/library/Nominations/Wrapper.ts b/src/library/Nominations/Wrapper.ts new file mode 100644 index 0000000000..4b1e615e0f --- /dev/null +++ b/src/library/Nominations/Wrapper.ts @@ -0,0 +1,23 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + flex: 1; + display: flex; + flex-flow: column wrap; + width: 100%; + + .head { + flex: 1; + display: flex; + flex-flow: row wrap; + padding: 0 0.25rem; + margin-top: 1rem; + + > h3 { + flex: 1; + } + } +`; diff --git a/src/library/Nominations/index.tsx b/src/library/Nominations/index.tsx new file mode 100644 index 0000000000..7729e76e06 --- /dev/null +++ b/src/library/Nominations/index.tsx @@ -0,0 +1,155 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCog, faStopCircle } from '@fortawesome/free-solid-svg-icons'; +import { ButtonHelp, ButtonPrimary } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useBonded } from 'contexts/Bonded'; +import { useHelp } from 'contexts/Help'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { CardHeaderWrapper } from 'library/Card/Wrappers'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { ValidatorList } from 'library/ValidatorList'; +import type { MaybeAddress } from 'types'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { ListStatusHeader } from 'library/List'; +import { Wrapper } from './Wrapper'; + +export const Nominations = ({ + bondFor, + nominator, +}: { + bondFor: 'pool' | 'nominator'; + nominator: MaybeAddress; +}) => { + const { t } = useTranslation('pages'); + const { + poolNominations, + selectedActivePool, + isOwner: isPoolOwner, + isNominator: isPoolNominator, + } = useActivePools(); + const { isSyncing } = useUi(); + const { openHelp } = useHelp(); + const { inSetup } = useStaking(); + const { + modal: { openModal }, + canvas: { openCanvas }, + } = useOverlay(); + const { getNominated } = useValidators(); + const { isFastUnstaking } = useUnstaking(); + const { activeAccount } = useActiveAccounts(); + const { getAccountNominations } = useBonded(); + const { isReadOnlyAccount } = useImportedAccounts(); + + // Determine if pool or nominator. + const isPool = bondFor === 'pool'; + + // Derive nominations from `bondFor` type. + const nominations = isPool + ? poolNominations.targets + : getAccountNominations(nominator); + const nominated = getNominated(bondFor); + + // Determine if this nominator is actually nominating. + const isNominating = nominated?.length ?? false; + + // Determine whether this is a pool that is in Destroying state & not nominating. + const poolDestroying = + isPool && + selectedActivePool?.bondedPool?.state === 'Destroying' && + !isNominating; + + // Determine whether to display buttons. + // + // If regular staking and nominating, or if pool and account is nominator or root, display stop + // button. + const displayBtns = + (!isPool && nominations.length) || + (isPool && (isPoolNominator() || isPoolOwner())); + + // Determine whether buttons are disabled. + const btnsDisabled = + (!isPool && inSetup()) || + isSyncing || + isReadOnlyAccount(activeAccount) || + poolDestroying || + isFastUnstaking; + + return ( + + +

+ {isPool ? t('nominate.poolNominations') : t('nominate.nominations')} + openHelp('Nominations')} /> +

+
+ {displayBtns && ( + <> + + openModal({ + key: 'ChangeNominations', + options: { + nominations: [], + bondFor, + }, + size: 'sm', + }) + } + /> + + openCanvas({ + key: 'ManageNominations', + scroll: false, + options: { + bondFor, + nominator, + nominated, + }, + size: 'xl', + }) + } + /> + + )} +
+
+ {nominated === null || isSyncing ? ( + {`${t('nominate.syncing')}...`} + ) : !nominator ? ( + {t('nominate.notNominating')}. + ) : nominated.length > 0 ? ( + + ) : poolDestroying ? ( + {t('nominate.poolDestroy')} + ) : ( + {t('nominate.notNominating')}. + )} +
+ ); +}; diff --git a/src/library/Nominations/types.ts b/src/library/Nominations/types.ts new file mode 100644 index 0000000000..22b7bc798d --- /dev/null +++ b/src/library/Nominations/types.ts @@ -0,0 +1,18 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyJson } from '@polkadot-cloud/react/types'; +import type { ListFormat } from 'library/PoolList/types'; +import type { Validator } from 'contexts/Validators/types'; + +export interface ManageNominationsInterface { + addToSelected: (item: AnyJson) => void; + removeFromSelected: (item: AnyJson) => void; + setListFormat: (format: ListFormat) => void; + setSelectActive: (active: boolean) => void; + resetSelected: () => void; + selected: Validator[]; + listFormat: ListFormat; + selectActive: boolean; + selectTogglable: boolean; +} diff --git a/src/library/Notifications/Wrapper.ts b/src/library/Notifications/Wrapper.ts new file mode 100644 index 0000000000..d014dbf1e3 --- /dev/null +++ b/src/library/Notifications/Wrapper.ts @@ -0,0 +1,42 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.ul` + position: fixed; + top: 0; + right: 0; + display: flex; + flex-direction: column; + list-style: none; + justify-content: flex-end; + z-index: 10; + + li { + background: var(--background-primary); + margin: 0.3rem 1.2rem; + position: relative; + border-radius: 1.25rem; + padding: 1rem 1.35rem; + display: flex; + flex-flow: column wrap; + justify-content: center; + cursor: pointer; + overflow: hidden; + width: 375px; + + h3 { + color: var(--accent-color-primary); + font-family: InterSemiBold, sans-serif; + font-size: 1.2rem; + margin: 0.15rem 0 0.4rem; + flex: 1; + } + h4 { + font-family: InterSemiBold, sans-serif; + font-size: 1.05rem; + line-height: 1.45rem; + } + } +`; diff --git a/src/library/Notifications/index.tsx b/src/library/Notifications/index.tsx new file mode 100644 index 0000000000..598e6d0ed3 --- /dev/null +++ b/src/library/Notifications/index.tsx @@ -0,0 +1,45 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { AnimatePresence, motion } from 'framer-motion'; +import { useNotifications } from 'contexts/Notifications'; +import type { NotificationInterface } from 'contexts/Notifications/types'; +import { Wrapper } from './Wrapper'; + +export const Notifications = () => { + const { notifications, removeNotification } = useNotifications(); + + return ( + + + {notifications.length > 0 && + notifications.map( + (notification: NotificationInterface, i: number) => { + const { item, index } = notification; + + return ( + removeNotification(index)} + > + {item.title &&

{item.title}

} + {item.subtitle &&

{item.subtitle}

} +
+ ); + } + )} +
+
+ ); +}; diff --git a/src/library/PayeeInput/Wrapper.ts b/src/library/PayeeInput/Wrapper.ts new file mode 100644 index 0000000000..5f0fee4294 --- /dev/null +++ b/src/library/PayeeInput/Wrapper.ts @@ -0,0 +1,92 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div<{ $activeInput?: boolean }>` + > .inner { + width: 100%; + display: flex; + flex-flow: column nowrap; + border-bottom: 1.5px solid + ${(props) => + props.$activeInput + ? 'var(--accent-color-primary)' + : 'var(--border-primary-color)'}; + padding: 0rem 0 0.4rem 0; + transition: border var(--transition-duration); + + > h4 { + color: var(--text-color-secondary); + margin-top: 1.25rem; + margin-bottom: 0.5rem; + } + + > .account { + width: 100%; + display: flex; + flex-flow: row nowrap; + align-items: center; + margin-top: 0.2rem; + + > .emptyIcon { + background: var(--background-list-item); + width: 2.5rem; + height: 2.5rem; + border-radius: 50%; + } + + > .input { + color: var(--text-color-secondary); + display: flex; + flex-flow: column nowrap; + margin-left: 0.75rem; + min-width: 150px; + max-width: 100%; + + > input { + color: var(--text-color-secondary); + font-size: 1.25rem; + z-index: 1; + opacity: 1; + + &:disabled { + opacity: 0.75; + } + } + + .hidden { + font-size: 1.25rem; + opacity: 0; + position: absolute; + top: -999px; + } + } + } + } + + .label { + position: relative; + display: flex; + align-items: flex-end; + max-width: 100%; + overflow: hidden; + height: 2rem; + margin-top: 0.65rem; + font-size: 0.85rem; + + h5 { + color: var(--text-color-secondary); + position: absolute; + top: 0; + left: 0; + max-width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + > svg { + margin-right: 0.4rem; + } + } + } +`; diff --git a/src/library/PayeeInput/index.tsx b/src/library/PayeeInput/index.tsx new file mode 100644 index 0000000000..ce24e13e6f --- /dev/null +++ b/src/library/PayeeInput/index.tsx @@ -0,0 +1,155 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { isValidAddress, remToUnit } from '@polkadot-cloud/utils'; +import React, { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBonded } from 'contexts/Bonded'; +import { Polkicon } from '@polkadot-cloud/react'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useNetwork } from 'contexts/Network'; +import { formatAccountSs58 } from 'contexts/Connect/Utils'; +import { Wrapper } from './Wrapper'; +import type { PayeeInputProps } from './types'; + +export const PayeeInput = ({ + payee, + account, + setAccount, + handleChange, +}: PayeeInputProps) => { + const { t } = useTranslation('library'); + const { getBondedAccount } = useBonded(); + const { accounts } = useImportedAccounts(); + const { + networkData: { ss58 }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const controller = getBondedAccount(activeAccount); + + const accountMeta = accounts.find((a) => a.address === activeAccount); + + // store whether account value is valid. + const [valid, setValid] = useState(isValidAddress(account || '')); + + // Store whether input is currently active. + const [inputActive, setInputActive] = useState(false); + + const hiddenRef = useRef(null); + const showingRef = useRef(null); + + // Adjust the width of account input based on text length. + const handleAdjustWidth = () => { + if (hiddenRef.current && showingRef.current) { + const hiddenWidth = hiddenRef.current.offsetWidth; + showingRef.current.style.width = `${hiddenWidth + remToUnit('2.5rem')}px`; + } + }; + + // Handle change of account value. Updates setup progress if the account is a valid value. + const handleChangeAccount = (e: React.ChangeEvent) => { + const newAddress = e.target.value; + const formatted = formatAccountSs58(newAddress, ss58) || newAddress || null; + const isValid = isValidAddress(formatted || ''); + + setValid(isValid); + setAccount(formatted); + + if (isValid) { + handleChange(formatted); + } else { + handleChange(null); + } + }; + + // Adjust width as ref values change. + useEffect(() => { + handleAdjustWidth(); + }, [hiddenRef.current, showingRef.current, payee.destination]); + + // Adjust width on window resize. + useEffect(() => { + window.addEventListener('resize', handleAdjustWidth); + return () => { + window.removeEventListener('resize', handleAdjustWidth); + }; + }, []); + + // Show empty Identicon on `None` and invalid `Account` accounts. + const showEmpty = + payee.destination === 'None' || (payee.destination === 'Account' && !valid); + + const accountDisplay = + payee.destination === 'Account' + ? account + : payee.destination === 'None' + ? '' + : payee.destination === 'Controller' + ? controller + : activeAccount; + + const placeholderDisplay = + payee.destination === 'None' ? t('noPayoutAddress') : t('payoutAddress'); + + return ( + <> + +
+

{t('payoutAccount')}:

+
+ {showEmpty ? ( +
+ ) : ( + + )} +
+ setInputActive(true)} + onBlur={() => setInputActive(false)} + onChange={handleChangeAccount} + /> +
+ {payee.destination === 'Account' + ? activeAccount + : accountDisplay} +
+
+
+
+
+
+ {payee.destination === 'Account' ? ( + <> + {account === '' ? ( + t('insertPayoutAddress') + ) : !valid ? ( + t('notValidAddress') + ) : ( + <> + + {t('validAddress')} + + )} + + ) : payee.destination === 'None' ? null : ( + <> + + {accountMeta?.name || ''} + + )} +
+
+ + + ); +}; diff --git a/src/library/PayeeInput/types.ts b/src/library/PayeeInput/types.ts new file mode 100644 index 0000000000..e8d971af5b --- /dev/null +++ b/src/library/PayeeInput/types.ts @@ -0,0 +1,13 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { Dispatch, SetStateAction } from 'react'; +import type { PayeeConfig } from 'contexts/Setup/types'; +import type { MaybeAddress } from 'types'; + +export interface PayeeInputProps { + payee: PayeeConfig; + account: MaybeAddress; + setAccount: Dispatch>; + handleChange: (a: MaybeAddress) => void; +} diff --git a/src/library/PluginLabel/Wrapper.ts b/src/library/PluginLabel/Wrapper.ts new file mode 100644 index 0000000000..45e48e96a1 --- /dev/null +++ b/src/library/PluginLabel/Wrapper.ts @@ -0,0 +1,23 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div<{ $active: boolean }>` + position: absolute; + right: 10px; + top: 10px; + font-size: 0.9rem; + border-radius: 0.3rem; + padding: 0.25rem 0.4rem; + color: ${(props) => + props.$active + ? 'var(--accent-color-primary)' + : 'var(--text-color-secondary)'}; + opacity: ${(props) => (props.$active ? 1 : 0.5)}; + z-index: 2; + + > svg { + margin-right: 0.3rem; + } +`; diff --git a/src/library/PluginLabel/index.tsx b/src/library/PluginLabel/index.tsx new file mode 100644 index 0000000000..d01ba11347 --- /dev/null +++ b/src/library/PluginLabel/index.tsx @@ -0,0 +1,20 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { usePlugins } from 'contexts/Plugins'; +import { capitalizeFirstLetter } from '@polkadot-cloud/utils'; +import { Wrapper } from './Wrapper'; +import type { PluginLabelProps } from './types'; + +export const PluginLabel = ({ plugin }: PluginLabelProps) => { + const { plugins } = usePlugins(); + + return ( + + + {capitalizeFirstLetter(plugin)} + + ); +}; diff --git a/src/library/PluginLabel/types.ts b/src/library/PluginLabel/types.ts new file mode 100644 index 0000000000..8eabbec574 --- /dev/null +++ b/src/library/PluginLabel/types.ts @@ -0,0 +1,8 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { Plugin } from 'types'; + +export interface PluginLabelProps { + plugin: Plugin; +} diff --git a/src/library/Pool/Rewards.tsx b/src/library/Pool/Rewards.tsx new file mode 100644 index 0000000000..11ac4d90e6 --- /dev/null +++ b/src/library/Pool/Rewards.tsx @@ -0,0 +1,165 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { + TooltipTrigger, + ValidatorPulseWrapper, +} from 'library/ListItem/Wrappers'; +import { useTooltip } from 'contexts/Tooltip'; +import { MaxEraRewardPointsEras } from 'consts'; +import { useApi } from 'contexts/Api'; +import { + normaliseEraPoints, + prefillEraPoints, +} from 'library/ValidatorList/ValidatorItem/Utils'; +import type { AnyJson } from '@polkadot-cloud/react/types'; +import { usePoolPerformance } from 'contexts/Pools/PoolPerformance'; +import { useTranslation } from 'react-i18next'; +import type { RewardProps, RewardsGraphProps } from './types'; + +export const Rewards = ({ address, displayFor = 'default' }: RewardProps) => { + const { t } = useTranslation('library'); + const { isReady } = useApi(); + const { setTooltipTextAndOpen } = useTooltip(); + const { eraPointsBoundaries } = useValidators(); + const { poolRewardPoints, poolRewardPointsFetched } = usePoolPerformance(); + + const eraRewardPoints = Object.fromEntries( + Object.entries(poolRewardPoints[address] || {}).map(([k, v]: AnyJson) => [ + k, + new BigNumber(v), + ]) + ); + + const high = eraPointsBoundaries?.high || new BigNumber(1); + const normalisedPoints = normaliseEraPoints(eraRewardPoints, high); + const prefilledPoints = prefillEraPoints(Object.values(normalisedPoints)); + + const empty = Object.values(poolRewardPoints).length === 0; + const syncing = !isReady || poolRewardPointsFetched !== 'synced'; + const tooltipText = `${MaxEraRewardPointsEras} ${t('dayPoolPerformance')}`; + + return ( + + {syncing &&
} + setTooltipTextAndOpen(tooltipText)} + /> + + + ); +}; + +export const RewardsGraph = ({ points = [], syncing }: RewardsGraphProps) => { + const totalSegments = points.length - 1; + const vbWidth = 512; + const vbHeight = 115; + const xPadding = 5; + const yPadding = 10; + const xArea = vbWidth - 2 * xPadding; + const yArea = vbHeight - 2 * yPadding; + const xSegment = xArea / totalSegments; + let xCursor = xPadding; + + const pointsCoords = points.map((point: number) => { + const coord = { + x: xCursor, + y: vbHeight - yPadding - yArea * point, + zero: point === 0, + }; + xCursor += xSegment; + return coord; + }); + + const lineCoords = []; + for (let i = 0; i <= pointsCoords.length - 1; i++) { + const startZero = pointsCoords[i].zero; + const endZero = pointsCoords[i + 1]?.zero; + + lineCoords.push({ + x1: pointsCoords[i].x, + y1: pointsCoords[i].y, + x2: pointsCoords[i + 1]?.x || pointsCoords[i].x, + y2: pointsCoords[i + 1]?.y || pointsCoords[i].y, + zero: startZero && endZero, + }); + } + + const barCoords = []; + for (let i = 0; i <= pointsCoords.length - 1; i++) { + barCoords.push({ + x1: pointsCoords[i].x, + y1: vbHeight - yPadding, + x2: pointsCoords[i].x, + y2: pointsCoords[i]?.y, + }); + } + + return ( + + {!syncing && + [{ y1: vbHeight * 0.5, y2: vbHeight * 0.5 }].map( + ({ y1, y2 }, index) => { + return ( + + ); + } + )} + + {!syncing && + barCoords.map(({ x1, y1, x2, y2 }, index) => { + return ( + + ); + })} + + {!syncing && + lineCoords.map(({ x1, y1, x2, y2, zero }, index) => { + return ( + + ); + })} + + ); +}; diff --git a/src/library/Pool/index.tsx b/src/library/Pool/index.tsx new file mode 100644 index 0000000000..98ed0be7f7 --- /dev/null +++ b/src/library/Pool/index.tsx @@ -0,0 +1,173 @@ +// Copyright 2023 @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 { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMenu } from 'contexts/Menu'; +import { useNotifications } from 'contexts/Notifications'; +import type { NotificationText } from 'contexts/Notifications/types'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { useUi } from 'contexts/UI'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { usePoolCommission } from 'library/Hooks/usePoolCommission'; +import { FavoritePool } from 'library/ListItem/Labels/FavoritePool'; +import { PoolBonded } from 'library/ListItem/Labels/PoolBonded'; +import { PoolCommission } from 'library/ListItem/Labels/PoolCommission'; +import { PoolIdentity } from 'library/ListItem/Labels/PoolIdentity'; +import { + Labels, + MenuPosition, + Separator, + Wrapper, +} from 'library/ListItem/Wrappers'; +import { usePoolsTabs } from 'pages/Pools/Home/context'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +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'; + +export const Pool = ({ pool, batchKey, batchIndex }: PoolProps) => { + const { t } = useTranslation('library'); + const { memberCounter, addresses, id, state } = pool; + const { isPoolSyncing } = useUi(); + const { meta } = useBondedPools(); + const { validators } = useValidators(); + const { setActiveTab } = usePoolsTabs(); + const { openModal } = useOverlay().modal; + const { membership } = usePoolMemberships(); + const { activeAccount } = useActiveAccounts(); + const { addNotification } = useNotifications(); + const { isReadOnlyAccount } = useImportedAccounts(); + const { getCurrentCommission } = usePoolCommission(); + const { setMenuPosition, setMenuItems, open }: any = useMenu(); + + const currentCommission = getCurrentCommission(id); + + // get metadata from pools metabatch + const nominations = meta[batchKey]?.nominations ?? []; + + // get pool targets from nominations metadata + const targets = nominations[batchIndex]?.targets ?? []; + + // extract validator entries from pool targets + const targetValidators = validators.filter(({ address }) => + targets.includes(address) + ); + + // configure floating menu position + const posRef = useRef(null); + + // copy address notification + const notificationCopyAddress: NotificationText | null = + addresses.stash == null + ? null + : { + title: t('addressCopiedToClipboard'), + subtitle: addresses.stash, + }; + + // consruct pool menu items + const menuItems: any[] = []; + + // add view pool nominations button to menu + menuItems.push({ + icon: , + wrap: null, + title: `${t('viewPoolNominations')}`, + cb: () => { + openModal({ + key: 'PoolNominations', + options: { + nominator: addresses.stash, + targets: targetValidators, + }, + }); + }, + }); + + // add copy pool address button to menu + menuItems.push({ + icon: , + wrap: null, + title: t('copyPoolAddress'), + cb: () => { + navigator.clipboard.writeText(addresses.stash); + if (notificationCopyAddress) { + addNotification(notificationCopyAddress); + } + }, + }); + + // toggle menu handler + const toggleMenu = () => { + if (!open) { + setMenuItems(menuItems); + setMenuPosition(posRef); + } + }; + + const displayJoin = + !isPoolSyncing && + state === 'Open' && + !membership && + !isReadOnlyAccount(activeAccount) && + activeAccount; + + return ( + +
+ +
+ +
+ + +
+ +
+
+
+
+ +
+
+ +
+
+ + {currentCommission > 0 && ( + + )} + + + + + {displayJoin && ( + + + + )} +
+
+
+
+ ); +}; diff --git a/src/library/Pool/types.ts b/src/library/Pool/types.ts new file mode 100644 index 0000000000..acaf7ae0b0 --- /dev/null +++ b/src/library/Pool/types.ts @@ -0,0 +1,30 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { PoolAddresses, PoolRoles, PoolState } from 'contexts/Pools/types'; +import type { DisplayFor } from 'types'; + +export interface PoolProps { + pool: Pool; + batchKey: string; + batchIndex: number; +} + +export interface Pool { + points: string; + memberCounter: string; + addresses: PoolAddresses; + id: number; + state: PoolState; + roles: PoolRoles; +} + +export interface RewardProps { + address: string; + displayFor?: DisplayFor; +} + +export interface RewardsGraphProps { + points: number[]; + syncing: boolean; +} diff --git a/src/library/PoolList/Default.tsx b/src/library/PoolList/Default.tsx new file mode 100644 index 0000000000..9c470d015b --- /dev/null +++ b/src/library/PoolList/Default.tsx @@ -0,0 +1,282 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBars, faGripVertical } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { isNotZero } from '@polkadot-cloud/utils'; +import { motion } from 'framer-motion'; +import React, { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ListItemsPerBatch, ListItemsPerPage } from 'consts'; +import { useApi } from 'contexts/Api'; +import { useFilters } from 'contexts/Filters'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { useTheme } from 'contexts/Themes'; +import { useUi } from 'contexts/UI'; +import { Tabs } from 'library/Filter/Tabs'; +import { usePoolFilters } from 'library/Hooks/usePoolFilters'; +import { + FilterHeaderWrapper, + List, + ListStatusHeader, + Wrapper as ListWrapper, +} from 'library/List'; +import { MotionContainer } from 'library/List/MotionContainer'; +import { Pagination } from 'library/List/Pagination'; +import { SearchInput } from 'library/List/SearchInput'; +import { Pool } from 'library/Pool'; +import { useNetwork } from 'contexts/Network'; +import { usePoolList } from './context'; +import type { PoolListProps } from './types'; + +export const PoolList = ({ + allowMoreCols, + pagination, + batchKey = '', + disableThrottle, + allowSearch, + pools, + defaultFilters, + allowListFormat = true, +}: PoolListProps) => { + const { t } = useTranslation('library'); + const { mode } = useTheme(); + const { isReady } = useApi(); + const { + networkData: { colors }, + } = useNetwork(); + const { isSyncing } = useUi(); + const { applyFilter } = usePoolFilters(); + const { activeEra } = useNetworkMetrics(); + const { listFormat, setListFormat } = usePoolList(); + const { getFilters, setMultiFilters, getSearchTerm, setSearchTerm } = + useFilters(); + const { fetchPoolsMetaBatch, poolSearchFilter, meta } = useBondedPools(); + + const includes = getFilters('include', 'pools'); + const excludes = getFilters('exclude', 'pools'); + const searchTerm = getSearchTerm('pools'); + + // current page + const [page, setPage] = useState(1); + + // current render iteration + const [renderIteration, setRenderIterationState] = useState(1); + + // default list of pools + const [poolsDefault, setPoolsDefault] = useState(pools); + + // manipulated list (ordering, filtering) of pools + const [listPools, setListPools] = useState(pools); + + // 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); + }; + + // pagination + const totalPages = Math.ceil(listPools.length / ListItemsPerPage); + const pageEnd = page * ListItemsPerPage - 1; + const pageStart = pageEnd - (ListItemsPerPage - 1); + + // render batch + const batchEnd = Math.min( + renderIteration * ListItemsPerBatch - 1, + ListItemsPerPage + ); + + // get throttled subset or entire list + const poolsToDisplay = disableThrottle + ? listPools + : listPools.slice(pageStart).slice(0, ListItemsPerPage); + + // handle pool list bootstrapping + const setupPoolList = () => { + setPoolsDefault(pools); + setListPools(pools); + setFetched(true); + fetchPoolsMetaBatch(batchKey, pools, true); + }; + + // handle filter / order update + const handlePoolsFilterUpdate = ( + filteredPools: any = Object.assign(poolsDefault) + ) => { + filteredPools = applyFilter(includes, excludes, filteredPools, batchKey); + if (searchTerm) { + filteredPools = poolSearchFilter(filteredPools, batchKey, searchTerm); + } + setListPools(filteredPools); + setPage(1); + setRenderIteration(1); + }; + + const handleSearchChange = (e: React.FormEvent) => { + const newValue = e.currentTarget.value; + let filteredPools = Object.assign(poolsDefault); + filteredPools = applyFilter(includes, excludes, filteredPools, batchKey); + filteredPools = poolSearchFilter(filteredPools, batchKey, newValue); + + // ensure no duplicates + filteredPools = filteredPools.filter( + (value: any, index: number, self: any) => + index === self.findIndex((i: any) => 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. + useEffect(() => { + if (isReady && isNotZero(activeEra.index) && !fetched) { + setupPoolList(); + } + }, [isReady, fetched, activeEra.index]); + + // Render throttling. Only render a batch of pools at a time. + useEffect(() => { + if (!(batchEnd >= pageEnd || disableThrottle)) { + setTimeout(() => { + setRenderIteration(renderIterationRef.current + 1); + }, 500); + } + }, [renderIterationRef.current]); + + // List ui changes / validator changes trigger re-render of list. + useEffect(() => { + // only filter when pool nominations have been synced. + if (!isSyncing && meta[batchKey]?.nominations) { + handlePoolsFilterUpdate(); + } + }, [isSyncing, includes, excludes, meta]); + + // Scroll to top of the window on every filter. + useEffect(() => { + 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); + } + }, []); + + return ( + + + {allowSearch && poolsDefault.length > 0 && ( + + )} + +
+ +
+
+ {allowListFormat && ( +
+ + +
+ )} +
+
+ + {pagination && poolsToDisplay.length > 0 && ( + + )} + + {poolsToDisplay.length ? ( + <> + {poolsToDisplay.map((pool: any, index: number) => ( + + + + ))} + + ) : ( + + {isSyncing ? `${t('syncingPoolList')}...` : t('noMatch')} + + )} + +
+
+ ); +}; diff --git a/src/library/PoolList/context.tsx b/src/library/PoolList/context.tsx new file mode 100644 index 0000000000..f441f39664 --- /dev/null +++ b/src/library/PoolList/context.tsx @@ -0,0 +1,33 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useState } from 'react'; +import { defaultListFormat, defaultPoolList } from './defaults'; +import type { ListFormat, PoolListContextProps } from './types'; + +export const PoolListContext: React.Context = + React.createContext(defaultPoolList); + +export const usePoolList = () => React.useContext(PoolListContext); + +export const PoolListProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [listFormat, setListFormatState] = + useState(defaultListFormat); + + const setListFormat = (v: ListFormat) => setListFormatState(v); + + return ( + + {children} + + ); +}; diff --git a/src/library/PoolList/defaults.ts b/src/library/PoolList/defaults.ts new file mode 100644 index 0000000000..ffc885bc74 --- /dev/null +++ b/src/library/PoolList/defaults.ts @@ -0,0 +1,12 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import type { PoolListContextProps } from './types'; + +export const defaultListFormat = 'col'; + +export const defaultPoolList: PoolListContextProps = { + setListFormat: (v) => {}, + listFormat: defaultListFormat, +}; diff --git a/src/library/PoolList/types.ts b/src/library/PoolList/types.ts new file mode 100644 index 0000000000..e99b0de132 --- /dev/null +++ b/src/library/PoolList/types.ts @@ -0,0 +1,23 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export type ListFormat = 'row' | 'col'; + +export interface PoolListContextProps { + setListFormat: (v: ListFormat) => void; + listFormat: ListFormat; +} +export interface PoolListProps { + allowMoreCols?: boolean; + allowSearch?: boolean; + pagination?: boolean; + batchKey?: string; + disableThrottle?: boolean; + refetchOnListUpdate?: string; + allowListFormat?: boolean; + pools?: any; + defaultFilters?: { + includes: string[] | null; + excludes: string[] | null; + }; +} diff --git a/src/library/Prompt/Title.tsx b/src/library/Prompt/Title.tsx new file mode 100644 index 0000000000..95d287958b --- /dev/null +++ b/src/library/Prompt/Title.tsx @@ -0,0 +1,59 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ButtonHelp, ButtonSecondary } from '@polkadot-cloud/react'; +import type { FunctionComponent } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { usePrompt } from 'contexts/Prompt'; +import { TitleWrapper } from './Wrappers'; + +interface TitleProps { + title: string; + icon?: IconProp; + Svg?: FunctionComponent; + helpKey?: string; + hideDone?: boolean; + closeText?: string; +} + +export const Title = ({ + helpKey, + title, + icon, + Svg, + hideDone, + closeText, +}: TitleProps) => { + const { t } = useTranslation('library'); + const { closePrompt } = usePrompt(); + const { openHelp } = useHelp(); + + const graphic = Svg ? ( + + ) : icon ? ( + + ) : null; + + return ( + +
+ {graphic} +

+ {title} + {helpKey ? openHelp(helpKey)} /> : null} +

+
+ {hideDone !== true ? ( +
+ closePrompt()} + /> +
+ ) : null} +
+ ); +}; diff --git a/src/library/Prompt/Wrappers.ts b/src/library/Prompt/Wrappers.ts new file mode 100644 index 0000000000..f73c3466f9 --- /dev/null +++ b/src/library/Prompt/Wrappers.ts @@ -0,0 +1,201 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const PromptWrapper = styled.div` + background: var(--overlay-background-color); + position: fixed; + width: 100%; + height: 100%; + z-index: 11; + + /* content wrapper */ + > div { + height: 100%; + display: flex; + flex-flow: row wrap; + justify-content: center; + align-items: center; + padding: 2rem 2rem; + + /* click anywhere behind overlay to close */ + .close { + position: fixed; + width: 100%; + height: 100%; + z-index: 8; + cursor: default; + } + + /* status message placed below title */ + h4.subheading { + margin-bottom: 1rem; + } + + /* padded content to give extra spacing */ + .padded { + padding: 1rem 1.5rem; + } + } +`; + +export const HeightWrapper = styled.div<{ size: string }>` + transition: height 0.5s cubic-bezier(0.1, 1, 0.2, 1); + width: 100%; + max-width: ${(props) => (props.size === 'small' ? '500px' : '700px')}; + max-height: 100%; + border-radius: 1.5rem; + z-index: 9; + position: relative; + overflow: auto; +`; + +export const ContentWrapper = styled.div` + background: var(--background-default); + width: 100%; + height: auto; + overflow: hidden; + position: relative; + + a { + color: var(--accent-color-primary); + } + .header { + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 1rem 2rem 0 2rem; + } + .body { + padding: 0.5rem 1.5rem 1.25rem 1.5rem; + h4 { + margin: 1rem 0; + } + } +`; + +export const TitleWrapper = styled.div` + padding: 1.5rem 1rem 0 1rem; + display: flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + + > div { + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 0 0.5rem; + + button { + padding: 0; + } + + path { + fill: var(--text-color-primary); + } + + &:first-child { + flex-grow: 1; + + > h2 { + display: flex; + align-items: center; + font-family: 'Unbounded', 'sans-serif', sans-serif; + font-size: 1.3rem; + + > button { + margin-left: 0.85rem; + } + } + > svg { + margin-right: 0.9rem; + } + } + } +`; + +export const FilterListWrapper = styled.div` + padding-bottom: 0.5rem; + + > .body { + button:last-child { + margin-bottom: 0; + } + } +`; + +export const FilterListButton = styled.button<{ $active: boolean }>` + border: 1px solid + ${(props) => + props.$active + ? 'var(--accent-color-stroke)' + : 'var(--button-primary-background)'}; + background: var(--button-primary-background); + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: center; + border-radius: 1rem; + padding: 0rem 1rem; + margin: 1rem 0; + transition: border var(--transition-duration); + + h4 { + color: ${(props) => + props.$active + ? 'var(--accent-color-stroke)' + : 'var(--text-color-secondary)'}; + transition: color var(--transition-duration); + } + + svg { + color: ${(props) => + props.$active + ? 'var(--accent-color-stroke)' + : 'var(--text-color-secondary)'}; + opacity: ${(props) => (props.$active ? 1 : 0.7)}; + transition: color var(--transition-duration); + margin-left: 0.2rem; + margin-right: 0.9rem; + } +`; + +export const FooterWrapper = styled.div` + margin: 1.5rem 0 0.5rem 0; +`; + +export const PromptListItem = styled.div` + display: flex; + align-items: center; + border-bottom: 1px solid var(--border-primary-color); + + &.inactive { + opacity: var(--opacity-disabled); + } +`; + +export const PromptSelectItem = styled.button` + border-bottom: 1px solid var(--border-primary-color); + display: flex; + flex-direction: column; + align-items: flex-start; + padding: 1rem 0.5rem; + border-radius: 0.25rem; + width: 100%; + + > h4 { + margin-top: 0.3rem; + } + &:hover { + background: var(--button-hover-background); + } + &.inactive { + h3, + h4 { + color: var(--accent-color-primary); + } + } +`; diff --git a/src/library/Prompt/index.tsx b/src/library/Prompt/index.tsx new file mode 100644 index 0000000000..55c6842cdb --- /dev/null +++ b/src/library/Prompt/index.tsx @@ -0,0 +1,26 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { usePrompt } from 'contexts/Prompt'; +import { ContentWrapper, HeightWrapper, PromptWrapper } from './Wrappers'; + +export const Prompt = () => { + const { closePrompt, size, status, Prompt: PromptInner } = usePrompt(); + + if (status === 0) { + return <>; + } + + return ( + +
+ + {PromptInner} + + +
+
+ ); +}; diff --git a/src/library/QRCode/Display.tsx b/src/library/QRCode/Display.tsx new file mode 100644 index 0000000000..7563ca79fa --- /dev/null +++ b/src/library/QRCode/Display.tsx @@ -0,0 +1,121 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { objectSpread } from '@polkadot/util'; +import { xxhashAsHex } from '@polkadot/util-crypto'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { DisplayWrapper } from './Wrappers.js'; +import { qrcode } from './qrcode.js'; +import type { DisplayProps, FrameState, TimerState } from './types.js'; +import { createFrames, createImgSize } from './util.js'; + +const DEFAULT_FRAME_DELAY = 2750; +const TIMER_INC = 500; + +const getDataUrl = (value: Uint8Array): string => { + const qr = qrcode(0, 'M'); + + // HACK See our qrcode stringToBytes override as used internally. This + // will only work for the case where we actually pass `Bytes` in here + qr.addData(value as unknown as string, 'Byte'); + qr.make(); + + return qr.createDataURL(16, 0); +}; + +const Display = ({ + className = '', + size, + skipEncoding, + style = {}, + timerDelay = DEFAULT_FRAME_DELAY, + value, +}: DisplayProps): React.ReactElement | null => { + const [{ image }, setFrameState] = useState({ + frameIdx: 0, + frames: [], + image: null, + valueHash: null, + }); + const timerRef = useRef({ timerDelay, timerId: null }); + + const containerStyle = useMemo(() => createImgSize(size), [size]); + + // run on initial load to setup the global timer and provide and unsubscribe + useEffect((): (() => void) => { + const nextFrame = () => + setFrameState((state): FrameState => { + // when we have a single frame, we only ever fire once + if (state.frames.length <= 1) { + return state; + } + + let frameIdx = state.frameIdx + 1; + + // when we overflow, skip to the first and slightly increase the delay between frames + if (frameIdx === state.frames.length) { + frameIdx = 0; + timerRef.current.timerDelay += TIMER_INC; + } + + // only encode the frames on demand, not above as part of the + // state derivation - in the case of large payloads, this should + // be slightly more responsive on initial load + const newState = objectSpread({}, state, { + frameIdx, + image: getDataUrl(state.frames[frameIdx]), + }); + + // set the new timer last + timerRef.current.timerId = setTimeout( + nextFrame, + timerRef.current.timerDelay + ); + + return newState; + }); + + timerRef.current.timerId = setTimeout( + nextFrame, + timerRef.current.timerDelay + ); + + return () => { + if (timerRef.current.timerId) clearTimeout(timerRef.current.timerId); + }; + }, []); + + useEffect((): void => { + setFrameState((state): FrameState => { + const valueHash = xxhashAsHex(value); + + if (valueHash === state.valueHash) { + return state; + } + + const frames: Uint8Array[] = skipEncoding ? [value] : createFrames(value); + + // encode on demand + return { + frameIdx: 0, + frames, + image: getDataUrl(frames[0]), + valueHash, + }; + }); + }, [skipEncoding, value]); + + if (!image) { + return null; + } + + return ( + +
+ img +
+
+ ); +}; + +export const QrDisplay = React.memo(Display); diff --git a/src/library/QRCode/DisplayPayload.tsx b/src/library/QRCode/DisplayPayload.tsx new file mode 100644 index 0000000000..583eeb2735 --- /dev/null +++ b/src/library/QRCode/DisplayPayload.tsx @@ -0,0 +1,39 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useMemo } from 'react'; +import { QrDisplay } from './Display.js'; +import type { DisplayPayloadProps } from './types.js'; +import { createSignPayload } from './util.js'; + +const DisplayPayload = ({ + address, + className, + cmd, + genesisHash, + payload, + size, + style, + timerDelay, +}: DisplayPayloadProps): React.ReactElement | null => { + const data = useMemo( + () => createSignPayload(address, cmd, payload, genesisHash), + [address, cmd, payload, genesisHash] + ); + + if (!data) { + return null; + } + + return ( + + ); +}; + +export const QrDisplayPayload = React.memo(DisplayPayload); diff --git a/src/library/QRCode/Scan.tsx b/src/library/QRCode/Scan.tsx new file mode 100644 index 0000000000..93ec11c64d --- /dev/null +++ b/src/library/QRCode/Scan.tsx @@ -0,0 +1,49 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useCallback, useMemo } from 'react'; +import Reader from 'react-qr-reader'; +import { ScanWrapper } from './Wrappers.js'; +import type { ScanProps } from './types.js'; +import { createImgSize } from './util.js'; + +const DEFAULT_DELAY = 150; + +const DEFAULT_ERROR = (error: Error): void => { + throw new Error(error.message); +}; + +const Scan = ({ + className = '', + delay = DEFAULT_DELAY, + onError = DEFAULT_ERROR, + onScan, + size, + style = {}, +}: ScanProps): React.ReactElement => { + const containerStyle = useMemo(() => createImgSize(size), [size]); + + const onErrorCallback = useCallback( + (error: Error) => onError(error), + [onError] + ); + + const onScanCallback = useCallback( + (data: string | null) => data && onScan(data), + [onScan] + ); + + return ( + + + + ); +}; + +export const QrScan = React.memo(Scan); diff --git a/src/library/QRCode/ScanSignature.tsx b/src/library/QRCode/ScanSignature.tsx new file mode 100644 index 0000000000..1188a1d8f0 --- /dev/null +++ b/src/library/QRCode/ScanSignature.tsx @@ -0,0 +1,32 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useCallback } from 'react'; +import { QrScan } from './Scan.js'; +import type { ScanSignatureProps } from './types.js'; + +const ScanSignature = ({ + className, + onError, + onScan, + size, + style, +}: ScanSignatureProps): React.ReactElement => { + const onScanCallback = useCallback( + (signature: string | null) => + signature && onScan({ signature: `0x${signature}` }), + [onScan] + ); + + return ( + + ); +}; + +export const QrScanSignature = React.memo(ScanSignature); diff --git a/src/library/QRCode/Wrappers.ts b/src/library/QRCode/Wrappers.ts new file mode 100644 index 0000000000..5b983bb502 --- /dev/null +++ b/src/library/QRCode/Wrappers.ts @@ -0,0 +1,33 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const DisplayWrapper = styled.div` + .ui--qr-Display { + height: 100%; + width: 100%; + + img, + svg { + background: white; + height: auto !important; + max-height: 100%; + max-width: 100%; + width: auto !important; + } + } +`; + +export const ScanWrapper = styled.div` + .ui--qr-Scan { + display: inline-block; + height: 100%; + transform: matrix(-1, 0, 0, 1, 0, 0); + width: 100%; + + video { + margin: 0; + } + } +`; diff --git a/src/library/QRCode/constants.ts b/src/library/QRCode/constants.ts new file mode 100644 index 0000000000..cc998ab00b --- /dev/null +++ b/src/library/QRCode/constants.ts @@ -0,0 +1,8 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export const ADDRESS_PREFIX = 'substrate'; +export const SEED_PREFIX = 'secret'; +export const FRAME_SIZE = 1024; +export const SUBSTRATE_ID = new Uint8Array([0x53]); +export const CRYPTO_SR25519 = new Uint8Array([0x01]); diff --git a/src/library/QRCode/qrcode.ts b/src/library/QRCode/qrcode.ts new file mode 100644 index 0000000000..75cfb35df2 --- /dev/null +++ b/src/library/QRCode/qrcode.ts @@ -0,0 +1,13 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import _qrcode from 'qrcode-generator'; + +// A small hurdle to jump through, just to get the default/default correct (as generated) +const qrcode: typeof _qrcode = _qrcode; + +// HACK The default function take string -> number[], the Uint8array is compatible +// with that signature and the use thereof +(qrcode as any).stringToBytes = (data: Uint8Array): Uint8Array => data; + +export { qrcode }; diff --git a/src/library/QRCode/types.ts b/src/library/QRCode/types.ts new file mode 100644 index 0000000000..5a8812a08f --- /dev/null +++ b/src/library/QRCode/types.ts @@ -0,0 +1,58 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { HexString } from '@polkadot/util/types'; +import type React from 'react'; + +export interface FrameState { + frames: Uint8Array[]; + frameIdx: number; + image: string | null; + valueHash: string | null; +} + +export interface ScanType { + signature: HexString; +} + +export interface TimerState { + timerDelay: number; + timerId: ReturnType | null; +} + +export interface DisplayProps { + className?: string | undefined; + size?: string | number | undefined; + skipEncoding?: boolean; + style?: React.CSSProperties | undefined; + timerDelay?: number | undefined; + value: Uint8Array; +} + +export interface DisplayPayloadProps { + address: string; + className?: string; + cmd: number; + genesisHash: Uint8Array | string; + payload: Uint8Array; + size?: string | number; + style?: React.CSSProperties; + timerDelay?: number; +} + +export interface ScanProps { + className?: string | undefined; + delay?: number; + onError?: undefined | ((error: Error) => void); + onScan: (data: string) => void; + size?: string | number | undefined; + style?: React.CSSProperties | undefined; +} + +export interface ScanSignatureProps { + className?: string; + onError?: (error: Error) => void; + onScan: (scanned: ScanType) => void; + size?: string | number; + style?: React.CSSProperties; +} diff --git a/src/library/QRCode/util.ts b/src/library/QRCode/util.ts new file mode 100644 index 0000000000..b7d5577275 --- /dev/null +++ b/src/library/QRCode/util.ts @@ -0,0 +1,77 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { isString, u8aConcat, u8aToU8a } from '@polkadot/util'; +import { decodeAddress } from '@polkadot/util-crypto'; +import { CRYPTO_SR25519, FRAME_SIZE, SUBSTRATE_ID } from './constants.js'; + +const MULTIPART = new Uint8Array([0]); + +export const encodeNumber = (value: number): Uint8Array => + // eslint-disable-next-line no-bitwise + new Uint8Array([value >> 8, value & 0xff]); + +export const encodeString = (value: string): Uint8Array => { + const count = value.length; + const u8a = new Uint8Array(count); + + for (let i = 0; i < count; i++) { + u8a[i] = value.charCodeAt(i); + } + + return u8a; +}; + +export const createSignPayload = ( + address: string, + cmd: number, + payload: string | Uint8Array, + genesisHash: string | Uint8Array +): Uint8Array => + u8aConcat( + SUBSTRATE_ID, + CRYPTO_SR25519, + new Uint8Array([cmd]), + decodeAddress(address), + u8aToU8a(payload), + u8aToU8a(genesisHash) + ); + +export const createFrames = (input: Uint8Array): Uint8Array[] => { + const frames = []; + let idx = 0; + + while (idx < input.length) { + frames.push(input.subarray(idx, idx + FRAME_SIZE)); + + idx += FRAME_SIZE; + } + + return frames.map( + (frame, index: number): Uint8Array => + u8aConcat( + MULTIPART, + encodeNumber(frames.length), + encodeNumber(index), + frame + ) + ); +}; + +export const createImgSize = ( + size?: string | number +): Record => { + if (!size) { + return { + height: 'auto', + width: '100%', + }; + } + + const height = isString(size) ? size : `${size}px`; + + return { + height, + width: height, + }; +}; diff --git a/src/library/SelectItems/Item.tsx b/src/library/SelectItems/Item.tsx new file mode 100644 index 0000000000..d0231af0b8 --- /dev/null +++ b/src/library/SelectItems/Item.tsx @@ -0,0 +1,50 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCircle } from '@fortawesome/free-regular-svg-icons'; +import { faCircleCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Wrapper } from './Wrapper'; +import type { SelectItemProps } from './types'; + +export const SelectItem = ({ + title, + subtitle, + icon, + selected, + onClick, + layout, + hoverBorder = false, + grow = true, + disabled = false, + includeToggle = true, + bodyRef, + containerRef, +}: SelectItemProps) => ( + +
+ +
+
+); diff --git a/src/library/SelectItems/Wrapper.ts b/src/library/SelectItems/Wrapper.ts new file mode 100644 index 0000000000..b7f6fb9b90 --- /dev/null +++ b/src/library/SelectItems/Wrapper.ts @@ -0,0 +1,161 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const TwoThreshold = 800; +export const ThreeRowThreshold = 1300; + +const TwoThresholdMin = TwoThreshold + 1; +const ThreeRowThresholdMin = ThreeRowThreshold + 1; + +// The outer container of select items. +// Removes the outer padding for 2 and 3 row layouts. +export const SelectItemsWrapper = styled.div` + width: 100%; + display: flex; + flex-flow: row wrap; + + &.two-col { + /* Remove outer padding for 2-per-row layout */ + @media (min-width: ${TwoThresholdMin}px) { + > div:nth-child(2n) { + padding-right: 0; + } + > div:nth-child(2n + 1) { + padding-left: 0; + } + } + } + + &.three-col { + /* Remove outer padding for 2-per-row layout */ + @media (min-width: ${TwoThresholdMin}px) and (max-width: ${ThreeRowThreshold}px) { + > div:nth-child(2n) { + padding-right: 0; + } + > div:nth-child(2n + 1) { + padding-left: 0; + } + } + /* Remove outer padding for 3-per-row layout */ + @media (min-width: ${ThreeRowThresholdMin}px) { + > div:nth-child(3n) { + padding-right: 0; + } + > div:nth-child(3n + 1) { + padding-left: 0; + } + } + } +`; + +// Item and surrounding padded area. +export const Wrapper = styled.div<{ + $selected?: boolean; + $grow: boolean; + $hoverBorder: boolean; +}>` + padding: 0.6rem; + width: 100%; + flex-grow: ${(props) => (props.$grow ? 1 : 0)}; + + &.two-col { + width: 50%; + /* flex basis for 3-per-row layout */ + @media (max-width: ${TwoThreshold}px) { + width: 100%; + } + } + + &.three-col { + width: 33.33%; + /* flex basis for 2-per-row layout */ + @media (min-width: ${TwoThreshold}px) and (max-width: ${ThreeRowThreshold}px) { + width: 50%; + } + /* flex basis for 3-per-row layout */ + @media (max-width: ${TwoThreshold}px) { + width: 100%; + } + } + + > .inner { + transition: border var(--transition-duration); + background: var(--background-primary); + border: 1.75px solid + ${(props) => + props.$selected + ? 'var(--accent-color-primary)' + : 'var(--border-primary-color)'}; + border-radius: 1rem; + width: 100%; + position: relative; + overflow: hidden; + + &:hover { + border-color: ${(props) => + props.$hoverBorder + ? 'var(--accent-color-primary)' + : props.$selected + ? 'var(--accent-color-primary)' + : 'var(--border-primary-color)'}; + } + + > button { + position: absolute; + top: 0; + left: 0; + width: 100%; + text-align: left; + display: flex; + flex-flow: row wrap; + align-items: center; + + &:disabled { + opacity: var(--opacity-disabled); + } + + > .icon { + background: var(--background-list-item); + color: var(--accent-color-primary); + width: 6rem; + display: flex; + align-items: center; + justify-content: center; + } + + > .body { + flex: 1; + display: flex; + flex-flow: column wrap; + justify-content: center; + padding: 1.25rem 1.35rem; + overflow: hidden; + + h3 { + font-family: InterSemiBold, sans-serif; + padding: 0; + margin: 0; + } + + p { + padding: 0; + margin: 0.4rem 0 0 0; + } + } + + > .toggle { + color: ${(props) => + props.$selected + ? 'var(--accent-color-primary)' + : 'var(--text-color-secondary)'}; + opacity: ${(props) => (props.$selected ? 1 : 0.5)}; + width: 4rem; + display: flex; + align-items: center; + justify-content: center; + } + } + } +`; diff --git a/src/library/SelectItems/index.tsx b/src/library/SelectItems/index.tsx new file mode 100644 index 0000000000..53c980d31c --- /dev/null +++ b/src/library/SelectItems/index.tsx @@ -0,0 +1,92 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { MutableRefObject } from 'react'; +import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import type { AnyJson } from 'types'; +import { SelectItemsWrapper, TwoThreshold } from './Wrapper'; +import type { SelectItemsProps } from './types'; + +export const SelectItems = ({ layout, children }: SelectItemsProps) => { + // Initialise refs for container and body of items. + const containerRefs: MutableRefObject[] = []; + const bodyRefs: MutableRefObject[] = []; + + if (children) { + for (let i = 0; i < children.length; i++) { + bodyRefs.push(useRef(null)); + containerRefs.push(useRef(null)); + } + } + + // Adjust all container heights to be uniform. + const handleAdjustHeight = () => { + const refsInitialised = [...containerRefs] + .concat(bodyRefs) + .every((r: AnyJson) => r !== null); + + if (refsInitialised) { + // Get max height from button refs. + let maxHeight = 0; + for (let i = 0; i < bodyRefs.length; i++) { + const { current } = bodyRefs[i]; + if (current) { + const thisHeight = current.offsetHeight || 0; + if (thisHeight > maxHeight) { + maxHeight = thisHeight; + } + } + } + + // Update container heights to max height. + for (let i = 0; i < containerRefs.length; i++) { + const { current } = containerRefs[i]; + + if (current) { + const icon: AnyJson = current.querySelector('.icon'); + const toggle: AnyJson = current.querySelector('.toggle'); + + if (window.innerWidth <= TwoThreshold) { + current.style.height = `${bodyRefs[i].current.offsetHeight}px`; + if (icon) + icon.style.height = `${bodyRefs[i].current.offsetHeight}px`; + if (toggle) + toggle.style.height = `${bodyRefs[i].current.offsetHeight}px`; + } else { + current.style.height = `${maxHeight}px`; + if (icon) icon.style.height = `${maxHeight}px`; + if (toggle) toggle.style.height = `${maxHeight}px`; + } + } + } + } + }; + + // Update on ref change. + useLayoutEffect(() => { + handleAdjustHeight(); + }, [children, bodyRefs, containerRefs]); + + // Adjust height on window resize. + useEffect(() => { + window.addEventListener('resize', handleAdjustHeight); + return () => { + window.removeEventListener('resize', handleAdjustHeight); + }; + }, []); + + return ( + + {children + ? children.map((child: any, i: number) => ( + + {React.cloneElement(child, { + bodyRef: bodyRefs[i], + containerRef: containerRefs[i], + })} + + )) + : null} + + ); +}; diff --git a/src/library/SelectItems/types.ts b/src/library/SelectItems/types.ts new file mode 100644 index 0000000000..fc88e8867b --- /dev/null +++ b/src/library/SelectItems/types.ts @@ -0,0 +1,28 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type React from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import type { AnyJson, MaybeAddress } from 'types'; + +export interface SelectItemsProps { + layout?: 'two-col' | 'three-col'; + children?: React.ReactNode[]; +} + +export interface SelectItemProps { + title: string; + subtitle: string; + icon: AnyJson; + selected: boolean; + onClick: () => void; + layout?: 'two-col' | 'three-col'; + hoverBorder?: boolean; + grow?: boolean; + disabled?: boolean; + includeToggle?: boolean; + bodyRef?: AnyJson; + containerRef?: AnyJson; + account?: MaybeAddress; + setAccount?: Dispatch>; +} diff --git a/src/library/SetupSteps/Footer/Wrapper.ts b/src/library/SetupSteps/Footer/Wrapper.ts new file mode 100644 index 0000000000..73a1fc7d57 --- /dev/null +++ b/src/library/SetupSteps/Footer/Wrapper.ts @@ -0,0 +1,14 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: flex-end; + padding: 0 0.25rem; + margin-top: 1rem; +`; diff --git a/src/library/SetupSteps/Footer/index.tsx b/src/library/SetupSteps/Footer/index.tsx new file mode 100644 index 0000000000..7a2ecf204b --- /dev/null +++ b/src/library/SetupSteps/Footer/index.tsx @@ -0,0 +1,36 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonPrimary } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useSetup } from 'contexts/Setup'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import type { FooterProps } from '../types'; +import { Wrapper } from './Wrapper'; + +export const Footer = ({ complete, bondFor }: FooterProps) => { + const { t } = useTranslation('library'); + const { activeAccount } = useActiveAccounts(); + const { getSetupProgress, setActiveAccountSetupSection } = useSetup(); + const setup = getSetupProgress(bondFor, activeAccount); + + return ( + +
+ {complete ? ( + + setActiveAccountSetupSection(bondFor, setup.section + 1) + } + /> + ) : ( +
+ +
+ )} +
+
+ ); +}; diff --git a/src/library/SetupSteps/Header/Wrapper.ts b/src/library/SetupSteps/Header/Wrapper.ts new file mode 100644 index 0000000000..bc5db5e3b6 --- /dev/null +++ b/src/library/SetupSteps/Header/Wrapper.ts @@ -0,0 +1,44 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: center; + + > section { + color: var(--text-color-secondary); + display: flex; + flex-flow: row wrap; + align-items: center; + } + + > section:last-child { + flex: 1; + justify-content: flex-end; + + .progress { + color: var(--text-color-secondary); + opacity: 0.5; + } + + .complete { + margin: 0; + color: var(--accent-color-primary); + } + + span { + margin-right: 1rem; + } + } + + h2 { + padding: 0.3rem 0; + display: flex; + flex-flow: row wrap; + align-items: center; + } +`; diff --git a/src/library/SetupSteps/Header/index.tsx b/src/library/SetupSteps/Header/index.tsx new file mode 100644 index 0000000000..dd59fac29d --- /dev/null +++ b/src/library/SetupSteps/Header/index.tsx @@ -0,0 +1,54 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp, ButtonSecondary } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { useSetup } from 'contexts/Setup'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import type { HeaderProps } from '../types'; +import { Wrapper } from './Wrapper'; + +export const Header = ({ + title, + helpKey, + complete, + thisSection, + bondFor, +}: HeaderProps) => { + const { t } = useTranslation('library'); + const { activeAccount } = useActiveAccounts(); + const { getSetupProgress, setActiveAccountSetupSection } = useSetup(); + const setup = getSetupProgress(bondFor, activeAccount); + const { openHelp } = useHelp(); + + return ( + +
+

+ {title} + {helpKey !== undefined ? ( + openHelp(helpKey)} /> + ) : null} +

+
+
+ {complete && ( + <> + {setup.section !== thisSection && thisSection < setup.section && ( + + { + setActiveAccountSetupSection(bondFor, thisSection); + }} + /> + + )} +

{t('complete')}

+ + )} +
+
+ ); +}; diff --git a/src/library/SetupSteps/MotionContainer.tsx b/src/library/SetupSteps/MotionContainer.tsx new file mode 100644 index 0000000000..6a2d4dae7d --- /dev/null +++ b/src/library/SetupSteps/MotionContainer.tsx @@ -0,0 +1,39 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; + +export const MotionContainer = ({ + thisSection, + activeSection, + children, +}: any) => { + // container variants + const containerVariants = { + hidden: { + height: '0px', + }, + visible: { + height: 'auto', + }, + }; + + // animate container default + const animate = thisSection === activeSection ? 'visible' : 'hidden'; + + return ( + + {children} + + ); +}; diff --git a/src/library/SetupSteps/Nominate.tsx b/src/library/SetupSteps/Nominate.tsx new file mode 100644 index 0000000000..cc0748605a --- /dev/null +++ b/src/library/SetupSteps/Nominate.tsx @@ -0,0 +1,62 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useSetup } from 'contexts/Setup'; +import { Footer } from 'library/SetupSteps/Footer'; +import { Header } from 'library/SetupSteps/Header'; +import { MotionContainer } from 'library/SetupSteps/MotionContainer'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Subheading } from 'pages/Nominate/Wrappers'; +import { GenerateNominations } from '../GenerateNominations'; +import type { NominationsProps } from './types'; + +export const Nominate = ({ bondFor, section }: NominationsProps) => { + const { t } = useTranslation('library'); + const { consts } = useApi(); + const { activeAccount } = useActiveAccounts(); + const { getSetupProgress, setActiveAccountSetup } = useSetup(); + const setup = getSetupProgress(bondFor, activeAccount); + const { progress } = setup; + const { maxNominations } = consts; + + // Handler for updating setup. + const handleSetupUpdate = (value: any) => + setActiveAccountSetup(bondFor, value); + + return ( + <> +
0} + title={t('nominate')} + helpKey="Nominating" + bondFor={bondFor} + /> + + +

+ {t('chooseValidators', { + maxNominations: maxNominations.toString(), + })} +

+
+ getSetupProgress(bondFor, activeAccount).progress, + }, + set: handleSetupUpdate, + }, + ]} + nominations={progress.nominations} + /> + +
0} bondFor={bondFor} /> + + + ); +}; diff --git a/src/library/SetupSteps/types.ts b/src/library/SetupSteps/types.ts new file mode 100644 index 0000000000..315bc9e8e1 --- /dev/null +++ b/src/library/SetupSteps/types.ts @@ -0,0 +1,26 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { BondFor } from 'types'; + +export interface NominationsProps { + bondFor: BondFor; + section: number; +} + +export interface FooterProps { + complete: boolean; + bondFor: BondFor; +} + +export interface HeaderProps { + title?: string; + helpKey?: string; + complete?: boolean | null; + thisSection: number; + bondFor: BondFor; +} + +export interface SetupStepProps { + section: number; +} diff --git a/src/library/SideMenu/Heading/Heading.tsx b/src/library/SideMenu/Heading/Heading.tsx new file mode 100644 index 0000000000..816f6ff418 --- /dev/null +++ b/src/library/SideMenu/Heading/Heading.tsx @@ -0,0 +1,11 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { HeadingProps } from '../types'; +import { Wrapper } from './Wrapper'; + +export const Heading = ({ title, minimised }: HeadingProps) => ( + + {minimised ?
:
{title}
} +
+); diff --git a/src/library/SideMenu/Heading/Wrapper.ts b/src/library/SideMenu/Heading/Wrapper.ts new file mode 100644 index 0000000000..882efcc2d2 --- /dev/null +++ b/src/library/SideMenu/Heading/Wrapper.ts @@ -0,0 +1,19 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div<{ $minimised: boolean }>` + display: flex; + flex-flow: row wrap; + justify-content: ${(props) => (props.$minimised ? 'center' : 'flex-start')}; + opacity: ${(props) => (props.$minimised ? 0.5 : 1)}; + align-items: center; + + h5 { + color: var(--text-color-secondary); + margin: 1.1rem 0 0.3rem 0; + padding: 0 0.5rem; + opacity: 0.7; + } +`; diff --git a/src/library/SideMenu/Main.tsx b/src/library/SideMenu/Main.tsx new file mode 100644 index 0000000000..87515c1b12 --- /dev/null +++ b/src/library/SideMenu/Main.tsx @@ -0,0 +1,192 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation } from 'react-router-dom'; +import { PageCategories, PagesConfig } from 'config/pages'; +import { PolkadotUrl } from 'consts'; +import { useBonded } from 'contexts/Bonded'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { useSetup } from 'contexts/Setup'; +import type { SetupContextInterface } from 'contexts/Setup/types'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import type { UIContextInterface } from 'contexts/UI/types'; +import type { PageCategory, PageItem, PagesConfigItems } from 'types'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { Heading } from './Heading/Heading'; +import { Primary } from './Primary'; +import { LogoWrapper } from './Wrapper'; + +export const Main = () => { + const { t, i18n } = useTranslation('base'); + const { networkData } = useNetwork(); + const { pathname } = useLocation(); + const { getBondedAccount } = useBonded(); + const { accounts } = useImportedAccounts(); + const { activeAccount } = useActiveAccounts(); + const { inSetup: inNominatorSetup, addressDifferentToStash } = useStaking(); + const { membership } = usePoolMemberships(); + const controller = getBondedAccount(activeAccount); + const { + onNominatorSetup, + onPoolSetup, + getPoolSetupPercent, + getNominatorSetupPercent, + }: SetupContextInterface = useSetup(); + const { isSyncing, sideMenuMinimised }: UIContextInterface = useUi(); + const controllerDifferentToStash = addressDifferentToStash(controller); + + const [pageConfig, setPageConfig] = useState({ + categories: Object.assign(PageCategories), + pages: Object.assign(PagesConfig), + }); + + useEffect(() => { + if (!accounts.length) return; + + // inject actions into menu items + const pages = Object.assign(pageConfig.pages); + for (let i = 0; i < pages.length; i++) { + const { uri } = pages[i]; + + // set undefined action as default + pages[i].action = undefined; + if (uri === `${import.meta.env.BASE_URL}`) { + const warning = !isSyncing && controllerDifferentToStash; + if (warning) { + pages[i].action = { + type: 'bullet', + status: 'warning', + }; + } + } + + if (uri === `${import.meta.env.BASE_URL}nominate`) { + // configure Stake action + const staking = !inNominatorSetup(); + const warning = !isSyncing && controllerDifferentToStash; + const setupPercent = getNominatorSetupPercent(activeAccount); + + if (staking) { + pages[i].action = { + type: 'text', + status: 'success', + text: t('active'), + }; + } + if (warning) { + pages[i].action = { + type: 'bullet', + status: 'warning', + }; + } + if (!staking && (onNominatorSetup || setupPercent > 0)) { + pages[i].action = { + type: 'text', + status: 'warning', + text: `${setupPercent}%`, + }; + } + } + + if (uri === `${import.meta.env.BASE_URL}pools`) { + // configure Pools action + const inPool = membership; + const setupPercent = getPoolSetupPercent(activeAccount); + + if (inPool) { + pages[i].action = { + type: 'text', + status: 'success', + text: t('active'), + }; + } + if (!inPool && (setupPercent > 0 || onPoolSetup)) { + pages[i].action = { + type: 'text', + status: 'warning', + text: `${setupPercent}%`, + }; + } + } + } + setPageConfig({ + categories: pageConfig.categories, + pages, + }); + }, [ + networkData, + activeAccount, + accounts, + controllerDifferentToStash, + isSyncing, + membership, + inNominatorSetup(), + getNominatorSetupPercent(activeAccount), + getPoolSetupPercent(activeAccount), + i18n.resolvedLanguage, + onNominatorSetup, + onPoolSetup, + ]); + + // remove pages that network does not support + const pagesToDisplay: PagesConfigItems = Object.values(pageConfig.pages); + + return ( + <> + window.open(PolkadotUrl, '_blank')} + > + {sideMenuMinimised ? ( + + ) : ( + <> + + + )} + + + {pageConfig.categories.map( + ({ id: categoryId, key: categoryKey }: PageCategory) => ( + + {/* display heading if not `default` (used for top links) */} + {categoryKey !== 'default' && ( + + )} + + {/* display category links */} + {pagesToDisplay.map( + ({ category, hash, key, lottie, action }: PageItem) => ( + + {category === categoryId && ( + + )} + + ) + )} + + ) + )} + + ); +}; diff --git a/src/library/SideMenu/Primary/Wrappers.ts b/src/library/SideMenu/Primary/Wrappers.ts new file mode 100644 index 0000000000..6db03b9792 --- /dev/null +++ b/src/library/SideMenu/Primary/Wrappers.ts @@ -0,0 +1,104 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +export const Wrapper = styled(motion.div)` + border: none; + border-radius: 0.7rem; + height: 3.2rem; + display: flex; + flex-flow: row wrap; + align-items: center; + margin: 0.4rem 0.2rem 0.3rem 0; + padding: 0rem 0.5rem; + position: relative; + + &.minimised { + border: 1px solid rgba(255, 255, 255, 0); + border-radius: 0.5rem; + font-size: 1.1rem; + justify-content: center; + margin: 0.7rem 0.2rem 0.5rem 0; + padding: 0.65rem 0rem; + + &.success { + border: 1px solid var(--accent-color-primary); + } + &.warning { + border: 1px solid var(--status-warning-color); + } + } + + .dotlottie { + color: var(--text-color-primary); + margin-left: 0.25rem; + margin-right: 0.65rem; + width: 1.35rem; + height: 1.35rem; + .fa-icon { + margin: 0 0.15rem; + } + &.minimised { + margin: 0; + width: 1.5rem; + height: 1.5rem; + } + } + .name { + font-family: InterSemiBold, sans-serif; + margin: 0; + padding: 0; + line-height: 1.35rem; + } + .action { + color: var(--status-success-color); + display: flex; + flex: 1; + font-size: 0.88rem; + flex-flow: row wrap; + justify-content: flex-end; + margin-right: 0.4rem; + opacity: 0.7; + + > span { + &.success { + color: var(--accent-color-primary); + border: 1px solid var(--accent-color-primary); + } + &.warning { + color: var(--status-warning-color); + border: 1px solid var(--status-warning-color-transparent); + } + border-radius: 0.5rem; + padding: 0.15rem 0.5rem; + } + + &.success { + svg { + color: var(--status-success-color); + } + } + &.warning { + svg { + color: var(--status-warning-color); + } + } + &.minimised { + > svg { + flex: 0; + position: absolute; + right: -3px; + top: -4px; + } + } + } + + &.active { + background: var(--highlight-primary); + } + &.inactive:hover { + background: var(--highlight-secondary); + } +`; diff --git a/src/library/SideMenu/Primary/index.tsx b/src/library/SideMenu/Primary/index.tsx new file mode 100644 index 0000000000..f7c825623e --- /dev/null +++ b/src/library/SideMenu/Primary/index.tsx @@ -0,0 +1,79 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCircle } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Link } from 'react-router-dom'; +import { useUi } from 'contexts/UI'; +import { useDotLottieButton } from 'library/Hooks/useDotLottieButton'; +import type { PrimaryProps } from '../types'; +import { Wrapper } from './Wrappers'; + +export const Primary = ({ + name, + active, + to, + action, + minimised, + lottie, +}: PrimaryProps) => { + const { setSideMenu } = useUi(); + + const { icon, play } = useDotLottieButton(lottie); + + let Action = null; + const actionStatus = action?.status ?? null; + + switch (action?.type) { + case 'text': + Action = ( +
+ + {action?.text ?? ''} + +
+ ); + break; + case 'bullet': + Action = ( +
+ +
+ ); + break; + default: + Action = null; + } + + return ( + { + if (!active) { + play(); + setSideMenu(false); + } + }} + > + +
+ {icon} +
+ {!minimised && ( + <> +

{name}

{Action} + + )} +
+ + ); +}; diff --git a/src/library/SideMenu/Secondary/Wrappers.ts b/src/library/SideMenu/Secondary/Wrappers.ts new file mode 100644 index 0000000000..17ab5f51b0 --- /dev/null +++ b/src/library/SideMenu/Secondary/Wrappers.ts @@ -0,0 +1,104 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; +import type { MinimisedProps } from '../types'; + +export const Wrapper = styled(motion.button)` + border: 1px solid var(--border-primary-color); + border-radius: 0.7rem; + height: 3.2rem; + display: flex; + flex-flow: row wrap; + align-items: center; + position: relative; + padding: 0.75rem 0rem 0.75rem 0.5rem; + margin: 0.8rem 0.2rem 0.8rem 0; + width: 100%; + + .name { + color: var(--text-color-primary); + font-family: InterSemiBold, sans-serif; + font-size: 1.1rem; + } + .light { + color: var(--text-color-primary); + margin-left: 0.2rem; + } + .action { + color: var(--text-color-primary); + flex: 1; + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + } + + &.active { + background: var(--highlight-primary); + } + &.inactive:hover { + background: var(--highlight-secondary); + } + &.success { + border: 1px solid var(--status-success-color-transparent); + } + &.warning { + border: 1px solid var(--status-warning-color-transparent); + } + &.danger { + border: 1px solid var(--status-danger-color-transparent); + } +`; + +export const MinimisedWrapper = styled(motion.button)` + border: 1px solid var(--border-primary-color); + border-radius: 0.5rem; + display: flex; + flex-flow: row wrap; + justify-content: center; + align-items: center; + position: relative; + padding: 0rem 0rem; + margin: 0.6rem 0 0.6rem 0; + min-height: 2.8rem; + width: 100%; + + &.active { + background: var(--highlight-primary); + } + &.inactive:hover { + background: var(--highlight-secondary); + } + .icon { + margin: 0; + } + .action { + &.minimised { + flex: 0; + position: absolute; + top: -2px; + right: -13px; + } + } + &.success { + border: 1px solid var(--status-success-color-transparent); + } + &.warning { + border: 1px solid var(--status-warning-color-transparent); + } + &.danger { + border: 1px solid var(--status-danger-color-transparent); + } +`; + +export const IconWrapper = styled.div<{ $minimised: boolean }>` + margin-left: ${(props) => (props.$minimised ? 0 : '0.25rem')}; + margin-right: ${(props) => (props.$minimised ? 0 : '0.65rem')}; + + svg { + .primary { + fill: var(--text-color-primary); + } + } +`; diff --git a/src/library/SideMenu/Secondary/index.tsx b/src/library/SideMenu/Secondary/index.tsx new file mode 100644 index 0000000000..4cee45febf --- /dev/null +++ b/src/library/SideMenu/Secondary/index.tsx @@ -0,0 +1,47 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { SecondaryProps } from '../types'; +import { IconWrapper, MinimisedWrapper, Wrapper } from './Wrappers'; + +export const Secondary = ({ + action, + classes, + name, + icon, + minimised, + onClick, +}: SecondaryProps) => { + const { Svg, size } = icon || {}; + + const StyledWrapper = minimised ? MinimisedWrapper : Wrapper; + + return ( + { + onClick(); + }} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + transition={{ + duration: 0.1, + }} + > + + {Svg && } + + + {!minimised && ( + <> +
{name}
+ {action &&
{action}
} + + )} +
+ ); +}; diff --git a/src/library/SideMenu/Wrapper.ts b/src/library/SideMenu/Wrapper.ts new file mode 100644 index 0000000000..1f230562f1 --- /dev/null +++ b/src/library/SideMenu/Wrapper.ts @@ -0,0 +1,110 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { + SideMenuMaximisedWidth, + SideMenuMinimisedWidth, + SideMenuStickyThreshold, +} from 'consts'; +import type { MinimisedProps } from './types'; + +export const Wrapper = styled.div` + border-radius: ${(props) => (props.$minimised ? '0.7rem' : 0)}; + background: none; + padding: 1rem 1rem 1rem 1.25rem; + overflow: auto; + flex-grow: 1; + margin: 0.75rem 0 3.35rem 0rem; + display: flex; + flex-flow: column nowrap; + backdrop-filter: blur(4px); + width: ${(props) => + props.$minimised + ? `${SideMenuMinimisedWidth}px` + : `${SideMenuMaximisedWidth}px`}; + + &::-webkit-scrollbar { + display: none; + } + -ms-overflow-style: none; + scrollbar-width: none; + + @media (max-width: ${SideMenuStickyThreshold}px) { + background: var(--gradient-side-menu); + transition: all var(--transition-duration); + border-radius: 0.75rem; + } + + section { + &:first-child { + flex-grow: 1; + } + /* Footer */ + &:last-child { + display: flex; + flex-flow: ${(props) => (props.$minimised ? 'column wrap' : 'row wrap')}; + align-items: center; + padding-top: 0.5rem; + + button { + color: var(--text-color-secondary); + position: relative; + transition: color var(--transition-duration); + margin-top: ${(props) => (props.$minimised ? '1rem' : 0)}; + margin-right: ${(props) => (props.$minimised ? 0 : '0.9rem')}; + opacity: 0.75; + padding: 0.1rem; + + path { + fill: var(--text-color-secondary); + } + &:hover { + opacity: 1; + } + } + } + } +`; + +export const LogoWrapper = styled.button` + display: flex; + flex-flow: row wrap; + justify-content: ${(props) => (props.$minimised ? 'center' : 'flex-start')}; + width: 100%; + height: 2.8rem; + padding: ${(props) => (props.$minimised ? '0' : '0.4rem 0.5rem')}; + margin-bottom: ${(props) => (props.$minimised ? '1.5rem' : '1rem')}; + position: relative; + + ellipse { + fill: var(--accent-color-primary); + } +`; + +export const Separator = styled.div` + border-bottom: 1px solid var(--border-primary-color); + width: 100%; + margin: 1rem 1rem 0.5rem 0; +`; + +export const ConnectionSymbol = styled.div` + width: 0.6rem; + height: 0.6rem; + background: ${(props) => props.color}; + border-radius: 50%; + margin: 0 0.7rem; + + &.success { + background: var(--status-success-color); + color: var(--status-success-color); + } + &.warning { + background: var(--status-warning-color); + color: var(--status-warning-color); + } + &.danger { + background: var(--status-danger-color); + color: var(--status-danger-color); + } +`; diff --git a/src/library/SideMenu/index.tsx b/src/library/SideMenu/index.tsx new file mode 100644 index 0000000000..ea17a6e3d6 --- /dev/null +++ b/src/library/SideMenu/index.tsx @@ -0,0 +1,161 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCompressAlt, faExpandAlt } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { capitalizeFirstLetter } from '@polkadot-cloud/utils'; +import throttle from 'lodash.throttle'; +import { useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { SideMenuStickyThreshold } from 'consts'; +import { useApi } from 'contexts/Api'; +import { useHelp } from 'contexts/Help'; +import { useTheme } from 'contexts/Themes'; +import { useUi } from 'contexts/UI'; +import type { UIContextInterface } from 'contexts/UI/types'; +import CogOutlineSVG from 'img/cog-outline.svg?react'; +import ForumSVG from 'img/forum.svg?react'; +import InfoSVG from 'img/info.svg?react'; +import LanguageSVG from 'img/language.svg?react'; +import LogoGithubSVG from 'img/logo-github.svg?react'; +import MoonOutlineSVG from 'img/moon-outline.svg?react'; +import SunnyOutlineSVG from 'img/sunny-outline.svg?react'; +import { useOutsideAlerter } from 'library/Hooks'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { Heading } from './Heading/Heading'; +import { Main } from './Main'; +import { Secondary } from './Secondary'; +import { ConnectionSymbol, Separator, Wrapper } from './Wrapper'; + +export const SideMenu = () => { + const { t } = useTranslation('base'); + const { apiStatus } = useApi(); + const { networkData, network } = useNetwork(); + const { mode, toggleTheme } = useTheme(); + const { openModal } = useOverlay().modal; + const { + setSideMenu, + sideMenuMinimised, + userSideMenuMinimised, + setUserSideMenuMinimised, + }: UIContextInterface = useUi(); + const { openHelp } = useHelp(); + + // listen to window resize to hide SideMenu + useEffect(() => { + window.addEventListener('resize', windowThrottle); + return () => { + window.removeEventListener('resize', windowThrottle); + }; + }, []); + + const throttleCallback = () => { + if (window.innerWidth >= SideMenuStickyThreshold) { + setSideMenu(false); + } + }; + const windowThrottle = throttle(throttleCallback, 200, { + trailing: true, + leading: false, + }); + + const ref = useRef(null); + useOutsideAlerter(ref, () => { + setSideMenu(false); + }); + + const apiStatusClass = + apiStatus === 'connecting' + ? 'warning' + : apiStatus === 'connected' + ? 'success' + : 'danger'; + + return ( + +
+
+ + { + openHelp(null); + }} + name={t('resources')} + minimised={sideMenuMinimised} + icon={{ + Svg: InfoSVG, + size: sideMenuMinimised ? '1.4em' : '1.2em', + }} + /> + openModal({ key: 'GoToFeedback' })} + name={t('feedback')} + minimised={sideMenuMinimised} + icon={{ + Svg: ForumSVG, + size: sideMenuMinimised ? '1.4em' : '1.2em', + }} + /> + + + openModal({ key: 'Networks' })} + icon={{ + Svg: networkData.brand.inline.svg, + size: networkData.brand.inline.size, + }} + minimised={sideMenuMinimised} + action={ + + } + /> +
+ +
+ + + + + {mode === 'dark' ? ( + + ) : ( + + )} +
+
+ ); +}; diff --git a/src/library/SideMenu/types.ts b/src/library/SideMenu/types.ts new file mode 100644 index 0000000000..18b8b0827f --- /dev/null +++ b/src/library/SideMenu/types.ts @@ -0,0 +1,40 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type React from 'react'; +import type { FunctionComponent, SVGProps } from 'react'; +import type { AnyJson } from 'types'; + +export interface MinimisedProps { + $minimised?: boolean; +} + +export interface HeadingProps { + title: string; + minimised: boolean; +} + +export interface PrimaryProps { + name: string; + active: boolean; + to: string; + lottie: AnyJson; + action: undefined | { type: string; status: string; text?: string }; + minimised: boolean; +} + +export interface SecondaryProps { + name: string; + classes?: string[]; + onClick: () => void; + active?: boolean; + to?: string; + icon: IconProps; + action?: React.ReactNode; + minimised: boolean; +} + +export interface IconProps { + Svg: FunctionComponent>; + size?: string; +} diff --git a/src/library/Stat/Wrapper.ts b/src/library/Stat/Wrapper.ts new file mode 100644 index 0000000000..fd48be1beb --- /dev/null +++ b/src/library/Stat/Wrapper.ts @@ -0,0 +1,88 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div<{ $isAddress?: boolean }>` + width: 100%; + padding: 0.15rem 0.25rem; + h4 { + font-family: InterSemiBold, sans-serif; + display: flex; + flex-flow: row wrap; + align-items: center; + margin: 0 0 0.15rem 0; + + > .btn { + color: var(--text-color-secondary); + background: var(--background-primary); + display: flex; + flex-flow: row wrap; + justify-content: center; + align-items: center; + border-radius: 2rem; + width: 1.5rem; + height: 1.5rem; + margin-left: 0.65rem; + transition: color var(--transition-duration); + &:hover { + color: var(--accent-color-primary); + } + } + } + + .content { + display: flex; + flex-flow: column nowrap; + align-items: center; + height: 2.6rem; + position: relative; + width: auto; + max-width: 100%; + overflow: hidden; + + .text { + padding-left: ${(props) => (props.$isAddress ? '3rem' : 0)}; + font-family: InterBold, sans-serif; + color: var(--text-color-primary); + padding-top: 0.25rem; + position: absolute; + left: 0; + top: 0; + margin: 0; + height: 2.6rem; + height: 2.6rem; + font-size: 1.4rem; + width: auto; + max-width: 100%; + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + + h2 { + font-family: InterBold, sans-serif; + display: flex; + align-items: center; + text-overflow: ellipsis; + line-height: 1.4rem; + } + + .identicon { + position: absolute; + display: flex; + left: 0; + top: 0; + flex-flow: row wrap; + align-items: center; + } + + > span { + position: absolute; + display: flex; + right: 0.2rem; + top: 0rem; + } + } + } +`; diff --git a/src/library/Stat/index.tsx b/src/library/Stat/index.tsx new file mode 100644 index 0000000000..2d25c096a8 --- /dev/null +++ b/src/library/Stat/index.tsx @@ -0,0 +1,144 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCopy } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + ButtonHelp, + ButtonPrimary, + ButtonSecondary, + Polkicon, + Odometer, +} from '@polkadot-cloud/react'; +import { applyWidthAsPadding, minDecimalPlaces } from '@polkadot-cloud/utils'; +import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import { useHelp } from 'contexts/Help'; +import { useNotifications } from 'contexts/Notifications'; +import { useNetwork } from 'contexts/Network'; +import { Wrapper } from './Wrapper'; +import type { StatAddress, StatProps } from './types'; + +export const Stat = ({ + label, + stat, + buttons, + helpKey, + icon, + copy, + type = 'string', + buttonType = 'primary', +}: StatProps) => { + const { + brand: { token: Token }, + } = useNetwork().networkData; + const { openHelp } = useHelp(); + const { addNotification } = useNotifications(); + + const containerRef = useRef(null); + const subjectRef = useRef(null); + + const handleAdjustLayout = () => { + applyWidthAsPadding(subjectRef, containerRef); + }; + + useLayoutEffect(() => { + handleAdjustLayout(); + }); + + useEffect(() => { + window.addEventListener('resize', handleAdjustLayout); + return () => { + window.removeEventListener('resize', handleAdjustLayout); + }; + }, []); + + const Button = buttonType === 'primary' ? ButtonPrimary : ButtonSecondary; + + let display; + switch (type) { + case 'address': + display = stat.display; + break; + case 'odometer': + display = ( +

+ + + {stat?.unit ? stat.unit : null} +

+ ); + break; + default: + display = stat; + } + + return ( + +

+ {label} + {helpKey !== undefined ? ( + openHelp(helpKey)} /> + ) : null} + {copy !== undefined ? ( + + ) : null} +

+
+
+ {icon ? ( + <> + +   + + ) : null} + {type === 'address' ? ( +
+ +
+ ) : null} + {display} + {buttons ? ( + + {buttons.map((btn: any, index: number) => ( + +
+
+
+ ); +}; diff --git a/src/library/Stat/types.ts b/src/library/Stat/types.ts new file mode 100644 index 0000000000..e115163ce3 --- /dev/null +++ b/src/library/Stat/types.ts @@ -0,0 +1,28 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { IconProp } from '@fortawesome/fontawesome-svg-core'; +import type { AnyObject } from '@polkadot-cloud/utils/types'; +import type { MaybeAddress } from 'types'; + +export interface StatProps { + label: string; + stat: AnyObject; + type?: string; + buttons?: any; + helpKey: string; + icon?: IconProp; + buttonType?: string; + copy?: { + content: string; + notification: { + title: string; + subtitle: string; + }; + }; +} + +export interface StatAddress { + address: MaybeAddress; + display: string; +} diff --git a/src/library/StatBoxList/Item.tsx b/src/library/StatBoxList/Item.tsx new file mode 100644 index 0000000000..5b3bc3bd3e --- /dev/null +++ b/src/library/StatBoxList/Item.tsx @@ -0,0 +1,37 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React from 'react'; +import { Number } from './Number'; +import { Pie } from './Pie'; +import { Text } from './Text'; +import { StatBoxWrapper } from './Wrapper'; + +export const StatBox = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); + +export const StatBoxListItem = ({ format, params }: any) => { + switch (format) { + case 'chart-pie': + return ; + + case 'number': + return ; + + case 'text': + return ; + + default: + return null; + } +}; diff --git a/src/library/StatBoxList/Number.tsx b/src/library/StatBoxList/Number.tsx new file mode 100644 index 0000000000..3fa585df2a --- /dev/null +++ b/src/library/StatBoxList/Number.tsx @@ -0,0 +1,42 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp, Odometer } from '@polkadot-cloud/react'; +import { useHelp } from 'contexts/Help'; +import BigNumber from 'bignumber.js'; +import { StatBox } from './Item'; +import type { NumberProps } from './types'; + +export const Number = ({ + label, + value, + unit, + helpKey, + decimals, +}: NumberProps) => { + const help = helpKey !== undefined; + const { openHelp } = useHelp(); + + return ( + +
+
+

+ + {unit ? <>{unit} : null} +

+

+ {label} + {help ? ( + openHelp(helpKey)} /> + ) : null} +

+
+
+
+ ); +}; diff --git a/src/library/StatBoxList/Pie.tsx b/src/library/StatBoxList/Pie.tsx new file mode 100644 index 0000000000..af5807d350 --- /dev/null +++ b/src/library/StatBoxList/Pie.tsx @@ -0,0 +1,76 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp, Chart, Odometer } from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useHelp } from 'contexts/Help'; +import BigNumber from 'bignumber.js'; +import { StatBox } from './Item'; +import type { PieProps } from './types'; + +export const Pie = ({ label, stat, graph, tooltip, helpKey }: PieProps) => { + const help = helpKey !== undefined; + const showTotal = !!stat?.total; + const { openHelp } = useHelp(); + + const [values, setValues] = useState({ + value: Number(stat?.value || 0), + total: Number(stat?.total || 0), + }); + + useEffect(() => { + setValues({ + value: Number(stat?.value || 0), + total: Number(stat?.total || 0), + }); + }, [stat]); + + return ( + +
+
+ + {tooltip ? ( +
+

{tooltip}

+
+ ) : null} +
+ +
+

+ + {stat?.unit && <>{stat?.unit}} + + {showTotal ? ( + + /  + + {stat?.unit ? <>{stat?.unit}unit : null} + + ) : null} +

+

+ {label}{' '} + {help ? ( + openHelp(helpKey)} /> + ) : null} +

+
+
+
+ ); +}; diff --git a/src/library/StatBoxList/Text.tsx b/src/library/StatBoxList/Text.tsx new file mode 100644 index 0000000000..d5ab758a13 --- /dev/null +++ b/src/library/StatBoxList/Text.tsx @@ -0,0 +1,37 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp } from '@polkadot-cloud/react'; +import { useHelp } from 'contexts/Help'; +import { StatBox } from './Item'; +import { TextTitleWrapper } from './Wrapper'; +import type { TextProps } from './types'; + +export const Text = ({ + label, + value, + secondaryValue, + helpKey, + primary, +}: TextProps) => { + const help = helpKey !== undefined; + const { openHelp } = useHelp(); + return ( + +
+
+ + {value} + {secondaryValue ? {secondaryValue} : null} + +

+ {label} + {help ? ( + openHelp(helpKey)} /> + ) : null} +

+
+
+
+ ); +}; diff --git a/src/library/StatBoxList/Timeleft.tsx b/src/library/StatBoxList/Timeleft.tsx new file mode 100644 index 0000000000..e28ad34fad --- /dev/null +++ b/src/library/StatBoxList/Timeleft.tsx @@ -0,0 +1,59 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp, Chart } from '@polkadot-cloud/react'; +import { useHelp } from 'contexts/Help'; +import { Countdown } from 'library/Countdown'; +import { StatBox } from './Item'; +import { TimeLeftWrapper } from './Wrapper'; +import type { TimeleftProps } from './types'; + +export const Timeleft = ({ + label, + timeleft, + graph, + tooltip, + helpKey, +}: TimeleftProps) => { + const help = helpKey !== undefined; + const { openHelp } = useHelp(); + + return ( + +
+
+ + {tooltip ? ( +
+

{tooltip}

+
+ ) : null} +
+ +
+ + + +

+ {label}{' '} + {help ? ( + openHelp(helpKey)} /> + ) : null} +

+
+
+
+ ); +}; diff --git a/src/library/StatBoxList/Wrapper.ts b/src/library/StatBoxList/Wrapper.ts new file mode 100644 index 0000000000..12bcc3aa8a --- /dev/null +++ b/src/library/StatBoxList/Wrapper.ts @@ -0,0 +1,199 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +export const ListWrapper = styled.div` + display: flex; + flex-flow: row wrap; + padding-top: 1rem; + + > div:last-child { + margin-bottom: 0; + .content { + margin-right: 0; + } + } +`; + +export const StatBoxWrapper = styled(motion.div)` + display: flex; + flex-flow: column wrap; + z-index: 0; + flex: 1; + flex-basis: 100%; + margin-bottom: 1rem; + + @media (min-width: 800px) { + flex-basis: 33%; + min-width: 200px; + max-width: none; + margin-bottom: 0; + } + + /* responsive screen sizing */ + h3 { + font-size: 1.2rem; + } + @media (min-width: 950px) { + max-width: 300px; + h3 { + font-size: 1.25rem; + } + } + + .content { + background: var(--background-primary); + box-shadow: var(--card-shadow-secondary); + + @media (max-width: 799px) { + box-shadow: var(--card-shadow); + } + display: flex; + border-radius: 0.95rem; + margin-right: 1.25rem; + padding: 0.9rem 0rem; + max-height: 5.25rem; + flex-flow: row wrap; + + @media (max-width: 749px) { + margin-right: 0; + padding: 0.9rem 0; + } + + h4 { + font-family: InterSemiBold, sans-serif; + flex: 1; + display: flex; + flex-flow: row wrap; + align-items: center; + } + + > .chart { + position: relative; + display: flex; + justify-content: center; + align-items: center; + padding-left: 1rem; + + .graph { + overflow: hidden; + } + + .tooltip { + background: var(--background-invert); + opacity: 0; + position: absolute; + top: -20px; + left: -8px; + z-index: 2; + border-radius: 0.5rem; + padding: 0 0.5rem; + width: max-content; + max-width: 250px; + transition: opacity var(--transition-duration); + + h3 { + color: var(--text-color-invert); + font-family: InterSemiBold, sans-serif; + text-align: center; + margin: 0; + font-size: 0.9rem; + } + } + + &:hover { + .tooltip { + opacity: 1; + } + } + } + + > .labels { + padding-left: 1.25rem; + flex-basis: 70%; + flex: 1; + display: flex; + flex-flow: column wrap; + justify-content: center; + overflow: hidden; + + h3 { + font-family: InterBold, sans-serif; + display: flex; + flex-flow: row wrap; + margin-top: 0.1rem; + margin-bottom: 0.1rem; + + &.text { + margin-top: 0.15rem; + display: flex; + align-items: center; + } + span.total { + color: var(--text-color-secondary); + display: flex; + font-size: 0.95rem; + margin-left: 0.4rem; + position: relative; + bottom: 0.1rem; + } + } + } + } +`; + +export const TextTitleWrapper = styled.div<{ $primary?: boolean }>` + color: ${(props) => + props.$primary === true + ? 'var(--accent-color-primary)' + : 'var(--text-color-primary)'}; + font-family: InterBold, sans-serif; + display: flex; + flex-flow: row wrap; + margin-bottom: 0.35rem; + + font-size: 1.2rem; + @media (min-width: 950px) { + max-width: 300px; + font-size: 1.25rem; + } + + &.text { + margin-top: 0.15rem; + } + + span { + color: var(--text-color-primary); + font-family: InterSemiBold, sans-serif; + font-size: 0.95rem; + margin-left: 0.55rem; + margin-top: 0.1rem; + } +`; + +export const TimeLeftWrapper = styled.div<{ primary?: boolean }>` + color: ${(props) => + props.primary === true + ? 'var(--accent-color-primary)' + : 'var(--text-color-primary)'}; + font-family: InterBold, sans-serif; + display: flex; + flex-flow: row wrap; + font-size: 1.2rem; + @media (min-width: 950px) { + max-width: 300px; + font-size: 1.25rem; + } + margin-bottom: 0.15rem; + + span { + color: var(--text-color-primary); + font-family: InterSemiBold, sans-serif; + font-size: 0.95rem; + margin-left: 0.3rem; + margin-top: 0.1rem; + margin-right: 0.75rem; + } +`; diff --git a/src/library/StatBoxList/index.tsx b/src/library/StatBoxList/index.tsx new file mode 100644 index 0000000000..f5600d5b32 --- /dev/null +++ b/src/library/StatBoxList/index.tsx @@ -0,0 +1,12 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { StatBoxRow } from '@polkadot-cloud/react'; +import React from 'react'; +import { ListWrapper } from './Wrapper'; + +export const StatBoxList = ({ children }: { children: React.ReactNode }) => ( + + {children} + +); diff --git a/src/library/StatBoxList/types.ts b/src/library/StatBoxList/types.ts new file mode 100644 index 0000000000..e20fb52591 --- /dev/null +++ b/src/library/StatBoxList/types.ts @@ -0,0 +1,49 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { TimeLeftFormatted } from 'library/Hooks/useTimeLeft/types'; + +export interface NumberProps { + label: string; + value: number; + decimals?: number; + unit: string; + helpKey: string; +} + +export interface PieProps { + label: string; + stat: { + value: string | number; + unit: string | number; + total?: string | number; + }; + graph: { + value1: number; + value2: number; + }; + tooltip?: string; + helpKey: string; +} + +export interface TextProps { + primary?: boolean; + label: string; + value: string; + secondaryValue?: string; + helpKey: string; +} + +export interface TimeleftProps { + label: string; + timeleft: TimeLeftFormatted; + graph: { + value1: number; + value2: number; + }; + tooltip?: string; + helpKey: string; +} + +export type TimeLeftRaw = TimeLeftRawItem[]; +export type TimeLeftRawItem = Array; diff --git a/src/library/StatsHead/Wrapper.ts b/src/library/StatsHead/Wrapper.ts new file mode 100644 index 0000000000..ba80c8702d --- /dev/null +++ b/src/library/StatsHead/Wrapper.ts @@ -0,0 +1,72 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { SmallFontSizeMaxWidth } from 'consts'; + +export const Wrapper = styled.div` + flex-grow: 1; + display: flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + + @media (min-width: ${SmallFontSizeMaxWidth + 225}px) { + margin-bottom: 1rem; + } + + > div { + border-right: 0; + flex-basis: 100%; + flex-grow: 1; + margin-bottom: 0.5rem; + + &:last-child { + border-right: 0; + } + + @media (min-width: ${SmallFontSizeMaxWidth + 225}px) { + border-right: 1px solid var(--border-primary-color); + flex-basis: 25%; + margin-bottom: 0; + padding-left: 1rem; + padding-right: 1rem; + max-width: 275px; + + &:last-child { + max-width: none; + } + } + + > .inner { + border-bottom: 1px solid var(--border-primary-color); + display: flex; + flex-flow: column wrap; + padding: 0.5rem 0.5rem 1rem 0.5rem; + + @media (min-width: ${SmallFontSizeMaxWidth + 225}px) { + margin-bottom: 0; + } + + h2 { + color: var(--accent-color-primary); + } + + h4 { + color: var(--text-color-secondary); + font-family: InterSemiBold, sans-serif; + display: flex; + flex-flow: row wrap; + align-items: center; + margin-top: 0.45rem; + } + } + + &:first-child { + padding-left: 0; + } + &:last-child { + padding-right: 0; + } + } +`; diff --git a/src/library/StatsHead/index.tsx b/src/library/StatsHead/index.tsx new file mode 100644 index 0000000000..9088bbe14a --- /dev/null +++ b/src/library/StatsHead/index.tsx @@ -0,0 +1,29 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp } from '@polkadot-cloud/react'; +import { useHelp } from 'contexts/Help'; +import { Wrapper } from './Wrapper'; +import type { StatsHeadProps } from './types'; + +export const StatsHead = ({ items }: StatsHeadProps) => { + const { openHelp } = useHelp(); + + return ( + + {items.map(({ label, value, helpKey }, i) => ( +
+
+

{value}

+

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

+
+
+ ))} +
+ ); +}; diff --git a/src/library/StatsHead/types.ts b/src/library/StatsHead/types.ts new file mode 100644 index 0000000000..afc90c0f00 --- /dev/null +++ b/src/library/StatsHead/types.ts @@ -0,0 +1,10 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface StatsHeadProps { + items: { + value: string; + label: string; + helpKey?: string; + }[]; +} diff --git a/src/library/StatusButton/Wrapper.ts b/src/library/StatusButton/Wrapper.ts new file mode 100644 index 0000000000..9ed5e1dfe5 --- /dev/null +++ b/src/library/StatusButton/Wrapper.ts @@ -0,0 +1,33 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.button` + background: var(--button-primary-background); + color: var(--text-color-primary); + width: 100%; + flex: 1; + padding: 1rem 0.75rem; + border-radius: 0.75rem; + margin-bottom: 1rem; + font-size: 1rem; + display: flex; + flex-flow: row-reverse wrap; + align-items: center; + transition: all var(--transition-duration); + + > section:last-child { + color: var(--text-color-secondary); + padding-left: 0.25rem; + display: flex; + flex-flow: row wrap; + flex: 1; + } + + &:hover { + > section { + color: var(--text-color-primary); + } + } +`; diff --git a/src/library/StatusButton/index.tsx b/src/library/StatusButton/index.tsx new file mode 100644 index 0000000000..bb2f8d2cff --- /dev/null +++ b/src/library/StatusButton/index.tsx @@ -0,0 +1,30 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCircle } from '@fortawesome/free-regular-svg-icons'; +import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Wrapper } from './Wrapper'; +import type { StatusButtonProps } from './types'; + +export const StatusButton = ({ + checked, + label, + onClick, +}: StatusButtonProps) => ( + { + if (onClick !== undefined) { + onClick(); + } + }} + > +
+ +
+
{label}
+
+); diff --git a/src/library/StatusButton/types.ts b/src/library/StatusButton/types.ts new file mode 100644 index 0000000000..420b62f425 --- /dev/null +++ b/src/library/StatusButton/types.ts @@ -0,0 +1,8 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface StatusButtonProps { + checked: boolean; + label: string; + onClick: () => void; +} diff --git a/src/library/StatusLabel/Wrapper.ts b/src/library/StatusLabel/Wrapper.ts new file mode 100644 index 0000000000..b073f88ed8 --- /dev/null +++ b/src/library/StatusLabel/Wrapper.ts @@ -0,0 +1,40 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import type { WrapperProps } from './types'; + +export const Wrapper = styled.div` + position: absolute; + top: ${(props) => (props.$topOffset ? props.$topOffset : '50%')}; + left: 0; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + z-index: 2; + + > div { + background: var(--background-list-item); + min-width: 125px; + opacity: 0.75; + padding: 1rem 1.25rem; + border-radius: 1rem; + display: flex; + align-items: center; + justify-content: center; + + > svg { + color: var(--text-color-secondary); + } + h2 { + padding: 0; + margin: 0; + display: flex; + flex-flow: row wrap; + align-items: center; + font-size: 1.2rem; + opacity: 0.75; + } + } +`; diff --git a/src/library/StatusLabel/index.tsx b/src/library/StatusLabel/index.tsx new file mode 100644 index 0000000000..c4948bf4f1 --- /dev/null +++ b/src/library/StatusLabel/index.tsx @@ -0,0 +1,61 @@ +// Copyright 2023 @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 { ButtonHelp } from '@polkadot-cloud/react'; +import { useHelp } from 'contexts/Help'; +import { usePlugins } from 'contexts/Plugins'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { Wrapper } from './Wrapper'; +import type { StatusLabelProps } from './types'; + +export const StatusLabel = ({ + title, + helpKey, + hideIcon, + statusFor, + topOffset = '40%', + status = 'sync_or_setup', +}: StatusLabelProps) => { + const { isSyncing } = useUi(); + const { plugins } = usePlugins(); + const { inSetup } = useStaking(); + const { membership } = usePoolMemberships(); + const { openHelp } = useHelp(); + + // syncing or not staking + if (status === 'sync_or_setup') { + if (isSyncing || !inSetup() || membership !== null) { + return <>; + } + } + + if (status === 'active_service' && statusFor) + if (plugins.includes(statusFor)) { + return <>; + } + + return ( + +
+ {hideIcon !== true && } +

+    + {title} + {helpKey ? ( + + openHelp(helpKey)} + background="secondary" + /> + + ) : null} +

+
+
+ ); +}; diff --git a/src/library/StatusLabel/types.ts b/src/library/StatusLabel/types.ts new file mode 100644 index 0000000000..9e97ea75e5 --- /dev/null +++ b/src/library/StatusLabel/types.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { Plugin } from 'types'; + +export interface StatusLabelProps { + hideIcon?: boolean; + status: string; + statusFor?: Plugin; + title: string; + topOffset?: string; + helpKey?: string; +} + +export interface WrapperProps { + $topOffset?: string; +} diff --git a/src/library/SubmitTx/Default.tsx b/src/library/SubmitTx/Default.tsx new file mode 100644 index 0000000000..6d11ae3809 --- /dev/null +++ b/src/library/SubmitTx/Default.tsx @@ -0,0 +1,46 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faArrowAltCircleUp } from '@fortawesome/free-regular-svg-icons'; +import { ButtonSubmit } from '@polkadot-cloud/react'; +import React from 'react'; +import { useTxMeta } from 'contexts/TxMeta'; +import { EstimatedTxFee } from 'library/EstimatedTxFee'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import type { SubmitProps } from './types'; + +export const Default = ({ + onSubmit, + submitting, + valid, + submitText, + buttons, + submitAddress, + displayFor, +}: SubmitProps & { buttons?: React.ReactNode[] }) => { + const { txFeesValid } = useTxMeta(); + const { accountHasSigner } = useImportedAccounts(); + + const disabled = + submitting || !valid || !accountHasSigner(submitAddress) || !txFeesValid; + + return ( + <> +
+ +
+
+ {buttons} + onSubmit()} + disabled={disabled} + pulse={!disabled} + /> +
+ + ); +}; diff --git a/src/library/SubmitTx/ManualSign/Ledger.tsx b/src/library/SubmitTx/ManualSign/Ledger.tsx new file mode 100644 index 0000000000..d9509bc787 --- /dev/null +++ b/src/library/SubmitTx/ManualSign/Ledger.tsx @@ -0,0 +1,174 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faSquarePen } from '@fortawesome/free-solid-svg-icons'; +import { ButtonHelp, ButtonSubmit } from '@polkadot-cloud/react'; +import React, { useEffect, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLedgerHardware } from 'contexts/Hardware/Ledger'; +import type { LedgerResponse } from 'contexts/Hardware/types'; +import { useHelp } from 'contexts/Help'; +import { useTxMeta } from 'contexts/TxMeta'; +import { EstimatedTxFee } from 'library/EstimatedTxFee'; +import { useLedgerLoop } from 'library/Hooks/useLedgerLoop'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import type { LedgerAccount } from '@polkadot-cloud/react/types'; +import type { SubmitProps } from '../types'; + +export const Ledger = ({ + uid, + onSubmit, + submitting, + valid, + submitText, + buttons, + submitAddress, + displayFor, +}: SubmitProps & { buttons?: React.ReactNode[] }) => { + const { t } = useTranslation('library'); + const { + pairDevice, + transportResponse, + setIsExecuting, + resetStatusCodes, + getIsExecuting, + handleNewStatusCode, + isPaired, + getStatusCodes, + getFeedback, + setFeedback, + handleUnmount, + } = useLedgerHardware(); + const { openHelp } = useHelp(); + const { setModalResize } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { accountHasSigner } = useImportedAccounts(); + const { getAccount } = useImportedAccounts(); + const { txFeesValid, setTxSignature, getTxSignature } = useTxMeta(); + + const getAddressIndex = () => { + return (getAccount(activeAccount) as LedgerAccount)?.index || 0; + }; + + // Ledger loop needs to keep track of whether this component is mounted. If it is unmounted then + // the loop will cancel & ledger metadata will be cleared up. isMounted needs to be given as a + // function so the interval fetches the real value. + const isMounted = useRef(true); + const getIsMounted = () => isMounted.current; + + const { handleLedgerLoop } = useLedgerLoop({ + tasks: ['sign_tx'], + options: { + accountIndex: getAddressIndex, + }, + mounted: getIsMounted, + }); + + // Handle new Ledger status report. + const handleLedgerStatusResponse = (response: LedgerResponse) => { + if (!response) return; + const { ack, statusCode, body } = response; + + if (statusCode === 'SignedPayload') { + if (uid !== body.uid) { + // UIDs do not match, so this is not the transaction we are waiting for. + setFeedback(t('wrongTransaction'), 'Wrong Transaction'); + resetStatusCodes(); + setTxSignature(null); + } else { + // Important: only set the signature (and therefore trigger the transaction submission) if + // UIDs match. + handleNewStatusCode(ack, statusCode); + setTxSignature(body.sig); + resetStatusCodes(); + } + setIsExecuting(false); + } else { + handleNewStatusCode(ack, statusCode); + } + }; + + // Resize modal on content change. + useEffect(() => { + setModalResize(); + }, [isPaired, getStatusCodes()]); + + // Listen for new Ledger status reports. + useEffect(() => { + if (getIsExecuting()) { + handleLedgerStatusResponse(transportResponse); + } + }, [transportResponse]); + + // Tidy up context state when this component is no longer mounted. + useEffect(() => { + return () => { + isMounted.current = false; + handleUnmount(); + }; + }, []); + + // Get the latest Ledger loop feedback. + const feedback = getFeedback(); + + // Help key based on Ledger status. + const helpKey = feedback?.helpKey; + + // The state under which submission is disabled. + const disabled = + submitting || !valid || !accountHasSigner(submitAddress) || !txFeesValid; + + return ( + <> +
+ + {valid ? ( +

+ {feedback?.message || t('submitTransaction')} + {helpKey ? ( + openHelp(helpKey)} + background="secondary" + /> + ) : null} +

+ ) : ( +

...

+ )} +
+
+ {buttons} + {getTxSignature() !== null || submitting ? ( + onSubmit()} + disabled={disabled} + pulse={!(disabled || getIsExecuting())} + /> + ) : ( + { + const paired = await pairDevice(); + if (paired) { + setIsExecuting(true); + handleLedgerLoop(); + } + }} + disabled={disabled || getIsExecuting()} + pulse={!(disabled || getIsExecuting())} + /> + )} +
+ + ); +}; diff --git a/src/library/SubmitTx/ManualSign/Vault/SignPrompt.tsx b/src/library/SubmitTx/ManualSign/Vault/SignPrompt.tsx new file mode 100644 index 0000000000..c7e7aaaf0f --- /dev/null +++ b/src/library/SubmitTx/ManualSign/Vault/SignPrompt.tsx @@ -0,0 +1,98 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faChevronLeft, + faChevronRight, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ButtonPrimary, ButtonSecondary } from '@polkadot-cloud/react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { usePrompt } from 'contexts/Prompt'; +import { useTxMeta } from 'contexts/TxMeta'; +import { QRViewerWrapper } from 'library/Import/Wrappers'; +import { QrDisplayPayload } from 'library/QRCode/DisplayPayload'; +import { QrScanSignature } from 'library/QRCode/ScanSignature'; +import type { SignerPromptProps } from 'library/SubmitTx/types'; +import type { AnyJson } from 'types'; + +export const SignPrompt = ({ submitAddress }: SignerPromptProps) => { + const { t } = useTranslation('library'); + const { getTxPayload, setTxSignature } = useTxMeta(); + const payload = getTxPayload(); + const payloadU8a = payload?.toU8a(); + const { setStatus: setPromptStatus } = usePrompt(); + + // Whether user is on sign or submit stage. + const [stage, setStage] = useState(1); + + return ( + + {stage === 1 &&

{t('scanPolkadotVault')}

} + {stage === 2 &&

{t('signPolkadotVault')}

} + +
+ Scan + + Sign +
+ {stage === 1 && ( +
+ +
+ )} + {stage === 2 && ( +
+ { + setPromptStatus(0); + setTxSignature(signature); + }} + /> +
+ )} +
+
+ {stage === 2 && ( + setStage(1)} + iconLeft={faChevronLeft} + iconTransform="shrink-3" + /> + )} + {stage === 1 && ( + { + setStage(2); + }} + iconRight={faChevronRight} + iconTransform="shrink-3" + /> + )} + setPromptStatus(0)} + /> +
+
+
+ ); +}; diff --git a/src/library/SubmitTx/ManualSign/Vault/index.tsx b/src/library/SubmitTx/ManualSign/Vault/index.tsx new file mode 100644 index 0000000000..e7c0875d7d --- /dev/null +++ b/src/library/SubmitTx/ManualSign/Vault/index.tsx @@ -0,0 +1,70 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faSquarePen } from '@fortawesome/free-solid-svg-icons'; +import { ButtonSubmit } from '@polkadot-cloud/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { usePrompt } from 'contexts/Prompt'; +import { useTxMeta } from 'contexts/TxMeta'; +import { EstimatedTxFee } from 'library/EstimatedTxFee'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import type { SubmitProps } from '../../types'; +import { SignPrompt } from './SignPrompt'; + +export const Vault = ({ + onSubmit, + submitting, + valid, + submitText, + buttons, + submitAddress, + displayFor, +}: SubmitProps & { buttons?: React.ReactNode[] }) => { + const { t } = useTranslation('library'); + const { accountHasSigner } = useImportedAccounts(); + const { txFeesValid, getTxSignature } = useTxMeta(); + const { openPromptWith, status: promptStatus } = usePrompt(); + + // The state under which submission is disabled. + const disabled = + submitting || !valid || !accountHasSigner(submitAddress) || !txFeesValid; + + return ( + <> +
+ + {valid ?

{t('submitTransaction')}

:

...

} +
+
+ {buttons} + {getTxSignature() !== null || submitting ? ( + onSubmit()} + disabled={disabled} + pulse={!(!valid || promptStatus !== 0)} + /> + ) : ( + { + openPromptWith( + , + 'small' + ); + }} + disabled={disabled || promptStatus !== 0} + pulse={!disabled || promptStatus === 0} + /> + )} +
+ + ); +}; diff --git a/src/library/SubmitTx/ManualSign/index.tsx b/src/library/SubmitTx/ManualSign/index.tsx new file mode 100644 index 0000000000..9220efa5b5 --- /dev/null +++ b/src/library/SubmitTx/ManualSign/index.tsx @@ -0,0 +1,34 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useEffect } from 'react'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import type { SubmitProps } from '../types'; +import { Ledger } from './Ledger'; +import { Vault } from './Vault'; + +export const ManualSign = ( + props: SubmitProps & { buttons?: React.ReactNode[] } +) => { + const { getAccount } = useImportedAccounts(); + const { getTxSignature, sender } = useTxMeta(); + const accountMeta = getAccount(sender); + const source = accountMeta?.source; + + const { onSubmit } = props; + + // Automatically submit transaction once it is signed. + useEffect(() => { + if (getTxSignature() !== null) { + onSubmit(); + } + }, [getTxSignature()]); + + return ( + <> + {source === 'ledger' && } + {source === 'vault' && } + + ); +}; diff --git a/src/library/SubmitTx/index.tsx b/src/library/SubmitTx/index.tsx new file mode 100644 index 0000000000..485ac70789 --- /dev/null +++ b/src/library/SubmitTx/index.tsx @@ -0,0 +1,111 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { Tx } from '@polkadot-cloud/react'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBonded } from 'contexts/Bonded'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { Default } from './Default'; +import { ManualSign } from './ManualSign'; +import type { SubmitTxProps } from './types'; + +export const SubmitTx = ({ + uid, + onSubmit, + submitText, + buttons = [], + submitAddress, + valid = false, + noMargin = false, + submitting = false, + proxySupported, + displayFor = 'default', + fromController = false, +}: SubmitTxProps) => { + const { t } = useTranslation(); + const { getBondedAccount } = useBonded(); + const { unit } = useNetwork().networkData; + const { setModalResize } = useOverlay().modal; + const { activeAccount, activeProxy } = useActiveAccounts(); + const { notEnoughFunds, sender, setTxSignature } = useTxMeta(); + const { getAccount, requiresManualSign } = useImportedAccounts(); + const controller = getBondedAccount(activeAccount); + + // Default to active account + let signingOpts = { + label: t('signer', { ns: 'library' }), + who: getAccount(activeAccount), + }; + + if (activeProxy && proxySupported) { + signingOpts = { + label: t('signedByProxy', { ns: 'library' }), + who: getAccount(activeProxy), + }; + } else if (!(activeProxy && proxySupported) && fromController) { + signingOpts = { + label: t('signedByController', { ns: 'library' }), + who: getAccount(controller), + }; + } + + submitText = + submitText || + `${ + submitting + ? t('submitting', { ns: 'modals' }) + : t('submit', { ns: 'modals' }) + }`; + + // Set resize on not enough funds. + useEffect(() => { + setModalResize(); + }, [notEnoughFunds, fromController]); + + // Reset tx metadata on unmount. + useEffect(() => { + return () => { + setTxSignature(null); + }; + }, []); + + return ( + + ) : ( + + ) + } + /> + ); +}; diff --git a/src/library/SubmitTx/types.ts b/src/library/SubmitTx/types.ts new file mode 100644 index 0000000000..5b3261e832 --- /dev/null +++ b/src/library/SubmitTx/types.ts @@ -0,0 +1,27 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type React from 'react'; +import type { DisplayFor, MaybeAddress } from 'types'; + +export type SubmitTxProps = SubmitProps & { + buttons?: React.ReactNode[]; + fromController?: boolean; + proxySupported: boolean; + submitAddress?: MaybeAddress; + noMargin?: boolean; +}; + +export interface SubmitProps { + uid?: number; + onSubmit: () => void; + submitting: boolean; + valid: boolean; + submitText?: string; + submitAddress: MaybeAddress; + displayFor?: DisplayFor; +} + +export interface SignerPromptProps { + submitAddress: MaybeAddress; +} diff --git a/src/library/Tips/Tip.tsx b/src/library/Tips/Tip.tsx new file mode 100644 index 0000000000..c25e0eb3d0 --- /dev/null +++ b/src/library/Tips/Tip.tsx @@ -0,0 +1,88 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; +import { + ButtonPrimary, + ButtonPrimaryInvert, + ButtonSecondary, +} from '@polkadot-cloud/react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { Title } from 'library/Prompt/Title'; +import { usePrompt } from 'contexts/Prompt'; +import { usePlugins } from 'contexts/Plugins'; + +export const Tip = ({ title, description, page }: any) => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { togglePlugin } = usePlugins(); + const { closePrompt } = usePrompt(); + + const [disabling, setDisabling] = useState(false); + + return ( + <> + {disabling ? ( + <> + + <div className="body"> + <h4>{t('module.dismissResult', { ns: 'tips' })}</h4> + <h4>{t('module.reEnable', { ns: 'tips' })}</h4> + + <div style={{ display: 'flex', marginTop: '1.5rem' }}> + <ButtonPrimary + marginRight + text={t('module.disableTips', { ns: 'tips' })} + onClick={() => { + togglePlugin('tips'); + closePrompt(); + }} + /> + <ButtonPrimaryInvert + text={t('module.cancel', { ns: 'tips' })} + onClick={() => setDisabling(false)} + style={{ marginLeft: '0.5rem' }} + /> + </div> + </div> + </> + ) : ( + <> + <Title title={title} /> + <div className="body"> + {description.map((item: any, index: number) => ( + <h4 key={`inner_def_${index}`} className="definition"> + {item} + </h4> + ))} + <div style={{ marginTop: '1.75rem', display: 'flex' }}> + {!!page && ( + <ButtonPrimary + marginRight + text={`${t('goTo', { ns: 'base' })} ${t(page, { + ns: 'base', + })}`} + onClick={() => { + closePrompt(); + navigate(`/${page}`); + }} + iconRight={faAngleRight} + iconTransform="shrink-1" + /> + )} + <ButtonSecondary + marginRight + text={t('module.disableTips', { ns: 'tips' })} + onClick={() => { + setDisabling(true); + }} + /> + </div> + </div> + </> + )} + </> + ); +}; diff --git a/src/library/Tips/Wrappers.ts b/src/library/Tips/Wrappers.ts new file mode 100644 index 0000000000..5c4fcfb644 --- /dev/null +++ b/src/library/Tips/Wrappers.ts @@ -0,0 +1,51 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +export const TipWrapper = styled(motion.div)` + width: 100%; + display: flex; + border-radius: 1.5rem; + margin-bottom: 1.25rem; + padding: 2rem 2rem 1rem 2rem; + flex-flow: column wrap; + position: relative; + overflow: hidden; + flex: 1; + + h2 { + margin: 0 0 1.5rem 0; + display: flex; + flex-flow: row wrap; + align-items: center; + > span { + color: var(--text-color-secondary); + margin-left: 0.75rem; + opacity: 0.75; + font-size: 1.1rem; + } + } + + h4 { + margin-bottom: 1.25rem; + } + + p { + color: var(--text-color-primary); + margin: 0.5rem 0 0 0; + text-align: left; + } + + p.icon { + opacity: 0.5; + } + + .buttons { + padding-bottom: 1rem; + > div { + margin-right: 1rem; + } + } +`; diff --git a/src/library/Tooltip/Wrapper.ts b/src/library/Tooltip/Wrapper.ts new file mode 100644 index 0000000000..fdbc82d51c --- /dev/null +++ b/src/library/Tooltip/Wrapper.ts @@ -0,0 +1,23 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + background: var(--background-invert); + transition: opacity var(--transition-duration); + display: flex; + flex-flow: column wrap; + border-radius: 0.5rem; + padding: 0.25rem 0.75rem; + width: max-content; + max-width: 200px; + + h3 { + color: var(--text-color-invert); + font-family: InterSemiBold, sans-serif; + font-size: 0.9rem; + padding: 0; + text-align: center; + } +`; diff --git a/src/library/Tooltip/index.tsx b/src/library/Tooltip/index.tsx new file mode 100644 index 0000000000..ad1e02fc04 --- /dev/null +++ b/src/library/Tooltip/index.tsx @@ -0,0 +1,71 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useEffect, useRef } from 'react'; +import { useTooltip } from 'contexts/Tooltip'; +import { Wrapper } from './Wrapper'; + +export const Tooltip = () => { + const { + open, + text, + show, + position, + showTooltip, + closeTooltip, + setTooltipPosition, + } = useTooltip(); + + // Ref for the tooltip element itself. + const tooltipRef: any = useRef(null); + + useEffect(() => { + if (open === 1) { + window.addEventListener('mousemove', mouseMoveCallback); + } else { + window.removeEventListener('mousemove', mouseMoveCallback); + } + return () => { + window.removeEventListener('mousemove', mouseMoveCallback); + }; + }, [open]); + + const mouseMoveCallback = (e: any) => { + const { target, pageX, pageY } = e; + + if (tooltipRef?.current) { + setTooltipPosition(pageX, pageY - (tooltipRef.current.offsetHeight || 0)); + if (!show) showTooltip(); + } + + const isTriggerElement = target?.classList.contains( + 'tooltip-trigger-element' + ); + const dataAttribute = target?.getAttribute('data-tooltip-text') ?? false; + if (!isTriggerElement) { + closeTooltip(); + } else if (dataAttribute !== text) { + closeTooltip(); + } + }; + + return ( + <> + {open === 1 && ( + <Wrapper + className="tooltip-trigger-element" + ref={tooltipRef} + style={{ + position: 'absolute', + left: `${position[0]}px`, + top: `${position[1]}px`, + zIndex: 99, + opacity: show === 1 ? 1 : 0, + }} + > + <h3 className="tooltip-trigger-element">{text}</h3> + </Wrapper> + )} + </> + ); +}; diff --git a/src/library/UpdateHeader/Wrapper.ts b/src/library/UpdateHeader/Wrapper.ts new file mode 100644 index 0000000000..237ef47cbd --- /dev/null +++ b/src/library/UpdateHeader/Wrapper.ts @@ -0,0 +1,29 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + flex: 1; + display: flex; + align-items: center; + margin-bottom: 1rem; + + > span { + color: var(--text-color-secondary); + margin: 0 0.75rem; + opacity: 0.5; + } + + /* input element of dropdown */ + > div { + border-bottom: 1px solid var(--border-primary-color); + display: flex; + justify-content: center; + flex: 1; + + > h4 { + padding: 0.5rem 1rem; + } + } +`; diff --git a/src/library/UpdateHeader/index.tsx b/src/library/UpdateHeader/index.tsx new file mode 100644 index 0000000000..3dbf180cad --- /dev/null +++ b/src/library/UpdateHeader/index.tsx @@ -0,0 +1,36 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faAnglesRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { usePayeeConfig } from 'library/Hooks/usePayeeConfig'; +import { Wrapper } from './Wrapper'; + +interface UpdateHeaderProps { + current: string | null; + selected: string | null; +} + +export const UpdateHeader = ({ current, selected }: UpdateHeaderProps) => { + const { getPayeeItems } = usePayeeConfig(); + + const currentTitle = + getPayeeItems(true).find((p) => p.value === current)?.title || ''; + + const selectedTitle = + getPayeeItems(true).find((p) => p.value === selected)?.title || ''; + + return ( + <Wrapper> + <div> + <h4>{currentTitle}</h4> + </div> + <span> + <FontAwesomeIcon icon={faAnglesRight} /> + </span> + <div> + <h4>{selectedTitle}</h4> + </div> + </Wrapper> + ); +}; diff --git a/src/library/ValidatorList/FilterValidators.tsx b/src/library/ValidatorList/FilterValidators.tsx new file mode 100644 index 0000000000..00cce3ab52 --- /dev/null +++ b/src/library/ValidatorList/FilterValidators.tsx @@ -0,0 +1,62 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCheckCircle, faCircle } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { Title } from 'library/Prompt/Title'; +import { FilterListButton, FilterListWrapper } from 'library/Prompt/Wrappers'; +import { useFilters } from 'contexts/Filters'; +import { useValidatorFilters } from '../Hooks/useValidatorFilters'; + +export const FilterValidators = () => { + const { t } = useTranslation('library'); + const { getFilters, toggleFilter } = useFilters(); + const { excludesToLabels, includesToLabels } = useValidatorFilters(); + + const includes = getFilters('include', 'validators'); + const excludes = getFilters('exclude', 'validators'); + + return ( + <FilterListWrapper> + <Title title={t('filterValidators')} /> + <div className="body"> + <h4>{t('include')}:</h4> + {Object.entries(includesToLabels).map(([f, l]: any, i) => ( + <FilterListButton + $active={includes?.includes(f) ?? false} + key={`validator_include_${i}`} + type="button" + onClick={() => { + toggleFilter('include', 'validators', f); + }} + > + <FontAwesomeIcon + transform="grow-4" + icon={includes?.includes(f) ? faCheckCircle : faCircle} + /> + <h4>{l}</h4> + </FilterListButton> + ))} + + <h4>{t('exclude')}:</h4> + {Object.entries(excludesToLabels).map(([f, l]: any, i) => ( + <FilterListButton + $active={excludes?.includes(f) ?? false} + key={`validator_exclude_${i}`} + type="button" + onClick={() => { + toggleFilter('exclude', 'validators', f); + }} + > + <FontAwesomeIcon + transform="grow-5" + icon={excludes?.includes(f) ? faCheckCircle : faCircle} + /> + <h4>{l}</h4> + </FilterListButton> + ))} + </div> + </FilterListWrapper> + ); +}; diff --git a/src/library/ValidatorList/Filters/FilterBadges.tsx b/src/library/ValidatorList/Filters/FilterBadges.tsx new file mode 100644 index 0000000000..9d28aa20dc --- /dev/null +++ b/src/library/ValidatorList/Filters/FilterBadges.tsx @@ -0,0 +1,64 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBan, faCheck } from '@fortawesome/free-solid-svg-icons'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useFilters } from 'contexts/Filters'; +import { Container } from 'library/Filter/Container'; +import { Item } from 'library/Filter/Item'; +import { useValidatorFilters } from 'library/Hooks/useValidatorFilters'; + +export const FilterBadges = () => { + const { t } = useTranslation('library'); + const { getFilters, getOrder, toggleFilter } = useFilters(); + const { includesToLabels, excludesToLabels, ordersToLabels } = + useValidatorFilters(); + + const includes = getFilters('include', 'validators'); + const excludes = getFilters('exclude', 'validators'); + const hasFilters = includes?.length || excludes?.length; + const order = getOrder('validators'); + + // scroll to top of the window on every filter. + useEffect(() => { + window.scrollTo(0, 0); + }, [includes, excludes]); + + return ( + <Container> + <div className="items"> + <Item + label={ + order === 'default' + ? `${t('unordered')}` + : `${t('order')}: ${ordersToLabels[order]}` + } + disabled + /> + {!hasFilters && <Item label={t('noFilters')} disabled />} + {includes?.map((e, i) => ( + <Item + key={`validator_include_${i}`} + label={includesToLabels[e]} + icon={faCheck} + onClick={() => { + toggleFilter('include', 'validators', e); + }} + /> + ))} + {excludes?.map((e, i) => ( + <Item + key={`validator_exclude_${i}`} + label={excludesToLabels[e]} + icon={faBan} + transform="shrink-2" + onClick={() => { + toggleFilter('exclude', 'validators', e); + }} + /> + ))} + </div> + </Container> + ); +}; diff --git a/src/library/ValidatorList/Filters/FilterHeaders.tsx b/src/library/ValidatorList/Filters/FilterHeaders.tsx new file mode 100644 index 0000000000..eb72f7173e --- /dev/null +++ b/src/library/ValidatorList/Filters/FilterHeaders.tsx @@ -0,0 +1,57 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faArrowDownWideShort, + faFilterCircleXmark, +} from '@fortawesome/free-solid-svg-icons'; +import { ButtonPrimaryInvert, ButtonSecondary } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useFilters } from 'contexts/Filters'; +import { usePrompt } from 'contexts/Prompt'; +import { OrderValidators } from '../OrderValidators'; +import { FilterValidators } from '../FilterValidators'; + +export const FilterHeaders = () => { + const { t } = useTranslation('library'); + const { openPromptWith } = usePrompt(); + const { resetFilters, getFilters } = useFilters(); + + const includes = getFilters('include', 'validators'); + const excludes = getFilters('exclude', 'validators'); + const hasFilters = includes?.length || excludes?.length; + + return ( + <div + style={{ + display: 'flex', + marginBottom: '1.1rem', + }} + > + <ButtonPrimaryInvert + text={t('order')} + marginRight + iconLeft={faArrowDownWideShort} + onClick={() => { + openPromptWith(<OrderValidators />); + }} + /> + <ButtonPrimaryInvert + text={t('filter')} + marginRight + iconLeft={faFilterCircleXmark} + onClick={() => { + openPromptWith(<FilterValidators />); + }} + /> + <ButtonSecondary + text={t('clear')} + onClick={() => { + resetFilters('include', 'validators'); + resetFilters('exclude', 'validators'); + }} + disabled={!hasFilters} + /> + </div> + ); +}; diff --git a/src/library/ValidatorList/OrderValidators.tsx b/src/library/ValidatorList/OrderValidators.tsx new file mode 100644 index 0000000000..b4e9175a20 --- /dev/null +++ b/src/library/ValidatorList/OrderValidators.tsx @@ -0,0 +1,40 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCheckCircle, faCircle } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { Title } from 'library/Prompt/Title'; +import { FilterListButton, FilterListWrapper } from 'library/Prompt/Wrappers'; +import { useFilters } from 'contexts/Filters'; +import { useValidatorFilters } from '../Hooks/useValidatorFilters'; + +export const OrderValidators = () => { + const { t } = useTranslation('library'); + const { getOrder, setOrder } = useFilters(); + const { ordersToLabels } = useValidatorFilters(); + + const order = getOrder('validators'); + + return ( + <FilterListWrapper> + <Title title={t('orderValidators')} /> + <div className="body"> + {Object.entries(ordersToLabels).map(([o, l]: any, i: number) => ( + <FilterListButton + $active={order === o ?? false} + key={`validator_filter_${i}`} + type="button" + onClick={() => setOrder('validators', o)} + > + <FontAwesomeIcon + transform="grow-5" + icon={order === o ? faCheckCircle : faCircle} + /> + <h4>{l}</h4> + </FilterListButton> + ))} + </div> + </FilterListWrapper> + ); +}; diff --git a/src/library/ValidatorList/ValidatorItem/Default.tsx b/src/library/ValidatorList/ValidatorItem/Default.tsx new file mode 100644 index 0000000000..1c0b79f022 --- /dev/null +++ b/src/library/ValidatorList/ValidatorItem/Default.tsx @@ -0,0 +1,148 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faBars, + faChartLine, + faGlobe, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMenu } from 'contexts/Menu'; +import { CopyAddress } from 'library/ListItem/Labels/CopyAddress'; +import { ParaValidator } from 'library/ListItem/Labels/ParaValidator'; +import { + Labels, + MenuPosition, + Separator, + Wrapper, +} from 'library/ListItem/Wrappers'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { usePlugins } from 'contexts/Plugins'; +import type { AnyJson } from 'types'; +import { Quartile } from 'library/ListItem/Labels/Quartile'; +import { useValidators } from '../../../contexts/Validators/ValidatorEntries'; +import { useList } from '../../List/context'; +import { Blocked } from '../../ListItem/Labels/Blocked'; +import { Commission } from '../../ListItem/Labels/Commission'; +import { EraStatus } from '../../ListItem/Labels/EraStatus'; +import { FavoriteValidator } from '../../ListItem/Labels/FavoriteValidator'; +import { Identity } from '../../ListItem/Labels/Identity'; +import { Oversubscribed } from '../../ListItem/Labels/Oversubscribed'; +import { Select } from '../../ListItem/Labels/Select'; +import { getIdentityDisplay } from './Utils'; +import type { ValidatorItemProps } from './types'; +import { Pulse } from './Pulse'; + +export const Default = ({ + validator, + toggleFavorites, + showMenu, + displayFor, +}: ValidatorItemProps) => { + const { t } = useTranslation('library'); + const { selectActive } = useList(); + const { pluginEnabled } = usePlugins(); + const { openModal } = useOverlay().modal; + const { setMenuPosition, setMenuItems, open } = useMenu(); + const { validatorIdentities, validatorSupers } = useValidators(); + + const { address, prefs, validatorStatus, totalStake } = validator; + const commission = prefs?.commission ?? null; + + const identity = getIdentityDisplay( + validatorIdentities[address], + validatorSupers[address] + ); + + // configure floating menu + const posRef = useRef(null); + const menuItems: AnyJson[] = []; + menuItems.push({ + icon: <FontAwesomeIcon icon={faChartLine} transform="shrink-3" />, + wrap: null, + title: `${t('viewMetrics')}`, + cb: () => { + openModal({ + key: 'ValidatorMetrics', + options: { + address, + identity, + }, + }); + }, + }); + + if (pluginEnabled('polkawatch')) { + menuItems.push({ + icon: <FontAwesomeIcon icon={faGlobe} transform="shrink-3" />, + wrap: null, + title: `${t('viewDecentralization')}`, + cb: () => { + openModal({ + key: 'ValidatorGeo', + options: { + address, + identity, + }, + }); + }, + }); + } + + const toggleMenu = () => { + if (!open) { + setMenuItems(menuItems); + setMenuPosition(posRef); + } + }; + + return ( + <Wrapper> + <div className={`inner ${displayFor}`}> + <MenuPosition ref={posRef} /> + <div className="row top"> + {selectActive && <Select item={validator} />} + <Identity address={address} /> + <div> + <Labels className={displayFor}> + <CopyAddress address={address} /> + {toggleFavorites && <FavoriteValidator address={address} />} + + {/* restrict opening modal within a canvas */} + {displayFor === 'default' && showMenu && ( + <div className="label"> + <button type="button" onClick={() => toggleMenu()}> + <FontAwesomeIcon icon={faBars} transform="shrink-2" /> + </button> + </div> + )} + </Labels> + </div> + </div> + <Separator /> + <div className="row bottom lg"> + <div> + <Pulse address={address} displayFor={displayFor} /> + </div> + <div> + <Labels style={{ marginBottom: '0.9rem' }}> + <Quartile address={address} /> + <Oversubscribed address={address} /> + <Blocked prefs={prefs} /> + <Commission commission={commission} /> + <ParaValidator address={address} /> + </Labels> + <EraStatus + address={address} + status={validatorStatus} + totalStake={totalStake} + noMargin + /> + </div> + </div> + </div> + </Wrapper> + ); +}; diff --git a/src/library/ValidatorList/ValidatorItem/Nomination.tsx b/src/library/ValidatorList/ValidatorItem/Nomination.tsx new file mode 100644 index 0000000000..ec3bc94285 --- /dev/null +++ b/src/library/ValidatorList/ValidatorItem/Nomination.tsx @@ -0,0 +1,81 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { ParaValidator } from 'library/ListItem/Labels/ParaValidator'; +import { Labels, Separator, Wrapper } from 'library/ListItem/Wrappers'; +import { Quartile } from 'library/ListItem/Labels/Quartile'; +import { useList } from '../../List/context'; +import { Blocked } from '../../ListItem/Labels/Blocked'; +import { Commission } from '../../ListItem/Labels/Commission'; +import { CopyAddress } from '../../ListItem/Labels/CopyAddress'; +import { FavoriteValidator } from '../../ListItem/Labels/FavoriteValidator'; +import { Identity } from '../../ListItem/Labels/Identity'; +import { Metrics } from '../../ListItem/Labels/Metrics'; +import { NominationStatus } from '../../ListItem/Labels/NominationStatus'; +import { Oversubscribed } from '../../ListItem/Labels/Oversubscribed'; +import { Select } from '../../ListItem/Labels/Select'; +import { getIdentityDisplay } from './Utils'; +import type { ValidatorItemProps } from './types'; +import { Pulse } from './Pulse'; + +export const Nomination = ({ + validator, + nominator, + toggleFavorites, + bondFor, + displayFor, + nominationStatus, +}: ValidatorItemProps) => { + const { selectActive } = useList(); + const { validatorIdentities, validatorSupers } = useValidators(); + + const { address, prefs } = validator; + const commission = prefs?.commission ?? null; + + return ( + <Wrapper> + <div className={`inner ${displayFor}`}> + <div className="row top"> + {selectActive && <Select item={validator} />} + <Identity address={address} /> + <div> + <Labels className={displayFor}> + <CopyAddress address={address} /> + {toggleFavorites && <FavoriteValidator address={address} />} + <Metrics + address={address} + display={getIdentityDisplay( + validatorIdentities[address], + validatorSupers[address] + )} + /> + </Labels> + </div> + </div> + <Separator /> + <div className="row bottom lg"> + <div> + <Pulse address={address} displayFor={displayFor} /> + </div> + <div> + <Labels style={{ marginBottom: '0.9rem' }}> + <Quartile address={address} /> + <Oversubscribed address={address} /> + <Blocked prefs={prefs} /> + <Commission commission={commission} /> + <ParaValidator address={address} /> + </Labels> + <NominationStatus + address={address} + bondFor={bondFor} + nominator={nominator} + status={nominationStatus} + noMargin + /> + </div> + </div> + </div> + </Wrapper> + ); +}; diff --git a/src/library/ValidatorList/ValidatorItem/Pulse.tsx b/src/library/ValidatorList/ValidatorItem/Pulse.tsx new file mode 100644 index 0000000000..0a09b5e18e --- /dev/null +++ b/src/library/ValidatorList/ValidatorItem/Pulse.tsx @@ -0,0 +1,179 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { Fragment } from 'react'; +import { + TooltipTrigger, + ValidatorPulseWrapper, +} from 'library/ListItem/Wrappers'; +import { useTooltip } from 'contexts/Tooltip'; +import { useTranslation } from 'react-i18next'; +import { MaxEraRewardPointsEras } from 'consts'; +import { useApi } from 'contexts/Api'; +import { normaliseEraPoints, prefillEraPoints } from './Utils'; +import type { PulseGraphProps, PulseProps } from './types'; + +export const Pulse = ({ address, displayFor }: PulseProps) => { + const { t } = useTranslation('library'); + const { isReady } = useApi(); + const { activeEra } = useNetworkMetrics(); + const { setTooltipTextAndOpen } = useTooltip(); + const { getValidatorPointsFromEras, eraPointsBoundaries, erasRewardPoints } = + useValidators(); + const startEra = activeEra.index.minus(1); + const eraRewardPoints = getValidatorPointsFromEras(startEra, address); + + const high = eraPointsBoundaries?.high || new BigNumber(1); + const normalisedPoints = normaliseEraPoints(eraRewardPoints, high); + const prefilledPoints = prefillEraPoints(Object.values(normalisedPoints)); + + const syncing = !isReady || !Object.values(erasRewardPoints).length; + const tooltipText = t('validatorPerformance', { + count: MaxEraRewardPointsEras, + }); + + return ( + <ValidatorPulseWrapper className={displayFor}> + {syncing && <div className="preload" />} + <TooltipTrigger + className="tooltip-trigger-element" + data-tooltip-text={tooltipText} + onMouseMove={() => setTooltipTextAndOpen(tooltipText)} + /> + <PulseGraph + points={prefilledPoints} + syncing={syncing} + displayFor={displayFor} + /> + </ValidatorPulseWrapper> + ); +}; + +export const PulseGraph = ({ + points: rawPoints = [], + syncing, + displayFor, +}: PulseGraphProps) => { + // Prefill with duplicate of start point. + let points = [rawPoints[0] || 0]; + points = points.concat(rawPoints); + // Prefill with duplicate of end point. + points.push(rawPoints[rawPoints.length - 1] || 0); + + const totalSegments = points.length - 2; + const vbWidth = 512; + const vbHeight = 115; + const xPadding = 5; + const yPadding = 10; + const xArea = vbWidth - 2 * xPadding; + const yArea = vbHeight - 2 * yPadding; + const xSegment = xArea / totalSegments; + let xCursor = xPadding; + + const pointsCoords = points.map((point: number, index: number) => { + const coord = { + x: xCursor, + y: vbHeight - yPadding - yArea * point, + zero: point === 0, + }; + + if (index === 0 || index === points.length - 2) { + xCursor += xSegment * 0.5; + } else { + xCursor += xSegment; + } + return coord; + }); + + const lineCoords = []; + for (let i = 0; i <= pointsCoords.length - 1; i++) { + const startZero = pointsCoords[i].zero; + const endZero = pointsCoords[i + 1]?.zero; + + lineCoords.push({ + x1: pointsCoords[i].x, + y1: pointsCoords[i].y, + x2: pointsCoords[i + 1]?.x || pointsCoords[i].x, + y2: pointsCoords[i + 1]?.y || pointsCoords[i].y, + zero: startZero && endZero, + }); + } + + return ( + <svg + width="100%" + height="100%" + viewBox={`0 0 ${vbWidth} ${vbHeight}`} + version="1.1" + xmlns="http://www.w3.org/2000/svg" + > + {lineCoords.map(({ x1 }, index) => { + if (index === 0 || index === lineCoords.length - 1) { + return <Fragment key={`grid_y_coord_${index}`} />; + } + return ( + <line + key={`grid_coord_${index}`} + strokeWidth="3.75" + stroke={ + displayFor === 'canvas' + ? 'var(--grid-color-secondary)' + : 'var(--grid-color-primary)' + } + x1={x1} + y1={0} + x2={x1} + y2={vbHeight} + /> + ); + })} + + {!syncing && + [{ y1: vbHeight * 0.5, y2: vbHeight * 0.5 }].map( + ({ y1, y2 }, index) => { + return ( + <line + key={`grid_coord_${index}`} + strokeWidth="3.75" + stroke={ + displayFor === 'canvas' + ? 'var(--grid-color-secondary)' + : 'var(--grid-color-primary)' + } + x1={0} + y1={y1} + x2={vbWidth} + y2={y2} + opacity={0.5} + /> + ); + } + )} + + {!syncing && + lineCoords.map(({ x1, y1, x2, y2, zero }, index) => { + const startOrEnd = index === 0 || index === lineCoords.length - 2; + const opacity = startOrEnd ? 0.25 : zero ? 0.5 : 1; + return ( + <line + key={`line_coord_${index}`} + strokeWidth={5} + opacity={opacity} + stroke={ + zero + ? 'var(--text-color-tertiary)' + : 'var(--accent-color-primary)' + } + x1={x1} + y1={y1} + x2={x2} + y2={y2} + /> + ); + })} + </svg> + ); +}; diff --git a/src/library/ValidatorList/ValidatorItem/Utils.tsx b/src/library/ValidatorList/ValidatorItem/Utils.tsx new file mode 100644 index 0000000000..aae21dab1c --- /dev/null +++ b/src/library/ValidatorList/ValidatorItem/Utils.tsx @@ -0,0 +1,83 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { u8aToString, u8aUnwrapBytes } from '@polkadot/util'; +import type BigNumber from 'bignumber.js'; +import { MaxEraRewardPointsEras } from 'consts'; + +export const getIdentityDisplay = (_identity: any, _superIdentity: any) => { + let displayFinal = ''; + let foundSuper = false; + + // check super identity exists, get display.Raw if it does + const superIdentity = _superIdentity?.identity ?? null; + const superRaw = _superIdentity?.superOf?.[1]?.Raw ?? null; + + const superDisplay = superIdentity?.info?.display?.Raw ?? null; + + // check if super raw has been encoded + const superRawAsBytes = u8aToString(u8aUnwrapBytes(superRaw)); + + // check if super identity has been byte encoded + const superIdentityAsBytes = u8aToString(u8aUnwrapBytes(superDisplay)); + + if (superIdentityAsBytes !== '') { + displayFinal = superIdentityAsBytes; + foundSuper = true; + } else if (superDisplay !== null) { + displayFinal = superDisplay; + foundSuper = true; + } + + if (!foundSuper) { + // cehck sub identity exists, get display.Raw if it does + const identity = _identity?.info?.display?.Raw ?? null; + + // check if identity has been byte encoded + const subIdentityAsBytes = u8aToString(u8aUnwrapBytes(identity)); + + if (subIdentityAsBytes !== '') { + displayFinal = subIdentityAsBytes; + } else if (identity !== null) { + displayFinal = identity; + } + } + if (displayFinal === '') { + return null; + } + + return ( + <> + {displayFinal} + {superRawAsBytes !== '' ? ( + <span>/ {superRawAsBytes}</span> + ) : superRaw !== null ? ( + <span>/ {superRaw}</span> + ) : null} + </> + ); +}; + +// Normalise era points between 0 and 1 relative to the highest recorded value. +export const normaliseEraPoints = ( + eraPoints: Record<string, BigNumber>, + high: BigNumber +): Record<string, number> => { + const percentile = high.dividedBy(100); + + return Object.fromEntries( + Object.entries(eraPoints).map(([era, points]) => [ + era, + points.dividedBy(percentile).multipliedBy(0.01).toNumber(), + ]) + ); +}; + +// Prefill low values where no points are recorded. +export const prefillEraPoints = (eraPoints: number[]): number[] => { + const missing = Math.max(MaxEraRewardPointsEras - eraPoints.length, 0); + + if (!missing) return eraPoints; + + return Array(missing).fill(0).concat(eraPoints); +}; diff --git a/src/library/ValidatorList/ValidatorItem/index.tsx b/src/library/ValidatorList/ValidatorItem/index.tsx new file mode 100644 index 0000000000..69568262dc --- /dev/null +++ b/src/library/ValidatorList/ValidatorItem/index.tsx @@ -0,0 +1,27 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React from 'react'; +import { Default } from './Default'; +import { Nomination } from './Nomination'; +import type { ValidatorItemProps } from './types'; + +export const ValidatorItemInner = (props: ValidatorItemProps) => { + const { format } = props; + + return format === 'nomination' ? ( + <Nomination {...props} /> + ) : ( + <Default {...props} /> + ); +}; + +export class ValidatorItem extends React.Component<ValidatorItemProps> { + shouldComponentUpdate(nextProps: ValidatorItemProps) { + return this.props.validator.address !== nextProps.validator.address; + } + + render() { + return <ValidatorItemInner {...this.props} />; + } +} diff --git a/src/library/ValidatorList/ValidatorItem/types.ts b/src/library/ValidatorList/ValidatorItem/types.ts new file mode 100644 index 0000000000..e84d9e696c --- /dev/null +++ b/src/library/ValidatorList/ValidatorItem/types.ts @@ -0,0 +1,29 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { MaybeAddress } from '@polkadot-cloud/react/types'; +import type { ValidatorListEntry } from 'contexts/Validators/types'; +import type { BondFor, DisplayFor } from 'types'; + +export interface ValidatorItemProps { + validator: ValidatorListEntry; + bondFor: BondFor; + displayFor: DisplayFor; + nominator: MaybeAddress; + format?: string; + showMenu?: boolean; + toggleFavorites?: boolean; + nominationStatus?: NominationStatus; +} + +export interface PulseProps { + address: string; + displayFor: DisplayFor; +} +export interface PulseGraphProps { + points: number[]; + syncing: boolean; + displayFor: DisplayFor; +} + +export type NominationStatus = 'active' | 'inactive' | 'waiting'; diff --git a/src/library/ValidatorList/index.tsx b/src/library/ValidatorList/index.tsx new file mode 100644 index 0000000000..ab464a887f --- /dev/null +++ b/src/library/ValidatorList/index.tsx @@ -0,0 +1,417 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBars, faGripVertical } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { isNotZero } from '@polkadot-cloud/utils'; +import { motion } from 'framer-motion'; +import React, { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ListItemsPerBatch, ListItemsPerPage } from 'consts'; +import { useApi } from 'contexts/Api'; +import { useFilters } from 'contexts/Filters'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useTheme } from 'contexts/Themes'; +import { useUi } from 'contexts/UI'; +import { + FilterHeaderWrapper, + List, + Wrapper as ListWrapper, +} from 'library/List'; +import { MotionContainer } from 'library/List/MotionContainer'; +import { Pagination } from 'library/List/Pagination'; +import { SearchInput } from 'library/List/SearchInput'; +import { Selectable } from 'library/List/Selectable'; +import { ValidatorItem } from 'library/ValidatorList/ValidatorItem'; +import type { Validator } from 'contexts/Validators/types'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { useNominationStatus } from 'library/Hooks/useNominationStatus'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { useValidatorFilters } from '../Hooks/useValidatorFilters'; +import { ListProvider, useList } from '../List/context'; +import type { ValidatorListProps } from './types'; +import { FilterHeaders } from './Filters/FilterHeaders'; +import { FilterBadges } from './Filters/FilterBadges'; +import type { NominationStatus } from './ValidatorItem/types'; + +export const ValidatorListInner = ({ + nominator: initialNominator, + validators: initialValidators, + allowMoreCols, + allowFilters, + toggleFavorites, + pagination, + format, + selectable, + bondFor, + onSelected, + actions = [], + showMenu = true, + displayFor = 'default', + allowSearch = false, + allowListFormat = true, + alwaysRefetchValidators = false, + defaultOrder = undefined, + defaultFilters = undefined, + disableThrottle = false, +}: ValidatorListProps) => { + const { t } = useTranslation('library'); + const { + networkData: { colors }, + } = useNetwork(); + const { + getFilters, + setMultiFilters, + getOrder, + setOrder, + getSearchTerm, + setSearchTerm, + resetFilters, + resetOrder, + clearSearchTerm, + } = useFilters(); + const { mode } = useTheme(); + const { isReady } = useApi(); + const { isSyncing } = useUi(); + const listProvider = useList(); + const { activeEra } = useNetworkMetrics(); + const { activeAccount } = useActiveAccounts(); + const { setModalResize } = useOverlay().modal; + const { injectValidatorListData } = useValidators(); + const { getNomineesStatus } = useNominationStatus(); + const { getPoolNominationStatus } = useBondedPools(); + const { applyFilter, applyOrder, applySearch } = useValidatorFilters(); + + const { selected, listFormat, setListFormat } = listProvider; + const includes = getFilters('include', 'validators'); + const excludes = getFilters('exclude', 'validators'); + const order = getOrder('validators'); + const searchTerm = getSearchTerm('validators'); + const actionsAll = [...actions].filter((action) => !action.onSelected); + const actionsSelected = [...actions].filter((action) => action.onSelected); + + // Determine the nominator of the validator list. Fallback to activeAccount if not provided. + const nominator = initialNominator || activeAccount; + + // Store the current nomination status of validator records relative to the supplied nominator. + const nominationStatus = useRef<Record<string, NominationStatus>>({}); + + // Get nomination status relative to supplied nominator, if `format` is `nomination`. + const processNominationStatus = () => { + if (format === 'nomination') + if (bondFor === 'pool') { + nominationStatus.current = Object.fromEntries( + initialValidators.map(({ address }) => [ + address, + getPoolNominationStatus(nominator, address), + ]) + ); + } else { + // get all active account's nominations. + const nominationStatuses = getNomineesStatus(nominator, 'nominator'); + + // find the nominator status within the returned nominations. + nominationStatus.current = Object.fromEntries( + initialValidators.map(({ address }) => [ + address, + nominationStatuses[address], + ]) + ); + } + }; + + // Injects status into supplied initial validators. + const prepareInitialValidators = () => { + processNominationStatus(); + const statusToIndex = { + active: 2, + inactive: 1, + waiting: 0, + }; + return injectValidatorListData(initialValidators).sort( + (a, b) => + statusToIndex[nominationStatus.current[b.address]] - + statusToIndex[nominationStatus.current[a.address]] + ); + }; + + // Current page. + const [page, setPage] = useState<number>(1); + + // Default list of validators. + const [validatorsDefault, setValidatorsDefault] = useState( + prepareInitialValidators() + ); + + // Manipulated list (custom ordering, filtering) of validators. + const [validators, setValidators] = useState(prepareInitialValidators()); + + // Store whether the validator list has been fetched initially. + const [fetched, setFetched] = useState(false); + + // Store whether the search bar is being used. + const [isSearching, setIsSearching] = useState(false); + + // Current render iteration. + const [renderIteration, setRenderIterationState] = useState<number>(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 + ); + + // Reset list when validator list changes. + useEffect(() => { + if (alwaysRefetchValidators) { + if ( + JSON.stringify(initialValidators.map((v) => v.address)) !== + JSON.stringify(validatorsDefault.map((v) => v.address)) + ) { + setFetched(false); + } + } else { + setFetched(false); + } + }, [initialValidators, nominator]); + + // handle filter / order update + const handleValidatorsFilterUpdate = ( + filteredValidators = Object.assign(validatorsDefault) + ) => { + if (allowFilters) { + if (order !== 'default') { + filteredValidators = applyOrder(order, filteredValidators); + } + filteredValidators = applyFilter(includes, excludes, filteredValidators); + if (searchTerm) { + filteredValidators = applySearch(filteredValidators, searchTerm); + } + setValidators(filteredValidators); + setPage(1); + setRenderIteration(1); + } + }; + + // get throttled subset or entire list + const listValidators = disableThrottle + ? validators + : validators.slice(pageStart).slice(0, ListItemsPerPage); + + // if in modal, handle resize + const maybeHandleModalResize = () => { + if (displayFor === 'modal') setModalResize(); + }; + + const handleSearchChange = (e: React.FormEvent<HTMLInputElement>) => { + const newValue = e.currentTarget.value; + + let filteredValidators = Object.assign(validatorsDefault); + if (order !== 'default') { + filteredValidators = applyOrder(order, filteredValidators); + } + filteredValidators = applyFilter(includes, excludes, filteredValidators); + filteredValidators = applySearch(filteredValidators, newValue); + + // ensure no duplicates + filteredValidators = filteredValidators.filter( + (value: Validator, index: number, self: Validator[]) => + index === self.findIndex((i) => i.address === value.address) + ); + + setValidators(filteredValidators); + setPage(1); + setIsSearching(e.currentTarget.value !== ''); + setRenderIteration(1); + setSearchTerm('validators', newValue); + }; + + // Set default filters. + useEffect(() => { + if (allowFilters) { + if (defaultFilters?.includes?.length) { + setMultiFilters( + 'include', + 'validators', + defaultFilters?.includes, + false + ); + } + if (defaultFilters?.excludes?.length) { + setMultiFilters( + 'exclude', + 'validators', + defaultFilters?.excludes, + false + ); + } + + if (defaultOrder) { + setOrder('validators', defaultOrder); + } + } + return () => { + if (allowFilters) { + resetFilters('exclude', 'validators'); + resetFilters('include', 'validators'); + resetOrder('validators'); + clearSearchTerm('validators'); + } + }; + }, []); + + // Handle validator list bootstrapping. + const setupValidatorList = () => { + setValidatorsDefault(prepareInitialValidators()); + setValidators(prepareInitialValidators()); + setFetched(true); + }; + + // Configure validator list when network is ready to fetch. + useEffect(() => { + if (isReady && isNotZero(activeEra.index) && !fetched) setupValidatorList(); + }, [isReady, activeEra.index, 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) onSelected(listProvider); + }, [selected]); + + // List ui changes / validator changes trigger re-render of list. + useEffect(() => { + if (allowFilters && fetched) handleValidatorsFilterUpdate(); + }, [order, isSyncing, includes, excludes]); + + // Handle modal resize on list format change. + useEffect(() => { + maybeHandleModalResize(); + }, [listFormat, renderIteration, validators, page]); + + return ( + <ListWrapper> + <List $flexBasisLarge={allowMoreCols ? '33.33%' : '50%'}> + {allowSearch && ( + <SearchInput + handleChange={handleSearchChange} + placeholder={t('searchAddress')} + /> + )} + <FilterHeaderWrapper> + <div>{allowFilters && <FilterHeaders />}</div> + <div> + {allowListFormat === true && ( + <> + <button type="button" onClick={() => setListFormat('row')}> + <FontAwesomeIcon + icon={faBars} + color={ + listFormat === 'row' ? colors.primary[mode] : 'inherit' + } + /> + </button> + <button type="button" onClick={() => setListFormat('col')}> + <FontAwesomeIcon + icon={faGripVertical} + color={ + listFormat === 'col' ? colors.primary[mode] : 'inherit' + } + /> + </button> + </> + )} + </div> + </FilterHeaderWrapper> + {allowFilters && <FilterBadges />} + + {listValidators.length > 0 && pagination && ( + <Pagination page={page} total={totalPages} setter={setPage} /> + )} + + {selectable ? ( + <Selectable + canSelect={listValidators.length > 0} + actionsAll={actionsAll} + actionsSelected={actionsSelected} + displayFor={displayFor} + /> + ) : null} + + <MotionContainer> + {listValidators.length ? ( + <> + {listValidators.map((validator, index) => ( + <motion.div + key={`nomination_${index}`} + className={`item ${listFormat === 'row' ? 'row' : 'col'}`} + variants={{ + hidden: { + y: 15, + opacity: 0, + }, + show: { + y: 0, + opacity: 1, + }, + }} + > + <ValidatorItem + validator={validator} + nominator={nominator} + toggleFavorites={toggleFavorites} + format={format} + showMenu={showMenu} + bondFor={bondFor} + displayFor={displayFor} + nominationStatus={ + nominationStatus.current[validator.address] + } + /> + </motion.div> + ))} + </> + ) : ( + <h4 style={{ marginTop: '1rem' }}> + {isSearching ? t('noValidatorsMatch') : t('noValidators')} + </h4> + )} + </MotionContainer> + </List> + </ListWrapper> + ); +}; + +export const ValidatorList = (props: ValidatorListProps) => { + const { selectActive, selectToggleable } = props; + return ( + <ListProvider + selectActive={selectActive} + selectToggleable={selectToggleable} + > + <ValidatorListInner {...props} /> + </ListProvider> + ); +}; diff --git a/src/library/ValidatorList/types.ts b/src/library/ValidatorList/types.ts new file mode 100644 index 0000000000..325c2fe4f0 --- /dev/null +++ b/src/library/ValidatorList/types.ts @@ -0,0 +1,33 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyJson } from '@polkadot-cloud/react/types'; +import type { Validator } from 'contexts/Validators/types'; +import type { AnyFunction, BondFor, DisplayFor, MaybeAddress } from 'types'; + +export interface ValidatorListProps { + validators: Validator[]; + bondFor: BondFor; + allowMoreCols?: boolean; + generateMethod?: string; + nominator?: MaybeAddress; + allowFilters?: boolean; + toggleFavorites?: boolean; + pagination?: boolean; + title?: string; + format?: 'nomination' | 'default'; + selectable?: boolean; + onSelected?: AnyFunction; + actions?: AnyJson[]; + showMenu?: boolean; + displayFor?: DisplayFor; + allowSearch?: boolean; + allowListFormat?: boolean; + alwaysRefetchValidators?: boolean; + defaultFilters?: AnyJson; + defaultOrder?: string; + disableThrottle?: boolean; + selectActive?: boolean; + selectToggleable?: boolean; + refetchOnListUpdate?: boolean; +} diff --git a/src/locale/cn/base.json b/src/locale/cn/base.json new file mode 100644 index 0000000000..cf19eb8a9a --- /dev/null +++ b/src/locale/cn/base.json @@ -0,0 +1,56 @@ +{ + "base": { + "active": "激活", + "allowAll": "允许所有", + "allowAnyoneCompound": "允许任何人代表您复利收益", + "allowAnyoneCompoundWithdraw": "允许任何人代表您复利或取出收益", + "allowAnyoneWithdraw": "允许任何人代表您取出收益", + "allowCompound": "允许复利", + "allowWithdraw": "允许取出收益", + "community": "社区", + "feedback": "反馈", + "goTo": "查看", + "help": "帮助", + "inactive": "未激活", + "network": "网络", + "nominate": "抵押", + "overview": "总览", + "payee": { + "account": { + "subtitle": "收益作为自由余额发到其他帐户", + "title": "转到其他帐户" + }, + "none": { + "subtitle": "未设置收益到账地址'", + "title_active": "未分配", + "title_default": "无" + }, + "staked": { + "subtitle": "自动将收益添加到现有的抵押余额中", + "title_active": "复利中", + "title_default": "复利" + }, + "stash": { + "subtitle": "收益作为自由余额发回账户", + "title": "转到您的帐户" + } + }, + "payouts": "收益", + "pools": "提名池", + "resources": "信息", + "stake": "抵押", + "support": "支持", + "time": { + "day": "天", + "hour": "小时", + "hr": "小时", + "min": "分钟", + "minute": "分钟", + "second": "秒" + }, + "title_kusama": "Kusama抵押平台", + "title_polkadot": "Polkadot抵押平台", + "title_westend": "Westend抵押平台", + "validators": "验证人" + } +} diff --git a/src/locale/cn/help.json b/src/locale/cn/help.json new file mode 100644 index 0000000000..b162cd38bd --- /dev/null +++ b/src/locale/cn/help.json @@ -0,0 +1,417 @@ +{ + "help": { + "definitions": { + "activeNominators": [ + "活跃提名人", + [ + "当前Session中活跃的提名人.", + "因为您的验证人也可能己超额认选,所以活跃的提名人并不能保证能获得奖励." + ] + ], + "activePools": [ + "活跃提名池", + ["{NETWORK_NAME}网络上当前活跃的提名池数量."] + ], + "activeStakeThreshold": [ + "保持活跃度的抵押阈值", + [ + "在一个Era中为保持活跃提名的所需的{NETWORK_UNIT}数量.", + "适用于提名人和提名池. 对于提名池情况来说, 在加入池时重要的是池的总质押金额不能低于该值.", + "高于这个标准能保证可以一直在这个Era的活跃提名人名单中. 但这数额不保证能得到奖励,因为您的活跃提名人有超额认选的可能.", + "在{NETWORK_NAME},只有前{MAX_NOMINATOR_REWARDED_PER_VALIDATOR}名提名人才能获得每个验证人的奖励. 确保您的活跃度质押金额高于此阈值将增加获得奖励的机会.", + "可以从页面中追踪这些指标, 并在必要时执行抵押操作如增加{NETWORK_UNIT}或更改提名或加入其它提名池." + ] + ], + "activeValidator": [ + "活跃验证人", + [ + "正在验证区块的验证人. 奖励根据验证人的活动表现累积.", + "每个Era都会选择一组新的验证人,因此不能保证同一验证人在随后的Era中会处于活跃状态.", + "{NETWORK_NAME}允许提名者最多提名16名验证人,最大限度地提高您在每个Era 提名活跃验证人的机会." + ] + ], + "adjustedRewardsRate": [ + "调整后收益率", + [ + "基于{NETWORK_NAME}奖励分配模型的估算年收益.", + "该数字实际上是历史报酬率减去通货膨胀率." + ] + ], + "averageCommission": [ + "平均佣金", + [ + "{NETWORK_NAME}上验证人的平均佣金额.", + "该指标不包括拥有100%佣金的验证人,因为这些节点通常不让提名是因为他们本身是交易平台自身来抵押." + ] + ], + "blockedNominations": [ + "停止提名", + ["当验证人停止(冻结)被提名功能时,提名人无法提名他们."] + ], + "bondedInPool": [ + "质押在池中的金额", + [ + "池中当前质押的{NETWORK_UNIT}金额.", + "直接抵押的质押金额保留在您的帐户中并被锁定,与之不同的是,质押到池中的{NETWORK_UNIT}将被转移到池的隐藏帐户。尽管如此,池成员仍然可以随时解除其资金的绑定." + ] + ], + "bonding": [ + "质押", + [ + "质押金额是“锁定”(或抵押){NETWORK_UNIT}的过程. 质押的{NETWORK_UNIT}将自动分配给一个或多个指定验证人.", + "作为提名人,您可自主分配提名额。在提名池中,提名池所有者或池提名人将代表您分配提名额,您的质押金将用于支持这些提名.", + "最低收益抵押额统计数是指在当前Era提名人中质押最少的{NETWORK_UNIT}.该值也是获得奖励所需的最低金额." + ] + ], + "commission": [ + "佣金", + [ + "验证人可以获得一定比例的奖励。这部分被称为他们的佣金.", + "提名佣金率较低的验证人意味着您将获得他们产生的更大份额的奖励.", + "许多验证人的佣金率为100%,这意味着提名这些验证人将不会获得任何奖励.", + "这类代表包括交易所运营的验证人,其中提名和奖励分配在相关交易所集中进行.", + "验证人可以随时更新他们的佣金率,这些变化将对您的盈利能力产生影响. 请务必在页面上监控您的提名,以留意其佣金率更新." + ] + ], + "controllerAccountEligibility": [ + "Controller账户标准", + [ + "一个账户要成为Controller,其余额必须有少最低存款. 在{NETWORK_NAME}上最低存款为 {EXISTENTIAL_DEPOSIT} {NETWORK_UNIT}.", + "如果一个帐户没达到该数额, 您将看到'{NETWORK_UNIT}不足' 的提示.", + "用至少{EXISTENTIAL_DEPOSIT} {NETWORK_UNIT}将使其符合资格并可选择作为Controller." + ] + ], + "epoch": [ + "Epoch", + [ + "Epoch是{NETWORK_NAME} Session的另一个名称. 每个Era分为6个Epoch, 在此期间, 验证人被指定为特定时段或Slots的区块生产者.", + "1个Epoch在波卡为4小时." + ] + ], + "era": [ + "Era", + [ + "在每个Era结束时,根据验证人在当前Era累积的Era点数奖励{NETWORK_UNIT}. 该奖励随后会分配给该验证人下的提名人.", + "1 Era目前在波卡是24小时." + ] + ], + "eraPoints": [ + "Era 点数", + [ + "Era 点由验证人在每个Era累积,取决于验证人的性能.", + "作为抵押者,您不需要在意Era点数. 一般来说,性能更好的验证人会产生更多的Era 点数,这反过来会得到更高的奖励." + ] + ], + "historicalRewardsRate": [ + "历史年收益", + ["根据{NETWORK_NAME}奖励分配数据估算的年收益."] + ], + "idealStaked": ["最优比例", ["理想网络条件下的抵押占发行量的百分比."]], + "inactiveNominations": [ + "非活跃提名", + ["指质押资金未分配到当前Era的活跃验证人群中的提名."] + ], + "inflation": [ + "通货膨胀", + [ + "{NETWORK_UNIT}具有通货膨胀性.所以无封顶数量.", + "在验证人的奖励是基于抵押金额, 其余归国库所有的情况下, 每年通货膨胀率约为10%." + ] + ], + "lastEraPayout": [ + "上一Era收益", + [ + "上一Era活跃的总{NETWORK_UNIT}奖励金额.", + "收益在该Era的活跃验证人之间平均分配,然后进一步分配给参与该Era的活跃提名人.", + "得到的收益金额取决于提名人和验证人自己在那个Era绑定了多少{NETWORK_UNIT}." + ] + ], + "ledgerHardwareWallets": [ + "Ledger硬件钱包", + [ + "Staking Dashboard己完全支持可兼容的Ledger设备.您可开始使用Ledger管理{NETWORK_NAME}上的抵押.", + "因为您的私钥一直在硬件设备上,所以硬件钱包也称为冷钱包.硬件钱包没有连接互联网,所以使用该类钱包可确保安全.", + "为了使用Ledger设备导入和签署交易, 您的设备必须安装{NETWORK_NAME} Ledger App. 该应用程序可以通过Ledger自己的应用程序Ledger Live安装.", + "支持USB和蓝牙连接. 如果您的Ledger有蓝牙功能, 并且您的设备允许蓝牙连接, 您将能够通过Ledger设备无线导入帐户并签署交易.", + "详情请在其官方网站上查找:ledger.com." + ] + ], + "ledgerRejectedTransaction": [ + "Ledger拒绝交易", + [ + "己在Ledger设备上拒绝交易, 该交易不会被上交.", + "请在Ledger设备上通过批准交易来提交交易." + ] + ], + "ledgerRequestTimeout": [ + "Ledger请求超时", + [ + "当Ledger设备繁忙时, 或者当操作未能及时完成时, 可能会发生超时报错.", + "请尝试再次执行该操作, 如果错误仍然存在, 请尝试重新启动Ledger设备." + ] + ], + "lockedBalance": [ + "锁定余额", + [ + "在{NETWORK_NAME}中,某些操作需要锁定余额,例如抵押或对治理进行投票。如果您的部分余额被锁定,则无法转移.", + "该应用显示的锁定余额代表除您的抵押余额外的所有锁定.", + "您可以用锁定余额做任何提名,但不能加入池,因为这需要转移余额到池帐户." + ] + ], + "minimumToCreatePool": [ + "最低建提名池质押金", + ["建池所需的最低{NETWORK_UNIT}金额.", "创建池需要比加入池更多的存款."] + ], + "minimumToJoinPool": [ + "最低入提名池质押金", + [ + "加入池所需的最低{NETWORK_UNIT}金额.", + "该金额不同于创建池所需的质押金." + ] + ], + "nominating": [ + "提名中", + [ + "提名是将{NETWORK_UNIT}作为抵押的验证人的过程. 每个帐户最多提名16个验证者.", + "一旦提名了选定的验证人,他们就会成为您的提名." + ] + ], + "nominationPools": [ + "提名池", + [ + "提名池允许用户通过抵押{NETWORK_UNIT}获得奖励.", + "与个人提名不同,使用提名池只需要少量{NETWORK_UNIT},提名池就会代表您管理提名人选." + ] + ], + "nominationStatus": [ + "提名状态", + [ + "当前的抵押状态.", + "当这些提名人中没有一个是在当前活跃验证人群内(被选择用于验证网络的验证人群体)时,这组提名将处于非活跃状态.", + "当您的被提名人中至少有一人是活跃时,此提名状态将显示为活跃提名-但这仍然不能保证奖励.", + "每个活跃验证人的前{MAX_NOMINATOR_REWARDED_PER_VALIDATOR}提名人可获得{NETWORK_NAME}奖励。因此,如果被提名人活跃且超额认购,您必须是{MAX_NOMINATOR_REWARDED_PER_VALIDATOR}最高质押提名人里的一员才能获得奖励.", + "如果一位活跃的被提名人没有超额认购,您将获得奖励." + ] + ], + "nominations": [ + "提名", + [ + "提名指选择验证人. 在{NETWORK_NAME}里最高提名数为 {MAX_NOMINATIONS}.", + "对于提名池,提名池所有者和池提名人负责代表所有的池成员提名验证人.", + "一旦提名一交, 质押金额会自动分配给当前Era活跃的提名人.", + "只要本人提名中至少有一个提名在当前session中活跃执行验证,您的资金就会与该验证人绑定从而获得奖励." + ] + ], + "nominatorStake": [ + "提名人抵押", + [ + "{NETWORK_UNIT}验证人的总提名人质押的数量.", + "该值和验证人的自我抵押一起形成验证人总抵押数,值得注意的是,该值因为随着提名人在该Session的质押金额给活跃验证人的重新分配,每一个era都会发生变化." + ] + ], + "openAppOnLedger": [ + "在Ledger上打开相应的App", + [ + "当{NETWORK_NAME} Ledger App当前未在连接的设备上打开时, 会出现此消息.", + "请在Ledger设备上打开 {NETWORK_NAME} App 然后重试." + ] + ], + "overSubscribed": [ + "超额认选", + [ + "只有每个验证人的前{MAX_NOMINATOR_REWARDED_PER_VALIDATOR}名提名人才能在{NETWORK_NAME}获得奖励. 当超过该数时,该验证人将被视为超额认选." + ] + ], + "payout": [ + "收益", + [ + "在{NETWORK_NAME}里抵押的收益. 取决于您验证人随时间累积的“Era点数.奖励金额会在每个Era结束时确定(24小时).", + "要获得抵押奖励,需要手动申领.任何支持该验证人的提名人都可以申领.", + "一个申请可触发每所有个提名人的奖励申领." + ] + ], + "payoutDestination": [ + "收益到账地址", + [ + "收益到账地址决定您的收益发送到哪个帐户", + "收益可以自动绑定在当前质押额上,也可以发送到您的Stash, Controller或您选择的外部帐户" + ] + ], + "payoutHistory": [ + "收益记录", + [ + "一名活跃提名人的收益历史记录.", + "申领奖励是一个手动的过程,可能会快速连续或以零星方式收到多次收益. 因此,您的收益图可能会在同一天发生多个收益,或者几天没有收益.", + "这并不意味着您在该期间没有提名或产生奖励,只是该期间的收益尚未被申领." + ] + ], + "polkadotVault": [ + "Polkadot Vault", + [ + "Polkadot Vault(前身为Parity Signer)是一种冷钱包解决方案, 允许您在飞行模式下将手机用作网络隔离钱包.", + "从技术上讲, Vault App不是一个钱包, 因为它不允许转账.", + "它更像是一个钥匙链工具, 能够创建、管理和恢复帐户." + ] + ], + "poolCommissionChangeRate": [ + "提名池佣金变化率", + [ + "佣金变化率由池提名主理人设置,并决定佣金可以增加多少以及多久增加一次.", + "最大增加率决定了在一次更新中可以增加多少佣金率. 最小延迟决定了在佣金率可被再次提高之前必须通过多少区块.", + "一旦设置了初始变化率,此后只能设置更限制的值。更限制的值包括更小的最大增加和更大的最小延迟." + ] + ], + "poolCommissionRate": [ + "提名池佣金率", + [ + "提名池的佣金率.该值由提名池主理人设置,是提名池将从其成员那里获得的奖励的百分比", + "必须提供收款人账户才能收取佣金.", + "佣金率在从提名池分配给会员之前的奖励中提取." + ] + ], + "poolMaxCommission": [ + "提名池最大佣金率", + [ + "最高佣金率可被提名池所有者设置.", + "此值由提名池主理人设置, 是提名池从其成员处获得的奖励的最大百分比.", + "一旦设定了初始最大佣金, 此后只能设定更限制的值." + ] + ], + "poolMembership": [ + "提名池成员制", + [ + "提名池成员身份状态会显示您是否是提名池成员.", + "提名池成员身份包括提名池成员或提名池所有者.", + "目前在{NETWORK_NAME}上,帐户一次只能加入一个提名池. 如果想加入另一个提名池,你必须先离开当前提名池.", + "要离开提名池,只需解除质押并取出所有质押的{NETWORK_UNIT}. 这里提供了一个专用的离开按钮,用于从提名池中解除绑定." + ] + ], + "poolRewards": [ + "提名池的奖励", + [ + "作为提名池的活跃参与者所产生的{NETWORK_UNIT}奖励金额.", + "用户需要申请提名池奖励才能获得奖励.", + "用户有2种领取奖励的选择. 奖励可以绑定回池, 这将增加您在池中的份额并积累更多奖励. 或作为免费{NETWORK_UNIT}发送到您的帐户." + ] + ], + "poolRoles": [ + "提名池里的角色", + [ + "一个提名池里有4种角色,每个角色在管理池的运行方面具有不同的职责.", + "主理人:可以更改提名人、守护人或其本身. 此外,它可以执行提名人或守护人可以执行的任何操作.", + "存款人:创建提名池并作为初始成员.只有在所有其他成员离开后能离开提名池.一旦他们退出,提名池将从系统中完全移除.", + "提名人:可以选择提名池验证人.", + "守护人:如果提名池被冻结,可以更改池的状态和踢出(无权限解除绑定/撤回)成员." + ] + ], + "proxyAccounts": [ + "代理帐户", + [ + "代理帐户是代理另一个帐户的帐户.", + "在代理帐户术语中, 代理帐户称为委托帐户, 代理帐户所代表的代理帐户被称为委托人帐户.", + "Staking Dashboard允许被委托人代表委托人签署交易. 如果代理账户在没有委托人的委托的情况下而存在, 则委托人将自动列为只读帐户." + ] + ], + "readOnlyAccounts": [ + "只读帐户", + [ + "只读帐户是可以导入但不能签署交易记录的帐户.这意味着您可以查看帐户的余额和质押信息, 但不能使用它执行任何抵押操作." + ] + ], + "reserveBalance": [ + "储备金额", + [ + "在{NETWORK_NAME}中,帐户必须有高于一定金额的余额才能在链上存在.这一数额称为“最低存款“.", + "该应用确保账户永远不会低于这个数额." + ] + ], + "reserveBalanceForExistentialDeposit": [ + "为最低存款存储", + [ + "如果您的账户已经锁定了足够涵盖最低存款的金额, 例如为提名而锁定的金额, Staking Dashboard则不会另外锁定任何额外的可用金额.", + "在这种情况下, \"无\" 会显示在存储下的 \"为最低存款存储\"部分." + ] + ], + "rewardsByCountryAndNetwork": [ + "按国家和网络划分的收益", + [ + "显示来自不同国家和IP网络收益的预估百分比.", + "Polkawatch分布式分析器是通过聚合验证人的IP位置的收益来计算的, 取样期为过去的60天." + ] + ], + "selfStake": [ + "自我抵押", + [ + "验证人自身质押的{NETWORK_UNIT}数量.", + "这个值也会被添加到提名人质押{NETWORK_UNIT}数量中,作为验证人的总抵押数的一部分." + ] + ], + "stashAndControllerAccounts": [ + "Stash和Controller帐户", + [ + "Stash和Controller只是用于管理抵押操作的{NETWORK_NAME}账户.", + "Stash账户是用于存放抵押资金的账户,而Controlle账户则用于代表Stash账户执行抵押操作.", + "切换帐户实际上是在自动切换Stash帐户到Controller帐户.", + "请提前导入Stash和Controller帐户。否则将无法使用该应用的所有功能.", + "可在抵押页面上导入不同的Controller帐户." + ] + ], + "supplyStaked": [ + "抵押比例", + [ + "目前全球{NETWORK_UNIT}的累计发行量.", + "抵押的百分比与{NETWORK_UNIT}总发行量相关." + ] + ], + "totalNominators": [ + "总提名人数", + [ + "在网络中参与抵押的账户,无论他们在当前Session中是否活跃.", + "抵押{NETWORK_UNIT}的前提是成为提名人或加入提名池,提名池本身就是一个提名人." + ] + ], + "validator": [ + "验证人", + [ + "验证在{NETWORK_NAME}中继链中的区块的实体. 验证人通过确保网络安全并生成区块在{NETWORK_NAME}起着关键作用.", + "作为提名人,您可以选择支持哪个验证人并获得奖励." + ] + ], + "wrongTransaction": [ + "错误交易", + [ + "此错误发生原因为当Ledger设备正在签名或签名后的交易与Staking Dashboard上当前活动交易不同.", + "请确保Ledger设备上没有未完成的交易, 然后重试." + ] + ], + "yourBalance": [ + "余额", + [ + "除了抵押的总金额外,还包括在提名池中抵押了的{NETWORK_UNIT}总金额.", + "和抵押的金额不同,质押的池金额是被持有并锁定在提名池中." + ] + ] + }, + "externals": { + "bondMore": "质押更多代币到现有的抵押", + "changeAccount": "更改您的Controller账号", + "changeDestination": "更改奖励钱包地址", + "changeNominations": "更改您的提名", + "chooseValidators": "如何选择验证人?", + "claimRewards": "申领提名池奖励", + "connectAccounts": "如何连接您的帐户", + "createPools": "创建提名池", + "howToUse": "如何使用Staking Dashboard:概述", + "rebonding": "解除质押中", + "stakeDot": "抵押您的DOT", + "unbondingTokens": "解除您的质押" + }, + "modal": { + "articles": "文章", + "close": "关闭", + "definitions": "定义", + "helpResources": "帮助信息", + "related": "相关" + } + } +} diff --git a/src/locale/cn/library.json b/src/locale/cn/library.json new file mode 100644 index 0000000000..24debbb519 --- /dev/null +++ b/src/locale/cn/library.json @@ -0,0 +1,191 @@ +{ + "library": { + "100Commission": "100% 佣金", + "accountConnected": "帐户已连接", + "accounts": "账户", + "active": "活跃", + "activeLowCommission": "活跃低佣金", + "activeLowCommissionSubtitle": "选择低佣金且高效表现的验证人", + "activePools": "活跃提名池", + "activeValidator": "活跃验证人", + "activeValidators": "活跃验证人", + "add": "添加", + "addFromFavorites": "从收藏夹添加", + "address": "地址", + "addressCopiedToClipboard": "复制到剪贴板的地址", + "all": "全部", + "alreadyImported": "地址已导入", + "asAPoolMember": "作为提名池成员", + "asThePoolDepositor": "作为提名池存款人", + "atLeast": "质押金最低为", + "available": "可用", + "backToMethods": "返回方案选择", + "backToScan": "回到扫描", + "blockedNominations": "己冻结提名", + "blockingNominations": "冻结提名中", + "bond": "质押", + "bondAmountDecimals": "质押金额最多只能有 {{units}}个小数位", + "bondDecimalsError": "质押金额能最多有 {{units}} 位点数", + "bonded": "己质押", + "cancel": "取消", + "cancelled": "已取消", + "chooseValidators": "最多能选择 {{maxNominations}} 个验证人。", + "chooseValidators2": "自动生成提名或手动加入提名", + "clear": "清除", + "clearSelection": "清除选择", + "clickToReload": "重新加载", + "complete": "完成", + "confirm": "确认", + "confirmReformat": "地址已重新格式化。请确认", + "connect": "连接", + "connectedTo": "连接到", + "connectedToNetwork": "已连接网络", + "connecting": "连接中", + "continue": "继续", + "copyAddress": "复制地址", + "copyPoolAddress": "复制池地址", + "createPool": "创建提名池", + "dayAverage": "日平均值", + "dayPerformance": "天内表现", + "dayPerformanceStanding": "{{count}}天活跃验证人内表现排名", + "dayPoolPerformance": "天内提名池表现", + "destroying": "销毁中", + "destroyingPools": "正在销毁提名池", + "disclaimer": "免责声明", + "disconnected": "已断开", + "displayingValidators": "正在显示 {{count}} 个验证人", + "done": "完成", + "enablePermissionlessClaiming": "启用己许可申领", + "eraPoints": "Era 点数", + "errorUnknown": "抱歉,页面出现点小问题哦", + "errorWithTransaction": "交易出错", + "estimatedFee": "预计费用", + "exclude": "不含", + "failed": "失败", + "fastUnstake": "快速解除抵押", + "fastUnstakeCheckingEras": "正在查验 {{total}} Eras中的 {{checked}}", + "fastUnstakeExposed": "在 {{count}} Era前己被显示", + "favorite": "收藏夹", + "favoritePoolAdded": "己添加提名池", + "favoritePoolRemoved": "己删除提名池", + "favoriteValidatorAdded": "验证人已添加到收藏夹", + "favoriteValidatorRemoved": "收藏夹已删除验证人", + "filter": "筛选", + "filterValidators": "过滤验证人", + "finalized": "交易已确认", + "free": "己解锁", + "fromFavorites": "来自收藏夹", + "fromFavoritesSubtitle": "获取一组您喜欢的验证人", + "graphInactive": "不活跃", + "highCommission": "高佣金", + "highPerformanceValidator": "高效表现的验证人", + "iHaveScanned": "己扫描", + "import": "导入", + "importing": "导入中", + "inBlock": "己在区块中", + "inQueue": "在队列中", + "inactive": "非活跃", + "include": "包含", + "insertPayoutAddress": "输入收益到账地址", + "invalid": "地址无效", + "join": "加入提名池", + "legalDisclosures": "法律论述", + "listItemActive": "活跃", + "locked": "己锁", + "lockedPools": "已锁定提名池", + "lowCommission": "低佣金", + "manual": "手动", + "manualSelectionSubtitle": "从头开始添加验证人", + "manual_selection": "手动选择", + "max": "最高", + "minimumBond": "最低质押为 {{minBondUnit}} {{unit}}", + "missingIdentity": "无ID", + "moreThanBalance": "质押金额超过余额", + "next": "下一页", + "noFilters": "无筛选", + "noFree": "您没有可用的 {{unit}} 可质押", + "noMatch": "没有符合此条件的池", + "noPayoutAddress": "无收益到账地址", + "noValidators": "没有验证人", + "noValidatorsMatch": "没有符合此条件的验证人", + "nominate": "提名", + "nominateActive": "激活", + "nominateInactive": "未激活", + "nominationsReverted": "已恢复原来提名", + "nominator": "提名人", + "notEnough": "不足", + "notEnoughAfter": "交易费用后 {{unit}} 不足以质押", + "notEnoughFunds": "您的 {{unit}} 不足以提交这次交易", + "notImported": "未导入", + "notMeet": "未达到最低质押金款", + "notNominating": "非提名状态中", + "notStaking": "无抵押", + "notValidAddress": "无效地址", + "optimalSelection": "最佳选择", + "optimalSelectionSubtitle": "选择表现最佳且定期活跃的验证人", + "order": "顺序", + "orderValidators": "验证人排序", + "overSubscribed": " 己超额认选", + "overSubscribedMinReward": "超额认选:最低奖励质押为", + "page": "{{page}} / {{total}}", + "payout": "收益", + "payoutAccount": "收益到账账户", + "payoutAddress": "收益到账地址", + "pending": "待定中", + "permissionlessClaimingTurnedOff": "己许可申领己关闭", + "points": "点数", + "pool": "提名池", + "poolClaim": "提名池申领", + "poolCommission": "提名池佣金", + "poolId": "提名池ID", + "poolMembers": "提名池成员", + "prev": "上一页", + "privacy": "隐私", + "proxy": "代理账户", + "randomValidator": "随机验证人", + "reGenerate": "重新生成", + "remove": "删除", + "removeSelected": "移除选定项", + "reset": "重设", + "revertedToActiveSelection": "提名已恢复为您原来的选择", + "scanPolkadotVault": "请在Polkadot Vault上扫描", + "search": "搜索池ID、名称或地址", + "searchAddress": "搜索地址或身份", + "select": "选择", + "sign": "签署", + "signPolkadotVault": "请在Polkadot Vault上签名", + "signedByController": "由Controller签署", + "signedByProxy": "代理账户己签名", + "signer": "签名账户", + "signing": "签署中", + "submitTransaction": "准备提交交易", + "syncing": "正在同步", + "syncingPoolList": "同步提名池列表", + "tooSmall": "质押金额太少", + "top": "首", + "transactionCancelled": "交易已取消", + "transactionInBlock": "交易己包含区块里", + "transactionInitiated": "交易已启动", + "transactionSuccessful": "交易成功", + "unbond": "解除质押", + "unbondAmount": "解除金额超过质押余额", + "unbonding": "正在解除质押", + "unclaimedPayouts": "未申领收益", + "unlocking": "正在解锁", + "unordered": "无顺序", + "update": "更改", + "valid": "可用地址", + "validAddress": "有效地址", + "validatingParachainBlocks": "验证平行链区块", + "validatorCommission": "验证人佣金", + "validatorPerformance": "{{count}}天内验证人表现", + "valueTooSmall": "值太小", + "viewDecentralization": "分布式指标", + "viewMetrics": "验证人指标", + "viewPoolNominations": "查看池提名", + "waiting": "等待中", + "walletNotFound": "未找到钱包", + "whenActivelyNominating": "当活跃提名时", + "wrongTransaction": "错误交易,请重新签名" + } +} diff --git a/src/locale/cn/modals.json b/src/locale/cn/modals.json new file mode 100644 index 0000000000..982b7c3ed6 --- /dev/null +++ b/src/locale/cn/modals.json @@ -0,0 +1,279 @@ +{ + "modals": { + "aboveExisting": "高于现有", + "aboveGlobalMax": "高于总体最大值", + "aboveMax": "高于最大值", + "account": "账户", + "accountAlreadyImported": "帐户已导入", + "accounts": "账户", + "activeRoles": "有活跃角色在 {{count}} 个提名池", + "activeRoles_zero": "无活跃角色在提名池", + "add": "添加", + "addToBond": "添加到质押", + "addToNominations": "添加到提名列表", + "addUpToFavorites": "您最多可从收藏夹添加到{{count}}个验证人", + "addedToBond": "这笔{{unit}} 将添加到您当前的质押金中", + "addingFavorite": "正在添加{{count}} 个提名", + "addressReceived": "己收到地址:", + "afterClaiming": "申领后资金将立即变成可用余额", + "allNominations": "所有提名", + "allPoolRoles": "所有池角色", + "allowToJoin": "允许新成员加入", + "amountToBond": "质押额:", + "approveTransactionLedger": "在Ledger上批准交易", + "back": "返回", + "beingDestroyed": "正在销毁此池。", + "belowExisting": "低于现有", + "beyondMaxIncrease": "超出最大增量", + "binanceApi": "币安API", + "bond": "质押", + "bondAll": "质押所有", + "bondAllAvailable": "质押所有可用金额", + "bondExtra": "质押多余", + "bondMore": "质押更多", + "bondingWithdraw": "质押还将提取您的未申领收益", + "bouncer": " 守护人", + "braveText": "<b> 致Brave的用户</b> ! 由于最近的更新 (<i>Brave 版本 1.36</i>), 使用轻型客户端时可能会出现问题(例如不能连接).", + "cancel": "取消", + "changeControllerAccount": "更改Controller 帐户", + "changeNomination": "一旦提交,您的提名将被移除,从下一个Era开始就不会被提名", + "changePoolRoles": "更改池角色", + "changeRate": "更改率", + "changeToDestroy": "更改为销毁状态", + "checking": "检验中...", + "chooseLanguage": "选择语言", + "claim": "申领", + "claimCommission": "申领佣金", + "claimOutstandingCommission": "在提名池奖励帐户中申领任何未付佣金", + "claimPayouts": "申领收益", + "claimReward1": "提交后,收益将被绑定回提名池中。可以随时提取", + "claimReward2": "提取收益将立即将其作为余额转入帐户", + "claimsOnBehalf": "代表所有提名同一验证人的提名人申领收益。交易费通常更高,大多数提名人都依赖验证人代表他们进行收益申领", + "commissionRate": "佣金值率", + "compound": "复利", + "confirmReset": "确认重设", + "connect": "连接", + "connectLedgerToContinue": "连接您的Ledger 设备继续", + "connected": "己连接", + "connectionType": "连接类型", + "continue": "继续", + "controllerImported": "您必须导入您的Controller帐户才能签署此交易", + "dashboardTips": "提示消息", + "days": "天", + "decentralizationAnalyticsNotAvailable": "无法使用该分布式分析器", + "decentralizationAnalyticsNotSupported": "此网络不支持该分布式分析器", + "declare": "添加", + "declared": "己添加", + "destroyIrreversible": "销毁池是不可逆转的", + "destroyPool": "销毁提名池", + "destroyPoolResult": "销毁提名池后,所有成员都可以无权限解除质押,提名池永远无法重新开启", + "developerTools": "开发者工具", + "differentNetworkAddress": "不同的网络地址", + "disconnect": "断开", + "done": "完成", + "ensureLedgerIsConnected": "提示:请确保您的Ledger设备已连接", + "exitYourStakingPosition": "退出抵押", + "extensionConnected": "扩展已连接", + "extensions": "扩展", + "fastUnstakeCurrentQueue": "当前处于快速解除抵押队列中的帐户数", + "fastUnstakeExposedAgo": "您上次被显示于 {{count}} Era 前", + "fastUnstakeNote1": "如需快速解除抵押, 您必须无活跃抵押长于 {{bondDuration}} 个Eras", + "fastUnstakeNote2": "如您在至少 {{count}} 个era内处于非活动状态,您可选择快速解除抵押", + "fastUnstakeOnceRegistered": "点击快速解除抵押后,您将被列在等待队列中", + "fastUnstakeRegistered": "已点击并等待快速解除抵押中", + "fastUnstakeSubmit_cancel": "取消快速解除抵押", + "fastUnstakeSubmit_register": "快速解除抵押", + "fastUnstakeUnorderedNote": "快速解除抵押队列是无序的,因此被选择的确切时间是不确定的", + "fastUnstakeWarningUnlocksActive": "您有 {{count}} 个己解锁处于活跃状态, ", + "fastUnstakeWarningUnlocksActiveMore": "无己解锁可被快速解除抵押。重新解除质押或撤回己解锁以重回绑定,然后再次尝试快速解除抵押", + "fastUnstake_register": "点击快速解除抵押", + "fastUnstake_title": "快速解除抵押", + "favoriteNotNominated": "收藏夹中的验证人/未提名", + "favoriteValidators": "收藏夹中的验证人", + "favoritesAddedSubtitle": "已将{{count}}个验证人添加到您的提名列表中", + "favoritesAddedTitle": "已添加", + "feedback": "反馈", + "feedbackPage": "我们的反馈页面在", + "for": "委托人 {{who}}", + "forget": "清除", + "free": "可用余额", + "getAnotherAccount": "获取更多帐户", + "gettingAccount": "正在获取帐户", + "gettingAddress": "正在获取地址...", + "goToAccounts": "帐户", + "goToConnect": "连接", + "hardware": "硬件", + "hide": "隐藏", + "hours": "小时", + "import": "导入", + "importAccount": "导入帐户", + "importAddress": "导入地址", + "importAnotherAccount": "导入另一个帐户", + "importedAccount": "{{count}} 个己导入帐户", + "inPool": "提名池中", + "inputAddress": "输入地址", + "inputDelegatorAddress": "输入委托人地址", + "inputPayeeAccount": "输入收款人账户", + "invalidAddress": "无效地址", + "joinPool": "加入池", + "leavePool": "离开池", + "ledgerAccount": "Ledger帐户", + "ledgerAccounts": "正在显示Ledger帐户", + "ledgerRequestTimeout": "Ledger 请求超时.请再试一次", + "ledgerWillBeReset": "您的Ledger帐户列表将被重置,并且所有导入的帐户都将被删除", + "lightClient": "轻客户端", + "lockPool": "锁定", + "lockPoolSubtitle": "一旦锁定提名池,其他人就无法加入提名池", + "manageCommission": "管理佣金值", + "manageNominations": "提名管理", + "managePool": "管理池", + "maxCommission": "最高佣金值", + "maximumCommissionUpdated": "最高佣金值最新值", + "minDelayBetweenUpdates": "更新之间的最小延迟", + "minutes": "分钟", + "missingNesting": "该调用缺少内置支持,无法签署交易", + "months": "月", + "moreFavoritesSurpassLimit": "添加更多收藏夹提名人将超过{{max}}的提名限制", + "networks": "网络", + "newTotalBond": "新质押总额:", + "newlyBondedFunds": "新质押金将从下一个Era开始归回到活跃的提名中", + "noAccounts": "无帐户", + "noActiveAccount": "无活跃帐户", + "noEnough": "您没有足够的可使用余额做快速解除抵押。快速解除抵押需要余额", + "noFavoritesAvailable": "无收藏夹", + "noFavoritesSelected": "无选中收藏夹", + "noNominationsSet": "没有提名任何人", + "noNominatorRole": "您未参与任何提名池活动", + "noProxyAccountsDeclared": "尚未添加任何代理帐户", + "noReadOnlyAdded": "未添加任何只读帐户", + "noRewards": "无收益可申领", + "noVaultAccountsImported": "尚未导入任何Polkadot Vault帐户", + "nominateFavorites": "提名收藏夹中的验证人", + "nominating": "提名中", + "nominatingAndInPool": "己在提名并提名池中", + "nomination": "{{count}} 个提名", + "nominator": "提名人", + "nominatorStake": "提名人抵押", + "none": "无", + "notAuthenticated": "未验证", + "notInstalled": "未安装", + "notMeetMinimum": "未达到{{minNominatorBondUnit}} {{unit}}的提名人最低额.请在提名前质押一些资金", + "notStaking": "无抵押", + "notToClaim": "验证人通常代表其提名人申领收益。如果您决定现在不申领收益,您很可能会在1-2天内收到收益", + "notePoolDepositorMinBond_depositor": "作为提名池存款人,您必须至少持有 {{bond}} {{unit}}", + "notePoolDepositorMinBond_member": "作为一名提名池成员,你必须至少持有 {{bond}} {{unit}}", + "onceUnbonding": "一旦解除质押,您的资金过了{{bondDurationFormatted}} 后就能获回", + "openAppOnLedger": "在您的Ledger设备中打开{{appName}} App", + "openFeedback": "在 Canny.io反馈", + "payeeAdded": "已添加收款人", + "payoutDestination": "收益到账地址", + "pendingPayout": "{{count}} 个待申领收益", + "polkawatchDisabled": "Polkawatch己断开", + "pool": "池", + "poolIsNotNominating": "该提名池未提名任何验证人", + "poolName": "提名池名称", + "poolNominations": "池的提名", + "provider": "服务端", + "proxies": "代理", + "proxy": "代理账户", + "proxyAccounts": "代理帐户", + "queuedTransactionRejected": "上一笔交易己被拒绝.请再试一次", + "readOnly": "只读", + "readOnlyAccounts": "只读帐户", + "readOnlyCannotSign": "只读帐户无法签署交易", + "rebond": "重新质押", + "rebondSubtitle": "从下Era开始, 重新质押的资金将重回活跃的提名中", + "rebondUnlock": "在此之后可随时重新质押解锁,或将其提出", + "recentEraPoints": "最近Era点数", + "registerFastUnstake": "快速解除抵押需要余额", + "remove": "解除", + "removeAccount": "删除帐户", + "removeBond": "解除质押", + "renamePool": "重命名", + "reserveBalance": "预存金额", + "reserveForExistentialDeposit": "为最低存款预存金额", + "reserveForTxFees": "为交易费用预存金额", + "reserveText": "控制预存多少{{unit}}用于支付交易费用. 这笔金额是在您账户最低存款基础上增加的.", + "resetLedgerAccounts": "重设Ledger帐户", + "revertChanges": "回复原状", + "revertNominationChanges": "您确定要还原更改前的提名吗?", + "revertNominations": "还原提名", + "rewards": "收益", + "rewardsByCountryAndNetwork": "按国家/地区和网络供应商划分的收益", + "root": "主理人", + "rpcProviders": "RPC服务端", + "scanFromPolkadotVault": "从Polkadot Vault中扫描", + "searchAccount": "搜索帐户", + "selectNetwork": "网络选择", + "selectRpcProvider": "可选择任意一个RPC服务端来更改Staking Dashboard连接的{{network}}节点", + "selected": "己选", + "selfStake": "自我抵押", + "sentToCommissionPayee": "此金额将发送到为此提名池己设置的佣金收款人帐户", + "setToDestroying": "销毁", + "setToDestroyingSubtitle": "将提名池设置为销毁后将无法撤回, 请确定计划关闭提名池时才设置此状态", + "settings": "设置", + "signedTransactionSuccessfully": "已成功签署交易", + "stop": "停止", + "stopJoiningPool": "阻止新成员加入", + "stopNominating": "停止提名", + "stopNominatingBefore": "在解除所有质押前停止提名", + "storedOnChain": "更新的名称将作为编码字节存储在链上, 更新后将立即生效", + "submit": "提交", + "submitLock": "提交提名池锁定", + "submitUnlock": "提交提名池解锁", + "submitting": "提交中", + "subscanDisabled": "Subscan己断开", + "successfullyFetchedAddress": "成功获取地址", + "thisMinimumDelay": "这个最小延迟近似等于{{count}}个块", + "titleExtensionConnected": "{{title}} 扩展已连接", + "toggleFeatures": "功能切换", + "togglePlugins": "切换插件", + "total": "总共", + "transactionRejectedPending": "交易被拒后等待处理中", + "tryAgain": "请重试一次", + "unbond": "解除质押", + "unbondAll": "解除所有质押", + "unbondErrorBelowMinimum": "无法取消质押.您的债券资金低于{{bond}} {{unit}}", + "unbondErrorNoFunds": "您无{{unit}}可解除质押", + "unbondFundsLeavePool": "解除质押并退出提名池", + "unbondMemberFunds": "解除会员质押金", + "unbondSomeOfYour": "解除部分质押", + "unbondToMaintain": "存款人解除质押到{{minJoinBondUnit}} {{unit}}最小值,以保持池成员身份", + "unbondToMinimum": "解除至最少质押", + "unbondToMinimumCreate": "存款人解除质押到{{minCreateBondUnit}} {{unit}}的最低保证金", + "unbonding": "解除质押中", + "unbondingWithdraw": "解除质押还将提取您的未申领收益", + "undergoingMaintenance": "正在进行维护", + "unlockChunk": "己解锁的金额当前无法在池中重新质押。如果您希望重新质押,请先取出解锁金额并添加到您的质押中", + "unlockLedgerToContinue": "解锁您的Ledger设备后继续", + "unlockPool": "解锁", + "unlockPoolSubtitle": "一旦提名池被解锁, 任何帐户都可以作为成员加入提名池", + "unlockTake": "{{bondDurationFormatted}}后解锁金额可被提取", + "unlocked": "已解锁", + "unlocks": "解锁", + "unlocksAfter": "解锁于", + "unlocksInEra": "解锁于Era", + "unstake": "解除抵押", + "unstakeStopNominating": "停止提名 {{count}} 个验证人", + "unstakeUnbond": "解除质押 {{bond}} {{unit}}", + "updateClaimPermission": "更新申领权限", + "updateName": "更改名称", + "updatePayoutDestination": "更新收益到账地址", + "updatePoolCommission": "更新提名池佣金设置", + "updateWhoClaimRewards": "更改代表申领奖励地址", + "updated": "己更新", + "validatorDecentralization": "验证人的分布式地理指标", + "validatorMetrics": "验证人指标", + "vaultAccounts": "{{count}} 个帐户己被导入", + "vaultAccounts_zero": "无帐户导入", + "waitingForQRCode": "请扫描二维码", + "web": "网络", + "welcomeToReport": "欢迎Bug报告、功能请求和改进", + "willSurpass": "添加过多收藏夹将超{{maxNominations}}提名数", + "withdraw": "提取", + "withdrawMemberFunds": "取出会员资金", + "withdrawSubtitle": "资金将在取款后立即转作可用余额", + "withdrawUnlocked": "取出己解锁", + "years": "年" + } +} diff --git a/src/locale/cn/pages.json b/src/locale/cn/pages.json new file mode 100644 index 0000000000..43ac4827e2 --- /dev/null +++ b/src/locale/cn/pages.json @@ -0,0 +1,230 @@ +{ + "pages": { + "community": { + "bio": "简介", + "connecting": "连接中", + "email": "邮箱", + "fetchingValidators": "正在获取验证人信息", + "goBack": "返回", + "noValidators": "不包含验证人", + "validator": "{{count}} 个验证人", + "website": "个人网站" + }, + "nominate": { + "activeNominations": "活跃提名人", + "addressCopied": "地址已复制到剪贴板", + "automaticallyBonded": "将自动质押收益到现有的质押余额中", + "back": "返回", + "bond": "质押", + "bondAmount": "质押金额", + "bondedFunds": "己质押金额", + "cancel": "取消", + "change": "更改", + "controllerAccount": "Controller 账户", + "controllerAccountsDeprecated": "Controller帐户将逐渐被淘汰", + "controllerNotImported": "尚未导入Controller帐户。如果无法访问该帐户,请立即设置新帐户。否则,请将该账户导入扩展之一", + "earningRewards": "挣取收益中", + "inactiveNominations": "非活跃提名人", + "manage": "管理", + "minimumToEarnRewards": "最低收益抵押额", + "minimumToNominate": "最低提名质押额", + "noNominationsSet": "非活跃:未设置提名", + "nominate": "提名", + "nominating": "提名中", + "nominatingAnd": "提名并", + "nominations": "提名", + "none": "无", + "notAssigned": "未分配", + "notEarningRewards": "非挣取收益状态", + "notNominating": "未提名", + "payoutDestination": "收益到账地址", + "payoutDestinationSubtitle": "选择如何接收收益。收益可以是复合式增长,也可以作为自由余额发回账户", + "pendingPayouts": "待付", + "poolDestroy": "提名池正在被摧毁,不能做提名操作", + "poolNominations": "池的提名", + "proxyprompt": "因本应用将很快取消对Controller帐户, 转而支持代理帐户. 请您尽快将Controller帐户切换到Stash帐户, 以便有更好体验", + "readOnly": "只读帐户无法签署交易", + "setNewController": "设置新Controller帐户", + "startNominating": "开始提名", + "status": "提名人状态", + "stop": "停止", + "summary": "总结", + "syncing": "正在同步", + "totalSupplyStaked": "所有抵押比例", + "unlocked": "已解锁", + "unstake": "解除抵押", + "unstakePromptInProgress_fast": "快速解除抵押正在进行中", + "unstakePromptInProgress_regular": "解除抵押正在进行中", + "unstakePromptInQueue": "您正在快速解除抵押队列中。当注册快速解除抵押时,您将无法执行任何提名人任务", + "unstakePromptReadyToWithdraw": "您的质押资金现已解锁并可提取", + "unstakePromptRevert": "如果您需要取消解除抵押, 请重新质押您的{{unit}}并再次开始提名", + "unstakePromptWaitingForUnlocks": "等待己解锁变为可提取", + "update": "更改", + "updateToStash": "将Controller帐户更新为Stash帐户", + "validator": "{{count}} 提名人", + "waitingForActiveNominations": "等待有效提名" + }, + "overview": { + "activeEra": "活跃 Era", + "activeNominators": "活跃提名人", + "activePools": "活跃提名池", + "addressCopied": "地址已复制到剪贴板", + "adjustedRewardsRate": "调整后收益率", + "afterInflation": "加入通货膨胀后", + "available": "可用", + "balance": "余额", + "bondedInPools": "提名池中当前质押的{{networkUnit}}总数", + "connect": "连接", + "free": "可用余额", + "historicalRewardsRate": "历史奖励率", + "inPool": "提名池中", + "inPools": "当前质押在提名池中", + "inflationRate": "通货膨胀率", + "locked": "己锁", + "manage": "管理", + "memberOf": "提名池", + "moreResources": "更多资讯", + "networkCurrentlyStaked": "{{total}} {{unit}} 目前正在{{network}}进行抵押", + "networkCurrentlyStakedSubtitle": "当前共有{{unit}} 在所有验证人和提名人之间做抵押", + "networkStats": "网络信息", + "noActiveAccount": "无活跃帐户", + "nominating": "提名中", + "notStaking": "无抵押", + "overview": "概述", + "pool": "提名池", + "poolMembersBonding": "名成员正在提名池活跃质押中", + "proxy": "代理账户", + "recentPayouts": "最近收益", + "reserve": "储备", + "reserveBalance": "预存金额", + "reserved": "己储备", + "start": "开始", + "subscanDisabled": "Subscan己断开", + "supplyStaked": "抵押比例", + "syncingStatus": "正在同步状态", + "timeRemainingThisEra": "本Era剩余时间", + "totalNominators": "提名人总数", + "totalNumAccounts": "已加入提名池的帐户总数", + "totalValidators": "所有验证人", + "unitSupplyStaked": "{{unit}} 抵押比例", + "unlocking": "正在解锁", + "updateReserve": "更新预存" + }, + "payouts": { + "deductedFromBond": "从质押里扣除", + "fromPool": "从提名池", + "lastEraPayout": "上Era收益", + "none": "无", + "notStaking": "无抵押", + "payout": "收益", + "payoutHistory": "收益记录", + "poolClaim": "提名池收益", + "recentPayouts": "最近收益", + "slashed": "除名", + "subscanDisabled": "Subscan己断开" + }, + "pools": { + "activePools": "活跃提名池", + "address": "地址", + "addressCopied": "地址已复制到剪贴板", + "addressInvalid": "地址无效", + "allPools": "所有提名池", + "allRoles": "所有角色", + "assigned": "己分配", + "assignedToAnyAccount": " 您的<b>主理人</b>、<b>提名人</b>和<b>守护人</b>角色可以分配给任何帐户。", + "availableToClaim": "成员可申领的奖励{{unit}}金额", + "back": "返回", + "beenClaimed": "已被申领。", + "beenClaimedBy": "成员已申领奖励{{unit}}总数", + "bond": "质押", + "bondAmount": "质押金额", + "bondedFunds": "己质押金额", + "bouncer": "守护人", + "cancel": "取消", + "closePool": "可提取己解锁金额并关闭池", + "compound": "复利", + "create": "创建", + "createAPool": "创建提名池", + "createPool": "创建提名池", + "depositor": "存款人", + "destroyPool": "销毁提名池", + "destroying": "销毁中", + "earningRewards": "挣取收益中", + "edit": "编辑", + "favorites": "收藏夹", + "fetchingFavoritePools": "正在获取收藏表中的提名池", + "fetchingMemberList": "正在获取会员列表", + "generateNominations": "生成提名", + "inPool": "提名池中", + "inactivePoolNotNominating": "非活跃:提名池未提名任何验证人", + "join": "加入", + "leave": "离开", + "leavingPool": "离开提名池中", + "leftThePool": "所有成员已离开", + "locked": "己锁", + "manage": "管理", + "managementOptions": "选择管理选项", + "memberOfPool": "提名池成员", + "members": "成员", + "minimumToCreatePool": "最低建提名池质押金", + "minimumToJoinPool": "最低入提名池质押金", + "noFavorites": "无收藏夹", + "nominate": "提名", + "nominating": "提名中", + "nominatingAnd": "提名中和", + "nominator": "提名人", + "notEarningRewards": "非挣取收益状态", + "notInPool": "不在提名池中", + "notSet": "未设置", + "open": "打开", + "outstandingReward": "未申领奖励", + "overview": "概述", + "ownerOfPool": "提名池所有者", + "permissionToUnbond": "您有权解除和取出提名池里任何成员的资金。使用成员菜单", + "poolCommission": "提名池佣金", + "poolCreator": " 作为提名池创建者,您可使用提名池的<b>存款人</b>角色", + "poolCurrentlyLocked": "该提名池当前正处于锁定状态", + "poolInDestroyingState": "该提名池正处于销毁状态", + "poolMembers": "成员", + "poolMembership": "提名池成员名单", + "poolName": "提名池名称", + "poolNameSupport": "提名池名称支持字符、符号和表情-发挥你的创意吧!", + "poolState": "提名池状态", + "poolStats": "提名池信息", + "poolStatus": "提名池状态", + "pools": "提名池", + "readOnly": "只读帐户无法签署交易", + "reformatted": "地址已重新格式化", + "roles": "角色", + "root": "主理人", + "save": "保存", + "stopNominating": "如需继续销毁池,请先停止提名", + "summary": "总结", + "totalBonded": "总绑定金额", + "unbond": "解除质押", + "unbondFunds": "解除质押资金", + "unbondYourFunds": "可解锁资金质押", + "unclaimedRewards": "待申领奖励", + "unlocked": "已解锁", + "validator": "{{count}} 提名人", + "waitingForActiveNominations": "等待有效提名", + "withdraw": "提取", + "withdrawFunds": "取款", + "withdrawUnlock": "请取出己解锁金额以继续关闭池" + }, + "validators": { + "activeValidators": "活跃验证人", + "allValidators": "所有验证人", + "averageCommission": "平均佣金", + "connecting": "连接中", + "favoriteValidators": "收藏夹中的验证人", + "favorites": "收藏夹", + "fetchingFavoriteValidators": "正在获取收藏夹的验证人", + "fetchingValidators": "正在获取验证人信息", + "networkValidators": "验证人列表", + "noFavorites": "无收藏夹", + "totalValidators": "所有验证人", + "validators": "验证人" + } + } +} diff --git a/src/locale/cn/tips.json b/src/locale/cn/tips.json new file mode 100644 index 0000000000..681e8f3bc5 --- /dev/null +++ b/src/locale/cn/tips.json @@ -0,0 +1,103 @@ +{ + "tips": { + "connectExtensions": [ + "连接扩展", + "连接您的帐户,开始使用Polkadot Staking Dashboard.", + [ + "连接您的帐户,开始使用Polkadot Staking Dashboard.", + "通过充当钱包的网络扩展访问账户。您的钱包用于签署在应用中提交的交易.", + "从应用中右上角的“连接”按钮连接您的钱包,然后选择您希望用于抵押的帐户继续.", + "该应用支持一系列扩展和钱包." + ] + ], + "howToStake": [ + "您会如何抵押?", + "要么成为提名人,要么成为提名池成员.", + [ + "有多种方式抵押 {NETWORK_NAME}.", + "要么成为提名人,要么成为提名池成员.", + "直接提名要求您质押{NETWORK_UNIT}并选择要提名的验证人,并提名最多{MAX_NOMINATIONS} 个验证人。要作为提名人获得奖励,你目前至少需要有{MIN_ACTIVE_STAKE} {NETWORK_UNIT}.", + "加入提名池要便宜得多,需要最低存款 {MIN_POOL_JOIN_BOND} {NETWORK_UNIT}. 提名验证人是代表您完成的,您只需从提名池中领取奖励.", + "创建提名池至少需要{MIN_POOL_CREATE_BOND} {NETWORK_UNIT}." + ] + ], + "joinAnotherPool": [ + "加入另一个提名池", + "切换到其它帐户以加入其它提名池.", + [ + "{NETWORK_NAME}上的每个帐户只能加入一个提名池. 若要加入更多提名池,请首先切换到另一个帐户." + ] + ], + "keepPoolNominating": [ + "管理您的提名池", + "保持您的提名池活跃地提名很重要.", + [ + "保持您的提名池活跃地提名很重要.", + "如果您的提名池没有赚取奖励,会员将离开并加入另一个提名池.", + "您的主理人和提名人角色对于管理您的提名非常重要。如果提名表现不佳,请选择不同的提名人,以增加您的提名池获得奖励的机会." + ] + ], + "managingNominations": [ + "管理提名", + "务必定期检查提名人的表现.", + [ + "务必定期检查提名人的表现.", + "为了最大化验证人的多样性和分散混合,请从一系列实体中选择验证人." + ] + ], + "module": { + "cancel": "取消", + "close": "关闭", + "disableTips": "取消提示", + "dismissResult": "在该页面中取消提示窗口", + "dismissTips": "解除提示", + "more": "更多", + "of": "/", + "oneMoment": "请稍等", + "reEnable": "提示可以通过侧菜单左下角的齿轮图标访问从设置中重新启用", + "syncingWith": "同步 {{network}}", + "tips": "提示" + }, + "monitoringPool": [ + "管理提名池成员", + "最好定期检查您的提名池是否正在活跃地赚取奖励.", + [ + "提名池将代表您管理提名,但定期检查您的提名池是否跃地赚取奖励是最好的.", + "监控您的提名池状态,以确保其正常运行,如果您没有收到任何奖励,请考虑加入另一个提名池." + ] + ], + "recommendedJoinPool": [ + "推荐: 加入池", + "您的帐户最适合加入提名池.", + [ + "根据您的帐户当前持有的{NETWORK_UNIT}金额,加入提名池是您开始抵押的最佳方式.", + "加入提名池需要最低存款{MIN_POOL_JOIN_BOND} {NETWORK_UNIT}." + ] + ], + "recommendedNominator": [ + "推荐:成为提名人", + "您有足够的{NETWORK_UNIT}成为提名人.", + [ + "您有足够的{NETWORK_UNIT}成为提名人并开始赚取奖励.", + "然而,直接提名确实需要您主动检查提名的验证人." + ] + ], + "reviewingPayouts": [ + "查看收益", + "定期审查您的奖金是衡量你的提名表现如何的好方法.", + [ + "定期审查您的奖金是衡量您的提名表现如何的好方法.", + "转到“收益”页面,获取您收到的每一笔付款的详细明细,以及付款来自哪个验证人(或提名池)." + ] + ], + "understandingValidatorPerformance": [ + "测量验证人性能", + "各种因素会影响验证人的奖励金额.", + [ + "各种因素都会影响验证人获得多少奖励,比如它产生的Era点数、有多少提名者支持它,以及它是否被超额认购.", + "所有这些指标都会随着时间而变化,有时会以不可预测的方式发生变化。因此,提名人积极监控验证人及其表现非常重要.", + "该应用提供了一系列指标,帮助您了解验证人的性能." + ] + ] + } +} diff --git a/src/locale/en/base.json b/src/locale/en/base.json new file mode 100644 index 0000000000..60d343dae8 --- /dev/null +++ b/src/locale/en/base.json @@ -0,0 +1,62 @@ +{ + "base": { + "active": "Active", + "allowAll": "Allow All", + "allowAnyoneCompound": "Allow anyone to compound rewards on your behalf.", + "allowAnyoneCompoundWithdraw": "Allow anyone to compound or withdraw rewards on your behalf.", + "allowAnyoneWithdraw": "Allow anyone to withdraw rewards on your behalf.", + "allowCompound": "Allow Compound", + "allowWithdraw": "Allow Withdraw", + "community": "Community", + "feedback": "Feedback", + "goTo": "Go To", + "help": "Help", + "inactive": "Inactive", + "network": "Network", + "nominate": "Nominate", + "overview": "Overview", + "payee": { + "account": { + "subtitle": "Send payouts to another account as free balance.", + "title": "To Another Account" + }, + "none": { + "subtitle": "Have no payout destination set.", + "title_active": "Not Assigned", + "title_default": "None" + }, + "staked": { + "subtitle": "Add payouts to your existing staked balance automatically.", + "title_active": "Compounding", + "title_default": "Compound" + }, + "stash": { + "subtitle": "Payouts are sent to your account as free balance.", + "title": "To Your Account" + } + }, + "payouts": "Payouts", + "pools": "Pools", + "resources": "Resources", + "stake": "Stake", + "support": "Support", + "time": { + "day_one": "day", + "day_other": "days", + "hour_one": "hour", + "hour_other": "hours", + "hr_one": "hr", + "hr_other": "hrs", + "min_one": "min", + "min_other": "mins", + "minute_one": "minute", + "minute_other": "minutes", + "second_one": "second", + "second_other": "seconds" + }, + "title_kusama": "Kusama Staking Dashboard", + "title_polkadot": "Polkadot Staking Dashboard", + "title_westend": "Westend Staking Dashboard", + "validators": "Validators" + } +} diff --git a/src/locale/en/help.json b/src/locale/en/help.json new file mode 100644 index 0000000000..6e510fe69f --- /dev/null +++ b/src/locale/en/help.json @@ -0,0 +1,411 @@ +{ + "help": { + "definitions": { + "activeNominators": [ + "Active Nominators", + [ + "Nominators who are active in the current session.", + "Being an active nominator does not guarantee rewards, as your nominees may be oversubscribed." + ] + ], + "activePools": [ + "Active Pools", + ["The current amount of active nomination pools on {NETWORK_NAME}."] + ], + "activeStakeThreshold": [ + "Active Stake Threshold", + [ + "The amount of {NETWORK_UNIT} needed to be actively nominating in an era.", + "This value applies to nominators and for pools. In the pool's case, it is important to join a pool with a total bond amount of at least this value.", + "Being above this metric simply guarantees that you will be present in the active nominator set for the era. This amount still does not guarantee rewards, as your active nominations may still be over-subscribed.", + "Only the top {MAX_NOMINATOR_REWARDED_PER_VALIDATOR} nominators are rewarded per validator in {NETWORK_NAME}. Ensuring your active bond is above this threshold will increase your chances of rewards.", + "You can keep track of these metrics from the dashboard and amend your staking position if necessary, whether increasing your bonded {NETWORK_UNIT}, changing your nominations, or joining another pool." + ] + ], + "activeValidator": [ + "Active Validator", + [ + "A validator that is actively validating blocks. Rewards are accumulated based on the validator's activity.", + "A new set of validators are chosen for each era, so there is no guarantee the same validator will be active in subsequent eras.", + "{NETWORK_NAME} allows a nominator to nominate up to 16 validators, maximising your chances of nominating an active validator in each era." + ] + ], + "adjustedRewardsRate": [ + "Adjusted Rewards Rate", + [ + "An estimated realized annual yield based on the {NETWORK_NAME} reward distribution model.", + "This figure is effectively the historical rewards rate minus the inflation rate." + ] + ], + "averageCommission": [ + "Average Commission", + [ + "The average validator commission rate on {NETWORK_NAME}.", + "This metric excludes validators who host a 100% commission, as these nodes usually block nominations and are run for the purposes of staking on central exchange platforms." + ] + ], + "blockedNominations": [ + "Blocked Nominations", + [ + "When a validator has blocked nominations, nominators are unable to nominate them." + ] + ], + "bondedInPool": [ + "Bonded in Pool", + [ + "The amount of {NETWORK_UNIT} currently bonded in a pool.", + "Unlike nominating directly, where your bonded funds remain in your account but become locked, the {NETWORK_UNIT} you bond to a pool is transferred to the pool's stash account. Nonetheless, pool members still have access to unbond their funds at any time." + ] + ], + "bonding": [ + "Bonding", + [ + "Bonding funds is the process of 'locking' (or staking) {NETWORK_UNIT}. Bonded {NETWORK_UNIT} will then be automatically allocated to one or more of your nominated validators.", + "As a nominator, you allocate nominations yourself. In a pool, the pool owner or pool nominator will allocate nominations on your behalf, and your bonded funds will back those nominations.", + "The minimum to earn rewards statistic is the minimum {NETWORK_UNIT} being bonded by a nominator for the current era. This value is also the minimum amount required to receive rewards." + ] + ], + "commission": [ + "Commission", + [ + "Validators can take a percentage of the rewards they earn. This chunk is called their commission.", + "Nominating validators with low commissions mean you will receive a larger share of the rewards they generate.", + "Many validators will have a commission rate of 100%, meaning you will receive no rewards by nominating these validators.", + "Examples of such validators include those operating on behalf of exchanges, where nominating and reward distribution is done centrally on the exchange in question.", + "A validator can update their commission rates as and when they please, and such changes will have an impact on your profitability. Be sure to monitor your nominations on this dashboard to keep updated on their commission rates." + ] + ], + "epoch": [ + "Epoch", + [ + "An epoch is another name for a session in {NETWORK_NAME}. Each era is divided into 6 epochs during which validators are assigned as block producers to specific time frames or slots.", + "1 epoch is currently 4 hours in Polkadot." + ] + ], + "era": [ + "Era", + [ + "At the end of each era, validators are rewarded {NETWORK_UNIT} based on how many era points they accumulated in that era. This {NETWORK_UNIT} reward is then distributed amongst the nominators of the validator via a payout.", + "1 era is currently 24 hours in Polkadot." + ] + ], + "eraPoints": [ + "Era Points", + [ + "Era Points are accumulated by validators during each era, and depend on a validator's performance.", + "As a staker, you do not need to worry about Era Points. In general, better performing validators produce more Era Points, which in-turn lead to higher staking rewards." + ] + ], + "historicalRewardsRate": [ + "Historical Rewards Rate", + [ + "An estimated annual yield based on the {NETWORK_NAME} reward distribution model." + ] + ], + "idealStaked": [ + "Ideal Staked", + ["The percentage of staked total supply in ideal network conditions."] + ], + "inactiveNominations": [ + "Inactive Nominations", + [ + "Nominations that are in the active validator set for the current era, but bonded funds have not been assigned to these nominations." + ] + ], + "inflation": [ + "Inflation", + [ + "{NETWORK_UNIT} is inflationary; there is no maximum number of {NETWORK_UNIT}.", + "Inflation is designed to be approximately 10% annually, with validator rewards being a function of the amount staked and the remainder going to treasury." + ] + ], + "lastEraPayout": [ + "Last Era Payout", + [ + "The total amount of {NETWORK_UNIT} paid out for the last active era.", + "Payouts are distributed evenly amongst the active validators for that era, and are then further distributed to the active nominators that took part in that era.", + "The payout amounts received depend on how much {NETWORK_UNIT} the nominators, and validators themselves, had bonded for that era." + ] + ], + "ledgerHardwareWallets": [ + "Ledger Hardware Wallets", + [ + "Compatible Ledger devices are fully supported on staking dashboard. Use your Ledger to manage your staking activity on {NETWORK_NAME}.", + "Using a hardware wallet ensures a secure experience, whereby your keys stay on the hardware device at all times. This is also known as cold storage, as there is no active internet connection to your wallet.", + "In order to import and sign transactions with your Ledger device, your device must have the {NETWORK_NAME} Ledger app installed. The {NETWORK_NAME} app can be installed via Ledger's very own Ledger Live application.", + "Both USB and Bluetooth connections are supported in staking dashboard. So if your Ledger is bluetooth enabled, and your device allows bluetooth connections, you will be able to import accounts and sign transactions wirelessly through your Ledger device.", + "Find more information about Ledger at their official website, ledger.com." + ] + ], + "ledgerRejectedTransaction": [ + "Ledger Rejected Transaction", + [ + "The transaction was rejected on the Ledger device and will not be submitted.", + "Submit a transaction by approving it on the Ledger device." + ] + ], + "ledgerRequestTimeout": [ + "Ledger Request Timeout", + [ + "Timeout errors can occur when the Ledger device is busy, or when an operation fails to complete in time.", + "Try performing the operation again, and if the error persists, try restarting the Ledger device." + ] + ], + "lockedBalance": [ + "Locked Balance", + [ + "In {NETWORK_NAME} some actions require your balance to be locked, such as staking or voting on governance. If some of your balance is locked, it cannot be transferred.", + "The amount of locked balance displayed on staking dashboard represents all your locks apart from the staking lock - hence displaying all the locked balance at your disposal that you can use to nominate.", + "You are able to nominate with any remaining locked balance, but you are not able to join a pool with it, as doing so requires a transfer to the pool account." + ] + ], + "minimumToCreatePool": [ + "Minimum To Create Pool", + [ + "The minimum amount of {NETWORK_UNIT} needed to bond in order to create a pool.", + "Creating a pool requires a larger deposit than that of joining a pool." + ] + ], + "minimumToJoinPool": [ + "Minimum To Join Pool", + [ + "The minimum amount of {NETWORK_UNIT} needed to bond in order to join a pool.", + "This amount is different from the bond needed to create a pool." + ] + ], + "nominating": [ + "Nominating", + [ + "Nominating is the process of selecting validators you wish to stake your {NETWORK_UNIT} to. You can choose to nominate up to 16 validators for each of your accounts.", + "Once you have nominated your selected validators, they become your nominations." + ] + ], + "nominationPools": [ + "Nomination Pools", + [ + "Nomination pools allow users to contribute {NETWORK_UNIT} and earn staking rewards.", + "Unlike nominating, staking using pools requires a small amount of {NETWORK_UNIT}, and the pool manages nominees on your behalf." + ] + ], + "nominationStatus": [ + "Nomination Status", + [ + "The status of your nominations at a glance.", + "A set of nominations will be inactive when none of those nominees are participating in the current validator set (the set of validators currently elected to validate the network).", + "When at least one of your nominees are active, this nomination status will display as actively nominating - but this still does not guarantee rewards.", + "The top {MAX_NOMINATOR_REWARDED_PER_VALIDATOR} nominators of each active validator receive rewards on {NETWORK_NAME}. So if a nominee is active and over-subscribed, you must be a part of the {MAX_NOMINATOR_REWARDED_PER_VALIDATOR} highest bonded nominators to receive rewards.", + "If an active nominee is not over-subscribed, you will receive rewards." + ] + ], + "nominations": [ + "Nominations", + [ + "Nominations are the validators a staker chooses to nominate. You can nominate up to {MAX_NOMINATIONS} validators on {NETWORK_NAME}.", + "For nomination pools, the pool owner and pool nominator are in charge of nominating validators on behalf of all the pool members.", + "Once nominations have been submitted, bonded funds are automatically distributed to nominees that are active in the current era.", + "As long as at least one of your nominations is actively validating in a session, your funds will be backing that validator." + ] + ], + "nominatorStake": [ + "Nominator Stake", + [ + "The amount of {NETWORK_UNIT} backing the validator from its nominators.", + "This value is added to the validator's self stake to form the total stake of the validator. Note that this value changes every era as the bonded funds of nominators are re-distributed to the active validators of that session." + ] + ], + "openAppOnLedger": [ + "Open App On Ledger", + [ + "This message occurs when the {NETWORK_NAME} Ledger app is not currently open on the connected device.", + "Open the {NETWORK_NAME} app on the Ledger device and try again." + ] + ], + "overSubscribed": [ + "Over Subscribed", + [ + "Only the top {MAX_NOMINATOR_REWARDED_PER_VALIDATOR} nominators for each validator are rewarded in {NETWORK_NAME}. When this number is surpassed, this validator is considered over subscribed." + ] + ], + "payout": [ + "Payout", + [ + "Payouts are staking rewards on {NETWORK_NAME}. They depend on how many 'Era Points' your nominated validators accrue over time. Rewards are determined at the end of every Era (24 hour periods).", + "To receive staking rewards, a Payout needs to be requested. Any nominator backing the validator in question can request a Payout.", + "One payout request triggers the reward payout for every nominator." + ] + ], + "payoutDestination": [ + "Payout Destination", + [ + "Your payout destination determines to which account your payouts are sent to.", + "Payouts can be automatically bonded on top of your current bond, or they can be sent to your stash, controller, or an external account of your choosing." + ] + ], + "payoutHistory": [ + "Payout History", + [ + "Historical records of payouts made for being an active nominator.", + "Requesting payouts is a manual process, so you may receive payouts for multiple eras in quick succession or in a sporadic fashion. Your payout graphs may therefore have multiple payouts occur on the same day, or have days where there were no payouts.", + "This does not mean that you were not nominating or generating rewards in that period - only that the payout for that period was not yet made." + ] + ], + "polkadotVault": [ + "Polkadot Vault", + [ + "Polkadot Vault (formerly Parity Signer) is a cold storage solution that allows you to use a phone in airplane mode as an air-gapped wallet.", + "The Vault app is not technically a wallet, as it does not allow to transfer funds.", + "It is more of a key-chain tool that will enable you the create, manage, and restore accounts." + ] + ], + "poolCommissionChangeRate": [ + "Pool Commission Change Rate", + [ + "The commission change rate is set by the pool Root, and dictates by how much and how often commission can be increased.", + "The maximum increase of the change rate dictates how much the commission rate can be increaesd in a single update. The minimum delay dictates how many blocks must pass before the commission rate can be increased again.", + "Once an initial change rate is set, only more restrictive values can be set thereafter. More restrictive values comprise of a smaller max increase and larger minimum delay." + ] + ], + "poolCommissionRate": [ + "Pool Commission Rate", + [ + "The commission rate of the pool. This value is set by the pool Root, and is the percentage of rewards the pool will take from its members.", + "A payee account must also be provided to receive the commission.", + "The commission rate is taken from the pool's rewards before they are distributed to its members." + ] + ], + "poolMaxCommission": [ + "Pool Max Commission", + [ + "The maximum commission rate a pool owner can set for the pool.", + "This value is set by the pool Root, and is the maximum percentage of rewards the pool will take from its members.", + "Once an initial maximum commission is set, only more restrictive values can be set thereafter." + ] + ], + "poolMembership": [ + "Pool Membership", + [ + "Your pool membership status reflects whether you are a member of a pool.", + "Pool memberships can either be that of a pool member or a pool owner.", + "Currently on {NETWORK_NAME}, accounts can only join one pool at a time. If you wish to join another pool, you must leave your current pool first.", + "To leave a pool, you simply need to unbond and withdraw all your bonded {NETWORK_UNIT}. Staking dashboard provides a dedicated Leave button to unbond from a pool." + ] + ], + "poolRewards": [ + "Pool Rewards", + [ + "The amount of {NETWORK_UNIT} generated by being an active participant in a pool.", + "Pool members are required to claim their rewards in order to have them transferred to their balance.", + "Users have 2 choices for claiming rewards. They can be bonded back into the pool, that will increase your share of the pool and accumulate further rewards. Rewards can also be withdrawn from the pool to your account as free {NETWORK_UNIT}." + ] + ], + "poolRoles": [ + "Pool Roles", + [ + "A pool consists of 4 roles, each of which having different responsibilities in managing the running of the pool.", + "Root: Can change the nominator, bouncer, or itself. Further, it can perform any of the actions the nominator or bouncer can.", + "Depositor: Creates the pool and is the initial member. The depositor can only leave the pool once all other members have left. Once they leave by withdrawing, the pool is fully removed from the system.", + "Nominator: Can select the validators the pool nominates.", + "Bouncer: Can change the pool's state and kick (permissionlessly unbond/withdraw) members if the pool is blocked." + ] + ], + "proxyAccounts": [ + "Proxy Accounts", + [ + "Proxy accounts are accounts that act on behalf of another account.", + "In proxy account terms, the proxy account is called the delegate account, and the proxied account it is acting on behalf of is called the delegator account.", + "Staking dashboard allows delegates to sign transactions on behalf of a delegator. If a delegate exists in the dashboard without its delegator, the delegator is imported as a read only account automatically." + ] + ], + "readOnlyAccounts": [ + "Read Only Accounts", + [ + "Read Only accounts are accounts that can be imported, but are not able to sign transactions. This means that you can view the account's balance and staking information, but you cannot perform any staking actions with it." + ] + ], + "reserveBalance": [ + "Reserve Balance", + [ + "In {NETWORK_NAME}, you must have a balance above a certain amount for your account to exist on-chain. This amount is called your 'existential deposit'.", + "Staking dashboard ensures that this amount of {NETWORK_UNIT} is never touched." + ] + ], + "reserveBalanceForExistentialDeposit": [ + "Reserve Balance for Existential Deposit", + [ + "If your account already has locked balance that covers your account's existential deposit, such as balance locked for nominating, then staking dashboard will not lock any additional free balance.", + "In this scenario, \"None\" is displayed under the \"Reserve For Existential Deposit\" section in your Reserve Balance settings." + ] + ], + "rewardsByCountryAndNetwork": [ + "Rewards By Country And Network", + [ + "Shows the estimated percentage of rewards that are generated from different countries and IP networks.", + "Polkawatch decentralization analytics are computed by aggregating rewards by the IP Geolocation of the Validator. The sample period is the last 60 days." + ] + ], + "selfStake": [ + "Self Stake", + [ + "The amount of {NETWORK_UNIT} the validator has bonded themselves.", + "This value is added to the amount of {NETWORK_UNIT} bonded by nominators to form the total stake of the validator." + ] + ], + "supplyStaked": [ + "Supply Staked", + [ + "The current cumulative supply of {NETWORK_UNIT} being staked globally.", + "The percentage of staked {NETWORK_UNIT} is relative to the total supply of {NETWORK_UNIT}." + ] + ], + "totalNominators": [ + "Total Nominators", + [ + "Accounts who are staking in the network, regardless of whether they are active or inactive in the current session.", + "In order to stake {NETWORK_UNIT}, you can either become a nominator or join a pool - that act as nominators themselves." + ] + ], + "validator": [ + "Validator", + [ + "An entity that validates blocks for the {NETWORK_NAME} Relay Chain. Validators play a key role in {NETWORK_NAME} to secure the network and produce blocks.", + "As a nominator, you choose which validators you wish to back, and receive rewards for doing so." + ] + ], + "wrongTransaction": [ + "Wrong Transaction", + [ + "This error occurs when a Ledger device signed or is in the process of signing a different transaction than the currently active one on the dashboard.", + "Ensure there are no outstanding transactions on the Ledger device and try again." + ] + ], + "yourBalance": [ + "Your Balance", + [ + "Your balance represents the total {NETWORK_UNIT} you have available in addition to your total staked amount, that includes the amount you have bonded in a pool.", + "Unlike your staked balance, your bonded pool balance is held and locked in the pool itself." + ] + ] + }, + "externals": { + "bondMore": "Bond More Tokens to Your Existing Stake", + "changeAccount": "Changing your Controller Account", + "changeDestination": "Changing Your Payout Destination", + "changeNominations": "Changing Your Nominations", + "chooseValidators": "How do I Know Which Validators to Choose?", + "claimRewards": "Claiming Nomination Pool Rewards", + "connectAccounts": "How to Connect Your Accounts", + "createPools": "Creating Nomination Pools", + "howToUse": "How to Use the Staking Dashboard: Overview", + "rebonding": "Rebonding", + "stakeDot": "Staking your DOT", + "unbondingTokens": "Unbonding Your Tokens" + }, + "modal": { + "articles": "Articles", + "close": "Close", + "definitions": "Definitions", + "helpResources": "Help Resources", + "related": "Related" + } + } +} diff --git a/src/locale/en/library.json b/src/locale/en/library.json new file mode 100644 index 0000000000..a2d856f2b2 --- /dev/null +++ b/src/locale/en/library.json @@ -0,0 +1,193 @@ +{ + "library": { + "100Commission": "100% commission", + "accountConnected": "Account Connected", + "accounts": "Accounts", + "active": "Active", + "activeLowCommission": "Active Low Commission", + "activeLowCommissionSubtitle": "Selects high performing validators with low commission.", + "activePools": "Active Pools", + "activeValidator": "Active Validator", + "activeValidators": "Active Validators", + "add": "Add", + "addFromFavorites": "Add From Favorites", + "address": "Address", + "addressCopiedToClipboard": "Address Copied to Clipboard", + "all": "All", + "alreadyImported": "Address Already Imported", + "asAPoolMember": "as a pool member.", + "asThePoolDepositor": "as the pool depositor.", + "atLeast": "Bond amount must be at least", + "available": "Available", + "backToMethods": "Back to Methods", + "backToScan": "Back to Scan", + "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", + "cancel": "Cancel", + "cancelled": "Cancelled", + "chooseValidators": "Choose up to {{maxNominations}} validators to nominate.", + "chooseValidators2": "Generate your nominations automatically or manually insert them.", + "clear": "Clear", + "clearSelection": "clear selection", + "clickToReload": "Click to reload", + "complete": "Complete", + "confirm": "Confirm", + "confirmReformat": "Address was reformatted. Please confirm.", + "connect": "Connect", + "connectedTo": "Connected To", + "connectedToNetwork": "Connected to Network", + "connecting": "Connecting", + "continue": "Continue", + "copyAddress": "Copy Address", + "copyPoolAddress": "Copy Pool Address", + "createPool": "Create Pool", + "dayAverage": "Day Average", + "dayPerformance": "Day Performance", + "dayPerformanceStanding": "{{count}} Day Active Validators Performance Standing", + "dayPoolPerformance": "Day Pool Performance", + "destroying": "Destroying", + "destroyingPools": "Destroying Pools", + "disclaimer": "Disclaimer", + "disconnected": "Disconnected", + "displayingValidators_one": "Displaying {{count}} Validator", + "displayingValidators_other": "Displaying {{count}} Validators", + "done": "Done", + "enablePermissionlessClaiming": "Enable Permissionless Claiming", + "eraPoints": "Era Points", + "errorUnknown": "Oops, Something Went Wrong", + "errorWithTransaction": "Error with transaction", + "estimatedFee": "Estimated Fee", + "exclude": "Exclude", + "failed": "Failed", + "fastUnstake": "Fast Unstake", + "fastUnstakeCheckingEras": "Checking {{checked}} of {{total}} Eras", + "fastUnstakeExposed_one": "Exposed {{count}} Era Ago", + "fastUnstakeExposed_other": "Exposed {{count}} Eras Ago", + "favorite": "Favorite", + "favoritePoolAdded": "Favorite Pool Added", + "favoritePoolRemoved": "Favorite Pool Removed", + "favoriteValidatorAdded": "Favorite Validator Added", + "favoriteValidatorRemoved": "Favorite Validator Removed", + "filter": "Filter", + "filterValidators": "Filter Validators", + "finalized": "Finalized", + "free": "Free", + "fromFavorites": "From Favorites", + "fromFavoritesSubtitle": "Gets a set of your favorite validators.", + "graphInactive": "Inactive", + "highCommission": "High Commission", + "highPerformanceValidator": "High Performance Validator", + "iHaveScanned": "I Have Scanned", + "import": "Import", + "importing": "Importing", + "inBlock": "In Block", + "inQueue": "In Queue", + "inactive": "Inactive", + "include": "Include", + "insertPayoutAddress": "Insert a payout address", + "invalid": "Address Invalid", + "join": "Join", + "legalDisclosures": "Legal Disclosures", + "listItemActive": "Active", + "locked": "Locked", + "lockedPools": "Locked Pools", + "lowCommission": "Low Commission", + "manual": "Manual", + "manualSelectionSubtitle": "Add validators from scratch.", + "manual_selection": "Manual Selection", + "max": "Max", + "minimumBond": "A minimum bond of {{minBondUnit}} {{unit}} is required", + "missingIdentity": "Missing Identity", + "moreThanBalance": "Bond amount is more than your free balance.", + "next": "Next", + "noFilters": "No filters", + "noFree": "You have no free {{unit}} to bond.", + "noMatch": "No pools match this criteria.", + "noPayoutAddress": "No Payout Adddress", + "noValidators": "No validators.", + "noValidatorsMatch": "No validators match this criteria.", + "nominate": "Nominate", + "nominateActive": "Active", + "nominateInactive": "Inactive", + "nominationsReverted": "Nominations Reverted", + "nominator": "Nominator", + "notEnough": "Not Enough", + "notEnoughAfter": "Not enough {{unit}} to bond after transaction fees.", + "notEnoughFunds": "The sender does have enough {{unit}} to submit this transaction.", + "notImported": "Not Imported", + "notMeet": "You do not meet the minimum bond of", + "notNominating": "Not Nominating", + "notStaking": "Not Staking", + "notValidAddress": "Not a valid address", + "optimalSelection": "Optimal Selection", + "optimalSelectionSubtitle": "Selects top performing and regularly active validators.", + "order": "Order", + "orderValidators": "Order Validators", + "overSubscribed": "Over Subscribed", + "overSubscribedMinReward": "Over subscribed: Minimum reward bond is", + "page": "Page {{page}} of {{total}}", + "payout": "Payout", + "payoutAccount": "Payout Account", + "payoutAddress": "Payout Adddress", + "pending": "Pending", + "permissionlessClaimingTurnedOff": "Permissionless claiming is turned off.", + "points": "Points", + "pool": "Pool", + "poolClaim": "Pool Claim", + "poolCommission": "Pool Commission", + "poolId": "Pool ID", + "poolMembers": "Pool Members", + "prev": "Prev", + "privacy": "Privacy", + "proxy": "Proxy", + "randomValidator": "Random Validator", + "reGenerate": "Re-Generate", + "remove": "Remove", + "removeSelected": "Remove Selected", + "reset": "Reset", + "revertedToActiveSelection": "Nominations have been reverted to your active selection.", + "scanPolkadotVault": "Scan on Polkadot Vault", + "search": "Search Pool ID, Name or Address", + "searchAddress": "Search Address or Identity", + "select": "Select", + "sign": "Sign", + "signPolkadotVault": "Sign From Polkadot Vault", + "signedByController": "Signed by Controller", + "signedByProxy": "Signed by Proxy", + "signer": "Signer", + "signing": "Signing", + "submitTransaction": "Ready to submit transaction.", + "syncing": "Syncing", + "syncingPoolList": "Syncing Pool list", + "tooSmall": "Bond amount is too small.", + "top": "Top", + "transactionCancelled": "Transaction was cancelled", + "transactionInBlock": "Transaction in block", + "transactionInitiated": "Transaction was initiated.", + "transactionSuccessful": "Transaction successful", + "unbond": "Unbond", + "unbondAmount": "Unbond amount is more than your bonded balance.", + "unbonding": "Unbonding", + "unclaimedPayouts": "Unclaimed Payouts", + "unlocking": "Unlocking", + "unordered": "Unordered", + "update": "Update", + "valid": "Valid Address", + "validAddress": "Valid address", + "validatingParachainBlocks": "Validating Parachain Blocks", + "validatorCommission": "Validator Commission", + "validatorPerformance": "{{count}} Day Validator Performance", + "valueTooSmall": "Value is too small", + "viewDecentralization": "View Decentralization", + "viewMetrics": "View Metrics", + "viewPoolNominations": "View Pool Nominations", + "waiting": "Waiting", + "walletNotFound": "wallet not found", + "whenActivelyNominating": "when actively nominating.", + "wrongTransaction": "Wrong transaction, please sign again." + } +} diff --git a/src/locale/en/modals.json b/src/locale/en/modals.json new file mode 100644 index 0000000000..74afe08f76 --- /dev/null +++ b/src/locale/en/modals.json @@ -0,0 +1,294 @@ +{ + "modals": { + "aboveExisting": "Above Existing", + "aboveGlobalMax": "Above Global Max", + "aboveMax": "Above Max", + "account": "Account", + "accountAlreadyImported": "Account Already Imported", + "accounts": "Accounts", + "activeRoles_one": "Active Roles in {{count}} Pool", + "activeRoles_other": "Active Roles in {{count}} Pools", + "activeRoles_zero": "Active Roles In No Pool", + "add": "Add", + "addToBond": "Add to Bond", + "addToNominations": "Add to Nominations", + "addUpToFavorites_one": "You can add {{count}} more validator", + "addUpToFavorites_other": "You can add up to {{count}} more favorite validators", + "addedToBond": "This amount of {{unit}} will be added to your current bonded funds.", + "addingFavorite_one": "Adding {{count}} Nomination", + "addingFavorite_other": "Adding {{count}} Nominations", + "addressReceived": "Address Received:", + "afterClaiming": "Funds will be immediately available as free balance after claiming.", + "allNominations": "All Nominations", + "allPoolRoles": "All Pool Roles", + "allowToJoin": "Allow new members to join the pool.", + "amountToBond": "Amount to bond:", + "approveTransactionLedger": "Approve transaction on Ledger", + "back": "Back", + "beingDestroyed": "This pool is being destroyed.", + "belowExisting": "Below Existing", + "beyondMaxIncrease": "Beyond Max Increase", + "binanceApi": "Binance Spot API", + "bond": "Bond", + "bondAll": "Bond All", + "bondAllAvailable": "Bond all available", + "bondExtra": "Bond Extra", + "bondMore": "Bond more", + "bondingWithdraw": "Bonding will also withdraw your outstanding rewards of", + "bouncer": "Bouncer", + "braveText": "<b>To Brave users!</b> Due to a recent update (<i>Brave version 1.36</i>), there may appear issues while using light clients (e.g. not connected).", + "cancel": "Cancel", + "changeControllerAccount": "Change Controller Account", + "changeNomination": "Once submitted, your nominations will be removed from your dashboard immediately, and will not be nominated from the start of the next era.", + "changePoolRoles": "Change Pool Roles", + "changeRate": "Change Rate", + "changeToDestroy": "Change pool to destroying state.", + "checking": "Checking...", + "chooseLanguage": "Choose Language", + "claim": "Claim", + "claimCommission": "Claim Commission", + "claimOutstandingCommission": " Claim any outstanding commission in the pool reward account.", + "claimPayouts": "Claim Payouts", + "claimReward1": "Once submitted, your rewards will be bonded back into the pool. You own these additional bonded funds and will be able to withdraw them at any time.", + "claimReward2": "Withdrawing rewards will immediately transfer them to your account as free balance.", + "claimsOnBehalf": "Claiming a payout claims on behalf of every nominator backing the validator for the era you are claiming for. For this reason transaction fees are usually higher, and most nominators rely on the validator to claim on their behalf.", + "commissionRate": "Commission Rate", + "compound": "Compound", + "confirmReset": "Confirm Reset", + "connect": "Connect", + "connectLedgerToContinue": "Connect your Ledger device to continue.", + "connected": "Connected", + "connectionType": "Connection Type", + "continue": "Continue", + "controllerImported": "You must have your controller account imported to sign this transaction.", + "dashboardTips": "Dashboard Tips", + "days": "Days", + "decentralizationAnalyticsNotAvailable": "Decentralization analytics not available", + "decentralizationAnalyticsNotSupported": "Decentralization analytics not Supported in this Network", + "declare": "Declare", + "declared": "Declared", + "destroyIrreversible": "Destroying a Pool is Irreversible", + "destroyPool": "Destroy Pool", + "destroyPoolResult": " Once you Destroy the pool, all members can be permissionlessly unbonded, and the pool can never be reopened.", + "developerTools": "Developer Tools", + "differentNetworkAddress": "Different Network Address", + "disconnect": "Disconnect", + "done": "Done", + "ensureLedgerIsConnected": "Tip: Ensure your Ledger device is connected before continuing.", + "exitYourStakingPosition": "Exit your staking position.", + "extensionConnected": "Extension Connected", + "extensions": "Extensions", + "fastUnstakeCurrentQueue": "Number of accounts currently in the fast unstake queue", + "fastUnstakeExposedAgo_one": "You were last exposed {{count}} Era Ago", + "fastUnstakeExposedAgo_other": "You were last exposed {{count}} Eras Ago", + "fastUnstakeNote1": "To register for fast unstake, you must not be actively staking for more than {{bondDuration}} eras.", + "fastUnstakeNote2_one": "If you are inactive for at least {{count}} more Era, you will be able to register for fast unstake.", + "fastUnstakeNote2_other": "If you are inactive for at least {{count}} more Eras, you will be able to register for fast unstake.", + "fastUnstakeOnceRegistered": "Once registerd you will be waiting in the fast unstake queue.", + "fastUnstakeRegistered": "Registered and Waiting to Unstake", + "fastUnstakeSubmit_cancel": "Cancel Fast Unstake", + "fastUnstakeSubmit_register": "Register", + "fastUnstakeUnorderedNote": "The fast unstake queue is unordered, so the exact timing of being selected is not known.", + "fastUnstakeWarningUnlocksActiveMore": "No unlocks can be active to register for fast unstake. Rebond or withdraw your unlocks to become fully bonded and try registering for fast unstake again.", + "fastUnstakeWarningUnlocksActive_one": "You have {{count}} unlock active.", + "fastUnstakeWarningUnlocksActive_other": "You have {{count}} unlocks active.", + "fastUnstake_register": "Register Fast Unstake", + "fastUnstake_title": "Fast Unstake", + "favoriteNotNominated": "Favorite Validators / Not Nominated", + "favoriteValidators": "Favorite Validators", + "favoritesAddedSubtitle_one": "{{count}} validator has been added to your nominees", + "favoritesAddedSubtitle_other": "{{count}} validators have been added to your nominees", + "favoritesAddedTitle_one": "Favorite Added", + "favoritesAddedTitle_other": "Favorites Added", + "feedback": "Feedback", + "feedbackPage": "We host a feedback page on", + "for": "For {{who}}", + "forget": "Forget", + "free": "Free", + "getAnotherAccount": "Get Another Account", + "gettingAccount": "Getting Account", + "gettingAddress": "Getting Address...", + "goToAccounts": "Go To Accounts", + "goToConnect": "Go To Connect", + "hardware": "Hardware", + "hide": "Hide", + "hours": "Hours", + "import": "Import", + "importAccount": "Import Account", + "importAddress": "Import Address", + "importAnotherAccount": "Import Another Account", + "importedAccount_one": "{{count}} Imported Account", + "importedAccount_other": "{{count}} Imported Accounts", + "inPool": "In Pool", + "inputAddress": "Input Address", + "inputDelegatorAddress": "Input Delegator Address", + "inputPayeeAccount": "Input Payee Account", + "invalidAddress": "Invalid Address", + "joinPool": "Join Pool", + "leavePool": "Leave Pool", + "ledgerAccount": "Ledger Account", + "ledgerAccounts_one": "Displaying {{count}} Ledger Account", + "ledgerAccounts_other": " Displaying {{count}} Ledger Accounts", + "ledgerRequestTimeout": "The Ledger request timed out. Please try again.", + "ledgerWillBeReset": "Your Ledger address list will be reset, and any imported accounts will be removed from the dashboard.", + "lightClient": "Light Client", + "lockPool": "Lock Pool", + "lockPoolSubtitle": "Once you lock the pool no one else can join the pool.", + "manageCommission": "Manage Commission", + "manageNominations": "Manage Nominations", + "managePool": "Manage Pool", + "maxCommission": "Max Commission", + "maximumCommissionUpdated": "Maximum Commission Updated", + "minDelayBetweenUpdates": "Min Delay Between Updates", + "minutes": "Minutes", + "missingNesting": "Call missing nesting support, cannot sign.", + "months": "Months", + "moreFavoritesSurpassLimit": "Adding more favorites will surpass {{max}} nominations.", + "networks": "Networks", + "newTotalBond": "New total bond:", + "newlyBondedFunds": "Newly bonded funds will back active nominations from the start of the next era.", + "noAccounts": "No Accounts", + "noActiveAccount": "No Active Account", + "noEnough": "You do not have enough free balance to register for fast unstake. Fast unstake requires a deposit of", + "noFavoritesAvailable": "No Favorites Available.", + "noFavoritesSelected": "No Favorites Selected", + "noNominationsSet": "You have no nominations set.", + "noNominatorRole": "You do not have a nominator role in any pools.", + "noProxyAccountsDeclared": "No proxy accounts have been declared.", + "noReadOnlyAdded": "No Read Only accounts have been added", + "noRewards": "You have no rewards to claim.", + "noVaultAccountsImported": "No Polkadot Vault accounts have been imported.", + "nominateFavorites": "Nominate Favorites", + "nominating": "Nominating", + "nominatingAndInPool": "Nominating and In Pool", + "nomination_one": "{{count}} Nomination", + "nomination_other": "{{count}} Nominations", + "nominator": "Nominator", + "nominatorStake": "Nominator Stake", + "none": "None", + "notAuthenticated": "Not Authenticated", + "notInstalled": "Not Installed", + "notMeetMinimum": "You do not meet the minimum nominator bond of {{minNominatorBondUnit}} {{unit}}. Please bond some funds before nominating.", + "notStaking": "Not Staking", + "notToClaim": "Validators usually claim payouts on behalf of their nominators. If you decide not to claim here, it is likely you will receive your payouts within 1-2 days of them becoming available.", + "notePoolDepositorMinBond_depositor": "As the pool depositor you must maintain a bond of at least {{bond}} {{unit}}.", + "notePoolDepositorMinBond_member": "As a pool member you must maintain a bond of at least {{bond}} {{unit}}.", + "onceUnbonding": "Once unbonding, your funds to become available after {{bondDurationFormatted}}.", + "openAppOnLedger": "Open the {{appName}} app on your Ledger device.", + "openFeedback": "Open Feedback on Canny.io", + "payeeAdded": "Payee Added", + "payoutDestination": "Payout Destination", + "pendingPayout_one": "{{count}} Pending Payout", + "pendingPayout_other": "{{count}} Pending Payouts", + "polkawatchDisabled": "Polkawatch Disabled", + "pool": "Pool", + "poolIsNotNominating": "Pool is Not Nominating.", + "poolName": "Pool Name", + "poolNominations": "Pool Nominations", + "provider": "Provider", + "proxies": "Proxies", + "proxy": "Proxy", + "proxyAccounts": "Proxy Accounts", + "queuedTransactionRejected": "Previous transaction rejected. Please try again.", + "readOnly": "Read Only", + "readOnlyAccounts": "Read Only Accounts", + "readOnlyCannotSign": "Your account is read only, and cannot sign transactions.", + "rebond": "Rebond", + "rebondSubtitle": "Rebonded funds will back active nominations from the start of the next era.", + "rebondUnlock": "You can rebond unlocks at any time in this period, or withdraw them to your free balance thereafter.", + "recentEraPoints": "Recent Era Points", + "registerFastUnstake": "Registering for fast unstake requires a deposit of", + "remove": "Remove", + "removeAccount": "Remove Account", + "removeBond": "Remove Bond", + "renamePool": "Rename Pool", + "reserveBalance": "Reserve Balance", + "reserveForExistentialDeposit": "Reserve For Existential Deposit", + "reserveForTxFees": "Reserve For Tx Fees", + "reserveText": "Control how much {{unit}} is reserved to pay for transaction fees. This amount is added on top of any funds needed to cover the existential deposit to your account.", + "resetLedgerAccounts": "Reset Ledger Accounts", + "revertChanges": "Revert Changes", + "revertNominationChanges": "Are you sure you wish to revert the changes to your selected nominations?", + "revertNominations": "Revert Nominations", + "rewards": "Rewards", + "rewardsByCountryAndNetwork": "Rewards by Country, Network Provider", + "root": "Root", + "rpcProviders": "RPC Providers", + "scanFromPolkadotVault": "Scan From Polkadot Vault", + "searchAccount": "Search Account", + "selectNetwork": "Select Network", + "selectRpcProvider": "Select an RPC provider to change the {{network}} node Staking Dashboard connects to.", + "selected": "Selected", + "selfStake": "Self Stake", + "sentToCommissionPayee": "This amount will be sent to the commission payee account configured for this pool.", + "setToDestroying": "Set To Destroying", + "setToDestroyingSubtitle": "Setting a pool to destroying cannot be reversed. Only set this state if you intend to close the pool.", + "settings": "Settings", + "signedTransactionSuccessfully": "Signed Transaction Successfully.", + "stop": "Stop", + "stopJoiningPool": "Stop new members from joining the pool.", + "stopNominating": "Stop Nominating", + "stopNominatingBefore": "Stop nominating before unbonding all funds.", + "storedOnChain": "Your updated name will be stored on-chain as encoded bytes. The update will take effect immediately.", + "submit": "Submit", + "submitLock": "Submit Pool Lock", + "submitUnlock": "Submit Pool Unlock", + "submitting": "Submitting", + "subscanDisabled": "Subscan Disabled", + "successfullyFetchedAddress": "Successfully Fetched Address", + "thisMinimumDelay_one": "This minimum delay is the approximate equivalent of {{count}} block.", + "thisMinimumDelay_other": "This minimum delay is the approximate equivalent of {{count}} blocks.", + "titleExtensionConnected": "The {{title}} extension has been connected.", + "toggleFeatures": "Toggle Features", + "togglePlugins": "Toggle Plugins", + "total": "Total", + "transactionRejectedPending": "Transaction was rejected and is still pending.", + "tryAgain": "Try Again", + "unbond": "Unbond", + "unbondAll": "Unbond All", + "unbondErrorBelowMinimum": "Unable to unbond. Your bonded funds are below the minumum of {{bond}} {{unit}}.", + "unbondErrorNoFunds": "You have no {{unit}} to unbond.", + "unbondFundsLeavePool": "Unbond your funds and leave the nomination pool.", + "unbondMemberFunds": "Unbond Member Funds", + "unbondSomeOfYour": "Unbond some of your", + "unbondToMaintain": "Unbond up to the {{minJoinBondUnit}} {{unit}} minimum to maintain your pool membership", + "unbondToMinimum": "Unbond To Minimum", + "unbondToMinimumCreate": "Unbond up to the {{minCreateBondUnit}} {{unit}} minimum bond for pool depositors.", + "unbonding": "Unbonding", + "unbondingWithdraw": "Unbonding will also withdraw your outstanding rewards of", + "undergoingMaintenance": "Undergoing Maintenance", + "unlockChunk": "Unlock chunks cannot currently be rebonded in a pool. If you wish to rebond, withdraw the unlock chunk first and re-bond the funds.", + "unlockLedgerToContinue": "Unlock your Ledger device to continue.", + "unlockPool": "Unlock Pool", + "unlockPoolSubtitle": "Once a pool is unlocked, any account will be able to join as a pool member.", + "unlockTake": "Unlocks can be withdrawn after {{bondDurationFormatted}}.", + "unlocked": "Unlocked", + "unlocks": "Unlocks", + "unlocksAfter": "Unlocks after", + "unlocksInEra": "Unlocks in Era", + "unstake": "Unstake", + "unstakeStopNominating_one": "Stop Nominating {{count}} Validator", + "unstakeStopNominating_other": "Stop Nominating {{count}} Validators", + "unstakeUnbond": "Unbond {{bond}} {{unit}}", + "updateClaimPermission": "Update Claim Permission", + "updateName": "Update the public name of the pool.", + "updatePayoutDestination": "Update Payout Destination", + "updatePoolCommission": "Update pool commission settings.", + "updateWhoClaimRewards": "Update who can claim rewards on your behalf.", + "updated": "Updated", + "validatorDecentralization": "Validator Decentralization", + "validatorMetrics": "Validator Metrics", + "vaultAccounts_one": "{{count}} Account Imported", + "vaultAccounts_other": "{{count}} Accounts Imported", + "vaultAccounts_zero": "No Accounts Imported", + "waitingForQRCode": "Waiting for QR Code", + "web": "Web", + "welcomeToReport": "Bug reports, feature requests and improvements are all welcome.", + "willSurpass": "Adding this many favorites will surpass {{maxNominations}} nominations.", + "withdraw": "Withdraw", + "withdrawMemberFunds": "Withdraw Member Funds", + "withdrawSubtitle": "Funds will be immediately available as free balance after withdrawing.", + "withdrawUnlocked": "Withdraw Unlocked", + "years": "Years" + } +} diff --git a/src/locale/en/pages.json b/src/locale/en/pages.json new file mode 100644 index 0000000000..37f16565dd --- /dev/null +++ b/src/locale/en/pages.json @@ -0,0 +1,233 @@ +{ + "pages": { + "community": { + "bio": "Bio", + "connecting": "Connecting", + "email": "Email", + "fetchingValidators": "Fetching validators", + "goBack": "Go Back", + "noValidators": "This entity contains no validators.", + "validator_one": "{{count}} Validator", + "validator_other": "{{count}} Validators", + "website": "Website" + }, + "nominate": { + "activeNominations": "Active Nominations", + "addressCopied": "Address Copied to Clipboard", + "automaticallyBonded": "Automatically bond payouts to your existing staked balance.", + "back": "Back", + "bond": "Bond", + "bondAmount": "Bond Amount", + "bondedFunds": "Bonded Funds", + "cancel": "Cancel", + "change": "Change", + "controllerAccount": "Controller Account", + "controllerAccountsDeprecated": "Controller Accounts Are Being Deprecated", + "controllerNotImported": "You have not imported your controller account. If you have lost access to your controller account, set a new one now. Otherwise, import the controller into one of your active extensions.", + "earningRewards": "Earning Rewards", + "inactiveNominations": "Inactive Nominations", + "manage": "Manage", + "minimumToEarnRewards": "Minimum To Earn Rewards", + "minimumToNominate": "Minimum To Nominate", + "noNominationsSet": "Inactive: No Nominations Set", + "nominate": "Nominate", + "nominating": "Nominating", + "nominatingAnd": "Nominating and", + "nominations": "Nominations", + "none": "None", + "notAssigned": "Not Assigned", + "notEarningRewards": "Not Earning Rewards", + "notNominating": "Not Nominating", + "payoutDestination": "Payout Destination", + "payoutDestinationSubtitle": "Choose how payouts will be received. Payouts can either be compounded or sent to an account as free balance.", + "pendingPayouts": "Pending Payouts", + "poolDestroy": "Pool is being destroyed and nominating is no longer possible.", + "poolNominations": "Pool Nominations", + "proxyprompt": "Staking dashboard will soon remove support for controller accounts in favour of proxies. Switch your controller account to your stash account to continue using the dashboard and to help in the transition to a better", + "readOnly": "Your account is read only, and cannot sign transactions.", + "setNewController": "Set New Controller", + "startNominating": "Start Nominating", + "status": "Nominator Status", + "stop": "Stop", + "summary": "Summary", + "syncing": "Syncing", + "totalSupplyStaked": "Total Supply Staked", + "unlocked": "Unlocked", + "unstake": "Unstake", + "unstakePromptInProgress_fast": "Fast Unstake in Progress", + "unstakePromptInProgress_regular": "Unstake in Progress", + "unstakePromptInQueue": "You are in the fast unstake queue. You will not be able to carry out any nominator tasks while you are registered for fast unstake.", + "unstakePromptReadyToWithdraw": "Your bonded funds are now unlocked and ready to withdraw.", + "unstakePromptRevert": "If you no longer wish to unstake, rebond your {{unit}} and start nominating again.", + "unstakePromptWaitingForUnlocks": "Waiting for unlocks to become available to withdraw.", + "update": "Update", + "updateToStash": "Update Controller To Stash", + "validator_one": "{{count}} Validator", + "validator_other": "{{count}} Validators", + "waitingForActiveNominations": "Waiting for Active Nominations" + }, + "overview": { + "activeEra": "Active Era", + "activeNominators": "Active Nominators", + "activePools": "Active Pools", + "addressCopied": "Address Copied to Clipboard", + "adjustedRewardsRate": "Adjusted Rewards Rate", + "afterInflation": "after inflation", + "available": "Available", + "balance": "Balance", + "bondedInPools": "The total {{networkUnit}} currently bonded in nomination pools.", + "connect": "Connect", + "free": "Free", + "historicalRewardsRate": "Historical Rewards Rate", + "inPool": "In a Pool", + "inPools": "is currently bonded in pools.", + "inflationRate": "Inflation Rate", + "locked": "Locked", + "manage": "Manage", + "memberOf": "Member of", + "moreResources": "More Resources", + "networkCurrentlyStaked": "{{total}} {{unit}} is currently being staked on {{network}}.", + "networkCurrentlyStakedSubtitle": "The total {{unit}} currently being staked amongst all validators and nominators.", + "networkStats": "Network Stats", + "noActiveAccount": "No Active Account", + "nominating": "Nominating", + "notStaking": "Not Staking", + "overview": "Overview", + "pool": "Pool", + "poolMembersBonding": "pool members are actively bonding in pools.", + "proxy": "Proxy", + "recentPayouts": "Recent Payouts", + "reserve": "Reserve", + "reserveBalance": "Reserve Balance", + "reserved": "Reserved", + "start": "Start", + "subscanDisabled": "Subscan Disabled", + "supplyStaked": "Supply Staked", + "syncingStatus": "Syncing Status", + "timeRemainingThisEra": "Time Remaining This Era", + "totalNominators": "Total Nominators", + "totalNumAccounts": "The total number of accounts that have joined a pool.", + "totalValidators": "Total Validators", + "unitSupplyStaked": "{{unit}} Supply Staked", + "unlocking": "Unlocking", + "updateReserve": "Update Reserve" + }, + "payouts": { + "deductedFromBond": "Deducted from bond", + "fromPool": "From Pool", + "lastEraPayout": "Last Era Payout", + "none": "None", + "notStaking": "Not Staking", + "payout": "Payout", + "payoutHistory": "Payout History", + "poolClaim": "Pool Claim", + "recentPayouts": "Recent Payouts", + "slashed": "Slashed", + "subscanDisabled": "Subscan Disabled" + }, + "pools": { + "activePools": "Active Pools", + "address": "Address", + "addressCopied": "Address Copied to Clipboard", + "addressInvalid": "Address Invalid", + "allPools": "All Pools", + "allRoles": "All Roles", + "assigned": "Assigned", + "assignedToAnyAccount": " Your <b>Root</b>, <b>Nominator</b> and <b>Bouncer</b> roles can be assigned to any account.", + "availableToClaim": "The outstanding amount of {{unit}} available to claim by pool members.", + "back": "Back", + "beenClaimed": "in rewards have been claimed.", + "beenClaimedBy": "The total amount of {{unit}} that has been claimed by pool members.", + "bond": "Bond", + "bondAmount": "Bond Amount", + "bondedFunds": "Bonded Funds", + "bouncer": "Bouncer", + "cancel": "Cancel", + "closePool": "You can now withdraw and close the pool.", + "compound": "Compound", + "create": "Create", + "createAPool": "Create a Pool", + "createPool": "Create Pool", + "depositor": "Depositor", + "destroyPool": "Destroy Pool", + "destroying": "Destroying", + "earningRewards": "Earning Rewards", + "edit": "Edit", + "favorites": "Favorites", + "fetchingFavoritePools": "Fetching favorite pools", + "fetchingMemberList": "Fetching Member List", + "generateNominations": "Generate Nominations", + "inPool": "In Pool", + "inactivePoolNotNominating": "Inactive: Pool Not Nominating", + "join": "Join", + "leave": "Leave", + "leavingPool": "Leaving Pool", + "leftThePool": "All members have now left the pool", + "locked": "Locked", + "manage": "Manage", + "managementOptions": "to select management options.", + "memberOfPool": "Member of Pool", + "members": "Members", + "minimumToCreatePool": "Minimum To Create Pool", + "minimumToJoinPool": "Minimum To Join Pool", + "noFavorites": "No Favorites.", + "nominate": "Nominate", + "nominating": "Nominating", + "nominatingAnd": "Nominating and", + "nominator": "Nominator", + "notEarningRewards": "Not Earning Rewards", + "notInPool": "Not in Pool", + "notSet": "Not Set", + "open": "Open", + "outstandingReward": "outstanding reward balance.", + "overview": "Overview", + "ownerOfPool": "Owner of Pool", + "permissionToUnbond": "You have permission to unbond and withdraw funds of any pool member. Use a member's's menu", + "poolCommission": "Pool Commission", + "poolCreator": " As the pool creator, you will consume your pool's <b>Depositor</b> role.", + "poolCurrentlyLocked": "Pool Currently Locked", + "poolInDestroyingState": "Pool in Destroying State", + "poolMembers": "Pool Members", + "poolMembership": "Pool Membership", + "poolName": "Pool Name", + "poolNameSupport": "Pool names support characters, symbols and emojis - be creative!", + "poolState": "Pool State", + "poolStats": "Pool Stats", + "poolStatus": "Pool Status", + "pools": "Pools", + "readOnly": "Your account is read only, and cannot sign transactions.", + "reformatted": "Address was reformatted", + "roles": "Roles", + "root": "Root", + "save": "Save", + "stopNominating": "To continue with pool closure, stop nominating.", + "summary": "Summary", + "totalBonded": "Total Bonded", + "unbond": "Unbond", + "unbondFunds": "Unbond Funds", + "unbondYourFunds": "You can now unbond your funds.", + "unclaimedRewards": "Unclaimed Rewards", + "unlocked": "Unlocked", + "validator_one": "{{count}} Validator", + "validator_other": "{{count}} Validators", + "waitingForActiveNominations": "Waiting for Active Nominations", + "withdraw": "Withdraw", + "withdrawFunds": "Withdraw Funds", + "withdrawUnlock": "Withdraw your unlock chunk to proceed with pool closure." + }, + "validators": { + "activeValidators": "Active Validators", + "allValidators": "All Validators", + "averageCommission": "Average Commission", + "connecting": "Connecting", + "favoriteValidators": "Favorite Validators", + "favorites": "Favorites", + "fetchingFavoriteValidators": "Fetching favorite validators", + "fetchingValidators": "Fetching validators", + "networkValidators": "Network Validators", + "noFavorites": "No Favorites.", + "totalValidators": "Total Validators", + "validators": "Validators" + } + } +} diff --git a/src/locale/en/tips.json b/src/locale/en/tips.json new file mode 100644 index 0000000000..6d7c402f55 --- /dev/null +++ b/src/locale/en/tips.json @@ -0,0 +1,103 @@ +{ + "tips": { + "connectExtensions": [ + "Connect Extensions", + "Connect your accounts to begin using Polkadot Staking Dashboard.", + [ + "Connect your accounts to begin using Polkadot Staking Dashboard.", + "Accounts are accessed via web extensions, that act as wallets. Your wallet is used to sign transactions that you submit within the dashboard.", + "Connect your wallets from the Connect button at the top right of the dashboard, and select the account you wish to stake with to continue.", + "Staking dashboard supports a range of extensions and wallets." + ] + ], + "howToStake": [ + "How Would You Like to Stake?", + "Either become a nominator or become a member of a pool.", + [ + "There are multiple ways to stake on {NETWORK_NAME}.", + "You can either become a nominator or become a member of a pool.", + "Directly nominating requires you to bond {NETWORK_UNIT} and choose validators you wish to back, and to nominate up to {MAX_NOMINATIONS} of them. To earn rewards as a nominator, you currently need to bond at least {MIN_ACTIVE_STAKE} {NETWORK_UNIT}.", + "Joining a pool is much cheaper, requiring a minimum deposit of {MIN_POOL_JOIN_BOND} {NETWORK_UNIT}. Nominating validators is done on your behalf, and you simply claim rewards from the pool.", + "Creating a pool is also possible, requiring a minimum of {MIN_POOL_CREATE_BOND} {NETWORK_UNIT}." + ] + ], + "joinAnotherPool": [ + "Joining Another Pool", + "Switch to a different account to join another pool.", + [ + "Only one pool can be joined per account on {NETWORK_NAME}. To join more pools, firstly switch to another account." + ] + ], + "keepPoolNominating": [ + "Managing Your Pool", + "It is important to keep your pool actively nominating.", + [ + "It is important to keep your pool actively nominating.", + "If your pool is not earning rewards, members will be tempted to leave and join another pool.", + "Your root and nominator roles are important for managing your nominations. If nominations are not performing well, choose different ones to increase your pool's chances of earning rewards." + ] + ], + "managingNominations": [ + "Managing Nominations", + "Be sure to check the performance of your nominations on a regular basis.", + [ + "Be sure to check the performance of your nominations on a regular basis.", + "To maximise variety and a decentralised mix of validators, choose validators from a range of entities." + ] + ], + "module": { + "cancel": "Cancel", + "close": "Close", + "disableTips": "Disable Dashboard Tips", + "dismissResult": "Dismissing tips will remove them from your overview.", + "dismissTips": "Dismiss Tips", + "more": "More", + "of": "of", + "oneMoment": "One moment please", + "reEnable": "Tips can be re-enabled from dashboard settings, that can be accessed via the cog icon in the bottom left corner of the side menu.", + "syncingWith": "Syncing with {{network}}", + "tips": "Tips" + }, + "monitoringPool": [ + "Managing Pool Membership", + "It's a good idea to check regularly whether your pool is actively earning rewards.", + [ + "Pools will manage nominations on your behalf, but it is still a good idea to check regularly whether your pool is actively earning rewards.", + "Monitor your pool's status to ensure that it is performing well regularly, and consider joining another pool if you are not receiving any rewards." + ] + ], + "recommendedJoinPool": [ + "Recommended: Join a Pool", + "Your account is best suited to join a pool.", + [ + "Based on the amount of {NETWORK_UNIT} your account currently holds, joining a pool is the best way for you to start staking.", + "Joining a pool requires a minimum deposit of {MIN_POOL_JOIN_BOND} {NETWORK_UNIT}." + ] + ], + "recommendedNominator": [ + "Recommended: Become a Nominator", + "You have enough {NETWORK_UNIT} to become a nominator.", + [ + "You have enough {NETWORK_UNIT} to become a nominator and start earning rewards.", + "Nominating directly does however require you to actively check your backed validators." + ] + ], + "reviewingPayouts": [ + "Reviewing Payouts", + "Regularly reviewing your payouts is a good way to gauge how well your nominations are performing.", + [ + "Regularly reviewing your payouts is a good way to gauge how well your nominations are performing.", + "Go to the Payouts page to get a detailed breakdown of each payout you receive, and by which validator (or pool) the payout came from." + ] + ], + "understandingValidatorPerformance": [ + "Measuring Validator Performance", + "Various factors affect how much validators are rewarded.", + [ + "Various factors affect how much validators are rewarded, such as the amount of era points it generates, how many nominators are backing it, and whether it is over subscribed.", + "All these metrics change over time, sometimes in an unpredictable manner.It is therefore important that nominators actively monitor validators and their performance.", + "Staking dashboard provides a range of metrics to help you understand how a validator is performing." + ] + ] + } +} diff --git a/src/locale/index.tsx b/src/locale/index.tsx new file mode 100644 index 0000000000..2699e028fb --- /dev/null +++ b/src/locale/index.tsx @@ -0,0 +1,91 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { enGB, zhCN } from 'date-fns/locale'; +import i18next from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { AppVersion, DefaultLocale } from 'consts'; +import type { AnyJson } from 'types'; +import baseEn from './en/base.json'; +import helpEn from './en/help.json'; +import libEn from './en/library.json'; +import modalsEn from './en/modals.json'; +import pagesEn from './en/pages.json'; +import tipsEn from './en/tips.json'; +import { doDynamicImport, getInitialLanguage, getResources } from './utils'; + +// available locales as key value pairs +export const locales: Record<string, AnyJson> = { + en: enGB, + cn: zhCN, +}; + +// available languages as an array of strings. +export const availableLanguages: Array<string[]> = [ + ['en', 'English'], + ['cn', '中文'], +]; + +// the supported namespaces. +export const lngNamespaces = [ + 'base', + 'help', + 'library', + 'modals', + 'pages', + 'tips', +]; + +// default structure of language resources. +export const fallbackResources = { + ...baseEn, + ...helpEn, + ...libEn, + ...modalsEn, + ...pagesEn, + ...tipsEn, +}; + +// Refresh local storage resources if in development, or if new app version is present. +if ( + localStorage.getItem('app_version') !== AppVersion || + import.meta.env.MODE === 'development' +) { + localStorage.removeItem('lng_resources'); +} + +// get initial language. +const lng: string = getInitialLanguage(); + +// get default resources and whether a dynamic load is required for +// the active language. +const { resources, dynamicLoad } = getResources(lng); + +// default language to show before any dynamic load +const defaultLng = dynamicLoad ? DefaultLocale : lng; + +// configure i18n object. +i18next.use(initReactI18next).init({ + debug: import.meta.env.VITE_DEBUG_I18N === '1', + fallbackLng: DefaultLocale, + lng: defaultLng, + resources, +}); + +// dynamically load default language resources if needed. +if (dynamicLoad) { + doDynamicImport(lng, i18next); +} + +// map i18n to BCP 47 keys, with any custom amendments. +const i18ToLocaleMap: Record<string, string> = { + ...Object.fromEntries(availableLanguages.map((a) => [a[0], a[0]])), + en: 'en-gb', + cn: 'zh-cn', +}; + +// convert i18n locale key to BCP 47 key if needed. +export const i18ToLocale = (l: string) => i18ToLocaleMap[l] || DefaultLocale; + +// export i18next for context. +export { i18next }; diff --git a/src/locale/utils.ts b/src/locale/utils.ts new file mode 100644 index 0000000000..9b1b47bfa1 --- /dev/null +++ b/src/locale/utils.ts @@ -0,0 +1,137 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { extractUrlValue, varToUrlHash } from '@polkadot-cloud/utils'; +import { DefaultLocale } from 'consts'; +import type { AnyApi, AnyJson } from 'types'; +import { availableLanguages, fallbackResources, lngNamespaces } from '.'; + +// Gets the active language +// +// Get the stored language from localStorage, or fallback to +// DefaultLocale otherwise. + +export const getInitialLanguage = () => { + // get language from url if present + const urlLng = extractUrlValue('l'); + if (availableLanguages.find((n: any) => n[0] === urlLng) && urlLng) { + localStorage.setItem('lng', urlLng); + return urlLng; + } + + // fall back to localStorage if present. + const localLng = localStorage.getItem('lng'); + if (availableLanguages.find((n: any) => n[0] === localLng) && localLng) { + return localLng; + } + + localStorage.setItem('lng', DefaultLocale); + return DefaultLocale; +}; + +// Determine resources of selected language, and whether a dynamic +// import is needed for missing language resources. +// +// If selected language is DefaultLocale, then we fall back to +// the default language resources that have already been imported. +export const getResources = (lng: string) => { + let dynamicLoad = false; + + let resources: AnyJson = null; + if (lng === DefaultLocale) { + // determine resources exist without dynamically importing them. + resources = { + en: fallbackResources, + }; + localStorage.setItem( + 'lng_resources', + JSON.stringify({ l: DefaultLocale, r: fallbackResources }) + ); + } else { + // not the default locale, check if local resources exist + let localValid = false; + const localResources = localStorage.getItem('lng_resources'); + if (localResources !== null) { + const { l, r } = JSON.parse(localResources); + + if (l === lng) { + localValid = true; + // local resources found, load them in + resources = { + [lng]: { + ...r, + }, + }; + } + } + if (!localValid) { + // no resources exist locally, dynamic import needed. + dynamicLoad = true; + resources = { + en: fallbackResources, + }; + } + } + return { + resources, + dynamicLoad, + }; +}; + +// Change language +// +// On click handler for changing language in-app. +export const changeLanguage = async (lng: string, i18next: AnyApi) => { + // check whether resources exist and need to by dynamically loaded. + const { resources, dynamicLoad } = getResources(lng); + + localStorage.setItem('lng', lng); + // dynamically load default language resources if needed. + if (dynamicLoad) { + await doDynamicImport(lng, i18next); + } else { + localStorage.setItem( + 'lng_resources', + JSON.stringify({ l: lng, r: resources }) + ); + i18next.changeLanguage(lng); + } + // update url `l` if needed. + varToUrlHash('l', lng, false); +}; + +// Load language resources dynamically. +// +// Bootstraps i18next with additional language resources. +export const loadLngAsync = async (l: string) => { + const resources: AnyJson = await Promise.all( + lngNamespaces.map(async (u) => { + const mod = await import(`./${l}/${u}.json`); + return mod; + }) + ); + + const r: AnyJson = {}; + resources.forEach((mod: AnyJson, i: number) => { + r[lngNamespaces[i]] = mod[lngNamespaces[i]]; + }); + + return { + l, + r, + }; +}; + +// Handles a dynamic import +// +// Once imports have been loaded, they are added to i18next as resources. +// Finally, the active langauge is changed to the imported language. +export const doDynamicImport = async (lng: string, i18next: AnyApi) => { + const { l, r } = await loadLngAsync(lng); + localStorage.setItem('lng_resources', JSON.stringify({ l: lng, r })); + + Object.entries(r).forEach(([ns, inner]: [string, AnyJson]) => { + i18next.addResourceBundle(l, ns, inner); + }); + i18next.changeLanguage(l); +}; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000000..109a09bea8 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,27 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +// Network themes. +import '@polkadot-cloud/core/accent/polkadot-relay.css'; +import '@polkadot-cloud/core/accent/kusama-relay.css'; +import '@polkadot-cloud/core/accent/westend-relay.css'; + +// Default template fonts. +import '@polkadot-cloud/core/theme/default/fonts/index.css'; +// Default template theme. +import '@polkadot-cloud/core/theme/default/index.css'; + +// Polkadot Cloud core styles. +import '@polkadot-cloud/core/css/styles/index.css'; + +import { createRoot } from 'react-dom/client'; +import { App } from 'App'; + +// App font sizes. +import 'styles/index.scss'; + +const rootElement = document.getElementById('root'); +if (!rootElement) throw new Error('Failed to find the root element'); +const root = createRoot(rootElement); + +root.render(<App />); diff --git a/src/modals/AccountPoolRoles/Wrappers.ts b/src/modals/AccountPoolRoles/Wrappers.ts new file mode 100644 index 0000000000..a974d6ad65 --- /dev/null +++ b/src/modals/AccountPoolRoles/Wrappers.ts @@ -0,0 +1,35 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const ContentWrapper = styled.div` + width: 100%; + + > h4 { + color: var(--text-color-secondary); + border-bottom: 1px solid var(--border-primary-color); + margin: 0.75rem 0; + padding-bottom: 0.5rem; + width: 100%; + } + + .items { + position: relative; + border-bottom: none; + width: auto; + border-radius: 0.75rem; + overflow: hidden; + overflow-y: auto; + z-index: 1; + width: 100%; + margin: 1rem 0 1.5rem 0; + + h4 { + margin: 0.2rem 0; + } + h2 { + margin: 0.75rem 0; + } + } +`; diff --git a/src/modals/AccountPoolRoles/index.tsx b/src/modals/AccountPoolRoles/index.tsx new file mode 100644 index 0000000000..e27a8a211c --- /dev/null +++ b/src/modals/AccountPoolRoles/index.tsx @@ -0,0 +1,87 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBars } from '@fortawesome/free-solid-svg-icons'; +import { ButtonOption, ModalPadding, Polkicon } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { Title } from 'library/Modal/Title'; +import { useStatusButtons } from 'pages/Pools/Home/Status/useStatusButtons'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { ContentWrapper } from './Wrappers'; + +export const AccountPoolRoles = () => { + const { t } = useTranslation('modals'); + const { options } = useOverlay().modal.config; + const { getAccountPools } = useBondedPools(); + const { membership } = usePoolMemberships(); + const { who } = options; + const accountPools = getAccountPools(who); + const totalAccountPools = Object.entries(accountPools).length; + const { label } = useStatusButtons(); + + return ( + <> + <Title title={t('allPoolRoles')} icon={faBars} /> + <ModalPadding> + <ContentWrapper> + {membership && ( + <> + <h4>{label}</h4> + <div className="items"> + <Button item={['member']} poolId={String(membership.poolId)} /> + </div> + </> + )} + <h4> + {t('activeRoles', { + count: totalAccountPools, + })} + </h4> + <div className="items"> + {Object.entries(accountPools).map(([key, item]: any, i: number) => ( + <Button item={item} poolId={key} key={`all_roles_root_${i}`} /> + ))} + </div> + </ContentWrapper> + </ModalPadding> + </> + ); +}; + +const Button = ({ item, poolId }: { item: string[]; poolId: string }) => { + const { t } = useTranslation('modals'); + const { setModalStatus } = useOverlay().modal; + const { bondedPools } = useBondedPools(); + const { setSelectedPoolId } = useActivePools(); + const pool = bondedPools.find((b) => String(b.id) === poolId); + const stash = pool?.addresses?.stash || ''; + + return ( + <ButtonOption + content + disabled={false} + onClick={() => { + setSelectedPoolId(poolId); + setModalStatus('closing'); + }} + > + <div className="icon"> + <Polkicon address={stash} size={30} /> + </div> + + <div className="details"> + <h3> + {t('pool')} {poolId} + </h3> + <h4> + {item.includes('root') ? <span>{t('root')}</span> : null} + {item.includes('nominator') ? <span>{t('nominator')}</span> : null} + {item.includes('bouncer') ? <span>{t('bouncer')}</span> : null} + </h4> + </div> + </ButtonOption> + ); +}; diff --git a/src/modals/Accounts/Account.tsx b/src/modals/Accounts/Account.tsx new file mode 100644 index 0000000000..51b4d8bdd0 --- /dev/null +++ b/src/modals/Accounts/Account.tsx @@ -0,0 +1,136 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faGlasses } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ellipsisFn, planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { ExtensionIcons } from '@polkadot-cloud/assets/extensions'; +import LedgerSVG from '@polkadot-cloud/assets/extensions/svg/ledger.svg?react'; +import PolkadotVaultSVG from '@polkadot-cloud/assets/extensions/svg/polkadotvault.svg?react'; +import { Polkicon } from '@polkadot-cloud/react'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { AccountWrapper } from './Wrappers'; +import type { AccountItemProps } from './types'; + +export const AccountButton = ({ + label, + address, + delegator, + proxyType, + noBorder = false, +}: AccountItemProps) => { + const { t } = useTranslation('modals'); + const { getAccount } = useImportedAccounts(); + const { + activeProxy, + activeAccount, + setActiveAccount, + setActiveProxy, + activeProxyType, + } = useActiveAccounts(); + const { setModalStatus } = useOverlay().modal; + const { units, unit } = useNetwork().networkData; + const { getTransferOptions } = useTransferOptions(); + const { freeBalance } = getTransferOptions(address || ''); + + // Accumulate account data. + const meta = getAccount(address || ''); + + const imported = !!meta; + const connectTo = delegator || address || ''; + const connectProxy = delegator ? address || null : ''; + + // Determine account source icon. + const Icon = + meta?.source === 'ledger' + ? LedgerSVG + : meta?.source === 'vault' + ? PolkadotVaultSVG + : ExtensionIcons[meta?.source || ''] || undefined; + + // Determine if this account is active (active account or proxy). + const isActive = + (connectTo === activeAccount && + address === activeAccount && + !activeProxy) || + (connectProxy === activeProxy && + proxyType === activeProxyType && + activeProxy); + + // Handle account click. Handles both active account and active proxy. + const handleClick = () => { + if (!imported) return; + setActiveAccount(getAccount(connectTo)?.address || null); + setActiveProxy(proxyType ? { address: connectProxy, proxyType } : null); + setModalStatus('closing'); + }; + + return ( + <AccountWrapper className={isActive ? 'active' : undefined}> + <div className={noBorder ? 'noBorder' : undefined}> + <section className="head"> + <button + type="button" + onClick={() => handleClick()} + disabled={!imported} + > + {delegator && ( + <div className="delegator"> + <Polkicon address={delegator} size={23} /> + </div> + )} + <div className="identicon"> + <Polkicon address={address ?? ''} size={23} /> + </div> + <span className="name"> + {delegator && ( + <> + <span> + {proxyType} {t('proxy')} + </span> + </> + )} + {meta?.name ?? ellipsisFn(address ?? '')} + </span> + {meta?.source === 'external' && ( + <div + className="label warning" + style={{ color: '#a17703', paddingLeft: '0.5rem' }} + > + {t('readOnly')} + </div> + )} + <div className={label === undefined ? `` : label[0]}> + {label !== undefined ? <h5>{label[1]}</h5> : null} + {Icon !== undefined ? ( + <span className="icon"> + <Icon /> + </span> + ) : null} + + {meta?.source === 'external' && ( + <FontAwesomeIcon + icon={faGlasses} + className="icon" + style={{ opacity: 0.7 }} + /> + )} + </div> + </button> + </section> + <section className="foot"> + <span className="balance"> + {`${t('free')}: ${planckToUnit(freeBalance, units) + .decimalPlaces(3) + .toFormat()} ${unit}`} + </span> + </section> + </div> + </AccountWrapper> + ); +}; diff --git a/src/modals/Accounts/Delegates/Wrapper.ts b/src/modals/Accounts/Delegates/Wrapper.ts new file mode 100644 index 0000000000..698a5ff8c6 --- /dev/null +++ b/src/modals/Accounts/Delegates/Wrapper.ts @@ -0,0 +1,22 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { styled } from 'styled-components'; + +export const DelegatesWrapper = styled.div` + border-left: 1px solid var(--border-primary-color); + width: 100%; + display: flex; + flex-direction: column; + padding-left: 1rem; + margin: 0.65rem 0 1.25rem 0; + + > div { + &:first-child { + margin-top: 0; + } + &:last-child { + margin-bottom: 0; + } + } +`; diff --git a/src/modals/Accounts/Delegates/index.tsx b/src/modals/Accounts/Delegates/index.tsx new file mode 100644 index 0000000000..96ed618870 --- /dev/null +++ b/src/modals/Accounts/Delegates/index.tsx @@ -0,0 +1,40 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { isSupportedProxy } from 'config/proxies'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { AccountButton } from '../Account'; +import { DelegatesWrapper } from './Wrapper'; +import type { DelegatesProps } from '../types'; + +export const Delegates = ({ delegates, delegator }: DelegatesProps) => { + const { accounts } = useImportedAccounts(); + const { getAccount } = useImportedAccounts(); + + // Filter delegates that are external or not imported. Default to empty array if there are no + // delegates for this address. + const delegatesList = + delegates?.delegates.filter( + ({ delegate, proxyType }) => + accounts.find(({ address }) => address === delegate) !== undefined && + isSupportedProxy(proxyType) && + getAccount(delegate || null)?.source !== 'external' + ) || []; + + return ( + <> + {delegatesList.length ? ( + <DelegatesWrapper> + {delegatesList.map(({ delegate, proxyType }, i) => ( + <AccountButton + key={`_del_${i}`} + address={delegate} + delegator={delegator} + proxyType={proxyType} + /> + ))} + </DelegatesWrapper> + ) : null} + </> + ); +}; diff --git a/src/modals/Accounts/Wrappers.ts b/src/modals/Accounts/Wrappers.ts new file mode 100644 index 0000000000..01407c1aa1 --- /dev/null +++ b/src/modals/Accounts/Wrappers.ts @@ -0,0 +1,163 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const AccountWrapper = styled.div` + transition: transform var(--transition-duration); + margin: 0.6rem 0 0 0; + width: 100%; + + &.active { + > div { + border: 1px solid var(--accent-color-primary); + } + } + + &:hover { + transform: scale(1.01); + } + + > div { + background: var(--button-primary-background); + color: var(--text-color-primary); + font-family: InterSemiBold, sans-serif; + border: 1px solid transparent; + display: flex; + align-items: flex-start; + flex-direction: column; + border-radius: 0.85rem; + width: 100%; + overflow: hidden; + + &.noBorder { + border: none; + } + + > section { + display: flex; + flex-direction: row; + align-items: center; + width: 100%; + + /* Top half of the button, account information */ + &.head { + background: var(--button-tertiary-background); + + > button { + color: var(--text-color-primary); + display: flex; + align-items: center; + justify-content: flex-start; + flex-shrink: 1; + padding: 0.5rem 0.75rem; + font-size: 1.05rem; + width: 100%; + transition: background var(--transition-duration); + + &:hover { + .name { + color: var(--accent-color-primary); + } + } + + .label { + font-size: 0.95rem; + display: flex; + align-items: flex-end; + } + + overflow: hidden; + .name { + transition: color var(--transition-duration); + font-family: InterSemiBold, sans-serif; + max-width: 100%; + margin: 0 0.5rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + > span { + opacity: 0.7; + margin-right: 0.6rem; + > svg { + margin-left: 0.5rem; + } + } + } + + .badge { + background-color: var(--background-floating-card); + color: var(--text-color-secondary); + margin-left: 1rem; + padding: 0.25rem 0.5rem; + border-radius: 0.45rem; + font-size: 0.9rem; + } + .delegator { + width: 1rem; + z-index: 0; + } + .identicon { + z-index: 1; + } + + /* svg theming */ + svg { + .light { + fill: var(--text-color-invert); + } + .dark { + fill: var(--text-color-secondary); + } + } + + > div:last-child { + display: flex; + flex-grow: 1; + justify-content: flex-end; + + &.neutral { + h5 { + color: var(--text-color-secondary); + opacity: 0.75; + } + } + &.danger { + h5 { + color: var(--status-danger-color); + } + } + .icon { + width: 1.25rem; + height: 1.25rem; + margin-left: 0.75rem; + + svg { + width: inherit; + height: inherit; + } + } + } + } + } + + /* Bottom half of the button, account metadata */ + &.foot { + border-top: 1px solid var(--border-primary-color); + padding: 0.7rem 1rem; + + > .balance { + color: var(--text-color-secondary); + font-size: 0.9rem; + opacity: 0.6; + } + } + } + } +`; + +export const AccountSeparator = styled.div` + width: 100%; + height: 0.5rem; +`; diff --git a/src/modals/Accounts/index.tsx b/src/modals/Accounts/index.tsx new file mode 100644 index 0000000000..1f534ec27f --- /dev/null +++ b/src/modals/Accounts/index.tsx @@ -0,0 +1,245 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft, faLinkSlash } from '@fortawesome/free-solid-svg-icons'; +import { + ActionItem, + ButtonPrimaryInvert, + ButtonText, + ModalCustomHeader, + ModalPadding, +} from '@polkadot-cloud/react'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBalances } from 'contexts/Balances'; +import { useBonded } from 'contexts/Bonded'; +import { + useExtensions, + useEffectIgnoreInitial, + useOverlay, +} from '@polkadot-cloud/react/hooks'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { useProxies } from 'contexts/Proxies'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { AccountButton } from './Account'; +import { Delegates } from './Delegates'; +import { AccountSeparator, AccountWrapper } from './Wrappers'; +import type { + AccountInPool, + AccountNominating, + AccountNominatingAndInPool, + AccountNotStaking, +} from './types'; + +export const Accounts = () => { + const { t } = useTranslation('modals'); + const { balances } = useBalances(); + const { getDelegates } = useProxies(); + const { bondedAccounts } = useBonded(); + const { ledgers, getLocks } = useBalances(); + const { extensionsStatus } = useExtensions(); + const { memberships } = usePoolMemberships(); + const { + replaceModal, + status: modalStatus, + setModalResize, + } = useOverlay().modal; + const { accounts } = useImportedAccounts(); + const { activeAccount, setActiveAccount, setActiveProxy } = + useActiveAccounts(); + + // Store local copy of accounts. + const [localAccounts, setLocalAccounts] = useState(accounts); + + const stashes: string[] = []; + // accumulate imported stash accounts + for (const { address } of localAccounts) { + const locks = getLocks(address); + + // account is a stash if they have an active `staking` lock + if (locks.find(({ id }) => id === 'staking')) { + stashes.push(address); + } + } + + // construct account groupings + const nominating: AccountNominating[] = []; + const inPool: AccountInPool[] = []; + const nominatingAndPool: AccountNominatingAndInPool[] = []; + const notStaking: AccountNotStaking[] = []; + + for (const { address } of localAccounts) { + let isNominating = false; + let isInPool = false; + const isStash = stashes[stashes.indexOf(address)] ?? null; + const delegates = getDelegates(address); + + const poolMember = memberships.find((m) => m.address === address) ?? null; + + // Check if nominating. + if (isStash && nominating.find((a) => a.address === address) === undefined) + isNominating = true; + + // Check if in pool. + if (poolMember) + if (!inPool.find((n) => n.address === address)) isInPool = true; + + // If not doing anything, add address to `notStaking`. + if ( + !isStash && + !poolMember && + !notStaking.find((n) => n.address === address) + ) { + notStaking.push({ address, delegates }); + continue; + } + + // If both nominating and in pool, add to this list. + if ( + isNominating && + isInPool && + poolMember && + !nominatingAndPool.find((n) => n.address === address) + ) { + nominatingAndPool.push({ + ...poolMember, + address, + stashImported: true, + delegates, + }); + continue; + } + + // Nominating only. + if (isNominating && !isInPool) { + nominating.push({ address, stashImported: true, delegates }); + continue; + } + + // In pool only. + if (!isNominating && isInPool && poolMember) + inPool.push({ ...poolMember, delegates }); + } + + // Refresh local accounts state when context accounts change. + useEffect(() => setLocalAccounts(accounts), [accounts]); + + // Resize if modal open upon state changes. + useEffectIgnoreInitial(() => { + if (modalStatus === 'open') setModalResize(); + }, [ + activeAccount, + accounts, + bondedAccounts, + balances, + ledgers, + extensionsStatus, + ]); + + return ( + <ModalPadding> + <ModalCustomHeader> + <div className="first"> + <h1>{t('accounts')}</h1> + <ButtonPrimaryInvert + text={t('goToConnect')} + iconLeft={faChevronLeft} + iconTransform="shrink-3" + onClick={() => + replaceModal({ key: 'Connect', options: { disableScroll: true } }) + } + marginLeft + /> + </div> + <div> + {activeAccount && ( + <ButtonText + style={{ + color: 'var(--accent-color-primary)', + }} + text={t('disconnect')} + iconRight={faLinkSlash} + onClick={() => { + setActiveAccount(null); + setActiveProxy(null); + }} + /> + )} + </div> + </ModalCustomHeader> + {!activeAccount && !accounts.length && ( + <AccountWrapper style={{ marginTop: '1.5rem' }}> + <div> + <div> + <h4 style={{ padding: '0.75rem 1rem' }}> + {t('noActiveAccount')} + </h4> + </div> + <div /> + </div> + </AccountWrapper> + )} + + {nominatingAndPool.length ? ( + <> + <AccountSeparator /> + <ActionItem text={t('nominatingAndInPool')} /> + {nominatingAndPool.map(({ address, delegates }, i) => ( + <React.Fragment key={`acc_nominating_and_pool_${i}`}> + <AccountButton address={address} /> + {address && ( + <Delegates delegator={address} delegates={delegates} /> + )} + </React.Fragment> + ))} + </> + ) : null} + + {nominating.length ? ( + <> + <AccountSeparator /> + <ActionItem text={t('nominating')} /> + {nominating.map(({ address, delegates }, i) => ( + <React.Fragment key={`acc_nominating_${i}`}> + <AccountButton address={address} /> + {address && ( + <Delegates delegator={address} delegates={delegates} /> + )} + </React.Fragment> + ))} + </> + ) : null} + + {inPool.length ? ( + <> + <AccountSeparator /> + <ActionItem text={t('inPool')} /> + {inPool.map(({ address, delegates }, i) => ( + <React.Fragment key={`acc_in_pool_${i}`}> + <AccountButton address={address} /> + {address && ( + <Delegates delegator={address} delegates={delegates} /> + )} + </React.Fragment> + ))} + </> + ) : null} + + {notStaking.length ? ( + <> + <AccountSeparator /> + <ActionItem text={t('notStaking')} /> + {notStaking.map(({ address, delegates }, i) => ( + <React.Fragment key={`acc_not_staking_${i}`}> + <AccountButton address={address} /> + {address && ( + <Delegates delegator={address} delegates={delegates} /> + )} + </React.Fragment> + ))} + </> + ) : null} + </ModalPadding> + ); +}; diff --git a/src/modals/Accounts/types.ts b/src/modals/Accounts/types.ts new file mode 100644 index 0000000000..8584ec8439 --- /dev/null +++ b/src/modals/Accounts/types.ts @@ -0,0 +1,37 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { PoolMembership } from 'contexts/Pools/types'; +import type { Proxy } from 'contexts/Proxies/type'; +import type { MaybeAddress } from 'types'; + +export interface AccountItemProps { + address?: MaybeAddress; + label?: string[]; + asElement?: boolean; + delegator?: string; + noBorder?: boolean; + proxyType?: string; +} + +export interface DelegatesProps { + delegator: string; + delegates: Proxy | undefined; +} + +export interface AccountInPool extends PoolMembership { + delegates?: Proxy; +} + +export interface AccountNominating { + address: MaybeAddress; + stashImported: boolean; + delegates?: Proxy; +} + +export interface AccountNotStaking { + address: string; + delegates?: Proxy; +} + +export type AccountNominatingAndInPool = AccountNominating & AccountInPool; diff --git a/src/modals/BalanceTest/index.tsx b/src/modals/BalanceTest/index.tsx new file mode 100644 index 0000000000..44203c2bf4 --- /dev/null +++ b/src/modals/BalanceTest/index.tsx @@ -0,0 +1,74 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding } from '@polkadot-cloud/react'; +import { unitToPlanck } from '@polkadot-cloud/utils'; +import { useApi } from 'contexts/Api'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useBatchCall } from 'library/Hooks/useBatchCall'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { useEffect } from 'react'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const BalanceTest = () => { + const { api } = useApi(); + const { + networkData: { units }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { notEnoughFunds } = useTxMeta(); + const { newBatchCall } = useBatchCall(); + const { setModalStatus, setModalResize } = useOverlay().modal; + + // tx to submit + const getTx = () => { + const tx = null; + if (!api || !activeAccount) { + return tx; + } + + const txs = [ + api.tx.balances.transfer( + { + id: '1554u1a67ApEt5xmjbZwjgDNaVckbzB6cjRHWAQ1SpNkNxTd', + }, + unitToPlanck('0.1', units).toString() + ), + api.tx.balances.transfer( + { + id: '1554u1a67ApEt5xmjbZwjgDNaVckbzB6cjRHWAQ1SpNkNxTd', + }, + unitToPlanck('0.05', units).toString() + ), + ]; + const batch = newBatchCall(txs, activeAccount); + + return batch; + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + useEffect(() => setModalResize(), [notEnoughFunds]); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded">Balance Test</h2> + </ModalPadding> + <SubmitTx valid {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/modals/Bio/Wrapper.ts b/src/modals/Bio/Wrapper.ts new file mode 100644 index 0000000000..2c3566adcb --- /dev/null +++ b/src/modals/Bio/Wrapper.ts @@ -0,0 +1,19 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + display: flex; + flex-flow: column wrap; + padding: 0.5rem; + + h2 { + color: var(--text-color-primary); + margin-top: 0.5rem; + } + + h3 { + margin-bottom: 0.5rem; + } +`; diff --git a/src/modals/Bio/index.tsx b/src/modals/Bio/index.tsx new file mode 100644 index 0000000000..5afc2cc22c --- /dev/null +++ b/src/modals/Bio/index.tsx @@ -0,0 +1,20 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding } from '@polkadot-cloud/react'; +import { Title } from 'library/Modal/Title'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { Wrapper } from './Wrapper'; + +export const Bio = () => { + const { name, bio } = useOverlay().modal.config.options; + + return ( + <> + <Title title={name} /> + <ModalPadding> + <Wrapper>{bio !== undefined && <h4>{bio}</h4>}</Wrapper> + </ModalPadding> + </> + ); +}; diff --git a/src/modals/Bond/index.tsx b/src/modals/Bond/index.tsx new file mode 100644 index 0000000000..81104962d5 --- /dev/null +++ b/src/modals/Bond/index.tsx @@ -0,0 +1,179 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding, ModalWarnings } from '@polkadot-cloud/react'; +import { planckToUnit, unitToPlanck } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { BondFeedback } from 'library/Form/Bond/BondFeedback'; +import { Warning } from 'library/Form/Warning'; +import { useBondGreatestFee } from 'library/Hooks/useBondGreatestFee'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const Bond = () => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { notEnoughFunds } = useTxMeta(); + const { selectedActivePool } = useActivePools(); + const { getSignerWarnings } = useSignerWarnings(); + const { feeReserve, getTransferOptions } = useTransferOptions(); + const { + setModalStatus, + config: { options }, + setModalResize, + } = useOverlay().modal; + + const { bondFor } = options; + const isStaking = bondFor === 'nominator'; + const isPooling = bondFor === 'pool'; + const { nominate, pool } = getTransferOptions(activeAccount); + + const freeBalanceBn = + bondFor === 'nominator' + ? nominate.totalAdditionalBond + : pool.totalAdditionalBond; + + const freeBalance = planckToUnit(freeBalanceBn.minus(feeReserve), units); + const largestTxFee = useBondGreatestFee({ bondFor }); + + // calculate any unclaimed pool rewards. + let { pendingRewards } = selectedActivePool || {}; + pendingRewards = pendingRewards ?? new BigNumber(0); + pendingRewards = planckToUnit(pendingRewards, units); + + // local bond value. + const [bond, setBond] = useState<{ bond: string }>({ + bond: freeBalance.toString(), + }); + + // bond valid. + const [bondValid, setBondValid] = useState<boolean>(false); + + // feedback errors to trigger modal resize + const [feedbackErrors, setFeedbackErrors] = useState<string[]>([]); + + // bond minus tx fees. + const enoughToCoverTxFees: boolean = freeBalance + .minus(bond.bond) + .isGreaterThan(planckToUnit(largestTxFee, units)); + + // bond value after max tx fees have been deducated. + let bondAfterTxFees: BigNumber; + + if (enoughToCoverTxFees) { + bondAfterTxFees = unitToPlanck(String(bond.bond), units); + } else { + bondAfterTxFees = BigNumber.max( + unitToPlanck(String(bond.bond), units).minus(largestTxFee), + 0 + ); + } + + // update bond value on task change. + useEffect(() => { + setBond({ bond: freeBalance.toString() }); + }, [freeBalance.toString()]); + + // determine whether this is a pool or staking transaction. + const determineTx = (bondToSubmit: BigNumber) => { + let tx = null; + if (!api) { + return tx; + } + + const bondAsString = !bondValid + ? '0' + : bondToSubmit.isNaN() + ? '0' + : bondToSubmit.toString(); + + if (isPooling) { + tx = api.tx.nominationPools.bondExtra({ + FreeBalance: bondAsString, + }); + } else if (isStaking) { + tx = api.tx.staking.bondExtra(bondAsString); + } + return tx; + }; + + // the actual bond tx to submit + const getTx = (bondToSubmit: BigNumber) => { + if (!api || !activeAccount) { + return null; + } + return determineTx(bondToSubmit); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(bondAfterTxFees), + from: activeAccount, + shouldSubmit: bondValid, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + // modal resize on form update + useEffect( + () => setModalResize(), + [bond, bondValid, notEnoughFunds, feedbackErrors.length, warnings.length] + ); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded">{t('addToBond')}</h2> + {pendingRewards > 0 && bondFor === 'pool' ? ( + <ModalWarnings withMargin> + <Warning + text={`${t('bondingWithdraw')} ${pendingRewards} ${unit}.`} + /> + </ModalWarnings> + ) : null} + <BondFeedback + syncing={largestTxFee.isZero()} + bondFor={bondFor} + listenIsValid={(valid, errors) => { + setBondValid(valid); + setFeedbackErrors(errors); + }} + defaultBond={null} + setters={[ + { + set: setBond, + current: bond, + }, + ]} + parentErrors={warnings} + txFees={largestTxFee} + /> + <p>{t('newlyBondedFunds')}</p> + </ModalPadding> + <SubmitTx valid={bondValid} {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/modals/ChangeNominations/index.tsx b/src/modals/ChangeNominations/index.tsx new file mode 100644 index 0000000000..3801a330c7 --- /dev/null +++ b/src/modals/ChangeNominations/index.tsx @@ -0,0 +1,155 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + ModalPadding, + ModalSeparator, + ModalWarnings, +} from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBonded } from 'contexts/Bonded'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const ChangeNominations = () => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { activeAccount } = useActiveAccounts(); + const { notEnoughFunds } = useTxMeta(); + const { getSignerWarnings } = useSignerWarnings(); + const { getBondedAccount, getAccountNominations } = useBonded(); + const { + setModalStatus, + config: { options }, + setModalResize, + } = useOverlay().modal; + const { poolNominations, isNominator, isOwner, selectedActivePool } = + useActivePools(); + const { nominations: newNominations, provider, bondFor } = options; + + const isPool = bondFor === 'pool'; + const isStaking = bondFor === 'nominator'; + const controller = getBondedAccount(activeAccount); + const signingAccount = isPool ? activeAccount : controller; + + const nominations = + isPool === true + ? poolNominations.targets + : getAccountNominations(activeAccount); + const removing = nominations.length - newNominations.length; + const remaining = newNominations.length; + + // valid to submit transaction + const [valid, setValid] = useState<boolean>(false); + + // ensure selected key is valid + useEffect(() => { + setValid(nominations.length > 0); + }, [nominations]); + + // ensure roles are valid + let isValid = nominations.length > 0; + if (isPool) { + isValid = (isNominator() || isOwner()) ?? false; + } + + useEffect(() => setModalResize(), [notEnoughFunds]); + + useEffect(() => setValid(isValid), [isValid]); + + // tx to submit + const getTx = () => { + let tx = null; + if (!valid || !api) { + return tx; + } + + // targets submission differs between staking and pools + const targetsToSubmit = newNominations.map((item: any) => + isPool + ? item + : { + Id: item, + } + ); + + if (isPool) { + // if nominations remain, call nominate + if (remaining !== 0) { + tx = api.tx.nominationPools.nominate( + selectedActivePool?.id || 0, + targetsToSubmit + ); + } else { + // wishing to stop all nominations, call chill + tx = api.tx.nominationPools.chill(selectedActivePool?.id || 0); + } + } else if (isStaking) { + if (remaining !== 0) { + tx = api.tx.staking.nominate(targetsToSubmit); + } else { + tx = api.tx.staking.chill(); + } + } + return tx; + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: signingAccount, + shouldSubmit: valid, + callbackSubmit: () => { + setModalStatus('closing'); + + // if removing a subset of nominations, reset selected list + if (provider) { + provider.setSelectActive(false); + provider.resetSelected(); + } + }, + callbackInBlock: () => {}, + }); + + const warnings = getSignerWarnings( + activeAccount, + isStaking, + submitExtrinsic.proxySupported + ); + + if (!nominations.length) { + warnings.push(`${t('noNominationsSet')}`); + } + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded"> + {t('stop')}{' '} + {!remaining + ? t('allNominations') + : `${t('nomination', { count: removing })}`} + </h2> + <ModalSeparator /> + {warnings.length ? ( + <ModalWarnings> + {warnings.map((text, i) => ( + <Warning key={`warning_${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <p>{t('changeNomination')}</p> + </ModalPadding> + <SubmitTx fromController={isStaking} valid={valid} {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/modals/ChangePoolRoles/RoleChange.tsx b/src/modals/ChangePoolRoles/RoleChange.tsx new file mode 100644 index 0000000000..b2bb1b9455 --- /dev/null +++ b/src/modals/ChangePoolRoles/RoleChange.tsx @@ -0,0 +1,37 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faAnglesRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ellipsisFn, remToUnit } from '@polkadot-cloud/utils'; +import { Polkicon } from '@polkadot-cloud/react'; +import { RoleChangeWrapper } from './Wrapper'; + +export const RoleChange = ({ roleName, oldAddress, newAddress }: any) => { + return ( + <RoleChangeWrapper> + <div className="label">{roleName}</div> + <div className="role-change"> + <div className="input-wrap selected"> + <Polkicon address={oldAddress ?? ''} size={remToUnit('2rem')} /> + <input + className="input" + disabled + value={oldAddress ? ellipsisFn(oldAddress) : ''} + /> + </div> + <span> + <FontAwesomeIcon icon={faAnglesRight} /> + </span> + <div className="input-wrap selected"> + <Polkicon address={newAddress ?? ''} size={remToUnit('2rem')} /> + <input + className="input" + disabled + value={newAddress ? ellipsisFn(newAddress) : ''} + /> + </div> + </div> + </RoleChangeWrapper> + ); +}; diff --git a/src/modals/ChangePoolRoles/Wrapper.ts b/src/modals/ChangePoolRoles/Wrapper.ts new file mode 100644 index 0000000000..7ce538a0d5 --- /dev/null +++ b/src/modals/ChangePoolRoles/Wrapper.ts @@ -0,0 +1,60 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + display: flex; + flex-flow: column wrap; + margin-top: 1rem; + width: 100%; +`; + +export const RoleChangeWrapper = styled.div` + position: relative; + width: 100%; + height: auto; + overflow: hidden; + + .label { + color: var(--text-color-secondary); + margin: 0.25rem 0 0.75rem 0; + } + .role-change { + flex: 1; + display: flex; + align-items: center; + margin-bottom: 1rem; + + > span { + color: var(--text-color-secondary); + margin: 0 0.75rem; + opacity: 0.5; + } + } + + .input-wrap { + border-bottom: 1px solid var(--border-primary-color); + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 0.25rem 0 0 0; + margin: 0.25rem 0.7rem 0 0.7rem; + flex: 1; + + &.selected { + border: 1px solid var(--border-primary-color); + border-radius: 1rem; + margin: 0; + padding: 0.1rem 0.75rem; + } + } + .input { + border: none; + padding-left: 0.75rem; + flex: 1; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } +`; diff --git a/src/modals/ChangePoolRoles/index.tsx b/src/modals/ChangePoolRoles/index.tsx new file mode 100644 index 0000000000..53845ead90 --- /dev/null +++ b/src/modals/ChangePoolRoles/index.tsx @@ -0,0 +1,90 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useEffect } from 'react'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { RoleChange } from './RoleChange'; +import { Wrapper } from './Wrapper'; + +export const ChangePoolRoles = () => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { activeAccount } = useActiveAccounts(); + const { notEnoughFunds } = useTxMeta(); + const { replacePoolRoles } = useBondedPools(); + const { + setModalStatus, + config: { options }, + setModalResize, + } = useOverlay().modal; + const { id: poolId, roleEdits } = options; + + // tx to submit + const getTx = () => { + let tx = null; + const root = roleEdits?.root?.newAddress + ? { Set: roleEdits?.root?.newAddress } + : 'Remove'; + const nominator = roleEdits?.nominator?.newAddress + ? { Set: roleEdits?.nominator?.newAddress } + : 'Remove'; + const bouncer = roleEdits?.bouncer?.newAddress + ? { Set: roleEdits?.bouncer?.newAddress } + : 'Remove'; + + tx = api?.tx.nominationPools?.updateRoles(poolId, root, nominator, bouncer); + return tx; + }; + + // handle extrinsic + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => { + // manually update bondedPools with new pool roles + replacePoolRoles(poolId, roleEdits); + }, + }); + + useEffect(() => setModalResize(), [notEnoughFunds]); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded">{t('changePoolRoles')}</h2> + <Wrapper> + <RoleChange + roleName={t('root')} + oldAddress={roleEdits?.root?.oldAddress} + newAddress={roleEdits?.root?.newAddress} + /> + <RoleChange + roleName={t('nominator')} + oldAddress={roleEdits?.nominator?.oldAddress} + newAddress={roleEdits?.nominator?.newAddress} + /> + <RoleChange + roleName={t('bouncer')} + oldAddress={roleEdits?.bouncer?.oldAddress} + newAddress={roleEdits?.bouncer?.newAddress} + /> + </Wrapper> + </ModalPadding> + <SubmitTx {...submitExtrinsic} valid /> + </> + ); +}; diff --git a/src/modals/ChooseLanguage/Wrapper.ts b/src/modals/ChooseLanguage/Wrapper.ts new file mode 100644 index 0000000000..06999dac0e --- /dev/null +++ b/src/modals/ChooseLanguage/Wrapper.ts @@ -0,0 +1,55 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const ContentWrapper = styled.div` + box-sizing: border-box; + width: 100%; + + .items { + box-sizing: border-box; + position: relative; + box-sizing: border-box; + border-bottom: none; + width: auto; + border-radius: 0.75rem; + overflow: hidden; + overflow-y: auto; + z-index: 1; + width: 100%; + margin: 1rem 0 1.5rem 0; + } +`; + +export const LocaleButton = styled.button<{ $connected: boolean }>` + color: var(--text-color-primary); + background: var(--button-primary-background); + font-family: InterSemiBold, sans-serif; + box-sizing: border-box; + padding: 1rem; + cursor: pointer; + border-radius: 0.75rem; + display: inline-flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + border: 1px solid var(--status-success-color-transparent); + margin: 0.5rem 0; + ${(props) => + props.$connected !== true && + ` + border: 1px solid rgba(0,0,0,0); +`} + + h4 { + color: var(--text-color-secondary); + &.selected { + color: var(--status-success-color); + margin-left: 0.75rem; + } + } + &:hover { + background: var(--button-hover-background); + } +`; diff --git a/src/modals/ChooseLanguage/index.tsx b/src/modals/ChooseLanguage/index.tsx new file mode 100644 index 0000000000..b5c021c2be --- /dev/null +++ b/src/modals/ChooseLanguage/index.tsx @@ -0,0 +1,50 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import LanguageSVG from 'img/language.svg?react'; +import { Title } from 'library/Modal/Title'; +import { availableLanguages } from 'locale'; +import { changeLanguage } from 'locale/utils'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { ContentWrapper, LocaleButton } from './Wrapper'; + +export const ChooseLanguage = () => { + const { i18n, t } = useTranslation('modals'); + const { setModalStatus } = useOverlay().modal; + + return ( + <> + <Title title={t('chooseLanguage')} Svg={LanguageSVG} /> + <ModalPadding> + <ContentWrapper> + <div className="item"> + {availableLanguages.map((a, i) => { + const code = a[0]; + const label = a[1]; + + return ( + <h3 key={`${code}_${i}`}> + <LocaleButton + $connected={i18n.resolvedLanguage === code} + type="button" + onClick={() => { + changeLanguage(code, i18n); + setModalStatus('closing'); + }} + > + {label} + {i18n.resolvedLanguage === code && ( + <h4 className="selected">{t('selected')}</h4> + )} + </LocaleButton> + </h3> + ); + })} + </div> + </ContentWrapper> + </ModalPadding> + </> + ); +}; diff --git a/src/modals/ClaimPayouts/Forms.tsx b/src/modals/ClaimPayouts/Forms.tsx new file mode 100644 index 0000000000..98de20ecbd --- /dev/null +++ b/src/modals/ClaimPayouts/Forms.tsx @@ -0,0 +1,163 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { + ActionItem, + ButtonSubmitInvert, + ModalWarnings, +} from '@polkadot-cloud/react'; +import { planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { forwardRef, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { SubmitTx } from 'library/SubmitTx'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useBatchCall } from 'library/Hooks/useBatchCall'; +import type { AnyApi, AnySubscan } from 'types'; +import { useSubscan } from 'contexts/Plugins/Subscan'; +import { usePayouts } from 'contexts/Payouts'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import type { FormProps, ActivePayout } from './types'; +import { ContentWrapper } from './Wrappers'; + +export const Forms = forwardRef( + ({ setSection, payouts, setPayouts }: FormProps, ref: any) => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { newBatchCall } = useBatchCall(); + const { removeEraPayout } = usePayouts(); + const { setModalStatus } = useOverlay().modal; + const { getSignerWarnings } = useSignerWarnings(); + const { unclaimedPayouts: unclaimedPayoutsSubscan, setUnclaimedPayouts } = + useSubscan(); + + const totalPayout = + payouts?.reduce( + (total: BigNumber, cur: ActivePayout) => total.plus(cur.payout), + new BigNumber(0) + ) || new BigNumber(0); + + const getCalls = () => { + if (!api) return []; + + const calls: AnyApi[] = []; + payouts?.forEach(({ era, validators }) => { + if (!validators) return []; + + return validators.forEach((v) => + calls.push(api.tx.staking.payoutStakers(v, era)) + ); + }); + return calls; + }; + + // Store whether form is valid to submit transaction. + const [valid, setValid] = useState<boolean>( + totalPayout.isGreaterThan(0) && getCalls().length > 0 + ); + + // Ensure payouts value is valid. + useEffect( + () => setValid(totalPayout.isGreaterThan(0) && getCalls().length > 0), + [payouts] + ); + + const getTx = () => { + const tx = null; + const calls = getCalls(); + if (!valid || !api || !calls.length) return tx; + + return calls.length === 1 + ? calls.pop() + : newBatchCall(calls, activeAccount); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: valid, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => { + // Remove Subscan unclaimed payout record(s) if they exists. + let newUnclaimedPayoutsSubscan = unclaimedPayoutsSubscan; + + payouts?.forEach(({ era, validators }) => { + validators?.forEach((validator) => { + newUnclaimedPayoutsSubscan = newUnclaimedPayoutsSubscan.filter( + (u: AnySubscan) => + !(u.validator_stash === validator && String(u.era) === era) + ); + }); + }); + setUnclaimedPayouts(newUnclaimedPayoutsSubscan); + + // Deduct from `unclaimedPayouts` in Payouts context. + payouts?.forEach(({ era, validators }) => { + for (const v of validators || []) { + removeEraPayout(era, v); + } + }); + + // Reset active form payouts for this modal. + setPayouts([]); + }, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + return ( + <ContentWrapper> + <div ref={ref}> + <div className="padding"> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <div style={{ marginBottom: '2rem' }}> + <ActionItem + text={`${t('claim')} ${planckToUnit( + totalPayout, + units + )} ${unit}`} + /> + <p>{t('afterClaiming')}</p> + </div> + </div> + <SubmitTx + fromController={false} + valid={valid} + buttons={[ + <ButtonSubmitInvert + key="button_back" + text={t('back')} + iconLeft={faChevronLeft} + iconTransform="shrink-1" + onClick={() => setSection(0)} + />, + ]} + {...submitExtrinsic} + /> + </div> + </ContentWrapper> + ); + } +); diff --git a/src/modals/ClaimPayouts/Item.tsx b/src/modals/ClaimPayouts/Item.tsx new file mode 100644 index 0000000000..1bfdae8e99 --- /dev/null +++ b/src/modals/ClaimPayouts/Item.tsx @@ -0,0 +1,67 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonSubmit } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import BigNumber from 'bignumber.js'; +import { planckToUnit } from '@polkadot-cloud/utils'; +import { useNetwork } from 'contexts/Network'; +import { ItemWrapper } from './Wrappers'; +import type { ItemProps } from './types'; + +export const Item = ({ + era, + unclaimedPayout, + setPayouts, + setSection, +}: ItemProps) => { + const { t } = useTranslation('modals'); + const { + networkData: { units, unit }, + } = useNetwork(); + + const totalPayout = Object.values(unclaimedPayout).reduce( + (acc: BigNumber, cur: string) => acc.plus(cur), + new BigNumber(0) + ); + + const numPayouts = Object.values(unclaimedPayout).length; + + return ( + <ItemWrapper> + <div> + <section> + <h4> + <span> + Era {era}: {numPayouts} + {t('pendingPayout', { + count: numPayouts, + })} + </span> + </h4> + <h2> + {planckToUnit(totalPayout, units).toString()} {unit} + </h2> + </section> + + <section> + <div> + <ButtonSubmit + text={t('claim')} + onClick={() => { + setPayouts([ + { + era, + payout: totalPayout.toString(), + validators: Object.keys(unclaimedPayout), + }, + ]); + setSection(1); + }} + /> + </div> + </section> + </div> + </ItemWrapper> + ); +}; diff --git a/src/modals/ClaimPayouts/Overview.tsx b/src/modals/ClaimPayouts/Overview.tsx new file mode 100644 index 0000000000..f1da7cc7e4 --- /dev/null +++ b/src/modals/ClaimPayouts/Overview.tsx @@ -0,0 +1,39 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalNotes } from '@polkadot-cloud/react'; +import { forwardRef } from 'react'; +import { usePayouts } from 'contexts/Payouts'; +import { useTranslation } from 'react-i18next'; +import { Item } from './Item'; +import { ContentWrapper } from './Wrappers'; +import type { OverviewProps } from './types'; + +export const Overview = forwardRef( + ({ setSection, setPayouts }: OverviewProps, ref: any) => { + const { t } = useTranslation('modals'); + const { unclaimedPayouts } = usePayouts(); + + return ( + <ContentWrapper> + <div className="padding" ref={ref}> + {Object.entries(unclaimedPayouts || {}).map( + ([era, unclaimedPayout]: any, i: number) => ( + <Item + key={`unclaimed_payout_${i}`} + era={era} + unclaimedPayout={unclaimedPayout} + setPayouts={setPayouts} + setSection={setSection} + /> + ) + )} + <ModalNotes withPadding> + <p>{t('claimsOnBehalf')}</p> + <p>{t('notToClaim')}</p> + </ModalNotes> + </div> + </ContentWrapper> + ); + } +); diff --git a/src/modals/ClaimPayouts/Wrappers.ts b/src/modals/ClaimPayouts/Wrappers.ts new file mode 100644 index 0000000000..a449d8d90c --- /dev/null +++ b/src/modals/ClaimPayouts/Wrappers.ts @@ -0,0 +1,61 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const ContentWrapper = styled.div` + border-radius: 1rem; + display: flex; + flex-flow: column nowrap; + flex-basis: 50%; + flex-grow: 0; + flex-shrink: 1; + height: fit-content; + + .padding { + padding: 0 1rem; + } + + > div:last-child { + margin-bottom: 0; + } +`; + +export const ItemWrapper = styled.div<any>` + flex: 1; + display: flex; + flex-flow: column wrap; + margin-top: 1.25rem; + + > div { + background: var(--button-primary-background); + display: flex; + flex-flow: row wrap; + width: 100%; + padding: 0.5rem 1rem; + border-radius: 1rem; + + > section { + display: flex; + flex-flow: column wrap; + justify-content: flex-end; + padding: 0.75rem 0; + + &:first-child { + flex-grow: 1; + } + &:last-child { + justify-content: center; + } + } + } + + h2 { + margin: 0.75rem 0 0 0; + } + + h4 { + color: var(--text-color-secondary); + margin: 0; + } +`; diff --git a/src/modals/ClaimPayouts/index.tsx b/src/modals/ClaimPayouts/index.tsx new file mode 100644 index 0000000000..9dc9048520 --- /dev/null +++ b/src/modals/ClaimPayouts/index.tsx @@ -0,0 +1,102 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + ModalFixedTitle, + ModalMotionTwoSection, + ModalSection, +} from '@polkadot-cloud/react'; +import { setStateWithRef } from '@polkadot-cloud/utils'; +import { useEffect, useRef, useState } from 'react'; +import { Title } from 'library/Modal/Title'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { usePayouts } from 'contexts/Payouts'; +import { useTranslation } from 'react-i18next'; +import { Forms } from './Forms'; +import { Overview } from './Overview'; +import type { ActivePayout } from './types'; + +export const ClaimPayouts = () => { + const { t } = useTranslation('modals'); + const { notEnoughFunds } = useTxMeta(); + const { unclaimedPayouts } = usePayouts(); + const { setModalHeight } = useOverlay().modal; + + // Active modal section. + const [section, setSectionState] = useState(0); + const sectionRef = useRef(section); + + const setSection = (s: number) => { + setStateWithRef(s, setSectionState, sectionRef); + }; + + // Unclaimed payout(s) that will be applied to submission form. + const [payouts, setPayouts] = useState<ActivePayout[] | null>(null); + + const headerRef = useRef<HTMLDivElement>(null); + const overviewRef = useRef<HTMLDivElement>(null); + const formsRef = useRef<HTMLDivElement>(null); + + const getModalHeight = () => { + let h = headerRef.current?.clientHeight ?? 0; + if (sectionRef.current === 0) { + h += overviewRef.current?.clientHeight ?? 0; + } else { + h += formsRef.current?.clientHeight ?? 0; + } + return h; + }; + + // Resize modal on state change. + useEffect(() => { + setModalHeight(getModalHeight()); + }, [unclaimedPayouts, notEnoughFunds, section]); + + // Resize this modal on window resize. + useEffect(() => { + window.addEventListener('resize', resizeCallback); + return () => { + window.removeEventListener('resize', resizeCallback); + }; + }, []); + const resizeCallback = () => { + setModalHeight(getModalHeight()); + }; + + return ( + <ModalSection type="carousel"> + <ModalFixedTitle ref={headerRef}> + <Title title={t('claimPayouts')} fixed /> + </ModalFixedTitle> + <ModalMotionTwoSection + animate={sectionRef.current === 0 ? 'home' : 'next'} + transition={{ + duration: 0.5, + type: 'spring', + bounce: 0.1, + }} + variants={{ + home: { + left: 0, + }, + next: { + left: '-100%', + }, + }} + > + <Overview + setSection={setSection} + setPayouts={setPayouts} + ref={overviewRef} + /> + <Forms + ref={formsRef} + payouts={payouts} + setPayouts={setPayouts} + setSection={setSection} + /> + </ModalMotionTwoSection> + </ModalSection> + ); +}; diff --git a/src/modals/ClaimPayouts/types.ts b/src/modals/ClaimPayouts/types.ts new file mode 100644 index 0000000000..e31995da68 --- /dev/null +++ b/src/modals/ClaimPayouts/types.ts @@ -0,0 +1,28 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { EraUnclaimedPayouts } from 'contexts/Payouts/types'; + +export interface ItemProps { + era: string; + unclaimedPayout: EraUnclaimedPayouts; + setSection: (v: number) => void; + setPayouts: (payout: ActivePayout[] | null) => void; +} + +export interface ActivePayout { + era: string; + payout: string; + validators: string[]; +} + +export interface OverviewProps { + setSection: (s: number) => void; + setPayouts: (p: ActivePayout[] | null) => void; +} + +export interface FormProps { + setSection: (s: number) => void; + payouts: ActivePayout[] | null; + setPayouts: (p: ActivePayout[] | null) => void; +} diff --git a/src/modals/ClaimReward/index.tsx b/src/modals/ClaimReward/index.tsx new file mode 100644 index 0000000000..9b3edd12e6 --- /dev/null +++ b/src/modals/ClaimReward/index.tsx @@ -0,0 +1,119 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ActionItem, ModalPadding, ModalWarnings } from '@polkadot-cloud/react'; +import { greaterThanZero, planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const ClaimReward = () => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { notEnoughFunds } = useTxMeta(); + const { selectedActivePool } = useActivePools(); + const { getSignerWarnings } = useSignerWarnings(); + const { + setModalStatus, + config: { options }, + setModalResize, + } = useOverlay().modal; + + let { pendingRewards } = selectedActivePool || {}; + pendingRewards = pendingRewards ?? new BigNumber(0); + const { claimType } = options; + + // ensure selected payout is valid + useEffect(() => { + if (pendingRewards?.isGreaterThan(0)) { + setValid(true); + } else { + setValid(false); + } + }, [selectedActivePool]); + + // valid to submit transaction + const [valid, setValid] = useState<boolean>(false); + + // tx to submit + const getTx = () => { + let tx = null; + if (!api) { + return tx; + } + + if (claimType === 'bond') { + tx = api.tx.nominationPools.bondExtra('Rewards'); + } else { + tx = api.tx.nominationPools.claimPayout(); + } + return tx; + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: valid, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + if (!greaterThanZero(pendingRewards)) { + warnings.push(`${t('noRewards')}`); + } + + useEffect(() => setModalResize(), [notEnoughFunds, warnings.length]); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded"> + {claimType === 'bond' ? t('compound') : t('withdraw')} {t('rewards')} + </h2> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <ActionItem + text={`${t('claim')} ${`${planckToUnit( + pendingRewards, + units + )} ${unit}`}`} + /> + {claimType === 'bond' ? ( + <p>{t('claimReward1')}</p> + ) : ( + <p>{t('claimReward2')}</p> + )} + </ModalPadding> + <SubmitTx valid={valid} {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/modals/Connect/Extension.tsx b/src/modals/Connect/Extension.tsx new file mode 100644 index 0000000000..9f283a53eb --- /dev/null +++ b/src/modals/Connect/Extension.tsx @@ -0,0 +1,110 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faExternalLinkAlt, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ModalConnectItem } from '@polkadot-cloud/react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useExtensions, + useExtensionAccounts, +} from '@polkadot-cloud/react/hooks'; +import { useNotifications } from 'contexts/Notifications'; +import { ExtensionIcons } from '@polkadot-cloud/assets/extensions'; +import { ExtensionInner } from './Wrappers'; +import type { ExtensionProps } from './types'; + +export const Extension = ({ meta, size, flag }: ExtensionProps) => { + const { t } = useTranslation('modals'); + const { addNotification } = useNotifications(); + const { connectExtensionAccounts } = useExtensionAccounts(); + const { extensionsStatus, extensionInstalled, extensionCanConnect } = + useExtensions(); + const { title, website, id } = meta; + const isInstalled = extensionInstalled(id); + const canConnect = extensionCanConnect(id); + + // Force re-render on click. + const [increment, setIncrement] = useState(0); + + // 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) + addNotification({ + title: t('extensionConnected'), + subtitle: `${t('titleExtensionConnected', { title })}`, + }); + } + }; + + const Icon = ExtensionIcons[id || ''] || undefined; + // determine message to be displayed based on extension status. + let statusJsx; + switch (extensionsStatus[id]) { + case 'connected': + statusJsx = <p className="success">{t('connected')}</p>; + break; + case 'not_authenticated': + statusJsx = <p>{t('notAuthenticated')}</p>; + break; + default: + statusJsx = ( + <p className="active"> + <FontAwesomeIcon icon={faPlus} className="plus" /> + {t('connect')} + </p> + ); + } + + const shortUrl = Array.isArray(website) ? website[0] : website; + const longUrl = Array.isArray(website) ? website[1] : website; + const disabled = extensionsStatus[id] === 'connected' || !isInstalled; + + return ( + <ModalConnectItem canConnect={canConnect}> + <ExtensionInner> + <div> + <div className="body"> + {!disabled ? ( + <button + type="button" + className="button" + onClick={() => handleClick()} + > +   + </button> + ) : null} + + <div className="row icon"> + <Icon style={{ width: size, height: size }} /> + </div> + <div className="status"> + {flag && flag} + {isInstalled ? statusJsx : <p>{t('notInstalled')}</p>} + </div> + <div className="row"> + <h3>{title}</h3> + </div> + </div> + <div className="foot"> + <a + className="link" + href={`https://${longUrl}`} + target="_blank" + rel="noreferrer" + > + {shortUrl} + <FontAwesomeIcon icon={faExternalLinkAlt} transform="shrink-6" /> + </a> + </div> + </div> + </ExtensionInner> + </ModalConnectItem> + ); +}; diff --git a/src/modals/Connect/Ledger.tsx b/src/modals/Connect/Ledger.tsx new file mode 100644 index 0000000000..0129bb3e22 --- /dev/null +++ b/src/modals/Connect/Ledger.tsx @@ -0,0 +1,86 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChrome, faUsb } from '@fortawesome/free-brands-svg-icons'; +import { + faExclamationTriangle, + faExternalLinkAlt, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + ButtonHelp, + ButtonPrimaryInvert, + ButtonText, + ModalConnectItem, + ModalHardwareItem, +} from '@polkadot-cloud/react'; +import { inChrome } from '@polkadot-cloud/utils'; +import React from 'react'; +import { useHelp } from 'contexts/Help'; +import LedgerLogoSVG from 'img/ledgerLogo.svg?react'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; + +export const Ledger = (): React.ReactElement => { + const { openHelp } = useHelp(); + const { replaceModal } = useOverlay().modal; + const { network } = useNetwork(); + const url = 'ledger.com'; + + // Only render on Polkadot and Kusama networks. + if (!['polkadot', 'kusama'].includes(network)) { + return <></>; + } + + return ( + <ModalConnectItem> + <ModalHardwareItem> + <div className="body"> + <div className="status"> + <ButtonHelp onClick={() => openHelp('Ledger Hardware Wallets')} /> + </div> + <div className="row"> + <LedgerLogoSVG className="logo mono" /> + </div> + <div className="row margin"> + <ButtonText + text={network === 'polkadot' ? 'BETA' : 'EXPERIMENTAL'} + disabled + marginRight + iconLeft={ + network === 'polkadot' ? undefined : faExclamationTriangle + } + style={{ opacity: 0.5 }} + /> + <ButtonText + text="Chrome / Brave" + disabled + iconLeft={faChrome} + style={{ opacity: 0.5 }} + /> + </div> + <div className="row margin"> + <ButtonPrimaryInvert + text="USB" + onClick={() => replaceModal({ key: 'ImportLedger' })} + iconLeft={faUsb} + iconTransform="shrink-1" + disabled={!inChrome()} + /> + </div> + </div> + <div className="foot"> + <a + className="link" + href={`https://${url}`} + target="_blank" + rel="noreferrer" + > + {url} + <FontAwesomeIcon icon={faExternalLinkAlt} transform="shrink-6" /> + </a> + </div> + </ModalHardwareItem> + </ModalConnectItem> + ); +}; diff --git a/src/modals/Connect/Proxies.tsx b/src/modals/Connect/Proxies.tsx new file mode 100644 index 0000000000..fad0651e5e --- /dev/null +++ b/src/modals/Connect/Proxies.tsx @@ -0,0 +1,116 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faChevronRight, + faMinus, + faPlus, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + ButtonHelp, + ButtonMonoInvert, + ButtonSecondary, + Polkicon, +} from '@polkadot-cloud/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { useProxies } from 'contexts/Proxies'; +import { AccountInput } from 'library/AccountInput'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { + ActionWithButton, + ManualAccount, + ManualAccountsWrapper, +} from './Wrappers'; +import type { ListWithInputProps } from './types'; + +export const Proxies = ({ setInputOpen, inputOpen }: ListWithInputProps) => { + const { t } = useTranslation('modals'); + const { openHelp } = useHelp(); + const { accounts } = useImportedAccounts(); + const { getAccount } = useImportedAccounts(); + const { delegates, handleDeclareDelegate } = useProxies(); + + // Filter delegates to only show those who are imported in the dashboard. + const importedDelegates = Object.fromEntries( + Object.entries(delegates).filter(([delegate]) => + accounts.find((a) => a.address === delegate) + ) + ); + return ( + <> + <ActionWithButton> + <div> + <FontAwesomeIcon icon={faChevronRight} transform="shrink-4" /> + <h3>{t('proxyAccounts')}</h3> + <ButtonHelp marginLeft onClick={() => openHelp('Proxy Accounts')} /> + </div> + <div> + <ButtonMonoInvert + iconLeft={inputOpen ? faMinus : faPlus} + text={!inputOpen ? t('declare') : t('hide')} + onClick={() => { + setInputOpen(!inputOpen); + }} + /> + </div> + </ActionWithButton> + <ManualAccountsWrapper> + <div className="content"> + {inputOpen && ( + <> + <AccountInput + resetOnSuccess + defaultLabel={t('inputDelegatorAddress')} + successCallback={async (delegator) => { + const result = await handleDeclareDelegate(delegator); + return result; + }} + /> + </> + )} + {Object.entries(importedDelegates).length ? ( + <div className="accounts"> + {Object.entries(importedDelegates).map( + ([delegate, delegators], i) => ( + <React.Fragment key={`user_delegate_account_${i}}`}> + {delegators.map(({ delegator, proxyType }, j) => ( + <ManualAccount key={`user_delegate_${i}_delegator_${j}`}> + <div> + <span> + <Polkicon address={delegate} size={26} /> + </span> + <div className="text"> + <h4 className="title"> + <span> + {proxyType} {t('proxy')} + </span> + {getAccount(delegate)?.name || delegate} + </h4> + <h4 className="subtitle"> + {t('for', { + who: getAccount(delegator)?.name || delegator, + })} + </h4> + </div> + </div> + <div /> + <ButtonSecondary text={t('declared')} disabled /> + </ManualAccount> + ))} + </React.Fragment> + ) + )} + </div> + ) : ( + <div style={{ padding: '0.5rem' }}> + <h4>{t('noProxyAccountsDeclared')}</h4> + </div> + )} + </div> + </ManualAccountsWrapper> + </> + ); +}; diff --git a/src/modals/Connect/ReadOnly.tsx b/src/modals/Connect/ReadOnly.tsx new file mode 100644 index 0000000000..ba916f7504 --- /dev/null +++ b/src/modals/Connect/ReadOnly.tsx @@ -0,0 +1,115 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faChevronRight, + faMinus, + faPlus, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + ButtonHelp, + ButtonMonoInvert, + ButtonSecondary, + Polkicon, +} from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { AccountInput } from 'library/AccountInput'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; +import type { ExternalAccount } from '@polkadot-cloud/react/types'; +import { + ActionWithButton, + ManualAccount, + ManualAccountsWrapper, +} from './Wrappers'; +import type { ListWithInputProps } from './types'; + +export const ReadOnly = ({ setInputOpen, inputOpen }: ListWithInputProps) => { + const { t } = useTranslation('modals'); + const { openHelp } = useHelp(); + const { accounts } = useImportedAccounts(); + const { setModalResize } = useOverlay().modal; + const { forgetExternalAccounts, addExternalAccount } = useOtherAccounts(); + + // get all external accounts + const externalAccountsOnly = accounts.filter( + ({ source }) => source === 'external' + ) as ExternalAccount[]; + + // get external accounts added by user + const externalAccounts = externalAccountsOnly.filter( + ({ addedBy }) => addedBy === 'user' + ); + + // forget account + const forgetAccount = (account: ExternalAccount) => { + forgetExternalAccounts([account]); + setModalResize(); + }; + return ( + <> + <ActionWithButton> + <div> + <FontAwesomeIcon icon={faChevronRight} transform="shrink-4" /> + <h3>{t('readOnlyAccounts')}</h3> + <ButtonHelp + marginLeft + onClick={() => openHelp('Read Only Accounts')} + /> + </div> + <div> + <ButtonMonoInvert + iconLeft={inputOpen ? faMinus : faPlus} + text={!inputOpen ? t('add') : t('hide')} + onClick={() => { + setInputOpen(!inputOpen); + }} + /> + </div> + </ActionWithButton> + <ManualAccountsWrapper> + <div className="content"> + {inputOpen && ( + <AccountInput + resetOnSuccess + defaultLabel={t('inputAddress')} + successCallback={async (value: string) => { + addExternalAccount(value, 'user'); + return true; + }} + /> + )} + {externalAccounts.length ? ( + <div className="accounts"> + {externalAccounts.map((a, i) => ( + <ManualAccount key={`user_external_account_${i}`}> + <div> + <span> + <Polkicon address={a.address} size={26} /> + </span> + <div className="text"> + <h4>{a.address}</h4> + </div> + </div> + <ButtonSecondary + text={t('forget')} + onClick={() => { + forgetAccount(a); + }} + /> + </ManualAccount> + ))} + </div> + ) : ( + <div style={{ padding: '0.5rem' }}> + <h4>{t('noReadOnlyAdded')}</h4> + </div> + )} + </div> + </ManualAccountsWrapper> + </> + ); +}; diff --git a/src/modals/Connect/Vault.tsx b/src/modals/Connect/Vault.tsx new file mode 100644 index 0000000000..b4602ffc57 --- /dev/null +++ b/src/modals/Connect/Vault.tsx @@ -0,0 +1,72 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faExternalLinkAlt, faQrcode } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + ButtonHelp, + ButtonPrimaryInvert, + ButtonText, + ModalConnectItem, + ModalHardwareItem, +} from '@polkadot-cloud/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import PolkadotVaultSVG from '@polkadot-cloud/assets/extensions/svg/polkadotvault.svg?react'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; + +export const Vault = (): React.ReactElement => { + const { t } = useTranslation('modals'); + const { openHelp } = useHelp(); + const { replaceModal } = useOverlay().modal; + const url = 'signer.parity.io'; + + return ( + <ModalConnectItem> + <ModalHardwareItem> + <div className="body"> + <div className="status"> + <ButtonHelp onClick={() => openHelp('Polkadot Vault')} /> + </div> + <div className="row"> + <PolkadotVaultSVG className="logo vault" /> + </div> + <div className="row margin"> + <ButtonText + text="Polkadot Vault" + disabled + marginRight + style={{ + opacity: 1, + color: 'var(--accent-color-primary)', + fontFamily: 'Unbounded', + }} + /> + </div> + <div className="row margin"> + <ButtonPrimaryInvert + text={t('import')} + onClick={() => { + replaceModal({ key: 'ImportVault' }); + }} + iconLeft={faQrcode} + iconTransform="shrink-1" + /> + </div> + </div> + <div className="foot"> + <a + className="link" + href={`https://${url}`} + target="_blank" + rel="noreferrer" + > + {url} + <FontAwesomeIcon icon={faExternalLinkAlt} transform="shrink-6" /> + </a> + </div> + </ModalHardwareItem> + </ModalConnectItem> + ); +}; diff --git a/src/modals/Connect/Wrappers.ts b/src/modals/Connect/Wrappers.ts new file mode 100644 index 0000000000..8819a85c91 --- /dev/null +++ b/src/modals/Connect/Wrappers.ts @@ -0,0 +1,218 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { TwoThreshold } from 'library/SelectItems/Wrapper'; + +// Wraps a list of extensions. `SelectItems` typically follows this wrapper, with the items embedded +// within it. +export const ExtensionsWrapper = styled.div` + width: 100%; + padding: 0 0.4rem; + margin: 0.5rem 0 1rem 0; + + @media (max-width: ${TwoThreshold}px) { + padding: 0; + } +`; + +// Styling for an extension item, which can reflect the status of the extension connection. +export const ExtensionInner = styled.div` + background: var(--button-primary-background); + width: 100%; + border-radius: 1rem; + overflow: hidden; + position: relative; + + h3 { + font-family: InterSemiBold, sans-serif; + margin: 1rem 0 0 0; + > svg { + margin-right: 0.5rem; + } + } + p { + color: var(--text-color-secondary); + padding: 0; + margin: 0; + .plus { + margin-right: 0.4rem; + } + } + .body { + width: 100%; + padding: 1.35rem 0.85rem 0.75rem 0.85rem; + position: relative; + + .button { + z-index: 1; + position: absolute; + background: none; + top: 0; + left: 0; + width: 100%; + height: 100%; + &:disabled { + cursor: default; + } + } + } + .row { + width: 100%; + display: flex; + } + .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); + } + } + .icon { + color: var(--text-color-primary); + width: 100%; + + svg { + max-width: 2.6rem; + max-height: 2.6rem; + } + } + svg { + .light { + fill: var(--text-color-invert); + } + .dark { + fill: var(--text-color-secondary); + } + } +`; + +// Styling for a separator between ExtensionItems. +export const Separator = styled.div` + width: 100%; + height: 0.25rem; +`; + +export const ActionWithButton = styled.div` + border-bottom: 1px solid var(--border-primary-color); + width: 100%; + color: var(--text-color-primary); + display: flex; + align-items: center; + margin: 1.25rem 0 0; + padding-bottom: 0.75rem; + + > div { + &:first-child { + display: flex; + align-items: center; + flex-grow: 1; + font-family: InterSemiBold, sans-serif; + > svg { + margin-right: 0.5rem; + } + } + &:last-child { + font-family: InterSemiBold, sans-serif; + } + } +`; + +export const ManualAccountsWrapper = styled.div` + color: var(--text-color-primary); + width: 100%; + display: flex; + flex-flow: column nowrap; + + h3 { + display: flex; + align-items: center; + > span { + margin-left: 1rem; + } + } + h4 { + margin: 0.25rem 0 0 0; + } + + > .content { + width: 100%; + > h5 { + margin-top: 1rem; + } + } + + .accounts { + margin-top: 1rem; + width: 100%; + } +`; + +export const ManualAccount = styled.div` + background: var(--button-primary-background); + width: 100%; + border-radius: 1rem; + margin-bottom: 1rem; + padding: 1rem; + display: flex; + flex-flow: row wrap; + align-items: center; + transition: border 0.1s; + + > div { + color: var(--text-color-secondary); + transition: opacity var(--transition-duration); + + &:first-child { + flex: 1; + display: flex; + align-items: center; + + > span { + margin-right: 0.75rem; + } + + > .text { + display: flex; + flex-direction: column; + h4 { + margin: 0; + &.title { + font-family: InterSemiBold, sans-serif; + > svg { + margin: 0 0.6rem; + } + } + &.subtitle { + margin-top: 0.4rem; + } + + &.title > span, + &.subtitle > span { + color: var(--text-color-secondary); + opacity: 0.65; + margin-right: 0.65rem; + } + .arrow { + margin: 0 0.25rem; + } + } + } + } + &:last-child { + padding-left: 2rem; + opacity: 0.25; + } + } + + button { + font-size: 1rem; + } +`; diff --git a/src/modals/Connect/index.tsx b/src/modals/Connect/index.tsx new file mode 100644 index 0000000000..cfe0379b95 --- /dev/null +++ b/src/modals/Connect/index.tsx @@ -0,0 +1,194 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import { + ActionItem, + ButtonPrimaryInvert, + ButtonTab, + ModalCustomHeader, + ModalFixedTitle, + ModalMotionThreeSection, + ModalPadding, + ModalSection, +} from '@polkadot-cloud/react'; +import { ExtensionsArray } from '@polkadot-cloud/assets/extensions'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + useExtensions, + useEffectIgnoreInitial, + useOverlay, +} from '@polkadot-cloud/react/hooks'; +import { Close } from 'library/Modal/Close'; +import { SelectItems } from 'library/SelectItems'; +import type { AnyFunction } from 'types'; +import { Extension } from './Extension'; +import { Ledger } from './Ledger'; +import { Proxies } from './Proxies'; +import { ReadOnly } from './ReadOnly'; +import { Vault } from './Vault'; +import { ExtensionsWrapper } from './Wrappers'; + +export const Connect = () => { + const { t } = useTranslation('modals'); + const { extensionsStatus } = useExtensions(); + const { replaceModal, setModalHeight, modalMaxHeight } = useOverlay().modal; + + const web = ExtensionsArray.filter((a) => a.id !== 'polkadot-js'); + const pjs = ExtensionsArray.filter((a) => a.id === 'polkadot-js'); + + const installed = web.filter((a) => + Object.keys(extensionsStatus).find((key) => key === a.id) + ); + const other = web.filter((a) => !installed.find((b) => b.id === a.id)); + + // toggle read only management + const [readOnlyOpen, setReadOnlyOpen] = useState(false); + + // toggle proxy delegate management + const [newProxyOpen, setNewProxyOpen] = useState(false); + + // active modal section + const [section, setSection] = useState<number>(0); + + // refs for wrappers + const headerRef = useRef<HTMLDivElement>(null); + const homeRef = useRef<HTMLDivElement>(null); + const readOnlyRef = useRef<HTMLDivElement>(null); + const proxiesRef = useRef<HTMLDivElement>(null); + + const refreshModalHeight = () => { + // Preserve height by taking largest height from modals. + let height = headerRef.current?.clientHeight || 0; + height += Math.max( + homeRef.current?.clientHeight || 0, + readOnlyRef.current?.clientHeight || 0, + proxiesRef.current?.clientHeight || 0 + ); + setModalHeight(height); + }; + + // Resize modal on state change. + useEffectIgnoreInitial(() => { + refreshModalHeight(); + }, [section, readOnlyOpen, newProxyOpen, extensionsStatus]); + + useEffect(() => { + window.addEventListener('resize', refreshModalHeight); + return () => { + window.removeEventListener('resize', refreshModalHeight); + }; + }, []); + + return ( + <> + <ModalSection type="carousel"> + <Close /> + <ModalFixedTitle ref={headerRef} withStyle> + <ModalCustomHeader> + <div className="first"> + <h1>{t('connect')}</h1> + <ButtonPrimaryInvert + text={t('goToAccounts')} + iconRight={faChevronRight} + iconTransform="shrink-3" + onClick={() => replaceModal({ key: 'Accounts' })} + marginLeft + /> + </div> + <ModalSection type="tab"> + <ButtonTab + title={t('extensions')} + onClick={() => setSection(0)} + active={section === 0} + /> + <ButtonTab + title={t('readOnly')} + onClick={() => setSection(1)} + active={section === 1} + /> + <ButtonTab + title={t('proxies')} + onClick={() => setSection(2)} + active={section === 2} + /> + </ModalSection> + </ModalCustomHeader> + </ModalFixedTitle> + + <ModalMotionThreeSection + style={{ + maxHeight: modalMaxHeight - (headerRef.current?.clientHeight || 0), + }} + animate={ + section === 0 ? 'home' : section === 1 ? 'readOnly' : 'proxies' + } + transition={{ + duration: 0.5, + type: 'spring', + bounce: 0.1, + }} + variants={{ + home: { + left: 0, + }, + readOnly: { + left: '-100%', + }, + proxies: { + left: '-200%', + }, + }} + > + <div className="section"> + <ModalPadding horizontalOnly ref={homeRef}> + <ActionItem text={t('hardware')} /> + <ExtensionsWrapper> + <SelectItems layout="two-col"> + {[Vault, Ledger].map((Item: AnyFunction, i: number) => ( + <Item key={`hardware_item_${i}`} /> + ))} + </SelectItems> + </ExtensionsWrapper> + + <ActionItem text={t('web')} /> + <ExtensionsWrapper> + <SelectItems layout="two-col"> + {installed.concat(other).map((extension, i) => ( + <Extension key={`extension_item_${i}`} meta={extension} /> + ))} + </SelectItems> + </ExtensionsWrapper> + + <ActionItem text={t('developerTools')} /> + <ExtensionsWrapper> + <SelectItems layout="two-col"> + {pjs.map((extension, i) => ( + <Extension key={`extension_item_${i}`} meta={extension} /> + ))} + </SelectItems> + </ExtensionsWrapper> + </ModalPadding> + </div> + <div className="section"> + <ModalPadding horizontalOnly ref={readOnlyRef}> + <ReadOnly + setInputOpen={setReadOnlyOpen} + inputOpen={readOnlyOpen} + /> + </ModalPadding> + </div> + <div className="section"> + <ModalPadding horizontalOnly ref={proxiesRef}> + <Proxies + setInputOpen={setNewProxyOpen} + inputOpen={newProxyOpen} + /> + </ModalPadding> + </div> + </ModalMotionThreeSection> + </ModalSection> + </> + ); +}; diff --git a/src/modals/Connect/types.ts b/src/modals/Connect/types.ts new file mode 100644 index 0000000000..cec867e696 --- /dev/null +++ b/src/modals/Connect/types.ts @@ -0,0 +1,29 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface ExtensionProps { + meta: ExtensionMetaProps; + installed?: any; + size?: string; + message?: string; + flag?: boolean; + status?: string; +} + +export interface ExtensionMetaProps { + id: string; + title: string; + status?: string; + website: string | [string, string]; +} + +export interface ListWithInputProps { + setInputOpen: (k: boolean) => void; + inputOpen: boolean; +} + +export interface forwardRefProps { + setSection?: any; + readOnlyOpen: boolean; + setReadOnlyOpen: (e: boolean) => void; +} diff --git a/src/modals/DismissTips/index.tsx b/src/modals/DismissTips/index.tsx new file mode 100644 index 0000000000..0a56dfb6c5 --- /dev/null +++ b/src/modals/DismissTips/index.tsx @@ -0,0 +1,43 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonSubmit, ModalPadding } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { usePlugins } from 'contexts/Plugins'; +import { Title } from 'library/Modal/Title'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; + +export const DismissTips = () => { + const { t } = useTranslation('tips'); + const { togglePlugin } = usePlugins(); + const { setModalStatus } = useOverlay().modal; + + return ( + <> + <Title title={t('module.dismissTips')} /> + <ModalPadding horizontalOnly> + <div + style={{ + padding: '0 0.5rem 1.25rem 0.5rem', + width: '100%', + }} + > + <div> + <h4>{t('module.dismissResult')}</h4> + <h4>{t('module.reEnable')}</h4> + </div> + <div className="buttons"> + <ButtonSubmit + marginRight + text={t('module.disableTips')} + onClick={() => { + togglePlugin('tips'); + setModalStatus('closing'); + }} + /> + </div> + </div> + </ModalPadding> + </> + ); +}; diff --git a/src/modals/GoToFeedback/index.tsx b/src/modals/GoToFeedback/index.tsx new file mode 100644 index 0000000000..aeb36b2197 --- /dev/null +++ b/src/modals/GoToFeedback/index.tsx @@ -0,0 +1,45 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ModalPadding } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import ForumSVG from 'img/forum.svg?react'; +import { Title } from 'library/Modal/Title'; + +export const GoToFeedback = () => { + const { t } = useTranslation('modals'); + return ( + <> + <Title title={t('feedback')} Svg={ForumSVG} /> + <ModalPadding verticalOnly> + <div + style={{ + padding: '0 1.75rem 0.5rem 1.75rem', + width: '100%', + }} + > + <h4 style={{ paddingBottom: '0.75rem' }}> + {t('feedbackPage')}{' '} + <a href="https://canny.io/" target="_blank" rel="noreferrer"> + Canny.io + </a> + . {t('welcomeToReport')} + </h4> + <h2 style={{ marginTop: '0.75rem' }}> + <a + href="https://polkadot-staking-dashboard.canny.io/feedback" + target="_blank" + rel="noreferrer" + style={{ color: 'var(--accent-color-primary' }} + > + {t('openFeedback')}   + <FontAwesomeIcon icon={faExternalLinkAlt} transform="shrink-3" /> + </a> + </h2> + </div> + </ModalPadding> + </> + ); +}; diff --git a/src/modals/ImportLedger/Addresses.tsx b/src/modals/ImportLedger/Addresses.tsx new file mode 100644 index 0000000000..c7ede6ba6b --- /dev/null +++ b/src/modals/ImportLedger/Addresses.tsx @@ -0,0 +1,110 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faArrowDown } from '@fortawesome/free-solid-svg-icons'; +import { ButtonText, HardwareAddress, Polkicon } from '@polkadot-cloud/react'; +import { ellipsisFn, unescape } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useLedgerHardware } from 'contexts/Hardware/Ledger'; +import { getLocalLedgerAddresses } from 'contexts/Hardware/Utils'; +import { usePrompt } from 'contexts/Prompt'; +import { Confirm } from 'library/Import/Confirm'; +import { Remove } from 'library/Import/Remove'; +import { AddressesWrapper } from 'library/Import/Wrappers'; +import type { AnyJson } from 'types'; +import { useNetwork } from 'contexts/Network'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; + +export const Addresess = ({ addresses, handleLedgerLoop }: AnyJson) => { + const { t } = useTranslation('modals'); + const { network } = useNetwork(); + + const { + getIsExecuting, + ledgerAccountExists, + renameLedgerAccount, + addLedgerAccount, + removeLedgerAccount, + setIsExecuting, + getLedgerAccount, + pairDevice, + } = useLedgerHardware(); + const isExecuting = getIsExecuting(); + const { openPromptWith } = usePrompt(); + const { renameOtherAccount } = useOtherAccounts(); + + const renameHandler = (address: string, newName: string) => { + renameLedgerAccount(address, newName); + renameOtherAccount(address, newName); + }; + + const openConfirmHandler = (address: string, index: number) => { + openPromptWith( + <Confirm address={address} index={index} addHandler={addLedgerAccount} />, + 'small' + ); + }; + + const openRemoveHandler = (address: string) => { + openPromptWith( + <Remove + address={address} + removeHandler={removeLedgerAccount} + getHandler={getLedgerAccount} + />, + 'small' + ); + }; + + return ( + <> + <AddressesWrapper> + <div className="items"> + {addresses.map(({ address, index }: AnyJson, i: number) => { + const initialName = (() => { + const localAddress = getLocalLedgerAddresses().find( + (a) => a.address === address && a.network === network + ); + return localAddress?.name + ? unescape(localAddress.name) + : ellipsisFn(address); + })(); + + return ( + <HardwareAddress + key={i} + address={address} + index={index} + initial={initialName} + Identicon={<Polkicon address={address} size={40} />} + existsHandler={ledgerAccountExists} + renameHandler={renameHandler} + openRemoveHandler={openRemoveHandler} + openConfirmHandler={openConfirmHandler} + t={{ + tRemove: t('remove'), + tImport: t('import'), + }} + /> + ); + })} + </div> + <div className="more"> + <ButtonText + iconLeft={faArrowDown} + text={isExecuting ? t('gettingAccount') : t('getAnotherAccount')} + disabled={isExecuting} + onClick={async () => { + // re-pair the device if it has been disconnected. + const paired = await pairDevice(); + if (paired) { + setIsExecuting(true); + handleLedgerLoop(); + } + }} + /> + </div> + </AddressesWrapper> + </> + ); +}; diff --git a/src/modals/ImportLedger/Manage.tsx b/src/modals/ImportLedger/Manage.tsx new file mode 100644 index 0000000000..7eef81fb0e --- /dev/null +++ b/src/modals/ImportLedger/Manage.tsx @@ -0,0 +1,87 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { HardwareStatusBar } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useLedgerHardware } from 'contexts/Hardware/Ledger'; +import { getLedgerApp } from 'contexts/Hardware/Utils'; +import { useHelp } from 'contexts/Help'; +import { usePrompt } from 'contexts/Prompt'; +import LedgerSVG from '@polkadot-cloud/assets/extensions/svg/ledger.svg?react'; +import { Heading } from 'library/Import/Heading'; +import type { AnyJson } from 'types'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { Addresess } from './Addresses'; +import { Reset } from './Reset'; + +export const Manage = ({ + addresses, + handleLedgerLoop, + removeLedgerAddress, +}: AnyJson) => { + const { t } = useTranslation(); + const { network } = useNetwork(); + const { setIsExecuting, getIsExecuting, resetStatusCodes, getFeedback } = + useLedgerHardware(); + const { openPromptWith } = usePrompt(); + const { replaceModal } = useOverlay().modal; + const { openHelp } = useHelp(); + + const { appName, Icon } = getLedgerApp(network); + const isExecuting = getIsExecuting(); + + const fallbackMessage = `${t('ledgerAccounts', { + ns: 'modals', + count: addresses.length, + })}`; + const feedback = getFeedback(); + const helpKey = feedback?.helpKey; + + return ( + <> + <Heading + connectTo="Ledger" + title={appName} + Icon={Icon} + disabled={!addresses.length} + handleReset={() => { + openPromptWith( + <Reset removeLedgerAddress={removeLedgerAddress} />, + 'small' + ); + }} + /> + <Addresess + addresses={addresses} + handleLedgerLoop={handleLedgerLoop} + removeLedgerAddress={removeLedgerAddress} + /> + <HardwareStatusBar + show + Icon={LedgerSVG} + text={feedback?.message || fallbackMessage} + help={ + helpKey + ? { + helpKey, + handleHelp: openHelp, + } + : undefined + } + inProgress={isExecuting} + handleCancel={() => { + setIsExecuting(false); + resetStatusCodes(); + }} + handleDone={() => + replaceModal({ key: 'Connect', options: { disableScroll: true } }) + } + t={{ + tDone: t('done', { ns: 'library' }), + tCancel: t('cancel', { ns: 'library' }), + }} + /> + </> + ); +}; diff --git a/src/modals/ImportLedger/Reset.tsx b/src/modals/ImportLedger/Reset.tsx new file mode 100644 index 0000000000..ba55b9fa87 --- /dev/null +++ b/src/modals/ImportLedger/Reset.tsx @@ -0,0 +1,55 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonMono, ButtonMonoInvert } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useLedgerHardware } from 'contexts/Hardware/Ledger'; +import { getLocalLedgerAddresses } from 'contexts/Hardware/Utils'; +import type { LedgerAddress } from 'contexts/Hardware/types'; +import { usePrompt } from 'contexts/Prompt'; +import { ConfirmWrapper } from 'library/Import/Wrappers'; +import type { AnyJson } from 'types'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; +import type { LedgerAccount } from '@polkadot-cloud/react/types'; + +export const Reset = ({ removeLedgerAddress }: AnyJson) => { + const { t } = useTranslation('modals'); + const { setStatus } = usePrompt(); + const { replaceModal } = useOverlay().modal; + const { forgetOtherAccounts } = useOtherAccounts(); + const { ledgerAccounts, removeLedgerAccount } = useLedgerHardware(); + + const removeAccounts = () => { + // Remove imported Ledger accounts. + ledgerAccounts.forEach((account: LedgerAccount) => { + removeLedgerAccount(account.address); + }); + forgetOtherAccounts(ledgerAccounts); + + // Remove local Ledger addresses. + getLocalLedgerAddresses().forEach((address: LedgerAddress) => { + removeLedgerAddress(address.address); + }); + + // Go back to Connect modal. + replaceModal({ key: 'Connect', options: { disableScroll: true } }); + }; + + return ( + <ConfirmWrapper> + <h3>{t('resetLedgerAccounts')}</h3> + <p>{t('ledgerWillBeReset')}</p> + <div className="footer"> + <ButtonMonoInvert text={t('cancel')} onClick={() => setStatus(0)} /> + <ButtonMono + text={t('confirmReset')} + onClick={() => { + removeAccounts(); + setStatus(0); + }} + /> + </div> + </ConfirmWrapper> + ); +}; diff --git a/src/modals/ImportLedger/Splash.tsx b/src/modals/ImportLedger/Splash.tsx new file mode 100644 index 0000000000..9edd8fc95f --- /dev/null +++ b/src/modals/ImportLedger/Splash.tsx @@ -0,0 +1,112 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { ButtonHelp, ButtonSecondary } from '@polkadot-cloud/react'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLedgerHardware } from 'contexts/Hardware/Ledger'; +import { useHelp } from 'contexts/Help'; +import { useTheme } from 'contexts/Themes'; +import LogoSVG from 'img/ledgerLogo.svg?react'; +import type { AnyFunction } from 'types'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { SplashWrapper } from './Wrappers'; + +export const Splash = ({ handleLedgerLoop }: AnyFunction) => { + const { t } = useTranslation('modals'); + const { + getStatusCodes, + isPaired, + getIsExecuting, + setIsExecuting, + pairDevice, + getFeedback, + } = useLedgerHardware(); + const { mode } = useTheme(); + const { openHelp } = useHelp(); + const { replaceModal, setModalResize } = useOverlay().modal; + + const statusCodes = getStatusCodes(); + + const initFetchAddress = async () => { + const paired = await pairDevice(); + if (paired) { + setIsExecuting(true); + handleLedgerLoop(); + } + }; + + const fallbackMessage = t('checking'); + const feedback = getFeedback(); + const helpKey = feedback?.helpKey; + + // Initialise listeners for Ledger IO. + useEffect(() => { + if (isPaired !== 'paired') { + pairDevice(); + } + }, []); + + // Once the device is paired, start `handleLedgerLoop`. + useEffect(() => { + initFetchAddress(); + }, [isPaired]); + + // Resize modal on new message + useEffect(() => setModalResize(), [statusCodes, feedback]); + + return ( + <> + <div style={{ display: 'flex', padding: '1rem' }}> + <h1> + <ButtonSecondary + text={t('back')} + iconLeft={faChevronLeft} + iconTransform="shrink-3" + onClick={async () => + replaceModal({ key: 'Connect', options: { disableScroll: true } }) + } + /> + </h1> + </div> + <SplashWrapper> + <div className="icon"> + <LogoSVG + style={{ transform: 'scale(0.6)' }} + opacity={mode === 'dark' ? 0.5 : 0.1} + /> + </div> + + <div className="content"> + <h2> + {feedback?.message || fallbackMessage} + {helpKey ? ( + <ButtonHelp + marginLeft + onClick={() => openHelp(helpKey)} + background="secondary" + /> + ) : null} + </h2> + + {!getIsExecuting() ? ( + <> + <h5>{t('ensureLedgerIsConnected')}</h5> + <div className="button"> + <ButtonSecondary + text={ + statusCodes[0]?.statusCode === 'DeviceNotConnected' + ? t('continue') + : t('tryAgain') + } + onClick={async () => initFetchAddress()} + /> + </div> + </> + ) : null} + </div> + </SplashWrapper> + </> + ); +}; diff --git a/src/modals/ImportLedger/Wrappers.ts b/src/modals/ImportLedger/Wrappers.ts new file mode 100644 index 0000000000..2b25b3e7c3 --- /dev/null +++ b/src/modals/ImportLedger/Wrappers.ts @@ -0,0 +1,99 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const SplashWrapper = styled.div` + width: 100%; + height: 100%; + display: flex; + flex-flow: column wrap; + align-items: center; + justify-content: center; + + .icon { + width: 100%; + display: flex; + justify-content: center; + z-index: 0; + margin-bottom: 2rem; + } + + .content { + z-index: 1; + display: flex; + flex-flow: column nowrap; + justify-content: center; + margin-bottom: 2rem; + + h2, + h5 { + color: var(--text-color-secondary); + display: flex; + align-items: center; + justify-content: center; + margin-top: 0.35rem; + } + + h2 { + margin-bottom: 0.75rem; + } + h5 { + min-height: 2rem; + } + + .button { + display: flex; + justify-content: center; + margin-top: 1rem; + } + } +`; + +export const TitleWrapper = styled.div` + --tab-height: 2.25rem; + + background: var(--background-primary); + -webkit-app-region: drag; + padding: 0.95rem 0.85rem 0.7rem 0.85rem; + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 3; + display: flex; + flex-direction: column; + + .tabs { + width: 100%; + height: var(--tab-height); + display: flex; + margin: 0.5rem 0 0.1rem 0; + + button { + padding: 0rem 1rem; + transition: background 0.15s; + height: var(--tab-height); + border-radius: 0.4rem; + margin-right: 0.75rem; + + > div { + height: var(--tab-height); + display: flex; + align-items: center; + } + &:hover { + background: var(--background-secondary); + } + &.active { + background: var(--background-secondary); + } + } + } + + > h5 { + svg { + margin-right: 0.4rem; + } + } +`; diff --git a/src/modals/ImportLedger/index.tsx b/src/modals/ImportLedger/index.tsx new file mode 100644 index 0000000000..685279d5fa --- /dev/null +++ b/src/modals/ImportLedger/index.tsx @@ -0,0 +1,169 @@ +// Copyright 2022 @paritytech/polkadot-native authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ellipsisFn, setStateWithRef } from '@polkadot-cloud/utils'; +import React, { useEffect, useRef, useState } from 'react'; +import { useLedgerHardware } from 'contexts/Hardware/Ledger'; +import { getLocalLedgerAddresses } from 'contexts/Hardware/Utils'; +import type { LedgerAddress, LedgerResponse } from 'contexts/Hardware/types'; +import { useLedgerLoop } from 'library/Hooks/useLedgerLoop'; +import type { AnyJson } from 'types'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { Manage } from './Manage'; +import { Splash } from './Splash'; + +export const ImportLedger: React.FC = () => { + const { network } = useNetwork(); + const { setModalResize } = useOverlay().modal; + const { + transportResponse, + getIsExecuting, + setIsExecuting, + resetStatusCodes, + handleNewStatusCode, + isPaired, + getStatusCodes, + handleUnmount, + } = useLedgerHardware(); + + // Gets the next non-imported address index. + const getNextAddressIndex = () => { + if (!addressesRef.current.length) { + return 0; + } + return addressesRef.current[addressesRef.current.length - 1].index + 1; + }; + + // Ledger loop needs to keep track of whether this component is mounted. If it is unmounted then + // the loop will cancel & ledger metadata will be cleared up. isMounted needs to be given as a + // function so the interval fetches the real value. + const isMounted = useRef(true); + const getIsMounted = () => isMounted.current; + + const { handleLedgerLoop } = useLedgerLoop({ + tasks: ['get_address'], + options: { + accountIndex: getNextAddressIndex, + }, + mounted: getIsMounted, + }); + + // Store addresses retreived from Ledger device. Defaults to local addresses. + const [addresses, setAddresses] = useState<LedgerAddress[]>( + getLocalLedgerAddresses(network) + ); + const addressesRef = useRef(addresses); + + const removeLedgerAddress = (address: string) => { + let newLedgerAddresses = getLocalLedgerAddresses(); + + newLedgerAddresses = newLedgerAddresses.filter((a) => { + if (a.address !== address) { + return true; + } + if (a.network !== network) { + return true; + } + return false; + }); + if (!newLedgerAddresses.length) { + localStorage.removeItem('ledger_addresses'); + } else { + localStorage.setItem( + 'ledger_addresses', + JSON.stringify(newLedgerAddresses) + ); + } + setStateWithRef( + newLedgerAddresses.filter((a: LedgerAddress) => a.network === network), + setAddresses, + addressesRef + ); + }; + + // refresh imported ledger accounts on network change. + useEffect(() => { + setStateWithRef( + getLocalLedgerAddresses(network), + setAddresses, + addressesRef + ); + }, [network]); + + // Handle new Ledger status report. + const handleLedgerStatusResponse = (response: LedgerResponse) => { + if (!response) return; + + const { ack, statusCode, body, options } = response; + handleNewStatusCode(ack, statusCode); + + if (statusCode === 'ReceivedAddress') { + const newAddress = body.map(({ pubKey, address }: LedgerAddress) => ({ + index: options.accountIndex, + pubKey, + address, + name: ellipsisFn(address), + network, + })); + + // update the full list of local ledger addresses with new entry. + const newAddresses = getLocalLedgerAddresses() + .filter((a: AnyJson) => { + if (a.address !== newAddress.address) { + return true; + } + if (a.network !== network) { + return true; + } + return false; + }) + .concat(newAddress); + localStorage.setItem('ledger_addresses', JSON.stringify(newAddresses)); + + setIsExecuting(false); + + // store only those accounts on the current network in state. + setStateWithRef( + newAddresses.filter((a) => a.network === network), + setAddresses, + addressesRef + ); + resetStatusCodes(); + } + }; + + // Resize modal on content change. + useEffect(() => { + setModalResize(); + }, [isPaired, getStatusCodes(), addressesRef.current]); + + // Listen for new Ledger status reports. + useEffect(() => { + if (getIsExecuting()) { + handleLedgerStatusResponse(transportResponse); + } + }, [transportResponse]); + + // Tidy up context state when this component is no longer mounted. + useEffect(() => { + return () => { + isMounted.current = false; + handleUnmount(); + }; + }, []); + + return ( + <> + {!addressesRef.current.length ? ( + <Splash handleLedgerLoop={handleLedgerLoop} /> + ) : ( + <Manage + addresses={addressesRef.current} + removeLedgerAddress={removeLedgerAddress} + handleLedgerLoop={handleLedgerLoop} + /> + )} + </> + ); +}; diff --git a/src/modals/ImportVault/Reader.tsx b/src/modals/ImportVault/Reader.tsx new file mode 100644 index 0000000000..406a56f970 --- /dev/null +++ b/src/modals/ImportVault/Reader.tsx @@ -0,0 +1,93 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonSecondary } from '@polkadot-cloud/react'; +import { isValidAddress } from '@polkadot-cloud/utils'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useVaultHardware } from 'contexts/Hardware/Vault'; +import { usePrompt } from 'contexts/Prompt'; +import { QRViewerWrapper } from 'library/Import/Wrappers'; +import { QrScanSignature } from 'library/QRCode/ScanSignature'; +import { useNetwork } from 'contexts/Network'; +import { formatAccountSs58 } from 'contexts/Connect/Utils'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; + +export const Reader = () => { + const { t } = useTranslation('modals'); + const { + networkData: { ss58 }, + } = useNetwork(); + const { addOtherAccounts } = useOtherAccounts(); + const { setStatus: setPromptStatus } = usePrompt(); + const { addVaultAccount, vaultAccountExists, vaultAccounts } = + useVaultHardware(); + + // Store data from QR Code scanner. + const [qrData, setQrData] = useState<any>(undefined); + + // Store QR data feedback. + const [feedback, setFeedback] = useState<string>(''); + + const handleQrData = (signature: string) => { + setQrData(signature.split(':')?.[1] || ''); + }; + + const valid = + isValidAddress(qrData) && + !vaultAccountExists(qrData) && + !formatAccountSs58(qrData, ss58); + + // Reset QR data on open. + useEffect(() => { + setQrData(undefined); + }, []); + + useEffect(() => { + // Add account and close overlay if valid. + if (valid) { + const account = addVaultAccount(qrData, vaultAccounts.length); + if (account) { + addOtherAccounts([account]); + } + setPromptStatus(0); + } + + // Display feedback. + setFeedback( + qrData === undefined + ? `${t('waitingForQRCode')}` + : isValidAddress(qrData) + ? formatAccountSs58(qrData, ss58) + ? `${t('differentNetworkAddress')}` + : vaultAccountExists(qrData) + ? `${t('accountAlreadyImported')}` + : `${t('addressReceived')}` + : `${t('invalidAddress')}` + ); + }, [qrData]); + + return ( + <QRViewerWrapper> + <h3 className="title">{t('scanFromPolkadotVault')}</h3> + <div className="viewer"> + <QrScanSignature + size={279} + onScan={({ signature }) => { + handleQrData(signature); + }} + /> + </div> + <div className="foot"> + <h3>{feedback}</h3> + <div> + <ButtonSecondary + lg + text={t('cancel')} + onClick={() => setPromptStatus(0)} + /> + </div> + </div> + </QRViewerWrapper> + ); +}; diff --git a/src/modals/ImportVault/index.tsx b/src/modals/ImportVault/index.tsx new file mode 100644 index 0000000000..ce6e3e8bd5 --- /dev/null +++ b/src/modals/ImportVault/index.tsx @@ -0,0 +1,143 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faQrcode } from '@fortawesome/free-solid-svg-icons'; +import { + ButtonPrimary, + ButtonText, + HardwareAddress, + HardwareStatusBar, + Polkicon, +} from '@polkadot-cloud/react'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useVaultHardware } from 'contexts/Hardware/Vault'; +import { usePrompt } from 'contexts/Prompt'; +import PolkadotVaultSVG from '@polkadot-cloud/assets/extensions/svg/polkadotvault.svg?react'; +import { Confirm } from 'library/Import/Confirm'; +import { Heading } from 'library/Import/Heading'; +import { NoAccounts } from 'library/Import/NoAccounts'; +import { Remove } from 'library/Import/Remove'; +import { AddressesWrapper } from 'library/Import/Wrappers'; +import type { AnyJson } from 'types'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useOtherAccounts } from 'contexts/Connect/OtherAccounts'; +import { Reader } from './Reader'; + +export const ImportVault = () => { + const { t } = useTranslation(); + const { replaceModal } = useOverlay().modal; + const { renameOtherAccount } = useOtherAccounts(); + const { openPromptWith, status: promptStatus } = usePrompt(); + + const { + vaultAccounts, + vaultAccountExists, + renameVaultAccount, + addVaultAccount, + removeVaultAccount, + getVaultAccount, + } = useVaultHardware(); + const { setModalResize } = useOverlay().modal; + + const renameHandler = (address: string, newName: string) => { + renameVaultAccount(address, newName); + renameOtherAccount(address, newName); + }; + + const openConfirmHandler = (address: string, index: number) => { + openPromptWith( + <Confirm address={address} index={index} addHandler={addVaultAccount} />, + 'small' + ); + }; + + const openRemoveHandler = (address: string) => { + openPromptWith( + <Remove + address={address} + removeHandler={removeVaultAccount} + getHandler={getVaultAccount} + />, + 'small' + ); + }; + + useEffect(() => { + setModalResize(); + }, [vaultAccounts]); + + return ( + <> + {vaultAccounts.length === 0 ? ( + <NoAccounts + Icon={PolkadotVaultSVG} + text={t('noVaultAccountsImported', { ns: 'modals' })} + > + <div> + <ButtonPrimary + lg + iconLeft={faQrcode} + text={t('importAccount', { ns: 'modals' })} + disabled={promptStatus !== 0} + onClick={() => { + openPromptWith(<Reader />, 'small'); + }} + /> + </div> + </NoAccounts> + ) : ( + <> + <Heading title={vaultAccounts.length ? 'Polkadot Vault' : ''} /> + <AddressesWrapper> + <div className="items"> + {vaultAccounts.map(({ address, name, index }: AnyJson, i) => ( + <HardwareAddress + key={i} + address={address} + index={index} + initial={name} + Identicon={<Polkicon address={address} size={40} />} + existsHandler={vaultAccountExists} + renameHandler={renameHandler} + openRemoveHandler={openRemoveHandler} + openConfirmHandler={openConfirmHandler} + t={{ + tRemove: t('remove', { ns: 'modals' }), + tImport: t('import', { ns: 'modals' }), + }} + /> + ))} + </div> + <div className="more"> + <ButtonText + iconLeft={faQrcode} + text={t('importAnotherAccount', { ns: 'modals' })} + disabled={promptStatus !== 0} + onClick={() => { + openPromptWith(<Reader />, 'small'); + }} + /> + </div> + </AddressesWrapper> + <HardwareStatusBar + show + Icon={PolkadotVaultSVG} + text={t('vaultAccounts', { + ns: 'modals', + count: vaultAccounts.length, + })} + inProgress={false} + handleDone={() => + replaceModal({ key: 'Connect', options: { disableScroll: true } }) + } + t={{ + tDone: t('done', { ns: 'library' }), + tCancel: t('cancel', { ns: 'library' }), + }} + /> + </> + )} + </> + ); +}; diff --git a/src/modals/JoinPool/index.tsx b/src/modals/JoinPool/index.tsx new file mode 100644 index 0000000000..10fee24f7f --- /dev/null +++ b/src/modals/JoinPool/index.tsx @@ -0,0 +1,160 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding } from '@polkadot-cloud/react'; +import { planckToUnit, unitToPlanck } from '@polkadot-cloud/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 type { ClaimPermission } from 'contexts/Pools/types'; +import { useSetup } from 'contexts/Setup'; +import { defaultPoolProgress } from 'contexts/Setup/defaults'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useTxMeta } from 'contexts/TxMeta'; +import { BondFeedback } from 'library/Form/Bond/BondFeedback'; +import { ClaimPermissionInput } from 'library/Form/ClaimPermissionInput'; +import { useBatchCall } from 'library/Hooks/useBatchCall'; +import { useBondGreatestFee } from 'library/Hooks/useBondGreatestFee'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const JoinPool = () => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { + networkData: { units }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { newBatchCall } = useBatchCall(); + const { setActiveAccountSetup } = useSetup(); + const { txFees, notEnoughFunds } = useTxMeta(); + const { getSignerWarnings } = useSignerWarnings(); + const { getTransferOptions } = useTransferOptions(); + const { queryPoolMember, addToPoolMembers } = usePoolMembers(); + const { + setModalStatus, + config: { options }, + setModalResize, + } = useOverlay().modal; + + const { id: poolId, setActiveTab } = options; + + const { totalPossibleBond, totalAdditionalBond } = + getTransferOptions(activeAccount).pool; + + const largestTxFee = useBondGreatestFee({ bondFor: 'pool' }); + + // if we are bonding, subtract tx fees from bond amount + const freeBondAmount = BigNumber.max(totalAdditionalBond.minus(txFees), 0); + + // local bond value + const [bond, setBond] = useState<{ bond: string }>({ + bond: planckToUnit(totalPossibleBond, units).toString(), + }); + + // Updated claim permission value + const [claimPermission, setClaimPermission] = useState< + ClaimPermission | undefined + >('Permissioned'); + + // bond valid + const [bondValid, setBondValid] = useState<boolean>(false); + + // feedback errors to trigger modal resize + const [feedbackErrors, setFeedbackErrors] = useState<string[]>([]); + + // modal resize on form update + useEffect( + () => setModalResize(), + [bond, notEnoughFunds, feedbackErrors.length] + ); + + // tx to submit + const getTx = () => { + const tx = null; + if (!api) { + 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)]; + + if (![undefined, 'Permissioned'].includes(claimPermission)) { + txs.push(api.tx.nominationPools.setClaimPermission(claimPermission)); + } + + if (txs.length === 1) { + return txs[0]; + } + + return newBatchCall(txs, activeAccount); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: bondValid, + callbackSubmit: () => { + setModalStatus('closing'); + setActiveTab(0); + }, + callbackInBlock: async () => { + // query and add account to poolMembers list + const member = await queryPoolMember(activeAccount); + addToPoolMembers(member); + + // reset localStorage setup progress + setActiveAccountSetup('pool', defaultPoolProgress); + }, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded">{t('joinPool')}</h2> + <BondFeedback + syncing={largestTxFee.isZero()} + joiningPool + bondFor="pool" + listenIsValid={(valid, errors) => { + setBondValid(valid); + setFeedbackErrors(errors); + }} + defaultBond={null} + setters={[ + { + set: setBond, + current: bond, + }, + ]} + parentErrors={warnings} + txFees={largestTxFee} + /> + <ClaimPermissionInput + current={undefined} + permissioned={false} + onChange={(val: ClaimPermission | undefined) => { + setClaimPermission(val); + }} + disabled={freeBondAmount.isZero()} + /> + </ModalPadding> + <SubmitTx valid={bondValid} {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/modals/ManageFastUnstake/index.tsx b/src/modals/ManageFastUnstake/index.tsx new file mode 100644 index 0000000000..1e3fe4ca14 --- /dev/null +++ b/src/modals/ManageFastUnstake/index.tsx @@ -0,0 +1,223 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + ActionItem, + ModalNotes, + ModalPadding, + ModalWarnings, +} from '@polkadot-cloud/react'; +import { planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBonded } from 'contexts/Bonded'; +import { useFastUnstake } from 'contexts/FastUnstake'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const ManageFastUnstake = () => { + const { t } = useTranslation('modals'); + const { api, consts } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { notEnoughFunds } = useTxMeta(); + const { getBondedAccount } = useBonded(); + const { isFastUnstaking } = useUnstaking(); + const { setModalResize, setModalStatus } = useOverlay().modal; + const { getSignerWarnings } = useSignerWarnings(); + const { activeEra, metrics } = useNetworkMetrics(); + const { feeReserve, getTransferOptions } = useTransferOptions(); + const { isExposed, counterForQueue, queueDeposit, meta } = useFastUnstake(); + + const { bondDuration, fastUnstakeDeposit } = consts; + const { fastUnstakeErasToCheckPerBlock } = metrics; + const { checked } = meta; + const controller = getBondedAccount(activeAccount); + const allTransferOptions = getTransferOptions(activeAccount); + const { nominate, freeBalance } = allTransferOptions; + const { totalUnlockChuncks } = nominate; + + const enoughForDeposit = freeBalance + .minus(feeReserve) + .isGreaterThanOrEqualTo(fastUnstakeDeposit); + + // valid to submit transaction + const [valid, setValid] = useState<boolean>(false); + + useEffect(() => { + setValid( + fastUnstakeErasToCheckPerBlock > 0 && + ((!isFastUnstaking && + enoughForDeposit && + isExposed === false && + totalUnlockChuncks === 0) || + isFastUnstaking) + ); + }, [ + isExposed, + fastUnstakeErasToCheckPerBlock, + totalUnlockChuncks, + isFastUnstaking, + fastUnstakeDeposit, + freeBalance, + feeReserve, + ]); + + useEffect( + () => setModalResize(), + [notEnoughFunds, isExposed, queueDeposit, isFastUnstaking] + ); + + // tx to submit + const getTx = () => { + let tx = null; + if (!valid || !api) { + return tx; + } + if (!isFastUnstaking) { + tx = api.tx.fastUnstake.registerFastUnstake(); + } else { + tx = api.tx.fastUnstake.deregister(); + } + return tx; + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: controller, + shouldSubmit: valid, + callbackSubmit: () => {}, + callbackInBlock: () => { + setModalStatus('closing'); + }, + }); + + // warnings + const warnings = getSignerWarnings( + activeAccount, + true, + submitExtrinsic.proxySupported + ); + + if (!isFastUnstaking) { + if (!enoughForDeposit) { + warnings.push( + `${t('noEnough')} ${planckToUnit( + fastUnstakeDeposit, + units + ).toString()} ${unit}` + ); + } + + if (totalUnlockChuncks > 0) { + warnings.push( + `${t('fastUnstakeWarningUnlocksActive', { + count: totalUnlockChuncks, + })} ${t('fastUnstakeWarningUnlocksActiveMore')}` + ); + } + } + + // manage last exposed + const lastExposedAgo = !isExposed + ? new BigNumber(0) + : activeEra.index.minus(checked[0] || 0); + + const erasRemaining = BigNumber.max(1, bondDuration.minus(lastExposedAgo)); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded"> + {t('fastUnstake', { context: 'title' })} + </h2> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning_${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + + {isExposed ? ( + <> + <ActionItem + text={t('fastUnstakeExposedAgo', { + count: lastExposedAgo.toNumber(), + })} + /> + <ModalNotes> + <p> + {t('fastUnstakeNote1', { + bondDuration: bondDuration.toString(), + })} + </p> + <p> + {t('fastUnstakeNote2', { count: erasRemaining.toNumber() })} + </p> + </ModalNotes> + </> + ) : ( + <> + {!isFastUnstaking ? ( + <> + <ActionItem text={t('fastUnstake', { context: 'register' })} /> + <ModalNotes> + <p> + <> + {t('registerFastUnstake')}{' '} + {planckToUnit(fastUnstakeDeposit, units).toString()}{' '} + {unit}. {t('fastUnstakeOnceRegistered')} + </> + </p> + <p> + {t('fastUnstakeCurrentQueue')}: <b>{counterForQueue}</b> + </p> + </ModalNotes> + </> + ) : ( + <> + <ActionItem text={t('fastUnstakeRegistered')} /> + <ModalNotes> + <p> + {t('fastUnstakeCurrentQueue')}: <b>{counterForQueue}</b> + </p> + <p>{t('fastUnstakeUnorderedNote')}</p> + </ModalNotes> + </> + )} + </> + )} + </ModalPadding> + {!isExposed ? ( + <SubmitTx + fromController + valid={valid} + submitText={ + submitExtrinsic.submitting + ? t('submitting') + : t('fastUnstakeSubmit', { + context: isFastUnstaking ? 'cancel' : 'register', + }) + } + {...submitExtrinsic} + /> + ) : null} + </> + ); +}; diff --git a/src/modals/ManagePool/Forms/ClaimCommission.tsx b/src/modals/ManagePool/Forms/ClaimCommission.tsx new file mode 100644 index 0000000000..2abe71ed60 --- /dev/null +++ b/src/modals/ManagePool/Forms/ClaimCommission.tsx @@ -0,0 +1,106 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { + ActionItem, + ButtonSubmitInvert, + ModalNotes, + ModalWarnings, +} from '@polkadot-cloud/react'; +import { greaterThanZero, planckToUnit, rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { SubmitTx } from 'library/SubmitTx'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const ClaimCommission = ({ setSection }: any) => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { setModalStatus } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { isOwner, selectedActivePool } = useActivePools(); + const { getSignerWarnings } = useSignerWarnings(); + const poolId = selectedActivePool?.id; + const pendingCommission = new BigNumber( + rmCommas(selectedActivePool?.rewardPool?.totalCommissionPending || '0') + ); + + // valid to submit transaction + const [valid, setValid] = useState<boolean>(false); + + useEffect(() => { + setValid(isOwner() && greaterThanZero(pendingCommission)); + }, [selectedActivePool, pendingCommission]); + + // tx to submit + const getTx = () => { + if (!valid || !api) { + return null; + } + return api.tx.nominationPools.claimCommission(poolId); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + return ( + <> + <div className="padding"> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <ActionItem + text={`${t('claim')} ${planckToUnit( + pendingCommission, + units + )} ${unit} `} + /> + <ModalNotes> + <p>{t('sentToCommissionPayee')}</p> + </ModalNotes> + </div> + <SubmitTx + valid={valid} + buttons={[ + <ButtonSubmitInvert + key="button_back" + text={t('back')} + iconLeft={faChevronLeft} + iconTransform="shrink-1" + onClick={() => setSection(0)} + />, + ]} + {...submitExtrinsic} + /> + </> + ); +}; diff --git a/src/modals/ManagePool/Forms/Commission.tsx b/src/modals/ManagePool/Forms/Commission.tsx new file mode 100644 index 0000000000..123b1475f3 --- /dev/null +++ b/src/modals/ManagePool/Forms/Commission.tsx @@ -0,0 +1,656 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { + ActionItem, + ButtonHelp, + ButtonSubmitInvert, + ModalWarnings, +} from '@polkadot-cloud/react'; +import { rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { intervalToDuration } from 'date-fns'; +import Slider from 'rc-slider'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useHelp } from 'contexts/Help'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { AccountInput } from 'library/AccountInput'; +import { MinDelayInput } from 'library/Form/MinDelayInput'; +import { Warning } from 'library/Form/Warning'; +import { useBatchCall } from 'library/Hooks/useBatchCall'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { SubmitTx } from 'library/SubmitTx'; +import 'rc-slider/assets/index.css'; +import type { MaybeAddress } from 'types'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { SliderWrapper } from '../Wrappers'; +import type { ChangeRateInput } from './types'; + +export const Commission = ({ setSection, incrementCalculateHeight }: any) => { + const { t } = useTranslation('modals'); + const { openHelp } = useHelp(); + const { api, consts } = useApi(); + const { activeAccount } = useActiveAccounts(); + const { newBatchCall } = useBatchCall(); + const { stats } = usePoolsConfig(); + const { setModalStatus } = useOverlay().modal; + const { getSignerWarnings } = useSignerWarnings(); + const { isOwner, selectedActivePool } = useActivePools(); + const { getBondedPool, updateBondedPools } = useBondedPools(); + const { expectedBlockTime } = consts; + const { globalMaxCommission } = stats; + + const poolId = selectedActivePool?.id || 0; + const bondedPool = getBondedPool(poolId); + + const commissionCurrentSet = !!bondedPool?.commission?.current; + const initialCommission = Number( + (bondedPool?.commission?.current?.[0] || '0%').slice(0, -1) + ); + const initialPayee = bondedPool?.commission?.current?.[1] || null; + + const maxCommissionSet = !!bondedPool?.commission?.max; + const initialMaxCommission = Number( + (bondedPool?.commission?.max || '100%').slice(0, -1) + ); + + const changeRateSet = !!bondedPool?.commission?.changeRate; + const initialChangeRate = (() => { + const raw = bondedPool?.commission?.changeRate; + return raw + ? { + maxIncrease: Number(raw.maxIncrease.slice(0, -1)), + minDelay: Number(rmCommas(raw.minDelay)), + } + : { + maxIncrease: 100, + minDelay: 0, + }; + })(); + + // Store the current commission value. + const [commission, setCommission] = useState<number>(initialCommission); + + // Max commission enabled. + const [maxCommissionEnabled, setMaxCommissionEnabled] = useState<boolean>( + !!maxCommissionSet + ); + + // Change rate enabled. + const [changeRateEnabled, setChangeRateEnabled] = useState<boolean>( + !!changeRateSet + ); + + // Store the commission payee. + const [payee, setPayee] = useState<MaybeAddress>(initialPayee); + + // Store the maximum commission value. + const [maxCommission, setMaxCommission] = + useState<number>(initialMaxCommission); + + // Store the change rate value. + const [changeRate, setChangeRate] = useState<{ + maxIncrease: number; + minDelay: number; + }>(initialChangeRate); + + // Convert a block number into an estimated change rate duration. + const minDelayToInput = (delay: number) => { + const milliseconds = expectedBlockTime.multipliedBy(delay); + const end = milliseconds.isZero() + ? 0 + : milliseconds.integerValue().toNumber(); + + const { years, months, days, hours, minutes } = intervalToDuration({ + start: 0, + end, + }); + + return { + years: years || 0, + months: months || 0, + days: days || 0, + hours: hours || 0, + minutes: minutes || 0, + }; + }; + + const inputToMinDelay = (input: ChangeRateInput) => { + const { years, months, days, hours, minutes } = input; + + // calculate number of seconds from changeRateInput + const yearsSeconds = new BigNumber(years).multipliedBy(31536000); + const monthsSeconds = new BigNumber(months).multipliedBy(2628288); + const daysSeconds = new BigNumber(days).multipliedBy(86400); + const hoursSeconds = new BigNumber(hours).multipliedBy(3600); + const minutesSeconds = new BigNumber(minutes).multipliedBy(60); + + return yearsSeconds + .plus(monthsSeconds) + .plus(daysSeconds) + .plus(hoursSeconds) + .plus(minutesSeconds) + .dividedBy(expectedBlockTime.dividedBy(1000)) + .integerValue() + .toNumber(); + }; + + // Store the change rate value in input format. + const [changeRateInput, setChangeRateInput] = useState<ChangeRateInput>( + minDelayToInput(changeRate.minDelay) + ); + + // Valid to submit transaction + const [valid, setValid] = useState<boolean>(false); + + const handleChangeRateInput = (field: string, value: number) => { + const newChangeRateInput = { + ...changeRateInput, + [field]: value, + }; + setChangeRateInput(newChangeRateInput); + setChangeRate({ + ...changeRate, + minDelay: inputToMinDelay(newChangeRateInput), + }); + }; + + const resetToDefault = () => { + setCommission(initialCommission); + setPayee(initialPayee); + setMaxCommission(initialMaxCommission); + }; + + const hasCurrentCommission = payee && commission !== 0; + const commissionCurrent = () => { + return hasCurrentCommission ? [`${commission.toFixed(2)}%`, payee] : null; + }; + + // Monitor when input items change. + const commissionUpdated = + (!commissionCurrentSet && commission !== initialCommission) || + (commissionCurrentSet && commission !== initialCommission); + + const maxCommissionUpdated = + (!maxCommissionSet && maxCommission === initialMaxCommission) || + maxCommission !== initialMaxCommission || + (!maxCommissionSet && maxCommissionEnabled); + + const changeRateUpdated = + (!changeRateSet && + JSON.stringify(changeRate) === JSON.stringify(initialChangeRate)) || + (changeRateSet && + JSON.stringify(changeRate) !== JSON.stringify(initialChangeRate)) || + (!changeRateSet && changeRateEnabled); + + const maxIncreaseUpdated = + changeRate.maxIncrease !== initialChangeRate.maxIncrease; + const minDelayUpdated = changeRate.minDelay !== initialChangeRate.minDelay; + + // Global form change. + const noChange = + !commissionUpdated && !maxCommissionUpdated && !changeRateUpdated; + + // Monitor when input items are invalid. + const commissionAboveMax = commission > maxCommission; + const commissionAboveGlobal = commission > globalMaxCommission; + + const commissionAboveMaxIncrease = + changeRateSet && commission - initialCommission > changeRate.maxIncrease; + + const invalidCurrentCommission = + commissionUpdated && + ((commission === 0 && payee !== null) || + (commission !== 0 && payee === null) || + commissionAboveMax || + commissionAboveMaxIncrease || + commission > globalMaxCommission); + + const invalidMaxCommission = + maxCommissionUpdated && maxCommission > initialMaxCommission; + const maxCommissionAboveGlobal = maxCommission > globalMaxCommission; + + // Change rate is invalid if updated is not more restrictive than current. + const invalidMaxIncrease = + changeRateUpdated && changeRate.maxIncrease > initialChangeRate.maxIncrease; + + const invalidMinDelay = + changeRateUpdated && changeRate.minDelay < initialChangeRate.minDelay; + + const invalidChangeRate = invalidMaxIncrease || invalidMinDelay; + + // Check there are txs to submit. + const txsToSubmit = + commissionUpdated || + (maxCommissionUpdated && maxCommissionEnabled) || + (changeRateUpdated && changeRateEnabled); + + useEffect(() => { + setValid( + isOwner() && + !invalidCurrentCommission && + !commissionAboveGlobal && + !invalidMaxCommission && + !maxCommissionAboveGlobal && + !invalidChangeRate && + !noChange && + txsToSubmit + ); + }, [ + isOwner(), + invalidCurrentCommission, + invalidMaxCommission, + commissionAboveGlobal, + maxCommissionAboveGlobal, + invalidChangeRate, + bondedPool, + noChange, + txsToSubmit, + ]); + + useEffect(() => { + resetToDefault(); + }, [bondedPool]); + + // Trigger modal resize when commission options are enabled / disabled. + useEffect(() => { + incrementCalculateHeight(); + }, [maxCommissionEnabled, changeRateEnabled]); + + // tx to submit. + const getTx = () => { + if (!valid || !api) { + return null; + } + + const txs = []; + if (commissionUpdated) { + txs.push( + api.tx.nominationPools.setCommission( + poolId, + hasCurrentCommission + ? [ + new BigNumber(commission).multipliedBy(10000000).toString(), + payee, + ] + : null + ) + ); + } + if (maxCommissionUpdated && maxCommissionEnabled) { + txs.push( + api.tx.nominationPools.setCommissionMax( + poolId, + new BigNumber(maxCommission).multipliedBy(10000000).toString() + ) + ); + } + if (changeRateUpdated && changeRateEnabled) { + txs.push( + api.tx.nominationPools.setCommissionChangeRate(poolId, { + maxIncrease: new BigNumber(changeRate.maxIncrease) + .multipliedBy(10000000) + .toString(), + minDelay: changeRate.minDelay.toString(), + }) + ); + } + + if (txs.length === 1) { + return txs[0]; + } + return newBatchCall(txs, activeAccount); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => { + const pool = getBondedPool(poolId); + if (pool) { + updateBondedPools([ + { + ...pool, + commission: { + ...pool.commission, + current: commissionCurrent(), + max: maxCommissionUpdated + ? `${maxCommission.toFixed(2)}%` + : pool.commission?.max || null, + changeRate: changeRateUpdated + ? { + maxIncrease: `${changeRate.maxIncrease.toFixed(2)}%`, + minDelay: String(changeRate.minDelay), + } + : pool.commission?.changeRate || null, + }, + }, + ]); + } + }, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + const commissionFeedback = (() => { + if (!commissionUpdated) { + return undefined; + } + if (commissionAboveMaxIncrease) { + return { + text: t('beyondMaxIncrease'), + label: 'danger', + }; + } + if (commissionAboveGlobal) { + return { + text: t('aboveGlobalMax'), + label: 'danger', + }; + } + if (commissionAboveMax) { + return { + text: t('aboveMax'), + label: 'danger', + }; + } + return { + text: t('updated'), + label: 'neutral', + }; + })(); + + const maxCommissionFeedback = (() => { + if (!maxCommissionUpdated) { + return undefined; + } + if (invalidMaxCommission) { + return { + text: t('aboveExisting'), + label: 'danger', + }; + } + if (maxCommissionAboveGlobal) { + return { + text: t('aboveGlobalMax'), + label: 'danger', + }; + } + return { + text: t('updated'), + label: 'neutral', + }; + })(); + + const maxIncreaseFeedback = (() => { + if (!maxIncreaseUpdated) { + return undefined; + } + if (invalidMaxIncrease) { + return { + text: t('aboveExisting'), + label: 'danger', + }; + } + return { + text: t('updated'), + label: 'neutral', + }; + })(); + + const minDelayFeedback = (() => { + if (!minDelayUpdated) { + return undefined; + } + if (invalidMinDelay) { + return { + text: t('belowExisting'), + label: 'danger', + }; + } + return { + text: t('updated'), + label: 'neutral', + }; + })(); + + const sliderProps = { + trackStyle: { + backgroundColor: 'var(--accent-color-primary)', + }, + railStyle: { + backgroundColor: 'var(--button-secondary-background)', + }, + handleStyle: { + backgroundColor: 'var(--background-primary)', + borderColor: 'var(--accent-color-primary)', + opacity: 1, + }, + activeDotStyle: { + backgroundColor: 'var(--background-primary)', + }, + }; + + return ( + <> + <div className="padding"> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + + <ActionItem + text={t('commissionRate')} + inlineButton={ + <ButtonHelp onClick={() => openHelp('Pool Commission Rate')} /> + } + /> + + <SliderWrapper> + <div> + <h2>{commission}% </h2> + <h5 className={commissionFeedback?.label || 'neutral'}> + {!!commissionFeedback && commissionFeedback.text} + </h5> + </div> + + <div className="slider"> + <Slider + value={commission} + step={0.1} + onChange={(val) => { + if (typeof val === 'number') { + setCommission(val); + if (val > maxCommission && maxCommissionEnabled) { + setMaxCommission(Math.min(initialMaxCommission, val)); + } + } + }} + {...sliderProps} + /> + </div> + </SliderWrapper> + + <AccountInput + defaultLabel={t('inputPayeeAccount')} + successLabel={t('payeeAdded')} + locked={payee !== null} + successCallback={async (input) => { + setPayee(input); + }} + resetCallback={() => { + setPayee(null); + }} + disallowAlreadyImported={false} + initialValue={payee} + inactive={commission === 0} + border={payee === null} + /> + + <ActionItem + style={{ + marginTop: '2rem', + borderBottomWidth: maxCommissionEnabled ? '1px' : 0, + }} + text={t('maxCommission')} + toggled={maxCommissionEnabled} + onToggle={(val) => setMaxCommissionEnabled(val)} + disabled={!!maxCommissionSet} + inlineButton={ + <ButtonHelp onClick={() => openHelp('Pool Max Commission')} /> + } + /> + + {maxCommissionEnabled && ( + <SliderWrapper> + <div> + <h2>{maxCommission}% </h2> + <h5 className={maxCommissionFeedback?.label || 'neutral'}> + {!!maxCommissionFeedback && maxCommissionFeedback.text} + </h5> + </div> + + <div className="slider"> + <Slider + value={maxCommission} + step={0.1} + onChange={(val) => { + if (typeof val === 'number') { + setMaxCommission(val); + if (val < commission) { + setCommission(val); + } + } + }} + {...sliderProps} + /> + </div> + </SliderWrapper> + )} + + <ActionItem + style={{ + marginTop: '2rem', + borderBottomWidth: changeRateEnabled ? '1px' : 0, + }} + text={t('changeRate')} + toggled={changeRateEnabled} + onToggle={(val) => setChangeRateEnabled(val)} + disabled={!!changeRateSet} + inlineButton={ + <ButtonHelp + onClick={() => openHelp('Pool Commission Change Rate')} + /> + } + /> + + {changeRateEnabled && ( + <SliderWrapper> + <div> + <h2>{changeRate.maxIncrease}% </h2> + <h5 className={maxIncreaseFeedback?.label || 'neutral'}> + {!!maxIncreaseFeedback && maxIncreaseFeedback.text} + </h5> + </div> + + <div className="slider"> + <Slider + value={changeRate.maxIncrease} + step={0.1} + onChange={(val) => { + if (typeof val === 'number') { + setChangeRate({ + ...changeRate, + maxIncrease: val, + }); + } + }} + {...sliderProps} + /> + </div> + + <h5 style={{ marginTop: '1rem' }}> + {t('minDelayBetweenUpdates')} + {minDelayFeedback && ( + <span className={minDelayFeedback?.label || 'neutral'}> + {minDelayFeedback.text} + </span> + )} + </h5> + <div className="changeRate"> + <MinDelayInput + initial={changeRateInput.years} + field="years" + label={t('years')} + handleChange={handleChangeRateInput} + /> + <MinDelayInput + initial={changeRateInput.months} + field="months" + label={t('months')} + handleChange={handleChangeRateInput} + /> + <MinDelayInput + initial={changeRateInput.days} + field="days" + label={t('days')} + handleChange={handleChangeRateInput} + /> + <MinDelayInput + initial={changeRateInput.hours} + field="hours" + label={t('hours')} + handleChange={handleChangeRateInput} + /> + <MinDelayInput + initial={changeRateInput.minutes} + field="minutes" + label={t('minutes')} + handleChange={handleChangeRateInput} + /> + </div> + <p> + {t('thisMinimumDelay', { + count: changeRate.minDelay, + })} + </p> + </SliderWrapper> + )} + </div> + <SubmitTx + valid={valid} + buttons={[ + <ButtonSubmitInvert + key="button_back" + text={t('back')} + iconLeft={faChevronLeft} + iconTransform="shrink-1" + onClick={() => { + setSection(0); + resetToDefault(); + }} + />, + ]} + {...submitExtrinsic} + /> + </> + ); +}; diff --git a/src/modals/ManagePool/Forms/LeavePool.tsx b/src/modals/ManagePool/Forms/LeavePool.tsx new file mode 100644 index 0000000000..f9e79b5c71 --- /dev/null +++ b/src/modals/ManagePool/Forms/LeavePool.tsx @@ -0,0 +1,152 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { + ActionItem, + ButtonSubmitInvert, + ModalWarnings, +} from '@polkadot-cloud/react'; +import { + greaterThanZero, + planckToUnit, + unitToPlanck, +} from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { getUnixTime } from 'date-fns'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { Warning } from 'library/Form/Warning'; +import { useErasToTimeLeft } from 'library/Hooks/useErasToTimeLeft'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { timeleftAsString } from 'library/Hooks/useTimeLeft/utils'; +import { SubmitTx } from 'library/SubmitTx'; +import { StaticNote } from 'modals/Utils/StaticNote'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const LeavePool = ({ setSection }: any) => { + const { t } = useTranslation('modals'); + const { api, consts } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { setModalStatus, setModalResize } = useOverlay().modal; + const { getTransferOptions } = useTransferOptions(); + const { selectedActivePool } = useActivePools(); + const { erasToSeconds } = useErasToTimeLeft(); + const { getSignerWarnings } = useSignerWarnings(); + + const allTransferOptions = getTransferOptions(activeAccount); + const { active: activeBn } = allTransferOptions.pool; + const { bondDuration } = consts; + + const bondDurationFormatted = timeleftAsString( + t, + getUnixTime(new Date()) + 1, + erasToSeconds(bondDuration), + true + ); + + let { pendingRewards } = selectedActivePool || {}; + pendingRewards = pendingRewards ?? new BigNumber(0); + pendingRewards = planckToUnit(pendingRewards, units); + + // convert BigNumber values to number + const freeToUnbond = planckToUnit(activeBn, units); + + // local bond value + const [bond, setBond] = useState<{ bond: string }>({ + bond: freeToUnbond.toString(), + }); + + // bond valid + const [bondValid, setBondValid] = useState(false); + + // unbond all validation + const isValid = (() => greaterThanZero(freeToUnbond))(); + + // update bond value on task change + useEffect(() => { + setBond({ bond: freeToUnbond.toString() }); + setBondValid(isValid); + }, [freeToUnbond.toString(), isValid]); + + // modal resize on form update + useEffect(() => setModalResize(), [bond]); + + // tx to submit + const getTx = () => { + let tx = null; + if (!api || !activeAccount) { + return tx; + } + + const bondToSubmit = unitToPlanck(!bondValid ? '0' : bond.bond, units); + const bondAsString = bondToSubmit.isNaN() ? '0' : bondToSubmit.toString(); + tx = api.tx.nominationPools.unbond(activeAccount, bondAsString); + return tx; + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: bondValid, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + if (greaterThanZero(pendingRewards)) { + warnings.push( + `${t('unbondingWithdraw')} ${pendingRewards.toString()} ${unit}.` + ); + } + + return ( + <> + <div className="padding"> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <ActionItem text={`${t('unbond')} ${freeToUnbond} ${unit}`} /> + <StaticNote + value={bondDurationFormatted} + tKey="onceUnbonding" + valueKey="bondDurationFormatted" + deps={[bondDuration]} + /> + </div> + <SubmitTx + valid={bondValid} + buttons={[ + <ButtonSubmitInvert + key="button_back" + text={t('back')} + iconLeft={faChevronLeft} + iconTransform="shrink-1" + onClick={() => setSection(0)} + />, + ]} + {...submitExtrinsic} + /> + </> + ); +}; diff --git a/src/modals/ManagePool/Forms/SetClaimPermission.tsx b/src/modals/ManagePool/Forms/SetClaimPermission.tsx new file mode 100644 index 0000000000..0bf3035701 --- /dev/null +++ b/src/modals/ManagePool/Forms/SetClaimPermission.tsx @@ -0,0 +1,109 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { ButtonSubmitInvert, ModalWarnings } from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import type { ClaimPermission } from 'contexts/Pools/types'; +import { ClaimPermissionInput } from 'library/Form/ClaimPermissionInput'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { SubmitTx } from 'library/SubmitTx'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const SetClaimPermission = ({ setSection, section }: any) => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { activeAccount } = useActiveAccounts(); + const { setModalStatus } = useOverlay().modal; + const { isOwner, isMember } = useActivePools(); + const { getSignerWarnings } = useSignerWarnings(); + const { membership } = usePoolMemberships(); + + // Valid to submit transaction. + const [valid, setValid] = useState<boolean>(false); + + // Updated claim permission value. + const [claimPermission, setClaimPermission] = useState< + ClaimPermission | undefined + >(membership?.claimPermission); + + // Determine current pool metadata and set in state. + useEffect(() => { + const current = membership?.claimPermission; + if (current) { + setClaimPermission(membership?.claimPermission); + } + }, [section, membership]); + + useEffect(() => { + setValid(isOwner() || (isMember() && claimPermission !== undefined)); + }, [isOwner(), isMember()]); + + // tx to submit. + const getTx = () => { + if (!valid || !api) { + return null; + } + return api.tx.nominationPools.setClaimPermission(claimPermission); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + return ( + <> + <div className="padding"> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + + <ClaimPermissionInput + current={membership?.claimPermission} + permissioned={ + ![undefined, 'Permissioned'].includes(membership?.claimPermission) + } + onChange={(val: ClaimPermission | undefined) => { + setClaimPermission(val); + }} + /> + </div> + <SubmitTx + valid={valid && claimPermission !== membership?.claimPermission} + buttons={[ + <ButtonSubmitInvert + key="button_back" + text={t('back')} + iconLeft={faChevronLeft} + iconTransform="shrink-1" + onClick={() => setSection(0)} + />, + ]} + {...submitExtrinsic} + /> + </> + ); +}; diff --git a/src/modals/ManagePool/Forms/SetMetadata.tsx b/src/modals/ManagePool/Forms/SetMetadata.tsx new file mode 100644 index 0000000000..1ed361ca57 --- /dev/null +++ b/src/modals/ManagePool/Forms/SetMetadata.tsx @@ -0,0 +1,118 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { u8aToString, u8aUnwrapBytes } from '@polkadot/util'; +import { ButtonSubmitInvert, ModalWarnings } from '@polkadot-cloud/react'; +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { SubmitTx } from 'library/SubmitTx'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const SetMetadata = ({ setSection, section }: any) => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { setModalStatus } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { isOwner, selectedActivePool } = useActivePools(); + const { bondedPools, meta } = useBondedPools(); + const { getSignerWarnings } = useSignerWarnings(); + + const poolId = selectedActivePool?.id; + + // Valid to submit transaction + const [valid, setValid] = useState<boolean>(false); + + // Updated metadata value + const [metadata, setMetadata] = useState<string>(''); + + // Determine current pool metadata and set in state. + useEffect(() => { + const pool = bondedPools.find( + ({ addresses }) => addresses.stash === selectedActivePool?.addresses.stash + ); + if (pool) { + const metadataBatch = meta.bonded_pools?.metadata ?? []; + const batchIndex = bondedPools.indexOf(pool); + setMetadata(u8aToString(u8aUnwrapBytes(metadataBatch[batchIndex]))); + } + }, [section]); + + useEffect(() => { + setValid(isOwner()); + }, [isOwner()]); + + // tx to submit + const getTx = () => { + if (!valid || !api) { + return null; + } + return api.tx.nominationPools.setMetadata(poolId, metadata); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const handleMetadataChange = (e: React.FormEvent<HTMLInputElement>) => { + setMetadata(e.currentTarget.value); + setValid(true); + }; + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + return ( + <> + <div className="padding"> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <input + className="textbox" + style={{ width: '100%' }} + placeholder={t('poolName')} + type="text" + onChange={(e: React.FormEvent<HTMLInputElement>) => + handleMetadataChange(e) + } + value={metadata ?? ''} + /> + <p>{t('storedOnChain')}</p> + </div> + <SubmitTx + valid={valid} + buttons={[ + <ButtonSubmitInvert + key="button_back" + text={t('back')} + iconLeft={faChevronLeft} + iconTransform="shrink-1" + onClick={() => setSection(0)} + />, + ]} + {...submitExtrinsic} + /> + </> + ); +}; diff --git a/src/modals/ManagePool/Forms/SetState.tsx b/src/modals/ManagePool/Forms/SetState.tsx new file mode 100644 index 0000000000..2262eb9f41 --- /dev/null +++ b/src/modals/ManagePool/Forms/SetState.tsx @@ -0,0 +1,158 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { + ActionItem, + ButtonSubmitInvert, + ModalWarnings, +} from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { SubmitTx } from 'library/SubmitTx'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const SetState = ({ setSection, task }: any) => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { setModalStatus } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { isOwner, isBouncer, selectedActivePool } = useActivePools(); + const { updateBondedPools, getBondedPool } = useBondedPools(); + const { getSignerWarnings } = useSignerWarnings(); + + const poolId = selectedActivePool?.id; + + // valid to submit transaction + const [valid, setValid] = useState<boolean>(false); + + // ensure account has relevant roles for task + const canToggle = + (isOwner() || isBouncer()) && + ['destroy_pool', 'unlock_pool', 'lock_pool'].includes(task); + + useEffect(() => { + setValid(canToggle); + }, [canToggle]); + + const content = (() => { + let title; + let message; + switch (task) { + case 'destroy_pool': + title = <ActionItem text={t('setToDestroying')} />; + message = <p>{t('setToDestroyingSubtitle')}</p>; + break; + case 'unlock_pool': + title = <ActionItem text={t('unlockPool')} />; + message = <p>{t('unlockPoolSubtitle')}</p>; + break; + default: + title = <ActionItem text={t('lockPool')} />; + message = <p>{t('lockPoolSubtitle')}</p>; + } + return { title, message }; + })(); + + const poolStateFromTask = (s: string) => { + switch (s) { + case 'destroy_pool': + return 'Destroying'; + case 'lock_pool': + return 'Blocked'; + default: + return 'Open'; + } + }; + + // tx to submit + const getTx = () => { + if (!valid || !api) { + return null; + } + + let tx; + switch (task) { + case 'destroy_pool': + tx = api.tx.nominationPools.setState(poolId, 'Destroying'); + break; + case 'unlock_pool': + tx = api.tx.nominationPools.setState(poolId, 'Open'); + break; + case 'lock_pool': + tx = api.tx.nominationPools.setState(poolId, 'Blocked'); + break; + default: + tx = null; + } + return tx; + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => { + // reflect updated state in `bondedPools` list. + if ( + ['destroy_pool', 'unlock_pool', 'lock_pool'].includes(task) && + poolId + ) { + const pool = getBondedPool(poolId); + if (pool) { + updateBondedPools([ + { + ...pool, + state: poolStateFromTask(task), + }, + ]); + } + } + }, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + return ( + <> + <div className="padding"> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + {content.title} + {content.message} + </div> + <SubmitTx + valid={valid} + buttons={[ + <ButtonSubmitInvert + key="button_back" + text={t('back')} + iconLeft={faChevronLeft} + iconTransform="shrink-1" + onClick={() => setSection(0)} + />, + ]} + {...submitExtrinsic} + /> + </> + ); +}; diff --git a/src/modals/ManagePool/Forms/index.tsx b/src/modals/ManagePool/Forms/index.tsx new file mode 100644 index 0000000000..cc790bac70 --- /dev/null +++ b/src/modals/ManagePool/Forms/index.tsx @@ -0,0 +1,41 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { forwardRef } from 'react'; +import { ContentWrapper } from '../Wrappers'; +import { ClaimCommission } from './ClaimCommission'; +import { Commission } from './Commission'; +import { LeavePool } from './LeavePool'; +import { SetClaimPermission } from './SetClaimPermission'; +import { SetMetadata } from './SetMetadata'; +import { SetState } from './SetState'; + +export const Forms = forwardRef( + ({ setSection, task, section, incrementCalculateHeight }: any, ref: any) => { + return ( + <> + <ContentWrapper> + <div className="items" ref={ref}> + {task === 'set_pool_metadata' ? ( + <SetMetadata setSection={setSection} section={section} /> + ) : task === 'manage_commission' ? ( + <Commission + setSection={setSection} + section={section} + incrementCalculateHeight={incrementCalculateHeight} + /> + ) : task === 'set_claim_permission' ? ( + <SetClaimPermission setSection={setSection} section={section} /> + ) : task === 'leave_pool' ? ( + <LeavePool setSection={setSection} /> + ) : task === 'claim_commission' ? ( + <ClaimCommission setSection={setSection} /> + ) : ( + <SetState setSection={setSection} task={task} /> + )} + </div> + </ContentWrapper> + </> + ); + } +); diff --git a/src/modals/ManagePool/Forms/types.ts b/src/modals/ManagePool/Forms/types.ts new file mode 100644 index 0000000000..43560597b9 --- /dev/null +++ b/src/modals/ManagePool/Forms/types.ts @@ -0,0 +1,10 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface ChangeRateInput { + years: number; + months: number; + days: number; + hours: number; + minutes: number; +} diff --git a/src/modals/ManagePool/Tasks.tsx b/src/modals/ManagePool/Tasks.tsx new file mode 100644 index 0000000000..4b65f14a3f --- /dev/null +++ b/src/modals/ManagePool/Tasks.tsx @@ -0,0 +1,150 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonOption } from '@polkadot-cloud/react'; +import { forwardRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { Warning } from 'library/Form/Warning'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { ContentWrapper } from './Wrappers'; + +export const Tasks = forwardRef(({ setSection, setTask }: any, ref: any) => { + const { t } = useTranslation('modals'); + const { activeAccount } = useActiveAccounts(); + const { selectedActivePool, isOwner, isBouncer, isMember, isDepositor } = + useActivePools(); + const { getTransferOptions } = useTransferOptions(); + const { stats } = usePoolsConfig(); + const { globalMaxCommission } = stats; + const { active } = getTransferOptions(activeAccount).pool; + + const poolLocked = selectedActivePool?.bondedPool?.state === 'Blocked'; + const poolDestroying = selectedActivePool?.bondedPool?.state === 'Destroying'; + + return ( + <ContentWrapper> + <div className="padding"> + <div className="items" ref={ref} style={{ paddingBottom: '1.5rem' }}> + <div style={{ paddingBottom: '0.75rem' }}> + {poolDestroying && <Warning text={t('beingDestroyed')} />} + </div> + {isOwner() && ( + <> + {globalMaxCommission > 0 && ( + <> + <ButtonOption + onClick={() => { + setSection(1); + setTask('claim_commission'); + }} + > + <div> + <h3>{t('claimCommission')}</h3> + <p>{t('claimOutstandingCommission')}</p> + </div> + </ButtonOption> + <ButtonOption + onClick={() => { + setSection(1); + setTask('manage_commission'); + }} + > + <div> + <h3>{t('manageCommission')}</h3> + <p>{t('updatePoolCommission')}</p> + </div> + </ButtonOption> + </> + )} + </> + )} + <ButtonOption + onClick={() => { + setSection(1); + setTask('set_claim_permission'); + }} + > + <div> + <h3>{t('updateClaimPermission')}</h3> + <p>{t('updateWhoClaimRewards')}</p> + </div> + </ButtonOption> + + {isOwner() && ( + <ButtonOption + disabled={poolDestroying} + onClick={() => { + setSection(1); + setTask('set_pool_metadata'); + }} + > + <div> + <h3>{t('renamePool')}</h3> + <p>{t('updateName')}</p> + </div> + </ButtonOption> + )} + {(isOwner() || isBouncer()) && ( + <> + {poolLocked ? ( + <ButtonOption + disabled={poolDestroying} + onClick={() => { + setSection(1); + setTask('unlock_pool'); + }} + > + <div> + <h3>{t('unlockPool')}</h3> + <p>{t('allowToJoin')}</p> + </div> + </ButtonOption> + ) : ( + <ButtonOption + disabled={poolDestroying} + onClick={() => { + setSection(1); + setTask('lock_pool'); + }} + > + <div> + <h3>{t('lockPool')}</h3> + <p>{t('stopJoiningPool')}</p> + </div> + </ButtonOption> + )} + <ButtonOption + disabled={poolDestroying} + onClick={() => { + setSection(1); + setTask('destroy_pool'); + }} + > + <div> + <h3>{t('destroyPool')}</h3> + <p>{t('changeToDestroy')}</p> + </div> + </ButtonOption> + </> + )} + {isMember() && !isDepositor() && active?.isGreaterThan(0) && ( + <ButtonOption + onClick={() => { + setSection(1); + setTask('leave_pool'); + }} + > + <div> + <h3>{t('leavePool')}</h3> + <p>{t('unbondFundsLeavePool')}</p> + </div> + </ButtonOption> + )} + </div> + </div> + </ContentWrapper> + ); +}); diff --git a/src/modals/ManagePool/Wrappers.ts b/src/modals/ManagePool/Wrappers.ts new file mode 100644 index 0000000000..2683239755 --- /dev/null +++ b/src/modals/ManagePool/Wrappers.ts @@ -0,0 +1,136 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const ContentWrapper = styled.div` + border-radius: 1rem; + display: flex; + flex-flow: column nowrap; + flex-basis: 50%; + min-width: 50%; + height: auto; + flex-grow: 1; + + .padding { + padding: 0 1rem 1rem 1rem; + + h2 { + margin-bottom: 0.5rem; + } + + input { + font-family: InterBold, sans-serif; + margin-top: 0.5rem; + } + } + + .items { + position: relative; + padding: 0.5rem 0 0rem 0; + border-bottom: none; + width: auto; + border-radius: 0.75rem; + overflow: hidden; + overflow-y: auto; + z-index: 1; + width: 100%; + + h4 { + margin: 0.2rem 0; + } + + .arrow { + color: var(--text-color-primary); + } + } +`; + +export const SliderWrapper = styled.div` + display: flex; + flex-direction: column; + padding: 0 0.5rem 0 0.5rem; + + h5 { + font-family: InterSemiBold, sans-serif; + margin: 0; + margin-left: 0.75rem; + + > span { + margin-left: 0.75rem; + } + &.neutral, + .neutral { + color: var(--accent-color-primary); + opacity: 0.8; + } + &.danger, + .danger { + color: var(--status-danger-color); + } + &.success, + .success { + color: var(--status-success-color); + } + } + + > div:first-child { + display: flex; + align-items: center; + margin: 1.25rem 0 0.5rem 0; + + h2 { + margin: 0; + font-family: InterBold, sans-serif; + } + } + + .changeRate { + display: flex; + flex-wrap: wrap; + margin: 0.25rem 0; + } + + > div { + display: flex; + align-items: center; + + > .slider { + flex-grow: 1; + + &.no-value { + padding-left: 0; + } + + .rc-slider-handle-dragging { + box-shadow: 0 0 0 5px var(--accent-color-transparent) !important; + } + } + } + + .stats { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-top: 1rem; + h2 { + border-bottom: 1px solid var(--border-primary-color); + font-family: InterBold, sans-serif; + margin-top: 0rem; + padding-bottom: 1rem; + } + } + + .done { + display: flex; + justify-content: flex-end; + margin-top: 1rem; + } + + .confirm { + display: flex; + flex-flow: column wrap; + align-items: flex-end; + margin-top: 2.5rem; + } +`; diff --git a/src/modals/ManagePool/index.tsx b/src/modals/ManagePool/index.tsx new file mode 100644 index 0000000000..cc7ee659eb --- /dev/null +++ b/src/modals/ManagePool/index.tsx @@ -0,0 +1,92 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + ModalFixedTitle, + ModalMotionTwoSection, + ModalSection, +} from '@polkadot-cloud/react'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { Title } from 'library/Modal/Title'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { Forms } from './Forms'; +import { Tasks } from './Tasks'; + +export const ManagePool = () => { + const { t } = useTranslation('modals'); + const { notEnoughFunds } = useTxMeta(); + const { setModalHeight } = useOverlay().modal; + const { isOwner, selectedActivePool } = useActivePools(); + + // modal task + const [task, setTask] = useState<string>(); + + // active modal section + const [section, setSection] = useState<number>(0); + + // counter to trigger modal height calculation + const [calculateHeight, setCalculateHeight] = useState<number>(0); + const incrementCalculateHeight = () => + setCalculateHeight(calculateHeight + 1); + + // refs for wrappers + const headerRef = useRef<HTMLDivElement>(null); + const tasksRef = useRef<HTMLDivElement>(null); + const formsRef = useRef<HTMLDivElement>(null); + + // Resize modal on state change. + useEffect(() => { + let height = headerRef.current?.clientHeight || 0; + if (section === 0) { + height += tasksRef.current?.clientHeight || 0; + } else { + height += formsRef.current?.clientHeight || 0; + } + setModalHeight(height); + }, [ + section, + task, + notEnoughFunds, + calculateHeight, + selectedActivePool?.bondedPool?.state, + ]); + + return ( + <ModalSection type="carousel"> + <ModalFixedTitle ref={headerRef}> + <Title + title={`${t('managePool')}${!isOwner() ? ` Membership` : ``}`} + fixed + /> + </ModalFixedTitle> + <ModalMotionTwoSection + animate={section === 0 ? 'home' : 'next'} + transition={{ + duration: 0.5, + type: 'spring', + bounce: 0.1, + }} + variants={{ + home: { + left: 0, + }, + next: { + left: '-100%', + }, + }} + > + <Tasks setSection={setSection} setTask={setTask} ref={tasksRef} /> + <Forms + setSection={setSection} + task={task} + section={section} + ref={formsRef} + incrementCalculateHeight={incrementCalculateHeight} + /> + </ModalMotionTwoSection> + </ModalSection> + ); +}; diff --git a/src/modals/Networks/ProvidersPrompt.tsx b/src/modals/Networks/ProvidersPrompt.tsx new file mode 100644 index 0000000000..d2f05b3292 --- /dev/null +++ b/src/modals/Networks/ProvidersPrompt.tsx @@ -0,0 +1,52 @@ +import { useApi } from 'contexts/Api'; +import { Title } from 'library/Prompt/Title'; +import { useTranslation } from 'react-i18next'; +import { PromptSelectItem } from 'library/Prompt/Wrappers'; +import { useNetwork } from 'contexts/Network'; +import { NetworkList } from 'config/networks'; +import { usePrompt } from 'contexts/Prompt'; +import { capitalizeFirstLetter } from '@polkadot-cloud/utils'; + +export const ProvidersPrompt = () => { + const { t } = useTranslation(); + const { network } = useNetwork(); + const { closePrompt } = usePrompt(); + const { rpcEndpoint, setRpcEndpoint } = useApi(); + + const rpcProviders = NetworkList[network].endpoints.rpcEndpoints; + return ( + <> + <Title + title={t('rpcProviders', { ns: 'modals' })} + closeText={t('cancel', { ns: 'modals' })} + /> + <div className="padded"> + <h4 className="subheading"> + {t('selectRpcProvider', { + ns: 'modals', + network: capitalizeFirstLetter(network), + })} + </h4> + {Object.entries(rpcProviders)?.map(([key, url], i) => { + const isDisabled = rpcEndpoint === key; + + return ( + <PromptSelectItem + key={`favorite_${i}`} + className={isDisabled ? 'inactive' : undefined} + onClick={() => { + closePrompt(); + setRpcEndpoint(key); + }} + > + <h3> + {key} {isDisabled && ` (${t('selected', { ns: 'modals' })})`} + </h3> + <h4>{url}</h4> + </PromptSelectItem> + ); + })} + </div> + </> + ); +}; diff --git a/src/modals/Networks/Wrapper.ts b/src/modals/Networks/Wrapper.ts new file mode 100644 index 0000000000..0c3826700b --- /dev/null +++ b/src/modals/Networks/Wrapper.ts @@ -0,0 +1,204 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { SectionFullWidthThreshold } from 'consts'; +import styled from 'styled-components'; + +export const Wrapper = styled.div` + display: flex; + flex-flow: column wrap; + align-items: center; + padding: 1rem; + + h2 { + color: var(--text-color-primary); + margin-top: 0.5rem; + margin-bottom: 1rem; + } +`; + +export const ContentWrapper = styled.div` + width: 100%; + + > h4 { + border-bottom: 1px solid var(--border-primary-color); + color: var(--text-color-secondary); + margin: 0.75rem 0; + padding-bottom: 0.5rem; + width: 100%; + } + + .items { + position: relative; + border-bottom: none; + width: auto; + border-radius: 0.75rem; + overflow: hidden; + overflow-y: auto; + z-index: 1; + width: 100%; + margin: 1rem 0 1.5rem 0; + + h4 { + margin: 0.2rem 0; + } + h2 { + margin: 0.75rem 0; + } + } +`; + +export const NetworkButton = styled.button<{ $connected: boolean }>` + background: var(--button-primary-background); + border: 1px solid var(--status-success-color-transparent); + padding: 1rem; + cursor: pointer; + margin-bottom: 1rem; + border-radius: 0.75rem; + display: inline-flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + ${(props) => + props.$connected !== true && + ` + border: 1px solid rgba(0,0,0,0); + `} + + &:last-child { + margin-bottom: 0; + } + + h3 { + font-family: InterSemiBold, sans-serif; + margin: 0 0.5rem; + } + + h4 { + &.selected { + color: var(--status-success-color); + margin-left: 0.75rem; + } + } + + > *:last-child { + flex: 1; + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + } + &:hover { + background: var(--button-hover-background); + } + .icon { + margin-right: 0.5rem; + } + + svg { + color: var(--text-color-secondary); + fill: var(--text-color-secondary); + } + p { + color: var(--text-color-primary); + font-size: 1rem; + } + + &:disabled { + cursor: default; + &:hover { + background: var(--button-primary-background); + } + } +`; + +export const BraveWarning = styled.div` + border: 1px solid var(--border-primary-color); + display: flex; + border-radius: 0.75rem; + padding: 1rem; + + .brave-text { + color: var(--text-color-primary); + width: 90%; + padding-left: 1rem; + font-size: 1.2rem; + align-self: center; + + .learn-more { + color: var(--text-color-secondary); + text-decoration: underline var(--border-primary-color); + } + } +`; + +export const ConnectionsWrapper = styled.div` + display: flex; + flex-flow: row wrap; + align-items: flex-start; + margin-top: 1rem; + margin-bottom: 1.5rem; + + > div { + flex-basis: 50%; + display: flex; + flex-direction: column; + align-items: flex-start; + + &:first-child { + padding-right: 1rem; + } + + @media (max-width: ${SectionFullWidthThreshold - 400}px) { + flex-basis: 100%; + &:first-child { + padding-right: 0; + } + } + } +`; + +export const ConnectionButton = styled.button<{ $connected: boolean }>` + background: var(--button-primary-background); + border: 1px solid var(--status-success-color-transparent); + position: relative; + padding: 1rem 0.75rem; + margin-bottom: 1rem; + margin-right: 0.5rem; + border-radius: 0.75rem; + ${(props) => + props.$connected !== true && + ` + border: 1px solid rgba(0,0,0,0); + `} + display: inline-flex; + flex-flow: row wrap; + align-items: center; + width: 100%; + + &:hover { + background: var(--button-hover-background); + } + + > h3 { + font-family: InterSemiBold, sans-serif; + margin: 0 0.75rem; + } + h4 { + &.selected { + color: var(--status-success-color); + margin: 0 0.75rem 0 0; + } + } + + &:disabled { + cursor: default; + &:hover { + background: var(--button-primary-background); + } + &.off { + h3 { + opacity: var(--opacity-disabled); + } + } + } +`; diff --git a/src/modals/Networks/index.tsx b/src/modals/Networks/index.tsx new file mode 100644 index 0000000000..52defcf211 --- /dev/null +++ b/src/modals/Networks/index.tsx @@ -0,0 +1,151 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronRight, faGlobe } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ButtonTertiary, ModalPadding } from '@polkadot-cloud/react'; +import { capitalizeFirstLetter } from '@polkadot-cloud/utils'; +import { useEffect } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { NetworkList } from 'config/networks'; +import { useApi } from 'contexts/Api'; +import { Title } from 'library/Modal/Title'; +import type { NetworkName } from 'types'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useUi } from 'contexts/UI'; +import { usePrompt } from 'contexts/Prompt'; +import BraveIconSVG from '../../img/brave-logo.svg?react'; +import { + BraveWarning, + ConnectionButton, + ConnectionsWrapper, + ContentWrapper, + NetworkButton, +} from './Wrapper'; +import { ProvidersPrompt } from './ProvidersPrompt'; + +export const Networks = () => { + const { t } = useTranslation('modals'); + const { isBraveBrowser } = useUi(); + const { openPromptWith } = usePrompt(); + const { network, switchNetwork } = useNetwork(); + const { setModalStatus, setModalResize } = useOverlay().modal; + const { isLightClient, setIsLightClient, rpcEndpoint } = useApi(); + const networkKey = network; + + // Likely never going to happen; here just to be safe. + useEffect(() => setModalResize(), [isBraveBrowser]); + + return ( + <> + <Title title={t('networks')} icon={faGlobe} /> + <ModalPadding> + <ContentWrapper> + <h4>{t('selectNetwork')}</h4> + <div className="items"> + {Object.entries(NetworkList).map( + ([key, item]: any, index: number) => { + const Svg = item.brand.inline.svg; + const rpcDisabled = networkKey === key; + + return ( + <NetworkButton + $connected={networkKey === key} + disabled={rpcDisabled} + key={`network_switch_${index}`} + type="button" + onClick={() => { + if (networkKey !== key) { + switchNetwork(key); + setModalStatus('closing'); + } + }} + > + <div style={{ width: '1.75rem' }}> + <Svg + width={item.brand.inline.size} + height={item.brand.inline.size} + /> + </div> + <h3>{capitalizeFirstLetter(item.name)}</h3> + {networkKey === key && ( + <h4 className="selected">{t('selected')}</h4> + )} + <div> + <FontAwesomeIcon + transform="shrink-2" + icon={faChevronRight} + /> + </div> + </NetworkButton> + ); + } + )} + </div> + <h4>{t('connectionType')}</h4> + <ConnectionsWrapper> + <div> + <ConnectionButton + $connected={!isLightClient} + disabled={!isLightClient} + type="button" + onClick={() => { + setIsLightClient(false); + switchNetwork(networkKey as NetworkName); + setModalStatus('closing'); + }} + > + <h3>RPC</h3> + {!isLightClient && ( + <h4 className="selected">{t('selected')}</h4> + )} + </ConnectionButton> + <div + style={{ + padding: '0 0.25rem', + display: 'flex', + alignItems: 'center', + }} + > + {t('provider')}:{' '} + <ButtonTertiary + text={rpcEndpoint} + onClick={() => openPromptWith(<ProvidersPrompt />)} + marginLeft + /> + </div> + </div> + <div> + <ConnectionButton + $connected={isLightClient} + className="off" + type="button" + onClick={() => { + setIsLightClient(true); + switchNetwork(networkKey as NetworkName); + setModalStatus('closing'); + }} + > + <h3>{t('lightClient')}</h3> + {isLightClient && <h4 className="selected">{t('selected')}</h4>} + </ConnectionButton> + </div> + </ConnectionsWrapper> + + {isBraveBrowser ? ( + <BraveWarning> + <BraveIconSVG /> + <div className="brave-text"> + <Trans + defaults={t('braveText')} + components={{ b: <b />, i: <i /> }} + /> + </div> + </BraveWarning> + ) : null} + </ContentWrapper> + </ModalPadding> + </> + ); +}; diff --git a/src/modals/Networks/types.ts b/src/modals/Networks/types.ts new file mode 100644 index 0000000000..968cacbefd --- /dev/null +++ b/src/modals/Networks/types.ts @@ -0,0 +1,6 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface NetworkButtonProps { + connected?: boolean; +} diff --git a/src/modals/PoolNominations/Wrappers.ts b/src/modals/PoolNominations/Wrappers.ts new file mode 100644 index 0000000000..e043f85937 --- /dev/null +++ b/src/modals/PoolNominations/Wrappers.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @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 new file mode 100644 index 0000000000..694259460d --- /dev/null +++ b/src/modals/PoolNominations/index.tsx @@ -0,0 +1,41 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { Title } from 'library/Modal/Title'; +import { ValidatorList } from 'library/ValidatorList'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { ListWrapper } from './Wrappers'; + +export const PoolNominations = () => { + const { + config: { options }, + } = useOverlay().modal; + const { nominator, targets } = options; + const { t } = useTranslation('modals'); + + return ( + <> + <Title title={t('poolNominations')} /> + <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/modals/Settings/index.tsx b/src/modals/Settings/index.tsx new file mode 100644 index 0000000000..1a54994fda --- /dev/null +++ b/src/modals/Settings/index.tsx @@ -0,0 +1,53 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { usePlugins } from 'contexts/Plugins'; +import { Title } from 'library/Modal/Title'; +import { StatusButton } from 'library/StatusButton'; +import { ContentWrapper } from '../Networks/Wrapper'; + +export const Settings = () => { + const { plugins, togglePlugin } = usePlugins(); + const { t } = useTranslation('modals'); + + // fetch flag to disable fiat + const DISABLE_FIAT = Number(import.meta.env.VITE_DISABLE_FIAT ?? 0); + + return ( + <> + <Title title={t('settings')} /> + <ModalPadding> + <ContentWrapper> + <h4>{t('togglePlugins')}</h4> + <StatusButton + checked={plugins.includes('subscan')} + label="Subscan API" + onClick={() => togglePlugin('subscan')} + /> + <StatusButton + checked={plugins.includes('polkawatch')} + label="Polkawatch API" + onClick={() => togglePlugin('polkawatch')} + /> + {!DISABLE_FIAT && ( + <StatusButton + checked={plugins.includes('binance_spot')} + label={t('binanceApi')} + onClick={() => togglePlugin('binance_spot')} + /> + )} + + <h4>{t('toggleFeatures')}</h4> + + <StatusButton + checked={plugins.includes('tips')} + label={t('dashboardTips')} + onClick={() => togglePlugin('tips')} + /> + </ContentWrapper> + </ModalPadding> + </> + ); +}; diff --git a/src/modals/Unbond/index.tsx b/src/modals/Unbond/index.tsx new file mode 100644 index 0000000000..36d176fd1d --- /dev/null +++ b/src/modals/Unbond/index.tsx @@ -0,0 +1,245 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalNotes, ModalPadding, ModalWarnings } from '@polkadot-cloud/react'; +import { isNotZero, planckToUnit, unitToPlanck } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { getUnixTime } from 'date-fns'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBonded } from 'contexts/Bonded'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { useStaking } from 'contexts/Staking'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useTxMeta } from 'contexts/TxMeta'; +import { UnbondFeedback } from 'library/Form/Unbond/UnbondFeedback'; +import { Warning } from 'library/Form/Warning'; +import { useErasToTimeLeft } from 'library/Hooks/useErasToTimeLeft'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { timeleftAsString } from 'library/Hooks/useTimeLeft/utils'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { StaticNote } from 'modals/Utils/StaticNote'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const Unbond = () => { + const { t } = useTranslation('modals'); + const { txFees } = useTxMeta(); + const { staking } = useStaking(); + const { stats } = usePoolsConfig(); + const { activeAccount } = useActiveAccounts(); + const { notEnoughFunds } = useTxMeta(); + const { getBondedAccount } = useBonded(); + const { api, consts } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { erasToSeconds } = useErasToTimeLeft(); + const { getSignerWarnings } = useSignerWarnings(); + const { getTransferOptions } = useTransferOptions(); + const { isDepositor, selectedActivePool } = useActivePools(); + const { + setModalStatus, + setModalResize, + config: { options }, + } = useOverlay().modal; + + const { bondFor } = options; + const controller = getBondedAccount(activeAccount); + const { minNominatorBond: minNominatorBondBn } = staking; + const { minJoinBond: minJoinBondBn, minCreateBond: minCreateBondBn } = stats; + const { bondDuration } = consts; + + const bondDurationFormatted = timeleftAsString( + t, + getUnixTime(new Date()) + 1, + erasToSeconds(bondDuration), + true + ); + + let { pendingRewards } = selectedActivePool || {}; + pendingRewards = pendingRewards ?? new BigNumber(0); + pendingRewards = planckToUnit(pendingRewards, units); + + const isStaking = bondFor === 'nominator'; + const isPooling = bondFor === 'pool'; + + const allTransferOptions = getTransferOptions(activeAccount); + const { active: activeBn } = isPooling + ? allTransferOptions.pool + : allTransferOptions.nominate; + + // convert BigNumber values to number + const freeToUnbond = planckToUnit(activeBn, units); + const minJoinBond = planckToUnit(minJoinBondBn, units); + const minCreateBond = planckToUnit(minCreateBondBn, units); + const minNominatorBond = planckToUnit(minNominatorBondBn, units); + + // local bond value + const [bond, setBond] = useState<{ bond: string }>({ + bond: freeToUnbond.toString(), + }); + + // bond valid + const [bondValid, setBondValid] = useState<boolean>(false); + + // feedback errors to trigger modal resize + const [feedbackErrors, setFeedbackErrors] = useState<string[]>([]); + + // get the max amount available to unbond + const unbondToMin = isPooling + ? isDepositor() + ? BigNumber.max(freeToUnbond.minus(minCreateBond), 0) + : BigNumber.max(freeToUnbond.minus(minJoinBond), 0) + : BigNumber.max(freeToUnbond.minus(minNominatorBond), 0); + + // update bond value on task change + useEffect(() => { + setBond({ bond: unbondToMin.toString() }); + }, [freeToUnbond.toString()]); + + // tx to submit + const getTx = () => { + let tx = null; + if (!api || !activeAccount) { + return tx; + } + + const bondToSubmit = unitToPlanck(!bondValid ? '0' : bond.bond, units); + const bondAsString = bondToSubmit.isNaN() ? '0' : bondToSubmit.toString(); + + // determine tx + if (isPooling) { + tx = api.tx.nominationPools.unbond(activeAccount, bondAsString); + } else if (isStaking) { + tx = api.tx.staking.unbond(bondAsString); + } + return tx; + }; + + const signingAccount = isPooling ? activeAccount : controller; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: signingAccount, + shouldSubmit: bondValid, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const nominatorActiveBelowMin = + bondFor === 'nominator' && + isNotZero(activeBn) && + activeBn.isLessThan(minNominatorBondBn); + + const poolToMinBn = isDepositor() ? minCreateBondBn : minJoinBondBn; + const poolActiveBelowMin = + bondFor === 'pool' && activeBn.isLessThan(poolToMinBn); + + // accumulate warnings. + const warnings = getSignerWarnings( + activeAccount, + isStaking, + submitExtrinsic.proxySupported + ); + + if (pendingRewards > 0 && bondFor === 'pool') { + warnings.push(`${t('unbondingWithdraw')} ${pendingRewards} ${unit}.`); + } + if (nominatorActiveBelowMin) { + warnings.push( + t('unbondErrorBelowMinimum', { + bond: minNominatorBond, + unit, + }) + ); + } + if (poolActiveBelowMin) { + warnings.push( + t('unbondErrorBelowMinimum', { + bond: planckToUnit(poolToMinBn, units), + unit, + }) + ); + } + if (activeBn.isZero()) { + warnings.push(t('unbondErrorNoFunds', { unit })); + } + + // modal resize on form update + useEffect( + () => setModalResize(), + [bond, notEnoughFunds, feedbackErrors.length, warnings.length] + ); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded">{t('removeBond')}</h2> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <UnbondFeedback + bondFor={bondFor} + listenIsValid={(valid, errors) => { + setBondValid(valid); + setFeedbackErrors(errors); + }} + setters={[ + { + set: setBond, + current: bond, + }, + ]} + txFees={txFees} + /> + <ModalNotes withPadding> + {bondFor === 'pool' ? ( + <> + {isDepositor() ? ( + <p> + {t('notePoolDepositorMinBond', { + context: 'depositor', + bond: minCreateBond, + unit, + })} + </p> + ) : ( + <p> + {t('notePoolDepositorMinBond', { + context: 'member', + bond: minJoinBond, + unit, + })} + </p> + )} + </> + ) : null} + <StaticNote + value={bondDurationFormatted} + tKey="onceUnbonding" + valueKey="bondDurationFormatted" + deps={[bondDuration]} + /> + </ModalNotes> + </ModalPadding> + <SubmitTx + fromController={isStaking} + valid={bondValid} + {...submitExtrinsic} + /> + </> + ); +}; diff --git a/src/modals/UnbondPoolMember/index.tsx b/src/modals/UnbondPoolMember/index.tsx new file mode 100644 index 0000000000..6e8857ff72 --- /dev/null +++ b/src/modals/UnbondPoolMember/index.tsx @@ -0,0 +1,128 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ActionItem, ModalPadding, ModalWarnings } from '@polkadot-cloud/react'; +import { + greaterThanZero, + planckToUnit, + rmCommas, + unitToPlanck, +} from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { getUnixTime } from 'date-fns'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { Warning } from 'library/Form/Warning'; +import { useErasToTimeLeft } from 'library/Hooks/useErasToTimeLeft'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { timeleftAsString } from 'library/Hooks/useTimeLeft/utils'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { StaticNote } from 'modals/Utils/StaticNote'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const UnbondPoolMember = () => { + const { t } = useTranslation('modals'); + const { api, consts } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { notEnoughFunds } = useTxMeta(); + const { erasToSeconds } = useErasToTimeLeft(); + const { getSignerWarnings } = useSignerWarnings(); + const { + setModalStatus, + setModalResize, + config: { options }, + } = useOverlay().modal; + + const { bondDuration } = consts; + const { member, who } = options; + const { points } = member; + const freeToUnbond = planckToUnit(new BigNumber(rmCommas(points)), units); + + const bondDurationFormatted = timeleftAsString( + t, + getUnixTime(new Date()) + 1, + erasToSeconds(bondDuration), + true + ); + + // local bond value + const [bond, setBond] = useState<{ bond: string }>({ + bond: freeToUnbond.toString(), + }); + + // bond valid + const [bondValid, setBondValid] = useState(false); + + // unbond all validation + const isValid = (() => greaterThanZero(freeToUnbond))(); + + // update bond value on task change + useEffect(() => { + setBond({ bond: freeToUnbond.toString() }); + setBondValid(isValid); + }, [freeToUnbond.toString(), isValid]); + + useEffect(() => setModalResize(), [bond, notEnoughFunds]); + + // tx to submit + const getTx = () => { + let tx = null; + if (!api || !activeAccount) { + return tx; + } + // remove decimal errors + const bondToSubmit = unitToPlanck(!bondValid ? '0' : bond.bond, units); + const bondAsString = bondToSubmit.isNaN() ? '0' : bondToSubmit.toString(); + tx = api.tx.nominationPools.unbond(who, bondAsString); + return tx; + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: bondValid, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded">{t('unbondMemberFunds')}</h2> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <ActionItem text={`${t('unbond')} ${freeToUnbond} ${unit}`} /> + <StaticNote + value={bondDurationFormatted} + tKey="onceUnbonding" + valueKey="bondDurationFormatted" + deps={[bondDuration]} + /> + </ModalPadding> + <SubmitTx valid={bondValid} {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/modals/UnlockChunks/Chunk.tsx b/src/modals/UnlockChunks/Chunk.tsx new file mode 100644 index 0000000000..37a7472317 --- /dev/null +++ b/src/modals/UnlockChunks/Chunk.tsx @@ -0,0 +1,76 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonSubmit } from '@polkadot-cloud/react'; +import { planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { fromUnixTime } from 'date-fns'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { Countdown } from 'library/Countdown'; +import { useErasToTimeLeft } from 'library/Hooks/useErasToTimeLeft'; +import { useTimeLeft } from 'library/Hooks/useTimeLeft'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { ChunkWrapper } from './Wrappers'; + +export const Chunk = ({ chunk, bondFor, onRebond }: any) => { + const { t } = useTranslation('modals'); + + const { + networkData: { units, unit }, + network, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { activeEra } = useNetworkMetrics(); + const { isFastUnstaking } = useUnstaking(); + const { erasToSeconds } = useErasToTimeLeft(); + const { timeleft, setFromNow } = useTimeLeft(); + const isStaking = bondFor === 'nominator'; + + const { era, value } = chunk; + const left = new BigNumber(era).minus(activeEra.index); + const start = activeEra.start.multipliedBy(0.001); + const erasDuration = erasToSeconds(left); + + const dateFrom = fromUnixTime(start.toNumber()); + const dateTo = fromUnixTime(start.plus(erasDuration).toNumber()); + + // reset timer on account or network change. + useEffect(() => { + setFromNow(dateFrom, dateTo); + }, [activeAccount, network]); + + return ( + <ChunkWrapper> + <div> + <section> + <h2>{`${planckToUnit(value, units)} ${unit}`}</h2> + <h4> + {left.isLessThanOrEqualTo(0) ? ( + t('unlocked') + ) : ( + <> + {t('unlocksInEra')} {era} /  + <Countdown timeleft={timeleft.formatted} markup={false} /> + </> + )} + </h4> + </section> + {isStaking ? ( + <section> + <div> + <ButtonSubmit + text={t('rebond')} + disabled={isFastUnstaking} + onClick={() => onRebond(chunk)} + /> + </div> + </section> + ) : null} + </div> + </ChunkWrapper> + ); +}; diff --git a/src/modals/UnlockChunks/Forms.tsx b/src/modals/UnlockChunks/Forms.tsx new file mode 100644 index 0000000000..131dc0ebbf --- /dev/null +++ b/src/modals/UnlockChunks/Forms.tsx @@ -0,0 +1,179 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { + ActionItem, + ButtonSubmitInvert, + ModalWarnings, +} from '@polkadot-cloud/react'; +import { planckToUnit, rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { forwardRef, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBonded } from 'contexts/Bonded'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { SubmitTx } from 'library/SubmitTx'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { ContentWrapper } from './Wrappers'; + +export const Forms = forwardRef( + ({ setSection, unlock, task, incrementCalculateHeight }: any, ref: any) => { + const { t } = useTranslation('modals'); + const { api, consts } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { removeFavorite: removeFavoritePool } = usePoolsConfig(); + const { membership } = usePoolMemberships(); + const { selectedActivePool } = useActivePools(); + const { removeFromBondedPools } = useBondedPools(); + const { removePoolMember } = usePoolMembers(); + const { + setModalStatus, + config: { options }, + } = useOverlay().modal; + const { getBondedAccount } = useBonded(); + const { getSignerWarnings } = useSignerWarnings(); + + const { bondFor, poolClosure } = options || {}; + const { historyDepth } = consts; + const controller = getBondedAccount(activeAccount); + + const isStaking = bondFor === 'nominator'; + const isPooling = bondFor === 'pool'; + + // valid to submit transaction + const [valid, setValid] = useState<boolean>( + unlock?.value?.toNumber() > 0 ?? false + ); + + // tx to submit + const getTx = () => { + let tx = null; + if (!valid || !api) { + return tx; + } + // rebond is only available when staking directly. + if (task === 'rebond' && isStaking) { + tx = api.tx.staking.rebond(unlock.value.toNumber()); + } else if (task === 'withdraw' && isStaking) { + tx = api.tx.staking.withdrawUnbonded(historyDepth.toString()); + } else if (task === 'withdraw' && isPooling && selectedActivePool) { + tx = api.tx.nominationPools.withdrawUnbonded( + activeAccount, + historyDepth.toString() + ); + } + return tx; + }; + const signingAccount = isStaking ? controller : activeAccount; + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: signingAccount, + shouldSubmit: valid, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => { + // if pool is being closed, remove from static lists + if (poolClosure) { + removeFavoritePool(selectedActivePool?.addresses?.stash ?? ''); + removeFromBondedPools(selectedActivePool?.id ?? 0); + } + + // if no more bonded funds from pool, remove from poolMembers list + if (bondFor === 'pool') { + const points = membership?.points ? rmCommas(membership.points) : 0; + const bonded = planckToUnit(new BigNumber(points), units); + if (bonded.isZero()) { + removePoolMember(activeAccount); + } + } + }, + }); + + const value = unlock?.value ?? new BigNumber(0); + + const warnings = getSignerWarnings( + activeAccount, + isStaking, + submitExtrinsic.proxySupported + ); + + // Ensure unlock value is valid. + useEffect(() => { + setValid(unlock?.value?.toNumber() > 0 ?? false); + }, [unlock]); + + // Trigger modal resize when commission options are enabled / disabled. + useEffect(() => { + incrementCalculateHeight(); + }, [valid]); + + return ( + <ContentWrapper> + <div ref={ref}> + <div className="padding"> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <div style={{ marginBottom: '2rem' }}> + {task === 'rebond' && ( + <> + <ActionItem + text={`${t('rebond')} ${planckToUnit( + value, + units + )} ${unit}`} + /> + <p>{t('rebondSubtitle')}</p> + </> + )} + {task === 'withdraw' && ( + <> + <ActionItem + text={`${t('withdraw')} ${planckToUnit( + value, + units + )} ${unit}`} + /> + <p>{t('withdrawSubtitle')}</p> + </> + )} + </div> + </div> + <SubmitTx + fromController={isStaking} + valid={valid} + buttons={[ + <ButtonSubmitInvert + key="button_back" + text={t('back')} + iconLeft={faChevronLeft} + iconTransform="shrink-1" + onClick={() => setSection(0)} + />, + ]} + {...submitExtrinsic} + /> + </div> + </ContentWrapper> + ); + } +); diff --git a/src/modals/UnlockChunks/Overview.tsx b/src/modals/UnlockChunks/Overview.tsx new file mode 100644 index 0000000000..cdd235215e --- /dev/null +++ b/src/modals/UnlockChunks/Overview.tsx @@ -0,0 +1,147 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCheckCircle, faClock } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ButtonSubmit, ModalNotes } from '@polkadot-cloud/react'; +import { planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { getUnixTime } from 'date-fns'; +import { forwardRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useErasToTimeLeft } from 'library/Hooks/useErasToTimeLeft'; +import { timeleftAsString } from 'library/Hooks/useTimeLeft/utils'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { StatWrapper, StatsWrapper } from 'library/Modal/Wrappers'; +import { StaticNote } from 'modals/Utils/StaticNote'; +import type { AnyJson } from 'types'; +import { useNetwork } from 'contexts/Network'; +import { Chunk } from './Chunk'; +import { ContentWrapper } from './Wrappers'; + +export const Overview = forwardRef( + ({ unlocking, bondFor, setSection, setUnlock, setTask }: any, ref: any) => { + const { t } = useTranslation('modals'); + const { consts } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeEra } = useNetworkMetrics(); + const { bondDuration } = consts; + const { isFastUnstaking } = useUnstaking(); + const { erasToSeconds } = useErasToTimeLeft(); + + const bondDurationFormatted = timeleftAsString( + t, + getUnixTime(new Date()) + 1, + erasToSeconds(bondDuration), + true + ); + + const isStaking = bondFor === 'nominator'; + + let withdrawAvailable = new BigNumber(0); + let totalUnbonding = new BigNumber(0); + for (const c of unlocking) { + const { era, value } = c; + const left = new BigNumber(era).minus(activeEra.index); + + totalUnbonding = totalUnbonding.plus(value); + if (left.isLessThanOrEqualTo(0)) { + withdrawAvailable = withdrawAvailable.plus(value); + } + } + + const onRebondHandler = (chunk: AnyJson) => { + setTask('rebond'); + setUnlock(chunk); + setSection(1); + }; + + return ( + <ContentWrapper> + <div className="padding" ref={ref}> + <StatsWrapper> + <StatWrapper> + <div className="inner"> + <h4> + <FontAwesomeIcon icon={faCheckCircle} className="icon" />{' '} + {t('unlocked')} + </h4> + <h2> + {planckToUnit(withdrawAvailable, units) + .decimalPlaces(3) + .toFormat()}{' '} + {unit} + </h2> + </div> + </StatWrapper> + <StatWrapper> + <div className="inner"> + <h4> + <FontAwesomeIcon icon={faClock} className="icon" />{' '} + {t('unbonding')} + </h4> + <h2> + {planckToUnit(totalUnbonding.minus(withdrawAvailable), units) + .decimalPlaces(3) + .toFormat()}{' '} + {unit} + </h2> + </div> + </StatWrapper> + <StatWrapper> + <div className="inner"> + <h4>{t('total')}</h4> + <h2> + {planckToUnit(totalUnbonding, units) + .decimalPlaces(3) + .toFormat()}{' '} + {unit} + </h2> + </div> + </StatWrapper> + </StatsWrapper> + + {withdrawAvailable.toNumber() > 0 && ( + <div style={{ margin: '1rem 0 0.5rem 0' }}> + <ButtonSubmit + disabled={isFastUnstaking} + text={t('withdrawUnlocked')} + onClick={() => { + setTask('withdraw'); + setUnlock({ + era: 0, + value: withdrawAvailable, + }); + setSection(1); + }} + /> + </div> + )} + + {unlocking.map((chunk: any, i: number) => ( + <Chunk + key={`unlock_chunk_${i}`} + chunk={chunk} + bondFor={bondFor} + onRebond={onRebondHandler} + /> + ))} + <ModalNotes withPadding> + <StaticNote + value={bondDurationFormatted} + tKey="unlockTake" + valueKey="bondDurationFormatted" + deps={[bondDuration]} + /> + <p> {isStaking ? ` ${t('rebondUnlock')}` : null}</p> + {!isStaking ? <p>{t('unlockChunk')}</p> : null} + </ModalNotes> + </div> + </ContentWrapper> + ); + } +); diff --git a/src/modals/UnlockChunks/Wrappers.ts b/src/modals/UnlockChunks/Wrappers.ts new file mode 100644 index 0000000000..304d36900b --- /dev/null +++ b/src/modals/UnlockChunks/Wrappers.ts @@ -0,0 +1,59 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const ContentWrapper = styled.div` + border-radius: 1rem; + display: flex; + flex-flow: column nowrap; + flex-basis: 50%; + flex: 1; + + .padding { + padding: 0 1rem; + } + + > div:last-child { + margin-bottom: 0; + } +`; + +export const ChunkWrapper = styled.div<any>` + flex: 1; + display: flex; + flex-flow: column wrap; + margin-top: 1.25rem; + + > div { + background: var(--button-primary-background); + display: flex; + flex-flow: row wrap; + width: 100%; + padding: 0.5rem 1rem; + border-radius: 1rem; + + > section { + display: flex; + flex-flow: column wrap; + justify-content: flex-end; + padding: 0.75rem 0; + + &:first-child { + flex-grow: 1; + } + &:last-child { + justify-content: center; + } + } + } + + h2 { + margin: 0; + } + + h4 { + color: var(--text-color-secondary); + margin: 0.75rem 0 0 0; + } +`; diff --git a/src/modals/UnlockChunks/index.tsx b/src/modals/UnlockChunks/index.tsx new file mode 100644 index 0000000000..b4f124e0df --- /dev/null +++ b/src/modals/UnlockChunks/index.tsx @@ -0,0 +1,141 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + ModalFixedTitle, + ModalMotionTwoSection, + ModalSection, +} from '@polkadot-cloud/react'; +import { setStateWithRef } from '@polkadot-cloud/utils'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBalances } from 'contexts/Balances'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { Title } from 'library/Modal/Title'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Forms } from './Forms'; +import { Overview } from './Overview'; + +export const UnlockChunks = () => { + const { t } = useTranslation('modals'); + const { + config: { options }, + setModalHeight, + } = useOverlay().modal; + const { notEnoughFunds } = useTxMeta(); + const { getStashLedger } = useBalances(); + const { activeAccount } = useActiveAccounts(); + const { getPoolUnlocking } = useActivePools(); + const { bondFor } = options || {}; + + // get the unlocking per bondFor + const getUnlocking = () => { + let unlocking = []; + let ledger; + switch (bondFor) { + case 'pool': + unlocking = getPoolUnlocking(); + break; + default: + ledger = getStashLedger(activeAccount); + unlocking = ledger.unlocking; + } + return unlocking; + }; + + const unlocking = getUnlocking(); + + // active modal section + const [section, setSectionState] = useState(0); + const sectionRef = useRef(section); + + const setSection = (s: number) => { + setStateWithRef(s, setSectionState, sectionRef); + }; + + // modal task + const [task, setTask] = useState<string | null>(null); + + // unlock value of interest + const [unlock, setUnlock] = useState(null); + + // counter to trigger modal height calculation + const [calculateHeight, setCalculateHeight] = useState<number>(0); + const incrementCalculateHeight = () => + setCalculateHeight(calculateHeight + 1); + + // refs for wrappers + const headerRef = useRef<HTMLDivElement>(null); + const overviewRef = useRef<HTMLDivElement>(null); + const formsRef = useRef<HTMLDivElement>(null); + + const getModalHeight = () => { + let h = headerRef.current?.clientHeight ?? 0; + + if (sectionRef.current === 0) { + h += overviewRef.current?.clientHeight ?? 0; + } else { + h += formsRef.current?.clientHeight ?? 0; + } + return h; + }; + + const resizeCallback = () => { + setModalHeight(getModalHeight()); + }; + + // resize modal on state change + useEffect(() => { + setModalHeight(getModalHeight()); + }, [task, calculateHeight, notEnoughFunds, sectionRef.current, unlocking]); + + // resize this modal on window resize + useEffect(() => { + window.addEventListener('resize', resizeCallback); + return () => { + window.removeEventListener('resize', resizeCallback); + }; + }, []); + + return ( + <ModalSection type="carousel"> + <ModalFixedTitle ref={headerRef}> + <Title title={t('unlocks')} fixed /> + </ModalFixedTitle> + <ModalMotionTwoSection + animate={sectionRef.current === 0 ? 'home' : 'next'} + transition={{ + duration: 0.5, + type: 'spring', + bounce: 0.1, + }} + variants={{ + home: { + left: 0, + }, + next: { + left: '-100%', + }, + }} + > + <Overview + unlocking={unlocking} + bondFor={bondFor} + setSection={setSection} + setUnlock={setUnlock} + setTask={setTask} + ref={overviewRef} + /> + <Forms + incrementCalculateHeight={incrementCalculateHeight} + setSection={setSection} + unlock={unlock} + task={task} + ref={formsRef} + /> + </ModalMotionTwoSection> + </ModalSection> + ); +}; diff --git a/src/modals/Unstake/index.tsx b/src/modals/Unstake/index.tsx new file mode 100644 index 0000000000..ac9a88ca2a --- /dev/null +++ b/src/modals/Unstake/index.tsx @@ -0,0 +1,152 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ActionItem, ModalPadding, ModalWarnings } from '@polkadot-cloud/react'; +import { + greaterThanZero, + planckToUnit, + unitToPlanck, +} from '@polkadot-cloud/utils'; +import { getUnixTime } from 'date-fns'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBonded } from 'contexts/Bonded'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { Warning } from 'library/Form/Warning'; +import { useBatchCall } from 'library/Hooks/useBatchCall'; +import { useErasToTimeLeft } from 'library/Hooks/useErasToTimeLeft'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { timeleftAsString } from 'library/Hooks/useTimeLeft/utils'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { StaticNote } from 'modals/Utils/StaticNote'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const Unstake = () => { + const { t } = useTranslation('modals'); + const { newBatchCall } = useBatchCall(); + const { notEnoughFunds } = useTxMeta(); + const { activeAccount } = useActiveAccounts(); + const { api, consts } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { erasToSeconds } = useErasToTimeLeft(); + const { getSignerWarnings } = useSignerWarnings(); + const { getTransferOptions } = useTransferOptions(); + const { setModalStatus, setModalResize } = useOverlay().modal; + const { getBondedAccount, getAccountNominations } = useBonded(); + + const controller = getBondedAccount(activeAccount); + const nominations = getAccountNominations(activeAccount); + const { bondDuration } = consts; + const allTransferOptions = getTransferOptions(activeAccount); + const { active } = allTransferOptions.nominate; + + const bondDurationFormatted = timeleftAsString( + t, + getUnixTime(new Date()) + 1, + erasToSeconds(bondDuration), + true + ); + + // convert BigNumber values to number + const freeToUnbond = planckToUnit(active, units); + + // local bond value + const [bond, setBond] = useState<{ bond: string }>({ + bond: freeToUnbond.toString(), + }); + + // bond valid + const [bondValid, setBondValid] = useState(false); + + // unbond all validation + const isValid = (() => greaterThanZero(freeToUnbond))(); + + // update bond value on task change + useEffect(() => { + setBond({ bond: freeToUnbond.toString() }); + setBondValid(isValid); + }, [freeToUnbond.toString(), isValid]); + + // modal resize on form update + useEffect(() => setModalResize(), [bond, notEnoughFunds]); + + // tx to submit + const getTx = () => { + const tx = null; + if (!api || !activeAccount) { + return tx; + } + // remove decimal errors + const bondToSubmit = unitToPlanck( + String(!bondValid ? '0' : bond.bond), + units + ); + const bondAsString = bondToSubmit.isNaN() ? '0' : bondToSubmit.toString(); + + if (!bondAsString) { + return api.tx.staking.chill(); + } + const txs = [api.tx.staking.chill(), api.tx.staking.unbond(bondAsString)]; + return newBatchCall(txs, controller); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: controller, + shouldSubmit: bondValid, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const warnings = getSignerWarnings( + activeAccount, + true, + submitExtrinsic.proxySupported + ); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded">{t('unstake')} </h2> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + {greaterThanZero(freeToUnbond) ? ( + <ActionItem + text={t('unstakeUnbond', { + bond: freeToUnbond.toFormat(), + unit, + })} + /> + ) : null} + {nominations.length > 0 && ( + <ActionItem + text={t('unstakeStopNominating', { count: nominations.length })} + /> + )} + <StaticNote + value={bondDurationFormatted} + tKey="onceUnbonding" + valueKey="bondDurationFormatted" + deps={[bondDuration]} + /> + </ModalPadding> + <SubmitTx fromController valid={bondValid} {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/modals/UpdateController/Switch/Wrappers.ts b/src/modals/UpdateController/Switch/Wrappers.ts new file mode 100644 index 0000000000..105de3bec6 --- /dev/null +++ b/src/modals/UpdateController/Switch/Wrappers.ts @@ -0,0 +1,68 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const StyledSelect = styled.div` + position: relative; + width: 100%; + height: auto; + overflow: hidden; + + .label { + margin: 0.25rem 0 0.75rem 0; + } + .current { + flex: 1; + display: flex; + align-items: center; + margin-bottom: 1rem; + + > span { + color: var(--text-color-secondary); + margin: 0 0.75rem; + opacity: 0.5; + } + } + + /* input element of dropdown */ + .input-wrap { + border-bottom: 1px solid var(--border-primary-color); + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 0.25rem 0 0 0; + margin: 0.25rem 0rem 0 0rem; + flex: 1; + + &.selected { + margin: 0; + padding: 0.1rem 0.75rem; + } + } + + /* input element of dropdown */ + .input { + border: none; + padding-left: 0.75rem; + flex: 1; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } +`; + +export const StyledController = styled.button<any>` + color: var(--text-color-primary); + border: none; + position: absolute; + right: 0.5rem; + top: 0.4rem; + width: 2.2rem; + height: 2.2rem; + display: flex; + flex-flow: column wrap; + justify-content: center; + align-items: center; + border-radius: 0.5rem; +`; diff --git a/src/modals/UpdateController/Switch/index.tsx b/src/modals/UpdateController/Switch/index.tsx new file mode 100644 index 0000000000..fa9c33bde3 --- /dev/null +++ b/src/modals/UpdateController/Switch/index.tsx @@ -0,0 +1,50 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faAnglesRight } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { remToUnit } from '@polkadot-cloud/utils'; +import { Polkicon } from '@polkadot-cloud/react'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import type { AccountDropdownProps } from '../../../library/Form/types'; +import { StyledSelect } from './Wrappers'; + +export const Switch = ({ current, to }: AccountDropdownProps) => { + const { getAccount } = useImportedAccounts(); + const toAccount = getAccount(to); + + return ( + <StyledSelect> + <div> + <div className="current"> + <div className="input-wrap selected"> + {toAccount !== null && ( + <Polkicon + address={current?.address ?? ''} + size={remToUnit('2rem')} + /> + )} + <input className="input" disabled value={current?.name ?? ''} /> + </div> + <span> + <FontAwesomeIcon icon={faAnglesRight} /> + </span> + + <div className="input-wrap selected"> + {current?.active ? ( + <Polkicon + address={toAccount?.address ?? ''} + size={remToUnit('2rem')} + /> + ) : undefined} + <input + className="input" + disabled + value={toAccount?.address ? toAccount?.name : '...'} + /> + </div> + </div> + </div> + </StyledSelect> + ); +}; diff --git a/src/modals/UpdateController/Wrapper.ts b/src/modals/UpdateController/Wrapper.ts new file mode 100644 index 0000000000..48b0652194 --- /dev/null +++ b/src/modals/UpdateController/Wrapper.ts @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + display: flex; + flex-flow: column wrap; + padding: 0rem 0.25rem; + width: 100%; + + .form { + width: 100%; + height: 250px; + overflow: hidden; + } +`; diff --git a/src/modals/UpdateController/index.tsx b/src/modals/UpdateController/index.tsx new file mode 100644 index 0000000000..b4de652bed --- /dev/null +++ b/src/modals/UpdateController/index.tsx @@ -0,0 +1,86 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding, ModalWarnings } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBonded } from 'contexts/Bonded'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useEffect } from 'react'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { Switch } from './Switch'; +import { Wrapper } from './Wrapper'; + +export const UpdateController = () => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { notEnoughFunds } = useTxMeta(); + const { getBondedAccount } = useBonded(); + const { activeAccount } = useActiveAccounts(); + const { getAccount } = useImportedAccounts(); + const { getSignerWarnings } = useSignerWarnings(); + const { setModalStatus, setModalResize } = useOverlay().modal; + + const controller = getBondedAccount(activeAccount); + const account = getAccount(controller); + + // tx to submit + const getTx = () => { + let tx = null; + if (!api) { + return tx; + } + tx = api.tx.staking.setController(); + return tx; + }; + + useEffect(() => setModalResize(), [notEnoughFunds]); + + // handle extrinsic + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title unbounded">{t('changeControllerAccount')}</h2> + <Wrapper> + <div style={{ width: '100%' }}> + <div style={{ marginBottom: '1.5rem' }}> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + </div> + <Switch current={account} to={activeAccount} /> + </div> + </Wrapper> + </ModalPadding> + <SubmitTx valid={activeAccount !== null} {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/modals/UpdatePayee/index.tsx b/src/modals/UpdatePayee/index.tsx new file mode 100644 index 0000000000..2b4865c34f --- /dev/null +++ b/src/modals/UpdatePayee/index.tsx @@ -0,0 +1,167 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ModalPadding, ModalWarnings } from '@polkadot-cloud/react'; +import { isValidAddress } from '@polkadot-cloud/utils'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBonded } from 'contexts/Bonded'; +import type { PayeeConfig, PayeeOptions } from 'contexts/Setup/types'; +import { useStaking } from 'contexts/Staking'; +import { Warning } from 'library/Form/Warning'; +import { usePayeeConfig } from 'library/Hooks/usePayeeConfig'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Title } from 'library/Modal/Title'; +import { PayeeInput } from 'library/PayeeInput'; +import { SelectItems } from 'library/SelectItems'; +import { SelectItem } from 'library/SelectItems/Item'; +import { SubmitTx } from 'library/SubmitTx'; +import type { MaybeAddress } from 'types'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const UpdatePayee = () => { + const { t } = useTranslation('modals'); + const { api } = useApi(); + const { staking } = useStaking(); + const { activeAccount } = useActiveAccounts(); + const { notEnoughFunds } = useTxMeta(); + const { getBondedAccount } = useBonded(); + const { getPayeeItems } = usePayeeConfig(); + const { getSignerWarnings } = useSignerWarnings(); + const { setModalStatus, setModalResize } = useOverlay().modal; + + const controller = getBondedAccount(activeAccount); + const { payee } = staking; + + const DefaultSelected: PayeeConfig = { + destination: null, + account: null, + }; + + // Store the current user-inputted custom payout account. + const [account, setAccount] = useState<MaybeAddress>(payee.account); + + // Store the currently selected payee option. + const [selected, setSelected]: any = useState<PayeeConfig>(DefaultSelected); + + // update setup progress with payee config. + const handleChangeDestination = (destination: PayeeOptions) => { + setSelected({ + destination, + account: isValidAddress(account || '') ? account : null, + }); + }; + + // update setup progress with payee account. + const handleChangeAccount = (newAccount: MaybeAddress) => { + setSelected({ + destination: selected?.destination ?? null, + account: newAccount, + }); + }; + + // determine whether this section is completed. + const isComplete = () => + selected.destination !== null && + !(selected.destination === 'Account' && selected.account === null); + + // Tx to submit. + const getTx = () => { + let tx = null; + + if (!api) { + return tx; + } + const payeeToSubmit = !isComplete() + ? 'Staked' + : selected.destination === 'Account' + ? { + Account: selected.account, + } + : selected.destination; + + tx = api.tx.staking.setPayee(payeeToSubmit); + return tx; + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: controller, + shouldSubmit: isComplete(), + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => {}, + }); + + // Reset selected value on account change. + useEffect(() => { + setSelected(DefaultSelected); + }, [activeAccount]); + + // Inject default value after component mount. + useEffect(() => { + const initialSelected = getPayeeItems(true).find( + (item) => item.value === payee.destination + ); + setSelected( + initialSelected + ? { + destination: initialSelected.value, + account, + } + : DefaultSelected + ); + }, []); + + useEffect(() => setModalResize(), [notEnoughFunds]); + + const warnings = getSignerWarnings( + activeAccount, + true, + submitExtrinsic.proxySupported + ); + + return ( + <> + <Title + title={t('updatePayoutDestination')} + helpKey="Payout Destination" + /> + <ModalPadding style={{ paddingBottom: 0 }}> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + <div style={{ width: '100%', padding: '0 0.5rem' }}> + <PayeeInput + payee={selected} + account={account} + setAccount={setAccount} + handleChange={handleChangeAccount} + /> + </div> + <SelectItems> + {getPayeeItems(true).map((item) => ( + <SelectItem + key={`payee_option_${item.value}`} + account={account} + setAccount={setAccount} + selected={selected.destination === item.value} + onClick={() => handleChangeDestination(item.value)} + {...item} + /> + ))} + </SelectItems> + </ModalPadding> + <SubmitTx fromController valid={isComplete()} {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/modals/UpdateReserve/index.tsx b/src/modals/UpdateReserve/index.tsx new file mode 100644 index 0000000000..07db63a5a5 --- /dev/null +++ b/src/modals/UpdateReserve/index.tsx @@ -0,0 +1,153 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faLock } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + ButtonHelp, + ButtonPrimaryInvert, + ModalPadding, +} from '@polkadot-cloud/react'; +import { planckToUnit, unitToPlanck } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import Slider from 'rc-slider'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { CardHeaderWrapper } from 'library/Card/Wrappers'; +import { Close } from 'library/Modal/Close'; +import { Title } from 'library/Modal/Title'; +import { SliderWrapper } from 'modals/ManagePool/Wrappers'; +import 'rc-slider/assets/index.css'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const UpdateReserve = () => { + const { t } = useTranslation('modals'); + const { + network, + networkData: { units, unit }, + } = useNetwork(); + const { openHelp } = useHelp(); + const { setModalStatus } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { accountHasSigner } = useImportedAccounts(); + const { feeReserve, setFeeReserveBalance, getTransferOptions } = + useTransferOptions(); + + const { edReserved } = getTransferOptions(activeAccount); + const minReserve = planckToUnit(edReserved, units); + const maxReserve = minReserve.plus( + ['polkadot', 'westend'].includes(network) ? 3 : 1 + ); + + const [sliderReserve, setSliderReserve] = useState<number>( + planckToUnit(feeReserve, units).plus(minReserve).decimalPlaces(3).toNumber() + ); + + const sliderProps = { + trackStyle: { + backgroundColor: 'var(--accent-color-primary)', + }, + handleStyle: { + backgroundColor: 'var(--background-primary)', + borderColor: 'var(--accent-color-primary)', + opacity: 1, + }, + }; + + const handleChange = (val: BigNumber) => { + // deduct ED from reserve amount. + val = val.decimalPlaces(3); + const actualReserve = BigNumber.max(val.minus(minReserve), 0).toNumber(); + const actualReservePlanck = unitToPlanck(actualReserve.toString(), units); + setSliderReserve(val.decimalPlaces(3).toNumber()); + setFeeReserveBalance(actualReservePlanck); + }; + + return ( + <> + <Close /> + <ModalPadding> + <Title + title={t('reserveBalance')} + helpKey="Reserve Balance" + style={{ padding: '0.5rem 0 0 0' }} + /> + <SliderWrapper style={{ marginTop: '1rem' }}> + <p>{t('reserveText', { unit })}</p> + <div> + <div className="slider no-value"> + <Slider + min={0} + max={maxReserve.toNumber()} + value={sliderReserve} + step={0.01} + onChange={(val) => { + if (typeof val === 'number' && val >= minReserve.toNumber()) { + handleChange(new BigNumber(val)); + } + }} + {...sliderProps} + /> + </div> + </div> + + <div className="stats"> + <CardHeaderWrapper> + <h4> + {t('reserveForExistentialDeposit')} + <FontAwesomeIcon + icon={faLock} + transform="shrink-3" + style={{ marginLeft: '0.5rem' }} + /> + </h4> + <h2> + {minReserve.isZero() ? ( + <> + {t('none')} + <ButtonHelp + onClick={() => + openHelp('Reserve Balance For Existential Deposit') + } + style={{ marginLeft: '0.65rem' }} + /> + </> + ) : ( + `${minReserve.decimalPlaces(4).toString()} ${unit}` + )} + </h2> + </CardHeaderWrapper> + + <CardHeaderWrapper> + <h4>{t('reserveForTxFees')}</h4> + <h2> + {BigNumber.max( + new BigNumber(sliderReserve) + .minus(minReserve) + .decimalPlaces(4) + .toString(), + 0 + ).toString()} +   + {unit} + </h2> + </CardHeaderWrapper> + </div> + + <div className="done"> + <ButtonPrimaryInvert + text={t('done')} + onClick={() => setModalStatus('closing')} + disabled={!accountHasSigner(activeAccount)} + /> + </div> + </SliderWrapper> + </ModalPadding> + </> + ); +}; diff --git a/src/modals/Utils/StaticNote.tsx b/src/modals/Utils/StaticNote.tsx new file mode 100644 index 0000000000..d63e7f227b --- /dev/null +++ b/src/modals/Utils/StaticNote.tsx @@ -0,0 +1,33 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { AnyJson } from 'types'; + +interface StaticNoteProps { + value: string; + tKey: string; + valueKey: string; + deps?: AnyJson[]; +} + +// Static notes store a single piece of text that is not updated after the initial render unless +// `deps` change. Deps should only change when syncing is complete. +export const StaticNote = ({ + value, + tKey, + valueKey, + deps = [], +}: StaticNoteProps) => { + const { t } = useTranslation('modals'); + + // store the next to be displayed. + const [staticText, setStaticText] = useState<string>(value); + + useEffect(() => { + setStaticText(value); + }, [...deps]); + + return <p>{t(tKey, { [valueKey]: staticText })}</p>; +}; diff --git a/src/modals/ValidatorGeo/index.tsx b/src/modals/ValidatorGeo/index.tsx new file mode 100644 index 0000000000..bde83a430b --- /dev/null +++ b/src/modals/ValidatorGeo/index.tsx @@ -0,0 +1,147 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp, Polkicon } from '@polkadot-cloud/react'; +import { ellipsisFn } from '@polkadot-cloud/utils'; +import { useEffect, useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; +import { GeoDonut } from 'library/Graphs/GeoDonut'; +import { formatSize } from 'library/Graphs/Utils'; +import { GraphWrapper } from 'library/Graphs/Wrapper'; +import { useSize } from 'library/Hooks/useSize'; +import { Title } from 'library/Modal/Title'; +import { StatusLabel } from 'library/StatusLabel'; +import type { ValidatorDetail } from '@polkawatch/ddp-client'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { PluginLabel } from 'library/PluginLabel'; +import { usePolkawatchApi } from 'contexts/Plugins/Polkawatch'; +import { usePlugins } from 'contexts/Plugins'; + +export const ValidatorGeo = () => { + const { t } = useTranslation('modals'); + const { pwApi, networkSupported } = usePolkawatchApi(); + const { options } = useOverlay().modal.config; + const { address, identity } = options; + const { openHelp } = useHelp(); + + const ref = useRef<HTMLDivElement>(null); + const size = useSize(ref.current); + const { height, minHeight } = formatSize(size, 300); + const [pwData, setPwData] = useState({} as ValidatorDetail); + const [analyticsAvailable, setAnalyticsAvailable] = useState(true); + const { pluginEnabled } = usePlugins(); + + const enabled = pluginEnabled('polkawatch'); + + // In Small Screens we will display the most relevant chart. + // For now, we are not going to complicate the UI. + const isSmallScreen = window.innerWidth <= 650; + const chartWidth = '330px'; + + useEffect(() => { + if (networkSupported && enabled) + pwApi + .ddpIpfsValidatorDetail({ + lastDays: 60, + validator: address, + validationType: 'public', + }) + .then((response) => { + setAnalyticsAvailable(true); + setPwData(response.data); + }) + .catch(() => setAnalyticsAvailable(false)); + else setAnalyticsAvailable(false); + return () => {}; + }, [pwApi, address]); + + return ( + <> + <Title title={t('validatorDecentralization')} /> + <div className="header"> + <Polkicon address={address} size={33} /> + <h2> +    + {identity === null ? ellipsisFn(address) : identity} + </h2> + </div> + <div + className="body" + style={{ position: 'relative', marginTop: '0.5rem' }} + > + <PluginLabel plugin="polkawatch" /> + <CardWrapper + className="transparent" + style={{ + margin: '0 0 0 0.5rem', + height: 350, + }} + > + <CardHeaderWrapper $withMargin> + <h4> + {t('rewardsByCountryAndNetwork')}{' '} + <ButtonHelp + marginLeft + onClick={() => openHelp('Rewards By Country And Network')} + /> + </h4> + </CardHeaderWrapper> + <div + ref={ref} + style={{ + minHeight, + display: 'flex', + justifyContent: 'space-evenly', + }} + > + {!enabled || analyticsAvailable ? ( + <StatusLabel + status="active_service" + statusFor="polkawatch" + title={t('polkawatchDisabled')} + /> + ) : ( + <StatusLabel + status="no_analytic_data" + title={ + networkSupported + ? t('decentralizationAnalyticsNotAvailable') + : t('decentralizationAnalyticsNotSupported') + } + /> + )} + <GraphWrapper + style={{ + height: `${height}px`, + }} + > + <GeoDonut + title={t('rewards')} + series={pwData.topCountryDistributionChart} + height={`${height}px`} + width={chartWidth} + /> + </GraphWrapper> + + <div style={{ display: isSmallScreen ? 'none' : 'block' }}> + <GraphWrapper + style={{ + height: `${height}px`, + }} + > + <GeoDonut + title={t('rewards')} + series={pwData.topNetworkDistributionChart} + height={`${height}px`} + width={chartWidth} + /> + </GraphWrapper> + </div> + </div> + </CardWrapper> + </div> + </> + ); +}; diff --git a/src/modals/ValidatorMetrics/index.tsx b/src/modals/ValidatorMetrics/index.tsx new file mode 100644 index 0000000000..2cba755735 --- /dev/null +++ b/src/modals/ValidatorMetrics/index.tsx @@ -0,0 +1,143 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp, ModalPadding, Polkicon } from '@polkadot-cloud/react'; +import { ellipsisFn, planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useStaking } from 'contexts/Staking'; +import { useSubscan } from 'contexts/Plugins/Subscan'; +import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; +import { EraPoints as EraPointsGraph } from 'library/Graphs/EraPoints'; +import { formatSize } from 'library/Graphs/Utils'; +import { GraphWrapper } from 'library/Graphs/Wrapper'; +import { useSize } from 'library/Hooks/useSize'; +import { Title } from 'library/Modal/Title'; +import { StatWrapper, StatsWrapper } from 'library/Modal/Wrappers'; +import { StatusLabel } from 'library/StatusLabel'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { PluginLabel } from 'library/PluginLabel'; +import { useNetwork } from 'contexts/Network'; + +export const ValidatorMetrics = () => { + const { t } = useTranslation('modals'); + const { + networkData: { units, unit }, + } = useNetwork(); + const { options } = useOverlay().modal.config; + const { address, identity } = options; + const { fetchEraPoints }: any = useSubscan(); + const { activeEra } = useNetworkMetrics(); + const { + eraStakers: { stakers }, + } = useStaking(); + const { openHelp } = useHelp(); + + // is the validator in the active era + const validatorInEra = stakers.find((s) => s.address === address) || null; + + let validatorOwnStake = new BigNumber(0); + let otherStake = new BigNumber(0); + if (validatorInEra) { + const { others, own } = validatorInEra; + + others.forEach(({ value }) => { + otherStake = otherStake.plus(value); + }); + if (own) { + validatorOwnStake = new BigNumber(own); + } + } + const [list, setList] = useState([]); + + const ref = useRef<HTMLDivElement>(null); + const size = useSize(ref.current); + const { width, height, minHeight } = formatSize(size, 300); + + const handleEraPoints = async () => { + setList(await fetchEraPoints(address, activeEra.index)); + }; + + useEffect(() => { + handleEraPoints(); + }, []); + + const stats = [ + { + label: t('selfStake'), + value: `${planckToUnit(validatorOwnStake, units).toFormat()} ${unit}`, + help: 'Self Stake', + }, + { + label: t('nominatorStake'), + value: `${planckToUnit(otherStake, units).toFormat()} ${unit}`, + help: 'Nominator Stake', + }, + ]; + return ( + <> + <Title title={t('validatorMetrics')} /> + <div className="header"> + <Polkicon address={address} size={33} /> + <h2> +    + {identity === null ? ellipsisFn(address) : identity} + </h2> + </div> + + <ModalPadding horizontalOnly> + <StatsWrapper> + {stats.map((s, i) => ( + <StatWrapper key={`metrics_stat_${i}`}> + <div className="inner"> + <h4> + {s.label}{' '} + <ButtonHelp marginLeft onClick={() => openHelp(s.help)} /> + </h4> + <h2>{s.value}</h2> + </div> + </StatWrapper> + ))} + </StatsWrapper> + </ModalPadding> + <div + className="body" + style={{ position: 'relative', marginTop: '0.5rem' }} + > + <PluginLabel plugin="subscan" /> + <CardWrapper + className="transparent" + style={{ + margin: '0 0 0 0.5rem', + height: 350, + }} + > + <CardHeaderWrapper $withMargin> + <h4> + {t('recentEraPoints')}{' '} + <ButtonHelp marginLeft onClick={() => openHelp('Era Points')} /> + </h4> + </CardHeaderWrapper> + <div ref={ref} style={{ minHeight }}> + <StatusLabel + status="active_service" + statusFor="subscan" + title={t('subscanDisabled')} + /> + <GraphWrapper + style={{ + height: `${height}px`, + width: `${width}px`, + }} + > + <EraPointsGraph items={list} height={250} /> + </GraphWrapper> + </div> + </CardWrapper> + </div> + </> + ); +}; diff --git a/src/modals/WithdrawPoolMember/index.tsx b/src/modals/WithdrawPoolMember/index.tsx new file mode 100644 index 0000000000..14d3bf7b0c --- /dev/null +++ b/src/modals/WithdrawPoolMember/index.tsx @@ -0,0 +1,111 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ActionItem, ModalPadding, ModalWarnings } from '@polkadot-cloud/react'; +import { isNotZero, planckToUnit, rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import { Warning } from 'library/Form/Warning'; +import { useSignerWarnings } from 'library/Hooks/useSignerWarnings'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Close } from 'library/Modal/Close'; +import { SubmitTx } from 'library/SubmitTx'; +import { useTxMeta } from 'contexts/TxMeta'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const WithdrawPoolMember = () => { + const { t } = useTranslation('modals'); + const { api, consts } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { + setModalStatus, + config: { options }, + setModalResize, + } = useOverlay().modal; + const { activeEra } = useNetworkMetrics(); + const { removePoolMember } = usePoolMembers(); + const { getSignerWarnings } = useSignerWarnings(); + const { notEnoughFunds } = useTxMeta(); + + const { member, who } = options; + const { historyDepth } = consts; + const { unbondingEras, points } = member; + + // calculate total for withdraw + let totalWithdrawUnit = new BigNumber(0); + + Object.entries(unbondingEras).forEach((entry: any) => { + const [era, amount] = entry; + if (activeEra.index > era) { + totalWithdrawUnit = totalWithdrawUnit.plus( + new BigNumber(rmCommas(amount)) + ); + } + }); + + const bonded = planckToUnit(new BigNumber(rmCommas(points)), units); + + const totalWithdraw = planckToUnit(new BigNumber(totalWithdrawUnit), units); + + // valid to submit transaction + const [valid] = useState<boolean>(isNotZero(totalWithdraw) ?? false); + + // tx to submit + const getTx = () => { + let tx = null; + if (!valid || !api) { + return tx; + } + tx = api.tx.nominationPools.withdrawUnbonded(who, historyDepth.toString()); + return tx; + }; + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTx(), + from: activeAccount, + shouldSubmit: valid, + callbackSubmit: () => { + setModalStatus('closing'); + }, + callbackInBlock: () => { + // remove the pool member from context if no more funds bonded + if (bonded.isZero()) { + removePoolMember(who); + } + }, + }); + + useEffect(() => setModalResize(), [notEnoughFunds]); + + const warnings = getSignerWarnings( + activeAccount, + false, + submitExtrinsic.proxySupported + ); + + return ( + <> + <Close /> + <ModalPadding> + <h2 className="title">{t('withdrawMemberFunds')}</h2> + <ActionItem text={`${t('withdraw')} ${totalWithdraw} ${unit}`} /> + {warnings.length > 0 ? ( + <ModalWarnings withMargin> + {warnings.map((text, i) => ( + <Warning key={`warning${i}`} text={text} /> + ))} + </ModalWarnings> + ) : null} + </ModalPadding> + <SubmitTx valid={valid} {...submitExtrinsic} /> + </> + ); +}; diff --git a/src/overlay/index.tsx b/src/overlay/index.tsx new file mode 100644 index 0000000000..535497141f --- /dev/null +++ b/src/overlay/index.tsx @@ -0,0 +1,80 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useHelp } from 'contexts/Help'; +import { ErrorFallbackModal } from 'library/ErrorBoundary'; +import { Overlay } from '@polkadot-cloud/react'; +import { ClaimPayouts } from 'modals/ClaimPayouts'; +import { AccountPoolRoles } from '../modals/AccountPoolRoles'; +import { Accounts } from '../modals/Accounts'; +import { Bio } from '../modals/Bio'; +import { Bond } from '../modals/Bond'; +import { ChangeNominations } from '../modals/ChangeNominations'; +import { ChangePoolRoles } from '../modals/ChangePoolRoles'; +import { ChooseLanguage } from '../modals/ChooseLanguage'; +import { ClaimReward } from '../modals/ClaimReward'; +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'; +import { PoolNominations } from '../modals/PoolNominations'; +import { Settings } from '../modals/Settings'; +import { Unbond } from '../modals/Unbond'; +import { UnbondPoolMember } from '../modals/UnbondPoolMember'; +import { UnlockChunks } from '../modals/UnlockChunks'; +import { Unstake } from '../modals/Unstake'; +import { UpdateController } from '../modals/UpdateController'; +import { UpdatePayee } from '../modals/UpdatePayee'; +import { UpdateReserve } from '../modals/UpdateReserve'; +import { ValidatorMetrics } from '../modals/ValidatorMetrics'; +import { ValidatorGeo } from '../modals/ValidatorGeo'; +import { WithdrawPoolMember } from '../modals/WithdrawPoolMember'; +import { ManageNominations } from '../canvas/ManageNominations'; + +export const Overlays = () => { + const { status } = useHelp(); + return ( + <Overlay + fallback={ErrorFallbackModal} + externalOverlayStatus={status} + modals={{ + Bio, + AccountPoolRoles, + Bond, + ChangeNominations, + ChangePoolRoles, + ChooseLanguage, + ClaimPayouts, + ClaimReward, + Connect, + Accounts, + GoToFeedback, + JoinPool, + ImportLedger, + ImportVault, + ManagePool, + ManageFastUnstake, + Networks, + PoolNominations, + Settings, + ValidatorMetrics, + ValidatorGeo, + UnbondPoolMember, + UnlockChunks, + Unstake, + UpdateController, + Unbond, + UpdatePayee, + UpdateReserve, + WithdrawPoolMember, + }} + canvas={{ + ManageNominations, + }} + /> + ); +}; diff --git a/src/pages/Community/Entity.tsx b/src/pages/Community/Entity.tsx new file mode 100644 index 0000000000..0fb92f7c43 --- /dev/null +++ b/src/pages/Community/Entity.tsx @@ -0,0 +1,101 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { ButtonSecondary, PageHeading, PageRow } from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { ValidatorList } from 'library/ValidatorList'; +import { useNetwork } from 'contexts/Network'; +import { Item } from './Item'; +import { ItemsWrapper } from './Wrappers'; +import { useCommunitySections } from './context'; + +export const Entity = () => { + const { t } = useTranslation('pages'); + const { isReady } = useApi(); + const { network } = useNetwork(); + const { validators: allValidators } = useValidators(); + const { setActiveSection, activeItem } = useCommunitySections(); + + const { name, validators: entityAllValidators } = activeItem; + const validators = entityAllValidators[network] ?? []; + + // include validators that exist in `erasStakers` + const [activeValidators, setActiveValidators] = useState( + allValidators.filter((v) => validators.includes(v.address)) + ); + + useEffect(() => { + setActiveValidators( + allValidators.filter((v) => validators.includes(v.address)) + ); + }, [allValidators, network]); + + useEffect(() => { + const newValidators = [...activeValidators]; + setActiveValidators(newValidators); + }, [name, activeItem, network]); + + const container = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + duration: 0.5, + staggerChildren: 0.05, + }, + }, + }; + + return ( + <PageRow> + <PageHeading> + <ButtonSecondary + text={t('community.goBack')} + iconLeft={faChevronLeft} + iconTransform="shrink-3" + onClick={() => setActiveSection(0)} + /> + </PageHeading> + <ItemsWrapper variants={container} initial="hidden" animate="show"> + <Item item={activeItem} actionable={false} /> + </ItemsWrapper> + <CardWrapper> + {!isReady ? ( + <div className="item"> + <h3>{t('community.connecting')}...</h3> + </div> + ) : ( + <> + {activeValidators.length === 0 && ( + <div className="item"> + <h3> + {validators.length + ? `${t('community.fetchingValidators')}...` + : t('community.noValidators')} + </h3> + </div> + )} + {activeValidators.length > 0 && ( + <ValidatorList + bondFor="nominator" + validators={activeValidators} + allowListFormat={false} + selectable={false} + allowMoreCols + pagination + toggleFavorites + allowFilters + refetchOnListUpdate + /> + )} + </> + )} + </CardWrapper> + </PageRow> + ); +}; diff --git a/src/pages/Community/Item.tsx b/src/pages/Community/Item.tsx new file mode 100644 index 0000000000..32365e35bc --- /dev/null +++ b/src/pages/Community/Item.tsx @@ -0,0 +1,169 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faTwitter } from '@fortawesome/free-brands-svg-icons'; +import { + faEnvelope, + faExternalLink, + faServer, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Suspense, lazy, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { ItemWrapper } from './Wrappers'; +import { useCommunitySections } from './context'; +import type { ItemProps } from './types'; + +export const Item = ({ item, actionable }: ItemProps) => { + const { t } = useTranslation('pages'); + const { openModal } = useOverlay().modal; + const { network } = useNetwork(); + + const { + bio, + name, + email, + twitter, + website, + thumbnail, + validators: entityAllValidators, + } = item; + const validatorCount = entityAllValidators[network]?.length ?? 0; + + const { setActiveSection, setActiveItem, setScrollPos } = + useCommunitySections(); + + const listItem = { + hidden: { + opacity: 0, + y: 25, + transition: { + duration: 0.4, + }, + }, + show: { + opacity: 1, + y: 0, + transition: { + duration: 0.4, + type: 'spring', + bounce: 0.2, + }, + }, + }; + + const Thumbnail = useMemo( + () => lazy(() => import(`../../config/validators/${thumbnail}.tsx`)), + [] + ); + + return ( + <ItemWrapper + whileHover={{ scale: 1.005 }} + transition={{ duration: 0.15 }} + variants={listItem} + > + <div className="inner"> + <section> + <Suspense fallback={<div />}> + <Thumbnail /> + </Suspense> + </section> + <section> + <h3> + {name} + <button + type="button" + onClick={() => openModal({ key: 'Bio', options: { name, bio } })} + className="active" + > + <span>{t('community.bio')}</span> + </button> + </h3> + + <div className="stats"> + <button + className={actionable ? 'active' : undefined} + disabled={!actionable} + type="button" + onClick={() => { + if (actionable) { + setActiveSection(1); + setActiveItem(item); + setScrollPos(window.scrollY); + } + }} + > + <FontAwesomeIcon + icon={faServer} + className="icon-left" + transform="shrink-1" + /> + <h4> + {t('community.validator', { + count: validatorCount, + })} + </h4> + </button> + {email !== undefined && ( + <button + type="button" + className="active" + onClick={() => { + window.open(`mailto:${email}`, '_blank'); + }} + > + <FontAwesomeIcon + icon={faEnvelope} + transform="shrink-1" + className="icon-left" + /> + <h4>{t('community.email')}</h4> + <FontAwesomeIcon + icon={faExternalLink} + className="icon-right" + transform="shrink-3" + /> + </button> + )} + {twitter !== undefined && ( + <button + type="button" + className="active" + onClick={() => { + window.open(`https://twitter.com/${twitter}`, '_blank'); + }} + > + <FontAwesomeIcon icon={faTwitter} className="icon-left" /> + <h4>{twitter}</h4> + <FontAwesomeIcon + icon={faExternalLink} + className="icon-right" + transform="shrink-3" + /> + </button> + )} + {website !== undefined && ( + <button + type="button" + className="active" + onClick={() => { + window.open(website, '_blank'); + }} + > + <h4>{t('community.website')}</h4> + <FontAwesomeIcon + icon={faExternalLink} + className="icon-right" + transform="shrink-3" + /> + </button> + )} + </div> + </section> + </div> + </ItemWrapper> + ); +}; diff --git a/src/pages/Community/List.tsx b/src/pages/Community/List.tsx new file mode 100644 index 0000000000..98c285dd1b --- /dev/null +++ b/src/pages/Community/List.tsx @@ -0,0 +1,51 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { PageRow } from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { useNetwork } from 'contexts/Network'; +import { Item } from './Item'; +import { ItemsWrapper } from './Wrappers'; +import { useCommunitySections } from './context'; + +export const List = () => { + const { network } = useNetwork(); + const { validatorCommunity } = useValidators(); + const { scrollPos } = useCommunitySections(); + + const [entityItems, setEntityItems] = useState( + validatorCommunity.filter((v) => v.validators[network] !== undefined) + ); + + useEffect(() => { + setEntityItems( + validatorCommunity.filter((v) => v.validators[network] !== undefined) + ); + }, [network]); + + useEffect(() => { + window.scrollTo(0, scrollPos); + }, [scrollPos]); + + const container = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + duration: scrollPos ? 0 : 0.5, + staggerChildren: scrollPos ? 0 : 0.025, + }, + }, + }; + + return ( + <PageRow yMargin> + <ItemsWrapper variants={container} initial="hidden" animate="show"> + {entityItems.map((item: any, index: number) => ( + <Item key={`community_item_${index}`} item={item} actionable /> + ))} + </ItemsWrapper> + </PageRow> + ); +}; diff --git a/src/pages/Community/Wrappers.ts b/src/pages/Community/Wrappers.ts new file mode 100644 index 0000000000..4281a46fe3 --- /dev/null +++ b/src/pages/Community/Wrappers.ts @@ -0,0 +1,188 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +const VERTICAL_THRESHOLD = 800; + +export const Wrapper = styled.div` + h2 { + color: var(--text-color-secondary); + margin-top: 2rem; + margin-bottom: 1rem; + } +`; +export const ItemsWrapper = styled(motion.div)` + display: flex; + flex-flow: row wrap; + width: 100%; +`; + +export const ItemWrapper = styled(motion.div)` + flex-shrink: 0; + flex-grow: 1; + flex-basis: 100%; + margin: 1rem 1rem 0 0; + + height: auto; + @media (min-width: ${VERTICAL_THRESHOLD + 1}px) { + height: 8.8rem; + } + + > .inner { + color: var(--text-color-secondary); + background: var(--background-primary); + box-shadow: var(--card-shadow); + border-radius: 0.75rem; + width: 100%; + height: 100%; + display: flex; + align-items: center; + flex-flow: column wrap; + justify-content: center; + + svg { + border-radius: 0.74rem; + } + + /* vertical validator thumbnail / content tiling */ + section { + display: flex; + flex-flow: column wrap; + padding: 0 1rem; + overflow: hidden; + + h3 { + display: flex; + flex-flow: row wrap; + align-items: center; + + > button { + font-size: 1.1rem; + &.active { + color: var(--text-color-secondary); + background: var(--background-list-item); + &:hover { + background: var(--background-list-item); + } + } + padding: 0.35rem 0.75rem; + margin-left: 0.75rem; + } + } + + button { + display: flex; + flex-flow: row wrap; + align-items: center; + border-radius: 1rem; + padding: 0.3rem 1rem; + + svg { + color: var(--text-color-secondary); + } + + margin: 0.5rem 1rem 0.5rem 0; + @media (min-width: ${VERTICAL_THRESHOLD + 1}px) { + margin: 0.25rem 1rem 0.25rem 0; + } + &:disabled { + cursor: default; + } + &.active { + background: var(--button-secondary-background); + transition: background var(--transition-duration); + &:hover { + background: var(--button-hover-background); + } + } + &:last-child { + margin-right: none; + } + .icon-left { + margin-right: 0.5rem; + } + .icon-right { + margin-left: 0.65rem; + opacity: 0.75; + } + } + + > .stats { + display: flex; + flex-flow: row wrap; + width: 100%; + margin-top: 0rem; + margin-bottom: 1rem; + @media (min-width: ${VERTICAL_THRESHOLD + 1}px) { + margin-top: 0.25rem; + margin-bottom: 0; + } + } + + &:first-child { + flex-flow: row wrap; + align-items: center; + width: 100%; + padding: 1rem; + svg { + width: 4.5rem; + height: 4.5rem; + } + } + &:last-child { + border-top: 1px solid var(--border-primary-color); + border-left: none; + flex-flow: column wrap; + justify-content: center; + height: 100%; + height: 50%; + width: 100%; + flex: 1; + + h3 { + margin-top: 0.5rem; + margin-bottom: 0.4rem; + } + } + } + + /* horizontal validator thumbnail / content tiling */ + @media (min-width: ${VERTICAL_THRESHOLD + 1}px) { + flex-flow: row wrap; + section { + padding: 0 1.5rem; + justify-content: flex-start; + + &:first-child { + flex-flow: row wrap; + align-items: center; + justify-content: flex-start; + height: 100%; + width: 7rem; + + svg { + width: 4.5rem; + height: 4.5rem; + } + } + &:last-child { + padding: 1rem 0; + align-items: center; + flex-flow: column wrap; + align-items: flex-start; + justify-content: center; + border-left: none; + border-top: none; + height: 100%; + flex: 1; + + h3 { + margin-top: 0.25rem; + } + } + } + } + } +`; diff --git a/src/pages/Community/context.tsx b/src/pages/Community/context.tsx new file mode 100644 index 0000000000..b7bd4948cf --- /dev/null +++ b/src/pages/Community/context.tsx @@ -0,0 +1,56 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useEffect, useState } from 'react'; +import { useNetwork } from 'contexts/Network'; +import * as defaults from './defaults'; + +export const CommunitySectionsContext: React.Context<any> = React.createContext( + defaults.defaultContext +); + +export const useCommunitySections = () => + React.useContext(CommunitySectionsContext); + +export const CommunitySectionsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { network } = useNetwork(); + + // store the active section of the community page + const [activeSection, setActiveSectionState] = useState<number>(0); + + // store the active entity item of the community page + const [activeItem, setActiveItem] = useState(defaults.item); + + // store the Y scroll position when the last entity was visited + // used to automatically scroll back down upon returning to the entity lsit. + const [scrollPos, setScrollPos] = useState<number>(0); + + // go back to first section and reset item when network switches + useEffect(() => { + setActiveSectionState(0); + setActiveItem(defaults.item); + }, [network]); + + const setActiveSection = (t: any) => { + setActiveSectionState(t); + }; + + return ( + <CommunitySectionsContext.Provider + value={{ + activeSection, + setActiveSection, + activeItem, + setActiveItem, + scrollPos, + setScrollPos, + }} + > + {children} + </CommunitySectionsContext.Provider> + ); +}; diff --git a/src/pages/Community/defaults.ts b/src/pages/Community/defaults.ts new file mode 100644 index 0000000000..face250f00 --- /dev/null +++ b/src/pages/Community/defaults.ts @@ -0,0 +1,14 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable @typescript-eslint/no-unused-vars */ + +export const item = { + name: '', + thumbnail: null, + validators: [], +}; + +export const defaultContext = { + setActiveSection: (t: number) => {}, + activeSection: 0, +}; diff --git a/src/pages/Community/index.tsx b/src/pages/Community/index.tsx new file mode 100644 index 0000000000..298175a38e --- /dev/null +++ b/src/pages/Community/index.tsx @@ -0,0 +1,31 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { PageTitle } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import type { PageProps } from 'types'; +import { Entity } from './Entity'; +import { List } from './List'; +import { Wrapper } from './Wrappers'; +import { CommunitySectionsProvider, useCommunitySections } from './context'; + +export const CommunityInner = ({ page }: PageProps) => { + const { t } = useTranslation('base'); + const { activeSection } = useCommunitySections(); + + const { key } = page; + + return ( + <Wrapper> + <PageTitle title={t(key)} /> + {activeSection === 0 && <List />} + {activeSection === 1 && <Entity />} + </Wrapper> + ); +}; + +export const Community = (props: PageProps) => ( + <CommunitySectionsProvider> + <CommunityInner {...props} /> + </CommunitySectionsProvider> +); diff --git a/src/pages/Community/types.ts b/src/pages/Community/types.ts new file mode 100644 index 0000000000..688026280e --- /dev/null +++ b/src/pages/Community/types.ts @@ -0,0 +1,16 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors + +export interface ItemProps { + item: Item; + actionable: boolean; +} + +export interface Item { + bio: string; + name: string; + email: string; + twitter: string; + website: string; + thumbnail: string; + validators: Record<string, string>; +} diff --git a/src/pages/Nominate/Active/ControllerNotStash.tsx b/src/pages/Nominate/Active/ControllerNotStash.tsx new file mode 100644 index 0000000000..bd45dac55d --- /dev/null +++ b/src/pages/Nominate/Active/ControllerNotStash.tsx @@ -0,0 +1,70 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faCircleArrowRight, + faExclamationTriangle, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { stringUpperFirst } from '@polkadot/util'; +import { ButtonPrimary, PageRow } from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useBonded } from 'contexts/Bonded'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const ControllerNotStash = () => { + const { t } = useTranslation('pages'); + const { network } = useNetwork(); + const { activeAccount } = useActiveAccounts(); + const { addressDifferentToStash } = useStaking(); + const { getBondedAccount } = useBonded(); + const { openModal } = useOverlay().modal; + const { isSyncing } = useUi(); + const { isReadOnlyAccount } = useImportedAccounts(); + const controller = getBondedAccount(activeAccount); + + const [showPrompt, setShowPrompt] = useState<boolean>( + addressDifferentToStash(controller) + ); + + useEffect(() => { + setShowPrompt(addressDifferentToStash(controller)); + }, [controller]); + + return ( + <> + {showPrompt + ? !isSyncing && + !isReadOnlyAccount(activeAccount) && ( + <PageRow> + <CardWrapper className="warning"> + <CardHeaderWrapper> + <h3 style={{ marginBottom: '0.75rem' }}> + <FontAwesomeIcon icon={faExclamationTriangle} /> +   {t('nominate.controllerAccountsDeprecated')} + </h3> + <h4> + {t('nominate.proxyprompt')} {stringUpperFirst(network)}. + </h4> + </CardHeaderWrapper> + <div> + <ButtonPrimary + text={t('nominate.updateToStash')} + iconLeft={faCircleArrowRight} + onClick={() => openModal({ key: 'UpdateController' })} + /> + </div> + </CardWrapper> + </PageRow> + ) + : null} + </> + ); +}; diff --git a/src/pages/Nominate/Active/ManageBond.tsx b/src/pages/Nominate/Active/ManageBond.tsx new file mode 100644 index 0000000000..6b7805fea3 --- /dev/null +++ b/src/pages/Nominate/Active/ManageBond.tsx @@ -0,0 +1,131 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faLockOpen } from '@fortawesome/free-solid-svg-icons'; +import { + ButtonHelp, + ButtonPrimary, + ButtonRow, + Odometer, +} from '@polkadot-cloud/react'; +import { minDecimalPlaces, planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useBalances } from 'contexts/Balances'; +import { useHelp } from 'contexts/Help'; +import { useStaking } from 'contexts/Staking'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useUi } from 'contexts/UI'; +import { CardHeaderWrapper } from 'library/Card/Wrappers'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { BondedChart } from 'library/BarChart/BondedChart'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const ManageBond = () => { + const { t } = useTranslation('pages'); + const { + networkData: { + units, + brand: { token: Token }, + }, + } = useNetwork(); + const { isSyncing } = useUi(); + const { openHelp } = useHelp(); + const { inSetup } = useStaking(); + const { openModal } = useOverlay().modal; + const { getStashLedger } = useBalances(); + const { isFastUnstaking } = useUnstaking(); + const { isReadOnlyAccount } = useImportedAccounts(); + const { getTransferOptions, feeReserve } = useTransferOptions(); + const { activeAccount } = useActiveAccounts(); + const ledger = getStashLedger(activeAccount); + const { active }: { active: BigNumber } = ledger; + const allTransferOptions = getTransferOptions(activeAccount); + + const { freeBalance, edReserved } = allTransferOptions; + const { totalUnlocking, totalUnlocked, totalUnlockChuncks } = + allTransferOptions.nominate; + const totalFree = BigNumber.max( + 0, + freeBalance.minus(edReserved.plus(feeReserve)) + ); + + return ( + <> + <CardHeaderWrapper> + <h4> + {t('nominate.bondedFunds')} + <ButtonHelp marginLeft onClick={() => openHelp('Bonding')} /> + </h4> + <h2> + <Token className="networkIcon" /> + <Odometer + value={minDecimalPlaces(planckToUnit(active, units).toFormat(), 2)} + zeroDecimals={2} + /> + </h2> + <ButtonRow> + <ButtonPrimary + disabled={ + inSetup() || + isSyncing || + isReadOnlyAccount(activeAccount) || + isFastUnstaking + } + marginRight + onClick={() => + openModal({ + key: 'Bond', + options: { bondFor: 'nominator' }, + size: 'sm', + }) + } + text="+" + /> + <ButtonPrimary + disabled={ + inSetup() || + isSyncing || + isReadOnlyAccount(activeAccount) || + isFastUnstaking + } + marginRight + onClick={() => + openModal({ + key: 'Unbond', + options: { bondFor: 'nominator' }, + size: 'sm', + }) + } + text="-" + /> + <ButtonPrimary + disabled={ + isSyncing || inSetup() || isReadOnlyAccount(activeAccount) + } + iconLeft={faLockOpen} + marginRight + onClick={() => + openModal({ + key: 'UnlockChunks', + options: { bondFor: 'nominator', disableWindowResize: true }, + size: 'sm', + }) + } + text={String(totalUnlockChuncks ?? 0)} + /> + </ButtonRow> + </CardHeaderWrapper> + <BondedChart + active={planckToUnit(active, units)} + unlocking={planckToUnit(totalUnlocking, units)} + unlocked={planckToUnit(totalUnlocked, units)} + free={planckToUnit(totalFree, units)} + inactive={active.isZero()} + /> + </> + ); +}; diff --git a/src/pages/Nominate/Active/Stats/ActiveNominators.tsx b/src/pages/Nominate/Active/Stats/ActiveNominators.tsx new file mode 100644 index 0000000000..393c4ee8ab --- /dev/null +++ b/src/pages/Nominate/Active/Stats/ActiveNominators.tsx @@ -0,0 +1,41 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useStaking } from 'contexts/Staking'; +import { Pie } from 'library/StatBoxList/Pie'; + +export const ActiveNominatorsStat = () => { + const { t } = useTranslation('pages'); + const { consts } = useApi(); + const { maxElectingVoters } = consts; + const { totalActiveNominators } = useStaking().eraStakers; + + // active nominators as percent + let totalNominatorsAsPercent = 0; + if (maxElectingVoters.isGreaterThan(0)) { + totalNominatorsAsPercent = + totalActiveNominators / maxElectingVoters.dividedBy(100).toNumber(); + } + + const params = { + label: t('overview.activeNominators'), + stat: { + value: totalActiveNominators, + total: maxElectingVoters.toNumber(), + unit: '', + }, + graph: { + value1: totalActiveNominators, + value2: maxElectingVoters.minus(totalActiveNominators).toNumber(), + }, + tooltip: `${new BigNumber(totalNominatorsAsPercent) + .decimalPlaces(2) + .toFormat()}%`, + helpKey: 'Active Nominators', + }; + + return <Pie {...params} />; +}; diff --git a/src/pages/Nominate/Active/Stats/MinimumActiveStake.tsx b/src/pages/Nominate/Active/Stats/MinimumActiveStake.tsx new file mode 100644 index 0000000000..d6600c10e0 --- /dev/null +++ b/src/pages/Nominate/Active/Stats/MinimumActiveStake.tsx @@ -0,0 +1,27 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { Number } from 'library/StatBoxList/Number'; +import { useNetwork } from 'contexts/Network'; + +export const MinimumActiveStakeStat = () => { + const { t } = useTranslation('pages'); + const { + networkData: { unit, units }, + } = useNetwork(); + const { metrics } = useNetworkMetrics(); + const { minimumActiveStake } = metrics; + + const params = { + label: t('nominate.minimumToEarnRewards'), + value: planckToUnit(minimumActiveStake, units).toNumber(), + decimals: 3, + unit: `${unit}`, + helpKey: 'Bonding', + }; + + return <Number {...params} />; +}; diff --git a/src/pages/Nominate/Active/Stats/MinimumNominatorBond.tsx b/src/pages/Nominate/Active/Stats/MinimumNominatorBond.tsx new file mode 100644 index 0000000000..4df7ef99a1 --- /dev/null +++ b/src/pages/Nominate/Active/Stats/MinimumNominatorBond.tsx @@ -0,0 +1,25 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useStaking } from 'contexts/Staking'; +import { Number } from 'library/StatBoxList/Number'; +import { useNetwork } from 'contexts/Network'; + +export const MinimumNominatorBondStat = () => { + const { t } = useTranslation('pages'); + const { staking } = useStaking(); + const { unit, units } = useNetwork().networkData; + const { minNominatorBond } = staking; + + const params = { + label: t('nominate.minimumToNominate'), + value: planckToUnit(minNominatorBond, units).toNumber(), + decimals: 3, + unit: `${unit}`, + helpKey: 'Bonding', + }; + + return <Number {...params} />; +}; diff --git a/src/pages/Nominate/Active/Status/NominationStatus.tsx b/src/pages/Nominate/Active/Status/NominationStatus.tsx new file mode 100644 index 0000000000..1a2bf33222 --- /dev/null +++ b/src/pages/Nominate/Active/Status/NominationStatus.tsx @@ -0,0 +1,109 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faBolt, + faChevronCircleRight, + 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 { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useSetup } from 'contexts/Setup'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { useNominationStatus } from 'library/Hooks/useNominationStatus'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { Stat } from 'library/Stat'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const NominationStatus = ({ + showButtons = true, + buttonType = 'primary', +}: { + showButtons?: boolean; + buttonType?: string; +}) => { + const { t } = useTranslation('pages'); + const { isReady } = useApi(); + const { inSetup } = useStaking(); + const { isNetworkSyncing } = useUi(); + const { openModal } = useOverlay().modal; + const { metrics } = useNetworkMetrics(); + const { getBondedAccount } = useBonded(); + const { checking, isExposed } = useFastUnstake(); + const { isReadOnlyAccount } = useImportedAccounts(); + const { getNominationStatus } = useNominationStatus(); + const { activeAccount } = useActiveAccounts(); + const { getFastUnstakeText, isUnstaking } = useUnstaking(); + const { setOnNominatorSetup, getNominatorSetupPercent } = useSetup(); + + const fastUnstakeText = getFastUnstakeText(); + const controller = getBondedAccount(activeAccount); + const { fastUnstakeErasToCheckPerBlock } = metrics; + const nominationStatus = getNominationStatus(activeAccount, 'nominator'); + + // Determine whether to display fast unstake button or regular unstake button. + const unstakeButton = + fastUnstakeErasToCheckPerBlock > 0 && + !nominationStatus.nominees.active.length && + (checking || !isExposed) + ? { + disabled: checking || isReadOnlyAccount(controller), + title: fastUnstakeText, + icon: faBolt, + onClick: () => { + openModal({ key: 'ManageFastUnstake', size: 'sm' }); + }, + } + : { + title: t('nominate.unstake'), + icon: faSignOutAlt, + disabled: !isReady || isReadOnlyAccount(controller) || !activeAccount, + 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 ( + <Stat + label={t('nominate.status')} + helpKey="Nomination Status" + stat={nominationStatus.message} + buttons={ + !showButtons + ? [] + : !inSetup() + ? !isUnstaking + ? [unstakeButton] + : [] + : isNetworkSyncing + ? [] + : [ + { + title: startTitle, + icon: faChevronCircleRight, + transform: 'grow-1', + disabled: + !isReady || + isReadOnlyAccount(activeAccount) || + !activeAccount, + onClick: () => setOnNominatorSetup(true), + }, + ] + } + buttonType={buttonType} + /> + ); +}; diff --git a/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx b/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx new file mode 100644 index 0000000000..fa7ca94ddb --- /dev/null +++ b/src/pages/Nominate/Active/Status/PayoutDestinationStatus.tsx @@ -0,0 +1,72 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faGear, faWallet } from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { usePayeeConfig } from 'library/Hooks/usePayeeConfig'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { Stat } from 'library/Stat'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const PayoutDestinationStatus = () => { + const { t } = useTranslation('pages'); + const { isSyncing } = useUi(); + const { openModal } = useOverlay().modal; + const { staking, inSetup } = useStaking(); + const { isFastUnstaking } = useUnstaking(); + const { getPayeeItems } = usePayeeConfig(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + const { payee } = staking; + + // Get payee status text to display. + const getPayeeStatus = () => { + if (inSetup()) { + return t('nominate.notAssigned'); + } + const status = getPayeeItems(true).find( + ({ value }) => value === payee.destination + )?.activeTitle; + + if (status) { + return status; + } + return t('nominate.notAssigned'); + }; + + // Get the payee destination icon to display, falling back to wallet icon. + const payeeIcon = inSetup() + ? undefined + : getPayeeItems(true).find(({ value }) => value === payee.destination) + ?.icon || faWallet; + + return ( + <Stat + label={t('nominate.payoutDestination')} + helpKey="Payout Destination" + icon={payeeIcon} + stat={getPayeeStatus()} + buttons={ + !inSetup() + ? [ + { + title: t('nominate.update'), + icon: faGear, + small: true, + disabled: + inSetup() || + isSyncing || + isReadOnlyAccount(activeAccount) || + isFastUnstaking, + onClick: () => openModal({ key: 'UpdatePayee', size: 'sm' }), + }, + ] + : [] + } + /> + ); +}; diff --git a/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx new file mode 100644 index 0000000000..8fc09bd1e8 --- /dev/null +++ b/src/pages/Nominate/Active/Status/UnclaimedPayoutsStatus.tsx @@ -0,0 +1,68 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { Stat } from 'library/Stat'; +import { usePayouts } from 'contexts/Payouts'; +import BigNumber from 'bignumber.js'; +import { useApi } from 'contexts/Api'; +import { minDecimalPlaces, planckToUnit } from '@polkadot-cloud/utils'; +import { faCircleDown } from '@fortawesome/free-solid-svg-icons'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const UnclaimedPayoutsStatus = () => { + const { t } = useTranslation(); + const { isReady } = useApi(); + const { + networkData: { units }, + } = useNetwork(); + const { openModal } = useOverlay().modal; + const { unclaimedPayouts } = usePayouts(); + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + + const totalUnclaimed = Object.values(unclaimedPayouts || {}).reduce( + (total, validators) => + Object.values(validators) + .reduce((amount, value) => amount.plus(value), new BigNumber(0)) + .plus(total), + new BigNumber(0) + ); + + return ( + <Stat + label={t('nominate.pendingPayouts', { ns: 'pages' })} + helpKey="Payout" + type="odometer" + stat={{ + value: minDecimalPlaces( + planckToUnit(totalUnclaimed, units).toFormat(), + 2 + ), + }} + buttons={ + Object.keys(unclaimedPayouts || {}).length > 0 + ? [ + { + title: t('claim', { ns: 'modals' }), + icon: faCircleDown, + disabled: !isReady || isReadOnlyAccount(activeAccount), + small: true, + onClick: () => + openModal({ + key: 'ClaimPayouts', + size: 'sm', + options: { + disableWindowResize: true, + }, + }), + }, + ] + : undefined + } + /> + ); +}; diff --git a/src/pages/Nominate/Active/Status/index.tsx b/src/pages/Nominate/Active/Status/index.tsx new file mode 100644 index 0000000000..1f28051986 --- /dev/null +++ b/src/pages/Nominate/Active/Status/index.tsx @@ -0,0 +1,18 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { Separator } from '@polkadot-cloud/react'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { UnclaimedPayoutsStatus } from './UnclaimedPayoutsStatus'; +import { NominationStatus } from './NominationStatus'; +import { PayoutDestinationStatus } from './PayoutDestinationStatus'; + +export const Status = ({ height }: { height: number }) => ( + <CardWrapper height={height}> + <NominationStatus /> + <Separator /> + <UnclaimedPayoutsStatus /> + <Separator /> + <PayoutDestinationStatus /> + </CardWrapper> +); diff --git a/src/pages/Nominate/Active/UnstakePrompts.tsx b/src/pages/Nominate/Active/UnstakePrompts.tsx new file mode 100644 index 0000000000..8cf09fc07c --- /dev/null +++ b/src/pages/Nominate/Active/UnstakePrompts.tsx @@ -0,0 +1,99 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBolt, faLockOpen } from '@fortawesome/free-solid-svg-icons'; +import { ButtonPrimary, ButtonRow, PageRow } from '@polkadot-cloud/react'; +import { isNotZero } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useTheme } from 'contexts/Themes'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useUi } from 'contexts/UI'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const UnstakePrompts = () => { + const { t } = useTranslation('pages'); + const { unit, colors } = useNetwork().networkData; + const { activeAccount } = useActiveAccounts(); + const { mode } = useTheme(); + const { openModal } = useOverlay().modal; + const { isNetworkSyncing } = useUi(); + const { isFastUnstaking, isUnstaking, getFastUnstakeText } = useUnstaking(); + const { getTransferOptions } = useTransferOptions(); + const { active, totalUnlockChuncks, totalUnlocked, totalUnlocking } = + getTransferOptions(activeAccount).nominate; + const annuncementBorderColor = colors.secondary[mode]; + + // unstaking can withdraw + const canWithdrawUnlocks = + isUnstaking && + active.isZero() && + totalUnlocking.isZero() && + isNotZero(totalUnlocked); + + return ( + <> + {(isUnstaking || isFastUnstaking) && !isNetworkSyncing && ( + <PageRow> + <CardWrapper + style={{ border: `1px solid ${annuncementBorderColor}` }} + > + <div className="content"> + <h3> + {t('nominate.unstakePromptInProgress', { + context: isFastUnstaking ? 'fast' : 'regular', + })} + </h3> + <h4> + {isFastUnstaking + ? t('nominate.unstakePromptInQueue') + : !canWithdrawUnlocks + ? t('nominate.unstakePromptWaitingForUnlocks') + : `${t('nominate.unstakePromptReadyToWithdraw')} ${t( + 'nominate.unstakePromptRevert', + { unit } + )}`} + </h4> + <ButtonRow yMargin> + {isFastUnstaking ? ( + <ButtonPrimary + marginRight + iconLeft={faBolt} + text={getFastUnstakeText()} + onClick={() => + openModal({ key: 'ManageFastUnstake', size: 'sm' }) + } + /> + ) : ( + <ButtonPrimary + iconLeft={faLockOpen} + text={ + canWithdrawUnlocks + ? t('nominate.unlocked') + : String(totalUnlockChuncks ?? 0) + } + disabled={false} + onClick={() => + openModal({ + key: 'UnlockChunks', + options: { + bondFor: 'nominator', + poolClosure: true, + disableWindowResize: true, + }, + size: 'sm', + }) + } + /> + )} + </ButtonRow> + </div> + </CardWrapper> + </PageRow> + )} + </> + ); +}; diff --git a/src/pages/Nominate/Active/index.tsx b/src/pages/Nominate/Active/index.tsx new file mode 100644 index 0000000000..a98e757078 --- /dev/null +++ b/src/pages/Nominate/Active/index.tsx @@ -0,0 +1,108 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronCircleRight } from '@fortawesome/free-solid-svg-icons'; +import { + ButtonHelp, + ButtonPrimary, + PageRow, + PageTitle, + RowSection, +} from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; +import { useUnstaking } from 'library/Hooks/useUnstaking'; +import { StatBoxList } from 'library/StatBoxList'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Nominations } from 'library/Nominations'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { ListStatusHeader } from 'library/List'; +import { ControllerNotStash } from './ControllerNotStash'; +import { ManageBond } from './ManageBond'; +import { ActiveNominatorsStat } from './Stats/ActiveNominators'; +import { MinimumActiveStakeStat } from './Stats/MinimumActiveStake'; +import { MinimumNominatorBondStat } from './Stats/MinimumNominatorBond'; +import { Status } from './Status'; +import { UnstakePrompts } from './UnstakePrompts'; + +export const Active = () => { + const { t } = useTranslation(); + const { isSyncing } = useUi(); + const { openHelp } = useHelp(); + const { inSetup } = useStaking(); + const { nominated } = useValidators(); + const { isFastUnstaking } = useUnstaking(); + const { openCanvas } = useOverlay().canvas; + const { activeAccount } = useActiveAccounts(); + + const ROW_HEIGHT = 220; + + return ( + <> + <PageTitle title={t('nominate.nominate', { ns: 'pages' })} /> + <StatBoxList> + <ActiveNominatorsStat /> + <MinimumNominatorBondStat /> + <MinimumActiveStakeStat /> + </StatBoxList> + <ControllerNotStash /> + <UnstakePrompts /> + <PageRow> + <RowSection hLast> + <Status height={ROW_HEIGHT} /> + </RowSection> + <RowSection secondary> + <CardWrapper height={ROW_HEIGHT}> + <ManageBond /> + </CardWrapper> + </RowSection> + </PageRow> + <PageRow> + <CardWrapper> + {nominated?.length || inSetup() || isSyncing ? ( + <Nominations bondFor="nominator" nominator={activeAccount} /> + ) : ( + <> + <CardHeaderWrapper $withAction $withMargin> + <h3> + {t('nominate.nominate', { ns: 'pages' })} + <ButtonHelp + marginLeft + onClick={() => openHelp('Nominations')} + /> + </h3> + <div> + <ButtonPrimary + iconLeft={faChevronCircleRight} + iconTransform="grow-1" + text={t('nominate.nominate', { ns: 'pages' })} + disabled={inSetup() || isSyncing || isFastUnstaking} + onClick={() => + openCanvas({ + key: 'ManageNominations', + scroll: false, + options: { + bondFor: 'nominator', + nominator: activeAccount, + nominated, + }, + size: 'xl', + }) + } + /> + </div> + </CardHeaderWrapper> + <ListStatusHeader> + {t('notNominating', { ns: 'library' })}. + </ListStatusHeader> + </> + )} + </CardWrapper> + </PageRow> + </> + ); +}; diff --git a/src/pages/Nominate/Active/types.ts b/src/pages/Nominate/Active/types.ts new file mode 100644 index 0000000000..27c8ddc97d --- /dev/null +++ b/src/pages/Nominate/Active/types.ts @@ -0,0 +1,12 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type BigNumber from 'bignumber.js'; + +export interface BondedChartProps { + active: BigNumber; + free: BigNumber; + unlocking: BigNumber; + unlocked: BigNumber; + inactive: boolean; +} diff --git a/src/pages/Nominate/Setup/Bond/index.tsx b/src/pages/Nominate/Setup/Bond/index.tsx new file mode 100644 index 0000000000..2b842645ac --- /dev/null +++ b/src/pages/Nominate/Setup/Bond/index.tsx @@ -0,0 +1,93 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSetup } from 'contexts/Setup'; +import { useTxMeta } from 'contexts/TxMeta'; +import { BondFeedback } from 'library/Form/Bond/BondFeedback'; +import { NominateStatusBar } from 'library/Form/NominateStatusBar'; +import { Footer } from 'library/SetupSteps/Footer'; +import { Header } from 'library/SetupSteps/Header'; +import { MotionContainer } from 'library/SetupSteps/MotionContainer'; +import type { SetupStepProps } from 'library/SetupSteps/types'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const Bond = ({ section }: SetupStepProps) => { + const { t } = useTranslation('pages'); + const { activeAccount } = useActiveAccounts(); + const { txFees } = useTxMeta(); + const { getSetupProgress, setActiveAccountSetup } = useSetup(); + const setup = getSetupProgress('nominator', activeAccount); + const { progress } = setup; + + // either free to bond or existing setup value + const initialBondValue = progress.bond === '0' ? '0' : progress.bond; + + // store local bond amount for form control + const [bond, setBond] = useState<{ bond: string }>({ + bond: initialBondValue, + }); + + // bond valid + const [bondValid, setBondValid]: any = useState(false); + + // handler for updating bond + const handleSetupUpdate = (value: any) => { + setActiveAccountSetup('nominator', value); + }; + + // update bond on account change + useEffect(() => { + setBond({ + bond: initialBondValue, + }); + }, [activeAccount]); + + // apply initial bond value to setup progress + useEffect(() => { + // only update if Bond is currently active + if (setup.section === section) { + setActiveAccountSetup('nominator', { + ...progress, + bond: initialBondValue, + }); + } + }, [setup.section]); + + return ( + <> + <Header + thisSection={section} + complete={progress.bond !== '0' && progress.bond !== ''} + title={t('nominate.bond')} + helpKey="Bonding" + bondFor="nominator" + /> + <MotionContainer thisSection={section} activeSection={setup.section}> + <BondFeedback + syncing={txFees.isZero()} + bondFor="nominator" + inSetup + listenIsValid={(valid) => setBondValid(valid)} + defaultBond={initialBondValue} + setters={[ + { + set: handleSetupUpdate, + current: progress, + }, + { + set: setBond, + current: bond, + }, + ]} + txFees={txFees} + maxWidth + /> + <NominateStatusBar value={new BigNumber(bond.bond)} /> + <Footer complete={bondValid} bondFor="nominator" /> + </MotionContainer> + </> + ); +}; diff --git a/src/pages/Nominate/Setup/Payee/index.tsx b/src/pages/Nominate/Setup/Payee/index.tsx new file mode 100644 index 0000000000..6639fc2520 --- /dev/null +++ b/src/pages/Nominate/Setup/Payee/index.tsx @@ -0,0 +1,110 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSetup } from 'contexts/Setup'; +import type { PayeeConfig, PayeeOptions } from 'contexts/Setup/types'; +import { Spacer } from 'library/Form/Wrappers'; +import { usePayeeConfig } from 'library/Hooks/usePayeeConfig'; +import { PayeeInput } from 'library/PayeeInput'; +import { SelectItems } from 'library/SelectItems'; +import { SelectItem } from 'library/SelectItems/Item'; +import { Footer } from 'library/SetupSteps/Footer'; +import { Header } from 'library/SetupSteps/Header'; +import { MotionContainer } from 'library/SetupSteps/MotionContainer'; +import type { SetupStepProps } from 'library/SetupSteps/types'; +import type { MaybeAddress } from 'types'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Subheading } from 'pages/Nominate/Wrappers'; + +export const Payee = ({ section }: SetupStepProps) => { + const { t } = useTranslation('pages'); + const { getPayeeItems } = usePayeeConfig(); + const { activeAccount } = useActiveAccounts(); + const { getSetupProgress, setActiveAccountSetup } = useSetup(); + + const setup = getSetupProgress('nominator', activeAccount); + const { progress } = setup; + const { payee } = progress; + + // Store the current user-inputted custom payout account. + const [account, setAccount] = useState<MaybeAddress>(payee.account); + + const DefaultPayeeConfig: PayeeConfig = { + destination: 'Staked', + account: null, + }; + + // determine whether this section is completed. + const isComplete = () => + payee.destination !== null && + !(payee.destination === 'Account' && payee.account === null); + + // update setup progress with payee config. + const handleChangeDestination = (destination: PayeeOptions) => { + // set local value to update input element set setup payee + setActiveAccountSetup('nominator', { + ...progress, + payee: { destination, account }, + }); + }; + + // update setup progress with payee account. + const handleChangeAccount = (newAccount: MaybeAddress) => { + // set local value to update input element set setup payee + setActiveAccountSetup('nominator', { + ...progress, + payee: { ...payee, account: newAccount }, + }); + }; + + // set initial payee value to `Staked` if not yet set. + useEffect(() => { + if (!payee || (!payee.destination && !payee.account)) { + setActiveAccountSetup('nominator', { + ...progress, + payee: DefaultPayeeConfig, + }); + } + }, [activeAccount]); + + return ( + <> + <Header + thisSection={section} + complete={isComplete()} + title={t('nominate.payoutDestination')} + helpKey="Payout Destination" + bondFor="nominator" + /> + <MotionContainer thisSection={section} activeSection={setup.section}> + <Subheading> + <h4>{t('nominate.payoutDestinationSubtitle')}</h4> + </Subheading> + + <SelectItems layout="three-col"> + {getPayeeItems().map((item) => ( + <SelectItem + key={`payee_option_${item.value}`} + account={account} + setAccount={setAccount} + selected={payee.destination === item.value} + onClick={() => handleChangeDestination(item.value)} + layout="three-col" + {...item} + /> + ))} + </SelectItems> + <Spacer /> + <PayeeInput + payee={payee} + account={account} + setAccount={setAccount} + handleChange={handleChangeAccount} + /> + <Footer complete={isComplete()} bondFor="nominator" /> + </MotionContainer> + </> + ); +}; diff --git a/src/pages/Nominate/Setup/Summary/Wrapper.ts b/src/pages/Nominate/Setup/Summary/Wrapper.ts new file mode 100644 index 0000000000..63cb28611e --- /dev/null +++ b/src/pages/Nominate/Setup/Summary/Wrapper.ts @@ -0,0 +1,43 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const SummaryWrapper = styled.div` + display: flex; + flex-flow: row wrap; + width: 100%; + margin-bottom: 1rem; + + > section { + border-bottom: 1px solid var(--border-primary-color); + flex-basis: 100%; + display: flex; + flex-flow: row wrap; + margin-top: 1rem; + padding: 0.5rem 0 0.75rem 0; + + > div:first-child { + color: var(--text-color-secondary); + width: 200px; + display: flex; + flex-flow: row wrap; + align-items: center; + + svg { + color: var(--accent-color-primary); + } + } + + > div:last-child { + color: var(--text-color-secondary); + flex-grow: 1; + display: flex; + flex-flow: column nowrap; + + p { + margin: 0.25rem 0; + } + } + } +`; diff --git a/src/pages/Nominate/Setup/Summary/index.tsx b/src/pages/Nominate/Setup/Summary/index.tsx new file mode 100644 index 0000000000..7895ab7e09 --- /dev/null +++ b/src/pages/Nominate/Setup/Summary/index.tsx @@ -0,0 +1,139 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCheckCircle } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ellipsisFn, unitToPlanck } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useSetup } from 'contexts/Setup'; +import { Warning } from 'library/Form/Warning'; +import { useBatchCall } from 'library/Hooks/useBatchCall'; +import { usePayeeConfig } from 'library/Hooks/usePayeeConfig'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Header } from 'library/SetupSteps/Header'; +import { MotionContainer } from 'library/SetupSteps/MotionContainer'; +import type { SetupStepProps } from 'library/SetupSteps/types'; +import { SubmitTx } from 'library/SubmitTx'; +import { useNetwork } from 'contexts/Network'; +import { useApi } from 'contexts/Api'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { SummaryWrapper } from './Wrapper'; + +export const Summary = ({ section }: SetupStepProps) => { + const { t } = useTranslation('pages'); + const { api } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { newBatchCall } = useBatchCall(); + const { getPayeeItems } = usePayeeConfig(); + const { accountHasSigner } = useImportedAccounts(); + const { activeAccount, activeProxy } = useActiveAccounts(); + const { getSetupProgress, removeSetupProgress } = useSetup(); + + const setup = getSetupProgress('nominator', activeAccount); + const { progress } = setup; + const { bond, nominations, payee } = progress; + + const getTxs = () => { + if (!activeAccount || !api) { + return null; + } + + const targetsToSubmit = nominations.map((item: any) => ({ + Id: item.address, + })); + + const payeeToSubmit = + payee.destination === 'Account' + ? { + Account: payee.account, + } + : payee.destination; + + const bondToSubmit = unitToPlanck(bond, units); + const bondAsString = bondToSubmit.isNaN() ? '0' : bondToSubmit.toString(); + + const txs = [ + api.tx.staking.bond(bondAsString, payeeToSubmit), + api.tx.staking.nominate(targetsToSubmit), + ]; + return newBatchCall(txs, activeAccount); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTxs(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => {}, + callbackInBlock: () => { + removeSetupProgress('nominator', activeAccount); + }, + }); + + const payeeDisplay = + getPayeeItems().find(({ value }) => value === payee.destination)?.title || + payee.destination; + + return ( + <> + <Header + thisSection={section} + complete={null} + title={t('nominate.summary')} + bondFor="nominator" + /> + <MotionContainer thisSection={section} activeSection={setup.section}> + {!( + accountHasSigner(activeAccount) || accountHasSigner(activeProxy) + ) && <Warning text={t('nominate.readOnly')} />} + <SummaryWrapper> + <section> + <div> + <FontAwesomeIcon icon={faCheckCircle} transform="grow-1" />  {' '} + {t('nominate.payoutDestination')}: + </div> + <div> + {payee.destination === 'Account' + ? `${payeeDisplay}: ${ellipsisFn(payee.account || '')}` + : payeeDisplay} + </div> + </section> + <section> + <div> + <FontAwesomeIcon icon={faCheckCircle} transform="grow-1" />  {' '} + {t('nominate.nominating')}: + </div> + <div>{t('nominate.validator', { count: nominations.length })}</div> + </section> + <section> + <div> + <FontAwesomeIcon icon={faCheckCircle} transform="grow-1" />  {' '} + {t('nominate.bondAmount')}: + </div> + <div> + {new BigNumber(bond).toFormat()} {unit} + </div> + </section> + </SummaryWrapper> + <div + style={{ + flex: 1, + width: '100%', + borderRadius: '1rem', + overflow: 'hidden', + }} + > + <SubmitTx + submitText={t('nominate.startNominating')} + valid + {...submitExtrinsic} + displayFor="canvas" /* Edge case: not canvas, but the larger button sizes suit this UI more. */ + /> + </div> + </MotionContainer> + </> + ); +}; diff --git a/src/pages/Nominate/Setup/index.tsx b/src/pages/Nominate/Setup/index.tsx new file mode 100644 index 0000000000..a6519b356b --- /dev/null +++ b/src/pages/Nominate/Setup/index.tsx @@ -0,0 +1,89 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { + ButtonSecondary, + PageHeading, + PageRow, + PageTitle, +} from '@polkadot-cloud/react'; +import { extractUrlValue, removeVarFromUrlHash } from '@polkadot-cloud/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'; + +export const Setup = () => { + const { t } = useTranslation('pages'); + const navigate = useNavigate(); + const { activeAccount } = useActiveAccounts(); + const { setOnNominatorSetup, removeSetupProgress } = useSetup(); + + return ( + <> + <PageTitle title={t('nominate.startNominating')} /> + <PageRow> + <PageHeading> + <span> + <ButtonSecondary + text={t('nominate.back')} + iconLeft={faChevronLeft} + iconTransform="shrink-3" + onClick={() => { + if (extractUrlValue('f') === 'overview') { + navigate('/overview'); + } else { + removeVarFromUrlHash('f'); + setOnNominatorSetup(false); + } + }} + /> + </span> + <span> + <ButtonSecondary + text={t('nominate.cancel')} + iconLeft={faTimes} + onClick={() => { + removeVarFromUrlHash('f'); + setOnNominatorSetup(false); + removeSetupProgress('nominator', activeAccount); + }} + /> + </span> + <div className="right" /> + </PageHeading> + </PageRow> + <PageRow> + <CardWrapper> + <Element name="payee" style={{ position: 'absolute' }} /> + <Payee section={1} /> + </CardWrapper> + </PageRow> + <PageRow> + <CardWrapper> + <Element name="nominate" style={{ position: 'absolute' }} /> + <Nominate bondFor="nominator" section={2} /> + </CardWrapper> + </PageRow> + <PageRow> + <CardWrapper> + <Element name="bond" style={{ position: 'absolute' }} /> + <Bond section={3} /> + </CardWrapper> + </PageRow> + <PageRow> + <CardWrapper> + <Element name="summary" style={{ position: 'absolute' }} /> + <Summary section={4} /> + </CardWrapper> + </PageRow> + </> + ); +}; diff --git a/src/pages/Nominate/Wrappers.ts b/src/pages/Nominate/Wrappers.ts new file mode 100644 index 0000000000..93df8167f6 --- /dev/null +++ b/src/pages/Nominate/Wrappers.ts @@ -0,0 +1,31 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + display: flex; + flex-flow: column wrap; +`; + +export const Spacer = styled.div` + width: 100%; + height: 1px; + margin: 0.75rem 0; +`; + +export const Subheading = styled.div` + margin: 0.4rem 0 1rem 0; + + h3, + h4 { + margin-top: 0; + margin-left: 0; + display: flex; + align-items: center; + + > button { + margin-left: 0.75rem; + } + } +`; diff --git a/src/pages/Nominate/index.tsx b/src/pages/Nominate/index.tsx new file mode 100644 index 0000000000..eaf2e2f04b --- /dev/null +++ b/src/pages/Nominate/index.tsx @@ -0,0 +1,12 @@ +// Copyright 2023 @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 <Wrapper>{onNominatorSetup ? <Setup /> : <Active />}</Wrapper>; +}; diff --git a/src/pages/Overview/ActiveAccounts/Item.tsx b/src/pages/Overview/ActiveAccounts/Item.tsx new file mode 100644 index 0000000000..849f220a2f --- /dev/null +++ b/src/pages/Overview/ActiveAccounts/Item.tsx @@ -0,0 +1,96 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCopy } from '@fortawesome/free-regular-svg-icons'; +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ellipsisFn, remToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useNotifications } from 'contexts/Notifications'; +import type { NotificationText } from 'contexts/Notifications/types'; +import { useProxies } from 'contexts/Proxies'; +import { Polkicon } from '@polkadot-cloud/react'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { ItemWrapper } from './Wrappers'; +import type { ActiveAccountProps } from './types'; + +export const Item = ({ address, delegate = null }: ActiveAccountProps) => { + const { t } = useTranslation('pages'); + const { getProxyDelegate } = useProxies(); + const { getAccount } = useImportedAccounts(); + const { addNotification } = useNotifications(); + + const primaryAddress = delegate || address || ''; + const delegatorAddress = delegate ? address : null; + + const accountData = getAccount(primaryAddress); + + // click to copy notification + let notification: NotificationText | null = null; + if (accountData !== null) { + notification = { + title: t('overview.addressCopied'), + subtitle: accountData.address, + }; + } + + const proxyDelegate = getProxyDelegate(delegatorAddress, primaryAddress); + + return ( + <ItemWrapper> + <div className="title"> + <h4> + {accountData && ( + <> + {delegatorAddress && ( + <div className="delegator"> + <Polkicon + address={delegatorAddress || ''} + size={remToUnit('1.7rem')} + /> + </div> + )} + <div className="icon"> + <Polkicon address={primaryAddress} size={remToUnit('1.7rem')} /> + </div> + {delegatorAddress && ( + <> + <span> + {proxyDelegate?.proxyType} {t('overview.proxy')} + <FontAwesomeIcon icon={faArrowLeft} transform="shrink-2" /> + </span> + </> + )} + {ellipsisFn(primaryAddress)} + <button + type="button" + onClick={() => { + navigator.clipboard.writeText(primaryAddress); + if (notification) { + addNotification(notification); + } + }} + > + <FontAwesomeIcon + className="copy" + icon={faCopy} + transform="shrink-4" + /> + </button> + {accountData.name !== ellipsisFn(primaryAddress) && ( + <> + <div className="sep" /> + <div className="rest"> + <span className="name">{accountData.name}</span> + </div> + </> + )} + </> + )} + + {!accountData ? t('overview.noActiveAccount') : null} + </h4> + </div> + </ItemWrapper> + ); +}; diff --git a/src/pages/Overview/ActiveAccounts/Wrappers.ts b/src/pages/Overview/ActiveAccounts/Wrappers.ts new file mode 100644 index 0000000000..8fdfec1d33 --- /dev/null +++ b/src/pages/Overview/ActiveAccounts/Wrappers.ts @@ -0,0 +1,120 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const ActiveAccounsWrapper = styled.div` + width: 100%; + display: flex; + flex-direction: column; + + > div { + border-bottom: 1px solid var(--border-primary-color); + padding: 0.65rem 0; + + &:last-child { + border: none; + padding-bottom: 0; + } + } +`; + +export const ItemWrapper = styled.div` + display: flex; + flex-flow: row wrap; + align-items: center; + overflow: hidden; + width: 100%; + + .delegator { + width: 0.75rem; + z-index: 0; + } + + .icon { + position: relative; + top: 0.1rem; + margin-right: 0.5rem; + z-index: 1; + } + .title { + font-family: InterSemiBold, sans-serif; + margin: 0; + padding: 0; + flex: 1; + overflow: hidden; + + &.signer { + padding-left: 2rem; + } + } + .rest { + flex: 1 1 0%; + min-height: 1.8rem; + overflow: hidden; + position: relative; + + .name { + color: var(--text-color-tertiary); + position: absolute; + left: 0; + bottom: 0; + display: inline; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + max-width: 100%; + } + } + + button { + width: 2rem; + height: 2rem; + border-radius: 50%; + margin-left: 0.25rem; + padding: 0; + } + + h4 { + display: flex; + flex-flow: row wrap; + align-items: center; + flex: 1; + + > .sep { + border-right: 1px solid var(--border-secondary-color); + margin: 0 0.65rem 0 0.25rem; + width: 1px; + height: 1.25rem; + } + > .addr { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + > span { + opacity: 0.7; + margin: 0 0.5rem; + > svg { + margin-left: 0.5rem; + } + } + } + + > *:last-child { + flex-grow: 1; + display: flex; + flex-flow: row-reverse wrap; + + .copy { + color: var(--text-color-secondary); + opacity: 0.9; + cursor: pointer; + transition: opacity var(--transition-duration); + &:hover { + opacity: 1; + } + } + } +`; diff --git a/src/pages/Overview/ActiveAccounts/index.tsx b/src/pages/Overview/ActiveAccounts/index.tsx new file mode 100644 index 0000000000..323ae101e2 --- /dev/null +++ b/src/pages/Overview/ActiveAccounts/index.tsx @@ -0,0 +1,17 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Item } from './Item'; +import { ActiveAccounsWrapper } from './Wrappers'; + +export const ActiveAccounts = () => { + const { activeProxy, activeAccount } = useActiveAccounts(); + + return ( + <ActiveAccounsWrapper> + <Item address={activeAccount} /> + {activeProxy && <Item address={activeAccount} delegate={activeProxy} />} + </ActiveAccounsWrapper> + ); +}; diff --git a/src/pages/Overview/ActiveAccounts/types.ts b/src/pages/Overview/ActiveAccounts/types.ts new file mode 100644 index 0000000000..e94406ab44 --- /dev/null +++ b/src/pages/Overview/ActiveAccounts/types.ts @@ -0,0 +1,9 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { MaybeAddress } from 'types'; + +export interface ActiveAccountProps { + address: MaybeAddress; + delegate?: MaybeAddress; +} diff --git a/src/pages/Overview/BalanceChart.tsx b/src/pages/Overview/BalanceChart.tsx new file mode 100644 index 0000000000..abc589aca8 --- /dev/null +++ b/src/pages/Overview/BalanceChart.tsx @@ -0,0 +1,297 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCheck, faCheckDouble } from '@fortawesome/free-solid-svg-icons'; +import { ButtonTertiary, Odometer } from '@polkadot-cloud/react'; +import { + greaterThanZero, + minDecimalPlaces, + planckToUnit, +} from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useBalances } from 'contexts/Balances'; +import { usePlugins } from 'contexts/Plugins'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useUi } from 'contexts/UI'; +import { BarSegment } from 'library/BarChart/BarSegment'; +import { LegendItem } from 'library/BarChart/LegendItem'; +import { Bar, BarChartWrapper, Legend } from 'library/BarChart/Wrappers'; +import { CardHeaderWrapper } from 'library/Card/Wrappers'; +import { usePrices } from 'library/Hooks/usePrices'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const BalanceChart = () => { + const { t } = useTranslation('pages'); + const { + networkData: { + units, + unit, + brand: { token: Token }, + }, + } = useNetwork(); + const prices = usePrices(); + const { plugins } = usePlugins(); + const { isNetworkSyncing } = useUi(); + const { openModal } = useOverlay().modal; + const { getBalance, getLocks } = useBalances(); + const { activeAccount } = useActiveAccounts(); + const { accountHasSigner } = useImportedAccounts(); + const { feeReserve, getTransferOptions } = useTransferOptions(); + const balance = getBalance(activeAccount); + const allTransferOptions = getTransferOptions(activeAccount); + const { edReserved } = allTransferOptions; + const poolBondOpions = allTransferOptions.pool; + const unlockingPools = poolBondOpions.totalUnlocking.plus( + poolBondOpions.totalUnlocked + ); + + // user's total balance + const { free, frozen } = balance; + const totalBalance = planckToUnit( + free.plus(poolBondOpions.active).plus(unlockingPools), + units + ); + // convert balance to fiat value + const freeFiat = totalBalance.multipliedBy( + new BigNumber(prices.lastPrice).decimalPlaces(2) + ); + + // total funds nominating + const nominating = planckToUnit( + allTransferOptions.nominate.active + .plus(allTransferOptions.nominate.totalUnlocking) + .plus(allTransferOptions.nominate.totalUnlocked), + units + ); + // total funds in pool + const inPool = planckToUnit( + allTransferOptions.pool.active + .plus(allTransferOptions.pool.totalUnlocking) + .plus(allTransferOptions.pool.totalUnlocked), + units + ); + + // check account non-staking locks + const locks = getLocks(activeAccount); + const locksStaking = locks.find(({ id }) => id === 'staking'); + const lockStakingAmount = locksStaking + ? locksStaking.amount + : new BigNumber(0); + + // total funds available, including existential deposit, minus staking. + const graphAvailable = planckToUnit( + BigNumber.max(free.minus(lockStakingAmount), 0), + units + ); + const notStaking = graphAvailable; + + // graph percentages + const graphTotal = nominating.plus(inPool).plus(graphAvailable); + const graphNominating = greaterThanZero(nominating) + ? nominating.dividedBy(graphTotal.multipliedBy(0.01)) + : new BigNumber(0); + + const graphInPool = greaterThanZero(inPool) + ? inPool.dividedBy(graphTotal.multipliedBy(0.01)) + : new BigNumber(0); + + const graphNotStaking = greaterThanZero(graphTotal) + ? BigNumber.max( + new BigNumber(100).minus(graphNominating).minus(graphInPool), + 0 + ) + : new BigNumber(0); + + // available balance data + const fundsLocked = planckToUnit( + BigNumber.max(frozen.minus(lockStakingAmount), 0), + units + ); + let fundsReserved = planckToUnit(edReserved.plus(feeReserve), units); + const fundsFree = planckToUnit( + BigNumber.max( + allTransferOptions.freeBalance.minus(feeReserve).minus(fundsLocked), + 0 + ), + units + ); + // available balance percentages + const graphLocked = greaterThanZero(fundsLocked) + ? fundsLocked.dividedBy(graphAvailable.multipliedBy(0.01)) + : new BigNumber(0); + + const graphFree = greaterThanZero(fundsFree) + ? fundsFree.dividedBy(graphAvailable.multipliedBy(0.01)) + : new BigNumber(0); + + // get total available balance, including reserve and locks + if (graphAvailable.isLessThan(fundsReserved)) { + fundsReserved = graphAvailable; + } + + // formatter for price feed. + const usdFormatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }); + + const isNominating = greaterThanZero(nominating); + const isInPool = greaterThanZero( + poolBondOpions.active + .plus(poolBondOpions.totalUnlocked) + .plus(poolBondOpions.totalUnlocking) + ); + + return ( + <> + <CardHeaderWrapper> + <h4>{t('overview.balance')}</h4> + <h2> + <Token className="networkIcon" /> + <Odometer + value={minDecimalPlaces(totalBalance.toFormat(), 2)} + zeroDecimals={2} + /> + <span className="note"> + {plugins.includes('binance_spot') ? ( + <> {usdFormatter.format(freeFiat.toNumber())}</> + ) : null} + </span> + </h2> + </CardHeaderWrapper> + + <BarChartWrapper> + <Legend> + {isNominating ? ( + <LegendItem dataClass="d1" label={t('overview.nominating')} /> + ) : null} + {greaterThanZero(inPool) ? ( + <LegendItem dataClass="d2" label={t('overview.inPool')} /> + ) : null} + <LegendItem dataClass="d4" label={t('overview.notStaking')} /> + </Legend> + <Bar> + <BarSegment + dataClass="d1" + widthPercent={Number(graphNominating.toFixed(2))} + flexGrow={!inPool && !notStaking && isNominating ? 1 : 0} + label={`${nominating.decimalPlaces(3).toFormat()} ${unit}`} + /> + <BarSegment + dataClass="d2" + widthPercent={Number(graphInPool.toFixed(2))} + flexGrow={!isNominating && !notStaking && inPool ? 1 : 0} + label={`${inPool.decimalPlaces(3).toFormat()} ${unit}`} + /> + <BarSegment + dataClass="d4" + widthPercent={Number(graphNotStaking.toFixed(2))} + flexGrow={!isNominating && !inPool ? 1 : 0} + label={`${notStaking.decimalPlaces(3).toFormat()} ${unit}`} + forceShow={!isNominating && !isInPool} + /> + </Bar> + <section className="available"> + <div + style={{ + flex: 1, + minWidth: '8.5rem', + flexBasis: `${ + greaterThanZero(graphFree) && greaterThanZero(graphLocked) + ? `${graphFree.toFixed(2)}%` + : 'auto' + }`, + }} + > + <Legend> + <LegendItem label={t('overview.free')} helpKey="Your Balance" /> + </Legend> + <Bar> + <BarSegment + dataClass="d4" + widthPercent={100} + flexGrow={1} + label={`${fundsFree.decimalPlaces(3).toFormat()} ${unit}`} + /> + </Bar> + </div> + {greaterThanZero(fundsLocked) ? ( + <div + style={{ + flex: 1, + minWidth: '8.5rem', + flexBasis: `${graphLocked.toFixed(2)}%`, + }} + > + <Legend> + <LegendItem + label={t('overview.locked')} + helpKey="Reserve Balance" + /> + </Legend> + <Bar> + <BarSegment + dataClass="d4" + widthPercent={100} + flexGrow={1} + label={`${fundsLocked.decimalPlaces(3).toFormat()} ${unit}`} + /> + </Bar> + </div> + ) : null} + {greaterThanZero(fundsReserved) ? ( + <div + style={{ + flex: 0, + minWidth: '12.5rem', + maxWidth: '12.5rem', + flexBasis: '50%', + }} + > + <Legend className="end"> + <LegendItem + label="" + button={ + <ButtonTertiary + text={t('overview.reserveBalance')} + onClick={() => + openModal({ key: 'UpdateReserve', size: 'sm' }) + } + iconRight={ + isNetworkSyncing + ? undefined + : !feeReserve.isZero() && !edReserved.isZero() + ? faCheckDouble + : feeReserve.isZero() && edReserved.isZero() + ? undefined + : faCheck + } + iconTransform="shrink-1" + disabled={ + !activeAccount || + isNetworkSyncing || + !accountHasSigner(activeAccount) + } + /> + } + /> + </Legend> + <Bar> + <BarSegment + dataClass="d4" + widthPercent={100} + flexGrow={1} + label={`${fundsReserved.decimalPlaces(3).toFormat()} ${unit}`} + /> + </Bar> + </div> + ) : null} + </section> + </BarChartWrapper> + </> + ); +}; diff --git a/src/pages/Overview/BalanceLinks.tsx b/src/pages/Overview/BalanceLinks.tsx new file mode 100644 index 0000000000..ee9b5892f2 --- /dev/null +++ b/src/pages/Overview/BalanceLinks.tsx @@ -0,0 +1,59 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; +import { ButtonPrimaryInvert, Separator } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useStaking } from 'contexts/Staking'; +import { MoreWrapper } from './Wrappers'; + +export const BalanceLinks = () => { + const { t } = useTranslation('pages'); + const { network } = useNetwork(); + const { isNominating } = useStaking(); + const { activeAccount } = useActiveAccounts(); + + return ( + <MoreWrapper> + <Separator /> + <h4>{t('overview.moreResources')}</h4> + <section> + <ButtonPrimaryInvert + lg + onClick={() => + window.open( + `https://${network}.subscan.io/account/${activeAccount}`, + '_blank' + ) + } + iconRight={faExternalLinkAlt} + iconTransform="shrink-2" + text="Subscan" + marginRight + disabled={!activeAccount} + /> + <ButtonPrimaryInvert + lg + onClick={() => + window.open( + `https://${network}.polkawatch.app/nomination/${activeAccount}`, + '_blank' + ) + } + iconRight={faExternalLinkAlt} + iconTransform="shrink-2" + text="Polkawatch" + disabled={ + !( + activeAccount && + ['polkadot', 'kusama'].includes(network) && + isNominating() + ) + } + /> + </section> + </MoreWrapper> + ); +}; diff --git a/src/pages/Overview/NetworkSats/Announcements.tsx b/src/pages/Overview/NetworkSats/Announcements.tsx new file mode 100644 index 0000000000..bef4483d1c --- /dev/null +++ b/src/pages/Overview/NetworkSats/Announcements.tsx @@ -0,0 +1,137 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBullhorn as faBack } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + capitalizeFirstLetter, + planckToUnit, + rmCommas, + sortWithNull, +} from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import type { BondedPool } from 'contexts/Pools/types'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { Announcement as AnnouncementLoader } from 'library/Loader/Announcement'; +import { useNetwork } from 'contexts/Network'; +import { Item } from './Wrappers'; + +export const Announcements = () => { + const { t } = useTranslation('pages'); + const { isSyncing } = useUi(); + const { staking } = useStaking(); + const { stats } = usePoolsConfig(); + const { + network, + networkData: { units, unit }, + } = useNetwork(); + const { bondedPools } = useBondedPools(); + + const { totalStaked } = staking; + const { counterForPoolMembers } = stats; + + let totalPoolPoints = new BigNumber(0); + bondedPools.forEach((b: BondedPool) => { + totalPoolPoints = totalPoolPoints.plus(rmCommas(b.points)); + }); + const totalPoolPointsUnit = planckToUnit(totalPoolPoints, units); + + const container = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + staggerChildren: 0.25, + }, + }, + }; + + const listItem = { + hidden: { + opacity: 0, + }, + show: { + opacity: 1, + }, + }; + + const announcements = []; + + const networkUnit = unit; + + // total staked on the network + if (!isSyncing) { + announcements.push({ + class: 'neutral', + title: t('overview.networkCurrentlyStaked', { + total: planckToUnit(totalStaked, units).integerValue().toFormat(), + unit, + network: capitalizeFirstLetter(network), + }), + subtitle: t('overview.networkCurrentlyStakedSubtitle', { + unit, + }), + }); + } else { + announcements.push(null); + } + + // total locked in pools + if (bondedPools.length) { + announcements.push({ + class: 'neutral', + title: `${totalPoolPointsUnit.integerValue().toFormat()} ${unit} ${t( + 'overview.inPools' + )}`, + subtitle: `${t('overview.bondedInPools', { networkUnit })}`, + }); + } else { + announcements.push(null); + } + + if (counterForPoolMembers.isGreaterThan(0)) { + // total locked in pools + announcements.push({ + class: 'neutral', + title: `${counterForPoolMembers.toFormat()} ${t( + 'overview.poolMembersBonding' + )}`, + subtitle: `${t('overview.totalNumAccounts')}`, + }); + } else { + announcements.push(null); + } + + announcements.sort(sortWithNull(true)); + + return ( + <motion.div + variants={container} + initial="hidden" + animate="show" + style={{ width: '100%' }} + > + {announcements.map((item, index) => + item === null ? ( + <AnnouncementLoader key={`announcement_${index}`} /> + ) : ( + <Item key={`announcement_${index}`} variants={listItem}> + <h4 className={item.class}> + <FontAwesomeIcon + icon={faBack} + style={{ marginRight: '0.6rem' }} + /> + {item.title} + </h4> + <p>{item.subtitle}</p> + </Item> + ) + )} + </motion.div> + ); +}; diff --git a/src/pages/Overview/NetworkSats/Wrappers.ts b/src/pages/Overview/NetworkSats/Wrappers.ts new file mode 100644 index 0000000000..ba1b243e41 --- /dev/null +++ b/src/pages/Overview/NetworkSats/Wrappers.ts @@ -0,0 +1,52 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +export const Wrapper = styled.div` + display: flex; + flex-flow: column wrap; + width: 100%; +`; + +export const Item = styled(motion.div)` + border-bottom: 1px solid var(--border-primary-color); + list-style: none; + flex: 1; + margin-bottom: 1rem; + padding: 0.75rem; + padding-bottom: 1.5rem; + + &:last-child { + border-bottom: 0; + margin-bottom: 0; + } + + h4 { + display: flex; + flex-flow: row wrap; + align-items: center; + margin: 0 0 0.5rem; + padding-bottom: 0.2rem; + + &.neutral { + color: var(--accent-color-primary); + } + &.danger { + color: #d2545d; + } + &.warning { + color: #b5a200; + } + &.pools { + color: var(--accent-color-secondary); + } + } + + p { + color: var(--text-color-secondary); + margin: 0; + line-height: 1.2rem; + } +`; diff --git a/src/pages/Overview/NetworkSats/index.tsx b/src/pages/Overview/NetworkSats/index.tsx new file mode 100644 index 0000000000..23dba70951 --- /dev/null +++ b/src/pages/Overview/NetworkSats/index.tsx @@ -0,0 +1,62 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { useStaking } from 'contexts/Staking'; +import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; +import { useInflation } from 'library/Hooks/useInflation'; +import { StatsHead } from 'library/StatsHead'; +import { Announcements } from './Announcements'; +import { Wrapper } from './Wrappers'; + +export const NetworkStats = () => { + const { t } = useTranslation('pages'); + const { bondedPools } = useBondedPools(); + const { inflation } = useInflation(); + const { metrics } = useNetworkMetrics(); + const { staking } = useStaking(); + const { totalNominators, totalValidators } = staking; + const { totalIssuance } = metrics; + + const items = [ + { + label: t('overview.totalValidators'), + value: totalValidators.toFormat(0), + helpKey: 'Validator', + }, + { + label: t('overview.totalNominators'), + value: totalNominators.toFormat(0), + helpKey: 'Total Nominators', + }, + { + label: t('overview.activePools'), + value: new BigNumber(bondedPools.length).toFormat(), + helpKey: 'Active Pools', + }, + { + label: t('overview.inflationRate'), + value: `${ + totalIssuance.toString() === '0' + ? '0' + : new BigNumber(inflation).decimalPlaces(2).toFormat() + }%`, + helpKey: 'Inflation', + }, + ]; + + return ( + <CardWrapper style={{ boxShadow: 'var(--card-shadow-secondary)' }}> + <CardHeaderWrapper $withMargin> + <h3>{t('overview.networkStats')}</h3> + </CardHeaderWrapper> + <Wrapper> + <StatsHead items={items} /> + <Announcements /> + </Wrapper> + </CardWrapper> + ); +}; diff --git a/src/pages/Overview/Payouts.tsx b/src/pages/Overview/Payouts.tsx new file mode 100644 index 0000000000..f3c22e7ac6 --- /dev/null +++ b/src/pages/Overview/Payouts.tsx @@ -0,0 +1,61 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { usePlugins } from 'contexts/Plugins'; +import { useStaking } from 'contexts/Staking'; +import { useUi } from 'contexts/UI'; +import { PayoutBar } from 'library/Graphs/PayoutBar'; +import { PayoutLine } from 'library/Graphs/PayoutLine'; +import { formatSize } from 'library/Graphs/Utils'; +import { GraphWrapper } from 'library/Graphs/Wrapper'; +import { useSize } from 'library/Hooks/useSize'; +import { StatusLabel } from 'library/StatusLabel'; + +export const Payouts = () => { + const { t } = useTranslation('pages'); + const { isSyncing } = useUi(); + const { plugins } = usePlugins(); + const { inSetup } = useStaking(); + const notStaking = !isSyncing && inSetup(); + + const ref = React.useRef<HTMLDivElement>(null); + + const size = useSize(ref.current); + const { width, height, minHeight } = formatSize(size, 260); + + return ( + <div className="inner" ref={ref} style={{ minHeight }}> + {!plugins.includes('subscan') ? ( + <StatusLabel + status="active_service" + statusFor="subscan" + title={t('overview.subscanDisabled')} + topOffset="37%" + /> + ) : ( + <StatusLabel + status="sync_or_setup" + title={t('overview.notStaking')} + topOffset="37%" + /> + )} + + <GraphWrapper + style={{ + height: `${height}px`, + width: `${width}px`, + position: 'absolute', + opacity: notStaking ? 0.75 : 1, + transition: 'opacity 0.5s', + }} + > + <PayoutBar days={19} height="150px" /> + <div style={{ marginTop: '3rem' }}> + <PayoutLine days={19} average={10} height="65px" /> + </div> + </GraphWrapper> + </div> + ); +}; diff --git a/src/pages/Overview/StakeStatus/Tips/Items.tsx b/src/pages/Overview/StakeStatus/Tips/Items.tsx new file mode 100644 index 0000000000..ab14117221 --- /dev/null +++ b/src/pages/Overview/StakeStatus/Tips/Items.tsx @@ -0,0 +1,135 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useAnimationControls } from 'framer-motion'; +import React, { useEffect, useState } from 'react'; +import { usePrompt } from 'contexts/Prompt'; +import { Tip } from 'library/Tips/Tip'; +import { ItemInnerWrapper, ItemWrapper, ItemsWrapper } from './Wrappers'; + +export const ItemsInner = ({ items, page }: any) => { + const controls = useAnimationControls(); + + // stores whether this is the initial display of tips + const [initial, setInitial] = useState(true); + + useEffect(() => { + doControls(true); + setInitial(false); + }, [page]); + + const doControls = async (transition: boolean) => { + if (transition) { + controls.set('hidden'); + controls.start('show'); + } else { + controls.set('show'); + } + }; + + return ( + <ItemsWrapper + initial="hidden" + animate={controls} + variants={{ + hidden: { opacity: 0 }, + show: { + opacity: 1, + }, + }} + > + {items.map((item: any, index: number) => ( + <Item + key={`tip_${index}_${page}`} + index={index} + {...item} + controls={controls} + initial={initial} + /> + ))} + </ItemsWrapper> + ); +}; + +const Item = ({ + title, + subtitle, + description, + index, + controls, + initial, + page, +}: any) => { + const { openPromptWith } = usePrompt(); + const [isStopped, setIsStopped] = useState(true); + + useEffect(() => { + const delay = index * 75; + + if (initial) { + setTimeout(() => { + if (isStopped) { + setIsStopped(false); + } + }, delay); + } + }, []); + + return ( + <ItemWrapper + animate={controls} + custom={index} + transition={{ + delay: index * 0.2, + duration: 0.7, + type: 'spring', + bounce: 0.35, + }} + variants={{ + hidden: { + y: 15, + }, + show: { + y: 0, + }, + }} + > + <ItemInnerWrapper> + <section /> + <section> + <div className="desc active"> + <button + onClick={() => + openPromptWith( + <Tip title={title} description={description} page={page} />, + 'large' + ) + } + type="button" + > + <h4> + {subtitle} + <FontAwesomeIcon + icon={faExternalLinkAlt} + transform="shrink-2" + /> + </h4> + </button> + </div> + </section> + </ItemInnerWrapper> + </ItemWrapper> + ); +}; + +export class Items extends React.Component<any, any> { + shouldComponentUpdate(nextProps: any) { + return JSON.stringify(this.props.items) !== JSON.stringify(nextProps.items); + } + + render() { + return <ItemsInner {...this.props} />; + } +} diff --git a/src/pages/Overview/StakeStatus/Tips/PageToggle.tsx b/src/pages/Overview/StakeStatus/Tips/PageToggle.tsx new file mode 100644 index 0000000000..d77048f9e6 --- /dev/null +++ b/src/pages/Overview/StakeStatus/Tips/PageToggle.tsx @@ -0,0 +1,69 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faChevronCircleLeft, + faChevronCircleRight, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useTranslation } from 'react-i18next'; +import { useUi } from 'contexts/UI'; +import { PageToggleWrapper } from './Wrappers'; +import type { PageToggleProps } from './types'; + +export const PageToggle = ({ + start, + end, + page, + itemsPerPage, + totalItems, + setPageHandler, +}: PageToggleProps) => { + const { t } = useTranslation(); + const { isNetworkSyncing } = useUi(); + + totalItems = isNetworkSyncing ? 1 : totalItems; + const totalPages = Math.ceil(totalItems / itemsPerPage); + + return ( + <PageToggleWrapper> + <button + type="button" + disabled={totalPages === 1 || page === 1} + onClick={() => { + setPageHandler(page - 1); + }} + > + <FontAwesomeIcon + icon={faChevronCircleLeft} + className="icon" + transform="grow-2" + /> + </button> + <h4 className={totalPages === 1 ? `disabled` : undefined}> + <span> + {start} + {itemsPerPage > 1 && totalItems > 1 && start !== end && ` - ${end}`} + </span> + {totalPages > 1 && ( + <> + {t('module.of', { ns: 'tips' })} <span>{totalItems}</span> + </> + )} + </h4> + <button + type="button" + disabled={totalPages === 1 || page === totalPages} + onClick={() => { + setPageHandler(page + 1); + }} + > + <FontAwesomeIcon + icon={faChevronCircleRight} + className="icon" + transform="grow-2" + /> + </button> + </PageToggleWrapper> + ); +}; diff --git a/src/pages/Overview/StakeStatus/Tips/Syncing.tsx b/src/pages/Overview/StakeStatus/Tips/Syncing.tsx new file mode 100644 index 0000000000..810e8010de --- /dev/null +++ b/src/pages/Overview/StakeStatus/Tips/Syncing.tsx @@ -0,0 +1,45 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { useDotLottieButton } from 'library/Hooks/useDotLottieButton'; +import { ItemInnerWrapper, ItemWrapper, ItemsWrapper } from './Wrappers'; + +export const Syncing = () => { + const { t } = useTranslation('tips'); + const { icon } = useDotLottieButton('refresh', { autoLoop: true }); + + return ( + <ItemsWrapper + initial="show" + animate={undefined} + variants={{ + hidden: { opacity: 0 }, + show: { + opacity: 1, + }, + }} + > + <ItemWrapper> + <ItemInnerWrapper> + <section + style={{ + marginRight: '0.5rem', + width: '1.5rem', + height: '1.5rem', + }} + > + {icon} + </section> + <section> + <div className="desc"> + <button type="button" disabled> + <h4>{t('module.oneMoment')}...</h4> + </button> + </div> + </section> + </ItemInnerWrapper> + </ItemWrapper> + </ItemsWrapper> + ); +}; diff --git a/src/pages/Overview/StakeStatus/Tips/Wrappers.ts b/src/pages/Overview/StakeStatus/Tips/Wrappers.ts new file mode 100644 index 0000000000..58efb14be3 --- /dev/null +++ b/src/pages/Overview/StakeStatus/Tips/Wrappers.ts @@ -0,0 +1,155 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; +import { SideMenuStickyThreshold, SmallFontSizeMaxWidth } from 'consts'; + +export const TipsWrapper = styled.div` + width: 100%; + display: flex; + position: relative; + padding: 0.15rem 1rem 0.7rem 1.25rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; + + @media (max-width: ${SideMenuStickyThreshold}px) { + padding: 0.5rem 1rem; + } +`; + +export const ItemsWrapper = styled(motion.div)` + width: 100%; + display: flex; + justify-items: center; + margin: 0.25rem 0 0rem 0; +`; +export const ItemWrapper = styled(motion.div)` + padding: 0 0.25rem; + flex-basis: 100%; + &:last-child { + margin-right: 0.25rem; + } +`; + +export const ItemInnerWrapper = styled.div` + display: flex; + flex-flow: row wrap; + align-items: center; + + > section { + height: 100%; + + &:nth-child(1) { + display: flex; + flex-flow: row wrap; + align-items: center; + padding-top: 0.1rem; + } + + &:nth-child(2) { + display: flex; + flex-flow: column nowrap; + align-items: flex-start; + justify-content: center; + flex: 1; + + .desc { + display: flex; + flex-flow: column nowrap; + align-items: center; + justify-content: flex-start; + overflow: hidden; + width: 100%; + height: 1.85rem; + position: relative; + + &.active { + h4:hover { + color: var(--accent-color-primary); + .more { + color: var(--accent-color-primary); + opacity: 1; + } + } + } + + > button { + position: absolute; + top: 0; + left: 0; + height: 1.85rem; + max-width: 100%; + width: auto; + + > h4 { + color: var(--text-color-secondary); + transition: color var(--transition-duration); + font-family: InterSemiBold, sans-serif; + text-align: left; + font-size: 1.05rem; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding: 0.15rem 1.75rem 0rem 0; + width: 100%; + + > svg { + color: var(--text-color-secondary); + transition: all var(--transition-duration); + position: absolute; + right: 0.2rem; + top: 0.43rem; + display: flex; + align-items: center; + font-size: 1rem; + opacity: 0.5; + margin-left: 0.4rem; + } + } + } + } + } + } +`; + +export const PageToggleWrapper = styled.div` + color: var(--text-color-secondary); + border-radius: 1.5rem; + position: relative; + top: 0.2rem; + display: flex; + flex-flow: row wrap; + margin-left: 0.5rem; + + > button { + margin: 0 0.5rem; + opacity: 0.75; + font-size: 1.1rem; + transition: color var(--transition-duration); + > svg { + color: var(--text-color-secondary); + } + &:hover { + opacity: 1; + color: var(--accent-color-primary); + } + &:disabled { + color: var(--text-color-secondary); + opacity: var(--opacity-disabled); + } + } + + h4 { + @media (max-width: ${SmallFontSizeMaxWidth}px) { + display: none; + } + margin: 0; + span { + margin: 0 0.5rem; + } + &.disabled { + opacity: var(--opacity-disabled); + } + } +`; diff --git a/src/pages/Overview/StakeStatus/Tips/index.tsx b/src/pages/Overview/StakeStatus/Tips/index.tsx new file mode 100644 index 0000000000..0055ecda0d --- /dev/null +++ b/src/pages/Overview/StakeStatus/Tips/index.tsx @@ -0,0 +1,202 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { setStateWithRef } from '@polkadot-cloud/utils'; +import throttle from 'lodash.throttle'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { TipsConfig } from 'config/tips'; +import { DefaultLocale, TipsThresholdMedium, TipsThresholdSmall } from 'consts'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { useStaking } from 'contexts/Staking'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useUi } from 'contexts/UI'; +import { useFillVariables } from 'library/Hooks/useFillVariables'; +import type { AnyJson } from 'types'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Items } from './Items'; +import { PageToggle } from './PageToggle'; +import { Syncing } from './Syncing'; +import { TipsWrapper } from './Wrappers'; + +export const Tips = () => { + const { i18n, t } = useTranslation(); + const { network } = useNetwork(); + const { isNetworkSyncing } = useUi(); + const { activeAccount } = useActiveAccounts(); + const { fillVariables } = useFillVariables(); + const { membership } = usePoolMemberships(); + const { isNominating, staking } = useStaking(); + const { isOwner } = useActivePools(); + const { feeReserve, getTransferOptions } = useTransferOptions(); + const { minNominatorBond } = staking; + const transferOptions = getTransferOptions(activeAccount); + + // multiple tips per row is currently turned off. + const multiTipsPerRow = false; + + // helper function to determine the number of items to display per page. + // UI displays 1 item by default. + const getItemsPerPage = () => { + if (!multiTipsPerRow) { + return 1; + } + if (window.innerWidth < TipsThresholdSmall) { + return 1; + } + if ( + window.innerWidth >= TipsThresholdSmall && + window.innerWidth < TipsThresholdMedium + ) { + return 2; + } + return 3; + }; + + // helper function to determine which page we should be on upon page resize. + // This function ensures totalPages is never surpassed, but does not guarantee + // that the start item will maintain across resizes. + const getPage = () => { + const totalItmes = isNetworkSyncing ? 1 : items.length; + const itemsPerPage = getItemsPerPage(); + const totalPages = Math.ceil(totalItmes / itemsPerPage); + if (pageRef.current > totalPages) { + return totalPages; + } + const end = pageRef.current * itemsPerPage; + const start = end - (itemsPerPage - 1); + return Math.ceil(start / itemsPerPage); + }; + + // resize callback + const resizeCallback = () => { + setStateWithRef(getPage(), setPage, pageRef); + setStateWithRef(getItemsPerPage(), setItemsPerPageState, itemsPerPageRef); + }; + + // throttle resize callback + const throttledResizeCallback = throttle(resizeCallback, 200, { + trailing: true, + leading: false, + }); + + // re-sync page when active account changes + useEffect(() => { + setStateWithRef(getPage(), setPage, pageRef); + }, [activeAccount, network]); + + // resize event listener + useEffect(() => { + window.addEventListener('resize', throttledResizeCallback); + return () => { + window.removeEventListener('resize', throttledResizeCallback); + }; + }, []); + + // store the current amount of allowed items on display + const [itemsPerPage, setItemsPerPageState] = useState<number>( + getItemsPerPage() + ); + const itemsPerPageRef = useRef(itemsPerPage); + + // store the current page + const [page, setPage] = useState<number>(1); + const pageRef = useRef(page); + + // accumulate segments to include in tips + const segments: AnyJson = []; + if (!activeAccount) { + segments.push(1); + } else if (!isNominating() && !membership) { + if ( + transferOptions.freeBalance + .minus(feeReserve) + .isGreaterThan(minNominatorBond) + ) { + segments.push(2); + } else { + segments.push(3); + } + segments.push(4); + } else { + if (isNominating()) { + segments.push(5); + } + if (membership) { + if (!isOwner()) { + segments.push(6); + } else { + segments.push(7); + } + } + segments.push(8); + } + + // filter tips relevant to connected account. + let items = TipsConfig.filter((i: AnyJson) => segments.includes(i.s)); + + items = items.map((i: any) => { + const { id } = i; + + return fillVariables( + { + ...i, + title: t(`${id}.0`, { ns: 'tips' }), + subtitle: t(`${id}.1`, { ns: 'tips' }), + description: i18n.getResource( + i18n.resolvedLanguage ?? DefaultLocale, + 'tips', + `${id}.2` + ), + }, + ['title', 'subtitle', 'description'] + ); + }); + + // determine items to be displayed + const end = isNetworkSyncing + ? 1 + : Math.min(pageRef.current * itemsPerPageRef.current, items.length); + const start = isNetworkSyncing + ? 1 + : pageRef.current * itemsPerPageRef.current - (itemsPerPageRef.current - 1); + + const itemsDisplay = items.slice(start - 1, end); + + const setPageHandler = (newPage: number) => { + setStateWithRef(newPage, setPage, pageRef); + }; + return ( + <TipsWrapper> + <div style={{ flexGrow: 1 }}> + {isNetworkSyncing ? ( + <Syncing /> + ) : ( + <Items + items={itemsDisplay} + page={pageRef.current} + showTitle={false} + /> + )} + </div> + <div + style={{ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }} + > + <PageToggle + start={start} + end={end} + page={page} + itemsPerPage={itemsPerPage} + totalItems={items.length} + setPageHandler={setPageHandler} + /> + </div> + </TipsWrapper> + ); +}; diff --git a/src/pages/Overview/StakeStatus/Tips/types.ts b/src/pages/Overview/StakeStatus/Tips/types.ts new file mode 100644 index 0000000000..1d53e35f20 --- /dev/null +++ b/src/pages/Overview/StakeStatus/Tips/types.ts @@ -0,0 +1,11 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface PageToggleProps { + start: number; + end: number; + page: number; + itemsPerPage: number; + totalItems: number; + setPageHandler: (p: number) => void; +} diff --git a/src/pages/Overview/StakeStatus/Wrappers.ts b/src/pages/Overview/StakeStatus/Wrappers.ts new file mode 100644 index 0000000000..d345659ff2 --- /dev/null +++ b/src/pages/Overview/StakeStatus/Wrappers.ts @@ -0,0 +1,45 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { SideMenuStickyThreshold } from 'consts'; + +export const StatusWrapper = styled.div` + display: flex; + flex-wrap: wrap; + padding: 1.5rem 1.5rem 0 1.5rem; + + @media (max-width: ${SideMenuStickyThreshold}px) { + padding: 1.5rem 0.75rem 0 0.75rem; + } + + > div { + @media (max-width: ${SideMenuStickyThreshold}px) { + margin-top: 1rem; + } + + &:first-child { + margin-top: 0; + } + + &:last-child { + padding-left: 1.5rem; + @media (max-width: ${SideMenuStickyThreshold}px) { + padding-left: 0; + } + } + + > section { + border-bottom: 1px solid var(--border-primary-color); + padding-bottom: 0.75rem; + @media (max-width: ${SideMenuStickyThreshold}px) { + padding-bottom: 0.5rem; + } + border-radius: 0; + + > div { + padding-top: 0; + } + } + } +`; diff --git a/src/pages/Overview/StakeStatus/index.tsx b/src/pages/Overview/StakeStatus/index.tsx new file mode 100644 index 0000000000..d689a64039 --- /dev/null +++ b/src/pages/Overview/StakeStatus/index.tsx @@ -0,0 +1,34 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { RowSection } from '@polkadot-cloud/react'; +import { usePlugins } from 'contexts/Plugins'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { NominationStatus } from 'pages/Nominate/Active/Status/NominationStatus'; +import { MembershipStatus } from 'pages/Pools/Home/Status/MembershipStatus'; +import { Tips } from './Tips'; +import { StatusWrapper } from './Wrappers'; + +export const StakeStatus = () => { + const { plugins } = usePlugins(); + const showTips = plugins.includes('tips'); + + return ( + <CardWrapper style={{ padding: 0 }}> + <StatusWrapper> + <RowSection secondary> + <section> + <NominationStatus showButtons={false} /> + </section> + </RowSection> + <RowSection hLast vLast> + <section> + <MembershipStatus showButtons={false} /> + </section> + </RowSection> + </StatusWrapper> + + {showTips ? <Tips /> : null} + </CardWrapper> + ); +}; diff --git a/src/pages/Overview/Stats/ActiveEraTimeLeft.tsx b/src/pages/Overview/Stats/ActiveEraTimeLeft.tsx new file mode 100644 index 0000000000..9eaa264ca5 --- /dev/null +++ b/src/pages/Overview/Stats/ActiveEraTimeLeft.tsx @@ -0,0 +1,44 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { fromUnixTime } from 'date-fns'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useEraTimeLeft } from 'library/Hooks/useEraTimeLeft'; +import { useTimeLeft } from 'library/Hooks/useTimeLeft'; +import { fromNow } from 'library/Hooks/useTimeLeft/utils'; +import { Timeleft } from 'library/StatBoxList/Timeleft'; + +export const ActiveEraStat = () => { + const { t } = useTranslation('pages'); + const { apiStatus } = useApi(); + const { activeEra } = useNetworkMetrics(); + const { get: getEraTimeleft } = useEraTimeLeft(); + const { timeleft, setFromNow } = useTimeLeft(); + + const dateFrom = fromUnixTime(Date.now() / 1000); + const dateTo = fromNow(getEraTimeleft().timeleft.toNumber()); + + // re-set timer on era change (also covers network change). + useEffect(() => { + setFromNow(dateFrom, dateTo); + }, [apiStatus, activeEra]); + + // NOTE: this maybe should be called in an interval. Needs more testing. + const { percentSurpassed, percentRemaining } = getEraTimeleft(); + + const params = { + label: t('overview.timeRemainingThisEra'), + timeleft: timeleft.formatted, + graph: { + value1: activeEra.index.isZero() ? 0 : percentSurpassed.toNumber(), + value2: activeEra.index.isZero() ? 100 : percentRemaining.toNumber(), + }, + tooltip: `Era ${new BigNumber(activeEra.index).toFormat()}` ?? undefined, + helpKey: 'Era', + }; + return <Timeleft {...params} />; +}; diff --git a/src/pages/Overview/Stats/HistoricalRewardsRate.tsx b/src/pages/Overview/Stats/HistoricalRewardsRate.tsx new file mode 100644 index 0000000000..f147d07739 --- /dev/null +++ b/src/pages/Overview/Stats/HistoricalRewardsRate.tsx @@ -0,0 +1,38 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useInflation } from 'library/Hooks/useInflation'; +import { Text } from 'library/StatBoxList/Text'; + +export const HistoricalRewardsRateStat = () => { + const { t } = useTranslation('pages'); + const { metrics } = useNetworkMetrics(); + const { inflation, stakedReturn } = useInflation(); + const { totalIssuance } = metrics; + + const value = `${ + totalIssuance.isZero() + ? '0' + : new BigNumber(stakedReturn).decimalPlaces(2).toFormat() + }%`; + + const secondaryValue = + totalIssuance.isZero() || stakedReturn === 0 + ? undefined + : `/ ${new BigNumber(Math.max(0, stakedReturn - inflation)) + .decimalPlaces(2) + .toFormat()}% ${t('overview.afterInflation')}`; + + const params = { + label: t('overview.historicalRewardsRate'), + value, + secondaryValue, + helpKey: 'Historical Rewards Rate', + primary: true, + }; + + return <Text {...params} />; +}; diff --git a/src/pages/Overview/Stats/SupplyStaked.tsx b/src/pages/Overview/Stats/SupplyStaked.tsx new file mode 100644 index 0000000000..b72ccacb06 --- /dev/null +++ b/src/pages/Overview/Stats/SupplyStaked.tsx @@ -0,0 +1,47 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useStaking } from 'contexts/Staking'; +import { Pie } from 'library/StatBoxList/Pie'; +import { useNetwork } from 'contexts/Network'; + +export const SupplyStakedStat = () => { + const { t } = useTranslation('pages'); + const { units, unit } = useNetwork().networkData; + const { metrics } = useNetworkMetrics(); + const { staking } = useStaking(); + + const { lastTotalStake } = staking; + const { totalIssuance } = metrics; + + // total supply as percent. + const totalIssuanceUnit = planckToUnit(totalIssuance, units); + const lastTotalStakeUnit = planckToUnit(lastTotalStake, units); + const supplyAsPercent = + lastTotalStakeUnit.isZero() || totalIssuanceUnit.isZero() + ? new BigNumber(0) + : lastTotalStakeUnit.dividedBy(totalIssuanceUnit.multipliedBy(0.01)); + + const params = { + label: t('overview.unitSupplyStaked', { unit }), + stat: { + value: `${supplyAsPercent.decimalPlaces(2).toFormat()}`, + unit: '%', + }, + graph: { + value1: supplyAsPercent.decimalPlaces(2).toNumber(), + value2: new BigNumber(100) + .minus(supplyAsPercent) + .decimalPlaces(2) + .toNumber(), + }, + tooltip: `${supplyAsPercent.decimalPlaces(2).toFormat()}%`, + helpKey: 'Supply Staked', + }; + + return <Pie {...params} />; +}; diff --git a/src/pages/Overview/Wrappers.ts b/src/pages/Overview/Wrappers.ts new file mode 100644 index 0000000000..4f9525237d --- /dev/null +++ b/src/pages/Overview/Wrappers.ts @@ -0,0 +1,105 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { SectionFullWidthThreshold } from 'consts'; + +export const Separator = styled.div` + border-bottom: 1px solid var(--border-primary-color); + margin-top: 0.8rem; + width: 100%; + height: 1px; +`; + +export const MoreWrapper = styled.div` + padding: 0 0.5rem; + padding-bottom: 1rem; + width: 100%; + display: flex; + flex-flow: column wrap; + margin-top: 2.5rem; + + @media (max-width: ${SectionFullWidthThreshold}px) { + margin-top: 1.5rem; + padding: 0 0.75rem; + margin-bottom: 0.5rem; + } + h4 { + margin-top: 0.25rem; + margin-bottom: 0.5rem; + } + section { + display: flex; + align-items: center; + width: 100%; + margin-top: 0.1rem; + div { + margin-left: 0.5rem; + } + } +`; + +export const BannerWrapper = styled.div` + &.light { + border: 1px solid var(--accent-color-primary); + background: var(--background-primary); + + .label, + > div h3 { + color: var(--accent-color-primary); + } + } + &.dark { + border: 1px solid var(--border-secondary-color); + background: var(--accent-color-secondary); + + .label, + > div h3 { + color: white; + } + div > button { + color: white; + border-color: white; + } + } + box-shadow: var(--card-shadow-secondary); + border-radius: 1.25rem; + padding: 1.25rem 1.5rem; + margin-top: 5rem; + width: 100%; + + .label { + color: white; + margin-bottom: 0.75rem; + + .icon { + margin-right: 0.35rem; + } + } + + > div { + display: flex; + align-items: center; + + h3 { + font-family: InterSemiBold, sans-serif; + line-height: 1.8rem; + } + button { + flex-basis: auto; + font-size: 1.1rem; + margin-left: 0.75rem; + } + @media (max-width: 800px) { + flex-direction: column; + align-items: flex-start; + + button { + margin-top: 0.75rem; + margin-left: 0; + position: relative; + left: -0.5rem; + } + } + } +`; diff --git a/src/pages/Overview/index.tsx b/src/pages/Overview/index.tsx new file mode 100644 index 0000000000..2e2c6bc783 --- /dev/null +++ b/src/pages/Overview/index.tsx @@ -0,0 +1,132 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + Odometer, + PageHeading, + PageRow, + PageTitle, + RowSection, +} from '@polkadot-cloud/react'; +import BigNumber from 'bignumber.js'; +import { formatDistance, fromUnixTime, getUnixTime } from 'date-fns'; +import { useTranslation } from 'react-i18next'; +import { DefaultLocale } from 'consts'; +import { useSubscan } from 'contexts/Plugins/Subscan'; +import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; +import { formatRewardsForGraphs } from 'library/Graphs/Utils'; +import { StatBoxList } from 'library/StatBoxList'; +import { locales } from 'locale'; +import { ControllerNotStash } from 'pages/Nominate/Active/ControllerNotStash'; +import { minDecimalPlaces, planckToUnit } from '@polkadot-cloud/utils'; +import { PluginLabel } from 'library/PluginLabel'; +import { useNetwork } from 'contexts/Network'; +import { ActiveAccounts } from './ActiveAccounts'; +import { BalanceChart } from './BalanceChart'; +import { BalanceLinks } from './BalanceLinks'; +import { NetworkStats } from './NetworkSats'; +import { Payouts } from './Payouts'; +import { StakeStatus } from './StakeStatus'; +import { ActiveEraStat } from './Stats/ActiveEraTimeLeft'; +import { HistoricalRewardsRateStat } from './Stats/HistoricalRewardsRate'; +import { SupplyStakedStat } from './Stats/SupplyStaked'; + +export const Overview = () => { + const { i18n, t } = useTranslation('pages'); + const { + networkData: { + units, + brand: { token: Token }, + }, + } = useNetwork(); + const { payouts, poolClaims, unclaimedPayouts } = useSubscan(); + + const { lastReward } = formatRewardsForGraphs( + new Date(), + 14, + units, + payouts, + poolClaims, + unclaimedPayouts + ); + + const PAYOUTS_HEIGHT = 380; + + let formatFrom = new Date(); + let formatTo = new Date(); + let formatOpts = {}; + if (lastReward !== null) { + formatFrom = fromUnixTime( + lastReward?.block_timestamp ?? getUnixTime(new Date()) + ); + formatTo = new Date(); + formatOpts = { + addSuffix: true, + locale: locales[i18n.resolvedLanguage ?? DefaultLocale], + }; + } + + return ( + <> + <PageTitle title={t('overview.overview')} /> + <PageRow> + <PageHeading> + <ActiveAccounts /> + </PageHeading> + </PageRow> + <StatBoxList> + <HistoricalRewardsRateStat /> + <SupplyStakedStat /> + <ActiveEraStat /> + </StatBoxList> + <ControllerNotStash /> + <PageRow> + <StakeStatus /> + </PageRow> + <PageRow> + <RowSection secondary> + <CardWrapper height={PAYOUTS_HEIGHT}> + <BalanceChart /> + <BalanceLinks /> + </CardWrapper> + </RowSection> + <RowSection hLast vLast> + <CardWrapper style={{ minHeight: PAYOUTS_HEIGHT }}> + <PluginLabel plugin="subscan" /> + <CardHeaderWrapper> + <h4>{t('overview.recentPayouts')}</h4> + <h2> + <Token className="networkIcon" /> + <Odometer + value={minDecimalPlaces( + lastReward === null + ? '0' + : planckToUnit( + new BigNumber(lastReward.amount), + units + ).toFormat(), + 2 + )} + /> + + <span className="note"> + {lastReward === null ? ( + '' + ) : ( + <> +  {formatDistance(formatFrom, formatTo, formatOpts)} + </> + )} + </span> + </h2> + </CardHeaderWrapper> + <Payouts /> + </CardWrapper> + </RowSection> + </PageRow> + <PageRow> + <NetworkStats /> + </PageRow> + </> + ); +}; diff --git a/src/pages/Payouts/PayoutList/context.tsx b/src/pages/Payouts/PayoutList/context.tsx new file mode 100644 index 0000000000..d985cbca21 --- /dev/null +++ b/src/pages/Payouts/PayoutList/context.tsx @@ -0,0 +1,36 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +import React, { useState } from 'react'; +import type { PayoutListContextInterface } from 'pages/Pools/types'; + +export const PayoutListContext = + React.createContext<PayoutListContextInterface>({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setListFormat: (v: string) => {}, + listFormat: 'col', + }); + +export const usePayoutList = () => React.useContext(PayoutListContext); + +export const PayoutListProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [listFormat, _setListFormat] = useState('col'); + + const setListFormat = (v: string) => { + _setListFormat(v); + }; + + return ( + <PayoutListContext.Provider + value={{ + setListFormat, + listFormat, + }} + > + {children} + </PayoutListContext.Provider> + ); +}; diff --git a/src/pages/Payouts/PayoutList/index.tsx b/src/pages/Payouts/PayoutList/index.tsx new file mode 100644 index 0000000000..9d9143f051 --- /dev/null +++ b/src/pages/Payouts/PayoutList/index.tsx @@ -0,0 +1,279 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBars, faGripVertical } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ellipsisFn, isNotZero, planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { formatDistance, fromUnixTime } from 'date-fns'; +import { motion } from 'framer-motion'; +import React, { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { DefaultLocale, ListItemsPerBatch, ListItemsPerPage } from 'consts'; +import { useApi } from 'contexts/Api'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { StakingContext } from 'contexts/Staking'; +import { useTheme } from 'contexts/Themes'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { Header, List, Wrapper as ListWrapper } from 'library/List'; +import { MotionContainer } from 'library/List/MotionContainer'; +import { Pagination } from 'library/List/Pagination'; +import { Identity } from 'library/ListItem/Labels/Identity'; +import { PoolIdentity } from 'library/ListItem/Labels/PoolIdentity'; +import { locales } from 'locale'; +import type { AnySubscan } from 'types'; +import { useNetwork } from 'contexts/Network'; +import { ItemWrapper } from '../Wrappers'; +import type { PayoutListProps } from '../types'; +import { PayoutListProvider, usePayoutList } from './context'; + +export const PayoutListInner = ({ + allowMoreCols, + pagination, + title, + payouts: initialPayouts, + disableThrottle = false, +}: PayoutListProps) => { + const { i18n, t } = useTranslation('pages'); + const { mode } = useTheme(); + const { isReady } = useApi(); + const { + networkData: { units, unit, colors }, + } = useNetwork(); + const { activeEra } = useNetworkMetrics(); + const { listFormat, setListFormat } = usePayoutList(); + const { validators } = useValidators(); + const { bondedPools } = useBondedPools(); + + // current page + const [page, setPage] = useState<number>(1); + + // current render iteration + const [renderIteration, _setRenderIteration] = useState<number>(1); + + // manipulated list (ordering, filtering) of payouts + const [payouts, setPayouts] = useState(initialPayouts); + + // is this the initial fetch + const [fetched, setFetched] = useState<boolean>(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 + ); + + // refetch list when list changes + useEffect(() => { + setFetched(false); + }, [initialPayouts]); + + // configure list when network is ready to fetch + useEffect(() => { + if (isReady && isNotZero(activeEra.index) && !fetched) { + setPayouts(initialPayouts); + setFetched(true); + } + }, [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; + } + + if (!payouts.length) { + return <></>; + } + + return ( + <ListWrapper> + <Header> + <div> + <h4>{title}</h4> + </div> + <div> + <button type="button" onClick={() => setListFormat('row')}> + <FontAwesomeIcon + icon={faBars} + color={listFormat === 'row' ? colors.primary[mode] : 'inherit'} + /> + </button> + <button type="button" onClick={() => setListFormat('col')}> + <FontAwesomeIcon + icon={faGripVertical} + color={listFormat === 'col' ? colors.primary[mode] : 'inherit'} + /> + </button> + </div> + </Header> + <List $flexBasisLarge={allowMoreCols ? '33.33%' : '50%'}> + {pagination && ( + <Pagination page={page} total={totalPages} setter={setPage} /> + )} + <MotionContainer> + {listPayouts.map((p: AnySubscan, index: number) => { + const label = + p.event_id === 'PaidOut' + ? t('payouts.poolClaim') + : p.event_id === 'Rewarded' + ? t('payouts.payout') + : p.event_id; + + const labelClass = + p.event_id === 'PaidOut' + ? 'claim' + : p.event_id === 'Rewarded' + ? 'reward' + : undefined; + + // get validator if it exists + const validator = validators.find( + (v) => v.address === p.validator_stash + ); + + // get pool if it exists + const pool = bondedPools.find( + ({ id }) => String(id) === String(p.pool_id) + ); + + const batchIndex = validator + ? validators.indexOf(validator) + : pool + ? bondedPools.indexOf(pool) + : 0; + + return ( + <motion.div + className={`item ${listFormat === 'row' ? 'row' : 'col'}`} + key={`nomination_${index}`} + variants={{ + hidden: { + y: 15, + opacity: 0, + }, + show: { + y: 0, + opacity: 1, + }, + }} + > + <ItemWrapper> + <div className="inner"> + <div className="row"> + <div> + <div> + <h4 className={labelClass}> + <> + {p.event_id === 'Slashed' ? '-' : '+'} + {planckToUnit( + new BigNumber(p.amount), + units + ).toString()}{' '} + {unit} + </> + </h4> + </div> + <div> + <h5 className={labelClass}>{label}</h5> + </div> + </div> + </div> + <div className="row"> + <div> + <div> + {label === t('payouts.payout') && ( + <> + {batchIndex > 0 ? ( + <Identity address={p.validator_stash} /> + ) : ( + <div>{ellipsisFn(p.validator_stash)}</div> + )} + </> + )} + {label === t('payouts.poolClaim') && ( + <> + {pool ? ( + <PoolIdentity + batchKey="bonded_pools" + batchIndex={batchIndex} + pool={pool} + /> + ) : ( + <h4> + {t('payouts.fromPool')} {p.pool_id} + </h4> + )} + </> + )} + {label === t('payouts.slashed') && ( + <h4>{t('payouts.deductedFromBond')}</h4> + )} + </div> + <div> + <h5> + {formatDistance( + fromUnixTime(p.block_timestamp), + new Date(), + { + addSuffix: true, + locale: + locales[ + i18n.resolvedLanguage ?? DefaultLocale + ], + } + )} + </h5> + </div> + </div> + </div> + </div> + </ItemWrapper> + </motion.div> + ); + })} + </MotionContainer> + </List> + </ListWrapper> + ); +}; + +export const PayoutList = (props: PayoutListProps) => ( + <PayoutListProvider> + <PayoutListShouldUpdate {...props} /> + </PayoutListProvider> +); + +export class PayoutListShouldUpdate extends React.Component { + static contextType = StakingContext; + + render() { + return <PayoutListInner {...this.props} />; + } +} diff --git a/src/pages/Payouts/Stats/LastEraPayout.tsx b/src/pages/Payouts/Stats/LastEraPayout.tsx new file mode 100644 index 0000000000..de4d13b7c8 --- /dev/null +++ b/src/pages/Payouts/Stats/LastEraPayout.tsx @@ -0,0 +1,26 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useStaking } from 'contexts/Staking'; +import { Number } from 'library/StatBoxList/Number'; +import { useNetwork } from 'contexts/Network'; + +export const LastEraPayoutStat = () => { + const { t } = useTranslation('pages'); + const { unit, units } = useNetwork().networkData; + const { staking } = useStaking(); + const { lastReward } = staking; + + const lastRewardUnit = planckToUnit(lastReward, units).toNumber(); + + const params = { + label: t('payouts.lastEraPayout'), + value: lastRewardUnit, + decimals: 3, + unit, + helpKey: 'Last Era Payout', + }; + return <Number {...params} />; +}; diff --git a/src/pages/Payouts/Wrappers.ts b/src/pages/Payouts/Wrappers.ts new file mode 100644 index 0000000000..85981a9a27 --- /dev/null +++ b/src/pages/Payouts/Wrappers.ts @@ -0,0 +1,95 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +export const ItemWrapper = styled(motion.div)` + padding: 0.5rem; + width: 100%; + + > .inner { + background: var(--background-list-item); + padding: 0 0.75rem; + flex: 1; + border-radius: 1rem; + display: flex; + flex-flow: column wrap; + align-items: center; + flex: 1; + max-width: 100%; + + > .row { + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: center; + + &:first-child { + padding: 1rem 0 0.75rem 0; + } + + &:last-child { + border-top: 1px solid var(--border-primary-color); + padding-top: 0rem; + + > div { + min-height: 3.2rem; + } + } + + > div { + display: flex; + flex-flow: row wrap; + align-items: center; + flex: 1; + max-width: 100%; + + h4 { + color: var(--text-color-secondary); + font-family: InterSemiBold, sans-serif; + &.claim { + color: var(--accent-color-secondary); + } + &.reward { + color: var(--accent-color-primary); + } + } + + h5 { + color: var(--text-color-secondary); + &.claim { + color: var(--accent-color-secondary); + border: 1px solid var(--accent-color-secondary); + border-radius: 0.75rem; + padding: 0.2rem 0.5rem; + } + &.reward { + color: var(--accent-color-primary); + border: 1px solid var(--accent-color-primary); + border-radius: 0.75rem; + padding: 0.2rem 0.5rem; + } + } + + > div:first-child { + flex-grow: 1; + display: flex; + flex-flow: row wrap; + align-items: center; + } + + > div:last-child { + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + + > h4 { + color: var(--text-color-secondary); + opacity: 0.8; + } + } + } + } + } +`; diff --git a/src/pages/Payouts/index.tsx b/src/pages/Payouts/index.tsx new file mode 100644 index 0000000000..184dd7d55f --- /dev/null +++ b/src/pages/Payouts/index.tsx @@ -0,0 +1,124 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { ButtonHelp, PageRow, PageTitle } from '@polkadot-cloud/react'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { MaxPayoutDays } from 'consts'; +import { useHelp } from 'contexts/Help'; +import { usePlugins } from 'contexts/Plugins'; +import { useStaking } from 'contexts/Staking'; +import { useSubscan } from 'contexts/Plugins/Subscan'; +import { useUi } from 'contexts/UI'; +import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; +import { PayoutBar } from 'library/Graphs/PayoutBar'; +import { PayoutLine } from 'library/Graphs/PayoutLine'; +import { formatSize, sortNonZeroPayouts } from 'library/Graphs/Utils'; +import { GraphWrapper } from 'library/Graphs/Wrapper'; +import { useSize } from 'library/Hooks/useSize'; +import { StatBoxList } from 'library/StatBoxList'; +import { StatusLabel } from 'library/StatusLabel'; +import type { AnySubscan, PageProps } from 'types'; +import { PluginLabel } from 'library/PluginLabel'; +import { PayoutList } from './PayoutList'; +import { LastEraPayoutStat } from './Stats/LastEraPayout'; + +export const Payouts = ({ page }: PageProps) => { + const { t } = useTranslation(); + const { payouts, poolClaims, payoutsFromDate, payoutsToDate } = useSubscan(); + const { isSyncing } = useUi(); + const { plugins } = usePlugins(); + const { inSetup } = useStaking(); + const notStaking = !isSyncing && inSetup(); + const { openHelp } = useHelp(); + + const [payoutsList, setPayoutLists] = useState<AnySubscan>([]); + + const { key } = page; + + const ref = useRef<HTMLDivElement>(null); + const size = useSize(ref.current); + const { width, height, minHeight } = formatSize(size, 280); + + useEffect(() => { + // filter zero rewards and order via block timestamp, most recent first. + setPayoutLists(sortNonZeroPayouts(payouts, poolClaims, true)); + }, [payouts, poolClaims]); + + return ( + <> + <PageTitle title={t(key, { ns: 'base' })} /> + <StatBoxList> + <LastEraPayoutStat /> + </StatBoxList> + <PageRow> + <CardWrapper> + <PluginLabel plugin="subscan" /> + <CardHeaderWrapper> + <h4> + {t('payouts.payoutHistory', { ns: 'pages' })} + <ButtonHelp + marginLeft + onClick={() => openHelp('Payout History')} + /> + </h4> + <h2> + {payoutsFromDate && payoutsToDate ? ( + <> + {payoutsFromDate} + {payoutsToDate !== payoutsFromDate && ( + <> - {payoutsToDate}</> + )} + </> + ) : ( + t('payouts.none', { ns: 'pages' }) + )} + </h2> + </CardHeaderWrapper> + <div className="inner" ref={ref} style={{ minHeight }}> + {!plugins.includes('subscan') ? ( + <StatusLabel + status="active_service" + statusFor="subscan" + title={t('payouts.subscanDisabled', { ns: 'pages' })} + topOffset="30%" + /> + ) : ( + <StatusLabel + status="sync_or_setup" + title={t('payouts.notStaking', { ns: 'pages' })} + topOffset="30%" + /> + )} + + <GraphWrapper + style={{ + height: `${height}px`, + width: `${width}px`, + position: 'absolute', + opacity: notStaking ? 0.75 : 1, + transition: 'opacity 0.5s', + }} + > + <PayoutBar days={MaxPayoutDays} height="165px" /> + <PayoutLine days={MaxPayoutDays} average={10} height="65px" /> + </GraphWrapper> + </div> + </CardWrapper> + </PageRow> + {!payoutsList?.length ? ( + <></> + ) : ( + <PageRow> + <CardWrapper> + <PayoutList + title={t('payouts.recentPayouts', { ns: 'pages' })} + payouts={payoutsList} + pagination + /> + </CardWrapper> + </PageRow> + )} + </> + ); +}; diff --git a/src/pages/Payouts/types.ts b/src/pages/Payouts/types.ts new file mode 100644 index 0000000000..f9c08440d0 --- /dev/null +++ b/src/pages/Payouts/types.ts @@ -0,0 +1,13 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +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/Create/Bond/index.tsx b/src/pages/Pools/Create/Bond/index.tsx new file mode 100644 index 0000000000..d912d4d0e6 --- /dev/null +++ b/src/pages/Pools/Create/Bond/index.tsx @@ -0,0 +1,93 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import BigNumber from 'bignumber.js'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSetup } from 'contexts/Setup'; +import { useTxMeta } from 'contexts/TxMeta'; +import { BondFeedback } from 'library/Form/Bond/BondFeedback'; +import { CreatePoolStatusBar } from 'library/Form/CreatePoolStatusBar'; +import { Footer } from 'library/SetupSteps/Footer'; +import { Header } from 'library/SetupSteps/Header'; +import { MotionContainer } from 'library/SetupSteps/MotionContainer'; +import type { SetupStepProps } from 'library/SetupSteps/types'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const Bond = ({ section }: SetupStepProps) => { + const { t } = useTranslation('pages'); + const { activeAccount } = useActiveAccounts(); + const { txFees } = useTxMeta(); + const { getSetupProgress, setActiveAccountSetup } = useSetup(); + const setup = getSetupProgress('pool', activeAccount); + const { progress } = setup; + + // either free to bond or existing setup value + const initialBondValue = progress.bond === '0' ? '' : progress.bond; + + // store local bond amount for form control + const [bond, setBond] = useState<{ bond: string }>({ + bond: initialBondValue, + }); + + // bond valid + const [bondValid, setBondValid] = useState<boolean>(false); + + // handler for updating bond + const handleSetupUpdate = (value: any) => { + setActiveAccountSetup('pool', value); + }; + + // update bond on account change + useEffect(() => { + setBond({ + bond: initialBondValue, + }); + }, [activeAccount]); + + // apply initial bond value to setup progress + useEffect(() => { + // only update if Bond is currently active + if (setup.section === section) { + setActiveAccountSetup('pool', { + ...progress, + bond: initialBondValue, + }); + } + }, [setup.section]); + + return ( + <> + <Header + thisSection={section} + complete={progress.bond !== '0' && progress.bond !== ''} + title={t('pools.bond')} + helpKey="Bonding" + bondFor="pool" + /> + <MotionContainer thisSection={section} activeSection={setup.section}> + <BondFeedback + syncing={txFees.isZero()} + bondFor="pool" + inSetup + listenIsValid={(valid) => setBondValid(valid)} + defaultBond={initialBondValue} + setters={[ + { + set: handleSetupUpdate, + current: progress, + }, + { + set: setBond, + current: bond, + }, + ]} + txFees={txFees} + maxWidth + /> + <CreatePoolStatusBar value={new BigNumber(bond.bond)} /> + <Footer complete={bondValid} bondFor="pool" /> + </MotionContainer> + </> + ); +}; diff --git a/src/pages/Pools/Create/PoolName/Input.tsx b/src/pages/Pools/Create/PoolName/Input.tsx new file mode 100644 index 0000000000..4ea4150e11 --- /dev/null +++ b/src/pages/Pools/Create/PoolName/Input.tsx @@ -0,0 +1,55 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const Input = ({ + listenIsValid, + defaultValue, + setters = [], + value = 0, +}: any) => { + const { t } = useTranslation('pages'); + const { activeAccount } = useActiveAccounts(); + + // the current local bond value + const [metadata, setMetadata] = useState(value); + + // handle change for bonding + const handleChange = (e: any) => { + const val = e.target.value; + listenIsValid(val !== ''); + setMetadata(val); + + // apply value to parent setters + for (const s of setters) { + s.set({ + ...s.current, + metadata: val, + }); + } + }; + + // reset value to default when changing account + useEffect(() => { + setMetadata(defaultValue ?? ''); + }, [activeAccount]); + + return ( + <> + <div style={{ margin: '1rem 0' }}> + <input + className="textbox" + style={{ width: '100%', fontFamily: 'InterSemiBold, sans-serif' }} + placeholder={t('pools.poolName')} + type="text" + onChange={(e: React.FormEvent<HTMLInputElement>) => handleChange(e)} + value={metadata ?? ''} + /> + </div> + <p>{t('pools.poolNameSupport')}</p> + </> + ); +}; diff --git a/src/pages/Pools/Create/PoolName/index.tsx b/src/pages/Pools/Create/PoolName/index.tsx new file mode 100644 index 0000000000..6d29c7fc32 --- /dev/null +++ b/src/pages/Pools/Create/PoolName/index.tsx @@ -0,0 +1,81 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useSetup } from 'contexts/Setup'; +import { Footer } from 'library/SetupSteps/Footer'; +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 { Input } from './Input'; + +export const PoolName = ({ section }: SetupStepProps) => { + const { t } = useTranslation('pages'); + const { activeAccount } = useActiveAccounts(); + const { getSetupProgress, setActiveAccountSetup } = useSetup(); + const setup = getSetupProgress('pool', activeAccount); + const { progress } = setup; + + const initialValue = progress.metadata; + + // store local pool name for form control + const [metadata, setMetadata] = useState({ + metadata: initialValue, + }); + + // pool name valid + const [valid, setValid] = useState<boolean>(initialValue !== ''); + + // handler for updating bond + const handleSetupUpdate = (value: any) => { + setActiveAccountSetup('pool', value); + }; + + // update bond on account change + useEffect(() => { + setMetadata({ + metadata: initialValue, + }); + }, [activeAccount]); + + // apply initial metadata to setup progress + useEffect(() => { + // only update if this section is currently active + if (setup.section === section) { + setActiveAccountSetup('pool', { + ...progress, + metadata: initialValue, + }); + } + }, [setup.section]); + + return ( + <> + <Header + thisSection={section} + complete={progress.metadata !== ''} + title={t('pools.poolName')} + bondFor="pool" + /> + <MotionContainer thisSection={section} activeSection={setup.section}> + <Input + listenIsValid={setValid} + defaultValue={initialValue} + setters={[ + { + set: handleSetupUpdate, + current: progress, + }, + { + set: setMetadata, + current: metadata, + }, + ]} + /> + <Footer complete={valid} bondFor="pool" /> + </MotionContainer> + </> + ); +}; diff --git a/src/pages/Pools/Create/PoolRoles/index.tsx b/src/pages/Pools/Create/PoolRoles/index.tsx new file mode 100644 index 0000000000..1f3cb57167 --- /dev/null +++ b/src/pages/Pools/Create/PoolRoles/index.tsx @@ -0,0 +1,100 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useEffect, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useSetup } from 'contexts/Setup'; +import { Footer } from 'library/SetupSteps/Footer'; +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'; + +export const PoolRoles = ({ section }: SetupStepProps) => { + const { t } = useTranslation('pages'); + const { activeAccount } = useActiveAccounts(); + const { getSetupProgress, setActiveAccountSetup } = useSetup(); + const setup = getSetupProgress('pool', activeAccount); + const { progress } = setup; + + // if no roles in setup already, inject `activeAccount` to be + // root and depositor roles. + const initialValue = progress.roles ?? { + root: activeAccount, + depositor: activeAccount, + nominator: activeAccount, + bouncer: activeAccount, + }; + + // store local pool name for form control + const [roles, setRoles] = useState({ + roles: initialValue, + }); + + // pool name valid + const [rolesValid, setRolesValid] = useState<boolean>(true); + + // handler for updating pool roles + const handleSetupUpdate = (value: any) => { + setActiveAccountSetup('pool', value); + }; + + // update pool roles on account change + useEffect(() => { + setRoles({ + roles: initialValue, + }); + }, [activeAccount]); + + // apply initial pool roles to setup progress + useEffect(() => { + // only update if this section is currently active + if (setup.section === section) { + setActiveAccountSetup('pool', { + ...progress, + roles: initialValue, + }); + } + }, [setup.section]); + + return ( + <> + <Header + thisSection={section} + complete={progress.roles !== null} + title={t('pools.roles')} + helpKey="Pool Roles" + bondFor="pool" + /> + <MotionContainer thisSection={section} activeSection={setup.section}> + <h4 style={{ margin: '0.5rem 0' }}> + <Trans defaults={t('pools.poolCreator')} components={{ b: <b /> }} /> + </h4> + <h4 style={{ margin: '0.5rem 0 1.5rem 0' }}> + <Trans + defaults={t('pools.assignedToAnyAccount')} + components={{ b: <b /> }} + /> + </h4> + <Roles + inline + batchKey="pool_roles_create" + listenIsValid={setRolesValid} + defaultRoles={initialValue} + setters={[ + { + set: handleSetupUpdate, + current: progress, + }, + { + set: setRoles, + current: roles, + }, + ]} + /> + <Footer complete={rolesValid} bondFor="pool" /> + </MotionContainer> + </> + ); +}; diff --git a/src/pages/Pools/Create/Summary/Wrapper.ts b/src/pages/Pools/Create/Summary/Wrapper.ts new file mode 100644 index 0000000000..fb8d016312 --- /dev/null +++ b/src/pages/Pools/Create/Summary/Wrapper.ts @@ -0,0 +1,41 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const SummaryWrapper = styled.div` + display: flex; + flex-flow: row wrap; + width: 100%; + margin-bottom: 1rem; + + > section { + border-bottom: 1px solid var(--border-primary-color); + flex-basis: 100%; + display: flex; + flex-flow: row wrap; + align-items: flex-end; + margin-top: 1rem; + padding: 0.5rem 0 0.75rem 0; + + > div:first-child { + color: var(--text-color-secondary); + width: 200px; + display: flex; + flex-flow: row wrap; + align-items: center; + + svg { + color: var(--accent-color-primary); + } + } + + > div:last-child { + color: var(--text-color-secondary); + flex-grow: 1; + display: flex; + flex-flow: row wrap; + align-items: center; + } + } +`; diff --git a/src/pages/Pools/Create/Summary/index.tsx b/src/pages/Pools/Create/Summary/index.tsx new file mode 100644 index 0000000000..a926740aa0 --- /dev/null +++ b/src/pages/Pools/Create/Summary/index.tsx @@ -0,0 +1,152 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCheckCircle } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { unitToPlanck } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { useSetup } from 'contexts/Setup'; +import { Warning } from 'library/Form/Warning'; +import { useBatchCall } from 'library/Hooks/useBatchCall'; +import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; +import { Header } from 'library/SetupSteps/Header'; +import { MotionContainer } from 'library/SetupSteps/MotionContainer'; +import type { SetupStepProps } from 'library/SetupSteps/types'; +import { SubmitTx } from 'library/SubmitTx'; +import { useNetwork } from 'contexts/Network'; +import { useApi } from 'contexts/Api'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { SummaryWrapper } from './Wrapper'; + +export const Summary = ({ section }: SetupStepProps) => { + const { t } = useTranslation('pages'); + const { api } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { stats } = usePoolsConfig(); + const { newBatchCall } = useBatchCall(); + const { accountHasSigner } = useImportedAccounts(); + const { getSetupProgress, removeSetupProgress } = useSetup(); + const { queryPoolMember, addToPoolMembers } = usePoolMembers(); + const { queryBondedPool, addToBondedPools } = useBondedPools(); + const { activeAccount, activeProxy } = useActiveAccounts(); + + const { lastPoolId } = stats; + const poolId = lastPoolId.plus(1); + + const setup = getSetupProgress('pool', activeAccount); + const { progress } = setup; + + const { metadata, bond, roles, nominations } = progress; + + const getTxs = () => { + if (!activeAccount || !api) { + return null; + } + + const targetsToSubmit = nominations.map((item: any) => item.address); + + const bondToSubmit = unitToPlanck(bond, units); + const bondAsString = bondToSubmit.isNaN() ? '0' : bondToSubmit.toString(); + + const txs = [ + api.tx.nominationPools.create( + bondAsString, + roles?.root || activeAccount, + roles?.nominator || activeAccount, + roles?.bouncer || activeAccount + ), + api.tx.nominationPools.nominate(poolId.toString(), targetsToSubmit), + api.tx.nominationPools.setMetadata(poolId.toString(), metadata), + ]; + return newBatchCall(txs, activeAccount); + }; + + const submitExtrinsic = useSubmitExtrinsic({ + tx: getTxs(), + from: activeAccount, + shouldSubmit: true, + callbackSubmit: () => {}, + callbackInBlock: async () => { + // query and add created pool to bondedPools list + const pool = await queryBondedPool(poolId.toNumber()); + addToBondedPools(pool); + + // query and add account to poolMembers list + const member = await queryPoolMember(activeAccount); + addToPoolMembers(member); + + // reset localStorage setup progress + removeSetupProgress('pool', activeAccount); + }, + }); + + return ( + <> + <Header + thisSection={section} + complete={null} + title={t('pools.summary')} + bondFor="pool" + /> + <MotionContainer thisSection={section} activeSection={setup.section}> + {!( + accountHasSigner(activeAccount) || accountHasSigner(activeProxy) + ) && <Warning text={t('pools.readOnly')} />} + <SummaryWrapper> + <section> + <div> + <FontAwesomeIcon icon={faCheckCircle} transform="grow-1" />  {' '} + {t('pools.poolName')}: + </div> + <div>{metadata ?? `${t('pools.notSet')}`}</div> + </section> + <section> + <div> + <FontAwesomeIcon icon={faCheckCircle} transform="grow-1" />  {' '} + {t('pools.bondAmount')}: + </div> + <div> + {new BigNumber(bond).toFormat()} {unit} + </div> + </section> + <section> + <div> + <FontAwesomeIcon icon={faCheckCircle} transform="grow-1" />   + {t('pools.nominating')}: + </div> + <div>{t('nominate.validator', { count: nominations.length })}</div> + </section> + <section> + <div> + <FontAwesomeIcon icon={faCheckCircle} transform="grow-1" />  {' '} + {t('pools.roles')}: + </div> + <div>{t('pools.assigned')}</div> + </section> + </SummaryWrapper> + <div + style={{ + flex: 1, + width: '100%', + borderRadius: '1rem', + overflow: 'hidden', + }} + > + <SubmitTx + submitText={t('pools.createPool')} + valid + {...submitExtrinsic} + displayFor="canvas" /* Edge case: not canvas, but the larger button sizes suit this UI more. */ + /> + </div> + </MotionContainer> + </> + ); +}; diff --git a/src/pages/Pools/Create/index.tsx b/src/pages/Pools/Create/index.tsx new file mode 100644 index 0000000000..c40f0abd40 --- /dev/null +++ b/src/pages/Pools/Create/index.tsx @@ -0,0 +1,85 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; +import { + ButtonSecondary, + PageHeading, + PageRow, + PageTitle, +} from '@polkadot-cloud/react'; +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'; + +export const Create = () => { + const { t } = useTranslation('pages'); + const { activeAccount } = useActiveAccounts(); + const { setOnPoolSetup, removeSetupProgress } = useSetup(); + + return ( + <> + <PageTitle title={t('pools.createAPool')} /> + <PageRow> + <PageHeading> + <span> + <ButtonSecondary + text={t('pools.back')} + iconLeft={faChevronLeft} + iconTransform="shrink-3" + onClick={() => setOnPoolSetup(false)} + /> + </span> + <span> + <ButtonSecondary + text={t('pools.cancel')} + onClick={() => { + setOnPoolSetup(false); + removeSetupProgress('pool', activeAccount); + }} + /> + </span> + <div className="right" /> + </PageHeading> + </PageRow> + <PageRow> + <CardWrapper> + <Element name="metadata" style={{ position: 'absolute' }} /> + <PoolName section={1} /> + </CardWrapper> + </PageRow> + <PageRow> + <CardWrapper> + <Element name="nominate" style={{ position: 'absolute' }} /> + <Nominate bondFor="pool" section={2} /> + </CardWrapper> + </PageRow> + <PageRow> + <CardWrapper> + <Element name="roles" style={{ position: 'absolute' }} /> + <PoolRoles section={3} /> + </CardWrapper> + </PageRow> + <PageRow> + <CardWrapper> + <Element name="bond" style={{ position: 'absolute' }} /> + <Bond section={4} /> + </CardWrapper> + </PageRow> + + <PageRow> + <CardWrapper> + <Element name="summary" style={{ position: 'absolute' }} /> + <Summary section={5} /> + </CardWrapper> + </PageRow> + </> + ); +}; diff --git a/src/pages/Pools/Home/ClosurePrompts.tsx b/src/pages/Pools/Home/ClosurePrompts.tsx new file mode 100644 index 0000000000..f8b4e92e4b --- /dev/null +++ b/src/pages/Pools/Home/ClosurePrompts.tsx @@ -0,0 +1,110 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faLockOpen } from '@fortawesome/free-solid-svg-icons'; +import { ButtonPrimary, ButtonRow, PageRow } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { useTheme } from 'contexts/Themes'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useUi } from 'contexts/UI'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; + +export const ClosurePrompts = () => { + const { t } = useTranslation('pages'); + const { colors } = useNetwork().networkData; + const { activeAccount } = useActiveAccounts(); + const { mode } = useTheme(); + const { openModal } = useOverlay().modal; + const { membership } = usePoolMemberships(); + const { isPoolSyncing } = useUi(); + const { isBonding, selectedActivePool, isDepositor, poolNominations } = + useActivePools(); + const { getTransferOptions } = useTransferOptions(); + + const { state, memberCounter } = selectedActivePool?.bondedPool || {}; + const { active, totalUnlockChuncks } = getTransferOptions(activeAccount).pool; + const targets = poolNominations?.targets ?? []; + const annuncementBorderColor = colors.secondary[mode]; + + // is the pool in a state for the depositor to close + const depositorCanClose = + !isPoolSyncing && + isDepositor() && + state === 'Destroying' && + memberCounter === '1'; + + // depositor needs to unbond funds + const depositorCanUnbond = active.toNumber() > 0 && !targets.length; + + // depositor can withdraw & close pool + const depositorCanWithdraw = + active.toNumber() === 0 && totalUnlockChuncks === 0 && !targets.length; + + return ( + <> + {depositorCanClose && ( + <PageRow> + <CardWrapper + style={{ border: `1px solid ${annuncementBorderColor}` }} + > + <div className="content"> + <h3>{t('pools.destroyPool')}</h3> + <h4> + {t('pools.leftThePool')}.{' '} + {targets.length > 0 + ? t('pools.stopNominating') + : depositorCanWithdraw + ? t('pools.closePool') + : depositorCanUnbond + ? t('pools.unbondYourFunds') + : t('pools.withdrawUnlock')} + </h4> + <ButtonRow yMargin> + <ButtonPrimary + marginRight + text={t('pools.unbond')} + disabled={ + isPoolSyncing || + (!depositorCanWithdraw && !depositorCanUnbond) + } + onClick={() => + openModal({ + key: 'UnbondPoolMember', + options: { who: activeAccount, member: membership }, + size: 'sm', + }) + } + /> + <ButtonPrimary + iconLeft={faLockOpen} + text={ + depositorCanWithdraw + ? t('pools.unlocked') + : String(totalUnlockChuncks ?? 0) + } + disabled={isPoolSyncing || !isBonding()} + onClick={() => + openModal({ + key: 'UnlockChunks', + options: { + bondFor: 'pool', + poolClosure: true, + disableWindowResize: true, + }, + size: 'sm', + }) + } + /> + </ButtonRow> + </div> + </CardWrapper> + </PageRow> + )} + </> + ); +}; diff --git a/src/pages/Pools/Home/Favorites/index.tsx b/src/pages/Pools/Home/Favorites/index.tsx new file mode 100644 index 0000000000..62a91638d8 --- /dev/null +++ b/src/pages/Pools/Home/Favorites/index.tsx @@ -0,0 +1,67 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { PageRow } from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { useUi } from 'contexts/UI'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { PoolList } from 'library/PoolList/Default'; +import { ListStatusHeader } from 'library/List'; +import { PoolListProvider } from 'library/PoolList/context'; + +export const PoolFavorites = () => { + const { t } = useTranslation('pages'); + const { isReady } = useApi(); + const { isPoolSyncing } = useUi(); + const { bondedPools } = useBondedPools(); + const { favorites, removeFavorite } = usePoolsConfig(); + + // Store local favorite list and update when favorites list is mutated. + const [favoritesList, setFavoritesList] = useState<any[]>([]); + + useEffect(() => { + // map favorites to bonded pools + let newFavoritesList = favorites.map((f) => { + const pool = bondedPools.find((b) => b.addresses.stash === f); + if (!pool) removeFavorite(f); + return pool; + }); + + // filter not found bonded pools + newFavoritesList = newFavoritesList.filter((f: any) => f !== undefined); + + setFavoritesList(newFavoritesList); + }, [favorites]); + + return ( + <> + <PageRow> + <CardWrapper> + {favoritesList === null || isPoolSyncing ? ( + <ListStatusHeader> + {t('pools.fetchingFavoritePools')}... + </ListStatusHeader> + ) : ( + isReady && + (favoritesList.length > 0 ? ( + <PoolListProvider> + <PoolList + batchKey="favorite_pools" + pools={favoritesList} + allowMoreCols + pagination + /> + </PoolListProvider> + ) : ( + <ListStatusHeader>{t('pools.noFavorites')}</ListStatusHeader> + )) + )} + </CardWrapper> + </PageRow> + </> + ); +}; diff --git a/src/pages/Pools/Home/ManageBond.tsx b/src/pages/Pools/Home/ManageBond.tsx new file mode 100644 index 0000000000..620508f268 --- /dev/null +++ b/src/pages/Pools/Home/ManageBond.tsx @@ -0,0 +1,128 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faLockOpen } from '@fortawesome/free-solid-svg-icons'; +import { + ButtonHelp, + ButtonPrimary, + ButtonRow, + Odometer, +} from '@polkadot-cloud/react'; +import { minDecimalPlaces, planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useUi } from 'contexts/UI'; +import { BondedChart } from 'library/BarChart/BondedChart'; +import { CardHeaderWrapper } from 'library/Card/Wrappers'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const ManageBond = () => { + const { t } = useTranslation('pages'); + + const { + networkData: { + units, + brand: { token: Token }, + }, + } = useNetwork(); + const { openHelp } = useHelp(); + const { isPoolSyncing } = useUi(); + const { openModal } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + const { getTransferOptions } = useTransferOptions(); + const { isBonding, isMember, selectedActivePool } = useActivePools(); + + const allTransferOptions = getTransferOptions(activeAccount); + const { + active, + totalUnlocking, + totalUnlocked, + totalUnlockChuncks, + totalAdditionalBond, + } = allTransferOptions.pool; + + const { state } = selectedActivePool?.bondedPool || {}; + + return ( + <> + <CardHeaderWrapper> + <h4> + {t('pools.bondedFunds')} + <ButtonHelp marginLeft onClick={() => openHelp('Bonded in Pool')} /> + </h4> + <h2> + <Token className="networkIcon" /> + <Odometer + value={minDecimalPlaces(planckToUnit(active, units).toFormat(), 2)} + zeroDecimals={2} + /> + </h2> + <ButtonRow> + <ButtonPrimary + disabled={ + isPoolSyncing || + !isBonding() || + !isMember() || + isReadOnlyAccount(activeAccount) || + state === 'Destroying' + } + marginRight + onClick={() => + openModal({ + key: 'Bond', + options: { bondFor: 'pool' }, + size: 'sm', + }) + } + text="+" + /> + <ButtonPrimary + disabled={ + isPoolSyncing || + !isBonding() || + !isMember() || + isReadOnlyAccount(activeAccount) || + state === 'Destroying' + } + marginRight + onClick={() => + openModal({ + key: 'Unbond', + options: { bondFor: 'pool' }, + size: 'sm', + }) + } + text="-" + /> + <ButtonPrimary + disabled={ + isPoolSyncing || !isMember() || isReadOnlyAccount(activeAccount) + } + iconLeft={faLockOpen} + onClick={() => + openModal({ + key: 'UnlockChunks', + options: { bondFor: 'pool', disableWindowResize: true }, + size: 'sm', + }) + } + text={String(totalUnlockChuncks ?? 0)} + /> + </ButtonRow> + </CardHeaderWrapper> + <BondedChart + active={planckToUnit(active, units)} + unlocking={planckToUnit(totalUnlocking, units)} + unlocked={planckToUnit(totalUnlocked, units)} + free={planckToUnit(totalAdditionalBond, units)} + inactive={active.isZero()} + /> + </> + ); +}; diff --git a/src/pages/Pools/Home/ManagePool/Wrappers.ts b/src/pages/Pools/Home/ManagePool/Wrappers.ts new file mode 100644 index 0000000000..e6a5173994 --- /dev/null +++ b/src/pages/Pools/Home/ManagePool/Wrappers.ts @@ -0,0 +1,53 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; +import { SectionFullWidthThreshold } from 'consts'; + +export const RolesWrapper = styled.div` + display: flex; + flex-flow: row wrap; + width: 100%; + margin-top: 0.25rem; + + > section { + flex: 1 1 25%; + padding: 0 0.5rem; + border-right: 1px solid var(--border-primary-color); + + @media (max-width: ${SectionFullWidthThreshold}px) { + border-bottom: 1px solid var(--border-primary-color); + flex-basis: 100%; + border-right: none; + margin: 0.75rem 0; + + &:first-child { + margin-top: 0; + } + &:last-child { + margin-bottom: 0; + border-bottom: 0; + } + } + + &:last-child { + border-right: none; + } + + .inner { + flex: 1; + padding: 0 0.5rem; + + @media (max-width: ${SectionFullWidthThreshold}px) { + padding: 0; + } + + > h4 { + font-family: InterSemiBold, sans-serif; + display: flex; + align-items: center; + margin-top: 1.25rem; + } + } + } +`; diff --git a/src/pages/Pools/Home/ManagePool/index.tsx b/src/pages/Pools/Home/ManagePool/index.tsx new file mode 100644 index 0000000000..9f895b4f2a --- /dev/null +++ b/src/pages/Pools/Home/ManagePool/index.tsx @@ -0,0 +1,76 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faChevronCircleRight } from '@fortawesome/free-solid-svg-icons'; +import { ButtonHelp, ButtonPrimary, PageRow } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useHelp } from 'contexts/Help'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useUi } from 'contexts/UI'; +import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; +import { Nominations } from 'library/Nominations'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; + +export const ManagePool = () => { + const { t } = useTranslation(); + const { isSyncing } = useUi(); + const { poolNominated } = useValidators(); + const { openCanvas } = useOverlay().canvas; + const { activeAccount } = useActiveAccounts(); + const { isOwner, isNominator, poolNominations, selectedActivePool } = + useActivePools(); + + const isNominating = !!poolNominations?.targets?.length; + const nominator = selectedActivePool?.addresses?.stash ?? null; + const { state } = selectedActivePool?.bondedPool || {}; + const { openHelp } = useHelp(); + + const canNominate = isOwner() || isNominator(); + + return ( + <PageRow> + <CardWrapper> + {isSyncing ? ( + <Nominations bondFor="pool" nominator={activeAccount} /> + ) : canNominate && !isNominating && state !== 'Destroying' ? ( + <> + <CardHeaderWrapper $withAction $withMargin> + <h3> + {t('nominate.nominations', { ns: 'pages' })} + <ButtonHelp + marginLeft + onClick={() => openHelp('Nominations')} + /> + </h3> + <div> + <ButtonPrimary + iconLeft={faChevronCircleRight} + iconTransform="grow-1" + text={t('pools.nominate', { ns: 'pages' })} + disabled={!canNominate} + onClick={() => + openCanvas({ + key: 'ManageNominations', + scroll: false, + options: { + bondFor: 'pool', + nominator, + nominated: poolNominated || [], + }, + size: 'xl', + }) + } + /> + </div> + </CardHeaderWrapper> + <h4>{t('notNominating', { ns: 'library' })}.</h4> + </> + ) : ( + <Nominations bondFor="pool" nominator={nominator} /> + )} + </CardWrapper> + </PageRow> + ); +}; diff --git a/src/pages/Pools/Home/Members.tsx b/src/pages/Pools/Home/Members.tsx new file mode 100644 index 0000000000..143ae551d7 --- /dev/null +++ b/src/pages/Pools/Home/Members.tsx @@ -0,0 +1,94 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBars } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { PageRow } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { usePlugins } from 'contexts/Plugins'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import { useTheme } from 'contexts/Themes'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { useNetwork } from 'contexts/Network'; +import { MembersList as DefaultMemberList } from './MembersList/Default'; +import { MembersList as FetchPageMemberList } from './MembersList/FetchPage'; + +export const Members = () => { + const { t } = useTranslation('pages'); + const { mode } = useTheme(); + const { pluginEnabled } = usePlugins(); + const { getMembersOfPoolFromNode } = usePoolMembers(); + const { selectedActivePool, isOwner, isBouncer, selectedPoolMemberCount } = + useActivePools(); + const { colors } = useNetwork().networkData; + + const annuncementBorderColor = colors.secondary[mode]; + + const showBlockedPrompt = + selectedActivePool?.bondedPool?.state === 'Blocked' && + (isOwner() || isBouncer()); + + const membersListProps = { + batchKey: 'active_pool_members', + pagination: true, + selectToggleable: false, + allowMoreCols: true, + }; + + return ( + <> + {/* Pool in Blocked state: allow root & bouncer to unbond & withdraw members */} + {showBlockedPrompt && ( + <PageRow> + <CardWrapper + style={{ border: `1px solid ${annuncementBorderColor}` }} + > + <div className="content"> + <h3>{t('pools.poolCurrentlyLocked')}</h3> + <h4> + {t('pools.permissionToUnbond')}({' '} + <FontAwesomeIcon icon={faBars} transform="shrink-2" /> ){' '} + {t('pools.managementOptions')} + </h4> + </div> + </CardWrapper> + </PageRow> + )} + + {/* Pool in Destroying state: allow anyone to unbond & withdraw members */} + {selectedActivePool?.bondedPool?.state === 'Destroying' && ( + <PageRow> + <CardWrapper + style={{ border: `1px solid ${annuncementBorderColor}` }} + > + <div className="content"> + <h3>{t('pools.poolInDestroyingState')}</h3> + <h4> + {t('pools.permissionToUnbond')} ({' '} + <FontAwesomeIcon icon={faBars} transform="shrink-2" /> ){' '} + {t('pools.managementOptions')} + </h4> + </div> + </CardWrapper> + </PageRow> + )} + + <PageRow> + <CardWrapper> + {pluginEnabled('subscan') ? ( + <FetchPageMemberList + {...membersListProps} + memberCount={selectedPoolMemberCount} + /> + ) : ( + <DefaultMemberList + {...membersListProps} + members={getMembersOfPoolFromNode(selectedActivePool?.id ?? 0)} + /> + )} + </CardWrapper> + </PageRow> + </> + ); +}; diff --git a/src/pages/Pools/Home/MembersList/Default.tsx b/src/pages/Pools/Home/MembersList/Default.tsx new file mode 100644 index 0000000000..eac1f55f2d --- /dev/null +++ b/src/pages/Pools/Home/MembersList/Default.tsx @@ -0,0 +1,195 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBars, faGripVertical } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { isNotZero } from '@polkadot-cloud/utils'; +import { motion } from 'framer-motion'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ListItemsPerBatch, ListItemsPerPage } from 'consts'; +import { useApi } from 'contexts/Api'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import type { PoolMember } from 'contexts/Pools/types'; +import { useTheme } from 'contexts/Themes'; +import { + Header, + List, + ListStatusHeader, + Wrapper as ListWrapper, +} from 'library/List'; +import { MotionContainer } from 'library/List/MotionContainer'; +import { Pagination } from 'library/List/Pagination'; +import { ListProvider, useList } from 'library/List/context'; +import type { Sync } from 'types'; +import { useNetwork } from 'contexts/Network'; +import { Member } from './Member'; +import type { DefaultMembersListProps } from './types'; + +export const MembersListInner = ({ + allowMoreCols, + pagination, + batchKey, + members: initialMembers, + disableThrottle = false, +}: DefaultMembersListProps) => { + const { t } = useTranslation('pages'); + const { isReady } = useApi(); + const { + networkData: { colors }, + } = useNetwork(); + const provider = useList(); + const { mode } = useTheme(); + const { activeEra } = useNetworkMetrics(); + const { fetchPoolMembersMetaBatch } = usePoolMembers(); + + // get list provider properties. + const { listFormat, setListFormat } = provider; + + // current page + const [page, setPage] = useState<number>(1); + + // current render iteration + const [renderIteration, setRenderIterationState] = useState<number>(1); + + // default list of validators + const [membersDefault, setMembersDefault] = + useState<PoolMember[]>(initialMembers); + + // manipulated list (ordering, filtering) of payouts + const [members, setMembers] = useState<PoolMember[]>(initialMembers); + + // is this the initial fetch + const [fetched, setFetched] = useState<Sync>('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 + ); + + // get throttled subset or entire list + const listMembers = disableThrottle + ? members + : members.slice(pageStart).slice(0, ListItemsPerPage); + + // handle validator list bootstrapping + const setupMembersList = () => { + setMembersDefault(initialMembers); + setMembers(initialMembers); + fetchPoolMembersMetaBatch(batchKey, initialMembers, false); + setFetched('synced'); + }; + + // Refetch list when list changes. + useEffect(() => { + if (initialMembers !== membersDefault) { + setFetched('unsynced'); + } + }, [initialMembers]); + + // Configure list when network is ready to fetch. + useEffect(() => { + if (isReady && isNotZero(activeEra.index) && fetched === 'unsynced') { + setupMembersList(); + } + }, [isReady, fetched, activeEra.index]); + + // Render throttle. + useEffect(() => { + if (!(batchEnd >= pageEnd || disableThrottle)) { + setTimeout(() => { + setRenderIteration(renderIterationRef.current + 1); + }, 500); + } + }, [renderIterationRef.current]); + + return ( + <> + {!members.length ? ( + <></> + ) : ( + <ListWrapper> + <Header> + <div /> + <div> + <button type="button" onClick={() => setListFormat('row')}> + <FontAwesomeIcon + icon={faBars} + color={ + listFormat === 'row' ? colors.primary[mode] : 'inherit' + } + /> + </button> + <button type="button" onClick={() => setListFormat('col')}> + <FontAwesomeIcon + icon={faGripVertical} + color={ + listFormat === 'col' ? colors.primary[mode] : 'inherit' + } + /> + </button> + </div> + </Header> + <List $flexBasisLarge={allowMoreCols ? '33.33%' : '50%'}> + {listMembers.length > 0 && pagination && ( + <Pagination page={page} total={totalPages} setter={setPage} /> + )} + {fetched !== 'synced' ? ( + <ListStatusHeader style={{ marginTop: '0.5rem' }}> + {t('pools.fetchingMemberList')}... + </ListStatusHeader> + ) : ( + <MotionContainer> + {listMembers.map((member: PoolMember, index: number) => ( + <motion.div + className={`item ${listFormat === 'row' ? 'row' : 'col'}`} + key={`nomination_${index}`} + variants={{ + hidden: { + y: 15, + opacity: 0, + }, + show: { + y: 0, + opacity: 1, + }, + }} + > + <Member + who={member.who} + batchKey={batchKey} + batchIndex={membersDefault.indexOf(member)} + /> + </motion.div> + ))} + </MotionContainer> + )} + </List> + </ListWrapper> + )} + </> + ); +}; + +export const MembersList = (props: DefaultMembersListProps) => { + const { selectToggleable } = props; + return ( + <ListProvider selectToggleable={selectToggleable}> + <MembersListInner {...props} /> + </ListProvider> + ); +}; diff --git a/src/pages/Pools/Home/MembersList/FetchPage.tsx b/src/pages/Pools/Home/MembersList/FetchPage.tsx new file mode 100644 index 0000000000..ba0c3e7fb2 --- /dev/null +++ b/src/pages/Pools/Home/MembersList/FetchPage.tsx @@ -0,0 +1,201 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBars, faGripVertical } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { motion } from 'framer-motion'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ListItemsPerBatch, ListItemsPerPage } from 'consts'; +import { usePlugins } from 'contexts/Plugins'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import type { PoolMember } from 'contexts/Pools/types'; +import { useSubscan } from 'contexts/Plugins/Subscan'; +import { useTheme } from 'contexts/Themes'; +import { + Header, + List, + ListStatusHeader, + Wrapper as ListWrapper, +} from 'library/List'; +import { MotionContainer } from 'library/List/MotionContainer'; +import { Pagination } from 'library/List/Pagination'; +import { ListProvider, useList } from 'library/List/context'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { Member } from './Member'; +import type { FetchpageMembersListProps } from './types'; + +export const MembersListInner = ({ + allowMoreCols, + pagination, + batchKey, + disableThrottle = false, + memberCount, +}: FetchpageMembersListProps) => { + const { t } = useTranslation('pages'); + const { + network, + networkData: { colors }, + } = useNetwork(); + const provider = useList(); + const { mode } = useTheme(); + const { pluginEnabled } = usePlugins(); + const { fetchPoolMembers } = useSubscan(); + const { activeAccount } = useActiveAccounts(); + const { selectedActivePool } = useActivePools(); + const { + poolMembersApi, + setPoolMembersApi, + fetchedPoolMembersApi, + setFetchedPoolMembersApi, + fetchPoolMembersMetaBatch, + } = usePoolMembers(); + + // get list provider properties. + const { listFormat, setListFormat } = provider; + + // current page. + const [page, setPage] = useState<number>(1); + + // current render iteration. + const [renderIteration, setRenderIterationState] = useState<number>(1); + + // render throttle iteration. + const renderIterationRef = useRef(renderIteration); + const setRenderIteration = (iter: number) => { + renderIterationRef.current = iter; + setRenderIterationState(iter); + }; + + // pagination + const totalPages = Math.ceil(memberCount / ListItemsPerPage); + const pageEnd = ListItemsPerPage - 1; + const pageStart = pageEnd - (ListItemsPerPage - 1); + + // render batch + const batchEnd = Math.min( + renderIteration * ListItemsPerBatch - 1, + ListItemsPerPage + ); + + // handle validator list bootstrapping + const fetchingMemberList = useRef<boolean>(false); + + const setupMembersList = async () => { + const poolId = selectedActivePool?.id || 0; + + if (poolId > 0 && !fetchingMemberList.current) { + fetchingMemberList.current = true; + const newMembers: PoolMember[] = await fetchPoolMembers(poolId, page); + fetchingMemberList.current = false; + setPoolMembersApi([...newMembers]); + fetchPoolMembersMetaBatch(batchKey, newMembers, true); + setFetchedPoolMembersApi('synced'); + } + }; + + // get throttled subset or entire list + const listMembers = disableThrottle + ? poolMembersApi + : poolMembersApi.slice(pageStart).slice(0, ListItemsPerPage); + + // Refetch list when page changes. + useEffect(() => { + if (pluginEnabled('subscan')) { + setFetchedPoolMembersApi('unsynced'); + setPoolMembersApi([]); + } + }, [page, activeAccount, pluginEnabled('subscan')]); + + // Refetch list when network changes. + useEffect(() => { + setFetchedPoolMembersApi('unsynced'); + setPoolMembersApi([]); + setPage(1); + }, [network]); + + // Configure list when network is ready to fetch. + useEffect(() => { + if (fetchedPoolMembersApi === 'unsynced') { + setupMembersList(); + } + }, [fetchedPoolMembersApi, selectedActivePool]); + + // Render throttle. + useEffect(() => { + if (!(batchEnd >= pageEnd || disableThrottle)) { + setTimeout(() => { + setRenderIteration(renderIterationRef.current + 1); + }, 500); + } + }, [renderIterationRef.current]); + + return ( + <ListWrapper> + <Header> + <div /> + <div> + <button type="button" onClick={() => setListFormat('row')}> + <FontAwesomeIcon + icon={faBars} + color={listFormat === 'row' ? colors.primary[mode] : 'inherit'} + /> + </button> + <button type="button" onClick={() => setListFormat('col')}> + <FontAwesomeIcon + icon={faGripVertical} + color={listFormat === 'col' ? colors.primary[mode] : 'inherit'} + /> + </button> + </div> + </Header> + <List $flexBasisLarge={allowMoreCols ? '33.33%' : '50%'}> + {listMembers.length > 0 && pagination && ( + <Pagination page={page} total={totalPages} setter={setPage} /> + )} + {fetchedPoolMembersApi !== 'synced' ? ( + <ListStatusHeader style={{ marginTop: '0.5rem' }}> + {t('pools.fetchingMemberList')}.... + </ListStatusHeader> + ) : ( + <MotionContainer> + {listMembers.map((member: PoolMember, index: number) => ( + <motion.div + className={`item ${listFormat === 'row' ? 'row' : 'col'}`} + key={`nomination_${index}`} + variants={{ + hidden: { + y: 15, + opacity: 0, + }, + show: { + y: 0, + opacity: 1, + }, + }} + > + <Member + who={member.who} + batchKey={batchKey} + batchIndex={poolMembersApi.indexOf(member)} + /> + </motion.div> + ))} + </MotionContainer> + )} + </List> + </ListWrapper> + ); +}; + +export const MembersList = (props: FetchpageMembersListProps) => { + const { selectToggleable } = props; + + return ( + <ListProvider selectToggleable={selectToggleable}> + <MembersListInner {...props} /> + </ListProvider> + ); +}; diff --git a/src/pages/Pools/Home/MembersList/Member.tsx b/src/pages/Pools/Home/MembersList/Member.tsx new file mode 100644 index 0000000000..08c9029f23 --- /dev/null +++ b/src/pages/Pools/Home/MembersList/Member.tsx @@ -0,0 +1,140 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faBars, + faShare, + faUnlockAlt, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useMenu } from 'contexts/Menu'; +import { useNetworkMetrics } from 'contexts/NetworkMetrics'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { usePoolMembers } from 'contexts/Pools/PoolMembers'; +import { useList } from 'library/List/context'; +import { Identity } from 'library/ListItem/Labels/Identity'; +import { PoolMemberBonded } from 'library/ListItem/Labels/PoolMemberBonded'; +import { Select } from 'library/ListItem/Labels/Select'; +import { + Labels, + MenuPosition, + Separator, + Wrapper, +} from 'library/ListItem/Wrappers'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; + +export const Member = ({ who, batchKey, batchIndex }: any) => { + const { t } = useTranslation('pages'); + const { meta } = usePoolMembers(); + const { openModal } = useOverlay().modal; + const { selectActive } = useList(); + const { activeEra } = useNetworkMetrics(); + const { selectedActivePool, isOwner, isBouncer } = useActivePools(); + const { setMenuPosition, setMenuItems, open }: any = useMenu(); + const { state, roles } = selectedActivePool?.bondedPool || {}; + const { bouncer, root, depositor } = roles || {}; + + const canUnbondBlocked = + state === 'Blocked' && + (isOwner() || isBouncer()) && + ![root, bouncer].includes(who); + + const canUnbondDestroying = state === 'Destroying' && who !== depositor; + + const poolMembers = meta[batchKey]?.poolMembers ?? []; + const member = poolMembers[batchIndex] ?? null; + + const menuItems: any[] = []; + + if (member && (canUnbondBlocked || canUnbondDestroying)) { + const { points, unbondingEras } = member; + + if (points !== '0') { + menuItems.push({ + icon: <FontAwesomeIcon icon={faUnlockAlt} transform="shrink-3" />, + wrap: null, + title: `${t('pools.unbondFunds')}`, + cb: () => { + openModal({ + key: 'UnbondPoolMember', + options: { + who, + member, + }, + size: 'sm', + }); + }, + }); + } + + if (Object.values(unbondingEras).length) { + let canWithdraw = false; + for (const k of Object.keys(unbondingEras)) { + if (activeEra.index.isGreaterThan(Number(k))) { + canWithdraw = true; + } + } + + if (canWithdraw) { + menuItems.push({ + icon: <FontAwesomeIcon icon={faShare} transform="shrink-3" />, + wrap: null, + title: `${t('pools.withdrawFunds')}`, + cb: () => { + openModal({ + key: 'WithdrawPoolMember', + options: { who, member }, + size: 'sm', + }); + }, + }); + } + } + } + + // configure floating menu + const posRef = useRef(null); + const toggleMenu = () => { + if (!open) { + setMenuItems(menuItems); + setMenuPosition(posRef); + } + }; + + return ( + <Wrapper className="member"> + <div className="inner"> + <MenuPosition ref={posRef} /> + <div className="row top"> + {selectActive && <Select item={who} />} + <Identity address={who} /> + <div> + <Labels> + {menuItems.length > 0 && ( + <button + type="button" + className="label" + disabled={!member} + onClick={() => toggleMenu()} + > + <FontAwesomeIcon icon={faBars} /> + </button> + )} + </Labels> + </div> + </div> + <Separator /> + <div className="row bottom"> + <PoolMemberBonded + who={who} + meta={meta} + batchKey={batchKey} + batchIndex={batchIndex} + /> + </div> + </div> + </Wrapper> + ); +}; diff --git a/src/pages/Pools/Home/MembersList/types.ts b/src/pages/Pools/Home/MembersList/types.ts new file mode 100644 index 0000000000..59b7b1f438 --- /dev/null +++ b/src/pages/Pools/Home/MembersList/types.ts @@ -0,0 +1,18 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface MembersListProps { + allowMoreCols: boolean; + pagination: boolean; + batchKey: string; + disableThrottle?: boolean; + selectToggleable?: boolean; +} + +export type DefaultMembersListProps = MembersListProps & { + members: any; +}; + +export type FetchpageMembersListProps = MembersListProps & { + memberCount: number; +}; diff --git a/src/pages/Pools/Home/PoolStats/Announcements.tsx b/src/pages/Pools/Home/PoolStats/Announcements.tsx new file mode 100644 index 0000000000..dc2bc85d1d --- /dev/null +++ b/src/pages/Pools/Home/PoolStats/Announcements.tsx @@ -0,0 +1,104 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faBullhorn as faBack } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { planckToUnit, rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { Announcement as AnnouncementLoader } from 'library/Loader/Announcement'; +import { useNetwork } from 'contexts/Network'; +import { Item } from './Wrappers'; + +export const Announcements = () => { + const { t } = useTranslation('pages'); + const { consts } = useApi(); + const { + networkData: { units, unit }, + } = useNetwork(); + const { selectedActivePool } = useActivePools(); + const { rewardAccountBalance } = selectedActivePool || {}; + const { totalRewardsClaimed } = selectedActivePool?.rewardPool || {}; + const { existentialDeposit } = consts; + + // calculate the latest reward account balance + const rewardPoolBalance = BigNumber.max( + 0, + new BigNumber(rewardAccountBalance).minus(existentialDeposit) + ); + const rewardBalance = planckToUnit(rewardPoolBalance, units); + + // calculate total rewards claimed + const rewardsClaimed = planckToUnit( + totalRewardsClaimed + ? new BigNumber(rmCommas(totalRewardsClaimed)) + : new BigNumber(0), + units + ); + + const container = { + hidden: { opacity: 0 }, + show: { + opacity: 1, + transition: { + staggerChildren: 0.25, + }, + }, + }; + + const listItem = { + hidden: { + opacity: 0, + }, + show: { + opacity: 1, + }, + }; + + const announcements = []; + + announcements.push({ + class: 'neutral', + title: `${rewardsClaimed.decimalPlaces(3).toFormat()} ${unit} ${t( + 'pools.beenClaimed' + )}`, + subtitle: `${t('pools.beenClaimedBy', { unit })}`, + }); + + announcements.push({ + class: 'neutral', + title: `${rewardBalance.decimalPlaces(3).toFormat()} ${unit} ${t( + 'pools.outstandingReward' + )}`, + subtitle: `${t('pools.availableToClaim', { unit })}`, + }); + + return ( + <motion.div + variants={container} + initial="hidden" + animate="show" + style={{ width: '100%' }} + > + {announcements.map((item, index) => + item === null ? ( + <AnnouncementLoader key={`announcement_${index}`} /> + ) : ( + <Item key={`announcement_${index}`} variants={listItem}> + <h4 className={item.class}> + <FontAwesomeIcon + icon={faBack} + style={{ marginRight: '0.6rem' }} + /> + {item.title} + </h4> + <p>{item.subtitle}</p> + </Item> + ) + )} + </motion.div> + ); +}; diff --git a/src/pages/Pools/Home/PoolStats/Wrappers.ts b/src/pages/Pools/Home/PoolStats/Wrappers.ts new file mode 100644 index 0000000000..07f2c82a24 --- /dev/null +++ b/src/pages/Pools/Home/PoolStats/Wrappers.ts @@ -0,0 +1,53 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { motion } from 'framer-motion'; +import styled from 'styled-components'; + +export const Wrapper = styled.div` + flex: 1; + display: flex; + flex-flow: column wrap; + width: 100%; +`; + +export const Item = styled(motion.div)` + border-bottom: 1px solid var(--border-primary-color); + list-style: none; + flex: 1; + margin-bottom: 1rem; + padding: 0.75rem; + padding-bottom: 1.5rem; + + &:last-child { + border-bottom: 0; + margin-bottom: 0; + } + + h4 { + display: flex; + flex-flow: row wrap; + align-items: center; + margin: 0 0 0.5rem; + padding-bottom: 0.2rem; + + &.neutral { + color: var(--accent-color-primary); + } + &.danger { + color: #d2545d; + } + &.warning { + color: #b5a200; + } + &.pools { + color: var(--accent-color-secondary); + } + } + + p { + color: var(--text-color-secondary); + margin: 0; + line-height: 1.2rem; + } +`; diff --git a/src/pages/Pools/Home/PoolStats/index.tsx b/src/pages/Pools/Home/PoolStats/index.tsx new file mode 100644 index 0000000000..140c0b118d --- /dev/null +++ b/src/pages/Pools/Home/PoolStats/index.tsx @@ -0,0 +1,82 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit, rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; +import { usePoolCommission } from 'library/Hooks/usePoolCommission'; +import { StatsHead } from 'library/StatsHead'; +import { useNetwork } from 'contexts/Network'; +import { Announcements } from './Announcements'; +import { Wrapper } from './Wrappers'; + +export const PoolStats = () => { + const { t } = useTranslation('pages'); + const { + networkData: { units, unit }, + } = useNetwork(); + const { selectedActivePool, selectedPoolMemberCount } = useActivePools(); + const { getCurrentCommission } = usePoolCommission(); + + const { state, points } = selectedActivePool?.bondedPool || {}; + const currentCommission = getCurrentCommission(selectedActivePool?.id ?? 0); + + const bonded = planckToUnit( + new BigNumber(points ? rmCommas(points) : 0), + units + ) + .decimalPlaces(3) + .toFormat(); + + let stateDisplay; + switch (state) { + case 'Blocked': + stateDisplay = t('pools.locked'); + break; + case 'Destroying': + stateDisplay = t('pools.destroying'); + break; + default: + stateDisplay = t('pools.open'); + break; + } + + const items = [ + { + label: t('pools.poolState'), + value: stateDisplay, + }, + ]; + + if (currentCommission) { + items.push({ + label: t('pools.poolCommission'), + value: `${currentCommission}%`, + }); + } + + items.push( + { + label: t('pools.poolMembers'), + value: `${selectedPoolMemberCount}`, + }, + { + label: t('pools.totalBonded'), + value: `${bonded} ${unit}`, + } + ); + + return ( + <CardWrapper style={{ boxShadow: 'var(--card-shadow-secondary)' }}> + <CardHeaderWrapper $withMargin> + <h3>{t('pools.poolStats')}</h3> + </CardHeaderWrapper> + <Wrapper> + <StatsHead items={items} /> + <Announcements /> + </Wrapper> + </CardWrapper> + ); +}; diff --git a/src/pages/Pools/Home/Stats/ActivePools.tsx b/src/pages/Pools/Home/Stats/ActivePools.tsx new file mode 100644 index 0000000000..c02f994def --- /dev/null +++ b/src/pages/Pools/Home/Stats/ActivePools.tsx @@ -0,0 +1,19 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { Number } from 'library/StatBoxList/Number'; + +export const ActivePoolsStat = () => { + const { t } = useTranslation('pages'); + const { stats } = usePoolsConfig(); + + const params = { + label: t('pools.activePools'), + value: stats.counterForBondedPools.toNumber(), + unit: '', + helpKey: 'Active Pools', + }; + return <Number {...params} />; +}; diff --git a/src/pages/Pools/Home/Stats/MinCreateBond.tsx b/src/pages/Pools/Home/Stats/MinCreateBond.tsx new file mode 100644 index 0000000000..d3d0f08c15 --- /dev/null +++ b/src/pages/Pools/Home/Stats/MinCreateBond.tsx @@ -0,0 +1,25 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { Number } from 'library/StatBoxList/Number'; +import { useNetwork } from 'contexts/Network'; + +export const MinCreateBondStat = () => { + const { t } = useTranslation('pages'); + const { + networkData: { units, unit }, + } = useNetwork(); + const { stats } = usePoolsConfig(); + + const params = { + label: t('pools.minimumToCreatePool'), + value: planckToUnit(stats.minCreateBond, units).toNumber(), + decimals: 3, + unit, + helpKey: 'Minimum To Create Pool', + }; + return <Number {...params} />; +}; diff --git a/src/pages/Pools/Home/Stats/MinJoinBond.tsx b/src/pages/Pools/Home/Stats/MinJoinBond.tsx new file mode 100644 index 0000000000..d844490c37 --- /dev/null +++ b/src/pages/Pools/Home/Stats/MinJoinBond.tsx @@ -0,0 +1,25 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { Number } from 'library/StatBoxList/Number'; +import { useNetwork } from 'contexts/Network'; + +export const MinJoinBondStat = () => { + const { t } = useTranslation('pages'); + const { + networkData: { units, unit }, + } = useNetwork(); + const { stats } = usePoolsConfig(); + + const params = { + label: t('pools.minimumToJoinPool'), + value: planckToUnit(stats.minJoinBond, units).toNumber(), + decimals: 3, + unit: ` ${unit}`, + helpKey: 'Minimum To Join Pool', + }; + return <Number {...params} />; +}; diff --git a/src/pages/Pools/Home/Status/MembershipStatus.tsx b/src/pages/Pools/Home/Status/MembershipStatus.tsx new file mode 100644 index 0000000000..95496c118e --- /dev/null +++ b/src/pages/Pools/Home/Status/MembershipStatus.tsx @@ -0,0 +1,103 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCog } from '@fortawesome/free-solid-svg-icons'; +import { determinePoolDisplay } from '@polkadot-cloud/utils'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { useTransferOptions } from 'contexts/TransferOptions'; +import { useUi } from 'contexts/UI'; +import { Stat } from 'library/Stat'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { useStatusButtons } from './useStatusButtons'; + +export const MembershipStatus = ({ + showButtons = true, + buttonType = 'primary', +}: { + showButtons?: boolean; + buttonType?: string; +}) => { + const { t } = useTranslation('pages'); + const { isReady } = useApi(); + const { isPoolSyncing } = useUi(); + const { openModal } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { label, buttons } = useStatusButtons(); + const { bondedPools, meta } = useBondedPools(); + const { isReadOnlyAccount } = useImportedAccounts(); + const { getTransferOptions } = useTransferOptions(); + const { selectedActivePool, isOwner, isBouncer, isMember } = useActivePools(); + + const { active } = getTransferOptions(activeAccount).pool; + const poolState = selectedActivePool?.bondedPool?.state ?? null; + + const membershipButtons = []; + let membershipDisplay = t('pools.notInPool'); + + if (selectedActivePool) { + const pool = bondedPools.find( + (p: any) => p.addresses.stash === selectedActivePool.addresses.stash + ); + if (pool) { + // Determine pool membership display. + const metadata = meta.bonded_pools?.metadata ?? []; + const batchIndex = bondedPools.indexOf(pool); + membershipDisplay = determinePoolDisplay( + selectedActivePool.addresses.stash, + metadata[batchIndex] + ); + } + + // Display manage button if active account is pool owner or bouncer. + // Or display manage button if active account is a pool member. + if ( + (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 }, + size: 'sm', + }), + }); + } + } + + return ( + <> + {selectedActivePool ? ( + <> + <Stat + label={label} + helpKey="Pool Membership" + type="address" + stat={{ + address: selectedActivePool?.addresses?.stash ?? '', + display: membershipDisplay, + }} + buttons={showButtons ? membershipButtons : []} + /> + </> + ) : ( + <Stat + label={t('pools.poolMembership')} + helpKey="Pool Membership" + stat={t('pools.notInPool')} + buttons={!showButtons || isPoolSyncing ? [] : buttons} + buttonType={buttonType} + /> + )} + </> + ); +}; diff --git a/src/pages/Pools/Home/Status/PoolStatus.tsx b/src/pages/Pools/Home/Status/PoolStatus.tsx new file mode 100644 index 0000000000..ac0cb6ea18 --- /dev/null +++ b/src/pages/Pools/Home/Status/PoolStatus.tsx @@ -0,0 +1,67 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faExclamationTriangle, + faLock, +} from '@fortawesome/free-solid-svg-icons'; +import { useTranslation } from 'react-i18next'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useUi } from 'contexts/UI'; +import { useNominationStatus } from 'library/Hooks/useNominationStatus'; +import { Stat } from 'library/Stat'; + +export const PoolStatus = () => { + const { t } = useTranslation('pages'); + const { isPoolSyncing } = useUi(); + const { getNominationStatus } = useNominationStatus(); + const { selectedActivePool, poolNominations } = useActivePools(); + + const poolStash = selectedActivePool?.addresses?.stash || ''; + const { earningRewards, nominees } = getNominationStatus(poolStash, 'pool'); + const poolState = selectedActivePool?.bondedPool?.state ?? null; + const poolNominating = !!poolNominations?.targets?.length; + + // Determine pool state icon. + let poolStateIcon; + switch (poolState) { + case 'Blocked': + poolStateIcon = faLock; + break; + case 'Destroying': + poolStateIcon = faExclamationTriangle; + break; + default: + poolStateIcon = undefined; + } + + // Determine pool status - left side. + const poolStatusLeft = + poolState === 'Blocked' + ? `${t('pools.locked')} / ` + : poolState === 'Destroying' + ? `${t('pools.destroying')} / ` + : ''; + + // Determine pool status - right side. + const poolStatusRight = isPoolSyncing + ? t('pools.inactivePoolNotNominating') + : !poolNominating + ? t('pools.inactivePoolNotNominating') + : nominees.active.length + ? `${t('pools.nominatingAnd')} ${ + earningRewards + ? t('pools.earningRewards') + : t('pools.notEarningRewards') + }` + : t('pools.waitingForActiveNominations'); + + return ( + <Stat + icon={isPoolSyncing ? undefined : poolStateIcon} + label={t('pools.poolStatus')} + helpKey="Nomination Status" + stat={`${poolStatusLeft}${poolStatusRight}`} + /> + ); +}; diff --git a/src/pages/Pools/Home/Status/RewardsStatus.tsx b/src/pages/Pools/Home/Status/RewardsStatus.tsx new file mode 100644 index 0000000000..d700c47b18 --- /dev/null +++ b/src/pages/Pools/Home/Status/RewardsStatus.tsx @@ -0,0 +1,81 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCircleDown, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { planckToUnit } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useUi } from 'contexts/UI'; +import { Stat } from 'library/Stat'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; + +export const RewardsStatus = () => { + const { t } = useTranslation('pages'); + const { + networkData: { units }, + } = useNetwork(); + const { isReady } = useApi(); + const { isPoolSyncing } = useUi(); + const { openModal } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { selectedActivePool } = useActivePools(); + const { isReadOnlyAccount } = useImportedAccounts(); + + let { pendingRewards } = selectedActivePool || {}; + pendingRewards = pendingRewards ?? new BigNumber(0); + + // Set the minimum unclaimed planck value to prevent e numbers. + const minUnclaimedDisplay = new BigNumber(1_000_000); + + const labelRewards = pendingRewards.isGreaterThan(minUnclaimedDisplay) + ? planckToUnit(pendingRewards, units).toString() + : '0'; + + // Display Reward buttons if unclaimed rewards is a non-zero value. + const buttonsRewards = pendingRewards.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) || + selectedActivePool?.bondedPool?.state === 'Destroying', + small: true, + onClick: () => + openModal({ + key: 'ClaimReward', + options: { claimType: 'bond' }, + size: 'sm', + }), + }, + ] + : undefined; + + return ( + <Stat + label={t('pools.unclaimedRewards')} + helpKey="Pool Rewards" + type="odometer" + stat={{ value: labelRewards }} + buttons={isPoolSyncing ? [] : buttonsRewards} + /> + ); +}; diff --git a/src/pages/Pools/Home/Status/index.tsx b/src/pages/Pools/Home/Status/index.tsx new file mode 100644 index 0000000000..0a2036aeeb --- /dev/null +++ b/src/pages/Pools/Home/Status/index.tsx @@ -0,0 +1,27 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { Separator } from '@polkadot-cloud/react'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { MembershipStatus } from './MembershipStatus'; +import { PoolStatus } from './PoolStatus'; +import { RewardsStatus } from './RewardsStatus'; + +export const Status = ({ height }: { height: number }) => { + const { selectedActivePool } = useActivePools(); + + return ( + <CardWrapper height={height}> + <MembershipStatus /> + <Separator /> + <RewardsStatus /> + {selectedActivePool && ( + <> + <Separator /> + <PoolStatus /> + </> + )} + </CardWrapper> + ); +}; diff --git a/src/pages/Pools/Home/Status/useStatusButtons.tsx b/src/pages/Pools/Home/Status/useStatusButtons.tsx new file mode 100644 index 0000000000..0674328e89 --- /dev/null +++ b/src/pages/Pools/Home/Status/useStatusButtons.tsx @@ -0,0 +1,82 @@ +// Copyright 2023 @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 { useActivePools } from 'contexts/Pools/ActivePools'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +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'; + +export const useStatusButtons = () => { + const { t } = useTranslation('pages'); + const { isReady } = useApi(); + const { stats } = usePoolsConfig(); + const { isOwner } = useActivePools(); + const { setActiveTab } = usePoolsTabs(); + const { bondedPools } = useBondedPools(); + const { membership } = usePoolMemberships(); + const { activeAccount } = useActiveAccounts(); + const { getTransferOptions } = useTransferOptions(); + const { isReadOnlyAccount } = useImportedAccounts(); + const { setOnPoolSetup, getPoolSetupPercent } = useSetup(); + + const { maxPools } = stats; + const { active } = getTransferOptions(activeAccount).pool; + const poolSetupPercent = getPoolSetupPercent(activeAccount); + + const disableCreate = () => { + if (!isReady || isReadOnlyAccount(activeAccount) || !activeAccount) + return true; + if ( + maxPools && + (maxPools.isZero() || bondedPools.length === stats.maxPools?.toNumber()) + ) + return true; + return false; + }; + + 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(2), + }; + + if (!membership) { + label = t('pools.poolMembership'); + buttons = [createBtn, joinPoolBtn]; + } else if (isOwner()) { + label = `${t('pools.ownerOfPool')} ${membership.poolId}`; + } else if (active?.isGreaterThan(0)) { + label = `${t('pools.memberOfPool')} ${membership.poolId}`; + } else { + label = `${t('pools.leavingPool')} ${membership.poolId}`; + } + return { label, buttons }; +}; diff --git a/src/pages/Pools/Home/context.tsx b/src/pages/Pools/Home/context.tsx new file mode 100644 index 0000000000..cb9c9f4c99 --- /dev/null +++ b/src/pages/Pools/Home/context.tsx @@ -0,0 +1,43 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { extractUrlValue } from '@polkadot-cloud/utils'; +import React, { useState } from 'react'; +import type { PoolsTabsContextInterface } from '../types'; + +export const PoolsTabsContext: React.Context<PoolsTabsContextInterface> = + React.createContext({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setActiveTab: (t: number) => {}, + activeTab: 0, + }); + +export const usePoolsTabs = () => React.useContext(PoolsTabsContext); + +export const PoolsTabsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const tabFromUrl = extractUrlValue('t'); + const initialActiveTab = [0, 1, 2, 3].includes(Number(tabFromUrl)) + ? Number(tabFromUrl) + : 0; + + const [activeTab, setActiveTabState] = useState<number>(initialActiveTab); + + const setActiveTab = (t: number) => { + setActiveTabState(t); + }; + + return ( + <PoolsTabsContext.Provider + value={{ + activeTab, + setActiveTab, + }} + > + {children} + </PoolsTabsContext.Provider> + ); +}; diff --git a/src/pages/Pools/Home/index.tsx b/src/pages/Pools/Home/index.tsx new file mode 100644 index 0000000000..f9de8fda5a --- /dev/null +++ b/src/pages/Pools/Home/index.tsx @@ -0,0 +1,177 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { PageRow, PageTitle, RowSection } from '@polkadot-cloud/react'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import type { PageTitleTabProps } from '@polkadot-cloud/react/types'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useBondedPools } from 'contexts/Pools/BondedPools'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { PoolList } from 'library/PoolList/Default'; +import { StatBoxList } from 'library/StatBoxList'; +import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { PoolListProvider } from 'library/PoolList/context'; +import { Roles } from '../Roles'; +import { ClosurePrompts } from './ClosurePrompts'; +import { PoolFavorites } from './Favorites'; +import { ManageBond } from './ManageBond'; +import { ManagePool } from './ManagePool'; +import { Members } from './Members'; +import { PoolStats } from './PoolStats'; +import { ActivePoolsStat } from './Stats/ActivePools'; +import { MinCreateBondStat } from './Stats/MinCreateBond'; +import { MinJoinBondStat } from './Stats/MinJoinBond'; +import { Status } from './Status'; +import { PoolsTabsProvider, usePoolsTabs } from './context'; + +export const HomeInner = () => { + const { t } = useTranslation('pages'); + const { openModal } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { + favorites, + stats: { counterForBondedPools }, + } = usePoolsConfig(); + const { activeTab, setActiveTab } = usePoolsTabs(); + const { bondedPools, getAccountPools } = useBondedPools(); + const { getPoolRoles, selectedActivePool, selectedPoolMemberCount } = + useActivePools(); + const accountPools = getAccountPools(activeAccount); + const totalAccountPools = Object.entries(accountPools).length; + + let tabs: PageTitleTabProps[] = [ + { + title: t('pools.overview'), + active: activeTab === 0, + onClick: () => setActiveTab(0), + }, + ]; + + if (selectedActivePool) { + tabs = tabs.concat({ + title: t('pools.members'), + active: activeTab === 1, + onClick: () => setActiveTab(1), + badge: String(selectedPoolMemberCount), + }); + } + + tabs = tabs.concat( + { + title: t('pools.allPools'), + active: activeTab === 2, + onClick: () => setActiveTab(2), + badge: String(counterForBondedPools.toString()), + }, + { + title: t('pools.favorites'), + active: activeTab === 3, + onClick: () => setActiveTab(3), + badge: String(favorites.length), + } + ); + + // Back to tab 0 if not in a pool & on members tab. + useEffect(() => { + if (!selectedActivePool && [1].includes(activeTab)) { + setActiveTab(0); + } + }, [selectedActivePool]); + + const ROW_HEIGHT = 220; + + return ( + <> + <PageTitle + title={t('pools.pools')} + tabs={tabs} + button={ + totalAccountPools + ? { + title: t('pools.allRoles'), + onClick: () => + openModal({ + key: 'AccountPoolRoles', + options: { who: activeAccount }, + }), + } + : undefined + } + /> + {activeTab === 0 && ( + <> + <StatBoxList> + <ActivePoolsStat /> + <MinJoinBondStat /> + <MinCreateBondStat /> + </StatBoxList> + + <ClosurePrompts /> + + <PageRow> + <RowSection hLast> + <Status height={ROW_HEIGHT} /> + </RowSection> + <RowSection secondary> + <CardWrapper height={ROW_HEIGHT}> + <ManageBond /> + </CardWrapper> + </RowSection> + </PageRow> + {selectedActivePool !== null && ( + <> + <ManagePool /> + <PageRow> + <CardWrapper> + <Roles + batchKey="pool_roles_manage" + defaultRoles={getPoolRoles()} + /> + </CardWrapper> + </PageRow> + <PageRow> + <PoolStats /> + </PageRow> + </> + )} + </> + )} + {activeTab === 1 && <Members />} + {activeTab === 2 && ( + <> + <PageRow> + <CardWrapper> + <PoolListProvider> + <PoolList + batchKey="bonded_pools" + pools={bondedPools} + defaultFilters={{ + includes: ['active'], + excludes: ['locked', 'destroying'], + }} + allowMoreCols + allowSearch + pagination + /> + </PoolListProvider> + </CardWrapper> + </PageRow> + </> + )} + {activeTab === 3 && ( + <> + <PoolFavorites /> + </> + )} + </> + ); +}; + +export const Home = () => ( + <PoolsTabsProvider> + <HomeInner /> + </PoolsTabsProvider> +); diff --git a/src/pages/Pools/PoolAccount/Wrapper.ts b/src/pages/Pools/PoolAccount/Wrapper.ts new file mode 100644 index 0000000000..a5ba8ccd3c --- /dev/null +++ b/src/pages/Pools/PoolAccount/Wrapper.ts @@ -0,0 +1,53 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 100%; + display: flex; + flex-flow: column wrap; + padding-bottom: 0.5rem; + margin-top: 1rem; + + .account { + width: 100%; + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 0; + + button { + color: var(--text-color-primary); + } + + .icon { + position: relative; + top: 0.1rem; + margin-right: 0.5rem; + } + h4 { + padding: 0; + + > .addr { + opacity: 0.75; + } + } + + > :last-child { + display: flex; + flex-flow: row-reverse wrap; + margin-left: 0.5rem; + + > .copy { + color: var(--text-color-secondary); + cursor: pointer; + transition: opacity var(--transition-duration); + margin-left: 0.5rem; + &:hover { + opacity: 0.8; + } + } + } + } +`; diff --git a/src/pages/Pools/PoolAccount/index.tsx b/src/pages/Pools/PoolAccount/index.tsx new file mode 100644 index 0000000000..9c820a9b65 --- /dev/null +++ b/src/pages/Pools/PoolAccount/index.tsx @@ -0,0 +1,95 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { faCopy } from '@fortawesome/free-regular-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ellipsisFn, remToUnit } from '@polkadot-cloud/utils'; +import { motion } from 'framer-motion'; +import { useTranslation } from 'react-i18next'; +import { useIdentities } from 'contexts/Identities'; +import { useNotifications } from 'contexts/Notifications'; +import type { NotificationText } from 'contexts/Notifications/types'; +import { Polkicon } from '@polkadot-cloud/react'; +import { getIdentityDisplay } from 'library/ValidatorList/ValidatorItem/Utils'; +import type { PoolAccountProps } from '../types'; +import { Wrapper } from './Wrapper'; + +export const PoolAccount = ({ + address, + batchKey, + batchIndex, +}: PoolAccountProps) => { + const { t } = useTranslation('pages'); + const { meta } = useIdentities(); + const { addNotification } = useNotifications(); + + const identities = meta[batchKey]?.identities ?? []; + const supers = meta[batchKey]?.supers ?? []; + + const identitiesSynced = identities.length > 0 ?? false; + const supersSynced = supers.length > 0 ?? false; + const synced = identitiesSynced && supersSynced; + + const display = getIdentityDisplay( + identities[batchIndex], + supers[batchIndex] + ); + + let notification: NotificationText | null = null; + if (address !== null) { + notification = { + title: t('pools.addressCopied'), + subtitle: address, + }; + } + + return ( + <Wrapper> + <motion.div + className="account" + initial={{ opacity: 0.5 }} + animate={{ opacity: 1 }} + transition={{ duration: 0.3 }} + > + {address === null ? ( + <h4>{t('pools.notSet')}</h4> + ) : synced && display !== null ? ( + <> + <div className="icon"> + <Polkicon address={address} size={remToUnit('1.6rem')} /> + </div> + <h4>{display}</h4> + </> + ) : ( + <> + <div className="icon"> + <Polkicon address={address} size={remToUnit('1.6rem')} /> + </div> + <h4>{ellipsisFn(address)}</h4> + </> + )} + <div> + <motion.div + className="copy" + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.97 }} + > + {address !== null && ( + <button + type="button" + onClick={() => { + navigator.clipboard.writeText(address); + if (notification) { + addNotification(notification); + } + }} + > + <FontAwesomeIcon icon={faCopy} transform="shrink-2" /> + </button> + )} + </motion.div> + </div> + </motion.div> + </Wrapper> + ); +}; diff --git a/src/pages/Pools/Roles/RoleEditInput/Wrapper.ts b/src/pages/Pools/Roles/RoleEditInput/Wrapper.ts new file mode 100644 index 0000000000..04d46482d3 --- /dev/null +++ b/src/pages/Pools/Roles/RoleEditInput/Wrapper.ts @@ -0,0 +1,47 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import styled from 'styled-components'; + +export const Wrapper = styled.div` + width: 100%; + margin-top: 0.5rem; + + .input { + border: 1px solid var(--border-primary-color); + border-radius: 1rem; + display: flex; + flex-flow: row wrap; + align-items: center; + padding: 0.25rem 0.5rem 0.25rem 1rem; + + > section { + display: flex; + flex-flow: column wrap; + + > input { + font-family: InterSemiBold, sans-serif; + width: 100%; + border: none; + padding-right: 1rem; + } + + &:first-child { + flex: 1; + } + } + } + h5 { + margin: 0.75rem 0.25rem; + &.neutral { + color: var(--text-color-secondary); + opacity: 0.8; + } + &.danger { + color: var(--status-danger-color); + } + &.success { + color: var(--status-success-color); + } + } +`; diff --git a/src/pages/Pools/Roles/RoleEditInput/index.tsx b/src/pages/Pools/Roles/RoleEditInput/index.tsx new file mode 100644 index 0000000000..98490320e9 --- /dev/null +++ b/src/pages/Pools/Roles/RoleEditInput/index.tsx @@ -0,0 +1,70 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { isValidAddress } from '@polkadot-cloud/utils'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNetwork } from 'contexts/Network'; +import { formatAccountSs58 } from 'contexts/Connect/Utils'; +import { Wrapper } from './Wrapper'; + +export const RoleEditInput = ({ setRoleEdit, roleKey, roleEdit }: any) => { + const { t } = useTranslation('pages'); + const { + networkData: { ss58 }, + } = useNetwork(); + + const processRoleEdit = (newAddress: string) => { + let edit = { + newAddress, + valid: newAddress === '', // empty address is valid and removes the role + reformatted: false, + }; + if (isValidAddress(newAddress)) { + const addressFormatted = formatAccountSs58(newAddress, ss58); + if (addressFormatted) { + edit = { + newAddress: addressFormatted, + valid: true, + reformatted: true, + }; + } else { + edit = { newAddress, valid: true, reformatted: false }; + } + } + return { ...roleEdit, ...edit }; + }; + + const handleChange = (e: React.FormEvent<HTMLInputElement>) => { + const newValue = e.currentTarget.value; + // set value on key change + const edit = processRoleEdit(newValue); + setRoleEdit(roleKey, edit); + }; + + let label; + let labelClass; + if (!roleEdit?.valid) { + label = t('pools.addressInvalid'); + labelClass = 'danger'; + } else if (roleEdit?.reformatted) { + label = t('pools.reformatted'); + labelClass = 'neutral'; + } + + return ( + <Wrapper> + <div className="input"> + <section> + <input + placeholder={t('pools.address')} + type="text" + onChange={(e: React.FormEvent<HTMLInputElement>) => handleChange(e)} + value={roleEdit?.newAddress ?? ''} + /> + </section> + </div> + {label && <h5 className={labelClass}>{label}</h5>} + </Wrapper> + ); +}; diff --git a/src/pages/Pools/Roles/index.tsx b/src/pages/Pools/Roles/index.tsx new file mode 100644 index 0000000000..8518007c21 --- /dev/null +++ b/src/pages/Pools/Roles/index.tsx @@ -0,0 +1,264 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { + faCheckCircle, + faEdit, + faTimesCircle, +} from '@fortawesome/free-solid-svg-icons'; +import { + ButtonHelp, + ButtonPrimary, + ButtonPrimaryInvert, +} from '@polkadot-cloud/react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useHelp } from 'contexts/Help'; +import { useIdentities } from 'contexts/Identities'; +import { useActivePools } from 'contexts/Pools/ActivePools'; +import { useUi } from 'contexts/UI'; +import { CardHeaderWrapper } from 'library/Card/Wrappers'; +import { useOverlay } from '@polkadot-cloud/react/hooks'; +import { useNetwork } from 'contexts/Network'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useImportedAccounts } from 'contexts/Connect/ImportedAccounts'; +import { RolesWrapper } from '../Home/ManagePool/Wrappers'; +import { PoolAccount } from '../PoolAccount'; +import { RoleEditInput } from './RoleEditInput'; +import type { RoleEditEntry, RolesProps } from './types'; + +export const Roles = ({ + batchKey, + defaultRoles, + setters = [], + inline = false, + listenIsValid = () => {}, +}: RolesProps) => { + const { t } = useTranslation('pages'); + const { isReady } = useApi(); + const { openHelp } = useHelp(); + const { network } = useNetwork(); + const { isPoolSyncing } = useUi(); + const { openModal } = useOverlay().modal; + const { activeAccount } = useActiveAccounts(); + const { isReadOnlyAccount } = useImportedAccounts(); + const { fetchIdentitiesMetaBatch } = useIdentities(); + const { isOwner, selectedActivePool } = useActivePools(); + const { id } = selectedActivePool || { id: 0 }; + const roles = defaultRoles; + + const initialiseEdits = (() => { + const initState: Record<string, RoleEditEntry> = {}; + Object.entries(defaultRoles).forEach(([role, who]) => { + initState[role] = { + oldAddress: who, + newAddress: who, + valid: true, + reformatted: false, + }; + }); + return initState; + })(); + + // store any role edits that take place + const [roleEdits, setRoleEdits] = useState(initialiseEdits); + + // store whether roles are being edited + const [isEditing, setIsEditing] = useState(false); + + // store role accounts + const [accounts, setAccounts] = useState(Object.values(roles)); + + // is this the initial fetch + const [fetched, setFetched] = useState(false); + + // update default roles on account switch + useEffect(() => { + setAccounts(Object.values(roles)); + setIsEditing(false); + setRoleEdits(initialiseEdits); + setFetched(false); + }, [activeAccount, network]); + + // fetch accounts meta batch + useEffect(() => { + if (isReady && !fetched) { + setFetched(true); + fetchIdentitiesMetaBatch(batchKey, Object.values(roles), true); + } + }, [isReady, fetched]); + + const isRoleEditsValid = () => { + for (const roleEdit of Object.values<RoleEditEntry>(roleEdits)) { + if (roleEdit?.valid === false) { + return false; + } + } + return true; + }; + + // logic for saving edit state + const saveHandler = () => { + setIsEditing(false); + + // if setters available, use those to update + // parent component state. + if (setters.length) { + if (listenIsValid) { + listenIsValid(isRoleEditsValid()); + } + const rolesUpdated: any = {}; + for (const [k, v] of Object.entries(roleEdits)) { + rolesUpdated[k] = v.newAddress; + } + for (const s of setters) { + s.set({ + ...s.current, + roles: rolesUpdated, + }); + } + } else { + // else, open modal with role edits data to update pool roles. + openModal({ + key: 'ChangePoolRoles', + options: { id, roleEdits }, + size: 'sm', + }); + } + }; + + // enter edit state + const editHandler = () => { + setRoleEdits(initialiseEdits); + setIsEditing(true); + }; + + // cancel editing and revert edit state + const cancelHandler = () => { + setRoleEdits(initialiseEdits); + setIsEditing(false); + }; + + // passed down to `RoleEditInput` to update roleEdits + const setRoleEditHandler = (role: string, edit: RoleEditEntry) => { + const newEdit = { + ...roleEdits, + [role]: edit, + }; + setRoleEdits(newEdit); + }; + + const ButtonType = inline ? ButtonPrimaryInvert : ButtonPrimary; + + return ( + <> + <CardHeaderWrapper $withAction $withMargin> + {!inline && ( + <h3> + {t('pools.roles')} + <ButtonHelp marginLeft onClick={() => openHelp('Pool Roles')} /> + </h3> + )} + + {!(isOwner() === true || setters.length) ? ( + <></> + ) : ( + <> + {isEditing && ( + <div> + <ButtonType + iconLeft={faTimesCircle} + iconTransform="grow-1" + text={t('pools.cancel')} + disabled={isPoolSyncing || isReadOnlyAccount(activeAccount)} + onClick={() => cancelHandler()} + /> + </div> + )} +    + <div> + <ButtonType + iconLeft={isEditing ? faCheckCircle : faEdit} + iconTransform="grow-1" + text={isEditing ? t('pools.save') : t('pools.edit')} + disabled={ + isPoolSyncing || + isReadOnlyAccount(activeAccount) || + !isRoleEditsValid() + } + onClick={() => (isEditing ? saveHandler() : editHandler())} + /> + </div> + </> + )} + </CardHeaderWrapper> + <RolesWrapper> + <section> + <div className="inner"> + <h4>{t('pools.depositor')}</h4> + <PoolAccount + address={roles.depositor ?? null} + batchIndex={accounts.indexOf(roles.depositor ?? '-1')} + batchKey={batchKey} + /> + </div> + </section> + <section> + <div className="inner"> + <h4>{t('pools.root')}</h4> + {isEditing ? ( + <RoleEditInput + roleKey="root" + roleEdit={roleEdits?.root} + setRoleEdit={setRoleEditHandler} + /> + ) : ( + <PoolAccount + address={roles.root ?? null} + batchIndex={accounts.indexOf(roles.root ?? '-1')} + batchKey={batchKey} + /> + )} + </div> + </section> + <section> + <div className="inner"> + <h4>{t('pools.nominator')}</h4> + {isEditing ? ( + <RoleEditInput + roleKey="nominator" + roleEdit={roleEdits?.nominator} + setRoleEdit={setRoleEditHandler} + /> + ) : ( + <PoolAccount + address={roles.nominator ?? null} + batchIndex={accounts.indexOf(roles.nominator ?? '-1')} + batchKey={batchKey} + /> + )} + </div> + </section> + <section> + <div className="inner"> + <h4>{t('pools.bouncer')}</h4> + {isEditing ? ( + <RoleEditInput + roleKey="bouncer" + roleEdit={roleEdits?.bouncer} + setRoleEdit={setRoleEditHandler} + /> + ) : ( + <PoolAccount + address={roles.bouncer ?? null} + batchIndex={accounts.indexOf(roles.bouncer ?? '-1')} + batchKey={batchKey} + /> + )} + </div> + </section> + </RolesWrapper> + </> + ); +}; diff --git a/src/pages/Pools/Roles/types.ts b/src/pages/Pools/Roles/types.ts new file mode 100644 index 0000000000..326001e1dd --- /dev/null +++ b/src/pages/Pools/Roles/types.ts @@ -0,0 +1,19 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { PoolRoles } from 'contexts/Pools/types'; + +export interface RolesProps { + batchKey: string; + defaultRoles: PoolRoles; + listenIsValid?: any; + setters?: any; + inline?: boolean; +} + +export type RoleEditEntry = { + oldAddress: string; + newAddress: string; + valid: boolean; + reformatted: boolean; +}; diff --git a/src/pages/Pools/index.tsx b/src/pages/Pools/index.tsx new file mode 100644 index 0000000000..129eb2ed3c --- /dev/null +++ b/src/pages/Pools/index.tsx @@ -0,0 +1,11 @@ +// Copyright 2023 @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 ? <Create /> : <Home />}</>; +}; diff --git a/src/pages/Pools/types.ts b/src/pages/Pools/types.ts new file mode 100644 index 0000000000..0853c7dd1f --- /dev/null +++ b/src/pages/Pools/types.ts @@ -0,0 +1,18 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +export interface PoolAccountProps { + address: string | null; + batchKey: string; + batchIndex: number; +} + +export interface PoolsTabsContextInterface { + setActiveTab: (t: number) => void; + activeTab: number; +} + +export interface PayoutListContextInterface { + setListFormat: (v: string) => void; + listFormat: string; +} diff --git a/src/pages/Validators/AllValidators.tsx b/src/pages/Validators/AllValidators.tsx new file mode 100644 index 0000000000..e8414300ff --- /dev/null +++ b/src/pages/Validators/AllValidators.tsx @@ -0,0 +1,70 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { PageRow } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { StatBoxList } from 'library/StatBoxList'; +import { ValidatorList } from 'library/ValidatorList'; +import { ActiveValidatorsStat } from './Stats/ActiveValidators'; +import { AverageCommissionStat } from './Stats/AverageCommission'; +import { TotalValidatorsStat } from './Stats/TotalValidators'; + +export const AllValidators = () => { + const { t } = useTranslation('pages'); + const { isReady } = useApi(); + const { validators } = useValidators(); + + return ( + <> + <StatBoxList> + <ActiveValidatorsStat /> + <TotalValidatorsStat /> + <AverageCommissionStat /> + </StatBoxList> + <PageRow> + <CardWrapper> + {!isReady ? ( + <div className="item"> + <h3>{t('validators.connecting')}...</h3> + </div> + ) : ( + <> + {validators.length === 0 && ( + <div className="item"> + <h3>{t('validators.fetchingValidators')}...</h3> + </div> + )} + + {validators.length > 0 && ( + <ValidatorList + bondFor="nominator" + validators={validators} + title={t('validators.networkValidators')} + selectable={false} + defaultFilters={{ + includes: ['active'], + excludes: [ + 'all_commission', + 'blocked_nominations', + 'missing_identity', + ], + }} + defaultOrder="rank" + allowListFormat={false} + allowMoreCols + allowFilters + allowSearch + pagination + toggleFavorites + /> + )} + </> + )} + </CardWrapper> + </PageRow> + </> + ); +}; diff --git a/src/pages/Validators/Favorites.tsx b/src/pages/Validators/Favorites.tsx new file mode 100644 index 0000000000..9f108addda --- /dev/null +++ b/src/pages/Validators/Favorites.tsx @@ -0,0 +1,45 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { PageRow } from '@polkadot-cloud/react'; +import { useTranslation } from 'react-i18next'; +import { useApi } from 'contexts/Api'; +import { CardWrapper } from 'library/Card/Wrappers'; +import { ValidatorList } from 'library/ValidatorList'; +import { useFavoriteValidators } from 'contexts/Validators/FavoriteValidators'; +import { ListStatusHeader } from 'library/List'; + +export const ValidatorFavorites = () => { + const { t } = useTranslation('pages'); + const { isReady } = useApi(); + const { favoritesList } = useFavoriteValidators(); + + return ( + <PageRow> + <CardWrapper> + {favoritesList === null ? ( + <ListStatusHeader> + {t('validators.fetchingFavoriteValidators')}... + </ListStatusHeader> + ) : ( + isReady && + (favoritesList.length > 0 ? ( + <ValidatorList + bondFor="nominator" + validators={favoritesList} + title={t('validators.favoriteValidators')} + selectable={false} + allowListFormat={false} + allowFilters + refetchOnListUpdate + allowMoreCols + toggleFavorites + /> + ) : ( + <ListStatusHeader>{t('validators.noFavorites')}</ListStatusHeader> + )) + )} + </CardWrapper> + </PageRow> + ); +}; diff --git a/src/pages/Validators/Stats/ActiveValidators.tsx b/src/pages/Validators/Stats/ActiveValidators.tsx new file mode 100644 index 0000000000..5accb0fcd8 --- /dev/null +++ b/src/pages/Validators/Stats/ActiveValidators.tsx @@ -0,0 +1,41 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { greaterThanZero } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useStaking } from 'contexts/Staking'; +import { Pie } from 'library/StatBoxList/Pie'; + +export const ActiveValidatorsStat = () => { + const { t } = useTranslation('pages'); + const { + staking: { validatorCount }, + eraStakers: { activeValidators }, + } = useStaking(); + + // active validators as percent. Avoiding dividing by zero. + let activeValidatorsAsPercent = new BigNumber(0); + if (greaterThanZero(validatorCount)) { + activeValidatorsAsPercent = new BigNumber(activeValidators).dividedBy( + validatorCount.multipliedBy(0.01) + ); + } + + const params = { + label: t('validators.activeValidators'), + stat: { + value: activeValidators, + total: validatorCount.toNumber(), + unit: '', + }, + graph: { + value1: activeValidators, + value2: validatorCount.minus(activeValidators).toNumber(), + }, + tooltip: `${activeValidatorsAsPercent.decimalPlaces(2).toFormat()}%`, + helpKey: 'Active Validator', + }; + + return <Pie {...params} />; +}; diff --git a/src/pages/Validators/Stats/AverageCommission.tsx b/src/pages/Validators/Stats/AverageCommission.tsx new file mode 100644 index 0000000000..77c8c8f7df --- /dev/null +++ b/src/pages/Validators/Stats/AverageCommission.tsx @@ -0,0 +1,18 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { useTranslation } from 'react-i18next'; +import { useValidators } from 'contexts/Validators/ValidatorEntries'; +import { Text } from 'library/StatBoxList/Text'; + +export const AverageCommissionStat = () => { + const { t } = useTranslation('pages'); + const { avgCommission } = useValidators(); + + const params = { + label: t('validators.averageCommission'), + value: `${String(avgCommission)}%`, + helpKey: 'Average Commission', + }; + return <Text {...params} />; +}; diff --git a/src/pages/Validators/Stats/TotalValidators.tsx b/src/pages/Validators/Stats/TotalValidators.tsx new file mode 100644 index 0000000000..5481a09442 --- /dev/null +++ b/src/pages/Validators/Stats/TotalValidators.tsx @@ -0,0 +1,40 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { greaterThanZero } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useStaking } from 'contexts/Staking'; +import { Pie } from 'library/StatBoxList/Pie'; + +export const TotalValidatorsStat = () => { + const { t } = useTranslation('pages'); + const { staking } = useStaking(); + const { totalValidators, maxValidatorsCount } = staking; + + // total validators as percent + let totalValidatorsAsPercent = 0; + if (greaterThanZero(maxValidatorsCount)) { + totalValidatorsAsPercent = totalValidators + .div(maxValidatorsCount.dividedBy(100)) + .toNumber(); + } + + const params = { + label: t('validators.totalValidators'), + stat: { + value: totalValidators.toNumber(), + total: maxValidatorsCount.toNumber(), + unit: '', + }, + graph: { + value1: totalValidators.toNumber(), + value2: maxValidatorsCount.minus(totalValidators).toNumber(), + }, + tooltip: `${new BigNumber(totalValidatorsAsPercent) + .decimalPlaces(2) + .toFormat()}%`, + helpKey: 'Validator', + }; + return <Pie {...params} />; +}; diff --git a/src/pages/Validators/context.tsx b/src/pages/Validators/context.tsx new file mode 100644 index 0000000000..654ef46a90 --- /dev/null +++ b/src/pages/Validators/context.tsx @@ -0,0 +1,47 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { extractUrlValue } from '@polkadot-cloud/utils'; +import React, { useState } from 'react'; + +export interface ValidatorsTabsContextInterface { + setActiveTab: (t: number) => void; + activeTab: number; +} + +export const ValidatorsTabsContext: React.Context<ValidatorsTabsContextInterface> = + React.createContext({ + // eslint-disable-next-line + setActiveTab: (t: number) => {}, + activeTab: 0, + }); + +export const useValidatorsTabs = () => React.useContext(ValidatorsTabsContext); + +export const ValidatorsTabsProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const tabFromUrl = extractUrlValue('t'); + const initialActiveTab = [0, 1].includes(Number(tabFromUrl)) + ? Number(tabFromUrl) + : 0; + + const [activeTab, setActiveTabState] = useState<number>(initialActiveTab); + + const setActiveTab = (t: number) => { + setActiveTabState(t); + }; + + return ( + <ValidatorsTabsContext.Provider + value={{ + activeTab, + setActiveTab, + }} + > + {children} + </ValidatorsTabsContext.Provider> + ); +}; diff --git a/src/pages/Validators/index.tsx b/src/pages/Validators/index.tsx new file mode 100644 index 0000000000..735c757b00 --- /dev/null +++ b/src/pages/Validators/index.tsx @@ -0,0 +1,53 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { PageTitle } from '@polkadot-cloud/react'; +import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useFavoriteValidators } from 'contexts/Validators/FavoriteValidators'; +import type { PageTitleTabProps } from '@polkadot-cloud/react/types'; +import { AllValidators } from './AllValidators'; +import { ValidatorFavorites } from './Favorites'; +import { ValidatorsTabsProvider, useValidatorsTabs } from './context'; + +export const ValidatorsInner = () => { + const { t } = useTranslation('pages'); + const { favorites } = useFavoriteValidators(); + const { activeTab, setActiveTab } = useValidatorsTabs(); + + // back to tab 0 if not in the first tab + useEffect(() => { + if (![0].includes(activeTab)) { + setActiveTab(0); + } + }, []); + + let tabs: PageTitleTabProps[] = [ + { + title: t('validators.allValidators'), + active: activeTab === 0, + onClick: () => setActiveTab(0), + }, + ]; + + tabs = tabs.concat({ + title: t('validators.favorites'), + active: activeTab === 1, + onClick: () => setActiveTab(1), + badge: String(favorites.length), + }); + + return ( + <> + <PageTitle title={t('validators.validators')} tabs={tabs} /> + {activeTab === 0 && <AllValidators />} + {activeTab === 1 && <ValidatorFavorites />} + </> + ); +}; + +export const Validators = () => ( + <ValidatorsTabsProvider> + <ValidatorsInner /> + </ValidatorsTabsProvider> +); diff --git a/src/styles/graphs.ts b/src/styles/graphs.ts new file mode 100644 index 0000000000..c50d52a722 --- /dev/null +++ b/src/styles/graphs.ts @@ -0,0 +1,23 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { AnyJson } from 'types'; + +export const graphColors: Record<string, AnyJson> = { + inactive: { + light: '#eee', + dark: 'rgb(39,35,39)', + }, + tooltip: { + light: '#333', + dark: '#ddd', + }, + label: { + light: '#fafafa', + dark: '#0e0e0e', + }, + grid: { + light: '#e8e8e8', + dark: 'rgb(64,55,64)', + }, +}; diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000000..be4cabf696 --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,9 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +html { + font-size: 10.5px; + @media (min-width: 600px) { + font-size: 11px; + } +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000000..4a18a689f1 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,129 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type React from 'react'; +import type { FunctionComponent, SVGProps } from 'react'; +import type { Theme } from 'contexts/Themes/types'; +import type { ExtensionInjected } from '@polkadot-cloud/react/types'; + +declare global { + interface Window { + injectedWeb3?: Record<string, ExtensionInjected>; + } +} + +export type NetworkName = 'polkadot' | 'kusama' | 'westend'; + +export type Networks = Record<string, Network>; + +type NetworkColor = + | 'primary' + | 'secondary' + | 'stroke' + | 'transparent' + | 'pending'; +export interface Network { + name: NetworkName; + endpoints: { + lightClient: AnyApi; + defaultRpcEndpoint: string; + rpcEndpoints: Record<string, string>; + }; + namespace: string; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + colors: Record<NetworkColor, { [key in Theme]: string }>; + subscanEndpoint: string; + unit: string; + units: number; + ss58: number; + brand: { + icon: FunctionComponent< + SVGProps<SVGSVGElement> & { title?: string | undefined } + >; + token: FunctionComponent< + SVGProps<SVGSVGElement> & { title?: string | undefined } + >; + logo: { + svg: FunctionComponent< + SVGProps<SVGSVGElement> & { title?: string | undefined } + >; + width: string; + }; + inline: { + svg: FunctionComponent< + SVGProps<SVGSVGElement> & { title?: string | undefined } + >; + size: string; + }; + }; + api: { + unit: string; + priceTicker: string; + }; + params: Record<string, number>; + defaultFeeReserve: number; +} + +export interface PageCategory { + id: number; + key: string; +} + +export type PageCategoryItems = PageCategory[]; + +export interface PageItem { + category: number; + key: string; + uri: string; + hash: string; + Entry: React.FC<PageProps>; + lottie: AnyJson; + action?: { + type: string; + status: string; + text?: string | undefined; + }; +} + +export type PagesConfigItems = PageItem[]; + +export interface PageProps { + page: PageProp; +} + +interface PageProp { + key: string; +} + +export type MaybeAddress = string | null; + +export type MaybeString = string | null; + +// list of available plugins. +export type Plugin = 'subscan' | 'binance_spot' | 'tips' | 'polkawatch'; + +// track the status of a syncing / fetching process. +export type Sync = 'unsynced' | 'syncing' | 'synced'; + +// track whether bonding should be for nominator or nomination pool. +export type BondFor = 'pool' | 'nominator'; + +// which medium components are being displayed on. +export type DisplayFor = 'default' | 'modal' | 'canvas'; + +// generic function with no args or return type. +export type Fn = () => void; + +// any types to compress compiler warnings +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyApi = any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyJson = any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyFunction = any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyMetaBatch = any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnySubscan = any; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type AnyPolkawatch = any; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000000..5eeb1139ed --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,8 @@ +/// <reference types="vite/client" /> +/// <reference types="vite-plugin-svgr/client" /> + +declare namespace JSX { + interface IntrinsicElements { + 'dotlottie-player': any; + } +} diff --git a/src/workers/poolPerformance.ts b/src/workers/poolPerformance.ts new file mode 100644 index 0000000000..ad4b46af4d --- /dev/null +++ b/src/workers/poolPerformance.ts @@ -0,0 +1,63 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only +/* eslint-disable no-await-in-loop */ + +import type { Exposure } from 'contexts/Staking/types'; +import type { ErasRewardPoints } from 'contexts/Validators/types'; +import type { AnyApi, AnyJson } from 'types'; + +// eslint-disable-next-line no-restricted-globals +export const ctx: Worker = self as any; + +// handle incoming message and route to correct handler. +ctx.addEventListener('message', async (event: AnyJson) => { + const { data } = event; + const { task } = data; + let message: AnyJson = {}; + switch (task) { + case 'processNominationPoolsRewardData': + message = await processErasStakersForNominationPoolRewards(data); + break; + default: + } + postMessage({ task, ...message }); +}); + +// Process `erasStakersClipped` and generate nomination pool reward data. +const processErasStakersForNominationPoolRewards = async ({ + bondedPools, + era, + erasRewardPoints, + exposures, +}: { + bondedPools: string[]; + era: string; + erasRewardPoints: ErasRewardPoints; + exposures: Exposure[]; +}) => { + const poolRewardData: Record<string, Record<string, string>> = {}; + + for (const address of bondedPools) { + let validator = null; + for (const exposure of exposures) { + const { others } = exposure.val; + const inOthers = others.find((o: AnyApi) => o.who === address); + + if (inOthers) { + validator = exposure.keys[1]; + break; + } + } + + if (validator) { + const rewardPoints: string = + erasRewardPoints[era]?.individual?.[validator || ''] ?? 0; + if (!poolRewardData[address]) poolRewardData[address] = {}; + poolRewardData[address][era] = rewardPoints; + } + } + + return { + poolRewardData, + }; +}; diff --git a/src/workers/stakers.ts b/src/workers/stakers.ts new file mode 100644 index 0000000000..4353bae77b --- /dev/null +++ b/src/workers/stakers.ts @@ -0,0 +1,204 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import { planckToUnit, rmCommas } from '@polkadot-cloud/utils'; +import BigNumber from 'bignumber.js'; +import type { + ActiveAccountStaker, + ExposureOther, + Staker, +} from 'contexts/Staking/types'; +import type { AnyJson } from 'types'; +import type { LocalValidatorExposure } from 'contexts/Payouts/types'; +import type { DataInitialiseExposures } from './types'; + +// eslint-disable-next-line no-restricted-globals +export const ctx: Worker = self as any; + +// handle incoming message and route to correct handler. +ctx.addEventListener('message', (event: AnyJson) => { + const { data } = event; + const { task } = data; + let message: AnyJson = {}; + switch (task) { + case 'processExposures': + message = processExposures(data as DataInitialiseExposures); + break; + case 'processEraForExposure': + message = processEraForExposure(data); + break; + default: + } + postMessage({ task, ...message }); +}); + +// Process era exposures and return if an account was exposed, along with the validator they backed. +const processEraForExposure = (data: AnyJson) => { + const { era, exposures, exitOnExposed, task, networkName, who } = data; + let exposed = false; + + // If exposed, the validator that was backed. + const exposedValidators: Record<string, LocalValidatorExposure> = {}; + + // Check exposed as validator or nominator. + exposures.every(({ keys, val }: any) => { + const validator = keys[1]; + const others = val?.others ?? []; + const own = val?.own || 0; + const total = val?.total || 0; + const isValidator = validator === who; + + if (isValidator) { + const share = new BigNumber(own).isZero() + ? '0' + : new BigNumber(own).dividedBy(total).toString(); + + exposedValidators[validator] = { + staked: own, + total, + share, + isValidator, + }; + + exposed = true; + if (exitOnExposed) return false; + } + + const inOthers = others.find((o: AnyJson) => o.who === who); + + if (inOthers) { + const share = new BigNumber(inOthers.value).isZero() + ? '0' + : new BigNumber(inOthers.value).dividedBy(total).toString(); + + exposedValidators[validator] = { + staked: inOthers.value, + total, + share, + isValidator, + }; + exposed = true; + if (exitOnExposed) return false; + } + + return true; + }); + + return { + networkName, + era, + exposed, + exposedValidators: Object.keys(exposedValidators).length + ? exposedValidators + : null, + task, + who, + }; +}; + +// process exposures. +// +// abstracts active nominators erasStakers. +const processExposures = (data: DataInitialiseExposures) => { + const { + task, + networkName, + era, + units, + exposures, + activeAccount, + maxNominatorRewardedPerValidator, + } = data; + + const stakers: Staker[] = []; + let activeValidators = 0; + const activeAccountOwnStake: ActiveAccountStaker[] = []; + const nominators: ExposureOther[] = []; + + exposures.forEach(({ keys, val }) => { + activeValidators++; + + const address = keys[1]; + let others = + val?.others.map((o) => ({ + ...o, + value: rmCommas(o.value), + })) ?? []; + + // Accumulate active nominators and min active stake threshold. + if (others.length) { + // Sort `others` by value bonded, largest first. + others = others.sort((a, b) => { + const r = new BigNumber(rmCommas(b.value)).minus(rmCommas(a.value)); + return r.isZero() ? 0 : r.isLessThan(0) ? -1 : 1; + }); + + const lowestRewardIndex = Math.min( + maxNominatorRewardedPerValidator - 1, + others.length + ); + + const lowestReward = + others.length > 0 + ? planckToUnit( + new BigNumber(others[lowestRewardIndex]?.value || 0), + units + ).toString() + : '0'; + + const oversubscribed = others.length > maxNominatorRewardedPerValidator; + + stakers.push({ + address, + lowestReward, + oversubscribed, + others, + own: rmCommas(val.own), + total: rmCommas(val.total), + }); + + // Accumulate active stake for all nominators. + for (const o of others) { + const value = new BigNumber(rmCommas(o.value)); + + // Check nominator already exists. + const index = nominators.findIndex(({ who }) => who === o.who); + + // Add value to nominator, otherwise add new entry. + if (index === -1) { + nominators.push({ + who: o.who, + value: value.toString(), + }); + } else { + nominators[index].value = new BigNumber(nominators[index].value) + .plus(value) + .toString(); + } + } + + // get own stake if present + const own = others.find(({ who }) => who === activeAccount); + if (own !== undefined) { + activeAccountOwnStake.push({ + address, + value: planckToUnit( + new BigNumber(rmCommas(own.value)), + units + ).toString(), + }); + } + } + }); + + return { + networkName, + era, + stakers, + totalActiveNominators: nominators.length, + activeAccountOwnStake, + activeValidators, + task, + who: activeAccount, + }; +}; diff --git a/src/workers/types.ts b/src/workers/types.ts new file mode 100644 index 0000000000..7293c7dd03 --- /dev/null +++ b/src/workers/types.ts @@ -0,0 +1,30 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import type { + ActiveAccountStaker, + Exposure, + Staker, +} from 'contexts/Staking/types'; +import type { MaybeAddress, NetworkName } from 'types'; + +export interface DataInitialiseExposures { + task: string; + networkName: NetworkName; + era: string; + activeAccount: MaybeAddress; + units: number; + exposures: Exposure[]; + maxNominatorRewardedPerValidator: number; +} + +export interface ResponseInitialiseExposures { + task: string; + networkName: NetworkName; + era: string; + stakers: Staker[]; + totalActiveNominators: number; + activeAccountOwnStake: ActiveAccountStaker[]; + activeValidators: number; + who: MaybeAddress; +} diff --git a/tests/graphs.test.ts b/tests/graphs.test.ts new file mode 100644 index 0000000000..557d325c06 --- /dev/null +++ b/tests/graphs.test.ts @@ -0,0 +1,157 @@ +/* Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors + * SPDX-License-Identifier: GPL-3.0-only */ + +import { fromUnixTime, getUnixTime, startOfToday, subDays } from 'date-fns'; +import { expect, test } from 'vitest'; +import { + daysPassed, + normalisePayouts, + postFillMissingDays, + prefillMissingDays, +} from 'library/Graphs/Utils'; + +// payouts that were made 2, 3 and 4 days ago. +const mockPayouts = [ + { + amount: '10000000000', + block_timestamp: getUnixTime(subDays(new Date(), 2)), + }, + { + amount: '15000000000', + block_timestamp: getUnixTime(subDays(new Date(), 3)), + }, + { + amount: '5000000000', + block_timestamp: getUnixTime(subDays(new Date(), 4)), + }, +]; + +// Get the correct amount of days passed between 2 payout timestamps. +// +// `daysPassed` is a utility function that is used throughout the graph data accumulation process. +test('days passed works', () => { + const payouts = normalisePayouts(mockPayouts); + // days passed works on `mockPayouts`. + expect( + daysPassed(fromUnixTime(payouts[0].block_timestamp), startOfToday()) + ).toBe(2); + expect( + daysPassed(fromUnixTime(payouts[1].block_timestamp), startOfToday()) + ).toBe(3); + expect( + daysPassed(fromUnixTime(payouts[2].block_timestamp), startOfToday()) + ).toBe(4); + + // max amount of missing days to process should be correct. + for (let i = 1; i < 368; i++) { + expect(daysPassed(subDays(new Date(), i), new Date())).toBe(i); + } +}); + +// Fill missing days from the latest payout to the current day. +// +// Note that the latest payout is assumed to be the first in the payout list. +test('post fill missing days works', () => { + // p0, p1, p2, p3, p4, p5, p6 + // - - x x x 0 0 + const payouts = normalisePayouts(mockPayouts); + const fromDate = new Date(); + const maxDays = 7; + + // post fill the missing days for mock payouts. + const missingDays = postFillMissingDays(payouts, fromDate, maxDays); + + // amount of missing days returned should be correct. + expect(missingDays.length).toBe(2); + + // concatenated payouts are correct + const concatPayouts = missingDays.concat(payouts); + + // days passed and ordering are correct. + for (let i = 0; i < concatPayouts.length; i++) { + if (i > 0) { + expect( + daysPassed( + fromUnixTime(concatPayouts[i].block_timestamp), + fromUnixTime(concatPayouts[i - 1].block_timestamp) + ) + ).toBe(1); + expect(concatPayouts[i].block_timestamp).toBeLessThan( + concatPayouts[i - 1].block_timestamp + ); + } + } +}); + +// Fill missing days from the earliest payout to the current day. +// +// Note that the earliest payout is assumed to be the last in the payout list. +test('pre fill missing days works', () => { + // p0, p1, p2, p3, p4, p5, p6 + // x x x - - + const payouts = normalisePayouts(mockPayouts); + const fromDate = new Date(); + const maxDays = 7; + + // post fill the missing days for mock payouts. + const missingDays = prefillMissingDays(payouts, fromDate, maxDays); + + // expect amount of missing days to be 2 + expect(missingDays.length).toBe(2); + + // concatenated payouts are correct + const concatPayouts = payouts.concat(missingDays); + + // days passed and ordering are correct. + for (let i = 0; i < concatPayouts.length; i++) { + if (i > 0) { + expect( + daysPassed( + fromUnixTime(concatPayouts[i].block_timestamp), + fromUnixTime(concatPayouts[i - 1].block_timestamp) + ) + ).toBe(1); + expect(concatPayouts[i].block_timestamp).toBeLessThan( + concatPayouts[i - 1].block_timestamp + ); + } + } +}); + +// Use post-fill and pre-fill together. +// +// Test filling days from both directions. +test('pre fill and post fill missing days work together', () => { + // p0, p1, p2, p3, p4, p5, p6, p7, p8, p9 + // - - x x x - - - - - + const payouts = normalisePayouts(mockPayouts); + const fromDate = new Date(); + const maxDays = 10; + + // post fill the missing days for mock payouts. + const missingPostDays = postFillMissingDays(payouts, fromDate, maxDays); + expect(missingPostDays.length).toBe(2); + + const missingPreDays = prefillMissingDays(payouts, fromDate, maxDays); + expect(missingPreDays.length).toBe(5); + + const finalPayouts = missingPostDays.concat(payouts).concat(missingPreDays); + expect(finalPayouts.length).toBe(10); + + // days passed and ordering are correct. + for (let i = 0; i < finalPayouts.length; i++) { + if (i > 0) { + expect( + daysPassed( + fromUnixTime(finalPayouts[i].block_timestamp), + fromUnixTime(finalPayouts[i - 1].block_timestamp) + ) + ).toBe(1); + expect(finalPayouts[i].block_timestamp).toBeLessThan( + finalPayouts[i - 1].block_timestamp + ); + } + } +}); + +export {}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..9a4e47a622 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": [ + "react", + "react-dom", + ], + }, + "include": [ + "src", + "tests" + ], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000000..9d31e2aed9 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000..1fa9c14a85 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,46 @@ +// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: GPL-3.0-only + +import react from '@vitejs/plugin-react-swc'; +import { defineConfig } from 'vite'; +import checker from 'vite-plugin-checker'; +import eslint from 'vite-plugin-eslint'; +import svgr from 'vite-plugin-svgr'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +// https://vitejs.dev/config/ +// +// NOTES: +// - `base` is configured in `package.json` with the vite --base flag. In local dev it is `/`, +// whereas gh-pages always deploys to `/polkadot-staking-dashboard/`. Producution builds can also +// be configureed with the `--base` flag. +// - `BASE_URL`env variable is used in the codebase to refer to the supplied base. +export default defineConfig({ + plugins: [ + eslint(), + react(), + svgr(), + tsconfigPaths(), + checker({ + typescript: true, + }), + ], + build: { + outDir: 'build', + rollupOptions: { + output: { + manualChunks: { + '@substrate/connect': ['@substrate/connect'], + }, + }, + }, + }, + server: { + fs: { + strict: false, + }, + }, + optimizeDeps: { + include: ['react/jsx-runtime'], + }, +}); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000..29ac167619 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,5200 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/compat-data@^7.22.9": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" + integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ== + +"@babel/core@^7.21.3": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" + integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helpers" "^7.23.2" + "@babel/parser" "^7.23.0" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" + integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.15" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-module-transforms@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e" + integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-option@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" + integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== + +"@babel/helpers@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767" + integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ== + dependencies: + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + +"@babel/runtime@^7.10.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.21.3", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@chainsafe/metamask-polkadot-adapter@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@chainsafe/metamask-polkadot-adapter/-/metamask-polkadot-adapter-0.5.1.tgz#04ecf78ce6cdcc63ebdc2c97f67d0c1efdb8bee8" + integrity sha512-t3/KoGTgayt2qQOmEWj5Gx8UVvqjhEuybMYadInWsvsDvmyhSKUxzGC8JlUSr4ww0Fa8Dzem21Hxo0FT9rxjyg== + dependencies: + "@polkadot/api" "^10.9.1" + "@polkadot/extension-inject" "^0.46.5" + "@polkadot/types-augment" "^10.9.1" + +"@dotlottie/player-component@^2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@dotlottie/player-component/-/player-component-2.7.0.tgz#b7f5edfa9a54a0d99eb5cd85b94f0aebcd9270b3" + integrity sha512-UZiBEViLVSjXGRZKHgFDRAFHettwKWJgh3eBriGY4ljNfaKxbItx/+KWVpUgPQxDGmIQt1ouMyDeQEZGwonCjQ== + dependencies: + lit "^2.7.5" + +"@emotion/is-prop-valid@^0.8.2": + version "0.8.8" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" + integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== + dependencies: + "@emotion/memoize" "0.7.4" + +"@emotion/is-prop-valid@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" + integrity sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw== + dependencies: + "@emotion/memoize" "^0.8.1" + +"@emotion/memoize@0.7.4": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" + integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== + +"@emotion/memoize@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== + +"@emotion/unitless@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + +"@esbuild/android-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz#984b4f9c8d0377443cc2dfcef266d02244593622" + integrity sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ== + +"@esbuild/android-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz#fedb265bc3a589c84cc11f810804f234947c3682" + integrity sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw== + +"@esbuild/android-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz#35cf419c4cfc8babe8893d296cd990e9e9f756f2" + integrity sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg== + +"@esbuild/darwin-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz#08172cbeccf95fbc383399a7f39cfbddaeb0d7c1" + integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA== + +"@esbuild/darwin-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz#d70d5790d8bf475556b67d0f8b7c5bdff053d85d" + integrity sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ== + +"@esbuild/freebsd-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz#98755cd12707f93f210e2494d6a4b51b96977f54" + integrity sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw== + +"@esbuild/freebsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz#c1eb2bff03915f87c29cece4c1a7fa1f423b066e" + integrity sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ== + +"@esbuild/linux-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz#bad4238bd8f4fc25b5a021280c770ab5fc3a02a0" + integrity sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA== + +"@esbuild/linux-arm@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz#3e617c61f33508a27150ee417543c8ab5acc73b0" + integrity sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg== + +"@esbuild/linux-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz#699391cccba9aee6019b7f9892eb99219f1570a7" + integrity sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA== + +"@esbuild/linux-loong64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz#e6fccb7aac178dd2ffb9860465ac89d7f23b977d" + integrity sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg== + +"@esbuild/linux-mips64el@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz#eeff3a937de9c2310de30622a957ad1bd9183231" + integrity sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ== + +"@esbuild/linux-ppc64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz#2f7156bde20b01527993e6881435ad79ba9599fb" + integrity sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA== + +"@esbuild/linux-riscv64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz#6628389f210123d8b4743045af8caa7d4ddfc7a6" + integrity sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A== + +"@esbuild/linux-s390x@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz#255e81fb289b101026131858ab99fba63dcf0071" + integrity sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ== + +"@esbuild/linux-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz#c7690b3417af318a9b6f96df3031a8865176d338" + integrity sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w== + +"@esbuild/netbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz#30e8cd8a3dded63975e2df2438ca109601ebe0d1" + integrity sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A== + +"@esbuild/openbsd-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz#7812af31b205055874c8082ea9cf9ab0da6217ae" + integrity sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg== + +"@esbuild/sunos-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz#d5c275c3b4e73c9b0ecd38d1ca62c020f887ab9d" + integrity sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ== + +"@esbuild/win32-arm64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz#73bc7f5a9f8a77805f357fab97f290d0e4820ac9" + integrity sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg== + +"@esbuild/win32-ia32@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz#ec93cbf0ef1085cc12e71e0d661d20569ff42102" + integrity sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g== + +"@esbuild/win32-x64@0.18.20": + version "0.18.20" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d" + integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" + integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.52.0": + version "8.52.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.52.0.tgz#78fe5f117840f69dc4a353adf9b9cd926353378c" + integrity sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA== + +"@fortawesome/fontawesome-common-types@6.4.2": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5" + integrity sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA== + +"@fortawesome/fontawesome-svg-core@^6.4.2": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz#37f4507d5ec645c8b50df6db14eced32a6f9be09" + integrity sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg== + dependencies: + "@fortawesome/fontawesome-common-types" "6.4.2" + +"@fortawesome/free-brands-svg-icons@^6.4.2": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.4.2.tgz#9b8e78066ea6dd563da5dfa686615791d0f7cc71" + integrity sha512-LKOwJX0I7+mR/cvvf6qIiqcERbdnY+24zgpUSouySml+5w8B4BJOx8EhDR/FTKAu06W12fmUIcv6lzPSwYKGGg== + dependencies: + "@fortawesome/fontawesome-common-types" "6.4.2" + +"@fortawesome/free-regular-svg-icons@^6.4.2": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz#aee79ed76ce5dd04931352f9d83700761b8b1b25" + integrity sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q== + dependencies: + "@fortawesome/fontawesome-common-types" "6.4.2" + +"@fortawesome/free-solid-svg-icons@^6.4.2": + version "6.4.2" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz#33a02c4cb6aa28abea7bc082a9626b7922099df4" + integrity sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA== + dependencies: + "@fortawesome/fontawesome-common-types" "6.4.2" + +"@fortawesome/react-fontawesome@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4" + integrity sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw== + dependencies: + prop-types "^15.8.1" + +"@humanwhocodes/config-array@^0.11.13": + version "0.11.13" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" + integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== + dependencies: + "@humanwhocodes/object-schema" "^2.0.1" + debug "^4.1.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" + integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@kurkle/color@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" + integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== + +"@ledgerhq/devices@^8.0.7": + version "8.0.7" + resolved "https://registry.yarnpkg.com/@ledgerhq/devices/-/devices-8.0.7.tgz#206434dbd8a097529bbfc95f5eef94c2923c7578" + integrity sha512-BbPyET52lXnVs7CxJWrGYqmtGdbGzj+XnfCqLsDnA7QYr1CZREysxmie+Rr6BKpNDBRVesAovXjtaVaZOn+upw== + dependencies: + "@ledgerhq/errors" "^6.14.0" + "@ledgerhq/logs" "^6.10.1" + rxjs "6" + semver "^7.3.5" + +"@ledgerhq/errors@^6.14.0": + version "6.14.0" + resolved "https://registry.yarnpkg.com/@ledgerhq/errors/-/errors-6.14.0.tgz#0bf253983773ef12eebce2091f463bc719223b37" + integrity sha512-ZWJw2Ti6Dq1Ott/+qYqJdDWeZm16qI3VNG5rFlb0TQ3UcAyLIQZbnnzzdcVVwVeZiEp66WIpINd/pBdqsHVyOA== + +"@ledgerhq/hw-transport-webhid@^6.27.19": + version "6.27.19" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport-webhid/-/hw-transport-webhid-6.27.19.tgz#5a655b497258d94ec6494db7b56e17dd0c610638" + integrity sha512-RMnktayqqLE2uFQDw9TKoW+WSP8KnT0ElKcIISf3sXVrzHD2y0moPk/wXOzGfi+cgN4uiKy86UD/5mgz3wlm6Q== + dependencies: + "@ledgerhq/devices" "^8.0.7" + "@ledgerhq/errors" "^6.14.0" + "@ledgerhq/hw-transport" "^6.28.8" + "@ledgerhq/logs" "^6.10.1" + +"@ledgerhq/hw-transport@^6.27.1", "@ledgerhq/hw-transport@^6.28.8": + version "6.28.8" + resolved "https://registry.yarnpkg.com/@ledgerhq/hw-transport/-/hw-transport-6.28.8.tgz#f99a5c71c5c09591e9bfb1b970c42aafbe81351f" + integrity sha512-XxQVl4htd018u/M66r0iu5nlHi+J6QfdPsORzDF6N39jaz+tMqItb7tUlXM/isggcuS5lc7GJo7NOuJ8rvHZaQ== + dependencies: + "@ledgerhq/devices" "^8.0.7" + "@ledgerhq/errors" "^6.14.0" + events "^3.3.0" + +"@ledgerhq/logs@^6.10.1": + version "6.10.1" + resolved "https://registry.yarnpkg.com/@ledgerhq/logs/-/logs-6.10.1.tgz#5bd16082261d7364eabb511c788f00937dac588d" + integrity sha512-z+ILK8Q3y+nfUl43ctCPuR4Y2bIxk/ooCQFwZxhtci1EhAtMDzMAx2W25qx8G1PPL9UUOdnUax19+F0OjXoj4w== + +"@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.2.tgz#d693d972974a354034454ec1317eb6afd0b00312" + integrity sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g== + +"@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.3.tgz#25b4eece2592132845d303e091bad9b04cdcfe03" + integrity sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.0.0" + +"@noble/curves@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== + dependencies: + "@noble/hashes" "1.3.2" + +"@noble/hashes@1.3.2", "@noble/hashes@^1.2.0", "@noble/hashes@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pkgr/utils@^2.3.1": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@pkgr/utils/-/utils-2.4.2.tgz#9e638bbe9a6a6f165580dc943f138fd3309a2cbc" + integrity sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw== + dependencies: + cross-spawn "^7.0.3" + fast-glob "^3.3.0" + is-glob "^4.0.3" + open "^9.1.0" + picocolors "^1.0.0" + tslib "^2.6.0" + +"@polkadot-cloud/assets@0.1.32", "@polkadot-cloud/assets@^0.1.32": + version "0.1.32" + resolved "https://registry.yarnpkg.com/@polkadot-cloud/assets/-/assets-0.1.32.tgz#b619e24db310ac9a23e4bb20a64245f096b73072" + integrity sha512-TC8m6RFbHtJ5omW4VuIugKp1u7YzGBIgG9vGVEXkeSMWKwgzvkrxn7qpcjcelu1XO8ujb/+64dnhIpuhdj4e9g== + +"@polkadot-cloud/core@^1.0.30": + version "1.0.30" + resolved "https://registry.yarnpkg.com/@polkadot-cloud/core/-/core-1.0.30.tgz#cc1e3f3477f402b8842338ed3e2ea4bfe8b19a23" + integrity sha512-hn3oo/JK17W5alwXPxL5M0zmZidihjklEsYv7hXF7XqWZP9musntdn5LydzHz0J7m05OTnH+9tw/GNusvqBaSg== + +"@polkadot-cloud/react@^0.1.101": + version "0.1.101" + resolved "https://registry.yarnpkg.com/@polkadot-cloud/react/-/react-0.1.101.tgz#59323a9880b5421f9980ee34ea0d2324974872c3" + integrity sha512-y8diS5HfHIseItbaGLeY2ez60cVGZXbJUJGOquzNhcHgZsuE9W05j4lwl+EKlxnrNr+lnhfyESz1eE1YO6j0yA== + dependencies: + "@chainsafe/metamask-polkadot-adapter" "^0.5.1" + "@fortawesome/fontawesome-svg-core" "^6.4.2" + "@fortawesome/free-brands-svg-icons" "^6.4.2" + "@fortawesome/free-regular-svg-icons" "^6.4.2" + "@fortawesome/free-solid-svg-icons" "^6.4.2" + "@fortawesome/react-fontawesome" "^0.2.0" + "@polkadot-cloud/assets" "0.1.32" + "@polkadot-cloud/core" "^1.0.30" + "@polkadot-cloud/utils" "^0.0.23" + "@polkadot/keyring" "^12.5.1" + "@polkadot/util" "^12.5.1" + "@polkadot/util-crypto" "^12.5.1" + framer-motion "^10.15.0" + react-error-boundary "^4.0.11" + +"@polkadot-cloud/utils@^0.0.23": + version "0.0.23" + resolved "https://registry.yarnpkg.com/@polkadot-cloud/utils/-/utils-0.0.23.tgz#eb703f74c7f05f763f7624727ba83589131ae1c5" + integrity sha512-IWqtn/cBgxletCH+MqBwZhxPxEbFLhfCU6L4RmCSCoes0bC1xUHMtFevPiJuaEkXQTdFvOnosA8BbSI01/GUuA== + dependencies: + "@polkadot/keyring" "^12.5.1" + "@polkadot/util" "^12.5.1" + bignumber.js "^9.1.1" + +"@polkadot/api-augment@10.10.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-augment/-/api-augment-10.10.1.tgz#d3d296c923b0ff915c8d4f163e9b3bad70b89b9b" + integrity sha512-J0r1DT1M5y75iO1iwcpUBokKD3q6b22kWlPfiHEDNFydVw5vm7OTRBk9Njjl8rOnlSzcW/Ya8qWfV/wkrqHxUQ== + dependencies: + "@polkadot/api-base" "10.10.1" + "@polkadot/rpc-augment" "10.10.1" + "@polkadot/types" "10.10.1" + "@polkadot/types-augment" "10.10.1" + "@polkadot/types-codec" "10.10.1" + "@polkadot/util" "^12.5.1" + tslib "^2.6.2" + +"@polkadot/api-base@10.10.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-base/-/api-base-10.10.1.tgz#2d02f96960cbdd9d0ab61fe016587585902d1ee8" + integrity sha512-joH2Ywxnn+AStkw+JWAdF3i3WJy4NcBYp0SWJM/WqGafWR/FuHnati2pcj/MHzkHT8JkBippmSSJFvsqRhlwcQ== + dependencies: + "@polkadot/rpc-core" "10.10.1" + "@polkadot/types" "10.10.1" + "@polkadot/util" "^12.5.1" + rxjs "^7.8.1" + tslib "^2.6.2" + +"@polkadot/api-derive@10.10.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-10.10.1.tgz#555d755c393f57c8855b9fc28062148a3723e333" + integrity sha512-Q9Ibs4eRPqdV8qnRzFPD3dlWNbLHxRqMqNTNPmNQwKPo5m6fcQbZ0UZy3yJ+PI9S4AQHGhsWtfoi5qW8006GHQ== + dependencies: + "@polkadot/api" "10.10.1" + "@polkadot/api-augment" "10.10.1" + "@polkadot/api-base" "10.10.1" + "@polkadot/rpc-core" "10.10.1" + "@polkadot/types" "10.10.1" + "@polkadot/types-codec" "10.10.1" + "@polkadot/util" "^12.5.1" + "@polkadot/util-crypto" "^12.5.1" + rxjs "^7.8.1" + tslib "^2.6.2" + +"@polkadot/api@10.10.1", "@polkadot/api@^10.10.1", "@polkadot/api@^10.9.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-10.10.1.tgz#06fcbdcc8e17d2312d4b4093733d506f15ff62ad" + integrity sha512-YHVkmNvjGF4Eg3thAbVhj9UX3SXx+Yxk6yVuzsEcckEudIRHzL2ikIWGCfUprfzSeFNpUCKdJIi1tsxVHtA7Tg== + dependencies: + "@polkadot/api-augment" "10.10.1" + "@polkadot/api-base" "10.10.1" + "@polkadot/api-derive" "10.10.1" + "@polkadot/keyring" "^12.5.1" + "@polkadot/rpc-augment" "10.10.1" + "@polkadot/rpc-core" "10.10.1" + "@polkadot/rpc-provider" "10.10.1" + "@polkadot/types" "10.10.1" + "@polkadot/types-augment" "10.10.1" + "@polkadot/types-codec" "10.10.1" + "@polkadot/types-create" "10.10.1" + "@polkadot/types-known" "10.10.1" + "@polkadot/util" "^12.5.1" + "@polkadot/util-crypto" "^12.5.1" + eventemitter3 "^5.0.1" + rxjs "^7.8.1" + tslib "^2.6.2" + +"@polkadot/extension-inject@^0.46.5": + version "0.46.5" + resolved "https://registry.yarnpkg.com/@polkadot/extension-inject/-/extension-inject-0.46.5.tgz#6abee0eb28a73fd1a9461f257cac5875c2e9fc63" + integrity sha512-QcpkCMuv7iFbWjufkw14JRozpEYFyjP0H8KOJ8IsHGfPd2DPiismQ0NXr+AS7f6U+0I+Rhv9E4dnXxtJPROVMQ== + dependencies: + "@polkadot/api" "^10.9.1" + "@polkadot/rpc-provider" "^10.9.1" + "@polkadot/types" "^10.9.1" + "@polkadot/util" "^12.3.2" + "@polkadot/util-crypto" "^12.3.2" + "@polkadot/x-global" "^12.3.2" + tslib "^2.5.3" + +"@polkadot/keyring@^12.1.1", "@polkadot/keyring@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-12.5.1.tgz#2f38504aa915f54bbd265f3793a6be55010eb1f5" + integrity sha512-u6b+Q7wI6WY/vwmJS9uUHy/5hKZ226nTlVNmxjkj9GvrRsQvUSwS94163yHPJwiZJiIv5xK5m0rwCMyoYu+wjA== + dependencies: + "@polkadot/util" "12.5.1" + "@polkadot/util-crypto" "12.5.1" + tslib "^2.6.2" + +"@polkadot/networks@12.5.1", "@polkadot/networks@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-12.5.1.tgz#685c69d24d78a64f4e750609af22678d57fe1192" + integrity sha512-PP6UUdzz6iHHZH4q96cUEhTcydHj16+61sqeaYEJSF6Q9iY+5WVWQ26+rdjmre/EBdrMQkSS/CKy73mO5z/JkQ== + dependencies: + "@polkadot/util" "12.5.1" + "@substrate/ss58-registry" "^1.43.0" + tslib "^2.6.2" + +"@polkadot/rpc-augment@10.10.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-augment/-/rpc-augment-10.10.1.tgz#c25ec45687631ea649e2d5c7f7f9b0813ac4ca9f" + integrity sha512-PcvsX8DNV8BNDXXnY2K8F4mE7cWz7fKg8ykXNZTN8XUN6MrI4k/ohv7itYic7X5LaP25ZmQt5UiGyjKDGIELow== + dependencies: + "@polkadot/rpc-core" "10.10.1" + "@polkadot/types" "10.10.1" + "@polkadot/types-codec" "10.10.1" + "@polkadot/util" "^12.5.1" + tslib "^2.6.2" + +"@polkadot/rpc-core@10.10.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-10.10.1.tgz#5837e9ce635d5804cad897c6336771b61f3ef61a" + integrity sha512-awfFfJYsVF6W4DrqTj5RP00SSDRNB770FIoe1QE1Op4NcSrfeLpwh54HUJS716f4l5mOSYuvMp+zCbKzt8zKow== + dependencies: + "@polkadot/rpc-augment" "10.10.1" + "@polkadot/rpc-provider" "10.10.1" + "@polkadot/types" "10.10.1" + "@polkadot/util" "^12.5.1" + rxjs "^7.8.1" + tslib "^2.6.2" + +"@polkadot/rpc-provider@10.10.1", "@polkadot/rpc-provider@^10.9.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-10.10.1.tgz#387b1a915fa7b40d5f48a408c7b0ee5980f7ce07" + integrity sha512-VMDWoJgx6/mPHAOT66Sq+Jf2lJABfV/ZUIXtT2k8HjOndbm6oKrFqGEOSSLvB2q4olDee3FkFFxkyW1s6k4JaQ== + dependencies: + "@polkadot/keyring" "^12.5.1" + "@polkadot/types" "10.10.1" + "@polkadot/types-support" "10.10.1" + "@polkadot/util" "^12.5.1" + "@polkadot/util-crypto" "^12.5.1" + "@polkadot/x-fetch" "^12.5.1" + "@polkadot/x-global" "^12.5.1" + "@polkadot/x-ws" "^12.5.1" + eventemitter3 "^5.0.1" + mock-socket "^9.3.1" + nock "^13.3.4" + tslib "^2.6.2" + optionalDependencies: + "@substrate/connect" "0.7.33" + +"@polkadot/types-augment@10.10.1", "@polkadot/types-augment@^10.9.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-augment/-/types-augment-10.10.1.tgz#178ce0b22681109396fc681a027f35da7d757cef" + integrity sha512-XRHE75IocXfFE6EADYov3pqXCyBk5SWbiHoZ0+4WYWP9SwMuzsBaAy84NlhLBlkG3+ehIqi0HpAd/qrljJGZbg== + dependencies: + "@polkadot/types" "10.10.1" + "@polkadot/types-codec" "10.10.1" + "@polkadot/util" "^12.5.1" + tslib "^2.6.2" + +"@polkadot/types-codec@10.10.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-codec/-/types-codec-10.10.1.tgz#61d28a461493bfb72606b4399078460969a049c8" + integrity sha512-ETPG0wzWzt/bDKRQmYbO7CLe/0lUt8VrG6/bECdv+Kye+8Qedba2LZyTWm/9f2ngms8TZ82yI8mPv/mozdtfnw== + dependencies: + "@polkadot/util" "^12.5.1" + "@polkadot/x-bigint" "^12.5.1" + tslib "^2.6.2" + +"@polkadot/types-create@10.10.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-create/-/types-create-10.10.1.tgz#76f1729ef3f4699d99e708801312e43825368827" + integrity sha512-7OiLzd+Ter5zrpjP7fDwA1m89kd38VvMVixfOSv8x7ld2pDT+yyyKl14TCwRSWrKWCMtIb6M3iasPhq5cUa7cw== + dependencies: + "@polkadot/types-codec" "10.10.1" + "@polkadot/util" "^12.5.1" + tslib "^2.6.2" + +"@polkadot/types-known@10.10.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-known/-/types-known-10.10.1.tgz#ccaa1364ea1073a95c5cb0d73258e154de5103d2" + integrity sha512-yRa1lbDRqg3V/zoa0vSwdGOiYTIWktILW8OfkaLDExTu0GZBSbVHZlLAta52XVpA9Zww7mrUUC9+iernOwk//w== + dependencies: + "@polkadot/networks" "^12.5.1" + "@polkadot/types" "10.10.1" + "@polkadot/types-codec" "10.10.1" + "@polkadot/types-create" "10.10.1" + "@polkadot/util" "^12.5.1" + tslib "^2.6.2" + +"@polkadot/types-support@10.10.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-support/-/types-support-10.10.1.tgz#a22d319d4ba795e386000ddf6fdc8c55f9d81a9c" + integrity sha512-Cd2mwk9RG6LlX8X3H0bRY7wCTbZPqU3z38CMFhvNkFDAyjqKjtn8hpS4n8mMrZK2EwCs/MjQH1wb7rtFkaWmJw== + dependencies: + "@polkadot/util" "^12.5.1" + tslib "^2.6.2" + +"@polkadot/types@10.10.1", "@polkadot/types@^10.9.1": + version "10.10.1" + resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-10.10.1.tgz#4a55909ff35b0b568c0b1539ae923a259b0dba6a" + integrity sha512-Ben62P1tjYEhKag34GBGcLX6NqcFR1VD5nNbWaxgr+t36Jl/tlHs6P9DlbFqQP7Tt9FmGrAYY0m3oTkhjG1NzA== + dependencies: + "@polkadot/keyring" "^12.5.1" + "@polkadot/types-augment" "10.10.1" + "@polkadot/types-codec" "10.10.1" + "@polkadot/types-create" "10.10.1" + "@polkadot/util" "^12.5.1" + "@polkadot/util-crypto" "^12.5.1" + rxjs "^7.8.1" + tslib "^2.6.2" + +"@polkadot/util-crypto@12.5.1", "@polkadot/util-crypto@^12.3.2", "@polkadot/util-crypto@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-12.5.1.tgz#1753b23abfb9d72db950399ef65b0cbe5bef9f2f" + integrity sha512-Y8ORbMcsM/VOqSG3DgqutRGQ8XXK+X9M3C8oOEI2Tji65ZsXbh9Yh+ryPLM0oBp/9vqOXjkLgZJbbVuQceOw0A== + dependencies: + "@noble/curves" "^1.2.0" + "@noble/hashes" "^1.3.2" + "@polkadot/networks" "12.5.1" + "@polkadot/util" "12.5.1" + "@polkadot/wasm-crypto" "^7.2.2" + "@polkadot/wasm-util" "^7.2.2" + "@polkadot/x-bigint" "12.5.1" + "@polkadot/x-randomvalues" "12.5.1" + "@scure/base" "^1.1.3" + tslib "^2.6.2" + +"@polkadot/util@12.5.1", "@polkadot/util@^12.3.2", "@polkadot/util@^12.4.2", "@polkadot/util@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-12.5.1.tgz#f4e7415600b013d3b69527aa88904acf085be3f5" + integrity sha512-fDBZL7D4/baMG09Qowseo884m3QBzErGkRWNBId1UjWR99kyex+cIY9fOSzmuQxo6nLdJlLHw1Nz2caN3+Bq0A== + dependencies: + "@polkadot/x-bigint" "12.5.1" + "@polkadot/x-global" "12.5.1" + "@polkadot/x-textdecoder" "12.5.1" + "@polkadot/x-textencoder" "12.5.1" + "@types/bn.js" "^5.1.1" + bn.js "^5.2.1" + tslib "^2.6.2" + +"@polkadot/wasm-bridge@7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.2.2.tgz#957b82b17927fe080729e8930b5b5c554f77b8df" + integrity sha512-CgNENd65DVYtackOVXXRA0D1RPoCv5+77IdBCf7kNqu6LeAnR4nfTI6qjaApUdN1xRweUsQjSH7tu7VjkMOA0A== + dependencies: + "@polkadot/wasm-util" "7.2.2" + tslib "^2.6.1" + +"@polkadot/wasm-crypto-asmjs@7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.2.2.tgz#25243a4d5d8d997761141b616623cacff4329f13" + integrity sha512-wKg+cpsWQCTSVhjlHuNeB/184rxKqY3vaklacbLOMbUXieIfuDBav5PJdzS3yeiVE60TpYaHW4iX/5OYHS82gg== + dependencies: + tslib "^2.6.1" + +"@polkadot/wasm-crypto-init@7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.2.2.tgz#ffd105b87fc1b679c06c85c0848183c27bc539e3" + integrity sha512-vD4iPIp9x+SssUIWUenxWLPw4BVIwhXHNMpsV81egK990tvpyIxL205/EF5QRb1mKn8WfWcNFm5tYwwh9NdnnA== + dependencies: + "@polkadot/wasm-bridge" "7.2.2" + "@polkadot/wasm-crypto-asmjs" "7.2.2" + "@polkadot/wasm-crypto-wasm" "7.2.2" + "@polkadot/wasm-util" "7.2.2" + tslib "^2.6.1" + +"@polkadot/wasm-crypto-wasm@7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.2.2.tgz#9e49a1565bda2bc830708693b491b37ad8a2144d" + integrity sha512-3efoIB6jA3Hhv6k0YIBwCtlC8gCSWCk+R296yIXRLLr3cGN415KM/PO/d1JIXYI64lbrRzWRmZRhllw3jf6Atg== + dependencies: + "@polkadot/wasm-util" "7.2.2" + tslib "^2.6.1" + +"@polkadot/wasm-crypto@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.2.2.tgz#3c4b300c0997f4f7e2ddcdf8101d97fa1f5d1a7f" + integrity sha512-1ZY1rxUTawYm0m1zylvBMFovNIHYgG2v/XoASNp/EMG5c8FQIxCbhJRaTBA983GVq4lN/IAKREKEp9ZbLLqssA== + dependencies: + "@polkadot/wasm-bridge" "7.2.2" + "@polkadot/wasm-crypto-asmjs" "7.2.2" + "@polkadot/wasm-crypto-init" "7.2.2" + "@polkadot/wasm-crypto-wasm" "7.2.2" + "@polkadot/wasm-util" "7.2.2" + tslib "^2.6.1" + +"@polkadot/wasm-util@7.2.2", "@polkadot/wasm-util@^7.2.2": + version "7.2.2" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.2.2.tgz#f8aa62eba9a35466aa23f3c5634f3e8dbd398bbf" + integrity sha512-N/25960ifCc56sBlJZ2h5UBpEPvxBmMLgwYsl7CUuT+ea2LuJW9Xh8VHDN/guYXwmm92/KvuendYkEUykpm/JQ== + dependencies: + tslib "^2.6.1" + +"@polkadot/x-bigint@12.5.1", "@polkadot/x-bigint@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-12.5.1.tgz#0a6a3a34fae51468e7b02b42e0ff0747fd88a80a" + integrity sha512-Fw39eoN9v0sqxSzfSC5awaDVdzojIiE7d1hRSQgVSrES+8whWvtbYMR0qwbVhTuW7DvogHmye41P9xKMlXZysg== + dependencies: + "@polkadot/x-global" "12.5.1" + tslib "^2.6.2" + +"@polkadot/x-fetch@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-12.5.1.tgz#41532d1324cef56a28c31490ac81062d487b16fb" + integrity sha512-Bc019lOKCoQJrthiS+H3LwCahGtl5tNnb2HK7xe3DBQIUx9r2HsF/uEngNfMRUFkUYg5TPCLFbEWU8NIREBS1A== + dependencies: + "@polkadot/x-global" "12.5.1" + node-fetch "^3.3.2" + tslib "^2.6.2" + +"@polkadot/x-global@12.5.1", "@polkadot/x-global@^12.3.2", "@polkadot/x-global@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-12.5.1.tgz#947bb90e0c46c853ffe216dd6dcb6847d5c18a98" + integrity sha512-6K0YtWEg0eXInDOihU5aSzeb1t9TiDdX9ZuRly+58ALSqw5kPZYmQLbzE1d8HWzyXRXK+YH65GtLzfMGqfYHmw== + dependencies: + tslib "^2.6.2" + +"@polkadot/x-randomvalues@12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-12.5.1.tgz#b30c6fa8749f5776f1d8a78b6edddb9b0f9c2853" + integrity sha512-UsMb1d+77EPNjW78BpHjZLIm4TaIpfqq89OhZP/6gDIoS2V9iE/AK3jOWKm1G7Y2F8XIoX1qzQpuMakjfagFoQ== + dependencies: + "@polkadot/x-global" "12.5.1" + tslib "^2.6.2" + +"@polkadot/x-textdecoder@12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-12.5.1.tgz#8d89d2b5efbffb2550a48f8afb4a834e1d8d4f6e" + integrity sha512-j2YZGWfwhMC8nHW3BXq10fAPY02ObLL/qoTjCMJ1Cmc/OGq18Ep7k9cXXbjFAq3wf3tUUewt/u/hStKCk3IvfQ== + dependencies: + "@polkadot/x-global" "12.5.1" + tslib "^2.6.2" + +"@polkadot/x-textencoder@12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-12.5.1.tgz#9104e37a60068df2fbf57c81a7ce48669430c76c" + integrity sha512-1JNNpOGb4wD+c7zFuOqjibl49LPnHNr4rj4s3WflLUIZvOMY6euoDuN3ISjQSHCLlVSoH0sOCWA3qXZU4bCTDQ== + dependencies: + "@polkadot/x-global" "12.5.1" + tslib "^2.6.2" + +"@polkadot/x-ws@^12.5.1": + version "12.5.1" + resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-12.5.1.tgz#ff9fc78ef701e18d765443779ab95296a406138c" + integrity sha512-efNMhB3Lh6pW2iTipMkqwrjpuUtb3EwR/jYZftiIGo5tDPB7rqoMOp9s6KRFJEIUfZkLnMUtbkZ5fHzUJaCjmQ== + dependencies: + "@polkadot/x-global" "12.5.1" + tslib "^2.6.2" + ws "^8.14.1" + +"@polkawatch/ddp-client@^2.0.8": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@polkawatch/ddp-client/-/ddp-client-2.0.9.tgz#fedb4bd8ad3addebee7df2165e4cb83a2047b8d4" + integrity sha512-1e2nE7nfPT8PhdCuh37hdicvGzEiH7xNPsSKPAmcKii11C02qBFijJCF4nykx7VgaOFXAdaxZuD6tl3lqwcRCQ== + dependencies: + axios "^0.21.4" + +"@remix-run/router@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.10.0.tgz#e2170dc2049b06e65bbe883adad0e8ddf8291278" + integrity sha512-Lm+fYpMfZoEucJ7cMxgt4dYt8jLfbpwRCzAjm9UgSLOkmlqo9gupxt6YX3DY0Fk155NT9l17d/ydi+964uS9Lw== + +"@rollup/pluginutils@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" + integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@rollup/pluginutils@^5.0.4": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz#bbb4c175e19ebfeeb8c132c2eea0ecb89941a66c" + integrity sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^2.3.1" + +"@scure/base@^1.1.1", "@scure/base@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f" + integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@substrate/connect-extension-protocol@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@substrate/connect-extension-protocol/-/connect-extension-protocol-1.0.1.tgz#fa5738039586c648013caa6a0c95c43265dbe77d" + integrity sha512-161JhCC1csjH3GE5mPLEd7HbWtwNSPJBg3p1Ksz9SFlTzj/bgEwudiRN2y5i0MoLGCIJRYKyKGMxVnd29PzNjg== + +"@substrate/connect@0.7.33": + version "0.7.33" + resolved "https://registry.yarnpkg.com/@substrate/connect/-/connect-0.7.33.tgz#6fa309557b5b45cb918f5f4fe25a356384de9808" + integrity sha512-1B984/bmXVQvTT9oV3c3b7215lvWmulP9rfP3T3Ri+OU3uIsyCzYw0A+XG6J8/jgO2FnroeNIBWlgoLaUM1uzw== + dependencies: + "@substrate/connect-extension-protocol" "^1.0.1" + smoldot "2.0.1" + +"@substrate/connect@^0.7.34": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@substrate/connect/-/connect-0.7.34.tgz#e1b74125a3bf04b0037ec6f71852c943db1dd16f" + integrity sha512-APxn6PlxM+V+bJU4NSwD4WcBivqoNAQPVZAFxrM4Qf0aExHNBmB9j+uISyyc6ZbM84bN7/PWh4xd+3Wy7xEZMA== + dependencies: + "@substrate/connect-extension-protocol" "^1.0.1" + smoldot "2.0.6" + +"@substrate/ss58-registry@^1.43.0": + version "1.43.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.43.0.tgz#93108e45cb7ef6d82560c153e3692c2aa1c711b3" + integrity sha512-USEkXA46P9sqClL7PZv0QFsit4S8Im97wchKG0/H/9q3AT/S76r40UHfCr4Un7eBJPE23f7fU9BZ0ITpP9MCsA== + +"@svgr/babel-plugin-add-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" + integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== + +"@svgr/babel-plugin-remove-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" + integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== + +"@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" + integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== + +"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" + integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== + +"@svgr/babel-plugin-svg-dynamic-title@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" + integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== + +"@svgr/babel-plugin-svg-em-dimensions@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" + integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== + +"@svgr/babel-plugin-transform-react-native-svg@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" + integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== + +"@svgr/babel-plugin-transform-svg-component@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" + integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== + +"@svgr/babel-preset@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" + integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0" + "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" + "@svgr/babel-plugin-svg-dynamic-title" "8.0.0" + "@svgr/babel-plugin-svg-em-dimensions" "8.0.0" + "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" + "@svgr/babel-plugin-transform-svg-component" "8.0.0" + +"@svgr/core@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" + integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + camelcase "^6.2.0" + cosmiconfig "^8.1.3" + snake-case "^3.0.4" + +"@svgr/hast-util-to-babel-ast@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" + integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== + dependencies: + "@babel/types" "^7.21.3" + entities "^4.4.0" + +"@svgr/plugin-jsx@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" + integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + "@svgr/hast-util-to-babel-ast" "8.0.0" + svg-parser "^2.0.4" + +"@swc/core-darwin-arm64@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.95.tgz#e6b6363fc0a22ee3cd9a63130d2042d5027aae2c" + integrity sha512-VAuBAP3MNetO/yBIBzvorUXq7lUBwhfpJxYViSxyluMwtoQDhE/XWN598TWMwMl1ZuImb56d7eUsuFdjgY7pJw== + +"@swc/core-darwin-x64@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.3.95.tgz#7911a03f4e0f9359710d3d6ad1dba7b5569efe5d" + integrity sha512-20vF2rvUsN98zGLZc+dsEdHvLoCuiYq/1B+TDeE4oolgTFDmI1jKO+m44PzWjYtKGU9QR95sZ6r/uec0QC5O4Q== + +"@swc/core-linux-arm-gnueabihf@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.95.tgz#95a2c9fc6849df9f1944957669c82c559d65b24f" + integrity sha512-oEudEM8PST1MRNGs+zu0cx5i9uP8TsLE4/L9HHrS07Ck0RJ3DCj3O2fU832nmLe2QxnAGPwBpSO9FntLfOiWEQ== + +"@swc/core-linux-arm64-gnu@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.95.tgz#1914d42041469582e3cc56619890edbcc54e83d6" + integrity sha512-pIhFI+cuC1aYg+0NAPxwT/VRb32f2ia8oGxUjQR6aJg65gLkUYQzdwuUmpMtFR2WVf7WVFYxUnjo4UyMuyh3ng== + +"@swc/core-linux-arm64-musl@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.95.tgz#8d73822a5807575a572d6a2d6cb64587a9f19ce6" + integrity sha512-ZpbTr+QZDT4OPJfjPAmScqdKKaT+wGurvMU5AhxLaf85DuL8HwUwwlL0n1oLieLc47DwIJEMuKQkYhXMqmJHlg== + +"@swc/core-linux-x64-gnu@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.95.tgz#80467727ec11da3de49e6be2abf735964a808483" + integrity sha512-n9SuHEFtdfSJ+sHdNXNRuIOVprB8nbsz+08apKfdo4lEKq6IIPBBAk5kVhPhkjmg2dFVHVo4Tr/OHXM1tzWCCw== + +"@swc/core-linux-x64-musl@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.95.tgz#675a53ac037846bd1bb9840a95ebcb5289265d3b" + integrity sha512-L1JrVlsXU3LC0WwmVnMK9HrOT2uhHahAoPNMJnZQpc18a0paO9fqifPG8M/HjNRffMUXR199G/phJsf326UvVg== + +"@swc/core-win32-arm64-msvc@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.95.tgz#7f0b5d0d0a090c5c625bbc54ffaf427d861c068a" + integrity sha512-YaP4x/aZbUyNdqCBpC2zL8b8n58MEpOUpmOIZK6G1SxGi+2ENht7gs7+iXpWPc0sy7X3YPKmSWMAuui0h8lgAA== + +"@swc/core-win32-ia32-msvc@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.95.tgz#06e2778549a37f0b505b24fd8f40c1c038e29f3e" + integrity sha512-w0u3HI916zT4BC/57gOd+AwAEjXeUlQbGJ9H4p/gzs1zkSHtoDQghVUNy3n/ZKp9KFod/95cA8mbVF9t1+6epQ== + +"@swc/core-win32-x64-msvc@1.3.95": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.95.tgz#40f6b131e84ba6ed97f516edf0f9d5a766c0da64" + integrity sha512-5RGnMt0S6gg4Gc6QtPUJ3Qs9Un4sKqccEzgH/tj7V/DVTJwKdnBKxFZfgQ34OR2Zpz7zGOn889xwsFVXspVWNA== + +"@swc/core@^1.3.85": + version "1.3.95" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.3.95.tgz#2743b8460e6f29385e3dbe49f3f66277ab233536" + integrity sha512-PMrNeuqIusq9DPDooV3FfNEbZuTu5jKAc04N3Hm6Uk2Fl49cqElLFQ4xvl4qDmVDz97n3n/C1RE0/f6WyGPEiA== + dependencies: + "@swc/counter" "^0.1.1" + "@swc/types" "^0.1.5" + optionalDependencies: + "@swc/core-darwin-arm64" "1.3.95" + "@swc/core-darwin-x64" "1.3.95" + "@swc/core-linux-arm-gnueabihf" "1.3.95" + "@swc/core-linux-arm64-gnu" "1.3.95" + "@swc/core-linux-arm64-musl" "1.3.95" + "@swc/core-linux-x64-gnu" "1.3.95" + "@swc/core-linux-x64-musl" "1.3.95" + "@swc/core-win32-arm64-msvc" "1.3.95" + "@swc/core-win32-ia32-msvc" "1.3.95" + "@swc/core-win32-x64-msvc" "1.3.95" + +"@swc/counter@^0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.2.tgz#bf06d0770e47c6f1102270b744e17b934586985e" + integrity sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw== + +"@swc/types@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.5.tgz#043b731d4f56a79b4897a3de1af35e75d56bc63a" + integrity sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw== + +"@types/bn.js@^5.1.1": + version "5.1.3" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.3.tgz#0857f00da3bf888a26a44b4a477c7819b17dacc5" + integrity sha512-wT1B4iIO82ecXkdN6waCK8Ou7E71WU+mP1osDA5Q8c6Ur+ozU2vIKUIhSpUr6uE5L2YHocKS1Z2jG2fBC1YVeg== + dependencies: + "@types/node" "*" + +"@types/chai-subset@^1.3.3": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.4.tgz#7938fa929dd12db451457e4d6faa27bcd599a729" + integrity sha512-CCWNXrJYSUIojZ1149ksLl3AN9cmZ5djf+yUoVVV+NuYrtydItQVlL2ZDqyC6M6O9LWRnVf8yYDxbXHO2TfQZg== + dependencies: + "@types/chai" "*" + +"@types/chai@*", "@types/chai@^4.3.5": + version "4.3.9" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.9.tgz#144d762491967db8c6dea38e03d2206c2623feec" + integrity sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg== + +"@types/chroma-js@^2.4.0": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.4.2.tgz#5c57e9f9ce5343f134e376fb76e07fd3271f150f" + integrity sha512-gbiHvCuBS9aXkE3OEDfS69bscNLTYtbbx2TQf6WyOu+4eCH1AH1gPSiDGF2UzwkRFAbqKNsC5F0mY0xcaEHCbg== + +"@types/eslint@^8.4.5": + version "8.44.6" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.6.tgz#60e564551966dd255f4c01c459f0b4fb87068603" + integrity sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.3.tgz#2be19e759a3dd18c79f9f436bd7363556c1a73dd" + integrity sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ== + +"@types/json-schema@*", "@types/json-schema@^7.0.12": + version "7.0.14" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.14.tgz#74a97a5573980802f32c8e47b663530ab3b6b7d1" + integrity sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/lodash.throttle@^4.1.7": + version "4.1.8" + resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.8.tgz#9d1a630d6e413a32030be1dda040ab913b80d57a" + integrity sha512-EJT8Wg9HLcrsaTlFJ+wmolrGMCC/WBmqOISNi1y9hukgp15cYnfO435X1ReUl0VTIAYnRailHqSZEmzLJb5fiQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*": + version "4.14.200" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149" + integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q== + +"@types/node@*": + version "20.8.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.9.tgz#646390b4fab269abce59c308fc286dcd818a2b08" + integrity sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg== + dependencies: + undici-types "~5.26.4" + +"@types/prop-types@*": + version "15.7.9" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d" + integrity sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g== + +"@types/react-dom@^18.2.14": + version "18.2.14" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.14.tgz#c01ba40e5bb57fc1dc41569bb3ccdb19eab1c539" + integrity sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ== + dependencies: + "@types/react" "*" + +"@types/react-helmet@^6.1.8": + version "6.1.8" + resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.8.tgz#92942afbf620435602de1f500cd9b47d3c09a218" + integrity sha512-UyJFvbGWO8xKvfCPFTt/DG/vsgkMqyXbUQAa1pSPco1Whw85Z3ypMEqoHtCDfoW4Qu8XgJp63jyXEhOa4te5Kw== + dependencies: + "@types/react" "*" + +"@types/react-qr-reader@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@types/react-qr-reader/-/react-qr-reader-2.1.6.tgz#72ebc45530bd40aabfa1aa978c6fb58b674fdee8" + integrity sha512-KF3WXsCUczlVasxLTiXNy0bO3043g/qWyYdklFK2xyZuqVyQZyzAY5Cg0+55DZ1WFmyQoL5eIqWndnlMk6RyWg== + dependencies: + "@types/react" "*" + +"@types/react-scroll@^1.8.9": + version "1.8.9" + resolved "https://registry.yarnpkg.com/@types/react-scroll/-/react-scroll-1.8.9.tgz#690dcde92442e0083027eb7f285f36de372c675d" + integrity sha512-FvfY7wmN7UqwStZFny6riMMnx56rdTBshE3irmoaZPeIEgBRvt1R9/lDNPQ3CVhm5OPJLHKDSvMk4c3JSwoY0g== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18.2.33": + version "18.2.33" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.33.tgz#055356243dc4350a9ee6c6a2c07c5cae12e38877" + integrity sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/scheduler@*": + version "0.16.5" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.5.tgz#4751153abbf8d6199babb345a52e1eb4167d64af" + integrity sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw== + +"@types/semver@^7.5.0": + version "7.5.4" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.4.tgz#0a41252ad431c473158b22f9bfb9a63df7541cff" + integrity sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ== + +"@types/stylis@^4.0.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.2.tgz#baabb6b3aa6787e90a6bd6cd75cd8fb9a4f256a3" + integrity sha512-Rm17MsTpQQP5Jq4BF7CdrxJsDufoiL/q5IbJZYZmOZAJALyijgF7BzLgobXUqraNcQdqFYLYGeglDp6QzaxPpg== + +"@types/trusted-types@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.5.tgz#5cac7e7df3275bb95f79594f192d97da3b4fd5fe" + integrity sha512-I3pkr8j/6tmQtKV/ZzHtuaqYSQvyjGRKH4go60Rr0IDLlFxuRT5V32uvB1mecM5G1EVAUyF/4r4QZ1GHgz+mxA== + +"@typescript-eslint/eslint-plugin@^6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.0.tgz#fdb6f3821c0167e3356e9d89c80e8230b2e401f4" + integrity sha512-lgX7F0azQwRPB7t7WAyeHWVfW1YJ9NIgd9mvGhfQpRY56X6AVf8mwM8Wol+0z4liE7XX3QOt8MN1rUKCfSjRIA== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.9.0" + "@typescript-eslint/type-utils" "6.9.0" + "@typescript-eslint/utils" "6.9.0" + "@typescript-eslint/visitor-keys" "6.9.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.4" + natural-compare "^1.4.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/parser@^6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.9.0.tgz#2b402cadeadd3f211c25820e5433413347b27391" + integrity sha512-GZmjMh4AJ/5gaH4XF2eXA8tMnHWP+Pm1mjQR2QN4Iz+j/zO04b9TOvJYOX2sCNIQHtRStKTxRY1FX7LhpJT4Gw== + dependencies: + "@typescript-eslint/scope-manager" "6.9.0" + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/typescript-estree" "6.9.0" + "@typescript-eslint/visitor-keys" "6.9.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.9.0.tgz#2626e9a7fe0e004c3e25f3b986c75f584431134e" + integrity sha512-1R8A9Mc39n4pCCz9o79qRO31HGNDvC7UhPhv26TovDsWPBDx+Sg3rOZdCELIA3ZmNoWAuxaMOT7aWtGRSYkQxw== + dependencies: + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/visitor-keys" "6.9.0" + +"@typescript-eslint/type-utils@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.9.0.tgz#23923c8c9677c2ad41457cf8e10a5f2946be1b04" + integrity sha512-XXeahmfbpuhVbhSOROIzJ+b13krFmgtc4GlEuu1WBT+RpyGPIA4Y/eGnXzjbDj5gZLzpAXO/sj+IF/x2GtTMjQ== + dependencies: + "@typescript-eslint/typescript-estree" "6.9.0" + "@typescript-eslint/utils" "6.9.0" + debug "^4.3.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/types@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.9.0.tgz#86a0cbe7ac46c0761429f928467ff3d92f841098" + integrity sha512-+KB0lbkpxBkBSiVCuQvduqMJy+I1FyDbdwSpM3IoBS7APl4Bu15lStPjgBIdykdRqQNYqYNMa8Kuidax6phaEw== + +"@typescript-eslint/typescript-estree@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.0.tgz#d0601b245be873d8fe49f3737f93f8662c8693d4" + integrity sha512-NJM2BnJFZBEAbCfBP00zONKXvMqihZCrmwCaik0UhLr0vAgb6oguXxLX1k00oQyD+vZZ+CJn3kocvv2yxm4awQ== + dependencies: + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/visitor-keys" "6.9.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.9.0.tgz#5bdac8604fca4823f090e4268e681c84d3597c9f" + integrity sha512-5Wf+Jsqya7WcCO8me504FBigeQKVLAMPmUzYgDbWchINNh1KJbxCgVya3EQ2MjvJMVeXl3pofRmprqX6mfQkjQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.9.0" + "@typescript-eslint/types" "6.9.0" + "@typescript-eslint/typescript-estree" "6.9.0" + semver "^7.5.4" + +"@typescript-eslint/visitor-keys@6.9.0": + version "6.9.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.0.tgz#cc69421c10c4ac997ed34f453027245988164e80" + integrity sha512-dGtAfqjV6RFOtIP8I0B4ZTBRrlTT8NHHlZZSchQx3qReaoDeXhYM++M4So2AgFK9ZB0emRPA6JI1HkafzA2Ibg== + dependencies: + "@typescript-eslint/types" "6.9.0" + eslint-visitor-keys "^3.4.1" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@vitejs/plugin-react-swc@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react-swc/-/plugin-react-swc-3.4.0.tgz#53ca6a07423abadec92f967e188d5ba49b350830" + integrity sha512-m7UaA4Uvz82N/0EOVpZL4XsFIakRqrFKeSNxa1FBLSXGvWrWRBwmZb4qxk+ZIVAZcW3c3dn5YosomDgx62XWcQ== + dependencies: + "@swc/core" "^1.3.85" + +"@vitest/expect@0.34.6": + version "0.34.6" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.34.6.tgz#608a7b7a9aa3de0919db99b4cc087340a03ea77e" + integrity sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw== + dependencies: + "@vitest/spy" "0.34.6" + "@vitest/utils" "0.34.6" + chai "^4.3.10" + +"@vitest/runner@0.34.6": + version "0.34.6" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.6.tgz#6f43ca241fc96b2edf230db58bcde5b974b8dcaf" + integrity sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ== + dependencies: + "@vitest/utils" "0.34.6" + p-limit "^4.0.0" + pathe "^1.1.1" + +"@vitest/snapshot@0.34.6": + version "0.34.6" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.6.tgz#b4528cf683b60a3e8071cacbcb97d18b9d5e1d8b" + integrity sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w== + dependencies: + magic-string "^0.30.1" + pathe "^1.1.1" + pretty-format "^29.5.0" + +"@vitest/spy@0.34.6": + version "0.34.6" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.6.tgz#b5e8642a84aad12896c915bce9b3cc8cdaf821df" + integrity sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ== + dependencies: + tinyspy "^2.1.1" + +"@vitest/utils@0.34.6": + version "0.34.6" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.6.tgz#38a0a7eedddb8e7291af09a2409cb8a189516968" + integrity sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A== + dependencies: + diff-sequences "^29.4.3" + loupe "^2.3.6" + pretty-format "^29.5.0" + +"@zondax/ledger-substrate@^0.41.3": + version "0.41.3" + resolved "https://registry.yarnpkg.com/@zondax/ledger-substrate/-/ledger-substrate-0.41.3.tgz#04e33a8aa8c589551caf63139653aba4ed7b9219" + integrity sha512-pjsTGODRHP+SG+h4hBkA9NmvHQeplkj48cB5/TUlzRVBZSz7k172Cu70lpGDkVsKDKG6AuCP2pyWKKzPQIzNTA== + dependencies: + "@ledgerhq/hw-transport" "^6.27.1" + bip32 "^4.0.0" + bip32-ed25519 "https://github.com/Zondax/bip32-ed25519" + bip39 "^3.0.4" + blakejs "^1.2.1" + bs58 "^5.0.0" + hash.js "^1.1.7" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^8.2.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.0.tgz#2097665af50fd0cf7a2dfccd2b9368964e66540f" + integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== + +acorn@^8.10.0, acorn@^8.9.0: + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-query@^5.1.3: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + +array-includes@^3.1.6, array-includes@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" + integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-string "^1.0.7" + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== + dependencies: + array-uniq "^1.0.1" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== + +array.prototype.findlastindex@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207" + integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.2.1" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.1, array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz#620eff7442503d66c799d95503f82b475745cefd" + integrity sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.2.1" + +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== + +async@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynciterator.prototype@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz#8c5df0514936cdd133604dfcc9d3fb93f09b2b62" + integrity sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg== + dependencies: + has-symbols "^1.0.3" + +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +axe-core@^4.6.2: + version "4.8.2" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.8.2.tgz#2f6f3cde40935825cf4465e3c1c9e77b240ff6ae" + integrity sha512-/dlp0fxyM3R8YW7MFzaHWXrf4zzbr0vaYb23VBFCl83R7nWNPg/yaQw2Dc8jzCMmDVLhSdzH8MjrsuIUuvX+6g== + +axios@^0.21.4: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + +axobject-query@^3.1.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" + integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== + dependencies: + dequal "^2.0.3" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base-x@^3.0.2: + version "3.0.9" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320" + integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ== + dependencies: + safe-buffer "^5.0.1" + +base-x@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a" + integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +big-integer@^1.6.44: + version "1.6.51" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.51.tgz#0df92a5d9880560d3ff2d5fd20245c889d130686" + integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== + +bignumber.js@^9.1.1, bignumber.js@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +"bip32-ed25519@https://github.com/Zondax/bip32-ed25519": + version "0.0.4" + resolved "https://github.com/Zondax/bip32-ed25519#0949df01b5c93885339bc28116690292088f6134" + dependencies: + bn.js "^5.1.1" + elliptic "^6.4.1" + hash.js "^1.1.7" + +bip32@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/bip32/-/bip32-4.0.0.tgz#7fac3c05072188d2d355a4d6596b37188f06aa2f" + integrity sha512-aOGy88DDlVUhspIXJN+dVEtclhIsfAUppD43V0j40cPTld3pv/0X/MlrZSZ6jowIaQQzFwP8M6rFU2z2mVYjDQ== + dependencies: + "@noble/hashes" "^1.2.0" + "@scure/base" "^1.1.1" + typeforce "^1.11.5" + wif "^2.0.6" + +bip39@^3.0.4: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" + integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A== + dependencies: + "@noble/hashes" "^1.2.0" + +blakejs@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/blakejs/-/blakejs-1.2.1.tgz#5057e4206eadb4a97f7c0b6e197a505042fc3814" + integrity sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ== + +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.1.1, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +bplist-parser@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/bplist-parser/-/bplist-parser-0.2.0.tgz#43a9d183e5bf9d545200ceac3e712f79ebbe8d0e" + integrity sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw== + dependencies: + big-integer "^1.6.44" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browserslist@^4.21.9: + version "4.22.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" + integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== + dependencies: + caniuse-lite "^1.0.30001541" + electron-to-chromium "^1.4.535" + node-releases "^2.0.13" + update-browserslist-db "^1.0.13" + +bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + +bs58@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279" + integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ== + dependencies: + base-x "^4.0.0" + +bs58check@<3.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" + integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== + dependencies: + bs58 "^4.0.0" + create-hash "^1.1.0" + safe-buffer "^5.1.2" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bundle-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-3.0.0.tgz#ba59bcc9ac785fb67ccdbf104a2bf60c099f0e1a" + integrity sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw== + dependencies: + run-applescript "^5.0.0" + +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + +caniuse-lite@^1.0.30001541: + version "1.0.30001555" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001555.tgz#e36f4d49e345337d6788f32093867cec8d951789" + integrity sha512-NzbUFKUnJ3DTcq6YyZB6+qqhfD112uR3uoEnkmfzm2wVzUNsFkU7AwBjKQ654Sp5cau0JxhFyRSn/tQZ+XfygA== + +chai@^4.3.10: + version "4.3.10" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.10.tgz#d784cec635e3b7e2ffb66446a63b4e33bd390384" + integrity sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chart.js@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.0.tgz#df843fdd9ec6bd88d7f07e2b95348d221bd2698c" + integrity sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ== + dependencies: + "@kurkle/color" "^0.3.0" + +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chroma-js@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0" + integrity sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A== + +cipher-base@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +classnames@^2.2.5: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== + +commander@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +confusing-browser-globals@^1.0.10: + version "1.0.11" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" + integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cosmiconfig@^8.1.3: + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== + dependencies: + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + path-type "^4.0.0" + +create-hash@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + +css-to-react-native@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + +csstype@^3.0.2, csstype@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + +date-fns@^2.29.3: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-eql@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== + dependencies: + type-detect "^4.0.0" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +default-browser-id@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-3.0.0.tgz#bee7bbbef1f4e75d31f98f4d3f1556a14cea790c" + integrity sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA== + dependencies: + bplist-parser "^0.2.0" + untildify "^4.0.0" + +default-browser@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-4.0.0.tgz#53c9894f8810bf86696de117a6ce9085a3cbc7da" + integrity sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA== + dependencies: + bundle-name "^3.0.0" + default-browser-id "^3.0.0" + execa "^7.1.1" + titleize "^3.0.0" + +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +diff-sequences@^29.4.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +electron-to-chromium@^1.4.535: + version "1.4.569" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.569.tgz#1298b67727187ffbaac005a7425490d157f3ad03" + integrity sha512-LsrJjZ0IbVy12ApW3gpYpcmHS3iRxH4bkKOW98y1/D+3cvDUWGcbzbsFinfUS8knpcZk/PG/2p/RnkMCYN7PVg== + +elliptic@^6.4.1: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +email-addresses@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-5.0.0.tgz#7ae9e7f58eef7d5e3e2c2c2d3ea49b78dc854fa6" + integrity sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +enhanced-resolve@^5.12.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.22.1: + version "1.22.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.3.tgz#48e79f5573198de6dee3589195727f4f74bc4f32" + integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.2" + available-typed-arrays "^1.0.5" + call-bind "^1.0.5" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.2" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.12" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.13" + +es-iterator-helpers@^1.0.12: + version "1.0.15" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz#bd81d275ac766431d19305923707c3efd9f1ae40" + integrity sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g== + dependencies: + asynciterator.prototype "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.1" + es-abstract "^1.22.1" + es-set-tostringtag "^2.0.1" + function-bind "^1.1.1" + get-intrinsic "^1.2.1" + globalthis "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + iterator.prototype "^1.1.2" + safe-array-concat "^1.0.1" + +es-set-tostringtag@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz#11f7cc9f63376930a5f20be4915834f4bc74f9c9" + integrity sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q== + dependencies: + get-intrinsic "^1.2.2" + has-tostringtag "^1.0.0" + hasown "^2.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +esbuild@^0.18.10: + version "0.18.20" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.18.20.tgz#4709f5a34801b43b799ab7d6d82f7284a9b7a7a6" + integrity sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA== + optionalDependencies: + "@esbuild/android-arm" "0.18.20" + "@esbuild/android-arm64" "0.18.20" + "@esbuild/android-x64" "0.18.20" + "@esbuild/darwin-arm64" "0.18.20" + "@esbuild/darwin-x64" "0.18.20" + "@esbuild/freebsd-arm64" "0.18.20" + "@esbuild/freebsd-x64" "0.18.20" + "@esbuild/linux-arm" "0.18.20" + "@esbuild/linux-arm64" "0.18.20" + "@esbuild/linux-ia32" "0.18.20" + "@esbuild/linux-loong64" "0.18.20" + "@esbuild/linux-mips64el" "0.18.20" + "@esbuild/linux-ppc64" "0.18.20" + "@esbuild/linux-riscv64" "0.18.20" + "@esbuild/linux-s390x" "0.18.20" + "@esbuild/linux-x64" "0.18.20" + "@esbuild/netbsd-x64" "0.18.20" + "@esbuild/openbsd-x64" "0.18.20" + "@esbuild/sunos-x64" "0.18.20" + "@esbuild/win32-arm64" "0.18.20" + "@esbuild/win32-ia32" "0.18.20" + "@esbuild/win32-x64" "0.18.20" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-airbnb-base@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-15.0.0.tgz#6b09add90ac79c2f8d723a2580e07f3925afd236" + integrity sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig== + dependencies: + confusing-browser-globals "^1.0.10" + object.assign "^4.1.2" + object.entries "^1.1.5" + semver "^6.3.0" + +eslint-config-airbnb-typescript@^17.1.0: + version "17.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz#fda960eee4a510f092a9a1c139035ac588937ddc" + integrity sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig== + dependencies: + eslint-config-airbnb-base "^15.0.0" + +eslint-config-airbnb@^19.0.4: + version "19.0.4" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz#84d4c3490ad70a0ffa571138ebcdea6ab085fdc3" + integrity sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew== + dependencies: + eslint-config-airbnb-base "^15.0.0" + object.assign "^4.1.2" + object.entries "^1.1.5" + +eslint-config-prettier@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f" + integrity sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw== + +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-import-resolver-typescript@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" + integrity sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.12.0" + eslint-module-utils "^2.7.4" + fast-glob "^3.3.1" + get-tsconfig "^4.5.0" + is-core-module "^2.11.0" + is-glob "^4.0.3" + +eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz#8133232e4329ee344f2f612885ac3073b0b7e155" + integrity sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.14.2" + +eslint-plugin-jsx-a11y@^6.7.1: + version "6.7.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976" + integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== + dependencies: + "@babel/runtime" "^7.20.7" + aria-query "^5.1.3" + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" + ast-types-flow "^0.0.7" + axe-core "^4.6.2" + axobject-query "^3.1.1" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + has "^1.0.3" + jsx-ast-utils "^3.3.3" + language-tags "=1.0.5" + minimatch "^3.1.2" + object.entries "^1.1.6" + object.fromentries "^2.0.6" + semver "^6.3.0" + +eslint-plugin-prefer-arrow-functions@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-prefer-arrow-functions/-/eslint-plugin-prefer-arrow-functions-3.2.4.tgz#7bfbaab9646d1bb7cd7e29a58cfb796548c69508" + integrity sha512-HbPmlbO/iYQeVs2fuShNkGVJDfVfgSd84Vzxv+xlh+nIVoSsZvTj6yOqszw4mtG9JbiqMShVWqbVeoVsejE59w== + +eslint-plugin-prefer-arrow@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.3.tgz#e7fbb3fa4cd84ff1015b9c51ad86550e55041041" + integrity sha512-J9I5PKCOJretVuiZRGvPQxCbllxGAV/viI20JO3LYblAodofBxyMnZAJ+WGeClHgANnSJberTNoFWWjrWKBuXQ== + +eslint-plugin-prettier@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz#a3b399f04378f79f066379f544e42d6b73f11515" + integrity sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.8.5" + +eslint-plugin-react@^7.33.2: + version "7.33.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz#69ee09443ffc583927eafe86ffebb470ee737608" + integrity sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw== + dependencies: + array-includes "^3.1.6" + array.prototype.flatmap "^1.3.1" + array.prototype.tosorted "^1.1.1" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.12" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.6" + object.fromentries "^2.0.6" + object.hasown "^1.1.2" + object.values "^1.1.6" + prop-types "^15.8.1" + resolve "^2.0.0-next.4" + semver "^6.3.1" + string.prototype.matchall "^4.0.8" + +eslint-plugin-unused-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.0.0.tgz#d25175b0072ff16a91892c3aa72a09ca3a9e69e7" + integrity sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw== + dependencies: + eslint-rule-composer "^0.3.0" + +eslint-rule-composer@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz#79320c927b0c5c0d3d3d2b76c8b4a488f25bbaf9" + integrity sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg== + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.52.0: + version "8.52.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.52.0.tgz#d0cd4a1fac06427a61ef9242b9353f36ea7062fc" + integrity sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.2" + "@eslint/js" "8.52.0" + "@humanwhocodes/config-array" "^0.11.13" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^2.0.1, estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +execa@^7.1.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-7.2.0.tgz#657e75ba984f42a70f38928cedc87d6f2d4fe4e9" + integrity sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.1" + human-signals "^4.3.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^3.0.7" + strip-final-newline "^3.0.0" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +filename-reserved-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz#abf73dfab735d045440abfea2d91f389ebbfa229" + integrity sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ== + +filenamify@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-4.3.0.tgz#62391cb58f02b09971c9d4f9d63b3cf9aba03106" + integrity sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg== + dependencies: + filename-reserved-regex "^2.0.0" + strip-outer "^1.0.1" + trim-repeated "^1.0.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.1.tgz#a02a15fdec25a8f844ff7cc658f03dd99eb4609b" + integrity sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.2.9" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== + +follow-redirects@^1.14.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + +framer-motion@^10.15.0, framer-motion@^10.16.3: + version "10.16.4" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-10.16.4.tgz#30279ef5499b8d85db3a298ee25c83429933e9f8" + integrity sha512-p9V9nGomS3m6/CALXqv6nFGMuFOxbWsmaOrdmhyQimMIlLl3LC7h7l86wge/Js/8cRu5ktutS/zlzgR7eBOtFA== + dependencies: + tslib "^2.4.0" + optionalDependencies: + "@emotion/is-prop-valid" "^0.8.2" + +fs-extra@^11.1.0, fs-extra@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" + integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.1, function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^6.0.0, get-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +get-tsconfig@^4.5.0: + version "4.7.2" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.2.tgz#0dcd6fb330391d46332f4c6c1bf89a6514c2ddce" + integrity sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A== + dependencies: + resolve-pkg-maps "^1.0.0" + +gh-pages@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/gh-pages/-/gh-pages-6.0.0.tgz#3bb46ea13dc7cee306662db0d3f02bf05635cdc1" + integrity sha512-FXZWJRsvP/fK2HJGY+Di6FRNHvqFF6gOIELaopDjXXgjeOYSNURcuYwEO/6bwuq6koP5Lnkvnr5GViXzuOB89g== + dependencies: + async "^3.2.4" + commander "^11.0.0" + email-addresses "^5.0.0" + filenamify "^4.3.0" + find-cache-dir "^3.3.1" + fs-extra "^11.1.1" + globby "^6.1.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.0.3, glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.23.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.23.0.tgz#ef31673c926a0976e1f61dab4dca57e0c0a8af02" + integrity sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw== + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== + dependencies: + get-intrinsic "^1.2.2" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.4.tgz#2eb2860e000011dae4f1406a86fe80e530fb2ec6" + integrity sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ== + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +human-signals@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" + integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== + +i18next-browser-languagedetector@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.1.0.tgz#01876fac51f86b78975e79b48ccb62e2313a2d7d" + integrity sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA== + dependencies: + "@babel/runtime" "^7.19.4" + +i18next@^23.6.0: + version "23.6.0" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.6.0.tgz#c6e996cfd3fef0bf60be3b7c581c35338dba5a71" + integrity sha512-z0Cxr0MGkt+kli306WS4nNNM++9cgt2b2VCMprY92j+AIab/oclgPxdwtTZVLP1zn5t5uo8M6uLsZmYrcjr3HA== + dependencies: + "@babel/runtime" "^7.22.5" + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0, ignore@^5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +immutable@^4.0.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" + integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== + +import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +internal-slot@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" + integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg== + dependencies: + get-intrinsic "^1.2.2" + hasown "^2.0.0" + side-channel "^1.0.4" + +is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-map@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127" + integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.2.tgz#90755fa4c2562dc1c5d4024760d6119b94ca18ec" + integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-weakset@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" + integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json-stringify-safe@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonc-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsqr@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jsqr/-/jsqr-1.4.0.tgz#8efb8d0a7cc6863cb6d95116b9069123ce9eb2d1" + integrity sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A== + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +language-subtag-registry@~0.3.2: + version "0.3.22" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" + integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== + +language-tags@=1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" + integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== + dependencies: + language-subtag-registry "~0.3.2" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +lit-element@^3.3.0: + version "3.3.3" + resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-3.3.3.tgz#10bc19702b96ef5416cf7a70177255bfb17b3209" + integrity sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.1.0" + "@lit/reactive-element" "^1.3.0" + lit-html "^2.8.0" + +lit-html@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.8.0.tgz#96456a4bb4ee717b9a7d2f94562a16509d39bffa" + integrity sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q== + dependencies: + "@types/trusted-types" "^2.0.2" + +lit@^2.7.5: + version "2.8.0" + resolved "https://registry.yarnpkg.com/lit/-/lit-2.8.0.tgz#4d838ae03059bf9cafa06e5c61d8acc0081e974e" + integrity sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA== + dependencies: + "@lit/reactive-element" "^1.6.0" + lit-element "^3.3.0" + lit-html "^2.8.0" + +local-pkg@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963" + integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.pick@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loupe@^2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magic-string@^0.30.1: + version "0.30.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" + integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mlly@^1.2.0, mlly@^1.4.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e" + integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg== + dependencies: + acorn "^8.10.0" + pathe "^1.1.1" + pkg-types "^1.0.3" + ufo "^1.3.0" + +mock-socket@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.3.1.tgz#24fb00c2f573c84812aa4a24181bb025de80cc8e" + integrity sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +nock@^13.3.4: + version "13.3.6" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.6.tgz#b279968ec8d076c2393810a6c9bf2d4d5b3a1071" + integrity sha512-lT6YuktKroUFM+27mubf2uqQZVy2Jf+pfGzuh9N6VwdHlFoZqvi4zyxFTVR1w/ChPqGY6yxGehHp6C3wqCASCw== + dependencies: + debug "^4.1.0" + json-stringify-safe "^5.0.1" + propagate "^2.0.0" + +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + +node-fetch@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b" + integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA== + dependencies: + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" + +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +npm-run-path@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" + integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q== + dependencies: + path-key "^4.0.0" + +object-assign@^4.0.1, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1, object-inspect@^1.9.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.2, object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.5, object.entries@^1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" + integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.fromentries@^2.0.6, object.fromentries@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" + integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.groupby@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee" + integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + +object.hasown@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.3.tgz#6a5f2897bb4d3668b8e79364f98ccf971bda55ae" + integrity sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA== + dependencies: + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.values@^1.1.6, object.values@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" + integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + +open@^8.4.0: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +open@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/open/-/open-9.1.0.tgz#684934359c90ad25742f5a26151970ff8c6c80b6" + integrity sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg== + dependencies: + default-browser "^4.0.0" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^2.2.0" + +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-limit@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-4.0.0.tgz#914af6544ed32bfa54670b061cafcbd04984b644" + integrity sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ== + dependencies: + yocto-queue "^1.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pathe@^1.1.0, pathe@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.1.tgz#1dd31d382b974ba69809adc9a7a347e65d84829a" + integrity sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== + +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-types@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" + integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A== + dependencies: + jsonc-parser "^3.2.0" + mlly "^1.2.0" + pathe "^1.1.0" + +postcss-value-parser@^4.0.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.27, postcss@^8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier-plugin-organize-imports@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.3.tgz#6b0141ac71f7ee9a673ce83e95456319e3a7cf0d" + integrity sha512-KFvk8C/zGyvUaE3RvxN2MhCLwzV6OBbFSkwZ2OamCrs9ZY4i5L77jQ/w4UmUr+lqX8qbaqVq6bZZkApn+IgJSg== + +prettier@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" + integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== + +pretty-format@^29.5.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +propagate@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45" + integrity sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag== + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +qrcode-generator@1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7" + integrity sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +rc-slider@^10.3.1: + version "10.3.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.3.1.tgz#345e818975f4bb61b66340799af8cfccad7c8ad7" + integrity sha512-XszsZLkbjcG9ogQy/zUC0n2kndoKUAnY/Vnk1Go5Gx+JJQBz0Tl15d5IfSiglwBUZPS9vsUJZkfCmkIZSqWbcA== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.27.0" + +rc-util@^5.27.0: + version "5.38.0" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.38.0.tgz#18a3d1c26ba3c43fabfbe6303e825dabd9e5f4f0" + integrity sha512-yV/YBNdFn+edyBpBdCqkPE29Su0jWcHNgwx2dJbRqMrMfrUcMJUjCRV+ZPhcvWyKFJ63GzEerPrz9JIVo0zXmA== + dependencies: + "@babel/runtime" "^7.18.3" + react-is "^18.2.0" + +react-chartjs-2@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz#43c1e3549071c00a1a083ecbd26c1ad34d385f5d" + integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA== + +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-error-boundary@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c" + integrity sha512-U13ul67aP5DOSPNSCWQ/eO0AQEYzEFkVljULQIjMV0KlffTAhxuDoBKdO0pb/JZ8mDhMKFZ9NZi0BmLGUiNphw== + dependencies: + "@babel/runtime" "^7.12.5" + +react-fast-compare@^3.1.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + +react-helmet@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" + integrity sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw== + dependencies: + object-assign "^4.1.1" + prop-types "^15.7.2" + react-fast-compare "^3.1.1" + react-side-effect "^2.1.0" + +react-i18next@^13.3.1: + version "13.3.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-13.3.1.tgz#9b072bf4dd4cafb028e92315a8a1415f8034bdca" + integrity sha512-JAtYREK879JXaN9GdzfBI4yJeo/XyLeXWUsRABvYXiFUakhZJ40l+kaTo+i+A/3cKIED41kS/HAbZ5BzFtq/Og== + dependencies: + "@babel/runtime" "^7.22.5" + html-parse-stringify "^3.0.1" + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^18.0.0, react-is@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-qr-reader@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-2.2.1.tgz#dc89046d1c1a1da837a683dd970de5926817d55b" + integrity sha512-EL5JEj53u2yAOgtpAKAVBzD/SiKWn0Bl7AZy6ZrSf1lub7xHwtaXe6XSx36Wbhl1VMGmvmrwYMRwO1aSCT2fwA== + dependencies: + jsqr "^1.2.0" + prop-types "^15.7.2" + webrtc-adapter "^7.2.1" + +react-router-dom@^6.17.0: + version "6.17.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.17.0.tgz#ea73f89186546c1cf72b10fcb7356d874321b2ad" + integrity sha512-qWHkkbXQX+6li0COUUPKAUkxjNNqPJuiBd27dVwQGDNsuFBdMbrS6UZ0CLYc4CsbdLYTckn4oB4tGDuPZpPhaQ== + dependencies: + "@remix-run/router" "1.10.0" + react-router "6.17.0" + +react-router@6.17.0: + version "6.17.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.17.0.tgz#7b680c4cefbc425b57537eb9c73bedecbdc67c1e" + integrity sha512-YJR3OTJzi3zhqeJYADHANCGPUu9J+6fT5GLv82UWRGSxu6oJYCKVmxUcaBQuGm9udpWmPsvpme/CdHumqgsoaA== + dependencies: + "@remix-run/router" "1.10.0" + +react-scroll@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/react-scroll/-/react-scroll-1.9.0.tgz#2984006e184afd0e4018f837d127edf5fa8f152c" + integrity sha512-mamNcaX9Ng+JeSbBu97nWwRhYvL2oba+xR2GxvyXsbDeGP+gkYIKZ+aDMMj/n20TbV9SCWm/H7nyuNTSiXA6yA== + dependencies: + lodash.throttle "^4.1.1" + prop-types "^15.7.2" + +react-side-effect@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.2.tgz#dc6345b9e8f9906dc2eeb68700b615e0b4fe752a" + integrity sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw== + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +readable-stream@^3.6.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +reflect.getprototypeof@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz#aaccbf41aca3821b87bb71d9dcbc7ad0ba50a3f3" + integrity sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + +regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.4: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rollup-plugin-visualizer@^5.9.2: + version "5.9.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.2.tgz#f1aa2d9b1be8ebd6869223c742324897464d8891" + integrity sha512-waHktD5mlWrYFrhOLbti4YgQCn1uR24nYsNuXxg7LkPH8KdTXVWR9DNY1WU0QqokyMixVXJS4J04HNrVTMP01A== + dependencies: + open "^8.4.0" + picomatch "^2.3.1" + source-map "^0.7.4" + yargs "^17.5.1" + +rollup@^2.77.2: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + +rollup@^3.27.1: + version "3.29.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981" + integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw== + optionalDependencies: + fsevents "~2.3.2" + +rtcpeerconnection-shim@^1.2.15: + version "1.2.15" + resolved "https://registry.yarnpkg.com/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz#e7cc189a81b435324c4949aa3dfb51888684b243" + integrity sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw== + dependencies: + sdp "^2.6.0" + +run-applescript@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-5.0.0.tgz#e11e1c932e055d5c6b40d98374e0268d9b11899c" + integrity sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg== + dependencies: + execa "^5.0.0" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@6: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +safe-array-concat@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +sass@^1.69.5: + version "1.69.5" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.5.tgz#23e18d1c757a35f2e52cc81871060b9ad653dfde" + integrity sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +sdp@^2.12.0, sdp@^2.6.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.12.0.tgz#338a106af7560c86e4523f858349680350d53b22" + integrity sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw== + +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.4, semver@^7.3.5, semver@^7.5.0, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +set-function-name@^2.0.0, set-function-name@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + +sha.js@^2.4.0: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallowequal@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + +signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +smoldot@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/smoldot/-/smoldot-2.0.1.tgz#c899cbb0827a010d3ca7944034f081786f533a4d" + integrity sha512-Wqw2fL/sELQByLSeeTX1Z/d0H4McmphPMx8vh6UZS/bIIDx81oU7s/drmx2iL/ME36uk++YxpRuJey8/MOyfOA== + dependencies: + ws "^8.8.1" + +smoldot@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/smoldot/-/smoldot-2.0.6.tgz#9e24cb4ab2a308c639cf4db1e95daf7066f319eb" + integrity sha512-/FQ6urmnG1TQuIu15NB9lXeNS0MX5f57vkX4RoBuVA+gAq4bcF1k2nMb40tfKCBW9PKyfEf478DN1YUMVOlWjg== + dependencies: + ws "^8.8.1" + +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + +std-env@^3.3.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" + integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.matchall@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" + integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + regexp.prototype.flags "^1.5.0" + set-function-name "^2.0.0" + side-channel "^1.0.4" + +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-literal@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.3.0.tgz#db3942c2ec1699e6836ad230090b84bb458e3a07" + integrity sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg== + dependencies: + acorn "^8.10.0" + +strip-outer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== + dependencies: + escape-string-regexp "^1.0.2" + +styled-components@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.0.tgz#228e3ab9c1ee1daa4b0a06aae30df0ed14fda274" + integrity sha512-VWNfYYBuXzuLS/QYEeoPgMErP26WL+dX9//rEh80B2mmlS1yRxRxuL5eax4m6ybYEUoHWlTy2XOU32767mlMkg== + dependencies: + "@emotion/is-prop-valid" "^1.2.1" + "@emotion/unitless" "^0.8.0" + "@types/stylis" "^4.0.2" + css-to-react-native "^3.2.0" + csstype "^3.1.2" + postcss "^8.4.31" + shallowequal "^1.1.0" + stylis "^4.3.0" + tslib "^2.5.0" + +stylis@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.0.tgz#abe305a669fc3d8777e10eefcfc73ad861c5588c" + integrity sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-parser@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +synckit@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.5.tgz#b7f4358f9bb559437f9f167eb6bc46b3c9818fa3" + integrity sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q== + dependencies: + "@pkgr/utils" "^2.3.1" + tslib "^2.5.0" + +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +tiny-invariant@^1.1.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + +tinybench@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.1.tgz#3408f6552125e53a5a48adee31261686fd71587e" + integrity sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg== + +tinypool@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.7.0.tgz#88053cc99b4a594382af23190c609d93fddf8021" + integrity sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww== + +tinyspy@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.0.tgz#9dc04b072746520b432f77ea2c2d17933de5d6ce" + integrity sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg== + +titleize@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/titleize/-/titleize-3.0.0.tgz#71c12eb7fdd2558aa8a44b0be83b8a76694acd53" + integrity sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + integrity sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg== + dependencies: + escape-string-regexp "^1.0.2" + +ts-api-utils@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" + integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== + +tsconfck@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-2.1.2.tgz#f667035874fa41d908c1fe4d765345fcb1df6e35" + integrity sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg== + +tsconfig-paths@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" + integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^1.9.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@^4.0.0, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + +typeforce@^1.11.5: + version "1.18.0" + resolved "https://registry.yarnpkg.com/typeforce/-/typeforce-1.18.0.tgz#d7416a2c5845e085034d70fcc5b6cc4a90edbfdc" + integrity sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g== + +typescript@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== + +ufo@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.1.tgz#e085842f4627c41d4c1b60ebea1f75cdab4ce86b" + integrity sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +vite-bundle-visualizer@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/vite-bundle-visualizer/-/vite-bundle-visualizer-0.10.0.tgz#bdeafe5f8e69eb4c157174ae8d852279272e3010" + integrity sha512-11AwKlkhvw6jjiGbTiCZqBSGg/FQDLc0mVcoLWVov2jU/Ban67l+Sk4Fa0Iyctb5sObqg/dA28HkKCEmSRjw9g== + dependencies: + cac "^6.7.14" + rollup-plugin-visualizer "^5.9.2" + +vite-node@0.34.6: + version "0.34.6" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.6.tgz#34d19795de1498562bf21541a58edcd106328a17" + integrity sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA== + dependencies: + cac "^6.7.14" + debug "^4.3.4" + mlly "^1.4.0" + pathe "^1.1.1" + picocolors "^1.0.0" + vite "^3.0.0 || ^4.0.0 || ^5.0.0-0" + +vite-plugin-checker@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/vite-plugin-checker/-/vite-plugin-checker-0.6.2.tgz#3790381734440033e6cb3cee9d92fcfdd69a4d71" + integrity sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ== + dependencies: + "@babel/code-frame" "^7.12.13" + ansi-escapes "^4.3.0" + chalk "^4.1.1" + chokidar "^3.5.1" + commander "^8.0.0" + fast-glob "^3.2.7" + fs-extra "^11.1.0" + lodash.debounce "^4.0.8" + lodash.pick "^4.4.0" + npm-run-path "^4.0.1" + semver "^7.5.0" + strip-ansi "^6.0.0" + tiny-invariant "^1.1.0" + vscode-languageclient "^7.0.0" + vscode-languageserver "^7.0.0" + vscode-languageserver-textdocument "^1.0.1" + vscode-uri "^3.0.2" + +vite-plugin-eslint@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz#0381b8272e7f0fd8b663311b64f7608d55d8b04c" + integrity sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang== + dependencies: + "@rollup/pluginutils" "^4.2.1" + "@types/eslint" "^8.4.5" + rollup "^2.77.2" + +vite-plugin-svgr@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vite-plugin-svgr/-/vite-plugin-svgr-4.1.0.tgz#f11072a873856039702bb66657379c53d3bb5d5a" + integrity sha512-v7Qic+FWmCChgQNGSI4V8X63OEYsdUoLt66iqIcHozq9bfK/Dwmr0V+LBy1NE8CE98Y8HouEBJ+pto4AMfN5xw== + dependencies: + "@rollup/pluginutils" "^5.0.4" + "@svgr/core" "^8.1.0" + "@svgr/plugin-jsx" "^8.1.0" + +vite-tsconfig-paths@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.1.tgz#e53b89096b91d31a6d1e26f75999ea8c336a89ed" + integrity sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^2.1.0" + +"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^4.4.11: + version "4.5.0" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26" + integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw== + dependencies: + esbuild "^0.18.10" + postcss "^8.4.27" + rollup "^3.27.1" + optionalDependencies: + fsevents "~2.3.2" + +vitest@^0.34.5: + version "0.34.6" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.34.6.tgz#44880feeeef493c04b7f795ed268f24a543250d7" + integrity sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q== + dependencies: + "@types/chai" "^4.3.5" + "@types/chai-subset" "^1.3.3" + "@types/node" "*" + "@vitest/expect" "0.34.6" + "@vitest/runner" "0.34.6" + "@vitest/snapshot" "0.34.6" + "@vitest/spy" "0.34.6" + "@vitest/utils" "0.34.6" + acorn "^8.9.0" + acorn-walk "^8.2.0" + cac "^6.7.14" + chai "^4.3.10" + debug "^4.3.4" + local-pkg "^0.4.3" + magic-string "^0.30.1" + pathe "^1.1.1" + picocolors "^1.0.0" + std-env "^3.3.3" + strip-literal "^1.0.1" + tinybench "^2.5.0" + tinypool "^0.7.0" + vite "^3.1.0 || ^4.0.0 || ^5.0.0-0" + vite-node "0.34.6" + why-is-node-running "^2.2.2" + +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + +vscode-jsonrpc@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== + +vscode-languageclient@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" + integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== + dependencies: + minimatch "^3.0.4" + semver "^7.3.4" + vscode-languageserver-protocol "3.16.0" + +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== + dependencies: + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" + +vscode-languageserver-textdocument@^1.0.1: + version "1.0.11" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" + integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== + +vscode-languageserver-types@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== + +vscode-languageserver@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" + integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== + dependencies: + vscode-languageserver-protocol "3.16.0" + +vscode-uri@^3.0.2: + version "3.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" + integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== + +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== + +webrtc-adapter@^7.2.1: + version "7.7.1" + resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-7.7.1.tgz#b2c227a6144983b35057df67bd984a7d4bfd17f1" + integrity sha512-TbrbBmiQBL9n0/5bvDdORc6ZfRY/Z7JnEj+EYOD1ghseZdpJ+nF2yx14k3LgQKc7JZnG7HAcL+zHnY25So9d7A== + dependencies: + rtcpeerconnection-shim "^1.2.15" + sdp "^2.12.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +which-collection@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + +which-typed-array@^1.1.11, which-typed-array@^1.1.13, which-typed-array@^1.1.9: + version "1.1.13" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.4" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +why-is-node-running@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e" + integrity sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + +wif@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/wif/-/wif-2.0.6.tgz#08d3f52056c66679299726fade0d432ae74b4704" + integrity sha512-HIanZn1zmduSF+BQhkE+YXIbEiH0xPr1012QbFEGB0xsKqJii0/SqJjyn8dFv6y36kOznMgMB+LGcbZTJ1xACQ== + dependencies: + bs58check "<3.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.14.1, ws@^8.8.1: + version "8.14.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.5.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yocto-queue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== From 74b7dec96a4d1ae2601dad6cb62c75ebc4c8c6ea Mon Sep 17 00:00:00 2001 From: Raid Ateir <ateirraid@gmail.com> Date: Wed, 14 Feb 2024 18:34:54 +0100 Subject: [PATCH 2/6] Minor Changes --- src/consts.ts | 2 +- src/types/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/consts.ts b/src/consts.ts index acf05a88c7..95c7f0156e 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -10,7 +10,7 @@ import type { Plugin } from 'types'; */ export const AppVersion = '1.0.8'; export const DappName = 'Cere Staking Dashboard'; -export const PolkadotUrl = 'https://cere.network'; +export const CereUrl = 'https://cere.network'; export const DefaultNetwork = 'Cere Mainnet'; export const ManualSigners = ['ledger', 'vault']; /* diff --git a/src/types/index.ts b/src/types/index.ts index 28586009d0..38dc7c8098 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -100,7 +100,7 @@ export type MaybeAddress = string | null; export type MaybeString = string | null; // list of available plugins. -export type Plugin = 'subscan' | 'binance_spot' | 'tips' | 'polkawatch' | 'cerestats'; // ToDo +export type Plugin = 'subscan' | 'binance_spot' | 'tips' | 'polkawatch' | 'cereStats'; // ToDo // track the status of a syncing / fetching process. export type Sync = 'unsynced' | 'syncing' | 'synced'; From e174b5cedaa758b863ead734f2819c19b8a23edf Mon Sep 17 00:00:00 2001 From: Raid Ateir <ateirraid@gmail.com> Date: Thu, 15 Feb 2024 10:52:37 +0100 Subject: [PATCH 3/6] fix build errors --- index.html | 2 +- src/Utils.ts | 4 +- src/Wrappers.tsx | 458 ------------ src/config/extensions/icons/dot_icon.svg | 1 - src/config/extensions/icons/enkrypt_icon.svg | 9 - src/config/extensions/icons/nova_wallet.svg | 13 - src/config/extensions/icons/polkadot_js.svg | 1 - src/config/extensions/icons/signer_icon.svg | 3 - .../extensions/icons/subwallet_icon.svg | 82 --- src/config/extensions/icons/talisman_icon.svg | 1 - src/config/extensions/index.ts | 49 -- src/config/ledger.ts | 8 +- src/config/networks.ts | 2 +- src/config/validators/index.ts | 214 ------ src/contexts/Account/defaults.ts | 2 +- src/contexts/Account/index.tsx | 4 +- src/contexts/Account/types.ts | 2 +- src/contexts/CereStats/defaults.ts | 5 +- src/contexts/CereStats/index.tsx | 30 +- src/contexts/CereStats/types.ts | 12 +- .../Connect/Hooks/useImportExtension.tsx | 115 --- src/contexts/Connect/defaults.ts | 28 - src/contexts/Connect/index.tsx | 490 ------------- src/contexts/Connect/types.ts | 38 - src/contexts/Extensions/defaults.ts | 16 - src/contexts/Extensions/index.tsx | 82 --- src/contexts/Extensions/types.ts | 31 - src/contexts/Modal/defaults.ts | 20 - src/contexts/Modal/index.tsx | 96 --- src/contexts/Modal/types.ts | 20 - src/contexts/Network/defaults.ts | 4 +- src/contexts/Overlay/defaults.tsx | 2 +- src/contexts/Overlay/index.tsx | 2 +- src/contexts/Overlay/types.ts | 4 +- src/contexts/SessionEra/defaults.ts | 17 - src/contexts/SessionEra/index.tsx | 93 --- src/contexts/SessionEra/types.ts | 15 - src/contexts/Subscan/defaults.ts | 11 - src/contexts/Subscan/index.tsx | 221 ------ src/contexts/Subscan/types.ts | 10 - src/contexts/Tips/defaults.ts | 2 +- src/contexts/Tips/index.tsx | 4 +- src/contexts/Tips/types.ts | 2 +- src/contexts/TxFees/defaults.ts | 16 - src/contexts/TxFees/index.tsx | 74 -- src/contexts/Validators/defaults.ts | 37 - src/contexts/Validators/index.tsx | 669 ------------------ src/index.tsx | 19 - src/library/Account/index.tsx | 90 --- src/library/Button/index.tsx | 97 --- src/library/Button/types.ts | 23 - src/library/Filter/Category.tsx | 2 +- src/library/Filter/context.tsx | 389 ---------- src/library/Form/AccountDropdown/Wrappers.ts | 124 ---- src/library/Form/AccountDropdown/index.tsx | 151 ---- src/library/Form/AccountSelect/Wrappers.ts | 121 ---- src/library/Form/AccountSelect/index.tsx | 138 ---- src/library/Form/Dropdown/index.tsx | 106 --- .../Form/Utils/getEligibleControllers.tsx | 76 -- src/library/Graphs/Bonded.tsx | 125 ---- src/library/Graphs/StatBoxPie.tsx | 77 -- src/library/Graphs/Wrappers.ts | 284 -------- src/library/Headers/Dropdown.tsx | 2 +- src/library/Hooks/useInflation/index.tsx | 20 +- src/library/Identicon/index.tsx | 25 - src/library/Identicon/types.ts | 7 - src/library/Loaders/Announcement.tsx | 36 - src/library/Loaders/DataList.tsx | 32 - src/library/Loaders/Stake.tsx | 137 ---- src/library/OpenHelpIcon/Wrapper.tsx | 31 - src/library/OpenHelpIcon/index.tsx | 30 - src/library/OpenHelpIcon/types.ts | 8 - src/library/Overlay/Title.tsx | 42 -- src/library/Overlay/Wrappers.tsx | 163 ----- src/library/Overlay/index.tsx | 26 - src/library/PageTitle/index.tsx | 84 --- src/library/PageTitle/types.ts | 11 - src/library/PoolAccount/Wrapper.ts | 74 -- src/library/PoolAccount/index.tsx | 94 --- src/library/PoolAccount/types.ts | 19 - src/library/PoolList/FilterPools.tsx | 61 -- src/library/PoolList/Filters.tsx | 85 --- src/library/PoolList/index.tsx | 276 -------- src/library/SubscanButton/index.tsx | 49 -- src/library/SubscanButton/types.ts | 14 - src/library/Tips/Items/Dismiss.tsx | 45 -- src/library/Tips/Items/Tip.tsx | 36 - src/library/ValidatorList/Filters.tsx | 105 --- .../ValidatorList/Validator/Default.tsx | 155 ---- .../ValidatorList/Validator/Nomination.tsx | 88 --- src/library/ValidatorList/Validator/Utils.tsx | 56 -- src/library/ValidatorList/Validator/index.tsx | 32 - src/library/ValidatorList/Validator/types.ts | 23 - src/modals/Connect/Ledger.tsx | 4 +- src/modals/ConnectAccounts/Account.tsx | 96 --- src/modals/ConnectAccounts/Accounts.tsx | 296 -------- src/modals/ConnectAccounts/Extension.tsx | 132 ---- src/modals/ConnectAccounts/Extensions.tsx | 64 -- .../ConnectAccounts/ReadOnly/Wrapper.ts | 74 -- src/modals/ConnectAccounts/ReadOnly/index.tsx | 94 --- .../ConnectAccounts/ReadOnlyInput/Wrapper.ts | 47 -- .../ConnectAccounts/ReadOnlyInput/index.tsx | 126 ---- src/modals/ConnectAccounts/Wrappers.ts | 314 -------- src/modals/ConnectAccounts/index.tsx | 90 --- src/modals/ConnectAccounts/types.ts | 62 -- src/modals/LeavePool/Wrapper.ts | 8 - src/modals/LeavePool/index.tsx | 18 - src/modals/ManagePool/Forms.tsx | 256 ------- src/modals/Nominate/index.tsx | 130 ---- src/modals/NominateFromFavorites/Wrappers.ts | 39 - src/modals/NominateFromFavorites/index.tsx | 201 ------ src/modals/NominatePool/index.tsx | 120 ---- src/modals/SelectFavorites/Wrappers.ts | 39 - src/modals/SelectFavorites/index.tsx | 107 --- src/modals/UpdateBond/Forms/BondAll.tsx | 155 ---- src/modals/UpdateBond/Forms/BondSome.tsx | 148 ---- src/modals/UpdateBond/Forms/FormFooter.tsx | 45 -- src/modals/UpdateBond/Forms/UnbondAll.tsx | 167 ----- .../UpdateBond/Forms/UnbondPoolToMinimum.tsx | 134 ---- src/modals/UpdateBond/Forms/UnbondSome.tsx | 170 ----- src/modals/UpdateBond/Forms/index.tsx | 23 - src/modals/UpdateBond/Tasks.tsx | 132 ---- src/modals/UpdateBond/Wrappers.ts | 101 --- src/modals/UpdateBond/index.tsx | 88 --- src/modals/UpdateBond/types.ts | 8 - src/modals/ValidatorMetrics/index.tsx | 6 +- src/modals/Wrappers.ts | 188 ----- src/modals/index.tsx | 152 ---- src/pages/Favorites/index.tsx | 61 -- src/pages/Feedback/Wrappers.ts | 12 - src/pages/Feedback/index.tsx | 63 -- .../Nominate/Active/Controller/Wrapper.ts | 46 -- .../Nominate/Active/Controller/index.tsx | 60 -- .../Nominate/Active/ControllerNotImported.tsx | 49 -- .../Nominate/Active/Nominations/Wrapper.tsx | 23 - .../Nominate/Active/Nominations/index.tsx | 194 ----- .../Active/Stats/ActiveNominations.tsx | 36 - .../Active/Stats/InactiveNominations.tsx | 42 -- .../Active/Stats/MinimumActiveBond.tsx | 25 - .../Nominate/Active/Stats/SupplyStaked.tsx | 50 -- src/pages/Nominate/Active/Status.tsx | 168 ----- src/pages/Nominate/Setup/Payee/Wrappers.tsx | 64 -- src/pages/Nominate/Setup/SetController.tsx | 108 --- src/pages/Overview/ActiveAccount.tsx | 78 -- src/pages/Overview/BalanceGraph.tsx | 215 ------ src/pages/Overview/NetworkSats/Inflation.tsx | 75 -- src/pages/Overview/Reserve.tsx | 49 -- src/pages/Overview/Stats/ActiveEra.tsx | 39 - src/pages/Overview/Stats/ActiveNominators.tsx | 44 -- src/pages/Overview/Stats/TotalNominations.tsx | 42 -- src/pages/Overview/Tips/Items.tsx | 167 ----- src/pages/Overview/Tips/Syncing.tsx | 59 -- src/pages/Overview/Tips/Wrappers.tsx | 195 ----- src/pages/Overview/Tips/index.tsx | 244 ------- src/pages/Pools/Home/MembersList/index.tsx | 210 ------ src/pages/Pools/Home/PoolStats/Header.tsx | 71 -- .../Pools/Home/Status/Membership/Wrapper.ts | 55 -- .../Pools/Home/Status/Membership/index.tsx | 104 --- src/reportWebVitals.ts | 18 - src/theme/default.ts | 153 ---- src/theme/index.ts | 261 ------- src/types/index.ts | 14 +- src/types/styles.ts | 2 +- webpack.config.js | 40 -- yarn.lock | 163 ++++- 165 files changed, 232 insertions(+), 13610 deletions(-) delete mode 100644 src/Wrappers.tsx delete mode 100644 src/config/extensions/icons/dot_icon.svg delete mode 100644 src/config/extensions/icons/enkrypt_icon.svg delete mode 100644 src/config/extensions/icons/nova_wallet.svg delete mode 100644 src/config/extensions/icons/polkadot_js.svg delete mode 100644 src/config/extensions/icons/signer_icon.svg delete mode 100644 src/config/extensions/icons/subwallet_icon.svg delete mode 100644 src/config/extensions/icons/talisman_icon.svg delete mode 100644 src/config/extensions/index.ts delete mode 100644 src/config/validators/index.ts delete mode 100644 src/contexts/Connect/Hooks/useImportExtension.tsx delete mode 100644 src/contexts/Connect/defaults.ts delete mode 100644 src/contexts/Connect/index.tsx delete mode 100644 src/contexts/Connect/types.ts delete mode 100644 src/contexts/Extensions/defaults.ts delete mode 100644 src/contexts/Extensions/index.tsx delete mode 100644 src/contexts/Extensions/types.ts delete mode 100644 src/contexts/Modal/defaults.ts delete mode 100644 src/contexts/Modal/index.tsx delete mode 100644 src/contexts/Modal/types.ts delete mode 100644 src/contexts/SessionEra/defaults.ts delete mode 100644 src/contexts/SessionEra/index.tsx delete mode 100644 src/contexts/SessionEra/types.ts delete mode 100644 src/contexts/Subscan/defaults.ts delete mode 100644 src/contexts/Subscan/index.tsx delete mode 100644 src/contexts/Subscan/types.ts delete mode 100644 src/contexts/TxFees/defaults.ts delete mode 100644 src/contexts/TxFees/index.tsx delete mode 100644 src/contexts/Validators/defaults.ts delete mode 100644 src/contexts/Validators/index.tsx delete mode 100644 src/index.tsx delete mode 100644 src/library/Account/index.tsx delete mode 100644 src/library/Button/index.tsx delete mode 100644 src/library/Button/types.ts delete mode 100644 src/library/Filter/context.tsx delete mode 100644 src/library/Form/AccountDropdown/Wrappers.ts delete mode 100644 src/library/Form/AccountDropdown/index.tsx delete mode 100644 src/library/Form/AccountSelect/Wrappers.ts delete mode 100644 src/library/Form/AccountSelect/index.tsx delete mode 100644 src/library/Form/Dropdown/index.tsx delete mode 100644 src/library/Form/Utils/getEligibleControllers.tsx delete mode 100644 src/library/Graphs/Bonded.tsx delete mode 100644 src/library/Graphs/StatBoxPie.tsx delete mode 100644 src/library/Graphs/Wrappers.ts delete mode 100644 src/library/Identicon/index.tsx delete mode 100644 src/library/Identicon/types.ts delete mode 100644 src/library/Loaders/Announcement.tsx delete mode 100644 src/library/Loaders/DataList.tsx delete mode 100644 src/library/Loaders/Stake.tsx delete mode 100644 src/library/OpenHelpIcon/Wrapper.tsx delete mode 100644 src/library/OpenHelpIcon/index.tsx delete mode 100644 src/library/OpenHelpIcon/types.ts delete mode 100644 src/library/Overlay/Title.tsx delete mode 100644 src/library/Overlay/Wrappers.tsx delete mode 100644 src/library/Overlay/index.tsx delete mode 100644 src/library/PageTitle/index.tsx delete mode 100644 src/library/PageTitle/types.ts delete mode 100644 src/library/PoolAccount/Wrapper.ts delete mode 100644 src/library/PoolAccount/index.tsx delete mode 100644 src/library/PoolAccount/types.ts delete mode 100644 src/library/PoolList/FilterPools.tsx delete mode 100644 src/library/PoolList/Filters.tsx delete mode 100644 src/library/PoolList/index.tsx delete mode 100644 src/library/SubscanButton/index.tsx delete mode 100644 src/library/SubscanButton/types.ts delete mode 100644 src/library/Tips/Items/Dismiss.tsx delete mode 100644 src/library/Tips/Items/Tip.tsx delete mode 100644 src/library/ValidatorList/Filters.tsx delete mode 100644 src/library/ValidatorList/Validator/Default.tsx delete mode 100644 src/library/ValidatorList/Validator/Nomination.tsx delete mode 100644 src/library/ValidatorList/Validator/Utils.tsx delete mode 100644 src/library/ValidatorList/Validator/index.tsx delete mode 100644 src/library/ValidatorList/Validator/types.ts delete mode 100644 src/modals/ConnectAccounts/Account.tsx delete mode 100644 src/modals/ConnectAccounts/Accounts.tsx delete mode 100644 src/modals/ConnectAccounts/Extension.tsx delete mode 100644 src/modals/ConnectAccounts/Extensions.tsx delete mode 100644 src/modals/ConnectAccounts/ReadOnly/Wrapper.ts delete mode 100644 src/modals/ConnectAccounts/ReadOnly/index.tsx delete mode 100644 src/modals/ConnectAccounts/ReadOnlyInput/Wrapper.ts delete mode 100644 src/modals/ConnectAccounts/ReadOnlyInput/index.tsx delete mode 100644 src/modals/ConnectAccounts/Wrappers.ts delete mode 100644 src/modals/ConnectAccounts/index.tsx delete mode 100644 src/modals/ConnectAccounts/types.ts delete mode 100644 src/modals/LeavePool/Wrapper.ts delete mode 100644 src/modals/LeavePool/index.tsx delete mode 100644 src/modals/ManagePool/Forms.tsx delete mode 100644 src/modals/Nominate/index.tsx delete mode 100644 src/modals/NominateFromFavorites/Wrappers.ts delete mode 100644 src/modals/NominateFromFavorites/index.tsx delete mode 100644 src/modals/NominatePool/index.tsx delete mode 100644 src/modals/SelectFavorites/Wrappers.ts delete mode 100644 src/modals/SelectFavorites/index.tsx delete mode 100644 src/modals/UpdateBond/Forms/BondAll.tsx delete mode 100644 src/modals/UpdateBond/Forms/BondSome.tsx delete mode 100644 src/modals/UpdateBond/Forms/FormFooter.tsx delete mode 100644 src/modals/UpdateBond/Forms/UnbondAll.tsx delete mode 100644 src/modals/UpdateBond/Forms/UnbondPoolToMinimum.tsx delete mode 100644 src/modals/UpdateBond/Forms/UnbondSome.tsx delete mode 100644 src/modals/UpdateBond/Forms/index.tsx delete mode 100644 src/modals/UpdateBond/Tasks.tsx delete mode 100644 src/modals/UpdateBond/Wrappers.ts delete mode 100644 src/modals/UpdateBond/index.tsx delete mode 100644 src/modals/UpdateBond/types.ts delete mode 100644 src/modals/Wrappers.ts delete mode 100644 src/modals/index.tsx delete mode 100644 src/pages/Favorites/index.tsx delete mode 100644 src/pages/Feedback/Wrappers.ts delete mode 100644 src/pages/Feedback/index.tsx delete mode 100644 src/pages/Nominate/Active/Controller/Wrapper.ts delete mode 100644 src/pages/Nominate/Active/Controller/index.tsx delete mode 100644 src/pages/Nominate/Active/ControllerNotImported.tsx delete mode 100644 src/pages/Nominate/Active/Nominations/Wrapper.tsx delete mode 100644 src/pages/Nominate/Active/Nominations/index.tsx delete mode 100644 src/pages/Nominate/Active/Stats/ActiveNominations.tsx delete mode 100644 src/pages/Nominate/Active/Stats/InactiveNominations.tsx delete mode 100644 src/pages/Nominate/Active/Stats/MinimumActiveBond.tsx delete mode 100644 src/pages/Nominate/Active/Stats/SupplyStaked.tsx delete mode 100644 src/pages/Nominate/Active/Status.tsx delete mode 100644 src/pages/Nominate/Setup/Payee/Wrappers.tsx delete mode 100644 src/pages/Nominate/Setup/SetController.tsx delete mode 100644 src/pages/Overview/ActiveAccount.tsx delete mode 100644 src/pages/Overview/BalanceGraph.tsx delete mode 100644 src/pages/Overview/NetworkSats/Inflation.tsx delete mode 100644 src/pages/Overview/Reserve.tsx delete mode 100644 src/pages/Overview/Stats/ActiveEra.tsx delete mode 100644 src/pages/Overview/Stats/ActiveNominators.tsx delete mode 100644 src/pages/Overview/Stats/TotalNominations.tsx delete mode 100644 src/pages/Overview/Tips/Items.tsx delete mode 100644 src/pages/Overview/Tips/Syncing.tsx delete mode 100644 src/pages/Overview/Tips/Wrappers.tsx delete mode 100644 src/pages/Overview/Tips/index.tsx delete mode 100644 src/pages/Pools/Home/MembersList/index.tsx delete mode 100644 src/pages/Pools/Home/PoolStats/Header.tsx delete mode 100644 src/pages/Pools/Home/Status/Membership/Wrapper.ts delete mode 100644 src/pages/Pools/Home/Status/Membership/index.tsx delete mode 100644 src/reportWebVitals.ts delete mode 100644 src/theme/default.ts delete mode 100644 src/theme/index.ts delete mode 100644 webpack.config.js diff --git a/index.html b/index.html index 09726b6ee2..d5cb01207c 100644 --- a/index.html +++ b/index.html @@ -81,4 +81,4 @@ <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> -</html> +</html> \ No newline at end of file diff --git a/src/Utils.ts b/src/Utils.ts index 059ce67f22..6309e02861 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -4,8 +4,8 @@ import { decodeAddress, encodeAddress } from '@polkadot/keyring'; import { hexToU8a, isHex, u8aToString, u8aUnwrapBytes } from '@polkadot/util'; import BN from 'bn.js'; -import { MutableRefObject } from 'react'; -import { AnyMetaBatch } from 'types/index'; +import type { MutableRefObject } from 'react'; +import type { AnyMetaBatch } from 'types/index'; export const clipAddress = (val: string) => { if (typeof val !== 'string') { diff --git a/src/Wrappers.tsx b/src/Wrappers.tsx deleted file mode 100644 index ed186553b6..0000000000 --- a/src/Wrappers.tsx +++ /dev/null @@ -1,458 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { - InterfaceMaximumWidth, - ShowAccountsButtonWidthThreshold, - SideMenuMaximisedWidth, - SideMenuMinimisedWidth, - SideMenuStickyThreshold, -} from 'consts'; -import { motion } from 'framer-motion'; -import styled from 'styled-components'; -import { - backgroundGradient, - backgroundPrimary, - borderPrimary, - buttonSecondaryBackground, - textPrimary, - textSecondary, -} from 'theme'; -import { - InterfaceLayoutProps, - PageRowWrapperProps, - PageTitleWrapperProps, - SideInterfaceWrapperProps, -} from 'types/styles'; - -/* EntryWrapper - * - * Highest level app component. - * Provides global styling for headers and other global - * classes used throughout the app and possibly the library. - */ -export const EntryWrapper = styled.div` - background: ${backgroundGradient}; - width: 100%; - background-attachment: fixed; - display: flex; - flex-flow: column nowrap; - min-height: 100vh; - flex-grow: 1; - - h1 { - color: ${textPrimary}; - } - h2 { - color: ${textPrimary}; - } - h3 { - color: ${textPrimary}; - } - h4 { - color: ${textPrimary}; - } - h5 { - color: ${textPrimary}; - } - p { - color: ${textSecondary}; - } - a { - color: ${textSecondary}; - } - input { - color: ${textPrimary}; - } - - path.primary { - fill: ${textPrimary}; - } - - ellipse.primary { - fill: ${textPrimary}; - } - - input:focus, - textarea:focus, - select:focus { - outline: none; - } - - input { - border: none; - padding: 0.7rem 0rem; - font-size: 1.1rem; - background: none; - transition: all 0.1s; - } - - input::placeholder { - color: #aaa; - } - - .textbox, - .textbox:focus { - border-bottom: 1px solid #ddd; - } - - .searchbox, - .searchbox:focus { - border: 1px solid #ddd; - } - - .page-padding { - padding-left: 1.25rem; - padding-right: 1.25rem; - - @media (min-width: ${ShowAccountsButtonWidthThreshold + 1}px) { - padding-left: 2.25rem; - padding-right: 2.25rem; - } - @media (min-width: ${SideMenuStickyThreshold + 1}px) { - padding: 0 5rem 0 2.5rem; - } - @media (min-width: 1500px) { - padding: 0 5rem 0 2.5rem; - } - } -`; - -/* BodyInterfaceWrapper - * - * An element that houses SideInterface and MainInterface. - * Used once in Router. - */ -export const BodyInterfaceWrapper = styled.div` - display: flex; - flex-flow: row nowrap; - position: relative; - flex-grow: 1; -`; - -/* SideInterfaceWrapper - * - * An element that houses the side menu and handles resizing - * on smaller screens. - * Used once in Router. - */ -export const SideInterfaceWrapper = styled.div<SideInterfaceWrapperProps>` - height: 100vh; - display: flex; - flex-flow: column nowrap; - position: sticky; - top: 0px; - z-index: 7; - flex: 0; - overflow: hidden; - min-width: ${(props) => - props.minimised - ? `${SideMenuMinimisedWidth}px` - : `${SideMenuMaximisedWidth}px`}; - max-width: ${(props) => - props.minimised - ? `${SideMenuMinimisedWidth}px` - : `${SideMenuMaximisedWidth}px`}; - transition: all 0.5s cubic-bezier(0.1, 1, 0.2, 1); - - @media (max-width: ${SideMenuStickyThreshold}px) { - position: fixed; - top: 0; - left: ${(props) => (props.open ? 0 : `-${SideMenuMaximisedWidth}px`)}; - } -`; - -/* MainInterfaceWrapper - * - * A column flex wrapper that hosts the main page content. - * Used once in Router. - */ -export const MainInterfaceWrapper = styled.div` - flex: 1; - display: flex; - flex-flow: column nowrap; - position: relative; -`; - -/* PageWrapper - * - * A motion.div that wraps every page. - * Transitions can be applied to this wrapper that will - * affect the entire page. - */ -export const PageWrapper = styled(motion.div)` - max-width: ${InterfaceMaximumWidth}px; - display: flex; - flex-flow: column nowrap; - padding-bottom: 4.5rem; - width: 100%; - margin: 0 auto; -`; - -/* PageTitleWrapper - * - * The element that wraps a page title. Determines the padding - * and position relative to top of screen when the element - * is stuck. - */ -export const PageTitleWrapper = styled.header<PageTitleWrapperProps>` - background: ${backgroundPrimary}; - position: sticky; - top: 0px; - padding-top: ${(props) => (props.sticky ? '1.5rem' : '0.5rem')}; - margin-top: 4rem; - margin-bottom: 0.25rem; - padding-bottom: ${(props) => (props.sticky ? '0.25rem' : 0)}; - width: 100%; - z-index: 5; - display: flex; - flex-flow: column wrap; - justify-content: flex-end; - transition: padding 0.3s ease-out; - - @media (max-width: ${SideMenuStickyThreshold}px) { - top: 4rem; - padding-top: 0.75rem; - padding-bottom: 0.5rem; - } - - .title { - display: flex; - flex-flow: row wrap; - align-items: center; - width: 100%; - margin-bottom: ${(props) => (props.sticky ? '0.75rem ' : 0)}; - - > div { - &:last-child { - padding-left: 1rem; - flex-grow: 1; - } - } - - button { - color: ${textSecondary}; - border: 1px solid ${borderPrimary}; - padding: 0.5rem 0.75rem; - margin: 0; - border-radius: 0.75rem; - font-size: 1.1rem; - - &:hover { - background: ${buttonSecondaryBackground}; - } - - .icon { - margin-left: 0.75rem; - } - } - } - - h1 { - font-family: 'Unbounded', 'sans-serif', sans-serif; - font-size: ${(props) => (props.sticky ? '1.4rem ' : '1.75rem')}; - @media (max-width: ${SideMenuStickyThreshold}px) { - font-size: 1.5rem; - } - transition: font 0.5s; - margin: 0; - } - - .tabs { - overflow: hidden; - max-width: ${InterfaceMaximumWidth}px; - transition: margin 0.2s; - height: 3.6rem; - border-bottom: ${(props) => (props.sticky ? '0px' : '1px solid')}; - border-bottom-color: ${borderPrimary}; - - margin-top: ${(props) => (props.sticky ? '0.5rem' : '0.9rem')}; - @media (max-width: ${SideMenuStickyThreshold}px) { - margin-top: 0.5rem; - } - - > .scroll { - width: 100%; - height: 4.5rem; - overflow-x: auto; - overflow-y: hidden; - } - - .inner { - display: flex; - flex-flow: row nowrap; - - > button { - padding: 0.65rem 1rem; - margin-bottom: 0.5rem; - margin-right: 0.75rem; - font-size: ${(props) => (props.sticky ? '1.05rem' : '1.15rem')}; - color: ${textSecondary}; - transition: opacity 0.1s, font-size 0.1s; - border-radius: 0.5rem; - - &.active { - background: ${buttonSecondaryBackground}; - } - &:last-child { - margin-right: 0; - } - &:hover { - opacity: 0.8; - } - } - } - } -`; - -/* MenuPaddingWrapper - * - * A fixed block that is used to hide scrollable content - * on smaller screens when a PageTitle is fixed. - * Purely cosmetic. Applied in Pagetitle. - */ -export const MenuPaddingWrapper = styled.div` - background: ${backgroundPrimary}; - position: fixed; - top: 0px; - width: 100%; - height: 4rem; - z-index: 4; - display: none; - @media (max-width: ${SideMenuStickyThreshold}px) { - display: block; - } -`; - -/* PageRowWrapper - * - * Used to separate page content based on rows. - * Commonly used with RowPrimaryWrapper and RowSecondaryWrapper. - */ -export const PageRowWrapper = styled.div<PageRowWrapperProps>` - margin-top: ${(props) => (props.noVerticalSpacer === true ? '0' : '1rem')}; - margin-bottom: ${(props) => (props.noVerticalSpacer === true ? '0' : '1rem')}; - display: flex; - flex-shrink: 0; - flex-flow: row wrap; - width: 100%; - /* kill heading padding, already applied to wrapper */ - h1, - h2, - h3, - h4 { - margin-top: 0; - } -`; - -/* RowPrimaryWrapper - * - * The primary module in a PageRow. - */ -export const RowPrimaryWrapper = styled.div<InterfaceLayoutProps>` - order: ${(props) => props.vOrder}; - flex: 1; - flex-basis: 100%; - max-width: 100%; - - @media (min-width: ${(props) => props.thresholdStickyMenu + 1}px) { - ${(props) => props.hOrder === 0 && ' padding-right: 0.75rem;'} - ${(props) => props.hOrder === 1 && 'padding-left: 0.75rem;'} - order: ${(props) => props.hOrder}; - flex: 1; - flex-basis: 56%; - width: 56%; - max-width: ${(props) => (props.maxWidth ? props.maxWidth : 'none')}; - } - - @media (min-width: ${(props) => props.thresholdFullWidth + 400}px) { - flex-basis: 62%; - width: 62%; - max-width: ${(props) => (props.maxWidth ? props.maxWidth : 'none')}; - } -`; - -/* RowSecondaryWrapper - * - * The secondary module in a PageRow. - */ -export const RowSecondaryWrapper = styled.div<InterfaceLayoutProps>` - order: ${(props) => props.vOrder}; - flex-basis: 100%; - width: 100%; - border-radius: 1rem; - - @media (min-width: ${(props) => props.thresholdStickyMenu + 1}px) { - ${(props) => props.hOrder === 1 && ' padding-left: 0.75rem;'} - ${(props) => props.hOrder === 0 && 'padding-right: 0.75rem;'} - order: ${(props) => props.hOrder}; - flex: 1; - flex-basis: 44%; - width: 44%; - max-width: ${(props) => (props.maxWidth ? props.maxWidth : 'none')}; - } - - @media (min-width: ${(props) => props.thresholdFullWidth + 400}px) { - flex-basis: 38%; - max-width: ${(props) => (props.maxWidth ? props.maxWidth : '38%')}; - } -`; - -/* Separator - * - * A horizontal spacer with a bottom border. - * General spacer for separating content by row. - */ -export const Separator = styled.div` - border-bottom: 1px solid ${borderPrimary}; - width: 100%; - margin: 0.75rem 0; -`; - -/* TopBarWrapper - * - * Positioned under titles for a Go Back button and other page header info. - */ -export const TopBarWrapper = styled.div` - display: flex; - flex-flow: row wrap; - align-items: center; - border-bottom: 1px solid ${borderPrimary}; - padding-top: 0.75rem; - padding-bottom: 0.75rem; - width: 100%; - margin-bottom: 0.25rem; - - > span { - margin-right: 1rem; - } - - h3 { - color: ${textSecondary}; - font-size: 1.15rem; - margin: 0.25rem 0; - min-height: 2rem; - } - - .right { - flex: 1 1 0%; - display: flex; - flex-flow: row wrap; - justify-content: flex-end; - - button { - margin: 0 0 0 1rem; - } - } -`; - -/* ButtonRowWrapper - * - * A flex container for a row of buttons - */ -export const ButtonRowWrapper = styled.div<{ verticalSpacing?: boolean }>` - display: flex; - align-items: center; - justify-content: flex-start; - margin-top: ${(props) => (props.verticalSpacing ? '1rem' : 0)}; -`; diff --git a/src/config/extensions/icons/dot_icon.svg b/src/config/extensions/icons/dot_icon.svg deleted file mode 100644 index be7a14a54d..0000000000 --- a/src/config/extensions/icons/dot_icon.svg +++ /dev/null @@ -1 +0,0 @@ -<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 372.78 498.62"><defs><style>.cls-2{fill:#e6007a;}</style></defs><path class="dark" d="M260.4,6.38C156.25,6.8,71.92,91.12,71.51,195.27a193.86,193.86,0,0,0,10,61.36,27,27,0,0,0,33.23,16.55,26.88,26.88,0,0,0,16-33.11,124.53,124.53,0,0,1-7.35-48.15A136.26,136.26,0,1,1,267.87,332s-26.51,1.61-39.7,3.23a134.73,134.73,0,0,0-14.45,2.87,1.75,1.75,0,0,1-2.47,0l0,0a1.77,1.77,0,0,1,0-2.12l4.11-22.39,24.9-112a25.89,25.89,0,0,0-50.64-10.84s-59.23,274.13-59.23,276.63a24.9,24.9,0,0,0,18,30.26,3.48,3.48,0,0,0,.44.1h1.38a24.89,24.89,0,0,0,30.32-17.87,6.77,6.77,0,0,0,.16-.68,6.42,6.42,0,0,1,0-1.24c.73-3.22,8.2-39.7,8.2-39.7a67.19,67.19,0,0,1,55.62-52.89c5.72-.88,29.75-2.49,29.75-2.49C378,372.57,453.7,280.08,443.34,176.33A188.78,188.78,0,0,0,260.4,6.38Z" transform="translate(-71.51 -6.38)"/><path class="cls-2" d="M271.85,441.92a31.47,31.47,0,0,0-37.23,24.4c0,.16-.06.32-.1.48a31.36,31.36,0,0,0,24,37.31.07.07,0,0,1,.06,0h.87a30.86,30.86,0,0,0,37.24-22.75c0-.12.06-.26.1-.38v-1.73A32.64,32.64,0,0,0,271.85,441.92Z" transform="translate(-71.51 -6.38)"/></svg> \ No newline at end of file diff --git a/src/config/extensions/icons/enkrypt_icon.svg b/src/config/extensions/icons/enkrypt_icon.svg deleted file mode 100644 index f114a68d83..0000000000 --- a/src/config/extensions/icons/enkrypt_icon.svg +++ /dev/null @@ -1,9 +0,0 @@ -<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M0 6.15778C0 2.75671 2.75698 0 6.15778 0H32V4.72485C32 7.16928 30.0186 9.15072 27.5741 9.15072H15.4242C12.0231 9.15072 9.26641 11.9077 9.26641 15.3088V16.8756C9.26641 20.2767 12.0231 23.0333 15.4242 23.0333H27.5741C30.0186 23.0333 32 25.015 32 27.4595V32.0003H6.15778C2.75698 32.0003 0 29.243 0 25.8421V6.15778ZM15.6901 11.7259H28.1513C30.277 11.7259 32 13.4491 32 15.5746V16.6097C32 18.7352 30.277 20.4585 28.1513 20.4585H15.6901C13.5644 20.4585 11.8412 18.7352 11.8412 16.6097V15.5746C11.8412 13.4491 13.5644 11.7259 15.6901 11.7259Z" fill="url(#paint0_radial_102_36)"/> -<defs> -<radialGradient id="paint0_radial_102_36" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(0.888265 -0.888331) rotate(55.378) scale(45.3387 181.167)"> -<stop offset="0.051483" stop-color="#C549FF"/> -<stop offset="0.815643" stop-color="#704BFF"/> -</radialGradient> -</defs> -</svg> diff --git a/src/config/extensions/icons/nova_wallet.svg b/src/config/extensions/icons/nova_wallet.svg deleted file mode 100644 index f806807c85..0000000000 --- a/src/config/extensions/icons/nova_wallet.svg +++ /dev/null @@ -1,13 +0,0 @@ -<svg width="145" height="145" viewBox="0 0 145 145" fill="none" xmlns="http://www.w3.org/2000/svg"> - <path d="M0 40C0 17.9086 17.9086 0 40 0H105C127.091 0 145 17.9086 145 40V105C145 127.091 127.091 145 105 145H40C17.9086 145 0 127.091 0 105V40Z" fill="url(#paint0_linear_1014_14373)" /> - <path d="M71.515 24.6219C71.7083 23.5186 73.2917 23.5186 73.485 24.6219L79.265 57.6109C79.9918 61.7596 83.2404 65.0082 87.3891 65.7351L120.378 71.515C121.481 71.7083 121.481 73.2917 120.378 73.485L87.3891 79.265C83.2404 79.9918 79.9918 83.2404 79.265 87.3891L73.485 120.378C73.2917 121.481 71.7083 121.481 71.515 120.378L65.7351 87.3891C65.0082 83.2404 61.7596 79.9918 57.6109 79.265L24.6219 73.485C23.5186 73.2917 23.5186 71.7083 24.6219 71.515L57.6109 65.7351C61.7596 65.0082 65.0082 61.7596 65.7351 57.6109L71.515 24.6219Z" fill="white" /> - <defs> - <linearGradient id="paint0_linear_1014_14373" x1="138.302" y1="-63.9403" x2="-1.11118e-05" y2="205.228" gradientUnits="userSpaceOnUse"> - <stop offset="0.205836" /> - <stop offset="0.369792" stop-color="#541E7E" /> - <stop offset="0.47142" stop-color="#3F51D1" /> - <stop offset="0.609119" stop-color="#73AFE3" /> - <stop offset="0.801091" stop-color="#90D7FF" /> - </linearGradient> - </defs> -</svg> \ No newline at end of file diff --git a/src/config/extensions/icons/polkadot_js.svg b/src/config/extensions/icons/polkadot_js.svg deleted file mode 100644 index f2d53e64ea..0000000000 --- a/src/config/extensions/icons/polkadot_js.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 106.2 106.2"><defs><style>.cls-2{fill:#fff}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><circle cx="53.1" cy="53.1" r="53.1" fill="#f29235"/><path class="cls-2" d="M54.47 13.76a28.85 28.85 0 00-28.73 28.73 29.34 29.34 0 001.52 9.34 4 4 0 107.49-2.52A18.67 18.67 0 0133.63 42a20.72 20.72 0 1122 21.31s-4 .25-6 .49c-.74.11-1.48.26-2.2.44a.28.28 0 01-.38 0 .27.27 0 010-.32l.63-3.41 3.79-17a3.94 3.94 0 10-7.71-1.65s-9 41.7-9 42.08a3.79 3.79 0 002.74 4.6h.28a3.78 3.78 0 004.61-2.71.43.43 0 000-.11v-.19c.11-.49 1.25-6 1.25-6a10.23 10.23 0 018.46-8c.87-.13 4.53-.38 4.53-.38a28.71 28.71 0 00-2.11-57.27z"/><path class="cls-2" d="M56.21 80a4.78 4.78 0 00-5.66 3.71.24.24 0 010 .08 4.77 4.77 0 003.65 5.67h.14A4.7 4.7 0 0060 86v-.32A5 5 0 0056.21 80z"/></g></g></svg> \ No newline at end of file diff --git a/src/config/extensions/icons/signer_icon.svg b/src/config/extensions/icons/signer_icon.svg deleted file mode 100644 index 8bdc41c91b..0000000000 --- a/src/config/extensions/icons/signer_icon.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M78.7744 0L0 49.2384L10.352 65.7824L32.1856 52.2784V41.6384H49.2832L89.3152 16.8192L78.7744 0ZM92.0672 21.2736L59.1712 41.6992H96.016L102.349 37.8208L92.0672 21.2768V21.2736ZM105.158 42.16L96.7712 47.2768V56.8384H81.3504L59.952 70.1472H94.8576L114.848 57.7472L105.158 42.16V42.16ZM34.6432 56.8832L13.12 70.2624L23.3984 86.7264L32.272 81.232V70.1472H50.0896L71.488 56.8832H34.6432V56.8832ZM117.603 62.1568L96.8896 75.04V85.344H80.2048L38.6624 111.098L49.2224 128L128 78.7616L117.6 62.1536L117.603 62.1568ZM35.4464 85.344L26.1728 91.104L35.9328 106.701L70.3264 85.344H35.4464V85.344Z" className="primary" /> -</svg> diff --git a/src/config/extensions/icons/subwallet_icon.svg b/src/config/extensions/icons/subwallet_icon.svg deleted file mode 100644 index 69d0334c9f..0000000000 --- a/src/config/extensions/icons/subwallet_icon.svg +++ /dev/null @@ -1,82 +0,0 @@ -<svg width="134" height="134" viewBox="0 0 134 134" fill="none" xmlns="http://www.w3.org/2000/svg"> -<mask id="mask0_699_5101" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="134" height="134"> -<rect width="134" height="134" fill="#C4C4C4"/> -</mask> -<g mask="url(#mask0_699_5101)"> -<path d="M87.9615 64.3201L87.9456 47.7455L27.1191 16.2236V64.3041L66.0589 85.106L80.2884 78.8367L37.4403 56.1046L37.4722 37.887L87.9615 64.3201Z" fill="url(#paint0_linear_699_5101)"/> -<path d="M50.7607 44.8421V50.5052L37.3926 56.2321L37.4883 37.6636L50.7607 44.8421Z" fill="url(#paint1_linear_699_5101)"/> -<path d="M50.8095 91.822L80.2895 78.8368L37.4414 56.2163L50.6819 50.5054L105.765 79.2835L50.9212 103.212L50.8095 91.822Z" fill="url(#paint2_linear_699_5101)"/> -<path d="M37.4886 87.9773L50.6493 82.2982L50.9365 103.196L105.765 79.2832V97.118L37.377 127.077L37.4886 87.9773Z" fill="url(#paint3_linear_699_5101)"/> -<path d="M27.1191 82.5857L37.4403 87.9776L37.3765 127.013L27.1191 121.86V82.5857Z" fill="url(#paint4_linear_699_5101)"/> -<path d="M40.1522 76.7791L50.6489 82.2986L37.4403 87.9776L27.1191 82.5857L40.1522 76.7791Z" fill="url(#paint5_linear_699_5101)"/> -<path d="M105.765 56.5993L105.702 39.9131L87.9785 47.7457V64.3362L105.765 56.5993Z" fill="url(#paint6_linear_699_5101)"/> -<path d="M27.1191 16.2237L45.0337 7.97632L105.732 39.8811L87.9775 47.7456L27.1191 16.2237Z" fill="url(#paint7_linear_699_5101)"/> -</g> -<defs> -<linearGradient id="paint0_linear_699_5101" x1="11.9006" y1="50.6648" x2="119.372" y2="50.6648" gradientUnits="userSpaceOnUse"> -<stop stop-color="#FFD4B2"/> -<stop offset="0.36" stop-color="#9ACEB7"/> -<stop offset="0.67" stop-color="#47C8BB"/> -<stop offset="0.89" stop-color="#14C5BE"/> -<stop offset="1" stop-color="#00C4BF"/> -</linearGradient> -<linearGradient id="paint1_linear_699_5101" x1="44.0766" y1="62.8524" x2="44.0766" y2="21.2167" gradientUnits="userSpaceOnUse"> -<stop stop-color="#00FECF"/> -<stop offset="0.08" stop-color="#00E5D0"/> -<stop offset="0.24" stop-color="#00A5D1"/> -<stop offset="0.48" stop-color="#0040D4"/> -<stop offset="0.54" stop-color="#0025D5"/> -<stop offset="1"/> -</linearGradient> -<linearGradient id="paint2_linear_699_5101" x1="37.4414" y1="76.8587" x2="146.891" y2="76.8587" gradientUnits="userSpaceOnUse"> -<stop stop-color="#FDEC9F"/> -<stop offset="0.08" stop-color="#E4D8A4"/> -<stop offset="0.24" stop-color="#A4A6B2"/> -<stop offset="0.47" stop-color="#3F57C8"/> -<stop offset="0.61" stop-color="#0025D5"/> -<stop offset="1"/> -</linearGradient> -<linearGradient id="paint3_linear_699_5101" x1="15.0596" y1="103.18" x2="155.01" y2="103.18" gradientUnits="userSpaceOnUse"> -<stop offset="0.05" stop-color="#62A5FF"/> -<stop offset="0.45" stop-color="#1032D1"/> -<stop offset="1"/> -</linearGradient> -<linearGradient id="paint4_linear_699_5101" x1="628.741" y1="3244.93" x2="797.782" y2="3247.12" gradientUnits="userSpaceOnUse"> -<stop stop-color="#FFD4B2"/> -<stop offset="0.36" stop-color="#9ACEB7"/> -<stop offset="0.67" stop-color="#47C8BB"/> -<stop offset="0.89" stop-color="#14C5BE"/> -<stop offset="1" stop-color="#00C4BF"/> -</linearGradient> -<linearGradient id="paint5_linear_699_5101" x1="24.5987" y1="82.3783" x2="72.5834" y2="82.3783" gradientUnits="userSpaceOnUse"> -<stop stop-color="#00FECF"/> -<stop offset="0.08" stop-color="#00E5D0"/> -<stop offset="0.25" stop-color="#00A5D1"/> -<stop offset="0.49" stop-color="#0040D4"/> -<stop offset="0.56" stop-color="#0025D5"/> -</linearGradient> -<linearGradient id="paint6_linear_699_5101" x1="70.9573" y1="52.5952" x2="189.069" y2="50.4576" gradientUnits="userSpaceOnUse"> -<stop stop-color="#00FECF"/> -<stop offset="0.05" stop-color="#00E5D0"/> -<stop offset="0.15" stop-color="#00A5D1"/> -<stop offset="0.29" stop-color="#0040D4"/> -<stop offset="0.33" stop-color="#0025D5"/> -</linearGradient> -<linearGradient id="paint7_linear_699_5101" x1="27.1191" y1="27.8689" x2="173.642" y2="27.8689" gradientUnits="userSpaceOnUse"> -<stop stop-color="#FFD4AF"/> -<stop offset="0.1" stop-color="#E6D5BA"/> -<stop offset="0.31" stop-color="#A7D6D5"/> -<stop offset="0.61" stop-color="#43D9FF"/> -<stop offset="0.63" stop-color="#37B1D0"/> -<stop offset="0.65" stop-color="#2B8CA5"/> -<stop offset="0.67" stop-color="#216B7D"/> -<stop offset="0.7" stop-color="#184E5B"/> -<stop offset="0.72" stop-color="#10353F"/> -<stop offset="0.75" stop-color="#0A2228"/> -<stop offset="0.78" stop-color="#061316"/> -<stop offset="0.82" stop-color="#020809"/> -<stop offset="0.88" stop-color="#010202"/> -<stop offset="1"/> -</linearGradient> -</defs> -</svg> diff --git a/src/config/extensions/icons/talisman_icon.svg b/src/config/extensions/icons/talisman_icon.svg deleted file mode 100644 index c8893a2a2a..0000000000 --- a/src/config/extensions/icons/talisman_icon.svg +++ /dev/null @@ -1 +0,0 @@ -<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" class="logo"><path fill-rule="evenodd" clip-rule="evenodd" d="M25.058 17.832c.304.662 1.2.896 1.714.38l.945-.944a2.5 2.5 0 0 1 3.536 3.535l-7.636 7.636A9.979 9.979 0 0 1 15.965 32a9.983 9.983 0 0 1-7.883-3.847l-7.35-7.35a2.5 2.5 0 1 1 3.536-3.535l.93.93c.504.504 1.379.277 1.677-.37a.97.97 0 0 0 .09-.406V6a2.5 2.5 0 0 1 5 0v5.778c0 .497.51.835.984.685a.727.727 0 0 0 .516-.682V2.5a2.5 2.5 0 0 1 5 0v9.28c0 .315.217.588.517.683.474.15.983-.188.983-.685V6a2.5 2.5 0 0 1 5 0v11.418a.99.99 0 0 0 .093.414Z" className='dark'></path><path className='light' d="M23.965 23s-3.581 5-8 5c-4.418 0-8-5-8-5s3.582-5 8-5c4.419 0 8 5 8 5Z"></path><path d="M18.731 23a2.766 2.766 0 1 1-5.531 0 2.766 2.766 0 0 1 5.531 0Z" className='dark' stroke="currentColor" stroke-width="0.469"></path></svg> \ No newline at end of file diff --git a/src/config/extensions/index.ts b/src/config/extensions/index.ts deleted file mode 100644 index 55f9a0454b..0000000000 --- a/src/config/extensions/index.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { FunctionComponent, SVGProps } from 'react'; -import { ReactComponent as EnkryptSVG } from './icons/enkrypt_icon.svg'; -import { ReactComponent as NovaWalletSVG } from './icons/nova_wallet.svg'; -import { ReactComponent as PolkadotJSSVG } from './icons/polkadot_js.svg'; -import { ReactComponent as SignerSVG } from './icons/signer_icon.svg'; -import { ReactComponent as SubwalletSVG } from './icons/subwallet_icon.svg'; -import { ReactComponent as TalismanSVG } from './icons/talisman_icon.svg'; - -export interface ExtensionConfig { - id: string; - title: string; - icon: FunctionComponent< - SVGProps<SVGSVGElement> & { title?: string | undefined } - >; -} -export const EXTENSIONS: ExtensionConfig[] = [ - { - id: 'enkrypt', - title: 'Enkrypt', - icon: EnkryptSVG, - }, - { - id: 'polkadot-js', - title: (window as any)?.walletExtension?.isNovaWallet - ? 'Nova Wallet' - : 'Polkadot JS', - icon: (window as any)?.walletExtension?.isNovaWallet - ? NovaWalletSVG - : PolkadotJSSVG, - }, - { - id: 'subwallet-js', - title: 'SubWallet', - icon: SubwalletSVG, - }, - { - id: 'talisman', - title: 'Talisman', - icon: TalismanSVG, - }, - { - id: 'parity-signer-companion', - title: 'Parity Signer Companion', - icon: SignerSVG, - }, -]; diff --git a/src/config/ledger.ts b/src/config/ledger.ts index 597b0183ef..01193ccd0f 100644 --- a/src/config/ledger.ts +++ b/src/config/ledger.ts @@ -2,18 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only import type { LedgerApp } from 'contexts/Hardware/types'; -import KusamaSVG from 'img/appIcons/kusama.svg?react'; import PolkadotSVG from 'img/appIcons/polkadot.svg?react'; export const LedgerApps: LedgerApp[] = [ { - network: 'polkadot', + network: 'Cere Mainnet', appName: 'Polkadot', Icon: PolkadotSVG, }, - { - network: 'kusama', - appName: 'Kusama', - Icon: KusamaSVG, - }, ]; diff --git a/src/config/networks.ts b/src/config/networks.ts index 3197fa5bce..3d1948d883 100644 --- a/src/config/networks.ts +++ b/src/config/networks.ts @@ -128,4 +128,4 @@ export const NetworkList: Networks = { ...(includeTestnet ? { cereTestnet } : {}), cereDevnet, cereQAnet, -}; \ No newline at end of file +}; diff --git a/src/config/validators/index.ts b/src/config/validators/index.ts deleted file mode 100644 index 1c650c09f5..0000000000 --- a/src/config/validators/index.ts +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -/* Import your SVG Here. - * Use upper camel-case for your SVG import, lower camel case for the svg. - * import { ReactComponent as ValidatorEntityName } from './thumbnails/validatorEntityName.svg'; - */ -import { ReactComponent as Thumbnail4T2CAPITAL } from './thumbnails/4t2.svg'; -import { ReactComponent as AnyValid } from './thumbnails/anyvalid.svg'; -import { ReactComponent as Brightlystake } from './thumbnails/Brightlystake-logo.svg'; -import { ReactComponent as Cere } from './thumbnails/cere.svg'; -import { ReactComponent as EdgeServices } from './thumbnails/edgeservices.svg'; -import { ReactComponent as garm99 } from './thumbnails/garm99.svg'; -import { ReactComponent as Jinogami } from './thumbnails/Jinogami.svg'; -import { ReactComponent as medium } from './thumbnails/medium.svg'; -import { ReactComponent as SerGo } from './thumbnails/SerGo.svg'; -import { ReactComponent as StakeAngle } from './thumbnails/stakeangle.svg'; -import { ReactComponent as Testnetrun } from './thumbnails/Testnetrun.svg'; -import { ReactComponent as Tokem } from './thumbnails/tomek.svg'; -import { ReactComponent as TRK } from './thumbnails/TRK.svg'; -import { ReactComponent as wombat } from './thumbnails/wombat.svg'; -import { ReactComponent as XameyzIdentity } from './thumbnails/xameyz.svg'; - -export const VALIDATOR_COMMUNITY = [ - { - name: 'CERE', - Thumbnail: Cere, - thumbnail: 'Cere', - bio: `Official Validators from Cere Network, the world's first Decentralized Data Cloud platform.`, - email: 'team@cere.network', - website: 'https://cere.network', - twitter: '@CereNetwork', - validators: { - cere: [ - '6S4mrsCrqWoBAYrp2PKQNh7CYcCtyEtYpx5J626Kj5vszSyy', - '6QhzyvZQm3dLjDmeaoUnLPXzfuTi6X1HEo6AX6gfVbC3shzD', - '6RgfwDiQTLjgbkQ5CorrKtRtCaDABQKYsibk9MeyvzmKFrk2', - '6TBhZAgtFc3Wr8BeNu5tdMJG1NDpxKbG2Hwf2UbVtMGyFxzN', - '6Pyh9zZgp4XCP338VDG7oshK7PvsAdyuBN6S2NNm7CBoCXx8', - '6S9tXQmPYoeBXYey8vKYi9BMbNMD8Zgqb62k7SYMNQLUbydZ', - '6PwAv2L43zGPEwHTb1L7LyCWv7yq2Hc4dSVYHvvi1kscCR91', - '6Qshjra42mLDtc9ouHzUz1bMmYXg2qasmW2xSLgendRdsYED', - ], - }, - }, - { - name: 'Xameyz', - Thumbnail: XameyzIdentity, - thumbnail: 'Xameyz', - bio: `Just a humble validator on a humble decentralized network.`, - email: 'xameyz.crypto@yahoo.com', - website: '', - twitter: '@xameyz', - validators: { - cere: ['6TYC5go4hQ85NxmGK8c658cmJozxeohKfp6YbDGC5r1HJ6nZ'], - }, - }, - { - name: 'AnyValid', - Thumbnail: AnyValid, - thumbnail: 'AnyValid', - bio: `Professional Proof-of-Stake Networks Validation Services`, - email: 'mail@anyvalid.com', - website: 'https://anyvalid.com', - twitter: '@anyvalid', - validators: { - cere: ['6UDVCKB9opndqcRAxTpTvKFTFXUwvE36aYnp1bNyVV3Cfh16'], - }, - }, - { - name: 'SerGo', - Thumbnail: SerGo, - thumbnail: 'SerGo', - bio: `We validate with expertise, you earn with confidence.`, - email: 'contact@sergo.dev', - website: 'https://sergo.dev', - twitter: '', - validators: { - cere: ['6SpjH8swCtFwmntQmdikMWyDKgr59q1cLauePYc2iqwwe6Bv'], - }, - }, - { - name: 'StakeAngle', - Thumbnail: StakeAngle, - thumbnail: 'StakeAngle', - bio: `Non-custodial staking provider`, - email: 'info@stakeangle.com', - website: 'https://stakeangle.com', - twitter: '', - validators: { - cere: ['6T9794B5HPPNYqds6VNAxcFjYZHzpgbZfdWXSiE6q65eLpVm'], - }, - }, - { - name: 'medium', - Thumbnail: medium, - thumbnail: 'medium', - bio: `Individual staking services`, - email: 'hsonelove228@gmail.com', - website: 'https://github.com/mediumwe11', - twitter: '', - validators: { - cere: ['6TsKhZ5o9BFAG1bCY2HVriUXAuQMae744APMMcVgYqYhNJRy'], - }, - }, - { - name: 'Jinogami', - Thumbnail: Jinogami, - thumbnail: 'Jinogami', - bio: `A Community Cere Validator`, - email: 'steganosgraphos@gmail.com', - website: '', - twitter: '@steganosgraphos', - validators: { - cere: ['6RvHuBMLqBVcwvk62qto81m8DFPoKBq86WCAmVEwHH3rTuNQ'], - }, - }, - { - name: 'EdgeServices', - Thumbnail: EdgeServices, - thumbnail: 'EdgeServices', - bio: `Blockchain Nodes & Services`, - email: 'contact@edgeservices.io', - website: 'https://edgeservices.io', - twitter: '@EdgeServicesIO', - validators: { - cere: ['6RByFsuHYQET5V78TqaRuyeF8XQRKPuZhkE9admxL48VUEey'], - }, - }, - { - name: 'wombat', - Thumbnail: wombat, - thumbnail: 'wombat', - bio: `Professional blockchain validator`, - email: 'sdidenko566@yandex.ru', - website: 'https://github.com/wombatqq', - twitter: '', - validators: { - cere: ['6Pqj4UwFRN4mmu25PH8RqWaMT4jNd6ytFNgaJ4KtVTfgByez'], - }, - }, - { - name: 'Brightlystake', - Thumbnail: Brightlystake, - thumbnail: 'Brightlystake', - bio: `Cere node from Brightlystake. Contact us for any queries https://linktr.ee/brightlystake`, - email: 'contact@brightlystake.com', - website: 'https://brightlystake.com/', - twitter: '@brightlystake', - validators: { - cere: ['6TnQVHWvDtw5W5vqEjNcewypHagW9N5VtvDTDUNWq3drqZTB'], - }, - }, - { - name: 'garm99', - Thumbnail: garm99, - thumbnail: 'garm99', - bio: `Staking Provider of Proof-of-Stake Networks`, - email: 'info@nodeskeeper.com', - website: 'nodeskeeper.com', - twitter: '@GARM799', - validators: { - cere: ['6RLGWVUzwXBvjuyLHY7Hr95TqgdUV6UxboG9i3xZnVp1vVWk'], - }, - }, - { - name: '4T2.CAPITAL', - Thumbnail: Thumbnail4T2CAPITAL, - thumbnail: '4T2.CAPITAL', - bio: `valid thumbnail: 'XameyzIdentity',ating with love & care | secure and seamless staking experience | based in 🇳🇴`, - email: '4t2@4t2.capital', - website: 'https://4t2.capital', - twitter: '', - validators: { - cere: ['6VB5dkmPn6zpti4BaEZp2y7Ht8kaj8ELKGAHzpThowzXU66A'], - }, - }, - { - name: 'TRK', - Thumbnail: TRK, - thumbnail: 'TRK', - bio: `Validating...`, - email: '', - website: '', - twitter: '', - validators: { - cere: ['6TBNtFjPELfrzSa2sXYyTWhbP1omhjxhF5nk6jtR51S3pfrS'], - }, - }, - { - name: 'Testnetrun', - Thumbnail: Testnetrun, - thumbnail: 'Testnetrun', - bio: `Position yourself for the blockchain-powered future of the next decade by staking today, ensuring you're part of the fastest and most secure validator network in the space.`, - email: 'info@testnet.run', - website: 'https://stake.testnet.run', - twitter: '@testnetrun', - validators: { - cere: ['6QPgrdDzaMqj54YcHm1XpyqN8z9DTZ9sySXwF7uFwfUADkiL'], - }, - }, - { - name: 'TomekNode', - Thumbnail: Tokem, - thumbnail: 'Tokem', - bio: `Experienced and dedicated, I pride myself on being a reliable community validator node maintainer, ensuring optimal performance and trust for our nominators`, - email: 'cere.e69cg@passfwd.com', - website: '', - twitter: '', - validators: { - cere: ['6PbuJRgBSikmBNajCa75Zq9PqXmaYcCZ6e5QKk7sUyeebBDU'], - }, - }, -]; diff --git a/src/contexts/Account/defaults.ts b/src/contexts/Account/defaults.ts index 168934ea61..b09d389c6c 100644 --- a/src/contexts/Account/defaults.ts +++ b/src/contexts/Account/defaults.ts @@ -1,7 +1,7 @@ // Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AccountContextInterface } from './types'; +import type { AccountContextInterface } from './types'; export const defaultAccountContext: AccountContextInterface = { // eslint-disable-next-line diff --git a/src/contexts/Account/index.tsx b/src/contexts/Account/index.tsx index b2d9384ab8..cd714a52e7 100644 --- a/src/contexts/Account/index.tsx +++ b/src/contexts/Account/index.tsx @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useEffect, useRef, useState } from 'react'; -import { AnyApi, AnyMetaBatch } from 'types'; +import type { AnyApi, AnyMetaBatch } from 'types'; import { setStateWithRef } from 'Utils'; import { useApi } from '../Api'; import { defaultAccountContext } from './defaults'; -import { AccountContextInterface } from './types'; +import type { AccountContextInterface } from './types'; // context definition export const AccountContext = React.createContext<AccountContextInterface>( diff --git a/src/contexts/Account/types.ts b/src/contexts/Account/types.ts index caa779dab1..51e77dfb09 100644 --- a/src/contexts/Account/types.ts +++ b/src/contexts/Account/types.ts @@ -1,7 +1,7 @@ // Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { AnyMetaBatch } from 'types'; +import type { AnyMetaBatch } from 'types'; export interface AccountContextInterface { fetchAccountMetaBatch: (k: string, v: string[], r?: boolean) => void; diff --git a/src/contexts/CereStats/defaults.ts b/src/contexts/CereStats/defaults.ts index c1ec0d6005..0600df3cf7 100644 --- a/src/contexts/CereStats/defaults.ts +++ b/src/contexts/CereStats/defaults.ts @@ -1,8 +1,9 @@ -import { CereStatsContextInterface } from './types'; +import type { CereStatsContextInterface } from './types'; export const defaultCereStatsContext: CereStatsContextInterface = { // eslint-disable-next-line - fetchEraPoints: (v, e) => {}, + fetchEraPoints: (v, e) => Promise.resolve([]), payouts: [], poolClaims: [], + unclaimedPayouts: [], }; diff --git a/src/contexts/CereStats/index.tsx b/src/contexts/CereStats/index.tsx index 28072eec43..d360bb536b 100644 --- a/src/contexts/CereStats/index.tsx +++ b/src/contexts/CereStats/index.tsx @@ -1,16 +1,12 @@ -import { - ApolloClient, - gql, - InMemoryCache, - NormalizedCacheObject, -} from '@apollo/client'; +import type { NormalizedCacheObject } from '@apollo/client'; +import { ApolloClient, gql, InMemoryCache } from '@apollo/client'; import { WebSocketLink } from '@apollo/client/link/ws'; import React, { createContext, useEffect, useState } from 'react'; -import { Network } from '../../types'; -import { useApi } from '../Api'; -import { useConnect } from '../Connect'; +import { useActiveAccounts } from 'contexts/ActiveAccounts'; +import { useNetwork } from 'contexts/Network'; import { defaultCereStatsContext } from './defaults'; -import { CereStatsContextInterface } from './types'; +import type { CereStatsContextInterface } from './types'; +import type { Network, AnyJson } from '../../types'; const useApolloClient = (endpoint: Network['cereStatsEndpoint']) => { const [client, setClient] = @@ -24,12 +20,12 @@ const useApolloClient = (endpoint: Network['cereStatsEndpoint']) => { }, }); - const _client = new ApolloClient({ + const clientSetter = new ApolloClient({ link: wsLink, cache: new InMemoryCache(), }); - setClient(_client); + setClient(clientSetter); }, [endpoint]); return client; @@ -82,7 +78,7 @@ const usePayouts = ( client: ApolloClient<NormalizedCacheObject> | null, activeAccount: string | null ) => { - const [payouts, setPayouts] = useState([]); + const [payouts, setPayouts] = useState<AnyJson[]>([]); const normalizePayouts = ( payoutData: { blockNumber: number; data: string; timestamp: number }[] @@ -133,7 +129,6 @@ const usePayouts = ( }, }); - // @ts-ignore setPayouts(normalizePayouts(data.event)); }; @@ -155,10 +150,10 @@ export const CereStatsProvider = ({ }: { children: React.ReactNode; }) => { - const { network } = useApi(); - const { activeAccount } = useConnect(); + const { networkData } = useNetwork(); + const { activeAccount } = useActiveAccounts(); - const client = useApolloClient(network.cereStatsEndpoint); + const client = useApolloClient(networkData.cereStatsEndpoint); const fetchEraPoints = useFetchEraPoints(client); const payouts = usePayouts(client, activeAccount); @@ -172,6 +167,7 @@ export const CereStatsProvider = ({ fetchEraPoints, payouts, poolClaims: [], + unclaimedPayouts: [], }} > {children} diff --git a/src/contexts/CereStats/types.ts b/src/contexts/CereStats/types.ts index e6b0632d38..a16a3b9cf0 100644 --- a/src/contexts/CereStats/types.ts +++ b/src/contexts/CereStats/types.ts @@ -1,7 +1,17 @@ +export interface ActiveEraData { + era: number; + reward_point: RewardPoint; // You might want to change `any` to a more specific type if possible +} + +export interface RewardPoint { + era: number; +} + export interface CereStatsContextInterface { - fetchEraPoints: (v: string, e: number) => void; + fetchEraPoints: (v: string, e: number) => Promise<ActiveEraData[]>; payouts: any; // The Cere Stats does not currently support `poolClaims`. // We need it to maintain consistency with the `useSubscan` hook and for possible future support of `poolClaims`. poolClaims: []; + unclaimedPayouts: []; } diff --git a/src/contexts/Connect/Hooks/useImportExtension.tsx b/src/contexts/Connect/Hooks/useImportExtension.tsx deleted file mode 100644 index a06a6959f8..0000000000 --- a/src/contexts/Connect/Hooks/useImportExtension.tsx +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import Keyring from '@polkadot/keyring'; -import { useApi } from 'contexts/Api'; -import { useExtensions } from 'contexts/Extensions'; -import { ExtensionInteface } from 'contexts/Extensions/types'; -import { AnyFunction } from 'types'; -import { isValidAddress } from 'Utils'; -import { ExtensionAccount, ExternalAccount, ImportedAccount } from '../types'; -import { - addToLocalExtensions, - getActiveAccountLocal, - getInExternalAccounts, -} from '../Utils'; - -export const useImportExtension = () => { - const { network } = useApi(); - const { setExtensionStatus } = useExtensions(); - - // Handles importing of an extension. - // - // Adds extension metadata to state and updates local storage with - // connected extensions. Calls separate method to handle account importing. - const handleImportExtension = ( - id: string, - accounts: Array<ExtensionAccount>, - extension: ExtensionInteface, - injected: Array<ExtensionAccount>, - forget: (a: Array<ExternalAccount>) => void - ) => { - // update extensions status to connected. - setExtensionStatus(id, 'connected'); - // update local active extensions - addToLocalExtensions(id); - - if (injected.length) { - return handleInjectedAccounts(id, accounts, extension, injected, forget); - } - return []; - }; - - // Handles importing of extension accounts. - // - // Gets accounts to be imported and commits them to state. - const handleInjectedAccounts = ( - id: string, - accounts: Array<ExtensionAccount>, - extension: ExtensionInteface, - injected: Array<ExtensionAccount>, - forget: (a: Array<ExternalAccount>) => void - ) => { - // set network ss58 format - const keyring = new Keyring(); - keyring.setSS58Format(network.ss58); - - // remove accounts that do not contain correctly formatted addresses. - injected = injected.filter((i: ExtensionAccount) => { - return isValidAddress(i.address); - }); - - // reformat addresses to ensure correct ss58 format - injected.forEach(async (account: ExtensionAccount) => { - const { address } = keyring.addFromAddress(account.address); - account.address = address; - return account; - }); - - // remove injected if they exist in local external accounts - forget(getInExternalAccounts(injected, network)); - - // remove accounts that have already been injected via another extension. - injected = injected.filter( - (i: ExtensionAccount) => - !accounts.map((j: ImportedAccount) => j.address).includes(i.address) - ); - - // format account properties. - injected = injected.map((a: ExtensionAccount) => { - return { - address: a.address, - source: id, - name: a.name, - signer: extension.signer, - }; - }); - return injected; - }; - - // Get active extension account. - // - // checks if the local active account is in the extension. - const getActiveExtensionAccount = (injected: Array<ImportedAccount>) => - injected.find( - (a: ExtensionAccount) => a.address === getActiveAccountLocal(network) - ) ?? null; - - // Connect active extension account. - // - // Connects to active account if it is provided. - const connectActiveExtensionAccount = ( - account: ImportedAccount | null, - callback: AnyFunction - ) => { - if (account !== null) { - callback(account); - } - }; - - return { - handleImportExtension, - getActiveExtensionAccount, - connectActiveExtensionAccount, - }; -}; diff --git a/src/contexts/Connect/defaults.ts b/src/contexts/Connect/defaults.ts deleted file mode 100644 index 0fb2dba0d3..0000000000 --- a/src/contexts/Connect/defaults.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ConnectContextInterface } from 'contexts/Connect/types'; - -export const defaultConnectContext: ConnectContextInterface = { - // eslint-disable-next-line - formatAccountSs58: (a: string) => null, - // eslint-disable-next-line - connectExtensionAccounts: (e) => {}, - // eslint-disable-next-line - getAccount: (a) => null, - // eslint-disable-next-line - connectToAccount: (a) => {}, - disconnectFromAccount: () => {}, - // eslint-disable-next-line - addExternalAccount: (a, b) => {}, - getActiveAccount: () => null, - // eslint-disable-next-line - accountHasSigner: (a) => false, - // eslint-disable-next-line - isReadOnlyAccount: (a) => false, - // eslint-disable-next-line - forgetAccounts: (a) => {}, - accounts: [], - activeAccount: null, - activeAccountMeta: null, -}; diff --git a/src/contexts/Connect/index.tsx b/src/contexts/Connect/index.tsx deleted file mode 100644 index d476498ef1..0000000000 --- a/src/contexts/Connect/index.tsx +++ /dev/null @@ -1,490 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import Keyring from '@polkadot/keyring'; -import { DappName } from 'consts'; -import { useApi } from 'contexts/Api'; -import { - ConnectContextInterface, - ExtensionAccount, - ExternalAccount, - ImportedAccount, -} from 'contexts/Connect/types'; -import { useExtensions } from 'contexts/Extensions'; -import { Extension, ExtensionInteface } from 'contexts/Extensions/types'; -import React, { useEffect, useRef, useState } from 'react'; -import { AnyApi, MaybeAccount } from 'types'; -import { clipAddress, localStorageOrDefault, setStateWithRef } from 'Utils'; -import { defaultConnectContext } from './defaults'; -import { useImportExtension } from './Hooks/useImportExtension'; -import { - extensionIsLocal, - getActiveAccountLocal, - getLocalExternalAccounts, - removeFromLocalExtensions, - removeLocalExternalAccounts, -} from './Utils'; - -export const ConnectContext = React.createContext<ConnectContextInterface>( - defaultConnectContext -); - -export const useConnect = () => React.useContext(ConnectContext); - -export const ConnectProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const { network } = useApi(); - const { - setExtensionStatus, - extensionsFetched, - setExtensionsFetched, - extensions, - } = useExtensions(); - const { - handleImportExtension, - getActiveExtensionAccount, - connectActiveExtensionAccount, - } = useImportExtension(); - - // store accounts list - const [accounts, setAccounts] = useState<Array<ImportedAccount>>([]); - const accountsRef = useRef(accounts); - - // store the currently active account - const [activeAccount, _setActiveAccount] = useState<string | null>(null); - const activeAccountRef = useRef<string | null>(activeAccount); - - // store the currently active account metadata - const [activeAccountMeta, setActiveAccountMeta] = - useState<ImportedAccount | null>(null); - const activeAccountMetaRef = useRef(activeAccountMeta); - - // store unsubscribe handler for connected extensions - const [unsubscribe, setUnsubscribe] = useState<AnyApi>([]); - const unsubscribeRef = useRef(unsubscribe); - - /* re-sync extensions accounts on network switch - * do this if activeAccount is present. - * if activeAccount is present, and extensions have for some - * reason forgot the site, then all pop-ups will be summoned - * here. - */ - useEffect(() => { - // unsubscribe from all accounts and reset state - unsubscribeAll(); - setStateWithRef(null, _setActiveAccount, activeAccountRef); - setStateWithRef([], setAccounts, accountsRef); - setStateWithRef(null, setActiveAccountMeta, activeAccountMetaRef); - setExtensionsFetched(false); - - // get active extensions - const localExtensions = localStorageOrDefault( - `active_extensions`, - [], - true - ); - // if extensions have been fetched, - // get accounts if extensions exist and - // local extensions exist (previously connected). - if (extensions) { - if (extensions.length && localExtensions.length) { - connectActiveExtensions(); - } else { - setExtensionsFetched(true); - } - } - return () => { - unsubscribeAll(); - }; - }, [extensions?.length, network]); - - // once extension accounts are synced, fetch - // any external accounts present in localStorage. - useEffect(() => { - if (extensionsFetched) importExternalAccounts(); - }, [extensionsFetched]); - - /* - * Unsubscrbe all account subscriptions - */ - const unsubscribeAll = () => { - unsubscribeRef.current.forEach(({ unsub }: AnyApi) => unsub()); - }; - - /* - * Unsubscrbe from some account subscriptions and update the resulting state. - */ - const forgetAccounts = (forget: Array<ExternalAccount>) => { - if (!forget.length) return; - const addresses = forget.map((a: ExternalAccount) => a.address); - - // unsubscribe from provided addresses - Object.values( - unsubscribeRef.current.filter((f: AnyApi) => addresses.includes(f.key)) - ).forEach(({ unsub }: AnyApi) => unsub()); - - // filter addresses from current unsubs - const unsubsNew = unsubscribeRef.current.filter( - (f: AnyApi) => !addresses.includes(f.key) - ); - - // if active account is being forgotten, disconnect - const activeAccountUnsub = forget.find( - (a: ExternalAccount) => a.address === activeAccount - ); - if (activeAccountUnsub !== undefined) { - setStateWithRef(null, setActiveAccount, activeAccountRef); - setStateWithRef(null, setActiveAccountMeta, activeAccountMetaRef); - } - - // remove forgotten external accounts from localStorage - removeLocalExternalAccounts(network, forget); - - // update accounts - const accountsNew = accountsRef.current.filter( - (a: ImportedAccount) => - forget.find((e: ExternalAccount) => e.address === a.address) === - undefined - ); - - // update accounts and corresponding unsubs - setStateWithRef(accountsNew, setAccounts, accountsRef); - setStateWithRef(unsubsNew, setUnsubscribe, unsubscribeRef); - }; - - /* importExternalAccounts - * checks previously imported read-only accounts from - * localStorage and adds them to `accounts` state. - * if local active account is present, it will also be - * assigned as active. - * Should be called AFTER extension accounts are imported, as - * to not replace an extension account by an external account. - */ - const importExternalAccounts = () => { - // import any local external accounts - let localExternalAccounts = getLocalExternalAccounts(network, true); - - if (localExternalAccounts.length) { - // get and format active account if present - const activeAccountLocal = getActiveAccountLocal(network); - - const activeAccountIsExternal = - localExternalAccounts.find( - (a: ImportedAccount) => a.address === activeAccountLocal - ) ?? null; - - // remove already-imported accounts (extensions may have already imported) - localExternalAccounts = localExternalAccounts.filter( - (l: ExternalAccount) => - accountsRef.current.find( - (a: ImportedAccount) => a.address === l.address - ) === undefined - ); - - // set active account for network - if (activeAccountIsExternal) { - connectToAccount(activeAccountIsExternal); - } - // add external accounts to imported - setStateWithRef( - [...accountsRef.current].concat(localExternalAccounts), - setAccounts, - accountsRef - ); - } - }; - - /* connectActiveExtensions - * Connects to extensions that already have been connected - * to and stored in localStorage. - * Loop through extensions and connect to accounts. - * If `activeAccount` exists locally, we wait until all - * extensions are looped before connecting to it; there is - * no guarantee it still exists - must explicitly find it. - */ - const connectActiveExtensions = async () => { - const keyring = new Keyring(); - keyring.setSS58Format(network.ss58); - - // iterate extensions and add accounts to state - const total = extensions?.length ?? 0; - let activeWalletAccount: ImportedAccount | null = null; - - if (!extensions) { - return; - } - - let i = 0; - extensions.forEach(async (e: Extension) => { - i++; - const { id, enable } = e; - - // if extension is found locally, subscribe to accounts - if (extensionIsLocal(id)) { - try { - // summons extension popup - const extension: ExtensionInteface = await enable(DappName); - - if (extension !== undefined) { - const unsub = (await extension.accounts.subscribe( - (injected: ExtensionAccount[]) => { - if (injected) { - injected = handleImportExtension( - id, - accountsRef.current, - extension, - injected, - forgetAccounts - ); - // store active wallet account if found in this extension - if (!activeWalletAccount) { - activeWalletAccount = getActiveExtensionAccount(injected); - } - // set active account for network on final extension - if (i === total && activeAccountRef.current === null) { - connectActiveExtensionAccount( - activeWalletAccount, - connectToAccount - ); - } - // concat accounts and store - if (injected.length) { - setStateWithRef( - [...accountsRef.current].concat(injected), - setAccounts, - accountsRef - ); - } - } - } - )) as () => void; - - // update context state - setStateWithRef( - [...unsubscribeRef.current].concat({ - key: id, - unsub, - }), - setUnsubscribe, - unsubscribeRef - ); - } - } catch (err) { - handleExtensionError(id, String(err)); - } - } - - // set extension fetched to allow external accounts - // to be imported. - if (i === total) { - setExtensionsFetched(true); - } - }); - }; - - /* connectExtensionAccounts - * Similar to the above but only connects to a single extension. - * This is invoked by the user by clicking on an extension. - * If activeAccount is not found here, it is simply ignored. - */ - const connectExtensionAccounts = async (e: Extension) => { - const keyring = new Keyring(); - keyring.setSS58Format(network.ss58); - const { id, enable } = e; - - try { - // summons extension popup - const extension: ExtensionInteface = await enable(DappName); - - if (extension !== undefined) { - // subscribe to accounts - const unsub = (await extension.accounts.subscribe( - (injected: ExtensionAccount[]) => { - if (injected) { - injected = handleImportExtension( - id, - accountsRef.current, - extension, - injected, - forgetAccounts - ); - // set active account for network if not yet set - if (activeAccountRef.current === null) { - connectActiveExtensionAccount( - getActiveExtensionAccount(injected), - connectToAccount - ); - } - // concat accounts and store - setStateWithRef( - [...accountsRef.current].concat(injected), - setAccounts, - accountsRef - ); - } - } - )) as () => void; - - // update context state - setStateWithRef( - [...unsubscribeRef.current].concat({ - key: id, - unsub, - }), - setUnsubscribe, - unsubscribeRef - ); - } - } catch (err) { - handleExtensionError(id, String(err)); - } - }; - - const handleExtensionError = (id: string, err: string) => { - // authentication error (extension not enabled) - if (err.substring(0, 9) === 'AuthError') { - removeFromLocalExtensions(id); - setExtensionStatus(id, 'not_authenticated'); - } - - // extension not found (does not exist) - if (err.substring(0, 17) === 'NotInstalledError') { - removeFromLocalExtensions(id); - setExtensionStatus(id, 'not_found'); - } - - // general error (maybe enabled but no accounts trust app) - if (err.substring(0, 5) === 'Error') { - setExtensionStatus(id, 'no_accounts'); - } - }; - - const setActiveAccount = (address: string | null) => { - if (address === null) { - localStorage.removeItem(`${network.name.toLowerCase()}_active_account`); - } else { - localStorage.setItem( - `${network.name.toLowerCase()}_active_account`, - address - ); - } - setStateWithRef(address, _setActiveAccount, activeAccountRef); - }; - - const connectToAccount = (account: ImportedAccount | null) => { - setActiveAccount(account?.address ?? null); - setStateWithRef(account, setActiveAccountMeta, activeAccountMetaRef); - }; - - const disconnectFromAccount = () => { - localStorage.removeItem(`${network.name.toLowerCase()}_active_account`); - setActiveAccount(null); - setStateWithRef(null, setActiveAccountMeta, activeAccountMetaRef); - }; - - const getAccount = (addr: MaybeAccount) => { - const acc = - accountsRef.current.find((a: ImportedAccount) => a?.address === addr) || - null; - return acc; - }; - - const getActiveAccount = () => { - return activeAccountRef.current; - }; - - // adds an external account (non-wallet) to accounts - const addExternalAccount = (address: string, addedBy: string) => { - // ensure account is formatted correctly - const keyring = new Keyring(); - keyring.setSS58Format(network.ss58); - const formatted = keyring.addFromAddress(address).address; - - const externalAccount = { - address: formatted, - network: network.name, - name: clipAddress(address), - source: 'external', - addedBy, - }; - - // get all external accounts from localStorage - const localExternalAccounts = getLocalExternalAccounts(network, false); - const exists = localExternalAccounts.find( - (l: ExternalAccount) => - l.address === address && l.network === network.name - ); - - // add external account to localStorage if not there already - if (!exists) { - const localExternal = localExternalAccounts.concat(externalAccount); - localStorage.setItem('external_accounts', JSON.stringify(localExternal)); - } - - // add external account to imported accounts - setStateWithRef( - [...accountsRef.current].concat(externalAccount), - setAccounts, - accountsRef - ); - }; - - // checks whether an account can sign transactions - const accountHasSigner = (address: MaybeAccount) => { - const exists = - accountsRef.current.find( - (a: ImportedAccount) => a.address === address && a.source !== 'external' - ) !== undefined; - return exists; - }; - - const isReadOnlyAccount = (address: MaybeAccount) => { - const account = getAccount(address) ?? {}; - - if (Object.prototype.hasOwnProperty.call(account, 'addedBy')) { - const { addedBy } = account as ExternalAccount; - return addedBy === 'user'; - } - return false; - }; - - // check an account balance exists on-chain - const formatAccountSs58 = (address: string) => { - try { - const keyring = new Keyring(); - keyring.setSS58Format(network.ss58); - const formatted = keyring.addFromAddress(address).address; - if (formatted !== address) { - return formatted; - } - return null; - } catch (e) { - return null; - } - }; - - return ( - <ConnectContext.Provider - value={{ - formatAccountSs58, - connectExtensionAccounts, - getAccount, - connectToAccount, - disconnectFromAccount, - addExternalAccount, - getActiveAccount, - accountHasSigner, - isReadOnlyAccount, - forgetAccounts, - accounts: accountsRef.current, - activeAccount: activeAccountRef.current, - activeAccountMeta: activeAccountMetaRef.current, - }} - > - {children} - </ConnectContext.Provider> - ); -}; diff --git a/src/contexts/Connect/types.ts b/src/contexts/Connect/types.ts deleted file mode 100644 index f241fa32f9..0000000000 --- a/src/contexts/Connect/types.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { Extension } from 'contexts/Extensions/types'; -import { MaybeAccount } from 'types'; - -export interface ConnectContextInterface { - formatAccountSs58: (a: string) => string | null; - connectExtensionAccounts: (e: Extension) => void; - getAccount: (account: MaybeAccount) => ExtensionAccount | null; - connectToAccount: (a: ExtensionAccount) => void; - disconnectFromAccount: () => void; - addExternalAccount: (a: string, addedBy: string) => void; - getActiveAccount: () => string | null; - accountHasSigner: (a: MaybeAccount) => boolean; - isReadOnlyAccount: (a: MaybeAccount) => boolean; - forgetAccounts: (a: Array<ExternalAccount>) => void; - accounts: Array<ExtensionAccount>; - activeAccount: string | null; - activeAccountMeta: ExtensionAccount | null; -} -export interface ExtensionAccount { - addedBy?: string; - address: string; - source: string; - name?: string; - signer?: unknown; -} - -export type ImportedAccount = ExtensionAccount | ExternalAccount; - -export interface ExternalAccount { - address: string; - network: string; - name: string; - source: string; - addedBy: string; -} diff --git a/src/contexts/Extensions/defaults.ts b/src/contexts/Extensions/defaults.ts deleted file mode 100644 index 47c61958f6..0000000000 --- a/src/contexts/Extensions/defaults.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ExtensionsContextInterface } from './types'; - -export const defaultExtensionsContext: ExtensionsContextInterface = { - extensions: [], - extensionsStatus: {}, - extensionsFetched: false, - // eslint-disable-next-line - setExtensionStatus: (id, s) => {}, - // eslint-disable-next-line - setExtensionsFetched: (s) => {}, - // eslint-disable-next-line - setExtensions: (s) => {}, -}; diff --git a/src/contexts/Extensions/index.tsx b/src/contexts/Extensions/index.tsx deleted file mode 100644 index 695d49fba3..0000000000 --- a/src/contexts/Extensions/index.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ExtensionConfig, EXTENSIONS } from 'config/extensions'; -import { - Extension, - ExtensionsContextInterface, -} from 'contexts/Extensions/types'; -import React, { useEffect, useRef, useState } from 'react'; -import { AnyApi } from 'types'; -import { setStateWithRef } from 'Utils'; -import { defaultExtensionsContext } from './defaults'; - -export const ExtensionsContext = - React.createContext<ExtensionsContextInterface>(defaultExtensionsContext); - -export const useExtensions = () => React.useContext(ExtensionsContext); - -export const ExtensionsProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - // store the installed extensions in state - const [extensions, setExtensions] = useState<Array<Extension> | null>(null); - - // store whether extensions have been fetched - const [extensionsFetched, setExtensionsFetched] = useState(false); - - // store each extension's status in state. - const [extensionsStatus, setExtensionsStatus] = useState<{ - [key: string]: string; - }>({}); - const extensionsStatusRef = useRef(extensionsStatus); - - // initialise extensions. - useEffect(() => { - if (!extensions) { - // timeout for initialising injectedWeb3 - setTimeout(() => setExtensions(getInstalledExtensions()), 200); - } - }); - - const setExtensionStatus = (id: string, status: string) => { - setStateWithRef( - Object.assign(extensionsStatusRef.current, { - [id]: status, - }), - setExtensionsStatus, - extensionsStatusRef - ); - }; - - const getInstalledExtensions = () => { - const { injectedWeb3 }: AnyApi = window; - const _exts: Extension[] = []; - EXTENSIONS.forEach((e: ExtensionConfig) => { - if (injectedWeb3[e.id] !== undefined) { - _exts.push({ - ...e, - ...injectedWeb3[e.id], - }); - } - }); - return _exts; - }; - - return ( - <ExtensionsContext.Provider - value={{ - extensions: extensions ?? [], - setExtensionStatus, - extensionsStatus: extensionsStatusRef.current, - extensionsFetched, - setExtensionsFetched, - setExtensions, - }} - > - {children} - </ExtensionsContext.Provider> - ); -}; diff --git a/src/contexts/Extensions/types.ts b/src/contexts/Extensions/types.ts deleted file mode 100644 index 51145bec34..0000000000 --- a/src/contexts/Extensions/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { FunctionComponent, SVGProps } from 'react'; -import { AnyApi } from 'types'; - -export interface ExtensionsContextInterface { - extensions: Array<Extension>; - extensionsStatus: { [key: string]: string }; - extensionsFetched: boolean; - setExtensionStatus: (id: string, s: string) => void; - setExtensionsFetched: (s: boolean) => void; - setExtensions: (s: Array<Extension>) => void; -} - -export interface ExtensionInteface { - accounts: AnyApi; - metadata: AnyApi; - provider: AnyApi; - signer: AnyApi; -} - -export interface Extension { - id: string; - title: string; - icon: FunctionComponent< - SVGProps<SVGSVGElement> & { title?: string | undefined } - >; - enable: (n: string) => Promise<ExtensionInteface>; - version: string; -} diff --git a/src/contexts/Modal/defaults.ts b/src/contexts/Modal/defaults.ts deleted file mode 100644 index ca64ff8e9a..0000000000 --- a/src/contexts/Modal/defaults.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ModalContextInterface } from './types'; - -export const defaultModalContext: ModalContextInterface = { - status: 0, - // eslint-disable-next-line - setStatus: (status) => {}, - // eslint-disable-next-line - openModalWith: (m, c, s) => {}, - // eslint-disable-next-line - setModalHeight: (v) => {}, - setResize: () => {}, - modal: 'ConnectAccounts', - config: {}, - size: 'large', - height: 0, - resize: 0, -}; diff --git a/src/contexts/Modal/index.tsx b/src/contexts/Modal/index.tsx deleted file mode 100644 index 6676ebd152..0000000000 --- a/src/contexts/Modal/index.tsx +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useTxFees } from 'contexts/TxFees'; -import React, { useEffect, useState } from 'react'; -import { defaultModalContext } from './defaults'; -import { ModalConfig, ModalContextInterface, ModalContextState } from './types'; - -// default modal content -const DEFAULT_MODAL_COMPONENT = 'ConnectAccounts'; - -export const ModalContext = - React.createContext<ModalContextInterface>(defaultModalContext); - -export const useModal = () => React.useContext(ModalContext); - -// wrapper component to provide components with context -export const ModalProvider = ({ children }: { children: React.ReactNode }) => { - const { notEnoughFunds } = useTxFees(); - - const [state, setState] = useState<ModalContextState>({ - status: 0, - modal: DEFAULT_MODAL_COMPONENT, - config: {}, - size: 'large', - height: 0, - resize: 0, - }); - - useEffect(() => { - setResize(); - }, [state.status, notEnoughFunds]); - - const setStatus = (newStatus: number) => { - const _state = { - ...state, - status: newStatus, - resize: state.resize + 1, - height: newStatus === 0 ? 0 : state.height, - }; - setState(_state); - }; - - const openModalWith = ( - modal: string, - _config: ModalConfig = {}, - size = 'large' - ) => { - setState({ - ...state, - modal, - status: 1, - config: _config, - size, - resize: state.resize + 1, - }); - }; - - const setModalHeight = (h: number) => { - if (state.status === 0) return; - // set maximum height to 80% of window height - const maxHeight = window.innerHeight * 0.8; - h = h > maxHeight ? maxHeight : h; - setState({ - ...state, - height: h, - }); - }; - - const setResize = () => { - // increments resize to trigger a height transition - setState({ - ...state, - resize: state.resize + 1, - }); - }; - - return ( - <ModalContext.Provider - value={{ - status: state.status, - setStatus, - openModalWith, - setModalHeight, - setResize, - modal: state.modal, - config: state.config, - size: state.size, - height: state.height, - resize: state.resize, - }} - > - {children} - </ModalContext.Provider> - ); -}; diff --git a/src/contexts/Modal/types.ts b/src/contexts/Modal/types.ts deleted file mode 100644 index ed5b252db3..0000000000 --- a/src/contexts/Modal/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export interface ModalContextInterface extends ModalContextState { - setStatus: (status: number) => void; - openModalWith: (modal: string, config?: ModalConfig, size?: string) => void; - setModalHeight: (v: number) => void; - setResize: () => void; -} - -export interface ModalContextState { - status: number; - modal: string; - config: ModalConfig; - size: string; - height: number; - resize: number; -} - -export type ModalConfig = Record<string, string | any>; diff --git a/src/contexts/Network/defaults.ts b/src/contexts/Network/defaults.ts index 3066d85c43..19514ffb5b 100644 --- a/src/contexts/Network/defaults.ts +++ b/src/contexts/Network/defaults.ts @@ -4,7 +4,7 @@ import { NetworkList } from 'config/networks'; export const defaultNetworkContext = { - network: NetworkList.polkadot.name, - networkData: NetworkList.polkadot, + network: NetworkList.cereMainnet.name, + networkData: NetworkList.cereMainnet, switchNetwork: () => {}, }; diff --git a/src/contexts/Overlay/defaults.tsx b/src/contexts/Overlay/defaults.tsx index c666f3f0e9..70c979ffd3 100644 --- a/src/contexts/Overlay/defaults.tsx +++ b/src/contexts/Overlay/defaults.tsx @@ -1,7 +1,7 @@ // Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { OverlayContextInterface } from './types'; +import type { OverlayContextInterface } from './types'; export const defaultOverlayContext: OverlayContextInterface = { // eslint-disable-next-line diff --git a/src/contexts/Overlay/index.tsx b/src/contexts/Overlay/index.tsx index 5625d3653a..6a24a6841b 100644 --- a/src/contexts/Overlay/index.tsx +++ b/src/contexts/Overlay/index.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { defaultOverlayContext } from './defaults'; -import { OverlayContextInterface } from './types'; +import type { OverlayContextInterface } from './types'; export const OverlayContext = React.createContext<OverlayContextInterface>( defaultOverlayContext diff --git a/src/contexts/Overlay/types.ts b/src/contexts/Overlay/types.ts index 2ffe9ea121..0bc5cf0d85 100644 --- a/src/contexts/Overlay/types.ts +++ b/src/contexts/Overlay/types.ts @@ -1,8 +1,8 @@ // Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React from 'react'; -import { MaybeString } from 'types'; +import type React from 'react'; +import type { MaybeString } from 'types'; export interface OverlayContextInterface { openOverlayWith: (o: React.ReactNode | null, s?: string) => void; diff --git a/src/contexts/SessionEra/defaults.ts b/src/contexts/SessionEra/defaults.ts deleted file mode 100644 index 930e114dd2..0000000000 --- a/src/contexts/SessionEra/defaults.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { SessionEra, SessionEraContextInterface } from './types'; - -export const sessionEra: SessionEra = { - eraLength: 0, - eraProgress: 0, - sessionLength: 0, - sessionProgress: 0, - sessionsPerEra: 0, -}; - -export const defaultSessionEraContext: SessionEraContextInterface = { - getEraTimeLeft: () => 0, - sessionEra, -}; diff --git a/src/contexts/SessionEra/index.tsx b/src/contexts/SessionEra/index.tsx deleted file mode 100644 index 7ca14ce5f0..0000000000 --- a/src/contexts/SessionEra/index.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { getUnixTime } from 'date-fns'; -import React, { useEffect, useRef, useState } from 'react'; -import { AnyApi } from 'types'; -import { setStateWithRef } from 'Utils'; -import { useApi } from '../Api'; -import * as defaults from './defaults'; -import { SessionEra, SessionEraContextInterface } from './types'; - -export const SessionEraContext = - React.createContext<SessionEraContextInterface>( - defaults.defaultSessionEraContext - ); - -// Warning: Do not use this hook in heavy components. -// Using this hook in a component makes the component rerender per each new block. -export const useSessionEra = () => React.useContext(SessionEraContext); - -export const SessionEraProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const { isReady, api, status, consts } = useApi(); - const { expectedBlockTime } = consts; - - useEffect(() => { - if (status === 'connecting') { - setStateWithRef(defaults.sessionEra, setSessionEra, sessionEraRef); - } - }, [status]); - - // store network metrics in state - const [sessionEra, setSessionEra] = useState<SessionEra>(defaults.sessionEra); - const sessionEraRef = useRef(sessionEra); - - const [unsub, setUnsub] = useState<AnyApi>(null); - const unsubRef = useRef(unsub); - - // manage unsubscribe - useEffect(() => { - subscribeToSessionProgress(); - return () => { - if (unsubRef.current !== null) { - unsubRef.current(); - } - }; - }, [isReady]); - - // active subscription - const subscribeToSessionProgress = async () => { - if (isReady && api !== null) { - const _unsub = await api.derive.session.progress((session) => { - setStateWithRef( - { - eraLength: session.eraLength.toNumber(), - eraProgress: session.eraProgress.toNumber(), - sessionLength: session.sessionLength.toNumber(), - sessionProgress: session.sessionProgress.toNumber(), - sessionsPerEra: session.sessionsPerEra.toNumber(), - }, - setSessionEra, - sessionEraRef - ); - }); - setStateWithRef(_unsub, setUnsub, unsubRef); - } - }; - - const getEraTimeLeft = () => { - const eraBlocksLeft = - sessionEraRef.current.eraLength - sessionEraRef.current.eraProgress; - const eraTimeLeftSeconds = eraBlocksLeft * (expectedBlockTime * 0.001); - - const unixTime = getUnixTime(new Date()); - const eventTime = unixTime + eraTimeLeftSeconds; - const diffTime = eventTime - unixTime; - return diffTime; - }; - - return ( - <SessionEraContext.Provider - value={{ - getEraTimeLeft, - sessionEra, - }} - > - {children} - </SessionEraContext.Provider> - ); -}; diff --git a/src/contexts/SessionEra/types.ts b/src/contexts/SessionEra/types.ts deleted file mode 100644 index fed0d9eda1..0000000000 --- a/src/contexts/SessionEra/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export interface SessionEraContextInterface { - getEraTimeLeft: () => number; - sessionEra: SessionEra; -} - -export interface SessionEra { - eraLength: number; - eraProgress: number; - sessionLength: number; - sessionProgress: number; - sessionsPerEra: number; -} diff --git a/src/contexts/Subscan/defaults.ts b/src/contexts/Subscan/defaults.ts deleted file mode 100644 index bd76de363a..0000000000 --- a/src/contexts/Subscan/defaults.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { SubscanContextInterface } from './types'; - -export const defaultSubscanContext: SubscanContextInterface = { - // eslint-disable-next-line - fetchEraPoints: (v, e) => {}, - payouts: [], - poolClaims: [], -}; diff --git a/src/contexts/Subscan/index.tsx b/src/contexts/Subscan/index.tsx deleted file mode 100644 index f3642ee4c4..0000000000 --- a/src/contexts/Subscan/index.tsx +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ApiEndpoints, ApiSubscanKey } from 'consts'; -import { UIContextInterface } from 'contexts/UI/types'; -import React, { useEffect, useState } from 'react'; -import { AnyApi, AnySubscan } from 'types'; -import { useApi } from '../Api'; -import { useConnect } from '../Connect'; -import { useUi } from '../UI'; -import { defaultSubscanContext } from './defaults'; -import { SubscanContextInterface } from './types'; - -export const SubscanContext = React.createContext<SubscanContextInterface>( - defaultSubscanContext -); - -export const useSubscan = () => React.useContext(SubscanContext); - -export const SubscanProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const { network, isReady } = useApi(); - const { services, getServices }: UIContextInterface = useUi(); - const { activeAccount } = useConnect(); - - // store fetched payouts from Subscan - const [payouts, setPayouts] = useState<AnySubscan>([]); - - // store fetched pool claims from Subscan - const [poolClaims, setPoolClaims] = useState<AnySubscan>([]); - - // reset payouts on network switch - useEffect(() => { - setPayouts([]); - setPoolClaims([]); - }, [network]); - - // fetch payouts as soon as network is ready - useEffect(() => { - if (isReady) { - fetchPayouts(); - fetchPoolClaims(); - } - }, [isReady, network, activeAccount]); - - // fetch payouts on services toggle - useEffect(() => { - fetchPayouts(); - fetchPoolClaims(); - }, [services]); - - /* fetchPayouts - * fetches payout history from Subscan. - * Fetches a total of 300 records from 3 asynchronous requests. - * Also checks if subscan service is active *after* the fetch has resolved - * as the user could have turned off the service while payouts were fetching. - * Stores resulting payouts in context state. - */ - const fetchPayouts = async () => { - if (activeAccount === null || !services.includes('subscan')) { - setPayouts([]); - return; - } - - // fetch 2 pages of results if subscan is enabled - if (getServices().includes('subscan')) { - let _payouts: Array<AnySubscan> = []; - - // fetch 3 pages of results - const results = await Promise.all([ - handleFetch(activeAccount, 0, ApiEndpoints.subscanRewardSlash, { - is_stash: true, - claimed_filter: 'claimed', - }), - handleFetch(activeAccount, 1, ApiEndpoints.subscanRewardSlash, { - is_stash: true, - claimed_filter: 'claimed', - }), - ]); - - // user may have turned off service while results were fetching. - // test again whether subscan service is still active. - if (getServices().includes('subscan')) { - for (const result of results) { - if (!result?.data?.list) { - break; - } - // ensure no payouts have block_timestamp of 0 - const list = result.data.list.filter( - (l: AnyApi) => l.block_timestamp !== 0 - ); - _payouts = _payouts.concat(list); - } - setPayouts(_payouts); - } - } - }; - - /* fetchPoolClaims - * fetches claim history from Subscan. - * Fetches a total of 300 records from 3 asynchronous requests. - * Also checks if subscan service is active *after* the fetch has resolved - * as the user could have turned off the service while payouts were fetching. - * Stores resulting claims in context state. - */ - const fetchPoolClaims = async () => { - if (activeAccount === null || !services.includes('subscan')) { - setPoolClaims([]); - return; - } - - // fetch 2 pages of results if subscan is enabled - if (getServices().includes('subscan')) { - let _poolClaims: Array<AnySubscan> = []; - - // fetch 3 pages of results - const results = await Promise.all([ - handleFetch(activeAccount, 0, ApiEndpoints.subscanPoolRewards), - handleFetch(activeAccount, 1, ApiEndpoints.subscanPoolRewards), - ]); - - // user may have turned off service while results were fetching. - // test again whether subscan service is still active. - if (getServices().includes('subscan')) { - for (const result of results) { - // check incorrectly formatted result object - if (!result?.data?.list) { - break; - } - // check list has records - if (!result.data.list.length) { - break; - } - // ensure no payouts have block_timestamp of 0 - const list = result.data.list.filter( - (l: AnyApi) => l.block_timestamp !== 0 - ); - _poolClaims = _poolClaims.concat(list); - } - setPoolClaims(_poolClaims); - } - } - }; - - /* fetchEraPoints - * fetches recent era point history for a particular address. - * Also checks if subscan service is active *after* the fetch has resolved - * as the user could have turned off the service while payouts were fetching. - * returns eraPoints - */ - const fetchEraPoints = async (address: string, era: number) => { - if (address === '' || !services.includes('subscan')) { - return []; - } - - const res = await handleFetch(address, 0, ApiEndpoints.subscanEraStat); - - if (res.message === 'Success') { - if (getServices().includes('subscan')) { - if (res.data?.list !== null) { - const list = []; - for (let i = era; i > era - 100; i--) { - list.push({ - era: i, - reward_point: - res.data.list.find((item: AnySubscan) => item.era === i) - ?.reward_point ?? 0, - }); - } - // removes last zero item and returns - return list.reverse().splice(0, list.length - 1); - } - return []; - } - } - return []; - }; - - /* handleFetch - * utility to handle a fetch request to Subscan - * returns resulting JSON. - */ - const handleFetch = async ( - address: string, - page: number, - endpoint: string, - body: AnyApi = {} - ): Promise<AnySubscan> => { - const bodyJson = { - row: 100, - page, - address, - ...body, - }; - const res: Response = await fetch(network.subscanEndpoint + endpoint, { - headers: { - 'Content-Type': 'application/json', - 'X-API-Key': ApiSubscanKey, - }, - body: JSON.stringify(bodyJson), - method: 'POST', - }); - const resJson: AnySubscan = await res.json(); - return resJson; - }; - - return ( - <SubscanContext.Provider - value={{ - fetchEraPoints, - payouts, - poolClaims, - }} - > - {children} - </SubscanContext.Provider> - ); -}; diff --git a/src/contexts/Subscan/types.ts b/src/contexts/Subscan/types.ts deleted file mode 100644 index 752ab7b5d7..0000000000 --- a/src/contexts/Subscan/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { AnySubscan } from 'types'; - -export interface SubscanContextInterface { - fetchEraPoints: (v: string, e: number) => void; - payouts: AnySubscan; - poolClaims: AnySubscan; -} diff --git a/src/contexts/Tips/defaults.ts b/src/contexts/Tips/defaults.ts index fef1900078..c05f7ff595 100644 --- a/src/contexts/Tips/defaults.ts +++ b/src/contexts/Tips/defaults.ts @@ -1,7 +1,7 @@ // Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { TipsContextInterface } from './types'; +import type { TipsContextInterface } from './types'; export const defaultTipsContext: TipsContextInterface = { // eslint-disable-next-line diff --git a/src/contexts/Tips/index.tsx b/src/contexts/Tips/index.tsx index 6855355190..263140201e 100644 --- a/src/contexts/Tips/index.tsx +++ b/src/contexts/Tips/index.tsx @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 import React, { useEffect, useState } from 'react'; -import { MaybeString } from 'types'; +import type { MaybeString } from 'types'; import { defaultTipsContext } from './defaults'; -import { TipsContextInterface } from './types'; +import type { TipsContextInterface } from './types'; export const TipsContext = React.createContext<TipsContextInterface>(defaultTipsContext); diff --git a/src/contexts/Tips/types.ts b/src/contexts/Tips/types.ts index 3530088ac9..caa7c66613 100644 --- a/src/contexts/Tips/types.ts +++ b/src/contexts/Tips/types.ts @@ -1,7 +1,7 @@ // Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { MaybeString } from 'types'; +import type { MaybeString } from 'types'; export interface TipsContextInterface { openTipWith: (d: MaybeString, c: any) => void; diff --git a/src/contexts/TxFees/defaults.ts b/src/contexts/TxFees/defaults.ts deleted file mode 100644 index ae37120671..0000000000 --- a/src/contexts/TxFees/defaults.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import BN from 'bn.js'; -import { EstimatedFeeContext } from '.'; - -export const defaultTxFees: EstimatedFeeContext = { - txFees: new BN(0), - notEnoughFunds: false, - // eslint-disable-next-line - setTxFees: (f) => {}, - resetTxFees: () => {}, - // eslint-disable-next-line - setSender: (s) => {}, - txFeesValid: false, -}; diff --git a/src/contexts/TxFees/index.tsx b/src/contexts/TxFees/index.tsx deleted file mode 100644 index 0e527ee451..0000000000 --- a/src/contexts/TxFees/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import BN from 'bn.js'; -import { useConnect } from 'contexts/Connect'; -import { useTransferOptions } from 'contexts/TransferOptions'; -import React, { useEffect, useState } from 'react'; -import { MaybeAccount } from 'types'; -import * as defaults from './defaults'; - -export interface EstimatedFeeContext { - txFees: BN; - notEnoughFunds: boolean; - setTxFees: (f: BN) => void; - resetTxFees: () => void; - setSender: (s: MaybeAccount) => void; - txFeesValid: boolean; -} - -export const TxFeesContext = React.createContext<EstimatedFeeContext>( - defaults.defaultTxFees -); - -export const useTxFees = () => React.useContext(TxFeesContext); - -export const TxFeesProvider = ({ children }: { children: React.ReactNode }) => { - const { activeAccount } = useConnect(); - const { getTransferOptions } = useTransferOptions(); - - // store the transaction fees for the transaction. - const [txFees, _setTxFees] = useState(new BN(0)); - - // store the sender of the transaction - const [sender, setSender] = useState<MaybeAccount>(activeAccount); - - // store whether the sender does not have enough funds. - const [notEnoughFunds, setNotEnoughFunds] = useState(false); - - useEffect(() => { - const { freeBalance } = getTransferOptions(sender); - setNotEnoughFunds(freeBalance.sub(txFees).lt(new BN(0))); - }, [txFees, sender]); - - const setTxFees = (fees: BN) => { - _setTxFees(fees); - }; - - const resetTxFees = () => { - setSender(null); - _setTxFees(new BN(0)); - }; - - const txFeesValid = (() => { - if (txFees.isZero() || notEnoughFunds) { - return false; - } - return true; - })(); - - return ( - <TxFeesContext.Provider - value={{ - txFees, - notEnoughFunds, - setTxFees, - resetTxFees, - setSender, - txFeesValid, - }} - > - {children} - </TxFeesContext.Provider> - ); -}; diff --git a/src/contexts/Validators/defaults.ts b/src/contexts/Validators/defaults.ts deleted file mode 100644 index d3439fd101..0000000000 --- a/src/contexts/Validators/defaults.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ValidatorsContextInterface } from 'contexts/Validators/types'; - -export const sessionValidators = { - list: [], - unsub: null, -}; - -export const sessionParachainValidators = { - list: [], - unsub: null, -}; - -export const defaultValidatorsContext: ValidatorsContextInterface = { - // eslint-disable-next-line - fetchValidatorMetaBatch: (k, v, r) => {}, - // eslint-disable-next-line - removeValidatorMetaBatch: (k) => {}, - // eslint-disable-next-line - fetchValidatorPrefs: async (v) => null, - // eslint-disable-next-line - addFavorite: (a) => {}, - // eslint-disable-next-line - removeFavorite: (a) => {}, - validators: [], - avgCommission: 0, - meta: {}, - session: sessionValidators, - sessionParachain: [], - favorites: [], - nominated: null, - poolNominated: null, - favoritesList: null, - validatorCommunity: [], -}; diff --git a/src/contexts/Validators/index.tsx b/src/contexts/Validators/index.tsx deleted file mode 100644 index 79aa19d519..0000000000 --- a/src/contexts/Validators/index.tsx +++ /dev/null @@ -1,669 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import BN from 'bn.js'; -import { VALIDATOR_COMMUNITY } from 'config/validators'; -import { MinBondPrecision } from 'consts'; -import { - SessionParachainValidators, - SessionValidators, - Validator, - ValidatorAddresses, - ValidatorsContextInterface, -} from 'contexts/Validators/types'; -import React, { useEffect, useRef, useState } from 'react'; -import { AnyApi, AnyMetaBatch, Fn } from 'types'; -import { - planckBnToUnit, - removePercentage, - rmCommas, - setStateWithRef, - shuffle, - sleep, - toFixedIfNecessary, -} from 'Utils'; -import { useApi } from '../Api'; -import { useBalances } from '../Balances'; -import { useConnect } from '../Connect'; -import { useNetworkMetrics } from '../Network'; -import { useActivePools } from '../Pools/ActivePools'; -import * as defaults from './defaults'; - -export const ValidatorsContext = - React.createContext<ValidatorsContextInterface>( - defaults.defaultValidatorsContext - ); - -export const useValidators = () => React.useContext(ValidatorsContext); - -// wrapper component to provide components with context -export const ValidatorsProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const { isReady, api, network, consts } = useApi(); - const { activeAccount } = useConnect(); - const { metrics } = useNetworkMetrics(); - const { accounts, getAccountNominations } = useBalances(); - const { poolNominations } = useActivePools(); - const { units } = network; - const { maxNominatorRewardedPerValidator } = consts; - const { activeEra } = metrics; - - // stores the total validator entries - const [validators, setValidators] = useState<Array<Validator>>([]); - - // track whether the validator list has been fetched yet - const [fetchedValidators, setFetchedValidators] = useState<number>(0); - - // stores the currently active validator set - const [sessionValidators, setSessionValidators] = useState<SessionValidators>( - defaults.sessionValidators - ); - - // stores the average network commission rate - const [avgCommission, setAvgCommission] = useState(0); - - // stores the currently active parachain validator set - const [sessionParachainValidators, setSessionParachainValidators] = - useState<SessionParachainValidators>(defaults.sessionParachainValidators); - - // stores the meta data batches for validator lists - const [validatorMetaBatches, setValidatorMetaBatch] = useState<AnyMetaBatch>( - {} - ); - const validatorMetaBatchesRef = useRef(validatorMetaBatches); - - // stores the meta batch subscriptions for validator lists - const [validatorSubs, setValidatorSubs] = useState<{ - [key: string]: Array<Fn>; - }>({}); - const validatorSubsRef = useRef(validatorSubs); - - // get favorites from local storage - const getFavorites = () => { - const _favorites = localStorage.getItem( - `${network.name.toLowerCase()}_favorites` - ); - return _favorites !== null ? JSON.parse(_favorites) : []; - }; - - // stores the user's favorite validators - const [favorites, setFavorites] = useState<string[]>(getFavorites()); - - // stores the user's nominated validators as list - const [nominated, setNominated] = useState<Array<Validator> | null>(null); - - // stores the nominated validators by the members pool's as list - const [poolNominated, setPoolNominated] = useState<Array<Validator> | null>( - null - ); - - // stores the user's favorites validators as list - const [favoritesList, setFavoritesList] = useState<Array<Validator> | null>( - null - ); - - // stores validator community - - const [validatorCommunity] = useState<any>([...shuffle(VALIDATOR_COMMUNITY)]); - - // reset validators list on network change - useEffect(() => { - setFetchedValidators(0); - setSessionValidators(defaults.sessionValidators); - setSessionParachainValidators(defaults.sessionParachainValidators); - removeValidatorMetaBatch('validators_browse'); - setAvgCommission(0); - setValidators([]); - }, [network]); - - // fetch validators and session validators when activeEra ready - useEffect(() => { - if (isReady) { - fetchValidators(); - subscribeSessionValidators(api); - } - - return () => { - // unsubscribe from any validator meta batches - Object.values(validatorSubsRef.current).map((batch: AnyMetaBatch) => { - return Object.entries(batch).map(([, v]: AnyApi) => { - return v(); - }); - }); - }; - }, [isReady, activeEra]); - - // fetch parachain session validators when earliestStoredSession ready - useEffect(() => { - if (isReady) { - subscribeParachainValidators(api); - } - }, [isReady]); - - // pre-populating validator meta batches. Needed for generating nominations - useEffect(() => { - if (validators.length > 0) { - fetchValidatorMetaBatch('validators_browse', validators, true); - } - }, [isReady, validators]); - - // fetch active account's nominations in validator list format - useEffect(() => { - if (isReady && activeAccount) { - fetchNominatedList(); - } - }, [isReady, activeAccount, accounts]); - - const fetchNominatedList = async () => { - if (!activeAccount) { - return; - } - - console.log(`Active account: ${activeAccount}`); - // get raw targets list - const targets = getAccountNominations(activeAccount); - - // format to list format - const targetsFormatted = targets.map((item: any) => { - return { address: item }; - }); - console.log(`Fetching validators from nominated list:`); - console.log(targetsFormatted); - // fetch preferences - const nominationsWithPrefs = await fetchValidatorPrefs(targetsFormatted); - - if (nominationsWithPrefs) { - setNominated(nominationsWithPrefs); - } else { - setNominated([]); - } - }; - - // fetch active account's pool nominations in validator list format - useEffect(() => { - if (isReady && poolNominations) { - fetchPoolNominatedList(); - } - }, [isReady, poolNominations]); - - const fetchPoolNominatedList = async () => { - // get raw nominations list - let n = poolNominations.targets; - console.log(`Raw nominations list:`); - console.log(n); - // format to list format - n = n.map((item: string) => { - return { address: item }; - }); - // fetch preferences - console.log(`Fetching validators from pool nominated list: ${n}`); - const nominationsWithPrefs = await fetchValidatorPrefs(n); - if (nominationsWithPrefs) { - setPoolNominated(nominationsWithPrefs); - } else { - setPoolNominated([]); - } - }; - - // re-fetch favorites on network change - useEffect(() => { - setFavorites(getFavorites()); - }, [network]); - - // fetch favorites in validator list format - useEffect(() => { - if (isReady) { - fetchFavoriteList(); - } - }, [isReady, favorites]); - - const fetchFavoriteList = async () => { - // format to list format - const _favorites = [...favorites].map((item: string) => { - return { address: item }; - }); - console.log(`Favourite validators:`); - console.log(_favorites); - // // fetch preferences - const favoritesWithPrefs = await fetchValidatorPrefs(_favorites); - if (favoritesWithPrefs) { - setFavoritesList(favoritesWithPrefs); - } else { - setFavoritesList([]); - } - }; - - /* - * Fetches the active validator set. - * Validator meta batches are derived from this initial list. - */ - const fetchValidators = async () => { - if (!isReady || !api) { - return; - } - - // return if fetching not started - if ([1, 2].includes(fetchedValidators)) { - return; - } - - setFetchedValidators(1); - - // fetch validator set - const v: Array<Validator> = []; - let totalNonAllCommission: BN = new BN(0); - const exposures = await api.query.staking.validators.entries(); - exposures.forEach(([_args, _prefs]: AnyApi) => { - const address = _args.args[0].toHuman(); - const prefs = _prefs.toHuman(); - - const _commission = removePercentage(prefs.commission); - if (_commission !== 100) { - totalNonAllCommission = totalNonAllCommission.add(new BN(_commission)); - } - - v.push({ - address, - prefs: { - commission: parseFloat(_commission.toFixed(2)), - blocked: prefs.blocked, - }, - }); - }); - - // get average network commission for all non-100% commissioned validators. - const nonCommissionCount = exposures.filter( - (e: AnyApi) => e.commission !== '100%' - ).length; - - const _avgCommission = nonCommissionCount - ? toFixedIfNecessary( - totalNonAllCommission.toNumber() / nonCommissionCount, - 2 - ) - : 0; - - setFetchedValidators(2); - setAvgCommission(_avgCommission); - // shuffle validators before setting them. - setValidators(shuffle(v)); - }; - - /* - * subscribe to active session - */ - const subscribeSessionValidators = async (_api: AnyApi) => { - if (isReady) { - const unsub = await _api.query.session.validators( - (_validators: AnyApi) => { - setSessionValidators({ - ...sessionValidators, - list: _validators.toHuman(), - unsub, - }); - } - ); - } - }; - - /* - * subscribe to active parachain validators - */ - const subscribeParachainValidators = async (_api: AnyApi) => { - if (isReady) { - const unsub = await _api.query.session.validators( - // earliestStoredSession.toString(), - (_validators: AnyApi) => { - setSessionParachainValidators({ - ...sessionParachainValidators, - list: _validators.toHuman(), - unsub, - }); - } - ); - } - }; - - /* - * fetches prefs for a list of validators - */ - const fetchValidatorPrefs = async (_validators: ValidatorAddresses) => { - if (!_validators.length || !api) { - return null; - } - - const v: string[] = []; - for (const _v of _validators) { - v.push(_v.address); - } - - const prefsAll = await api.query.staking.validators.multi(v); - - const validatorsWithPrefs = []; - let i = 0; - for (const _prefs of prefsAll) { - const prefs: AnyApi = _prefs.toHuman(); - - const commission = removePercentage(prefs?.commission ?? '0%'); - - validatorsWithPrefs.push({ - address: v[i], - prefs: { - commission, - blocked: prefs.blocked, - }, - }); - i++; - } - return validatorsWithPrefs; - }; - - /* - Fetches a new batch of subscribed validator metadata. Stores the returning - metadata alongside the unsubscribe function in state. - structure: - { - key: { - [ - { - addresses [], - identities: [], - } - ] - }, - }; - */ - const fetchValidatorMetaBatch = async ( - key: string, - v: AnyMetaBatch, - refetch = false - ) => { - if (!isReady || !api) { - return; - } - - if (!v.length) { - return; - } - - if (!refetch) { - // if already exists, do not re-fetch - if (validatorMetaBatchesRef.current[key] !== undefined) { - return; - } - } else { - // tidy up if existing batch exists - delete validatorMetaBatches[key]; - delete validatorMetaBatchesRef.current[key]; - - if (validatorSubsRef.current[key] !== undefined) { - for (const unsub of validatorSubsRef.current[key]) { - unsub(); - } - } - } - - const addresses = []; - for (const _v of v) { - addresses.push(_v.address); - } - - // store batch addresses - const batchesUpdated = Object.assign(validatorMetaBatchesRef.current); - batchesUpdated[key] = {}; - batchesUpdated[key].addresses = addresses; - setStateWithRef( - { ...batchesUpdated }, - setValidatorMetaBatch, - validatorMetaBatchesRef - ); - - const subscribeToIdentities = async (addr: AnyApi) => { - const unsub = await api.query.identity.identityOf.multi<AnyApi>( - addr, - (_identities) => { - const identities = []; - for (let i = 0; i < _identities.length; i++) { - identities.push(_identities[i].toHuman()); - } - const _batchesUpdated = Object.assign( - validatorMetaBatchesRef.current - ); - - // check if batch still exists before updating - if (_batchesUpdated[key]) { - _batchesUpdated[key].identities = identities; - setStateWithRef( - { ..._batchesUpdated }, - setValidatorMetaBatch, - validatorMetaBatchesRef - ); - } - } - ); - return unsub; - }; - - const subscribeToSuperIdentities = async (addr: AnyApi) => { - const unsub = await api.query.identity.superOf.multi<AnyApi>( - addr, - async (_supers) => { - // determine where supers exist - const supers: AnyApi = []; - const supersWithIdentity: AnyApi = []; - - for (let i = 0; i < _supers.length; i++) { - const _super = _supers[i].toHuman(); - supers.push(_super); - if (_super !== null) { - supersWithIdentity.push(i); - } - } - - // get supers one-off multi query - const query = supers - .filter((s: AnyApi) => s !== null) - .map((s: AnyApi) => s[0]); - - const temp = await api.query.identity.identityOf.multi<AnyApi>( - query, - (_identities) => { - for (let j = 0; j < _identities.length; j++) { - const _identity = _identities[j].toHuman(); - // inject identity into super array - supers[supersWithIdentity[j]].identity = _identity; - } - } - ); - temp(); - - const _batchesUpdated = Object.assign( - validatorMetaBatchesRef.current - ); - - // check if batch still exists before updating - if (_batchesUpdated[key]) { - _batchesUpdated[key].supers = supers; - setStateWithRef( - { ..._batchesUpdated }, - setValidatorMetaBatch, - validatorMetaBatchesRef - ); - } - } - ); - return unsub; - }; - - await Promise.all([ - subscribeToIdentities(addresses), - subscribeToSuperIdentities(addresses), - ]).then((unsubs: Array<Fn>) => { - addMetaBatchUnsubs(key, unsubs); - }); - - // intentional throttle to prevent slow render updates. - await sleep(250); - - // subscribe to validator nominators - const args: AnyApi = []; - - for (let i = 0; i < v.length; i++) { - args.push([activeEra.index, v[i].address]); - } - - const unsub3 = await api.query.staking.erasStakers.multi<AnyApi>( - args, - (_validators) => { - const stake = []; - - for (let _validator of _validators) { - _validator = _validator.toHuman(); - let others = _validator.others ?? []; - - // account for yourself being an additional nominator - const totalNominations = others.length + 1; - - // get lowest active stake for the validator - others = others.sort((a: AnyApi, b: AnyApi) => { - const x = new BN(rmCommas(a.value)); - const y = new BN(rmCommas(b.value)); - return x.sub(y); - }); - - const lowestActive = - others.length > 0 - ? toFixedIfNecessary( - planckBnToUnit(new BN(rmCommas(others[0].value)), units), - MinBondPrecision - ) - : 0; - - // get the lowest reward stake of the validator, which is - // the largest index - `maxNominatorRewardedPerValidator`, - // or the first index if does not exist. - const lowestRewardIndex = Math.max( - others.length - maxNominatorRewardedPerValidator, - 0 - ); - - const lowestReward = - others.length > 0 - ? toFixedIfNecessary( - planckBnToUnit( - new BN(rmCommas(others[lowestRewardIndex]?.value)), - units - ), - MinBondPrecision - ) - : 0; - - stake.push({ - total: _validator.total, - own: _validator.own, - total_nominations: totalNominations, - lowest: lowestActive, - lowestReward, - }); - } - - // commit update - const _batchesUpdated = Object.assign(validatorMetaBatchesRef.current); - - // check if batch still exists before updating - if (_batchesUpdated[key]) { - _batchesUpdated[key].stake = stake; - setStateWithRef( - { ..._batchesUpdated }, - setValidatorMetaBatch, - validatorMetaBatchesRef - ); - } - } - ); - - addMetaBatchUnsubs(key, [unsub3]); - }; - - /* - * Helper function to add mataBatch unsubs by key. - */ - const addMetaBatchUnsubs = (key: string, unsubs: Array<Fn>) => { - const _unsubs = validatorSubsRef.current; - const _keyUnsubs = _unsubs[key] ?? []; - - _keyUnsubs.push(...unsubs); - _unsubs[key] = _keyUnsubs; - setStateWithRef(_unsubs, setValidatorSubs, validatorSubsRef); - }; - - const removeValidatorMetaBatch = (key: string) => { - if (validatorSubsRef.current[key] !== undefined) { - // ubsubscribe from updates - for (const unsub of validatorSubsRef.current[key]) { - unsub(); - } - // wipe data - delete validatorMetaBatches[key]; - delete validatorMetaBatchesRef.current[key]; - } - }; - - /* - * Adds a favorite validator. - */ - const addFavorite = (address: string) => { - const _favorites: any = Object.assign(favorites); - if (!_favorites.includes(address)) { - _favorites.push(address); - } - - localStorage.setItem( - `${network.name.toLowerCase()}_favorites`, - JSON.stringify(_favorites) - ); - setFavorites([..._favorites]); - }; - - /* - * Removes a favorite validator if they exist. - */ - const removeFavorite = (address: string) => { - let _favorites = Object.assign(favorites); - _favorites = _favorites.filter( - (validator: string) => validator !== address - ); - localStorage.setItem( - `${network.name.toLowerCase()}_favorites`, - JSON.stringify(_favorites) - ); - setFavorites([..._favorites]); - }; - - return ( - <ValidatorsContext.Provider - value={{ - fetchValidatorMetaBatch, - removeValidatorMetaBatch, - fetchValidatorPrefs, - addFavorite, - removeFavorite, - validators, - avgCommission, - meta: validatorMetaBatchesRef.current, - session: sessionValidators, - sessionParachain: sessionParachainValidators.list, - favorites, - nominated, - poolNominated, - favoritesList, - validatorCommunity, - }} - > - {children} - </ValidatorsContext.Provider> - ); -}; diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 66c68843bd..0000000000 --- a/src/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import '@rossbulat/polkadot-dashboard-ui/index.css'; -import App from 'App'; -import 'index.css'; -import { createRoot } from 'react-dom/client'; -import reportWebVitals from './reportWebVitals'; - -const rootElement = document.getElementById('root'); -if (!rootElement) throw new Error('Failed to find the root element'); -const root = createRoot(rootElement); - -root.render(<App />); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); diff --git a/src/library/Account/index.tsx b/src/library/Account/index.tsx deleted file mode 100644 index 38037b71cc..0000000000 --- a/src/library/Account/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faGlasses } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useConnect } from 'contexts/Connect'; -import { useTheme } from 'contexts/Themes'; -import { useEffect, useState } from 'react'; -import { defaultThemes } from 'theme/default'; -import { clipAddress, convertRemToPixels } from 'Utils'; -import Identicon from '../Identicon'; -import { AccountProps } from './types'; -import Wrapper from './Wrapper'; - -export const Account = ({ - filled = false, - fontSize = '1.05rem', - format, - value, - label, - readOnly, - canClick, - title, - onClick, -}: AccountProps) => { - const { mode } = useTheme(); - const { getAccount } = useConnect(); - const [displayValue, setDisplayValue] = useState<string | undefined>(); - - const unassigned = value === null || value === undefined || !value.length; - - useEffect(() => { - // format value based on `format` prop - switch (format) { - case 'name': - setDisplayValue( - value !== '' ? getAccount(value)?.name : clipAddress(value) - ); - break; - case 'text': - setDisplayValue(value); - break; - default: - if (value) setDisplayValue(clipAddress(value)); - } - - // if title prop is provided, override `displayValue` - if (title !== undefined) setDisplayValue(title); - }, [value, title]); - - return ( - <Wrapper - whileHover={{ scale: 1.01 }} - onClick={onClick} - cursor={canClick ? 'pointer' : 'default'} - fill={filled ? defaultThemes.buttons.secondary.background[mode] : 'none'} - fontSize={fontSize} - > - {label !== undefined && ( - <div className="account-label"> - {label}{' '} - {readOnly && ( - <> -   - <FontAwesomeIcon icon={faGlasses} /> - </> - )} - </div> - )} - - {unassigned ? ( - <span className="title unassigned">Not Staking</span> - ) : ( - <> - {format !== 'text' && ( - <span className="identicon"> - <Identicon - value={value} - size={convertRemToPixels(fontSize) * 1.4} - /> - </span> - )} - <span className="title">{displayValue}</span> - </> - )} - </Wrapper> - ); -}; - -export default Account; diff --git a/src/library/Button/index.tsx b/src/library/Button/index.tsx deleted file mode 100644 index 4d0619d5e3..0000000000 --- a/src/library/Button/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { motion } from 'framer-motion'; -import styled from 'styled-components'; -import { - buttonSecondaryBackground, - networkColor, - networkColorSecondary, - textSecondary, -} from 'theme'; -import { ButtonProps, ButtonWrapperProps } from './types'; - -export const ButtonRow = styled.div<{ verticalSpacing?: boolean }>` - display: flex; - align-items: center; - justify-content: flex-start; - margin-top: ${(props) => (props.verticalSpacing ? '1rem' : 0)}; -`; - -export const Wrapper = styled(motion.div)<ButtonWrapperProps>` - display: inline-block; - margin: ${(props) => props.margin}; - - > button { - display: flex; - flex-flow: row nowrap; - align-items: center; - background: ${(props) => - props.type === 'invert-primary' - ? networkColor - : props.type === 'invert-secondary' - ? networkColorSecondary - : buttonSecondaryBackground}; - color: ${(props) => - props.type === 'invert-primary' || props.type === 'invert-secondary' - ? 'white' - : textSecondary}; - - padding: ${(props) => props.padding}; - border-radius: 1.5rem; - font-size: ${(props) => props.fontSize}; - transition: opacity 0.2s; - - .space { - margin-right: 0.6rem; - } - - &:disabled { - cursor: default; - opacity: 0.25; - } - } -`; - -export const Button = (props: ButtonProps) => { - let { transform } = props; - const { primary, secondary, icon, title, disabled, small, inline } = props; - const { onClick } = props; - - transform = transform ?? 'shrink-1'; - - const type = primary - ? 'invert-primary' - : secondary - ? 'invert-secondary' - : 'default'; - - return ( - <Wrapper - whileHover={{ scale: !disabled ? 1.02 : 1 }} - whileTap={{ scale: !disabled ? 0.98 : 1 }} - type={type} - margin={inline ? '0' : '0 0.5rem'} - padding={small ? '0.42rem 0.9rem' : '0.52rem 1.2rem'} - fontSize={small ? '1rem' : '1.15rem'} - > - <button - type="button" - disabled={disabled} - onClick={() => onClick !== undefined && onClick()} - > - {icon && ( - <FontAwesomeIcon - icon={icon} - className={title ? 'space' : undefined} - transform={transform} - /> - )} - {title && title} - </button> - </Wrapper> - ); -}; - -export default Button; diff --git a/src/library/Button/types.ts b/src/library/Button/types.ts deleted file mode 100644 index b79c29e183..0000000000 --- a/src/library/Button/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { IconProp } from '@fortawesome/fontawesome-svg-core'; - -export interface ButtonProps { - onClick?: () => void; - primary?: boolean; - secondary?: boolean; - inline?: boolean; - small?: boolean; - disabled?: boolean; - icon?: IconProp; - transform?: string; - title: string; -} - -export interface ButtonWrapperProps { - margin: string; - type: string; - padding: string; - fontSize: string; -} diff --git a/src/library/Filter/Category.tsx b/src/library/Filter/Category.tsx index 7bde3d2c1e..b5451b79e2 100644 --- a/src/library/Filter/Category.tsx +++ b/src/library/Filter/Category.tsx @@ -1,7 +1,7 @@ // Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { CategoryProps } from './types'; +import type { CategoryProps } from './types'; export const Category = (props: CategoryProps) => { const { title } = props; diff --git a/src/library/Filter/context.tsx b/src/library/Filter/context.tsx deleted file mode 100644 index dc435bc46c..0000000000 --- a/src/library/Filter/context.tsx +++ /dev/null @@ -1,389 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { u8aToString, u8aUnwrapBytes } from '@polkadot/util'; -import { useApi } from 'contexts/Api'; -import { useValidators } from 'contexts/Validators'; -import React, { useState } from 'react'; -import * as defaults from './defaults'; -import { ValidatorFilterContextInterface } from './types'; - -// Note: This context should wrap both the filter component and UI in question for displaying the filtered list. -export const ValidatorFilterContext = - React.createContext<ValidatorFilterContextInterface>(defaults.defaultContext); - -export const useValidatorFilter = () => - React.useContext(ValidatorFilterContext); - -export const ValidatorFilterProvider = ({ - children, -}: { - children: React.ReactNode; -}) => { - const { consts } = useApi(); - const { meta, session, sessionParachain } = useValidators(); - const { maxNominatorRewardedPerValidator } = consts; - - // store validator filters that are currently active - const [validatorFilters, setValidatorFilters]: any = useState([]); - - // store the validator ordering method that is currently active - const [validatorOrder, setValidatorOrder]: any = useState('default'); - - const setValidatorsOrder = (by: string) => { - setValidatorOrder(by); - }; - - const setValidatorsFilter = (filter: any) => { - setValidatorFilters(filter); - }; - - /* - * toggleAllValidaorFilters - * Either turns all filters on or all filters off. - * This does not use the 'in_session' filter. - */ - const toggleAllValidatorFilters = (toggle: number) => { - if (toggle) { - setValidatorsFilter([ - 'all_commission', - 'blocked_nominations', - 'over_subscribed', - 'missing_identity', - 'inactive', - ]); - } else { - setValidatorFilters([]); - } - }; - - /* - * toggleFilterValidators - * Toggles a validator filter. If the filer is currently active, - * it is removed. State updates accordingly. - */ - const toggleFilterValidators = (f: string) => { - const filter = [...validatorFilters]; - const action = filter.includes(f) ? 'remove' : 'push'; - - if (action === 'remove') { - const index = filter.indexOf(f); - filter.splice(index, 1); - } else { - filter.push(f); - } - setValidatorsFilter(filter); - }; - - /* - * applyValidatorFilters - * Takes a list, batchKey and which filter should be applied - * to the data. The filter function in question is called, - * that is dependent on the filter key. - * The updated filtered list is returned. - * - */ - const applyValidatorFilters = ( - list: any, - batchKey: string, - filter: any = validatorFilters - ) => { - if (filter.includes('all_commission')) { - list = filterAllCommission(list); - } - if (filter.includes('blocked_nominations')) { - list = filterBlockedNominations(list); - } - if (filter.includes('over_subscribed')) { - list = filterOverSubscribed(list, batchKey); - } - if (filter.includes('missing_identity')) { - list = filterMissingIdentity(list, batchKey); - } - if (filter.includes('inactive')) { - list = filterInactive(list); - } - if (filter.includes('not_parachain_validator')) { - list = filterNonParachainValidator(list); - } - if (filter.includes('in_session')) { - list = filterInSession(list); - } - return list; - }; - - /* - * resetValidatorFilters - * Resets filters and ordering to the default state. - */ - const resetValidatorFilters = () => { - setValidatorFilters([]); - setValidatorOrder('default'); - }; - - /* - * filterMissingIdentity - * Iterates through the supplied list and refers to the meta - * batch of the list to filter those list items with - * missing identities. - * Returns the updated filtered list. - */ - const filterMissingIdentity = (list: any, batchKey: string) => { - if (meta[batchKey] === undefined) { - return list; - } - const filteredList: any = []; - for (const validator of list) { - const addressBatchIndex = - meta[batchKey].addresses?.indexOf(validator.address) ?? -1; - - // if we cannot derive data, fallback to include validator in filtered list - if (addressBatchIndex === -1) { - filteredList.push(validator); - continue; - } - - const identities = meta[batchKey]?.identities ?? []; - const supers = meta[batchKey]?.supers ?? []; - - // push validator if sync has not completed - if (!identities.length || !supers.length) { - filteredList.push(validator); - } - - const identityExists = identities[addressBatchIndex] ?? null; - const superExists = supers[addressBatchIndex] ?? null; - - // validator included if identity or super identity has been set - if (identityExists !== null || superExists !== null) { - filteredList.push(validator); - continue; - } - } - return filteredList; - }; - - /* - * filterOverSubscribed - * Iterates through the supplied list and refers to the meta - * batch of the list to filter those list items that are - * over subscribed. - * Returns the updated filtered list. - */ - const filterOverSubscribed = (list: any, batchKey: string) => { - if (meta[batchKey] === undefined) { - return list; - } - const filteredList: any = []; - for (const validator of list) { - const addressBatchIndex = - meta[batchKey].addresses?.indexOf(validator.address) ?? -1; - - // if we cannot derive data, fallback to include validator in filtered list - if (addressBatchIndex === -1) { - filteredList.push(validator); - continue; - } - const stake = meta[batchKey]?.stake ?? false; - if (!stake) { - filteredList.push(validator); - continue; - } - const totalNominations = stake[addressBatchIndex].total_nominations ?? 0; - if (totalNominations < maxNominatorRewardedPerValidator) { - filteredList.push(validator); - continue; - } - } - return filteredList; - }; - - /* - * filterAllCommission - * Filters the supplied list and removes items with 100% commission. - * Returns the updated filtered list. - */ - const filterAllCommission = (list: any) => { - list = list.filter( - (validator: any) => validator?.prefs?.commission !== 100 - ); - return list; - }; - - /* - * filterBlockedNominations - * Filters the supplied list and removes items that have blocked nominations. - * Returns the updated filtered list. - */ - const filterBlockedNominations = (list: any) => { - list = list.filter((validator: any) => validator?.prefs?.blocked !== true); - return list; - }; - - /* - * filterInactive - * Filters the supplied list and removes items that are inactive. - * Returns the updated filtered list. - */ - const filterInactive = (list: any) => { - // if list has not yet been populated, return original list - if (session.list.length === 0) { - return list; - } - list = list.filter((validator: any) => - session.list.includes(validator.address) - ); - return list; - }; - - /* - * filterNonParachainValidator - * Filters the supplied list and removes items that are inactive. - * Returns the updated filtered list. - */ - const filterNonParachainValidator = (list: any) => { - // if list has not yet been populated, return original list - if ((sessionParachain?.length ?? 0) === 0) { - return list; - } - list = list.filter((validator: any) => - sessionParachain.includes(validator.address) - ); - return list; - }; - - /* - * filterInSession - * Filters the supplied list and removes items that are in the current session. - * Returns the updated filtered list. - */ - const filterInSession = (list: any) => { - // if list has not yet been populated, return original list - if (session.list.length === 0) { - return list; - } - list = list.filter( - (validator: any) => !session.list.includes(validator.address) - ); - return list; - }; - - /* - * orderValidators - * Sets the ordering key for orderValidators - */ - const orderValidators = (by: string) => { - const order = validatorOrder === by ? 'default' : by; - setValidatorsOrder(order); - }; - - /* - * orderValidators - * Applies an ordering function to the validator list. - * Returns the updated ordered list. - */ - const applyValidatorOrder = (list: any, order: string) => { - if (order === 'commission') { - return orderLowestCommission(list); - } - return list; - }; - - /* - * orderLowestCommission - * Orders a list by commission. - * Returns the updated ordered list. - */ - const orderLowestCommission = (list: any) => { - const orderedList = [...list].sort( - (a: any, b: any) => a.prefs.commission - b.prefs.commission - ); - return orderedList; - }; - - /* - * validatorSearchFilter - * Iterates through the supplied list and refers to the meta - * batch of the list to filter those list items that match - * the search term. - * Returns the updated filtered list. - */ - const validatorSearchFilter = ( - list: any, - batchKey: string, - searchTerm: string - ) => { - if (meta[batchKey] === undefined) { - return list; - } - const filteredList: any = []; - for (const validator of list) { - const batchIndex = - meta[batchKey].addresses?.indexOf(validator.address) ?? -1; - - // if we cannot derive data, fallback to include validator in filtered list - if (batchIndex === -1) { - filteredList.push(validator); - continue; - } - const identities = meta[batchKey]?.identities ?? false; - if (!identities) { - filteredList.push(validator); - continue; - } - const supers = meta[batchKey]?.supers ?? false; - if (!supers) { - filteredList.push(validator); - continue; - } - - const address = meta[batchKey].addresses[batchIndex]; - - const identity = identities[batchIndex] ?? ''; - const identityRaw = identity?.info?.display?.Raw ?? ''; - const identityAsBytes = u8aToString(u8aUnwrapBytes(identityRaw)); - const identitySearch = ( - identityAsBytes === '' ? identityRaw : identityAsBytes - ).toLowerCase(); - - const superIdentity = supers[batchIndex] ?? null; - const superIdentityRaw = - superIdentity?.identity?.info?.display?.Raw ?? ''; - const superIdentityAsBytes = u8aToString( - u8aUnwrapBytes(superIdentityRaw) - ); - const superIdentitySearch = ( - superIdentityAsBytes === '' ? superIdentityRaw : superIdentityAsBytes - ).toLowerCase(); - - if (address.toLowerCase().includes(searchTerm.toLowerCase())) { - filteredList.push(validator); - } - if ( - identitySearch.includes(searchTerm.toLowerCase()) || - superIdentitySearch.includes(searchTerm.toLowerCase()) - ) { - filteredList.push(validator); - } - } - return filteredList; - }; - - return ( - <ValidatorFilterContext.Provider - value={{ - orderValidators, - applyValidatorOrder, - applyValidatorFilters, - resetValidatorFilters, - toggleFilterValidators, - toggleAllValidatorFilters, - validatorSearchFilter, - validatorFilters, - validatorOrder, - }} - > - {children} - </ValidatorFilterContext.Provider> - ); -}; diff --git a/src/library/Form/AccountDropdown/Wrappers.ts b/src/library/Form/AccountDropdown/Wrappers.ts deleted file mode 100644 index e72f783adf..0000000000 --- a/src/library/Form/AccountDropdown/Wrappers.ts +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; -import { - backgroundDropdown, - borderPrimary, - borderSecondary, - textPrimary, - textSecondary, -} from 'theme'; - -export const StyledDownshift = styled.div` - position: relative; - width: 100%; - height: auto; - overflow: hidden; - - .label { - margin: 0.25rem 0 0.75rem 0; - } - .current { - flex: 1; - display: flex; - align-items: center; - margin-bottom: 1rem; - - > span { - margin: 0 0.75rem; - color: ${textSecondary}; - opacity: 0.5; - } - } - - /* input element of dropdown */ - .input-wrap { - border-bottom: 1px solid ${borderPrimary}; - display: flex; - flex-flow: row wrap; - align-items: center; - padding: 0.25rem 0 0 0; - margin: 0.25rem 0.7rem 0 0.7rem; - flex: 1; - - &.selected { - border: 1px solid ${borderPrimary}; - border-radius: 1rem; - margin: 0; - padding: 0.1rem 0.75rem; - } - } - - /* input element of dropdown */ - .input { - border: none; - padding-left: 0.75rem; - flex: 1; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } -`; - -export const StyledController = styled.button<any>` - color: ${textPrimary}; - border: none; - position: absolute; - right: 0.5rem; - top: 0.4rem; - width: 2.2rem; - height: 2.2rem; - display: flex; - flex-flow: column wrap; - justify-content: center; - align-items: center; - border-radius: 0.5rem; -`; - -/* dropdown box for vertical scroll */ -export const StyledDropdown = styled.div<any>` - background: ${backgroundDropdown}; - position: relative; - margin: 0.5rem 0 0; - border-bottom: none; - border-radius: 0.75rem; - z-index: 1; - - .items { - width: auto; - height: ${(props) => (props.height ? props.height : 'auto')}; - overflow: auto; - - .item { - padding: 0.5rem; - cursor: pointer; - margin: 0.25rem; - border-radius: 0.75rem; - display: flex; - flex-flow: row wrap; - justify-content: flex-start; - align-items: center; - - .icon { - margin-right: 0.5rem; - } - span { - color: ${textSecondary}; - border: 1px solid ${borderSecondary}; - border-radius: 0.5rem; - padding: 0.2rem 0.5rem; - font-size: 0.9rem; - margin-right: 0.5rem; - } - p { - font-size: 1rem; - color: ${textPrimary}; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - flex: 1; - } - } - } -`; diff --git a/src/library/Form/AccountDropdown/index.tsx b/src/library/Form/AccountDropdown/index.tsx deleted file mode 100644 index 240cb062a5..0000000000 --- a/src/library/Form/AccountDropdown/index.tsx +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faAnglesRight, faTimes } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useApi } from 'contexts/Api'; -import { useTheme } from 'contexts/Themes'; -import { useCombobox, UseComboboxStateChange } from 'downshift'; -import Identicon from 'library/Identicon'; -import { useEffect, useState } from 'react'; -import { defaultThemes, networkColors } from 'theme/default'; -import { convertRemToPixels } from 'Utils'; -import { AccountDropdownProps, InputItem } from '../types'; -import { StyledController, StyledDownshift, StyledDropdown } from './Wrappers'; - -export const AccountDropdown = ({ - items, - onChange, - placeholder, - value, - current, - height, -}: AccountDropdownProps) => { - // store input items - const [inputItems, setInputItems] = useState<Array<InputItem>>(items); - - useEffect(() => { - setInputItems(items); - }, [items]); - - const itemToString = (item: InputItem) => { - const name = item?.name ?? ''; - return name; - }; - - const c = useCombobox({ - items: inputItems, - itemToString, - onSelectedItemChange: onChange, - initialSelectedItem: value, - onInputValueChange: ({ inputValue }: UseComboboxStateChange<InputItem>) => { - setInputItems( - items.filter((item: InputItem) => - inputValue - ? item?.name?.toLowerCase().startsWith(inputValue?.toLowerCase()) - : true - ) - ); - }, - }); - - return ( - <StyledDownshift> - <div> - <div className="label" {...c.getLabelProps()}> - Currently Selected: - </div> - <div> - <div className="current"> - <div className="input-wrap selected"> - {current !== null && ( - <Identicon - value={current?.address ?? ''} - size={convertRemToPixels('2rem')} - /> - )} - <input className="input" disabled value={current?.name ?? ''} /> - </div> - <span> - <FontAwesomeIcon icon={faAnglesRight} /> - </span> - <div className="input-wrap selected"> - {value !== null && ( - <Identicon - value={value?.address ?? ''} - size={convertRemToPixels('2rem')} - /> - )} - <input className="input" disabled value={value?.name ?? '...'} /> - </div> - </div> - - <StyledDropdown {...c.getMenuProps()} height={height}> - <div className="input-wrap" {...c.getComboboxProps()}> - <input {...c.getInputProps({ placeholder })} className="input" /> - </div> - - <div className="items"> - {c.selectedItem && ( - <StyledController - onClick={() => c.reset()} - aria-label="clear selection" - > - <FontAwesomeIcon transform="grow-2" icon={faTimes} /> - </StyledController> - )} - - {inputItems.map((item: InputItem, index: number) => ( - <DropdownItem - key={`controller_acc_${index}`} - c={c} - item={item} - index={index} - /> - ))} - </div> - </StyledDropdown> - </div> - </div> - </StyledDownshift> - ); -}; - -const DropdownItem = ({ c, item, index }: any) => { - const { network } = useApi(); - const { mode } = useTheme(); - - let color; - let border; - - if (c.selectedItem === item) { - color = networkColors[`${network.name}-${mode}`]; - border = `2px solid ${networkColors[`${network.name}-${mode}`]}`; - } else { - color = defaultThemes.text.primary[mode]; - border = `2px solid ${defaultThemes.transparent[mode]}`; - } - - // disable item in list if account doesn't satisfy controller budget. - const itemProps = item.active - ? c.getItemProps({ key: item.name, index, item }) - : {}; - const opacity = item.active ? 1 : 0.5; - const cursor = item.active ? 'pointer' : 'default'; - - return ( - <div - className="item" - {...itemProps} - style={{ color, border, opacity, cursor }} - > - <div className="icon"> - <Identicon value={item.address} size={26} /> - </div> - {!item.active && <span>Not Enough {network.unit}</span>} - <p>{item.name}</p> - </div> - ); -}; - -export default AccountDropdown; diff --git a/src/library/Form/AccountSelect/Wrappers.ts b/src/library/Form/AccountSelect/Wrappers.ts deleted file mode 100644 index b858cedbe9..0000000000 --- a/src/library/Form/AccountSelect/Wrappers.ts +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; -import { backgroundToggle, borderPrimary, textPrimary } from 'theme'; - -export const StyledDownshift = styled.div<any>` - position: relative; - width: 100%; - height: ${(props) => (props.height ? props.height : 'auto')}; - overflow: hidden; - - /* title of dropdown */ - .label { - margin: 1rem 0; - display: block; - } - - /* input element of dropdown */ - .input-wrap { - border: 1px solid ${borderPrimary}; - display: flex; - flex-flow: row wrap; - align-items: center; - border-radius: 1rem; - padding: 0.1rem 0.75rem; - margin: 0.25rem 0; - } - - .input { - border: none; - padding-left: 0.75rem; - flex-grow: 1; - } -`; - -export const StyledController = styled.button<any>` - color: ${textPrimary}; - position: absolute; - right: 0.5rem; - top: 0.6rem; - width: 2.2rem; - height: 2.2rem; - display: flex; - flex-flow: column wrap; - justify-content: center; - align-items: center; -`; - -/* dropdown box for horizontal scroll */ -export const StyledSelect = styled.div` - border: 1px solid ${borderPrimary}; - position: relative; - margin: 0.75rem 0 0; - width: 100%; - border-radius: 1rem; - z-index: 1; - height: 170px; - padding: 0.25rem; - overflow: auto; - display: flex; - flex-flow: row wrap; - flex: 1; - border-radius: 1rem; - - .wrapper { - position: relative; - min-width: 240px; - height: 125px; - flex: 1 1 20%; - max-width: 20%; - padding: 0.35rem; - - .item { - background: ${backgroundToggle}; - position: relative; - width: 100%; - height: 100%; - padding: 0.65rem 1rem; - cursor: pointer; - border-radius: 1rem; - display: flex; - flex-flow: column wrap; - justify-content: center; - align-items: flex-start; - flex-grow: 1; - overflow: hidden; - - > .title { - display: flex; - flex-flow: row wrap; - max-width: 100%; - - h3 { - display: inline-block; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - } - - &:first-child { - margin-left: 0rem; - } - &:last-child { - margin-right: 0rem; - } - p { - color: ${textPrimary}; - margin: 0.15rem 0 0; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - flex: 1; - } - .icon { - margin-bottom: 0.7rem; - } - } - } -`; diff --git a/src/library/Form/AccountSelect/index.tsx b/src/library/Form/AccountSelect/index.tsx deleted file mode 100644 index d3ad43bab3..0000000000 --- a/src/library/Form/AccountSelect/index.tsx +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faTimes } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useApi } from 'contexts/Api'; -import { useTheme } from 'contexts/Themes'; -import { useCombobox, UseComboboxStateChange } from 'downshift'; -import Identicon from 'library/Identicon'; -import { StatusLabel } from 'library/StatusLabel'; -import { useEffect, useState } from 'react'; -import { defaultThemes, networkColors } from 'theme/default'; -import { clipAddress, convertRemToPixels } from 'Utils'; -import { AccountSelectProps, InputItem } from '../types'; -import { StyledController, StyledDownshift, StyledSelect } from './Wrappers'; - -export const AccountSelect = ({ - items, - onChange, - placeholder, - value, -}: AccountSelectProps) => { - const [inputItems, setInputItems] = useState<Array<InputItem>>(items); - - useEffect(() => { - setInputItems(items); - }, [items]); - - const itemToString = (item: InputItem) => { - const name = item?.name ?? ''; - return name; - }; - - const c = useCombobox({ - items: inputItems, - itemToString, - onSelectedItemChange: onChange, - initialSelectedItem: value, - onInputValueChange: ({ inputValue }: UseComboboxStateChange<InputItem>) => { - setInputItems( - items.filter((item: InputItem) => - inputValue - ? item?.name?.toLowerCase().startsWith(inputValue?.toLowerCase()) - : true - ) - ); - }, - }); - - return ( - <StyledDownshift> - <div> - <div style={{ position: 'relative' }}> - <div className="input-wrap" {...c.getComboboxProps()}> - {value !== null && ( - <Identicon - value={value?.address ?? ''} - size={convertRemToPixels('2rem')} - /> - )} - <input {...c.getInputProps({ placeholder })} className="input" /> - </div> - - {c.selectedItem && ( - <StyledController - onClick={() => c.reset()} - aria-label="clear selection" - > - <FontAwesomeIcon transform="grow-4" icon={faTimes} /> - </StyledController> - )} - <StyledSelect {...c.getMenuProps()}> - {inputItems.map((item: InputItem, index: number) => ( - <DropdownItem - key={`controller_acc_${index}`} - c={c} - item={item} - index={index} - /> - ))} - </StyledSelect> - </div> - </div> - </StyledDownshift> - ); -}; - -const DropdownItem = ({ c, item, index }: any) => { - const { network } = useApi(); - const { mode } = useTheme(); - - // disable item in list if account doesn't satisfy controller budget. - const itemProps = item.active ? c.getItemProps({ index, item }) : {}; - - const color = - c.selectedItem?.address === item?.address - ? networkColors[`${network.name}-${mode}`] - : defaultThemes.text.primary[mode]; - - const border = - c.selectedItem?.address === item?.address - ? `2px solid ${networkColors[`${network.name}-${mode}`]}` - : `2px solid ${defaultThemes.transparent[mode]}`; - - const opacity = item.active ? 1 : 0.1; - - return ( - <div className="wrapper" key={item.name} {...itemProps}> - {!item.active && ( - <StatusLabel - status="not_enough_unit" - title={item.alert} - topOffset="40%" - helpKey="Controller Account Eligibility" - hideIcon - /> - )} - <div - className="item" - style={{ - color, - border, - opacity, - }} - > - <div className="icon"> - <Identicon value={item.address} size={40} /> - </div> - <div className="title"> - <h3 style={{ color }}>{item.name}</h3> - </div> - <p>{clipAddress(item.address)}</p> - </div> - </div> - ); -}; - -export default AccountSelect; diff --git a/src/library/Form/Dropdown/index.tsx b/src/library/Form/Dropdown/index.tsx deleted file mode 100644 index 4589122ece..0000000000 --- a/src/library/Form/Dropdown/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faAnglesRight } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useApi } from 'contexts/Api'; -import { useTheme } from 'contexts/Themes'; -import { useCombobox } from 'downshift'; -import { useState } from 'react'; -import { defaultThemes, networkColors } from 'theme/default'; -import { StyledDownshift, StyledDropdown } from '../AccountDropdown/Wrappers'; -import { DropdownInput, DropdownProps } from '../types'; - -export const Dropdown = ({ - items, - onChange, - label, - placeholder, - value, - current, -}: DropdownProps) => { - const [inputItems, setInputItems] = useState<Array<DropdownInput>>(items); - - const itemToString = (item: DropdownInput | null) => item?.name ?? ''; - - const c = useCombobox({ - items: inputItems, - itemToString, - onSelectedItemChange: onChange, - initialSelectedItem: value, - onInputValueChange: () => { - setInputItems(items); - }, - }); - - return ( - <StyledDownshift> - <div> - {label ? ( - <div className="label" {...c.getLabelProps()}> - {label} - </div> - ) : null} - <div style={{ position: 'relative' }}> - <div className="current"> - <div className="input-wrap selected"> - <input className="input" disabled value={current?.name ?? ''} /> - </div> - <span> - <FontAwesomeIcon icon={faAnglesRight} /> - </span> - <div className="input-wrap selected" {...c.getComboboxProps()}> - <input - className="input" - disabled - {...c.getInputProps({ placeholder })} - value={value?.name ?? '...'} - /> - </div> - </div> - - <StyledDropdown {...c.getMenuProps()}> - <div className="items"> - {inputItems.map((item: DropdownInput, index: number) => ( - <DropdownItem - key={`controller_acc_${index}`} - c={c} - item={item} - index={index} - /> - ))} - </div> - </StyledDropdown> - </div> - </div> - </StyledDownshift> - ); -}; - -const DropdownItem = ({ c, item, index }: any) => { - const { name } = useApi().network; - const { mode } = useTheme(); - const color = - c.selectedItem?.key === item.key - ? networkColors[`${name}-${mode}`] - : defaultThemes.text.primary[mode]; - const border = - c.selectedItem?.key === item.key - ? `2px solid ${networkColors[`${name}-${mode}`]}` - : `2px solid ${defaultThemes.transparent[mode]}`; - - return ( - <div - className="item" - {...c.getItemProps({ key: item.name, index, item })} - style={{ - color, - border, - }} - > - <p>{item.name}</p> - </div> - ); -}; - -export default Dropdown; diff --git a/src/library/Form/Utils/getEligibleControllers.tsx b/src/library/Form/Utils/getEligibleControllers.tsx deleted file mode 100644 index 0e32c6cdee..0000000000 --- a/src/library/Form/Utils/getEligibleControllers.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import BN from 'bn.js'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { ImportedAccount } from 'contexts/Connect/types'; -import { useEffect, useState } from 'react'; -import { planckBnToUnit } from 'Utils'; -import { InputItem } from '../types'; - -export const getEligibleControllers = (): Array<InputItem> => { - const { network } = useApi(); - const { activeAccount, accounts: connectAccounts } = useConnect(); - const { - isController, - existentialAmount, - getAccountBalance, - accounts: balanceAccounts, - } = useBalances(); - - const [accounts, setAccounts] = useState<Array<InputItem>>([]); - - useEffect(() => { - setAccounts(filterAccounts()); - }, [activeAccount, connectAccounts, balanceAccounts]); - - const filterAccounts = () => { - // remove read only accounts - let _accounts = connectAccounts.filter((acc: ImportedAccount) => { - return acc?.source !== 'external'; - }); - // filter items that are already controller accounts - _accounts = _accounts.filter((acc: ImportedAccount) => { - return !isController(acc?.address ?? null); - }); - - // remove active account from eligible accounts - _accounts = _accounts.filter( - (acc: ImportedAccount) => acc.address !== activeAccount - ); - - // inject balances and whether account can be an active item - let _accountsAsInput: Array<InputItem> = _accounts.map( - (acc: ImportedAccount) => { - const balance = getAccountBalance(acc?.address); - return { - ...acc, - balance, - active: - planckBnToUnit(balance.free, network.units) > - planckBnToUnit(existentialAmount, network.units), - alert: `Not Enough ${network.unit}`, - }; - } - ); - - // sort accounts with at least free balance first - _accountsAsInput = _accountsAsInput.sort((a: InputItem, b: InputItem) => { - const aFree = a?.balance?.free ?? new BN(0); - const bFree = b?.balance?.free ?? new BN(0); - - if (bFree.lt(aFree)) { - return -1; - // eslint-disable-next-line no-else-return - } else { - return bFree.sub(aFree).toNumber(); - } - }); - - return _accountsAsInput; - }; - - return accounts; -}; diff --git a/src/library/Graphs/Bonded.tsx b/src/library/Graphs/Bonded.tsx deleted file mode 100644 index f2734a8834..0000000000 --- a/src/library/Graphs/Bonded.tsx +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ArcElement, Chart as ChartJS, Legend, Tooltip } from 'chart.js'; -import { useApi } from 'contexts/Api'; -import { useTheme } from 'contexts/Themes'; -import { Doughnut } from 'react-chartjs-2'; -import { defaultThemes, networkColors } from 'theme/default'; -import { humanNumber } from 'Utils'; -import { BondedProps } from './types'; -import { GraphWrapper } from './Wrappers'; - -ChartJS.register(ArcElement, Tooltip, Legend); - -export const Bonded = ({ - active, - free, - unlocking, - unlocked, - inactive, -}: BondedProps) => { - const { mode } = useTheme(); - const { network } = useApi(); - - // graph data - let graphActive = active; - let graphUnlocking = unlocking + unlocked; - let graphFree = free; - - let zeroBalance = false; - if (inactive) { - graphActive = -1; - graphUnlocking = -1; - graphFree = -1; - zeroBalance = true; - } - - const options = { - responsive: true, - maintainAspectRatio: false, - spacing: zeroBalance ? 0 : 2, - plugins: { - legend: { - padding: { - right: 20, - }, - display: true, - position: 'left' as const, - align: 'center' as const, - labels: { - padding: 20, - color: defaultThemes.text.primary[mode], - font: { - size: 13, - weight: '600', - }, - }, - }, - tooltip: { - displayColors: false, - backgroundColor: defaultThemes.graphs.tooltip[mode], - titleColor: defaultThemes.text.invert[mode], - bodyColor: defaultThemes.text.invert[mode], - bodyFont: { - weight: '600', - }, - callbacks: { - label: (context: any) => { - if (inactive) { - return 'Inactive'; - } - return `${ - context.parsed === -1 ? 0 : humanNumber(context.parsed) - } ${network.unit}`; - }, - }, - }, - }, - cutout: '75%', - }; - const _colors = zeroBalance - ? [ - defaultThemes.graphs.colors[1][mode], - defaultThemes.graphs.inactive2[mode], - defaultThemes.graphs.inactive[mode], - ] - : [ - networkColors[`${network.name}-${mode}`], - defaultThemes.graphs.colors[0][mode], - defaultThemes.graphs.colors[1][mode], - ]; - - const data = { - labels: ['Active', 'Unlocking', 'Free'], - datasets: [ - { - label: network.unit, - data: [graphActive, graphUnlocking, graphFree], - backgroundColor: _colors, - borderWidth: 0, - }, - ], - }; - - return ( - <GraphWrapper - transparent - noMargin - style={{ border: 'none', boxShadow: 'none' }} - > - <div - className="graph" - style={{ - flex: 0, - paddingRight: '1rem', - height: 160, - }} - > - <Doughnut options={options} data={data} /> - </div> - </GraphWrapper> - ); -}; - -export default Bonded; diff --git a/src/library/Graphs/StatBoxPie.tsx b/src/library/Graphs/StatBoxPie.tsx deleted file mode 100644 index 26db1641f7..0000000000 --- a/src/library/Graphs/StatBoxPie.tsx +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ArcElement, Chart as ChartJS, Tooltip } from 'chart.js'; -import { useApi } from 'contexts/Api'; -import { useTheme } from 'contexts/Themes'; -import { Pie } from 'react-chartjs-2'; -import { - defaultThemes, - networkColors, - networkColorsTransparent, -} from 'theme/default'; -import { StatPieProps } from './types'; - -ChartJS.register(ArcElement, Tooltip); - -export const StatPie = ({ value, value2 }: StatPieProps) => { - // format zero value graph - const isZero = !value && !value; - if (isZero) { - value = 1; - value2 = 0; - } - - const { name } = useApi().network; - const { mode } = useTheme(); - - const borderColor = isZero - ? defaultThemes.buttons.toggle.background[mode] - : [networkColors[`${name}-${mode}`], defaultThemes.transparent[mode]]; - - const backgroundColor = isZero - ? defaultThemes.buttons.toggle.background[mode] - : networkColorsTransparent[`${name}-${mode}`]; - - const options = { - borderColor, - hoverBorderColor: borderColor, - backgroundColor, - hoverBackgroundColor: [ - networkColorsTransparent[`${name}-${mode}`], - defaultThemes.transparent[mode], - ], - responsive: true, - maintainAspectRatio: false, - spacing: 0, - plugins: { - legend: { - display: false, - }, - tooltip: { - enabled: false, - }, - }, - }; - - const data = { - datasets: [ - { - data: [value, value2], - backgroundColor: [ - networkColorsTransparent[`${name}-${mode}`], - defaultThemes.transparent[mode], - ], - borderWidth: 1.6, - }, - ], - }; - - return ( - <div className="graph" style={{ width: 36, height: 36 }}> - <Pie options={options} data={data} /> - </div> - ); -}; - -export default StatPie; diff --git a/src/library/Graphs/Wrappers.ts b/src/library/Graphs/Wrappers.ts deleted file mode 100644 index 5f90ae3efc..0000000000 --- a/src/library/Graphs/Wrappers.ts +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { SideMenuStickyThreshold } from 'consts'; -import styled from 'styled-components'; -import { - backgroundSecondary, - borderPrimary, - cardBorder, - cardShadow, - networkColor, - shadowColor, - textPrimary, - textSecondary, -} from 'theme'; -import { - CardHeaderWrapperProps, - CardWrapperProps, - GraphWrapperProps, -} from './types'; - -/* CardHeaderWrapper - * - * Used as headers for individual cards. Usually a h4 accompanied - * with a h2. withAction allows a full-width header with a right-side - * button. - */ -export const CardHeaderWrapper = styled.div<CardHeaderWrapperProps>` - display: flex; - flex-flow: ${(props) => (props.withAction ? 'row' : 'column')} wrap; - width: 100%; - padding: ${(props) => (props.padded ? '0.5rem 1.2rem' : '0.25rem')}; - - h2, - h3 { - color: ${textPrimary}; - display: flex; - flex-flow: row wrap; - align-items: center; - flex-grow: ${(props) => (props.withAction ? 1 : 0)}; - - .help-icon { - margin-left: 0.6rem; - } - } - h4 { - margin: 0 0 0.6rem 0; - display: flex; - flex-flow: row wrap; - align-items: center; - justify-content: flex-start; - flex-grow: ${(props) => (props.withAction ? 1 : 0)}; - - .help-icon { - margin-left: 0.5rem; - } - } - - > div { - display: flex; - flex-flow: row nowrap; - align-items: center; - } -`; - -/* CardWrapper - * - * Used to separate the main modules throughout the app. - */ -export const CardWrapper = styled.div<CardWrapperProps>` - border: ${cardBorder} ${borderPrimary}; - box-shadow: ${cardShadow} ${shadowColor}; - padding: ${(props) => - props.noPadding ? '0rem' : props.transparent ? '0rem 0rem' : '1.2rem'}; - border-radius: 1.1rem; - background: ${(props) => (props.transparent ? 'none' : backgroundSecondary)}; - display: flex; - flex-flow: column nowrap; - align-content: flex-start; - align-items: flex-start; - flex: 1; - width: 100%; - margin-top: ${(props) => (props.transparent ? '0rem' : '1.4rem')}; - position: relative; - ${(props) => - props.transparent && - ` - border: none; - box-shadow: none; - background: none; - `} - - @media (max-width: ${SideMenuStickyThreshold}px) { - padding: ${(props) => - props.noPadding - ? '0rem' - : props.transparent - ? '0rem 0rem' - : '1rem 0.75rem'}; - } - - @media (min-width: ${SideMenuStickyThreshold + 1}px) { - height: ${(props) => (props.height ? `${props.height}px` : 'inherit')}; - } - - .content { - padding: 0 0.5rem; - - h3 { - margin-bottom: 0.75rem; - } - h4 { - margin-top: 0; - margin-bottom: 0; - } - } - - .inner { - padding: 1rem; - display: flex; - flex-flow: column nowrap; - align-content: flex-start; - align-items: flex-start; - width: 100%; - position: relative; - } - - .option { - border-bottom: 1px solid #ddd; - padding: 0.75rem 1rem; - font-size: 1rem; - text-align: left; - } -`; - -/* GraphWrapper - * - * Acts as a module, but used to wrap graphs. - */ - -export const GraphWrapper = styled.div<GraphWrapperProps>` - border: ${cardBorder} ${borderPrimary}; - box-shadow: ${cardShadow} ${shadowColor}; - border-radius: 1rem; - background: ${backgroundSecondary}; - display: flex; - flex-flow: column nowrap; - align-content: flex-start; - align-items: flex-start; - flex: 1; - position: relative; - overflow: hidden; - margin-top: ${(props) => (props.noMargin ? 0 : '1.4rem')}; - ${(props) => - props.transparent && - ` - border: none; - box-shadow: none; - background: none; - `} - - .inner { - width: 100%; - height: 100%; - } - - .label { - position: absolute; - right: 10px; - top: 10px; - font-size: 0.8rem; - background: ${networkColor}; - border-radius: 0.3rem; - padding: 0.2rem 0.4rem; - color: #fff; - opacity: 0.8; - } - .head { - padding: 0.5rem 1.2rem; - } - - h2 { - .amount { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - } - - h2, - h4 { - margin: 0; - padding: 0.25rem 0 0.5rem 0; - display: flex; - flex-flow: row wrap; - align-content: flex-end; - align-items: flex-end; - justify-content: flex-start; - - .fiat { - color: ${textSecondary}; - font-size: 1.1rem; - margin-top: 0.2rem; - margin-left: 0.3rem; - } - } - h2 { - display: flex; - flex-flow: row wrap; - justify-content: flex-start; - align-items: center; - } - p { - margin: 0.25rem 0 0; - } - h4 { - align-items: center; - margin-top: 0.4rem; - - .help-icon { - margin-left: 0.55rem; - } - } - - h5 { - &.secondary { - color: ${textSecondary}; - opacity: 0.7; - margin-bottom: 0; - margin-top: 1.5rem; - } - } - .small_button { - background: #f1f1f1; - padding: 0.25rem 0.75rem; - border-radius: 1rem; - } - .graph { - position: relative; - flex: ${(props) => (props.flex ? 1 : 0)}; - flex-flow: row wrap; - justify-content: center; - width: 100%; - padding: 1rem 1.5rem; - } - .graph_line { - margin-top: 0.6rem; - padding: 0rem 1rem 0.5rem 0rem; - } - .graph_with_extra { - width: 100%; - display: flex; - flex-flow: row nowrap; - justify-content: flex-start; - align-items: flex-start; - height: 190px; - flex: 1; - - .extra { - flex: 1; - display: flex; - flex-flow: row wrap; - justify-content: flex-end; - align-items: flex-end; - align-content: flex-end; - height: 190px; - border: 1px solid; - } - } - - .change { - margin-left: 0.6rem; - font-size: 0.9rem; - color: white; - border-radius: 0.75rem; - padding: 0.15rem 0.5rem; - &.pos { - background: #3eb955; - } - &.neg { - background: #d2545d; - } - } -`; diff --git a/src/library/Headers/Dropdown.tsx b/src/library/Headers/Dropdown.tsx index d36cb56816..ce38888b83 100644 --- a/src/library/Headers/Dropdown.tsx +++ b/src/library/Headers/Dropdown.tsx @@ -3,7 +3,7 @@ import { useOutsideAlerter } from 'library/Hooks'; import { useRef } from 'react'; -import { DropdownProps } from './types'; +import type { DropdownProps } from './types'; export const Dropdown = ({ toggleMenu, items }: DropdownProps) => { const ref = useRef(null); diff --git a/src/library/Hooks/useInflation/index.tsx b/src/library/Hooks/useInflation/index.tsx index 04eaf8fc5b..efd8b88e80 100644 --- a/src/library/Hooks/useInflation/index.tsx +++ b/src/library/Hooks/useInflation/index.tsx @@ -13,23 +13,13 @@ export const useInflation = () => { const { metrics } = useNetworkMetrics(); const { staking } = useStaking(); const { lastTotalStake } = staking; - const { totalIssuance, auctionCounter } = metrics; + const { totalIssuance } = metrics; - const { - auctionAdjust, - auctionMax, - falloff, - maxInflation, - minInflation, - stakeTarget, - } = params; + const { falloff, maxInflation, minInflation, stakeTarget } = params; const BIGNUMBER_MILLION = new BigNumber(1_000_000); - const calculateInflation = ( - totalStaked: BigNumber, - numAuctions: BigNumber - ) => { + const calculateInflation = (totalStaked: BigNumber) => { const stakedFraction = totalStaked.isZero() || totalIssuance.isZero() ? 0 @@ -37,7 +27,7 @@ export const useInflation = () => { .multipliedBy(BIGNUMBER_MILLION) .dividedBy(totalIssuance) .toNumber() / BIGNUMBER_MILLION.toNumber(); - + // The idealStake is equal to stakeTarget since // Cere Network doesn't provide auctionMax, numAuctions and auctionAdjust so far. const idealStake = stakeTarget; @@ -60,5 +50,5 @@ export const useInflation = () => { }; }; - return calculateInflation(lastTotalStake, auctionCounter); + return calculateInflation(lastTotalStake); }; diff --git a/src/library/Identicon/index.tsx b/src/library/Identicon/index.tsx deleted file mode 100644 index a5bf94f492..0000000000 --- a/src/library/Identicon/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { Identicon as IdenticonDefault } from '@polkadot/react-identicon'; -import styled from 'styled-components'; -import { backgroundIdenticon } from 'theme'; -import { IdenticonProps } from './types'; - -const Wrapper = styled.div` - svg > circle:first-child { - fill: ${backgroundIdenticon}; - } -`; -export const Identicon = ({ value, size }: IdenticonProps) => ( - <Wrapper> - <IdenticonDefault - value={value} - size={size} - theme="polkadot" - style={{ cursor: 'default' }} - /> - </Wrapper> -); - -export default Identicon; diff --git a/src/library/Identicon/types.ts b/src/library/Identicon/types.ts deleted file mode 100644 index 832a86ca64..0000000000 --- a/src/library/Identicon/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export interface IdenticonProps { - value: string; - size?: number; -} diff --git a/src/library/Loaders/Announcement.tsx b/src/library/Loaders/Announcement.tsx deleted file mode 100644 index 3af0884b01..0000000000 --- a/src/library/Loaders/Announcement.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useTheme } from 'contexts/Themes'; -import ContentLoader from 'react-content-loader'; -import { defaultThemes } from 'theme/default'; - -export const Announcement = () => { - const { mode } = useTheme(); - - return ( - <> - <ContentLoader - height={90} - width="100%" - backgroundColor={defaultThemes.loader.background[mode]} - foregroundColor={defaultThemes.loader.foreground[mode]} - opacity={0.2} - style={{ marginTop: '0.75rem', marginBottom: '0.75rem' }} - > - <rect x="0" y="0" rx="0.5rem" ry="0.5rem" width="100%" height="100%" /> - </ContentLoader> - <ContentLoader - height={90} - width="100%" - backgroundColor={defaultThemes.loader.background[mode]} - foregroundColor={defaultThemes.loader.foreground[mode]} - opacity={0.2} - > - <rect x="0" y="0" rx="0.5rem" ry="0.5rem" width="100%" height="100%" /> - </ContentLoader> - </> - ); -}; - -export default Announcement; diff --git a/src/library/Loaders/DataList.tsx b/src/library/Loaders/DataList.tsx deleted file mode 100644 index bbc5586db6..0000000000 --- a/src/library/Loaders/DataList.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import ContentLoader from 'react-content-loader'; - -export const DataList = () => { - return ( - <ContentLoader - height={304} - backgroundColor="#e4e4e4" - foregroundColor="#d3d3d3" - style={{ width: '100%' }} - > - <rect x="583" y="33" rx="2" ry="2" width="123" height="33" /> - <rect x="66" y="49" rx="0" ry="0" width="165" height="11" /> - <rect x="66" y="97" rx="0" ry="0" width="202" height="41" /> - <rect x="284" y="97" rx="0" ry="0" width="202" height="41" /> - <rect x="506" y="96" rx="0" ry="0" width="202" height="41" /> - <rect x="66" y="155" rx="0" ry="0" width="642" height="4" /> - <rect x="66" y="175" rx="0" ry="0" width="642" height="4" /> - <rect x="65" y="195" rx="0" ry="0" width="642" height="4" /> - <rect x="66" y="215" rx="0" ry="0" width="642" height="4" /> - <rect x="66" y="236" rx="0" ry="0" width="642" height="4" /> - <rect x="65" y="256" rx="0" ry="0" width="642" height="4" /> - <rect x="315" y="274" rx="0" ry="0" width="31" height="28" /> - <rect x="356" y="274" rx="0" ry="0" width="31" height="28" /> - <rect x="396" y="274" rx="0" ry="0" width="31" height="28" /> - </ContentLoader> - ); -}; - -export default DataList; diff --git a/src/library/Loaders/Stake.tsx b/src/library/Loaders/Stake.tsx deleted file mode 100644 index c130932f83..0000000000 --- a/src/library/Loaders/Stake.tsx +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useTheme } from 'contexts/Themes'; -import ContentLoader from 'react-content-loader'; -import { defaultThemes } from 'theme/default'; -import { PageRowWrapper } from 'Wrappers'; - -export const Stake = () => { - const { mode } = useTheme(); - - return ( - <> - <PageRowWrapper - className="page-padding" - noVerticalSpacer - style={{ marginTop: '1rem', marginBottom: '1rem' }} - > - <ContentLoader - height={80} - width="100%" - backgroundColor={defaultThemes.loader.background[mode]} - foregroundColor={defaultThemes.loader.foreground[mode]} - opacity={0.2} - style={{ maxWidth: 275, marginRight: '1rem' }} - > - <rect - x="0" - y="0" - rx="0.75rem" - ry="0.75rem" - width="100%" - height="100%" - /> - </ContentLoader> - <ContentLoader - height={80} - width="100%" - backgroundColor={defaultThemes.loader.background[mode]} - foregroundColor={defaultThemes.loader.foreground[mode]} - opacity={0.2} - style={{ maxWidth: 275, marginRight: '1rem' }} - > - <rect - x="0" - y="0" - rx="0.75rem" - ry="0.75rem" - width="100%" - height="100%" - /> - </ContentLoader> - <ContentLoader - height={80} - width="100%" - backgroundColor={defaultThemes.loader.background[mode]} - foregroundColor={defaultThemes.loader.foreground[mode]} - opacity={0.2} - style={{ maxWidth: 275 }} - > - <rect - x="0" - y="0" - rx="0.75rem" - ry="0.75rem" - width="100%" - height="100%" - /> - </ContentLoader> - </PageRowWrapper> - <PageRowWrapper - className="page-padding" - noVerticalSpacer - style={{ marginBottom: '1rem' }} - > - <ContentLoader - height={60} - width="100%" - backgroundColor={defaultThemes.loader.background[mode]} - foregroundColor={defaultThemes.loader.foreground[mode]} - opacity={0.2} - > - <rect - x="0" - y="0" - rx="0.75rem" - ry="0.75rem" - width="100%" - height="100%" - /> - </ContentLoader> - </PageRowWrapper> - <PageRowWrapper - className="page-padding" - noVerticalSpacer - style={{ marginBottom: '1rem' }} - > - <ContentLoader - height={60} - width="100%" - backgroundColor={defaultThemes.loader.background[mode]} - foregroundColor={defaultThemes.loader.foreground[mode]} - opacity={0.2} - > - <rect - x="0" - y="0" - rx="0.75rem" - ry="0.75rem" - width="100%" - height="100%" - /> - </ContentLoader> - </PageRowWrapper> - <PageRowWrapper className="page-padding" noVerticalSpacer> - <ContentLoader - height={60} - width="100%" - backgroundColor={defaultThemes.loader.background[mode]} - foregroundColor={defaultThemes.loader.foreground[mode]} - opacity={0.2} - > - <rect - x="0" - y="0" - rx="0.75rem" - ry="0.75rem" - width="100%" - height="100%" - /> - </ContentLoader> - </PageRowWrapper> - </> - ); -}; - -export default Stake; diff --git a/src/library/OpenHelpIcon/Wrapper.tsx b/src/library/OpenHelpIcon/Wrapper.tsx deleted file mode 100644 index 9813ba04ef..0000000000 --- a/src/library/OpenHelpIcon/Wrapper.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; -import { - buttonHelpBackground, - buttonPrimaryBackground, - networkColor, - textSecondary, -} from 'theme'; - -export const Wrapper = styled.button<{ light?: boolean }>` - background: ${(props) => - props.light ? buttonPrimaryBackground : buttonHelpBackground}; - color: ${textSecondary}; - fill: ${textSecondary}; - display: flex; - flex-flow: row wrap; - align-items: center; - justify-content: center; - border-radius: 50%; - padding: 0.05rem; - transition: all 0.15s; - font-size: 1.15rem; - - &:hover { - fill: ${networkColor}; - } -`; - -export default Wrapper; diff --git a/src/library/OpenHelpIcon/index.tsx b/src/library/OpenHelpIcon/index.tsx deleted file mode 100644 index 36c905efca..0000000000 --- a/src/library/OpenHelpIcon/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useHelp } from 'contexts/Help'; -import { ReactComponent as IconSVG } from 'img/info-outline.svg'; -import { OpenHelpIconProps } from './types'; -import { Wrapper } from './Wrapper'; - -export const OpenHelpIcon = (props: OpenHelpIconProps) => { - const { openHelpWith } = useHelp(); - - const { helpKey } = props; - - const size = props.size ?? '1.3em'; - - return ( - <Wrapper - onClick={() => { - openHelpWith(helpKey, {}); - }} - className="help-icon" - style={{ width: size, height: size }} - light={props.light ?? false} - > - <IconSVG /> - </Wrapper> - ); -}; - -export default OpenHelpIcon; diff --git a/src/library/OpenHelpIcon/types.ts b/src/library/OpenHelpIcon/types.ts deleted file mode 100644 index d2c9e2b495..0000000000 --- a/src/library/OpenHelpIcon/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export interface OpenHelpIconProps { - helpKey: string; - size?: string; - light?: boolean; -} diff --git a/src/library/Overlay/Title.tsx b/src/library/Overlay/Title.tsx deleted file mode 100644 index 4d5a91daf2..0000000000 --- a/src/library/Overlay/Title.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { ButtonInvertRounded } from '@rossbulat/polkadot-dashboard-ui'; -import { useOverlay } from 'contexts/Overlay'; -import { OpenHelpIcon } from 'library/OpenHelpIcon'; -import { FunctionComponent } from 'react'; -import { TitleWrapper } from './Wrappers'; - -interface TitleProps { - title: string; - icon?: IconProp; - Svg?: FunctionComponent<any>; - helpKey?: string; -} - -export const Title = ({ helpKey, title, icon, Svg }: TitleProps) => { - const { closeOverlay } = useOverlay(); - - const graphic = Svg ? ( - <Svg style={{ width: '1.5rem', height: '1.5rem' }} /> - ) : icon ? ( - <FontAwesomeIcon transform="grow-3" icon={icon} /> - ) : null; - - return ( - <TitleWrapper> - <div> - {graphic} - <h2> - {title} - {helpKey && <OpenHelpIcon helpKey={helpKey} />} - </h2> - </div> - <div> - <ButtonInvertRounded text="Done" onClick={() => closeOverlay()} /> - </div> - </TitleWrapper> - ); -}; diff --git a/src/library/Overlay/Wrappers.tsx b/src/library/Overlay/Wrappers.tsx deleted file mode 100644 index 4bf1b868c1..0000000000 --- a/src/library/Overlay/Wrappers.tsx +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; -import { - buttonPrimaryBackground, - cardShadow, - modalBackground, - networkColor, - networkColorStroke, - overlayBackground, - shadowColor, - textPrimary, - textSecondary, -} from 'theme'; - -export const OverlayWrapper = styled.div` - background: ${overlayBackground}; - position: fixed; - width: 100%; - height: 100%; - z-index: 9; - - /* content wrapper */ - > div { - height: 100%; - display: flex; - flex-flow: row wrap; - justify-content: center; - align-items: center; - padding: 1rem 2rem; - - /* click anywhere behind overlay to close */ - .close { - position: fixed; - width: 100%; - height: 100%; - z-index: 8; - cursor: default; - } - } -`; - -export const HeightWrapper = styled.div<{ size: string }>` - box-shadow: ${cardShadow} ${shadowColor}; - transition: height 0.5s cubic-bezier(0.1, 1, 0.2, 1); - width: 100%; - max-width: ${(props) => (props.size === 'small' ? '500px' : '700px')}; - max-height: 100%; - border-radius: 1.5rem; - z-index: 9; - position: relative; - overflow: hidden; -`; - -export const ContentWrapper = styled.div` - background: ${modalBackground}; - width: 100%; - height: auto; - overflow: hidden; - position: relative; - - a { - color: ${networkColor}; - } - .header { - width: 100%; - display: flex; - flex-flow: row wrap; - align-items: center; - padding: 1rem 1rem 0 1rem; - } - .body { - padding: 0.5rem 1.5rem 1rem 1.5rem; - - h4 { - margin: 1rem 0; - } - } -`; - -export const TitleWrapper = styled.div` - padding: 1.5rem 1rem 0 1rem; - display: flex; - flex-flow: row wrap; - justify-content: flex-start; - align-items: center; - width: 100%; - - > div { - display: flex; - flex-flow: row wrap; - align-items: center; - padding: 0 0.5rem; - - button { - padding: 0; - } - - path { - fill: ${textPrimary}; - } - - &:first-child { - flex-grow: 1; - - > h2 { - display: flex; - flex-flow: row nowrap; - align-items: center; - font-family: 'Unbounded', 'sans-serif', sans-serif; - font-size: 1.3rem; - margin: 0; - - > button { - margin-left: 0.85rem; - } - } - > svg { - margin-right: 0.9rem; - } - } - } -`; - -export const FilterListWrapper = styled.div` - padding-bottom: 0.5rem; - - > .body { - button:last-child { - margin-bottom: 0; - } - } -`; - -export const FilterListButton = styled.button<{ active: boolean }>` - border: 1px solid - ${(props) => (props.active ? networkColorStroke : buttonPrimaryBackground)}; - background: ${buttonPrimaryBackground}; - width: 100%; - display: flex; - flex-flow: row wrap; - align-items: center; - border-radius: 1rem; - padding: 0rem 1rem; - margin: 1rem 0; - transition: border 0.1s; - - h4 { - color: ${(props) => (props.active ? networkColorStroke : textSecondary)}; - font-variation-settings: 'wght' 560; - transition: color 0.1s; - margin: 0; - } - - svg { - color: ${(props) => (props.active ? networkColorStroke : textSecondary)}; - opacity: ${(props) => (props.active ? 1 : 0.7)}; - transition: color 0.1s; - margin-left: 0.2rem; - margin-right: 0.9rem; - } -`; diff --git a/src/library/Overlay/index.tsx b/src/library/Overlay/index.tsx deleted file mode 100644 index da193f76bb..0000000000 --- a/src/library/Overlay/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useOverlay } from 'contexts/Overlay'; -import { ContentWrapper, HeightWrapper, OverlayWrapper } from './Wrappers'; - -export const Overlay = () => { - const { closeOverlay, size, status, Overlay: OverlayInner } = useOverlay(); - - if (status === 0) { - return <></>; - } - - return ( - <OverlayWrapper> - <div> - <HeightWrapper size={size}> - <ContentWrapper>{OverlayInner}</ContentWrapper> - </HeightWrapper> - <button type="button" className="close" onClick={() => closeOverlay()}> -   - </button> - </div> - </OverlayWrapper> - ); -}; diff --git a/src/library/PageTitle/index.tsx b/src/library/PageTitle/index.tsx deleted file mode 100644 index 59f338a301..0000000000 --- a/src/library/PageTitle/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faBars } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useEffect, useRef, useState } from 'react'; -import { MenuPaddingWrapper, PageTitleWrapper } from 'Wrappers'; -import { PageTitleProps } from './types'; - -export const PageTitle = (props: PageTitleProps) => { - const { title, button } = props; - const tabs = props.tabs ?? []; - - const [sticky, setSticky] = useState(false); - - const ref = useRef<HTMLElement>(null); - - useEffect(() => { - const cachedRef = ref.current; - const observer = new IntersectionObserver( - ([e]) => { - setSticky(e.intersectionRatio < 1); - }, - { threshold: [1], rootMargin: '-1px 0px 0px 0px' } - ); - - if (cachedRef) { - observer.observe(cachedRef); - } - // unmount - return () => { - if (cachedRef) { - observer.unobserve(cachedRef); - } - }; - }, [sticky]); - - return ( - <> - <MenuPaddingWrapper /> - <PageTitleWrapper ref={ref} sticky={sticky}> - <div className="page-padding"> - <section className="title"> - <div> - <h1>{title}</h1> - </div> - <div> - {button && ( - <button type="button" onClick={() => button.onClick()}> - {button.title} - <FontAwesomeIcon - icon={faBars} - className="icon" - transform="shrink-4" - /> - </button> - )} - </div> - </section> - {tabs.length > 0 && ( - <section className="tabs"> - <div className="scroll"> - <div className="inner"> - {tabs.map((tab: any, i: number) => ( - <button - className={tab.active ? `active` : ``} - key={`page_tab_${i}`} - type="button" - onClick={() => tab.onClick()} - > - {tab.title} - </button> - ))} - </div> - </div> - </section> - )} - </div> - </PageTitleWrapper> - </> - ); -}; - -export default PageTitle; diff --git a/src/library/PageTitle/types.ts b/src/library/PageTitle/types.ts deleted file mode 100644 index a7783ce58b..0000000000 --- a/src/library/PageTitle/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export interface PageTitleProps { - title: string; - tabs?: Array<any>; - button?: { - title: string; - onClick: () => void; - }; -} diff --git a/src/library/PoolAccount/Wrapper.ts b/src/library/PoolAccount/Wrapper.ts deleted file mode 100644 index 696981d7b6..0000000000 --- a/src/library/PoolAccount/Wrapper.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { motion } from 'framer-motion'; -import styled from 'styled-components'; -import { borderSecondary, textSecondary } from 'theme'; -import { WrapperProps } from './types'; - -export const Wrapper = styled(motion.button)<WrapperProps>` - border-radius: 1rem; - box-shadow: none; - display: flex; - flex-flow: row wrap; - justify-content: flex-start; - align-items: center; - cursor: ${(props) => props.cursor}; - background: ${(props) => props.fill}; - font-size: ${(props) => props.fontSize}; - padding: 0 1rem; - max-width: 250px; - flex: 1; - - .identicon { - margin: 0.15rem 0.25rem 0 0; - } - - .account-label { - border-right: 1px solid ${borderSecondary}; - color: ${textSecondary}; - font-size: 0.8em; - display: flex; - flex-flow: row nowrap; - align-items: center; - justify-content: flex-end; - margin-right: 0.5rem; - padding-right: 0.5rem; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - flex-shrink: 1; - } - - .title { - color: ${textSecondary}; - margin-left: 0.25rem; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - line-height: 2.2rem; - flex: 1; - - &.syncing { - opacity: 0.4; - } - - &.unassigned { - color: ${textSecondary}; - opacity: 0.45; - } - } - - .wallet { - width: 1em; - height: 1em; - margin-left: 0.8rem; - opacity: 0.8; - - path { - fill: ${textSecondary}; - } - } -`; - -export default Wrapper; diff --git a/src/library/PoolAccount/index.tsx b/src/library/PoolAccount/index.tsx deleted file mode 100644 index 33429abccb..0000000000 --- a/src/library/PoolAccount/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { u8aToString, u8aUnwrapBytes } from '@polkadot/util'; -import { useApi } from 'contexts/Api'; -import { useConnect } from 'contexts/Connect'; -import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useTheme } from 'contexts/Themes'; -import Identicon from 'library/Identicon'; -import { useEffect, useState } from 'react'; -import { defaultThemes } from 'theme/default'; -import { clipAddress, convertRemToPixels } from '../../Utils'; -import { PoolAccountProps } from './types'; -import Wrapper from './Wrapper'; - -export const PoolAccount = (props: PoolAccountProps) => { - const { mode } = useTheme(); - const { isReady } = useApi(); - const { activeAccount } = useConnect(); - const { fetchPoolsMetaBatch, meta } = useBondedPools(); - - const { label } = props; - - // is this the initial fetch - const [fetched, setFetched] = useState(false); - - const batchKey = 'pool_header'; - - // refetch when pool or active account changes - useEffect(() => { - setFetched(false); - }, [activeAccount, props.pool]); - - // configure pool list when network is ready to fetch - useEffect(() => { - if (isReady) { - setFetched(true); - - if (!fetched) { - getPoolMeta(); - } - } - }, [isReady, fetched]); - - // handle pool list bootstrapping - const getPoolMeta = () => { - const pools: any = [{ id: props.pool.id }]; - fetchPoolsMetaBatch(batchKey, pools, true); - }; - - const filled = props.filled ?? false; - const fontSize = props.fontSize ?? '1.05rem'; - const { canClick }: { canClick: boolean } = props; - - const metaBatch = meta[batchKey]; - const metaData = metaBatch?.metadata?.[0]; - const syncing = metaData === undefined; - - // display value - const defaultDisplay = clipAddress(props.pool.addresses.stash); - let display = syncing ? 'Syncing...' : metaData ?? defaultDisplay; - - // check if super identity has been byte encoded - const displayAsBytes = u8aToString(u8aUnwrapBytes(display)); - if (displayAsBytes !== '') { - display = displayAsBytes; - } - // if still empty string, default to clipped address - if (display === '') { - display = defaultDisplay; - } - - return ( - <Wrapper - whileHover={{ scale: 1.01 }} - onClick={props.onClick} - cursor={canClick ? 'pointer' : 'default'} - fill={filled ? defaultThemes.buttons.secondary.background[mode] : 'none'} - fontSize={fontSize} - > - {label !== undefined && <div className="account-label">{label}</div>} - - <span className="identicon"> - <Identicon - value={props.pool.addresses.stash} - size={convertRemToPixels(fontSize) * 1.45} - /> - </span> - <span className={`title${syncing === true ? ` syncing` : ``}`}> - {display} - </span> - </Wrapper> - ); -}; diff --git a/src/library/PoolAccount/types.ts b/src/library/PoolAccount/types.ts deleted file mode 100644 index 4159bcc11b..0000000000 --- a/src/library/PoolAccount/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export interface PoolAccountProps { - label?: string; - pool?: any; - filled?: boolean; - fontSize?: string; - canClick: any; - onClick?: () => void; - value?: string; - title?: string | undefined; -} - -export interface WrapperProps { - fill: string; - fontSize: string; - cursor: string; -} diff --git a/src/library/PoolList/FilterPools.tsx b/src/library/PoolList/FilterPools.tsx deleted file mode 100644 index 10f8523278..0000000000 --- a/src/library/PoolList/FilterPools.tsx +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faCheckCircle, faCircle } from '@fortawesome/free-regular-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useFilters } from 'contexts/Filters'; -import { FilterType } from 'contexts/Filters/types'; -import { usePoolFilters } from 'library/Hooks/usePoolFilters'; -import { Title } from 'library/Overlay/Title'; -import { FilterListButton, FilterListWrapper } from 'library/Overlay/Wrappers'; - -export const FilterPools = () => { - const { getFilters, toggleFilter } = useFilters(); - const { includesToLabels, excludesToLabels } = usePoolFilters(); - - const includes = getFilters(FilterType.Include, 'pools'); - const excludes = getFilters(FilterType.Exclude, 'pools'); - - return ( - <FilterListWrapper> - <Title title="Filter Pools" /> - <div className="body"> - <h4>Include:</h4> - {Object.entries(includesToLabels).map(([f, l]: any, i: number) => ( - <FilterListButton - active={includes?.includes(f) ?? false} - key={`pool_include_${i}`} - type="button" - onClick={() => { - toggleFilter(FilterType.Include, 'pools', f); - }} - > - <FontAwesomeIcon - transform="grow-4" - icon={includes?.includes(f) ? faCheckCircle : faCircle} - /> - <h4>{l}</h4> - </FilterListButton> - ))} - - <h4>Exclude:</h4> - {Object.entries(excludesToLabels).map(([f, l]: any, i: number) => ( - <FilterListButton - active={excludes?.includes(f) ?? false} - key={`validator_filter_${i}`} - type="button" - onClick={() => { - toggleFilter(FilterType.Exclude, 'pools', f); - }} - > - <FontAwesomeIcon - transform="grow-5" - icon={excludes?.includes(f) ? faCheckCircle : faCircle} - /> - <h4>{l}</h4> - </FilterListButton> - ))} - </div> - </FilterListWrapper> - ); -}; diff --git a/src/library/PoolList/Filters.tsx b/src/library/PoolList/Filters.tsx deleted file mode 100644 index ef594b6492..0000000000 --- a/src/library/PoolList/Filters.tsx +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { - faBan, - faCheck, - faFilterCircleXmark, -} from '@fortawesome/free-solid-svg-icons'; -import { - ButtonInvertRounded, - ButtonSecondary, -} from '@rossbulat/polkadot-dashboard-ui'; -import { useFilters } from 'contexts/Filters'; -import { FilterType } from 'contexts/Filters/types'; -import { useOverlay } from 'contexts/Overlay'; -import { Container } from 'library/Filter/Container'; -import { Item } from 'library/Filter/Item'; -import { useEffect } from 'react'; -import { usePoolFilters } from '../Hooks/usePoolFilters'; -import { FilterPools } from './FilterPools'; - -export const Filters = () => { - const { resetFilters, getFilters, toggleFilter } = useFilters(); - const { includesToLabels, excludesToLabels } = usePoolFilters(); - const { openOverlayWith } = useOverlay(); - - const includes = getFilters(FilterType.Include, 'pools'); - const excludes = getFilters(FilterType.Exclude, 'pools'); - const hasFilters = includes?.length || excludes?.length; - - // scroll to top of the window on every filter. - useEffect(() => { - window.scrollTo(0, 0); - }, [includes, excludes]); - - return ( - <> - <div style={{ marginBottom: '1.1rem' }}> - <ButtonInvertRounded - text="Filter" - marginRight - iconLeft={faFilterCircleXmark} - onClick={() => { - openOverlayWith(<FilterPools />); - }} - /> - <ButtonSecondary - text="Clear" - onClick={() => { - resetFilters(FilterType.Include, 'pools'); - resetFilters(FilterType.Exclude, 'pools'); - }} - disabled={!hasFilters} - /> - </div> - <Container> - <div className="items"> - {!hasFilters && <Item label="No filters" disabled />} - {includes?.map((e: string, i: number) => ( - <Item - key={`pool_include_${i}`} - label={includesToLabels[e]} - icon={faCheck} - transform="grow-2" - onClick={() => { - toggleFilter(FilterType.Include, 'pools', e); - }} - /> - ))} - {excludes?.map((e: string, i: number) => ( - <Item - key={`pool_filter_${i}`} - label={excludesToLabels[e]} - icon={faBan} - transform="grow-0" - onClick={() => { - toggleFilter(FilterType.Exclude, 'pools', e); - }} - /> - ))} - </div> - </Container> - </> - ); -}; diff --git a/src/library/PoolList/index.tsx b/src/library/PoolList/index.tsx deleted file mode 100644 index a7152822dc..0000000000 --- a/src/library/PoolList/index.tsx +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faBars, faGripVertical } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { ListItemsPerBatch, ListItemsPerPage } from 'consts'; -import { useApi } from 'contexts/Api'; -import { useFilters } from 'contexts/Filters'; -import { FilterType } from 'contexts/Filters/types'; -import { useNetworkMetrics } from 'contexts/Network'; -import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { StakingContext } from 'contexts/Staking'; -import { useTheme } from 'contexts/Themes'; -import { useUi } from 'contexts/UI'; -import { motion } from 'framer-motion'; -import { usePoolFilters } from 'library/Hooks/usePoolFilters'; -import { Header, List, Wrapper as ListWrapper } from 'library/List'; -import { MotionContainer } from 'library/List/MotionContainer'; -import { Pagination } from 'library/List/Pagination'; -import { SearchInput } from 'library/List/SearchInput'; -import { Pool } from 'library/Pool'; -import React, { useEffect, useRef, useState } from 'react'; -import { networkColors } from 'theme/default'; -import { PoolListProvider, usePoolList } from './context'; -import { Filters } from './Filters'; -import { PoolListProps } from './types'; - -export const PoolListInner = ({ - allowMoreCols, - pagination, - batchKey = '', - disableThrottle, - allowSearch, - pools, - title, - defaultFilters, -}: PoolListProps) => { - const { mode } = useTheme(); - const { isReady, network } = useApi(); - const { metrics } = useNetworkMetrics(); - const { fetchPoolsMetaBatch, poolSearchFilter, meta } = useBondedPools(); - const { listFormat, setListFormat } = usePoolList(); - const { isSyncing } = useUi(); - - const { getFilters, setMultiFilters, getSearchTerm, setSearchTerm } = - useFilters(); - const { applyFilter } = usePoolFilters(); - const includes = getFilters(FilterType.Include, 'pools'); - const excludes = getFilters(FilterType.Exclude, 'pools'); - const searchTerm = getSearchTerm('pools'); - - // current page - const [page, setPage] = useState<number>(1); - - // current render iteration - const [renderIteration, _setRenderIteration] = useState<number>(1); - - // default list of pools - const [poolsDefault, setPoolsDefault] = useState(pools); - - // manipulated list (ordering, filtering) of pools - const [_pools, _setPools] = useState(pools); - - // is this the initial fetch - const [fetched, setFetched] = useState<boolean>(false); - - // render throttle iteration - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - _setRenderIteration(iter); - }; - - // pagination - const totalPages = Math.ceil(_pools.length / ListItemsPerPage); - const pageEnd = page * ListItemsPerPage - 1; - const pageStart = pageEnd - (ListItemsPerPage - 1); - - // render batch - const batchEnd = renderIteration * ListItemsPerBatch - 1; - - // refetch list when pool list changes - useEffect(() => { - if (pools !== poolsDefault) { - setFetched(false); - } - }, [pools]); - - // configure pool list when network is ready to fetch - useEffect(() => { - if (isReady && metrics.activeEra.index !== 0 && !fetched) { - setupPoolList(); - } - }, [isReady, fetched, metrics.activeEra.index]); - - // handle pool list bootstrapping - const setupPoolList = () => { - setPoolsDefault(pools); - _setPools(pools); - setFetched(true); - fetchPoolsMetaBatch(batchKey, pools, true); - }; - - // render throttle - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); - } - }, [renderIterationRef.current]); - - // list ui changes / validator changes trigger re-render of list - useEffect(() => { - // only filter when pool nominations have been synced. - if (!isSyncing && meta[batchKey]?.nominations) { - handlePoolsFilterUpdate(); - } - }, [isSyncing, includes?.length, excludes?.length, meta]); - - // set default filters - useEffect(() => { - if (defaultFilters?.includes?.length) { - setMultiFilters(FilterType.Include, 'pools', defaultFilters?.includes); - } - if (defaultFilters?.excludes?.length) { - setMultiFilters(FilterType.Exclude, 'pools', defaultFilters?.excludes); - } - }, []); - - // handle filter / order update - const handlePoolsFilterUpdate = ( - filteredPools: any = Object.assign(poolsDefault) - ) => { - filteredPools = applyFilter(includes, excludes, filteredPools, batchKey); - if (searchTerm) { - filteredPools = poolSearchFilter(filteredPools, batchKey, searchTerm); - } - _setPools(filteredPools); - setPage(1); - setRenderIteration(1); - }; - - // get pools to render - let listPools = []; - - // get throttled subset or entire list - if (!disableThrottle) { - listPools = _pools.slice(pageStart).slice(0, ListItemsPerPage); - } else { - listPools = _pools; - } - - const handleSearchChange = (e: React.FormEvent<HTMLInputElement>) => { - const newValue = e.currentTarget.value; - let filteredPools = Object.assign(poolsDefault); - filteredPools = applyFilter(includes, excludes, filteredPools, batchKey); - filteredPools = poolSearchFilter(filteredPools, batchKey, newValue); - - // ensure no duplicates - filteredPools = filteredPools.filter( - (value: any, index: any, self: any) => - index === self.findIndex((t: any) => t.id === value.id) - ); - - setPage(1); - setRenderIteration(1); - _setPools(filteredPools); - setSearchTerm('pools', newValue); - }; - - return ( - <ListWrapper> - <Header> - <div> - <h4>{title}</h4> - </div> - <div> - <button type="button" onClick={() => setListFormat('row')}> - <FontAwesomeIcon - icon={faBars} - color={ - listFormat === 'row' - ? networkColors[`${network.name}-${mode}`] - : 'inherit' - } - /> - </button> - <button type="button" onClick={() => setListFormat('col')}> - <FontAwesomeIcon - icon={faGripVertical} - color={ - listFormat === 'col' - ? networkColors[`${network.name}-${mode}`] - : 'inherit' - } - /> - </button> - </div> - </Header> - <List flexBasisLarge={allowMoreCols ? '33.33%' : '50%'}> - {allowSearch && poolsDefault.length > 0 && ( - <SearchInput - handleChange={handleSearchChange} - placeholder="Search Pool ID, Name or Address" - /> - )} - <Filters /> - {pagination && listPools.length > 0 && ( - <Pagination page={page} total={totalPages} setter={setPage} /> - )} - <MotionContainer> - {listPools.length ? ( - <> - {listPools.map((pool: any, index: number) => { - // fetch batch data by referring to default list index. - const batchIndex = poolsDefault.indexOf(pool); - - return ( - <motion.div - className={`item ${listFormat === 'row' ? 'row' : 'col'}`} - key={`nomination_${index}`} - variants={{ - hidden: { - y: 15, - opacity: 0, - }, - show: { - y: 0, - opacity: 1, - }, - }} - > - <Pool - pool={pool} - batchKey={batchKey} - batchIndex={batchIndex} - /> - </motion.div> - ); - })} - </> - ) : ( - <h4 style={{ padding: '1rem 1rem 0 1rem' }}> - {isSyncing - ? 'Syncing Pool list...' - : 'No pools match this criteria.'} - </h4> - )} - </MotionContainer> - </List> - </ListWrapper> - ); -}; - -export const PoolList = (props: any) => { - return ( - <PoolListProvider> - <PoolListShouldUpdate {...props} /> - </PoolListProvider> - ); -}; - -export class PoolListShouldUpdate extends React.Component<any, any> { - static contextType = StakingContext; - - shouldComponentUpdate(nextProps: PoolListProps) { - return this.props.pools !== nextProps.pools; - } - - render() { - return <PoolListInner {...this.props} />; - } -} - -export default PoolList; diff --git a/src/library/SubscanButton/index.tsx b/src/library/SubscanButton/index.tsx deleted file mode 100644 index 2093847b52..0000000000 --- a/src/library/SubscanButton/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faProjectDiagram } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useApi } from 'contexts/Api'; -import { useTheme } from 'contexts/Themes'; -import { useUi } from 'contexts/UI'; -import styled from 'styled-components'; -import { defaultThemes, networkColors } from 'theme/default'; -import { WrapperProps } from './types'; - -const Wrapper = styled.div<WrapperProps>` - position: absolute; - right: 10px; - top: 10px; - font-size: 0.9rem; - border-radius: 0.3rem; - padding: 0.25rem 0.4rem; - color: ${(props) => props.color}; - opacity: ${(props) => props.opacity}; - z-index: 2; -`; - -export const SubscanButton = () => { - const { network } = useApi(); - const { mode } = useTheme(); - const { services } = useUi(); - - return ( - <Wrapper - color={ - services.includes('subscan') - ? networkColors[`${network.name}-${mode}`] - : defaultThemes.text.secondary[mode] - } - opacity={services.includes('cereStats') ? 1 : 0.5} - > - <FontAwesomeIcon - icon={faProjectDiagram} - transform="shrink-2" - style={{ marginRight: '0.3rem' }} - /> - Cere Stats - </Wrapper> - ); -}; - -export default SubscanButton; diff --git a/src/library/SubscanButton/types.ts b/src/library/SubscanButton/types.ts deleted file mode 100644 index 33e788290d..0000000000 --- a/src/library/SubscanButton/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export interface StatusLabelProps { - status: string; - statusFor?: string; - title: string; - topOffset?: string; -} - -export interface WrapperProps { - color: string; - opacity: number; -} diff --git a/src/library/Tips/Items/Dismiss.tsx b/src/library/Tips/Items/Dismiss.tsx deleted file mode 100644 index aa3671aacc..0000000000 --- a/src/library/Tips/Items/Dismiss.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useTips } from 'contexts/Tips'; -import { useUi } from 'contexts/UI'; -import { Button } from 'library/Button'; -import { TipWrapper } from '../Wrappers'; - -export const Dismiss = () => { - const { closeTip } = useTips(); - const { toggleService } = useUi(); - - return ( - <TipWrapper> - <div> - <h1>Dismiss Tips</h1> - </div> - <div> - <h4>Dismissing tips will remove them from your overview.</h4> - <h4> - Tips can be turned re-enabled from dashboard settings, that can be - accessed via the cog icon in the bottom left corner of the side menu. - </h4> - <div className="buttons"> - <Button - primary - inline - title="Disable Dashboard Tips" - onClick={() => { - toggleService('tips'); - closeTip(); - }} - /> - <Button - inline - title="Cancel" - onClick={() => { - closeTip(); - }} - /> - </div> - </div> - </TipWrapper> - ); -}; diff --git a/src/library/Tips/Items/Tip.tsx b/src/library/Tips/Items/Tip.tsx deleted file mode 100644 index 0cbb95db6d..0000000000 --- a/src/library/Tips/Items/Tip.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faTimes } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useTips } from 'contexts/Tips'; -import { TipWrapper } from '../Wrappers'; - -export const Tip = (props: any) => { - const { title, description } = props; - - const { closeTip } = useTips(); - - return ( - <> - <TipWrapper> - <div className="close-button"> - <button type="button" onClick={() => closeTip()}> - <FontAwesomeIcon icon={faTimes} /> - Close - </button> - </div> - <div> - <h1>{title}</h1> - </div> - <div> - {description.map((item: any, index: number) => ( - <h4 key={`inner_def_${index}`} className="definition"> - {item} - </h4> - ))} - </div> - </TipWrapper> - </> - ); -}; diff --git a/src/library/ValidatorList/Filters.tsx b/src/library/ValidatorList/Filters.tsx deleted file mode 100644 index c4d90cd76f..0000000000 --- a/src/library/ValidatorList/Filters.tsx +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { - faArrowDownWideShort, - faBan, - faCheck, - faFilterCircleXmark, -} from '@fortawesome/free-solid-svg-icons'; -import { - ButtonInvertRounded, - ButtonSecondary, -} from '@rossbulat/polkadot-dashboard-ui'; -import { useFilters } from 'contexts/Filters'; -import { FilterType } from 'contexts/Filters/types'; -import { useOverlay } from 'contexts/Overlay'; -import { Container } from 'library/Filter/Container'; -import { Item } from 'library/Filter/Item'; -import { useEffect } from 'react'; -import { useValidatorFilters } from '../Hooks/useValidatorFilters'; -import { FilterValidators } from './FilterValidators'; -import { OrderValidators } from './OrderValidators'; - -export const Filters = () => { - const { openOverlayWith } = useOverlay(); - const { resetFilters, getFilters, getOrder, toggleFilter } = useFilters(); - const { includesToLabels, excludesToLabels, ordersToLabels } = - useValidatorFilters(); - - const includes = getFilters(FilterType.Include, 'validators'); - const excludes = getFilters(FilterType.Exclude, 'validators'); - const hasFilters = includes?.length || excludes?.length; - const order = getOrder('validators'); - - // scroll to top of the window on every filter. - useEffect(() => { - window.scrollTo(0, 0); - }, [includes, excludes]); - - return ( - <> - <div style={{ marginBottom: '1.1rem' }}> - <ButtonInvertRounded - text="Order" - marginRight - iconLeft={faArrowDownWideShort} - onClick={() => { - openOverlayWith(<OrderValidators />); - }} - /> - <ButtonInvertRounded - text="Filter" - marginRight - iconLeft={faFilterCircleXmark} - onClick={() => { - openOverlayWith(<FilterValidators />); - }} - /> - <ButtonSecondary - text="Clear" - onClick={() => { - resetFilters(FilterType.Include, 'validators'); - resetFilters(FilterType.Exclude, 'validators'); - }} - disabled={!hasFilters} - /> - </div> - <Container> - <div className="items"> - <Item - label={ - order === 'default' - ? 'Unordered' - : `Order: ${ordersToLabels[order]}` - } - disabled - /> - {!hasFilters && <Item label="No filters" disabled />} - {includes?.map((e: string, i: number) => ( - <Item - key={`validator_include_${i}`} - label={includesToLabels[e]} - icon={faCheck} - transform="grow-2" - onClick={() => { - toggleFilter(FilterType.Include, 'validators', e); - }} - /> - ))} - {excludes?.map((e: string, i: number) => ( - <Item - key={`validator_exclude_${i}`} - label={excludesToLabels[e]} - icon={faBan} - transform="grow-0" - onClick={() => { - toggleFilter(FilterType.Exclude, 'validators', e); - }} - /> - ))} - </div> - </Container> - </> - ); -}; diff --git a/src/library/ValidatorList/Validator/Default.tsx b/src/library/ValidatorList/Validator/Default.tsx deleted file mode 100644 index 1b4aa500c8..0000000000 --- a/src/library/ValidatorList/Validator/Default.tsx +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faCopy } from '@fortawesome/free-regular-svg-icons'; -import { faBars, faChartLine } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useMenu } from 'contexts/Menu'; -import { useModal } from 'contexts/Modal'; -import { useNotifications } from 'contexts/Notifications'; -import { NotificationText } from 'contexts/Notifications/types'; -import CopyAddress from 'library/ListItem/Labels/CopyAddress'; -import { ParaValidator } from 'library/ListItem/Labels/ParaValidator'; -import { - Labels, - MenuPosition, - Separator, - Wrapper, -} from 'library/ListItem/Wrappers'; -import { useRef } from 'react'; -import { useValidators } from '../../../contexts/Validators'; -import { useList } from '../../List/context'; -import { Blocked } from '../../ListItem/Labels/Blocked'; -import { Commission } from '../../ListItem/Labels/Commission'; -import { EraStatus } from '../../ListItem/Labels/EraStatus'; -import { FavoriteValidator } from '../../ListItem/Labels/FavoriteValidator'; -import { Identity } from '../../ListItem/Labels/Identity'; -import { Oversubscribed } from '../../ListItem/Labels/Oversubscribed'; -import { Select } from '../../ListItem/Labels/Select'; -import { DefaultProps } from './types'; -import { getIdentityDisplay } from './Utils'; - -export const Default = (props: DefaultProps) => { - const { - validator, - toggleFavorites, - batchIndex, - batchKey, - showMenu, - inModal, - } = props; - - const { openModalWith } = useModal(); - const { addNotification } = useNotifications(); - const { setMenuPosition, setMenuItems, open }: any = useMenu(); - const { meta } = useValidators(); - const { selectActive } = useList(); - - const identities = meta[batchKey]?.identities ?? []; - const supers = meta[batchKey]?.supers ?? []; - - const { address, prefs } = validator; - const commission = prefs?.commission ?? null; - - const identity = getIdentityDisplay( - identities[batchIndex], - supers[batchIndex] - ); - - // copy address notification - const notificationCopyAddress: NotificationText | null = - address == null - ? null - : { - title: 'Address Copied to Clipboard', - subtitle: address, - }; - - // configure floating menu - const posRef = useRef(null); - const menuItems = [ - { - icon: <FontAwesomeIcon icon={faChartLine as IconProp} />, - wrap: null, - title: `View Metrics`, - cb: () => { - openModalWith( - 'ValidatorMetrics', - { - address, - identity, - }, - 'large' - ); - }, - }, - { - icon: <FontAwesomeIcon icon={faCopy as IconProp} />, - wrap: null, - title: `Copy Address`, - cb: () => { - navigator.clipboard.writeText(address); - if (notificationCopyAddress) { - addNotification(notificationCopyAddress); - } - }, - }, - ]; - - const toggleMenu = () => { - if (!open) { - setMenuItems(menuItems); - setMenuPosition(posRef); - } - }; - - return ( - <Wrapper format="nomination" inModal={inModal}> - <div className="inner"> - <MenuPosition ref={posRef} /> - <div className="row"> - {selectActive && <Select item={validator} />} - <Identity - meta={meta} - address={address} - batchIndex={batchIndex} - batchKey={batchKey} - /> - <div> - <Labels> - <Oversubscribed batchIndex={batchIndex} batchKey={batchKey} /> - <Blocked prefs={prefs} /> - <Commission commission={commission} /> - <ParaValidator address={address} /> - - {toggleFavorites && <FavoriteValidator address={address} />} - {showMenu && ( - <button - type="button" - className="label" - onClick={() => toggleMenu()} - > - <FontAwesomeIcon icon={faBars} /> - </button> - )} - </Labels> - </div> - </div> - <Separator /> - <div className="row status"> - <EraStatus address={address} /> - {inModal && ( - <> - <Labels> - <CopyAddress validator={validator} /> - </Labels> - </> - )} - </div> - </div> - </Wrapper> - ); -}; - -export default Default; diff --git a/src/library/ValidatorList/Validator/Nomination.tsx b/src/library/ValidatorList/Validator/Nomination.tsx deleted file mode 100644 index 214a4488c3..0000000000 --- a/src/library/ValidatorList/Validator/Nomination.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useValidators } from 'contexts/Validators'; -import { ParaValidator } from 'library/ListItem/Labels/ParaValidator'; -import { Labels, Separator, Wrapper } from 'library/ListItem/Wrappers'; -import { useList } from '../../List/context'; -import { Blocked } from '../../ListItem/Labels/Blocked'; -import { Commission } from '../../ListItem/Labels/Commission'; -import { CopyAddress } from '../../ListItem/Labels/CopyAddress'; -import { FavoriteValidator } from '../../ListItem/Labels/FavoriteValidator'; -import { Identity } from '../../ListItem/Labels/Identity'; -import { Metrics } from '../../ListItem/Labels/Metrics'; -import { NominationStatus } from '../../ListItem/Labels/NominationStatus'; -import { Oversubscribed } from '../../ListItem/Labels/Oversubscribed'; -import { Select } from '../../ListItem/Labels/Select'; -import { NominationProps } from './types'; -import { getIdentityDisplay } from './Utils'; - -export const Nomination = (props: NominationProps) => { - const { meta } = useValidators(); - const { selectActive } = useList(); - - const { - validator, - nominator, - toggleFavorites, - batchIndex, - batchKey, - bondType, - inModal, - } = props; - - const identities = meta[batchKey]?.identities ?? []; - const supers = meta[batchKey]?.supers ?? []; - - const { address, prefs } = validator; - const commission = prefs?.commission ?? null; - - return ( - <Wrapper format="nomination" inModal={inModal}> - <div className="inner"> - <div className="row"> - {selectActive && <Select item={validator} />} - <Identity - meta={meta} - address={address} - batchIndex={batchIndex} - batchKey={batchKey} - /> - <div> - <Labels> - <CopyAddress validator={validator} /> - {toggleFavorites && <FavoriteValidator address={address} />} - </Labels> - </div> - </div> - <Separator /> - <div className="row status"> - <NominationStatus - address={address} - bondType={bondType} - nominator={nominator} - /> - <Labels> - <Oversubscribed batchIndex={batchIndex} batchKey={batchKey} /> - <Blocked prefs={prefs} /> - <Commission commission={commission} /> - <ParaValidator address={address} /> - - {/* restrict opening another modal within a modal */} - {!inModal && ( - <Metrics - address={address} - display={getIdentityDisplay( - identities[batchIndex], - supers[batchIndex] - )} - /> - )} - </Labels> - </div> - </div> - </Wrapper> - ); -}; - -export default Nomination; diff --git a/src/library/ValidatorList/Validator/Utils.tsx b/src/library/ValidatorList/Validator/Utils.tsx deleted file mode 100644 index 92e5eaad9a..0000000000 --- a/src/library/ValidatorList/Validator/Utils.tsx +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { u8aToString, u8aUnwrapBytes } from '@polkadot/util'; - -export const getIdentityDisplay = (_identity: any, _superIdentity: any) => { - let displayFinal = ''; - let foundSuper = false; - - // check super identity exists, get display.Raw if it does - const superIdentity = _superIdentity?.identity ?? null; - const superRaw = _superIdentity?.[1]?.Raw ?? null; - const superDisplay = superIdentity?.info?.display?.Raw ?? null; - - // check if super raw has been encoded - const superRawAsBytes = u8aToString(u8aUnwrapBytes(superRaw)); - - // check if super identity has been byte encoded - const superIdentityAsBytes = u8aToString(u8aUnwrapBytes(superDisplay)); - - if (superIdentityAsBytes !== '') { - displayFinal = superIdentityAsBytes; - foundSuper = true; - } else if (superDisplay !== null) { - displayFinal = superDisplay; - foundSuper = true; - } - - if (!foundSuper) { - // cehck sub identity exists, get display.Raw if it does - const identity = _identity?.info?.display?.Raw ?? null; - - // check if identity has been byte encoded - const subIdentityAsBytes = u8aToString(u8aUnwrapBytes(identity)); - - if (subIdentityAsBytes !== '') { - displayFinal = subIdentityAsBytes; - } else if (identity !== null) { - displayFinal = identity; - } - } - if (displayFinal === '') { - return null; - } - - return ( - <> - {displayFinal} - {superRawAsBytes !== '' ? ( - <span>/ {superRawAsBytes}</span> - ) : superRaw !== null ? ( - <span>/ {superRaw}</span> - ) : null} - </> - ); -}; diff --git a/src/library/ValidatorList/Validator/index.tsx b/src/library/ValidatorList/Validator/index.tsx deleted file mode 100644 index 2e329dcbc4..0000000000 --- a/src/library/ValidatorList/Validator/index.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import React from 'react'; -import { Default } from './Default'; -import { Nomination } from './Nomination'; - -export const ValidatorInner = (props: any) => { - const { format } = props; - - return format === 'nomination' ? ( - <Nomination {...props} /> - ) : ( - <Default {...props} /> - ); -}; - -export class Validator extends React.Component<any, any> { - shouldComponentUpdate(nextProps: any) { - return ( - this.props.validator.address !== nextProps.validator.address || - this.props.batchIndex !== nextProps.batchIndex || - this.props.batchKey !== nextProps.batchKey - ); - } - - render() { - return <ValidatorInner {...this.props} />; - } -} - -export default Validator; diff --git a/src/library/ValidatorList/Validator/types.ts b/src/library/ValidatorList/Validator/types.ts deleted file mode 100644 index 08250bae2d..0000000000 --- a/src/library/ValidatorList/Validator/types.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { MaybeAccount } from 'types'; - -export interface NominationProps { - validator: any; - nominator: MaybeAccount; - toggleFavorites: boolean; - batchIndex: number; - batchKey: string; - bondType: string; - inModal: boolean; -} - -export interface DefaultProps { - validator: any; - toggleFavorites: boolean; - batchIndex: number; - batchKey: string; - showMenu: boolean; - inModal: boolean; -} diff --git a/src/modals/Connect/Ledger.tsx b/src/modals/Connect/Ledger.tsx index 0129bb3e22..f1d6dff560 100644 --- a/src/modals/Connect/Ledger.tsx +++ b/src/modals/Connect/Ledger.tsx @@ -44,11 +44,11 @@ export const Ledger = (): React.ReactElement => { </div> <div className="row margin"> <ButtonText - text={network === 'polkadot' ? 'BETA' : 'EXPERIMENTAL'} + text={network === 'Cere Mainnet' ? 'BETA' : 'EXPERIMENTAL'} disabled marginRight iconLeft={ - network === 'polkadot' ? undefined : faExclamationTriangle + network === 'Cere Mainnet' ? undefined : faExclamationTriangle } style={{ opacity: 0.5 }} /> diff --git a/src/modals/ConnectAccounts/Account.tsx b/src/modals/ConnectAccounts/Account.tsx deleted file mode 100644 index 11ed885025..0000000000 --- a/src/modals/ConnectAccounts/Account.tsx +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faGlasses } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useConnect } from 'contexts/Connect'; -import { useExtensions } from 'contexts/Extensions'; -import { Extension } from 'contexts/Extensions/types'; -import { useModal } from 'contexts/Modal'; -import Identicon from 'library/Identicon'; -import { clipAddress } from 'Utils'; -import { AccountElementProps } from './types'; -import { AccountWrapper } from './Wrappers'; - -export const AccountElement = (props: AccountElementProps) => { - return ( - <AccountWrapper> - <div> - <AccountInner {...props} /> - </div> - </AccountWrapper> - ); -}; - -export const AccountButton = (props: AccountElementProps) => { - const { meta } = props; - const disconnect = props.disconnect ?? false; - const { connectToAccount, disconnectFromAccount } = useConnect(); - const { setStatus } = useModal(); - const imported = meta !== null; - - return ( - <AccountWrapper> - <button - type="button" - disabled={!disconnect && !imported} - onClick={() => { - if (imported) { - if (disconnect) { - disconnectFromAccount(); - } else { - connectToAccount(meta); - setStatus(2); - } - } - }} - > - <AccountInner {...props} /> - </button> - </AccountWrapper> - ); -}; - -export const AccountInner = (props: AccountElementProps) => { - const { address, meta } = props; - - const { extensions } = useExtensions(); - const extension = extensions.find((e: Extension) => e.id === meta?.source); - const Icon = extension?.icon ?? null; - const label = props.label ?? null; - const source = meta?.source ?? null; - const imported = meta !== null && source !== 'external'; - - return ( - <> - <div> - <Identicon value={address ?? ''} size={26} /> - <span className="name"> -   {meta?.name ?? clipAddress(address ?? '')} - </span> - </div> - {!imported && ( - <div - className="label warning" - style={{ color: '#a17703', paddingLeft: '0.5rem' }} - > - Read Only - </div> - )} - - <div className={label === null ? `` : label[0]}> - {label !== null && <h5>{label[1]}</h5>} - {Icon !== null && <Icon className="icon" />} - - {!imported && ( - <FontAwesomeIcon - icon={faGlasses as IconProp} - className="icon" - style={{ opacity: 0.7 }} - /> - )} - </div> - </> - ); -}; diff --git a/src/modals/ConnectAccounts/Accounts.tsx b/src/modals/ConnectAccounts/Accounts.tsx deleted file mode 100644 index fce93f79d9..0000000000 --- a/src/modals/ConnectAccounts/Accounts.tsx +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { - faCog, - faProjectDiagram, - faUsers, -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { ButtonSecondary } from '@rossbulat/polkadot-dashboard-ui'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { ImportedAccount } from 'contexts/Connect/types'; -import { useModal } from 'contexts/Modal'; -import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; -import { PoolMembership } from 'contexts/Pools/types'; -import { forwardRef, useEffect, useState } from 'react'; -import { AnyJson } from 'types'; -import { AccountButton, AccountElement } from './Account'; -import { - ActivelyStakingAccount, - ControllerAccount, - StashAcount, -} from './types'; -import { - AccountGroupWrapper, - AccountWrapper, - ContentWrapper, - PaddingWrapper, -} from './Wrappers'; - -export const Accounts = forwardRef((props: AnyJson, ref: AnyJson) => { - const { setSection } = props; - - const { isReady } = useApi(); - const { getAccount, activeAccount } = useConnect(); - const { - getLedgerForController, - getAccountLocks, - getBondedAccount, - accounts: balanceAccounts, - ledgers, - } = useBalances(); - const { connectToAccount } = useConnect(); - const { setStatus } = useModal(); - const { accounts } = useConnect(); - const { memberships } = usePoolMemberships(); - - const _controllers: Array<ControllerAccount> = []; - const _stashes: Array<StashAcount> = []; - - // store local copy of accounts - const [localAccounts, setLocalAccounts] = useState(accounts); - - // store staking statuses - const [activeStaking, setActiveStaking] = useState< - Array<ActivelyStakingAccount> - >([]); - const [activePooling, setActivePooling] = useState<Array<PoolMembership>>([]); - const [inactive, setInactive] = useState<string[]>([]); - - useEffect(() => { - setLocalAccounts(accounts); - }, [isReady, accounts]); - - useEffect(() => { - getStakingStatuses(); - }, [localAccounts, balanceAccounts, ledgers, accounts, memberships]); - - const getStakingStatuses = () => { - // accumulate imported stash accounts - for (const account of localAccounts) { - const locks = getAccountLocks(account.address); - - // account is a stash if they have an active `staking` lock - const activeLocks = locks.find((l) => { - const { id } = l; - return id.trim() === 'staking'; - }); - if (activeLocks !== undefined) { - _stashes.push({ - address: account.address, - controller: getBondedAccount(account.address), - }); - } - } - - // accumulate imported controller accounts - for (const account of localAccounts) { - const ledger = getLedgerForController(account.address); - if (ledger) { - _controllers.push({ - address: account.address, - ledger, - }); - } - } - - // construct account groupings - const _activeStaking: Array<ActivelyStakingAccount> = []; - const _activePooling: Array<PoolMembership> = []; - const _inactive: string[] = []; - - for (const account of localAccounts) { - const stash = - _stashes.find((s: StashAcount) => s.address === account.address) ?? - null; - const controller = - _controllers.find( - (c: ControllerAccount) => c.address === account.address - ) ?? null; - const poolMember = - memberships.find( - (m: PoolMembership) => m.address === account.address - ) ?? null; - - // if stash, get controller - if (stash) { - const applied = - _activeStaking.find( - (a: ActivelyStakingAccount) => a.stash === account.address - ) !== undefined; - - if (!applied) { - const _record = { - stash: account.address, - controller: stash.controller, - stashImported: true, - controllerImported: - localAccounts.find( - (a: ImportedAccount) => a.address === stash.controller - ) !== undefined, - }; - _activeStaking.push(_record); - } - } - - // if controller, get stash - if (controller) { - const applied = - _activeStaking.find((a) => a.controller === account.address) !== - undefined; - - if (!applied) { - const _record = { - stash: controller.ledger.stash, - controller: controller.address, - stashImported: - localAccounts.find( - (a: ImportedAccount) => a.address === controller.ledger.stash - ) !== undefined, - controllerImported: true, - }; - _activeStaking.push(_record); - } - } - - // if pooling, add to active pooling - if (poolMember) { - if (!_activePooling.includes(poolMember)) { - _activePooling.push(poolMember); - } - } - - // if not doing anything, add to inactive - if (!stash && !controller && !poolMember) { - if (!_inactive.includes(account.address)) { - _inactive.push(account.address); - } - } - } - setActiveStaking(_activeStaking); - setActivePooling(_activePooling); - setInactive(_inactive); - }; - - return ( - <ContentWrapper> - <PaddingWrapper ref={ref}> - <div className="head"> - <div> - <h1>Accounts</h1> - </div> - <div> - <ButtonSecondary - text="Extensions" - iconLeft={faCog} - iconTransform="shrink-2" - onClick={() => setSection(0)} - /> - </div> - </div> - {activeAccount ? ( - <AccountButton - address={activeAccount} - meta={getAccount(activeAccount)} - label={['danger', 'Disconnect']} - disconnect - /> - ) : ( - <AccountWrapper> - <div> - <div> - <h3>No Account Connected</h3> - </div> - <div /> - </div> - </AccountWrapper> - )} - {activeStaking.length > 0 && ( - <> - <h3 className="heading"> - <FontAwesomeIcon icon={faProjectDiagram} transform="shrink-4" />{' '} - Nominating - </h3> - {activeStaking.map((item: ActivelyStakingAccount, i: number) => { - const { stash, controller } = item; - const stashAccount = getAccount(stash); - const controllerAccount = getAccount(controller); - - return ( - <AccountGroupWrapper - key={`active_staking_${i}`} - onClick={() => { - if (stashAccount) { - connectToAccount(stashAccount); - setStatus(2); - } - }} - > - <section> - <AccountElement - address={stash} - meta={stashAccount} - label={['neutral', 'Stash']} - asElement - /> - </section> - <section> - <AccountElement - address={controller} - meta={controllerAccount} - label={['neutral', 'Controller']} - asElement - /> - </section> - </AccountGroupWrapper> - ); - })} - </> - )} - - {activePooling.length > 0 && ( - <> - <h3 className="heading"> - <FontAwesomeIcon icon={faUsers} transform="shrink-4" /> In Pool - </h3> - {activePooling.map((item: PoolMembership, i: number) => { - const { address } = item; - const account = getAccount(address); - - return ( - <AccountButton - address={address} - meta={account} - key={`active_pool_${i}`} - /> - ); - })} - </> - )} - - {inactive.length > 0 && ( - <> - <h3 className="heading">Not Staking</h3> - {inactive.map((item: string, i: number) => { - const account = getAccount(item); - const address = account?.address ?? ''; - - return ( - <AccountButton - address={address} - meta={account} - key={`not_staking_${i}`} - /> - ); - })} - </> - )} - </PaddingWrapper> - </ContentWrapper> - ); -}); - -export default Accounts; diff --git a/src/modals/ConnectAccounts/Extension.tsx b/src/modals/ConnectAccounts/Extension.tsx deleted file mode 100644 index 5df14a4af5..0000000000 --- a/src/modals/ConnectAccounts/Extension.tsx +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faCheckCircle, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useConnect } from 'contexts/Connect'; -import { useExtensions } from 'contexts/Extensions'; -import { Extension as ExtensionInterface } from 'contexts/Extensions/types'; -import { useState } from 'react'; -import { ExtensionProps } from './types'; -import { ExtensionWrapper } from './Wrappers'; - -export const Extension = (props: ExtensionProps) => { - const { extensions } = useExtensions(); - const { extensionsStatus } = useExtensions(); - const { meta } = props; - const { id } = meta; - - const installed = extensions.find((e: ExtensionInterface) => e.id === id); - const status = !installed ? 'not_found' : extensionsStatus[id]; - - // determine message to be displayed based on extension status. - let message; - switch (status) { - case 'connected': - message = `Connected`; - break; - case 'not_authenticated': - message = 'Not Authenticated. Authenticate and Try Again'; - break; - default: - message = status === 'no_accounts' ? 'No Accounts' : 'Not Connected'; - } - - return ( - <ExtensionWrapper> - {status === 'connected' || !installed ? ( - <ExtensionElement - {...props} - message={message} - status={status} - size="1.5rem" - /> - ) : ( - <ExtensionButton - {...props} - message={message} - status={status} - size="1.5rem" - installed={installed} - /> - )} - </ExtensionWrapper> - ); -}; - -export const ExtensionButton = (props: any) => { - const { meta, setSection, installed } = props; - const { status } = meta; - - const { connectExtensionAccounts } = useConnect(); - - // force re-render on click - const [increment, setIncrement] = useState(0); - - // click to connect to extension - const handleClick = async () => { - if (status === 'connected') { - setSection(1); - } else { - (() => { - connectExtensionAccounts(installed); - // force re-render to display error messages - setIncrement(increment + 1); - })(); - } - }; - - return ( - <button - type="button" - disabled={status === 'connected'} - onClick={() => { - if (status !== 'connected') { - handleClick(); - } - }} - > - <ExtensionInner {...props} /> - </button> - ); -}; - -export const ExtensionElement = (props: any) => { - return ( - <div> - <ExtensionInner {...props} /> - </div> - ); -}; - -export const ExtensionInner = (props: any) => { - const { size, message, flag, meta, status } = props; - const { title, icon: Icon } = meta; - - return ( - <> - <div> - <Icon width={size} height={size} /> - <h3> - <span className="name">  {title}</span> - </h3> - </div> - - <div className={status === 'connected' ? 'success' : 'neutral'}> - <h4> - <span - className={`message ${status === 'connected' ? 'success' : ''}`} - > - {message} - </span> - </h4> - {flag && flag} - <FontAwesomeIcon - icon={status === 'connected' ? faCheckCircle : faPlus} - transform="shrink-0" - className="icon" - /> - </div> - </> - ); -}; diff --git a/src/modals/ConnectAccounts/Extensions.tsx b/src/modals/ConnectAccounts/Extensions.tsx deleted file mode 100644 index b258955b33..0000000000 --- a/src/modals/ConnectAccounts/Extensions.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { ExtensionConfig, EXTENSIONS } from 'config/extensions'; -import { useConnect } from 'contexts/Connect'; -import { forwardRef } from 'react'; -import { Extension } from './Extension'; -import { ReadOnly } from './ReadOnly'; -import { forwardRefProps } from './types'; -import { - ContentWrapper, - ExtensionWrapper, - PaddingWrapper, - Separator, -} from './Wrappers'; - -export const Extensions = forwardRef((props: forwardRefProps, ref: any) => { - const { setSection } = props; - - const { accounts } = useConnect(); - - return ( - <ContentWrapper> - <PaddingWrapper ref={ref}> - <div className="head"> - <h1>Extensions</h1> - </div> - <ExtensionWrapper> - <button - type="button" - onClick={() => { - setSection(1); - }} - > - <div> - <h3> - <span className="name"> - {accounts.length} Imported Account - {accounts.length !== 1 && 's'} - </span> - </h3> - </div> - <div className="neutral"> - <FontAwesomeIcon icon={faAngleDoubleRight} className="icon" /> - </div> - </button> - </ExtensionWrapper> - <Separator /> - {EXTENSIONS.map((extension: ExtensionConfig, i: number) => { - return ( - <Extension - key={`active_extension_${i}`} - meta={extension} - setSection={setSection} - /> - ); - })} - <ReadOnly {...props} /> - </PaddingWrapper> - </ContentWrapper> - ); -}); diff --git a/src/modals/ConnectAccounts/ReadOnly/Wrapper.ts b/src/modals/ConnectAccounts/ReadOnly/Wrapper.ts deleted file mode 100644 index efdf8d2ced..0000000000 --- a/src/modals/ConnectAccounts/ReadOnly/Wrapper.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; -import { - borderPrimary, - borderSecondary, - buttonPrimaryBackground, - textPrimary, - textSecondary, -} from 'theme'; - -export const Wrapper = styled.div` - color: ${textPrimary}; - border-radius: 0.75rem; - width: 100%; - margin: 1rem 0; - border-radius: 0.5rem; - background: ${buttonPrimaryBackground}; - transition: background 0.15s; - display: flex; - flex-flow: column nowrap; - align-items: flex-start; - min-height: 3.5rem; - - > .content { - padding: 0 1rem; - width: 100%; - } - - .accounts { - margin-top: 1rem; - width: 100%; - } - - .account { - width: 100%; - border: 1px solid ${borderPrimary}; - border-radius: 0.75rem; - margin: 1rem 0; - padding: 1rem; - display: flex; - flex-flow: row wrap; - transition: border 0.1s; - - > div { - color: ${textSecondary}; - transition: opacity 0.2s; - - &:first-child { - flex: 1; - display: flex; - flex-flow: row wrap; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - &:last-child { - padding-left: 2rem; - opacity: 0.25; - } - } - - &:hover { - > div:last-child { - opacity: 1; - } - } - - &:hover { - border-color: ${borderSecondary}; - } - } -`; diff --git a/src/modals/ConnectAccounts/ReadOnly/index.tsx b/src/modals/ConnectAccounts/ReadOnly/index.tsx deleted file mode 100644 index 40df4dd84b..0000000000 --- a/src/modals/ConnectAccounts/ReadOnly/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faCog, faGlasses } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useConnect } from 'contexts/Connect'; -import { ExternalAccount, ImportedAccount } from 'contexts/Connect/types'; -import { ReadOnlyInput } from '../ReadOnlyInput'; -import { ReadOnlyProps } from '../types'; -import { ExtensionWrapper } from '../Wrappers'; -import { Wrapper } from './Wrapper'; - -export const ReadOnly = (props: ReadOnlyProps) => { - const { setReadOnlyOpen, readOnlyOpen } = props; - - const { accounts, forgetAccounts } = useConnect(); - - // get all external accounts - const externalAccountsOnly = accounts.filter((a: ImportedAccount) => { - return a.source === 'external'; - }) as Array<ExternalAccount>; - - // get external accounts added by user - const externalAccountsByUser = externalAccountsOnly.filter( - (a: ExternalAccount) => a.addedBy === 'user' - ); - - // forget account - const forgetAccount = (account: ExternalAccount) => { - forgetAccounts([account]); - }; - return ( - <Wrapper> - <ExtensionWrapper noSpacing> - <button - type="button" - onClick={() => { - setReadOnlyOpen(!readOnlyOpen); - }} - > - <FontAwesomeIcon - icon={faGlasses} - transform="grow-2" - style={{ margin: '0 0.75rem 0 1.25rem' }} - /> - <h3> - <span className="name">Read Only Accounts</span> - </h3> - - <div> - <h3> - <span - className={`message${ - externalAccountsByUser.length ? ` success` : `` - }`} - > - {externalAccountsByUser.length - ? `${externalAccountsByUser.length} Connected` - : ``} - </span> - </h3> - {!readOnlyOpen && <FontAwesomeIcon icon={faCog} className="icon" />} - </div> - </button> - </ExtensionWrapper> - {readOnlyOpen && ( - <div className="content"> - <ReadOnlyInput /> - {externalAccountsByUser.length > 0 && ( - <h5> - {externalAccountsByUser.length} Read Only Account - {externalAccountsByUser.length === 1 ? '' : 's'} - </h5> - )} - <div className="accounts"> - {externalAccountsByUser.map((a: ExternalAccount, i: number) => ( - <div key={`user_external_account_${i}`} className="account"> - <div>{a.address}</div> - <button - type="button" - onClick={() => { - forgetAccount(a); - }} - > - Forget - </button> - </div> - ))} - </div> - </div> - )} - </Wrapper> - ); -}; diff --git a/src/modals/ConnectAccounts/ReadOnlyInput/Wrapper.ts b/src/modals/ConnectAccounts/ReadOnlyInput/Wrapper.ts deleted file mode 100644 index c1c95bb873..0000000000 --- a/src/modals/ConnectAccounts/ReadOnlyInput/Wrapper.ts +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; -import { borderPrimary, textDanger, textSecondary, textSuccess } from 'theme'; - -export const Wrapper = styled.div` - width: 100%; - margin-top: 0.5rem; - - .input { - border: 1px solid ${borderPrimary}; - border-radius: 1rem; - display: flex; - flex-flow: row wrap; - align-items: center; - padding: 0.25rem 0.5rem 0.25rem 1rem; - - > section { - display: flex; - flex-flow: column wrap; - - > input { - width: 100%; - border: none; - padding-right: 1rem; - } - - &:first-child { - flex: 1; - } - } - } - h5 { - margin: 0.75rem 0.25rem; - &.neutral { - color: ${textSecondary}; - opacity: 0.8; - } - &.danger { - color: ${textDanger}; - } - &.success { - color: ${textSuccess}; - } - } -`; diff --git a/src/modals/ConnectAccounts/ReadOnlyInput/index.tsx b/src/modals/ConnectAccounts/ReadOnlyInput/index.tsx deleted file mode 100644 index cc3ae58837..0000000000 --- a/src/modals/ConnectAccounts/ReadOnlyInput/index.tsx +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ButtonSecondary } from '@rossbulat/polkadot-dashboard-ui'; -import { useConnect } from 'contexts/Connect'; -import { ImportedAccount } from 'contexts/Connect/types'; -import React, { useState } from 'react'; -import { isValidAddress } from 'Utils'; -import { Wrapper } from './Wrapper'; - -export const ReadOnlyInput = () => { - const { formatAccountSs58, accounts, addExternalAccount } = useConnect(); - - // store current input value - const [value, setValue] = useState(''); - - // store whether current input value is valid - const [valid, setValid] = useState<string | null>(null); - - // store whether address was formatted (displays confirm prompt) - const [reformatted, setReformatted] = useState(false); - - const handleChange = (e: React.FormEvent<HTMLInputElement>) => { - const newValue = e.currentTarget.value; - // set value on key change - setValue(newValue); - - // reset reformatted if true - value has changed - if (reformatted) { - setReformatted(false); - } - - // reset valid if empty value - if (newValue === '') { - setValid(null); - return; - } - // check address already imported - const alreadyImported = accounts.find( - (a: ImportedAccount) => a.address.toUpperCase() === newValue.toUpperCase() - ); - if (alreadyImported !== undefined) { - setValid('already_imported'); - return; - } - // check if valid address - setValid(isValidAddress(newValue) ? 'valid' : 'not_valid'); - }; - - const handleImport = () => { - // reformat address if in wrong format - const addressFormatted = formatAccountSs58(value); - if (addressFormatted) { - setValid('confirm_reformat'); - setValue(addressFormatted); - setReformatted(true); - } else { - // add as external account - addExternalAccount(value, 'user'); - // reset state - setReformatted(false); - setValue(''); - setValid(null); - } - }; - - let label; - let labelClass; - switch (valid) { - case 'confirm_reformat': - label = 'Address was reformatted. Please confirm.'; - labelClass = 'neutral'; - - break; - case 'already_imported': - label = 'Address Already Imported'; - labelClass = 'danger'; - break; - case 'not_valid': - label = 'Address Invalid'; - labelClass = 'danger'; - break; - case 'valid': - label = 'Valid Address'; - labelClass = 'success'; - break; - default: - label = 'Input Address'; - labelClass = 'neutral'; - } - - const handleConfirm = () => { - setValid('valid'); - setReformatted(false); - handleImport(); - }; - - return ( - <Wrapper> - <h5 className={labelClass}>{label}</h5> - <div className="input"> - <section> - <input - placeholder="Address" - type="text" - onChange={(e: React.FormEvent<HTMLInputElement>) => handleChange(e)} - value={value} - /> - </section> - <section> - {!reformatted ? ( - <ButtonSecondary - onClick={() => handleImport()} - text="Import" - disabled={valid !== 'valid'} - /> - ) : ( - <ButtonSecondary onClick={() => handleConfirm()} text="Confirm" /> - )} - </section> - </div> - </Wrapper> - ); -}; - -export default ReadOnlyInput; diff --git a/src/modals/ConnectAccounts/Wrappers.ts b/src/modals/ConnectAccounts/Wrappers.ts deleted file mode 100644 index 29f8d602bb..0000000000 --- a/src/modals/ConnectAccounts/Wrappers.ts +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { motion } from 'framer-motion'; -import styled from 'styled-components'; -import { - backgroundToggle, - borderPrimary, - buttonPrimaryBackground, - modalBackground, - textDanger, - textInvert, - textPrimary, - textSecondary, - textSuccess, -} from 'theme'; - -export const CardsWrapper = styled(motion.div)` - width: 200%; - display: flex; - flex-flow: row nowrap; - overflow: hidden; - position: relative; -`; - -export const ContentWrapper = styled.div` - border-radius: 1rem; - display: flex; - flex-flow: column nowrap; - width: 50%; - height: auto; - padding: 0 1rem 1rem 1rem; -`; - -export const Wrapper = styled.div` - display: flex; - flex-flow: column wrap; - align-items: center; - justify-content: flex-start; - padding: 0; - width: 100%; - overflow: hidden; - - h1 { - color: ${textPrimary}; - font-size: 1.4rem; - font-family: 'Unbounded', 'sans-serif', sans-serif; - padding: 0.5rem 0.5rem 0 0.5rem; - } - - h3 { - color: ${textPrimary}; - - &.heading { - border-bottom: 1px solid ${borderPrimary}; - padding-bottom: 0.75rem; - margin: 2rem 0 1rem 0; - } - } - - .head { - width: 100%; - display: flex; - flex-flow: row wrap; - align-items: center; - margin: 0.5rem 0 1.5rem 0; - - > div:last-child { - display: flex; - flex-flow: row-reverse wrap; - flex-grow: 1; - - button { - background: none; - opacity: 0.85; - } - } - } -`; - -export const PaddingWrapper = styled.div` - padding: 1rem 0 0.5rem 0rem; - height: auto; -`; - -export const AccountGroupWrapper = styled(motion.button)` - border-radius: 1rem; - width: 100%; - display: flex; - flex-flow: row wrap; - align-items: center; - background: ${buttonPrimaryBackground}; - margin-bottom: 1rem; - transition: background 0.15s; - - > section { - display: flex; - flex-flow: row wrap; - flex-basis: 100%; - - @media (min-width: 800px) { - flex-basis: 50%; - - &:first-child { - padding-right: 0.25rem; - } - &:last-child { - padding-left: 0.25rem; - } - } - - > h5 { - margin: 0 0 0.25rem 0; - opacity: 0.75; - } - - > div { - margin: 0.4rem 0; - > button, - > div { - border-radius: 0.75rem; - border: 1px solid ${modalBackground}; - margin: 0; - } - } - } - - &:hover { - background: ${backgroundToggle}; - > section > div { - > button, - > div { - background: ${backgroundToggle}; - } - } - } -`; - -export const AccountWrapper = styled.div` - width: 100%; - margin: 0.5rem 0; - - > button { - &:hover { - background: ${backgroundToggle}; - } - &:disabled { - cursor: default; - border: 2px solid rgba(242, 185, 27, 0.25); - } - } - - > div, - button { - width: 100%; - border-radius: 0.75rem; - font-size: 1rem; - background: ${buttonPrimaryBackground}; - transition: background 0.15s; - color: ${textPrimary}; - display: flex; - flex-flow: row nowrap; - align-items: center; - min-height: 3.5rem; - padding-left: 0.4rem; - padding-right: 0.4rem; - - > div { - display: flex; - flex-flow: row nowrap; - justify-content: flex-start; - align-items: center; - padding: 0 0.25rem; - - &:first-child { - flex-shrink: 1; - overflow: hidden; - .name { - max-width: 100%; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - } - - &:last-child { - flex-grow: 1; - justify-content: flex-end; - } - - &.neutral { - h5 { - color: ${textSecondary}; - opacity: 0.75; - } - } - &.danger { - h5 { - color: ${textDanger}; - } - } - .icon { - width: 1.05rem; - height: 1.05rem; - margin-left: 0.75rem; - } - - /* svg theming */ - svg { - .light { - fill: ${textInvert}; - } - .dark { - fill: ${textSecondary}; - } - } - } - } -`; - -export const ExtensionWrapper = styled.div<{ noSpacing?: boolean }>` - width: 100%; - - > button, - > div { - width: 100%; - margin: ${(props) => (props.noSpacing ? 0 : '1rem 0')}; - padding: ${(props) => (props.noSpacing ? 0 : '1rem 0.25rem')}; - font-size: 1rem; - background: ${buttonPrimaryBackground}; - border-radius: 0.75rem; - transition: background 0.15s; - color: ${textPrimary}; - display: flex; - flex-flow: row nowrap; - align-items: center; - min-height: 3.5rem; - - > div { - display: flex; - flex-flow: row nowrap; - justify-content: flex-start; - align-items: center; - padding: 0 1rem; - h3, - h4 { - margin: 0; - padding: 0; - } - span { - margin-right: 1.25rem; - &.name { - max-width: 100%; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - &.message { - opacity: 0.75; - } - } - &:first-child { - flex-shrink: 1; - overflow: hidden; - } - &:last-child { - flex-grow: 1; - justify-content: flex-end; - .icon { - margin-left: 1rem; - } - } - } - .neutral { - color: ${textSecondary}; - } - .danger { - color: ${textDanger}; - } - .success { - color: ${textSuccess}; - } - /* svg theming */ - svg { - .light { - fill: ${textInvert}; - } - .dark { - fill: ${textSecondary}; - } - } - } - > button { - padding: 0 0.2rem; - &:hover { - background: ${backgroundToggle}; - } - - &:disabled { - cursor: default; - opacity: 1; - &:hover { - background: ${buttonPrimaryBackground}; - } - } - } -`; - -export const Separator = styled.div` - border-top: 1px solid ${textSecondary}; - width: 100%; - opacity: 0.1; - margin: 1.5rem 0rem; -`; diff --git a/src/modals/ConnectAccounts/index.tsx b/src/modals/ConnectAccounts/index.tsx deleted file mode 100644 index 3d2e1b2470..0000000000 --- a/src/modals/ConnectAccounts/index.tsx +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useConnect } from 'contexts/Connect'; -import { ImportedAccount } from 'contexts/Connect/types'; -import { useExtensions } from 'contexts/Extensions'; -import { useModal } from 'contexts/Modal'; -import { useEffect, useRef, useState } from 'react'; -import { Accounts } from './Accounts'; -import { Extensions } from './Extensions'; -import { CardsWrapper, Wrapper } from './Wrappers'; - -export const ConnectAccounts = () => { - const modal = useModal(); - const { extensions } = useExtensions(); - const { activeAccount } = useConnect(); - let { accounts } = useConnect(); - const { config } = modal; - const _section = config?.section ?? null; - - // active section of modal - const [section, setSection] = useState( - _section !== null ? _section : activeAccount !== null ? 1 : 0 - ); - - // toggle read only management - const [readOnlyOpen, setReadOnlyOpen] = useState(false); - - // resize modal on state change - const extensionsRef = useRef<HTMLDivElement>(null); - const accountsRef = useRef<HTMLDivElement>(null); - - const resizeModal = () => { - let _height = 0; - if (section === 0) { - _height = extensionsRef.current?.clientHeight ?? 0; - } else if (section === 1) { - _height = accountsRef.current?.clientHeight ?? 0; - } - modal.setModalHeight(_height); - }; - - useEffect(() => { - resizeModal(); - }, [ - section, - activeAccount, - accounts, - extensions, - modal.height, - readOnlyOpen, - ]); - - // remove active account from connect list - accounts = accounts.filter( - (item: ImportedAccount) => item.address !== activeAccount - ); - - return ( - <Wrapper> - <CardsWrapper - animate={section === 0 ? 'home' : 'next'} - initial={false} - transition={{ - duration: 0.5, - type: 'spring', - bounce: 0.1, - }} - variants={{ - home: { - left: 0, - }, - next: { - left: '-100%', - }, - }} - > - <Extensions - setSection={setSection} - readOnlyOpen={readOnlyOpen} - setReadOnlyOpen={setReadOnlyOpen} - ref={extensionsRef} - /> - <Accounts setSection={setSection} ref={accountsRef} /> - </CardsWrapper> - </Wrapper> - ); -}; - -export default ConnectAccounts; diff --git a/src/modals/ConnectAccounts/types.ts b/src/modals/ConnectAccounts/types.ts deleted file mode 100644 index bc667880eb..0000000000 --- a/src/modals/ConnectAccounts/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { BalanceLedger } from 'contexts/Balances/types'; -import { ExtensionAccount } from 'contexts/Connect/types'; -import { FunctionComponent, SVGProps } from 'react'; -import { MaybeAccount } from 'types'; - -export interface ExtensionProps { - meta: ExtensionMetaProps; - setSection: (n: number) => void; - installed?: any; - size?: string; - message?: string; - flag?: boolean; - status?: string; -} - -export interface ExtensionMetaProps { - id: string; - title: string; - icon: FunctionComponent< - SVGProps<SVGSVGElement> & { title?: string | undefined } - >; - status?: string; -} - -export interface AccountElementProps { - meta: ExtensionAccount | null; - address?: MaybeAccount; - label?: string[]; - disconnect?: boolean; - asElement?: boolean; -} - -export interface ReadOnlyProps { - setReadOnlyOpen: (k: boolean) => void; - readOnlyOpen: boolean; -} - -export interface forwardRefProps { - setSection?: any; - readOnlyOpen: boolean; - setReadOnlyOpen: (e: boolean) => void; -} - -export interface ControllerAccount { - address: string; - ledger: BalanceLedger; -} - -export interface StashAcount { - address: string; - controller: MaybeAccount; -} - -export interface ActivelyStakingAccount { - stash: MaybeAccount; - controller: MaybeAccount; - stashImported: boolean; - controllerImported: boolean; -} diff --git a/src/modals/LeavePool/Wrapper.ts b/src/modals/LeavePool/Wrapper.ts deleted file mode 100644 index 8694a143dc..0000000000 --- a/src/modals/LeavePool/Wrapper.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; - -export const ContentWrapper = styled.div` - width: 100%; -`; diff --git a/src/modals/LeavePool/index.tsx b/src/modals/LeavePool/index.tsx deleted file mode 100644 index 461376e7ae..0000000000 --- a/src/modals/LeavePool/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; -import { Title } from 'library/Modal/Title'; -import { UnbondAll } from 'modals/UpdateBond/Forms/UnbondAll'; -import { PaddingWrapper } from '../Wrappers'; - -export const LeavePool = () => { - return ( - <> - <Title title="Leave Pool" icon={faSignOutAlt} /> - <PaddingWrapper> - <UnbondAll /> - </PaddingWrapper> - </> - ); -}; diff --git a/src/modals/ManagePool/Forms.tsx b/src/modals/ManagePool/Forms.tsx deleted file mode 100644 index c546fc7d35..0000000000 --- a/src/modals/ManagePool/Forms.tsx +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faArrowAltCircleUp } from '@fortawesome/free-regular-svg-icons'; -import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { u8aToString, u8aUnwrapBytes } from '@polkadot/util'; -import { ButtonSubmit } from '@rossbulat/polkadot-dashboard-ui'; -import { useApi } from 'contexts/Api'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { BondedPool, PoolState } from 'contexts/Pools/types'; -import { useTxFees } from 'contexts/TxFees'; -import { EstimatedTxFee } from 'library/EstimatedTxFee'; -import { Warning } from 'library/Form/Warning'; -import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; -import React, { forwardRef, useEffect, useState } from 'react'; -import { Separator } from 'Wrappers'; -import { FooterWrapper, NotesWrapper } from '../Wrappers'; -import { ContentWrapper } from './Wrappers'; - -export const Forms = forwardRef((props: any, ref: any) => { - const { setSection, task, section } = props; - - const { api } = useApi(); - const { setStatus: setModalStatus } = useModal(); - const { activeAccount, accountHasSigner } = useConnect(); - const { isOwner, isStateToggler, selectedActivePool } = useActivePools(); - const { bondedPools, meta, updateBondedPools, getBondedPool } = - useBondedPools(); - const { txFeesValid } = useTxFees(); - const poolId = selectedActivePool?.id; - - // valid to submit transaction - const [valid, setValid] = useState<boolean>(false); - - // updated metadata value - const [metadata, setMetadata] = useState<string>(''); - - // ensure account has relevant roles for task - const canToggle = - (isOwner() || isStateToggler()) && - ['destroy_pool', 'unlock_pool', 'lock_pool'].includes(task); - const canRename = isOwner() && task === 'set_pool_metadata'; - const isValid = canToggle || canRename; - - // determine current pool metadata and set in state - useEffect(() => { - if (task === 'set_pool_metadata') { - let _metadata = ''; - const pool = bondedPools.find((p: any) => { - return p.addresses.stash === selectedActivePool?.addresses.stash; - }); - - if (pool) { - const metadataBatch = meta.bonded_pools?.metadata ?? []; - const batchIndex = bondedPools.indexOf(pool); - _metadata = metadataBatch[batchIndex]; - setMetadata(u8aToString(u8aUnwrapBytes(_metadata))); - } - } - }, [section]); - - useEffect(() => { - setValid(isValid); - }, [isValid]); - - const content = (() => { - let title; - let message; - switch (task) { - case 'set_pool_metadata': - title = undefined; - message = ( - <p> - Your updated name will be stored on-chain as encoded bytes. The - update will take effect immediately. - </p> - ); - break; - case 'destroy_pool': - title = <h2>Destroying a Pool is Irreversible</h2>; - message = ( - <p> - Once you Destroy the pool, all members can be permissionlessly - unbonded, and the pool can never be reopened. - </p> - ); - break; - case 'unlock_pool': - title = <h2>Submit Pool Unlock</h2>; - message = <p>Once you Unlock the pool new people can join the pool.</p>; - break; - case 'lock_pool': - title = <h2>Submit Pool Lock</h2>; - message = <p>Once you Lock the pool no one else can join the pool.</p>; - break; - default: - title = null; - message = null; - } - return { title, message }; - })(); - - const poolStateFromTask = (t: string) => { - switch (t) { - case 'destroy_pool': - return PoolState.Destroy; - case 'lock_pool': - return PoolState.Block; - default: - return PoolState.Open; - } - }; - - // tx to submit - const getTx = () => { - let tx = null; - - if (!valid || !api) { - return tx; - } - - // remove decimal errors - switch (task) { - case 'set_pool_metadata': - tx = api.tx.nominationPools.setMetadata(poolId, metadata); - break; - case 'destroy_pool': - tx = api.tx.nominationPools.setState(poolId, PoolState.Destroy); - break; - case 'unlock_pool': - tx = api.tx.nominationPools.setState(poolId, PoolState.Open); - break; - case 'lock_pool': - tx = api.tx.nominationPools.setState(poolId, PoolState.Block); - break; - default: - tx = null; - } - - return tx; - }; - - const { submitTx, submitting } = useSubmitExtrinsic({ - tx: getTx(), - from: activeAccount, - shouldSubmit: true, - callbackSubmit: () => { - setModalStatus(2); - }, - callbackInBlock: () => { - // reflect updated state in bondedPools list - if ( - ['destroy_pool', 'unlock_pool', 'lock_pool'].includes(task) && - poolId - ) { - const pool: BondedPool | null = getBondedPool(poolId); - - if (pool) { - updateBondedPools([ - { - ...pool, - state: poolStateFromTask(task), - }, - ]); - } - } - }, - }); - - const handleMetadataChange = (e: React.FormEvent<HTMLInputElement>) => { - const newValue = e.currentTarget.value; - setMetadata(newValue); - // any string is valid metadata - setValid(true); - }; - - return ( - <ContentWrapper> - <div className="items" ref={ref}> - {!accountHasSigner(activeAccount) && ( - <Warning text="Your account is read only, and cannot sign transactions." /> - )} - <div> - <> - {/* include task title if present */} - {content.title !== undefined && ( - <> - {content.title} - <Separator /> - </> - )} - - {/* include form element if task is to set metadata */} - {task === 'set_pool_metadata' && ( - <> - <h2>Update Pool Name</h2> - <input - className="textbox" - style={{ width: '100%' }} - placeholder="Pool Name" - type="text" - onChange={(e: React.FormEvent<HTMLInputElement>) => - handleMetadataChange(e) - } - value={metadata ?? ''} - /> - </> - )} - - <NotesWrapper> - {content.message} - <EstimatedTxFee /> - </NotesWrapper> - </> - </div> - <FooterWrapper> - <div> - <button - type="button" - className="submit secondary" - onClick={() => setSection(0)} - disabled={submitting} - > - <FontAwesomeIcon - transform="grow-2" - icon={faChevronLeft as IconProp} - /> - Back - </button> - </div> - <div> - <ButtonSubmit - text={`Submit${submitting ? 'ting' : ''}`} - iconLeft={faArrowAltCircleUp} - iconTransform="grow-2" - onClick={() => submitTx()} - disabled={ - submitting || - !accountHasSigner(activeAccount) || - !valid || - !txFeesValid - } - /> - </div> - </FooterWrapper> - </div> - </ContentWrapper> - ); -}); - -export default Forms; diff --git a/src/modals/Nominate/index.tsx b/src/modals/Nominate/index.tsx deleted file mode 100644 index a842675c0f..0000000000 --- a/src/modals/Nominate/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faArrowAltCircleUp } from '@fortawesome/free-regular-svg-icons'; -import { faPlayCircle } from '@fortawesome/free-solid-svg-icons'; -import { ButtonSubmit } from '@rossbulat/polkadot-dashboard-ui'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useStaking } from 'contexts/Staking'; -import { useTxFees } from 'contexts/TxFees'; -import { EstimatedTxFee } from 'library/EstimatedTxFee'; -import { Warning } from 'library/Form/Warning'; -import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; -import { Title } from 'library/Modal/Title'; -import { useEffect, useState } from 'react'; -import { planckBnToUnit } from 'Utils'; -import { - FooterWrapper, - NotesWrapper, - PaddingWrapper, - Separator, -} from '../Wrappers'; - -export const Nominate = () => { - const { api, network } = useApi(); - const { activeAccount } = useConnect(); - const { targets, staking, getControllerNotImported } = useStaking(); - const { getBondedAccount, getLedgerForStash } = useBalances(); - const { setStatus: setModalStatus } = useModal(); - const { txFeesValid } = useTxFees(); - const { units } = network; - const { minNominatorBond } = staking; - const controller = getBondedAccount(activeAccount); - const { nominations } = targets; - const ledger = getLedgerForStash(activeAccount); - const { active } = ledger; - - const activeBase = planckBnToUnit(active, units); - const minNominatorBondBase = planckBnToUnit(minNominatorBond, units); - - // valid to submit transaction - const [valid, setValid] = useState<boolean>(false); - - // ensure selected key is valid - useEffect(() => { - setValid(nominations.length > 0 && activeBase >= minNominatorBondBase); - }, [targets]); - - // tx to submit - const getTx = () => { - let tx = null; - if (!valid || !api) { - return tx; - } - const targetsToSubmit = nominations.map((item: any) => { - return { - Id: item.address, - }; - }); - tx = api.tx.staking.nominate(targetsToSubmit); - return tx; - }; - - const { submitTx, submitting } = useSubmitExtrinsic({ - tx: getTx(), - from: controller, - shouldSubmit: valid, - callbackSubmit: () => { - setModalStatus(2); - }, - callbackInBlock: () => {}, - }); - - // warnings - const warnings = []; - if (getControllerNotImported(controller)) { - warnings.push( - 'You must have your controller account imported to start nominating' - ); - } - if (!nominations.length) { - warnings.push('You have no nominations set.'); - } - if (activeBase < minNominatorBondBase) { - warnings.push( - `You do not meet the minimum nominator bond of ${minNominatorBondBase} ${network.unit}. Please bond some funds before nominating.` - ); - } - - return ( - <> - <Title title="Nominate" icon={faPlayCircle} /> - <PaddingWrapper verticalOnly> - <div style={{ padding: '0 1rem', width: '100%' }}> - {warnings.map((text: any, index: number) => ( - <Warning key={index} text={text} /> - ))} - <h2> - You Have {nominations.length} Nomination - {nominations.length === 1 ? '' : 's'} - </h2> - <Separator /> - <NotesWrapper> - <p> - Once submitted, you will start nominating your chosen validators. - </p> - <EstimatedTxFee /> - </NotesWrapper> - <FooterWrapper> - <div> - <ButtonSubmit - text={`Submit${submitting ? 'ting' : ''}`} - iconLeft={faArrowAltCircleUp} - iconTransform="grow-2" - onClick={() => submitTx()} - disabled={ - !valid || submitting || warnings.length > 0 || !txFeesValid - } - /> - </div> - </FooterWrapper> - </div> - </PaddingWrapper> - </> - ); -}; - -export default Nominate; diff --git a/src/modals/NominateFromFavorites/Wrappers.ts b/src/modals/NominateFromFavorites/Wrappers.ts deleted file mode 100644 index 5692daea1a..0000000000 --- a/src/modals/NominateFromFavorites/Wrappers.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; -import { networkColor, textSecondary } from 'theme'; - -export const ListWrapper = styled.div` - display: flex; - flex-flow: column wrap; - align-items: center; - justify-content: flex-start; - position: relative; - width: 100%; - - > div, - h3 { - width: 100%; - } -`; - -export const FooterWrapper = styled.div` - position: relative; - bottom: 0px; - left: 0px; - margin: 1rem 0; - width: 100%; - display: flex; - flex-flow: row wrap; - - button { - font-size: 1.2rem; - color: ${networkColor}; - - &:disabled { - opacity: 0.5; - color: ${textSecondary}; - } - } -`; diff --git a/src/modals/NominateFromFavorites/index.tsx b/src/modals/NominateFromFavorites/index.tsx deleted file mode 100644 index 1bbd55e236..0000000000 --- a/src/modals/NominateFromFavorites/index.tsx +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faArrowAltCircleUp } from '@fortawesome/free-solid-svg-icons'; -import { ButtonSubmit } from '@rossbulat/polkadot-dashboard-ui'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { useTxFees } from 'contexts/TxFees'; -import { useValidators } from 'contexts/Validators'; -import { Validator } from 'contexts/Validators/types'; -import { EstimatedTxFee } from 'library/EstimatedTxFee'; -import { Warning } from 'library/Form/Warning'; -import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; -import { Title } from 'library/Modal/Title'; -import { ValidatorList } from 'library/ValidatorList'; -import { useEffect, useState } from 'react'; -import { FooterWrapper, NotesWrapper, PaddingWrapper } from '../Wrappers'; -import { ListWrapper } from './Wrappers'; - -export const NominateFromFavorites = () => { - const { consts, api } = useApi(); - const { activeAccount, accountHasSigner } = useConnect(); - const { getBondedAccount } = useBalances(); - const { config, setStatus: setModalStatus, setResize } = useModal(); - const { favoritesList } = useValidators(); - const { selectedActivePool, isNominator, isOwner } = useActivePools(); - const controller = getBondedAccount(activeAccount); - const { txFeesValid } = useTxFees(); - - const { maxNominations } = consts; - const { bondType, nominations } = config; - const signingAccount = bondType === 'pool' ? activeAccount : controller; - - // store filtered favorites - const [availableFavorites, setAvailableFavorites] = useState< - Array<Validator> - >([]); - - // store selected favorites in local state - const [selectedFavorites, setSelectedFavorites] = useState<Array<Validator>>( - [] - ); - - // store filtered favorites - useEffect(() => { - if (favoritesList) { - const _availableFavorites = favoritesList.filter( - (favorite: Validator) => - !nominations.find( - (nomination: string) => nomination === favorite.address - ) && !favorite.prefs.blocked - ); - setAvailableFavorites(_availableFavorites); - } - }, []); - - // calculate active + selected favorites - const nominationsToSubmit = nominations.concat( - selectedFavorites.map((favorite: Validator) => favorite.address) - ); - - // valid to submit transaction - const [valid, setValid] = useState<boolean>(false); - - useEffect(() => { - setResize(); - }, [selectedFavorites]); - - // ensure selected list is within limits - useEffect(() => { - setValid( - nominationsToSubmit.length > 0 && - nominationsToSubmit.length <= maxNominations && - selectedFavorites.length > 0 - ); - }, [selectedFavorites]); - - const batchKey = 'nominate_from_favorites'; - - const onSelected = (provider: any) => { - const { selected } = provider; - setSelectedFavorites(selected); - }; - - const totalAfterSelection = nominations.length + selectedFavorites.length; - const overMaxNominations = totalAfterSelection > maxNominations; - - // tx to submit - const getTx = () => { - let tx = null; - if (!valid || !api) { - return tx; - } - - const targetsToSubmit = nominationsToSubmit.map((item: any) => - bondType === 'pool' - ? item - : { - Id: item, - } - ); - - if (bondType === 'pool') { - tx = api.tx.nominationPools.nominate( - selectedActivePool?.id, - targetsToSubmit - ); - } else { - tx = api.tx.staking.nominate(targetsToSubmit); - } - return tx; - }; - - const { submitTx, submitting } = useSubmitExtrinsic({ - tx: getTx(), - from: signingAccount, - shouldSubmit: valid, - callbackSubmit: () => { - setModalStatus(2); - }, - callbackInBlock: () => {}, - }); - - return ( - <> - <Title title="Nominate Favorites" /> - <PaddingWrapper> - <div style={{ marginBottom: '1rem' }}> - {!accountHasSigner(signingAccount) && ( - <Warning - text={`You must have your${ - bondType === 'stake' ? ' controller' : ' ' - }account imported to add nominations.`} - /> - )} - </div> - <ListWrapper> - {availableFavorites.length > 0 ? ( - <ValidatorList - bondType="stake" - validators={availableFavorites} - batchKey={batchKey} - title="Favorite Validators / Not Nominated" - selectable - selectActive - selectToggleable={false} - onSelected={onSelected} - showMenu={false} - inModal - allowMoreCols - refetchOnListUpdate - /> - ) : ( - <h3>No Favorites Available.</h3> - )} - </ListWrapper> - <NotesWrapper style={{ paddingBottom: 0 }}> - <EstimatedTxFee /> - </NotesWrapper> - <FooterWrapper> - <h3 - className={ - selectedFavorites.length === 0 || - nominationsToSubmit.length > maxNominations - ? '' - : 'active' - } - > - {selectedFavorites.length > 0 - ? overMaxNominations - ? `Adding this many favorites will surpass ${maxNominations} nominations.` - : `Adding ${selectedFavorites.length} Nomination${ - selectedFavorites.length !== 1 ? `s` : `` - }` - : `No Favorites Selected`} - </h3> - <div> - <ButtonSubmit - text={`Submit${submitting ? 'ting' : ''}`} - iconLeft={faArrowAltCircleUp} - iconTransform="grow-2" - onClick={() => submitTx()} - disabled={ - !valid || - submitting || - (bondType === 'pool' && !isNominator() && !isOwner()) || - !accountHasSigner(signingAccount) || - !txFeesValid - } - /> - </div> - </FooterWrapper> - </PaddingWrapper> - </> - ); -}; - -export default NominateFromFavorites; diff --git a/src/modals/NominatePool/index.tsx b/src/modals/NominatePool/index.tsx deleted file mode 100644 index 46df800d8e..0000000000 --- a/src/modals/NominatePool/index.tsx +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faArrowAltCircleUp } from '@fortawesome/free-regular-svg-icons'; -import { faPlayCircle } from '@fortawesome/free-solid-svg-icons'; -import { ButtonSubmit } from '@rossbulat/polkadot-dashboard-ui'; -import { useApi } from 'contexts/Api'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { useTxFees } from 'contexts/TxFees'; -import { EstimatedTxFee } from 'library/EstimatedTxFee'; -import { Warning } from 'library/Form/Warning'; -import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; -import { Title } from 'library/Modal/Title'; -import { useEffect, useState } from 'react'; -import { - FooterWrapper, - NotesWrapper, - PaddingWrapper, - Separator, -} from '../Wrappers'; - -export const NominatePool = () => { - const { api } = useApi(); - const { setStatus: setModalStatus } = useModal(); - const { activeAccount, accountHasSigner } = useConnect(); - const { selectedActivePool, isOwner, isNominator, targets } = - useActivePools(); - const { txFeesValid } = useTxFees(); - const { nominations } = targets; - - // valid to submit transaction - const [valid, setValid] = useState<boolean>(false); - - const poolId = selectedActivePool?.id ?? null; - - // ensure selected roles are valid - const isValid = - (poolId !== null && isNominator() && nominations.length > 0) ?? false; - useEffect(() => { - setValid(isValid); - }, [isValid]); - - // tx to submit - const getTx = () => { - let tx = null; - if (!valid || !api) { - return tx; - } - const targetsToSubmit = nominations.map((item: any) => item.address); - tx = api.tx.nominationPools.nominate(poolId, targetsToSubmit); - return tx; - }; - - const { submitTx, submitting } = useSubmitExtrinsic({ - tx: getTx(), - from: activeAccount, - shouldSubmit: valid, - callbackSubmit: () => { - setModalStatus(2); - }, - callbackInBlock: () => {}, - }); - - // warnings - const warnings = []; - if (!accountHasSigner(activeAccount)) { - warnings.push('Your account is read only, and cannot sign transactions.'); - } - if (!nominations.length) { - warnings.push('You have no nominations set.'); - } - if (!isOwner() || !isNominator()) { - warnings.push(`You do not have a nominator role in any pools.`); - } - - return ( - <> - <Title title="Nominate" icon={faPlayCircle} /> - <PaddingWrapper verticalOnly> - <div style={{ padding: '0 1rem', width: '100%' }}> - {warnings.map((text: string, index: number) => ( - <Warning key={`warning_${index}`} text={text} /> - ))} - <h2> - You Have {nominations.length} Nomination - {nominations.length === 1 ? '' : 's'} - </h2> - <Separator /> - <NotesWrapper> - <p> - Once submitted, you will start nominating your chosen validators. - </p> - <EstimatedTxFee /> - </NotesWrapper> - <FooterWrapper> - <div> - <ButtonSubmit - text={`Submit${submitting ? 'ting' : ''}`} - iconLeft={faArrowAltCircleUp} - iconTransform="grow-2" - onClick={() => submitTx()} - disabled={ - !valid || - submitting || - warnings.length > 0 || - !accountHasSigner(activeAccount) || - !txFeesValid - } - /> - </div> - </FooterWrapper> - </div> - </PaddingWrapper> - </> - ); -}; - -export default NominatePool; diff --git a/src/modals/SelectFavorites/Wrappers.ts b/src/modals/SelectFavorites/Wrappers.ts deleted file mode 100644 index 559575732e..0000000000 --- a/src/modals/SelectFavorites/Wrappers.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; -import { networkColor, textSecondary } from 'theme'; - -export const ListWrapper = styled.div` - display: flex; - flex-flow: column wrap; - align-items: center; - justify-content: flex-start; - position: relative; - width: 100%; - - > div { - width: 100%; - } -`; - -export const FooterWrapper = styled.div` - position: relative; - bottom: 0px; - left: 0px; - margin: 1rem 0; - width: 100%; - display: flex; - flex-flow: row wrap; - - button { - font-size: 1.2rem; - color: ${networkColor}; - - &:disabled { - opacity: 0.5; - color: ${textSecondary}; - } - } -`; -export default ListWrapper; diff --git a/src/modals/SelectFavorites/index.tsx b/src/modals/SelectFavorites/index.tsx deleted file mode 100644 index d53d955898..0000000000 --- a/src/modals/SelectFavorites/index.tsx +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useApi } from 'contexts/Api'; -import { useModal } from 'contexts/Modal'; -import { useValidators } from 'contexts/Validators'; -import { Validator } from 'contexts/Validators/types'; -import { Title } from 'library/Modal/Title'; -import { ValidatorList } from 'library/ValidatorList'; -import { useEffect, useState } from 'react'; -import { PaddingWrapper } from '../Wrappers'; -import { FooterWrapper, ListWrapper } from './Wrappers'; - -export const SelectFavorites = () => { - const { consts } = useApi(); - const { config, setStatus, setResize } = useModal(); - const { favoritesList } = useValidators(); - const { maxNominations } = consts; - const { nominations, callback: generateNominationsCallback } = config; - - // store filtered favorites - const [availableFavorites, setAvailableFavorites] = useState< - Array<Validator> - >([]); - - // store selected favorites in local state - const [selectedFavorites, setSelectedFavorites] = useState([]); - - // store filtered favorites - useEffect(() => { - if (favoritesList) { - const _availableFavorites = favoritesList.filter( - (favorite: Validator) => - !nominations.find( - (nomination: Validator) => nomination.address === favorite.address - ) && !favorite.prefs.blocked - ); - setAvailableFavorites(_availableFavorites); - } - }, []); - - useEffect(() => { - setResize(); - }, [selectedFavorites]); - - const batchKey = 'favorite_validators'; - - const onSelected = (provider: any) => { - const { selected } = provider; - setSelectedFavorites(selected); - }; - - const submitSelectedFavorites = () => { - if (!selectedFavorites.length) return; - const newNominations = [...nominations].concat(...selectedFavorites); - generateNominationsCallback(newNominations); - setStatus(0); - }; - - const totalAfterSelection = nominations.length + selectedFavorites.length; - const overMaxNominations = totalAfterSelection > maxNominations; - - return ( - <> - <Title title="Add From Favorites" /> - <PaddingWrapper> - <ListWrapper> - {availableFavorites.length > 0 ? ( - <ValidatorList - bondType="stake" - validators={availableFavorites} - batchKey={batchKey} - title="Favorite Validators" - selectable - selectActive - selectToggleable={false} - onSelected={onSelected} - showMenu={false} - inModal - allowMoreCols - refetchOnListUpdate - /> - ) : ( - <h3>No Favorites Available.</h3> - )} - </ListWrapper> - <FooterWrapper> - <button - type="button" - disabled={!selectedFavorites.length || overMaxNominations} - onClick={() => submitSelectedFavorites()} - > - {selectedFavorites.length > 0 - ? overMaxNominations - ? `Adding this many favorites will surpass ${maxNominations} nominations.` - : `Add ${selectedFavorites.length} Favorite${ - selectedFavorites.length !== 1 ? `s` : `` - } to Nominations` - : `No Favorites Selected`} - </button> - </FooterWrapper> - </PaddingWrapper> - </> - ); -}; - -export default SelectFavorites; diff --git a/src/modals/UpdateBond/Forms/BondAll.tsx b/src/modals/UpdateBond/Forms/BondAll.tsx deleted file mode 100644 index 3b3edfec13..0000000000 --- a/src/modals/UpdateBond/Forms/BondAll.tsx +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { BN_ZERO } from '@polkadot/util'; -import BN from 'bn.js'; -import { useApi } from 'contexts/Api'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { useTransferOptions } from 'contexts/TransferOptions'; -import { useTxFees } from 'contexts/TxFees'; -import { EstimatedTxFee } from 'library/EstimatedTxFee'; -import { Warning } from 'library/Form/Warning'; -import useBondGreatestFee from 'library/Hooks/useBondGreatestFee'; -import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; -import { useEffect, useState } from 'react'; -import { planckBnToUnit } from 'Utils'; -import { Separator } from '../../Wrappers'; -import { FormsProps } from '../types'; -import { FormFooter } from './FormFooter'; - -export const BondAll = (props: FormsProps) => { - const { setSection, setLocalResize } = props; - - const { api, network } = useApi(); - const { units } = network; - const { setStatus: setModalStatus, config } = useModal(); - const { activeAccount, accountHasSigner } = useConnect(); - const { getTransferOptions } = useTransferOptions(); - const { bondType } = config; - const { txFees, txFeesValid } = useTxFees(); - const { selectedActivePool } = useActivePools(); - const largestTxFee = useBondGreatestFee({ bondType }); - - let { unclaimedRewards } = selectedActivePool || {}; - unclaimedRewards = unclaimedRewards ?? new BN(0); - unclaimedRewards = planckBnToUnit(unclaimedRewards, network.units); - - const isStaking = bondType === 'stake'; - const isPooling = bondType === 'pool'; - - const allTransferOptions = getTransferOptions(activeAccount); - const { freeBalance: freeBalanceBn } = allTransferOptions; - const { totalPossibleBond: totalPossibleBondBn } = isPooling - ? allTransferOptions.pool - : allTransferOptions.nominate; - - // convert BN values to number - const freeBalance = planckBnToUnit(freeBalanceBn, units); - - // local bond value - const [bond, setBond] = useState({ bond: freeBalance }); - - // bond minus tx fees - const bondAfterTxFees = BN.max(freeBalanceBn.sub(largestTxFee), BN_ZERO); - - // total possible bond after tx fees - const totalPossibleBond = planckBnToUnit( - BN.max(totalPossibleBondBn.sub(largestTxFee), BN_ZERO), - units - ); - - // bond valid - const [bondValid, setBondValid] = useState(false); - - // update bond value on task change - useEffect(() => { - const _bond = freeBalance; - setBond({ bond: _bond }); - if (_bond > 0 && bondAfterTxFees.gt(BN_ZERO)) { - setBondValid(true); - } else { - setBondValid(false); - } - }, [freeBalance, txFees]); - - // modal resize on form update - useEffect(() => { - if (setLocalResize) setLocalResize(); - }, [bond]); - - // tx to submit - const getTx = () => { - let tx = null; - if (!bondValid || !api || !activeAccount) { - return tx; - } - - // convert to submittable string - const bondToSubmit = bondAfterTxFees.toString(); - - // determine tx - if (isPooling) { - tx = api.tx.nominationPools.bondExtra({ FreeBalance: bondToSubmit }); - } else if (isStaking) { - tx = api.tx.staking.bondExtra(bondToSubmit); - } - return tx; - }; - - const { submitTx, submitting } = useSubmitExtrinsic({ - tx: getTx(), - from: activeAccount, - shouldSubmit: bondValid, - callbackSubmit: () => { - setModalStatus(2); - }, - callbackInBlock: () => {}, - }); - - return ( - <> - <div className="items"> - <> - {!accountHasSigner(activeAccount) && ( - <Warning text="Your account is read only, and cannot sign transactions." /> - )} - {freeBalance === 0 && ( - <Warning text={`You have no free ${network.unit} to bond.`} /> - )} - {unclaimedRewards > 0 && bondType === 'pool' && ( - <Warning - text={`Bonding will also withdraw your outstanding rewards of ${unclaimedRewards} ${network.unit}.`} - /> - )} - <h4>Amount to bond:</h4> - <h2> - {largestTxFee.eq(new BN(0)) - ? '...' - : `${planckBnToUnit(bondAfterTxFees, units)} ${network.unit}`} - </h2> - <p> - This amount of {network.unit} will be added to your current bonded - funds. - </p> - <Separator /> - <h4>New total bond:</h4> - <h2> - {largestTxFee.eq(new BN(0)) - ? '...' - : `${totalPossibleBond} ${network.unit}`} - </h2> - <Separator /> - <EstimatedTxFee /> - </> - </div> - <FormFooter - setSection={setSection} - submitTx={submitTx} - submitting={submitting} - isValid={bondValid && accountHasSigner(activeAccount) && txFeesValid} - /> - </> - ); -}; diff --git a/src/modals/UpdateBond/Forms/BondSome.tsx b/src/modals/UpdateBond/Forms/BondSome.tsx deleted file mode 100644 index 0361fc051f..0000000000 --- a/src/modals/UpdateBond/Forms/BondSome.tsx +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import BN, { max } from 'bn.js'; -import { useApi } from 'contexts/Api'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { useTransferOptions } from 'contexts/TransferOptions'; -import { useTxFees } from 'contexts/TxFees'; -import { EstimatedTxFee } from 'library/EstimatedTxFee'; -import { BondFeedback } from 'library/Form/Bond/BondFeedback'; -import { Warning } from 'library/Form/Warning'; -import { useBondGreatestFee } from 'library/Hooks/useBondGreatestFee'; -import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; -import { useEffect, useState } from 'react'; -import { planckBnToUnit, unitToPlanckBn } from 'Utils'; -import { NotesWrapper } from '../../Wrappers'; -import { FormsProps } from '../types'; -import { FormFooter } from './FormFooter'; - -export const BondSome = ({ setSection, setLocalResize }: FormsProps) => { - const { api, network } = useApi(); - const { units } = network; - const { setStatus: setModalStatus, config, setResize } = useModal(); - const { activeAccount, accountHasSigner } = useConnect(); - const { getTransferOptions } = useTransferOptions(); - const { txFeesValid } = useTxFees(); - const { selectedActivePool } = useActivePools(); - const { bondType } = config; - const isStaking = bondType === 'stake'; - const isPooling = bondType === 'pool'; - const { freeBalance: freeBalanceBn } = getTransferOptions(activeAccount); - const freeBalance = planckBnToUnit(freeBalanceBn, units); - const largestTxFee = useBondGreatestFee({ bondType }); - - // calculate any unclaimed pool rewards. - let { unclaimedRewards } = selectedActivePool || {}; - unclaimedRewards = unclaimedRewards ?? new BN(0); - unclaimedRewards = planckBnToUnit(unclaimedRewards, network.units); - - // local bond value. - const [bond, setBond] = useState({ bond: freeBalance }); - - // bond valid. - const [bondValid, setBondValid] = useState<boolean>(false); - - // bond minus tx fees. - const enoughToCoverTxFees: boolean = - freeBalance - Number(bond.bond) > planckBnToUnit(largestTxFee, units); - - // bond value after max tx fees have been deducated. - let bondAfterTxFees: BN; - if (enoughToCoverTxFees) { - bondAfterTxFees = unitToPlanckBn(Number(bond.bond), units); - } else { - bondAfterTxFees = max( - unitToPlanckBn(Number(bond.bond), units).sub(largestTxFee), - new BN(0) - ); - } - - // update bond value on task change. - useEffect(() => { - const _bond = freeBalance; - setBond({ bond: _bond }); - }, [freeBalance]); - - // modal resize on form update - useEffect(() => { - setResize(); - }, [bond]); - - // determine whether this is a pool or staking transaction. - const determineTx = (bondToSubmit: string) => { - let tx = null; - if (!api) { - return tx; - } - if (isPooling) { - tx = api.tx.nominationPools.bondExtra({ - FreeBalance: bondToSubmit, - }); - } else if (isStaking) { - tx = api.tx.staking.bondExtra(bondToSubmit); - } - return tx; - }; - - // the actual bond tx to submit - const getTx = (bondToSubmit: string) => { - if (!bondValid || !activeAccount) { - return null; - } - return determineTx(bondToSubmit); - }; - - const { submitTx, submitting } = useSubmitExtrinsic({ - tx: getTx(bondAfterTxFees.toString()), - from: activeAccount, - shouldSubmit: bondValid, - callbackSubmit: () => { - setModalStatus(2); - }, - callbackInBlock: () => {}, - }); - - const warnings = []; - if (!accountHasSigner(activeAccount)) { - warnings.push('Your account is read only, and cannot sign transactions.'); - } - - return ( - <> - <div className="items"> - {unclaimedRewards > 0 && bondType === 'pool' && ( - <Warning - text={`Bonding will also withdraw your outstanding rewards of ${unclaimedRewards} ${network.unit}.`} - /> - )} - <BondFeedback - syncing={largestTxFee.eq(new BN(0))} - bondType={bondType} - listenIsValid={setBondValid} - defaultBond={null} - setLocalResize={setLocalResize} - setters={[ - { - set: setBond, - current: bond, - }, - ]} - warnings={warnings} - txFees={largestTxFee} - /> - <NotesWrapper> - <EstimatedTxFee /> - </NotesWrapper> - </div> - <FormFooter - setSection={setSection} - submitTx={submitTx} - submitting={submitting} - isValid={bondValid && accountHasSigner(activeAccount) && txFeesValid} - /> - </> - ); -}; diff --git a/src/modals/UpdateBond/Forms/FormFooter.tsx b/src/modals/UpdateBond/Forms/FormFooter.tsx deleted file mode 100644 index 31d4d1ac23..0000000000 --- a/src/modals/UpdateBond/Forms/FormFooter.tsx +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faArrowAltCircleUp } from '@fortawesome/free-regular-svg-icons'; -import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'; -import { ButtonInvert, ButtonSubmit } from '@rossbulat/polkadot-dashboard-ui'; -import { FooterWrapper } from '../../Wrappers'; - -export const FormFooter = ({ - setSection, - submitTx, - submitting, - isValid, -}: any) => { - const hasSections = setSection !== undefined; - - const handleSubmit = () => { - if (hasSections) { - setSection(0); - } - }; - - return ( - <FooterWrapper> - <div> - {hasSections && ( - <ButtonInvert - text="Back" - iconLeft={faChevronLeft} - onClick={() => handleSubmit()} - /> - )} - </div> - <div> - <ButtonSubmit - text={`Submit${submitting ? `ting` : ''}`} - iconLeft={faArrowAltCircleUp} - iconTransform="grow-2" - onClick={() => submitTx()} - disabled={submitting || !isValid} - /> - </div> - </FooterWrapper> - ); -}; diff --git a/src/modals/UpdateBond/Forms/UnbondAll.tsx b/src/modals/UpdateBond/Forms/UnbondAll.tsx deleted file mode 100644 index d07fcd5dce..0000000000 --- a/src/modals/UpdateBond/Forms/UnbondAll.tsx +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { BN } from 'bn.js'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { useStaking } from 'contexts/Staking'; -import { useTransferOptions } from 'contexts/TransferOptions'; -import { useTxFees } from 'contexts/TxFees'; -import { EstimatedTxFee } from 'library/EstimatedTxFee'; -import { Warning } from 'library/Form/Warning'; -import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; -import { useEffect, useState } from 'react'; -import { planckBnToUnit, unitToPlanckBn } from 'Utils'; -import { NotesWrapper, Separator } from '../../Wrappers'; -import { FormsProps } from '../types'; -import { FormFooter } from './FormFooter'; - -export const UnbondAll = (props: FormsProps) => { - const { setSection } = props; - - const { api, network, consts } = useApi(); - const { units } = network; - const { setStatus: setModalStatus, setResize, config } = useModal(); - const { activeAccount, accountHasSigner } = useConnect(); - const { getControllerNotImported } = useStaking(); - const { getBondedAccount, getAccountNominations } = useBalances(); - const { bondType } = config; - const { getTransferOptions } = useTransferOptions(); - const { txFeesValid } = useTxFees(); - const { selectedActivePool } = useActivePools(); - - let { unclaimedRewards } = selectedActivePool || {}; - unclaimedRewards = unclaimedRewards ?? new BN(0); - unclaimedRewards = planckBnToUnit(unclaimedRewards, network.units); - - const controller = getBondedAccount(activeAccount); - const nominations = getAccountNominations(activeAccount); - const controllerNotImported = getControllerNotImported(controller); - - const { bondDuration } = consts; - const isStaking = bondType === 'stake'; - const isPooling = bondType === 'pool'; - - const allTransferOptions = getTransferOptions(activeAccount); - const { freeToUnbond: freeToUnbondBn } = isPooling - ? allTransferOptions.pool - : allTransferOptions.nominate; - - // convert BN values to number - const freeToUnbond = planckBnToUnit(freeToUnbondBn, units); - - // local bond value - const [bond, setBond] = useState({ - bond: freeToUnbond, - }); - - // bond valid - const [bondValid, setBondValid] = useState(false); - - // unbond all validation - const isValid = (() => { - let _isValid = false; - if (isPooling) { - _isValid = freeToUnbond > 0; - } else { - _isValid = - freeToUnbond > 0 && nominations.length === 0 && !controllerNotImported; - } - return _isValid; - })(); - - // update bond value on task change - useEffect(() => { - const _bond = freeToUnbond; - setBond({ bond: _bond }); - setBondValid(isValid); - }, [freeToUnbond, isValid]); - - // modal resize on form update - useEffect(() => { - setResize(); - }, [bond]); - - // tx to submit - const getTx = () => { - let tx = null; - if (!bondValid || !api || !activeAccount) { - return tx; - } - - // stake unbond: controller must be imported - if (isStaking && controllerNotImported) { - return tx; - } - // remove decimal errors - const bondToSubmit = unitToPlanckBn(bond.bond, units); - - // determine tx - if (isPooling) { - tx = api.tx.nominationPools.unbond(activeAccount, bondToSubmit); - } else if (isStaking) { - tx = api.tx.staking.unbond(bondToSubmit); - } - return tx; - }; - - const signingAccount = isPooling ? activeAccount : controller; - - const { submitTx, submitting } = useSubmitExtrinsic({ - tx: getTx(), - from: signingAccount, - shouldSubmit: bondValid, - callbackSubmit: () => { - setModalStatus(2); - }, - callbackInBlock: () => {}, - }); - - return ( - <> - <div className="items"> - <> - {!accountHasSigner(signingAccount) && ( - <Warning text="Your account is read only, and cannot sign transactions." /> - )} - {isStaking && controllerNotImported ? ( - <Warning text="You must have your controller account imported to unbond." /> - ) : ( - <></> - )} - {isStaking && nominations.length ? ( - <Warning text="Stop nominating before unbonding all funds." /> - ) : ( - <></> - )} - {unclaimedRewards > 0 && ( - <Warning - text={`Unbonding will also withdraw your outstanding rewards of ${unclaimedRewards} ${network.unit}.`} - /> - )} - <h4>Amount to unbond:</h4> - <h2> - {freeToUnbond} {network.unit} - </h2> - <Separator /> - <NotesWrapper> - <p> - Once unbonding, you must wait {bondDuration} eras for your funds - to become available. - </p> - {bondValid && <EstimatedTxFee />} - </NotesWrapper> - </> - </div> - <FormFooter - setSection={setSection} - submitTx={submitTx} - submitting={submitting} - isValid={bondValid && accountHasSigner(signingAccount) && txFeesValid} - /> - </> - ); -}; diff --git a/src/modals/UpdateBond/Forms/UnbondPoolToMinimum.tsx b/src/modals/UpdateBond/Forms/UnbondPoolToMinimum.tsx deleted file mode 100644 index 3fc37d70af..0000000000 --- a/src/modals/UpdateBond/Forms/UnbondPoolToMinimum.tsx +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { BN } from 'bn.js'; -import { useApi } from 'contexts/Api'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; -import { useTransferOptions } from 'contexts/TransferOptions'; -import { useTxFees } from 'contexts/TxFees'; -import { EstimatedTxFee } from 'library/EstimatedTxFee'; -import { Warning } from 'library/Form/Warning'; -import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; -import { useEffect, useState } from 'react'; -import { planckBnToUnit, unitToPlanckBn } from 'Utils'; -import { NotesWrapper, Separator } from '../../Wrappers'; -import { FormsProps } from '../types'; -import { FormFooter } from './FormFooter'; - -export const UnbondPoolToMinimum = (props: FormsProps) => { - const { setSection } = props; - - const { api, network, consts } = useApi(); - const { units } = network; - const { setStatus: setModalStatus, setResize } = useModal(); - const { activeAccount, accountHasSigner } = useConnect(); - const { isDepositor, selectedActivePool } = useActivePools(); - const { getTransferOptions } = useTransferOptions(); - const { stats } = usePoolsConfig(); - const { txFeesValid } = useTxFees(); - - let { unclaimedRewards } = selectedActivePool || {}; - unclaimedRewards = unclaimedRewards ?? new BN(0); - unclaimedRewards = planckBnToUnit(unclaimedRewards, network.units); - - const { minJoinBond, minCreateBond } = stats; - const { bondDuration } = consts; - - const { freeToUnbond: freeToUnbondBn } = - getTransferOptions(activeAccount).pool; - - // unbond amount to minimum threshold - const freeToUnbond = isDepositor() - ? planckBnToUnit( - BN.max(freeToUnbondBn.sub(minCreateBond), new BN(0)), - units - ) - : planckBnToUnit(BN.max(freeToUnbondBn.sub(minJoinBond), new BN(0)), units); - - // local bond value - const [bond, setBond] = useState({ - bond: freeToUnbond, - }); - - // bond valid - const [bondValid, setBondValid] = useState(false); - - // unbond all validation - const isValid = (() => { - return freeToUnbond > 0; - })(); - - // update bond value on task change - useEffect(() => { - const _bond = freeToUnbond; - setBond({ bond: _bond }); - setBondValid(isValid); - }, [freeToUnbond, isValid]); - - // modal resize on form update - useEffect(() => { - setResize(); - }, [bond]); - - // tx to submit - const getTx = () => { - let tx = null; - if (!bondValid || !api || !activeAccount) { - return tx; - } - - // remove decimal errors - const bondToSubmit = unitToPlanckBn(bond.bond, units); - - tx = api.tx.nominationPools.unbond(activeAccount, bondToSubmit); - return tx; - }; - - const { submitTx, submitting } = useSubmitExtrinsic({ - tx: getTx(), - from: activeAccount, - shouldSubmit: bondValid, - callbackSubmit: () => { - setModalStatus(2); - }, - callbackInBlock: () => {}, - }); - - return ( - <> - <div className="items"> - <> - {!accountHasSigner(activeAccount) && ( - <Warning text="Your account is read only, and cannot sign transactions." /> - )} - {unclaimedRewards > 0 && ( - <Warning - text={`Unbonding will also withdraw your outstanding rewards of ${unclaimedRewards} ${network.unit}.`} - /> - )} - <h4>Amount to unbond:</h4> - <h2> - {freeToUnbond} {network.unit} - </h2> - <Separator /> - <NotesWrapper> - <p> - Once unbonding, you must wait {bondDuration} eras for your funds - to become available. - </p> - {bondValid && <EstimatedTxFee />} - </NotesWrapper> - </> - </div> - <FormFooter - setSection={setSection} - submitTx={submitTx} - submitting={submitting} - isValid={bondValid && accountHasSigner(activeAccount) && txFeesValid} - /> - </> - ); -}; diff --git a/src/modals/UpdateBond/Forms/UnbondSome.tsx b/src/modals/UpdateBond/Forms/UnbondSome.tsx deleted file mode 100644 index b7547e1234..0000000000 --- a/src/modals/UpdateBond/Forms/UnbondSome.tsx +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { BN } from 'bn.js'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; -import { useStaking } from 'contexts/Staking'; -import { useTransferOptions } from 'contexts/TransferOptions'; -import { useTxFees } from 'contexts/TxFees'; -import { EstimatedTxFee } from 'library/EstimatedTxFee'; -import { UnbondFeedback } from 'library/Form/Unbond/UnbondFeedback'; -import { Warning } from 'library/Form/Warning'; -import { useSubmitExtrinsic } from 'library/Hooks/useSubmitExtrinsic'; -import { useEffect, useState } from 'react'; -import { planckBnToUnit, unitToPlanckBn } from 'Utils'; -import { NotesWrapper } from '../../Wrappers'; -import { FormsProps } from '../types'; -import { FormFooter } from './FormFooter'; - -export const UnbondSome = (props: FormsProps) => { - const { setSection } = props; - - const { api, network, consts } = useApi(); - const { units } = network; - const { setStatus: setModalStatus, setResize, config } = useModal(); - const { activeAccount, accountHasSigner } = useConnect(); - const { staking, getControllerNotImported } = useStaking(); - const { getBondedAccount } = useBalances(); - const { bondType } = config; - const { stats } = usePoolsConfig(); - const { isDepositor, selectedActivePool } = useActivePools(); - const { txFees, txFeesValid } = useTxFees(); - const { getTransferOptions } = useTransferOptions(); - - const controller = getBondedAccount(activeAccount); - const controllerNotImported = getControllerNotImported(controller); - const { minNominatorBond: minNominatorBondBn } = staking; - const { minJoinBond: minJoinBondBn, minCreateBond: minCreateBondBn } = stats; - const { bondDuration } = consts; - - let { unclaimedRewards } = selectedActivePool || {}; - unclaimedRewards = unclaimedRewards ?? new BN(0); - unclaimedRewards = planckBnToUnit(unclaimedRewards, network.units); - - const isStaking = bondType === 'stake'; - const isPooling = bondType === 'pool'; - - const allTransferOptions = getTransferOptions(activeAccount); - const { freeToUnbond: freeToUnbondBn } = isPooling - ? allTransferOptions.pool - : allTransferOptions.nominate; - - // convert BN values to number - const freeToUnbond = planckBnToUnit(freeToUnbondBn, units); - const minJoinBond = planckBnToUnit(minJoinBondBn, units); - const minCreateBond = planckBnToUnit(minCreateBondBn, units); - const minNominatorBond = planckBnToUnit(minNominatorBondBn, units); - - // local bond value - const [bond, setBond] = useState({ bond: freeToUnbond }); - - // bond valid - const [bondValid, setBondValid] = useState<boolean>(false); - - // get the max amount available to unbond - const freeToUnbondToMin = isPooling - ? isDepositor() - ? Math.max(freeToUnbond - minCreateBond, 0) - : Math.max(freeToUnbond - minJoinBond, 0) - : Math.max(freeToUnbond - minNominatorBond, 0); - - // unbond some validation - const isValid = isPooling ? true : !controllerNotImported; - - // update bond value on task change - useEffect(() => { - const _bond = freeToUnbondToMin; - setBond({ bond: _bond }); - - setBondValid(isValid); - }, [freeToUnbondToMin, isValid]); - - // modal resize on form update - useEffect(() => { - setResize(); - }, [bond]); - - // tx to submit - const getTx = () => { - let tx = null; - if (!bondValid || !api || !activeAccount) { - return tx; - } - // stake unbond: controller must be imported - if (isStaking && controllerNotImported) { - return tx; - } - // remove decimal errors - const bondToSubmit = unitToPlanckBn(bond.bond, units); - - // determine tx - if (isPooling) { - tx = api.tx.nominationPools.unbond(activeAccount, bondToSubmit); - } else if (isStaking) { - tx = api.tx.staking.unbond(bondToSubmit); - } - return tx; - }; - - const signingAccount = isPooling ? activeAccount : controller; - - const { submitTx, submitting } = useSubmitExtrinsic({ - tx: getTx(), - from: signingAccount, - shouldSubmit: bondValid, - callbackSubmit: () => { - setModalStatus(2); - }, - callbackInBlock: () => {}, - }); - - const warnings = []; - if (!accountHasSigner(activeAccount)) { - warnings.push('Your account is read only, and cannot sign transactions.'); - } - - return ( - <> - <div className="items"> - <> - {unclaimedRewards > 0 && bondType === 'pool' && ( - <Warning - text={`Unbonding will also withdraw your outstanding rewards of ${unclaimedRewards} ${network.unit}.`} - /> - )} - <UnbondFeedback - bondType={bondType} - listenIsValid={setBondValid} - defaultBond={freeToUnbondToMin} - setters={[ - { - set: setBond, - current: bond, - }, - ]} - warnings={warnings} - txFees={txFees} - /> - <NotesWrapper> - <p> - Once unbonding, you must wait {bondDuration} eras for your funds - to become available. - </p> - <EstimatedTxFee /> - </NotesWrapper> - </> - </div> - <FormFooter - setSection={setSection} - submitTx={submitTx} - submitting={submitting} - isValid={bondValid && accountHasSigner(signingAccount) && txFeesValid} - /> - </> - ); -}; diff --git a/src/modals/UpdateBond/Forms/index.tsx b/src/modals/UpdateBond/Forms/index.tsx deleted file mode 100644 index fecf4bc2a0..0000000000 --- a/src/modals/UpdateBond/Forms/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { forwardRef } from 'react'; -import { ContentWrapper } from '../Wrappers'; -import { BondAll } from './BondAll'; -import { BondSome } from './BondSome'; -import { UnbondAll } from './UnbondAll'; -import { UnbondPoolToMinimum } from './UnbondPoolToMinimum'; -import { UnbondSome } from './UnbondSome'; - -export const Forms = forwardRef((props: any, ref: any) => { - const { task } = props; - return ( - <ContentWrapper ref={ref}> - {task === 'bond_some' && <BondSome {...props} />} - {task === 'bond_all' && <BondAll {...props} />} - {task === 'unbond_some' && <UnbondSome {...props} />} - {task === 'unbond_all' && <UnbondAll {...props} />} - {task === 'unbond_pool_to_minimum' && <UnbondPoolToMinimum {...props} />} - </ContentWrapper> - ); -}); diff --git a/src/modals/UpdateBond/Tasks.tsx b/src/modals/UpdateBond/Tasks.tsx deleted file mode 100644 index 1e913a9b8c..0000000000 --- a/src/modals/UpdateBond/Tasks.tsx +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useApi } from 'contexts/Api'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { usePoolsConfig } from 'contexts/Pools/PoolsConfig'; -import { forwardRef } from 'react'; -import { planckBnToUnit } from 'Utils'; -import { ContentWrapper } from './Wrappers'; - -export const Tasks = forwardRef((props: any, ref: any) => { - const { setSection, setTask, bondType } = props; - - const { network } = useApi(); - const { units, unit } = network; - const { config } = useModal(); - const { fn } = config; - const { isDepositor } = useActivePools(); - const { stats } = usePoolsConfig(); - const { minCreateBond, minJoinBond } = stats; - - const minJoinBondBase = planckBnToUnit(minJoinBond, units); - const minCreateBondBase = planckBnToUnit(minCreateBond, units); - - return ( - <ContentWrapper> - <div className="items" ref={ref}> - {fn === 'add' && ( - <> - <button - type="button" - className="action-button" - onClick={() => { - setSection(1); - setTask('bond_some'); - }} - > - <div> - <h3>Bond Extra</h3> - <p>Bond more {network.unit}.</p> - </div> - <div> - <FontAwesomeIcon transform="shrink-2" icon={faChevronRight} /> - </div> - </button> - <button - type="button" - className="action-button" - onClick={() => { - setSection(1); - setTask('bond_all'); - }} - > - <div> - <h3>Bond All</h3> - <p>Bond all available {network.unit}.</p> - </div> - <div> - <FontAwesomeIcon transform="shrink-2" icon={faChevronRight} /> - </div> - </button> - </> - )} - {fn === 'remove' && ( - <> - <button - type="button" - className="action-button" - onClick={() => { - setSection(1); - setTask('unbond_some'); - }} - > - <div> - <h3>Unbond</h3> - <p>Unbond some of your {network.unit}.</p> - </div> - <div> - <FontAwesomeIcon transform="shrink-2" icon={faChevronRight} /> - </div> - </button> - {bondType === 'stake' && ( - <button - type="button" - className="action-button" - onClick={() => { - setSection(1); - setTask('unbond_all'); - }} - > - <div> - <h3>Unbond All</h3> - <p>Exit your staking position.</p> - </div> - <div> - <FontAwesomeIcon transform="shrink-2" icon={faChevronRight} /> - </div> - </button> - )} - {bondType === 'pool' && ( - <button - type="button" - className="action-button" - onClick={() => { - setSection(1); - setTask('unbond_pool_to_minimum'); - }} - > - <div> - <h3>Unbond To Minimum</h3> - <p> - {isDepositor() - ? `Unbond up to the ${minCreateBondBase} ${unit} minimum bond for pool depositors.` - : `Unbond up to the ${minJoinBondBase} ${unit} minimum to maintain your pool membership`} - </p> - </div> - <div> - <FontAwesomeIcon transform="shrink-2" icon={faChevronRight} /> - </div> - </button> - )} - </> - )} - </div> - </ContentWrapper> - ); -}); - -export default Tasks; diff --git a/src/modals/UpdateBond/Wrappers.ts b/src/modals/UpdateBond/Wrappers.ts deleted file mode 100644 index 907bc1412f..0000000000 --- a/src/modals/UpdateBond/Wrappers.ts +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { motion } from 'framer-motion'; -import styled from 'styled-components'; -import { backgroundToggle, buttonPrimaryBackground, textPrimary } from 'theme'; - -export const Wrapper = styled.div` - display: flex; - flex-flow: column wrap; - align-items: flex-start; - justify-content: flex-start; - padding: 0; -`; - -export const FixedContentWrapper = styled.div` - padding-top: 1rem; - width: 100%; -`; - -export const CardsWrapper = styled(motion.div)` - width: 200%; - display: flex; - flex-flow: row nowrap; - overflow: hidden; - position: relative; -`; - -export const ContentWrapper = styled.div` - border-radius: 1rem; - display: flex; - flex-flow: column nowrap; - flex-basis: 50%; - min-width: 50%; - height: auto; - flex: 1; - max-height: 100%; - padding: 0 1.25rem 1rem 1.25rem; - - .items { - position: relative; - padding: 0.5rem 0 1.5rem 0; - border-bottom: none; - width: auto; - border-radius: 0.75rem; - overflow: hidden; - overflow-y: auto; - z-index: 1; - width: 100%; - - h4 { - margin: 0.2rem 0; - } - h2 { - margin: 0.75rem 0; - } - - .action-button { - background: ${buttonPrimaryBackground}; - padding: 1rem; - cursor: pointer; - margin-bottom: 1rem; - border-radius: 0.75rem; - display: flex; - flex-flow: row wrap; - justify-content: flex-start; - align-items: center; - transition: all 0.15s; - width: 100%; - - &:last-child { - margin-bottom: 0; - } - - h3, - p { - text-align: left; - margin: 0; - } - h3 { - margin-bottom: 0.5rem; - } - > *:last-child { - flex: 1; - display: flex; - flex-flow: row wrap; - justify-content: flex-end; - } - &:hover { - background: ${backgroundToggle}; - } - .icon { - margin-right: 0.5rem; - } - p { - color: ${textPrimary}; - font-size: 1rem; - } - } - } -`; diff --git a/src/modals/UpdateBond/index.tsx b/src/modals/UpdateBond/index.tsx deleted file mode 100644 index 10d78d25bc..0000000000 --- a/src/modals/UpdateBond/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; -import { useModal } from 'contexts/Modal'; -import { Title } from 'library/Modal/Title'; -import { useEffect, useRef, useState } from 'react'; -import { Forms } from './Forms'; -import { Tasks } from './Tasks'; -import { CardsWrapper, FixedContentWrapper, Wrapper } from './Wrappers'; - -export const UpdateBond = () => { - const { config, setModalHeight } = useModal(); - const { fn, bondType } = config; - - // modal task - const [task, setTask] = useState(null); - - // active modal section - const [section, setSection] = useState(0); - - // increment to resize modal - const [localResize, _setLocalResize] = useState(0); - const setLocalResize = () => { - _setLocalResize(localResize + 1); - }; - - // refs for wrappers - const headerRef = useRef<HTMLDivElement>(null); - const tasksRef = useRef<HTMLDivElement>(null); - const formsRef = useRef<HTMLDivElement>(null); - - // resize modal on state change - useEffect(() => { - let _height = headerRef.current?.clientHeight ?? 0; - if (section === 0) { - _height += tasksRef.current?.clientHeight ?? 0; - } else { - _height += formsRef.current?.clientHeight ?? 0; - } - setModalHeight(_height); - }, [section, task, localResize]); - - return ( - <Wrapper> - <FixedContentWrapper ref={headerRef}> - <Title - title={`${fn === 'add' ? 'Add To' : 'Remove'} Bond`} - icon={fn === 'add' ? faPlus : faMinus} - fixed - /> - </FixedContentWrapper> - <CardsWrapper - animate={section === 0 ? 'home' : 'next'} - transition={{ - duration: 0.5, - type: 'spring', - bounce: 0.1, - }} - variants={{ - home: { - left: 0, - }, - next: { - left: '-100%', - }, - }} - > - <Tasks - bondType={bondType} - setSection={setSection} - setTask={setTask} - ref={tasksRef} - /> - <Forms - section={section} - setSection={setSection} - task={task} - ref={formsRef} - bondType={bondType} - setLocalResize={setLocalResize} - /> - </CardsWrapper> - </Wrapper> - ); -}; - -export default UpdateBond; diff --git a/src/modals/UpdateBond/types.ts b/src/modals/UpdateBond/types.ts deleted file mode 100644 index b9e7579281..0000000000 --- a/src/modals/UpdateBond/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -export interface FormsProps { - section?: number; - setSection?: string; - setLocalResize?: () => void; -} diff --git a/src/modals/ValidatorMetrics/index.tsx b/src/modals/ValidatorMetrics/index.tsx index 4d64cedbcb..2b2ff9d078 100644 --- a/src/modals/ValidatorMetrics/index.tsx +++ b/src/modals/ValidatorMetrics/index.tsx @@ -10,7 +10,6 @@ import { useTranslation } from 'react-i18next'; import { useHelp } from 'contexts/Help'; import { useNetworkMetrics } from 'contexts/NetworkMetrics'; import { useStaking } from 'contexts/Staking'; -import { useSubscan } from 'contexts/Plugins/Subscan'; import { CardHeaderWrapper, CardWrapper } from 'library/Card/Wrappers'; import { EraPoints as EraPointsGraph } from 'library/Graphs/EraPoints'; import { formatSize } from 'library/Graphs/Utils'; @@ -22,6 +21,7 @@ import { StatusLabel } from 'library/StatusLabel'; import { useOverlay } from '@polkadot-cloud/react/hooks'; import { PluginLabel } from 'library/PluginLabel'; import { useNetwork } from 'contexts/Network'; +import type { AnyJson } from 'types'; export const ValidatorMetrics = () => { const { t } = useTranslation('modals'); @@ -52,14 +52,14 @@ export const ValidatorMetrics = () => { validatorOwnStake = new BigNumber(own); } } - const [list, setList] = useState([]); + const [list, setList] = useState<AnyJson[]>([]); const ref = useRef<HTMLDivElement>(null); const size = useSize(ref.current); const { width, height, minHeight } = formatSize(size, 300); const handleEraPoints = async () => { - setList(await fetchEraPoints(address, activeEra.index)); + setList(await fetchEraPoints(address, activeEra.index.toNumber())); }; useEffect(() => { diff --git a/src/modals/Wrappers.ts b/src/modals/Wrappers.ts deleted file mode 100644 index 600e43b0ef..0000000000 --- a/src/modals/Wrappers.ts +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { motion } from 'framer-motion'; -import styled from 'styled-components'; -import { - borderPrimary, - cardBorder, - cardShadow, - modalBackground, - modalOverlayBackground, - networkColor, - shadowColor, - textSecondary, -} from 'theme'; - -// Blurred background modal wrapper -export const ModalWrapper = styled(motion.div)` - background: ${modalOverlayBackground}; - position: fixed; - width: 100%; - height: 100%; - z-index: 9; - backdrop-filter: blur(4px); - - /* modal content wrapper */ - > div { - height: 100%; - display: flex; - flex-flow: row wrap; - justify-content: center; - align-items: center; - padding: 1rem 2rem; - - /* click anywhere behind modal content to close */ - .close { - position: fixed; - width: 100%; - height: 100%; - z-index: 8; - cursor: default; - } - } -`; - -export const HeightWrapper = styled.div<{ size: string }>` - border: ${cardBorder} ${borderPrimary}; - box-shadow: ${cardShadow} ${shadowColor}; - transition: height 0.5s cubic-bezier(0.1, 1, 0.2, 1); - width: 100%; - max-width: ${(props) => - props.size === 'xl' - ? '1250px' - : props.size === 'large' - ? '800px' - : '600px'}; - max-height: 100%; - border-radius: 1.5rem; - z-index: 9; - position: relative; -`; - -// Modal content wrapper -export const ContentWrapper = styled.div` - background: ${modalBackground}; - width: 100%; - height: auto; - overflow: hidden; - position: relative; - - a { - color: ${networkColor}; - } - .header { - width: 100%; - display: flex; - flex-flow: row wrap; - align-items: center; - padding: 1rem 1rem 0 1rem; - } - .body { - padding: 1rem; - } - .notes { - padding: 1rem 0; - > p { - color: ${textSecondary}; - } - } -`; - -// generic wrapper for modal padding -export const PaddingWrapper = styled.div<{ - verticalOnly?: boolean; - horizontalOnly?: boolean; -}>` - display: flex; - flex-flow: column wrap; - align-items: flex-start; - justify-content: flex-start; - width: 100%; - padding: ${(props) => - props.verticalOnly - ? '1rem 0' - : props.horizontalOnly - ? '0 1rem' - : '1rem 1.25rem'}; -`; - -// modal header, used for extrinsics forms -export const HeadingWrapper = styled.h3<{ noPadding?: boolean }>` - display: flex; - flex-flow: row wrap; - justify-content: flex-start; - align-items: center; - width: 100%; - margin-top: 0.25rem; - padding: ${(props) => (props.noPadding ? '0' : '0 1rem')}; - color: ${textSecondary}; - flex: 1; - - > svg { - margin-right: 0.75rem; - } -`; - -// modal footer, used for extrinsics forms -export const FooterWrapper = styled.div` - display: flex; - flex-flow: row wrap; - justify-content: flex-end; - align-items: center; - width: 100%; - - h3 { - color: ${textSecondary}; - opacity: 0.5; - margin: 0; - &.active { - opacity: 1; - color: ${networkColor}; - } - } - - > div { - margin-left: 1rem; - } - .submit { - padding: 0.5rem 0.75rem; - border-radius: 0.7rem; - font-size: 1rem; - display: flex; - flex-flow: row nowrap; - justify-content: flex-start; - align-items: center; - &.primary { - color: white; - background: ${networkColor}; - border: 1px solid ${networkColor}; - } - &.secondary { - color: ${networkColor}; - border: 1px solid ${networkColor}; - } - - &:disabled { - opacity: 0.25; - } - svg { - margin-right: 0.5rem; - } - } -`; - -export const Separator = styled.div` - border-top: 1px solid ${textSecondary}; - width: 100%; - opacity: 0.1; - margin: 0.75rem 0rem; -`; - -export const NotesWrapper = styled.div` - width: 100%; - padding: 1rem 0; - > p { - color: ${textSecondary}; - } -`; diff --git a/src/modals/index.tsx b/src/modals/index.tsx deleted file mode 100644 index e49e890ea0..0000000000 --- a/src/modals/index.tsx +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useModal } from 'contexts/Modal'; -import { useAnimation } from 'framer-motion'; -import { ErrorFallbackModal } from 'library/ErrorBoundary'; -import { useEffect, useRef } from 'react'; -import { ErrorBoundary } from 'react-error-boundary'; -import { AccountPoolRoles } from './AccountPoolRoles'; -import { Bio } from './Bio'; -import { ChangeNominations } from './ChangeNominations'; -import { ChangePoolRoles } from './ChangePoolRoles'; -import { ClaimReward } from './ClaimReward'; -import { ConnectAccounts } from './ConnectAccounts'; -import { DismissTips } from './DismissTips'; -import { GoToFeedback } from './GoToFeedback'; -import { JoinPool } from './JoinPool'; -import { LeavePool } from './LeavePool'; -import { ManagePool } from './ManagePool'; -import { Networks } from './Networks'; -import { Nominate } from './Nominate'; -import { NominateFromFavorites } from './NominateFromFavorites'; -import { NominatePool } from './NominatePool'; -import { PoolNominations } from './PoolNominations'; -import { SelectFavorites } from './SelectFavorites'; -import { Settings } from './Settings'; -import { UnbondPoolMember } from './UnbondPoolMember'; -import { UnlockChunks } from './UnlockChunks'; -import { UpdateBond } from './UpdateBond'; -import { UpdateController } from './UpdateController'; -import { UpdatePayee } from './UpdatePayee'; -import { ValidatorMetrics } from './ValidatorMetrics'; -import { WithdrawPoolMember } from './WithdrawPoolMember'; -import { ContentWrapper, HeightWrapper, ModalWrapper } from './Wrappers'; - -export const Modal = () => { - const { setModalHeight, setStatus, status, modal, size, height, resize } = - useModal(); - const controls = useAnimation(); - - const maxHeight = window.innerHeight * 0.8; - - const onFadeIn = async () => { - await controls.start('visible'); - }; - - const onFadeOut = async () => { - await controls.start('hidden'); - setStatus(0); - }; - - const variants = { - hidden: { - opacity: 0, - }, - visible: { - opacity: 1, - }, - }; - - useEffect(() => { - // modal has been opened - fade in - if (status === 1) { - onFadeIn(); - } - // an external component triggered modal closure - fade out - if (status === 2) { - onFadeOut(); - } - }, [status]); - - const modalRef = useRef<HTMLDivElement>(null); - - // resize modal on status or resize change - useEffect(() => { - handleResize(); - }, [resize]); - - const handleResize = () => { - let _height = modalRef.current?.clientHeight ?? 0; - _height = _height > maxHeight ? maxHeight : _height; - setModalHeight(_height); - }; - - if (status === 0) { - return <></>; - } - - return ( - <ModalWrapper - initial={{ - opacity: 0, - }} - animate={controls} - transition={{ - duration: 0.15, - }} - variants={variants} - > - <div> - <HeightWrapper - size={size} - style={{ - height, - overflow: height >= maxHeight ? 'scroll' : 'hidden', - }} - > - <ContentWrapper ref={modalRef}> - <ErrorBoundary FallbackComponent={ErrorFallbackModal}> - {modal === 'AccountPoolRoles' && <AccountPoolRoles />} - {modal === 'Bio' && <Bio />} - {modal === 'ChangeNominations' && <ChangeNominations />} - {modal === 'ChangePoolRoles' && <ChangePoolRoles />} - {modal === 'ClaimReward' && <ClaimReward />} - {modal === 'ConnectAccounts' && <ConnectAccounts />} - {modal === 'DismissTips' && <DismissTips />} - {modal === 'JoinPool' && <JoinPool />} - {modal === 'Settings' && <Settings />} - {modal === 'UpdateController' && <UpdateController />} - {modal === 'UpdateBond' && <UpdateBond />} - {modal === 'UpdatePayee' && <UpdatePayee />} - {modal === 'ValidatorMetrics' && <ValidatorMetrics />} - {modal === 'ManagePool' && <ManagePool />} - {modal === 'Nominate' && <Nominate />} - {modal === 'UnlockChunks' && <UnlockChunks />} - {modal === 'NominatePool' && <NominatePool />} - {modal === 'LeavePool' && <LeavePool />} - {modal === 'SelectFavorites' && <SelectFavorites />} - {modal === 'Networks' && <Networks />} - {modal === 'NominateFromFavorites' && <NominateFromFavorites />} - {modal === 'PoolNominations' && <PoolNominations />} - {modal === 'UnbondPoolMember' && <UnbondPoolMember />} - {modal === 'WithdrawPoolMember' && <WithdrawPoolMember />} - {modal === 'GoToFeedback' && <GoToFeedback />} - </ErrorBoundary> - </ContentWrapper> - </HeightWrapper> - <button - type="button" - className="close" - onClick={() => { - onFadeOut(); - }} - > -   - </button> - </div> - </ModalWrapper> - ); -}; - -export default Modal; diff --git a/src/pages/Favorites/index.tsx b/src/pages/Favorites/index.tsx deleted file mode 100644 index a045c7bc83..0000000000 --- a/src/pages/Favorites/index.tsx +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useApi } from 'contexts/Api'; -import { useValidators } from 'contexts/Validators'; -import { CardWrapper } from 'library/Graphs/Wrappers'; -import { PageTitle } from 'library/PageTitle'; -import { ValidatorList } from 'library/ValidatorList'; -import { useTranslation } from 'react-i18next'; -import { PageRowWrapper } from 'Wrappers'; -import { PageProps } from '../types'; - -export const Favorites = (props: PageProps) => { - const { isReady } = useApi(); - const { page } = props; - const { key } = page; - const { favoritesList } = useValidators(); - const { t } = useTranslation(); - - const batchKey = 'favorite_validators'; - - return ( - <> - <PageTitle title={t(key, { ns: 'base' })} /> - <PageRowWrapper className="page-padding" noVerticalSpacer> - <CardWrapper> - {favoritesList === null ? ( - <h3> - {t('favorites.fetching_favorite_validators', { ns: 'pages' })} - </h3> - ) : ( - <> - {isReady && ( - <> - {favoritesList.length > 0 ? ( - <ValidatorList - bondType="stake" - validators={favoritesList} - batchKey={batchKey} - title={t('favorites.favorite_validators', { - ns: 'pages', - })} - selectable={false} - refetchOnListUpdate - allowMoreCols - toggleFavorites - /> - ) : ( - <h3>{t('favorites.no_favorites', { ns: 'pages' })}</h3> - )} - </> - )} - </> - )} - </CardWrapper> - </PageRowWrapper> - </> - ); -}; - -export default Favorites; diff --git a/src/pages/Feedback/Wrappers.ts b/src/pages/Feedback/Wrappers.ts deleted file mode 100644 index c94710cdc0..0000000000 --- a/src/pages/Feedback/Wrappers.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; -import { textSecondary } from 'theme'; - -export const Wrapper = styled.div` - h2 { - color: ${textSecondary}; - margin-top: 2rem; - } -`; diff --git a/src/pages/Feedback/index.tsx b/src/pages/Feedback/index.tsx deleted file mode 100644 index 2ef9cab5fa..0000000000 --- a/src/pages/Feedback/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable */ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { PageTitle } from 'library/PageTitle'; -import { useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PageRowWrapper } from 'Wrappers'; -import { Wrapper } from '../Community/Wrappers'; -import { PageProps } from '../types'; - -const BoardToken = '2dda48aa-e149-da7b-f016-98e22279df1e'; - -const Feedback = (props: PageProps) => { - const { page } = props; - const { key } = page; - const { t } = useTranslation('base'); - - useEffect(() => { - (function (w: any, d: any, i: any, s: any) { - function l() { - if (!d.getElementById(i)) { - var f = d.getElementsByTagName(s)[0], - e = d.createElement(s); - (e.type = 'text/javascript'), - (e.async = !0), - (e.src = 'https://canny.io/sdk.js'), - f.parentNode.insertBefore(e, f); - } - } - if ('function' != typeof w.Canny) { - var c: any = function () { - c.q.push(arguments); - }; - (c.q = []), - (w.Canny = c), - 'complete' === d.readyState - ? l() - : w.attachEvent - ? w.attachEvent('onload', l) - : w.addEventListener('load', l, !1); - } - })(window, document, 'canny-jssdk', 'script'); - - // @ts-ignore - Canny('render', { - boardToken: BoardToken, - basePath: null, // See step 2 - ssoToken: null, // See step 3 - }); - }, []); - - return ( - <Wrapper> - <PageTitle title={t(key)} /> - <PageRowWrapper className="page-padding"> - <div data-canny style={{ width: '100%' }} /> - </PageRowWrapper> - </Wrapper> - ); -}; - -export default Feedback; diff --git a/src/pages/Nominate/Active/Controller/Wrapper.ts b/src/pages/Nominate/Active/Controller/Wrapper.ts deleted file mode 100644 index 61a0742d5d..0000000000 --- a/src/pages/Nominate/Active/Controller/Wrapper.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; - -interface WrapperProps { - paddingLeft: boolean; - paddingRight: boolean; -} - -export const Wrapper = styled.div<WrapperProps>` - display: flex; - flex-flow: row wrap; - - > .hide-with-padding { - padding-left: ${(props) => (props.paddingLeft ? '3rem' : '0')}; - padding-right: ${(props) => (props.paddingRight ? '8.2rem' : '0')}; - padding-top: 0.4rem; - padding-bottom: 0.4rem; - flex-shrink: 1; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - position: relative; - margin-bottom: 0; - - .icon { - position: absolute; - left: 0; - top: 0; - display: flex; - flex-flow: row wrap; - align-items: center; - } - - .btn { - position: absolute; - right: 0; - top: 0; - padding: 0.1rem; - display: flex; - flex-flow: row wrap; - align-items: center; - } - } -`; diff --git a/src/pages/Nominate/Active/Controller/index.tsx b/src/pages/Nominate/Active/Controller/index.tsx deleted file mode 100644 index d10d3d474f..0000000000 --- a/src/pages/Nominate/Active/Controller/index.tsx +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faExchangeAlt } from '@fortawesome/free-solid-svg-icons'; -import { ButtonPrimary } from '@rossbulat/polkadot-dashboard-ui'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useStaking } from 'contexts/Staking'; -import { Identicon } from 'library/Identicon'; -import OpenHelpIcon from 'library/OpenHelpIcon'; -import { Wrapper as StatWrapper } from 'library/Stat/Wrapper'; -import { useTranslation } from 'react-i18next'; -import { clipAddress } from 'Utils'; -import { Wrapper } from './Wrapper'; - -export const Controller = ({ label }: { label: string }) => { - const { isReady } = useApi(); - const { activeAccount, isReadOnlyAccount, getAccount } = useConnect(); - const { openModalWith } = useModal(); - const { hasController } = useStaking(); - const { getBondedAccount } = useBalances(); - const controller = getBondedAccount(activeAccount); - const { t } = useTranslation('pages'); - - let display = t('nominate.none'); - if (hasController() && controller) { - display = clipAddress(controller); - } - - const displayName = getAccount(controller)?.name; - - return ( - <StatWrapper> - <h4> - {label} <OpenHelpIcon helpKey="Stash and Controller Accounts" /> - </h4> - <Wrapper paddingLeft={hasController()} paddingRight> - <h2 className="hide-with-padding"> - <div className="icon"> - <Identicon value={controller || ''} size={26} /> - </div> - {displayName || display}  - <div className="btn"> - <ButtonPrimary - text={t('nominate.change')} - iconLeft={faExchangeAlt} - disabled={ - !isReady || !hasController() || isReadOnlyAccount(activeAccount) - } - onClick={() => openModalWith('UpdateController', {}, 'large')} - style={{ minWidth: '7.5rem' }} - /> - </div> - </h2> - </Wrapper> - </StatWrapper> - ); -}; diff --git a/src/pages/Nominate/Active/ControllerNotImported.tsx b/src/pages/Nominate/Active/ControllerNotImported.tsx deleted file mode 100644 index a349d3dce0..0000000000 --- a/src/pages/Nominate/Active/ControllerNotImported.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ButtonPrimary } from '@rossbulat/polkadot-dashboard-ui'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useStaking } from 'contexts/Staking'; -import { useTheme } from 'contexts/Themes'; -import { useUi } from 'contexts/UI'; -import { CardHeaderWrapper, CardWrapper } from 'library/Graphs/Wrappers'; -import { useTranslation } from 'react-i18next'; -import { defaultThemes } from 'theme/default'; -import { PageRowWrapper } from 'Wrappers'; - -export const ControllerNotImported = () => { - const { openModalWith } = useModal(); - const { isSyncing } = useUi(); - const { mode } = useTheme(); - const { getControllerNotImported } = useStaking(); - const { activeAccount, isReadOnlyAccount } = useConnect(); - const { getBondedAccount } = useBalances(); - const controller = getBondedAccount(activeAccount); - const { t } = useTranslation('pages'); - - return ( - <> - {getControllerNotImported(controller) && - !isSyncing && - !isReadOnlyAccount(activeAccount) && ( - <PageRowWrapper className="page-padding" noVerticalSpacer> - <CardWrapper - style={{ - border: `1px solid ${defaultThemes.status.warning.transparent[mode]}`, - }} - > - <CardHeaderWrapper> - <h4>{t('nominate.controller_not_imported')}</h4> - </CardHeaderWrapper> - <ButtonPrimary - text="Set New Controller" - onClick={() => openModalWith('UpdateController', {}, 'large')} - /> - </CardWrapper> - </PageRowWrapper> - )} - </> - ); -}; diff --git a/src/pages/Nominate/Active/Nominations/Wrapper.tsx b/src/pages/Nominate/Active/Nominations/Wrapper.tsx deleted file mode 100644 index b17ab85fb0..0000000000 --- a/src/pages/Nominate/Active/Nominations/Wrapper.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; - -export const Wrapper = styled.div` - flex: 1; - display: flex; - flex-flow: column wrap; - width: 100%; - - .head { - flex: 1; - display: flex; - flex-flow: row wrap; - padding: 0 0.25rem; - margin-top: 1rem; - - > h3 { - flex: 1; - } - } -`; diff --git a/src/pages/Nominate/Active/Nominations/index.tsx b/src/pages/Nominate/Active/Nominations/index.tsx deleted file mode 100644 index 4d05d46aff..0000000000 --- a/src/pages/Nominate/Active/Nominations/index.tsx +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faStopCircle } from '@fortawesome/free-solid-svg-icons'; -import { ButtonPrimary } from '@rossbulat/polkadot-dashboard-ui'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { PoolState } from 'contexts/Pools/types'; -import { useStaking } from 'contexts/Staking'; -import { useUi } from 'contexts/UI'; -import { useValidators } from 'contexts/Validators'; -import { CardHeaderWrapper } from 'library/Graphs/Wrappers'; -import { OpenHelpIcon } from 'library/OpenHelpIcon'; -import { ValidatorList } from 'library/ValidatorList'; -import { useTranslation } from 'react-i18next'; -import { MaybeAccount } from 'types'; -import { Wrapper } from './Wrapper'; - -export const Nominations = ({ - bondType, - nominator, -}: { - bondType: 'pool' | 'stake'; - nominator: MaybeAccount; -}) => { - const { openModalWith } = useModal(); - const { inSetup } = useStaking(); - const { isSyncing } = useUi(); - const { activeAccount, isReadOnlyAccount } = useConnect(); - const { getAccountNominations } = useBalances(); - const { nominated: stakeNominated, poolNominated } = useValidators(); - const { t } = useTranslation('pages'); - let { favoritesList } = useValidators(); - if (favoritesList === null) { - favoritesList = []; - } - - const { - poolNominations, - isNominator: isPoolNominator, - isOwner: isPoolOwner, - selectedActivePool, - } = useActivePools(); - - const isPool = bondType === 'pool'; - const nominations = isPool - ? poolNominations.targets - : getAccountNominations(nominator); - const nominated = isPool ? poolNominated : stakeNominated; - const batchKey = isPool ? 'pool_nominations' : 'stake_nominations'; - - const nominating = nominated?.length ?? false; - - // callback function to stop nominating selected validators - const cbStopNominatingSelected = (provider: any) => { - const { selected } = provider; - const _nominations = [...nominations].filter((n) => { - return !selected.map((_s: any) => _s.address).includes(n); - }); - openModalWith( - 'ChangeNominations', - { - nominations: _nominations, - provider, - bondType, - }, - 'small' - ); - }; - - // callback function for adding nominations - const cbAddNominations = ({ setSelectActive }: any) => { - setSelectActive(false); - openModalWith( - 'NominateFromFavorites', - { - nominations, - bondType, - }, - 'xl' - ); - }; - - // determine whether buttons are disabled - const poolDestroying = - isPool && - selectedActivePool?.bondedPool?.state === PoolState.Destroy && - !nominating; - - const stopBtnDisabled = - (!isPool && inSetup()) || - isSyncing || - isReadOnlyAccount(activeAccount) || - poolDestroying; - - return ( - <Wrapper> - <CardHeaderWrapper withAction> - <h3> - {isPool ? t('nominate.pool_nominations') : t('nominate.nominations')} - <OpenHelpIcon helpKey="Nominations" /> - </h3> - <div> - {/* If regular staking and nominating, display stop button. - If Pool and account is nominator or root, display stop button. - */} - {((!isPool && nominations.length) || - (isPool && (isPoolNominator() || isPoolOwner()))) && ( - <ButtonPrimary - iconLeft={faStopCircle} - iconTransform="grow-1" - text={t('nominate.stop')} - disabled={stopBtnDisabled} - onClick={() => - openModalWith( - 'ChangeNominations', - { - nominations: [], - bondType, - }, - 'small' - ) - } - /> - )} - </div> - </CardHeaderWrapper> - {nominated === null || isSyncing ? ( - <div className="head"> - <h4> - {!isSyncing && nominated === null - ? t('nominate.not_nominating') - : t('nominate.syncing')} - </h4> - </div> - ) : !nominator ? ( - <div className="head"> - <h4>{t('nominate.not_nominating')}</h4> - </div> - ) : ( - <> - {nominated.length > 0 ? ( - <div style={{ marginTop: '1rem' }}> - <ValidatorList - bondType={isPool ? 'pool' : 'stake'} - validators={nominated} - nominator={nominator} - batchKey={batchKey} - title={t('nominate.your_nominations')} - format="nomination" - selectable={ - !isReadOnlyAccount(activeAccount) && - (!isPool || isPoolNominator() || isPoolOwner()) - } - actions={ - isReadOnlyAccount(activeAccount) - ? [] - : [ - { - title: t('nominate.stop_nominating_selected'), - onClick: cbStopNominatingSelected, - onSelected: true, - }, - { - disabled: !favoritesList.length, - title: t('nominate.add_from_favorites'), - onClick: cbAddNominations, - onSelected: false, - }, - ] - } - refetchOnListUpdate - allowMoreCols - disableThrottle - /> - </div> - ) : ( - <div className="head"> - {poolDestroying ? ( - <h4>{t('nominate.pool_destroy')}</h4> - ) : ( - <h4>{t('nominate.not_nominating')}</h4> - )} - </div> - )} - </> - )} - </Wrapper> - ); -}; - -export default Nominations; diff --git a/src/pages/Nominate/Active/Stats/ActiveNominations.tsx b/src/pages/Nominate/Active/Stats/ActiveNominations.tsx deleted file mode 100644 index 67d5146185..0000000000 --- a/src/pages/Nominate/Active/Stats/ActiveNominations.tsx +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useStaking } from 'contexts/Staking'; -import { Pie } from 'library/StatBoxList/Pie'; -import { useTranslation } from 'react-i18next'; - -export const ActiveNominationsStatBox = () => { - const { getNominationsStatus } = useStaking(); - const nominationStatuses = getNominationsStatus(); - const { t } = useTranslation('pages'); - - const total = Object.values(nominationStatuses).length; - const active = - Object.values(nominationStatuses).filter((_v) => _v === 'active').length ?? - 0; - - const params = { - label: t('nominate.active_nominations'), - stat: { - value: active, - total, - unit: '', - }, - graph: { - value1: active, - value2: active ? 0 : 1, - }, - tooltip: active ? 'Active' : undefined, - helpKey: 'Nominations', - }; - - return <Pie {...params} />; -}; - -export default ActiveNominationsStatBox; diff --git a/src/pages/Nominate/Active/Stats/InactiveNominations.tsx b/src/pages/Nominate/Active/Stats/InactiveNominations.tsx deleted file mode 100644 index d759ef5664..0000000000 --- a/src/pages/Nominate/Active/Stats/InactiveNominations.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useStaking } from 'contexts/Staking'; -import { Pie } from 'library/StatBoxList/Pie'; -import { useTranslation } from 'react-i18next'; - -export const ActiveNominationsStatBox = () => { - const { getNominationsStatus, isNominating } = useStaking(); - const nominationStatuses = getNominationsStatus(); - const { t } = useTranslation('pages'); - - const total = Object.values(nominationStatuses).length; - const inactive = - Object.values(nominationStatuses).filter((_v) => _v === 'inactive') - .length ?? 0; - - // inactive nominations as percent - let inactiveAsPercent = 0; - if (total > 0) { - inactiveAsPercent = inactive / (total * 0.01); - } - - const params = { - label: t('nominate.inactive_nominations'), - stat: { - value: inactive, - total, - unit: '', - }, - graph: { - value1: inactive, - value2: total - inactive, - }, - tooltip: isNominating() ? `${inactiveAsPercent}%` : undefined, - helpKey: 'Inactive Nominations', - }; - - return <Pie {...params} />; -}; - -export default ActiveNominationsStatBox; diff --git a/src/pages/Nominate/Active/Stats/MinimumActiveBond.tsx b/src/pages/Nominate/Active/Stats/MinimumActiveBond.tsx deleted file mode 100644 index cbd7a92b8e..0000000000 --- a/src/pages/Nominate/Active/Stats/MinimumActiveBond.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useApi } from 'contexts/Api'; -import { useStaking } from 'contexts/Staking'; -import { Number } from 'library/StatBoxList/Number'; -import { useTranslation } from 'react-i18next'; - -export const MinimumActiveBondStatBox = () => { - const { network } = useApi(); - const { eraStakers } = useStaking(); - const { minActiveBond } = eraStakers; - const { t } = useTranslation('pages'); - - const params = { - label: t('nominate.minimum_active_bond'), - value: minActiveBond, - unit: network.unit, - helpKey: 'Bonding', - }; - - return <Number {...params} />; -}; - -export default MinimumActiveBondStatBox; diff --git a/src/pages/Nominate/Active/Stats/SupplyStaked.tsx b/src/pages/Nominate/Active/Stats/SupplyStaked.tsx deleted file mode 100644 index 6c33eae48c..0000000000 --- a/src/pages/Nominate/Active/Stats/SupplyStaked.tsx +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import BN from 'bn.js'; -import { useApi } from 'contexts/Api'; -import { useNetworkMetrics } from 'contexts/Network'; -import { useStaking } from 'contexts/Staking'; -import { Pie } from 'library/StatBoxList/Pie'; -import { toFixedIfNecessary } from 'Utils'; - -export const SupplyStakedStatBox = () => { - const { network } = useApi(); - const { units } = network; - const { metrics } = useNetworkMetrics(); - const { totalIssuance } = metrics; - const { staking } = useStaking(); - - const { lastTotalStake } = staking; - - // total supply as percent - let supplyAsPercent = 0; - if (totalIssuance.gt(new BN(0))) { - supplyAsPercent = lastTotalStake - .div(totalIssuance.div(new BN(100))) - .toNumber(); - } - - // base values - const lastTotalStakeBase = lastTotalStake.div(new BN(10 ** units)); - const totalIssuanceBase = totalIssuance.div(new BN(10 ** units)); - - const params = { - label: 'Total Supply Staked', - stat: { - value: lastTotalStakeBase.toNumber(), - unit: network.unit, - }, - graph: { - value1: lastTotalStakeBase.toNumber(), - value2: totalIssuanceBase.sub(lastTotalStakeBase).toNumber(), - }, - - tooltip: `${toFixedIfNecessary(supplyAsPercent, 2)}%`, - helpKey: 'Supply Staked', - }; - - return <Pie {...params} />; -}; - -export default SupplyStakedStatBox; diff --git a/src/pages/Nominate/Active/Status.tsx b/src/pages/Nominate/Active/Status.tsx deleted file mode 100644 index bcb2e86398..0000000000 --- a/src/pages/Nominate/Active/Status.tsx +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faCircle } from '@fortawesome/free-regular-svg-icons'; -import { - faChevronCircleRight, - faRedoAlt, - faWallet, -} from '@fortawesome/free-solid-svg-icons'; -import { BN } from 'bn.js'; -import { PayeeStatus } from 'consts'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useStaking } from 'contexts/Staking'; -import { useUi } from 'contexts/UI'; -import { useValidators } from 'contexts/Validators'; -import { CardWrapper } from 'library/Graphs/Wrappers'; -import Stat from 'library/Stat'; -import { useTranslation } from 'react-i18next'; -import { planckBnToUnit, rmCommas } from 'Utils'; -import { Separator } from 'Wrappers'; -import { Controller } from './Controller'; - -export const Status = ({ height }: { height: number }) => { - const { isReady, network } = useApi(); - const { setOnNominatorSetup, getStakeSetupProgressPercent }: any = useUi(); - const { openModalWith } = useModal(); - const { activeAccount, isReadOnlyAccount } = useConnect(); - const { isSyncing } = useUi(); - const { getNominationsStatus, staking, inSetup, eraStakers } = useStaking(); - const { getAccountNominations } = useBalances(); - const { stakers } = eraStakers; - const { payee } = staking; - const { meta, validators } = useValidators(); - const nominations = getAccountNominations(activeAccount); - const { t } = useTranslation('pages'); - - // get nomination status - const nominationStatuses = getNominationsStatus(); - - // get active nominations - const activeNominees = Object.entries(nominationStatuses) - .map(([k, v]: any) => (v === 'active' ? k : false)) - .filter((v) => v !== false); - - // check if rewards are being earned - const stake = meta.validators_browse?.stake ?? []; - const stakeSynced = stake.length > 0 ?? false; - - let earningRewards = false; - if (stakeSynced) { - for (const nominee of activeNominees) { - const validator = validators.find((v: any) => v.address === nominee); - if (validator) { - const batchIndex = validators.indexOf(validator); - const nomineeMeta = stake[batchIndex]; - const { lowestReward } = nomineeMeta; - - const validatorInEra = - stakers.find((s: any) => s.address === nominee) || null; - - if (validatorInEra) { - const { others } = validatorInEra; - const stakedValue = - others?.find((o: any) => o.who === activeAccount)?.value ?? false; - if (stakedValue) { - const stakedValueBase = planckBnToUnit( - new BN(rmCommas(stakedValue)), - network.units - ); - if (stakedValueBase >= lowestReward) { - earningRewards = true; - break; - } - } - } - } - } - } - - const payeeStatus = PayeeStatus.find((item) => item.key === payee); - - let startTitle = t('nominate.start_nominating'); - if (inSetup()) { - const progress = getStakeSetupProgressPercent(activeAccount); - if (progress > 0) { - startTitle += `: ${progress}%`; - } - } - return ( - <CardWrapper height={height}> - <Stat - label={t('nominate.status')} - helpKey="Nomination Status" - stat={ - inSetup() || isSyncing - ? t('nominate.not_nominating') - : !nominations.length - ? t('nominate.no_nominations_set') - : activeNominees.length - ? `${t('nominate.nominating_and')} ${ - earningRewards - ? t('nominate.earning_rewards') - : t('nominate.not_earning_rewards') - }` - : t('nominate.waiting_for_active_nominations') - } - buttons={ - !inSetup() - ? [] - : [ - { - title: startTitle, - icon: faChevronCircleRight, - transform: 'grow-1', - large: true, - disabled: - !isReady || - isReadOnlyAccount(activeAccount) || - !activeAccount, - onClick: () => setOnNominatorSetup(1), - }, - ] - } - /> - <Separator /> - <Stat - label={t('nominate.reward_destination')} - helpKey="Reward Destination" - icon={ - (payee === null - ? faCircle - : payee === 'Staked' - ? faRedoAlt - : payee === 'None' - ? faCircle - : faWallet) as IconProp - } - stat={ - inSetup() - ? t('nominate.not_assigned') - : payeeStatus?.name ?? t('nominate.not_assigned') - } - buttons={ - !inSetup() - ? [ - { - title: t('nominate.update'), - icon: faWallet, - small: true, - disabled: - inSetup() || isSyncing || isReadOnlyAccount(activeAccount), - onClick: () => openModalWith('UpdatePayee', {}, 'small'), - }, - ] - : [] - } - /> - <Separator /> - <Controller label={t('nominate.controller_account')} /> - </CardWrapper> - ); -}; - -export default Status; diff --git a/src/pages/Nominate/Setup/Payee/Wrappers.tsx b/src/pages/Nominate/Setup/Payee/Wrappers.tsx deleted file mode 100644 index 7251fb8461..0000000000 --- a/src/pages/Nominate/Setup/Payee/Wrappers.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { SectionFullWidthThreshold } from 'consts'; -import styled from 'styled-components'; -import { - borderPrimary, - buttonPrimaryBackground, - networkColor, - textPrimary, - textSecondary, -} from 'theme'; - -export const Items = styled.div` - position: relative; - margin: 0.75rem 0 0; - width: 100%; - border-radius: 0.75rem; - padding: 0.25rem; - overflow: auto; - display: flex; - flex-flow: row wrap; - flex: 1; -`; - -export const Item = styled.button<{ selected?: boolean }>` - flex-basis: 33%; - @media (max-width: ${SectionFullWidthThreshold}px) { - flex-basis: 100%; - } - padding: 0.5rem; - - > div { - background: ${buttonPrimaryBackground}; - border: 1.5px solid - ${(props) => (props.selected ? networkColor : borderPrimary)}; - width: 100%; - border-radius: 1rem; - display: flex; - flex-flow: row wrap; - justify-content: flex-start; - align-items: center; - padding: 1.25rem; - - > div { - width: 100%; - } - h3 { - color: ${textPrimary}; - font-size: 1.2rem; - } - &:first-child { - margin-left: 0rem; - } - &:last-child { - margin-right: 0rem; - } - p { - color: ${textSecondary}; - margin: 0.5rem 0 0 0; - text-align: left; - } - } -`; diff --git a/src/pages/Nominate/Setup/SetController.tsx b/src/pages/Nominate/Setup/SetController.tsx deleted file mode 100644 index dd37ccedc6..0000000000 --- a/src/pages/Nominate/Setup/SetController.tsx +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useApi } from 'contexts/Api'; -import { useConnect } from 'contexts/Connect'; -import { useUi } from 'contexts/UI'; -import { SetupType } from 'contexts/UI/types'; -import { AccountSelect } from 'library/Form/AccountSelect'; -import { InputItem } from 'library/Form/types'; -import { getEligibleControllers } from 'library/Form/Utils/getEligibleControllers'; -import { Warning } from 'library/Form/Warning'; -import { Footer } from 'library/SetupSteps/Footer'; -import { Header } from 'library/SetupSteps/Header'; -import { MotionContainer } from 'library/SetupSteps/MotionContainer'; -import { SetupStepProps } from 'library/SetupSteps/types'; -import { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { planckBnToUnit } from 'Utils'; -import { Spacer } from '../Wrappers'; - -export const SetController = (props: SetupStepProps) => { - const { section } = props; - const { t } = useTranslation('pages'); - - const { consts, network } = useApi(); - const { activeAccount, accounts, getAccount } = useConnect(); - const { getSetupProgress, setActiveAccountSetup } = useUi(); - const setup = getSetupProgress(SetupType.Stake, activeAccount); - const { existentialDeposit } = consts; - const existentialDepositBase = planckBnToUnit( - existentialDeposit, - network.units - ); - - // store the currently selected controller account - const _selected = setup.controller !== null ? setup.controller : null; - const [selected, setSelected] = useState<InputItem | null>( - getAccount(_selected) - ); - - // get eligible controllers for input - const items = getEligibleControllers(); - - // check if at least one item has enough unit to become a controller - const itemsWithEnoughBalance = items - .map( - (i: InputItem) => - i?.balance?.freeAfterReserve.gt(existentialDeposit) ?? false - ) - .filter((i: boolean) => i).length; - - // update selected value on account switch - useEffect(() => { - const _initial = getAccount( - setup.controller !== null ? setup.controller : null - ); - setSelected(_initial); - }, [activeAccount, accounts]); - - const handleOnChange = ({ selectedItem }: { selectedItem: InputItem }) => { - setSelected(selectedItem); - setActiveAccountSetup(SetupType.Stake, { - ...setup, - controller: selectedItem?.address ?? null, - }); - }; - - return ( - <> - <Header - thisSection={section} - title={t('nominate.set_controller_account') || ''} - helpKey="Stash and Controller Accounts" - complete={setup.controller !== null} - setupType={SetupType.Stake} - /> - <MotionContainer thisSection={section} activeSection={setup.section}> - {items.length === 0 && ( - <Warning - text={`${t('nominate.none_of_your')} ${existentialDepositBase} ${ - network.unit - }. ${t('nominate.top_up_account')}`} - /> - )} - {itemsWithEnoughBalance === 0 && ( - <Warning - text={`${t( - 'nominate.select_a_controller' - )} ${existentialDepositBase} ${network.unit}.`} - /> - )} - <Spacer /> - <AccountSelect - items={items} - onChange={handleOnChange} - placeholder={t('nominate.search_account')} - value={selected} - /> - <Footer - complete={setup.controller !== null} - setupType={SetupType.Stake} - /> - </MotionContainer> - </> - ); -}; - -export default SetController; diff --git a/src/pages/Overview/ActiveAccount.tsx b/src/pages/Overview/ActiveAccount.tsx deleted file mode 100644 index 865d9a6e94..0000000000 --- a/src/pages/Overview/ActiveAccount.tsx +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { faCopy } from '@fortawesome/free-regular-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useConnect } from 'contexts/Connect'; -import { useNotifications } from 'contexts/Notifications'; -import { NotificationText } from 'contexts/Notifications/types'; -import { Identicon } from 'library/Identicon'; -import { useTranslation } from 'react-i18next'; -import { clipAddress, convertRemToPixels } from 'Utils'; -import { ActiveAccounWrapper } from './Wrappers'; - -export const ActiveAccount = () => { - const { addNotification } = useNotifications(); - const { activeAccount, getAccount } = useConnect(); - const accountData = getAccount(activeAccount); - const { t } = useTranslation('pages'); - - // click to copy notification - let notification: NotificationText | null = null; - if (accountData !== null) { - notification = { - title: t('overview.address_copied'), - subtitle: accountData.address, - }; - } - - return ( - <ActiveAccounWrapper> - <div className="account"> - <div className="title"> - <h3> - {accountData && ( - <> - <div className="icon"> - <Identicon - value={accountData.address} - size={convertRemToPixels('1.7rem')} - /> - </div> - {clipAddress(accountData.address)} - <button - type="button" - onClick={() => { - navigator.clipboard.writeText(accountData.address); - if (notification) { - addNotification(notification); - } - }} - > - <FontAwesomeIcon - className="copy" - icon={faCopy as IconProp} - transform="shrink-1" - /> - </button> - {accountData.name !== clipAddress(accountData.address) && ( - <> - <div className="sep" /> - <div className="rest"> - <span className="name">{accountData.name}</span> - </div> - </> - )} - </> - )} - - {!accountData && t('overview.no_account_connected')} - </h3> - </div> - </div> - </ActiveAccounWrapper> - ); -}; - -export default ActiveAccount; diff --git a/src/pages/Overview/BalanceGraph.tsx b/src/pages/Overview/BalanceGraph.tsx deleted file mode 100644 index 7be3b8c1f9..0000000000 --- a/src/pages/Overview/BalanceGraph.tsx +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ArcElement, Chart as ChartJS, Legend, Tooltip } from 'chart.js'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { useConnect } from 'contexts/Connect'; -import { useTheme } from 'contexts/Themes'; -import { useTransferOptions } from 'contexts/TransferOptions'; -import { useUi } from 'contexts/UI'; -import { formatSize, useSize } from 'library/Graphs/Utils'; -import { usePrices } from 'library/Hooks/usePrices'; -import { OpenHelpIcon } from 'library/OpenHelpIcon'; -import React from 'react'; -import { Doughnut } from 'react-chartjs-2'; -import { useTranslation } from 'react-i18next'; -import { - defaultThemes, - networkColors, - networkColorsSecondary, -} from 'theme/default'; -import { - humanNumber, - planckBnToUnit, - toFixedIfNecessary, - usdFormatter, -} from 'Utils'; - -ChartJS.register(ArcElement, Tooltip, Legend); - -export const BalanceGraph = () => { - const { mode } = useTheme(); - const { network } = useApi(); - const { units } = network; - const { activeAccount } = useConnect(); - const { getAccountBalance } = useBalances(); - const { getTransferOptions } = useTransferOptions(); - const balance = getAccountBalance(activeAccount); - const { services } = useUi(); - const prices = usePrices(); - const { t } = useTranslation('pages'); - - const allTransferOptions = getTransferOptions(activeAccount); - const { freeBalance } = allTransferOptions; - - const { - freeToUnbond: staked, - totalUnlocking, - totalUnlocked, - } = allTransferOptions.nominate; - - const poolBondOpions = allTransferOptions.pool; - const unlockingPools = poolBondOpions.totalUnlocking.add( - poolBondOpions.totalUnlocked - ); - - const unlocking = unlockingPools.add(totalUnlocked).add(totalUnlocking); - - // get user's total balance - const { free } = balance; - const freeBase = planckBnToUnit( - free.add(poolBondOpions.active).add(unlockingPools), - units - ); - - // convert balance to fiat value - const freeFiat = toFixedIfNecessary(Number(freeBase * prices.lastPrice), 2); - - // graph data - let graphStaked = planckBnToUnit(staked, units); - let graphFreeToStake = planckBnToUnit(freeBalance, units); - - let graphInPool = planckBnToUnit(poolBondOpions.active, units); - let graphUnlocking = planckBnToUnit(unlocking, units); - - let zeroBalance = false; - if ( - graphStaked === 0 && - graphFreeToStake === 0 && - graphUnlocking === 0 && - graphInPool === 0 - ) { - graphStaked = -1; - graphUnlocking = -1; - graphFreeToStake = -1; - graphInPool = -1; - zeroBalance = true; - } - - const options = { - responsive: true, - maintainAspectRatio: false, - spacing: zeroBalance ? 0 : 5, - plugins: { - legend: { - display: true, - padding: { - right: 10, - }, - position: 'left' as const, - align: 'center' as const, - labels: { - padding: 20, - color: defaultThemes.text.primary[mode], - font: { - size: 13, - weight: '600', - }, - }, - }, - tooltip: { - displayColors: false, - backgroundColor: defaultThemes.graphs.tooltip[mode], - titleColor: defaultThemes.text.invert[mode], - bodyColor: defaultThemes.text.invert[mode], - bodyFont: { - weight: '600', - }, - callbacks: { - label: (context: any) => { - return `${ - context.parsed === -1 ? 0 : humanNumber(context.parsed) - } ${network.unit}`; - }, - }, - }, - }, - cutout: '78%', - }; - - // determine stats - const _labels = [ - t('overview.available'), - t('overview.unlocking'), - t('overview.nominating'), - t('overview.in_pool'), - ]; - const _data = [graphFreeToStake, graphUnlocking, graphStaked, graphInPool]; - const _colors = zeroBalance - ? [ - defaultThemes.graphs.colors[1][mode], - defaultThemes.graphs.inactive2[mode], - defaultThemes.graphs.inactive2[mode], - defaultThemes.graphs.inactive[mode], - ] - : [ - defaultThemes.graphs.colors[1][mode], - defaultThemes.graphs.colors[0][mode], - networkColors[`${network.name}-${mode}`], - networkColorsSecondary[`${network.name}-${mode}`], - ]; - - // default to a greyscale 50/50 donut on zero balance - let dataSet; - if (zeroBalance) { - dataSet = { - label: network.unit, - data: _data, - backgroundColor: _colors, - borderWidth: 0, - }; - } else { - dataSet = { - label: network.unit, - data: _data, - backgroundColor: _colors, - borderWidth: 0, - }; - } - - const data = { - labels: _labels, - datasets: [dataSet], - }; - - const ref = React.useRef<HTMLDivElement>(null); - const size = useSize(ref.current); - const { width, height, minHeight } = formatSize(size, 185); - - return ( - <> - <div className="head"> - <h4> - {t('overview.balance')} - <OpenHelpIcon helpKey="Your Balance" /> - </h4> - <h2> - <span className="amount">{humanNumber(freeBase)}</span>  - {network.unit} - <span className="fiat"> - {services.includes('binance_spot') && ( - <> {usdFormatter.format(Number(freeFiat))}</> - )} - </span> - </h2> - </div> - <div style={{ paddingTop: '1rem' }} /> - <div className="inner" ref={ref} style={{ minHeight }}> - <div - className="graph" - style={{ - height: `${height}px`, - width: `${width}px`, - position: 'absolute', - }} - > - <Doughnut options={options} data={data} /> - </div> - </div> - <div style={{ paddingTop: '1rem' }} /> - </> - ); -}; - -export default BalanceGraph; diff --git a/src/pages/Overview/NetworkSats/Inflation.tsx b/src/pages/Overview/NetworkSats/Inflation.tsx deleted file mode 100644 index b83f6bb0b3..0000000000 --- a/src/pages/Overview/NetworkSats/Inflation.tsx +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useApi } from 'contexts/Api'; -import { useNetworkMetrics } from 'contexts/Network'; -import { useStaking } from 'contexts/Staking'; -import useInflation from 'library/Hooks/useInflation'; -import { OpenHelpIcon } from 'library/OpenHelpIcon'; -import { useTranslation } from 'react-i18next'; -import { planckBnToUnit, toFixedIfNecessary } from 'Utils'; -import { InflationWrapper } from './Wrappers'; - -export const Inflation = () => { - const { units } = useApi().network; - const { metrics } = useNetworkMetrics(); - const { staking } = useStaking(); - const { inflation, stakedReturn } = useInflation(); - const { t } = useTranslation('pages'); - - const { lastTotalStake } = staking; - const { totalIssuance } = metrics; - - // total supply as percent - const totalIssuanceBase = planckBnToUnit(totalIssuance, units); - const lastTotalStakeBase = planckBnToUnit(lastTotalStake, units); - const supplyAsPercent = - lastTotalStakeBase === 0 - ? 0 - : lastTotalStakeBase / (totalIssuanceBase * 0.01); - - return ( - <InflationWrapper> - <section> - <div className="items"> - <div> - <div className="inner"> - <h2> - {totalIssuance.toString() === '0' - ? '0' - : toFixedIfNecessary(stakedReturn, 2)} - % - </h2> - <h4> - {t('overview.historical_rewards_rate')}{' '} - <OpenHelpIcon helpKey="Historical Rewards Rate" /> - </h4> - </div> - </div> - <div> - <div className="inner"> - <h2> - {totalIssuance.toString() === '0' - ? '0' - : toFixedIfNecessary(inflation, 2)} - % - </h2> - <h4> - {t('overview.inflation')} <OpenHelpIcon helpKey="Inflation" /> - </h4> - </div> - </div> - <div> - <div className="inner"> - <h2>{toFixedIfNecessary(supplyAsPercent, 2)}%</h2> - <h4> - {t('overview.supply_staked')}{' '} - <OpenHelpIcon helpKey="Supply Staked" /> - </h4> - </div> - </div> - </div> - </section> - </InflationWrapper> - ); -}; diff --git a/src/pages/Overview/Reserve.tsx b/src/pages/Overview/Reserve.tsx deleted file mode 100644 index 2df6bb3098..0000000000 --- a/src/pages/Overview/Reserve.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faLock } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useApi } from 'contexts/Api'; -import { useBalances } from 'contexts/Balances'; -import { OpenHelpIcon } from 'library/OpenHelpIcon'; -import { useTranslation } from 'react-i18next'; -import { planckBnToUnit, toFixedIfNecessary } from 'Utils'; -import { ReserveProps } from './types'; -import { ReserveWrapper, SectionWrapper, Separator } from './Wrappers'; - -export const Reserve = (props: ReserveProps) => { - const { height } = props; - const { network } = useApi(); - const { existentialAmount } = useBalances(); - const { t } = useTranslation('pages'); - - return ( - <SectionWrapper style={{ height }}> - <ReserveWrapper> - <Separator /> - <h4> - {t('overview.reserved')} - <OpenHelpIcon helpKey="Reserve Balance" /> - </h4> - - <div className="inner"> - <section> - <h3 className="reserve"> - <FontAwesomeIcon - icon={faLock} - transform="shrink-4" - className="icon" - /> - {`${toFixedIfNecessary( - planckBnToUnit(existentialAmount, network.units), - 5 - )} ${network.unit}`} - </h3> - </section> - </div> - </ReserveWrapper> - </SectionWrapper> - ); -}; - -export default Reserve; diff --git a/src/pages/Overview/Stats/ActiveEra.tsx b/src/pages/Overview/Stats/ActiveEra.tsx deleted file mode 100644 index cd3dfd62cf..0000000000 --- a/src/pages/Overview/Stats/ActiveEra.tsx +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useNetworkMetrics } from 'contexts/Network'; -import { useSessionEra } from 'contexts/SessionEra'; -import { format, fromUnixTime } from 'date-fns'; -import { useEraTimeLeft } from 'library/Hooks/useEraTimeLeft'; -import { Pie } from 'library/StatBoxList/Pie'; -import { locales } from 'locale'; -import { useTranslation } from 'react-i18next'; - -const ActiveEraStatBox = () => { - const { metrics } = useNetworkMetrics(); - const { sessionEra } = useSessionEra(); - const eraTimeLeft = useEraTimeLeft(); - const { i18n, t } = useTranslation('pages'); - - const _timeleft = fromUnixTime(eraTimeLeft); - const timeleft = format(_timeleft, 'kk:mm:ss', { - locale: locales[i18n.resolvedLanguage], - }); - - const params = { - label: t('overview.active_era'), - stat: { - value: metrics.activeEra.index, - unit: '', - }, - graph: { - value1: sessionEra.eraProgress, - value2: sessionEra.eraLength - sessionEra.eraProgress, - }, - tooltip: metrics.activeEra.index === 0 ? undefined : timeleft, - helpKey: 'Era', - }; - return <Pie {...params} />; -}; - -export default ActiveEraStatBox; diff --git a/src/pages/Overview/Stats/ActiveNominators.tsx b/src/pages/Overview/Stats/ActiveNominators.tsx deleted file mode 100644 index 7f7e75cb19..0000000000 --- a/src/pages/Overview/Stats/ActiveNominators.tsx +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import BN from 'bn.js'; -import { useApi } from 'contexts/Api'; -import { useStaking } from 'contexts/Staking'; -import { Pie } from 'library/StatBoxList/Pie'; -import { useTranslation } from 'react-i18next'; -import { toFixedIfNecessary } from 'Utils'; - -export const ActiveNominatorsStatBox = () => { - const { consts } = useApi(); - const { maxElectingVoters } = consts; - const { eraStakers } = useStaking(); - const { totalActiveNominators } = eraStakers; - const { t } = useTranslation('pages'); - - // active nominators as percent - let totalNominatorsAsPercent = 0; - if (maxElectingVoters > 0) { - totalNominatorsAsPercent = - totalActiveNominators / - new BN(maxElectingVoters).div(new BN(100)).toNumber(); - } - - const params = { - label: t('overview.active_nominators'), - stat: { - value: totalActiveNominators, - total: maxElectingVoters, - unit: '', - }, - graph: { - value1: totalActiveNominators, - value2: maxElectingVoters - totalActiveNominators, - }, - tooltip: `${toFixedIfNecessary(totalNominatorsAsPercent, 2)}%`, - helpKey: 'Active Nominators', - }; - - return <Pie {...params} />; -}; - -export default ActiveNominatorsStatBox; diff --git a/src/pages/Overview/Stats/TotalNominations.tsx b/src/pages/Overview/Stats/TotalNominations.tsx deleted file mode 100644 index 4361a574a9..0000000000 --- a/src/pages/Overview/Stats/TotalNominations.tsx +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import BN from 'bn.js'; -import { useStaking } from 'contexts/Staking'; -import { Pie } from 'library/StatBoxList/Pie'; -import { useTranslation } from 'react-i18next'; -import { toFixedIfNecessary } from 'Utils'; - -export const TotalNominatorsStatBox = () => { - const { staking } = useStaking(); - const { totalNominators, maxNominatorsCount } = staking; - const { t } = useTranslation('pages'); - - // total active nominators as percent - let totalNominatorsAsPercent = 0; - if (maxNominatorsCount.gt(new BN(0))) { - totalNominatorsAsPercent = totalNominators - .div(maxNominatorsCount.div(new BN(100))) - .toNumber(); - } - - const params = { - label: t('overview.total_nominators'), - stat: { - value: totalNominators.toNumber(), - total: maxNominatorsCount.toNumber(), - unit: '', - }, - graph: { - value1: totalNominators.toNumber(), - value2: maxNominatorsCount.sub(totalNominators).toNumber(), - }, - - tooltip: `${toFixedIfNecessary(totalNominatorsAsPercent, 2)}%`, - helpKey: 'Total Nominators', - }; - - return <Pie {...params} />; -}; - -export default TotalNominatorsStatBox; diff --git a/src/pages/Overview/Tips/Items.tsx b/src/pages/Overview/Tips/Items.tsx deleted file mode 100644 index 1cd5355619..0000000000 --- a/src/pages/Overview/Tips/Items.tsx +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useOverlay } from 'contexts/Overlay'; -import { motion, useAnimationControls } from 'framer-motion'; -import { Tip } from 'library/Tips/Tip'; -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import Lottie from 'react-lottie'; -import { ItemInnerWrapper, ItemsWrapper, ItemWrapper } from './Wrappers'; - -export const ItemsInner = ({ items, page }: any) => { - const controls = useAnimationControls(); - - // stores whether this is the initial display of tips - const [initial, setInitial] = useState(true); - - useEffect(() => { - doControls(true); - setInitial(false); - }, [page]); - - const doControls = async (transition: boolean) => { - if (transition) { - controls.set('hidden'); - controls.start('show'); - } else { - controls.set('show'); - } - }; - - return ( - <ItemsWrapper - initial="hidden" - animate={controls} - variants={{ - hidden: { opacity: 0 }, - show: { - opacity: 1, - }, - }} - > - {items.map((item: any, index: number) => ( - <Item - key={`tip_${index}_${page}`} - index={index} - {...item} - controls={controls} - initial={initial} - /> - ))} - </ItemsWrapper> - ); -}; - -const Item = ({ - title, - subtitle, - description, - icon, - index, - controls, - initial, -}: any) => { - const { openOverlayWith } = useOverlay(); - const { t } = useTranslation('tips'); - - const [isStopped, setIsStopped] = useState(true); - - useEffect(() => { - const delay = index * 75; - - if (initial) { - setTimeout(() => { - if (isStopped) { - setIsStopped(false); - } - }, delay); - } - }, []); - - const animateOptions = { - loop: false, - autoplay: false, - animationData: icon, - rendererSettings: { - preserveAspectRatio: 'xMidYMid slice', - }, - }; - - return ( - <ItemWrapper - animate={controls} - custom={index} - transition={{ - delay: index * 0.2, - duration: 0.7, - type: 'spring', - bounce: 0.35, - }} - variants={{ - hidden: { - y: 15, - }, - show: { - y: 0, - }, - }} - > - <ItemInnerWrapper> - <section> - <Lottie - options={animateOptions} - width="2.2rem" - height="2.2rem" - isStopped={isStopped} - isPaused={isStopped} - eventListeners={[ - { - eventName: 'loopComplete', - callback: () => setIsStopped(true), - }, - ]} - /> - </section> - <section> - <div className="title"> - <h3>{title}</h3> - </div> - <div className="desc"> - <h4> - {subtitle} - <motion.button - whileHover={{ scale: 1.02 }} - onClick={() => - openOverlayWith( - <Tip title={title} description={description} />, - 'large' - ) - } - type="button" - className="more" - > - {t('module.more')} - <FontAwesomeIcon icon={faChevronRight} transform="shrink-2" /> - </motion.button> - </h4> - </div> - </section> - </ItemInnerWrapper> - </ItemWrapper> - ); -}; - -export class Items extends React.Component<any, any> { - shouldComponentUpdate(nextProps: any) { - return JSON.stringify(this.props.items) !== JSON.stringify(nextProps.items); - } - - render() { - return <ItemsInner {...this.props} />; - } -} - -export default Items; diff --git a/src/pages/Overview/Tips/Syncing.tsx b/src/pages/Overview/Tips/Syncing.tsx deleted file mode 100644 index 39a25da4bb..0000000000 --- a/src/pages/Overview/Tips/Syncing.tsx +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { useApi } from 'contexts/Api'; -import * as refreshChangeJson from 'img/json/refresh-change-outline.json'; -import { useTranslation } from 'react-i18next'; -import Lottie from 'react-lottie'; -import { ItemInnerWrapper, ItemsWrapper, ItemWrapper } from './Wrappers'; - -export const Syncing = () => { - const { - network: { name }, - } = useApi(); - const { t } = useTranslation('tips'); - - const animateOptions = { - loop: true, - autoplay: true, - animationData: refreshChangeJson, - rendererSettings: { - preserveAspectRatio: 'xMidYMid slice', - }, - }; - - return ( - <ItemsWrapper - initial="show" - animate={undefined} - variants={{ - hidden: { opacity: 0 }, - show: { - opacity: 1, - }, - }} - > - <ItemWrapper> - <ItemInnerWrapper inactive> - <section> - <Lottie - options={animateOptions} - width="2.2rem" - height="2.2rem" - isStopped={false} - isPaused={false} - /> - </section> - <section> - <div className="title"> - <h3>{t('module.syncing_with', { network: name })}</h3> - </div> - <div className="desc"> - <h4>{t('module.one_moment')}</h4> - </div> - </section> - </ItemInnerWrapper> - </ItemWrapper> - </ItemsWrapper> - ); -}; diff --git a/src/pages/Overview/Tips/Wrappers.tsx b/src/pages/Overview/Tips/Wrappers.tsx deleted file mode 100644 index 857bbc9cde..0000000000 --- a/src/pages/Overview/Tips/Wrappers.tsx +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { motion } from 'framer-motion'; -import styled from 'styled-components'; -import { - backgroundLabel, - networkColor, - textPrimary, - textSecondary, -} from 'theme'; - -export const ItemsWrapper = styled(motion.div)` - width: 100%; - display: flex; - flex-flow: row nowrap; - justify-items: center; - margin: 0.25rem 0 0rem 0; -`; -export const ItemWrapper = styled(motion.div)` - padding: 0; - flex-basis: 100%; - &:last-child { - margin-right: 0.25rem; - } -`; - -export const ItemInnerWrapper = styled.div<{ inactive?: boolean }>` - border-radius: 1.25rem; - transition: border 0.05s; - display: flex; - flex-flow: row wrap; - height: 4rem; - transition: border 0.2s; - - > section { - height: 100%; - - &:nth-child(1) { - display: flex; - flex-flow: row wrap; - align-items: center; - padding-right: 1rem; - - .lpf { - fill: ${networkColor}; - } - .lps { - stroke: ${networkColor}; - } - } - - &:nth-child(2) { - display: flex; - flex-flow: column nowrap; - align-items: flex-start; - justify-content: center; - flex: 1; - - .title { - display: flex; - flex-flow: row nowrap; - align-items: center; - text-align: left; - overflow: hidden; - position: relative; - width: 100%; - height: 1.9rem; - position: relative; - top: 0.2rem; - - > h3 { - position: absolute; - top: 0; - left: 0; - width: auto; - max-width: 100%; - color: ${textPrimary}; - font-variation-settings: 'wght' 625; - margin: 0; - font-size: 1.2rem; - - padding-right: 6.75rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - height: 1.9rem; - - > span { - position: absolute; - right: 0; - min-width: 6.2rem; - font-variation-settings: 'wght' 500; - background: ${backgroundLabel}; - color: ${textSecondary}; - font-size: 0.97rem; - margin-left: 0.25rem; - padding: 0rem 0.6rem; - border-radius: 1.5rem; - opacity: 0.9; - text-align: center; - } - } - } - - .desc { - display: flex; - flex-flow: column nowrap; - align-items: center; - justify-content: flex-start; - overflow: hidden; - width: 100%; - height: 1.85rem; - position: relative; - - h4 { - color: ${textSecondary}; - position: absolute; - top: 0; - left: 0; - width: auto; - height: 1.85rem; - max-width: 100%; - margin: 0; - padding: 0.15rem 6.3rem 0rem 0; - text-align: left; - font-size: 1.05rem; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - } - .more { - position: absolute; - right: 0.2rem; - top: 0rem; - display: flex; - flex-flow: row nowrap; - align-items: center; - border: 1px solid ${networkColor}; - color: ${networkColor}; - border-radius: 1.5rem; - padding: 0rem 0.8rem; - font-size: 1rem; - - > svg { - margin-left: 0.4rem; - } - } - } - } - } -`; - -export const PageToggleWrapper = styled.div` - background: ${backgroundLabel}; - color: ${textSecondary}; - padding: 0.25rem 0.5rem; - border-radius: 1.5rem; - position: relative; - top: -0.2rem; - display: flex; - flex-flow: row wrap; - margin-left: 0.75rem; - - > button { - margin: 0 0.5rem; - opacity: 0.75; - font-size: 1.1rem; - transition: color 0.2s; - - > svg { - color: ${textSecondary}; - } - - &:hover { - opacity: 1; - color: ${networkColor}; - } - - &:disabled { - color: ${textSecondary}; - opacity: 0.1; - } - } - - h4 { - margin: 0; - span { - margin: 0 0.5rem; - } - &.disabled { - opacity: 0.25; - } - } -`; diff --git a/src/pages/Overview/Tips/index.tsx b/src/pages/Overview/Tips/index.tsx deleted file mode 100644 index b01e62bb8e..0000000000 --- a/src/pages/Overview/Tips/index.tsx +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { - faChevronCircleLeft, - faChevronCircleRight, - faCog, -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { TIPS_CONFIG } from 'config/tips'; -import { TipsThresholdMedium, TipsThresholdSmall } from 'consts'; -import { useApi } from 'contexts/Api'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { usePoolMemberships } from 'contexts/Pools/PoolMemberships'; -import { useStaking } from 'contexts/Staking'; -import { useTransferOptions } from 'contexts/TransferOptions'; -import { useUi } from 'contexts/UI'; -import { CardHeaderWrapper, CardWrapper } from 'library/Graphs/Wrappers'; -import useFillVariables from 'library/Hooks/useFillVariables'; -import { OpenHelpIcon } from 'library/OpenHelpIcon'; -import throttle from 'lodash.throttle'; -import { useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { AnyJson } from 'types'; -import { setStateWithRef } from 'Utils'; -import { Items } from './Items'; -import { Syncing } from './Syncing'; -import { PageToggleWrapper } from './Wrappers'; - -export const Tips = () => { - const { network } = useApi(); - const { activeAccount } = useConnect(); - const { networkSyncing } = useUi(); - const { openModalWith } = useModal(); - const { fillVariables } = useFillVariables(); - const { membership } = usePoolMemberships(); - const { isNominating, staking } = useStaking(); - const { isOwner } = useActivePools(); - const { getTransferOptions } = useTransferOptions(); - const { minNominatorBond, totalNominators, maxNominatorsCount } = staking; - const transferOptions = getTransferOptions(activeAccount); - const { t, i18n } = useTranslation(); - - // multiple tips per row is currently turned off. - const multiTipsPerRow = false; - - // helper function to determine the number of items to display per page. - // UI displays 1 item by default. - const getItemsPerPage = () => { - if (!multiTipsPerRow) { - return 1; - } - if (window.innerWidth < TipsThresholdSmall) { - return 1; - } - if ( - window.innerWidth >= TipsThresholdSmall && - window.innerWidth < TipsThresholdMedium - ) { - return 2; - } - return 3; - }; - - // helper function to determine which page we should be on upon page resize. - // This function ensures totalPages is never surpassed, but does not guarantee - // that the start item will maintain across resizes. - const getPage = () => { - const totalItmes = networkSyncing ? 1 : items.length; - const itemsPerPage = getItemsPerPage(); - const totalPages = Math.ceil(totalItmes / itemsPerPage); - if (pageRef.current > totalPages) { - return totalPages; - } - const end = pageRef.current * itemsPerPage; - const start = end - (itemsPerPage - 1); - return Math.ceil(start / itemsPerPage); - }; - - // resize callback - const resizeCallback = () => { - setStateWithRef(getPage(), setPage, pageRef); - setStateWithRef(getItemsPerPage(), setItemsPerPage, itemsPerPageRef); - }; - - // throttle resize callback - const throttledResizeCallback = throttle(resizeCallback, 200, { - trailing: true, - leading: false, - }); - - // re-sync page when active account changes - useEffect(() => { - setStateWithRef(getPage(), setPage, pageRef); - }, [activeAccount, network]); - - // resize event listener - useEffect(() => { - window.addEventListener('resize', throttledResizeCallback); - return () => { - window.removeEventListener('resize', throttledResizeCallback); - }; - }, []); - - // store the current amount of allowed items on display - const [itemsPerPage, setItemsPerPage] = useState<number>(getItemsPerPage()); - const itemsPerPageRef = useRef(itemsPerPage); - - // store the current page - const [page, setPage] = useState<number>(1); - const pageRef = useRef(page); - - const _itemsPerPage = itemsPerPageRef.current; - const _page = pageRef.current; - - // accumulate segments to include in tips - const segments: AnyJson = []; - if (!activeAccount) { - segments.push(1); - } else if (!isNominating() && !membership) { - if ( - transferOptions.freeBalance.gt(minNominatorBond) && - totalNominators.lt(maxNominatorsCount) - ) { - segments.push(2); - } else { - segments.push(3); - } - segments.push(4); - } else { - if (isNominating()) { - segments.push(5); - } - if (membership) { - if (!isOwner()) { - segments.push(6); - } else { - segments.push(7); - } - } - segments.push(8); - } - - // filter tips relevant to connected account. - let items = TIPS_CONFIG.filter((i: AnyJson) => segments.includes(i.s)); - - items = items.map((i: any) => { - const { id } = i; - - return fillVariables( - { - ...i, - title: t(`${id}.0`, { ns: 'tips' }), - subtitle: t(`${id}.1`, { ns: 'tips' }), - description: i18n.getResource(i18n.resolvedLanguage, 'tips', `${id}.2`), - }, - ['title', 'subtitle', 'description'] - ); - }); - - // determine items to be displayed - const endItem = networkSyncing - ? 1 - : Math.min(_page * _itemsPerPage, items.length); - const startItem = networkSyncing - ? 1 - : _page * _itemsPerPage - (_itemsPerPage - 1); - - const totalItems = networkSyncing ? 1 : items.length; - const itemsDisplay = items.slice(startItem - 1, endItem); - const totalPages = Math.ceil(totalItems / _itemsPerPage); - - return ( - <CardWrapper> - <CardHeaderWrapper withAction> - <h4> - {t('module.tips', { ns: 'tips' })} - <OpenHelpIcon helpKey="Dashboard Tips" /> - </h4> - <div> - <PageToggleWrapper> - <button - type="button" - disabled={totalPages === 1 || _page === 1} - onClick={() => { - setStateWithRef(_page - 1, setPage, pageRef); - }} - > - <FontAwesomeIcon - icon={faChevronCircleLeft} - className="icon" - transform="grow-1" - /> - </button> - <h4 className={totalPages === 1 ? `disabled` : undefined}> - <span> - {startItem} - {_itemsPerPage > 1 && - totalItems > 1 && - startItem !== endItem && - ` - ${endItem}`} - </span> - {totalPages > 1 && ( - <> - {t('module.of', { ns: 'tips' })} <span>{items.length}</span> - </> - )} - </h4> - <button - type="button" - disabled={totalPages === 1 || _page === totalPages} - onClick={() => { - setStateWithRef(_page + 1, setPage, pageRef); - }} - > - <FontAwesomeIcon - icon={faChevronCircleRight} - className="icon" - transform="grow-1" - /> - </button> - </PageToggleWrapper> - <PageToggleWrapper> - <button - type="button" - onClick={() => { - openModalWith('DismissTips', {}); - }} - > - <FontAwesomeIcon icon={faCog} /> - </button> - </PageToggleWrapper> - </div> - </CardHeaderWrapper> - {networkSyncing ? ( - <Syncing /> - ) : ( - <Items items={itemsDisplay} page={pageRef.current} /> - )} - </CardWrapper> - ); -}; diff --git a/src/pages/Pools/Home/MembersList/index.tsx b/src/pages/Pools/Home/MembersList/index.tsx deleted file mode 100644 index 2f91b75174..0000000000 --- a/src/pages/Pools/Home/MembersList/index.tsx +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faBars, faGripVertical } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { ListItemsPerBatch, ListItemsPerPage } from 'consts'; -import { useApi } from 'contexts/Api'; -import { useNetworkMetrics } from 'contexts/Network'; -import { usePoolMembers } from 'contexts/Pools/PoolMembers'; -import { useTheme } from 'contexts/Themes'; -import { motion } from 'framer-motion'; -import { Header, List, Wrapper as ListWrapper } from 'library/List'; -import { ListProvider, useList } from 'library/List/context'; -import { MotionContainer } from 'library/List/MotionContainer'; -import { Pagination } from 'library/List/Pagination'; -import { Selectable } from 'library/List/Selectable'; -import { useEffect, useRef, useState } from 'react'; -import { networkColors } from 'theme/default'; -import { AnyApi, Sync } from 'types'; -import { Member } from './Member'; - -export const MembersListInner = (props: any) => { - const { allowMoreCols, pagination, selectable, batchKey } = props; - - const actions = props.actions ?? []; - - const { mode } = useTheme(); - const provider = useList(); - const { isReady, network } = useApi(); - const { metrics } = useNetworkMetrics(); - const { fetchPoolMembersMetaBatch } = usePoolMembers(); - - // get list provider props - const { selected, listFormat, setListFormat } = provider; - - // get actions - const actionsAll = [...actions].filter((action) => !action.onSelected); - const actionsSelected = [...actions].filter( - (action: any) => action.onSelected - ); - - const disableThrottle = props.disableThrottle ?? false; - - // current page - const [page, setPage] = useState<number>(1); - - // current render iteration - const [renderIteration, _setRenderIteration] = useState<number>(1); - - // default list of validators - const [membersDefault, setMembersDefault] = useState(props.members); - - // manipulated list (ordering, filtering) of payouts - const [members, setMembers] = useState(props.members); - - // is this the initial fetch - const [fetched, setFetched] = useState<Sync>(Sync.Unsynced); - - // render throttle iteration - const renderIterationRef = useRef(renderIteration); - const setRenderIteration = (iter: number) => { - renderIterationRef.current = iter; - _setRenderIteration(iter); - }; - - // pagination - const totalPages = Math.ceil(members.length / ListItemsPerPage); - const pageEnd = page * ListItemsPerPage - 1; - const pageStart = pageEnd - (ListItemsPerPage - 1); - - // render batch - const batchEnd = renderIteration * ListItemsPerBatch - 1; - - // refetch list when list changes - useEffect(() => { - if (props.members !== membersDefault) { - setFetched(Sync.Unsynced); - } - }, [props.members]); - - // configure list when network is ready to fetch - useEffect(() => { - if (isReady && metrics.activeEra.index !== 0 && fetched === Sync.Unsynced) { - setupMembersList(); - } - }, [isReady, fetched, metrics.activeEra.index]); - - // render throttle - useEffect(() => { - if (!(batchEnd >= pageEnd || disableThrottle)) { - setTimeout(() => { - setRenderIteration(renderIterationRef.current + 1); - }, 500); - } - }, [renderIterationRef.current]); - - // trigger onSelected when selection changes - useEffect(() => { - if (props.onSelected) { - props.onSelected(provider); - } - }, [selected]); - - // handle validator list bootstrapping - const setupMembersList = () => { - setMembersDefault(props.members); - setMembers(props.members); - fetchPoolMembersMetaBatch(batchKey, props.members, false); - setFetched(Sync.Synced); - }; - - // get list items to render - let listMembers = []; - - // get throttled subset or entire list - if (!disableThrottle) { - listMembers = members.slice(pageStart).slice(0, ListItemsPerPage); - } else { - listMembers = members; - } - - if (!members.length) { - return <></>; - } - - return ( - <ListWrapper> - <Header> - <div> - <h4>{props.title}</h4> - </div> - <div> - <button type="button" onClick={() => setListFormat('row')}> - <FontAwesomeIcon - icon={faBars} - color={ - listFormat === 'row' - ? networkColors[`${network.name}-${mode}`] - : 'inherit' - } - /> - </button> - <button type="button" onClick={() => setListFormat('col')}> - <FontAwesomeIcon - icon={faGripVertical} - color={ - listFormat === 'col' - ? networkColors[`${network.name}-${mode}`] - : 'inherit' - } - /> - </button> - </div> - </Header> - <List flexBasisLarge={allowMoreCols ? '33.33%' : '50%'}> - {listMembers.length > 0 && pagination && ( - <Pagination page={page} total={totalPages} setter={setPage} /> - )} - {selectable && ( - <Selectable - actionsAll={actionsAll} - actionsSelected={actionsSelected} - /> - )} - <MotionContainer> - {listMembers.map((member: AnyApi, index: number) => { - // fetch batch data by referring to default list index. - const batchIndex = membersDefault.indexOf(member); - - return ( - <motion.div - className={`item ${listFormat === 'row' ? 'row' : 'col'}`} - key={`nomination_${index}`} - variants={{ - hidden: { - y: 15, - opacity: 0, - }, - show: { - y: 0, - opacity: 1, - }, - }} - > - <Member - who={member.who} - batchKey={batchKey} - batchIndex={batchIndex} - /> - </motion.div> - ); - })} - </MotionContainer> - </List> - </ListWrapper> - ); -}; - -export const MembersList = (props: any) => { - const { selectActive, selectToggleable } = props; - - return ( - <ListProvider - selectActive={selectActive} - selectToggleable={selectToggleable} - > - <MembersListInner {...props} /> - </ListProvider> - ); -}; diff --git a/src/pages/Pools/Home/PoolStats/Header.tsx b/src/pages/Pools/Home/PoolStats/Header.tsx deleted file mode 100644 index 0b67873941..0000000000 --- a/src/pages/Pools/Home/PoolStats/Header.tsx +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import BN from 'bn.js'; -import { useApi } from 'contexts/Api'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { usePoolMembers } from 'contexts/Pools/PoolMembers'; -import { PoolState } from 'contexts/Pools/types'; -import { useTranslation } from 'react-i18next'; -import { planckBnToUnit, rmCommas, toFixedIfNecessary } from 'Utils'; -import { HeaderWrapper } from './Wrappers'; - -export const Header = () => { - const { network } = useApi(); - const { selectedActivePool } = useActivePools(); - const { getMembersOfPool } = usePoolMembers(); - const { t } = useTranslation('pages'); - - const { state, points } = selectedActivePool?.bondedPool || {}; - const poolMembers = getMembersOfPool(selectedActivePool?.id ?? 0); - - const bonded = toFixedIfNecessary( - planckBnToUnit( - points ? new BN(rmCommas(points)) : new BN(0), - network.units - ), - 3 - ); - - let stateDisplay; - switch (state) { - case PoolState.Block: - stateDisplay = t('pools.locked'); - break; - case PoolState.Destroy: - stateDisplay = t('pools.destroying'); - break; - default: - stateDisplay = t('pools.open'); - break; - } - - return ( - <HeaderWrapper> - <section> - <div className="items"> - <div> - <div className="inner"> - <h2>{stateDisplay}</h2> - <h4>{t('pools.pool_state')}</h4> - </div> - </div> - <div> - <div className="inner"> - <h2>{poolMembers.length}</h2> - <h4>{t('pools.pool_members')}</h4> - </div> - </div> - <div> - <div className="inner"> - <h2> - {bonded} {network.unit} - </h2> - <h4>{t('pools.total_bonded')}</h4> - </div> - </div> - </div> - </section> - </HeaderWrapper> - ); -}; diff --git a/src/pages/Pools/Home/Status/Membership/Wrapper.ts b/src/pages/Pools/Home/Status/Membership/Wrapper.ts deleted file mode 100644 index ff20c16876..0000000000 --- a/src/pages/Pools/Home/Status/Membership/Wrapper.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import styled from 'styled-components'; - -interface WrapperProps { - paddingLeft: boolean; - paddingRight: string | null; -} - -export const Wrapper = styled.div<WrapperProps>` - display: flex; - flex-flow: row wrap; - - > .hide-with-padding { - padding-left: ${(props) => (props.paddingLeft ? '3rem' : '0')}; - padding-right: ${(props) => - props.paddingRight ? props.paddingRight : '0'}; - padding-top: 0.4rem; - padding-bottom: 0.4rem; - flex-shrink: 1; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - position: relative; - margin-bottom: 0; - - .icon { - position: absolute; - left: 0; - top: 0; - display: flex; - flex-flow: row wrap; - align-items: center; - } - - .btn { - position: absolute; - right: 0; - top: 0; - padding: 0.1rem; - display: flex; - flex-flow: row wrap; - align-items: center; - width: ${(props) => props.paddingRight}; - > div { - margin-left: 0.75rem; - } - - > span { - padding-left: 0.8rem; - } - } - } -`; diff --git a/src/pages/Pools/Home/Status/Membership/index.tsx b/src/pages/Pools/Home/Status/Membership/index.tsx deleted file mode 100644 index c52affca08..0000000000 --- a/src/pages/Pools/Home/Status/Membership/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { faCog, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; -import { ButtonPrimary } from '@rossbulat/polkadot-dashboard-ui'; -import { useApi } from 'contexts/Api'; -import { useConnect } from 'contexts/Connect'; -import { useModal } from 'contexts/Modal'; -import { useActivePools } from 'contexts/Pools/ActivePools'; -import { useBondedPools } from 'contexts/Pools/BondedPools'; -import { useTransferOptions } from 'contexts/TransferOptions'; -import { Identicon } from 'library/Identicon'; -import OpenHelpIcon from 'library/OpenHelpIcon'; -import { Wrapper as StatWrapper } from 'library/Stat/Wrapper'; -import { useTranslation } from 'react-i18next'; -import { determinePoolDisplay } from 'Utils'; -import { Wrapper } from './Wrapper'; - -export const Membership = ({ label }: { label: string }) => { - const { isReady } = useApi(); - const { activeAccount, isReadOnlyAccount } = useConnect(); - const { openModalWith } = useModal(); - const { bondedPools, meta } = useBondedPools(); - const { selectedActivePool, isDepositor, isOwner, isMember, isStateToggler } = - useActivePools(); - const { getTransferOptions } = useTransferOptions(); - const { active } = getTransferOptions(activeAccount).pool; - const { t } = useTranslation('pages'); - - let display = t('pools.not_in_pool'); - if (selectedActivePool) { - const pool = bondedPools.find((p: any) => { - return p.addresses.stash === selectedActivePool.addresses.stash; - }); - - if (pool) { - const metadata = meta.bonded_pools?.metadata ?? []; - const batchIndex = bondedPools.indexOf(pool); - display = determinePoolDisplay( - selectedActivePool.addresses.stash, - metadata[batchIndex] - ); - } - } - - const buttons = []; - let paddingRight = 0; - - if (isOwner() || isStateToggler()) { - paddingRight += 9; - buttons.push( - <ButtonPrimary - text={t('pools.manage')} - iconLeft={faCog} - disabled={!isReady || isReadOnlyAccount(activeAccount)} - onClick={() => openModalWith('ManagePool', {}, 'small')} - /> - ); - } - - if (isMember() && !isDepositor() && active?.gtn(0)) { - paddingRight += 8.5; - buttons.push( - <ButtonPrimary - text={t('pools.leave')} - iconLeft={faSignOutAlt} - disabled={!isReady || isReadOnlyAccount(activeAccount)} - onClick={() => - openModalWith('LeavePool', { bondType: 'pool' }, 'small') - } - /> - ); - } - - return ( - <StatWrapper> - <h4> - {label} - <OpenHelpIcon helpKey="Pool Membership" /> - </h4> - <Wrapper - paddingLeft={selectedActivePool !== null} - paddingRight={paddingRight === 0 ? null : `${String(paddingRight)}rem`} - > - <h2 className="hide-with-padding"> - <div className="icon"> - <Identicon - value={selectedActivePool?.addresses?.stash ?? ''} - size={26} - /> - </div> - {display} - {buttons.length > 0 && ( - <div className="btn"> - {buttons.map((b: any, i: number) => ( - <span key={i}>{b}</span> - ))} - </div> - )} - </h2> - </Wrapper> - </StatWrapper> - ); -}; diff --git a/src/reportWebVitals.ts b/src/reportWebVitals.ts deleted file mode 100644 index 8141a5b856..0000000000 --- a/src/reportWebVitals.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { ReportHandler } from 'web-vitals'; - -const reportWebVitals = (onPerfEntry?: ReportHandler) => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/src/theme/default.ts b/src/theme/default.ts deleted file mode 100644 index a41ac7176a..0000000000 --- a/src/theme/default.ts +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import { NETWORKS } from 'config/networks'; -import { Network } from 'types'; - -// configure theme -const v = (light: string, dark: string) => ({ - light, - dark, -}); - -// eslint-disable-next-line -export const defaultThemes: { [key: string]: any } = { - transparent: v('rgba(255,255,255,0', 'rgba(0,0,0,0)'), - text: { - primary: v('#333', '#ccc'), - secondary: v('#444', '#aaa'), - invert: v('#fafafa', '#0e0e0e'), - warning: v('#be7900', '#be7900'), - danger: v('#ae2324', '#d14445'), - success: v('green', 'green'), - }, - background: { - primary: v('rgba(245,244,244,1)', 'rgba(39,39,39,1)'), - gradient: v( - 'linear-gradient(180deg, rgba(245,244,244,1) 0%, rgba(245,244,244,1) 100px, rgba(230,230,230, 1) 80%, rgba(253,239,234,1) 100%)', - 'linear-gradient(180deg, rgba(39,39,39,1) 0%, rgba(39,39,39,1) 100px, rgba(21,21,21,1) 100%)' - ), - secondary: v('rgba(255,255,255,0.58)', 'rgba(0,0,0,0.25)'), - network: v('rgba(244,225,225,0.75)', 'rgba(39,39,39,0.75)'), - dropdown: v('rgba(237,237,237,0.6)', 'rgba(33,33,33,0.6)'), - modalitem: v('rgba(244,244,244,0.6)', 'rgba(22,22,22,0.4)'), - validator: v( - 'linear-gradient(90deg, rgba(240,240,239,0.95) 0%, rgba(240,240,239,0.7) 100%)', - 'linear-gradient(90deg, rgba(30,30,30,0.8) 0%, rgba(30,30,30,0.5) 100%)' - ), - label: v( - 'linear-gradient(90deg, rgba(243,240,239,1) 0%, rgba(243,240,239,0.95) 100%)', - 'linear-gradient(90deg, rgba(40,40,40,0.85) 0%, rgba(40,40,40,0.95) 100%)' - ), - tag: v('rgba(220,220,220,0.75)', 'rgba(36,36,36,0.75)'), - identicon: v('#eee', '#333'), - overlay: v( - 'linear-gradient(180deg, rgba(244,242,242,0.93) 0%, rgba(228,225,225,0.93) 100%)', - 'linear-gradient(180deg, rgba(20,20,20,0.93) 0%, rgba(14,14,14,0.93) 100%)' - ), - }, - highlight: { - primary: v( - 'linear-gradient(90deg, rgba(0,0,0,0.06) 0%, rgba(0,0,0,0.03) 100%)', - 'linear-gradient(90deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0.04) 100%)' - ), - secondary: v( - 'linear-gradient(90deg, rgba(0,0,0,0.04) 0%, rgba(0,0,0,0.01) 100%)', - 'linear-gradient(90deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.01) 100%)' - ), - }, - graphs: { - colors: [v('#ccc', '#555'), v('#eee', '#222')], - inactive: v('#cfcfcf', '#1a1a1a'), - inactive2: v('#dadada', '#383838'), - tooltip: v('#333', '#ddd'), - grid: v('#e8e8e8', '#222'), - }, - buttons: { - primary: { background: v('rgba(248, 248, 248, 0.9)', '#0f0f0f') }, - secondary: { background: v('#eeecec', '#333') }, - toggle: { background: v('rgba(244,243,242,1)', '#1a1a1a') }, - help: { background: v('#ececec', '#242424') }, - hover: { background: v('#e8e6e6', '#080808') }, - disabled: { - background: v('#F3F6F4', '#000000'), - text: v('#ececec', '#444444'), - }, - }, - border: { - primary: v('#e6e6e6', '#282828'), - secondary: v('#ccc', '#444'), - }, - modal: { - overlay: v('rgba(242,240,240,0.6)', 'rgba(16,16,16,0.6)'), - background: v('#fff', '#0b0b0b'), - }, - overlay: { - background: v('rgba(200,200,200,0.45)', 'rgba(30,30,30,0.6)'), - }, - help: { - button: { - background: v('rgba(255,255,255,0.90)', 'rgba(0,0,0,0.85)'), - }, - }, - loader: { - foreground: v('#e1e1e1', '#151515'), - background: v('#dadada', '#101010'), - }, - shadow: { - primary: v('#dedede', '#1f1f1f'), - secondary: v('#eaeaea', '#222'), - }, - status: { - danger: { - solid: v('red', 'red'), - transparent: v('rgba(255,0,0,0.25)', 'rgba(255,0,0,0.25)'), - }, - warning: { - solid: v('rgba(219, 161, 0, 1)', 'rgba(219, 161, 0,1)'), - transparent: v('rgba(255,165,0,0.5)', 'rgba(255,165,0,0.5)'), - }, - success: { - solid: v('green', 'green'), - transparent: v('rgba(0,128,0,0.25)', 'rgba(0,128,0,0.25)'), - }, - }, -}; - -// configure card style -const c = (flat: string, border: string, shadow: string) => ({ - flat, - border, - shadow, -}); - -// eslint-disable-next-line -export const cardThemes = { - card: { - border: c('none', '1px solid', 'none'), - shadow: c('none', 'none', '-2px 2px 10px'), - }, -}; - -// configure network colors -export const networkColors: { [key: string]: string } = {}; -export const networkColorsSecondary: { [key: string]: string } = {}; -export const networkColorsStroke: { [key: string]: string } = {}; -export const networkColorsTransparent: { [key: string]: string } = {}; - -Object.values(NETWORKS).forEach((node: Network) => { - const { name, colors } = node; - const { primary, secondary, stroke, transparent } = colors; - - networkColors[`${name}-light`] = primary.light; - networkColors[`${name}-dark`] = primary.dark; - - networkColorsSecondary[`${name}-light`] = secondary.light; - networkColorsSecondary[`${name}-dark`] = secondary.dark; - - networkColorsStroke[`${name}-light`] = stroke.light; - networkColorsStroke[`${name}-dark`] = stroke.dark; - - networkColorsTransparent[`${name}-light`] = transparent.light; - networkColorsTransparent[`${name}-dark`] = transparent.dark; -}); diff --git a/src/theme/index.ts b/src/theme/index.ts deleted file mode 100644 index 127647d86a..0000000000 --- a/src/theme/index.ts +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -import theme from 'styled-theming'; -import { - cardThemes, - defaultThemes, - networkColors, - networkColorsSecondary, - networkColorsStroke, - networkColorsTransparent, -} from './default'; - -/* Aggregates all theme configurations and serves the currently - * active mode via the theming context. - */ -const v = 'mode'; - -// text colors - -export const textPrimary: theme.ThemeSet = theme(v, defaultThemes.text.primary); - -export const textSecondary: theme.ThemeSet = theme( - v, - defaultThemes.text.secondary -); - -export const textInvert: theme.ThemeSet = theme(v, defaultThemes.text.invert); - -export const textWarning: theme.ThemeSet = theme(v, defaultThemes.text.warning); - -export const textDanger: theme.ThemeSet = theme(v, defaultThemes.text.danger); - -export const textSuccess: theme.ThemeSet = theme(v, defaultThemes.text.success); - -// background colors - -export const backgroundPrimary: theme.ThemeSet = theme( - v, - defaultThemes.background.primary -); - -export const backgroundSecondary: theme.ThemeSet = theme( - v, - defaultThemes.background.secondary -); - -export const backgroundGradient: theme.ThemeSet = theme( - v, - defaultThemes.background.gradient -); - -export const backgroundNetworkBar: theme.ThemeSet = theme( - v, - defaultThemes.background.network -); - -export const backgroundDropdown: theme.ThemeSet = theme( - v, - defaultThemes.background.dropdown -); - -export const backgroundModalItem: theme.ThemeSet = theme( - v, - defaultThemes.background.modalitem -); - -export const backgroundValidator: theme.ThemeSet = theme( - v, - defaultThemes.background.validator -); - -export const backgroundLabel: theme.ThemeSet = theme( - v, - defaultThemes.background.label -); - -export const backgroundIdenticon: theme.ThemeSet = theme( - v, - defaultThemes.background.identicon -); - -export const backgroundOverlay: theme.ThemeSet = theme( - v, - defaultThemes.background.overlay -); - -// highlights - -export const highlightPrimary: theme.ThemeSet = theme( - v, - defaultThemes.highlight.primary -); - -export const highlightSecondary: theme.ThemeSet = theme( - v, - defaultThemes.highlight.secondary -); - -// buttons - -export const buttonPrimaryBackground: theme.ThemeSet = theme( - v, - defaultThemes.buttons.primary.background -); - -export const buttonSecondaryBackground: theme.ThemeSet = theme( - v, - defaultThemes.buttons.secondary.background -); - -export const backgroundToggle: theme.ThemeSet = theme( - v, - defaultThemes.buttons.toggle.background -); - -export const buttonHelpBackground: theme.ThemeSet = theme( - v, - defaultThemes.buttons.help.background -); - -export const buttonHoverBackground: theme.ThemeSet = theme( - v, - defaultThemes.buttons.hover.background -); - -export const buttonDisabledBackground: theme.ThemeSet = theme( - v, - defaultThemes.buttons.disabled.background -); - -export const buttonDisabledText: theme.ThemeSet = theme( - v, - defaultThemes.buttons.disabled.text -); - -// labels - -export const tagBackground: theme.ThemeSet = theme( - v, - defaultThemes.background.tag -); - -// graphs - -export const tooltipBackground: theme.ThemeSet = theme( - v, - defaultThemes.graphs.tooltip -); - -export const gridColor: theme.ThemeSet = theme(v, defaultThemes.graphs.grid); - -// borders - -export const borderPrimary: theme.ThemeSet = theme( - v, - defaultThemes.border.primary -); - -export const borderSecondary: theme.ThemeSet = theme( - v, - defaultThemes.border.secondary -); - -// modal - -export const modalOverlayBackground: theme.ThemeSet = theme( - v, - defaultThemes.modal.overlay -); - -export const modalBackground: theme.ThemeSet = theme( - v, - defaultThemes.modal.background -); - -// overlay - -export const overlayBackground: theme.ThemeSet = theme( - v, - defaultThemes.overlay.background -); - -// help - -export const helpButton: theme.ThemeSet = theme( - v, - defaultThemes.help.button.background -); - -// status colors - -export const danger: theme.ThemeSet = theme( - v, - defaultThemes.status.danger.solid -); - -export const dangerTransparent: theme.ThemeSet = theme( - v, - defaultThemes.status.danger.transparent -); - -export const warning: theme.ThemeSet = theme( - v, - defaultThemes.status.warning.solid -); - -export const warningTransparent: theme.ThemeSet = theme( - v, - defaultThemes.status.warning.transparent -); - -export const success: theme.ThemeSet = theme( - v, - defaultThemes.status.success.solid -); - -export const successTransparent: theme.ThemeSet = theme( - v, - defaultThemes.status.success.transparent -); - -// shadow - -export const shadowColor: theme.ThemeSet = theme( - v, - defaultThemes.shadow.primary -); - -export const shadowColorSecondary: theme.ThemeSet = theme( - v, - defaultThemes.shadow.secondary -); - -/* Aggregates all card configurations and serves the currently - * active card style via the theming context. - */ -const c = 'card'; - -export const cardBorder: theme.ThemeSet = theme(c, cardThemes.card.border); - -export const cardShadow: theme.ThemeSet = theme(c, cardThemes.card.shadow); - -/* Serves the currently active network color via the theming context. - */ - -const n = 'network'; - -export const networkColor: theme.ThemeSet = theme(n, networkColors); - -export const networkColorSecondary: theme.ThemeSet = theme( - n, - networkColorsSecondary -); - -export const networkColorStroke: theme.ThemeSet = theme(n, networkColorsStroke); - -export const networkColorTransparent: theme.ThemeSet = theme( - n, - networkColorsTransparent -); diff --git a/src/types/index.ts b/src/types/index.ts index 38dc7c8098..fa939af016 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -12,7 +12,11 @@ declare global { } } -export type NetworkName = 'Cere Mainnet' | 'Cere Testnet' | 'Cere Devnet' | 'Cere Qanet'; +export type NetworkName = + | 'Cere Mainnet' + | 'Cere Testnet' + | 'Cere Devnet' + | 'Cere Qanet'; export type Networks = Record<string, Network>; @@ -29,6 +33,7 @@ export interface Network { defaultRpcEndpoint: string; rpcEndpoints: Record<string, string>; }; + cereStatsEndpoint: string; namespace: string; // eslint-disable-next-line @typescript-eslint/no-unused-vars colors: Record<NetworkColor, { [key in Theme]: string }>; @@ -100,7 +105,12 @@ export type MaybeAddress = string | null; export type MaybeString = string | null; // list of available plugins. -export type Plugin = 'subscan' | 'binance_spot' | 'tips' | 'polkawatch' | 'cereStats'; // ToDo +export type Plugin = + | 'subscan' + | 'binance_spot' + | 'tips' + | 'polkawatch' + | 'cereStats'; // ToDo // track the status of a syncing / fetching process. export type Sync = 'unsynced' | 'syncing' | 'synced'; diff --git a/src/types/styles.ts b/src/types/styles.ts index fa4f8b6d55..69481d43b1 100644 --- a/src/types/styles.ts +++ b/src/types/styles.ts @@ -1,7 +1,7 @@ // Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors // SPDX-License-Identifier: Apache-2.0 -import { RefObject } from 'react'; +import type { RefObject } from 'react'; export interface PageRowWrapperProps { noVerticalSpacer?: boolean; diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index e1afff2090..0000000000 --- a/webpack.config.js +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors -// SPDX-License-Identifier: Apache-2.0 - -// eslint-disable-next-line -const TerserPlugin = require('terser-webpack-plugin'); - -module.exports = { - module: { - rules: [ - { - test: /\.worker\.ts$/, - loader: 'worker-loader', - options: { - inline: true, - }, - }, - { - test: /\.ts$/, - loader: 'ts-loader', - options: { - inline: true, - }, - }, - ], - }, - resolve: { - extensions: ['.ts', '.js'], - }, - optimization: { - minimize: true, - minimizer: [new TerserPlugin({ - sourceMap: true, - terserOptions: { - compress: { - drop_console: true, - }, - } - })], - }, -}; diff --git a/yarn.lock b/yarn.lock index 29ac167619..81470807d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15,6 +15,26 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@apollo/client@^3.7.14": + version "3.9.4" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.9.4.tgz#a0230ce42a4d0c26c9f75f2a10c0f330f3ef135c" + integrity sha512-Ip6dxjshDT2Dp6foLASTnKBW45Fytew/5JZutZwgc78hVrrGpO9UtZA9xteHXYdap0wIgCxCfeIQwbSu1ZdQpw== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + "@wry/caches" "^1.0.0" + "@wry/equality" "^0.5.6" + "@wry/trie" "^0.5.0" + graphql-tag "^2.12.6" + hoist-non-react-statics "^3.3.2" + optimism "^0.18.0" + prop-types "^15.7.2" + rehackt "0.0.4" + response-iterator "^0.2.6" + symbol-observable "^4.0.0" + ts-invariant "^0.10.3" + tslib "^2.3.0" + zen-observable-ts "^1.2.5" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.13": version "7.22.13" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" @@ -428,6 +448,11 @@ dependencies: prop-types "^15.8.1" +"@graphql-typed-document-node/core@^3.1.1": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" + integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -1459,6 +1484,41 @@ loupe "^2.3.6" pretty-format "^29.5.0" +"@wry/caches@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52" + integrity sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA== + dependencies: + tslib "^2.3.0" + +"@wry/context@^0.7.0": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.4.tgz#e32d750fa075955c4ab2cfb8c48095e1d42d5990" + integrity sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ== + dependencies: + tslib "^2.3.0" + +"@wry/equality@^0.5.6": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.7.tgz#72ec1a73760943d439d56b7b1e9985aec5d497bb" + integrity sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw== + dependencies: + tslib "^2.3.0" + +"@wry/trie@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.4.3.tgz#077d52c22365871bf3ffcbab8e95cb8bc5689af4" + integrity sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w== + dependencies: + tslib "^2.3.0" + +"@wry/trie@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.5.0.tgz#11e783f3a53f6e4cd1d42d2d1323f5bc3fa99c94" + integrity sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA== + dependencies: + tslib "^2.3.0" + "@zondax/ledger-substrate@^0.41.3": version "0.41.3" resolved "https://registry.yarnpkg.com/@zondax/ledger-substrate/-/ledger-substrate-0.41.3.tgz#04e33a8aa8c589551caf63139653aba4ed7b9219" @@ -1685,6 +1745,11 @@ axobject-query@^3.1.1: dependencies: dequal "^2.0.3" +backo2@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -2626,6 +2691,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +eventemitter3@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + eventemitter3@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" @@ -2994,6 +3064,18 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +graphql-tag@^2.12.6: + version "2.12.6" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" + integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== + dependencies: + tslib "^2.1.0" + +graphql@^16.6.0: + version "16.8.1" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" + integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== + has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" @@ -3071,6 +3153,13 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + html-parse-stringify@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" @@ -3377,6 +3466,11 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== +iterall@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" + integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== + iterator.prototype@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" @@ -3875,6 +3969,16 @@ open@^9.1.0: is-inside-container "^1.0.0" is-wsl "^2.2.0" +optimism@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.18.0.tgz#e7bb38b24715f3fdad8a9a7fc18e999144bbfa63" + integrity sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ== + dependencies: + "@wry/caches" "^1.0.0" + "@wry/context" "^0.7.0" + "@wry/trie" "^0.4.3" + tslib "^2.3.0" + optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -4161,7 +4265,7 @@ react-i18next@^13.3.1: "@babel/runtime" "^7.22.5" html-parse-stringify "^3.0.1" -react-is@^16.13.1: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -4257,6 +4361,11 @@ regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: define-properties "^1.2.0" set-function-name "^2.0.0" +rehackt@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.0.4.tgz#dca5498e1f6c81d3d610ff2abfb3c3feec6afce8" + integrity sha512-xFroSGCbMEK/cTJVhq+c8l/AzIeMeojVyLqtZmr2jmIAFvePjapkCSGg9MnrcNk68HPaMxGf+Ndqozotu78ITw== + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -4290,6 +4399,11 @@ resolve@^2.0.0-next.4: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +response-iterator@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/response-iterator/-/response-iterator-0.2.6.tgz#249005fb14d2e4eeb478a3f735a28fd8b4c9f3da" + integrity sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -4655,6 +4769,17 @@ stylis@^4.3.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.0.tgz#abe305a669fc3d8777e10eefcfc73ad861c5588c" integrity sha512-E87pIogpwUsUwXw7dNyU4QDjdgVMy52m+XEOPEKUn161cCzWjjhPSQhByfd1CcNvrOLnXQ6OnnZDwnJrz/Z4YQ== +subscriptions-transport-ws@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.11.0.tgz#baf88f050cba51d52afe781de5e81b3c31f89883" + integrity sha512-8D4C6DIH5tGiAIpp5I0wD/xRlNiZAPGHygzCe7VzyzUoxHtawzjNAY9SUTXU05/EY2NMY9/9GF0ycizkXr1CWQ== + dependencies: + backo2 "^1.0.2" + eventemitter3 "^3.1.0" + iterall "^1.2.1" + symbol-observable "^1.0.4" + ws "^5.2.0 || ^6.0.0 || ^7.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -4679,6 +4804,16 @@ svg-parser@^2.0.4: resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== +symbol-observable@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + +symbol-observable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + synckit@^0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.5.tgz#b7f4358f9bb559437f9f167eb6bc46b3c9818fa3" @@ -4746,6 +4881,13 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331" integrity sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg== +ts-invariant@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.10.3.tgz#3e048ff96e91459ffca01304dbc7f61c1f642f6c" + integrity sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ== + dependencies: + tslib "^2.1.0" + tsconfck@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-2.1.2.tgz#f667035874fa41d908c1fe4d765345fcb1df6e35" @@ -4766,7 +4908,7 @@ tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: +tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.5.3, tslib@^2.6.0, tslib@^2.6.1, tslib@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== @@ -5151,6 +5293,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +"ws@^5.2.0 || ^6.0.0 || ^7.0.0": + version "7.5.9" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" + integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== + ws@^8.14.1, ws@^8.8.1: version "8.14.2" resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" @@ -5198,3 +5345,15 @@ yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + +zen-observable-ts@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58" + integrity sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg== + dependencies: + zen-observable "0.8.15" + +zen-observable@0.8.15: + version "0.8.15" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" + integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== From d429787bb3b0d45487a965ded283259a8d68331b Mon Sep 17 00:00:00 2001 From: Raid Ateir <ateirraid@gmail.com> Date: Thu, 15 Feb 2024 16:15:56 +0100 Subject: [PATCH 4/6] fixing errors with networks & index.html --- index.html | 2 +- package.json | 4 +- public/index.html | 95 ---------------------------------- src/Providers.tsx | 2 + src/config/ledger.ts | 2 +- src/config/networks.ts | 8 +-- src/consts.ts | 2 +- src/contexts/Network/index.tsx | 11 +++- src/modals/Connect/Ledger.tsx | 4 +- src/types/index.ts | 8 +-- 10 files changed, 26 insertions(+), 112 deletions(-) delete mode 100644 public/index.html diff --git a/index.html b/index.html index d5cb01207c..09726b6ee2 100644 --- a/index.html +++ b/index.html @@ -81,4 +81,4 @@ <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> -</html> \ No newline at end of file +</html> diff --git a/package.json b/package.json index fe70fe2371..d64da79073 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "license": "GPL-3.0-only", "private": false, "scripts": { - "build": "tsc && vite build --base '/'", + "build": "tsc && vite build --base './'", "build:pages": "tsc && vite build --base '/polkadot-staking-dashboard/'", "clear": "rm -rf node_modules build tsconfig.tsbuildinfo", "deploy:pages": "yarn build:pages && gh-pages -d build", - "dev": "vite", + "dev": "vite --base './'", "lint": "eslint . --fix && npx prettier --write . && npx prettier --write ./.scripts && node ./.scripts/localeOrderKeys.cjs", "locale:order": "node ./.scripts/localeOrderKeys.cjs", "locale:validate": "node ./.scripts/localeValidate.cjs", diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 2db5a71a35..0000000000 --- a/public/index.html +++ /dev/null @@ -1,95 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> - -<head> - <meta charset="utf-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <meta http-equiv="Permissions-Policy" content="interest-cohort=()" /> - <link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/favicons/apple-touch-icon.png"> - <link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicons/favicon-32x32.png"> - <link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicons/favicon-16x16.png"> - <link rel="mask-icon" href="%PUBLIC_URL%/favicons/safari-pinned-tab.svg" color="#e6007a"> - <meta name="msapplication-TileColor" content="#e6007a"> - <meta name="theme-color" content="#fff"> - - <meta name="title" content="Cere Staking Dashboard | Cere Staking (CERE)"> - <meta name="description" - content="Cere Staking Dashboard is the easiest way to stake CERE, check validator stats, manage your nominations and join nomination pools. Stake on Cere Network (CERE)."> - - <!-- Open Graph / Facebook --> - <meta property="og:type" content="website"> - <meta property="og:url" content="https://staking.cere.network"> - <meta property="og:title" content="Cere Staking Dashboard | Cere Staking (CERE)"> - <meta property="og:description" - content="Cere Staking Dashboard is the easiest way to stake CERE, check validator stats, manage your nominations and join nomination pools. Stake on Cere Network (CERE)."> - <meta property="og:image" content="%PUBLIC_URL%/img/og-image.png"> - - <!-- Twitter --> - <meta property="twitter:card" content="summary_large_image"> - <meta property="twitter:url" content="https://staking.cere.network"> - <meta property="twitter:title" content="Cere Staking Dashboard | Cere Staking (CERE)"> - <meta property="twitter:description" - content="Cere Staking Dashboard is the easiest way to stake CERE, check validator stats, manage your nominations and join nomination pools. Stake on Cere Network (CERE)."> - <meta property="twitter:image" content="%PUBLIC_URL%/img/og-image.png"> - - <!-- - manifest.json provides metadata used when your web app is installed on a - user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ - --> - <link rel="manifest" href="%PUBLIC_URL%/favicons/site.webmanifest" crossorigin="use-credentials"> - <!-- - Notice the use of %PUBLIC_URL% in the tags above. - It will be replaced with the URL of the `public` folder during the build. - Only files inside the `public` folder can be referenced from the HTML. - - Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will - work correctly both with client-side routing and a non-root public URL. - Learn how to configure a non-root public URL by running `npm run build`. - --> - <title>Cere Staking Dashboard - - - - - - - - - - - -
- - - - - - - - diff --git a/src/Providers.tsx b/src/Providers.tsx index 7f1be0804d..51bca58a5d 100644 --- a/src/Providers.tsx +++ b/src/Providers.tsx @@ -53,6 +53,8 @@ import { CereStatsProvider } from './contexts/CereStats'; // Embed providers from hook. export const Providers = () => { + const networkAllData = useNetwork(); + console.warn(networkAllData); const { network, networkData: { ss58 }, diff --git a/src/config/ledger.ts b/src/config/ledger.ts index 01193ccd0f..0651a01126 100644 --- a/src/config/ledger.ts +++ b/src/config/ledger.ts @@ -6,7 +6,7 @@ import PolkadotSVG from 'img/appIcons/polkadot.svg?react'; export const LedgerApps: LedgerApp[] = [ { - network: 'Cere Mainnet', + network: 'cereMainnet', appName: 'Polkadot', Icon: PolkadotSVG, }, diff --git a/src/config/networks.ts b/src/config/networks.ts index 3d1948d883..f8f048bbb6 100644 --- a/src/config/networks.ts +++ b/src/config/networks.ts @@ -5,10 +5,10 @@ import CereLogoSvg from 'img/cere_logo.svg?react'; import type { NetworkName, Networks } from 'types'; import BigNumber from 'bignumber.js'; -const CereMainnet: NetworkName = 'Cere Mainnet'; -const CereTestnet: NetworkName = 'Cere Testnet'; -const CereDevnet: NetworkName = 'Cere Devnet'; -const CereQanet: NetworkName = 'Cere Qanet'; +const CereMainnet: NetworkName = 'cereMainnet'; +const CereTestnet: NetworkName = 'cereTestnet'; +const CereDevnet: NetworkName = 'cereDevnet'; +const CereQanet: NetworkName = 'cereQanet'; const cereMainnet = { name: CereMainnet, diff --git a/src/consts.ts b/src/consts.ts index 95c7f0156e..2d045a6da9 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -11,7 +11,7 @@ import type { Plugin } from 'types'; export const AppVersion = '1.0.8'; export const DappName = 'Cere Staking Dashboard'; export const CereUrl = 'https://cere.network'; -export const DefaultNetwork = 'Cere Mainnet'; +export const DefaultNetwork = 'cereMainnet'; export const ManualSigners = ['ledger', 'vault']; /* * Data Structure Helpers diff --git a/src/contexts/Network/index.tsx b/src/contexts/Network/index.tsx index c41711a351..70a72e2e91 100644 --- a/src/contexts/Network/index.tsx +++ b/src/contexts/Network/index.tsx @@ -10,6 +10,8 @@ import type { NetworkState } from 'contexts/Api/types'; import type { NetworkContextInterface } from './types'; import { defaultNetworkContext } from './defaults'; +console.log('Default network context'); +console.log(defaultNetworkContext); export const NetworkProvider = ({ children, }: { @@ -43,9 +45,12 @@ export const NetworkProvider = ({ // handle network switching const switchNetwork = (name: NetworkName) => { + console.warn(`Switching network to name: ${name}`); + console.warn(NetworkList); + setNetwork({ name, - meta: NetworkList[name], + meta: NetworkList.cereMainnet, }); // update url `n` if needed. @@ -54,9 +59,11 @@ export const NetworkProvider = ({ // Store the initial active network. const initialNetwork = getInitialNetwork(); + console.warn('Initial network'); + console.warn(NetworkList); const [network, setNetwork] = useState({ name: initialNetwork, - meta: NetworkList[initialNetwork], + meta: NetworkList.cereMainnet, }); return ( diff --git a/src/modals/Connect/Ledger.tsx b/src/modals/Connect/Ledger.tsx index f1d6dff560..7d43a96d6e 100644 --- a/src/modals/Connect/Ledger.tsx +++ b/src/modals/Connect/Ledger.tsx @@ -44,11 +44,11 @@ export const Ledger = (): React.ReactElement => {
diff --git a/src/types/index.ts b/src/types/index.ts index fa939af016..50e953a60c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -13,10 +13,10 @@ declare global { } export type NetworkName = - | 'Cere Mainnet' - | 'Cere Testnet' - | 'Cere Devnet' - | 'Cere Qanet'; + | 'cereMainnet' + | 'cereTestnet' + | 'cereDevnet' + | 'cereQanet'; export type Networks = Record; From 5b407d07da41907e5d323253e521e1dfd9f57bca Mon Sep 17 00:00:00 2001 From: Raid Ateir Date: Fri, 23 Feb 2024 09:52:59 +0100 Subject: [PATCH 5/6] update favicons & changelog --- CHANGELOG.md | 12 ++++++++++++ public/favicons/kusama/favicon-16x16.png | Bin 524 -> 580 bytes public/favicons/kusama/favicon-32x32.png | Bin 659 -> 1262 bytes public/favicons/polkadot/favicon-16x16.png | Bin 680 -> 580 bytes public/favicons/polkadot/favicon-32x32.png | Bin 1166 -> 1262 bytes public/favicons/westend/favicon-16x16.png | Bin 1487 -> 580 bytes public/favicons/westend/favicon-32x32.png | Bin 2494 -> 1262 bytes 7 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85106ca70a..320888625e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.0] - 2023-02-23 + +## Added + +- Support for substrate node v1.0.0 + +## [1.6.0] - 2024-01-18 + +## Added + +- Support for substrate node v0.9.31-0.9.37 + ## [1.5.0] - 2023-11-27 ## Added diff --git a/public/favicons/kusama/favicon-16x16.png b/public/favicons/kusama/favicon-16x16.png index 9f22c2cf83015df0447e55fb600993bbd8258cd5..1a7034f64c47470e7f89d4194d00d6a320905f03 100644 GIT binary patch delta 566 zcmV-60?GZ11jGc88Gi-<001BJ|6u?C00DDSM?wIu&K&6g00I6y~R?b8&3O zW)XziL%hMe!6@Ffi=={IIfszLOG*2UAJ6;y-uL;QmrzRY=zp4a2;ey()|MlDIXy)z7Q^eky_*5T z?k+;1(9-3vXJ=?Oo6vO~5Bz>4Qz?9?)$ll(L^hMLHt~C}hf=A8;?52diG=CDjS-A| z7@@k}#fpiYP6uy~j<8rP5DW(4bUHB{4k61jGMNnWo13P5SVq|>?)7>Y$TH^hIqq{D zB9Vx7lrOdER3wD_-!cJJvRr3fKLn-!w$8u_>+uMG08{kxQ5e`JTmS$707*qoM6N<$ Ef=g!uj{pDw literal 524 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{XiaP zfk$L91A~|<2s3&HseAwmvX^-Jy0SlK;TJ#4;`*Z&5z(7(Y};kPk~l-e1x8R_`v^(D!^vfoJ^Z_rjt_%4<)GCVDEGJ!_lQ z4s@q#iEBhjN@7W>RdP`(kYX@0Ff!IPFw`}$3^A~@GBLF>G1oRQure@M|MhqtiiX_$ zl+3hB+#2q!sJ{f%5MC7$Q4*9`u24{vpO%@Es!&o{kgAYbP?F5RP%-E6CmxQ%Fb$1U z{-@7)J`G}ER_4}A<`z~K_MR-lEUe(tU~)KxS$T7a!s#1VP8>ONMCJ(l=?0GlUV03# X#05(}IhjrcTEXDy>gTe~DWM4fL?yS4 diff --git a/public/favicons/kusama/favicon-32x32.png b/public/favicons/kusama/favicon-32x32.png index aadbb775c323c1e0cd6e6bcdfe701b9a466b59a4..e87aaaef734ab349907702de0fc9de1586ca6094 100644 GIT binary patch delta 1253 zcmVL_t(oN5xlbNSkLA ze!fKOT{APRTC|F2>oyby-j(X0YrE=xbhQ4Mu8MR@nY7het+?U^tq#0V4QdO5%yvbS z7@7T}e^jte+0^ZL}uiycp?*Up@t$=SPx{_kVonJUQoi&xOOTm^2z& zy|eReP9*vsz~=yL%x6dRZ2+J)0j+wyUgqoT`=0D_^r6*iy~HBXO8{RSkuxDP<2y#< zH&Ur|n$u`Bwx38O!~dgmGq`aVy*`A~>2z5fP$>lYIL=K*V>YMP>HY%n<#9MTfX%-N z0FmK9gfp2;P=Bk{7#tkL_*JGiGOJhv=v%1P2Gh^UN98 z+Sp)oa}zT&GZ=gQ8Uq6Z?3pA!J|3BwnXnRzd4X62Kz|`Guc$yzPY*bb!*|!NAvrl2 zj*gDJ2G`fu@$C6?w6(RtXfz`B!UYr*6tMUA6N>;83I*zAGFVwzF(Wb2(Sjj0EA;mE zqPV1lJ?7-(;PRzQhXb&@yo?`i-elptd*==k5)$?d(AghdT^Ji3#lqquL?RKqy}c0~ z9gVNz;(uUmZH-6ei~oAFy~E1aF3@vOC#<(@>Z*CQo06>Dp2 z`~D+x7!?@_xkABmUtLp!&n(;uUI0p^lCj=kFd#HE6lJBQdwJ~b>jRxrSiQWxvGd^r z#?@+cc6MTMad8jWkGF0iF)`6%+AMNla&i(`*?-wA4Y|3w`1;~Si^d3SV^b4UDiuOP zLhz`pjF)_7LxR_k*xb=Cf0!f`fy@QTm4J_}ayii|PCm1na01BlNEiEnV zzLPOoeLd{$?LoaEFE0=84F=ZvK9x$bv9SU5n>VZvP(8OiVz(vcl#8pR;EX5fK4@e}AUe-QCR^88xBO(o){m%nN|X z^m9%QMn^~CpmB|IPeLily?-B0PELpCz{hXUKcSM`E|+6$KV|P9!>+ z#RSOaXenJeq18-Bs~P3uM3+-DTWCp@NF=v8;CR<_qp?-8x+>##*Yke>ovt$J4SMe4 P00000NkvXXu0mjf8jVij literal 659 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4UEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+10f+@+{-G$+Qd;gjJKptm- zM`SSrgP1A^GkON8d;kivmw5WRvOj0!@`{gMHoKZ zf9}cSlP_@ekao9-rr2ypvF0hRVQ$oDc{$% zUUI(wJ?GWjpa0J7{C=lcf9eXBl}6XiXBub~E&VaQ=&zp$i@(^F>D}2qC%Bpxyoian z+;MI~?0NY;39*}3EwQ+ieET#zUz2g~$?WB4cm8W?PO#HlXZGSkH0S=SQ+u{Yd<*~z z1(!}*)@vF5r;YpD7PXtdo@_5>ca7Y9$+Kd<>`$S(WY66ie9KF2PY`BVzizJ8RPh+e zj^m+LYj=vL7=Ay~R?b8&3O zW)XziL%hMe!6@Ffi=={IIfszLOG*2UAJ6;y-uL;QmrzRY=zp4a2;ey()|MlDIXy)z7Q^eky_*5T z?k+;1(9-3vXJ=?Oo6vO~5Bz>4Qz?9?)$ll(L^hMLHt~C}hf=A8;?52diG=CDjS-A| z7@@k}#fpiYP6uy~j<8rP5DW(4bUHB{4k61jGMNnWo13P5SVq|>?)7>Y$TH^hIqq{D zB9Vx7lrOdER3wD_-!cJJvRr3fKLn-!w$8u_>+uMG08{kxQ5e`JTmS$707*qoM6N<$ Ef{TO$tN;K2 literal 680 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>zS|?@4%%U6N zs`ym(2T%`VlDE5y=Be$iDnJfedff|6tghKf0lKk;xBhG}S=@;`mX^Jx$Rvog0{ zGPkg@u=ivUW?==F29v`n%*vZX6i(l`a^lFDBQi(WPd9ih@X}*=B`#R<$;oso&L_t(oN5xlbNSkLA ze!fKOT{APRTC|F2>oyby-j(X0YrE=xbhQ4Mu8MR@nY7het+?U^tq#0V4QdO5%yvbS z7@7T}e^jte+0^ZL}uiycp?*Up@t$=SPx{_kVonJUQoi&xOOTm^2z& zy|eReP9*vsz~=yL%x6dRZ2+J)0j+wyUgqoT`=0D_^r6*iy~HBXO8{RSkuxDP<2y#< zH&Ur|n$u`Bwx38O!~dgmGq`aVy*`A~>2z5fP$>lYIL=K*V>YMP>HY%n<#9MTfX%-N z0FmK9gfp2;P=Bk{7#tkL_*JGiGOJhv=v%1P2Gh^UN98 z+Sp)oa}zT&GZ=gQ8Uq6Z?3pA!J|3BwnXnRzd4X62Kz|`Guc$yzPY*bb!*|!NAvrl2 zj*gDJ2G`fu@$C6?w6(RtXfz`B!UYr*6tMUA6N>;83I*zAGFVwzF(Wb2(Sjj0EA;mE zqPV1lJ?7-(;PRzQhXb&@yo?`i-elptd*==k5)$?d(AghdT^Ji3#lqquL?RKqy}c0~ z9gVNz;(uUmZH-6ei~oAFy~E1aF3@vOC#<(@>Z*CQo06>Dp2 z`~D+x7!?@_xkABmUtLp!&n(;uUI0p^lCj=kFd#HE6lJBQdwJ~b>jRxrSiQWxvGd^r z#?@+cc6MTMad8jWkGF0iF)`6%+AMNla&i(`*?-wA4Y|3w`1;~Si^d3SV^b4UDiuOP zLhz`pjF)_7LxR_k*xb=Cf0!f`fy@QTm4J_}ayii|PCm1na01BlNEiEnV zzLPOoeLd{$?LoaEFE0=84F=ZvK9x$bv9SU5n>VZvP(8OiVz(vcl#8pR;EX5fK4@e}AUe-QCR^88xBO(o){m%nN|X z^m9%QMn^~CpmB|IPeLily?-B0PELpCz{hXUKcSM`E|+6$KV|P9!>+ z#RSOaXenJeq18-Bs~P3uM3+-DTWCp@NF=v8;CR<_qp?-8x+>##*Yke>ovt$J4SMe4 P00000NkvXXu0mjf6{Jq! literal 1166 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081EXeuPlzi}!83-c#|&jp z8LFN#)I4OUc+ZgkjG_7&L(OxBDxlOT0*1i<|NoP7^rr&@E3qWVFBnKe0e|Aomw#SK zE&txZx{-z3ue(Af^GAm7qQ&eSMfd&|ep@K|Or1$3dCIB{?f>2zn>|*3VcNynDR}&t z+Jhrvs`p*CgfoU&?!IDfA$;|@>xZ_K)$5m(b1h5Q5$Vu<6=(xvlDE4{lJ-tnV01CC zmw5WRvOi;$7S)qAyvw;CC_Ty3#WBRCF^C1VGz0aqqb_6lrbe2qAt)QSaTWi6B zChlMV|F`U2?X=PL%<|oLcB|-DDn2>RF!x@9$;@v&Rk98tY%_F!D{Zdrj$~$xI65DV~{ zD50ClXu>Ge8)j^C8m|5+dA_x&&1(8~ghFx3**h?11Vl2ohYqEsNo zU}Ruqq-$WRYh)f`WN2k#W@TilZD3$!V35oxW{IL9H$NpatrE8ep_p~8Kn>wlArU1( ziRB6fMfqu&IjIUIl?AB^nFS@u3=9=>9)IHDC=AokIOTu(jOWuJ24-b$y<~1-Wnu5h zBFw@HE)6D!Q<#-EhbWxBaplC3Ge=~Ou%B-5Sm33{@Jd{;RG<|Mp00i_>zopr E0Pw5bF#rGn diff --git a/public/favicons/westend/favicon-16x16.png b/public/favicons/westend/favicon-16x16.png index d9421bac17e8df92cef40207b3a94a939bb0b406..1a7034f64c47470e7f89d4194d00d6a320905f03 100644 GIT binary patch delta 566 zcmV-60?GZ)3&aGF8Gi-<001BJ|6u?C00DDSM?wIu&K&6g00I6y~R?b8&3O zW)XziL%hMe!6@Ffi=={IIfszLOG*2UAJ6;y-uL;QmrzRY=zp4a2;ey()|MlDIXy)z7Q^eky_*5T z?k+;1(9-3vXJ=?Oo6vO~5Bz>4Qz?9?)$ll(L^hMLHt~C}hf=A8;?52diG=CDjS-A| z7@@k}#fpiYP6uy~j<8rP5DW(4bUHB{4k61jGMNnWo13P5SVq|>?)7>Y$TH^hIqq{D zB9Vx7lrOdER3wD_-!cJJvRr3fKLn-!w$8u_>+uMG08{kxQ5e`JTmS$707*qoM6N<$ Eg6{kS*#H0l literal 1487 zcmZ`&dtB316u*Rq4p9tKQ_~r0q=Ewk6&NrWu<_W&Hs;tEV=vxggZ;L#CrFk`l&O(s zXlkJe`AW0WEDg#BSmq<21^fNB{eF8wC|QQkZ=e3^56|aw?z!jO^F8ODbI;wvV&sGo z<`Dn@VRRamjmz)=PYK4?sOT9STuiE<6i@&-u{Cr|G8xy2G8(%80OK+Mnl}RQ1UEHf z0BA`7xXS=Y+6BNIb;p^)6ab#P$jawt;SS!TsV)s4;Xp7?=c33Q;KY4an~iBi7iu3* zfc^=cW46(qBt=%5{Kjk>&FnK~BV}qYsIgJaUR}XxD`^O9ax65u)kPRbhOnW57LD7; z#p;EqSmq?xIf`rSf;xoPfUvdhYOYIBj0ww7TBTi}LW&w-mKH8(bkjwyYza&=zzIfA zuE0$bA*)Qta`W(pWWRw5$C?N8n}?goLlz3W#$uy14O_{6J=Ktv2eiPMapxd%dwhW6`pXbpskrV@* zYVzn=E>Q_47NcB+y;y}5YLHwVlGcbZWmruKDwjHQYVE>0n5PX?B&=~2i?C8Lnpf#e zu5)mdfm*m4n4xn~B`#hm#*w0#)y@Q!gRHjG>g|~jlBP#i8+_Vax0L4+h%k{DWtThC zln$oS&Q~Fv2Dm^6lMQgX$!DN>8dxrY7!!)oB88KpatM?NA42GQm>RgMPm||SGltvS zMmi2+CH0REo&V#=DGb^;dZFK4yVFL~!$~HeBHvraar1dYo7=`b{$Xq9Q*7iP1Rd@; zGUgeFrwKfV%{SHgA+*LziUV<6=<+v-kE;VPQbI+3ph__{j zfBS(aHoIr@UYG#|-Z#jyI7G~+-JcEs5a`8ndU4hCa zPIi@pr9^mj_?);Z_-IxIDv z2nzF^U)73Z=F_PZ?%W$e;Xy;=ff&!dyl@e5@hg!_f+yiBnEd?GWv@n+SG=}-#mbN= zcvwPoOl4JdO|7y{wTcuv4TQzUsp}gwkXEN}G{mo-4iXZLCbPxb)V!hP^^I?=Sqs)B zy}4=gme#l0lyASY_1)wYKu+DZeaCz4@9*68!R`+`($WEC&)$#r?LYAGC!GfmeR{Yn z17vo8cI4=><0n2pdFqSPUw%aeS=nEoIs47IZ@)Wt{=)Y?y*YqJ|KZ1rmo8uF>%aQb z&)0JEKz@Pom+Lof-um_SoxAt$Gnilot8mkU-+mt$w83}pu{q&j<_p{+#O`pSn9I#u zA2ADn7iaT}1wxOvL{uuCGdED~OY_PkQrTj8q+<%}c%gy$gCn`kHC`a$|sd&P-?|BcUBn0q9u_>d}m{mVW_=q@@A? diff --git a/public/favicons/westend/favicon-32x32.png b/public/favicons/westend/favicon-32x32.png index 85e595b0dc556be1bc10bae98d9eef7dcb454681..e87aaaef734ab349907702de0fc9de1586ca6094 100644 GIT binary patch delta 1242 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 Date: Mon, 26 Feb 2024 10:14:42 +0100 Subject: [PATCH 6/6] fix version, community and total staked functionality --- package.json | 2 +- src/config/validators/index.ts | 214 ++++++++++++++++++ src/contexts/NetworkMetrics/defaults.ts | 2 +- src/contexts/NetworkMetrics/index.tsx | 5 +- src/contexts/NetworkMetrics/types.ts | 2 +- .../Validators/ValidatorEntries/index.tsx | 2 +- src/pages/Community/List.tsx | 2 + src/pages/Overview/Stats/SupplyStaked.tsx | 2 + 8 files changed, 224 insertions(+), 7 deletions(-) create mode 100644 src/config/validators/index.ts diff --git a/package.json b/package.json index d64da79073..112f08cc98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "polkadot-staking-dashboard", - "version": "0.1.0", + "version": "1.7.0", "type": "module", "license": "GPL-3.0-only", "private": false, diff --git a/src/config/validators/index.ts b/src/config/validators/index.ts new file mode 100644 index 0000000000..3aa8dba661 --- /dev/null +++ b/src/config/validators/index.ts @@ -0,0 +1,214 @@ +// Copyright 2022 @paritytech/polkadot-staking-dashboard authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/* Import your SVG Here. + * Use upper camel-case for your SVG import, lower camel case for the svg. + * import ValidatorEntityName from './thumbnails/validatorEntityName.svg'; + */ +import Thumbnail4T2CAPITAL from './thumbnails/4t2.svg'; +import AnyValid from './thumbnails/anyvalid.svg'; +import Brightlystake from './thumbnails/Brightlystake-logo.svg'; +import Cere from './thumbnails/cere.svg'; +import EdgeServices from './thumbnails/edgeservices.svg'; +import garm99 from './thumbnails/garm99.svg'; +import Jinogami from './thumbnails/Jinogami.svg'; +import medium from './thumbnails/medium.svg'; +import SerGo from './thumbnails/SerGo.svg'; +import StakeAngle from './thumbnails/stakeangle.svg'; +import Testnetrun from './thumbnails/Testnetrun.svg'; +import Tokem from './thumbnails/tomek.svg'; +import TRK from './thumbnails/TRK.svg'; +import wombat from './thumbnails/wombat.svg'; +import XameyzIdentity from './thumbnails/xameyz.svg'; + +export const ValidatorCommunity = [ + { + name: 'CERE', + Thumbnail: Cere, + thumbnail: 'Cere', + bio: `Official Validators from Cere Network, the world's first Decentralized Data Cloud platform.`, + email: 'team@cere.network', + website: 'https://cere.network', + twitter: '@CereNetwork', + validators: { + cere: [ + '6S4mrsCrqWoBAYrp2PKQNh7CYcCtyEtYpx5J626Kj5vszSyy', + '6QhzyvZQm3dLjDmeaoUnLPXzfuTi6X1HEo6AX6gfVbC3shzD', + '6RgfwDiQTLjgbkQ5CorrKtRtCaDABQKYsibk9MeyvzmKFrk2', + '6TBhZAgtFc3Wr8BeNu5tdMJG1NDpxKbG2Hwf2UbVtMGyFxzN', + '6Pyh9zZgp4XCP338VDG7oshK7PvsAdyuBN6S2NNm7CBoCXx8', + '6S9tXQmPYoeBXYey8vKYi9BMbNMD8Zgqb62k7SYMNQLUbydZ', + '6PwAv2L43zGPEwHTb1L7LyCWv7yq2Hc4dSVYHvvi1kscCR91', + '6Qshjra42mLDtc9ouHzUz1bMmYXg2qasmW2xSLgendRdsYED', + ], + }, + }, + { + name: 'Xameyz', + Thumbnail: XameyzIdentity, + thumbnail: 'Xameyz', + bio: `Just a humble validator on a humble decentralized network.`, + email: 'xameyz.crypto@yahoo.com', + website: '', + twitter: '@xameyz', + validators: { + cere: ['6TYC5go4hQ85NxmGK8c658cmJozxeohKfp6YbDGC5r1HJ6nZ'], + }, + }, + { + name: 'AnyValid', + Thumbnail: AnyValid, + thumbnail: 'AnyValid', + bio: `Professional Proof-of-Stake Networks Validation Services`, + email: 'mail@anyvalid.com', + website: 'https://anyvalid.com', + twitter: '@anyvalid', + validators: { + cere: ['6UDVCKB9opndqcRAxTpTvKFTFXUwvE36aYnp1bNyVV3Cfh16'], + }, + }, + { + name: 'SerGo', + Thumbnail: SerGo, + thumbnail: 'SerGo', + bio: `We validate with expertise, you earn with confidence.`, + email: 'contact@sergo.dev', + website: 'https://sergo.dev', + twitter: '', + validators: { + cere: ['6SpjH8swCtFwmntQmdikMWyDKgr59q1cLauePYc2iqwwe6Bv'], + }, + }, + { + name: 'StakeAngle', + Thumbnail: StakeAngle, + thumbnail: 'StakeAngle', + bio: `Non-custodial staking provider`, + email: 'info@stakeangle.com', + website: 'https://stakeangle.com', + twitter: '', + validators: { + cere: ['6T9794B5HPPNYqds6VNAxcFjYZHzpgbZfdWXSiE6q65eLpVm'], + }, + }, + { + name: 'medium', + Thumbnail: medium, + thumbnail: 'medium', + bio: `Individual staking services`, + email: 'hsonelove228@gmail.com', + website: 'https://github.com/mediumwe11', + twitter: '', + validators: { + cere: ['6TsKhZ5o9BFAG1bCY2HVriUXAuQMae744APMMcVgYqYhNJRy'], + }, + }, + { + name: 'Jinogami', + Thumbnail: Jinogami, + thumbnail: 'Jinogami', + bio: `A Community Cere Validator`, + email: 'steganosgraphos@gmail.com', + website: '', + twitter: '@steganosgraphos', + validators: { + cere: ['6RvHuBMLqBVcwvk62qto81m8DFPoKBq86WCAmVEwHH3rTuNQ'], + }, + }, + { + name: 'EdgeServices', + Thumbnail: EdgeServices, + thumbnail: 'EdgeServices', + bio: `Blockchain Nodes & Services`, + email: 'contact@edgeservices.io', + website: 'https://edgeservices.io', + twitter: '@EdgeServicesIO', + validators: { + cere: ['6RByFsuHYQET5V78TqaRuyeF8XQRKPuZhkE9admxL48VUEey'], + }, + }, + { + name: 'wombat', + Thumbnail: wombat, + thumbnail: 'wombat', + bio: `Professional blockchain validator`, + email: 'sdidenko566@yandex.ru', + website: 'https://github.com/wombatqq', + twitter: '', + validators: { + cere: ['6Pqj4UwFRN4mmu25PH8RqWaMT4jNd6ytFNgaJ4KtVTfgByez'], + }, + }, + { + name: 'Brightlystake', + Thumbnail: Brightlystake, + thumbnail: 'Brightlystake', + bio: `Cere node from Brightlystake. Contact us for any queries https://linktr.ee/brightlystake`, + email: 'contact@brightlystake.com', + website: 'https://brightlystake.com/', + twitter: '@brightlystake', + validators: { + cere: ['6TnQVHWvDtw5W5vqEjNcewypHagW9N5VtvDTDUNWq3drqZTB'], + }, + }, + { + name: 'garm99', + Thumbnail: garm99, + thumbnail: 'garm99', + bio: `Staking Provider of Proof-of-Stake Networks`, + email: 'info@nodeskeeper.com', + website: 'nodeskeeper.com', + twitter: '@GARM799', + validators: { + cere: ['6RLGWVUzwXBvjuyLHY7Hr95TqgdUV6UxboG9i3xZnVp1vVWk'], + }, + }, + { + name: '4T2.CAPITAL', + Thumbnail: Thumbnail4T2CAPITAL, + thumbnail: '4T2.CAPITAL', + bio: `valid thumbnail: 'XameyzIdentity',ating with love & care | secure and seamless staking experience | based in 🇳🇴`, + email: '4t2@4t2.capital', + website: 'https://4t2.capital', + twitter: '', + validators: { + cere: ['6VB5dkmPn6zpti4BaEZp2y7Ht8kaj8ELKGAHzpThowzXU66A'], + }, + }, + { + name: 'TRK', + Thumbnail: TRK, + thumbnail: 'TRK', + bio: `Validating...`, + email: '', + website: '', + twitter: '', + validators: { + cere: ['6TBNtFjPELfrzSa2sXYyTWhbP1omhjxhF5nk6jtR51S3pfrS'], + }, + }, + { + name: 'Testnetrun', + Thumbnail: Testnetrun, + thumbnail: 'Testnetrun', + bio: `Position yourself for the blockchain-powered future of the next decade by staking today, ensuring you're part of the fastest and most secure validator network in the space.`, + email: 'info@testnet.run', + website: 'https://stake.testnet.run', + twitter: '@testnetrun', + validators: { + cere: ['6QPgrdDzaMqj54YcHm1XpyqN8z9DTZ9sySXwF7uFwfUADkiL'], + }, + }, + { + name: 'TomekNode', + Thumbnail: Tokem, + thumbnail: 'Tokem', + bio: `Experienced and dedicated, I pride myself on being a reliable community validator node maintainer, ensuring optimal performance and trust for our nominators`, + email: 'cere.e69cg@passfwd.com', + website: '', + twitter: '', + validators: { + cere: ['6PbuJRgBSikmBNajCa75Zq9PqXmaYcCZ6e5QKk7sUyeebBDU'], + }, + }, +]; diff --git a/src/contexts/NetworkMetrics/defaults.ts b/src/contexts/NetworkMetrics/defaults.ts index 02af14f760..cc036ba693 100644 --- a/src/contexts/NetworkMetrics/defaults.ts +++ b/src/contexts/NetworkMetrics/defaults.ts @@ -14,7 +14,7 @@ export const activeEra: ActiveEra = { }; export const metrics: NetworkMetrics = { totalIssuance: new BigNumber(0), - auctionCounter: new BigNumber(0), + // auctionCounter: new BigNumber(0), earliestStoredSession: new BigNumber(0), fastUnstakeErasToCheckPerBlock: 0, minimumActiveStake: new BigNumber(0), diff --git a/src/contexts/NetworkMetrics/index.tsx b/src/contexts/NetworkMetrics/index.tsx index 725c3f24a0..58ea7cdc58 100644 --- a/src/contexts/NetworkMetrics/index.tsx +++ b/src/contexts/NetworkMetrics/index.tsx @@ -43,14 +43,13 @@ export const NetworkMetricsProvider = ({ const unsub = await api.queryMulti( [ api.query.balances.totalIssuance, - api.query.auctions.auctionCounter, + // api.query.auctions.auctionCounter, api.query.paraSessionInfo.earliestStoredSession, api.query.fastUnstake.erasToCheckPerBlock, api.query.staking.minimumActiveStake, ], ([ totalIssuance, - auctionCounter, earliestStoredSession, erasToCheckPerBlock, minimumActiveStake, @@ -58,7 +57,7 @@ export const NetworkMetricsProvider = ({ setStateWithRef( { totalIssuance: new BigNumber(totalIssuance.toString()), - auctionCounter: new BigNumber(auctionCounter.toString()), + // auctionCounter: new BigNumber('0'.toString()), earliestStoredSession: new BigNumber( earliestStoredSession.toString() ), diff --git a/src/contexts/NetworkMetrics/types.ts b/src/contexts/NetworkMetrics/types.ts index b3c5dd7e4b..6cb64fe838 100644 --- a/src/contexts/NetworkMetrics/types.ts +++ b/src/contexts/NetworkMetrics/types.ts @@ -10,7 +10,7 @@ export interface NetworkMetricsContextInterface { export interface NetworkMetrics { totalIssuance: BigNumber; - auctionCounter: BigNumber; + // auctionCounter: BigNumber; earliestStoredSession: BigNumber; fastUnstakeErasToCheckPerBlock: number; minimumActiveStake: BigNumber; diff --git a/src/contexts/Validators/ValidatorEntries/index.tsx b/src/contexts/Validators/ValidatorEntries/index.tsx index 38a8918770..5e30d33902 100644 --- a/src/contexts/Validators/ValidatorEntries/index.tsx +++ b/src/contexts/Validators/ValidatorEntries/index.tsx @@ -4,7 +4,7 @@ import { greaterThanZero, rmCommas, shuffle } from '@polkadot-cloud/utils'; import BigNumber from 'bignumber.js'; import React, { useEffect, useRef, useState } from 'react'; -import { ValidatorCommunity } from '@polkadot-cloud/assets/validators'; +import { ValidatorCommunity } from 'config/validators'; import type { AnyApi, AnyJson, BondFor, Fn, Sync } from 'types'; import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks'; import { useBonded } from 'contexts/Bonded'; diff --git a/src/pages/Community/List.tsx b/src/pages/Community/List.tsx index 98c285dd1b..ac5d859bb3 100644 --- a/src/pages/Community/List.tsx +++ b/src/pages/Community/List.tsx @@ -18,6 +18,8 @@ export const List = () => { validatorCommunity.filter((v) => v.validators[network] !== undefined) ); + console.warn(`All validators: ${validatorCommunity}`); + useEffect(() => { setEntityItems( validatorCommunity.filter((v) => v.validators[network] !== undefined) diff --git a/src/pages/Overview/Stats/SupplyStaked.tsx b/src/pages/Overview/Stats/SupplyStaked.tsx index b72ccacb06..e23c67bc51 100644 --- a/src/pages/Overview/Stats/SupplyStaked.tsx +++ b/src/pages/Overview/Stats/SupplyStaked.tsx @@ -18,6 +18,8 @@ export const SupplyStakedStat = () => { const { lastTotalStake } = staking; const { totalIssuance } = metrics; + console.warn(`Total Issuance: ${totalIssuance}`); + console.warn(`Total Stake: ${lastTotalStake}`); // total supply as percent. const totalIssuanceUnit = planckToUnit(totalIssuance, units); const lastTotalStakeUnit = planckToUnit(lastTotalStake, units);