diff --git a/src/constants.tsx b/src/constants.tsx index 7dbe3a83..72e3ff6ef 100644 --- a/src/constants.tsx +++ b/src/constants.tsx @@ -10,5 +10,4 @@ export default { nativeAssetId: '0', basiliskAddressPrefix: 10041, // prefix for the ss58 address formatting of substrate addresses basiliskWeb3ProviderName: 'basilisk-ui', - defaultValue: '0', }; diff --git a/src/errors.tsx b/src/errors.tsx index d9a8bec8..94f92407 100644 --- a/src/errors.tsx +++ b/src/errors.tsx @@ -4,4 +4,5 @@ export default { noArgumentsProvidedBalanceQuery: 'No Arguments have been provided to the balance query', invalidTransferVariables: 'Invalid transfer parameters provided', + vestingScheduleIncomplete: 'Vesting schedule has at least one undefined property' }; diff --git a/src/hooks/apollo/useApollo.tsx b/src/hooks/apollo/useApollo.tsx index 7c8bbcdc..2b190517 100644 --- a/src/hooks/apollo/useApollo.tsx +++ b/src/hooks/apollo/useApollo.tsx @@ -13,7 +13,7 @@ import { usePoolsMutationResolvers } from '../pools/resolvers/usePoolsMutationRe import { useExtensionResolvers } from '../extension/resolvers/useExtensionResolvers'; import { usePersistentConfig } from '../config/usePersistentConfig'; import { useFaucetResolvers } from '../faucet/resolvers/useFaucetResolvers'; -import { useVestingScheduleQueryResolvers } from '../vesting/useVestingScheduleQueryResolvers'; +import { useVestingQueryResolvers } from '../vesting/useVestingQueryResolvers'; /** * Add all local gql resolvers here @@ -48,7 +48,7 @@ export const useResolvers: () => Resolvers = () => { LBPPool, Account: { ...useBalanceQueryResolvers(), - ...useVestingScheduleQueryResolvers() + ...useVestingQueryResolvers() } }; }; diff --git a/src/hooks/vesting/calculateClaimableAmount.test.tsx b/src/hooks/vesting/calculateClaimableAmount.test.tsx deleted file mode 100644 index 86ee35ad..00000000 --- a/src/hooks/vesting/calculateClaimableAmount.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import BigNumber from 'bignumber.js'; -import constants from '../../constants'; -import { - calculateClaimableAmount, - calculateFutureLock, - toBN, -} from './calculateClaimableAmount'; - -describe('calculateClaimableAmount', () => { - const vestingSchedule = { - start: '10', - period: '10', - periodCount: '30', - perPeriod: '100', - }; - const currentBlock = new BigNumber(30); - const lockedTokens = { id: 'ormlvest', amount: '10000' }; - - describe('toBN', () => { - it('returns default value for undefined', () => { - const value = toBN(undefined); - expect(value).toEqual(new BigNumber(constants.defaultValue)); - }); - }); - - describe('calculateFutureLock', () => { - it('can calculate future lock for one vesting schedule', () => { - const futureLock = calculateFutureLock(vestingSchedule, currentBlock); - - expect(futureLock).toEqual(new BigNumber(2800)); - }); - }); - - describe('calculateClaimableAmount', () => { - it('can calculate claimable amount', () => { - // const claimableAmount = calculateClaimableAmount( - // [vestingSchedule, vestingSchedule], - // lockedTokens, - // currentBlock - // ); - - // expect(claimableAmount).toEqual( - // toBN(lockedTokens.amount).minus(toBN('2800').multipliedBy(2)) - // ); - }); - }); -}); diff --git a/src/hooks/vesting/calculateClaimableAmount.tsx b/src/hooks/vesting/calculateClaimableAmount.tsx index ba5c15f0..e96e1cef 100644 --- a/src/hooks/vesting/calculateClaimableAmount.tsx +++ b/src/hooks/vesting/calculateClaimableAmount.tsx @@ -1,10 +1,9 @@ import { ApiPromise } from '@polkadot/api'; -import { BalanceLock } from '@polkadot/types/interfaces'; import { Codec } from '@polkadot/types/types'; import BigNumber from 'bignumber.js'; import { find } from 'lodash'; -import constants from '../../constants'; -import { VestingSchedule } from './useGetVestingScheduleByAddress'; +import errors from '../../errors'; +import { VestingSchedule } from './useGetVestingByAddress'; export const balanceLockDataType = 'Vec'; export const tokensLockDataType = balanceLockDataType; @@ -61,58 +60,61 @@ export const getLockedBalanceByAddressAndLockId = async ( }; /** - * This function casts a number in string representation - * to a BigNumber. If the input is undefined, it returns - * a default value. + * Calculates original and future lock for given VestingSchedule. + * https://gist.github.com/maht0rz/53466af0aefba004d5a4baad23f8ce26 */ -export const toBN = (numberAsString: string | undefined) => { - // TODO: check if it is any good to use default values - // on undefined VestingSchedule properties! - if (!numberAsString) return new BigNumber(constants.defaultValue); - return new BigNumber(numberAsString); -}; - -// https://gist.github.com/maht0rz/53466af0aefba004d5a4baad23f8ce26 -// TODO: check if calc makes sense for undefined VestingSchedule properties -export const calculateFutureLock = ( +export const calculateLock = ( vestingSchedule: VestingSchedule, - currentBlockNumber: BigNumber -) => { - const startPeriod = toBN(vestingSchedule.start); - const period = toBN(vestingSchedule.period); - const numberOfPeriods = currentBlockNumber + currentBlockNumber: string +): [BigNumber, BigNumber] => { + // check for undefined property in vestingSchedule + if ( + !vestingSchedule.perPeriod || + !vestingSchedule.period || + !vestingSchedule.periodCount || + !vestingSchedule.start + ) + throw Error(errors.vestingScheduleIncomplete); + + const startPeriod = new BigNumber(vestingSchedule.start); + const period = new BigNumber(vestingSchedule.period); + const numberOfPeriods = new BigNumber(currentBlockNumber) .minus(startPeriod) .dividedBy(period); - const perPeriod = toBN(vestingSchedule.perPeriod); + const perPeriod = new BigNumber(vestingSchedule.perPeriod); const vestedOverPeriods = numberOfPeriods.multipliedBy(perPeriod); - const periodCount = toBN(vestingSchedule.periodCount); + const periodCount = new BigNumber(vestingSchedule.periodCount); const originalLock = periodCount.multipliedBy(perPeriod); const futureLock = originalLock.minus(vestedOverPeriods); - return futureLock; + + return [originalLock, futureLock]; }; -// get lockedVestingAmount from function getLockedBalanceByAddressAndLockId -export const calculateClaimableAmount = ( +export const calculateTotalLocks = ( vestingSchedules: VestingSchedule[], - lockedVestingAmount: string, - currentBlockNumber: BigNumber -): BigNumber => { - // calculate futureLock for every vesting schedule and sum to total - const totalFutureLocks = vestingSchedules.reduce(function ( - total, - vestingSchedule - ) { - const futureLock = calculateFutureLock(vestingSchedule, currentBlockNumber); - return total.plus(futureLock); - }, - new BigNumber(0)); - - // calculate claimable amount - const remainingVestingAmount = toBN(lockedVestingAmount); - const claimableAmount = remainingVestingAmount.minus(totalFutureLocks); - - return claimableAmount; + currentBlockNumber: string +) => { + // calculate originalLock and futureLock for every vesting schedule and sum to total + const total = vestingSchedules.reduce( + function (total, vestingSchedule) { + const [originalLock, futureLock] = calculateLock( + vestingSchedule, + currentBlockNumber + ); + + total.original.plus(originalLock); + total.future.plus(futureLock); + + return total; + }, + { original: new BigNumber('0'), future: new BigNumber('0') } + ); + + return { + original: total.original.toString(), + future: total.future.toString(), + }; }; diff --git a/src/hooks/vesting/useGetVestingScheduleByAddress.tsx b/src/hooks/vesting/useGetVestingByAddress.tsx similarity index 50% rename from src/hooks/vesting/useGetVestingScheduleByAddress.tsx rename to src/hooks/vesting/useGetVestingByAddress.tsx index a7566ccf..915346b6 100644 --- a/src/hooks/vesting/useGetVestingScheduleByAddress.tsx +++ b/src/hooks/vesting/useGetVestingByAddress.tsx @@ -1,29 +1,39 @@ -import { useCallback, useMemo } from 'react'; +import { useMemo } from 'react'; import { usePolkadotJsContext } from '../polkadotJs/usePolkadotJs'; import { Vec } from '@polkadot/types'; import { VestingScheduleOf } from '@open-web3/orml-types/interfaces'; import { ApiPromise } from '@polkadot/api'; -import { calculateClaimableAmount, getLockedBalanceByAddressAndLockId, vestingBalanceLockId } from './calculateClaimableAmount'; +import { + calculateTotalLocks, + getLockedBalanceByAddressAndLockId, + vestingBalanceLockId, +} from './calculateClaimableAmount'; import { readLastBlock } from '../lastBlock/readLastBlock'; -import { ApolloCache, ApolloClient } from '@apollo/client'; +import { ApolloClient } from '@apollo/client'; import BigNumber from 'bignumber.js'; import { fromPrecision12 } from '../math/useFromPrecision'; export const vestingScheduleDataType = 'Vec'; -// TODO: use type from graphql in VestingSchedule.graphql +// TODO: use type from graphql in VestingSchedule.graphql // without remainingVestingAmount property - currently yarn run codegen is broken export type VestingSchedule = { start: string | undefined; - period: string| undefined; - periodCount: string| undefined; - perPeriod: string| undefined; + period: string | undefined; + periodCount: string | undefined; + perPeriod: string | undefined; }; -export const getVestingSchedulesByAddressFactory = - (apiInstance?: ApiPromise) => async (client: ApolloClient, address?: string) => { +export const getVestingByAddressFactory = + (apiInstance?: ApiPromise) => + async (client: ApolloClient, address?: string) => { if (!apiInstance || !address) return; + const currentBlockNumber = + readLastBlock(client)?.lastBlock?.relaychainBlockNumber; + if (!currentBlockNumber) + throw Error(`Can't calculate locks without current block number.`); + // TODO: instead of multiple .createType calls, use the following // https://github.com/AcalaNetwork/acala.js/blob/9634e2291f1723a84980b3087c55573763c8e82e/packages/sdk-core/src/functions/getSubscribeOrAtQuery.ts#L4 const vestingSchedulesData = apiInstance.createType( @@ -31,12 +41,6 @@ export const getVestingSchedulesByAddressFactory = await apiInstance.query.vesting.vestingSchedules(address) ) as Vec; - const lockedVestingBalance = (await getLockedBalanceByAddressAndLockId( - apiInstance, - address, - vestingBalanceLockId - ))?.amount?.toString(); - const vestingSchedules = vestingSchedulesData.map((vestingSchedule) => { // remap to object with string properties return { @@ -47,32 +51,42 @@ export const getVestingSchedulesByAddressFactory = } as VestingSchedule; }); - const currentBlockNumber = readLastBlock(client)?.lastBlock?.relaychainBlockNumber; + const totalLocks = calculateTotalLocks( + vestingSchedules, + currentBlockNumber! + ); + + const lockedVestingBalance = ( + await getLockedBalanceByAddressAndLockId( + apiInstance, + address, + vestingBalanceLockId + ) + )?.amount?.toString(); - const shouldCalculate = lockedVestingBalance && currentBlockNumber; + if (!lockedVestingBalance) + throw Error(`Can't fetch remaining vesting balance`); - const claimableAmount = shouldCalculate ? calculateClaimableAmount( - vestingSchedules, - lockedVestingBalance, - new BigNumber(currentBlockNumber) - ) : '0' + const totalRemainingVesting = new BigNumber(lockedVestingBalance!); + // claimable = remainingVesting - all future locks + const claimableAmount = totalRemainingVesting.minus( + new BigNumber(totalLocks.future) + ); - console.log('claimableAmount', { + return { claimableAmount: fromPrecision12(claimableAmount.toString()), - lockedVestingBalance: fromPrecision12(lockedVestingBalance) - }) - - return vestingSchedules; + originalLockBalance: fromPrecision12(totalLocks.original), + lockedVestingBalance: fromPrecision12(totalRemainingVesting.toString()), + }; }; -// TODO: change to plural "Schedules" -export const useGetVestingScheduleByAddress = () => { +export const useGetVestingByAddress = () => { const { apiInstance } = usePolkadotJsContext(); - const getVestingScheduleByAddress = useMemo( - () => getVestingSchedulesByAddressFactory(apiInstance), + const getVestingByAddress = useMemo( + () => getVestingByAddressFactory(apiInstance), [apiInstance] ); - return getVestingScheduleByAddress; + return getVestingByAddress; }; diff --git a/src/hooks/vesting/useVestingScheduleQueryResolvers.tsx b/src/hooks/vesting/useVestingQueryResolvers.tsx similarity index 51% rename from src/hooks/vesting/useVestingScheduleQueryResolvers.tsx rename to src/hooks/vesting/useVestingQueryResolvers.tsx index 33c0ddb6..8b599470 100644 --- a/src/hooks/vesting/useVestingScheduleQueryResolvers.tsx +++ b/src/hooks/vesting/useVestingQueryResolvers.tsx @@ -1,19 +1,19 @@ -import { ApolloCache, ApolloClient } from '@apollo/client'; +import { ApolloClient } from '@apollo/client'; import { useCallback } from 'react'; import { Account } from '../../generated/graphql'; import { withErrorHandler } from '../apollo/withErrorHandler'; -import { useGetVestingScheduleByAddress } from './useGetVestingScheduleByAddress'; +import { useGetVestingByAddress } from './useGetVestingByAddress'; -export const useVestingScheduleQueryResolvers = () => { - const getVestingScheduleByAddress = useGetVestingScheduleByAddress(); +export const useVestingQueryResolvers = () => { + const getVestingByAddress = useGetVestingByAddress(); const vestingSchedule = withErrorHandler( useCallback( async ( account: Account, _args: any, { client }: { client: ApolloClient } - ) => await getVestingScheduleByAddress(client, account.id), - [getVestingScheduleByAddress] + ) => await getVestingByAddress(client, account.id), + [getVestingByAddress] ), 'vestingSchedule' );