From e4e88515cc2ed04c6b2ef252f10130baea588033 Mon Sep 17 00:00:00 2001 From: guibescos <59208140+guibescos@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:01:23 +0000 Subject: [PATCH] [frontend] Try to fix API (#339) * Test * Go * Try another one * Try this * Clean it * try again * Test * EDge test * Another one * Another one * Go * Update import * Continue * Finish refactor * Refactor complete * Increase max duration * CMC works * CMC works * Cleanup * Increase timeout * Logs * Revert "Logs" This reverts commit 26aa610b7d1c3f1cb0e275ad0ab9078952e691f7. * Revert some stuff * Do it --- frontend/package.json | 1 + frontend/pages/api/getAllStakingAccounts.ts | 32 +++ frontend/pages/api/v1/all_locked_accounts.ts | 74 +----- frontend/pages/api/v1/cmc/supply.ts | 80 +----- frontend/pages/api/v1/locked_accounts.ts | 133 +--------- package-lock.json | 30 +-- staking/app/api_utils.ts | 249 +++++++++++++++++++ staking/app/constants.ts | 5 + staking/app/index.ts | 1 + vercel.json | 3 - 10 files changed, 335 insertions(+), 273 deletions(-) create mode 100644 frontend/pages/api/getAllStakingAccounts.ts create mode 100644 staking/app/api_utils.ts diff --git a/frontend/package.json b/frontend/package.json index 501fcdf6..627f3b66 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@solana/wallet-adapter-wallets": "^0.19.16", "@solana/web3.js": "^1.87.5", "@tippyjs/react": "^4.2.6", + "axios": "^1.6.7", "dotenv": "^16.0.0", "next": "12.2.5", "prop-types": "^15.8.1", diff --git a/frontend/pages/api/getAllStakingAccounts.ts b/frontend/pages/api/getAllStakingAccounts.ts new file mode 100644 index 00000000..daa7c770 --- /dev/null +++ b/frontend/pages/api/getAllStakingAccounts.ts @@ -0,0 +1,32 @@ +import { bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes' +import { PublicKey } from '@solana/web3.js' +import axios from 'axios' + +// The JSON payload is too big when using the @solana/web3.js getProgramAccounts +// We get around this by using the base64+ztsd encoding instead of base64 that @solana/web3.js uses +export async function getAllStakeAccounts(url: string): Promise { + const response = await axios({ + method: 'post', + url: url, + data: { + jsonrpc: '2.0', + id: 1, + method: 'getProgramAccounts', + params: [ + 'pytS9TjG1qyAZypk7n8rw8gfW9sUaqqYyMhJQ4E7JCQ', + { + encoding: 'base64+zstd', + filters: [ + { + memcmp: { + offset: 0, + bytes: bs58.encode(Buffer.from('55c3f14f7cc04f0b', 'hex')), // Positions account discriminator + }, + }, + ], + }, + ], + }, + }) + return response.data.result.map((x: any) => new PublicKey(x.pubkey)) +} diff --git a/frontend/pages/api/v1/all_locked_accounts.ts b/frontend/pages/api/v1/all_locked_accounts.ts index 39882d15..59afd37f 100644 --- a/frontend/pages/api/v1/all_locked_accounts.ts +++ b/frontend/pages/api/v1/all_locked_accounts.ts @@ -2,22 +2,22 @@ import { NextApiRequest, NextApiResponse } from 'next' import { PythBalance } from '@pythnetwork/staking/app/pythBalance' import BN from 'bn.js' import { STAKING_ADDRESS } from '@pythnetwork/staking/app/constants' -import { Connection, Keypair, PublicKey } from '@solana/web3.js' -import { bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes' -import { Program, AnchorProvider, IdlAccounts } from '@coral-xyz/anchor' +import { + getAllMetadataAccounts, + getCustodyAccountAddress, + hasStandardLockup, +} from '@pythnetwork/staking/app/api_utils' +import { Connection, Keypair } from '@solana/web3.js' +import { Program, AnchorProvider } from '@coral-xyz/anchor' import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' import { Staking } from '@pythnetwork/staking/lib/target/types/staking' import idl from '@pythnetwork/staking/target/idl/staking.json' import { splTokenProgram } from '@coral-xyz/spl-token' import { TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { - getCustodyAccountAddress, - getMetadataAccountAddress, -} from './locked_accounts' - -const ONE_YEAR = new BN(3600 * 24 * 365) +import { getAllStakeAccounts } from '../getAllStakingAccounts' -const connection = new Connection(process.env.BACKEND_ENDPOINT!) +const RPC_URL = process.env.BACKEND_ENDPOINT! +const connection = new Connection(RPC_URL) const provider = new AnchorProvider( connection, new NodeWallet(new Keypair()), @@ -37,7 +37,7 @@ export default async function handlerAllLockedAccounts( req: NextApiRequest, res: NextApiResponse ) { - const allStakeAccounts = await getAllStakeAccounts(connection) + const allStakeAccounts = await getAllStakeAccounts(RPC_URL) const allMetadataAccounts = await getAllMetadataAccounts( stakingProgram, @@ -86,55 +86,3 @@ export default async function handlerAllLockedAccounts( res.setHeader('Cache-Control', 'max-age=0, s-maxage=3600') res.status(200).json(data) } - -function hasStandardLockup( - metadataAccountData: IdlAccounts['stakeAccountMetadataV2'] -) { - return ( - metadataAccountData.lock.periodicVestingAfterListing && - metadataAccountData.lock.periodicVestingAfterListing.numPeriods.eq( - new BN(4) - ) && - metadataAccountData.lock.periodicVestingAfterListing.periodDuration.eq( - ONE_YEAR - ) - ) -} -export async function getAllStakeAccounts(connection: Connection) { - const response = await connection.getProgramAccounts(STAKING_ADDRESS, { - encoding: 'base64', - filters: [ - { - memcmp: { - offset: 0, - bytes: bs58.encode(Buffer.from('55c3f14f7cc04f0b', 'hex')), // Positions account discriminator - }, - }, - ], - }) - return response.map((account) => { - return account.pubkey - }) -} - -export async function getAllMetadataAccounts( - stakingProgram: Program, - stakeAccounts: PublicKey[] -): Promise<(IdlAccounts['stakeAccountMetadataV2'] | null)[]> { - const metadataAccountAddresses = stakeAccounts.map((account) => - getMetadataAccountAddress(account) - ) - return stakingProgram.account.stakeAccountMetadataV2.fetchMultiple( - metadataAccountAddresses - ) -} - -export async function getAllCustodyAccounts( - tokenProgram: any, - stakeAccounts: PublicKey[] -) { - const allCustodyAccountAddresses = stakeAccounts.map((account) => - getCustodyAccountAddress(account) - ) - return tokenProgram.account.account.fetchMultiple(allCustodyAccountAddresses) -} diff --git a/frontend/pages/api/v1/cmc/supply.ts b/frontend/pages/api/v1/cmc/supply.ts index ebfc7ff5..5f965557 100644 --- a/frontend/pages/api/v1/cmc/supply.ts +++ b/frontend/pages/api/v1/cmc/supply.ts @@ -2,23 +2,24 @@ import { NextApiRequest, NextApiResponse } from 'next' import { PythBalance } from '@pythnetwork/staking/app/pythBalance' import BN from 'bn.js' import { STAKING_ADDRESS } from '@pythnetwork/staking/app/constants' -import { Connection, Keypair, PublicKey } from '@solana/web3.js' -import { Program, AnchorProvider, IdlAccounts } from '@coral-xyz/anchor' +import { Connection, Keypair } from '@solana/web3.js' +import { Program, AnchorProvider } from '@coral-xyz/anchor' import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' import { Staking } from '@pythnetwork/staking/lib/target/types/staking' import idl from '@pythnetwork/staking/target/idl/staking.json' import { splTokenProgram } from '@coral-xyz/spl-token' import { TOKEN_PROGRAM_ID } from '@solana/spl-token' -import { getConfig } from './../locked_accounts' import { - getAllCustodyAccounts, + getCurrentlyLockedAmount, + getTotalSupply, getAllMetadataAccounts, - getAllStakeAccounts, -} from '../all_locked_accounts' - -const PYTH_TOKEN = new PublicKey('HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3') + getConfig, + getAllCustodyAccounts, +} from '@pythnetwork/staking/app/api_utils' +import { getAllStakeAccounts } from 'pages/api/getAllStakingAccounts' -const connection = new Connection(process.env.BACKEND_ENDPOINT!) +const RPC_URL = process.env.BACKEND_ENDPOINT! +const connection = new Connection(RPC_URL) const provider = new AnchorProvider( connection, new NodeWallet(new Keypair()), @@ -49,12 +50,13 @@ export default async function handlerSupply( res.status(200).send((await getTotalSupply(tokenProgram)).toString(false)) } else if (q === 'circulatingSupply') { const configAccountData = await getConfig(stakingProgram) - const allStakeAccounts = await getAllStakeAccounts(connection) + const allStakeAccounts = await getAllStakeAccounts(RPC_URL) const allMetadataAccounts = await getAllMetadataAccounts( stakingProgram, allStakeAccounts ) + const allCustodyAccounts = await getAllCustodyAccounts( tokenProgram, allStakeAccounts @@ -88,61 +90,3 @@ export default async function handlerSupply( }) } } - -function getCurrentlyLockedAmount( - metadataAccountData: IdlAccounts['stakeAccountMetadataV2'], - configAccountData: IdlAccounts['globalConfig'] -): PythBalance { - const lock = metadataAccountData.lock - const listTime = configAccountData.pythTokenListTime - if (lock.fullyVested) { - return PythBalance.zero() - } else if (lock.periodicVestingAfterListing) { - if (!listTime) { - return new PythBalance(lock.periodicVestingAfterListing.initialBalance) - } else { - return getCurrentlyLockedAmountPeriodic( - listTime, - lock.periodicVestingAfterListing.periodDuration, - lock.periodicVestingAfterListing.numPeriods, - lock.periodicVestingAfterListing.initialBalance - ) - } - } else if (lock.periodicVesting) { - return getCurrentlyLockedAmountPeriodic( - lock.periodicVesting.startDate, - lock.periodicVesting.periodDuration, - lock.periodicVesting.numPeriods, - lock.periodicVesting.initialBalance - ) - } else { - throw new Error('Should be unreachable') - } -} - -function getCurrentlyLockedAmountPeriodic( - startDate: BN, - periodDuration: BN, - numPeriods: BN, - initialBalance: BN -): PythBalance { - const currentTimestamp = new BN(Math.floor(Date.now() / 1000)) - if (currentTimestamp.lte(startDate)) { - return new PythBalance(initialBalance) - } else { - const periodsElapsed = currentTimestamp.sub(startDate).div(periodDuration) - if (periodsElapsed.gte(numPeriods)) { - return PythBalance.zero() - } else { - const remainingPeriods = numPeriods.sub(periodsElapsed) - return new PythBalance( - remainingPeriods.mul(initialBalance).div(numPeriods) - ) - } - } -} - -async function getTotalSupply(tokenProgram: any): Promise { - const pythTokenMintData = await tokenProgram.account.mint.fetch(PYTH_TOKEN) - return new PythBalance(pythTokenMintData.supply) -} diff --git a/frontend/pages/api/v1/locked_accounts.ts b/frontend/pages/api/v1/locked_accounts.ts index 1e908136..f114a892 100644 --- a/frontend/pages/api/v1/locked_accounts.ts +++ b/frontend/pages/api/v1/locked_accounts.ts @@ -1,15 +1,16 @@ import { NextApiRequest, NextApiResponse } from 'next' -import { PythBalance } from '@pythnetwork/staking/app/pythBalance' -import BN from 'bn.js' import { STAKING_ADDRESS } from '@pythnetwork/staking/app/constants' import { Connection, Keypair, PublicKey } from '@solana/web3.js' -import { bs58 } from '@coral-xyz/anchor/dist/cjs/utils/bytes' -import { Program, AnchorProvider, IdlAccounts } from '@coral-xyz/anchor' +import { Program, AnchorProvider } from '@coral-xyz/anchor' import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' import { Staking } from '@pythnetwork/staking/lib/target/types/staking' import idl from '@pythnetwork/staking/target/idl/staking.json' import { splTokenProgram } from '@coral-xyz/spl-token' import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { + getStakeAccountDetails, + getStakeAccountsByOwner, +} from '@pythnetwork/staking/app/api_utils' const connection = new Connection(process.env.BACKEND_ENDPOINT!) const provider = new AnchorProvider( @@ -38,134 +39,16 @@ export default async function handlerLockedAccounts( error: "Must provide the 'owner' query parameters", }) } else { - const stakeAccounts = await getStakeAccounts( + const stakeAccounts = await getStakeAccountsByOwner( connection, new PublicKey(owner) ) + const stakeAccountDetails = await Promise.all( stakeAccounts.map((account) => { - return getStakeAccountDetails(account) + return getStakeAccountDetails(stakingProgram, tokenProgram, account) }) ) res.status(200).json(stakeAccountDetails) } } - -export async function getConfig( - stakingProgram: Program -): Promise['globalConfig']> { - const configAccountAddress = PublicKey.findProgramAddressSync( - [Buffer.from('config')], - STAKING_ADDRESS - )[0] - return await stakingProgram.account.globalConfig.fetch(configAccountAddress) -} - -async function getStakeAccountDetails(positionAccountAddress: PublicKey) { - const configAccountData = await getConfig(stakingProgram) - - const metadataAccountAddress = getMetadataAccountAddress( - positionAccountAddress - ) - const metadataAccountData = - await stakingProgram.account.stakeAccountMetadataV2.fetch( - metadataAccountAddress - ) - - const lock = metadataAccountData.lock - - const custodyAccountAddress = getCustodyAccountAddress(positionAccountAddress) - const custodyAccountData = await tokenProgram.account.account.fetch( - custodyAccountAddress - ) - - return { - custodyAccount: custodyAccountAddress.toBase58(), - actualAmount: new PythBalance(custodyAccountData.amount).toString(), - lock: getLockSummary(lock, configAccountData.pythTokenListTime), - } -} - -export function getMetadataAccountAddress(positionAccountAddress: PublicKey) { - return PublicKey.findProgramAddressSync( - [Buffer.from('stake_metadata'), positionAccountAddress.toBuffer()], - STAKING_ADDRESS - )[0] -} - -export function getCustodyAccountAddress(positionAccountAddress: PublicKey) { - return PublicKey.findProgramAddressSync( - [Buffer.from('custody'), positionAccountAddress.toBuffer()], - STAKING_ADDRESS - )[0] -} - -async function getStakeAccounts(connection: Connection, owner: PublicKey) { - const response = await connection.getProgramAccounts(STAKING_ADDRESS, { - encoding: 'base64', - filters: [ - { - memcmp: { - offset: 0, - bytes: bs58.encode(Buffer.from('55c3f14f7cc04f0b', 'hex')), // Positions account discriminator - }, - }, - { - memcmp: { - offset: 8, - bytes: owner.toBase58(), - }, - }, - ], - }) - return response.map((account) => { - return account.pubkey - }) -} - -export function getLockSummary(lock: any, listTime: BN | null) { - if (lock.fullyVested) { - return { type: 'fullyUnlocked' } - } else if (lock.periodicVestingAfterListing) { - return { - type: 'periodicUnlockingAfterListing', - schedule: getUnlockEvents( - listTime, - lock.periodicVestingAfterListing.periodDuration, - lock.periodicVestingAfterListing.numPeriods, - lock.periodicVestingAfterListing.initialBalance - ), - } - } else if (lock.periodicVesting) { - return { - type: 'periodicUnlocking', - schedule: getUnlockEvents( - lock.periodicVesting.startDate, - lock.periodicVesting.periodDuration, - lock.periodicVesting.numPeriods, - lock.periodicVesting.initialBalance - ), - } - } -} - -export function getUnlockEvents( - startData: BN | null, - periodDuration: BN, - numberOfPeriods: BN, - initialBalance: BN -) { - if (startData) { - return Array(numberOfPeriods.toNumber()) - .fill(0) - .map((_, i) => { - return { - date: startData.add(periodDuration.muln(i + 1)).toString(), - amount: new PythBalance( - initialBalance.divn(numberOfPeriods.toNumber()) - ).toString(), - } - }) - } - return [] -} diff --git a/package-lock.json b/package-lock.json index 3c9c0dcd..372ee89a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@solana/wallet-adapter-wallets": "^0.19.16", "@solana/web3.js": "^1.87.5", "@tippyjs/react": "^4.2.6", + "axios": "^1.6.7", "dotenv": "^16.0.0", "next": "12.2.5", "prop-types": "^15.8.1", @@ -9765,11 +9766,11 @@ } }, "node_modules/axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -12902,9 +12903,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -31408,11 +31409,11 @@ "from": "avsc@https://github.com/Irys-xyz/avsc#csp-fixes" }, "axios": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", - "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", + "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", "requires": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.4", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } @@ -33862,9 +33863,9 @@ "peer": true }, "follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==" }, "for-each": { "version": "0.3.3", @@ -38727,6 +38728,7 @@ "@types/node": "20.3.1", "@types/react": "^18.0.1", "autoprefixer": "^10.4.0", + "axios": "^1.6.7", "dotenv": "^16.0.0", "next": "12.2.5", "postcss": "^8.4.5", diff --git a/staking/app/api_utils.ts b/staking/app/api_utils.ts new file mode 100644 index 00000000..aab2d6dd --- /dev/null +++ b/staking/app/api_utils.ts @@ -0,0 +1,249 @@ +// This file contains utility functions for the API. Unfortunately we can't use StakeConnection directly because it has wasm imports that are not compatible with the Next API. + +import { Connection, PublicKey } from "@solana/web3.js"; +import { STAKING_ADDRESS, PYTH_TOKEN } from "./constants"; +import BN from "bn.js"; +import { PythBalance } from "./pythBalance"; +import { IdlAccounts, Program } from "@coral-xyz/anchor"; +import { Staking } from "../target/types/staking"; +import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes"; + +const ONE_YEAR = new BN(3600 * 24 * 365); + +// ====================================== +// PDA derivations +// ====================================== + +export function getMetadataAccountAddress(positionAccountAddress: PublicKey) { + return PublicKey.findProgramAddressSync( + [Buffer.from("stake_metadata"), positionAccountAddress.toBuffer()], + STAKING_ADDRESS + )[0]; +} + +export function getCustodyAccountAddress(positionAccountAddress: PublicKey) { + return PublicKey.findProgramAddressSync( + [Buffer.from("custody"), positionAccountAddress.toBuffer()], + STAKING_ADDRESS + )[0]; +} + +// ====================================== +// One-user getters +// ====================================== + +export async function getStakeAccountsByOwner( + connection: Connection, + owner: PublicKey +) { + const response = await connection.getProgramAccounts(STAKING_ADDRESS, { + encoding: "base64", + filters: [ + { + memcmp: { + offset: 0, + bytes: bs58.encode(Buffer.from("55c3f14f7cc04f0b", "hex")), // Positions account discriminator + }, + }, + { + memcmp: { + offset: 8, + bytes: owner.toBase58(), + }, + }, + ], + }); + return response.map((account) => { + return account.pubkey; + }); +} + +export async function getStakeAccountDetails( + stakingProgram: Program, + tokenProgram: any, + positionAccountAddress: PublicKey +) { + const configAccountData = await getConfig(stakingProgram); + + const metadataAccountAddress = getMetadataAccountAddress( + positionAccountAddress + ); + const metadataAccountData = + await stakingProgram.account.stakeAccountMetadataV2.fetch( + metadataAccountAddress + ); + + const lock = metadataAccountData.lock; + + const custodyAccountAddress = getCustodyAccountAddress( + positionAccountAddress + ); + const custodyAccountData = await tokenProgram.account.account.fetch( + custodyAccountAddress + ); + + return { + custodyAccount: custodyAccountAddress.toBase58(), + actualAmount: new PythBalance(custodyAccountData.amount).toString(), + lock: getLockSummary(lock, configAccountData.pythTokenListTime), + }; +} + +// ====================================== +// Global getters +// ====================================== + +export async function getConfig( + stakingProgram: Program +): Promise["globalConfig"]> { + const configAccountAddress = PublicKey.findProgramAddressSync( + [Buffer.from("config")], + STAKING_ADDRESS + )[0]; + return await stakingProgram.account.globalConfig.fetch(configAccountAddress); +} + +export async function getTotalSupply(tokenProgram: any): Promise { + const pythTokenMintData = await tokenProgram.account.mint.fetch(PYTH_TOKEN); + return new PythBalance(pythTokenMintData.supply); +} + +export async function getAllMetadataAccounts( + stakingProgram: Program, + stakeAccounts: PublicKey[] +): Promise<(IdlAccounts["stakeAccountMetadataV2"] | null)[]> { + const metadataAccountAddresses = stakeAccounts.map((account) => + getMetadataAccountAddress(account) + ); + return stakingProgram.account.stakeAccountMetadataV2.fetchMultiple( + metadataAccountAddresses + ); +} + +export async function getAllCustodyAccounts( + tokenProgram: any, + stakeAccounts: PublicKey[] +) { + const allCustodyAccountAddresses = stakeAccounts.map((account) => + getCustodyAccountAddress(account) + ); + return tokenProgram.account.account.fetchMultiple(allCustodyAccountAddresses); +} + +// ====================================== +// Locked accounts +// ====================================== + +export function hasStandardLockup( + metadataAccountData: IdlAccounts["stakeAccountMetadataV2"] +) { + return ( + metadataAccountData.lock.periodicVestingAfterListing && + metadataAccountData.lock.periodicVestingAfterListing.numPeriods.eq( + new BN(4) + ) && + metadataAccountData.lock.periodicVestingAfterListing.periodDuration.eq( + ONE_YEAR + ) + ); +} + +export function getCurrentlyLockedAmount( + metadataAccountData: IdlAccounts["stakeAccountMetadataV2"], + configAccountData: IdlAccounts["globalConfig"] +): PythBalance { + const lock = metadataAccountData.lock; + const listTime = configAccountData.pythTokenListTime; + if (lock.fullyVested) { + return PythBalance.zero(); + } else if (lock.periodicVestingAfterListing) { + if (!listTime) { + return new PythBalance(lock.periodicVestingAfterListing.initialBalance); + } else { + return getCurrentlyLockedAmountPeriodic( + listTime, + lock.periodicVestingAfterListing.periodDuration, + lock.periodicVestingAfterListing.numPeriods, + lock.periodicVestingAfterListing.initialBalance + ); + } + } else if (lock.periodicVesting) { + return getCurrentlyLockedAmountPeriodic( + lock.periodicVesting.startDate, + lock.periodicVesting.periodDuration, + lock.periodicVesting.numPeriods, + lock.periodicVesting.initialBalance + ); + } else { + throw new Error("Should be unreachable"); + } +} + +function getCurrentlyLockedAmountPeriodic( + startDate: BN, + periodDuration: BN, + numPeriods: BN, + initialBalance: BN +): PythBalance { + const currentTimestamp = new BN(Math.floor(Date.now() / 1000)); + if (currentTimestamp.lte(startDate)) { + return new PythBalance(initialBalance); + } else { + const periodsElapsed = currentTimestamp.sub(startDate).div(periodDuration); + if (periodsElapsed.gte(numPeriods)) { + return PythBalance.zero(); + } else { + const remainingPeriods = numPeriods.sub(periodsElapsed); + return new PythBalance( + remainingPeriods.mul(initialBalance).div(numPeriods) + ); + } + } +} + +export function getLockSummary(lock: any, listTime: BN | null) { + if (lock.fullyVested) { + return { type: "fullyUnlocked" }; + } else if (lock.periodicVestingAfterListing) { + return { + type: "periodicUnlockingAfterListing", + schedule: getUnlockEvents( + listTime, + lock.periodicVestingAfterListing.periodDuration, + lock.periodicVestingAfterListing.numPeriods, + lock.periodicVestingAfterListing.initialBalance + ), + }; + } else if (lock.periodicVesting) { + return { + type: "periodicUnlocking", + schedule: getUnlockEvents( + lock.periodicVesting.startDate, + lock.periodicVesting.periodDuration, + lock.periodicVesting.numPeriods, + lock.periodicVesting.initialBalance + ), + }; + } +} + +function getUnlockEvents( + startData: BN | null, + periodDuration: BN, + numberOfPeriods: BN, + initialBalance: BN +) { + if (startData) { + return Array(numberOfPeriods.toNumber()) + .fill(0) + .map((_, i) => { + return { + date: startData.add(periodDuration.muln(i + 1)).toString(), + amount: new PythBalance( + initialBalance.divn(numberOfPeriods.toNumber()) + ).toString(), + }; + }); + } + return []; +} diff --git a/staking/app/constants.ts b/staking/app/constants.ts index 1a4eb65e..21f9308c 100644 --- a/staking/app/constants.ts +++ b/staking/app/constants.ts @@ -20,4 +20,9 @@ export const REALM_ID = new PublicKey( "4ct8XU5tKbMNRphWy4rePsS9kBqPhDdvZoGpmprPaug4" ); +// This one is valid on mainnet only +export const PYTH_TOKEN = new PublicKey( + "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3" +); + export const EPOCH_DURATION = 3600 * 24 * 7; diff --git a/staking/app/index.ts b/staking/app/index.ts index 674cb673..40539fb4 100644 --- a/staking/app/index.ts +++ b/staking/app/index.ts @@ -6,4 +6,5 @@ export { VestingAccountState, } from "./StakeConnection"; export * from "./constants"; +export * from "./api_utils"; export { PYTH_DECIMALS, PythBalance } from "./pythBalance"; diff --git a/vercel.json b/vercel.json index 5ac78045..bc978dc8 100644 --- a/vercel.json +++ b/vercel.json @@ -5,9 +5,6 @@ }, "pages/api/v1/cmc/supply.ts": { "maxDuration": 30 - }, - "pages/api/internal/create_ephemeral_account.ts": { - "maxDuration": 30 } } }