My friend Sameer and I are currently working on our first Web3 project together, and we’re trying to learn how to use Solana. So far it’s been kind of complicated and a bit of a pain, so I just wanted to jot down a few notes here in this blog post for future reference. Mainly, I want to record my notes on getting to “hello, world” in Solana using Rust & Web3.js.
So far I haven’t found a great tutorial yet, however, there’s an okay course called recommended by the Solana team called Solana 101 that my friend and I both just took, and I think it has most of the basic building blocks you’ll need to get started with Solana programming.
If you’d like to take the Solana 101 course yourself, you can do that here: Solana 101
I believe the course used to be something you could take on Figment’s website, but now it’s simply a Github repo that you download. After you download the repo you an follow the instructions in the README to launch the course in your web browser running off of a local web server running on your machine.
Now for the real meat of what I wanted to post here… The course above has like 12 different lessons all featuring a code snippet demonstrating how to do some basic operation in Solana. However, the course isn’t hosted on the internet anymore! If you want to reference it or take it again, you have to spin your local web server up again to read the code snippets. So I decided that I really wanted to have a copy of all the code snippets up on the web that I could reference when needed. So without further ado, the 12 essential code snippets from Figment’s Solana 101 Course:
Solana 101: Connect to Solana (From Figment.io’s Solana 101 Course)
import type {NextApiRequest, NextApiResponse} from 'next'; | |
import {getNodeURL} from '@figment-solana/lib'; | |
import {Connection} from '@solana/web3.js'; | |
export default async function connect( | |
req: NextApiRequest, | |
res: NextApiResponse<string>, | |
) { | |
try { | |
const {network} = req.body; | |
const url = getNodeURL(network); | |
const connection = new Connection(url, 'confirmed'); | |
const version = await connection.getVersion(); | |
res.status(200).json(version['solana-core']); | |
} catch (error) { | |
let errorMessage = error instanceof Error ? error.message : 'Unknown Error'; | |
res.status(500).json(errorMessage); | |
} | |
} |
Solana 101: Creating Solana Accounts AKA Keypair (From Figment.io’s Solana 101 Course)
import type {NextApiRequest, NextApiResponse} from 'next'; | |
import {Keypair} from '@solana/web3.js'; | |
/* | |
* Like with most Web 3 protocols, transactions on Solana happen between accounts. | |
* To create an account, a client generates a keypair which has a public key (or | |
* address, used to identify and lookup an account) and a secret key used to sign | |
* transactions. | |
*/ | |
type ResponseT = { | |
secret: string; | |
address: string; | |
}; | |
export default function keypair( | |
_req: NextApiRequest, | |
res: NextApiResponse<string | ResponseT>, | |
) { | |
try { | |
const keypair = Keypair.generate(); | |
const address = keypair?.publicKey.toString(); | |
const secret = JSON.stringify(Array.from(keypair.secretKey)); | |
res.status(200).json({ | |
secret, | |
address, | |
}); | |
} catch (error) { | |
let errorMessage = error instanceof Error ? error.message : 'Unknown Error'; | |
res.status(500).json(errorMessage); | |
} | |
} |
Solana 101: Funding Your Solana Account via Airdrop (From Figment.io’s Solana 101 Course)
import {Connection, PublicKey, LAMPORTS_PER_SOL} from '@solana/web3.js'; | |
import type {NextApiRequest, NextApiResponse} from 'next'; | |
import {getNodeURL} from '@figment-solana/lib'; | |
export default async function fund( | |
req: NextApiRequest, | |
res: NextApiResponse<string>, | |
) { | |
try { | |
const {network, address} = req.body; | |
const url = getNodeURL(network); | |
const connection = new Connection(url, 'confirmed'); | |
const publicKey = new PublicKey(address); | |
const hash = await connection.requestAirdrop(publicKey, LAMPORTS_PER_SOL); | |
await undefined; | |
res.status(200).json(hash); | |
} catch (error) { | |
let errorMessage = error instanceof Error ? error.message : 'Unknown Error'; | |
res.status(500).json(errorMessage); | |
} | |
} | |
Solana 101: Getting Your Balance (From Figment.io’s Solana 101 Course)
import type {NextApiRequest, NextApiResponse} from 'next'; | |
import {Connection, PublicKey} from '@solana/web3.js'; | |
import {getNodeURL} from '@figment-solana/lib'; | |
export default async function balance( | |
req: NextApiRequest, | |
res: NextApiResponse<string | number>, | |
) { | |
try { | |
const {network, address} = req.body; | |
const url = getNodeURL(network); | |
const connection = new Connection(url, 'confirmed'); | |
const publicKey = new PublicKey(address); | |
const balance = await connection.getBalance(publicKey); | |
if (balance === 0 || balance === undefined) { | |
throw new Error('Account not funded'); | |
} | |
res.status(200).json(balance); | |
} catch (error) { | |
let errorMessage = error instanceof Error ? error.message : 'Unknown Error'; | |
res.status(500).json(errorMessage); | |
} | |
} |
Solana 101: Transfering Funds (From Figment.io’s Solana 101 Course)
import type {NextApiRequest, NextApiResponse} from 'next'; | |
import {getNodeURL} from '@figment-solana/lib'; | |
import { | |
Connection, | |
PublicKey, | |
SystemProgram, | |
Transaction, | |
sendAndConfirmTransaction, | |
} from '@solana/web3.js'; | |
export default async function transfer( | |
req: NextApiRequest, | |
res: NextApiResponse<string>, | |
) { | |
try { | |
const {address, secret, recipient, lamports, network} = req.body; | |
const url = getNodeURL(network); | |
const connection = new Connection(url, 'confirmed'); | |
const fromPubkey = new PublicKey(address); | |
const toPubkey = new PublicKey(recipient); | |
// The secret key is stored in our state as a stringified array | |
const secretKey = Uint8Array.from(JSON.parse(secret as string)); | |
//... let's skip the beginning as it should be familiar for you by now! | |
// Find the parameter to pass | |
const instructions = SystemProgram.transfer({ | |
fromPubkey, | |
toPubkey, | |
lamports, | |
}); | |
// How could you construct a signer array's | |
const signers = [ | |
{ | |
publicKey: fromPubkey, | |
secretKey, | |
}, | |
]; | |
// Maybe adding something to a Transaction could be interesting ? | |
const transaction = new Transaction().add(instructions); | |
// We can send and confirm a transaction in one row. | |
const hash = await sendAndConfirmTransaction(connection, transaction, signers); | |
res.status(200).json(hash); | |
} catch (error) { | |
let errorMessage = error instanceof Error ? error.message : 'Unknown Error'; | |
res.status(500).json(errorMessage); | |
} | |
} |
Solana 101: Deploy Rust Program to Solana Blockchain (from Figment.io’s Solana 101 Course)
use borsh::{BorshDeserialize, BorshSerialize}; | |
use solana_program::{ | |
account_info::{next_account_info, AccountInfo}, | |
entrypoint, | |
entrypoint::ProgramResult, | |
msg, | |
program_error::ProgramError, | |
pubkey::Pubkey, | |
}; | |
/// Define the type of state stored in accounts | |
#[derive(BorshSerialize, BorshDeserialize, Debug)] | |
pub struct GreetingAccount { | |
/// number of greetings | |
pub counter: u32, | |
} | |
// Declare and export the program's entrypoint | |
entrypoint!(process_instruction); | |
// Program entrypoint's implementation | |
pub fn process_instruction( | |
program_id: &Pubkey, // Public key of the account the hello world program was loaded into | |
accounts: &[AccountInfo], // The account to say hello to | |
_instruction_data: &[u8], // Ignored, all helloworld instructions are hellos | |
) -> ProgramResult { | |
msg!("Hello World Rust program entrypoint"); | |
// Iterating accounts is safer then indexing | |
let accounts_iter = &mut accounts.iter(); | |
// Get the account to say hello to | |
let account = next_account_info(accounts_iter)?; | |
// The account must be owned by the program in order to modify its data | |
if account.owner != program_id { | |
msg!("Greeted account does not have the correct program id"); | |
return Err(ProgramError::IncorrectProgramId); | |
} | |
// Increment and store the number of times the account has been greeted | |
let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?; | |
greeting_account.counter += 1; | |
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?; | |
msg!("Greeted {} time(s)!", greeting_account.counter); | |
Ok(()) | |
} | |
// Sanity tests | |
#[cfg(test)] | |
mod test { | |
use super::*; | |
use solana_program::clock::Epoch; | |
use std::mem; | |
#[test] | |
fn test_sanity() { | |
let program_id = Pubkey::default(); | |
let key = Pubkey::default(); | |
let mut lamports = 0; | |
let mut data = vec![0; mem::size_of::<u32>()]; | |
let owner = Pubkey::default(); | |
let account = AccountInfo::new( | |
&key, | |
false, | |
true, | |
&mut lamports, | |
&mut data, | |
&owner, | |
false, | |
Epoch::default(), | |
); | |
let instruction_data: Vec<u8> = Vec::new(); | |
let accounts = vec![account]; | |
assert_eq!( | |
GreetingAccount::try_from_slice(&accounts[0].data.borrow()) | |
.unwrap() | |
.counter, | |
0 | |
); | |
process_instruction(&program_id, &accounts, &instruction_data).unwrap(); | |
assert_eq!( | |
GreetingAccount::try_from_slice(&accounts[0].data.borrow()) | |
.unwrap() | |
.counter, | |
1 | |
); | |
process_instruction(&program_id, &accounts, &instruction_data).unwrap(); | |
assert_eq!( | |
GreetingAccount::try_from_slice(&accounts[0].data.borrow()) | |
.unwrap() | |
.counter, | |
2 | |
); | |
} | |
} |
Solana 101: Check Deployed Solana Program from Client (From Figment.io’s Solana 101 Course)
import type {NextApiRequest, NextApiResponse} from 'next'; | |
import {Connection, PublicKey} from '@solana/web3.js'; | |
import {getNodeURL} from '@figment-solana/lib'; | |
import path from 'path'; | |
import fs from 'mz/fs'; | |
const PROGRAM_PATH = path.resolve('dist/solana/program'); | |
const PROGRAM_SO_PATH = path.join(PROGRAM_PATH, 'helloworld.so'); | |
export default async function deploy( | |
req: NextApiRequest, | |
res: NextApiResponse<string | boolean>, | |
) { | |
try { | |
const {network, programId} = req.body; | |
const url = getNodeURL(network); | |
const connection = new Connection(url, 'confirmed'); | |
// Re-create publicKeys from params | |
const publicKey = new PublicKey(programId); | |
const programInfo = await connection.getAccountInfo(publicKey); | |
if (programInfo === null) { | |
if (fs.existsSync(PROGRAM_SO_PATH)) { | |
throw new Error( | |
'Program needs to be deployed with `solana program deploy`', | |
); | |
} else { | |
throw new Error('Program needs to be built and deployed'); | |
} | |
} else if (!programInfo.executable) { | |
throw new Error(`Program is not executable`); | |
} | |
res.status(200).json(true); | |
} catch (error) { | |
let errorMessage = error instanceof Error ? error.message : 'Unknown Error'; | |
res.status(500).json(errorMessage); | |
} | |
} |
Solana 101: Storing Data on the Solana Blockchain (From Figment.io’s Solana 101 Course)
import { | |
Connection, | |
PublicKey, | |
Keypair, | |
SystemProgram, | |
Transaction, | |
sendAndConfirmTransaction, | |
} from '@solana/web3.js'; | |
import type {NextApiRequest, NextApiResponse} from 'next'; | |
import {getNodeURL} from '@figment-solana/lib'; | |
import * as borsh from 'borsh'; | |
// The state of a greeting account managed by the hello world program | |
class GreetingAccount { | |
counter = 0; | |
constructor(fields: {counter: number} | undefined = undefined) { | |
if (fields) { | |
this.counter = fields.counter; | |
} | |
} | |
} | |
// Borsh schema definition for greeting accounts | |
const GreetingSchema = new Map([ | |
[GreetingAccount, {kind: 'struct', fields: [['counter', 'u32']]}], | |
]); | |
// The expected size of each greeting account. | |
const GREETING_SIZE = borsh.serialize( | |
GreetingSchema, | |
new GreetingAccount(), | |
).length; | |
type ResponseT = { | |
hash: string; | |
greeter: string; | |
}; | |
export default async function greeter( | |
req: NextApiRequest, | |
res: NextApiResponse<string | ResponseT>, | |
) { | |
try { | |
const {network, secret, programId: programAddress} = req.body; | |
const url = getNodeURL(network); | |
const connection = new Connection(url, 'confirmed'); | |
const programId = new PublicKey(programAddress); | |
const payer = Keypair.fromSecretKey(new Uint8Array(JSON.parse(secret))); | |
const GREETING_SEED = 'hello'; | |
// Are there any methods from PublicKey to derive a public key from a seed? | |
const greetedPubkey = await PublicKey.createWithSeed( | |
payer.publicKey, | |
GREETING_SEED, | |
programId, | |
); | |
// This function calculates the fees we have to pay to keep the newly | |
// created account alive on the blockchain. We're naming it lamports because | |
// that is the denomination of the amount being returned by the function. | |
const lamports = await connection.getMinimumBalanceForRentExemption( | |
GREETING_SIZE, | |
); | |
// Find which instructions are expected and complete SystemProgram with | |
// the required arguments. | |
const transaction = new Transaction().add(SystemProgram.createAccountWithSeed({ | |
fromPubkey: payer.publicKey, | |
basePubkey: payer.publicKey, | |
seed: GREETING_SEED, | |
newAccountPubkey: greetedPubkey, | |
lamports, | |
space: GREETING_SIZE, | |
programId, | |
})); | |
// Complete this function call with the expected arguments. | |
const hash = await sendAndConfirmTransaction(connection, transaction, [payer]); | |
res.status(200).json({ | |
hash: hash, | |
greeter: greetedPubkey.toBase58(), | |
}); | |
} catch (error) { | |
let errorMessage = error instanceof Error ? error.message : 'Unknown Error'; | |
res.status(500).json(errorMessage); | |
} | |
} |
Solana 101: Get Data from Solana Blockchain (From Figment.io’s Solana 101 Course)
import type {NextApiRequest, NextApiResponse} from 'next'; | |
import {Connection, PublicKey} from '@solana/web3.js'; | |
import {getNodeURL} from '@figment-solana/lib'; | |
import * as borsh from 'borsh'; | |
// The state of a greeting account managed by the hello world program | |
class GreetingAccount { | |
counter = 0; | |
constructor(fields: {counter: number} | undefined = undefined) { | |
if (fields) { | |
this.counter = fields.counter; | |
} | |
} | |
} | |
// Borsh schema definition for greeting accounts | |
const GreetingSchema = new Map([ | |
[GreetingAccount, {kind: 'struct', fields: [['counter', 'u32']]}], | |
]); | |
export default async function getter( | |
req: NextApiRequest, | |
res: NextApiResponse<string | number>, | |
) { | |
try { | |
const {network, greeter} = req.body; | |
const url = getNodeURL(network); | |
const connection = new Connection(url, 'confirmed'); | |
const greeterPublicKey = new PublicKey(greeter); | |
const accountInfo = await connection.getAccountInfo(greeterPublicKey); | |
if (accountInfo === null) { | |
throw new Error('Error: cannot find the greeted account'); | |
} | |
// Find the expected parameters. | |
const greeting = borsh.deserialize( | |
GreetingSchema, | |
GreetingAccount, | |
accountInfo.data, | |
); | |
// A little helper | |
console.log(greeting); | |
// Pass the counter to the client-side as JSON | |
res.status(200).json(greeting.counter); | |
} catch (error) { | |
let errorMessage = error instanceof Error ? error.message : 'Unknown Error'; | |
console.log(errorMessage); | |
res.status(500).json(errorMessage); | |
} | |
} |
Solana 101: Set Data on Solana Blockchain (From Figment.io’s Solana 101 Course)
import { | |
Connection, | |
PublicKey, | |
Keypair, | |
TransactionInstruction, | |
Transaction, | |
sendAndConfirmTransaction, | |
} from '@solana/web3.js'; | |
import type {NextApiRequest, NextApiResponse} from 'next'; | |
import {getNodeURL} from '@figment-solana/lib'; | |
export default async function setter( | |
req: NextApiRequest, | |
res: NextApiResponse<string>, | |
) { | |
try { | |
const {greeter, secret, programId, network} = req.body; | |
const url = getNodeURL(network); | |
const connection = new Connection(url, 'confirmed'); | |
const greeterPublicKey = new PublicKey(greeter); | |
const programKey = new PublicKey(programId); | |
const payerSecretKey = new Uint8Array(JSON.parse(secret)); | |
const payerKeypair = Keypair.fromSecretKey(payerSecretKey); | |
// this your turn to figure out | |
// how to create this instruction | |
const instruction = new TransactionInstruction({ | |
keys: [{pubkey: greeterPublicKey, isSigner: false, isWritable: true}], | |
programId: programKey, | |
data: Buffer.alloc(0), | |
}); | |
// this your turn to figure out | |
// how to create this transaction | |
const hash = await sendAndConfirmTransaction( | |
connection, | |
new Transaction().add(instruction), | |
[payerKeypair] | |
); | |
res.status(200).json(hash); | |
} catch (error) { | |
console.error(error); | |
res.status(500).json('Get balance failed'); | |
} | |
} |