编写程序
如何在程序中转移 SOL
你的Solana程序可以在不"调用"系统程序的情况下将lamports从一个账户转移给另一个账户。基本规则是,你的程序可以将lamports从你的程序所拥有的任何账户转移到任何账户。
接收方账户不一定要是你的程序所拥有的账户。
/// 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(())
}
如何在程序中获取时钟
获取时钟的方法有两种:
- 将
SYSVAR_CLOCK_PUBKEY
作为指令的参数传入。 - 在指令内部直接访问时钟。
了解这两种方法会对你有好处,因为一些传统的程序仍然将SYSVAR_CLOCK_PUBKEY作为一个账户来使用。
在指令中将时钟作为一个账户传递
让我们创建一个指令,该指令接收一个账户用于初始化,并接收 SYSVAR 的公钥。
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;
现在,我们通过客户端传递时钟的 SYSVAR 公共地址:
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}`);
})();
在指令内部直接访问时钟
让我们创建同样的指令,但这次我们不需要从客户端传递SYSVAR_CLOCK_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
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;
现在,客户端只需要传递状态和支付账户的指令:
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}`);
})();
如何更改账户大小
你可以使用realloc
函数来更改程序拥有的账户的大小。realloc
函数可以将账户的大小调整到最大10KB。当你使用realloc
增加账户的大小时,你需要转移lamports以保持该账户的租金免除状态。
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)?;
跨程序调用的方法
跨程序调用,简单来说,就是在我们的程序中调用另一个程序的指令。一个很好的例子是Uniswap
的swap
功能。UniswapV2Router
合约调用必要的逻辑进行交换,并调用ERC20
合约的transfer函数将代币从一个人转移到另一个人。同样的方式,我们可以调用程序的指令来实现多种目的。
让我们来看看我们的第一个例子,即SPL Token Program
的transfer
指令。进行转账所需的账户包括:
- 源代币账户(我们持有代币的账户)
- 目标代币账户(我们要将代币转移至的账户)
- 源代币账户的持有者(我们将为其签名的钱包地址)
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,
)?;
相应的客户端指令如下所示。有关了解铸币和代币创建指令,请参考附近的完整代码。
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_KEYPAIR = 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_KEYPAIR
);
// 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}`);
})();
现在让我们来看另一个例子,即System Program
的create_account
指令。这里与上面提到的指令有一点不同。在上述例子中,我们不需要在invoke
函数中将token_program
作为账户之一传递。然而,在某些情况下,您需要传递调用指令的program_id
。在我们的例子中,它将是System Program
的program_id
("11111111111111111111111111111111")。所以现在所需的账户包括:
- 负责支付租金的支付账户
- 将要创建的账户
- 系统程序(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)?;
对应的客户端代码如下所示:
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}`);
})();
如何创建PDA
程序派生地址(Program Derived Address,PDA)是程序拥有的账户,但没有私钥。相反,它的签名是通过一组种子和一个阻碍值(一个确保其不在曲线上的随机数)获取的。"生成"程序地址与"创建"它是不同的。可以使用Pubkey::find_program_address
来生成PDA。创建PDA实质上意味着初始化该地址的空间并将其状态设置为初始状态。普通的密钥对账户可以在我们的程序之外创建,然后将其用于初始化PDA的状态。不幸的是,对于PDA来说,它必须在链上创建,因为它本身无法代表自己进行签名。因此,我们使用invoke_signed
来传递PDA的种子,以及资金账户的签名,从而实现了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],
)?;
可以通过客户端按如下方式发送所需的账户:
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]);
})();
如何读取账户
在Solana中,几乎所有的指令都至少需要2-3个账户,并且在指令处理程序中会说明它期望的账户顺序。如果我们利用Rust中的iter()
方法,而不是手动索引账户,那么这将非常简单。next_account_info
方法基本上是对可迭代对象的第一个索引进行切片,并返回账户数组中存在的账户。让我们看一个简单的指令,它期望一堆账户并需要解析每个账户。
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(())
}
如何验证账户
由于Solana中的程序是无状态的,作为程序创建者,我们必须尽可能验证传递的账户,以避免任何恶意账户的进入。可以进行的基本检查包括:
- 检查预期的签名账户是否已签名。
- 检查预期的状态账户是否已标记为可写。
- 检查预期的状态账户的所有者是否为调用程序的程序ID。
- 如果首次初始化状态,请检查账户是否已经初始化。
- 检查是否按预期传递了任何跨程序的ID(在需要时)。
下面是一个基本的指令,它使用上述检查初始化英雄状态账户的示例:
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(())
}
如何从一个交易中读取多个指令
Solana允许我们查看当前交易中的所有指令。我们可以将它们存储在一个变量中,并对其进行迭代。我们可以利用这一点做许多事情,比如检查可疑的交易。
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);
}