Transakcje i instrukcje

W Solanie użytkownicy wysyłają transakcje, aby wejść w interakcję z siecią. Transakcje zawierają jedną lub więcej instrukcji, które określają operacje do przetworzenia. Logika wykonania instrukcji jest przechowywana w programach wdrożonych w sieci Solana, gdzie każdy program definiuje własny zestaw instrukcji.

Poniżej znajdują się kluczowe szczegóły dotyczące przetwarzania transakcji w Solanie:

  • Jeśli transakcja zawiera wiele instrukcji, instrukcje są wykonywane w kolejności, w jakiej zostały dodane do transakcji.
  • Transakcje są "atomowe" – wszystkie instrukcje muszą zostać przetworzone pomyślnie, w przeciwnym razie cała transakcja kończy się niepowodzeniem i nie dochodzi do żadnych zmian.

Transakcja to w zasadzie żądanie przetworzenia jednej lub więcej instrukcji.

Uproszczona transakcjaUproszczona transakcja

Transakcja jest jak koperta zawierająca formularze. Każdy formularz to instrukcja, która mówi sieci, co zrobić. Wysłanie transakcji jest jak wysłanie koperty, aby formularze zostały przetworzone.

Kluczowe punkty

  • Transakcje w Solanie zawierają instrukcje, które wywołują programy w sieci.
  • Transakcje są atomowe – jeśli jakakolwiek instrukcja zakończy się niepowodzeniem, cała transakcja kończy się niepowodzeniem i nie dochodzi do żadnych zmian.
  • Instrukcje w transakcji są wykonywane w kolejności sekwencyjnej.
  • Limit rozmiaru transakcji wynosi 1232 bajtów.
  • Każda instrukcja wymaga trzech elementów informacji:
    1. Adres programu do wywołania
    2. Konta, z których instrukcja odczytuje dane lub na które zapisuje dane
    3. Dodatkowe dane wymagane przez instrukcję (np. argumenty funkcji)

Przykład transferu SOL

Poniższy diagram przedstawia transakcję z pojedynczą instrukcją transferu SOL od nadawcy do odbiorcy.

Na Solanie "portfele" to konta zarządzane przez System Program. Tylko właściciel programu może zmieniać dane konta, więc transfer SOL wymaga wysłania transakcji w celu wywołania System Program.

Transfer SOLTransfer SOL

Konto nadawcy musi podpisać (is_signer) transakcję, aby System Program mógł odjąć saldo lamportów. Konta nadawcy i odbiorcy muszą być zapisywalne (is_writable), ponieważ ich salda lamportów ulegają zmianie.

Po wysłaniu transakcji System Program przetwarza instrukcję transferu. Następnie System Program aktualizuje salda lamportów zarówno konta nadawcy, jak i odbiorcy.

Proces transferu SOLProces transferu SOL

Poniższe przykłady pokazują, jak wysłać transakcję, która przenosi SOL z jednego konta na drugie.

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);
Click to execute the code.

Biblioteki klienckie często upraszczają szczegóły budowania instrukcji programów. Jeśli biblioteka nie jest dostępna, możesz ręcznie zbudować instrukcję. Wymaga to znajomości szczegółów implementacji instrukcji.

Poniższe przykłady pokazują, jak ręcznie zbudować instrukcję transferu. Zakładka Expanded Instruction jest funkcjonalnie równoważna zakładce Instruction.

  • Kit
const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount * LAMPORTS_PER_SOL
});
  • Legacy
const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = SystemProgram.transfer({
fromPubkey: sender.publicKey,
toPubkey: receiver.publicKey,
lamports: transferAmount * LAMPORTS_PER_SOL
});
  • Rust
let transfer_amount = LAMPORTS_PER_SOL / 100; // 0.01 SOL
let transfer_instruction =
system_instruction::transfer(&sender.pubkey(), &recipient.pubkey(), transfer_amount);

W poniższych sekcjach omówimy szczegóły transakcji i instrukcji.

Instrukcje

Instrukcja w programie Solana może być traktowana jako funkcja publiczna, którą każdy może wywołać, korzystając z sieci Solana.

Wywołanie instrukcji programu wymaga trzech kluczowych informacji:

  • ID programu: Program zawierający logikę wykonania instrukcji
  • Konta: Lista kont wymaganych przez instrukcję
  • Dane instrukcji: Tablica bajtów określająca instrukcję do wywołania w programie oraz wszelkie argumenty wymagane przez instrukcję
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>,
}

Instrukcja transakcjiInstrukcja transakcji

AccountMeta

Każde konto wymagane przez instrukcję musi być dostarczone jako AccountMeta, które zawiera:

  • pubkey: Adres konta
  • is_signer: Czy konto musi podpisać transakcję
  • is_writable: Czy instrukcja modyfikuje dane konta
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,
}

AccountMetaAccountMeta

Określając z góry, które konta są odczytywane lub zapisywane przez instrukcję, transakcje, które nie modyfikują tych samych kont, mogą być wykonywane równolegle.

Przykładowa struktura instrukcji

Uruchom poniższe przykłady, aby zobaczyć strukturę instrukcji transferu 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));
Click to execute the code.

Poniższe przykłady pokazują wynik wcześniejszych fragmentów kodu. Dokładny format różni się w zależności od SDK, ale każda instrukcja Solana wymaga następujących informacji:

  • ID programu: Adres programu, który wykona instrukcję.
  • Konta: Lista kont wymaganych przez instrukcję. Dla każdego konta instrukcja musi określić jego adres, czy musi podpisać transakcję oraz czy będzie na nie zapisywane.
  • Dane: Bufor bajtów, który informuje program, którą instrukcję wykonać, oraz zawiera wszelkie argumenty wymagane przez instrukcję.
{
"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
}
}

Transakcje

Transakcja w Solana transaction składa się z:

  1. Podpisów: Tablica podpisów dołączonych do transakcji.
  2. Wiadomości: Lista instrukcji do przetworzenia atomowego.
Transaction
pub struct Transaction {
#[wasm_bindgen(skip)]
#[serde(with = "short_vec")]
pub signatures: Vec<Signature>,
#[wasm_bindgen(skip)]
pub message: Message,
}

Format transakcjiFormat transakcji

Struktura wiadomości transakcji składa się z:

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>,
}

Wiadomość transakcjiWiadomość transakcji

Rozmiar transakcji

Transakcje w Solana mają limit rozmiaru wynoszący 1232 bajtów. Limit ten wynika z maksymalnej jednostki transmisji (MTU) IPv6 wynoszącej 1280 bajtów, minus 48 bajtów na nagłówki sieciowe (40 bajtów IPv6 + 8 bajtów nagłówka fragmentu).

Całkowity rozmiar transakcji (podpisy i wiadomość) musi mieścić się w tym limicie i obejmuje:

  • Podpisy: 64 bajty każdy
  • Wiadomość: Nagłówek (3 bajty), klucze kont (32 bajty każdy), najnowszy blockhash (32 bajty) oraz instrukcje

Format transakcjiFormat transakcji

Nagłówek wiadomości

Nagłówek wiadomości używa trzech bajtów do zdefiniowania uprawnień kont.

  1. Wymagane podpisy
  2. Liczba podpisanych kont tylko do odczytu
  3. Liczba niepodpisanych kont tylko do odczytu
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,
}

Nagłówek wiadomościNagłówek wiadomości

Format tablicy kompaktowej

Kompaktowa tablica w wiadomości transakcji to tablica serializowana w następującym formacie:

  1. Długość tablicy (zakodowana jako compact-u16)
  2. Elementy tablicy wymienione jeden po drugim

Format tablicy kompaktowejFormat tablicy kompaktowej

Ten format jest używany do kodowania długości tablic adresów kont oraz instrukcji w wiadomościach transakcji.

Tablica adresów kont

Wiadomość transakcji zawiera tablicę adresów kont wymaganych przez jej instrukcje. Tablica zaczyna się od liczby compact-u16, która wskazuje, ile adresów zawiera. Adresy są następnie uporządkowane według ich uprawnień, jak określono w nagłówku wiadomości.

  • Konta, które są zapisywalne i podpisujące
  • Konta, które są tylko do odczytu i podpisujące
  • Konta, które są zapisywalne i nie podpisujące
  • Konta, które są tylko do odczytu i nie podpisujące

Kompaktowa tablica adresów kontKompaktowa tablica adresów kont

Ostatni Blockhash

Każda transakcja wymaga ostatniego blockhash, który pełni dwie funkcje:

  1. Działa jako znacznik czasu
  2. Zapobiega duplikacji transakcji

Blockhash wygasa po 150 blokach (około 1 minuty, zakładając czas bloku 400 ms), po czym transakcja nie może zostać przetworzona.

Możesz użyć metody RPC getLatestBlockhash, aby uzyskać aktualny blockhash i ostatnią wysokość bloku, przy której blockhash będzie ważny. Oto przykład na Solana Playground.

Tablica instrukcji

Wiadomość transakcji zawiera tablicę instrukcji w typie CompiledInstruction. Instrukcje są konwertowane na ten typ podczas dodawania do transakcji.

Podobnie jak tablica adresów kont w wiadomości, zaczyna się od długości compact-u16, a następnie zawiera dane instrukcji. Każda instrukcja zawiera:

  1. Indeks ID programu: Indeks typu u8 wskazujący adres programu w tablicy adresów kont. Określa program, który przetworzy instrukcję.
  2. Indeksy kont: Tablica indeksów typu u8 wskazujących adresy kont wymagane dla tej instrukcji.
  3. Dane instrukcji: Tablica bajtów określająca, którą instrukcję wywołać w programie oraz wszelkie dodatkowe dane wymagane przez instrukcję (np. argumenty funkcji).
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>,
}

Kompaktowa tablica instrukcjiKompaktowa tablica instrukcji

Przykładowa struktura transakcji

Uruchom poniższe przykłady, aby zobaczyć strukturę transakcji z pojedynczą instrukcją transferu 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));
Click to execute the code.

Poniższe przykłady pokazują wynik wiadomości transakcji z poprzednich fragmentów kodu. Dokładny format różni się w zależności od SDK, ale zawiera te same informacje.

{
"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
}
}
]
}

Kiedy pobierasz transakcję za pomocą jej podpisu po wysłaniu jej do sieci, otrzymasz odpowiedź o następującej strukturze.

Pole message zawiera następujące pola:

  • header: Określa uprawnienia do odczytu/zapisu i podpisywania dla adresów w tablicy accountKeys

  • accountKeys: Tablica wszystkich adresów kont używanych w instrukcjach transakcji

  • recentBlockhash: Blockhash używany do oznaczania czasem transakcji

  • instructions: Tablica instrukcji do wykonania. Każdy account i programIdIndex w instrukcji odnosi się do tablicy accountKeys za pomocą indeksu.

  • signatures: Tablica zawierająca podpisy dla wszystkich kont wymaganych jako podpisujących przez instrukcje w transakcji. Podpis jest tworzony przez podpisanie wiadomości transakcji za pomocą odpowiadającego klucza prywatnego konta.

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?

Spis treści

Edytuj stronę