diff --git a/apps/web/src/components/Menu/config/config.ts b/apps/web/src/components/Menu/config/config.ts index 9513c33198853..1dfe75e27b45c 100644 --- a/apps/web/src/components/Menu/config/config.ts +++ b/apps/web/src/components/Menu/config/config.ts @@ -13,6 +13,7 @@ import { SwapIcon, } from '@pancakeswap/uikit' import { + FIXED_STAKING_SUPPORTED_CHAINS, LIQUID_STAKING_SUPPORTED_CHAINS, SUPPORT_BUY_CRYPTO, SUPPORT_FARMS, @@ -117,6 +118,11 @@ const config: ( href: '/liquid-staking', supportChainIds: LIQUID_STAKING_SUPPORTED_CHAINS, }, + { + label: t('Simple Staking'), + href: '/simple-staking', + supportChainIds: FIXED_STAKING_SUPPORTED_CHAINS, + }, ].map((item) => addMenuItemSupported(item, chainId)), }, { diff --git a/apps/web/src/config/abi/fixedStaking.ts b/apps/web/src/config/abi/fixedStaking.ts new file mode 100644 index 0000000000000..5415bc1766df6 --- /dev/null +++ b/apps/web/src/config/abi/fixedStaking.ts @@ -0,0 +1,1105 @@ +export const fixedStakingABI = [ + { + inputs: [], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'user', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'poolIndex', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'uint128', + name: 'amount', + type: 'uint128', + }, + ], + name: 'Deposit', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'user', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'poolIndex', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'uint128', + name: 'amount', + type: 'uint128', + }, + ], + name: 'Harvest', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint8', + name: 'version', + type: 'uint8', + }, + ], + name: 'Initialized', + 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', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Paused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'user', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'poolIndex', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint128', + name: 'accumAmount', + type: 'uint128', + }, + ], + name: 'PendingWithdraw', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'uint32', + name: 'lockPeriod', + type: 'uint32', + }, + { + indexed: false, + internalType: 'uint256', + name: 'poolIndex', + type: 'uint256', + }, + ], + name: 'PoolAdded', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'poolIndex', + type: 'uint256', + }, + { + indexed: false, + internalType: 'bool', + name: 'state', + type: 'bool', + }, + ], + name: 'PoolChangeState', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'poolIndex', + type: 'uint256', + }, + ], + name: 'PoolChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'to', + type: 'address', + }, + ], + name: 'TokenWithdraw', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'Unpaused', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'user', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'poolIndex', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'token', + type: 'address', + }, + { + indexed: false, + internalType: 'uint128', + name: 'amount', + type: 'uint128', + }, + ], + name: 'Withdraw', + type: 'event', + }, + { + inputs: [], + name: 'PERCENT_BASE', + outputs: [ + { + internalType: 'uint128', + name: '', + type: 'uint128', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + components: [ + { + internalType: 'contract IERC20Upgradeable', + name: 'token', + type: 'address', + }, + { + internalType: 'uint32', + name: 'endDay', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'lockDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'boostDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'unlockDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'lockPeriod', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'withdrawalCut1', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'withdrawalCut2', + type: 'uint32', + }, + { + internalType: 'bool', + name: 'depositEnabled', + type: 'bool', + }, + { + internalType: 'uint128', + name: 'maxDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'minDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'totalDeposited', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'maxPoolAmount', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'minBoostAmount', + type: 'uint128', + }, + ], + internalType: 'struct PancakeFixedStaking.Pool', + name: '_pool', + type: 'tuple', + }, + ], + name: 'addPool', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'cakePool', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_poolIndex', + type: 'uint256', + }, + { + components: [ + { + internalType: 'contract IERC20Upgradeable', + name: 'token', + type: 'address', + }, + { + internalType: 'uint32', + name: 'endDay', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'lockDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'boostDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'unlockDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'lockPeriod', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'withdrawalCut1', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'withdrawalCut2', + type: 'uint32', + }, + { + internalType: 'bool', + name: 'depositEnabled', + type: 'bool', + }, + { + internalType: 'uint128', + name: 'maxDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'minDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'totalDeposited', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'maxPoolAmount', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'minBoostAmount', + type: 'uint128', + }, + ], + internalType: 'struct PancakeFixedStaking.Pool', + name: '_pool', + type: 'tuple', + }, + ], + name: 'changePool', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint32', + name: '', + type: 'uint32', + }, + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'dailyDeposit', + outputs: [ + { + internalType: 'uint128', + name: '', + type: 'uint128', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint32', + name: '', + type: 'uint32', + }, + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'dailyWithdraw', + outputs: [ + { + internalType: 'uint128', + name: '', + type: 'uint128', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_poolIndex', + type: 'uint256', + }, + { + internalType: 'uint128', + name: '_amount', + type: 'uint128', + }, + ], + name: 'deposit', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'earn', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getCurrentDay', + outputs: [ + { + internalType: 'uint32', + name: 'currentDay', + type: 'uint32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'poolId', + type: 'uint256', + }, + { + internalType: 'uint32', + name: 'firstDay', + type: 'uint32', + }, + { + internalType: 'uint256', + name: 'count', + type: 'uint256', + }, + ], + name: 'getDailyBalances', + outputs: [ + { + internalType: 'uint128[]', + name: '_deposit', + type: 'uint128[]', + }, + { + internalType: 'uint128[]', + name: '_withdraw', + type: 'uint128[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_poolIndex', + type: 'uint256', + }, + { + internalType: 'address', + name: '_user', + type: 'address', + }, + ], + name: 'getUserInfo', + outputs: [ + { + components: [ + { + components: [ + { + internalType: 'contract IERC20Upgradeable', + name: 'token', + type: 'address', + }, + { + internalType: 'uint32', + name: 'endDay', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'lockDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'boostDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'unlockDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'lockPeriod', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'withdrawalCut1', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'withdrawalCut2', + type: 'uint32', + }, + { + internalType: 'bool', + name: 'depositEnabled', + type: 'bool', + }, + { + internalType: 'uint128', + name: 'maxDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'minDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'totalDeposited', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'maxPoolAmount', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'minBoostAmount', + type: 'uint128', + }, + ], + internalType: 'struct PancakeFixedStaking.Pool', + name: 'pool', + type: 'tuple', + }, + { + components: [ + { + internalType: 'uint128', + name: 'userDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'accrueInterest', + type: 'uint128', + }, + { + internalType: 'uint32', + name: 'lastDayAction', + type: 'uint32', + }, + { + internalType: 'bool', + name: 'boost', + type: 'bool', + }, + ], + internalType: 'struct PancakeFixedStaking.UserInfo', + name: 'userInfo', + type: 'tuple', + }, + { + internalType: 'uint32', + name: 'endLockTime', + type: 'uint32', + }, + ], + internalType: 'struct PancakeFixedStaking.InfoFront', + name: 'info', + type: 'tuple', + }, + { + internalType: 'uint32', + name: 'currentDay', + type: 'uint32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_poolIndex', + type: 'uint256', + }, + ], + name: 'harvest', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '_cakePool', + type: 'address', + }, + { + internalType: 'address', + name: '_earn', + type: 'address', + }, + ], + name: 'initialize', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'pause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'paused', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint32', + name: '', + type: 'uint32', + }, + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'pendingWithdraw', + outputs: [ + { + internalType: 'uint128', + name: '', + type: 'uint128', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'poolLength', + outputs: [ + { + internalType: 'uint256', + name: 'length', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'pools', + outputs: [ + { + internalType: 'contract IERC20Upgradeable', + name: 'token', + type: 'address', + }, + { + internalType: 'uint32', + name: 'endDay', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'lockDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'boostDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'unlockDayPercent', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'lockPeriod', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'withdrawalCut1', + type: 'uint32', + }, + { + internalType: 'uint32', + name: 'withdrawalCut2', + type: 'uint32', + }, + { + internalType: 'bool', + name: 'depositEnabled', + type: 'bool', + }, + { + internalType: 'uint128', + name: 'maxDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'minDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'totalDeposited', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'maxPoolAmount', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'minBoostAmount', + type: 'uint128', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'renounceOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '_newCakePool', + type: 'address', + }, + ], + name: 'setCakePool', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '_newEarn', + type: 'address', + }, + ], + name: 'setEarn', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_poolIndex', + type: 'uint256', + }, + { + internalType: 'uint32', + name: '_endDay', + type: 'uint32', + }, + ], + name: 'setPoolEndDay', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_poolIndex', + type: 'uint256', + }, + { + internalType: 'bool', + name: '_state', + type: 'bool', + }, + ], + name: 'setPoolState', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'newOwner', + type: 'address', + }, + ], + name: 'transferOwnership', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'unpause', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'userInfo', + outputs: [ + { + internalType: 'uint128', + name: 'userDeposit', + type: 'uint128', + }, + { + internalType: 'uint128', + name: 'accrueInterest', + type: 'uint128', + }, + { + internalType: 'uint32', + name: 'lastDayAction', + type: 'uint32', + }, + { + internalType: 'bool', + name: 'boost', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + name: 'userPendingWithdraw', + outputs: [ + { + internalType: 'uint32', + name: '', + type: 'uint32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint256', + name: '_poolIndex', + type: 'uint256', + }, + ], + name: 'withdraw', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'contract IERC20Upgradeable', + name: '_token', + type: 'address', + }, + { + internalType: 'uint256', + name: '_amount', + type: 'uint256', + }, + { + internalType: 'address', + name: '_to', + type: 'address', + }, + ], + name: 'withdrawToken', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] diff --git a/apps/web/src/config/constants/contracts.ts b/apps/web/src/config/constants/contracts.ts index dc8369150ecdb..18c8e1b300941 100644 --- a/apps/web/src/config/constants/contracts.ts +++ b/apps/web/src/config/constants/contracts.ts @@ -260,4 +260,9 @@ export default { [ChainId.BSC]: '0x0a073aa17275ef839ee77BC6c589D9E661270480', [ChainId.BSC_TESTNET]: '0x', }, + fixedStaking: { + [ChainId.ETHEREUM]: '0x', + [ChainId.BSC]: '0xC0E92c9B437734a0c0e0466F76cDf71c5478b0AB', + [ChainId.BSC_TESTNET]: '0x', + }, } as const satisfies Record> diff --git a/apps/web/src/config/constants/supportChains.ts b/apps/web/src/config/constants/supportChains.ts index 73437c012233c..0b913e279f42f 100644 --- a/apps/web/src/config/constants/supportChains.ts +++ b/apps/web/src/config/constants/supportChains.ts @@ -31,3 +31,4 @@ export const LIQUID_STAKING_SUPPORTED_CHAINS = [ ChainId.BSC_TESTNET, ChainId.ARBITRUM_GOERLI, ] +export const FIXED_STAKING_SUPPORTED_CHAINS = [ChainId.BSC] diff --git a/apps/web/src/config/constants/tokenLists/pancake-default.tokenlist.json b/apps/web/src/config/constants/tokenLists/pancake-default.tokenlist.json index 99522b18c2943..cdb11edc332f4 100644 --- a/apps/web/src/config/constants/tokenLists/pancake-default.tokenlist.json +++ b/apps/web/src/config/constants/tokenLists/pancake-default.tokenlist.json @@ -154,6 +154,14 @@ "decimals": 18, "logoURI": "https://pancakeswap.finance/images/tokens/0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82.png" }, + { + "name": "PancakeSwap Token", + "symbol": "CAKE3", + "address": "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56", + "chainId": 97, + "decimals": 18, + "logoURI": "https://pancakeswap.finance/images/tokens/0x0E09FaBB73Bd3Ade0a17ECC321fD13a19e81cE82.png" + }, { "name": "Test BUSD Token", "symbol": "tBUSD", diff --git a/apps/web/src/hooks/useContract.ts b/apps/web/src/hooks/useContract.ts index 57adfef3959b5..e4936149740d5 100644 --- a/apps/web/src/hooks/useContract.ts +++ b/apps/web/src/hooks/useContract.ts @@ -49,6 +49,7 @@ import { getVCakeContract, getRevenueSharingPoolContract, getAnniversaryAchievementContract, + getFixedStakingContract, } from 'utils/contractHelpers' import { WNATIVE, pancakePairV2ABI } from '@pancakeswap/sdk' @@ -426,3 +427,11 @@ export const useAnniversaryAchievementContract = ({ chainId: chainId_ }: { chain [signer, chainId_, chainId], ) } + +export const useFixedStakingContract = () => { + const { chainId } = useActiveChainId() + + const { data: signer } = useWalletClient() + + return useMemo(() => getFixedStakingContract(signer, chainId), [chainId, signer]) +} diff --git a/apps/web/src/pages/simple-staking/index.tsx b/apps/web/src/pages/simple-staking/index.tsx new file mode 100644 index 0000000000000..ef922d4b911c5 --- /dev/null +++ b/apps/web/src/pages/simple-staking/index.tsx @@ -0,0 +1,10 @@ +import { ChainId } from '@pancakeswap/chains' +import FixedStaking from 'views/FixedStaking' + +const FixedStakingPage = () => { + return +} + +FixedStakingPage.chains = [ChainId.BSC] + +export default FixedStakingPage diff --git a/apps/web/src/utils/addressHelpers.ts b/apps/web/src/utils/addressHelpers.ts index 45ff41d15a8e1..6a83e2e779f49 100644 --- a/apps/web/src/utils/addressHelpers.ts +++ b/apps/web/src/utils/addressHelpers.ts @@ -152,3 +152,7 @@ export const getRevenueSharingPoolAddress = (chainId?: number) => { export const getAnniversaryAchievementAddress = (chainId?: number) => { return getAddressFromMap(addresses.anniversaryAchievement, chainId) } + +export const getFixedStakingAddress = (chainId?: number) => { + return getAddressFromMap(addresses.fixedStaking, chainId) +} diff --git a/apps/web/src/utils/contractHelpers.ts b/apps/web/src/utils/contractHelpers.ts index a65c18b6b5236..c43e260cded90 100644 --- a/apps/web/src/utils/contractHelpers.ts +++ b/apps/web/src/utils/contractHelpers.ts @@ -35,6 +35,7 @@ import { getVCakeAddress, getRevenueSharingPoolAddress, getAnniversaryAchievementAddress, + getFixedStakingAddress, } from 'utils/addressHelpers' // ABI @@ -87,6 +88,7 @@ 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' +import { fixedStakingABI } from 'config/abi/fixedStaking' export const getContract = ({ abi, @@ -417,6 +419,13 @@ export const getAnniversaryAchievementContract = (signer?: WalletClient, chainId return getContract({ abi: anniversaryAchievementABI, address: getAnniversaryAchievementAddress(chainId), + }) +} + +export const getFixedStakingContract = (signer?: WalletClient, chainId?: number) => { + return getContract({ + abi: fixedStakingABI, + address: getFixedStakingAddress(chainId), signer, chainId, }) diff --git a/apps/web/src/utils/formatTime.ts b/apps/web/src/utils/formatTime.ts new file mode 100644 index 0000000000000..7f93f9fd2d23e --- /dev/null +++ b/apps/web/src/utils/formatTime.ts @@ -0,0 +1,5 @@ +import { format } from 'date-fns' + +export function formatTime(time: number | Date) { + return format(time, 'MMM d, yyyy HH:mm') +} diff --git a/apps/web/src/utils/getLiquidityUrlPathParts.ts b/apps/web/src/utils/getLiquidityUrlPathParts.ts index 48a4888ee7edc..e709abb823e2f 100644 --- a/apps/web/src/utils/getLiquidityUrlPathParts.ts +++ b/apps/web/src/utils/getLiquidityUrlPathParts.ts @@ -10,7 +10,7 @@ const getLiquidityUrlPathParts = ({ feeAmount, chainId, }: { - quoteTokenAddress: string + quoteTokenAddress?: string tokenAddress: string feeAmount?: FeeAmount chainId: number diff --git a/apps/web/src/views/BuyCrypto/components/AccordionDropdown/AccordionItem.tsx b/apps/web/src/views/BuyCrypto/components/AccordionDropdown/AccordionItem.tsx index 4f96b45a669a1..f158616f62671 100644 --- a/apps/web/src/views/BuyCrypto/components/AccordionDropdown/AccordionItem.tsx +++ b/apps/web/src/views/BuyCrypto/components/AccordionDropdown/AccordionItem.tsx @@ -1,16 +1,16 @@ -import { useTranslation } from '@pancakeswap/localization' import { Box, Flex, InfoFilledIcon, RowBetween, Text, TooltipText, useTooltip } from '@pancakeswap/uikit' import getTimePeriods from '@pancakeswap/utils/getTimePeriods' import { CryptoCard } from 'components/Card' import { FiatOnRampModalButton } from 'components/FiatOnRampModal/FiatOnRampModal' import Image from 'next/image' import { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react' -import { isMobile } from 'react-device-detect' +import { getRefValue } from 'views/BuyCrypto/hooks/useGetRefValue' +import { CryptoFormView, ProviderQuote } from 'views/BuyCrypto/types' import { styled } from 'styled-components' +import { useTranslation } from '@pancakeswap/localization' +import { isMobile } from 'react-device-detect' import formatLocaleNumber from 'utils/formatLocaleNumber' import { CURRENT_CAMPAIGN_TIMESTAMP, ONRAMP_PROVIDERS, providerFeeTypes } from 'views/BuyCrypto/constants' -import { getRefValue } from 'views/BuyCrypto/hooks/useGetRefValue' -import { CryptoFormView, ProviderQuote } from 'views/BuyCrypto/types' import pocketWatch from '../../../../../public/images/pocket-watch.svg' import OnRampProviderLogo from '../OnRampProviderLogo/OnRampProviderLogo' diff --git a/apps/web/src/views/Farms/components/FarmCard/V3/SingleFarmV3Card.tsx b/apps/web/src/views/Farms/components/FarmCard/V3/SingleFarmV3Card.tsx index bf1c6166385c4..0f432579ed066 100644 --- a/apps/web/src/views/Farms/components/FarmCard/V3/SingleFarmV3Card.tsx +++ b/apps/web/src/views/Farms/components/FarmCard/V3/SingleFarmV3Card.tsx @@ -44,7 +44,7 @@ import FarmV3StakeAndUnStake, { FarmV3LPPosition, FarmV3LPPositionDetail, FarmV3 const { FarmV3HarvestAction } = FarmWidget.FarmV3Table -const ActionContainer = styled(Flex)` +export const ActionContainer = styled(Flex)` width: 100%; border: 2px solid ${({ theme }) => theme.colors.input}; border-radius: 16px; diff --git a/apps/web/src/views/FixedStaking/components/AmountWithUSDSub.tsx b/apps/web/src/views/FixedStaking/components/AmountWithUSDSub.tsx new file mode 100644 index 0000000000000..279cb6c7b1f4c --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/AmountWithUSDSub.tsx @@ -0,0 +1,31 @@ +import { CurrencyAmount, Token } from '@pancakeswap/swap-sdk-core' +import { Balance, Text } from '@pancakeswap/uikit' + +import { useStablecoinPriceAmount } from 'hooks/useBUSDPrice' +import toNumber from 'lodash/toNumber' +import React from 'react' + +export function AmountWithUSDSub({ + amount, + shouldStrike, + fontSize, + mb = '-4px', +}: { + fontSize?: string + shouldStrike?: boolean + amount: CurrencyAmount + mb?: string +}) { + const formattedUsdAmount = useStablecoinPriceAmount(amount.currency, toNumber(amount.toSignificant(6))) + + return React.createElement( + shouldStrike ? 's' : React.Fragment, + undefined, + <> + + {amount.toSignificant(5)} {amount.currency.symbol} + + + , + ) +} diff --git a/apps/web/src/views/FixedStaking/components/AprCell.tsx b/apps/web/src/views/FixedStaking/components/AprCell.tsx new file mode 100644 index 0000000000000..373b4074741dc --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/AprCell.tsx @@ -0,0 +1,60 @@ +import { Flex, StarCircle, Text } from '@pancakeswap/uikit' + +import { FixedStakingCalculator } from './FixedStakingCalculator' +import { AprRange, calculateAPRPercent } from './AprRange' +import { FixedStakingPool, PoolGroup } from '../type' + +export default function AprCell({ + selectedPeriodIndex, + selectedPool, + pool, + hideCalculator, +}: { + selectedPeriodIndex: number + selectedPool: FixedStakingPool + pool: PoolGroup + hideCalculator?: boolean +}) { + return selectedPeriodIndex === null || !selectedPool ? ( + + + + + {hideCalculator ? null : ( + + )} + + ) : ( + + {calculateAPRPercent(selectedPool.boostDayPercent).greaterThan(0) ? ( + <> + + + Up to {calculateAPRPercent(selectedPool.boostDayPercent).toSignificant(2)}% + + + ) : null} + + {calculateAPRPercent(selectedPool.boostDayPercent).greaterThan(0) ? ( + {calculateAPRPercent(selectedPool.lockDayPercent).toSignificant(2)}% + ) : ( + <>{calculateAPRPercent(selectedPool.lockDayPercent).toSignificant(2)}% + )} + + {hideCalculator ? null : ( + + )} + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/AprFooter.tsx b/apps/web/src/views/FixedStaking/components/AprFooter.tsx new file mode 100644 index 0000000000000..8a71ba16b0c82 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/AprFooter.tsx @@ -0,0 +1,47 @@ +import { Flex, StarCircle, Text } from '@pancakeswap/uikit' +import { Token } from '@pancakeswap/swap-sdk-core' + +import { FixedStakingCalculator } from './FixedStakingCalculator' +import { useFixedStakeAPR } from '../hooks/useFixedStakeAPR' +import { FixedStakingPool } from '../type' + +export function AprFooter({ + lockPeriod, + stakingToken, + boostDayPercent, + lockDayPercent, + unlockDayPercent, + pools, +}: { + lockPeriod: number + stakingToken: Token + boostDayPercent: number + lockDayPercent: number + unlockDayPercent: number + pools: FixedStakingPool[] +}) { + const { boostAPR, lockAPR } = useFixedStakeAPR({ boostDayPercent, lockDayPercent, unlockDayPercent }) + + return ( + + {lockPeriod}D APR: + + {boostAPR.greaterThan(0) ? ( + <> + + + Up to {boostAPR.toSignificant(2)}% + + + ) : null} + {boostAPR.greaterThan(0) ? {lockAPR.toSignificant(2)}% : <>{lockAPR.toSignificant(2)}%} + + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/AprRange.tsx b/apps/web/src/views/FixedStaking/components/AprRange.tsx new file mode 100644 index 0000000000000..b7727dc3611e2 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/AprRange.tsx @@ -0,0 +1,23 @@ +import { Percent } from '@pancakeswap/sdk' +import { DAYS_A_YEAR, PERCENT_DIGIT } from '../constant' + +export function calculateAPRPercent(percent: number) { + return new Percent(percent, PERCENT_DIGIT).multiply(DAYS_A_YEAR) +} + +export function AprRange({ + minLockDayPercent, + maxLockDayPercent, +}: { + minLockDayPercent: number + maxLockDayPercent: number +}) { + const minAPR = calculateAPRPercent(minLockDayPercent) + const maxAPR = calculateAPRPercent(maxLockDayPercent) + + return ( + <> + {minAPR.toSignificant(2)}% ~ {maxAPR.toSignificant(2)}% + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/ClaimModal.tsx b/apps/web/src/views/FixedStaking/components/ClaimModal.tsx new file mode 100644 index 0000000000000..29feb7c87377a --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/ClaimModal.tsx @@ -0,0 +1,224 @@ +import { Box, Flex, Modal, ModalV2, PreTitle, Text, Button, useModalV2, Card } from '@pancakeswap/uikit' +import { LightCard } from 'components/Card' +import { useTranslation } from '@pancakeswap/localization' +import { CurrencyAmount, Percent, Token } from '@pancakeswap/swap-sdk-core' +import { ReactNode, useMemo, useState } from 'react' +import { formatTime } from 'utils/formatTime' + +import { UnstakeEndedModal } from './UnstakeModal' +import { HarvestModal } from './HarvestModal' +import { PoolGroup, StakePositionUserInfo, StakedPosition } from '../type' +import { useHandleWithdrawSubmission } from '../hooks/useHandleWithdrawSubmission' +import { useCalculateProjectedReturnAmount } from '../hooks/useCalculateProjectedReturnAmount' +import { AmountWithUSDSub } from './AmountWithUSDSub' +import { ModalTitle } from './ModalTitle' +import { StakedLimitEndOn } from './StakedLimitEndOn' +import { useCurrentDay } from '../hooks/useStakedPools' +import useIsBoost from '../hooks/useIsBoost' + +export function ClaimModal({ + token, + lockPeriod, + children, + unlockTime, + lockAPR, + stakePositionUserInfo, + poolIndex, + pool, + boostAPR, + unlockAPR, + poolEndDay, + stakePosition, +}: { + stakePosition: StakedPosition + poolEndDay: number + token: Token + lockPeriod: number + unlockTime: number + lockAPR: Percent + boostAPR: Percent + unlockAPR: Percent + stakePositionUserInfo: StakePositionUserInfo + poolIndex: number + pool: PoolGroup + stakedPeriods: number[] + children: (openClaimModal: () => void) => ReactNode +}) { + const { t } = useTranslation() + const unstakeModal = useModalV2() + const claimModal = useModalV2() + const [isConfirmed, setIsConfirmed] = useState(false) + + const amountDeposit = useMemo( + () => CurrencyAmount.fromRawAmount(token, stakePositionUserInfo.userDeposit.toString()), + [stakePositionUserInfo.userDeposit, token], + ) + const currentDay = useCurrentDay() + + const apr = useMemo( + () => (stakePositionUserInfo.boost ? boostAPR : lockAPR), + [boostAPR, lockAPR, stakePositionUserInfo.boost], + ) + + const isBoost = useIsBoost({ + minBoostAmount: stakePosition.pool.minBoostAmount, + boostDayPercent: stakePosition.pool.boostDayPercent, + }) + + const { projectedReturnAmount } = useCalculateProjectedReturnAmount({ + amountDeposit, + lastDayAction: currentDay, + lockPeriod, + apr: isBoost ? boostAPR : lockAPR, + poolEndDay, + unlockAPR, + }) + + const accrueInterest = useMemo( + () => CurrencyAmount.fromRawAmount(token, stakePositionUserInfo.accrueInterest.toString()), + [stakePositionUserInfo.accrueInterest, token], + ) + + const { handleSubmission, pendingTx } = useHandleWithdrawSubmission({ + poolIndex, + stakingToken: token, + onSuccess: () => (unstakeModal.isOpen ? unstakeModal.onDismiss() : setIsConfirmed(true)), + }) + + const poolEnded = unlockTime >= poolEndDay * 86400 + 43200 + + const unlockTimeFormat = formatTime(unlockTime * 1_000) + + return ( + <> + {children(claimModal.onOpen)} + + {(openModal) => ( + + } + width={['100%', '100%', '420px']} + maxWidth={['100%', , '420px']} + > + {t('Overview')} + + + + + {t('Stake Amount')} + + + + + + {t('Fixed-staking Ends')} + + + + {t('Ended')} + + + + On {unlockTimeFormat} + + + + + + {t('Details')} + + + + + + {t('Rewards')} + + + + + + + + {t('Restake period ends on')} + + + + + + + + {t('APR')} + + {apr.toSignificant(2)}% + + {poolEnded ? null : ( + + )} + + + + + + )} + + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/DisclaimerCheckBox.tsx b/apps/web/src/views/FixedStaking/components/DisclaimerCheckBox.tsx new file mode 100644 index 0000000000000..928f3a3b16ffe --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/DisclaimerCheckBox.tsx @@ -0,0 +1,32 @@ +import { Checkbox, Flex, Link, Text } from '@pancakeswap/uikit' +import { Dispatch, SetStateAction } from 'react' + +export function DisclaimerCheckBox({ + check, + setCheck, +}: { + check: boolean + setCheck: Dispatch> +}) { + return ( + +
+ setCheck((prev) => !prev)} /> +
+ + By checking this box, I understand that my funds will be custodied by Binance Earn.
+ For more information please read{' '} + + the Terms & Conditions. + +
+
+ ) +} diff --git a/apps/web/src/views/FixedStaking/components/FixedStakingCalculator.tsx b/apps/web/src/views/FixedStaking/components/FixedStakingCalculator.tsx new file mode 100644 index 0000000000000..7aa58938ba9e3 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/FixedStakingCalculator.tsx @@ -0,0 +1,193 @@ +import { useTranslation } from '@pancakeswap/localization' +import { + ModalV2, + useModalV2, + Box, + PreTitle, + IconButton, + CalculateIcon, + RoiCard, + CalculatorMode, + Flex, + Button, +} from '@pancakeswap/uikit' +import { CurrencyAmount, Percent, Token } from '@pancakeswap/sdk' +import toNumber from 'lodash/toNumber' +import { useMemo } from 'react' +import { useStablecoinPriceAmount } from 'hooks/useBUSDPrice' + +import { FixedStakingPool } from '../type' +import FixedStakingOverview from './FixedStakingOverview' +import { StakingModalTemplate } from './StakingModalTemplate' +import { useCurrentDay } from '../hooks/useStakedPools' +import { useCalculateProjectedReturnAmount } from '../hooks/useCalculateProjectedReturnAmount' + +function FixedStakingRoiCard({ + stakeAmount, + lockAPR, + boostAPR, + unlockAPR, + isBoost, + lockPeriod, + lastDayAction, + poolEndDay, +}: { + stakeAmount: CurrencyAmount + lockAPR: Percent + boostAPR: Percent + unlockAPR: Percent + poolEndDay: number + lastDayAction?: number + lockPeriod?: number + isBoost?: boolean +}) { + const apr = useMemo(() => (isBoost ? boostAPR : lockAPR), [boostAPR, isBoost, lockAPR]) + + const safeAlreadyStakedAmount = useMemo( + () => CurrencyAmount.fromRawAmount(stakeAmount.currency, '0'), + [stakeAmount.currency], + ) + + const currentDay = useCurrentDay() + + const { projectedReturnAmount } = useCalculateProjectedReturnAmount({ + amountDeposit: stakeAmount.add(safeAlreadyStakedAmount), + lastDayAction: safeAlreadyStakedAmount.greaterThan(0) && stakeAmount.equalTo(0) ? lastDayAction : currentDay, + lockPeriod: lockPeriod || 0, + apr, + poolEndDay, + unlockAPR, + }) + + const formattedUsdProjectedReturnAmount = useStablecoinPriceAmount( + projectedReturnAmount.currency, + toNumber(projectedReturnAmount?.toSignificant(2)), + ) + + return ( + + ) +} + +export function FixedStakingCalculator({ + stakingToken, + pools, + initialLockPeriod, + hideBackButton, +}: { + stakingToken: Token + pools: FixedStakingPool[] + initialLockPeriod: number + hideBackButton?: boolean +}) { + const stakedPeriods = useMemo(() => pools.map((p) => p.lockPeriod), [pools]) + + const { t } = useTranslation() + const stakeModal = useModalV2() + + return ( + <> + { + e.stopPropagation() + stakeModal.onOpen() + }} + > + + + { + stakeModal.onDismiss() + }} + closeOnOverlayClick + > + stakeModal.onDismiss()} + hideStakeButton + stakingToken={stakingToken} + pools={pools} + initialLockPeriod={initialLockPeriod} + stakedPeriods={stakedPeriods} + body={({ + setLockPeriod, + stakeCurrencyAmount, + lockPeriod, + boostAPR, + lockAPR, + unlockAPR, + poolEndDay, + lastDayAction, + isBoost, + }) => ( + <> + + {t('Stake Duration')} + + + {pools.map((pool) => ( + + ))} + + + + + + + {t('Position Overview')} + + + + + )} + /> + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/FixedStakingCard.tsx b/apps/web/src/views/FixedStaking/components/FixedStakingCard.tsx new file mode 100644 index 0000000000000..3c9878424709a --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/FixedStakingCard.tsx @@ -0,0 +1,95 @@ +import { useTranslation } from '@pancakeswap/localization' +import { CardBody, Flex, Heading, Tag, Box, Button, StarCircle } from '@pancakeswap/uikit' +import { Pool } from '@pancakeswap/widgets-internal' +import CurrencyLogo from 'components/Logo/CurrencyLogo' +import BigNumber from 'bignumber.js' +import first from 'lodash/first' +import { LightGreyCard } from 'components/Card' +import Divider from 'components/Divider' +import React from 'react' + +import { FixedStakingCardBody } from './FixedStakingCardBody' +import { StakedPositionSection } from './StakedPositionSection' +import { FixedStakingModal } from './FixedStakingModal' +import { InlineText } from './InlineText' +import { StakedPosition, PoolGroup } from '../type' + +export function FixedStakingCard({ pool, stakedPositions }: { pool: PoolGroup; stakedPositions: StakedPosition[] }) { + const { t } = useTranslation() + + return ( + + + <> + + + + {pool.token.symbol} + + {new BigNumber(first(pool.pools)?.boostDayPercent).gt(0) ? ( + }> + {t('Locked Cake Boost')} + + ) : null} + + + + + + {(selectedPeriodIndex, setSelectedPeriodIndex) => ( + <> + {stakedPositions.length ? ( + <> + + {stakedPositions.length} {t('Staked Position')} + + + {stakedPositions.map((stakePosition, index) => ( + + position.pool.lockPeriod)} + /> + {index < stakedPositions.length - 1 ? ( + + + + ) : null} + + ))} + + + ) : null} + {pool?.pools?.length ? ( + + {(openModal, hideStakeButton) => + hideStakeButton ? null : + } + + ) : null} + + )} + + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/FixedStakingCardBody.tsx b/apps/web/src/views/FixedStaking/components/FixedStakingCardBody.tsx new file mode 100644 index 0000000000000..6fdfaf5e6ddeb --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/FixedStakingCardBody.tsx @@ -0,0 +1,105 @@ +import { Box, ButtonMenu, ButtonMenuItem, Flex, LockIcon, Text, UnlockIcon } from '@pancakeswap/uikit' +import { useTranslation } from '@pancakeswap/localization' + +import { ReactNode } from 'react' +import { CurrencyAmount } from '@pancakeswap/swap-sdk-core' + +import { PoolGroup, StakedPosition } from '../type' +import { FixedStakingCardFooter } from './FixedStakingCardFooter' +import { AmountWithUSDSub } from './AmountWithUSDSub' +import { AprFooter } from './AprFooter' +import useSelectedPeriod from '../hooks/useSelectedPeriod' +import AprCell from './AprCell' + +function OverviewDataRow({ title, detail, alignItems = 'center' }) { + return ( + + + {title} + + {detail} + + ) +} + +export function FixedStakingCardBody({ + children, + pool, + stakedPositions, +}: { + pool: PoolGroup + stakedPositions: StakedPosition[] + children: (selectedPeriodIndex: number | null, setSelectedPeriodIndex: (index: number | null) => void) => ReactNode +}) { + const { t } = useTranslation() + const totalStakedAmount = CurrencyAmount.fromRawAmount(pool.token, pool.totalDeposited.toNumber()) + + const { selectedPeriodIndex, setSelectedPeriodIndex, claimedIndexes, lockedIndexes, selectedPool } = + useSelectedPeriod({ + pool, + stakedPositions, + }) + + return ( + <> + } + /> + + { + if ([...claimedIndexes, ...lockedIndexes].includes(index)) { + return + } + + setSelectedPeriodIndex(index) + }} + scale="sm" + variant="subtle" + > + {pool.pools.map((p, index) => ( + + {claimedIndexes.includes(index) ? : null} + {lockedIndexes.includes(index) ? : null} + {p.lockPeriod}D + + ))} + + } + /> + + + + + } + /> + + + {children(selectedPeriodIndex, setSelectedPeriodIndex)} + + + + {pool.pools.map((p) => ( + + ))} + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/FixedStakingCardFooter.tsx b/apps/web/src/views/FixedStaking/components/FixedStakingCardFooter.tsx new file mode 100644 index 0000000000000..d3575ff98659f --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/FixedStakingCardFooter.tsx @@ -0,0 +1,36 @@ +import { useTranslation } from '@pancakeswap/localization' +import { ExpandableLabel, Flex } from '@pancakeswap/uikit' +import { ReactNode, useState } from 'react' +import { styled } from 'styled-components' + +const ExpandableButtonWrapper = styled(Flex)` + align-items: center; + justify-content: space-between; + button { + padding: 0; + } +` + +const ExpandedWrapper = styled(Flex)` + width: 100%; + $ > svg { + height: 14px; + width: 14px; + } +` + +export function FixedStakingCardFooter({ children }: { children: ReactNode }) { + const { t } = useTranslation() + const [isExpanded, setIsExpanded] = useState(false) + + return ( + + + setIsExpanded(!isExpanded)}> + {isExpanded ? t('Hide') : t('Info')} + + + {isExpanded && {children}} + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/FixedStakingModal.tsx b/apps/web/src/views/FixedStaking/components/FixedStakingModal.tsx new file mode 100644 index 0000000000000..daa8a4b205f56 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/FixedStakingModal.tsx @@ -0,0 +1,150 @@ +import { useTranslation } from '@pancakeswap/localization' +import { ModalV2, useModalV2, Flex, Box, PreTitle, MessageText, Message, Button } from '@pancakeswap/uikit' +import { ReactNode, useMemo } from 'react' +import Divider from 'components/Divider' +import { Token } from '@pancakeswap/sdk' +import { differenceInMilliseconds } from 'date-fns' + +import ConnectWalletButton from 'components/ConnectWalletButton' +import useAccountActiveChain from 'hooks/useAccountActiveChain' +import { FixedStakingPool, StakedPosition } from '../type' +import FixedStakingOverview from './FixedStakingOverview' +import { StakingModalTemplate } from './StakingModalTemplate' +import { FixedStakingCalculator } from './FixedStakingCalculator' +import { useCurrentDay } from '../hooks/useStakedPools' +import WithdrawalMessage from './WithdrawalMessage' + +export function FixedStakingModal({ + stakingToken, + pools, + children, + initialLockPeriod, + stakedPositions, + setSelectedPeriodIndex, +}: { + stakingToken: Token + pools: FixedStakingPool[] + children: (openModal: () => void, hideStakeButton: boolean) => ReactNode + initialLockPeriod: number + stakedPositions: StakedPosition[] + setSelectedPeriodIndex?: (value: number | null) => void +}) { + const { account } = useAccountActiveChain() + + const { t } = useTranslation() + const stakeModal = useModalV2() + + const stakedPeriods = useMemo( + () => + stakedPositions + .filter((sP) => differenceInMilliseconds(sP.endLockTime * 1_000, new Date()) > 0) + .map((sP) => sP.pool.lockPeriod), + [stakedPositions], + ) + + const claimedPeriods = useMemo( + () => + stakedPositions + .filter((sP) => differenceInMilliseconds(sP.endLockTime * 1_000, new Date()) <= 0) + .map((sP) => sP.pool.lockPeriod), + [stakedPositions], + ) + + const hideStakeButton = stakedPositions.length === pools.length + + const currentDay = useCurrentDay() + + return account ? ( + <> + {children(stakeModal.onOpen, hideStakeButton)} + { + if (setSelectedPeriodIndex) setSelectedPeriodIndex(null) + stakeModal.onDismiss() + }} + closeOnOverlayClick + > + ( + <> + {pools.length > 1 ? ( + <> + + {t('Stake Duration')} + + + {pools.map((pool) => ( + + ))} + + + {isStaked ? ( + + + {`You already have a position in ${lockPeriod}D lock period, adding to the position will restart locking and non-withdrawal period`} + + + ) : null} + + + + ) : null} + + + + {t('Position Overview')} + + + } + /> + + + )} + /> + + + ) : ( + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/FixedStakingOverview.tsx b/apps/web/src/views/FixedStaking/components/FixedStakingOverview.tsx new file mode 100644 index 0000000000000..b7fc3929d584d --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/FixedStakingOverview.tsx @@ -0,0 +1,151 @@ +import { useTranslation } from '@pancakeswap/localization' +import { Flex, Text, Box } from '@pancakeswap/uikit' + +import { LightGreyCard } from 'components/Card' + +import { ReactNode, useMemo } from 'react' +import TextRow from 'views/Pools/components/LockedPool/Common/Overview/TextRow' +import { formatTime } from 'utils/formatTime' + +import { CurrencyAmount, Percent, Token } from '@pancakeswap/swap-sdk-core' +import { AmountWithUSDSub } from './AmountWithUSDSub' +import { StakedLimitEndOn } from './StakedLimitEndOn' +import { useCurrentDay } from '../hooks/useStakedPools' +import { useCalculateProjectedReturnAmount } from '../hooks/useCalculateProjectedReturnAmount' + +function DiffDuration({ lockPeriod }: { lockPeriod: number }) { + const { t } = useTranslation() + + const lastDayAction = useCurrentDay() + + const lockEndDay = lastDayAction + lockPeriod + + return ( + + ) +} + +export default function FixedStakingOverview({ + stakeAmount, + lockAPR, + boostAPR, + unlockAPR, + isBoost, + lockPeriod, + lastDayAction, + poolEndDay, + isUnstakeView, + alreadyStakedAmount, + disableStrike, + calculator, + unlockTime, +}: { + unlockTime?: number + disableStrike?: boolean + isUnstakeView?: boolean + stakeAmount: CurrencyAmount + alreadyStakedAmount?: CurrencyAmount + lockAPR: Percent + boostAPR: Percent + unlockAPR: Percent + poolEndDay: number + lastDayAction?: number + lockPeriod?: number + calculator?: ReactNode + isBoost?: boolean +}) { + const { t } = useTranslation() + + const apr = useMemo(() => (isBoost ? boostAPR : lockAPR), [boostAPR, isBoost, lockAPR]) + + const safeAlreadyStakedAmount = useMemo( + () => alreadyStakedAmount || CurrencyAmount.fromRawAmount(stakeAmount.currency, '0'), + [alreadyStakedAmount, stakeAmount.currency], + ) + + const currentDay = useCurrentDay() + + const { projectedReturnAmount } = useCalculateProjectedReturnAmount({ + amountDeposit: stakeAmount.add(safeAlreadyStakedAmount), + lastDayAction: safeAlreadyStakedAmount.greaterThan(0) && stakeAmount.equalTo(0) ? lastDayAction : currentDay, + lockPeriod: lockPeriod || 0, + apr, + poolEndDay, + unlockAPR, + }) + + return ( + + {!isUnstakeView ? ( + + ) : null} + {!isUnstakeView && lockPeriod ? ( + safeAlreadyStakedAmount.greaterThan(0) && stakeAmount.greaterThan(0) ? ( + + ) : ( + + + {t('Duration')} + + + {lockPeriod} {t('days')} + + + ) + ) : null} + + + {t('APR')} + + {apr?.toSignificant(2)}% + + {boostAPR && lockAPR ? ( + + + {t('Locked Cake Boost')} + + {boostAPR.divide(lockAPR).divide(100).toSignificant(2)}x + + ) : null} + + + {t('Stake Period Ends')} + + + {unlockTime ? ( + formatTime(unlockTime * 1_000) + ) : ( + + )} + + + + + {t('Projected Return')} + + + + + + {calculator} + + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/FixedStakingRow.tsx b/apps/web/src/views/FixedStaking/components/FixedStakingRow.tsx new file mode 100644 index 0000000000000..4ab2425e5a1de --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/FixedStakingRow.tsx @@ -0,0 +1,258 @@ +import { + CurrencyLogo, + Text, + Flex, + Box, + Button, + ButtonMenuItem, + ButtonMenu, + useMatchBreakpoints, + UnlockIcon, + LockIcon, +} from '@pancakeswap/uikit' +import { Pool } from '@pancakeswap/widgets-internal' +import { StyledCell } from 'views/Pools/components/PoolsTable/Cells/NameCell' +import { useTranslation } from '@pancakeswap/localization' +import React from 'react' +import Divider from 'components/Divider' + +import { + ActionContainer, + InfoSection, + StyledActionPanel, +} from 'views/Pools/components/PoolsTable/ActionPanel/ActionPanel' +import { LightGreyCard } from 'components/Card' +import { CurrencyAmount } from '@pancakeswap/swap-sdk-core' + +import { PoolGroup, StakedPosition } from '../type' +import { InlineText } from './InlineText' +import { FixedStakingModal } from './FixedStakingModal' +import { AmountWithUSDSub } from './AmountWithUSDSub' +import { AprFooter } from './AprFooter' +import { StakedPositionSection } from './StakedPositionSection' +import useSelectedPeriod from '../hooks/useSelectedPeriod' +import AprCell from './AprCell' + +const FixedStakingRow = ({ pool, stakedPositions }: { pool: PoolGroup; stakedPositions: StakedPosition[] }) => { + const { t } = useTranslation() + const totalStakedAmount = CurrencyAmount.fromRawAmount(pool.token, pool.totalDeposited.toNumber()) + const { isMobile, isTablet } = useMatchBreakpoints() + + const { selectedPeriodIndex, setSelectedPeriodIndex, claimedIndexes, lockedIndexes, selectedPool } = + useSelectedPeriod({ + pool, + stakedPositions, + }) + + const hideStakeButton = stakedPositions.length === pool.pools.length + + return ( + <> + {isMobile ? ( + + + + + {t('Stake & Earn')} + + + {pool.token.symbol} + + + + + {t('APR')} + + + + + + ) : null} + + + {isMobile || isTablet ? ( + + {t('Total Staked:')} + + + + + ) : null} + {pool.pools.map((p) => ( + + ))} + + + {stakedPositions?.length ? ( + + + {stakedPositions.map((stakePosition, index) => ( + + position.pool.lockPeriod)} + /> + {index < stakedPositions.length - 1 ? ( + + + + ) : null} + + ))} + + + ) : null} + {hideStakeButton ? null : ( + + + + + {t('Stake')} + + + {`${pool.token.symbol} `} + + + + {(openModal) => } + + + + )} + + + } + > + <> + {isMobile ? null : ( + + + + + {t('Stake & Earn')} + + + {pool.token.symbol} + + + + )} + + + + + {t('Stake Periods')} + + { + event.stopPropagation() + + if ([...claimedIndexes, ...lockedIndexes].includes(index)) { + return + } + + setSelectedPeriodIndex(index) + }} + scale="sm" + variant="subtle" + > + {pool.pools.map((p, index) => ( + + {claimedIndexes.includes(index) ? : null} + {lockedIndexes.includes(index) ? : null} + {p.lockPeriod}D + + ))} + + + + + {isMobile ? null : ( + + + + {t('APR')} + + + + + + )} + {isMobile || isTablet ? null : ( + + + + {t('Total Staked:')} + + + + + )} + + + + ) +} + +export default FixedStakingRow diff --git a/apps/web/src/views/FixedStaking/components/HarvestModal.tsx b/apps/web/src/views/FixedStaking/components/HarvestModal.tsx new file mode 100644 index 0000000000000..7a4b9ce50f80e --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/HarvestModal.tsx @@ -0,0 +1,193 @@ +import { useTranslation } from '@pancakeswap/localization' +import { Button, ModalV2, useModalV2, Modal, Flex, Text, Box, PreTitle, InfoFilledIcon, Card } from '@pancakeswap/uikit' + +import { ReactNode, useState } from 'react' +import { LightGreyCard } from 'components/Card' +import { CurrencyAmount, Percent, Token } from '@pancakeswap/sdk' +import ConnectWalletButton from 'components/ConnectWalletButton' +import useAccountActiveChain from 'hooks/useAccountActiveChain' + +import { FixedStakingPool, UnstakeType } from '../type' +import { DisclaimerCheckBox } from './DisclaimerCheckBox' +import { AmountWithUSDSub } from './AmountWithUSDSub' +import { StakedLimitEndOn } from './StakedLimitEndOn' +import { StakeConfirmModal } from './StakeConfirmModal' +import { FixedStakingCalculator } from './FixedStakingCalculator' +import { ModalTitle } from './ModalTitle' + +export function HarvestModal({ + stakingToken, + children, + lockPeriod, + amountDeposit, + accrueInterest, + projectedReturnAmount, + boostAPR, + lockAPR, + handleSubmission, + pendingTx, + onBack, + poolEndDay, + pools, + isConfirmed, + isBoost, + unlockAPR, +}: { + isBoost?: boolean + isConfirmed?: boolean + poolEndDay: number + onBack: () => void + stakingToken: Token + pools: FixedStakingPool[] + children: (openModal: () => void) => ReactNode + lockPeriod: number + amountDeposit: CurrencyAmount + accrueInterest: CurrencyAmount + projectedReturnAmount: CurrencyAmount + boostAPR: Percent + lockAPR: Percent + unlockAPR: Percent + pendingTx: boolean + handleSubmission: (type: UnstakeType, amount: CurrencyAmount) => Promise +}) { + const { account } = useAccountActiveChain() + const [check, setCheck] = useState(false) + + const { t } = useTranslation() + const restakeModal = useModalV2() + + return account ? ( + <> + {children(restakeModal.onOpen)} + + { + restakeModal.onDismiss() + onBack() + }} + title={ + + } + width={['100%', '100%', '420px']} + maxWidth={['100%', , '420px']} + > + {isConfirmed ? ( + + ) : ( + <> + + + + {t('Claim Reward')} + + + + + + + + {t('Claimed amount will be sent to your wallet')} + + + + + + {t('Restaking')} + + {t('Position Overview')} + + + + + {t('Stake Amount')} + + + {amountDeposit.toSignificant(5)} {amountDeposit.currency.symbol} + + + + + {t('Duration')} + + + {lockPeriod} {t('days')} + + + + + {t('APR')} + + {isBoost ? boostAPR.toSignificant(2) : lockAPR?.toSignificant(2)}% + + {boostAPR?.greaterThan(0) ? ( + + + {t('Locked Cake Boost')} + + {boostAPR.divide(lockAPR).divide(100).toSignificant(2)}x + + ) : null} + + + {t('Fixed Staking Ends On')} + + + + + + + + {t('Projected Return')} + + + + + + + + + + + + + + )} + + + + ) : ( + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/InlineText.tsx b/apps/web/src/views/FixedStaking/components/InlineText.tsx new file mode 100644 index 0000000000000..55a896150fbec --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/InlineText.tsx @@ -0,0 +1,6 @@ +import { Text } from '@pancakeswap/uikit' +import { styled } from 'styled-components' + +export const InlineText = styled(Text)` + display: inline; +` diff --git a/apps/web/src/views/FixedStaking/components/LockedFixedTag.tsx b/apps/web/src/views/FixedStaking/components/LockedFixedTag.tsx new file mode 100644 index 0000000000000..385a10f9fe1ac --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/LockedFixedTag.tsx @@ -0,0 +1,14 @@ +import { Tag, LockIcon } from '@pancakeswap/uikit' +import { ReactNode, CSSProperties } from 'react' + +export function LockedFixedTag({ children, style }: { children: ReactNode; style?: CSSProperties }) { + return ( + } + > + {children} + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/ModalTitle.tsx b/apps/web/src/views/FixedStaking/components/ModalTitle.tsx new file mode 100644 index 0000000000000..3b02b526ed787 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/ModalTitle.tsx @@ -0,0 +1,32 @@ +import { CurrencyLogo, Flex, Heading } from '@pancakeswap/uikit' +import { Token } from '@pancakeswap/sdk' +import { useTranslation } from '@pancakeswap/localization' +import { UnlockedFixedTag } from './UnlockedFixedTag' + +export function ModalTitle({ + tokenTitle, + token, + lockPeriod, + isEnded, +}: { + isEnded?: boolean + token: Token + tokenTitle: string + lockPeriod?: number +}) { + const { t } = useTranslation() + + return ( + + + + {tokenTitle} + + {lockPeriod ? ( + + {lockPeriod}D {isEnded ? t('Ended') : null} + + ) : null} + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/RestakeFixedStakingModal.tsx b/apps/web/src/views/FixedStaking/components/RestakeFixedStakingModal.tsx new file mode 100644 index 0000000000000..df96e3bc44182 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/RestakeFixedStakingModal.tsx @@ -0,0 +1,127 @@ +import { useTranslation } from '@pancakeswap/localization' +import { ModalV2, useModalV2, Text, Box, PreTitle, Flex, Message, MessageText } from '@pancakeswap/uikit' +import { ReactNode } from 'react' +import { CurrencyAmount, Token } from '@pancakeswap/sdk' +import { LightGreyCard } from 'components/Card' + +import ConnectWalletButton from 'components/ConnectWalletButton' +import useAccountActiveChain from 'hooks/useAccountActiveChain' + +import { FixedStakingPool, StakedPosition } from '../type' +import FixedStakingOverview from './FixedStakingOverview' +import { StakingModalTemplate } from './StakingModalTemplate' +import { FixedStakingCalculator } from './FixedStakingCalculator' +import { AmountWithUSDSub } from './AmountWithUSDSub' +import WithdrawalMessage from './WithdrawalMessage' + +export function FixedRestakingModal({ + stakingToken, + pools, + children, + initialLockPeriod, + stakedPeriods, + setSelectedPeriodIndex, + amountDeposit, + stakedPositions, +}: { + stakingToken: Token + pools: FixedStakingPool[] + children: (openModal: () => void) => ReactNode + initialLockPeriod: number + stakedPeriods: number[] + setSelectedPeriodIndex?: (value: number | null) => void + amountDeposit: CurrencyAmount + stakedPositions: StakedPosition[] +}) { + const { account } = useAccountActiveChain() + + const { t } = useTranslation() + const stakeModal = useModalV2() + + return account ? ( + <> + {children(stakeModal.onOpen)} + { + if (setSelectedPeriodIndex) setSelectedPeriodIndex(null) + stakeModal.onDismiss() + }} + closeOnOverlayClick + > + ( + + + {t('Adding stake to the position will restart the lock and withdrawal period.')} + + + )} + body={({ + unlockAPR, + isBoost, + stakeCurrencyAmount, + alreadyStakedAmount, + poolEndDay, + lockPeriod, + boostAPR, + lockAPR, + lastDayAction, + }) => ( + <> + + + + + {t('Overview')} + + + + + + {t('New')} + {t('Staked Amount')} + + + + + {t('Stake Period')} + {lockPeriod} Days + + + + + + + + {t('Position Details')} + + + } + /> + + + )} + /> + + + ) : ( + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/StakeConfirmModal.tsx b/apps/web/src/views/FixedStaking/components/StakeConfirmModal.tsx new file mode 100644 index 0000000000000..4b5d202af5ee5 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/StakeConfirmModal.tsx @@ -0,0 +1,75 @@ +import { useTranslation } from '@pancakeswap/localization' +import { PreTitle, Flex, Box, Text } from '@pancakeswap/uikit' +import { GreyCard } from 'components/Card' +import { CurrencyAmount, Percent, Token } from '@pancakeswap/sdk' + +import FixedStakingOverview from './FixedStakingOverview' +import { AmountWithUSDSub } from './AmountWithUSDSub' +import { StakedLimitEndOn } from './StakedLimitEndOn' + +export function StakeConfirmModal({ + stakeCurrencyAmount, + poolEndDay, + lockAPR, + boostAPR, + unlockAPR, + lockPeriod, + isBoost, +}: { + stakeCurrencyAmount: CurrencyAmount + lockPeriod: number + boostAPR: Percent + lockAPR: Percent + unlockAPR: Percent + poolEndDay: number + isBoost?: boolean +}) { + const { t } = useTranslation() + + return ( + <> + {t('Overview')} + + + + + {t('Stake Amount')} + + + + + + {t('Ends In')} + + + + {lockPeriod} {t('days')} + + + + On + + + + + + {t('Position Details')} + + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/StakedLimitEndOn.tsx b/apps/web/src/views/FixedStaking/components/StakedLimitEndOn.tsx new file mode 100644 index 0000000000000..8a9d0e5bf2dc1 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/StakedLimitEndOn.tsx @@ -0,0 +1,23 @@ +import { formatTime } from 'utils/formatTime' + +import { useCurrentDay } from '../hooks/useStakedPools' + +export function StakedLimitEndOn({ lockPeriod, poolEndDay }: { lockPeriod: number; poolEndDay: number }) { + const lastDayAction = useCurrentDay() + + /** + * Duplicate logic on Smart Contract + * uint32 lockEndDay = info.userInfo.lastDayAction + info.pool.lockPeriod; + info.endLockTime = info.userInfo.userDeposit > 0 + ? lockEndDay < info.pool.endDay ? lockEndDay * 86400 + 43200 : info.pool.endDay * 86400 + 43200 + : info.userInfo.lastDayAction * 86400 + 43200; + */ + + const lockEndDay = lastDayAction + lockPeriod + + const exceedPoolEndDay = lockEndDay > poolEndDay + + const endTime = exceedPoolEndDay ? poolEndDay : lockEndDay + + return <>{formatTime((endTime * 86400 + 43200) * 1_000)} +} diff --git a/apps/web/src/views/FixedStaking/components/StakedPositionSection.tsx b/apps/web/src/views/FixedStaking/components/StakedPositionSection.tsx new file mode 100644 index 0000000000000..3e96c0eca4d22 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/StakedPositionSection.tsx @@ -0,0 +1,261 @@ +import { Box, Button, Flex, Text, IconButton, AddIcon, MinusIcon, ChevronRightIcon } from '@pancakeswap/uikit' +import { useTranslation } from '@pancakeswap/localization' +import { CurrencyAmount, Percent, Token } from '@pancakeswap/swap-sdk-core' +import { useMemo } from 'react' + +import { differenceInMilliseconds, format } from 'date-fns' +import { styled } from 'styled-components' + +import { LockedFixedTag } from './LockedFixedTag' +import { PoolGroup, StakePositionUserInfo, StakedPosition } from '../type' +import { ClaimModal } from './ClaimModal' +import { UnlockedFixedTag } from './UnlockedFixedTag' +import { FixedRestakingModal } from './RestakeFixedStakingModal' +import { UnstakeBeforeEnededModal } from './UnstakeBeforeEndedModal' +import { useFixedStakeAPR } from '../hooks/useFixedStakeAPR' +import { AmountWithUSDSub } from './AmountWithUSDSub' +import { useCalculateProjectedReturnAmount } from '../hooks/useCalculateProjectedReturnAmount' +import { useCurrentDay } from '../hooks/useStakedPools' + +const FlexLeft = styled(Flex)` + width: 100%; + align-items: center; + margin-right: 16px; + border-right: 1px solid ${({ theme }) => theme.colors.cardBorder}; +` + +function InfoSection({ + apr, + shouldUnlock, + unlockTime, + accrueInterest, + tokenSymbol, + projectedReturnAmount, +}: { + apr: Percent + shouldUnlock: boolean + unlockTime: number + accrueInterest: CurrencyAmount + tokenSymbol: string + projectedReturnAmount: CurrencyAmount +}) { + const { t } = useTranslation() + + return ( + + + APR: {apr.toSignificant(2)}% + + + + {shouldUnlock ? 'Fixed Staking ended' : `Ends on ${format(unlockTime * 1_000, 'MMM d, yyyy')}`} + + + + {shouldUnlock ? ( + <> + {t('Reward')}: {accrueInterest.toSignificant(3)} {tokenSymbol} + + ) : ( + <> + {t('Est. Reward')}: {projectedReturnAmount.toSignificant(3)} {tokenSymbol} + + )} + + + ) +} + +function StakedPositionRowView({ amountDeposit, shouldUnlock, lockPeriod, children }) { + return ( + + + + + + + + + {shouldUnlock ? ( + {lockPeriod}D + ) : ( + {lockPeriod}D + )} + + {children} + + ) +} + +function StakedPositionCardView({ amountDeposit, lockPeriod, shouldUnlock, children }) { + return ( + <> + + + + + + + + {shouldUnlock ? ( + {lockPeriod}D + ) : ( + {lockPeriod}D + )} + + {children} + + ) +} + +export function StakedPositionSection({ + token, + stakePositionUserInfo, + unlockTime, + lockPeriod, + poolIndex, + lockDayPercent, + boostDayPercent, + unlockDayPercent, + withdrawalFee, + pool, + stakedPeriods, + stakePosition, + showRow, +}: { + unlockTime: number + boostDayPercent: number + token: Token + stakePosition: StakedPosition + stakePositionUserInfo: StakePositionUserInfo + lockPeriod: number + poolIndex: number + lockDayPercent: number + unlockDayPercent: number + withdrawalFee: number + pool: PoolGroup + stakedPeriods: number[] + showRow?: boolean +}) { + const { t } = useTranslation() + + const { boostAPR, lockAPR, unlockAPR } = useFixedStakeAPR({ lockDayPercent, boostDayPercent, unlockDayPercent }) + + const amountDeposit = useMemo( + () => CurrencyAmount.fromRawAmount(token, stakePositionUserInfo.userDeposit.toString()), + [stakePositionUserInfo.userDeposit, token], + ) + + const { projectedReturnAmount } = useCalculateProjectedReturnAmount({ + amountDeposit, + lastDayAction: stakePositionUserInfo.lastDayAction, + lockPeriod, + apr: stakePositionUserInfo.boost ? boostAPR : lockAPR, + poolEndDay: stakePosition.pool.endDay, + unlockAPR, + }) + + const accrueInterest = useMemo( + () => CurrencyAmount.fromRawAmount(token, stakePositionUserInfo.accrueInterest.toString()), + [stakePositionUserInfo.accrueInterest, token], + ) + + const poolEndDay = pool.pools.find((p) => p.poolIndex === poolIndex)?.endDay || 0 + + const shouldUnlock = differenceInMilliseconds(unlockTime * 1_000, new Date()) <= 0 + + const stakedPositions = useMemo(() => [stakePosition], [stakePosition]) + + const apr = stakePosition.userInfo.boost ? boostAPR : lockAPR + + const currentDay = useCurrentDay() + + const actionSection = ( + + + + {(openClaimModal) => + shouldUnlock ? ( + + ) : null + } + + {shouldUnlock ? null : ( + + + {(openUnstakeModal, notAllowWithdrawal) => ( + + + + )} + + + {(openModal) => ( + poolEndDay} variant="secondary" onClick={openModal}> + + + )} + + + )} + + ) + + if (showRow) { + return ( + + {actionSection} + + ) + } + + return ( + <> + + {actionSection} + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/StakingModalTemplate.tsx b/apps/web/src/views/FixedStaking/components/StakingModalTemplate.tsx new file mode 100644 index 0000000000000..8779766af6abb --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/StakingModalTemplate.tsx @@ -0,0 +1,396 @@ +import { useTranslation } from '@pancakeswap/localization' +import { Button, Modal, Flex, Text, BalanceInput, Slider, Box, PreTitle, useToast, Link } from '@pancakeswap/uikit' +import { getFullDisplayBalance, getDecimalAmount } from '@pancakeswap/utils/formatBalance' +import { getFullDecimalMultiplier } from '@pancakeswap/utils/getFullDecimalMultiplier' +import BigNumber from 'bignumber.js' +import useTokenBalance from 'hooks/useTokenBalance' +import { Dispatch, ReactNode, SetStateAction, useCallback, useEffect, useMemo, useState } from 'react' +import Divider from 'components/Divider' +import { useFixedStakingContract } from 'hooks/useContract' +import useCatchTxError from 'hooks/useCatchTxError' +import { useCallWithGasPrice } from 'hooks/useCallWithGasPrice' +import { ToastDescriptionWithTx } from 'components/Toast' +import { ApprovalState, useApproveCallback } from 'hooks/useApproveCallback' +import { CurrencyAmount, Percent, Token } from '@pancakeswap/sdk' +import { useStablecoinPriceAmount } from 'hooks/useBUSDPrice' +import toNumber from 'lodash/toNumber' +import { CurrencyLogo } from 'components/Logo' +import first from 'lodash/first' +import { differenceInMilliseconds } from 'date-fns' +import usePrevious from 'views/V3Info/hooks/usePrevious' +import { styled } from 'styled-components' + +import { FixedStakingPool, StakedPosition } from '../type' +import { DisclaimerCheckBox } from './DisclaimerCheckBox' +import { useFixedStakeAPR } from '../hooks/useFixedStakeAPR' +import { StakeConfirmModal } from './StakeConfirmModal' +import { ModalTitle } from './ModalTitle' +import useIsBoost from '../hooks/useIsBoost' + +const StyledButton = styled(Button)` + flex-grow: 1; +` + +interface BodyParam { + setLockPeriod: Dispatch> + stakeCurrencyAmount: CurrencyAmount + alreadyStakedAmount: CurrencyAmount + lockPeriod: number + isStaked: boolean + boostAPR: Percent + lockAPR: Percent + unlockAPR: Percent + poolEndDay: number + isBoost: boolean + lastDayAction: number +} + +export function StakingModalTemplate({ + stakingToken, + pools, + initialLockPeriod, + stakedPeriods, + body, + head, + hideStakeButton, + stakedPositions = [], + onBack, + title, +}: { + title?: string + stakingToken: Token + pools: FixedStakingPool[] + stakedPositions?: StakedPosition[] + initialLockPeriod: number + stakedPeriods: number[] + head?: () => ReactNode + body: ReactNode | ((params: BodyParam) => ReactNode) + hideStakeButton?: boolean + onBack?: () => void +}) { + const { t } = useTranslation() + const [stakeAmount, setStakeAmount] = useState('') + const [isConfirmed, setIsConfirmed] = useState(false) + const [check, setCheck] = useState(false) + + const claimedPeriods = useMemo( + () => + stakedPositions + .filter((sP) => differenceInMilliseconds(sP.endLockTime * 1_000, new Date()) <= 0) + .map((sP) => sP.pool.lockPeriod), + [stakedPositions], + ) + + const [lockPeriod, setLockPeriod] = useState( + initialLockPeriod === null || initialLockPeriod === undefined + ? first(pools.filter((p) => !claimedPeriods.includes(p.lockPeriod))).lockPeriod + : initialLockPeriod, + ) + + const selectedStakedPosition = useMemo( + () => stakedPositions?.find((sP) => sP.pool.lockPeriod === lockPeriod), + [lockPeriod, stakedPositions], + ) + + const depositedAmount = useMemo(() => { + return CurrencyAmount.fromRawAmount( + stakingToken, + selectedStakedPosition ? selectedStakedPosition.userInfo.userDeposit.toString() : '0', + ) + }, [selectedStakedPosition, stakingToken]) + + const selectedPool = useMemo(() => pools.find((p) => p.lockPeriod === lockPeriod), [lockPeriod, pools]) + + const isBoost = useIsBoost({ + minBoostAmount: selectedPool?.minBoostAmount, + boostDayPercent: selectedPool?.boostDayPercent, + }) + + const [percent, setPercent] = useState(0) + const { balance: stakingTokenBalance } = useTokenBalance(stakingToken?.address) + const { fetchWithCatchTxError, loading: pendingTx } = useCatchTxError() + const fixedStakingContract = useFixedStakingContract() + const { callWithGasPrice } = useCallWithGasPrice() + const { toastSuccess } = useToast() + + const formattedUsdValueStaked = useStablecoinPriceAmount(stakingToken, toNumber(stakeAmount)) + + const rawAmount = getDecimalAmount(new BigNumber(stakeAmount), stakingToken.decimals) + + const stakeCurrencyAmount = CurrencyAmount.fromRawAmount(stakingToken, rawAmount.gt(0) ? rawAmount.toString() : '0') + + const totalPoolDeposited = CurrencyAmount.fromRawAmount( + stakingToken, + selectedPool ? selectedPool.totalDeposited.toString() : '0', + ) + + const maxStakeAmount = CurrencyAmount.fromRawAmount(stakingToken, selectedPool ? selectedPool.maxDeposit : '0') + const minStakeAmount = CurrencyAmount.fromRawAmount(stakingToken, selectedPool ? selectedPool.minDeposit : '0') + const maxStakePoolAmount = CurrencyAmount.fromRawAmount(stakingToken, selectedPool ? selectedPool.maxPoolAmount : '0') + + let error = null + + const totalStakedAmount = stakeCurrencyAmount.add(depositedAmount) + + if (stakeCurrencyAmount.greaterThan(stakingTokenBalance.toNumber())) { + error = t('Insufficient %symbol% balance', { symbol: stakingToken.symbol }) + } else if (totalStakedAmount.greaterThan(maxStakeAmount)) { + error = t('Maximum %amount% %symbol%', { + amount: maxStakeAmount.toSignificant(2), + symbol: stakingToken.symbol, + }) + } else if (stakeCurrencyAmount.lessThan(minStakeAmount)) { + error = t('Minimum %amount% %symbol%', { + amount: minStakeAmount.toSignificant(2), + symbol: stakingToken.symbol, + }) + } else if (stakeCurrencyAmount.add(totalPoolDeposited).greaterThan(maxStakePoolAmount)) { + error = t('Maximum pool %amount% %symbol%', { + amount: maxStakePoolAmount.toSignificant(2), + symbol: stakingToken.symbol, + }) + } + + const { approvalState, approveCallback } = useApproveCallback(stakeCurrencyAmount, fixedStakingContract?.address) + + const handleSubmission = useCallback(async () => { + const receipt = await fetchWithCatchTxError(() => { + const methodArgs = [selectedPool?.poolIndex, rawAmount.toString()] + + return callWithGasPrice(fixedStakingContract, 'deposit', methodArgs) + }) + + if (receipt?.status) { + toastSuccess( + t('Staked!'), + + {t('Your funds have been staked in the pool')} + , + ) + setIsConfirmed(true) + } + }, [ + callWithGasPrice, + fetchWithCatchTxError, + fixedStakingContract, + rawAmount, + selectedPool?.poolIndex, + t, + toastSuccess, + ]) + + const handleStakeInputChange = useCallback( + (input: string) => { + if (input) { + const convertedInput = new BigNumber(input).multipliedBy(getFullDecimalMultiplier(stakingToken.decimals)) + const percentage = Math.floor(convertedInput.dividedBy(stakingTokenBalance).multipliedBy(100).toNumber()) + setPercent(percentage > 100 ? 100 : percentage) + } else { + setPercent(0) + } + setStakeAmount(input) + }, + [stakingToken.decimals, stakingTokenBalance], + ) + + const handleChangePercent = useCallback( + (sliderPercent: number) => { + if (sliderPercent > 0) { + const percentageOfStakingMax = stakingTokenBalance.dividedBy(100).multipliedBy(sliderPercent) + const amountToStake = getFullDisplayBalance( + percentageOfStakingMax, + stakingToken.decimals, + stakingToken.decimals, + ) + setStakeAmount(amountToStake) + } else { + setStakeAmount('') + } + setPercent(sliderPercent) + }, + [stakingToken.decimals, stakingTokenBalance], + ) + + const isStaked = !!stakedPeriods.find((p) => p === lockPeriod) + + const aprParams = useMemo( + () => ({ + boostDayPercent: selectedPool?.boostDayPercent || 0, + lockDayPercent: selectedPool?.lockDayPercent || 0, + unlockDayPercent: selectedPool?.unlockDayPercent || 0, + }), + [selectedPool?.boostDayPercent, selectedPool?.lockDayPercent, selectedPool?.unlockDayPercent], + ) + + const { boostAPR, lockAPR, unlockAPR } = useFixedStakeAPR(aprParams) + + const params = useMemo( + () => ({ + alreadyStakedAmount: depositedAmount, + stakeCurrencyAmount, + setLockPeriod, + lockPeriod, + isStaked, + boostAPR, + lockAPR, + unlockAPR, + isBoost, + poolEndDay: selectedPool?.endDay || 0, + lastDayAction: selectedStakedPosition ? selectedStakedPosition.userInfo.lastDayAction : 0, + }), + [ + depositedAmount, + stakeCurrencyAmount, + lockPeriod, + isStaked, + boostAPR, + lockAPR, + unlockAPR, + isBoost, + selectedPool?.endDay, + selectedStakedPosition, + ], + ) + + const prevDepositedAmount = usePrevious(depositedAmount) + + useEffect(() => { + // TODO: WHen user stake, we need to show Staked Amount in Confirm Modal + // Currently, because of the delay in getting latest desposited amount + // To show latest FE, it needs to programmatically add Staked Amount + depostied Amount when SC data has not been synced + // and reset Staked Amount when the SC data is synced + // To show Confirm Modal correctly + if (prevDepositedAmount && !depositedAmount.equalTo(prevDepositedAmount)) { + setStakeAmount('') + } + }, [depositedAmount, prevDepositedAmount]) + + if (isConfirmed) { + return ( + } + width={['100%', '100%', '420px']} + maxWidth={['100%', , '420px']} + > + + + ) + } + + return ( + } + width={['100%', '100%', '420px']} + maxWidth={['100%', , '420px']} + onBack={onBack} + > + {head ? head() : null} + + + {t('Stake Amount')} + + + + + {stakingToken.symbol} + + + + + + {t('Balance: %balance%', { balance: getFullDisplayBalance(stakingTokenBalance, stakingToken.decimals) })} + + + + + + handleChangePercent(25)}> + 25% + + handleChangePercent(50)}> + 50% + + handleChangePercent(75)}> + 75% + + handleChangePercent(100)}> + {t('Max')} + + + + + {typeof body === 'function' ? body(params) : body} + + {hideStakeButton ? null : ( + <> + + {error ? ( + + ) : !rawAmount.gt(0) || approvalState === ApprovalState.APPROVED ? ( + + ) : ( + + )} + + {stakingTokenBalance.eq(0) ? ( + + ) : null} + + )} + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/UnlockedFixedTag.tsx b/apps/web/src/views/FixedStaking/components/UnlockedFixedTag.tsx new file mode 100644 index 0000000000000..e0614fb57e910 --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/UnlockedFixedTag.tsx @@ -0,0 +1,15 @@ +import { Tag, UnlockIcon } from '@pancakeswap/uikit' +import { CSSProperties, ReactNode } from 'react' + +export function UnlockedFixedTag({ children, style }: { children: ReactNode; style?: CSSProperties }) { + return ( + } + > + {children} + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/UnstakeBeforeEndedModal.tsx b/apps/web/src/views/FixedStaking/components/UnstakeBeforeEndedModal.tsx new file mode 100644 index 0000000000000..7cb03dd4c3e8a --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/UnstakeBeforeEndedModal.tsx @@ -0,0 +1,154 @@ +import { useTranslation } from '@pancakeswap/localization' +import { CurrencyAmount, Percent, Token } from '@pancakeswap/swap-sdk-core' +import { Box, Button, Flex, Message, MessageText, Modal, ModalV2, PreTitle, Text, useModalV2 } from '@pancakeswap/uikit' +import { LightCard } from 'components/Card' +import { ReactNode, useMemo } from 'react' + +import { FixedStakingPool, StakePositionUserInfo, UnstakeType } from '../type' +import { useHandleWithdrawSubmission } from '../hooks/useHandleWithdrawSubmission' +import FixedStakingOverview from './FixedStakingOverview' +import { AmountWithUSDSub } from './AmountWithUSDSub' +import { FixedStakingCalculator } from './FixedStakingCalculator' +import { ModalTitle } from './ModalTitle' +import { useShouldNotAllowWithdraw } from '../hooks/useStakedPools' + +export function UnstakeBeforeEnededModal({ + token, + lockPeriod, + lockAPR, + stakePositionUserInfo, + withdrawalFee, + poolIndex, + boostAPR, + unlockAPR, + pools, + poolEndDay, + lastDayAction, + unlockTime, + children, +}: { + unlockTime: number + lastDayAction: number + poolEndDay: number + boostAPR: Percent + unlockAPR: Percent + token: Token + lockPeriod: number + lockAPR: Percent + stakePositionUserInfo: StakePositionUserInfo + withdrawalFee: number + poolIndex: number + children: (openModal: () => void, notAllowWithdrawal: boolean) => ReactNode + pools: FixedStakingPool[] +}) { + const { t } = useTranslation() + const unstakeModal = useModalV2() + + const amountDeposit = useMemo( + () => CurrencyAmount.fromRawAmount(token, stakePositionUserInfo.userDeposit.toString()), + [stakePositionUserInfo.userDeposit, token], + ) + + const accrueInterest = useMemo( + () => CurrencyAmount.fromRawAmount(token, stakePositionUserInfo.accrueInterest.toString()), + [stakePositionUserInfo.accrueInterest, token], + ) + + const feePercent = useMemo(() => new Percent(withdrawalFee, 10000), [withdrawalFee]) + + const withdrawFee = useMemo( + () => amountDeposit.multiply(feePercent).add(accrueInterest), + [accrueInterest, amountDeposit, feePercent], + ) + + const totalGetAmount = useMemo( + () => amountDeposit.add(accrueInterest).subtract(withdrawFee), + [accrueInterest, amountDeposit, withdrawFee], + ) + + const { handleSubmission, pendingTx: loading } = useHandleWithdrawSubmission({ + poolIndex, + stakingToken: token, + onSuccess: () => unstakeModal.onDismiss(), + }) + + const notAllowWithdrawal = useShouldNotAllowWithdraw({ + lastDayAction, + lockPeriod, + }) + + return ( + <> + {children(unstakeModal.onOpen, notAllowWithdrawal)} + + } + width={['100%', '100%', '420px']} + maxWidth={['100%', , '420px']} + > + {t('Unstake Overview')} + + + + + {t('Commission')} + + + {withdrawFee.toSignificant(2)} {token.symbol} + + + + {t('for early withdrawal')} + + + + + {t('You will get')} + + + + + + + + {t('Position Details')} + + + } + /> + + + {t('No rewards are credited for early withdrawal, and commission is required')} + + + + + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/UnstakeModal.tsx b/apps/web/src/views/FixedStaking/components/UnstakeModal.tsx new file mode 100644 index 0000000000000..9f290f686fa0d --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/UnstakeModal.tsx @@ -0,0 +1,66 @@ +import { ModalV2, Modal, Text, Button, Card } from '@pancakeswap/uikit' +import { LightCard } from 'components/Card' +import { useTranslation } from '@pancakeswap/localization' + +import { AmountWithUSDSub } from './AmountWithUSDSub' +import { UnstakeType } from '../type' +import { ModalTitle } from './ModalTitle' + +export function UnstakeEndedModal({ + unstakeModal, + lockPeriod, + token, + handleSubmission, + stakeAmount, + accrueInterest, + loading, + onBack, +}) { + const { t } = useTranslation() + + return ( + + { + unstakeModal.onDismiss() + onBack() + }} + title={} + width={['100%', '100%', '420px']} + maxWidth={['100%', , '420px']} + > + + + {t('Unstaked Amount')} + + + + + + + {t('Reward Amount')} + + + + + + + + ) +} diff --git a/apps/web/src/views/FixedStaking/components/WithdrawalMessage.tsx b/apps/web/src/views/FixedStaking/components/WithdrawalMessage.tsx new file mode 100644 index 0000000000000..1d7a9daca478d --- /dev/null +++ b/apps/web/src/views/FixedStaking/components/WithdrawalMessage.tsx @@ -0,0 +1,37 @@ +import { useTranslation } from '@pancakeswap/localization' +import { Text, Link, Flex, InfoFilledIcon } from '@pancakeswap/uikit' +import floor from 'lodash/floor' + +export default function WithdrawalMessage({ lockPeriod }: { lockPeriod: number }) { + const { t } = useTranslation() + + return ( + + + + {t( + 'Funds will not be available for withdrawal for the first %days% days, and subsequently an early withdrawal fee will be applied if amount if unstaked before locked period is up. ', + { days: floor(lockPeriod / 3) }, + )} + + {t('Click here for more information')} + + + + ) +} diff --git a/apps/web/src/views/FixedStaking/constant.ts b/apps/web/src/views/FixedStaking/constant.ts new file mode 100644 index 0000000000000..b843911053da2 --- /dev/null +++ b/apps/web/src/views/FixedStaking/constant.ts @@ -0,0 +1,14 @@ +import { ChainId } from '@pancakeswap/chains' +import { bscTokens } from '@pancakeswap/tokens' + +import { FIXED_STAKING_PERIOD } from './type' + +export const PERIOD_ARR = [FIXED_STAKING_PERIOD.D30, FIXED_STAKING_PERIOD.D60, FIXED_STAKING_PERIOD.D90] + +export const PERCENT_DIGIT = 1000000000 + +export const DAYS_A_YEAR = 365 + +export const DISABLED_POOLS = { + [ChainId.BSC]: [bscTokens.wbnb.address], +} diff --git a/apps/web/src/views/FixedStaking/hooks/useCalculateProjectedReturnAmount.ts b/apps/web/src/views/FixedStaking/hooks/useCalculateProjectedReturnAmount.ts new file mode 100644 index 0000000000000..30a708f0a88b7 --- /dev/null +++ b/apps/web/src/views/FixedStaking/hooks/useCalculateProjectedReturnAmount.ts @@ -0,0 +1,52 @@ +import { CurrencyAmount, Percent, Token } from '@pancakeswap/swap-sdk-core' +import { useMemo } from 'react' +import { DAYS_A_YEAR } from '../constant' +import { useCurrentDay } from './useStakedPools' + +export function useCalculateProjectedReturnAmount({ + amountDeposit, + lockPeriod, + apr, + unlockAPR, + poolEndDay, + lastDayAction, +}: { + amountDeposit: CurrencyAmount + lockPeriod: number + poolEndDay: number + lastDayAction: number + apr: Percent + unlockAPR: Percent +}) { + const currentDay = useCurrentDay() + + const lockEndDay = lastDayAction + lockPeriod + + /** + * In locked period (currentDay <= lockEndDay) + * finalLockedPeriod = lockPeriod + * unlockPeriod = 0 + * After locked period (currentDay > lockEndDay) + * finalLockedPeriod = lockPeriod + * unlockPeriod = currentDay - lockEndDay + * After pool ended day (currentDay >= poolEnday) + * finalLockedPeriod = lockPeriod + * unlockPeriod = poolEndDay - lockEndDay + * Special case, if lockEndDay > poolEndDay + * finalLockedPeriod = poolEnday - lastDayAction + */ + const unlockPeriod = + currentDay <= lockEndDay ? 0 : currentDay >= poolEndDay ? poolEndDay - lockEndDay : currentDay - lockEndDay + const finalLockedPeriod = lockEndDay > poolEndDay ? poolEndDay - lastDayAction : lockPeriod + + const lockReward = amountDeposit?.multiply(finalLockedPeriod)?.multiply(apr.divide(DAYS_A_YEAR)) + const unlockReward = amountDeposit?.multiply(unlockPeriod)?.multiply(unlockAPR.divide(DAYS_A_YEAR)) + + return useMemo( + () => ({ + projectedReturnAmount: lockReward.add(unlockReward), + amountDeposit, + }), + [amountDeposit, lockReward, unlockReward], + ) +} diff --git a/apps/web/src/views/FixedStaking/hooks/useFixedStakeAPR.ts b/apps/web/src/views/FixedStaking/hooks/useFixedStakeAPR.ts new file mode 100644 index 0000000000000..034e3a2fb302d --- /dev/null +++ b/apps/web/src/views/FixedStaking/hooks/useFixedStakeAPR.ts @@ -0,0 +1,22 @@ +import { Percent } from '@pancakeswap/sdk' +import { useMemo } from 'react' +import { DAYS_A_YEAR, PERCENT_DIGIT } from '../constant' + +export function useFixedStakeAPR({ + boostDayPercent, + lockDayPercent, + unlockDayPercent, +}: { + boostDayPercent: number + lockDayPercent: number + unlockDayPercent: number +}) { + return useMemo( + () => ({ + boostAPR: new Percent(boostDayPercent || 0, PERCENT_DIGIT).multiply(DAYS_A_YEAR), + lockAPR: new Percent(lockDayPercent || 0, PERCENT_DIGIT).multiply(DAYS_A_YEAR), + unlockAPR: new Percent(unlockDayPercent || 0, PERCENT_DIGIT).multiply(DAYS_A_YEAR), + }), + [boostDayPercent, lockDayPercent, unlockDayPercent], + ) +} diff --git a/apps/web/src/views/FixedStaking/hooks/useHandleWithdrawSubmission.ts b/apps/web/src/views/FixedStaking/hooks/useHandleWithdrawSubmission.ts new file mode 100644 index 0000000000000..f48739863d2d2 --- /dev/null +++ b/apps/web/src/views/FixedStaking/hooks/useHandleWithdrawSubmission.ts @@ -0,0 +1,102 @@ +import { useTranslation } from '@pancakeswap/localization' +import { useToast, Link } from '@pancakeswap/uikit' +import { ToastDescriptionWithTx } from 'components/Toast' +import { useCallWithGasPrice } from 'hooks/useCallWithGasPrice' +import useCatchTxError from 'hooks/useCatchTxError' +import { useFixedStakingContract } from 'hooks/useContract' +import { createElement, useCallback, useMemo } from 'react' +import { CurrencyAmount, Token } from '@pancakeswap/swap-sdk-core' +import { useContractRead } from 'wagmi' +import useAccountActiveChain from 'hooks/useAccountActiveChain' + +import { getBep20Contract } from 'utils/contractHelpers' + +import { UnstakeType } from '../type' + +export function useHandleWithdrawSubmission({ + poolIndex, + stakingToken, + onSuccess, +}: { + poolIndex: number + stakingToken: Token + onSuccess?: () => void +}) { + const { t } = useTranslation() + const { toastSuccess, toastInfo } = useToast() + const { callWithGasPrice } = useCallWithGasPrice() + const { fetchWithCatchTxError, loading: pendingTx } = useCatchTxError() + const fixedStakingContract = useFixedStakingContract() + const { chainId } = useAccountActiveChain() + + const tokenContract = getBep20Contract(stakingToken.address) + + const { data } = useContractRead({ + chainId, + ...tokenContract, + enabled: Boolean(fixedStakingContract.address), + functionName: 'balanceOf', + args: [fixedStakingContract.address], + }) + + const stakingTokenBalanceInPool = CurrencyAmount.fromRawAmount(stakingToken, data || '0') + + const handleSubmission = useCallback( + async (type: UnstakeType, totalGetAmount: CurrencyAmount) => { + if (totalGetAmount.greaterThan(stakingTokenBalanceInPool)) { + const linkElement = createElement( + Link, + { + href: '/', + target: '_blank', + }, + t('Learn more'), + ) + + toastInfo('Withdawal approval is pending', [ + t('Please come back to check later at a certain amount of time'), + linkElement, + ]) + } else { + const receipt = await fetchWithCatchTxError(() => { + const methodArgs = [poolIndex] + + return callWithGasPrice(fixedStakingContract, type, methodArgs) + }) + + if (receipt?.status) { + const successComp = createElement( + ToastDescriptionWithTx, + { txHash: receipt.transactionHash }, + type === UnstakeType.HARVEST + ? t('Your harvest request has been submitted.') + : t('Your funds have been restaked in the pool'), + ) + + toastSuccess(t('Successfully submitted!'), successComp) + + if (onSuccess) onSuccess() + } + } + }, + [ + callWithGasPrice, + fetchWithCatchTxError, + fixedStakingContract, + onSuccess, + poolIndex, + stakingTokenBalanceInPool, + t, + toastInfo, + toastSuccess, + ], + ) + + return useMemo( + () => ({ + handleSubmission, + pendingTx, + }), + [handleSubmission, pendingTx], + ) +} diff --git a/apps/web/src/views/FixedStaking/hooks/useIsBoost.ts b/apps/web/src/views/FixedStaking/hooks/useIsBoost.ts new file mode 100644 index 0000000000000..7f71cb614dcc0 --- /dev/null +++ b/apps/web/src/views/FixedStaking/hooks/useIsBoost.ts @@ -0,0 +1,14 @@ +import { getBalanceAmount } from '@pancakeswap/utils/formatBalance' +import BigNumber from 'bignumber.js' + +import { useIfUserLocked } from './useStakedPools' + +export default function useIsBoost({ minBoostAmount, boostDayPercent }) { + const { locked, amount: lockedCakeAmount } = useIfUserLocked() + + return Boolean( + boostDayPercent > 0 && + locked && + lockedCakeAmount.gte(getBalanceAmount(new BigNumber(minBoostAmount || ('0' as unknown as BigNumber.Value)))), + ) +} diff --git a/apps/web/src/views/FixedStaking/hooks/useSelectedPeriod.tsx b/apps/web/src/views/FixedStaking/hooks/useSelectedPeriod.tsx new file mode 100644 index 0000000000000..bde4b994e86da --- /dev/null +++ b/apps/web/src/views/FixedStaking/hooks/useSelectedPeriod.tsx @@ -0,0 +1,43 @@ +import { differenceInMilliseconds } from 'date-fns' +import { useMemo, useState } from 'react' +import { FixedStakingPool } from '../type' + +export default function useSelectedPeriod({ pool, stakedPositions }) { + const [selectedPeriodIndex, setSelectedPeriodIndex] = useState(null) + + const claimedPeriods = useMemo( + () => + stakedPositions + .filter((sP) => differenceInMilliseconds(sP.endLockTime * 1_000, new Date()) <= 0) + .map((sP) => sP.pool.lockPeriod), + [stakedPositions], + ) + + const lockedPeriods = useMemo( + () => + stakedPositions + .filter((sP) => differenceInMilliseconds(sP.endLockTime * 1_000, new Date()) > 0) + .map((sP) => sP.pool.lockPeriod), + [stakedPositions], + ) + + const claimedIndexes: number[] = useMemo( + () => pool.pools.map((p, index) => (claimedPeriods.includes(p.lockPeriod) ? index : undefined)), + [claimedPeriods, pool.pools], + ) + + const lockedIndexes: number[] = useMemo( + () => pool.pools.map((p, index) => (lockedPeriods.includes(p.lockPeriod) ? index : undefined)), + [lockedPeriods, pool.pools], + ) + + const selectedPool: FixedStakingPool = pool.pools[selectedPeriodIndex] + + return { + setSelectedPeriodIndex, + selectedPeriodIndex, + selectedPool, + claimedIndexes, + lockedIndexes, + } +} diff --git a/apps/web/src/views/FixedStaking/hooks/useStakedPools.ts b/apps/web/src/views/FixedStaking/hooks/useStakedPools.ts new file mode 100644 index 0000000000000..c5589fa8c49da --- /dev/null +++ b/apps/web/src/views/FixedStaking/hooks/useStakedPools.ts @@ -0,0 +1,221 @@ +import { useOfficialsAndUserAddedTokens } from 'hooks/Tokens' +import useActiveWeb3React from 'hooks/useActiveWeb3React' +import { useFixedStakingContract, useVaultPoolContract } from 'hooks/useContract' +import { getAddress } from 'viem' +import { useContractRead } from 'wagmi' +import toNumber from 'lodash/toNumber' +import { useMemo } from 'react' +import { useSingleContractMultipleData } from 'state/multicall/hooks' +import BigNumber from 'bignumber.js' +import { VaultKey } from '@pancakeswap/pools' +import { VaultPosition, getVaultPosition } from 'utils/cakePool' +import { getBalanceAmount } from '@pancakeswap/utils/formatBalance' + +import { FixedStakingPool, StakedPosition } from '../type' +import { DISABLED_POOLS } from '../constant' + +export function useCurrentDay(): number { + const fixedStakingContract = useFixedStakingContract() + + const { chainId } = useActiveWeb3React() + + const { data } = useContractRead({ + abi: fixedStakingContract.abi, + address: fixedStakingContract.address as `0x${string}`, + functionName: 'getCurrentDay', + enabled: true, + watch: true, + chainId, + }) + + return (data || 0) as number +} + +export function useShouldNotAllowWithdraw({ lockPeriod, lastDayAction }) { + const poolLockPeriodUnit = lockPeriod / 3 + + const currentDay = useCurrentDay() + + return currentDay - lastDayAction <= poolLockPeriodUnit +} + +export function useIfUserLocked() { + const vaultPoolContract = useVaultPoolContract(VaultKey.CakeVault) + const { account, chainId } = useActiveWeb3React() + + const { data } = useContractRead({ + chainId, + abi: vaultPoolContract.abi, + address: vaultPoolContract.address, + functionName: 'userInfo', + args: [account], + enabled: !!account, + }) + + return useMemo(() => { + if (!Array.isArray(data)) + return { + locked: false, + amount: getBalanceAmount(new BigNumber(0)), + } + + const [userShares, , , , , lockEndTime, , locked, lockedAmount] = data + + const vaultPosition = getVaultPosition({ + userShares: new BigNumber(userShares as unknown as BigNumber.Value), + locked, + lockEndTime: lockEndTime.toString(), + }) + + return { + locked: VaultPosition.Locked === vaultPosition, + amount: getBalanceAmount(new BigNumber(lockedAmount as unknown as BigNumber.Value)), + } + }, [data]) +} +export function useStakedPositionsByUser(poolIndexes: number[]): StakedPosition[] { + const fixedStakingContract = useFixedStakingContract() + const { account } = useActiveWeb3React() + const tokens = useOfficialsAndUserAddedTokens() + + const results = useSingleContractMultipleData({ + contract: { + abi: fixedStakingContract.abi, + address: fixedStakingContract.address, + }, + functionName: 'getUserInfo', + args: account ? poolIndexes.map((index) => [index, account]) : [], + }) + + const currentDay = useCurrentDay() + + return useMemo(() => { + if (!Array.isArray(results)) return [] + return results + .map(({ result }, index) => { + if (!Array.isArray(result)) return undefined + + const userInfoUserDeposit = new BigNumber(result[0].userInfo.userDeposit) + + if (userInfoUserDeposit.eq(0)) { + return undefined + } + + const position = result[0] + const endPoolTime = position.pool.endDay * 86400 + 43200 + const endLockTime = position.endLockTime > endPoolTime ? endPoolTime : position.endLockTime + + const poolLockPeriodUnit = position.pool.lockPeriod / 3 + const { lastDayAction } = position.userInfo + const { withdrawalCut1, withdrawalCut2 } = position.pool + + let withdrawalFee: BigNumber | null = null + + const days = currentDay - lastDayAction + + // logic copied from Smart Contract + if (days <= poolLockPeriodUnit * 2) { + withdrawalFee = withdrawalCut1 + } else if (days <= poolLockPeriodUnit * 3) { + withdrawalFee = withdrawalCut2 + } + + return { + ...position, + pool: { + ...position.pool, + withdrawalFee, + poolIndex: poolIndexes[index], + token: tokens[getAddress(position.pool.token)], + }, + endLockTime, + } + }) + .filter(Boolean) + }, [currentDay, poolIndexes, results, tokens]) +} + +export function useStakedPools(): FixedStakingPool[] { + const fixedStakingContract = useFixedStakingContract() + const tokens = useOfficialsAndUserAddedTokens() + const { chainId } = useActiveWeb3React() + + const { data: poolLength } = useContractRead({ + abi: fixedStakingContract.abi, + address: fixedStakingContract.address as `0x${string}`, + functionName: 'poolLength', + chainId, + args: [], + }) + + const numberOfPools = poolLength ? toNumber(poolLength.toString()) : 0 + + const fixedStakePools = useSingleContractMultipleData({ + contract: { + abi: fixedStakingContract.abi, + address: fixedStakingContract.address, + }, + functionName: 'pools', + args: useMemo( + () => Array.from(Array(numberOfPools).keys()).map((index) => [BigInt(index)] as const), + [numberOfPools], + ), + }) + + return useMemo(() => { + if (!fixedStakePools?.length) return [] + + return fixedStakePools + .map(({ result: fixedStakePool }, index) => { + if (!fixedStakePool) return null + + const token = tokens[getAddress(fixedStakePool[0])] + + const disabled = DISABLED_POOLS[chainId]?.includes(token.address) + + if (disabled) { + return null + } + + /* + struct Pool { + IERC20Upgradeable token; + uint32 endDay; + uint32 lockDayPercent; + uint32 boostDayPercent; + uint32 unlockDayPercent; + uint32 lockPeriod; // Multiples of 3 + uint32 withdrawalCut1; + uint32 withdrawalCut2; + bool depositEnabled; + uint128 maxDeposit; + uint128 minDeposit; + uint128 totalDeposited; + uint128 maxPoolAmount; + uint128 minBoostAmount; + } + */ + + return { + poolIndex: index, + token, + endDay: fixedStakePool[1], + lockDayPercent: fixedStakePool[2], + boostDayPercent: fixedStakePool[3], + unlockDayPercent: fixedStakePool[4], + lockPeriod: fixedStakePool[5], + withdrawalCut1: fixedStakePool[6], + withdrawalCut2: fixedStakePool[7], + // set widthdrawalFee as withdrawalCut2 to display pools' fee with unconnected account + withdrawalFee: fixedStakePool[7], + depositEnabled: fixedStakePool[8], + maxDeposit: fixedStakePool[9], + minDeposit: fixedStakePool[10], + totalDeposited: new BigNumber(fixedStakePool[11]), + maxPoolAmount: fixedStakePool[12], + minBoostAmount: fixedStakePool[13], + } + }) + .filter(Boolean) + }, [chainId, fixedStakePools, tokens]) +} diff --git a/apps/web/src/views/FixedStaking/index.tsx b/apps/web/src/views/FixedStaking/index.tsx new file mode 100644 index 0000000000000..0091a40b97f5e --- /dev/null +++ b/apps/web/src/views/FixedStaking/index.tsx @@ -0,0 +1,111 @@ +import { useTranslation } from '@pancakeswap/localization' +import { Flex, FlexLayout, Heading, PageHeader, ToggleView, ViewMode } from '@pancakeswap/uikit' +import { Pool } from '@pancakeswap/widgets-internal' +import Page from 'components/Layout/Page' +import { useMemo, useState } from 'react' +import min from 'lodash/min' +import max from 'lodash/max' +import BigNumber from 'bignumber.js' + +import { useStakedPools, useStakedPositionsByUser } from './hooks/useStakedPools' +import { FixedStakingCard } from './components/FixedStakingCard' +import FixedStakingRow from './components/FixedStakingRow' +import { FixedStakingPool } from './type' + +const FixedStaking = () => { + const { t } = useTranslation() + + const [viewMode, setViewMode] = useState(ViewMode.TABLE) + + const displayPools = useStakedPools() + + const stakedPositions = useStakedPositionsByUser(displayPools.map((p) => p.poolIndex)) + + // Groupd pools with same token + const groupPoolsByToken = useMemo>(() => { + return displayPools + .filter((pool) => pool.token) + .reduce((pools, pool) => { + if (Array.isArray(pools[pool.token.address])) { + pools[pool.token.address].push(pool) + + return pools + } + return { + [pool.token.address]: [pool], + ...pools, + } + }, {}) + }, [displayPools]) + + const poolGroup = useMemo(() => { + return Object.keys(groupPoolsByToken).reduce((poolGroupResult, key) => { + const pools = groupPoolsByToken[key] + + const minLockDayPercent = min(pools.map((p) => p.lockDayPercent || p.boostDayPercent)) + const maxLockDayPercent = max(pools.map((p) => p.boostDayPercent || p.lockDayPercent)) + + const totalDeposited = pools.reduce((sum, p) => sum.plus(p.totalDeposited), new BigNumber(0)) + + return { + [key]: { + token: pools[0].token, + minLockDayPercent, + maxLockDayPercent, + totalDeposited, + pools, + }, + ...poolGroupResult, + } + }, {}) + }, [groupPoolsByToken]) + + return ( + <> + + + + + {t('Simple Staking')} + + + {t('Single-Sided Simple Earn Staking')} + + + + + + + + + {viewMode === ViewMode.TABLE ? ( + + {Object.keys(poolGroup).map((key) => ( + stakedPool.token.address === poolGroup[key].token.address, + )} + pool={poolGroup[key]} + /> + ))} + + ) : ( + + {Object.keys(poolGroup).map((key) => ( + stakedPool.token.address === poolGroup[key].token.address, + )} + /> + ))} + + )} + + + ) +} + +export default FixedStaking diff --git a/apps/web/src/views/FixedStaking/type.ts b/apps/web/src/views/FixedStaking/type.ts new file mode 100644 index 0000000000000..4fc96050d241e --- /dev/null +++ b/apps/web/src/views/FixedStaking/type.ts @@ -0,0 +1,53 @@ +import { Token } from '@pancakeswap/sdk' +import BigNumber from 'bignumber.js' + +export enum FIXED_STAKING_PERIOD { + D30 = '30D', + D60 = '60D', + D90 = '90D', +} + +export interface FixedStakingPool { + poolIndex: number + token: Token + endDay: number + lockDayPercent: number + boostDayPercent: number + unlockDayPercent: number + lockPeriod: number + withdrawalCut1: number + withdrawalCut2: number + withdrawalFee: number + depositEnabled: boolean + maxDeposit: number + minDeposit: number + totalDeposited: BigNumber + maxPoolAmount: number + minBoostAmount: number +} + +export interface StakePositionUserInfo { + accrueInterest: BigNumber + boost: boolean + lastDayAction: number + userDeposit: BigNumber +} + +export interface StakedPosition { + endLockTime: number + userInfo: StakePositionUserInfo + pool: FixedStakingPool +} + +export interface PoolGroup { + token: Token + minLockDayPercent: number + maxLockDayPercent: number + totalDeposited: BigNumber + pools: FixedStakingPool[] +} + +export enum UnstakeType { + WITHDRAW = 'withdraw', + HARVEST = 'harvest', +} diff --git a/apps/web/src/views/Home/components/EcoSystemSection/index.tsx b/apps/web/src/views/Home/components/EcoSystemSection/index.tsx index e699f9f6771cd..e46c31e47e8d2 100644 --- a/apps/web/src/views/Home/components/EcoSystemSection/index.tsx +++ b/apps/web/src/views/Home/components/EcoSystemSection/index.tsx @@ -261,13 +261,6 @@ const useEarnBlockData = () => { defaultImage: earnLiquidStakingPurple, path: '/liquid-staking', }, - // { - // title: t('Coming Soon'), - // description: t('Fixed staking'), - // ctaTitle: t('Learn More'), - // image: earnFixedStaking, - // defaultImage: earnFixedStakingPurple, - // }, ] }, [t]) } diff --git a/apps/web/src/views/Pools/components/PoolsTable/ActionPanel/ActionPanel.tsx b/apps/web/src/views/Pools/components/PoolsTable/ActionPanel/ActionPanel.tsx index 6a22c228b19c9..0e933f517c540 100644 --- a/apps/web/src/views/Pools/components/PoolsTable/ActionPanel/ActionPanel.tsx +++ b/apps/web/src/views/Pools/components/PoolsTable/ActionPanel/ActionPanel.tsx @@ -39,7 +39,7 @@ const collapseAnimation = keyframes` } ` -const StyledActionPanel = styled.div<{ expanded: boolean }>` +export const StyledActionPanel = styled.div<{ expanded: boolean }>` animation: ${({ expanded }) => expanded ? css` @@ -61,7 +61,7 @@ const StyledActionPanel = styled.div<{ expanded: boolean }>` } ` -const ActionContainer = styled.div<{ isAutoVault?: boolean; hasBalance?: boolean }>` +export const ActionContainer = styled(Box)<{ isAutoVault?: boolean; hasBalance?: boolean }>` display: flex; flex-direction: column; flex: 1; @@ -83,7 +83,7 @@ interface ActionPanelProps { expanded: boolean } -const InfoSection = styled(Box)` +export const InfoSection = styled(Box)` flex-grow: 0; flex-shrink: 0; flex-basis: auto; diff --git a/apps/web/src/views/Pools/components/PoolsTable/Cells/NameCell.tsx b/apps/web/src/views/Pools/components/PoolsTable/Cells/NameCell.tsx index 22ec70f91b6b2..774ca89016a47 100644 --- a/apps/web/src/views/Pools/components/PoolsTable/Cells/NameCell.tsx +++ b/apps/web/src/views/Pools/components/PoolsTable/Cells/NameCell.tsx @@ -16,7 +16,7 @@ interface NameCellProps { pool: Pool.DeserializedPool } -const StyledCell = styled(Pool.BaseCell)` +export const StyledCell = styled(Pool.BaseCell)` flex: 5; flex-direction: row; padding-left: 12px; diff --git a/packages/localization/src/config/translations.json b/packages/localization/src/config/translations.json index badac7c0ae6f5..0e406b50653f9 100644 --- a/packages/localization/src/config/translations.json +++ b/packages/localization/src/config/translations.json @@ -35,6 +35,20 @@ "Unset": "Unset", "Continue Staking": "Continue Staking", "Active": "Active", + "Successfully submitted!": "Successfully submitted!", + "Stake Amount": "Stake Amount", + "Position Details": "Position Details", + "APR:": "APR:", + "Stake Periods:": "Stake Periods:", + "Total Staked:": "Total Staked:", + "Simple Staking": "Simple Staking", + "Position Overview": "Position Overview", + "Duration": "Duration", + "days": "days", + "Your harvest request has been submitted.": "Your harvest request has been submitted.", + "Ended": "Ended", + "Single-Sided Simple Earn Staking": "Single-Sided Simple Earn Staking", + "No rewards are credited for early withdrawal, and commission is required": "No rewards are credited for early withdrawal, and commission is required", "View Position": "View Position", "Yield booster available": "Yield booster available", "Unset other boosters to activate": "Unset other boosters to activate", @@ -69,6 +83,36 @@ "Farms": "Farms", "Pools": "Pools", "NFT": "NFT", + "Fixed-staking Ends": "Fixed-staking Ends", + "Restake period ends on": "Restake period ends on", + "Claim Reward & Restake": "Claim Reward & Restake", + "Stake Duration": "Stake Duration", + "Staked Position": "Staked Position", + "Locked Cake Boost": "Locked Cake Boost", + "Stake Period Ends": "Stake Period Ends", + "Projected Return": "Projected Return", + "Restake": "Restake", + "Claim Reward": "Claim Reward", + "Claimed amount will be sent to your wallet": "Claimed amount will be sent to your wallet", + "Restaking": "Restaking", + "Fixed Staking Ends On": "Fixed Staking Ends On", + "Confirm Claim & Restake": "Confirm Claim & Restake", + "Adding stake to the position will restart the lock and withdrawal period.": "Adding stake to the position will restart the lock and withdrawal period.", + "Stake Period": "Stake Period", + "Reward": "Reward", + "Est. Reward": "Est. Reward", + "Unstake Overview": "Unstake Overview", + "for early withdrawal": "for early withdrawal", + "You will get": "You will get", + "Unstaked Amount": "Unstaked Amount", + "Reward Amount": "Reward Amount", + "%lockPeriod% days": "%lockPeriod% days", + "Stake & Earn": "Stake & Earn", + "Stake Periods": "Stake Periods", + "Staked Amount": "Staked Amount", + "Click here for more information": "Click here for more information", + "Funds will not be available for withdrawal for the first %days% days, and subsequently an early withdrawal fee will be applied if amount if unstaked before locked period is up. ": "Funds will not be available for withdrawal for the first %days% days, and subsequently an early withdrawal fee will be applied if amount if unstaked before locked period is up. ", + "Your funds have been restaked in the pool": "Your funds have been restaked in the pool", "This collection has been inactived for a while. Trade at your own risk.": "This collection has been inactived for a while. Trade at your own risk.", "Info": "Info", "IFO": "IFO", @@ -1082,7 +1126,13 @@ "You had %amount% ticket this round": "You had %amount% ticket this round", "Remove Liquidity": "Remove Liquidity", "Minimum received": "Minimum received", + "Maximum %amount% %symbol%": "Maximum %amount% %symbol%", + "Minimum %amount% %symbol%": "Minimum %amount% %symbol%", + "Maximum pool %amount% %symbol%": "Maximum pool %amount% %symbol%", "Maximum sold": "Maximum sold", + "Please come back to check later at a certain amount of time": "Please come back to check later at a certain amount of time", + "Learn more": "Learn more", + "Ends In": "Ends In", "The difference between the market price and estimated price due to trade size.": "The difference between the market price and estimated price due to trade size.", "The difference between the market price and your price due to trade size.": "The difference between the market price and your price due to trade size.", "Liquidity Provider Fee": "Liquidity Provider Fee", @@ -1090,6 +1140,7 @@ "Output will be sent to %recipient%": "Output will be sent to %recipient%", "Price Updated": "Price Updated", "Accept": "Accept", + "Confirm Unstake": "Confirm Unstake", "For each trade a %amount% fee is paid": "For each trade a %amount% fee is paid", "%amount% to LP token holders": "%amount% to LP token holders", "%amount% to the Treasury": "%amount% to the Treasury", @@ -2702,7 +2753,6 @@ "Buy Now": "Buy Now", "Stake Now": "Stake Now", "Earn rewards while retaining asset flexibility": "Earn rewards while retaining asset flexibility", - "Fixed staking": "Fixed staking", "Forecast token prices within minutes": "Forecast token prices within minutes", "Try Now": "Try Now", "Immersive PvP & PvE tower-defense GameFi": "Immersive PvP & PvE tower-defense GameFi", diff --git a/packages/uikit/src/components/Button/theme.ts b/packages/uikit/src/components/Button/theme.ts index f475fee797760..2a6045f2daaa6 100644 --- a/packages/uikit/src/components/Button/theme.ts +++ b/packages/uikit/src/components/Button/theme.ts @@ -63,5 +63,8 @@ export const styleVariants = { background: vars.colors.gradientBubblegum, color: "textSubtle", boxShadow: "none", + ":disabled": { + background: vars.colors.disabled, + }, }, }; diff --git a/packages/uikit/src/components/ButtonMenu/ButtonMenu.tsx b/packages/uikit/src/components/ButtonMenu/ButtonMenu.tsx index 93764e418e2cd..62cc46065a9b2 100644 --- a/packages/uikit/src/components/ButtonMenu/ButtonMenu.tsx +++ b/packages/uikit/src/components/ButtonMenu/ButtonMenu.tsx @@ -46,7 +46,6 @@ const StyledButtonMenu = styled.div.withConfig({ opacity: 0.5; & > button:disabled { - background-color: transparent; color: ${variant === variants.PRIMARY ? theme.colors.primary : theme.colors.textSubtle}; } `; @@ -71,7 +70,7 @@ const ButtonMenu: React.FC> = ({ {Children.map(children, (child: ReactElement, index) => { return cloneElement(child, { isActive: activeIndex === index, - onClick: onItemClick ? () => onItemClick(index) : undefined, + onClick: onItemClick ? (e: React.MouseEvent) => onItemClick(index, e) : undefined, scale, variant, disabled, diff --git a/packages/uikit/src/components/ButtonMenu/types.ts b/packages/uikit/src/components/ButtonMenu/types.ts index df0503749fc19..4ab995bd6b539 100644 --- a/packages/uikit/src/components/ButtonMenu/types.ts +++ b/packages/uikit/src/components/ButtonMenu/types.ts @@ -7,9 +7,9 @@ export interface ButtonMenuItemProps extends BaseButtonProps { } export interface ButtonMenuProps extends SpaceProps { - variant?: typeof variants.PRIMARY | typeof variants.SUBTLE; + variant?: typeof variants.PRIMARY | typeof variants.SUBTLE | typeof variants.LIGHT; activeIndex?: number; - onItemClick?: (index: number) => void; + onItemClick?: (index: number, event: React.MouseEvent) => void; scale?: Scale; disabled?: boolean; children: ReactElement[]; diff --git a/packages/uikit/src/components/Card/Card.tsx b/packages/uikit/src/components/Card/Card.tsx index 02dd8c074896e..cc020ece31b07 100644 --- a/packages/uikit/src/components/Card/Card.tsx +++ b/packages/uikit/src/components/Card/Card.tsx @@ -2,10 +2,16 @@ import React from "react"; import { StyledCard, StyledCardInner } from "./StyledCard"; import { CardProps } from "./types"; -const Card: React.FC> = ({ ribbon, children, background, ...props }) => { +const Card: React.FC> = ({ + ribbon, + children, + background, + innerCardProps, + ...props +}) => { return ( - + {ribbon} {children} diff --git a/packages/uikit/src/components/Card/types.ts b/packages/uikit/src/components/Card/types.ts index 1aa3ef7eed37e..1e02346f5d535 100644 --- a/packages/uikit/src/components/Card/types.ts +++ b/packages/uikit/src/components/Card/types.ts @@ -1,6 +1,7 @@ import { HTMLAttributes } from "react"; import { SpaceProps } from "styled-system"; import { Colors } from "../../theme/types"; +import { BoxProps } from "../Box/types"; export interface CardRibbonProps extends SpaceProps, HTMLAttributes { variantColor?: keyof Colors; @@ -31,4 +32,5 @@ export interface CardProps extends SpaceProps, HTMLAttributes { ribbon?: React.ReactNode; borderBackground?: string; background?: string; + innerCardProps?: BoxProps; } diff --git a/packages/uikit/src/components/RoiCalculatorModal/RoiCard.tsx b/packages/uikit/src/components/RoiCalculatorModal/RoiCard.tsx index 330d5c48ce201..98b65ccf8938c 100644 --- a/packages/uikit/src/components/RoiCalculatorModal/RoiCard.tsx +++ b/packages/uikit/src/components/RoiCalculatorModal/RoiCard.tsx @@ -1,7 +1,7 @@ import { useRef, useEffect, useState } from "react"; import { styled } from "styled-components"; import { useTranslation } from "@pancakeswap/localization"; -import { CalculatorMode, RoiCalculatorReducerState } from "./useRoiCalculatorReducer"; +import { CalculatorMode, RoiCalculatorDataState } from "./useRoiCalculatorReducer"; import { Box, Flex } from "../Box"; import { Text } from "../Text"; import { Input } from "../Input"; @@ -72,11 +72,18 @@ export const RoiDollarAmount = styled(Text)<{ fadeOut: boolean }>` `} `; +export interface RoiCalculatorCardState { + controls: { + mode: CalculatorMode; + }; + data: Omit; +} + interface RoiCardProps { earningTokenSymbol: string; - calculatorState: RoiCalculatorReducerState; - setTargetRoi: (amount: string) => void; - setCalculatorMode: (mode: CalculatorMode) => void; + calculatorState: RoiCalculatorCardState; + setTargetRoi?: (amount: string) => void; + setCalculatorMode?: (mode: CalculatorMode) => void; } const RoiCard: React.FC> = ({ @@ -99,7 +106,7 @@ const RoiCard: React.FC> = ({ }, [mode]); const onEnterEditing = () => { - setCalculatorMode(CalculatorMode.PRINCIPAL_BASED_ON_ROI); + if (setCalculatorMode) setCalculatorMode(CalculatorMode.PRINCIPAL_BASED_ON_ROI); setExpectedRoi( roiUSD.toLocaleString("en", { minimumFractionDigits: roiUSD > MILLION ? 0 : 2, @@ -109,12 +116,12 @@ const RoiCard: React.FC> = ({ }; const onExitRoiEditing = () => { - setCalculatorMode(CalculatorMode.ROI_BASED_ON_PRINCIPAL); + if (setCalculatorMode) setCalculatorMode(CalculatorMode.ROI_BASED_ON_PRINCIPAL); }; const handleExpectedRoiChange = (event: React.ChangeEvent) => { if (event.currentTarget.validity.valid) { const roiAsString = event.target.value.replace(/,/g, "."); - setTargetRoi(roiAsString); + if (setTargetRoi) setTargetRoi(roiAsString); setExpectedRoi(roiAsString); } }; @@ -157,9 +164,11 @@ const RoiCard: React.FC> = ({ })} - - - + {setTargetRoi && setCalculatorMode ? ( + + + + ) : null} )} diff --git a/packages/uikit/src/components/RoiCalculatorModal/useRoiCalculatorReducer.ts b/packages/uikit/src/components/RoiCalculatorModal/useRoiCalculatorReducer.ts index 20b5af48ee4c5..2682067bf6e0e 100644 --- a/packages/uikit/src/components/RoiCalculatorModal/useRoiCalculatorReducer.ts +++ b/packages/uikit/src/components/RoiCalculatorModal/useRoiCalculatorReducer.ts @@ -32,22 +32,26 @@ export enum CalculatorMode { PRINCIPAL_BASED_ON_ROI, // User edits ROI value and sees what principal they need to invest to reach it } +export interface RoiCalculatorControlState { + compounding: boolean; // Compounding checkbox state + compoundingFrequency: number; // Compounding frequency in number of compounds per day + activeCompoundingIndex: number; // index of active compounding button in ButtonMenu + stakingDuration: number; // index of active staking duration button in ButtonMenu + mode: CalculatorMode; + editingCurrency: EditingCurrency; +} + +export interface RoiCalculatorDataState { + principalAsToken: string; // Used as value for Inputs + principalAsUSD: string; // Used as value for Inputs + roiUSD: number; + roiTokens: number; + roiPercentage: number; // ROI expressed in percentage relative to principal +} + export interface RoiCalculatorReducerState { - controls: { - compounding: boolean; // Compounding checkbox state - compoundingFrequency: number; // Compounding frequency in number of compounds per day - activeCompoundingIndex: number; // index of active compounding button in ButtonMenu - stakingDuration: number; // index of active staking duration button in ButtonMenu - mode: CalculatorMode; - editingCurrency: EditingCurrency; - }; - data: { - principalAsToken: string; // Used as value for Inputs - principalAsUSD: string; // Used as value for Inputs - roiUSD: number; - roiTokens: number; - roiPercentage: number; // ROI expressed in percentage relative to principal - }; + controls: RoiCalculatorControlState; + data: RoiCalculatorDataState; } const defaultState: RoiCalculatorReducerState = { diff --git a/packages/uikit/src/components/Svg/Icons/StarCircle.tsx b/packages/uikit/src/components/Svg/Icons/StarCircle.tsx new file mode 100644 index 0000000000000..d49ba021f0e6c --- /dev/null +++ b/packages/uikit/src/components/Svg/Icons/StarCircle.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import Svg from "../Svg"; +import { SvgProps } from "../types"; + +const StarCircle: React.FC> = (props) => { + return ( + + + + ); +}; + +export default StarCircle; diff --git a/packages/uikit/src/components/Svg/index.tsx b/packages/uikit/src/components/Svg/index.tsx index c33c5277d077e..eb3d2d7791411 100644 --- a/packages/uikit/src/components/Svg/index.tsx +++ b/packages/uikit/src/components/Svg/index.tsx @@ -188,4 +188,5 @@ export { GovernanceIcon } from "./Icons/Governance"; export { FavoriteBorderIcon } from "./Icons/FavoriteBorder"; export { BarChartIcon } from "./Icons/BarChart"; export { StoreIcon } from "./Icons/Store"; +export { default as StarCircle } from "./Icons/StarCircle"; export type { SvgProps } from "./types"; diff --git a/packages/uikit/src/components/index.ts b/packages/uikit/src/components/index.ts index bde9d89050386..7e085738b02da 100644 --- a/packages/uikit/src/components/index.ts +++ b/packages/uikit/src/components/index.ts @@ -69,3 +69,4 @@ export * from "./DynamicSection"; export * from "./Chart"; export * from "./AtomBox"; +export { default as RoiCard } from "./RoiCalculatorModal/RoiCard"; diff --git a/packages/widgets-internal/pool/withCollectModal.tsx b/packages/widgets-internal/pool/withCollectModal.tsx index bb85f6537588e..979cdd3c7b261 100644 --- a/packages/widgets-internal/pool/withCollectModal.tsx +++ b/packages/widgets-internal/pool/withCollectModal.tsx @@ -1,22 +1,19 @@ import BigNumber from 'bignumber.js' -import { ReactElement } from 'react' +import { ReactElement, ReactNode } from 'react' import { useTranslation } from '@pancakeswap/localization' import { getFullDisplayBalance, getBalanceNumber, formatNumber } from '@pancakeswap/utils/formatBalance' import { Flex, Heading, Button, Text, Skeleton, Balance, useModal } from '@pancakeswap/uikit' - import { CollectModalProps } from './CollectModal' import { HarvestAction as TableHarvestAction } from './PoolsTable/HarvestAction' import { HarvestActionsProps } from './types' -const HarvestActions: React.FC> = ({ - earnings, - isLoading, - onPresentCollect, - earningTokenPrice, - earningTokenBalance, - earningTokenDollarBalance, -}) => { - const { t } = useTranslation() +export const BalanceWithActions: React.FC< + React.PropsWithChildren< + Omit & { + actions: ReactNode + } + > +> = ({ earnings, isLoading, earningTokenPrice, earningTokenBalance, earningTokenDollarBalance, actions }) => { const hasEarnings = earnings.toNumber() > 0 return ( @@ -52,13 +49,38 @@ const HarvestActions: React.FC> = ( )} - + {actions} ) } +export const HarvestActions: React.FC> = ({ + earnings, + isLoading, + onPresentCollect, + earningTokenPrice, + earningTokenBalance, + earningTokenDollarBalance, +}) => { + const { t } = useTranslation() + const hasEarnings = earnings.toNumber() > 0 + + return ( + + {t('Harvest')} + + } + /> + ) +} + interface WithHarvestActionsProps { earnings: BigNumber earningTokenSymbol: string