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éeTransaction 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 :
    1. L'adresse du programme à invoquer
    2. Les comptes que l'instruction lit ou modifie
    3. 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 SOLTransfert 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 SOLProcessus 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 cluster
const rpc = createSolanaRpc("http://localhost:8899");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://localhost:8900");
// Generate sender and recipient keypairs
const 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 airdrop
await airdropFactory({ rpc, rpcSubscriptions })({
recipientAddress: sender.address,
lamports: lamports(LAMPORTS_PER_SOL), // 1 SOL
commitment: "confirmed"
});
// Check balance before transfer
const { 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 recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount // 0.01 SOL in lamports
});
// Add the transfer instruction to a new transaction
const { 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 network
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(
signedTransaction,
{ commitment: "confirmed" }
);
const transactionSignature = getSignatureFromTransaction(signedTransaction);
// Check balance after transfer
const { 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);
Console
Click to execute the code.

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 SOL
const 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.
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 transactionInstruction 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
AccountMeta
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.

AccountMetaAccountMeta

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 keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const 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 recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount
});
console.log(JSON.stringify(transferInstruction, null, 2));
Console
Click to execute the code.

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 :

  1. 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.
  2. Message : Le message de la transaction inclut la liste des instructions à traiter de manière atomique.
Transaction
pub struct Transaction {
#[wasm_bindgen(skip)]
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
#[wasm_bindgen(skip)]
pub message: Message,
}

Format de transactionFormat 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.
Message
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 transactionFormat 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.

  1. Le nombre de signatures requises pour toutes les instructions de la transaction.
  2. Le nombre de comptes signés qui sont en lecture seule.
  3. Le nombre de comptes non signés qui sont en lecture seule.
MessageHeader
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 messageEn-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 :

  1. La longueur du tableau (encodée comme compact-u16)
  2. Les éléments du tableau listés les uns après les autres

Format de tableau compactFormat 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 :

  1. Comptes qui sont modifiables et signataires
  2. Comptes qui sont en lecture seule et signataires
  3. Comptes qui sont modifiables et non signataires
  4. 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 comptesTableau compact d'adresses de comptes

Blockhash récent

Chaque transaction nécessite un blockhash récent qui sert deux objectifs :

  1. Agit comme un horodatage pour indiquer quand la transaction a été créée
  2. 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 :

  1. 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.
  2. Index des comptes : Un tableau d'index qui pointent vers les adresses de comptes requises pour cette instruction.
  3. 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).
CompiledInstruction
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'instructionsTableau 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 keypairs
const sender = await generateKeyPairSigner();
const recipient = await generateKeyPairSigner();
// Define the amount to transfer
const 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 recipient
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount
});
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(sender, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions([transferInstruction], tx)
);
const signedTransaction =
await signTransactionMessageWithSigners(transactionMessage);
// Decode the messageBytes
const compiledTransactionMessage =
getCompiledTransactionMessageDecoder().decode(signedTransaction.messageBytes);
console.log(JSON.stringify(compiledTransactionMessage, null, 2));
Console
Click to execute the code.

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.

Transaction Data
{
"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?

Table des matières

Modifier la page