From 8c804e849a13e03d50d144e76ec882a7888cd80f Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Fri, 19 Apr 2024 17:00:12 +0200 Subject: [PATCH] Release 2.0.0 (#154) Co-authored-by: Adnan Khan Co-authored-by: Aaron Cook Co-authored-by: James Mealy Co-authored-by: Usame Algan --- .eslintrc.json | 5 +- .github/workflows/cla.yml | 2 +- .github/workflows/tag-release.yml | 4 +- .gitignore | 4 +- LICENSE | 661 +++++++++++++++ jest.config.js | 5 +- jest.setup.js | 4 + package.json | 14 +- pages/_app.tsx | 17 +- pages/_document.tsx | 2 + pages/activity-program.tsx | 9 + pages/activity.tsx | 9 + pages/claim.tsx | 15 +- pages/governance.tsx | 9 + pages/index.tsx | 10 +- pages/safedao.tsx | 9 - pages/splash.tsx | 9 + pages/unlock.tsx | 9 + public/images/assets-stored.png | Bin 0 -> 29284 bytes public/images/asterix.svg | 9 + public/images/barcode.svg | 28 + public/images/clock-alt.svg | 4 + public/images/clock.svg | 4 +- public/images/diamond.png | Bin 0 -> 53493 bytes public/images/empty-activity.png | Bin 0 -> 3682 bytes public/images/empty-breakdown.svg | 4 + public/images/leaderboard-first-place.svg | 3 + public/images/leaderboard-second-place.svg | 3 + public/images/leaderboard-third-place.svg | 3 + public/images/leaderboard-title-star.svg | 3 + public/images/safe-logo-round.svg | 6 + public/images/safe-pass.svg | 12 + public/images/star.svg | 3 + public/images/transaction-number.png | Bin 0 -> 23922 bytes public/images/transaction-volume.png | Bin 0 -> 23403 bytes public/images/user-bust.png | Bin 0 -> 13555 bytes public/images/weekly-user.png | Bin 0 -> 20391 bytes public/manifest.json | 6 +- src/analytics/lockEvents.ts | 31 + src/analytics/navigation.ts | 23 + src/components/AccordionContainer/index.tsx | 24 + .../AccordionContainer/styles.module.css | 12 + src/components/AccountCenter/index.tsx | 6 + src/components/Activities/index.tsx | 125 +++ src/components/BackgroundCircles/index.tsx | 22 +- .../BackgroundCircles/styles.module.css | 33 +- src/components/BoostCounter/index.tsx | 109 +++ src/components/BoostCounter/styles.module.css | 0 .../index.tsx => SuccessfulClaim.tsx} | 21 +- src/components/Claim/index.tsx | 268 +++++- .../Claim/steps/ClaimOverview/index.tsx | 236 ------ .../ClaimOverview => }/styles.module.css | 0 src/components/ClaimCard/index.tsx | 37 +- src/components/ClaimCard/styles.module.css | 14 - src/components/ConnectWallet/index.tsx | 63 -- .../ConnectWallet/styles.module.css | 38 + .../DashboardWidgets/ClaimingWidget.tsx | 8 +- src/components/Delegation/index.tsx | 9 +- src/components/EducationSeries/index.tsx | 38 - .../steps/Disclaimer/index.tsx | 47 -- .../steps/Distribution/index.tsx | 150 ---- .../steps/Distribution/styles.module.css | 4 - .../EducationSeries/steps/SafeDao/index.tsx | 76 -- .../steps/SafeDao/styles.module.css | 6 - .../EducationSeries/steps/SafeInfo/index.tsx | 72 -- .../EducationSeries/steps/SafeToken/index.tsx | 71 -- .../steps/SafeToken/styles.module.css | 57 -- .../EnsureWalletConnection/index.tsx | 6 +- src/components/ExternalLink/index.tsx | 40 +- src/components/FloatingTiles/index.tsx | 164 ---- .../FloatingTiles/styles.module.css | 20 - .../index.tsx | 86 +- .../GovernanceAndClaiming/styles.module.css | 5 + src/components/Header/index.tsx | 2 + src/components/Header/styles.module.css | 4 + src/components/InfoAlert/index.tsx | 2 +- src/components/Intro/styles.module.css | 8 - src/components/MediumPaper/index.tsx | 8 + src/components/MediumPaper/styles.module.css | 12 + src/components/NavTabs/index.tsx | 72 ++ src/components/NavTabs/styles.module.css | 49 ++ src/components/OverviewLinks/index.tsx | 32 +- src/components/PageLayout/index.tsx | 52 +- src/components/PageLayout/styles.module.css | 39 +- src/components/PaperContainer/index.tsx | 12 + .../PaperContainer/styles.module.css | 6 + src/components/Redirect/index.tsx | 14 +- src/components/SelectedDelegate/index.tsx | 4 +- src/components/SplashScreen/index.tsx | 163 ++++ src/components/SplashScreen/styles.module.css | 55 ++ src/components/StepHeader/index.tsx | 2 +- src/components/TestChainSwitch/index.tsx | 9 +- src/components/TestDateSelect/index.tsx | 34 + .../TockenUnlocking/UnlockStats.tsx | 24 + .../TockenUnlocking/UnlockTokenWidget.tsx | 195 +++++ .../TockenUnlocking/WithdrawWidget.tsx | 107 +++ src/components/TockenUnlocking/index.tsx | 56 ++ .../TockenUnlocking/styles.module.css | 52 ++ src/components/TokenAmount/index.tsx | 45 + src/components/TokenAmount/styles.module.css | 12 + .../TokenLocking/ActivityRewardsInfo.tsx | 100 +++ .../TokenLocking/BoostBreakdown.tsx | 101 +++ .../ActionNavigation/ActionNavigation.tsx | 38 + .../ActionNavigation/styles.module.css | 16 + .../BoostGraph/ArrowDownLabel.tsx | 30 + .../TokenLocking/BoostGraph/AxisTopLabel.tsx | 21 + .../BoostGraph/BoostGradients.tsx | 48 ++ .../TokenLocking/BoostGraph/BoostGraph.tsx | 229 +++++ .../TokenLocking/BoostGraph/ScatterDot.tsx | 89 ++ .../TokenLocking/BoostGraph/graphConstants.ts | 3 + .../TokenLocking/BoostGraph/helper.ts | 15 + .../TokenLocking/BoostGraph/theme.ts | 194 +++++ src/components/TokenLocking/BoostMeter.tsx | 99 +++ src/components/TokenLocking/CurrentStats.tsx | 25 + src/components/TokenLocking/Leaderboard.tsx | 281 +++++++ .../TokenLocking/LockTokenWidget.tsx | 238 ++++++ src/components/TokenLocking/MilesReceipt.tsx | 102 +++ src/components/TokenLocking/index.tsx | 73 ++ src/components/TokenLocking/styles.module.css | 278 ++++++ src/components/TotalVotingPower/index.tsx | 4 +- src/components/Track/index.tsx | 51 ++ src/components/WalletIcon/index.tsx | 22 +- src/components/WhatIsBoost/index.tsx | 98 +++ src/components/WhatIsBoost/styles.module.css | 18 + src/config/constants.ts | 47 +- src/config/routes.ts | 6 +- src/hooks/__tests__/useAmounts.test.ts | 2 +- .../__tests__/useContractDelegate.test.ts | 10 +- src/hooks/__tests__/useEnsResolution.test.ts | 14 +- src/hooks/__tests__/useIsTokenPaused.test.ts | 8 +- .../__tests__/useSafeTokenAllocation.test.ts | 52 +- .../useSummarizedLockHistory.test.ts | 122 +++ src/hooks/useAddress.ts | 2 +- src/hooks/useChainId.ts | 2 +- src/hooks/useDebounce.ts | 16 + src/hooks/useEnsLookup.ts | 15 + src/hooks/useEnsResolution.ts | 2 +- src/hooks/useGatewayBaseUrl.ts | 8 + src/hooks/useIsDarkMode.ts | 6 - src/hooks/useIsSafeApp.ts | 17 +- src/hooks/useLeaderboard.ts | 72 ++ src/hooks/useLockHistory.ts | 100 +++ src/hooks/useSafeTokenAllocation.ts | 36 +- src/hooks/useSafeTokenBalance.ts | 47 ++ src/hooks/useStartDates.ts | 15 + src/hooks/useSummarizedLockHistory.ts | 73 ++ src/hooks/useTxSender.ts | 42 + src/hooks/useWallet.ts | 9 +- src/hooks/useWeb3.ts | 2 +- src/styles/colors-dark.ts | 2 +- src/styles/theme.ts | 5 + src/tests/test-utils.tsx | 2 +- src/utils/__tests__/boost.test.ts | 247 ++++++ src/utils/__tests__/claim.test.ts | 12 +- src/utils/__tests__/date.test.ts | 90 ++ src/utils/__tests__/lock.test.ts | 39 + src/utils/analytics.ts | 13 + src/utils/boost.ts | 68 ++ src/utils/claim.ts | 2 +- src/utils/date.ts | 67 ++ src/utils/gateway.ts | 4 + src/utils/lock.ts | 96 +++ src/utils/onboard.ts | 66 +- src/utils/safe-apps.ts | 2 +- src/utils/safe-token.ts | 59 ++ src/utils/wallet.ts | 27 - src/utils/web3.ts | 4 +- yarn.lock | 788 +++++++++++++++++- 168 files changed, 6812 insertions(+), 1555 deletions(-) create mode 100644 LICENSE create mode 100644 pages/activity-program.tsx create mode 100644 pages/activity.tsx create mode 100644 pages/governance.tsx delete mode 100644 pages/safedao.tsx create mode 100644 pages/splash.tsx create mode 100644 pages/unlock.tsx create mode 100644 public/images/assets-stored.png create mode 100644 public/images/asterix.svg create mode 100644 public/images/barcode.svg create mode 100644 public/images/clock-alt.svg create mode 100644 public/images/diamond.png create mode 100644 public/images/empty-activity.png create mode 100644 public/images/empty-breakdown.svg create mode 100644 public/images/leaderboard-first-place.svg create mode 100644 public/images/leaderboard-second-place.svg create mode 100644 public/images/leaderboard-third-place.svg create mode 100644 public/images/leaderboard-title-star.svg create mode 100644 public/images/safe-logo-round.svg create mode 100644 public/images/safe-pass.svg create mode 100644 public/images/star.svg create mode 100644 public/images/transaction-number.png create mode 100644 public/images/transaction-volume.png create mode 100644 public/images/user-bust.png create mode 100644 public/images/weekly-user.png create mode 100644 src/analytics/lockEvents.ts create mode 100644 src/analytics/navigation.ts create mode 100644 src/components/AccordionContainer/index.tsx create mode 100644 src/components/AccordionContainer/styles.module.css create mode 100644 src/components/Activities/index.tsx create mode 100644 src/components/BoostCounter/index.tsx create mode 100644 src/components/BoostCounter/styles.module.css rename src/components/Claim/{steps/SuccessfulClaim/index.tsx => SuccessfulClaim.tsx} (53%) delete mode 100644 src/components/Claim/steps/ClaimOverview/index.tsx rename src/components/Claim/{steps/ClaimOverview => }/styles.module.css (100%) delete mode 100644 src/components/ConnectWallet/index.tsx create mode 100644 src/components/ConnectWallet/styles.module.css delete mode 100644 src/components/EducationSeries/index.tsx delete mode 100644 src/components/EducationSeries/steps/Disclaimer/index.tsx delete mode 100644 src/components/EducationSeries/steps/Distribution/index.tsx delete mode 100644 src/components/EducationSeries/steps/Distribution/styles.module.css delete mode 100644 src/components/EducationSeries/steps/SafeDao/index.tsx delete mode 100644 src/components/EducationSeries/steps/SafeDao/styles.module.css delete mode 100644 src/components/EducationSeries/steps/SafeInfo/index.tsx delete mode 100644 src/components/EducationSeries/steps/SafeToken/index.tsx delete mode 100644 src/components/EducationSeries/steps/SafeToken/styles.module.css delete mode 100644 src/components/FloatingTiles/index.tsx delete mode 100644 src/components/FloatingTiles/styles.module.css rename src/components/{Intro => GovernanceAndClaiming}/index.tsx (50%) create mode 100644 src/components/GovernanceAndClaiming/styles.module.css delete mode 100644 src/components/Intro/styles.module.css create mode 100644 src/components/MediumPaper/index.tsx create mode 100644 src/components/MediumPaper/styles.module.css create mode 100644 src/components/NavTabs/index.tsx create mode 100644 src/components/NavTabs/styles.module.css create mode 100644 src/components/PaperContainer/index.tsx create mode 100644 src/components/PaperContainer/styles.module.css create mode 100644 src/components/SplashScreen/index.tsx create mode 100644 src/components/SplashScreen/styles.module.css create mode 100644 src/components/TestDateSelect/index.tsx create mode 100644 src/components/TockenUnlocking/UnlockStats.tsx create mode 100644 src/components/TockenUnlocking/UnlockTokenWidget.tsx create mode 100644 src/components/TockenUnlocking/WithdrawWidget.tsx create mode 100644 src/components/TockenUnlocking/index.tsx create mode 100644 src/components/TockenUnlocking/styles.module.css create mode 100644 src/components/TokenAmount/index.tsx create mode 100644 src/components/TokenAmount/styles.module.css create mode 100644 src/components/TokenLocking/ActivityRewardsInfo.tsx create mode 100644 src/components/TokenLocking/BoostBreakdown.tsx create mode 100644 src/components/TokenLocking/BoostGraph/ActionNavigation/ActionNavigation.tsx create mode 100644 src/components/TokenLocking/BoostGraph/ActionNavigation/styles.module.css create mode 100644 src/components/TokenLocking/BoostGraph/ArrowDownLabel.tsx create mode 100644 src/components/TokenLocking/BoostGraph/AxisTopLabel.tsx create mode 100644 src/components/TokenLocking/BoostGraph/BoostGradients.tsx create mode 100644 src/components/TokenLocking/BoostGraph/BoostGraph.tsx create mode 100644 src/components/TokenLocking/BoostGraph/ScatterDot.tsx create mode 100644 src/components/TokenLocking/BoostGraph/graphConstants.ts create mode 100644 src/components/TokenLocking/BoostGraph/helper.ts create mode 100644 src/components/TokenLocking/BoostGraph/theme.ts create mode 100644 src/components/TokenLocking/BoostMeter.tsx create mode 100644 src/components/TokenLocking/CurrentStats.tsx create mode 100644 src/components/TokenLocking/Leaderboard.tsx create mode 100644 src/components/TokenLocking/LockTokenWidget.tsx create mode 100644 src/components/TokenLocking/MilesReceipt.tsx create mode 100644 src/components/TokenLocking/index.tsx create mode 100644 src/components/TokenLocking/styles.module.css create mode 100644 src/components/Track/index.tsx create mode 100644 src/components/WhatIsBoost/index.tsx create mode 100644 src/components/WhatIsBoost/styles.module.css create mode 100644 src/hooks/__tests__/useSummarizedLockHistory.test.ts create mode 100644 src/hooks/useDebounce.ts create mode 100644 src/hooks/useEnsLookup.ts create mode 100644 src/hooks/useGatewayBaseUrl.ts delete mode 100644 src/hooks/useIsDarkMode.ts create mode 100644 src/hooks/useLeaderboard.ts create mode 100644 src/hooks/useLockHistory.ts create mode 100644 src/hooks/useSafeTokenBalance.ts create mode 100644 src/hooks/useStartDates.ts create mode 100644 src/hooks/useSummarizedLockHistory.ts create mode 100644 src/hooks/useTxSender.ts create mode 100644 src/utils/__tests__/boost.test.ts create mode 100644 src/utils/__tests__/date.test.ts create mode 100644 src/utils/__tests__/lock.test.ts create mode 100644 src/utils/analytics.ts create mode 100644 src/utils/boost.ts create mode 100644 src/utils/date.ts create mode 100644 src/utils/lock.ts create mode 100644 src/utils/safe-token.ts diff --git a/.eslintrc.json b/.eslintrc.json index 7b21d659..b7b9ac74 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,9 +1,10 @@ { "extends": ["next/core-web-vitals", "prettier"], - "plugins": ["prettier", "@typescript-eslint"], + "plugins": ["unused-imports", "prettier", "@typescript-eslint"], "rules": { "@next/next/no-img-element": "off", - "no-constant-condition": "warn" + "no-constant-condition": "warn", + "unused-imports/no-unused-imports-ts": "error" }, "ignorePatterns": ["node_modules/", ".next/", ".github/", "src/types/contracts"] } diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml index 4dc448c6..0f6b86a0 100644 --- a/.github/workflows/cla.yml +++ b/.github/workflows/cla.yml @@ -24,7 +24,7 @@ jobs: # branch should not be protected branch: 'main' # user names of users allowed to contribute without CLA - allowlist: lukasschor,mikheevm,rmeissner,germartinez,Uxio0,dasanra,francovenica,tschubotz,luarx,bot*,DaniSomoza,iamacook,yagopv,usame-algan,schmanu,DiogoSoaress,JagoFigueroa + allowlist: lukasschor,mikheevm,rmeissner,germartinez,Uxio0,dasanra,francovenica,tschubotz,luarx,bot*,DaniSomoza,iamacook,yagopv,usame-algan,schmanu,DiogoSoaress,JagoFigueroa,jmealy # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken # enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 2c6aeaa7..2cd7e5da 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -19,10 +19,12 @@ jobs: - name: Extract version if: github.event.pull_request.merged == true id: version + env: + PR_BODY: ${{ github.event.pull_request.body }} run: | NEW_VERSION=$(node -p 'require("./package.json").version') echo "version=v$NEW_VERSION" >> $GITHUB_OUTPUT - echo "${{ github.event.pull_request.body }}" > CHANGELOG.md + echo "$PR_BODY" > CHANGELOG.md - name: Create a git tag if: github.event.pull_request.merged == true diff --git a/.gitignore b/.gitignore index c0d2ec8d..7e820010 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ # misc .DS_Store *.pem +.idea # debug npm-debug.log* @@ -27,7 +28,8 @@ yarn-error.log* .pnpm-debug.log* # local env files -.env*.local +.env +*.local # vercel .vercel diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..0ad25db4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 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 Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero 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 Affero 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/jest.config.js b/jest.config.js index 665b8bcd..6e88b8fb 100644 --- a/jest.config.js +++ b/jest.config.js @@ -15,4 +15,7 @@ const customJestConfig = { testEnvironment: 'jest-environment-jsdom', } -module.exports = createJestConfig(customJestConfig) +module.exports = async () => ({ + ...(await createJestConfig(customJestConfig)()), + transformIgnorePatterns: ['node_modules/(?!(isows)/)'], +}) diff --git a/jest.setup.js b/jest.setup.js index e643574f..8af8c3ed 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -43,3 +43,7 @@ jest.mock('@web3-onboard/core', () => () => ({ get: () => mockOnboardState, }, })) + +const { TextEncoder, TextDecoder } = require('util') +global.TextEncoder = TextEncoder +global.TextDecoder = TextDecoder diff --git a/package.json b/package.json index 3222d9c1..12dc5bb4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "safe-dao-governance-app", "homepage": "https://github.com/safe-global/safe-dao-governance-app", "license": "GPL-3.0", - "version": "1.2.2", + "version": "2.0.0", "scripts": { "build": "next build && next export", "lint": "tsc && next lint", @@ -24,10 +24,11 @@ "@emotion/react": "^11.10.5", "@emotion/server": "^11.10.0", "@emotion/styled": "^11.10.5", - "@gnosis.pm/safe-apps-provider": "^0.15.1", - "@gnosis.pm/safe-apps-react-sdk": "^4.6.2", - "@mui/icons-material": "^5.11.0", + "@mui/icons-material": "^5.15.15", "@mui/material": "^5.11.5", + "@mui/x-date-pickers": "^7.1.1", + "@safe-global/safe-apps-provider": "^0.18.2", + "@safe-global/safe-apps-react-sdk": "^4.7.1", "@safe-global/safe-gateway-typescript-sdk": "^3.5.6", "@web3-onboard/coinbase": "^2.2.4", "@web3-onboard/core": "2.20.4", @@ -40,12 +41,14 @@ "bezier-easing": "^2.1.0", "clsx": "^1.2.1", "date-fns": "^2.29.3", + "dayjs": "^1.11.10", "ethereum-blockies-base64": "^1.0.2", "ethers": "^5.7.2", "next": "13.1.2", "react": "18.2.0", "react-dom": "18.2.0", - "swr": "^2.1.0" + "swr": "^2.1.0", + "victory": "^36.9.2" }, "devDependencies": { "@svgr/webpack": "^6.5.1", @@ -62,6 +65,7 @@ "eslint-config-next": "13.1.2", "eslint-config-prettier": "^8.6.0", "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-unused-imports": "^3.1.0", "hardhat": "^2.12.6", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index 3d1057f4..a5f23004 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,11 +1,11 @@ -import SafeProvider from '@gnosis.pm/safe-apps-react-sdk' +import SafeProvider from '@safe-global/safe-apps-react-sdk' import { useEffect, useMemo } from 'react' import { CacheProvider } from '@emotion/react' import { Experimental_CssVarsProvider as CssVarsProvider, experimental_extendTheme as extendMuiTheme, } from '@mui/material/styles' -import { useMediaQuery, CssBaseline } from '@mui/material' +import { CssBaseline } from '@mui/material' import { setBaseUrl as setGatewayBaseUrl } from '@safe-global/safe-gateway-typescript-sdk' import { useRouter } from 'next/router' import type { EmotionCache } from '@emotion/react' @@ -22,12 +22,14 @@ import { useIsTokenPaused } from '@/hooks/useIsTokenPaused' import { useInitWallet } from '@/hooks/useWallet' import { EnsureWalletConnection } from '@/components/EnsureWalletConnection' import { createEmotionCache } from '@/styles/emotion' -import { GATEWAY_URL } from '@/config/constants' import { isDashboard } from '@/utils/routes' import { useSafeSnapshot } from '@/hooks/useSafeSnapshot' import { usePendingDelegations } from '@/hooks/usePendingDelegations' import '@/styles/globals.css' +import { useSafeTokenBalance } from '@/hooks/useSafeTokenBalance' +import { useLockHistory } from '@/hooks/useLockHistory' +import { GATEWAY_URL } from '@/config/constants' const InitApp = (): null => { setGatewayBaseUrl(GATEWAY_URL) @@ -38,12 +40,16 @@ const InitApp = (): null => { usePendingDelegations() - // Populate caches + // Populate claiming app caches useChain() useDelegatesFile() useIsTokenPaused() useSafeSnapshot() + // Populate locking app caches + useSafeTokenBalance() + useLockHistory() + return null } @@ -58,10 +64,9 @@ const App = ({ emotionCache?: EmotionCache }): ReactElement => { const { pathname, query } = useRouter() - const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)') // Workaround for dark mode widgets - const isDarkMode = query.theme ? query.theme === 'dark' : prefersDarkMode + const isDarkMode = query.theme ? query.theme !== 'light' : true useEffect(() => { if (typeof document !== 'undefined') { diff --git a/pages/_document.tsx b/pages/_document.tsx index ae80932a..b6486dbd 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -10,12 +10,14 @@ import createEmotionServer from '@emotion/server/create-instance' import { Favicons } from '@/components/Favicons' import { createEmotionCache } from '@/styles/emotion' +import { IS_PRODUCTION } from '@/config/constants' export default class MyDocument extends Document { render() { return ( + {(this.props as any).emotionStyleTags} diff --git a/pages/activity-program.tsx b/pages/activity-program.tsx new file mode 100644 index 00000000..8cc152f1 --- /dev/null +++ b/pages/activity-program.tsx @@ -0,0 +1,9 @@ +import type { NextPage } from 'next' + +import Activities from '@/components/Activities' + +const ActivityProgramPage: NextPage = () => { + return +} + +export default ActivityProgramPage diff --git a/pages/activity.tsx b/pages/activity.tsx new file mode 100644 index 00000000..680dd281 --- /dev/null +++ b/pages/activity.tsx @@ -0,0 +1,9 @@ +import type { NextPage } from 'next' + +import TokenLocking from '@/components/TokenLocking' + +const LockPage: NextPage = () => { + return +} + +export default LockPage diff --git a/pages/claim.tsx b/pages/claim.tsx index d11c07d2..06b182b9 100644 --- a/pages/claim.tsx +++ b/pages/claim.tsx @@ -1,9 +1,20 @@ import type { NextPage } from 'next' -import { Claim } from '@/components/Claim' +import SuccessfulClaim from '@/components/Claim/SuccessfulClaim' +import { useRouter, useSearchParams } from 'next/navigation' +import { AppRoutes } from '@/config/routes' +import MediumPaper from '@/components/MediumPaper' const ClaimPage: NextPage = () => { - return + const router = useRouter() + const query = useSearchParams() + const claimedAmount = query.get('claimedAmount') || '' + + return ( + + router.push(AppRoutes.governance)} /> + + ) } export default ClaimPage diff --git a/pages/governance.tsx b/pages/governance.tsx new file mode 100644 index 00000000..90fb783b --- /dev/null +++ b/pages/governance.tsx @@ -0,0 +1,9 @@ +import type { NextPage } from 'next' + +import { GovernanceAndClaiming } from '@/components/GovernanceAndClaiming' + +const GovernanceAndClaimingPage: NextPage = () => { + return +} + +export default GovernanceAndClaimingPage diff --git a/pages/index.tsx b/pages/index.tsx index a66de8e1..359c5b0f 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,15 +1,9 @@ import type { NextPage } from 'next' -import { useWallet } from '@/hooks/useWallet' -import { ConnectWallet } from '@/components/ConnectWallet' -import { Intro } from '@/components/Intro' -import { useIsSafeApp } from '@/hooks/useIsSafeApp' +import { SplashScreen } from '@/components/SplashScreen' const IndexPage: NextPage = () => { - const isSafeApp = useIsSafeApp() - const wallet = useWallet() - - return !isSafeApp && !wallet ? : + return } export default IndexPage diff --git a/pages/safedao.tsx b/pages/safedao.tsx deleted file mode 100644 index 0592e286..00000000 --- a/pages/safedao.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { NextPage } from 'next' - -import { EducationSeries } from '@/components/EducationSeries' - -const SafeDaoPage: NextPage = () => { - return -} - -export default SafeDaoPage diff --git a/pages/splash.tsx b/pages/splash.tsx new file mode 100644 index 00000000..14e7fd0e --- /dev/null +++ b/pages/splash.tsx @@ -0,0 +1,9 @@ +import type { NextPage } from 'next' + +import { SplashScreen } from '@/components/SplashScreen' + +const SplashPage: NextPage = () => { + return +} + +export default SplashPage diff --git a/pages/unlock.tsx b/pages/unlock.tsx new file mode 100644 index 00000000..fefe3fee --- /dev/null +++ b/pages/unlock.tsx @@ -0,0 +1,9 @@ +import type { NextPage } from 'next' + +import TokenUnlocking from '@/components/TockenUnlocking' + +const UnlockPage: NextPage = () => { + return +} + +export default UnlockPage diff --git a/public/images/assets-stored.png b/public/images/assets-stored.png new file mode 100644 index 0000000000000000000000000000000000000000..ebe65c570a68f0d7c8ae94e3a218fd62addd5e8e GIT binary patch literal 29284 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 + + + + + + + + \ No newline at end of file diff --git a/public/images/barcode.svg b/public/images/barcode.svg new file mode 100644 index 00000000..e8700e26 --- /dev/null +++ b/public/images/barcode.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/images/clock-alt.svg b/public/images/clock-alt.svg new file mode 100644 index 00000000..6490aced --- /dev/null +++ b/public/images/clock-alt.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/clock.svg b/public/images/clock.svg index 65952798..06676e74 100644 --- a/public/images/clock.svg +++ b/public/images/clock.svg @@ -1,4 +1,4 @@ - - + + diff --git a/public/images/diamond.png b/public/images/diamond.png new file mode 100644 index 0000000000000000000000000000000000000000..c0c88edcb27c919423ed8a3c17775ccdd9737a53 GIT binary patch literal 53493 zcmeEtWm8>E(=`NlcXxMpIJir2x8UyX9NgXA-7UC7@Pk8u;2I>jy*qEwWmk>K&+!N9Sas{cB; ztQzI@*C4p7nlu2cewOg`>j%IBtSJHE%>)VzY(836LQLHY{Gu18uuMyb`K|um zzb9ANwfxas&0l;_Sb2T|IW+m=8Ensr5BVnM+-6U`sKOroKN4ef5D>x7~cQ2|FgjV{}!m= zMl}Cw5}ES#K7$9n#IBz)K6xMMQvSF}nFFRL zc-xHgoh}JhW9x}G*9dJ3zGFVbHg9hsx%-#fb9aat)GiFEy#?p`@LDdn;VJ-?r|k5{y- zss0H6D0n^nVeN?}xz3tj8vD8G_)eexH08gk$glM((*N_bAP_oq8EM~ zn7@bE-TI}e~(tn{7s!fl})%Aw?Q~2v-3{hB-Vn5f#ghOD_QhRXfJ} za;zC$hZSjF)}YwzAm?)nAI38L+E&Sq(_UsX$Ci~eWL5_Ji>y)=Tyx=fto(7s6YwtS z`*3k8`gNkH|D9-`Z)QaApNy4)fK$mw`YGU?$@-V%D>uep9Ttl;j78xGf zb7x`L6G`F1Cyd%c2N@X=g&hYb_D_N$xaY7r{j~^;va!v1LcCD*+_$-+c=@!Pi-f3H z5uWBgSJa`TG{&X-gqoLYoC2?XngZwdd&hI}uLZ#VZvjLH_SHjx{uEP~S_vYln2`z!-jg&g0FSHraGFB^#wFCUnnL?Ra*K6D>WSRrX52?Vx;9R7cc9DfexrG3E8)w zR9$8cqp1sIoJKDvh$C=#f61@teNSU~#Wy$iD@cN4+NT_EdzjkM^B)`Oj_cXnpK;Y! zb!Z%OsI3{qn~}23!TEmvv)HCGo1U?uY)`Dlq;te;utAb4tJ1^DxT0nuU9-OI^*#5LT)AirNZlEP}?~hUcMlTAM$)awEcV z^-=<)oIa$|x_{RGA>Ax^yLi);79{gH_%D4un;h9E^-MEk@0DuB~T8%cf;F$-|9x6B{fuWoujR0sW~w z!1>K1z9*?+#-;@vC-vsm5?2^68$(4ZK#pLHdm>p@2*Ck=>pTFhY`( zSQENfC@@JSFXowr3`vV%*&h9@MS^{i6GELEtt*T$3quk+`G?7F!ZXV{Ocfjdz=vX2M{3byF2>1{Y_t&qLGR?Bonr)ifw98AyGM z`&AW5(^@XV>@cx5il{*0qaQcrRkGU99NPL9KaV!XMIKN{DfWG1zit5$RDG981bj=O z_u{zG?1f9o9{(zmOK$II{sxojmLkpytT9_A69{z-{TwI>*x8YviCnc3wR<19WEoHZ zC3fArZfaE->Z6~OAeiQDep$=#LsIW0F>Riho z8f8uRt~Z@xUShZmxB@oROB-UvAks=oU1ZZsT(sUOzV?A=EaGB>Xgk_4+B1Y}!MB&T zL{Ou@5i!?6WszzmW%-^A#U`Mv-J%p~sib87MnK5N{2@X3We?@!kJj(k-;!TABmNI( zk60*g&jmX_-+#L1uMk>`v~iC1Mq8{8HFbay|3dhTy?QEBo2I0PUWV4))ktxQoTZk7 zkpLHqB>=(@{i>_QSbsf6@xRz8$oir(f5E?8 z-OuQ{ROD?~UaEq|kds~b{67FbwKJ@iW zBR*_6Pz7Nk*52kVQ;S_8jC14R(PNM-a15e+ur%x|h{v6=s6L$LqO?d(%-AkH=FjlC ziKPfOPMw7tP`7njKw|KrR`xfQ2tmS_NzU9V%@6$S4_swEA!td}f#gp+>#n=Wo~zD% zYe(NbgoLl#AQ0`#tNJR|eAa(Zpae|18u74m-v}1;XBRf+n=GnuNqV!W0rHh1cNGw^ zu$7=a$#7E62u!Pu2MRAIMQ7!C@kYfW6XBFrQV+vbz)X$VMo{Avs>r)&A)0n|cqhy4 zuw~FF{VIF7|1c^5@`E!)BO%iA@W^4wO_4BM&>7JCP@{p8WZFlgg5-ZspYbZrBX5Vx z0C76%;oXRLSwIZ;?}Q$1E=*S2**mM}Xtgh$l@&7I^$bxu*Z7bA9*m8M#6IQZELDu3 zL^K{4vAFvheI6n4h@1*F_^i()Cfyi`Qn$wb#Us9}>G!^Q@y+|VYSu6`mW*r4Zf?RU zlGVUViA5lj#sIjR$Kr@fQ*sj%)*Tf~B+-$vqdBXxY+|k3D3_XZby%23nQRJRWbq@| zmm*Iph98RNMxuX!-b*&{<~Na1xOyoMP9dZzT!!b#D5!w ztM@w3DG=6C}Xdor?uUMvcW4HiLx>sii=bbx;eyPL4RvWZ9og#Df*{eOOY@|dZR;sY04Mm+c~mD%Ot1ak zC$mfgTZ6;%S$yy@(g}mNP5UaZVLGmEkYg)#q3%7NgJ+kUMO=KRtB#@R8RuLF?=AAF z!%1&2=(C_7oS0SR7&wH9AHM(QRuZ_3x6Nbf2iQOEtA~C2JnlGlK3Z62@2Y?JX-$Ns z-x!T)I0i1oNb}UbLMU4(5;o4C1j~g2;=uGP8TQ!RKKV+3a4FQqLu)fctmZ_?-fh~) zwrWT};hR%ACr70jZ0IVvr%QEdx;RUGWdYu|R^M(;3UjJubac*EGkIfEA!)OM56T&< znAN^hAN^8qvyNmTl(kR3B4@5<^^UOxA=ZdZ&78ipu}UH3_TOq1^+O^=B#0kBA-Wyo z7<(V#G7{wncZo|EcI+C>%`MSu%+LgrO>A^IaaNk%Y-D`sa&OwNJ)5C~*=HC+xAUDn zE+ZDn7pssNQL%65w=Y&3s4*RzD#i8bknyYA2&}Z34ApSGnXmU7)}PPzZ9W>^iF`gZ z2!oM4i9+Gw?fe(I-ZHK;dEPg^ujV?B`Nk}A*S3)fY83k}@WRc(iP06&s!nic?vD65 zDv(l4HNl-;V@rfb`P2+bn6MrH)nCJhM25Q(eP+ae97Q@MU%uHp zSBaaWrzWV2;20jyW5rF$=2Zs}WfSmYs_gtkTOX~iFMq5KhyX3$F5sPbX)I^uMC8*{ zg^Q0HJM4g;c%cu+Ay(6)q5#2{bjro=jNv}Z>gr>E@Y%$Cn`#DU`sD33gVDcEtq`>} z-3VMX9hE(s177l40*DCcevBI`kSiI|hpBQESP;SlEZq8xjj39t16KxRLD)6bo)^0d zM!x&XhWF54R}ymK7oqGK8{YXh{b=NRWKH70se7I%=p-t^wP&Z!TQ)apwIhan==?Di zjTQ>Zp=F)-!^j*6R*K2$C9g@O9i)_;MXh_94GXcH%nffWOQuzYy)&dl-$+l>$BtkS z_e{h(kU!7K9NteXaFAzuqCQ9htfFQ>(~}*wI|YsFr_(aQqi70cits|jyrOu8bHVhk z^PSAj4xA}m37%|TN6_s^d*+HwUo+3gRh09StR+a>lyNKN7D7vw<|tO z(W^{C_v|PhFZe}N;5&T&x%(IhRo~TF&XZq3B(DY}FM2^Aznry&4~BzZ z69xwQub5w)_I?jHUTENWv`D*~y-P|~7*jb*QP)d2TTdv<#DVPVkVmptm?XHguo#3~ ze5$R(ezB*gL-a~WVYZ1pG^+ykWbI4W!;p@Rz#hBL=sw=j`rD*BWd<*`Z_ErBCZp?i zsyLGGvbl(~)T5s2umXbbzay=6$KP3Z;S zvJjk1?fyFrk5ccX;lVjM4obLj4S-;-xJ&6;C|?y4(Zj`8wtuDTZ6edKr~j5ny&>gU6_DuM~NOyNhHJ5-*l29lK{ zQU7%jfkTi$yIn9*h7hVbIT7rj-mnJ&m=HF`P=^h=b8&zHl@Lvtki6mujMKFjimoEW zxh9u&0WWLZuAi`NMZv>`rHIFK`a{!kAn7rP{exrzo@rlT%do^Nkh}|vzMQ~-$tnnbKX8%t6?iFR&*a5A3 zny4Erv>TnuCX81i%d0Ice015E)J9UgLUd%Tx*YlM{w-KiszT)WN|S8k!r0V6w9@5+ zoQ2It;!jg9V~2tL-Sea5a|&=RIH8T=FsTyol1py=f4>TVZ50KlEEkcWO7KqpYX)@< zb*t(%^)@!=X+;^TS!vaH-era99omcEY|xkGKWzT}MS3HVYw60`?|&9h4p?mX%2NV^ zzNq~U8u!naB(ulBYcmmz_}FcP?O6N|3UqJ=$!Y3?`lkipgQ`uJat^pazG#ZDbhxHh zcwzM9NPR!+fkN1cx&oZ*L>?`@l5N>P?X~wxf6`_DW0~az(r!>Nyg^8WrLmJ8IRBme zJRdv6^sAr4G=_HDJlQ@}F*b@&b#C{{R_Z}(DK^7pW^n0uxP@hRHB_V~vZn7weL#%E zA512a8b5HEYi(wGu{0xP>tnLTcv;3aHK?GO(e+*g!`Kzw{K%_{WJkQzY`L;on{2T3 z-DbF0yBs)lUQStU`}nG=ohI^s8915$IZjJ-n;BA#S9Z0HSGOM)A)!OM*x-YnQS)(s znVY9=o+n|PAr^j#<@q>R{)G9GW03wM($ZdAMsE*}t>vrl_%nYPpYi1KqRl&#O zGrI|+(o?MvSFM+m&6W;LuYLhl(vLu?lwD)f6K>{g$~ZlNPJYd_jh#3!2fDCDrnkia zGm%1Omf@j6ypz}d**%>T-$i^vqieDuJfGi>@Q85;)ea#W-{Afx4K1t{Vl@9jB>-ZC z(#s9`2e%SJx=JBJ@y}Y4F@%xd$avAwp;bjq$q5i& z0Fm=nlTfVA8_9RpB{SFx_S^7}0b?zNgnEZKRyS&D4y-3JJp@SHdpx!p;AO#o}sqOYY-8M zS6_FAvL|IF6Ld;IP2G?3IInjnel1iv#ZA$9tJ|m^9H)HA$4d};$o0EPUn9zNXMfLC zSdPdu9Pcrla^c@tAX>J;Kv-Xfl!Ga2(G;J&IML(tn8YL~k$u>S9nboW0*ISHSksHI z4LHUc%;&B%Pv9smjBy!>)BNjjtDQ+AZCan`t|9JH38b&UPT2Z-2ir^QgB&ec?FWhgC@@Lve7*-n-!V6 zaA$vg;waro)`!m6=iv>c;~TdepPwrdmK7=21-j3C2U;{XiIYgf3HHiXmk+R8ui3`~ z)t42|ww&bA%7AKgi-WB!RQ+TT0>sXdqMiMHd_>*@ZMNy>0sxeK!|%WT3!I!eP&Itx z;kyVQ)2F{!g#UIFTz7f`q@+T=Lh`4w;58G=?>3v&tGTS^Omm9JFXlQNMU6Znqh9%^ zGhmIfivzDT2_(_h)s4_wGG64nYNCB>u0fO;Ugpoy=y%O4+5mqgKx8-kj}~r0CV6J( z9>sjX^3Q@39s|{>o$4|f&N#i5C`3o7uM<@FNLW&3^@%8F(!a1R%qq3&vSoEzE}2J@ zKew8xd7)3afbd&9bnr{@xm7sX5Ik}!A?}PYA)*o6w4)hQeAfI>^p4s1#B`NJ2DRJ- z#ZR8D00{ltTuX6Zn^2?KnyT(dW+|>n^`Fv#Rgz`S1qv>r#jhTQeE*%$ida6J;aP#bXxM)R;T=>){n8I|ay_zf zL4R&*LcK?p-5J4UCXtG7g5$@ehGbn8rP9Tr!e583^RxA7|Iq+r(jYM>m2M>*y!yf8 z?axk7-QdXR+30Q}SV;#^C#%#J=Zk^lWRu|w?ts_8(UFF^nP}n^{69aIWSQCnVUGpP z>1FTSxs&NfmM~W-l64`Z@R77TUI{y&@aC>bjOWIoj~6Gp9Ygu|3D7Bd;AAyxe_zj3 zyI>-ZT~h%0O$N@Ekf2pw=J!_=OPr3@_un?r%YgnQ0vhh(k84fReIHW{kK$6$P7$lI zv5qkjeTUf@?d9cx<+Qwx7FEB0ecCyFQ`#N+!A^!*ObxFH{fmMfFRFZHA}k_|^RFI|`%^BoGo7Cr zLx4fgbpR7YV74B)J2iw)b>qohm{n?+I#tM0+k(3 z^3T$!aSWM8k3sF?P1O<^m8fQx?n~B%sOs19eI;rw_adv;&4b5*$WtkhBdo}`L&ewe zgjaiYR{t^N&u+w-vPl8TS*?s*hKXZ^t!h&ZDi`A9ST3$(0UV2-(grPMp3wHv70^n3 zL#)t#LI1}+%HYR4mwE2(e^{|^dc9ZHZSh!L4?Oz&Ml;cEf9POzEJCMUl(d9aj5i|3 z9OpG7GoktmDW;$sV(mQ+k<_lEUqEL;MF-bo z*jb2CNY`62#n#6Hzo!m-+1^0eFdmUeqe8xpYJ!jUO6(!bDL>Z!Mo>z_G))V=1wC0k z#}9WfMPFSdQuc}&2~SO%4Y5KFH~qX{|Cc{qSD8tt3}j*=Spf&%18H?%>h)8nsS>LO zK!Ou>ILZ$LEsoyC5RZKA+WzJdFow$ooNw#$6=s^jLQcX!Jcdg9Wh@!MX#x6&-bgMK zaNc43?%h+!#{W$hO~QuBejl9&Zr0jCdFGj21Vw@)9-}ReheH~Ld5E7Rz>ZsCjRPug z`nEa7%egI z{Li_E@V3VKo(&Wavw5q8XIA|f7c-5Har(F+g~>G=(KJo@VyRAe;&Q` zf1ZmcI!}>*soLAug&jY=?k8rM{xZajWhcTcvSynI@DJh29I-7CzXl}I*?FO?8f_AU z8*DSpJ8)OI`kL8cPAxdo>QR{ElD_tWB5yo2)K{hnn=v8&j5{hpWc2@Xg~#uEl(`C%;VIe3X-|6-#+Mv-kiv%a^LMu!g2BW zcg!!DSaay&rE9#uYysaNR{+nRa94#$MuF{MYyc4{tbO-|lLErOq}^%okEYf0VBJ6Z zLLKf(U=`N}a4d|b?V)qWD)i1;h%h1()X7p@9c02~P6Y#fL=3Q$y7S@=TlprjM;fvR zh?LUE-#8NYN*;>0phe_uof~Lp&Fye;_HFCwF$z9=Qr8eG-uiX@kS1dT*BgFnNO@+l zFDQ_7JpdU&OC+8=j!*5;cd8;Trwda-jVYvg*(K95@RXAv)?eK3V7c4R1G7C!%*gUk z)FK$vC4eGYqA}EMp!e2IMUtcl=uvl^*nZT=yVXLQ%y6JH1;nj7`%&k0A=~ZnB9|MC zsjwMemaOx#x1F_?<@DVoy{}Wc$o+>4RN{{XBOi4>?&nRzytE`iJ4y~bnYIa!NYW6~ z7!H+!m7Ah58!VJr%xDgfb@B3bpuMuqe#wU;pFpO^1}A5as#hhLf-#qO=Au&;*g z{*g$Nq$>kPqGBXzdv{2o5@}dFyoQVMP~NBdGSF2(PfU?Y%2hDHh{_}9x^%_CyNz2I z`fvmivyM%-edpB6^!(;l&;)O%^bT6|?*<yx9ur$A*x46k0FYWV<7HH^tojt~?2vfz)6_G-dwCCi z66qi4!Zgcwdp%j8{8mfjyY@hl2glTyQ@!G_ zU z*eB`z?6`;m_fv?^`-^Qm}ZyO7-bH|GmLOJvHaYHe6ANU8w% z!TB=-9TDY$8lDfs7bjc7qb%K1@K#07!j6+246%x``d#kUz>{K@a->3~SY8ne-%w#Y z^_2f78tk5^kW!A@_3pW^lx$*bUNVF2>SHuPW^X?(00hTqAuxp}2kp-LI-2 z=)LieKgU-*-42&}?}bY$qgdr(Y^d2NII`R5VLh&7w4XmyS1VW=f{~VOR3h|X$MCq) zuGIt>e7fxVhjCpGviCDKKa8-FoyTkcR83tf-GuL8Qr=usi_7)4E^Fm7C19 zDYSL!d`MXg=vZUXvbX-SLfRYgHZp^Qtj}}`#U4wKoLl%?(ak%B#wtJVVK9j$(w243 ze2VcCr*!il>8Ba$>uDOrQ0@#w!!%@N64UC^jt1k^S#k8FC6!F1F|cY0GY?vTui9rb z5QuytO!o#Obl&`~2#xp2c^|z}YuH>FV|uVT|5`cOqU&6jvkEiMoqpRff?L_MnDn_d z!pBJiY&ugjC063=4BY>|7rbA*T3A@PVC;W~{EExQs|59#+O7{eB#cul+*ixE;^r%u zoRx{+j0@b=b))D9gBw~e9CC3v_qGo0n~b$HWQNS}g+bKg)hoLBYuRQL^SQa#U2U@6 zH5E$a!jRT+z)#9!3V4&UYT0GAOb&%*Rh8rihtH(tho1qgcln;I zjEkj{Cslc+f#fQr1}rz`!o2Z#P3;d7bp8Cl=sG4cjXi?KWg4=fM)Q0VHfkkj#rir! z1B5!?2^ik%A)26F*u=@Sk!U#3wp)BOMkk}QLFkOXy-``e*h3!ENjkAuf;4w=`bLq%&W7d{}CuA7H z1rMHy&T{{ONdLM;>s{B^^vtHWMf1Bjti7z`z{wN@H8T4 z#6m&9xU~HrSxmP}QXGUoDO1}94UsG@Aq~-)xNq&q-o#n#T?qg2Mq$KQOtyHCRiEf$ zw%NFGf9UoJgSPI)B0FfS<4+IW*V5$mu;#xdbkS z*tAB6VcvgCv*KD9RVIPSGx?-A@B3g+9ahpCkK6CN5j+2$`#osawAjqIk{4 z%#v)jo*D!sGTN(HQq$6gL{*Q;POxI%s!7FaxWj>`Uo?2@@|@p8;?i82~>{|;&HCY zHFWWk`6~bGE=XmSO^V8*MEa{cEjc4Dg(m}tWD2{&r|OC-?iXD93;ytID6HgdDUx~S zI$y)ZjDKiSXrV3eX-seL7fF96q8JDT6e$K-n$*^^r8zT+LyaQ1Uh@k{nuim)y%sNY zxum4EKVZ)_{NtS}`+jinXag|rY(N-rd3jnzvg0vJ_4$;h>}N+}o(q}?^MK#Kon~Pf z5B!syuu?L%8CDb;ea1|0bOuYw&8-4>tf zVb4G01V~AmDpG(cROEFD_SC;Dm)gazBR|r1$@sWAL!2rCoOqJ*GrQ{GQ9J4~&MP`!$Nnp?EY*>_vG)y!%XZe(rqSH;mb< z3V>KujU-9}H5Ks@QqQZ><2PTV&$qYrI#y^3*~i2%-dG|vnBfAt8!=exB(~ANE&oEv zfux%h_9Ov>5vF#C(GRq3VrML#mD2(>rSz!Ge;kO|%!54L)lFcL;L4|BLFK>|t3Kw7 z7m>niBdyv?dEta`cw6hfVQ)stT0^3v=S9LC@)eNtdq(vnC#Y&^k9AV6!Y5MmA?*lJ z*f9gvTHM3W=93C)Z!m~LOH91jTG5EowvS$)M{6n^vc0Hgk!5H6-2AefOK;_`@Ix9);K}vg@E7AuCdb&Ii+=6jr#gZ$tg8%{Y2oVQn#0yz+8z*{IsRQ%ZQZ2MlwNYi-nT@{yXSa$I8{n z(0pIgvWO&AvtREuu?U4^QQx46*4t{#CMa=r(Tvo+1S1k3Ibnm`x|Wf$#(R*}H7Mp7 zJ+*uUGIVJ{Bo(U0#9!V~E<|>8P=!e^^93rZP$p8gucp->H(Xzy8j3vuSJSb*(ZpBA zs*{M4S>0SvIXiH!e@x_zaB26E&F)N4mH2Z-I8)f3L%~58f)`Rp^@rP%d1n99eAaF$ zdXfXrN-UT0W%a^H>Yqr^)P`DRb3*CSa5_=LLHhfS1!9R7G{09s-H}ek_UFg8^82XK z8-jUmDlOSQXt*sSBgN7opoED4Bilo(KMOg2%NU@caDf!*MBdsus?kHg7yIEujJfTU zsd{J5IWG{ziopJEnQ2a{q1_!gycD*!h@vZ*GnANVM1V&Ux-!DsTA?{yl+yR7yHcf`kJVIc4-2$?^3 zZS#eit?xb`06bN}qbClrUPd1=jXSq@eikJ}W^q7_niRg&8F3=`Xy zR@AB%(E+d+mLcpDi9>%-Ef^1!R+pVhbkLF&Y^U4;tIqJdvt;@pD5cf>Zv7eH_*JJc zp&#fGk7*g2KB?`2F=Y!sIof@&6zmNVC~U`!-R^l~>vF=X6m3mr9`0HB`37Hy*K2L_ zWX89z`=VX85~E zm>Rnbr5t9>1CotWmDa0h}+aF%W$mK57 z+Etd-R0lu^>m3xA>0V;O?3~b08029?Who)rD?%Ad5Q+AUbQ`bi%`_=!Kb z?YMcgWiI@KC)C+aH~E{6jOS6P5j6_nX{eK1x5TiDfd~j>sv10d7K3!M)+M`p#Ocj2 zusj6qxa1ZupwWOtYweD5mv5KDbUZ{>Z}V9-Y|20hsUk@}W|E(Djb<=nEm!R8cv6%qcM@+sMv1NHv&7Zl1ec9p-)suPOsEz?eqFak zNJ*C-gScr%Zo$(k%|mG2KE?cqV6HqO1!;$P8>Z>&vC!uOU{8EBj(Daro@^4Xzn?>& z;N{~+Gmz00LH_fx#^~)0i=^jaV@vjc5`fH?Z)#<~`P@@R!;u%&f*O)YhDOTectRrT z#u(BKjet9whz9M@lKi{X;0?6r!Dqdw%yK>Wi}^-{+f>W}NMnO42VCT%2n zX_R4}k1NOBiF^S%&J%t9i7KCOSCOhz)Tq<&QF(!qDr`k<*dVdqKRzqeeARBOP*-si zd4hH-7V6lV0^FLE26rZrzj*EN_&;#?bHEV|B2WRP_OcUmMsO*1IIivzR;p(U0R+@= zFx9HS*%H|8Yk%hDV=6tvABuz3Gv~%O)10;Iduhuy%Ie=s8bAIi<_|~}mIB9%6`U2B zJi+o^B2c~ql4Ojv&5mjnB0`_JVh44-3+C)aS5>ytWN?Ox zOLAGWsxyP#_U^tFm_bmUP&0D*JuUc1{@v*rwOm%>VOl$M-_#nDBJo?fat(Mj0~3M3 zqd{s02!vnHqx8Q-m7KS~mG$c})0xu>5#&{(H_B$Jq)Nf&1#cZfL=ZU32st`q*j26k zX8MJ@9E>;h&Wks&+*{F=oeD)!BnAk>Hx5bsThdkg9;4UqLGkl$!`5W)iIIzw*jRw} zjCXE);jtmgYA9%$5~uET@)u|f2r%WJ9Z_O|@#^sVE`!W|PWjx!yDX2`A5ux=`I zc7P(4TnsyfdU1Wf=MLO-$1+V?ZKO>yWDVq;bY2_F-7iIfsmFgTZCiub}sxyU8>lAj*(_d}I;-!fKXr%gCvP1B9{%D5>%4%d@p{H9-J9mSye~#FnX6QDC-;V?91qz6D`F zgIJ3#Gm@2?KPx+*)Y~L6Gqq!DH%&t&TYtB(!I%cbp1s%3fpd$#2QiUA>-V@a#47pz zXtTjwxm<(&t|w$|Zvd0BlroQR?Xpc;*sn6hEU}>5iwstj&!R1Uz$b7Lo*TB?x>_n< zFW)J}oG!(yYINpbMF&|$Nu7X&K_~>Db^i*~q}6(MavrW}%@5buhHj;gX9mgTy39*C zvJ(PHkQi}<3cla*2;^@if~171@~(yxX1c+79-+q_UN(4$KWQ3#Mk`47!~RQwAdb*J zoi0Be;v0Op`)KXy=qDk+!BxxJ6j`Y#GCpb3gh8Xed5!E)?RegB;toG(jzJc4(1}fg z5!wzeYL^mdI-??y4@1MWHQkx=kNw%XF>AR#D)8qb6lt78@e(?FQ%j2U0qbA9kK||$BmJOKF?_aNMBY#W=8ec$b&R>NX&~I4xeN@h6r?^OQhU^<;x+WC&8$J-W4qD>}bC{1ek#xaLvy{!=6WV0V@A_wI%0!(R z_&D8w=l(Wrc-9{#4d*`W5SbU8CZ){^@u(U_K(YI^*GkYYFn-}TZSOLmR_p0-VI@e) zPy2R7sqL}c@ydI^*YIXY;LD^^vqgE|ybg?dUvA=>qAFa@=*_3E%IQXh<4TNhs>Q^& z@)uRKnY`fG*T=p_7cGZ$Tc|iRTFv{`RXJyFv?FU4G@()RC=IW*qF~kIg>r3j4~%3x zBP0B25@0*#ZE6s(zh_uA`n}3pBgI@N-xrwScgY;^_d@PaFk@-RVm-aD{O??pi&6*E z`Es|a4+}bt=VAnE9}_%x3~22)d?gyTkt3_vGJgLrRT8z|oU1^445qbXjnx|u9O!(| zm_gB9$n8U@!qI!lYhWq!;J-YYHetH9_?^{D3%vGMv-Ncc2ezEmzK^dwGw{KE)qp1` z76UtLUdiQc?X%A!&!G1qF2I~9UI1;@i@rU5^gLoW{@n>Lvyf=lkXUW4KF(MCYNe zluI`LVT`EXc=~7^o0H_bQ;U`vAYTFIDK*hbX1>Q^w=b4g+laSA$a!5-qr8YK=&2I+ z;jZx=R~KU4J zAKVNFjvnKZRr!knGZx_aft%hu5aIOP4-!udDP{rflRMq3#;2gsV^-h4_n1OP2+Aj#d*^=r4 z9c1heev}Jyy5r~JpSkI(F=9^EX^|yS*Xw(BTi)7B6h`!7 z*AUM$th>-nqL7sZWt-}4P=h*E{`gBKf-^c$0bHaHP7w0))7Qs=PtqTE(dsb3l3ZLi zULIN#MnpQfj%Sq|$#G+Eohf%`F_*nG5C|{F*lnAVb2WZ=qI{Xva49zNOI=KYD+CB^k1jFda9&8l+S4}J=J1}vsz%no_y)c~ZA;IC^f8yQrxj;a-Ze4ID&a*yV{oKj!c;@MhR!5>jq@ z2Y*+~q&@bY=6^x2=F2{v_oQt61wy!+M$djXMKiktoMc1=#0K!NV9h)F$hBEn3Kwb+ zLtaxSwq{q8Xb^v4!>=qnGA^9<25IvU_=QB%ll@B1qse1tMiN)Q&wmU3;S?|q$C*he zLRl8zk|&OTX!|AY(`XBRSZ`#FRXbbNW(7rEo<}z7elKDDGaQ0ZQFk9b-5+5hrkaV5 z4su<6EMZn#3~3ac*hLZNbfi?MT!pRt4xCEiujB&qgD)GGfU{FEWQ5UhF=`{+fT#lu zVNEhc8;8$V0aEU~)UXggtF5@BB^Kppczr%*O9y2l+x_@-O5hQVPaexDtzr`qGApxV zXO}F<@>Iccu)kYk3Q|wkHN{p@66!n`fdBrfK7abK*H_^P6l#%gJSafX#A4k!Q-eXj zzy)kOoq18>bB`9NVf3VXl}Y*Zcf2X$!pB?WT7AjM=*8Ls_qB`d)Hi(Y{R1@2$+*a2 z2geB5KCqsfh!{h_lu+zkwlyadzBY+6F-or95@hjA#)g8j5=ly73~1Z!@};6Rk$&6H zfTxcBS6JMF&FUru-~IpkbjrlW1ftV0W3bZ;+iHG{4MAooQs85Z)FGckkp4Z)pBW~&g621qo|k4oInG1f zCAeVSD!6ri8D{4eU~$4gXT+h;7ZlDTGsJ0s&I^$Lu^<_ze^XtM;G0#di~P75K@Z9CEdQFIvZ+( z71XsjE1d~7ddXtJtYWPhYCC3zZj96hNmLEo@ffdU54a!Y01iS2#_TejIWYp?IO==X2-ubfTczs<_rJaf41nC-}%}f7YnzBAt8{H=A4v>gK`uq7@Fe^grJf+ z*;17WAdW&4Lu^AI@?j5!BGuH9ED~R*!VQMiV5*u$pzv#1iRbzPp)MD~ei|NRqKP*n zHjs%X7)#DU)nb*9u#1$JTJaS9)GXUFtph57vL`A!;GHzcC8tMy$cQ4d)FbT zw~j&2|DMnRIf4$zN{Z~U1VAGc{&D`;@i+Xhmp=zuX&HQ?;tCP=$~1gpPE2@8?|+=ZP|sWy+fFkg14=4apsm98Iv zP<72WI<((TF%YF817kR78EoZ5tTfsUHCM^;gpU5qQ)f@AqqqtfDqK0?9Zjg$w#gAbuL`zm~X3v$oRa| zFlGej&|*F<7EOZ?p;-Ot=VC(dM!iXJ@s#ORXlEs)Rzd9BSW8!t&<2ZJO3P8;@1r5a z?1u8Wdgl-tTFdv#eVQ%pgXfn+53Z113uuPw{m`X#w~vp2ZES@*zO@I+om0>&y5QRW zXuVQv4s6&BnopLOm%niE=Dq(DGER78LOaRO_y0cqfVGHi{~!qqsjAT7HRO0iVHp#G zS9HgI_VNa3`2Qi8(_}zP1&cv{#(<{h=z7BHd7?t4)%7H`RQ6!W_UJiox7NWI=d{kvx%Wqh(-@>4y=L5@IBEJ4l!zBGnn}OIca^RIni40C!7hLUra)5YWj$*$)r?r4Jh4-*V&{2S~g3kN`EFYhT;|GsGcXk=Ni_5Tdd=bvP z{64UH(-sKfW*75!K#I~N->e>#Np7al{L?4ve%UOb*=o>ncshf9{=awx+nXa$ZruiV z+;tG_rnXmceORV;Sq&KJH|V_k{lWafI}X13&wlq8k3q%>k4$JM8UFq)|33HF-~RC# zKa8ybwb^S&c^SC1e{Ki~ih9vN+@{_HY^WxHa~0UJ!H0ATp4NW8u?&O4Q3z)N-}7O? z<$@`yR^n5o1yQ*L(}9peE$x>y&!RcF$Ws>7K^Rn&iAqSxK^I?1T0@;l(9h4kRueE%WhForriIp{s`8pa6vH zX_A6!J$pR|E3wE5>ToR1#B+eqHq3?3IDT1^(sH7*@UgS705f+SgxP(EVgAr@Sejjc z1^+mA7@sdfr`zK)37jyrw6OcfE`(K^H^a~c5wZ=x6Od3PND;R?lxs>@^vgiIhrw=W_8PF%>%8-iU-^=M zhK!Q}nb1x)TzB1|zx2RQ789+r!0^|r3bG^eh^0|iFg8OhN|72ju1sRqzL*x@ziR%WMHc8Ge5J#ks z7}873k~m08@sH0nq`iF-_@LQQ&F#%0Y4Wvap`Y{!8#`gLL8=TZR4 zUCEJYmk=ZC7`h>;Qkf^y z{_u5S_on_SU@lP798y^@ti&`JslhrnZN%$J%ec5N z_3=RxHjd}VJq^i?cxnVJ)}}i67H5hmZ6mv6Z87AVh0uU(JPpxB)f1hH!(;@(m;pjG zoaa;3qTw*H)f=UWNFaeEHJ=0!jV<~b%+Lg;p&_tYK{_i~Oj}hMP4>8zbzG|OAQ(rY zf-_;eM$SpMhwsLl13A}(@c4U^Yo2u1sML7lqVb=dg>^#Z10v7m!^dE8{|xkwE%?7h zQd)C&9EQcibFg@P0s6~5J`Trb4s-zGWT|ArpsA#JW^e6aPpV1IzoI$lYS7v>EA7?v zyR8fVyRi*8_n{ZT_KVMt*C6@M__rY2cJVjq*eMiBAZQ=h2Fr)KM~XJI8%;W<1|@X4 z+6?n({h~+tJUpvop6x2L2&Z|acF~T_aMST+SUWoh{Z`{JWOa5@Arsn3ihsKL5C81Z zfAcFlTI-6}QF{$HOg=iuQ&7i9h|ZYMuoe;prHn&G+KD1zCz}$F4Jtv>XtIu)8I(j# zP-kKA*In{%A%s6+SWzhn>R=sez>FJOM~@H8JzpCKsmn9w6r(AF2`5EVa;m7T5Kbhu zVu;yYET|5{cX|YAehVb5SPu~s9|s5GWD;l^q(!3<=@AXM0W2S%gMlZixw{U*zz^8V zM;BnxhXd#D}=1vMmy{MtP-3k&pfz7kSdqhpl7Fitb0wq}6s0IK{-_S8soiCeHFO?1otZl5OU#*hBEwk= za0Y~Cy8(~=<)4Nf7oE?9r1f%2q7DLy5rJW2eVna&fHPMcn!y@pt)|!bS>NZ)yfSO! zY~MBC2!!*(@TY%`uG?o$D#*uwnnd-px;?Jv0I^N?{; zAQRe2j)%VOc-wV_nebtsB6QpjN=8XZh$<421+6Pn0x|}Y@Es3dHfeMmhIFH5 zju1c#T|`nMV}gv^452(BP^FL5C=yhoN%MO#T&D_drMUzELfu)xE2tI)$lIU`*? zl>F1ySUUe)QI=qFp1D>NE_&qs;DIl?ih5e=Yy>F@)ULcP(DMbI+M>QD93|pU# zcy%^`Q~VH*aTjA!x3y3zt_xC1Qud%i7p|!(jEYeKB(-gx(AusAdph;rFMsK#XTRr_ zFZcvxobbqmcCzCtD4z7QzjNHSr^d0JFWo%{V@6a}fdqm=lEGhQxgH`OYohu{`Xm)L zX{iMe)ljnC!Vg9oqNK-!lvIs@kfBm!P=|1L1*jw!lpmO(5G0XfN;F^+gGoGB*?kg{ zO;lAvifW1hh&eGAPgS6KF15Cc+VTVHVAkiazI6Z=_a24rf#aU2j(U|f3xmZjbmy0$ zx74Ma6_b_}C#!m@T_7z`=bwW>KCE;!HW>*6!A*eKtRXs?=98n@2VC5a-gGc2I#OCg zh7=dCoO3asi0f1`!vX%r8m$H?wDEP5@U%a86|}}C_$Hy2?@e5q7~^J>i4YsAF(Nf2 zF)Iqq(Q_iO%EIbqgWQ_D85fQpAYLmxmKt+tKG)V4QmbY9Q49@_cv9O^w_wzbz@Fu5 z;kNxpp8G$a`INtaj1w9;hjy~#1Ag3kMZM^UfN^|D^L9B%!f0_Y*GCtWl)D@8h2hM^ z@K_)s2Qh!9d8IH}Xrk&;8;2ZAVayOQ1ifd>3Sx0XDp6~z^!Qz}E}w6@uH=nK8O6>h zwU`lHJfwIG!3rN07SB(n9Wg5Ttn!2C0Q$2_K8J<*s~K3jYX;``9D?P2$6?8nSJ$hp zsyC3(S{;;0C1F3ZuOpecNS8I`uPlZAh~R1mG3N!t{ST9ChySapK{#n7rg~7zR4SFv zZ53?z=npu&Y;D+p5~P* zB!}PjqJO;IbYQE0b;iiZcysH9_q^y&|8nD7f9fg!9WqW>WI{X1@u0UneO1vMSrfx( zE)?MoS?WzfTrzN{&4U4w@F76rA~X_~gP~6LsUC58&=X$a~jjQK9I%7?s@PN-QA zDKew`L3NVM(kwwZzi(|89B)39v^1xt;Y36sbINH*QNx7EJ&?R7-zlngsW~&4+w!l^ zedxt-)8Jz3h zEZoUq!Nt8ZRF_rboLgdLA5bny$g`@2RD?3?Py);(mxcpnc|AQ;X(n6?{!gg!bJDNS zG11kK^qs)qE2pRuIby zj|fbDFSS(b(>Vbbivo{E zejmCEi%=h4fVurgpqlMaZPp@^)}6;;VgF1ZD+>Q%=uXTOfs{0#CFY4bsIaK2C`Y7L zcB+6j|C4}jcFd>B=i8UhG zQ315nmw|jYYSHII=I6Rx_|oXu3nnZKVWL7q71i*e#^*l{#@DP7)?-2gjh1TDNqLVB z#}f{98L3!-@gu9&x-MX(CG()U*b#-Vg{U=Dd8DNE_v^wBix1TwtKUx1cjTb7jUI)K z`wkms1MKtRNc^5RHAbPae(FV2k9+c0J)yl7GEOLDLOV%;)5uKiKJCq>)xHnS`DFP{ znC`)?NpfAlgN>vjG~bmfBki~jCf;T1a_Tq?oj+0|2*_|^5<9#Zu03O=w$d#p`&D9d zF6MWnt4HkV&PojLx2=$wLC2sE1upG9O76hHu_fsF+*ZfuvN}iSp>u4Jb6Ft7QAKy3 zl9;R2JfWdN0H~vVJs2cVRSZ0|B@`I8? zlg7kk$@lLy{4q807_`ja$jw6dJvoStvn0e?>&|rYSiWY3E2%l@01Z+NM+Mmfhf^6j zIAnVMSRAY?#;j@7FZ65tf631=;{N%pRRMAS$Q-P8lpi+ z?E7ln2PF^_YtvE}BP)qWVDz50J2Fvqg?cm--IKiCbUhGj_`|)oUdOk$h^Pg2cY14##1!)s9M>DA`9_xCZ07@C0 zT~&ui;|EVPR75wR5?M`yI}gS#kP4C@orIDhg)!MF2-ZT(a{&bRp=ARIU^tiDh9#3J z%?E~WC;3Yx$-0VRM7arIRTS*G<6{uX`~}tXt=|$v-M?56SgswWuzq`LbGS%PK<- zM^a>cCdsPL%5}8O>I}(7XXB=6Z=eo!cczArgM{S$aJ?NX&2T+1k7oN`QC5^w>(&+N zyM|wQwI{u4HwM@J)pgt7`TMWC6EaR1WQBH;;XxmL?dr8t>pxhuS`Wq7Ry>9JsA7j_ zq>inqC@d(+`vQ{wjV`h5(@5vSdmwA;;D;!5l150wg>ERP12$UTiJ(ZLh+zWNORo?8 z)BSHkQqA`rL&CV5Fr;)3-O~0m4a#~>)0XbzX{{2^a=}IgI!Np{e=!M<_A_DMOj&pf^%H!WXPrG@7z5B z7LAr4%(~PF7s;+@HT>g%)R~p~Dssox0g*dV z8s$1sV^-7{u1$9Pg4GksQq{_5wizQWs+d+pUu2wMrQ$CEsducw7ZyD$r zt3-gNx+Sr{VKG9)N;5k1!3c)+JNp7|xM82}c_UZMf(P(Y+F{&$Vk(bB8@k9k%tt{o*;`9HBe`Y1eUMt<<;4H#S*8z35 zB`%H9B(X`ugM{Wa*f+>|ARLe@Bq~o%o#P8oFZN(C+l6H$tOIj2*`wyCG_S@XI1OPo zBt*s&olp@lsx0SX4@Du}Mx*V+Q+;TUPfA#ew`rYM4WzINw$KBI-BjtAwh8V%+M;yV zU@=M~BiB38amn0ZNQ9mw%0`n&83%tYcE;n!0p8K_Jg`!&7b!yt(=n?@DneB_s3e3~ zQ^<>!NJEGYi3;M}WO9yVttaBsW!aOkr-d0A z9ieMThm496!<>zho;SQYn_N2umph4 z2h$pLq#TX99_4$d<8oZD2X!5KVmg(+s!HYDbuPC`T8M$w5G4OQppfUF4oVHTw(LJW z@}=ANL&gb(OlT(wuKfFJuV}Q#pHWDAD>i{h4@M0e;%f;7h1seltZ0q844JH$!#IaQ zVK<|LaBNVEO>qY^9j~$$se^kp+k<+!56eg9U@(u*J&EM~0jaYFD<-3kHil!em{GxsD-yTE^Ov())jTPlH=0lG{gj8n=+rc|w}M)7Z^VC^D^r&!t5#R@wWs$h zvFq1r+xM4cq^9&D2=V9_=`;#J4LtcqBfZJt^RMMNIkf1&#gMPzuqSQKIBD<<)Ty5$ zvPB)RP!g*gp{jppTd6VXhg!tM@QX2N(n{sjZH;)KMPHK z&lck7bV02cAxc(jP6(QFkJ=$bKf`0j_3#>OK4%B)xZj2Tbs^>tQSJD9Lj6=(DRC%7 zp%p=u1u`2*6teOJUslik(CY~P9p%Q+>ijJBm-1vn}ob1EE%E*`a7sq&AL#v z!fLPJ&}iX*-wVqhzyA8``;c)$Arsn3fXBZ3m1niuBM-J@#I~T$MWerwhk_q`8Cn|%(b=o;2)VZlKLI(3nZ1t!RFcd}#sb&IDLyCq?jl`HR zXNC=P>9OSe5r?2;M3c&Hry)WMdGtzt%4GbH6E^5LP+boc3DSa*q38<*$ZrRGjETUB0(Dp(~YS=_VgQ^MPFj9(M zjbNt@GIFRTJW+38qpIZBOcWSuRP%)qHD;w%Q=C|nYonZgrXf8m1XUCg>#gg!NL_C5V}WO@)s0U4dK@gM^VLyH?&cvfpGTZzo|*{Fs!HnlAv|Z~%#mLX@O@ z8BVtc)ZIOFu9tJw<0_?y+Q~W``s2@#v?>#8u5?Nm5JJXKqncG5Ss?4sj;z*>C5BZU zx)E^-nQ#L@>xg1DR+=PpZgPN)iY*W7?8cPZIz3ev=Ra#UOIW*QEo?sbY#{d{{uZPF z9MpO*YK={oTs-Z>2*NeqsG2#~OQNvp)&95?yw|L*>3@1>pbkZ0A=O;;8*^M-t-(cA zY{BO*MpG&i!L@8XbiMc{!NKCv@)y7H&Od!4WSo%5gmyB(*a{~vEGc(pLs4{r)@Li> zMaiS^_$`)vvE7sN(K4}Vt1294&c5<8xZ!P|fT@jJeOSvAnpbM<+;Bu-G%AVGp*waQ zbFJpQY^ZBfbp;`W#?C`h0i`8}8Q%E^GNE(gV=Od8Cxx{Q2^H%0g4>U6eC)tO$Q)Vd zEMU^&!M2j3VhxFE7>c>XfC~p8VNcmtLz)58Kxrj^g`r0Y6=JrC)nBy!2bE?ZHXsHU zh%l8|Jo&SPIMc?%xB)SnkYR-sX(c(LnsSgv!iA)EgfuxXSW9@>F{#q-YGu*rFco4n zgMr*gu!O3qT?w-fsa5~iTUhY<)OqOp5}4WwA=8omtGAcqpLX&MxI)gb|(GL8Qu;zjmh$ zX*gs_jYLG1amWbfd^1-rIZz$6iSHRsa zjF1`s!{@Lf7g%fSDgcxGPx%7P1;vJGIQ_Brftx?@Z~l|BVS49jejQI}{-;waxgY_G zBGZ;i>Pj?1thE&55~Kq%*M&*;4UbC{4i4aK7_mY?oPX1NlfI`@&`1&xIDZ6jzBe>S zNI`ONG$w7EnvUl-)9F~K=b;cYDHu_ZJm5hxK$;Maw37cS#GFEe2Aqe|`LBiRf(tW* zx-9pB#84`}Za7s;Y}2P2vynC(xBqTAY^GDt0EC<&=Gu&gWAStBax&mHT4*1v993IU zdnscoSU7kT7WN;4rK7XZIg0%)kHPY>1?tSwU0SC6o?bg6?X8^7LhZlTo%cT<&Uw_s zi2(Ix$O(ZVDlvKBdq~oB!~>z3if=S<25Kcb)9}BIiC(M;lLElMJUm`OhP#UX2 z^I&X*FiHLmC)>pKOre%b!gyLEZmkUiaHwsP@Fv_t5j3j=E#`K?~7)rjkR^u0Y&P^Lg#(UbF76X&eXRF$*U{mRrz&2DfzJA z@{t+;cMRt4^2BxcD9j(6fi8v$J!v5U;(FT5xTx z^76?iwnloFTRSG|jJv595>~UK&X}@n2tlf9QwzU!u`|AgCr^LGecJc2{P72Y0i!h z0|&Dvk(>?cE$)_xvfx_1I)paW!6{}GJj5n>*P4KK8D?FZ7 zbugBI7)7W~OpP64A8)F<*I7wTn^j{Buw`E_P+C$z>cvW#2dXpw{oMCySHR@@t<*-$N~f4w zribDD83<{`LTM%qDPgE2{8+Pv!>nEaJK7&oTs+4aeiu$6jwYxsfXGdC+Rsrsq3}mIM(%QN!hB-Z+96dI6cF&hK_N*$unhc*j#mf&_}lM=&cQ>l;hfXpjE7$i zjpn#thU?BSJV(@MOdtn#(Ah|EN^-#a;X$Yng%lEEyd4?X1GLSQ8cd{ny!J~ZBSamY zc&xbUP!C3rbS(n&iS|muf^_~6&La_EY%Tx@YDtTYb#1-t7uPrsw1$-s2X%SSb-}P= z*jQr+qLONA_0?Nm^5k?F77rhR&ha@PB0K_%{%>~Q5g#U;fzI3_DK+XyuQLJoZ4D$; zNZg70RAmd2YjZCAu_$%X_yRCgkdB{~)OfhyZXnGT?0VoO@Yr8}t>@JS-4GR;N2FYr zki1y(Yay7rvNF$^h8TIj1AN`=f!#7E?alampx=RuAz%6>>6atE4q8Uu*V;eo_@Rmq z_l!APxz@(%T?LaG5p~6n^Ro+I`nUJ}!$Yro*Sn5G#tDy1XeSw7{O)TXzIpd)e^nL} zW7f22z{koooub7`jwj5;1w1uJnDPZ6C^qIe6Xxr}i4bQF%)lpJ_b#ygC74)04flHT z6JTW38o%8US|=Dm5>*zq2OvHU`Po^yvo~=$HE<|-aIJGXUz!Y}CaOTS?z2)8G7Z~s zfkY((7A@Tp-h|ZDVWPd?g;pOX4*o-rl7|`Ep60jYa}F#5XX|RJNgH%JusnN=@>p}8 zsCu5D<_~$o+ItXY@7xFTK2*rW%-ug~=~u|puIO1XN!9Yl^QJjAj7TWaDYI(0Q^^YA zoNe0FIGyYh$+e_DjU|zp^`YB|wbO9*AN&ri-n5kzJVi$xf0sr=d!RLEg^cZ}gs1=_S+U#F2=U3a5b@@uMHegYTBfg2;;V&pEBQWa z==FLFd%nK!iSKy+%l{EFP6}i~I|=dX55M`<)0;N@Ueit*R6j-N8p7eXSWGRUNXyE} zibUgXAVLz!BmUu(^>=>poABv(d>G1l5vDe*feU})@i4x2BT%a=QA+xV2%Yx}t~oOL zt23Jaf)M5kc{Ye4OLhzjnHhJt&%@7Hh}S7$13)sdfP@@Pm?gSSLW@d_0O|6oU=mW0 zU5WVNL>Ed1G_ii6Kd~ z=UY*k{Jso&H20T0%5t}5Fz$Mzq9x%~&> z@U3@JcyMWU0gm3W->a;nbgumYTjWt?Vuul~#{$sVqr>E(Y5)PA|BRLDi_-h_!3_K7{<5sv^v6-RH`O!Qzowxb5>_0e5^BKJ||G!hN6m z6R>6XF2B$yFQ-kBH4JDtAI zj3BsyTJmE&;+92C4$1EFg3ndW!t(q)9N%{cX6`!R!-7ZQ@SZ#2=&k$w=S!qMkdXQ{ zw_mCypGGaN0D?O)Z8praOG7YasMF90iQ!{k8y7nl$+rrD(CFNhnzTrqqTWDqit<^Q zL(OqJtGbR|P)uk?#upT?28JSOPBgF1MpsY6LtplCe{4-MVbH7#(iMb{#YW=`v6Tq~ zhM`7q8fKK{!uY`spROvUe8e;B|PgcHr4N|*!2OidikaN@gnN6ABZ>yAVndU{K zy1Tl%^Y z40UNHDQ=bl7AHZRJvI*?d*i#|_Mv-~Xy7!Rhz87|3a98niqTAoq}rDon$R z>RN>4F$k`NjFiA!j87)Z#r#+j6@<{4gWx!iW%v$V%@+sv+yM(mW}t(_<#Sj^eMs=g z9s6L>pWD*h0u98KkPY^oth9XFtjq(lOQAf$Fq_Zz~snlsW+FF+u1I;KL4%1!LFg619UtWS-H3uyQ-W3xR1m%ZpE zaQ?#|OE)B{m76A*?DS5^9`JE7Tz<R%>eLg%mPn|r-vLS7t`l6`Vz)*#MAZoA}nq*T( zYz`y381k%@MN~04AvyJ@fKS!#m=dHeF`7XkGe!pR<=$xeyfu(n=qJBwp~LDrp7sQs<)dyR2OdS4yZc4PEMdcX$KP z@APl~(ntRJ()Ydo4-Z1dNry~mrvQHMWAFN{H5=FdlC@J9NMeF9vDK8SBdeqZ@orxU z#46#1ycWN|?Eh~5>{sEF|KFc`QkeI5$9-t|4=#AvL*PD-d!!#6C;WierghL1A|A^j zV#9|z#t`{644X$ZeB`L%FDGk|hz8wdICT3RaLk7UkM3i`1uCmUx8DWxGjp)$pI2g8 z@8R5KQb$HzFhnz{x}pGNRfm5SQ5m_QFqF)cl(a*=ibmWZI|$h=7@*|gR3XM<_Gp_o zkp7d51PIA23H;da_hYV1Chk=8M_9*1a@0K6-L5I9DmUV4EWJ8wLvvykJn?tl468Qm zAZ5nw)EpO4{3jPAF2A{&lBY(`7#rT z4u;4rTKx6KqYlb#;9T|1FW>ysw?6w-AB2pP5;=!<3gDK{e(HB7AMwcZN5-2^vUY+7 zw$c_LR3`sT{-+^8U|W4qY9RRvF^gc{Y24qX@4Ej*KF@R*KL6Jr_v6SMEc+bPS3miG zVQJsl=*drqv9+hu^4O-zhTnwbqH6vPXlPISGjbSG-l|k2`Q=v6 zLou%NW$G=B3t_|nxhCP*^e-V`Cu&jWK?@l+x#O@(qH<_-qvHRL9z6P|p3FW387D0= zp`8MF+qG|7x@xw1?qi?+6X%w#?%9^eB{~Q7e;S4amxdt$c)EL?`I_&*DI`dKdIMwr z^?e`z<7A$=`Ey_NgNIi}exSeQ`hWKdstb>L!B0ck#=W6Zi7DhvLw9c>R@Vd=>=ECL2XI+E$xz`h2My@t}LwT z0EWsAv9Be}>niq(?bMatN^r1Z&Qw&)5xTKI};} zKbaavID*IujeZYG7ugZOLOnp2#QScb!^w9w)4~|fP--@vs#0+qtaR} zx_g(L38EHNh0S&=ExNVRV-k4QYeXUKt2q291;z!t2-Ov+7|Klnx1`e6T{MZObP<7M zTWZK4x1odyRY7XGfT}N6W{DFs-9|!)Ol-r-P;+@&Jg>PoVQ4ycB>h55Hw@?Jh{(J$ zE1EvkI64lG{hhbKa2Fdp+l(C3TPM{Fol%5b0b=O^ zB;iQ9!b6HnO&>Y~bQRPbm{`Of39$~#*AkKq-znXkSjwB(vI%8t1nzzHb7A*my`r|9 zCo4*64h%E_fOORtLx;{ZP1nQGTxUi)N#kgb@(1Se$;{B==_DSsQp)XEqYGg zi^dR03bN&QkPsh)-w$^{K74TIuU`L%XFVCRIy>oMA>)+7&s_2B_jiuZz1?7Wv*X=;ZE45w^yTSMU>z)ts@!z1|Bb9~A;{c7%Yf);I z6fTnr!}mprDd8dEaNa4JBCJe_g5<>A>p|2{9n3IcF>= zY{|<1B$|x+q`Y~6WLbEG`mpeND9dDGPLZRNGrH{m09?PszJb!tYMQpq$f4gY%jw5`-G z>Gm6L^#ff8r{^Fe0sh+19MN6q0=X1Dncx7gCJV~Jh>FmHWCZS88}F)m<7K3&Z4!Hn z?XY+~n>0KQ7eLgGIxLkSRLv6%9)nuqdFzskG&b1eQyaf3 z;+PMUdDDz4EC?!#&dVkvw-7XLlkNBbI`>;t*xa4O7NI(x^PqU_metwF)HFQc#jk_W z=?zj-hBqpO)Ne^^t6GdGwT3sPghQu^(eNWjBB9)I6pG0uNKY=f6Xms4^$)|a5-Qc} zZ&k$x!gHb9j9iHjs;pI^`4#uwa_EKcc;2h7gN#!Gnb1x_Tz}p5{mUQ!$j^>Xjy=(< zjaAk)c(9cLA`L$(aRqxnKnRz`=Q3zpZVHUuB-WpC8XUasE+2Y4?g#!3=ehhi(`XgY zTkd%UHh|zN6!NO1u1iB>5W;IFs10r5q@gvnvenW$%1S;AgiyGPebP@-=2BC=K%W&h zkRlDP4ptPKM~l7Yyh7-%j-}hN>qi`P6}-I|3R7pHhC)>}Dw$d#uQ0O!B>U9U*5j24 zRq?#3cFx$OZzHQzoh@7o+;JuYbTis!%M)P>c0cYJu;tzl7gwulClee0IxW&>^~HY@ zg5)FR(hE`fZfXHqp3uxN_ktRb0>XK4xJc8oL}I)PZnY)L2I2xfF1G4RNIG0$Cc9&Y zj{n_%d;B%O02!wYa>(%%#n1f2v%huQ*S`6auJ*A%qjqYEuZ00k-Ze2lq$H;A$J!_e zZHyZJan#@aU-{}6z^V-!plnQ0*LE7T{9w9j{X|eWs?fM4RI***!5nu>F2+K_d@{i6 zzSZ)s`LIg!a==Bskzh>7m&v$GBtVULLq5(1)u!a9fi1TBvs-MF)7D4=qg3A|v9;>1tH{r1e(e@MSoxNYU^%>$Ihn zhR9qzM23?zmc5L*g0HpZslURVDdm_}Rd_>hDW`G$gsRpvS4?*gP3q-L>9 z%#wN8)Hv-36zxGc9#u}^$~u?)gjh}KCAUEd8yL#F=}*DI$~{W%OnYfkk4QIrWd1e( z>sdedb;vlSkO}P+$8#R`l>d3`$k9J@un-9?$u}WfGSKLMHD94j5pyF9{9)k?Y3sLa zgezY5(=al+#uM5I<#}*h&5RUz_a({P}4&KCtn*TZtA z$Wq@#38_k6Es&3#BOa?3iDOe@Z7Li>j840M)M3Br1fH=xfQdkF2Jl#!aCwe13^|^8 z`IWGG%Pw*>(orXQFg6LsKq1ZOCZR@EMzzzZugqLM*@1Gyyj;NKHYW+$YW^}MI{mB; zsy2R-Mg?SLp)`$r%4~CvyT>dfodaWCP1lAS+fIX~abr6tYHTNsZKJVIY#S%GjmEas zG`8*UJn#1p_UxIp*1a%u(bMZcjsjl(ynfqF5~XTHZrM2QEL=YJ2AcCZho40BOCy7h zX(;nYYmXc*eg-5po%-VSzLUH%D_B&F%7qpJ3t4$tAa||Bg^v{Nw}PC?RPL!;4%v}f z(DQ%{t3L2jPEXMq#9}S%XiHHblK;su6e?E6CWw z*IpnBv_MN{gG6jd%fsPS8*|UKCnk$O<}WoFQEEBk3()%`sSKgiLX|e#(r){1|2VU; z0@6*{M#|3*Up7{6g}(}ZeMa?2G1se^qty@V`nI83+aF&e!O-Mo29MR{5U#e%<=<Kui*kcA82#1p@v2+!aJ%Iy7W8qsJ^hBT;!)$AznW3Zuh5TH zyHaIjQ-ER`86M1a9N2;3tj1e}_>~$FzZu3sQoi5i3%aCiWQnDKBq50?{l?(?tSarF zS4!$4UJMg1*DBQE)?cplR#~#ysW9m>3*q>p5jw59p%1@a+GG|aNAN<7oZzkKiK3(i z8<0k+3D-m!nK&7i_(|izor~X_nGDKiA!sarA6I+e`N>5vAzgH))jBMq(YBCi@<)78 zmZ_HJTzXc{M594LT}X#Cnf$J^GwJ9ID>eE0o@wu52rb!*K;mi9gWrf*}i|p%}=I8vVdQ zjm|eh%`V3VQdNz3UXj3Fg47Z&+23HE=3#OrM!lnGwJ@s8qLcfD$Y09fS1VU3k&(#l z&xI-kc|OSz1I=N*s0=t~GH1A>7xoTQAelkzs7YEEaq@%JAB|w2ZT>{XjTy(FXH!w zCPZRWrX19N!zh#QvDWc3LRecj3l*GqRXsm`rKRrJb1%3zo#i6zaen#9L%!9~JuRj# z13IyDze?_Tt7_>1mvgniv{3lpqgc7mT(@k)e|7p3OehzMhGrmaXtMSjsZBOwq(~ME zH*b*SYcc{W{5}SmIh&Re2@^DfR0)Br>Q$NWKceK&WO*|>pzy4_xJipV1iIE!-?i18 zj9^;AY}wj?WE-EB2R)Vhj5bIzB9&lUjwWZ_>J?X<0@;ViiX ztgL;E4xRt{r!?)6L8?P75FZ_jm5W^0m3-bh$6M9d@_pYsVOieED*r2x z=w}+dN{LPxg(!JYD=sq>hD>r6dGJeBoJ4S14Wb7Jh=Rg4*G9R}4s@GvYt?!Al!K5e zqm~Zjd#=BARo{)OE-5U?AN(yRBu(=4iU^*GUB(ZI^gB7sXW^A4X@S{GgobMz8^vL0Q%`&{ZNWT$Wq2Ow+J%lKuC~~}c@jdj^X(WC?)1;Zd7Tb^ z9ETVJ!ZVK4(R>jQ-9MP0i1yote13$oB#UeY0wYOBDbx4$J|Nxjw#+$P*`}7PGGtsc za7U}im?mK{eTpg%8Y>L9ksN+0D;e8rbQwlubWxLKeW@l-NERd@KVFgxm z1c&9u-Wiw-*LC>ZT0XITK+fT6cs7y!5Kd+6P#%!#j>)-s)IdmQaEI0Z=m5O^xdels zz&$tu_kN_@t3Gls`?cFB-zJwvpdF@_$Ox)!sfO)3j5+__4`mZnWmJSYgt;S-r)CmOfQa{-=D5iD zrdR)?)3oGm&2>xPE9nUVW_7>?8xe5`jw_XuZ%aaE(BHh1t&i23uBT!&zuzRR@*ZFh z&??cnV>#Wm{jcSr8{L3_4V%A_;s-2dHR7f4m_F)Ui0IHr&x0q#koVuS+38Q(mCjoi z1*=3w&deG5)%RAx#>$wk63#CY3N)@4JPi zzb3Gc9jq%+#vNk97xOPv;4CJGQ92lR?`L0XHQ(gmAf*$nOu}u4RfUg9jD&v^&Z$Bo zPlmE6NaQ-V*72~yJbsjEybe36w^6ydhx=jvknNF$BI}gmf@TdxB2hA4M^3`h5we}v z4|48@F0qQeCMaK3%VM}^+1)a+B|~WR&o>E z5z;gAs&v%W>~Jp4hinOW@PrRmgPRX_L!0fg@=3!9&DXxGavM^RR5)#E)lF$$4ph9v zz18HT!L72L-^c`B%n{`P`NOkp&ZNq=NB+&Yuv`WUJ(lDkB(|lIC&-h3TaUGqz#7E0 zjRmK;tE&KCO4(dDL)A!Wy*xvH-@S$OADd6QyBEKui9FmJkwEJ)p%4^qo&hw2I&Vb| zf|EY4TjMS7QL)YPH>ozTWriO4&bth`PZRb+;=pfQ)KgDrEMCL;3gATiv8{5pz9h&p zU`d}xjljDDRaDNy)qvA%@Kt<>*Oz{lrrr}%-Bue-|8qhE!aTfilw66HLz+<6YdlCy z>*szDh&Y7EHVdhe{_c>70w3+7^lvTh_)kob$EYfb7E7Kaeto$S8RzAa&>Bwe=mgMN zSd+!Dv|#QrDJrWpr4-hXM#5Z_kBRCyFHnwmoI_!bKnD9HAD!tC4JDLt99CiD_|~j~ z^TPj?|FPxxzP4JvFCV?X;W%0XnG2GpZd<|NZllXU!Yc_cnsoTVv}|OaSLpd56j@A^ zOk!2SJI)$cWN3B?fUny1&h^)EQorlQRX->SSr|;|`9$A(S!#JP&uKU%y$$1i7Gv0=`2c^X=qqUW(5{ z3q)Wda_C`#;UAy{z3@3iGJq{CeOt3d*|PDDXv7{ye^7f4uCDWW2EvpCYCyYUVsbeO zrRc>dzJ85OIdTCy2cp!g+qK7e0s&O;UFkqR`eQM!tBPIxAvfDfg`){3$^ zISJAAw3M7of^Pqgy#9I&B5gqv5Fq?VkOy2d`F+MBJ^K79($DzYc|bdq#IT?)FJ%HF zJV?eY2s`KhbR_#m1a}hMF%{hJe(}f?5Atn23DK?=?||6c@79p%o5Q?$ zPjDN?r&ex><#jHHxU?r)b}TdYM8ZA8rGngdS>BtncoAy?(W+>V7MB+sHKe*Y0uf0y zQHQ29p(Og#e-93#g_@z=5%J8S3x(*lKr&lQ=_*ss0QO;tU#?0JCpxx_qw7ljrb42| zqub)7RytkB?5%qlTY>eni1|Mge=xOCxl{8BqFGb)0Jn9OV$U1UgpBLQ53|toktjHI zmU>fNpZLgmxkgF$d5cevN_e$O?AjnT&R<__r$0wz{LZ(?$c|Z`*qrlL_8c&3%s2X5B zQAsTk>H4%Mj)rCb1#-CN=z*Ag;D+9}RJMgCGdBzFysHOxX2c;&coHXljTQFVG!hX+ zNavAU$jEv`j9A#WM&W2Tq%7fKRWmgtQ7TzwhZ}yJ?!26?eo06BABmZ+G3pYYt!3jm z9EtSBUm|tVh|S%Co#l678$)ZT1f1x&^b9@!&I}MRv{X);w$@70eVtzO5|H#Nr1(_D zym0C0kTt;~O7`6MVez>o&SPBuy2K1|uE+gIZ!doUO90_x$8hzv+rT7LM9RC_EMLaJh3Y7fsQg#YwevN%j77015KC!Wq38N2E1op)0fNe1uN$ zrlIGCc4Xv<^FOF5@op~Qp>FrF>#W{h-bTl{s8Yg3bz-D;+AiL~;R<3EN^F_Vu~JJ_Bncibe&fQq~MAX4ww>CZ>~(D&C%L7d;lR-Oi1R^3!|ik?6Ww zt=+zxe+MJt279^xkPFLJWyh&&hSS2BFBCa&NAUijz!cf2T5jQMkm{SXv^w$ zFn2UX)o$?34iNq7Gxla53jTgs`ps6=3pN3#@cKILoYVpqQgmR#)m#lT2U_Xp1Zcy3 zR*CDKD(ik%qp!Vz@n9VZg+m+zIX-DW*&D8W=gu?aSDZMhxB_5oc(ks+BNR8$Uf5o~ z^j4DfwaKj}N>@sjIk|PODWG(QU19wa?P2R?n(GZx$Ipk9mGuM4BqZfp*Vy6csOPeC zZzp?S(t}B(g{P7xUz{Vj5ZoBi+wI~@3_G&e^bOK=?W(h}8Yu#?+e^d2b1I zY<`p{uok@ER-Hy{G#pzBQ)DNH_4B2BcAK>mW8_CEwsokJq{$dOR=^V3vYLgyWJA3_ z5u~N^)Kr>eMk9uQr&4)IdWqXE&-+Dfk%4kCnOLZ2zh%ZCiZZ4xEeIjpcb~ zVGV<~^ybk4*#Q5u=xSa(h6wB3B zNkfIuH~Mb48cKSt8VVuZ;cH!AXCq81F1uiR-;Z(MhL!$WQYR_X`Mx<3yx%giV(22~ zoO`K48_v?d>sH#}bm8F6jOADAYgAwjIUggtTtGJ15 z=hS6>*^s%|wYXSi<*l$c5+r4los#XpGA|q@6A%ht+@5Xua|rqsKeOtoVaG{v(@oJV4&%D&UmO^?$@@FtRwl52!N%WOUp3sE6#Y&@Ak!m0q(UoB~^f$NzW z2M79_!upOZzY?BGGJ$1up_b~n zDIw@|MHcHb!IiNKQh?YsO>C$OXZYjrG%MAr0@{ywpkG@{7~c!eA)=S5DKBZNKn8;6pbKANk)_k znpq*7l<$wjlqh;Z-p*a6` zE^I3Qtw#PV3CcPf=RvhgP#K1pb@!f{s(E1ie2kE_C&Gdo>AL1%MuVakEEP@d_KYcg zMyxq)$fCoePF4Ur)A7HNmWQ5$py|)Sp8wWN0baAW0<+d4mp%XETfIdxn%m6P-oWFD zYgKvVWfvWFqG2HFi--ls;pzgt@-hp%RFmXKL-b)b+^bzXK}rnMm*(N`uO0dj<{cNV z;50V!=X8?{Ql^IiiWv%Az&Fk0!h9hmr~E7JK4axnpy5#53kEEUsJsj+-~>AFSe26~ z#U#{d?LBD0AmenNoKt7I*knRifnMv5Wnsl4rmrjany%Jr*be;zM60)ltr<>5fxC!N zDa(X7>KQH2jlA1|DoiH8cdy9}z*S3;8DacUlXSJ$&gVh8FjX^vYcDkCKrT2%oc>+{ z@I7MFha@1_g81Kx9d@I++UH+5woIhl;183;d0 zXfGy&*zBU(8S6(*kS@>H#x>1tu;QR`Ws&J*&4p*OrIC%CMMo+{{?D+{)q|M`#fs? z`&>d$mBdT0qv-hxe{ZwXghDzQ=6qa10$v5LlH-S?B3rNO+z%wUJ|`QZAubyrU-zBq zr{2~l3XNnq6fcmdjtzje<9e2&kl(`!{3^PlI3jb>_Gb{4h?8uPnKXljdss>^{k`bECg@ z_!Pm#o|Da1{Brl}HPgg-6>GQg9xvEw)A~M-Nn8RP#<_C0&Y33Fa~xi9{`gx>BLk@J zEx1cj_grgsBy>nl7hpN?c+h@yJ0CJrESK2!EQdJsf^z@CJ*b(4{UZUwG*Yj9#2&uQ zjqCKA9O>4nBWrYeb8XVG^8#&Ct)_oq88*?x1@jrYIQ{+zZ?qt4v1} ztV%I7E!)J1$;L*1;&1Xx1c~IAAhEdMC(%-nDS42bP9RUmA1LpzDIv74th3q$+?qYD z_QkhN6LnoIwSe`vdq5>vq8vUQe%|(WrRv{LbQL2gxKvvT$$pa$RY)zZ*M={|rSoGB z8`#KXJk(LOvYuK)k+^6jK0qVAwRcN1fW_ILhs_~|d-oi9h@rz}CFdmXCY>O+5Zc_whlA-;ufBJ||d+GX7UmdJR-~ z8|CYH_(QEDu!&F0VGd)Xjk9OE@ zR(oW)^wcr!>dw(+7z1B_SisK~vzUEUz3CqrCDUXyt>f|iQYibzQNptJt9XQ*86Th9 z(UE=UV?=5vSa@~IfGdYjzP<1Bxm(xQq+V-5Oplco;^__{G= z=SKZ3cI0_zr@Yj9bqZ=ltgNUIAknw5A^}DLkjT@L5A4Ne0giq#=19_wz-ep3-M_qgpCdx3kohlxU)aZ&OP0W=U z+8|?6@igNL1&hrJW11COL$kWdI?eklT=D(V*7@j5{ren{dhz@gE%4u_f*S?9uVDiB z9%$X4)ES_8g-58H-)dYd+cd#HdDszIG8Em5e=e7p>RF9cKGHC!{2Yau*tA%S53C%Q~z(?-TAL*rRZaWj+{j_{(>&G1}pSbZSK%~_7)p8cLt_>-Orz%hY5Gk{iF z_y_zwz=H=*Kk*kud**zZ!AjG0%GaXF^e>lSyrj0&QM_QhHD+JPOZV9pB(j`}an=E? zuA(a)!%I;4iTA0q{^w}V`#zu*2du1-{&UhkgNK4Q@okZ$KKrQ=@dt^~ww$!oRL~)X zgW}<(OIBoTL??0oG@Gt(l7FZDcqV$4{s&ogpDECbVsJ6V2Vvgsmh93(lSWf_l(8L{ z$>f&Gl`pBS8`V4ml#}=Q7U197U4`1#iZ6mSq?SNf|J<-GW}Xpq+`i37DXyDNW)up3 zQ{dA#$mRt!|Mg#j`8y%I1oaVrCALBbMNA2nD{#kwCl>$0`MT#LI$o}Ux!VK61elYr z7sZKyFWzFbV7!zbrx7^``&duB!!So)JK#=T);v1E`jP)^O0a=%b zYF@W}<7SuJ`C%2107B?cTBRX;@1zt9UViO39NA=U3Mu!QuJ+&#$=|9%&MC_2OzcLg zAi^HAz*K+o8#c!M^jC79v~B}Z?E20|{o?(_eRxJ*d=ERe)Ro)an|cXPE_P-hKBSzp zd4Q@w`n~5O*XOyy=LL_u?+H>QIDOCu_dlaZp*KoCZ#Hw=pRN@)4Ydb<%12g*WYGY` zj~QyF#CTOrAnSUO1B2d#Y>txm7W?!E$IS2UoDJ=!Cu_aEr+T8E*<1wO@Lua~L!nr_ z7o_mecx9DrSSZTIVdwzECw=vCdsBgPn8`&x-&Fd;fe>;l-Abb(r`q9^1(~xlvQya} ztn1#n5`7mEfKcltf2nZyq4KC_&Sc5>KT-50O7gaPoiCkTGgfK^Ls@n)PR31{P_7EE4{W8F3 zynl&`C2u#;5DI|k4)>)iW3*i2-W*8sinFYLU*v{zIv*E%Rj2Q0tEq3b46D4p zKtvGKP~!~|Rk^l@U>GQwFC=mmTfrGKJlZQXfm#52S{O4yQ7uMY6GSA_2!D8eg5D81 zKSJ?Yh#Xu?-cr3YU5bk0>~4f=n1JAO?1B3Qfs0eyHchh!=E8NKPRKZn%!LM8CL2(R zMRp|8O@GxQ(-(EKH9N`1%()t(jFXHtr(A#u72|q+o7Zj91{t0@QhBh(_C-4ueu9$2 za&*zyXm3}KoBgx;QQ&m{6B#TY$^T1%q54|{Z@2feAG_bMCd;lX(&S}j*rF%~fvQ|w_pQ~fkZ+@-yT_&^8RCncuKMKwgmkf)n zpnRjX#zjA!F{+-VI2D;FnZ0;x?|YU8^8#D9h&OJS{O&ZSTX$s?#`1@vE(!iihlH*d zWBn}54xwsVO--!MXq)J&<_%J@RdE^MOTQ?Kee`LT+8f{*xlPyrsiZVvveg`qW=XZ2P>RzJoVH`XAtJcCesqz7) z2Zvx*30pzmtylu$ek2uGOD3rbZwk>Xpr|w(ocfT$bdWknWB(Rq_JKYO3*c6ZoGXEX zmmflx-lx1$T{{1Z2HN$h+l1mFNKY~d%K~maazSJMu;r$DPzI#RC`sqcOaHEqL&DZJ zo?Q-uxioVaFR}B!2(5YF^a}+aW3vAi`^T|=`0r!1Gs%mIMHC0AwrHkqENeg84@PzZ z)-s;@d*^7vo`WeeoyZ}I?>?lWq>}0wRdOYweh=LAq=I|lU(9t5MtL)fm1(y>p~+95LB;%dMG!Ru&kk!a z`p<9#9z3V13g-_5` z%AqCL){KQ7=v>EQAH{fM`p zL+t|((CwMDW~}D(purnHA5uIB6CBRRV^cEFpXfI?kS}d!^@YW;osiR(dHbX=+d!_M zT-L%h#NM!rXx85p3!dZ)@auH)kZ z<6$jBVH;ke_N|}^UIW$ZdX_?eis&?aL8E1Kz3x8o$vF0PR!niA_JTG_w)|&|KyK?c zPA#imM*Ovakh%o{0Q;=%q6W^+>4tR49`Gtn6Ul+9o#z?UewDfMS?7eFV_hvL{z#?* ztY$*>a>&6RQs5(v*}UR$2+Hy68Q-q!`@I;HE#_(S1%q`SU-xLPHySN8Oq3lnF`>L! zgb+KDuwl-zkPunq5DfZU14gPDl*)nj(u%UQwnd}YFFrL_?vJbk`=qU!L2L1kjS|Kq z*_0RJND1z@(=FMq*i=-nh3#>rgdzh)=*pshnFgF5ua_!lE~q>;bq`0#B5 zM2ZXKysu4KEJ$t+Oo*#68oFM{gw|aB*EN0wny=w9f-Ypr;`e2>Td07&7-zO?mlX zZ{ErChSmg};{~#i7h(ra`9PWT)CgY!6zH(PhVaAwpsyN8l z7C+ktN^bP~GaV%fo=6^;Fibg7Djpxl+rYvPhi#N(X|<ZrinXi5*RaD zuu8!;Qcc~7y_RTgjIWrLnw%xrC6D|K{Q)iCY&x6;%DjegTrkURDR4Sc>-#rvn=njk z%-($M{>Qiba=G7PfmX{YB#v#$DgE9A@u|CSno{MHV{UKO{jp+638|+H_o&2OJ_n{? zN#G;)-vM>;KDYlo1L~#%>K2JeJ!)C1{<#rZwU~VK7>tqpL4(@gje(ab# z1=9tB%kz6nBRxyi^zblUBC<6KsF^NPip88EC<|@h7M1l2u_KYcG2v)gU)*1SC~M=loidsE<(S#;#iACSRZF& z-%m3AT&HscIQ}!X{{*W9BpjM=5rD&I2_S#I;@L!dMgiO1sM%Iat0Y`IJRDDg=@*7C zib*N9{>N}RacJbh+2FxCw|G>jYM*MTbb*i@MZrpdiVcODN*pz$owi>rUm1M#1Sbct zKtzU`v3ik3Td}j7%C^-wfzFiQv1*G|o2gj5(3&5S^Kt&pqQBGY7T;&*}8nMi7 z7AfM)db?7~hMU9DD61*$+K)HVAm^N?@81pSD$yrNX?xpnrsREX`&=O{InVyRvEg=# zR8a6+P#D@7D~N-Fhkl?029R^&FV)uk&{dJ4QJvsY_u?ZYV}(1uclLTD@jbWKzd!2< z0!Okh|1Csv`zxu}R?a4x-`K2C;k5-EO2wq-pb-XiI%*3qP+3oIQ3f7Pysu1$sPk(D z*Pp!gko|Y=P1$9p{%ldiO1RtcdtUw$ZM9`g!X!6MW$WTtPkgI|nNED|XY1n~@O=E* zidwEd-h$GW$DCb_HriwNfwo0#)efXpB6?DY#oM3HkUZTr1PUN^c;j2r=|P{wTaKvo zqyo^{{c!JeX9G-bAHe)T>nuCi$IwFcq_^pT2+<%<_I6%)CXTp5=QsjK2`7PH#8M~- zXrkVxK}lNS`9rzCc`8?3j}@)yTpN$sJ?JmwM2g)?>j+0nk&X*#OMAxu`$lh%sT@rx_QGpLqR_n{0Ya(>g)esUQQm~?`3yp z+w%|NvxP(8o&6U!2>T4v)kqYdkrx>wsZV+X`@7RO#T<-!wmF)X)xhxV$H3Lh9!NEJ zg4!#>fbHRWwHYF-J8gKn3C8SJyr#iqkwpR@s`>t4bV@sozNqLW_{L)I(rc|A#K~(8 z9scYvc}f->;|csVCw#~NY^sArhp!dlbgk1Drb^b9)ZU3MV=YoN@wY}9P;4SP*X2bg zlgJqP?E?M$j!uQchlhaMnAFcXq=(C`T-|5_fZ|@?NffD;_uiUyH%rUmlDXRA4r>Ni zBNN#{DUCZ-OrJKx8EgVXZe%P3x9XwJSwLyX@=9uR?hu38nR!V_h) zoVEPIf=oIXrPb@FOn&D`T-`e_r!|-0`X(B_)_<12`!)CJ4?_pZ{avw!EZTJf;x{D( zmbK*eug04h#py@M;nL(&{FBd34|h~FYoJ<(*A@p!SQON?xx z@KJ<|6@%at2&(6YZ%R!7^}J-=}g9(QVj;2HFUBl@F=4LlG zowu{C;Gq!;3uu*TsD&pbv_x~EIb1PNg(ii7fOdaTM@M0GgpgT$AQCe}@e~gStcm&i5si1> zfuvuy`TaTqm+91BKL~jGZNa=QS_d)TvWy!HIJt<^W_qhs!>xT*nhuDUGZ#^+rlJf{ znK5YCF1t@;A4I~yvK@kPNBB|cQm?r2%WtN{`>Q)_y!@M>yP1i0+BupJY*gjnIBZ3< zP;4G=!fBL6vJhNr>cc6dI+)x;cCUXIlor@9!3bR00T!YG+@n)Q>+G0!$kf^Yz8nq(6+KKPdUO+T9Nq=hlvWT86xV}|eI(IQhwpL+*1u#z@9Dp^&TcL63f>L7>zI-E(NLv)j>Kld5>UN~T4A9am&S2(8QW z`V8kabGuIY@atv!fKs-wVR@K!CwbtlWrg^QL$77r-ehB<1O=NLrwBYBA&B!m?NnlkI!YaPRhq6&SPng)}%Rvi$0_$-A-tyc^y z&t*?*JeIsVLbDc6`-C~)B>rtscR{P?x&o*`h5F{g=zyq%k8h@C;y^ez+Q)VN21{)1khD!p2WEuG-2|>pH!76O z;yBy=R_*x_;}g)&UymynEg*Z1`OW9QxY;kFMzg4nj3HqvJCiNL)a!hc^=H;PnPI_~ z%s{3d#$HRh`o-3OXY{%TZ|9)P=+CYKqC-oTq7$_%tnm;l zP6vEOx~Lfx*0ebD1-c2R?mgI8!$(-<59lgRlbC_g4BwFl(fi0G=!O9t%Kd{S9R`p} zeLDa=?9}cWikiI;=M<$WIWJ})b3r9}toouhid1QQE|be^3-PM+o!o!l9QfZ~rVzZE z3nk%hzl@=S4SEh~zi>*c#uda!1STLi#g&8B%B=7r>hI%IQ;A{Ag6&YHgS?4a z-tImOzQD0lqyf120ye|2|NJ;YWbAuia;_G))7@c(=cwAfznUg7rFiW_h1S5&SFbf6 zkXyeQCM9N8XR$yR0jIzYA@iiGo?)~SH3rej`Z=y_H8C9GWsyL|$YPkQIprFaq0`~d z+FddQIa!jJLpnBT^;u*{>B_!}Q9~IOLv@ReC3nNqjPGdzvKf5y_{D5=l5@d|M*LcF z#$m&x*n|`jY;oBn)PH7oZ!e$!Wd6Nl0lPU2k*R#steK~w;g3BebHTeYl%E}r`bZ3t zgqr@S<4edT0Ty4ltVke6H3+_=P%{e0?OV2|tDPR-UIeV?m;ZMk5T(clSFE6GKLU@F z)v`pxOw)deC_ON;`WeOIInKEDFf*-tCxaWydiO4m2$?`Lik)Prb5*qhD50k)sV2XBBmV?Cvsec*svti+M>!yuK=QHZ@BleR84J?2~z*@_( z&v+aL?Pd9)d=ZVF&@I&2G)s=Kkj3G5a`0O43Z$by@G}q}q2?Me`PtLO7tmbs1C9IR z+&Pr1r!zGU8WUOk8=_JHH2inS?`zUF<$aLWj$f=r7p;i3skMU`OXQJcOtr?fU4O_I zjTEw@OP*rl;Gm?@BzF2s!KdpbaBX=_UF1yhy#AGpgc{%L`iymLd(3ga^y1Q@d%p2` zUEdPi!M8kHCy(BY&0Jn*3Goeet^)m-EGsrC+Zr}|%vu$X)9zA<(entO;QMHFf@-ot z!`z=nk}{U@U_qi3`O0|3`MC7>H8Ze5|vq0l~f z>|1NJzajnQ03`MPIbW`~eLGN)n%tQ7+J2q`PhLt5yy(Ii3-XvOZTPx#mvWklb9r-@ zjsDvysy}MfNGTGVWiajdy_KGDqs|%hL{s9T&fY@A*Fqb4puSNf5%KH87L;JXs1pmN z7GEZXWhHkR)?N{5IO;2aY$&-XLOfD)l=7N`R3+f zI(?bd@TD|AirAQwwoImHBi>}sR8Kv>gj|G=ZS#EHX(^V;-F9?>eEOU*=l;^P4U}Y8 zhv=C@eBm0I)N~&GJLpmB@y|y61b#B1t_RcS-}IE<>so(P6YR6p$=FV7N}Q&Y8Sbsp zO<$&$a-yF-ld|Xh9#EFvNEz&qi zz$4~p^nyNW>X+xdzv{KU*)y;QJH6i$OP6?c>q?iJVewq0+nL;5q)h+86Zkj;A(&X! zmC%N`EDA!T8_n%7b#Esz2ggAdfOxAY}Yw1tPyxEuTSc~=MlN$Ffni98wf$Ttr{qv($6rOvnrqX!P z7GDcmD7Ck@&+(l%I12%GcHgcT>BMysuQwn->v?Wmj1ljj!5|@Far*t_;geQkGY(KfryBS&t08(L10*?Bct38_BA^14gg zR7wrTNhJ+FZcmE0zHU+^jy~N(mu0Sx)LmD^7TWCK!DYCa@nx*<&F!M{of;$@H7mO; z-@5OUKDmUT^PukiYG~Kk0*n`C88Dz%cwK+|CxXIH^OAka&f1}rTn)3D{UF~CJf_5J zThRnL*>nR=nS-1U@pRWcw(%v8*})Wu*BsT?$*J_IY5w`2P?2Na%qkZ!WvZJ#D)IpJ z(fClc_ro_z+M?;=H6{{GH~Qtvf|{cx)X}B9!`$`hw|}Cso%`>2x>pPks7})UmM0q= z`fYjk*DU(d33@8e70GaxKNg#NG?Tsh`Z#c;lkF0nNhTr^vJh9J5@-`YINa=0GiXKs zYy3t&w6^)Dgb&OMvFhfG7Q3eLS2h@(R~E3|GEK_jPR`YzEL>c96t>#y!X+9V7!j?Z z(y+at>2XHxZIw_y9hXiVUHa9z4&Q6&fArp-x3Yl02bx{hn(1>m6R^Z*{^6A{=I^9( zXiFd$vfah-Izypz-Af<6G_ljfMW+fI>0cr;qpY+*%Ow{n7O9e8Z#1<$mW0}|r;|Pl zsG-p2l~eS(N9r(9S7;h3G#Wg#o1WQ0uH5&)9_X7dXkkHX9Q^fmNR*ki)@J(qlGdL0 zb|Msp8(Pt6pmB-k^08-~t7g|kO)f@#PRZ<7f%cf~Px7#&)nqZ-*Zo8Xylw@IepQT; zF^*Pxx~1bcEwk-bOMvA{GOnv8-T2%D9M>)PY+06vj$96Bjh=TiD|2}Pd0kWR6EuXJ z%j~y=n%KeDNo~Ea2fDx}_V<+-fy=hNO15Q0hp=yXhT7}C)2`7ut1?R{^qy-!uv`u- zbdgStKA7w^Fc*G|$y1nUmTO+t&x`L=_x>&q6}ud-znPKWN+J`KJVvjyQBUEVY}cY% zgJ!l}8nKC+XADbUPCMjGBjzaGZ!GE>9Kc#kAwSSckAeKL#Sedf@)HKG*_1K5=Zy$wggJ8+#}&n0`yVvd*z$ANFhA zE~$B&D*-g=`isItps@9Uw)9(EXCqPt)>hZMq}PSjJ@V@HU1nu)Hht7*8pl zx7>0=PV*Zrpi)pcn}gGyy_U`F15<*%Ue_uO1Ak?-=N0P+HFzNrGM0AzYh~2xtmTuP zj(w}AioGJBG|8%BVhGYBs&rvNpd@iGimX|$yvd*ZCHQoKG~FTZFNzHQjG>^@W_$jZ zsKgxg<4Qq=)AmbGdO|>-w#rkaAb?G7-|zYGbx786&i?hLm!2bF6t2}tmp(dBD}z%v zeYPrf_1n@!-)%9PL-T}%oe>G?-&`<1WI84W6fS0MF{dnxtRFKLY8z?6Fuc20W;yF> z8OoJ9ZeQH{dBX~-!JOIu@fgM)Q`-XbU&s2v?YLqn_WqFLLhB3AHqoG(t2Y! z(tYD(L^9x_iFRvVW=gg@CSu9r3^8gdL}k%7`%1MgEq66HS?m|B%V%9I7}l1F=HRwD z0o#L`JT|WHkal;!IF6zE9GH><^;O{J53XrCTw@TnR*qc-=g_IyM+hgs0w>{XkKgjp z^5)g020#5DvM_%jf$4g&y#k}A@6;o2yO(;#nTg`5w5HhZ zRb9giNpkil?K2hEKA<6KlF<}RGRaqJm1d4`;Vr+Q;~~cT3_M!#68I&Q29nS$EB~|2 zZuzaqd~1`Z;_!P_V#KLM5T z&9ZW0F6*A!{Bk`$@}OY(sV;Jm`_|KbQ|e!@6w1g7Iwqu0Q%p^Iv zmoaR`IoPmlb4~82RH6-`VH-Os8|E%E_pkHw_h0zFzrMe`&-;0w&-1>Y=Y8HcMnUy1 zeAinzx$&o6e+;dVI2%oS*eY~rnf;+wfs zm1y6v?x)5RO#r_%!9OcCG zH%s?-MJ@*aYxxct=UeRHxg6Uv_9tr9U0d_a>=d zIrMBaSpviL1XjLE!h08%3a?qt#kcziYSAK{A*kpyuGQA`DQvFU%&ym1g-#M7mmY`O zZe-)?J!1o7V|N@fiIir&s#m2hjh+~vzEmV87YP$kS#k@LP~1|=@_e2g2IOZS^o%my zz1r)t$uA%H5y$#hZZ^5Oki&O9#5YQs?{3SLlOD z6{#+)n$giV+2GNQwroJUS5Bl4YtZZSDrA^Ay!B{m<)H%ofTDJ{KNqzz{^B?pu<(R1 zl+_yXim`KBuG_oA=;hOA&Si-%{^DGlxq7j-;J0f4ctK_U?szbD!Yo}vNeL$JM9XZc zr8L%BK&3@Xgn&5)I`(CPqVX&9By=TBbMx^?Q$W;z%zROTi0L{plNc;>7)^dO{)iB8 zfx2iJvmoih^Qv&(XE)2Uc^I>Sa;e8LhtS`2JLm6+$4no2*D?v?_g4tR;%|zs73%y< zJ+2l#%s^9Jh!upW-LE@gU+(Cd)O2szaV*j{Hz09`nqiB=?bUMXlP3S=^^Gs>P7$gz zG2E$s-CFn9z=S<}BNg3mFR&LRfT}Su0q}5HR%DRcEe~o%MA0eeK)$nq?x8X}F#Xo0 z^e9-R8n0JvX+YyFcd@P>yv4G;@VZ_y@Js_;eEFFbmM+uxm6{oGLd}QFgqKE|{ADPJ z*^qEP*@zt_0&afm-=353sXJCb_;ZbA8Wh!D$p)S#cJ{Cs&oj;G`y7;nFAlW0f7r9(tN9U~*loD<0(Z$6oMKDzqDJ}JC%O`KM@Q`v83 z*!tmjME=Ua9|;KwXRFnF)fD#1ZOd8~d0)6X)2l1(#d(#!=?{-*RPkX-0;H_0P=+@&R zCY5AC>3m%Y8(tulRjj-F57{%%)dzK?19}3dhoEQGi@@;Z5Uh)EvOQXaJg<7$2@@UI zr!wE&eO!z9GgU&k5gT~wz_zQ7EA?8;dMs8tyzdm&{G)Ohy3)9%|1-)Sc>wNqv#P@y zA_P_jTqrj=Lk|Ddvrn0@Y#NTFWO#}<@r&{BlG5N^7r+1uBhT>cKQxgP8)x&c$bhwO z<7YH}zSMsvVp13?ed+yXK3NeSy50OA_t|1YlePELTx)N<+|3Horn{K|l(=y4oQphK zk{+!pxRWA7G3w@!$f($2Ra%EUAe&wX3DlkJ1DDXgo8=)^Li;#)>2)I;0N9!GX>iZm zvT@aI-NvC+3PA;>Lp2o@AM@~A%j`61KU=aL4ab-LrO=|i&Sl4}7wemNhb+a_Jw~}3 zAVoPcV#+-181y*FhgD#ru<~ct=58|+cd&u2mwA|d0(StK>>6|b>B~->#|mPwVgi4P z<}$ZBFe#Rn_FYx3P~b3I{Wc{0JGF$>&LywK=C8FXrb=53Lf$`G=SSfprBS*=@wkbcaO8|R2;)xhJ_CH~<}QbwKQf!eo%snf2k)h{s^ zf&BEBFsDq4o|x)N=hO@}B=i$PxT0oEWaQJyJ!cfR%FtpcX2B>uU*4$BwvC<^N+Ljm z@2*)Ve<6HOE{%Q`!ur%%Z49|18OjLx$MvtHEjeMDGA1IBX!UyC&Lc#p8bK!q^eY6O zscWat#8rbnCHlXXG~;KYKGX)ue|}fw?BBx5GPunW2Y`-$DE784u0lBwuiFydUkTuN zEZP~yfbuld6p;tszqfooHWmUNtvC%EfKR=9VQ5Qdof=oVHS#$J25q?3{-*r{n5#T6 zHy~@NTVW3eleV4NYO3XXnX6q?eY5fUjT}&NdXDQ=a%yhlDMNb1m=b@HnN&hPxY`IW zEyzt@_78ksY1k9v4eT@1nq(ZExy;eYFcpNfv6^a`EfbG%Tcd-HPRZ%Qt<;O+^4H~U z>K+iAMwwg!t;NOQ)-$h>d5`J^br@p)xo#ra#|AOqYE8o)YUhisCw%N)6Vj(5I7060{G2@q zX+;IIY473`cIk&iW?Q}>v%v7+DyDV^x?_t!ZQU37R+WbrSK;{ANqIx8df*hyFhNtI zl(yiZ%^J9Ow%4D0W$Lq869{7t3`jvh|3qiyPiga4?x5WJ#klB=34opS4=@e%RmtD@ z-vA#jIq_bU|IGZ((jMm(PY3INJaKfbC_eoa2wT>j zsFuU{b$}fe@;h2ZVRxj9O2FT)lVw$%jOG!h#6)y0TarFe&GusMn-Ww=B;1dE;)do2 rk~}<*1yGMWxb6S;KN*M>(Kde}qhCGsZH&0`uNUTU#h!KXPTYS1Or|wD literal 0 HcmV?d00001 diff --git a/public/images/empty-activity.png b/public/images/empty-activity.png new file mode 100644 index 0000000000000000000000000000000000000000..90ec573166768b9411d51e4c0067545910ef8281 GIT binary patch literal 3682 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 + + + diff --git a/public/images/leaderboard-first-place.svg b/public/images/leaderboard-first-place.svg new file mode 100644 index 00000000..f63d6f2d --- /dev/null +++ b/public/images/leaderboard-first-place.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/leaderboard-second-place.svg b/public/images/leaderboard-second-place.svg new file mode 100644 index 00000000..9ad42c95 --- /dev/null +++ b/public/images/leaderboard-second-place.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/leaderboard-third-place.svg b/public/images/leaderboard-third-place.svg new file mode 100644 index 00000000..efc2e956 --- /dev/null +++ b/public/images/leaderboard-third-place.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/images/leaderboard-title-star.svg b/public/images/leaderboard-title-star.svg new file mode 100644 index 00000000..0d6ea86c --- /dev/null +++ b/public/images/leaderboard-title-star.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/safe-logo-round.svg b/public/images/safe-logo-round.svg new file mode 100644 index 00000000..348f9003 --- /dev/null +++ b/public/images/safe-logo-round.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/safe-pass.svg b/public/images/safe-pass.svg new file mode 100644 index 00000000..7da9462c --- /dev/null +++ b/public/images/safe-pass.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/images/star.svg b/public/images/star.svg new file mode 100644 index 00000000..f5ce86af --- /dev/null +++ b/public/images/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/transaction-number.png b/public/images/transaction-number.png new file mode 100644 index 0000000000000000000000000000000000000000..6b34d562112e5c4fcaa65be0782c8b4d1123f8fb GIT binary patch literal 23922 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 z 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 I<*h>g2az%M6951J literal 0 HcmV?d00001 diff --git a/public/images/transaction-volume.png b/public/images/transaction-volume.png new file mode 100644 index 0000000000000000000000000000000000000000..633e0d508485f2bf51fbb4ddd417a850b8df6b57 GIT binary patch literal 23403 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 z≪yAxL&V79Y$@ovxhmmw*A%+6fQ3{~G!nW!?F)YICtOcHYx-pRF){w}+d|Lu(~3 zFxgatuWD@@;ez!(5`?f6xGj`xYon`RAt` zu5Kv%9X-=KKfExbKPp2Z{@cwzn()Cjff3pEqc(9E3^eImmQklP2^Tn~>lxw7Kf!k; ztD++PD{_|>zdmGza&KJRUWWMZkVz&769Q4>^NJx?_t5&>-yEn#+!Np>%*9oBj%=h>$kgI%td zu69jn?c~59*~T*`)X?u!yxP%?gfk^|;u4pt{cnjA#6XW)OMM3*wKVkz_&v0IcEx0F z0)9avG53&PMR*q3Hk))rBfCk#SWL-6QkUR$6$}~zR*9epJQaz%B#$o*I3_xtwyD>2GJ9jPD}Rr z$K0JMNOZX-kA;Z7cX{0IR-hMk+Z?!w3ACp7j~(-f`!k`cK3^1=&L1ACne#rYB+}R$ zg50(K`1t&}B3-zb!6`o;xU=~oBWn+G=US2P{-82nVBWsHU>hOgvLxy)DFd>%w@-d+ zFRK*cZnQ0u#Ys`4>+9|L(Dv_Qf&r9PkAkp>Jq@1F0w1*tHUq@UP{clD=j*+iXssFX7acw&ieJroS=Z#J@pYc{zx;nb f9~#+p`($04VXgPUa0xX;8%;@0O}0+jBK-dV7h%E3 literal 0 HcmV?d00001 diff --git a/public/images/user-bust.png b/public/images/user-bust.png new file mode 100644 index 0000000000000000000000000000000000000000..6c78736f90bda3675b18e4e26179bcc176dd0d1b GIT binary patch literal 13555 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 { + const theme = useTheme() + const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg')) + + if (!isSmallScreen) return <>{children} + + return ( + + }> + + {title} + + + {children} + + ) +} diff --git a/src/components/AccordionContainer/styles.module.css b/src/components/AccordionContainer/styles.module.css new file mode 100644 index 00000000..f26698a2 --- /dev/null +++ b/src/components/AccordionContainer/styles.module.css @@ -0,0 +1,12 @@ +.container :global .MuiAccordionSummary-content { + margin: auto; + height: 64px; +} + +.container :global .Mui-expanded.MuiAccordionSummary-root { + border-bottom: 1px solid var(--mui-palette-border-light); +} + +.container :global .MuiAccordionDetails-root { + padding: 0; +} diff --git a/src/components/AccountCenter/index.tsx b/src/components/AccountCenter/index.tsx index d6a6447d..9ce6a36a 100644 --- a/src/components/AccountCenter/index.tsx +++ b/src/components/AccountCenter/index.tsx @@ -11,18 +11,24 @@ import { useChain } from '@/hooks/useChain' import { WalletInfo, UNKNOWN_CHAIN_NAME } from '@/components/WalletInfo' import { EthHashInfo } from '@/components/EthHashInfo' import type { ConnectedWallet } from '@/hooks/useWallet' +import { useIsSafeApp } from '@/hooks/useIsSafeApp' +import { AppRoutes } from '@/config/routes' +import { useRouter } from 'next/router' import css from './styles.module.css' const Popper = ({ wallet }: { wallet: ConnectedWallet }): ReactElement => { const [anchorEl, setAnchorEl] = useState(null) const onboard = useOnboard() + const router = useRouter() + const isSafeApp = useIsSafeApp() const chain = useChain() const connectedChain = chain?.chainId === wallet.chainId ? chain : undefined const handleDisconnect = () => { onboard?.disconnectWallet({ label: wallet.label }) + if (!isSafeApp) router.push({ pathname: AppRoutes.splash }) } const handleClick = (event: MouseEvent) => { diff --git a/src/components/Activities/index.tsx b/src/components/Activities/index.tsx new file mode 100644 index 00000000..2d40e0a0 --- /dev/null +++ b/src/components/Activities/index.tsx @@ -0,0 +1,125 @@ +import { ReactNode } from 'react' +import NextLink from 'next/link' +import { Box, Link, Stack, SvgIcon, Typography } from '@mui/material' +import { AppRoutes } from '@/config/routes' +import PaperContainer from '@/components//PaperContainer' +import { ExternalLink } from '@/components/ExternalLink' +import { ChevronLeft } from '@mui/icons-material' +import SafePass from '@/public/images/safe-pass.svg' +import UserBustIcon from '@/public/images/user-bust.png' +import WeeklyUser from '@/public/images/weekly-user.png' +import TransactionsVolumeIcon from '@/public/images/transaction-volume.png' +import TransactionsNumberIcon from '@/public/images/transaction-number.png' +import AssetsStoredIcon from '@/public/images/assets-stored.png' +import EmptyActivityIcon from '@/public/images/empty-activity.png' +import Image from 'next/image' +import { SAFE_PASS_LANDING_PAGE } from '@/config/constants' + +const ActivityItem = ({ title, description, icon }: { title: string; description: ReactNode; icon: ReactNode }) => { + return ( + + {icon} + + {title} + + + {description} + + + ) +} + +const Activities = () => { + return ( + + + palette.primary.main }} + > + + Back to main + + + Eligible activities + + + User bust + + + + With{' '} + + {'Safe {Pass}'} + {' '} + you can earn Points by using your Safe Account. Some activities are rewarded throughout the entire season, + some activities are only rewarded temporarily. + + + Learn more about {'Safe {Pass}'} + + + } + title="Weekly user" + description="Transacting with your Safe Account on a weekly basis" + /> + + } + title="Volume transacted" + description="Volume of your transactions" + /> + + } + title="No. of transactions" + description="The number of transactions made with your Safe Account" + /> + } + title="Assets stored" + description="The total assets value in your Safe Account" + /> + } + title="Other activities coming soon" + description={ + <> + To stay updated{' '} + + follow us on X + {' '} + and{' '} + + Farcaster + + + } + /> + + + + + + ) +} + +export default Activities diff --git a/src/components/BackgroundCircles/index.tsx b/src/components/BackgroundCircles/index.tsx index 0e77f3c3..eba38484 100644 --- a/src/components/BackgroundCircles/index.tsx +++ b/src/components/BackgroundCircles/index.tsx @@ -1,25 +1,7 @@ import { Box } from '@mui/material' -import clsx from 'clsx' - -import palette from '@/styles/colors' import css from './styles.module.css' -const Circle = ({ color, className }: { color: string; className: string }) => { - return ( - - ) -} - -export const TopCircle = () => { - return -} - -export const BottomCircle = () => { - return +export const BackgroundCircles = () => { + return } diff --git a/src/components/BackgroundCircles/styles.module.css b/src/components/BackgroundCircles/styles.module.css index ce85cb4c..cf2bb002 100644 --- a/src/components/BackgroundCircles/styles.module.css +++ b/src/components/BackgroundCircles/styles.module.css @@ -1,25 +1,12 @@ -.circle { - position: absolute; - border-radius: 50%; +.circles { z-index: -1; -} - -.top { - width: 350px; - height: 350px; - left: -100px; - top: 50px; -} - -.bottom { - width: 450px; - height: 450px; - right: -100px; - bottom: -100px; -} - -@media (max-width: 600px) { - .circle { - display: none; - } + position: fixed; + height: 100%; + width: 100%; + overflow: hidden; + top: 0; + left: 0; + background: radial-gradient(circle at 75% 25%, rgba(18, 255, 128, 0.3) 0%, #000000 40%), + radial-gradient(circle at 25% 125%, rgba(41, 182, 246, 0.5) 0%, #121312 50%); + background-blend-mode: screen; } diff --git a/src/components/BoostCounter/index.tsx b/src/components/BoostCounter/index.tsx new file mode 100644 index 00000000..b1beea67 --- /dev/null +++ b/src/components/BoostCounter/index.tsx @@ -0,0 +1,109 @@ +import { floorNumber } from '@/utils/boost' +import { NorthRounded, SouthRounded } from '@mui/icons-material' +import { Box, Typography } from '@mui/material' +import { TypographyProps } from '@mui/material/Typography' +import BezierEasing from 'bezier-easing' +import { useState, useEffect, useRef } from 'react' +const easeInOut = BezierEasing(0.42, 0, 0.58, 1) +const DURATION = 1000 +const SCALE_FACTOR = 0.15 + +const digitRotations: Record = { + [1]: 0, + [2]: Math.random() * 6 - 3, + [3]: Math.random() * 6 - 3, + [4]: Math.random() * 6 - 3, + [5]: Math.random() * 6 - 3, +} + +const BoostCounter = ({ + value, + direction, + ...props +}: TypographyProps & { value: number; direction?: 'north' | 'south' }) => { + const [isAnimating, setIsAnimating] = useState(false) + const [start, setStart] = useState(0) + const [target, setTarget] = useState(0) + + const targetRef = useRef(1) + + const [currentNumber, setCurrentNumber] = useState(target) + + const rotationRef = useRef(0) + + const scale = 1 + Math.floor(currentNumber) * SCALE_FACTOR + + useEffect(() => { + if (targetRef.current === target) { + // No new target + return + } + + targetRef.current = target + const startTime = new Date().getTime() + const offset = target - start + + const startRotation = digitRotations[floorNumber(currentNumber, 0)] + + const tick = () => { + setIsAnimating(true) + const elapsed = new Date().getTime() - startTime + + // If the browser window is in the background / minimized it will optimize and not call the requestAnimationFrame until in foreground causing wrong numbers for progress. + const progress = Math.min(elapsed / DURATION, 1) + const easedProgress = easeInOut(progress) + + const newNumber = start + easedProgress * offset + + if ( + startRotation !== digitRotations[floorNumber(newNumber, 0)] && + rotationRef.current !== digitRotations[floorNumber(newNumber, 0)] + ) { + rotationRef.current = digitRotations[floorNumber(newNumber, 0)] + } + setCurrentNumber(floorNumber(newNumber, 3)) + + if (elapsed < DURATION && targetRef.current === target) { + requestAnimationFrame(tick) + } else { + setIsAnimating(false) + rotationRef.current = 0 + } + } + + tick() + }, [target, currentNumber, start]) + + useEffect(() => { + setStart(target) + setTarget(value) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value]) + + const digit = currentNumber.toString().slice(0, 1) + const localeSeparator = (1.1).toLocaleString().charAt(1) + const decimals = currentNumber.toString().slice(2).slice(0, 2) + + return ( + + {direction === 'north' && } + {direction === 'south' && } + + + {digit} + + + {decimals !== '' ? `${localeSeparator}${decimals}x` : 'x'} + + + ) +} + +export default BoostCounter diff --git a/src/components/BoostCounter/styles.module.css b/src/components/BoostCounter/styles.module.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Claim/steps/SuccessfulClaim/index.tsx b/src/components/Claim/SuccessfulClaim.tsx similarity index 53% rename from src/components/Claim/steps/SuccessfulClaim/index.tsx rename to src/components/Claim/SuccessfulClaim.tsx index 9bb71e5a..0ae88431 100644 --- a/src/components/Claim/steps/SuccessfulClaim/index.tsx +++ b/src/components/Claim/SuccessfulClaim.tsx @@ -1,33 +1,28 @@ import { Grid, Typography, Button } from '@mui/material' -import type { Theme } from '@mui/material/styles' import type { ReactElement } from 'react' -import { useIsDarkMode } from '@/hooks/useIsDarkMode' import SafeLogo from '@/public/images/safe-logo.svg' -import type { ClaimFlow } from '@/components/Claim' -const SuccessfulClaim = ({ data, onNext }: { data: ClaimFlow; onNext: () => void }): ReactElement => { - const isDarkMode = useIsDarkMode() - - const backgroundColor = ({ palette }: Theme) => (isDarkMode ? undefined : palette.primary.main) - const textColor = isDarkMode ? undefined : 'white' - const buttonColor = isDarkMode ? undefined : 'secondary' +export type ClaimFlow = { + claimedAmount: string +} +const SuccessfulClaim = ({ data, onNext }: { data: ClaimFlow; onNext: () => void }): ReactElement => { return ( - + - + Congrats! - + You successfully started claiming {data.claimedAmount || '0'} Safe Tokens!
Once the transaction is signed and executed, the Safe Tokens will be available in your Safe Account.
-
diff --git a/src/components/Claim/index.tsx b/src/components/Claim/index.tsx index 4df0af7b..af267211 100644 --- a/src/components/Claim/index.tsx +++ b/src/components/Claim/index.tsx @@ -1,34 +1,262 @@ -import { useRouter } from 'next/router' -import type { ReactElement } from 'react' +import { + Grid, + Typography, + Button, + Paper, + Box, + Stack, + SvgIcon, + Divider, + TextField, + CircularProgress, + InputAdornment, +} from '@mui/material' +import { useState, type ReactElement, ChangeEvent } from 'react' + +import PaperContainer from '../PaperContainer' -import { useStepper } from '@/hooks/useStepper' -import ClaimOverview from '@/components/Claim/steps/ClaimOverview' -import SuccessfulClaim from '@/components/Claim/steps/SuccessfulClaim' +import StarIcon from '@/public/images/star.svg' +import { maxDecimals, minMaxValue, mustBeFloat } from '@/utils/validation' +import { useIsTokenPaused } from '@/hooks/useIsTokenPaused' +import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' +import { useTaggedAllocations } from '@/hooks/useTaggedAllocations' +import { getVestingTypes } from '@/utils/vesting' +import { formatEther } from 'ethers/lib/utils' +import { createClaimTxs } from '@/utils/claim' +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' +import { useIsWrongChain } from '@/hooks/useIsWrongChain' +import SafeToken from '@/public/images/token.svg' + +import css from './styles.module.css' +import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' -import { ProgressBar } from '@/components/ProgressBar' +import { canRedeemSep5Airdrop } from '@/utils/airdrop' +import { Sep5InfoBox } from '../Sep5InfoBox' +import { formatAmount } from '@/utils/formatters' +import { ClaimCard } from '../ClaimCard' +import { InfoAlert } from '../InfoAlert' +import { useWallet } from '@/hooks/useWallet' +import { isSafe } from '@/utils/wallet' +import { getGovernanceAppSafeAppUrl } from '@/utils/safe-apps' +import { useChainId } from '@/hooks/useChainId' + +const validateAmount = (amount: string, maxAmount: string) => { + if (isNaN(Number(amount))) { + return 'The value must be a number' + } + return mustBeFloat(amount) || minMaxValue(0, maxAmount, amount) || maxDecimals(amount, 18) +} + +const getDecimalLength = (amount: string) => { + const length = Number(amount).toFixed(2).length + + if (length > 10) { + return 0 + } -export type ClaimFlow = { - claimedAmount: string + if (length > 9) { + return 1 + } + + return 2 } -export const Claim = (): ReactElement => { +const ClaimOverview = (): ReactElement => { + const { sdk, safe } = useSafeAppsSDK() + const isWrongChain = useIsWrongChain() const router = useRouter() + const wallet = useWallet() + const chainId = useChainId() + + const [amount, setAmount] = useState('0') + const [isMaxAmountSelected, setIsMaxAmountSelected] = useState(false) + const [amountError, setAmountError] = useState() + const [creatingTxs, setCreatingTxs] = useState(false) + + const { data: isTokenPaused } = useIsTokenPaused() + + // Allocation, vesting and voting power + const { data: allocation } = useSafeTokenAllocation() + + const canRedeemSep5 = canRedeemSep5Airdrop(allocation) + + const { ecosystemVesting, investorVesting } = getVestingTypes(allocation?.vestingData ?? []) + + const { sep5, user, ecosystem, investor, total } = useTaggedAllocations() + const totalClaimableAmountInEth = formatEther(total.claimable) + + const decimals = getDecimalLength(total.inVesting) - const { step, data, nextStep } = useStepper({ - claimedAmount: '', + // Flags + const isInvestorClaimingDisabled = !!investorVesting && isTokenPaused + + const isAmountGTZero = !!amount && !amountError && Number.parseFloat(amount) > 0 + + const isClaimDisabled = + !isAmountGTZero || (isInvestorClaimingDisabled && isAmountGTZero) || !!amountError || creatingTxs || isWrongChain + + // Handlers + const onChangeAmount = (event: ChangeEvent) => { + const error = validateAmount(event.target.value || '0', totalClaimableAmountInEth) + setAmount(event.target.value) + setAmountError(error) + + setIsMaxAmountSelected(false) + } + + const onClick = (handler: () => Promise) => async () => { + // Safe is connected via WC + if (wallet && (await isSafe(wallet))) { + window.open(getGovernanceAppSafeAppUrl(chainId, wallet.address), '_blank')?.focus() + } else { + handler() + } + } + + const onClaim = onClick(async () => { + setCreatingTxs(true) + + const txs = createClaimTxs({ + vestingData: allocation?.vestingData ?? [], + safeAddress: safe.safeAddress, + isMax: isMaxAmountSelected, + amount: amount || '0', + sep5Claimable: sep5.claimable, + userClaimable: user.claimable, + investorClaimable: investor.claimable, + isTokenPaused: !!isTokenPaused, + }) + + try { + await sdk.txs.send({ txs }) + router.push({ pathname: AppRoutes.claim, query: { claimedAmount: amount } }) + setCreatingTxs(false) + } catch (error) { + console.error(error) + setCreatingTxs(false) + } }) - const steps = [ - nextStep(data)} />, - router.push(AppRoutes.index)} />, - ] + const setToMaxAmount = () => { + const amountAsNumber = Number(formatEther(total.claimable)) + setAmount(amountAsNumber.toFixed(2)) + setAmountError(undefined) - const progress = ((step + 1) / steps.length) * 100 + setIsMaxAmountSelected(true) + } return ( - <> - - {steps[step]} - + + + + + + + + + + + + + + + Total awarded allocation is{' '} + + {formatAmount(formatEther(total.allocation), 2)} SAFE + + + + + palette.background.default, + color: ({ palette }) => palette.text.primary, + position: 'relative', + height: '100%', + display: 'flex', + flexDirection: 'column', + gap: 1, + }} + > + + + + + Claim your tokens as rewards! + + You get more tokens if you are active in activity program. + + + + + + + + + How much do you want to claim? + + + Select all tokens or define a custom amount. + + + + {canRedeemSep5 && ( + + + + )} + + + + + + + ), + endAdornment: ( + + + + ), + }} + className={css.input} + /> + + + + + + + + + + ) } + +export default ClaimOverview diff --git a/src/components/Claim/steps/ClaimOverview/index.tsx b/src/components/Claim/steps/ClaimOverview/index.tsx deleted file mode 100644 index e7079fd4..00000000 --- a/src/components/Claim/steps/ClaimOverview/index.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { Button, CircularProgress, Divider, Grid, InputAdornment, TextField, Typography } from '@mui/material' -import { useState } from 'react' -import SafeToken from '@/public/images/token.svg' -import { formatEther } from 'ethers/lib/utils' -import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk' -import type { ChangeEvent, ReactElement } from 'react' - -import { SelectedDelegate } from '@/components/SelectedDelegate' -import { maxDecimals, minMaxValue, mustBeFloat } from '@/utils/validation' -import { ClaimCard } from '@/components/ClaimCard' -import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' -import { useDelegate } from '@/hooks/useDelegate' -import { InfoAlert } from '@/components/InfoAlert' -import { getVestingTypes } from '@/utils/vesting' -import { formatAmount } from '@/utils/formatters' -import { StepHeader } from '@/components/StepHeader' -import { createClaimTxs } from '@/utils/claim' -import { useIsTokenPaused } from '@/hooks/useIsTokenPaused' -import { useTaggedAllocations } from '@/hooks/useTaggedAllocations' -import { useIsWrongChain } from '@/hooks/useIsWrongChain' -import { Sep5InfoBox } from '@/components/Sep5InfoBox' -import { SEP5_EXPIRATION } from '@/config/constants' -import { canRedeemSep5Airdrop } from '@/utils/airdrop' -import type { ClaimFlow } from '@/components/Claim' - -import css from './styles.module.css' - -const validateAmount = (amount: string, maxAmount: string) => { - return mustBeFloat(amount) || minMaxValue(0, maxAmount, amount) || maxDecimals(amount, 18) -} - -const getDecimalLength = (amount: string) => { - const length = Number(amount).toFixed(2).length - - if (length > 10) { - return 0 - } - - if (length > 9) { - return 1 - } - - return 2 -} - -const ClaimOverview = ({ onNext }: { onNext: (data: ClaimFlow) => void }): ReactElement => { - const { sdk, safe } = useSafeAppsSDK() - const isWrongChain = useIsWrongChain() - - const [amount, setAmount] = useState('') - const [isMaxAmountSelected, setIsMaxAmountSelected] = useState(false) - const [amountError, setAmountError] = useState() - const [creatingTxs, setCreatingTxs] = useState(false) - - const delegate = useDelegate() - - const { data: isTokenPaused } = useIsTokenPaused() - - // Allocation, vesting and voting power - const { data: allocation } = useSafeTokenAllocation() - - const { sep5Vesting, ecosystemVesting, investorVesting } = getVestingTypes(allocation?.vestingData ?? []) - - const { sep5, user, ecosystem, investor, total } = useTaggedAllocations() - const totalClaimableAmountInEth = formatEther(total.claimable) - - const decimals = getDecimalLength(total.inVesting) - - // Flags - const canRedeemSep5 = canRedeemSep5Airdrop(allocation) - - const isInvestorClaimingDisabled = !!investorVesting && isTokenPaused - - const isAmountGTZero = !!amount && !amountError && Number.parseFloat(amount) > 0 - - const isClaimDisabled = - !isAmountGTZero || (isInvestorClaimingDisabled && isAmountGTZero) || !!amountError || creatingTxs || isWrongChain - - // Handlers - const onChangeAmount = (event: ChangeEvent) => { - const error = validateAmount(amount || '0', totalClaimableAmountInEth) - setAmount(event.target.value) - setAmountError(error) - - setIsMaxAmountSelected(false) - } - - const setToMaxAmount = () => { - const amountAsNumber = Number(formatEther(total.claimable)) - setAmount(amountAsNumber.toFixed(2)) - setAmountError(undefined) - - setIsMaxAmountSelected(true) - } - - const onClaim = async () => { - setCreatingTxs(true) - - const txs = createClaimTxs({ - vestingData: allocation?.vestingData ?? [], - safeAddress: safe.safeAddress, - isMax: isMaxAmountSelected, - amount: amount || '0', - sep5Claimable: sep5.claimable, - userClaimable: user.claimable, - investorClaimable: investor.claimable, - isTokenPaused: !!isTokenPaused, - }) - - try { - await sdk.txs.send({ txs }) - - onNext({ claimedAmount: amount }) - } catch (error) { - console.error(error) - } - - setCreatingTxs(false) - } - - return ( - - - - - - - - - - - - - - - - - Total allocation is{' '} - - {formatAmount(formatEther(total.allocation), 2)} SAFE - - - - - {canRedeemSep5 && ( - <> - - - - - - - - Execute at least one claim of any amount of your allocation before {SEP5_EXPIRATION} otherwise it will - be transferred back to the Safe{`{DAO}`} treasury. - - - - - )} - - - - - - - How much do you want to claim? - - - Select all Safe Tokens or define a custom amount. - - - - - - - - ), - endAdornment: ( - - - - ), - }} - className={css.input} - /> - - - - - - - - {isInvestorClaimingDisabled && ( - - - Claiming will be available once the Safe Token is transferable - - - )} - - {delegate && ( - - - - )} - - ) -} - -export default ClaimOverview diff --git a/src/components/Claim/steps/ClaimOverview/styles.module.css b/src/components/Claim/styles.module.css similarity index 100% rename from src/components/Claim/steps/ClaimOverview/styles.module.css rename to src/components/Claim/styles.module.css diff --git a/src/components/ClaimCard/index.tsx b/src/components/ClaimCard/index.tsx index 02e6034b..355d3bf8 100644 --- a/src/components/ClaimCard/index.tsx +++ b/src/components/ClaimCard/index.tsx @@ -1,12 +1,9 @@ import { ShieldOutlined } from '@mui/icons-material' import { Grid, Paper, Typography, Tooltip, Badge } from '@mui/material' -import { useTheme } from '@mui/material/styles' import { formatEther } from 'ethers/lib/utils' import InfoOutlined from '@mui/icons-material/InfoOutlined' import type { ReactElement } from 'react' -import SingleGreenTile from '@/public/images/single-green-tile.svg' -import DoubleGreenTile from '@/public/images/double-green-tile.svg' import { formatAmount } from '@/utils/formatters' import { Odometer } from '@/components/Odometer' @@ -25,33 +22,14 @@ export const ClaimCard = ({ variant: 'claimable' | 'vesting' decimals: number }): ReactElement => { - const { palette } = useTheme() - const ecosystemAmountInEth = formatEther(ecosystemAmount) const totalAmountInEth = formatEther(totalAmount) - const numericalTotalAmountInEth = Number(totalAmountInEth) const isClaimable = variant === 'claimable' - const color = isClaimable ? palette.background.default : palette.text.primary - return ( - (isClaimable ? palette.primary.main : palette.background.default), - color, - position: 'relative', - }} - > - {isClaimable && ( - <> - - - - )} - - + + {isClaimable ? 'Claim now' : 'Claim in future (vesting)'} {!isClaimable && ( @@ -60,21 +38,14 @@ export const ClaimCard = ({ arrow placement="top" > - + )} - + Total { - const onboard = useOnboard() - const chainId = useChainId() - - const onClick = async () => { - if (!onboard) { - return - } - - try { - const wallets = await onboard.connectWallet() - const wallet = getConnectedWallet(wallets) - - // Here we check non-hardware wallets. Hardware wallets will always be on the correct - // chain as onboard is only ever initialised with the current chain config - const isWrongChain = wallet && wallet.chainId !== chainId - if (isWrongChain) { - await onboard.setChain({ wallet: wallet.label, chainId: hexValue(parseInt(chainId)) }) - } - } catch { - return - } - } - - return ( - - - - - Welcome to the next generation of digital ownership - - - - - - - - Connect your wallet to view your SAFE balance and delegate voting power - - - - - - - - - - - ) -} diff --git a/src/components/ConnectWallet/styles.module.css b/src/components/ConnectWallet/styles.module.css new file mode 100644 index 00000000..d3ccbb5c --- /dev/null +++ b/src/components/ConnectWallet/styles.module.css @@ -0,0 +1,38 @@ +.milesReceipt { + width: 90%; + height: 537px; + display: flex; + overflow: hidden; + margin: 16px auto; +} + +.leftReceipt, +.rightReceipt { + background-color: #121312; + border-radius: 20px; + padding: 40px; +} + +.leftReceipt { + position: relative; + flex-grow: 1; +} + +.leftReceipt:after { + content: ''; + border-right: 1px dashed white; + position: absolute; + right: 0; + top: 20px; + height: calc(100% - 40px); +} + +.rightReceipt { + width: 388px; + position: relative; +} + +.barcode { + position: absolute; + right: 0; +} diff --git a/src/components/DashboardWidgets/ClaimingWidget.tsx b/src/components/DashboardWidgets/ClaimingWidget.tsx index 3287d2ec..4b8eed59 100644 --- a/src/components/DashboardWidgets/ClaimingWidget.tsx +++ b/src/components/DashboardWidgets/ClaimingWidget.tsx @@ -1,6 +1,6 @@ import { BigNumber } from 'ethers' import { formatEther } from 'ethers/lib/utils' -import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk' +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' import { Box, Button, Typography, Link, Skeleton, Card, IconButton, Grid, Tooltip } from '@mui/material' import ModeEditOutlinedIcon from '@mui/icons-material/ModeEditOutlined' import CheckSharpIcon from '@mui/icons-material/CheckSharp' @@ -19,7 +19,6 @@ import { formatAmount } from '@/utils/formatters' import { Sep5DeadlineChip } from '@/components/Sep5DeadlineChip' import { TypographyChip } from '@/components/TypographyChip' import { canRedeemSep5Airdrop } from '@/utils/airdrop' -import { useIsDarkMode } from '@/hooks/useIsDarkMode' import css from './styles.module.css' @@ -166,7 +165,6 @@ const VotingPowerWidget = (): ReactElement => { export const ClaimingWidget = (): ReactElement => { const { data: allocation, isLoading } = useSafeTokenAllocation() const canRedeemSep5 = canRedeemSep5Airdrop(allocation) - const isDarkMode = useIsDarkMode() if (isLoading) { return ( @@ -188,9 +186,7 @@ export const ClaimingWidget = (): ReactElement => { sx={{ minWidth: WIDGET_WIDTH, maxWidth: WIDGET_WIDTH, - border: canRedeemSep5 - ? ({ palette }) => (isDarkMode ? `1px solid ${palette.primary.main}` : `1px solid ${palette.secondary.main}`) - : undefined, + border: canRedeemSep5 ? ({ palette }) => `1px solid ${palette.primary.main}` : undefined, }} > <>{allocation?.votingPower.eq(0) ? : } diff --git a/src/components/Delegation/index.tsx b/src/components/Delegation/index.tsx index d28e856a..9011289c 100644 --- a/src/components/Delegation/index.tsx +++ b/src/components/Delegation/index.tsx @@ -12,6 +12,7 @@ import { ProgressBar } from '@/components/ProgressBar' import { AppRoutes } from '@/config/routes' import type { Delegate } from '@/hooks/useDelegate' import type { ContractDelegate } from '@/hooks/useContractDelegate' +import MediumPaper from '../MediumPaper' export type DelegateFlow = { safeGuardian?: FileDelegate @@ -36,15 +37,17 @@ export const Delegation = (): ReactElement => { const steps = [ nextStep(data)} />, nextStep(data)} />, - router.push(AppRoutes.index)} />, + router.push(AppRoutes.governance)} />, ] const progress = ((step + 1) / steps.length) * 100 return ( <> - - {steps[step]} + + + {steps[step]} + ) } diff --git a/src/components/EducationSeries/index.tsx b/src/components/EducationSeries/index.tsx deleted file mode 100644 index 7c88935c..00000000 --- a/src/components/EducationSeries/index.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { ReactElement } from 'react' - -import { useRouter } from 'next/router' -import { useStepper } from '@/hooks/useStepper' -import SafeInfo from '@/components/EducationSeries/steps/SafeInfo' -import Distribution from '@/components/EducationSeries/steps/Distribution' -import SafeToken from '@/components/EducationSeries/steps/SafeToken' -import SafeDao from '@/components/EducationSeries/steps/SafeDao' -import Disclaimer from '@/components/EducationSeries/steps/Disclaimer' -import { AppRoutes } from '@/config/routes' -import { ProgressBar } from '@/components/ProgressBar' - -export const EducationSeries = (): ReactElement => { - const router = useRouter() - - const { step, prevStep, nextStep } = useStepper(undefined) - - const onNext = () => { - nextStep(undefined) - } - - const steps = [ - , - , - , - , - router.push(AppRoutes.index)} />, - ] - - const progress = ((step + 1) / steps.length) * 100 - - return ( - <> - - {steps[step]} - - ) -} diff --git a/src/components/EducationSeries/steps/Disclaimer/index.tsx b/src/components/EducationSeries/steps/Disclaimer/index.tsx deleted file mode 100644 index 867a462b..00000000 --- a/src/components/EducationSeries/steps/Disclaimer/index.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Grid, Typography } from '@mui/material' -import type { ReactElement } from 'react' - -import { StepHeader } from '@/components/StepHeader' -import { NavButtons } from '@/components/NavButtons' -import { useIsSafeApp } from '@/hooks/useIsSafeApp' - -const Disclaimer = ({ onBack, onNext }: { onBack: () => void; onNext: () => void }): ReactElement => { - const isSafeApp = useIsSafeApp() - - return ( - - - - - - - This {isSafeApp ? 'Safe{App}' : 'App'} is for our community to encourage Safe ecosystem contributors and - users to unlock Safe{`{DAO}`} governance. -
-
- THIS APP IS PROVIDED “AS IS” AND “AS AVAILABLE,” AT YOUR OWN RISK, AND WITHOUT WARRANTIES OF ANY KIND. We will - not be liable for any loss, whether such loss is direct, indirect, special or consequential, suffered by any - party as a result of their use of this app. -
-
- By accessing this app, you represent and warrant -
- that you are of legal age and that you will comply with any laws applicable to you and not engage in any - illegal activities; -
- that you are claiming Safe Tokens to participate in the Safe{`{DAO}`} governance process and that they - do not represent consideration for past or future services; -
- that you, the country you are a resident of and your wallet address is not on any sanctions lists - maintained by the United Nations, Switzerland, the EU, UK or the US; -
- that you are responsible for any tax obligations arising out of the interaction with this app. -
-
- None of the information available on this app, or made otherwise available to you in relation to its use, - constitutes any legal, tax, financial or other advice. Where in doubt as to the action you should take, please - consult your own legal, financial, tax or other professional advisors. -
- - -
- ) -} - -export default Disclaimer diff --git a/src/components/EducationSeries/steps/Distribution/index.tsx b/src/components/EducationSeries/steps/Distribution/index.tsx deleted file mode 100644 index 796bf87c..00000000 --- a/src/components/EducationSeries/steps/Distribution/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { Box, Grid, Typography } from '@mui/material' -import type { ReactElement } from 'react' - -import { StepHeader } from '@/components/StepHeader' -import DistributionChart from '@/public/images/distribution-chart.svg' -import lightPalette from '@/styles/colors' -import { ExternalLink } from '@/components/ExternalLink' -import { NavButtons } from '@/components/NavButtons' - -import css from './styles.module.css' - -const DISTRIBUTION_PROPOSAL_URL = 'https://forum.gnosis-safe.io/t/safe-voting-power-and-circulating-supply/558' - -const Distribution = ({ onBack, onNext }: { onBack: () => void; onNext: () => void }): ReactElement => { - return ( - - - - - - - Safe Tokens are distributed to stakeholders of the ecosystem interested in shaping the future of Safe and - Safe Accounts. - - - Read full proposal - - - - - - - - - - palette.secondary.main, - color: lightPalette.text.primary, - }} - > - 60% - - - Community Treasuries - - - - 40% Safe{`{DAO}`} Treasury -
- 15% GnosisDAO Treasury -
- 5% Joint Treasury (GNO <> SAFE) -
-
- - - - palette.secondary.light, - color: lightPalette.text.primary, - }} - > - 15% - - - Core Contributors - - - Current and future core contributor teams - - - - - palette.info.main, - color: lightPalette.text.primary, - }} - > - 15% - - - Safe Foundation - - - - 8% strategic raise -
- 7% grants and reserve -
-
- - - - palette.success.main, - color: ({ palette }) => palette.background.paper, - }} - > - 5% - - - Guardians - - - - 1.25% allocation -
- 1.25% vested allocation -
- 2.5% future programs -
-
- - - - palette.warning.main, - color: ({ palette }) => palette.background.paper, - }} - > - 5% - - - User - - - - 2.5% allocation -
- 2.5% vested allocation -
-
-
- - -
- ) -} - -export default Distribution diff --git a/src/components/EducationSeries/steps/Distribution/styles.module.css b/src/components/EducationSeries/steps/Distribution/styles.module.css deleted file mode 100644 index 4b0b75b0..00000000 --- a/src/components/EducationSeries/steps/Distribution/styles.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.percentage { - border-radius: var(--mui-shape-borderRadius); - padding: 0px 8px; -} diff --git a/src/components/EducationSeries/steps/SafeDao/index.tsx b/src/components/EducationSeries/steps/SafeDao/index.tsx deleted file mode 100644 index 56dc2267..00000000 --- a/src/components/EducationSeries/steps/SafeDao/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Grid, Typography, Box, Paper } from '@mui/material' -import Checkmark from '@mui/icons-material/Check' -import type { ReactElement, ReactNode } from 'react' - -import { StepHeader } from '@/components/StepHeader' -import { ExternalLink } from '@/components/ExternalLink' -import { NavButtons } from '@/components/NavButtons' -import { DISCORD_URL, FORUM_URL, GOVERNANCE_URL, CHAIN_SNAPSHOT_URL } from '@/config/constants' -import { useChainId } from '@/hooks/useChainId' - -import css from './styles.module.css' - -const Point = ({ children }: { children: ReactNode }): ReactElement => { - return ( - - - {children} - - ) -} - -const SafeDao = ({ onBack, onNext }: { onBack: () => void; onNext: () => void }): ReactElement => { - const chainId = useChainId() - - const snapshotUrl = CHAIN_SNAPSHOT_URL[chainId] - - return ( - - - - - - - Safe{`{DAO}`} aims to foster a vibrant ecosystem of applications and wallets leveraging Safe Accounts. This will - be achieved through data-backed discussions, grants, ecosystem investments, as well as providing developer tools - and infrastructure. - - - - How to get involved: - - - - - Discuss Safe{`{DAO}`} improvements - post topics and discuss in our{' '} - Forum - - - - Propose improvements - read our governance process and post - an "SEP". - - - - Govern improvements - vote on our Snapshot. - - - - Chat with the community - join our Safe Discord. - - - - - Now… -
- Help decide on the future of ownership with SAFE. -
-
-
- - -
- ) -} - -export default SafeDao diff --git a/src/components/EducationSeries/steps/SafeDao/styles.module.css b/src/components/EducationSeries/steps/SafeDao/styles.module.css deleted file mode 100644 index 94822e1f..00000000 --- a/src/components/EducationSeries/steps/SafeDao/styles.module.css +++ /dev/null @@ -1,6 +0,0 @@ -.info { - background-color: var(--mui-palette-secondary-background); - border-radius: var(--mui-shape-borderRadius); - padding: 24px; - min-width: 220px; -} diff --git a/src/components/EducationSeries/steps/SafeInfo/index.tsx b/src/components/EducationSeries/steps/SafeInfo/index.tsx deleted file mode 100644 index 50426ed2..00000000 --- a/src/components/EducationSeries/steps/SafeInfo/index.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Grid, Typography, Box } from '@mui/material' -import type { ReactElement } from 'react' - -import LockIcon from '@/public/images/lock.svg' -import AssetsIcon from '@/public/images/assets.svg' -import { StepHeader } from '@/components/StepHeader' -import { NavButtons } from '@/components/NavButtons' -import { InfoBox } from '@/components/InfoBox' - -const SafeInfo = ({ onNext }: { onNext: () => void }): ReactElement => { - return ( - - - - What is Safe? - - } - /> - - - - Safe is critical infrastructure for Web3. It is a programmable wallet that enables secure management of - digital assets, data and identity. - - - - - - - Total Safe Accounts created - - - - - >1,000,000 - - - - - - - - - Total value protected - - - - - $41B - - - - - - - - Why do we have a Token? - - - - As critical web3 infrastructure, Safe needs to be a community-owned, censorship resistant project, with a - committed ecosystem stewarding its decisions. A governance Token is needed to help coordinate this effort. - - - - - ) -} - -export default SafeInfo diff --git a/src/components/EducationSeries/steps/SafeToken/index.tsx b/src/components/EducationSeries/steps/SafeToken/index.tsx deleted file mode 100644 index 5523c75c..00000000 --- a/src/components/EducationSeries/steps/SafeToken/index.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Accordion, AccordionSummary, Typography, AccordionDetails, List, ListItem, Grid } from '@mui/material' -import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import type { ReactElement } from 'react' - -import { StepHeader } from '@/components/StepHeader' -import { NavButtons } from '@/components/NavButtons' - -import css from './styles.module.css' - -const InfoAccordion = ({ summaryText, details }: { summaryText: ReactElement | string; details: string[] }) => { - return ( - - } className={css.summary}> - {summaryText} - - - - - {details.map((detail, i) => ( - {detail} - ))} - - - - ) -} - -const SafeToken = ({ onBack, onNext }: { onBack: () => void; onNext: () => void }): ReactElement => { - return ( - - - - - - - SAFE is an ERC-20 governance token that stewards infrastructure components of the Safe ecosystem, - including: - - - - - - - - - - - - - ) -} - -export default SafeToken diff --git a/src/components/EducationSeries/steps/SafeToken/styles.module.css b/src/components/EducationSeries/steps/SafeToken/styles.module.css deleted file mode 100644 index 02ea8dfa..00000000 --- a/src/components/EducationSeries/steps/SafeToken/styles.module.css +++ /dev/null @@ -1,57 +0,0 @@ -.accordion { - border: 1px solid var(--mui-palette-border-light); - border-radius: var(--mui-shape-borderRadius); - margin-bottom: 16px; - box-shadow: none; - width: 100%; -} - -.accordion::before { - display: none; -} - -.accordion :global .Mui-expanded { - border-radius: var(--mui-shape-borderRadius); - background-color: var(--mui-palette-background-light); -} - -.accordion:hover { - border: 1px solid var(--mui-palette-secondary-main); - background-color: var(--mui-palette-secondary-background); -} - -[data-theme='dark'] .accordion:hover { - border: 1px solid var(--mui-palette-primary-main); - background-color: var(--mui-palette-background-light); -} - -.details { - padding-top: 0; - background-color: var(--mui-palette-background-light); -} - -.summaryText { - font-weight: 700; - position: relative; - margin-left: 24px; - list-style-type: none; -} - -.summaryText::before { - content: ''; - border-radius: 2px; - position: absolute; - background-color: var(--mui-palette-text-primary); - min-width: 6px; - min-height: 6px; - top: 9px; - left: -24px; -} - -.list { - padding-top: 0; -} - -.list :global .MuiListItem-root { - padding: 4px 0 4px 24px; -} diff --git a/src/components/EnsureWalletConnection/index.tsx b/src/components/EnsureWalletConnection/index.tsx index c2b6c93e..a5a23d3e 100644 --- a/src/components/EnsureWalletConnection/index.tsx +++ b/src/components/EnsureWalletConnection/index.tsx @@ -6,7 +6,9 @@ import { Redirect } from '../Redirect' import { useWeb3 } from '@/hooks/useWeb3' const isProviderRoute = (pathname: string) => { - return [AppRoutes.claim, AppRoutes.delegate].includes(pathname) + return [AppRoutes.claim, AppRoutes.delegate, AppRoutes.activity, AppRoutes.governance, AppRoutes.unlock].includes( + pathname, + ) } export const EnsureWalletConnection = ({ children }: { children: ReactElement }): ReactElement => { @@ -15,5 +17,5 @@ export const EnsureWalletConnection = ({ children }: { children: ReactElement }) const shouldRedirect = !web3 && isProviderRoute(router.pathname) - return shouldRedirect ? : children + return shouldRedirect ? : children } diff --git a/src/components/ExternalLink/index.tsx b/src/components/ExternalLink/index.tsx index 89d3885f..98df548e 100644 --- a/src/components/ExternalLink/index.tsx +++ b/src/components/ExternalLink/index.tsx @@ -1,4 +1,4 @@ -import { Link } from '@mui/material' +import { Button, Link } from '@mui/material' import type { LinkProps } from '@mui/material' import { CSSProperties, forwardRef, ReactElement } from 'react' @@ -8,14 +8,32 @@ const styles: CSSProperties = { marginLeft: '4px', } -export const ExternalLink = forwardRef( - ({ children, icon = true, ...props }, ref): ReactElement => { - return ( - - {children} - {icon && } - - ) - }, -) +export const ExternalLink = forwardRef< + HTMLAnchorElement, + LinkProps & { href: string; icon?: boolean; variant?: string } +>(({ children, icon = true, variant = 'link', href, ...props }, ref): ReactElement => { + return ( + <> + {variant === 'button' ? ( + + ) : ( + + {children} + {icon && } + + )} + + ) +}) ExternalLink.displayName = 'ExternalLink' diff --git a/src/components/FloatingTiles/index.tsx b/src/components/FloatingTiles/index.tsx deleted file mode 100644 index cdada02e..00000000 --- a/src/components/FloatingTiles/index.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { Box } from '@mui/material' -import styled from '@emotion/styled' -import { useRef, useEffect, useMemo, useState } from 'react' - -import css from './styles.module.css' - -const DIMENSIONS = 1000 -const RADIUS = 40 // Percentage -const MIN_SIZE = 20 -const MAX_SIZE = 50 -const VARIANCE_FACTOR = 4 -const ANIMATION_DURATION = '90s' - -// Uses Box Muller transform to generate a 0,1 gaussian -const randomGaussian = () => { - const u = 1 - Math.random() - const v = Math.random() - return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v) -} - -const randomPointOnCircle = () => { - const angle = Math.random() * Math.PI * 2 - const adjustedRadius = RADIUS + VARIANCE_FACTOR * randomGaussian() - const x = Math.cos(angle) * adjustedRadius + 50 - const y = Math.sin(angle) * adjustedRadius + 50 - return [x, y] -} - -const Orbit = styled(Box)` - width: ${DIMENSIONS}px; - height: ${DIMENSIONS}px; - z-index: -1; - margin: auto; - opacity: 70%; - overflow: visible; - animation: 2s ease-in-out 1 grow, ${ANIMATION_DURATION} linear 2s infinite orbit; - - @keyframes grow { - from { - transform: scale(1); - } - to { - transform: scale(1); - } - } - - @keyframes orbit { - 0% { - transform: rotate(0deg); - } - 50% { - transform: rotate(180deg); - } - 100% { - transform: rotate(360deg); - } - } - - /* Tone down the animation to avoid vestibular motion triggers */ - @media (prefers-reduced-motion) { - animation-name: none; - } -` - -const TileBox = styled(Box)` - animation-iteration-count: infinite; - animation-timing-function: linear; - animation-delay: 2s; - animation-name: anti-orbit; - animation-duration: ${ANIMATION_DURATION}; - position: absolute; - - @keyframes anti-orbit { - 0% { - transform: rotate(0deg); - } - 50% { - transform: rotate(-180deg); - } - 100% { - transform: rotate(-360deg); - } - } - - /* Tone down the animation to avoid vestibular motion triggers */ - @media (prefers-reduced-motion) { - animation-name: none; - } -` - -const Tile = ({ - top, - left, - size, - startTime, -}: { - top: string - left: string - size: string - startTime: number | undefined | null -}) => { - const tileRef = useRef(null) - - useEffect(() => { - const animation = tileRef.current?.getAnimations()[0] - if (animation && startTime) { - animation.startTime = startTime - } - }, [startTime, tileRef]) - - return ( - -

- - ) -} - -export const FloatingTiles = ({ tiles }: { tiles: number }) => { - const orbitRef = useRef(null) - const [animationStartTime, setAnimationStartTime] = useState() - - useEffect(() => { - const timer = setTimeout(() => { - if (!animationStartTime) { - setAnimationStartTime(orbitRef.current?.getAnimations()[0]?.startTime) - } - }, 0) - - return () => { - clearTimeout(timer) - } - }, [animationStartTime, orbitRef]) - - const tilesArr: { - top: string - left: string - size: string - }[] = useMemo(() => { - return Array.apply('', Array(tiles)).map(() => { - const [x, y] = randomPointOnCircle() - - const top = `${x.toFixed(2)}%` - const left = `${y.toFixed(2)}%` - - const size = `${(Math.random() * (MAX_SIZE - MIN_SIZE) + MIN_SIZE).toFixed(2)}px` - - return { - top, - left, - size, - } - }) - }, [tiles]) - - return ( -
- - {tilesArr.map((tile, i) => ( - - ))} - -
- ) -} diff --git a/src/components/FloatingTiles/styles.module.css b/src/components/FloatingTiles/styles.module.css deleted file mode 100644 index 2c39af80..00000000 --- a/src/components/FloatingTiles/styles.module.css +++ /dev/null @@ -1,20 +0,0 @@ -.container { - width: 100%; - height: 100%; - position: fixed; - display: flex; - z-index: -1; -} - -.spacer { - position: absolute; - transition: transform 1s ease-in; - border-radius: 20%; - width: 100%; - height: 100%; - background-color: var(--mui-palette-secondary-main); -} - -[data-theme='dark'] .spacer { - background-color: var(--mui-palette-primary-main); -} diff --git a/src/components/Intro/index.tsx b/src/components/GovernanceAndClaiming/index.tsx similarity index 50% rename from src/components/Intro/index.tsx rename to src/components/GovernanceAndClaiming/index.tsx index 073911dc..67c65e05 100644 --- a/src/components/Intro/index.tsx +++ b/src/components/GovernanceAndClaiming/index.tsx @@ -1,6 +1,5 @@ -import { Grid, Typography, Box, Button, CircularProgress } from '@mui/material' +import { Grid, Typography, Box, Button, CircularProgress, Stack } from '@mui/material' import { useRouter } from 'next/router' -import { formatEther } from 'ethers/lib/utils' import { BigNumber } from 'ethers' import type { ReactElement } from 'react' @@ -10,7 +9,6 @@ import { SelectedDelegate } from '@/components/SelectedDelegate' import { AppRoutes } from '@/config/routes' import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' import { TotalVotingPower } from '@/components/TotalVotingPower' -import { formatAmount } from '@/utils/formatters' import { useTaggedAllocations } from '@/hooks/useTaggedAllocations' import { useIsWrongChain } from '@/hooks/useIsWrongChain' import SafeToken from '@/public/images/token.svg' @@ -18,14 +16,13 @@ import { getGovernanceAppSafeAppUrl } from '@/utils/safe-apps' import { useChainId } from '@/hooks/useChainId' import { useWallet } from '@/hooks/useWallet' import { isSafe } from '@/utils/wallet' -import { InfoBox } from '@/components/InfoBox' import { useIsDelegationPending } from '@/hooks/usePendingDelegations' -import { Sep5InfoBox } from '@/components/Sep5InfoBox' -import { canRedeemSep5Airdrop } from '@/utils/airdrop' +import ClaimOverview from '@/components/Claim' +import PaperContainer from '@/components/PaperContainer' import css from './styles.module.css' -export const Intro = (): ReactElement => { +export const GovernanceAndClaiming = (): ReactElement => { const router = useRouter() const isWrongChain = useIsWrongChain() const wallet = useWallet() @@ -36,9 +33,6 @@ export const Intro = (): ReactElement => { const { isLoading, data: allocation } = useSafeTokenAllocation() const { total } = useTaggedAllocations() - const canRedeemSep5 = canRedeemSep5Airdrop(allocation) - - const hasAllocation = Number(total.allocation) > 0 const isClaimable = Number(total.claimable) > 0 const isDelegating = useIsDelegationPending() @@ -54,8 +48,6 @@ export const Intro = (): ReactElement => { } } - const onClaim = onClick(AppRoutes.claim) - const onDelegate = onClick(AppRoutes.delegate) const action = ( @@ -72,56 +64,38 @@ export const Intro = (): ReactElement => { ) } return ( - - - - - - - - - - - - {hasAllocation && ( - - - - Claimable now - - {formatAmount(formatEther(total.claimable), 2)} SAFE - - - - Claimable in the future - - {formatAmount(formatEther(total.inVesting), 2)} SAFE - - - )} + + + Claim SAFE tokens and engage in governance + - {canRedeemSep5 && ( - - - - )} + + + {isClaimable && } - {isClaimable && ( - - + + + Delegate your voting power + + + - )} - + + - - - + + + + - - + + Total voting power is + + + + + ) diff --git a/src/components/GovernanceAndClaiming/styles.module.css b/src/components/GovernanceAndClaiming/styles.module.css new file mode 100644 index 00000000..09d716db --- /dev/null +++ b/src/components/GovernanceAndClaiming/styles.module.css @@ -0,0 +1,5 @@ +@media (max-width: 899px) { + .pageTitle { + margin: 16px; + } +} diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index 2e9a546f..47857d5e 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -6,6 +6,7 @@ import { AccountCenter } from '@/components/AccountCenter' import { ChainSwitcher } from '@/components/ChainSwitcher' import { useIsSafeApp } from '@/hooks/useIsSafeApp' import { TestChainSwitch } from '@/components/TestChainSwitch' +import { TestDateSelect } from '@/components/TestDateSelect' import css from './styles.module.css' @@ -21,6 +22,7 @@ export const Header = (): ReactElement | null => { +
diff --git a/src/components/Header/styles.module.css b/src/components/Header/styles.module.css index 53ce330d..1c8e7d14 100644 --- a/src/components/Header/styles.module.css +++ b/src/components/Header/styles.module.css @@ -4,6 +4,10 @@ align-items: center; border-radius: 0 !important; border-bottom: 1px solid var(--mui-palette-border-light); + position: absolute; + top: 0; + left: 0; + width: 100%; } .wallet { diff --git a/src/components/InfoAlert/index.tsx b/src/components/InfoAlert/index.tsx index 18434892..1baf2f66 100644 --- a/src/components/InfoAlert/index.tsx +++ b/src/components/InfoAlert/index.tsx @@ -5,7 +5,7 @@ import type { ReactElement } from 'react' export const InfoAlert = ({ children, ...props }: BoxProps): ReactElement => { return ( - + { + return {props.children} +} + +export default MediumPaper diff --git a/src/components/MediumPaper/styles.module.css b/src/components/MediumPaper/styles.module.css new file mode 100644 index 00000000..95c1246a --- /dev/null +++ b/src/components/MediumPaper/styles.module.css @@ -0,0 +1,12 @@ +.mediumPaper { + width: 650px; + position: relative; + margin: auto; +} + +@media (max-width: 600px) { + .mediumPaper { + height: 100%; + width: 100%; + } +} diff --git a/src/components/NavTabs/index.tsx b/src/components/NavTabs/index.tsx new file mode 100644 index 00000000..ae6b9be8 --- /dev/null +++ b/src/components/NavTabs/index.tsx @@ -0,0 +1,72 @@ +import React, { forwardRef, ReactElement } from 'react' +import NextLink, { type LinkProps as NextLinkProps } from 'next/link' +import { Tab, Tabs, Typography, type TabProps } from '@mui/material' +import { useRouter } from 'next/router' +import css from './styles.module.css' +import { useIsSafeApp } from '@/hooks/useIsSafeApp' +import Track from '../Track' + +export type NavItem = { + label: string + icon?: ReactElement + href: string + event: { action: string } +} + +type Props = TabProps & NextLinkProps + +// This is needed in order for the tabs to work properly with Next Link e.g. tabbing with the keyboard +// Based on https://github.com/mui/material-ui/blob/master/examples/nextjs-with-typescript/src/Link.tsx +const NextLinkComposed = forwardRef(function NextComposedLink(props: Props, ref) { + const { href, as, replace, scroll, shallow, prefetch, legacyBehavior = true, locale, ...other } = props + + return ( + + {/* @ts-ignore */} + + + ) +}) + +const NavTabs = ({ tabs }: { tabs: NavItem[] }) => { + const router = useRouter() + const activeTab = Math.max(0, tabs.map((tab) => tab.href).indexOf(router.pathname)) + + const isSafeApp = useIsSafeApp() + + return ( + + {tabs.map((tab, idx) => ( + + + {tab.label} + + } + /> + + ))} + + ) +} + +export default NavTabs diff --git a/src/components/NavTabs/styles.module.css b/src/components/NavTabs/styles.module.css new file mode 100644 index 00000000..604ec8c3 --- /dev/null +++ b/src/components/NavTabs/styles.module.css @@ -0,0 +1,49 @@ +.tabs { + overflow: initial; + align-self: flex-start; + width: 100%; +} + +/* Scroll buttons */ +.tabs :global .MuiTabs-scrollButtons.Mui-disabled { + opacity: 0.3; +} + +.tabs :global .MuiTabScrollButton-root ~ .MuiTabs-scroller p { + padding-bottom: 0; +} + +.tabs :global .MuiTabScrollButton-root:first-of-type { + margin-left: calc(16 * -1); +} + +.tabs :global .MuiTabScrollButton-root:last-of-type { + margin-right: calc(16 * -1); +} + +.tabs :global .MuiTabScrollButton-root ~ .MuiTabs-scroller p { + padding-bottom: 0; +} + +.tab { + opacity: 1; + padding: 0 24; + position: relative; + z-index: 2; +} + +.label { + text-transform: none; + padding-bottom: 6px; +} + +@media (max-width: 599px) { + .tabs span { + flex: 1; + min-width: fit-content; + } + + .tab { + width: 100%; + } +} diff --git a/src/components/OverviewLinks/index.tsx b/src/components/OverviewLinks/index.tsx index e3a883a6..288e67c4 100644 --- a/src/components/OverviewLinks/index.tsx +++ b/src/components/OverviewLinks/index.tsx @@ -1,10 +1,8 @@ -import NextLink from 'next/link' -import { SvgIcon, Grid, Typography, Paper, Box, Link } from '@mui/material' +import { SvgIcon, Typography, Paper, Box, Stack } from '@mui/material' import { useRef } from 'react' import type { ReactElement, SyntheticEvent } from 'react' import Hat from '@/public/images/hat.svg' -import { AppRoutes } from '@/config/routes' import { FORUM_URL, CHAIN_SNAPSHOT_URL } from '@/config/constants' import { ExternalLink } from '@/components/ExternalLink' import { useChainId } from '@/hooks/useChainId' @@ -27,14 +25,12 @@ const SafeDaoCard = () => {
- - What is -
- Safe{`{DAO}`}? + + What is Safe DAO? - + Learn more - +
) @@ -65,18 +61,10 @@ export const OverviewLinks = (): ReactElement => { const snapshotUrl = CHAIN_SNAPSHOT_URL[chainId] return ( - - - - - - - - - - - - - + + + + + ) } diff --git a/src/components/PageLayout/index.tsx b/src/components/PageLayout/index.tsx index 34e4ab60..69620f04 100644 --- a/src/components/PageLayout/index.tsx +++ b/src/components/PageLayout/index.tsx @@ -1,15 +1,23 @@ import Head from 'next/head' -import { Box, Paper } from '@mui/material' -import type { ReactElement, ReactNode } from 'react' +import { Box } from '@mui/material' +import { ReactElement, ReactNode } from 'react' import manifestJson from '@/public/manifest.json' -import { BottomCircle, TopCircle } from '@/components/BackgroundCircles' +import { BackgroundCircles } from '@/components/BackgroundCircles' import { Header } from '@/components/Header' -import { FloatingTiles } from '@/components/FloatingTiles' import css from './styles.module.css' +import NavTabs from '../NavTabs' +import { AppRoutes } from '@/config/routes' +import { useRouter } from 'next/router' +import { NAVIGATION_EVENTS } from '@/analytics/navigation' + +const RoutesWithNavigation = [AppRoutes.activity, AppRoutes.governance] export const PageLayout = ({ children }: { children: ReactNode }): ReactElement => { + const router = useRouter() + const showNavigation = RoutesWithNavigation.includes(router.route) + return ( <> @@ -18,18 +26,30 @@ export const PageLayout = ({ children }: { children: ReactNode }): ReactElement
-
- -
- - - - - - {children} - - - + + + + + {showNavigation && ( + + + + )} + {children} + ) diff --git a/src/components/PageLayout/styles.module.css b/src/components/PageLayout/styles.module.css index c00c63ef..70cecc72 100644 --- a/src/components/PageLayout/styles.module.css +++ b/src/components/PageLayout/styles.module.css @@ -1,21 +1,42 @@ .container { - width: 650px; + width: 100%; + display: flex; + align-items: center; + flex-direction: column; +} + +.navigation { + width: 100%; + padding-left: 15vw; + padding-right: 15vw; + border-bottom: 1px solid #303033; +} + +.pageContent { + width: 70vw; position: relative; margin: auto; } -.tiles { - position: relative; - top: -200px; +@media (max-width: 1600px) { + .pageContent { + width: 90vw; + } + .navigation { + padding-left: 5vw; + padding-right: 5vw; + } } -@media (max-width: 600px) { +@media (max-width: 899px) { .container { - width: 100%; height: 100%; + width: 100%; } - - .tiles { - display: none; + .pageContent { + width: 100%; + } + .navigation { + padding: 0; } } diff --git a/src/components/PaperContainer/index.tsx b/src/components/PaperContainer/index.tsx new file mode 100644 index 00000000..5e9a0db5 --- /dev/null +++ b/src/components/PaperContainer/index.tsx @@ -0,0 +1,12 @@ +import { Paper, PaperProps } from '@mui/material' +import css from './styles.module.css' + +const PaperContainer = (props: PaperProps) => { + return ( + + {props.children} + + ) +} + +export default PaperContainer diff --git a/src/components/PaperContainer/styles.module.css b/src/components/PaperContainer/styles.module.css new file mode 100644 index 00000000..54ee4d98 --- /dev/null +++ b/src/components/PaperContainer/styles.module.css @@ -0,0 +1,6 @@ +.paper { + padding: 24px; + display: flex; + flex-direction: column; + gap: 24px; +} diff --git a/src/components/Redirect/index.tsx b/src/components/Redirect/index.tsx index 616627ec..ff429a72 100644 --- a/src/components/Redirect/index.tsx +++ b/src/components/Redirect/index.tsx @@ -1,16 +1,24 @@ import { useRouter } from 'next/router' -import type { UrlObject } from 'url' import { useIsomorphicLayoutEffect } from '@/hooks/useIsomorphicLayoutEffect' +import { ParsedUrlQueryInput } from 'querystring' -export const Redirect = ({ url, replace }: { url: string | UrlObject; replace?: boolean }): null => { +export const Redirect = ({ + url, + replace, + query, +}: { + url: string + replace?: boolean + query?: ParsedUrlQueryInput +}): null => { const router = useRouter() useIsomorphicLayoutEffect(() => { if (replace) { router.replace(url) } else { - router.push(url) + router.push({ pathname: url, query }) } }, [replace, router, url]) diff --git a/src/components/SelectedDelegate/index.tsx b/src/components/SelectedDelegate/index.tsx index e26e1229..1aeff57e 100644 --- a/src/components/SelectedDelegate/index.tsx +++ b/src/components/SelectedDelegate/index.tsx @@ -86,8 +86,8 @@ export const SelectedDelegate = ({ {hint && ( - - You only delegate your voting power and not the ownership of your Safe Tokens. + + You only delegate your voting power and not the ownership over your tokens. )} diff --git a/src/components/SplashScreen/index.tsx b/src/components/SplashScreen/index.tsx new file mode 100644 index 00000000..4fa9678d --- /dev/null +++ b/src/components/SplashScreen/index.tsx @@ -0,0 +1,163 @@ +import { Typography, Button, Chip, Stack, SvgIcon, Box, CircularProgress, Grid } from '@mui/material' +import { hexValue } from 'ethers/lib/utils' +import { ReactElement, useState } from 'react' + +import { useOnboard } from '@/hooks/useOnboard' + +import { useChainId } from '@/hooks/useChainId' +import { getConnectedWallet, useWallet } from '@/hooks/useWallet' +import { useRouter } from 'next/router' +import { AppRoutes } from '@/config/routes' +import Barcode from '@/public/images/barcode.svg' + +import css from './styles.module.css' +import SafePass from '@/public/images/safe-pass.svg' +import { useIsSafeApp } from '@/hooks/useIsSafeApp' +import Asterix from '@/public/images/asterix.svg' +import { localItem } from '@/services/storage/local' +import { useIsomorphicLayoutEffect } from '@/hooks/useIsomorphicLayoutEffect' +import { isSafe } from '@/utils/wallet' +import { trackSafeAppEvent } from '@/utils/analytics' +import { NAVIGATION_EVENTS } from '@/analytics/navigation' + +const Step = ({ index, title, active }: { index: number; title: string; active: boolean }) => { + return ( + + + {title} + + ) +} + +// Check if new or returning user +const ALREADY_VISITED = 'alreadyVisited' +const alreadyVisitedStorage = localItem(ALREADY_VISITED) + +/** + * This page handles wallet connection and initial data loading. + */ +export const SplashScreen = (): ReactElement => { + const onboard = useOnboard() + const chainId = useChainId() + const router = useRouter() + const isSafeApp = useIsSafeApp() + + const [isConnecting, setIsConnecting] = useState(false) + const [error, setError] = useState() + + const wallet = useWallet() + + const isDisconnected = !isSafeApp && !wallet + + const onConnect = async () => { + if (!onboard) { + return + } + setError(undefined) + setIsConnecting(true) + try { + const wallets = await onboard.connectWallet() + const wallet = getConnectedWallet(wallets) + + // Here we check non-hardware wallets. Hardware wallets will always be on the correct + // chain as onboard is only ever initialised with the current chain config + const isWrongChain = wallet && wallet.chainId !== chainId + if (isWrongChain) { + await onboard.setChain({ wallet: wallet.label, chainId: hexValue(parseInt(chainId)) }) + } + + // When using the standalone app, only allow Safe accounts to be connected + if (wallet && !isSafeApp && !(await isSafe(wallet))) { + await onboard.disconnectWallet({ label: wallet.label }) + setError('Connected wallet must be a Safe') + return + } + onContinue() + } catch (error) { + setError('Wallet connection failed.') + return + } finally { + setIsConnecting(false) + } + } + + const onContinue = async () => { + trackSafeAppEvent(NAVIGATION_EVENTS.OPEN_LOCKING.action, 'opening') + alreadyVisitedStorage.set(true) + const nextPage = router.query.next === AppRoutes.governance ? AppRoutes.governance : AppRoutes.activity + router.push(nextPage) + } + + useIsomorphicLayoutEffect(() => { + const hasAlreadyVisited = alreadyVisitedStorage.get() + if (isSafeApp && hasAlreadyVisited) { + // We are a returning user and already connected + // We only skip this step for the Safe App as we have to connect a wallet in the standalone version + onContinue() + } + }, [isSafeApp]) + + return ( + + + + + + + + + Interact with Safe and get rewards + + Get your pass now! Lock your tokens and be active on Safe to get rewarded. + + {isDisconnected ? ( + + ) : ( + + )} + {error && ( + + {error} + + )} + + + + + + + + How it works + + + + + + + + + + + + ) +} diff --git a/src/components/SplashScreen/styles.module.css b/src/components/SplashScreen/styles.module.css new file mode 100644 index 00000000..f082accc --- /dev/null +++ b/src/components/SplashScreen/styles.module.css @@ -0,0 +1,55 @@ +.milesReceipt { + width: 90%; + display: flex; + overflow: hidden; +} + +.leftReceipt, +.rightReceipt { + background-color: #121312; + border-radius: 20px; + padding: 40px; + height: 537px; + width: 100%; +} + +.leftReceipt { + position: relative; + flex-grow: 1; +} + +.leftReceipt:after { + content: ''; + border-right: 1px dashed white; + position: absolute; + right: 0; + top: 20px; + height: calc(100% - 40px); +} + +.barcode { + position: absolute; + right: 10%; +} + +@media (max-width: 899px) { + .milesReceipt { + width: 100%; + } + + .leftReceipt, + .rightReceipt { + border-radius: 0px; + } + .barcode { + right: 0px; + } + + .leftReceipt:after { + display: none; + } + + .leftReceipt { + border-bottom: 1px dashed white; + } +} diff --git a/src/components/StepHeader/index.tsx b/src/components/StepHeader/index.tsx index cbceafef..61484dc2 100644 --- a/src/components/StepHeader/index.tsx +++ b/src/components/StepHeader/index.tsx @@ -9,7 +9,7 @@ export const StepHeader = ({ title }: { title: ReactNode }): ReactElement => { const router = useRouter() const onClose = () => { - router.push(AppRoutes.index) + router.push(AppRoutes.governance) } return ( diff --git a/src/components/TestChainSwitch/index.tsx b/src/components/TestChainSwitch/index.tsx index 8033de1d..c3be599c 100644 --- a/src/components/TestChainSwitch/index.tsx +++ b/src/components/TestChainSwitch/index.tsx @@ -1,7 +1,7 @@ import { FormControlLabel, Switch } from '@mui/material' import type { ReactElement } from 'react' -import { Chains, IS_PRODUCTION, _DEFAULT_CHAIN_ID } from '@/config/constants' +import { Chains, IS_PRODUCTION } from '@/config/constants' import { useChainId, defaultChainIdStore } from '@/hooks/useChainId' export const TestChainSwitch = (): ReactElement | null => { @@ -9,7 +9,7 @@ export const TestChainSwitch = (): ReactElement | null => { const onToggle = () => { defaultChainIdStore.setStore((prev) => { - return prev === Chains.GOERLI ? Chains.MAINNET : Chains.GOERLI + return prev === Chains.SEPOLIA ? Chains.MAINNET : Chains.SEPOLIA }) } @@ -18,6 +18,9 @@ export const TestChainSwitch = (): ReactElement | null => { } return ( - } label="Use Goerli" /> + } + label="Use Sepolia" + /> ) } diff --git a/src/components/TestDateSelect/index.tsx b/src/components/TestDateSelect/index.tsx new file mode 100644 index 00000000..92af128c --- /dev/null +++ b/src/components/TestDateSelect/index.tsx @@ -0,0 +1,34 @@ +import type { ReactElement } from 'react' + +import { IS_PRODUCTION } from '@/config/constants' +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker' +import { LocalizationProvider } from '@mui/x-date-pickers' +import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns' +import { startDateStore, useStartDate } from '@/hooks/useStartDates' + +export const TestDateSelect = (): ReactElement | null => { + const { startTime } = useStartDate() + + const onDateSelect = (date: Date | null) => { + if (date) { + const timestamp = date.getTime() + + startDateStore.setStore(timestamp) + } + } + + if (IS_PRODUCTION) { + return null + } + + return ( + + onDateSelect(value)} + slotProps={{ textField: { size: 'small' } }} + label="start date" + /> + + ) +} diff --git a/src/components/TockenUnlocking/UnlockStats.tsx b/src/components/TockenUnlocking/UnlockStats.tsx new file mode 100644 index 00000000..5826a336 --- /dev/null +++ b/src/components/TockenUnlocking/UnlockStats.tsx @@ -0,0 +1,24 @@ +import { Grid } from '@mui/material' +import { BigNumberish } from 'ethers' + +import { TokenAmount } from '../TokenAmount' + +export const UnlockStats = ({ + currentlyLocked, + unlockedTotal, +}: { + currentlyLocked: BigNumberish + unlockedTotal: BigNumberish +}) => { + return ( + + + + + + + + + + ) +} diff --git a/src/components/TockenUnlocking/UnlockTokenWidget.tsx b/src/components/TockenUnlocking/UnlockTokenWidget.tsx new file mode 100644 index 00000000..455a9a96 --- /dev/null +++ b/src/components/TockenUnlocking/UnlockTokenWidget.tsx @@ -0,0 +1,195 @@ +import SafeToken from '@/public/images/token.svg' +import { getBoostFunction } from '@/utils/boost' +import css from './styles.module.css' +import { Stack, Grid, Typography, TextField, InputAdornment, Button, CircularProgress, Box } from '@mui/material' +import { formatUnits, parseUnits } from 'ethers/lib/utils' +import { BoostGraph } from '../TokenLocking/BoostGraph/BoostGraph' +import { useDebounce } from '@/hooks/useDebounce' +import { createUnlockTx, LockHistory } from '@/utils/lock' +import { useState, useMemo, ChangeEvent, useCallback, useEffect } from 'react' +import { BigNumber, BigNumberish } from 'ethers' +import { useChainId } from '@/hooks/useChainId' +import { getCurrentDays } from '@/utils/date' +import { SEASON2_START } from '@/config/constants' +import { BoostBreakdown } from '../TokenLocking/BoostBreakdown' +import Track from '../Track' +import { LOCK_EVENTS } from '@/analytics/lockEvents' +import { trackSafeAppEvent } from '@/utils/analytics' +import MilesReceipt from '@/components/TokenLocking/MilesReceipt' +import { useTxSender } from '@/hooks/useTxSender' +import { useStartDate } from '@/hooks/useStartDates' + +export const UnlockTokenWidget = ({ + lockHistory, + currentlyLocked, +}: { + lockHistory: LockHistory[] + currentlyLocked: BigNumberish +}) => { + const [receiptOpen, setReceiptOpen] = useState(false) + const [receiptInformation, setReceiptInformation] = useState<{ newFinalBoost: number; amount: string }>({ + amount: '0', + newFinalBoost: 1, + }) + const [unlockAmount, setUnlockAmount] = useState('0') + + const [unlockAmountError, setUnlockAmountError] = useState() + + const [isUnlocking, setIsUnlocking] = useState(false) + + const chainId = useChainId() + const txSender = useTxSender() + + const { startTime } = useStartDate() + const todayInDays = getCurrentDays(startTime) + + const debouncedAmount = useDebounce(unlockAmount, 1000, '0') + const cleanedAmount = useMemo(() => (debouncedAmount.trim() === '' ? '0' : debouncedAmount.trim()), [debouncedAmount]) + + useEffect(() => { + if (debouncedAmount !== '0') { + trackSafeAppEvent(LOCK_EVENTS.CHANGE_UNLOCK_AMOUNT.action, LOCK_EVENTS.CHANGE_UNLOCK_AMOUNT.label) + } + }, [debouncedAmount]) + + const onCloseReceipt = () => { + setUnlockAmount('0') + setReceiptOpen(false) + } + + const currentBoostFunction = useMemo(() => getBoostFunction(todayInDays, 0, lockHistory), [todayInDays, lockHistory]) + const newBoostFunction = useMemo( + () => getBoostFunction(todayInDays, -Number(cleanedAmount), lockHistory), + [cleanedAmount, lockHistory, todayInDays], + ) + + const onChangeUnlockAmount = (event: ChangeEvent) => { + const newValue = event.target.value.replaceAll(',', '.') + const error = validateAmount(newValue || '0') + setUnlockAmount(newValue) + setUnlockAmountError(error) + } + + const validateAmount = useCallback( + (newAmount: string) => { + const numberAmount = Number(newAmount) + if (isNaN(numberAmount)) { + return 'The value must be a number' + } + const parsed = parseUnits(numberAmount.toString(), 18) + if (parsed.gt(currentlyLocked ?? '0')) { + return 'Amount exceeds your locked tokens.' + } + + if (parsed.lte(0)) { + return 'Amount must be greater than zero' + } + }, + [currentlyLocked], + ) + + const onSetToMax = useCallback(() => { + if (!currentlyLocked || BigNumber.from(currentlyLocked).eq(0)) { + return + } + setUnlockAmount(formatUnits(currentlyLocked, 18)) + }, [currentlyLocked]) + + const onUnlock = async () => { + setIsUnlocking(true) + const unlockTx = createUnlockTx(chainId, parseUnits(unlockAmount, 18)) + const newFinalBoost = newBoostFunction({ x: SEASON2_START }) + try { + await txSender?.sendTxs([unlockTx]) + trackSafeAppEvent(LOCK_EVENTS.UNLOCK_SUCCESS.action) + setReceiptInformation({ newFinalBoost, amount: unlockAmount }) + setUnlockAmount('0') + setReceiptOpen(true) + } catch (err) { + console.error(err) + } + setIsUnlocking(false) + } + + const isDisabled = !txSender || Boolean(unlockAmountError) || isUnlocking || cleanedAmount === '0' + + return ( + <> + + + Unlock + + + Unlocking tokens will result in reduced boost. Your unlocked tokens will be available in 24 hours. + + + + + + + + + + Select amount to unlock + { + event.target.select() + }} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: ( + + + + ), + }} + /> + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/TockenUnlocking/WithdrawWidget.tsx b/src/components/TockenUnlocking/WithdrawWidget.tsx new file mode 100644 index 00000000..78a91e8d --- /dev/null +++ b/src/components/TockenUnlocking/WithdrawWidget.tsx @@ -0,0 +1,107 @@ +import { LOCK_EVENTS } from '@/analytics/lockEvents' +import { Box, Typography, Grid, Paper, Button, CircularProgress, SvgIcon } from '@mui/material' +import { formatUnits } from 'ethers/lib/utils' +import { Odometer } from '../Odometer' +import Track from '../Track' +import SafeToken from '@/public/images/token.svg' +import ClockIcon from '@/public/images/clock.svg' + +import css from './styles.module.css' +import { BigNumber } from 'ethers' +import { useState } from 'react' +import { trackSafeAppEvent } from '@/utils/analytics' +import { createWithdrawTx } from '@/utils/lock' +import { useTxSender } from '@/hooks/useTxSender' +import { useChainId } from '@/hooks/useChainId' +import { UnlockEvent } from '@/hooks/useLockHistory' +import { formatAmount } from '@/utils/formatters' +import { DAY_IN_MS, formatDate } from '@/utils/date' + +export const WithdrawWidget = ({ + totalWithdrawable, + pendingUnlocks, +}: { + totalWithdrawable: BigNumber + pendingUnlocks: UnlockEvent[] | undefined +}) => { + const [isWithdrawing, setIsWithdrawing] = useState(false) + const txSender = useTxSender() + const chainId = useChainId() + const isTransactionPossible = !!txSender + + const onWithdraw = async () => { + setIsWithdrawing(true) + const withdrawTx = createWithdrawTx(chainId) + try { + await txSender?.sendTxs([withdrawTx]) + trackSafeAppEvent(LOCK_EVENTS.WITHDRAW_SUCCESS.action) + } catch (error) { + console.error(error) + } + + setIsWithdrawing(false) + } + + return ( + <> + + Withdraw + + The unlocked tokens will be available to withdraw in 24 hours. + + + palette.background.default, + color: ({ palette }) => palette.text.primary, + position: 'relative', + }} + > + + + + + Withdrawable + + + + SAFE + + + + + + + + + + + {pendingUnlocks?.map((nextUnlock) => ( + + + + {formatAmount(formatUnits(nextUnlock.amount, 18), 0)} SAFE {' '} + will be withdrawable starting {formatDate(new Date(Date.parse(nextUnlock.executionDate) + DAY_IN_MS))}. + + + ))} + + + + ) +} diff --git a/src/components/TockenUnlocking/index.tsx b/src/components/TockenUnlocking/index.tsx new file mode 100644 index 00000000..47b6dfd6 --- /dev/null +++ b/src/components/TockenUnlocking/index.tsx @@ -0,0 +1,56 @@ +import { Box, Link, Stack, Typography } from '@mui/material' + +import NextLink from 'next/link' +import { AppRoutes } from '@/config/routes' +import { toRelativeLockHistory } from '@/utils/lock' +import PaperContainer from '../PaperContainer' +import { UnlockStats } from './UnlockStats' +import { UnlockTokenWidget } from './UnlockTokenWidget' +import { useLockHistory } from '@/hooks/useLockHistory' +import { ChevronLeft } from '@mui/icons-material' + +import { useMemo } from 'react' +import { useSummarizedLockHistory } from '@/hooks/useSummarizedLockHistory' +import { WithdrawWidget } from './WithdrawWidget' +import { useStartDate } from '@/hooks/useStartDates' +import { NAVIGATION_EVENTS } from '@/analytics/navigation' +import Track from '../Track' + +const TokenUnlocking = () => { + const { startTime } = useStartDate() + const lockHistory = useLockHistory() + + const relativeLockHistory = useMemo(() => toRelativeLockHistory(lockHistory, startTime), [lockHistory, startTime]) + + const { totalLocked, totalUnlocked, totalWithdrawable, pendingUnlocks } = useSummarizedLockHistory(lockHistory) + + return ( + + + + palette.primary.main }} + > + + Back to main + + + + Unlock / Withdraw + + + + + + + + + + + + ) +} + +export default TokenUnlocking diff --git a/src/components/TockenUnlocking/styles.module.css b/src/components/TockenUnlocking/styles.module.css new file mode 100644 index 00000000..77e99c09 --- /dev/null +++ b/src/components/TockenUnlocking/styles.module.css @@ -0,0 +1,52 @@ +.bordered { + border-radius: 6px; + border: var(--mui-palette-border-light) 1px solid; +} + +.badge :global .MuiBadge-badge { + right: 4px; + bottom: 5px; + height: 6px; + min-width: 6px; +} + +.badge :global .MuiBadge-badge { + bottom: 4px; + right: 4px; + background-color: var(--mui-palette-secondary-main); +} + +.maxButton { + border: 0; + background: none; + color: var(--mui-palette-primary-main); + padding: 0; + font-size: 16px; + font-weight: bold; + cursor: pointer; +} + +.gauge { + transition: width 200ms; +} + +.amountDisplay { + line-height: 2em; + font-weight: 700; + display: inline-flex; + gap: 4px; + align-items: center; + z-index: 1; +} + +.nextWithdrawal { + border-radius: 6px; + border: 1px var(--mui-palette-border-main) solid; + color: var(--mui-palette-text-secondary); + padding: 16px; + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + margin-top: 8px; +} diff --git a/src/components/TokenAmount/index.tsx b/src/components/TokenAmount/index.tsx new file mode 100644 index 00000000..f6872ddc --- /dev/null +++ b/src/components/TokenAmount/index.tsx @@ -0,0 +1,45 @@ +import { Paper, Stack, Typography, Grid, Skeleton, Box } from '@mui/material' +import { formatUnits } from 'ethers/lib/utils' +import { Odometer } from '../Odometer' + +import SafeToken from '@/public/images/token.svg' +import css from './styles.module.css' +import { BigNumberish } from 'ethers' + +export const TokenAmount = ({ loading, amount, label }: { loading: boolean; amount: BigNumberish; label: string }) => { + return ( + palette.background.default, + color: ({ palette }) => palette.text.primary, + position: 'relative', + }} + > + + + + {label} + + + + {loading ? ( + + ) : ( + <> + SAFE + + )} + + + + + + ) +} diff --git a/src/components/TokenAmount/styles.module.css b/src/components/TokenAmount/styles.module.css new file mode 100644 index 00000000..7f98749a --- /dev/null +++ b/src/components/TokenAmount/styles.module.css @@ -0,0 +1,12 @@ +.amountDisplay { + line-height: 2em; + font-weight: 700; + display: inline-flex; + gap: 4px; + align-items: center; + z-index: 1; +} + +.content svg { + margin-bottom: 6px; +} diff --git a/src/components/TokenLocking/ActivityRewardsInfo.tsx b/src/components/TokenLocking/ActivityRewardsInfo.tsx new file mode 100644 index 00000000..f0f0b55b --- /dev/null +++ b/src/components/TokenLocking/ActivityRewardsInfo.tsx @@ -0,0 +1,100 @@ +import { Box, Divider, Link, SvgIcon, Tooltip, Typography } from '@mui/material' +import css from './styles.module.css' +import clsx from 'clsx' +import StarIcon from '@/public/images/star.svg' +import { useOwnRank } from '@/hooks/useLeaderboard' +import Asterix from '@/public/images/asterix.svg' +import { AccordionContainer } from '@/components/AccordionContainer' +import NextLink from 'next/link' + +import PaperContainer from '../PaperContainer' +import { ReactNode } from 'react' +import { AppRoutes } from '@/config/routes' +import { InfoOutlined } from '@mui/icons-material' + +const Step = ({ active, title, description }: { active: boolean; title?: ReactNode; description?: string }) => { + return ( +
+ + {title} + {!active && ( + + Coming soon + + )} + + {description && ( + + {description} + + )} +
+ ) +} + +export const ActivityRewardsInfo = () => { + const ownRankResult = useOwnRank() + const { data: ownRank } = ownRankResult + + return ( + + + + + +
+ {ownRank && ( + <> + + + Your current ranking + + + + + + + #{ownRank.position} + + + )} + + How does it work? + + + + + + + Repeat! + +
+ + + View eligible activities + +
+
+
+ ) +} diff --git a/src/components/TokenLocking/BoostBreakdown.tsx b/src/components/TokenLocking/BoostBreakdown.tsx new file mode 100644 index 00000000..455ba57b --- /dev/null +++ b/src/components/TokenLocking/BoostBreakdown.tsx @@ -0,0 +1,101 @@ +import { floorNumber } from '@/utils/boost' +import { SignalCellularAlt, SignalCellularAlt1Bar, SignalCellularAlt2Bar } from '@mui/icons-material' +import { Stack, Typography, Box } from '@mui/material' +import BoostCounter from '../BoostCounter' +import { BoostMeter } from './BoostMeter' + +import EmptyBreakdown from '@/public/images/empty-breakdown.svg' + +import css from './styles.module.css' + +const BoostStrengthSignal = ({ boost, color }: { boost: number; color: 'primary' | 'warning' | undefined }) => { + const strength = Math.floor((boost - 1) / 0.25) + + if (strength === 0) { + return null + } + + const iconProps = { + fontSize: 'large', + color, + sx: { + position: 'relative', + left: -35, + }, + } as const + + if (strength === 1) { + return + } + if (strength === 2) { + return + } + return +} + +export const BoostBreakdown = ({ + realizedBoost, + currentFinalBoost, + newFinalBoost, + boostPrediction, + isLock, +}: { + realizedBoost: number + currentFinalBoost: number + newFinalBoost: number + boostPrediction?: number + isLock: boolean +}) => { + const isVisibleDifference = Math.abs(floorNumber(currentFinalBoost, 2) - floorNumber(newFinalBoost, 2)) > 0 + + // If everything is 1.0 there are no relevant locks / no amount is put in + const isInitialState = realizedBoost === 1 && currentFinalBoost === 1 && newFinalBoost === 1 + + return ( + + + + + {!isInitialState && ( + <> + + + + )} + + Current boost: {floorNumber(currentFinalBoost, 2)}x + + + {isInitialState ? ( + + + + Start locking tokens to get your points boost. + + + ) : ( + + + + Your boost + + + )} + + + + + + + + ) +} diff --git a/src/components/TokenLocking/BoostGraph/ActionNavigation/ActionNavigation.tsx b/src/components/TokenLocking/BoostGraph/ActionNavigation/ActionNavigation.tsx new file mode 100644 index 00000000..69538737 --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/ActionNavigation/ActionNavigation.tsx @@ -0,0 +1,38 @@ +import { NAVIGATION_EVENTS } from '@/analytics/navigation' +import { AppRoutes } from '@/config/routes' +import { Typography, Stack, Button, Box } from '@mui/material' +import { useRouter } from 'next/router' +import Track from '../../../Track' +import clsx from 'clsx' + +import css from './styles.module.css' + +export const ActionNavigation = ({ disabled }: { disabled: boolean }) => { + const router = useRouter() + + const onNavigate = (route: (typeof AppRoutes)[keyof typeof AppRoutes]) => async () => { + router.push(route) + } + + const onUnlockAndWithdraw = onNavigate(AppRoutes.unlock) + return ( + + + + Want to withdraw your tokens? + + + + + + + ) +} diff --git a/src/components/TokenLocking/BoostGraph/ActionNavigation/styles.module.css b/src/components/TokenLocking/BoostGraph/ActionNavigation/styles.module.css new file mode 100644 index 00000000..f54823e2 --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/ActionNavigation/styles.module.css @@ -0,0 +1,16 @@ +.buttonContainer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24px; + gap: 8px; + border-radius: 6px; + border: var(--mui-palette-border-light) 1px solid; +} + +@media (max-width: 600px) { + .buttonContainer { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/src/components/TokenLocking/BoostGraph/ArrowDownLabel.tsx b/src/components/TokenLocking/BoostGraph/ArrowDownLabel.tsx new file mode 100644 index 00000000..f162abf7 --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/ArrowDownLabel.tsx @@ -0,0 +1,30 @@ +import { useTheme } from '@mui/material/styles' +import { Background, VictoryLabel, VictoryLabelProps, VictoryLabelStyleObject } from 'victory' + +export const ArrowDownLabel = ({ backgroundColor, ...props }: VictoryLabelProps & { backgroundColor: string }) => { + const theme = useTheme() + if (props.x === undefined || props.y === undefined || props.text === '') { + return null + } + return ( + <> + + } + backgroundStyle={[{ fill: backgroundColor }]} + style={{ ...props.style, fill: theme.palette.background.main } as VictoryLabelStyleObject} + dy={-36} + /> + + ) +} diff --git a/src/components/TokenLocking/BoostGraph/AxisTopLabel.tsx b/src/components/TokenLocking/BoostGraph/AxisTopLabel.tsx new file mode 100644 index 00000000..a615fdcb --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/AxisTopLabel.tsx @@ -0,0 +1,21 @@ +import { formatDay } from '@/utils/date' +import { VictoryLabel, VictoryLabelProps } from 'victory' + +export const AxisTopLabel = ({ startTime, ...props }: VictoryLabelProps & { startTime: number; datum?: number }) => { + const days = props.datum + + if (days === undefined) { + return null + } + + const dateLabel = formatDay(days, startTime) + + const style = props.style && !Array.isArray(props.style) ? props.style : {} + + return ( + <> + + + + ) +} diff --git a/src/components/TokenLocking/BoostGraph/BoostGradients.tsx b/src/components/TokenLocking/BoostGraph/BoostGradients.tsx new file mode 100644 index 00000000..3b9e84a6 --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/BoostGradients.tsx @@ -0,0 +1,48 @@ +import { useTheme } from '@mui/material/styles' + +/** + * Renders svg gradient definitions. + */ +export const BoostGradients = () => { + const theme = useTheme() + return ( + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/TokenLocking/BoostGraph/BoostGraph.tsx b/src/components/TokenLocking/BoostGraph/BoostGraph.tsx new file mode 100644 index 00000000..f8c7101b --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/BoostGraph.tsx @@ -0,0 +1,229 @@ +import { SEASON1_START, SEASON2_START } from '@/config/constants' +import { floorNumber, getBoostFunction } from '@/utils/boost' +import { getCurrentDays } from '@/utils/date' +import { formatAmount } from '@/utils/formatters' +import { LockHistory } from '@/utils/lock' +import { useTheme } from '@mui/material/styles' +import { useMemo } from 'react' +import { VictoryAxis, VictoryChart, VictoryLine, VictoryScatter, DomainTuple, ForAxes, VictoryArea } from 'victory' +import { ArrowDownLabel } from './ArrowDownLabel' +import { AxisTopLabel } from './AxisTopLabel' +import { BoostGradients } from './BoostGradients' +import { generatePointsFromHistory } from './helper' +import { ScatterDot } from './ScatterDot' + +import { useVictoryTheme } from './theme' +import { useStartDate } from '@/hooks/useStartDates' + +const DOMAIN: ForAxes = { x: [-5, SEASON2_START + 5], y: [0.8, 2.3] } + +export const BoostGraph = ({ + lockedAmount, + pastLocks, + isLock, +}: { + lockedAmount: number + pastLocks: LockHistory[] + isLock: boolean +}) => { + const theme = useTheme() + const victoryTheme = useVictoryTheme() + const { startTime } = useStartDate() + + const now = useMemo(() => getCurrentDays(startTime), [startTime]) + + const currentBoostFunction = useMemo(() => getBoostFunction(now, 0, pastLocks), [now, pastLocks]) + const newBoostFunction = useMemo(() => getBoostFunction(now, lockedAmount, pastLocks), [lockedAmount, now, pastLocks]) + + const pastLockPoints = useMemo(() => generatePointsFromHistory(pastLocks, now), [pastLocks, now]) + + const format = (value: number) => formatAmount(floorNumber(value, 2), 2) + + const currentBoostDataPoints = useMemo( + () => [ + ...pastLockPoints, + { x: 0, y: currentBoostFunction({ x: 0 }) }, + { x: now, y: currentBoostFunction({ x: now }) }, + { x: SEASON1_START, y: currentBoostFunction({ x: SEASON1_START }) }, + { x: SEASON2_START, y: currentBoostFunction({ x: SEASON2_START }) }, + ], + [currentBoostFunction, now, pastLockPoints], + ) + + const projectedBoostDataPoints = useMemo( + () => [ + ...pastLockPoints, + { x: 0, y: newBoostFunction({ x: 0 }) }, + { x: now, y: newBoostFunction({ x: now }) }, + { x: SEASON1_START, y: newBoostFunction({ x: SEASON1_START }) }, + { x: SEASON2_START, y: newBoostFunction({ x: SEASON2_START }) }, + ], + [newBoostFunction, now, pastLockPoints], + ) + + return ( +
+ + + + + + + + + Number(d).toFixed(1) + 'x'} + theme={victoryTheme} + style={{ + tickLabels: { + padding: 16, + }, + }} + /> + { + if (value === 0) { + return 'Start' + } + if (value === SEASON1_START) { + return 'Early boost end' + } + + return 'Season 1 end' + }} + domain={DOMAIN} + style={{ + grid: { stroke: theme.palette.primary.light, strokeDasharray: 2, strokeDashoffset: 2 }, + ticks: { size: 5 }, + tickLabels: { fontSize: 12, padding: 16, fill: theme.palette.primary.light }, + }} + tickLabelComponent={} + theme={victoryTheme} + /> + { + if (value === now) { + return 'Today' + } + return '' + }} + domain={DOMAIN} + style={{ + grid: { stroke: theme.palette.text.primary, strokeDasharray: 2, strokeDashoffset: 2 }, + ticks: { size: 5 }, + tickLabels: { fontSize: 12, padding: 0, fill: theme.palette.text.primary }, + }} + theme={victoryTheme} + /> + + + } + domain={DOMAIN} + data={pastLockPoints} + theme={victoryTheme} + /> + + } + size={4} + dataComponent={ + + } + domain={DOMAIN} + data={[ + { x: now, y: newBoostFunction({ x: now }) }, + { x: SEASON2_START, y: newBoostFunction({ x: SEASON2_START }) }, + ]} + theme={victoryTheme} + /> + +
+ ) +} diff --git a/src/components/TokenLocking/BoostGraph/ScatterDot.tsx b/src/components/TokenLocking/BoostGraph/ScatterDot.tsx new file mode 100644 index 00000000..1f5c83c7 --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/ScatterDot.tsx @@ -0,0 +1,89 @@ +import { Point, PointProps } from 'victory' +import { useTheme } from '@mui/material/styles' +import { useState } from 'react' +import { ArrowDownLabel } from './ArrowDownLabel' +import { floorNumber } from '@/utils/boost' +import { SEASON2_START } from '@/config/constants' + +/** + * Draws different circles based on the date + * @param props + * @returns + */ +export const ScatterDot = ({ + today, + backgroundColor, + ...props +}: PointProps & { today: number; backgroundColor: string }) => { + const theme = useTheme() + + const [hovered, setHovered] = useState(false) + + // 39, 25, 12 ,6 + if (props.datum.x === today) { + return ( + <> + + + + + setHovered(true)} + onMouseLeave={() => setHovered(false)} + /> + {hovered && props.datum.x !== SEASON2_START && ( + + )} + + ) + } + return ( + <> + + + setHovered(true)} + onMouseLeave={() => setHovered(false)} + /> + {hovered && props.datum.x !== SEASON2_START && ( + + )} + + ) +} diff --git a/src/components/TokenLocking/BoostGraph/graphConstants.ts b/src/components/TokenLocking/BoostGraph/graphConstants.ts new file mode 100644 index 00000000..9c82e977 --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/graphConstants.ts @@ -0,0 +1,3 @@ +export const BIG_SCREEN = { height: 280, padding: 52, width: 524 } + +export const SMALL_SCREEN = { height: 180, padding: 52, width: 476 } diff --git a/src/components/TokenLocking/BoostGraph/helper.ts b/src/components/TokenLocking/BoostGraph/helper.ts new file mode 100644 index 00000000..4efafd2c --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/helper.ts @@ -0,0 +1,15 @@ +import { getBoostFunction } from '@/utils/boost' +import { LockHistory } from '@/utils/lock' + +export const generatePointsFromHistory = (pastLocks: LockHistory[], nowInDays: number): { x: number; y: number }[] => { + const boostFunction = getBoostFunction(nowInDays, 0, pastLocks) + // find each individual day + const days = pastLocks.map((lock) => lock.day).filter((day, index, days) => days.indexOf(day) === index) + + return days + .map((day) => ({ + x: day, + y: boostFunction({ x: day }), + })) + .slice(0, -1) +} diff --git a/src/components/TokenLocking/BoostGraph/theme.ts b/src/components/TokenLocking/BoostGraph/theme.ts new file mode 100644 index 00000000..944cfa3c --- /dev/null +++ b/src/components/TokenLocking/BoostGraph/theme.ts @@ -0,0 +1,194 @@ +import { useMediaQuery } from '@mui/material' +import { useTheme } from '@mui/material/styles' +import { VictoryThemeDefinition } from 'victory' +import { SMALL_SCREEN, BIG_SCREEN } from './graphConstants' + +const colors = ['#DDDEE0', '#525252', '#737373', '#969696', '#bdbdbd', '#d9d9d9', '#f0f0f0'] +const grey = '#A1A3A7' + +// Typography +const sansSerif = "'DM Sans'" +const letterSpacing = 'normal' +const fontSize = '12px' + +// Strokes +const strokeLinecap = 'round' +const strokeLinejoin = 'round' + +// Put it all together... +export const useVictoryTheme = (): VictoryThemeDefinition => { + const muiTheme = useTheme() + + const isSmallScreen = useMediaQuery(muiTheme.breakpoints.down('sm')) + + // Layout + const baseProps = { ...(isSmallScreen ? SMALL_SCREEN : BIG_SCREEN), colorScale: colors } + const fontColor = muiTheme.palette.text.secondary + + // Labels + const baseLabelStyles = { + fontFamily: sansSerif, + fontSize, + letterSpacing, + padding: 8, + backgroundColor: 'black', + fill: fontColor, + stroke: 'transparent', + } + + const centeredLabelStyles = Object.assign({ textAnchor: 'middle' }, baseLabelStyles) + + return { + area: Object.assign( + { + style: { + data: { + fill: fontColor, + }, + labels: baseLabelStyles, + }, + }, + baseProps, + ), + axis: Object.assign( + { + style: { + axis: { + fill: 'transparent', + stroke: 'none', + strokeWidth: 1, + strokeLinecap, + strokeLinejoin, + }, + axisLabel: Object.assign({}, centeredLabelStyles, { + padding: 16, + }), + grid: { + fill: 'none', + stroke: 'none', + }, + ticks: { + fill: 'transparent', + size: 1, + stroke: 'transparent', + }, + tickLabels: baseLabelStyles, + }, + }, + baseProps, + ), + chart: baseProps, + errorbar: Object.assign( + { + borderWidth: 8, + style: { + data: { + fill: 'transparent', + stroke: fontColor, + strokeWidth: 2, + }, + labels: baseLabelStyles, + }, + }, + baseProps, + ), + group: Object.assign( + { + colorScale: colors, + }, + baseProps, + ), + histogram: Object.assign( + { + style: { + data: { + fill: grey, + stroke: fontColor, + strokeWidth: 2, + }, + labels: baseLabelStyles, + }, + }, + baseProps, + ), + legend: { + colorScale: colors, + gutter: 10, + orientation: 'vertical', + titleOrientation: 'top', + style: { + data: { + type: 'circle', + }, + labels: baseLabelStyles, + title: Object.assign({}, baseLabelStyles, { padding: 5 }), + }, + }, + line: Object.assign( + { + style: { + data: { + fill: 'transparent', + stroke: fontColor, + strokeWidth: 2, + }, + labels: baseLabelStyles, + }, + }, + baseProps, + ), + scatter: Object.assign( + { + style: { + labels: baseLabelStyles, + }, + }, + baseProps, + ), + pie: { + style: { + data: { + padding: 10, + stroke: 'transparent', + strokeWidth: 1, + }, + labels: Object.assign({}, baseLabelStyles, { padding: 20 }), + }, + colorScale: colors, + width: 400, + height: 400, + padding: 50, + }, + tooltip: { + style: Object.assign({}, baseLabelStyles, { padding: 0, pointerEvents: 'none' }), + flyoutStyle: { + stroke: fontColor, + strokeWidth: 1, + fill: '#f0f0f0', + pointerEvents: 'none', + }, + flyoutPadding: 5, + cornerRadius: 5, + pointerLength: 10, + }, + voronoi: Object.assign( + { + style: { + data: { + fill: 'transparent', + stroke: 'transparent', + strokeWidth: 0, + }, + labels: Object.assign({}, baseLabelStyles, { padding: 5, pointerEvents: 'none' }), + flyout: { + stroke: fontColor, + strokeWidth: 1, + fill: '#f0f0f0', + pointerEvents: 'none', + }, + }, + }, + baseProps, + ), + } +} diff --git a/src/components/TokenLocking/BoostMeter.tsx b/src/components/TokenLocking/BoostMeter.tsx new file mode 100644 index 00000000..18baf8c5 --- /dev/null +++ b/src/components/TokenLocking/BoostMeter.tsx @@ -0,0 +1,99 @@ +import { floorNumber, getTimeFactor } from '@/utils/boost' +import { getCurrentDays } from '@/utils/date' +import { AccessTime } from '@mui/icons-material' +import { Box, LinearProgress, Stack, Tooltip, Typography } from '@mui/material' +import { ReactElement, useMemo } from 'react' + +import { useTheme } from '@mui/material/styles' +import { useStartDate } from '@/hooks/useStartDates' +import { SEASON1_START } from '@/config/constants' + +export const BoostMeter = ({ + isLock, + isVisibleDifference, + prediction, +}: { + isLock: boolean + isVisibleDifference: boolean + prediction?: number +}) => { + const { startTime } = useStartDate() + + const now = useMemo(() => getCurrentDays(startTime), [startTime]) + + const fullBoostDaysLeft = SEASON1_START - now + 1 + + const currentTimeFactor = getTimeFactor(now) + const value = currentTimeFactor * 100 + + let boostMeterInfo: ReactElement | null = null + + const theme = useTheme() + + if (isLock) { + if (fullBoostDaysLeft > 0) { + boostMeterInfo = ( + + Lock within the next {fullBoostDaysLeft} days + to get the highest boost. + + ) + } else if (isVisibleDifference && prediction) { + boostMeterInfo = ( + + If you lock in 10 days your boost will only be{' '} + {floorNumber(prediction, 2)}x + + ) + } else { + boostMeterInfo = + value > 25 ? ( + The earlier you lock the higher the boost. + ) : ( + Your last chance to get the early boost. + ) + } + } else { + boostMeterInfo = ( + You will stop getting the boost for unlocked tokens. + ) + } + + return ( + + 50 ? 'primary' : 'warning'} + sx={{ + height: '100%', + border: ({ palette }) => `4px solid ${palette.border.light}`, + padding: '2px', + borderRadius: '8px', + backgroundColor: ({ palette }) => palette.border.main, + '& .MuiLinearProgress-bar': { + borderRadius: '6px', + transform: () => { + return `translateY(${100 - value}%) !important` + }, + }, + }} + /> + + + + + + Early boost meter + + {boostMeterInfo} + + + ) +} diff --git a/src/components/TokenLocking/CurrentStats.tsx b/src/components/TokenLocking/CurrentStats.tsx new file mode 100644 index 00000000..025a780b --- /dev/null +++ b/src/components/TokenLocking/CurrentStats.tsx @@ -0,0 +1,25 @@ +import { Grid } from '@mui/material' +import { BigNumberish } from 'ethers' +import { TokenAmount } from '../TokenAmount' + +export const CurrentStats = ({ + loading, + safeBalance, + currentlyLocked, +}: { + safeBalance: BigNumberish + currentlyLocked: BigNumberish + loading: boolean +}) => { + return ( + + + + + + + + + + ) +} diff --git a/src/components/TokenLocking/Leaderboard.tsx b/src/components/TokenLocking/Leaderboard.tsx new file mode 100644 index 00000000..329e20e4 --- /dev/null +++ b/src/components/TokenLocking/Leaderboard.tsx @@ -0,0 +1,281 @@ +import { + Box, + Link, + Paper, + Skeleton, + SvgIcon, + Table, + TableBody, + TableContainer, + TableHead, + TableRow, + Typography, + useMediaQuery, +} from '@mui/material' +import { useTheme } from '@mui/material/styles' + +import { styled } from '@mui/material/styles' +import TableCell, { tableCellClasses } from '@mui/material/TableCell' +import { Identicon } from '../Identicon' +import FirstPlaceIcon from '@/public/images/leaderboard-first-place.svg' +import SecondPlaceIcon from '@/public/images/leaderboard-second-place.svg' +import ThirdPlaceIcon from '@/public/images/leaderboard-third-place.svg' +import TitleStar from '@/public/images/leaderboard-title-star.svg' +import { type LeaderboardEntry, useGlobalLeaderboardPage, useOwnRank } from '@/hooks/useLeaderboard' +import { formatAmount } from '@/utils/formatters' +import { formatEther } from 'ethers/lib/utils' +import { ReactElement, useState } from 'react' +import { useEnsLookup } from '@/hooks/useEnsLookup' +import Track from '../Track' +import { NAVIGATION_EVENTS } from '@/analytics/navigation' +import { useChainId } from '@/hooks/useChainId' +import { CHAIN_EXPLORER_URL } from '@/config/constants' +import { ExternalLink } from '../ExternalLink' + +const PAGE_SIZE = 10 + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: 'none', + color: theme.palette.common.white, + border: 0, + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + borderTop: `1px ${theme.palette.background.paper} solid`, + borderBottom: `1px ${theme.palette.background.paper} solid`, + }, +})) + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + // hide last border + '&:hover td': { + backgroundColor: theme.palette.background.main, + }, + '& td:first-child': { + borderRadius: '6px 0 0 6px', + }, + '& td:last-child': { + borderRadius: '0 6px 6px 0', + }, +})) + +const HighlightedTableRow = styled(TableRow)(({ theme }) => ({ + '& td:first-child': { + borderRadius: '6px 0 0 6px', + }, + '& td:last-child': { + borderRadius: '0 6px 6px 0', + }, + '& td': { + backgroundColor: theme.palette.background.light, + background: + 'linear-gradient(var(--mui-palette-background-light), var(--mui-palette-background-light)) padding-box,linear-gradient(to bottom, #5FDDFF 12.5%, #12FF80 88.07%) border-box', + border: '1px solid transparent !important', + }, + '& td:not(:first-child)': { + borderLeft: 'none !important', + }, + '& td:not(:last-child)': { + borderRight: 'none !important', + }, +})) + +const StyledTable = styled(Table)(({ theme }) => ({ + borderCollapse: 'separate', +})) + +export const shortenAddress = (address: string, length = 4): string => { + if (!address) { + return '' + } + + return `${address.slice(0, length + 2)}...${address.slice(-length)}` +} + +const LookupAddress = ({ address }: { address: string }) => { + const name = useEnsLookup(address) + const theme = useTheme() + const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')) + const displayAddress = isSmallScreen ? shortenAddress(address) : address + const chainId = useChainId() + const explorerURL = CHAIN_EXPLORER_URL[chainId] + + return ( + <> + + + {explorerURL ? ( + {name ? name : displayAddress} + ) : ( + {name ? name : displayAddress} + )} + + + ) +} + +const Ranking = ({ position }: { position: number }) => { + return position <= 3 ? ( + + ) : ( + <>{position} + ) +} + +const OwnEntry = ({ entry }: { entry: LeaderboardEntry | undefined }) => { + if (entry) { + return ( + + + + + + + + {formatAmount(formatEther(entry.lockedAmount), 0)} + + ) + } + + return null +} + +const LeaderboardPage = ({ + index, + onLoadMore, + ownEntry, +}: { + index: number + onLoadMore?: () => void + ownEntry: LeaderboardEntry | undefined +}): ReactElement => { + const leaderboardPage = useGlobalLeaderboardPage(PAGE_SIZE, index * PAGE_SIZE) + const rows = leaderboardPage?.results ?? [] + const isLeaderboardEmpty = index === 0 && (!rows || rows.length === 0) + + if (leaderboardPage === undefined) { + return ( + <> + + + + + + + + + + + + + ) + } + if (isLeaderboardEmpty) { + return ( + <> + + + No entries + + + + + ) + } + return ( + <> + {rows.map((row) => { + return row.holder === ownEntry?.holder ? ( + + ) : ( + + + + + + + + {formatAmount(formatEther(row.lockedAmount), 0)} + + ) + })} + {onLoadMore && leaderboardPage?.next && ( + + + + + Show more + + + + + )} + + ) +} + +export const Leaderboard = () => { + const [pages, setPages] = useState(1) + const ownLeaderboardEntry = useOwnRank() + const { data: ownEntry } = ownLeaderboardEntry + + return ( + + + + + + Locking Leaderboard + + + Higher ranking means higher chances to get rewards. + + + + + + + + + + + + Tokens Locked + + + + + {ownEntry && ownEntry?.position > PAGE_SIZE && } + {Array.from(new Array(pages)).map((_, index) => ( + setPages((prev) => prev + 1) : undefined} + ownEntry={ownEntry} + /> + ))} + + + + + ) +} diff --git a/src/components/TokenLocking/LockTokenWidget.tsx b/src/components/TokenLocking/LockTokenWidget.tsx new file mode 100644 index 00000000..3f08fb44 --- /dev/null +++ b/src/components/TokenLocking/LockTokenWidget.tsx @@ -0,0 +1,238 @@ +import { Typography, Stack, Grid, TextField, InputAdornment, Button, Box, CircularProgress } from '@mui/material' +import NorthEastIcon from '@mui/icons-material/NorthEast' +import { formatUnits, parseUnits } from 'ethers/lib/utils' +import SafeToken from '@/public/images/token.svg' + +import { BoostGraph } from './BoostGraph/BoostGraph' + +import css from './styles.module.css' +import { createLockTx, toRelativeLockHistory } from '@/utils/lock' +import { createApproveTx } from '@/utils/safe-token' +import { useState, ChangeEvent, useMemo, useCallback, useEffect } from 'react' +import { BigNumber, BigNumberish } from 'ethers' +import { useChainId } from '@/hooks/useChainId' +import { getBoostFunction } from '@/utils/boost' +import { useLockHistory } from '@/hooks/useLockHistory' +import { useDebounce } from '@/hooks/useDebounce' +import { SAFE_PASS_HELP_ARTICLE_URL, SEASON2_START, UNLIMITED_APPROVAL_AMOUNT } from '@/config/constants' +import { getCurrentDays } from '@/utils/date' +import { BoostBreakdown } from './BoostBreakdown' +import Track from '../Track' +import { LOCK_EVENTS } from '@/analytics/lockEvents' +import { trackSafeAppEvent } from '@/utils/analytics' +import MilesReceipt from '@/components/TokenLocking/MilesReceipt' +import { BaseTransaction, useTxSender } from '@/hooks/useTxSender' +import { useSafeTokenLockingAllowance } from '@/hooks/useSafeTokenBalance' +import { useStartDate } from '@/hooks/useStartDates' +import { NAVIGATION_EVENTS } from '@/analytics/navigation' +import { ExternalLink } from '../ExternalLink' +import { formatAmount } from '@/utils/formatters' + +export const LockTokenWidget = ({ safeBalance }: { safeBalance: BigNumberish | undefined }) => { + const [receiptOpen, setReceiptOpen] = useState(false) + const chainId = useChainId() + const txSender = useTxSender() + const { startTime } = useStartDate() + const todayInDays = getCurrentDays(startTime) + const { data: safeTokenAllowance, isLoading: isAllowanceLoading } = useSafeTokenLockingAllowance() + + const pastLocks = useLockHistory() + + const relativeLockHistory = useMemo(() => toRelativeLockHistory(pastLocks, startTime), [pastLocks, startTime]) + + const [amount, setAmount] = useState('0') + + const [amountError, setAmountError] = useState(undefined) + + const [isLocking, setIsLocking] = useState(false) + + const [receiptInformation, setReceiptInformation] = useState<{ newFinalBoost: number; amount: string }>({ + amount: '0', + newFinalBoost: 1, + }) + + const onCloseReceipt = () => { + setReceiptOpen(false) + } + + const debouncedAmount = useDebounce(amount, 1000, '0') + const cleanedAmount = useMemo(() => (debouncedAmount.trim() === '' ? '0' : debouncedAmount.trim()), [debouncedAmount]) + + useEffect(() => { + if (debouncedAmount !== '0') { + trackSafeAppEvent(LOCK_EVENTS.CHANGE_LOCK_AMOUNT.action, LOCK_EVENTS.CHANGE_LOCK_AMOUNT.label) + } + }, [debouncedAmount]) + + const currentBoostFunction = useMemo( + () => getBoostFunction(todayInDays, 0, relativeLockHistory), + [relativeLockHistory, todayInDays], + ) + const newBoostFunction = useMemo( + () => getBoostFunction(todayInDays, Number(cleanedAmount), relativeLockHistory), + [cleanedAmount, relativeLockHistory, todayInDays], + ) + + const boostIn10DaysFunction = useMemo( + () => getBoostFunction(todayInDays + 10, Number(cleanedAmount), relativeLockHistory), + [cleanedAmount, relativeLockHistory, todayInDays], + ) + + const validateAmount = useCallback( + (newAmount: string) => { + const numberAmount = Number(newAmount) + if (isNaN(numberAmount)) { + return 'The value must be a number' + } + const parsed = parseUnits(numberAmount.toString(), 18) + if (parsed.gt(safeBalance ?? '0')) { + return 'Amount exceeds balance' + } + if (parsed.lte(0)) { + return 'Amount must be greater than zero' + } + }, + [safeBalance], + ) + + const onChangeAmount = useCallback( + (event: ChangeEvent) => { + const newValue = event.target.value.replaceAll(',', '.') + const error = validateAmount(newValue || '0') + + setAmount(newValue) + setAmountError(error) + }, + [validateAmount], + ) + + const onSetToMax = useCallback(() => { + if (!safeBalance) { + return + } + setAmount(formatUnits(safeBalance, 18)) + }, [safeBalance]) + + const isMaxDisabled = BigNumber.from(0).gte(safeBalance ?? 0) + + const onLockTokens = async () => { + if (!txSender) { + throw new Error('Cannot lock tokens without connected wallet') + } + try { + const amountToLockWei = parseUnits(amount, 18) + setIsLocking(true) + let txs: BaseTransaction[] = [] + if (BigNumber.from(safeTokenAllowance).lt(amountToLockWei)) { + // Approval is too low for the locking operation + const approvalAmount = txSender?.isBatchingSupported ? amountToLockWei : UNLIMITED_APPROVAL_AMOUNT + txs.push(createApproveTx(chainId, approvalAmount)) + } + txs.push(createLockTx(chainId, amountToLockWei)) + + if (txSender?.isBatchingSupported) { + await txSender.sendTxs(txs) + } else { + for (let i = 0; i < txs.length; i++) { + await txSender?.sendTxs([txs[i]]) + } + } + const finalBoostAfterLocking = newBoostFunction({ x: SEASON2_START }) + trackSafeAppEvent(LOCK_EVENTS.LOCK_SUCCESS.action) + setReceiptInformation({ newFinalBoost: finalBoostAfterLocking, amount }) + setAmount('0') + setReceiptOpen(true) + } catch (error) { + console.error(error) + } finally { + setIsLocking(false) + } + } + + const isDisabled = isAllowanceLoading || Boolean(amountError) || isLocking || cleanedAmount === '0' + + return ( + <> + + + + + Lock tokens to boost your points + + + + More about the boost + + + + + + + + + + Select amount to lock + { + event.target.select() + }} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: ( + + + + ), + }} + className={css.input} + /> + + + + + + + + + Balance: {formatAmount(formatUnits(safeBalance ?? '0', 18), 2)} + + + + + + + + + ) +} diff --git a/src/components/TokenLocking/MilesReceipt.tsx b/src/components/TokenLocking/MilesReceipt.tsx new file mode 100644 index 00000000..dd51a517 --- /dev/null +++ b/src/components/TokenLocking/MilesReceipt.tsx @@ -0,0 +1,102 @@ +import { IconButton, Modal, Stack, SvgIcon, Typography } from '@mui/material' +import css from './styles.module.css' +import SafePass from '@/public/images/safe-pass.svg' +import Barcode from '@/public/images/barcode.svg' +import { NorthRounded, SouthRounded } from '@mui/icons-material' +import XIcon from '@mui/icons-material/X' +import CloseIcon from '@mui/icons-material/Close' +import clsx from 'clsx' +import { formatAmount } from '@/utils/formatters' +import { floorNumber } from '@/utils/boost' +import { ExternalLink } from '@/components/ExternalLink' + +const TWEET_CONTENT = 'Just got my @Safe%7BPass%7D to join the smart account revolution!' + +const MilesReceipt = ({ + open, + onClose, + amount, + newFinalBoost, + isUnlock = false, +}: { + open: boolean + onClose: () => void + amount: string + newFinalBoost: number + isUnlock?: boolean +}) => { + const ArrowIcon = isUnlock ? SouthRounded : NorthRounded + + return ( + + <> +
+ + + + + {isUnlock ? 'Unclocking' : 'Locking'} started... + + + You successfully started {isUnlock ? 'unlocking' : 'locking'} your SAFE tokens.
+ Once the transaction is signed and executed{' '} + {isUnlock + ? 'the tokens will be available to withdraw in 24 hours.' + : 'your tokens will be locked and you will get boosted.'} +
+ + {!isUnlock && ( + + + Share on + + + )} +
+ + + Your overview + + + + + + {formatAmount(amount, 0)} + + + Tokens {isUnlock ? 'unlocked' : 'locked'} + + + +
+ + + + {formatAmount(floorNumber(newFinalBoost, 2), 2)}x + + + Your boost + + +
+ + + +
+ +
+ + + ) +} + +export default MilesReceipt diff --git a/src/components/TokenLocking/index.tsx b/src/components/TokenLocking/index.tsx new file mode 100644 index 00000000..4e713349 --- /dev/null +++ b/src/components/TokenLocking/index.tsx @@ -0,0 +1,73 @@ +import { Box, Grid, Stack, Typography } from '@mui/material' +import { useSafeTokenBalance } from '@/hooks/useSafeTokenBalance' +import { Leaderboard } from './Leaderboard' +import { CurrentStats } from './CurrentStats' +import { LockTokenWidget } from './LockTokenWidget' +import { ActivityRewardsInfo } from './ActivityRewardsInfo' +import { ActionNavigation } from './BoostGraph/ActionNavigation/ActionNavigation' +import PaperContainer from '../PaperContainer' +import { useSummarizedLockHistory } from '@/hooks/useSummarizedLockHistory' +import { useLockHistory } from '@/hooks/useLockHistory' + +import css from './styles.module.css' +import { ExternalLink } from '../ExternalLink' +import { SAFE_TERMS_AND_CONDITIONS_URL } from '@/config/constants' + +const TokenLocking = () => { + const { isLoading: safeBalanceLoading, data: safeBalance } = useSafeTokenBalance() + const { totalLocked, totalUnlocked, totalWithdrawable } = useSummarizedLockHistory(useLockHistory()) + + const isUnlockAvailable = totalLocked.gt(0) || totalUnlocked.gt(0) || totalWithdrawable.gt(0) + + return ( + + + {'Get rewards with Safe{Pass}'} + + {'What is Safe{Pass}'} + + + + + + + + + + + + + + + + + + + + + + + + LEGAL DISCLAIMER + + + Please note that residents in{' '} + certain jurisdictions (including the + United States) may not be eligible for the boost and reward. This means that your boost might not be + applied to certain reward types, e.g. token rewards such as Safe. + + + + Terms and Conditions + + + + + ) +} + +export default TokenLocking diff --git a/src/components/TokenLocking/styles.module.css b/src/components/TokenLocking/styles.module.css new file mode 100644 index 00000000..a794f631 --- /dev/null +++ b/src/components/TokenLocking/styles.module.css @@ -0,0 +1,278 @@ +.badge :global .MuiBadge-badge { + right: 4px; + bottom: 5px; + height: 6px; + min-width: 6px; +} + +.boostInfoBox { + display: flex; + flex-direction: column; +} + +.bordered { + border-radius: 6px; + border: var(--mui-palette-border-light) 1px solid; +} + +.amountDisplay { + line-height: 2em; + font-weight: 700; + display: inline-flex; + gap: 4px; + align-items: center; + z-index: 1; +} + +.badge :global .MuiBadge-badge { + bottom: 4px; + right: 4px; + background-color: var(--mui-palette-secondary-main); +} + +.maxButton { + border: 0; + background: none; + color: var(--mui-palette-primary-main); + padding: 0; + font-size: 16px; + font-weight: bold; + cursor: pointer; +} + +.gauge { + transition: width 200ms; +} + +@keyframes highlight { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.2); + } + 100% { + transform: scale(1); + } +} + +.highlight { + animation: highlight 1s; +} + +.gradientText { + background: linear-gradient(225deg, #5fddff 12.5%, #12ff80 88.07%); + background-clip: text; + color: transparent; +} + +.steps { + z-index: 1; +} + +.step { + position: relative; + padding-left: 40px; + margin-bottom: 40px; + counter-increment: item; +} + +.step:last-child { + margin-bottom: 0; +} + +.step:before { + content: counter(item); + width: 24px; + height: 24px; + display: block; + background: #636669; + border-radius: 50%; + color: #121312; + text-align: center; + position: absolute; + left: 0; +} + +.step:after { + content: ''; + left: 12px; + top: 36px; + position: absolute; + height: calc(100% - 16px); + width: 1px; + background-color: #303033; +} + +.activeStep:before { + background-color: #12ff80; +} + +.activeStep:after { + background-color: #12ff80; +} + +.comingSoon { + background-color: #303033; + border-radius: 4px; + padding: 4px 8px; + position: absolute; + margin-left: 8px; +} + +.stepDate { + background-color: #121312; + padding: 20px; + border-radius: 6px; +} + +.rewards { + position: relative; +} + +.rewardsBackground { + pointer-events: none; +} + +.rewardsBackground:before { + content: ''; + background: radial-gradient(41.34% 41.34% at 50% 50%, #29b6f6 0%, transparent 82.5%); + position: absolute; + width: 500px; + height: 500px; + top: -200px; + left: 100px; + opacity: 0.2; +} + +.rewardsBackground:after { + content: ''; + background: radial-gradient(41.34% 41.34% at 50% 50%, rgba(18, 255, 128, 0.6) 0%, rgba(18, 255, 128, 0) 82.5%); + position: absolute; + width: 700px; + height: 700px; + top: -100px; + left: -250px; + opacity: 0.2; +} + +.rewards:after { + content: ''; + left: 12px; + top: -4px; + position: absolute; + height: calc(100% - 16px); + width: 1px; + background-color: #303033; +} + +.diamondImage { + text-align: center; +} + +.milesReceipt { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 90vw; + max-width: 585px; + display: flex; + overflow: hidden; + background-color: #121312; + border-radius: 20px; + padding: 40px; + width: 100%; +} + +.topReceipt { + border-bottom: 1px dashed white; + padding: 40px; + margin: 0 -40px; +} + +.barcode { + position: absolute; + bottom: 0; + left: 50%; + z-index: 1; + transform: translate(-50%, 50%) rotate(90deg); + pointer-events: none; +} + +.closeButton { + position: absolute; + right: 24px; + top: 24px; + z-index: 1; +} + +@media (max-width: 899px) { + .milesReceipt { + width: 100%; + } + + .leftReceipt, + .rightReceipt { + border-radius: 0px; + } + .barcode, + .closeButton { + right: 0px; + } + + .leftReceipt:after { + display: none; + } + + .leftReceipt { + border-bottom: 1px dashed white; + } + + .pageTitle { + margin: 16px; + } +} + +.gradientBackground { + position: absolute; + width: 1000px; + height: 1000px; + opacity: 0.3; + z-index: 1; + transform: translate(-50%, -50%); + + top: 50%; + left: 50%; + background: radial-gradient(41.34% 41.34% at 50% 50%, rgba(18, 255, 128, 0.6) 0%, rgba(18, 255, 128, 0) 82.5%); + pointer-events: none; +} + +.sidebarGradientBackground { + position: absolute; + width: 400px; + height: 400px; + opacity: 0.3; + z-index: 1; + top: 700px; + left: 50px; + background: radial-gradient(41.34% 41.34% at 50% 50%, rgba(41, 182, 246, 0.5) 0%, rgba(18, 255, 128, 0) 82.5%); + pointer-events: none; +} + +.unlockGradient { + background: radial-gradient(41.34% 41.34% at 50% 50%, #ff8061 0%, rgba(255, 128, 97, 0) 82.5%); +} + +.lockingHeader { + display: flex; + flex-direction: row; + align-items: center; +} + +@media (max-width: 599px) { + .lockingHeader { + flex-direction: column; + align-items: flex-start; + } +} diff --git a/src/components/TotalVotingPower/index.tsx b/src/components/TotalVotingPower/index.tsx index 09e1519b..fa42ec80 100644 --- a/src/components/TotalVotingPower/index.tsx +++ b/src/components/TotalVotingPower/index.tsx @@ -12,9 +12,7 @@ export const TotalVotingPower = (): ReactElement => { return ( <> - Total voting power is - - + {formattedAmount} SAFE diff --git a/src/components/Track/index.tsx b/src/components/Track/index.tsx new file mode 100644 index 00000000..a4973e01 --- /dev/null +++ b/src/components/Track/index.tsx @@ -0,0 +1,51 @@ +import { trackSafeAppEvent } from '@/utils/analytics' +import type { ReactElement } from 'react' +import { Fragment, useEffect, useRef } from 'react' + +type Props = { + children: ReactElement + as?: 'span' | 'div' + action: string + label?: string +} + +const shouldTrack = (el: HTMLDivElement) => { + const disabledChildren = el.querySelectorAll('*[disabled]') + return disabledChildren.length === 0 +} + +const Track = ({ children, as: Wrapper = 'span', ...trackData }: Props): typeof children => { + const el = useRef(null) + + useEffect(() => { + if (!el.current) { + return + } + + const trackEl = el.current + + const handleClick = () => { + if (shouldTrack(trackEl)) { + trackSafeAppEvent(trackData.action, trackData.label) + } + } + + // We cannot use onClick as events in children do not always bubble up + trackEl.addEventListener('click', handleClick) + return () => { + trackEl.removeEventListener('click', handleClick) + } + }, [el, trackData]) + + if (children.type === Fragment) { + throw new Error('Fragments cannot be tracked.') + } + + return ( + + {children} + + ) +} + +export default Track diff --git a/src/components/WalletIcon/index.tsx b/src/components/WalletIcon/index.tsx index faab6a3b..b7d5a023 100644 --- a/src/components/WalletIcon/index.tsx +++ b/src/components/WalletIcon/index.tsx @@ -1,29 +1,15 @@ import { Skeleton } from '@mui/material' -import metamaskIcon from '@web3-onboard/injected-wallets/dist/icons/metamask' -import coinbaseIcon from '@web3-onboard/coinbase/dist/icon' -import keystoneIcon from '@web3-onboard/keystone/dist/icon' import walletConnectIcon from '@web3-onboard/walletconnect/dist/icon' -import trezorIcon from '@web3-onboard/trezor/dist/icon' -import ledgerIcon from '@web3-onboard/ledger/dist/icon' -import tahoIcon from '@web3-onboard/taho/dist/icon' -import { INJECTED_WALLET_KEYS, WALLET_KEYS } from '@/utils/onboard' +import { WALLET_KEYS } from '@/utils/onboard' type Props = { - [k in keyof (typeof WALLET_KEYS & typeof INJECTED_WALLET_KEYS)]: string + [k in keyof typeof WALLET_KEYS]: string } const WALLET_ICONS: Props = { - [INJECTED_WALLET_KEYS.METAMASK]: metamaskIcon, - [WALLET_KEYS.COINBASE]: coinbaseIcon, - [WALLET_KEYS.INJECTED]: metamaskIcon, - [WALLET_KEYS.KEYSTONE]: keystoneIcon, - [WALLET_KEYS.WALLETCONNECT]: walletConnectIcon, - [WALLET_KEYS.WALLETCONNECT_V2]: walletConnectIcon, - [WALLET_KEYS.TREZOR]: trezorIcon, - [WALLET_KEYS.LEDGER]: ledgerIcon, - [WALLET_KEYS.TAHO]: tahoIcon, -} + WALLETCONNECT: walletConnectIcon, +} as const export const WalletIcon = ({ provider }: { provider: string }) => { const icon = WALLET_ICONS[provider.toUpperCase() as keyof typeof WALLET_ICONS] diff --git a/src/components/WhatIsBoost/index.tsx b/src/components/WhatIsBoost/index.tsx new file mode 100644 index 00000000..b08f29cf --- /dev/null +++ b/src/components/WhatIsBoost/index.tsx @@ -0,0 +1,98 @@ +import { Link, Stack, SvgIcon, Typography } from '@mui/material' +import { AppRoutes } from '@/config/routes' +import NextLink from 'next/link' +import { ChevronLeft, SignalCellularAlt, SignalCellularAlt2Bar } from '@mui/icons-material' +import PaperContainer from '@/components/PaperContainer' +import TokenBoost from '@/public/images/token-boost.png' +import Timefactor from '@/public/images/timefactor.png' +import SafeLogo from '@/public/images/safe-logo-round.svg' +import ClockIcon from '@/public/images/clock-alt.svg' +import Image from 'next/image' +import css from './styles.module.css' +import { NAVIGATION_EVENTS } from '@/analytics/navigation' +import Track from '../Track' + +const WhatIsBoost = () => { + return ( + + + palette.primary.main }} + > + + Back to main + + + + What is Boost and how does it work? + + + + You can multiply your Miles by locking SAFE. The more SAFE you lock and the longer you keep it locked, the + higher your multiplier. + + + Expressed as a mathematical formula, the total boost can be calculated as follows: + + +
+ + +
+ Total boost = token boost * timefactor boost + 1 +
+ + + The total boost consists of two components: the token boost and the timefactor boost. Let’s break these down + further: + + + + + Token boost + + + + This component depends on the amount of tokens you lock. You will be placed in one of 6 different tiers + depending on how many tokens you lock. How the exact formula for the token boost works depends on the tier you + are in. + + + Token boost diagram + + + palette.primary.main }} /> + Timefactor boost + + + + This component ensures that the earlier you lock, the higher your boost will be. Depending on when you lock, + you will fall into a different tier that results in a different formula (similar to the above). However, all + the formulae are time dependent. This means that every day you decide not to lock you will lose your potential + boost compared to if you would have done so that day. You can see the formulae for the different tiers below. + + + + We differentiate between a realised and expected final boost in our app. Realised boost is the boost you have + already secured and which cannot be taken away by unlocking. This has no projection over time. The expected + final boost is the boost you will get at the start of the distribution of rewards, assuming that you didn’t + unlock or withdraw your tokens before. + + + Timefactor diagram + + +
+ + +
+ Note that only the final boost at the end of the season will be multiplied by all gained points. +
+
+
+ ) +} + +export default WhatIsBoost diff --git a/src/components/WhatIsBoost/styles.module.css b/src/components/WhatIsBoost/styles.module.css new file mode 100644 index 00000000..9ea38fea --- /dev/null +++ b/src/components/WhatIsBoost/styles.module.css @@ -0,0 +1,18 @@ +.info { + background: #121312; + padding: 24px; + border-radius: 6px; + font-weight: bold; + display: flex; + align-items: center; + gap: 16px; +} + +.signalWrapper { + position: relative; +} + +.signalWrapper svg:last-child { + position: absolute; + left: 0; +} \ No newline at end of file diff --git a/src/config/constants.ts b/src/config/constants.ts index ca93caf0..40bd72ca 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -1,3 +1,5 @@ +import { BigNumber } from 'ethers' + // General export const IS_PRODUCTION = process.env.NEXT_PUBLIC_IS_PRODUCTION === 'true' export const INFURA_TOKEN = process.env.NEXT_PUBLIC_INFURA_TOKEN || '' @@ -15,8 +17,8 @@ export const TREZOR_EMAIL = 'support@safe.global' // Deployments export const DEPLOYMENT_URL = IS_PRODUCTION - ? 'https://governance.safe.global' - : 'https://safe-dao-governance.dev.5afe.dev/' + ? 'https://community.safe.global' + : 'https://safe-dao-governance.dev.5afe.dev' export const GATEWAY_URL = IS_PRODUCTION ? 'https://safe-client.safe.global' : 'https://safe-client.staging.5afe.dev' @@ -25,23 +27,33 @@ export const SAFE_URL = IS_PRODUCTION ? 'https://app.safe.global' : 'https://saf // Chains export const Chains = { MAINNET: '1', - GOERLI: '5', + SEPOLIA: '11155111', } // Strictly type configuration for each chain above type ChainConfig = Record<(typeof Chains)[keyof typeof Chains], T> -export const _DEFAULT_CHAIN_ID = IS_PRODUCTION ? Chains.MAINNET : Chains.GOERLI +export const _DEFAULT_CHAIN_ID = IS_PRODUCTION ? Chains.MAINNET : Chains.SEPOLIA export const CHAIN_SHORT_NAME: ChainConfig = { [Chains.MAINNET]: 'eth', - [Chains.GOERLI]: 'gor', + [Chains.SEPOLIA]: 'sep', +} + +export const CHAIN_START_TIMESTAMPS: ChainConfig = { + [Chains.MAINNET]: Date.parse('Tue Apr 23 2024 12:00:00 GMT+0000'), + [Chains.SEPOLIA]: Date.parse('Tue Mar 01 2024 12:00:00 GMT+0000'), } // Token export const CHAIN_SAFE_TOKEN_ADDRESS: ChainConfig = { [Chains.MAINNET]: '0x5afe3855358e112b5647b952709e6165e1c1eeee', - [Chains.GOERLI]: '0x61fD3b6d656F39395e32f46E2050953376c3f5Ff', + [Chains.SEPOLIA]: '0xd16d9C09d13E9Cf77615771eADC5d51a1Ae92a26', +} + +export const CHAIN_SAFE_LOCKING_ADDRESS: ChainConfig = { + [Chains.MAINNET]: '0x0a7CB434f96f65972D46A5c1A64a9654dC9959b2', + [Chains.SEPOLIA]: '0xb161ccb96b9b817F9bDf0048F212725128779DE9', } // Claiming @@ -49,6 +61,11 @@ const CLAIMING_DATA_URL = IS_PRODUCTION ? 'https://safe-claiming-app-data.safe.global' : 'https://safe-claiming-app-data.staging.5afe.dev' +export const CGW_BASE_URL = { + [Chains.MAINNET]: 'https://safe-client.safe.global', + [Chains.SEPOLIA]: 'https://safe-client.staging.5afe.dev', +} + export const GUARDIANS_URL = `${CLAIMING_DATA_URL}/guardians/guardians.json` export const GUARDIANS_IMAGE_URL = `${CLAIMING_DATA_URL}/guardians/images` export const VESTING_URL = `${CLAIMING_DATA_URL}/allocations` @@ -66,7 +83,7 @@ export const AIRDROP_TAGS = { // Delegation export const CHAIN_DELEGATE_ID: ChainConfig = { [Chains.MAINNET]: 'safe.eth', - [Chains.GOERLI]: 'tutis.eth', + [Chains.SEPOLIA]: 'panzerschrank.eth', } export const DELEGATE_REGISTRY_ADDRESS = '0x469788fe6e9e9681c6ebf3bf78e7fd26fc015446' @@ -77,10 +94,24 @@ export const GOVERNANCE_URL = 'https://forum.gnosis-safe.io/t/how-to-safedao-gov export const CHAIN_SNAPSHOT_URL: ChainConfig = { [Chains.MAINNET]: `https://snapshot.org/#/${CHAIN_DELEGATE_ID[Chains.MAINNET]}`, - [Chains.GOERLI]: `https://snapshot.org/#/${CHAIN_DELEGATE_ID[Chains.GOERLI]}`, + [Chains.SEPOLIA]: `https://snapshot.org/#/${CHAIN_DELEGATE_ID[Chains.SEPOLIA]}`, +} + +export const CHAIN_EXPLORER_URL: ChainConfig = { + [Chains.MAINNET]: 'https://etherscan.io/address/', + [Chains.SEPOLIA]: 'https://sepolia.etherscan.io/address/', } export const SEP5_PROPOSAL_URL = 'https://snapshot.org/#/safe.eth/proposal/0xb4765551b4814b592d02ce67de05527ac1d2b88a8c814c4346ecc0c947c9b941' +export const SAFE_PASS_LANDING_PAGE = 'https://safe.global/pass' +export const SAFE_PASS_HELP_ARTICLE_URL = 'https://help.safe.global/en/articles/157043-what-is-safe-pass' +export const SAFE_TERMS_AND_CONDITIONS_URL = 'https://help.safe.global/en/articles/157469-terms-and-conditions' + export const DISCORD_URL = 'https://chat.safe.global' + +export const UNLIMITED_APPROVAL_AMOUNT = BigNumber.from(2).pow(256).sub(1) + +export const SEASON2_START = 160 +export const SEASON1_START = 27 diff --git a/src/config/routes.ts b/src/config/routes.ts index 65118f01..f74a7259 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -1,8 +1,12 @@ export const AppRoutes = { '404': '/404', widgets: '/widgets', - safedao: '/safedao', + unlock: '/unlock', + splash: '/splash', index: '/', + governance: '/governance', delegate: '/delegate', claim: '/claim', + activity: '/activity', + activityProgram: '/activity-program', } diff --git a/src/hooks/__tests__/useAmounts.test.ts b/src/hooks/__tests__/useAmounts.test.ts index 62d75274..21e7c849 100644 --- a/src/hooks/__tests__/useAmounts.test.ts +++ b/src/hooks/__tests__/useAmounts.test.ts @@ -24,7 +24,7 @@ const createMockVesting = ( amount: parseEther(amount.toString()).toString(), curve: 0, durationWeeks, - chainId: 4, + chainId: 11155111, contract: hexZeroPad('0x2', 20), tag: 'user', startDate: Math.floor(fakeNow.getTime() / 1000) - DESYNC_BUFFER + ONE_WEEK * vestingStartDiffInWeeks, diff --git a/src/hooks/__tests__/useContractDelegate.test.ts b/src/hooks/__tests__/useContractDelegate.test.ts index d78a84aa..e56d6d62 100644 --- a/src/hooks/__tests__/useContractDelegate.test.ts +++ b/src/hooks/__tests__/useContractDelegate.test.ts @@ -21,7 +21,7 @@ describe('_getContractDelegate()', () => { () => ({ _isSigner: true, - getChainId: jest.fn(() => Promise.resolve(5)), + getChainId: jest.fn(() => Promise.resolve(11155111)), getAddress: jest.fn(() => Promise.resolve(SAFE_ADDRESS)), call: mockCall, } as unknown as JsonRpcSigner), @@ -42,7 +42,7 @@ describe('_getContractDelegate()', () => { }) it('ignore the ZERO_ADDRESS as delegate', async () => { - const delegateIDInBytes = formatBytes32String(CHAIN_DELEGATE_ID['5']) + const delegateIDInBytes = formatBytes32String(CHAIN_DELEGATE_ID['11155111']) mockCall.mockImplementation((transaction) => { expect(transaction.to?.toString().toLowerCase()).toEqual(DELEGATE_REGISTRY_ADDRESS.toLowerCase()) @@ -54,14 +54,14 @@ describe('_getContractDelegate()', () => { return Promise.resolve(hexZeroPad(ZERO_ADDRESS, 32)) }) - const result = await _getContractDelegate('5', SAFE_ADDRESS, web3Provider) + const result = await _getContractDelegate('11155111', SAFE_ADDRESS, web3Provider) expect(mockCall).toBeCalledTimes(1) expect(result).toBe(null) }) it('should encode the correct data and fetch the delegate on-chain once', async () => { - const delegateIDInBytes = formatBytes32String(CHAIN_DELEGATE_ID['5']) + const delegateIDInBytes = formatBytes32String(CHAIN_DELEGATE_ID['11155111']) const delegateAddress = hexZeroPad('0x1', 20) @@ -75,7 +75,7 @@ describe('_getContractDelegate()', () => { return Promise.resolve(hexZeroPad(delegateAddress, 32)) }) - const result = await _getContractDelegate('5', SAFE_ADDRESS, web3Provider) + const result = await _getContractDelegate('11155111', SAFE_ADDRESS, web3Provider) expect(mockCall).toBeCalledTimes(1) expect(result).toEqual({ address: delegateAddress, ens: 'test.eth' }) diff --git a/src/hooks/__tests__/useEnsResolution.test.ts b/src/hooks/__tests__/useEnsResolution.test.ts index 83f8d64e..34d5e714 100644 --- a/src/hooks/__tests__/useEnsResolution.test.ts +++ b/src/hooks/__tests__/useEnsResolution.test.ts @@ -1,7 +1,7 @@ import { act } from '@testing-library/react' import { waitFor } from '@testing-library/react' import { JsonRpcProvider } from '@ethersproject/providers' -import * as SafeAppsSdk from '@gnosis.pm/safe-apps-react-sdk' +import * as SafeAppsSdk from '@safe-global/safe-apps-react-sdk' import { renderHook } from '@/tests/test-utils' import * as useWeb3 from '@/hooks/useWeb3' @@ -10,10 +10,10 @@ import * as useWallet from '@/hooks/useWallet' import { useEnsResolution } from '@/hooks/useEnsResolution' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' -jest.mock('@gnosis.pm/safe-apps-react-sdk', () => { +jest.mock('@safe-global/safe-apps-react-sdk', () => { return { __esModule: true, - ...jest.requireActual('@gnosis.pm/safe-apps-react-sdk'), + ...jest.requireActual('@safe-global/safe-apps-react-sdk'), } }) @@ -68,12 +68,12 @@ describe('useEnsResolution()', () => { }) it('should accept EIP-3770 addresses with correct chain prefix', async () => { - const prefixedAddress = 'gor:0x1000000000000000000000000000000000000000' + const prefixedAddress = 'sep:0x1000000000000000000000000000000000000000' web3Provider.resolveName = jest.fn() jest.spyOn(useWeb3, 'useWeb3').mockImplementation(() => web3Provider) - jest.spyOn(useChain, 'useChain').mockImplementation(() => ({ chainId: '5', shortName: 'gor' } as ChainInfo)) + jest.spyOn(useChain, 'useChain').mockImplementation(() => ({ chainId: '11155111', shortName: 'sep' } as ChainInfo)) jest.useFakeTimers() @@ -95,14 +95,14 @@ describe('useEnsResolution()', () => { web3Provider.resolveName = jest.fn() jest.spyOn(useWeb3, 'useWeb3').mockImplementation(() => web3Provider) - jest.spyOn(useChain, 'useChain').mockImplementation(() => ({ chainId: '5', shortName: 'gor' } as ChainInfo)) + jest.spyOn(useChain, 'useChain').mockImplementation(() => ({ chainId: '11155111', shortName: 'sep' } as ChainInfo)) jest.useFakeTimers() const { result } = renderHook(() => useEnsResolution(prefixedAddress)) expect(result.current[0]).toBeUndefined() - expect(result.current[1]).toEqual('The chain prefix does not match that of the current chain (gor)') + expect(result.current[1]).toEqual('The chain prefix does not match that of the current chain (sep)') expect(result.current[2]).toBeFalsy() expect(web3Provider.resolveName).not.toHaveBeenCalled() diff --git a/src/hooks/__tests__/useIsTokenPaused.test.ts b/src/hooks/__tests__/useIsTokenPaused.test.ts index c790c7ae..59c49663 100644 --- a/src/hooks/__tests__/useIsTokenPaused.test.ts +++ b/src/hooks/__tests__/useIsTokenPaused.test.ts @@ -18,7 +18,7 @@ describe('_getIsTokenPaused', () => { it('should return true on error', async () => { mockCall.mockImplementation(() => Promise.reject()) - const result = await _getIsTokenPaused('5', web3Provider) + const result = await _getIsTokenPaused('11155111', web3Provider) expect(result).toBeTruthy() expect(mockCall).toBeCalledTimes(1) @@ -27,7 +27,7 @@ describe('_getIsTokenPaused', () => { it('should return true if token is paused', async () => { mockCall.mockImplementation(async () => Promise.resolve(safeTokenInterface.encodeFunctionResult('paused', [true]))) - const result = await _getIsTokenPaused('5', web3Provider) + const result = await _getIsTokenPaused('11155111', web3Provider) expect(result).toBeTruthy() expect(mockCall).toBeCalledTimes(1) @@ -36,14 +36,14 @@ describe('_getIsTokenPaused', () => { it('should return false if token is unpaused', async () => { mockCall.mockImplementation(async () => Promise.resolve(safeTokenInterface.encodeFunctionResult('paused', [false]))) - const result = await _getIsTokenPaused('5', web3Provider) + const result = await _getIsTokenPaused('11155111', web3Provider) expect(result).toBeFalsy() expect(mockCall).toBeCalledTimes(1) }) it('returns null if no provider is defined', async () => { - const result = await _getIsTokenPaused('5', undefined) + const result = await _getIsTokenPaused('11155111', undefined) expect(result).toBe(null) }) diff --git a/src/hooks/__tests__/useSafeTokenAllocation.test.ts b/src/hooks/__tests__/useSafeTokenAllocation.test.ts index a15a5609..3cee242d 100644 --- a/src/hooks/__tests__/useSafeTokenAllocation.test.ts +++ b/src/hooks/__tests__/useSafeTokenAllocation.test.ts @@ -235,28 +235,46 @@ describe('useSafeTokenAllocation', () => { it('return 0 if no balances / vestings exist', async () => { mockCall.mockImplementation((transaction: Deferrable) => { - const sigHash = keccak256(toUtf8Bytes('balanceOf(address)')).slice(0, 10) - if (typeof transaction.data === 'string' && transaction.data.startsWith(sigHash)) { + const balanceOfSigHash = keccak256(toUtf8Bytes('balanceOf(address)')).slice(0, 10) + const getUserTokenBalanceSigHash = keccak256(toUtf8Bytes('getUserTokenBalance(address)')).slice(0, 10) + if ( + typeof transaction.data === 'string' && + (transaction.data.startsWith(balanceOfSigHash) || transaction.data.startsWith(getUserTokenBalanceSigHash)) + ) { return Promise.resolve('0x0') } return Promise.resolve('0x') }) - const result = await _getVotingPower({ chainId: '5', address: SAFE_ADDRESS, web3: web3Provider, vestingData: [] }) + const result = await _getVotingPower({ + chainId: '11155111', + address: SAFE_ADDRESS, + web3: web3Provider, + vestingData: [], + }) expect(result?.toNumber()).toEqual(0) }) - it('return balance if no vestings exists', async () => { + it('return total balance of tokens held and tokens in locking contract if no vestings exists', async () => { mockCall.mockImplementation((transaction: Deferrable) => { - const sigHash = keccak256(toUtf8Bytes('balanceOf(address)')).slice(0, 10) - if (typeof transaction.data === 'string' && transaction.data.startsWith(sigHash)) { + const balanceOfSigHash = keccak256(toUtf8Bytes('balanceOf(address)')).slice(0, 10) + const getUserTokenBalanceSigHash = keccak256(toUtf8Bytes('getUserTokenBalance(address)')).slice(0, 10) + if (typeof transaction.data === 'string' && transaction.data.startsWith(balanceOfSigHash)) { + return Promise.resolve(parseEther('100').toHexString()) + } + if (typeof transaction.data === 'string' && transaction.data.startsWith(getUserTokenBalanceSigHash)) { return Promise.resolve(parseEther('100').toHexString()) } return Promise.resolve('0x') }) - const result = await _getVotingPower({ chainId: '5', address: SAFE_ADDRESS, web3: web3Provider, vestingData: [] }) - expect(result?.eq(parseEther('100'))).toBeTruthy() + const result = await _getVotingPower({ + chainId: '11155111', + address: SAFE_ADDRESS, + web3: web3Provider, + vestingData: [], + }) + expect(result?.eq(parseEther('200'))).toBeTruthy() }) it('include unredeemed allocations if deadline has not passed', async () => { @@ -280,16 +298,20 @@ describe('useSafeTokenAllocation', () => { mockCall.mockImplementation((transaction: Deferrable) => { const balanceOfSigHash = keccak256(toUtf8Bytes('balanceOf(address)')).slice(0, 10) + const getUserTokenBalanceSigHash = keccak256(toUtf8Bytes('getUserTokenBalance(address)')).slice(0, 10) if (typeof transaction.data === 'string' && transaction.data.startsWith(balanceOfSigHash)) { return Promise.resolve(parseEther('0').toHexString()) } + if (typeof transaction.data === 'string' && transaction.data.startsWith(getUserTokenBalanceSigHash)) { + return Promise.resolve(parseEther('0').toHexString()) + } return Promise.resolve('0x') }) const result = await _getVotingPower({ - chainId: '5', + chainId: '11155111', address: SAFE_ADDRESS, web3: web3Provider, vestingData: mockVestings, @@ -318,16 +340,20 @@ describe('useSafeTokenAllocation', () => { mockCall.mockImplementation((transaction: Deferrable) => { const balanceOfSigHash = keccak256(toUtf8Bytes('balanceOf(address)')).slice(0, 10) + const getUserTokenBalanceSigHash = keccak256(toUtf8Bytes('getUserTokenBalance(address)')).slice(0, 10) if (typeof transaction.data === 'string' && transaction.data.startsWith(balanceOfSigHash)) { return Promise.resolve(BigNumber.from('2000').toHexString()) } + if (typeof transaction.data === 'string' && transaction.data.startsWith(getUserTokenBalanceSigHash)) { + return Promise.resolve(BigNumber.from('0').toHexString()) + } return Promise.resolve('0x') }) const result = await _getVotingPower({ - chainId: '5', + chainId: '11155111', address: SAFE_ADDRESS, web3: web3Provider, vestingData: mockAllocation, @@ -356,16 +382,20 @@ describe('useSafeTokenAllocation', () => { mockCall.mockImplementation((transaction: Deferrable) => { const balanceOfSigHash = keccak256(toUtf8Bytes('balanceOf(address)')).slice(0, 10) + const getUserTokenBalanceSigHash = keccak256(toUtf8Bytes('getUserTokenBalance(address)')).slice(0, 10) if (typeof transaction.data === 'string' && transaction.data.startsWith(balanceOfSigHash)) { return Promise.resolve(BigNumber.from('0').toHexString()) } + if (typeof transaction.data === 'string' && transaction.data.startsWith(getUserTokenBalanceSigHash)) { + return Promise.resolve(BigNumber.from('0').toHexString()) + } return Promise.resolve('0x') }) const result = await _getVotingPower({ - chainId: '5', + chainId: '11155111', address: SAFE_ADDRESS, web3: web3Provider, vestingData: mockAllocation, diff --git a/src/hooks/__tests__/useSummarizedLockHistory.test.ts b/src/hooks/__tests__/useSummarizedLockHistory.test.ts new file mode 100644 index 00000000..d0206fa1 --- /dev/null +++ b/src/hooks/__tests__/useSummarizedLockHistory.test.ts @@ -0,0 +1,122 @@ +import { renderHook } from '@/tests/test-utils' +import { hexZeroPad } from 'ethers/lib/utils' +import { WithdrawEvent, type LockEvent, type UnlockEvent } from '../useLockHistory' +import { useSummarizedLockHistory } from '../useSummarizedLockHistory' + +const holder = hexZeroPad('0x1234', 20) + +const FAKE_YESTERDAY = '2024-03-19T11:00:00.000Z' +const FAKE_TODAY = '2024-03-20T12:00:00.000Z' +const FAKE_1H_AGO = '2024-03-20T11:00:00.000Z' + +const createLock = (amount: string, executionDate: string = FAKE_TODAY.toString()): LockEvent => ({ + executionDate, + transactionHash: '0x93ba0541fe594f935807b559cfe21ae6d20d6785647579a2c7517f8aa9d38076', + holder, + amount, + logIndex: '1', + eventType: 'LOCKED', +}) + +const createUnlock = ( + amount: string, + unlockIndex: string, + executionDate: string = FAKE_TODAY.toString(), +): UnlockEvent => ({ + executionDate, + transactionHash: '0x93ba0541fe594f935807b559cfe21ae6d20d6785647579a2c7517f8aa9d38076', + holder, + amount, + logIndex: '1', + eventType: 'UNLOCKED', + unlockIndex, +}) + +const createWithdrawal = ( + amount: string, + unlockIndex: string, + executionDate: string = FAKE_TODAY.toString(), +): WithdrawEvent => ({ + executionDate, + transactionHash: '0x93ba0541fe594f935807b559cfe21ae6d20d6785647579a2c7517f8aa9d38076', + holder, + amount, + logIndex: '1', + eventType: 'WITHDRAWN', + unlockIndex, +}) + +describe('useSummarizedLockHistory', () => { + beforeAll(() => { + jest.useFakeTimers().setSystemTime(Date.parse(FAKE_TODAY)) + }) + + afterAll(() => { + jest.useRealTimers() + }) + it('should return zeros for empty history', () => { + const { result } = renderHook(() => useSummarizedLockHistory([])) + + expect(result.current.totalLocked.eq(0)).toBeTruthy() + expect(result.current.totalUnlocked.eq(0)).toBeTruthy() + expect(result.current.totalWithdrawable.eq(0)).toBeTruthy() + expect(result.current.pendingUnlocks).toEqual([]) + }) + + it('should add lock events', () => { + const { result } = renderHook(() => + useSummarizedLockHistory([createLock('100'), createLock('200'), createLock('400')]), + ) + + expect(result.current.totalLocked.eq(700)).toBeTruthy() + expect(result.current.totalUnlocked.eq(0)).toBeTruthy() + expect(result.current.totalWithdrawable.eq(0)).toBeTruthy() + expect(result.current.pendingUnlocks).toEqual([]) + }) + + it('should summarize lock and unlock events', () => { + const newestUnlock = createUnlock('200', '1') + const expectedNextUnlock = createUnlock('400', '2', FAKE_1H_AGO) + const { result } = renderHook(() => + useSummarizedLockHistory([createLock('1000'), newestUnlock, expectedNextUnlock]), + ) + + expect(result.current.totalLocked.eq(400)).toBeTruthy() + expect(result.current.totalUnlocked.eq(600)).toBeTruthy() + expect(result.current.totalWithdrawable.eq(0)).toBeTruthy() + expect(result.current.pendingUnlocks).toEqual([expectedNextUnlock, newestUnlock]) + }) + + it('should show withdrawable amount 24h after unlock', () => { + const expectedNextUnlock = createUnlock('100', '2', FAKE_TODAY.toString()) + const { result } = renderHook(() => + useSummarizedLockHistory([ + createLock('1000', FAKE_YESTERDAY.toString()), + createUnlock('200', '1', FAKE_YESTERDAY.toString()), + expectedNextUnlock, + ]), + ) + + expect(result.current.totalLocked.eq(700)).toBeTruthy() + expect(result.current.totalUnlocked.eq(300)).toBeTruthy() + expect(result.current.totalWithdrawable.eq(200)).toBeTruthy() + expect(result.current.pendingUnlocks).toEqual([expectedNextUnlock]) + }) + + it('should substract already withdrawn amounts from withdrawable amount', () => { + const expectedNextUnlock = createUnlock('100', '2', FAKE_TODAY.toString()) + const { result } = renderHook(() => + useSummarizedLockHistory([ + createLock('1000', FAKE_YESTERDAY.toString()), + createUnlock('200', '1', FAKE_YESTERDAY.toString()), + createWithdrawal('200', '1', FAKE_TODAY.toString()), + createUnlock('100', '2', FAKE_TODAY.toString()), + ]), + ) + + expect(result.current.totalLocked.eq(700)).toBeTruthy() + expect(result.current.totalUnlocked.eq(100)).toBeTruthy() + expect(result.current.totalWithdrawable.eq(0)).toBeTruthy() + expect(result.current.pendingUnlocks).toEqual([expectedNextUnlock]) + }) +}) diff --git a/src/hooks/useAddress.ts b/src/hooks/useAddress.ts index b98841b5..5c2b16a5 100644 --- a/src/hooks/useAddress.ts +++ b/src/hooks/useAddress.ts @@ -1,4 +1,4 @@ -import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk' +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' import { useIsSafeApp } from '@/hooks/useIsSafeApp' import { useWallet } from '@/hooks/useWallet' diff --git a/src/hooks/useChainId.ts b/src/hooks/useChainId.ts index 37bf7885..2bb69091 100644 --- a/src/hooks/useChainId.ts +++ b/src/hooks/useChainId.ts @@ -1,7 +1,7 @@ import { ExternalStore } from '@/services/ExternalStore' import { Chains, IS_PRODUCTION, _DEFAULT_CHAIN_ID } from '@/config/constants' import { useIsSafeApp } from '@/hooks/useIsSafeApp' -import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk' +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' export const defaultChainIdStore = new ExternalStore(_DEFAULT_CHAIN_ID) diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 00000000..b7b4c54d --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from 'react' + +export const useDebounce = (value: T, timeout: number, initialValue: T) => { + const [result, setResult] = useState(initialValue) + + useEffect(() => { + const update = (value: T) => { + setResult(value) + } + + const updateTimeout = setTimeout(() => update(value), timeout) + return () => clearTimeout(updateTimeout) + }, [value, timeout]) + + return result +} diff --git a/src/hooks/useEnsLookup.ts b/src/hooks/useEnsLookup.ts new file mode 100644 index 00000000..4b964f08 --- /dev/null +++ b/src/hooks/useEnsLookup.ts @@ -0,0 +1,15 @@ +import { useWeb3 } from './useWeb3' +import useSWRImmutable from 'swr/immutable' + +export const useEnsLookup = (address: string | undefined): string | undefined => { + const web3 = useWeb3() + + const lookupResult = useSWRImmutable(web3 && address ? `lookup-address-${address}` : null, () => { + if (address) { + return web3?.lookupAddress(address) + } + return undefined + }) + + return lookupResult.data || undefined +} diff --git a/src/hooks/useEnsResolution.ts b/src/hooks/useEnsResolution.ts index 72824d22..59de8a51 100644 --- a/src/hooks/useEnsResolution.ts +++ b/src/hooks/useEnsResolution.ts @@ -1,4 +1,4 @@ -import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk' +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' import { getAddress, isAddress } from 'ethers/lib/utils' import { useEffect, useState } from 'react' diff --git a/src/hooks/useGatewayBaseUrl.ts b/src/hooks/useGatewayBaseUrl.ts new file mode 100644 index 00000000..51bdcca0 --- /dev/null +++ b/src/hooks/useGatewayBaseUrl.ts @@ -0,0 +1,8 @@ +import { CGW_BASE_URL } from '@/config/constants' +import { useChainId } from '@/hooks/useChainId' + +export const useGatewayBaseUrl = (): string => { + const chainId = useChainId() + + return CGW_BASE_URL[chainId] +} diff --git a/src/hooks/useIsDarkMode.ts b/src/hooks/useIsDarkMode.ts deleted file mode 100644 index e1d10f69..00000000 --- a/src/hooks/useIsDarkMode.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useTheme } from '@mui/material/styles' - -export const useIsDarkMode = (): boolean => { - const { palette } = useTheme() - return palette.mode === 'dark' -} diff --git a/src/hooks/useIsSafeApp.ts b/src/hooks/useIsSafeApp.ts index 9f33fa61..9fcc3590 100644 --- a/src/hooks/useIsSafeApp.ts +++ b/src/hooks/useIsSafeApp.ts @@ -1,14 +1,13 @@ -import { useState } from 'react' - -import { useIsomorphicLayoutEffect } from '@/hooks/useIsomorphicLayoutEffect' +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' +import { useEffect, useState } from 'react' export const useIsSafeApp = (): boolean => { - const [isSafeApp, setIsSafeApp] = useState(false) - - useIsomorphicLayoutEffect(() => { - const isIframe = typeof window !== 'undefined' && window.self !== window.top - setIsSafeApp(isIframe) - }, [setIsSafeApp]) + const { connected } = useSafeAppsSDK() + const [isSafeApp, setIsSafeApp] = useState(connected) + useEffect(() => { + const isApp = connected || (typeof window !== 'undefined' && window.self !== window.top) + setIsSafeApp(isApp) + }, [connected]) return isSafeApp } diff --git a/src/hooks/useLeaderboard.ts b/src/hooks/useLeaderboard.ts new file mode 100644 index 00000000..857c1085 --- /dev/null +++ b/src/hooks/useLeaderboard.ts @@ -0,0 +1,72 @@ +import { useAddress } from './useAddress' +import useSWR from 'swr' +import { POLLING_INTERVAL } from '@/config/constants' +import { toCursorParam } from '@/utils/gateway' +import { useGatewayBaseUrl } from './useGatewayBaseUrl' + +export type LeaderboardEntry = { + holder: string + position: number + lockedAmount: string + unlockedAmount: string + withdrawnAmount: string +} + +type LeaderboardPage = { + count: number + next: string | null + previous: string | null + results: LeaderboardEntry[] +} + +export const useOwnRank = () => { + const address = useAddress() + const gatewayBaseUrl = useGatewayBaseUrl() + + return useSWR( + address ? `${gatewayBaseUrl}/v1/locking/leaderboard/rank/${address}` : null, + async (url: string | null) => { + if (!url) { + return undefined + } + return await fetch(url).then((resp) => { + if (resp.ok) { + return resp.json() as Promise + } else { + throw new Error('Error fetching own ranking.') + } + }) + }, + { refreshInterval: POLLING_INTERVAL }, + ) +} + +export const useGlobalLeaderboardPage = (limit: number, offset?: number) => { + const gatewayBaseUrl = useGatewayBaseUrl() + + const getKey = (limit: number, offset?: number) => { + if (!offset) { + // Load first page + return `${gatewayBaseUrl}/v1/locking/leaderboard?${toCursorParam(limit)}` + } + + // Load next page + return `${gatewayBaseUrl}/v1/locking/leaderboard?${toCursorParam(limit, offset)}` + } + + const { data } = useSWR( + getKey(limit, offset), + async (url: string) => { + return await fetch(url).then((resp) => { + if (resp.ok) { + return resp.json() as Promise + } else { + throw new Error('Error fetching leaderboard.') + } + }) + }, + { refreshInterval: POLLING_INTERVAL }, + ) + + return data +} diff --git a/src/hooks/useLockHistory.ts b/src/hooks/useLockHistory.ts new file mode 100644 index 00000000..7351c075 --- /dev/null +++ b/src/hooks/useLockHistory.ts @@ -0,0 +1,100 @@ +import { useAddress } from './useAddress' +import useSWRInfinite from 'swr/infinite' +import { useMemo } from 'react' +import { useGatewayBaseUrl } from './useGatewayBaseUrl' +import { POLLING_INTERVAL } from '@/config/constants' +import { toCursorParam } from '@/utils/gateway' + +export type LockEvent = { + eventType: 'LOCKED' + executionDate: string + transactionHash: string + holder: string + amount: string + logIndex: string +} + +export type UnlockEvent = { + eventType: 'UNLOCKED' + executionDate: string + transactionHash: string + holder: string + amount: string + logIndex: string + unlockIndex: string +} + +export type WithdrawEvent = { + eventType: 'WITHDRAWN' + executionDate: string + transactionHash: string + holder: string + amount: string + logIndex: string + unlockIndex: string +} + +export type LockingHistoryEntry = LockEvent | UnlockEvent | WithdrawEvent + +type LockingHistoryEventPage = { + count: number + next: string | null + previous: string | null + results: LockingHistoryEntry[] +} + +const LIMIT = 100 + +export const useLockHistory = () => { + const address = useAddress() + const gatewayBaseUrl = useGatewayBaseUrl() + + const getKey = useMemo( + () => (pageIndex: number, previousPageData: LockingHistoryEventPage) => { + if (!address) { + // We cannot fetch data while the address is resolving + return null + } + if (!previousPageData) { + // Load first page + return `${gatewayBaseUrl}/v1/locking/${address}/history?${toCursorParam(LIMIT)}` + } + if (previousPageData && !previousPageData.next) return null // reached the end + + // Load next page + return previousPageData.next + }, + [address, gatewayBaseUrl], + ) + + const { data, size, setSize } = useSWRInfinite( + getKey, + async (url: string) => { + return await fetch(url).then((resp) => { + if (resp.ok) { + return resp.json() as Promise + } else { + throw new Error('Error fetching lock history.') + } + }) + }, + { + refreshInterval: POLLING_INTERVAL, + }, + ) + + // We need to load everything + if (data && data.length > 0) { + const totalPages = Math.ceil(data[0].count / LIMIT) + if (totalPages > size) { + setSize(Math.ceil(data[0].count / LIMIT)) + } + } + + return useMemo(() => { + if (data === undefined) { + return [] + } + return data.flatMap((entry) => entry.results) + }, [data]) +} diff --git a/src/hooks/useSafeTokenAllocation.ts b/src/hooks/useSafeTokenAllocation.ts index e8add827..0cfbf480 100644 --- a/src/hooks/useSafeTokenAllocation.ts +++ b/src/hooks/useSafeTokenAllocation.ts @@ -12,10 +12,10 @@ import { sameAddress } from '@/utils/addresses' import { useWeb3 } from '@/hooks/useWeb3' import { isDashboard } from '@/utils/routes' import { Allocation, useAllocations } from '@/hooks/useAllocations' -import { CHAIN_SAFE_TOKEN_ADDRESS } from '@/config/constants' -import { getSafeTokenInterface } from '@/services/contracts/SafeToken' import { useChainId } from '@/hooks/useChainId' import { useAddress } from '@/hooks/useAddress' +import { fetchTokenBalance } from '@/utils/safe-token' +import { fetchLockingContractBalance } from '@/utils/lock' export type Vesting = Allocation & { isExpired: boolean @@ -86,33 +86,20 @@ export const _getVestingData = async (web3: JsonRpcProvider, allocations: Alloca return getValidVestingAllocation(vestingData) } -const tokenInterface = getSafeTokenInterface() - -const fetchTokenBalance = async (chainId: string, safeAddress: string, provider: JsonRpcProvider): Promise => { - const safeTokenAddress = CHAIN_SAFE_TOKEN_ADDRESS[chainId] - - if (!safeTokenAddress) { - return '0' - } - - try { - return await provider.call({ - to: safeTokenAddress, - data: tokenInterface.encodeFunctionData('balanceOf', [safeAddress]), - }) - } catch (err) { - throw Error(`Error fetching Safe Token balance: ${err}`) - } -} - -const computeVotingPower = (validVestingData: Vesting[], balance: string): BigNumber => { +const computeVotingPower = ( + validVestingData: Vesting[], + balance: string, + lockingContractBalance: string, +): BigNumber => { const tokensInVesting = validVestingData.reduce( (acc, data) => acc.add(data.amount).sub(data.amountClaimed), BigNumber.from(0), ) + const totalBalance = BigNumber.from(balance).add(BigNumber.from(lockingContractBalance)) + // add balance - return tokensInVesting.add(BigNumber.from(balance)) + return tokensInVesting.add(totalBalance) } export const _getVotingPower = async ({ @@ -127,8 +114,9 @@ export const _getVotingPower = async ({ vestingData: Vesting[] }): Promise => { const balance = await fetchTokenBalance(chainId, address, web3) + const lockingContractBalance = await fetchLockingContractBalance(chainId, address, web3) - return computeVotingPower(vestingData, balance) + return computeVotingPower(vestingData, balance, lockingContractBalance) } const getSafeTokenAllocation = async ({ diff --git a/src/hooks/useSafeTokenBalance.ts b/src/hooks/useSafeTokenBalance.ts new file mode 100644 index 00000000..b95769ee --- /dev/null +++ b/src/hooks/useSafeTokenBalance.ts @@ -0,0 +1,47 @@ +import { useAddress } from './useAddress' +import { useChainId } from './useChainId' +import { useWeb3 } from './useWeb3' +import useSWR from 'swr' +import { fetchTokenBalance, fetchTokenLockingAllowance } from '@/utils/safe-token' +import { BigNumber } from 'ethers' +import { POLLING_INTERVAL } from '@/config/constants' + +export const useSafeTokenBalance = () => { + const QUERY_KEY = 'safe-token-balance' + const web3 = useWeb3() + const chainId = useChainId() + const address = useAddress() + + return useSWR( + web3 && address ? QUERY_KEY : null, + () => { + if (!address || !web3) { + return '0' + } + return fetchTokenBalance(chainId, address, web3) + }, + { + refreshInterval: POLLING_INTERVAL, + }, + ) +} + +export const useSafeTokenLockingAllowance = () => { + const QUERY_KEY = 'safe-token-locking-allowance' + const web3 = useWeb3() + const chainId = useChainId() + const address = useAddress() + + return useSWR(web3 && address ? QUERY_KEY : null, () => { + if (!address || !web3) { + return '0' + } + return fetchTokenLockingAllowance(chainId, address, web3) + }) +} + +export type SingleUnlock = { + unlockAmount: BigNumber + unlockedAt: BigNumber + isUnlocked: boolean +} diff --git a/src/hooks/useStartDates.ts b/src/hooks/useStartDates.ts new file mode 100644 index 00000000..b6eb859b --- /dev/null +++ b/src/hooks/useStartDates.ts @@ -0,0 +1,15 @@ +import { CHAIN_START_TIMESTAMPS } from '@/config/constants' +import { IS_PRODUCTION, Chains } from '@/config/constants' + +import { useChainId } from '@/hooks/useChainId' +import { ExternalStore } from '@/services/ExternalStore' + +export const startDateStore = new ExternalStore(CHAIN_START_TIMESTAMPS[Chains.SEPOLIA]) + +export const useStartDate = () => { + const chainId = useChainId() + + const startTime = IS_PRODUCTION ? CHAIN_START_TIMESTAMPS[chainId] : startDateStore.useStore()! + + return { startTime } +} diff --git a/src/hooks/useSummarizedLockHistory.ts b/src/hooks/useSummarizedLockHistory.ts new file mode 100644 index 00000000..3f76abd9 --- /dev/null +++ b/src/hooks/useSummarizedLockHistory.ts @@ -0,0 +1,73 @@ +import { isGreaterThan24HoursDiff } from '@/utils/date' +import { isUnlockEvent, isWithdrawEvent } from '@/utils/lock' +import { BigNumber } from 'ethers' +import { useMemo } from 'react' +import { UnlockEvent, useLockHistory, WithdrawEvent } from './useLockHistory' + +export const useSummarizedLockHistory = ( + lockHistory: ReturnType, +): { + totalLocked: BigNumber + totalUnlocked: BigNumber + totalWithdrawable: BigNumber + pendingUnlocks: UnlockEvent[] | undefined +} => { + const totalLocked = useMemo( + () => + lockHistory.reduce((prev, event) => { + switch (event.eventType) { + case 'LOCKED': + return prev.add(event.amount) + + case 'UNLOCKED': + return prev.sub(event.amount) + + case 'WITHDRAWN': + return prev + } + }, BigNumber.from(0)), + [lockHistory], + ) + const totalUnlocked = useMemo( + () => + lockHistory.reduce((prev, event) => { + switch (event.eventType) { + case 'LOCKED': + return prev + case 'UNLOCKED': + return prev.add(event.amount) + case 'WITHDRAWN': + return prev.sub(event.amount) + } + }, BigNumber.from(0)), + [lockHistory], + ) + const { totalWithdrawable, pendingUnlocks } = useMemo(() => { + const unlocks = lockHistory.filter((event) => isUnlockEvent(event)).map((event) => event as UnlockEvent) + const withdrawnIds = lockHistory + .filter((event) => isWithdrawEvent(event)) + .map((event) => event as WithdrawEvent) + .map((withdraw) => withdraw.unlockIndex) + // Unlocks that have not been withdrawn and are older than 24h + const withdrawableUnlocks = unlocks.filter( + (unlock) => + !withdrawnIds.includes(unlock.unlockIndex) && + isGreaterThan24HoursDiff(Date.parse(unlock.executionDate), Date.now()), + ) + const totalWithdrawable = withdrawableUnlocks.reduce((prev, event) => prev.add(event.amount), BigNumber.from(0)) + const pendingUnlocks = unlocks + .filter( + (unlock) => + !withdrawnIds.includes(unlock.unlockIndex) && + !isGreaterThan24HoursDiff(Date.parse(unlock.executionDate), Date.now()), + ) + .reverse() + + return { + totalWithdrawable, + pendingUnlocks, + } + }, [lockHistory]) + + return { totalLocked, totalUnlocked, totalWithdrawable, pendingUnlocks } +} diff --git a/src/hooks/useTxSender.ts b/src/hooks/useTxSender.ts new file mode 100644 index 00000000..c8584a27 --- /dev/null +++ b/src/hooks/useTxSender.ts @@ -0,0 +1,42 @@ +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' +import { useIsSafeApp } from './useIsSafeApp' +import { useWallet } from './useWallet' + +import SafeAppsSDK from '@safe-global/safe-apps-sdk' + +export type BaseTransaction = Parameters[0]['txs'][number] + +export type TxSender = { + isBatchingSupported: boolean + sendTxs: (txs: BaseTransaction[]) => Promise +} + +export const useTxSender = (): TxSender | undefined => { + const wallet = useWallet() + const isSafeApp = useIsSafeApp() + + const { sdk } = useSafeAppsSDK() + + if (isSafeApp && sdk) { + return { + isBatchingSupported: true, + sendTxs: (txs: BaseTransaction[]) => { + return sdk.txs.send({ txs }) + }, + } + } + + if (wallet) { + return { + isBatchingSupported: false, + sendTxs: (txs: BaseTransaction[]) => { + // No batched txs allowed + if (txs.length !== 1) { + throw new Error('Batched txs are only supported when opened as Safe App') + } + const tx = txs[0] + return wallet.provider.request({ method: 'eth_sendTransaction', params: [{ ...tx }] }) + }, + } + } +} diff --git a/src/hooks/useWallet.ts b/src/hooks/useWallet.ts index 54b57833..32a664fa 100644 --- a/src/hooks/useWallet.ts +++ b/src/hooks/useWallet.ts @@ -4,7 +4,6 @@ import type { EIP1193Provider, WalletState } from '@web3-onboard/core' import { useOnboard } from '@/hooks/useOnboard' import { localItem } from '@/services/storage/local' -import { isWalletUnlocked } from '@/utils/wallet' export type ConnectedWallet = { label: string @@ -99,12 +98,8 @@ export const useInitWallet = () => { return } - isWalletUnlocked(label).then((isUnlocked) => { - if (isUnlocked) { - onboard.connectWallet({ - autoSelect: { label, disableModals: true }, - }) - } + onboard.connectWallet({ + autoSelect: { label, disableModals: true }, }) }, [onboard]) } diff --git a/src/hooks/useWeb3.ts b/src/hooks/useWeb3.ts index 63e4855b..aa94b011 100644 --- a/src/hooks/useWeb3.ts +++ b/src/hooks/useWeb3.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk' +import { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' import type { JsonRpcProvider } from '@ethersproject/providers' import { useWallet } from '@/hooks/useWallet' diff --git a/src/styles/colors-dark.ts b/src/styles/colors-dark.ts index aaacd0f1..3ea7e651 100644 --- a/src/styles/colors-dark.ts +++ b/src/styles/colors-dark.ts @@ -1,7 +1,7 @@ const darkPalette = { text: { primary: '#FFFFFF', - secondary: '#636669', + secondary: '#A1A3A7', disabled: '#636669', }, primary: { diff --git a/src/styles/theme.ts b/src/styles/theme.ts index fe6607f8..64d2c8ce 100644 --- a/src/styles/theme.ts +++ b/src/styles/theme.ts @@ -224,8 +224,13 @@ const initTheme = (darkMode: boolean) => { styleOverrides: { root: ({ theme }) => ({ fontWeight: 700, + fontSize: '14px', + letterSpacing: '0.46px', + color: theme.palette.text.primary, + textDecorationColor: 'inherit', '&:hover': { color: theme.palette.primary.light, + textDecorationColor: 'inherit', }, }), }, diff --git a/src/tests/test-utils.tsx b/src/tests/test-utils.tsx index 4740d18b..91e6fe5c 100644 --- a/src/tests/test-utils.tsx +++ b/src/tests/test-utils.tsx @@ -1,4 +1,4 @@ -import SafeProvider from '@gnosis.pm/safe-apps-react-sdk' +import SafeProvider from '@safe-global/safe-apps-react-sdk' import { render, renderHook } from '@testing-library/react' import type { RenderHookOptions } from '@testing-library/react' diff --git a/src/utils/__tests__/boost.test.ts b/src/utils/__tests__/boost.test.ts new file mode 100644 index 00000000..6ac7e264 --- /dev/null +++ b/src/utils/__tests__/boost.test.ts @@ -0,0 +1,247 @@ +import { SEASON1_START, SEASON2_START } from '@/config/constants' +import { floorNumber, getBoostFunction, getTimeFactor, getTokenBoost } from '../boost' +import { LockHistory } from '../lock' + +describe('boost', () => { + describe('floorNumber', () => { + it('should floor numbers without decimals', () => { + expect(floorNumber(2.0, 2)).toEqual(2) + }) + + it('should floor numbers above .5 decimals', () => { + expect(floorNumber(2.449, 1)).toEqual(2.4) + }) + + it('should floor numbers with .5 decimals', () => { + expect(floorNumber(2.45, 1)).toEqual(2.4) + }) + + it('should floor numbers above .5 decimals', () => { + expect(floorNumber(2.49, 1)).toEqual(2.4) + }) + }) + + describe('getTokenBoost', () => { + it('should return 0 for NaN amount', () => { + expect(getTokenBoost(Number('*'))).toBe(0) + expect(getTokenBoost(Number('100-10'))).toBe(0) + expect(getTokenBoost(Number('100**10'))).toBe(0) + }) + it('should return 0 for amounts below 100', () => { + expect(getTokenBoost(0)).toBe(0) + expect(getTokenBoost(1)).toBe(0) + expect(getTokenBoost(50)).toBe(0) + expect(getTokenBoost(99)).toBe(0) + expect(getTokenBoost(100)).toBe(0) + }) + + it('should use the correct function for amounts between 100 and 1000', () => { + expect(getTokenBoost(101)).toBe(101 * 0.000277778 - 0.0277778) + expect(getTokenBoost(500)).toBe(500 * 0.000277778 - 0.0277778) + expect(getTokenBoost(1000)).toBeCloseTo(0.25) + }) + + it('should use the correct function for amounts between 1000 and 10000', () => { + expect(getTokenBoost(1001)).toBe(1001 * 0.0000277778 + 0.222222) + expect(getTokenBoost(5000)).toBe(5000 * 0.0000277778 + 0.222222) + expect(getTokenBoost(10000)).toBe(0.5) + }) + + it('should use the correct function for amounts between 10001 and 100000', () => { + expect(getTokenBoost(10001)).toBe(10001 * 5.55556 * 10 ** -6 + 0.444444) + expect(getTokenBoost(50000)).toBe(50000 * 5.55556 * 10 ** -6 + 0.444444) + expect(getTokenBoost(100000)).toBe(1) + }) + + it('should be 1 for numbers above 100_000', () => { + expect(getTokenBoost(100001)).toBe(1) + }) + }) + + describe('getTimeFactor', () => { + it('should return 1 for negative days', () => { + expect(getTimeFactor(-1)).toBe(1) + }) + + it('should return 1 on first day', () => { + expect(getTimeFactor(0)).toBe(1) + }) + + it('should return 1 on first 28 days', () => { + expect(getTimeFactor(27)).toBe(1) + }) + + it('should use the correct function after 28 days', () => { + expect(getTimeFactor(30)).toBeCloseTo(1 - 3 / 133) + }) + + it('it should be 0 after the season', () => { + expect(getTimeFactor(200)).toBe(0) + }) + }) + + describe('getBoostFunction', () => { + describe('without prior locks on day 0', () => { + it('should always return 1.0 for amount NaN', () => { + const boostFunction = getBoostFunction(0, Number.NaN, []) + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBe(1) + expect(boostFunction({ x: SEASON1_START })).toBe(1) + expect(boostFunction({ x: SEASON2_START })).toBe(1) + expect(boostFunction({ x: 1000 })).toBe(1) + }) + it('should always return 1.0 for amount 0', () => { + const boostFunction = getBoostFunction(0, 0, []) + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBe(1) + expect(boostFunction({ x: SEASON1_START })).toBe(1) + expect(boostFunction({ x: SEASON2_START })).toBe(1) + expect(boostFunction({ x: 1000 })).toBe(1) + }) + + it('should compute boost with 1000 tokens locked', () => { + const boostFunction = getBoostFunction(0, 1000, []) + expect(boostFunction({ x: -1 })).toBeCloseTo(1) + expect(boostFunction({ x: 0 })).toBeCloseTo(1.25) + expect(boostFunction({ x: SEASON1_START })).toBeCloseTo(1.25) + expect(boostFunction({ x: SEASON2_START })).toBeCloseTo(1.25) + expect(boostFunction({ x: 1000 })).toBeCloseTo(1.25) + }) + + it('should compute boost with 10000 tokens locked', () => { + const boostFunction = getBoostFunction(0, 10000, []) + expect(boostFunction({ x: -1 })).toBeCloseTo(1) + expect(boostFunction({ x: 0 })).toBeCloseTo(1.5) + expect(boostFunction({ x: SEASON1_START })).toBeCloseTo(1.5) + expect(boostFunction({ x: SEASON2_START })).toBeCloseTo(1.5) + expect(boostFunction({ x: 1000 })).toBeCloseTo(1.5) + }) + + it('should compute boost with 1000000 tokens locked', () => { + const boostFunction = getBoostFunction(0, 1000000, []) + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBe(2) + expect(boostFunction({ x: SEASON1_START })).toBe(2) + expect(boostFunction({ x: SEASON2_START })).toBe(2) + expect(boostFunction({ x: 1000 })).toBe(2) + }) + }) + + it('should compute for 100_000 tokens locked on day 0', () => { + const boostFunction = getBoostFunction(0, 100_000, []) + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBeCloseTo(2) + expect(boostFunction({ x: 39 })).toBeCloseTo(2) + expect(boostFunction({ x: 40 })).toBeCloseTo(2) + expect(boostFunction({ x: 1000 })).toBeCloseTo(2) + }) + + it('should compute for 100_000 tokens locked on day 45', () => { + const boostFunction = getBoostFunction(45, 100_000, []) + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBeCloseTo(1) + expect(boostFunction({ x: 44 })).toBeCloseTo(1) + // 1.0 * 0.86 + 1 = 1.86 + expect(boostFunction({ x: 45 })).toBeCloseTo(1.86) + expect(boostFunction({ x: 1000 })).toBeCloseTo(1.86) + }) + + it('should keep boost unchanged if NaN is the locked amount', () => { + const priorLock: LockHistory = { + day: 0, + amount: 1000, + } + const boostFunction = getBoostFunction(40, Number.NaN, [priorLock]) + + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBeCloseTo(1.25) + expect(boostFunction({ x: 40 })).toBeCloseTo(1.25) + expect(boostFunction({ x: 1000 })).toBeCloseTo(1.25) + }) + + it('should compute for 1000 tokens on day one and 1000 after 40 days', () => { + const priorLock: LockHistory = { + day: 0, + amount: 1000, + } + const boostFunction = getBoostFunction(40, 1000, [priorLock]) + + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBeCloseTo(1.25) + expect(boostFunction({ x: 39 })).toBeCloseTo(1.25) + // + expect(boostFunction({ x: 40 })).toBeCloseTo(1.275) + expect(boostFunction({ x: 1000 })).toBeCloseTo(1.275) + }) + + it('should compute for 1000 tokens on day one and 1000 after 40 days and another 1000 after 80 days', () => { + const priorLocks: LockHistory[] = [ + { + day: 0, + amount: 1000, + }, + { + day: 40, + amount: 1000, + }, + { + day: 80, + amount: 1000, + }, + ] + const boostFunction = getBoostFunction(100, 0, priorLocks) + + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBeCloseTo(1.25) + expect(boostFunction({ x: 39 })).toBeCloseTo(1.25) + expect(boostFunction({ x: 40 })).toBeCloseTo(1.275) + expect(boostFunction({ x: 79 })).toBeCloseTo(1.275) + expect(boostFunction({ x: 80 })).toBeCloseTo(1.292) + expect(boostFunction({ x: 1000 })).toBeCloseTo(1.292) + }) + + it('should drop to 1 if everything gets unlocked', () => { + const priorLocks: LockHistory[] = [ + { + day: 0, + amount: 10000, + }, + ] + const boostFunction = getBoostFunction(SEASON1_START, -10000, priorLocks) + + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBeCloseTo(1.5) + expect(boostFunction({ x: SEASON1_START })).toBe(1) + expect(boostFunction({ x: SEASON2_START })).toBe(1) + expect(boostFunction({ x: 1000 })).toBe(1) + }) + + it('should compute for 2000 tokens on day one, unlocking 1000 after 40 days and locking another 1000 after 80 days', () => { + const priorLocks: LockHistory[] = [ + { + day: 0, + amount: 2000, + }, + { + day: 39, + amount: -1000, + }, + { + day: 79, + amount: 1000, + }, + ] + const boostFunction = getBoostFunction(100, 0, priorLocks) + + expect(boostFunction({ x: -1 })).toBe(1) + expect(boostFunction({ x: 0 })).toBeCloseTo(1.277) + expect(boostFunction({ x: 38 })).toBeCloseTo(1.277) + // 0.25 * 0.91 + 1 + expect(boostFunction({ x: 39 })).toBeCloseTo(1.2275) + expect(boostFunction({ x: 78 })).toBeCloseTo(1.2275) + // 1.2275 + (1.277 - 1.25) * (0.609) + expect(boostFunction({ x: 79 })).toBeCloseTo(1.2439) + expect(boostFunction({ x: 1000 })).toBeCloseTo(1.2439) + }) + }) +}) diff --git a/src/utils/__tests__/claim.test.ts b/src/utils/__tests__/claim.test.ts index c50c6a70..44bb9886 100644 --- a/src/utils/__tests__/claim.test.ts +++ b/src/utils/__tests__/claim.test.ts @@ -20,7 +20,7 @@ describe('createClaimTxs', () => { account: safeAddress, amount: parseEther('100').toString(), amountClaimed: '0', - chainId: 5, + chainId: 11155111, contract: mockUserAirdropAddress, curve: 0, durationWeeks: 416, @@ -76,7 +76,7 @@ describe('createClaimTxs', () => { account: safeAddress, amount: parseEther('200').toString(), amountClaimed: '0', - chainId: 5, + chainId: 11155111, contract: mockSep5AirdropAddress, curve: 0, durationWeeks: 416, @@ -132,7 +132,7 @@ describe('createClaimTxs', () => { account: safeAddress, amount: parseEther('100').toString(), amountClaimed: '0', - chainId: 5, + chainId: 11155111, contract: mockUserAirdropAddress, curve: 0, durationWeeks: 416, @@ -147,7 +147,7 @@ describe('createClaimTxs', () => { account: safeAddress, amount: parseEther('50').toString(), amountClaimed: '0', - chainId: 5, + chainId: 11155111, contract: mockSep5AirdropAddress, curve: 0, durationWeeks: 416, @@ -188,7 +188,7 @@ describe('createClaimTxs', () => { account: safeAddress, amount: parseEther('100').toString(), amountClaimed: '0', - chainId: 5, + chainId: 11155111, contract: mockInvestorVestingAddress, curve: 0, durationWeeks: 416, @@ -223,7 +223,7 @@ describe('createClaimTxs', () => { account: safeAddress, amount: parseEther('100').toString(), amountClaimed: '0', - chainId: 5, + chainId: 11155111, contract: mockInvestorVestingAddress, curve: 0, durationWeeks: 416, diff --git a/src/utils/__tests__/date.test.ts b/src/utils/__tests__/date.test.ts new file mode 100644 index 00000000..199d40cd --- /dev/null +++ b/src/utils/__tests__/date.test.ts @@ -0,0 +1,90 @@ +import { Chains, CHAIN_START_TIMESTAMPS } from '@/config/constants' +import { formatDay, timeRemaining } from '../date' + +describe('date', () => { + let now = 0 + beforeAll(() => { + jest.useFakeTimers().setSystemTime(new Date('2024-01-01')) + now = Date.now() / 1000 + }) + describe('timeRemaining', () => { + it('should return zeroes for 0 millis', () => { + expect(timeRemaining(now)).toEqual({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + }) + }) + + it('should return zeroes for negative millis', () => { + expect(timeRemaining(now - 1)).toEqual({ + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + }) + }) + + it('should compute days', () => { + expect(timeRemaining(now + 60 * 60 * 24 * 3)).toEqual({ + days: 3, + hours: 0, + minutes: 0, + seconds: 0, + }) + }) + + it('should compute hours', () => { + expect(timeRemaining(now + 60 * 60 * 4)).toEqual({ + days: 0, + hours: 4, + minutes: 0, + seconds: 0, + }) + }) + it('should compute minutes', () => { + expect(timeRemaining(now + 60 * 5)).toEqual({ + days: 0, + hours: 0, + minutes: 5, + seconds: 0, + }) + }) + it('should compute seconds', () => { + expect(timeRemaining(now + 6)).toEqual({ + days: 0, + hours: 0, + minutes: 0, + seconds: 6, + }) + }) + + it('should compute mixed amounds', () => { + expect(timeRemaining(now + 6 + 60 * 5 + 60 * 60 * 4 + 60 * 60 * 24 * 3)).toEqual({ + days: 3, + hours: 4, + minutes: 5, + seconds: 6, + }) + }) + }) + + describe('formatDay', () => { + it('should work for negative numbers', () => { + expect(formatDay(-1, CHAIN_START_TIMESTAMPS[Chains.MAINNET])).toEqual('April 22') + }) + + it('should work for zero', () => { + expect(formatDay(0, CHAIN_START_TIMESTAMPS[Chains.MAINNET])).toEqual('April 23') + }) + + it('should work for positive numbers', () => { + expect(formatDay(7, CHAIN_START_TIMESTAMPS[Chains.MAINNET])).toEqual('April 30') + }) + + it('should work for future months', () => { + expect(formatDay(30, CHAIN_START_TIMESTAMPS[Chains.MAINNET])).toEqual('May 23') + }) + }) +}) diff --git a/src/utils/__tests__/lock.test.ts b/src/utils/__tests__/lock.test.ts new file mode 100644 index 00000000..fc980721 --- /dev/null +++ b/src/utils/__tests__/lock.test.ts @@ -0,0 +1,39 @@ +import { Chains, CHAIN_START_TIMESTAMPS } from '@/config/constants' +import { toRelativeLockHistory } from '../lock' + +describe('toRelativeLockHistory', () => { + it('should sort by execution timestamp', () => { + const relativeHistory = toRelativeLockHistory( + [ + { + executionDate: '2024-04-03T14:49:24.000Z', + transactionHash: '0xe3be658eb5ab72b0ed05f58c0987328dfeaa1b23b23111fb3325314dad83c9f6', + holder: '0xC4934eA242Bf292CA59A2aF85ED116506F8f2d03', + amount: '500000000000000000000000', + logIndex: '88', + unlockIndex: '0', + eventType: 'UNLOCKED', + }, + { + executionDate: '2024-03-22T08:37:12.000Z', + transactionHash: '0x2fde285a44b821d8482047cc516b71c0a510ae198553ce98c8973d5a5119f91b', + holder: '0xC4934eA242Bf292CA59A2aF85ED116506F8f2d03', + amount: '1000000000000000000000000', + logIndex: '193', + eventType: 'LOCKED', + }, + ], + CHAIN_START_TIMESTAMPS[Chains.SEPOLIA], + ) + + expect(relativeHistory).toHaveLength(2) + expect(relativeHistory[0]).toEqual({ + day: 20, + amount: 1000000, + }) + expect(relativeHistory[1]).toEqual({ + day: 33, + amount: -500000, + }) + }) +}) diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts new file mode 100644 index 00000000..7bcfdbb9 --- /dev/null +++ b/src/utils/analytics.ts @@ -0,0 +1,13 @@ +const SAFE_APPS_ANALYTICS_CATEGORY = 'safe-apps-analytics' + +export const trackSafeAppEvent = (action: string, label?: string) => { + window.parent.postMessage( + { + category: SAFE_APPS_ANALYTICS_CATEGORY, + action, + label, + safeAppName: 'Safe{Explorers}', + }, + '*', + ) +} diff --git a/src/utils/boost.ts b/src/utils/boost.ts new file mode 100644 index 00000000..bbad1775 --- /dev/null +++ b/src/utils/boost.ts @@ -0,0 +1,68 @@ +import { LockHistory } from './lock' + +export const floorNumber = (num: number, digits: number) => { + const decimal = Math.pow(10, digits) + return Math.floor(num * decimal) / decimal +} + +export const getTokenBoost = (amountLocked: number) => { + if (isNaN(amountLocked) || amountLocked <= 100) { + return 0 + } + if (amountLocked <= 1_000) { + return amountLocked * 0.000277778 - 0.0277778 + } + if (amountLocked <= 10_000) { + return amountLocked * 0.0000277778 + 0.222222 + } + if (amountLocked < 100_000) { + return amountLocked * 5.55556 * 10 ** -6 + 0.444444 + } + return 1 +} + +export const getTimeFactor = (days: number) => { + if (days <= 27) { + return 1 + } + + return Math.max(0, 1 - (days - 27) / 133) +} + +/** + * + * @param now today in days since begin of program + * @param amountDiff user entered amount in the app + * @param history lock history + * @returns + */ +export const getBoostFunction = + (now: number, amountDiff: number, history: LockHistory[]) => + (d: { x: number }): number => { + // Add new boost to history + const newHistory: LockHistory[] = [...history, { amount: isNaN(amountDiff) ? 0 : amountDiff, day: now }] + + // Filter out all entries that were made after the current day (x) + const filteredHistory = newHistory.filter((entry) => entry.day <= d.x) + let currentBoost = 1 + let lockedAmount = 0 + for (let i = 0; i < filteredHistory.length; i++) { + const currentEvent = filteredHistory[i] + const prevLockedAmount = lockedAmount + lockedAmount = lockedAmount + currentEvent.amount + + if (currentEvent.amount >= 0) { + // For the first lock we need to only consider the time factor of today so we divide by 1 + // handle lock event + const boostGain = getTokenBoost(lockedAmount) - getTokenBoost(prevLockedAmount) + const timeFactorLock = getTimeFactor(currentEvent.day) + currentBoost = currentBoost + boostGain * timeFactorLock + } else { + // handle unlock + currentBoost = getTokenBoost(lockedAmount) * getTimeFactor(currentEvent.day) + 1 + } + } + + // Compute and add the boost for each interval + 1 + return currentBoost + } diff --git a/src/utils/claim.ts b/src/utils/claim.ts index 092c6f2e..591189e8 100644 --- a/src/utils/claim.ts +++ b/src/utils/claim.ts @@ -1,10 +1,10 @@ import { BigNumber } from 'ethers' -import type { BaseTransaction } from '@gnosis.pm/safe-apps-sdk/dist/src/types/sdk.d' import { getVestingTypes } from '@/utils/vesting' import { getAirdropInterface } from '@/services/contracts/Airdrop' import { splitAirdropAmounts } from '@/utils/airdrop' import type { Vesting } from '@/hooks/useSafeTokenAllocation' +import { BaseTransaction } from '@/hooks/useTxSender' const airdropInterface = getAirdropInterface() diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 00000000..2448ff3c --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,67 @@ +const DAY = 60 * 60 * 24 +const HOUR = 60 * 60 +const MINUTE = 60 + +export const timeRemaining = (timestampInSeconds: number) => { + let remainingSeconds = Math.max(0, timestampInSeconds - Date.now() / 1000) + const days = Math.floor(remainingSeconds / DAY) + remainingSeconds = remainingSeconds % DAY + const hours = Math.floor(remainingSeconds / HOUR) + remainingSeconds = remainingSeconds % HOUR + const minutes = Math.floor(remainingSeconds / MINUTE) + const seconds = remainingSeconds % MINUTE + + return { + days, + hours, + minutes, + seconds, + } +} + +const MONTH_LABEL: Record = { + 0: 'January', + 1: 'February', + 2: 'March', + 3: 'April', + 4: 'May', + 5: 'June', + 6: 'July', + 7: 'August', + 8: 'September', + 9: 'October', + 10: 'November', + 11: 'December', +} + +export const formatDay = (days: number, start: number) => { + const date = new Date(start + days * DAY * 1000) + const month = date.getMonth() + const day = date.getDate() + + return `${MONTH_LABEL[month]} ${day}` +} + +export const DAY_IN_MS = 1000 * 60 * 60 * 24 + +export const isGreaterThan24HoursDiff = (timestamp1: number, timestamp2: number) => { + return Math.abs(timestamp1 - timestamp2) > DAY_IN_MS +} + +export const toDaysSinceStart = (timestamp: number, start: number) => { + return Math.floor((timestamp - start) / DAY_IN_MS) +} + +export const getCurrentDays = (startTime: number) => toDaysSinceStart(Date.now(), startTime) + +export const formatDate = (date: Date) => { + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + } + + return `${date.toLocaleString(undefined, options)}h` +} diff --git a/src/utils/gateway.ts b/src/utils/gateway.ts index 7ee54f15..4be377ed 100644 --- a/src/utils/gateway.ts +++ b/src/utils/gateway.ts @@ -16,3 +16,7 @@ export const getHashedExplorerUrl = ( return replaceTemplate(blockExplorerUriTemplate[param], { [param]: hash }) } + +export const toCursorParam = (limit: number, offset?: number) => { + return `cursor=limit%3D${limit}${offset ? `%26offset%3D${offset}` : ``}` +} diff --git a/src/utils/lock.ts b/src/utils/lock.ts new file mode 100644 index 00000000..21325cbf --- /dev/null +++ b/src/utils/lock.ts @@ -0,0 +1,96 @@ +import { CHAIN_SAFE_LOCKING_ADDRESS } from '@/config/constants' +import { BigNumber } from 'ethers' +import type { JsonRpcProvider } from '@ethersproject/providers' +import { formatUnits, Interface } from 'ethers/lib/utils' +import { LockingHistoryEntry, UnlockEvent, useLockHistory, WithdrawEvent } from '@/hooks/useLockHistory' +import { toDaysSinceStart } from './date' + +const safeLockingInterface = new Interface([ + 'function lock(uint96)', + 'function unlock(uint96)', + 'function getUser(address)', + 'function getUserTokenBalance(address)', + 'function getUnlock(address,uint32)', + 'function withdraw(uint32)', +]) + +export const isUnlockEvent = (event: LockingHistoryEntry): event is UnlockEvent => event.eventType === 'UNLOCKED' +export const isWithdrawEvent = (event: LockingHistoryEntry): event is WithdrawEvent => event.eventType === 'WITHDRAWN' + +export type LockHistory = { + day: number + // can be negative for unlocks + amount: number +} + +/** + * Return the relative lock history sorted by execution time + * + * @param data lockHistory + * @param startTime startTime as timestamp + * @returns sorted history of lock amount differences + */ +export const toRelativeLockHistory = (data: ReturnType, startTime: number): LockHistory[] => { + const sortedHistory = [...data].sort((d1, d2) => Date.parse(d1.executionDate) - Date.parse(d2.executionDate)) + return sortedHistory + .filter((entry) => entry.eventType !== 'WITHDRAWN') + .map((entry) => ({ + day: toDaysSinceStart(Date.parse(entry.executionDate), startTime), + amount: Number(formatUnits(BigNumber.from(entry.amount), 18)) * (entry.eventType === 'LOCKED' ? 1 : -1), + })) +} + +export const createLockTx = (chainId: string, amount: BigNumber) => { + return { + to: CHAIN_SAFE_LOCKING_ADDRESS[chainId], + data: safeLockingInterface.encodeFunctionData('lock', [amount]), + value: '0', + operation: 0, + } +} + +export const createUnlockTx = (chainId: string, amount: BigNumber) => { + return { + to: CHAIN_SAFE_LOCKING_ADDRESS[chainId], + data: safeLockingInterface.encodeFunctionData('unlock', [amount]), + value: '0', + operation: 0, + } +} + +/** + * Calls the `withdraw` function with 0 `maxUnlocks`. + * This will withdraw all available unlocks. + */ +export const createWithdrawTx = (chainId: string) => { + return { + to: CHAIN_SAFE_LOCKING_ADDRESS[chainId], + data: safeLockingInterface.encodeFunctionData('withdraw', [0]), + value: '0', + } +} + +/** + * Returns total tokens in the locking contract including locked and unlocked amounts. + * + * @param chainId + * @param safeAddress + * @param provider + * @returns total token balance + */ +export const fetchLockingContractBalance = async (chainId: string, safeAddress: string, provider: JsonRpcProvider) => { + const lockingAddress = CHAIN_SAFE_LOCKING_ADDRESS[chainId] + + if (!lockingAddress) { + return '0' + } + + try { + return await provider.call({ + to: lockingAddress, + data: safeLockingInterface.encodeFunctionData('getUserTokenBalance', [safeAddress]), + }) + } catch (err) { + throw Error(`Error fetching Safe Token balance in locking contract: ${err}`) + } +} diff --git a/src/utils/onboard.ts b/src/utils/onboard.ts index 2c795584..5747db3a 100644 --- a/src/utils/onboard.ts +++ b/src/utils/onboard.ts @@ -1,61 +1,19 @@ -import coinbaseModule from '@web3-onboard/coinbase' -import injectedWalletModule, { ProviderLabel } from '@web3-onboard/injected-wallets' -import keystoneModule from '@web3-onboard/keystone' -import ledgerModule from '@web3-onboard/ledger/dist/index' -import trezorModule from '@web3-onboard/trezor' import walletConnect from '@web3-onboard/walletconnect' -import tahoModule from '@web3-onboard/taho' -import type { RecommendedInjectedWallets, WalletInit, WalletModule } from '@web3-onboard/common/dist/types.d' +import type { WalletInit } from '@web3-onboard/common/dist/types.d' import { hexValue } from '@ethersproject/bytes' import Onboard from '@web3-onboard/core' import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' import manifestJson from '@/public/manifest.json' import { getRpcServiceUrl } from '@/utils/web3' -import { TREZOR_APP_URL, TREZOR_EMAIL, WC_BRIDGE, WC_PROJECT_ID } from '@/config/constants' +import { WC_PROJECT_ID } from '@/config/constants' export const enum WALLET_KEYS { - COINBASE = 'COINBASE', - INJECTED = 'INJECTED', - KEYSTONE = 'KEYSTONE', - LEDGER = 'LEDGER', - TAHO = 'TAHO', - TREZOR = 'TREZOR', - WALLETCONNECT = 'WALLETCONNECT', - WALLETCONNECT_V2 = 'WALLETCONNECT_V2', -} - -export const enum INJECTED_WALLET_KEYS { - METAMASK = 'METAMASK', + WALLETCONNECT = 'WalletConnect', } const CGW_NAMES: { [key in WALLET_KEYS]: string } = { - [WALLET_KEYS.COINBASE]: 'coinbase', - [WALLET_KEYS.INJECTED]: 'detectedwallet', - [WALLET_KEYS.KEYSTONE]: 'keystone', - [WALLET_KEYS.LEDGER]: 'ledger', - [WALLET_KEYS.TAHO]: 'tally', - [WALLET_KEYS.TREZOR]: 'trezor', - [WALLET_KEYS.WALLETCONNECT]: 'walletConnect', - [WALLET_KEYS.WALLETCONNECT_V2]: 'walletConnect_v2', -} - -const prefersDarkMode = (): boolean => { - return window?.matchMedia('(prefers-color-scheme: dark)')?.matches -} - -// We need to modify the module name as onboard dedupes modules with the same label and the WC v1 and v2 modules have the same -// @see https://github.com/blocknative/web3-onboard/blob/d399e0b76daf7b363d6a74b100b2c96ccb14536c/packages/core/src/store/actions.ts#L419 -// TODO: When removing this, also remove the associated CSS in `onboard.css` -export const WALLET_CONNECT_V1_MODULE_NAME = 'WalletConnect v1' -const walletConnectV1 = (): WalletInit => { - return (helpers) => { - const walletConnectModule = walletConnect({ version: 1, bridge: WC_BRIDGE })(helpers) as WalletModule - - walletConnectModule.label = WALLET_CONNECT_V1_MODULE_NAME - - return walletConnectModule - } + [WALLET_KEYS.WALLETCONNECT]: 'WalletConnect', } const walletConnectV2 = (chain: ChainInfo): WalletInit => { @@ -66,31 +24,20 @@ const walletConnectV2 = (chain: ChainInfo): WalletInit => { themeVariables: { '--wcm-z-index': '1302', }, - themeMode: prefersDarkMode() ? 'dark' : 'light', + themeMode: 'dark', }, requiredChains: [parseInt(chain.chainId)], }) } const WALLET_MODULES: { [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit } = { - [WALLET_KEYS.COINBASE]: () => coinbaseModule({ darkMode: prefersDarkMode() }), - [WALLET_KEYS.INJECTED]: () => injectedWalletModule(), - [WALLET_KEYS.KEYSTONE]: () => keystoneModule(), - [WALLET_KEYS.LEDGER]: () => ledgerModule(), - [WALLET_KEYS.TAHO]: () => tahoModule(), - [WALLET_KEYS.TREZOR]: () => trezorModule({ appUrl: TREZOR_APP_URL, email: TREZOR_EMAIL }), - [WALLET_KEYS.WALLETCONNECT]: () => walletConnectV1(), - [WALLET_KEYS.WALLETCONNECT_V2]: (chain) => walletConnectV2(chain), + [WALLET_KEYS.WALLETCONNECT]: (chain) => walletConnectV2(chain), } const getAllWallets = (chain: ChainInfo): WalletInit[] => { return Object.values(WALLET_MODULES).map((module) => module(chain)) } -const getRecommendedInjectedWallets = (): RecommendedInjectedWallets[] => { - return [{ name: ProviderLabel.MetaMask, url: 'https://metamask.io' }] -} - export const createOnboard = (chainConfigs: ChainInfo[], currentChain: ChainInfo) => { const chains = chainConfigs.map((cfg) => ({ id: hexValue(parseInt(cfg.chainId)), @@ -113,7 +60,6 @@ export const createOnboard = (chainConfigs: ChainInfo[], currentChain: ChainInfo name: manifestJson.name, icon: '/images/app-logo.svg', description: `Please select a wallet to connect to ${manifestJson.name}`, - recommendedInjectedWallets: getRecommendedInjectedWallets(), }, connect: { removeWhereIsMyWalletWarning: true, diff --git a/src/utils/safe-apps.ts b/src/utils/safe-apps.ts index bdc0e7ac..e570ddd0 100644 --- a/src/utils/safe-apps.ts +++ b/src/utils/safe-apps.ts @@ -5,7 +5,7 @@ export const getGovernanceAppSafeAppUrl = (chainId: string, address: string): st const shortName = CHAIN_SHORT_NAME[chainId] url.searchParams.append('safe', `${shortName}:${address}`) - url.searchParams.append('appUrl', DEPLOYMENT_URL) + url.searchParams.append('appUrl', `${DEPLOYMENT_URL}/governance`) return url.toString() } diff --git a/src/utils/safe-token.ts b/src/utils/safe-token.ts new file mode 100644 index 00000000..d6715ae9 --- /dev/null +++ b/src/utils/safe-token.ts @@ -0,0 +1,59 @@ +import { CHAIN_SAFE_LOCKING_ADDRESS, CHAIN_SAFE_TOKEN_ADDRESS } from '@/config/constants' +import { getSafeTokenInterface } from '@/services/contracts/SafeToken' +import type { JsonRpcProvider } from '@ethersproject/providers' +import { BigNumber } from 'ethers' + +const tokenInterface = getSafeTokenInterface() + +export const fetchTokenBalance = async ( + chainId: string, + safeAddress: string, + provider: JsonRpcProvider, +): Promise => { + const safeTokenAddress = CHAIN_SAFE_TOKEN_ADDRESS[chainId] + + if (!safeTokenAddress) { + return '0' + } + + try { + return await provider.call({ + to: safeTokenAddress, + data: tokenInterface.encodeFunctionData('balanceOf', [safeAddress]), + }) + } catch (err) { + throw Error(`Error fetching Safe Token balance: ${err}`) + } +} + +export const fetchTokenLockingAllowance = async ( + chainId: string, + safeAddress: string, + provider: JsonRpcProvider, +): Promise => { + const safeTokenAddress = CHAIN_SAFE_TOKEN_ADDRESS[chainId] + const safeLockingAddress = CHAIN_SAFE_LOCKING_ADDRESS[chainId] + + if (!safeTokenAddress) { + return '0' + } + + try { + return await provider.call({ + to: safeTokenAddress, + data: tokenInterface.encodeFunctionData('allowance', [safeAddress, safeLockingAddress]), + }) + } catch (err) { + throw Error(`Error fetching Safe Token balance: ${err}`) + } +} + +export const createApproveTx = (chainId: string, amount: BigNumber) => { + const safeTokenAddress = CHAIN_SAFE_TOKEN_ADDRESS[chainId] + const safeLockingAddress = CHAIN_SAFE_LOCKING_ADDRESS[chainId] + return { + to: safeTokenAddress, + value: '0', + data: tokenInterface.encodeFunctionData('approve', [safeLockingAddress, amount]), + } +} diff --git a/src/utils/wallet.ts b/src/utils/wallet.ts index 2f13b6b8..2717ef34 100644 --- a/src/utils/wallet.ts +++ b/src/utils/wallet.ts @@ -1,34 +1,7 @@ -import { ProviderLabel } from '@web3-onboard/injected-wallets' import { getSafeInfo } from '@safe-global/safe-gateway-typescript-sdk' -import { local } from '@/services/storage/local' -import { WALLET_CONNECT_V1_MODULE_NAME } from '@/utils/onboard' import type { ConnectedWallet } from '@/hooks/useWallet' -export const WalletNames = { - METAMASK: ProviderLabel.MetaMask, - WALLET_CONNECT: WALLET_CONNECT_V1_MODULE_NAME, -} - -export const isWalletUnlocked = async (walletName: string): Promise => { - if (typeof window === 'undefined') { - return false - } - - // Only MetaMask exposes a method to check if the wallet is unlocked - if (walletName === WalletNames.METAMASK) { - return window.ethereum?._metamask?.isUnlocked?.() || false - } - - // Wallet connect creates a localStorage entry when connected and removes it when disconnected - if (walletName === WalletNames.WALLET_CONNECT) { - const WC_SESSION_KEY = 'walletconnect' - return local.getItem(WC_SESSION_KEY) !== null - } - - return false -} - export const isSafe = async (wallet: ConnectedWallet): Promise => { try { return !!(await getSafeInfo(wallet.chainId, wallet.address)) diff --git a/src/utils/web3.ts b/src/utils/web3.ts index ba0a3bfa..c365804c 100644 --- a/src/utils/web3.ts +++ b/src/utils/web3.ts @@ -1,7 +1,7 @@ import { RPC_AUTHENTICATION } from '@safe-global/safe-gateway-typescript-sdk' import { Web3Provider } from '@ethersproject/providers' -import { SafeAppProvider } from '@gnosis.pm/safe-apps-provider' -import type { useSafeAppsSDK } from '@gnosis.pm/safe-apps-react-sdk' +import { SafeAppProvider } from '@safe-global/safe-apps-provider' +import type { useSafeAppsSDK } from '@safe-global/safe-apps-react-sdk' import type { EIP1193Provider } from '@web3-onboard/core' import type { RpcUri } from '@safe-global/safe-gateway-typescript-sdk' diff --git a/yarn.lock b/yarn.lock index cd05a15b..76de154c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.0.2.tgz#bd7d13543a186c3ff3eabb44bec2b22e8fb18ee0" integrity sha512-Fx6tYjk2wKUgLi8uMANZr8GNZx05u44ArIJldn9VxLvolzlJVgHbTUCbwhMd6bcYky178+WUSxPHO3DAtGLWpw== +"@adraffy/ens-normalize@1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" + integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== + "@ampproject/remapping@^2.1.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" @@ -997,6 +1002,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.23.9", "@babel/runtime@^7.24.0": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" @@ -1088,6 +1100,17 @@ "@emotion/weak-memoize" "^0.3.0" stylis "4.1.3" +"@emotion/cache@^11.11.0": + version "11.11.0" + resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.11.0.tgz#809b33ee6b1cb1a625fef7a45bc568ccd9b8f3ff" + integrity sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ== + dependencies: + "@emotion/memoize" "^0.8.1" + "@emotion/sheet" "^1.2.2" + "@emotion/utils" "^1.2.1" + "@emotion/weak-memoize" "^0.3.1" + stylis "4.2.0" + "@emotion/hash@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7" @@ -1105,6 +1128,11 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== +"@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/react@^11.10.5": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d" @@ -1145,6 +1173,11 @@ resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c" integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA== +"@emotion/sheet@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec" + integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA== + "@emotion/styled@^11.10.5": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.5.tgz#1fe7bf941b0909802cb826457e362444e7e96a79" @@ -1172,11 +1205,21 @@ resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561" integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw== +"@emotion/utils@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" + integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== + "@emotion/weak-memoize@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== +"@emotion/weak-memoize@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz#d0fce5d07b0620caa282b5131c297bb60f9d87e6" + integrity sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww== + "@eslint/eslintrc@^1.4.1": version "1.4.1" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.4.1.tgz#af58772019a2d271b7e2d4c23ff4ddcba3ccfb3e" @@ -1964,6 +2007,33 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@floating-ui/core@^1.0.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" + integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g== + dependencies: + "@floating-ui/utils" "^0.2.1" + +"@floating-ui/dom@^1.6.1": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef" + integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw== + dependencies: + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/react-dom@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d" + integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw== + dependencies: + "@floating-ui/dom" "^1.6.1" + +"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== + "@formatjs/ecma402-abstract@1.11.4": version "1.11.4" resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz#b962dfc4ae84361f9f08fbce411b4e4340930eda" @@ -2003,36 +2073,6 @@ dependencies: tslib "^2.1.0" -"@gnosis.pm/safe-apps-provider@^0.15.1": - version "0.15.1" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-provider/-/safe-apps-provider-0.15.1.tgz#1d070a7de87311190d09bb8e467137428c475d63" - integrity sha512-jVU0jpXf30HFdLHSHkmscIm04BYSwfoddjcHI4uPolP+u4F+tHRgfah1xo4XNRSCDqs4GTq38d7YAipGTj0CLA== - dependencies: - "@gnosis.pm/safe-apps-sdk" "7.8.0" - events "^3.3.0" - -"@gnosis.pm/safe-apps-react-sdk@^4.6.2": - version "4.6.2" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-react-sdk/-/safe-apps-react-sdk-4.6.2.tgz#205908311c685b378338f82da261f06f10336532" - integrity sha512-93r9aDw9HzxC/xMmvnW4j5XswKxaAAFizCCZhA+xfQ6EBxH0nnBUmDq3kbxAWofvNgUKaSVgY/iHMvwkDBrllQ== - dependencies: - "@gnosis.pm/safe-apps-sdk" "7.8.0" - -"@gnosis.pm/safe-apps-sdk@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-apps-sdk/-/safe-apps-sdk-7.8.0.tgz#295ab9d563b94208e3042495cdba787fa035c71e" - integrity sha512-kO8fJi1ebiKN9qH1NdDToVBuDQQ0U9NkL467U+84LtNTx5PzUJIu6O7tb4nZD24e/OItinf5W8GDWhhfZeiqOA== - dependencies: - "@gnosis.pm/safe-react-gateway-sdk" "^3.1.3" - ethers "^5.6.8" - -"@gnosis.pm/safe-react-gateway-sdk@^3.1.3": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@gnosis.pm/safe-react-gateway-sdk/-/safe-react-gateway-sdk-3.5.2.tgz#088b0d5db2eb7524ff4141795ecf948adc40c97c" - integrity sha512-6P2uJMnhHcJeErd/t13ChH6sda+vUIOqcrcUDKyWCNXpcmMniPcZzkQxZ8cYz186gQFbslsHSjQ6twnh4yhXUw== - dependencies: - cross-fetch "^3.1.5" - "@hapi/hoek@^9.0.0": version "9.3.0" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" @@ -2630,17 +2670,30 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/base@^5.0.0-beta.40": + version "5.0.0-beta.40" + resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.40.tgz#1f8a782f1fbf3f84a961e954c8176b187de3dae2" + integrity sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ== + dependencies: + "@babel/runtime" "^7.23.9" + "@floating-ui/react-dom" "^2.0.8" + "@mui/types" "^7.2.14" + "@mui/utils" "^5.15.14" + "@popperjs/core" "^2.11.8" + clsx "^2.1.0" + prop-types "^15.8.1" + "@mui/core-downloads-tracker@^5.11.6": version "5.11.6" resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.6.tgz#79a60c0d95a08859cccd62a8d9a5336ef477a840" integrity sha512-lbD3qdafBOf2dlqKhOcVRxaPAujX+9UlPC6v8iMugMeAXe0TCgU3QbGXY3zrJsu6ex64WYDpH4y1+WOOBmWMuA== -"@mui/icons-material@^5.11.0": - version "5.11.0" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.11.0.tgz#9ea6949278b2266d2683866069cd43009eaf6464" - integrity sha512-I2LaOKqO8a0xcLGtIozC9xoXjZAto5G5gh0FYUMAlbsIHNHIjn4Xrw9rvjY20vZonyiGrZNMAlAXYkY6JvhF6A== +"@mui/icons-material@^5.15.15": + version "5.15.15" + resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.15.15.tgz#84ce08225a531d9f5dc5132009d91164b456a0ae" + integrity sha512-kkeU/pe+hABcYDH6Uqy8RmIsr2S/y5bP2rp+Gat4CcRjCcVne6KudS1NrZQhUCRysrTDCAhcbcf9gt+/+pGO2g== dependencies: - "@babel/runtime" "^7.20.6" + "@babel/runtime" "^7.23.9" "@mui/material@^5.11.5": version "5.11.6" @@ -2669,6 +2722,15 @@ "@mui/utils" "^5.11.2" prop-types "^15.8.1" +"@mui/private-theming@^5.15.14": + version "5.15.14" + resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.15.14.tgz#edd9a82948ed01586a01c842eb89f0e3f68970ee" + integrity sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/utils" "^5.15.14" + prop-types "^15.8.1" + "@mui/styled-engine@^5.11.0": version "5.11.0" resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.11.0.tgz#79afb30c612c7807c4b77602cf258526d3997c7b" @@ -2679,6 +2741,16 @@ csstype "^3.1.1" prop-types "^15.8.1" +"@mui/styled-engine@^5.15.14": + version "5.15.14" + resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.15.14.tgz#168b154c4327fa4ccc1933a498331d53f61c0de2" + integrity sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw== + dependencies: + "@babel/runtime" "^7.23.9" + "@emotion/cache" "^11.11.0" + csstype "^3.1.3" + prop-types "^15.8.1" + "@mui/system@^5.11.5": version "5.11.5" resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.11.5.tgz#c880199634708c866063396f88d3fdd4c1dfcb48" @@ -2693,6 +2765,25 @@ csstype "^3.1.1" prop-types "^15.8.1" +"@mui/system@^5.15.14": + version "5.15.15" + resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.15.15.tgz#658771b200ce3c4a0f28e58169f02e5e718d1c53" + integrity sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ== + dependencies: + "@babel/runtime" "^7.23.9" + "@mui/private-theming" "^5.15.14" + "@mui/styled-engine" "^5.15.14" + "@mui/types" "^7.2.14" + "@mui/utils" "^5.15.14" + clsx "^2.1.0" + csstype "^3.1.3" + prop-types "^15.8.1" + +"@mui/types@^7.2.14": + version "7.2.14" + resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.14.tgz#8a02ac129b70f3d82f2f9b76ded2c8d48e3fc8c9" + integrity sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ== + "@mui/types@^7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.3.tgz#06faae1c0e2f3a31c86af6f28b3a4a42143670b9" @@ -2709,6 +2800,30 @@ prop-types "^15.8.1" react-is "^18.2.0" +"@mui/utils@^5.15.14": + version "5.15.14" + resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.15.14.tgz#e414d7efd5db00bfdc875273a40c0a89112ade3a" + integrity sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA== + dependencies: + "@babel/runtime" "^7.23.9" + "@types/prop-types" "^15.7.11" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/x-date-pickers@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-7.1.1.tgz#13523f3d1cc9df89def9a6f90b19ae2d8d5d13ea" + integrity sha512-doSaoNfYR4nAXSN2mz5MwktYUmPt37jZ8/t5QrPgFtEFc3KWZoBps0YEcno5qUynY1ISpOjvnVr18zqszzG+RA== + dependencies: + "@babel/runtime" "^7.24.0" + "@mui/base" "^5.0.0-beta.40" + "@mui/system" "^5.15.14" + "@mui/utils" "^5.15.14" + "@types/react-transition-group" "^4.4.10" + clsx "^2.1.0" + prop-types "^15.8.1" + react-transition-group "^4.4.5" + "@next/env@13.1.2": version "13.1.2" resolved "https://registry.yarnpkg.com/@next/env/-/env-13.1.2.tgz#4f13e3e9d44bb17fdc1d4543827459097035f10f" @@ -2799,6 +2914,13 @@ jsbi "^3.1.5" sha.js "^2.4.11" +"@noble/curves@1.2.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/ed25519@^1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.1.tgz#6899660f6fbb97798a6fbd227227c4589a454724" @@ -2809,6 +2931,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183" integrity sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA== +"@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== + "@noble/hashes@^1.1.2", "@noble/hashes@~1.1.1": version "1.1.5" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.5.tgz#1a0377f3b9020efe2fae03290bd2a12140c95c11" @@ -2819,6 +2946,11 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== +"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" + integrity sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA== + "@noble/secp256k1@1.6.3", "@noble/secp256k1@~1.6.0": version "1.6.3" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.6.3.tgz#7eed12d9f4404b416999d0c87686836c4c5c9b94" @@ -3065,6 +3197,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -3123,6 +3260,34 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728" integrity sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg== +"@safe-global/safe-apps-provider@^0.18.2": + version "0.18.2" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.2.tgz#336f3f4bb6ebbad9354e6551687491efc73991bc" + integrity sha512-yHHAcppwE7aIUWEeZiYAClQzZCdP5l0Kbd0CBlhKAsTcqZnx4Gh3G3G3frY5LlWcGzp9qmQ5jv+J1GBpaZLDgw== + dependencies: + "@safe-global/safe-apps-sdk" "^9.0.0" + events "^3.3.0" + +"@safe-global/safe-apps-react-sdk@^4.7.1": + version "4.7.1" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-react-sdk/-/safe-apps-react-sdk-4.7.1.tgz#3df442496edee9778ef7217ee78a031cac0e1701" + integrity sha512-sbVroIUxYy9XOzN7769wCTR2ytk6LMp8ECmFlc+d23/7EHOuX5Yw8Si+tf5SYbYhS9ygdSsMcVRQeNvKOhZnEw== + dependencies: + "@safe-global/safe-apps-sdk" "^9.0.0" + +"@safe-global/safe-apps-sdk@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.0.0.tgz#56635663f5a73773c5929d9c45ffea2b75dab69b" + integrity sha512-fEqmQBU3JqTjORSl3XYrcaxdxkUqeeM39qsQjqCzzTHioN8DEfg3JCLq6EBoXzcKTVOYi8SPzLV7KJccdDw+4w== + dependencies: + "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" + viem "^1.6.0" + +"@safe-global/safe-gateway-typescript-sdk@^3.5.3": + version "3.19.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.19.0.tgz#18637c205c83bfc0a6be5fddbf202d6bb4927302" + integrity sha512-TRlP05KY6t3wjLJ74FiirWlEt3xTclnUQM2YdYto1jx5G1o0meMnugIUZXhzm7Bs3rDEDNhz/aDf2KMSZtoCFg== + "@safe-global/safe-gateway-typescript-sdk@^3.5.6": version "3.7.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.7.0.tgz#2af52f1bc73759b1b6a549fed598781c8c5fce72" @@ -3135,6 +3300,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== +"@scure/base@~1.1.2": + version "1.1.6" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.6.tgz#8ce5d304b436e4c84f896e0550c83e4d88cb917d" + integrity sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g== + "@scure/bip32@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.1.0.tgz#dea45875e7fbc720c2b4560325f1cf5d2246d95b" @@ -3144,6 +3314,15 @@ "@noble/secp256k1" "~1.6.0" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" + integrity sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA== + dependencies: + "@noble/curves" "~1.2.0" + "@noble/hashes" "~1.3.2" + "@scure/base" "~1.1.2" + "@scure/bip39@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.0.tgz#92f11d095bae025f166bef3defcc5bf4945d419a" @@ -3152,6 +3331,14 @@ "@noble/hashes" "~1.1.1" "@scure/base" "~1.1.0" +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -3782,6 +3969,57 @@ dependencies: "@types/node" "*" +"@types/d3-array@^3.0.3": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-ease@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-interpolate@^3.0.1": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== + +"@types/d3-scale@^4.0.2": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-shape@^3.1.0": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" + integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time@*", "@types/d3-time@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" + integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== + +"@types/d3-timer@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + "@types/graceful-fs@^4.1.3": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" @@ -3887,6 +4125,11 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== +"@types/prop-types@^15.7.11": + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + "@types/react-dom@18.0.10", "@types/react-dom@^18.0.0": version "18.0.10" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.10.tgz#3b66dec56aa0f16a6cc26da9e9ca96c35c0b4352" @@ -3901,6 +4144,13 @@ dependencies: "@types/react" "*" +"@types/react-transition-group@^4.4.10": + version "4.4.10" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.10.tgz#6ee71127bdab1f18f11ad8fb3322c6da27c327ac" + integrity sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.4.5": version "4.4.5" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416" @@ -4605,6 +4855,11 @@ abab@^2.0.6: resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== +abitype@0.9.8: + version "0.9.8" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.8.tgz#1f120b6b717459deafd213dfbf3a3dd1bf10ae8c" + integrity sha512-puLifILdm+8sjyss4S+fsUN09obiT1g2YW6CtcQF+QDzxR0euzgEB29MZujC6zMk2a6SVmtttq1fc6+YFA7WYQ== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -5546,6 +5801,11 @@ clsx@^1.1.0, clsx@^1.2.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== +clsx@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" + integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -5803,6 +6063,87 @@ csstype@^3.0.2, csstype@^3.1.1: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +"d3-array@2 - 3", "d3-array@2.10.0 - 3", d3-array@^3.1.6: + version "3.2.4" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.4.tgz#15fec33b237f97ac5d7c986dc77da273a8ed0bb5" + integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg== + dependencies: + internmap "1 - 2" + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +d3-ease@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-format@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== + +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +d3-path@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" + integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== + +d3-scale@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" + integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== + dependencies: + d3-array "2.10.0 - 3" + d3-format "1 - 3" + d3-interpolate "1.2.0 - 3" + d3-time "2.1.1 - 3" + d3-time-format "2 - 4" + +d3-shape@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" + integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== + dependencies: + d3-path "^3.1.0" + +"d3-time-format@2 - 4": + version "4.1.0" + resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" + integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== + dependencies: + d3-time "1 - 3" + +"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" + integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== + dependencies: + d3-array "2 - 3" + +d3-timer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +d3-voronoi@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297" + integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg== + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -5830,6 +6171,11 @@ date-fns@^2.29.3: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== +dayjs@^1.11.10: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -5920,6 +6266,18 @@ define-properties@^1.1.3, define-properties@^1.1.4: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +delaunator@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-4.0.1.tgz#3d779687f57919a7a418f8ab947d3bddb6846957" + integrity sha512-WNPWi1IRKZfCt/qIDMfERkDp93+iZEmOxN2yy4Jg+Xhv8SLk2UTqqbe1sfiipn0and9QrE914/ihdx82Y/Giag== + +delaunay-find@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/delaunay-find/-/delaunay-find-0.0.6.tgz#2ed017a79410013717fa7d9422e082c2502d4ae3" + integrity sha512-1+almjfrnR7ZamBk0q3Nhg6lqSe6Le4vL0WJDSMx4IDbQwTpUTXPjxC00lqLBT8MYsJpPCbI16sIkw9cPsbi7Q== + dependencies: + delaunator "^4.0.0" + delay@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" @@ -6470,6 +6828,18 @@ eslint-plugin-react@^7.31.7: semver "^6.3.0" string.prototype.matchall "^4.0.8" +eslint-plugin-unused-imports@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.1.0.tgz#db015b569d3774e17a482388c95c17bd303bc602" + integrity sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw== + 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@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -6841,7 +7211,7 @@ ethers@5.5.4: "@ethersproject/web" "5.5.1" "@ethersproject/wordlists" "5.5.0" -ethers@5.7.2, ethers@^5.6.8, ethers@^5.7.2: +ethers@5.7.2, ethers@^5.7.2: version "5.7.2" resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== @@ -7655,6 +8025,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" +"internmap@1 - 2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" + integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== + interpret@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" @@ -7963,6 +8338,11 @@ isomorphic-ws@^4.0.1: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== +isows@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" + integrity sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg== + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" @@ -8765,7 +9145,7 @@ lodash.uniqby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" integrity sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww== -lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.16, lodash@^4.17.20, lodash@^4.17.4: +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.16, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -9787,6 +10167,11 @@ react-dom@^17.0.2: object-assign "^4.1.1" scheduler "^0.20.2" +react-fast-compare@^3.2.0: + 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-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" @@ -9950,6 +10335,11 @@ regenerator-runtime@^0.13.11: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regenerator-transform@^0.15.1: version "0.15.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" @@ -10732,6 +11122,11 @@ stylis@4.1.3: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== +stylis@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" + integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== + superstruct@^0.14.2: version "0.14.2" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b" @@ -11296,6 +11691,320 @@ varuint-bitcoin@^1.1.2: dependencies: safe-buffer "^5.1.1" +victory-area@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-area/-/victory-area-36.9.2.tgz#8dd79834cb182cbac0eb480d040dd6059e24bc43" + integrity sha512-32aharvPf2RgdQB+/u1j3/ajYFNH/7ugLX9ZRpdd65gP6QEbtXL+58gS6CxvFw6gr/y8a0xMlkMKkpDVacXLpw== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + victory-vendor "^36.9.2" + +victory-axis@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-axis/-/victory-axis-36.9.2.tgz#80137a900671e918d9296f0f12f8252b6094b09b" + integrity sha512-4Odws+IAjprJtBg2b2ZCxEPgrQ6LgIOa22cFkGghzOSfTyNayN4M3AauNB44RZyn2O/hDiM1gdBkEg1g9YDevQ== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-bar@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-bar/-/victory-bar-36.9.2.tgz#8ab0f67337394b71d8bd6ee1599bd260f3d63303" + integrity sha512-R3LFoR91FzwWcnyGK2P8DHNVv9gsaWhl5pSr2KdeNtvLbZVEIvUkTeVN9RMBMzterSFPw0mbWhS1Asb3sV6PPw== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + victory-vendor "^36.9.2" + +victory-box-plot@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-box-plot/-/victory-box-plot-36.9.2.tgz#504c0ceef303a7c56ce2877711d53df99915e9c4" + integrity sha512-nUD45V/YHDkAKZyak7YDsz+Vk1F9N0ica3jWQe0AY0JqD9DleHa8RY/olSVws26kLyEj1I+fQqva6GodcLaIqQ== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + victory-vendor "^36.9.2" + +victory-brush-container@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-brush-container/-/victory-brush-container-36.9.2.tgz#989c2b4787fb222f8354202c7ff0d0b3fa236e53" + integrity sha512-KcQjzFeo40tn52cJf1A02l5MqeR9GKkk3loDqM3T2hfi1PCyUrZXEUjGN5HNlLizDRvtcemaAHNAWlb70HbG/g== + dependencies: + lodash "^4.17.19" + react-fast-compare "^3.2.0" + victory-core "^36.9.2" + +victory-brush-line@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-brush-line/-/victory-brush-line-36.9.2.tgz#8fc446c77cb56d981e482f3a8119cc399ba1860b" + integrity sha512-/ncj8HEyl73fh8bhU4Iqe79DL62QP2rWWoogINxsGvndrhpFbL9tj7IPSEawi+riOh/CmohgI/ETu/V7QU9cJw== + dependencies: + lodash "^4.17.19" + react-fast-compare "^3.2.0" + victory-core "^36.9.2" + +victory-candlestick@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-candlestick/-/victory-candlestick-36.9.2.tgz#c2a4cc775c476f20d8853f8402f5df0c734ba9ff" + integrity sha512-hbStzF61GHkkflJWFgLTZSR8SOm8siJn65rwApLJBIA283yWOlyPjdr/kIQtO/h5QkIiXIuLb7RyiUAJEnH9WA== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-canvas@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-canvas/-/victory-canvas-36.9.2.tgz#5da579eeb47f9a8c14499c4c656137d12a6a9bd8" + integrity sha512-ImHJ7JQCpQ9aGCsh37EeVAmqJc7R0gl2CLM99gP9GfuJuZeoZ/GVfX6QFamfr19rYQOD2m9pVbecySBzdYI1zQ== + dependencies: + lodash "^4.17.19" + victory-bar "^36.9.2" + victory-core "^36.9.2" + +victory-chart@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-chart/-/victory-chart-36.9.2.tgz#ab09f566722d7337e55ebca45a6a82ed071fb277" + integrity sha512-dMNcS0BpqL3YiGvI4BSEmPR76FCksCgf3K4CSZ7C/MGyrElqB6wWwzk7afnlB1Qr71YIHXDmdwsPNAl/iEwTtA== + dependencies: + lodash "^4.17.19" + react-fast-compare "^3.2.0" + victory-axis "^36.9.2" + victory-core "^36.9.2" + victory-polar-axis "^36.9.2" + victory-shared-events "^36.9.2" + +victory-core@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-core/-/victory-core-36.9.2.tgz#bb82846e8f60b62f51e70b2658192c8434596d02" + integrity sha512-AzmMy+9MYMaaRmmZZovc/Po9urHne3R3oX7bbXeQdVuK/uMBrlPiv11gVJnuEH2SXLVyep43jlKgaBp8ef9stQ== + dependencies: + lodash "^4.17.21" + react-fast-compare "^3.2.0" + victory-vendor "^36.9.2" + +victory-create-container@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-create-container/-/victory-create-container-36.9.2.tgz#d913683cc2a9dda25f58c1f1336e0985f8288712" + integrity sha512-uA0dh1R0YDzuXyE/7StZvq4qshet+WYceY7R1UR5mR/F9079xy+iQsa2Ca4h97/GtVZoLO6r1eKLWBt9TN+U7A== + dependencies: + lodash "^4.17.19" + victory-brush-container "^36.9.2" + victory-core "^36.9.2" + victory-cursor-container "^36.9.2" + victory-selection-container "^36.9.2" + victory-voronoi-container "^36.9.2" + victory-zoom-container "^36.9.2" + +victory-cursor-container@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-cursor-container/-/victory-cursor-container-36.9.2.tgz#4f874c76c02c80a4f3d09ffa741076f905f8ed4f" + integrity sha512-jidab4j3MaciF3fGX70jTj4H9rrLcY8o2LUrhJ67ZLvEFGGmnPtph+p8Fe97Umrag7E/DszjNxQZolpwlgUh3g== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-errorbar@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-errorbar/-/victory-errorbar-36.9.2.tgz#8dca0ee735f1328809399cbdf625e66a4f6e8bcf" + integrity sha512-i/WPMN6/7F55FpEpN9WcwiWwaFJ+2ymfTgfBDLkUD3XJ52HGen4BxUt1ouwDA3FXz9kLa/h6Wbp/fnRhX70row== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-group@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-group/-/victory-group-36.9.2.tgz#4451b3cf9a4a9488271277c31d85022dfdb59397" + integrity sha512-wBmpsjBTKva8mxHvHNY3b8RE58KtnpLLItEyyAHaYkmExwt3Uj8Cld3sF3vmeuijn2iR64NPKeMbgMbfZJzycw== + dependencies: + lodash "^4.17.19" + react-fast-compare "^3.2.0" + victory-core "^36.9.2" + victory-shared-events "^36.9.2" + +victory-histogram@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-histogram/-/victory-histogram-36.9.2.tgz#e1314ca0c950c7a8950157a8254debaf4ecb4b04" + integrity sha512-w0ipFwWZ533qyqduRacr5cf+H4PGAUTdWNyGvZbWyu4+GtYYjGdoOolfUcO1ee8VJ1kZodpG8Z7ud6I/GWIzjQ== + dependencies: + lodash "^4.17.19" + react-fast-compare "^3.2.0" + victory-bar "^36.9.2" + victory-core "^36.9.2" + victory-vendor "^36.9.2" + +victory-legend@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-legend/-/victory-legend-36.9.2.tgz#2ca9e36b7be60bc4a64711f25524ac1290e75453" + integrity sha512-cucFJpv6fty+yXp5pElQFQnHBk1TqA4guGUMI+XF/wLlnuM4bhdAtASobRIIBkz0mHGBaCAAV4PzL9azPU/9dg== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-line@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-line/-/victory-line-36.9.2.tgz#02e3e1f404ac4b0a2cca4ae4684c20037e2a51a3" + integrity sha512-kmYFZUo0o2xC8cXRsmt/oUBRQSZJVT2IJnAkboUepypoj09e6CY5tRH4TSdfEDGkBk23xQkn7d4IFgl4kAGnSA== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + victory-vendor "^36.9.2" + +victory-pie@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-pie/-/victory-pie-36.9.2.tgz#2af3c12b9251de20f11a8325c821aede9cb5f8a5" + integrity sha512-i3zWezvy5wQEkhXKt4rS9ILGH7Vr9Q5eF9fKO4GMwDPBdYOTE3Dh2tVaSrfDC8g9zFIc0DKzOtVoJRTb+0AkPg== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + victory-vendor "^36.9.2" + +victory-polar-axis@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-polar-axis/-/victory-polar-axis-36.9.2.tgz#574a7deede92d227e20e9ad4938c57633f5e5ac3" + integrity sha512-HBR90FF4M56yf/atXjSmy3DMps1vSAaLXmdVXLM/A5g+0pUS7HO719r5x6dsR3I6Rm+8x6Kk8xJs0qgpnGQIEw== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-scatter@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-scatter/-/victory-scatter-36.9.2.tgz#f07dced7660f90e2a898053431462d3c6372149f" + integrity sha512-hK9AtbJQfaW05i8BH7Lf1HK7vWMAfQofj23039HEQJqTKbCL77YT+Q0LhZw1a1BRCpC/5aSg9EuqblhfIYw2wg== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-selection-container@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-selection-container/-/victory-selection-container-36.9.2.tgz#bff359d27d50b04a473eacdb8e8c66488afd20a4" + integrity sha512-chboroEwqqVlMB60kveXM2WznJ33ZM00PWkFVCoJDzHHlYs7TCADxzhqet2S67SbZGSyvSprY2YztSxX8kZ+XQ== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-shared-events@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-shared-events/-/victory-shared-events-36.9.2.tgz#cf0cf2220ee1eb90baa16e202873b20254ab9cde" + integrity sha512-W/atiw3Or6MnpBuhluFv6007YrixIRh5NtiRvtFLGxNuQJLYjaSh6koRAih5xJer5Pj7YUx0tL9x67jTRcJ6Dg== + dependencies: + json-stringify-safe "^5.0.1" + lodash "^4.17.19" + react-fast-compare "^3.2.0" + victory-core "^36.9.2" + +victory-stack@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-stack/-/victory-stack-36.9.2.tgz#25cd48ed66b4c9163993e6ac8d770dca791e2074" + integrity sha512-imR6FniVlDFlBa/B3Est8kTryNhWj2ZNpivmVOebVDxkKcVlLaDg3LotCUOI7NzOhBQaro0UzeE9KmZV93JcYA== + dependencies: + lodash "^4.17.19" + react-fast-compare "^3.2.0" + victory-core "^36.9.2" + victory-shared-events "^36.9.2" + +victory-tooltip@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-tooltip/-/victory-tooltip-36.9.2.tgz#8e240a03f80909e80a9501419bec01bc13700919" + integrity sha512-76seo4TWD1WfZHJQH87IP3tlawv38DuwrUxpnTn8+uW6/CUex82poQiVevYdmJzhataS9jjyCWv3w7pOmLBCLg== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-vendor@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-vendor/-/victory-vendor-36.9.2.tgz#668b02a448fa4ea0f788dbf4228b7e64669ff801" + integrity sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ== + dependencies: + "@types/d3-array" "^3.0.3" + "@types/d3-ease" "^3.0.0" + "@types/d3-interpolate" "^3.0.1" + "@types/d3-scale" "^4.0.2" + "@types/d3-shape" "^3.1.0" + "@types/d3-time" "^3.0.0" + "@types/d3-timer" "^3.0.0" + d3-array "^3.1.6" + d3-ease "^3.0.1" + d3-interpolate "^3.0.1" + d3-scale "^4.0.2" + d3-shape "^3.1.0" + d3-time "^3.0.0" + d3-timer "^3.0.1" + +victory-voronoi-container@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-voronoi-container/-/victory-voronoi-container-36.9.2.tgz#1b3ce4dd43ceb371f6caba6b0724ca563de87b96" + integrity sha512-NIVYqck9N4OQnEz9mgQ4wILsci3OBWWK7RLuITGHyoD7Ne/+WH1i0Pv2y9eIx+f55rc928FUTugPPhkHvXyH3A== + dependencies: + delaunay-find "0.0.6" + lodash "^4.17.19" + react-fast-compare "^3.2.0" + victory-core "^36.9.2" + victory-tooltip "^36.9.2" + +victory-voronoi@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-voronoi/-/victory-voronoi-36.9.2.tgz#b58883b2a14c8ad0e4c131a1d02c6e428ecae612" + integrity sha512-50fq0UBTAFxxU+nabOIPE5P2v/2oAbGAX+Ckz6lu8LFwwig4J1DSz0/vQudqDGjzv3JNEdqTD4FIpyjbxLcxiA== + dependencies: + d3-voronoi "^1.1.4" + lodash "^4.17.19" + victory-core "^36.9.2" + +victory-zoom-container@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory-zoom-container/-/victory-zoom-container-36.9.2.tgz#2899c8fa06d772864128b44130d48744298b76d0" + integrity sha512-pXa2Ji6EX/pIarKT6Hcmmu2n7IG/x8Vs0D2eACQ/nbpvZa+DXWIxCRW4hcg2Va35fmXcDIEpGaX3/soXzZ+pbw== + dependencies: + lodash "^4.17.19" + victory-core "^36.9.2" + +victory@^36.9.2: + version "36.9.2" + resolved "https://registry.yarnpkg.com/victory/-/victory-36.9.2.tgz#0f4ceec0c732bf166f9a3997cbf6c6df54bf1476" + integrity sha512-kgVgiSno4KpD0HxmUo5GzqWI4P/eILLOM6AmJfAlagCnOzrtYGsAw+N1YxOcYvTiKsh/zmWawxHlpw3TMenFDQ== + dependencies: + victory-area "^36.9.2" + victory-axis "^36.9.2" + victory-bar "^36.9.2" + victory-box-plot "^36.9.2" + victory-brush-container "^36.9.2" + victory-brush-line "^36.9.2" + victory-candlestick "^36.9.2" + victory-canvas "^36.9.2" + victory-chart "^36.9.2" + victory-core "^36.9.2" + victory-create-container "^36.9.2" + victory-cursor-container "^36.9.2" + victory-errorbar "^36.9.2" + victory-group "^36.9.2" + victory-histogram "^36.9.2" + victory-legend "^36.9.2" + victory-line "^36.9.2" + victory-pie "^36.9.2" + victory-polar-axis "^36.9.2" + victory-scatter "^36.9.2" + victory-selection-container "^36.9.2" + victory-shared-events "^36.9.2" + victory-stack "^36.9.2" + victory-tooltip "^36.9.2" + victory-voronoi "^36.9.2" + victory-voronoi-container "^36.9.2" + victory-zoom-container "^36.9.2" + +viem@^1.6.0: + version "1.21.4" + resolved "https://registry.yarnpkg.com/viem/-/viem-1.21.4.tgz#883760e9222540a5a7e0339809202b45fe6a842d" + integrity sha512-BNVYdSaUjeS2zKQgPs+49e5JKocfo60Ib2yiXOWBT6LuVxY1I/6fFX3waEtpXvL1Xn4qu+BVitVtMh9lyThyhQ== + dependencies: + "@adraffy/ens-normalize" "1.10.0" + "@noble/curves" "1.2.0" + "@noble/hashes" "1.3.2" + "@scure/bip32" "1.3.2" + "@scure/bip39" "1.2.1" + abitype "0.9.8" + isows "1.0.3" + ws "8.13.0" + w3c-xmlserializer@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" @@ -11488,6 +12197,11 @@ ws@7.5.9, ws@^7.2.0, ws@^7.4.5, ws@^7.4.6, ws@^7.5.1: resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@8.13.0: + version "8.13.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" + integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== + ws@^8.11.0, ws@^8.5.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8"