Skip to content

Commit dc82cbb

Browse files
authored
feat(neuron-ui): add global apy estimation (#1092)
* feat(neuron-ui): add global apy estimation * test(neuron-ui): fix the test of calculateGlobalAPY
1 parent 4eb367f commit dc82cbb

7 files changed

Lines changed: 92 additions & 1 deletion

File tree

packages/neuron-ui/src/components/NervosDAO/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import appState from 'states/initStates/app'
77
import { AppActions, StateWithDispatch } from 'states/stateProvider/reducer'
88
import { updateNervosDaoData, clearNervosDaoData } from 'states/stateProvider/actionCreators'
99

10+
import calculateGlobalAPY from 'utils/calculateGlobalAPY'
1011
import calculateFee from 'utils/calculateFee'
1112
import { shannonToCKBFormatter, CKBToShannonFormatter } from 'utils/formatters'
1213
import { MIN_DEPOSIT_AMOUNT, MEDIUM_FEE_RATE, CapacityUnit } from 'utils/const'
@@ -28,6 +29,7 @@ const NervosDAO = ({
2829
loadings: { sending = false },
2930
tipBlockNumber,
3031
tipBlockHash,
32+
tipBlockTimestamp,
3133
epoch,
3234
},
3335
wallet,
@@ -41,6 +43,7 @@ const NervosDAO = ({
4143
const [activeRecord, setActiveRecord] = useState<State.NervosDAORecord | null>(null)
4244
const [errorMessage, setErrorMessage] = useState('')
4345
const [withdrawList, setWithdrawList] = useState<(string | null)[]>([])
46+
const [globalAPY, setGlobalAPY] = useState(0)
4447

4548
const clearGeneratedTx = useCallback(() => {
4649
dispatch({
@@ -92,6 +95,14 @@ const NervosDAO = ({
9295
}
9396
}, [clearGeneratedTx, dispatch, updateDepositValue, wallet.id])
9497

98+
useEffect(() => {
99+
if (tipBlockTimestamp) {
100+
calculateGlobalAPY(tipBlockTimestamp).then(apy => {
101+
setGlobalAPY(apy)
102+
})
103+
}
104+
}, [tipBlockTimestamp])
105+
95106
const onDepositDialogDismiss = () => {
96107
setShowDepositDialog(false)
97108
setDepositValue(`${MIN_DEPOSIT_AMOUNT}`)
@@ -278,9 +289,10 @@ const NervosDAO = ({
278289
<Text as="span" variant="small" block>{`Epoch number: ${epochInfo.number}`}</Text>
279290
<Text as="span" variant="small" block>{`Epoch index: ${epochInfo.index}`}</Text>
280291
<Text as="span" variant="small" block>{`Epoch length: ${epochInfo.length}`}</Text>
292+
<Text as="span" variant="small" block>{`APY: ~${globalAPY}%`}</Text>
281293
</Stack>
282294
)
283-
}, [epoch])
295+
}, [epoch, globalAPY])
284296

285297
return (
286298
<>

packages/neuron-ui/src/containers/Main/hooks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const useSyncChainData = ({ chainURL, dispatch }: { chainURL: string; dis
4040
payload: {
4141
tipBlockNumber: `${BigInt(header.number)}`,
4242
tipBlockHash: header.hash,
43+
tipBlockTimestamp: +header.timestamp,
4344
chain: chainInfo.chain,
4445
difficulty: `${BigInt(chainInfo.difficulty)}`,
4546
epoch: chainInfo.epoch,

packages/neuron-ui/src/states/initStates/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CapacityUnit } from 'utils/const'
33
const appState: State.App = {
44
tipBlockNumber: '',
55
tipBlockHash: '',
6+
tipBlockTimestamp: 0,
67
chain: '',
78
difficulty: '',
89
epoch: '',
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default {
2+
'return 0 if the genesis block is not loaded': {
3+
currentTime: Date.now(),
4+
genesisTime: undefined,
5+
expectAPY: 0,
6+
},
7+
'one period and one handrand days': {
8+
currentTime: new Date('2023-04-10').getTime(),
9+
genesisTime: new Date('2019-01-01').getTime(),
10+
expectAPY: 0.02369552868619654,
11+
},
12+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import calculateGlobalAPY from 'utils/calculateGlobalAPY'
2+
import fixtures from './fixtures'
3+
4+
describe('calculate the global apy', () => {
5+
const fixtureTable = Object.entries(fixtures).map(([title, { currentTime, genesisTime, expectAPY }]) => [
6+
title,
7+
currentTime,
8+
genesisTime,
9+
expectAPY,
10+
])
11+
12+
test.each(fixtureTable)(`%s`, async (_title, currentTime, genesisTime, expectAPY) => {
13+
const apy = await calculateGlobalAPY(currentTime, genesisTime)
14+
expect(apy).toBe(expectAPY === 0 ? 0 : +expectAPY.toFixed(2))
15+
})
16+
})

packages/neuron-ui/src/types/App/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ declare namespace State {
7575
interface App {
7676
tipBlockNumber: string
7777
tipBlockHash: string
78+
tipBlockTimestamp: number
7879
chain: string
7980
difficulty: string
8081
epoch: string
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { getBlockByNumber } from '../services/chain'
2+
3+
const INITIAL_OFFER = BigInt(33600000000)
4+
const SECONDARY_OFFER = BigInt(1344000000)
5+
const DAYS_PER_PERIOD = 365 * 4 * 1
6+
const MILLI_SECONDS_PER_DAY = 24 * 3600 * 1000
7+
const PERIOD_LENGTH = DAYS_PER_PERIOD * MILLI_SECONDS_PER_DAY
8+
9+
let cachedGenesisTimestamp: number | undefined
10+
11+
export default async (now: number, initialTimestamp: number | undefined = cachedGenesisTimestamp) => {
12+
let genesisTimestamp = initialTimestamp
13+
if (genesisTimestamp === undefined) {
14+
genesisTimestamp = await getBlockByNumber('0x0')
15+
.then(b => {
16+
cachedGenesisTimestamp = +b.header.timestamp
17+
return cachedGenesisTimestamp
18+
})
19+
.catch(() => undefined)
20+
}
21+
if (genesisTimestamp === undefined || now <= genesisTimestamp) {
22+
return 0
23+
}
24+
25+
const pastPeriods = BigInt(now - genesisTimestamp) / BigInt(PERIOD_LENGTH)
26+
const pastDays = Math.ceil(((now - genesisTimestamp) % PERIOD_LENGTH) / MILLI_SECONDS_PER_DAY)
27+
28+
const realSecondaryOffer =
29+
BigInt(4) * SECONDARY_OFFER * pastPeriods +
30+
(BigInt(4) * SECONDARY_OFFER * BigInt(pastDays)) / BigInt(DAYS_PER_PERIOD)
31+
32+
let realPrimaryOffer = BigInt(0)
33+
34+
let PRIMARY_OFFER = INITIAL_OFFER
35+
for (let i = 0; i < Number(pastPeriods); i++) {
36+
PRIMARY_OFFER /= BigInt(2)
37+
const offer = PRIMARY_OFFER
38+
realPrimaryOffer += offer
39+
}
40+
41+
PRIMARY_OFFER /= BigInt(2)
42+
43+
const primaryOfferFraction = (BigInt(pastDays) * PRIMARY_OFFER) / BigInt(DAYS_PER_PERIOD)
44+
realPrimaryOffer += primaryOfferFraction
45+
46+
const totalOffer = INITIAL_OFFER + realPrimaryOffer + realSecondaryOffer
47+
return +(Number(SECONDARY_OFFER) / Number(totalOffer)).toFixed(2)
48+
}

0 commit comments

Comments
 (0)