diff --git a/apps/staking/next.config.js b/apps/staking/next.config.js index aaf916ed81..12bcd51ba7 100644 --- a/apps/staking/next.config.js +++ b/apps/staking/next.config.js @@ -52,4 +52,14 @@ export default { ], }, ], + + async rewrites() { + return [ + { + source: "/api/publishers-ranking", + destination: + "https://web-api.pyth.network/publishers_ranking?cluster=pythnet", + }, + ]; + }, }; diff --git a/apps/staking/package.json b/apps/staking/package.json index aaceb7fe64..1402bbb48e 100644 --- a/apps/staking/package.json +++ b/apps/staking/package.json @@ -26,6 +26,7 @@ "@bonfida/spl-name-service": "^3.0.0", "@heroicons/react": "^2.1.4", "@next/third-parties": "^14.2.5", + "@pythnetwork/hermes-client": "workspace:*", "@pythnetwork/staking-sdk": "workspace:*", "@solana/wallet-adapter-base": "^0.9.20", "@solana/wallet-adapter-react": "^0.15.28", @@ -41,7 +42,8 @@ "react-aria-components": "^1.3.3", "react-dom": "^18.3.1", "recharts": "^2.12.7", - "swr": "^2.2.5" + "swr": "^2.2.5", + "zod": "^3.23.8" }, "devDependencies": { "@axe-core/react": "^4.9.1", diff --git a/apps/staking/src/api.ts b/apps/staking/src/api.ts index 05c3819426..f14cb160c4 100644 --- a/apps/staking/src/api.ts +++ b/apps/staking/src/api.ts @@ -1,7 +1,10 @@ // TODO remove these disables when moving off the mock APIs -/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import type { HermesClient } from "@pythnetwork/hermes-client"; import { + epochToDate, + extractPublisherData, getAmountByTargetAndState, getCurrentEpoch, PositionState, @@ -9,6 +12,16 @@ import { type StakeAccountPositions, } from "@pythnetwork/staking-sdk"; import { PublicKey } from "@solana/web3.js"; +import { z } from "zod"; + +const publishersRankingSchema = z + .object({ + publisher: z.string(), + rank: z.number(), + numSymbols: z.number(), + timestamp: z.string(), + }) + .array(); type Data = { total: bigint; @@ -37,6 +50,7 @@ type Data = { cooldown: bigint; cooldown2: bigint; }; + yieldRate: bigint; integrityStakingPublishers: { name: string | undefined; publicKey: PublicKey; @@ -144,22 +158,39 @@ export const getStakeAccounts = async ( export const loadData = async ( client: PythStakingClient, + hermesClient: HermesClient, stakeAccount: StakeAccountPositions, ): Promise => { const [ stakeAccountCustody, - publishers, - ownerAtaAccount, - currentEpoch, + poolData, + ownerPythBalance, unlockSchedule, + poolConfig, + claimableRewards, + currentEpoch, + publisherRankingsResponse, + publisherCaps, ] = await Promise.all([ client.getStakeAccountCustody(stakeAccount.address), - client.getPublishers(), - client.getOwnerPythAtaAccount(), - getCurrentEpoch(client.connection), + client.getPoolDataAccount(), + client.getOwnerPythBalance(), client.getUnlockSchedule(stakeAccount.address), + client.getPoolConfigAccount(), + client.getClaimableRewards(stakeAccount.address), + getCurrentEpoch(client.connection), + fetch("/api/publishers-ranking"), + hermesClient.getLatestPublisherCaps({ + parsed: true, + }), ]); + const publishers = extractPublisherData(poolData); + + const publisherRankings = publishersRankingSchema.parse( + await publisherRankingsResponse.json(), + ); + const filterGovernancePositions = (positionState: PositionState) => getAmountByTargetAndState({ stakeAccountPositions: stakeAccount, @@ -179,11 +210,19 @@ export const loadData = async ( epoch: currentEpoch, }); + const getPublisherCap = (publisher: PublicKey) => + BigInt( + publisherCaps.parsed?.[0]?.publisher_stake_caps.find( + ({ publisher: p }) => p === publisher.toBase58(), + )?.cap ?? 0, + ); + return { lastSlash: undefined, // TODO - availableRewards: 0n, // TODO + availableRewards: claimableRewards, expiringRewards: undefined, // TODO total: stakeAccountCustody.amount, + yieldRate: poolConfig.y, governance: { warmup: filterGovernancePositions(PositionState.LOCKING), staked: filterGovernancePositions(PositionState.LOCKED), @@ -192,24 +231,47 @@ export const loadData = async ( }, unlockSchedule, locked: unlockSchedule.reduce((sum, { amount }) => sum + amount, 0n), - walletAmount: ownerAtaAccount.amount, - integrityStakingPublishers: publishers.map(({ pubkey: publisher }) => ({ - apyHistory: [], // TODO - isSelf: false, // TODO - name: undefined, // TODO - numFeeds: 0, // TODO - poolCapacity: 100n, // TODO - poolUtilization: 0n, // TODO - publicKey: publisher, - qualityRanking: 0, // TODO - selfStake: 0n, // TODO - positions: { - warmup: filterOISPositions(publisher, PositionState.LOCKING), - staked: filterOISPositions(publisher, PositionState.LOCKED), - cooldown: filterOISPositions(publisher, PositionState.PREUNLOCKING), - cooldown2: filterOISPositions(publisher, PositionState.UNLOCKED), - }, - })), + walletAmount: ownerPythBalance, + integrityStakingPublishers: publishers.map((publisherData) => { + const publisherPubkeyString = publisherData.pubkey.toBase58(); + const publisherRanking = publisherRankings.find( + (ranking) => ranking.publisher === publisherPubkeyString, + ); + const apyHistory = publisherData.apyHistory.map(({ epoch, apy }) => ({ + date: epochToDate(epoch + 1n), + apy: Number(apy), + })); + return { + apyHistory, + isSelf: + publisherData.stakeAccount?.equals(stakeAccount.address) ?? false, + name: undefined, // TODO + numFeeds: publisherRanking?.numSymbols ?? 0, + poolCapacity: getPublisherCap(publisherData.pubkey), + poolUtilization: publisherData.totalDelegation, + publicKey: publisherData.pubkey, + qualityRanking: publisherRanking?.rank ?? 0, + selfStake: publisherData.selfDelegation, + positions: { + warmup: filterOISPositions( + publisherData.pubkey, + PositionState.LOCKING, + ), + staked: filterOISPositions( + publisherData.pubkey, + PositionState.LOCKED, + ), + cooldown: filterOISPositions( + publisherData.pubkey, + PositionState.PREUNLOCKING, + ), + cooldown2: filterOISPositions( + publisherData.pubkey, + PositionState.UNLOCKED, + ), + }, + }; + }), }; }; @@ -253,19 +315,27 @@ export const stakeGovernance = async ( }; export const cancelWarmupGovernance = async ( - _client: PythStakingClient, - _stakeAccount: PublicKey, - _amount: bigint, + client: PythStakingClient, + stakeAccount: PublicKey, + amount: bigint, ): Promise => { - throw new NotImplementedError(); + await client.unstakeFromGovernance( + stakeAccount, + PositionState.LOCKING, + amount, + ); }; export const unstakeGovernance = async ( - _client: PythStakingClient, - _stakeAccount: PublicKey, - _amount: bigint, + client: PythStakingClient, + stakeAccount: PublicKey, + amount: bigint, ): Promise => { - throw new NotImplementedError(); + await client.unstakeFromGovernance( + stakeAccount, + PositionState.LOCKED, + amount, + ); }; export const delegateIntegrityStaking = async ( @@ -278,36 +348,30 @@ export const delegateIntegrityStaking = async ( }; export const cancelWarmupIntegrityStaking = async ( - _client: PythStakingClient, - _stakeAccount: PublicKey, - _publisherKey: PublicKey, - _amount: bigint, + client: PythStakingClient, + stakeAccount: PublicKey, + publisherKey: PublicKey, + amount: bigint, ): Promise => { - throw new NotImplementedError(); + await client.unstakeFromPublisher( + stakeAccount, + publisherKey, + PositionState.LOCKING, + amount, + ); }; export const unstakeIntegrityStaking = async ( - _client: PythStakingClient, - _stakeAccount: PublicKey, - _publisherKey: PublicKey, - _amount: bigint, + client: PythStakingClient, + stakeAccount: PublicKey, + publisherKey: PublicKey, + amount: bigint, ): Promise => { - throw new NotImplementedError(); -}; - -export const calculateApy = ( - poolCapacity: bigint, - poolUtilization: bigint, - isSelf: boolean, -) => { - const maxApy = isSelf ? 25 : 20; - const minApy = isSelf ? 10 : 5; - return Math.min( - Math.max( - maxApy - Number((poolUtilization - poolCapacity) / 100_000_000n), - minApy, - ), - maxApy, + await client.unstakeFromPublisher( + stakeAccount, + publisherKey, + PositionState.LOCKED, + amount, ); }; @@ -367,10 +431,3 @@ const mkMockHistory = (): AccountHistory => [ locked: 0n, }, ]; - -class NotImplementedError extends Error { - constructor() { - super("Not yet implemented!"); - this.name = "NotImplementedError"; - } -} diff --git a/apps/staking/src/components/Dashboard/index.tsx b/apps/staking/src/components/Dashboard/index.tsx index df026f1720..39c5220948 100644 --- a/apps/staking/src/components/Dashboard/index.tsx +++ b/apps/staking/src/components/Dashboard/index.tsx @@ -33,6 +33,7 @@ type Props = { cooldown: bigint; cooldown2: bigint; }; + yieldRate: bigint; integrityStakingPublishers: ComponentProps< typeof OracleIntegrityStaking >["publishers"]; @@ -48,6 +49,7 @@ export const Dashboard = ({ integrityStakingPublishers, locked, unlockSchedule, + yieldRate, }: Props) => { const availableToStakeGovernance = useMemo( () => @@ -156,6 +158,7 @@ export const Dashboard = ({ cooldown={integrityStakingCooldown} cooldown2={integrityStakingCooldown2} publishers={integrityStakingPublishers} + yieldRate={yieldRate} /> diff --git a/apps/staking/src/components/OracleIntegrityStaking/index.tsx b/apps/staking/src/components/OracleIntegrityStaking/index.tsx index 0ded2050bf..cab6815bae 100644 --- a/apps/staking/src/components/OracleIntegrityStaking/index.tsx +++ b/apps/staking/src/components/OracleIntegrityStaking/index.tsx @@ -3,7 +3,7 @@ import { XMarkIcon, MagnifyingGlassIcon, } from "@heroicons/react/24/outline"; -import type { PythStakingClient } from "@pythnetwork/staking-sdk"; +import { calculateApy, type PythStakingClient } from "@pythnetwork/staking-sdk"; import { PublicKey } from "@solana/web3.js"; import clsx from "clsx"; import { @@ -27,7 +27,6 @@ import { delegateIntegrityStaking, cancelWarmupIntegrityStaking, unstakeIntegrityStaking, - calculateApy, } from "../../api"; import { Button } from "../Button"; import { ProgramSection } from "../ProgramSection"; @@ -47,6 +46,7 @@ type Props = { cooldown: bigint; cooldown2: bigint; publishers: PublisherProps["publisher"][]; + yieldRate: bigint; }; export const OracleIntegrityStaking = ({ @@ -57,6 +57,7 @@ export const OracleIntegrityStaking = ({ cooldown, cooldown2, publishers, + yieldRate, }: Props) => { const self = useMemo( () => publishers.find((publisher) => publisher.isSelf), @@ -115,6 +116,7 @@ export const OracleIntegrityStaking = ({ availableToStake={availableToStake} publisher={self} totalStaked={staked} + yieldRate={yieldRate} /> @@ -132,6 +134,7 @@ export const OracleIntegrityStaking = ({ availableToStake={availableToStake} publishers={otherPublishers} totalStaked={staked} + yieldRate={yieldRate} /> @@ -143,6 +146,7 @@ type PublisherListProps = { availableToStake: bigint; totalStaked: bigint; publishers: PublisherProps["publisher"][]; + yieldRate: bigint; }; const PublisherList = ({ @@ -150,6 +154,7 @@ const PublisherList = ({ availableToStake, publishers, totalStaked, + yieldRate, }: PublisherListProps) => { const [search, setSearch] = useState(""); const [sort, setSort] = useState({ @@ -173,10 +178,22 @@ const PublisherList = ({ b.name ?? b.publicKey.toBase58(), ); } - case SortField.LastEpochAPY: { + case SortField.APY: { return ( - calculateApy(a.poolCapacity, a.poolUtilization, false) - - calculateApy(b.poolCapacity, b.poolUtilization, false) + calculateApy({ + isSelf: false, + selfStake: a.selfStake, + poolCapacity: a.poolCapacity, + poolUtilization: a.poolUtilization, + yieldRate, + }) - + calculateApy({ + isSelf: false, + selfStake: b.selfStake, + poolCapacity: b.poolCapacity, + poolUtilization: b.poolUtilization, + yieldRate, + }) ); } case SortField.NumberOfFeeds: { @@ -197,7 +214,7 @@ const PublisherList = ({ } }); return sort.descending ? sorted.reverse() : sorted; - }, [publishers, search, sort.field, sort.descending, filter]); + }, [publishers, search, sort.field, sort.descending, filter, yieldRate]); const paginatedPublishers = useMemo( () => @@ -276,11 +293,11 @@ const PublisherList = ({ Pool - Last epoch APY + APY Historical APY ))} @@ -425,6 +443,7 @@ type PublisherProps = { } | undefined; }; + yieldRate: bigint; }; const Publisher = ({ @@ -432,6 +451,7 @@ const Publisher = ({ availableToStake, totalStaked, isSelf, + yieldRate, }: PublisherProps) => { const warmup = useMemo( () => @@ -518,11 +538,13 @@ const Publisher = ({
- {calculateApy( - publisher.poolCapacity, - publisher.poolUtilization, - publisher.isSelf, - )} + {calculateApy({ + isSelf: publisher.isSelf, + selfStake: publisher.selfStake, + poolCapacity: publisher.poolCapacity, + poolUtilization: publisher.poolUtilization, + yieldRate, + })} %
@@ -557,6 +579,8 @@ const Publisher = ({ publisherKey={publisher.publicKey} publisherName={publisher.name} isSelf={publisher.isSelf} + selfStake={publisher.selfStake} + yieldRate={yieldRate} /> )} @@ -641,6 +665,8 @@ type StakeToPublisherButtonProps = { poolCapacity: bigint; poolUtilization: bigint; isSelf: boolean; + selfStake: bigint; + yieldRate: bigint; }; const StakeToPublisherButton = ({ @@ -650,6 +676,8 @@ const StakeToPublisherButton = ({ poolUtilization, availableToStake, isSelf, + selfStake, + yieldRate, }: StakeToPublisherButtonProps) => { const delegate = useTransferActionForPublisher( delegateIntegrityStaking, @@ -669,12 +697,24 @@ const StakeToPublisherButton = ({
APY after staking
- {calculateApy( - poolCapacity, - poolUtilization + - (amount.type === AmountType.Valid ? amount.amount : 0n), - isSelf, - )} + {isSelf + ? calculateApy({ + isSelf, + selfStake: + selfStake + + (amount.type === AmountType.Valid ? amount.amount : 0n), + poolCapacity, + yieldRate, + }) + : calculateApy({ + isSelf, + selfStake, + poolCapacity, + poolUtilization: + poolUtilization + + (amount.type === AmountType.Valid ? amount.amount : 0n), + yieldRate, + })} %
@@ -712,7 +752,7 @@ const hasAnyPositions = ({ positions }: PublisherProps["publisher"]) => enum SortField { PublisherName, PoolUtilization, - LastEpochAPY, + APY, SelfStake, NumberOfFeeds, QualityRanking, diff --git a/apps/staking/src/hooks/use-dashboard-data.ts b/apps/staking/src/hooks/use-dashboard-data.ts index 1c507094cc..ccde6aa0a6 100644 --- a/apps/staking/src/hooks/use-dashboard-data.ts +++ b/apps/staking/src/hooks/use-dashboard-data.ts @@ -12,11 +12,11 @@ const REFRESH_INTERVAL = 1 * ONE_MINUTE_IN_MS; export const getCacheKey = (stakeAccount: PublicKey) => stakeAccount.toBase58(); export const useDashboardData = () => { - const { client, account } = useSelectedStakeAccount(); + const { client, hermesClient, account } = useSelectedStakeAccount(); const { data, isLoading, mutate, ...rest } = useSWR( getCacheKey(account.address), - () => loadData(client, account), + () => loadData(client, hermesClient, account), { refreshInterval: REFRESH_INTERVAL, }, diff --git a/apps/staking/src/hooks/use-stake-account.tsx b/apps/staking/src/hooks/use-stake-account.tsx index 3efd0d44d9..e119574a49 100644 --- a/apps/staking/src/hooks/use-stake-account.tsx +++ b/apps/staking/src/hooks/use-stake-account.tsx @@ -1,5 +1,6 @@ "use client"; +import { HermesClient } from "@pythnetwork/hermes-client"; import type { StakeAccountPositions } from "@pythnetwork/staking-sdk"; import { PythStakingClient } from "@pythnetwork/staking-sdk"; import { useConnection, useWallet } from "@solana/wallet-adapter-react"; @@ -38,12 +39,14 @@ const State = { Loaded: ( client: PythStakingClient, + hermesClient: HermesClient, account: StakeAccountPositions, allAccounts: [StakeAccountPositions, ...StakeAccountPositions[]], selectAccount: (account: StakeAccountPositions) => void, ) => ({ type: StateType.Loaded as const, client, + hermesClient, account, allAccounts, selectAccount, @@ -83,7 +86,13 @@ const useStakeAccountState = () => { (account: StakeAccountPositions) => { setState((cur) => cur.type === StateType.Loaded - ? State.Loaded(cur.client, account, cur.allAccounts, setAccount) + ? State.Loaded( + cur.client, + cur.hermesClient, + account, + cur.allAccounts, + setAccount, + ) : cur, ); }, @@ -109,6 +118,8 @@ const useStakeAccountState = () => { signTransaction: wallet.signTransaction, }, }); + // TODO: use env var to support mainnet + const hermesClient = new HermesClient("https://hermes-beta.pyth.network"); getStakeAccounts(client) .then((accounts) => { const [firstAccount, ...otherAccounts] = accounts; @@ -116,6 +127,7 @@ const useStakeAccountState = () => { setState( State.Loaded( client, + hermesClient, firstAccount, [firstAccount, ...otherAccounts], setAccount, diff --git a/governance/pyth_staking_sdk/src/index.ts b/governance/pyth_staking_sdk/src/index.ts index 184a2233b8..19f5f2281b 100644 --- a/governance/pyth_staking_sdk/src/index.ts +++ b/governance/pyth_staking_sdk/src/index.ts @@ -1,4 +1,6 @@ export * from "./pyth-staking-client"; export * from "./utils/clock"; export * from "./utils/position"; +export * from "./utils/pool"; +export * from "./utils/apy"; export * from "./types"; diff --git a/governance/pyth_staking_sdk/src/pyth-staking-client.ts b/governance/pyth_staking_sdk/src/pyth-staking-client.ts index 59a6f80cd6..3034a7a04d 100644 --- a/governance/pyth_staking_sdk/src/pyth-staking-client.ts +++ b/governance/pyth_staking_sdk/src/pyth-staking-client.ts @@ -6,7 +6,12 @@ import { getAssociatedTokenAddress, } from "@solana/spl-token"; import type { AnchorWallet } from "@solana/wallet-adapter-react"; -import { Connection, PublicKey } from "@solana/web3.js"; +import { + Connection, + PublicKey, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; import { getConfigAddress, @@ -14,14 +19,20 @@ import { getStakeAccountCustodyAddress, getStakeAccountMetadataAddress, } from "./pdas"; -import type { - GlobalConfig, - PoolConfig, - PoolDataAccount, - StakeAccountPositions, +import { + PositionState, + type GlobalConfig, + type PoolConfig, + type PoolDataAccount, + type StakeAccountPositions, } from "./types"; import { convertBigIntToBN, convertBNToBigInt } from "./utils/bn"; -import { deserializeStakeAccountPositions } from "./utils/position"; +import { getCurrentEpoch } from "./utils/clock"; +import { extractPublisherData } from "./utils/pool"; +import { + deserializeStakeAccountPositions, + getPositionState, +} from "./utils/position"; import { sendTransaction } from "./utils/transaction"; import { getUnlockSchedule } from "./utils/vesting"; import * as IntegrityPoolIdl from "../idl/integrity-pool.json"; @@ -174,6 +185,15 @@ export class PythStakingClient { ); } + public async getOwnerPythBalance(): Promise { + try { + const ataAccount = await this.getOwnerPythAtaAccount(); + return ataAccount.amount; + } catch { + return 0n; + } + } + public async getPoolConfigAccount(): Promise { const poolConfigAnchor = await this.integrityPoolProgram.account.poolConfig.fetch( @@ -190,26 +210,6 @@ export class PythStakingClient { return convertBNToBigInt(poolDataAccountAnchor); } - public async getPublishers(): Promise< - { - pubkey: PublicKey; - stakeAccount: PublicKey | null; - }[] - > { - const poolData = await this.getPoolDataAccount(); - - return poolData.publishers - .map((publisher, index) => ({ - pubkey: publisher, - stakeAccount: - poolData.publisherStakeAccounts[index] === undefined || - poolData.publisherStakeAccounts[index].equals(PublicKey.default) - ? null - : poolData.publisherStakeAccounts[index], - })) - .filter(({ pubkey }) => !pubkey.equals(PublicKey.default)); - } - public async stakeToGovernance( stakeAccountPositions: PublicKey, amount: bigint, @@ -229,6 +229,117 @@ export class PythStakingClient { return sendTransaction([instruction], this.connection, this.wallet); } + public async unstakeFromGovernance( + stakeAccountPositions: PublicKey, + positionState: PositionState.LOCKED | PositionState.LOCKING, + amount: bigint, + ) { + const stakeAccountPositionsData = await this.getStakeAccountPositions( + stakeAccountPositions, + ); + const currentEpoch = await getCurrentEpoch(this.connection); + + let remainingAmount = amount; + const instructionPromises: Promise[] = []; + + const eligiblePositions = stakeAccountPositionsData.data.positions + .map((p, i) => ({ position: p, index: i })) + .reverse() + .filter( + ({ position }) => + position.targetWithParameters.voting !== undefined && + positionState === getPositionState(position, currentEpoch), + ); + + for (const { position, index } of eligiblePositions) { + if (position.amount < remainingAmount) { + instructionPromises.push( + this.stakingProgram.methods + .closePosition(index, convertBigIntToBN(position.amount), { + voting: {}, + }) + .accounts({ + stakeAccountPositions, + }) + .instruction(), + ); + remainingAmount -= position.amount; + } else { + instructionPromises.push( + this.stakingProgram.methods + .closePosition(index, convertBigIntToBN(remainingAmount), { + voting: {}, + }) + .accounts({ + stakeAccountPositions, + }) + .instruction(), + ); + break; + } + } + + const instructions = await Promise.all(instructionPromises); + return sendTransaction(instructions, this.connection, this.wallet); + } + + public async unstakeFromPublisher( + stakeAccountPositions: PublicKey, + publisher: PublicKey, + positionState: PositionState.LOCKED | PositionState.LOCKING, + amount: bigint, + ) { + const stakeAccountPositionsData = await this.getStakeAccountPositions( + stakeAccountPositions, + ); + const currentEpoch = await getCurrentEpoch(this.connection); + + let remainingAmount = amount; + const instructionPromises: Promise[] = []; + + const eligiblePositions = stakeAccountPositionsData.data.positions + .map((p, i) => ({ position: p, index: i })) + .reverse() + .filter( + ({ position }) => + position.targetWithParameters.integrityPool?.publisher !== + undefined && + position.targetWithParameters.integrityPool.publisher.equals( + publisher, + ) && + positionState === getPositionState(position, currentEpoch), + ); + + for (const { position, index } of eligiblePositions) { + if (position.amount < remainingAmount) { + instructionPromises.push( + this.integrityPoolProgram.methods + .undelegate(index, convertBigIntToBN(position.amount)) + .accounts({ + stakeAccountPositions, + publisher, + }) + .instruction(), + ); + remainingAmount -= position.amount; + } else { + instructionPromises.push( + this.integrityPoolProgram.methods + .undelegate(index, convertBigIntToBN(remainingAmount)) + .accounts({ + stakeAccountPositions, + publisher, + }) + .instruction(), + ); + break; + } + } + + const instructions = await Promise.all(instructionPromises); + return sendTransaction(instructions, this.connection, this.wallet); + } + public async depositTokensToStakeAccountCustody( stakeAccountPositions: PublicKey, amount: bigint, @@ -313,13 +424,24 @@ export class PythStakingClient { }); } - public async advanceDelegationRecord(stakeAccountPositions: PublicKey) { - // TODO: optimize to only send transactions for publishers that have positive rewards - const publishers = await this.getPublishers(); + async getAdvanceDelegationRecordInstructions( + stakeAccountPositions: PublicKey, + ) { + const poolData = await this.getPoolDataAccount(); + const stakeAccountPositionsData = await this.getStakeAccountPositions( + stakeAccountPositions, + ); + const allPublishers = extractPublisherData(poolData); + const publishers = allPublishers.filter(({ pubkey }) => + stakeAccountPositionsData.data.positions.some( + ({ targetWithParameters }) => + targetWithParameters.integrityPool?.publisher.equals(pubkey), + ), + ); // anchor does not calculate the correct pda for other programs // therefore we need to manually calculate the pdas - const instructions = await Promise.all( + return Promise.all( publishers.map(({ pubkey, stakeAccount }) => this.integrityPoolProgram.methods .advanceDelegationRecord() @@ -338,7 +460,27 @@ export class PythStakingClient { .instruction(), ), ); + } + + public async advanceDelegationRecord(stakeAccountPositions: PublicKey) { + const instructions = await this.getAdvanceDelegationRecordInstructions( + stakeAccountPositions, + ); return sendTransaction(instructions, this.connection, this.wallet); } + + public async getClaimableRewards(stakeAccountPositions: PublicKey) { + const instructions = await this.getAdvanceDelegationRecordInstructions( + stakeAccountPositions, + ); + + for (const instruction of instructions) { + await this.connection.simulateTransaction( + new Transaction().add(instruction), + ); + } + + return 1n; + } } diff --git a/governance/pyth_staking_sdk/src/types.ts b/governance/pyth_staking_sdk/src/types.ts index 10fbcd13d8..ddd27972b9 100644 --- a/governance/pyth_staking_sdk/src/types.ts +++ b/governance/pyth_staking_sdk/src/types.ts @@ -11,13 +11,7 @@ export type Convert = T extends From ? Convert[] : T extends Record ? { - [K in keyof T]: T[K] extends From - ? To - : T[K] extends From | null - ? To | null - : T[K] extends Record | unknown[] - ? Convert - : T[K]; + [K in keyof T]: Convert; } : T; @@ -54,6 +48,14 @@ export type StakeAccountPositions = { }; }; +export type PublisherData = { + pubkey: PublicKey; + stakeAccount: PublicKey | null; + totalDelegation: bigint; + selfDelegation: bigint; + apyHistory: { epoch: bigint; apy: bigint; selfApy: bigint }[]; +}[]; + export enum PositionState { UNLOCKED, LOCKING, diff --git a/governance/pyth_staking_sdk/src/utils/apy.ts b/governance/pyth_staking_sdk/src/utils/apy.ts new file mode 100644 index 0000000000..b182f698ae --- /dev/null +++ b/governance/pyth_staking_sdk/src/utils/apy.ts @@ -0,0 +1,36 @@ +export const calculateApy = ( + options: { + selfStake: bigint; + poolCapacity: bigint; + yieldRate: bigint; + } & ({ isSelf: true } | { isSelf: false; poolUtilization: bigint }), +) => { + const { selfStake, poolCapacity, yieldRate, isSelf } = options; + const eligibleSelfStake = selfStake > poolCapacity ? poolCapacity : selfStake; + + const apyPercentage = (Number(yieldRate) * 52 * 100) / 1_000_000; + + if (isSelf) { + if (selfStake === 0n) { + return apyPercentage; + } + return (apyPercentage * Number(eligibleSelfStake)) / Number(selfStake); + } + + const { poolUtilization } = options; + + const delegatorPoolUtilization = poolUtilization - selfStake; + const delegatorPoolCapacity = poolCapacity - eligibleSelfStake; + const eligibleStake = + delegatorPoolUtilization > delegatorPoolCapacity + ? delegatorPoolCapacity + : delegatorPoolUtilization; + + if (poolUtilization === selfStake) { + return apyPercentage; + } + + return ( + (apyPercentage * Number(eligibleStake)) / Number(delegatorPoolUtilization) + ); +}; diff --git a/governance/pyth_staking_sdk/src/utils/clock.ts b/governance/pyth_staking_sdk/src/utils/clock.ts index 0c9e41ef2c..2caf48506d 100644 --- a/governance/pyth_staking_sdk/src/utils/clock.ts +++ b/governance/pyth_staking_sdk/src/utils/clock.ts @@ -17,3 +17,7 @@ export const getCurrentEpoch: ( const timestamp = await getCurrentSolanaTimestamp(connection); return timestamp / EPOCH_DURATION; }; + +export const epochToDate = (epoch: bigint): Date => { + return new Date(Number(epoch * EPOCH_DURATION * 1000n)); +}; diff --git a/governance/pyth_staking_sdk/src/utils/pool.ts b/governance/pyth_staking_sdk/src/utils/pool.ts new file mode 100644 index 0000000000..9df6672e25 --- /dev/null +++ b/governance/pyth_staking_sdk/src/utils/pool.ts @@ -0,0 +1,34 @@ +import { PublicKey } from "@solana/web3.js"; + +import type { PoolDataAccount, PublisherData } from "../types"; + +export const extractPublisherData = ( + poolData: PoolDataAccount, +): PublisherData => { + return poolData.publishers + .filter((publisher) => !publisher.equals(PublicKey.default)) + .map((publisher, index) => ({ + pubkey: publisher, + stakeAccount: + poolData.publisherStakeAccounts[index] === undefined || + poolData.publisherStakeAccounts[index].equals(PublicKey.default) + ? null + : poolData.publisherStakeAccounts[index], + totalDelegation: + (poolData.delState[index]?.totalDelegation ?? 0n) + + (poolData.selfDelState[index]?.totalDelegation ?? 0n), + selfDelegation: poolData.selfDelState[index]?.totalDelegation ?? 0n, + apyHistory: poolData.events + .filter((event) => event.epoch > 0n) + .map((event) => ({ + epoch: event.epoch, + apy: + (event.y * (event.eventData[index]?.otherRewardRatio ?? 0n)) / + 1_000_000n, + selfApy: + (event.y * (event.eventData[index]?.selfRewardRatio ?? 0n)) / + 1_000_000n, + })) + .sort((a, b) => Number(a.epoch) - Number(b.epoch)), + })); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6355639e7a..b9d556c0c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,7 +68,7 @@ importers: version: 2.1.1 connectkit: specifier: ^1.8.2 - version: 1.8.2(m5fu6jwi7nvuqo5lp7m3jyfehy) + version: 1.8.2(@babel/core@7.24.7)(@tanstack/react-query@5.45.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.10.4(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.1(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) cryptocurrency-icons: specifier: ^0.18.1 version: 0.18.1 @@ -333,6 +333,9 @@ importers: '@next/third-parties': specifier: ^14.2.5 version: 14.2.6(next@14.2.6(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@pythnetwork/hermes-client': + specifier: workspace:* + version: link:../hermes/client/js '@pythnetwork/staking-sdk': specifier: workspace:* version: link:../../governance/pyth_staking_sdk @@ -381,6 +384,9 @@ importers: swr: specifier: ^2.2.5 version: 2.2.5(react@18.3.1) + zod: + specifier: ^3.23.8 + version: 3.23.8 devDependencies: '@axe-core/react': specifier: ^4.9.1 @@ -1444,7 +1450,7 @@ importers: version: 0.9.24(bufferutil@4.0.7)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@6.0.3) '@matterlabs/hardhat-zksync': specifier: ^1.1.0 - version: 1.1.0(w3kf4mhrlhkqd2mcprd2ix74zu) + version: 1.1.0(@matterlabs/hardhat-zksync-deploy@0.6.6(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(zksync-web3@0.13.4(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))))(@matterlabs/hardhat-zksync-ethers@1.1.0(bufferutil@4.0.7)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)(zksync-ethers@6.11.2(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))))(@matterlabs/hardhat-zksync-node@1.1.1(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(@matterlabs/hardhat-zksync-solc@0.3.17(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(@matterlabs/hardhat-zksync-upgradable@1.5.2(bufferutil@4.0.7)(encoding@0.1.13)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(@matterlabs/hardhat-zksync-verify@1.6.0(@nomicfoundation/hardhat-verify@2.0.9(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3) '@matterlabs/hardhat-zksync-deploy': specifier: ^0.6.6 version: 0.6.6(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(zksync-web3@0.13.4(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))) @@ -28087,8 +28093,8 @@ snapshots: - encoding - supports-color - '@matterlabs/hardhat-zksync@1.1.0(w3kf4mhrlhkqd2mcprd2ix74zu)': - dependencies: + ? '@matterlabs/hardhat-zksync@1.1.0(@matterlabs/hardhat-zksync-deploy@0.6.6(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(zksync-web3@0.13.4(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))))(@matterlabs/hardhat-zksync-ethers@1.1.0(bufferutil@4.0.7)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)(zksync-ethers@6.11.2(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))))(@matterlabs/hardhat-zksync-node@1.1.1(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(@matterlabs/hardhat-zksync-solc@0.3.17(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(@matterlabs/hardhat-zksync-upgradable@1.5.2(bufferutil@4.0.7)(encoding@0.1.13)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(@matterlabs/hardhat-zksync-verify@1.6.0(@nomicfoundation/hardhat-verify@2.0.9(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)))(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)' + : dependencies: '@matterlabs/hardhat-zksync-deploy': 0.6.6(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3))(zksync-web3@0.13.4(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))) '@matterlabs/hardhat-zksync-ethers': 1.1.0(bufferutil@4.0.7)(encoding@0.1.13)(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)(zksync-ethers@6.11.2(ethers@5.7.2(bufferutil@4.0.7)(utf-8-validate@6.0.3))) '@matterlabs/hardhat-zksync-node': 1.1.1(encoding@0.1.13)(hardhat@2.22.8(bufferutil@4.0.7)(ts-node@10.9.2(@types/node@22.5.1)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@6.0.3)) @@ -35027,8 +35033,8 @@ snapshots: '@vue/shared@3.4.34': {} - '@wagmi/connectors@5.0.16(mehtb7r3xxh3anmscqllj3vxmi)': - dependencies: + ? '@wagmi/connectors@5.0.16(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.3)(@wagmi/core@2.11.4(@tanstack/query-core@5.45.0)(@types/react@18.3.3)(bufferutil@4.0.8)(immer@9.0.21)(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)' + : dependencies: '@coinbase/wallet-sdk': 4.0.3 '@metamask/sdk': 0.26.0(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8) @@ -37893,8 +37899,8 @@ snapshots: transitivePeerDependencies: - supports-color - connectkit@1.8.2(m5fu6jwi7nvuqo5lp7m3jyfehy): - dependencies: + ? connectkit@1.8.2(@babel/core@7.24.7)(@tanstack/react-query@5.45.1(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(wagmi@2.10.4(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.1(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8)) + : dependencies: '@tanstack/react-query': 5.45.1(react@18.3.1) buffer: 6.0.3 detect-browser: 5.3.0 @@ -51434,7 +51440,7 @@ snapshots: wagmi@2.10.4(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@tanstack/query-core@5.45.0)(@tanstack/react-query@5.45.1(react@18.3.1))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(immer@9.0.21)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8): dependencies: '@tanstack/react-query': 5.45.1(react@18.3.1) - '@wagmi/connectors': 5.0.16(mehtb7r3xxh3anmscqllj3vxmi) + '@wagmi/connectors': 5.0.16(@react-native-async-storage/async-storage@1.23.1(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10)))(@types/react@18.3.3)(@wagmi/core@2.11.4(@tanstack/query-core@5.45.0)(@types/react@18.3.3)(bufferutil@4.0.8)(immer@9.0.21)(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8))(bufferutil@4.0.8)(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react-i18next@13.5.0(i18next@22.5.1)(react-dom@18.3.1(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1))(react-native@0.74.2(@babel/core@7.24.7)(@babel/preset-env@7.24.7(@babel/core@7.24.7))(@types/react@18.3.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(utf-8-validate@5.0.10))(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) '@wagmi/core': 2.11.4(@tanstack/query-core@5.45.0)(@types/react@18.3.3)(bufferutil@4.0.8)(immer@9.0.21)(react@18.3.1)(typescript@5.5.2)(utf-8-validate@5.0.10)(viem@2.15.1(bufferutil@4.0.8)(typescript@5.5.2)(utf-8-validate@5.0.10)(zod@3.23.8))(zod@3.23.8) react: 18.3.1 use-sync-external-store: 1.2.0(react@18.3.1)