Menulis Program
Bagaimana cara mentransfer SOL di dalam sebuah program
Program Solana anda dapat mentransfer lamports dari sebuah akun ke akun lainnya tanpa 'invoke' program system. Aturan dasar adalah program anda dapat mentransfer lamports dari akun mana saja yang dimiliki oleh program anda ke semua akun.
Penerima akun tidak harus menjadi sebuah akun yang dimilik oleh program anda.
/// Transfers lamports from one account (must be program owned)
/// to another account. The recipient can by any account
fn transfer_service_fee_lamports(
from_account: &AccountInfo,
to_account: &AccountInfo,
amount_of_lamports: u64,
) -> ProgramResult {
// Does the from account have enough lamports to transfer?
if **from_account.try_borrow_lamports()? < amount_of_lamports {
return Err(CustomError::InsufficientFundsForTransaction.into());
}
// Debit from_account and credit to_account
**from_account.try_borrow_mut_lamports()? -= amount_of_lamports;
**to_account.try_borrow_mut_lamports()? += amount_of_lamports;
Ok(())
}
/// Primary function handler associated with instruction sent
/// to your program
fn instruction_handler(accounts: &[AccountInfo]) -> ProgramResult {
// Get the 'from' and 'to' accounts
let account_info_iter = &mut accounts.iter();
let from_account = next_account_info(account_info_iter)?;
let to_service_account = next_account_info(account_info_iter)?;
// Extract a service 'fee' of 5 lamports for performing this instruction
transfer_service_fee_lamports(from_account, to_service_account, 5u64)?;
// Perform the primary instruction
// ... etc.
Ok(())
}
Bagaimana cara mendapatkan waktu dalam sebuah program
Mendapatkan waktu dapat dilakukan melalui 2 cara
- Passing
SYSVAR_CLOCK_PUBKEY
ke sebuah instruksi - Mengakses waktu secara langsung di dalam sebuah instruksi.
Adalah baik untuk mengetahui dua cara tersebut, karena beberapa legacy program masih memerlukan SYSVAR_CLOCK_PUBKEY
sebagai sebuah akun.
Passing Waktu sebagai sebuah account di dalam sebuah instruksi
Mari membuat sebuah instruksi yang menerima sebuah akun untuk menginisiasi dan sysvar pubkey
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint,
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
sysvar::Sysvar,
};
entrypoint!(process_instruction);
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
is_initialized: bool,
}
// Accounts required
/// 1. [signer, writable] Payer
/// 2. [writable] Hello state account
/// 3. [] Clock sys var
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
// Payer account
let _payer_account = next_account_info(accounts_iter)?;
// Hello state account
let hello_state_account = next_account_info(accounts_iter)?;
// Clock sysvar
let sysvar_clock_pubkey = next_account_info(accounts_iter)?;
let mut hello_state = HelloState::try_from_slice(&hello_state_account.data.borrow())?;
hello_state.is_initialized = true;
hello_state.serialize(&mut &mut hello_state_account.data.borrow_mut()[..])?;
msg!("Account initialized :)");
// Type casting [AccountInfo] to [Clock]
let clock = Clock::from_account_info(&sysvar_clock_pubkey)?;
// Getting timestamp
let current_timestamp = clock.unix_timestamp;
msg!("Current Timestamp: {}", current_timestamp);
Ok(())
}
let clock = Clock::from_account_info(&sysvar_clock_pubkey)?;
let current_timestamp = clock.unix_timestamp;
Now we pass the clock's sysvar public address via the client
import {
clusterApiUrl,
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
SYSVAR_CLOCK_PUBKEY,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
(async () => {
const programId = new PublicKey(
"77ezihTV6mTh2Uf3ggwbYF2NyGJJ5HHah1GrdowWJVD3"
);
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
// Airdropping 1 SOL
const feePayer = Keypair.generate();
await connection.confirmTransaction(
await connection.requestAirdrop(feePayer.publicKey, LAMPORTS_PER_SOL)
);
// Hello state account
const helloAccount = Keypair.generate();
const accountSpace = 1; // because there exists just one boolean variable
const rentRequired = await connection.getMinimumBalanceForRentExemption(
accountSpace
);
// Allocating space for hello state account
const allocateHelloAccountIx = SystemProgram.createAccount({
fromPubkey: feePayer.publicKey,
lamports: rentRequired,
newAccountPubkey: helloAccount.publicKey,
programId: programId,
space: accountSpace,
});
// Passing Clock Sys Var
const passClockIx = new TransactionInstruction({
programId: programId,
keys: [
{
isSigner: true,
isWritable: true,
pubkey: feePayer.publicKey,
},
{
isSigner: false,
isWritable: true,
pubkey: helloAccount.publicKey,
},
{
isSigner: false,
isWritable: false,
pubkey: SYSVAR_CLOCK_PUBKEY,
},
],
});
const transaction = new Transaction();
transaction.add(allocateHelloAccountIx, passClockIx);
const txHash = await connection.sendTransaction(transaction, [
feePayer,
helloAccount,
]);
console.log(`Transaction succeeded. TxHash: ${txHash}`);
})();
(async () => {
const programId = new PublicKey(
"77ezihTV6mTh2Uf3ggwbYF2NyGJJ5HHah1GrdowWJVD3"
);
// Passing Clock Sys Var
const passClockIx = new TransactionInstruction({
programId: programId,
keys: [
{
isSigner: false,
isWritable: true,
pubkey: helloAccount.publicKey,
},
{
is_signer: false,
is_writable: false,
pubkey: SYSVAR_CLOCK_PUBKEY,
},
],
});
const transaction = new Transaction();
transaction.add(passClockIx);
const txHash = await connection.sendTransaction(transaction, [
feePayer,
helloAccount,
]);
console.log(`Transaction succeeded. TxHash: ${txHash}`);
})();
Mengakses Waktu secara langsung di dalam sebuah instruksi
Mari membuat sebuah instruksi yang sama, tetapi tanpa mengharapkan SYSVAR_CLOCK_PUBKEY
dari client side.
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint,
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
sysvar::Sysvar,
};
entrypoint!(process_instruction);
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
is_initialized: bool,
}
// Accounts required
/// 1. [signer, writable] Payer
/// 2. [writable] Hello state account
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
// Payer account
let _payer_account = next_account_info(accounts_iter)?;
// Hello state account
let hello_state_account = next_account_info(accounts_iter)?;
// Getting clock directly
let clock = Clock::get()?;
let mut hello_state = HelloState::try_from_slice(&hello_state_account.data.borrow())?;
hello_state.is_initialized = true;
hello_state.serialize(&mut &mut hello_state_account.data.borrow_mut()[..])?;
msg!("Account initialized :)");
// Getting timestamp
let current_timestamp = clock.unix_timestamp;
msg!("Current Timestamp: {}", current_timestamp);
Ok(())
}
let clock = Clock::get()?;
let current_timestamp = clock.unix_timestamp;
Instruksi di client side sekarang hanya perlu diberikan state dan akun pembayar.
import {
clusterApiUrl,
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
(async () => {
const programId = new PublicKey(
"4ZEdbCtb5UyCSiAMHV5eSHfyjq3QwbG3yXb6oHD7RYjk"
);
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
// Airdropping 1 SOL
const feePayer = Keypair.generate();
await connection.confirmTransaction(
await connection.requestAirdrop(feePayer.publicKey, LAMPORTS_PER_SOL)
);
// Hello state account
const helloAccount = Keypair.generate();
const accountSpace = 1; // because there exists just one boolean variable
const rentRequired = await connection.getMinimumBalanceForRentExemption(
accountSpace
);
// Allocating space for hello state account
const allocateHelloAccountIx = SystemProgram.createAccount({
fromPubkey: feePayer.publicKey,
lamports: rentRequired,
newAccountPubkey: helloAccount.publicKey,
programId: programId,
space: accountSpace,
});
const initIx = new TransactionInstruction({
programId: programId,
keys: [
{
isSigner: true,
isWritable: true,
pubkey: feePayer.publicKey,
},
{
isSigner: false,
isWritable: true,
pubkey: helloAccount.publicKey,
},
],
});
const transaction = new Transaction();
transaction.add(allocateHelloAccountIx, initIx);
const txHash = await connection.sendTransaction(transaction, [
feePayer,
helloAccount,
]);
console.log(`Transaction succeeded. TxHash: ${txHash}`);
})();
(async () => {
const programId = new PublicKey(
"4ZEdbCtb5UyCSiAMHV5eSHfyjq3QwbG3yXb6oHD7RYjk"
);
// No more requirement to pass clock sys var key
const initAccountIx = new TransactionInstruction({
programId: programId,
keys: [
{
isSigner: false,
isWritable: true,
pubkey: helloAccount.publicKey,
},
],
});
const transaction = new Transaction();
transaction.add(initAccountIx);
const txHash = await connection.sendTransaction(transaction, [
feePayer,
helloAccount,
]);
console.log(`Transaction succeeded. TxHash: ${txHash}`);
})();
Bagaimana cara merubah size akun
Anda dapat merubah size program yang dimliiki dengan mengunakan realloc
. realloc
dapat meresize sebuah akun sampai 10KB. Ketika anda mengunakan realloc
untuk menambah size dari sebuah akun, anda haru mentransfer lamport secara berurutan untuk menjaga agar akun rent-exempt.
use {
crate::{
instruction::WhitelistInstruction,
state::WhiteListData,
},
borsh::{BorshDeserialize, BorshSerialize},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
program::invoke_signed,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
sysvar::Sysvar,
sysvar::rent::Rent,
system_instruction,
},
std::convert::TryInto,
};
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
input: &[u8],
) -> ProgramResult {
// Length = BOOL + VEC + Pubkey * n (n = number of keys)
const INITIAL_ACCOUNT_LEN: usize = 1 + 4 + 0 ;
msg!("input: {:?}", input);
let instruction = WhitelistInstruction::try_from_slice(input)?;
let accounts_iter = &mut accounts.iter();
let funding_account = next_account_info(accounts_iter)?;
let pda_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
match instruction {
WhitelistInstruction::Initialize => {
msg!("Initialize");
let (pda, pda_bump) = Pubkey::find_program_address(
&[
b"customaddress",
&funding_account.key.to_bytes(),
],
_program_id,
);
let signers_seeds: &[&[u8]; 3] = &[
b"customaddress",
&funding_account.key.to_bytes(),
&[pda_bump],
];
if pda.ne(&pda_account.key) {
return Err(ProgramError::InvalidAccountData);
}
let lamports_required = Rent::get()?.minimum_balance(INITIAL_ACCOUNT_LEN);
let create_pda_account_ix = system_instruction::create_account(
&funding_account.key,
&pda_account.key,
lamports_required,
INITIAL_ACCOUNT_LEN.try_into().unwrap(),
&_program_id,
);
invoke_signed(
&create_pda_account_ix,
&[
funding_account.clone(),
pda_account.clone(),
system_program.clone(),
],
&[signers_seeds],
)?;
let mut pda_account_state = WhiteListData::try_from_slice(&pda_account.data.borrow())?;
pda_account_state.is_initialized = true;
pda_account_state.white_list = Vec::new();
pda_account_state.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;
Ok(())
}
WhitelistInstruction::AddKey { key } => {
msg!("AddKey");
let mut pda_account_state = WhiteListData::try_from_slice(&pda_account.data.borrow())?;
if !pda_account_state.is_initialized {
return Err(ProgramError::InvalidAccountData);
}
let new_size = pda_account.data.borrow().len() + 32;
let rent = Rent::get()?;
let new_minimum_balance = rent.minimum_balance(new_size);
let lamports_diff = new_minimum_balance.saturating_sub(pda_account.lamports());
invoke(
&system_instruction::transfer(funding_account.key, pda_account.key, lamports_diff),
&[
funding_account.clone(),
pda_account.clone(),
system_program.clone(),
],
)?;
pda_account.realloc(new_size, false)?;
pda_account_state.white_list.push(key);
pda_account_state.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;
Ok(())
}
}
}
// adding a publickey to the account
let new_size = pda_account.data.borrow().len() + 32;
let rent = Rent::get()?;
let new_minimum_balance = rent.minimum_balance(new_size);
let lamports_diff = new_minimum_balance.saturating_sub(pda_account.lamports());
invoke(
&system_instruction::transfer(funding_account.key, pda_account.key, lamports_diff),
&[
funding_account.clone(),
pda_account.clone(),
system_program.clone(),
],
)?;
pda_account.realloc(new_size, false)?;
Bagaimana cara untuk membuat program Cross Invocation
Program cross invocaction hanya dengan memanggil instruksi program lainnya di dalam program kita. Salah satu contoh yang baik untuk menghasilkan adalah dengan fungsi swap
di Uniswap. UniswapV2Router
contract, memanggil logic yang diperlukan untuk swap, dana memanggil fungsi ERC20
contract transfer untuk swap dari seseorang ke lainnya. Hal yang sama, kieta dapat memanggil sebuah instruksi program untuk dapat menghasilkan banyak tujuan.
Mari kita lihat contoh instruksi pertama kita SPL Token Program's transfer
. Akun-akun yang kita perlukan untuk menjadikan sebuah transfer adalah
- The Source Token Account (Akun yang memegang tokens kita)
- The Destination Token Account (Akun yang ingin kita transferkan token kita)
- The Source Token Account's Holder (alamat wallet kita yang akan kita gunakan untuk sign-in)
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
};
use spl_token::instruction::transfer;
entrypoint!(process_instruction);
// Accounts required
/// 1. [writable] Source Token Account
/// 2. [writable] Destination Token Account
/// 3. [signer] Source Token Account holder's PubKey
/// 4. [] Token Program
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
// Accounts required for token transfer
// 1. Token account we hold
let source_token_account = next_account_info(accounts_iter)?;
// 2. Token account to send to
let destination_token_account = next_account_info(accounts_iter)?;
// 3. Our wallet address
let source_token_account_holder = next_account_info(accounts_iter)?;
// 4. Token Program
let token_program = next_account_info(accounts_iter)?;
// Parsing the token transfer amount from instruction data
// a. Getting the 0th to 8th index of the u8 byte array
// b. Converting the obtained non zero u8 to a proper u8 (as little endian integers)
// c. Converting the little endian integers to a u64 number
let token_transfer_amount = instruction_data
.get(..8)
.and_then(|slice| slice.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(ProgramError::InvalidAccountData)?;
msg!(
"Transferring {} tokens from {} to {}",
token_transfer_amount,
source_token_account.key.to_string(),
destination_token_account.key.to_string()
);
// Creating a new TransactionInstruction
/*
Internal representation of the instruction's return value (Result<Instruction, ProgramError>)
Ok(Instruction {
program_id: *token_program_id, // PASSED FROM USER
accounts,
data,
})
*/
let transfer_tokens_instruction = transfer(
&token_program.key,
&source_token_account.key,
&destination_token_account.key,
&source_token_account_holder.key,
&[&source_token_account_holder.key],
token_transfer_amount,
)?;
let required_accounts_for_transfer = [
source_token_account.clone(),
destination_token_account.clone(),
source_token_account_holder.clone(),
];
// Passing the TransactionInstruction to send
invoke(
&transfer_tokens_instruction,
&required_accounts_for_transfer,
)?;
msg!("Transfer successful");
Ok(())
}
let token_transfer_amount = instruction_data
.get(..8)
.and_then(|slice| slice.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(ProgramError::InvalidAccountData)?;
let transfer_tokens_instruction = transfer(
&token_program.key,
&source_token_account.key,
&destination_token_account.key,
&source_token_account_holder.key,
&[&source_token_account_holder.key],
token_transfer_amount,
)?;
let required_accounts_for_transfer = [
source_token_account.clone(),
destination_token_account.clone(),
source_token_account_holder.clone(),
];
invoke(
&transfer_tokens_instruction,
&required_accounts_for_transfer,
)?;
Korespondensi instruksi client adalah sebagai berikut. Untuk mengetahui mint dan instruksi pembuatan token, tolong lihat ke full code sekitar.
import {
clusterApiUrl,
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
import {
AccountLayout,
MintLayout,
Token,
TOKEN_PROGRAM_ID,
u64,
} from "@solana/spl-token";
import * as BN from "bn.js";
// Users
const PAYER_KEYPAIR = Keypair.generate();
const RECEIVER_PUBKEY = Keypair.generate().publicKey;
// Mint and token accounts
const TOKEN_MINT_ACCOUNT = Keypair.generate();
const SOURCE_TOKEN_ACCOUNT = Keypair.generate();
const DESTINATION_TOKEN_ACCOUNT = Keypair.generate();
// Numbers
const DEFAULT_DECIMALS_COUNT = 9;
const TOKEN_TRANSFER_AMOUNT = 50 * 10 ** DEFAULT_DECIMALS_COUNT;
const TOKEN_TRANSFER_AMOUNT_BUFFER = Buffer.from(
Uint8Array.of(...new BN(TOKEN_TRANSFER_AMOUNT).toArray("le", 8))
);
(async () => {
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const programId = new PublicKey(
"EfYK91eN3AqTwY1C34W6a33qGAtQ8HJYVhNv7cV4uMZj"
);
const mintDataSpace = MintLayout.span;
const mintRentRequired = await connection.getMinimumBalanceForRentExemption(
mintDataSpace
);
const tokenDataSpace = AccountLayout.span;
const tokenRentRequired = await connection.getMinimumBalanceForRentExemption(
tokenDataSpace
);
// Airdropping some SOL
await connection.confirmTransaction(
await connection.requestAirdrop(PAYER_KEYPAIR.publicKey, LAMPORTS_PER_SOL)
);
// Allocating space and rent for mint account
const createMintAccountIx = SystemProgram.createAccount({
fromPubkey: PAYER_KEYPAIR.publicKey,
lamports: mintRentRequired,
newAccountPubkey: TOKEN_MINT_ACCOUNT.publicKey,
programId: TOKEN_PROGRAM_ID,
space: mintDataSpace,
});
// Initializing mint with decimals and authority
const initializeMintIx = Token.createInitMintInstruction(
TOKEN_PROGRAM_ID,
TOKEN_MINT_ACCOUNT.publicKey,
DEFAULT_DECIMALS_COUNT,
PAYER_KEYPAIR.publicKey, // mintAuthority
PAYER_KEYPAIR.publicKey // freezeAuthority
);
// Allocating space and rent for source token account
const createSourceTokenAccountIx = SystemProgram.createAccount({
fromPubkey: PAYER_KEYPAIR.publicKey,
newAccountPubkey: SOURCE_TOKEN_ACCOUNT.publicKey,
lamports: tokenRentRequired,
programId: TOKEN_PROGRAM_ID,
space: tokenDataSpace,
});
// Initializing token account with mint and owner
const initializeSourceTokenAccountIx = Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
TOKEN_MINT_ACCOUNT.publicKey,
SOURCE_TOKEN_ACCOUNT.publicKey,
PAYER_KEYPAIR.publicKey
);
// Minting tokens to the source token account for transferring later to destination account
const mintTokensIx = Token.createMintToInstruction(
TOKEN_PROGRAM_ID,
TOKEN_MINT_ACCOUNT.publicKey,
SOURCE_TOKEN_ACCOUNT.publicKey,
PAYER_KEYPAIR.publicKey,
[PAYER_KEYPAIR],
TOKEN_TRANSFER_AMOUNT
);
// Allocating space and rent for destination token account
const createDestinationTokenAccountIx = SystemProgram.createAccount({
fromPubkey: PAYER_KEYPAIR.publicKey,
newAccountPubkey: DESTINATION_TOKEN_ACCOUNT.publicKey,
lamports: tokenRentRequired,
programId: TOKEN_PROGRAM_ID,
space: tokenDataSpace,
});
// Initializing token account with mint and owner
const initializeDestinationTokenAccountIx =
Token.createInitAccountInstruction(
TOKEN_PROGRAM_ID,
TOKEN_MINT_ACCOUNT.publicKey,
DESTINATION_TOKEN_ACCOUNT.publicKey,
RECEIVER_PUBKEY
);
// Our program's CPI instruction (transfer)
const transferTokensIx = new TransactionInstruction({
programId: programId,
data: TOKEN_TRANSFER_AMOUNT_BUFFER,
keys: [
{
isSigner: false,
isWritable: true,
pubkey: SOURCE_TOKEN_ACCOUNT.publicKey,
},
{
isSigner: false,
isWritable: true,
pubkey: DESTINATION_TOKEN_ACCOUNT.publicKey,
},
{
isSigner: true,
isWritable: true,
pubkey: PAYER_KEYPAIR.publicKey,
},
{
isSigner: false,
isWritable: false,
pubkey: TOKEN_PROGRAM_ID,
},
],
});
const transaction = new Transaction();
// Adding up all the above instructions
transaction.add(
createMintAccountIx,
initializeMintIx,
createSourceTokenAccountIx,
initializeSourceTokenAccountIx,
mintTokensIx,
createDestinationTokenAccountIx,
initializeDestinationTokenAccountIx,
transferTokensIx
);
const txHash = await connection.sendTransaction(transaction, [
PAYER_KEYPAIR,
TOKEN_MINT_ACCOUNT,
SOURCE_TOKEN_ACCOUNT,
DESTINATION_TOKEN_ACCOUNT,
]);
console.log(`Token transfer CPI success: ${txHash}`);
})();
(async () => {
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const programId = new PublicKey(
"EfYK91eN3AqTwY1C34W6a33qGAtQ8HJYVhNv7cV4uMZj"
);
const transferTokensIx = new TransactionInstruction({
programId: programId,
data: TOKEN_TRANSFER_AMOUNT_BUFFER,
keys: [
{
isSigner: false,
isWritable: true,
pubkey: SOURCE_TOKEN_ACCOUNT.publicKey,
},
{
isSigner: false,
isWritable: true,
pubkey: DESTINATION_TOKEN_ACCOUNT.publicKey,
},
{
isSigner: true,
isWritable: true,
pubkey: PAYER_KEYPAIR.publicKey,
},
{
isSigner: false,
isWritable: false,
pubkey: TOKEN_PROGRAM_ID,
},
],
});
const transaction = new Transaction();
transaction.add(transferTokensIx);
const txHash = await connection.sendTransaction(transaction, [
PAYER_KEYPAIR,
TOKEN_MINT_ACCOUNT,
SOURCE_TOKEN_ACCOUNT,
DESTINATION_TOKEN_ACCOUNT,
]);
console.log(`Token transfer CPI success: ${txHash}`);
})();
Mari sekarang lihat contoh lainnya yang adalah instruksi System Program's create_account
. Ada beberapa perbedaan sedikit antara instruksi yang disebutkan diatas dan ini. Di atas, kita tidak pernah memerlukan token_program
untuk dipass sebagai sebuah akun di dalam fungsi invoke
. Namun, ada beberapa pengecualian dimana anda perlu untuk pass instruksi untuk meng-invoke program_id
. Dalam kasus ini, itu adalah System Program's
program_id. ("11111111111111111111111111111111"). Jadi akun-akun yang diperlukan sekarang akan menjadi
- Akun pembayar akun yang akan mendanai rent
- Akun yang akan dibuat
- Akun System Program
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
system_instruction::create_account,
sysvar::Sysvar,
};
entrypoint!(process_instruction);
// Accounts required
/// 1. [signer, writable] Payer Account
/// 2. [signer, writable] General State Account
/// 3. [] System Program
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
// Accounts required for token transfer
// 1. Payer account for the state account creation
let payer_account = next_account_info(accounts_iter)?;
// 2. Token account we hold
let general_state_account = next_account_info(accounts_iter)?;
// 3. System Program
let system_program = next_account_info(accounts_iter)?;
msg!(
"Creating account for {}",
general_state_account.key.to_string()
);
// Parsing the token transfer amount from instruction data
// a. Getting the 0th to 8th index of the u8 byte array
// b. Converting the obtained non zero u8 to a proper u8 (as little endian integers)
// c. Converting the little endian integers to a u64 number
let account_span = instruction_data
.get(..8)
.and_then(|slice| slice.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(ProgramError::InvalidAccountData)?;
let lamports_required = (Rent::get()?).minimum_balance(account_span as usize);
// Creating a new TransactionInstruction
/*
Internal representation of the instruction's return value (Instruction)
Instruction::new_with_bincode(
system_program::id(), // NOT PASSED FROM USER
&SystemInstruction::CreateAccount {
lamports,
space,
owner: *owner,
},
account_metas,
)
*/
let create_account_instruction = create_account(
&payer_account.key,
&general_state_account.key,
lamports_required,
account_span,
program_id,
);
let required_accounts_for_create = [
payer_account.clone(),
general_state_account.clone(),
system_program.clone(),
];
// Passing the TransactionInstruction to send (with the issused program_id)
invoke(&create_account_instruction, &required_accounts_for_create)?;
msg!("Transfer successful");
Ok(())
}
let account_span = instruction_data
.get(..8)
.and_then(|slice| slice.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(ProgramError::InvalidAccountData)?;
let lamports_required = (Rent::get()?).minimum_balance(account_span as usize);
let create_account_instruction = create_account(
&payer_account.key,
&general_state_account.key,
lamports_required,
account_span,
program_id,
);
let required_accounts_for_create = [
payer_account.clone(),
general_state_account.clone(),
system_program.clone(),
];
invoke(&create_account_instruction, &required_accounts_for_create)?;
Code di client side akan terlihat seperti ini
import { clusterApiUrl, Connection, Keypair } from "@solana/web3.js";
import { LAMPORTS_PER_SOL, PublicKey, SystemProgram } from "@solana/web3.js";
import { Transaction, TransactionInstruction } from "@solana/web3.js";
import * as BN from "bn.js";
// Users
const PAYER_KEYPAIR = Keypair.generate();
const GENERAL_STATE_KEYPAIR = Keypair.generate();
const ACCOUNT_SPACE_BUFFER = Buffer.from(
Uint8Array.of(...new BN(100).toArray("le", 8))
);
(async () => {
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const programId = new PublicKey(
"DkuQ5wsndkzXfgqDB6Lgf4sDjBi4gkLSak1dM5Mn2RuQ"
);
// Airdropping some SOL
await connection.confirmTransaction(
await connection.requestAirdrop(PAYER_KEYPAIR.publicKey, LAMPORTS_PER_SOL)
);
// Our program's CPI instruction (create_account)
const createAccountIx = new TransactionInstruction({
programId: programId,
data: ACCOUNT_SPACE_BUFFER,
keys: [
{
isSigner: true,
isWritable: true,
pubkey: PAYER_KEYPAIR.publicKey,
},
{
isSigner: true,
isWritable: true,
pubkey: GENERAL_STATE_KEYPAIR.publicKey,
},
{
isSigner: false,
isWritable: false,
pubkey: SystemProgram.programId,
},
],
});
const transaction = new Transaction();
// Adding up all the above instructions
transaction.add(createAccountIx);
const txHash = await connection.sendTransaction(transaction, [
PAYER_KEYPAIR,
GENERAL_STATE_KEYPAIR,
]);
console.log(`Create Account CPI Success: ${txHash}`);
})();
(async () => {
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const programId = new PublicKey(
"DkuQ5wsndkzXfgqDB6Lgf4sDjBi4gkLSak1dM5Mn2RuQ"
);
// Airdropping some SOL
await connection.confirmTransaction(
await connection.requestAirdrop(PAYER_KEYPAIR.publicKey, LAMPORTS_PER_SOL)
);
// Our program's CPI instruction (create_account)
const creataAccountIx = new TransactionInstruction({
programId: programId,
data: ACCOUNT_SPACE_BUFFER,
keys: [
{
isSigner: true,
isWritable: true,
pubkey: PAYER_KEYPAIR.publicKey,
},
{
isSigner: true,
isWritable: true,
pubkey: GENERAL_STATE_KEYPAIR.publicKey,
},
{
isSigner: false,
isWritable: false,
pubkey: SystemProgram.programId,
},
],
});
const transaction = new Transaction();
// Adding up all the above instructions
transaction.add(creataAccountIx);
const txHash = await connection.sendTransaction(transaction, [
PAYER_KEYPAIR,
GENERAL_STATE_KEYPAIR,
]);
console.log(`Create Account CPI Success: ${txHash}`);
})();
Bagaimana cara membuat sebuah PDA
Program Derived Address hanyalah sebuah program yang dimiliki oleh program, tetapi tidak memiliki private key. Sebaliknya signature-nya didapat dari set of seeds dan sebuah bump (sebuah nonce yang memastikannya untuk keluar kurva). "Men-generate" sebuah Program Address berbeda dari "membuat"nya. Seseorang dapat mengenerate sebuah PDA dengan mengunakan Pubkey::find_program_address
. Membuat sebuah PDA pada essensinya berarti mengininisasi alamat dengan kapasitas dan men-set state ke dalamnya. Sebuah akun Keypair normal dapat dibuat dari luar program kita dan kemudian diperintahkan untuk menginisasikan statenya. Sayangnya, untuk PDA-PDA harus dibuat dalam chain, oleh sebab naturenya yang tidak dapat disign on atas namanya sendiri.Jadi kita memerlukan invoke_signed
untuk men-pass PDA seeds, bersama dengan signature akun yang mendanai yang menghasilkan pembuatan akun PDA.
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
program::invoke_signed,
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
system_instruction,
sysvar::Sysvar,
};
entrypoint!(process_instruction);
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
is_initialized: bool,
}
// Accounts required
/// 1. [signer, writable] Funding account
/// 2. [writable] PDA account
/// 3. [] System Program
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
const ACCOUNT_DATA_LEN: usize = 1;
let accounts_iter = &mut accounts.iter();
// Getting required accounts
let funding_account = next_account_info(accounts_iter)?;
let pda_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
// Getting PDA Bump from instruction data
let (pda_bump, _) = instruction_data
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Checking if passed PDA and expected PDA are equal
let signers_seeds: &[&[u8]; 3] = &[
b"customaddress",
&funding_account.key.to_bytes(),
&[*pda_bump],
];
let pda = Pubkey::create_program_address(signers_seeds, program_id)?;
if pda.ne(&pda_account.key) {
return Err(ProgramError::InvalidAccountData);
}
// Assessing required lamports and creating transaction instruction
let lamports_required = Rent::get()?.minimum_balance(ACCOUNT_DATA_LEN);
let create_pda_account_ix = system_instruction::create_account(
&funding_account.key,
&pda_account.key,
lamports_required,
ACCOUNT_DATA_LEN.try_into().unwrap(),
&program_id,
);
// Invoking the instruction but with PDAs as additional signer
invoke_signed(
&create_pda_account_ix,
&[
funding_account.clone(),
pda_account.clone(),
system_program.clone(),
],
&[signers_seeds],
)?;
// Setting state for PDA
let mut pda_account_state = HelloState::try_from_slice(&pda_account.data.borrow())?;
pda_account_state.is_initialized = true;
pda_account_state.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;
Ok(())
}
let create_pda_account_ix = system_instruction::create_account(
&funding_account.key,
&pda_account.key,
lamports_required,
ACCOUNT_DATA_LEN.try_into().unwrap(),
&program_id,
);
invoke_signed(
&create_pda_account_ix,
&[funding_account.clone(), pda_account.clone()],
&[signers_seeds],
)?;
Seseorang dapat mengirim akun yang diperlukan melalui client dalam contoh berikut
import {
clusterApiUrl,
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction,
} from "@solana/web3.js";
const PAYER_KEYPAIR = Keypair.generate();
(async () => {
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const programId = new PublicKey(
"6eW5nnSosr2LpkUGCdznsjRGDhVb26tLmiM1P8RV1QQp"
);
// Airdop to Payer
await connection.confirmTransaction(
await connection.requestAirdrop(PAYER_KEYPAIR.publicKey, LAMPORTS_PER_SOL)
);
const [pda, bump] = await PublicKey.findProgramAddress(
[Buffer.from("customaddress"), PAYER_KEYPAIR.publicKey.toBuffer()],
programId
);
console.log(`PDA Pubkey: ${pda.toString()}`);
const createPDAIx = new TransactionInstruction({
programId: programId,
data: Buffer.from(Uint8Array.of(bump)),
keys: [
{
isSigner: true,
isWritable: true,
pubkey: PAYER_KEYPAIR.publicKey,
},
{
isSigner: false,
isWritable: true,
pubkey: pda,
},
{
isSigner: false,
isWritable: false,
pubkey: SystemProgram.programId,
},
],
});
const transaction = new Transaction();
transaction.add(createPDAIx);
const txHash = await connection.sendTransaction(transaction, [PAYER_KEYPAIR]);
console.log(`Created PDA successfully. Tx Hash: ${txHash}`);
})();
const PAYER_KEYPAIR = Keypair.generate();
(async () => {
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const programId = new PublicKey(
"6eW5nnSosr2LpkUGCdznsjRGDhVb26tLmiM1P8RV1QQp"
);
const [pda, bump] = await PublicKey.findProgramAddress(
[Buffer.from("customaddress"), PAYER_KEYPAIR.publicKey.toBuffer()],
programId
);
const createPDAIx = new TransactionInstruction({
programId: programId,
data: Buffer.from(Uint8Array.of(bump)),
keys: [
{
isSigner: true,
isWritable: true,
pubkey: PAYER_KEYPAIR.publicKey,
},
{
isSigner: false,
isWritable: true,
pubkey: pda,
},
{
isSigner: false,
isWritable: false,
pubkey: SystemProgram.programId,
},
],
});
const transaction = new Transaction();
transaction.add(createPDAIx);
const txHash = await connection.sendTransaction(transaction, [PAYER_KEYPAIR]);
})();
Bagaimana cara membaca akun
Hampir semua instruksi di dalam Solana memerlukan sekurang-kurangnya 2 - 3 akun, dan mereka perlu di-mentioned melalui instruction handlers mengenai apa yang diharapkan dari set akun tersebut. Adalah sangat mudah jika kita mengambil keuntungan dari iter()
method dalam Rust, dibandingkan dengan men-index akun-akun secara manual. next_account_info
method pada dasarnya men-slices index pertama yang dapat diiterasi dan mengembalikan kehadiran akun di dalam array akun-akun. Mari kita lihat instruksi sederhana yang mengharapkan beberapa akun dan perlu untuk mengurai masing-masing akun.
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
};
entrypoint!(process_instruction);
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
is_initialized: bool,
}
// Accounts required
/// 1. [signer] Payer
/// 2. [writable] Hello state account
/// 3. [] Rent account
/// 4. [] System Program
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
// Fetching all the accounts as a iterator (facilitating for loops and iterations)
let accounts_iter = &mut accounts.iter();
// Payer account
let payer_account = next_account_info(accounts_iter)?;
// Hello state account
let hello_state_account = next_account_info(accounts_iter)?;
// Rent account
let rent_account = next_account_info(accounts_iter)?;
// System Program
let system_program = next_account_info(accounts_iter)?;
Ok(())
}
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
// Fetching all the accounts as a iterator (facilitating for loops and iterations)
let accounts_iter = &mut accounts.iter();
// Payer account
let payer_account = next_account_info(accounts_iter)?;
// Hello state account
let hello_state_account = next_account_info(accounts_iter)?;
// Rent account
let rent_account = next_account_info(accounts_iter)?;
// System Program
let system_program = next_account_info(accounts_iter)?;
Ok(())
}
Bagaimana cara men-verifikasi akun
Karena program-program di Solana adalah stateless, kita sebagai pembuat program harus memastikan validasi akun-akun yang yang dipass sebanyak mungkin untuk menghindari masuknya akun yang disusupi (malicious). Hal dasar yang dapat kita lakukan adalah:
- Periksa apakah akun signer yang diharapkan bener-benar sudah sign
- Periksa apakah state akun yang diharapkan sudah dicheck sebagai writeable
- Periksa apakah state pemilik akun yang diharapkan adalah id program yang dipanggil
- Jika menginisasi state untuk pertama kali, periksa apakah akun sudah ter-inisiasi atau belum.
- Periksa apakah ada id-id cross program yang dipass (kapanpun diperlukan) sudah seperti yang diharapkan.
Dibawah ini, instruksi dasar yang menginisiasi sebuah hero state acccount, tetapi dengan pemeriksaan yang disebutkan diatas
A basic instruction which initializes a hero state account, but with the above mentioned checks is defined below
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint,
entrypoint::ProgramResult,
msg,
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
system_program::ID as SYSTEM_PROGRAM_ID,
sysvar::Sysvar,
};
entrypoint!(process_instruction);
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct HelloState {
is_initialized: bool,
}
// Accounts required
/// 1. [signer] Payer
/// 2. [writable] Hello state account
/// 3. [] System Program
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
// Payer account
let payer_account = next_account_info(accounts_iter)?;
// Hello state account
let hello_state_account = next_account_info(accounts_iter)?;
// System Program
let system_program = next_account_info(accounts_iter)?;
let rent = Rent::get()?;
// Checking if payer account is the signer
if !payer_account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Checking if hello state account is rent exempt
if !rent.is_exempt(hello_state_account.lamports(), 1) {
return Err(ProgramError::AccountNotRentExempt);
}
// Checking if hello state account is writable
if !hello_state_account.is_writable {
return Err(ProgramError::InvalidAccountData);
}
// Checking if hello state account's owner is the current program
if hello_state_account.owner.ne(&program_id) {
return Err(ProgramError::IllegalOwner);
}
// Checking if the system program is valid
if system_program.key.ne(&SYSTEM_PROGRAM_ID) {
return Err(ProgramError::IncorrectProgramId);
}
let mut hello_state = HelloState::try_from_slice(&hello_state_account.data.borrow())?;
// Checking if the state has already been initialized
if hello_state.is_initialized {
return Err(ProgramError::AccountAlreadyInitialized);
}
hello_state.is_initialized = true;
hello_state.serialize(&mut &mut hello_state_account.data.borrow_mut()[..])?;
msg!("Account initialized :)");
Ok(())
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let payer_account = next_account_info(accounts_iter)?;
let hello_state_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let rent = Rent::get()?;
// Checking if payer account is the signer
if !payer_account.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}
// Checking if hello state account is rent exempt
if !rent.is_exempt(hello_state_account.lamports(), 1) {
return Err(ProgramError::AccountNotRentExempt);
}
// Checking if hello state account is writable
if !hello_state_account.is_writable {
return Err(ProgramError::InvalidAccountData);
}
// Checking if hello state account's owner is the current program
if hello_state_account.owner.ne(&program_id) {
return Err(ProgramError::IllegalOwner);
}
// Checking if the system program is valid
if system_program.key.ne(&SYSTEM_PROGRAM_ID) {
return Err(ProgramError::IncorrectProgramId);
}
let mut hello_state = HelloState::try_from_slice(&hello_state_account.data.borrow())?;
// Checking if the state has already been initialized
if hello_state.is_initialized {
return Err(ProgramError::AccountAlreadyInitialized);
}
hello_state.is_initialized = true;
hello_state.serialize(&mut &mut hello_state_account.data.borrow_mut()[..])?;
msg!("Account initialized :)");
Ok(())
}
Bagaimana cara membaca beberapa instruksi dalam sebuah transaksi
Solana memberikan kita kemampuan untuk mengintip semua instruksi-instruksi dalam transaksi berjalan. Kita dapat menyimpannya di dalam sebuah variable dan mengulanginya lagi. Kita dapat melakukan banyak hal dengan ini, seperti memeriksa transaksi yang mencurigakan.
use anchor_lang::{
prelude::*,
solana_program::{
sysvar,
serialize_utils::{read_pubkey,read_u16}
}
};
declare_id!("8DJXJRV8DBFjJDYyU9cTHBVK1F1CTCi6JUBDVfyBxqsT");
#[program]
pub mod cookbook {
use super::*;
pub fn read_multiple_instruction<'info>(ctx: Context<ReadMultipleInstruction>, creator_bump: u8) -> Result<()> {
let instruction_sysvar_account = &ctx.accounts.instruction_sysvar_account;
let instruction_sysvar_account_info = instruction_sysvar_account.to_account_info();
let id = "8DJXJRV8DBFjJDYyU9cTHBVK1F1CTCi6JUBDVfyBxqsT";
let instruction_sysvar = instruction_sysvar_account_info.data.borrow();
let mut idx = 0;
let num_instructions = read_u16(&mut idx, &instruction_sysvar)
.map_err(|_| MyError::NoInstructionFound)?;
for index in 0..num_instructions {
let mut current = 2 + (index * 2) as usize;
let start = read_u16(&mut current, &instruction_sysvar).unwrap();
current = start as usize;
let num_accounts = read_u16(&mut current, &instruction_sysvar).unwrap();
current += (num_accounts as usize) * (1 + 32);
let program_id = read_pubkey(&mut current, &instruction_sysvar).unwrap();
if program_id != id
{
msg!("Transaction had ix with program id {}", program_id);
return Err(MyError::SuspiciousTransaction.into());
}
}
Ok(())
}
}
#[derive(Accounts)]
#[instruction(creator_bump:u8)]
pub struct ReadMultipleInstruction<'info> {
#[account(address = sysvar::instructions::id())]
instruction_sysvar_account: UncheckedAccount<'info>
}
#[error_code]
pub enum MyError {
#[msg("No instructions found")]
NoInstructionFound,
#[msg("Suspicious transaction detected")]
SuspiciousTransaction
}
let mut idx = 0;
let num_instructions = read_u16(&mut idx, &instruction_sysvar)
.map_err(|_| MyError::NoInstructionFound)?;
for index in 0..num_instructions {
let mut current = 2 + (index * 2) as usize;
let start = read_u16(&mut current, &instruction_sysvar).unwrap();
current = start as usize;
let num_accounts = read_u16(&mut current, &instruction_sysvar).unwrap();
current += (num_accounts as usize) * (1 + 32);
}