Transactions et instructions
Sur Solana, les utilisateurs envoient des transactions pour interagir avec le réseau. Les transactions contiennent une ou plusieurs instructions qui spécifient les opérations à traiter. La logique d'exécution des instructions est stockée dans les programmes déployés sur le réseau Solana, où chaque programme définit son propre ensemble d'instructions.
Voici les détails essentiels concernant le traitement des transactions Solana :
- Si une transaction inclut plusieurs instructions, celles-ci s'exécutent dans l'ordre dans lequel elles ont été ajoutées à la transaction.
- Les transactions sont "atomiques" - toutes les instructions doivent être traitées avec succès, sinon la transaction entière échoue et aucun changement n'est effectué.
Une transaction est essentiellement une demande de traitement d'une ou plusieurs instructions.
Transaction simplifiée
Une transaction est comme une enveloppe contenant des formulaires. Chaque formulaire est une instruction qui indique au réseau quoi faire. Envoyer la transaction revient à poster l'enveloppe pour faire traiter les formulaires.
Points clés
- Les transactions Solana incluent des instructions qui invoquent des programmes sur le réseau.
- Les transactions sont atomiques - si une instruction échoue, la transaction entière échoue et aucun changement n'est effectué.
- Les instructions d'une transaction s'exécutent dans un ordre séquentiel.
- La limite de taille d'une transaction est de 1232 octets.
- Chaque instruction nécessite trois éléments d'information :
- L'adresse du programme à invoquer
- Les comptes que l'instruction lit ou modifie
- Toute donnée supplémentaire requise par l'instruction (par ex., arguments de fonction)
Exemple de transfert de SOL
Le diagramme ci-dessous représente une transaction avec une seule instruction pour transférer du SOL d'un expéditeur à un destinataire.
Sur Solana, les "portefeuilles" sont des comptes détenus par le Programme Système. Seul le propriétaire du programme peut modifier les données d'un compte, donc transférer du SOL nécessite d'envoyer une transaction pour invoquer le Programme Système.
Transfert SOL
Le compte expéditeur doit signer (is_signer
) la transaction pour permettre au
Programme Système de déduire son solde en lamports. Les comptes expéditeur et
destinataire doivent être modifiables (is_writable
) puisque leurs soldes en
lamports changent.
Après l'envoi de la transaction, le Programme Système traite l'instruction de transfert. Le Programme Système met ensuite à jour les soldes en lamports des comptes expéditeur et destinataire.
Processus de transfert SOL
Les exemples ci-dessous montrent comment envoyer une transaction qui transfère du SOL d'un compte à un autre.
import {airdropFactory,appendTransactionMessageInstructions,createSolanaRpc,createSolanaRpcSubscriptions,createTransactionMessage,generateKeyPairSigner,getSignatureFromTransaction,lamports,pipe,sendAndConfirmTransactionFactory,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,signTransactionMessageWithSigners} from "@solana/kit";import { getTransferSolInstruction } from "@solana-program/system";// Create a connection to clusterconst rpc = createSolanaRpc("http://localhost:8899");const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");// Generate sender and recipient keypairsconst sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const LAMPORTS_PER_SOL = 1_000_000_000n;const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL// Fund sender with airdropawait airdropFactory({ rpc, rpcSubscriptions })({recipientAddress: sender.address,lamports: lamports(LAMPORTS_PER_SOL), // 1 SOLcommitment: "confirmed"});// Check balance before transferconst { value: preBalance1 } = await rpc.getBalance(sender.address).send();const { value: preBalance2 } = await rpc.getBalance(recipient.address).send();// Create a transfer instruction for transferring SOL from sender to recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount // 0.01 SOL in lamports});// Add the transfer instruction to a new transactionconst { value: latestBlockhash } = await rpc.getLatestBlockhash().send();const transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));// Send the transaction to the networkconst signedTransaction =await signTransactionMessageWithSigners(transactionMessage);await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction,{ commitment: "confirmed" });const transactionSignature = getSignatureFromTransaction(signedTransaction);// Check balance after transferconst { value: postBalance1 } = await rpc.getBalance(sender.address).send();const { value: postBalance2 } = await rpc.getBalance(recipient.address).send();console.log("Sender prebalance:",Number(preBalance1) / Number(LAMPORTS_PER_SOL));console.log("Recipient prebalance:",Number(preBalance2) / Number(LAMPORTS_PER_SOL));console.log("Sender postbalance:",Number(postBalance1) / Number(LAMPORTS_PER_SOL));console.log("Recipient postbalance:",Number(postBalance2) / Number(LAMPORTS_PER_SOL));console.log("Transaction Signature:", transactionSignature);
Les bibliothèques client abstraient souvent les détails de construction des instructions de programme. Si une bibliothèque n'est pas disponible, vous pouvez construire manuellement l'instruction. Cela nécessite de connaître les détails d'implémentation de l'instruction.
Les exemples ci-dessous montrent comment construire manuellement l'instruction
de transfert. L'onglet Expanded Instruction
est fonctionnellement équivalent à
l'onglet Instruction
.
- Kit
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount * LAMPORTS_PER_SOL});
- Legacy
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = SystemProgram.transfer({fromPubkey: sender.publicKey,toPubkey: receiver.publicKey,lamports: transferAmount * LAMPORTS_PER_SOL});
- Rust
let transfer_amount = LAMPORTS_PER_SOL / 100; // 0.01 SOLlet transfer_instruction =system_instruction::transfer(&sender.pubkey(), &recipient.pubkey(), transfer_amount);
Dans les sections suivantes, nous examinerons en détail les transactions et les instructions.
Instructions
Une instruction sur un programme Solana peut être considérée comme une fonction publique qui peut être appelée par n'importe quel utilisateur du réseau Solana.
L'invocation d'une instruction de programme nécessite trois éléments d'information clés :
- ID du programme : Le programme contenant la logique d'exécution pour l'instruction
- Comptes : Liste des comptes dont l'instruction a besoin
- Données d'instruction : Tableau d'octets spécifiant l'instruction à invoquer sur le programme et tous les arguments requis par l'instruction
pub struct Instruction {/// Pubkey of the program that executes this instruction.pub program_id: Pubkey,/// Metadata describing accounts that should be passed to the program.pub accounts: Vec<AccountMeta>,/// Opaque data passed to the program for its own interpretation.pub data: Vec<u8>,}
Instruction de transaction
AccountMeta
Chaque compte requis par une instruction doit être fourni sous forme d'AccountMeta qui contient :
pubkey
: Adresse du compteis_signer
: Indique si le compte doit signer la transactionis_writable
: Indique si l'instruction modifie les données du compte
pub struct AccountMeta {/// An account's public key.pub pubkey: Pubkey,/// True if an `Instruction` requires a `Transaction` signature matching `pubkey`.pub is_signer: bool,/// True if the account data or metadata may be mutated during program execution.pub is_writable: bool,}
AccountMeta
En spécifiant à l'avance quels comptes une instruction lit ou écrit, les transactions qui ne modifient pas les mêmes comptes peuvent s'exécuter en parallèle.
Structure d'instruction exemplaire
Exécutez les exemples ci-dessous pour voir la structure d'une instruction de transfert SOL.
import { generateKeyPairSigner, lamports } from "@solana/kit";import { getTransferSolInstruction } from "@solana-program/system";// Generate sender and recipient keypairsconst sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();// Define the amount to transferconst LAMPORTS_PER_SOL = 1_000_000_000n;const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL// Create a transfer instruction for transferring SOL from sender to recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount});console.log(JSON.stringify(transferInstruction, null, 2));
Les exemples suivants montrent la sortie des extraits de code précédents. Le format exact diffère selon le SDK, mais chaque instruction Solana nécessite les informations suivantes :
- ID du programme : L'adresse du programme qui exécutera l'instruction.
- Comptes : Une liste des comptes requis par l'instruction. Pour chaque compte, l'instruction doit spécifier son adresse, s'il doit signer la transaction, et s'il sera modifié.
- Données : Un tampon d'octets qui indique au programme quelle instruction exécuter et inclut tous les arguments requis par l'instruction.
{"accounts": [{"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC","role": 3,"signer": {"address": "Hu28vRMGWpQXN56eaE7jRiDDRRz3vCXEs7EKHRfL6bC","keyPair": {"privateKey": {},"publicKey": {}}}},{"address": "2mBY6CTgeyJNJDzo6d2Umipw2aGUquUA7hLdFttNEj7p","role": 1}],"programAddress": "11111111111111111111111111111111","data": {"0": 2,"1": 0,"2": 0,"3": 0,"4": 128,"5": 150,"6": 152,"7": 0,"8": 0,"9": 0,"10": 0,"11": 0}}
Transactions
Une transaction Solana se compose de :
- Signatures : Un tableau de signatures incluses dans la transaction.
- Message : Liste d'instructions à traiter de manière atomique.
pub struct Transaction {#[wasm_bindgen(skip)]#[serde(with = "short_vec")]pub signatures: Vec<Signature>,#[wasm_bindgen(skip)]pub message: Message,}
Format de transaction
La structure d'un message de transaction se compose de :
- En-tête de message : Spécifie le nombre de signataires et de comptes en lecture seule.
- Adresses de comptes : Un tableau d'adresses de comptes requis par les instructions de la transaction.
- Blockhash récent : Agit comme un horodatage pour la transaction.
- Instructions : Un tableau d'instructions à exécuter.
pub struct Message {/// The message header, identifying signed and read-only `account_keys`.pub header: MessageHeader,/// All the account keys used by this transaction.#[serde(with = "short_vec")]pub account_keys: Vec<Pubkey>,/// The id of a recent ledger entry.pub recent_blockhash: Hash,/// Programs that will be executed in sequence and committed in/// one atomic transaction if all succeed.#[serde(with = "short_vec")]pub instructions: Vec<CompiledInstruction>,}
Message de transaction
Taille de transaction
Les transactions Solana ont une limite de taille de 1232 octets. Cette limite provient de la taille maximale d'unité de transmission (MTU) IPv6 de 1280 octets, moins 48 octets pour les en-têtes réseau (40 octets IPv6 + 8 octets d'en-tête de fragment).
La taille totale d'une transaction (signatures et message) doit rester sous cette limite et comprend :
- Signatures : 64 octets chacune
- Message : En-tête (3 octets), clés de compte (32 octets chacune), blockhash récent (32 octets), et instructions
Format de transaction
En-tête de message
L'en-tête de message utilise trois octets pour définir les privilèges des comptes.
- Signatures requises
- Nombre de comptes signés en lecture seule
- Nombre de comptes non signés en lecture seule
pub struct MessageHeader {/// The number of signatures required for this message to be considered/// valid. The signers of those signatures must match the first/// `num_required_signatures` of [`Message::account_keys`].pub num_required_signatures: u8,/// The last `num_readonly_signed_accounts` of the signed keys are read-only/// accounts.pub num_readonly_signed_accounts: u8,/// The last `num_readonly_unsigned_accounts` of the unsigned keys are/// read-only accounts.pub num_readonly_unsigned_accounts: u8,}
En-tête de message
Format de tableau compact
Un tableau compact dans un message de transaction est un tableau sérialisé dans le format suivant :
- La longueur du tableau (encodée comme compact-u16)
- Les éléments du tableau listés les uns après les autres
Format de tableau compact
Ce format est utilisé pour encoder les longueurs des tableaux d'adresses de compte et d'instructions dans les messages de transaction.
Tableau d'adresses de compte
Un message de transaction contient un tableau d'adresses de compte requises par ses instructions. Le tableau commence par un nombre compact-u16 indiquant combien d'adresses il contient. Les adresses sont ensuite ordonnées selon leurs privilèges, tels que déterminés par l'en-tête du message.
- Comptes qui sont modifiables et signataires
- Comptes qui sont en lecture seule et signataires
- Comptes qui sont modifiables et non signataires
- Comptes qui sont en lecture seule et non signataires
Tableau compact d'adresses de compte
Blockhash récent
Chaque transaction nécessite un blockhash récent qui sert deux objectifs :
- Agit comme un horodatage
- Empêche les transactions en double
Un blockhash expire après 150 blocs (environ 1 minute en supposant des temps de bloc de 400 ms), après quoi la transaction ne peut plus être traitée.
Vous pouvez utiliser la méthode RPC
getLatestBlockhash
pour obtenir le
blockhash actuel et la dernière hauteur de bloc à laquelle le blockhash sera
valide. Voici un exemple sur
Solana Playground.
Tableau d'instructions
Un message de transaction contient un tableau d'instructions dans le type CompiledInstruction. Les instructions sont converties dans ce type lorsqu'elles sont ajoutées à une transaction.
Comme le tableau d'adresses de compte dans le message, il commence par une longueur compact-u16 suivie par les données d'instruction. Chaque instruction contient :
- Index d'ID de programme : Un index u8 qui pointe vers l'adresse du programme dans le tableau d'adresses de compte. Ceci spécifie le programme qui traitera l'instruction.
- Index de comptes : Un tableau d'index u8 qui pointent vers les adresses de compte requises pour cette instruction.
- Données d'instruction : Un tableau d'octets spécifiant quelle instruction invoquer sur le programme et toutes données supplémentaires requises par l'instruction (ex. arguments de fonction).
pub struct CompiledInstruction {/// Index into the transaction keys array indicating the program account that executes this instruction.pub program_id_index: u8,/// Ordered indices into the transaction keys array indicating which accounts to pass to the program.#[serde(with = "short_vec")]pub accounts: Vec<u8>,/// The program input data.#[serde(with = "short_vec")]pub data: Vec<u8>,}
Tableau compact d'instructions
Exemple de structure de transaction
Exécutez les exemples ci-dessous pour voir la structure d'une transaction avec une seule instruction de transfert de SOL.
import {createSolanaRpc,generateKeyPairSigner,lamports,createTransactionMessage,setTransactionMessageFeePayerSigner,setTransactionMessageLifetimeUsingBlockhash,appendTransactionMessageInstructions,pipe,signTransactionMessageWithSigners,getCompiledTransactionMessageDecoder} from "@solana/kit";import { getTransferSolInstruction } from "@solana-program/system";const rpc = createSolanaRpc("http://localhost:8899");const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();// Generate sender and recipient keypairsconst sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();// Define the amount to transferconst LAMPORTS_PER_SOL = 1_000_000_000n;const transferAmount = lamports(LAMPORTS_PER_SOL / 100n); // 0.01 SOL// Create a transfer instruction for transferring SOL from sender to recipientconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount});// Create transaction messageconst transactionMessage = pipe(createTransactionMessage({ version: 0 }),(tx) => setTransactionMessageFeePayerSigner(sender, tx),(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),(tx) => appendTransactionMessageInstructions([transferInstruction], tx));const signedTransaction =await signTransactionMessageWithSigners(transactionMessage);// Decode the messageBytesconst compiledTransactionMessage =getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);console.log(JSON.stringify(compiledTransactionMessage, null, 2));
Les exemples suivants montrent la sortie du message de transaction des extraits de code précédents. Le format exact diffère selon le SDK, mais inclut les mêmes informations.
{"version": 0,"header": {"numSignerAccounts": 1,"numReadonlySignerAccounts": 0,"numReadonlyNonSignerAccounts": 1},"staticAccounts": ["HoCy8p5xxDDYTYWEbQZasEjVNM5rxvidx8AfyqA4ywBa","5T388jBjovy7d8mQ3emHxMDTbUF8b7nWvAnSiP3EAdFL","11111111111111111111111111111111"],"lifetimeToken": "EGCWPUEXhqHJWYBfDirq3mHZb4qDpATmYqBZMBy9TBC1","instructions": [{"programAddressIndex": 2,"accountIndices": [0, 1],"data": {"0": 2,"1": 0,"2": 0,"3": 0,"4": 128,"5": 150,"6": 152,"7": 0,"8": 0,"9": 0,"10": 0,"11": 0}}]}
Lorsque vous récupérez une transaction en utilisant sa signature après l'avoir envoyée au réseau, vous recevrez une réponse avec la structure suivante.
Le champ message
contient les champs suivants :
-
header
: Spécifie les privilèges de lecture/écriture et de signataire pour les adresses dans le tableauaccountKeys
-
accountKeys
: Tableau de toutes les adresses de compte utilisées dans les instructions de la transaction -
recentBlockhash
: Blockhash utilisé pour horodater la transaction -
instructions
: Tableau d'instructions à exécuter. Chaqueaccount
etprogramIdIndex
dans une instruction référence le tableauaccountKeys
par index. -
signatures
: Tableau incluant les signatures pour tous les comptes requis comme signataires par les instructions de la transaction. Une signature est créée en signant le message de transaction à l'aide de la clé privée correspondante pour un compte.
{"blockTime": 1745196488,"meta": {"computeUnitsConsumed": 150,"err": null,"fee": 5000,"innerInstructions": [],"loadedAddresses": {"readonly": [],"writable": []},"logMessages": ["Program 11111111111111111111111111111111 invoke [1]","Program 11111111111111111111111111111111 success"],"postBalances": [989995000, 10000000, 1],"postTokenBalances": [],"preBalances": [1000000000, 0, 1],"preTokenBalances": [],"rewards": [],"status": {"Ok": null}},"slot": 13049,"transaction": {"message": {"header": {"numReadonlySignedAccounts": 0,"numReadonlyUnsignedAccounts": 1,"numRequiredSignatures": 1},"accountKeys": ["8PLdpLxkuv9Nt8w3XcGXvNa663LXDjSrSNon4EK7QSjQ","7GLg7bqgLBv1HVWXKgWAm6YoPf1LoWnyWGABbgk487Ma","11111111111111111111111111111111"],"recentBlockhash": "7ZCxc2SDhzV2bYgEQqdxTpweYJkpwshVSDtXuY7uPtjf","instructions": [{"accounts": [0, 1],"data": "3Bxs4NN8M2Yn4TLb","programIdIndex": 2,"stackHeight": null}],"indexToProgramIds": {}},"signatures": ["3jUKrQp1UGq5ih6FTDUUt2kkqUfoG2o4kY5T1DoVHK2tXXDLdxJSXzuJGY4JPoRivgbi45U2bc7LZfMa6C4R3szX"]},"version": "legacy"}
Is this page helpful?