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 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:
- Adres programu do wywołania
- Konta, z których instrukcja odczytuje dane lub na które zapisuje dane
- 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 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 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 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);
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 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);
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ę
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 transakcji
AccountMeta
Każde konto wymagane przez instrukcję musi być dostarczone jako AccountMeta, które zawiera:
pubkey
: Adres kontais_signer
: Czy konto musi podpisać transakcjęis_writable
: Czy instrukcja modyfikuje dane konta
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
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 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));
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:
- Podpisów: Tablica podpisów dołączonych do transakcji.
- Wiadomości: Lista instrukcji do przetworzenia atomowego.
pub struct Transaction {#[wasm_bindgen(skip)]#[serde(with = "short_vec")]pub signatures: Vec<Signature>,#[wasm_bindgen(skip)]pub message: Message,}
Format transakcji
Struktura wiadomości transakcji składa się z:
- Nagłówka wiadomości: Określa liczbę podpisujących i kont tylko do odczytu.
- Adresów kont: Tablica adresów kont wymaganych przez instrukcje w transakcji.
- Najnowszego Blockhash: Działa jako znacznik czasu dla transakcji.
- Instrukcji: Tablica instrukcji do wykonania.
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ść 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 transakcji
Nagłówek wiadomości
Nagłówek wiadomości używa trzech bajtów do zdefiniowania uprawnień kont.
- Wymagane podpisy
- Liczba podpisanych kont tylko do odczytu
- Liczba niepodpisanych kont tylko do odczytu
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ści
Format tablicy kompaktowej
Kompaktowa tablica w wiadomości transakcji to tablica serializowana w następującym formacie:
- Długość tablicy (zakodowana jako compact-u16)
- Elementy tablicy wymienione jeden po drugim
Format 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 kont
Ostatni Blockhash
Każda transakcja wymaga ostatniego blockhash, który pełni dwie funkcje:
- Działa jako znacznik czasu
- 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:
- Indeks ID programu: Indeks typu u8 wskazujący adres programu w tablicy adresów kont. Określa program, który przetworzy instrukcję.
- Indeksy kont: Tablica indeksów typu u8 wskazujących adresy kont wymagane dla tej instrukcji.
- 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).
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 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 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));
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 tablicyaccountKeys
-
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żdyaccount
iprogramIdIndex
w instrukcji odnosi się do tablicyaccountKeys
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.
{"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?