Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/staking/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const loadDataForStakeAccount = async (
client: PythStakingClient,
hermesClient: HermesClient,
stakeAccount: PublicKey,
) => {
): Promise<Data> => {
const [
{ publishers, ...baseInfo },
stakeAccountCustody,
Expand Down Expand Up @@ -240,7 +240,7 @@ const loadDataForStakeAccount = async (
cooldown: filterGovernancePositions(PositionState.PREUNLOCKING),
cooldown2: filterGovernancePositions(PositionState.UNLOCKING),
},
unlockSchedule,
unlockSchedule: unlockSchedule.schedule,
integrityStakingPublishers: publishers.map((publisher) => ({
...publisher,
positions: {
Expand Down
95 changes: 95 additions & 0 deletions apps/staking/src/app/api/stake-accounts/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { PythStakingClient } from "@pythnetwork/staking-sdk";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";
import type { NextRequest } from "next/server";
import { z } from "zod";

import { IS_MAINNET, RPC } from "../../../config/server";

const UnlockScheduleSchema = z.object({
date: z.date(),
amount: z.number(),
});

const LockSchema = z.object({
type: z.string(),
schedule: z.array(UnlockScheduleSchema),
});

const ResponseSchema = z.array(
z.object({
custodyAccount: z.string(),
actualAmount: z.number(),
lock: LockSchema,
}),
);

const stakingClient = new PythStakingClient({
connection: new Connection(
RPC ??
clusterApiUrl(
IS_MAINNET ? WalletAdapterNetwork.Mainnet : WalletAdapterNetwork.Devnet,
),
),
});

const isValidPublicKey = (publicKey: string) => {
try {
new PublicKey(publicKey);
return true;
} catch {
return false;
}
};

export async function GET(req: NextRequest) {
const owner = req.nextUrl.searchParams.get("owner");

if (owner === null || !isValidPublicKey(owner)) {
return Response.json(
{
error:
"Must provide the 'owner' query parameters as a valid base58 public key",
},
{
status: 400,
},
);
}

const positions = await stakingClient.getAllStakeAccountPositions(
new PublicKey(owner),
);

const responseRaw = await Promise.all(
positions.map(async (position) => {
const custodyAccount =
await stakingClient.getStakeAccountCustody(position);
const lock = await stakingClient.getUnlockSchedule(position, true);
return {
custodyAccount: custodyAccount.address.toBase58(),
actualAmount: Number(custodyAccount.amount),
lock: {
type: lock.type,
schedule: lock.schedule.map((unlock) => ({
date: unlock.date,
amount: Number(unlock.amount),
})),
},
};
}),
);

const response = ResponseSchema.safeParse(responseRaw);

return response.success
? Response.json(response.data)
: Response.json(
{
error: "Internal server error",
},
{
status: 500,
},
);
}
42 changes: 42 additions & 0 deletions apps/staking/src/app/api/supply/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PythStakingClient } from "@pythnetwork/staking-sdk";
import { WalletAdapterNetwork } from "@solana/wallet-adapter-base";
import { clusterApiUrl, Connection } from "@solana/web3.js";
import type { NextRequest } from "next/server";
import { z } from "zod";

import { IS_MAINNET, RPC } from "../../../config/server";

const stakingClient = new PythStakingClient({
connection: new Connection(
RPC ??
clusterApiUrl(
IS_MAINNET ? WalletAdapterNetwork.Mainnet : WalletAdapterNetwork.Devnet,
),
),
});

const querySchema = z.enum(["totalSupply", "circulatingSupply"]);

export async function GET(req: NextRequest) {
const query = querySchema.safeParse(req.nextUrl.searchParams.get("q"));
if (!query.success) {
return Response.json(
{
error:
"The 'q' query parameter must be one of 'totalSupply' or 'circulatingSupply'.",
},
{
status: 400,
},
);
}
const q = query.data;

if (q === "circulatingSupply") {
const circulatingSupply = await stakingClient.getCirculatingSupply();
return Response.json(Number(circulatingSupply));
} else {
const pythMint = await stakingClient.getPythTokenMint();
return Response.json(Number(pythMint.supply));
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems a bit odd to me to have a single endpoint that has a query param that enables it to do two totally different and unrelated things. Why not just split these into separate endpoints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i 100% agree, but let's keep it the same for now since other users are already integrated using this with the old ui.

}
1 change: 1 addition & 0 deletions governance/pyth_staking_sdk/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const ONE_MINUTE_IN_SECONDS = 60n;
const ONE_HOUR_IN_SECONDS = 60n * ONE_MINUTE_IN_SECONDS;
const ONE_DAY_IN_SECONDS = 24n * ONE_HOUR_IN_SECONDS;
const ONE_WEEK_IN_SECONDS = 7n * ONE_DAY_IN_SECONDS;
export const ONE_YEAR_IN_SECONDS = 365n * ONE_DAY_IN_SECONDS;

export const EPOCH_DURATION = ONE_WEEK_IN_SECONDS;

Expand Down
8 changes: 5 additions & 3 deletions governance/pyth_staking_sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export * from "./pdas";
export * from "./pyth-staking-client";
export * from "./types";
export * from "./utils/apy";
export * from "./utils/clock";
export * from "./utils/position";
export * from "./utils/pool";
export * from "./utils/apy";
export * from "./types";
export * from "./utils/position";
export * from "./utils/vesting";
59 changes: 54 additions & 5 deletions governance/pyth_staking_sdk/src/pyth-staking-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
createTransferInstruction,
getAccount,
getAssociatedTokenAddress,
getMint,
type Mint,
} from "@solana/spl-token";
import type { AnchorWallet } from "@solana/wallet-adapter-react";
import {
Expand All @@ -21,7 +23,12 @@ import {
TransactionInstruction,
} from "@solana/web3.js";

import { GOVERNANCE_ADDRESS, POSITIONS_ACCOUNT_SIZE } from "./constants";
import {
FRACTION_PRECISION_N,
GOVERNANCE_ADDRESS,
ONE_YEAR_IN_SECONDS,
POSITIONS_ACCOUNT_SIZE,
} from "./constants";
import {
getConfigAddress,
getDelegationRecordAddress,
Expand All @@ -35,6 +42,7 @@ import {
type PoolConfig,
type PoolDataAccount,
type StakeAccountPositions,
type VestingSchedule,
} from "./types";
import { convertBigIntToBN, convertBNToBigInt } from "./utils/bn";
import { epochToDate, getCurrentEpoch } from "./utils/clock";
Expand All @@ -55,7 +63,7 @@ import type { Staking } from "../types/staking";

export type PythStakingClientConfig = {
connection: Connection;
wallet: AnchorWallet | undefined;
wallet?: AnchorWallet;
};

export class PythStakingClient {
Expand Down Expand Up @@ -104,7 +112,9 @@ export class PythStakingClient {
}

/** Gets a users stake accounts */
public async getAllStakeAccountPositions(): Promise<PublicKey[]> {
public async getAllStakeAccountPositions(
owner?: PublicKey,
): Promise<PublicKey[]> {
const positionDataMemcmp = this.stakingProgram.coder.accounts.memcmp(
"positionData",
) as {
Expand All @@ -123,7 +133,7 @@ export class PythStakingClient {
{
memcmp: {
offset: 8,
bytes: this.wallet.publicKey.toBase58(),
bytes: owner?.toBase58() ?? this.wallet.publicKey.toBase58(),
},
},
],
Expand Down Expand Up @@ -511,7 +521,10 @@ export class PythStakingClient {
return sendTransaction([instruction], this.connection, this.wallet);
}

public async getUnlockSchedule(stakeAccountPositions: PublicKey) {
public async getUnlockSchedule(
stakeAccountPositions: PublicKey,
includePastPeriods = false,
) {
const stakeAccountMetadataAddress = getStakeAccountMetadataAddress(
stakeAccountPositions,
);
Expand All @@ -530,7 +543,38 @@ export class PythStakingClient {
return getUnlockSchedule({
vestingSchedule,
pythTokenListTime: config.pythTokenListTime,
includePastPeriods,
});
}

public async getCirculatingSupply() {
const vestingSchedule: VestingSchedule = {
periodicVestingAfterListing: {
initialBalance: 8_500_000_000n * FRACTION_PRECISION_N,
numPeriods: 4n,
periodDuration: ONE_YEAR_IN_SECONDS,
},
};

const config = await this.getGlobalConfig();

if (config.pythTokenListTime === null) {
throw new Error("Pyth token list time not set in global config");
}

const unlockSchedule = getUnlockSchedule({
vestingSchedule,
pythTokenListTime: config.pythTokenListTime,
includePastPeriods: false,
});

const totalLocked = unlockSchedule.schedule.reduce(
(total, unlock) => total + unlock.amount,
0n,
);

const mint = await this.getPythTokenMint();
return mint.supply - totalLocked;
}

async getAdvanceDelegationRecordInstructions(
Expand Down Expand Up @@ -687,4 +731,9 @@ export class PythStakingClient {
undefined,
);
}

public async getPythTokenMint(): Promise<Mint> {
const globalConfig = await this.getGlobalConfig();
return getMint(this.connection, globalConfig.pythTokenMint);
}
}
9 changes: 6 additions & 3 deletions governance/pyth_staking_sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ export type VestingScheduleAnchor = IdlTypes<Staking>["vestingSchedule"];
export type VestingSchedule = ConvertBNToBigInt<VestingScheduleAnchor>;

export type UnlockSchedule = {
date: Date;
amount: bigint;
}[];
type: "fullyUnlocked" | "periodicUnlockingAfterListing" | "periodicUnlocking";
schedule: {
date: Date;
amount: bigint;
}[];
};

export type StakeAccountPositions = {
address: PublicKey;
Expand Down
52 changes: 33 additions & 19 deletions governance/pyth_staking_sdk/src/utils/vesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,38 @@ import type { UnlockSchedule, VestingSchedule } from "../types";
export const getUnlockSchedule = (options: {
pythTokenListTime: bigint;
vestingSchedule: VestingSchedule;
includePastPeriods: boolean;
}): UnlockSchedule => {
const { vestingSchedule, pythTokenListTime } = options;
const { vestingSchedule, pythTokenListTime, includePastPeriods } = options;

if (vestingSchedule.fullyVested) {
return [];
return {
type: "fullyUnlocked",
schedule: [],
};
} else if (vestingSchedule.periodicVestingAfterListing) {
return getPeriodicUnlockSchedule({
balance: vestingSchedule.periodicVestingAfterListing.initialBalance,
numPeriods: vestingSchedule.periodicVestingAfterListing.numPeriods,
periodDuration:
vestingSchedule.periodicVestingAfterListing.periodDuration,
startDate: pythTokenListTime,
});
return {
type: "periodicUnlockingAfterListing",
schedule: getPeriodicUnlockSchedule({
balance: vestingSchedule.periodicVestingAfterListing.initialBalance,
numPeriods: vestingSchedule.periodicVestingAfterListing.numPeriods,
periodDuration:
vestingSchedule.periodicVestingAfterListing.periodDuration,
startDate: pythTokenListTime,
includePastPeriods,
}),
};
} else {
return getPeriodicUnlockSchedule({
balance: vestingSchedule.periodicVesting.initialBalance,
numPeriods: vestingSchedule.periodicVesting.numPeriods,
periodDuration: vestingSchedule.periodicVesting.periodDuration,
startDate: vestingSchedule.periodicVesting.startDate,
});
return {
type: "periodicUnlocking",
schedule: getPeriodicUnlockSchedule({
balance: vestingSchedule.periodicVesting.initialBalance,
numPeriods: vestingSchedule.periodicVesting.numPeriods,
periodDuration: vestingSchedule.periodicVesting.periodDuration,
startDate: vestingSchedule.periodicVesting.startDate,
includePastPeriods,
}),
};
}
};

Expand All @@ -31,16 +43,18 @@ export const getPeriodicUnlockSchedule = (options: {
startDate: bigint;
periodDuration: bigint;
numPeriods: bigint;
}): UnlockSchedule => {
const { balance, startDate, periodDuration, numPeriods } = options;
includePastPeriods: boolean;
}): UnlockSchedule["schedule"] => {
const { balance, startDate, periodDuration, numPeriods, includePastPeriods } =
options;

const unlockSchedule: UnlockSchedule = [];
const unlockSchedule: UnlockSchedule["schedule"] = [];
const currentTimeStamp = Date.now() / 1000;

for (let i = 0; i < numPeriods; i++) {
const unlockTimeStamp =
Number(startDate) + Number(periodDuration) * (i + 1);
if (currentTimeStamp < unlockTimeStamp) {
if (currentTimeStamp < unlockTimeStamp || includePastPeriods) {
unlockSchedule.push({
date: new Date(unlockTimeStamp * 1000),
amount: balance / numPeriods,
Expand Down
Loading