diff --git a/apps/web/public/images/pool/lockCAKEbenefit.png b/apps/web/public/images/pool/lockCAKEbenefit.png new file mode 100644 index 0000000000000..b5e8a2289c05c Binary files /dev/null and b/apps/web/public/images/pool/lockCAKEbenefit.png differ diff --git a/apps/web/public/images/pool/lockcaketooltip.png b/apps/web/public/images/pool/lockcaketooltip.png new file mode 100644 index 0000000000000..c0c1933a53c5f Binary files /dev/null and b/apps/web/public/images/pool/lockcaketooltip.png differ diff --git a/apps/web/src/config/abi/revenueSharingPool.ts b/apps/web/src/config/abi/revenueSharingPool.ts new file mode 100644 index 0000000000000..9abfe092feb62 --- /dev/null +++ b/apps/web/src/config/abi/revenueSharingPool.ts @@ -0,0 +1,243 @@ +export const revenueSharingPoolABI = [ + { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: 'uint256', name: '_timestamp', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: '_tokens', type: 'uint256' }, + ], + name: 'LogCheckpointToken', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: '_recipient', type: 'address' }, + { indexed: false, internalType: 'uint256', name: '_amount', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: '_claimEpoch', type: 'uint256' }, + { indexed: false, internalType: 'uint256', name: '_maxEpoch', type: 'uint256' }, + ], + name: 'LogClaimed', + type: 'event', + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'uint256', name: '_amount', type: 'uint256' }], + name: 'LogFeed', + type: 'event', + }, + { anonymous: false, inputs: [], name: 'LogKilled', type: 'event' }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: 'bool', name: '_toggleFlag', type: 'bool' }], + name: 'LogSetCanCheckpointToken', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: '_caller', type: 'address' }, + { indexed: true, internalType: 'address', name: '_address', type: 'address' }, + { indexed: false, internalType: 'bool', name: '_ok', type: 'bool' }, + ], + name: 'LogSetWhitelistedCheckpointCallers', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'previousOwner', type: 'address' }, + { indexed: true, internalType: 'address', name: 'newOwner', type: 'address' }, + ], + name: 'OwnershipTransferred', + type: 'event', + }, + { + inputs: [], + name: 'TOKEN_CHECKPOINT_DEADLINE', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'VCake', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'WEEK', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_user', type: 'address' }, + { internalType: 'uint256', name: '_timestamp', type: 'uint256' }, + ], + name: 'balanceOfAt', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'canCheckpointToken', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { inputs: [], name: 'checkpointToken', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { inputs: [], name: 'checkpointTotalSupply', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [{ internalType: 'address', name: '_user', type: 'address' }], + name: 'claim', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address[]', name: '_users', type: 'address[]' }], + name: 'claimMany', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'emergencyReturn', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_amount', type: 'uint256' }], + name: 'feed', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: '_timestamp', type: 'uint256' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + ], + name: 'injectReward', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'isKilled', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { inputs: [], name: 'kill', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [], + name: 'lastTokenBalance', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'lastTokenTimestamp', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { inputs: [], name: 'renounceOwnership', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [], + name: 'rewardToken', + outputs: [{ internalType: 'contract IERC20', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'bool', name: '_newCanCheckpointToken', type: 'bool' }], + name: 'setCanCheckpointToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address[]', name: '_callers', type: 'address[]' }, + { internalType: 'bool', name: '_ok', type: 'bool' }, + ], + name: 'setWhitelistedCheckpointCallers', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'startWeekCursor', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'tokensPerWeek', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'totalSupplyAt', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: 'newOwner', type: 'address' }], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'userEpochOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'weekCursor', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'weekCursorOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'whitelistedCheckpointCallers', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, +] as const diff --git a/apps/web/src/config/abi/vCake.ts b/apps/web/src/config/abi/vCake.ts new file mode 100644 index 0000000000000..98527c241c604 --- /dev/null +++ b/apps/web/src/config/abi/vCake.ts @@ -0,0 +1,210 @@ +export const vCakeABI = [ + { + inputs: [ + { internalType: 'contract ICakePool', name: '_cakePool', type: 'address' }, + { internalType: 'contract IMasterChefV2', name: '_masterchefV2', type: 'address' }, + { internalType: 'uint256', name: '_pid', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { indexed: false, internalType: 'uint256', name: 'lockedAmount', type: 'uint256' }, + ], + name: 'Sync', + type: 'event', + }, + { + inputs: [], + name: 'CakePool', + outputs: [{ internalType: 'contract ICakePool', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'CakePoolPID', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'DURATION_FACTOR_OVERDUE', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MULTIPLIER', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'MasterchefV2', + outputs: [{ internalType: 'contract IMasterChefV2', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'PRECISION_FACTOR', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'UNLOCK_FREE_DURATION', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'WEEK', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_user', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_user', type: 'address' }, + { internalType: 'uint256', name: '_blockNumber', type: 'uint256' }, + ], + name: 'balanceOfAt', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { inputs: [], name: 'checkpoint', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [], + name: 'decimals', + outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '_user', type: 'address' }, + { internalType: 'uint256', name: '_amount', type: 'uint256' }, + { internalType: 'uint256', name: '_lockDuration', type: 'uint256' }, + ], + name: 'deposit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'epoch', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'initialization', + outputs: [{ internalType: 'bool', name: '', type: 'bool' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'name', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'pointHistory', + outputs: [ + { internalType: 'int128', name: 'bias', type: 'int128' }, + { internalType: 'int128', name: 'slope', type: 'int128' }, + { internalType: 'uint256', name: 'timestamp', type: 'uint256' }, + { internalType: 'uint256', name: 'blockNumber', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + name: 'slopeChanges', + outputs: [{ internalType: 'int128', name: '', type: 'int128' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'symbol', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { inputs: [], name: 'syncFromCakePool', outputs: [], stateMutability: 'nonpayable', type: 'function' }, + { + inputs: [], + name: 'totalSupply', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: '_blockNumber', type: 'uint256' }], + name: 'totalSupplyAt', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'userPointEpoch', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: '', type: 'address' }, + { internalType: 'uint256', name: '', type: 'uint256' }, + ], + name: 'userPointHistory', + outputs: [ + { internalType: 'int128', name: 'bias', type: 'int128' }, + { internalType: 'int128', name: 'slope', type: 'int128' }, + { internalType: 'uint256', name: 'timestamp', type: 'uint256' }, + { internalType: 'uint256', name: 'blockNumber', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '', type: 'address' }], + name: 'userPrevLockedAmount', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [{ internalType: 'address', name: '_user', type: 'address' }], + name: 'withdraw', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const diff --git a/apps/web/src/config/constants/contracts.ts b/apps/web/src/config/constants/contracts.ts index 3fe8fd97f5169..7a9a056a32858 100644 --- a/apps/web/src/config/constants/contracts.ts +++ b/apps/web/src/config/constants/contracts.ts @@ -226,4 +226,14 @@ export default { 56: '0x549d484F493b778A5c70638E30Fc6Dc6B2Dcc4c0', 97: '0x', }, + vCake: { + 1: '0x', + 56: '0xa3b8321173Cf3DdF37Ce3e7548203Fc25d86402F', + 97: '0x5DD37E97716A8b358BCbc731516F36FFff978454', + }, + revenueSharingPool: { + 1: '0x', + 56: '0xCD5d1935e9bfa4905f9f007C97aB1f1763dC1607', + 97: '0xd2d1DD41700d9132d3286e0FcD8D6E1D8E5052F5', + }, } as const satisfies Record> diff --git a/apps/web/src/hooks/useContract.ts b/apps/web/src/hooks/useContract.ts index f0effed0a1f7a..6280ad8111618 100644 --- a/apps/web/src/hooks/useContract.ts +++ b/apps/web/src/hooks/useContract.ts @@ -46,6 +46,8 @@ import { getV3AirdropContract, getV3MigratorContract, getTradingRewardTopTradesContract, + getVCakeContract, + getRevenueSharingPoolContract, } from 'utils/contractHelpers' import { ChainId, WNATIVE, pancakePairV2ABI } from '@pancakeswap/sdk' @@ -392,3 +394,15 @@ export const useTradingRewardTopTraderContract = ({ chainId: chainId_ }: { chain const { data: signer } = useWalletClient() return useMemo(() => getTradingRewardTopTradesContract(signer, chainId_ ?? chainId), [signer, chainId_, chainId]) } + +export const useVCakeContract = ({ chainId: chainId_ }: { chainId?: ChainId } = {}) => { + const { chainId } = useActiveChainId() + const { data: signer } = useWalletClient() + return useMemo(() => getVCakeContract(signer, chainId_ ?? chainId), [signer, chainId_, chainId]) +} + +export const useRevenueSharingPoolContract = ({ chainId: chainId_ }: { chainId?: ChainId } = {}) => { + const { chainId } = useActiveChainId() + const { data: signer } = useWalletClient() + return useMemo(() => getRevenueSharingPoolContract(signer, chainId_ ?? chainId), [signer, chainId_, chainId]) +} diff --git a/apps/web/src/utils/addressHelpers.ts b/apps/web/src/utils/addressHelpers.ts index 667cac02f9887..366b87f512fa7 100644 --- a/apps/web/src/utils/addressHelpers.ts +++ b/apps/web/src/utils/addressHelpers.ts @@ -140,3 +140,11 @@ export const getAffiliateProgramAddress = (chainId?: number) => { export const getTradingRewardTopTradesAddress = (chainId?: number) => { return getAddressFromMap(addresses.tradingRewardTopTrades, chainId) } + +export const getVCakeAddress = (chainId?: number) => { + return getAddressFromMap(addresses.vCake, chainId) +} + +export const getRevenueSharingPoolAddress = (chainId?: number) => { + return getAddressFromMap(addresses.revenueSharingPool, chainId) +} diff --git a/apps/web/src/utils/contractHelpers.ts b/apps/web/src/utils/contractHelpers.ts index 5f999d6f3bc64..a94a0ca850f44 100644 --- a/apps/web/src/utils/contractHelpers.ts +++ b/apps/web/src/utils/contractHelpers.ts @@ -32,6 +32,8 @@ import { getV3MigratorAddress, getAffiliateProgramAddress, getTradingRewardTopTradesAddress, + getVCakeAddress, + getRevenueSharingPoolAddress, } from 'utils/addressHelpers' // ABI @@ -78,6 +80,8 @@ import { tradingCompetitionMoboxABI } from 'config/abi/tradingCompetitionMobox' import { tradingRewardABI } from 'config/abi/tradingReward' import { v3AirdropABI } from 'config/abi/v3Airdrop' import { v3MigratorABI } from 'config/abi/v3Migrator' +import { vCakeABI } from 'config/abi/vCake' +import { revenueSharingPoolABI } from 'config/abi/revenueSharingPool' import { getViemClients, viemClients } from 'utils/viem' import { Abi, PublicClient, WalletClient, getContract as viemGetContract } from 'viem' import { Address, erc20ABI, erc721ABI } from 'wagmi' @@ -388,3 +392,21 @@ export const getTradingRewardTopTradesContract = (signer?: WalletClient, chainId chainId, }) } + +export const getVCakeContract = (signer?: WalletClient, chainId?: number) => { + return getContract({ + abi: vCakeABI, + address: getVCakeAddress(chainId), + signer, + chainId, + }) +} + +export const getRevenueSharingPoolContract = (signer?: WalletClient, chainId?: number) => { + return getContract({ + abi: revenueSharingPoolABI, + address: getRevenueSharingPoolAddress(chainId), + signer, + chainId, + }) +} diff --git a/apps/web/src/views/Farms/components/FarmCard/V3/FarmV3Card.tsx b/apps/web/src/views/Farms/components/FarmCard/V3/FarmV3Card.tsx index f8ea8db973b02..d4434ce38fe92 100644 --- a/apps/web/src/views/Farms/components/FarmCard/V3/FarmV3Card.tsx +++ b/apps/web/src/views/Farms/components/FarmCard/V3/FarmV3Card.tsx @@ -58,12 +58,7 @@ interface FarmCardProps { account?: string } -export const FarmV3Card: React.FC> = ({ - farm, - removed, - // cakePrice, - account, -}) => { +export const FarmV3Card: React.FC> = ({ farm, removed, account }) => { const { t } = useTranslation() const { chainId } = useActiveChainId() const [showExpandableSection, setShowExpandableSection] = useState(false) diff --git a/apps/web/src/views/Pools/components/CakeVaultCard/index.tsx b/apps/web/src/views/Pools/components/CakeVaultCard/index.tsx index d1b2b722c1f1a..ec955ee922d52 100644 --- a/apps/web/src/views/Pools/components/CakeVaultCard/index.tsx +++ b/apps/web/src/views/Pools/components/CakeVaultCard/index.tsx @@ -1,4 +1,16 @@ -import { Box, CardBody, CardProps, Flex, Text, TokenPairImage, FlexGap, Skeleton, Pool } from '@pancakeswap/uikit' +import { + Box, + CardBody, + CardProps, + Button, + Flex, + Text, + TokenPairImage, + FlexGap, + Skeleton, + Pool, + useModal, +} from '@pancakeswap/uikit' import { useAccount } from 'wagmi' import ConnectWalletButton from 'components/ConnectWalletButton' import { vaultPoolConfig } from 'config/constants/pools' @@ -7,6 +19,9 @@ import { useVaultPoolByKey } from 'state/pools/hooks' import { VaultKey, DeserializedLockedCakeVault, DeserializedCakeVault } from 'state/types' import styled from 'styled-components' import { Token } from '@pancakeswap/sdk' +import BenefitsModal from 'views/Pools/components/RevenueSharing/BenefitsModal' +import { getVaultPosition, VaultPosition } from 'utils/cakePool' +import useVCake from 'views/Pools/hooks/useVCake' import CardFooter from '../PoolCard/CardFooter' import { VaultPositionTagWithLabel } from '../Vault/VaultPositionTag' @@ -50,7 +65,15 @@ export const CakeVaultDetail: React.FC { const { t } = useTranslation() + const { isInitialization } = useVCake() + const [onPresentViewBenefitsModal] = useModal( + , + true, + false, + 'revenueModal', + ) + const vaultPosition = getVaultPosition(vaultPool.userData) const isLocked = (vaultPool as DeserializedLockedCakeVault)?.userData?.locked if (!pool) { @@ -61,15 +84,22 @@ export const CakeVaultDetail: React.FC {account && pool.vaultKey === VaultKey.CakeVault && ( - + )} {account && pool.vaultKey === VaultKey.CakeVault && isLocked ? ( - + <> + + {vaultPosition === VaultPosition.Locked && isInitialization && !showICake && ( + + )} + ) : ( <> diff --git a/apps/web/src/views/Pools/components/LockedPool/Common/LockedActions.tsx b/apps/web/src/views/Pools/components/LockedPool/Common/LockedActions.tsx index 8faedfc2fafb2..34dfcf58aceea 100644 --- a/apps/web/src/views/Pools/components/LockedPool/Common/LockedActions.tsx +++ b/apps/web/src/views/Pools/components/LockedPool/Common/LockedActions.tsx @@ -1,5 +1,5 @@ import { useMemo } from 'react' -import { Flex, Box } from '@pancakeswap/uikit' +import { Flex, Box, ButtonProps } from '@pancakeswap/uikit' import BigNumber from 'bignumber.js' import { getVaultPosition, VaultPosition } from 'utils/cakePool' import { BIG_ZERO } from '@pancakeswap/utils/bigNumber' @@ -10,7 +10,7 @@ import ExtendButton from '../Buttons/ExtendDurationButton' import AfterLockedActions from './AfterLockedActions' import { LockedActionsPropsType } from '../types' -const LockedActions: React.FC> = ({ +const LockedActions: React.FC> = ({ userShares, locked, lockEndTime, @@ -19,6 +19,7 @@ const LockedActions: React.FC> = stakingTokenBalance, stakingTokenPrice, lockedAmount, + variant, }) => { const position = useMemo( () => @@ -42,6 +43,7 @@ const LockedActions: React.FC> = > = + userData: DeserializedLockedVaultUser +} + +const LockedStaking: React.FC> = ({ buttonVariant, pool, userData }) => { + const { t } = useTranslation() + + const position = useMemo( + () => + getVaultPosition({ + userShares: userData?.userShares, + locked: userData?.locked, + lockEndTime: userData?.lockEndTime, + }), + [userData], + ) + + const stakingToken = pool?.stakingToken + const stakingTokenPrice = pool?.stakingTokenPrice + const stakingTokenBalance = pool?.userData?.stakingTokenBalance + + const currentLockedAmountAsBigNumber = useMemo(() => { + return userData?.balance?.cakeAsBigNumber + }, [userData?.balance?.cakeAsBigNumber]) + + const currentLockedAmount = getBalanceNumber(currentLockedAmountAsBigNumber) + + const usdValueStaked = useMemo( + () => + stakingToken && stakingTokenPrice + ? getBalanceNumber(userData?.balance?.cakeAsBigNumber.multipliedBy(stakingTokenPrice), stakingToken?.decimals) + : null, + [userData?.balance?.cakeAsBigNumber, stakingTokenPrice, stakingToken], + ) + + const { lockEndDate, remainingTime, burnStartTime } = useUserDataInVaultPresenter({ + lockStartTime: userData?.lockStartTime, + lockEndTime: userData?.lockEndTime, + burnStartTime: userData?.burnStartTime, + }) + + const { + targetRef: tagTargetRefOfLocked, + tooltip: tagTooltipOfLocked, + tooltipVisible: tagTooltipVisibleOfLocked, + } = useTooltip(, { + placement: 'bottom', + }) + + const tooltipContentOfBurn = t( + 'After Burning starts at %burnStartTime%. You need to renew your fix-term position, to initiate a new lock or convert your staking position to flexible before it starts. Otherwise all the rewards will be burned within the next 90 days.', + { burnStartTime }, + ) + const { + targetRef: tagTargetRefOfBurn, + tooltip: tagTooltipOfBurn, + tooltipVisible: tagTooltipVisibleOfBurn, + } = useTooltip(tooltipContentOfBurn, { + placement: 'bottom', + }) + + return ( + + + + + {t('CAKE locked')} + + + + {tagTooltipVisibleOfLocked && tagTooltipOfLocked} + + + + + + + + + {t('Unlocks In')} + + + = VaultPosition.LockedEnd ? '#D67E0A' : 'text'} bold fontSize="16px"> + {position >= VaultPosition.LockedEnd ? t('Unlocked') : remainingTime} + + {tagTooltipVisibleOfBurn && tagTooltipOfBurn} + + + + + = VaultPosition.LockedEnd ? '#D67E0A' : 'text'} fontSize="12px"> + {t('On %date%', { date: lockEndDate })} + + + + + + + + ) +} + +export default LockedStaking diff --git a/apps/web/src/views/Pools/components/LockedPool/LockedStakingApy.tsx b/apps/web/src/views/Pools/components/LockedPool/LockedStakingApy.tsx index 7218a95117929..fed750e6450e2 100644 --- a/apps/web/src/views/Pools/components/LockedPool/LockedStakingApy.tsx +++ b/apps/web/src/views/Pools/components/LockedPool/LockedStakingApy.tsx @@ -1,8 +1,6 @@ -import styled from 'styled-components' import { useMemo, memo } from 'react' import { getVaultPosition, VaultPosition } from 'utils/cakePool' - -import { Flex, Text, Box, TooltipText, useTooltip, HelpIcon, BalanceWithLoading, Pool } from '@pancakeswap/uikit' +import { Flex, Text, TooltipText, useTooltip, BalanceWithLoading, Pool } from '@pancakeswap/uikit' import { LightGreyCard } from 'components/Card' import { useTranslation } from '@pancakeswap/localization' import { useVaultApy } from 'hooks/useVaultApy' @@ -11,7 +9,6 @@ import { Token } from '@pancakeswap/sdk' import isUndefinedOrNull from '@pancakeswap/utils/isUndefinedOrNull' import { getBalanceNumber, getFullDisplayBalance } from '@pancakeswap/utils/formatBalance' import BurningCountDown from './Common/BurningCountDown' -import LockedActions from './Common/LockedActions' import YieldBoostRow from './Common/YieldBoostRow' import LockDurationRow from './Common/LockDurationRow' import IfoCakeRow from './Common/IfoCakeRow' @@ -19,11 +16,7 @@ import useUserDataInVaultPresenter from './hooks/useUserDataInVaultPresenter' import { LockedStakingApyPropsType } from './types' import LockedAprTooltipContent from './Common/LockedAprTooltipContent' import AutoEarningsBreakdown from '../AutoEarningsBreakdown' -import OriginalLockedInfo from '../OriginalLockedInfo' - -const HelpIconWrapper = styled.div` - align-self: center; -` +import LockedStaking from './LockedStaking' interface LockedStakingApyProps extends LockedStakingApyPropsType { showICake?: boolean @@ -48,26 +41,7 @@ const LockedStakingApy: React.FC> [userData], ) - const stakingToken = pool?.stakingToken - const stakingTokenPrice = pool?.stakingTokenPrice - const stakingTokenBalance = pool?.userData?.stakingTokenBalance - - const currentLockedAmountAsBigNumber = userData?.balance?.cakeAsBigNumber - - const currentLockedAmount = useMemo( - () => getBalanceNumber(currentLockedAmountAsBigNumber), - [currentLockedAmountAsBigNumber], - ) - - const usdValueStaked = useMemo( - () => - stakingToken && stakingTokenPrice - ? getBalanceNumber(currentLockedAmountAsBigNumber.multipliedBy(stakingTokenPrice), stakingToken?.decimals) - : null, - [currentLockedAmountAsBigNumber, stakingTokenPrice, stakingToken], - ) - - const { weekDuration, lockEndDate, secondDuration, remainingTime, burnStartTime } = useUserDataInVaultPresenter({ + const { weekDuration, secondDuration } = useUserDataInVaultPresenter({ lockStartTime: userData?.lockStartTime, lockEndTime: userData?.lockEndTime, burnStartTime: userData?.burnStartTime, @@ -77,8 +51,8 @@ const LockedStakingApy: React.FC> // earningTokenBalance includes overdue fee if any const earningTokenBalance = useMemo(() => { - return getBalanceNumber(currentLockedAmountAsBigNumber.minus(userData?.cakeAtLastUserAction)) - }, [currentLockedAmountAsBigNumber, userData?.cakeAtLastUserAction]) + return getBalanceNumber(userData?.balance?.cakeAsBigNumber.minus(userData?.cakeAtLastUserAction)) + }, [userData?.balance?.cakeAsBigNumber, userData?.cakeAtLastUserAction]) const boostedYieldAmount = useMemo(() => { return getFullDisplayBalance(userData?.cakeAtLastUserAction, 18, 5) @@ -87,28 +61,8 @@ const LockedStakingApy: React.FC> const tooltipContent = const { targetRef, tooltip, tooltipVisible } = useTooltip(tooltipContent, { placement: 'bottom-start' }) - const tooltipContentOfBurn = t( - 'After Burning starts at %burnStartTime%. You need to renew your fix-term position, to initiate a new lock or convert your staking position to flexible before it starts. Otherwise all the rewards will be burned within the next 90 days.', - { burnStartTime }, - ) - const { - targetRef: tagTargetRefOfBurn, - tooltip: tagTooltipOfBurn, - tooltipVisible: tagTooltipVisibleOfBurn, - } = useTooltip(tooltipContentOfBurn, { - placement: 'bottom', - }) - const originalLockedAmount = getBalanceNumber(userData?.lockedAmount) - const { - targetRef: tagTargetRefOfLocked, - tooltip: tagTooltipOfLocked, - tooltipVisible: tagTooltipVisibleOfLocked, - } = useTooltip(, { - placement: 'bottom', - }) - const { targetRef: tagTargetRefOfRecentProfit, tooltip: tagTooltipOfRecentProfit, @@ -119,57 +73,7 @@ const LockedStakingApy: React.FC> return ( - - - - {t('CAKE locked')} - - - - {tagTooltipVisibleOfLocked && tagTooltipOfLocked} - - - - - - - - - {t('Unlocks In')} - - - = VaultPosition.LockedEnd ? '#D67E0A' : 'text'} bold fontSize="16px"> - {position >= VaultPosition.LockedEnd ? t('Unlocked') : remainingTime} - - {tagTooltipVisibleOfBurn && tagTooltipOfBurn} - - - - - = VaultPosition.LockedEnd ? '#D67E0A' : 'text'} fontSize="12px"> - {t('On %date%', { date: lockEndDate })} - - - - - - + {![VaultPosition.LockedEnd, VaultPosition.AfterBurning].includes(position) && ( diff --git a/apps/web/src/views/Pools/components/PoolsTable/ActionPanel/AutoHarvest.tsx b/apps/web/src/views/Pools/components/PoolsTable/ActionPanel/AutoHarvest.tsx index b36cd089ec251..d22d48b418fcb 100644 --- a/apps/web/src/views/Pools/components/PoolsTable/ActionPanel/AutoHarvest.tsx +++ b/apps/web/src/views/Pools/components/PoolsTable/ActionPanel/AutoHarvest.tsx @@ -10,6 +10,8 @@ import { Pool, useTooltip, HelpIcon, + Button, + useModal, } from '@pancakeswap/uikit' import { useAccount } from 'wagmi' import { getCakeVaultEarnings } from 'views/Pools/helpers' @@ -19,6 +21,8 @@ import { VaultKey, DeserializedLockedCakeVault } from 'state/types' import { getVaultPosition, VaultPosition } from 'utils/cakePool' import { useVaultApy } from 'hooks/useVaultApy' import { Token } from '@pancakeswap/sdk' +import BenefitsModal from 'views/Pools/components/RevenueSharing/BenefitsModal' +import useVCake from 'views/Pools/hooks/useVCake' import { ActionContainer, ActionTitles, ActionContent, RowActionContainer } from './styles' import UnstakingFeeCountdownRow from '../../CakeVaultCard/UnstakingFeeCountdownRow' @@ -36,6 +40,7 @@ interface AutoHarvestActionProps { const AutoHarvestAction: React.FunctionComponent> = ({ pool }) => { const { t } = useTranslation() const { address: account } = useAccount() + const { isInitialization } = useVCake() const { isMobile } = useMatchBreakpoints() const { earningTokenPrice, vaultKey, userDataLoaded } = pool @@ -76,6 +81,13 @@ const AutoHarvestAction: React.FunctionComponent, + true, + false, + 'revenueModal', + ) + const actionTitle = ( {t('Recent CAKE profit')} @@ -105,76 +117,85 @@ const AutoHarvestAction: React.FunctionComponent - - {actionTitle} - - - <> - {hasAutoEarnings ? ( - <> - - - {tagTooltipVisibleOfRecentProfit && tagTooltipOfRecentProfit} - - - - - {Number.isFinite(earningTokenPrice) && earningTokenPrice > 0 && ( - - )} - - ) : ( - <> - 0 - - 0 USD - - - )} - - - - {[VaultPosition.Flexible, VaultPosition.None].includes(vaultPosition) && ( - - )} - {/* IFO credit here */} - - - - {!isMobile && vaultKey === VaultKey.CakeVault && (vaultData as DeserializedLockedCakeVault).userData.locked && ( - - - - {t('Yield boost')} - - + + + + {actionTitle} - - - {t('Lock for %duration%', { duration: weekDuration })} - + <> + {hasAutoEarnings ? ( + <> + + + {tagTooltipVisibleOfRecentProfit && tagTooltipOfRecentProfit} + + + + + {Number.isFinite(earningTokenPrice) && earningTokenPrice > 0 && ( + + )} + + ) : ( + <> + 0 + + 0 USD + + + )} + + + + {[VaultPosition.Flexible, VaultPosition.None].includes(vaultPosition) && ( + + )} - )} + {!isMobile && vaultKey === VaultKey.CakeVault && (vaultData as DeserializedLockedCakeVault).userData.locked && ( + + + + {t('Yield boost')} + + + + + + + {t('Lock for %duration%', { duration: weekDuration })} + + + + + )} + + {vaultKey === VaultKey.CakeVault && + (vaultData as DeserializedLockedCakeVault).userData.locked && + vaultPosition === VaultPosition.Locked && + isInitialization && ( + + )} ) } diff --git a/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/BenefitsText.tsx b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/BenefitsText.tsx new file mode 100644 index 0000000000000..64b320d9b9d8a --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/BenefitsText.tsx @@ -0,0 +1,28 @@ +import { ReactElement } from 'react' +import { Flex, Text } from '@pancakeswap/uikit' +import BenefitsTooltipsText from 'views/Pools/components/RevenueSharing/BenefitsModal/BenefitsTooltipsText' + +interface BenefitsTextProps { + title: string + value: string + icon: ReactElement + tooltipComponent?: ReactElement +} + +const BenefitsText: React.FC> = ({ + title, + value, + icon, + tooltipComponent, +}) => { + return ( + + + + + {value} + + ) +} + +export default BenefitsText diff --git a/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/BenefitsTooltipsText.tsx b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/BenefitsTooltipsText.tsx new file mode 100644 index 0000000000000..15867d0eef6b6 --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/BenefitsTooltipsText.tsx @@ -0,0 +1,35 @@ +import { ReactElement } from 'react' +import { Flex, TooltipText, useTooltip, useMatchBreakpoints } from '@pancakeswap/uikit' + +interface BenefitsTooltipsTextProps { + title: string + icon?: ReactElement + tooltipComponent?: ReactElement +} + +const BenefitsTooltipsText: React.FC> = ({ + title, + icon, + tooltipComponent, +}) => { + const { isMobile } = useMatchBreakpoints() + + const { targetRef, tooltipVisible, tooltip } = useTooltip(<>{tooltipComponent}, { + placement: 'bottom', + ...(isMobile && { hideTimeout: 2000 }), + }) + + return ( + <> + + {icon} + + {title} + + + {tooltipVisible && tooltip} + + ) +} + +export default BenefitsTooltipsText diff --git a/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/ClaimButton.tsx b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/ClaimButton.tsx new file mode 100644 index 0000000000000..adf5fd8b987cd --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/ClaimButton.tsx @@ -0,0 +1,53 @@ +import { useCallback, useMemo } from 'react' +import BigNumber from 'bignumber.js' +import { useTranslation } from '@pancakeswap/localization' +import { Button, useToast } from '@pancakeswap/uikit' +import useCatchTxError from 'hooks/useCatchTxError' +import { useRevenueSharingPoolContract } from 'hooks/useContract' +import { ToastDescriptionWithTx } from 'components/Toast' +import useAccountActiveChain from 'hooks/useAccountActiveChain' + +interface ClaimButtonProps { + availableClaim: string + onDismiss?: () => void +} + +const ClaimButton: React.FunctionComponent> = ({ + availableClaim, + onDismiss, +}) => { + const { t } = useTranslation() + const { toastSuccess } = useToast() + const { chainId, account } = useAccountActiveChain() + const contract = useRevenueSharingPoolContract({ chainId }) + const { fetchWithCatchTxError, loading: isPending } = useCatchTxError() + + const isReady = useMemo(() => new BigNumber(availableClaim).gt(0) && !isPending, [availableClaim, isPending]) + + const handleClaim = useCallback(async () => { + try { + const receipt = await fetchWithCatchTxError(() => contract.write.claim([account], { account, chainId })) + + if (receipt?.status) { + toastSuccess( + t('Success!'), + + {t('You have successfully claimed your rewards.')} + , + ) + + onDismiss?.() + } + } catch (error) { + console.error('[ERROR] Submit Revenue Claim Button', error) + } + }, [account, chainId, contract, fetchWithCatchTxError, onDismiss, t, toastSuccess]) + + return ( + + ) +} + +export default ClaimButton diff --git a/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/LockedBenefits.tsx b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/LockedBenefits.tsx new file mode 100644 index 0000000000000..a2766b1aaf5ff --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/LockedBenefits.tsx @@ -0,0 +1,113 @@ +import { useMemo } from 'react' +import { useTranslation } from '@pancakeswap/localization' +import { Box, Flex, Text, Card, ICakeIcon, BCakeIcon, VCakeIcon, NextLinkFromReactRouter } from '@pancakeswap/uikit' +import Image from 'next/image' +import BigNumber from 'bignumber.js' +import BenefitsText from 'views/Pools/components/RevenueSharing/BenefitsModal/BenefitsText' +import useCakeBenefits from 'components/Menu/UserMenu/hooks/useCakeBenefits' +import { useVaultApy } from 'hooks/useVaultApy' +import { VaultKey, DeserializedLockedCakeVault } from 'state/types' +import { useVaultPoolByKey } from 'state/pools/hooks' +import useUserDataInVaultPresenter from 'views/Pools/components/LockedPool/hooks/useUserDataInVaultPresenter' + +const LockedBenefits = () => { + const { t } = useTranslation() + const { data: cakeBenefits } = useCakeBenefits() + const { getLockedApy, getBoostFactor } = useVaultApy() + const { userData } = useVaultPoolByKey(VaultKey.CakeVault) as DeserializedLockedCakeVault + const { secondDuration } = useUserDataInVaultPresenter({ + lockStartTime: userData?.lockStartTime ?? '0', + lockEndTime: userData?.lockEndTime ?? '0', + }) + + const lockedApy = useMemo(() => getLockedApy(secondDuration), [getLockedApy, secondDuration]) + const boostFactor = useMemo(() => getBoostFactor(secondDuration), [getBoostFactor, secondDuration]) + const delApy = useMemo(() => new BigNumber(lockedApy).div(boostFactor).toNumber(), [lockedApy, boostFactor]) + + const iCakeTooltipComponent = () => ( + <> + + {t('iCAKE allows you to participate in the IFO public sales and commit up to %iCake% amount of CAKE.', { + iCake: cakeBenefits?.iCake, + })} + + + + {t('Learn More')} + + + + ) + + const bCakeTooltipComponent = () => ( + <> + {t('bCAKE allows you to boost your yield in PancakeSwap Farms by up to 2x.')} + + + {t('Learn More')} + + + + ) + + const vCakeTooltipComponent = () => ( + <> + + {t('vCAKE boosts your voting power to %totalScore% in the PancakeSwap voting governance.', { + totalScore: cakeBenefits?.vCake?.totalScore, + })} + + + + {t('Learn More')} + + + + ) + + return ( + + + lockCAKEbenefit + + + + + {t('locked benefits')} + + + + + {t('CAKE Yield')} + + + {`${Number(lockedApy).toFixed(2)}%`} + + {`${Number(delApy).toFixed(2)}%`} + + } + /> + } + /> + } + /> + + + + + ) +} + +export default LockedBenefits diff --git a/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/RevenueSharing.tsx b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/RevenueSharing.tsx new file mode 100644 index 0000000000000..bc61e9a879d80 --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/RevenueSharing.tsx @@ -0,0 +1,194 @@ +import { useMemo } from 'react' +import BigNumber from 'bignumber.js' +import { useTranslation } from '@pancakeswap/localization' +import { usePriceCakeUSD } from 'state/farms/hooks' +import { + Box, + Flex, + Text, + Card, + LinkExternal, + Message, + MessageText, + WarningIcon, + Balance, + NextLinkFromReactRouter, +} from '@pancakeswap/uikit' +import { ONE_WEEK_DEFAULT } from '@pancakeswap/pools' +import { useVaultPoolByKey } from 'state/pools/hooks' +import { timeFormat } from 'views/TradingReward/utils/timeFormat' +import useRevenueSharingPool from 'views/Pools/hooks/useRevenueSharingPool' +import { getBalanceAmount } from '@pancakeswap/utils/formatBalance' +import { distanceToNowStrict } from 'utils/timeHelper' +import { VaultKey, DeserializedLockedCakeVault } from 'state/types' +import ClaimButton from 'views/Pools/components/RevenueSharing/BenefitsModal/ClaimButton' +import BenefitsTooltipsText from 'views/Pools/components/RevenueSharing/BenefitsModal/BenefitsTooltipsText' + +interface RevenueSharingProps { + onDismiss?: () => void +} + +const RevenueSharing: React.FunctionComponent> = ({ onDismiss }) => { + const { + t, + currentLanguage: { locale }, + } = useTranslation() + const cakePriceBusd = usePriceCakeUSD() + const { userData } = useVaultPoolByKey(VaultKey.CakeVault) as DeserializedLockedCakeVault + + const { balanceOfAt, totalSupplyAt, nextDistributionTimestamp, lastTokenTimestamp, availableClaim } = + useRevenueSharingPool() + const yourShare = useMemo(() => getBalanceAmount(new BigNumber(balanceOfAt)).toNumber(), [balanceOfAt]) + const yourSharePercentage = useMemo( + () => new BigNumber(balanceOfAt).div(totalSupplyAt).times(100).toNumber() || 0, + [balanceOfAt, totalSupplyAt], + ) + + const availableCake = useMemo(() => getBalanceAmount(new BigNumber(availableClaim)).toNumber(), [availableClaim]) + const availableCakeUsdValue = useMemo( + () => new BigNumber(availableCake).times(cakePriceBusd).toNumber(), + [availableCake, cakePriceBusd], + ) + + const showExpireSoonWarning = useMemo(() => { + const endTime = new BigNumber(nextDistributionTimestamp).plus(ONE_WEEK_DEFAULT) + return new BigNumber(userData?.lockEndTime ?? '0').lt(endTime) + }, [nextDistributionTimestamp, userData?.lockEndTime]) + + const showNoCakeAmountWarning = useMemo( + () => new BigNumber(userData?.lockedAmount ?? '0').lte(0), + [userData?.lockedAmount], + ) + + return ( + + + + {t('revenue sharing')} + + + + + + {t('The virtual shares you currently own and the % against the whole revenue sharing pool.')} + + + {t( + 'Please note that after locking or updating, your shares will only update upon revenue distributions.', + )} + + + + {t('Learn More')} + + + + } + /> + + {yourShare === 0 ? ( + + 0 + + ) : ( + <> + {yourShare <= 0.01 ? ( + {`< 0.01`} + ) : ( + + )} + {yourSharePercentage <= 0.01 ? ( + {`(< 0.01%)`} + ) : ( + + )} + + )} + + + + + {t('Time remaining until the next revenue distribution and share updates.')} + } + /> + + {t('in %day%', { day: distanceToNowStrict(nextDistributionTimestamp * 1000) })} + + + {showExpireSoonWarning && ( + }> + + + {t( + 'Your fixed-term staking position will have less than 1 week in remaining duration upon the next distribution.', + )} + + + {t('Extend your stakings to receive shares in the next distribution.')} + + + + )} + + + {t('The time of the last revenue distribution and shares update.')}} + /> + {timeFormat(locale, lastTokenTimestamp)} + + + + {t('Amount of revenue available for claiming in CAKE.')}} + /> + + + + + + + {showNoCakeAmountWarning && ( + }> + + {t('You need to update your staking in order to start earning from protocol revenue sharing.')} + + + )} + + + {t('Learn More')} + + + + ) +} + +export default RevenueSharing diff --git a/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/SharingPoolNameCell.tsx b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/SharingPoolNameCell.tsx new file mode 100644 index 0000000000000..0e0a3c1e35e50 --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/SharingPoolNameCell.tsx @@ -0,0 +1,57 @@ +import { useMemo } from 'react' +import { useTranslation } from '@pancakeswap/localization' +import { Text, Flex, LogoRoundIcon, Box, Balance, Pool } from '@pancakeswap/uikit' +import { getBalanceNumber } from '@pancakeswap/utils/formatBalance' +import { useVaultPoolByKey, usePoolsWithVault } from 'state/pools/hooks' +import { VaultKey, DeserializedLockedCakeVault } from 'state/types' +import { Token } from '@pancakeswap/sdk' + +const SharingPoolNameCell = () => { + const { t } = useTranslation() + const { userData } = useVaultPoolByKey(VaultKey.CakeVault) as DeserializedLockedCakeVault + const { pools } = usePoolsWithVault() + + const cakePool = useMemo( + () => pools.find((pool) => pool.userData && pool.sousId === 0), + [pools], + ) as Pool.DeserializedPool + const stakingToken = cakePool?.stakingToken + const stakingTokenPrice = cakePool?.stakingTokenPrice + + const currentLockedAmountNumber = useMemo( + () => userData?.balance?.cakeAsNumberBalance, + [userData?.balance?.cakeAsNumberBalance], + ) + + const usdValueStaked = useMemo( + () => + stakingToken && stakingTokenPrice + ? getBalanceNumber(userData?.balance?.cakeAsBigNumber.multipliedBy(stakingTokenPrice), stakingToken?.decimals) + : null, + [userData?.balance?.cakeAsBigNumber, stakingTokenPrice, stakingToken], + ) + + return ( + + + + + {t('CAKE locked')} + + + + + + ) +} + +export default SharingPoolNameCell diff --git a/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/index.tsx b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/index.tsx new file mode 100644 index 0000000000000..e6fe95d08b8fa --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/BenefitsModal/index.tsx @@ -0,0 +1,99 @@ +import styled from 'styled-components' +import { useTranslation } from '@pancakeswap/localization' +import { AtomBox } from '@pancakeswap/ui' +import { + ModalContainer, + ModalCloseButton, + Text, + RowBetween, + ModalBody, + Flex, + ModalActions, + AutoColumn, + Pool, +} from '@pancakeswap/uikit' +import { useAccount } from 'wagmi' +import LockedBenefits from 'views/Pools/components/RevenueSharing/BenefitsModal/LockedBenefits' +import RevenueSharing from 'views/Pools/components/RevenueSharing/BenefitsModal/RevenueSharing' +import SharingPoolNameCell from 'views/Pools/components/RevenueSharing/BenefitsModal/SharingPoolNameCell' +import { Token } from '@pancakeswap/sdk' +import LockedActions from 'views/Pools/components/LockedPool/Common/LockedActions' +import { DeserializedLockedVaultUser } from 'state/types' + +const Container = styled(ModalContainer)` + width: 100%; + overflow: hidden; + max-height: 90vh; + + ${({ theme }) => theme.mediaQueries.md} { + width: 375px; + } +` + +const ScrollableContainer = styled(Flex)` + flex-direction: column; + height: auto; + ${({ theme }) => theme.mediaQueries.xs} { + max-height: 100vh; + } + ${({ theme }) => theme.mediaQueries.md} { + max-height: none; + } +` + +interface BenefitsModalProps { + pool: Pool.DeserializedPool + userData: DeserializedLockedVaultUser + onDismiss?: () => void +} + +const BenefitsModal: React.FunctionComponent> = ({ + pool, + userData, + onDismiss, +}) => { + const { t } = useTranslation() + + useAccount({ + onConnect: ({ connector }) => { + connector?.addListener('change', () => onDismiss?.()) + }, + onDisconnect: () => onDismiss?.(), + }) + + return ( + + + + + {t('Locked CAKE Benefits')} + + + + + + + + + + + + + + + + + + ) +} + +export default BenefitsModal diff --git a/apps/web/src/views/Pools/components/RevenueSharing/JoinRevenueModal/JoinButton.tsx b/apps/web/src/views/Pools/components/RevenueSharing/JoinRevenueModal/JoinButton.tsx new file mode 100644 index 0000000000000..6a09bea50ef8d --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/JoinRevenueModal/JoinButton.tsx @@ -0,0 +1,47 @@ +import { useCallback } from 'react' +import { Button, useToast } from '@pancakeswap/uikit' +import { useTranslation } from '@pancakeswap/localization' +import useCatchTxError from 'hooks/useCatchTxError' +import { useVCakeContract } from 'hooks/useContract' +import { ToastDescriptionWithTx } from 'components/Toast' +import useAccountActiveChain from 'hooks/useAccountActiveChain' + +interface JoinButtonProps { + refresh?: () => void + onDismiss?: () => void +} + +const JoinButton: React.FunctionComponent> = ({ refresh, onDismiss }) => { + const { t } = useTranslation() + const { toastSuccess } = useToast() + const { chainId } = useAccountActiveChain() + const vCakeContract = useVCakeContract({ chainId }) + const { fetchWithCatchTxError, loading: isPending } = useCatchTxError() + + const handleJoinButton = useCallback(async () => { + try { + const receipt = await fetchWithCatchTxError(() => vCakeContract.write.syncFromCakePool([])) + + if (receipt?.status) { + toastSuccess( + t('Success!'), + + {t('Joined Revenue Sharing Pool.')} + , + ) + await refresh?.() + onDismiss?.() + } + } catch (error) { + console.error('[ERROR] Submit vCake syncFromCakePool', error) + } + }, [fetchWithCatchTxError, onDismiss, refresh, t, toastSuccess, vCakeContract.write]) + + return ( + + ) +} + +export default JoinButton diff --git a/apps/web/src/views/Pools/components/RevenueSharing/JoinRevenueModal/VCakeModal.tsx b/apps/web/src/views/Pools/components/RevenueSharing/JoinRevenueModal/VCakeModal.tsx new file mode 100644 index 0000000000000..74d699c3e2176 --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/JoinRevenueModal/VCakeModal.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react' +import { ModalV2 } from '@pancakeswap/uikit' +import { ChainId } from '@pancakeswap/sdk' +import { useAccount } from 'wagmi' +import useAccountActiveChain from 'hooks/useAccountActiveChain' +import useVCake from 'views/Pools/hooks/useVCake' +import JoinRevenueModal from 'views/Pools/components/RevenueSharing/JoinRevenueModal' +import useCakeBenefits from 'components/Menu/UserMenu/hooks/useCakeBenefits' +import { VaultPosition } from 'utils/cakePool' +import { FetchStatus } from 'config/constants/types' + +const VCakeModal = () => { + const { account, chainId } = useAccountActiveChain() + const { isInitialization, refresh } = useVCake() + const [open, setOpen] = useState(false) + const { data: cakeBenefits, status: cakeBenefitsFetchStatus } = useCakeBenefits() + + useEffect(() => { + if ( + account && + chainId === ChainId.BSC && + isInitialization === false && + cakeBenefitsFetchStatus === FetchStatus.Fetched && + cakeBenefits?.lockPosition === VaultPosition.Locked + ) { + setOpen(true) + } + }, [account, cakeBenefits?.lockPosition, cakeBenefitsFetchStatus, chainId, isInitialization]) + + useAccount({ + onConnect: ({ connector }) => { + connector?.addListener('change', () => closeModal()) + }, + onDisconnect: () => closeModal(), + }) + + const closeModal = () => { + setOpen(false) + } + + return ( + closeModal()}> + closeModal()} /> + + ) +} + +export default VCakeModal diff --git a/apps/web/src/views/Pools/components/RevenueSharing/JoinRevenueModal/index.tsx b/apps/web/src/views/Pools/components/RevenueSharing/JoinRevenueModal/index.tsx new file mode 100644 index 0000000000000..ae201f877d11a --- /dev/null +++ b/apps/web/src/views/Pools/components/RevenueSharing/JoinRevenueModal/index.tsx @@ -0,0 +1,126 @@ +import { useMemo } from 'react' +import styled from 'styled-components' +import { Modal, Text, Card, Flex, Box, LinkExternal, useMatchBreakpoints, Pool } from '@pancakeswap/uikit' +import Image from 'next/image' +import useTheme from 'hooks/useTheme' +import { Token } from '@pancakeswap/sdk' +import { useTranslation } from '@pancakeswap/localization' +import JoinButton from 'views/Pools/components/RevenueSharing/JoinRevenueModal/JoinButton' +import { useVaultPoolByKey, usePoolsWithVault } from 'state/pools/hooks' +import { VaultKey, DeserializedLockedCakeVault } from 'state/types' +import LockedStaking from 'views/Pools/components/LockedPool/LockedStaking' + +interface JoinRevenueModalProps { + refresh?: () => void + onDismiss?: () => void +} + +const TooltipContainer = styled(Box)` + position: relative; + padding: 16px; + max-width: 264px; + margin: 0 0 10px 10px; + height: fit-content; + border-radius: 16px; + background-color: ${({ theme }) => (theme.isDark ? '#FFFFFF' : '#27262c')}; + + ${Text} { + color: ${({ theme }) => (theme.isDark ? '#280D5F' : '#F4EEFF')}; + } + + &:before { + content: ''; + position: absolute; + bottom: 25%; + right: -6px; + width: 0; + height: 0; + border-top: 10px solid transparent; + border-bottom: 10px solid transparent; + border-left: ${({ theme }) => (theme.isDark ? '10px solid #FFFFFF' : '10px solid #27262c')}; + } + + ${({ theme }) => theme.mediaQueries.sm} { + margin: 0 0 0 10px; + } +` + +const InlineLink = styled(LinkExternal)` + display: inline-flex; + margin: 0 4px; + + svg { + width: 16px; + } +` + +const JoinRevenueModal: React.FunctionComponent> = ({ + refresh, + onDismiss, +}) => { + const { t } = useTranslation() + const { theme } = useTheme() + const { isMobile } = useMatchBreakpoints() + + const { pools } = usePoolsWithVault() + const cakePool = useMemo( + () => pools.find((pool) => pool.userData && pool.sousId === 0), + [pools], + ) as Pool.DeserializedPool + + const vaultPool = useVaultPoolByKey(VaultKey.CakeVault) as DeserializedLockedCakeVault + + return ( + + + + + + { + 'Update your CAKE staking position to join the revenue sharing program for weekly revenue sharing distributions! ' + } + + + lockCakeTooltip + + {!vaultPool?.userData?.isLoading && ( + + + + + + )} + + + + {t('Once updated, you need to wait a full distribution cycle to start claiming rewards.')} + + + {t('Learn More')} + + + + + ) +} + +export default JoinRevenueModal diff --git a/apps/web/src/views/Pools/hooks/useRevenueSharingPool.ts b/apps/web/src/views/Pools/hooks/useRevenueSharingPool.ts new file mode 100644 index 0000000000000..8c64237b83cb1 --- /dev/null +++ b/apps/web/src/views/Pools/hooks/useRevenueSharingPool.ts @@ -0,0 +1,87 @@ +import useSWR from 'swr' +import useAccountActiveChain from 'hooks/useAccountActiveChain' +import { publicClient } from 'utils/wagmi' +import { useRevenueSharingPoolContract } from 'hooks/useContract' +import { getRevenueSharingPoolAddress } from 'utils/addressHelpers' +import { Address } from 'viem' +import { revenueSharingPoolABI } from 'config/abi/revenueSharingPool' +import { ONE_WEEK_DEFAULT } from '@pancakeswap/pools' +import BigNumber from 'bignumber.js' +import { useInitialBlockTimestamp } from 'state/block/hooks' + +interface RevenueSharingPool { + balanceOfAt: string + totalSupplyAt: string + nextDistributionTimestamp: number + lastTokenTimestamp: number + availableClaim: string +} + +const initialData: RevenueSharingPool = { + balanceOfAt: '0', + totalSupplyAt: '0', + nextDistributionTimestamp: 0, + lastTokenTimestamp: 0, + availableClaim: '0', +} + +const useRevenueSharingPool = (): RevenueSharingPool => { + const { account, chainId } = useAccountActiveChain() + const contract = useRevenueSharingPoolContract({ chainId }) + const contractAddress = getRevenueSharingPoolAddress(chainId) + const blockTimestamp = useInitialBlockTimestamp() + + const { data } = useSWR(account && chainId && ['/revenue-sharing-pool', account, chainId], async () => { + try { + const now = Math.floor(blockTimestamp / ONE_WEEK_DEFAULT) * ONE_WEEK_DEFAULT + const revenueCalls = [ + { + functionName: 'balanceOfAt', + address: contractAddress as Address, + abi: revenueSharingPoolABI, + args: [account, now], + }, + { + functionName: 'totalSupplyAt', + address: contractAddress as Address, + abi: revenueSharingPoolABI, + args: [now], + }, + { + functionName: 'lastTokenTimestamp', + address: contractAddress as Address, + abi: revenueSharingPoolABI, + args: [], + }, + ] + + const client = publicClient({ chainId }) + const [revenueResult, claimResult] = await Promise.all([ + client.multicall({ + contracts: revenueCalls, + allowFailure: true, + }), + contract.simulate.claim([account]), + ]) + + const nextDistributionTimestamp = new BigNumber(revenueResult[2].result.toString()) + .plus(ONE_WEEK_DEFAULT) + .toNumber() + + return { + balanceOfAt: revenueResult[0].result.toString(), + totalSupplyAt: revenueResult[1].result.toString(), + nextDistributionTimestamp, + lastTokenTimestamp: Number(revenueResult[2].result.toString()), + availableClaim: claimResult.result.toString(), + } + } catch (error) { + console.error('[ERROR] Fetching Revenue Sharing Pool', error) + return initialData + } + }) + + return data ?? initialData +} + +export default useRevenueSharingPool diff --git a/apps/web/src/views/Pools/hooks/useVCake.tsx b/apps/web/src/views/Pools/hooks/useVCake.tsx new file mode 100644 index 0000000000000..f3c328debb00b --- /dev/null +++ b/apps/web/src/views/Pools/hooks/useVCake.tsx @@ -0,0 +1,34 @@ +import useSWR from 'swr' +import { ChainId } from '@pancakeswap/sdk' +import useAccountActiveChain from 'hooks/useAccountActiveChain' +import { useVCakeContract } from 'hooks/useContract' + +interface UseVCake { + isInitialization: null | boolean + refresh: () => void +} + +const useVCake = (): UseVCake => { + const { account, chainId } = useAccountActiveChain() + const vCakeContract = useVCakeContract({ chainId }) + + const { data, mutate } = useSWR( + account && chainId === ChainId.BSC && ['/v-cake-initialization', account, chainId], + async () => { + try { + const initialization = await vCakeContract.read.initialization([account]) + return initialization + } catch (error) { + console.error('[ERROR] Fetching vCake initialization', error) + return null + } + }, + ) + + return { + isInitialization: data, + refresh: mutate, + } +} + +export default useVCake diff --git a/apps/web/src/views/Pools/index.tsx b/apps/web/src/views/Pools/index.tsx index f61235e2c0163..a566ff938d289 100644 --- a/apps/web/src/views/Pools/index.tsx +++ b/apps/web/src/views/Pools/index.tsx @@ -10,6 +10,7 @@ import { ChainId, Token } from '@pancakeswap/sdk' import { TokenPairImage } from 'components/TokenImage' import { useActiveChainId } from 'hooks/useActiveChainId' import { V3SubgraphHealthIndicator } from 'components/SubgraphHealthIndicator' +import VCakeModal from 'views/Pools/components/RevenueSharing/JoinRevenueModal/VCakeModal' import CardActions from './components/PoolCard/CardActions' import AprRow from './components/PoolCard/AprRow' @@ -46,6 +47,7 @@ const Pools: React.FC = () => { return ( <> + diff --git a/packages/localization/src/config/translations.json b/packages/localization/src/config/translations.json index 73a13f9fb89df..f5a296adf07ba 100644 --- a/packages/localization/src/config/translations.json +++ b/packages/localization/src/config/translations.json @@ -2613,5 +2613,26 @@ "Swap and Provide Liquidity on zkSync Era Now": "Swap and Provide Liquidity on zkSync Era Now", "PancakeSwap Now Live on zkSync Era!": "PancakeSwap Now Live on zkSync Era!", "Zksync Lormips LIVE!": "Zksync Lormips LIVE!", - "Claim Your NFT": "Claim Your NFT" + "Claim Your NFT": "Claim Your NFT", + "View Benefits": "View Benefits", + "locked benefits": "locked benefits", + "CAKE Yield": "CAKE Yield", + "revenue sharing": "revenue sharing", + "Your shares": "Your shares", + "Next distribution": "Next distribution", + "in %day%": "in %day%", + "Last distribution": "Last distribution", + "You need to update your staking in order to start earning from protocol revenue sharing.": "You need to update your staking in order to start earning from protocol revenue sharing.", + "Locked CAKE Benefits": "Locked CAKE Benefits", + "Joined Revenue Sharing Pool.": "Joined Revenue Sharing Pool.", + "Update Staking Position": "Update Staking Position", + "Join revenue sharing": "Join revenue sharing", + "Once updated, you need to wait a full distribution cycle to start claiming rewards.": "Once updated, you need to wait a full distribution cycle to start claiming rewards.", + "The virtual shares you currently own and the % against the whole revenue sharing pool.": "The virtual shares you currently own and the % against the whole revenue sharing pool.", + "Please note that after locking or updating, your shares will only update upon revenue distributions.": "Please note that after locking or updating, your shares will only update upon revenue distributions.", + "Time remaining until the next revenue distribution and share updates.": "Time remaining until the next revenue distribution and share updates.", + "The time of the last revenue distribution and shares update.": "The time of the last revenue distribution and shares update.", + "Amount of revenue available for claiming in CAKE.": "Amount of revenue available for claiming in CAKE.", + "Extend your stakings to receive shares in the next distribution.": "Extend your stakings to receive shares in the next distribution.", + "Your fixed-term staking position will have less than 1 week in remaining duration upon the next distribution.": "Your fixed-term staking position will have less than 1 week in remaining duration upon the next distribution." } diff --git a/packages/uikit/src/components/Svg/Icons/BCake.tsx b/packages/uikit/src/components/Svg/Icons/BCake.tsx new file mode 100644 index 0000000000000..9331488c5982a --- /dev/null +++ b/packages/uikit/src/components/Svg/Icons/BCake.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import { useTheme } from "styled-components"; +import Svg from "../Svg"; +import { SvgProps } from "../types"; + +const Icon: React.FC> = (props) => { + const theme = useTheme(); + const primaryColor = theme.isDark ? "#372F47" : "#EEEAF4"; + const secondaryColor = theme.isDark ? "#B8ADD2" : "#7A6EAA"; + + return ( + + + + + + ); +}; + +export default Icon; diff --git a/packages/uikit/src/components/Svg/Icons/ICake.tsx b/packages/uikit/src/components/Svg/Icons/ICake.tsx new file mode 100644 index 0000000000000..4fa35d6859275 --- /dev/null +++ b/packages/uikit/src/components/Svg/Icons/ICake.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { useTheme } from "styled-components"; +import Svg from "../Svg"; +import { SvgProps } from "../types"; + +const Icon: React.FC> = (props) => { + const theme = useTheme(); + const primaryColor = theme.isDark ? "#372F47" : "#EEEAF4"; + const secondaryColor = theme.isDark ? "#B8ADD2" : "#7A6EAA"; + + return ( + + + + + + + ); +}; + +export default Icon; diff --git a/packages/uikit/src/components/Svg/Icons/VCake.tsx b/packages/uikit/src/components/Svg/Icons/VCake.tsx new file mode 100644 index 0000000000000..a0c5a7bd50cd8 --- /dev/null +++ b/packages/uikit/src/components/Svg/Icons/VCake.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { useTheme } from "styled-components"; +import Svg from "../Svg"; +import { SvgProps } from "../types"; + +const Icon: React.FC> = (props) => { + const theme = useTheme(); + const primaryColor = theme.isDark ? "#372F47" : "#EEEAF4"; + const secondaryColor = theme.isDark ? "#B8ADD2" : "#7A6EAA"; + + return ( + + + + + + + ); +}; + +export default Icon; diff --git a/packages/uikit/src/components/Svg/index.tsx b/packages/uikit/src/components/Svg/index.tsx index 686ab74839729..3184f3d8b8b26 100644 --- a/packages/uikit/src/components/Svg/index.tsx +++ b/packages/uikit/src/components/Svg/index.tsx @@ -174,4 +174,7 @@ export { default as ZoomInIcon } from "./Icons/ZoomIn"; export { default as ZoomOutIcon } from "./Icons/ZoomOut"; export { default as EthChainIcon } from "./Icons/EthChain"; export { default as PancakeProtectorIcon } from "./Icons/PancakeProtector"; +export { default as ICakeIcon } from "./Icons/ICake"; +export { default as BCakeIcon } from "./Icons/BCake"; +export { default as VCakeIcon } from "./Icons/VCake"; export type { SvgProps } from "./types";