diff --git a/src/app/api/stats/supply/[token]/route.ts b/src/app/api/stats/supply/[token]/route.ts index 2873618..6b0eab2 100644 --- a/src/app/api/stats/supply/[token]/route.ts +++ b/src/app/api/stats/supply/[token]/route.ts @@ -1,14 +1,31 @@ -import { NextRequest, NextResponse } from "next/server" +import { + HNT_MAX_SUPPLY, + IOT_MAX_SUPPLY, + MOBILE_MAX_SUPPLY, +} from "@/app/stats/utils/emissions" import { fetchMint } from "@/app/stats/utils/fetchMint" -import { HNT_MINT, MOBILE_MINT, IOT_MINT, toNumber } from "@helium/spl-utils" import { - getRemainingEmissions, - getDailyEmisisons, MAX_DAILY_NET_EMISSIONS, + getDailyEmisisons, + getRemainingEmissions, } from "@/app/stats/utils/remainingEmissions" +import { db } from "@/knex/db" +import { MaxSupply } from "@/knex/maxSupply" +import { HNT_MINT, IOT_MINT, MOBILE_MINT, toNumber } from "@helium/spl-utils" +import { NextRequest, NextResponse } from "next/server" + +enum SupplyToken { + HNT = "hnt", + MOBILE = "mobile", + IOT = "iot", +} -type SupplyToken = "hnt" | "mobile" | "iot" -type SupplyType = "circulating" | "total" | "max" +enum SupplyType { + CIRCULATING = "circulating", + TOTAL = "total", + LIMIT = "limit", + MAX = "max", +} export async function GET( request: NextRequest, @@ -19,33 +36,41 @@ export async function GET( const type = searchParams.get("type") as SupplyType if ( - !["hnt", "mobile", "iot"].includes(token) || - !["circulating", "total", "max"].includes(type) + !Object.values(SupplyToken).includes(token) || + !Object.values(SupplyType).includes(type) ) { return new NextResponse(null, { status: 400 }) } const mintInfo = await fetchMint( { - hnt: HNT_MINT, - mobile: MOBILE_MINT, - iot: IOT_MINT, + [SupplyToken.HNT]: HNT_MINT, + [SupplyToken.MOBILE]: MOBILE_MINT, + [SupplyToken.IOT]: IOT_MINT, }[token] ) - if (type === "circulating") { + if (type === SupplyType.CIRCULATING || type === SupplyType.TOTAL) { const circulatingSupply = mintInfo.info?.info.supply! return NextResponse.json( toNumber(circulatingSupply, mintInfo?.info?.info.decimals || 0) ) - } else if (type === "total" || type === "max") { - // Return the same thing for total and max as they are functionally the same for us - let remainingEmissions = Math.ceil(getRemainingEmissions(new Date(), token)) + } else if (type === SupplyType.LIMIT) { + let remainingEmissions = 0 + let supply = mintInfo.info?.info.supply! - if (token === "hnt") { + if (token === SupplyToken.HNT) { // Due to Net Emissions, assume the max amount will be re-emitted - remainingEmissions += Math.ceil(MAX_DAILY_NET_EMISSIONS) + remainingEmissions = Math.ceil(MAX_DAILY_NET_EMISSIONS) + + // using existing supply limit logic to avoid repeating edge case logic + const maxSupplyDb = new MaxSupply(db) + const supplyLimit = (await maxSupplyDb.getLatest({ withBurn: false })) + ?.max_supply! + supply = supplyLimit + } else { + remainingEmissions += Math.ceil(getRemainingEmissions(new Date(), token)) } // Add the daily emissions for today to be conservative @@ -54,12 +79,23 @@ export async function GET( remainingEmissions += Math.ceil(dailyEmissions) const totalSupply = - mintInfo.info?.info.supply! + + supply + BigInt(remainingEmissions) * BigInt(Math.pow(10, mintInfo?.info?.info.decimals || 0)) return NextResponse.json( toNumber(totalSupply, mintInfo?.info?.info.decimals || 0) ) + } else if (type === SupplyType.MAX) { + switch (token) { + case SupplyToken.HNT: + return NextResponse.json(HNT_MAX_SUPPLY) + case SupplyToken.MOBILE: + return NextResponse.json(MOBILE_MAX_SUPPLY) + case SupplyToken.IOT: + return NextResponse.json(IOT_MAX_SUPPLY) + default: + return new NextResponse(null, { status: 400 }) + } } } diff --git a/src/app/stats/components/HntInfo.tsx b/src/app/stats/components/HntInfo.tsx index 2d63ab2..a86a43e 100644 --- a/src/app/stats/components/HntInfo.tsx +++ b/src/app/stats/components/HntInfo.tsx @@ -19,6 +19,7 @@ import { fetchHntBurn, fetchHntEmissions, } from "../utils/dune/fetchHntEmissions" +import { HNT_MAX_SUPPLY } from "../utils/emissions" import { fetchHntGovernanceStats } from "../utils/fetchGovernanceMetrics" import { fetchMint } from "../utils/fetchMint" import { getNextHalvening } from "../utils/getNextHalvening" @@ -127,7 +128,7 @@ export const HntInfo = async () => { }} /> { tooltip={{ description: "Maximum supply of HNT derived by summing current supply, remaining emissions, and today's burned HNT (which are re-emitted via net emissions). This is an upper limit that will not be reached and does not consider future HNT burn. Accurate within 1643 HNT.", - cadence: `Every 8h (last run ${format( + cadence: `Daily (last run ${format( maxSupplyRecord.recorded_at, DATE_FORMAT )})`, + id: "HNT Supply Limit", + }} + /> + diff --git a/src/app/stats/components/SubDaoInfo.tsx b/src/app/stats/components/SubDaoInfo.tsx index cc47b22..92c9a6f 100644 --- a/src/app/stats/components/SubDaoInfo.tsx +++ b/src/app/stats/components/SubDaoInfo.tsx @@ -11,6 +11,7 @@ import { toNumber, } from "@helium/spl-utils" import { PublicKey } from "@solana/web3.js" +import { IOT_MAX_SUPPLY, MOBILE_MAX_SUPPLY } from "../utils/emissions" import { fetchSubDaoGovernanceStats } from "../utils/fetchGovernanceMetrics" import { fetchMint } from "../utils/fetchMint" import { fetchSubDaoEpochInfo } from "../utils/fetchSubDaoEpochInfo" @@ -31,6 +32,7 @@ type SubDaoType = { subDaoMint: PublicKey maxDescription: string activeDetails: string + maxSupply: number } const MOBILE_INFO: SubDaoType = { @@ -43,6 +45,7 @@ const MOBILE_INFO: SubDaoType = { maxDescription: "This is an upper limit that will not be reached and does not consider future MOBILE burn. Reason: Daily emissions are currently only 86% of scheduled emissions, as not all rewardable entities (service providers, and oracles) exist or currently receive rewards.", activeDetails: " This exclusively includes active gateways (not radios).", + maxSupply: MOBILE_MAX_SUPPLY, } const IOT_INFO: SubDaoType = { @@ -55,6 +58,7 @@ const IOT_INFO: SubDaoType = { maxDescription: "This is an upper limit that will not be reached and does not consider future IOT burn. Reason: Daily emissions are currently only 93% of scheduled emissions, as oracles do not currently receive rewards.", activeDetails: "", + maxSupply: IOT_MAX_SUPPLY, } export const SubDaoInfo = async ({ subDao }: { subDao: SubDao }) => { @@ -67,6 +71,7 @@ export const SubDaoInfo = async ({ subDao }: { subDao: SubDao }) => { icon, subDaoMint, maxDescription, + maxSupply, } = subDao === "mobile" ? MOBILE_INFO : IOT_INFO const [activeCount, mintInfo, epochInfo, treasuryInfo, governanceMetrics] = await Promise.all([ @@ -86,7 +91,7 @@ export const SubDaoInfo = async ({ subDao }: { subDao: SubDao }) => { const remainingEmissions = Math.round( getRemainingEmissions(new Date(), subDao) ) - const maxSupply = + const supplyLimit = mintInfo.info?.info.supply! + BigInt(remainingEmissions) * BigInt(1000000) const supplyStaked = governanceMetrics.total.hnt @@ -173,15 +178,24 @@ export const SubDaoInfo = async ({ subDao }: { subDao: SubDao }) => { }} /> + diff --git a/src/app/stats/utils/emissions.ts b/src/app/stats/utils/emissions.ts new file mode 100644 index 0000000..4b2d196 --- /dev/null +++ b/src/app/stats/utils/emissions.ts @@ -0,0 +1,3 @@ +export const HNT_MAX_SUPPLY = 223_000_000 +export const IOT_MAX_SUPPLY = 200_000_000_000 +export const MOBILE_MAX_SUPPLY = 200_000_000_000 diff --git a/src/knex/maxSupply.ts b/src/knex/maxSupply.ts index e60b302..5e5fa1b 100644 --- a/src/knex/maxSupply.ts +++ b/src/knex/maxSupply.ts @@ -15,7 +15,7 @@ export class MaxSupply { this.knex = knex } - private async getLatest({ + async getLatest({ withBurn, }: { withBurn: boolean