diff --git a/src/sdk/utils/math.test.ts b/src/sdk/utils/math.test.ts new file mode 100644 index 00000000..4e1f1757 --- /dev/null +++ b/src/sdk/utils/math.test.ts @@ -0,0 +1,97 @@ +import { computeStakingEmissionPerPeriod } from "./math" +import { Params } from "src/protojs/nibiru/inflation/v1/genesis" +import Long from "long" +import { QueryEpochMintProvisionResponse } from "src/protojs/nibiru/inflation/v1/query" + +const params = { + inflationEnabled: true, + polynomialFactors: [ + "-0.000147085524000000", + "0.074291982762000000", + "-18.867415611180000000", + "3128.641926954698000000", + "-334834.740631598223000000", + "17827464.906540066004000000", + ], + inflationDistribution: { + stakingRewards: "0.281250000000000000", + communityPool: "0.354825000000000000", + strategicReserves: "0.363925000000000000", + }, + epochsPerPeriod: new Long(30), + periodsPerYear: new Long(12), + maxPeriod: new Long(96), + hasInflationStarted: true, +} + +describe("computeStakingEmissionPerPeriod", () => { + interface TestCase { + name: string + in: { + myStake: number + totalStaked: number + params: Params + epochMintProvision: QueryEpochMintProvisionResponse + } + expected: number + shouldFail?: boolean + } + + const tests: TestCase[] = [ + { + name: "real", + in: { + myStake: 10, + totalStaked: 10_000_000, + params, + epochMintProvision: QueryEpochMintProvisionResponse.fromPartial({ + epochMintProvision: { amount: "17827464.906540066004" }, + }), + }, + expected: 5.013974504964393, + }, + { + name: "real - no stake", + in: { + myStake: 0, + totalStaked: 10_000_000, + params, + epochMintProvision: QueryEpochMintProvisionResponse.fromPartial({ + epochMintProvision: { amount: "17827464.906540066004" }, + }), + }, + expected: 0, + }, + { + name: "NaN", + in: { + myStake: 0, + totalStaked: 0, + params, + epochMintProvision: QueryEpochMintProvisionResponse.fromPartial({ + epochMintProvision: { amount: "0" }, + }), + }, + expected: NaN, + }, + ] + + test.each(tests)("%o", (tt) => { + let failed = false + try { + const res = computeStakingEmissionPerPeriod( + tt.in.params, + tt.in.epochMintProvision, + tt.in.totalStaked, + tt.in.myStake + ) + expect(res).toEqual(tt.expected) + } catch (e) { + if (!tt.shouldFail) { + console.error(`Test ${tt.name} failed with error: ${e}`) + } + failed = true + } + expect(failed).toBe(!!tt.shouldFail) + }) +}) diff --git a/src/sdk/utils/math.ts b/src/sdk/utils/math.ts new file mode 100644 index 00000000..68bb6850 --- /dev/null +++ b/src/sdk/utils/math.ts @@ -0,0 +1,35 @@ +import BigNumber from "bignumber.js" +import { Params } from "src/protojs/nibiru/inflation/v1/genesis" +import { QueryEpochMintProvisionResponse } from "src/protojs/nibiru/inflation/v1/query" + +/** + * Computes the amount of staking inflation claimable via the + * "cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward" transaction + * message based on the inflation parameters (`Params`), current inflation + * period, and the users percentage ownership of the total stake, or voting + * power. + * + * @param params - Nibiru inflation module parameters, which specify the + * polynomial for the NIBI token emmissions. + * @param epochMintProvision - EpochMintProvisionResponse from chain using txClient. + * @param totalStaked - Total stake (unibi) of all stakers in in the network. + * @param myStake - User's current stake. + * @param addedStake - New stake to add to the user's current stake. This is + * used to compute a new annual percentage return after a staking tx. + * */ +export const computeStakingEmissionPerPeriod = ( + params: Params, + epochMintProvision: QueryEpochMintProvisionResponse, + totalStaked: number, + myStake: number, + addedStake = 0 +) => { + const rewardForPeriod = BigNumber( + epochMintProvision.epochMintProvision?.amount ?? 0 + ).times(params.inflationDistribution?.stakingRewards ?? 0) + + return BigNumber(myStake + addedStake) + .div(totalStaked + addedStake) + .times(rewardForPeriod) + .toNumber() +}