From 05f9db676e56950c9a3e84a4770f04a258a73ca2 Mon Sep 17 00:00:00 2001 From: chefjackson <116779127+chefjackson@users.noreply.github.com> Date: Mon, 30 Oct 2023 19:07:22 +0800 Subject: [PATCH] feat: Position Managers (#7113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 🤖 Generated by Copilot at 195239b ### Summary 🚀📦🛠️ This pull request adds a new feature to the web app that allows users to manage their liquidity positions across different chains. It introduces a new `@pancakeswap/position-managers` package that contains the logic and types for the feature, and a new page and view component in the web app that display the feature. It also updates the dependencies and configuration files for the web app and the position managers package. > _`position-managers`_ > _New feature for web app users_ > _Autumn of liquidity_ ### Walkthrough * Add the `@pancakeswap/position-managers` package as a new workspace in the monorepo and declare its metadata, dependencies, constants, types, and exports ([link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-93d327b5200a18ff6a4a34a9ccbe4ff89f8937b260e131e964486e2e49e06d66R1), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-1ed2b1245149f57164abbf7db733ca5d2836dabbcb4b2d5decc2e1c6f1b0c9d5R1-R31), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-14c6ec0f991698b50606b47d892d8a319f7880fa5e1ac89e986a0a62c1cc901aR1), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-e349e2e4d8f12a0b7cf06dddf1cf8a7d1ef03071789521e8fe58cb8c5ef1b61eR1-R5), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-158cb35322573e79423b8ca53dbf6c731d80a20b3a2d525f50ac0b56604830e5R1), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-85043a95f7c7606aebdc80534f55556896d547da5aab96b14c3be75f442d7d98R1-R12), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-a9cb7c9b5e8959aea1c4c2cdb5b5c3e6d53f118594e2ec3812210db9f6d6e0baR1-R9)) * Add the `@pancakeswap/position-managers` package as a dependency of the web app and configure the `next.config.mjs` file to enable the position managers feature ([link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-197cd8ca285a4abd2f21479e0bf6e36e90b08528fcd7f3bdbe8d1221897e377dR59), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-197cd8ca285a4abd2f21479e0bf6e36e90b08528fcd7f3bdbe8d1221897e377dL188-R189), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-14b60f636e1a2b0061da57aaf231cb1ed15a5dc0c592425ed82e58fec95d42d8R42)) * Create a new page and a new view component for the position managers feature in the web app and export them from their source directories ([link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-aa62fe482d9d1378569330e920bc4fae0cb19590945938bb0596e7ea0f49d6a1R1-R9), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-5cc22ea8dc37e7cf4b13720d5da0421ab17ae24e38c11f6d6507b957eaee56a6R1-R3)) * Increase the concurrency limit for the `dev` script in the root `package.json` file to accommodate the new position managers package ([link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519L14-R14)) * Update the `pnpm-lock.yaml` file with the new dependency information for the position managers package, the react-countup package, and other updated packages, and remove unnecessary dependencies from some packages ([link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR580-R582), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL711-R714), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR1191-R1233), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL1207-R1253), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL1972-R2021), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR2027), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL1994-R2044), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR2086), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL2125-R2173), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL2141-R2189), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL2196-R2244), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL2250-R2298), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL2293-R2341), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL2304-R2352), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR4246-R4262), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL5333-R5398), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL5775-R5840), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL6497-R6562), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL7526-R7610), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL7751-R7835), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL8713-R8797), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10237-R10321), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10265-R10349), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10303-R10387), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10323-R10407), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10358-R10442), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10378-R10462), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10407-R10491), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10428-R10512), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10782-R10868), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL10802-R10888), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL12183-R12267), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL12191-R12275), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR14810), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL14738), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL14927-R15011), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL15583-R15667), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL16108-R16192), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL16180-R16264), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL17071-R17155), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL17552-R17636), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL17562-R17646), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL17589-R17673), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL17599-R17683), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL18223-R18307), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL19030-R19114), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL19061-R19145), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL20352-R20436), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL21322-R21406), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL21892-R21976), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL21971-R22055), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL22162-R22261), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL23505-R23604), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL24100-R24199), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL24189), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL24389-R24511), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL24750-R24872), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL24960-R25082), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL25431-R25553), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL25454-R25576), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL25476-R25598), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL25498-R25620), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL25521-R25643), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL25541-R25663), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL25676-R25798), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbL25740-R25862), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR26069-R26107), [link](https://github.com/pancakeswap/pancake-frontend/pull/7113/files?diff=unified&w=0#diff-32824c984905bb02bc7ffcef96a77addd1f1602cff71a11fbbfdd7f53ee026bbR26146)) --------- Co-authored-by: chefmomota Co-authored-by: Chef Yogi <99634186+Chef-Yogi@users.noreply.github.com> --- apps/web/next.config.mjs | 29 +- apps/web/package.json | 1 + .../position-manager-bunny.png | Bin 0 -> 26357 bytes .../src/components/CurrencyInput/index.tsx | 62 +++ apps/web/src/components/Menu/config/config.ts | 7 + apps/web/src/hooks/useContract.ts | 22 +- apps/web/src/hooks/usePositionPrices.ts | 80 +++ .../pages/position-managers/[[...slug]].tsx | 32 ++ apps/web/src/utils/contractHelpers.ts | 21 +- .../components/AddLiquidity.tsx | 459 ++++++++++++++++++ .../PositionManagers/components/AprButton.tsx | 113 +++++ .../components/CardLayout.tsx | 15 + .../components/CardSection.tsx | 30 ++ .../PositionManagers/components/CardTitle.tsx | 68 +++ .../components/ControlsContainer.tsx | 37 ++ .../components/DYORWarning.tsx | 52 ++ .../components/DuoTokenVaultCard.tsx | 230 +++++++++ .../components/ExpandableSection.tsx | 24 + .../PositionManagers/components/Filters.tsx | 85 ++++ .../PositionManagers/components/Header.tsx | 32 ++ .../PositionManagers/components/InnerCard.tsx | 22 + .../components/LiquidityManagement.tsx | 208 ++++++++ .../components/LiveSwitch.tsx | 44 ++ .../components/ManagerInfo.tsx | 47 ++ .../components/PercentSlider.tsx | 47 ++ .../components/RemoveLiquidity.tsx | 193 ++++++++ .../components/RewardAssets.tsx | 94 ++++ .../components/SingleTokenWarning.tsx | 31 ++ .../components/StakedAssets.tsx | 113 +++++ .../components/StyledModal.tsx | 8 + .../PositionManagers/components/Tags.tsx | 26 + .../PositionManagers/components/Toggles.tsx | 27 ++ .../components/TokenPairLogos.tsx | 53 ++ .../PositionManagers/components/VaultInfo.tsx | 98 ++++ .../components/VaultLinks.tsx | 83 ++++ .../components/ViewSwitch.tsx | 10 + .../PositionManagers/components/YieldInfo.tsx | 65 +++ .../PositionManagers/components/index.ts | 21 + .../PositionManagers/containers/Controls.tsx | 45 ++ .../containers/PCSVaultCard.tsx | 195 ++++++++ .../containers/VaultCards.tsx | 82 ++++ .../PositionManagers/containers/index.ts | 2 + .../src/views/PositionManagers/hooks/index.ts | 14 + .../PositionManagers/hooks/useAdapterInfo.ts | 245 ++++++++++ .../views/PositionManagers/hooks/useApr.ts | 101 ++++ .../hooks/useEarningTokenPriceInfo.ts | 19 + .../PositionManagers/hooks/useFetchApr.ts | 62 +++ .../PositionManagers/hooks/useFilters.ts | 35 ++ .../hooks/usePositionManagerDetailsData.ts | 24 + .../hooks/usePositionManagerStatus.ts | 42 ++ .../PositionManagers/hooks/useToggles.ts | 23 + .../hooks/useTokenPriceFromSubgraph.ts | 42 ++ .../hooks/useTotalAssetInUsd.ts | 18 + .../hooks/useTotalStakedInUsd.ts | 32 ++ .../views/PositionManagers/hooks/useVault.ts | 29 ++ .../PositionManagers/hooks/useVaultConfigs.ts | 10 + .../PositionManagers/hooks/useViewMode.ts | 32 ++ apps/web/src/views/PositionManagers/index.tsx | 16 + .../utils/getReadableManagerFeeType.ts | 11 + .../PositionManagers/utils/getVaultName.ts | 3 + .../src/views/PositionManagers/utils/index.ts | 3 + .../views/PositionManagers/utils/strategy.ts | 13 + package.json | 2 +- .../hooks/src/useDebouncedChangeHandler.ts | 4 +- .../localization/src/config/translations.json | 40 +- packages/position-managers/index.ts | 1 + packages/position-managers/package.json | 28 ++ packages/position-managers/src/abi/index.ts | 2 + .../src/abi/positionManagerAdapter.ts | 243 ++++++++++ .../src/abi/positionManagerWrapper.ts | 350 +++++++++++++ .../src/constants/endpoints.ts | 1 + .../position-managers/src/constants/index.ts | 4 + .../src/constants/managers.ts | 24 + .../src/constants/supportedChains.ts | 5 + .../src/constants/vaults/1.ts | 3 + .../src/constants/vaults/56.ts | 120 +++++ .../src/constants/vaults/index.ts | 20 + packages/position-managers/src/index.ts | 4 + .../position-managers/src/managers/index.ts | 7 + .../position-managers/src/managers/pcs.ts | 41 ++ packages/position-managers/src/types.ts | 134 +++++ packages/position-managers/tsconfig.json | 12 + packages/position-managers/vitest.config.ts | 9 + .../components/Button/ExpandableButton.tsx | 16 +- packages/utils/clientRouter.ts | 23 + packages/utils/formatTimestamp.ts | 53 ++ packages/utils/getTimePeriods.ts | 2 +- packages/utils/package.json | 3 + pnpm-lock.yaml | 108 +++-- 89 files changed, 4883 insertions(+), 63 deletions(-) create mode 100644 apps/web/public/images/position-manager/position-manager-bunny.png create mode 100644 apps/web/src/components/CurrencyInput/index.tsx create mode 100644 apps/web/src/hooks/usePositionPrices.ts create mode 100644 apps/web/src/pages/position-managers/[[...slug]].tsx create mode 100644 apps/web/src/views/PositionManagers/components/AddLiquidity.tsx create mode 100644 apps/web/src/views/PositionManagers/components/AprButton.tsx create mode 100644 apps/web/src/views/PositionManagers/components/CardLayout.tsx create mode 100644 apps/web/src/views/PositionManagers/components/CardSection.tsx create mode 100644 apps/web/src/views/PositionManagers/components/CardTitle.tsx create mode 100644 apps/web/src/views/PositionManagers/components/ControlsContainer.tsx create mode 100644 apps/web/src/views/PositionManagers/components/DYORWarning.tsx create mode 100644 apps/web/src/views/PositionManagers/components/DuoTokenVaultCard.tsx create mode 100644 apps/web/src/views/PositionManagers/components/ExpandableSection.tsx create mode 100644 apps/web/src/views/PositionManagers/components/Filters.tsx create mode 100644 apps/web/src/views/PositionManagers/components/Header.tsx create mode 100644 apps/web/src/views/PositionManagers/components/InnerCard.tsx create mode 100644 apps/web/src/views/PositionManagers/components/LiquidityManagement.tsx create mode 100644 apps/web/src/views/PositionManagers/components/LiveSwitch.tsx create mode 100644 apps/web/src/views/PositionManagers/components/ManagerInfo.tsx create mode 100644 apps/web/src/views/PositionManagers/components/PercentSlider.tsx create mode 100644 apps/web/src/views/PositionManagers/components/RemoveLiquidity.tsx create mode 100644 apps/web/src/views/PositionManagers/components/RewardAssets.tsx create mode 100644 apps/web/src/views/PositionManagers/components/SingleTokenWarning.tsx create mode 100644 apps/web/src/views/PositionManagers/components/StakedAssets.tsx create mode 100644 apps/web/src/views/PositionManagers/components/StyledModal.tsx create mode 100644 apps/web/src/views/PositionManagers/components/Tags.tsx create mode 100644 apps/web/src/views/PositionManagers/components/Toggles.tsx create mode 100644 apps/web/src/views/PositionManagers/components/TokenPairLogos.tsx create mode 100644 apps/web/src/views/PositionManagers/components/VaultInfo.tsx create mode 100644 apps/web/src/views/PositionManagers/components/VaultLinks.tsx create mode 100644 apps/web/src/views/PositionManagers/components/ViewSwitch.tsx create mode 100644 apps/web/src/views/PositionManagers/components/YieldInfo.tsx create mode 100644 apps/web/src/views/PositionManagers/components/index.ts create mode 100644 apps/web/src/views/PositionManagers/containers/Controls.tsx create mode 100644 apps/web/src/views/PositionManagers/containers/PCSVaultCard.tsx create mode 100644 apps/web/src/views/PositionManagers/containers/VaultCards.tsx create mode 100644 apps/web/src/views/PositionManagers/containers/index.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/index.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useAdapterInfo.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useApr.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useEarningTokenPriceInfo.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useFetchApr.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useFilters.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/usePositionManagerDetailsData.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/usePositionManagerStatus.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useToggles.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useTokenPriceFromSubgraph.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useTotalAssetInUsd.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useTotalStakedInUsd.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useVault.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useVaultConfigs.ts create mode 100644 apps/web/src/views/PositionManagers/hooks/useViewMode.ts create mode 100644 apps/web/src/views/PositionManagers/index.tsx create mode 100644 apps/web/src/views/PositionManagers/utils/getReadableManagerFeeType.ts create mode 100644 apps/web/src/views/PositionManagers/utils/getVaultName.ts create mode 100644 apps/web/src/views/PositionManagers/utils/index.ts create mode 100644 apps/web/src/views/PositionManagers/utils/strategy.ts create mode 100644 packages/position-managers/index.ts create mode 100644 packages/position-managers/package.json create mode 100644 packages/position-managers/src/abi/index.ts create mode 100644 packages/position-managers/src/abi/positionManagerAdapter.ts create mode 100644 packages/position-managers/src/abi/positionManagerWrapper.ts create mode 100644 packages/position-managers/src/constants/endpoints.ts create mode 100644 packages/position-managers/src/constants/index.ts create mode 100644 packages/position-managers/src/constants/managers.ts create mode 100644 packages/position-managers/src/constants/supportedChains.ts create mode 100644 packages/position-managers/src/constants/vaults/1.ts create mode 100644 packages/position-managers/src/constants/vaults/56.ts create mode 100644 packages/position-managers/src/constants/vaults/index.ts create mode 100644 packages/position-managers/src/index.ts create mode 100644 packages/position-managers/src/managers/index.ts create mode 100644 packages/position-managers/src/managers/pcs.ts create mode 100644 packages/position-managers/src/types.ts create mode 100644 packages/position-managers/tsconfig.json create mode 100644 packages/position-managers/vitest.config.ts create mode 100644 packages/utils/clientRouter.ts create mode 100644 packages/utils/formatTimestamp.ts diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index 981b7c05fa367..a55dd1bb9da68 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -19,21 +19,21 @@ const withVanillaExtract = createVanillaExtractPlugin() const sentryWebpackPluginOptions = process.env.VERCEL_ENV === 'production' ? { - // Additional config options for the Sentry Webpack plugin. Keep in mind that - // the following options are set automatically, and overriding them is not - // recommended: - // release, url, org, project, authToken, configFile, stripPrefix, - // urlPrefix, include, ignore - silent: false, // Logging when deploying to check if there is any problem - validate: true, - hideSourceMaps: false, - // https://github.com/getsentry/sentry-webpack-plugin#options. - } + // Additional config options for the Sentry Webpack plugin. Keep in mind that + // the following options are set automatically, and overriding them is not + // recommended: + // release, url, org, project, authToken, configFile, stripPrefix, + // urlPrefix, include, ignore + silent: false, // Logging when deploying to check if there is any problem + validate: true, + hideSourceMaps: false, + // https://github.com/getsentry/sentry-webpack-plugin#options. + } : { - hideSourceMaps: false, - silent: true, // Suppresses all logs - dryRun: !process.env.SENTRY_AUTH_TOKEN, - } + hideSourceMaps: false, + silent: true, // Suppresses all logs + dryRun: !process.env.SENTRY_AUTH_TOKEN, + } const workerDeps = Object.keys(smartRouterPkgs.dependencies) .map((d) => d.replace('@pancakeswap/', 'packages/')) @@ -56,6 +56,7 @@ const config = { }, transpilePackages: [ '@pancakeswap/farms', + '@pancakeswap/position-managers', '@pancakeswap/localization', '@pancakeswap/hooks', '@pancakeswap/utils', diff --git a/apps/web/package.json b/apps/web/package.json index 01c5269a89906..1089a58dddc3d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -41,6 +41,7 @@ "@pancakeswap/localization": "workspace:*", "@pancakeswap/multicall": "workspace:*", "@pancakeswap/pools": "workspace:*", + "@pancakeswap/position-managers": "workspace:*", "@pancakeswap/sdk": "workspace:*", "@pancakeswap/smart-router": "workspace:*", "@pancakeswap/swap-sdk-core": "workspace:*", diff --git a/apps/web/public/images/position-manager/position-manager-bunny.png b/apps/web/public/images/position-manager/position-manager-bunny.png new file mode 100644 index 0000000000000000000000000000000000000000..29c513e5a8b0cd9fe6b7aeb8ffa3d2ec70ea1602 GIT binary patch literal 26357 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 AHvj+t literal 0 HcmV?d00001 diff --git a/apps/web/src/components/CurrencyInput/index.tsx b/apps/web/src/components/CurrencyInput/index.tsx new file mode 100644 index 0000000000000..631dfc44eccc5 --- /dev/null +++ b/apps/web/src/components/CurrencyInput/index.tsx @@ -0,0 +1,62 @@ +import { useMemo, useCallback, ReactNode, MouseEvent } from 'react' +import { Currency, CurrencyAmount } from '@pancakeswap/sdk' +import { CurrencyLogo } from '@pancakeswap/widgets-internal' +import { BalanceInput, Text, Flex, Button } from '@pancakeswap/uikit' + +interface Props { + value: string | number + onChange: (val: string) => void + currency?: Currency + balance?: CurrencyAmount + balanceText?: ReactNode + maxText?: ReactNode +} + +export function CurrencyInput({ currency, balance, value, onChange, balanceText, maxText = 'Max', ...rest }: Props) { + const isMax = useMemo(() => balance && value && balance.toExact() === value, [balance, value]) + const onMaxClick = useCallback( + (e: MouseEvent) => { + e.stopPropagation() + e.preventDefault() + onChange?.(balance?.toExact() || '') + }, + [onChange, balance], + ) + + const currencyDisplay = currency ? ( + + + + {currency.symbol} + + + ) : null + + const balanceDisplay = balance ? ( + + + {balanceText} + + + + ) : null + + return ( + + ) +} diff --git a/apps/web/src/components/Menu/config/config.ts b/apps/web/src/components/Menu/config/config.ts index 56301655d15be..675d30d467b06 100644 --- a/apps/web/src/components/Menu/config/config.ts +++ b/apps/web/src/components/Menu/config/config.ts @@ -1,5 +1,6 @@ import { ContextApi } from '@pancakeswap/localization' import { SUPPORTED_CHAIN_IDS as POOL_SUPPORTED_CHAINS } from '@pancakeswap/pools' +import { SUPPORTED_CHAIN_IDS as POSITION_MANAGERS_SUPPORTED_CHAINS } from '@pancakeswap/position-managers' import { NftIcon, NftFillIcon, @@ -114,6 +115,12 @@ const config: ( href: '/pools', supportChainIds: POOL_SUPPORTED_CHAINS, }, + { + label: t('Position Manager'), + href: '/position-managers', + supportChainIds: POSITION_MANAGERS_SUPPORTED_CHAINS, + status: { text: t('New'), color: 'success' }, + }, { label: t('Liquid Staking'), href: '/liquid-staking', diff --git a/apps/web/src/hooks/useContract.ts b/apps/web/src/hooks/useContract.ts index e4936149740d5..057a8beca4184 100644 --- a/apps/web/src/hooks/useContract.ts +++ b/apps/web/src/hooks/useContract.ts @@ -12,6 +12,8 @@ import { getBCakeFarmBoosterContract, getBCakeFarmBoosterProxyFactoryContract, getBCakeFarmBoosterV3Contract, + getPositionManagerWrapperContract, + getPositionManagerAdapterContract, getBCakeProxyContract, getBunnyFactoryContract, getCakeFlexibleSideVaultV2Contract, @@ -313,6 +315,24 @@ export function useBCakeFarmBoosterV3Contract() { return useMemo(() => getBCakeFarmBoosterV3Contract(signer ?? undefined, chainId), [signer, chainId]) } +export function usePositionManagerWrapperContract(address: Address) { + const { chainId } = useActiveChainId() + const { data: signer } = useWalletClient() + return useMemo( + () => getPositionManagerWrapperContract(address, signer ?? undefined, chainId), + [signer, chainId, address], + ) +} + +export function usePositionManagerAdepterContract(address: Address) { + const { chainId } = useActiveChainId() + const { data: signer } = useWalletClient() + return useMemo( + () => getPositionManagerAdapterContract(address, signer ?? undefined, chainId), + [signer, chainId, address], + ) +} + export function useBCakeFarmBoosterProxyFactoryContract() { const { data: signer } = useWalletClient() return useMemo(() => getBCakeFarmBoosterProxyFactoryContract(signer ?? undefined), [signer]) @@ -433,5 +453,5 @@ export const useFixedStakingContract = () => { const { data: signer } = useWalletClient() - return useMemo(() => getFixedStakingContract(signer, chainId), [chainId, signer]) + return useMemo(() => getFixedStakingContract(signer ?? undefined, chainId), [chainId, signer]) } diff --git a/apps/web/src/hooks/usePositionPrices.ts b/apps/web/src/hooks/usePositionPrices.ts new file mode 100644 index 0000000000000..29d6aeaa3a2d6 --- /dev/null +++ b/apps/web/src/hooks/usePositionPrices.ts @@ -0,0 +1,80 @@ +import { Currency } from '@pancakeswap/sdk' +import { tickToPrice } from '@pancakeswap/v3-sdk' +import { useCallback, useMemo, useState } from 'react' + +interface PositionInfo { + currencyA?: Currency + currencyB?: Currency + tickLower?: number + tickUpper?: number + tickCurrent?: number +} + +export function usePositionPrices({ + currencyA: initialBaseCurrency, + currencyB: initialQuoteCurrency, + tickLower, + tickUpper, + tickCurrent, +}: PositionInfo) { + const [invert, setInvert] = useState(false) + const toggleInvert = useCallback(() => setInvert(!invert), [invert]) + const currencyA = useMemo( + () => (invert ? initialQuoteCurrency : initialBaseCurrency), + [invert, initialBaseCurrency, initialQuoteCurrency], + ) + const currencyB = useMemo( + () => (invert ? initialBaseCurrency : initialQuoteCurrency), + [invert, initialBaseCurrency, initialQuoteCurrency], + ) + + const sorted = useMemo( + () => + Boolean( + currencyA?.wrapped && + currencyB?.wrapped && + !currencyA.wrapped.equals(currencyB.wrapped) && + currencyA.wrapped.sortsBefore(currencyB.wrapped), + ), + [currencyA, currencyB], + ) + + const tickLowerPrice = useMemo( + () => + currencyA?.wrapped && + currencyB?.wrapped && + typeof tickLower === 'number' && + tickToPrice(currencyA.wrapped, currencyB.wrapped, tickLower), + [tickLower, currencyA, currencyB], + ) + const tickUpperPrice = useMemo( + () => + currencyA?.wrapped && + currencyB?.wrapped && + typeof tickUpper === 'number' && + tickToPrice(currencyA.wrapped, currencyB.wrapped, tickUpper), + [tickUpper, currencyA, currencyB], + ) + const [priceLower, priceUpper] = useMemo( + () => (sorted ? [tickLowerPrice, tickUpperPrice] : [tickUpperPrice, tickLowerPrice]), + [sorted, tickLowerPrice, tickUpperPrice], + ) + const priceCurrent = useMemo( + () => + currencyA?.wrapped && + currencyB?.wrapped && + typeof tickCurrent === 'number' && + tickToPrice(currencyA.wrapped, currencyB.wrapped, tickCurrent), + [tickCurrent, currencyA, currencyB], + ) + + return { + currencyA, + currencyB, + priceLower, + priceUpper, + priceCurrent, + invert: toggleInvert, + inverted: invert, + } +} diff --git a/apps/web/src/pages/position-managers/[[...slug]].tsx b/apps/web/src/pages/position-managers/[[...slug]].tsx new file mode 100644 index 0000000000000..13bcedb56d1ae --- /dev/null +++ b/apps/web/src/pages/position-managers/[[...slug]].tsx @@ -0,0 +1,32 @@ +import { SUPPORTED_CHAIN_IDS } from '@pancakeswap/position-managers' +import type { GetStaticPaths, GetStaticProps } from 'next' + +import { PositionManagers } from 'views/PositionManagers' + +const Page = () => + +Page.chains = SUPPORTED_CHAIN_IDS + +export const getStaticProps: GetStaticProps = async () => { + return { props: {} } +} + +export const getStaticPaths: GetStaticPaths = async () => { + return { + paths: [ + { + params: { + slug: [], + }, + }, + { + params: { + slug: ['history'], + }, + }, + ], + fallback: false, + } +} + +export default Page diff --git a/apps/web/src/utils/contractHelpers.ts b/apps/web/src/utils/contractHelpers.ts index c43e260cded90..6034a73648dde 100644 --- a/apps/web/src/utils/contractHelpers.ts +++ b/apps/web/src/utils/contractHelpers.ts @@ -61,6 +61,7 @@ import { affiliateProgramABI } from 'config/abi/affiliateProgram' import { bCakeFarmBoosterABI } from 'config/abi/bCakeFarmBooster' import { bCakeFarmBoosterProxyFactoryABI } from 'config/abi/bCakeFarmBoosterProxyFactory' import { bCakeFarmBoosterV3ABI } from 'config/abi/bCakeFarmBoosterV3' +import { positionManagerAdapterABI, positionManagerWrapperABI } from '@pancakeswap/position-managers' import { bCakeProxyABI } from 'config/abi/bCakeProxy' import { bunnyFactoryABI } from 'config/abi/bunnyFactory' import { chainlinkOracleABI } from 'config/abi/chainlinkOracle' @@ -247,6 +248,24 @@ export const getBCakeFarmBoosterV3Contract = (signer?: WalletClient, chainId?: n return getContract({ abi: bCakeFarmBoosterV3ABI, address: getBCakeFarmBoosterV3Address(chainId), signer, chainId }) } +export const getPositionManagerWrapperContract = (address: `0x${string}`, signer?: WalletClient, chainId?: number) => { + return getContract({ + abi: positionManagerWrapperABI, + address, + signer, + chainId, + }) +} + +export const getPositionManagerAdapterContract = (address: `0x${string}`, signer?: WalletClient, chainId?: number) => { + return getContract({ + abi: positionManagerAdapterABI, + address, + signer, + chainId, + }) +} + export const getBCakeFarmBoosterProxyFactoryContract = (signer?: WalletClient) => { return getContract({ abi: bCakeFarmBoosterProxyFactoryABI, @@ -348,7 +367,7 @@ export const getMasterChefV3Contract = (signer?: WalletClient, chainId?: number) return mcv3Address ? getContract({ abi: masterChefV3ABI, - address: getMasterChefV3Address(chainId), + address: mcv3Address, chainId, signer, }) diff --git a/apps/web/src/views/PositionManagers/components/AddLiquidity.tsx b/apps/web/src/views/PositionManagers/components/AddLiquidity.tsx new file mode 100644 index 0000000000000..4192c7bda31e7 --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/AddLiquidity.tsx @@ -0,0 +1,459 @@ +import { useTranslation } from '@pancakeswap/localization' +import { MANAGER } from '@pancakeswap/position-managers' +import { Currency, CurrencyAmount, Percent } from '@pancakeswap/sdk' +import { Button, Flex, LinkExternal, ModalV2, RowBetween, Text, useToast } from '@pancakeswap/uikit' +import tryParseAmount from '@pancakeswap/utils/tryParseAmount' +import { FeeAmount } from '@pancakeswap/v3-sdk' +import { useWeb3React } from '@pancakeswap/wagmi' +import { ConfirmationPendingContent } from '@pancakeswap/widgets-internal' +import BigNumber from 'bignumber.js' +import { CurrencyInput } from 'components/CurrencyInput' +import { ToastDescriptionWithTx } from 'components/Toast' +import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback' +import useCatchTxError from 'hooks/useCatchTxError' +import { usePositionManagerWrapperContract } from 'hooks/useContract' +import { memo, useCallback, useMemo, useState } from 'react' +import { styled } from 'styled-components' +import { formatCurrencyAmount } from 'utils/formatCurrencyAmount' +import { Address } from 'viem' +import { DYORWarning } from 'views/PositionManagers/components/DYORWarning' +import { SingleTokenWarning } from 'views/PositionManagers/components/SingleTokenWarning' +import { StyledModal } from 'views/PositionManagers/components/StyledModal' +import { FeeTag } from 'views/PositionManagers/components/Tags' +import { useApr } from 'views/PositionManagers/hooks/useApr' +import { AprDataInfo } from '../hooks' +import { AprButton } from './AprButton' + +interface Props { + id: string | number + manager: { + id: MANAGER + name: string + } + isOpen?: boolean + onDismiss?: () => void + vaultName: string + feeTier: FeeAmount + currencyA: Currency + currencyB: Currency + ratio: number + isSingleDepositToken: boolean + allowDepositToken0: boolean + allowDepositToken1: boolean + onAmountChange?: (info: { value: string; currency: Currency; otherAmount: CurrencyAmount }) => { + otherAmount: CurrencyAmount + } + refetch?: () => void + contractAddress: Address + userCurrencyBalances: { + token0Balance: CurrencyAmount | undefined + token1Balance: CurrencyAmount | undefined + } + userVaultPercentage?: Percent + poolToken0Amount?: bigint + poolToken1Amount?: bigint + token0PriceUSD?: number + token1PriceUSD?: number + rewardPerSecond: string + earningToken: Currency + aprDataInfo: { + info: AprDataInfo | undefined + isLoading: boolean + } + rewardEndTime: number + rewardStartTime: number + onAdd?: (params: { amountA: CurrencyAmount; amountB: CurrencyAmount }) => Promise + totalAssetsInUsd: number + totalStakedInUsd: number + userLpAmounts?: bigint + totalSupplyAmounts?: bigint + precision?: bigint + strategyInfoUrl?: string + learnMoreAboutUrl?: string +} + +const StyledCurrencyInput = styled(CurrencyInput)` + flex: 1; +` + +export const AddLiquidity = memo(function AddLiquidity({ + id, + manager, + ratio, + isOpen, + vaultName, + currencyA, + currencyB, + feeTier, + isSingleDepositToken, + allowDepositToken1, + allowDepositToken0, + contractAddress, + userCurrencyBalances, + poolToken0Amount, + poolToken1Amount, + token0PriceUSD, + token1PriceUSD, + rewardPerSecond, + earningToken, + aprDataInfo, + rewardEndTime, + rewardStartTime, + refetch, + onDismiss, + totalAssetsInUsd, + userLpAmounts, + totalSupplyAmounts, + precision, + totalStakedInUsd, + strategyInfoUrl, + learnMoreAboutUrl, +}: Props) { + const [valueA, setValueA] = useState('') + const [valueB, setValueB] = useState('') + const { + t, + currentLanguage: { locale }, + } = useTranslation() + const { account, chain } = useWeb3React() + const tokenPairName = useMemo(() => `${currencyA.symbol}-${currencyB.symbol}`, [currencyA, currencyB]) + + const onInputChange = useCallback( + ({ + value, + setValue, + setOtherValue, + isToken0, + }: { + value: string + currency: Currency + otherValue: string + otherCurrency: Currency + setValue: (value: string) => void + setOtherValue: (value: string) => void + isToken0: boolean + }) => { + setValue(value) + setOtherValue((Number(value) * (isToken0 ? 1 / ratio : ratio)).toString()) + }, + [ratio], + ) + + const onCurrencyAChange = useCallback( + (value: string) => + onInputChange({ + value, + currency: currencyA, + otherValue: valueB, + otherCurrency: currencyB, + setValue: setValueA, + setOtherValue: setValueB, + isToken0: true, + }), + [currencyA, currencyB, valueB, onInputChange], + ) + + const onCurrencyBChange = useCallback( + (value: string) => + onInputChange({ + value, + currency: currencyB, + otherValue: valueA, + otherCurrency: currencyA, + setValue: setValueB, + setOtherValue: setValueA, + isToken0: false, + }), + [currencyA, currencyB, valueA, onInputChange], + ) + + const amountA = useMemo( + () => tryParseAmount(valueA, currencyA) || CurrencyAmount.fromRawAmount(currencyA, '0'), + [valueA, currencyA], + ) + const amountB = useMemo( + () => tryParseAmount(valueB, currencyB) || CurrencyAmount.fromRawAmount(currencyB, '0'), + [valueB, currencyB], + ) + + const userVaultPercentage = useMemo(() => { + const totalPoolToken0Usd = new BigNumber(amountA?.toSignificant() ?? 0).times(token0PriceUSD ?? 0)?.toNumber() + const totalPoolToken1Usd = new BigNumber(amountB?.toSignificant() ?? 0).times(token1PriceUSD ?? 0)?.toNumber() + const userTotalDepositUSD = + (allowDepositToken0 ? totalPoolToken0Usd : 0) + (allowDepositToken1 ? totalPoolToken1Usd : 0) + + return ((userTotalDepositUSD + totalAssetsInUsd) / (totalStakedInUsd + userTotalDepositUSD)) * 100 + }, [ + allowDepositToken0, + allowDepositToken1, + amountA, + amountB, + token0PriceUSD, + token1PriceUSD, + totalStakedInUsd, + totalAssetsInUsd, + ]) + + const apr = useApr({ + currencyA, + currencyB, + poolToken0Amount, + poolToken1Amount, + token0PriceUSD, + token1PriceUSD, + rewardPerSecond, + earningToken, + avgToken0Amount: aprDataInfo?.info?.token0 ?? 0, + avgToken1Amount: aprDataInfo?.info?.token1 ?? 0, + rewardEndTime, + rewardStartTime, + }) + + const displayBalanceText = useCallback( + (balanceAmount: CurrencyAmount | undefined) => + balanceAmount ? `Balances: ${balanceAmount?.toSignificant(6)}` : '', + [], + ) + + const onDone = useCallback(() => { + onDismiss?.() + refetch?.() + }, [onDismiss, refetch]) + + const disabled = useMemo(() => { + const balanceAmountMoreThenValueA = + allowDepositToken0 && + amountA.greaterThan('0') && + Number(userCurrencyBalances?.token0Balance?.toSignificant()) < Number(amountA?.toSignificant()) + + const balanceAmountMoreThenValueB = + allowDepositToken1 && + amountB.greaterThan('0') && + Number(userCurrencyBalances?.token1Balance?.toSignificant()) < Number(amountB?.toSignificant()) + return ( + (allowDepositToken0 && (amountA.equalTo('0') || balanceAmountMoreThenValueA)) || + (allowDepositToken1 && (amountB.equalTo('0') || balanceAmountMoreThenValueB)) + ) + }, [allowDepositToken0, allowDepositToken1, amountA, amountB, userCurrencyBalances]) + + const positionManagerWrapperContract = usePositionManagerWrapperContract(contractAddress) + const { fetchWithCatchTxError, loading: pendingTx } = useCatchTxError() + const { toastSuccess } = useToast() + + const mintThenDeposit = useCallback(async () => { + const receipt = await fetchWithCatchTxError(() => + positionManagerWrapperContract.write.mintThenDeposit( + [allowDepositToken0 ? amountA?.numerator ?? 0n : 0n, allowDepositToken1 ? amountB?.numerator ?? 0n : 0n, '0x'], + { + account: account ?? '0x', + chain, + }, + ), + ) + + if (receipt?.status) { + toastSuccess( + `${t('Staked')}!`, + + {t('Your funds have been staked in position manager.')} + , + ) + onDone() + } + }, [ + amountA, + amountB, + positionManagerWrapperContract, + account, + chain, + toastSuccess, + t, + fetchWithCatchTxError, + onDone, + allowDepositToken0, + allowDepositToken1, + ]) + + const translationData = useMemo( + () => ({ + amountA: allowDepositToken0 ? formatCurrencyAmount(amountA, 4, locale) : '', + symbolA: allowDepositToken0 ? currencyA.symbol : '', + amountB: allowDepositToken1 ? formatCurrencyAmount(amountB, 4, locale) : '', + symbolB: allowDepositToken1 ? currencyB.symbol : '', + }), + [allowDepositToken0, allowDepositToken1, amountA, amountB, currencyA.symbol, currencyB.symbol, locale], + ) + + const pendingText = useMemo( + () => + !isSingleDepositToken + ? t('Supplying %amountA% %symbolA% and %amountB% %symbolB%', translationData) + : t('Supplying %amountA% %symbolA% %amountB% %symbolB%', translationData), + [t, isSingleDepositToken, translationData], + ) + + return ( + + + {pendingTx ? ( + + ) : ( + <> + + {t('Adding')}: + + + {tokenPairName} + + + {vaultName} + + + + + {allowDepositToken0 && ( + + + + )} + {allowDepositToken1 && ( + + + + )} + + + {t('Your share in the vault')}: + {`${userVaultPercentage?.toFixed(2)}%`} + + + {t('APR')}: + + + + {isSingleDepositToken && } + + + + + + )} + + + ) +}) + +interface AddLiquidityButtonProps { + amountA: CurrencyAmount | undefined + amountB: CurrencyAmount | undefined + contractAddress: `0x${string}` + disabled?: boolean + onAddLiquidity?: () => void + isLoading?: boolean + learnMoreAboutUrl?: string +} + +export const AddLiquidityButton = memo(function AddLiquidityButton({ + amountA, + amountB, + contractAddress, + disabled, + onAddLiquidity, + isLoading, + learnMoreAboutUrl, +}: AddLiquidityButtonProps) { + const { t } = useTranslation() + + const { approvalState: approvalStateToken0, approveCallback: approveCallbackToken0 } = useApproveCallback( + amountA, + contractAddress, + ) + const { approvalState: approvalStateToken1, approveCallback: approveCallbackToken1 } = useApproveCallback( + amountB, + contractAddress, + ) + + const showAmountButtonA = useMemo( + () => amountA && approvalStateToken0 === ApprovalState.NOT_APPROVED, + [amountA, approvalStateToken0], + ) + const showAmountButtonB = useMemo( + () => amountB && approvalStateToken1 === ApprovalState.NOT_APPROVED, + [amountB, approvalStateToken1], + ) + const isConfirmButtonDisabled = useMemo( + () => + disabled || + (amountA && approvalStateToken0 !== ApprovalState.APPROVED) || + (amountB && approvalStateToken1 !== ApprovalState.APPROVED), + [amountA, amountB, disabled, approvalStateToken0, approvalStateToken1], + ) + + return ( + <> + {showAmountButtonA && ( + + )} + {showAmountButtonB && ( + + )} + + + {t('Learn more about the strategy')} + + + ) +}) diff --git a/apps/web/src/views/PositionManagers/components/AprButton.tsx b/apps/web/src/views/PositionManagers/components/AprButton.tsx new file mode 100644 index 0000000000000..a1c110b09cf0f --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/AprButton.tsx @@ -0,0 +1,113 @@ +import { useTranslation } from '@pancakeswap/localization' +import { Flex, RoiCalculatorModal, Skeleton, Text, useModal, useTooltip } from '@pancakeswap/uikit' +import BigNumber from 'bignumber.js' +import { useCakePrice } from 'hooks/useCakePrice' +import { memo, useMemo } from 'react' +import { styled } from 'styled-components' +import { useAccount } from 'wagmi' +import { AprResult } from '../hooks' + +interface Props { + id: number | string + apr: AprResult + isAprLoading: boolean + lpSymbol: string + totalAssetsInUsd: number + userLpAmounts?: bigint + totalSupplyAmounts?: bigint + precision?: bigint +} + +const AprText = styled(Text)` + text-underline-offset: 0.125em; + text-decoration: dotted underline; + cursor: pointer; +` + +export const AprButton = memo(function YieldInfo({ + id, + apr, + isAprLoading, + totalAssetsInUsd, + lpSymbol, + userLpAmounts, + precision, +}: Props) { + const { t } = useTranslation() + + const { address: account } = useAccount() + const cakePriceBusd = useCakePrice() + const tokenBalance = useMemo( + () => new BigNumber(Number(((userLpAmounts ?? 0n) * 10000n) / (precision ?? 1n)) / 10000 ?? 0), + [userLpAmounts, precision], + ) + + const tokenPrice = useMemo( + () => totalAssetsInUsd / (Number(((userLpAmounts ?? 0n) * 10000n) / (precision ?? 1n)) / 10000 ?? 0), + [userLpAmounts, precision, totalAssetsInUsd], + ) + const { targetRef, tooltip, tooltipVisible } = useTooltip( + <> + + {t('Combined APR')}:{' '} + + {`${apr.combinedApr}%`} + + +
    + {apr.isInCakeRewardDateRange && ( +
  • + {t('CAKE APR')}:{' '} + + {`${apr.cakeYieldApr}%`} + +
  • + )} +
  • + {t('LP APR')}:{' '} + + {apr.lpApr}% + +
  • +
+ + {t('Calculated based on previous 7 days average data.')} + + , + { + placement: 'top', + }, + ) + + const [onPresentApyModal] = useModal( + , + false, + true, + `PositionManagerModal${id}`, + ) + + return ( + + {apr && !isAprLoading ? ( + + {`${apr.combinedApr}%`} + {tooltipVisible && tooltip} + + ) : ( + + )} + + ) +}) diff --git a/apps/web/src/views/PositionManagers/components/CardLayout.tsx b/apps/web/src/views/PositionManagers/components/CardLayout.tsx new file mode 100644 index 0000000000000..42b2506e2cbed --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/CardLayout.tsx @@ -0,0 +1,15 @@ +import { styled } from 'styled-components' +import { FlexLayout, CardHeader as CardHeaderComp } from '@pancakeswap/uikit' + +export const CardLayout = styled(FlexLayout)` + justify-content: flex-start; +` + +export const CardHeader = styled(CardHeaderComp)` + background: none; + display: flex; + justify-content: space-between; + flex-direction: row; + align-items: flex-start; + padding: 1.5em 1.5em 0 1.5em; +` diff --git a/apps/web/src/views/PositionManagers/components/CardSection.tsx b/apps/web/src/views/PositionManagers/components/CardSection.tsx new file mode 100644 index 0000000000000..bf49ca29b8a05 --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/CardSection.tsx @@ -0,0 +1,30 @@ +import { styled } from 'styled-components' +import { SpaceProps } from 'styled-system' +import { PropsWithChildren, ReactNode, memo } from 'react' +import { Box, Text } from '@pancakeswap/uikit' + +interface Props extends SpaceProps { + title: ReactNode +} + +const Section = styled(Box)` + & + & { + margin-top: 1em; + } +` + +const Title = styled(Text).attrs({ + color: 'textSubtle', + textTransform: 'uppercase', + fontSize: '12px', + bold: true, +})`` + +export const CardSection = memo(function CardSection({ title, children, ...props }: PropsWithChildren) { + return ( +
+ {title} + {children} +
+ ) +}) diff --git a/apps/web/src/views/PositionManagers/components/CardTitle.tsx b/apps/web/src/views/PositionManagers/components/CardTitle.tsx new file mode 100644 index 0000000000000..fea242ea049fd --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/CardTitle.tsx @@ -0,0 +1,68 @@ +import { memo, useMemo } from 'react' +import { Currency } from '@pancakeswap/sdk' +import { FeeAmount } from '@pancakeswap/v3-sdk' +import { Flex, Text } from '@pancakeswap/uikit' + +import { CardHeader } from './CardLayout' +import { TokenPairLogos } from './TokenPairLogos' +import { FeeTag, FarmTag, SingleTokenTag } from './Tags' + +interface Props { + currencyA: Currency + currencyB: Currency + vaultName: string + feeTier: FeeAmount + isSingleDepositToken: boolean + allowDepositToken1: boolean + autoFarm?: boolean + autoCompound?: boolean +} + +export const CardTitle = memo(function CardTitle({ + currencyA, + currencyB, + vaultName, + feeTier, + isSingleDepositToken, + autoFarm, + autoCompound, + allowDepositToken1, +}: Props) { + const isTokenDisplayReverse = useMemo( + () => isSingleDepositToken && allowDepositToken1, + [isSingleDepositToken, allowDepositToken1], + ) + const displayCurrencyA = useMemo( + () => (isTokenDisplayReverse ? currencyB : currencyA), + [isTokenDisplayReverse, currencyA, currencyB], + ) + const displayCurrencyB = useMemo( + () => (isTokenDisplayReverse ? currencyA : currencyB), + [isTokenDisplayReverse, currencyA, currencyB], + ) + const tokenPairName = useMemo( + () => `${displayCurrencyA.symbol}-${displayCurrencyB.symbol}`, + [displayCurrencyA, displayCurrencyB], + ) + + return ( + + + + + + {tokenPairName} + + + {vaultName} + + + + + {autoFarm && } + {isSingleDepositToken && } + + + + ) +}) diff --git a/apps/web/src/views/PositionManagers/components/ControlsContainer.tsx b/apps/web/src/views/PositionManagers/components/ControlsContainer.tsx new file mode 100644 index 0000000000000..22436bb6ae7b0 --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/ControlsContainer.tsx @@ -0,0 +1,37 @@ +import { Flex } from '@pancakeswap/uikit' +import { styled } from 'styled-components' + +export const ControlsContainer = styled(Flex).attrs({ + alignItems: 'center', + justifyContent: 'space-between', + flexDirection: 'column', +})` + width: 100%; + position: relative; + + margin-bottom: 2em; + + ${({ theme }) => theme.mediaQueries.sm} { + flex-direction: row; + flex-wrap: wrap; + padding: 1em 2em; + margin-bottom: 0; + } +` + +export const ControlGroup = styled(Flex)` + width: 100%; + align-items: ${(props) => props.alignItems || 'center'}; + flex-direction: ${(props) => props.flexDirection || 'row'}; + justify-content: ${(props) => props.justifyContent || 'space-between'}; + margin-bottom: 1em; + + &:last-child { + margin-bottom: 0; + } + + ${({ theme }) => theme.mediaQueries.sm} { + width: auto; + margin-bottom: 0; + } +` diff --git a/apps/web/src/views/PositionManagers/components/DYORWarning.tsx b/apps/web/src/views/PositionManagers/components/DYORWarning.tsx new file mode 100644 index 0000000000000..a4578ccfac7bc --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/DYORWarning.tsx @@ -0,0 +1,52 @@ +import { useMemo } from 'react' +import { useTranslation } from '@pancakeswap/localization' +import { Message, MessageText, Text, Flex, Box, Link } from '@pancakeswap/uikit' +import { MANAGER, baseManagers, BaseManager } from '@pancakeswap/position-managers' + +interface DYORWarningProps { + manager: { + id: MANAGER + name: string + } +} + +export const DYORWarning: React.FC = ({ manager }) => { + const { t } = useTranslation() + const managerInfo: BaseManager = useMemo(() => baseManagers[manager.id], [manager]) + + if (!managerInfo?.name && !managerInfo?.introLink) { + return null + } + + return ( + + + + + + {t('You are providing liquidity via a 3rd party liquidity manager')} + + + {managerInfo.name} + + + {t('which is responsible for managing the underlying assets.')} + + + + {t('Please always DYOR before depositing your assets.')} + + + + + ) +} diff --git a/apps/web/src/views/PositionManagers/components/DuoTokenVaultCard.tsx b/apps/web/src/views/PositionManagers/components/DuoTokenVaultCard.tsx new file mode 100644 index 0000000000000..b820342e18a48 --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/DuoTokenVaultCard.tsx @@ -0,0 +1,230 @@ +import { MANAGER, Strategy } from '@pancakeswap/position-managers' +import { Card, CardBody } from '@pancakeswap/uikit' +import { Currency, Percent, Price, CurrencyAmount } from '@pancakeswap/sdk' +import { FeeAmount } from '@pancakeswap/v3-sdk' +import { Address } from 'viem' +import { ReactNode, memo, PropsWithChildren, useMemo } from 'react' +import { styled } from 'styled-components' +import { useApr } from 'views/PositionManagers/hooks/useApr' +import { CardTitle } from './CardTitle' +import { YieldInfo } from './YieldInfo' +import { ManagerInfo } from './ManagerInfo' +import { LiquidityManagement } from './LiquidityManagement' +import { getVaultName } from '../utils' +import { ExpandableSection } from './ExpandableSection' +import { VaultInfo } from './VaultInfo' +import { VaultLinks } from './VaultLinks' +import { AprDataInfo } from '../hooks' + +const StyledCard = styled(Card)` + align-self: baseline; + max-width: 100%; + margin: 0 0 24px 0; + ${({ theme }) => theme.mediaQueries.sm} { + max-width: 350px; + margin: 0 12px 46px; + } +` + +interface Props { + currencyA: Currency + currencyB: Currency + earningToken: Currency + name: string + id: string | number + feeTier: FeeAmount + ratio: number + strategy: Strategy + manager: { + id: MANAGER + name: string + } + managerFee?: Percent + autoFarm?: boolean + autoCompound?: boolean + info?: ReactNode + isSingleDepositToken: boolean + allowDepositToken0?: boolean + allowDepositToken1?: boolean + contractAddress: Address + poolToken0Amount?: bigint + poolToken1Amount?: bigint + stakedToken0Amount?: bigint + stakedToken1Amount?: bigint + token0PriceUSD?: number + token1PriceUSD?: number + pendingReward: bigint | undefined + userVaultPercentage?: Percent + managerAddress: Address + managerInfoUrl: string + strategyInfoUrl: string + projectVaultUrl?: string + learnMoreAboutUrl?: string + rewardPerSecond: string + aprDataInfo: { + info: AprDataInfo | undefined + isLoading: boolean + } + rewardEndTime: number + rewardStartTime: number + refetch?: () => void + totalAssetsInUsd: number + totalStakedInUsd: number + userLpAmounts?: bigint + totalSupplyAmounts?: bigint + precision?: bigint +} + +export const DuoTokenVaultCard = memo(function DuoTokenVaultCard({ + currencyA, + currencyB, + earningToken, + name, + id, + feeTier, + autoFarm, + autoCompound, + manager, + managerFee, + strategy, + ratio, + isSingleDepositToken, + allowDepositToken0 = true, + allowDepositToken1 = true, + contractAddress, + stakedToken0Amount, + stakedToken1Amount, + poolToken0Amount, + poolToken1Amount, + token0PriceUSD, + token1PriceUSD, + pendingReward, + managerInfoUrl, + strategyInfoUrl, + projectVaultUrl, + rewardPerSecond, + aprDataInfo, + rewardEndTime, + refetch, + rewardStartTime, + totalAssetsInUsd, + userLpAmounts, + totalSupplyAmounts, + precision, + managerAddress, + totalStakedInUsd, + learnMoreAboutUrl, +}: PropsWithChildren) { + const apr = useApr({ + currencyA, + currencyB, + poolToken0Amount, + poolToken1Amount, + token0PriceUSD, + token1PriceUSD, + rewardPerSecond, + earningToken, + avgToken0Amount: aprDataInfo?.info?.token0 ?? 0, + avgToken1Amount: aprDataInfo?.info?.token1 ?? 0, + rewardEndTime, + rewardStartTime, + }) + + const price = new Price(currencyA, currencyB, 100000n, 100000n) + const vaultName = useMemo(() => getVaultName(id, name), [name, id]) + const staked0Amount = stakedToken0Amount ? CurrencyAmount.fromRawAmount(currencyA, stakedToken0Amount) : undefined + const staked1Amount = stakedToken1Amount ? CurrencyAmount.fromRawAmount(currencyB, stakedToken1Amount) : undefined + + const withCakeReward: boolean = useMemo(() => earningToken.symbol === 'CAKE', [earningToken]) + + return ( + + + + + + + + + + + + + ) +}) diff --git a/apps/web/src/views/PositionManagers/components/ExpandableSection.tsx b/apps/web/src/views/PositionManagers/components/ExpandableSection.tsx new file mode 100644 index 0000000000000..a908b56bb570e --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/ExpandableSection.tsx @@ -0,0 +1,24 @@ +import { SpaceProps } from 'styled-system' +import { PropsWithChildren, memo, useCallback, useState } from 'react' +import { Flex, ExpandableLabel, Text } from '@pancakeswap/uikit' +import { useTranslation } from '@pancakeswap/localization' + +export const ExpandableSection = memo(function ExpandableSection({ + children, + ...props +}: PropsWithChildren) { + const { t } = useTranslation() + const [expanded, setExpanded] = useState(false) + const toggle = useCallback(() => setExpanded(!expanded), [expanded]) + + return ( + + + + {expanded ? t('Hide') : t('Info')} + + + {expanded ? children : null} + + ) +}) diff --git a/apps/web/src/views/PositionManagers/components/Filters.tsx b/apps/web/src/views/PositionManagers/components/Filters.tsx new file mode 100644 index 0000000000000..ba33c2951ef50 --- /dev/null +++ b/apps/web/src/views/PositionManagers/components/Filters.tsx @@ -0,0 +1,85 @@ +import { Flex, Text, Select, OptionProps, SearchInput } from '@pancakeswap/uikit' +import { styled } from 'styled-components' +import { useTranslation } from '@pancakeswap/localization' +import { useMemo, useCallback, ChangeEvent } from 'react' + +import { useSearch, useSortBy } from '../hooks' + +const LabelWrapper = styled.div` + > ${Text} { + font-size: 12px; + } +` + +const ControlStretch = styled(Flex)` + > div { + flex: 1; + } +` + +export function SortFilter() { + const { t } = useTranslation() + const [sortBy, setSortBy] = useSortBy() + const options = useMemo( + () => [ + { + label: t('Hot'), + value: 'hot', + }, + { + label: t('APR'), + value: 'apr', + }, + { + label: t('Earned'), + value: 'earned', + }, + { + label: t('Total staked'), + value: 'totalStaked', + }, + { + label: t('Latest'), + value: 'latest', + }, + ], + [t], + ) + const selected = useMemo(() => { + const index = options.findIndex((option) => option.value === sortBy) + // FIXME weird design of Select component. Need further refactor + return index >= 0 ? index + 1 : 0 + }, [options, sortBy]) + + const handleSortOptionChange = useCallback((option: OptionProps) => setSortBy(option.value), [setSortBy]) + + return ( + + + {t('Sort by')} + + +