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 informacje 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 żadne zmiany nie są wprowadzane.

Transakcja to w zasadzie żądanie przetworzenia jednej lub więcej instrukcji. Można ją porównać do koperty zawierającej formularze. Każdy formularz to instrukcja, która mówi sieci, co ma zrobić. Wysłanie transakcji jest jak wysłanie koperty, aby przetworzyć formularze.

Transakcja uproszczonaTransakcja uproszczona

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 żadne zmiany nie są wprowadzane.
  • 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 lub do których zapisuje
    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. Kod źródłowy instrukcji transferu System Program można znaleźć tutaj.

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.

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.

const transferAmount = 0.01; // 0.01 SOL
const transferInstruction = getTransferSolInstruction({
source: sender,
destination: recipient.address,
amount: transferAmount * LAMPORTS_PER_SOL
});

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

Instrukcje

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

Program Solana można porównać do serwera internetowego hostowanego w sieci Solana, gdzie każda instrukcja jest jak publiczny punkt końcowy API, który użytkownicy mogą wywołać, aby wykonać określone działania. Wywołanie instrukcji jest podobne do wysłania POST żądania do punktu końcowego API, co pozwala użytkownikom na wykonanie logiki biznesowej programu.

Aby wywołać instrukcję programu w Solana, należy skonstruować Instruction z trzema elementami informacji:

  • Program ID: Adres programu zawierającego logikę biznesową dla wywoływanej instrukcji.
  • Accounts: Lista wszystkich kont, z których instrukcja odczytuje dane lub na które zapisuje.
  • Instruction Data: Tablica bajtów określająca, którą instrukcję wywołać 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

Podczas tworzenia Instruction należy dostarczyć każde wymagane konto jako AccountMeta. AccountMeta określa następujące elementy:

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

Określając z góry, które konta instrukcja odczytuje lub zapisuje, transakcje, które nie modyfikują tych samych kont, mogą być wykonywane równolegle.

Aby dowiedzieć się, które konta są wymagane przez instrukcję, w tym które muszą być zapisywalne, tylko do odczytu lub podpisywać transakcję, należy odwołać się do implementacji instrukcji zdefiniowanej przez program.

W praktyce zazwyczaj nie musisz ręcznie tworzyć Instruction. Większość programistów dostarcza biblioteki klienckie z funkcjami pomocniczymi, które tworzą instrukcje za Ciebie.

AccountMetaAccountMeta

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

Poniższe przykłady pokazują wynik z poprzednich 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:

  • Program ID: Adres programu, który wykona instrukcję.
  • Accounts: Lista kont wymaganych przez instrukcję. Dla każdego konta instrukcja musi określić jego adres, czy musi podpisać transakcję oraz czy będzie zapisywane.
  • Data: 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

Po utworzeniu instrukcji, które chcesz wywołać, następnym krokiem jest utworzenie Transaction i dodanie instrukcji do transakcji. Solana transakcja składa się z:

  1. Podpisy: Tablica podpisów ze wszystkich kont wymaganych jako sygnatariusze dla instrukcji w transakcji. Podpis jest tworzony przez podpisanie transakcji Message za pomocą klucza prywatnego konta.
  2. Wiadomość: Transakcja wiadomość zawiera listę 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:

  • Nagłówek wiadomości: Określa liczbę sygnatariuszy i kont tylko do odczytu.
  • Adresy kont: Tablica adresów kont wymaganych przez instrukcje w transakcji.
  • Najnowszy Blockhash: Działa jako znacznik czasu dla transakcji.
  • Instrukcje: Tablica instrukcji do wykonania.
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>,
}

Rozmiar transakcji

Transakcje 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).

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) i instrukcje

Format transakcjiFormat transakcji

Nagłówek wiadomości

Nagłówek wiadomości określa uprawnienia dla konta w transakcji. Działa w połączeniu z ściśle uporządkowanymi adresami kont, aby określić, które konta są sygnatariuszami, a które są zapisywalne.

  1. Liczba podpisów wymaganych dla wszystkich instrukcji w transakcji.
  2. Liczba podpisanych kont, które są tylko do odczytu.
  3. Liczba niepodpisanych kont, które są 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 pojedynczą listę wszystkich adresów kont wymaganych przez jej instrukcje. Tablica zaczyna się od liczby compact-u16, która wskazuje, ile adresów zawiera.

Aby zaoszczędzić miejsce, transakcja nie przechowuje uprawnień dla każdego konta indywidualnie. Zamiast tego opiera się na kombinacji MessageHeader oraz ściśle określonej kolejności adresów kont w celu ustalenia uprawnień.

Adresy są zawsze uporządkowane w następujący sposób:

  1. Konta, które są zapisywalne i podpisujące
  2. Konta, które są tylko do odczytu i podpisujące
  3. Konta, które są zapisywalne i niepodpisujące
  4. Konta, które są tylko do odczytu i niepodpisujące

MessageHeader dostarcza wartości używane do określenia liczby kont dla każdej grupy uprawnień.

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 dla momentu utworzenia transakcji
  2. Zapobiega duplikacji transakcji

Blockhash wygasa po 150 blokach (około 1 minuty, zakładając czas bloku 400 ms), po czym transakcja jest uznawana za wygasłą i 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.

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 w formacie compact-u16, a następnie zawiera dane instrukcji. Każda instrukcja zawiera:

  1. Indeks ID programu: Indeks wskazujący adres programu w tablicy adresów kont. Określa program, który przetwarza instrukcję.
  2. Indeksy kont: Tablica indeksów 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));
Console
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
}
}
]
}

Po przesłaniu transakcji możesz pobrać jej szczegóły za pomocą metody RPC getTransaction. Odpowiedź będzie miała strukturę podobną do poniższego fragmentu. Alternatywnie możesz sprawdzić transakcję za pomocą Solana Explorer.

"Podpis transakcji" jednoznacznie identyfikuje transakcję w Solana. Używasz tego podpisu, aby wyszukać szczegóły transakcji w sieci. Podpis transakcji to po prostu pierwszy podpis w transakcji. Należy zauważyć, że pierwszy podpis to także podpis płatnika opłaty transakcyjnej.

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ę