Skip to content

Commit

Permalink
[chore] Add CMC endpoint (#307)
Browse files Browse the repository at this point in the history
* Add supply

* Fix commas

* Go go go

* Fix bug

* Cleanup

* Cleanup

* Fix

* Cleanup

* Remove this sifle for now

* Address comment
  • Loading branch information
guibescos authored Dec 18, 2023
1 parent c674b26 commit 1b0272b
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 16 deletions.
37 changes: 28 additions & 9 deletions frontend/pages/api/v1/all_locked_accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 } from '@solana/web3.js'
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 NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet'
Expand Down Expand Up @@ -39,17 +39,14 @@ export default async function handlerAllLockedAccounts(
) {
const allStakeAccounts = await getAllStakeAccounts(connection)

const allMetadataAccountAddresses = allStakeAccounts.map((account) =>
getMetadataAccountAddress(account)
const allMetadataAccounts = await getAllMetadataAccounts(
stakingProgram,
allStakeAccounts
)

const allCustodyAccountAddresses = allStakeAccounts.map((account) =>
getCustodyAccountAddress(account)
)

const allMetadataAccounts =
await stakingProgram.account.stakeAccountMetadataV2.fetchMultiple(
allMetadataAccountAddresses
)
const allCustodyAccounts = await tokenProgram.account.account.fetchMultiple(
allCustodyAccountAddresses
)
Expand Down Expand Up @@ -103,7 +100,7 @@ function hasStandardLockup(
)
)
}
async function getAllStakeAccounts(connection: Connection) {
export async function getAllStakeAccounts(connection: Connection) {
const response = await connection.getProgramAccounts(STAKING_ADDRESS, {
encoding: 'base64',
filters: [
Expand All @@ -119,3 +116,25 @@ async function getAllStakeAccounts(connection: Connection) {
return account.pubkey
})
}

export async function getAllMetadataAccounts(
stakingProgram: Program<Staking>,
stakeAccounts: PublicKey[]
): Promise<(IdlAccounts<Staking>['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)
}
147 changes: 147 additions & 0 deletions frontend/pages/api/v1/cmc/supply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
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 { PYTH_TOKEN } from '@pythnetwork/staking/app/deploy/mainnet_beta'
import { Connection, Keypair } from '@solana/web3.js'
import { Program, AnchorProvider, IdlAccounts } 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,
getAllMetadataAccounts,
getAllStakeAccounts,
} from '../all_locked_accounts'

const connection = new Connection(process.env.BACKEND_ENDPOINT!)
const provider = new AnchorProvider(
connection,
new NodeWallet(new Keypair()),
{}
)
const stakingProgram = new Program<Staking>(
idl as Staking,
STAKING_ADDRESS,
provider
)
const tokenProgram = splTokenProgram({
programId: TOKEN_PROGRAM_ID,
provider: provider as any,
})

/**
* This API imitates the one implemented by https://avascan.info/api/v1/supply
* It is used by Coinmarketcap to display the right circulating and total supply of PYTH
*/
export default async function handlerSupply(
req: NextApiRequest,
res: NextApiResponse
) {
const { q } = req.query

if (q === 'totalSupply') {
res.setHeader('Cache-Control', 'max-age=0, s-maxage=3600')
res.status(200).send((await getTotalSupply(tokenProgram)).toString(false))
} else if (q === 'circulatingSupply') {
const configAccountData = await getConfig(stakingProgram)
const allStakeAccounts = await getAllStakeAccounts(connection)

const allMetadataAccounts = await getAllMetadataAccounts(
stakingProgram,
allStakeAccounts
)
const allCustodyAccounts = await getAllCustodyAccounts(
tokenProgram,
allStakeAccounts
)

const totalLockedAmount = allMetadataAccounts.reduce(
(total: PythBalance, account: any, index: number) => {
return total.add(
allCustodyAccounts[index].amount && account.lock
? new PythBalance(allCustodyAccounts[index].amount).min(
getCurrentlyLockedAmount(account, configAccountData)
)
: PythBalance.zero()
)
},
PythBalance.zero()
)

res.setHeader('Cache-Control', 'max-age=0, s-maxage=3600')
res
.status(200)
.send(
(await getTotalSupply(tokenProgram))
.sub(totalLockedAmount)
.toString(false)
)
} else {
res.status(400).send({
error:
"The 'q' query parameter must be one of 'totalSupply' or 'circulatingSupply'.",
})
}
}

function getCurrentlyLockedAmount(
metadataAccountData: IdlAccounts<Staking>['stakeAccountMetadataV2'],
configAccountData: IdlAccounts<Staking>['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<PythBalance> {
const pythTokenMintData = await tokenProgram.account.mint.fetch(PYTH_TOKEN)
return new PythBalance(pythTokenMintData.supply)
}
14 changes: 9 additions & 5 deletions frontend/pages/api/v1/locked_accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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 } from '@coral-xyz/anchor'
import { Program, AnchorProvider, IdlAccounts } 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'
Expand Down Expand Up @@ -51,14 +51,18 @@ export default async function handlerLockedAccounts(
}
}

async function getStakeAccountDetails(positionAccountAddress: PublicKey) {
export async function getConfig(
stakingProgram: Program<Staking>
): Promise<IdlAccounts<Staking>['globalConfig']> {
const configAccountAddress = PublicKey.findProgramAddressSync(
[Buffer.from('config')],
STAKING_ADDRESS
)[0]
const configAccountData = await stakingProgram.account.globalConfig.fetch(
configAccountAddress
)
return await stakingProgram.account.globalConfig.fetch(configAccountAddress)
}

async function getStakeAccountDetails(positionAccountAddress: PublicKey) {
const configAccountData = await getConfig(stakingProgram)

const metadataAccountAddress = getMetadataAccountAddress(
positionAccountAddress
Expand Down
13 changes: 11 additions & 2 deletions staking/app/pythBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ export class PythBalance {
}
}

toString(): string {
toString(commas = true): string {
const padded = this.toBN()
.toString()
.padStart(PYTH_DECIMALS + 1, "0");

const integerPart = padded.slice(0, padded.length - PYTH_DECIMALS);
return (
addCommas(padded.slice(0, padded.length - PYTH_DECIMALS)) +
(commas ? addCommas(integerPart) : integerPart) +
("." + padded.slice(padded.length - PYTH_DECIMALS)).replace(
TRAILING_ZEROS,
""
Expand Down Expand Up @@ -91,9 +92,17 @@ export class PythBalance {
return new PythBalance(other.toBN().add(this.toBN()));
}

sub(other: PythBalance): PythBalance {
return new PythBalance(this.toBN().sub(other.toBN()));
}

isZero(): boolean {
return this.eq(PythBalance.zero());
}

min(other: PythBalance): PythBalance {
return this.lt(other) ? this : other;
}
}

const addCommas = (x: string) => {
Expand Down
3 changes: 3 additions & 0 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
"functions": {
"pages/api/v1/all_locked_accounts.ts": {
"maxDuration": 30
},
"pages/api/v1/cmc/supply.ts": {
"maxDuration": 30
}
}
}

2 comments on commit 1b0272b

@vercel
Copy link

@vercel vercel bot commented on 1b0272b Dec 18, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

staking-devnet – ./

staking-devnet-git-main-pyth-web.vercel.app
staking-devnet-pyth-web.vercel.app
governance-nu.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 1b0272b Dec 18, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.