Skip to content
Open
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
6 changes: 6 additions & 0 deletions tokens/token-2022/nft-meta-data-pointer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ yarn install
yarn dev
```

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

# Minting the NFT

For the creating of the NFT we perform the following steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ resolution = true
skip-lint = false

[programs.localnet]
extension_nft = "4nSA2i5bEiBWLLDuZfqTYKwTAwakLEbW2KJXH9cZtDU3"
extension_nft = "H31ofLpWqeAzF2Pg54HSPQGYifJad843tTJg8vCYVoh3"

[provider]
cluster = "localnet"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub mod instructions;
pub mod state;
use instructions::*;

declare_id!("4nSA2i5bEiBWLLDuZfqTYKwTAwakLEbW2KJXH9cZtDU3");
declare_id!("H31ofLpWqeAzF2Pg54HSPQGYifJad843tTJg8vCYVoh3");

#[program]
pub mod extension_nft {
Expand Down
12 changes: 12 additions & 0 deletions tokens/token-2022/nft-meta-data-pointer/app/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Cluster endpoint used for account reads, transaction sending, the wallet
# connector, and the session-key Connection.
NEXT_PUBLIC_RPC=https://rpc.magicblock.app/devnet
NEXT_PUBLIC_WSS_RPC=wss://rpc.magicblock.app/devnet

# Metaplex Digital Asset Standard endpoint, used to list the NFTs a wallet owns.
# DAS is a separate service from the base Solana JSON-RPC, so this is not the
# same URL as NEXT_PUBLIC_RPC. Providers embed auth differently: Helius and
# Shyft take an API key as a query param, QuickNode and Triton as a path
# segment. Copy this file to .env.local and fill in your own endpoint - the NFT
# list stays empty until you do.
NEXT_PUBLIC_DAS_RPC=
3 changes: 3 additions & 0 deletions tokens/token-2022/nft-meta-data-pointer/app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# codama-generated kit client
/generated
6 changes: 6 additions & 0 deletions tokens/token-2022/nft-meta-data-pointer/app/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,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).

## Solana client stack

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).

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.

## Getting Started

First, run the development server:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { Button, HStack, VStack } from '@chakra-ui/react';
import { useSessionWallet } from '@magicblock-labs/gum-react-sdk';
import { TOKEN_2022_PROGRAM_ID } from '@solana/spl-token';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { PublicKey, SystemProgram } from '@solana/web3.js';
import { useKitTransactionSigner } from '@solana/connector/react';
import { createNoopSigner, type Address } from '@solana/kit';
import Image from 'next/image';
import { useCallback, useState } from 'react';
import { useGameState } from '@/contexts/GameStateProvider';
import { useNftState } from '@/contexts/NftProvider';
import { GAME_DATA_SEED, gameDataPDA, program } from '@/utils/anchor';
import { getChopTreeInstructionAsync } from '@/generated/instructions';
import { findNftAuthorityPda } from '@/generated/pdas';
import { useSendInstruction } from '@/hooks/useSendInstruction';
import { GAME_DATA_SEED } from '@/utils/anchor';
import { kitInstructionToLegacyTransaction } from '@/utils/legacyBridge';

const ChopTreeButton = () => {
const { publicKey, sendTransaction } = useWallet();
const { connection } = useConnection();
const { signer } = useKitTransactionSigner();
const sendInstruction = useSendInstruction();
const sessionWallet = useSessionWallet();
const { gameState, playerDataPDA } = useGameState();
const [isLoadingSession, setIsLoadingSession] = useState(false);
Expand All @@ -27,7 +30,7 @@ const ChopTreeButton = () => {
}
setTransactionCounter(transactionCounter + 1);

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

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

let nft = null;

for (let i = 0; i < nftState.items.length; i++) {
try {
const nftData = nftState.items[i];
if (nftData.authorities[0] === nftAuthority[0].toBase58()) {
if (nftData.authorities[0] === nftAuthority) {
nft = nftData;
}
console.log('NFT data', nftData);
Expand All @@ -57,19 +59,17 @@ const ChopTreeButton = () => {
}

try {
const transaction = await program.methods
.chopTree(GAME_DATA_SEED, transactionCounter)
.accounts({
player: playerDataPDA,
gameData: gameDataPDA,
signer: sessionWallet.publicKey,
sessionToken: sessionWallet.sessionToken,
nftAuthority: nftAuthority[0],
mint: nft.id,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_2022_PROGRAM_ID,
})
.transaction();
const instruction = await getChopTreeInstructionAsync({
sessionToken: sessionWallet.sessionToken as Address,
player: playerDataPDA,
signer: createNoopSigner(sessionWallet.publicKey.toBase58() as Address),
mint: nft.id as Address,
nftAuthority,
levelSeed: GAME_DATA_SEED,
counter: transactionCounter,
});

const transaction = kitInstructionToLegacyTransaction(instruction);

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

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

const onChopMainWalletClick = useCallback(async () => {
if (!publicKey || !playerDataPDA) return;
if (!signer || !playerDataPDA) return;

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

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

if (authorityAddress === nftAuthority[0].toBase58()) {
if (authorityAddress === nftAuthority) {
nft = nftData;
}
console.log('NFT data', nftData);
Expand All @@ -125,34 +125,28 @@ const ChopTreeButton = () => {
const nftAuthorityAddress =
typeof nft.authorities[0] === 'string' ? nft.authorities[0] : nft.authorities[0].address;
console.log('NFTid', nft.id, 'NFT authority', nftAuthorityAddress);
const transaction = await program.methods
.chopTree(GAME_DATA_SEED, transactionCounter)
.accounts({
player: playerDataPDA,
gameData: gameDataPDA,
signer: publicKey,
sessionToken: null,
nftAuthority: nftAuthority[0].toBase58(),
mint: nft.id,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_2022_PROGRAM_ID,
})
.transaction();

const txSig = await sendTransaction(transaction, connection, {
skipPreflight: true,

const instruction = await getChopTreeInstructionAsync({
player: playerDataPDA,
signer,
mint: nft.id as Address,
nftAuthority: nftAuthorityAddress as Address,
levelSeed: GAME_DATA_SEED,
counter: transactionCounter,
});

const txSig = await sendInstruction(instruction, signer);
console.log(`https://explorer.solana.com/tx/${txSig}?cluster=devnet`);
} catch (error) {
console.log('error', `Chopping failed! ${error instanceof Error ? error.message : String(error)}`);
} finally {
setIsLoadingMainWallet(false);
}
}, [publicKey, playerDataPDA, connection, nftState, transactionCounter, sendTransaction]);
}, [signer, playerDataPDA, nftState, transactionCounter, sendInstruction]);

return (
<>
{publicKey && gameState && (
{signer && gameState && (
<VStack>
<Image src="/Beaver.png" alt="Energy Icon" width={64} height={64} />
<HStack>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { HStack, Text, VStack } from '@chakra-ui/react';
import { useWallet } from '@solana/wallet-adapter-react';
import { useKitTransactionSigner } from '@solana/connector/react';
import Image from 'next/image';
import { useGameState } from '@/contexts/GameStateProvider';
import { TOTAL_WOOD_AVAILABLE } from '@/utils/anchor';

const DisplayPlayerData = () => {
const { publicKey } = useWallet();
const { signer } = useKitTransactionSigner();
const { gameState, nextEnergyIn, totalWoodAvailable } = useGameState();

return (
<>
{gameState && publicKey && (
{gameState && signer && (
<HStack justifyContent="center" spacing={4}>
<HStack>
<Image src="/Wood.png" alt="Wood Icon" width={64} height={64} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useWallet } from '@solana/wallet-adapter-react';
import { useKitTransactionSigner } from '@solana/connector/react';
import Image from 'next/image';
import { useState } from 'react';
import { useNftState } from '@/contexts/NftProvider';
Expand All @@ -14,7 +14,7 @@ export class Nft {
}

const DisplayNfts = () => {
const { publicKey } = useWallet();
const { signer } = useKitTransactionSigner();
const { nftState } = useNftState();
const [showItems, setShowItems] = useState(false);

Expand Down Expand Up @@ -46,7 +46,7 @@ const DisplayNfts = () => {

return (
<>
{nftState && publicKey && (
{nftState && signer && (
<div>
<button type="button" onClick={handleButtonClick}>
Show NFTs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,43 @@
import { Button } from '@chakra-ui/react';
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { SystemProgram } from '@solana/web3.js';
import { useKitTransactionSigner } from '@solana/connector/react';
import { useCallback, useState } from 'react';
import { useGameState } from '@/contexts/GameStateProvider';
import { GAME_DATA_SEED, gameDataPDA, program } from '@/utils/anchor';
import { getInitPlayerInstructionAsync } from '@/generated/instructions';
import { useSendInstruction } from '@/hooks/useSendInstruction';
import { GAME_DATA_SEED } from '@/utils/anchor';

const InitPlayerButton = () => {
const { publicKey, sendTransaction } = useWallet();
const { connection } = useConnection();
const { signer } = useKitTransactionSigner();
const sendInstruction = useSendInstruction();
const [isLoading, setIsLoading] = useState(false);
const { gameState, playerDataPDA } = useGameState();

// Init player button click handler
const handleClick = useCallback(async () => {
if (!publicKey || !playerDataPDA) return;
if (!signer || !playerDataPDA) return;

setIsLoading(true);

try {
const transaction = await program.methods
.initPlayer(GAME_DATA_SEED)
.accounts({
player: playerDataPDA,
gameData: gameDataPDA,
signer: publicKey,
systemProgram: SystemProgram.programId,
})
.transaction();

const txSig = await sendTransaction(transaction, connection, {
skipPreflight: true,
const instruction = await getInitPlayerInstructionAsync({
player: playerDataPDA,
signer,
levelSeed: GAME_DATA_SEED,
});

const txSig = await sendInstruction(instruction, signer);

console.log(`https://explorer.solana.com/tx/${txSig}?cluster=devnet`);
} catch (error) {
console.log(error);
} finally {
setIsLoading(false); // set loading state back to false
}
}, [publicKey, playerDataPDA, connection, sendTransaction]);
}, [signer, playerDataPDA, sendInstruction]);

return (
<>
{!gameState && publicKey && (
{!gameState && signer && (
<Button onClick={handleClick} isLoading={isLoading}>
Init Player
</Button>
Expand Down
Loading
Loading