Skip to content

Commit e3d8bfe

Browse files
feat: add new pyth staking sdk (#1860)
* feat: initialize sdk package * wip * wip * try using the sdk in the app * wip * add deposit tokens * update ts config * add bigint interface * add pool config * add pool data * fix wallet * pnpm lcok * chore: update jest.config.js to set rootDir for tests * add publisher caps program * feat: update idl * feat: add stake to publisher * feat: rename staking sdk package * go * gp * refactor * fix types * bug * withdraw * refactor * add unlock schedule * fix * fix * add claim * fix * fix * fix * fix * fix * fix package.json * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * fix * ci * fix * fix * Run staking build command from root * Log errors when loading dashboard data * Use env var to select network * ci --------- Co-authored-by: Connor Prussin <[email protected]>
1 parent 1956d58 commit e3d8bfe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+10461
-337
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ patches/
1818
# build graph config.
1919
apps/api-reference
2020
apps/staking
21+
governance/pyth_staking_sdk

apps/staking/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"@headlessui/react": "^2.1.2",
2828
"@heroicons/react": "^2.1.4",
2929
"@next/third-parties": "^14.2.5",
30+
"@pythnetwork/staking-sdk": "workspace:*",
3031
"@solana/wallet-adapter-base": "^0.9.20",
3132
"@solana/wallet-adapter-react": "^0.15.28",
3233
"@solana/wallet-adapter-react-ui": "^0.9.27",

apps/staking/src/api.ts

Lines changed: 141 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
// TODO remove these disables when moving off the mock APIs
22
/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-non-null-assertion */
33

4+
import {
5+
getAmountByTargetAndState,
6+
getCurrentEpoch,
7+
PositionState,
8+
PythStakingClient,
9+
type StakeAccountPositions,
10+
} from "@pythnetwork/staking-sdk";
411
import type { AnchorWallet } from "@solana/wallet-adapter-react";
5-
import type { Connection } from "@solana/web3.js";
6-
7-
export type StakeAccount = {
8-
publicKey: `0x${string}`;
9-
};
12+
import { PublicKey, type Connection } from "@solana/web3.js";
1013

1114
export type Context = {
1215
connection: Connection;
1316
wallet: AnchorWallet;
14-
stakeAccount: StakeAccount;
17+
stakeAccount: StakeAccountPositions;
1518
};
1619

1720
type Data = {
@@ -23,10 +26,12 @@ type Data = {
2326
date: Date;
2427
}
2528
| undefined;
26-
expiringRewards: {
27-
amount: bigint;
28-
expiry: Date;
29-
};
29+
expiringRewards:
30+
| {
31+
amount: bigint;
32+
expiry: Date;
33+
}
34+
| undefined;
3035
locked: bigint;
3136
unlockSchedule: {
3237
date: Date;
@@ -41,7 +46,7 @@ type Data = {
4146
};
4247
integrityStakingPublishers: {
4348
name: string;
44-
publicKey: `0x${string}`;
49+
publicKey: string;
4550
isSelf: boolean;
4651
selfStake: bigint;
4752
poolCapacity: bigint;
@@ -140,96 +145,177 @@ type AccountHistory = {
140145
}[];
141146

142147
export const getStakeAccounts = async (
143-
_connection: Connection,
144-
_wallet: AnchorWallet,
145-
): Promise<StakeAccount[]> => {
146-
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
147-
return MOCK_STAKE_ACCOUNTS;
148+
connection: Connection,
149+
wallet: AnchorWallet,
150+
): Promise<StakeAccountPositions[]> => {
151+
const pythStakingClient = new PythStakingClient({ connection, wallet });
152+
return pythStakingClient.getAllStakeAccountPositions(wallet.publicKey);
148153
};
149154

150155
export const loadData = async (context: Context): Promise<Data> => {
151156
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
152157
// While using mocks we need to clone the MOCK_DATA object every time
153158
// `loadData` is called so that swr treats the response as changed and
154159
// triggers a rerender.
155-
return { ...MOCK_DATA[context.stakeAccount.publicKey]! };
160+
161+
const pythStakingClient = new PythStakingClient({
162+
connection: context.connection,
163+
wallet: context.wallet,
164+
});
165+
const stakeAccountPositions = context.stakeAccount;
166+
167+
const [stakeAccountCustody, publishers, ownerAtaAccount, currentEpoch] =
168+
await Promise.all([
169+
pythStakingClient.getStakeAccountCustody(stakeAccountPositions.address),
170+
pythStakingClient.getPublishers(),
171+
pythStakingClient.getOwnerPythAtaAccount(),
172+
getCurrentEpoch(context.connection),
173+
]);
174+
175+
const unlockSchedule = await pythStakingClient.getUnlockSchedule({
176+
stakeAccountPositions: stakeAccountPositions.address,
177+
});
178+
179+
const filterGovernancePositions = (positionState: PositionState) =>
180+
getAmountByTargetAndState({
181+
stakeAccountPositions,
182+
targetWithParameters: { voting: {} },
183+
positionState,
184+
epoch: currentEpoch,
185+
});
186+
const filterOISPositions = (
187+
publisher: PublicKey,
188+
positionState: PositionState,
189+
) =>
190+
getAmountByTargetAndState({
191+
stakeAccountPositions,
192+
targetWithParameters: { integrityPool: { publisher } },
193+
positionState,
194+
epoch: currentEpoch,
195+
});
196+
197+
return {
198+
lastSlash: undefined, // TODO
199+
availableRewards: 0n, // TODO
200+
expiringRewards: undefined, // TODO
201+
total: stakeAccountCustody.amount,
202+
governance: {
203+
warmup: filterGovernancePositions(PositionState.LOCKING),
204+
staked: filterGovernancePositions(PositionState.LOCKED),
205+
cooldown: filterGovernancePositions(PositionState.PREUNLOCKING),
206+
cooldown2: filterGovernancePositions(PositionState.UNLOCKED),
207+
},
208+
unlockSchedule,
209+
locked: unlockSchedule.reduce((sum, { amount }) => sum + amount, 0n),
210+
walletAmount: ownerAtaAccount.amount,
211+
integrityStakingPublishers: publishers.map(({ pubkey: publisher }) => ({
212+
apyHistory: [], // TODO
213+
isSelf: false, // TODO
214+
name: publisher.toString(),
215+
numFeeds: 0, // TODO
216+
poolCapacity: 100n, // TODO
217+
poolUtilization: 0n, // TODO
218+
publicKey: publisher.toString(),
219+
qualityRanking: 0, // TODO
220+
selfStake: 0n, // TODO
221+
positions: {
222+
warmup: filterOISPositions(publisher, PositionState.LOCKING),
223+
staked: filterOISPositions(publisher, PositionState.LOCKED),
224+
cooldown: filterOISPositions(publisher, PositionState.PREUNLOCKING),
225+
cooldown2: filterOISPositions(publisher, PositionState.UNLOCKED),
226+
},
227+
})),
228+
};
156229
};
157230

158231
export const loadAccountHistory = async (
159-
context: Context,
232+
_context: Context,
160233
): Promise<AccountHistory> => {
161234
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
162-
return [...MOCK_HISTORY[context.stakeAccount.publicKey]!];
235+
return MOCK_HISTORY["0x000000"]!;
163236
};
164237

165238
export const deposit = async (
166239
context: Context,
167240
amount: bigint,
168241
): Promise<void> => {
169-
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
170-
MOCK_DATA[context.stakeAccount.publicKey]!.total += amount;
171-
MOCK_DATA[context.stakeAccount.publicKey]!.walletAmount -= amount;
242+
const pythStakingClient = new PythStakingClient({
243+
connection: context.connection,
244+
wallet: context.wallet,
245+
});
246+
await pythStakingClient.depositTokensToStakeAccountCustody(
247+
context.stakeAccount.address,
248+
amount,
249+
);
172250
};
173251

174252
export const withdraw = async (
175253
context: Context,
176254
amount: bigint,
177255
): Promise<void> => {
178-
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
179-
MOCK_DATA[context.stakeAccount.publicKey]!.total -= amount;
180-
MOCK_DATA[context.stakeAccount.publicKey]!.walletAmount += amount;
256+
const pythStakingClient = new PythStakingClient({
257+
connection: context.connection,
258+
wallet: context.wallet,
259+
});
260+
await pythStakingClient.withdrawTokensFromStakeAccountCustody(
261+
context.stakeAccount.address,
262+
amount,
263+
);
181264
};
182265

183266
export const claim = async (context: Context): Promise<void> => {
184-
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
185-
MOCK_DATA[context.stakeAccount.publicKey]!.total +=
186-
MOCK_DATA[context.stakeAccount.publicKey]!.availableRewards;
187-
MOCK_DATA[context.stakeAccount.publicKey]!.availableRewards = 0n;
188-
MOCK_DATA[context.stakeAccount.publicKey]!.expiringRewards.amount = 0n;
267+
const pythStakingClient = new PythStakingClient({
268+
connection: context.connection,
269+
wallet: context.wallet,
270+
});
271+
await pythStakingClient.advanceDelegationRecord({
272+
stakeAccountPositions: context.stakeAccount.address,
273+
});
189274
};
190275

191276
export const stakeGovernance = async (
192277
context: Context,
193278
amount: bigint,
194279
): Promise<void> => {
195-
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
196-
MOCK_DATA[context.stakeAccount.publicKey]!.governance.warmup += amount;
280+
const pythStakingClient = new PythStakingClient({
281+
connection: context.connection,
282+
wallet: context.wallet,
283+
});
284+
await pythStakingClient.stakeToGovernance(
285+
context.stakeAccount.address,
286+
amount,
287+
);
197288
};
198289

199290
export const cancelWarmupGovernance = async (
200-
context: Context,
201-
amount: bigint,
291+
_context: Context,
292+
_amount: bigint,
202293
): Promise<void> => {
203294
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
204-
MOCK_DATA[context.stakeAccount.publicKey]!.governance.warmup -= amount;
205295
};
206296

207297
export const unstakeGovernance = async (
208-
context: Context,
209-
amount: bigint,
298+
_context: Context,
299+
_amount: bigint,
210300
): Promise<void> => {
211301
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
212-
MOCK_DATA[context.stakeAccount.publicKey]!.governance.staked -= amount;
213-
MOCK_DATA[context.stakeAccount.publicKey]!.governance.cooldown += amount;
214302
};
215303

216304
export const delegateIntegrityStaking = async (
217305
context: Context,
218306
publisherKey: string,
219307
amount: bigint,
220308
): Promise<void> => {
221-
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
222-
const publisher = MOCK_DATA[
223-
context.stakeAccount.publicKey
224-
]!.integrityStakingPublishers.find(
225-
(publisher) => publisher.publicKey === publisherKey,
226-
);
227-
if (publisher) {
228-
publisher.positions ||= {};
229-
publisher.positions.warmup = (publisher.positions.warmup ?? 0n) + amount;
230-
} else {
231-
throw new Error(`Invalid publisher key: "${publisherKey}"`);
232-
}
309+
const pythStakingClient = new PythStakingClient({
310+
connection: context.connection,
311+
wallet: context.wallet,
312+
});
313+
314+
await pythStakingClient.stakeToPublisher({
315+
stakeAccountPositions: context.stakeAccount.address,
316+
publisher: new PublicKey(publisherKey),
317+
amount,
318+
});
233319
};
234320

235321
export const cancelWarmupIntegrityStaking = async (
@@ -239,7 +325,7 @@ export const cancelWarmupIntegrityStaking = async (
239325
): Promise<void> => {
240326
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
241327
const publisher = MOCK_DATA[
242-
context.stakeAccount.publicKey
328+
context.stakeAccount.address.toString()
243329
]!.integrityStakingPublishers.find(
244330
(publisher) => publisher.publicKey === publisherKey,
245331
);
@@ -259,7 +345,7 @@ export const unstakeIntegrityStaking = async (
259345
): Promise<void> => {
260346
await new Promise((resolve) => setTimeout(resolve, MOCK_DELAY));
261347
const publisher = MOCK_DATA[
262-
context.stakeAccount.publicKey
348+
context.stakeAccount.address.toString()
263349
]!.integrityStakingPublishers.find(
264350
(publisher) => publisher.publicKey === publisherKey,
265351
);
@@ -308,11 +394,6 @@ export const getNextFullEpoch = (): Date => {
308394

309395
const MOCK_DELAY = 500;
310396

311-
const MOCK_STAKE_ACCOUNTS: StakeAccount[] = [
312-
{ publicKey: "0x000000" },
313-
{ publicKey: "0x111111" },
314-
];
315-
316397
const mkMockData = (isDouro: boolean): Data => ({
317398
total: 15_000_000n,
318399
availableRewards: 156_000n,
@@ -417,10 +498,7 @@ const mkMockData = (isDouro: boolean): Data => ({
417498
],
418499
});
419500

420-
const MOCK_DATA: Record<
421-
(typeof MOCK_STAKE_ACCOUNTS)[number]["publicKey"],
422-
Data
423-
> = {
501+
const MOCK_DATA: Record<string, Data> = {
424502
"0x000000": mkMockData(true),
425503
"0x111111": mkMockData(false),
426504
};
@@ -464,10 +542,7 @@ const mkMockHistory = (): AccountHistory => [
464542
},
465543
];
466544

467-
const MOCK_HISTORY: Record<
468-
(typeof MOCK_STAKE_ACCOUNTS)[number]["publicKey"],
469-
AccountHistory
470-
> = {
545+
const MOCK_HISTORY: Record<string, AccountHistory> = {
471546
"0x000000": mkMockHistory(),
472547
"0x111111": mkMockHistory(),
473548
};

apps/staking/src/components/AccountHistory/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ const useAccountHistoryData = () => {
135135
const apiContext = useApiContext();
136136

137137
const { data, isLoading, ...rest } = useSWR(
138-
`${apiContext.stakeAccount.publicKey}/history`,
138+
`${apiContext.stakeAccount.address.toBase58()}/history`,
139139
() => loadAccountHistory(apiContext),
140140
{
141141
refreshInterval: REFRESH_INTERVAL,

apps/staking/src/components/AccountSummary/index.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ type Props = {
2424
| undefined;
2525
walletAmount: bigint;
2626
availableRewards: bigint;
27-
expiringRewards: {
28-
amount: bigint;
29-
expiry: Date;
30-
};
27+
expiringRewards:
28+
| {
29+
amount: bigint;
30+
expiry: Date;
31+
}
32+
| undefined;
3133
availableToWithdraw: bigint;
3234
};
3335

@@ -137,14 +139,15 @@ export const AccountSummary = ({
137139
amount={availableRewards}
138140
description="Rewards you have earned but not yet claimed from the Integrity Staking program"
139141
action={<ClaimButton disabled={availableRewards === 0n} />}
140-
{...(expiringRewards.amount > 0n && {
141-
warning: (
142-
<>
143-
<Tokens>{expiringRewards.amount}</Tokens> will expire on{" "}
144-
{expiringRewards.expiry.toLocaleDateString()}
145-
</>
146-
),
147-
})}
142+
{...(expiringRewards !== undefined &&
143+
expiringRewards.amount > 0n && {
144+
warning: (
145+
<>
146+
<Tokens>{expiringRewards.amount}</Tokens> will expire on{" "}
147+
{expiringRewards.expiry.toLocaleDateString()}
148+
</>
149+
),
150+
})}
148151
/>
149152
</div>
150153
</div>

0 commit comments

Comments
 (0)