Focus mode
Our movie review program is decent, but it's not very web3. All we're doing right now is using Solana as a database. Let's spice it up by giving people tokens for using our app! We'll mint tokens to someone every time they review a movie or leave a comment. Think of it like StackOverflow but with tokens instead of upvotes.
You can pick up where you left off in your last local environment, or set up a new one by copying this or:
git clone https://github.com/buildspace/solana-movie-program/
cd solana-movie-program
git checkout solution-add-comments
npm i
We'll be using the SPL token program to make all of this magic happen so go ahead and update the dependencies in Cargo.toml
:
[dependencies]
solana-program = "~1.10.29"
borsh = "0.9.3"
thiserror = "1.0.31"
spl-token = { version="3.2.0", features = [ "no-entrypoint" ] }
spl-associated-token-account = { version="=1.0.5", features = [ "no-entrypoint" ] }
Let's quickly test out that everything builds with these new dependencies with cargo build-bpf
.
We're ready to get building!
We'll start by creating a token mint. Reminder: a token mint is a special account that holds data about our token.
This is a new instruction, so we'll follow the same steps as we did when we added support for comments:
process_instruction
funcitonFrom the top in instruction.rs
, we've got the enum update:
pub enum MovieInstruction {
AddMovieReview {
title: String,
rating: u8,
description: String,
},
UpdateMovieReview {
title: String,
rating: u8,
description: String,
},
AddComment {
comment: String,
},
InitializeMint,
}
We won't need any fields here - all it takes to call that function is addresses!
Next, we'll update the unpack function:
impl MovieInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
Ok(match variant {
0 => {
let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
Self::AddMovieReview {
title: payload.title,
rating: payload.rating,
description: payload.description,
}
}
1 => {
let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
Self::UpdateMovieReview {
title: payload.title,
rating: payload.rating,
description: payload.description,
}
}
2 => {
let payload = CommentPayload::try_from_slice(rest).unwrap();
Self::AddComment {
comment: payload.comment,
}
}
// New variant added here
3 => Self::InitializeMint,
_ => return Err(ProgramError::InvalidInstructionData),
})
}
}
You'll immediately notice an error in the process_instruction
match statement in processor.rs
since we're not handling all the cases. Let's fix this by bringing in our new SPL imports and adding to the match statement:
// Update imports at the top
use solana_program::{
//Existing imports within solana_program
sysvar::{rent::Rent, Sysvar, rent::ID as RENT_PROGRAM_ID},
native_token::LAMPORTS_PER_SOL,
system_program::ID as SYSTEM_PROGRAM_ID
}
use spl_associated_token_account::get_associated_token_address;
use spl_token::{instruction::initialize_mint, ID as TOKEN_PROGRAM_ID};
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = MovieInstruction::unpack(instruction_data)?;
match instruction {
MovieInstruction::AddMovieReview {
title,
rating,
description,
} => add_movie_review(program_id, accounts, title, rating, description),
MovieInstruction::UpdateMovieReview {
title,
rating,
description,
} => update_movie_review(program_id, accounts, title, rating, description),
MovieInstruction::AddComment { comment } => add_comment(program_id, accounts, comment),
// New instruction handled here to initialize the mint account
MovieInstruction::InitializeMint => initialize_token_mint(program_id, accounts),
}
}
// Rest of the file remains the same
Finally, we can implement the initialize_token_mint
account at the bottom of processor.rs
after the add_comment
function:
pub fn initialize_token_mint(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
// The order of accounts is not arbitrary, the client will send them in this order
// Whoever sent in the transaction
let initializer = next_account_info(account_info_iter)?;
// Token mint PDA - derived on the client
let token_mint = next_account_info(account_info_iter)?;
// Token mint authorirty (this should be you)
let mint_auth = next_account_info(account_info_iter)?;
// System program to create a new account
let system_program = next_account_info(account_info_iter)?;
// Solana Token program address
let token_program = next_account_info(account_info_iter)?;
// System account to calcuate the rent
let sysvar_rent = next_account_info(account_info_iter)?;
// Derive the mint PDA again so we can validate it
// The seed is just "token_mint"
let (mint_pda, mint_bump) = Pubkey::find_program_address(&[b"token_mint"], program_id);
// Derive the mint authority so we can validate it
// The seed is just "token_auth"
let (mint_auth_pda, _mint_auth_bump) =
Pubkey::find_program_address(&[b"token_auth"], program_id);
msg!("Token mint: {:?}", mint_pda);
msg!("Mint authority: {:?}", mint_auth_pda);
// Validate the important accounts passed in
if mint_pda != *token_mint.key {
msg!("Incorrect token mint account");
return Err(ReviewError::IncorrectAccountError.into());
}
if *token_program.key != TOKEN_PROGRAM_ID {
msg!("Incorrect token program");
return Err(ReviewError::IncorrectAccountError.into());
}
if *mint_auth.key != mint_auth_pda {
msg!("Incorrect mint auth account");
return Err(ReviewError::IncorrectAccountError.into());
}
if *system_program.key != SYSTEM_PROGRAM_ID {
msg!("Incorrect system program");
return Err(ReviewError::IncorrectAccountError.into());
}
if *sysvar_rent.key != RENT_PROGRAM_ID {
msg!("Incorrect rent program");
return Err(ReviewError::IncorrectAccountError.into());
}
// Calculate the rent
let rent = Rent::get()?;
// We know the size of a mint account is 82 (remember it lol)
let rent_lamports = rent.minimum_balance(82);
// Create the token mint PDA
invoke_signed(
&system_instruction::create_account(
initializer.key,
token_mint.key,
rent_lamports,
82, // Size of the token mint account
token_program.key,
),
// Accounts we're reading from or writing to
&[
initializer.clone(),
token_mint.clone(),
system_program.clone(),
],
// Seeds for our token mint account
&[&[b"token_mint", &[mint_bump]]],
)?;
msg!("Created token mint account");
// Initialize the mint account
invoke_signed(
&initialize_mint(
token_program.key,
token_mint.key,
mint_auth.key,
Option::None, // Freeze authority - we don't want anyone to be able to freeze!
9, // Number of decimals
)?,
// Which accounts we're reading from or writing to
&[token_mint.clone(), sysvar_rent.clone(), mint_auth.clone()],
// The seeds for our token mint PDA
&[&[b"token_mint", &[mint_bump]]],
)?;
msg!("Initialized token mint");
Ok(())
}
At a high level, here's what's happening here:
Go over the code comments, I added context wherever I could!
You'll now be getting an error since we're calling a new error without declaring it. Pop open error.rs
and add IncorrectAccountError
to the ReviewError
enum:
#[derive(Debug, Error)]
pub enum ReviewError {
#[error("Account not initialized yet")]
UninitializedAccount,
#[error("PDA derived does not equal PDA passed in")]
InvalidPDA,
#[error("Input data exceeds max length")]
InvalidDataLength,
#[error("Rating greater than 5 or less than 1")]
InvalidRating,
// New error added
#[error("Accounts do not match")]
IncorrectAccountError,
}
Pretty self-explanatory :)
Open up the target folder in your file explorer and get rid of the keypair in the deploy folder.
In your console:
cargo build-bpf
Copy and paste the deploy command that's printed out.
If you get insufficient funds
just run solana airdrop 2
.
Once you've deployed locally, it's time to test! We're going to use a local client script to test out initializing the account. Here's what you'll need to set up:
git clone https://github.com/buildspace/solana-movie-token-client
cd solana-movie-token-client
npm install
Before you can run the script you'll need to:
PROGRAM_ID
in index.ts
const connection = new web3.Connection("http://localhost:8899");
solana logs PROGRAM_ID_HERE
in a second console windowYou should now have a console logging all the outputs for this program and are ready to run the script!
If you run npm start
you should see the logs for the mint account creation :D
Programs to Accelerate Your Progress in a Software Career
Join our 4-8 month intensive Patika+ bootcamps, start with the fundamentals and gain comprehensive knowledge to kickstart your software career!
You need to enroll in the course to be able to comment!