Yet another repository, part of my journey to becoming a Rust Auditor/Blockchain Developer.
Here, I have documented and developed several Solana programs while following these great tutorials.
Let’s start with some basic rules of Solana programs (smart contracts):
- In Solana, everything is an account, similar to Solidity and many other blockchains.
- A "smart contract" in Solana is often referred to as a program.
- Each account consists of a public and a private key.
- Accounts can either hold the bytecode of your programs (i.e., the raw logic of your smart contract) or store the state variables of a specific program. This differs from Solidity, where the contract itself holds its state.
- Program-derived data accounts (PDA), which store and manage program state, are created by programs themselves.
- Development and testing of Solana programs often take place directly on the devnet, as it's easier and similar to working in a local environment.
- Official Solana Documentation - Well-formed and simply explained documentation regarding the basic concepts of Solana development.
- Video Tutorials
This is my first Solana program written in Rust using the Solana Web3.js SDK—a basic "Hello World" example.
- Set up and configured the
Cargo.tomlfile to use the Solana Rust SDK. - Used the
entrypointmacro to declare the main function that executes whenever a transaction or instruction is sent to our program (essentially, the account holding the program). - Key parameters for the
process_instructionfunction:program_id: The public key of the current program.accounts: An array of accounts passed in the transaction instructions when the transaction was sent to our program.instruction_data: Additional data (in bytes) passed in the transaction.
- The simplest or most practical way to invoke our program is by using the JavaScript Web3 library for Solana, hence we also created a TypeScript client.
- The
clientcode demonstrates how a transaction is constructed and sent to our program. - In our client, we defined a
TransactionInstruction, where we passed theprogram_id,accounts(keysfield in the Solana Web3.js SDK), andinstruction_data(datafield in the Solana Web3.js SDK).
- Official Solana Documentation for Rust Programs
- Official Solana Documentation for JS Client
- Solana Web3.js Documentation
- The
math-stuffprogram provides basic functionalities like square and sum operations. - The
advanced-math-stuffprogram extendsmath-stuffby adding subtraction and division. It introduces the concept of usinginstruction_datato determine which operation to perform, all within the same program. - Both programs have state, which is stored in another account (commonly referred to as a data account), separate from the one holding the program bytecode.
- In the
math.tsfile, you can see how the client account is created and passed into thekeysfield inTransactionInstruction— this account serves as our data account.
- Official Solana Documentation - Regarding the Solana account model.
This is a basic program for transferring SOL between accounts. You can refer to the reference repo for more information on generating example accounts.
- Every account must have a minimum balance of SOL to exist on the blockchain. You cannot simply send one lamport to an account with a balance of 0 SOL. You need to meet the minimum required balance so the account can store its data on-chain.
- The
system_programmodule in the Rust Solana crate has many built-in programs to handle common operations, such as transferring SOL. - In this program, multiple accounts are passed into the
TransactionInstruction. Notably, one of the accounts must be marked as a signer to authorize the transaction.
This is the first program related to NFTs in this tutorial series, developed using vanilla Rust with the official Solana crates: solana-program, spl-token, spl-associated-token-account.
In the first video related to NFTs, the CLI is used to pseudo-create an NFT token by following these steps:
-
Create a Token Account This account will hold the related information. The token is created with 0 decimals, meaning there cannot be such a thing as 0.5 tokens:
spl-token create-token --decimals 0
The structure of the information held in an
spl_token(similar to ERC-20) at the time of writing looks like this:pub struct Mint { /// Optional authority used to mint new tokens. pub mint_authority: COption<Pubkey>, /// Total supply of tokens. pub supply: u64, /// Number of base 10 digits to the right of the decimal place. pub decimals: u8, /// Is `true` if this structure has been initialized pub is_initialized: bool, /// Optional authority to freeze token accounts. pub freeze_authority: COption<Pubkey>, }
-
Create an Account for Your Wallet This account will hold information regarding the balance of the token created in step 1:
spl-token create-account [token-pub-key]
-
Mint Tokens
Mint tokens into the account created in the previous step:spl-token mint [pub-key-of-data-account-for-your-token]
-
Disable Minting
Restrict further minting of the token, making you the only owner of the single minted token:spl-token authorize [token-pub-key] mint --disable
- The above four steps are essentially replicated in Rust, except that before creating a token account (step 1), you must first create a normal account (with sufficient storage to hold the structure shown above, which is 82 bytes in size).
- Once you create the regular account, you make it a mint account using the following command:
invoke( &token_instruction::initialize_mint( &token_program.key, &mint.key, &mint_authority.key, Some(&mint_authority.key), 0, ).unwrap(), &[ mint.clone(), mint_authority.clone(), token_program.clone(), rent.clone(), ], )?;
- Pay close attention to the ownership of specific accounts:
- Even though you create the token account, the SPL Token program is actually the owner of that account. The same applies to the account holding the balance of your custom tokens.
- Below is a diagram that illustrates the process, gathered from the video series:
This program introduces the well-known Anchor framework.
Initially, you need to install some packages and dependencies to set up your Anchor project. These steps are covered in the tutorial series, but I recommend following the official documentation to do it on your own.
It’s best to explore the framework yourself and try developing parts of the application independently. This will help you get familiar with the core concepts like Context and how to interact with deployed programs using TypeScript.
This directory also includes the next video, where we implement logic for selling the NFT created in the previous video.
- The steps to create an NFT with
Anchorare essentially the same; we just write it using anchor-lang. This applies to most of the logic. - The main difference is the ease of implementing programs and interacting with them using TypeScript. You don’t need to focus too much on structuring the transaction sent to your deployed program.
- It’s simply about initializing an object of your program and calling the function you implemented in
lib.rs, for example:
// 'program' is your deployed program, and 'mint' is the method called
await program.methods.mint(
testNftTitle, testNftSymbol, testNftUri // arguments
)
.accounts({ // required accounts:
mint: mintKeypair.publicKey,
tokenAccount: tokenAddress
})
.signers([mintKeypair]) // signer/payer of the transaction (your wallet)
.rpc();- Metaplex crate is not needed at all right now (October 2024), so the original video and repository code may not work with the latest versions of Anchor. You can check my code (hopefully, it runs on your machine) for a more recent implementation sourced from this excellent guide.
- Metaplex provides a metadata program, which we use to assign data in a specific format.
- Metaplex also offers a master edition program that creates a master edition account as proof of non-fungibility. However, the instruction
create_master_edition_v3used over 200,000 compute units, causing the transaction to fail — if anyone can help with this, it would be greatly appreciated.
- Anchor Framework
- Video Series on NFTs
- Anchor Official Documentation - Installation
- Context in Anchor
- Metaplex Metadata Program
- Metaplex Master Edition Program
This is the second program using anchor-lang and the first one that explores the fundamental concept of PDAs - Program Derived Addresses.
The image below (from the series) illustrates our use case. Though it may not be particularly useful in a real-world scenario, it provides a good example of how PDAs work and how they can be used if you need to store information on-chain, meaning you will store it in a different account than your program since we're working on Solana.
- This program demonstrates how easily Anchor handles PDAs with a simple macro like
init:
#[derive(Accounts)]
#[instruction(color: String)]
pub struct CreateLedger<'info> {
#[account(
init, // This is where the magic happens -> creating by anchor
payer = wallet,
space = 82,
seeds = [wallet.key().as_ref(), b"_", color.as_ref()],
bump
)]
pub ledger_account: Account<'info, Ledger>,
// some other accounts
}
#[account]
pub struct Ledger {
pub color: String,
pub balance: u32,
}- Interacting with the state of your program, like the instance of the
Ledgerstruct in our case, is also very easy with TypeScript:
let data;
// ...
data = await program.account.ledger.fetch(pda);
