-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a helper function in sei-js to calculate expected APR. (#119)
* 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
Showing
6 changed files
with
230 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@sei-js/core': minor | ||
--- | ||
|
||
Add APR utilities |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters