Skip to content

Commit 4f90945

Browse files
Harsh-H-Shahclaudeamilz
authored
feat(nft-meta-data-pointer): migrate frontend to @solana/kit (#673)
* feat(nft-meta-data-pointer): migrate frontend to @solana/kit Follow-up to #657, where dev-jodee asked for web3.js to be dropped in favor of kit. Replaces @anchor-lang/core with a Codama-generated @solana/kit client (from a real anchor-build-extracted IDL, not the hand-maintained one), and @solana/wallet-adapter-react with @solana/connector for wallet connection. @magicblock-labs/gum-react-sdk (the session-key feature) is pinned to @solana/web3.js and @solana/wallet-adapter-react in its own published API and predates kit, so full removal isn't possible while keeping that feature. That stays as a single, documented legacy boundary (utils/legacyBridge.ts, contexts/SessionProvider.tsx) bridging the one connected wallet into the shape gum-sdk needs, rather than running two separately-connected wallet instances. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: commit refilled energy back to player state Greptile caught this on review: the refill-tick loop computed incremented energy/lastLogin into local variables but never wrote them back to playerState, so the displayed energy counter stayed stale between real account-change notifications. The previous BN-based version got away with mutating playerState.energy in place and relying on the same effects setTimePassed/setEnergyNextIn calls to force a re-render - that trick got dropped when the local let variables were introduced. Now commits the computed values via setPlayerState explicitly, only when something changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: clear energy countdown once max energy is reached Greptile flagged this on review. Pre-existing behavior (the same early return existed before this migration) surfaced by having Greptile look closely at code already being touched here: once playerState.energy hits MAX_ENERGY, the interval returned early without ever calling setEnergyNextIn(0), so the last computed countdown stayed on screen next to a full energy bar indefinitely. Splits the guard so the max-energy case explicitly clears the countdown before returning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(nft-meta-data-pointer): consolidate RPC config and replace DAS client with kit Point declare_id! and Anchor.toml at the program actually deployed on devnet, so a fresh anchor build reproduces the checked-in IDL. Route the wallet connector and the session-key Connection through the same RPC_URL the kit client uses, so NEXT_PUBLIC_RPC moves every cluster connection together instead of leaving two endpoints hardcoded. Reset playerDataPDA and check the abort signal when the signer changes, so a click during the async PDA derivation cannot pair a new signer with the previous wallet's player account. Replace the axios-based WrappedConnection with a DAS client built on kit's createJsonRpcApi, following the pattern in solana-foundation/templates kit/nextjs-das-nfts. The DAS endpoint carries a provider credential, so it now comes from NEXT_PUBLIC_DAS_RPC with no default and the NFT list reports when it is unset. Move the remaining web3.js conversions behind legacyBridge and add a typecheck script, which needs generate-client to have run. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: amilz <85324096+amilz@users.noreply.github.com>
1 parent ce1d843 commit 4f90945

30 files changed

Lines changed: 2614 additions & 1505 deletions

tokens/token-2022/nft-meta-data-pointer/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ yarn install
3434
yarn dev
3535
```
3636

37+
Copy `app/.env.example` to `app/.env.local` first. Listing the NFTs a wallet
38+
owns goes through the Metaplex Digital Asset Standard API, which is a separate
39+
service from the base Solana RPC and requires an endpoint from a provider such
40+
as Helius, Shyft, QuickNode, or Triton. Without `NEXT_PUBLIC_DAS_RPC` set, the
41+
app runs but the NFT list stays empty.
42+
3743
# Minting the NFT
3844

3945
For the creating of the NFT we perform the following steps:

tokens/token-2022/nft-meta-data-pointer/anchor/Anchor.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ resolution = true
77
skip-lint = false
88

99
[programs.localnet]
10-
extension_nft = "4nSA2i5bEiBWLLDuZfqTYKwTAwakLEbW2KJXH9cZtDU3"
10+
extension_nft = "H31ofLpWqeAzF2Pg54HSPQGYifJad843tTJg8vCYVoh3"
1111

1212
[provider]
1313
cluster = "localnet"

tokens/token-2022/nft-meta-data-pointer/anchor/programs/extension_nft/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub mod instructions;
77
pub mod state;
88
use instructions::*;
99

10-
declare_id!("4nSA2i5bEiBWLLDuZfqTYKwTAwakLEbW2KJXH9cZtDU3");
10+
declare_id!("H31ofLpWqeAzF2Pg54HSPQGYifJad843tTJg8vCYVoh3");
1111

1212
#[program]
1313
pub mod extension_nft {
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Cluster endpoint used for account reads, transaction sending, the wallet
2+
# connector, and the session-key Connection.
3+
NEXT_PUBLIC_RPC=https://rpc.magicblock.app/devnet
4+
NEXT_PUBLIC_WSS_RPC=wss://rpc.magicblock.app/devnet
5+
6+
# Metaplex Digital Asset Standard endpoint, used to list the NFTs a wallet owns.
7+
# DAS is a separate service from the base Solana JSON-RPC, so this is not the
8+
# same URL as NEXT_PUBLIC_RPC. Providers embed auth differently: Helius and
9+
# Shyft take an API key as a query param, QuickNode and Triton as a path
10+
# segment. Copy this file to .env.local and fill in your own endpoint - the NFT
11+
# list stays empty until you do.
12+
NEXT_PUBLIC_DAS_RPC=

tokens/token-2022/nft-meta-data-pointer/app/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ yarn-error.log*
3333
# typescript
3434
*.tsbuildinfo
3535
next-env.d.ts
36+
37+
# codama-generated kit client
38+
/generated

tokens/token-2022/nft-meta-data-pointer/app/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
22

3+
## Solana client stack
4+
5+
The on-chain client is [`@solana/kit`](https://github.com/anza-xyz/kit), generated from `idl/extension_nft.json` via [Codama](https://github.com/codama-idl/codama) (`pnpm generate-client`, wired as a `predev`/`prebuild` step) into a gitignored `generated/` directory — see `scripts/generate-client.ts`. Wallet connection is [`@solana/connector`](https://www.npmjs.com/package/@solana/connector) (wallet-standard, no per-wallet adapter packages).
6+
7+
One legacy pocket remains: the session-key feature (`@magicblock-labs/gum-react-sdk`) is pinned to `@solana/web3.js` and `@solana/wallet-adapter-react` in its own published API and predates kit. Rather than run a second, separately-connected wallet-adapter-react instance alongside the kit-native wallet connection, `contexts/SessionProvider.tsx` and `utils/legacyBridge.ts` bridge the single kit-connected wallet into the `AnchorWallet` shape gum-sdk needs. `@solana/web3.js` and `@solana/wallet-adapter-react` stay in `package.json`, but only for that one boundary — nothing else in the app touches them.
8+
39
## Getting Started
410

511
First, run the development server:

tokens/token-2022/nft-meta-data-pointer/app/components/ChopTreeButton.tsx

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { Button, HStack, VStack } from '@chakra-ui/react';
22
import { useSessionWallet } from '@magicblock-labs/gum-react-sdk';
3-
import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
4-
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
5-
import { PublicKey, SystemProgram } from '@solana/web3.js';
3+
import { useKitTransactionSigner } from '@solana/connector/react';
4+
import { createNoopSigner, type Address } from '@solana/kit';
65
import Image from 'next/image';
76
import { useCallback, useState } from 'react';
87
import { useGameState } from '@/contexts/GameStateProvider';
98
import { useNftState } from '@/contexts/NftProvider';
10-
import { GAME_DATA_SEED, gameDataPDA, program } from '@/utils/anchor';
9+
import { getChopTreeInstructionAsync } from '@/generated/instructions';
10+
import { findNftAuthorityPda } from '@/generated/pdas';
11+
import { useSendInstruction } from '@/hooks/useSendInstruction';
12+
import { GAME_DATA_SEED } from '@/utils/anchor';
13+
import { kitInstructionToLegacyTransaction } from '@/utils/legacyBridge';
1114

1215
const ChopTreeButton = () => {
13-
const { publicKey, sendTransaction } = useWallet();
14-
const { connection } = useConnection();
16+
const { signer } = useKitTransactionSigner();
17+
const sendInstruction = useSendInstruction();
1518
const sessionWallet = useSessionWallet();
1619
const { gameState, playerDataPDA } = useGameState();
1720
const [isLoadingSession, setIsLoadingSession] = useState(false);
@@ -27,7 +30,7 @@ const ChopTreeButton = () => {
2730
}
2831
setTransactionCounter(transactionCounter + 1);
2932

30-
const nftAuthority = await PublicKey.findProgramAddress([Buffer.from('nft_authority')], program.programId);
33+
const [nftAuthority] = await findNftAuthorityPda();
3134

3235
if (nftState == null) {
3336
window.alert('Load NFT state first');
@@ -36,11 +39,10 @@ const ChopTreeButton = () => {
3639
}
3740

3841
let nft = null;
39-
4042
for (let i = 0; i < nftState.items.length; i++) {
4143
try {
4244
const nftData = nftState.items[i];
43-
if (nftData.authorities[0] === nftAuthority[0].toBase58()) {
45+
if (nftData.authorities[0] === nftAuthority) {
4446
nft = nftData;
4547
}
4648
console.log('NFT data', nftData);
@@ -57,19 +59,17 @@ const ChopTreeButton = () => {
5759
}
5860

5961
try {
60-
const transaction = await program.methods
61-
.chopTree(GAME_DATA_SEED, transactionCounter)
62-
.accounts({
63-
player: playerDataPDA,
64-
gameData: gameDataPDA,
65-
signer: sessionWallet.publicKey,
66-
sessionToken: sessionWallet.sessionToken,
67-
nftAuthority: nftAuthority[0],
68-
mint: nft.id,
69-
systemProgram: SystemProgram.programId,
70-
tokenProgram: TOKEN_2022_PROGRAM_ID,
71-
})
72-
.transaction();
62+
const instruction = await getChopTreeInstructionAsync({
63+
sessionToken: sessionWallet.sessionToken as Address,
64+
player: playerDataPDA,
65+
signer: createNoopSigner(sessionWallet.publicKey.toBase58() as Address),
66+
mint: nft.id as Address,
67+
nftAuthority,
68+
levelSeed: GAME_DATA_SEED,
69+
counter: transactionCounter,
70+
});
71+
72+
const transaction = kitInstructionToLegacyTransaction(instruction);
7373

7474
const txids = await sessionWallet.signAndSendTransaction?.(transaction);
7575

@@ -86,10 +86,10 @@ const ChopTreeButton = () => {
8686
}, [sessionWallet, nftState, playerDataPDA, transactionCounter]);
8787

8888
const onChopMainWalletClick = useCallback(async () => {
89-
if (!publicKey || !playerDataPDA) return;
89+
if (!signer || !playerDataPDA) return;
9090

9191
setIsLoadingMainWallet(true);
92-
const nftAuthority = await PublicKey.findProgramAddress([Buffer.from('nft_authority')], program.programId);
92+
const [nftAuthority] = await findNftAuthorityPda();
9393

9494
if (nftState == null) {
9595
window.alert('Load NFT state first');
@@ -104,9 +104,9 @@ const ChopTreeButton = () => {
104104
const nftData = nftState.items[i];
105105
const authority = nftData.authorities[0];
106106
const authorityAddress = typeof authority === 'string' ? authority : authority.address;
107-
console.log(`${authorityAddress} == ${nftAuthority[0].toBase58()}`);
107+
console.log(`${authorityAddress} == ${nftAuthority}`);
108108

109-
if (authorityAddress === nftAuthority[0].toBase58()) {
109+
if (authorityAddress === nftAuthority) {
110110
nft = nftData;
111111
}
112112
console.log('NFT data', nftData);
@@ -125,34 +125,28 @@ const ChopTreeButton = () => {
125125
const nftAuthorityAddress =
126126
typeof nft.authorities[0] === 'string' ? nft.authorities[0] : nft.authorities[0].address;
127127
console.log('NFTid', nft.id, 'NFT authority', nftAuthorityAddress);
128-
const transaction = await program.methods
129-
.chopTree(GAME_DATA_SEED, transactionCounter)
130-
.accounts({
131-
player: playerDataPDA,
132-
gameData: gameDataPDA,
133-
signer: publicKey,
134-
sessionToken: null,
135-
nftAuthority: nftAuthority[0].toBase58(),
136-
mint: nft.id,
137-
systemProgram: SystemProgram.programId,
138-
tokenProgram: TOKEN_2022_PROGRAM_ID,
139-
})
140-
.transaction();
141-
142-
const txSig = await sendTransaction(transaction, connection, {
143-
skipPreflight: true,
128+
129+
const instruction = await getChopTreeInstructionAsync({
130+
player: playerDataPDA,
131+
signer,
132+
mint: nft.id as Address,
133+
nftAuthority: nftAuthorityAddress as Address,
134+
levelSeed: GAME_DATA_SEED,
135+
counter: transactionCounter,
144136
});
137+
138+
const txSig = await sendInstruction(instruction, signer);
145139
console.log(`https://explorer.solana.com/tx/${txSig}?cluster=devnet`);
146140
} catch (error) {
147141
console.log('error', `Chopping failed! ${error instanceof Error ? error.message : String(error)}`);
148142
} finally {
149143
setIsLoadingMainWallet(false);
150144
}
151-
}, [publicKey, playerDataPDA, connection, nftState, transactionCounter, sendTransaction]);
145+
}, [signer, playerDataPDA, nftState, transactionCounter, sendInstruction]);
152146

153147
return (
154148
<>
155-
{publicKey && gameState && (
149+
{signer && gameState && (
156150
<VStack>
157151
<Image src="/Beaver.png" alt="Energy Icon" width={64} height={64} />
158152
<HStack>

tokens/token-2022/nft-meta-data-pointer/app/components/DisplayGameState.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { HStack, Text, VStack } from '@chakra-ui/react';
2-
import { useWallet } from '@solana/wallet-adapter-react';
2+
import { useKitTransactionSigner } from '@solana/connector/react';
33
import Image from 'next/image';
44
import { useGameState } from '@/contexts/GameStateProvider';
55
import { TOTAL_WOOD_AVAILABLE } from '@/utils/anchor';
66

77
const DisplayPlayerData = () => {
8-
const { publicKey } = useWallet();
8+
const { signer } = useKitTransactionSigner();
99
const { gameState, nextEnergyIn, totalWoodAvailable } = useGameState();
1010

1111
return (
1212
<>
13-
{gameState && publicKey && (
13+
{gameState && signer && (
1414
<HStack justifyContent="center" spacing={4}>
1515
<HStack>
1616
<Image src="/Wood.png" alt="Wood Icon" width={64} height={64} />

tokens/token-2022/nft-meta-data-pointer/app/components/DisplayNfts.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useWallet } from '@solana/wallet-adapter-react';
1+
import { useKitTransactionSigner } from '@solana/connector/react';
22
import Image from 'next/image';
33
import { useState } from 'react';
44
import { useNftState } from '@/contexts/NftProvider';
@@ -14,7 +14,7 @@ export class Nft {
1414
}
1515

1616
const DisplayNfts = () => {
17-
const { publicKey } = useWallet();
17+
const { signer } = useKitTransactionSigner();
1818
const { nftState } = useNftState();
1919
const [showItems, setShowItems] = useState(false);
2020

@@ -46,7 +46,7 @@ const DisplayNfts = () => {
4646

4747
return (
4848
<>
49-
{nftState && publicKey && (
49+
{nftState && signer && (
5050
<div>
5151
<button type="button" onClick={handleButtonClick}>
5252
Show NFTs

tokens/token-2022/nft-meta-data-pointer/app/components/InitPlayerButton.tsx

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,43 @@
11
import { Button } from '@chakra-ui/react';
2-
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
3-
import { SystemProgram } from '@solana/web3.js';
2+
import { useKitTransactionSigner } from '@solana/connector/react';
43
import { useCallback, useState } from 'react';
54
import { useGameState } from '@/contexts/GameStateProvider';
6-
import { GAME_DATA_SEED, gameDataPDA, program } from '@/utils/anchor';
5+
import { getInitPlayerInstructionAsync } from '@/generated/instructions';
6+
import { useSendInstruction } from '@/hooks/useSendInstruction';
7+
import { GAME_DATA_SEED } from '@/utils/anchor';
78

89
const InitPlayerButton = () => {
9-
const { publicKey, sendTransaction } = useWallet();
10-
const { connection } = useConnection();
10+
const { signer } = useKitTransactionSigner();
11+
const sendInstruction = useSendInstruction();
1112
const [isLoading, setIsLoading] = useState(false);
1213
const { gameState, playerDataPDA } = useGameState();
1314

1415
// Init player button click handler
1516
const handleClick = useCallback(async () => {
16-
if (!publicKey || !playerDataPDA) return;
17+
if (!signer || !playerDataPDA) return;
1718

1819
setIsLoading(true);
1920

2021
try {
21-
const transaction = await program.methods
22-
.initPlayer(GAME_DATA_SEED)
23-
.accounts({
24-
player: playerDataPDA,
25-
gameData: gameDataPDA,
26-
signer: publicKey,
27-
systemProgram: SystemProgram.programId,
28-
})
29-
.transaction();
30-
31-
const txSig = await sendTransaction(transaction, connection, {
32-
skipPreflight: true,
22+
const instruction = await getInitPlayerInstructionAsync({
23+
player: playerDataPDA,
24+
signer,
25+
levelSeed: GAME_DATA_SEED,
3326
});
3427

28+
const txSig = await sendInstruction(instruction, signer);
29+
3530
console.log(`https://explorer.solana.com/tx/${txSig}?cluster=devnet`);
3631
} catch (error) {
3732
console.log(error);
3833
} finally {
3934
setIsLoading(false); // set loading state back to false
4035
}
41-
}, [publicKey, playerDataPDA, connection, sendTransaction]);
36+
}, [signer, playerDataPDA, sendInstruction]);
4237

4338
return (
4439
<>
45-
{!gameState && publicKey && (
40+
{!gameState && signer && (
4641
<Button onClick={handleClick} isLoading={isLoading}>
4742
Init Player
4843
</Button>

0 commit comments

Comments
 (0)