Posted By : Ankit
Decentralized applications (dApps) are changing how we build software, and we're diving into the exciting world of Solana blockchain development using Rust. Rust is known for its efficiency and Solana for its powerful blockchain architecture. They come together to create robust and scalable DApps.
Creating a decentralized application (DApp) on Solana involves a strategic and step-by-step approach. To begin, you'll need a clear understanding of Solana's architecture and functionalities. Start by defining your DApp's purpose, outlining the core features, and considering the user experience. Utilize Solana's developer-friendly tools and documentation to set up your development environment.
Next, dive into coding the smart contracts using Rust, Solana's preferred programming language. Leverage Solana's unique features, such as its fast transaction processing and low gas fees, to optimize your dApp's performance. Test your smart contracts thoroughly to ensure security and efficiency.
Once you are done with smart contract development, proceed to integrate the front end of your DApp. Explore Solana's ecosystem tools like Wormhole for interoperability if needed. Lastly, deploy your DApp on the Solana blockchain, making it accessible to users. Throughout this process, referring to Solana's community forums and seeking feedback from experienced developers can enhance your development journey.
Anatoly Yakovenko unveiled the Solana blockchain in 2017. It stands as an open-source, public blockchain network distinguished by its innovative blend of Proof of Stake and Proof of History consensus mechanisms. Fueling peer-to-peer transactions within the network is SOL, Solana's native currency. Furthermore, Solana's dApp development encompasses the seamless integration of NFTs and smart contracts, enriching the Solana ecosystem.
The Proof of History consensus, a time-keeping technique, addresses scalability and transaction processing time challenges. According to Solana's founder, this consensus method has the potential to automate the transaction sequencing process within the Solana blockchain network.
Solana's native command-line program supports various wallet types on the blockchain. Solana recommends the use of application wallets or online browser-based wallets, with the latter providing a more user-friendly experience compared to command-line tools. Those seeking guidance on creating a dApp on Solana are likely to opt for command-line-based wallets to access the new features of the Solana blockchain.
Here are some essential steps you need to follow to build your dApp on Solana.
Step 1: Installation of Rust and Solana:
Before you start building decentralized applications (dApps) on the Solana blockchain, ensure you have the necessary tools in place. The first thing you'll need to install is Rust, a programming language used for creating smart contracts on Solana. To do this, simply use the following command or you can follow the link - https://www.rust-lang.org/tools/install
$ curl https://sh.rustup.rs -sSf | sh
Note: If you are working with Windows and do not have such command support in your system, you can use the git bash terminal from Git Hub, which provides the support to use the command in the windows as well. Install git bash terminal and use the same above command to install Rust in your Windows system.
In this phase of Solana dApp development, the next significant step entails incorporating Rust executables using the provided command.
export PATH="$HOME/.cargo/bin:$PATH"
You can initiate the initial stage of dApp development on the Solana blockchain by installing it. The following command could help in installing the Solana blockchain on your machine or you can follow the link - https://docs.solanalabs.com/cli/install
sh -c "$(curl -sSfL https://release.solana.com/v1.18.0/install)"
Step 2: Node.js and Yarn Installation
To make decentralized applications (dApps) on Solana, it's suggested to install Node.js along with some other necessary tools. Developers need to install Node.js with yarn, an important tool that works with Anchor programs. You can choose to install the recommended version of Node.js and make sure it's properly installed by using the following command.
node --version
npm --version
Developers could then use the following command for installing yarn.
npm i -g yarn
Step 3: Anchor Installation
After setting up all the important tools, you can proceed to the next stage of the Solana dApp tutorial by installing something called "Anchor." Anchor is a crucial tool for programming on the Solana blockchain. To get it installed on the Solana blockchain, simply use the following command or you can follow the link - https://www.anchor-lang.com/docs/installation
cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
Finally, you have to confirm the installation by using the following command.
anchor --version
Step 4: Project Setup
After getting all the required tools ready for your Solana dApp, the next step is setting up the specific project you're planning to work on.
But Solana is initially set up to work on the mainnet, which means each transaction comes with a unique SOL token as a kind of fee. So, when you're developing your dApp on Solana, it's important to avoid doing the same things over and over again to save time and effort.
When you're setting up to create Solana dApps, the configuration involves dealing with the following tasks.
2. Init a project within the Anchor framework.
Now, you need to set up Solana to work with the devnet network by using the following command.
solana config set --url devnet
To create a dApp on Solana, you'll need to access your wallet to run and deploy programs. Use the following command to get access to the wallet needed for Solana development.
solana-keygen new --force
When you need to make sure their wallet address is verified. You can do this using the following command.
solana address
In addition, developers building dApps on the Solana network should include the airdrop command to get test Solana tokens. You can also use the following command to check the balance in your Solana wallet.
solana balance
Now, you can set up the Anchor framework with the help of the following command.
anchor init (name-of-the-project)
After initializing the anchor with the command "anchor init (project-name)," the template is initialized with anchor, and it comes with some dependencies provided by the anchor.
For example, we initialized a project - vesting-dapp
fig(1.1)
In Fig (1.1), an example is shown for the initialization of an Anchor program using the command "anchor init vesting-dapp".
Now, we need to write our Rust code in the lib.rs file located at programs/vesting-dapp/src. Define all our instruction code in the lib.rs file.
Necessary Imports:
We need to include all the required imports in our Solana program code to access and utilize the necessary functionalities.
Code snippet:
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token::{Mint, Token, TokenAccount, Transfer},
};
anchor_lang is a crate that provides tools for developing Solana programs using the Anchor framework. The prelude module typically includes common items that are useful to have in scope for Anchor-based development.
anchor_spl is a crate that provides Solana Program Library (SPL) integration for the Anchor framework.
associated_token::AssociatedToken is related to managing associated token accounts.
token::{Mint, Token, TokenAccount, Transfer} includes various components related to token management, such as mint, token, token account, and transfer operations.
These imports are necessary for utilizing the Anchor framework for Solana program development along with SPL for managing associated token accounts and token-related functionalities.
First , we defined the struct of accounts to make the contract instruction for the dApp. Here for the vesting of tokens for lock and unlock functionality we use the structs as defined below :
Code snippet for account structs :
#[derive(InitSpace)]
#[account]
pub struct Vault {}
#[derive(InitSpace)]
#[account]
pub struct Locking {
mint: Pubkey, // mint address of token to lock
receiver: Pubkey, // receiver of locked tokens
amount: u64,
amount_unlocked: u64,
start_date: u64,
end_date: u64,
}
The above code defines two structs in the context of a Solana program, and it includes attributes (#[derive(InitSpace)] and #[account]) that are specific to Solana's programming model.
#[derive(InitSpace)]
#[account]
pub struct Vault {}
This defines a struct named Vault.
The #[derive(InitSpace)] attribute suggests that this struct is related to space initialization, meaning it might be used to create an account with a specific initial space allocation.
#[derive(InitSpace)]
#[account]
pub struct Locking {
mint: Pubkey,
receiver: Pubkey,
amount: u64,
amount_unlocked: u64,
start_date: u64,
end_date: u64,
}
This defines a struct named Locking.
The #[derive(InitSpace)] attribute indicates that this struct is also related to space initialization, suggesting it might be used to create an account with a specific initial space allocation.
The struct has fields representing information related to locking tokens:
mint: Represents the mint address of the token to be locked.
receiver: Represents the address of the account that will receive the locked tokens.
amount: Represents the total amount of tokens to be locked.
amount_unlocked: Represents the amount of tokens that have already been unlocked.
start_date: Represents the starting date of the lock as a Unix timestamp in seconds.
end_date: Represents the ending date of the lock as a Unix timestamp in seconds.
Lock Tokens functionality :
Code Snippet:
pub struct Lock<'info> {
#[account(
init_if_needed,
seeds=[b"vault"],
bump,
payer = signer,
space = Vault::INIT_SPACE + 8,
)]
pub vault: Account<'info, Vault>,
#[account(
init_if_needed,
associated_token::mint = mint,
associated_token::authority = vault,
payer = signer,
)]
pub vault_ata: Account<'info, TokenAccount>,
#[account(
init,
seeds=[b"locking", receiver.key().as_ref(), mint.key().as_ref()],
bump,
payer = signer,
space = Locking::INIT_SPACE + 8,
)]
pub locking: Account<'info, Locking>,
#[account(
mut,
constraint = signer_ata.owner.key() == signer.key(),
constraint = signer_ata.mint.key() == mint.key(),
)]
pub signer_ata: Account<'info, TokenAccount>,
#[account(mut)]
pub signer: Signer<'info>,
pub mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
For lock token functionality , the first part after account structs , for our rust code to lock tokens-
A struct named Lock is being defined. This struct seems to represent a set of data or accounts needed for locking functionality in a Solana blockchain-based decentralized application (dApp).
The struct has various fields representing Solana program accounts, such as vault, vault_ata, locking, signer_ata, signer, mint, token_program, associated_token_program, and system_program.
Each account field has specific attributes (e.g., #[account(init_if_needed)], #[account(mut)]) indicating how these accounts should be treated during program execution.
Constraints and initialization conditions are specified for certain accounts. For example, vault and vault_ata have initialization conditions, ensuring they are created if needed. The locking account is initialized with specific seed values and space allocation.
There is an associated token account (vault_ata) associated with the vault account. The authority for this associated token account is set to the vault account itself.
Constraints are specified for the signer_ata account, ensuring that it is mutable and that its owner and mint are related to the signer and mint accounts, respectively.
The signer field represents an account that must be a signer for the transaction. The mint, token_program, associated_token_program, and system_program fields are references to other necessary Solana programs.
Unlock Tokens functionality :
Code Snippet:
#[derive(Accounts)]
pub struct Unlock<'info> {
#[account(
mut,
seeds=[b"vault"],
bump,
)]
pub vault: Account<'info, Vault>,
#[account(
mut,
constraint = vault_ata.owner == vault.key(),
constraint = vault_ata.mint == mint.key(),
)]
pub vault_ata: Account<'info, TokenAccount>,
#[account(
init_if_needed,
associated_token::mint = mint,
associated_token::authority = receiver,
payer = signer,
)]
pub receiver_ata: Account<'info, TokenAccount>,
/// CHECK: Just a public key of a Solana account.
pub receiver: AccountInfo<'info>,
#[account(
mut,
seeds=[b"locking", receiver.key().as_ref(), mint.key().as_ref()],
bump,
constraint = locking.receiver.key() == receiver.key(),
constraint = locking.mint.key() == mint.key(),
constraint = locking.amount_unlocked < locking.amount,
)]
pub locking: Account<'info, Locking>,
#[account(mut)]
pub signer: Signer<'info>,
pub mint: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
For the functionality to unlock tokens in our Rust code, a struct named Unlock has been defined. This struct encapsulates the necessary data and accounts required for the unlocking process within a Solana blockchain-based decentralized application (dApp).
The Unlock struct comprises various fields that represent Solana program accounts essential for the unlocking functionality. These fields include vault, vault_ata, receiver_ata, receiver, locking, signer, mint, token_program, associated_token_program, and system_program.
Attributes are assigned to each account field to specify how these accounts should be treated during the execution of the Solana program. These attributes include #[account(init_if_needed)] and #[account(mut)], among others.
Constraints and initialization conditions are defined for specific accounts within the struct. For instance, the vault and vault_ata accounts have initialization conditions, ensuring they are created if necessary. The locking account undergoes initialization with specific seed values and space allocation.
An associated token account (vault_ata) is linked to the vault account. The authority for this associated token account is set to the vault account itself, establishing a relationship for managing associated tokens.
Constraints are outlined for the signer_ata account, ensuring mutability and establishing relationships between its owner and mint with the signer and mint accounts, respectively.
The signer field designates an account that must act as a signer for the transaction, contributing to the security and authorization of the unlocking process. References to other essential Solana programs, such as mint, token_program, associated_token_program, and system_program, are also included in the struct.
Functions for locking tokens:
Code snippet:
pub fn lock(
ctx: Context<Lock>,
receiver: Pubkey,
amount: u64,
start_date: u64,
end_date: u64,
) -> Result<()> {
let locking = &mut ctx.accounts.locking;
let mint = &ctx.accounts.mint;
let signer = &ctx.accounts.signer;
let vault_ata = &ctx.accounts.vault_ata;
let signer_ata = &ctx.accounts.signer_ata;
let token_program = &ctx.accounts.token_program;
require!(end_date > start_date, CustomError::EndBeforeStart);
let transfer = Transfer {
from: signer_ata.to_account_info(),
to: vault_ata.to_account_info(),
authority: signer.to_account_info(),
};
let token_transfer_context = CpiContext::new(token_program.to_account_info(), transfer);
token::transfer(token_transfer_context, amount)?;
locking.mint = mint.key();
locking.receiver = receiver;
locking.amount = amount;
locking.amount_unlocked = 0;
locking.start_date = start_date;
locking.end_date = end_date;
Ok(())
}
This above code defines a Rust function named lock within the vesting_program module.
The function takes several parameters: ctx (a context containing various accounts needed for the operation), receiver (the recipient's address), amount (the number of tokens to lock), start_date and end_date (timestamps indicating the locking period).
The function refers to specific accounts through the ctx context. These include accounts like locking, mint, signer, vault_ata, signer_ata, and token_program. These accounts are needed for the token-locking operation.
There's a check (require!(end_date > start_date, CustomError::EndBeforeStart);) to ensure that the end date for locking tokens is after the start date. If not, it raises a custom error (CustomError::EndBeforeStart).
The function initiates a transfer of tokens using the token::transfer function from the Solana Token program (token_program). It transfers amount tokens from the signer's associated token account (signer_ata) to the vault's associated token account (vault_ata).
The locking struct is updated with relevant information. It sets the mint, receiver, amount, amount_unlocked (initialized to 0), start_date, and end_date.
The function returns a Result<()> indicating whether the operation was successful or if an error occurred.
Overall, this lock function is like a set of instructions for locking a specific amount of tokens for a certain period. It checks that the specified period is valid, transfers the tokens from one account to another, and updates information about the lock. The function ensures that everything is done correctly and reports if there are any issues.
pub fn unlock(ctx: Context<Unlock>) -> Result<()> {
let locking = &mut ctx.accounts.locking;
let vault = &ctx.accounts.vault;
let vault_ata = &ctx.accounts.vault_ata;
let receiver_ata = &ctx.accounts.receiver_ata;
let token_program = &ctx.accounts.token_program;
let now: u64 = Clock::get().unwrap().unix_timestamp.try_into().unwrap();
require!(now > locking.start_date, CustomError::CliffPeriodNotPassed);
let passed_seconds = now - locking.start_date;
let total_seconds = locking.end_date - locking.start_date;
let entitled_amount = if now >= locking.end_date {
locking.amount - locking.amount_unlocked
} else {
locking.amount * passed_seconds / total_seconds - locking.amount_unlocked
};
let seeds = [b"vault".as_ref(), &[ctx.bumps.vault]];
let signer = &[&seeds[..]];
let transfer = Transfer {
from: vault_ata.to_account_info(),
to: receiver_ata.to_account_info(),
authority: vault.to_account_info(),
};
let token_transfer_context =
CpiContext::new_with_signer(token_program.to_account_info(), transfer, signer);
token::transfer(token_transfer_context, entitled_amount)?;
locking.amount_unlocked += entitled_amount;
Ok(())
}
This above code defines a Rust function named unlock within the vesting_program module.
The function references various accounts needed for the unlocking process, such as locking, vault, vault_ata, receiver_ata, and token_program.
It retrieves the current timestamp (now) using the Solana Clock::get() function. This represents the current time in Unix timestamp format.
The function checks if the current time is greater than the locking's start date. If not, it raises a custom error (CustomError::CliffPeriodNotPassed). This ensures that the "cliff period" has passed before unlocking can occur.
It calculates the entitled amount of tokens to be unlocked based on the current time relative to the locking period. If the current time is after the locking's end date, the entitled amount is the remaining locked tokens. Otherwise, it calculates the entitlement based on the time passed within the locking period.
It initiates a token transfer from the vault's associated token account (vault_ata) to the receiver's associated token account (receiver_ata). The authority for this transfer is the vault itself. The transfer amount is the previously calculated entitled_amount.
The locking struct's amount_unlocked field is updated by adding the entitled_amount.
The function returns a Result<()> indicating whether the operation was successful or if an error occurred.
This unlock function checks if the cliff period has passed, calculates the entitled amount of tokens to be unlocked, transfers those tokens from the vault to the receiver, and updates the amount of tokens that have been unlocked. This function is a key part of the Solana program's functionality for unlocking vested tokens.
Error Codes used :
Code Snippet:
#[error_code]
pub enum CustomError {
EndBeforeStart,
CliffPeriodNotPassed,
}
The #[error_code] attribute is used to indicate that this enum is intended to represent error codes. In Solana programs, custom errors are often used to handle specific failure conditions gracefully.
The CustomError enum is declared to represent different types of custom errors that can occur during the execution of the Solana program.
There are two error variants within the enum:
EndBeforeStart: This represents an error condition where the end date for a certain operation is before the start date.
CliffPeriodNotPassed: This represents an error condition where a specified "cliff period" has not passed.
The CustomError enum defines specific error conditions that can occur in the Solana program, providing a structured way to handle and communicate different failure scenarios.
Certainly! After completing the development of your Solana program, the next steps typically involve building and deploying the program. Here's a general guide:
You may need to run specific Anchor commands to build the program. This often includes using anchor build
Once the program is successfully built, you can deploy it to the Solana blockchain. Use the anchor deploy command to deploy the program. This command may require specifying the network (e.g., devnet, testnet, or mainnet-beta) and providing relevant configuration details.
anchor deploy --network <network>
After deploying the program, you can interact with it using various commands or scripts. Depending on the functionality of your program, this might involve invoking specific transactions or methods within your Solana program.
Solana: https://docs.solanalabs.com/
Rust : https://www.rust-lang.org/
Anchor : https://book.anchor-lang.com/
September 8, 2024 at 02:28 am
Your comment is awaiting moderation.