# CORE 6
## Section 1
### 6.1.1 Explore - Intro to Ship Week!
Welcome to your final week of Solana Core! This week will be different from the past five. Our sole focus is to prepare our project to get it shipped by the end of the core.
Structurally, there will be no more formal lessons, instead we will give you tasks to accomplish on your own. Today's task is to build out a lootbox solution. We will keep it on the simpler side for now. There will be written instructions in the next section. There will be some hints on how to approach it. Get creative, do what makes the most sense for your project.
We will also drop videos of the solution walkthroughs, but they'll look a bit different from prior videos. In the past, we have coded along with you, this week, we will simply go over the code solution so that you can do more on your own. Remember, our solution is one potential option, by all means, please build what you would like.
We have covered a lot, you are ready for this, do the work independently, and then watch the solution walkthroughs if necessary, or if you'd like, attempt other paths to get the code working.
### 6.1.2 Build a simple loot box program
**Overview**
Welcome to Ship week! This whole week is dedicated to getting your project ready to ship. For us, that means the buildoors project. For you, itāll be similar but hopefully with some personal flair!
The main thing thatās left to do is implement the loot box. For now, create a simple loot box implementation.
That means:
- No verified randomness - you can stick with a simple pseudorandom implementation or not do randomness at all.
- Be okay with a static solution, e.g. donāt make it so you can update the gear type later. Keep it simple for now.
You can and should get creative with this. And by that I donāt just mean āuse different images.ā Feel free to create your own architecture and really experiment. In the immortal words of Miss Frizzle: āTake chances, make mistakes, and get messy!ā
**Hints**
That being said, here are a couple of pointers based on how we approached the loot box program (yes, we did it in a separate program):
We usedĀ [fungible assets](https://docs.metaplex.com/programs/token-metadata/token-standard#the-fungible-asset-standard)Ā instead of NFTs for the gear distributed by a loot box.
We just modified the script used for creating the BLD token to make one āfungible assetā token per piece of gear.
The loot box is really just an idea - thereās not a token or anything representing it.
To assign (randomly or otherwise) from multiple possible mints, we needed to set up the program to have two separate transactions:
- The first we calledĀ `open_lootbox`. It handles burning the $BLD tokens required to open a loot box, then pseudorandomly selects the gear to be minted to the caller, but then instead of minting it right then, stores the selection in a PDA associated to the user. Had to do this otherwise youād have to pass all possible gear mints into the instruction which would be really annoying.
- The second we calledĀ `retrieve_item_from_lootbox`. This one checks the aforementioned PDA and mints the specified gear to the user.
Again, donāt feel locked into our implementation. And try this out on your own before looking at our solution. And I donāt mean try it out for 20 minutes. This took me virtually an entire day to figure out so donāt be afraid to work independently for quite a bit before turning to our solution walkthrough.
Good luck!
### 6.1.3 Walkthrough of a script for creating gear tokens āļø
**Overview**
Let's walk through one possible solution for gear tokens.Ā
The solution code we're walking through is on theĀ `solution-simple-gear`Ā branch of theĀ [Buildoors Frontend repository](https://github.com/jamesrp13/buildspace-buildoors/tree/solution-simple-gear). Try to build this on your own if you haven't already rather than copy-pasting from solution code.
We'll be looking at two different repositories. If you recall, we had the creation of the BLD token and our NFTs in the client-side project. It just happens that's where we did the work, we could if we wanted, move that over to the program project.
You can find the images of the gear in the `/tokens/gear/assets` folder. Instead of these being NFTs, we've chosen to make them fungible assets, or SPL tokens with associated metadata and 0 decimals, this is so they're not limited to just one unit.
The script inside [`/tokens/gear/index.ts`](https://github.com/jamesrp13/buildspace-buildoors/blob/solution-simple-gear/tokens/gear/index.ts) is responsible for generating our mints associated with these assets, and stores them in the `cache.json` file in the same folder.
Inside the script, toward the bottom you'll see our main function.
``` typescript
async function main() {
const connection = new web3.Connection(web3.clusterApiUrl("devnet"))
const payer = await initializeKeypair(connection)
await createGear(
connection,
payer,
new web3.PublicKey("6GE3ki2igpw2ZTAt6BV4pTjF5qvtCbFVQP7SGPJaEuoa"),
["Bow", "Glasses", "Hat", "Keyboard", "Mustache"]
)
}
```
The public key we pass in is for our program, and the list of the names of the mints, which need to correspond to what's in the assets folder.
If you scroll up in the function, you'll see it first starts out with an empty object where the mints will go.
` let collection: any = {} `
Then we create our metaplex object, followed by a loop, which executes the functionality of the script for every single mint.
It starts with an empty array of mints, this is so we can add multiple mints per asset.
`let mints: Array<string> = []`
Then we get the image buffer and upload it to Arweave.
``` typescript
const imageBuffer = fs.readFileSync(`tokens/gear/assets/${assets[i]}.png`)
const file = toMetaplexFile(imageBuffer, `${assets[i]}.png`)
const imageUri = await metaplex.storage().upload(file)
```
After that, for as many times as you want different xp levels, for this gear, let's loop through that many times -- our example is only doing it once since we're starting and ending as xp level 10. If you want five of each, simply increase our upper bound to 50, `xp <= 50`.
`for (let xp = 10; xp <= 10; xp += 10)...`
Once inside the loop, we grab the mint auth that we will assign down the road, this it he PDA on the program that we want to be doing the minting -- the PDA for the lootbox program.
``` typescript
const [mintAuth] = await web3.PublicKey.findProgramAddress(
[Buffer.from("mint")],
programId
)
```
We then create a brand new mint and set its decimal to 0, as it is a non-divisible asset.
``` typescript
const tokenMint = await token.createMint(
connection,
payer,
payer.publicKey,
payer.publicKey,
0
)
```
Once that mint is created, we push it into the mints array.
` mints.push(tokenMint.toBase58())`
Next we upload our off-chain metadata, which is a name, description, an image uri, and two attributes.
``` typescript
const { uri } = await metaplex
.nfts()
.uploadMetadata({
name: assets[i],
description: "Gear that levels up your buildoor",
image: imageUri,
attributes: [
{
trait_type: "xp",
value: `${xp}`,
},
],
})
.run()
```
Then we grab the metadata PDA for that mint.
`const metadataPda = await findMetadataPda(tokenMint)`
Next we create the on-chain version of the metadata.
``` typescript
const tokenMetadata = {
name: assets[i],
symbol: "BLDRGEAR",
uri: uri,
sellerFeeBasisPoints: 0,
creators: null,
collection: null,
uses: null,
} as DataV2
```
Followe by creating our V2 instruction as we have done previously.
``` typescript
const instruction = createCreateMetadataAccountV2Instruction(
{
metadata: metadataPda,
mint: tokenMint,
mintAuthority: payer.publicKey,
payer: payer.publicKey,
updateAuthority: payer.publicKey,
},
{
createMetadataAccountArgsV2: {
data: tokenMetadata,
isMutable: true,
},
}
)
```
You'll notice our mint authority is our payer, we will change this shortly.
We then create a transaction, add the instruction and send it.
``` typescript
const transaction = new web3.Transaction()
transaction.add(instruction)
const transactionSignature = await web3.sendAndConfirmTransaction(
connection,
transaction,
[payer]
)
```
And now we change the authority to be the mintAuth, which is the PDA on the lootbox program.
``` typescript
await token.setAuthority(
connection,
payer,
tokenMint,
payer.publicKey,
token.AuthorityType.MintTokens,
mintAuth
)
}
```
Finally, outside that inner loop, we put the mints in the array, so the first one would be 'Bow' (for our example).
`collection[assets[i]] = mints`
Finally, outside all the loops, we write to file the entire collection, which will just be five items for our implementation.
`fs.writeFileSync("tokens/gear/cache.json", JSON.stringify(collection))`
This is just one way to do it, it's quite a simple solution. If you've not already built your code, and you watched this, try to do it on your own, and then come back and use the solution code if need be.
### 6.1.4 Walkthrough of a simple loot box program š
Now we'll walk through the solution for the simple loot box implementation. We will create a new program, use it for creating a loot box, and retrieving an item from the loot box.
The solution code we'll be reviewing is one the solution-naive-loot-boxes branch of the [Anchor NFT Staking repository](https://github.com/Unboxed-Software/anchor-nft-staking-program/tree/solution-naive-loot-boxes).
I'll suggest it again, try to do his on your own instead of copy-pasting solution code.
While in the `programs` directory, you can use `anchor new <program-name>` to create a new program, we called it `lootbox-program`.
If you have a look, in the `Anchor.toml` file the ID for the nft-staking program is changed, and we added an ID for the loot box program. You need to update both of these on your end.
First, let's review the changes we made to the [original staking program](https://github.com/Unboxed-Software/anchor-nft-staking-program/blob/solution-naive-loot-boxes/programs/anchor-nft-staking/src/lib.rs).
If you scroll down to the `UserStakeInfo` object, we added the `total_earned` field. This basically tracks the user's staking journey, as they earn more over time, and as they hit new milestones that will earn them more loot box items.
Related to that, have a look at the `redeem_amount`.
First you'll notice there are commented out notes, this is simply so we have enough tokens redeemed for testing purposes. Make sure comment/uncomment the right code for testing.
Scrolling down a bit further, you'll see this new line added.
`ctx.accounts.stake_state.total_earned += redeem_amount as u64;`
This is a way to track total earned, which starts at 0. Then you add the amount redeemed, which is the new total earned.
You'll also see both the testing notes and the redeem amount change in the unstake function below.
Finally, one last change in this file. If your program is identical to mine, as we run this, we run out of space in the stack, simply due to adding this one new field. I chose a random account, and put a Box around it, to make sure it is allocated to the heap, instead of the stack, to address this space issue. You can do it on the user stake ATA, or any other account of your choosing.
`pub user_stake_ata: Box<Account<'info, TokenAccount>>,`
Alright, let's go into the files for the new lootbox program.
In the `Cargo.toml` you'll notice we added a new dependency for our original anchor-nft-staking program.
```
[dependencies]
anchor-lang = { version="0.25.0", features=["init-if-needed"] }
anchor-spl = "0.25.0"
anchor-nft-staking = { path = "../anchor-nft-staking", features = ["cpi"] }
```
Now let's hop into the main [loot box program](https://github.com/Unboxed-Software/anchor-nft-staking-program/blob/solution-naive-loot-boxes/programs/lootbox-program/src/lib.rs) file.
You'll notice in the use statements, we now import anchor nft staking, so we can check the total earned field.
``` rust
use anchor_lang::prelude::*;
use anchor_nft_staking::UserStakeInfo;
use anchor_spl::token;
use anchor_spl::{
associated_token::AssociatedToken,
token::{Burn, Mint, MintTo, Token, TokenAccount},
};
```
In here, we have only two instructions, `open_lootbox` and `retrieve_item_from_lootbox`. The reason we have two is that when you say, give me something random from the loot box, and the program is deciding which of all the possible things to mint and give, then the client would have to pass in all of the possible mint accounts. This would make the program less dyanamic and add some overhead of checking a bunch of different accounts to make sure there are options, it's also really annoying on the client side. So, we created one for open loot box, which basically make sure that of all the possible mint options, give me one of them. We also chose this as the place to pay, this is where we will burn BLD tokens. As for the second instruction, at this point the client knows which mint they're getting, and can pass that in, and we can mint from that.
First, let's go through open lootbox, here are the accounts we need.
``` rust
#[derive(Accounts)]
pub struct OpenLootbox<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init_if_needed,
payer = user,
space = std::mem::size_of::<LootboxPointer>() + 8,
seeds=["lootbox".as_bytes(), user.key().as_ref()],
bump
)]
pub lootbox_pointer: Account<'info, LootboxPointer>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
// Swap the next two lines out between prod/testing
// #[account(mut)]
#[account(
mut,
address="6YR1nuLqkk8VC1v42xJaPKvE9X9pnuqVAvthFUSDsMUL".parse::<Pubkey>().unwrap()
)]
pub stake_mint: Account<'info, Mint>,
#[account(
mut,
associated_token::mint=stake_mint,
associated_token::authority=user
)]
pub stake_mint_ata: Account<'info, TokenAccount>,
pub associated_token_program: Program<'info, AssociatedToken>,
#[account(
constraint=stake_state.user_pubkey==user.key(),
)]
pub stake_state: Account<'info, UserStakeInfo>,
}
```
You'll notice a new one called `lootbox_pointer`, it is a new type. It has a mint, a boolean of whether it is claimed or not, and is_initialized.
This is a PDA that is associated with the user, hence "lootbox" and `user` are its seeds. What this lets us do is that when we choose a mint, we can't return data to the client, instead we store it in the account somewhere. So, it is a PDA that the user can look at to retrieve their item.
Another thing of note, there's a line that starts with 'Swap' commented out, in order for the tests to work, uncomment these and comment out the other `stake_mint` attribute with the mind address in it.
``` rust
#[account]
pub struct LootboxPointer {
mint: Pubkey,
claimed: bool,
is_initialized: bool,
}
```
Let's look at the instruction, first we check to see if it's a valid loot box.
They pass in a box number, and the instruction runs an infinite loop where for each iteration, if the number of BLD tokens it too low, we error. The other two paths are the that we either double the loot_box number, or (middle option) if we match on our loot_box number and box_number, we require the stake_state PDA's total earned is greater than or equal to the box_number that was passed in -- you'll have to have earned more than box number.
``` rust
pub fn open_lootbox(ctx: Context<OpenLootbox>, box_number: u64) -> Result<()> {
let mut loot_box = 10;
loop {
if loot_box > box_number {
return err!(LootboxError::InvalidLootbox);
}
if loot_box == box_number {
require!(
ctx.accounts.stake_state.total_earned >= box_number,
LootboxError::InvalidLootbox
);
break;
} else {
loot_box = loot_box * 2;
}
}
require!(
!ctx.accounts.lootbox_pointer.is_initialized || ctx.accounts.lootbox_pointer.claimed,
LootboxError::InvalidLootbox
);
```
Then we move on to the token burn, and burn the number of tokens that the box_number requires.
``` rust
token::burn(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Burn {
mint: ctx.accounts.stake_mint.to_account_info(),
from: ctx.accounts.stake_mint_ata.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
},
),
box_number * u64::pow(10, 2),
)?;
```
Next we say, this is the available gear. This is hard-coded for now, this is the data in the `cache.json` file on the client side code. There are more dynamic ways to do this.
``` rust
let available_gear: Vec<Pubkey> = vec![
"DQmrQJkErmfe6a1fD2hPwdLSnawzkdyrKfSUmd6vkC89"
.parse::<Pubkey>()
.unwrap(),
"A26dg2NBfGgU6gpFPfsiLpxwsV13ZKiD58zgjeQvuad"
.parse::<Pubkey>()
.unwrap(),
"GxR5UVvQDRwB19bCsB1wJh6RtLRZUbEAigtgeAsm6J7N"
.parse::<Pubkey>()
.unwrap(),
"3rL2p6LsGyHVn3iwQQYV9bBmchxMHYPice6ntp7Qw8Pa"
.parse::<Pubkey>()
.unwrap(),
"73JnegAtAWHmBYL7pipcSTpQkkAx77pqCQaEys2Qmrb2"
.parse::<Pubkey>()
.unwrap(),
];
```
Then there's this pseudo-random thing, definitely not safe. We get the time, in seconds, then modulus over 5, to figure out which of those 5 items we should get. Once we get it, we assign it to the lootbox pointer.
``` rust
let clock = Clock::get()?;
let i: usize = (clock.unix_timestamp % 5).try_into().unwrap();
// Add in randomness later for selecting mint
let mint = available_gear[i];
ctx.accounts.lootbox_pointer.mint = mint;
ctx.accounts.lootbox_pointer.claimed = false;
ctx.accounts.lootbox_pointer.is_initialized = true;
Ok(())
}
```
We will work on actual randomness later, but this is sufficient for now. We will want to add a check later, to ensure they can't just open loot box over and over again, to get the item they prefer. As it is now, once they open the loot box, they can see what the item is. We can do a check to see if the lootbox pointer is initialized, if it is not, then we're good and can move forward. While they would be paying for each attempt, it's up to you if you want that to be a feature.
Alright, let's hop over to the retrieve instruction and look at the accounts needed.
``` rust
#[derive(Accounts)]
pub struct RetrieveItem<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
seeds=["lootbox".as_bytes(), user.key().as_ref()],
bump,
constraint=lootbox_pointer.is_initialized
)]
pub lootbox_pointer: Account<'info, LootboxPointer>,
#[account(
mut,
constraint=lootbox_pointer.mint==mint.key()
)]
pub mint: Account<'info, Mint>,
#[account(
init_if_needed,
payer=user,
associated_token::mint=mint,
associated_token::authority=user
)]
pub user_gear_ata: Account<'info, TokenAccount>,
/// CHECK: Mint authority - not used as account
#[account(
seeds=["mint".as_bytes()],
bump
)]
pub mint_authority: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
```
A couple of things of note. The mint account is for the gear they're claiming. The mint authority is the mint PDA we assigned in our client-side script.
As for the logic for this. First we require that the lootbox pointer is not already claimed.
``` rust
pub fn retrieve_item_from_lootbox(ctx: Context<RetrieveItem>) -> Result<()> {
require!(
!ctx.accounts.lootbox_pointer.claimed,
LootboxError::AlreadyClaimed
);
```
We then mint it to you.
``` rust
token::mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.user_gear_ata.to_account_info(),
authority: ctx.accounts.mint_authority.to_account_info(),
},
&[&[
b"mint".as_ref(),
&[*ctx.bumps.get("mint_authority").unwrap()],
]],
),
1,
)?;
```
Finally, we set claimed to true.
``` rust
ctx.accounts.lootbox_pointer.claimed = true;
Ok(())
}
```
Make sure you don't miss the code for the custom errors we created at the bttom of the file.
``` rust
#[error_code]
enum LootboxError {
#[msg("Mint already claimed")]
AlreadyClaimed,
#[msg("Haven't staked long enough for this loot box or invalid loot box number")]
InvalidLootbox,
}
```
That's about it. If you've not already implemented this, have a go at it, get some tests running. Try to do it independently.
A brief look at the tests, they're all in a [single file](https://github.com/Unboxed-Software/anchor-nft-staking-program/blob/solution-naive-loot-boxes/tests/anchor-nft-staking.ts). You'll notice we added two more, "Chooses a mint pseudorandomly" and "Mints the selected gear". Reminder, go to the places we had 'Swap' and change the lines of code for the tests to work. Then run the tests, they should all be working as expected.
## Section 2
### 6.2.1 Randomize loot with Switchboard's verified š
**Task**
Now that youāve got simple loot boxes working, letās see if we can level up with true randomness using Switchboard (well, technically still pseudorandom but multiple orders of magnitude better).
Switchboard is a decentralized oracle network built on Solana. An oracle is a gateway between blockchains and the real world, providing mechanisms to arrive at a consensus for data given multiple sources. In the case of randomness, that means providing a verifiable pseudorandom result that you simply canāt get without an oracle. This is key to having a loot box implementation that canāt be ācheated.ā
Interacting with an Oracle is an exercise that brings together literally everything weāve learned throughout this course. You typically need the following flow:
Some sort of client-side setup directly with the oracle programInitialization of oracle-specific accounts (usually PDAs) with your own program. A CPI call from your program to the oracle program that requests specific data. In this case, a verifiably random buffer. An instruction that the oracle can call on your program to deliver the requested information. An instruction that does whatever your program is trying to do with the requested data
**Documentation**
So, first things first. Documentation is still sparse across Web3, but you can read a brief overview about Switchboardās Verifiable RandomnessĀ [here](https://docs.switchboard.xyz/solana/randomness). Then you should read through theirĀ [Integration docs](https://docs.switchboard.xyz/solana/randomness/integration).
Youāll likely still come away from that with a lot of questions. Thatās okay. Donāt feel demoralized. Weāre flexing some necessary āfigure-it-outā muscles.
The next thing you can do is look at theirĀ [step by step walkthrough](https://github.com/switchboard-xyz/vrf-demo-walkthrough)Ā for getting randomness. This will take you through the process of setting up a switchboard environment, initializing your requesting client, issuing a CPI to theĀ `request_randomness`Ā instruction, adding aĀ `consume_randomness`Ā instruction to your program that Switchboard can invoke to provide randomness, and more.
**Final note**
This is going to be challenging. Itās meant to be. Itās the culmination of a lot of work trying to understand Solana over the last six weeks. There are videos with an overview of using Switchboard in our loot box program.
Feel free to watch them right away. Normally I tell you to wait until you've done some independent work, but the documentation on Switchboard is sparse enough that it'll be helpful to view the walkthrough ASAP. What I will say though is to not copy and past my solution. Instead, watch the walkthrough and then try to recreate something similar on your own. If you are ready to reference solution code before we have the walkthrough up, feel free to look at theĀ `solution-randomize-lootbranch`Ā [here](https://github.com/Unboxed-Software/anchor-nft-staking-program/tree/solution-randomize-loot).
You might not get this working before the end of the week. This is doable, but may take more time hacking this week than you have. Thatās okay. You can still ship with your simple pseudorandom solution from before and then retrofit with more robust randomness later. Just create a new branch and do your best with Switchboard.
Youāve got this. Good luck!
### 6.2.2 Walkthrough of Switchboard setup š¶š½š
**Overview**
We're going to build a basic program to request randomness using Switchboard. In this video we'll focus on the client-side setup for Switchboard in our test environment.
We'll first do the switchboard setup, which is in its own file at [/tests/utils/setupSwitchboard.ts](https://github.com/Unboxed-Software/anchor-nft-staking-program/blob/solution-randomize-loot/tests/utils/setupSwitchboard.ts).
This is the setup that lets us run our tests against it. Their documenation is very sparse, but we should do well enough for the randomization piece.
Let's review the code, here are the three imports we will need.
``` typescript
import { SwitchboardTestContext } from "@switchboard-xyz/sbv2-utils"
import * as anchor from "@project-serum/anchor"
import * as sbv2 from "@switchboard-xyz/switchboard-v2"
```
For the actual function, you'll notice the three items we pass in are provider, lootboxProgram and payer.
The first thing we do is load the devnet queue, which gives us a testing environment on devnet. The ID is the program ID for switchboard, and the 100,000,000 are switchboard tokens, which we need to access their stuff.
``` typescript
export const setupSwitchboard = async (provider, lootboxProgram, payer) => {
const switchboard = await SwitchboardTestContext.loadDevnetQueue(
provider,
"F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy",
100_000_000
)
```
Then we have a bunch of logs to make sure everything is good and ready to go.
``` typescript
console.log(switchboard.mint.address.toString())
await switchboard.oracleHeartbeat()
const { queue, unpermissionedVrfEnabled, authority } =
await switchboard.queue.loadData()
console.log(`oracleQueue: ${switchboard.queue.publicKey}`)
console.log(`unpermissionedVrfEnabled: ${unpermissionedVrfEnabled}`)
console.log(`# of oracles heartbeating: ${queue.length}`)
console.log(
"\x1b[32m%s\x1b[0m",
`\u2714 Switchboard devnet environment loaded successfully\n`
)
```
The const state is the key component in the above, it loads the switchboard queue data we need which we will use through the rest of the function.
We then create our verified random function (VRF) account. This is very particular to the part of swtichboard we're using. As you can see, it generates a new keypair.
``` typescript
// CREATE VRF ACCOUNT
// keypair for vrf account
const vrfKeypair = anchor.web3.Keypair.generate()
```
As part of creating the VRF account, we're going to need access to a couple of PDAs.
``` typescript
// find PDA used for our client state pubkey
const [userState] = anchor.utils.publicKey.findProgramAddressSync(
[vrfKeypair.publicKey.toBytes(), payer.publicKey.toBytes()],
lootboxProgram.programId
)
// lootboxPointerPda for callback
const [lootboxPointerPda] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("lootbox"), payer.publicKey.toBuffer()],
lootboxProgram.programId
)
```
You'll see that we are using the vrf and payer public keys as seeds. In production, those will need to be static, it will only be the payer publickey. This code ensures that we have a different vrf keypair and user state every time we run the test so we don't run into any issues trying to recreate an account we've already created while testing.
Now we can create the VRF account using the sbv2 library, passing in the switchboard program, the keypair we want to the give the vrf account, the authority which is the userState PDA, the switchboard queue, and the callback.
So, what will happen is that when we want a new random number, we're gonna do a CPI to the switchboard program to get a random number, and it has to know an instruction on our program to CPI back to, to give us the random number. As with all instructions, it has a program ID, a list of accounts, and the instruction data. As for the accounts, the first one is where it's going to write the data for us, then vrf account, the lootbox pointer PDA where we will write the mint that has been seleted, and finally the payer.
``` typescript
// create new vrf acount
const vrfAccount = await sbv2.VrfAccount.create(switchboard.program, {
keypair: vrfKeypair,
authority: userState, // set vrfAccount authority as PDA
queue: switchboard.queue,
callback: {
programId: lootboxProgram.programId,
accounts: [
{ pubkey: userState, isSigner: false, isWritable: true },
{ pubkey: vrfKeypair.publicKey, isSigner: false, isWritable: false },
{ pubkey: lootboxPointerPda, isSigner: false, isWritable: true },
{ pubkey: payer.publicKey, isSigner: false, isWritable: false },
],
ixData: new anchor.BorshInstructionCoder(lootboxProgram.idl).encode(
"consumeRandomness",
""
),
},
})
```
Next we create what's called a permission account.
``` typescript
// CREATE PERMISSION ACCOUNT
const permissionAccount = await sbv2.PermissionAccount.create(
switchboard.program,
{
authority,
granter: switchboard.queue.publicKey,
grantee: vrfAccount.publicKey,
}
)
```
The authority field is what we got from the load data on the queue above. This is giving our vrf account permissions in switchboard.
Next, we're chaging the permission to be us, setting the authority to payer.
``` typescript
// If queue requires permissions to use VRF, check the correct authority was provided
if (!unpermissionedVrfEnabled) {
if (!payer.publicKey.equals(authority)) {
throw new Error(
`queue requires PERMIT_VRF_REQUESTS and wrong queue authority provided`
)
}
await permissionAccount.set({
authority: payer,
permission: sbv2.SwitchboardPermission.PERMIT_VRF_REQUESTS,
enable: true,
})
}
```
As we'll need the switchboard account bump in a couple of instructions later, we pull that out, as well as the switchboardStateBump, which is the program account for switchboard.
``` typescript
// GET PERMISSION BUMP AND SWITCHBOARD STATE BUMP
const [_permissionAccount, permissionBump] = sbv2.PermissionAccount.fromSeed(
switchboard.program,
authority,
switchboard.queue.publicKey,
vrfAccount.publicKey
)
const [switchboardStateAccount, switchboardStateBump] =
sbv2.ProgramStateAccount.fromSeed(switchboard.program)
```
That is all the data we'll need to run our test to interact with both our program and switchboard, which we return at the end.
``` typescript
return {
switchboard: switchboard,
lootboxPointerPda: lootboxPointerPda,
permissionBump: permissionBump,
permissionAccount: permissionAccount,
switchboardStateBump: switchboardStateBump,
switchboardStateAccount: switchboardStateAccount,
vrfAccount: vrfAccount,
}
```
We end up calling this entire function in our testing environment setup in `anchor-nft-staking.ts`, so our before will now look like this.
``` typescript
before(async () => {
;({ nft, stakeStatePda, mint, tokenAddress } = await setupNft(
program,
wallet.payer
))
;({
switchboard,
lootboxPointerPda,
permissionBump,
switchboardStateBump,
vrfAccount,
switchboardStateAccount,
permissionAccount,
} = await setupSwitchboard(provider, lootboxProgram, wallet.payer))
})
```
That's the primer on what we'll need for switchboard on the client side.
### 6.2.3 Walkthrough of init_user instruction š¶
First things first, for our lootbox program, we previously had everything in `lib.rs`, but it was getting big and unwieldy, so it's now broken up, [have a look](https://github.com/Unboxed-Software/anchor-nft-staking-program/tree/solution-randomize-loot/programs/lootbox-program) to see the file structure.
Now the lib file is mostly just a bunch of use statements, the declare id macro, and our four instructions, which are not doing anything but calling to other files.
Init_user will create the user state account that we'll share between our program and switchboard, it's like a liason account.
Open lootbox is same as before, it will kick off the process of generating a random mint, but it won't finish the process, instead it will generate a CPI to call out to switchboard to ask for a random number.
Consume randomness is what will be called by switchboard to give back the number in the instruction so we can use it, and finish the process when setting the mint.
Retrieve item from lootbox is pretty much unchanged.
Let's jump into each, first init_user.
At the top, you'll find the init user context, and at the bottom is an implementation, where there's a function called process instruction, which does the logic previously in the lib file.
There are four accounts in the InitUser context. The state is our user state object, which has the vrf and payer key seeds, this is the version for testing. For production code, you only need the payer seed. We're doing this instead of using environment variables, just to save time. Then there's the vrf account, which switchboard does not load automatically, hence the constraint with the .load() call. There is likely a different way to do this with switchboard, but we're using the simplest/fastest path for now to get up and running, as always, feel free to explore and improve it. And finally, we have the payer and system program for creating a new account.
``` rust
use crate::*;
#[derive(Accounts)]
#[instruction(params: InitUserParams)]
pub struct InitUser<'info> {
#[account(
init,
// TESTING - Comment out these seeds for testing
// seeds = [
// payer.key().as_ref(),
// ],
// TESTING - Uncomment these seeds for testing
seeds = [
vrf.key().as_ref(),
payer.key().as_ref()
],
payer = payer,
space = 8 + std::mem::size_of::<UserState>(),
bump,
)]
pub state: AccountLoader<'info, UserState>,
#[account(
constraint = vrf.load()?.authority == state.key() @ LootboxError::InvalidVrfAuthorityError
)]
pub vrf: AccountLoader<'info, VrfAccountData>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}
```
For the logic, we're grabbing an account called state, for we set the bump, switchboard state bump, the vrf permission bump, the vrf account and the user it is associated with. You'll notice there's a struct that simply has the two bumps we previously talked about.
``` rust
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
pub struct InitUserParams {
pub switchboard_state_bump: u8,
pub vrf_permission_bump: u8,
}
impl InitUser<'_> {
pub fn process_instruction(ctx: &Context<Self>, params: &InitUserParams) -> Result<()> {
let mut state = ctx.accounts.state.load_init()?;
*state = UserState::default();
state.bump = ctx.bumps.get("state").unwrap().clone();
state.switchboard_state_bump = params.switchboard_state_bump;
state.vrf_permission_bump = params.vrf_permission_bump;
state.vrf = ctx.accounts.vrf.key();
state.user = ctx.accounts.payer.key();
Ok(())
}
}
```
Let's take a quick look at the [user state](https://github.com/Unboxed-Software/anchor-nft-staking-program/blob/solution-randomize-loot/programs/lootbox-program/src/state.rs) file so we know what it is.
The new thing here is the result buffer. This is where we pull out the randomness, they send it to us as a 32-byte array of random data, which we can turn into whatever randomness we need.
Notice there are two attributes added, the `[account(zero_copy)]` is what needs the loading piece, I simply used it as suggested in the switchboard examples.
``` rust
#[repr(packed)]
#[account(zero_copy)]
#[derive(Default)]
pub struct UserState {
pub bump: u8,
pub switchboard_state_bump: u8,
pub vrf_permission_bump: u8,
pub result_buffer: [u8; 32],
pub vrf: Pubkey,
pub user: Pubkey,
}
```
...and that's it for the init user, on we go.
### 6.2.4 Open loot box and consume randomness š“š
We're hopping into the open lootbox instruction, first thing you'll notice, it requires a lot of accounts, 19 in total!
Up until the stake_state, it is all information we've had before.
Things we're adding related to switchboard: our user state, which we just initialized in init user. Then there are a bunch of switchboard accounts, which are the vrf account, the oracle queue account, queue authority, which is just a PDA for that authority, the data buffer account, permission account, escrow account, program state account, and switchboard program account itself.
You'll notice there are quite a few types we've not talked about, they're coming from the switchboard-v2 crate. Here are the two dependencies you'll need to add to the `Cargo.toml` to make all those types work.
```
switchboard-v2 = { version = "^0.1.14", features = ["devnet"] }
bytemuck = "1.7.2"
```
And the last two accounts are payer wallet, which is the token account associated with your swithboard token, it is how you pay for the randomness, and the recent blockhashes.
``` rust
use crate::*;
use anchor_lang::solana_program;
#[derive(Accounts)]
pub struct OpenLootbox<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
init_if_needed,
payer = user,
space = std::mem::size_of::<LootboxPointer>() + 8,
seeds=["lootbox".as_bytes(), user.key().as_ref()],
bump
)]
pub lootbox_pointer: Box<Account<'info, LootboxPointer>>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
// TESTING - Uncomment the next line during testing
// #[account(mut)]
// TESTING - Comment out the next three lines during testing
#[account(
mut,
address="D7F9JnGcjxQwz9zEQmasksX1VrwFcfRKu8Vdqrk2enHR".parse::<Pubkey>().unwrap()
)]
pub stake_mint: Account<'info, Mint>,
#[account(
mut,
associated_token::mint=stake_mint,
associated_token::authority=user
)]
pub stake_mint_ata: Box<Account<'info, TokenAccount>>,
pub associated_token_program: Program<'info, AssociatedToken>,
#[account(
constraint=stake_state.user_pubkey==user.key(),
)]
pub stake_state: Box<Account<'info, UserStakeInfo>>,
#[account(
mut,
// TESTING - Comment out these seeds for testing
seeds = [
user.key().as_ref(),
],
// TESTING - Uncomment these seeds for testing
// seeds = [
// vrf.key().as_ref(),
// user.key().as_ref()
// ],
bump = state.load()?.bump,
has_one = vrf @ LootboxError::InvalidVrfAccount
)]
pub state: AccountLoader<'info, UserState>,
// SWITCHBOARD ACCOUNTS
#[account(mut,
has_one = escrow
)]
pub vrf: AccountLoader<'info, VrfAccountData>,
#[account(mut,
has_one = data_buffer
)]
pub oracle_queue: AccountLoader<'info, OracleQueueAccountData>,
/// CHECK:
#[account(mut,
constraint =
oracle_queue.load()?.authority == queue_authority.key()
)]
pub queue_authority: UncheckedAccount<'info>,
/// CHECK
#[account(mut)]
pub data_buffer: AccountInfo<'info>,
#[account(mut)]
pub permission: AccountLoader<'info, PermissionAccountData>,
#[account(mut,
constraint =
escrow.owner == program_state.key()
&& escrow.mint == program_state.load()?.token_mint
)]
pub escrow: Account<'info, TokenAccount>,
#[account(mut)]
pub program_state: AccountLoader<'info, SbState>,
/// CHECK:
#[account(
address = *vrf.to_account_info().owner,
constraint = switchboard_program.executable == true
)]
pub switchboard_program: AccountInfo<'info>,
// PAYER ACCOUNTS
#[account(mut,
constraint =
payer_wallet.owner == user.key()
&& escrow.mint == program_state.load()?.token_mint
)]
pub payer_wallet: Account<'info, TokenAccount>,
// SYSTEM ACCOUNTS
/// CHECK:
#[account(address = solana_program::sysvar::recent_blockhashes::ID)]
pub recent_blockhashes: AccountInfo<'info>,
}
```
With the accounts behind us, here's what we're actually doing in the openlootbox implementation, remember this is where our logic sits.
Up until we load our state, it is all the same as before. Once we load our state, we get our bump from the state, then the other two bumps, which we added in init user. We also drop state from memory.
``` rust
let state = ctx.accounts.state.load()?;
let bump = state.bump.clone();
let switchboard_state_bump = state.switchboard_state_bump;
let vrf_permission_bump = state.vrf_permission_bump;
drop(state);
```
Next we get the switchboard program itself from the accounts list. Then we bulid the vrf request randomness, this is basically our context which we'll use for the CPI, which happens when we call `vrf_request_randomness` a few lines later.
Again, you'll notice some code to comment out for production vs. testing. We only use the vrf account for testing purposes.
``` rust
let switchboard_program = ctx.accounts.switchboard_program.to_account_info();
let vrf_request_randomness = VrfRequestRandomness {
authority: ctx.accounts.state.to_account_info(),
vrf: ctx.accounts.vrf.to_account_info(),
oracle_queue: ctx.accounts.oracle_queue.to_account_info(),
queue_authority: ctx.accounts.queue_authority.to_account_info(),
data_buffer: ctx.accounts.data_buffer.to_account_info(),
permission: ctx.accounts.permission.to_account_info(),
escrow: ctx.accounts.escrow.clone(),
payer_wallet: ctx.accounts.payer_wallet.clone(),
payer_authority: ctx.accounts.user.to_account_info(),
recent_blockhashes: ctx.accounts.recent_blockhashes.to_account_info(),
program_state: ctx.accounts.program_state.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
};
let payer = ctx.accounts.user.key();
// TESTING - uncomment the following during tests
let vrf = ctx.accounts.vrf.key();
let state_seeds: &[&[&[u8]]] = &[&[vrf.as_ref(), payer.as_ref(), &[bump]]];
// TESTING - comment out the next line during tests
// let state_seeds: &[&[&[u8]]] = &[&[payer.as_ref(), &[bump]]];
```
This does the call to switchboard.
``` rust
msg!("requesting randomness");
vrf_request_randomness.invoke_signed(
switchboard_program,
switchboard_state_bump,
vrf_permission_bump,
state_seeds,
)?;
msg!("randomness requested successfully");
```
And finally, we change random requested and is initialized to true.
``` rust
ctx.accounts.lootbox_pointer.randomness_requested = true;
ctx.accounts.lootbox_pointer.is_initialized = true;
ctx.accounts.lootbox_pointer.available_lootbox = box_number * 2;
Ok(())
```
Let's hop back to the lootbox pointer struct, there's a `redeemable` property. This property lets our client observe the lootbox pointer account, once it goes from false to true, then we know that we've got the randomness back, and are ready to mint. This change happens in the consume randomness function.
``` rust
#[account]
pub struct LootboxPointer {
pub mint: Pubkey,
pub redeemable: bool,
pub randomness_requested: bool,
pub available_lootbox: u64,
pub is_initialized: bool,
}
```
Let's hop over to that function and review. This is the one that gets called by switchboard, it is what we provided as the `callback` in `setupSwitchboard` file. The four accounts in the callback match the accounts in ConsumeRandomness, where loobox pointer and state are both mutable.
``` rust
use crate::state::*;
use crate::*;
#[derive(Accounts)]
pub struct ConsumeRandomness<'info> {
#[account(
mut,
// TESTING - Comment out these seeds for testing
seeds = [
payer.key().as_ref(),
],
// TESTING - Uncomment these seeds for testing
// seeds = [
// vrf.key().as_ref(),
// payer.key().as_ref()
// ],
bump = state.load()?.bump,
has_one = vrf @ LootboxError::InvalidVrfAccount
)]
pub state: AccountLoader<'info, UserState>,
pub vrf: AccountLoader<'info, VrfAccountData>,
#[account(
mut,
seeds=["lootbox".as_bytes(), payer.key().as_ref()],
bump
)]
pub lootbox_pointer: Account<'info, LootboxPointer>,
/// CHECK: ...
pub payer: AccountInfo<'info>,
}
```
As for the actual implementation, in the process instruction function, we first load the vrf and state account. Then we get the result buffer from the vrf account, and check to make sure it's not empty.
``` rust
impl ConsumeRandomness<'_> {
pub fn process_instruction(ctx: &mut Context<Self>) -> Result<()> {
let vrf = ctx.accounts.vrf.load()?;
let state = &mut ctx.accounts.state.load_mut()?;
let result_buffer = vrf.get_result()?;
if result_buffer == [0u8; 32] {
msg!("vrf buffer empty");
return Ok(());
}
if result_buffer == state.result_buffer {
msg!("result_buffer unchanged");
return Ok(());
}
```
Then we map the available gear. For now, we're just using constants defined below so we can necessary make changes as we our building our program. This give us a vector of public keys.
``` rust
let available_gear: Vec<Pubkey> = Self::AVAILABLE_GEAR
.into_iter()
.map(|key| key.parse::<Pubkey>().unwrap())
.collect();
```
The `value` variable is where we take our result buffer and turns it into a unsigned 8-bit integer, this is how switchboard recommended it be implemented, using the `bytemuck` crate. We finally use a modulo operator, with the max number of mints available, to randomly select one.
``` rust
// maximum value to convert randomness buffer
let max_result = available_gear.len();
let value: &[u8] = bytemuck::cast_slice(&result_buffer[..]);
let i = (value[0] as usize) % max_result;
msg!("The chosen mint index is {} out of {}", i, max_result);
```
We finally assign the value at the i'th index to mint, then assign it to the lootbox pointer mint, and changing the value of redeemable to true. This allows to observe it on the client side, and once it is true, the user can mint their gear.
``` rust
let mint = available_gear[i];
msg!("Next mint is {:?}", mint);
ctx.accounts.lootbox_pointer.mint = mint;
ctx.accounts.lootbox_pointer.redeemable = true;
Ok(())
}
const AVAILABLE_GEAR: [&'static str; 5] = [
"87QkviUPcxNqjdo1N6C4FrQe3ZiYdAyxGoT44ioDUG8m",
"EypLPq3xBRREfpsdbyXfFjobVAnHsNerP892NMHWzrKj",
"Ds1txTXZadjsjKtt2ybH56GQ2do4nbGc8nrSH3Ln8G9p",
"EHPo4mSNCfYzX3Dtr832boZAiR8vy39eTsUfKprXbFus",
"HzUvbXymUCBtubKQD9yiwWdivAbTiyKhpzVBcgD9DhrV",
];
}
```
As mentioned before, the retrieve item from lootbox instruction has hardly changed. If you have another look at it, you'll see it has no interaction with switchboard, hence no need to make any updates.
### 6.2.5 Client-side interaction/testing š„ļø & š§Ŗ
At last, we're gonna go over the tests related to switchboard stuff. We already reviewd the setupSwitchboard function to prepare the tests. Our first three tests are still for staking, redeeming, and unstaking. The next test is for init_user, which is pretty straight forward, we just need to pass in the swtichboard state bump and permission bump, and four acounts.
``` typescript
it("init user", async () => {
const tx = await lootboxProgram.methods
.initUser({
switchboardStateBump: switchboardStateBump,
vrfPermissionBump: permissionBump,
})
.accounts({
state: userState,
vrf: vrfAccount.publicKey,
payer: wallet.pubkey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc()
})
```
Next is the chooses mint pseudorandomly test, which is trickier. The first bit is similar to others. First we create a fake mint to mint this stuff. Then get or create an ATA, and mint to it. And our stake account, which would've been use to actually stake our NFT.
``` typescript
it("Chooses a mint pseudorandomly", async () => {
const mint = await createMint(
provider.connection,
wallet.payer,
wallet.publicKey,
wallet.publicKey,
2
)
const ata = await getOrCreateAssociatedTokenAccount(
provider.connection,
wallet.payer,
mint,
wallet.publicKey
)
await mintToChecked(
provider.connection,
wallet.payer,
mint,
ata.address,
wallet.payer,
1000,
2
)
const [stakeAccount] = anchor.web3.PublicKey.findProgramAddressSync(
[wallet.publicKey.toBuffer(), nft.tokenAddress.toBuffer()],
program.programId
)
```
We then load data from the vrf account, and get our authority and dataBuffer from the switchboard queue. Then we call open lootbox, which needs all the appropriate accounts...there are quite a few. Most come from the setupSwitchboard function, and a couple from the switchboard queue we just grabbed.
``` typescript
const vrfState = await vrfAccount.loadData()
const { authority, dataBuffer } = await switchboard.queue.loadData()
await lootboxProgram.methods
.openLootbox(new BN(10))
.accounts({
user: wallet.publicKey,
stakeMint: mint,
stakeMintAta: ata.address,
stakeState: stakeAccount,
state: userState,
vrf: vrfAccount.publicKey,
oracleQueue: switchboard.queue.publicKey,
queueAuthority: authority,
dataBuffer: dataBuffer,
permission: permissionAccount.publicKey,
escrow: vrfState.escrow,
programState: switchboardStateAccount.publicKey,
switchboardProgram: switchboard.program.programId,
payerWallet: switchboard.payerTokenWallet,
recentBlockhashes: anchor.web3.SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
})
.rpc()
```
Then we have this awaitCallback function where we pass in lootbox program, pointer PDA and pick a time of 20 seconds, where we'll wait to see if that lootbox pointer updates with the new mint.
``` typescript
await awaitCallback(
lootboxProgram,
lootboxPointerPda,
20_000,
"Didn't get random mint"
)
})
```
Below is the await callback function, feel free to paste this. In here you'll see how it just sits there and waits. It looks for account changes on the lootbox pointer, and if there are changes, it checks the lootbox pointer to see if the redeemable has been set to true, if so, it resolves it and the callback is done, and we're good. If it doesn't happen in 20 seconds, it will error out with the "Didn't get random mint" error.
``` typescript
async function awaitCallback(
program: Program<LootboxProgram>,
lootboxPointerAddress: anchor.web3.PublicKey,
timeoutInterval: number,
errorMsg = "Timed out waiting for VRF Client callback"
) {
let ws: number | undefined = undefined
const result: boolean = await promiseWithTimeout(
timeoutInterval,
new Promise((resolve: (result: boolean) => void) => {
ws = program.provider.connection.onAccountChange(
lootboxPointerAddress,
async (
accountInfo: anchor.web3.AccountInfo<Buffer>,
context: anchor.web3.Context
) => {
const lootboxPointer = await program.account.lootboxPointer.fetch(
lootboxPointerAddress
)
if (lootboxPointer.redeemable) {
resolve(true)
}
}
)
}).finally(async () => {
if (ws) {
await program.provider.connection.removeAccountChangeListener(ws)
}
ws = undefined
}),
new Error(errorMsg)
).finally(async () => {
if (ws) {
await program.provider.connection.removeAccountChangeListener(ws)
}
ws = undefined
})
return result
}
```
And at last, the test for minting the selected gear. It gets the lootbox pointer, gets the mint from that, and the ATA we need for it to work. Then we check to see if we had that same gear before, in case we're running this multiple times. Then we call retrieve item from lootbox, and double check to make sure the new gear amount is the previous number plus one.
``` typescript
it("Mints the selected gear", async () => {
const [pointerAddress] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("lootbox"), wallet.publicKey.toBuffer()],
lootboxProgram.programId
)
const pointer = await lootboxProgram.account.lootboxPointer.fetch(
pointerAddress
)
let previousGearCount = 0
const gearAta = await getAssociatedTokenAddress(
pointer.mint,
wallet.publicKey
)
try {
let gearAccount = await getAccount(provider.connection, gearAta)
previousGearCount = Number(gearAccount.amount)
} catch (error) {}
await lootboxProgram.methods
.retrieveItemFromLootbox()
.accounts({
mint: pointer.mint,
userGearAta: gearAta,
})
.rpc()
const gearAccount = await getAccount(provider.connection, gearAta)
expect(Number(gearAccount.amount)).to.equal(previousGearCount + 1)
})
})
```
Now run it all and hope it works. If it doesn't work immediately, do not be disheartened. It took a couple of days to debug it on our end.
## Section 3
### 6.3.1 The Home Stretch š šš»āāļø
**Overview**
This is the final stretch! Congrats on getting here! This has been a wild ride for everyone. No matter the state of your NFT project, take a breath and pat yourself on the back. Youāre awesome.
Now, assess what you have so far and think of theĀ least amount of workĀ you could do to get your project ready to ship. If that means skipping the Switchboard stuff for now, so be it.
Right now, itās time to hook up your UI to your loot box and gear instructions, then do any final polish you need and get this thing shipped!
In our case, this means:
- CreatingĀ `GearItem`Ā andĀ `Lootbox`Ā components in place of the mockĀ `ItemBox`Ā weāve used for that part of the UI
- Adding anĀ `instructions.ts`Ā file where we create functions for:
- - Creating all the instructions required to initialize our lootbox and switchboard
- - Creating all the instructions required to open the lootbox
- - NOTE: this was pretty intense lol - have a look at our solution code but also give it a shot on your own
- Doing a whole lot of debugging and polishing
Honestly the list goes on. We added a bunch to multiple components to ensure state gets updated after transactions and on-chain changes. Even so, itās not perfect. Thereās always room to do more, but donāt let your perfectionism be your enemy. Do what you can and then ship!
**Solution code**
Our solution is on theĀ `solution-lootboxes`Ā branch of theĀ [Buildoors repository](https://github.com/jamesrp13/buildspace-buildoors/tree/solution-lootboxes). There are a few commits in there that make it differ from what you last saw, so if you want to see all the changes make sure toĀ [view the diff from last weekās branch](https://github.com/jamesrp13/buildspace-buildoors/compare/solution-core-5...solution-lootboxes?expand=1).
There are video walkthroughs, but go ahead and get started on your own first. Good luck!
### 6.3.2 Walkthrough of final product
**Overview**
Everything you need for the final project was in the last lesson. From here on out, it's you and the videos baby. Let's do it!
This is what the final product should look like, this here screenshot is from one working example of this project.

In this section, we'll focus on getting the loot box and gear functionality working. The final code can use some more polish, and there are a few minor bugs that need addressing, do with it as you wish before you ship.
If you watch the video, you'll see a full walkthrough of the functioning front-end, along with all the glorious bugs that need addressing.
A couple of notes, make sure to copy and paste over your new IDL every time you change the program. Double check your React hooks and dependencies. Try to break everything into small chunks as much as possible.
### 6.3.3 Walkthrough of instruction.rs š
We're gonna dive into some of the code changes now. Lets hop into `/components/WorkspaceProvider.tsx`.
There are only a few changes, mostly to incorporate adding switchboard program.
There is one new useState.
`const [switchboardProgram, setProgramSwitchboard] = useState<any>()`
We then load the switchboard program, and a useEffect that sets the program switchboard, that way our workspace is always up to date on all of the programs that we need. Unless you're a React pro, this may be challenging, so feel free to reference this code in depth.
``` typescript
async function program() {
let response = await loadSwitchboardProgram(
"devnet",
connection,
((provider as AnchorProvider).wallet as AnchorWallet).payer
)
return response
}
useEffect(() => {
program().then((result) => {
setProgramSwitchboard(result)
console.log("result", result)
})
}, [connection])
```
Ok, next we hop into the `instructions.ts` file in the `utils` folder, this is a new file. There are two public functions here, the `createOpenLootboxInstructions` instructions and the `createInitSwitchboardInstructions` instructions. The latter packages up initializing stuff on the swtichboard program, and initializing the user in the lootbox program.
``` typescript
export async function createOpenLootboxInstructions(
connection: Connection,
stakingProgram: Program<AnchorNftStaking>,
switchboardProgram: SwitchboardProgram,
lootboxProgram: Program<LootboxProgram>,
userPubkey: PublicKey,
nftTokenAccount: PublicKey,
box: number
): Promise<TransactionInstruction[]> {
const [userStatePda] = PublicKey.findProgramAddressSync(
[userPubkey.toBytes()],
lootboxProgram.programId
)
const state = await lootboxProgram.account.userState.fetch(userStatePda)
const accounts = await getAccountsAndData(
lootboxProgram,
switchboardProgram,
userPubkey,
state.vrf
)
return await createAllOpenLootboxInstructions(
connection,
stakingProgram,
lootboxProgram,
switchboardProgram,
accounts,
nftTokenAccount,
box
)
}
```
Further down, there is the `getAccountsAndData` function, it takes four fields as you can see, for the last one, you'll need to generate or fetch the vrf account beforehand. What this does is gets a few accounts, bumps and other data, packages it all up, and returns them all as one object.
``` typescript
async function getAccountsAndData(
lootboxProgram: Program<LootboxProgram>,
switchboardProgram: SwitchboardProgram,
userPubkey: PublicKey,
vrfAccount: PublicKey
): Promise<AccountsAndDataSuperset> {
const [userStatePda] = PublicKey.findProgramAddressSync(
[userPubkey.toBytes()],
lootboxProgram.programId
)
// required switchboard accoount
const [programStateAccount, stateBump] =
ProgramStateAccount.fromSeed(switchboardProgram)
// required switchboard accoount
const queueAccount = new OracleQueueAccount({
program: switchboardProgram,
// devnet permissionless queue
publicKey: new PublicKey("F8ce7MsckeZAbAGmxjJNetxYXQa9mKr9nnrC3qKubyYy"),
})
// required switchboard accoount
const queueState = await queueAccount.loadData()
// wrapped SOL is used to pay for switchboard VRF requests
const wrappedSOLMint = await queueAccount.loadMint()
// required switchboard accoount
const [permissionAccount, permissionBump] = PermissionAccount.fromSeed(
switchboardProgram,
queueState.authority,
queueAccount.publicKey,
vrfAccount
)
// required switchboard accoount
// escrow wrapped SOL token account owned by the VRF account we will initialize
const escrow = await spl.getAssociatedTokenAddress(
wrappedSOLMint.address,
vrfAccount,
true
)
const size = switchboardProgram.account.vrfAccountData.size
return {
userPubkey: userPubkey,
userStatePda: userStatePda,
vrfAccount: vrfAccount,
escrow: escrow,
wrappedSOLMint: wrappedSOLMint,
programStateAccount: programStateAccount,
stateBump: stateBump,
permissionBump: permissionBump,
queueAccount: queueAccount,
queueState: queueState,
permissionAccount: permissionAccount,
size: size,
}
}
```
That object is defined at the bottom of the file as an interface, this is simply to make sure you have everything you need and are able to call them appropriately. While each instruction won't need every field in the object, it will make it easy for each instruction to get all access to the data it needs.
``` typescript
interface AccountsAndDataSuperset {
userPubkey: PublicKey
userStatePda: PublicKey
vrfAccount: PublicKey
escrow: PublicKey
wrappedSOLMint: spl.Mint
programStateAccount: ProgramStateAccount
stateBump: number
permissionBump: number
queueAccount: OracleQueueAccount
queueState: any
permissionAccount: PermissionAccount
size: number
}
```
Let's dive into the `createInitSwitchboardInstructions`. It first generates a vrf keypair, it then calls `getAccountsAndData` to get us all the accounts we need. Then with `initSwitchboardLootboxUser`, it assembles the instructions. It then returns the instructions, as well as the vrf keypair, which is needed to sign.
``` typescript
export async function createInitSwitchboardInstructions(
switchboardProgram: SwitchboardProgram,
lootboxProgram: Program<LootboxProgram>,
userPubkey: PublicKey
): Promise<{
instructions: Array<TransactionInstruction>
vrfKeypair: Keypair
}> {
const vrfKeypair = Keypair.generate()
const accounts = await getAccountsAndData(
lootboxProgram,
switchboardProgram,
userPubkey,
vrfKeypair.publicKey
)
const initInstructions = await initSwitchboardLootboxUser(
switchboardProgram,
lootboxProgram,
accounts,
vrfKeypair
)
return { instructions: initInstructions, vrfKeypair: vrfKeypair }
}
```
As for the `initSwitchboardLootboxUser`, we first get a PDA and the state bump.
``` typescript
async function initSwitchboardLootboxUser(
switchboardProgram: SwitchboardProgram,
lootboxProgram: Program<LootboxProgram>,
accountsAndData: AccountsAndDataSuperset,
vrfKeypair: Keypair
): Promise<Array<TransactionInstruction>> {
// lootbox account PDA
const [lootboxPointerPda] = await PublicKey.findProgramAddress(
[Buffer.from("lootbox"), accountsAndData.userPubkey.toBytes()],
lootboxProgram.programId
)
const stateBump = accountsAndData.stateBump
```
Then we start assembling an array of instructions. First thing we need to do is create an escrow associated token account, owned by the vrf keypair.
``` typescript
const txnIxns: TransactionInstruction[] = [
// create escrow ATA owned by VRF account
spl.createAssociatedTokenAccountInstruction(
accountsAndData.userPubkey,
accountsAndData.escrow,
vrfKeypair.publicKey,
accountsAndData.wrappedSOLMint.address
),
```
Next is the set authority instruction.
``` typescript
// transfer escrow ATA owner to switchboard programStateAccount
spl.createSetAuthorityInstruction(
accountsAndData.escrow,
vrfKeypair.publicKey,
spl.AuthorityType.AccountOwner,
accountsAndData.programStateAccount.publicKey,
[vrfKeypair]
),
```
Then we call create account to create the vrf account.
``` typescript
// request system program to create new account using newly generated keypair for VRF account
SystemProgram.createAccount({
fromPubkey: accountsAndData.userPubkey,
newAccountPubkey: vrfKeypair.publicKey,
space: accountsAndData.size,
lamports:
await switchboardProgram.provider.connection.getMinimumBalanceForRentExemption(
accountsAndData.size
),
programId: switchboardProgram.programId,
}),
```
Then we use the switchboard program methods for vrf init, where we provide it the consume randomness callback.
``` typescript
// initialize new VRF account, included the callback CPI into lootbox program as instruction data
await switchboardProgram.methods
.vrfInit({
stateBump,
callback: {
programId: lootboxProgram.programId,
accounts: [
{
pubkey: accountsAndData.userStatePda,
isSigner: false,
isWritable: true,
},
{
pubkey: vrfKeypair.publicKey,
isSigner: false,
isWritable: false,
},
{ pubkey: lootboxPointerPda, isSigner: false, isWritable: true },
{
pubkey: accountsAndData.userPubkey,
isSigner: false,
isWritable: false,
},
],
ixData: new BorshInstructionCoder(lootboxProgram.idl).encode(
"consumeRandomness",
""
),
},
})
.accounts({
vrf: vrfKeypair.publicKey,
escrow: accountsAndData.escrow,
authority: accountsAndData.userStatePda,
oracleQueue: accountsAndData.queueAccount.publicKey,
programState: accountsAndData.programStateAccount.publicKey,
tokenProgram: spl.TOKEN_PROGRAM_ID,
})
.instruction(),
// initialize switchboard permission account, required account
```
Next we use switchboard to call the permission init.
``` typescript
await switchboardProgram.methods
.permissionInit({})
.accounts({
permission: accountsAndData.permissionAccount.publicKey,
authority: accountsAndData.queueState.authority,
granter: accountsAndData.queueAccount.publicKey,
grantee: vrfKeypair.publicKey,
payer: accountsAndData.userPubkey,
systemProgram: SystemProgram.programId,
})
.instruction(),
```
And finally, we call our lootbox program init user, and return the instructions, which will get packaged up into a transaction by the caller.
``` typescript
await lootboxProgram.methods
.initUser({
switchboardStateBump: accountsAndData.stateBump,
vrfPermissionBump: accountsAndData.permissionBump,
})
.accounts({
// state: userStatePDA,
vrf: vrfKeypair.publicKey,
// payer: publicKey,
// systemProgram: anchor.web3.SystemProgram.programId,
})
.instruction(),
]
return txnIxns
}
```
Lastly, let's go over the `createOpenLootboxInstructions`. First we get the user state PDA, we have to actually fetch that account so we can pull the vrf keypair off of it.
``` typescript
export async function createOpenLootboxInstructions(
connection: Connection,
stakingProgram: Program<AnchorNftStaking>,
switchboardProgram: SwitchboardProgram,
lootboxProgram: Program<LootboxProgram>,
userPubkey: PublicKey,
nftTokenAccount: PublicKey,
box: number
): Promise<TransactionInstruction[]> {
const [userStatePda] = PublicKey.findProgramAddressSync(
[userPubkey.toBytes()],
lootboxProgram.programId
)
const state = await lootboxProgram.account.userState.fetch(userStatePda)
```
Here we call `getAccountsAndData` to get all the accounts we need. Followed by `createAllOpenLootboxInstructions`, which we'll dive into next.
``` typescript
const accounts = await getAccountsAndData(
lootboxProgram,
switchboardProgram,
userPubkey,
state.vrf
)
return await createAllOpenLootboxInstructions(
connection,
stakingProgram,
lootboxProgram,
switchboardProgram,
accounts,
nftTokenAccount,
box
)
}
```
We get the wrapped token account, with wrapped SOL, as that's what we have to use to pay for requesting randomness.
``` typescript
async function createAllOpenLootboxInstructions(
connection: Connection,
stakingProgram: Program<AnchorNftStaking>,
lootboxProgram: Program<LootboxProgram>,
switchboardProgram: SwitchboardProgram,
accountsAndData: AccountsAndDataSuperset,
nftTokenAccount: PublicKey,
box: number
): Promise<TransactionInstruction[]> {
// user Wrapped SOL token account
// wSOL amount is then transferred to escrow account to pay switchboard oracle for VRF request
const wrappedTokenAccount = await spl.getAssociatedTokenAddress(
accountsAndData.wrappedSOLMint.address,
accountsAndData.userPubkey
)
```
Next we get the `stakeTokenAccount` which is associated with the BLD, so you can use BLD tokens in exchange for opening a lootbox. Then the stake account to make sure you have earned enough BLD through staking, to open the lootbox.
``` typescript
// user BLD token account, used to pay BLD tokens to call the request randomness instruction on Lootbox program
const stakeTokenAccount = await spl.getAssociatedTokenAddress(
STAKE_MINT,
accountsAndData.userPubkey
)
const [stakeAccount] = PublicKey.findProgramAddressSync(
[accountsAndData.userPubkey.toBytes(), nftTokenAccount.toBuffer()],
stakingProgram.programId
)
```
Here we start to assemble instructions. If there is no wrapped token account, we add an instruction for creating it.
``` typescript
let instructions: TransactionInstruction[] = []
// check if a wrapped SOL token account exists, if not add instruction to create one
const account = await connection.getAccountInfo(wrappedTokenAccount)
if (!account) {
instructions.push(
spl.createAssociatedTokenAccountInstruction(
accountsAndData.userPubkey,
wrappedTokenAccount,
accountsAndData.userPubkey,
accountsAndData.wrappedSOLMint.address
)
)
}
```
Then we push a transfer instruction for transferring SOL to wrapped SOL. Then an instruction for synching wrapped SOL balance.
``` typescript
// transfer SOL to user's own wSOL token account
instructions.push(
SystemProgram.transfer({
fromPubkey: accountsAndData.userPubkey,
toPubkey: wrappedTokenAccount,
lamports: 0.002 * LAMPORTS_PER_SOL,
})
)
// sync wrapped SOL balance
instructions.push(spl.createSyncNativeInstruction(wrappedTokenAccount))
```
Finally we build and return our instruction for opening the lootbox, so the caller can package them up and send them.
``` typescript
// Lootbox program request randomness instruction
instructions.push(
await lootboxProgram.methods
.openLootbox(new BN(box))
.accounts({
user: accountsAndData.userPubkey,
stakeMint: STAKE_MINT,
stakeMintAta: stakeTokenAccount,
stakeState: stakeAccount,
state: accountsAndData.userStatePda,
vrf: accountsAndData.vrfAccount,
oracleQueue: accountsAndData.queueAccount.publicKey,
queueAuthority: accountsAndData.queueState.authority,
dataBuffer: accountsAndData.queueState.dataBuffer,
permission: accountsAndData.permissionAccount.publicKey,
escrow: accountsAndData.escrow,
programState: accountsAndData.programStateAccount.publicKey,
switchboardProgram: switchboardProgram.programId,
payerWallet: wrappedTokenAccount,
recentBlockhashes: SYSVAR_RECENT_BLOCKHASHES_PUBKEY,
})
.instruction()
)
return instructions
}
```
That's it for instructions, let's go have a look at the new lootbox component, where these instructions will be used.
### 6.3.4 Walkthrough of Lootbox component šļø
Alright, let's hop into `/components/Lootbox.tsx`. Let's have a quick look at the layout before we dive into the logic.
We center everything and simply have three checks of whether there's an available lootbox, a stake account, and if the total earned number is greater than the lootbox. If yes, it renders a box with various options of, otherwise it says to keep staking. We'll shortly look at `handleRedeemLoot` or `handleOpenLootbox`.
``` typescript
return (
<Center
height="120px"
width="120px"
bgColor={"containerBg"}
borderRadius="10px"
>
{availableLootbox &&
stakeAccount &&
stakeAccount.totalEarned.toNumber() >= availableLootbox ? (
<Button
borderRadius="25"
onClick={mint ? handleRedeemLoot : handleOpenLootbox}
isLoading={isConfirmingTransaction}
>
{mint
? "Redeem"
: userAccountExists
? `${availableLootbox} $BLD`
: "Enable"}
</Button>
) : (
<Text color="bodyText">Keep Staking</Text>
)}
</Center>
)
```
In the function, first we have a ton of setup, with a lot of state. There's a useEffect to make sure we have a public key, a lootbox program and staking program, if those are all there, it calls `handleStateRefresh`.
``` typescript
export const Lootbox = ({
stakeAccount,
nftTokenAccount,
fetchUpstreamState,
}: {
stakeAccount?: StakeAccount
nftTokenAccount: PublicKey
fetchUpstreamState: () => void
}) => {
const [isConfirmingTransaction, setIsConfirmingTransaction] = useState(false)
const [availableLootbox, setAvailableLootbox] = useState(0)
const walletAdapter = useWallet()
const { stakingProgram, lootboxProgram, switchboardProgram } = useWorkspace()
const { connection } = useConnection()
const [userAccountExists, setUserAccountExist] = useState(false)
const [mint, setMint] = useState<PublicKey>()
useEffect(() => {
if (!walletAdapter.publicKey || !lootboxProgram || !stakingProgram) return
handleStateRefresh(lootboxProgram, walletAdapter.publicKey)
}, [walletAdapter, lootboxProgram])
```
The state refresh is packaged up as a separate function as it is called after every transaction as well. This simply calls two functions.
``` typescript
const handleStateRefresh = async (
lootboxProgram: Program<LootboxProgram>,
publicKey: PublicKey
) => {
checkUserAccount(lootboxProgram, publicKey)
fetchLootboxPointer(lootboxProgram, publicKey)
}
```
`checkUserAccount` will get the user state PDA, and if it does exist, we call `setUserAccountExist` and set it to true.
``` typescript
// check if UserState account exists
// if UserState account exists also check if there is a redeemable item from lootbox
const checkUserAccount = async (
lootboxProgram: Program<LootboxProgram>,
publicKey: PublicKey
) => {
try {
const [userStatePda] = PublicKey.findProgramAddressSync(
[publicKey.toBytes()],
lootboxProgram.programId
)
const account = await lootboxProgram.account.userState.fetch(userStatePda)
if (account) {
setUserAccountExist(true)
} else {
setMint(undefined)
setUserAccountExist(false)
}
} catch {}
}
```
`fetchLootboxPointer` which basically gets the lootbox pointer, to set the available lootbox and the mint, if it is redeemable.
``` typescript
const fetchLootboxPointer = async (
lootboxProgram: Program<LootboxProgram>,
publicKey: PublicKey
) => {
try {
const [lootboxPointerPda] = PublicKey.findProgramAddressSync(
[Buffer.from("lootbox"), publicKey.toBytes()],
LOOTBOX_PROGRAM_ID
)
const lootboxPointer = await lootboxProgram.account.lootboxPointer.fetch(
lootboxPointerPda
)
setAvailableLootbox(lootboxPointer.availableLootbox.toNumber())
setMint(lootboxPointer.redeemable ? lootboxPointer.mint : undefined)
} catch (error) {
console.log(error)
setAvailableLootbox(10)
setMint(undefined)
}
}
```
Back to the two main pieces of logic, one is `handleOpenLootbox`. It first checks to make sure we have all the necessary items to pass into the function, and then calls `openLootbox`.
``` typescript
const handleOpenLootbox: MouseEventHandler<HTMLButtonElement> = useCallback(
async (event) => {
if (
event.defaultPrevented ||
!walletAdapter.publicKey ||
!lootboxProgram ||
!switchboardProgram ||
!stakingProgram
)
return
openLootbox(
connection,
userAccountExists,
walletAdapter.publicKey,
lootboxProgram,
switchboardProgram,
stakingProgram
)
},
[
lootboxProgram,
connection,
walletAdapter,
userAccountExists,
walletAdapter,
switchboardProgram,
stakingProgram,
]
)
```
`openLootbox` starts with a check to see if user account exists, if not, it calls `createInitSwitchboardInstructions` from the instruction file, which gives us back instructions and vrfKeypair. If that account does not exist, we have not initialized switchboard yet.
``` typescript
const openLootbox = async (
connection: Connection,
userAccountExists: boolean,
publicKey: PublicKey,
lootboxProgram: Program<LootboxProgram>,
switchboardProgram: SwitchboardProgram,
stakingProgram: Program<AnchorNftStaking>
) => {
if (!userAccountExists) {
const { instructions, vrfKeypair } =
await createInitSwitchboardInstructions(
switchboardProgram,
lootboxProgram,
publicKey
)
```
We then create a new transaction, add the instructions and call `sendAndConfirmTransaction`, which we created. It takes an object as the vrfKeypair as a signer.
``` typescript
const transaction = new Transaction()
transaction.add(...instructions)
sendAndConfirmTransaction(connection, walletAdapter, transaction, {
signers: [vrfKeypair],
})
}
...
```
Let's hop out of the logic and look at `sendAndConfirmTransaction`. First we set that we're loading with `setIsConfirmingTransaction(true)`.
Then we call to send the transaction, but we pass it options, which is optional as we don't always have it. This is how we can send the signer of vrfKeypair, but we don't always do that.
Once it confirms, we use `await Promise.all` where we call `handleStateRefresh` and `fetchUpstreamState`. The latter comes in as a prop, it's basically the fetch state function on the stake component.
``` typescript
const sendAndConfirmTransaction = async (
connection: Connection,
walletAdapter: WalletContextState,
transaction: Transaction,
options?: SendTransactionOptions
) => {
setIsConfirmingTransaction(true)
try {
const signature = await walletAdapter.sendTransaction(
transaction,
connection,
options
)
const latestBlockhash = await connection.getLatestBlockhash()
await connection.confirmTransaction(
{
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: signature,
},
"finalized"
)
console.log("Transaction complete")
await Promise.all([
handleStateRefresh(lootboxProgram!, walletAdapter.publicKey!),
fetchUpstreamState(),
])
} catch (error) {
console.log(error)
throw error
} finally {
setIsConfirmingTransaction(false)
}
}
```
Now back to the else statement for the `handleOpenLootbox`, this is the logic for when the account does exist. So we set the open lootbo instruction and send those. Then call `sendAndConfirmTransaction`. Once confirmed, that function will set is confirming to false, so we then set it true again.
``` typescript
...
else {
const instructions = await createOpenLootboxInstructions(
connection,
stakingProgram,
switchboardProgram,
lootboxProgram,
publicKey,
nftTokenAccount,
availableLootbox
)
const transaction = new Transaction()
transaction.add(...instructions)
try {
await sendAndConfirmTransaction(connection, walletAdapter, transaction)
setIsConfirmingTransaction(true)
```
Finally, this is the logic for waiting to see when the mint gets deposited into the lootbox pointer, so we can redeem it. (this code is only working intermittently, don't rely on it, and fix it if you can).
``` typescript
const [lootboxPointerPda] = PublicKey.findProgramAddressSync(
[Buffer.from("lootbox"), publicKey.toBytes()],
lootboxProgram.programId
)
const id = await connection.onAccountChange(
lootboxPointerPda,
async (_) => {
try {
const account = await lootboxProgram.account.lootboxPointer.fetch(
lootboxPointerPda
)
if (account.redeemable) {
setMint(account.mint)
connection.removeAccountChangeListener(id)
setIsConfirmingTransaction(false)
}
} catch (error) {
console.log("Error in waiter:", error)
}
}
)
} catch (error) {
console.log(error)
}
}
}
```
A quick hop over to the `/pages/stake.tsx`. A small modification where we say if there is `nftData` and `nftTokenAccount`, then display lootbox and pass in the stake account, the nft token account, and call fetchstate with the mint address as the fetch state upstream prop.
``` typescript
<HStack>
{nftData && nftTokenAccount && (
<Lootbox
stakeAccount={stakeAccount}
nftTokenAccount={nftTokenAccount}
fetchUpstreamState={() => {
fetchstate(nftData.mint.address)
}}
/>
)}
</HStack>
```
Now hope back over and let's review `handleRedeemLoot`, which is a lot more straightfoward. We first get the associated token. Then we create a new trasnaction with our `retrieveItemFromLootbox` function, and then send and confirm the transaction.
``` typescript
const handleRedeemLoot: MouseEventHandler<HTMLButtonElement> = useCallback(
async (event) => {
if (
event.defaultPrevented ||
!walletAdapter.publicKey ||
!lootboxProgram ||
!mint
)
return
const userGearAta = await getAssociatedTokenAddress(
mint,
walletAdapter.publicKey
)
const transaction = new Transaction()
transaction.add(
await lootboxProgram.methods
.retrieveItemFromLootbox()
.accounts({
mint: mint,
userGearAta: userGearAta,
})
.instruction()
)
sendAndConfirmTransaction(connection, walletAdapter, transaction)
},
[walletAdapter, lootboxProgram, mint]
)
```
That was a lot, we hopped around quite a bit, so if you need to reference the code for the whole file, have a [look here](https://github.com/jamesrp13/buildspace-buildoors/blob/solution-lootboxes/components/Lootbox.tsx).
### 6.3.5 Walkthrough of GearItem component āļø
Alas, let's have a look at the `GearItem` component. This one is a bit less complicated, and much shorter.
``` typescript
import { Center, Image, VStack, Text } from "@chakra-ui/react"
import { Metaplex, walletAdapterIdentity } from "@metaplex-foundation/js"
import { useConnection, useWallet } from "@solana/wallet-adapter-react"
import { PublicKey } from "@solana/web3.js"
import { useEffect, useState } from "react"
export const GearItem = ({
item,
balance,
}: {
item: string
balance: number
}) => {
const [metadata, setMetadata] = useState<any>()
const { connection } = useConnection()
const walletAdapter = useWallet()
useEffect(() => {
const metaplex = Metaplex.make(connection).use(
walletAdapterIdentity(walletAdapter)
)
const mint = new PublicKey(item)
try {
metaplex
.nfts()
.findByMint({ mintAddress: mint })
.run()
.then((nft) => fetch(nft.uri))
.then((response) => response.json())
.then((nftData) => setMetadata(nftData))
} catch (error) {
console.log("error getting gear token:", error)
}
}, [item, connection, walletAdapter])
return (
<VStack>
<Center
height="120px"
width="120px"
bgColor={"containerBg"}
borderRadius="10px"
>
<Image src={metadata?.image ?? ""} alt="gear token" padding={4} />
</Center>
<Text color="white" as="b" fontSize="md" width="100%" textAlign="center">
{`x${balance}`}
</Text>
</VStack>
)
}
```
The layout is quite similar to the last one, but now we display an image, with the metadata on the gear token as the source. Below it, we display the number of each gear token you have.
As for the logic, we pass in the item, as a base58 encoded string representing the mint of the token, and how many you have.
In the useEffect, we create a metaplex object. We turn that string for `item` into a public key. Then call metaplex to find items by the mint. We get back the nft, call fetch on the uri of the nft, which gets us the off-chain metadata. We take that response, turn it into a json, and set it as the metadata, which gives us an image property which we can show in the return call.
Back to the `stake.tsx` file. First we add a line for state for gear balances.
`const [gearBalances, setGearBalances] = useState<any>({})`
We then call it inside of fetchSate.
In fetch state, we set balances to an empty object. Then loop through our different gear options, and get the ATA for the current user, that's associated with that mint. That gives us an address, which we use to get the account, and set the balances for that specific gear mint, to the number we have. After that loop, we call `setGearBalances(balances)`.
So in the UI, we check to see if the length of gear balances is great than zero, then we show all the gear stuff, or don't show it at all.
``` typescript
<HStack spacing={10} align="start">
{Object.keys(gearBalances).length > 0 && (
<VStack alignItems="flex-start">
<Text color="white" as="b" fontSize="2xl">
Gear
</Text>
<SimpleGrid
columns={Math.min(2, Object.keys(gearBalances).length)}
spacing={3}
>
{Object.keys(gearBalances).map((key, _) => {
return (
<GearItem
item={key}
balance={gearBalances[key]}
key={key}
/>
)
})}
</SimpleGrid>
</VStack>
)}
<VStack alignItems="flex-start">
<Text color="white" as="b" fontSize="2xl">
Loot Box
</Text>
<HStack>
{nftData && nftTokenAccount && (
<Lootbox
stakeAccount={stakeAccount}
nftTokenAccount={nftTokenAccount}
fetchUpstreamState={() => {
fetchstate(nftData.mint.address)
}}
/>
)}
</HStack>
</VStack>
</HStack>
```
That's it for checking for and displaying gear. Here's the [code in the repo](https://github.com/jamesrp13/buildspace-buildoors/blob/solution-lootboxes/components/GearItem.tsx) for your reference.
What happens next is in your hands. You can decide out which bugs you want to fix, and which you're ok to live with. Get everything off localhost and get it shipped, so you can share a public link.
...and if you're feeling up to it, get it production ready, and deploy it to mainnet. There are obviously many more improvements you can and should make before going live on mainnet, e.g. fixing the bugs, adding more checks, have more NFTs, etc -- if you're up for it, get it out there!
### 6.3.6 The Finale
***This was a video of James saying thanks, some encouragement, reminding us it's quite challenging, etc.***
### 6.3.7 THE LAST SHIP
My goodness. You have FINALLY made itĀ š«”
Thank you so much for being part of our alpha program! This would not have been able to happen without you.
A few last things from the team at buildspace -Ā
1. Please drop your submission into the showcase channel! We want to see what you built!!
2. Tweet about it! You've done a massive amount of work, the world should know!
You are officially surfing on glass -- see you out there.