From aa9b8fff7913d6b71284092edc5e52cb789292c7 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 19:04:39 +0000 Subject: [PATCH 01/24] Test --- frontend/pages/api/v1/locked_accounts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/pages/api/v1/locked_accounts.ts b/frontend/pages/api/v1/locked_accounts.ts index 1e908136..ab3ceb0b 100644 --- a/frontend/pages/api/v1/locked_accounts.ts +++ b/frontend/pages/api/v1/locked_accounts.ts @@ -10,6 +10,7 @@ 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 { StakeConnection } from '@pythnetwork/staking' const connection = new Connection(process.env.BACKEND_ENDPOINT!) const provider = new AnchorProvider( From 8c1578fbc93dfc0c433fe466e639595ee56fc63a Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 19:12:44 +0000 Subject: [PATCH 02/24] Go --- frontend/pages/api/v1/locked_accounts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/pages/api/v1/locked_accounts.ts b/frontend/pages/api/v1/locked_accounts.ts index ab3ceb0b..1cef839e 100644 --- a/frontend/pages/api/v1/locked_accounts.ts +++ b/frontend/pages/api/v1/locked_accounts.ts @@ -43,6 +43,8 @@ export default async function handlerLockedAccounts( connection, new PublicKey(owner) ) + await StakeConnection.connect(connection, new NodeWallet(new Keypair())) + const stakeAccountDetails = await Promise.all( stakeAccounts.map((account) => { return getStakeAccountDetails(account) From f05e34966fd826245958f03c93808c918e083330 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 19:33:22 +0000 Subject: [PATCH 03/24] Try another one --- frontend/next.config.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/next.config.js b/frontend/next.config.js index 64ade807..36331613 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -15,12 +15,12 @@ module.exports = { webpack(config, { isServer }) { config.experiments = { asyncWebAssembly: true, layers: true } // This is hack to fix the import of the wasm files https://github.com/vercel/next.js/issues/25852 - if (isServer) { - config.output.webassemblyModuleFilename = - './../static/wasm/[modulehash].wasm' - } else { - config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' - } + // if (isServer) { + // config.output.webassemblyModuleFilename = + // './../static/wasm/[modulehash].wasm' + // } else { + config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' + // } config.optimization.moduleIds = 'named' // End of hack From 8d1ea5955fac6f8480505eab80018a520f2d83d6 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 19:35:56 +0000 Subject: [PATCH 04/24] Try this --- frontend/next.config.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/next.config.js b/frontend/next.config.js index 36331613..64ade807 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -15,12 +15,12 @@ module.exports = { webpack(config, { isServer }) { config.experiments = { asyncWebAssembly: true, layers: true } // This is hack to fix the import of the wasm files https://github.com/vercel/next.js/issues/25852 - // if (isServer) { - // config.output.webassemblyModuleFilename = - // './../static/wasm/[modulehash].wasm' - // } else { - config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' - // } + if (isServer) { + config.output.webassemblyModuleFilename = + './../static/wasm/[modulehash].wasm' + } else { + config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' + } config.optimization.moduleIds = 'named' // End of hack From 5479659a6e012050d173ce76a8f098c0b996e1c3 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 19:45:35 +0000 Subject: [PATCH 05/24] Clean it --- frontend/next.config.js | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/frontend/next.config.js b/frontend/next.config.js index 64ade807..2178bac9 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -12,14 +12,12 @@ module.exports = { ENDPOINT: process.env.ENDPOINT, CLUSTER: process.env.CLUSTER, }, - webpack(config, { isServer }) { + webpack(config, { isServer, dev }) { config.experiments = { asyncWebAssembly: true, layers: true } // This is hack to fix the import of the wasm files https://github.com/vercel/next.js/issues/25852 - if (isServer) { - config.output.webassemblyModuleFilename = - './../static/wasm/[modulehash].wasm' - } else { - config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' + if (!dev && isServer) { + config.output.webassemblyModuleFilename = 'chunks/[id].wasm' + config.plugins.push(new WasmChunksFixPlugin()) } config.optimization.moduleIds = 'named' // End of hack @@ -32,3 +30,22 @@ module.exports = { return config }, } + +class WasmChunksFixPlugin { + apply(compiler) { + compiler.hooks.thisCompilation.tap('WasmChunksFixPlugin', (compilation) => { + compilation.hooks.processAssets.tap( + { name: 'WasmChunksFixPlugin' }, + (assets) => + Object.entries(assets).forEach(([pathname, source]) => { + if (!pathname.match(/\.wasm$/)) return + compilation.deleteAsset(pathname) + + const name = pathname.split('/')[1] + const info = compilation.assetsInfo.get(pathname) + compilation.emitAsset(name, source, info) + }) + ) + }) + } +} From 3212cdbc3c90d3f812c0e9f1c5151eced1b1bdb3 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 20:02:07 +0000 Subject: [PATCH 06/24] try again --- frontend/next.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/next.config.js b/frontend/next.config.js index 2178bac9..90681687 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -19,7 +19,6 @@ module.exports = { config.output.webassemblyModuleFilename = 'chunks/[id].wasm' config.plugins.push(new WasmChunksFixPlugin()) } - config.optimization.moduleIds = 'named' // End of hack // Import the browser version of wasm instead of the node version From 0f967a758dcef8c8a0486be2c4465979878bb59f Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 20:15:17 +0000 Subject: [PATCH 07/24] Test --- frontend/pages/api/v1/locked_accounts.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/pages/api/v1/locked_accounts.ts b/frontend/pages/api/v1/locked_accounts.ts index 1cef839e..e200ed3f 100644 --- a/frontend/pages/api/v1/locked_accounts.ts +++ b/frontend/pages/api/v1/locked_accounts.ts @@ -12,6 +12,9 @@ import { splTokenProgram } from '@coral-xyz/spl-token' import { TOKEN_PROGRAM_ID } from '@solana/spl-token' import { StakeConnection } from '@pythnetwork/staking' +export const runtime = 'edge' // 'nodejs' is the default +export const dynamic = 'force-dynamic' // static by default, unless reading the request + const connection = new Connection(process.env.BACKEND_ENDPOINT!) const provider = new AnchorProvider( connection, From b6076fedcbd185b8e30f6622bad25edc37d7b173 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 20:24:00 +0000 Subject: [PATCH 08/24] EDge test --- frontend/next.config.js | 30 ++++++------------------ frontend/pages/api/v1/locked_accounts.ts | 3 +++ 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/frontend/next.config.js b/frontend/next.config.js index 90681687..64ade807 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -12,13 +12,16 @@ module.exports = { ENDPOINT: process.env.ENDPOINT, CLUSTER: process.env.CLUSTER, }, - webpack(config, { isServer, dev }) { + webpack(config, { isServer }) { config.experiments = { asyncWebAssembly: true, layers: true } // This is hack to fix the import of the wasm files https://github.com/vercel/next.js/issues/25852 - if (!dev && isServer) { - config.output.webassemblyModuleFilename = 'chunks/[id].wasm' - config.plugins.push(new WasmChunksFixPlugin()) + if (isServer) { + config.output.webassemblyModuleFilename = + './../static/wasm/[modulehash].wasm' + } else { + config.output.webassemblyModuleFilename = 'static/wasm/[modulehash].wasm' } + config.optimization.moduleIds = 'named' // End of hack // Import the browser version of wasm instead of the node version @@ -29,22 +32,3 @@ module.exports = { return config }, } - -class WasmChunksFixPlugin { - apply(compiler) { - compiler.hooks.thisCompilation.tap('WasmChunksFixPlugin', (compilation) => { - compilation.hooks.processAssets.tap( - { name: 'WasmChunksFixPlugin' }, - (assets) => - Object.entries(assets).forEach(([pathname, source]) => { - if (!pathname.match(/\.wasm$/)) return - compilation.deleteAsset(pathname) - - const name = pathname.split('/')[1] - const info = compilation.assetsInfo.get(pathname) - compilation.emitAsset(name, source, info) - }) - ) - }) - } -} diff --git a/frontend/pages/api/v1/locked_accounts.ts b/frontend/pages/api/v1/locked_accounts.ts index e200ed3f..7c2f8b30 100644 --- a/frontend/pages/api/v1/locked_accounts.ts +++ b/frontend/pages/api/v1/locked_accounts.ts @@ -14,6 +14,9 @@ import { StakeConnection } from '@pythnetwork/staking' export const runtime = 'edge' // 'nodejs' is the default export const dynamic = 'force-dynamic' // static by default, unless reading the request +export const config = { + runtime: 'experimental-edge', +} const connection = new Connection(process.env.BACKEND_ENDPOINT!) const provider = new AnchorProvider( From 42e80ef53caeb985e09670c83196f8024a0fe234 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 21:18:36 +0000 Subject: [PATCH 09/24] Another one --- frontend/pages/api/v1/locked_accounts.ts | 26 ++++-------------------- staking/app/index.ts | 1 + 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/frontend/pages/api/v1/locked_accounts.ts b/frontend/pages/api/v1/locked_accounts.ts index 7c2f8b30..4c803e1b 100644 --- a/frontend/pages/api/v1/locked_accounts.ts +++ b/frontend/pages/api/v1/locked_accounts.ts @@ -10,13 +10,10 @@ 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 { StakeConnection } from '@pythnetwork/staking' - -export const runtime = 'edge' // 'nodejs' is the default -export const dynamic = 'force-dynamic' // static by default, unless reading the request -export const config = { - runtime: 'experimental-edge', -} +import { + getCustodyAccountAddress, + getMetadataAccountAddress, +} from '@pythnetwork/staking' const connection = new Connection(process.env.BACKEND_ENDPOINT!) const provider = new AnchorProvider( @@ -49,7 +46,6 @@ export default async function handlerLockedAccounts( connection, new PublicKey(owner) ) - await StakeConnection.connect(connection, new NodeWallet(new Keypair())) const stakeAccountDetails = await Promise.all( stakeAccounts.map((account) => { @@ -95,20 +91,6 @@ async function getStakeAccountDetails(positionAccountAddress: PublicKey) { } } -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', 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"; From 3d7410e36c585a40aa91face98e48059dba89e64 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 21:21:04 +0000 Subject: [PATCH 10/24] Another one --- frontend/pages/api/v1/all_locked_accounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/pages/api/v1/all_locked_accounts.ts b/frontend/pages/api/v1/all_locked_accounts.ts index 39882d15..2976bd66 100644 --- a/frontend/pages/api/v1/all_locked_accounts.ts +++ b/frontend/pages/api/v1/all_locked_accounts.ts @@ -13,7 +13,7 @@ import { TOKEN_PROGRAM_ID } from '@solana/spl-token' import { getCustodyAccountAddress, getMetadataAccountAddress, -} from './locked_accounts' +} from '@pythnetwork/staking' const ONE_YEAR = new BN(3600 * 24 * 365) From 21f503b42c71b98b9348da68de5b888b38751ecb Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 21:24:13 +0000 Subject: [PATCH 11/24] Go --- staking/app/api_utils.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 staking/app/api_utils.ts diff --git a/staking/app/api_utils.ts b/staking/app/api_utils.ts new file mode 100644 index 00000000..588ea7ad --- /dev/null +++ b/staking/app/api_utils.ts @@ -0,0 +1,18 @@ +// This file contains utility functions for the API. We can't use StakeConnection directly because it has wasm imports that are not compatible with the Next API. + +import { PublicKey } from "@solana/web3.js"; +import { STAKING_ADDRESS } from "./constants"; + +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]; +} From 79fdb9a90f1d1b8de79a41cd18c4ca4df759f3a9 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 21:39:16 +0000 Subject: [PATCH 12/24] Update import --- frontend/pages/api/v1/all_locked_accounts.ts | 9 +++++---- frontend/pages/api/v1/locked_accounts.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/pages/api/v1/all_locked_accounts.ts b/frontend/pages/api/v1/all_locked_accounts.ts index 2976bd66..73375656 100644 --- a/frontend/pages/api/v1/all_locked_accounts.ts +++ b/frontend/pages/api/v1/all_locked_accounts.ts @@ -2,6 +2,11 @@ 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 { + getCustodyAccountAddress, + getMetadataAccountAddress, +} from '@pythnetwork/staking/app/api_utils' + 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' @@ -10,10 +15,6 @@ 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 '@pythnetwork/staking' const ONE_YEAR = new BN(3600 * 24 * 365) diff --git a/frontend/pages/api/v1/locked_accounts.ts b/frontend/pages/api/v1/locked_accounts.ts index 4c803e1b..7d9b462c 100644 --- a/frontend/pages/api/v1/locked_accounts.ts +++ b/frontend/pages/api/v1/locked_accounts.ts @@ -13,7 +13,7 @@ import { TOKEN_PROGRAM_ID } from '@solana/spl-token' import { getCustodyAccountAddress, getMetadataAccountAddress, -} from '@pythnetwork/staking' +} from '@pythnetwork/staking/app/api_utils' const connection = new Connection(process.env.BACKEND_ENDPOINT!) const provider = new AnchorProvider( From 28b04dfc216c9f59d89813a8fb9e6b04a02a6091 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 22:03:55 +0000 Subject: [PATCH 13/24] Continue --- frontend/pages/api/v1/all_locked_accounts.ts | 64 +---- frontend/pages/api/v1/cmc/supply.ts | 70 +----- frontend/pages/api/v1/locked_accounts.ts | 116 +-------- staking/app/api_utils.ts | 240 ++++++++++++++++++- staking/app/constants.ts | 5 + 5 files changed, 258 insertions(+), 237 deletions(-) diff --git a/frontend/pages/api/v1/all_locked_accounts.ts b/frontend/pages/api/v1/all_locked_accounts.ts index 73375656..3c10579d 100644 --- a/frontend/pages/api/v1/all_locked_accounts.ts +++ b/frontend/pages/api/v1/all_locked_accounts.ts @@ -3,21 +3,19 @@ import { PythBalance } from '@pythnetwork/staking/app/pythBalance' import BN from 'bn.js' import { STAKING_ADDRESS } from '@pythnetwork/staking/app/constants' import { + getAllMetadataAccounts, + getAllStakeAccounts, getCustodyAccountAddress, - getMetadataAccountAddress, + hasStandardLockup, } from '@pythnetwork/staking/app/api_utils' - -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 { 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' -const ONE_YEAR = new BN(3600 * 24 * 365) - const connection = new Connection(process.env.BACKEND_ENDPOINT!) const provider = new AnchorProvider( connection, @@ -87,55 +85,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..10a3a76e 100644 --- a/frontend/pages/api/v1/cmc/supply.ts +++ b/frontend/pages/api/v1/cmc/supply.ts @@ -2,21 +2,21 @@ 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 { + getCurrentlyLockedAmount, + getTotalSupply, getAllCustodyAccounts, getAllMetadataAccounts, + getConfig, getAllStakeAccounts, -} from '../all_locked_accounts' - -const PYTH_TOKEN = new PublicKey('HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3') +} from '@pythnetwork/staking/app/api_utils' const connection = new Connection(process.env.BACKEND_ENDPOINT!) const provider = new AnchorProvider( @@ -88,61 +88,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 7d9b462c..67ed0fcf 100644 --- a/frontend/pages/api/v1/locked_accounts.ts +++ b/frontend/pages/api/v1/locked_accounts.ts @@ -1,18 +1,15 @@ 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 { - getCustodyAccountAddress, - getMetadataAccountAddress, + getStakeAccountDetails, + getStakeAccounts, } from '@pythnetwork/staking/app/api_utils' const connection = new Connection(process.env.BACKEND_ENDPOINT!) @@ -49,114 +46,9 @@ export default async function handlerLockedAccounts( 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), - } -} - -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/staking/app/api_utils.ts b/staking/app/api_utils.ts index 588ea7ad..3a2b588e 100644 --- a/staking/app/api_utils.ts +++ b/staking/app/api_utils.ts @@ -1,7 +1,14 @@ // This file contains utility functions for the API. We can't use StakeConnection directly because it has wasm imports that are not compatible with the Next API. -import { PublicKey } from "@solana/web3.js"; -import { STAKING_ADDRESS } from "./constants"; +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); export function getMetadataAccountAddress(positionAccountAddress: PublicKey) { return PublicKey.findProgramAddressSync( @@ -16,3 +23,232 @@ export function getCustodyAccountAddress(positionAccountAddress: PublicKey) { STAKING_ADDRESS )[0]; } + +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 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); +} + +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 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), + }; +} + +export 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; + }); +} + +// ====================================== +// Locked accounts +// ====================================== + +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; From 989b87d8c5ddc461bd16d66fedff784036f3cc3c Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 22:06:43 +0000 Subject: [PATCH 14/24] Finish refactor --- frontend/pages/api/v1/locked_accounts.ts | 4 +- staking/app/api_utils.ts | 134 ++++++++++++----------- 2 files changed, 75 insertions(+), 63 deletions(-) diff --git a/frontend/pages/api/v1/locked_accounts.ts b/frontend/pages/api/v1/locked_accounts.ts index 67ed0fcf..f114a892 100644 --- a/frontend/pages/api/v1/locked_accounts.ts +++ b/frontend/pages/api/v1/locked_accounts.ts @@ -9,7 +9,7 @@ import { splTokenProgram } from '@coral-xyz/spl-token' import { TOKEN_PROGRAM_ID } from '@solana/spl-token' import { getStakeAccountDetails, - getStakeAccounts, + getStakeAccountsByOwner, } from '@pythnetwork/staking/app/api_utils' const connection = new Connection(process.env.BACKEND_ENDPOINT!) @@ -39,7 +39,7 @@ 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) ) diff --git a/staking/app/api_utils.ts b/staking/app/api_utils.ts index 3a2b588e..a754c68c 100644 --- a/staking/app/api_utils.ts +++ b/staking/app/api_utils.ts @@ -10,6 +10,10 @@ 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()], @@ -24,22 +28,14 @@ export function getCustodyAccountAddress(positionAccountAddress: PublicKey) { )[0]; } -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); -} +// ====================================== +// One-user getters +// ====================================== -export async function getAllStakeAccounts(connection: Connection) { +export async function getStakeAccountsByOwner( + connection: Connection, + owner: PublicKey +) { const response = await connection.getProgramAccounts(STAKING_ADDRESS, { encoding: "base64", filters: [ @@ -49,6 +45,12 @@ export async function getAllStakeAccounts(connection: Connection) { bytes: bs58.encode(Buffer.from("55c3f14f7cc04f0b", "hex")), // Positions account discriminator }, }, + { + memcmp: { + offset: 8, + bytes: owner.toBase58(), + }, + }, ], }); return response.map((account) => { @@ -56,42 +58,6 @@ export async function getAllStakeAccounts(connection: Connection) { }); } -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); -} - -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 async function getStakeAccountDetails( stakingProgram: Program, tokenProgram: any, @@ -123,10 +89,26 @@ export async function getStakeAccountDetails( }; } -export async function getStakeAccounts( - connection: Connection, - owner: PublicKey -) { +// ====================================== +// 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 getAllStakeAccounts(connection: Connection) { const response = await connection.getProgramAccounts(STAKING_ADDRESS, { encoding: "base64", filters: [ @@ -136,12 +118,6 @@ export async function getStakeAccounts( bytes: bs58.encode(Buffer.from("55c3f14f7cc04f0b", "hex")), // Positions account discriminator }, }, - { - memcmp: { - offset: 8, - bytes: owner.toBase58(), - }, - }, ], }); return response.map((account) => { @@ -149,10 +125,46 @@ export async function getStakeAccounts( }); } +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"] From 13c45918694a96e9cff4326c3cef67b84764f8be Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 22:07:10 +0000 Subject: [PATCH 15/24] Refactor complete --- staking/app/api_utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking/app/api_utils.ts b/staking/app/api_utils.ts index a754c68c..05ab3a7b 100644 --- a/staking/app/api_utils.ts +++ b/staking/app/api_utils.ts @@ -1,4 +1,4 @@ -// This file contains utility functions for the API. We can't use StakeConnection directly because it has wasm imports that are not compatible with the Next API. +// 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"; From 8902782e4a122b5aa97f2942186022bc3fd480e2 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 22:08:25 +0000 Subject: [PATCH 16/24] Increase max duration --- vercel.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/vercel.json b/vercel.json index 5ac78045..9a8f3c00 100644 --- a/vercel.json +++ b/vercel.json @@ -1,13 +1,10 @@ { "functions": { "pages/api/v1/all_locked_accounts.ts": { - "maxDuration": 30 + "maxDuration": 60 }, "pages/api/v1/cmc/supply.ts": { - "maxDuration": 30 - }, - "pages/api/internal/create_ephemeral_account.ts": { - "maxDuration": 30 + "maxDuration": 60 } } } From 1e063deeba5f7a44c813e1c411db021748ebd649 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 23:46:02 +0000 Subject: [PATCH 17/24] CMC works --- frontend/package.json | 1 + frontend/pages/api/getAllStakingAccounts.ts | 32 ++++++++++++++++++++ frontend/pages/api/v1/all_locked_accounts.ts | 7 +++-- frontend/pages/api/v1/cmc/supply.ts | 24 +++++++++------ package-lock.json | 30 +++++++++--------- staking/app/api_utils.ts | 17 ----------- 6 files changed, 68 insertions(+), 43 deletions(-) create mode 100644 frontend/pages/api/getAllStakingAccounts.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 3c10579d..59afd37f 100644 --- a/frontend/pages/api/v1/all_locked_accounts.ts +++ b/frontend/pages/api/v1/all_locked_accounts.ts @@ -4,7 +4,6 @@ import BN from 'bn.js' import { STAKING_ADDRESS } from '@pythnetwork/staking/app/constants' import { getAllMetadataAccounts, - getAllStakeAccounts, getCustodyAccountAddress, hasStandardLockup, } from '@pythnetwork/staking/app/api_utils' @@ -15,8 +14,10 @@ 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 { 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()), @@ -36,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, diff --git a/frontend/pages/api/v1/cmc/supply.ts b/frontend/pages/api/v1/cmc/supply.ts index 10a3a76e..cfb762f4 100644 --- a/frontend/pages/api/v1/cmc/supply.ts +++ b/frontend/pages/api/v1/cmc/supply.ts @@ -15,10 +15,12 @@ import { getAllCustodyAccounts, getAllMetadataAccounts, getConfig, - getAllStakeAccounts, + getCustodyAccountAddress, } 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()), @@ -48,23 +50,27 @@ export default async function handlerSupply( 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 allStakeAccounts = await getAllStakeAccounts(RPC_URL) const allMetadataAccounts = await getAllMetadataAccounts( stakingProgram, allStakeAccounts ) - const allCustodyAccounts = await getAllCustodyAccounts( - tokenProgram, - allStakeAccounts + + const allCustodyAccountAddresses = allStakeAccounts.map((account) => + getCustodyAccountAddress(account) ) + const allCustodyAccounts = await tokenProgram.account.account.fetchMultiple( + allCustodyAccountAddresses + ) + + const configAccountData = await getConfig(stakingProgram) const totalLockedAmount = allMetadataAccounts.reduce( (total: PythBalance, account: any, index: number) => { return total.add( - allCustodyAccounts[index].amount && account.lock - ? new PythBalance(allCustodyAccounts[index].amount).min( + allCustodyAccounts[index]?.amount && account.lock + ? new PythBalance(allCustodyAccounts[index]!.amount).min( getCurrentlyLockedAmount(account, configAccountData) ) : PythBalance.zero() 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 index 05ab3a7b..aab2d6dd 100644 --- a/staking/app/api_utils.ts +++ b/staking/app/api_utils.ts @@ -108,23 +108,6 @@ export async function getTotalSupply(tokenProgram: any): Promise { return new PythBalance(pythTokenMintData.supply); } -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[] From a98ba9f04be52fbb0f0d4872506beff94ddd2cc1 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 23:46:21 +0000 Subject: [PATCH 18/24] CMC works --- frontend/pages/api/v1/cmc/supply.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/pages/api/v1/cmc/supply.ts b/frontend/pages/api/v1/cmc/supply.ts index cfb762f4..e3597085 100644 --- a/frontend/pages/api/v1/cmc/supply.ts +++ b/frontend/pages/api/v1/cmc/supply.ts @@ -12,7 +12,6 @@ import { TOKEN_PROGRAM_ID } from '@solana/spl-token' import { getCurrentlyLockedAmount, getTotalSupply, - getAllCustodyAccounts, getAllMetadataAccounts, getConfig, getCustodyAccountAddress, From f1f186cc2f5f04ecf04bb6eb6e213fe028418ce6 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Thu, 25 Jan 2024 23:49:06 +0000 Subject: [PATCH 19/24] Cleanup --- staking/app/api_utils.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/staking/app/api_utils.ts b/staking/app/api_utils.ts index aab2d6dd..91c4dc1e 100644 --- a/staking/app/api_utils.ts +++ b/staking/app/api_utils.ts @@ -120,16 +120,6 @@ export async function getAllMetadataAccounts( ); } -export async function getAllCustodyAccounts( - tokenProgram: any, - stakeAccounts: PublicKey[] -) { - const allCustodyAccountAddresses = stakeAccounts.map((account) => - getCustodyAccountAddress(account) - ); - return tokenProgram.account.account.fetchMultiple(allCustodyAccountAddresses); -} - // ====================================== // Locked accounts // ====================================== From 9ac11dda06ac158885df8ebcbfa4a4bdf3c6bb7e Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 26 Jan 2024 00:02:38 +0000 Subject: [PATCH 20/24] Increase timeout --- vercel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel.json b/vercel.json index 9a8f3c00..c21c0658 100644 --- a/vercel.json +++ b/vercel.json @@ -1,10 +1,10 @@ { "functions": { "pages/api/v1/all_locked_accounts.ts": { - "maxDuration": 60 + "maxDuration": 90 }, "pages/api/v1/cmc/supply.ts": { - "maxDuration": 60 + "maxDuration": 90 } } } From 26aa610b7d1c3f1cb0e275ad0ab9078952e691f7 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 26 Jan 2024 00:23:24 +0000 Subject: [PATCH 21/24] Logs --- frontend/pages/api/getAllStakingAccounts.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/pages/api/getAllStakingAccounts.ts b/frontend/pages/api/getAllStakingAccounts.ts index daa7c770..3fad8bb2 100644 --- a/frontend/pages/api/getAllStakingAccounts.ts +++ b/frontend/pages/api/getAllStakingAccounts.ts @@ -5,6 +5,7 @@ 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 { + console.log('LOG') const response = await axios({ method: 'post', url: url, @@ -28,5 +29,6 @@ export async function getAllStakeAccounts(url: string): Promise { ], }, }) + console.log('LOG2') return response.data.result.map((x: any) => new PublicKey(x.pubkey)) } From 85ed14c1c999fd1312557a36f43812223762e512 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 26 Jan 2024 11:53:14 +0000 Subject: [PATCH 22/24] Revert "Logs" This reverts commit 26aa610b7d1c3f1cb0e275ad0ab9078952e691f7. --- frontend/pages/api/getAllStakingAccounts.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/pages/api/getAllStakingAccounts.ts b/frontend/pages/api/getAllStakingAccounts.ts index 3fad8bb2..daa7c770 100644 --- a/frontend/pages/api/getAllStakingAccounts.ts +++ b/frontend/pages/api/getAllStakingAccounts.ts @@ -5,7 +5,6 @@ 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 { - console.log('LOG') const response = await axios({ method: 'post', url: url, @@ -29,6 +28,5 @@ export async function getAllStakeAccounts(url: string): Promise { ], }, }) - console.log('LOG2') return response.data.result.map((x: any) => new PublicKey(x.pubkey)) } From aeb122a3ce69d4a4c7114738290af54b84d91817 Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 26 Jan 2024 18:49:56 +0000 Subject: [PATCH 23/24] Revert some stuff --- frontend/pages/api/v1/cmc/supply.ts | 10 ++++------ staking/app/api_utils.ts | 10 ++++++++++ vercel.json | 4 ++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/pages/api/v1/cmc/supply.ts b/frontend/pages/api/v1/cmc/supply.ts index e3597085..3d1651c6 100644 --- a/frontend/pages/api/v1/cmc/supply.ts +++ b/frontend/pages/api/v1/cmc/supply.ts @@ -14,7 +14,7 @@ import { getTotalSupply, getAllMetadataAccounts, getConfig, - getCustodyAccountAddress, + getAllCustodyAccounts, } from '@pythnetwork/staking/app/api_utils' import { getAllStakeAccounts } from 'pages/api/getAllStakingAccounts' @@ -56,11 +56,9 @@ export default async function handlerSupply( allStakeAccounts ) - const allCustodyAccountAddresses = allStakeAccounts.map((account) => - getCustodyAccountAddress(account) - ) - const allCustodyAccounts = await tokenProgram.account.account.fetchMultiple( - allCustodyAccountAddresses + const allCustodyAccounts = await getAllCustodyAccounts( + tokenProgram, + allStakeAccounts ) const configAccountData = await getConfig(stakingProgram) diff --git a/staking/app/api_utils.ts b/staking/app/api_utils.ts index 91c4dc1e..aab2d6dd 100644 --- a/staking/app/api_utils.ts +++ b/staking/app/api_utils.ts @@ -120,6 +120,16 @@ export async function getAllMetadataAccounts( ); } +export async function getAllCustodyAccounts( + tokenProgram: any, + stakeAccounts: PublicKey[] +) { + const allCustodyAccountAddresses = stakeAccounts.map((account) => + getCustodyAccountAddress(account) + ); + return tokenProgram.account.account.fetchMultiple(allCustodyAccountAddresses); +} + // ====================================== // Locked accounts // ====================================== diff --git a/vercel.json b/vercel.json index c21c0658..bc978dc8 100644 --- a/vercel.json +++ b/vercel.json @@ -1,10 +1,10 @@ { "functions": { "pages/api/v1/all_locked_accounts.ts": { - "maxDuration": 90 + "maxDuration": 30 }, "pages/api/v1/cmc/supply.ts": { - "maxDuration": 90 + "maxDuration": 30 } } } From d0737687c5b1320b13fd4b31bc80953d12916d8d Mon Sep 17 00:00:00 2001 From: Guillermo Bescos Date: Fri, 26 Jan 2024 18:52:10 +0000 Subject: [PATCH 24/24] Do it --- frontend/pages/api/v1/cmc/supply.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/pages/api/v1/cmc/supply.ts b/frontend/pages/api/v1/cmc/supply.ts index 3d1651c6..5f965557 100644 --- a/frontend/pages/api/v1/cmc/supply.ts +++ b/frontend/pages/api/v1/cmc/supply.ts @@ -49,6 +49,7 @@ export default async function handlerSupply( 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(RPC_URL) const allMetadataAccounts = await getAllMetadataAccounts( @@ -61,13 +62,11 @@ export default async function handlerSupply( allStakeAccounts ) - const configAccountData = await getConfig(stakingProgram) - const totalLockedAmount = allMetadataAccounts.reduce( (total: PythBalance, account: any, index: number) => { return total.add( - allCustodyAccounts[index]?.amount && account.lock - ? new PythBalance(allCustodyAccounts[index]!.amount).min( + allCustodyAccounts[index].amount && account.lock + ? new PythBalance(allCustodyAccounts[index].amount).min( getCurrentlyLockedAmount(account, configAccountData) ) : PythBalance.zero()