Skip to content

Commit

Permalink
Add a helper function in sei-js to calculate expected APR. (#119)
Browse files Browse the repository at this point in the history
* Add APR Helper to sei-js utils

* fix testts

* rename

* fix types

* remove renamed files

* more package issues

* add changeset

* rounding errors

* camelCase
  • Loading branch information
mj850 authored Feb 1, 2024
1 parent 8141e1d commit 712fc82
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/young-rocks-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sei-js/core': minor
---

Add APR utilities
4 changes: 3 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"bech32": "^2.0.0",
"buffer": "^6.0.3",
"elliptic": "^6.5.4",
"moment": "^2.30.1",
"process": "^0.11.10",
"readonly-date": "^1.0.0",
"sha.js": "^2.4.11",
Expand All @@ -65,7 +66,8 @@
"@babel/preset-env": "^7.22.20",
"@babel/preset-typescript": "^7.22.15",
"@types/elliptic": "^6.4.14",
"@types/sha.js": "^2.4.1"
"@types/sha.js": "^2.4.1",
"long5": "npm:long"
},
"publishConfig": {
"access": "public"
Expand Down
79 changes: 79 additions & 0 deletions packages/core/src/lib/utils/__tests__/apr.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect, it } from '@jest/globals';
import { getUpcomingMintTokens } from '../apr';
import moment from 'moment';
import Long from 'long5';

const releaseSchedule = [
{
"token_release_amount": new Long(500000),
"start_date": "2023-10-01",
"end_date": "2023-10-20"
},
{
"token_release_amount": new Long(500000),
"start_date": "2023-11-01",
"end_date": "2023-11-30"
},
{
"token_release_amount": new Long(500000),
"start_date": "2023-11-30",
"end_date": "2023-12-31"
},
{
"token_release_amount": new Long(10000000),
"start_date": "2024-01-01",
"end_date": "2024-12-31"
},
{
"token_release_amount": new Long(10000000),
"start_date": "2025-01-01",
"end_date": "2025-12-31"
},
{
"token_release_amount": new Long(8500000),
"start_date": "2023-01-01",
"end_date": "2023-09-30"
},
]

describe('getUpcomingMintTokens', () => {
// Test from 2023-01-01 to 2024-01-01 (exclusive).
// This should get the full distribution from 10/01 - 10/20, 11/01-11/30, and 11/30-12/31
it('should return a correct amount of tokens', () => {
const result = getUpcomingMintTokens(moment("2023-01-01"), 365, releaseSchedule);

expect(result).toBe(10000000);
});

it('handles gaps in releases', () => {
// Test from 2023-10-11 to 2023-11-16 (exclusive).
// This should get half the distribution from 10/1 - 10/20 and half from the release from 11/1 - 11/30
const result = getUpcomingMintTokens(moment("2023-10-11"), 36, releaseSchedule);

expect(result).toBe(500000);
});

it('handles input windows within the same release', () => {
// Test any 73 day window (1/5 of 365 days) in 2025.
// This should get 20% the distribution from 2025-01-01 to 2025-12-31
const result = getUpcomingMintTokens(moment("2025-04-13"), 73, releaseSchedule);

expect(result).toBe(2000000);
});

it('handles input windows that start before any schedule', () => {
// Test from 2022-12-15 - 2023-10-1 (exclusive).
// This should get 100% the distribution from 2023-01-01 to 2023-09-30
const result = getUpcomingMintTokens(moment("2022-12-15"), 289, releaseSchedule);

expect(result).toBe(8500000);
});

it('handles input windows that end after any schedule', () => {
// Test the last 73 days of 2025 to sometime in 2026.
// This should get 20% the distribution from 2025-01-01 to 2025-12-31
const result = getUpcomingMintTokens(moment("2025-10-20"), 365, releaseSchedule);

expect(result).toBe(2000000);
});
});
132 changes: 132 additions & 0 deletions packages/core/src/lib/utils/apr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { ScheduledTokenReleaseSDKType } from "@sei-js/proto/dist/types/codegen/seiprotocol/seichain/mint/v1beta1/mint";
import { getQueryClient } from "../queryClient";
import moment, { Moment } from 'moment';
export type QueryClient = Awaited<ReturnType<typeof getQueryClient>>;

export async function estimateStakingAPR(queryClient: QueryClient) {
// Query number of bonded tokens
const pool = await getPool(queryClient);
const bondedTokens = Number(pool?.bonded_tokens);

// Query mint schedule
const mintParams = await getMintParams(queryClient);
const mintSchedule = mintParams?.token_release_schedule;

if (!mintSchedule || !pool) {
throw new Error("Failed to query mintSchedule or pool");
}

// Calculate number of tokens to be minted in the next year.
const upcomingMintTokens = getUpcomingMintTokens(moment(), 365, mintSchedule);

// APR estimate is the number of tokens to be minted / current number of bonded tokens.
return upcomingMintTokens / bondedTokens
}

// Helper function to query the staking pool.
export async function getPool(queryClient: QueryClient) {
try {
const result = await queryClient.cosmos.staking.v1beta1.pool({});
return result.pool;
} catch (error) {
console.log(error);
}
}

// Helper function to query the mint module params.
export async function getMintParams(queryClient: QueryClient) {
try {
const result = await queryClient.seiprotocol.seichain.mint.params({});
return result.params;
} catch (error) {
console.log(error);
}
}

// Gets the number of tokens that will be minted in the given window based on the given releaseSchedule.
// Assumes that releaseSchedule has no overlapping schedules.
export function getUpcomingMintTokens(startDate: Moment, days: number, releaseSchedule: ScheduledTokenReleaseSDKType[]): number {
// End date is the exclusive end date of the window to query.
// Ie. if start date is 2023-1-1 and days is 365, end date here will be 2024-1-1 so rewards will be calculated from 2023-1-1 to 2023-12-31
const endDate = startDate.clone().add(days, 'days')

// Sort release schedule in increasing order of start time.
let sortedReleaseSchedule: ReleaseSchedule[] = getSortedReleaseSchedule(releaseSchedule);

var tokens: number = 0
for (var release of sortedReleaseSchedule) {
// Skip all schedules that ended before today.
if (release.endDate.isBefore(startDate)) {
continue;
}
// If the start date is after end date, we have come to the end of all releases we should consider.
if (release.startDate.isAfter(endDate)) {
break;
}
// All releases from here are part of the window.
// The case where this release started before today.
if (release.startDate.isBefore(startDate)) {

// Need to deduct 1 day from endDate to make it an inclusive end date.
let earlierInclusiveEndDate = moment.min(endDate.clone().subtract(1, "days"), release.endDate);

// Number of days left in this release.
let daysLeft: number = calculateDaysInclusive(startDate, earlierInclusiveEndDate);
let totalPeriod: number = calculateDaysInclusive(release.startDate, release.endDate);
tokens += (daysLeft / totalPeriod) * release.tokenReleaseAmount;
}

// The case where this release ends after our search window.
else if (release.endDate.isAfter(endDate)) {
let daysLeft: number = Math.round(endDate.diff(release.startDate, 'days', true));
let totalPeriod: number = calculateDaysInclusive(release.startDate, release.endDate);
tokens += (daysLeft / totalPeriod) * release.tokenReleaseAmount;
}

// In the final case, the entire period falls within our window.
else {
tokens += release.tokenReleaseAmount;
}
}

return tokens;
}

// Converts the releaseSchedule into ReleaseSchedule[] and sorts it by start date.
function getSortedReleaseSchedule(releaseSchedule: ScheduledTokenReleaseSDKType[]) {
let releaseScheduleTimes = releaseSchedule.map((schedule) => {
return createReleaseSchedule(schedule.start_date, schedule.end_date, schedule.token_release_amount);
})

// Sort release schedule in increasing order of start time.
let sortedReleaseSchedule = releaseScheduleTimes.sort((x, y) => {
if (x.startDate.isAfter(y.startDate)) {
return 1;
}
else if (y.startDate.isAfter(x.startDate)) {
return -1;
}
return 0;
})

return sortedReleaseSchedule;
}

// Returns the number of days in the window inclusive of the start and end date.
function calculateDaysInclusive(startDate: Moment, endDate: Moment) {
return Math.round(endDate.diff(startDate, 'days', true)) + 1;
}

interface ReleaseSchedule {
startDate: Moment;
endDate: Moment;
tokenReleaseAmount: number;
}

function createReleaseSchedule(start_date: string, end_date: string, token_release_amount: Long): ReleaseSchedule {
return {
startDate: moment(start_date),
endDate: moment(end_date),
tokenReleaseAmount: Number(token_release_amount),
}
}
1 change: 1 addition & 0 deletions packages/core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"strict": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noImplicitAny": false,
"lib": ["ES6", "DOM"]
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11561,6 +11561,11 @@ loglevel@^1.6.0:
resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4"
integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==

"long5@npm:long":
version "5.2.3"
resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1"
integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==

long@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
Expand Down Expand Up @@ -12072,6 +12077,11 @@ module-deps@^6.2.3:
through2 "^2.0.0"
xtend "^4.0.0"

moment@^2.30.1:
version "2.30.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==

monaco-editor@^0.38.0:
version "0.38.0"
resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.38.0.tgz#7b3cd16f89b1b8867fcd3c96e67fccee791ff05c"
Expand Down

0 comments on commit 712fc82

Please sign in to comment.