Skip to content

Commit 0863181

Browse files
authored
Merge pull request #2549 from pyth-network/cprussin/add-wallet-test-page-to-staking-app
feat(staking): add wallet test page to staking app
2 parents f922acb + fab2db6 commit 0863181

File tree

9 files changed

+283
-16
lines changed

9 files changed

+283
-16
lines changed

apps/staking/next.config.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,4 @@ export default {
6060
"https://web-api.pyth.network/publishers_ranking?cluster=pythnet",
6161
},
6262
],
63-
64-
redirects: () => [
65-
{
66-
source: "/test",
67-
destination: "https://staking-legacy.pyth.network/test",
68-
permanent: false,
69-
},
70-
],
7163
};

apps/staking/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
"@amplitude/analytics-browser": "catalog:",
2323
"@amplitude/plugin-autocapture-browser": "catalog:",
2424
"@bonfida/spl-name-service": "catalog:",
25+
"@coral-xyz/anchor": "catalog:",
2526
"@heroicons/react": "catalog:",
2627
"@next/third-parties": "catalog:",
2728
"@pythnetwork/hermes-client": "workspace:*",
2829
"@pythnetwork/known-publishers": "workspace:*",
30+
"@pythnetwork/solana-utils": "workspace:*",
2931
"@pythnetwork/staking-sdk": "workspace:*",
3032
"@react-hookz/web": "catalog:",
3133
"@solana/wallet-adapter-base": "catalog:",

apps/staking/src/app/test/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { WalletTester as default } from "../../components/WalletTester";
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
"use client";
2+
3+
import type { Idl } from "@coral-xyz/anchor";
4+
import { Program, AnchorProvider } from "@coral-xyz/anchor";
5+
import { WalletIcon } from "@heroicons/react/24/outline";
6+
import {
7+
TransactionBuilder,
8+
sendTransactions,
9+
} from "@pythnetwork/solana-utils";
10+
import type { AnchorWallet } from "@solana/wallet-adapter-react";
11+
import { useConnection } from "@solana/wallet-adapter-react";
12+
import { useWalletModal } from "@solana/wallet-adapter-react-ui";
13+
import { PublicKey, Connection } from "@solana/web3.js";
14+
import type { ComponentProps } from "react";
15+
import { useCallback, useState } from "react";
16+
17+
import WalletTesterIDL from "./wallet-tester-idl.json";
18+
import { StateType as ApiStateType, useApi } from "../../hooks/use-api";
19+
import { useData, StateType } from "../../hooks/use-data";
20+
import { useLogger } from "../../hooks/use-logger";
21+
import { useToast } from "../../hooks/use-toast";
22+
import { Button } from "../Button";
23+
24+
export const WalletTester = () => (
25+
<div className="grid size-full place-content-center">
26+
<div className="w-96 border border-neutral-600 p-10">
27+
<h1 className="mb-4 text-2xl font-medium text-neutral-300">
28+
Wallet Tester
29+
</h1>
30+
<WalletTesterContents />
31+
</div>
32+
</div>
33+
);
34+
35+
const WalletTesterContents = () => {
36+
const api = useApi();
37+
38+
switch (api.type) {
39+
case ApiStateType.WalletConnecting:
40+
case ApiStateType.WalletDisconnecting: {
41+
return <ConnectWallet isLoading />;
42+
}
43+
44+
case ApiStateType.NoWallet: {
45+
return <ConnectWallet />;
46+
}
47+
48+
case ApiStateType.NotLoaded:
49+
case ApiStateType.ErrorLoadingStakeAccounts:
50+
case ApiStateType.Loaded:
51+
case ApiStateType.LoadedNoStakeAccount:
52+
case ApiStateType.LoadingStakeAccounts: {
53+
return <WalletConnected wallet={api.wallet} />;
54+
}
55+
}
56+
};
57+
58+
const ConnectWallet = ({ isLoading }: { isLoading?: boolean | undefined }) => {
59+
const modal = useWalletModal();
60+
const showModal = useCallback(() => {
61+
modal.setVisible(true);
62+
}, [modal]);
63+
64+
return (
65+
<>
66+
<Description className="mb-10 text-neutral-400">
67+
Please connect your wallet to get started.
68+
</Description>
69+
<div className="flex justify-center">
70+
<Button
71+
className="px-10 py-4"
72+
size="nopad"
73+
isLoading={isLoading}
74+
{...(!isLoading && { onPress: showModal })}
75+
>
76+
{isLoading ? (
77+
"Loading..."
78+
) : (
79+
<>
80+
<WalletIcon className="size-4" />
81+
<div>Connect wallet</div>
82+
</>
83+
)}
84+
</Button>
85+
</div>
86+
</>
87+
);
88+
};
89+
90+
const WalletConnected = ({ wallet }: { wallet: AnchorWallet }) => {
91+
const { connection } = useConnection();
92+
93+
const testedStatus = useData(
94+
["wallet-tested", wallet.publicKey.toString()],
95+
() => getHasAlreadyTested(connection, wallet),
96+
{
97+
revalidateIfStale: false,
98+
revalidateOnFocus: false,
99+
revalidateOnReconnect: false,
100+
},
101+
);
102+
103+
switch (testedStatus.type) {
104+
case StateType.NotLoaded:
105+
case StateType.Loading: {
106+
return <Description>Loading...</Description>;
107+
}
108+
case StateType.Error: {
109+
return (
110+
<Description>
111+
Uh oh, we ran into an issue while checking if your wallet has been
112+
tested. Please reload and try again.
113+
</Description>
114+
);
115+
}
116+
case StateType.Loaded: {
117+
return testedStatus.data.hasTested ? (
118+
<p className="text-green-600">
119+
Your wallet has already been tested succesfully!
120+
</p>
121+
) : (
122+
<Tester wallet={wallet} />
123+
);
124+
}
125+
}
126+
};
127+
128+
const Tester = ({ wallet }: { wallet: AnchorWallet }) => {
129+
const logger = useLogger();
130+
const toast = useToast();
131+
const [tested, setTested] = useState(false);
132+
const { connection } = useConnection();
133+
const test = useCallback(() => {
134+
testWallet(connection, wallet)
135+
.then(() => {
136+
setTested(true);
137+
toast.success("Successfully tested wallet, thank you!");
138+
})
139+
.catch((error: unknown) => {
140+
logger.error(error);
141+
toast.error(error);
142+
});
143+
}, [setTested, logger, toast, wallet, connection]);
144+
145+
return tested ? (
146+
<p className="text-green-600">Your wallet has been tested succesfully!</p>
147+
) : (
148+
<>
149+
<Description>
150+
Please click the button below and accept the transaction in your wallet
151+
to test the browser wallet compatibility. You will need 0.001 SOL.
152+
</Description>
153+
<div className="flex justify-center">
154+
<Button className="px-10 py-4" size="nopad" onPress={test}>
155+
Click to test
156+
</Button>
157+
</div>
158+
</>
159+
);
160+
};
161+
162+
const getHasAlreadyTested = async (
163+
connection: Connection,
164+
wallet: AnchorWallet,
165+
) => {
166+
const receiptAddress = PublicKey.findProgramAddressSync(
167+
[wallet.publicKey.toBytes()],
168+
new PublicKey(WalletTesterIDL.address),
169+
)[0];
170+
const receipt = await connection.getAccountInfo(receiptAddress);
171+
return { hasTested: receipt !== null };
172+
};
173+
174+
const testWallet = async (connection: Connection, wallet: AnchorWallet) => {
175+
const walletTester = new Program(
176+
WalletTesterIDL as Idl,
177+
new AnchorProvider(connection, wallet),
178+
);
179+
const testMethod = walletTester.methods.test;
180+
if (testMethod) {
181+
return sendTransactions(
182+
await TransactionBuilder.batchIntoVersionedTransactions(
183+
wallet.publicKey,
184+
connection,
185+
[
186+
{
187+
instruction: await testMethod().instruction(),
188+
signers: [],
189+
},
190+
],
191+
{},
192+
),
193+
connection,
194+
wallet,
195+
);
196+
} else {
197+
throw new Error("No test method found in program");
198+
}
199+
};
200+
201+
const Description = (props: ComponentProps<"p">) => (
202+
<p className="mb-10 text-neutral-400" {...props} />
203+
);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"address": "tstPARXbQ5yxVkRU2UcZRbYphzbUEW6t5ihzpLaafgz",
3+
"metadata": {
4+
"name": "wallet_tester",
5+
"version": "1.0.0",
6+
"spec": "0.1.0",
7+
"description": "Created with Anchor"
8+
},
9+
"instructions": [
10+
{
11+
"name": "test",
12+
"discriminator": [163, 36, 134, 53, 232, 223, 146, 222],
13+
"accounts": [
14+
{
15+
"name": "payer",
16+
"writable": true,
17+
"signer": true
18+
},
19+
{
20+
"name": "test_receipt",
21+
"writable": true,
22+
"pda": {
23+
"seeds": [
24+
{
25+
"kind": "account",
26+
"path": "payer"
27+
}
28+
]
29+
}
30+
},
31+
{
32+
"name": "system_program",
33+
"address": "11111111111111111111111111111111"
34+
}
35+
],
36+
"args": []
37+
}
38+
]
39+
}

apps/staking/src/hooks/use-api.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { HermesClient } from "@pythnetwork/hermes-client";
44
import { PythnetClient, PythStakingClient } from "@pythnetwork/staking-sdk";
55
import { useLocalStorageValue } from "@react-hookz/web";
6+
import type { AnchorWallet } from "@solana/wallet-adapter-react";
67
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
78
import { Connection, PublicKey } from "@solana/web3.js";
89
import type { ComponentProps } from "react";
@@ -25,8 +26,6 @@ export enum StateType {
2526
}
2627

2728
const State = {
28-
[StateType.NotLoaded]: () => ({ type: StateType.NotLoaded as const }),
29-
3029
[StateType.NoWallet]: () => ({ type: StateType.NoWallet as const }),
3130

3231
[StateType.WalletDisconnecting]: () => ({
@@ -37,11 +36,18 @@ const State = {
3736
type: StateType.WalletConnecting as const,
3837
}),
3938

40-
[StateType.LoadingStakeAccounts]: () => ({
39+
[StateType.NotLoaded]: (wallet: AnchorWallet) => ({
40+
type: StateType.NotLoaded as const,
41+
wallet,
42+
}),
43+
44+
[StateType.LoadingStakeAccounts]: (wallet: AnchorWallet) => ({
4145
type: StateType.LoadingStakeAccounts as const,
46+
wallet,
4247
}),
4348

4449
[StateType.LoadedNoStakeAccount]: (
50+
wallet: AnchorWallet,
4551
isMainnet: boolean,
4652
client: PythStakingClient,
4753
pythnetClient: PythnetClient,
@@ -58,9 +64,11 @@ const State = {
5864
const account = await api.createStakeAccountAndDeposit(client, amount);
5965
return onCreateAccount(account);
6066
},
67+
wallet,
6168
}),
6269

6370
[StateType.Loaded]: (
71+
wallet: AnchorWallet,
6472
isMainnet: boolean,
6573
client: PythStakingClient,
6674
pythnetClient: PythnetClient,
@@ -95,6 +103,7 @@ const State = {
95103
allAccounts,
96104
selectAccount,
97105
dashboardDataCacheKey,
106+
wallet,
98107

99108
loadData: () =>
100109
api.loadData(
@@ -121,9 +130,15 @@ const State = {
121130
},
122131

123132
[StateType.ErrorLoadingStakeAccounts]: (
133+
wallet: AnchorWallet,
124134
error: LoadStakeAccountsError,
125135
reset: () => void,
126-
) => ({ type: StateType.ErrorLoadingStakeAccounts as const, error, reset }),
136+
) => ({
137+
type: StateType.ErrorLoadingStakeAccounts as const,
138+
error,
139+
reset,
140+
wallet,
141+
}),
127142
};
128143

129144
export type States = {
@@ -219,13 +234,16 @@ const useApiContext = (
219234
} else if (wallet.connected && pythStakingClient) {
220235
switch (stakeAccounts.type) {
221236
case DataStateType.NotLoaded: {
222-
return State[StateType.NotLoaded]();
237+
return State[StateType.NotLoaded](pythStakingClient.wallet);
223238
}
224239
case DataStateType.Loading: {
225-
return State[StateType.LoadingStakeAccounts]();
240+
return State[StateType.LoadingStakeAccounts](
241+
pythStakingClient.wallet,
242+
);
226243
}
227244
case DataStateType.Error: {
228245
return State[StateType.ErrorLoadingStakeAccounts](
246+
pythStakingClient.wallet,
229247
new LoadStakeAccountsError(stakeAccounts.error),
230248
stakeAccounts.reset,
231249
);
@@ -248,6 +266,7 @@ const useApiContext = (
248266
localStorageValue.set(firstAccount.toBase58());
249267
}
250268
return State[StateType.Loaded](
269+
pythStakingClient.wallet,
251270
isMainnet,
252271
pythStakingClient,
253272
pythnetClient,
@@ -262,6 +281,7 @@ const useApiContext = (
262281
);
263282
} else {
264283
return State[StateType.LoadedNoStakeAccount](
284+
pythStakingClient.wallet,
265285
isMainnet,
266286
pythStakingClient,
267287
pythnetClient,

governance/pyth_staking_sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"typescript": "catalog:"
3535
},
3636
"dependencies": {
37-
"@coral-xyz/anchor": "^0.30.1",
37+
"@coral-xyz/anchor": "catalog:",
3838
"@pythnetwork/client": "catalog:",
3939
"@pythnetwork/solana-utils": "workspace:*",
4040
"@solana/spl-governance": "^0.3.28",

0 commit comments

Comments
 (0)