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 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:
- Adres programu do wywołania
- Konta, z których instrukcja odczytuje lub do których zapisuje
- 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. 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 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
.
const transferAmount = 0.01; // 0.01 SOLconst 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ę.
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
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
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.
AccountMeta
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 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:
- 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. - Wiadomość: Transakcja wiadomość zawiera listę 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łó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.
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 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.
- Liczba podpisów wymaganych dla wszystkich instrukcji w transakcji.
- Liczba podpisanych kont, które są tylko do odczytu.
- Liczba niepodpisanych kont, które są 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 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:
- Konta, które są zapisywalne i podpisujące
- Konta, które są tylko do odczytu i podpisujące
- Konta, które są zapisywalne i niepodpisujące
- 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 kont
Ostatni Blockhash
Każda transakcja wymaga ostatniego blockhash, który pełni dwie funkcje:
- Działa jako znacznik czasu dla momentu utworzenia transakcji
- 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:
- Indeks ID programu: Indeks wskazujący adres programu w tablicy adresów kont. Określa program, który przetwarza instrukcję.
- Indeksy kont: Tablica indeksów 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}}]}
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.
{"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?