## Section 4 ## TODO: Gage video 4.4.1(NEEDS LINK) ### STAKING AND UI FOR THE BUILDOORS Here we go, let's make some progress on our buildoors NFT project. We have three things we want to accomplish in this core: 1. **Build the UI for our Staking page** This is what we're aiming for: ![](https://i.imgur.com/j75QTYE.png) Note that the squares that say “STAKING 4 DAYS” and “READY TO STAKE” wouldn’t be shown at the same time. Just show the one relevant to the current staking state of the NFT. Mock data where necessary and just get the UI looking roughly how you want it. And just note that your UI doesn’t need to be exactly like this. Please personalize it. 2. **Add the actual staking to our program** Remember, we did some work to store state but the program isn’t actually staking the NFT or minting BLD tokens yet. We'll fix that! 3. Once the program is fully ready to go, it’s time to **go back to the UI and get it working.** Specifically, the “claim $BLD,” “stake buildoor,” and “unstake buildoor” buttons should invoke the relevant instructions on the staking program. - - - - - - - As always, give this a shot independently. It’s not a trivial task so it may take a few hours or more. Once you’ve either finished or feel like you’re at your wit’s end, feel free to watch the upcoming videos walking through one possible solution in the next lessons. ## TODO: Gage video 4.4.2(NEEDS LINK) ### STAKING AND UI WALKTHROUGH #### Styling additions Back to building out more of the UI, first we'll add some colors to the theme in our app file (/<project-name>/pages/_app.tsx). ``` const colors = { background: "#1F1F1F", accent: "#833BBE", bodyText: "rgba(255, 255, 255, 0.75)", secondaryPurple: "#CB8CFF", containerBg: "rgba(255, 255, 255, 0.1)", containerBgSecondary: "rgba(255, 255, 255, 0.05)", buttonGreen: "#7EFFA7", } ``` #### NewMint routing We'll navigate to the *NewMint* file (/<project-name>/pages/newMint.tsx) to implement the `handleClick` function which will route to a new page upon staking. First, let's call `useRouter`, don't forget check for those pesky imports. `const router = useRouter()` Now jump into the event of this async function, and route to our new page, which we'll call `stake`. We'll also pass long the image since we already got it from the image source, so we don't have to load it again. ``` const handleClick: MouseEventHandler<HTMLButtonElement> = useCallback( async (event) => { router.push(`/stake?mint=${mint}&imageSrc=${metadata?.image}`) }, [router, mint, metadata] ) ``` Alas, it is currently a dead path and would give us an error, so let's go create the actual page. This will be a new file in the pages directory (/<project-name>/pages/stake.tsx). #### Stake landing page, left half Let's create a NextPage for `Stake` and make sure the 'next' library import happens. ``` const Stake: NextPage<StakeProps> = ({ mint, imageSrc }) => { return( <div></div> ) } ``` Let's create those all important props. ``` interface StakeProps { mint: PublicKey imageSrc: string } ``` Alright, we're moving alone here, so... a quick check with `npm run dev`, to make sure the front-end is rendering properly. If you've been doing a lot of minting, you may want to reset your candy machine. 🍬📠 Things are looking *niccceeee*. **A brief break from the front end**...it is a best practice is to create environment variables which you can use in your front-end. Create an `env.local` file at the top level directory, and use this format `NEXT_PUBLIC_<name of what you want>` to name your variables, which will then it will get injected into the browser side code, so then you can use it in your files. Then go on to replace the hard-coded keys in your code. **And back to the stake page**...let's go address what we actually want to render on the page. We'll use a number of items from Chakra, so make sure your imports are autocompleting, or add them manually. If you're a front-end guru, feel free to evolve the design here, otherwise just follow along with my beautiful pixel skills. 👾👾👾 A lot of this is similar to what we've done before for our other pages, here are a couple of items to pay attention to: 1. There is a staking check with `isStaking` that affects whether the page will say "STAKING" or "UNSTAKED". This requires a `useState` which is initially set to `false`. `const [isStaking, setIsStaking] = useState(false)` 2. We want to display the level of the staker, so another useState is needed. `const [level, setLevel] = useState(1)` Let's do another `npm run dev`...and ah, yes, we need some props so the page can display an image when we first visit, so let's make sure we call getInitialProps at the bottom of the file: ``` Stake.getInitialProps = async ({ query }: any) => { const { mint, imageSrc } = query if (!mint || !imageSrc) throw { error: "no mint" } try { const mintPubkey = new PublicKey(mint) return { mint: mintPubkey, imageSrc: imageSrc } } catch { throw { error: "invalid mint" } } } ``` #### Stake landing page, right half && the Stake Options Display component Alright, we have the left half of the page mostly done, let's now focus on the right side, we need another VStack which will have some separate logic in it, for what needs to be displayed. Soooo, let's create a separate component called `StakeOptionsDisplay` (/<project-name>/components/StakeOptionsDisplay.tsx). An obvious check here will be whether the buildoor is staking or not, we can start with that check, and build out the VStack. ``` export const StakeOptionsDisplay = ({ isStaking, }: { isStaking: boolean }) => { return( ) } ``` While you follow along the design code, the props we'll be checking in various parts are: 1. isStaking will either display number of days staking, or "Ready to stake." 2. daysStaked, as a number 3. totalEarned, as a number 4. claimable, as a number Here's the final product of what needs to be rendered, this is for those of you who prefer to paste the front-end code :P ``` return ( <VStack bgColor="containerBg" borderRadius="20px" padding="20px 40px" spacing={5} > <Text bgColor="containerBgSecondary" padding="4px 8px" borderRadius="20px" color="bodyText" as="b" fontSize="sm" > {isStaking ? `STAKING ${daysStaked} DAY${daysStaked === 1 ? "" : "S"}` : "READY TO STAKE"} </Text> <VStack spacing={-1}> <Text color="white" as="b" fontSize="4xl"> {isStaking ? `${totalEarned} $BLD` : "0 $BLD"} </Text> <Text color="bodyText"> {isStaking ? `${claimable} $BLD earned` : "earn $BLD by staking"} </Text> </VStack> <Button onClick={isStaking ? handleClaim : handleStake} bgColor="buttonGreen" width="200px" > <Text as="b">{isStaking ? "claim $BLD" : "stake buildoor"}</Text> </Button> {isStaking ? <Button onClick={handleUnstake}>unstake</Button> : null} </VStack> ) ``` As you've noticed, we need to build functions for `handleStake` and `handleClaim`, and `handleUnstake` -- we'll come back to these later in this core. ...and back to the stake file (/<project-name>/pages/stake.tsx) to import this component, and its necessary props. #### Gear and Loot box component Finally, let's build another component for the Gear and Loot boxes, we can call it ItemBox (/<project-name>/components/ItemBox.tsx). This is a relatively simple one, just follow along with the video, and feel free to compare your code with this. ``` import { Center } from "@chakra-ui/react" import { ReactNode } from "react" export const ItemBox = ({ children, bgColor, }: { children: ReactNode bgColor?: string }) => { return ( <Center height="120px" width="120px" bgColor={bgColor || "containerBg"} borderRadius="10px" > {children} </Center> ) } ``` That's it, feel free to move things around, design it as you'd like. Next we'll jump into staking program and add token stuff to it. Nice work, we know this is getting more dense, and has a lot more detailed work -- take your time, review the code, and hit us up in Discord if you're not grokking something. ## TODO: Gage video 4.4.3(NEEDS LINK) ### WALKTHROUGH OF TOKEN PROGRAM #### Minting, Staking and more... Alright, wow wow wow, we've come a long, let's get back to the NFT Staking Program. Today, we're gonna add all the token program interaction that's required to mint reward tokens to stakers, and to actually perform the staking operations. Unlike before, we're moving off of the Solana Playground, so we'll be doing all of this locally. Feel free to start with this code: [starter repo: solutions-sans-tokens branch](https://github.com/Unboxed-Software/solana-nft-staking-program/tree/solution-sans-tokens). You'll notice a couple of different things here. There's now a 'TS' folder which has everything we previously had in our client project, in the Solana Playground. One import modification is that in (/<project-name>/src/ts/src/utils/constants.ts), the `PROGRAM_ID` is being read from projects keypair. ``` const string = fs.readFileSync( "../target/deploy/solana_nft_staking_program-keypair.json", "utf8" ) ... export const PROGRAM_ID = Keypair.fromSecretKey(secretKey).publicKey ``` ok, Ready! Let's get going, first change into the *TS* directory, and run *npm run start* -- hopefully you've already done *cargo build bpf* and *solana deploy*, and your clusters are set to be the same, if yes to all that, it should get up and running. You should see that stakes, redeems, and unstakes printing to the console. Patience young padawan, this will take a minute or two. Assuming no errors 🎉, let's hop into the processor file: (/<project-name>/src/processor.rs). First, let's address some imports with the following use statements: ``` use mpl_token_metadata::ID as mpl_metadata_program_id; use spl_token::ID as spl_token_program_id; ``` Also, add `invoke` to *solana_program::program::{invoke_signed}* import. Hop on down to the `process_stake` function, here we will make our first changes. Get used to this, it'll happen often, we will find ourselves adding accounts, many accounts, to many places...so, time to add some accounts, which will allow us to actually work with the token program. ``` let nft_mint = next_account_info(account_info_iter)?; let nft_edition = next_account_info(account_info_iter)?; let stake_state = next_account_info(account_info_iter)?; let program_authority = next_account_info(account_info_iter)?; let token_program = next_account_info(account_info_iter)?; let metadata_program = next_account_info(account_info_iter)?; ``` #### Delegating and Freezing -- Staking Next, we need to add this program as a delegate for our NFT, delegating authority of our NFT so the program can submit transactions on our behalf. ``` msg!("Approving delegation"); invoke( &spl_token::instruction::approve( &spl_token_program_id, nft_token_account.key, program_authority.key, user.key, &[user.key], 1, )?, &[ nft_token_account.clone(), program_authority.clone(), user.clone(), token_program.clone(), ], )?; ``` Now we can move onto the actual freezing of the token. We're not actually changing ownership of the token, simply freezing it so nothing can be done with the token while it is staking. Before we do that, we need to derive the pda for the program authority. In short, we're using a PDA on the program, to be the authority that is delegated as the authority on the token mint, for being able to freeze the account. Don't forget to throw in those checks to make sure the PDAs are being derived. ``` let (delegated_auth_pda, delegate_bump) = Pubkey::find_program_address(&[b"authority"], program_id); if delegated_auth_pda != *program_authority.key { msg!("Invalid seeds for PDA"); return Err(StakeError::InvalidPda.into()); } ``` Back to the freezing itself, unlike the delegration approval, this one uses `invoke_signed` as it is signing from our program. ``` msg!("freezing NFT token account"); invoke_signed( &mpl_token_metadata::instruction::freeze_delegated_account( mpl_metadata_program_id, *program_authority.key, *nft_token_account.key, *nft_edition.key, *nft_mint.key, ), &[ program_authority.clone(), nft_token_account.clone(), nft_edition.clone(), nft_mint.clone(), metadata_program.clone(), ], &[&[b"authority", &[delegate_bump]]], )?; ``` This is a PDA of our program that now has the authority to freeze the token. 🧊 That's that, let's hop on over to the typescript file (/<project-name>/ts/src/utils/instruction.rs) and add more accounts (see, I told ya, add more accounts and add more accounts and...) to the `createStakingInstruction` function, to get this working. You want to match the accounts we have in the `process_stake` function in the (/<project-name>/src/processor.rs) file, let's make sure to add : ``` nftMint: PublicKey, nftEdition: PublicKey, tokenProgram: PublicKey, metadataProgram: PublicKey, ``` Next we add all those to the accounts below, in the right order, in the `TransactionInstruction`. The order matters. ...but first, pull in the authority account: ``` const [delegateAuthority] = PublicKey.findProgramAddressSync( [Buffer.from("authority")], programId ) ``` There are a total of 5 new accounts you need to, again, make sure they are in order. Additionally, check to see which are writable and which are signers. ``` ... { pubkey: nftMint, isWritable: false, isSigner: false, }, { pubkey: nftEdition, isWritable: false, isSigner: false, }, ... { pubkey: delegateAuthority, isWritable: true, isSigner: false, }, { pubkey: tokenProgram, isWritable: false, isSigner: false, }, { pubkey: metadataProgram, isWritable: false, isSigner: false, }, ``` #### Testing our staking functionality Next, hop on over to the index file (/<project-name>/ts/src/index.rs), to add the same matching accounts where the `stakeInstruction` is created, in the `testStaking` function. Here are the 4 additions: ``` nft.mintAddress, nft.masterEditionAddress, TOKEN_PROGRAM_ID, METADATA_PROGRAM_ID, ``` ```import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"``` Time to test it our progress, make sure you're inside the `ts` director and do an `npm run start`. Assuming no errors, let's pop back into the `processor.rs` file and add similar data to our `process_redeem` function. #### Delegating and Freezing -- Redeeming First, guess what, we add accounts -- there will be 4 of them!! ``` let stake_mint = next_account_info(account_info_iter)?; let stake_authority = next_account_info(account_info_iter)?; let user_stake_ata = next_account_info(account_info_iter)?; let token_program = next_account_info(account_info_iter)?; ``` Back to validation for some of these new accounts. Let's derive our `stake_auth_pda`, then the validation for the pda with a custom error. ``` let (stake_auth_pda, auth_bump) = Pubkey::find_program_address(&[b"mint"], program_id); if *stake_authority.key != stake_auth_pda { msg!("Invalid stake mint authority!"); return Err(StakeError::InvalidPda.into()); } ``` Scroll down a bit, after we figure out the `redeem_amount`, we will call an `invoke_signed`, to call the token program, to mint tokens. We need the various keys for the instruction, and then the required accounts, and finally the seeds for the auth. Don't forget to propogate the error with the `?` or the red squigglies lines won't leave you alone. ``` invoke_signed( &spl_token::instruction::mint_to( token_program.key, stake_mint.key, user_stake_ata.key, stake_authority.key, &[stake_authority.key], redeem_amount.try_into().unwrap(), )?, &[ stake_mint.clone(), user_stake_ata.clone(), stake_authority.clone(), token_program.clone(), ], &[&[b"mint", &[auth_bump]]], )?; ``` That should handle the minting in this file, but we have to go add the new accounts on the client side. We hop back into the `instruction.ts` file from earlier, scroll down to `createRedeemInstruction` to add the accounts below. ``` mint: PublicKey, userStakeATA: PublicKey, tokenProgram: PublicKey, ``` Now, remember, some accounts are derived, in this instance, it's the authority account, so we don't need to add it manually. Then hop down to the `TransactionInstruction` itself, first we derive the `mintAuth`. ``` const [mintAuth] = PublicKey.findProgramAddressSync( [Buffer.from("mint")], programId ) ``` Next hop into the `return new TransactionInstruction` to add the associated accounts, and whether they are writable and/or signable. Here are the 4 we need to add -- and remember, the order matters. ``` { pubkey: mint, isWritable: true, isSigner: false, }, { pubkey: mintAuth, isWritable: false, isSigner: false, }, { pubkey: userStakeATA, isWritable: true, isSigner: false, }, { pubkey: tokenProgram, isSigner: false, isWritable: false, }, ``` That should be everything we need for redeeming. We finally need to hop back into the same `index.ts` file, and make sure we're calling this properly, but it's a bit involved, so first let's go back into `processor.rs` and finish the `process_unstake` function. #### Delegating and Freezing -- UnStaking Process unstake is basically combining everything we just did for staking and redeeming, so it'll require all of the accounts we just worked with. Here are all the accounts we need to add: ``` let nft_mint = next_account_info(account_info_iter)?; let nft_edition = next_account_info(account_info_iter)?; ... (stake_state should be here from our previous code) let program_authority = next_account_info(account_info_iter)?; let stake_mint = next_account_info(account_info_iter)?; let stake_authority = next_account_info(account_info_iter)?; let user_stake_ata = next_account_info(account_info_iter)?; let token_program = next_account_info(account_info_iter)?; let metadata_program = next_account_info(account_info_iter)?; ``` We can scroll down and add a couple of validations, we are just copy/pasting from the `process_stake` and `process_redeem` functions: ``` let (delegated_auth_pda, delegate_bump) = Pubkey::find_program_address(&[b"authority"], program_id); if delegated_auth_pda != *program_authority.key { msg!("Invalid seeds for PDA"); return Err(StakeError::InvalidPda.into()); } let (stake_auth_pda, auth_bump) = Pubkey::find_program_address(&[b"mint"], program_id); if *stake_authority.key != stake_auth_pda { msg!("Invalid stake mint authority!"); return Err(StakeError::InvalidPda.into()); } ``` Alright, so this is quite new, we're going to "thaw" the NFT token account. If you recall, we froze it up above, so no we'll unfreeze it. This code is the exact opposite of the freeze code above, we just have to change the helper function and use `thaw_delegated_account`. ``` msg!("thawing NFT token account"); invoke_signed( &mpl_token_metadata::instruction::thaw_delegated_account( mpl_metadata_program_id, *program_authority.key, *nft_token_account.key, *nft_edition.key, *nft_mint.key, ), &[ program_authority.clone(), nft_token_account.clone(), nft_edition.clone(), nft_mint.clone(), metadata_program.clone(), ], &[&[b"authority", &[delegate_bump]]], )?; ``` Next, we need to revoke the delegation authority. This is similar, but not exactly the same as the approving of the delegation. We can remove the `program_authority` field as it's not necessary, and the `amount` from the approve helper function. ``` msg!("Revoke delegation"); invoke( &spl_token::instruction::revoke( &spl_token_program_id, nft_token_account.key, user.key, &[user.key], )?, &[ nft_token_account.clone(), user.clone(), token_program.clone(), ], )?; ``` Finally, we will copy the `invoke_signed` from the redeem function, paste it under the `redeem_amount`. ``` invoke_signed( &spl_token::instruction::mint_to( token_program.key, stake_mint.key, user_stake_ata.key, stake_authority.key, &[stake_authority.key], redeem_amount.try_into().unwrap(), )?, &[ stake_mint.clone(), user_stake_ata.clone(), stake_authority.clone(), token_program.clone(), ], &[&[b"mint", &[auth_bump]]], )?; ``` Oh, one more thing, we didn't actually set the `redeem_amount`, we just used `unix_time` previously. So instead, put `100 * unit_time`, we can adjust this later. Make sure to make the change in both functions above. That should be it here, back to `instruction.ts` file on the client side to add all the accounts. Scroll down to the `createUnstakeInstruction`, add the following as arguments. ``` nftMint: PublicKey, nftEdition: PublicKey, stakeMint: PublicKey, userStakeATA: PublicKey, tokenProgram: PublicKey, metadataProgram: PublicKey, ``` Again, a few are derived automatically, so we don't have add manually. Next we derive the `delegateAuthority` and `mintAuth`, this is identical to the code above. ``` const [delegateAuthority] = PublicKey.findProgramAddressSync( [Buffer.from("authority")], programId ) const [mintAuth] = PublicKey.findProgramAddressSync( [Buffer.from("mint")], programId ) ``` Finally, we add them all to the instruction. This is a lot of accounts, so we've just posted it all here, instead of just the ones we're adding. Save your eyes a bit of back and forth between functions and files. ``` { pubkey: nftHolder, isWritable: false, isSigner: true, }, { pubkey: nftTokenAccount, isWritable: true, isSigner: false, }, { pubkey: nftMint, isWritable: false, isSigner: false, }, { pubkey: nftEdition, isWritable: false, isSigner: false, }, { pubkey: stakeAccount, isWritable: true, isSigner: false, }, { pubkey: delegateAuthority, isWritable: true, isSigner: false, }, { pubkey: stakeMint, isWritable: true, isSigner: false, }, { pubkey: mintAuth, isWritable: false, isSigner: false, }, { pubkey: userStakeATA, isWritable: true, isSigner: false, }, { pubkey: tokenProgram, isWritable: false, isSigner: false, }, { pubkey: metadataProgram, isWritable: false, isSigner: false, }, ``` #### Testing our functionality Ok, ok, I know you can feel it, we're getting close...let's finally go back to the `index.ts` file to call out and test all the functions. We need the mint address for our token and token account for our user, for the `testRedeem` function, as well as the `createUnstakeInstruction`. First we add the following to the `testRedeem` function parameters. ``` stakeMint: web3.PublicKey, userStakeATA: web3.PublicKey ``` Then we add them to the `createRedeemInstruction` below. ``` stakeMint, userStakeATA, TOKEN_PROGRAM_ID, PROGRAM_ID ``` Make the same additions as above to the `testUnstaking` function. Then for `createUnstakingInstruction`, add the following. ``` nft.mintAddress, nft.masterEditionAddress, stakeMint, userStakeATA, TOKEN_PROGRAM_ID, METADATA_PROGRAM_ID, ``` Now scroll down to the call site in the `main()` function, you'll notice `testRedeem` and `testUnstaking` are both red as they need more info passed in. First we need to declare the `stakeMint`, which we will hardcode for now, and `userStakeATA`, which calls a function that will create the ATA if it doesn't exist yet. ``` const stakeMint = new web3.PublicKey( "EMPTY FOR A MINUTE" ) const userStakeATA = await getOrCreateAssociatedTokenAccount( connection, user, stakeMint, user.publicKey ) ``` ...and now, change the calls to take the additional arguments: ``` await testRedeem(connection, user, nft, stakeMint, userStakeATA.address) await testUnstaking(connection, user, nft, stakeMint, userStakeATA.address) ``` #### Front-end edits to test functionality We're briefly going to change to the front-end Buildoors project, into the `index.ts` file (/<project-name>/tokens/bld/index.ts). In here we are creating the BLD token with the `createBldToken` function. Inside that function, we call `token.CreateMint` the 3rd argument is the mint authority, which controls the minting. At first, this is a `payer.publicKey` for the intial call. In short order, we'll be changing the mint authority. First we add a parameter to the *createBldToken* function. ``` programId: web3.PublicKey ``` Then scroll all the way down to the call site in main, and for the `await createBldToken` call, add the 3rd argument. ```new web3.PublicKey("USE YOUR PROGRAM ID")``` If you cannot find your program ID, you can deploy again and the console will show you the program ID you need. Scroll back up, above `const tokenMint`, pull in `mintAuth`. You can find the auth for the below in redeem from the anchor-nft-staking program. ``` const [mintAuth] = await web3.PublicKey.findProgramAddress( [Buffer.from("mint")], programId ) ``` Scroll back down, after the `transactionSignature` is created, we'll set the new mint authority. (this is the change we mentioned above) ``` await token.setAuthority( connection, payer, tokenMint, payer.publicKey, token.AuthorityType.MintTokens, mintAuth ) ``` Now, we're able to recreate the BLD token with the new auth, and we can take that and add it to the `stakeMint` above. ``` const stakeMint = new web3.PublicKey( "EMPTY FOR A MINUTE" ) ``` #### Finally, test it all out So, switch into the home directory and run `npm run create-bld-token`. Make sure you are set to devnet. Check your build script, it should be: ```"creat-bld-token": "ts-node tokens/bld/index.ts"``` Once it is successfully finished, go grab your new mint key from the *cache.json* in the *tokens/bld* directory. Now, we finally head back to the nft-staking program, and use this key in the `stakeMint` creation, ``` const stakeMint = new web3.PublicKey( "MINT KEY FROM CACHE.JSON" ) ``` All should be set and working now, cd back into the ts directory, and test it all with *npm run start*. If all is well, your console should confirm initialization, staking, redeeming, and unstaking. This was A LOT. Take a breath, you're crushing. This is super challenging, go back, review, do it again, whatever it takes -- if you can master this stuff, you'll be well on your way to becoming a solid Solana dev. ## TODO: Gage video 4.4.4(NEEDS LINK) ### WALKTHROUGH OF CALLING STAKING #### Front-end Staking Can you feel it, the finish line is near...well, at least for this core. 😆 Our focus will be to get the instructions working, for staking and unstaking, on the front-end of the program. In your front-end project, let's create a new `utils` folder in the root directory. Then create a file called `instructions.ts` and copy/paste the entire `instructions.ts` file from the nft staking project. As it's well over 200 lines of code, I won't paste it here. 😬 Next we'll hop into the `StakeOptionsDisplay` file (/<project-name>/components/StakeOptionsDisplay.rs). You'll notice we have three empty functions for `handleStake`, `handleUnstake` and `handleClaim`. This is our focus for this section. As always, let's get our wallet and connection set up. ``` const walletAdapter = useWallet() const { connection } = useConnection() ``` Let's check for a wallet first. ``` if (!walletAdapter.connected || !walletAdapter.publicKey) { alert("Please connect your wallet") return } ``` If that passes, we can set up our instruction. ``` const stakeInstruction = createStakingInstruction( walletAdapter.publicKey, nftTokenAccount, nftData.mint.address, nftData.edition.address, TOKEN_PROGRAM_ID, -- needs to be imported METADATA_PROGRAM_ID, -- needs to be imported PROGRAM_ID -- needs to be imported from constants.ts ) ``` So, go into the `utils` folder, add a `constants.ts` file, add the following. ``` import { PublicKey } from "@solana/web3.js" export const PROGRAM_ID = new PublicKey( process.env.NEXT_PUBLIC_STAKE_PROGRAM_ID ?? "" ) ``` This is the program ID we are using in the instruction above. Make sure you have the correct program ID in your `env.local` file. The stake instruction should be all set, next we'll create a transaction, and add the instruction, and send it. ``` const transaction = new Transaction().add(stakeInstruction) const signature = await walletAdapter.sendTransaction(transaction, connection) ``` Since this is an await, make sure to scroll up and make the `handleStake` callback `async`. In fact, all three of these functions should be async callbacks. We can do a check to make sure it went through, so let's get the latest blockhash and confirm the transaction. ``` const latestBlockhash = await connection.getLatestBlockhash() await connection.confirmTransaction( { blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, signature: signature, }, "finalized" ) } catch (error) { console.log(error) } await checkStakingStatus() ``` After confirming the transaction we can check to see if we're still staking, so let's add this function toward the top of the `handleStake` code block. ``` const checkStakingStatus = useCallback(async () => { if (!walletAdapter.publicKey || !nftTokenAccount) { return } ``` We also need to add `walletAdapter` and `connection` as dependencies on the `handleStake` callback. We're going to need some state fields, so scroll up and add state for staking. ``` const [isStaking, setIsStaking] = useState(isStaked) ``` Let's also change the parameter for `StakeOptionsDisplay` from `isStaking` to `isStaked`, or else our state won't work. We also need to create a new file in `utils` called `accounts.ts` and copy over the file from our nft staking program utils folder. This will likely need an install for our borsh library. The reason we're bringing this over is that every time we check state, we're going to check the stake account, and see what is the value of staked. Then inside the callback for `checkStakingStatus`, we call `getStakeAccount`. ``` const account = await getStakeAccount( connection, walletAdapter.publicKey, nftTokenAccount ) setIsStaking(account.state === 0) } catch (e) { console.log("error:", e) } ``` Since we'll be sending multiple transactions, let's go ahead and set up a helper function for confirming our transactions, we can paste in the code from above. ``` const sendAndConfirmTransaction = useCallback( async (transaction: Transaction) => { try { const signature = await walletAdapter.sendTransaction( transaction, connection ) const latestBlockhash = await connection.getLatestBlockhash() await connection.confirmTransaction( { blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, signature: signature, }, "finalized" ) } catch (error) { console.log(error) } await checkStakingStatus() }, [walletAdapter, connection] ) ``` And now, just call `sendAndConfirmTransaction` in the `handleStake` function. #### Front-end Claim/Redeem That should do it for `handleStake`. For unstake and claim, it's practically the same thing, with the added complexity of whether we will need to create the user's token account, for the reward token that they're going to get. We can tackle `handleClaim` next. Use the same alert we had above checking whether the wallet adapter is connected, and has a public key. Next we'll check to see if the associated token account for rewards exists. ``` const userStakeATA = await getAssociatedTokenAddress( STAKE_MINT, walletAdapter.publicKey ) ``` Quickly hop over the `constants.ts` file we created and add this for our mint since we need `STAKE_MINT`. ``` export const STAKE_MINT = new PublicKey( process.env.NEXT_PUBLIC_STAKE_MINT_ADDRESS ?? "" ) ``` Once we have the ATA, we need to call `getAccountInfo` which will either return an account or null. ```const account = await connection.getAccountInfo(userStakeATA)``` Then we create our transaction and check whether there's an account, if there isn't one, we call `createAssociatedTokenAccountInstruction`, otherwise we just call `createRedeemInstruction`. ``` const transaction = new Transaction() if (!account) { transaction.add( createAssociatedTokenAccountInstruction( walletAdapter.publicKey, userStakeATA, walletAdapter.publicKey, STAKE_MINT ) ) } transaction.add( createRedeemInstruction( walletAdapter.publicKey, nftTokenAccount, nftData.mint.address, userStakeATA, TOKEN_PROGRAM_ID, PROGRAM_ID ) ) ``` Now we can call the helper transaction confirmation function created above. ``` await sendAndConfirmTransaction(transaction) }, [walletAdapter, connection, nftData, nftTokenAccount]) ``` Finally, add our dependencies of `walletAdapter` and `connection` to the callback. #### Front-end UnStaking Now, onto `handleUnstake`, make sure to make async like the others. The following can just be copied over from `handleClaim`. ``` if ( !walletAdapter.connected || !walletAdapter.publicKey || !nftTokenAccount ) { alert("Please connect your wallet") return } const userStakeATA = await getAssociatedTokenAddress( STAKE_MINT, walletAdapter.publicKey ) const account = await connection.getAccountInfo(userStakeATA) const transaction = new Transaction() if (!account) { transaction.add( createAssociatedTokenAccountInstruction( walletAdapter.publicKey, userStakeATA, walletAdapter.publicKey, STAKE_MINT ) ) } ``` Now we add instructions to our transaction, and call our helper function again. ``` transaction.add( createUnstakeInstruction( walletAdapter.publicKey, nftTokenAccount, nftData.address, nftData.edition.address, STAKE_MINT, userStakeATA, TOKEN_PROGRAM_ID, METADATA_PROGRAM_ID, PROGRAM_ID ) ) await sendAndConfirmTransaction(transaction) } ``` #### Stake page edits Let's hop over to `stake.tsx` (/<project-name>/pages/stake.tsx) and make a few changes related to the above. First, we need to change the use of `isStaking` to `isStaked` as per our edit above. This is in the `<StakeOptionsDisplay>` component. We also need to add a field for `nftData` and give it the value of `nftData`, which we need a state for. ```const [nftData, setNftData] = useState<any>()``` Right now, we don't have the actual data. We'll use a useEffect where we call metaplex and find the nft data via the mint address. ``` useEffect(() => { const metaplex = Metaplex.make(connection).use( walletAdapterIdentity(walletAdapter) ) try { metaplex .nfts() .findByMint({ mintAddress: mint }) .run() .then((nft) => { console.log("nft data on stake page:", nft) setNftData(nft) }) } catch (e) { console.log("error getting nft:", e) } }, [connection, walletAdapter]) ``` Don't forget to get a connection and walletAdapter above, as we have done many times. Alright, we're in a place where we can test things, let's do *npm run dev*, and open your localhost in the browser. Have it, push the buttons. 🔘 ⏏️ 🆒 #### A few more edits So, a few things may need work...briefly, pop back into the `StakeOptionsDisplay` file, add the following useEffect before the `handleStake` function. ``` useEffect(() => { checkStakingStatus() if (nftData) { connection .getTokenLargestAccounts(nftData.mint.address) .then((accounts) => setNftTokenAccount(accounts.value[0].address)) } }, [nftData, walletAdapter, connection]) ``` It's a quick check to make sure we have nft data, and if yes, setting a value for the nft token account. It's an nft, there's only one, so it'll be the first address, hence the index value of '0'. Additionally add `nftData` as a dependency on all three of the callback functions. Finally, inside `handleStake`, add this code before creating your transaction. ``` const [stakeAccount] = PublicKey.findProgramAddressSync( [walletAdapter.publicKey.toBuffer(), nftTokenAccount.toBuffer()], PROGRAM_ID ) const transaction = new Transaction() const account = await connection.getAccountInfo(stakeAccount) if (!account) { transaction.add( createInitializeStakeAccountInstruction( walletAdapter.publicKey, nftTokenAccount, PROGRAM_ID ) ) } ``` We need a stake account, a PDA on the program that stores the state data about your staking. The code above initializes that account for us, if we don't have one. Alas, we are DONE with core 4. This last bit was kind of all over the place, so to make sure we didn't miss anything, we're pasting entire `StakeOptionsDisplay` file below. ``` import { VStack, Text, Button } from "@chakra-ui/react" import { useConnection, useWallet } from "@solana/wallet-adapter-react" import { PublicKey, Transaction } from "@solana/web3.js" import { useCallback, useEffect, useState } from "react" import { createInitializeStakeAccountInstruction, createRedeemInstruction, createStakingInstruction, createUnstakeInstruction, } from "../utils/instructions" import { TOKEN_PROGRAM_ID, getAssociatedTokenAddress, createAssociatedTokenAccountInstruction, } from "@solana/spl-token" import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata" import { PROGRAM_ID, STAKE_MINT } from "../utils/constants" import { getStakeAccount } from "../utils/accounts" export const StakeOptionsDisplay = ({ nftData, isStaked, daysStaked, totalEarned, claimable, }: { nftData: any isStaked: boolean daysStaked: number totalEarned: number claimable: number }) => { const walletAdapter = useWallet() const { connection } = useConnection() const [isStaking, setIsStaking] = useState(isStaked) const [nftTokenAccount, setNftTokenAccount] = useState<PublicKey>() const checkStakingStatus = useCallback(async () => { if (!walletAdapter.publicKey || !nftTokenAccount) { return } try { const account = await getStakeAccount( connection, walletAdapter.publicKey, nftTokenAccount ) console.log("stake account:", account) setIsStaking(account.state === 0) } catch (e) { console.log("error:", e) } }, [walletAdapter, connection, nftTokenAccount]) useEffect(() => { checkStakingStatus() if (nftData) { connection .getTokenLargestAccounts(nftData.mint.address) .then((accounts) => setNftTokenAccount(accounts.value[0].address)) } }, [nftData, walletAdapter, connection]) const handleStake = useCallback(async () => { if ( !walletAdapter.connected || !walletAdapter.publicKey || !nftTokenAccount ) { alert("Please connect your wallet") return } const [stakeAccount] = PublicKey.findProgramAddressSync( [walletAdapter.publicKey.toBuffer(), nftTokenAccount.toBuffer()], PROGRAM_ID ) const transaction = new Transaction() const account = await connection.getAccountInfo(stakeAccount) if (!account) { transaction.add( createInitializeStakeAccountInstruction( walletAdapter.publicKey, nftTokenAccount, PROGRAM_ID ) ) } const stakeInstruction = createStakingInstruction( walletAdapter.publicKey, nftTokenAccount, nftData.mint.address, nftData.edition.address, TOKEN_PROGRAM_ID, METADATA_PROGRAM_ID, PROGRAM_ID ) transaction.add(stakeInstruction) await sendAndConfirmTransaction(transaction) }, [walletAdapter, connection, nftData, nftTokenAccount]) const sendAndConfirmTransaction = useCallback( async (transaction: Transaction) => { try { const signature = await walletAdapter.sendTransaction( transaction, connection ) const latestBlockhash = await connection.getLatestBlockhash() await connection.confirmTransaction( { blockhash: latestBlockhash.blockhash, lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, signature: signature, }, "finalized" ) } catch (error) { console.log(error) } await checkStakingStatus() }, [walletAdapter, connection] ) const handleUnstake = useCallback(async () => { if ( !walletAdapter.connected || !walletAdapter.publicKey || !nftTokenAccount ) { alert("Please connect your wallet") return } const userStakeATA = await getAssociatedTokenAddress( STAKE_MINT, walletAdapter.publicKey ) const account = await connection.getAccountInfo(userStakeATA) const transaction = new Transaction() if (!account) { transaction.add( createAssociatedTokenAccountInstruction( walletAdapter.publicKey, userStakeATA, walletAdapter.publicKey, STAKE_MINT ) ) } transaction.add( createUnstakeInstruction( walletAdapter.publicKey, nftTokenAccount, nftData.address, nftData.edition.address, STAKE_MINT, userStakeATA, TOKEN_PROGRAM_ID, METADATA_PROGRAM_ID, PROGRAM_ID ) ) await sendAndConfirmTransaction(transaction) }, [walletAdapter, connection, nftData, nftTokenAccount]) const handleClaim = useCallback(async () => { if ( !walletAdapter.connected || !walletAdapter.publicKey || !nftTokenAccount ) { alert("Please connect your wallet") return } const userStakeATA = await getAssociatedTokenAddress( STAKE_MINT, walletAdapter.publicKey ) const account = await connection.getAccountInfo(userStakeATA) const transaction = new Transaction() if (!account) { transaction.add( createAssociatedTokenAccountInstruction( walletAdapter.publicKey, userStakeATA, walletAdapter.publicKey, STAKE_MINT ) ) } transaction.add( createRedeemInstruction( walletAdapter.publicKey, nftTokenAccount, nftData.mint.address, userStakeATA, TOKEN_PROGRAM_ID, PROGRAM_ID ) ) await sendAndConfirmTransaction(transaction) }, [walletAdapter, connection, nftData, nftTokenAccount]) return ( <VStack bgColor="containerBg" borderRadius="20px" padding="20px 40px" spacing={5} > <Text bgColor="containerBgSecondary" padding="4px 8px" borderRadius="20px" color="bodyText" as="b" fontSize="sm" > {isStaking ? `STAKING ${daysStaked} DAY${daysStaked === 1 ? "" : "S"}` : "READY TO STAKE"} </Text> <VStack spacing={-1}> <Text color="white" as="b" fontSize="4xl"> {isStaking ? `${totalEarned} $BLD` : "0 $BLD"} </Text> <Text color="bodyText"> {isStaking ? `${claimable} $BLD earned` : "earn $BLD by staking"} </Text> </VStack> <Button onClick={isStaking ? handleClaim : handleStake} bgColor="buttonGreen" width="200px" > <Text as="b">{isStaking ? "claim $BLD" : "stake buildoor"}</Text> </Button> {isStaking ? <Button onClick={handleUnstake}>unstake</Button> : null} </VStack> ) } ```