From a6cac349343e64942173adbaad67910b8a3930d5 Mon Sep 17 00:00:00 2001 From: Victor Creed Date: Wed, 20 Sep 2023 10:47:42 +0300 Subject: [PATCH 1/7] feat: improved fee quering --- .changeset/smart-llamas-care.md | 5 + .../RewardsPage/hooks/useGetFeesEarned.ts | 289 +++++++----------- apps/frontend/src/hooks/useMulticall.ts | 51 ++-- packages/contracts/src/abis/feeSharing.json | 100 +++++- packages/contracts/src/types.ts | 5 +- packages/contracts/src/utils/global.ts | 17 +- 6 files changed, 249 insertions(+), 218 deletions(-) create mode 100644 .changeset/smart-llamas-care.md diff --git a/.changeset/smart-llamas-care.md b/.changeset/smart-llamas-care.md new file mode 100644 index 000000000..b780e14cb --- /dev/null +++ b/.changeset/smart-llamas-care.md @@ -0,0 +1,5 @@ +--- +'@sovryn/contracts': patch +--- + +feat: contracts exports ethers contract instance too diff --git a/apps/frontend/src/app/5_pages/RewardsPage/hooks/useGetFeesEarned.ts b/apps/frontend/src/app/5_pages/RewardsPage/hooks/useGetFeesEarned.ts index b08e82f73..f9313b968 100644 --- a/apps/frontend/src/app/5_pages/RewardsPage/hooks/useGetFeesEarned.ts +++ b/apps/frontend/src/app/5_pages/RewardsPage/hooks/useGetFeesEarned.ts @@ -1,204 +1,145 @@ -import { useEffect, useState, useMemo, useCallback } from 'react'; +import { useEffect, useState, useCallback } from 'react'; -import { SupportedTokens } from '@sovryn/contracts'; +import { BigNumber } from 'ethers'; + +import { SupportedTokens, getProtocolContract } from '@sovryn/contracts'; +import { getTokenContract } from '@sovryn/contracts'; +import { getProvider } from '@sovryn/ethers-provider'; + +import { defaultChainId } from '../../../../config/chains'; import { useAccount } from '../../../../hooks/useAccount'; -import { - useGetProtocolContract, - useGetTokenContract, -} from '../../../../hooks/useGetContract'; +import { useIsMounted } from '../../../../hooks/useIsMounted'; import { useMulticall } from '../../../../hooks/useMulticall'; import { EarnedFee } from '../RewardsPage.types'; -import { useGetTokenCheckpoints } from './useGetTokenCheckpoints'; -const DEFAULT_RBTC_DUMMY_ADDRESS = '0xeabd29be3c3187500df86a2613c6470e12f2d77d'; +const MAX_CHECKPOINTS = 50; +const FEE_ASSETS = [ + SupportedTokens.rbtc, + SupportedTokens.wrbtc, + SupportedTokens.sov, + SupportedTokens.zusd, + SupportedTokens.mynt, +]; + +let btcDummyAddress: string; + +const getRbtcDummyAddress = async () => { + if (!btcDummyAddress) { + const { contract } = await getProtocolContract( + 'feeSharing', + defaultChainId, + ); + btcDummyAddress = await contract( + getProvider(defaultChainId), + ).RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT(); + } + return btcDummyAddress; +}; export const useGetFeesEarned = () => { + const isMounted = useIsMounted(); const { account } = useAccount(); + const multicall = useMulticall(); + const [loading, setLoading] = useState(true); - const [RBTCDummyAddress, setRBTCDummyAddress] = useState( - DEFAULT_RBTC_DUMMY_ADDRESS, - ); - - const feeSharing = useGetProtocolContract('feeSharing'); - const sovContract = useGetTokenContract('sov'); - const myntContract = useGetTokenContract('mynt'); - const zusdContract = useGetTokenContract('zusd'); - - const contractAddresses = useMemo( - () => ({ - [SupportedTokens.rbtc]: RBTCDummyAddress, - [SupportedTokens.sov]: sovContract?.address!, - [SupportedTokens.zusd]: zusdContract?.address!, - [SupportedTokens.mynt]: myntContract?.address!, - }), - [ - RBTCDummyAddress, - myntContract?.address, - sovContract?.address, - zusdContract?.address, - ], - ); - - const isLoadingContracts = useMemo( - () => !!Object.keys(contractAddresses).find(key => !contractAddresses[key]), - [contractAddresses], - ); - - const { - userCheckpoint: sovUserCheckpoint, - maxWithdrawCheckpoint: sovMaxWithdrawCheckpoint, - } = useGetTokenCheckpoints(SupportedTokens.sov); - const { - userCheckpoint: myntUserCheckpoint, - maxWithdrawCheckpoint: myntMaxWithdrawCheckpoint, - } = useGetTokenCheckpoints(SupportedTokens.mynt); - const { - userCheckpoint: zusdUserCheckpoint, - maxWithdrawCheckpoint: zusdMaxWithdrawCheckpoint, - } = useGetTokenCheckpoints(SupportedTokens.zusd); - - const getStartFrom = useCallback( - (asset: SupportedTokens) => { - switch (asset) { - case SupportedTokens.sov: - return Number(sovUserCheckpoint?.checkpointNum) || 0; - case SupportedTokens.mynt: - return Number(myntUserCheckpoint?.checkpointNum) || 0; - case SupportedTokens.zusd: - return Number(zusdUserCheckpoint?.checkpointNum) || 0; - default: - return 0; - } - }, - [sovUserCheckpoint, myntUserCheckpoint, zusdUserCheckpoint], - ); - - const getMaxCheckpoints = useCallback( - (asset: SupportedTokens) => { - switch (asset) { - case SupportedTokens.sov: - return sovMaxWithdrawCheckpoint; - case SupportedTokens.mynt: - return myntMaxWithdrawCheckpoint; - case SupportedTokens.zusd: - return zusdMaxWithdrawCheckpoint; - default: - return 0; - } - }, - [ - sovMaxWithdrawCheckpoint, - myntMaxWithdrawCheckpoint, - zusdMaxWithdrawCheckpoint, - ], - ); - - const generateDefaultEarnedFees = useCallback(() => { - return Object.entries(contractAddresses).map( - ([token, contractAddress]): EarnedFee => ({ - token: token as SupportedTokens, - contractAddress, + const [earnedFees, setEarnedFees] = useState([]); + + const getAvailableFees = useCallback(async () => { + if (!isMounted()) { + return; + } + if (!account || !multicall) { + setEarnedFees([]); + setLoading(false); + return; + } + // set defaults + const defaultTokenData = await Promise.all( + FEE_ASSETS.map(async asset => ({ + token: asset, + contractAddress: + asset === SupportedTokens.rbtc + ? await getRbtcDummyAddress() + : ( + await getTokenContract(asset, defaultChainId) + ).address, value: '0', rbtcValue: 0, - ...(token !== SupportedTokens.rbtc + ...(asset !== SupportedTokens.rbtc ? { startFrom: 0, maxCheckpoints: 0 } : {}), - }), + })), ); - }, [contractAddresses]); - - const [earnedFees, setEarnedFees] = useState(generateDefaultEarnedFees()); + setEarnedFees(defaultTokenData); - const multicall = useMulticall(); - - const getAvailableFees = useCallback(async () => { - if ( - !sovMaxWithdrawCheckpoint || - !zusdMaxWithdrawCheckpoint || - !myntMaxWithdrawCheckpoint || - isLoadingContracts || - !account || - !feeSharing - ) { + if (!account) { + setLoading(false); return; } - const earnedFees = generateDefaultEarnedFees(); - - const callData = earnedFees.map(fee => { - const isRBTC = fee.token === SupportedTokens.rbtc; - const fnName = isRBTC - ? 'getAccumulatedRBTCFeeBalances' - : 'getAccumulatedFeesForCheckpointsRange'; - const startFrom = Math.max(getStartFrom(fee.token) - 1, 0); - const args = isRBTC - ? [account] - : [ - account, - fee.contractAddress, - startFrom, - getMaxCheckpoints(fee.token), - ]; - return { - contract: feeSharing, - fnName, - args, - key: fee.token, - parser: value => value[0].toString(), - }; - }); - - const result = await multicall(callData); - setLoading(true); - const fees = earnedFees.map((fee, i) => ({ - ...fee, - value: result[i], - })); + const { contract } = await getProtocolContract( + 'feeSharing', + defaultChainId, + ); - setEarnedFees([...fees]); - setLoading(false); - }, [ - account, - feeSharing, - generateDefaultEarnedFees, - getMaxCheckpoints, - getStartFrom, - isLoadingContracts, - multicall, - myntMaxWithdrawCheckpoint, - sovMaxWithdrawCheckpoint, - zusdMaxWithdrawCheckpoint, - ]); + const feeSharingContract = contract(getProvider(defaultChainId)); + + const checkpoints = await multicall( + defaultTokenData.flatMap(({ token, contractAddress }) => [ + { + contract: feeSharingContract, + fnName: 'totalTokenCheckpoints', + args: [contractAddress], + key: `${token}/totalTokenCheckpoints`, + parser: (value: string) => Number(value), + }, + { + contract: feeSharingContract, + fnName: 'processedCheckpoints', + args: [account, contractAddress], + key: `${token}/processedCheckpoints`, + parser: (value: string) => Number(value), + }, + ]), + ); - useEffect(() => { - const getRbtcDummyAddress = async () => { - try { - const result = await feeSharing?.RBTC_DUMMY_ADDRESS_FOR_CHECKPOINT(); + const amounts = await multicall( + defaultTokenData.map(({ token, contractAddress }) => ({ + contract: feeSharingContract, + fnName: 'getAllUserFeesPerMaxCheckpoints', + args: [ + account, + contractAddress, + Math.max(checkpoints[`${token}/processedCheckpoints`], 0), + MAX_CHECKPOINTS, + ], + key: token, + parser: ({ fees }: { fees: string[] }) => + fees.reduce((prev, cur) => prev.add(cur), BigNumber.from(0)), + })), + ); - setRBTCDummyAddress(result); - } catch (error) { - console.error('Error getting RBTC dummy address:', error); - } - }; + const results = defaultTokenData.map((tokenData, index) => ({ + token: tokenData.token, + contractAddress: tokenData.contractAddress, + value: amounts[tokenData.token].toString(), + rbtcValue: 0, + startFrom: checkpoints[`${tokenData.token}/processedCheckpoints`], + maxCheckpoints: checkpoints[`${tokenData.token}/totalTokenCheckpoints`], + })); - getRbtcDummyAddress(); - }, [feeSharing]); + if (isMounted()) { + setEarnedFees(results); + setLoading(false); + } + }, [account, isMounted, multicall]); useEffect(() => { - if (account && !isLoadingContracts) { - getAvailableFees(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - account, - sovMaxWithdrawCheckpoint, - zusdMaxWithdrawCheckpoint, - myntMaxWithdrawCheckpoint, - isLoadingContracts, - ]); + getAvailableFees().catch(console.error); + }, [getAvailableFees]); return { loading, diff --git a/apps/frontend/src/hooks/useMulticall.ts b/apps/frontend/src/hooks/useMulticall.ts index c7d668060..82c1f2df2 100644 --- a/apps/frontend/src/hooks/useMulticall.ts +++ b/apps/frontend/src/hooks/useMulticall.ts @@ -1,38 +1,35 @@ import { useCallback } from 'react'; +import { getProtocolContract } from '@sovryn/contracts'; +import { getProvider } from '@sovryn/ethers-provider'; + import { MultiCallData } from '../types/multicall'; import { getRskChainId } from '../utils/chain'; -import { useLoadContract } from './useLoadContract'; export const useMulticall = () => { - const multiCall = useLoadContract('multiCall', 'protocol', getRskChainId()); - - return useCallback( - async (callData: MultiCallData[]) => { - if (!multiCall) { - return []; - } + return useCallback(async (callData: MultiCallData[]) => { + const contract = await getProtocolContract('multiCall', getRskChainId()); + const multiCall = contract.contract(getProvider(getRskChainId())); - const data = callData.map(item => ({ - target: item.contract.address, - callData: item.contract.interface.encodeFunctionData( - item.fnName, - item.args, - ), - })); + const data = callData.map(item => ({ + target: item.contract.address, + callData: item.contract.interface.encodeFunctionData( + item.fnName, + item.args, + ), + })); - const { returnData } = await multiCall.callStatic.aggregate(data); + const { returnData } = await multiCall.callStatic.aggregate(data); - const result = callData.map((item, index) => { - const value = item.contract.interface.decodeFunctionResult( - item.fnName, - returnData[index], - ); - return item.parser ? item.parser(value) : value; - }); + const result = callData.reduce((p, c, index) => { + const value = c.contract.interface.decodeFunctionResult( + c.fnName, + returnData[index], + ); + p[c.key || index] = c.parser ? c.parser(value) : value; + return p; + }, {} as Record); - return result; - }, - [multiCall], - ); + return result; + }, []); }; diff --git a/packages/contracts/src/abis/feeSharing.json b/packages/contracts/src/abis/feeSharing.json index 90272d371..5107633d9 100644 --- a/packages/contracts/src/abis/feeSharing.json +++ b/packages/contracts/src/abis/feeSharing.json @@ -355,6 +355,42 @@ "stateMutability": "view", "type": "function" }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "address", + "name": "_token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_startFrom", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "_maxCheckpoints", + "type": "uint32" + } + ], + "name": "getAllUserFeesPerMaxCheckpoints", + "outputs": [ + { + "internalType": "uint256[]", + "name": "fees", + "type": "uint256[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, { "constant": true, "inputs": [ @@ -416,6 +452,27 @@ "stateMutability": "view", "type": "function" }, + { + "constant": true, + "inputs": [ + { + "internalType": "bytes4", + "name": "", + "type": "bytes4" + } + ], + "name": "isFunctionExecuted", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, { "constant": true, "inputs": [], @@ -529,6 +586,15 @@ "stateMutability": "view", "type": "function" }, + { + "constant": false, + "inputs": [], + "name": "recoverIncorrectAllocatedFees", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, { "constant": false, "inputs": [ @@ -744,6 +810,11 @@ { "constant": false, "inputs": [ + { + "internalType": "address[]", + "name": "_tokens", + "type": "address[]" + }, { "internalType": "uint32", "name": "_maxCheckpoints", @@ -755,7 +826,7 @@ "type": "address" } ], - "name": "withdrawRBTC", + "name": "withdrawRbtcTokens", "outputs": [], "payable": false, "stateMutability": "nonpayable", @@ -765,9 +836,14 @@ "constant": false, "inputs": [ { - "internalType": "uint256", - "name": "_fromCheckpoint", - "type": "uint256" + "internalType": "address[]", + "name": "_tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_fromCheckpoints", + "type": "uint256[]" }, { "internalType": "uint32", @@ -780,7 +856,7 @@ "type": "address" } ], - "name": "withdrawRBTCStartingFromCheckpoint", + "name": "withdrawRbtcTokensStartingFromCheckpoint", "outputs": [], "payable": false, "stateMutability": "nonpayable", @@ -790,14 +866,14 @@ "constant": false, "inputs": [ { - "internalType": "address", - "name": "_token", - "type": "address" + "internalType": "address[]", + "name": "_tokens", + "type": "address[]" }, { - "internalType": "uint256", - "name": "_fromCheckpoint", - "type": "uint256" + "internalType": "uint256[]", + "name": "_fromCheckpoints", + "type": "uint256[]" }, { "internalType": "uint32", @@ -810,7 +886,7 @@ "type": "address" } ], - "name": "withdrawStartingFromCheckpoint", + "name": "withdrawStartingFromCheckpoints", "outputs": [], "payable": false, "stateMutability": "nonpayable", diff --git a/packages/contracts/src/types.ts b/packages/contracts/src/types.ts index 5ecc827d7..1c2fea2f1 100644 --- a/packages/contracts/src/types.ts +++ b/packages/contracts/src/types.ts @@ -1,4 +1,6 @@ -import { ContractInterface } from 'ethers'; +import { Provider } from '@ethersproject/abstract-provider'; + +import type { Contract, ContractInterface, Signer } from 'ethers'; import type { ChainId } from '@sovryn/ethers-provider'; @@ -10,6 +12,7 @@ export type ContractNetworkName = keyof typeof contracts[ContractGroup]; export type ContractConfigData = { address: string; abi: ContractInterface; + contract: (signerOrProvider?: Signer | Provider) => Contract; }; export type AsyncContractConfigData = { diff --git a/packages/contracts/src/utils/global.ts b/packages/contracts/src/utils/global.ts index 42afb172a..2cf863716 100644 --- a/packages/contracts/src/utils/global.ts +++ b/packages/contracts/src/utils/global.ts @@ -1,4 +1,6 @@ -import { ContractInterface } from 'ethers'; +import type { Provider } from '@ethersproject/providers'; + +import { ContractInterface, Contract, Signer } from 'ethers'; import get from 'lodash.get'; import set from 'lodash.set'; @@ -93,7 +95,7 @@ export const getContract = async ( throw new Error(`getContract: Unknown contract: ${name}`); } - let contractData: ContractConfigData; + let contractData: Omit; if (typeof contract === 'string') { contractData = { @@ -107,9 +109,16 @@ export const getContract = async ( }; } - set(cacheByKey, [chainId, group, name], contractData); + const data: ContractConfigData = { + address: contractData.address, + abi: contractData.abi, + contract: (signerOrProvider?: Signer | Provider) => + new Contract(contractData.address, contractData.abi, signerOrProvider), + }; + + set(cacheByKey, [chainId, group, name], data); - return contractData; + return data; }; export const getContractGroupAbi = async ( From eb531600a178e3e853fe7695c7e5e4f6ef6e2675 Mon Sep 17 00:00:00 2001 From: Victor Creed Date: Wed, 20 Sep 2023 10:47:42 +0300 Subject: [PATCH 2/7] feat: claim all rewards with single tx --- .changeset/friendly-laws-type.md | 5 + .changeset/nice-crabs-jam.md | 5 + .../components/Staking/Staking.tsx | 71 +++---- .../WithdrawAllFees/WithdrawAllFees.tsx | 182 ++++++++++++++++++ .../components/WithdrawFee/WithdrawFee.tsx | 1 + apps/frontend/src/constants/gasLimits.ts | 1 + .../frontend/src/locales/en/translations.json | 3 +- .../ui/src/2_molecules/Table/Table.types.ts | 1 + .../components/TableDesktop/TableDesktop.tsx | 75 ++++---- 9 files changed, 273 insertions(+), 71 deletions(-) create mode 100644 .changeset/friendly-laws-type.md create mode 100644 .changeset/nice-crabs-jam.md create mode 100644 apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx diff --git a/.changeset/friendly-laws-type.md b/.changeset/friendly-laws-type.md new file mode 100644 index 000000000..ffbaf6aa2 --- /dev/null +++ b/.changeset/friendly-laws-type.md @@ -0,0 +1,5 @@ +--- +'@sovryn/ui': patch +--- + +feat: allow to hide table header row diff --git a/.changeset/nice-crabs-jam.md b/.changeset/nice-crabs-jam.md new file mode 100644 index 000000000..e1bf6fbac --- /dev/null +++ b/.changeset/nice-crabs-jam.md @@ -0,0 +1,5 @@ +--- +'frontend': patch +--- + +feat: claim all rewards with single tx diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx index 7c3946043..863fb214d 100644 --- a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx @@ -15,8 +15,7 @@ import { decimalic } from '../../../../../utils/math'; import { useGetFeesEarned } from '../../hooks/useGetFeesEarned'; import { useGetLiquidSovClaimAmount } from '../../hooks/useGetLiquidSovClaimAmount'; import { columns } from './Staking.constants'; -import { getStakingRevenueType } from './Staking.utils'; -import { WithdrawFee } from './components/WithdrawFee/WithdrawFee'; +import { WithdrawAllFees } from './components/WithdrawAllFees/WithdrawAllFees'; import { WithdrawLiquidFee } from './components/WithdrawLiquidFee/WithdrawLiquidFee'; export const Staking: FC = () => { @@ -29,29 +28,34 @@ export const Staking: FC = () => { refetch: refetchLiquidSovClaim, } = useGetLiquidSovClaimAmount(); - const rows = useMemo(() => { - const noRewards = - !earnedFees.some(earnedFee => decimalic(earnedFee.value).gt(0)) && - !decimalic(liquidSovClaimAmount).gt(0); - - if (!account || loading || noRewards) { - return []; - } + const noRewards = useMemo( + () => + (!earnedFees.some(earnedFee => decimalic(earnedFee.value).gt(0)) && + !decimalic(liquidSovClaimAmount).gt(0)) || + !account, + [account, earnedFees, liquidSovClaimAmount], + ); - return [ - ...earnedFees.map(earnedFee => ({ - type: getStakingRevenueType(earnedFee.token), + const rows1 = useMemo( + () => [ + { + type: t(translations.rewardPage.staking.stakingRevenue), amount: ( - +
+ {earnedFees.map(fee => ( + + ))} +
), - action: , - key: `${earnedFee.token}-fee`, - })), + action: , + key: `all-fee`, + }, { type: t(translations.rewardPage.staking.stakingSubsidies), amount: ( @@ -71,23 +75,22 @@ export const Staking: FC = () => { ), key: `${SupportedTokens.sov}-liquid-fee`, }, - ]; - }, [ - account, - earnedFees, - lastWithdrawalInterval, - liquidSovClaimAmount, - loading, - refetch, - refetchLiquidSovClaim, - ]); + ], + [ + earnedFees, + lastWithdrawalInterval, + liquidSovClaimAmount, + refetch, + refetchLiquidSovClaim, + ], + ); return ( -
+
row.key} noData={ diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx new file mode 100644 index 000000000..ef62b76a2 --- /dev/null +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx @@ -0,0 +1,182 @@ +import React, { FC, useCallback, useMemo } from 'react'; + +import { Contract } from 'ethers'; +import { t } from 'i18next'; + +import { SupportedTokens, getProtocolContract } from '@sovryn/contracts'; +import { getProvider } from '@sovryn/ethers-provider'; +import { Button, ButtonType, ButtonStyle } from '@sovryn/ui'; + +import { defaultChainId } from '../../../../../../../config/chains'; + +import { + Transaction, + TransactionType, +} from '../../../../../../3_organisms/TransactionStepDialog/TransactionStepDialog.types'; +import { GAS_LIMIT } from '../../../../../../../constants/gasLimits'; +import { useTransactionContext } from '../../../../../../../contexts/TransactionContext'; +import { useAccount } from '../../../../../../../hooks/useAccount'; +import { useGetProtocolContract } from '../../../../../../../hooks/useGetContract'; +import { useMaintenance } from '../../../../../../../hooks/useMaintenance'; +import { translations } from '../../../../../../../locales/i18n'; +import { decimalic } from '../../../../../../../utils/math'; +import { EarnedFee } from '../../../../RewardsPage.types'; + +type WithdrawFeeProps = { + fees: EarnedFee[]; + refetch: () => void; +}; + +const MAX_CHECKPOINTS = 10; +const MAX_NEXT_POSITIVE_CHECKPOINT = 75; + +export const WithdrawAllFees: FC = ({ fees, refetch }) => { + const { account } = useAccount(); + const { setTransactions, setIsOpen, setTitle } = useTransactionContext(); + + const { checkMaintenance, States } = useMaintenance(); + const claimFeesEarnedLocked = checkMaintenance(States.CLAIM_FEES_EARNED); + const rewardsLocked = checkMaintenance(States.REWARDS_FULL); + + const feeSharing = useGetProtocolContract('feeSharing'); + + const isClaimDisabled = useMemo( + () => + claimFeesEarnedLocked || + rewardsLocked || + fees.every(({ value }) => decimalic(value).lte(0)), + [claimFeesEarnedLocked, fees, rewardsLocked], + ); + + const onComplete = useCallback(() => { + refetch(); + }, [refetch]); + + const onSubmit = useCallback(async () => { + if (!feeSharing) { + return; + } + + const claimable = fees.filter(fee => decimalic(fee.value).gt(0)); + + // TODO: it might be not needed to fetch checkpoints when SC is updated. + // START: Fetch checkpoints + const checkpoints = await Promise.all( + claimable.map(fee => + getNextPositiveCheckpoint(account, fee).then(result => ({ + ...fee, + startFrom: result.checkpointNum, + hasSkippedCheckpoints: result.hasSkippedCheckpoints, + hasFees: result.hasFees, + })), + ), + ).then(result => result.filter(fee => fee.hasSkippedCheckpoints)); + + console.log({ checkpoints }); + + if (checkpoints.length === 0) { + // todo: show error message about impossibility to withdraw + console.warn('No checkpoints to withdraw'); + return; + } + + // END: Fetch checkpoints + + const transactions: Transaction[] = []; + const title = t(translations.rewardPage.stabilityPool.tx.withdrawGains); + const txTitle = t(translations.rewardPage.stabilityPool.tx.withdraw); + + transactions.push({ + title, + request: { + type: TransactionType.signTransaction, + contract: feeSharing, + fnName: 'withdrawStartingFromCheckpoints', + args: [ + claimable.map(({ contractAddress }) => contractAddress), + claimable.map(({ startFrom }) => startFrom), + MAX_CHECKPOINTS, + account, + ], + gasLimit: GAS_LIMIT.REWARDS_CLAIM, + }, + onComplete, + }); + + setTransactions(transactions); + setTitle(txTitle); + setIsOpen(true); + }, [ + account, + feeSharing, + fees, + onComplete, + setIsOpen, + setTitle, + setTransactions, + ]); + + return ( + - - {columns.map(column => ( - + + {columns.map(column => ( + - ))} - - + {isValidElement(column.filter) && column.filter} + + + ))} + + + )} {rows && rows.length >= 1 && From 9ea3b0f6c7ecff1b22a2357d121c7299782a0a58 Mon Sep 17 00:00:00 2001 From: Victor Creed Date: Wed, 20 Sep 2023 10:50:00 +0300 Subject: [PATCH 3/7] feat: implement new rewards withdrawal feature --- .../WithdrawAllFees/WithdrawAllFees.tsx | 44 +- packages/contracts/src/abis/feeSharing.json | 132 +-- packages/contracts/src/abis/protocol.json | 926 ++++++++++++++++-- 3 files changed, 911 insertions(+), 191 deletions(-) diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx index ef62b76a2..5a7fbfca7 100644 --- a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useMemo } from 'react'; +import React, { FC, useCallback, useMemo, useState } from 'react'; import { Contract } from 'ethers'; import { t } from 'i18next'; @@ -32,6 +32,7 @@ const MAX_NEXT_POSITIVE_CHECKPOINT = 75; export const WithdrawAllFees: FC = ({ fees, refetch }) => { const { account } = useAccount(); + const [loading, setLoading] = useState(false); const { setTransactions, setIsOpen, setTitle } = useTransactionContext(); const { checkMaintenance, States } = useMaintenance(); @@ -56,6 +57,7 @@ export const WithdrawAllFees: FC = ({ fees, refetch }) => { if (!feeSharing) { return; } + setLoading(true); const claimable = fees.filter(fee => decimalic(fee.value).gt(0)); @@ -70,16 +72,33 @@ export const WithdrawAllFees: FC = ({ fees, refetch }) => { hasFees: result.hasFees, })), ), - ).then(result => result.filter(fee => fee.hasSkippedCheckpoints)); - - console.log({ checkpoints }); + ).then(result => result.filter(fee => fee.hasFees)); if (checkpoints.length === 0) { - // todo: show error message about impossibility to withdraw console.warn('No checkpoints to withdraw'); + setLoading(false); return; } + const nonRbtcRegular = checkpoints + .filter( + item => !isBtcBasedToken(item.token) && !item.hasSkippedCheckpoints, + ) + .map(item => item.contractAddress); + + const rbtcRegular = checkpoints + .filter( + item => isBtcBasedToken(item.token) && !item.hasSkippedCheckpoints, + ) + .map(item => item.contractAddress); + + const tokensWithSkippedCheckpoints = checkpoints + .filter(item => item.hasSkippedCheckpoints) + .map(item => ({ + tokenAddress: item.contractAddress, + fromCheckpoint: item.startFrom, + })); + // END: Fetch checkpoints const transactions: Transaction[] = []; @@ -91,10 +110,11 @@ export const WithdrawAllFees: FC = ({ fees, refetch }) => { request: { type: TransactionType.signTransaction, contract: feeSharing, - fnName: 'withdrawStartingFromCheckpoints', + fnName: 'claimAllCollectedFees', args: [ - claimable.map(({ contractAddress }) => contractAddress), - claimable.map(({ startFrom }) => startFrom), + nonRbtcRegular, + rbtcRegular, + tokensWithSkippedCheckpoints, MAX_CHECKPOINTS, account, ], @@ -106,6 +126,7 @@ export const WithdrawAllFees: FC = ({ fees, refetch }) => { setTransactions(transactions); setTitle(txTitle); setIsOpen(true); + setLoading(false); }, [ account, feeSharing, @@ -122,7 +143,8 @@ export const WithdrawAllFees: FC = ({ fees, refetch }) => { style={ButtonStyle.secondary} text={t(translations.rewardPage.stabilityPool.actions.withdrawAll)} onClick={onSubmit} - disabled={isClaimDisabled} + disabled={isClaimDisabled || loading} + loading={loading} className="w-full lg:w-auto whitespace-nowrap" dataAttribute="rewards-withdraw" /> @@ -180,3 +202,7 @@ async function getNextPositiveCheckpoint( hasSkippedCheckpoints: false, }; } + +function isBtcBasedToken(token: SupportedTokens) { + return [SupportedTokens.rbtc, SupportedTokens.wrbtc].includes(token); +} diff --git a/packages/contracts/src/abis/feeSharing.json b/packages/contracts/src/abis/feeSharing.json index 5107633d9..d70f61549 100644 --- a/packages/contracts/src/abis/feeSharing.json +++ b/packages/contracts/src/abis/feeSharing.json @@ -272,6 +272,53 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "constant": false, + "inputs": [ + { + "internalType": "address[]", + "name": "_nonRbtcTokensRegularWithdraw", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_rbtcTokensRegularWithdraw", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "address", + "name": "tokenAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "fromCheckpoint", + "type": "uint256" + } + ], + "internalType": "struct FeeSharingCollectorStorage.TokenWithSkippedCheckpointsWithdraw[]", + "name": "_tokensWithSkippedCheckpoints", + "type": "tuple[]" + }, + { + "internalType": "uint32", + "name": "_maxCheckpoints", + "type": "uint32" + }, + { + "internalType": "address", + "name": "_receiver", + "type": "address" + } + ], + "name": "claimAllCollectedFees", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, { "constant": true, "inputs": [ @@ -807,91 +854,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "constant": false, - "inputs": [ - { - "internalType": "address[]", - "name": "_tokens", - "type": "address[]" - }, - { - "internalType": "uint32", - "name": "_maxCheckpoints", - "type": "uint32" - }, - { - "internalType": "address", - "name": "_receiver", - "type": "address" - } - ], - "name": "withdrawRbtcTokens", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address[]", - "name": "_tokens", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_fromCheckpoints", - "type": "uint256[]" - }, - { - "internalType": "uint32", - "name": "_maxCheckpoints", - "type": "uint32" - }, - { - "internalType": "address", - "name": "_receiver", - "type": "address" - } - ], - "name": "withdrawRbtcTokensStartingFromCheckpoint", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address[]", - "name": "_tokens", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "_fromCheckpoints", - "type": "uint256[]" - }, - { - "internalType": "uint32", - "name": "_maxCheckpoints", - "type": "uint32" - }, - { - "internalType": "address", - "name": "_receiver", - "type": "address" - } - ], - "name": "withdrawStartingFromCheckpoints", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, { "constant": false, "inputs": [ diff --git a/packages/contracts/src/abis/protocol.json b/packages/contracts/src/abis/protocol.json index 8f11be743..fafab3483 100644 --- a/packages/contracts/src/abis/protocol.json +++ b/packages/contracts/src/abis/protocol.json @@ -891,6 +891,37 @@ "name": "ProtocolModuleContractReplaced", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sourceTokenAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "destTokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IERC20[]", + "name": "defaultPath", + "type": "address[]" + } + ], + "name": "RemoveDefaultPathConversion", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -946,6 +977,31 @@ "name": "Rollover", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "oldAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "SetAdmin", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -1071,6 +1127,37 @@ "name": "SetBorrowingFeePercent", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "sourceTokenAddress", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "destTokenAddress", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IERC20[]", + "name": "defaultPath", + "type": "address[]" + } + ], + "name": "SetDefaultPathConversion", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -1246,6 +1333,31 @@ "name": "SetMinReferralsToPayoutAffiliates", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "oldPauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newPauser", + "type": "address" + } + ], + "name": "SetPauser", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -1371,6 +1483,31 @@ "name": "SetRolloverBaseReward", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldRolloverFlexFeePercent", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newRolloverFlexFeePercent", + "type": "uint256" + } + ], + "name": "SetRolloverFlexFeePercent", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -2066,14 +2203,83 @@ "type": "uint256" }, { - "internalType": "address[4]", + "components": [ + { + "internalType": "address", + "name": "lender", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "manager", + "type": "address" + } + ], + "internalType": "struct MarginTradeStructHelpers.SentAddresses", "name": "sentAddresses", - "type": "address[4]" + "type": "tuple" }, { - "internalType": "uint256[5]", + "components": [ + { + "internalType": "uint256", + "name": "interestRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "newPrincipal", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "interestInitialAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "loanTokenSent", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "collateralTokenSent", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minEntryPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "loanToCollateralSwapRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "interestDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "entryLeverage", + "type": "uint256" + } + ], + "internalType": "struct MarginTradeStructHelpers.SentAmounts", "name": "sentValues", - "type": "uint256[5]" + "type": "tuple" }, { "internalType": "bytes", @@ -2669,92 +2875,225 @@ { "constant": true, "inputs": [ - { - "internalType": "address", - "name": "referrer", - "type": "address" - } - ], - "name": "getAffiliateRewardsHeld", - "outputs": [ { "internalType": "uint256", - "name": "", + "name": "start", "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getAffiliateTradingTokenFeePercent", - "outputs": [ + }, { "internalType": "uint256", - "name": "affiliateTradingTokenFeePercent", + "name": "count", "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "address", - "name": "referrer", - "type": "address" - } - ], - "name": "getAffiliatesReferrerBalances", - "outputs": [ - { - "internalType": "address[]", - "name": "referrerTokensList", - "type": "address[]" - }, - { - "internalType": "uint256[]", - "name": "referrerTokensBalances", - "type": "uint256[]" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "address", - "name": "referrer", - "type": "address" }, { - "internalType": "address", - "name": "token", - "type": "address" + "internalType": "bool", + "name": "unsafeOnly", + "type": "bool" } ], - "name": "getAffiliatesReferrerTokenBalance", + "name": "getActiveLoansV2", "outputs": [ { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { + "components": [ + { + "internalType": "bytes32", + "name": "loanId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "loanToken", + "type": "address" + }, + { + "internalType": "address", + "name": "collateralToken", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "principal", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "collateral", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "interestOwedPerDay", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "interestDepositRemaining", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startMargin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maintenanceMargin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentMargin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLoanTerm", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLiquidatable", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxSeizable", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "creationTimestamp", + "type": "uint256" + } + ], + "internalType": "struct ISovryn.LoanReturnDataV2[]", + "name": "loansDataV2", + "type": "tuple[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getAdmin", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "referrer", + "type": "address" + } + ], + "name": "getAffiliateRewardsHeld", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getAffiliateTradingTokenFeePercent", + "outputs": [ + { + "internalType": "uint256", + "name": "affiliateTradingTokenFeePercent", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "referrer", + "type": "address" + } + ], + "name": "getAffiliatesReferrerBalances", + "outputs": [ + { + "internalType": "address[]", + "name": "referrerTokensList", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "referrerTokensBalances", + "type": "uint256[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "referrer", + "type": "address" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getAffiliatesReferrerTokenBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { "constant": true, "inputs": [ { @@ -2873,6 +3212,32 @@ "stateMutability": "view", "type": "function" }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "sourceTokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "destTokenAddress", + "type": "address" + } + ], + "name": "getDefaultPathConversion", + "outputs": [ + { + "internalType": "contract IERC20[]", + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, { "constant": true, "inputs": [ @@ -3235,34 +3600,163 @@ "type": "function" }, { - "constant": false, - "inputs": [ - { - "internalType": "uint256", - "name": "start", - "type": "uint256" - }, + "constant": true, + "inputs": [ + { + "internalType": "uint256", + "name": "start", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "name": "getLoanPoolsList", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "bytes32", + "name": "loanId", + "type": "bytes32" + } + ], + "name": "getLoanV2", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "loanId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "loanToken", + "type": "address" + }, + { + "internalType": "address", + "name": "collateralToken", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "principal", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "collateral", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "interestOwedPerDay", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "interestDepositRemaining", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startMargin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maintenanceMargin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentMargin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLoanTerm", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLiquidatable", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxSeizable", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "creationTimestamp", + "type": "uint256" + } + ], + "internalType": "struct ISovryn.LoanReturnDataV2", + "name": "loanDataV2", + "type": "tuple" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getLockedSOVAddress", + "outputs": [ { - "internalType": "uint256", - "name": "count", - "type": "uint256" + "internalType": "address", + "name": "", + "type": "address" } ], - "name": "getLoanPoolsList", - "outputs": [], "payable": false, - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { "constant": true, "inputs": [], - "name": "getLockedSOVAddress", + "name": "getMinReferralsToPayout", "outputs": [ { - "internalType": "address", + "internalType": "uint256", "name": "", - "type": "address" + "type": "uint256" } ], "payable": false, @@ -3272,12 +3766,12 @@ { "constant": true, "inputs": [], - "name": "getMinReferralsToPayout", + "name": "getPauser", "outputs": [ { - "internalType": "uint256", + "internalType": "address", "name": "", - "type": "uint256" + "type": "address" } ], "payable": false, @@ -3633,6 +4127,139 @@ "stateMutability": "view", "type": "function" }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "start", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "count", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "loanType", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "isLender", + "type": "bool" + }, + { + "internalType": "bool", + "name": "unsafeOnly", + "type": "bool" + } + ], + "name": "getUserLoansV2", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "loanId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "loanToken", + "type": "address" + }, + { + "internalType": "address", + "name": "collateralToken", + "type": "address" + }, + { + "internalType": "address", + "name": "borrower", + "type": "address" + }, + { + "internalType": "uint256", + "name": "principal", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "collateral", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "interestOwedPerDay", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "interestDepositRemaining", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "startMargin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maintenanceMargin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentMargin", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLoanTerm", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "endTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxLiquidatable", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxSeizable", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "creationTimestamp", + "type": "uint256" + } + ], + "internalType": "struct ISovryn.LoanReturnDataV2[]", + "name": "loansDataV2", + "type": "tuple[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, { "constant": true, "inputs": [ @@ -4378,6 +5005,51 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "constant": false, + "inputs": [ + { + "internalType": "bytes32", + "name": "loanId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "withdrawAmount", + "type": "uint256" + } + ], + "name": "reduceLoanDuration", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "sourceTokenAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "destTokenAddress", + "type": "address" + } + ], + "name": "removeDefaultPathConversion", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, { "constant": false, "inputs": [ @@ -4443,6 +5115,21 @@ "stateMutability": "view", "type": "function" }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "setAdmin", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, { "constant": false, "inputs": [ @@ -4508,6 +5195,21 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "constant": false, + "inputs": [ + { + "internalType": "contract IERC20[]", + "name": "defaultPath", + "type": "address[]" + } + ], + "name": "setDefaultPathConversion", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, { "constant": false, "inputs": [ @@ -4678,6 +5380,21 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "newPauser", + "type": "address" + } + ], + "name": "setPauser", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, { "constant": false, "inputs": [ @@ -4738,6 +5455,21 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "constant": false, + "inputs": [ + { + "internalType": "uint256", + "name": "newRolloverFlexFeePercent", + "type": "uint256" + } + ], + "name": "setRolloverFlexFeePercent", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, { "constant": false, "inputs": [ From 8dd1834395344cc6bc90c2aeffe3e953d20197f8 Mon Sep 17 00:00:00 2001 From: Victor Creed Date: Fri, 22 Sep 2023 14:23:57 +0300 Subject: [PATCH 4/7] fix: review comments --- .../5_pages/RewardsPage/components/Staking/Staking.tsx | 4 ++-- .../components/WithdrawAllFees/WithdrawAllFees.tsx | 9 +-------- .../components/WithdrawAllFees/WithdrawlAllFees.types.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawlAllFees.types.ts diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx index 863fb214d..3cdd166a2 100644 --- a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx @@ -36,7 +36,7 @@ export const Staking: FC = () => { [account, earnedFees, liquidSovClaimAmount], ); - const rows1 = useMemo( + const rows = useMemo( () => [ { type: t(translations.rewardPage.staking.stakingRevenue), @@ -90,7 +90,7 @@ export const Staking: FC = () => {
- - onHeaderClick(column)} - className={classNames(styles.title, { - [styles.sortable]: column.sortable, - })} - > - <> - {column.title || column.id} - {column.sortable && ( - - )} - - + {hideHeader === false && ( +
+ + onHeaderClick(column)} + className={classNames(styles.title, { + [styles.sortable]: column.sortable, + })} + > + <> + {column.title || column.id} + {column.sortable && ( + + )} + + - {isValidElement(column.filter) && column.filter} - -
row.key} noData={ diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx index 5a7fbfca7..5cd40044b 100644 --- a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx @@ -21,6 +21,7 @@ import { useMaintenance } from '../../../../../../../hooks/useMaintenance'; import { translations } from '../../../../../../../locales/i18n'; import { decimalic } from '../../../../../../../utils/math'; import { EarnedFee } from '../../../../RewardsPage.types'; +import { UserCheckpoint } from './WithdrawlAllFees.types'; type WithdrawFeeProps = { fees: EarnedFee[]; @@ -61,7 +62,6 @@ export const WithdrawAllFees: FC = ({ fees, refetch }) => { const claimable = fees.filter(fee => decimalic(fee.value).gt(0)); - // TODO: it might be not needed to fetch checkpoints when SC is updated. // START: Fetch checkpoints const checkpoints = await Promise.all( claimable.map(fee => @@ -151,13 +151,6 @@ export const WithdrawAllFees: FC = ({ fees, refetch }) => { ); }; -type UserCheckpoint = { - token: SupportedTokens; - checkpointNum: number; - hasFees: boolean; - hasSkippedCheckpoints: boolean; -}; - let feeSharingContract: Contract; const getFeeSharingContract = async () => { if (!feeSharingContract) { diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawlAllFees.types.ts b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawlAllFees.types.ts new file mode 100644 index 000000000..39a7c4c40 --- /dev/null +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawlAllFees.types.ts @@ -0,0 +1,8 @@ +import { SupportedTokens } from '@sovryn/contracts'; + +export type UserCheckpoint = { + token: SupportedTokens; + checkpointNum: number; + hasFees: boolean; + hasSkippedCheckpoints: boolean; +}; From 6aeb5fc79a2655961dc7772625e135e3edeaec8e Mon Sep 17 00:00:00 2001 From: Victor Creed Date: Fri, 22 Sep 2023 15:00:00 +0300 Subject: [PATCH 5/7] fix: increase withdrawable checkpoints --- .../Staking/components/WithdrawAllFees/WithdrawAllFees.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx index 5cd40044b..87d6687f6 100644 --- a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/components/WithdrawAllFees/WithdrawAllFees.tsx @@ -28,7 +28,7 @@ type WithdrawFeeProps = { refetch: () => void; }; -const MAX_CHECKPOINTS = 10; +const MAX_CHECKPOINTS = 50; const MAX_NEXT_POSITIVE_CHECKPOINT = 75; export const WithdrawAllFees: FC = ({ fees, refetch }) => { From 11057d8999b56e3d072184a91984aae79faf34c2 Mon Sep 17 00:00:00 2001 From: Victor Creed Date: Mon, 25 Sep 2023 16:29:43 +0300 Subject: [PATCH 6/7] fix: show single btc token --- .../components/Staking/Staking.tsx | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx index 3cdd166a2..63a9251cc 100644 --- a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx @@ -1,5 +1,6 @@ import React, { FC, useMemo } from 'react'; +import { BigNumber } from 'ethers'; import { formatUnits } from 'ethers/lib/utils'; import { t } from 'i18next'; @@ -12,6 +13,7 @@ import { getTokenDisplayName } from '../../../../../constants/tokens'; import { useAccount } from '../../../../../hooks/useAccount'; import { translations } from '../../../../../locales/i18n'; import { decimalic } from '../../../../../utils/math'; +import { EarnedFee } from '../../RewardsPage.types'; import { useGetFeesEarned } from '../../hooks/useGetFeesEarned'; import { useGetLiquidSovClaimAmount } from '../../hooks/useGetLiquidSovClaimAmount'; import { columns } from './Staking.constants'; @@ -36,13 +38,42 @@ export const Staking: FC = () => { [account, earnedFees, liquidSovClaimAmount], ); + const earnedFeesSum = useMemo(() => { + const btcFees = earnedFees.filter( + earnedFee => + earnedFee.token === SupportedTokens.rbtc || + earnedFee.token === SupportedTokens.wrbtc, + ); + + if (!btcFees.length) { + return earnedFees; + } + + const otherFees = earnedFees.filter( + earnedFee => + earnedFee.token !== SupportedTokens.rbtc && + earnedFee.token !== SupportedTokens.wrbtc, + ); + + const btcFeesSum: EarnedFee = { + token: SupportedTokens.rbtc, + value: btcFees + .reduce((sum, fee) => sum.add(fee.value), BigNumber.from(0)) + .toString(), + contractAddress: '', + rbtcValue: 0, + }; + + return [btcFeesSum, ...otherFees]; + }, [earnedFees]); + const rows = useMemo( () => [ { type: t(translations.rewardPage.staking.stakingRevenue), amount: (
- {earnedFees.map(fee => ( + {earnedFeesSum.map(fee => ( { ], [ earnedFees, + earnedFeesSum, lastWithdrawalInterval, liquidSovClaimAmount, refetch, From 9e0c9e00b1d5eed512396753c27d5c03dc021444 Mon Sep 17 00:00:00 2001 From: Victor Creed Date: Thu, 28 Sep 2023 15:22:34 +0300 Subject: [PATCH 7/7] fix: show no reward row if there is no fees --- .../components/Staking/Staking.tsx | 107 ++++++++++-------- 1 file changed, 62 insertions(+), 45 deletions(-) diff --git a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx index 63a9251cc..5fe5ade5d 100644 --- a/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx +++ b/apps/frontend/src/app/5_pages/RewardsPage/components/Staking/Staking.tsx @@ -30,12 +30,19 @@ export const Staking: FC = () => { refetch: refetchLiquidSovClaim, } = useGetLiquidSovClaimAmount(); + const hasEarnedFees = useMemo( + () => earnedFees.some(earnedFee => decimalic(earnedFee.value).gt(0)), + [earnedFees], + ); + + const hasLiquidSov = useMemo( + () => decimalic(liquidSovClaimAmount).gt(0), + [liquidSovClaimAmount], + ); + const noRewards = useMemo( - () => - (!earnedFees.some(earnedFee => decimalic(earnedFee.value).gt(0)) && - !decimalic(liquidSovClaimAmount).gt(0)) || - !account, - [account, earnedFees, liquidSovClaimAmount], + () => (!hasEarnedFees && !hasLiquidSov) || !account, + [hasEarnedFees, hasLiquidSov, account], ); const earnedFeesSum = useMemo(() => { @@ -69,50 +76,60 @@ export const Staking: FC = () => { const rows = useMemo( () => [ - { - type: t(translations.rewardPage.staking.stakingRevenue), - amount: ( -
- {earnedFeesSum.map(fee => ( - - ))} -
- ), - action: , - key: `all-fee`, - }, - { - type: t(translations.rewardPage.staking.stakingSubsidies), - amount: ( - - ), - action: ( - - ), - key: `${SupportedTokens.sov}-liquid-fee`, - }, + ...(hasEarnedFees + ? [ + { + type: t(translations.rewardPage.staking.stakingRevenue), + amount: ( +
+ {earnedFeesSum.map(fee => ( + + ))} +
+ ), + action: , + key: `all-fee`, + }, + ] + : []), + ...(hasLiquidSov + ? [ + { + type: t(translations.rewardPage.staking.stakingSubsidies), + amount: ( + + ), + action: ( + + ), + key: `${SupportedTokens.sov}-liquid-fee`, + }, + ] + : []), ], [ - earnedFees, + hasEarnedFees, earnedFeesSum, - lastWithdrawalInterval, - liquidSovClaimAmount, + earnedFees, refetch, + hasLiquidSov, + liquidSovClaimAmount, + lastWithdrawalInterval, refetchLiquidSovClaim, ], );