Skip to content

feat(staking): add wallet test page to staking app #2549

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions apps/staking/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,4 @@ export default {
"https://web-api.pyth.network/publishers_ranking?cluster=pythnet",
},
],

redirects: () => [
{
source: "/test",
destination: "https://staking-legacy.pyth.network/test",
permanent: false,
},
],
};
2 changes: 2 additions & 0 deletions apps/staking/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
"@amplitude/analytics-browser": "catalog:",
"@amplitude/plugin-autocapture-browser": "catalog:",
"@bonfida/spl-name-service": "catalog:",
"@coral-xyz/anchor": "catalog:",
"@heroicons/react": "catalog:",
"@next/third-parties": "catalog:",
"@pythnetwork/hermes-client": "workspace:*",
"@pythnetwork/known-publishers": "workspace:*",
"@pythnetwork/solana-utils": "workspace:*",
"@pythnetwork/staking-sdk": "workspace:*",
"@react-hookz/web": "catalog:",
"@solana/wallet-adapter-base": "catalog:",
Expand Down
1 change: 1 addition & 0 deletions apps/staking/src/app/test/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { WalletTester as default } from "../../components/WalletTester";
203 changes: 203 additions & 0 deletions apps/staking/src/components/WalletTester/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
"use client";

import type { Idl } from "@coral-xyz/anchor";
import { Program, AnchorProvider } from "@coral-xyz/anchor";
import { WalletIcon } from "@heroicons/react/24/outline";
import {
TransactionBuilder,
sendTransactions,
} from "@pythnetwork/solana-utils";
import type { AnchorWallet } from "@solana/wallet-adapter-react";
import { useConnection } from "@solana/wallet-adapter-react";
import { useWalletModal } from "@solana/wallet-adapter-react-ui";
import { PublicKey, Connection } from "@solana/web3.js";
import type { ComponentProps } from "react";
import { useCallback, useState } from "react";

import WalletTesterIDL from "./wallet-tester-idl.json";
import { StateType as ApiStateType, useApi } from "../../hooks/use-api";
import { useData, StateType } from "../../hooks/use-data";
import { useLogger } from "../../hooks/use-logger";
import { useToast } from "../../hooks/use-toast";
import { Button } from "../Button";

export const WalletTester = () => (
<div className="grid size-full place-content-center">
<div className="w-96 border border-neutral-600 p-10">
<h1 className="mb-4 text-2xl font-medium text-neutral-300">
Wallet Tester
</h1>
<WalletTesterContents />
</div>
</div>
);

const WalletTesterContents = () => {
const api = useApi();

switch (api.type) {
case ApiStateType.WalletConnecting:
case ApiStateType.WalletDisconnecting: {
return <ConnectWallet isLoading />;
}

case ApiStateType.NoWallet: {
return <ConnectWallet />;
}

case ApiStateType.NotLoaded:
case ApiStateType.ErrorLoadingStakeAccounts:
case ApiStateType.Loaded:
case ApiStateType.LoadedNoStakeAccount:
case ApiStateType.LoadingStakeAccounts: {
return <WalletConnected wallet={api.wallet} />;
}
}
};

const ConnectWallet = ({ isLoading }: { isLoading?: boolean | undefined }) => {
const modal = useWalletModal();
const showModal = useCallback(() => {
modal.setVisible(true);
}, [modal]);

return (
<>
<Description className="mb-10 text-neutral-400">
Please connect your wallet to get started.
</Description>
<div className="flex justify-center">
<Button
className="px-10 py-4"
size="nopad"
isLoading={isLoading}
{...(!isLoading && { onPress: showModal })}
>
{isLoading ? (
"Loading..."
) : (
<>
<WalletIcon className="size-4" />
<div>Connect wallet</div>
</>
)}
</Button>
</div>
</>
);
};

const WalletConnected = ({ wallet }: { wallet: AnchorWallet }) => {
const { connection } = useConnection();

const testedStatus = useData(
["wallet-tested", wallet.publicKey.toString()],
() => getHasAlreadyTested(connection, wallet),
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
},
);

switch (testedStatus.type) {
case StateType.NotLoaded:
case StateType.Loading: {
return <Description>Loading...</Description>;
}
case StateType.Error: {
return (
<Description>
Uh oh, we ran into an issue while checking if your wallet has been
tested. Please reload and try again.
</Description>
);
}
case StateType.Loaded: {
return testedStatus.data.hasTested ? (
<p className="text-green-600">
Your wallet has already been tested succesfully!
</p>
) : (
<Tester wallet={wallet} />
);
}
}
};

const Tester = ({ wallet }: { wallet: AnchorWallet }) => {
const logger = useLogger();
const toast = useToast();
const [tested, setTested] = useState(false);
const { connection } = useConnection();
const test = useCallback(() => {
testWallet(connection, wallet)
.then(() => {
setTested(true);
toast.success("Successfully tested wallet, thank you!");
})
.catch((error: unknown) => {
logger.error(error);
toast.error(error);
});
}, [setTested, logger, toast, wallet, connection]);

return tested ? (
<p className="text-green-600">Your wallet has been tested succesfully!</p>
) : (
<>
<Description>
Please click the button below and accept the transaction in your wallet
to test the browser wallet compatibility. You will need 0.001 SOL.
</Description>
<div className="flex justify-center">
<Button className="px-10 py-4" size="nopad" onPress={test}>
Click to test
</Button>
</div>
</>
);
};

const getHasAlreadyTested = async (
connection: Connection,
wallet: AnchorWallet,
) => {
const receiptAddress = PublicKey.findProgramAddressSync(
[wallet.publicKey.toBytes()],
new PublicKey(WalletTesterIDL.address),
)[0];
const receipt = await connection.getAccountInfo(receiptAddress);
return { hasTested: receipt !== null };
};

const testWallet = async (connection: Connection, wallet: AnchorWallet) => {
const walletTester = new Program(
WalletTesterIDL as Idl,
new AnchorProvider(connection, wallet),
);
const testMethod = walletTester.methods.test;
if (testMethod) {
return sendTransactions(
await TransactionBuilder.batchIntoVersionedTransactions(
wallet.publicKey,
connection,
[
{
instruction: await testMethod().instruction(),
signers: [],
},
],
{},
),
connection,
wallet,
);
} else {
throw new Error("No test method found in program");
}
};

const Description = (props: ComponentProps<"p">) => (
<p className="mb-10 text-neutral-400" {...props} />
);
39 changes: 39 additions & 0 deletions apps/staking/src/components/WalletTester/wallet-tester-idl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"address": "tstPARXbQ5yxVkRU2UcZRbYphzbUEW6t5ihzpLaafgz",
"metadata": {
"name": "wallet_tester",
"version": "1.0.0",
"spec": "0.1.0",
"description": "Created with Anchor"
},
"instructions": [
{
"name": "test",
"discriminator": [163, 36, 134, 53, 232, 223, 146, 222],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "test_receipt",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "payer"
}
]
}
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
}
],
"args": []
}
]
}
32 changes: 26 additions & 6 deletions apps/staking/src/hooks/use-api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { HermesClient } from "@pythnetwork/hermes-client";
import { PythnetClient, PythStakingClient } from "@pythnetwork/staking-sdk";
import { useLocalStorageValue } from "@react-hookz/web";
import type { AnchorWallet } from "@solana/wallet-adapter-react";
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { Connection, PublicKey } from "@solana/web3.js";
import type { ComponentProps } from "react";
Expand All @@ -25,8 +26,6 @@ export enum StateType {
}

const State = {
[StateType.NotLoaded]: () => ({ type: StateType.NotLoaded as const }),

[StateType.NoWallet]: () => ({ type: StateType.NoWallet as const }),

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

[StateType.LoadingStakeAccounts]: () => ({
[StateType.NotLoaded]: (wallet: AnchorWallet) => ({
type: StateType.NotLoaded as const,
wallet,
}),

[StateType.LoadingStakeAccounts]: (wallet: AnchorWallet) => ({
type: StateType.LoadingStakeAccounts as const,
wallet,
}),

[StateType.LoadedNoStakeAccount]: (
wallet: AnchorWallet,
isMainnet: boolean,
client: PythStakingClient,
pythnetClient: PythnetClient,
Expand All @@ -58,9 +64,11 @@ const State = {
const account = await api.createStakeAccountAndDeposit(client, amount);
return onCreateAccount(account);
},
wallet,
}),

[StateType.Loaded]: (
wallet: AnchorWallet,
isMainnet: boolean,
client: PythStakingClient,
pythnetClient: PythnetClient,
Expand Down Expand Up @@ -95,6 +103,7 @@ const State = {
allAccounts,
selectAccount,
dashboardDataCacheKey,
wallet,

loadData: () =>
api.loadData(
Expand All @@ -121,9 +130,15 @@ const State = {
},

[StateType.ErrorLoadingStakeAccounts]: (
wallet: AnchorWallet,
error: LoadStakeAccountsError,
reset: () => void,
) => ({ type: StateType.ErrorLoadingStakeAccounts as const, error, reset }),
) => ({
type: StateType.ErrorLoadingStakeAccounts as const,
error,
reset,
wallet,
}),
};

export type States = {
Expand Down Expand Up @@ -219,13 +234,16 @@ const useApiContext = (
} else if (wallet.connected && pythStakingClient) {
switch (stakeAccounts.type) {
case DataStateType.NotLoaded: {
return State[StateType.NotLoaded]();
return State[StateType.NotLoaded](pythStakingClient.wallet);
}
case DataStateType.Loading: {
return State[StateType.LoadingStakeAccounts]();
return State[StateType.LoadingStakeAccounts](
pythStakingClient.wallet,
);
}
case DataStateType.Error: {
return State[StateType.ErrorLoadingStakeAccounts](
pythStakingClient.wallet,
new LoadStakeAccountsError(stakeAccounts.error),
stakeAccounts.reset,
);
Expand All @@ -248,6 +266,7 @@ const useApiContext = (
localStorageValue.set(firstAccount.toBase58());
}
return State[StateType.Loaded](
pythStakingClient.wallet,
isMainnet,
pythStakingClient,
pythnetClient,
Expand All @@ -262,6 +281,7 @@ const useApiContext = (
);
} else {
return State[StateType.LoadedNoStakeAccount](
pythStakingClient.wallet,
isMainnet,
pythStakingClient,
pythnetClient,
Expand Down
2 changes: 1 addition & 1 deletion governance/pyth_staking_sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"typescript": "catalog:"
},
"dependencies": {
"@coral-xyz/anchor": "^0.30.1",
"@coral-xyz/anchor": "catalog:",
"@pythnetwork/client": "catalog:",
"@pythnetwork/solana-utils": "workspace:*",
"@solana/spl-governance": "^0.3.28",
Expand Down
Loading