diff --git a/tokens/token-2022/nft-meta-data-pointer/README.md b/tokens/token-2022/nft-meta-data-pointer/README.md index fda273848..24a0118f9 100644 --- a/tokens/token-2022/nft-meta-data-pointer/README.md +++ b/tokens/token-2022/nft-meta-data-pointer/README.md @@ -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: diff --git a/tokens/token-2022/nft-meta-data-pointer/anchor/Anchor.toml b/tokens/token-2022/nft-meta-data-pointer/anchor/Anchor.toml index a0e3084c1..9e83bc303 100644 --- a/tokens/token-2022/nft-meta-data-pointer/anchor/Anchor.toml +++ b/tokens/token-2022/nft-meta-data-pointer/anchor/Anchor.toml @@ -7,7 +7,7 @@ resolution = true skip-lint = false [programs.localnet] -extension_nft = "4nSA2i5bEiBWLLDuZfqTYKwTAwakLEbW2KJXH9cZtDU3" +extension_nft = "H31ofLpWqeAzF2Pg54HSPQGYifJad843tTJg8vCYVoh3" [provider] cluster = "localnet" diff --git a/tokens/token-2022/nft-meta-data-pointer/anchor/programs/extension_nft/src/lib.rs b/tokens/token-2022/nft-meta-data-pointer/anchor/programs/extension_nft/src/lib.rs index c2eb860c6..2af49fe1e 100644 --- a/tokens/token-2022/nft-meta-data-pointer/anchor/programs/extension_nft/src/lib.rs +++ b/tokens/token-2022/nft-meta-data-pointer/anchor/programs/extension_nft/src/lib.rs @@ -7,7 +7,7 @@ pub mod instructions; pub mod state; use instructions::*; -declare_id!("4nSA2i5bEiBWLLDuZfqTYKwTAwakLEbW2KJXH9cZtDU3"); +declare_id!("H31ofLpWqeAzF2Pg54HSPQGYifJad843tTJg8vCYVoh3"); #[program] pub mod extension_nft { diff --git a/tokens/token-2022/nft-meta-data-pointer/app/.env.example b/tokens/token-2022/nft-meta-data-pointer/app/.env.example new file mode 100644 index 000000000..1511278e9 --- /dev/null +++ b/tokens/token-2022/nft-meta-data-pointer/app/.env.example @@ -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= diff --git a/tokens/token-2022/nft-meta-data-pointer/app/.gitignore b/tokens/token-2022/nft-meta-data-pointer/app/.gitignore index 8f322f0d8..b8408c56b 100644 --- a/tokens/token-2022/nft-meta-data-pointer/app/.gitignore +++ b/tokens/token-2022/nft-meta-data-pointer/app/.gitignore @@ -33,3 +33,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# codama-generated kit client +/generated diff --git a/tokens/token-2022/nft-meta-data-pointer/app/README.md b/tokens/token-2022/nft-meta-data-pointer/app/README.md index 965a1228c..a6e7fbc8c 100644 --- a/tokens/token-2022/nft-meta-data-pointer/app/README.md +++ b/tokens/token-2022/nft-meta-data-pointer/app/README.md @@ -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: diff --git a/tokens/token-2022/nft-meta-data-pointer/app/components/ChopTreeButton.tsx b/tokens/token-2022/nft-meta-data-pointer/app/components/ChopTreeButton.tsx index 0208a9c0d..687d69805 100644 --- a/tokens/token-2022/nft-meta-data-pointer/app/components/ChopTreeButton.tsx +++ b/tokens/token-2022/nft-meta-data-pointer/app/components/ChopTreeButton.tsx @@ -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); @@ -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'); @@ -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); @@ -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); @@ -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'); @@ -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); @@ -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 && ( Energy Icon diff --git a/tokens/token-2022/nft-meta-data-pointer/app/components/DisplayGameState.tsx b/tokens/token-2022/nft-meta-data-pointer/app/components/DisplayGameState.tsx index 0f8a80443..77fc3ed7b 100644 --- a/tokens/token-2022/nft-meta-data-pointer/app/components/DisplayGameState.tsx +++ b/tokens/token-2022/nft-meta-data-pointer/app/components/DisplayGameState.tsx @@ -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 && ( Wood Icon diff --git a/tokens/token-2022/nft-meta-data-pointer/app/components/DisplayNfts.tsx b/tokens/token-2022/nft-meta-data-pointer/app/components/DisplayNfts.tsx index c07ce3867..6fbccba75 100644 --- a/tokens/token-2022/nft-meta-data-pointer/app/components/DisplayNfts.tsx +++ b/tokens/token-2022/nft-meta-data-pointer/app/components/DisplayNfts.tsx @@ -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'; @@ -14,7 +14,7 @@ export class Nft { } const DisplayNfts = () => { - const { publicKey } = useWallet(); + const { signer } = useKitTransactionSigner(); const { nftState } = useNftState(); const [showItems, setShowItems] = useState(false); @@ -46,7 +46,7 @@ const DisplayNfts = () => { return ( <> - {nftState && publicKey && ( + {nftState && signer && (
diff --git a/tokens/token-2022/nft-meta-data-pointer/app/components/MintNftButton.tsx b/tokens/token-2022/nft-meta-data-pointer/app/components/MintNftButton.tsx index 9061fa862..6787cff99 100644 --- a/tokens/token-2022/nft-meta-data-pointer/app/components/MintNftButton.tsx +++ b/tokens/token-2022/nft-meta-data-pointer/app/components/MintNftButton.tsx @@ -1,68 +1,53 @@ import { Button, HStack, VStack } from '@chakra-ui/react'; -import { ASSOCIATED_TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; -import { useConnection, useWallet } from '@solana/wallet-adapter-react'; -import { Keypair, PublicKey, SYSVAR_RENT_PUBKEY, SystemProgram } from '@solana/web3.js'; +import { useKitTransactionSigner } from '@solana/connector/react'; +import { generateKeyPairSigner } from '@solana/kit'; +import { findAssociatedTokenPda, TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; import Image from 'next/image'; import { useCallback, useState } from 'react'; import { useGameState } from '@/contexts/GameStateProvider'; -import { program } from '@/utils/anchor'; +import { getMintNftInstructionAsync } from '@/generated/instructions'; +import { useSendInstruction } from '@/hooks/useSendInstruction'; const MintNftButton = () => { - const { publicKey, sendTransaction } = useWallet(); - const { connection } = useConnection(); + const { signer } = useKitTransactionSigner(); + const sendInstruction = useSendInstruction(); const { gameState, playerDataPDA } = useGameState(); const [isLoadingMainWallet, showSpinner] = useState(false); const onMintNftClick = useCallback(async () => { - if (!publicKey || !playerDataPDA) return; + if (!signer || !playerDataPDA) return; showSpinner(true); try { - const nftAuthority = await PublicKey.findProgramAddress([Buffer.from('nft_authority')], program.programId); + const mint = await generateKeyPairSigner(); - const mint = new Keypair(); - - const destinationTokenAccount = getAssociatedTokenAddressSync( - mint.publicKey, - publicKey, - false, - TOKEN_2022_PROGRAM_ID, - ); - - const transaction = await program.methods - .mintNft() - .accounts({ - signer: publicKey, - systemProgram: SystemProgram.programId, - tokenProgram: TOKEN_2022_PROGRAM_ID, - tokenAccount: destinationTokenAccount, - mint: mint.publicKey, - rent: SYSVAR_RENT_PUBKEY, - associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, - nftAuthority: nftAuthority[0], - }) - .signers([mint]) - .transaction(); - - console.log('transaction', transaction); + const [tokenAccount] = await findAssociatedTokenPda({ + owner: signer.address, + mint: mint.address, + tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + }); - const txSig = await sendTransaction(transaction, connection, { - signers: [mint], - skipPreflight: true, + const instruction = await getMintNftInstructionAsync({ + signer, + tokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + tokenAccount, + mint, }); + const txSig = await sendInstruction(instruction, signer); + console.log(`https://explorer.solana.com/tx/${txSig}?cluster=devnet`); } catch (error) { console.log('error', `Minting failed! ${error instanceof Error ? error.message : String(error)}`); } finally { showSpinner(false); } - }, [publicKey, playerDataPDA, connection, sendTransaction]); + }, [signer, playerDataPDA, sendInstruction]); return ( <> - {publicKey && gameState && ( + {signer && gameState && ( Energy Icon diff --git a/tokens/token-2022/nft-meta-data-pointer/app/components/RequestAirdrop.tsx b/tokens/token-2022/nft-meta-data-pointer/app/components/RequestAirdrop.tsx index aa8f2c77d..56bebe946 100644 --- a/tokens/token-2022/nft-meta-data-pointer/app/components/RequestAirdrop.tsx +++ b/tokens/token-2022/nft-meta-data-pointer/app/components/RequestAirdrop.tsx @@ -1,33 +1,39 @@ import { Button, Text } from '@chakra-ui/react'; -import { useConnection, useWallet } from '@solana/wallet-adapter-react'; -import { LAMPORTS_PER_SOL } from '@solana/web3.js'; -import { useCallback, useEffect, useState } from 'react'; +import { useKitTransactionSigner } from '@solana/connector/react'; +import { airdropFactory, lamports } from '@solana/kit'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { rpc, rpcSubscriptions } from '@/utils/anchor'; + +const LAMPORTS_PER_SOL = 1_000_000_000n; const RequestAirdrop = () => { - const { publicKey } = useWallet(); - const { connection } = useConnection(); + const { signer } = useKitTransactionSigner(); const [balance, setBalance] = useState(0); const [isLoading, setIsLoading] = useState(false); + const airdrop = useMemo(() => airdropFactory({ rpc, rpcSubscriptions }), []); const getBalance = useCallback(async () => { - if (!publicKey) return; - const balance = await connection.getBalance(publicKey, 'confirmed'); - setBalance(balance / LAMPORTS_PER_SOL); - }, [publicKey, connection]); + if (!signer) return; + const { value: lamportsBalance } = await rpc.getBalance(signer.address, { commitment: 'confirmed' }).send(); + setBalance(Number(lamportsBalance) / Number(LAMPORTS_PER_SOL)); + }, [signer]); const onClick = useCallback(async () => { setIsLoading(true); - if (!publicKey) return; + if (!signer) return; try { - const txSig = await connection.requestAirdrop(publicKey, LAMPORTS_PER_SOL); - await connection.confirmTransaction(txSig); - getBalance(); + await airdrop({ + commitment: 'confirmed', + recipientAddress: signer.address, + lamports: lamports(LAMPORTS_PER_SOL), + }); + await getBalance(); } catch (error) { alert(error instanceof Error ? error.message : String(error)); } finally { setIsLoading(false); } - }, [publicKey, connection, getBalance]); + }, [signer, airdrop, getBalance]); useEffect(() => { getBalance(); @@ -35,7 +41,7 @@ const RequestAirdrop = () => { return ( <> - {publicKey && + {signer && (balance <= 0 ? (