Écrire des Programmes
Comment transférer SOL dans un programme
Votre Programme Solana peut transférer des lamports d'un compte à un autre sans "invoquer" le programme du Système (System program). La règle fondamentale est que votre programme peut transférer des lamports de n'importe quel compte appartenant à votre programme vers n'importe quel compte.
Le compte destinataire ne doit pas nécessairement être un compte appartenant à votre programme.
/// 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(())
}
Comment obtenir une référence à l'horloge dans un programme
L'obtention d'une horloge peut se faire de deux manières
- Passer
SYSVAR_CLOCK_PUBKEY
dans une instruction - Accéder à l'Horloge directement à l'intérieur d'une instruction.
Il est bon de connaître les deux méthodes, car certains programmes hérités attendent toujours la SYSVAR_CLOCK_PUBKEY
comme compte.
Passer l'Horloge comme un compte dans une instruction
Créons une instruction qui reçoit un compte pour l'initialisation et la clé publique de 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;
Maintenant nous passons l'adresse publique sysvar de l'horloge via le 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}`);
})();
Accéder à l'horloge directement dans une instruction
Créons la même instruction, mais sans exiger la SYSVAR_CLOCK_PUBKEY
du côté client.
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;
L'instruction côté client ne doit plus transmettre que les comptes de l'état et du payeur.
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}`);
})();
Comment modifier la taille d'un compte
Vous pouvez modifier la taille d'un compte propriétaire d'un programme en utilisant realloc
. realloc
peut redimensionner un compte jusqu'à 10KB. Lorsque vous utilisez realloc
pour augmenter la taille d'un compte, vous devez transférer des lamports afin de garder ce compte exempt de rente.
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)?;
Comment faire l'Invocation de Programme Croisé
Une invocation de programme croisé est tout simplement l'appel d'une instruction d'un autre programme dans notre programme. Le meilleur exemple à mettre en avant est la fonctionnalité swap
d'Uniswap. Le contrat UniswapV2Router
appelle la logique nécessaire pour faire le swap et appelle la fonction de transfert du contrat ERC20
pour effectuer l'échange d'une personne à une autre. De la même manière, on peut appeler l'instruction d'un programme pour avoir une multitude de buts.
Examinons notre premier exemple qui est l'instruction de transfert du Programme de Jetons SPL
. Les comptes requis pour qu'un transfert ait lieu sont les suivants
- Le Compte de Jetons Source (Le compte sur lequel nous détenons nos jetons)
- Le Compte de Jetons de Destination (Le compte vers lequel nous souhaitons transférer nos jetons)
- Le Propriétaire du Compte de Jetons Source (L'adresse de notre portefeuille avec lequel nous signerons)
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,
)?;
L'instruction client correspondante serait la suivante. Pour connaître les instructions de création de mint et de jetons, veuillez vous référer au code complet.
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}`);
})();
Maintenant, regardons un autre exemple qui est l'instruction create_account du Programme du Système
. Il y a une légère différence entre l'instruction mentionnée ci-dessus et celle-ci. Ici, nous n'avons pas à passer le token_program
comme l'un des comptes dans la fonction invoke
. Cependant, il y a des exceptions où vous devez transmettre le program_id
de l'instruction invoquante. Dans notre cas, il s'agit du program_id du Programme du Système
. ("11111111111111111111111111111111"). Ainsi, les comptes requis seraient les suivants
- Le compte payeur qui paie la rente
- Le compte qui va être créé
- Le compte du Programme du Système
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)?;
Le code côté client correspondant sera le suivant
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}`);
})();
Comment créer un PDA
Une Adresse Dérivée d'un Programme est simplement un compte appartenant au programme, mais qui n'a pas de clé privée. Au lieu de cela, sa signature est obtenue par un ensemble de seeds et un bump (un nonce qui permet de s'assurer qu'elle est en dehors de la courbe). "Générer" une Adresse de Programme est différent de la "Créer". On peut générer un PDA en utilisant Pubkey::find_program_address
. Créer un PDA signifie essentiellement initialiser l'adresse avec de l'espace et définir l'état. Un compte Keypair normal peut être créé en dehors de notre programme et ensuite alimenté pour initialiser son état. Malheureusement, pour les PDAs, elle doit être créée sur la blockchain, car elle ne peut pas signer en son nom. Nous utilisons donc invoke_signed
pour passer les seeds du PDA, ainsi que la signature du compte de financement, ce qui entraîne la création d'un compte 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],
)?;
On peut envoyer les comptes requis via le client comme suit
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]);
})();
Comment lire des comptes
Presque toutes les instructions dans Solana nécessitent au moins 2 ou 3 comptes, et ils sont mentionnés dans les gestionnaires d'instructions dans quel ordre ils attendent ces comptes. C'est assez simple si on profite de la méthode iter()
de Rust, au lieu d'indiquer manuellement les comptes. La méthode next_account_info
récupère le premier index de l'itérable et retourne le compte présent dans le tableau des comptes. Voyons une instruction simple qui attend un ensemble de comptes et qui demande d'analyser chacun d'entre eux.
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(())
}
Comment vérifier des comptes
Puisque les programmes dans Solana sont sans état, nous, en tant que créateur de programme, devons nous assurer que les comptes passés sont valides autant que possible pour éviter toute entrée de compte malveillant. Ainsi, les contrôles de base que l'on peut effectuer sont
- Vérifier si le compte du signataire attendu a effectivement signé
- Vérifier si les comptes d'état attendus ont été vérifiés comme accessibles en écriture
- Vérifiez si le propriétaire du compte d'état attendu est l'identifiant du programme appelé
- Si vous initialisez l'état pour la première fois, vérifiez si le compte a déjà été initialisé ou non.
- Vérifier si tous les identifiants de programmes croisés passés (si nécessaire) sont conformes aux attentes.
Une instruction de base qui initialise un compte d'état héroïque, mais avec les contrôles mentionnés ci-dessus, est définie ci-dessous
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(())
}
Comment lire plusieurs instructions à partir d'une transaction
Solana nous permet de consulter l'ensemble des instructions de la transaction en cours. Nous pouvons les stocker dans une variable et itérer sur eux. Nous pouvons faire beaucoup de choses avec ça, comme, par exemple, vérifier les transactions suspectes.
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);
}