Транзакції та інструкції
У Solana користувачі надсилають транзакції для взаємодії з мережею. Транзакції містять одну або більше інструкцій, які визначають операції для обробки. Логіка виконання інструкцій зберігається в програмах, розгорнутих у мережі Solana, де кожна програма визначає власний набір інструкцій.
Нижче наведено ключові деталі про обробку транзакцій Solana:
- Якщо транзакція включає кілька інструкцій, вони виконуються в порядку додавання до транзакції.
- Транзакції є "атомарними" - всі інструкції повинні бути оброблені успішно, інакше вся транзакція не виконується і жодних змін не відбувається.
Транзакція - це по суті запит на обробку однієї або кількох інструкцій. Ви можете уявити транзакцію як конверт, що містить форми. Кожна форма - це інструкція, яка вказує мережі, що робити. Надсилання транзакції подібне до відправлення конверта поштою для обробки форм.
Спрощена транзакція
Ключові моменти
- Транзакції Solana включають інструкції, які викликають програми в мережі.
- Транзакції є атомарними - якщо будь-яка інструкція не виконується, вся транзакція не виконується і жодних змін не відбувається.
- Інструкції в транзакції виконуються в послідовному порядку.
- Обмеження розміру транзакції становить 1232 байти.
- Кожна інструкція потребує трьох елементів інформації:
- Адреса програми для виклику
- Облікові записи, з яких інструкція читає або в які записує
- Будь-які додаткові дані, необхідні для інструкції (наприклад, аргументи функції)
Приклад переказу SOL
Діаграма нижче представляє транзакцію з однією інструкцією для переказу SOL від відправника до отримувача.
У Solana "гаманці" — це рахунки, якими володіє System Program. Лише власник програми може змінювати дані рахунку, тому для переказу SOL потрібно надіслати транзакцію для виклику System Program.
Переказ SOL
Рахунок відправника повинен підписати (is_signer
) транзакцію, щоб дозволити
System Program зменшити баланс його lamport. Рахунки відправника та отримувача
повинні бути доступними для запису (is_writable
), оскільки їхні баланси
lamport змінюються.
Після надсилання транзакції System Program обробляє інструкцію переказу. Потім System Program оновлює баланси lamport обох рахунків — відправника та отримувача.
Процес переказу SOL
Приклади нижче показують, як надіслати транзакцію, яка переказує SOL з одного рахунку на інший. Перегляньте вихідний код інструкції переказу System Program тут.
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);
Клієнтські бібліотеки часто абстрагують деталі для створення інструкцій програми. Якщо бібліотека недоступна, ви можете вручну створити інструкцію. Це вимагає знання деталей реалізації інструкції.
Приклади нижче показують, як вручну створити інструкцію переказу. Вкладка
Expanded Instruction
функціонально еквівалентна вкладці Instruction
.
const transferAmount = 0.01; // 0.01 SOLconst transferInstruction = getTransferSolInstruction({source: sender,destination: recipient.address,amount: transferAmount * LAMPORTS_PER_SOL});
У розділах нижче ми розглянемо деталі транзакцій та інструкцій.
Інструкції
Інструкцію в Solana програмі можна розглядати як публічну функцію, яку може викликати будь-хто, використовуючи мережу Solana.
Ви можете уявити програму Solana як веб-сервер, розміщений у мережі Solana, де
кожна інструкція подібна до публічної кінцевої точки API, яку користувачі можуть
викликати для виконання певних дій. Виклик інструкції схожий на надсилання
POST
запиту до кінцевої точки API, що дозволяє користувачам виконувати
бізнес-логіку програми.
Щоб викликати інструкцію програми в Solana, вам потрібно створити Instruction
з трьома елементами інформації:
- ID програми: Адреса програми з бізнес-логікою для інструкції, що викликається.
- Облікові записи: Список усіх облікових записів, з яких інструкція читає або в які записує.
- Instruction Data: Масив байтів, що визначає, яку інструкцію викликати в програмі, та будь-які аргументи, необхідні для інструкції.
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>,}
Інструкція транзакції
AccountMeta
Під час створення Instruction
ви повинні надати кожен необхідний обліковий
запис як
AccountMeta
.
AccountMeta
визначає наступне:
- pubkey: Адреса облікового запису
- is_signer: Чи повинен обліковий запис підписувати транзакцію
- is_writable: Чи змінює інструкція дані облікового запису
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,}
Завдяки попередньому визначенню того, які рахунки інструкція читає або записує, транзакції, які не змінюють одні й ті самі рахунки, можуть виконуватися паралельно.
Щоб знати, які рахунки потрібні для інструкції, включаючи ті, що мають бути доступними для запису, лише для читання або підписувати транзакцію, вам необхідно звернутися до реалізації інструкції, як визначено програмою.
На практиці вам зазвичай не потрібно створювати Instruction
вручну. Більшість
розробників програм надають клієнтські бібліотеки з допоміжними функціями, які
створюють інструкції за вас.
AccountMeta
Приклад структури інструкції
Запустіть наведені нижче приклади, щоб побачити структуру інструкції переказу 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));
Наступні приклади показують вивід з попередніх фрагментів коду. Точний формат відрізняється залежно від SDK, але кожна інструкція Solana вимагає наступної інформації:
- ID програми: Адреса програми, яка виконає інструкцію.
- Рахунки: Список рахунків, необхідних для інструкції. Для кожного рахунку інструкція повинна вказати його адресу, чи повинен він підписати транзакцію, і чи буде в нього здійснюватися запис.
- Дані: Байтовий буфер, який вказує програмі, яку інструкцію виконати, і включає будь-які аргументи, необхідні для інструкції.
{"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}}
Транзакції
Після того, як ви створили інструкції, які хочете викликати, наступним кроком є
створення Transaction
і додавання інструкцій до транзакції. Транзакція
Solana
складається з:
- Підписи: Масив
підписів
від усіх облікових записів, необхідних як підписанти для інструкцій у
транзакції. Підпис створюється шляхом підписання транзакції
Message
за допомогою приватного ключа облікового запису. - Повідомлення: Повідомлення транзакції містить список інструкцій, які будуть оброблені атомарно.
pub struct Transaction {#[wasm_bindgen(skip)]#[serde(with = "short_vec")]pub signatures: Vec<Signature>,#[wasm_bindgen(skip)]pub message: 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>,}
Розмір транзакції
Транзакції Solana мають обмеження розміру 1232 байти. Це обмеження походить від максимального розміру передачі IPv6 (MTU) у 1280 байтів, мінус 48 байтів для мережевих заголовків (40 байтів IPv6 + 8 байтів заголовка).
Загальний розмір транзакції (підписи та повідомлення) повинен залишатися в межах цього обмеження і включає:
- Підписи: 64 байти кожен
- Повідомлення: Заголовок (3 байти), ключі облікових записів (32 байти кожен), останній хеш блоку (32 байти) та інструкції
Формат транзакції
Заголовок повідомлення
Заголовок повідомлення визначає дозволи для облікового запису в транзакції. Він працює у поєднанні з чітко впорядкованими адресами облікових записів для визначення, які облікові записи є підписантами, а які доступні для запису.
- Кількість підписів, необхідних для всіх інструкцій у транзакції.
- Кількість підписаних облікових записів, доступних лише для читання.
- Кількість непідписаних облікових записів, доступних лише для читання.
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,}
Заголовок повідомлення
Формат компактного масиву
Компактний масив у повідомленні транзакції — це масив, серіалізований у такому форматі:
- Довжина масиву (закодована як compact-u16)
- Елементи масиву, перелічені один за одним
Формат компактного масиву
Цей формат використовується для кодування довжин масивів Адрес облікових записів та Інструкцій у повідомленнях транзакцій.
Масив адрес облікових записів
Повідомлення транзакції містить єдиний список усіх адрес облікових записів, необхідних для її інструкцій. Масив починається з compact-u16 числа, що вказує скільки адрес він містить.
Щоб заощадити місце, транзакція не зберігає дозволи для кожного облікового
запису окремо. Натомість вона покладається на комбінацію MessageHeader
та
суворий порядок адрес облікових записів для визначення дозволів.
Адреси завжди впорядковані таким чином:
- Облікові записи, які є записуваними та підписантами
- Облікові записи, які є лише для читання та підписантами
- Облікові записи, які є записуваними та не підписантами
- Облікові записи, які є лише для читання та не підписантами
MessageHeader
надає значення, які використовуються для визначення кількості
облікових записів для кожної групи дозволів.
Компактний масив адрес облікових записів
Останній хеш блоку
Кожна транзакція вимагає останнього хешу блоку, який служить двом цілям:
- Діє як часова мітка створення транзакції
- Запобігає дублюванню транзакцій
Хеш блоку закінчується після 150 блоків (приблизно 1 хвилина, припускаючи час блоку 400 мс), після чого транзакція вважається простроченою і не може бути оброблена.
Ви можете використовувати метод RPC
getLatestBlockhash
для отримання
поточного хешу блоку та останньої висоти блоку, на якій хеш блоку буде дійсним.
Масив інструкцій
Повідомлення транзакції містить масив інструкцій типу CompiledInstruction. Інструкції перетворюються на цей тип під час додавання до транзакції.
Як і масив адрес облікових записів у повідомленні, він починається з compact-u16 довжини, за якою слідують дані інструкції. Кожна інструкція містить:
- Індекс ідентифікатора програми: Індекс, який вказує на адресу програми в масиві адрес облікових записів. Це визначає програму, яка обробляє інструкцію.
- Індекси облікових записів: Масив індексів, які вказують на адреси облікових записів, необхідних для цієї інструкції.
- Instruction Data: Байтовий масив, який визначає, яку інструкцію викликати в програмі, та будь-які додаткові дані, необхідні для інструкції (наприклад, аргументи функції).
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>,}
Компактний масив інструкцій
Приклад структури транзакції
Запустіть наведені нижче приклади, щоб побачити структуру транзакції з однією інструкцією переказу 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));
Наступні приклади показують вивід повідомлення транзакції з попередніх фрагментів коду. Точний формат відрізняється залежно від SDK, але включає ту саму інформацію.
{"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}}]}
Після відправлення транзакції ви можете отримати її деталі за допомогою RPC-методу getTransaction. Відповідь матиме структуру, подібну до наступного фрагмента. Крім того, ви можете переглянути транзакцію за допомогою Solana Explorer.
"Підпис транзакції" унікально ідентифікує транзакцію в Solana. Ви використовуєте цей підпис для пошуку деталей транзакції в мережі. Підпис транзакції — це просто перший підпис у транзакції. Зауважте, що перший підпис також є підписом платника комісії за транзакцію.
{"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?