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 clés concernant le traitement des transactions Solana :
- Si une transaction inclut plusieurs instructions, celles-ci s'exécutent dans l'ordre où 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. Vous pouvez considérer une transaction comme une enveloppe contenant des formulaires. Chaque formulaire est une instruction qui indique au réseau quoi faire. Envoyer la transaction revient à envoyer l'enveloppe par la poste pour faire traiter les formulaires.
Transaction simplifiée
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, toute la transaction échoue et aucun changement n'a lieu.
- Les instructions dans 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
- Toutes données supplémentaires requises 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 des SOL d'un expéditeur à un destinataire.
Sur Solana, les "portefeuilles" sont des comptes détenus par le System Program. Seul le programme propriétaire peut modifier les données d'un compte, donc transférer des SOL nécessite d'envoyer une transaction pour invoquer le System Program.
Transfert de SOL
Le compte expéditeur doit signer (is_signer
) la transaction pour permettre au
System Program 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 System Program traite l'instruction de transfert. Le System Program met ensuite à jour les soldes en lamports des comptes expéditeur et destinataire.
Processus de transfert de SOL
Les exemples ci-dessous montrent comment envoyer une transaction qui transfère des SOL d'un compte à un autre. Consultez le code source de l'instruction de transfert du System Program ici.
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
.
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount * LAMPORTS_PER_SOL});
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 qui utilisant le réseau Solana.
Vous pouvez considérer un programme Solana comme un serveur web hébergé sur le
réseau Solana, où chaque instruction est comme un point d'accès API public que
les utilisateurs peuvent appeler pour effectuer des actions spécifiques.
Invoquer une instruction est similaire à l'envoi d'une requête POST
à un point
d'accès API, permettant aux utilisateurs d'exécuter la logique métier du
programme.
Pour appeler une instruction d'un programme sur Solana, vous devez construire
une Instruction
avec trois éléments d'information :
- ID du programme : L'adresse du programme contenant la logique métier pour l'instruction invoquée.
- Comptes : La liste de tous les comptes que l'instruction lit ou modifie.
- Instruction Data : Un tableau d'octets spécifiant quelle instruction invoquer sur le programme et 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
Lors de la création d'une Instruction
, vous devez fournir chaque compte requis
sous forme d'
AccountMeta
.
L'AccountMeta
spécifie les éléments suivants :
- pubkey : L'adresse du compte
- is_signer : Si le compte doit signer la transaction
- is_writable : 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,}
En spécifiant à l'avance quels comptes une instruction lit ou modifie, les transactions qui ne modifient pas les mêmes comptes peuvent s'exécuter en parallèle.
Pour savoir quels comptes une instruction requiert, y compris ceux qui doivent être modifiables, en lecture seule, ou signer la transaction, vous devez vous référer à l'implémentation de l'instruction telle que définie par le programme.
En pratique, vous n'avez généralement pas besoin de construire une Instruction
manuellement. La plupart des développeurs de programmes fournissent des
bibliothèques client avec des fonctions d'aide qui créent les instructions pour
vous.
AccountMeta
Exemple de structure d'instruction
Exécutez les exemples ci-dessous pour voir la structure d'une instruction de transfert de 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
Après avoir créé les instructions que vous souhaitez invoquer, l'étape suivante
consiste à créer une Transaction
et à ajouter les instructions à la
transaction. Une
transaction
Solana est composée de :
- Signatures : Un tableau de
signatures
de tous les comptes requis comme signataires pour les instructions dans la
transaction. Une signature est créée en signant le
Message
de la transaction avec la clé privée du compte. - Message : Le message de la transaction inclut la liste des 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 du message : Spécifie le nombre de comptes signataires et en lecture seule.
- Adresses des 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>,}
Taille de la 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).
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 du message
L'en-tête du message spécifie les permissions pour le compte dans la transaction. Il fonctionne en combinaison avec les adresses de compte strictement ordonnées pour déterminer quels comptes sont signataires et lesquels sont inscriptibles.
- Le nombre de signatures requises pour toutes les instructions de la transaction.
- Le nombre de comptes signés qui sont en lecture seule.
- Le nombre de comptes non signés qui sont 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 du 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 Adresses des comptes et Instructions dans les messages de transaction.
Tableau d'adresses de comptes
Un message de transaction contient une liste unique de toutes les adresses de comptes requises par ses instructions. Le tableau commence par un nombre compact-u16 indiquant combien d'adresses il contient.
Pour économiser de l'espace, la transaction ne stocke pas les permissions pour
chaque compte individuellement. Au lieu de cela, elle s'appuie sur une
combinaison du MessageHeader
et d'un ordre strict des adresses de comptes pour
déterminer les permissions.
Les adresses sont toujours ordonnées de la manière suivante :
- 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
Le MessageHeader
fournit les valeurs utilisées pour déterminer le nombre de
comptes pour chaque groupe de permissions.
Tableau compact d'adresses de comptes
Blockhash récent
Chaque transaction nécessite un blockhash récent qui sert deux objectifs :
- Agit comme un horodatage pour indiquer quand la transaction a été créée
- Empêche les transactions en double
Un blockhash expire après 150 blocs (environ 1 minute en supposant des temps de bloc de 400ms), après quoi la transaction est considérée comme expirée et ne peut pas ê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.
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 comptes dans le message, il commence par une longueur compact-u16 suivie par les données d'instruction. Chaque instruction contient :
- Index de l'ID du programme : Un index qui pointe vers l'adresse du programme dans le tableau d'adresses de comptes. Cela spécifie le programme qui traite l'instruction.
- Index des comptes : Un tableau d'index qui pointent vers les adresses de comptes requises pour cette instruction.
- Données d'instruction : Un tableau d'octets qui spécifie quelle instruction invoquer sur le programme et toutes données supplémentaires requises par l'instruction (par 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}}]}
Après avoir soumis une transaction, vous pouvez récupérer ses détails en utilisant la méthode RPC getTransaction. La réponse aura une structure similaire à l'extrait suivant. Alternativement, vous pouvez inspecter la transaction en utilisant Solana Explorer.
Une "signature de transaction" identifie de manière unique une transaction sur Solana. Vous utilisez cette signature pour rechercher les détails de la transaction sur le réseau. La signature de transaction est simplement la première signature sur la transaction. Notez que la première signature est également la signature du payeur des frais de transaction.
{"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?