diff --git a/packages/neuron-ui/src/components/NervosDAO/index.tsx b/packages/neuron-ui/src/components/NervosDAO/index.tsx index a1120c5431..5008c7e700 100644 --- a/packages/neuron-ui/src/components/NervosDAO/index.tsx +++ b/packages/neuron-ui/src/components/NervosDAO/index.tsx @@ -7,6 +7,7 @@ import appState from 'states/initStates/app' import { AppActions, StateWithDispatch } from 'states/stateProvider/reducer' import { updateNervosDaoData, clearNervosDaoData } from 'states/stateProvider/actionCreators' +import calculateGlobalAPY from 'utils/calculateGlobalAPY' import calculateFee from 'utils/calculateFee' import { shannonToCKBFormatter, CKBToShannonFormatter } from 'utils/formatters' import { MIN_DEPOSIT_AMOUNT, MEDIUM_FEE_RATE, CapacityUnit } from 'utils/const' @@ -28,6 +29,7 @@ const NervosDAO = ({ loadings: { sending = false }, tipBlockNumber, tipBlockHash, + tipBlockTimestamp, epoch, }, wallet, @@ -41,6 +43,7 @@ const NervosDAO = ({ const [activeRecord, setActiveRecord] = useState(null) const [errorMessage, setErrorMessage] = useState('') const [withdrawList, setWithdrawList] = useState<(string | null)[]>([]) + const [globalAPY, setGlobalAPY] = useState(0) const clearGeneratedTx = useCallback(() => { dispatch({ @@ -92,6 +95,14 @@ const NervosDAO = ({ } }, [clearGeneratedTx, dispatch, updateDepositValue, wallet.id]) + useEffect(() => { + if (tipBlockTimestamp) { + calculateGlobalAPY(tipBlockTimestamp).then(apy => { + setGlobalAPY(apy) + }) + } + }, [tipBlockTimestamp]) + const onDepositDialogDismiss = () => { setShowDepositDialog(false) setDepositValue(`${MIN_DEPOSIT_AMOUNT}`) @@ -278,9 +289,10 @@ const NervosDAO = ({ {`Epoch number: ${epochInfo.number}`} {`Epoch index: ${epochInfo.index}`} {`Epoch length: ${epochInfo.length}`} + {`APY: ~${globalAPY}%`} ) - }, [epoch]) + }, [epoch, globalAPY]) return ( <> diff --git a/packages/neuron-ui/src/containers/Main/hooks.ts b/packages/neuron-ui/src/containers/Main/hooks.ts index 7bea9fe347..568df8775a 100644 --- a/packages/neuron-ui/src/containers/Main/hooks.ts +++ b/packages/neuron-ui/src/containers/Main/hooks.ts @@ -40,6 +40,7 @@ export const useSyncChainData = ({ chainURL, dispatch }: { chainURL: string; dis payload: { tipBlockNumber: `${BigInt(header.number)}`, tipBlockHash: header.hash, + tipBlockTimestamp: +header.timestamp, chain: chainInfo.chain, difficulty: `${BigInt(chainInfo.difficulty)}`, epoch: chainInfo.epoch, diff --git a/packages/neuron-ui/src/states/initStates/app.ts b/packages/neuron-ui/src/states/initStates/app.ts index 690b1e9b43..1eabdd4b18 100644 --- a/packages/neuron-ui/src/states/initStates/app.ts +++ b/packages/neuron-ui/src/states/initStates/app.ts @@ -3,6 +3,7 @@ import { CapacityUnit } from 'utils/const' const appState: State.App = { tipBlockNumber: '', tipBlockHash: '', + tipBlockTimestamp: 0, chain: '', difficulty: '', epoch: '', diff --git a/packages/neuron-ui/src/tests/calculation/calculateGlobalAPY/fixtures.ts b/packages/neuron-ui/src/tests/calculation/calculateGlobalAPY/fixtures.ts new file mode 100644 index 0000000000..3b0eb42968 --- /dev/null +++ b/packages/neuron-ui/src/tests/calculation/calculateGlobalAPY/fixtures.ts @@ -0,0 +1,12 @@ +export default { + 'return 0 if the genesis block is not loaded': { + currentTime: Date.now(), + genesisTime: undefined, + expectAPY: 0, + }, + 'one period and one handrand days': { + currentTime: new Date('2023-04-10').getTime(), + genesisTime: new Date('2019-01-01').getTime(), + expectAPY: 0.02369552868619654, + }, +} diff --git a/packages/neuron-ui/src/tests/calculation/calculateGlobalAPY/index.test.ts b/packages/neuron-ui/src/tests/calculation/calculateGlobalAPY/index.test.ts new file mode 100644 index 0000000000..d82bbc797c --- /dev/null +++ b/packages/neuron-ui/src/tests/calculation/calculateGlobalAPY/index.test.ts @@ -0,0 +1,16 @@ +import calculateGlobalAPY from 'utils/calculateGlobalAPY' +import fixtures from './fixtures' + +describe('calculate the global apy', () => { + const fixtureTable = Object.entries(fixtures).map(([title, { currentTime, genesisTime, expectAPY }]) => [ + title, + currentTime, + genesisTime, + expectAPY, + ]) + + test.each(fixtureTable)(`%s`, async (_title, currentTime, genesisTime, expectAPY) => { + const apy = await calculateGlobalAPY(currentTime, genesisTime) + expect(apy).toBe(expectAPY === 0 ? 0 : +expectAPY.toFixed(2)) + }) +}) diff --git a/packages/neuron-ui/src/types/App/index.d.ts b/packages/neuron-ui/src/types/App/index.d.ts index e214ee89dd..b45c5513cc 100644 --- a/packages/neuron-ui/src/types/App/index.d.ts +++ b/packages/neuron-ui/src/types/App/index.d.ts @@ -75,6 +75,7 @@ declare namespace State { interface App { tipBlockNumber: string tipBlockHash: string + tipBlockTimestamp: number chain: string difficulty: string epoch: string diff --git a/packages/neuron-ui/src/utils/calculateGlobalAPY.ts b/packages/neuron-ui/src/utils/calculateGlobalAPY.ts new file mode 100644 index 0000000000..4c189275e5 --- /dev/null +++ b/packages/neuron-ui/src/utils/calculateGlobalAPY.ts @@ -0,0 +1,48 @@ +import { getBlockByNumber } from '../services/chain' + +const INITIAL_OFFER = BigInt(33600000000) +const SECONDARY_OFFER = BigInt(1344000000) +const DAYS_PER_PERIOD = 365 * 4 * 1 +const MILLI_SECONDS_PER_DAY = 24 * 3600 * 1000 +const PERIOD_LENGTH = DAYS_PER_PERIOD * MILLI_SECONDS_PER_DAY + +let cachedGenesisTimestamp: number | undefined + +export default async (now: number, initialTimestamp: number | undefined = cachedGenesisTimestamp) => { + let genesisTimestamp = initialTimestamp + if (genesisTimestamp === undefined) { + genesisTimestamp = await getBlockByNumber('0x0') + .then(b => { + cachedGenesisTimestamp = +b.header.timestamp + return cachedGenesisTimestamp + }) + .catch(() => undefined) + } + if (genesisTimestamp === undefined || now <= genesisTimestamp) { + return 0 + } + + const pastPeriods = BigInt(now - genesisTimestamp) / BigInt(PERIOD_LENGTH) + const pastDays = Math.ceil(((now - genesisTimestamp) % PERIOD_LENGTH) / MILLI_SECONDS_PER_DAY) + + const realSecondaryOffer = + BigInt(4) * SECONDARY_OFFER * pastPeriods + + (BigInt(4) * SECONDARY_OFFER * BigInt(pastDays)) / BigInt(DAYS_PER_PERIOD) + + let realPrimaryOffer = BigInt(0) + + let PRIMARY_OFFER = INITIAL_OFFER + for (let i = 0; i < Number(pastPeriods); i++) { + PRIMARY_OFFER /= BigInt(2) + const offer = PRIMARY_OFFER + realPrimaryOffer += offer + } + + PRIMARY_OFFER /= BigInt(2) + + const primaryOfferFraction = (BigInt(pastDays) * PRIMARY_OFFER) / BigInt(DAYS_PER_PERIOD) + realPrimaryOffer += primaryOfferFraction + + const totalOffer = INITIAL_OFFER + realPrimaryOffer + realSecondaryOffer + return +(Number(SECONDARY_OFFER) / Number(totalOffer)).toFixed(2) +}