Programme schreiben
So übertragen Sie SOL in einem Programm
Ihr Solana-Programm kann Lamports von einem Konto auf ein anderes übertragen ohne das Systemprogramm "aufzurufen". Die Grundregel ist die Ihr Programm kann Lamports von jedem Konto übertragen, das Ihrem Programm überhaupt auf irgendein Konto gehört .
Das Empfängerkonto muss nicht ein Konto Ihres Programms sein.
/// 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(())
}
Wie bekomme ich die Uhr in ein Programm?
Das Erhalten einer Uhr kann auf zwei Arten erfolgen
- Übergabe von
SYSVAR_CLOCK_PUBKEY
an eine Anweisung - Direkter Zugriff auf die Uhr innerhalb einer Anweisung.
Es ist schön, beide Methoden zu kennen, da einige ältere Programme immer noch den SYSVAR_CLOCK_PUBKEY
als Konto erwarten.
Passing Clock als Konto innerhalb einer Anweisung
Lassen Sie uns eine Anweisung erstellen, die ein Konto zum Initialisieren und den Sysvar-Pubkey erhält
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;
Nun übergeben wir die öffentliche Sysvar-Adresse der Uhr über den 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}`);
})();
Direkter Zugriff auf die Uhr innerhalb einer Anweisung
Lassen Sie uns dieselbe Anweisung erstellen, aber ohne den SYSVAR_CLOCK_PUBKEY
von der Client-Seite zu erwarten.
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;
Die clientseitige Anweisung muss jetzt nur noch die Staats- und Zahlerkonten übergeben.
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}`);
})();
So ändern Sie die Kontogröße
Sie können die Größe eines programmeigenen Kontos mit der Verwendung ändern von "realloc". realloc
kann die Größe eines Kontos auf bis zu 10 KB ändern. Wenn Sie "realloc" verwenden, um die Größe eines Kontos zu erhöhen, müssen Sie Lamports übertragen, um dieses Konto mietfrei zu behalten.
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)?;
Wie man einen programmübergreifenden Aufruf durchführt
Ein programmübergreifender Aufruf wird einfach als Aufruf von Programmanweisungen innerhalb unseres Programms bezeichnet
. Ein bestes Beispiel hervorzuheben ist die "swap"-Funktionalität von Uniswap. Der UniswapV2Router
Vertrag, ruft die notwendige Logik auf, und ruft die Übertragungsfunktion des "ERC20"-Vertrags auf, die von einer Person zur anderen wechseln. Genauso können wir die Anweisung eines Programms aufrufen, um eine Vielzahl von Zwecken zu erfüllen.
Werfen wir einen Blick auf unser erstes Beispiel, nämlich die Anweisung zur Übertragung des SPL-Token-Programms. Die erforderlichen Konten, die wir für eine Überweisung benötigen, sind
- Das Quell-Token-Konto (Das Konto, auf dem wir unsere Token halten)
- Das Ziel-Token-Konto (das Konto, auf das wir unsere Token übertragen würden)
- Inhaber des Quell-Token-Kontos (Unsere Wallet-Adresse, für die wir unterschreiben würden)
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,
)?;
Die entsprechende Client-Anweisung wäre wie folgt. Um die Mint- und Token-Erstellungsanweisungen zu kennen, lesen Sie bitte den vollständigen Code in der Nähe.
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}`);
})();
Schauen wir uns nun ein weiteres Beispiel an, nämlich die Anweisung create_account des Systemprogramms. Es gibt einen kleinen Unterschied zwischen der oben erwähnten Anweisung und dieser. Dort mussten wir das token_program
nie als eines der Konten innerhalb der invoke
-Funktion übergeben. Es gibt jedoch Ausnahmen, bei denen Sie die program_id
der aufrufenden Anweisung übergeben müssen. In unserem Fall wäre es die program_id des Systemprogramms
. ("11111111111111111111111111111111"). So jetzt wären die benötigten Accounts
- Das Zahlerkonto, das die Miete finanziert
- Das Konto, das erstellt werden soll
- Systemprogrammkonto
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)?;
Der entsprechende clientseitige Code sieht wie folgt aus
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}`);
})();
So erstellen Sie einen PDA
Eine vom Programm abgeleitete Adresse ist einfach ein Konto, das dem Programm gehört, aber keinen privaten Schlüssel hat. Stattdessen wird seine Signatur durch eine Reihe von Samen und eine Beule (eine Nonce, die sicherstellt, dass es außerhalb der Kurve liegt) erhalten. Das „Generieren“ einer Programmadresse unterscheidet sich von dem „Erstellen“. Man kann einen PDA mit Pubkey::find_program_address
generieren. Das Erstellen eines PDA bedeutet im Wesentlichen, die Adresse mit Leerzeichen zu initialisieren und den Status darauf zu setzen. Ein normales Keypair-Konto kann außerhalb unseres Programms erstellt und dann zur Initialisierung seines Status gefüttert werden. Leider wurde es für PDAs in einer Kette erstellt, da es nicht möglich ist, im eigenen Namen zu unterzeichnen. Daher verwenden wir "invoke_signed", um die Samen des PDA zusammen mit der Signatur des Finanzierungskontos zu übergeben, was zur Kontoerstellung eines PDA führt.
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],
)?;
Man kann die erforderlichen Konten wie folgt per Client senden
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]);
})();
Wie man Konten liest
Fast alle Anweisungen in Solana würden mindestens 2 - 3 Konten erfordern, und sie würden über den Anweisungshandlern erwähnt, in welcher Reihenfolge diese Konten erwartet werden. Es ist ziemlich einfach, wenn wir die Methode iter()
in Rust nutzen, anstatt die Konten manuell zu indizieren. Die Methode "next_account_info" schneidet im Grunde den ersten Index des Iterables und gibt das Konto zurück, das im Konten-Array vorhanden ist. Sehen wir uns eine einfache Anweisung an, die eine Reihe von Konten erwartet und jedes von ihnen analysieren muss.
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(())
}
So verifizieren Sie Konten
Da Programme in Solana zustandslos sind, müssen wir als Programmersteller sicherstellen, dass die übergebenen Konten so weit wie möglich validiert werden, um einen böswilligen Kontoeintrag zu vermeiden. Die grundlegenden Überprüfungen, die man durchführen kann, sind
- Überprüfen Sie, ob das erwartete Unterzeichnerkonto tatsächlich unterschrieben hat
- Überprüfen Sie, ob die erwarteten Statuskonten als beschreibbar markiert wurden
- Überprüfen Sie, ob der Besitzer des erwarteten Statuskontos die aufgerufene Programm-ID ist
- Wenn Sie den Status zum ersten Mal initialisieren, überprüfen Sie, ob das Konto bereits initialisiert wurde oder nicht.
- Überprüfen Sie, ob alle übergebenen programmübergreifenden IDs (falls erforderlich) wie erwartet sind.
Eine grundlegende Anweisung, die ein Heldenstatuskonto initialisiert, jedoch mit den oben erwähnten Überprüfungen, wird unten definiert.
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(())
}
So lesen Sie mehrere Anweisungen aus einer Transaktion
Solana ermöglicht es uns, einen Blick auf alle Anweisungen in der aktuellen Transaktion zu werfen. Wir können sie in einer Variablen und speichern über sie iterieren. Wir können damit viele Dinge tun, z. B. nach verdächtigen Transaktionen suchen.
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);
}