diff --git a/governance/pyth_staking_sdk/eslint.config.js b/governance/pyth_staking_sdk/eslint.config.mjs similarity index 100% rename from governance/pyth_staking_sdk/eslint.config.js rename to governance/pyth_staking_sdk/eslint.config.mjs diff --git a/governance/pyth_staking_sdk/package.json b/governance/pyth_staking_sdk/package.json index 67ac1256e3..dcc490a863 100644 --- a/governance/pyth_staking_sdk/package.json +++ b/governance/pyth_staking_sdk/package.json @@ -1,16 +1,17 @@ { "name": "@pythnetwork/staking-sdk", - "version": "0.0.0", + "version": "0.0.1", "description": "Pyth staking SDK", - "type": "module", - "exports": { - ".": "./src/index.ts" - }, + "main": "dist/src/index.js", + "types": "dist/src/index.d.ts", "publishConfig": { "access": "public" }, + "files": [ + "dist/**/*" + ], "scripts": { - "build": "tsc && node scripts/update-package-json.js", + "build": "tsc && node scripts/update-package-json.mjs", "test": "pnpm run test:format && pnpm run test:lint && pnpm run test:integration && pnpm run test:types", "fix": "pnpm fix:lint && pnpm fix:format", "fix:format": "prettier --write .", diff --git a/governance/pyth_staking_sdk/prettier.config.js b/governance/pyth_staking_sdk/prettier.config.mjs similarity index 100% rename from governance/pyth_staking_sdk/prettier.config.js rename to governance/pyth_staking_sdk/prettier.config.mjs diff --git a/governance/pyth_staking_sdk/scripts/update-package-json.js b/governance/pyth_staking_sdk/scripts/update-package-json.mjs similarity index 92% rename from governance/pyth_staking_sdk/scripts/update-package-json.js rename to governance/pyth_staking_sdk/scripts/update-package-json.mjs index 92574ce325..931eb14cfc 100644 --- a/governance/pyth_staking_sdk/scripts/update-package-json.js +++ b/governance/pyth_staking_sdk/scripts/update-package-json.mjs @@ -15,8 +15,6 @@ const distPackageJsonPath = path.join(__dirname, "..", "dist", "package.json"); const packageJson = JSON.parse(fs.readFileSync(distPackageJsonPath, "utf8")); -packageJson.exports = { - ".": "./src/index.js", -}; +packageJson.main = "src/index.js"; fs.writeFileSync(distPackageJsonPath, JSON.stringify(packageJson, null, 2)); diff --git a/governance/pyth_staking_sdk/src/constants.ts b/governance/pyth_staking_sdk/src/constants.ts index 78bbe549a5..0b5249ef95 100644 --- a/governance/pyth_staking_sdk/src/constants.ts +++ b/governance/pyth_staking_sdk/src/constants.ts @@ -9,6 +9,8 @@ export const ONE_YEAR_IN_SECONDS = 365n * ONE_DAY_IN_SECONDS; export const EPOCH_DURATION = ONE_WEEK_IN_SECONDS; +export const MAX_VOTER_WEIGHT = 10_000_000_000_000_000n; // 10 Billion with 6 decimals + export const FRACTION_PRECISION = 1_000_000; export const FRACTION_PRECISION_N = 1_000_000n; diff --git a/governance/pyth_staking_sdk/src/pdas.ts b/governance/pyth_staking_sdk/src/pdas.ts index a3e5f7afcc..29df5af889 100644 --- a/governance/pyth_staking_sdk/src/pdas.ts +++ b/governance/pyth_staking_sdk/src/pdas.ts @@ -50,3 +50,26 @@ export const getDelegationRecordAddress = ( INTEGRITY_POOL_PROGRAM_ADDRESS, )[0]; }; + +export const getTargetAccountAddress = () => { + return PublicKey.findProgramAddressSync( + [Buffer.from("target"), Buffer.from("voting")], + STAKING_PROGRAM_ADDRESS, + )[0]; +}; + +export const getVoterWeightRecordAddress = ( + stakeAccountPositions: PublicKey, +) => { + return PublicKey.findProgramAddressSync( + [Buffer.from("voter_weight"), stakeAccountPositions.toBuffer()], + STAKING_PROGRAM_ADDRESS, + ); +}; + +export const getMaxVoterWeightRecordAddress = () => { + return PublicKey.findProgramAddressSync( + [Buffer.from("max_voter")], + STAKING_PROGRAM_ADDRESS, + ); +}; diff --git a/governance/pyth_staking_sdk/src/pyth-staking-client.ts b/governance/pyth_staking_sdk/src/pyth-staking-client.ts index 714a5e8936..f32f00923c 100644 --- a/governance/pyth_staking_sdk/src/pyth-staking-client.ts +++ b/governance/pyth_staking_sdk/src/pyth-staking-client.ts @@ -25,8 +25,9 @@ import { } from "@solana/web3.js"; import { - FRACTION_PRECISION_N, GOVERNANCE_ADDRESS, + MAX_VOTER_WEIGHT, + FRACTION_PRECISION_N, ONE_YEAR_IN_SECONDS, POSITIONS_ACCOUNT_SIZE, } from "./constants"; @@ -36,6 +37,7 @@ import { getPoolConfigAddress, getStakeAccountCustodyAddress, getStakeAccountMetadataAddress, + getTargetAccountAddress, } from "./pdas"; import { PositionState, @@ -43,6 +45,8 @@ import { type PoolConfig, type PoolDataAccount, type StakeAccountPositions, + type TargetAccount, + type VoterWeightAction, type VestingSchedule, } from "./types"; import { convertBigIntToBN, convertBNToBigInt } from "./utils/bn"; @@ -51,6 +55,7 @@ import { extractPublisherData } from "./utils/pool"; import { deserializeStakeAccountPositions, getPositionState, + getVotingTokenAmount, } from "./utils/position"; import { sendTransaction } from "./utils/transaction"; import { getUnlockSchedule } from "./utils/vesting"; @@ -750,6 +755,121 @@ export class PythStakingClient { ); } + public async getTargetAccount(): Promise { + const targetAccount = + await this.stakingProgram.account.targetMetadata.fetch( + getTargetAccountAddress(), + ); + return convertBNToBigInt(targetAccount); + } + + /** + * This returns the current scaling factor between staked tokens and realms voter weight. + * The formula is n_staked_tokens = scaling_factor * n_voter_weight + */ + public async getScalingFactor(): Promise { + const targetAccount = await this.getTargetAccount(); + return Number(targetAccount.locked) / Number(MAX_VOTER_WEIGHT); + } + + public async getRecoverAccountInstruction( + stakeAccountPositions: PublicKey, + governanceAuthority: PublicKey, + ): Promise { + return this.stakingProgram.methods + .recoverAccount() + .accountsPartial({ + stakeAccountPositions, + governanceAuthority, + }) + .instruction(); + } + + public async getUpdatePoolAuthorityInstruction( + governanceAuthority: PublicKey, + poolAuthority: PublicKey, + ): Promise { + return this.stakingProgram.methods + .updatePoolAuthority(poolAuthority) + .accounts({ + governanceAuthority, + }) + .instruction(); + } + + public async getUpdateVoterWeightInstruction( + stakeAccountPositions: PublicKey, + action: VoterWeightAction, + remainingAccount?: PublicKey, + ) { + return this.stakingProgram.methods + .updateVoterWeight(action) + .accounts({ + stakeAccountPositions, + }) + .remainingAccounts( + remainingAccount + ? [ + { + pubkey: remainingAccount, + isWritable: false, + isSigner: false, + }, + ] + : [], + ) + .instruction(); + } + + public async getMainStakeAccount(owner?: PublicKey) { + const stakeAccountPositions = await this.getAllStakeAccountPositions(owner); + const currentEpoch = await getCurrentEpoch(this.connection); + + const stakeAccountVotingTokens = await Promise.all( + stakeAccountPositions.map(async (position) => { + const stakeAccountPositionsData = + await this.getStakeAccountPositions(position); + return { + stakeAccountPosition: position, + votingTokens: getVotingTokenAmount( + stakeAccountPositionsData, + currentEpoch, + ), + }; + }), + ); + + let mainAccount = stakeAccountVotingTokens[0]; + + if (mainAccount === undefined) { + return; + } + + for (let i = 1; i < stakeAccountVotingTokens.length; i++) { + const currentAccount = stakeAccountVotingTokens[i]; + if ( + currentAccount !== undefined && + currentAccount.votingTokens > mainAccount.votingTokens + ) { + mainAccount = currentAccount; + } + } + + return mainAccount; + } + + public async getVoterWeight(owner?: PublicKey) { + const mainAccount = await this.getMainStakeAccount(owner); + + if (mainAccount === undefined) { + return 0; + } + + const targetAccount = await this.getTargetAccount(); + + return (mainAccount.votingTokens * MAX_VOTER_WEIGHT) / targetAccount.locked; + } + public async getPythTokenMint(): Promise { const globalConfig = await this.getGlobalConfig(); return getMint(this.connection, globalConfig.pythTokenMint); diff --git a/governance/pyth_staking_sdk/src/types.ts b/governance/pyth_staking_sdk/src/types.ts index a03dbfacb8..92abacd464 100644 --- a/governance/pyth_staking_sdk/src/types.ts +++ b/governance/pyth_staking_sdk/src/types.ts @@ -35,6 +35,11 @@ export type TargetWithParameters = IdlTypes["targetWithParameters"]; export type VestingScheduleAnchor = IdlTypes["vestingSchedule"]; export type VestingSchedule = ConvertBNToBigInt; +export type TargetAccountAnchor = IdlAccounts["targetMetadata"]; +export type TargetAccount = ConvertBNToBigInt; + +export type VoterWeightAction = IdlTypes["voterWeightAction"]; + export type UnlockSchedule = { type: "fullyUnlocked" | "periodicUnlockingAfterListing" | "periodicUnlocking"; schedule: { diff --git a/governance/pyth_staking_sdk/src/utils/position.ts b/governance/pyth_staking_sdk/src/utils/position.ts index 1e4eb2430d..7abc2830a9 100644 --- a/governance/pyth_staking_sdk/src/utils/position.ts +++ b/governance/pyth_staking_sdk/src/utils/position.ts @@ -92,3 +92,22 @@ export const deserializeStakeAccountPositions = ( }, }; }; + +export const getVotingTokenAmount = ( + stakeAccountPositions: StakeAccountPositions, + epoch: bigint, +) => { + const positions = stakeAccountPositions.data.positions; + const votingPositions = positions + .filter((p) => p.targetWithParameters.voting) + .filter((p) => + [PositionState.LOCKED, PositionState.PREUNLOCKING].includes( + getPositionState(p, epoch), + ), + ); + const totalVotingTokenAmount = votingPositions.reduce( + (sum, p) => sum + p.amount, + 0n, + ); + return totalVotingTokenAmount; +}; diff --git a/governance/pyth_staking_sdk/tsconfig.json b/governance/pyth_staking_sdk/tsconfig.json index 1411fbaef3..4ae721b71f 100644 --- a/governance/pyth_staking_sdk/tsconfig.json +++ b/governance/pyth_staking_sdk/tsconfig.json @@ -7,11 +7,12 @@ "baseUrl": "./", "noEmit": false, "target": "ESNext", - "module": "ESNext", + "module": "CommonJS", "moduleResolution": "Node", "declaration": true, "composite": true, "declarationMap": true, - "esModuleInterop": true + "esModuleInterop": true, + "verbatimModuleSyntax": false } }